Skip to content

M6 design amendment 3: the scored-run harness (§2.8, unblocks the gate_m6 run)#181

Merged
MaxGhenis merged 2 commits into
masterfrom
m6-harness-design
Jul 13, 2026
Merged

M6 design amendment 3: the scored-run harness (§2.8, unblocks the gate_m6 run)#181
MaxGhenis merged 2 commits into
masterfrom
m6-harness-design

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

What

Adds §2.8 — the scored-run harness to docs/design/m6_projection_engine.md (design amendment 3). Docs-only: it edits no gates.yaml cell, moves no threshold, builds no floor, and writes no test.

Why

gate_m6 candidate-1 was registered as a one-shot temporal-holdout run (#42 comment 4962640241) and then stopped before any scored phase — the designed pre-scoring stop, comment 4962773701, graded a registration error. The engine (PR #173, merged 95a3e05) is build-only: the per-slice native-panel builders exist only as test stubs, the drift-scoring composition is unbuilt, and pre-flight 1 had only run on synthetic ensembles. The stop names this design amendment as the unblock path (referee round → build → engine-referee-style review → merge → fresh registration).

§2.8 pins every modeling decision so the harness build lane implements with zero design choices (the §2.7.6 bar), candidate-blind (builders and scorer exercised on synthetic frames only until the registered run; no holdout truth read in the build).

The pins (§2.8.1–§2.8.9)

  • 2.8.1 the adjudicated core — the year-0 closed panel carries realized histories: the builders read realized ≤2015-interview marriage/household histories for the year-0 population; projected years are constructed from simulated state. Argued from decision 5 + §4.4 + the built disability_step reproduction template (assembly.py:257-304) + support.py's GATED_REALIZED/FORWARD modes. Forward/production path out of scope (decision-5 successor gate); pointer restated.
  • 2.8.2 the projected-slice→native-panel builders, per field — against the certified MaritalPanel (episodes→changepoints→backward-asof) and HouseholdCompositionPanel (observed-states→evolve) schemas as the contract: episode extension, changepoint appends, duration accounting, censor handling — each stated in the certified schema, R1 plumbing, whole-window core run once per draw (the disability_step cache pattern).
  • 2.8.3 the year-0 realized-2015-interview slice — mirrors build_anchor_frame / presence_by_wave and the floor's PSID reads exactly (SEED_WAVE=2015); F6 start-wave weight and household_id fixed; presence-conditioned symmetrically via prepare_evaluation_support.
  • 2.8.4 the M6 drift-scoring layer — reuses the floor cell functions verbatim (named: build_anchor_frame, presence_by_wave, *_slices, *_cells, _rate, _wquantile, _score, split_panel_by_person, derive_tolerance), with a byte-identity self-test if reimplemented; per gate seed → side-A half-split, K=20 rbar vs realized rate_a, |ln(rbar/rate_a)| against the frozen v3 floor (e931c886…), 4-of-5 seed conjunction read from the locked block; undefined-draw + regenerated-surface guards.
  • 2.8.5 pre-flight 1 — the candidate-9 re-certification margin on the ≤2014-refit real panel over the gate seeds (check_candidate9_recertification, ≥3σ), holdout-blind; failure = designed abort (one-shot not consumed).
  • 2.8.6 pre-flight 2 — certified sign-path verification (_gate_sign_draw draw_sign branch vs the test seam), synthetic probe, path recorded.
  • 2.8.7 the runner phases — refit | pre-flight×2 | project+score per seed | report-only | assemble; one-shot write_new + .env.json sidecar, stamping the registration-id + EARNINGS_SPEC_REGISTRATION.
  • 2.8.8 the fence, extended — no gates.yaml read beyond the block's protocol/cells; no holdout-informed choice; no realized post-boundary macro read on the scored path (the §2.7.6.3 leakage prohibition); forward-mode inputs stay rejected.
  • 2.8.9residual open decisions: none. Two mechanical build-lane alignments recorded (once-per-draw cached core; extract the floor cell functions to one shared module).

Also updates the revision header (→ revision 4 / amendment 3) and the §10 design-parameters block (adds scored_run_harness).

Verification

  • docs-only — no engine, floor, or test changed.
  • All 39 design-doc-bound tests pass unchanged: pytest tests/test_gate_m6_floors.py tests/test_gate_m6_derivations.py → 39 passed.
  • The §10 m6-design-parameters JSON block parses.

Draft pending the amendment referee round.

🤖 Generated with Claude Code

…e_m6 run)

