Commit Graph

4 Commits

Author SHA1 Message Date
null 2577cb9ed8 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
null 81e326829b 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
null 876f09f488 fix(tools): doc-triggers found the repository by depth and left it
ROOT was `Path(__file__).resolve().parents[3]`, which is correct only while the
script sits at its template home, docs/architecture/scripts/. TOOLS.md tells an
adopting project to take scripts one at a time into its own scripts/, and from
<project>/scripts/doc-triggers.py that expression resolves to the *parent of the
project* -- outside the repository entirely.

The failure is silent and reads as a pass. main() opens with a `not DOCS.is_dir()`
guard that prints and returns 0, so an adopting project got exit 0 and one line
naming a directory two levels above the code it was asked about. The check that
enforces "update the triggered documents in the same commit as the code" had
quietly stopped running, in exactly the projects that took the template's advice.

That is the failure GUARDS.md opens with -- a guard that cannot fail is worse
than no guard, because it is trusted -- landed on the tool that polices the
documents. It could not be caught by running it here, because here parents[3] is
right; it takes a copy at the documented location to see it.

The root is now found rather than assumed: walk up from __file__ for a directory
holding both docs/ and .git, then either alone, then `git rev-parse
--show-toplevel`, then give up to the script's own parent. Both directories are
required before docs/ alone so that a repository vendoring a docs/ in some
subdirectory does not anchor on it.

Reproduced in a scratch repository with the script at scripts/, a document
governing src/**, and src/a.py staged: before, "no docs/ directory at <tmp>/docs";
after, the document and its trigger. The in-place layout is unchanged and still
fires DOC_TRUST_MAP.md, TOOLS.md and architecture/README.md.

Exit status is still always 0. This is a prompt, not a gate, so the fix belongs
in root resolution and not in the exit code.

closes #17

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:39:13 -05:00
null 6965915dbd chore(repo): put the template under version control
The basis for every project here was itself unversioned: no .git, no remote,
no history. Changes to it had no diff and no revert, and two of its own guards
could not run at all -- doc-claims.sh and doc-triggers.py both read git
history, so the script written to catch documentation drift could not be run
against the documents that define drift.

This is the tree as it stands, including work that until now existed only as
loose files on disk: WORK_CYCLE.md, TOOLS.md, the Portainer image-line fix in
deploy.py, the status vocabulary corrected to the four words the conformance
checker actually enforces, the Exempt: mechanism documented, and the Forgejo
instance named in README.md.

secrets.sh --tracked reports one candidate, migrate.sh:480. It is the comment
documenting the three Postgres credential shapes that script redacts, with
literal placeholders, and it is left alone deliberately: GUARDS.md section 2
is that a source-grep guard must tell code from the comment about code, and
deleting an explanation to quiet a scanner is the failure it names.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:44:26 -05:00