Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d7146c0
fix: make legacy dataset provenance explicit
charlesshawmsix Jul 23, 2026
f9b875b
fix: preserve verified dataset downloads on cache write failure
charlesshawmsix Jul 23, 2026
d730c0b
fix: complete dataset loader provenance safeguards
shawcharles Jul 27, 2026
5270086
docs: castle treatment-coding registry note + dataset provenance doc …
igerber Jul 27, 2026
35d1cf3
fix: widen download fallback to HTTPException; scope datasets.rst pro…
igerber Jul 27, 2026
aa4eb60
fix: clean up partial cache files on write failure; align stale datas…
igerber Jul 27, 2026
90d10ed
fix: correct castle income label, float32 exactness claim, and LLM-gu…
igerber Jul 27, 2026
35488b4
docs: reframe tutorial 09 divorce section as a simulation, not a repl…
igerber Jul 27, 2026
f63169f
fix: documented Card-Krueger workflow breaks on the canonical incompl…
igerber Jul 27, 2026
c8368cb
fix: qualify the fallback contract for verified-cache recovery; surfa…
igerber Jul 27, 2026
ee5a568
chore: map datasets.py to its REGISTRY section; correct stale castle-…
igerber Jul 27, 2026
3a3282c
docs: align the remaining fallback-contract surfaces with cache recovery
igerber Jul 27, 2026
49b85dc
fix: clear_cache() could not remove interrupted atomic-write scratch …
igerber Jul 27, 2026
8c8fa83
fix: verified cache must survive every fresh-download verification fa…
igerber Jul 27, 2026
d5c7e2a
docs: two internal contract statements still said a checksum mismatch…
igerber Jul 27, 2026
d126b6d
fix: test_clear_cache_creates_directory wiped the developer's real da…
igerber Jul 27, 2026
03c5433
test: exercise the pinned production mpdta bytes end-to-end offline
igerber Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 39 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
event-study surface) and M-093 (4.0 sentinel retirement, phase 5) are added.
**Estimator equations, weighting, variance, and numerical output are unchanged.**

### Changed
### Fixed
- **Legacy dataset loaders no longer silently present synthetic observations as
canonical data.** `load_card_krueger()`, `load_castle_doctrine()`, and
`load_mpdta()` now use commit-pinned, SHA-256-verified sources with schema and
panel-invariant validation; successful frames expose a dataset-specific
`df.attrs["source"]`. When neither a verified cache entry nor a verified fresh
download is available, the loader emits exactly one `UserWarning` containing
`SYNTHETIC` and marks the frame `source="synthetic_fallback"`. Every way a fresh
download can fail verification - transport error, size limit, or checksum
mismatch - falls back to a checksum-valid cache entry when one exists, including
under `force_download=True`, so a tampered or moved upstream can never downgrade
a user holding verified canonical bytes to generated data. A checksum mismatch
additionally warns that the upstream no longer matches the pin (a distinct
warning that deliberately does not contain `SYNTHETIC`, since no synthetic data
is involved). `load_divorce_laws()` now follows that explicit
fallback path because no verified source currently reproduces its composite
public schema without additional analytical choices. Verified fresh downloads
are returned even when best-effort cache persistence fails.

### Changed
- **Castle Doctrine now exposes first-year treatment intensity.** The loader
retains the source's fractional adoption-year `cdl` value as
`treatment_exposure`, while preserving the existing binary `treated` field.
Canonical frames carry the fractional value; synthetic fallback frames carry a
binary 0/1 copy of `treated`. Reproducing Cheng-Hoekstra requires regressing
`log(homicide_rate)` on `treatment_exposure`, not on `treated` - see
`docs/methodology/REGISTRY.md`.

- **`load_card_krueger()` returns the canonical 410-store survey, which is
incomplete.** 12 stores have no `emp_pre`, 14 no `emp_post`, and `wage_pre` /
`wage_post` are missing for 20 and 21 stores. The synthetic frame this loader
previously returned was complete, so code that reshapes to long format and
fits without dropping missing outcomes estimated fine before and now raises
`ValueError: Column '<outcome>' contains missing values`. Drop the missing
outcome rows before fitting; the loader docstring and
`docs/api/datasets.rst` examples show the pattern. Note that the same code
still succeeds offline, where the loader falls back to the complete synthetic
frame.