Adds §2.8 to docs/design/m6_projection_engine.md — the reviewed design of the
scored-run harness whose absence stopped the registered gate_m6 candidate-1 run
before scoring (#42 comment 4962640241 registered it; comment 4962773701 is the
designed pre-scoring stop, graded a registration error, naming this amendment as
the unblock path). Docs-only: edits no gates.yaml cell, moves no threshold,
builds no floor, writes no test.

§2.8 pins, so the harness build lane implements with zero design choices
(the §2.7.6 bar), candidate-blind (builders and scorer exercised on synthetic
frames only until the registered run):

- 2.8.1 the adjudicated core — the year-0 closed panel carries REALIZED
  histories (decision 5 reproduction-mode semantics extended from the built
  disability_step to the marital/household builders); forward/production path
  out of scope (decision-5 successor gate), pointer restated.
- 2.8.2 the projected-slice→native-panel builders per field, against the
  certified MaritalPanel (episodes→changepoints→backward-asof) and
  HouseholdCompositionPanel (observed states→evolve) schemas: episode
  extension, changepoint appends, duration accounting, censor handling.
- 2.8.3 the year-0 realized-2015-interview slice, mirroring build_anchor_frame
  / presence_by_wave and the floor's PSID reads exactly (F6 weight fixed).
- 2.8.4 the drift-scoring layer reusing the floor cell functions verbatim
  (named), byte-identity self-test; gate-seed side-A split, K=20 rbar vs
  realized rate_a, |ln(rbar/rate_a)|, 4-of-5 conjunction read from the block.
- 2.8.5/2.8.6 the two real-data pre-flights (candidate-9 recert margin,
  designed-abort; certified sign-path verification).
- 2.8.7 the runner phases + one-shot write_new/sidecar with registration-id +
  EARNINGS_SPEC_REGISTRATION stamping.
- 2.8.8 the fence extended to the harness; 2.8.9 residual open decisions: none.

Updates the revision header (→ revision 4 / amendment 3) and the §10
design-parameters block (adds scored_run_harness). All 39 design-doc-bound
tests (test_gate_m6_floors, test_gate_m6_derivations) pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
social-security-model Ready Ready Preview, Comment Jul 13, 2026 11:13pm

Request Review

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Amendment-3 referee round (§2.8, the scored-run harness): MAJOR REVISION

One finding of the §2.7.6 class — a pin that contradicts the certified code on the gated surface — plus five zero-choices precision gaps. Everything else in §2.8 verified code-true, line-exact, and certification-consistent; the revision is scoped, not structural. Verified against 22e8af0 (worktree build, .[dev] venv, PSID staged): every cited line was opened, the floor/gate/engine machinery was read end-to-end, and the harness was mentally implemented phase by phase against loop.py/assembly.py/support.py/composition.py/steps.py/forward_earnings.py/refit.py, the v1/v3 floor scripts, and the locked gates.gate_m6 block.


MAJOR

1. The gated-earnings scored support is contradicted by certified step-1 mortality — the one gated family §2.8 leaves unresolved.
§2.8.3 pins presence sets "applied to projection, truth, and floor symmetrically by support.prepare_evaluation_support," which raises on any support mismatch (support.py:311-315, "symmetric presence-conditioning requires identical projection and truth person-period support"). §2.8.4 pins the projected side as a reduction of "each K-draw's projected panel" — the engine's frame slices (loop.py:136-143). But the certified loop's step 1 removes simulated deaths from every subsequent frame (apply_mortality, steps.py:108-130, "return only the period's survivors"), so the projected earnings person-periods at {2016, 2018} are a strict subset of the realized truth support (family_earnings_panel rows, earnings_frame, build_m6_holdout_floors.py:354-378) on essentially every draw — dozens of simulated-dead-but-realized-present persons per draw at 25–64 mortality over 1–4 projected years, per side-A half of the 29,792-person closed panel.

