docs_lint rule 5: MANIFEST.md coverage, both directions - #591
Merged
Conversation
docs/MANIFEST.md is the by-surface ROUTING table (path / purpose / governs-what-surface / authority). Its own maintenance note said a row "is not yet hard-enforced (deliberately deferred ... measure whether a doc actually goes stale unnoticed before promoting this to a gate)". That measurement came back: EIGHT real docs had no row and no covering directory row, including features/stage-e-operations.md — the operational runbook for Stage E streaming runs and envelope-trip clearing. Nothing said so, because nothing was looking. The existing orphan rule targets this class and passed on every one of them, correctly: they are all reachable by some link from some sibling. Reachable and ROUTABLE are different properties, and the table exists to provide the second. Extending the orphan rule would have conflated them, so this is a fifth interconnection rule with its own message. Checked in the other direction too: a row whose path does not resolve. Nothing checked that either — the generic backtick-path rule requires a "/" in the candidate, so a top-level row like `theory.md` was never verified to exist at all. Nine rows added, drafted from each doc's own self-description rather than assigned: five features/ docs and reference/funnel-spec.md (historical — it self-declares "not a living spec"), identification-pipeline.md (reference — it self-declares as a companion to theory.md and hands off every normative surface), and one upstreaming/drafts/ directory row. Two index roots (README.md, MANIFEST.md) are allowlisted with per-entry reasons: a row for the routing table inside the routing table routes nobody anywhere. An unparseable MANIFEST table is a finding, not a pass — deriving an empty covered-set would check nothing and go green. MANIFEST.md's maintenance note and documentation-process.md's rule list (which said "four rules") are both updated; a doc asserting a check that does not exist is the same defect class this rule closes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
docs/MANIFEST.mdis the by-surface routing table —path|purpose|governs-what-surface|authority— so a task can be routed to its governing docs without anyone remembering which file covers what.Its own Maintenance section said:
That measurement came back. Eight real docs had no row and no covering directory row:
features/stage-e-operations.mdfeatures/theming.mdfeatures/display-left-rail.mdfeatures/foreign-order-resilience.mdfeatures/consent-toast.mdfeatures/bleed-measurement.mdidentification-pipeline.mdreference/funnel-spec.mdSix of the eight are BINDING-grade. Nothing said so, because nothing was looking.
Why not extend the orphan rule
The existing orphan rule targets this class and passed on every one of them — correctly. They are all reachable by some link from some sibling.
Reachable and routable are different properties. A doc reachable only through a see-also link buried in a sibling is reachable and unroutable at the same time, and this table exists to provide the second. Folding the check into the orphan rule would conflate the two and produce a message that names the wrong fix, so this is a fifth interconnection rule with its own message.
Both directions
The second half is a gap nobody had noticed at all: a row whose path does not resolve. The generic backtick-path rule requires a
/in the candidate, so a top-level row like`theory.md`was never verified to exist. A rename would have left the routing table silently pointing at nothing.An unparseable MANIFEST table is a finding, not a pass — deriving an empty covered-set would check nothing and go green.
The nine rows
Drafted from each doc's own self-description, not assigned by guesswork. Two examples of the judgment:
reference/funnel-spec.md→ historical, because it says so itself: "Reference only; not re-derived or updated after ratification — it is a point-in-time record of the ruling, not a living spec", and points atfeatures/grid-selector.mdas the living authority. Same shape as the already-historicalreference/vote-weight-matrix.md.identification-pipeline.md→ reference, the one genuinely arguable case. It describes current behaviour accurately and is actively maintained, but consistently positions itself downstream oftheory.md,pipeline-fidelity-gate.md,features/printing-tags.mdandfeatures/catalog-completion-plan.md— all already BINDING for those surfaces. Marking it BINDING would create four overlapping authorities for one surface. Flagging this as the row most worth a second opinion.Plus one
upstreaming/drafts/directory row (both files there self-declare a prep/branch-cut status; the binding checklist for such a branch isupstreaming/conventions.md, not the drafts).Allowlist: the two index roots only, per-entry-justified. A row for the routing table inside the routing table routes nobody anywhere.
Two stale doc claims fixed — same defect class
MANIFEST.md's Maintenance section still said the row convention was unenforced. It now records what changed and why, including that its own deferral's precondition was met.documentation-process.mdsaid docs_lint enforces the model with "four rules". It is five.Can it fail? Four demonstrations, restored to clean after each
docs/features/_demo-new.md):upstreaming/drafts/files the directory row covers).docs/reports/_demo.md):docs-lint: clean. exit=0.Verification
python3 .github/scripts/docs_lint.py --strict— clean.python3 .github/scripts/tests/test_docs_lint.py— 50 tests, OK (was 39 on master). NewTestManifestCoveragecovers: missing row (top-level and nested), directory-row coverage including nested depth, a sibling prefix not being satisfied by a similar directory row (reports/must not coverreports-archive/), unresolvable file row, unresolvable directory row, index-root allowlisting, allowlist exemption, header-separator rows ignored, a table row inside a fenced block not counted as a row, unparseable manifest, and a missing manifest not exploding.pre-commit run(ruff/isort/black/mypy/prettier) — passed.Nothing under
MPCAutofill/changed, so the backend suite was not re-run.Note on ordering
This branch is cut from
masterindependently of #587/#588/#590. #588 also editsdocs-lint.yml's path filters; this PR does not touch that file, so there is no conflict between them.🤖 Generated with Claude Code
https://claude.ai/code/session_013NhYmT1PxCcyemA16dFDxN