Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
37 changes: 37 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/sh
# 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`.
# 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.
#
# 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

# 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 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
2 changes: 1 addition & 1 deletion GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** 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.
Expand Down
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion scripts/README.md
Original file line number Diff line number Diff line change
@@ -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 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.

Expand Down