product/ is a directory of markdown files with frontmatter. Features, specs,
decisions, constraints, business rules, terms, and a roadmap that references
them. It is validated on every pull request, and a broken graph fails the build.
That is the whole idea. The interesting part is what "broken" means.
What the validator checks
Structure, first: a roadmap item must resolve to a feature file, a feature must
appear in exactly one horizon, a links.spec must exist on disk, a
governed_by must resolve to a decision.
Then the part that does the work — every acceptance criterion names tests, and every test reference must resolve to a real test:
acceptance:
- text: >-
A refund never takes back credit that was never given.
tests:
- tests/site/test_credits.py::TestReversalNeverExceedsCredit::test_a_refund_with_no_credit_behind_it_is_refused
Rename that class and the build fails with PAC-902. Not a warning in a report
nobody reads — a red check on the pull request that renamed it.
Why that specific check earns its place
It is the one that catches drift in the direction drift actually happens.
Nobody edits a spec to make it wrong. What happens is that the code changes and the document quietly stops describing it — and there is no natural moment at which anybody re-reads a paragraph about a feature they are not currently working on.
A test reference is a claim that is resolvable. It is the one part of a specification a machine can check against reality, so it is the thread that keeps the rest attached.
It caught us twice in one day. Moving the starting grant from sign-up to email verification renamed two test classes, and the gate failed until the feature file was corrected — which forced re-reading the acceptance text, which turned out to say "a new account receives" where it now had to say "a verified account receives". That sentence was the thing that changed, and the mechanical check dragged a human to it.
Authority flows one way
product/ defines what. Delivery proves how, and reports contradictions
back as diagnostics for a person to act on. Delivery never silently rewrites
intent.
The distinction matters because the tempting automation is the other direction: generate the spec from the code, so it can never be out of date. That produces documents that are always accurate and never normative — a description of what was built rather than a statement of what was agreed, and therefore useless for noticing that what was built is not what was agreed.
The definition of done
status: shipped is gated. A feature claiming it needs a PR link, a code link,
a resolving test per acceptance criterion, a deployment manifest, and a monitor
that watches it.
Everything in this repository is building or idea. Nothing is shipped,
including features that have been in production for weeks and are working.
That looks like pedantry and is the point. shipped means someone can tell if
this breaks, and until there is a named probe in the monitoring script, nobody
can. Being unable to claim it is the mechanism working.
What it costs
Every feature is a file before it is code, and the file is reviewed. Adding
payment-reversals meant writing why refund could not carry a negative
amount before writing the migration that added reversal.
That is slower. It is also where the actual thinking happened — the argument that a kind with two directions turns "unrepresentable" into "we are careful" was made in a spec, and the code that followed was the easy part.
The engine is not ours
The validator is KeelSpec, pinned
exactly. This repository's own rules — the definition of done, roadmap
resolution, traceability — are a plugin, not a fork. Diagnostic codes 0xx–06x
are the library's; 9xx are ours.
Codes are an API: retire, never renumber. There is a decision record about the one renumbering that adopting the library forced, and why it happens exactly once.