The amendment resolves exactly this interaction for the other three gated families — marital via the realized presence/death censor on a frame-independent panel (§2.8.2; the builder reads the §2.8.3 realized sources, not the live frame), disability via realized-support reproduction (assembly.py:257-304), household via realized person_waves support — and is silent for earnings, the only gated family computed from the live frame. As pinned, the build lane lands in one of two unadjudicated states:

  • (i) if the earnings frames route through the §2.8.3 guard, the run aborts on every draw (and note presence_by_wave's odd-wave keys cannot even key the even reference years — the earnings-side conditioning machinery is unspecified, see finding 6); or
  • (ii) if they do not, the run silently scores survivor-conditioned projected earnings against all-present realized truth — an unpinned support decision on 6 of the 11 gated cells, with a systematic direction (mortality–earnings correlation biases earn_p10.prime, earn_zero_rate.older et al.), and it functionally admits the mortality module — not_certified mortality drift, first margin — into the gated surface through survivorship.

Either branch falsifies §2.8.9's "residual open decisions: none." This is the analogous class to the §2.7.6 fit-range/gated-surface MAJOR: a scored-surface support definition the certified code contradicts. The ratified record points at the resolution — §4.7 calls the construction "the gate_m4 construction with the time axis substituted," and the gate_m4/reproduction precedent scores on realized support; §4.4's cancellation logic ("the same wave-presence selection sits on both the projected and the realized rate") requires it — i.e., the scored earnings person-periods should be the realized support, with the forward chain evaluated for realized-present persons regardless of simulated death. That is cleanly implementable without perturbing survivor bits (apply_earnings draws per person-keyed streams, steps.py:224-233, so evaluating dead persons' chains consumes no survivor entropy), and it keeps mortality fully report-only. But choosing it — versus disclosed survivor-conditioning — is an adjudication + pin, not build-lane mechanics. Amend §2.8.4 (and §2.8.3's presence bullet) to pin the gated-earnings support explicitly.

Required pins (zero-choices gaps; one sentence each)

2. The once-per-draw cached cores' stream address is unpinned. The certified cores are per-trajectory, one stream set per draw (verified — see the record below), but §2.8 never says which registry period the single cached run consumes: the first-invocation period-1 streams the loop passes (loop.py:234-237; assembly.py:213-215), or the period-0 deployment/compatibility address that reproduces the certified topology (rng.py:41-44; composition_rngs_from_registry(…, 0), composition.py:86-93 — the address the §2.8.5 internal reference itself uses, composition.py:643-653, n_periods=0). Both are candidate-blind and statistically valid; they differ bit-wise, and §4.9 pass verification is bit-exact. §2.8.2's household sentence — "consuming … the per-period HOUSEHOLD_COMPOSITION streams" — is additionally self-contradictory for a single whole-window call, which takes exactly one CompositionRngs (simulate_candidate9_injected, composition.py:532-539). Pin the address (and the marital main_rng/gap_rng and step-4 fertility stream with it).

3. The marital builder's person universe is under-pinned. The §2.8.2 table sources attrs "from the §2.8.3 year-0 slice" with holdout_ids = the full closed panel — but anchor persons absent from the marriage-history file (children; MH-uncovered adults) have no truth-side marital exposure at all (person_attributes/_valid_persons, transitions.py:239-277), while a builder that seeds them projects them as never_married at-risk person-years (the pd.isna entry path, marital.py:100-101) — asymmetric first_marriage denominators for the 16–17-at-2015 cohort entering band 18+ in-window. The entry-row cell's "identical machinery the truth side runs" implies the right universe; make it explicit: marital attrs = build_marital_panel(mh, deaths, anchor_weight).attrs ∩ anchor (which also keeps start_exposure ≥ birth_year + START_AGE and resolves the missing-entry-row case by construction).

4. "Realized ≤2015-interview histories" is wrong for the 2017/2019 openers. §2.8.1's headline and the §10 JSON core_adjudication pin the seed boundary at the 2015 interview; the 2017/2019 presence-conditioned openers (§2.8.3, correctly) seed at their anchor wave — their realized ≤2017/≤2019-interview state, which reads realized 2015–2018-dated events into the seed. That is the ratified conditioning (block f6_weight.start_wave; §4.4), not a leak — but the boundary statement should read "≤ the person's anchor interview (2015 for the bulk)" in §2.8.1 and the JSON, so the leakage fence is stated at the true per-person boundary.

5. The attrs.n_marriages pin is internally inconsistent (inert, but a pin should be exact). "Realized marriage count as of the seed wave … via person_attributes" — person_attributes yields MH18, the lifetime count as of last report (transitions.py:239-243; marriage.py:216), which includes post-boundary marriages; those differ. The field is provably unread by the core (order re-seeds to 1, marital.py:107,119) and overwritten in the output (_assemble_panel recomputes it from simulated episodes + residual, simulator.py:76-87, marital.py:262-272) — say which value the builder writes and that it is carried-inert, rather than pinning two different values in one cell.

6. Name the earnings-side support basis. §2.8.3's presence bullet covers flows only (presence_by_wave, START_OF_INTERVAL). The earnings truth support is the realized panel's own person-period row existence at {2014, 2016, 2018} (earnings_frame; "present, positive-weight, valid-earnings"), an EXACT_WAVE-class basis on reference years. One sentence naming it — folded into finding 1's resolution — closes the gap that lets a builder mis-apply the flow presence sets to even reference years.

Notes (disclosure-grade, no gate impact)

  • Fertility remains whole-window-recompute-per-period (apply_fertilitysimulate_fertility on the cached marital result each period with the period stream, steps.py:401-433): frame-materialized births are stitched across years. Inert on every gated cell (children are outside holdout_ids and the cell bands; births feed report-only entrants + the household roster) — worth one disclosure line or a third mechanical alignment.
  • The 2c-modifier view cannot move the gated marital cells: apply_first_marriage_modifier is (band, sex)-marginal-preserving by construction (§2.6 point 3b), and the scored reduction reads the raw panel events.

Verified sound (the record this round confirms)

  • §2.8.2 per-field pins are code-true, line-exact: seed mapping current_start = start_year − duration, dissolution_year = start_year − years_since, order re-seed (marital.py:104-119); loop over [start_exposure_year, censor_year] (marital.py:150-153); duration/years-since verbatim (marital.py:188,217); intact-at-censor end=NA (marital.py:243-249); kind_order dissolve-before-marry + allow_exact_matches=False (transitions.py:307-352,377-423); the simulated panel re-assembled through the certified _changepoints/_assign_state/_events_frame (simulator.py:67-118); no new changepoint kind. Household: the pinned seed states are exactly the consumed inputs (obs_parent/obs_multigen composition.py:240-241, obs_cohab :302, obs_skipgen :475-479), the evolve kernels read only wave-0 seeds (common.py:87-130), _add_transitions rebuild matches the drop/re-add (composition.py:584-593).
  • Censor symmetry (the P6-class check): SYMMETRIC. The pinned projected censor min(2022, person_attributes censor) equals the truth panel's min(mrr, death, 2023) everywhere inside the 2015–2021 scored/shock window; universe (anchor ∩ MH-valid, modulo finding 3), presence sets (keep_present's opening-wave rule = presence_by_wave), and F6 weights are the same objects on both sides; support.py:311-315 backstops the identity at runtime. Attrited-not-dead, absent-mid-window, and 2017/2019-opener cases all trace symmetric.
  • The adjudicated core's precedent is real and extends without new leakage: disability_step builds reproduction support from realized inputs.disability_panel filtered to [start_year+1, end_year] — not the projected frame — and caches one simulate_reproduction run (assembly.py:257-304); GATED_REALIZED requires / FORWARD structurally rejects realized inputs with the named structural_delta (support.py:236-264). Nothing post-boundary-dated enters any seed (backward-asof excludes boundary-year changepoints); every post-boundary read the builders make (censor, presence, anchor, F6) is the ratified §4.4/§4.7/block conditioning set — no addition.
  • Once-per-draw is certification-consistent; per-period recompute is not (the flagged §2.7.6-class hunt — it resolves in the amendment's favor): the certified candidate-16 is one whole-window trajectory per draw on one main + one gap generator ("preserve run_gate2_candidate16.py:826-1079 exactly", simulator.py:121-133; §3.1's K=20 = twenty whole-window runs on 5200+k), and certified candidate-9 is one whole-window evolve per draw at the frozen tag topology (composition.py:86-111,635-653). The built loop invokes the cores per period with per-period streams (loop.py:234-274) — with a real whole-window builder that stitches independent draws across years and breaks state-follows-events in the longitudinal marital cells, exactly as §2.8.2 argues. The cache is the certified shape; only its stream address needs the finding-2 pin.
  • Drift scoring: every named reuse target exists as named (build_anchor_frame :153-171, presence_by_wave :174-181, marital_tables :252-299 with the :259-261 machinery line-exact, mortality_slices, disability_pairs, earnings_frame, the four cell functions, MARITAL_AT_RISK, EARN_COHORTS, band_of, _rate, _wquantile, _score :603-621, harness.moments, split_panel_by_person, derive_tolerance); _score's metric branches are exactly the pinned log-ratio/abs-gap split; the byte-identity self-test is the right and sufficient fidelity pin for the floor-bound quantities (rate_a, tolerances), with extraction making identity by construction; gate seeds {0–4}, the 4-of-5 conjunction, the 11 cells, split_units, metrics/k, and tolerances are all read from the locked block (verified against gates.yaml gate_m6 views/scoring), and K=20 on 5200+k restates the ratified §3.1/§4.7–4.8 pins — invented nowhere; the dispersion/conformance guards match the prior scored-run artifact schema.
  • Pre-flights + fence: §2.8.5 is §2.6 requirement-4 verbatim — output channel moments over RECERTIFICATION_CHANNEL_SETS, ≥3σ with no tolerance floor, raise-on-fail naming the fuller re-ceremony (check_candidate9_recertification, composition.py:692-758), injected-vs-frozen-ft.simulate reference, holdout-blind; §2.8.6's two paths are exactly forward_earnings.py:815-819 (draw_sign) vs :820-826 (_target_models seam). The fence's new-read-path audit holds: the generator provably never reads the frame's realized nawi (explicit guard + FRAME_COLUMNS-only selection, forward_earnings.py:1059-1061), fit-time NAWI is ≤2014-validated (_precutoff_nawi :209-229; ols_log_nawi_2005_2014, refit.py:98), odd years carry forward (:1051-1053), and advance_age's realized-macro columns (steps.py:143-156) are scoped to the admissible non-scored/report-only paths — the sole realized-post-boundary influence reachable into a gated cell is finding 1's survivorship channel.
  • Runner/one-shot plumbing: refit_m6_components/M6RefitInputs/M6RefitBundle/RefitProvenance/EARNINGS_SPEC_SHA256 (refit.py:100-199,1063), EARNINGS_SPEC_REGISTRATION (refit.py:70), write_new exclusive-create + .env.json sidecar (artifacts.py:45-79), stub citations exact (tests/test_m6_engine_assembly.py:75-76,269-270).
  • Hygiene: docs-only diff (one file, +390/−5); 39/39 doc-bound tests pass in a fresh venv (test_gate_m6_floors.py + test_gate_m6_derivations.py); the §10 m6-design-parameters JSON parses (revision 4); CI green on fresh runs; MERGEABLE/CLEAN.

