Skip to content

Commit 6743693

Browse files
ci: add import-budget pre-push guards
Add a dedicated CI import-budget job and local pre-push guard hooks for drift/import checks. Co-authored-by: Codex <codex@openai.com>
1 parent ac23337 commit 6743693

2 files changed

Lines changed: 165 additions & 5 deletions

File tree

.github/workflows/ci-cd.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,42 @@ jobs:
297297
run: |
298298
pytest tests/ -m 'not slow' -n auto -o addopts=''
299299
300+
# Dedicated fast lazy-import contract gate (~2 min on a warm cache).
301+
# The cold-import budget — a bare ``import statspai`` must pull ZERO heavy
302+
# submodules (sklearn / torch / jax / pymc / pyfixest) — was previously only
303+
# exercised *inside* the ~1 h ``test-pandas3`` full suite, so a lazy-import
304+
# regression (e.g. a new eager ``import sklearn`` in a learner module) took an
305+
# hour to surface. Isolating it here on the default dependency set makes such
306+
# regressions fail fast on every push and PR. Intentionally NOT a ``needs:``
307+
# of build/publish, so it can never block or delay a release upload.
308+
import-budget:
309+
runs-on: ubuntu-latest
310+
311+
steps:
312+
- uses: actions/checkout@v5
313+
314+
- name: Set up Python 3.10
315+
uses: actions/setup-python@v6
316+
with:
317+
python-version: '3.10'
318+
319+
- name: Cache pip dependencies
320+
uses: actions/cache@v5
321+
with:
322+
path: ~/.cache/pip
323+
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
324+
restore-keys: |
325+
${{ runner.os }}-pip-
326+
327+
- name: Install dependencies
328+
run: |
329+
python -m pip install --upgrade pip
330+
pip install -e ".[dev]"
331+
332+
- name: Cold-import budget (lazy-import contract)
333+
run: |
334+
pytest tests/test_import_budget.py -q --no-header -o addopts=''
335+
300336
build:
301337
needs: test
302338
runs-on: ubuntu-latest

.pre-commit-config.yaml

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,170 @@
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+
157
repos:
258
- repo: https://github.com/pre-commit/pre-commit-hooks
359
rev: v4.4.0
460
hooks:
561
- id: trailing-whitespace
62+
stages: [pre-commit]
63+
exclude: *refdata_exclude
664
- id: end-of-file-fixer
65+
stages: [pre-commit]
66+
exclude: *refdata_exclude
767
- id: check-yaml
68+
stages: [pre-commit]
869
- id: check-toml
70+
stages: [pre-commit]
971
- id: check-added-large-files
72+
stages: [pre-commit]
1073
- id: check-merge-conflict
74+
stages: [pre-commit]
1175
- id: debug-statements
76+
stages: [pre-commit]
1277

1378
- repo: https://github.com/psf/black
1479
rev: 23.9.1
1580
hooks:
1681
- id: black
1782
language_version: python3
83+
stages: [pre-commit]
1884

1985
- repo: https://github.com/PyCQA/isort
2086
rev: 5.12.0
2187
hooks:
2288
- id: isort
89+
stages: [pre-commit]
2390

2491
- repo: https://github.com/PyCQA/flake8
2592
rev: 6.1.0
2693
hooks:
2794
- 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).
2898
args: ["--select=E9,F63,F7,F82", "--show-source", "--statistics"]
99+
stages: [pre-commit]
29100

30101
- repo: https://github.com/PyCQA/bandit
31102
rev: 1.7.5
32103
hooks:
33104
- id: bandit
34105
args: [-r, src/]
35106
exclude: ^tests/
107+
stages: [pre-commit]
36108

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+
# ---------------------------------------------------------------------------
37119
- repo: local
38120
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
42167
language: system
43-
types: [python]
44-
args: [tests/, -v]
45168
pass_filenames: false
46169
always_run: true
170+
stages: [manual]

0 commit comments

Comments
 (0)