From 6d020b17c7a217820bf4b802b390aa1aa6bd39f2 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 9 Aug 2026 09:42:41 -0700 Subject: [PATCH 1/3] Run the Diff-Scoped Doc Gates in a Hook This Repository Actually Has 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) --- .editorconfig | 5 +++++ .gitattributes | 2 ++ .husky/pre-commit | 22 ++++++++++++++++++++++ GOVERNANCE.md | 2 +- scripts/README.md | 2 +- 5 files changed, 31 insertions(+), 2 deletions(-) create mode 100755 .husky/pre-commit diff --git a/.editorconfig b/.editorconfig index 08aeec34..2ef0c084 100644 --- a/.editorconfig +++ b/.editorconfig @@ -60,6 +60,11 @@ end_of_line = lf [catalog/snippets/husky/pre-commit] end_of_line = lf +# This repository's own hook, paired with the `.gitattributes` pin. +# The git pin alone leaves the editor free to write a CRLF shebang, which would break it. +[.husky/pre-commit] +end_of_line = lf + # Linux scripts [*.sh] end_of_line = lf diff --git a/.gitattributes b/.gitattributes index 1c34a355..53f037cc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,6 +13,8 @@ *.sh text eol=lf # The husky pre-commit snippet is an extensionless shebang script (like a copied .husky/pre-commit). catalog/snippets/husky/pre-commit text eol=lf +# This repository's own hook, which is the extensionless case the comment above names. +.husky/pre-commit text eol=lf # Vanilla `.py` follows the CRLF default, since Python's universal newlines accept CRLF and it is commonly edited on Windows. # Pin LF only for a `.py` executed directly via its shebang, by path. diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..d3858614 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,22 @@ +#!/bin/sh +# 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`. +# A clone carries no hooks path, so this file does nothing until that is set. +# It deliberately does not source `_/husky.sh`. +# That file is gitignored and arrives with an npm install this repository does not have. +# Sourcing it would therefore break the hook in a fresh clone. +# The path is kept for the fleet convention the line-ending pins are written against. +# What is absent is a decision rather than an oversight. +# `repo_gate.py --check sha-pin` resolves same-owner pins against the GitHub API. +# It stays in CI, where a network is assumed, since a hook needing one fails offline. +# The doc linters that need Docker stay in CI and in the VS Code Lint tasks. +set -e + +# Scoped to what changed against HEAD, which is the policy for prose. +# A rule is applied as a file is next edited rather than swept across the tree. +# Whole-tree costs about 2.2 seconds where this costs about 0.2. +# The scope is the working tree rather than the index. +# A partially staged file is therefore judged on all of its edits, not only the staged ones. +# CI re-runs the same rules over the whole tree, which is what makes that affordable here. +python3 scripts/prose_lint.py . --diff HEAD +python3 scripts/repo_gate.py --check eol diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 3d9bef33..e2b1649a 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -368,7 +368,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i **Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): - **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. -- **The `.husky/pre-commit` hook** runs **language formatting only**: CSharpier + `dotnet format` (or ruff) via native tooling, no Docker and no doc linters, so it stays fast. +- **The `.husky/pre-commit` hook** runs **language formatting** (CSharpier + `dotnet format`, or ruff, via native tooling) and the **diff-scoped doc gates**, never Docker and never a network call, so it stays fast. 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. - **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks. The Docker invocations below are the same ones the VS Code tasks use, for ad-hoc or headless (agent) runs. diff --git a/scripts/README.md b/scripts/README.md index 9d684e26..938f1e8c 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,6 +1,6 @@ # Repo Scripts -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. Doc linters stay out of the pre-commit hook, which runs language formatting only so it stays fast. +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, `prose_lint.py --diff HEAD` and `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. **Hosted here and reached, never carried.** These are not declared in [`spec/files.json`][files], so the audit does not expect a downstream repo to ship them, the same footing as `spec/audit.py`. That is the fleet model rather than an omission: a script holding no per-repo content is one copy for the fleet, run from a hub checkout against the repository named on the command line, per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. A repository that cannot reach the hub reports the check as not run rather than reconstructing it, since a rebuilt gate encodes its author's reading of the rule and agrees with no other repository. CI reaches the same rules through the [`prose-gate`][prose-gate-action] composite action, which a caller pins to a commit SHA. It reads the copy bundled at that pin only where the run targets `main`, and takes the rules from hub `develop` on every other target, a feature-branch push included, so a released repo's gate is reproducible while every branch below it exercises a rule change before that change reaches `main`. A caller wanting one specific hub ref passes `rules-ref` and overrides both. From 8a693b52c1b2d0587a2cabe616b99d0dc0d73c7e Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 9 Aug 2026 09:58:50 -0700 Subject: [PATCH 2/3] Answer Both Review Findings, and Measure the Formatting Half Before Adding 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) --- .husky/pre-commit | 12 +++++++++--- GOVERNANCE.md | 2 +- TODO.md | 1 + scripts/README.md | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index d3858614..1b13dd68 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -6,9 +6,15 @@ # That file is gitignored and arrives with an npm install this repository does not have. # Sourcing it would therefore break the hook in a fresh clone. # The path is kept for the fleet convention the line-ending pins are written against. -# What is absent is a decision rather than an oversight. -# `repo_gate.py --check sha-pin` resolves same-owner pins against the GitHub API. -# It stays in CI, where a network is assumed, since a hook needing one fails offline. +# +# The language-formatting half the fleet convention names is absent here, and measured rather than assumed. +# This repository declares ruff in `pyproject.toml`, no workflow runs it, and the tree does not pass it. +# `ruff format --check` reports 13 of 57 files would be reformatted and `ruff check` reports 106 errors. +# A gate failing on the corpus it guards blocks every commit from the moment it lands. +# Converging the Python comes first, and the step is added here after that rather than before it. +# +# `repo_gate.py --check sha-pin` is absent for a different reason. +# It resolves same-owner pins against the GitHub API, and a hook needing a network fails offline. # The doc linters that need Docker stay in CI and in the VS Code Lint tasks. set -e diff --git a/GOVERNANCE.md b/GOVERNANCE.md index e2b1649a..c9d90508 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -368,7 +368,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i **Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): - **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. -- **The `.husky/pre-commit` hook** runs **language formatting** (CSharpier + `dotnet format`, or ruff, via native tooling) and the **diff-scoped doc gates**, never Docker and never a network call, so it stays fast. 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. +- **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. - **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks. The Docker invocations below are the same ones the VS Code tasks use, for ad-hoc or headless (agent) runs. diff --git a/TODO.md b/TODO.md index 34d78bd5..45e2a964 100644 --- a/TODO.md +++ b/TODO.md @@ -375,6 +375,7 @@ Small work with no research to preserve, selectable one bullet at a time. - **Reconsider whether the pre-commit hook runs the doc gates now that they are diff-scoped.** [`scripts/README.md`][scripts] records the current decision and its reason, that doc linters stay out of the hook so it stays fast, which was sound when the only mode was a whole-tree sweep, and a diff-scoped run finishes in about a second. The failure it would prevent is the most repeated one on record, comment sentences wrapped across lines caught after the commit rather than before it. Weigh it against the standing preference for a fast hook and against a hook that runs the gate from the wrong directory, which is its own false clean. - **Audit the fleet's shell surface by size and branching, and decide per script whether Python with unit tests is cheaper.** The evidence is the review record rather than a language preference, since a non-trivial shell script earns findings round after round while every gate under [`scripts/`][scripts] carries a test file beside it and converges in one or two. The measure is lines, branch count, and the review rounds each has cost. `repo-config/configure.sh` and the agent-safety installer are the two worth measuring, and a bootstrap script that needs the Python it exists to install is not a rewrite worth having, which protects the installer more than the config script. - **Make a table of contents standard for a long document rather than for the README alone.** [`spec/readme-structure.md`][readme-structure] fixes one at README position 4 and no other hub file carries one, which leaves the three longest documents without it, `CODESTYLE.md` at 516 lines, `GOVERNANCE.md` at 436 and `WORKFLOW.md` at 301, measured on `develop` at `3d1a0b1` on 2026-08-06. Settle the threshold in headings or lines so the audit can check it, and settle how it sits with the reference-link exception, since the four agent-instruction files keep inline links exactly because they are read one section at a time, which is the property that makes a contents list worth having in them. The mechanical constraint is that the list is filled by the Markdown All in One extension on save, so a file nobody opens in the editor grows a stale list, which is worse than absent because it is read as current. +- **Converge this repo's Python on the ruff configuration it already declares, then add the formatting half to the pre-commit hook.** `pyproject.toml` carries `[tool.ruff]` and [`spec/project-types.json`][project-types] declares `python.ruff.config`, yet no workflow runs ruff and the tree does not pass it, measured on `develop` at `6d020b1` on 2026-08-09 with ruff 0.16.2: `ruff format --check` reports 13 of 57 files would be reformatted, and `ruff check` reports 106 errors, of which 39 are auto-fixable. The largest groups are 24 `PLW1510` (a `subprocess.run` with no `check`), 17 `FURB167` (`re.M` for `re.MULTILINE`), 11 `EXE001` (a shebang on a non-executable file, which wants reading against the `eol-coverage` shebang set rather than fixed blindly), 9 `BLE001` and 9 `SIM117`. The hook deliberately ships without the ruff step for this reason, since a gate failing on the corpus it guards blocks every commit from the moment it lands, which is the measure-the-corpus-first rule applied to a gate rather than to an exemption. Decide whether CI gains a ruff job in the same pass, since a formatter enforced only by a hook is enforced only on the machines that enabled it. - **Adopt the OCI annotation keys for Docker image metadata across the Docker repos**, replacing the ad-hoc and label-schema keys, per [#363][issue-363]. - **Sweep the central package-version property to `Directory.Packages.props` fleet-wide**, since PlexCleaner sets it in `Directory.Build.props`, off the [`CODESTYLE.md`][codestyle] canonical. - **Canonicalize Python linter-config placement on `pyproject.toml`**, since one cataloged repo uses a standalone ruff config plus a pyright config. Track it as a drift finding and fix it downstream. diff --git a/scripts/README.md b/scripts/README.md index 938f1e8c..ba2e1203 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,6 +1,6 @@ # Repo Scripts -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, `prose_lint.py --diff HEAD` and `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. +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. **Hosted here and reached, never carried.** These are not declared in [`spec/files.json`][files], so the audit does not expect a downstream repo to ship them, the same footing as `spec/audit.py`. That is the fleet model rather than an omission: a script holding no per-repo content is one copy for the fleet, run from a hub checkout against the repository named on the command line, per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. A repository that cannot reach the hub reports the check as not run rather than reconstructing it, since a rebuilt gate encodes its author's reading of the rule and agrees with no other repository. CI reaches the same rules through the [`prose-gate`][prose-gate-action] composite action, which a caller pins to a commit SHA. It reads the copy bundled at that pin only where the run targets `main`, and takes the rules from hub `develop` on every other target, a feature-branch push included, so a released repo's gate is reproducible while every branch below it exercises a rule change before that change reaches `main`. A caller wanting one specific hub ref passes `rules-ref` and overrides both. From 134039526eb6ba9dcdfdd8f4ebaed1f59837cb8d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 9 Aug 2026 10:08:19 -0700 Subject: [PATCH 3/3] Say Which Gate Is Diff-Scoped, Since Only One of Them Is 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) --- .husky/pre-commit | 15 ++++++++++++--- GOVERNANCE.md | 2 +- scripts/README.md | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 1b13dd68..dce628bd 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,5 @@ #!/bin/sh -# Local pre-commit gate for this repository: the doc checks CI runs, scoped to the diff. +# Local pre-commit gate for this repository: the doc checks CI runs, on the commit's own scope. # Enable it per clone with `git config core.hooksPath .husky`. # A clone carries no hooks path, so this file does nothing until that is set. # It deliberately does not source `_/husky.sh`. @@ -18,11 +18,20 @@ # The doc linters that need Docker stay in CI and in the VS Code Lint tasks. set -e -# Scoped to what changed against HEAD, which is the policy for prose. +# Git already runs a hook from the top level, measured by committing from `scripts/` and printing `pwd`. +# This is belt and braces for an invocation that does not come from git. +# The relative paths below would otherwise resolve against whatever directory the caller was in. +cd "$(git rev-parse --show-toplevel)" + +# The prose gate is scoped to what changed against HEAD, which is the policy for prose. # A rule is applied as a file is next edited rather than swept across the tree. -# Whole-tree costs about 2.2 seconds where this costs about 0.2. +# Whole-tree costs about 2.2 seconds where the diff-scoped run costs about 0.13. # The scope is the working tree rather than the index. # A partially staged file is therefore judged on all of its edits, not only the staged ones. # CI re-runs the same rules over the whole tree, which is what makes that affordable here. python3 scripts/prose_lint.py . --diff HEAD + +# The eol check is repo-wide rather than diff-scoped, and it is here because it is already fast. +# It reads `.gitattributes` against `.editorconfig` for the whole repository and takes no file list. +# At about 0.04 seconds there is nothing to scope, so scoping it would only make it wrong. python3 scripts/repo_gate.py --check eol diff --git a/GOVERNANCE.md b/GOVERNANCE.md index c9d90508..534acd25 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -368,7 +368,7 @@ CI runs the full lint set, but run the linters locally before pushing to catch i **Each surface runs the lint with the tool that fits it, all from the same config files** (`.markdownlint-cli2.jsonc`, `cspell.json`, `.editorconfig`): - **CI (authoritative)** runs **markdownlint-cli2**, **cspell**, and **actionlint** as pinned action wrappers (Dependabot bumps them), plus **editorconfig-checker** via Docker `:latest` (its action only installs the CLI, so the Docker one-liner is what actually runs the check). markdownlint covers all `**/*.md`, and **cspell is scoped to `README.md` + `HISTORY.md`** (see [CODESTYLE.md](./CODESTYLE.md) "Markdown and Spelling" for why), matching the cspell one-liner below. -- **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. +- **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 runs each gate at the scope that fits it. The prose gate 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.13 and is what makes it affordable in a hook at all. A whole-repo check belongs there too when it is already fast and takes no file list, which the line-ending consistency check is, so scope is a property of the gate rather than a rule the hook applies to all of them. `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. - **The VS Code Lint tasks** run the full doc-lint set via Docker `:latest` on demand, the local surface for Markdown, spelling, workflow, and EditorConfig checks. The Docker invocations below are the same ones the VS Code tasks use, for ad-hoc or headless (agent) runs. diff --git a/scripts/README.md b/scripts/README.md index ba2e1203..83d13242 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,6 +1,6 @@ # Repo Scripts -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. +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 two deterministic doc gates, each at the scope that fits it. `python3 scripts/prose_lint.py . --diff HEAD` is diff-scoped, at about 0.13 seconds where its whole-tree sweep costs about 2.2. `python3 scripts/repo_gate.py --check eol` is repo-wide, since it reads `.gitattributes` against `.editorconfig` and takes no file list, and at about 0.04 seconds there is nothing to scope. 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. **Hosted here and reached, never carried.** These are not declared in [`spec/files.json`][files], so the audit does not expect a downstream repo to ship them, the same footing as `spec/audit.py`. That is the fleet model rather than an omission: a script holding no per-repo content is one copy for the fleet, run from a hub checkout against the repository named on the command line, per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. A repository that cannot reach the hub reports the check as not run rather than reconstructing it, since a rebuilt gate encodes its author's reading of the rule and agrees with no other repository. CI reaches the same rules through the [`prose-gate`][prose-gate-action] composite action, which a caller pins to a commit SHA. It reads the copy bundled at that pin only where the run targets `main`, and takes the rules from hub `develop` on every other target, a feature-branch push included, so a released repo's gate is reproducible while every branch below it exercises a rule change before that change reaches `main`. A caller wanting one specific hub ref passes `rules-ref` and overrides both.