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:
tests/test_route_versioning.pywalksapp.routesand asserts every path matches^/api/v\d+/or is in the explicitUNVERSIONED_ALLOWLIST(FastAPI's auto-doc routes only); FastAPI's response model inference checks the typed-response side at request time.
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.
Add a new ## N. <rule> section at the bottom of slots 6+. Each entry has three lines:
- The rule, one sentence.
- Where: module / config file path.
- Enforced by: test name, CI job, or
reviewif no automated check exists yet.
If the invariant is not yet automated, add a marker line whose first non-whitespace characters are *Aspirational or **Aspirational** — both shapes are recognised by the Aspirational ticket cite CI job (.github/scripts/check_aspirational_tickets.py). The marker line MUST cite at least one #NNN ticket; the gate fails CI otherwise. When the cited ticket closes, promote the invariant to enforced in the same PR (delete the marker line, fill in Enforced by:). Set ASPIRATIONAL_STRICT=1 on the gate's CI job to escalate closed-cite drift from ::warning:: to a hard failure.
Use **Production note:** (not **Aspirational**) for forward-looking product evolution that is NOT a future-enforced rule — e.g. "this changes when multi-tenant lands". Production notes are not picked up by the gate.