Conversation
…t-study survey composition + sup-t bootstrap Closes the two Phase 4.5 A NotImplementedError gates on design='mass_point' + weights/survey and aggregate='event_study' + weights/survey. Weighted 2SLS in _fit_mass_point_2sls follows the Wooldridge 2010 Ch. 12 pweight convention (w² in HC1 meat, w·u in CR1 cluster score, weighted bread Z'WX). HC1 and CR1 match estimatr::iv_robust bit-exactly at atol=1e-10 (new cross-language golden). Per-unit IF on β̂-scale scales so compute_survey_if_variance(psi, trivial) ≈ V_HC1 at atol=1e-10 (PR #359 convention applied uniformly). Event-study path threads weights + survey through the per-horizon loop, composing Binder-TSL variance per horizon and populating survey_metadata + variance_formula + effective_dose_mean (previously hardcoded None). New _sup_t_multiplier_bootstrap helper reuses generate_survey_multiplier_weights_batch / generate_bootstrap_weights_batch from diff_diff.bootstrap_utils — no custom Rademacher draws, no (1/n) prefactor. At H=1 reduces to Φ⁻¹(1-α/2) ≈ 1.96 (reduction-locked). New __init__ kwargs: n_bootstrap=999, seed=None. New fit() kwarg: cband=True. HeterogeneousAdoptionDiDEventStudyResults gains survey_metadata + variance_formula + effective_dose_mean + cband_* fields, surfaced through to_dict / to_dataframe / summary / __repr__. Unweighted event-study output (att/se) bit-exactly preserved; cband disabled on the unweighted path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Overall Assessment ⛔ Blocker Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
Path to Approval
|
R1 P0 (methodology): cband_low/high now NaN-gated for horizons with se <= 0 or non-finite, matching the safe_inference contract for pointwise output. Previously a horizon with se=0 produced a finite band equal to the point estimate, overstating precision. R1 P1 (methodology): sup-t multiplier bootstrap now applies stratum-centered + small-sample-corrected PSU aggregation before the matmul, so Var_xi(xi @ Psi_psu_scaled) reproduces the analytical Binder-TSL variance term-for-term (V = sum_h (1-f_h)(n_h/(n_h-1)) sum_j (psi_hj - psi_h_bar)^2). Previously the bootstrap omitted centering and the small-sample correction; under stratified designs the critical value diverged from the analytical target. Verified at H=1 G=400 n_strata=4: bootstrap q=1.985 matches Phi^-1(0.975)=1.960. R1 P1 (code quality): event-study weights= filtering now translates data_filtered.index LABELS to POSITIONAL offsets via data.index.get_indexer, so non-RangeIndex inputs work correctly. Previous code treated index labels as positions and silently broke on custom int / string indices. Raises a clear ValueError when index alignment fails (duplicate or malformed labels). R1 P3 (docs): HeterogeneousAdoptionDiDEventStudyResults docstring updated to describe Phase 4.5 B weighted/survey support + new cband_* fields + variance_formula / effective_dose_mean; fit() docstring updated to reflect the Phase 4.5 B dispatch matrix and document the new cband kwarg. Regression tests: - test_zero_se_horizon_nan_gates_cband locks the P0 NaN gate - test_weights_nonrange_index_aligned_positionally locks the P1 row-order contract across RangeIndex vs custom int indices - Existing TestSupTReducesToNormalAtH1 covers the stratum-centered path via the unit-PSU reduction case Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall AssessmentExecutive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
Path to Approval
|
R2 P1 (methodology): reject SurveyDesign(lonely_psu='adjust') with singleton strata in _sup_t_multiplier_bootstrap. The bootstrap helper pools singletons into a pseudo-stratum with NONZERO multipliers, but compute_survey_if_variance centers singleton PSU scores around the global mean — without the matching pseudo-stratum centering transform in the bootstrap, the simultaneous band target diverges from the analytical Binder-TSL variance. Clear NotImplementedError points users to lonely_psu='remove' (matches the 'remove' analytical target) or cband=False (skips bootstrap). 'remove' / 'certainty' continue to work unchanged. Deferred transform tracked for follow-up. R2 P1 (code quality): reject cluster= + weights/survey= on design='mass_point' on both static and event-study paths. The weighted path composes Binder-TSL variance via compute_survey_if_variance which was silently overriding the CR1 sandwich while result metadata still advertised vcov_type='cr1' and cluster_name=<col>. Clean NotImplementedError with a pointer to the two supported combinations: cluster= alone (unweighted CR1) or survey/weights alone (Binder-TSL). Combined cluster-robust survey inference requires a derivation not yet in scope. Regression tests (+4): - test_mass_point_survey_plus_cluster_rejected_static - test_mass_point_survey_plus_cluster_rejected_event_study - test_lonely_psu_adjust_with_singletons_rejected_on_cband - test_stratified_h1_sup_t_matches_analytical (H=1 quantile lock under n_strata=4, psu_per_unit: q=1.985, matches Phi^-1(0.975)) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall AssessmentExecutive Summary
MethodologyAffected methods: Wooldridge-style weighted 2SLS on the HAD mass-point path, Binder-TSL survey composition, and the new sup-t multiplier bootstrap.
Code QualityNo separate findings beyond the methodology issues above. PerformanceNo findings. MaintainabilityNo separate findings beyond the methodology issues above. Tech Debt
SecurityNo findings. Documentation/Tests
Path to Approval
Review based on diff inspection; I did not run the test suite in this environment. |
R3 P1 (methodology — trivial-survey sup-t): _sup_t_multiplier_bootstrap now treats ANY non-None resolved_survey as survey-aware bootstrap input, including the trivial SurveyDesign(weights=...) case (no explicit strata / PSU / FPC). Previously the gate required explicit strata / psu / fpc, so the trivial survey path fell through to raw unit-level Rademacher — Var(xi @ Psi) = Σ psi², NOT the centered (n/(n-1))·Σ(ψ−ψ̄)² target that compute_survey_if_variance uses for the analytical SE. The existing "no strata → single implicit stratum" branch in _sup_t_multiplier_bootstrap now handles the trivial case end-to-end. H=1 trivial-survey regression locks q ≈ 1.96. R3 P1 (methodology — classical weighted mass-point): reject vcov_type='classical' on mass-point whenever the IF matrix is used (static survey= path; event-study survey= OR weights= shortcut with cband=True). The IF is HC1-scaled (sqrt((n-1)/(n-k)) factor in _fit_mass_point_2sls targets compute_survey_if_variance(psi, trivial) ≈ V_HC1[1,1]); mixing it with a classical analytical SE either through the survey Binder-TSL override or the sup-t bootstrap normalization would silently report a V_HC1-targeted SE under a classical vcov_type label. Rejection message points users to hc1. weights= shortcut + cband=False + classical continues to work unchanged (no IF consumption; analytical classical sandwich returned as-is). Regression tests (+4): - test_trivial_survey_h1_sup_t_matches_analytical: H=1 trivial SurveyDesign reduces to Normal quantile, confirms stratum-demean + sqrt(n/(n-1)) correction fires on the trivial case. - test_mass_point_classical_survey_rejected_static - test_mass_point_classical_event_study_with_cband_rejected - test_mass_point_classical_event_study_cband_false_accepts (complement: confirms classical+weights+cband=False still works, no IF consumption) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall AssessmentExecutive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
Path to Approval
Review based on diff inspection only; I could not run the test suite here because the environment is missing project dependencies ( |
R4 P1 (cluster guard overreach): narrow the mass-point cluster+weighted rejection. Previously rejected ANY cluster= + weighted fit, which blocked the valid weights= shortcut + cluster= weighted-CR1 pweight sandwich path (parity-tested vs estimatr::iv_robust se_type='stata'). Narrowed to: - Static path: reject only cluster= + survey= (Binder-TSL override would silently overwrite CR1 while metadata says vcov_type='cr1'). - Event-study path: reject cluster= + survey= OR cluster= + weights= + cband=True (sup-t bootstrap mixes HC1-scale perturbations with CR1 analytical SE). cluster= + weights= + cband=False is allowed (no IF consumption). R4 P1 (lonely_psu="adjust" deviation): added REGISTRY.md Note explicitly documenting the HAD-specific unsupported combo (weighted event-study + cband=True + lonely_psu="adjust" with singleton strata). The shared survey bootstrap helper supports pooled singletons but compute_survey_if_variance centers them at the global mean; matching the two requires a pseudo-stratum centering transform not yet derived. Other survey-bootstrap consumers (CS, dCDH, SDID) retain full "adjust" support. R4 P2 (n_units reporting): weighted event-study now reports the POSITIVE-WEIGHT contributing sample size in n_units and n_obs_per_horizon, matching the static-path n_obs contract. Full- design size still surfaces through survey_metadata.n_psu / effective_n / etc. R4 P2 (docstring drift): HeterogeneousAdoptionDiDEventStudyResults.se docstring now documents the three regimes separately — unweighted analytical sandwich, weights= shortcut analytical (HC1 / classical / CR1), and survey= Binder-TSL. Previously said "Binder-TSL for all weighted fits" which was wrong for the weights= shortcut. Regression tests (+5): - test_mass_point_weights_plus_cluster_shortcut_allowed - test_mass_point_weights_plus_cluster_event_study_cband_false_allowed - test_mass_point_weights_plus_cluster_event_study_cband_true_rejected - test_event_study_zero_weight_units_excluded_from_n_units - Updated test_mass_point_survey_plus_cluster_rejected_static to use survey= instead of weights= (matches narrowed guard) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall AssessmentExecutive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
Path to Approval
|
R5 P1 (classical-guard bypass): the classical-on-IF-consumption rejection was only checking the RAW vcov_type kwarg, which missed the default mapping (vcov_type=None, robust=False → classical). Re-ordered both mass-point branches (static + event-study) to resolve vcov_requested first, then gate the rejection on the resolved value. Default-path users who pass HeterogeneousAdoptionDiD(design='mass_point').fit(..., survey=...) without an explicit vcov_type now hit the same clear NotImplementedError as the explicit vcov_type='classical' path. R5 P2 (regression coverage): added three tests covering the default-path cases that slipped through the previous guard: - test_mass_point_default_vcov_survey_rejected_static: default vcov_type=None + survey= on mass-point → NotImplementedError - test_mass_point_default_vcov_event_study_cband_rejected: default vcov_type=None + weights= + cband=True on event-study → rejects - test_mass_point_default_vcov_robust_true_survey_allowed: complement confirms robust=True (default mapping → hc1) survey= is allowed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment ✅ Looks good No unmitigated P0/P1 findings. One P2 performance item and one P3 test-gap note remain. Executive Summary
Methodology
Code Quality
Performance
Maintainability
Tech Debt
Security
Documentation/Tests
|
R6 P2 (event-study opt-out overhead): cband=False on the weighted
event-study path no longer allocates the stacked (G, H) IF matrix or
forces per-horizon IF return on the weights= shortcut. Split two
flags internally:
needs_per_horizon_if = survey= path OR (weights= AND cband=True)
needs_stacked_if_matrix = weights= AND cband=True (alias for
weighted_es AND cband)
- Psi allocation gated on needs_stacked_if_matrix.
- _fit_continuous force_return_influence gated on
(needs_stacked_if_matrix AND resolved_survey_unit_full is None) —
under survey= path, _fit_continuous returns the IF anyway via its
resolved_survey_unit gate, so no extra cost.
- _fit_mass_point_2sls return_influence gated on
needs_per_horizon_if — survey= path needs the per-horizon IF for
the Binder-TSL override regardless of cband.
Net effect: cband=False + weights= shortcut + weighted_es skips the
O(GH) Psi allocation and the per-horizon IF work entirely. cband=True
paths and survey= paths unchanged.
R6 P3 (event-study survey= integration coverage): added two end-to-end
integration tests for the previously-unguarded positive-path
estimator-level survey= + aggregate='event_study' dispatch:
- test_survey_event_study_continuous_end_to_end: continuous_at_zero +
SurveyDesign(strata='stratum') — asserts variance_formula=
'survey_binder_tsl', survey_metadata.df_survey=G-n_strata, cband_*
populated, PSU dispatch through _aggregate_unit_resolved_survey.
- test_survey_event_study_mass_point_end_to_end: mass_point +
SurveyDesign(strata=...) — asserts variance_formula=
'survey_binder_tsl_2sls' and that the 2SLS IF flows through
per-horizon Binder-TSL + sup-t bootstrap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment ⛔ Blocker One unmitigated P0 remains: the new mass-point weighted event-study shortcut miscalibrates the sup-t band on Executive Summary
Methodology Affected method:
Code Quality No findings. Performance No findings. The prior Maintainability No findings. Tech Debt No separate findings. The new follow-ups in Security No findings. Documentation/Tests
Path to Approval
|
R7 P0 (sup-t under-scaling on weights= shortcut): the per-unit IF returned by _fit_continuous / _fit_mass_point_2sls is HC1-scaled per the PR #359 convention — compute_survey_if_variance(psi, trivial_resolved) ≈ V_HC1. Routing the weights= shortcut through the unit-level ``resolved_survey=None`` branch of _sup_t_multiplier_bootstrap normalized against raw sum(psi²) = ((n-1)/n) · V_HC1, producing silently too-narrow simultaneous bands. Fix: when the weighted event-study + cband=True path runs, always route the sup-t bootstrap through a ResolvedSurveyDesign. On the weights= shortcut (no user-supplied survey), construct a synthetic trivial resolved (pweight, no strata/psu/fpc, lonely_psu='remove') so the centered + sqrt(n/(n-1))-corrected survey-aware branch fires. The no-strata/no-PSU path inside that branch falls through to the "single implicit stratum — demean across all PSUs, scale by sqrt(n_psu/(n_psu-1))" block, which gives Var_xi(xi @ Psi_psu) ≈ V_HC1 as the IF scale convention requires. Net effect: weights= shortcut and survey=SurveyDesign(weights=...) now target the SAME variance family in the bootstrap (~atol=0.05 between the two quantiles at matching seeds, bounded by the bc_fit.se_robust vs Binder-TSL per-horizon SE convergence tolerance from PR #359). Previously the shortcut was under-scaled by sqrt(n/(n-1)) relative to the analytical HC1 target. Regression tests (+2): - test_weights_shortcut_mass_point_h1_cband_matches_normal: helper- level H=1 lock with mass-point HC1-scaled IF + synthetic trivial resolved. q → Phi^-1(0.975) ≈ 1.96 at atol=0.15 (MC noise at B=5000). The pre-fix under-scaling would have produced q ≈ 1.94 (systematic drift outside MC noise). - test_weights_shortcut_cband_matches_trivial_survey: weights= shortcut and survey=SurveyDesign(weights='w') event-study cband quantiles agree within atol=0.05 on the same DGP / seed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
/ai-review |
|
🔁 AI review rerun (requested by @igerber) Head SHA: Overall Assessment ✅ Looks good Executive Summary
Methodology No unmitigated P0/P1 findings. The changed estimator logic now matches the documented HC1/Binder targets materially better than in the previous review state. Code Quality No findings. Performance No findings. Maintainability No findings. Tech Debt No findings. The follow-up work on analytical cross-horizon covariance and unweighted sup-t is properly tracked in Security No findings. Documentation/Tests
|
R8 P3 (doc-consistency drift): prose in _fit_event_study docstring, REGISTRY sup-t bullet, and CHANGELOG entry still described the pre-R7-fix weighted-path behavior (Binder-TSL for all weighted fits; raw unit-level shortcut bootstrap). Updated all three to match the actual implementation: - _fit_event_study docstring: distinguishes survey= path (Binder-TSL) from weights= shortcut (analytical CCT-2014 / 2SLS pweight sandwich), documents trivial-survey bootstrap routing for cband. - REGISTRY.md: new "weights= shortcut ↔ bootstrap routing" bullet under the sup-t section explaining the synthetic trivial ResolvedSurveyDesign construction and why (centered + sqrt(n/(n-1))-corrected branch targets V_HC1, raw unit-level would give ((n-1)/n) · V_HC1 under-scaling). - CHANGELOG.md: per-horizon variance on weights= shortcut clarified as analytical (not Binder-TSL); sup-t routing through trivial resolved documented. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the Phase 4.5 C0 promise (PR #367 commit 29f8b12). Linearity- family pretests now accept survey=/weights= keyword-only kwargs: - stute_test, yatchew_hr_test, stute_joint_pretest, joint_pretrends_test, joint_homogeneity_test, did_had_pretest_workflow. Stute family: PSU-level Mammen multiplier bootstrap via generate_survey_multiplier_weights_batch. Each replicate draws (B, n_psu) Mammen multipliers, broadcast to per-obs perturbation eta_obs[g] = eta_psu[psu(g)], weighted OLS refit, weighted CvM via new _cvm_statistic_weighted helper. Joint Stute SHARES the multiplier matrix across horizons within each replicate, preserving both vector-valued empirical-process unit-level dependence (Delgado 1993; Escanciano 2006) AND PSU clustering (Krieger-Pfeffermann 1997). NOT Rao-Wu rescaling -- multiplier bootstrap is a different mechanism. Yatchew: closed-form weighted OLS + pweight-sandwich variance components (no bootstrap): sigma2_lin = sum(w * eps^2) / sum(w) sigma2_diff = sum(w_avg * diff^2) / (2 * sum(w)) [Reviewer CRITICAL #2] sigma4_W = sum(w_avg * eps_g^2 * eps_{g-1}^2) / sum(w_avg) T_hr = sqrt(sum(w)) * (sigma2_lin - sigma2_diff) / sigma2_W where w_avg_g = (w_g + w_{g-1}) / 2 (Krieger-Pfeffermann 1997 Section 3). All three components reduce bit-exactly to existing unweighted formulas at w=ones(G); locked at atol=1e-14 by direct helper test. Workflow under survey/weights: skips the QUG step with UserWarning (per C0 deferral), sets qug=None on the report, dispatches the linearity family with survey-aware mechanism, appends "linearity-conditional verdict; QUG-under-survey deferred per Phase 4.5 C0" suffix to the verdict. all_pass drops the QUG-conclusiveness gate (one less precondition). HADPretestReport.qug retyped from QUGTestResults to Optional[QUGTestResults]; summary/to_dict/to_dataframe updated to None-tolerant rendering. Pweight shortcut routing: weights= passes through a synthetic trivial ResolvedSurveyDesign (new survey._make_trivial_resolved helper) so the same kernel handles both entry paths -- mirrors PR #363's R7 fix pattern on HAD sup-t. Replicate-weight survey designs (BRR/Fay/JK1/JKn/SDR) raise NotImplementedError at every entry point (defense in depth, reciprocal- guard discipline). The per-replicate weight-ratio rescaling for the OLS-on-residuals refit step is not covered by the multiplier-bootstrap composition; deferred to a parallel follow-up. Per-row weights= / survey=col aggregated to per-unit via existing HAD helpers (_aggregate_unit_weights, _aggregate_unit_resolved_survey; constant-within-unit invariant enforced) through new _resolve_pretest_unit_weights helper. Strictly-positive weights required on Yatchew (the adjacent-difference variance is undefined under contiguous-zero blocks). Stability invariants preserved: - Unweighted code paths bit-exact pre-PR (the new survey/weights branch is a separate if arm; existing 138 pretest tests pass unchanged). - Yatchew weighted variance components reduce to unweighted at w=1 at atol=1e-14 (locked by TestYatchewHRTestSurvey). - HADPretestReport schema bit-exact on the unweighted path; qug=None triggers the new None-tolerant rendering only on the survey path. 20 new tests across TestHADPretestWorkflowSurveyGuards (revised from C0 rejection-only to C functional + 2 mutex/replicate-weight retained), TestStuteTestSurvey (7), TestYatchewHRTestSurvey (7), TestJointStuteSurvey (5). Full pretest suite: 158 tests pass. Patch-level addition (additive on stable surfaces). See docs/methodology/REGISTRY.md "QUG Null Test" -- Note (Phase 4.5 C) for the full methodology. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Closes the two Phase 4.5 A
NotImplementedErrorgates:design=\"mass_point\" + weights/surveyatdiff_diff/had.py:2807-2817aggregate=\"event_study\" + weights/surveyatdiff_diff/had.py:2376-2382Ships weighted 2SLS mass-point path, event-study survey composition for all three designs, and a multiplier-bootstrap sup-t simultaneous confidence band on the weighted event-study path.
_fit_mass_point_2slsfollows Wooldridge 2010 Ch. 12 pweight convention (w²in HC1 meat,w·uin CR1 cluster score, weighted breadZ'WX). HC1 and CR1 bit-exact withestimatr::iv_robust(..., weights=, clusters=)atatol=1e-10on 5 DGPs (new cross-language golden).compute_survey_if_variance(psi, trivial_resolved) ≈ V_HC1[1,1]atatol=1e-10(PR HAD Phase 4.5: survey support on continuous-dose paths #359 convention, applied uniformly across continuous + mass-point).weights_unit_full+resolved_survey_unit_full, composes Binder-TSL variance per horizon viacompute_survey_if_variance, populatessurvey_metadata+variance_formula+effective_dose_mean(previously hardcodedNone).generate_survey_multiplier_weights_batch/generate_bootstrap_weights_batchfromdiff_diff.bootstrap_utils(stratum centering, FPC, lonely-PSU). Perturbationsxi @ Psiwith NO(1/n)prefactor (matchesstaggered_bootstrap.py:373). AtH=1reduces toΦ⁻¹(1-α/2) ≈ 1.96(locked byTestSupTReducesToNormalAtH1).Public API
HeterogeneousAdoptionDiD.__init__gainsn_bootstrap: int = 999,seed: Optional[int] = None.HeterogeneousAdoptionDiD.fit()gainscband: bool = True(weighted event-study only)._fit_mass_point_2slssignature: newweights=+return_influence=kwargs; always returns 3-tuple(beta, se, psi).HeterogeneousAdoptionDiDEventStudyResultsgainsvariance_formula,effective_dose_mean,cband_low,cband_high,cband_crit_value,cband_method,cband_n_bootstrap(allNoneon unweighted fits).to_dict,to_dataframe,summary,__repr__.Stability invariants
TestHADSurvey) unchanged on static path.aweight,fweight, replicate) rejected withNotImplementedErroron both new paths (reciprocal-guard discipline).filter_infoidentical across unweighted / uniform-weighted / informatively-weighted fits (identification-theory, not sampling-domain).qug_test,stute_test,yatchew_hr_test, joint variants,did_had_pretest_workflow) still don't accept weights/survey — deferred to Phase 4.5 C / C0.Test plan
pytest tests/test_had.py tests/test_bias_corrected_lprobust.py tests/test_nprobust_port.py tests/test_estimatr_iv_robust_parity.py -v— all 380 pass, 2 skippedRscript benchmarks/R/generate_estimatr_iv_robust_golden.R— regenerates 5-DGP goldenTestHADSurveyunchanged)TestSupTReducesToNormalAtH1confirmsq ≈ 1.96atH=1,G=500,B=5000TestEstimatrIVRobustHC1Parity/TestEstimatrIVRobustCR1Parityconfirm cross-language bit-parity atatol=1e-10TestIFScaleInvariantconfirms PR HAD Phase 4.5: survey support on continuous-dose paths #359 convention holds uniformly for 2SLS IF🤖 Generated with Claude Code