Skip to content

Commit dfc4590

Browse files
test: lock CI evidence contracts
1 parent e3345fe commit dfc4590

7 files changed

Lines changed: 118 additions & 22 deletions

File tree

.github/workflows/ci-cd.yml

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,16 @@ jobs:
9090
python -m pip install --upgrade pip
9191
pip install -e ".[dev]"
9292
93-
- name: Install parity extra (canonical env — runs the doubleml external-parity pins)
94-
# doubleml is the opt-in `parity` extra (not in `dev`). Installing it on
95-
# the canonical ubuntu+3.10 env makes tests/external_parity/
96-
# test_dml_python_parity.py actually run its 2 sp.dml-vs-doubleml pins,
97-
# so the JSS external-parity headline count (52) is verified rather than
98-
# silently skipped. Other matrix entries skip those 2 pins and the
93+
- name: Install exact-parity extras (canonical env — runs DoubleML + CCT pins)
94+
# doubleml and rdrobust are opt-in extras (not in `dev`). Installing them
95+
# on the canonical ubuntu+3.10 env makes tests/external_parity/
96+
# test_dml_python_parity.py run its 4 sp.dml-vs-doubleml pins and makes
97+
# test_published_replications.py run the bwselect='cct' RD pins instead
98+
# of skipping them. Other matrix entries may skip optional pins; the
9999
# count-drift guard degrades gracefully (see test_jss_validation_api.py).
100100
# Mirrors the flake8 / mypy / coverage canonical-env gating below.
101101
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10'
102-
run: pip install -e ".[parity]"
102+
run: pip install -e ".[parity,rd-cct]"
103103

104104
- name: Lint with flake8 (hard syntax errors — all matrix entries)
105105
# E9/F63/F7/F82 are real syntax / undefined-name breakage and must be
@@ -130,11 +130,13 @@ jobs:
130130
# matrices and the same pin used by the flake8/mypy gates — where numpy is
131131
# >= 2.1 and the bug is absent. Every other matrix entry runs the full
132132
# suite without ``--cov``. Root cause: docs/rfc (E1 analysis).
133-
# Coverage baseline: the 2026-05-28 local full-suite run (committed
134-
# ``coverage.xml``, v1.16.0) measured 70.5% line coverage repo-wide
135-
# (61,681 / 87,476 lines). The gate is therefore set to 60 — a 4x lift
136-
# over the historical 15 placeholder, held ~10pp below the measured
137-
# baseline so ordinary fold-split / platform variance cannot red a PR.
133+
# Coverage baseline: the 2026-05-28 local full-suite coverage XML
134+
# snapshot (v1.16.0) measured 70.5% line coverage repo-wide (61,681 /
135+
# 87,476 lines). The gate is therefore set to 60 — a 4x lift over the
136+
# historical 15 placeholder, held ~10pp below the measured baseline so
137+
# ordinary fold-split / platform variance cannot red a PR. The current
138+
# ratchet below consumes the CI-generated coverage.xml from this job; the
139+
# root coverage.xml is a working artifact, not a tracked source of truth.
138140
# Ratchet upward (60 -> 65 -> 70) once each step is confirmed green in
139141
# CI on the canonical ubuntu+3.10 env; never set the gate above the last
140142
# CI-verified number.
@@ -146,8 +148,8 @@ jobs:
146148
# Per-module ratchet for the six core estimators (did iv rd synth dml panel).
147149
# The repo-wide --cov-fail-under above is a soft floor; this gate locks each
148150
# core module at the hard ≥95% the coverage campaign achieved so none can
149-
# silently regress. Reuses the coverage.xml just produced. Ratchet the --min
150-
# upward only after a higher number is confirmed green here.
151+
# silently regress. Reuses the CI-generated coverage.xml just produced.
152+
# Ratchet the --min upward only after a higher number is confirmed green here.
151153
- name: Core-module coverage ratchet (did/iv/rd/synth/dml/panel ≥ 95%)
152154
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10'
153155
run: |

docs/joss_reviewer_guide.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ are named `plotting`, `fixest`, `deepiv`, `neural`, `performance`, `bayes`,
3333
`tune`, `parity`, `rd-cct`, `text`, and `docs`. The `parity` extra adds the
3434
canonical Python DoubleML reference so the `sp.dml` machine-precision check
3535
runs instead of skipping (see *Double Machine Learning vs the DoubleML
36-
reference* below).
36+
reference* below). The `rd-cct` extra adds the official `rdrobust` Python port
37+
used by the exact CCT RD parity pins; the canonical CI job installs both
38+
`.[parity,rd-cct]`.
3739

3840
## Smoke Test
3941

docs/joss_reviewer_qa.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,12 @@ mechanisms make the breadth auditable rather than assertable:
3232
(`test_results_full_suite.md`).
3333
3. **Coverage is honest and rising.** The CI line-coverage gate was lifted
3434
from a 15% placeholder to **60%** against a measured **70.5%** baseline
35-
(committed `coverage.xml`), and a dedicated, tracked campaign
36-
(`.coverage_campaign/`) is driving the six core estimator modules
35+
(historical full-suite coverage XML snapshot). The current CI gate uses the
36+
`coverage.xml` generated inside the same job, while `.coverage_campaign/`
37+
tracks the dedicated campaign driving the six core estimator modules
3738
(`did iv rd synth dml panel`) to **≥95%** line coverage under the full
38-
suite — test-only, with real numerical assertions, no mocking of
39-
numerical paths.
39+
suite — test-only, with real numerical assertions, no mocking of numerical
40+
paths.
4041

