Skip to content

Latest commit

 

History

History
114 lines (103 loc) · 49.2 KB

TODO.md

File metadata and controls

114 lines (103 loc) · 49.2 KB

Development TODO

The actionable backlog: work with a clear implementation path and no external blocker. Pull from here, top row first — rows are priority-ordered (higher priority first) within each section. Effort (Quick ≤1 day · Mid 3-10 CI rounds · Heavy derivation-free but large) is noted per row; Priority is carried from the originating PR review; the Origin column points to the originating PR number or review tag.

Related tracking surfaces:

  • ROADMAP.md — the public feature roadmap.
  • DEFERRED.md — the deferral & decision registry: blocked / parked work and decisions on the record. Do not pull from there without first clearing the named blocker.
  • docs/dev-status.md — monitoring and current-state notes (module sizes, SE / typing posture, platform quirks).

Actionable Backlog

Methodology / correctness

Issue Location Origin Effort Priority
One-way (unclustered) Bell-McCaffrey DOF for SunAbraham / WooldridgeDiD vcov_type="hc2_bm" + cluster=False: both reject the combination at 4.0 (item 7b) because their BM contrast helper _compute_cr2_bm_contrast_dof is cluster-only; port the one-way lane through linalg._compute_bm_dof_from_contrasts (the TWFE event-study core's helper) with a clubSandwich vcovCR(cluster=1:n, type="CR2") parity pin, then lift the two rejects and their tests diff_diff/sun_abraham.py, diff_diff/wooldridge.py #v4-7b Mid Low
decompose_twfe_weights(method="aipw"): port upstream implicit_aipw_weights (covariate-adjusted only; twfe_weights.py rejects the value today and lists "fwl"). The R goldens already carry the decompose.aipw / balance.aipw / two_period.* blocks under meta.reserved_blocks, so no R re-run is needed diff_diff/twfe_weights.py #812 Mid Medium
Consolidate the remaining estimator-entangled DR/logit score variants (staggered.py::_doubly_robust + RC twins, triple_diff.py, lwdid.py, wooldridge.py) onto the shared _dr_scores.py module, each migration with its own committed oracle capture (the ContinuousDiD lift's two-tier pattern in tests/test_dr_scores.py); and add a ridge vcov path to solve_ridge if an estimator ever needs analytical ridge inference diff_diff/_dr_scores.py dml-b0 Mid Low
Numeric between-period cohorts (e.g. first_treat=4.5 with integer times) are rejected by LWDiD while CallawaySantAnna estimates them and LWDiD's own datetime/Period cohorts map to the next observed period — close the dtype asymmetry by adopting the next-observed-period mapping for numeric cohorts too (contract documented in REGISTRY cohort-encodings Note + docs/api/lwdid.rst Input Contract). Lands only after PR #588 merges diff_diff/lwdid.py #588 Quick Low
Implement the LW 2026 eq. 7.9/7.10 unit-average cohort estimand (regress per-unit post-average transformed outcomes on [1, D_g] vs never-treated) as an alternative to the documented cell-mass cohort_effects convention (REGISTRY within-cohort aggregation Note; the two differ on unbalanced panels, where cell-mass weights units by observed post periods). Needs the 7.10 regression + its covariance on the NT path. Lands only after PR #588 merges diff_diff/lwdid_staggered.py #588 Quick Low
Expose cell-mass overall ATT (Stata Post_avg convention; = CS-simple on balanced panels) as an aggregate extra on LWDiD results — the fit's .att is the paper's tau_omega (cohort-mean-then-treated-weight, eq. 7.18); the authors' large-N display uses cell-mass weighting instead, and both are legitimate estimands (see the REGISTRY LWDiD Aggregation note). Lands only after PR #588 merges diff_diff/lwdid_results.py #588 Quick Low
Staggered-DDD power support: simulate_power/simulate_mde/simulate_sample_size now REJECT a staggered-configured TripleDifference (both registered DDD generators emit 2x2x2 data and fit with (group, partition, post), so a staggered config would be simulated under the wrong design). Support needs a staggered DDD DGP profile plus fit-kwargs builder, and a decision on whether the mode is selected by profile or by the estimator's own config diff_diff/power.py 3(b) Mid Low
Bootstrap-seed provenance on multiplier-bootstrap results containers: neither TripleDifferenceResults (staggered design) nor CallawaySantAnnaResults carries the seed that generated its bootstrap SEs / p-values / sup-t bands, so a serialized result cannot report the random configuration behind its inference. NOT a 3(b) regression - seed reaches the engine and get_params() correctly (same seed reproduces the SE bit-exactly, a different seed moves it), the gap is results-object observability only, it predates the merge, and both containers inherit it from the shared CallawaySantAnnaBootstrapMixin. Add seed (and consider n_bootstrap/bootstrap_weights/cband) to BOTH containers plus to_dict(), with seeded and unseeded pins; the M-014 container unification (4.0, item 6) landed without it - the unified container records n_bootstrap / inference_method but not seed - so add it to BOTH containers in one PR. Precedent for exposing it: ContinuousDiDResults, EfficientDiDResults, SyntheticDiDResults already do diff_diff/triple_diff_results.py, diff_diff/staggered_results.py 3(b) Quick Low
Staggered-mode cluster-robust ANALYTICAL SEs: cluster= raises in TripleDifference's staggered mode, so clustered inference there is bootstrap-only. Implementing a clustered analytical path for the GMM-combined influence function would let the raise become a real lane diff_diff/_staggered_triple_diff_engine.py 3(b) Heavy Low
diagnostic_report admission for EventStudyResults surfaces (the TWFE event-study mode + aggregate('event_study') containers): DiagnosticReport/BusinessReport now REJECT the surface explicitly (Phase 3(a); previously a silent zero-check report / all-null headline) and practitioner_next_steps serves the generic fall-through - admission needs source-aware routing (the type-name-keyed _APPLICABILITY/_HANDLERS registries cannot discriminate the unified container's producers) and a scalar-vs-per-period headline design; MPD-native results received {parallel_trends, pretrends_power, sensitivity, bacon, design_effect}. PR 4c-ii (2026-10-25) DELETED the MPD-native branches that logic lived in (DiagnosticReport's interaction_indices+vcov joint-Wald covariance source, the post_period_effects heterogeneity source, the pre_period_effects Bonferroni/NaN-filter collector branch, the avg_att alias row, practitioner's _handle_multi_period) - recover them from git history (git show 5a10b847^:diff_diff/diagnostic_report.py) when admission lands; event-study results reach _handle_generic meanwhile diff_diff/diagnostic_report.py, diff_diff/business_report.py, diff_diff/practitioner.py 3(a) Mid Medium
DiagnosticReport public skip bookkeeping omits RUNNER-level skips: applicable_checks reflects only gate outcomes, so a check whose runner returns status="skipped" (heterogeneity's empty/non-finite branches, _pt_event_study's empty-coefs branch - a pre-existing pattern, now also reachable via a failed post-fit event-study derivation on bootstrapped / kit-less ImputationDiD/TwoStageDiD/ContinuousDiD fits) stays listed as applicable while skipped_checks/schema["skipped"] omit it, so automation reading the public fields can misclassify the check as completed; reconcile runner-returned skipped sections into the public bookkeeping (all checks, one convention) or resolve those availabilities at the gate diff_diff/diagnostic_report.py derived-ES review Mid Low
Align the two pretrends container routes' pre_periods= contracts (the native MPD branch died in PR 4c-ii, so only the relative and calendar routes remain): the relative route silently collapses DUPLICATE requested labels (the calendar route rejects them, 3(a) R8), and it filters on SE only while the calendar route additionally requires a finite EFFECT (3(a) R9); one contract across both routes, with pinned rejection messages diff_diff/pretrends.py 3(a) R8 Quick Low
EventStudyResults inference-provenance fields: the container records no vcov_type/cluster_name/n_clusters/df_convention/Conley metadata, so a serialized surface cannot distinguish unit auto-clustering from explicit clustering, survey, Conley, or the one-way carve-out (3(a) R9 review). Adding them is a cross-producer M-092 schema amendment (six builders, to_dict/summary rendering, surface-suite pins) - follow the pre-cut amendment convention (optional fields appended last, ledger note same-diff) rather than bolting onto one producer diff_diff/results_base.py 3(a) R9 Mid Low
Opt-in singleton-group pruning for TwoWayFixedEffects (static + event-study mode; reghdfe parity): singleton units/periods are currently RETAINED class-wide - the within-demeaned row is zero so points are unchanged, but N/G/residual-df count it and CR1/finite-sample SEs shift (~0.41019 -> 0.40962 measured; REGISTRY "Deviation from R" Note, R5 review) - reghdfe iteratively drops singletons by default while fixest retains them (diff-diff matches fixest); an opt-in knob needs iterative unit+period pruning with consistent cluster/survey/replicate/Conley array subsetting and a default-flip decision protocol (moves published SEs) diff_diff/twfe.py, diff_diff/estimators.py, diff_diff/utils.py 3(a) R5 Mid Low
Cohort-timing validation input for the simultaneous-adoption event-study family (TWFE event_study=True; MultiPeriodDiD through 3.9): an optional first_treat=/cohort= column so simultaneous adoption becomes checkable under the contract-valid time-invariant D_i indicator - today the staggered-adoption advisory derives timing from within-unit 0->1 transitions, so it can only fire on off-contract time-varying D_it input, and with valid D_i adoption timing is not observable in the inputs at all (REGISTRY "staggered-adoption detection limit" Notes, both sections); design questions: validate-only vs steering error (the M-011 removal shipped in PR 4c-ii, so only the TWFE surface is in play) diff_diff/twfe.py, diff_diff/estimators.py 3(a) R2 Mid Medium
CS parity with DMLDiD's replicate conventions: per-cell replicate SEs for CallawaySantAnna (CS leaves per-cell SEs on the weighted sqrt-sum under replicate designs; DMLDiD now computes them via compute_replicate_if_variance) AND flip the replace-style effective-df relays to min-cap (min(df_survey, n_valid - 1)) at all five sites: CS overall (staggered.py overall relay), the shared event-study/group aggregation sites (staggered_aggregation.py df_survey_val = min(non_none_dfs) x2), and the staggered-DDD engine twins (_staggered_triple_diff_engine.py overall + event-study) — the replace convention can RAISE df above the QR-rank design df (anti-conservative); latent, fires only when n_valid < R (all-zero/non-finite replicate column). DMLDiD's min-cap + the pinned inherited ES/group behavior are the templates (REGISTRY DMLDiD replicate Note) diff_diff/staggered.py, diff_diff/staggered_aggregation.py, diff_diff/_staggered_triple_diff_engine.py DML replicate PR Mid Low
balance_e replay on bootstrapped ImputationDiD/TwoStageDiD fits: the unrestricted ES/group surface is relayed since 4.0 PR 3c (computed at fit, balance_e=None; M-021/M-022), but aggregate('event_study', balance_e=k) still raises there - a balanced-window replay needs retained per-target psi (ImputationDiD: the panel-backed kit retains everything the psi precompute reads, so a seeded replay is tractable) / retained per-level GMM scores (TwoStageDiD: function-locals today) diff_diff/imputation_results.py, diff_diff/two_stage_results.py, diff_diff/aggregation.py 2(b) PR-3b Mid Low
ContinuousDiD post-fit bootstrap-ES REPLAY: since 4.0 PR 3c a bootstrapped fit relays the event-study surface computed at fit (M-025), so the gap is a genuine post-fit replay (fresh draws, no fit-side cost, a route for pre-4.0 pickles); a seeded replay is tractable - the multiplier draws are seeded (np.random.default_rng(self.seed)) - but needs the FULL per-cell _bootstrap_info (bread/ee_treated/Psi_eval/dPsi_*/beta_pred) the pruned kit deliberately drops, so shipping it means a kit-payload change with its own memory contract diff_diff/continuous_did_aggregation.py, diff_diff/continuous_did_results.py 2(b) PR-3c Mid Low
practitioner step_name="heterogeneity" producer-side collisions: three OTHER estimators' advice steps reuse the key with non-heterogeneity labels (:975 ContinuousDiD dose-response, :1022 Triple placebo-group, :1413 LPDiD WAS arrays), so DiagnosticReport's heterogeneity completion silently drops that unrelated advice from next_steps via _filter_steps - the same latent collision fixed for StackedDiD in M-024 (renamed to sub_experiment_balance). Renaming these changes those estimators' report output; audit + rename with per-estimator pins. diff_diff/practitioner.py 2(b) PR-2 review R9 Quick Low
PreTrendsPower violation='linear' on CS base_period='varying' input targets the wrong alternative: δ_pre = M · |t| assumes level coefficients against a common reference, but varying-base pre-treatment effects are consecutive-period comparisons (constant increments under a linear trend). Both CS-sourced routes now WARN (REGISTRY PreTrendsPower Note), and universal-base GAPPED grids fail closed via the reference_event_times common-reference guard; what remains is the varying-base resolution - either transforming the violation vector through each coefficient's actual base mapping (needs per-horizon base provenance) or requiring base_period='universal' for the linear benchmark - a per-estimator methodology decision with a hand-calculated linear-violation gate diff_diff/pretrends.py 2(b) PR-1 R5 Mid Medium
run_placebo_test/run_all_placebo_tests expose ONE overloaded time param (calendar for timing/group tests, 0/1 post dummy forwarded into the M-137/M-138 callees - works only because a two-period 0/1 calendar column is both); redesign the wrapper signatures (dual time=/post= params or a split) so each callee gets an honestly-named argument diff_diff/diagnostics.py 2(c)-ii PR-B Mid Medium
absorbed_fe_rank N>=3 general rank: the helper keeps sum(levels-1) for 3+ absorbed dims, exact for independent connected dims but an over-count for duplicated/nested triples (measured a(5),b(4),c==b(4): true 7 vs formula 10) and for disconnected N-way graphs. Two-way is component-exact. Deriving general N-way FE rank is a hypergraph problem; do it with a reference (fixest's fixef.rm/reghdfe df_a) rather than a guess. See docs/methodology/variance-conventions.md D3. diff_diff/utils.py #variance-inventory Mid Low
SyntheticControl conformal (CWZ 2021) AR / innovation-permutation path (Lemmas 5-7) for time-series proxies — the residual-permutation shortcut is only valid for time-permutation-invariant proxies (SC/Lasso/DiD); an AR proxy needs innovation permutation. diff_diff/conformal.py, diff_diff/synthetic_control_results.py CWZ-2021 Heavy Low
Re-run the R-dependent benchmark refresh so docs/benchmarks.rst's TWFE "SE Rel Diff 0.1%" cell reflects the 3.9 K_reference convergence (expected 0.1% -> 0.0%; the table is generated, never hand-edited — the movement is noted in the CHANGELOG entry). docs/benchmarks.rst, benchmarks/R/ #variance-inventory Quick Low
SunAbraham: a cohort not observed at its own reference relative period (e = -1 - anticipation) makes that cohort's block collinear, so QR drops an unnamed column (dropping 1 of 12 columns (column 9)) and overall_att comes back NaN. Found by auditing the sibling estimator while fixing the ETWFE analogue (#724); PRE-EXISTING, not introduced there. Lower severity than #724 — that returned a silently WRONG finite number, this returns NaN with a rank warning — but the event-study surface still looks complete, so a user may not notice the loss. SA already omits its reference explicitly and tracks _reference_observed, so the fix is per-cohort support for that flag rather than the ETWFE-style redesign. diff_diff/sun_abraham.py #724-audit Mid Low
Define N_g (W2025 Eqs. 7.4/7.6) for UNBALANCED panels where comparison-support filtering removes every observation of some units in an estimated cohort, then replace the fail-closed guard with the defined behavior. _n_g_per_cohort is read off the final sample, so those units vanish from the cohort-share weights; measured on a cohort supplied with 100 units of which 90 appear only at a dropped period, aggregate(weights="cohort_share") moves 1.8078 -> 3.8157. The paper assumes a balanced panel and does not say whether N_g counts the supplied cohort or the surviving units, and the two disagree materially, so aggregate currently raises naming the cohorts and counts ([M-125]); weights="cell" is unaffected and balanced panels never trip it. Settle the estimand (likely: count the supplied cohort, since ATT(g,t) is a cohort-level quantity, but that weights units with no retained observation) and gate with a test computing Eq. 7.4 by hand on unequal cohort sizes. diff_diff/wooldridge_results.py, diff_diff/wooldridge.py #729-followup Mid Medium
WooldridgeDiD + survey_design= does not support DOMAIN ESTIMATION, so BOTH row-deleting paths are currently REFUSED (NotImplementedError, all three methods) rather than performed: unidentified-cohort exclusion ([M-123]) and comparison-support period filtering ([M-125]). One fix unblocks both. Implementing it properly means zero-padding the excluded rows' weights while retaining strata/PSU/FPC, per REGISTRY Subpopulation Analysis (Phase 6) / Lumley (2004) 3.4, so TSL variance and df_survey = n_PSU - n_strata use the full design (naive deletion measured 22 -> 14 on a two-stratum panel). SurveyDesign.subpopulation() already implements the contract and SpilloverDiD Wave E.3 is the in-repo precedent; the blocker is that the weighted within-transform rejects zero-weight units, shared machinery behind 7 estimators. Landing it would turn both refusals back into supported fits. Gate with a SurveyDesign.subpopulation() parity test on ATT, TSL SE and survey df where the excluded cohort exhausts a PSU. diff_diff/wooldridge.py, diff_diff/utils.py #724-codex-R4/R5 Heavy Medium
WooldridgeDiD REFUSES a fit whose only treatment cells fall inside the anticipation window, discarding estimates it successfully computed. _require_estimable_overall_att ([M-124]) raises when no cell has t >= g, because the overall ATT averages only t >= g (W2025 excludes anticipation leads) while cells from t >= g - anticipation are ESTIMATED. For a cohort never observed at or after its own treatment date, those anticipation-window ATT(g, t) are real, identified estimates and are thrown away with the fit. The refusal is a stopgap for the missing estimand semantics, not the intended end state. Real fix: decide what such a fit should return — most likely the per-cell ATT(g, t) plus an overall that is explicitly undefined with a stated reason (not a bare NaN, per the no-silent-NaN convention) — then relax the guard to that. Needs a REGISTRY note defining the estimand and an aggregate() story for the anticipation-only case. diff_diff/wooldridge.py #724-codex-R2 Mid Medium
WooldridgeDiD comparison-support accounting is PARTIAL. Per-period support now runs before the solve and removes periods with no eligible comparison, reporting them (REGISTRY per-period comparison support). What remains is the per-(g, t) half: the completeness gate still refuses when a cell is lost to a cause the period filter cannot see -- treated cohorts sharing no comparison period with each other, and covariate collinearity -- so those users get a refusal naming the cell rather than an upfront diagnostic naming the cause. Real fix: compute per-cell eligible-control support and report exactly which cells are unidentified and why BEFORE solving. Note the cohort-count proxy remains invalid and is still pinned (TestOverallAttFailsClosed::test_two_cohorts_without_same_period_controls_fail_closed, verified unaffected by the period filter). diff_diff/wooldridge.py #724-codex-R2 Mid Medium
WooldridgeDiD DROPS the observations of a cohort with no supported pre-period before g - anticipation ([M-123]) rather than identifying it. Excluding the rows is correct given g-1 normalization -- leaving them in silently loads the cohort's effect onto the time FE -- but dropping a cohort a user supplied is a lossy last resort. Route (b) is now SETTLED NEGATIVELY and is not the answer: the paper's no-never-treated last-cohort normalization shipped (W2025 Sec 5.4, per-period comparison support), and it does NOT identify these cohorts -- wooldridge-2025-review.md:477 is explicit that in the final period the last cohort's ATT is unidentified, and the implementation still excludes any cohort whose reference is None. Route (a) remains open: an explicit user-supplied reference period per cohort -- W2025 Section 6.1 says any pre-treatment period may serve and the pre-trend t-test is invariant to the choice, so a cohort with ANY supported pre-period is a candidate even when g-1 is missing. If route (a) also fails to identify the cohort, convert this row into a REGISTRY Note recording exclusion as the deliberate final answer. diff_diff/wooldridge.py, docs/methodology/REGISTRY.md #724 Heavy Medium
WooldridgeDiD REFUSES a panel whose units split into disconnected support groups within a cohort, rather than estimating what IS identified. The connectivity guard (REGISTRY within-cohort support connectivity) correctly detects that a closed component's cells are collinear with the unit FE — previously QR dropped one silently and the overall ATT averaged an incomplete set (issue #724's failure mode via unit support). Refusing is the safe answer, not the complete one. The connected component containing the reference is still fully identified, so the estimable resolution is either (a) estimate the connected component and report the disconnected units as excluded, with the estimand restated (a sub-population of units, so it needs a REGISTRY definition and interacts with the survey-domain row above), or (b) per-component references, if a component with its own pre-period can carry its own normalization — needs a methodology decision, since components then are not comparable on one baseline. Gate with the split-support fixture in TestWithinCohortSupportConnectivity. diff_diff/wooldridge.py #724-codex-R7 Heavy Medium
WooldridgeDiD fully resolves the SurveyDesign TWICE on every supported survey fit. The pre-exclusion validation pass (added so invalid metadata cannot hide in rows that cohort exclusion deletes) calls survey_design.resolve(sample), and each fitter then calls _resolve_survey_for_wooldridge -> _resolve_survey_for_fit on the same frame, repeating weight normalization, strata/PSU/FPC validation and design-array construction. Any fit that REACHES the second resolve has an unchanged sample (survey + unidentified-cohort exclusion raises first), so the first result is reusable: capture the _resolve_survey_for_fit 4-tuple early and thread it into the three fitters as an optional pre_resolved. Caveat that makes this non-trivial: sample = sample.reset_index(drop=True) runs BETWEEN the two calls, so the reused object must be verified index-independent (resolution extracts positional numpy arrays, but _inject_cluster_as_psu and the metadata recompute need checking), and the early call must stop suppressing warnings or the user loses the weight-normalization notice. Gate with a survey fit asserting one normalization warning and byte-identical SEs. diff_diff/wooldridge.py #724-codex-R10 Mid Low
ContinuousDiD CGBS-2024 remaining extensions (earlier phases — covariates= reg/dr, treatment_type="discrete", single-cohort control_group="lowest_dose" with estimand ATT(d)−ATT(d_L) — are already supported; see REGISTRY Note #7). Remaining (all deferred NotImplementedError, documented): estimation_method="ipw" on the dose curve (scalar-adjustment / degenerate); covariates= × survey_design= (weighted OR + weighted nuisance IF); multi-cohort heterogeneous-support discrete aggregation (support-aware: average each dose only over the cohorts that observe it); multi-cohort lowest_dose (within-cohort d_L reference + support-aware cross-cohort aggregation); and covariates= × lowest_dose (conditional-PT-relative-to-d_L estimand). Single-cohort / 2-period / shared-support multi-cohort are supported. continuous_did.py CGBS-2024 Heavy Low
WooldridgeDiD does not apply the W2025 Sec 5.4 D_{G_max} x X covariate normalization, and three sibling covariate rank deficiencies are pre-existing. Measured with the period range pinned and only the never-treated units toggled: (1) time-invariant exovar is absorbed by the unit FE, 4 of 26 columns, IDENTICALLY with and without never-treated units; (2) xgvar's cell x covariate block, 19 of 41, identical on both panels; (3) xtvar under demean_covariates=False does exhibit the sum_g D_g x = x dependency that the default demeaning removes; (4) the newly-reachable case -- time-VARYING data passed through exovar, which its own docstring reserves for time-invariant covariates -- where the paper's dT_i rule would give a deterministic D_{G_max} x X drop instead of QR's arbitrary pick (coefficients unaffected, 1.35e-14; rank_deficient_action="error" raises). REGISTRY's narrowed Sec 5.4 note cross-references this row. Trap for whoever takes it: xtvar under the DEFAULT demean_covariates=True is FULL RANK -- the raw block carries demeaned values while D_g x X carries raw ones -- and forcing the drop there moves overall_att 1.11903 -> 1.46269. Pinned as-is by TestComparisonSupportFiltering::test_cells_derived_groups_did_not_leak_into_the_design. diff_diff/wooldridge.py #729-followup Heavy Medium
WooldridgeDiD.n_control_units counts never-treated UNITS on control_group="never_treated" regardless of method, but on the nonlinear paths (logit/poisson) treated units' pre-treatment rows ARE the identifying comparison -- only the OLS path absorbs them into their own cells. So the reported count under-states the comparison pool exactly where the REGISTRY control-pool asymmetry note applies. Widen to not_yet_treated or (never_treated and method != "ols"), or document the count as never-treated-units-by-definition. Behavior is PRE-EXISTING; documented for now in the REGISTRY control-pool Note rather than changed, because widening moves a public results field and wants its own ledger row and test matrix. diff_diff/wooldridge.py #729-followup Mid Low
Bad-control imputation estimator (Caetano et al. 2026 Section 6.1, Eqs. 5-7, S8 influence function) on a CallawaySantAnna estimation_method="reg" host: two untreated-sample OLS fits per cell plus the generated-regressor IF line. staggered.py bad-controls PR-B Heavy Low
Bad-control SC "parallel trends for X" variant (Caetano et al. 2026 Section 7 / S17): a linearity-based alternative to covariate unconfoundedness with its own estimand. dml_did.py bad-controls PR-B Heavy Low
ATT_X(g,t) event-study aggregation + bootstrap replay for the bad-control pre-test (today analytical per-cell only; never aggregated). dml_did_results.py bad-controls PR-B Mid Low
Remark-4-weighted overall ATT for the bad-control lane (sum_g sum_{t>=g} [P(G=g given G in G_bar)/(T-g+1)] ATT(g,t): cohort mass among the ever-treated divided by the cohort's post-period count); the headline att is CS "simple" (REGISTRY DMLDiD Note). staggered_aggregation.py bad-controls PR-B Mid Low
Allow empty Z on the bad-control lane (the paper's Z may be empty; DMLDiD's covariates-required gate currently applies). dml_did.py bad-controls PR-B Quick Low
Recover the Supplementary Appendix DGPs 2 / 3 / 5 (nonlinear W / X terms) on the bad-control ML lane - needs a flexible-learner fixture via the duck-typed learner route (the parametric built-ins are inconsistent there by design, Table S1). tests/test_methodology_dml_did.py bad-controls PR-B Mid Low
DurationDiD alternative finite-sample PH estimators (the repaired cumulative-increment slope of Equation 3.5 and the average-hazard slope of Equation 3.6) as an opt-in beside the shipped mean-of-ratios coefficient; they coincide under exact PH and differ under sampling noise (review table, lines 306-311) duration_did.py #822 Mid Low
DiagnosticReport / BusinessReport admission of DurationDiDResults: both reject it by type today (their batteries are keyed to mean-outcome parallel-trends diagnostics); admission needs a hazard-restriction check family (the Algorithm 2 pretest, the invalid-curve statuses, per-family bootstrap failure counts) and a narrative for per-date absorption ATTs diagnostic_report.py, business_report.py #822 Mid Low
Conformal _auto_grid's half-width floor 0.5 * abs(center) dominates 8 * spread when the effect is large relative to the residual scale: a narrow interval then holds only 3-4 of the 100 default grid points, and its endpoints move by a grid step. Size the grid from the residual scale, or refine around the accepted set. conformal.py #838 Quick Low
SyntheticControl at an exact-fit tie (treated predictors inside the donors' convex hull): W and V are not identified, and the inner solve reports the active set's vertex (REGISTRY §SyntheticControl). A stated selection rule (Abadie-L'Hour 2021 penalty, or the minimum-norm W) is a methodology choice; note that any strictly convex tie-break makes W(V) continuous, and the nested search then moves V toward a corner where a predictor's weight is negligible (Tutorial 25 under a 1e-10 relative ridge: V = (0.017, 2e-12, 0.983)). synthetic_control.py, utils.py #838 Mid Low

Performance

Consolidates the former #### Performance tech-debt table and the standalone ## Performance Optimizations section. (Speculative / low-value perf notes — numba JIT, generic sparse-FE, QR+SVD rank-detection redundancy, check_finite bypass — moved to DEFERRED.md → Parked.)

Issue Location Origin Effort Priority
LWDiD staggered fit recomputes cohort-wide work across surfaces: each cohort's rolling transformation is built once per fit_staggered cohort loop, but unit-level post summaries, control-eligibility sets, and the composite path's ydot_by_cohort are re-derived independently by the cell, aggregate, and _composite_regression_aggregation paths — runtime grows ~cohorts × panel size. Cache the per-cohort transformation + post summaries in one pass and reuse; add a many-cohort unbalanced-panel benchmark diff_diff/lwdid_staggered.py, diff_diff/lwdid.py #588 Mid Low
Reuse the demeaner's factorized codes in absorbed_fe_rank/absorbed_fe_cr1_k_increment instead of re-factorizing: at 186k rows the rank helper adds ~1.9 ms per absorbed fit (7.7% of the fastest Rust-served TWFE fit) and the K_reference increment ~3.2 ms per clustered-hc1 absorbed fit (~13%; see docs/performance-plan.md "Component-aware absorbed-FE rank"), and the helpers and demean_by_groups factorize the same group columns. Threading the codes through the call sites halves the factorize work; the connected_components call itself is ~1.1 ms. Deliberately not done in the correctness PRs. diff_diff/utils.py #variance-inventory Quick Low
EfficientDiD conditional path: the largest remaining O(n) stage is the sieve/nuisance construction outside the tiled pass (~9s at 10k). (The _ridge_solve_weights Python-prep shave landed 2026-07-07 — the omega_stack[rest] fancy-index copy and tail scatter are skipped when no row is zero-masked, byte-identical outputs; the zero_mask abs scan itself remains, needed for correctness.) efficient_did_covariates.py CS-scaling Mid Low
_rq_fit LP assembly is dense (A_eq = [X, I, -I] with dense identity blocks, rebuilt per cell fit): a scipy.sparse construction would cut memory and likely HiGHS time for large cells / bootstrap-heavy covariate CiC/QDiD fits. CAVEAT before doing it: a different matrix representation can change HiGHS's vertex selection at degenerate/tied QR optima - end-to-end covariate goldens are tie-selection-gated (fine), but the qr_cases tight coefficient matches may shift to the equal-loss branch; re-run the parity suite and re-calibrate if needed. diff_diff/changes_in_changes.py::_rq_fit covariates PR Quick Low
Evaluate flipping DIFF_DIFF_SOLVE_OLS_FASTPATH default-ON after an opt-in soak (the 2026-07 certified normal-equations Cholesky fast path, both backends). A flip needs: golden/parity-suite recapture at the tol-bounded posture (fitted ~1e-8 abs / SE ~1e-6 rel — the default today is byte-pinned in several benchmark conventions), certification-rate telemetry across real workloads (any decline is silent-correct but forfeits the speedup), and a staged default-flip protocol of its own - a later flip is a major-version change on its own schedule, re-evaluated in 4.x. Decision at the 4.0 cut recorded in docs/v4-deprecations.yaml (M-008). diff_diff/linalg.py::_resolve_solve_ols_fastpath, rust/src/linalg.rs::solve_ols_chol CS-scaling Mid Low

Testing / docs

Issue Location Origin Effort Priority
Re-execute the nine tutorials whose committed outputs still show the 3.x residual-df p-values/CIs after the df_convention default moved to "cluster" (t(G-1)): 01_basic_did, 02_staggered_did, 04_parallel_trends, 05_honest_did, 07_pretrends_power, 09_real_world_examples, 11_imputation_did, 13_stacked_did, 16_wooldridge_etwfe (not 16_survey_did: the survey df wins). Run under DIFF_DIFF_BACKEND=rust with the per-worktree kernel, reconcile any prose that restates a moved number, and --nbmake each; scheduled for the 4.0 release PR (docs/v4-design.md section 9 item 10) together with any item-8/9 movers (24 was refreshed in #879; 23 in 7b, re-validated in 7c - it prints no p-values or CIs). 7b's label changes also drift any committed analytical DiD/TWFE summary (", G=" on the variance line) and every SunAbraham summary (the new "Std. errors:" line) - covered by the same re-execution. docs/tutorials/*.ipynb #879 Mid Medium
TWFE event-study one-way HC2-BM absorbed-FE R parity against the mpd_absorbed_fe_did golden (clubSandwich::vcovCR(cluster=1:n, type="CR2")): since 4.0 (item 7b) cluster=False + hc2_bm reaches the one-way lane directly (no singleton-cluster column needed; tests/test_v4_cluster_policy.py pins finiteness and the static-surface LinearRegression parity only), so the open item is the R pin itself; the hc2 lane is pinned by TestTWFEEventStudyAbsorbedFERParity and the clustered hc2_bm per-period lane by test_twfe_event_study_cluster_hc2_bm_per_period_df_matches_clubsandwich; the MultiPeriodDiD lanes died at the 4.0 cut (PR 4c-ii) tests/test_estimators_vcov_type.py #v4-4c-i Mid Medium
TWFE event-study power profile: simulate_power/simulate_mde/simulate_sample_size have no _EstimatorProfile for TwoWayFixedEffects.fit(event_study=True) since the MultiPeriodDiD registry entry went with the class (PR 4c-ii); EventStudyResults carries no scalar headline, so the profile needs a design (post-period average via the tests/helpers/event_study_average.py arithmetic, or a named period) plus its _SURVEY_FIT_BUILDERS twin diff_diff/power.py #v4-4c-ii Mid Low
Clustered hc2_bm Bell-McCaffrey DOF guard is batch-relative (linalg._cr2_bm_dof_inner: a contrast is NaN'd when its scaled max|B| falls below 1e-10 of the batch maximum), so the event-study core keeps the otherwise-unused post-period-average contrast column as the anchor to hold the 3.x per-period NaN masks bit-identical (PR 4c-ii, user decision 2026-10-25). Reproduction: x_nc = treated * 1[period >= 4] + 3e-6 * default_rng(seed).normal(size=n) as a covariate on the oracle panel flips the mask between [eye(k), avg] and eye(k) on 20/20 seeds (cond(X) ~ 9e5; 1e-7 never flips, <= 3e-8 rank-drops) - too BLAS-sensitive for a committed oracle, hence the structural pin test_hc2_bm_contrast_batch_keeps_post_average_anchor. Decide whether the guard should use a batch-independent anchor (the treatment contrast, or the absolute floor only) and then drop the column diff_diff/estimators.py, diff_diff/linalg.py #v4-4c-ii Mid Medium
Re-validate the plan-review dual engine on gpt-6.1-sol with tools/plan-review-eval: re-pin every arm's composite cli_version to the live Claude Code + codex CLI pair, add a gpt-6.1-sol arm, run the harness against the gpt-5.6-sol baseline (the 2026-10 swap carried the model over from the code-review campaign 2 verdict without a plan-review campaign). tools/plan-review-eval/config/configs.json reviewer-eval gpt61sol PR-2 Heavy Low
TWFE-weights tutorial section: attgt_weights (all three type= values side by side) + decompose_twfe_weights + plot_twfe_weights on mpdta, either as a section of 02_staggered_did.ipynb or a new notebook registered in docs/tutorials/index.rst; follow-on PR by decision, the feature ships without it docs/tutorials/02_staggered_did.ipynb #812 Mid Medium
Geo-experiments tutorial: the SDiD fits on the generate_factor_data(...) panel (conversions ~1,500, sigma-hat ~310) hit the Frank-Wolfe iteration cap - the placebo fit warns once and the bootstrap cross-check warns on 100/100 draws - yet the narrative presents the bootstrap SE as a paper-faithful robustness check. Pre-existing on main under BOTH backends (surfaced when the notebook outputs were refreshed for the shape-only fit diagnostic); the committed April outputs predate the aggregated non-convergence warning. Either rescale / re-tune the DGP (or raise max_iter / loosen min_decrease) so the refits converge and re-execute, or label the bootstrap result unreliable and drop it from the "Validity evidence" claim. docs/tutorials/18_geo_experiments.ipynb local codex review of the SDiD shape-only fit-RMSE PR Mid Low
Replicate Chang (2020) §4.2.1 ML-design RCS DGP — requires a penalized (Lasso-class) propensity learner or a maintainer fixture via the duck-typed propensity_learner= object route; native logit+linear verified pure noise at (N=500, p=100) across ~20 review seeds (att ~ −23..+16, SE ~4-10, EPV ~1.4-1.6 vs threshold 10, fitted out-of-fold clipping ~5-29%, mean ~14%); at (N=200, p=100) outcome_learner="linear" fails closed on control-fold rank deficiency even under an oracle propensity (use ridge/sieve there); §4.2.2 is replicated tests/test_methodology_dml_did.py, diff_diff/_learners.py DML PR-B2 Mid Low
Optional scheduled end-to-end execution gate for the MMM tutorials (29/30): a cron-only workflow (or extension of mmm-interop.yml) that executes both notebooks in isolated exact-pin environments, so a stale/invalid committed posterior cannot stay green indefinitely - today the hybrid posture (deliberate: notebooks execute locally with committed outputs; CI smoke-tests the exporters without sampling; drift tests pin source + committed-output needles) leaves the MCMC claims un-re-executed in CI .github/workflows/mmm-interop.yml, docs/tutorials/29_mmm_calibration_pymc.ipynb, docs/tutorials/30_mmm_calibration_meridian.ipynb mmm-interop Mid Low
Committed fixest::feols event-study golden for TWFE event_study=True (within + pooled specs, unbalanced + covariate panels, matched CR1 cluster convention, per-period effects + vcov block) - the in-suite gate is the committed pre-refactor capture (tests/fixtures/v4_twfe_event_study_oracles.json, a self-capture of the shared core at 68e383bd/d45d5316), so a defect that predates the capture would pass; the live-R harness (benchmarks/R/benchmark_multiperiod.R, feols(y ~ treated * time_f | unit)) validated the within design in docs/benchmarks.rst but is not a committed regression test - follow the fixest_did_twfe_golden.json committed-golden pattern (pytest.skip when absent) tests/test_fixest_did_twfe_parity.py, benchmarks/R/ 3(a) R2 Mid Medium
Evaluate adding the BaseEstimator param surface (get_params/set_params) to the exported classes that never had it - PowerAnalysis, LinearRegression, BusinessReport, DiagnosticReport, TWFEWeightsResult (a NEW public surface, deliberately out of the 2(c)-i pure-refactor scope; LinearRegression is the one fit-bearing class excluded from the contract suite's roster-completeness test). diff_diff/linalg.py, diff_diff/power.py mixin PR Mid Low
Tighten the mypy suppressions that back the enforced-zero posture: burn down prep_dgp's per-module [index] override (needs a None-vs-array restructure that preserves the seeded RNG stream), and evaluate re-enabling the globally disabled codes (arg-type, return-value, var-annotated, assignment) one at a time — assignment alone hid several real annotation drifts found during the 2026-07 triage. pyproject.toml [tool.mypy], diff_diff/prep_dgp.py lint-CI Mid Low
Route LWDiD into the practitioner code surfaces: practitioner_next_steps (diff_diff/practitioner.py), agent_workflow.py, DiagnosticReport, and BusinessReport never dispatch to or mention LWDiD — the AI guides now cover it, but the code surfaces need contract decisions (when to recommend it, what diagnostics to run) plus tests diff_diff/practitioner.py, diff_diff/agent_workflow.py, diff_diff/diagnostic_report.py, diff_diff/business_report.py LWDiD release audit Mid Low
df-provenance completion follow-up (PR C descoping): a Wooldridge public per-row df channel (a per-key dict that can also carry the hc2_bm BM contrast dofs the scalar _df_analytic_fallback cannot represent — the unified surface's Wooldridge df column stays NaN until then); ImputationDiD NON-survey per-lead df (the survey-fit scalar event_study_df + its bootstrap clearing shipped with the M-092 completion; what remains is surfacing the resolve_tail_df-derived per-lead dfs on non-survey fits, which needs the _compute_lead_coefficients return-path change); and the inference_df (DiD/SA/StackedDiD/SpilloverDiD) vs df_inference (CS/dCDH) adapter-naming unification. NOTE (M-024): StackedDiD's df_survey=None on analytical fits is DELIBERATE - the container resolver's df_inference fallback not matching inference_df is load-bearing (normal-theory FLCI; see the REGISTRY StackedDiD M-024 Note and the pin in tests/test_event_study_consumers.py) - the unification must preserve that outcome or renegotiate it explicitly. diff_diff/wooldridge_results.py, diff_diff/imputation.py, diff_diff/results_base.py #variance-PR-C Mid Low
worktree-rm safety via a tested argv helper: the prose rewrite (ask-before-remove confirmation gate, detached-HEAD reachability/rescue, tip-identity force-delete guard) was reverted to the main version because editing the prose repeatedly reintroduced shell-injection (last: sourcing a state file built from a git-derived branch name). Restore those guards in a worktree_rm.py that takes the name via file ingress, invokes git through argv arrays, and has a metacharacter-branch/path injection regression test — the pattern that worked for pr_prepare.py/premerge_scan.py. .claude/commands/worktree-rm.md, .claude/scripts/ skill-audit Heavy Medium
premerge_scan.py should scan the staged blob (git show :path) for staged methodology files, not the working-tree copy — a stage-then-revert-working-copy edit currently reads the safe working version and misses the staged violation. Union staged-index findings with unstaged/untracked filesystem findings. .claude/scripts/premerge_scan.py skill-audit Mid Low
Carousel PDF drift guard: the committed deck PDFs are build artifacts of their generate_*.py scripts (README rule: regenerate in the same commit), but nothing verifies a committed PDF matches its generator — a source edit without regeneration, or a stale raster chart, passes every claims test. Applies to ALL decks, not just the MMM one. fpdf embeds creation-date metadata, so a byte-compare needs fixed metadata or a per-page rendered-text manifest. Origin: mmm-carousel local review R1. carousel/, tests/ mmm-carousel Mid Low
Codex reviewer isolation (repo-wide): codex --sandbox read-only blocks writes but does NOT confine READS to the worktree/repo (verified: a probe read /etc/hosts), and prompt guards are not a security boundary. Affects /ai-review-local (which already documents and accepts this surface) and plan-review-eval dual arms equally. Evaluate OS-level isolation for codex invocations (container / sandbox-exec profile / dedicated low-privilege account exposing only the worktree). .claude/scripts/openai_review.py, tools/plan-review-eval/ plan-review-eval local review R6 Heavy Medium
plan-review-eval dual arms: terminate the surviving peer subprocess when the first reviewer of a dual pair fails (today an early Claude failure still waits out the codex peer — up to its 3600s ceiling — before the run becomes INFRA_ERROR; needs process handles threaded through _call_claude/call_codex or a shared cancellation event). tools/plan-review-eval/plan_adapters/plan_reviewer.py plan-review-eval local review R3 Mid Low
Reimplement the retired /review-plan --updated (delta re-review, ## Delta Assessment section) and --pr <url> (PR-comment feedback-coverage) modes in the plan-review skill — descoped from the initial skill ship (the advertised-but-unimplemented flags were removed). --updated needs prior-review load + a delta diff; --pr needs a gh comment fetch + a new reviewer-prompt token. .claude/skills/plan-review/SKILL.md plan-review skill local review Mid Low
plan-review skill dual-mode peer cancellation: when reviewer 1 (Opus subagent) fails, terminate the concurrently-running codex codex_review.py subprocess instead of waiting out its 3600s ceiling (and vice-versa). Interactive analogue of the eval-harness peer-cancellation row above; needs the codex process handle surfaced from codex_review.py so the skill can signal it. .claude/skills/plan-review/SKILL.md, .claude/skills/plan-review/codex_review.py plan-review skill local review R2 Mid Low
Clean-negative precision re-run: Campaign 1's s3_negative plans were base_sha-contaminated (real defects at the pinned base), so hallucination rate cleared (dual 3.4%) but trivia-flooding on a genuinely clean plan is unmeasured. Small future run (~30 s3-style reviews on 3-5 constructed-clean plans, not the full 120-matrix). tools/plan-review-eval/ campaign-1 verdict Mid Low
trop-silent-drop criteria regression: the rewritten criteria.md dropped a silent-failure catch the OLD criteria had (arm A caught it, B/C missed — campaign-1 A-vs-B contrast). Patch the criteria + re-validate (re-opens the criteria identity, so gated behind a re-validation run). .claude/skills/plan-review/criteria.md, tools/plan-review-eval/ campaign-1 verdict Mid Medium
The Stata jwdid parity arms cover BALANCED panels only. jwdid_alltreated pins the all-eventually-treated cell set, N and SE ratio on the mpdta subset (191 units, 955 rows, 764 estimated), but every arm is a balanced frame, so REGISTRY's parity claim is scoped to that case. The comparison-support predicate's UNBALANCED behavior -- where G_max may be unobserved at later periods so the closed form t < G_max - anticipation does not hold and support is set by whichever cohorts are actually observed -- is verified against the predicate directly, not against Stata. Add an unbalanced jwdid arm to close that gap. benchmarks/stata/generate_etwfe_cs_golden.do, tests/test_etwfe_cs_stata_parity.py #729-followup Mid Low
DurationDiD tutorial notebook (Appendix C-style simulation showing the sign reversal of standard DiD on an absorbing outcome, CD vs PH, the fitting window, and the pretest), registered in docs/tutorials/index.rst with a drift test; deferred from the estimator PR, which relies on the executed API-page examples docs/tutorials/ #822 Mid Low
Answer-key isolation for reviewer-eval corpus worktrees: after PR-1's opaque worktree leaves, the remaining channels are the shared object DB and refs (incl. the dangling synthetic commits verify-corpus and the run pre-flight leave behind), the enclosing checkout's corpus and run artifacts, the synthetic HEAD commit message (names the bug for S1), other cases' corpus files committed inside 9 of the 11 case trees themselves, and the committed, redacted stderr fixtures tests/fixtures/codex_stderr_0_162_1*.txt (command lines of a control review of the smoke case; no prompt, output or review text). Fix = a clone at the case sha OUTSIDE the checkout with a neutral commit message PLUS removal of tools/reviewer-eval/corpus/ from the materialized tree. Cross-links the "Codex reviewer isolation (repo-wide)" row above. tools/reviewer-eval/adapters/worktree.py #849 Mid Medium