Disposition: adjudicate + pin finding 1, add the five one-line pins (2–6), and this amendment meets the §2.7.6 bar; the flows/disability scoring layer, the adjudicated core, the pre-flights, and the fence need no structural change. No gate cell, threshold, or floor artifact is touched by any of the above.

Referee round #42 comment 4963629234 ruled MAJOR REVISION on §2.8 — one MAJOR
(F1) plus five zero-choice pins (F2–F6) and one disclosure. Censor-symmetry and
once-per-draw resolved in the amendment's favor (verified from code). Docs-only.

- F1 (MAJOR, adjudicated) — the gated earnings support. Certified step-1
  mortality (apply_mortality) drops simulated deaths from the frame, so projected
  {2016,2018} earnings person-periods are a strict subset of realized truth →
  the §2.8.3 guard aborts every draw, or 6/11 cells score survivor-conditioned
  (admitting not_certified mortality drift via survivorship). Pinned: the gated
  earnings cells are a reproduction read on the REALIZED earnings support (the
  forward chain evaluated for every realized-present person-period regardless of
  simulated death), bit-safe because apply_earnings draws on person-keyed EARNINGS
  streams (steps.py:222-233) so no survivor bit moves; mortality stays
  report-only. Argued from decision 5 + §4.4 cancellation + §4.7. Adds §2.8.4
  paragraph + §2.8.3 EXACT_WAVE earnings support bullet (folds F6).
