Skip to content

Commit 09b1027

Browse files
igerberclaude
andcommitted
docs(v4): diagnostic-family amendment - third object kind + RDPlot rename rows
Spec section 3.5 formalizes diagnostics as a third object kind alongside estimators and results, with exactly one load-bearing bit: estimator results carry the canonical inference quintet, diagnostic results do not. Membership is the library's EXISTING Diagnostics-and-Sensitivity docs family made checkable (parallel-trends testing, placebo suite, HonestDiD, pre-trends power, power analysis), with BaconDecomposition and RDPlot re-homed into it at 4.0 [M-090] and the upcoming CJM 2020 density test born into it. One Diagnostic marker base ships additively in Phase 2 on the class-shaped members (input shape deliberately unconstrained: data-in and results-in both belong); function members stay functions; import paths do not move; zero new classes beyond the base. (A narrower classes-only tier with HonestDiD/PowerAnalysis carve-outs was drafted and rejected: the docs family already draws the right line.) RDPlot.fit()'s post-freeze outcome_col/running_col params get standard rename rows [M-088]/[M-089] under the spec's missed-rename clause - the ledger's first live maintenance event. 74-row ledger; snapshot/floor updated in the same diff. Docs + ledger + test counts only; no public API or numerical behavior change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eVKbBqzenDKa5idwtexQa
1 parent cffbe79 commit 09b1027

4 files changed

Lines changed: 177 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2626
purpose-phrasing, restoring the case as a genuinely clean negative control.
2727

2828
### Added
29+
- **Internal: 4.0 diagnostic-family amendment to the design spec.** `docs/v4-design.md`
30+
gains section 3.5 formalizing a third object kind - diagnostics: the library's
31+
existing "Diagnostics & Sensitivity" family (parallel-trends testing, placebo
32+
suite, HonestDiD, pre-trends power, power analysis) made checkable, with
33+
`BaconDecomposition` and `RDPlot` re-homed into it at 4.0 and the upcoming
34+
density test born into it. One load-bearing bit: estimator results carry the
35+
canonical inference quintet, diagnostic results do not (exempt BY TYPE via a
36+
single `Diagnostic` marker base on the diagnostic RESULT containers, shipping
37+
additively in Phase 2 with a ledger-gated roster test (M-091) - zero new
38+
classes beyond the base, no import-path moves; raw-dict diagnostic functions
39+
participate in the docs family only). The 4.0 docs/roster
40+
reorganization is ledger-tracked (M-090) so the release cut cannot miss it,
41+
and `RDPlot.fit()`'s post-freeze `outcome_col`/`running_col` params get their
42+
standard rename rows (M-088/M-089) under the spec's missed-rename clause.
43+
Docs + ledger + test-count updates only - **no public API or numerical behavior
44+
change.**
2945
- **Reviewer-eval harness: N-arm matrix, blinded grading, corpus grown 2 -> 11
3046
(`tools/reviewer-eval/`, prep for the GPT-5.6 reviewer evaluation).**
3147
`config/configs.json` moves from the two-arm `control`/`candidate` shape to an

docs/v4-deprecations.yaml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,56 @@ rows:
869869
code_refs: [diff_diff/wooldridge_results.py]
870870
notes: "summary() unifies to the library-wide summary(alpha=None) signature (spec section 5); aggregation selection lives on aggregate(). The alpha param arrives additively in the same PR."
871871

872+
- id: M-088
873+
kind: param
874+
group: renames-col-suffix
875+
old: "diff_diff:RDPlot.fit[outcome_col]"
876+
new: "diff_diff:RDPlot.fit[outcome]"
877+
introduced_in: "3.9"
878+
deprecated_in: "3.9"
879+
removed_in: "4.0"
880+
status: planned
881+
phase: 2
882+
warning: FutureWarning
883+
code_refs: [diff_diff/rdplot.py]
884+
notes: "RDPlot shipped post-ledger-freeze with _col suffixes; the spec section 8 missed-rename clause applies (matches RegressionDiscontinuity [M-040])."
885+
- id: M-089
886+
kind: param
887+
group: renames-col-suffix
888+
old: "diff_diff:RDPlot.fit[running_col]"
889+
new: "diff_diff:RDPlot.fit[running]"
890+
introduced_in: "3.9"
891+
deprecated_in: "3.9"
892+
removed_in: "4.0"
893+
status: planned
894+
phase: 2
895+
warning: FutureWarning
896+
code_refs: [diff_diff/rdplot.py]
897+
notes: "Companion to [M-088]; matches [M-041]."
898+
- id: M-090
899+
kind: behavior
900+
group: diagnostic-family
901+
old: "diff_diff:BaconDecomposition"
902+
new: null
903+
deprecated_in: "4.0"
904+
removed_in: null
905+
status: planned
906+
phase: 5
907+
code_refs: [README.md, diff_diff/guides/llms.txt, diff_diff/bacon.py]
908+
notes: "4.0 diagnostic-family docs/roster reorganization (spec section 3.5): Bacon re-homed OUT of the API estimator roster (misfiled today); RDPlot (already documented as a diagnostic) consolidated under the unified family grouping; README/llms.txt/API/alias groupings and estimator-count claims split into estimators + diagnostics. The Phase 2 marker introduction is gated separately by [M-091]."
909+
- id: M-091
910+
kind: behavior
911+
group: diagnostic-family
912+
old: "diff_diff:BaconDecompositionResults"
913+
new: null
914+
introduced_in: "3.9"
915+
deprecated_in: null
916+
removed_in: null
917+
status: planned
918+
phase: 2
919+
code_refs: [diff_diff/bacon.py, diff_diff/rdplot.py, diff_diff/honest_did.py, diff_diff/pretrends.py, diff_diff/power.py, diff_diff/diagnostics.py, diff_diff/had_pretests.py, diff_diff/diagnostic_report.py]
920+
notes: "Phase 2 Diagnostic marker base on the RESULT roster (spec section 3.5): BaconDecompositionResults, RDPlotResult, HonestDiDResults + SensitivityResults, PreTrendsPowerResults + PreTrendsPowerCurve, PowerResults + Simulation*Results, PlaceboTestResults, and the HAD pretest containers (QUGTestResults, StuteTestResults, YatchewTestResults, StuteJointResult, HADPretestReport), and DiagnosticReportResults (serialization pair moves/delegates from the builder onto the container). Flip to done requires the dedicated roster test (isinstance + summary/to_dataframe on every member; estimator results NOT marked). Raw-dict functions and TWFEWeightsResult are docs-family only (narrowed, spec section 3.5). introduced_in gates the 3.9 cut: the marker cannot be silently skipped."
921+
872922
# ---- Behavior policies (schema-tracked, spec-governed; no reality probe) -
873923
- id: M-080
874924
kind: behavior

docs/v4-design.md

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ Derived from `diff_diff.__all__` at v3.8.0. 24 estimator classes -> 21.
9191
| ChangesInChanges | Keep + absorbs QDiD via `method=` [M-015] |
9292
| QDiD | Removed 4.0 [M-015] |
9393
| SyntheticDiD, SyntheticControl, TROP, ContinuousDiD, HeterogeneousAdoptionDiD, RegressionDiscontinuity, SpilloverDiD, ChaisemartinDHaultfoeuille | Keep |
94-
| BaconDecomposition | Keep (documented as a diagnostic, not an estimator, in the API docs and alias-table grouping) |
94+
| BaconDecomposition | Keep - moves into the diagnostic family (section 3.5) |
9595

9696
### 3.2 Final alias table
9797

@@ -148,6 +148,72 @@ signatures. Lifecycle facts live in the cited rows.
148148
[M-045]..[M-047] - fully redundant with `vcov_type`, and its default even
149149
differed across estimators (True/True/False).
150150

