@@ -9,6 +9,122 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
1010<!-- entries live in changelog.d/*.md; compiled at release by .claude/scripts/changelog_compile.py -->
1111
12+ ## [3.11.1] - 2026-08-30
13+
14+ ### Added
15+ - **DMLDiD replicate-weight survey designs**: `DMLDiD` now accepts
16+ replicate-weight `SurveyDesign`s (BRR / Fay / JK1 / JKn / SDR) on both
17+ lanes (panel and repeated cross sections), computing per-cell AND
18+ aggregate variances by IF-reweighting the augmented cross-fitted scores
19+ (`compute_replicate_if_variance`; nuisances are not re-estimated per
20+ replicate). Inference uses `df = rank(replicate matrix) - 1` with
21+ `min(df_survey, n_valid - 1)` capping; degenerate cells (zero or
22+ non-finite replicate variance) fail closed to NaN inference. Replicate +
23+ `cluster=` and replicate + `n_bootstrap > 0` are rejected with targeted
24+ errors (previously all replicate designs failed closed with a blanket
25+ `NotImplementedError`).
26+ - **Chang (2020) §4.2.2 RCS simulation-DGP replication** (`tests/test_methodology_dml_did.py`,
27+ DML PR-B2): the paper's own kernel-design repeated-cross-section DGP as
28+ maintainer validation fixtures for `DMLDiD(panel=False)` — a DGP-shape pin
29+ (distributions, all three innovation scales, the design's built-in confounded
30+ contrast → θ₀+1, and both correct-specification facts), seed-pinned recovery at
31+ both paper sample sizes with a discriminating comparison against the unadjusted
32+ contrast, and a slow Monte Carlo coverage lane. The §4.2 parameterizations are
33+ extracted into the paper review; the §4.2.1 ML design is documented as not
34+ replicable with the bundled unpenalized learners (narrowed TODO row) and the
35+ REGISTRY carries the replication-scope Note.
36+
37+ ### Changed
38+ - **`n_bootstrap` type guards aligned onto `utils.validate_n_bootstrap`**
39+ for the estimators the M-081 sweep deliberately left out —
40+ `HeterogeneousAdoptionDiD`, `ChaisemartinDHaultfoeuille`, `TROP`,
41+ `SyntheticDiD` (jackknife lane included), plus the two HAD pretest
42+ helpers (`stute_test`, `stute_joint_pretest`): previously-accepted
43+ type-blind values now raise the shared message — `True` (silently ran as
44+ 1 replicate on HAD/dCDH), floats like `2.5` (passed the `>= 2` floors),
45+ and bool/negative under SyntheticDiD's jackknife floor exemption. The
46+ estimator-specific floors are unchanged and keep their own messages for
47+ non-negative sub-floor integers (TROP/SDiD `n_bootstrap=1`, HAD `0`);
48+ NEGATIVE values now surface the shared validator's message instead of
49+ each estimator's former wording.
50+ - **`honest_did` inference-df resolution consolidated** onto the shared
51+ `aggregation.resolve_inference_df()` (three duplicated precedence blocks
52+ removed). Same precedence; `HonestDiDResults.df_survey` is now
53+ float-typed (`31.0` where it was `31`), and a fractional `df_inference`
54+ is preserved instead of truncated.
55+
56+ ### Fixed
57+ - **Per-row event-study df provenance (M-092 completion)** for the four
58+ remaining holes — `EfficientDiD`, `ImputationDiD`, `ContinuousDiD`, and
59+ `HeterogeneousAdoptionDiD`: each results class gains a results-level
60+ `event_study_df` scalar (appended last; positional `__init__` indexes
61+ unchanged) threaded into the unified event-study container's per-row `df`
62+ column, which was all-NaN even on survey fits whose p-values were governed
63+ by a finite survey df. Finite on analytical survey fits (ImputationDiD:
64+ the final replicate-override df, level-matched on replicate replays, lead
65+ rows included; EfficientDiD: the post-overall snapshot; HAD: the
66+ unit-level design df); `None` — never the replicate-undefined `0`
67+ sentinel — on non-survey fits, on bootstrapped fits (percentile inference
68+ used no df, matching the shipped producer convention), and when no
69+ event-study surface was built. Inference values are unchanged everywhere.
70+ - **`ContinuousDiD` `survey_metadata` granularity unified across inference
71+ branches**: the bootstrap and degenerate no-post-cells arms now publish
72+ the same UNIT-level metadata as the analytical arm (the
73+ CS/EfficientDiD convention); previously they kept the obs-level resolve,
74+ so `sum_weights`/`effective_n`/`n_psu` — and `df_survey` on implicit-PSU
75+ designs — differed from the analytical arm by panel length on the same
76+ data. Metadata provenance only; estimates and inference unchanged.
77+ - **Documented (no behavior change)**: the event-study container's
78+ `df_survey` SCALAR — the fit's resolved scalar inference df — deliberately
79+ persists on bootstrapped fit-time and replayed surfaces (CS, DMLDiD,
80+ EfficientDiD identically) as the consumer channel HonestDiD's container
81+ branches read; the per-row `df` column is the inference-provenance channel
82+ that percentile bootstrap clears. Recorded as a REGISTRY Note with a
83+ cross-estimator parity pin.
84+ - **`survey_metadata` raw-scale provenance on the unit-level recompute**
85+ (CallawaySantAnna panel + repeated-cross-section lanes,
86+ `TripleDifference`/`StaggeredTripleDifference` staggered engine,
87+ `ContinuousDiD` analytical branch, `EfficientDiD`): the recompute passed
88+ the RESOLVED (mean-1 rescaled) weights as `compute_survey_metadata`'s
89+ raw weights, so `sum_weights`/`weight_range` reported the normalized
90+ scale instead of the user's original weight scale. They now report the
91+ raw scale, matching every other estimator (DMLDiD got the pattern in
92+ its survey PR). For previously-successful fits whose survey design does
93+ not alias a mutated role column, this is metadata-provenance only:
94+ estimates, SEs, p-values, CIs, `df_survey`, `n_strata`, `n_psu` are
95+ byte-identical, and `effective_n`/`design_effect` are scale-invariant
96+ (unchanged within floating-point round-off). Additionally,
97+ `ContinuousDiD`'s zero-dose-unit filter now re-resolves the survey
98+ design from pristine input rows: a design column aliasing a mutated
99+ role column (e.g. `weights` naming the dose column) previously
100+ zero-weighted every never-treated unit on filtered fits (failing with
101+ "No valid (g,t) cells"); such fits now estimate under the user's
102+ original weights, consistent with the unfiltered path.
103+
104+ ### Internal
105+ - **Changelog fragments**: release notes are now authored as per-PR files
106+ under `changelog.d/` (see `changelog.d/README.md`) instead of editing
107+ `CHANGELOG.md`'s `## [Unreleased]` section, which stays pointer-only
108+ between releases (CI-enforced by `tests/test_changelog_fragments.py`) so
109+ concurrent PRs no longer conflict on the changelog. At release,
110+ `.claude/scripts/changelog_compile.py compile` merges the fragments into
111+ the new version section and deletes them. One ordering change relative to
112+ the old convention: within a category, compiled release sections list
113+ entries oldest-first (ascending fragment-filename order) rather than the
114+ newest-first order that prepending into Unreleased produced.
115+ - **Repo-process tooling** (infra sweep, part 2): a weekly `LWDiD Data
116+ Canary` CI lane that fails loudly when the SHA-pinned Prop 99 / Walmart
117+ loaders fall back to synthetic data (previously a visible-but-green test
118+ skip), then runs the replication tests it de-gates; a
119+ `tests/test_tracking_files.py` contract guard for TODO.md/DEFERRED.md
120+ (no deferred-work pointers in TODO rows, no ledger-lifecycle
121+ restatements beside `M-xxx` cross-links, documented table shapes with
122+ per-row column counts — which also surfaced and fixed two DEFERRED.md
123+ rows whose unescaped pipes broke the rendered tables and one row
124+ restating M-010's version target); and the `/push-pr-update`
125+ committed-range methodology scan restored via `premerge_scan.py --range`
126+ with the comparison ref passed as quoted data.
127+
12128## [3.11.0] - 2026-08-29
13129
14130### Added
@@ -5479,6 +5595,7 @@ for the full feature history leading to this release.
54795595[2.1.2]: https://github.com/igerber/diff-diff/compare/v2.1.1...v2.1.2
54805596[2.1.1]: https://github.com/igerber/diff-diff/compare/v2.1.0...v2.1.1
54815597[2.1.0]: https://github.com/igerber/diff-diff/compare/v2.0.3...v2.1.0
5598+ [3.11.1]: https://github.com/igerber/diff-diff/compare/v3.11.0...v3.11.1
54825599[3.11.0]: https://github.com/igerber/diff-diff/compare/v3.10.0...v3.11.0
54835600[3.10.0]: https://github.com/igerber/diff-diff/compare/v3.9.1...v3.10.0
54845601[3.9.1]: https://github.com/igerber/diff-diff/compare/v3.9.0...v3.9.1
0 commit comments