4142
The honest framing — also stated in the validation dossier — is that
4243
*certified* methods carry parity or analytic evidence, while *breadth*

scripts/coverage_campaign.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
python scripts/coverage_campaign.py gaps MODULE [--xml PATH] [--per-file]
1919
2020
The default XML is ``.coverage_campaign/baseline_fullsuite.xml`` if present,
21-
else the repo-root committed ``coverage.xml``.
21+
else the repo-root ``coverage.xml`` generated by ``pytest --cov-report=xml``.
22+
The root XML is a working artifact, not a committed source of truth.
2223
2324
This script is read-only over coverage data; it never runs pytest itself so it
2425
stays fast and side-effect free. Generate a fresh XML with, e.g.::
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Static guards for optional exact-parity dependencies in CI."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
8+
ROOT = Path(__file__).resolve().parents[1]
9+
10+
11+
def _read(path: str) -> str:
12+
return (ROOT / path).read_text(encoding="utf-8")
13+
14+
15+
def test_canonical_ci_installs_all_exact_parity_extras():
16+
workflow = _read(".github/workflows/ci-cd.yml")
17+
18+
assert "Install exact-parity extras" in workflow
19+
assert "matrix.os == 'ubuntu-latest' && matrix.python-version == '3.10'" in workflow
20+
assert 'pip install -e ".[parity,rd-cct]"' in workflow
21+
assert "test_dml_python_parity.py run its 4 sp.dml-vs-doubleml pins" in workflow
22+
assert "test_published_replications.py run the bwselect='cct' RD pins" in workflow
23+
24+
25+
def test_optional_dependency_names_match_pyproject_extras_and_skips():
26+
pyproject = _read("pyproject.toml")
27+
dml_test = _read("tests/external_parity/test_dml_python_parity.py")
28+
published_test = _read("tests/external_parity/test_published_replications.py")
29+
30+
assert "parity = [" in pyproject
31+
assert '"doubleml>=0.7"' in pyproject
32+
assert "rd-cct = [" in pyproject
33+
assert '"rdrobust>=1.3"' in pyproject
34+
35+
assert 'pytest.importorskip("doubleml")' in dml_test
36+
assert "all four DoubleML model classes" in dml_test
37+
38+
assert "pytest.importorskip(\n 'rdrobust'," in published_test
39+
assert "extras [rd-cct] are not installed" in published_test
40+
assert r"pip install statspai\[rd-cct\]" in published_test
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Coverage evidence must be generated live, not treated as stale fixtures."""
2+
3+
from __future__ import annotations
4+
5+
import subprocess
6+
from pathlib import Path
7+
8+
9+
ROOT = Path(__file__).resolve().parents[1]
10+
11+
12+
def _read(path: str) -> str:
13+
return (ROOT / path).read_text(encoding="utf-8")
14+
15+
16+
def test_root_coverage_xml_is_not_a_committed_evidence_artifact():
17+
proc = subprocess.run(
18+
["git", "ls-files", "coverage.xml"],
19+
cwd=ROOT,
20+
check=True,
21+
capture_output=True,
22+
text=True,
23+
)
24+
assert proc.stdout.strip() == ""
25+
26+
27+
def test_ci_coverage_ratchet_consumes_same_job_generated_xml():
28+
workflow = _read(".github/workflows/ci-cd.yml")
29+
coverage_step = (
30+
"pytest tests/ -v --cov=statspai --cov-report=xml "
31+
"--cov-report=term-missing --cov-fail-under=60"
32+
)
33+
ratchet_step = "python scripts/coverage_campaign.py report --xml coverage.xml --check --min 95"
34+
35+
assert coverage_step in workflow
36+
assert ratchet_step in workflow
37+
assert workflow.index(coverage_step) < workflow.index(ratchet_step)
38+
assert "CI-generated coverage.xml just produced" in workflow
39+
40+
41+
def test_coverage_campaign_and_docs_do_not_call_root_xml_committed_truth():
42+
script = _read("scripts/coverage_campaign.py")
43+
reviewer_qa = _read("docs/joss_reviewer_qa.md")
44+
45+
assert "committed ``coverage.xml``" not in script
46+
assert "committed `coverage.xml`" not in reviewer_qa
47+
assert "working artifact, not a committed source of truth" in script
48+
assert "The current CI gate uses the" in reviewer_qa
49+
assert "`coverage.xml` generated inside the same job" in reviewer_qa

tests/test_jss_validation_api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,9 @@ def test_validation_report_collected_counts_match_jss_headline():
193193
# The manuscript's external-parity count includes the 4
194194
# doubleml-gated pins; without the optional `parity` extra they
195195
# don't collect, so verifying the floor here would be a spurious
196-
# failure. CI installs `.[parity]` on the canonical env to check
197-
# the full count; skip it in minimal environments instead.
196+
# failure. CI installs `.[parity,rd-cct]` on the canonical env to
197+
# check the full count and to run the CCT RD pins; skip it in
198+
# minimal environments instead.
198199
continue
199200
assert actual >= expected, (
200201
f"{key}: collected {actual} tests, which is BELOW the JSS "

0 commit comments

Comments
 (0)