Skip to content

Run the Diff-Scoped Doc Gates in a Hook This Repository Actually Has - #642

Merged
ptr727 merged 3 commits into
developfrom
feat/pre-commit-doc-gates
Aug 9, 2026
Merged

Run the Diff-Scoped Doc Gates in a Hook This Repository Actually Has#642
ptr727 merged 3 commits into
developfrom
feat/pre-commit-doc-gates

Conversation

@ptr727

@ptr727 ptr727 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

TODO.md carried 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 .husky returned nothing. .husky/ held only _/husky.sh, which is gitignored, and core.hooksPath sat in .git/config, which no clone carries. Every commit in this tree ran nothing.

Both GOVERNANCE.md and scripts/README.md described what "the .husky/pre-commit hook" 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 runs dotnet husky run, and there is no .NET here.

The measurement that answers the original question

Command Time
prose_lint.py . whole-tree, as CI runs it 2.22 s
prose_lint.py . --diff HEAD 0.13 s
prose_lint.py . --diff HEAD~1 0.30 s
repo_gate.py --check eol 0.04 s

The 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-pin stays 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.md rather 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, .gitattributes for git and .editorconfig for the editor, since the git pin alone leaves the editor free to write a CRLF shebang and break it. Both are added. eol-coverage now reads 19 LF pins over 19 shebang files, up from 18 over 18.

Verification

The hook rejected its own first draft, on 13 comment-wrap findings 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.py with the CI check list exits 0, repo_gate.py reports eol, eol-coverage and sha-pin clean, editorconfig-checker exits 0, and markdownlint-cli2 reports 0 issues across 44 files. The hook is committed 100755, and is LF.

🤖 Generated with Claude Code

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>
Copilot AI lite review requested due to automatic review settings August 9, 2026 16:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-commit that runs scripts/prose_lint.py diff-scoped plus repo_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 .gitattributes and .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.

Comment thread scripts/README.md Outdated
Comment thread GOVERNANCE.md Outdated
…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>
Copilot AI review requested due to automatic review settings August 9, 2026 16:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. If git commit is invoked from a subdirectory, prose_lint.py . --diff HEAD can end up scanning only that sub-tree and miss changed files elsewhere, producing a false clean. Make the hook cd to 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 eol does not scope to the diff; it checks .gitattributes/.editorconfig consistency for the whole repo. Consider rewording so only prose_lint.py --diff is described as diff-scoped, and eol as 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 eol is not diff-scoped (it validates .gitattributes vs .editorconfig globally). 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 eol is not diff-scoped; it validates .gitattributes vs .editorconfig globally. 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>
Copilot AI review requested due to automatic review settings August 9, 2026 17:08
@ptr727

ptr727 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Answering the four suppressed findings from the round on 8a693b5, which carry no thread to resolve. Fixed in 1340395.

Three of them are one true claim, and it is accepted

This line describes both hook commands as "diff-scoped", but repo_gate.py --check eol does not scope to the diff; it checks .gitattributes/.editorconfig consistency for the whole repo.

Correct, and the script says so itself. check_eol reads .gitattributes against .editorconfig at the repository root, and its docstring records that files is unused and present only so every check in CHECKS shares one signature. Calling both gates diff-scoped described the hook by its faster half and would have misled anyone reasoning about why the hook is cheap.

Scope is now stated per gate in all three places the finding names, .husky/pre-commit, scripts/README.md and GOVERNANCE.md "Running the Linters Locally". The prose gate is diff-scoped, about 0.13 seconds against a 2.2 second whole-tree sweep. The line-ending check is repo-wide, and it belongs in a hook regardless because it is already fast at about 0.04 seconds and takes no file list, so scoping it would only make it wrong. The GOVERNANCE.md wording now says scope is a property of the gate rather than a rule the hook applies to all of them, which is the part that carries to a fleet repo adding a third gate later.

The fourth is a good hardening behind a failure mode that does not reach

The hook runs python3 scripts/... relative to the current working directory. If git commit is invoked from a subdirectory, prose_lint.py . --diff HEAD can end up scanning only that sub-tree and miss changed files elsewhere, producing a false clean.

The cd is added. The stated failure mode is not what would happen, and the distinction matters because a false clean and a loud failure call for different amounts of worry.

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 pwd probe:

$ cd scripts && git commit --allow-empty -m "cwd probe"
HOOK CWD: /.../ProjectTemplate

Had that not held, the run would not have produced a false clean either: python3 scripts/prose_lint.py would have failed to find the script and exited non-zero, blocking the commit loudly, rather than quietly scanning a subtree. prose_lint.py also refuses a --diff whose base resolves against a different repository root than the scan, for the same class of reason.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit fffac74 into develop Aug 9, 2026
7 checks passed
@ptr727
ptr727 deleted the feat/pre-commit-doc-gates branch August 9, 2026 17:24
ptr727 added a commit that referenced this pull request Aug 9, 2026
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>
ptr727 added a commit that referenced this pull request Aug 9, 2026
… 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants