How it works
Four steps, each one derived from the last. Declare the structure once, and validation, navigation, templates, agent context and publishing all fall out of it. Every example below uses real documents and real output from this repository.
A shelf is a region of the tree that carries a purpose. A kind is what a document permanently is โ it names the facets a document must declare, the sections it must carry, and the relations it may declare. All of it is data in one versioned schema, so two organizations with different documentation cultures run the same engine over different taxonomies.
The split has a standard name. Description logic calls the terminology a TBox and the assertions an ABox. A taxonomy is the terminology, and a corpus is the assertions. That is why the validation of a taxonomy and the check of a corpus are two different operations.
Resolution merges the base package with the overlays above it, validates the result against a meta-schema, and writes a content-hashed lock. Everything downstream reads the lock and never the sources. Your overlay adds what a package cannot know: your namespace, your phrases, your extra kinds. Customizing the taxonomy never means forking the tooling.
# .headwater/packages/headwater-standard/taxonomy.yml shelves: decisions: docs/decisions/** kinds: decision: purpose: rationale facets: [status, summary, last_verified] sections: [Context, Decision, Consequences] relations: [supersedes, governs, traces_to] # .headwater/overlay.yml โ yours add: identifier_schemes.decision_id.namespace: ACME
The engine parses each file once and builds one typed graph. Documents are typed nodes. Front-matter references are typed edges, and an edge names its target by identifier rather than by path โ a path dies at the first rename and an identifier does not. A relation that requires both ends owes both ends, and the engine writes the far half for you.
The build also emits a census, which is every file under the corpus root and what became of each one. The census fixes the denominator before any check runs, so a document that failed to classify is counted, reported, and visibly unchecked, rather than absent.
No language model issues a verdict. The graph is what the authors said, checked, and not what a model guessed. Nothing stores the graph either: every run rebuilds it from the documents and the lock, and a cache only makes that derivation cheap.
A check is a pure function from a scoped view of the graph to a set of findings: it reads no file, opens no socket, and reads no clock. Each finding names the rule that fired, the location, the repair, and โ in parentheses โ the obligation it discharges.
An obligation is a claim the system makes about itself, with the rule that verifies it named beside it. A run reaches that claim in two phases, in an order that fixes a failure mode: phase A classifies every file and builds the graph, and phase B runs the checks over what phase A produced. The census is fixed first, so a document that fails to parse is counted and reported rather than dropped behind a clean result.
A rule is an error when its repair takes no judgment, and advisory when the repair is a rewrite. headwater check --fix applies every mechanical repair in one command, and it never touches an advisory finding โ a plausible automatic fix for a defect that needs judgment would be worse than none, because it would be applied unread.
docs/decisions/0002-deliver-at-least-once.md:9:7 error
relation.reciprocity.missing (OB-REL-1): `ACME-DR-0002` declares `supersedes: ACME-DR-0001`, and `supersedes` requires both ends, so 0001-store-attempts-in-postgres.md owes `superseded_by`
fix (mechanical): add `superseded_by: ACME-DR-0002`
register
51 obligations: 48 verified, 2 gap, 1 unverifiable
each gap names its owner
every rule this engine carries reaches one obligation
That block is this repository's own run of 2026-10-02. The self-assessment carries the rest of it.
Shelf indexes, decision-lineage views, site navigation, agent instruction context โ generated from the graph, never maintained by hand. If a person can change a generated file and the system does not see it, that file will drift and readers will trust a file that is wrong.
So the system checks that generated files agree with their sources. headwater generate --check fails the build when the two drift apart. This repository's own specification index and verb index both work that way.
A projection may be filtered, and a filtered projection says so. An export profile names an audience and a filter over facet values, and what it carries and what it withholds partition the corpus. A document held back is a loss with a declared reason, and the census reports it like any other loss. No profile may produce a view that presents itself as total.
The assertions here are about documents. This document exists, it is of this kind, it governs that code path, it supersedes that other document. They are not about the claims inside the prose.
The service returns 404 on a missing key is a sentence. The system knows the document that contains it, and what that document governs. It does not know what the sentence asserts about the world. Reasoning stops at the document boundary, and a promise of semantic consistency past that boundary is a promise that this project cannot keep.
The four steps above describe the default shape: a shelf lives where the standard package puts it. An adopter with an existing tree does not have to match that shape โ an overlay relocates a shelf with one override key, though the move breaks every relative link into the old path until you repair them.
Relocate a shelf walks through the whole move, on this repository's own corpus, with the real output of every command.
Sixteen steps take an empty directory to a passing strict check, and every output block in the tutorial is held against the run that produced it โ no example on it can go stale quietly.