chore(P05): type & dependency hardening — mypy + recharts 3 (REQ-7-005)
Wave 5-1 mypy adoption (D-058 relaxed-first): mypy 2.3.1, [tool.mypy] check_untyped_defs + no_implicit_optional + warn_unused_ignores + warn_redundant_casts + warn_unreachable, no overrides (every dep ships py.typed), files = ai_service + scripts/sandbox-agent.py + tests (not excluded). 215 errors → 0: ~140 fixed at source incl. genuine bugs found by the gate (LLMProvider protocol async-generator shape, SessionStore protocol sync/async mismatch, structured_reply genericity, get_args migrations, sandbox-agent Optional guard honesty, proctor test arg-order, dead ExecResult kwarg, 16 sqlmodel col() sites); 34 live suppressions scoped-coded + reasoned. Original 27 type:ignore dispositioned: 12 dead removed, 15 scoped (warn_unused_ignores self-eliminates the rest). Wired: scripts/typecheck.sh + package.json + turbo task + root ai:typecheck — pnpm typecheck now 5 tsc + mypy. Wave 5-2 recharts 2.15.4 → 3.10.1 (D-049/D-059): one breaking class (Tooltip formatter ValueType typing); 2 one-line inference fixes on the 2 chart files; no rollback needed (well inside the G-106 timebox). Deprecated direct dep resolved; uuid@9 gone from the tree. Build 248/248 with chart SSR markers verified in exported employer + dashboard pages. Wave 5-3 deprecated-transitive audit (G-109): 4 lockfile entries remain, all dev-only, none actionable within NFR budget — glob@7.2.3 + inflight + rimraf@3 via Storybook docgen chain (flat-cache@3); glob@10.5.0 via c8→test-exclude (D-052-binding coverage tool — swapping it is out of budget). Python: no EOL packages; zero-warning regime under filterwarnings=error is the machine-checked proof. Counts: 670 py / 44 web / 64 cli, zero warnings, G-104 allowlist still exactly 2. ---ci--- phase: 5 milestone: v0.7 status: execute ---/ci---
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"phase": 4,
|
||||
"stage": "complete",
|
||||
"phase": 5,
|
||||
"stage": "verify",
|
||||
"milestone": "v0.7",
|
||||
"milestone_name": "test-harness-review",
|
||||
"phase_role": "execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-09-27T05:30:00Z"
|
||||
"updated_at": "2026-09-27T06:20:00Z"
|
||||
}
|
||||
@@ -6,6 +6,7 @@ The default pipeline: build_messages() → provider.stream_chat()/chat().
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import TypeVar
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -15,6 +16,8 @@ from ..llm.base import LLMProvider
|
||||
from ..llm.types import Message
|
||||
from .structured import structured_completion
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
class BaseAgent(ABC):
|
||||
"""A tutor agent: system prompt + message assembly + provider delegation."""
|
||||
@@ -64,10 +67,15 @@ class BaseAgent(ABC):
|
||||
history: list[Message] | None = None,
|
||||
user_input: str = "",
|
||||
learner_context: LearnerContext | None = None,
|
||||
schema: type[BaseModel] | None = None,
|
||||
schema: type[T] | None = None,
|
||||
schema_hint: str = "",
|
||||
) -> BaseModel:
|
||||
"""Non-streaming completion parsed into a pydantic model (D-020 defense)."""
|
||||
) -> T:
|
||||
"""Non-streaming completion parsed into a pydantic model (D-020 defense).
|
||||
|
||||
P5: generic over `schema` — the returned model is the schema's own
|
||||
type, so callers (assessor/proctor) get concrete types, no casts.
|
||||
`schema=None` raises before T is ever produced.
|
||||
"""
|
||||
if schema is None:
|
||||
raise ValueError("structured_reply requires a schema")
|
||||
messages = self.build_messages(history, user_input, learner_context)
|
||||
|
||||
@@ -28,15 +28,19 @@ class AgentSession:
|
||||
|
||||
|
||||
class SessionStore(Protocol):
|
||||
def get(self, session_id: str) -> AgentSession | None: ...
|
||||
def create(
|
||||
# P5 (D-058): the protocol was previously declared sync while the
|
||||
# shipped implementation is async — every API call site awaited a
|
||||
# non-awaitable per the old signature. The protocol now tells the
|
||||
# truth: all five methods are async (InMemorySessionStore's shape).
|
||||
async def get(self, session_id: str) -> AgentSession | None: ...
|
||||
async def create(
|
||||
self, session_id: str, agent: str, learner_id: str = "seed-learner-1"
|
||||
) -> AgentSession: ...
|
||||
def append(self, session_id: str, message: Message) -> None: ...
|
||||
def history_window(
|
||||
async def append(self, session_id: str, message: Message) -> None: ...
|
||||
async def history_window(
|
||||
self, session_id: str, max_messages: int = DEFAULT_WINDOW
|
||||
) -> list[Message]: ...
|
||||
def delete(self, session_id: str) -> None: ...
|
||||
async def delete(self, session_id: str) -> None: ...
|
||||
|
||||
|
||||
class InMemorySessionStore:
|
||||
|
||||
@@ -54,11 +54,12 @@ detail instead of rubric scores), so consumers read ONE shape.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..agents.assessor import AssessorAgent
|
||||
from ..agents.registry import AgentRegistry
|
||||
from ..agents.structured import StructuredOutputError
|
||||
from ..config import Settings
|
||||
@@ -103,7 +104,9 @@ async def assessment_evaluate(
|
||||
status_code=404,
|
||||
detail=f"no stored grade for {body.learner_id}/{body.task_id} - grade first",
|
||||
)
|
||||
agent = registry.get(provider, settings, "assessor")
|
||||
# P5: registry.get returns BaseAgent; "assessor" is registered as
|
||||
# AssessorAgent (register_builtin_agents) — the cast states that mapping.
|
||||
agent = cast(AssessorAgent, registry.get(provider, settings, "assessor"))
|
||||
learner_context = get_learner_context(body.learner_id)
|
||||
try:
|
||||
coaching = await agent.coach_grade(grade, learner_context)
|
||||
|
||||
@@ -9,11 +9,13 @@ D-016 envelope with agent=lab.
|
||||
|
||||
import json
|
||||
from collections.abc import AsyncIterator
|
||||
from typing import cast
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from ..agents.lab import LabAgent
|
||||
from ..agents.registry import AgentRegistry
|
||||
from ..config import Settings
|
||||
from ..corpus.learner_context import get_learner_context
|
||||
@@ -41,7 +43,9 @@ async def lab_feedback(
|
||||
provider=Depends(get_provider),
|
||||
trace_store=Depends(get_trace_store),
|
||||
) -> EventSourceResponse:
|
||||
agent = registry.get(provider, settings, "lab")
|
||||
# P5: registry.get returns BaseAgent; "lab" is registered as LabAgent
|
||||
# (register_builtin_agents) — the cast states that mapping.
|
||||
agent = cast(LabAgent, registry.get(provider, settings, "lab"))
|
||||
learner_context = get_learner_context(body.learner_id)
|
||||
trace = (
|
||||
trace_store.get_trace(body.learner_id, body.task_id)
|
||||
|
||||
@@ -283,7 +283,11 @@ async def search_postings(
|
||||
SearchEntry(
|
||||
posting=_posting_response(r.posting),
|
||||
match_score=round(r.score, 6) if has_query else None,
|
||||
skill_breakdown=r.skill_breakdown if has_query else [],
|
||||
# P5 scoped: search.skill_breakdown() yields raw dicts in the
|
||||
# exact entry shape (marketplace/ may not import api/ models —
|
||||
# D-048 boundary); pydantic coerces them into
|
||||
# SkillBreakdownEntry at this SearchEntry boundary.
|
||||
skill_breakdown=r.skill_breakdown if has_query else [], # type: ignore[arg-type]
|
||||
)
|
||||
for r in ranked
|
||||
],
|
||||
|
||||
@@ -171,6 +171,8 @@ async def create_posting(
|
||||
description=body.description,
|
||||
required_competencies=body.required_competencies,
|
||||
skills=body.skills,
|
||||
# P5: admin body carries str; Posting's Literal contract — the
|
||||
# store's validates hook rejects out-of-taxonomy values.
|
||||
seniority=body.seniority, # type: ignore[arg-type]
|
||||
category=body.category,
|
||||
location=body.location,
|
||||
|
||||
@@ -5,10 +5,12 @@ variant seed cross-check, no corpus scenarios. The proctor coaches:
|
||||
a pydantic-validated ProctorAssessment (JSON response, not SSE).
|
||||
"""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..agents.proctor import ProctorAssessment
|
||||
from ..agents.proctor import ProctorAgent, ProctorAssessment
|
||||
from ..agents.registry import AgentRegistry
|
||||
from ..config import Settings
|
||||
from ..corpus.learner_context import get_learner_context
|
||||
@@ -41,7 +43,9 @@ async def proctor_signals(
|
||||
voice_store=Depends(get_voice_store),
|
||||
) -> ProctorAssessment:
|
||||
"""Real integrity inputs: live digest + defense signals + variant context."""
|
||||
agent = registry.get(provider, settings, "proctor")
|
||||
# P5: registry.get returns BaseAgent; "proctor" is registered as
|
||||
# ProctorAgent (register_builtin_agents) — the cast states that mapping.
|
||||
agent = cast(ProctorAgent, registry.get(provider, settings, "proctor"))
|
||||
learner_context = get_learner_context(body.learner_id)
|
||||
|
||||
trace = (
|
||||
|
||||
@@ -23,6 +23,7 @@ never part of the API contract.
|
||||
|
||||
import time
|
||||
from collections import deque
|
||||
from collections.abc import Set as AbstractSet
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
||||
@@ -357,7 +358,9 @@ _GENERIC_FIRST_TOKENS = frozenset(
|
||||
|
||||
|
||||
def _enforce_exec_policy(
|
||||
cmd: list[str], environment: str | None, allowed: set[str] | None = None
|
||||
# P5: AbstractSet[str] — the caller unions frozenset + set; membership
|
||||
# is all this function does with it.
|
||||
cmd: list[str], environment: str | None, allowed: AbstractSet[str] | None = None
|
||||
) -> None:
|
||||
"""422 with the allowed set when a design/sim command is out of policy.
|
||||
|
||||
@@ -366,7 +369,8 @@ def _enforce_exec_policy(
|
||||
template must not reject its own Run command)."""
|
||||
if environment not in ("design", "simulation"):
|
||||
return # build kind: unchanged v0.3 semantics
|
||||
allowed = set(allowed) if allowed is not None else set(_GENERIC_FIRST_TOKENS)
|
||||
if allowed is None:
|
||||
allowed = _GENERIC_FIRST_TOKENS
|
||||
first = cmd[0] if cmd else ""
|
||||
if first in ("sh", "bash"):
|
||||
raise HTTPException(
|
||||
|
||||
@@ -37,7 +37,7 @@ from typing import Any, Protocol
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import JSON, Index
|
||||
from sqlalchemy.orm import validates
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine, select
|
||||
from sqlmodel import Field, Session, SQLModel, col, create_engine, select
|
||||
|
||||
from ..config import Settings
|
||||
|
||||
@@ -225,7 +225,9 @@ class SQLiteGradeStore:
|
||||
.where(GradeRecord.learner_id == learner_id)
|
||||
# Chronological; task_id is a deterministic tie-break for
|
||||
# grades stamped within the same instant.
|
||||
.order_by(GradeRecord.created_at, GradeRecord.task_id)
|
||||
# P5: col() — SQLModel class attrs are Mapped[T], the
|
||||
# column reference rides col() in order_by.
|
||||
.order_by(col(GradeRecord.created_at), GradeRecord.task_id)
|
||||
)
|
||||
results = session.exec(stmt).all()
|
||||
for row in results:
|
||||
|
||||
@@ -87,6 +87,8 @@ class MockIdentityProvider:
|
||||
)
|
||||
return IdentityVerdict(
|
||||
status="verified",
|
||||
# P5: derive_age_band returns str; the verdict's Literal contract —
|
||||
# the derived bands are in-contract by construction.
|
||||
age_band=band, # type: ignore[arg-type]
|
||||
provider="mock",
|
||||
mock=True,
|
||||
|
||||
@@ -22,13 +22,14 @@ module imports config only.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Protocol
|
||||
|
||||
from sqlalchemy import event, func, text
|
||||
from sqlalchemy.types import JSON, String
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine, select
|
||||
from sqlmodel import Field, Session, SQLModel, col, create_engine, select
|
||||
|
||||
from ..config import Settings
|
||||
from .base import IdentityStatus
|
||||
@@ -91,14 +92,16 @@ def _as_utc(ts: datetime) -> datetime:
|
||||
return ts
|
||||
|
||||
|
||||
def _sqlite_connect(dbapi_connection: object, _: object) -> None:
|
||||
"""Per-connection pragmas — mirrors the other D-027 stores."""
|
||||
cursor = dbapi_connection.cursor() # type: ignore[attr-defined]
|
||||
def _sqlite_connect(dbapi_connection: sqlite3.Connection, _: object) -> None:
|
||||
"""Per-connection pragmas — mirrors the other D-027 stores (P5: the
|
||||
concrete sqlite3.Connection type removes the two attr-defined ignores
|
||||
the loose `object` annotation needed)."""
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA journal_mode=WAL")
|
||||
cursor.execute("PRAGMA synchronous=NORMAL")
|
||||
cursor.execute("PRAGMA busy_timeout=5000")
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
cursor.close() # type: ignore[attr-defined]
|
||||
cursor.close()
|
||||
|
||||
|
||||
class IdentityStore(Protocol):
|
||||
@@ -172,7 +175,9 @@ class SQLiteIdentityStore:
|
||||
select(IdentityRecord)
|
||||
.where(IdentityRecord.learner_id == learner_id)
|
||||
.order_by(
|
||||
IdentityRecord.submitted_at.desc(),
|
||||
# P5: col() — SQLModel class attrs are Mapped[T], the
|
||||
# column operators (.desc()) ride col().
|
||||
col(IdentityRecord.submitted_at).desc(),
|
||||
text("rowid DESC"),
|
||||
)
|
||||
).first()
|
||||
|
||||
@@ -12,7 +12,11 @@ from .types import Message
|
||||
|
||||
|
||||
class LLMProvider(Protocol):
|
||||
async def stream_chat(
|
||||
# P5 (D-058): plain `def` returning AsyncIterator — the honest shape for
|
||||
# async-generator implementations. `async def -> AsyncIterator` makes mypy
|
||||
# see a coroutine WRAPPING the iterator (every call site would need an
|
||||
# extra await); async generators ARE functions returning AsyncIterator.
|
||||
def stream_chat(
|
||||
self,
|
||||
messages: list[Message],
|
||||
*,
|
||||
|
||||
@@ -326,7 +326,9 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
def _scrub_pii_validation(request, exc): # type: ignore[unused-arg]
|
||||
def _scrub_pii_validation(request, exc):
|
||||
# P5: the old `# type: ignore[unused-arg]` was inert (the unused-arg
|
||||
# code is not enabled by this baseline); removed per D-058.
|
||||
pii_fields = frozenset({"date_of_birth"})
|
||||
scrubbed = []
|
||||
for err in exc.errors():
|
||||
@@ -384,7 +386,9 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
||||
)
|
||||
|
||||
@app.exception_handler(404)
|
||||
async def _spa_404(request, exc): # type: ignore[unused]
|
||||
async def _spa_404(request, exc):
|
||||
# P5: the old `# type: ignore[unused]` was inert (the unused code
|
||||
# is not enabled by this baseline); removed per D-058.
|
||||
not_found_page = web_dir / "404.html"
|
||||
if not_found_page.is_file():
|
||||
return FileResponse(not_found_page, status_code=404)
|
||||
|
||||
@@ -21,6 +21,7 @@ this module imports config + base only.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
import struct
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
@@ -29,7 +30,7 @@ from typing import Any, Protocol
|
||||
|
||||
from sqlalchemy import UniqueConstraint, event, func, text
|
||||
from sqlalchemy.types import JSON, String
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine, select
|
||||
from sqlmodel import Field, Session, SQLModel, col, create_engine, select
|
||||
|
||||
from ..config import Settings
|
||||
from .base import (
|
||||
@@ -122,6 +123,7 @@ class PostingRow(SQLModel, table=True):
|
||||
description=self.description,
|
||||
required_competencies=list(self.required_competencies or []),
|
||||
skills=list(self.skills or []),
|
||||
# P5: row stores str; Posting's Literal[...] contract — validated at write.
|
||||
seniority=self.seniority, # type: ignore[arg-type]
|
||||
category=self.category,
|
||||
location=self.location,
|
||||
@@ -129,6 +131,7 @@ class PostingRow(SQLModel, table=True):
|
||||
salary_min=self.salary_min,
|
||||
salary_max=self.salary_max,
|
||||
status=self.status, # type: ignore[arg-type]
|
||||
# P5: tags is the raw JSON dict on the row; Posting wants the model.
|
||||
tags=self.tags, # type: ignore[arg-type]
|
||||
posted_at=_as_utc(self.posted_at) if self.posted_at else None,
|
||||
)
|
||||
@@ -271,14 +274,16 @@ def _decode_vector(blob: bytes, model: str, dim: int) -> list[float]:
|
||||
return list(struct.unpack(f"<{dim}f", bytes(blob)))
|
||||
|
||||
|
||||
def _sqlite_connect(dbapi_connection: object, _: object) -> None:
|
||||
"""Per-connection pragmas — mirrors the other D-027 stores."""
|
||||
cursor = dbapi_connection.cursor() # type: ignore[attr-defined]
|
||||
def _sqlite_connect(dbapi_connection: sqlite3.Connection, _: object) -> None:
|
||||
"""Per-connection pragmas — mirrors the other D-027 stores (P5: the
|
||||
concrete sqlite3.Connection type removes the two attr-defined ignores
|
||||
the loose `object` annotation needed)."""
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("PRAGMA journal_mode=WAL")
|
||||
cursor.execute("PRAGMA synchronous=NORMAL")
|
||||
cursor.execute("PRAGMA busy_timeout=5000")
|
||||
cursor.execute("PRAGMA foreign_keys=ON")
|
||||
cursor.close() # type: ignore[attr-defined]
|
||||
cursor.close()
|
||||
|
||||
|
||||
class InvalidTransition(ValueError):
|
||||
@@ -571,7 +576,9 @@ class SQLiteMarketplaceStore:
|
||||
session.exec(
|
||||
select(PostingRow)
|
||||
.where(PostingRow.status == "published")
|
||||
.order_by(PostingRow.posted_at.desc(), PostingRow.id)
|
||||
# P5: col() — SQLModel class attrs are Mapped[T], the
|
||||
# column operators (.desc()/.isnot()/.in_()) ride col().
|
||||
.order_by(col(PostingRow.posted_at).desc(), PostingRow.id)
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
)
|
||||
@@ -760,7 +767,7 @@ class SQLiteMarketplaceStore:
|
||||
params,
|
||||
).fetchall()
|
||||
]
|
||||
stmt = stmt.where(PostingRow.id.in_(skill_ids or ["__none__"]))
|
||||
stmt = stmt.where(col(PostingRow.id).in_(skill_ids or ["__none__"]))
|
||||
if seniority is not None:
|
||||
stmt = stmt.where(PostingRow.seniority == seniority)
|
||||
if category is not None:
|
||||
@@ -773,16 +780,16 @@ class SQLiteMarketplaceStore:
|
||||
# a posting qualifies when its advertised band overlaps
|
||||
# the requested band: posting.salary_max >= salary_min.
|
||||
stmt = stmt.where(
|
||||
PostingRow.salary_max.isnot(None),
|
||||
PostingRow.salary_max >= salary_min,
|
||||
col(PostingRow.salary_max).isnot(None),
|
||||
col(PostingRow.salary_max) >= salary_min,
|
||||
)
|
||||
if salary_max is not None:
|
||||
stmt = stmt.where(
|
||||
PostingRow.salary_min.isnot(None),
|
||||
PostingRow.salary_min <= salary_max,
|
||||
col(PostingRow.salary_min).isnot(None),
|
||||
col(PostingRow.salary_min) <= salary_max,
|
||||
)
|
||||
rows = session.exec(
|
||||
stmt.order_by(PostingRow.posted_at.desc(), PostingRow.id)
|
||||
stmt.order_by(col(PostingRow.posted_at).desc(), PostingRow.id)
|
||||
).all()
|
||||
return [r.to_posting() for r in rows]
|
||||
|
||||
@@ -880,7 +887,9 @@ class SQLiteMarketplaceStore:
|
||||
stmt = select(ApplicationRow)
|
||||
if posting_id is not None:
|
||||
stmt = stmt.where(ApplicationRow.posting_id == posting_id)
|
||||
rows = session.exec(stmt.order_by(ApplicationRow.created_at.desc())).all()
|
||||
rows = session.exec(
|
||||
stmt.order_by(col(ApplicationRow.created_at).desc())
|
||||
).all()
|
||||
return [r.to_application() for r in rows]
|
||||
|
||||
# -- P4-A: enrichment (append-only, D-053) --------------------------------
|
||||
@@ -915,7 +924,7 @@ class SQLiteMarketplaceStore:
|
||||
rows = session.exec(
|
||||
select(EmployerEnrichmentRow)
|
||||
.where(EmployerEnrichmentRow.employer_id == employer_id)
|
||||
.order_by(EmployerEnrichmentRow.created_at.desc(), EmployerEnrichmentRow.id)
|
||||
.order_by(col(EmployerEnrichmentRow.created_at).desc(), EmployerEnrichmentRow.id)
|
||||
).all()
|
||||
return [
|
||||
{
|
||||
@@ -949,7 +958,7 @@ class SQLiteMarketplaceStore:
|
||||
PostingRow.employer_id == employer_id,
|
||||
PostingRow.status == "published",
|
||||
)
|
||||
.order_by(PostingRow.posted_at.desc())
|
||||
.order_by(col(PostingRow.posted_at).desc())
|
||||
).all()
|
||||
return [r.title for r in rows]
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ BOUNDARY: this module NEVER imports `api/` or `agents/`.
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import builtins
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -281,7 +282,7 @@ class SandboxManager:
|
||||
|
||||
# -- reapers ------------------------------------------------------------
|
||||
|
||||
async def reap_expired(self) -> list[str]:
|
||||
async def reap_expired(self) -> builtins.list[str]:
|
||||
"""One reaper pass: wall-clock timeout + G-2 workdir-size sweep.
|
||||
|
||||
Destroys sandboxes older than `settings.sandbox_timeout_s`, then sweeps
|
||||
@@ -290,9 +291,14 @@ class SandboxManager:
|
||||
signal). Returns the ids destroyed this pass. Invoke on an async timer
|
||||
(the app lifespan owns the loop interval); both checks deliberately
|
||||
share one pass so the periodic work is O(live sandboxes) once.
|
||||
|
||||
P5: the return annotation is `builtins.list` because this class's
|
||||
own `list()` method shadows the `list[...]` subscript form in
|
||||
annotations below it (a mypy scoping quirk — the method name is
|
||||
public API and stays).
|
||||
"""
|
||||
now = self._clock()
|
||||
destroyed: list[str] = []
|
||||
destroyed: builtins.list[str] = []
|
||||
timeout_s = float(self._settings.sandbox_timeout_s)
|
||||
cap_bytes = int(self._settings.sandbox_max_workdir_mb) * 1024 * 1024
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Boundary (D-027): telemetry/ never imports agents/ or api/.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Literal, get_args
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from sqlalchemy import JSON, Index, String
|
||||
@@ -38,7 +38,9 @@ EventKind = Literal[
|
||||
"stdin",
|
||||
"stdout",
|
||||
]
|
||||
_EVENT_KINDS: frozenset[str] = frozenset(EventKind.__args__)
|
||||
# P5: get_args() is the typed accessor for Literal values (bare __args__
|
||||
# trips mypy on the typing special form).
|
||||
_EVENT_KINDS: frozenset[str] = frozenset(get_args(EventKind))
|
||||
|
||||
|
||||
class TelemetryEvent(SQLModel, table=True):
|
||||
|
||||
@@ -28,7 +28,7 @@ from pathlib import Path
|
||||
from typing import Any, Protocol
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlmodel import Session, SQLModel, create_engine, select
|
||||
from sqlmodel import Session, SQLModel, col, create_engine, select
|
||||
|
||||
from ..config import Settings
|
||||
from .models import TelemetryEvent
|
||||
@@ -151,7 +151,9 @@ class SQLiteTraceStore:
|
||||
select(TelemetryEvent)
|
||||
.where(TelemetryEvent.learner_id == learner_id)
|
||||
.where(TelemetryEvent.task_id == task_id)
|
||||
.order_by(TelemetryEvent.seq)
|
||||
# P5: col() — SQLModel class attrs are Mapped[T], the
|
||||
# column reference rides col() in order_by/func.count.
|
||||
.order_by(col(TelemetryEvent.seq))
|
||||
)
|
||||
results = session.exec(stmt).all()
|
||||
# Detach from the session: callers must not depend on open-session
|
||||
@@ -167,7 +169,7 @@ class SQLiteTraceStore:
|
||||
select(TelemetryEvent.seq)
|
||||
.where(TelemetryEvent.learner_id == learner_id)
|
||||
.where(TelemetryEvent.task_id == task_id)
|
||||
.order_by(TelemetryEvent.seq)
|
||||
.order_by(col(TelemetryEvent.seq))
|
||||
)
|
||||
# sqlmodel scalar select: rows are plain ints, not 1-tuples.
|
||||
return [int(seq) for seq in session.exec(stmt).all()]
|
||||
@@ -196,7 +198,7 @@ class SQLiteTraceStore:
|
||||
# docstring-free body keeps it obvious what the query shape is.
|
||||
with self._session() as session:
|
||||
stmt = (
|
||||
select(sa.func.count(TelemetryEvent.seq))
|
||||
select(sa.func.count(col(TelemetryEvent.seq)))
|
||||
.where(TelemetryEvent.learner_id == learner_id)
|
||||
.where(TelemetryEvent.task_id == task_id)
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ from typing import Any, Protocol
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import JSON, Index, String, UniqueConstraint
|
||||
from sqlalchemy.orm import validates
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine, select
|
||||
from sqlmodel import Field, Session, SQLModel, col, create_engine, select
|
||||
|
||||
from ..config import Settings
|
||||
|
||||
@@ -325,7 +325,9 @@ class SQLiteVariantStore:
|
||||
.where(VariantRecord.learner_id == learner_id)
|
||||
# Chronological; task_id is a deterministic tie-break for
|
||||
# variants stamped within the same instant.
|
||||
.order_by(VariantRecord.created_at, VariantRecord.task_id)
|
||||
# P5: col() — SQLModel class attrs are Mapped[T], the
|
||||
# column reference rides col() in order_by.
|
||||
.order_by(col(VariantRecord.created_at), VariantRecord.task_id)
|
||||
)
|
||||
results = session.exec(stmt).all()
|
||||
for row in results:
|
||||
@@ -339,7 +341,7 @@ class SQLiteVariantStore:
|
||||
select(VariantRecord)
|
||||
.where(VariantRecord.template_id == template_id)
|
||||
# Chronological; learner_id is a deterministic tie-break.
|
||||
.order_by(VariantRecord.created_at, VariantRecord.learner_id)
|
||||
.order_by(col(VariantRecord.created_at), VariantRecord.learner_id)
|
||||
)
|
||||
results = session.exec(stmt).all()
|
||||
for row in results:
|
||||
|
||||
@@ -63,22 +63,24 @@ from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal, Protocol
|
||||
from typing import Any, Literal, Protocol, get_args
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import JSON, Index, String
|
||||
from sqlalchemy.orm import validates
|
||||
from sqlmodel import Field, Session, SQLModel, create_engine, select
|
||||
from sqlmodel import Field, Session, SQLModel, col, create_engine, select
|
||||
|
||||
from ..config import Settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DefenseStatus = Literal["in_progress", "finished"]
|
||||
_DEFENSE_STATUSES: frozenset[str] = frozenset(DefenseStatus.__args__)
|
||||
# P5: get_args() is the typed accessor for Literal values (bare __args__
|
||||
# trips mypy on the typing special form).
|
||||
_DEFENSE_STATUSES: frozenset[str] = frozenset(get_args(DefenseStatus))
|
||||
|
||||
TurnRole = Literal["examiner", "learner"]
|
||||
_TURN_ROLES: frozenset[str] = frozenset(TurnRole.__args__)
|
||||
_TURN_ROLES: frozenset[str] = frozenset(get_args(TurnRole))
|
||||
|
||||
|
||||
class DefenseRecord(SQLModel, table=True):
|
||||
@@ -464,7 +466,9 @@ class SQLiteDefenseStore:
|
||||
stmt = (
|
||||
select(DefenseTurn)
|
||||
.where(DefenseTurn.defense_id == defense_id)
|
||||
.order_by(DefenseTurn.seq)
|
||||
# P5: col() — SQLModel class attrs are Mapped[T], the
|
||||
# column reference rides col() in order_by.
|
||||
.order_by(col(DefenseTurn.seq))
|
||||
)
|
||||
turns = session.exec(stmt).all()
|
||||
for turn in turns:
|
||||
@@ -489,7 +493,7 @@ class SQLiteDefenseStore:
|
||||
.where(DefenseRecord.learner_id == learner_id)
|
||||
# Chronological; id is a deterministic tie-break for
|
||||
# defenses stamped within the same instant.
|
||||
.order_by(DefenseRecord.created_at, DefenseRecord.id)
|
||||
.order_by(col(DefenseRecord.created_at), DefenseRecord.id)
|
||||
)
|
||||
results = session.exec(stmt).all()
|
||||
for row in results:
|
||||
|
||||
@@ -11,12 +11,14 @@ descriptor then honestly reports mode='mock' so the UI badge cannot lie.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
import httpx
|
||||
|
||||
from ..config import Settings
|
||||
from .base import VoiceProvider
|
||||
from .mock import MockVoiceProvider
|
||||
from .openai_audio import OpenAIAudioProvider
|
||||
from .openai_audio import OpenAIAudioProvider, TtsFormat
|
||||
|
||||
|
||||
class UnknownVoiceProviderError(ValueError):
|
||||
@@ -60,7 +62,10 @@ def voice_provider_from_settings(
|
||||
stt_model=settings.voice_stt_model,
|
||||
tts_model=settings.voice_tts_model,
|
||||
tts_voice=settings.voice_tts_voice,
|
||||
tts_format=settings.voice_tts_format,
|
||||
# P5: Settings.voice_tts_format is str, but its pydantic
|
||||
# validator normalizes to exactly mp3|wav|opus (invalid → mp3
|
||||
# with a warning) — the cast states that provenance.
|
||||
tts_format=cast(TtsFormat, settings.voice_tts_format),
|
||||
)
|
||||
raise UnknownVoiceProviderError(
|
||||
f"unknown AI_VOICE_PROVIDER {name!r}: use 'mock', 'browser', or 'openai-audio'"
|
||||
|
||||
@@ -19,6 +19,10 @@ import httpx
|
||||
|
||||
from .base import TranscriptSegment, VoiceDescriptor
|
||||
|
||||
#: TTS response formats the OpenAI audio endpoint accepts (P5: named so
|
||||
#: the factory can type-honestly pass the config-normalized value through).
|
||||
TtsFormat = Literal["mp3", "wav", "opus"]
|
||||
|
||||
|
||||
class OpenAIAudioProvider:
|
||||
"""Server STT (`/audio/transcriptions`) + TTS (`/audio/speech`)."""
|
||||
@@ -31,7 +35,7 @@ class OpenAIAudioProvider:
|
||||
stt_model: str = "whisper-1",
|
||||
tts_model: str = "tts-1",
|
||||
tts_voice: str = "alloy",
|
||||
tts_format: Literal["mp3", "wav", "opus"] = "mp3",
|
||||
tts_format: TtsFormat = "mp3",
|
||||
) -> None:
|
||||
self._client = http_client
|
||||
self._base_url = base_url.rstrip("/")
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"dev": "bash scripts/dev.sh",
|
||||
"test": "bash scripts/test.sh",
|
||||
"bootstrap": "bash scripts/bootstrap.sh",
|
||||
"lint": "bash scripts/lint.sh"
|
||||
"lint": "bash scripts/lint.sh",
|
||||
"typecheck": "bash scripts/typecheck.sh"
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,8 @@ dev = [
|
||||
"pytest-cov>=7.1,<8",
|
||||
"pytest-timeout>=2.4,<3",
|
||||
"ruff>=0.14",
|
||||
# P5 type hardening (D-058): mypy relaxed-first baseline. Resolved 2.3.1.
|
||||
"mypy>=2.3,<2.4",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
@@ -106,4 +108,39 @@ target-version = "py311"
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "W", "I", "UP", "B", "RUF100"]
|
||||
# B008: Depends() in argument defaults is the idiomatic FastAPI DI pattern
|
||||
ignore = ["B008"]
|
||||
ignore = ["B008"]
|
||||
|
||||
# P5 mypy relaxed-first baseline (D-058) — NOT full strict. The flags
|
||||
# below are the binding set from the decision; per-module tightening via
|
||||
# [[tool.mypy.overrides]] is available later (the plan's Wave-5 follow-up
|
||||
# form). Scope decision (recorded per the task's pragmatic clause): ALL
|
||||
# THREE trees pass — ai_service/ + scripts/sandbox-agent.py + tests/ —
|
||||
# so tests are NOT excluded; every test violation was either fixed at
|
||||
# source or scoped with a coded ignore + reason inline. Tests remain
|
||||
# fully checked; a future tests.* per-module relaxation override stays
|
||||
# available (D-058) if the annotation burden grows.
|
||||
[tool.mypy]
|
||||
python_version = "3.11"
|
||||
files = [
|
||||
"ai_service",
|
||||
"scripts/sandbox-agent.py",
|
||||
"tests",
|
||||
]
|
||||
# Untracked runtime droppings under tests/ (gitignored via
|
||||
# `apps/ai-service/**/sandboxes/`) must never be type-checked.
|
||||
exclude = ["tests/sandboxes"]
|
||||
# D-058 relaxed-first baseline:
|
||||
check_untyped_defs = true # real bugs hide in unannotated bodies too
|
||||
no_implicit_optional = true # implicit Optional is a genuine bug class
|
||||
warn_unused_ignores = true # stale `# type: ignore` becomes an error (self-eliminating suppressions)
|
||||
warn_redundant_casts = true
|
||||
warn_unreachable = true
|
||||
warn_unused_configs = true
|
||||
# Start strict on imports: every runtime dep checked code imports ships
|
||||
# stubs (verified py.typed: fastapi/uvicorn/pydantic/pydantic-settings/
|
||||
# httpx/sse-starlette/sqlmodel/sqlalchemy/websockets/python-multipart/
|
||||
# pypdf/pytest/pytest-asyncio; aiofiles + pytest-timeout/pytest-cov are
|
||||
# never imported by checked code). Overrides are reserved for deps that
|
||||
# genuinely lack stubs — currently NONE, so no ignore_missing_imports
|
||||
# overrides are listed; warn_unused_configs would flag any added no-op.
|
||||
ignore_missing_imports = false
|
||||
@@ -565,6 +565,10 @@ class Agent:
|
||||
"""Maintain the WS connection: connect, flush backlog, read, backoff."""
|
||||
backoff = self.config.backoff_base_s
|
||||
while not self._stop.is_set():
|
||||
# P5: explicit Optional — the else-branch read can observe None
|
||||
# (a concurrent _drop_conn between the check and the read), so
|
||||
# the `conn is None` guard below is genuinely reachable.
|
||||
conn: WsConnection | None
|
||||
if self._current_conn() is None:
|
||||
try:
|
||||
conn = WsConnection.connect(self.config.ingest_url)
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Type check: mypy via venv — the D-058 relaxed-first baseline
|
||||
# (check_untyped_defs, no_implicit_optional, warn_unused_ignores,
|
||||
# warn_redundant_casts, warn_unreachable; config in pyproject.toml
|
||||
# [tool.mypy], scope = ai_service + scripts/sandbox-agent.py + tests).
|
||||
#
|
||||
# Extra args pass through: `bash scripts/typecheck.sh --verbose`.
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
APP_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
VENV="$APP_DIR/.venv"
|
||||
|
||||
if [ ! -x "$VENV/bin/mypy" ]; then
|
||||
echo "venv missing mypy — run scripts/bootstrap.sh (or pip install the dev extras) first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$APP_DIR"
|
||||
exec "$VENV/bin/mypy" "$@"
|
||||
@@ -38,16 +38,22 @@ def test_lab_scenarios_addressable_by_id():
|
||||
def test_lab_scenarios_have_distinct_event_profiles():
|
||||
strong = get_lab_scenario("lab-scenario-strong")
|
||||
struggling = get_lab_scenario("lab-scenario-struggling")
|
||||
flagged = get_lab_scenario("lab-scenario-flagged")
|
||||
assert strong is not None, "seed scenario must resolve"
|
||||
assert struggling is not None, "seed scenario must resolve"
|
||||
assert flagged is not None, "seed scenario must resolve"
|
||||
def kinds(s):
|
||||
return {e.kind for e in s.events}
|
||||
assert "test_pass" in kinds(strong)
|
||||
assert "test_fail" in kinds(struggling)
|
||||
assert "idle" in kinds(struggling)
|
||||
assert "paste" in kinds(get_lab_scenario("lab-scenario-flagged"))
|
||||
assert "paste" in kinds(flagged)
|
||||
|
||||
|
||||
def test_summarize_scenario_mentions_events():
|
||||
text = summarize_scenario(get_lab_scenario("lab-scenario-struggling"))
|
||||
scenario = get_lab_scenario("lab-scenario-struggling")
|
||||
assert scenario is not None, "seed scenario must resolve"
|
||||
text = summarize_scenario(scenario)
|
||||
assert "test_fail" in text
|
||||
assert "ImportError" in text
|
||||
assert "stack-orchestration-c002" in text
|
||||
@@ -80,6 +86,7 @@ def test_transcripts_pair_with_artifacts():
|
||||
|
||||
def test_rubric_render_mentions_all_criteria():
|
||||
rubric = rubric_for_competency("stack-orchestration-c002")
|
||||
assert rubric is not None, "seed rubric must resolve"
|
||||
text = render_rubric(rubric)
|
||||
for criterion in rubric.criteria:
|
||||
assert criterion.criterion_id in text
|
||||
@@ -87,6 +94,7 @@ def test_rubric_render_mentions_all_criteria():
|
||||
|
||||
def test_transcript_render_has_both_speakers():
|
||||
transcript = get_transcript_for_artifact("art-eval-rag-dashboard")
|
||||
assert transcript is not None, "seed transcript must resolve"
|
||||
text = render_transcript(transcript)
|
||||
assert "examiner:" in text
|
||||
assert "learner:" in text
|
||||
|
||||
@@ -25,9 +25,13 @@ async def test_stream_feedback_mentions_scenario_events():
|
||||
ctx = get_learner_context()
|
||||
strong = get_lab_scenario("lab-scenario-strong")
|
||||
struggling = get_lab_scenario("lab-scenario-struggling")
|
||||
assert strong is not None, "seed scenario must resolve"
|
||||
assert struggling is not None, "seed scenario must resolve"
|
||||
|
||||
strong_reply = "".join([t async for t in lab.stream_feedback(strong, ctx)])
|
||||
struggling_reply = "".join([t async for t in lab.stream_feedback(struggling, ctx)])
|
||||
# P5 scoped: corpus scenario stands in for the TraceDigest (dormant-
|
||||
# corpus test seam — render_digest_timeline duck-types model_dump_json).
|
||||
strong_reply = "".join([t async for t in lab.stream_feedback(strong, ctx)]) # type: ignore[arg-type]
|
||||
struggling_reply = "".join([t async for t in lab.stream_feedback(struggling, ctx)]) # type: ignore[arg-type]
|
||||
assert strong_reply
|
||||
assert strong_reply != struggling_reply # scenario-driven, not canned
|
||||
|
||||
@@ -35,6 +39,7 @@ async def test_stream_feedback_mentions_scenario_events():
|
||||
def test_build_evaluation_messages_carry_timeline():
|
||||
lab = make_lab()
|
||||
scenario = get_lab_scenario("lab-scenario-flagged")
|
||||
assert scenario is not None, "seed scenario must resolve"
|
||||
timeline = summarize_scenario(scenario)
|
||||
messages = lab.build_messages(None, timeline, get_learner_context())
|
||||
assert messages[0].role == "system"
|
||||
|
||||
@@ -49,7 +49,13 @@ async def test_assess_returns_validated_signals():
|
||||
provider = ScriptedJSONProvider(VALID_ASSESSMENT)
|
||||
proctor = make_proctor(provider)
|
||||
scenario = get_proctor_scenario("proctor-scenario-distracted")
|
||||
result = await proctor.assess(scenario, get_learner_context())
|
||||
assert scenario is not None, "seed scenario must resolve"
|
||||
# P5 scoped: corpus scenario stands in for the TraceDigest (dormant-
|
||||
# corpus test seam — the agent renders digest.model_dump_json).
|
||||
result = await proctor.assess(
|
||||
scenario, # type: ignore[arg-type]
|
||||
learner_context=get_learner_context(),
|
||||
)
|
||||
assert isinstance(result, ProctorAssessment)
|
||||
assert len(result.signals) == 2
|
||||
assert result.signals[0].severity == "low"
|
||||
@@ -59,8 +65,13 @@ async def test_assess_returns_validated_signals():
|
||||
async def test_assess_rejects_invalid_after_retry():
|
||||
proctor = make_proctor(MockProvider()) # non-schema JSON
|
||||
scenario = get_proctor_scenario("proctor-scenario-healthy")
|
||||
assert scenario is not None, "seed scenario must resolve"
|
||||
with pytest.raises(StructuredOutputError):
|
||||
await proctor.assess(scenario, get_learner_context())
|
||||
# P5 scoped: corpus scenario as digest — same dormant-corpus seam.
|
||||
await proctor.assess(
|
||||
scenario, # type: ignore[arg-type]
|
||||
learner_context=get_learner_context(),
|
||||
)
|
||||
|
||||
|
||||
def test_system_prompt_is_coaching_not_punitive():
|
||||
@@ -72,6 +83,7 @@ def test_system_prompt_is_coaching_not_punitive():
|
||||
|
||||
def test_timeline_summary_carries_events():
|
||||
scenario = get_proctor_scenario("proctor-scenario-flagged")
|
||||
assert scenario is not None, "seed scenario must resolve"
|
||||
text = summarize_proctor_scenario(scenario)
|
||||
assert "paste_large" in text
|
||||
assert "3,100 chars" in text
|
||||
|
||||
@@ -14,7 +14,9 @@ async def test_create_and_get():
|
||||
store = InMemorySessionStore()
|
||||
session = await store.create("s1", agent="coach")
|
||||
assert session.agent == "coach"
|
||||
assert (await store.get("s1")).session_id == "s1"
|
||||
got = await store.get("s1")
|
||||
assert got is not None
|
||||
assert got.session_id == "s1"
|
||||
assert await store.get("missing") is None
|
||||
|
||||
|
||||
|
||||
@@ -69,7 +69,8 @@ async def test_structured_completion_retries_then_raises():
|
||||
# Plain MockProvider returns non-schema JSON for json_object requests →
|
||||
# both attempts fail validation → StructuredOutputError after ONE retry.
|
||||
provider = MockProvider()
|
||||
provider.received_calls = []
|
||||
# P5 scoped: test-only call ledger attribute on the shared mock.
|
||||
provider.received_calls = [] # type: ignore[attr-defined]
|
||||
messages = [Message(role="user", content="grade me")]
|
||||
with pytest.raises(StructuredOutputError):
|
||||
await structured_completion(
|
||||
@@ -141,7 +142,7 @@ async def test_structured_completion_sends_schema_instruction():
|
||||
messages, model=model, temperature=temperature, response_format=response_format
|
||||
)
|
||||
|
||||
provider.chat = recording_chat
|
||||
provider.chat = recording_chat # type: ignore[method-assign] # P5: monkeypatching the shared mock
|
||||
messages = [Message(role="user", content="grade")]
|
||||
await structured_completion(provider, messages, model="m", schema=Score, schema_hint=HINT)
|
||||
assert captured, "provider was never called"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
@@ -33,7 +34,7 @@ class CoachingMock(MockProvider):
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(tmp_path) -> TestClient:
|
||||
def client(tmp_path) -> Iterator[TestClient]:
|
||||
app = create_app(Settings(provider="mock"))
|
||||
app.state.provider = CoachingMock()
|
||||
app.state.trace_store = SQLiteTraceStore(db_path=tmp_path / "t.db")
|
||||
@@ -45,8 +46,10 @@ def client(tmp_path) -> TestClient:
|
||||
|
||||
def _seed_grade(client: TestClient) -> None:
|
||||
|
||||
# P5: TestClient.app is typed ASGIApp (Callable); at runtime it is the
|
||||
# FastAPI instance — this state-injection seam is the suite's pattern.
|
||||
app = client.app
|
||||
store: SQLiteGradeStore = app.state.grade_store
|
||||
store: SQLiteGradeStore = app.state.grade_store # type: ignore[attr-defined]
|
||||
store.save(
|
||||
GradeRecord(
|
||||
learner_id="api-learner",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -77,7 +78,7 @@ def app(tmp_path: Path):
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(app) -> TestClient:
|
||||
def client(app) -> Iterator[TestClient]:
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ scenarios; empty trace is a valid "no telemetry yet" coaching path.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
@@ -44,7 +45,7 @@ class StreamingMock(MockProvider):
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(tmp_path: Path) -> TestClient:
|
||||
def client(tmp_path: Path) -> Iterator[TestClient]:
|
||||
app = create_app(Settings(provider="mock"))
|
||||
app.state.provider = StreamingMock()
|
||||
app.state.trace_store = SQLiteTraceStore(db_path=tmp_path / "t.db")
|
||||
@@ -55,7 +56,7 @@ def client(tmp_path: Path) -> TestClient:
|
||||
|
||||
|
||||
def test_live_trace_streams_full_envelope(client: TestClient) -> None:
|
||||
store: SQLiteTraceStore = client.app.state.trace_store
|
||||
store: SQLiteTraceStore = client.app.state.trace_store # type: ignore[attr-defined] # P5: TestClient.app is typed ASGIApp; runtime FastAPI
|
||||
for e in [
|
||||
_event(0, "file_diff", {"path": "x.py"}, 0),
|
||||
_event(1, "command", {"cmd": "pytest -q"}, 10),
|
||||
|
||||
@@ -7,6 +7,7 @@ and gathers digest + defense signals + variant context server-side.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
@@ -49,7 +50,7 @@ class BrokenJSON(MockProvider):
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(tmp_path: Path) -> TestClient:
|
||||
def client(tmp_path: Path) -> Iterator[TestClient]:
|
||||
app = create_app(Settings(provider="mock"))
|
||||
app.state.trace_store = SQLiteTraceStore(db_path=tmp_path / "t.db")
|
||||
app.state.grade_store = SQLiteGradeStore(db_path=tmp_path / "g.db")
|
||||
@@ -61,7 +62,7 @@ def client(tmp_path: Path) -> TestClient:
|
||||
|
||||
|
||||
def _seed_trace(client: TestClient) -> None:
|
||||
store: SQLiteTraceStore = client.app.state.trace_store
|
||||
store: SQLiteTraceStore = client.app.state.trace_store # type: ignore[attr-defined] # P5: TestClient.app is typed ASGIApp; runtime FastAPI
|
||||
for e in [
|
||||
TelemetryEvent(
|
||||
learner_id="p-learner",
|
||||
@@ -86,7 +87,7 @@ def _seed_trace(client: TestClient) -> None:
|
||||
|
||||
|
||||
def test_signals_returns_validated_json(client: TestClient) -> None:
|
||||
client.app.state.provider = ProctorJSON()
|
||||
client.app.state.provider = ProctorJSON() # type: ignore[attr-defined] # P5: TestClient.app is typed ASGIApp; runtime FastAPI
|
||||
_seed_trace(client)
|
||||
resp = client.post(
|
||||
"/v1/proctor/signals", json={"learner_id": "p-learner", "task_id": "p-task"}
|
||||
@@ -99,7 +100,7 @@ def test_signals_returns_validated_json(client: TestClient) -> None:
|
||||
|
||||
def test_empty_trace_is_valid_not_404(client: TestClient) -> None:
|
||||
"""No telemetry → the proctor still assesses (nothing to flag)."""
|
||||
client.app.state.provider = ProctorJSON()
|
||||
client.app.state.provider = ProctorJSON() # type: ignore[attr-defined] # P5: TestClient.app is typed ASGIApp; runtime FastAPI
|
||||
resp = client.post(
|
||||
"/v1/proctor/signals", json={"learner_id": "nobody", "task_id": "nothing"}
|
||||
)
|
||||
@@ -107,7 +108,7 @@ def test_empty_trace_is_valid_not_404(client: TestClient) -> None:
|
||||
|
||||
|
||||
def test_unparseable_provider_502(client: TestClient) -> None:
|
||||
client.app.state.provider = BrokenJSON()
|
||||
client.app.state.provider = BrokenJSON() # type: ignore[attr-defined] # P5: TestClient.app is typed ASGIApp; runtime FastAPI
|
||||
_seed_trace(client)
|
||||
resp = client.post(
|
||||
"/v1/proctor/signals", json={"learner_id": "p-learner", "task_id": "p-task"}
|
||||
@@ -116,6 +117,6 @@ def test_unparseable_provider_502(client: TestClient) -> None:
|
||||
|
||||
|
||||
def test_missing_fields_422(client: TestClient) -> None:
|
||||
client.app.state.provider = ProctorJSON()
|
||||
client.app.state.provider = ProctorJSON() # type: ignore[attr-defined] # P5: TestClient.app is typed ASGIApp; runtime FastAPI
|
||||
resp = client.post("/v1/proctor/signals", json={"learner_id": "x"})
|
||||
assert resp.status_code == 422
|
||||
|
||||
@@ -530,10 +530,10 @@ def test_manager_urls_carry_valid_tokens(client: TestClient) -> None:
|
||||
accepts — mint/verify agree through the shared app.state instance."""
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
manager = client.app.state.sandbox_manager
|
||||
manager = client.app.state.sandbox_manager # type: ignore[attr-defined] # P5: TestClient.app is typed ASGIApp; runtime FastAPI
|
||||
env = manager._capture_env("sbx-tok-1", "learner-tok", "task-tok")
|
||||
parsed = urlparse(env["NC_INGEST_URL"])
|
||||
qs = parse_qs(parsed.query)
|
||||
assert qs["token"][0] == client.app.state.ingest_token.mint(
|
||||
assert qs["token"][0] == client.app.state.ingest_token.mint( # type: ignore[attr-defined] # P5: TestClient.app is typed ASGIApp; runtime FastAPI
|
||||
"learner-tok", "task-tok", "sbx-tok-1"
|
||||
)
|
||||
|
||||
@@ -153,7 +153,9 @@ class TestGenerate:
|
||||
assert body["task_id"].startswith("task-") and len(body["task_id"]) == len("task-") + 16
|
||||
assert body["seed"] # non-empty D-029 seed
|
||||
assert body["statement"] # rendered, non-empty
|
||||
assert body["starter_files"] == get_template(TEMPLATE).starter_files
|
||||
template = get_template(TEMPLATE)
|
||||
assert template is not None, "seed template must resolve"
|
||||
assert body["starter_files"] == template.starter_files
|
||||
assert body["params"] # seeded slot values
|
||||
assert body["created_at"] # ISO timestamp travels
|
||||
assert provider.calls == 1 # first POST renders exactly once
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -44,7 +46,7 @@ def pytest_collection_modifyitems(config, items) -> None:
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def sandbox_dir(tmp_path: Path) -> Path:
|
||||
def sandbox_dir(tmp_path: Path) -> Iterator[Path]:
|
||||
"""Per-test sandbox workdir root on a bind-mount-safe filesystem."""
|
||||
_HOME_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
d = Path(tempfile.mkdtemp(prefix=f"nc-test-{tmp_path.name}-", dir=_HOME_ROOT))
|
||||
@@ -53,7 +55,7 @@ def sandbox_dir(tmp_path: Path) -> Path:
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def _sweep_home_test_sandboxes() -> None:
|
||||
def _sweep_home_test_sandboxes() -> Iterator[None]:
|
||||
"""P2 (Task 2-1-03, G-110): session-end sweep of the anchored test roots.
|
||||
|
||||
Removes ONLY test-created roots, never a user's real state:
|
||||
@@ -156,7 +158,9 @@ def settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Settings:
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def identity_store(tmp_path: Path):
|
||||
def identity_store(tmp_path: Path) -> Iterator[Any]:
|
||||
# P5: the yield-fixture's Iterator return is what mypy needs; the
|
||||
# concrete type is imported INSIDE (lazy-import pattern preserved).
|
||||
from ai_service.identity.store import SQLiteIdentityStore
|
||||
|
||||
store = SQLiteIdentityStore(db_path=tmp_path / "identity-test.db")
|
||||
|
||||
@@ -31,6 +31,7 @@ fails — that is the calibration signal.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -131,14 +132,14 @@ class ArchetypeProvider:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def trace_store(tmp_path: Path) -> SQLiteTraceStore:
|
||||
def trace_store(tmp_path: Path) -> Iterator[SQLiteTraceStore]:
|
||||
store = SQLiteTraceStore(db_path=tmp_path / "traces.db")
|
||||
yield store
|
||||
store.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def grade_store(tmp_path: Path) -> SQLiteGradeStore:
|
||||
def grade_store(tmp_path: Path) -> Iterator[SQLiteGradeStore]:
|
||||
store = SQLiteGradeStore(db_path=tmp_path / "grades.db")
|
||||
yield store
|
||||
store.close()
|
||||
|
||||
@@ -19,6 +19,7 @@ Zero network: everything runs against the mock provider (conftest rule).
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
@@ -127,14 +128,14 @@ def _complete_trace(marker_command: bool = True) -> list[TelemetryEvent]:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def trace_store(tmp_path: Path) -> SQLiteTraceStore:
|
||||
def trace_store(tmp_path: Path) -> Iterator[SQLiteTraceStore]:
|
||||
store = SQLiteTraceStore(db_path=tmp_path / "traces.db")
|
||||
yield store
|
||||
store.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def grade_store(tmp_path: Path) -> SQLiteGradeStore:
|
||||
def grade_store(tmp_path: Path) -> Iterator[SQLiteGradeStore]:
|
||||
store = SQLiteGradeStore(db_path=tmp_path / "grades.db")
|
||||
yield store
|
||||
store.close()
|
||||
|
||||
@@ -17,6 +17,7 @@ Each test gets its own tmp-path SQLite file — no shared disk state. Covers:
|
||||
|
||||
import concurrent.futures
|
||||
import threading
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -61,7 +62,7 @@ def make_grade(
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store(tmp_path: Path) -> SQLiteGradeStore:
|
||||
def store(tmp_path: Path) -> Iterator[SQLiteGradeStore]:
|
||||
s = SQLiteGradeStore(db_path=tmp_path / "grades.db")
|
||||
yield s
|
||||
s.close()
|
||||
|
||||
@@ -14,6 +14,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
@@ -38,7 +39,7 @@ def _age_band(dob: str) -> str:
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def identity_store(tmp_path: Path) -> SQLiteIdentityStore:
|
||||
def identity_store(tmp_path: Path) -> Iterator[SQLiteIdentityStore]:
|
||||
store = SQLiteIdentityStore(db_path=tmp_path / "identity.db")
|
||||
yield store
|
||||
store.close()
|
||||
@@ -198,7 +199,7 @@ def await_(coro):
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def gated_client(tmp_path: Path, identity_store: SQLiteIdentityStore) -> TestClient:
|
||||
def gated_client(tmp_path: Path, identity_store: SQLiteIdentityStore) -> Iterator[TestClient]:
|
||||
seed_verified_identity(identity_store) # suite roster verified 18+
|
||||
settings = Settings(
|
||||
provider="mock",
|
||||
|
||||
@@ -62,7 +62,9 @@ async def test_cancellation_records_abort():
|
||||
p = MockProvider()
|
||||
gen = p.stream_chat(MSGS, model="m")
|
||||
await gen.__anext__()
|
||||
await gen.aclose()
|
||||
# P5 scoped: async generators expose aclose(); the declared protocol
|
||||
# return type (AsyncIterator) doesn't carry it in mypy's view.
|
||||
await gen.aclose() # type: ignore[attr-defined]
|
||||
assert p.abort_recorded is True
|
||||
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@ exact composition production runs.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import SecretStr
|
||||
|
||||
from ai_service.config import Settings
|
||||
from ai_service.identity.store import SQLiteIdentityStore
|
||||
@@ -29,7 +31,7 @@ ADMIN_TOKEN = "test-admin-token"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def marketplace_store(tmp_path: Path) -> SQLiteMarketplaceStore:
|
||||
def marketplace_store(tmp_path: Path) -> Iterator[SQLiteMarketplaceStore]:
|
||||
"""Hermetic store: the shared-DB pattern on a pytest tmp file."""
|
||||
store = SQLiteMarketplaceStore(db_path=tmp_path / "marketplace.db")
|
||||
yield store
|
||||
@@ -37,7 +39,7 @@ def marketplace_store(tmp_path: Path) -> SQLiteMarketplaceStore:
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def identity_store(tmp_path: Path) -> SQLiteIdentityStore:
|
||||
def identity_store(tmp_path: Path) -> Iterator[SQLiteIdentityStore]:
|
||||
store = SQLiteIdentityStore(db_path=tmp_path / "identity.db")
|
||||
seed_verified_identity(store, MARKETPLACE_LEARNERS)
|
||||
yield store
|
||||
@@ -69,7 +71,8 @@ def make_client(
|
||||
learner_allowlist=SUITE_LEARNERS,
|
||||
db_path=tmp_path / "app.db",
|
||||
sandbox_dir=tmp_path / "sandboxes",
|
||||
admin_token=admin_token or "",
|
||||
# P5: Settings.admin_token is SecretStr (never logged); wrap explicitly.
|
||||
admin_token=SecretStr(admin_token or ""),
|
||||
)
|
||||
app = create_app(settings)
|
||||
app.state.identity_store = identity_store # state-injection (G-9)
|
||||
@@ -78,7 +81,7 @@ def make_client(
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_apply_rate_window():
|
||||
def _reset_apply_rate_window() -> Iterator[None]:
|
||||
"""The apply rate cap is module-level process state (the D-019
|
||||
precedent); tests must not inherit each other's windows."""
|
||||
from ai_service.api.marketplace import _APPLY_TIMES
|
||||
@@ -93,7 +96,7 @@ def client(
|
||||
tmp_path: Path,
|
||||
identity_store: SQLiteIdentityStore,
|
||||
marketplace_store: SQLiteMarketplaceStore,
|
||||
) -> TestClient:
|
||||
) -> Iterator[TestClient]:
|
||||
with make_client(
|
||||
tmp_path, identity_store, marketplace_store, admin_token=ADMIN_TOKEN
|
||||
) as c:
|
||||
@@ -105,7 +108,7 @@ def seeded_client(
|
||||
tmp_path: Path,
|
||||
identity_store: SQLiteIdentityStore,
|
||||
marketplace_store: SQLiteMarketplaceStore,
|
||||
) -> TestClient:
|
||||
) -> Iterator[TestClient]:
|
||||
"""Client over an INGESTED store (20 published seed postings)."""
|
||||
from ai_service.marketplace.ingest import SeededCorpusSource, run_ingest
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import struct
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from pydantic import SecretStr
|
||||
|
||||
from ai_service.config import Settings
|
||||
from ai_service.marketplace.embedding import (
|
||||
@@ -172,7 +173,9 @@ class TestVectorCodec:
|
||||
store = SQLiteMarketplaceStore(db_path=tmp_path / "embed.db")
|
||||
values = embed_text("python flask backend")
|
||||
store.put_vector("p", values, MOCK_EMBED_MODEL, MOCK_EMBED_DIM)
|
||||
vals, model, dim = store.get_vector("p")
|
||||
fetched = store.get_vector("p")
|
||||
assert fetched is not None, "just-put vector must read back"
|
||||
vals, model, dim = fetched
|
||||
store.close()
|
||||
assert model == MOCK_EMBED_MODEL
|
||||
assert dim == MOCK_EMBED_DIM
|
||||
@@ -212,7 +215,7 @@ class TestFactory:
|
||||
Settings(
|
||||
embed_provider="openai-embed",
|
||||
embed_base_url="https://e.example/v1",
|
||||
embed_api_key="k",
|
||||
embed_api_key=SecretStr("k"),
|
||||
embed_model="embed-model",
|
||||
)
|
||||
)
|
||||
@@ -222,7 +225,7 @@ class TestFactory:
|
||||
Settings(
|
||||
embed_provider="openai-embed",
|
||||
embed_base_url="https://e.example/v1",
|
||||
embed_api_key="k",
|
||||
embed_api_key=SecretStr("k"),
|
||||
embed_model="embed-model",
|
||||
),
|
||||
httpx.AsyncClient(),
|
||||
|
||||
@@ -98,7 +98,10 @@ class TestSeededCorpusSource:
|
||||
"""G-107/MH-1f: matchScore is absent from EVERY serialized
|
||||
posting — no camelCase, no snake_case, no variant."""
|
||||
for posting in SeededCorpusSource().fetch():
|
||||
blob = json.dumps(posting_summary(posting)).lower()
|
||||
# P5 scoped: the seed source yields IngestPosting (the adapter
|
||||
# shape); posting_summary's model_dump works identically on both
|
||||
# pydantic shapes — the G-107 pin covers the wire form.
|
||||
blob = json.dumps(posting_summary(posting)).lower() # type: ignore[arg-type]
|
||||
assert "matchscore" not in blob
|
||||
assert "match_score" not in blob
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import logging
|
||||
from datetime import UTC, datetime
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import SecretStr
|
||||
|
||||
from ai_service.api import marketplace as marketplace_api
|
||||
from ai_service.config import Settings
|
||||
@@ -55,7 +56,7 @@ class TestAdminFailClosed:
|
||||
learner_allowlist=["pilot-learner"],
|
||||
db_path=tmp_path / "app.db",
|
||||
sandbox_dir=tmp_path / "sandboxes",
|
||||
admin_token="", # UNSET — the fail-closed case
|
||||
admin_token=SecretStr(""), # UNSET — the fail-closed case
|
||||
)
|
||||
app = create_app(settings)
|
||||
app.state.identity_store = identity
|
||||
@@ -620,7 +621,7 @@ class TestShellRoutesServeArbitraryIds:
|
||||
learner_allowlist=["pilot-learner"],
|
||||
db_path=tmp_path / "app.db",
|
||||
sandbox_dir=tmp_path / "sandboxes",
|
||||
admin_token="",
|
||||
admin_token=SecretStr(""),
|
||||
web_static_dir=web_dir,
|
||||
)
|
||||
return TestClient(create_app(settings))
|
||||
@@ -770,7 +771,7 @@ class TestAdminEditRoute:
|
||||
learner_allowlist=["pilot-learner"],
|
||||
db_path=tmp_path / "app.db",
|
||||
sandbox_dir=tmp_path / "sandboxes",
|
||||
admin_token=ADMIN_TOKEN,
|
||||
admin_token=SecretStr(ADMIN_TOKEN),
|
||||
)
|
||||
app = create_app(settings)
|
||||
app.state.identity_store = identity_store
|
||||
@@ -831,7 +832,7 @@ class TestIngestTags:
|
||||
learner_allowlist=["pilot-learner"],
|
||||
db_path=tmp_path / "app.db",
|
||||
sandbox_dir=tmp_path / "sandboxes",
|
||||
admin_token=ADMIN_TOKEN,
|
||||
admin_token=SecretStr(ADMIN_TOKEN),
|
||||
)
|
||||
app = create_app(settings)
|
||||
app.state.identity_store = identity_store
|
||||
@@ -871,11 +872,11 @@ class TestAdminTokenRobustness:
|
||||
provider="mock",
|
||||
db_path=tmp_path / "app.db",
|
||||
sandbox_dir=tmp_path / "s",
|
||||
admin_token=ADMIN_TOKEN,
|
||||
admin_token=SecretStr(ADMIN_TOKEN),
|
||||
)
|
||||
# The pre-fix code raised TypeError here (unhandled → 500).
|
||||
try:
|
||||
require_admin("café-nonce-\u00e9", settings) # type: ignore[arg-type]
|
||||
require_admin("café-nonce-\u00e9", settings)
|
||||
except HTTPException as exc:
|
||||
assert exc.status_code == 403 # honest rejection
|
||||
else:
|
||||
|
||||
@@ -11,6 +11,7 @@ import logging
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import SecretStr
|
||||
|
||||
from ai_service.llm.mock import MockProvider, ScriptedJSONProvider
|
||||
from ai_service.marketplace.resume import (
|
||||
@@ -77,7 +78,7 @@ class TestExtract:
|
||||
|
||||
def test_unsupported_format_rejected(self) -> None:
|
||||
with pytest.raises(ResumeFormatError):
|
||||
extract_resume_text(b"MZ\x90\x00", "application/vnd.exe") # type: ignore[arg-type]
|
||||
extract_resume_text(b"MZ\x90\x00", "application/vnd.exe")
|
||||
|
||||
def test_garbage_text_rejected(self) -> None:
|
||||
with pytest.raises(ResumeFormatError):
|
||||
@@ -122,7 +123,7 @@ class TestResumeRoutes:
|
||||
learner_allowlist=["pilot-learner"],
|
||||
db_path=tmp_path / "app.db",
|
||||
sandbox_dir=tmp_path / "sandboxes",
|
||||
admin_token=ADMIN_TOKEN,
|
||||
admin_token=SecretStr(ADMIN_TOKEN),
|
||||
)
|
||||
app = create_app(settings)
|
||||
app.state.identity_store = identity_store
|
||||
|
||||
@@ -23,10 +23,12 @@ Pins:
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import SecretStr
|
||||
|
||||
from ai_service.main import create_app
|
||||
from ai_service.marketplace.base import IngestPosting, Posting, PostingTags
|
||||
@@ -74,6 +76,7 @@ def _posting(
|
||||
title=title,
|
||||
description=description,
|
||||
skills=skills,
|
||||
# P5: fixture accepts raw str; Posting's Literal contract.
|
||||
seniority=seniority, # type: ignore[arg-type]
|
||||
category=category,
|
||||
location=location,
|
||||
@@ -81,7 +84,7 @@ def _posting(
|
||||
salary_min=salary_min,
|
||||
salary_max=salary_max,
|
||||
status=status, # type: ignore[arg-type]
|
||||
tags=PostingTags(skills=skills, seniority=seniority, category=category), # type: ignore[arg-type]
|
||||
tags=PostingTags(skills=skills, seniority=seniority, category=category), # type: ignore[arg-type] # P5: same str-vs-Literal fixture skew
|
||||
posted_at=posted_at or datetime(2026, 9, 1, tzinfo=UTC),
|
||||
)
|
||||
|
||||
@@ -430,7 +433,7 @@ class TestSearchRoute:
|
||||
@pytest.fixture()
|
||||
def search_client(
|
||||
self, tmp_path, identity_store, marketplace_store
|
||||
) -> TestClient:
|
||||
) -> Iterator[TestClient]:
|
||||
"""Client whose store is seeded, embedded, and ready to search."""
|
||||
import asyncio as _asyncio
|
||||
|
||||
@@ -639,7 +642,7 @@ class TestBootFallback:
|
||||
learner_allowlist=SUITE_LEARNERS,
|
||||
db_path=tmp_path / "app.db",
|
||||
sandbox_dir=tmp_path / "sandboxes",
|
||||
admin_token=ADMIN_TOKEN,
|
||||
admin_token=SecretStr(ADMIN_TOKEN),
|
||||
)
|
||||
app = create_app(settings)
|
||||
app.state.identity_store = identity_store
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import SecretStr
|
||||
|
||||
from ai_service.api.sitemap import STATIC_PAGES
|
||||
from ai_service.config import Settings
|
||||
@@ -33,7 +34,7 @@ def make_web_client(
|
||||
learner_allowlist=MARKETPLACE_LEARNERS,
|
||||
db_path=tmp_path / "app.db",
|
||||
sandbox_dir=tmp_path / "sandboxes",
|
||||
admin_token=ADMIN_TOKEN,
|
||||
admin_token=SecretStr(ADMIN_TOKEN),
|
||||
public_base_url=public_base_url,
|
||||
**({"web_static_dir": web_dir} if web_dir else {}),
|
||||
)
|
||||
@@ -146,7 +147,8 @@ class TestSitemap:
|
||||
assert "<script>" in body # escaped form present
|
||||
root = ET.fromstring(body) # still well-formed XML
|
||||
locs = [e.text for e in root.iter() if e.tag.endswith("loc")]
|
||||
assert locs and locs[0].startswith("https://x.example\">")
|
||||
assert locs and locs[0] is not None
|
||||
assert locs[0].startswith("https://x.example\">")
|
||||
|
||||
def test_static_pages_use_real_export_paths(
|
||||
self, tmp_path, ingested_store, identity
|
||||
@@ -241,6 +243,7 @@ class TestMountOrder:
|
||||
locs = [e.text for e in root.iter() if e.tag.endswith("loc")]
|
||||
assert locs, "sitemap enumerated nothing"
|
||||
for loc in locs:
|
||||
assert loc is not None, "a sitemap <loc> is empty"
|
||||
path = loc.replace(ORIGIN, "") or "/"
|
||||
probe = path if path.endswith("/") else path + "/"
|
||||
assert client.get(probe).status_code == 200, (
|
||||
|
||||
@@ -14,6 +14,7 @@ every other move.
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
@@ -27,11 +28,11 @@ def _posting(
|
||||
external_id: str = "job-x",
|
||||
source: str = "seed-corpus",
|
||||
posting_id: str | None = None,
|
||||
**overrides,
|
||||
**overrides: Any,
|
||||
) -> IngestPosting:
|
||||
"""A valid IngestPosting; `posting_id` defaults to external_id (the
|
||||
D-021 shared-id convention: id == external_id for seed ports)."""
|
||||
payload = dict(
|
||||
payload: dict[str, Any] = dict(
|
||||
id=posting_id or external_id,
|
||||
source=source,
|
||||
external_id=external_id,
|
||||
@@ -149,7 +150,7 @@ class TestPostingConstraints:
|
||||
def test_validate_hooks(self, marketplace_store) -> None:
|
||||
with pytest.raises(ValueError, match="invalid posting status"):
|
||||
marketplace_store.upsert_posting(
|
||||
_posting(), status="banana" # type: ignore[arg-type]
|
||||
_posting(), status="banana" # P5: deliberate invalid-status probe
|
||||
)
|
||||
with pytest.raises(ValueError, match="employer_id"):
|
||||
marketplace_store.upsert_posting(
|
||||
|
||||
@@ -62,7 +62,7 @@ class TestTagPosting:
|
||||
attempts = {"n": 0}
|
||||
|
||||
class RetryThenValid(MockProvider):
|
||||
def _reply_for(self, messages, response_format): # type: ignore[override]
|
||||
def _reply_for(self, messages, response_format):
|
||||
if response_format is not None and response_format.get("type") == "json_object":
|
||||
attempts["n"] += 1
|
||||
if attempts["n"] == 1:
|
||||
@@ -102,7 +102,7 @@ class TestTagOnIngest:
|
||||
"""json_object responses derived from the posting content —
|
||||
deterministic per posting."""
|
||||
|
||||
def _reply_for(self, messages, response_format): # type: ignore[override]
|
||||
def _reply_for(self, messages, response_format):
|
||||
import json
|
||||
|
||||
if response_format is not None and response_format.get("type") == "json_object":
|
||||
@@ -146,6 +146,7 @@ class TestTagOnIngest:
|
||||
async def _tag_all() -> None:
|
||||
for pid in ("job-001", "job-002"):
|
||||
posting = store.get_posting(pid)
|
||||
assert posting is not None, "hand-seeded posting must resolve"
|
||||
tags = await tag_posting(
|
||||
DeterministicTagger(),
|
||||
model=MODEL,
|
||||
|
||||
@@ -28,6 +28,7 @@ the session for suites outside ``tests/sandbox/``) is handled there.
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -39,7 +40,7 @@ _SANDBOX_DROPPINGS_ROOT = Path(__file__).resolve().parents[1] / "sandboxes"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def _sweep_repo_sandbox_droppings() -> None:
|
||||
def _sweep_repo_sandbox_droppings() -> Iterator[None]:
|
||||
"""After the session's last test, remove the anchored sandbox root.
|
||||
|
||||
``rmtree(ignore_errors=True)``: the whole ``tests/sandboxes/`` tree is
|
||||
|
||||
@@ -29,6 +29,7 @@ from collections.abc import Iterator
|
||||
from datetime import datetime
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -79,7 +80,11 @@ def _load_agent():
|
||||
return mod
|
||||
|
||||
|
||||
agent = _load_agent()
|
||||
# P5: the sandbox agent is loaded from its hyphenated path at runtime
|
||||
# (spec_from_file_location) — statically it is an opaque module, so the
|
||||
# Any alias keeps the suite's agent.Agent/AgentConfig/Spool annotations
|
||||
# checkable without pretending the dynamic load has static types.
|
||||
agent: Any = _load_agent()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Raw-socket frame helpers (test-harness mirror of the agent's codec)
|
||||
@@ -511,6 +516,7 @@ class TestFileDiffWatcher:
|
||||
test_agent.stop()
|
||||
|
||||
created = _find_diff(test_agent, "hello.txt", "created")
|
||||
assert created is not None, "created diff never observed"
|
||||
assert created["payload"]["diff"] # unified diff present
|
||||
assert "v1" in created["payload"]["diff"]
|
||||
|
||||
|
||||
@@ -86,7 +86,9 @@ async def test_task_sandbox_streams_events_to_ingest(tmp_path: Path, sandbox_dir
|
||||
)
|
||||
return env
|
||||
|
||||
manager._capture_env = _capture_env
|
||||
# P5 scoped: deliberate test seam — NC_INGEST_URL redirected to
|
||||
# the proxy under test (the wiring probe's whole mechanism).
|
||||
manager._capture_env = _capture_env # type: ignore[method-assign]
|
||||
|
||||
handle = await manager.create(learner, task_id=task)
|
||||
try:
|
||||
|
||||
@@ -88,11 +88,14 @@ async def _await_file_diffs(
|
||||
|
||||
|
||||
def _stored_diff_paths(store: SQLiteTraceStore, learner: str, task: str) -> set[str]:
|
||||
return {
|
||||
e.payload.get("path")
|
||||
for e in store.get_trace(learner, task)
|
||||
if e.kind == "file_diff"
|
||||
}
|
||||
paths: set[str] = set()
|
||||
for e in store.get_trace(learner, task):
|
||||
if e.kind != "file_diff":
|
||||
continue
|
||||
path = e.payload.get("path")
|
||||
if isinstance(path, str): # P5: payload.get is object|None — filter to str
|
||||
paths.add(path)
|
||||
return paths
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -147,14 +150,16 @@ async def test_disconnect_reconnect_loses_nothing(tmp_path: Path, sandbox_dir: P
|
||||
"NC_BACKOFF_MAX_S": "0.5",
|
||||
}
|
||||
|
||||
manager._capture_env = capture_env
|
||||
# P5 scoped: deliberate test seam — the proxy URL replaces the real
|
||||
# capture agent's env (the durability probe dials the KillableProxy).
|
||||
manager._capture_env = capture_env # type: ignore[method-assign]
|
||||
|
||||
try:
|
||||
async with serve(app, "127.0.0.1", server_port):
|
||||
handle = await manager.create(learner, task_id=task)
|
||||
try:
|
||||
live = manager._handles[handle.id]
|
||||
backend: UnshareBackend = manager._backend
|
||||
backend = manager._backend
|
||||
|
||||
# Phase A — connected: a write streams through.
|
||||
result = await backend.exec(live, ["sh", "-c", "echo one > a.txt"])
|
||||
@@ -258,14 +263,16 @@ async def test_midburst_disconnect_loses_nothing(tmp_path: Path, sandbox_dir: Pa
|
||||
"NC_BACKOFF_MAX_S": "0.5",
|
||||
}
|
||||
|
||||
manager._capture_env = capture_env
|
||||
# P5 scoped: deliberate test seam — the proxy URL replaces the real
|
||||
# capture agent's env (the durability probe dials the KillableProxy).
|
||||
manager._capture_env = capture_env # type: ignore[method-assign]
|
||||
|
||||
try:
|
||||
async with serve(app, "127.0.0.1", server_port):
|
||||
handle = await manager.create(learner, task_id=task)
|
||||
try:
|
||||
live = manager._handles[handle.id]
|
||||
backend: UnshareBackend = manager._backend
|
||||
backend = manager._backend
|
||||
|
||||
# Phase A — connected: confirm the pipe works (1 event minimum).
|
||||
result = await backend.exec(live, ["sh", "-c", "echo warm > warm.txt"])
|
||||
|
||||
@@ -6,6 +6,7 @@ canonical kwargs builder.
|
||||
|
||||
import json
|
||||
from datetime import UTC, datetime
|
||||
from typing import get_args
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
@@ -123,4 +124,5 @@ def test_tracespan_rejects_empty_ids():
|
||||
|
||||
def test_event_kind_type_is_exported_literal():
|
||||
# The kind discriminator is part of the public module surface.
|
||||
assert "command" in EventKind.__args__
|
||||
# P5: get_args() is the typed accessor (bare __args__ trips mypy).
|
||||
assert "command" in get_args(EventKind)
|
||||
|
||||
@@ -124,16 +124,13 @@ class TestExecPolicy:
|
||||
class ExecStubBackend(StubBackend):
|
||||
"""StubBackend + a working exec (policy fires BEFORE exec)."""
|
||||
|
||||
async def exec(self, handle, cmd): # type: ignore[override]
|
||||
from datetime import UTC, datetime
|
||||
|
||||
async def exec(self, handle, cmd):
|
||||
return ExecResult(
|
||||
cmd=list(cmd),
|
||||
returncode=0,
|
||||
stdout="ok",
|
||||
stderr="",
|
||||
duration_s=0.0,
|
||||
ts=datetime.now(UTC),
|
||||
)
|
||||
|
||||
vstore = SQLiteVariantStore(db_path=tmp_path / "v.db")
|
||||
@@ -167,18 +164,20 @@ class TestExecPolicy:
|
||||
"/v1/sandboxes",
|
||||
json={"learner_id": DESIGN_LEARNER, "task_id": var["task_id"]},
|
||||
).json()
|
||||
c._sandbox_id = sbx["id"] # type: ignore[attr-defined]
|
||||
c._sandbox_id = sbx["id"] # P5: private-attr seam the fixture sets; reads stay untyped
|
||||
yield c
|
||||
|
||||
def test_design_kind_rejects_out_of_policy_command(self, exec_client) -> None:
|
||||
sbx = exec_client._sandbox_id # type: ignore[attr-defined]
|
||||
# P5: _sandbox_id was set on the client by the fixture (Any-typed seam).
|
||||
sbx = exec_client._sandbox_id
|
||||
resp = exec_client.post(f"/v1/sandboxes/{sbx}/exec", json={"cmd": ["rm", "-rf", "/"]})
|
||||
assert resp.status_code == 422
|
||||
assert "'rm'" in resp.json()["detail"]
|
||||
assert "allowed" in resp.json()["detail"]
|
||||
|
||||
def test_design_kind_rejects_shell_passthrough(self, exec_client) -> None:
|
||||
sbx = exec_client._sandbox_id # type: ignore[attr-defined]
|
||||
# P5: _sandbox_id was set on the client by the fixture (Any-typed seam).
|
||||
sbx = exec_client._sandbox_id
|
||||
resp = exec_client.post(
|
||||
f"/v1/sandboxes/{sbx}/exec", json={"cmd": ["sh", "-c", "anything"]}
|
||||
)
|
||||
@@ -189,7 +188,8 @@ class TestExecPolicy:
|
||||
"""The policy passes the declared harness (StubBackend.exec raises
|
||||
NotImplementedError by design — any status EXCEPT 422 proves the
|
||||
policy allowed the command through)."""
|
||||
sbx = exec_client._sandbox_id # type: ignore[attr-defined]
|
||||
# P5: _sandbox_id was set on the client by the fixture (Any-typed seam).
|
||||
sbx = exec_client._sandbox_id
|
||||
resp = exec_client.post(
|
||||
f"/v1/sandboxes/{sbx}/exec",
|
||||
json={"cmd": ["python3", "validate_flow.py"]},
|
||||
|
||||
@@ -26,7 +26,9 @@ class ScriptedRenderProvider(MockProvider):
|
||||
super().__init__()
|
||||
self.calls = 0
|
||||
|
||||
async def chat(self, messages, model, response_format=None):
|
||||
async def chat(self, messages, *, model, temperature=0.7, response_format=None):
|
||||
# P5: keyword-only signature matches the MockProvider/LLMProvider
|
||||
# protocol shape (the old positional `model` was an override skew).
|
||||
self.calls += 1
|
||||
import json
|
||||
|
||||
@@ -43,7 +45,7 @@ class FailingRenderProvider(MockProvider):
|
||||
super().__init__()
|
||||
self.calls = 0
|
||||
|
||||
async def chat(self, messages, model, response_format=None):
|
||||
async def chat(self, messages, *, model, temperature=0.7, response_format=None):
|
||||
self.calls += 1
|
||||
return "this is not json at all"
|
||||
|
||||
@@ -88,6 +90,7 @@ async def test_params_are_schema_valid(store) -> None:
|
||||
gen = VariantGenerator(store, ScriptedRenderProvider(), model="mock")
|
||||
record = await gen.generate("learner-a", "tpl-guardrail-schema")
|
||||
template = get_template("tpl-guardrail-schema")
|
||||
# P5: get_template returns Optional (unknown id raises inside generate).
|
||||
for slot in template.slots: # type: ignore[union-attr]
|
||||
value = record.params[slot.name]
|
||||
assert slot.validate_value(value), f"slot {slot.name} drew invalid {value!r}"
|
||||
@@ -98,7 +101,7 @@ async def test_llm_failure_falls_back_deterministically(store) -> None:
|
||||
gen = VariantGenerator(store, provider, model="mock")
|
||||
record = await gen.generate("learner-a", "tpl-rag-chunker")
|
||||
template = get_template("tpl-rag-chunker")
|
||||
expected = template.render({k: v for k, v in record.params.items()}) # type: ignore
|
||||
expected = template.render({k: v for k, v in record.params.items()}) # type: ignore[union-attr]
|
||||
assert record.statement == expected # skeleton render, seed-auditable
|
||||
assert provider.calls == 2 # D-020 bounded retry, then fallback
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ Each test gets its own tmp-path SQLite file — no shared disk state. Covers:
|
||||
|
||||
import concurrent.futures
|
||||
import threading
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -73,7 +74,7 @@ def make_variant(
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store(tmp_path: Path) -> SQLiteVariantStore:
|
||||
def store(tmp_path: Path) -> Iterator[SQLiteVariantStore]:
|
||||
s = SQLiteVariantStore(db_path=tmp_path / "variants.db")
|
||||
yield s
|
||||
s.close()
|
||||
|
||||
@@ -36,7 +36,10 @@ def test_int_range_slot_bounds() -> None:
|
||||
slot = ParameterSlot(name="n", type="int_range", lo=2, hi=4)
|
||||
rng = random.Random(0)
|
||||
for _ in range(20):
|
||||
assert 2 <= slot.sample(rng) <= 4
|
||||
# P5: sample() returns str | int; int_range slots yield ints.
|
||||
drawn = slot.sample(rng)
|
||||
assert isinstance(drawn, int)
|
||||
assert 2 <= drawn <= 4
|
||||
assert slot.validate_value(3)
|
||||
assert not slot.validate_value(5)
|
||||
assert not slot.validate_value("3")
|
||||
|
||||
@@ -23,6 +23,7 @@ Each test gets its own tmp-path SQLite file — no shared disk state. Covers:
|
||||
- WAL + synchronous=NORMAL + foreign_keys=ON pragmas actually applied
|
||||
"""
|
||||
|
||||
from collections.abc import Iterator
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
@@ -78,7 +79,7 @@ def make_turn(
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store(tmp_path: Path) -> SQLiteDefenseStore:
|
||||
def store(tmp_path: Path) -> Iterator[SQLiteDefenseStore]:
|
||||
s = SQLiteDefenseStore(db_path=tmp_path / "defenses.db")
|
||||
yield s
|
||||
s.close()
|
||||
@@ -321,7 +322,7 @@ def test_finalize_rejects_non_dict_signals(store: SQLiteDefenseStore) -> None:
|
||||
store.start(make_defense())
|
||||
|
||||
with pytest.raises(ValueError, match="integrity_signals"):
|
||||
store.finalize("defense-1", None) # type: ignore[arg-type]
|
||||
store.finalize("defense-1", None) # type: ignore[arg-type] # P5: deliberate None probe (finalization contract)
|
||||
|
||||
fetched = store.get("defense-1")
|
||||
assert fetched is not None
|
||||
|
||||
@@ -14,6 +14,7 @@ not against its own definition.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -37,7 +38,7 @@ DEFENSE_TURN_BUDGET_MS = 4_000
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(tmp_path: Path) -> TestClient:
|
||||
def client(tmp_path: Path) -> Iterator[TestClient]:
|
||||
from ai_service.identity.store import SQLiteIdentityStore
|
||||
from tests.conftest import SUITE_LEARNERS, seed_verified_identity
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ Cloud-free: the real endpoint is a manual probe recipe (.env.example).
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -18,10 +19,10 @@ from ai_service.voice.openai_audio import OpenAIAudioProvider
|
||||
KEY = "sk-voice-test-xyz"
|
||||
|
||||
|
||||
def make_provider(handler, **overrides) -> OpenAIAudioProvider:
|
||||
def make_provider(handler, **overrides: Any) -> OpenAIAudioProvider:
|
||||
transport = httpx.MockTransport(handler)
|
||||
client = httpx.AsyncClient(transport=transport)
|
||||
kwargs = {
|
||||
kwargs: dict[str, Any] = {
|
||||
"base_url": "https://voice.example/v1",
|
||||
"api_key": KEY,
|
||||
"stt_model": "whisper-1",
|
||||
|
||||
@@ -113,7 +113,7 @@ export function AnalyticsCharts() {
|
||||
<Cell key={entry.name} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={tooltipStyle} formatter={(v: number) => `${v}%`} />
|
||||
<Tooltip contentStyle={tooltipStyle} formatter={(value) => `${value}%`} />
|
||||
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@@ -55,7 +55,7 @@ export function ProgressGraph() {
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={tooltipStyle}
|
||||
formatter={(v: number) => [`${v}%`, 'Mastery']}
|
||||
formatter={(value) => [`${value}%`, 'Mastery']}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
|
||||
@@ -26,10 +26,9 @@
|
||||
"next": "^15.1.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"recharts": "^2.15.0"
|
||||
"recharts": "^3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^8.6.14",
|
||||
"@storybook/nextjs": "^10.6.0",
|
||||
"@storybook/react": "^10.6.0",
|
||||
"@storybook/test": "^8.6.15",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"ai:test": "turbo run test --filter=@nextcraft/ai-service",
|
||||
"ai:bootstrap": "turbo run bootstrap --filter=@nextcraft/ai-service",
|
||||
"ai:lint": "turbo run lint --filter=@nextcraft/ai-service",
|
||||
"ai:typecheck": "turbo run typecheck --filter=@nextcraft/ai-service",
|
||||
"ai:test:fast": "cd apps/ai-service && FAST=1 bash scripts/test.sh",
|
||||
"cli:dev": "pnpm --filter @nextcraft/cli dev",
|
||||
"test": "turbo run test",
|
||||
|
||||
Generated
+107
-334
@@ -164,7 +164,7 @@ importers:
|
||||
version: link:../../packages/ui
|
||||
'@xyflow/react':
|
||||
specifier: ^12.3.5
|
||||
version: 12.11.6(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
version: 12.11.6(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(immer@11.1.18)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
lucide-react:
|
||||
specifier: ^0.468.0
|
||||
version: 0.468.0(react@19.3.0)
|
||||
@@ -178,12 +178,9 @@ importers:
|
||||
specifier: ^19.0.0
|
||||
version: 19.3.0(react@19.3.0)
|
||||
recharts:
|
||||
specifier: ^2.15.0
|
||||
version: 2.15.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
specifier: ^3
|
||||
version: 3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@18.3.1)(react@19.3.0)(redux@5.0.1)
|
||||
devDependencies:
|
||||
'@storybook/addon-essentials':
|
||||
specifier: ^8.6.14
|
||||
version: 8.6.14(@types/react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
'@storybook/nextjs':
|
||||
specifier: ^10.6.0
|
||||
version: 10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(clean-css@5.3.3)(esbuild@0.28.2)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(next@15.5.25(@babel/core@7.29.7(supports-color@8.1.1))(@types/node@20.17.6)(react-dom@19.3.0(react@19.3.0))(react@19.3.0))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(sharp@0.35.4(@types/node@20.17.6))(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))(supports-color@8.1.1)(type-fest@2.19.0)(typescript@5.9.3)(webpack-hot-middleware@2.26.1)(webpack@5.110.3(clean-css@5.3.3)(esbuild@0.28.2)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.28)(sharp@0.35.4(@types/node@20.17.6)))
|
||||
@@ -1286,12 +1283,6 @@ packages:
|
||||
'@keyv/serialize@1.1.1':
|
||||
resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
|
||||
|
||||
'@mdx-js/react@3.1.1':
|
||||
resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==}
|
||||
peerDependencies:
|
||||
'@types/react': '>=16'
|
||||
react: '>=16'
|
||||
|
||||
'@napi-rs/wasm-runtime@1.2.3':
|
||||
resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==}
|
||||
engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
|
||||
@@ -1617,68 +1608,23 @@ packages:
|
||||
webpack-plugin-serve:
|
||||
optional: true
|
||||
|
||||
'@storybook/addon-actions@8.6.14':
|
||||
resolution: {integrity: sha512-mDQxylxGGCQSK7tJPkD144J8jWh9IU9ziJMHfB84PKpI/V5ZgqMDnpr2bssTrUaGDqU5e1/z8KcRF+Melhs9pQ==}
|
||||
'@reduxjs/toolkit@2.12.0':
|
||||
resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==}
|
||||
peerDependencies:
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/addon-backgrounds@8.6.14':
|
||||
resolution: {integrity: sha512-l9xS8qWe5n4tvMwth09QxH2PmJbCctEvBAc1tjjRasAfrd69f7/uFK4WhwJAstzBTNgTc8VXI4w8ZR97i1sFbg==}
|
||||
peerDependencies:
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/addon-controls@8.6.14':
|
||||
resolution: {integrity: sha512-IiQpkNJdiRyA4Mq9mzjZlvQugL/aE7hNgVxBBGPiIZG6wb6Ht9hNnBYpap5ZXXFKV9p2qVI0FZK445ONmAa+Cw==}
|
||||
peerDependencies:
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/addon-docs@8.6.14':
|
||||
resolution: {integrity: sha512-Obpd0OhAF99JyU5pp5ci17YmpcQtMNgqW2pTXV8jAiiipWpwO++hNDeQmLmlSXB399XjtRDOcDVkoc7rc6JzdQ==}
|
||||
peerDependencies:
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/addon-essentials@8.6.14':
|
||||
resolution: {integrity: sha512-5ZZSHNaW9mXMOFkoPyc3QkoNGdJHETZydI62/OASR0lmPlJ1065TNigEo5dJddmZNn0/3bkE8eKMAzLnO5eIdA==}
|
||||
peerDependencies:
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/addon-highlight@8.6.14':
|
||||
resolution: {integrity: sha512-4H19OJlapkofiE9tM6K/vsepf4ir9jMm9T+zw5L85blJZxhKZIbJ6FO0TCG9PDc4iPt3L6+aq5B0X29s9zicNQ==}
|
||||
peerDependencies:
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/addon-measure@8.6.14':
|
||||
resolution: {integrity: sha512-1Tlyb72NX8aAqm6I6OICsUuGOP6hgnXcuFlXucyhKomPa6j3Eu2vKu561t/f0oGtAK2nO93Z70kVaEh5X+vaGw==}
|
||||
peerDependencies:
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/addon-outline@8.6.14':
|
||||
resolution: {integrity: sha512-CW857JvN6OxGWElqjlzJO2S69DHf+xO3WsEfT5mT3ZtIjmsvRDukdWfDU9bIYUFyA2lFvYjncBGjbK+I91XR7w==}
|
||||
peerDependencies:
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/addon-toolbars@8.6.14':
|
||||
resolution: {integrity: sha512-W/wEXT8h3VyZTVfWK/84BAcjAxTdtRiAkT2KAN0nbSHxxB5KEM1MjKpKu2upyzzMa3EywITqbfy4dP6lpkVTwQ==}
|
||||
peerDependencies:
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/addon-viewport@8.6.14':
|
||||
resolution: {integrity: sha512-gNzVQbMqRC+/4uQTPI2ZrWuRHGquTMZpdgB9DrD88VTEjNudP+J6r8myLfr2VvGksBbUMHkGHMXHuIhrBEnXYA==}
|
||||
peerDependencies:
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/blocks@8.6.14':
|
||||
resolution: {integrity: sha512-rBMHAfA39AGHgkrDze4RmsnQTMw1ND5fGWobr9pDcJdnDKWQWNRD7Nrlxj0gFlN3n4D9lEZhWGdFrCbku7FVAQ==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
storybook: ^8.6.14
|
||||
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
|
||||
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
react-redux:
|
||||
optional: true
|
||||
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@standard-schema/utils@0.3.0':
|
||||
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
|
||||
|
||||
'@storybook/builder-webpack5@10.6.0':
|
||||
resolution: {integrity: sha512-j71OmQZ/+j9S/FYa64tIRuSsXdVxU87zthJ+StKUan6m7wA88YyCAXzk2hAHQsfC9MA52A0kMcOPluNZzIh3xQ==}
|
||||
peerDependencies:
|
||||
@@ -1693,21 +1639,9 @@ packages:
|
||||
peerDependencies:
|
||||
storybook: ^10.6.0
|
||||
|
||||
'@storybook/csf-plugin@8.6.14':
|
||||
resolution: {integrity: sha512-dErtc9teAuN+eelN8FojzFE635xlq9cNGGGEu0WEmMUQ4iJ8pingvBO1N8X3scz4Ry7KnxX++NNf3J3gpxS8qQ==}
|
||||
peerDependencies:
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/global@5.0.0':
|
||||
resolution: {integrity: sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==}
|
||||
|
||||
'@storybook/icons@1.6.0':
|
||||
resolution: {integrity: sha512-hcFZIjW8yQz8O8//2WTIXylm5Xsgc+lW9ISLgUk1xGmptIJQRdlhVIXCpSyLrQaaRiyhQRaVg7l3BD9S216BHw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
|
||||
|
||||
'@storybook/icons@2.1.0':
|
||||
resolution: {integrity: sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==}
|
||||
peerDependencies:
|
||||
@@ -1770,13 +1704,6 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@storybook/react-dom-shim@8.6.14':
|
||||
resolution: {integrity: sha512-0hixr3dOy3f3M+HBofp3jtMQMS+sqzjKNgl7Arfuj3fvjmyXOks/yGjDImySR4imPtEllvPZfhiQNlejheaInw==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
|
||||
storybook: ^8.6.14
|
||||
|
||||
'@storybook/react@10.6.0':
|
||||
resolution: {integrity: sha512-0xfYYPRHcr5ta1hmXJCSezH4JGuUHJ+xVF9f1j+Y/MWEMOQE4wnaKWKsk/HLrDjZkT5hKfvmogt9b0yduP6zEQ==}
|
||||
peerDependencies:
|
||||
@@ -2033,9 +1960,6 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/mdx@2.0.14':
|
||||
resolution: {integrity: sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==}
|
||||
|
||||
'@types/node@20.17.6':
|
||||
resolution: {integrity: sha512-VEI7OdvK2wP7XHnsuXbAJnEpEkF6NjSN45QJlL4VGqZSXsnicpesdTWsg9RISeSdYd3yeRj/y3k5KGjUXYnFwQ==}
|
||||
|
||||
@@ -2056,8 +1980,8 @@ packages:
|
||||
'@types/semver@7.8.0':
|
||||
resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==}
|
||||
|
||||
'@types/uuid@9.0.8':
|
||||
resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==}
|
||||
'@types/use-sync-external-store@0.0.6':
|
||||
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.70.1':
|
||||
resolution: {integrity: sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA==}
|
||||
@@ -2798,9 +2722,6 @@ packages:
|
||||
dom-converter@0.2.0:
|
||||
resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==}
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||
|
||||
dom-serializer@1.4.1:
|
||||
resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
|
||||
|
||||
@@ -2882,6 +2803,9 @@ packages:
|
||||
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-toolkit@1.52.0:
|
||||
resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==}
|
||||
|
||||
esbuild@0.28.2:
|
||||
resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -2955,8 +2879,8 @@ packages:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
eventemitter3@4.0.7:
|
||||
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
|
||||
eventemitter3@5.0.4:
|
||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||
|
||||
events@3.3.0:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
@@ -2968,10 +2892,6 @@ packages:
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-equals@5.4.2:
|
||||
resolution: {integrity: sha512-Ywe6jodPTWOTL9/k0bV7gdfP8twKL5Y8I8CZ933fAY5gBekICZSUQTbyH6ut2NZCNyB05mSUwAuEqdEIaOOlDQ==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
fast-json-parse@1.0.3:
|
||||
resolution: {integrity: sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==}
|
||||
|
||||
@@ -3214,6 +3134,9 @@ packages:
|
||||
resolution: {integrity: sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
immer@11.1.18:
|
||||
resolution: {integrity: sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3484,10 +3407,6 @@ packages:
|
||||
lodash@4.18.1:
|
||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
|
||||
loupe@3.2.1:
|
||||
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
|
||||
|
||||
@@ -3523,9 +3442,6 @@ packages:
|
||||
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
map-or-similar@1.5.0:
|
||||
resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3537,9 +3453,6 @@ packages:
|
||||
resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
|
||||
memoizerific@1.11.3:
|
||||
resolution: {integrity: sha512-/EuHYwAPdLtXwAwSZkh/Gutery6pD2KYd44oQLhAvQp/50mpyduZh8Q7PYHXTCJ+wuXxt7oij2LXyIJOOYFPog==}
|
||||
|
||||
merge-stream@2.0.0:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
|
||||
@@ -3708,10 +3621,6 @@ packages:
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
object-inspect@1.13.4:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3861,10 +3770,6 @@ packages:
|
||||
resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
||||
polished@4.3.1:
|
||||
resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3942,9 +3847,6 @@ packages:
|
||||
resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
|
||||
engines: {node: '>= 0.6.0'}
|
||||
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
public-encrypt@4.0.3:
|
||||
resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==}
|
||||
|
||||
@@ -3991,31 +3893,28 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^19.3.0
|
||||
|
||||
react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
react-is@17.0.2:
|
||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
react-redux@9.3.0:
|
||||
resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==}
|
||||
peerDependencies:
|
||||
'@types/react': ^18.2.25 || ^19
|
||||
react: ^18.0 || ^19
|
||||
redux: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
redux:
|
||||
optional: true
|
||||
|
||||
react-refresh@0.14.2:
|
||||
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
react-smooth@4.0.4:
|
||||
resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
react-transition-group@4.4.5:
|
||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||
peerDependencies:
|
||||
react: '>=16.6.0'
|
||||
react-dom: '>=16.6.0'
|
||||
|
||||
react@19.3.0:
|
||||
resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -4039,21 +3938,26 @@ packages:
|
||||
resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
recharts-scale@0.4.5:
|
||||
resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
|
||||
|
||||
recharts@2.15.4:
|
||||
resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==}
|
||||
engines: {node: '>=14'}
|
||||
deprecated: 1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide
|
||||
recharts@3.10.1:
|
||||
resolution: {integrity: sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
redent@3.0.0:
|
||||
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
redux-thunk@3.1.0:
|
||||
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
|
||||
peerDependencies:
|
||||
redux: ^5.0.0
|
||||
|
||||
redux@5.0.1:
|
||||
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
||||
|
||||
regenerate-unicode-properties@10.2.2:
|
||||
resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -4090,6 +3994,9 @@ packages:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
reselect@5.2.0:
|
||||
resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -4513,10 +4420,6 @@ packages:
|
||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
unplugin@1.16.1:
|
||||
resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
update-browserslist-db@1.3.2:
|
||||
resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==}
|
||||
hasBin: true
|
||||
@@ -4544,17 +4447,12 @@ packages:
|
||||
utila@0.4.0:
|
||||
resolution: {integrity: sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==}
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
v8-to-istanbul@9.3.0:
|
||||
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
|
||||
victory-vendor@36.9.2:
|
||||
resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
|
||||
victory-vendor@37.3.6:
|
||||
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
||||
|
||||
vm-browserify@1.1.2:
|
||||
resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==}
|
||||
@@ -5776,12 +5674,6 @@ snapshots:
|
||||
|
||||
'@keyv/serialize@1.1.1': {}
|
||||
|
||||
'@mdx-js/react@3.1.1(@types/react@19.3.0)(react@19.3.0)':
|
||||
dependencies:
|
||||
'@types/mdx': 2.0.14
|
||||
'@types/react': 19.3.0
|
||||
react: 19.3.0
|
||||
|
||||
'@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.0)(@emnapi/runtime@1.11.0)':
|
||||
dependencies:
|
||||
'@emnapi/core': 1.11.0
|
||||
@@ -5967,92 +5859,21 @@ snapshots:
|
||||
type-fest: 2.19.0
|
||||
webpack-hot-middleware: 2.26.1
|
||||
|
||||
'@storybook/addon-actions@8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
'@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1))(react@19.3.0)':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
'@types/uuid': 9.0.8
|
||||
dequal: 2.0.3
|
||||
polished: 4.3.1
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
uuid: 9.0.1
|
||||
|
||||
'@storybook/addon-backgrounds@8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
memoizerific: 1.11.3
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
ts-dedent: 2.3.0
|
||||
|
||||
'@storybook/addon-controls@8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
dequal: 2.0.3
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
ts-dedent: 2.3.0
|
||||
|
||||
'@storybook/addon-docs@8.6.14(@types/react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.1(@types/react@19.3.0)(react@19.3.0)
|
||||
'@storybook/blocks': 8.6.14(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
'@storybook/csf-plugin': 8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
'@storybook/react-dom-shim': 8.6.14(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
react: 19.3.0
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
ts-dedent: 2.3.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@storybook/addon-essentials@8.6.14(@types/react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
'@storybook/addon-actions': 8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
'@storybook/addon-backgrounds': 8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
'@storybook/addon-controls': 8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
'@storybook/addon-docs': 8.6.14(@types/react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
'@storybook/addon-highlight': 8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
'@storybook/addon-measure': 8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
'@storybook/addon-outline': 8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
'@storybook/addon-toolbars': 8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
'@storybook/addon-viewport': 8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
ts-dedent: 2.3.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@storybook/addon-highlight@8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
|
||||
'@storybook/addon-measure@8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
tiny-invariant: 1.3.3
|
||||
|
||||
'@storybook/addon-outline@8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
ts-dedent: 2.3.0
|
||||
|
||||
'@storybook/addon-toolbars@8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
|
||||
'@storybook/addon-viewport@8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
memoizerific: 1.11.3
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
|
||||
'@storybook/blocks@8.6.14(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
'@storybook/icons': 1.6.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
ts-dedent: 2.3.0
|
||||
'@standard-schema/spec': 1.1.0
|
||||
'@standard-schema/utils': 0.3.0
|
||||
immer: 11.1.18
|
||||
redux: 5.0.1
|
||||
redux-thunk: 3.1.0(redux@5.0.1)
|
||||
reselect: 5.2.0
|
||||
optionalDependencies:
|
||||
react: 19.3.0
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
react-redux: 9.3.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1)
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@standard-schema/utils@0.3.0': {}
|
||||
|
||||
'@storybook/builder-webpack5@10.6.0(clean-css@5.3.3)(esbuild@0.28.2)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.28)(sharp@0.35.4(@types/node@20.17.6))(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
@@ -6100,18 +5921,8 @@ snapshots:
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
ts-dedent: 2.3.0
|
||||
|
||||
'@storybook/csf-plugin@8.6.14(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
unplugin: 1.16.1
|
||||
|
||||
'@storybook/global@5.0.0': {}
|
||||
|
||||
'@storybook/icons@1.6.0(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
|
||||
dependencies:
|
||||
react: 19.3.0
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
|
||||
'@storybook/icons@2.1.0(react@19.3.0)':
|
||||
dependencies:
|
||||
react: 19.3.0
|
||||
@@ -6255,12 +6066,6 @@ snapshots:
|
||||
'@types/react': 19.3.0
|
||||
'@types/react-dom': 19.3.0(@types/react@19.3.0)
|
||||
|
||||
'@storybook/react-dom-shim@8.6.14(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))':
|
||||
dependencies:
|
||||
react: 19.3.0
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
storybook: 10.6.0(@types/react@19.3.0)(react@19.3.0)
|
||||
|
||||
'@storybook/react@10.6.0(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(storybook@10.6.0(@types/react@19.3.0)(react@19.3.0))(supports-color@8.1.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@storybook/global': 5.0.0
|
||||
@@ -6514,8 +6319,6 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/mdx@2.0.14': {}
|
||||
|
||||
'@types/node@20.17.6':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
@@ -6536,7 +6339,7 @@ snapshots:
|
||||
|
||||
'@types/semver@7.8.0': {}
|
||||
|
||||
'@types/uuid@9.0.8': {}
|
||||
'@types/use-sync-external-store@0.0.6': {}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.70.1(@typescript-eslint/parser@8.70.1(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@8.1.1))(supports-color@8.1.1)(typescript@5.9.3)':
|
||||
dependencies:
|
||||
@@ -6765,13 +6568,13 @@ snapshots:
|
||||
|
||||
'@xtuc/long@4.2.2': {}
|
||||
|
||||
'@xyflow/react@12.11.6(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
|
||||
'@xyflow/react@12.11.6(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(immer@11.1.18)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)':
|
||||
dependencies:
|
||||
'@xyflow/system': 0.0.82
|
||||
classcat: 5.0.5
|
||||
react: 19.3.0
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
zustand: 4.5.7(@types/react@19.3.0)(react@19.3.0)
|
||||
zustand: 4.5.7(@types/react@19.3.0)(immer@11.1.18)(react@19.3.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.3.0
|
||||
'@types/react-dom': 19.3.0(@types/react@19.3.0)
|
||||
@@ -7398,11 +7201,6 @@ snapshots:
|
||||
dependencies:
|
||||
utila: 0.4.0
|
||||
|
||||
dom-helpers@5.2.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.7
|
||||
csstype: 3.2.3
|
||||
|
||||
dom-serializer@1.4.1:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
@@ -7489,6 +7287,8 @@ snapshots:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
es-toolkit@1.52.0: {}
|
||||
|
||||
esbuild@0.28.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.28.2
|
||||
@@ -7607,7 +7407,7 @@ snapshots:
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
eventemitter3@5.0.4: {}
|
||||
|
||||
events@3.3.0: {}
|
||||
|
||||
@@ -7618,8 +7418,6 @@ snapshots:
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-equals@5.4.2: {}
|
||||
|
||||
fast-json-parse@1.0.3: {}
|
||||
|
||||
fast-json-stable-stringify@2.1.0: {}
|
||||
@@ -7877,6 +7675,8 @@ snapshots:
|
||||
|
||||
ignore@7.0.10: {}
|
||||
|
||||
immer@11.1.18: {}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
dependencies:
|
||||
parent-module: 1.0.1
|
||||
@@ -8103,10 +7903,6 @@ snapshots:
|
||||
|
||||
lodash@4.18.1: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
loupe@3.2.1: {}
|
||||
|
||||
lower-case@2.0.2:
|
||||
@@ -8141,8 +7937,6 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.8.5
|
||||
|
||||
map-or-similar@1.5.0: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
md5.js@1.3.5:
|
||||
@@ -8155,10 +7949,6 @@ snapshots:
|
||||
dependencies:
|
||||
fs-monkey: 1.1.0
|
||||
|
||||
memoizerific@1.11.3:
|
||||
dependencies:
|
||||
map-or-similar: 1.5.0
|
||||
|
||||
merge-stream@2.0.0: {}
|
||||
|
||||
micromatch@4.0.8:
|
||||
@@ -8300,8 +8090,6 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
object-is@1.1.6:
|
||||
@@ -8494,10 +8282,6 @@ snapshots:
|
||||
dependencies:
|
||||
find-up: 6.3.0
|
||||
|
||||
polished@4.3.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.7
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss-loader@8.2.1(postcss@8.5.28)(typescript@5.9.3)(webpack@5.110.3(clean-css@5.3.3)(esbuild@0.28.2)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.28)(sharp@0.35.4(@types/node@20.17.6))):
|
||||
@@ -8576,12 +8360,6 @@ snapshots:
|
||||
|
||||
process@0.11.10: {}
|
||||
|
||||
prop-types@15.8.1:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
public-encrypt@4.0.3:
|
||||
dependencies:
|
||||
bn.js: 4.12.5
|
||||
@@ -8641,31 +8419,21 @@ snapshots:
|
||||
react: 19.3.0
|
||||
scheduler: 0.28.0
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
react-redux@9.3.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@types/use-sync-external-store': 0.0.6
|
||||
react: 19.3.0
|
||||
use-sync-external-store: 1.7.0(react@19.3.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.3.0
|
||||
redux: 5.0.1
|
||||
|
||||
react-refresh@0.14.2: {}
|
||||
|
||||
react-smooth@4.0.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
|
||||
dependencies:
|
||||
fast-equals: 5.4.2
|
||||
prop-types: 15.8.1
|
||||
react: 19.3.0
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
react-transition-group: 4.4.5(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
|
||||
react-transition-group@4.4.5(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.7
|
||||
dom-helpers: 5.2.1
|
||||
loose-envify: 1.4.0
|
||||
prop-types: 15.8.1
|
||||
react: 19.3.0
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
|
||||
react@19.3.0: {}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
@@ -8702,28 +8470,37 @@ snapshots:
|
||||
tiny-invariant: 1.3.3
|
||||
tslib: 2.8.1
|
||||
|
||||
recharts-scale@0.4.5:
|
||||
dependencies:
|
||||
decimal.js-light: 2.5.1
|
||||
|
||||
recharts@2.15.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
|
||||
recharts@3.10.1(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react-is@18.3.1)(react@19.3.0)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1))(react@19.3.0)
|
||||
clsx: 2.1.1
|
||||
eventemitter3: 4.0.7
|
||||
lodash: 4.18.1
|
||||
decimal.js-light: 2.5.1
|
||||
es-toolkit: 1.52.0
|
||||
eventemitter3: 5.0.4
|
||||
immer: 11.1.18
|
||||
react: 19.3.0
|
||||
react-dom: 19.3.0(react@19.3.0)
|
||||
react-is: 18.3.1
|
||||
react-smooth: 4.0.4(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
|
||||
recharts-scale: 0.4.5
|
||||
react-redux: 9.3.0(@types/react@19.3.0)(react@19.3.0)(redux@5.0.1)
|
||||
reselect: 5.2.0
|
||||
tiny-invariant: 1.3.3
|
||||
victory-vendor: 36.9.2
|
||||
use-sync-external-store: 1.7.0(react@19.3.0)
|
||||
victory-vendor: 37.3.6
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- redux
|
||||
|
||||
redent@3.0.0:
|
||||
dependencies:
|
||||
indent-string: 4.0.0
|
||||
strip-indent: 3.0.0
|
||||
|
||||
redux-thunk@3.1.0(redux@5.0.1):
|
||||
dependencies:
|
||||
redux: 5.0.1
|
||||
|
||||
redux@5.0.1: {}
|
||||
|
||||
regenerate-unicode-properties@10.2.2:
|
||||
dependencies:
|
||||
regenerate: 1.4.2
|
||||
@@ -8761,6 +8538,8 @@ snapshots:
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
reselect@5.2.0: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve-url-loader@5.0.0:
|
||||
@@ -9173,11 +8952,6 @@ snapshots:
|
||||
|
||||
universalify@2.0.1: {}
|
||||
|
||||
unplugin@1.16.1:
|
||||
dependencies:
|
||||
acorn: 8.18.0
|
||||
webpack-virtual-modules: 0.6.2
|
||||
|
||||
update-browserslist-db@1.3.2(browserslist@4.28.9):
|
||||
dependencies:
|
||||
browserslist: 4.28.9
|
||||
@@ -9209,15 +8983,13 @@ snapshots:
|
||||
|
||||
utila@0.4.0: {}
|
||||
|
||||
uuid@9.0.1: {}
|
||||
|
||||
v8-to-istanbul@9.3.0:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
'@types/istanbul-lib-coverage': 2.0.6
|
||||
convert-source-map: 2.0.0
|
||||
|
||||
victory-vendor@36.9.2:
|
||||
victory-vendor@37.3.6:
|
||||
dependencies:
|
||||
'@types/d3-array': 3.2.2
|
||||
'@types/d3-ease': 3.0.2
|
||||
@@ -9363,9 +9135,10 @@ snapshots:
|
||||
|
||||
zod@4.6.5: {}
|
||||
|
||||
zustand@4.5.7(@types/react@19.3.0)(react@19.3.0):
|
||||
zustand@4.5.7(@types/react@19.3.0)(immer@11.1.18)(react@19.3.0):
|
||||
dependencies:
|
||||
use-sync-external-store: 1.7.0(react@19.3.0)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.3.0
|
||||
immer: 11.1.18
|
||||
react: 19.3.0
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
"cache": false,
|
||||
"outputs": []
|
||||
},
|
||||
"@nextcraft/ai-service#typecheck": {
|
||||
"cache": false,
|
||||
"outputs": []
|
||||
},
|
||||
"web#test": {
|
||||
"cache": false,
|
||||
"outputs": []
|
||||
|
||||
Reference in New Issue
Block a user