- **dCDH Phase 1 (`L_max=None`) event-study row is now re-synced by the final-df
refresh.** Under replicate-weight designs the refresh recomputes `overall_*` with the
final effective df, but the Phase 1 event-study row is a VALUE COPY of that inference
Expand Down
3 changes: 1 addition & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,12 @@ generic sparse-FE, QR+SVD rank-detection redundancy, `check_finite` bypass — m

| Issue | Location | Origin | Effort | Priority |
|-------|----------|--------|--------|----------|
| 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 |
| 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 |
| MMM interop follow-up: Meridian `roi_calibration_period` mask builder - accept the MMM's time index + channel order and emit the boolean `(n_media_times, n_media_channels)` mask so `.to_code()` scopes the prior to the experiment window automatically (today the caller passes a mask expression / `full_model_window=True`). | `diff_diff/mmm.py` | mmm-interop | Quick | Low |
| MMM interop PR-B: calibration tutorial notebook (fit DiD/CS -> scope -> `to_pymc_marketing_lift_test` / `to_meridian_roi_prior`) + a `llms-practitioner.txt` Step 8 pointer to the exporters as the MMM hand-off. | `docs/tutorials/`, `diff_diff/guides/llms-practitioner.txt` | mmm-interop | Mid | Low |
| Section-8 naming-completeness guard test: sweep the public surface (module-level functions, class `__init__`/`fit` params, results dataclass fields) for contract-rename violations and fail unless each hit is either a `docs/v4-deprecations.yaml` row or an allowlist entry carrying a stated reason (seed the allowlist with the documented domain-vocabulary exemptions: the staggered family's ATT(g,t) `group`/`groups`, `TripleDifference.fit[group]`, wrapper params dying with `M-070..M-077`, and params inherited from `DifferenceInDifferences.__init__`). Today the rules are normative prose and the ledger holds only what an audit remembered — two successive manual sweeps each found surfaces the prior one missed (`M-094`/`M-095`, then the 19-row `M-097..M-115` function sweep). Should land BEFORE Phase 2c so the rename PR works from a mechanically-verified list. Two further checks belong in the same guard, each having been missed by hand across successive review rounds: (a) **phase-table agreement** — every row's `phase` must appear in the matching `docs/v4-design.md` section 9 table entry and vice versa (the section 9 checklist asserts this agreement but nothing verifies it, and three separate rounds caught a stale table); (b) **consumer coverage** (section 8 rule 11) — for each rename row, grep `diff_diff/` and `docs/methodology/` for the old name and require every hit to be either in the row's `code_refs` or allowlisted, since `getattr(obj, "old_name", default)` degrades silently rather than raising after removal. | `tests/`, `docs/v4-design.md` | gating-completeness amendment | Mid | Medium |
| Tracking-file contract guard test: reject NEW active deferred-work pointers at `TODO.md` (deferred rows live in `DEFERRED.md`; allowlist for historical/past-tense prose and actionable-row pointers) and assert rows cross-linking a `docs/v4-deprecations.yaml` `M-xxx` id don't restate ledger status. Origin: tracking-split local review R2. | `tests/`, `TODO.md`, `DEFERRED.md` | tracking-split | Quick | Low |
| 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 |
| 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. Follow-on from the loader-fallback repair (#723), which made provenance explicit but deliberately did not add a network-dependent CI lane. | `tests/test_methodology_lwdid.py`, `.github/workflows/` | LWDiD validation suite | Quick | 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 |
| Re-add committed-range methodology scanning to `/push-pr-update` §3b (clean tree, commits ahead) using `premerge_scan.py --range`, with the comparison ref passed as **data** (resolved into a quoted variable in one Bash call, never a raw `<placeholder>`). It was removed to avoid ref interpolation; the helper already implements and tests `--range`. | `.claude/commands/push-pr-update.md` | skill-audit | Quick | Low |
Expand Down
Loading
Loading