Files
nextcraft/apps/ai-service/pyproject.toml
T
CIAgent dc00a4f8f5 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---
2026-09-27 10:08:07 +00:00

146 lines
6.3 KiB
TOML

[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "nextcraft-ai-service"
version = "0.6.3"
description = "Nextcraft AI tutor service — six LLM agents behind a provider-agnostic layer"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.141,<0.142",
"uvicorn>=0.52,<0.53",
"pydantic>=2.13,<2.14",
"pydantic-settings>=2.15,<2.16",
"httpx>=0.28,<0.29",
"sse-starlette>=3.4,<3.5",
"sqlmodel>=0.0.24,<0.1",
"sqlalchemy>=2.0,<2.1",
"websockets>=13,<16",
"aiofiles>=24.1,<26",
# POST /v1/defense/{id}/answer multipart audio (REQ-3-006): FastAPI
# form/File parsing requires python-multipart at runtime.
"python-multipart>=0.0.32,<0.1",
# P4-B resume parse (REQ-6-005, D-054, A-410): PDF text extraction.
# Pure-Python, probe-verified 6.19.0 — the ONLY new v0.6 dependency.
"pypdf>=6.19,<7",
]
[project.optional-dependencies]
dev = [
"pytest>=9.1,<10",
"pytest-asyncio>=1.4,<2",
"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]
include = ["ai_service*"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
# D-056: pin the fixture loop scope — pytest-asyncio 1.4 warns at configure
# time when this is unset and a future default flip would change loop reuse
# for async fixtures. "function" is the current-effective default semantics.
asyncio_default_fixture_loop_scope = "function"
testpaths = ["tests"]
# P1 harness wiring (D-053/D-054): strict markers + a 60s global timeout.
# `timeout` also ships its own marker (`pytest.mark.timeout`); the 180s
# override for real_server/userns tests is applied by the collection hook
# in tests/conftest.py — marker-level values beat this global default.
markers = [
"fast: in-process unit test (default tier — unmarked tests are fast)",
"real_server: spins a real uvicorn server process",
"userns: enters a real unshare user namespace",
]
addopts = "--strict-markers --timeout=60 --timeout-method=signal"
# P3 zero-warning policy (D-056): base action `error` — every warning fails
# the run; new first-party warnings can never pass silently. The ONLY
# entries after "error" are the 2 known upstream import-time deprecations,
# each scoped by exact message prefix + emitting module (the module whose
# import triggers the warning, i.e. the frame the warning is attributed to
# after stacklevel adjustment — NOT the package the warning text mentions):
#
# 1. fastapi.testclient re-exports starlette's TestClient, and importing
# it emits starlette's httpx→httpx2 migration warning (stacklevel=2
# attributes it to fastapi/testclient.py's import line).
# 2. starlette.testclient resolves `anyio.abc.BlockingPortal` in a
# module-level type alias; anyio's lazy-import __getattr__ emits the
# alias-deprecation (stacklevel=3 attributes it to starlette's
# testclient.py).
#
# G-104 invariant: category-scoped ignores and ANY ignore matching
# ai_service.* are forbidden forever; new upstream deprecations may join
# this list only as module+message-scoped entries, justified in ship notes.
# Fields: action:message-regex:category:module-regex:lineno (pytest parses
# ini entries as raw regex — dots below are explicitly escaped).
filterwarnings = [
"error",
# Upstream (1/2): starlette 1.6 httpx→httpx2 migration notice, raised
# while importing fastapi.testclient (importer frame is fastapi's).
"ignore:Using `httpx` with `starlette\\.testclient` is deprecated:starlette.exceptions.StarletteDeprecationWarning:fastapi\\.testclient",
# Upstream (2/2): anyio 4.x lazy-import alias deprecation, raised while
# starlette.testclient evaluates its `_PortalFactoryType` alias.
"ignore:The anyio\\.abc\\.BlockingPortal alias is deprecated:DeprecationWarning:starlette\\.testclient",
]
[tool.coverage.run]
source = ["ai_service"] # tests/ excluded by construction — only the package is measured
branch = true
[tool.coverage.report]
# exclude_also APPENDS to coverage.py's built-ins (which already cover
# `pragma: no cover` and `if TYPE_CHECKING:`); these add the __main__
# entry-point guard (none in ai_service today — future-proof).
exclude_also = [
"if __name__ == [\"']__main__[\"']:",
"if TYPE_CHECKING:",
"pragma: no cover",
]
[tool.ruff]
line-length = 100
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"]
# 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