- F2 — pinned the cached-core stream address to the period-0 deployment address
  (rng.py:41-44; the address §2.8.5's internal reference uses, composition.py:
  643-653), and fixed the "per-period HOUSEHOLD_COMPOSITION streams"
  self-contradiction → one CompositionRngs per draw; marital main_rng/gap_rng and
  step-4 fertility stream pinned with it.
- F3 — marital builder universe = build_marital_panel(...).attrs ∩ anchor
  (MH-absent anchor persons excluded symmetrically).
- F4 — "realized ≤2015-interview" → "≤ the person's anchor interview (2015 for
  the bulk)" in §2.8.1, the household seed, and the §10 JSON (2017/2019 openers).
- F5 — attrs.n_marriages pinned to lifetime MH18 (person_attributes), carried-
  inert (unread; overwritten by _assemble_panel) — one value, not two.
- F6 — named the earnings-side support basis (realized reference-year row
  existence, EXACT_WAVE) distinct from the flow presence sets; folded into F1.
- Disclosure — added the fertility whole-window-recompute-per-period note (frame
  births stitched, inert on gated cells) as a third mechanical alignment.

§10 JSON → revision 5 (scored_run_harness updated). All 39 design-doc-bound
tests pass unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Revision 1 — addresses the amendment-3 referee round (comment 4963629234: MAJOR REVISION)

