StatsPAI 1.13.1 — Stability tiers + external-validity dossier + cold-start surgery
Headline
Stability tiers, external-validity dossier, and cold-start surgery in a
single release. Every FunctionSpec now carries a stability field
plus per-function limitations, surfaced through
sp.describe_function, sp.help, sp.list_functions(stability=...),
the statspai list CLI, and the LLM-facing sp.function_schema
description; sp.recommend / sp.causal / sp.paper default to
dropping experimental / deprecated entries unless
allow_experimental=True is passed — closing a path where an agent
could silently land on a frontier MVP. Eight high-impact estimators
(aipw, aggte, pretrends_test, sensitivity_rr, mccrary_test,
oster_bounds, wild_cluster_bootstrap, rd_honest) are upgraded
from auto-registered stubs to hand-written specs with full assumption
/ failure-mode / alternative metadata. A weak-instrument preflight
gate in sp.preflight(data, "ivreg", formula=...) raises a structured
warning row when the first-stage F falls below the Staiger–Stock
(1997) or Stock–Yogo (2005) thresholds, and sp.recommend(... design='iv') adaptively reorders LIML / AR ahead of 2SLS on weak
first stages. A 36-module R parity harness, 21-module Stata parity
harness, 4-dataset original-paper replay (Card 1995, Callaway–Sant'Anna
mpdta, Abadie Basque, LaLonde NSW + PSID-1), Track-C performance
harness (HDFE / CS-DiD / SCM / DML log-log scaling), B=1000
Monte-Carlo coverage run, and a 900-trial CausalAgentBench prompt
suite all ship under tests/r_parity/, tests/stata_parity/,
tests/orig_parity/, tests/perf/, and tests/agent_bench/ with
paired R/Python/Stata drivers, JSON results, and 3-way Markdown +
LaTeX parity tables suitable for direct paper inclusion. A new
sp.validation_report() / sp.coverage_matrix() /
sp.reproduce_jss_tables() meta-API summarises the live registry,
materialises the parity / coverage / agent-bench artifacts as JSON,
and optionally re-runs the harnesses end-to-end so referees can
verify StatsPAI's external-validity claims without leaving Python.
Cold-start surgery in three steps brings sklearn submodules pulled by
import statspai from 245 to 0 (statspai.forest lazy-loaded —
Step 1B; 18 estimator files import sklearn lazily inside function
bodies — Step 1C; HAL TMLE classes drop sklearn class inheritance —
Step 1D), pinned by a new
test_sklearn_budget_ceiling_on_bare_import_statspai contract. The
workflow / paper orchestration layer replaces silent except: pass
paths with WorkflowDegradedWarning + structured degradations
records on the result object, so optional-stage failures surface in
PaperDraft.to_dict() and the rendered Pipeline notes section
instead of disappearing. sp.principal_strat(instrument=...) ships a
proper Angrist-Imbens-Rubin Wald-LATE estimator (the kwarg was
previously stubbed); sp.hal_tmle(variant='projection') keeps its
NotImplementedError but now points at a written-out RFC
(docs/rfc/hal_tmle_projection.md) instead of raising in silence.
Lazy-loading of optional families via __getattr__ keeps import statspai fast without breaking same-name function/subpackage
collisions (bartik, deepiv, proximal, …) — pinned by a
late-bind / post-import-shadow contract test and a committed
__init__.pyi stub generator so IDE / mypy see lazy-loaded names. A
latent Callaway–Sant'Anna REG inference scaling bug — discovered
because the parity harness flagged it — is fixed in
did/callaway_santanna.py.
Added
-
Weak-instrument preflight gate in
sp.preflight(data, "ivreg", formula=...). The newfirst_stage_strengthcheck parses the
Wilkinson IV formula, runs the first-stage OLS, and emits a
warningrow when the partial F-statistic falls below either the
Staiger–Stock (1997) rule of thumb (F < 10, "very weak") or the
Stock–Yogo (2005) 10% maximum-size critical value of 16.38 for
one endog / one instrument (F < 16.38, "weak"). The warning
payload includes structuredrecovery_hintspointing at
method='liml',inference='ar', andsp.anderson_rubin_ci(...)
so an LLM agent can branch on the typed envelope without parsing
prose. Closes the §5.3 robustness DGP follow-up: Track B's
test_iv_weak_instrument_undercoveragedocuments that 2SLS+HC1
under-covers at the 0.88 level on a pi=0.10 first stage; the
preflight now flags this before the user pays for the 2SLS fit. -
Adaptive IV ranking in
sp.recommend(... design='iv'). When
the live first-stage F is below 10 the recommendation list is
reordered: LIML moves to the top, an Anderson–Rubin row is
inserted, and the 2SLS row is annotated with
very_weak_iv=Trueplus a rationale that explains the
HC1-coverage failure mode. When F is in [10, 16.38) the order
stays 2SLS → LIML but the rationales reference the
Stock–Yogo threshold. The 2SLS row now also carries
first_stage_Fas a numeric field so downstream tooling can
consume it without regex. -
Preflight / recommend tests.
tests/test_preflight.pyadds
TestIVFirstStageStrength(6 cases covering strong / weak /
borderline / non-IV / missing-columns / JSON-safe payloads).
tests/test_smart_workflow.py::TestRecommendadds two cases
pinning the 2SLS-first ordering on strong instruments and the
LIML-first / AR-included ordering on weak instruments. -
R parity harness — 36 paired R/Python modules. New
tests/r_parity/ships 36 paired scripts (one R, one Python) that
replay the same DGPs throughfixest/did/csdid/gsynth/
MatchIt/DoubleML/rdrobust/Synth/lme4/plm/
frontier/MR-PRESSO/lavaan/mediation/WeightIt/
cobalt/mlogit/nlme/ordinal/ … and StatsPAI's matching
estimator. Each module emits<id>_R.jsonand<id>_py.json;
compare.pyproduces a 3-way parity table (parity_table.md/
.tex/parity_table_3way.md/.tex) tightened with a small
ID-column + longtable layout for direct paper inclusion. Two
parallel tracks — "orig" (canonical-dataset replays) and "perf"
(timing under matched DGPs) — share the same compare-tooling. -
Stata parity harness — 21-module StatsPAI ↔ Stata 3-way
compare. Newtests/stata_parity/ships 21 paired
.do/.pyscripts coveringreghdfe,xtreg,csdid,did_imputation,
synth,synth_runner,ivreg2,xtivreg,rdrobust,
psmatch2,teffects,xtfrontier,mixed/melogit/
mepoisson,bayes,gmm,boottest, plus the Stata→Python
translator round-trip. Drivers write Stata results to JSON via the
stata-mcpstata_dotool; Python drivers run StatsPAI through
import statspai as sp;compare_stata.pyjoins on(module, estimator, statistic)and emitsparity_table_stata.md/
parity_table_stata.texplus the 3-way StatsPAI ↔ R ↔ Stata table. -
Canonical-dataset original-paper replays. New
tests/orig_parity/adds 4 module pairs that replay each paper's
headline number bit-equal to the published value: Card (1995)
returns-to-schooling onwooldridge::card; Callaway–Sant'Anna
(2021) staggered DiD ondid::mpdta(the package's vendored
minimum-wage panel); Abadie–Diamond–Hainmueller Basque on
Synth::basque; LaLonde (1986) NSW onMatchIt::lalondeplus a
4b sub-module oncausalsens::lalonde.psid(true Dehejia–Wahba
NSW + PSID-1, 2675 obs) wheresp.regress+sp.psmrecover the
published −15,205 to relative tolerance 1.5e-05. Drivers + bundled
data + JSON results +parity_table_orig.mdare all committed. -
Track-C performance harness — log-log timing. New
tests/perf/adds matched R/Python timing runs for HDFE
(fixest::feolsvssp.feols), CS-DiD (did::att_gtvs
sp.callaway_santanna), SCM (Synth::synthvssp.synth), and
DML (DoubleML::DoubleMLPLRvssp.dml) at log-spaced N. Drivers
pluscompare_perf.pyproduceperf_table.md/.texand a
track_c_loglog.{pdf,png}log-log scaling figure. -
Coverage Monte Carlo at B=1000. New
tests/coverage_monte_carlo/run_b1000.pymeasures 95% CI coverage
for OLS (0.952), 2×2 DiD (0.955), and strong-Z IV (0.962) — all
inside the 99% Wilson band [0.935, 0.967] around nominal 0.95. The
full slowpytest tests/coverage_monte_carlo/ -m slowsweep at
B=1000 also passes 8/8 (753.51 s). Frozen run lives at
results_b1000/coverage_b1000.json; previous fast-track results
remain at the default B=200. -
CausalAgentBench scaffolding (mock-mode shipped, API run gated).
Newtests/agent_bench/ships a 50-prompt × L1/L2/L3 difficulty
× 6 cells × 3 reps = 900-trial agent bench with a deterministic
mock-LLM runner (runners/mock_llm.py), a frozen OSF
pre-registration protocol (prompts/_protocol.md), and a grader
(runners/grader.py) emitting an H1–H5 directional results table.
Mock dry-run completes in <1 s and produces
results/headline.md+results/scores.csv+results/trials.jsonl;
the production--apiflag is one switch away once the OSF
pre-registration and API budget clear. -
sp.validation_report/sp.coverage_matrix/
sp.reproduce_jss_tables— JSS-grade validation meta-API. New
src/statspai/validation.py(863 LOC, 58-test battery in
tests/test_jss_validation_api.py) exposes three top-level
functions for the paper-submission audit trail:
sp.validation_report()summarises the livesp.registryplus
the materialised parity / coverage / agent-bench artifacts as a
structuredValidationReport(registry.total_functions,
evidence.r_parity_modules,evidence.stata_parity_modules,
evidence.coverage_b1000,evidence.agent_bench_trials, …) with a
one-paragraph.summary()and full JSON.to_dict();
sp.coverage_matrix()enumerates every reference-implementation
parity claim with its expected tolerance, observed gap, and the
driver script that produced the JSON;sp.reproduce_jss_tables()
returns aReproductionResultenumerating the exactRscript,
python,pytest, andxelatexcommands needed to regenerate
every table — and, when called withdry_run=False, executes them
in dependency order with timing + return codes recorded per
step. The default mode is metadata-only (no R / Stata / LaTeX
required), soimport statspai as sp; print(sp.validation_report().summary())
works on a stockpip install statspai==1.13.1install. -
8 high-impact estimators upgraded from auto-registered to
hand-written FunctionSpec.aipw,aggte,pretrends_test,
sensitivity_rr,mccrary_test,oster_bounds,
wild_cluster_bootstrap, andrd_honestnow ship with full
agent-native metadata: 2–4 assumptions per spec, 2 failure modes
with recovery hints, ranked alternatives, typical_n_min, vetted
references with paper.bib bib keys, and full enum-validated
ParamSpecs. Previously these were auto-registered with only the
first docstring line and inferred parameter types — agents calling
sp.describe_function('aipw')could not see the doubly-robust
guarantee, the propensity overlap requirement, or the alternatives
to fall back on. Hand-written count moves from 203 to 211; auto-
registered drops from 768 to 760. (Step H of v1.13 stability
roadmap.) -
sp.principal_strat(instrument=...)— encouragement-design AIR /
Wald LATE. The previously-stubbedinstrument=parameter now
routes to a proper estimator (Angrist-Imbens-Rubin 1996 §4): given
binary instrumentZ, treatmentD, post-treatment stratumS,
and outcomeY, under randomZ+ monotonicity D(1)>=D(0) +
exclusion + SUTVA, the function reports two Wald LATEs among
Z-compliers —τ_Yfor the effect ofDon the outcome andτ_S
for the effect ofDon the post-treatment stratum variable —
plus the complier shareπ_C(Z), all with bootstrap SE/CI. A
RuntimeWarningis emitted when the first stage degenerates or points
in the wrong direction for the supplied instrument coding.
method=is ignored on this path because identification comes from
Z, not from the post-treatment stratum decomposition. The
limitationsentry is rewritten: the only remaining gap on this
path is always-survivor SACE under encouragement design (Mealli &
Pacini 2013, partial identification). Seven new tests in
tests/test_principal_strat.py. -
sp.hal_tmle(variant='projection')RFC + sharper error. Rather
than ship an unverified port of the Li-Qiu-Wang-vdL (2025) §3.2
Riesz-projection step (the v1.11.x code path was a no-op on the
point estimate — see CHANGELOG), v1.13 keeps the
NotImplementedErrorand addsdocs/rfc/hal_tmle_projection.md
with the full implementation roadmap and the parity-test gates that
must clear before the variant can be promoted tostable. The
runtime exception message now points at the RFC and asks reporters
to file an issue with the publication's headline number they'd like
to match — so the next maintainer to pick this up has a clear
target. Registrylimitationsentry updated with the RFC link. -
Smart layer respects
FunctionSpec.stability.sp.recommend(...),
sp.causal(...), andsp.paper(...)now accept an
allow_experimental: bool = Falseflag (default agent-safe). When
False, recommendations whose backing function is registered as
stability='experimental'(or'deprecated') are dropped from
the ranked output and the workflow'swarnings/
pipeline_notesrecords what was filtered. PassTrueto
include frontier MVPs (e.g.did_multiplegt_dyn,
text_treatment_effect). This closes a gap where an LLM agent
askingsp.causal(df, ...)for a publication-grade analysis could
silently land on a frontier MVP just because the recommender
ranked it first. Tests intests/test_smart_stability_gating.py. -
Stability reverse-audit script.
scripts/stability_audit.py
cross-checks everystability='stable'claim in the registry
against parity-test coverage intests/reference_parity/and
tests/external_parity/. Splits the catalogue into hand-written
vs. auto-registered specs (the latter having been silently
classifiedstableby default) and reports the count of unbacked
claims in each bucket.--checkmode is CI-friendly and fails when
the unbacked-handwritten count exceeds a loose floor (currently
220) — bumping the floor requires editing the script as a
deliberate quality signal. Does NOT auto-downgrade; the call to
flip a function fromstabletoexperimentalbelongs to a
maintainer who has read the code. Tests in
tests/test_stability_audit.py. The audit fixed a registry bug
along the way: auto-registered specs were never tagged_auto=True
on theFunctionSpecinstance, sodescribe_functionerror hints
and the audit itself couldn't distinguish them from hand-written
entries; that's now fixed viaobject.__setattr__inside
_auto_spec_from_callable. -
Runtime consistency tests for
FunctionSpec.limitations. Each
limitationsentry on aFunctionSpecis now structurally audited
bytests/test_limitations_consistency.pyso the registry's
parity-grade-with-known-gaps claims cannot drift away from runtime
behaviour: every entry must (a) use vetted vocabulary and (b) be
classified as either runtime-testable (a curated map calls the
function with the unimplemented value and asserts the documented
exception) or descriptively-soft (silent fallback / caveat,
whitelisted inLIMITATIONS_DESCRIPTIVE_ONLY). Adding a new
limitation without classifying it now fails CI. Caught one drift
bug in this pass: thecgroup='nevertreated' + panel=False
limitation was attached towooldridge_did, but only theetwfe
alias exposes those parameters — moved toetwfeand surfaced the
missingcgroupParamSpec to the schema. -
Test-coverage battery for the four worst-covered files +
parity-grade smoke battery acrossdid/synth/rd/iv/tmle/bayes.
The v1.12.x audit flagged six causal-family modules at low
statement coverage (did14.7%,synth12.9%,rd16.9%,
iv18.0%,tmle14.8%,bayes14.1%) with four files entirely
unexercised:wooldridge_did.py,did_imputation.py,
synth/report.py,workflow/paper.py. Five new test files raise
per-file coverage to synth/report.py 4% → 81%, wooldridge_did.py
76% → 93%, did_imputation.py 85% → 99%, workflow/paper.py 66% →
86% and add a 30-test cross-family smoke battery
(tests/test_low_cov_battery.py) that exercises every headline
estimator's CI/SE/point-estimate contract:tests/test_synth_report.py(25 tests) — full text/markdown/LaTeX
SCM report renderer + every sensitivity sub-block + the LaTeX
escape table.tests/test_wooldridge_did_branches.py(31 tests) — Bacon + dCDH
decomposition, repeated-CS / never-only / xvar dispatch branches,
everyetwfevalidation guard, all fouretwfe_emfx
aggregations includinginclude_leads=True.tests/test_did_imputation_branches.py(14 tests) — every
ValueErrorguard, the controls + horizon event-study path with
pre-trend chi-squared test, and the_cluster_se_horizon
N_k == 0short-circuit.tests/test_paper_branches.py(31 tests) — every YAML/TeX/MD
helper, all fourto_qmdrendering branches (single vs.
multi-format, author / bibliography / csl),to_docxfallback
whenpython-docxis missing,write()extension dispatch, and
the_render_dag_sectiontext + mermaid branches.
-
CausalResult.summary()accepts both event-study column
conventions. The sharedsummary()previously hard-coded
(relative_time, att)and crashed withKeyError: 'relative_time'
onwooldridge_did/etwferesults, which carry the
(rel_time, estimate)schema instead. The renderer now auto-detects
whichever pair is present and silently skips the event-study block
when neither is — every existing caller keeps its formatting and
the wooldridge family no longer crashes a user's.summary()call.
Regression-pinned bytest_wooldridge_did_summary_renders_event_study. -
Stability tiers and per-function
limitations(parity-grade vs.
frontier-grade visibility). EveryFunctionSpecnow carries a
stabilityfield ("stable"/"experimental"/"deprecated",
exposed assp.STABILITY_TIERS) and alimitationslist that
enumerates partial-implementation gaps inside otherwise stable
functions (e.g.hal_tmle(variant='projection'),
principal_strat(instrument=...),rdrobust(weights=...)). The
fields flow throughsp.describe_function,sp.agent_card,
sp.function_schema(description prefix +Known limitations:
suffix so LLM tool-callers see the gap before calling),
sp.list_functions(stability=...),sp.agent_cards(stability=...),
theSTABILITYblock insp.help(), the per-function detail in
sp.help('<name>'), and a newstatspai list --stability ...CLI
flag. This closes a layering gap where users (and agents) could not
tell which functions are numerically aligned and signature-locked
vs. which are MVP / RFC-tracked frontier work, and where specific
unimplemented variants were only discoverable by triggering
NotImplementedErrormid-pipeline. Initial tagging covers the three
causal_text /did_multiplegt_dynexperimental entries plus
variant-level limitations onhal_tmle,principal_strat,
rdrobust,callaway_santanna,wooldridge_did,
network_exposure, andcontinuous_did. See the new
docs/guides/stability.mdfor the contract and promotion path.
Changed
-
Cold-start: lazy-load
statspai.forest(Step 1B).import statspaipreviously chained
from .forest.causal_forest import CausalForest, causal_forestplus
three sibling eager imports forforest_inference/
multi_arm_forest/iv_forestat module load, transitively pulling
~245sklearn.*submodules intosys.modules(~270 ms cumulative on
cold cache) for every session — even ones that never touch
heterogeneous-effect forests. The four eager lines are removed; the
ten public leaves (CausalForest,causal_forest,
calibration_test,test_calibration,rate,honest_variance,
multi_arm_forest,MultiArmForestResult,iv_forest,
IVForestResult) now resolve via_LAZY_ATTRSkeyed to dotted
submodule paths (e.g.forest.causal_forest) and fault in on first
sp.<name>access.forestdoes not collide with a top-level function
(nosp.forestcallable export) so the standard lazy path is safe;
sp.causal's callable shim and thestatspai.causaldeprecation
shim continue to work unchanged. Pinned by three new contracts in
tests/test_late_bind_contracts.py—import statspaimust not
pre-load anystatspai.forest.*submodule (subprocess-isolated to
avoidsys.modulespollution that would corrupt downstream
isinstancechecks); each of the 10 forest leaves must resolve to a
callable on first access; and a downstream
from statspai.forest.causal_forest import CausalForestmust not
re-shadowsp.causal_forestto the leaf module via Python's
post-import attribute binding. Other sklearn-eager paths
(did/overlap_did,metalearners/*,policy_learning/*,
synth/cluster, plus ~7 conflict-prone same-name modules pinned eager
for the late-bind contract) still pullsklearnon bare import; those
are tracked separately for Step 1C and do not block this lazy-forest
win. -
Cold-start: drop sklearn class inheritance from HAL estimators
(Step 1D).HALRegressor/HALClassifierin
tmle/hal_tmle.pypreviously subclassed
sklearn.base.BaseEstimatorplus a Mixin, which pulled ~39
sklearn.*submodules intosys.modulesat module-load time —
the only remaining sklearn footprint after Steps 1B/1C lazy-loaded
forestand the 18 estimator files. The inheritance is gratuitous
here:super_learner.fitonly needssklearn.base.clone(learner)
(which is duck-typed —get_params(deep=False)+cls(**params)
reconstruction) plus.fit/.predict/.predict_proba; no
code path calls.score(...),is_classifier(...), or
is_regressor(...)on the HAL classes. Replaced the inheritance
with a minimal_BaseHALproviding theget_params/
set_params/__repr__slice thatclone()actually
consumes (introspection viainspect.signature(self.__init__),
with object identity preserved so sklearn's post-clone
param1 is param2sanity check passes)._estimator_type = "regressor"/"classifier"class attributes keep
sklearn.base.is_regressor/is_classifierreturning True for
any future external caller. After Step 1D,import statspaipulls
zero sklearn submodules — full 245 → 0 — and the
test_sklearn_budget_ceiling_on_bare_import_statspaicontract is
tightened from<= 50to<= 0to pin the floor. 152 tests
acrosstest_hal_tmle/test_tmle/test_late_bind_contracts
/test_low_cov_battery/test_metalearnerspass cleanly. -
Cold-start: lazy-import sklearn across 18 estimator files (Step
1C). Building on Step 1B, every remaining top-level
from sklearn.X import Yin
did/overlap_did.py,metalearners/{auto_cate,metalearners,auto_cate_tuned}.py,
policy_learning/{policy_tree,ope}.py,synth/cluster.py,
proximal/pci_regression.py,bcf/{bcf,longitudinal}.py,
tmle/{tmle,super_learner,ltmle,ltmle_survival}.py,
dose_response/gps.py,multi_treatment/multi_ipw.py,
mediation/four_way.py, andinterference/orthogonal.pywas moved
inside the function bodies that actually use it.BaseEstimator
type annotations were converted to string-literal form under
if TYPE_CHECKING:soinspect.signature/ Pyright / mypy still
resolve them without forcingsklearn.baseat module load. Several
long-standing dead imports were dropped (BaseEstimator/
is_classifier/cross_val_predictin
metalearners/metalearners.py;LinearRegressionin
proximal/pci_regression.py;BaseEstimator/clone/
GradientBoostingClassifierinmulti_treatment/multi_ipw.py;
etc.). After Step 1B + 1C,import statspaipulls 39 sklearn
submodules instead of 245 — a 5.3× reduction. The 39 are
sklearn.baseplus its mandatory deps, pulled bytmle/hal_tmle.py
whoseHALRegressor(BaseEstimator, RegressorMixin)/
HALClassifier(BaseEstimator, ClassifierMixin)need sklearn at
class-definition time; refactoring that inheritance hierarchy is
out of scope. Pinned by a new
test_sklearn_budget_ceiling_on_bare_import_statspaicontract in
tests/test_late_bind_contracts.py(≤ 50 ceiling, ~39 floor + 11
slack for sklearn-version drift) running in a subprocess so the
cold-state measurement does not perturb other tests'sys.modules.
248 tests across the 18 affected modules (metalearners /
metalearner_frontiers / auto_cate / auto_cate_tuned / overlap_did /
tmle / hal_tmle / proximal / proximal_frontiers / bcf_longitudinal /
bcf_ordinal / conformal_bcf_bunching_mc / policy_learning / mediation
/ mediation_sensitivity / interference_extensions / late_bind_contracts
/ causal_forest_grf / forest_inference / ope_cevae / ope_extensions /
cluster_rct) pass cleanly. -
README lead aligned with the agent-native + parity-validated
positioning.README.mdandREADME_CN.mdboth foreground "first
agent-native Python platform" and surface R / Stata parity
validation in the lead paragraph (was previously buried in the Task
View comparison section further down). -
sp.recommend()now defaults to an agent-safe stability gate:
recommendations whose registry entry is marked
stability='experimental'orstability='deprecated'are dropped
unless the caller passesallow_experimental=True. The filter keeps
backward compatibility for unknown custom recommendation entries,
records dropped names inRecommendationResult.warnings, and is
forwarded throughsp.causal(..., allow_experimental=...)and
sp.paper(..., allow_experimental=...)so higher-level workflows
cannot silently land on frontier MVP estimators. -
Hardened the workflow/paper orchestration layer so optional failures
no longer disappear silently.sp.causal(...).run(full=True)now
records optional-stage failures (compare_estimators,
sensitivity_panel,cate) inworkflow.pipeline_notes, and
sp.causal(...).report(fmt='markdown')renders those notes in a
dedicated section instead of silently dropping the context. -
sp.paper(...)now constructs its internalCausalWorkflowwith
auto_run=Falseand advances stages exactly once. This removes the
prior double-execution path where the workflow could fully auto-run
beforepaper()manually re-randiagnose/recommend/estimate
(and sometimesrobustness) again. -
PaperDraftnow surfaces orchestration degradations directly in a
Pipeline notessection and includesdegradationsinto_dict(),
so missing DAG/citation/provenance/section-rendering steps are
visible in the artifact itself rather than only via warnings.
Fixed
-
⚠️ Correctness —sp.callaway_santanna(method='reg')inference.
The Callaway–Sant'Anna outcome-regression (REG) path produced
influence-function standard errors that were inconsistent with the
IPW / DR variants because the control-regression uncertainty was
not propagated and the per-cohort scaling in the influence-function
aggregation was off by the cohort-size weighting. Coverage
simulations under thempdtaDGP were running ~88% (nominal 95%)
onmethod='reg', while'ipw'and'dr'were inside the 99%
Wilson band. The fix tightens the REG influence-function scaling
and explicitly adds the control-regression contribution; the
parity table attests/r_parity/results/parity_table_3way.mdand
the coverage frame attests/coverage_monte_carlo/FINDINGS.mdare
refreshed accordingly. Regression-pinned by
tests/reference_parity/test_did_parity.pyand the new
tests/r_parity/04_csdid.pydriver. Re-run any v1.10–v1.13
Callaway–Sant'Anna analyses that usedmethod='reg';
'ipw'and'dr'are unchanged. -
isinstance(res, sp.OPEResult)no longer false-negative on results
fromsp.ope.*. During the lazy-load refactor of optional families
the eager re-export path that used to bindsp.OPEResultto
statspai.ope.estimators.OPEResultwas dropped, sosp.OPEResult
silently resolved to a parallel class defined in
statspai.policy_learning.ope— andisinstance(sp.ope.ips(...), sp.OPEResult)flipped fromTrue(v1.12.2) toFalse. The eager
from .policy_learning import ... OPEResultis removed so
sp.OPEResultfalls through to the lazy_register_lazy("ope", "OPEResult", ...)table, restoring v1.12.2 class identity.
Regression-pinned bytests/test_ope_cevae.py::test_ips_close_to_true_value. -
Hand-written registry specs for
aggteandprincipal_stratnow
exactly match their callable signatures (na_rm,alpha,seed),
with a regression test guarding the new v1.13 hand-written upgrades
against future signature drift. -
The natural-language
sp.paper(data, question, ..., include_robustness=False)
path no longer runs or renders the robustness section implicitly via
sp.causalauto-run side effects. -
paper_from_question()now carries its collected degradation records
into the returnedPaperDraft, so late provenance/citation/DAG
failures remain inspectable after draft construction. -
Top-level
statspai.__all__is now de-duplicated in order-preserving
fashion, reducing public-surface drift between the import namespace
and registry/help tooling. -
The top-level function-first API now survives the
sp.ivbootstrap
path for same-name families likebartikanddeepiv. The root
package eagerly rebinds the 14 function/subpackage collisions
(proximal,principal_strat,bartik,bridge,causal_impact,
bcf,bunching,deepiv,dose_response,frontier,
interference,msm,multi_treatment,tmle) while
statspai.ivlazy-loads its optionalbartik/deepiv
re-exports, sosp.bartik(...)/sp.deepiv(...)stay callable
instead of degenerating into bare module objects after import order
changes. -
smart.assumptions,smart.brief,smart.identification,
smart.sensitivity, andsmart.verifynow lazy-import
workflow._degradationonly inside failure paths. That removes a
prematureworkflow/__init__import duringimport statspai,
which had reintroduced partially initialized top-level symbols and
made the lazy API order-sensitive. -
Added a committed
src/statspai/__init__.pyigenerator and pinned it
with a regression test so IDE/type-checker visibility tracks the live
runtime namespace. The stub generator now skips exported constants
during leaf scanning and correctly typesSTABILITY_TIERSas
frozenset[str], avoiding duplicate/conflicting declarations. -
Pinned the two binding hazards introduced by the lazy-load refactor
with 21 explicit contracts intests/test_late_bind_contracts.py:
the five late-bind aliases re-bound by_article_aliases
(mediation,policy_tree,dml,matrix_completion,
causal_discovery) plus thesp.ivcallable dispatcher must each
remain callable rather than degenerating to a module on import
re-order; and the 14 function/subpackage collisions
(proximal,principal_strat,bridge,bcf,bunching,
dose_response,multi_treatment,causal_impact,frontier,
interference,tmle,msm,deepiv,bartik) must survive a
downstreamfrom statspai.X import Ywithout the auto-bound submodule
silently re-shadowing the function. Closes the residual gap left by
Codex's lazy-load refactor and Claude Code's same-name eager-rebind
follow-up.