Skip to content

M6: seed sub-15-at-anchor persons at certified marital entry (implements ratified 3g) - #210

Merged
MaxGhenis merged 3 commits into
masterfrom
m6-3g-impl
Jul 15, 2026
Merged

M6: seed sub-15-at-anchor persons at certified marital entry (implements ratified 3g)#210
MaxGhenis merged 3 commits into
masterfrom
m6-3g-impl

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

Summary

Implements the ratified amendment 3g (§2.8.2g in docs/design/m6_projection_engine.md, merged 92ad162, #209) — the marital projection domain law for sub-START_AGE-at-anchor persons. This is a zero-discovery transcription of a fully pinned design: the clamp, the tests, and the guard are all specified in §2.8.2g's Pins and Implementing patch text.

The closed-panel anchor universe (build_anchor_frame) admits minors with no age filter. marital_panel_builder overrode start_exposure_year := anchor_wave and then demanded the certified person_years entry row at that wave, but the certified marital person_years begin at birth_year + START_AGE (START_AGE = 15). Any person with anchor_wave < birth_year + START_AGE had no certified row at the anchor and the builder raised — the fifth-registration execution failure (ValueError: certified marital panel has no entry row at anchor; graded #42 comment 4979269487, forensics round‑2 comment 4979437110).

The patch

One-line clamp in marital_panel_builder (engine/panel_builders.py), mirroring the adjacent censor-clip idiom:

start_exposure_year = max(anchor_wave, birth_year + START_AGE)
  • Bulk (anchor_wave ≥ birth_year + START_AGE): equals anchor_wave, byte-unchanged.
  • Gap class (anchor_wave < birth_year + START_AGE): equals birth_year + START_AGE, the certified risk-set entry, which the subsequent entry-row read then finds by construction. The read still reads the certified person_years row (never an assumed constant) — the load-bearing N3 point.

START_AGE from populace_dynamics.data.transitions; birth_year already on attrs. The clamp lives only in the projected-side builder; the truth side (build_marital_panel) is untouched, so the frozen v3 floor and every gated denominator stay byte-identical. The certified entry-row raise stays (its cause is removed, not the invariant). No gates.yaml cell, no runs/ artifact, no floor, no schema-audit contract changes.

Adopted option B (seed-at-marital-entry) over A (exclude-and-mark): A's symmetric exclusion would move the frozen first_marriage.18-29|female tolerance 0.356 → 0.355, breaking byte-identity, which the ratified lock forbids.

Discrimination evidence

Three tests in tests/test_m6_panel_builders.py cover the crash class the born-1980–1982 fixtures lack (a born-2001 never_married seed, a born-2008 censor-drop, and a certified-married-entry-row N3 case). With the pre-3g override temporarily restored in the working tree, all three fail with the exact reg-5 signature:

ValueError: certified marital panel has no entry row at anchor for person_ids [20]

Restoring the clamp makes all three pass. The married-entry-row test is the N3 discriminator: an assumed-never_married seed would read the wrong state and fail.

Real-frame proof (read-only; no scoring, no artifacts)

Patched builder run over the realized anchor (seed-0 crash context) via the production loaders, compared in-memory against the unclamped origin/master builder:

check result ratified
no raise 23,062 seeded (20,424 bulk + 2,638 class)
sub-START_AGE class 2,850 — waves {2015: 2568, 2017: 246, 2019: 36}; ages 7–14, mean 10.80; born 2001–2008 2,850 / same split
net seeded 2,638 2,638
censor-dropped 212, all born 2008 (birth+15 = 2023 > 2022) 212
seeded entry states 2,638 never_married, 0 certified-row exceptions, entry rows exactly at birth+START_AGE, 0 duplicates 0 exceptions
bulk byte-identity 20,424 attrs + 20,424 person_years rows identical to the unclamped non-class run; survivor-id sets equal byte-identical
reg-5 crash ids (4979437110) 9/10 seeded with certified entry rows (never_married); 10187 (born 2008) censor-dropped by the 212-rule resolved

Every number matches the ratified amendment. reg-5 id 10187 is the one born-2008 member among the reported ten and is correctly censor-dropped, consistent with the 212-drop rule — not a divergence.

Guard and mechanics

  • Registers reg-5 4976428384 (graded Candidate 2 design: latent-permanent conditioned chained QRF (research memo) #42 comment 4979269487) in _KNOWN_STALE_REGISTRATIONS and the freshness-guard parametrize.
  • Tier recount: unit 434 → 437 (the three builder tests), artifact 1013 → 1014 (the parametrize case); tier_counts.json updated, test_tier_policy green on full collection (2,932 items).
  • black -l 79 clean; three small commits.

Amendment merge 92ad162 (#209) · forensics #42 comment 4979437110 · grading #42 comment 4979269487.

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits July 15, 2026 08:18
Implements the ratified amendment 3g pin (§2.8.2g, merged 92ad162).  In
marital_panel_builder, replace the unconditional override
start_exposure_year := anchor_wave with the clamp
start_exposure_year = max(anchor_wave, birth_year + START_AGE).

For the bulk (anchor_wave >= birth_year + START_AGE) this is byte-unchanged;
for the sub-START_AGE-at-anchor class (anchor_wave < birth_year + START_AGE,
the reg-5 crash class, graded #42 comment 4979269487, forensics 4979437110)
it is the certified marital person_years entry (birth_year + START_AGE), which
the subsequent entry-row read finds by construction.  The read still reads the
certified person_years row (never an assumed constant), the truth side is
untouched, and the frozen v3 floor plus every gated denominator stay
byte-identical.  START_AGE from populace_dynamics.data.transitions; birth_year
already on attrs.  The certified entry-row raise stays (its cause is removed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the crash-class fixture the born-1980-1982 marital builder fixtures lack
(reg-5, forensics #42 comment 4979437110), covering the three pinned cases:

- seeds a born-2001 person anchored 2015 at their certified marital entry
  (2016), never_married, in holdout_ids, with the entry row at 2016;
- drops a born-2008 person (birth+START_AGE = 2023 > the 2022-clipped censor)
  via the existing start_exposure <= censor filter, neither seeded nor held;
- reads a certified *married* entry row for a sub-START_AGE-at-anchor person
  and seeds them married (the N3 discrimination: the builder reads the
  certified row, it does not assume never_married).

Discrimination confirmed: with the pre-3g override restored in the working
tree, all three raise the exact reg-5 signature
"certified marital panel has no entry row at anchor for person_ids [20]".

Bump the unit tier count 434 -> 437 in tier_counts.json to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fifth gate_m6 registration (4976428384, graded #42 comment 4979269487)
failed to execute in the seed-1 scoring projection on the marital entry-row
gap that amendment 3g closes; forensics round-2 is #42 comment 4979437110.
Add it to _KNOWN_STALE_REGISTRATIONS and the freshness-guard parametrize so it
can never authorize a scored run, and extend the lineage comment.

Bump the artifact tier count 1013 -> 1014 for the added parametrize case.

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

vercel Bot commented Jul 15, 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 15, 2026 12:31pm

Request Review

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Adversarial referee — PR #210 (amendment 3g implementing patch)

Independent re-verification against the ratified §2.8.2g pins (master 92ad162), the reg-5 grading (4979269487), and forensics round 2 (4979437110). Fresh worktree at head 853134b, own venv (py3.14, two-stack: policyengine-us 1.752.2 + populace-fit/populace-frame + scikit-learn 1.8.0), real-frame work read-only through the harness loaders exactly as forensics — no scoring, no runs/ artifact, no gates.yaml read or write.

1. Ratified-pin fidelity — exact transcription, verified

  • The clamp sits at the pinned site (master panel_builders.py:187, the unconditional start_exposure_year := anchor_wave override — confirmed against the master blob), computes max(anchor_wave, birth_year + START_AGE) in the certified dtype via the same to_numpy(float64) → astype(source dtype) idiom as the adjacent censor clip. START_AGE from data.transitions; birth_year read from attrs (guaranteed non-null by _valid_persons). No NaN path, no tie-case drift (anchor == birth+15 stays bulk).
  • The downstream entry-row read is unchanged (now :211-216): the seeded state still comes from the certified person_years row selected at the (clamped) seed wave — never a constant. The N3 load-bearing property holds in code and is now test-enforced (§3).
  • The certified raise is retained (:220-225, byte-unchanged) — cause removed, invariant kept, exactly as pinned.
  • Nothing else moves: no other builder, no certified-core file, no gates.yaml, no runs/, no floor, no schema/manifest file. Truth side has no import chain into the builder (build_m6_holdout_floors.py and m6_cells.py never touch panel_builders; _truth_cells consumes prebuilt truth frames; the builder feeds only _project_side). Frozen v1/v2/v3 floor files untouched.
  • Test pins: the born-2001/anchored-2015 fixture and the born-2008 censor-drop case land exactly as §2.8.2g specifies, in the existing _marital_source idiom (extended, not rewritten). Pin assertion (a) — "under the pre-patch override the builder raises" — is not durably encodable post-merge; it was carried as working-tree discrimination evidence, which I reproduced independently (§3). The third test (certified-married-entry read) goes one step beyond the two pinned cases but enforces the §2.8.2g adjudication's stated load-bearing property ("that B reads the certified row, not an assumed state, is load-bearing") — within "extended, not rewritten", and it is the only class-level discriminator of that property (§3).
  • The stale-guard commit (853134b) is outside §2.8.2g's enumerated patch but implements the standing registration-freshness contract with exact precedent (M6 harness: source person sex canonically in build_realized_population (reg-3 crash-2 fix + schema audit) #203 added reg-3, M6 pre-flight 1: wire the §2.8.2-pinned fertility into the injected arm (reg-4 abort fix) #207 added reg-4 in their fix PRs) and is required before the SIXTH registration the ratified lineage names. Adjudicated in-scope, not a transcription deviation. Runner comment wording matches the grading verbatim ("seed-1 scoring projection", both pre-flights passed).

2. Real-frame proof — reproduced independently, every number matches

Read-only, harness loaders exactly as forensics (panels.demographic_panelbuild_anchor_frame, deaths.read_death_records, marriage.marriage_historybuild_marital_panel with the anchor-weight series):

check my independent run PR / ratified
class (anchor < birth+START_AGE) 2,850 — {2015: 2,568, 2017: 246, 2019: 36}; ages 7–14, mean 10.80; born 2001–2008 match
patched full-anchor run no raise; 23,062 = 20,424 bulk + 2,638 class match
censor-dropped 212, all born 2008 match
seeded class state 2,638 entry rows exactly at birth+START_AGE, all never_married, 0 duplicates, 0 certified-row exceptions match
bulk byte-identity patched-on-bulk-anchor vs unclamped-master-on-bulk-anchor: attrs.equals and person_years.equals True; full-run bulk subset vs master: True/True; 20,424 + 20,424 rows; survivor-id sets equal match
reg-5 ids 9/10 seeded never_married at their birth+15; 10187 (born 2008) censor-dropped match

Bonus confirmation: the unclamped master builder on the full anchor still raises the reg-5 signature, and its missing[:10] reports different ids (6035, 6038, 7050, …) than the crash's ten — independently confirming §2.8.2g's claim that reg-5's ids were the household-split side's lowest, not the class's.

3. Discrimination — proven both directions

  • Clamp reverted (pre-3g override restored in a scratch worktree): exactly the three new tests fail, each with the exact reg-5 signature ValueError: certified marital panel has no entry row at anchor for person_ids [20]; all 7 pre-existing builder tests still pass — confirming the doc's claim that the born-1980–1982 fixtures never exercised the crash class. Restored → 10/10 pass.
  • Constant-seed mutation (entry state overwritten with assumed never_married after the row read): test_marital_builder_reads_certified_married_entry_row_for_minor fails while the other two new tests pass — it is the sole class-level discriminator of the read-the-certified-row property (three pre-existing bulk tests also catch the mutation, but only for bulk seeds). Restored → all pass.

4. Mechanics

  • Stale guard: bare 4976428384 and the full issue-42 URL form both rejected (rsplit("-", 1) extraction verified empirically); fresh ids in both forms accepted. Parametrize follows the pre-existing bare-form shape.
  • Tier recount: collection = 2,932; manifest sum 437+1,014+802+520+159 = 2,932; +3 unit (builder tests, module classifies unit — no PSID/oracle indicator strings) and +1 artifact (runner parametrize case) mechanically consistent; test_tier_policy green against the full pre-deselection collection, locally and in CI at head.
  • black -l 79 and ruff check (repo config) clean on all four changed files. Three commits, each scoped to one pin component. CI fresh and green at head 853134b (pytest 3.11 + 3.13, lint, build); mergeable: MERGEABLE.
  • Beyond CI: the full m6 module family (22 files, 143 tests) passes at head against live PSID, including test_m6_truth_identity (floor-script vs harness reducer byte identity — the frozen-floor live guard) and test_m6_schema_audit_psid. This set is closed over the diff: the only test modules importing the changed production modules (directly: test_m6_inputs, test_m6_panel_builders, test_m6_population, test_m6_runner, test_m6_runner_cli; indirectly via the m6 harness) all sit inside it, and the only production importers are the m6 harness modules plus the registered-run script — no legacy gate-1/2/2b, w-series, or oracle test executes a changed line. In the live legacy tiers (which CI skips), a handful of committed-artifact reproductions (test_real_family_*_floor_reproduces_committed_run, gate-1 QRF test_seed0_reproduces_committed_artifact in candidate5a/5a2, test_downstream_relevance) fail on my unpinned py3.14 + scikit-learn 1.8 stack — identically at head and at origin/master in the same venv (controlled A/B: same failure set both sides), i.e. pre-existing environment sensitivity, not this PR.

Findings

BLOCKING — none.

SHOULD-FIX — none.

NOTE-1 (routes to the design lane, not this PR): §2.8.3f manifest now understates the builder's reads. The clamp reads birth_year from marital.attrs, but PHASE_FRAME_COLUMN_READS["marital_panel_builder"]["marital.attrs"] still lists {person_id, censor_year, start_exposure_year, weight} — and the manifest's own contract says each entry is "the set the function requires to EXIST on the frame". Dormant today (the audit checks manifest ⊆ schema, and birth_year is on the certified 9-column schema, so nothing fires), but the §2.8.3f guard would not catch a future schema change dropping birth_year ahead of this builder — the exact run-2 defect class it exists to catch. The patch is faithful here: §2.8.2g pins "Schema-audit / manifest deltas: none", so adding birth_year in this PR would itself deviate. The pin's rationale ("needs no new contract row") conflates no-new-row with no-delta-to-the-existing-row's read-set. Recommend a one-line erratum rider in the next docs amendment authorizing birth_year into the existing manifest row, then the one-token code follow-up.

NOTE-2 (comment currency; may ride this PR or the next): (a) the new clamp comment says "the read below (:200-205)" — the insertion itself shifted the read to :211-216, so the pointer is stale on arrival (the §2.8.2g master-coordinate habit carried into a post-patch file); (b) the builder docstring's universe sentence ("their anchor interview is within their realized report/death censor") is post-3g inaccurate for the class — the binding keep-test is now the clamped seed wave ≤ clipped censor, and the 212 born-2008 have their anchor interview within censor yet drop; (c) the new test comment's "All five graded pre-scoring terminations" is loose for reg-5, which crashed inside the seed-1 scoring projection (the grading stresses that reaching score_m6_seed was a first) — the same comment's next clause already states it precisely. All comment-only; none touches a pinned surface.

Verdict

VERIFIED — ready to merge. Exact transcription of the ratified §2.8.2g pins; the real-frame proof reproduces to the digit including bulk byte-identity; the tests discriminate in both directions; the guard, tiers, format, CI, and mergeability all check. The two notes are non-blocking (one is design-lane routing by construction, one is comment text).

Referee protocol: read-only real-frame verification through the harness loaders in a disposable worktree; no gates.yaml, runs/, or held-worktree contact; scratch worktrees and venvs removed after filing.

@MaxGhenis
MaxGhenis merged commit f4872f8 into master Jul 15, 2026
7 checks passed
MaxGhenis added a commit that referenced this pull request Jul 15, 2026
…carrier)

S1: PR #210 (3g impl) merged before this PR without the birth_year manifest fix and
no open PR touches m6_schema_audit.py, so that lane is closed — reassign the one-token
follow-up to the 3h implementing patch; carve out "3h in this docs PR edits no manifest".
S2: birth_store is write-only dead state (steps.py:471 into state.fertility, read by no
runner phase) — pin the actual carrier: publish roster_absent_births via
_publish_draw_output into the entrants block (m6_runner.py:909-920) next to
synthetic_births, so the record is published not just stored. Also fixes the clamp ref
(panel_builders.py:194-197) and birth_store :471. Prose + §10 JSON. Docs-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Jul 15, 2026
…on domain law (closes reg-6 crash) (#216)

* M6 amendment 3h: add §2.8.2h fertility/open-additions roster-materialization domain law

Adds the §2.8.2h prose section (mirrors §2.8.2g conventions), the domain law
separating the frame-independent at-risk SCHEDULE (scoring) from the simulated-
mortality ROSTER (materialization): scheduled maternal births materialize only for
roster-present mothers; a dead mother's scheduled birth is dropped with report-only
reconciliation, closing the reg-6 crash (guard steps.py:412 raise on {782173}).

Adjudicates the three resolution options (drop-with-reconciliation adopted over
re-draw / keep-raising), works the determinism / report-only-bias / certified-
scoring-invariance obligations, and includes the sibling sweep (merge-vs-materialize
arm-parity across all wave-loop open-addition paths). Cites forensics 4984997277
and grading 4984699959. Docs only.

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

* M6 amendment 3h: fold §2.8.9 — item (iii) fertility disclosure → law; residual decisions one→two

Item (iii) (step-4 fertility materialization) is promoted from "disclosed rather
than fixed" to closed by amendment 3h (§2.8.2h). The residual-open-decisions summary
now records two closed decisions (3g marital universe + 3h fertility roster
materialization), each tied to the registration failure that surfaced it.

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

* M6 amendment 3h: revision-log entry (§10 revision 14) + correct stale apply_fertility line-ref

Adds the amendment-3h revision-log bullet (→ §2.8.2h, §10 revision 14). Corrects the
pre-existing stale ref steps.py:401-433 → steps.py:440-472 in §2.8.9 item (iii)
(apply_fertility is at 440-472 at HEAD; 401-433 fell inside materialize_maternal_births).

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

* M6 amendment 3h: §10 JSON — revision 13→14, amendment_3h_section, residual + mechanical-alignment folds

Bumps the design-parameters revision to 14; adds amendment_3h_section (the dense
forensic ledger for 3h mirroring amendment_3g_section); promotes residual_open_
decisions one→two (3g marital + 3h fertility roster); folds mechanical_alignments
item (iii) from "disclosed not fixed" to governed by 3h (and corrects the stale
apply_fertility line-ref). JSON block validated parseable.

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

* M6 amendment 3h: fix sibling-sweep line refs (advance_age steps.py:137, allocate call site)

Self-review catch: the aging row cited steps.py:434 (advance_age is at :137, wired
aging=advance_age at assembly.py:428); the synthetic-id row now cites the allocate
call site (steps.py:418) and the allocator definition (loop.py:49) distinctly.

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

* M6 amendment 3h: B1 — add the live scheduled-realized-openers sweep row; re-ground immigrant row

Referee BLOCKING-1: the sibling sweep missed a live row-attaching wave-loop path.
Entrants ride loop-native SCHEDULED_ENTRIES_KEY metadata (loop.py:192-254, concat at
period top before mortality), live for the 2017/2019 opener cohorts (246+36), NOT a
missing PeriodModules step. Adds the scheduled-realized-openers row (loop-native, fresh
realized ids keyed to no removable person, overlap-guarded, ordinals pre-seeded → no
KeyError shape → holds by construction) and re-grounds the immigrant row as dormant
(nothing schedules immigrant frames; entrants report hardcodes immigrant_cohorts:0,
m6_runner.py:914). Both the §2.8.2h table and the §10 JSON. Docs-only. The law
survives; the enumeration is now complete.

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

* M6 amendment 3h: S4 — refresh stale line pins (simulate_fertility :469, marital merge :309-314, paternal :389-391)

Referee SHOULD-FIX-4: simulate_fertility is called at steps.py:469 (:467 is the else);
the marital sim_years left-merge is at assembly.py:309-314 inside fertility_step (:412
is the household merge, and household_step consumes marital only as conditioning); the
paternal-shadow docstring sentence spans steps.py:389-391. Prose + §10 JSON. birth_store
:471 and the clamp :194-197 land with S2/S1. Docs-only.

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

* M6 amendment 3h: S1 (erratum owner) + S2 (reconciliation publication carrier)

S1: PR #210 (3g impl) merged before this PR without the birth_year manifest fix and
no open PR touches m6_schema_audit.py, so that lane is closed — reassign the one-token
follow-up to the 3h implementing patch; carve out "3h in this docs PR edits no manifest".
S2: birth_store is write-only dead state (steps.py:471 into state.fertility, read by no
runner phase) — pin the actual carrier: publish roster_absent_births via
_publish_draw_output into the entrants block (m6_runner.py:909-920) next to
synthetic_births, so the record is published not just stored. Also fixes the clamp ref
(panel_builders.py:194-197) and birth_store :471. Prose + §10 JSON. Docs-only.

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

* M6 amendment 3h: S3 — restate the option-(ii) rejection on fabrication grounds

Referee SHOULD-FIX-3: "a re-draw shifts every downstream per-person stream" is
architecture-false under the keyed ProjectionRNGRegistry (rng.py isolates every
(period,module)/person stream; extra fertility-stream consumption shifts only that
stream's own later child-sex draw, never another module/person/period). The rejection
stands on fabrication alone — a re-draw materializes a birth the frame-independent
schedule never scheduled, breaking the schedule↔materialization correspondence. Prose
table cell + §10 JSON restated. Docs-only.

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

* M6 amendment 3h: NOTE-1/2/3 precision refinements

NOTE-1: name the one same-stream consumer (child-sex draw, steps.py:424-426) in the
byte-unchanged claim and pin the comparison baseline (pre-patch aborts before the sex
draw; only a hypothetical mother-present roster would shift same-wave sexes).
NOTE-2: make the same-wave KeyError safety's reliance on eligibility filters explicit
(earnings age>=15 steps.py:240; claiming age>=50 steps.py:342 exclude age-0 newborns).
NOTE-3a: per-draw drop count is bounded by absent fertile woman-YEARS (parity loop
marital.py:317-341), not by the 10 members.
NOTE-3b: label discriminating-test assertion (c) a rewiring tripwire (holds by
signature — simulate_fertility takes no roster arg), not a filter property.
Prose + §10 JSON. Docs-only.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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