Pushed 45e757d (docs-only, +119/−37). One MAJOR (F1) adjudicated + pin, the five zero-choice pins (F2–F6), and the fertility disclosure. §10 JSON → revision 5. All 39 design-doc-bound tests pass unchanged.

F1 (MAJOR — gated-earnings scored support vs certified step-1 mortality) → §2.8.4 + §2.8.3. Certified apply_mortality (steps.py:108-130, "return only the period's survivors") drops simulated deaths from every later frame slice, so the projected {2016, 2018} earnings person-periods are a strict subset of the realized earnings_frame support — the §2.8.3 identity guard (support.py:311-315) aborts every draw, or 6/11 cells score survivor-conditioned (admitting not_certified mortality drift through survivorship). Adjudicated, by the same symmetric-conditioning principle §2.8.1 applies to histories, §4.4's cancellation logic, and §4.7's "the gate_m4 construction with the time axis substituted" (whose reproduction precedent scores on realized support): the gated earnings cells are a reproduction read on the realized earnings support — the forward chain is evaluated for every realized-present person-period regardless of simulated death. Bit-safe because apply_earnings draws on person-keyed EARNINGS streams (steps.py:222-233), so a realized-present-but-simulated-dead person's chain consumes only its own entropy and perturbs no survivor's bits; projected mortality stays report-only. Stated plainly what it does/doesn't condition on (realized reference-year presence = conditioning-not-leakage; mortality's earnings-composition survivorship channel is not scored — the reproduction test's designed scope, same as the flows).

The support-basis sentence, verbatim:

The gated earnings cells score on the realized support, not the live projected frame (adjudicated — the one gated family §2.8 must resolve against certified step-1 mortality).

F2 → §2.8.2. Pinned the cached-core stream address to the period-0 deployment address (rng.py:41-44) — the certified single-period topology, and the address the §2.8.5 internal reference already uses (composition.py:643-653, n_periods=0) — with the marital main_rng/gap_rng and the step-4 fertility stream; fixed the "per-period HOUSEHOLD_COMPOSITION streams" self-contradiction → the single composition_rngs_from_registry(registry, 0) CompositionRngs per draw.

