feat(validation): family-wide anticipation domain validation (M-144) - #771
Conversation
All nine anticipation-taking estimators (CallawaySantAnna, SunAbraham, ImputationDiD, TwoStageDiD, StackedDiD, ContinuousDiD, EfficientDiD, WooldridgeDiD, SpilloverDiD) now validate anticipation at __init__ via the shared utils.validate_anticipation (non-negative integer; bool rejected; set_params transactional via the BaseEstimator probe re-init) AND re-check it on the fit path - the uniform direct-mutation defense, in the assignment form: the validator now RETURNS the normalized Python int, so numpy scalars (np.uint64 included) are normalized before any g-1-anticipation arithmetic can overflow. Previously seven of the nine accepted anything, and an out-of-domain window silently changed the ESTIMAND: measured, CallawaySantAnna(anticipation=-1) moved the overall ATT -85% and flipped its sign under control_group="not_yet_treated"; anticipation=True fit bit-identically to 1; SunAbraham(anticipation=1.5) returned att=nan without raising. - utils.validate_anticipation: -> int, returns int(anticipation); docstring states the family-wide adoption. - Seven constructors adopt the validating assignment; ContinuousDiD / EfficientDiD re-assign inside their param-validation helpers (idempotent on the fit-time re-run); TripleDifference keeps its early call and only normalizes at the assignment; Wooldridge's static < 0 check is deleted (dead anticipation param removed from _validate_constructor_args). - Fit-path re-checks on all nine + the shared staggered engine (mixin docstring updated for the one assigned constructor attribute); EfficientDiD.hausman_pretest normalizes its own argument (uint64 previously wrapped e < -ant and degraded the pretest to NaN-inconclusive). - SpilloverDiD: eager construction validation + the in-fit re-check kept ordered before the ref-period arithmetic (PR #456 R2 guarantee); its three construct-bad-then-fit tests re-pointed into constructor pins + three mutation-defense fit pins (None kept as the ordering pin). - New tests/test_anticipation_policy.py (M-081 sweep template): roster guard, full-text message pins per validator branch, boundary normalization (type is int), set_params rollback, fit-time mutation defense over eight classes, uint64 fit-normalization over all fit-path adopters incl. a TripleDifference staggered fit, hausman int-parity pin. - Ledger row M-144 (introduced_in 4.0, phase 5, terminal done) + test_v4_matrix count bumps/narrative true-ups + v4-design phase-5 cell citation and snapshot narrative + migration-4.0.md "Remaining 4.0 changes" bullet (numpy-scalar retyping + Wooldridge error-ordering disclosed) + REGISTRY family-wide adoption note + nine pointer notes + both LLM guides + CHANGELOG entry. TODO row retired.
Overall assessment✅ Looks good — no unmitigated P0 or P1 findings. Executive summary
Methodology
Code QualityNo findings. Validation is centralized and consistently assigned back to estimator state. PerformanceNo findings. The added validation is constant-time. MaintainabilityNo findings. The roster guard and shared validator reduce future policy drift. Tech DebtNo findings. The corresponding SecurityNo findings. No secrets, unsafe operations, or new external-input surfaces were introduced. Documentation/TestsNo findings. Tests cover constructor validation, normalization, transactional Execution note: the focused tests could not run because this review environment lacks |
Summary
anticipationdomain validation (ledger row [M-144]; retires the TODO "library-wide anticipation domain validation" row). All nine anticipation-taking estimators (CallawaySantAnna,SunAbraham,ImputationDiD,TwoStageDiD,StackedDiD,ContinuousDiD,EfficientDiD,WooldridgeDiD,SpilloverDiD) now validateanticipationat__init__via the sharedutils.validate_anticipation(non-negative integer;boolrejected;set_paramstransactional via the BaseEstimator probe re-init) AND re-check it on the fit path — the uniform direct-mutation defense.int, adopted via assignment at every call site (constructor and fit path), so numpy scalars —np.uint64included — are normalized before anyg - 1 - anticipationarithmetic can overflow.EfficientDiD.hausman_pretestnormalizes its own argument (an unsigned scalar previously wrappede < -antand silently degraded the pretest to an all-NaN inconclusive result).CallawaySantAnna(anticipation=-1)ran silently and moved the overall ATT by −85% (sign flip undercontrol_group="not_yet_treated");anticipation=Truefit bit-identically to1;SunAbraham(anticipation=1.5)returnedatt=nanwithout raising;StackedDiD(anticipation=1.5)crashed with an incidentalrange()TypeError.>= 0check is superseded (message text changed;None/str rawTypeError→ValueError; constructor error ordering moved behind the other constructor checks — disclosed in CHANGELOG/migration). The deprecatedStaggeredTripleDifferencestays construction-permissive by design (fit-validated via the shared engine).introduced_in: "4.0"per the locked release ladder, terminaldone, phase-5 cell cited indocs/v4-design.md),tests/test_v4_matrix.pycount bumps + narrative true-ups, migration-guide "Remaining 4.0 changes" bullet (numpy-scalar→intattribute retyping and the Wooldridge error-ordering change disclosed), REGISTRY family-wide adoption note + per-estimator pointer Notes, both LLM guides annotated, CHANGELOG entry.Methodology references (required if estimator / math changes)
StaggeredTripleDifference's construction-permissive exception is recorded in REGISTRY.md (frozen 3.x API shape; engine validates at fit).Validation
tests/test_anticipation_policy.py(new policy suite: roster guard, per-branch full-text message pins,type is intnormalization at construction and after fit, transactionalset_paramsrollback, fit-time mutation defense over eight classes,np.uint64fit-normalization over all fit-path adopters including a TripleDifference staggered fit,hausman_pretestint-parity pin);tests/test_spillover.py(three construct-bad-then-fit tests re-pointed into constructor pins + three mutation-defense fit pins, theNoneordering pin retained);tests/test_v4_matrix.py(M-144 row gating: floor/snapshot bumps + docstring enumerations).Security / privacy