From 9f85492c2f80c84fb48c29ac621a78e747a3cc4f Mon Sep 17 00:00:00 2001 From: igerber Date: Sun, 19 Jul 2026 13:35:03 -0400 Subject: [PATCH 1/3] chore(docs): rename TODO.md -> DEFERRED.md (pure move for blame continuity) Pure git mv, no content change. The next commit trims DEFERRED.md to the deferral/decision registry and recreates TODO.md as the actionable-only backlog - keeping the rename its own commit lets git pair the bulk of the content (the deferred tables) with its history. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01H8PDhdrdQH2DM8yAeEnib7 --- TODO.md => DEFERRED.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename TODO.md => DEFERRED.md (100%) diff --git a/TODO.md b/DEFERRED.md similarity index 100% rename from TODO.md rename to DEFERRED.md From b0af57c43ccbf40fc582f86619786c3c9a645b14 Mon Sep 17 00:00:00 2001 From: igerber Date: Sun, 19 Jul 2026 14:38:42 -0400 Subject: [PATCH 2/3] docs: split tracking into TODO.md (actionable) / DEFERRED.md (registry) / docs/dev-status.md (2/3) TODO.md is now the actionable backlog only (9 rows incl. a new tracking-contract guard-test row; priority-ordered, Medium first). DEFERRED.md (renamed from TODO.md in the previous commit for blame continuity) becomes the deferral & decision registry: Paper-gated (15) / Needs external reference (18) / Parked (36) / Version-gated (1, v4 ledger cross-linked) / Decision record (8). The two user-facing Known-Limitations rows fold into Parked; the "Deprecated Code" section is superseded by the CI-enforced docs/v4-deprecations.yaml ledger (M-001/M-002). Monitoring moves to the repo-internal docs/dev-status.md (module table refreshed: +_rdrobust_port/changes_in_changes/datasets/ rdd/rdplot, practitioner.py 1511->2113; added to conf.py exclude_patterns so the -W docs build has no orphan). Pointer sweep (~70 sites incl. four distinct stale phrasings: "TODO.md", bare "tracked in TODO", "TODO row NN", "TODO Deferred -> Parked"): source docstrings + runtime rejection messages, REGISTRY.md, METHODOLOGY_REVIEW.md, performance docs, api RST, paper reviews, tutorial notebooks 23+27, v4-design, and the v4 ledger notes now point at DEFERRED.md / TODO.md / docs/dev-status.md per row destination. The triple_diff conley message + its match="TODO" test pin updated as a pair (test renamed ..._deferred_pointer). CLAUDE.md gains the tracking-file map + decisions rule (behavioral -> REGISTRY Note; engineering -> DEFERRED.md Decision record; ledger = lifecycle authority with M-xxx cross-links); dev-checklists' stale "Tech Debt from Code Reviews" pointer fixed. Correctness fixes folded in: the stale "joint-Stute deferred" claim (shipped in Phase 3 via stute_joint_pretest) unstaled; four doc-referenced follow-ups that had NO row now have rows (SpilloverDiD Wave E.3 finite_mask parity, SpilloverDiD covariates= scope cut, SpilloverDiD vcov-reconstruction tests, HAD Theorem-6 covariates extension); the HAD row corrected against the shipped NotImplementedError trap; the MPD weighted-CR2-BM REGISTRY note rewritten to the verified survey-variance precedence contract (needs_survey_vcov is unconditionally True - no bare weights= on MultiPeriodDiD.fit); two investigated-and-parked rows moved out of Paper-gated; df_convention flip note repointed at ledger M-004..M-006. All 131 moved table rows byte-identical to the pre-split file (verified mechanically); exceptions are the two reshaped Known-Limitations rows and the practitioner.py line-count refresh. PR-1 (#698) taught the review contract to credit rows in either file; this PR removes its transitional comment wording. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01H8PDhdrdQH2DM8yAeEnib7 --- .claude/commands/dev-checklists.md | 6 +- .github/workflows/ai_pr_review.yml | 4 +- CHANGELOG.md | 12 + CLAUDE.md | 41 ++- DEFERRED.md | 243 ++++-------------- METHODOLOGY_REVIEW.md | 28 +- TODO.md | 47 ++++ diff_diff/chaisemartin_dhaultfoeuille.py | 4 +- diff_diff/continuous_did.py | 2 +- diff_diff/efficient_did.py | 4 +- diff_diff/had.py | 6 +- diff_diff/had_pretests.py | 8 +- diff_diff/imputation.py | 2 +- diff_diff/spillover.py | 6 +- diff_diff/stacked_did.py | 4 +- diff_diff/staggered.py | 2 +- diff_diff/synthetic_did.py | 10 +- diff_diff/triple_diff.py | 2 +- diff_diff/two_stage.py | 2 +- diff_diff/wooldridge.py | 8 +- diff_diff/wooldridge_results.py | 8 +- docs/api/had.rst | 2 +- docs/api/spillover.rst | 4 +- docs/conf.py | 2 + docs/dev-status.md | 124 +++++++++ docs/methodology/REGISTRY.md | 57 ++-- .../methodology/papers/gardner-2022-review.md | 2 +- .../papers/wooldridge-2023-review.md | 2 +- .../papers/wooldridge-2025-review.md | 4 +- docs/performance-plan.md | 10 +- docs/performance-scenarios.md | 2 +- docs/tutorials/23_spillover_tva.ipynb | 4 +- .../27_cic_distributional_effects.ipynb | 2 +- docs/v4-deprecations.yaml | 2 +- docs/v4-design.md | 2 +- tests/test_fixest_did_twfe_parity.py | 2 +- tests/test_methodology_conley.py | 2 +- tests/test_methodology_sun_abraham.py | 6 +- tests/test_openai_review.py | 4 +- tests/test_rust_backend.py | 2 +- tests/test_spillover.py | 6 +- tests/test_t23_spillover_tva_drift.py | 7 +- tests/test_triple_diff.py | 6 +- tests/test_two_stage.py | 2 +- 44 files changed, 379 insertions(+), 326 deletions(-) create mode 100644 TODO.md create mode 100644 docs/dev-status.md diff --git a/.claude/commands/dev-checklists.md b/.claude/commands/dev-checklists.md index 0b9dac7a3..4c70d6ab8 100644 --- a/.claude/commands/dev-checklists.md +++ b/.claude/commands/dev-checklists.md @@ -51,8 +51,10 @@ When implementing or modifying code that affects statistical methodology (estima (see CLAUDE.md "Documenting Deviations" for full format reference) - [ ] Include rationale (e.g., "defensive enhancement", "R errors here") - [ ] Ensure the deviation is an improvement, not a bug - - [ ] If deferring P2/P3 work: add row to `TODO.md` table under "Tech Debt from Code - Reviews" with columns `Issue | Location | PR | Priority` + - [ ] If deferring P2/P3 work: shippable items get a row in `TODO.md` (Actionable + Backlog; columns `Issue | Location | Origin | Effort | Priority`); blocked + items get a row in `DEFERRED.md` under the matching blocker section (columns + `Issue | Location | PR | Priority`) — see CLAUDE.md "Tracking-file map" 4. **Testing methodology-aligned behavior**: - [ ] Test that edge cases produce documented behavior (NaN, warning, etc.) diff --git a/.github/workflows/ai_pr_review.yml b/.github/workflows/ai_pr_review.yml index 395c77db6..38ac46fcb 100644 --- a/.github/workflows/ai_pr_review.yml +++ b/.github/workflows/ai_pr_review.yml @@ -113,8 +113,8 @@ jobs: # the `dismissed_comment` field on alert #14. The guard test # is belt-and-suspenders for accidental regressions, not a # complete adversarial parser. - # See DEFERRED.md (deferral registry; TODO.md until the split - # lands) for the long-term tracking of this gap. + # See DEFERRED.md (deferral registry) for the long-term + # tracking of this gap. # ───────────────────────────────────────────────────────────────── - name: Resolve PR number + metadata id: pr diff --git a/CHANGELOG.md b/CHANGELOG.md index ccbb5bc8d..323b439ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +- **Internal tracking docs reorganized (no library behavior change).** `TODO.md` is now + the actionable backlog only; blocked / parked work and won't-fix decisions moved to the + new root-level `DEFERRED.md` (deferral & decision registry, same blocker sections plus + a decision record); monitoring and current-state notes (module-size watch, SE / typing + posture, the Apple-Silicon M4 BLAS note) moved to the repo-internal + `docs/dev-status.md`. Version-gated v4 lifecycle items are canonically tracked in + `docs/v4-deprecations.yaml` (the former "Deprecated Code" section is folded into ledger + ids M-001/M-002). The AI-review contract credits tracked rows in either `TODO.md` or + `DEFERRED.md` (landed ahead of this split in #698); ~55 cross-references in source + docstrings, error messages, REGISTRY.md, and docs were repointed. Two previously + untracked follow-ups referenced by docs now have registry rows (SpilloverDiD Wave E.3 + `finite_mask` parity; HAD `covariates=` kwarg-trap). - **Documentation site restructured around 5 top-level sections** (Getting Started / Practitioner Guide / Tutorials / User Guide / API Reference). The previous flat navigation put 40 pages into the pydata-sphinx-theme header, leaving most of the diff --git a/CLAUDE.md b/CLAUDE.md index ab469b54b..cc7dcb717 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,25 +116,38 @@ wording will cause a P1 finding ("undocumented methodology deviation"). | `- **Deviation from R:** ` | Intentional differences from R packages | `- **Deviation from R:** R's fixest uses t-distribution at all levels` | | `**Note (deviation from R):** ` | Combined form, inline within edge case bullets | See SyntheticDiD section in REGISTRY.md | -**TODO.md format** — for deferring P2/P3 items only (P0/P1 cannot be deferred): - -Add a row to `TODO.md`. If the item is genuinely shippable (clear path, no external -blocker), put it under **Actionable Backlog** in the appropriate sub-section -(`Methodology / correctness`, `Performance`, or `Testing / docs`). If it is blocked, put it -under **Deferred / Documented** in the matching blocker sub-section (`Paper-gated / needs -methodology derivation`, `Needs external reference (R / Stata / Julia)`, `Parked — pending -user demand / out of scope`, or `Won't-fix / waived`). Either way the AI reviewer's -deviation-grep resolves on the row's `Location` + reason text. The two buckets use -different table shapes — Actionable rows carry an `Effort` column, Deferred rows a `PR` -column: - -Actionable Backlog: +**Tracking-file map** — for deferring P2/P3 items only (P0/P1 cannot be deferred): + +- **Shippable** (clear path, no external blocker) → a row in `TODO.md` under + **Actionable Backlog**, in the matching sub-section (`Methodology / correctness`, + `Performance`, or `Testing / docs`). +- **Blocked** → a row in `DEFERRED.md` under the matching blocker section + (`Paper-gated / needs methodology derivation`, `Needs external reference + (R / Stata / Julia)`, `Parked — pending user demand / out of scope`, or + `Version-gated (v4)`). +- **Decisions** (won't-fix / waived): if the decision pins **user-visible behavior or + methodology**, record it as a REGISTRY.md Note using the labels above; if it is + **internal engineering** (refactor waiver, perf trade-off, test-infrastructure call), + add it to `DEFERRED.md` → **Decision record — won't-fix / waived**. +- **Version-gated lifecycle items** (deprecated-kwarg removals, v4 default flips): + `docs/v4-deprecations.yaml` (CI-enforced) is the lifecycle authority — never restate + ledger status/targets in a row. A row carrying real implementation work (e.g. a soak + or recapture protocol) may exist in TODO.md/DEFERRED.md but must cross-link its + `M-xxx` id. +- **Monitoring / current-state notes** (module sizes, tooling posture, platform quirks) + go in `docs/dev-status.md`, not a backlog row. + +The AI reviewer's deviation-grep resolves on a row's `Location` + reason text in EITHER +`TODO.md` or `DEFERRED.md`. The two files use different table shapes — Actionable rows +carry an `Effort` column, DEFERRED rows a `PR` column: + +TODO.md → Actionable Backlog: | Issue | Location | Origin | Effort | Priority | |-------|----------|--------|--------|----------| | Description of the work item | `file.py` | #NNN | Quick/Mid/Heavy | Medium/Low | -Deferred / Documented: +DEFERRED.md (blocker sections): | Issue | Location | PR | Priority | |-------|----------|----|----------| diff --git a/DEFERRED.md b/DEFERRED.md index c17fb9eb3..2bba30ce8 100644 --- a/DEFERRED.md +++ b/DEFERRED.md @@ -1,70 +1,26 @@ -# Development TODO - -Internal tracking for technical debt, known limitations, and maintenance tasks. - -For the public feature roadmap, see [ROADMAP.md](ROADMAP.md). - -## How this file is organized - -- **[Actionable Backlog](#actionable-backlog)** — work with a clear implementation path and - no external blocker. **Pull from here.** 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. -- **[Deferred / Documented](#deferred--documented)** — known gaps that are **not currently - actionable**: blocked on a methodology derivation, on external tooling (R / Stata / Julia) - absent from CI, parked pending user demand / out of paper scope, or explicitly won't-fix. - Retained for provenance and AI-review deviation-documentation — **do not pull from here - without first clearing the named blocker.** -- **[Reference / Status](#reference--status)** — not backlog: user-facing limitations, - module-size monitoring, deprecations, and current-state notes. - -The `Origin` column (Actionable tables) and the `PR` column (Deferred tables) both point to the originating PR number or review tag. +# Deferral & Decision Registry + +Blocked, parked, and version-gated work, plus decisions on the record. **Not a backlog — +do not pull from here without first clearing the named blocker.** Rows are retained for +provenance and AI-review deviation-documentation: a row here (or in +[TODO.md](TODO.md)'s Actionable Backlog) marks a limitation as tracked for the PR reviewer. + +- Shippable work lives in [TODO.md](TODO.md); the public feature roadmap in + [ROADMAP.md](ROADMAP.md); monitoring and current-state notes in + [docs/dev-status.md](docs/dev-status.md). +- Version-gated lifecycle items (deprecated-kwarg removals, v4 default flips such as + `SyntheticDiD` `lambda_reg`/`zeta` and the clustered-df convention) are canonically + tracked in the CI-enforced ledger `docs/v4-deprecations.yaml`; rows here cross-link + their `M-xxx` ids where applicable. +- The `PR` column points to the originating PR number or review tag. --- -## Actionable Backlog - -### Methodology / correctness - -| Issue | Location | Origin | Effort | Priority | -|-------|----------|--------|--------|----------| -| `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. One-sided alternatives (Remark 1 signed statistic) and proxy covariates (eq 4/6 note) SHIPPED 2026-07. | `diff_diff/conformal.py`, `diff_diff/synthetic_control_results.py` | CWZ-2021 | Heavy | Low | -| `ContinuousDiD` CGBS-2024 extensions. (a) `covariates=` kwarg — **DONE (reg/dr)**; (b) discrete-treatment saturated regression (`treatment_type="discrete"`) — **DONE**; (c) lowest-dose-as-control per Remark 3.1 when `P(D=0)=0` (`control_group="lowest_dose"`) — **DONE** (discrete + continuous mass-point, single-cohort; estimand `ATT(d)−ATT(d_L)`; 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 | - -### 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 → Parked](#parked--pending-user-demand--out-of-scope).) - -| Issue | Location | Origin | Effort | Priority | -|-------|----------|--------|--------|----------| -| `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 the staged default-flip protocol used for `df_convention` (v4-class change). Lifecycle tracked 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 | -|-------|----------|--------|--------|----------| -| 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 | -| Align the four legacy dataset loaders (`load_card_krueger`, `load_castle_doctrine`, `load_divorce_laws`, `load_mpdta`) with the loud-fallback pattern of `load_prop99`/`load_walmart`: `UserWarning` + `df.attrs["source"]` marker on synthetic fallback (currently silent), plus optional checksum pinning for the CSV downloads. **Upgraded to a live defect 2026-07-13: the `causaldata/causal_datasets` GitHub repo backing castle/card_krueger/divorce is dead (404), so those loaders silently serve synthetic data everywhere - needs loud fallback + replacement sources.** | `diff_diff/datasets.py` | LWDiD precursor | Quick | Medium | -| Real-data CI canary for dataset-backed replication tests: `test_methodology_lwdid.py`'s Prop 99 / Walmart goldens skip (visibly) when loaders fall back to synthetic; add a lane or canary asserting `df.attrs["source"] == "lwdid_ssc_ancillary"` in CI so network regressions cannot silently de-gate the replication tests. Pairs with the loader-fallback repair row above. | `tests/test_methodology_lwdid.py`, `.github/workflows/` | LWDiD validation suite | Quick | Low | - ---- - -## Deferred / Documented - -Not currently actionable. Retained for provenance + AI-review deviation-documentation. - -### Paper-gated / needs methodology derivation +## Paper-gated / needs methodology derivation | Issue | Location | PR | Priority | |-------|----------|----|----------| | `PlaceboTests` `boundary_gap` — a permutation randomization-inference margin (SE-audit item (b)); NOT computed anywhere in code today, so this is a new feature + result field, not a coverage lock. **User-locked 2026-07-09: defer until a derivation/paper source exists** — do not design or implement from scratch. | `tests/test_methodology_placebo.py`, `diff_diff/diagnostics.py` | SE-audit | Low | -| `SyntheticControl` fit-snapshot residency (`_SyntheticControlFitSnapshot`) — **investigated 2026-07-07, parked**: the snapshot ALIASES the fit's own working pivots (zero extra construction cost); the retained residency implements the documented freeze contract (post-fit mutation of estimator inputs must not change `in_space_placebo()` / `leave_one_out()` / conformal output on an already-returned results object, and `__getstate__` already excludes it from pickles). A compact array representation saves only pandas overhead (the float panel dominates); releasing residency needs new API surface (`release`/opt-out flag) or a freeze-contract change. Revisit on user demand for very large donor panels. | `synthetic_control.py`, `synthetic_control_results.py` | follow-up | Low | -| Stratified survey-PSU multiplier-weight draw-tiling — **investigated 2026-07-07, parked**: the stratified generator (`generate_survey_multiplier_weights_batch`) consumes ONE sequential rng stream stratum-major (`rng.choice(size=(n_bootstrap, n_h))` per stratum, then lonely-PSU pooling), so draw-chunked assembly CANNOT reproduce the stream bit-identically (contra the old row's parenthetical) — it would need per-stratum generator state skipping (PCG64.advance + per-weight-type variate accounting; fragile) or a stream-layout change (MC-level SE changes → baseline/golden recapture + REGISTRY note). Stratified designs have few PSUs, so the full `(n_bootstrap × n_psu)` matrix rarely matters; unstratified (the large-`n_units` case) is already tiled. Revisit only if a large-PSU stratified design hits memory, as a documented stream change. | `diff_diff/bootstrap_chunking.py::iter_survey_multiplier_weight_blocks` | follow-up | Low | | CBWSDID covariate balancing (`StackedDiD(balance="entropy")`) v1 supports only balanced event windows + `weighting="aggregate"`; unbalanced/ragged panels fail closed (unit-count vs observation-count corrector convention unresolved off balanced panels). Matching-based balancing and the repeated `0→1`/`1→0` episode extension are also deferred. Documented in REGISTRY StackedDiD "Covariate balancing (CBWSDID)" Notes. | `stacked_did.py`, `balancing.py`, REGISTRY | follow-up | Low | | dCDH: Phase-1 per-period placebo `DID_M^pl` has NaN SE (no IF derivation for the per-period aggregation path). Multi-horizon placebos (`L_max ≥ 1`) have valid SE. | `chaisemartin_dhaultfoeuille.py` | #294 | Low | | dCDH: survey cell-period allocator's post-period attribution is a library convention, not derived from the observation-level survey linearization. MC coverage is empirically close to nominal; a formal derivation (or covariance-aware two-cell alternative) is deferred. Documented in REGISTRY survey IF expansion Note. | `chaisemartin_dhaultfoeuille.py`, REGISTRY | #408 | Medium | @@ -80,7 +36,7 @@ Not currently actionable. Retained for provenance + AI-review deviation-document | **`LPDiD` non-absorbing exit-event dynamics** (Dube et al. 2025 online Appendix C `eta_h^{g,n}`): the shipped `non_absorbing` modes estimate the **entry-effect** estimands (Eq. 12/13) only; separate dynamic event-studies for treatment switch-*offs* are not implemented. Needs the exit-event clean-sample derivation + estimand contract. | `lpdid.py`, REGISTRY | PR-C follow-up | Low | | **`LPDiD` non-absorbing interior-gap support**: non-absorbing modes require a gap-free panel within each unit's observed span and raise on interior time gaps (the `[t-L, t+h]` window conditions can't be verified across a gap). The absorbing path already reindexes interior gaps to the calendar grid; extending that fail-closed handling (per-window gap masking) to non-absorbing is deferred. | `lpdid.py::_prepare_panel` | PR-C follow-up | Low | -### Needs external reference (R / Stata / Julia) +## Needs external reference (R / Stata / Julia) Blocked on tooling absent from CI (no workflow installs R/Stata/Julia). A clear path exists but parity can't be verified without a local toolchain. @@ -106,19 +62,22 @@ exists but parity can't be verified without a local toolchain. | **`LPDiD` non-absorbing R-parity - DONE (PR-C2)** via an independent `fixest::feols` Eq. 12/13 reconstruction (point+SE ~1e-13/~1e-15 vw; `effect_stabilization` reweighted point + pinned SE). `alexCardazzi/lpdid`'s `nonabsorbing_lag` proved NOT a faithful Eq. 13 (off-switch clamp + non-paper boundary/placebo window; diverges ~0.01-0.05 even on a monotone panel), so it is recorded as a divergent reference, not a gate. **Residual external-reference gap:** the authors' canonical non-absorbing SE/RA is Stata `lpdid`/`teffects` only (no faithful R analogue) - same class as the absorbing RA-SE row above; revisit if a Stata toolchain or a corrected R package appears. | `benchmarks/R/generate_lpdid_golden.R`, `tests/test_methodology_lpdid.py` | PR-C2 | Low | | `HeterogeneousAdoptionDiD` Phase-3 R-parity: ships coverage-rate validation on synthetic DGPs, not tight point parity vs `chaisemartin::stute_test` / `yatchew_test` (needs bootstrap-seed-semantics + `B` alignment across numpy/R). | `tests/test_had_pretests.py` | Phase 3 | Low | -### Parked — pending user demand / out of scope +## Parked — pending user demand / out of scope Doable in principle, but no current caller and/or explicitly out of paper scope. +For survey-specific limitations (`NotImplementedError` paths), see the +[Current Limitations](docs/survey-roadmap.md#current-limitations) section of survey-roadmap.md. | Issue | Location | PR | Priority | |-------|----------|----|----------| +| `SyntheticControl` fit-snapshot residency (`_SyntheticControlFitSnapshot`) — **investigated 2026-07-07, parked**: the snapshot ALIASES the fit's own working pivots (zero extra construction cost); the retained residency implements the documented freeze contract (post-fit mutation of estimator inputs must not change `in_space_placebo()` / `leave_one_out()` / conformal output on an already-returned results object, and `__getstate__` already excludes it from pickles). A compact array representation saves only pandas overhead (the float panel dominates); releasing residency needs new API surface (`release`/opt-out flag) or a freeze-contract change. Revisit on user demand for very large donor panels. | `synthetic_control.py`, `synthetic_control_results.py` | follow-up | Low | +| Stratified survey-PSU multiplier-weight draw-tiling — **investigated 2026-07-07, parked**: the stratified generator (`generate_survey_multiplier_weights_batch`) consumes ONE sequential rng stream stratum-major (`rng.choice(size=(n_bootstrap, n_h))` per stratum, then lonely-PSU pooling), so draw-chunked assembly CANNOT reproduce the stream bit-identically (contra the old row's parenthetical) — it would need per-stratum generator state skipping (PCG64.advance + per-weight-type variate accounting; fragile) or a stream-layout change (MC-level SE changes → baseline/golden recapture + REGISTRY note). Stratified designs have few PSUs, so the full `(n_bootstrap × n_psu)` matrix rarely matters; unstratified (the large-`n_units` case) is already tiled. Revisit only if a large-PSU stratified design hits memory, as a documented stream change. | `diff_diff/bootstrap_chunking.py::iter_survey_multiplier_weight_blocks` | follow-up | Low | | ChangesInChanges FULL Melly-Santangelo covariate estimator (monotonized integrated-indicator conditional CDFs, treated-post `F_{X|11}` integration, exchangeable bootstrap with variance-weighted KS bands, tail trimming, pre-period specification test). The qte-`xformla` simplified form of the MS pipeline SHIPPED in the covariates PR (`covariates=` on both estimators, parity-tested vs qte 1.3.1); this row's earlier "No R parity target exists" claim was WRONG and is corrected in the MS review doc. The full estimator has no reference implementation (the MS Stata code is the only one; distinct from Kranker's `cic`) and would need simulation-based validation. Reviewed: `docs/methodology/papers/melly-santangelo-2015-review.md`. | `diff_diff/changes_in_changes.py` | #682 | Low | | ChangesInChanges discrete-outcome bounds + DCIC point identification (Athey-Imbens Sections 4/5.2 incl. Imbens-Manski intervals; Kranker's Stata `cic` is the reference). The shipped ties warning marks the boundary of the continuous scope. | `diff_diff/changes_in_changes.py` | #682 | Low | | ChangesInChanges analytical SEs (Athey-Imbens Theorems 5.1-5.3 influence functions, panel 5.5-5.7, Appendix B covariances; needs the footnote-31 boundary density estimator - note the review's suspected half-range/midpoint typo). Bootstrap is the shipped inference. | `diff_diff/changes_in_changes.py` | #682 | Low | | Staggered/multi-period distributional DiD (Athey-Imbens Section 6 / Ciaccio arXiv:2408.01208v2; `ecic` is the staggered event-study CiC lineage - a distinct method from Ciaccio's copula approach, do not conflate). Reviewed: `docs/methodology/papers/ciaccio-2024-review.md`; ROADMAP row is reviewed-deferred pending demand. | `diff_diff/changes_in_changes.py` | #682 | Low | | ChangesInChanges treatment-on-the-controls (Athey-Imbens Theorem 3.2: group-label exchange + negation; no qte equivalent to anchor parity). | `diff_diff/changes_in_changes.py` | #682 | Low | | Rust-backend CR2 Bell-McCaffrey port (`return_dof` in the Rust vcov dispatch + CR2 algebra) — **premise re-scoped 2026-07-09**: the scores-based DOF + low-rank factored `A_g` changes made the NumPy CR2-BM path BLAS-bound (`O(n_g k²)` per cluster; 4.1s→38ms at n=100k/k=40), so a Rust port buys ~nothing and adds a parity surface. Revisit only if profiling shows CR2-BM hot again. | `rust/src/linalg.rs` | — | Low | -| Clustered-CR1 inference df **default flip to `"cluster"` (G−1) at v4** — the opt-in `df_convention=` knob landed 2026-07 (DiD/TWFE/MPD + LinearRegression; REGISTRY §TwoWayFixedEffects deviation note); the remaining work is the major-version default change (moves every clustered p-value/CI) + migration note + flipping `TestDfConvention`/`test_moderate_t_pins_residual_df_convention` expectations. Also evaluate extending the knob to standalone estimators with CR1-t inference at that time. Lifecycle tracked in docs/v4-deprecations.yaml (M-004..M-006). | `diff_diff/linalg.py::LinearRegression`, `diff_diff/estimators.py`, `diff_diff/twfe.py` | — | Medium | | CallawaySantAnna **unbalanced-panel R parity — LANDED** via `allow_unbalanced_panel=True` (matches R `did::att_gt(allow_unbalanced_panel=TRUE)` / `DRDID::reg_did_rc`: ATT bit-exact on cells AND dynamic aggregation via fixed unit-cohort-mass `pg` + a per-unit WIF; SE up to the documented CR1 `sqrt(G/(G-1))` factor). The earlier "weighting" framing was a mis-diagnosis — on unbalanced panels the dominant divergence from R is the *estimator* (within-cell differencing vs RC-on-pooled-obs), not only the weighting; both are resolved by the flag. The DEFAULT path keeps within-cell differencing as a documented design choice and now emits a `UserWarning` on unbalanced input (no-silent-failures). **Remaining deferred:** `survey_design=` × `allow_unbalanced_panel=` (per-obs vs per-unit weight resolution — currently fail-closed `NotImplementedError`); and covariate / ipw / dr × the flag R-parity verification (the RC path supports them; the committed golden covers `reg` no-cov). | `staggered.py`, `staggered_aggregation.py` | SE-audit D3 | Low | | CallawaySantAnna event-study bucket/weight construction is duplicated between the analytical aggregator (`staggered_aggregation.py::_aggregate_event_study`) and the multiplier bootstrap (`staggered_bootstrap.py`): both group (g,t) by `e = t - g`, apply the finite/NaN/reference masks, and read cohort weights. Both already consume the same source-materialized universal reference cells (so they agree), but the bucket logic is copy-pasted. Extract one shared helper returning per-event-time buckets (finite cells, NaN cells, reference flags, cohort weights, combined-IF inputs) used by both. Pure refactor; gate on byte-identical analytical + bootstrap output. | `staggered_aggregation.py`, `staggered_bootstrap.py` | SE-audit D3 | Low | | `StackedDiD` survey re-resolution intra-file dedup (raw-weight extraction ×3, compose-normalize ×3, resolve-on-stacked ×2). The cross-estimator ContinuousDiD/EfficientDiD panel-to-unit collapse consolidation LANDED (#226 shared helpers `ResolvedSurveyDesign.subset_to_units_by_row_idx` / `build_unit_first_row_index`); StackedDiD is deliberately NOT on that path (control units are duplicated across sub-experiments, so it re-resolves at stacked granularity rather than collapsing to one row per unit). The residual is stacked-specific, low value, and touches the numerically-sensitive composed-weight renormalization. Post-filter re-resolution / metadata-recompute unification across the three estimators was assessed and is not warranted — they use genuinely different mechanisms and already delegate to shared `_resolve_survey_for_fit` / `compute_survey_metadata`. | `stacked_did.py` | #226 | Low | @@ -141,8 +100,29 @@ Doable in principle, but no current caller and/or explicitly out of paper scope. | `TestWorkflowDoesNotExecutePRHeadCode` (CodeQL #14 guard) doesn't model `bash/sh/./source