F3 → §2.8.2 table. Marital attrs = build_marital_panel(mh, deaths, anchor_weight).attrs ∩ anchor — the identical truth-side universe; MH-absent anchor persons (children, MH-uncovered adults) excluded on both sides symmetrically.

F4 → §2.8.1 + §2.8.2 household seed + §10 JSON. "realized ≤2015-interview" → "realized ≤ the person's anchor interview (2015 for the bulk; 2017/2019 openers seed later)" — the leakage fence stated at the true per-person boundary (ratified conditioning, no fitter sees the seed).

F5 → §2.8.2 table. attrs.n_marriages = the lifetime MH18 person_attributes writes (the truth-side value → byte-identical seed), carried-inert — the core never reads it (order re-seeds to 1, marital.py:107,119) and _assemble_panel overwrites it (marital.py:262-272). One value, not two.

F6 → §2.8.3. Named the earnings-side support basis — realized family_earnings_panel person-period row existence at {2016, 2018} (EXACT_WAVE-class on reference years), distinct from the odd-wave flow presence_by_wave sets; folded into F1's resolution.

Disclosure note → §2.8.9. Added the fertility whole-window-recompute-per-period disclosure (frame-materialized births stitched across years; inert on every gated cell — children outside holdout_ids and every band, births feed report-only entrant/roster only) as a third mechanical alignment.

No gate cell, threshold, or floor artifact touched. Stays DRAFT pending the referee's re-read.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Amendment-3 re-check (§2.8 revision 1, 45e757d): VERIFIED — ready to merge

All six findings are resolved, not renamed — each traced against the certified code, not the revision narrative. The MAJOR (F1) is adjudicated with a bit-safety claim I verified holds at both the entropy and the value level; F2–F6 are exact pins; the fertility disclosure landed. Verified on the worktree build (.[dev] venv, PSID staged): docs-only (+119/−37, one file), 39/39 doc-bound tests pass, §10 JSON parses at revision 5, CI green on 45e757d, MERGEABLE/CLEAN.

F1 (MAJOR) — the gated-earnings support adjudication and its bit-safety claim: VERIFIED from code.

The pin — gated earnings = a reproduction read on the realized family_earnings_panel support, the forward chain evaluated for every realized-present person-period regardless of simulated death — is bit-safe, and I confirmed the claim at the level that matters (does a dead-but-present person's chain perturb any survivor's value, not merely its entropy):

  1. Entropy is person-local. apply_earnings's registry branch draws each person on context.person_generator(EARNINGS, person_id) (steps.py:220-224) → person_ordinals[person_id]child_generator(period, EARNINGS, ordinal) (loop.py:91-105, rng.py:158-169) — a pure function of (k, period, module, ordinal), independent of frame composition.
  2. Survivor addresses are provably invariant. person_ordinals is built once from the initial slice's sorted ids (loop.py:202-207) and is append-only (loop.py:283-285); mortality-dropping never removes an ordinal. So every survivor's stream address is identical whether or not the dead-but-present persons are carried in the earnings-scoring support — the exact property the reproduction read needs.
  3. The value is person-local too — the half a rename would have missed. In the registry branch generate is called on a single row (frame.iloc[[index]], steps.py:225-226), and the generator's donor pools are frozen at fit time from the ≤2014 estimation panel (_build_donor_pools baked into ForwardEarningsGenerator(pools=pools), forward_earnings.py:762-792) — not rebuilt from the live frame; generate derives its seed solely from the passed per-person rng and reads only that row's conditioning + the frozen pools + the per-row gen_earn_w2/w4 chain memory (forward_earnings.py:1046-1107, steps.py:237-247). No live cross-person donor pool exists to perturb.

