|
| 1 | +# Pre-commit hooks for StatsPAI. |
| 2 | +# |
| 3 | +# Install once — this wires BOTH the commit-time and push-time stages thanks to |
| 4 | +# ``default_install_hook_types`` below (no separate ``--hook-type pre-push``): |
| 5 | +# |
| 6 | +# pre-commit install |
| 7 | +# |
| 8 | +# Stage layout — mirror the cheap CI gates locally so the self-inflicted CI |
| 9 | +# reds (registry / schema / examples drift, lazy-import regressions) are caught |
| 10 | +# BEFORE they reach GitHub, while keeping commits snappy: |
| 11 | +# |
| 12 | +# * pre-commit — formatting + hard syntax errors (fast, auto-fixing). Every |
| 13 | +# inherited hook pins ``stages: [pre-commit]`` EXPLICITLY: the |
| 14 | +# pre-commit-hooks manifest hard-codes ``stages`` (incl. push) |
| 15 | +# for trailing-whitespace / end-of-file-fixer / large-files, so |
| 16 | +# a config-level ``default_stages`` alone does NOT confine them |
| 17 | +# (verified empirically) — the per-hook override does. |
| 18 | +# * pre-push — deterministic drift / contract guards that mirror |
| 19 | +# parity-guards.yml + the ci-cd import-budget job (~10 s total). |
| 20 | +# None of these modify files. |
| 21 | +# * manual — the heavy 1000+ runnable-examples sweep (CI owns it; run on |
| 22 | +# demand: ``pre-commit run examples-runnable --hook-stage manual``) |
| 23 | +# |
| 24 | +# Two protections worth noting: |
| 25 | +# 1. trailing-whitespace / end-of-file-fixer carry an ``exclude`` for every |
| 26 | +# reference-data tree (parity fixtures, benchmark JSON, NIST .dat). Those |
| 27 | +# are byte-exact anchors from R / Stata / NIST — a whitespace "fix" would |
| 28 | +# corrupt a numerical-parity baseline (JOSS-sensitive). Never let an |
| 29 | +# auto-fixer rewrite them. |
| 30 | +# 2. The flake8 / mypy COUNT ratchets (scripts/quality_gate.py) are NOT |
| 31 | +# mirrored here: they are interpreter/tool-version sensitive (local flake8 |
| 32 | +# 7.x reports ~110 more than CI's pinned 6.1.0), so a local ratchet would |
| 33 | +# red even when CI is green. They live only in CI's pinned canonical env. |
| 34 | +# The pinned flake8 hook below (rev 6.1.0, hard-error select) is the |
| 35 | +# version-stable subset that is safe to run locally. |
| 36 | + |
| 37 | +default_install_hook_types: [pre-commit, pre-push] |
| 38 | + |
| 39 | +# Reference-data trees that must never be touched by an auto-fixing hook. |
| 40 | +# Anchored, used by trailing-whitespace / end-of-file-fixer below. |
| 41 | +# YAML anchor so the two hooks share one definition. |
| 42 | +exclude: &refdata_exclude | |
| 43 | + (?x)^( |
| 44 | + benchmarks/ |
| 45 | + | tests/fixtures/ |
| 46 | + | tests/agent_bench/ |
| 47 | + | tests/coverage_monte_carlo/ |
| 48 | + | tests/numerical_accuracy/ |
| 49 | + | tests/orig_parity/ |
| 50 | + | tests/perf/results/ |
| 51 | + | tests/r_parity/ |
| 52 | + | tests/stata_parity/ |
| 53 | + | tests/reference_parity/_fixtures/ |
| 54 | + | tests/spatial/fixtures/ |
| 55 | + ) |
| 56 | + |
1 | 57 | repos: |
2 | 58 | - repo: https://github.com/pre-commit/pre-commit-hooks |
3 | 59 | rev: v4.4.0 |
4 | 60 | hooks: |
5 | 61 | - id: trailing-whitespace |
| 62 | + stages: [pre-commit] |
| 63 | + exclude: *refdata_exclude |
6 | 64 | - id: end-of-file-fixer |
| 65 | + stages: [pre-commit] |
| 66 | + exclude: *refdata_exclude |
7 | 67 | - id: check-yaml |
| 68 | + stages: [pre-commit] |
8 | 69 | - id: check-toml |
| 70 | + stages: [pre-commit] |
9 | 71 | - id: check-added-large-files |
| 72 | + stages: [pre-commit] |
10 | 73 | - id: check-merge-conflict |
| 74 | + stages: [pre-commit] |
11 | 75 | - id: debug-statements |
| 76 | + stages: [pre-commit] |
12 | 77 |
|
13 | 78 | - repo: https://github.com/psf/black |
14 | 79 | rev: 23.9.1 |
15 | 80 | hooks: |
16 | 81 | - id: black |
17 | 82 | language_version: python3 |
| 83 | + stages: [pre-commit] |
18 | 84 |
|
19 | 85 | - repo: https://github.com/PyCQA/isort |
20 | 86 | rev: 5.12.0 |
21 | 87 | hooks: |
22 | 88 | - id: isort |
| 89 | + stages: [pre-commit] |
23 | 90 |
|
24 | 91 | - repo: https://github.com/PyCQA/flake8 |
25 | 92 | rev: 6.1.0 |
26 | 93 | hooks: |
27 | 94 | - id: flake8 |
| 95 | + # Hard syntax / undefined-name breakage only — must be zero on every |
| 96 | + # interpreter. The full violation-COUNT ratchet is CI-only (pinned env, |
| 97 | + # see note at top of file). |
28 | 98 | args: ["--select=E9,F63,F7,F82", "--show-source", "--statistics"] |
| 99 | + stages: [pre-commit] |
29 | 100 |
|
30 | 101 | - repo: https://github.com/PyCQA/bandit |
31 | 102 | rev: 1.7.5 |
32 | 103 | hooks: |
33 | 104 | - id: bandit |
34 | 105 | args: [-r, src/] |
35 | 106 | exclude: ^tests/ |
| 107 | + stages: [pre-commit] |
36 | 108 |
|
| 109 | + # --------------------------------------------------------------------------- |
| 110 | + # Local CI-mirror guards (pre-push stage). ``language: system`` runs them in |
| 111 | + # the active env — they need ``pip install -e ".[dev]"``. Every one is |
| 112 | + # deterministic (same result locally and in CI) and fast (<4 s each measured |
| 113 | + # at v1.20.0), reads only / never writes, so they gate ``git push`` without |
| 114 | + # the ~1 h pytest sweep that made the old full-pytest hook unusable (and |
| 115 | + # uninstalled). These mirror the checks that actually red the build between |
| 116 | + # releases: parity-guards.yml's registry/schema/examples drift gates and |
| 117 | + # ci-cd.yml's import-budget job. |
| 118 | + # --------------------------------------------------------------------------- |
37 | 119 | - repo: local |
38 | 120 | hooks: |
39 | | - - id: pytest |
40 | | - name: pytest |
41 | | - entry: pytest |
| 121 | + - id: registry-drift |
| 122 | + name: registry_stats --check (public-function / submodule drift) |
| 123 | + entry: python3 scripts/registry_stats.py --check |
| 124 | + language: system |
| 125 | + pass_filenames: false |
| 126 | + always_run: true |
| 127 | + stages: [pre-push] |
| 128 | + |
| 129 | + - id: schema-drift |
| 130 | + name: dump_schemas --check (MCP cold-start bundle drift) |
| 131 | + entry: python3 scripts/dump_schemas.py --check |
| 132 | + language: system |
| 133 | + pass_filenames: false |
| 134 | + always_run: true |
| 135 | + stages: [pre-push] |
| 136 | + |
| 137 | + - id: error-taxonomy |
| 138 | + name: error_taxonomy_audit --check (exception-taxonomy ratchet) |
| 139 | + entry: python3 scripts/error_taxonomy_audit.py --check |
| 140 | + language: system |
| 141 | + pass_filenames: false |
| 142 | + always_run: true |
| 143 | + stages: [pre-push] |
| 144 | + |
| 145 | + - id: examples-coverage |
| 146 | + name: examples_coverage --check (docstring Examples presence, budget 0) |
| 147 | + entry: python3 scripts/examples_coverage.py --check --max-missing 0 |
| 148 | + language: system |
| 149 | + pass_filenames: false |
| 150 | + always_run: true |
| 151 | + stages: [pre-push] |
| 152 | + |
| 153 | + - id: import-budget |
| 154 | + name: cold-import budget (lazy-import contract — 0 heavy submodules) |
| 155 | + entry: python3 -m pytest tests/test_import_budget.py -q --no-header -o addopts= |
| 156 | + language: system |
| 157 | + pass_filenames: false |
| 158 | + always_run: true |
| 159 | + stages: [pre-push] |
| 160 | + |
| 161 | + # Heavy: executes 1000+ docstring examples (>2 min). CI's parity-guards |
| 162 | + # workflow owns this gate; run locally on demand only: |
| 163 | + # pre-commit run examples-runnable --hook-stage manual |
| 164 | + - id: examples-runnable |
| 165 | + name: check_example_execution (docstring Examples runnability) |
| 166 | + entry: python3 scripts/check_example_execution.py --quiet --max-failures 0 |
42 | 167 | language: system |
43 | | - types: [python] |
44 | | - args: [tests/, -v] |
45 | 168 | pass_filenames: false |
46 | 169 | always_run: true |
| 170 | + stages: [manual] |
0 commit comments