diff --git a/.agents/skills/dotnet-codestyle/SKILL.md b/.agents/skills/dotnet-codestyle/SKILL.md index ee8d9a96..f3565a5c 100644 --- a/.agents/skills/dotnet-codestyle/SKILL.md +++ b/.agents/skills/dotnet-codestyle/SKILL.md @@ -49,8 +49,9 @@ All builds must complete without warnings, enforced three ways: surfaced as a warning fails the build and must be fixed or deliberately suppressed at the narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. - Git hooks are optional, and a repo may wire a local runner (Husky.Net, with `dotnet husky run` - as a style step) for pre-commit enforcement, but CI is the gate that matters. + A working local hook is strongly suggested, not optional: wire Husky.Net from the canonical + `catalog/snippets/husky/` config. See GOVERNANCE.md "Running the Linters Locally" for what the + hook must cover and what its absence means. **A new port is not a license to silence diagnostics.** Brownfield or just-ported status never justifies relaxing analyzer severities or muting newly surfaced warnings. Fix them. (The only @@ -94,8 +95,8 @@ updates, dependency upgrades, benchmarks) on top: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`. - **`dotnet-outdated-tool`** checks for dependency updates, and Nerdbank.GitVersioning owns version management. -- CI is the authoritative lint backstop. Local pre-commit hooks are optional, wire Husky.Net (or - another runner) if you want local enforcement. +- CI is the authoritative lint backstop. A local pre-commit hook is strongly suggested: wire + Husky.Net from `catalog/snippets/husky/` for local enforcement, including the shared doc gates. - **Required VS Code extensions**: CSharpier, markdownlint, CSpell. Use the workspace settings without overrides. diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 02697a72..afb345af 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -90,8 +90,10 @@ The Python clean-compile is `uv run ruff format` + `uv run ruff check` + the rep both (see Type checking above). Run it, plus `uv run pytest`, before committing. These are documented commands, and an optional VS Code tasks mirror (all `type: process`, no `&&` shell chaining, so it runs the same on any task shell) is in the hub `vscode-tasks-python.json` snippet. -CI runs the same clean-compile commands as the authoritative backstop. Git hooks are opt-in, so -wire `pre-commit` for `ruff` and the type checker yourself if you want local enforcement. +CI runs the same clean-compile commands as the authoritative backstop. A working local hook is +strongly suggested, not opt-in: wire the Python `pre-commit` framework from the canonical +`catalog/snippets/pre-commit/.pre-commit-config.yaml`. See GOVERNANCE.md "Running the Linters +Locally" for what the hook must cover and what its absence means. A restricted executor gives each task a cache directory under a writable temporary root. Point `UV_CACHE_DIR`, `RUFF_CACHE_DIR`, `MYPY_CACHE_DIR`, and `COVERAGE_FILE` into that directory before diff --git a/.agents/skills/repo-worktree/SKILL.md b/.agents/skills/repo-worktree/SKILL.md index 1d60f3a1..d4f9e0e4 100644 --- a/.agents/skills/repo-worktree/SKILL.md +++ b/.agents/skills/repo-worktree/SKILL.md @@ -185,9 +185,9 @@ in the new tree. - **Husky.Net:** When `.husky/pre-commit` sources `.husky/_/husky.sh` and the local .NET tool manifest declares Husky.Net, run `dotnet tool restore`, then `dotnet husky install` from the worktree root. -- **Python pre-commit:** When `.pre-commit-config.yaml` exists, install the repository's declared - Python environment, then run `pre-commit install` through that environment. A uv project runs - `uv sync --frozen`, then `uv run pre-commit install`. +- **Python pre-commit:** When `.pre-commit-config.yaml` exists, run `uv tool install pre-commit` + once per host if not already installed, then `pre-commit install` from the worktree root. + `pre-commit` is never a project dependency, so this is the same regardless of profile. - **Repository override:** Follow a repository's explicit hook-setup instructions when they differ from these standard cases. Do not infer a replacement command from the language alone. diff --git a/.claude-plugin/fleet-skills/.source-digest b/.claude-plugin/fleet-skills/.source-digest index cc914f52..93518134 100644 --- a/.claude-plugin/fleet-skills/.source-digest +++ b/.claude-plugin/fleet-skills/.source-digest @@ -1 +1 @@ -85b7db1858ecd00a +332c16d2c066383b diff --git a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md index ee8d9a96..f3565a5c 100644 --- a/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md @@ -49,8 +49,9 @@ All builds must complete without warnings, enforced three ways: surfaced as a warning fails the build and must be fixed or deliberately suppressed at the narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. - Git hooks are optional, and a repo may wire a local runner (Husky.Net, with `dotnet husky run` - as a style step) for pre-commit enforcement, but CI is the gate that matters. + A working local hook is strongly suggested, not optional: wire Husky.Net from the canonical + `catalog/snippets/husky/` config. See GOVERNANCE.md "Running the Linters Locally" for what the + hook must cover and what its absence means. **A new port is not a license to silence diagnostics.** Brownfield or just-ported status never justifies relaxing analyzer severities or muting newly surfaced warnings. Fix them. (The only @@ -94,8 +95,8 @@ updates, dependency upgrades, benchmarks) on top: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`. - **`dotnet-outdated-tool`** checks for dependency updates, and Nerdbank.GitVersioning owns version management. -- CI is the authoritative lint backstop. Local pre-commit hooks are optional, wire Husky.Net (or - another runner) if you want local enforcement. +- CI is the authoritative lint backstop. A local pre-commit hook is strongly suggested: wire + Husky.Net from `catalog/snippets/husky/` for local enforcement, including the shared doc gates. - **Required VS Code extensions**: CSharpier, markdownlint, CSpell. Use the workspace settings without overrides. diff --git a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md index 02697a72..afb345af 100644 --- a/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md @@ -90,8 +90,10 @@ The Python clean-compile is `uv run ruff format` + `uv run ruff check` + the rep both (see Type checking above). Run it, plus `uv run pytest`, before committing. These are documented commands, and an optional VS Code tasks mirror (all `type: process`, no `&&` shell chaining, so it runs the same on any task shell) is in the hub `vscode-tasks-python.json` snippet. -CI runs the same clean-compile commands as the authoritative backstop. Git hooks are opt-in, so -wire `pre-commit` for `ruff` and the type checker yourself if you want local enforcement. +CI runs the same clean-compile commands as the authoritative backstop. A working local hook is +strongly suggested, not opt-in: wire the Python `pre-commit` framework from the canonical +`catalog/snippets/pre-commit/.pre-commit-config.yaml`. See GOVERNANCE.md "Running the Linters +Locally" for what the hook must cover and what its absence means. A restricted executor gives each task a cache directory under a writable temporary root. Point `UV_CACHE_DIR`, `RUFF_CACHE_DIR`, `MYPY_CACHE_DIR`, and `COVERAGE_FILE` into that directory before diff --git a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md index 1d60f3a1..d4f9e0e4 100644 --- a/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md +++ b/.claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md @@ -185,9 +185,9 @@ in the new tree. - **Husky.Net:** When `.husky/pre-commit` sources `.husky/_/husky.sh` and the local .NET tool manifest declares Husky.Net, run `dotnet tool restore`, then `dotnet husky install` from the worktree root. -- **Python pre-commit:** When `.pre-commit-config.yaml` exists, install the repository's declared - Python environment, then run `pre-commit install` through that environment. A uv project runs - `uv sync --frozen`, then `uv run pre-commit install`. +- **Python pre-commit:** When `.pre-commit-config.yaml` exists, run `uv tool install pre-commit` + once per host if not already installed, then `pre-commit install` from the worktree root. + `pre-commit` is never a project dependency, so this is the same regardless of profile. - **Repository override:** Follow a repository's explicit hook-setup instructions when they differ from these standard cases. Do not infer a replacement command from the language alone. diff --git a/.github/skills/dotnet-codestyle/SKILL.md b/.github/skills/dotnet-codestyle/SKILL.md index ee8d9a96..f3565a5c 100644 --- a/.github/skills/dotnet-codestyle/SKILL.md +++ b/.github/skills/dotnet-codestyle/SKILL.md @@ -49,8 +49,9 @@ All builds must complete without warnings, enforced three ways: surfaced as a warning fails the build and must be fixed or deliberately suppressed at the narrowest scope that fits (see Analyzer suppressions below), never left to accumulate. - **CI lint backstop.** CI runs the clean-compile checks on every PR as the authoritative gate. - Git hooks are optional, and a repo may wire a local runner (Husky.Net, with `dotnet husky run` - as a style step) for pre-commit enforcement, but CI is the gate that matters. + A working local hook is strongly suggested, not optional: wire Husky.Net from the canonical + `catalog/snippets/husky/` config. See GOVERNANCE.md "Running the Linters Locally" for what the + hook must cover and what its absence means. **A new port is not a license to silence diagnostics.** Brownfield or just-ported status never justifies relaxing analyzer severities or muting newly surfaced warnings. Fix them. (The only @@ -94,8 +95,8 @@ updates, dependency upgrades, benchmarks) on top: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed`. - **`dotnet-outdated-tool`** checks for dependency updates, and Nerdbank.GitVersioning owns version management. -- CI is the authoritative lint backstop. Local pre-commit hooks are optional, wire Husky.Net (or - another runner) if you want local enforcement. +- CI is the authoritative lint backstop. A local pre-commit hook is strongly suggested: wire + Husky.Net from `catalog/snippets/husky/` for local enforcement, including the shared doc gates. - **Required VS Code extensions**: CSharpier, markdownlint, CSpell. Use the workspace settings without overrides. diff --git a/.github/skills/python-codestyle/SKILL.md b/.github/skills/python-codestyle/SKILL.md index 02697a72..afb345af 100644 --- a/.github/skills/python-codestyle/SKILL.md +++ b/.github/skills/python-codestyle/SKILL.md @@ -90,8 +90,10 @@ The Python clean-compile is `uv run ruff format` + `uv run ruff check` + the rep both (see Type checking above). Run it, plus `uv run pytest`, before committing. These are documented commands, and an optional VS Code tasks mirror (all `type: process`, no `&&` shell chaining, so it runs the same on any task shell) is in the hub `vscode-tasks-python.json` snippet. -CI runs the same clean-compile commands as the authoritative backstop. Git hooks are opt-in, so -wire `pre-commit` for `ruff` and the type checker yourself if you want local enforcement. +CI runs the same clean-compile commands as the authoritative backstop. A working local hook is +strongly suggested, not opt-in: wire the Python `pre-commit` framework from the canonical +`catalog/snippets/pre-commit/.pre-commit-config.yaml`. See GOVERNANCE.md "Running the Linters +Locally" for what the hook must cover and what its absence means. A restricted executor gives each task a cache directory under a writable temporary root. Point `UV_CACHE_DIR`, `RUFF_CACHE_DIR`, `MYPY_CACHE_DIR`, and `COVERAGE_FILE` into that directory before diff --git a/.github/skills/repo-worktree/SKILL.md b/.github/skills/repo-worktree/SKILL.md index 1d60f3a1..d4f9e0e4 100644 --- a/.github/skills/repo-worktree/SKILL.md +++ b/.github/skills/repo-worktree/SKILL.md @@ -185,9 +185,9 @@ in the new tree. - **Husky.Net:** When `.husky/pre-commit` sources `.husky/_/husky.sh` and the local .NET tool manifest declares Husky.Net, run `dotnet tool restore`, then `dotnet husky install` from the worktree root. -- **Python pre-commit:** When `.pre-commit-config.yaml` exists, install the repository's declared - Python environment, then run `pre-commit install` through that environment. A uv project runs - `uv sync --frozen`, then `uv run pre-commit install`. +- **Python pre-commit:** When `.pre-commit-config.yaml` exists, run `uv tool install pre-commit` + once per host if not already installed, then `pre-commit install` from the worktree root. + `pre-commit` is never a project dependency, so this is the same regardless of profile. - **Repository override:** Follow a repository's explicit hook-setup instructions when they differ from these standard cases. Do not infer a replacement command from the language alone. diff --git a/.husky/pre-commit b/.husky/pre-commit index 53028209..fe676efb 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -7,11 +7,7 @@ # 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. +# The language-formatting half the fleet convention names runs via uvx, matching CI's own invocation for a repo with no `uv.lock` (the Scripts profile, CODESTYLE.md "Two profiles"). # # `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. @@ -23,6 +19,11 @@ set -e # The relative paths below would otherwise resolve against whatever directory the caller was in. cd "$(git rev-parse --show-toplevel)" +# Ruff and mypy, via uvx per this repo's own CI invocation for a repo with no `uv.lock`. +uvx ruff@latest format --check . +uvx ruff@latest check . +uvx mypy@latest + # The interpreter is chosen by running the probes spec/host-tools.json declares, in its order. # On native Windows the python.org install registers `py` and not `python3`. # That name resolves to a Microsoft Store alias stub, and Git Bash inherits the Windows PATH. diff --git a/AUDIT.md b/AUDIT.md index 926db34b..7be358cc 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -87,7 +87,7 @@ A check with `intentRef`/`workflowRef` points at the prose section that owns the - **carried-scope** - the repo carries no file the hub hosts rather than carries. The set is derived, not listed: the hub's git-tracked paths minus the [`spec/files.json`][files] baseline, so a file dropped from the manifest starts being reported on the next run with no retirement list to remember to edit. The remedy is the opposite of every other file finding, a **deletion**, since the repo reaches the hub's copy per [GOVERNANCE.md "Hub-Hosted Tooling"][governance-hub-hosted-tooling]. The match is on path alone, so a hit is a candidate and not a verdict: a repo's own content at a path the hub also uses matches while carrying nothing of the hub's, which the first fleet run showed twice, a KiCad tooling doc at `scripts/README.md` and per-repo formatting hooks at `.husky/pre-commit`. A [`spec/divergences.json`][divergences] `gaps` disposition decides which case a hit is, so only `retire` asserts a deletion, `accepted` closes a collision or a repo-owned file, and an untriaged hit is read before it is acted on. - **verbatim-tree** - every applicable `trees[]` declaration in [`spec/files.json`][files] owns its target tree. The audit reports missing files as letter findings, stale or modified bytes as drift, and extra files under a pruned target as drift. An unreadable or truncated repository tree is undecided and produces drift rather than a clean result. - **repo-setup** - every required secret for the repo's publish mechanisms is configured, and no forbidden secret is present (per [`spec/secrets.json`][secrets]). -- **linter-parity** - one config per linter (`.markdownlint-cli2.jsonc`, `cspell.json`, ruff/pyright, editorconfig/csharpier, actionlint) drives the editor extension, the CLI, and CI, and CI runs each. +- **linter-parity** - one config per linter (`.markdownlint-cli2.jsonc`, `cspell.json`, ruff/pyright, editorconfig/csharpier, actionlint) drives the editor extension, the CLI, and CI, and CI runs each. A local hook exists and runs at minimum the diff-scoped prose gate and the eol check via `hub-fetch-run.py`, or the hub's own local script copies for the hub repo itself (`parity.hooks`, intent). A repo with none wired is a defect, and one mid-convergence on the language-formatting half stays operational. - **recurring-violations** - comments concise and non-narrative, ASCII only (no em-dash, no smart quotes), US spelling, line endings per `.editorconfig`. These are frequent regressions, so this dimension is high priority and always runs, and each check is grep-able (see below). - **readme-structure** - the README follows [`spec/readme-structure.md`][readme-structure] (applicable sections, in order). Mechanically checked against the declared model in [`spec/readme-sections.json`][readme-sections]: required sections present, declared sections in their relative order, `License` last, the shields each deliverable implies, the license shield in the closing License section, and the tagline and its mirrors. A heading the model does not name is dropped before the order comparison, so a repo-specific section is never a finding. diff --git a/CODESTYLE.md b/CODESTYLE.md index 2569b380..f52773d7 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -18,7 +18,7 @@ Each language defines a **clean-compile** verification: the combination of build - **Run it after every code change, and it is not the whole gate.** The relevant language's clean-compile must pass before you commit. CI runs those same language checks as a backstop **plus everything else its validation workflow runs**, and all of it reports into the one required status, so a green clean-compile does not predict a green CI. That remainder is at least the doc-lint set (markdownlint, cspell, actionlint, `editorconfig-checker`) and whatever spec, config, and script gates the repo carries, so read the workflow for the full list rather than assuming this sentence enumerates it. What has to pass before a push is the repo's **whole** lint gate, per [GOVERNANCE.md "Verification Discipline"][governance-verification-discipline]. Each linter's known-working invocation is in [GOVERNANCE.md "Running the Linters Locally"][governance-running-the-linters-locally]. - **The named task definition is the canonical spec** - its exact command sequence, arguments, and strictness. You may run it through the VS Code task **or** by invoking the equivalent native commands directly, and either is fine **only if the sequence, arguments, and strictness match exactly**. No shortcuts and no more-lenient options (for example, never drop `--verify-no-changes` or loosen a `--severity`). -- **A local commit/pre-commit gate is the repo's choice.** No single hook runner fits every language (a `dotnet`-tool runner like Husky.Net suits .NET but not Python), so none is mandated, but that is **not** a recommendation against commit gates. CI is the authoritative backstop regardless, and a local gate is an additive convenience a repo may wire and keep: Husky.Net (and `dotnet husky run` as a style step) for .NET, `pre-commit` for Python. Keeping a working gate is not drift. +- **A working local commit/pre-commit gate is strongly suggested, not the repo's free choice to skip.** No single hook runner fits every language (a `dotnet`-tool runner like Husky.Net suits .NET but not Python), so the *mechanism* stays the repo's choice: Husky.Net for .NET, `pre-commit` for Python, canonical configs for both in `catalog/snippets/`. What that gate must cover, and what its absence means for the audit, is [GOVERNANCE.md "Running the Linters Locally"][governance-running-the-linters-locally], not restated here. Keeping a working gate is not drift. ### Analyzer Diagnostics and Suppressions diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 4b6dad89..3fa98ccc 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -219,8 +219,10 @@ 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), **shellcheck** and **shfmt** the same way for a repo that carries a shell script, and, **for a repo that carries `.ps1` files**, **PSScriptAnalyzer** the same way (none of the three has an action). 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. A "shell script" here is a tracked `.sh` file, plus a tracked, extension-less file whose shebang names bash or sh, the shape a script meant to run as a bare command takes: a bare `*.sh` glob misses that second case, so both discovery paths run before either tool does. This whole block is the hub's `validate-task.yml` reusable workflow, so a fleet repo reaches it rather than carrying a copy of these steps. -- **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. +- **A working local hook is strongly suggested fleet-wide, and its absence is a measured audit finding, not an invisible gap.** `spec/project-types.json`'s `parity.hooks` check reads this section for its rationale, judged by hand during an `AUDIT.md` run like every sibling check in its dimension, never mechanized by `spec/audit.py`: a repo with no local hook mechanism wired at all is a `linter-parity` defect, the same severity a missing markdownlint config already gets, while a repo mid-convergence (below) stays operational. CI remains the authoritative run regardless. Two catalog snippets carry the canonical shape, `catalog/snippets/husky/` (Husky.Net, for .NET or any project including Python) and `catalog/snippets/pre-commit/` (the Python `pre-commit` framework, for a repo with no `.husky/` tree), each carrying a copy of `catalog/snippets/hub-fetch-run.py` alongside it. +- **The hook** runs **language formatting** and the **diff-scoped doc gates**, never Docker, so it stays fast. The formatting half is whatever the repo's own language needs, CSharpier and `dotnet format` for .NET or ruff and the repo's type checker 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 the working tree diff against `HEAD` 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. That scope is the working tree rather than the staged index, so a partially staged file is judged on all of its edits, not only the staged ones, and CI re-checks the whole tree regardless, which is what makes that scope affordable in a hook. 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 of the hook regardless, since it resolves a same-owner pin against the GitHub API. +- **The doc gates reach a downstream repo by fetching `.github/actions/prose-gate/prose_lint.py` and `.github/actions/repo-gate/repo_gate.py` fresh from `ptr727/ProjectTemplate`'s `main` branch, via `hub-fetch-run.py`, never vendored and never pinned.** Pinning anything Dependabot does not maintain goes stale by construction, and CI (this repo's own, and the hub's) is the backstop that catches a change landing broken on `main` before a locally fetched copy does real damage. These are the only network calls the doc gates make, one per fetched script. A Python repo's `uvx`-run ruff and type checker can also reach the network, to resolve `@latest` on a cache miss or refresh, the same category of dependency as the Docker pulls the VS Code Lint tasks already do routinely, not a new one. A fetch failure fails the commit, and it never silently skips the gate. The **hub's own** `.husky/pre-commit` is the one exception, staying local and offline, since it already carries `scripts/prose_lint.py` and `scripts/repo_gate.py` directly and has no hub to reach. A repo enables its hook per clone with `git config core.hooksPath .husky` or (`uv tool install pre-commit` once, then) `pre-commit install`. The Husky.Net snippet needs one more step per clone, `dotnet tool restore` then `dotnet husky install`, which generates `.husky/_/husky.sh`, the file the hook sources. 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, plus `Lint: Prose` and `Lint: EOL`, the same two hook gates in whole-repo mode rather than diff-scoped, for on-demand full-tree validation. The Docker invocations below run the same tools and configs as the VS Code tasks. Their headless form separates the image pull and minimizes repository exposure for an agent executor. diff --git a/STANDUP.md b/STANDUP.md index 4dbd33c4..ce3feda8 100644 --- a/STANDUP.md +++ b/STANDUP.md @@ -179,6 +179,8 @@ Carry `AGENTS.md`'s skill-dependency pointer paragraph, the one naming `scripts/ Choose the destination while scaffolding rather than after. Repo-specific content left in a carried file is drift, which the audit lists as an undeclared section to reconcile, and reconciling it later means moving prose that downstream readers have already started trusting in the wrong place. +**Wire a local commit hook here, not after the fact.** `.husky/pre-commit` and `.pre-commit-config.yaml` are deliberately excluded from the baseline above (each repo's own formatters make the content repo-owned, per [`spec/divergences.json`][divergences]), so nothing in the carry step above wires one. Copy and adapt the applicable catalog snippet, `catalog/snippets/husky/` for Husky.Net or `catalog/snippets/pre-commit/` for the Python `pre-commit` framework, each with `catalog/snippets/hub-fetch-run.py` alongside it, and enable it (`git config core.hooksPath .husky`, or `uv tool install pre-commit` once then `pre-commit install`) before the section 5 audit run. The Husky.Net snippet needs one more step per clone, `dotnet tool restore` then `dotnet husky install`, which generates `.husky/_/husky.sh`, the file the hook sources. A freshly stood-up repo with nothing wired starts pre-failed on `parity.hooks`. + ## 3. Stand Up the Workflows Implement the Actions that satisfy [`WORKFLOW.md`][workflow] for the repo's type (its section 6 per-type walkthrough): the source-only subset for a source-only repo, the file-target leaf(s) for a publishing repo, the two-workflow shape for an operational config repo. Reuse [`catalog/snippets/workflows/`][workflows] as the reference implementation, satisfying the contract by outcome rather than byte for byte. @@ -236,6 +238,7 @@ The same [`AUDIT.md`][audit] run is the on-demand audit for any known repo, and [audit]: ./AUDIT.md [codestyle]: ./CODESTYLE.md [content-import]: ./docs/content-import.md +[divergences]: ./spec/divergences.json [files]: ./spec/files.json [fleet-map]: ./docs/fleet-map.md [git-commit-conventions]: ./.agents/skills/git-commit-conventions/SKILL.md diff --git a/TODO.md b/TODO.md index c8ddb5a3..61d3a208 100644 --- a/TODO.md +++ b/TODO.md @@ -406,10 +406,8 @@ Blog carries `* -text` plus explicit named `eol=lf` pins plus a dedicated rotted Small work with no research to preserve, selectable one bullet at a time. - **Answer the symmetric reading of [`.editorconfig`][editorconfig], a path-specific section naming files that do not exist**, which is the half of [#633][issue-633] the `eol-coverage` check deliberately left open. The dead-pin reading it does ship is the `.gitattributes` side, and the same question on the other document is not the same shape: this repo's `[.github/workflows/*]` and `[catalog/snippets/workflows/*]` sections are legitimately broad, and the issue's own first attempt at it produced false positives because the matcher did not expand brace syntax, which [`scripts/repo_gate.py`][repo-gate] already implements. Measure the exemption against the live corpus before building the gate rather than after, since a stale exemption hands out a work list that damages correct documents, and decide whether `forward-declared` carries across or whether an editorconfig section needs its own marker. -- **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. The other objection, a hook running the gate from the wrong directory and reporting its own false clean, no longer applies: the rule set, the file set, the diff, and the keys joining them are all read from the repository being scanned rather than from wherever the process stands. - **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 under `scripts/tests/` 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/catalog/README.md b/catalog/README.md index 940689d9..8269cc2e 100644 --- a/catalog/README.md +++ b/catalog/README.md @@ -7,3 +7,4 @@ Reusable reference snippets: concrete config artifacts a repo can copy or compar - `snippets/configs/`: the config exemplars `vscode-tasks.json` (.NET clean-compile task group) and `vscode-tasks-python.json` (the Python equivalent, running `ruff`/type-check/`pytest`, all `type: process` so no `&&` chaining breaks Windows PowerShell 5.1), plus `dependabot.yml` (multi-ecosystem dual-target reference), `docker-hub-readme.md` (the size-limited Docker Hub overview, distinct from the project `README.md`). - `snippets/devcontainer/`: `.devcontainer` definitions for the .NET and Python toolchains. - `snippets/vscode/`: the composable `.code-workspace` fragments: `base.jsonc` (standard set) plus `dotnet.jsonc`, `python.jsonc`, `docker.jsonc` per-type additions. See `snippets/vscode/README.md`. +- `snippets/husky/` and `snippets/pre-commit/`: the two local commit-hook shapes, Husky.Net for .NET (or any project, ruff included) and the Python `pre-commit` framework, both carrying the same shared doc gates (prose/comment-style, line endings) via `snippets/hub-fetch-run.py`. See each directory's own README.md. diff --git a/catalog/snippets/configs/vscode-tasks-python.json b/catalog/snippets/configs/vscode-tasks-python.json index 5f2068c7..e24ce450 100644 --- a/catalog/snippets/configs/vscode-tasks-python.json +++ b/catalog/snippets/configs/vscode-tasks-python.json @@ -123,8 +123,12 @@ // Lint group, run on demand via Docker, where --pull=always forces each to re-pull :latest. // It mirrors the CI lint gate. // Broad live spell-checking is the cspell extension's job. - // Pre-commit does formatting only. - // These tasks are language-agnostic, identical to the .NET snippet's Lint group. + // Pre-commit already runs the prose/EOL gates diff-scoped. + // Lint: Prose and Lint: EOL below add the same two checks in whole-repo mode, for on-demand full-tree validation. + // Lint: EditorConfig/Workflows/Markdown/Spelling are language-agnostic, identical to the + // .NET snippet's Lint group. + // Lint: Prose and Lint: EOL assume the pre-commit-framework layout (hub-fetch-run.py at the workspace root). + // A repo using the Husky.Net snippet instead points these two tasks at .husky/hub-fetch-run.py. { "label": "Lint: EditorConfig", "type": "process", @@ -169,6 +173,28 @@ "clear": false } }, + { + "label": "Lint: Prose", + "type": "process", + "command": "uv", + "args": [ "run", "python", "${workspaceFolder}/hub-fetch-run.py", ".github/actions/prose-gate/prose_lint.py", "." ], + "problemMatcher": [], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "Lint: EOL", + "type": "process", + "command": "uv", + "args": [ "run", "python", "${workspaceFolder}/hub-fetch-run.py", ".github/actions/repo-gate/repo_gate.py", "--check", "eol" ], + "problemMatcher": [], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, { "label": "Lint: All", "dependsOrder": "sequence", @@ -176,7 +202,9 @@ "Lint: EditorConfig", "Lint: Workflows", "Lint: Markdown", - "Lint: Spelling" + "Lint: Spelling", + "Lint: Prose", + "Lint: EOL" ], "problemMatcher": [] } diff --git a/catalog/snippets/configs/vscode-tasks.json b/catalog/snippets/configs/vscode-tasks.json index f8d366fb..f1d1d6de 100644 --- a/catalog/snippets/configs/vscode-tasks.json +++ b/catalog/snippets/configs/vscode-tasks.json @@ -124,7 +124,8 @@ // Lint group, run on demand via Docker, where --pull=always pins each to the current :latest. // It mirrors the CI lint gate. // Broad live spell-checking is the cspell extension's job. - // Pre-commit does formatting only. + // Pre-commit already runs the prose/EOL gates diff-scoped. + // Lint: Prose and Lint: EOL below add the same two checks in whole-repo mode, for on-demand full-tree validation. { "label": "Lint: EditorConfig", "type": "process", @@ -169,6 +170,36 @@ "clear": false } }, + { + "label": "Lint: Prose", + "type": "process", + "command": "python3", + "args": [ "${workspaceFolder}/.husky/hub-fetch-run.py", ".github/actions/prose-gate/prose_lint.py", "." ], + "windows": { + "command": "py", + "args": [ "-3", "${workspaceFolder}/.husky/hub-fetch-run.py", ".github/actions/prose-gate/prose_lint.py", "." ] + }, + "problemMatcher": [], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, + { + "label": "Lint: EOL", + "type": "process", + "command": "python3", + "args": [ "${workspaceFolder}/.husky/hub-fetch-run.py", ".github/actions/repo-gate/repo_gate.py", "--check", "eol" ], + "windows": { + "command": "py", + "args": [ "-3", "${workspaceFolder}/.husky/hub-fetch-run.py", ".github/actions/repo-gate/repo_gate.py", "--check", "eol" ] + }, + "problemMatcher": [], + "presentation": { + "showReuseMessage": false, + "clear": false + } + }, { "label": "Lint: All", "dependsOrder": "sequence", @@ -176,7 +207,9 @@ "Lint: EditorConfig", "Lint: Workflows", "Lint: Markdown", - "Lint: Spelling" + "Lint: Spelling", + "Lint: Prose", + "Lint: EOL" ], "problemMatcher": [] } diff --git a/catalog/snippets/hub-fetch-run.py b/catalog/snippets/hub-fetch-run.py new file mode 100644 index 00000000..672ddd15 --- /dev/null +++ b/catalog/snippets/hub-fetch-run.py @@ -0,0 +1,102 @@ +"""Fetch a ptr727/ProjectTemplate script fresh from `main` and run it in-process. + +Never pinned or vendored: a pin nothing keeps current goes stale by construction, and CI +(this repo's own, and the hub's) is the backstop for a change that lands broken on `main`. +A fetch failure fails the caller loudly rather than silently skipping the gate it guards. +Usage: hub-fetch-run.py [script-args...] +Example: hub-fetch-run.py .github/actions/prose-gate/prose_lint.py . --diff HEAD +""" + +import runpy +import subprocess +import sys +import tempfile +import urllib.error +import urllib.request +from pathlib import Path + +HUB_RAW_BASE = "https://raw.githubusercontent.com/ptr727/ProjectTemplate/main" +EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" + + +def head_is_unborn() -> bool: + """Whether HEAD is confirmed unborn (a real branch, no commits yet), never guessed. + + `git rev-parse --verify -q HEAD` exits 1 with empty stderr for a confirmed unborn HEAD, + verified directly: a fresh `git init` with no commits gives exactly that signature. Any + other shape, a non-git directory (exit 128, a `fatal:` message even with `-q`), a missing + or broken git executable (raised as OSError), a permission error, or any other failure, is + a probe failure to propagate, never a reason to guess at the diff scope. + """ + try: + probe = subprocess.run( + ["git", "rev-parse", "--verify", "-q", "HEAD"], + capture_output=True, + check=False, + ) + except OSError as exc: + print(f"hub-fetch-run: could not run git to probe HEAD: {exc}", file=sys.stderr) + sys.exit(1) + if probe.returncode == 1 and not probe.stderr: + return True + if probe.returncode == 0: + return False + print( + f"hub-fetch-run: git rev-parse --verify -q HEAD failed unexpectedly " + f"(exit {probe.returncode}): {probe.stderr.decode(errors='replace').strip()}", + file=sys.stderr, + ) + sys.exit(1) + + +def resolve_unborn_head(argv: list[str]) -> list[str]: + """Replace a `--diff HEAD` pair with git's empty-tree hash when HEAD does not exist yet. + + A brand-new repository's first commit has no HEAD to diff against, and the fetched gate + scripts refuse to widen to a whole-tree scan rather than report the backlog as new. The + empty tree diffs cleanly against everything staged, which is the correct scope for a first + commit. Any other `--diff` value passes through unchanged. + """ + out = list(argv) + for i, token in enumerate(out[:-1]): + if token == "--diff" and out[i + 1] == "HEAD" and head_is_unborn(): + out[i + 1] = EMPTY_TREE + return out + + +def main(argv: list[str]) -> int: + if not argv: + print( + "hub-fetch-run: usage: hub-fetch-run.py [script-args...]", + file=sys.stderr, + ) + return 2 + hub_path, script_args = argv[0], resolve_unborn_head(argv[1:]) + url = f"{HUB_RAW_BASE}/{hub_path}" + try: + with urllib.request.urlopen(url, timeout=30) as response: + content = response.read() + except (urllib.error.URLError, OSError) as exc: + print(f"hub-fetch-run: could not fetch {url}: {exc}", file=sys.stderr) + print("hub-fetch-run: the gate did not run.", file=sys.stderr) + return 1 + with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as handle: + handle.write(content) + tmp_path = Path(handle.name) + old_argv = sys.argv + try: + sys.argv = [str(tmp_path), *script_args] + try: + runpy.run_path(str(tmp_path), run_name="__main__") + except SystemExit as exc: + if exc.code is None: + return 0 + return exc.code if isinstance(exc.code, int) else 1 + return 0 + finally: + sys.argv = old_argv + tmp_path.unlink(missing_ok=True) + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/catalog/snippets/husky/README.md b/catalog/snippets/husky/README.md index 0494e9d6..6f347f3d 100644 --- a/catalog/snippets/husky/README.md +++ b/catalog/snippets/husky/README.md @@ -1,7 +1,33 @@ # Husky snippet -`pre-commit` is the reference git pre-commit hook (installed under `.husky/` by Husky). It runs **language formatting and style only**: CSharpier and `dotnet format` style via `dotnet husky run` for .NET, or ruff for a Python repo. Native tooling and no Docker is what keeps it fast. +`pre-commit` is the reference git pre-commit hook (installed under `.husky/` by Husky). It +runs **language formatting/lint and the fleet's shared doc gates**: CSharpier and +`dotnet format` style via `dotnet husky run` for .NET, or `ruff format --check` / `ruff check` / +the repo's type checker for a Python repo (native tooling, no Docker, the same checks the +`../pre-commit/.pre-commit-config.yaml` snippet runs), plus the diff-scoped prose/comment-style +gate and the whole-tree line-ending check. Copy `../hub-fetch-run.py` alongside `pre-commit` for +the doc gates to run: it fetches those two checks fresh from `ptr727/ProjectTemplate`'s `main` +branch and runs them, rather than vendoring or pinning a copy. A pin nothing keeps current +goes stale by construction, and CI (this repo's own, and the hub's) is the backstop for a +change that lands broken on `main` before it does real damage locally. These are two more +network fetches alongside the Docker pulls the Lint tasks below already do. A fetch failure +fails the commit rather than silently skipping the gate. -Full linting (line endings, workflow YAML, Markdown, spelling) is **not** run in the hook. It runs in CI as pinned action wrappers, and on demand via the VS Code **Lint** tasks in `catalog/snippets/configs/vscode-tasks.json` (Docker at `:latest`). Keeping the doc linters out of the hook is what keeps it simple. +Full linting (workflow YAML, Markdown, spelling, EditorConfig) is **not** run in the hook. It +runs in CI as pinned action wrappers, and on demand via the VS Code **Lint** tasks in +`catalog/snippets/configs/vscode-tasks.json` (Docker at `:latest`), which also carries the +same prose/EOL gates in whole-repo mode for on-demand full-tree validation, not just the +diff-scoped commit-time run. Keeping the Docker-dependent doc linters out of the hook is what +keeps it fast, and Docker is the dependency it stays free of: the hook still needs network +for the two `hub-fetch-run.py` calls above, so a fully offline clone fails the commit loudly +on the fetch rather than silently skipping the doc gates. -A copied `.husky/pre-commit` is an extensionless shebang script, so pin it to **LF** in `.gitattributes` (`.husky/pre-commit text eol=lf`), git-level enforcement independent of the editor. The fleet's `[*]` `.editorconfig` default already gives it LF, no path-specific override needed. A CRLF shebang breaks execution. Drop the `dotnet husky run` line in a non-.NET repo. +A copied `.husky/pre-commit` is an extensionless shebang script, so pin it to **LF** in +`.gitattributes` (`.husky/pre-commit text eol=lf`), git-level enforcement independent of the +editor. The fleet's `[*]` `.editorconfig` default already gives it LF, no path-specific +override needed. A CRLF shebang breaks execution. + +Both language blocks run unconditionally, with no tool-presence guard: a repo that keeps a +block declares that tool required, so a missing one fails the commit loudly rather than +skipping the check silently. Drop the `dotnet husky run` block in a non-.NET repo, and drop +the Python block in a non-Python repo, rather than leaving it in place to no-op. diff --git a/catalog/snippets/husky/pre-commit b/catalog/snippets/husky/pre-commit index 0820b966..ba07fc01 100644 --- a/catalog/snippets/husky/pre-commit +++ b/catalog/snippets/husky/pre-commit @@ -2,11 +2,44 @@ # shellcheck disable=SC1091 # Generated by Husky install, not present at lint time. . "$(dirname "$0")/_/husky.sh" -# Local pre-commit: language formatting and style only (no Docker). -# Full lint runs in CI and the VS Code Lint tasks. +# Local pre-commit: language formatting/lint, plus the fleet's shared doc gates. +# The doc gates run via hub-fetch-run.py, copied alongside this file. +# It fetches them fresh from ptr727/ProjectTemplate's `main` branch rather than vendoring or pinning them. +# See hub-fetch-run.py's own docstring, and catalog/snippets/husky/README.md, for why. +# Full doc-lint set (Markdown, spelling, workflow YAML, EditorConfig) stays in CI and the VS Code Lint tasks, since those need Docker. + +# The absolute path is captured before the cd below, since $0 resolves against the original directory. +hub_fetch_run="$(cd "$(dirname "$0")" && pwd)/hub-fetch-run.py" + +# Git already runs a hook from the top level, but this is belt and braces for an invocation that does not. +# The doc gates below take a relative scan root, which would otherwise resolve against whatever directory the caller stood in. +cd "$(git rev-parse --show-toplevel)" || exit 1 # .NET: CSharpier + dotnet format style via Husky.Net. -# A Python repo runs ruff here instead. -if command -v dotnet >/dev/null 2>&1; then - dotnet husky run +# Drop this block in a non-.NET repo, rather than guarding it. +# A repo that keeps it declares dotnet required, and a missing tool fails the commit loudly, not silently. +dotnet husky run || exit 1 + +# Python: ruff format + ruff check + the repo's type checker via uv, native tooling. +# Drop this block in a non-Python repo, for the same reason the .NET block above is unguarded. +uv run ruff format --check || exit 1 +uv run ruff check || exit 1 +# Swap for `uv run mypy` if this repo's CI runs mypy instead. +uv run pyright || exit 1 + +# The interpreter is chosen by probing, not assumed, mirroring spec/host-tools.json's own probe order. +# On native Windows the python.org install registers `py`, not `python3`, and a `python3` found on PATH there is often the Microsoft Store alias stub, present but broken. +if python3 --version >/dev/null 2>&1; then + run_py() { python3 "$@"; } +elif py -3 --version >/dev/null 2>&1; then + run_py() { py -3 "$@"; } +else + echo "pre-commit: neither 'python3 --version' nor 'py -3 --version' ran, so the doc gates did not run." >&2 + exit 1 fi + +# The prose gate scopes to the working tree against HEAD, not only the staged index. +# A partially staged file is judged on all of its edits, not only the staged ones. +# CI re-checks the whole tree regardless, which is what makes that scope affordable in a hook. +run_py "$hub_fetch_run" .github/actions/prose-gate/prose_lint.py . --diff HEAD || exit 1 +run_py "$hub_fetch_run" .github/actions/repo-gate/repo_gate.py --check eol || exit 1 diff --git a/catalog/snippets/pre-commit/.pre-commit-config.yaml b/catalog/snippets/pre-commit/.pre-commit-config.yaml new file mode 100644 index 00000000..2e267ea4 --- /dev/null +++ b/catalog/snippets/pre-commit/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +# Reference pre-commit (pre-commit.com) config for a fleet Python repo with no .NET/Husky.Net tree. +# Copy into the repo root alongside ../hub-fetch-run.py. +# Adapt the type-check hook to match this repo's own CI checker (pyright or mypy, per python-codestyle). +# Install and enable with `uv tool install pre-commit` once, then `pre-commit install`. +# The type checker and ruff hooks run via uvx, native tooling, no Docker. +# That needs no project dependency, matching CI's own invocation for the lint-only profile. +# A repo on the build profile with a uv.lock may swap an entry for `uv run ` to pin its own project version instead. +# The prose/EOL hooks fetch their scripts fresh from ptr727/ProjectTemplate's `main` branch via hub-fetch-run.py, rather than vendoring or pinning a copy. +# See that file's docstring for why. +# A fetch failure fails the commit, and it never silently skips the gate. +repos: + - repo: local + hooks: + - id: ruff-format + name: ruff format --check + entry: uvx ruff@latest format --check + language: system + pass_filenames: false + - id: ruff-check + name: ruff check + entry: uvx ruff@latest check + language: system + pass_filenames: false + - id: type-check + name: pyright (swap for "uvx mypy@latest ." if this repo's CI runs mypy instead) + entry: uvx pyright@latest + language: system + pass_filenames: false + - id: prose-gate + name: prose/comment-style gate (diff-scoped, fetched fresh from ProjectTemplate@main) + entry: uv run python hub-fetch-run.py .github/actions/prose-gate/prose_lint.py . --diff HEAD + language: system + pass_filenames: false + - id: eol-gate + name: line-ending consistency gate (fetched fresh from ProjectTemplate@main) + entry: uv run python hub-fetch-run.py .github/actions/repo-gate/repo_gate.py --check eol + language: system + pass_filenames: false diff --git a/catalog/snippets/pre-commit/README.md b/catalog/snippets/pre-commit/README.md new file mode 100644 index 00000000..10453ad2 --- /dev/null +++ b/catalog/snippets/pre-commit/README.md @@ -0,0 +1,35 @@ +# Pre-commit snippet + +`.pre-commit-config.yaml` is the reference config for the Python `pre-commit` framework +(pre-commit.com), for a Python repo with no `.husky/` tree of its own. It runs `ruff format +--check` and `ruff check`. It also runs this repo's declared type checker, `pyright` or +`mypy`, matching whichever `python-codestyle` says this repo's CI runs. Each tool runs via +`uvx`, native tooling, never Docker. `uvx` needs no project dependency, matching CI's own +invocation for the lint-only profile (`CODESTYLE.md` "Two profiles"). A repo on the build +profile with a `uv.lock` may swap in `uv run ` per entry to pin the project's own +version instead. The config also runs the same two shared doc gates the Husky.Net snippet +carries: the diff-scoped prose/comment-style gate and the whole-tree line-ending check. + +Copy `../hub-fetch-run.py` alongside `.pre-commit-config.yaml` (repo root) for the doc gates +to run: it fetches those two checks fresh from `ptr727/ProjectTemplate`'s `main` branch and +runs them, rather than vendoring or pinning a copy. A pin that no tool keeps current goes +stale by construction, and CI (this repo's own, and the hub's) is the backstop for a change +that lands broken on `main` before it does real damage locally. These are two more network +fetches alongside the Docker pulls the VS Code Lint tasks already do. A fetch failure fails +the commit rather than silently skipping the gate. + +Install and enable with `uv tool install pre-commit` once, then `pre-commit install`. +`pre-commit` itself is never added as a project dependency: the lint-only profile has no +project environment to add it to, and `uv tool install` gives a persistent, PATH-available +command independent of any project, the same footing `uvx` gives the tools the hooks run. +If `pre-commit install` reports the command not found right after installing it, `uv tool +install`'s own bin directory is not yet on `PATH`: run `uv tool update-shell` and restart or +re-source the shell, or add the directory `uv tool dir --bin` prints directly. +Full linting (workflow YAML, Markdown, +spelling, EditorConfig) stays out of the hook: it runs in CI as pinned action wrappers, and on +demand via the VS Code **Lint** tasks in `catalog/snippets/configs/vscode-tasks-python.json` +(Docker at `:latest`), which also carries the same prose/EOL gates in whole-repo mode for +on-demand full-tree validation, not just the diff-scoped commit-time run. + +No LF pin is needed for `.pre-commit-config.yaml` itself: it is plain YAML, not a shebang +script, so the fleet's `[*]` `.editorconfig`/`.gitattributes` default already covers it. diff --git a/docs/pre-commit-hooks-rollout.md b/docs/pre-commit-hooks-rollout.md new file mode 100644 index 00000000..d770d958 --- /dev/null +++ b/docs/pre-commit-hooks-rollout.md @@ -0,0 +1,91 @@ +# Fleet Pre-Commit Hooks Rollout + +Tracks the fleet-wide local-hook posture change repo by repo. The policy itself lives in +[GOVERNANCE.md "Running the Linters Locally"][governance-running-the-linters-locally] and the +canonical configs live in [`catalog/snippets/husky/`][snippets-husky] and +[`catalog/snippets/pre-commit/`][snippets-pre-commit]. This doc is the rollout checklist only, +not a restatement of the rule. It is **hub-only** and is not carried downstream, the same way +[`docs/eol-lf-rollout.md`][eol-lf-rollout] is hub-only, because it tracks the hub's own migration +rather than a fact a downstream repo's own docs need to carry. + +**Maintenance rule.** This file is hub-only, so a downstream repo's own conversion PR cannot edit +it, and the audit that judges `parity.hooks` is always hub-authored, never a repo's own report of +itself, per `AUDIT.md`. Once the hub's post-merge audit reports a repo's `parity.hooks` operational, +the hub opens that repo's own small hub-side PR, the next step of the same `resync-a-repo` pass, +and checks the repo's box below in it. A repo's box stays unchecked while that follow-up PR is +pending, which is expected, not stale. It is stale only once the audit has confirmed +`parity.hooks` operational and no follow-up PR exists to show for it. + +## What Changed + +`spec/project-types.json`'s `crossCutting.linter-parity` dimension gained `parity.hooks`, judged +by hand during an `AUDIT.md` run like every sibling check in that dimension, never by +`spec/audit.py` itself: a repo with no local hook mechanism wired at all is a `linter-parity` +defect, the same severity a missing markdownlint config already gets. A repo mid-convergence, the +doc-gate half wired and the language-format half not yet because its own corpus does not pass it +clean, stays operational, per the existing carve-out in GOVERNANCE.md. Two catalog snippets carry +the canonical shape: Husky.Net +(`catalog/snippets/husky/`, for .NET or any project including Python) and the Python `pre-commit` +framework (`catalog/snippets/pre-commit/`, for a repo with no `.husky/` tree). Both now carry the +same shared doc gates, the diff-scoped prose/comment-style gate and the whole-tree line-ending +check, fetched fresh from the hub's `main` branch at run time via `hub-fetch-run.py` rather than +vendored or pinned. + +## Per-Repo Conversion + +Copy and adapt the applicable catalog snippet plus `catalog/snippets/hub-fetch-run.py`. +Enable it (`git config core.hooksPath .husky`, or `uv tool install pre-commit` once then +`pre-commit install`). For the Husky.Net snippet specifically, also run `dotnet tool restore` +then `dotnet husky install`, which generates `.husky/_/husky.sh`, the file the hook sources. +Confirm the doc gates run clean against the repo's current tree. +Open the PR through the repo's normal branching model. +A repo whose corpus does not yet pass its language formatter clean ships the doc-gate half first +and adds the language half once it does, per the mid-convergence carve-out. That partial state is +not a reason to leave the box unchecked, since the doc-gate half alone already satisfies +`parity.hooks`' intent tier. After the downstream PR merges and the hub's post-merge audit +confirms `parity.hooks` operational, open the small hub-side PR the maintenance rule above +describes and check the box below in it. + +A repo declaring neither `csharp` nor `python` in `registry/repos.json` (the `eda` repos, and any +repo with no fleet-covered language) has no language-format half to add: the doc-gate half alone +is the complete, operational state for that repo, not an incomplete one a later resync should +mistake for unfinished work. + +## Rollout Checklist + +Repos and their current `registry/repos.json` `types`, from the hub's own registry as of this +doc's authorship. Archived repos are not tracked. + +- [x] **ProjectTemplate** (`source-only`, `docs`): the hub itself. Doc gates wired in + `.husky/pre-commit` since PR #642, and the ruff/mypy half added in this PR's own commits, + once the corpus was confirmed clean (0 ruff errors, 200 files formatted, mypy clean). +- [ ] **Utilities** (`csharp`, `nuget`): Husky.Net + doc gates. +- [ ] **LanguageTags** (`csharp`, `nuget`, `codegen`): Husky.Net + doc gates. +- [ ] **aiopurpleair** (`python`, `pypi`): `pre-commit` framework + doc gates. +- [ ] **homeassistant-purpleair** (`python`, `homeassistant`): `pre-commit` framework + doc gates. +- [ ] **Financial-Modeling** (`python`, `source-only`): `pre-commit` framework + doc gates. +- [ ] **PlexCleaner** (`csharp`, `dotnet-publish`, `docker`, `python`): Husky.Net (with its ruff + block filled in) covers both languages in one hook, rather than wiring two mechanisms. +- [ ] **ESPHome-NonRoot** (`docker`, `upstream-wrapper`): doc-gate half only, no declared language. +- [ ] **VSCode-Server-DotNetCore** (`docker`): doc-gate half only, no declared language. +- [ ] **NxWitness** (`docker`, `upstream-wrapper`, `codegen`, `csharp`): Husky.Net + doc gates. +- [ ] **HomeAutomation-Config** (`source-only`): doc-gate half only, no declared language. +- [ ] **KiCadLibrary** (`eda`): doc-gate half only, no fleet-covered language. +- [ ] **EspDinIoT** (`eda`): doc-gate half only, no fleet-covered language. +- [ ] **ESPHome-Config** (`source-only`, `python`, `cpp`): `pre-commit` framework + doc gates for + the Python half. `cpp` has no fleet linter declared today, out of scope here. +- [ ] **HomeAssistant-Config** (`source-only`): doc-gate half only, no declared language. +- [ ] **DevKitCIoT** (`eda`): doc-gate half only, no fleet-covered language. +- [ ] **PhotoCleaner** (`csharp`, `dotnet-publish`, `docker`): Husky.Net + doc gates. +- [ ] **MediaTools** (`csharp`, `nuget`): Husky.Net + doc gates. +- [ ] **AudioCleaner** (`csharp`, `dotnet-publish`): Husky.Net + doc gates. +- [ ] **Vantage-Config** (`source-only`): doc-gate half only, no declared language. +- [ ] **HolidayLights** (`source-only`): doc-gate half only, no declared language. +- [ ] **Blog** (`hugo`, `source-only`): doc-gate half only, no declared language. + + + +[eol-lf-rollout]: ./eol-lf-rollout.md +[governance-running-the-linters-locally]: ../GOVERNANCE.md#running-the-linters-locally-known-working-invocations +[snippets-husky]: ../catalog/snippets/husky/README.md +[snippets-pre-commit]: ../catalog/snippets/pre-commit/README.md diff --git a/spec/project-types.json b/spec/project-types.json index 0dca70ae..7db32de5 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -167,7 +167,8 @@ "checks": [ { "id": "parity.markdownlint", "verdict": "letter", "assert": "One .markdownlint-cli2.jsonc drives the VS Code extension, the CLI, and CI, and CI runs it.", "intentRef": "GOVERNANCE.md#running-the-linters-locally-known-working-invocations" }, { "id": "parity.cspell", "verdict": "letter", "assert": "A cspell.json (not only a workspace word list) drives the extension, the CLI, and CI.", "intentRef": "CODESTYLE.md" }, - { "id": "parity.lang", "verdict": "intent", "assert": "ruff/pyright (Python) and editorconfig/csharpier (C#) configs are shared by editor, CLI, and CI where those languages are present.", "intentRef": "CODESTYLE.md" } + { "id": "parity.lang", "verdict": "intent", "assert": "ruff/pyright (Python) and editorconfig/csharpier (C#) configs are shared by editor, CLI, and CI where those languages are present.", "intentRef": "CODESTYLE.md" }, + { "id": "parity.hooks", "verdict": "intent", "assert": "A local hook config file exists in the repo's tracked tree (.husky/pre-commit or .pre-commit-config.yaml), and its content, read directly rather than inferred from per-clone core.hooksPath or pre-commit install state neither of which the audit can observe, invokes at minimum the diff-scoped prose gate and the eol check via hub-fetch-run.py. The hub repo itself is the one exception, calling its own scripts/prose_lint.py and scripts/repo_gate.py directly, since it has no separate hub to fetch from. The language-formatting half (ruff/pyright or mypy, CSharpier/dotnet format) is added once the repo's own corpus passes it clean, a repo mid-convergence rather than a repo out of conformance.", "intentRef": "GOVERNANCE.md#running-the-linters-locally-known-working-invocations" } ] }, "recurring-violations": { diff --git a/spec/third-party-tools.json b/spec/third-party-tools.json index 2ec9664e..f3bcc3a1 100644 --- a/spec/third-party-tools.json +++ b/spec/third-party-tools.json @@ -32,6 +32,7 @@ { "name": "NEbml", "link": "https://github.com/OlegZee/NEbml", "description": "EBML reader and writer for .NET." }, { "name": "Nerdbank.GitVersioning", "link": "https://github.com/dotnet/Nerdbank.GitVersioning", "description": "Version computation from git height." }, { "name": "PaperMod", "link": "https://github.com/adityatelange/hugo-PaperMod", "description": "Hugo theme." }, + { "name": "pre-commit", "link": "https://pre-commit.com/", "description": "Git hook manager for Python." }, { "name": "Proxmox VE", "link": "https://www.proxmox.com/en/proxmox-virtual-environment/overview", "description": "Virtualization platform." }, { "name": "regex101.com", "link": "https://regex101.com/", "description": "Regular expression tester." }, { "name": "RFC 5646 language tags", "link": "https://www.rfc-editor.org/rfc/rfc5646.html", "description": "Language tag standard." },