So evaluating a realized-present-but-simulated-dead person's chain consumes only that person's own entropy and reads only that person's own row + frozen pools — it changes no survivor's value or bits. The scored earnings quantity is therefore invariant to whether the implementation carries dead persons in a parallel earnings-support frame or a non-dropping frame — no residual scoring freedom (zero-choices re-check on the F1 path: clean). The support-basis bullet (§2.8.3) is correct — realized family_earnings_panel reference-year row existence at {2016, 2018}, EXACT_WAVE-class, explicitly distinct from the odd-wave presence_by_wave flow sets (whose keys cannot address even reference years). The scope statement is coherent with the locked block: mortality's earnings-composition survivorship channel is not scored, projected mortality stays report-only (feeds diagnostics + AIME/claiming/roster, not the gated read), and mortality drift stays not_certified — consistent with gates.gate_m6 (ZERO mortality cells gated, not_certified mortality_drift first).

F2–F6 + disclosure (resolution, one line each)

  • F2 — period-0 address pin: VERIFIED. Cached core pinned to generator(0, MARITAL_CORE) / child_generator(0, MARITAL_CORE, 1) / single composition_rngs_from_registry(registry, 0) / generator(0, FERTILITY) — period 0 reproduces the certified SeedSequence([5200+k, tag]) topology (rng.py:117-119) and is the exact address the §2.8.5 internal reference already uses (composition.py:643-653, n_periods=0), so pre-flight 1 compares like-with-like; the "per-period streams" self-contradiction is gone (simulate_candidate9_injected takes exactly one CompositionRngs, composition.py:532-539).
  • F3 — attrs ∩ anchor: VERIFIED symmetric. attrs = build_marital_panel(mh, deaths, anchor_weight).attrs ∩ anchor is the identical universe the truth side scores (marital_tables inner-merges anchor onto the same build_marital_panel, build_m6_holdout_floors.py:259-299); MH-absent persons drop on both sides via _valid_persons (transitions.py:265-277), which also enforces start_exposure ≤ censor and gives every seeded person an entry row.
  • F4 — anchor-interview boundary: VERIFIED. §2.8.1 body, §2.8.2 household seed, and §10 JSON core_adjudication now read "≤ the person's anchor interview (2015 for the bulk; 2017/2019 openers seed later)"; the two remaining §2.8 "2015-interview" strings are the §2.8.3 title (line 954) and the SEED_WAVE = 2015 fact (line 956), whose body (lines 960-966) explicitly handles the 2017/2019 openers — genuine bulk-case, not a suppressed opener.
  • F5 — n_marriages: VERIFIED one value. Now pinned to the lifetime MH18 person_attributes writes (transitions.py:239-243, marriage.py:216) = the truth-side value (byte-identical seed), carried-inert — the core re-seeds order to 1 (marital.py:107,119) and _assemble_panel overwrites it from simulated episodes + residual (marital.py:262-272). The two-different-values inconsistency is gone.
  • F6 — earnings support basis: VERIFIED named, folded into F1's resolution (see above).
  • Disclosure — VERIFIED. Fertility per-period recompute (apply_fertilitysimulate_fertility, steps.py:401-433, births stitched across years) added as the third mechanical alignment; inert on every gated cell (children are outside holdout_ids and every gated band; births feed only report-only entrant/roster paths) — correct.

Non-blocking nicety (no gate impact, not a condition of merge)

The amendment-3 revision header bullet (line 18) still glosses the slice as "year-0 realized-2015-interview slice mirroring the floor" — bulk shorthand from the original commit, now superseded by the per-person-anchor language in every authoritative pin (§2.8.1 body, §2.8.2, §2.8.3, §10 JSON). Purely cosmetic; worth a one-word tidy on a future touch, not a revision.

Gates

Docs-only diff (one file, +119/−37, no gate cell / threshold / floor artifact touched); 39/39 test_gate_m6_floors.py + test_gate_m6_derivations.py pass in a fresh venv; §10 m6-design-parameters JSON parses (revision 5, residual_open_decisions: none, 3 mechanical alignments); CI green on 45e757d (build, lint, pytest 3.11 + 3.13); mergeable: MERGEABLE, mergeStateStatus: CLEAN.

Disposition: VERIFIED — the amendment meets the §2.7.6 zero-choices bar; ready to merge and proceed to the harness build + engine-referee-style review + fresh registration.

@MaxGhenis MaxGhenis marked this pull request as ready for review July 13, 2026 23:28
@MaxGhenis MaxGhenis merged commit 91f12f2 into master Jul 13, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant