ADR 0181: An SSOT claim must anchor a declared canonical set
Status: accepted; extends the meta-registry contract.
Context
§ADR 0176 holds the canonical-set discipline to the repository in both directions, but its reverse sweeps see only the pattern's conventional footprint: guard tests matching five filename suffixes, codegen write targets, and formerly the fmt-exclusions. The decision named the hole honestly — a registry that grows none of that footprint is invisible to the sweeps. A July 2026 audit proved the hole real rather than theoretical: eight canonical sets lived outside the registry precisely because their class tests are named outside the convention (patterns_test.ts, improve_catalog_test.ts, engine_consent_gate_test.ts, and kin). The audit enrolled the instances; nothing closed the class, and a ninth escape was a matter of time.
The escape route has a signature. Every registry in this codebase announces itself in its doc comments — "the single source of truth for …", "the SSOT for …" — because the announcement is how an author tells the next reader not to fork the set. That is the one footprint a registry grows at the moment it is born, before any test or artifact exists, and it is authored intent rather than inferred shape.
Decision
§The claim itself is the enrolment trigger. A gate sweep (tests/ssot_claim_guard_test.ts, named inside the suffix convention so the existing reverse sweep polices it) scans the authored-TypeScript universe (AUTHORED_TS_FILES). A module whose module-level or export-attached doc blocks claim single-source-of-truth status for the module's own exports must be exactly one of two things: some entry's source.module in scripts/canonical_sets.ts, or a recorded absence in the new UNAFFILIATED_SETS ledger — module path (or path#EXPORT) to reason, mirroring UNAFFILIATED_GUARDS, rendered in the registry atlas beside the other unaffiliated records.
The matcher is conservative by construction. It fires on three claim shapes — predicative ("the SSOT for the mode vocabulary"), appositive after an em-dash ("The discern version — single source of truth."), and copular ("this table is the single source of truth") — and never on reference forms: a parenthetical aside ("iterates KNOWN_JOBS (the SSOT)"), a possessive holder ("the config's single source of truth"), or a sentence naming another module. Test files are excluded; their prose describes the sets they guard, never sets they own. Controls hold each shape and each veto, both sweep directions, and the staleness of every ledger record — a record whose module no longer exists, no longer claims, or lost its named export fails the gate. An anti-rot floor pins the matcher to the enrolled registries' own live claims, so it cannot silently go blind.
The explicit noes: the sweep reads claim text only — it never imports, executes, or shape-analyzes a scanned module, and ADR 0176's rejection of registry-shaped-export detection stands. There is no denylist of the phrase, and rewording a comment to dodge the sweep is the one move this decision forbids culturally: the honest answer to a fired claim is a declaration or a recorded reason, never quieter prose.
Consequences
§- The blind spot that let eight sets escape is structurally closed at its cheapest point: the next author who writes "single source of truth" above a new registry is stopped by the gate until the registry is declared or its absence reasoned. Enrolment friction lands exactly when the set is born.
- The residual is named: a claim phrased outside the three shapes, made in a line comment, or never written at all evades the sweep. The suffix conventions, the codegen chokepoint, and authoring discipline remain the other legs; this adds the earliest tripwire, not a replacement.
- Claim prose becomes load-bearing. Rewording a recorded module's doc comment can stale its ledger record, and the gate fails until the record follows. That is deliberate — records may never rot — but it makes comment edits on recorded modules slightly costlier.
- The first population surfaced two records that are enrolment debts, not settled absences:
src/engine/worktree/side_restrictions.tsandsrc/shared/git_admin_state.tsare true registries whose class tests are named outside the convention. They are recorded as candidates for enrolment; declaring them is an owner decision for a later change.
Alternatives considered
§- Widen the suffix conventions, or rename the eight escaped tests into them. Rejected: renames churn test history for no behavioral gain, and the next author who names a class test naturally (
patterns_test.ts) re-opens the hole. The convention polices names, and names are exactly what escaped. - Sweep for registry-shaped exports. Already rejected by ADR 0176, and the reasons hold:
export const X = […] as constis shape, not intent, and false enrolment demands teach authors to game the detector. - A structured annotation instead of prose matching. A tag would be precise, but it is a second vocabulary to teach, migrate onto, and police — and its absence is as silent as the hole it replaces. The prose claim already exists in every registry written to date; reading it costs authors nothing.