make install # venv + dependencies
make check # lint + typecheck + unit tests. No infrastructure needed.
.venv/bin/pre-commit installmake help lists every target.
A change is not done when it works locally. All of these must hold:
- Implementation complete, with no
TODOstanding in for missing behaviour - Tests written alongside the code, covering the failure modes as well as the happy path
-
make checkpasses: ruff, black, mypy strict, unit tests - Integration tests pass if the change touches persistence
- Errors handled explicitly; nothing swallowed to make a test pass
- Logging added for anything operationally interesting, with the canonical context keys
- Configuration externalised, not hardcoded
- Documentation updated: docstrings, plus the relevant
.mdif behaviour or design changed - Security implications considered — for untrusted input, explicitly
- An ADR added if an architectural decision was made
- For a substantial architectural or algorithmic change: does it create potentially valuable new technical IP? Usually no. If possibly yes, see docs/ip/
- Migration written and reversible if the schema changed
Short-lived branches off main:
feature/cpwd-crawler
feature/download-queue
fix/pdf-parser-timeout
Small, reviewable commits. One concern per commit; do not bundle unrelated changes. Commit messages describe what changed and why, not what file was touched.
CI must be green before merge. Do not disable a check to get a merge through — if a rule is wrong, change the rule deliberately in its own commit.
tests/unit/ No database, no network. Must stay fast (currently ~0.5s).
tests/integration/ Real PostgreSQL. Marked, and skipped when unavailable.
A skipped test must never look like a verified one. Set
REQUIRE_INTEGRATION_TESTS=1 (as make test-integration and CI do) to turn an unexpected skip
into a failure. Unit tests must never depend on whether infrastructure happens to be running
locally — an early readiness test did, and it silently inverted meaning the day a database was
installed.
Principles:
Test behaviour, not implementation. A test that breaks on every refactor is a liability.
Name the failure mode the test prevents. Prefer a docstring explaining why a test exists over a name restating the assertion. Compare:
def test_empty_payload_is_rejected(self) -> None:
"""Every empty file shares one digest, which would corrupt deduplication."""Write the regression test before the fix. When fixing a bug, reproduce it first, so you have evidence the fix works and a guard against its return.
Never weaken a test to make it pass. If a test fails, either the code is wrong or the test encodes a wrong expectation. Decide which, and say so in the commit message.
Do not suppress exceptions to get green. A swallowed error in an auditing system is a silently wrong answer.
- Strict typing. Avoid
Any; if it is genuinely needed, comment why. - Functions with one responsibility, typed parameters and returns, predictable side effects.
- Docstrings explain why, not what — the signature already says what.
- Comments earn their place by explaining a non-obvious decision or a rejected alternative. Do not narrate the code.
- Line length 100. Enforced.
See DATA_SOURCES.md. In short: add the YAML entry, leave it
enabled: false, review the terms of use as a separate documented step, and only then write a
crawler. The registry schema will refuse to enable it out of order.
make migration m="add findings table" # autogenerate against a running database
make migrate # apply
make downgrade # verify it reversesReview the generated file — autogenerate misses check constraints, server defaults, and data
migrations. The downgrade() function must be real; a test asserts every revision has
operations in it.
Adding one requires a reason. Prefer the standard library. Remove anything that stops being used — every dependency is future attack surface.
Two steps, always:
# 1. Declare intent: add the range to pyproject.toml
# 2. Re-resolve the lock
make lockmake install and CI both use uv sync --locked, which fails if the lock has drifted from
pyproject.toml. So forgetting step 2 is caught locally, not in review.
Read pyproject.toml for intent; treat uv.lock as generated output. A dependency change is
also reviewed by dependency-review-action on the PR, which blocks high-severity CVEs and
copyleft licences before merge — see ADR 0009.