How it works

Taxonomy โ†’ graph โ†’ checks โ†’ projections.

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.

1 ยท taxonomy
taxonomy.yml overlay.yml taxonomy.lock
2 ยท graph
typed nodes typed edges ACME-DR-0001
3 ยท checks
census findings obligation register
4 ยท projections
indexes site nav agent context graph export

1 ยทDeclare the taxonomy

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

2 ยทAuthors declare the graph

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.

0002-deliver-at-least-once.md
supersedes โ†“
โ†‘ superseded_by
0001-store-attempts-in-postgres.md

3 ยทEvery rule constrains that graph

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.

4 ยทEverything derived is a projection

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 graph, projected:
docs/spec/README.md โ† shelf index
docs/interfaces/README.md โ† verb index
decision lineage โ† supersedes chains
site navigation โ† MkDocs configuration
agent instruction context โ† per task, per shelf
$ headwater generate --check

Where the reasoning stops

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.

Advanced usage

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.