Skip to content

Latest commit

 

History

History
64 lines (37 loc) · 3.62 KB

File metadata and controls

64 lines (37 loc) · 3.62 KB

Invariants

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.

1. Every contract crossing a module or process boundary is a StrictModel

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 (asserts extra="forbid"); review.

2. API endpoints live under /api/v1/ and return typed responses

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.py walks app.routes and asserts every path matches ^/api/v\d+/ or is in the explicit UNVERSIONED_ALLOWLIST (FastAPI's auto-doc routes only); FastAPI's response model inference checks the typed-response side at request time.

3. Layer flow is one-way

api | evalagenttoolsdataobservabilitymodels. src.models imports nothing from src/. A reverse import collapses the layer story.

  • Where: pyproject.toml [tool.importlinter]
  • Enforced by: lint-imports job in CI; just architecture locally.

4. Coverage ≥ 75% on src/

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: Coverage job in CI.

5. No secret leaves the repo unscanned

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.

6+. Project-specific invariants

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.


How to add an invariant

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 review if 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.