diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 61716533..fecd896a 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -254,30 +254,26 @@ is useful for debugging the dispatcher or previewing action output. ## Pre-commit Hooks -Pre-commit runs on every `git commit`. Run all hooks manually before -pushing: +After `scripts/ci-local.sh --install-hooks`, Git runs `.githooks/pre-commit` +on every `git commit`. From the repository root, the hook runs these steps +in order and stops at the first failure: ```sh -pre-commit run --all-files +scripts/check_no_secrets.sh --staged +cargo fmt --all --check +cargo clippy --workspace --all-features --all-targets --locked -- -D warnings +RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace --locked +scripts/test_baseline.sh ``` -Hooks included: +These check staged secrets, Rust formatting, Clippy warnings, rustdoc warnings, +and the workspace test baseline. `scripts/test_baseline.sh` runs +`cargo nextest run --workspace --locked` and checks the measured test count. +Run `bash .githooks/pre-commit` to invoke the same gate manually. -| Hook | What it checks | -|---|---| -| trailing-whitespace | Removes trailing spaces | -| end-of-file-fixer | Ensures files end with a newline | -| check-yaml / check-toml / check-json | Syntax validity | -| no-commit-to-branch | Blocks direct commits to `main` | -| gitleaks | Detects hardcoded secrets | -| cargo fmt | Rust formatting (`--check` mode) | -| cargo check | Workspace compilation | -| tsc --noEmit | TypeScript type checking | -| markdownlint-cli2 | Markdown style | -| yamllint | YAML style | - -Intentionally excluded from pre-commit (they run in CI instead): -`cargo clippy` (20–30 s), `cargo nextest run` (minutes), `vitest` (minutes). +Vitest is not part of this pre-commit hook; it runs in CI. +`.pre-commit-config.yaml` is an unused remnant of the earlier framework setup, +not the active Git hook configuration. ## Configuration @@ -425,13 +421,12 @@ the script exits non-zero only if something in the summary actually failed. failure. Bypass a single push with `git push --no-verify`; undo the hook entirely with `git config --unset core.hooksPath`. -`.githooks/` already ships a `pre-commit` hook (`cargo fmt --all --check` + -`cargo nextest run --workspace --locked`) alongside the new `pre-push` one. +`.githooks/` also ships the `pre-commit` hook with the ordered checks listed +under [Pre-commit Hooks](#pre-commit-hooks). Both are opt-in via the same `core.hooksPath` setting. Note that `core.hooksPath` is a single switch: pointing it at `.githooks` means Git stops looking in `.git/hooks`, so it supersedes hooks installed by the -`pre-commit` framework (see [Pre-commit Hooks](#pre-commit-hooks) above) — -use one mechanism or the other, not both, per clone. +`pre-commit` framework. Use the repository's `.githooks` setup, not the framework. ### Full workflow replay diff --git a/scripts/check_evidence_claims.py b/scripts/check_evidence_claims.py index d8d380dc..8bd0f3e9 100755 --- a/scripts/check_evidence_claims.py +++ b/scripts/check_evidence_claims.py @@ -807,6 +807,28 @@ def load_test_baseline(root: Path) -> dict: return baseline +def check_pre_commit_commands(root: Path, guide: str) -> list[str]: + """Compare the documented gate with the executable hook, in order.""" + hook = root / ".githooks/pre-commit" + if not hook.exists(): + raise Failure(".githooks/pre-commit is missing") + commands = [ + line.strip() + for line in hook.read_text(encoding="utf-8").splitlines() + if line.strip() + and not line.lstrip().startswith(("#", "set ", "cd ", "echo ")) + ] + section = guide.split("## Pre-commit Hooks\n", 1)[-1].split("\n## ", 1)[0] + block = re.search(r"```sh\n(.*?)\n```", section, re.S) + documented = block.group(1).splitlines() if block else [] + if documented == commands: + return [] + return [ + "docs/developer-guide.md: pre-commit steps differ from .githooks/pre-commit; " + f"expected {commands!r}; documented {documented!r}" + ] + + def main() -> int: root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve() try: @@ -815,6 +837,7 @@ def main() -> int: baseline = load_test_baseline(root) problems = [] + problems += check_pre_commit_commands(root, texts["docs/developer-guide.md"]) problems += check_figure(texts, "Rust tests", baseline["tests"]) problems += check_figure(texts, "frontend tests", baseline["frontend_tests"]) problems += check_figure(texts, "typed actions", count_actions(root)) diff --git a/tests/release/public-claims.test.sh b/tests/release/public-claims.test.sh index f038bcc2..bae23353 100755 --- a/tests/release/public-claims.test.sh +++ b/tests/release/public-claims.test.sh @@ -36,6 +36,7 @@ PYEOF fixture_files=( "${claim_files_from_checker[@]}" + ".githooks/pre-commit" "assets/demo/mcp-flow-mock.sh" # Evidence the numeric claims derive from, and the source the action count is # counted out of. Without these the checker aborts on its own input check and @@ -94,6 +95,14 @@ assert_rejected_with_diagnostic() { done } +# Hook/prose drift must name the changed command, not merely fail on a fixture. +printf '\nnode --version\n' >> "$fixture/.githooks/pre-commit" +assert_rejected_with_diagnostic 'undocumented sixth hook step' 'pre-commit steps differ' 'node --version' +cp "$repo_root/.githooks/pre-commit" "$fixture/.githooks/pre-commit" +sed '/^scripts\/test_baseline.sh$/d' "$repo_root/.githooks/pre-commit" > "$fixture/.githooks/pre-commit" +assert_rejected_with_diagnostic 'documented step removed from hook' 'pre-commit steps differ' 'scripts/test_baseline.sh' +cp "$repo_root/.githooks/pre-commit" "$fixture/.githooks/pre-commit" + # The story coverage sentence is a derived claim, not a second catalogue. Mutate # its published All-family figure without repeating today's value in this test. read -r derived_all published_all < <(