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>
This commit is contained in:
parent
6cc9fe009c
commit
ef4494c83e
|
|
@ -80,7 +80,12 @@ written. The tracker is describing now.
|
|||
|
||||
## The status header
|
||||
|
||||
Every document in this tree opens with one, immediately after its H1:
|
||||
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
|
||||
|
|
@ -93,7 +98,12 @@ 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
|
||||
an unknown one rather than being quietly accepted. `Historical` is the word this
|
||||
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
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ can stop you, and where to start in a fresh clone.
|
|||
| `scripts/controls.sh` | which operational controls this project actually has, each row saying **how** it is known: measured, declared, n/a, or unknown. An unknown is never rendered as absent — "I could not tell" and "it is not there" send people to different places. |
|
||||
| `scripts/dev.sh` | bring the local stack up |
|
||||
| `scripts/scaffold.sh` | lay out a new project in this shape |
|
||||
| `scripts/doc-triggers.py` | which documents a **pending** change fires, read from their `Governs:` headers and narrowed by the optional `Fires on:` — the kinds of change (added, deleted, moved, changed) a document's trigger actually names, so one governing `docs/**` for existence changes alone does not fire on every edit. The `Review trigger` on each document names the change that should send somebody back to it; this is the check that asks before the commit rather than after |
|
||||
| `scripts/doc-triggers.py` | which documents a **pending** change fires, read from the `Governs:` headers of everything under `docs/` and of the documents at the repository root, narrowed by the optional `Fires on:` — the kinds of change (added, deleted, moved, changed) a document's trigger actually names, so one governing `docs/**` for existence changes alone does not fire on every edit. The `Review trigger` on each document names the change that should send somebody back to it; this is the check that asks before the commit rather than after |
|
||||
| `scripts/prove-guard.sh` | breaks the thing a guard protects, requires the guard to go red, restores the file from a trap. `GUARDS.md` §1 written out as a command, including the count — one failing test reported on six lines is not six failures |
|
||||
| `scripts/commit-mine.sh` | commits only the paths you name, by pathspec, after the secret scan. For a tree something else is also writing: what anyone else has staged is reported and left exactly as it was |
|
||||
| `scripts/doc-claims.sh` | every file a document names must exist, and (`--covers`) every file that exists is named — the second is the one that catches a list missing rows |
|
||||
|
|
|
|||
|
|
@ -142,6 +142,33 @@ def header_of(doc: pathlib.Path) -> dict[str, str]:
|
|||
return fields
|
||||
|
||||
|
||||
# `Status` must be one of these four. `DOC_TRUST_MAP.md` states it as a rule with
|
||||
# a checker behind it, and it is the one field that distinguishes a document from
|
||||
# a template for one: `project-readme-template.md` carries
|
||||
# `Status: <Current | Draft | Superseded | Archived>`, and its `Governs` describes
|
||||
# the README of the project that copies it, not anything in this repository.
|
||||
STATUS_WORDS = {"Current", "Draft", "Superseded", "Archived"}
|
||||
|
||||
|
||||
def governing_documents() -> list[pathlib.Path]:
|
||||
"""Every document that can fire, root ones included.
|
||||
|
||||
The walk used to start at `docs/`, so the documents at the repository root
|
||||
were not read at all — `README.md` and the two `START-HERE-*.md` carry a full
|
||||
status header, govern real paths, and fired nothing ever, while the output
|
||||
said "No document's Governs matched these paths". True of the tool and false
|
||||
of the repository, which is the same shape as the gloss bug one level out.
|
||||
|
||||
The root is read **non-recursively**: `ROOT.glob`, not `rglob`. A vendored
|
||||
copy of this template, a scratch checkout, or somebody's directory of notes
|
||||
would otherwise enrol its documents as governing this repository.
|
||||
"""
|
||||
docs = sorted(ROOT.glob("*.md"))
|
||||
if DOCS.is_dir():
|
||||
docs += sorted(DOCS.rglob("*.md"))
|
||||
return docs
|
||||
|
||||
|
||||
def looks_like_path(glob: str) -> bool:
|
||||
"""Whether a `Governs:` entry is a path pattern rather than a subject.
|
||||
|
||||
|
|
@ -346,20 +373,24 @@ def main() -> int:
|
|||
fired: list[tuple[str, list[tuple[str, str]], str]] = []
|
||||
subject_only: list[str] = []
|
||||
wrong_kind: list[tuple[str, str]] = []
|
||||
unfilled: list[tuple[str, str]] = []
|
||||
|
||||
for doc in sorted(DOCS.rglob("*.md")):
|
||||
for doc in governing_documents():
|
||||
rel = str(doc.relative_to(ROOT))
|
||||
header = header_of(doc)
|
||||
governs = header.get("Governs", "")
|
||||
if not governs:
|
||||
continue
|
||||
|
||||
filled = header.get("Status", "") in STATUS_WORDS
|
||||
|
||||
entries = [g.strip() for g in governs.split(",") if g.strip()]
|
||||
# Classification reads the whole entry and extraction reads inside it:
|
||||
# deciding "path or subject?" on a token would move documents between the
|
||||
# two lists as a side effect of this fix.
|
||||
path_globs = [g for e in entries if looks_like_path(e) for g in globs_in(e)]
|
||||
if not path_globs:
|
||||
if filled:
|
||||
subject_only.append(rel)
|
||||
continue
|
||||
|
||||
|
|
@ -372,6 +403,16 @@ def main() -> int:
|
|||
)
|
||||
|
||||
matched = {(s, p) for s, p in paths for g in path_globs if matches(p, g)}
|
||||
|
||||
if not filled:
|
||||
# A template for a document rather than a document. Named only when it
|
||||
# would otherwise have fired: a line on every run, about a file that is
|
||||
# supposed to look like this, is the noise this tool keeps being fixed
|
||||
# for.
|
||||
if matched:
|
||||
unfilled.append((rel, header.get("Status", "") or "(none)"))
|
||||
continue
|
||||
|
||||
hits = sorted({(s, p) for s, p in matched if s in letters}, key=lambda x: x[1])
|
||||
if hits:
|
||||
fired.append((rel, hits, header.get("Review trigger", "(none stated)")))
|
||||
|
|
@ -393,12 +434,12 @@ def main() -> int:
|
|||
print(f"{len(fired)} document(s) govern something in this change.")
|
||||
print("Read each trigger and decide — the rule is to update them in the")
|
||||
print("SAME commit as the code, not afterwards.")
|
||||
elif wrong_kind:
|
||||
elif wrong_kind or unfilled:
|
||||
# Distinct from matching nothing, and worth separating: a path here *is*
|
||||
# governed, and the reason nothing fired is a declaration somebody wrote,
|
||||
# not an area no document claims.
|
||||
print("Nothing fired. The paths in this change are governed, but only by")
|
||||
print("documents whose Fires on declaration excludes this kind of change.")
|
||||
# governed, and the reason nothing fired is a declaration somebody wrote
|
||||
# or a header nobody filled in, not an area no document claims.
|
||||
print("Nothing fired, but these paths are governed — see below for which")
|
||||
print("documents matched them and why each was not raised.")
|
||||
else:
|
||||
print("No document's Governs matched these paths. Worth a second look if")
|
||||
print("this change added a module, a migration, or a new boundary — an")
|
||||
|
|
@ -410,6 +451,12 @@ def main() -> int:
|
|||
for rel, declared in wrong_kind:
|
||||
print(f" {rel} — fires on {declared}")
|
||||
|
||||
if unfilled:
|
||||
print("\nGovern a path in this change but their status header is still a")
|
||||
print("template, so they are not treated as documents of this repository:")
|
||||
for rel, status in unfilled:
|
||||
print(f" {rel} — Status: {status}")
|
||||
|
||||
if subject_only:
|
||||
print("\nNot checked here — these govern a subject rather than paths, so")
|
||||
print("no change can fire them mechanically. Judge them yourself:")
|
||||
|
|
|
|||
Loading…
Reference in New Issue