The numbered rules below are load-bearing. Every PR is checked against them; CI fails the build when one is violated. Add project-specific invariants in slots 6+ as your domain accretes.
Pydantic with extra="forbid" raises on unknown keys at construction. That kills the silent-key class of bug at the seam instead of three calls deep.
- Where:
src/models/_base.py - Enforced by:
tests/test_models.py(assertsextra="forbid"); review.
A versioned prefix means future breaking changes ship at /api/v2/ without coordinated client deploys. Typed responses mean an OpenAPI schema is correct by construction.
- Where:
src/api/routes.py - Enforced by: route review; FastAPI's response model inference.
api | eval → agent → tools → data → observability → models. src.models imports nothing from src/. A reverse import collapses the layer story.
- Where:
pyproject.toml[tool.importlinter] - Enforced by:
lint-importsjob in CI;just architecturelocally.
Below 75 % the test suite stops being a meaningful gate; above ~90 % every PR slows down on coverage paperwork. 75 % is the load-bearing floor.
- Where:
pyproject.toml[tool.coverage.report] fail_under = 75 - Enforced by:
Coveragejob in CI.
Three independent checkpoints (PreToolUse hook → pre-commit gitleaks → CI gitleaks) catch staged secrets before push, force-push, or merge. Once a secret is in the remote it is compromised forever.
- Where:
.claude/hooks/pretooluse_bash.py,.pre-commit-config.yaml,.github/workflows/security.yml - Enforced by: the three layers above.
Add invariants below as your domain stabilises. Each entry should describe:
- The rule, in one sentence.
- Where it lives (module or config file path).
- Enforced by: test, review, or specific CI job.
Examples of the kind of invariant that earns a slot here: a domain-specific data contract that must validate at ingestion, a security boundary that must not log PII, a tool-call protocol that the agent must follow before the LLM emits a final response.