151+
### 3.5 The diagnostic family
152+
153+
diff-diff 4.0 formalizes a THIRD object kind alongside estimators and
154+
results: **diagnostics** (locked with the maintainer 2026-07-19). Exactly
155+
ONE bit is load-bearing: an estimator's result carries a causal-effect
156+
inference row (the section 5 quintet); a diagnostic's result does NOT -
157+
it assesses a design, an identifying assumption, or robustness (a
158+
decomposition table, binned plot data, a pre-trends p-value, sensitivity
159+
bounds, a power curve).
160+
161+
**Membership = the library's EXISTING "Diagnostics & Sensitivity" docs
162+
family, formalized** - not a new taxonomy: BaconDecomposition, RDPlot,
163+
parallel-trends testing, the placebo suite, HonestDiD, PreTrendsPower,
164+
PowerAnalysis, `twowayfeweights`, the HAD pretests, and the upcoming
165+
CJM 2020 density test (born into the family). (A narrower classes-only
166+
tier with separate categories for HonestDiD/PowerAnalysis was drafted and
167+
REJECTED 2026-07-19: the boundary needed adjudication, and the docs
168+
family already draws the right line.)
169+
170+
**Mechanics - the marker lives on RESULT containers.** Consumers hold
171+
results, not entry points, so the `Diagnostic` marker base (shipping
172+
ADDITIVELY in Phase 2 [M-091]) attaches to the class-backed diagnostic
173+
RESULT types: `BaconDecompositionResults`, `RDPlotResult`,
174+
`HonestDiDResults` + `SensitivityResults`, `PreTrendsPowerResults` +
175+
`PreTrendsPowerCurve`, `PowerResults` + the `Simulation*Results` family,
176+
`PlaceboTestResults`, the HAD pretest containers (`QUGTestResults`,
177+
`StuteTestResults`, `YatchewTestResults`, `StuteJointResult`,
178+
`HADPretestReport` - all already exposing the serialization pair), and
179+
`DiagnosticReportResults` (whose `summary()`/`to_dataframe()` live on the
180+
BUILDER today - Phase 2 moves or delegates them onto the container to meet
181+
the contract; BusinessReport exports no result container, so nothing to
182+
mark there) - plus every future diagnostic's result type (the density
183+
test's first). Entry-point classes are NOT marked (one
184+
marker, one meaning). Contract: marked results expose `summary()` /
185+
`to_dataframe()` and are exempt from the section 5 quintet BY TYPE
186+
(isinstance-checkable, so BusinessReport / DiagnosticReport /
187+
practitioner routing stop special-casing by result-class name). The
188+
family's entry points keep their existing shapes - data-in (Bacon,
189+
RDPlot) and results-in (HonestDiD, PreTrendsPower) both belong; the
190+
marker does not constrain them. NARROWING: function-shaped members that
191+
return raw dicts (`check_parallel_trends` and variants,
192+
`equivalence_test_trends`) and `TWFEWeightsResult` (no
193+
`summary()`/`to_dataframe()` today) participate in the DOCS family only,
194+
not the type contract; upgrading them to marked containers is optional
195+
Phase 2 follow-up work, not part of this contract. The Phase 2 gate
196+
[M-091] requires a dedicated roster test: every enumerated result type is
197+
`isinstance(result, Diagnostic)` and exposes the serialization pair, and
198+
representative ESTIMATOR results are NOT. Import paths do NOT move - the
199+
flat top-level namespace is kept (rejected: `diff_diff.diagnostics.*`
200+
moves). Zero new classes beyond the single marker.
201+
202+
**4.0 reorganization [M-090].** At 4.0 the public surface claims split
203+
into "estimators + diagnostics": Bacon is re-homed OUT of the API
204+
estimator roster (where it is misfiled today); RDPlot - already
205+
documented under Diagnostics & Sensitivity - is consolidated under the
206+
unified family grouping; README catalog, llms.txt, API reference and
207+
alias-table groupings, and estimator-count claims on JOSS-adjacent
208+
surfaces all split accordingly. Tracked as a behavior row so the cut
209+
sweep cannot miss it.
210+
211+
**RDPlot contract compliance [M-088] [M-089].** `RDPlot.fit()` shipped
212+
with `outcome_col` / `running_col` after the ledger froze - the section 8
213+
missed-rename clause applies, and the params migrate to bare `outcome` /
214+
`running` on the standard 3.9-shim / 4.0-removal path (matching
215+
RegressionDiscontinuity's [M-040] [M-041]).
216+
151217
## 4. The three merges
152218

153219
### 4.1 TwoWayFixedEffects absorbs MultiPeriodDiD [M-010]
@@ -272,9 +338,11 @@ removed [M-015] [M-061].
272338

273339
**Canonical quintet.** `att`, `se`, `t_stat`, `p_value`, `conf_int` - bound to
274340
ONE coherent inference row (locked library principle: uniform names never
275-
carry altered meaning). As of v3.8.0 every results class already exposes the
276-
full quintet as properties (verified 2026-07-18), so 3.9 needs no additive
277-
property work. At 4.0 the STORAGE flips [M-050]..[M-058]: the canonical names
341+
carry altered meaning). The quintet contract applies to ESTIMATOR results;
342+
diagnostic-family results (section 3.5) are exempt BY TYPE - they expose
343+
`summary()` / `to_dataframe()` but no inference row. As of v3.8.0 every
344+
ESTIMATOR result class already exposes the full quintet as properties
345+
(verified 2026-07-18), so 3.9 needs no additive property work. At 4.0 the STORAGE flips [M-050]..[M-058]: the canonical names
278346
become the native dataclass fields; `overall_att` (and sibling `overall_*`
279347
inference fields, plus ContinuousDiD's deviant `overall_att_*` family
280348
[M-058]) become FutureWarning properties, removed 5.0. MultiPeriodDiD's
@@ -409,10 +477,10 @@ above; anything only one PR cares about stays in that PR's plan.**
409477
| Phase | Ships in | PRs (each: dedicated shim/removal tests + matrix flips + CHANGELOG naming flipped row ids) |
410478
|---|---|---|
411479
| 1 (this PR) | - | Spec + matrix + enforcement test + support edits |
412-
| 2: contract foundations | 3.9 | (a) results base + unified event-study representation + to_dict completion; (b) `aggregate()` + fit(aggregate=) shims [M-020..M-027]; (c) param renames [M-030..M-047] + BaseEstimator mixin + ContinuousDiD covariates move; (d) alias introductions [M-062] [M-063] + wrapper deprecations [M-070..M-077] + n_bootstrap docs [M-081] |
480+
| 2: contract foundations | 3.9 | (a) results base + unified event-study representation + to_dict completion + the Diagnostic marker base on the diagnostic result roster [M-091] (section 3.5); (b) `aggregate()` + fit(aggregate=) shims [M-020..M-027]; (c) param renames [M-030..M-047] + BaseEstimator mixin + ContinuousDiD covariates move; (d) alias introductions [M-062] [M-063] + wrapper deprecations [M-070..M-077] + n_bootstrap docs [M-081] |
413481
| 3: merges | 3.9 | (a) TWFE event-study mode [M-010] + EventStudy warn [M-060] (gates: section 4.1's equivalence/divergence/pooled-parity test triple); (b) TripleDifference facade [M-013]; (c) CiC method= [M-015] |
414482
| 4: release + soak | 3.9 cut | Migration guide written (skeleton: section 10); maintainer cuts 3.9; maint/3.8 rule active |
415-
| 5: enforcement | 4.0 | Removals [M-010..M-016, M-030..M-047 old names, M-060, M-061, M-070..M-077, M-001..M-003]; storage flips [M-050..M-058]; default policies [M-004..M-006, M-080]; warning retirement [M-007]; fastpath go/no-go [M-008]; docs/llms.txt/README refresh |
483+
| 5: enforcement | 4.0 | Removals [M-010..M-016, M-030..M-047 old names, M-060, M-061, M-070..M-077, M-001..M-003]; storage flips [M-050..M-058]; default policies [M-004..M-006, M-080]; warning retirement [M-007]; fastpath go/no-go [M-008]; diagnostic-family docs/roster reorganization [M-090]; docs/llms.txt/README refresh |
416484
| 6: front door | 4.1 | `event_study(data, outcome, unit, time, first_treat, estimator=...)` comparison entry point over the staggered family (sketch only; specified in its own plan) |
417485

418486
**4.0-cut checklist (final item):** the due-row sweep is AUTOMATED -
@@ -505,8 +573,9 @@ forever - a removed symbol resurrecting is a test failure.
505573
`removed_in` (symbol/alias rows), flip version (`deprecated_in` on
506574
default-flip/warning-retirement/behavior rows), or `decision_due`
507575
(env-default rows), the status must be terminal; a due `introduced_in`
508-
means the row may no longer be `planned` (the new surface must have
509-
shipped - this is what gates introduce-only aliases); symbol rows with a
576+
means the row may no longer be `planned` OR `evaluate` (the new surface
577+
must have shipped; evaluate cannot satisfy an introduction - this is what
578+
gates introduce-only aliases and the Phase 2 marker); symbol rows with a
510579
due `deprecated_in` and a declared `warning` may no longer be `planned`.
511580
The gate is two-sided: an EARLY-removal guard fails any row that goes
512581
terminal while its scheduled version is still in the future (the shim
@@ -527,7 +596,7 @@ forever - a removed symbol resurrecting is a test failure.
527596
the same object as its target, so the deprecation warning rides the parent
528597
class row (schema-enforced). Top-level `diff_diff:Name` class/function rows
529598
and alias rows also assert `__all__` membership consistent with their
530-
status (stale `import *` entries fail). The initial 71 row ids are a
599+
status (stale `import *` entries fail). The initial 75 row ids are a
531600
committed snapshot in the enforcement test: ids are never deleted or
532601
reused, and the test fails if any snapshot id disappears.
533602

tests/test_v4_matrix.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,14 @@
109109
_FIELD_RE = re.compile(r"^ ([a-z_]+):\s*(.*?)\s*$")
110110
_MD_TOKEN_RE = re.compile(r"\[(M-\d{3})\]")
111111

112-
# Row-count floor: exactly the 71 rows shipped at Phase 1. Ids are never reused and terminal
112+
# Row-count floor: exactly the 75 rows shipped by the Phase 1 spec (incl. the diagnostic-family amendment). Ids are never reused and terminal
113113
# rows are never deleted, so the ledger only grows - raise the floor when rows are added; a
114114
# lower parse count means scanner/format drift or an illegal row deletion.
115-
ROW_COUNT_FLOOR = 71
115+
ROW_COUNT_FLOOR = 75
116116

117117
# Committed snapshot of the Phase 1 id set ("ids are never deleted or reused" contract - a
118118
# delete-one-add-one edit keeps the count above the floor but trips this). Extend, never edit.
119-
_INITIAL_ID_RANGES = [(1, 8), (10, 16), (20, 27), (30, 47), (50, 58), (60, 64), (70, 77), (80, 87)]
119+
_INITIAL_ID_RANGES = [(1, 8), (10, 16), (20, 27), (30, 47), (50, 58), (60, 64), (70, 77), (80, 91)]
120120
EXPECTED_INITIAL_IDS = frozenset(
121121
f"M-{n:03d}" for lo, hi in _INITIAL_ID_RANGES for n in range(lo, hi + 1)
122122
)
@@ -467,10 +467,18 @@ def collect_due_problems(rows, current):
467467
"(record the go/no-go either way)"
468468
)
469469
introduced_in = row.get("introduced_in")
470-
if introduced_in and current >= _version_tuple(introduced_in) and status == "planned":
470+
if (
471+
introduced_in
472+
and current >= _version_tuple(introduced_in)
473+
and status
474+
in (
475+
"planned",
476+
"evaluate",
477+
)
478+
):
471479
problems.append(
472-
f"{rid}: introduced_in {introduced_in} is due but status is still 'planned' "
473-
"(new surface not shipped?)"
480+
f"{rid}: introduced_in {introduced_in} is due but status is '{status}' - the "
481+
"new surface must have shipped (evaluate cannot satisfy an introduction)"
474482
)
475483
if (
476484
kind in LIFECYCLE_KINDS
@@ -505,7 +513,7 @@ def test_initial_ids_never_deleted():
505513
ROW_COUNT_FLOOR alone would let a delete-one-add-one edit pass; this snapshot cannot."""
506514
missing = sorted(EXPECTED_INITIAL_IDS - set(_ROW_IDS))
507515
assert not missing, f"ledger rows deleted (ids are permanent): {missing}"
508-
assert len(EXPECTED_INITIAL_IDS) == 71
516+
assert len(EXPECTED_INITIAL_IDS) == 75
509517

510518

511519
def test_version_tuple_pads_to_three_components():
@@ -589,6 +597,23 @@ def test_all_membership_helper_semantics():
589597
"removed_in 4.0 is due",
590598
(3, 9, 0),
591599
),
600+
# introduced_in cannot be dodged via 'evaluate' (behavior-row bypass regression)
601+
(
602+
{
603+
"id": "M-907",
604+
"kind": "behavior",
605+
"old": "diff_diff:BaconDecompositionResults",
606+
"new": None,
607+
"introduced_in": "3.9",
608+
"deprecated_in": None,
609+
"removed_in": None,
610+
"status": "evaluate",
611+
"phase": 2,
612+
},
613+
(3, 9, 0),
614+
"evaluate cannot satisfy an introduction",
615+
(3, 8, 0),
616+
),
592617
# overdue param-value removal: value migrations are due-gated like symbol rows
593618
(
594619
{
@@ -626,6 +651,7 @@ def test_all_membership_helper_semantics():
626651
"overdue-introduce-only-alias",
627652
"overdue-env-default-decision",
628653
"overdue-removal",
654+
"introduced-in-evaluate-bypass",
629655
"overdue-param-value-removal",
630656
"early-removal-before-schedule",
631657
],

0 commit comments

Comments
 (0)