Stability and Validation Tiers¶
StatsPAI now separates API lifecycle from numerical validation evidence. This is the main correction to the older catalogue: stability='stable' no longer means "R/Stata parity-grade" by itself.
Three Fields¶
| Field | Scope | Meaning |
|---|---|---|
stability |
whole function API | stable, experimental, or deprecated |
validation_status |
evidence for numerical output | certified, validated, api_stable, experimental, or deprecated |
limitations |
parameter/variant gaps | documented unsupported variants inside an otherwise usable function |
Use stability when you care about public API compatibility. Use validation_status when you care about audited numerical evidence.
Current JSS source-snapshot audit counts: 494 certified, 487 validated, 642 api_stable, and 3 experimental registry symbols. The intentionally harsh denominator is that 569 stable auto-registered symbols still lack parity backing; treat them as API-stable, not numerically validated. The audit decomposes that denominator into class-like/function-like and category counts, so breadth remains auditable rather than becoming a hidden validation claim. Within the hand-written stable API surface, the current audit enforces zero unbacked entries: API-only helpers carry unit-contract evidence while remaining api_stable, not numerically validated. Unit and regression tests are API-contract evidence; they do not promote a function to validated without known-truth, reference-parity, external-parity, coverage, or explicit convention evidence. Paper-JSS/replication/results/validation_evidence_audit.{json,md} verifies that all 542 certified/validated symbols have registry-attached evidence notes, that certified symbols carry attached evidence naming an external reference implementation and the tolerance it was held to, and that validated symbols are not backed only by unit/regression tests. Package metadata in the JSS source snapshot is 1.29.0; the source snapshot should still be synchronized with a clean tagged release before final publication. The JSS archive records this boundary in Paper-JSS/replication/results/source_snapshot_manifest.{json,md}, and cd Paper-JSS && make release-audit is the strict gate for a clean tagged final-publication snapshot.
Stability¶
stable: a breaking change to the signature comes only after a release that emits aDeprecationWarning, with aMIGRATION.mdentry.experimental: method/API may shift across minor versions.deprecated: scheduled for removal; replacement should be documented inMIGRATION.md.
Frozen core API¶
The 32 everyday entry points (sp.list_functions(core=True), listed first
by sp.help()) carry a stronger promise: code written against them keeps
running. Their signatures are recorded in tests/core_api_snapshot.json,
and tests/test_core_api_freeze.py (also a pre-push hook) fails on any
change that would break an existing call:
- a parameter disappears (a rename is fine when the old spelling stays
accepted through
@accepts_aliases); - a default changes, or an optional parameter becomes required;
- positional parameters are reordered, or one becomes keyword-only;
- a new parameter has no default;
- a core function is removed.
New optional parameters are not breaks. An intended break goes through a
deprecation release first, then
python scripts/core_api_snapshot.py --write --ack "fn.param=<MIGRATION.md text>",
which refuses unless the quoted text is in MIGRATION.md and keeps the
acknowledgement in the snapshot's history.
Every function card says which promise applies, in api_contract:
api_contract |
Applies to | Promise |
|---|---|---|
frozen |
the core API | snapshot-tested, as above |
deprecation-cycle |
other stable functions | breaking changes announced a minor release ahead, by policy |
unstable |
experimental functions | may change in any minor release |
deprecated |
deprecated functions | scheduled for removal |
api_contract is about the signature. Whether the numbers are checked
against a reference is validation_status, below; the two are independent.
Validation¶
certified: the function was compared against a named external reference implementation on identical inputs and agreed within a pre-registered tolerance — usuallytests/r_parity/+tests/stata_parity/, or a frozen R fixture intests/reference_parity/. Two rows follow CLAUDE.md §5.1 to a Python reference because that is the implementation the method's own authors maintain:sp.metalearneragainsteconmlandsp.dml_sensitivityagainstDoubleML.
The tier is derived, not hand-assigned. src/statspai/_parity_index.json is built from the committed goldens by scripts/build_parity_index.py, and statspai._parity_taxonomy.validation_tier_for is the single map from a parity grade to a tier, shared by the registry and the index. A test asserts the two cannot disagree for any stable symbol, so sp.parity_status(name) and sp.describe_function(name)["validation_status"] always tell the same story — including in an installed wheel, where the index ships in the package.
- validated: known-truth recovery (a closed form or a deterministic DGP with a known parameter) or replication of published numbers, with no same-byte cross-language comparison. Monte Carlo coverage rows and documented convention disclosures are recorded against a function but never earn a tier on their own (statspai._parity_taxonomy.validation_tier_for).
- api_stable: stable public API. Unit/regression tests may attach API-contract evidence here, but that evidence is not numerical validation.
- experimental: mirrors stability='experimental'.
- deprecated: mirrors stability='deprecated'.
Filtering¶
import statspai as sp
sp.list_functions() # all registered functions
sp.list_functions(stability="stable") # stable API
sp.list_functions(validation_status="certified") # parity-backed functions
sp.agent_cards(validation_status="certified") # parity-backed agent cards
spec = sp.describe_function("regress")
spec["stability"] # "stable"
spec["validation_status"] # "certified"
spec["validation_notes"] # parity artifact / reference notes
statspai list --stability experimental
statspai list --validation certified
statspai describe rdrobust
sp.help() prints both STABILITY and VALIDATION count blocks. Per-function help shows Stability:, Validation:, Evidence:, and Known limitations when available.
Promotion Path¶
- Promote
experimentaltostablewhen the public API is ready for SemVer compatibility. - Promote
api_stabletovalidatedwhen analytic/reference parity tests exist. - Promote
validatedtocertifiedwhen the function enters the cross-language or published-reference parity harness. - Remove a
limitationonly when the unsupported variant lands with its own test.
Current Limitation Hotspots¶
These are machine-readable through sp.describe_function(name)["limitations"] and should be treated as the priority backlog for production hardening:
callaway_santanna: repeated cross-sections (panel=False) supportestimator="dr"/"ipw"/"reg"(Sant'Anna-Zhaodrdid_rc/std_ipw_did_rc/reg_did_rc), both control groups and the multiplier bootstrap. Point estimates match Rdid::att_gt(panel=FALSE); the simple-ATT standard errors carry a ~0.15% gap from how the aggregation denominator is formed and are held at 1% (tests/reference_parity/test_cs_rcs_parity.py).clustervars=under RCS requiresbstrap=True.rdrobust: observation-level weights are reserved and raiseNotImplementedError; exact R parity is attached tobwselect="cct"or common manual bandwidths, while the defaultmserdselector is a documented convention.rddensity: native default bandwidths, mass-point ECDF handling, and jackknife CJM local-density inference mirrorrddensity::rddensityon the JSS parity fixture. Manual side-specific bandwidths are still treated as explicit user controls;backend="r"remains available when direct execution of the R package is required.synth: ADH/Synth parity requires the samespecial_predictorsrecipe; SDID/augmented/gsynth rows include documented regularisation or local-optimum convention gaps.causal_forest: the NSW-DW parity row is overlap-diagnostic evidence, not a clean ATT point-estimate parity claim.did_imputation: parity is aggregation-convention sensitive; inspectsp.parity_gap_report()before reporting exact cross-language equality.etwfe: the default panel estimate now reports the Retwfe::emfx(type="simple")/ Statajwdid, estat simpletreated-observation-weighted simple ATT forcgroup="notyet"; usecgroup="nevertreated"for the Rcgroup="never"estimand, orsp.etwfe_emfx(..., weighting="cohort")for the historical StatsPAI cohort-share summary.etwfe/wooldridge_did: cohort-level ATTs changed in 1.27.0.result.detail,sp.etwfe_emfx(type="group")and everyweighting="cohort"aggregation were read off an unsaturated cohort x post regression and were off by up to 37% against Retwfe::emfx(type="group"); they are now aggregated from the saturated cohort x period cells. Thesp.etwfepooled ATT is unchanged. See MIGRATION.md.hal_tmle:variant="projection"is reserved and raisesNotImplementedError.network_exposure: onlydesign="bernoulli"is implemented.etwfe:panel=Falsewithcgroup="nevertreated"is not implemented.continuous_did:method="cgs"is an MVP without full CGS parity.did_multiplegt_dyn: experimental. Effects, placebos, switcher counts,Av_tot_effand the analytic influence-function SEs are pinned toDIDmultiplegtDYN/ Statadid_multiplegt_dyn(Track A 78 and the castle-doctrine reference test);controls=,trends_nonparam=,normalized=andcontinuous=are implemented and pinned toDIDmultiplegtDYN2.3.4 at machine precision (tests/reference_parity/test_dcdh_options_parity.py). Not implemented:trends_lin(an implementation following the reference's documentation does not reproduce its numbers past horizon 0, so it is withheld rather than shipped wrong — seedocs/rfc/multiplegt_dyn.md),predict_het, and the heteroskedastic-weights variant; joint tests come from the cluster bootstrap only.
Auditing¶
python scripts/stability_audit.py
python scripts/stability_audit.py --unbacked
python scripts/stability_audit.py --check
python scripts/stability_audit.py --json
python Paper-JSS/replication/scripts/validation_evidence_audit.py
scripts/stability_audit.py --check fails if any hand-written stable API entry lacks attached validation or API/unit-contract evidence. Auto-registered entries are reported separately because they represent breadth imported into the registry, not the validated numerical core defended in the JSS paper.
The JSS packager also extracts Python source paths from registry evidence notes. The current submission manifest includes 779 such registry evidence files, and Paper-JSS/replication/scripts/verify_submission_package.py fails if any referenced evidence file is absent from the archive.
Programmatic evidence summaries:
sp.parity_gap_report() parses the already-generated 3-way parity table and reports documented convention gaps, missing Stata siblings, priorities, and next actions.
Last updated: JSS source-snapshot validation audit (2026-06-28).