From 2577cb9ed82bdc75e8289e773df9d3ab0b84d19c Mon Sep 17 00:00:00 2001 From: null Date: Mon, 17 Aug 2026 23:53:12 -0500 Subject: [PATCH] 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) --- docs/DOC_TRUST_MAP.md | 13 +++++++ docs/architecture/scripts/doc-triggers.py | 41 +++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/docs/DOC_TRUST_MAP.md b/docs/DOC_TRUST_MAP.md index 0770d4f..28c03bc 100644 --- a/docs/DOC_TRUST_MAP.md +++ b/docs/DOC_TRUST_MAP.md @@ -108,6 +108,19 @@ 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. +**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. + **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, diff --git a/docs/architecture/scripts/doc-triggers.py b/docs/architecture/scripts/doc-triggers.py index 459fe9b..fa9e70a 100755 --- a/docs/architecture/scripts/doc-triggers.py +++ b/docs/architecture/scripts/doc-triggers.py @@ -155,6 +155,36 @@ def looks_like_path(glob: str) -> bool: return "/" in glob or "*" in glob or re.search(r"\.\w{1,5}$", glob) is not None +# A `Governs:` entry is split on commas, so an entry that explains itself after +# the glob arrives whole: `docs/data/** — the assets privacyllc.dev renders for +# this project`. Used as a glob that matches nothing, ever, and because it +# contains a slash `looks_like_path` calls it a path — so the document was +# neither fired nor listed among the ones no change can fire mechanically. It was +# simply absent, which is the one outcome a reader cannot notice. +# +# Three of the seven path-governing documents here were in that state from the +# first commit, `docs/data/img/README.md` among them: editing the branding assets +# had never once prompted the document that specifies their names and sizes. +GLOSS = re.compile(r"\s+(?:—|–|--)\s+") + + +def globs_in(entry: str) -> list[str]: + """The globs inside one `Governs:` entry, with any trailing gloss removed. + + The cut requires whitespace on both sides of the dash: `source-grep` and + `doc-claims` appear in these headers and a bare `-` would halve them. Tokens + are taken from the left of the gloss rather than from the whole entry, + because prose on the right can itself look like a path — `privacyllc.dev` + passes the extension test and would become a glob that fires on a file + nobody has. + + An entry yielding no token falls back to itself, so a shape not foreseen here + behaves exactly as it did before. + """ + head = GLOSS.split(entry, 1)[0] + return [tok for tok in head.split() if looks_like_path(tok)] or [entry] + + def matches(path: str, glob: str) -> bool: """Whether `path` is governed by `glob`. @@ -167,6 +197,10 @@ def matches(path: str, glob: str) -> bool: return False if glob.endswith("/**"): return path.startswith(glob[:-2]) or path == glob[:-3] + if glob.endswith("/"): + # A bare directory, as `githooks/README.md` governs ".githooks/". fnmatch + # would not match a file inside it. + return path.startswith(glob) if "/**/" in glob: head, tail = glob.split("/**/", 1) return path.startswith(head + "/") and fnmatch.fnmatch(path, "*" + tail) @@ -320,8 +354,11 @@ def main() -> int: if not governs: continue - globs = [g.strip() for g in governs.split(",") if g.strip()] - path_globs = [g for g in globs if looks_like_path(g)] + 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: subject_only.append(rel) continue