Project-Template/docs/DOC_TRUST_MAP.md

220 lines
12 KiB
Markdown
Raw Permalink Normal View History

# Doc Trust Map — which document to believe
```
Status: Current
Owner: <who maintains this>
Last reviewed: <YYYY-MM-DD>
Governs: docs/**
Review trigger: Any doc added, deleted or moved; any change to which doc owns a subject
fix(tools): doc-triggers matched the glob but not the trigger's verb DOC_TRUST_MAP.md declares `Governs: docs/**`, the broadest glob in the tree, while its Review trigger is one of the narrowest -- any doc added, deleted or moved. Matching on the glob alone fired it on every edit to every document, forever, and correctly by the only rule the tool had. Touching one script fired three documents and exactly one of them applied. A prompt that always fires is one people stop reading, and it takes the true positives with it. This tool exits 0 by design -- it is a prompt, not a gate -- which makes it more vulnerable to that, not less, because nothing forces the reading. Documents now declare the kinds of change their trigger names, in an optional `Fires on:` header field, read against git's own status letter. Absent, empty or unparseable means every kind, so nothing changes for the six other path-governing documents and a document is only ever quietened by somebody writing the line deliberately. ## Why declared rather than read out of the trigger prose The obvious first cut is to look for added/deleted/moved with no changed/change to. Tried against the seven path-governing documents here, it misclassifies the one it exists to fix: DOC_TRUST_MAP.md's trigger ends "any change to which doc owns a subject", so it reads as a change-verb. That clause is about which document owns a subject, not about a file being edited, and nothing lexical separates it from architecture/README.md's "any change to a module boundary or a data shape", which genuinely does mean modification. Guessing at English is silent in the expensive direction: a document wrongly read as existence-only stops being prompted for and goes quietly stale, which is the failure this whole tool exists to prevent. So the narrowing is declared or it does not happen. ## Also changed_paths now carries a status letter per path, from --name-status for --staged and --range and from the porcelain columns for the working tree. Paths named on the command line have no diff to read, so the kind is inferred: absent from disk is a deletion, present but untracked is an addition, otherwise a modification. Documents that govern a path in the change but do not fire on its kind are named in their own short block rather than dropped, because a reader who saw nothing would have to guess whether they had been considered. The no-match message now distinguishes "nothing governs these paths" from "governed, but not this kind of change" -- the second is a declaration somebody wrote, not an unclaimed area. Verified: modifying a script fires TOOLS.md and architecture/README.md and not DOC_TRUST_MAP.md; adding, deleting and moving a document under docs/ each still fire it; modifying a document fires nothing; an unknown word warns and fires on everything; an empty or absent field fires on everything. closes #20 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:50:21 -05:00
Fires on: added, deleted, moved
```
> Write this file **last**, once the rest exists, and describe what is actually
> here rather than what a template said should be. Its whole value is being
> accurate about the others.
## Work items are not in this tree
The sequence of work, the open defects, and the things blocking a release all
live in this repository's **issue tracker**, not in `docs/`. Milestones are
batches; issues are deliverables; severity labels are `P0`, `P1`, `P2` and
`release-blocker`.
This section exists to stop the next contributor starting a fresh markdown
to-do list. A list of things to do in two places is two records that will
disagree, and nothing will say which one is right.
| Question | Answer lives in |
| --- | --- |
| What are we building, and for whom? | `docs/planning/PROJECT_PLAN.md` |
| What is the sequence of work? | milestones in the tracker |
| What is left in this batch? | open issues under that milestone |
| What is broken right now? | issues labelled `P0` / `P1` / `P2` |
| What makes a release wrong? | issues labelled `release-blocker` |
| What is the next action? | the `nextAction` field on the project at privacyllc.dev — the newest entry in `docs/history/DEVELOPMENT_LOG.md` says what it was *then* |
| What is blocking us? | the tracker, for the work; the blockers table at privacyllc.dev, for the stakeholder-facing version |
| What do I do when a piece of work is finished? | `docs/WORK_CYCLE.md` |
2026-08-17 22:57:20 -05:00
| What watches this in production, and what do I do when it breaks? | `docs/OPERATIONS.md` |
| What happened, and when? | `docs/history/DEVELOPMENT_LOG.md` |
| Did QA pass, and what does the tester think? | `docs/qa/ClaudeReport.md` |
| What did QA actually reach? | `docs/qa/ClaudeQACoverage.md` |
| How is it built? | `docs/architecture/` |
fix(docs): three documents named the template's layout, not the project's The remaining half of #16 was not a decision. It was three documents being wrong about their own audience. A freshly scaffolded project failed doc-claims on four PATH claims: docs/architecture/scripts (twice), docs/architecture/githooks, and docs/architecture/scripts/release.sh -- named by DOC_TRUST_MAP.md, TOOLS.md and WORK_CYCLE.md. I had modelled that as a tension between documents that were correct and a scaffold that declined to create what they named, and filed it needing a call from Kaspa between three unattractive options. The evidence says otherwise. Every script's own header reads "Copy to `scripts/<name>`", the hooks install to `.githooks/`, and FIVE documents already use that project-relative form -- OPERATIONS.md, architecture/README.md, GUARDS.md and parts of TOOLS.md and DOC_TRUST_MAP.md. Only three used `docs/architecture/...`, which is where the scripts live in THIS repository and nowhere a project that adopts them will ever look. So the documents now name the layout their reader will actually have. No tooling change, no empty directories, and the claims get more accurate rather than vaguer -- the opposite of the direction I was leaning. Verified both ways, since a fix that only works in one tree is what produced the bug: the template stays green at 112 claims, and a freshly scaffolded project committed and checked exits 0 for the first time, with the bare-filename notes from f5fd67b reported as information rather than failure. Worth recording why this was invisible from inside: every path in question resolves here. The documents were only wrong from a vantage point this repository does not have, which is why scaffolding into a scratch directory found it and reading it here never would. closes #16 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 00:16:48 -05:00
| How do I release, back up, or check this? | `scripts/` |
| Which script do I run, and can it stop me? | `docs/TOOLS.md` — the signpost; `docs/architecture/README.md` has the table |
fix(docs): three documents named the template's layout, not the project's The remaining half of #16 was not a decision. It was three documents being wrong about their own audience. A freshly scaffolded project failed doc-claims on four PATH claims: docs/architecture/scripts (twice), docs/architecture/githooks, and docs/architecture/scripts/release.sh -- named by DOC_TRUST_MAP.md, TOOLS.md and WORK_CYCLE.md. I had modelled that as a tension between documents that were correct and a scaffold that declined to create what they named, and filed it needing a call from Kaspa between three unattractive options. The evidence says otherwise. Every script's own header reads "Copy to `scripts/<name>`", the hooks install to `.githooks/`, and FIVE documents already use that project-relative form -- OPERATIONS.md, architecture/README.md, GUARDS.md and parts of TOOLS.md and DOC_TRUST_MAP.md. Only three used `docs/architecture/...`, which is where the scripts live in THIS repository and nowhere a project that adopts them will ever look. So the documents now name the layout their reader will actually have. No tooling change, no empty directories, and the claims get more accurate rather than vaguer -- the opposite of the direction I was leaning. Verified both ways, since a fix that only works in one tree is what produced the bug: the template stays green at 112 claims, and a freshly scaffolded project committed and checked exits 0 for the first time, with the bare-filename notes from f5fd67b reported as information rather than failure. Worth recording why this was invisible from inside: every path in question resolves here. The documents were only wrong from a vantage point this repository does not have, which is why scaffolding into a scratch directory found it and reading it here never would. closes #16 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 00:16:48 -05:00
| What runs before a commit? | `.githooks/` |
feat(guards): gate the claim of being finished, not only the artifact Every guard in this template fires on an artifact -- `githooks/pre-commit` on a commit, `audit-gate.mjs` and `preflight.sh` on a release, `verify.sh` when somebody runs it. `GUARDS.md` §6 is the rule they follow: guards belong before the artifact exists. An agent that says "done" and does not commit produces no artifact and trips none of them. That gap already has two rules pointing at it -- `WORK_CYCLE.md` opens with *"Done" is not a close*, and the tracker convention refuses a close under fifteen characters of evidence -- and a rule is a thing a reader can skip. This is the same rule with an exit code. `scripts/verify-before-done.sh` is a Claude Code `TaskCompleted` hook. Verified against Claude Code 2.1.251: the event exists and the binary carries the string `TaskCompleted hook prevented continuation`. WIRED TO `verify.sh`, NOT TO THE TEST COMMAND. `npm test` in a repo with no tests exits 0, so a gate built on it reports green having verified nothing -- `GUARDS.md` §4 and §8's whole subject. `verify.sh` already distinguishes the three answers, and this treats exit 2 (nothing was verified) as a failure with its own message rather than a pass. A repository with no checks yet fails this gate loudly, which is the intended answer. IT EXITS 2 TO BLOCK, AND THAT IS NOT THE USUAL CONVENTION. Claude Code reads a `1` from a hook as "the hook broke" and continues, so a gate written the ordinary way fails OPEN and lets through exactly what it was installed to catch, looking identical from the outside. `docs/TOOLS.md` now says this beside the exit-2-is-never-a-pass rule it inverts. THE LIMITATION IS IN THE HEADER IN CAPITALS, because it decides whether this is worth installing: it gates Claude Code and nothing else. A Codex session or a human in the same checkout writes past it. It is a second layer and never the layer -- `.githooks/pre-commit` is what git runs whoever is driving, and if a project's real suite is not wired into that hook, wiring it there is worth more than installing this. Proved rather than assumed, all four outcomes, per §1: no `verify.sh` -> 2 (fails closed); pass -> 0 with empty stderr; fail -> 2 naming the code; verified-nothing -> 2 with the distinct message. Docs in the same commit, per the triggers `doc-triggers.py` reported: `architecture/README.md` gains the row its own "Adding one" rule requires, `TOOLS.md` gains the paragraph under "Which ones can stop you", and `DOC_TRUST_MAP.md` gains the question it now answers. `doc-claims.sh` re-run: 51 claimed paths across the three, all present. Prompted by an XDA piece on Claude Code shipping unverified work. The idea is theirs; the exit-2 contract, the `verify.sh` wiring and the Codex caveat are what it needed to be true here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 08:42:00 -05:00
| What stops an agent reporting work it did not verify? | `docs/architecture/scripts/verify-before-done.sh`, and `docs/TOOLS.md` for why it is a second layer rather than the layer |
| How do I write a check that will actually catch something? | `docs/architecture/GUARDS.md` |
**Next action and blockers are recorded at the end of every piece of work, not
when somebody asks.** `docs/WORK_CYCLE.md` holds that procedure and the reason
each step is in it — including which of these channels updates itself and which
two only ever change because a person changed them.
## Folder layout
| Folder | Contents |
| --- | --- |
| `docs/planning/` | `PROJECT_PLAN` — the vision. Not the schedule; that is the tracker. |
| `docs/qa/` | `ClaudeQAPlan` (playbook), `ClaudeQACoverage` (what each pass reached), `ClaudeReport` (the verdict) |
| `docs/architecture/` | How it is built: module boundaries, data shapes, reference manuals |
| `docs/design/` | What it should feel like: product decisions, UI plans, copy |
| `docs/security/` | `SECURITY` — threat model, secret handling, what is out of scope |
| `docs/history/` | `DEVELOPMENT_LOG` (dated, append-only), `HISTORY` (the narrative), `BATCH_LEDGER` (archived, superseded by the tracker) |
| `docs/data/` | the three branding marks privacyllc.dev renders for this project |
2026-08-17 22:57:20 -05:00
| `docs/` root | this map; `WORK_CYCLE` — what to do when a piece of work is finished; `TOOLS` — where the scripts are and which can stop you; `OPERATIONS` — what watches production, *(only for a deployed service)* |
`README.md` stays at the repository root; it is the landing page and moving it
breaks that. Everything else lives under `docs/`.
## Source-of-truth ladder
When two sources disagree, believe them in this order:
1. **Verified code behaviour** — read the source, run the tests
2. **The issue tracker** — for anything about state of work: what is open, what
closed, when, and by which commit
3. **Specialist docs** — authoritative for their own subject only
4. **History and logs** — a record of *then*, not a description of *now*
A document that describes work as done is describing what was true when it was
written. The tracker is describing now.
## The status header
fix(tools): doc-triggers could not see the documents at the repository root Collection started at DOCS.rglob("*.md"), so nothing at the root was read. README.md, project-readme-template.md and both START-HERE documents carry a full status header, and Governs lines nothing ever looked at. Changing a file they govern fired nothing, and the run said "No document's Governs matched these paths" -- true of the tool, false of the repository. Third instance of one shape in two days, each a level further out. Documents whose Governs carried a gloss were handed a glob no file could satisfy; before that a root resolved by depth pointed the whole tool outside the repository; here four documents were never collected at all. Every one of them printed something reassuring while checking less than it claimed. Root documents are now collected alongside the tree. The root walk is glob, not rglob -- deliberately one level deep, so a vendored copy of this template, a scratch checkout or somebody's directory of notes cannot enrol its documents as governing the project that holds it. Verified: a vendor/Template/README.md declaring Governs: src/** is not consulted. ## Status is what separates a document from a template for one project-readme-template.md carries `Status: <Current | Draft | Superseded | Archived>` and a Governs describing the README of whichever project copies it. Collecting the root without a guard would trade a document that never fires for a template that always does, which is the pair of failures this script has spent two days on. DOC_TRUST_MAP.md already makes the status vocabulary a rule with a checker behind it, so that is the test: a document whose Status is not one of the four words is not treated as governing anything here. It is **named, not dropped** -- when such a document governs a path in the change it is listed with its status, because a silent exclusion is the failure being fixed, not a smaller version of it. A document that governs a subject rather than paths can never fire mechanically, so an unfilled one is left out of the judge-these-yourself list entirely rather than sitting in it permanently. Both branches were exercised: a root template governing src/** is named and not fired; the same file with Status: Current fires normally. The no-match message now covers this case too, rather than claiming nothing matched when something did and was set aside for a stated reason. ## Documents architecture/README.md's row says where doc-triggers reads from, which has changed. DOC_TRUST_MAP.md owns the status header: it now says root documents carry one and are read, that the root is one level deep and why, and what the four status words separate. Verified from a clean clone: touching START-HERE-New-Project.md fires README.md; docs/ behaviour is unchanged across a modified script, a modified document, a modified githook, a branding asset and a staged deletion; the scripts/ copy still resolves its own root. doc-claims reads 116 claimed paths across 23 files, all present. closes #22 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 00:12:49 -05:00
Every document in this tree opens with one, immediately after its H1 — and so
do the documents at the repository root, which `scripts/doc-triggers.py` reads
alongside this tree. The root is read one level deep only, so a vendored copy
of this template cannot enrol its documents as governing the project holding
it.
```
Status: Current | Draft | Superseded | Archived
Owner: <who maintains this>
Last reviewed: <YYYY-MM-DD>
Governs: <paths or subject this document is authoritative for>
Review trigger: <the change that should send someone back to this file>
fix(tools): doc-triggers matched the glob but not the trigger's verb DOC_TRUST_MAP.md declares `Governs: docs/**`, the broadest glob in the tree, while its Review trigger is one of the narrowest -- any doc added, deleted or moved. Matching on the glob alone fired it on every edit to every document, forever, and correctly by the only rule the tool had. Touching one script fired three documents and exactly one of them applied. A prompt that always fires is one people stop reading, and it takes the true positives with it. This tool exits 0 by design -- it is a prompt, not a gate -- which makes it more vulnerable to that, not less, because nothing forces the reading. Documents now declare the kinds of change their trigger names, in an optional `Fires on:` header field, read against git's own status letter. Absent, empty or unparseable means every kind, so nothing changes for the six other path-governing documents and a document is only ever quietened by somebody writing the line deliberately. ## Why declared rather than read out of the trigger prose The obvious first cut is to look for added/deleted/moved with no changed/change to. Tried against the seven path-governing documents here, it misclassifies the one it exists to fix: DOC_TRUST_MAP.md's trigger ends "any change to which doc owns a subject", so it reads as a change-verb. That clause is about which document owns a subject, not about a file being edited, and nothing lexical separates it from architecture/README.md's "any change to a module boundary or a data shape", which genuinely does mean modification. Guessing at English is silent in the expensive direction: a document wrongly read as existence-only stops being prompted for and goes quietly stale, which is the failure this whole tool exists to prevent. So the narrowing is declared or it does not happen. ## Also changed_paths now carries a status letter per path, from --name-status for --staged and --range and from the porcelain columns for the working tree. Paths named on the command line have no diff to read, so the kind is inferred: absent from disk is a deletion, present but untracked is an addition, otherwise a modification. Documents that govern a path in the change but do not fire on its kind are named in their own short block rather than dropped, because a reader who saw nothing would have to guess whether they had been considered. The no-match message now distinguishes "nothing governs these paths" from "governed, but not this kind of change" -- the second is a declaration somebody wrote, not an unclaimed area. Verified: modifying a script fires TOOLS.md and architecture/README.md and not DOC_TRUST_MAP.md; adding, deleting and moving a document under docs/ each still fire it; modifying a document fires nothing; an unknown word warns and fires on everything; an empty or absent field fires on everything. closes #20 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:50:21 -05:00
Fires on: <optional added, deleted, moved, changed>
```
**Exactly those four status words.** They are not a suggestion: a checker reads
them, and a document whose status falls outside the list is reported as having
fix(tools): doc-triggers could not see the documents at the repository root Collection started at DOCS.rglob("*.md"), so nothing at the root was read. README.md, project-readme-template.md and both START-HERE documents carry a full status header, and Governs lines nothing ever looked at. Changing a file they govern fired nothing, and the run said "No document's Governs matched these paths" -- true of the tool, false of the repository. Third instance of one shape in two days, each a level further out. Documents whose Governs carried a gloss were handed a glob no file could satisfy; before that a root resolved by depth pointed the whole tool outside the repository; here four documents were never collected at all. Every one of them printed something reassuring while checking less than it claimed. Root documents are now collected alongside the tree. The root walk is glob, not rglob -- deliberately one level deep, so a vendored copy of this template, a scratch checkout or somebody's directory of notes cannot enrol its documents as governing the project that holds it. Verified: a vendor/Template/README.md declaring Governs: src/** is not consulted. ## Status is what separates a document from a template for one project-readme-template.md carries `Status: <Current | Draft | Superseded | Archived>` and a Governs describing the README of whichever project copies it. Collecting the root without a guard would trade a document that never fires for a template that always does, which is the pair of failures this script has spent two days on. DOC_TRUST_MAP.md already makes the status vocabulary a rule with a checker behind it, so that is the test: a document whose Status is not one of the four words is not treated as governing anything here. It is **named, not dropped** -- when such a document governs a path in the change it is listed with its status, because a silent exclusion is the failure being fixed, not a smaller version of it. A document that governs a subject rather than paths can never fire mechanically, so an unfilled one is left out of the judge-these-yourself list entirely rather than sitting in it permanently. Both branches were exercised: a root template governing src/** is named and not fired; the same file with Status: Current fires normally. The no-match message now covers this case too, rather than claiming nothing matched when something did and was set aside for a stated reason. ## Documents architecture/README.md's row says where doc-triggers reads from, which has changed. DOC_TRUST_MAP.md owns the status header: it now says root documents carry one and are read, that the root is one level deep and why, and what the four status words separate. Verified from a clean clone: touching START-HERE-New-Project.md fires README.md; docs/ behaviour is unchanged across a modified script, a modified document, a modified githook, a branding asset and a staged deletion; the scripts/ copy still resolves its own root. doc-claims reads 116 claimed paths across 23 files, all present. closes #22 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 00:12:49 -05:00
an unknown one rather than being quietly accepted. It is also what separates a
document from a *template for* one: `project-readme-template.md` carries
`Status: <Current | Draft | Superseded | Archived>` and a `Governs:` describing
the README of whichever project copies it, so `doc-triggers.py` does not treat it
as governing anything here — and says so when it would otherwise have fired,
rather than dropping it quietly. `Historical` is the word this
convention used to have and deliberately dropped — `Archived` covers it, and a
vocabulary a script enforces beats one described in prose, which drifts the
moment somebody writes something adjacent to it. `Draft` earns its place by
naming a real state the old list had no word for: a document being written,
which is neither current nor superseded.
**Review trigger** is the line that matters. "Last reviewed" ages on its own and
a reader cannot tell a current document from an abandoned one by looking at it;
a trigger names the event that should bring somebody back, which is what stops a
document becoming confidently wrong. It is also checked: the header is read from
the first sixteen lines of the file, and one carrying `Status` without
`Review trigger` is reported as incomplete — that combination looks finished and
is not.
fix(tools): three documents doc-triggers has never once fired docs/data/README.md, docs/data/img/README.md and docs/architecture/githooks/README.md have never fired for anything, since the first commit. They were not reported as skipped either -- they fell into neither list, so nothing on screen said they had not been checked. Each carries a Governs entry that explains itself after the glob: Governs: docs/data/** -- the assets privacyllc.dev renders for this project Governs is split on commas only, so that is one entry and the whole string was used as the glob. It contains a slash, so looks_like_path() called it a path and the document was classified as path-governing -- which also kept it out of the "govern a subject rather than paths, judge them yourself" list, the one that exists so a reader does not conclude everything was checked. Then matches() tested the file against a glob ending "renders for this project", which is false and always would be. Same class as the previous commit and the opposite sign, which makes it worse. That one fired a document when it should not: a false prompt, costing a glance. This one silently did not fire when it should, costing a document that goes quietly stale while the tool reports success. GUARDS.md opens with the sentence that applies -- a guard that cannot fail is worse than no guard, because it is trusted. docs/data/img/README.md governs the branding assets, which is the subject of open issue #14. Editing them had never once prompted the document that specifies their names, dimensions and ceilings. The glob is now extracted from the entry: cut at the first spaced em dash, en dash or --, then take the tokens on the left that themselves look like paths, falling back to the entry unchanged if that yields nothing. Three details are load-bearing: - The cut requires whitespace both sides. A bare - would halve source-grep and doc-claims, both of which appear in these headers. - Tokens come from the left of the gloss, not the whole entry. privacyllc.dev in the docs/data gloss passes looks_like_path on the extension rule and would otherwise become a glob firing on a file nobody has. - Classification still reads the whole entry. Deciding path-or-subject on a token would move documents between the two lists as a side effect of this fix. A trailing / on a glob now means the directory and everything under it. githooks/README.md governs "the .githooks/ a project installs", which extraction yields as a bare .githooks/, and fnmatch would not match a file inside it. DOC_TRUST_MAP.md owns the header schema, so it now states the gloss form and that it is the only one recognised -- a gloss in parentheses or after a colon puts a document straight back into silence, which is the failure that was invisible here for the life of the repository. Verified: both docs/data documents fire on docs/data/img/icon.webp when it is added, deleted and modified; githooks/README.md fires on docs/architecture/githooks/pre-commit and on .githooks/pre-commit; privacyllc.dev matches nothing; the split stays 7 path-governing and 12 subject-governing, exactly as before. The previous commit's behaviour is unchanged -- a modified script still fires TOOLS.md and architecture/README.md and not DOC_TRUST_MAP.md. closes #21 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:53:12 -05:00
**Governs** is a comma-separated list, and an entry may explain itself after the
glob with a spaced dash — `docs/data/** — the assets privacyllc.dev renders for
this project`. `scripts/doc-triggers.py` cuts the entry there and reads the globs
from the left of it. **Use that form and no other.** A gloss in parentheses, or
after a colon, is not recognised: the whole entry becomes the glob, matches
nothing, and the document is silently never fired — not reported as skipped
either, because it still looks like a path. Three documents here sat in exactly
that state from the first commit, `docs/data/img/README.md` among them, so
editing the branding assets never once prompted the document that specifies their
names and sizes. A trailing `/` on a glob means the directory and everything in
it, which is how this file's `.githooks/` entry reaches a project's installed
hooks.
fix(tools): doc-triggers matched the glob but not the trigger's verb DOC_TRUST_MAP.md declares `Governs: docs/**`, the broadest glob in the tree, while its Review trigger is one of the narrowest -- any doc added, deleted or moved. Matching on the glob alone fired it on every edit to every document, forever, and correctly by the only rule the tool had. Touching one script fired three documents and exactly one of them applied. A prompt that always fires is one people stop reading, and it takes the true positives with it. This tool exits 0 by design -- it is a prompt, not a gate -- which makes it more vulnerable to that, not less, because nothing forces the reading. Documents now declare the kinds of change their trigger names, in an optional `Fires on:` header field, read against git's own status letter. Absent, empty or unparseable means every kind, so nothing changes for the six other path-governing documents and a document is only ever quietened by somebody writing the line deliberately. ## Why declared rather than read out of the trigger prose The obvious first cut is to look for added/deleted/moved with no changed/change to. Tried against the seven path-governing documents here, it misclassifies the one it exists to fix: DOC_TRUST_MAP.md's trigger ends "any change to which doc owns a subject", so it reads as a change-verb. That clause is about which document owns a subject, not about a file being edited, and nothing lexical separates it from architecture/README.md's "any change to a module boundary or a data shape", which genuinely does mean modification. Guessing at English is silent in the expensive direction: a document wrongly read as existence-only stops being prompted for and goes quietly stale, which is the failure this whole tool exists to prevent. So the narrowing is declared or it does not happen. ## Also changed_paths now carries a status letter per path, from --name-status for --staged and --range and from the porcelain columns for the working tree. Paths named on the command line have no diff to read, so the kind is inferred: absent from disk is a deletion, present but untracked is an addition, otherwise a modification. Documents that govern a path in the change but do not fire on its kind are named in their own short block rather than dropped, because a reader who saw nothing would have to guess whether they had been considered. The no-match message now distinguishes "nothing governs these paths" from "governed, but not this kind of change" -- the second is a declaration somebody wrote, not an unclaimed area. Verified: modifying a script fires TOOLS.md and architecture/README.md and not DOC_TRUST_MAP.md; adding, deleting and moving a document under docs/ each still fire it; modifying a document fires nothing; an unknown word warns and fires on everything; an empty or absent field fires on everything. closes #20 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:50:21 -05:00
**Fires on** is optional, and only for the case where `Governs:` is much broader
than the trigger. `Governs:` says *where* a document is authoritative; the
trigger says *which kinds of change* to that place should bring somebody back,
and `scripts/doc-triggers.py` can only read the first. This file is the extreme
of the gap: it governs `docs/**`, the broadest glob here, while its trigger is
one of the narrowest — *any doc added, deleted or moved*. Without the
declaration it fires on every edit to every document forever, and is right to by
the only rule the tool has. A prompt that always fires is one people stop
reading, and it takes the true positives with it.
The words are `added`, `deleted`, `moved` and `changed`, comma-separated. **Omit
the line unless it is genuinely needed** — absent means fire on every kind, which
is what almost every document wants. It is deliberately declared rather than
inferred from the trigger prose: the obvious heuristic — existence verbs and no
change verb — was tried against the documents here and misread this one, whose
trigger ends "any change to which doc owns a subject". That clause is about which
document owns a subject, not about a file being edited, and nothing mechanical
separates it from a trigger that does mean modification. A wrong guess is silent
in the expensive direction, so an unreadable or missing declaration fires on
everything and only a deliberate line ever quietens a document.
feat(docs): two markers for entries that do not apply everywhere This tree is copied into every project and some of what it carries will not apply to all of them. Rather than several templates, or scaffold profiles the website's conformance reader would have to know about before it could tell a legitimately-absent file from a missing one, entries say so inline. Two markers, on axes that are deliberately not merged: applicability -- *(only for a deployed service)*, *(only where money moves)*, *(only where there are accounts)*. Does this project have to do this at all? provenance -- *(precautionary)*. Was the rule earned here, or borrowed? They are independent, and one word cannot say both. Rate limiting is universally applicable and has never bitten us. Money flowing backwards applies only to projects that take money and is the most common defect in the audits it came from. Applicability tells a reader whether to keep a rule; provenance tells them whether to argue with it. The instruction that travels with the marker is ClaudeQAPlan.md's rule about passes, generalised: if it does not apply, delete it. A pass that never applies is noise; a pass that is always skipped is a lie -- and so is a checklist row, and so is a whole document. Deleting is safe because DOC_TRUST_MAP.md is the trust map: what a project keeps is what it meant to keep. Applied where it was already true: the authorisation group is conditional on having accounts, and the header/TLS and test-environment rows on being a deployed service. A library was being told it lacked a CSP. closes #1 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:55:56 -05:00
## Two markers: does this apply, and who earned it
This tree is copied into every project, and some of what it carries will not
apply to all of them. A static site has no refunds; a library has no uptime.
Rather than shipping several templates, entries that are not universal say so
inline, with **two markers on different axes**:
| Marker | Answers | Example |
| --- | --- | --- |
| *(only where money moves)*, *(only for a deployed service)* | does this project have to do this at all? | a payments QA pass; the operations runbook |
| *(precautionary)* | was this rule earned here, or borrowed from outside? | rate limits; secrets in a shipped bundle |
**They are independent, and conflating them loses the distinction.** A rule can
be universally applicable and precautionary — rate limiting applies to
everything here and has never yet bitten us. Another can be narrowly conditional
and thoroughly proven elsewhere — money flowing backwards applies only to
projects that take money, and is the single most common defect in the audits it
came from. One word cannot say both, and the reader needs both: applicability
tells them whether to keep the rule, provenance tells them whether to argue with
it.
**If it does not apply, delete it.** That is `docs/qa/ClaudeQAPlan.md`'s rule
about passes — *a pass that never applies is noise; a pass that is always
skipped is a lie* — and it holds for a checklist row and for a whole document
just as well. A project that is not a deployed service deletes `OPERATIONS.md`
rather than carrying its headings unanswered forever, because an empty runbook
reads as one nobody wrote rather than one that never applied.
Deleting is safe here precisely because this file is the trust map: what a
project keeps is what a project meant to keep.
## Declaring a document deliberately absent
A repository may decide it will not keep one of these documents in git — QA and
security notes held outside the repository on purpose, say. Say so **here**, one
line per path, anywhere in this file:
```
Exempt: <the path><why, in a few words>
```
**Write the real path only when you mean it.** Outside a fenced block, an
`Exempt:` line naming a real document is not an example — it is a declaration,
and the checker will report that document as deliberately absent in every
repository that copied the file. Keep illustrations fenced, and use placeholders
anyway, as the form above does.
Fenced blocks are skipped, but only since 2026-08-12. Before that the parser
read every line, which meant that documenting this feature *used* it: the
example a writer naturally reaches for is a real required path, because an
illustration with a fake one teaches nothing. A draft of this very section
carried `Exempt: docs/security/SECURITY.md` inside a fence and would have marked
that document deliberately absent everywhere it was copied. It is recorded here
rather than quietly fixed because the shape recurs — a parser that cannot tell a
description of a thing from the thing itself.
The declaration lives in this file because the decision does. Holding the list
somewhere else would be two records of one fact, and the other one would never
hear that this repository changed its mind.
**`docs/data/` and `docs/data/img/` cannot be exempted.** A declaration naming
either is refused rather than honoured, because the Command Center renders what
is in them — an exemption would produce a project card with no icon and nothing
explaining why, which is the failure the check exists to catch.