From 876f09f48888484154cff411b8406aed92544e00 Mon Sep 17 00:00:00 2001 From: null Date: Mon, 17 Aug 2026 23:39:13 -0500 Subject: [PATCH] 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 /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 /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) --- docs/architecture/scripts/doc-triggers.py | 40 ++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/docs/architecture/scripts/doc-triggers.py b/docs/architecture/scripts/doc-triggers.py index 81b1a55..ba4fc2d 100755 --- a/docs/architecture/scripts/doc-triggers.py +++ b/docs/architecture/scripts/doc-triggers.py @@ -49,7 +49,45 @@ import re import subprocess import sys -ROOT = pathlib.Path(__file__).resolve().parents[3] +def _find_root() -> pathlib.Path: + """The repository root, found rather than assumed. + + This was `parents[3]`, which is correct only while the script sits at + `docs/architecture/scripts/` — its home in the template. The moment a + project copies it to `scripts/`, as the template's own adoption + instructions say to, `parents[3]` climbs out of the repository entirely: in + a checkout at `~/Projects/thing/scripts/`, it resolves to `~/`, and the + script reports "no docs/ directory" about a directory two levels above the + project it was run in. + + So: walk up from the script looking for a directory that has both `docs/` + and `.git`, then fall back to either alone, then to git's own answer. + """ + here = pathlib.Path(__file__).resolve() + + for parent in here.parents: + if (parent / "docs").is_dir() and (parent / ".git").exists(): + return parent + + for parent in here.parents: + if (parent / "docs").is_dir(): + return parent + + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + cwd=here.parent, + capture_output=True, + text=True, + check=False, + ) + + if result.returncode == 0 and result.stdout.strip(): + return pathlib.Path(result.stdout.strip()) + + return here.parents[1] + + +ROOT = _find_root() DOCS = ROOT / "docs" # The status header is a fenced block immediately after the H1, and a long value