Run the Diff-Scoped Doc Gates in a Hook This Repository Actually Has - #642
Conversation
The standing decision kept doc linters out of the pre-commit hook so it stayed fast, which was sound when a whole-tree sweep was the only mode. The gates are diff-scoped now: prose_lint.py --diff HEAD and repo_gate.py --check eol cost about 0.2 seconds together where the sweep costs about 2.2. The failure they catch, a comment sentence wrapped across lines, is the most repeated one on this repo's record and is caught after the commit today rather than before it. Measuring first found the larger problem. This repository had no hook at all: `git ls-files .husky` returned nothing, .husky held only the gitignored _/husky.sh, and core.hooksPath sat in .git/config where no clone carries it. Both GOVERNANCE.md and scripts/README.md described what "the .husky/pre-commit hook" runs, which was true of the fleet convention and not of this tree. The cataloged snippet would not have helped either, since it runs `dotnet husky run` and there is no .NET here. The hook does not source _/husky.sh, which is gitignored and arrives with an npm install this repository does not have, so sourcing it would break the hook in a fresh clone. sha-pin stays in CI because it resolves same-owner pins against the GitHub API, and a hook that needs a network fails offline. The line-ending pins GOVERNANCE.md "Line Endings" makes mandatory for a repo shipping an extensionless hook are added in both places it requires, .gitattributes for git and .editorconfig for the editor. eol-coverage now reads 19 LF pins over 19 shebang files, up from 18 over 18. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a real, repo-local pre-commit hook and aligns documentation and line-ending enforcement so the repository can run the deterministic documentation gates locally (diff-scoped) instead of relying solely on CI.
Changes:
- Add
.husky/pre-committhat runsscripts/prose_lint.pydiff-scoped plusrepo_gate.py --check eol. - Update documentation to reflect the hook’s intended scope/perf characteristics and what remains CI-only.
- Pin LF for the extensionless hook in both
.gitattributesand.editorconfig.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/README.md | Updates repo-scripts docs to describe running diff-scoped doc gates in the hook. |
| GOVERNANCE.md | Updates “Running the Linters Locally” guidance to include diff-scoped doc gates in the hook model. |
| .husky/pre-commit | Adds the repo’s actual pre-commit hook invoking diff-scoped prose_lint.py plus repo_gate.py eol. |
| .gitattributes | Pins LF for .husky/pre-commit (extensionless shebang file). |
| .editorconfig | Pins LF for .husky/pre-commit so editors don’t write a CRLF shebang. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…dding It The scripts/README.md sentence named the gates bare while carrying arguments, which mixed this file's two forms: a bare name for the script, the full command where it is a command. It now spells python3 scripts/... for both. The GOVERNANCE.md bullet read as though every hook runs formatting and doc gates together. Adding the formatting half here to match it was the obvious fix and the wrong one. This repo declares [tool.ruff] in pyproject.toml, no workflow runs it, and the tree does not pass it: ruff 0.16.2 reports 13 of 57 files would be reformatted and 106 lint errors. A ruff step in the hook would have blocked every commit from the moment it landed, on a corpus nobody had measured. So the bullet now says a repo adds each half once its tree passes that half, and a hook running one half is a repo mid-convergence rather than one out of conformance. The measurement is recorded in TODO.md as a chore, with the rule groups broken out, since EXE001 wants reading against the eol-coverage shebang set rather than fixing blindly, and since a formatter enforced only by a hook is enforced only on the machines that enabled it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (4)
.husky/pre-commit:28
- The hook runs
python3 scripts/...relative to the current working directory. Ifgit commitis invoked from a subdirectory,prose_lint.py . --diff HEADcan end up scanning only that sub-tree and miss changed files elsewhere, producing a false clean. Make the hookcdto the repo root before running gates.
python3 scripts/prose_lint.py . --diff HEAD
python3 scripts/repo_gate.py --check eol
scripts/README.md:3
- This line describes both hook commands as "diff-scoped", but
repo_gate.py --check eoldoes not scope to the diff; it checks.gitattributes/.editorconfigconsistency for the whole repo. Consider rewording so onlyprose_lint.py --diffis described as diff-scoped, andeolas a fast global check.
The fleet's checks and review tooling, run by hand, with the deterministic ones also gating CI. Each one exists because the CI linters pass on the failure it catches: `markdownlint`, `cspell`, `actionlint`, and `editorconfig-checker` all report clean on prose that breaks a documented [`GOVERNANCE.md`][governance] rule. The pre-commit hook runs the deterministic doc gates diff-scoped, `python3 scripts/prose_lint.py . --diff HEAD` and `python3 scripts/repo_gate.py --check eol`, which together cost about 0.2 seconds where the whole-tree sweep costs about 2.2. The earlier decision to keep doc linters out was made when a sweep was the only mode, and what reversed it is the diff scope rather than a change of preference. The gates needing Docker, and `sha-pin` which resolves a pin against the GitHub API, stay in CI. The hook reads the working tree rather than the index, so a partially staged file is judged on all of its edits, which CI's whole-tree run is the backstop for.
.husky/pre-commit:3
- The comment says the hook runs CI's doc checks "scoped to the diff", but
scripts/repo_gate.py --check eolis not diff-scoped (it validates.gitattributesvs.editorconfigglobally). Reword to avoid implying both checks are diff-scoped.
This issue also appears on line 27 of the same file.
# Local pre-commit gate for this repository: the doc checks CI runs, scoped to the diff.
# Enable it per clone with `git config core.hooksPath .husky`.
GOVERNANCE.md:371
- This paragraph calls the hook's doc gates "diff-scoped", but
repo_gate.py --check eolis not diff-scoped; it validates.gitattributesvs.editorconfigglobally. Rewording to distinguish the diff-scoped prose gate from the repo-wide eol consistency check would make the documentation accurate.
- **The `.husky/pre-commit` hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker and never a network call, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff for Python, via native tooling. A repo adds each half once its tree passes that half, since a gate that fails on the corpus it guards blocks every commit from the moment it lands, so a hook running one half is a repo mid-convergence rather than a repo out of conformance. The doc half is scoped to what the commit changes rather than swept over the tree, which is the difference between about 2.2 seconds and about 0.2 seconds and is what makes it affordable in a hook at all. `repo_gate.py --check sha-pin` stays out, since it resolves a same-owner pin against the GitHub API and a hook that needs a network fails offline. A repo enables the hook per clone with `git config core.hooksPath .husky`, and CI remains the authoritative run either way.
Three of the four suppressed findings are the same true claim: repo_gate.py --check eol is not diff-scoped. Its docstring says so outright, that `files` is unused and present only so every check shares one signature, and it reads .gitattributes against .editorconfig for the whole repository. Calling both gates diff-scoped described the hook by its faster half. Scope is now stated per gate in all three places, the hook, scripts/README.md and GOVERNANCE.md: the prose gate is diff-scoped at about 0.13 seconds against a 2.2 second sweep, and the eol check is repo-wide and belongs in a hook anyway because it is already fast and takes no file list. The fourth finding asked the hook to cd to the repo root against a false clean from a commit made in a subdirectory. The stated failure does not reach: git runs a hook from the top level, measured by committing from scripts/ with a pwd probe, and had it not, `python3 scripts/prose_lint.py` would fail to find the file rather than silently scan a subtree. The cd is added regardless, for an invocation that does not come from git, with the measurement recorded beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Answering the four suppressed findings from the round on Three of them are one true claim, and it is accepted
Correct, and the script says so itself. Scope is now stated per gate in all three places the finding names, The fourth is a good hardening behind a failure mode that does not reach
The Git runs a hook from the top level of the working tree, so committing from a subdirectory already resolved these paths correctly. Measured rather than asserted, with the hook temporarily replaced by a $ cd scripts && git commit --allow-empty -m "cwd probe"
HOOK CWD: /.../ProjectTemplateHad that not held, the run would not have produced a false clean either: So the change is worth having for an invocation that does not come from git, and it is committed with the measurement recorded beside it rather than as a fix for a bug that was there. |
Answers the one finding on the promotion pull request #643, which cannot take a fix commit of its own: its head is `develop`, so the correction reaches it by landing here first. > The header comment says the hook runs doc checks "on the commit's own scope", but the script later documents (and implements) scope as the working tree diff against HEAD (not the index / staged content). Accepted. The header named the staged content where the hook evaluates the working tree diff against `HEAD`. The body of the same file already said so, and said why, so the header was the single line disagreeing both with the implementation and with the three sentences under it. That is the worse shape of the two, since a reader who stops at the header gets a wrong answer that the file itself contradicts further down. It now says each gate runs at the scope that fits it, which is what the file documents and what #642 settled: the prose gate is diff-scoped against `HEAD`, and the line-ending check is repo-wide because it reads `.gitattributes` against `.editorconfig` and takes no file list. The hook ran on this commit and exits 0. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… Gates (#643) Three commits, all backlog bookkeeping and local tooling. No rule changes meaning, and nothing here alters what CI gates. **#640, the tagline question.** #577 asked that the tagline be a distinct thing from the intro so a README may carry a second paragraph without the cap or the mirrors reaching it. That shipped in `c2ce145` (#632), so the issue is closed against it. Grepping the number before closing found `TODO.md` had also given #577 to a different entry, the README-to-About hop, which the issue's body never covers. That hop now carries #639 of its own. **#641, the divergence ledger.** `reports/divergences.md` is a live pass over each repo's ground-truth branch, so a committed copy is only as current as its last run. Regenerating moved three rows: `AGENTS.md` "Fleet Bootstrap" is now divergent at Blog and HomeAutomation-Config, and `GOVERNANCE.md` "Verification Discipline" and "Workflow YAML Conventions" widened from one repo to four. The reason `TODO.md` gave for regenerating did not survive the run and is corrected. Two spent backlog claims went with it. **#642, the pre-commit hook.** The chore was to reconsider whether the hook should run the doc gates now that they are diff-scoped. Measuring first found this repository had no hook at all, while `GOVERNANCE.md` and `scripts/README.md` both described what one runs. There is now a committed `.husky/pre-commit` running the prose gate diff-scoped and the line-ending check repo-wide, with the `.gitattributes` and `.editorconfig` pins that `GOVERNANCE.md` "Line Endings" makes mandatory for an extensionless hook. The formatting half the fleet convention names is deliberately absent, and that is the finding worth carrying: this repository declares `[tool.ruff]` in `pyproject.toml` and `spec/project-types.json` declares `python.ruff.config`, yet no workflow runs ruff and the tree does not pass it, at 13 of 57 files unformatted and 106 lint errors. A ruff step would have blocked every commit from the moment it landed. The `GOVERNANCE.md` bullet now states the condition rather than a tool list, that a repo adds each half once its tree passes that half, and the measurement is recorded as a `TODO.md` chore. ## Carried content in this promotion `GOVERNANCE.md` "Running the Linters Locally" changed, and that section is carried `verbatim`, so every downstream copy is byte-mismatched until the next fleet visit. It rides the re-vendor entry already tracked under `TODO.md` "Fleet Sweeps" rather than owing a sweep of its own. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
TODO.mdcarried a chore to reconsider whether the pre-commit hook should run the doc gates now that they are diff-scoped. Measuring it first, as the entry asked, found the premise was false for this repository.There was no hook
git ls-files .huskyreturned nothing..husky/held only_/husky.sh, which is gitignored, andcore.hooksPathsat in.git/config, which no clone carries. Every commit in this tree ran nothing.Both
GOVERNANCE.mdandscripts/README.mddescribed what "the.husky/pre-commithook" runs. That was true of the fleet convention and not of this tree, which is the shape a reader has no way to catch. The cataloged snippet would not have closed it either: it runsdotnet husky run, and there is no .NET here.The measurement that answers the original question
prose_lint.py .whole-tree, as CI runs itprose_lint.py . --diff HEADprose_lint.py . --diff HEAD~1repo_gate.py --check eolThe standing decision to keep doc linters out was made against the 2.2-second sweep. What reverses it is the diff scope rather than a change of preference, so both documents now say that rather than simply asserting the new arrangement.
What is deliberately absent
repo_gate.py --check sha-pinstays in CI. It measured 0.03 s here only because all 45 pins are under another owner and were read for shape alone. It resolves a same-owner pin against the GitHub API, and a hook that needs a network fails offline and on a slow link.The hook does not source
_/husky.sh. That file is gitignored and arrives with an npm install this repository does not have, so sourcing it would break the hook in a fresh clone. The.husky/path is kept regardless, because the fleet's line-ending rules are written against it by name.The hook reads the working tree rather than the index, so a partially staged file is judged on all of its edits rather than only the staged ones. That is stated in the hook and in
scripts/README.mdrather than glossed, and CI's whole-tree run is the backstop that makes it affordable.Line-ending pins
GOVERNANCE.md"Line Endings" makes both pins mandatory for a repository that ships an extensionless hook,.gitattributesfor git and.editorconfigfor the editor, since the git pin alone leaves the editor free to write a CRLF shebang and break it. Both are added.eol-coveragenow reads 19 LF pins over 19 shebang files, up from 18 over 18.Verification
The hook rejected its own first draft, on 13
comment-wrapfindings across the comments I had just written, which is the exact failure the chore named. Fixed and re-run, it exits 0, and it ran on the commit in this pull request.Full CI-equivalent set is clean:
prose_lint.pywith the CI check list exits 0,repo_gate.pyreportseol,eol-coverageandsha-pinclean,editorconfig-checkerexits 0, andmarkdownlint-cli2reports 0 issues across 44 files. The hook is committed100755, and is LF.🤖 Generated with Claude Code