Skip to content

M6 design amendment 3e: mortality-rates shape bridge + parameter-dir binding (§2.8.10, closes factory-referee F1/F2) - #196

Merged
MaxGhenis merged 1 commit into
masterfrom
m6-design-amendment-3e
Jul 14, 2026
Merged

M6 design amendment 3e: mortality-rates shape bridge + parameter-dir binding (§2.8.10, closes factory-referee F1/F2)#196
MaxGhenis merged 1 commit into
masterfrom
m6-design-amendment-3e

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

M6 design amendment 3e — mortality-rates shape bridge + parameter-dir binding

Docs-only amendment to §2.8.10 (new §2.8.10.5) + §10 JSON revision 11, closing the two run-gating findings the adversarial factory referee filed against the merged spec/engine pair in #191 comment 4969829131. The factory itself (registered_m6_inputs.py, merged 67e7fad) was graded MERGE-READY (build only) — it faithfully implements the pinned §2.8.10 shape with zero build-lane choices, so both are findings in the spec/engine, not build defects. This amendment is the reviewed pin the referee names as the third-gate_m6-registration unblock. It edits no gates.yaml cell, moves no threshold, builds no floor, and writes no test.

F1 (blocking) — the pinned seven-band shape is rejected by the projection model

§2.8.10.3 pins mortality_external_rates over the seven MORTALITY_BANDS (25-34 … 85+), but fit_mortality_model derives its model bands from those rows and constructs AgeSexMortalityModel, whose __post_init__ requires bands starting at age 0, contiguous through 120 (engine/steps.py:57-68 — so materialized births get a defined hazard, not the np.zeros default). The run hits this at its first phase, fit_mortality_model (m6_runner.py:348), before pre-flights/score/write, raising mortality bands must start at age zero. A bare "add a 0-24 external row" then trips the per-cell fit gate (no <25 PSID exposure). The whole mortality data pipeline — floors, holdout truth, scored cells, external anchor — is 25+ only; the 0-start requirement is solely a projection-model invariant.

Pinned resolution — option (b): an input-side inert (0,24) projection-coverage pad on both fit inputs, engine untouched.

  • External pad: central_rate = (l_0 − l_25)/(T_0 − T_25) (NCHS-2010, ≈ 0.000766 M / 0.000451 F) — outcome-inert because aligned_rate = central_rate × (psid_rate/central_rate) = psid_rate cancels it.
  • Exposure pad: event_year = required_interview_year = 2014 (survives ≤T* truncation), death = 0psid_rate = 0<25 hazard = 0 exactly (no modeled mortality below the age-25 PSID exposure floor).

Verified by execution on the merged refit.py: unpadded raises; padded yields a valid 8-band 0→120 model; the fourteen 25+ cells are byte-identical and invariant to the pad's value/weight; the MORTALITY_BANDS == mf.BANDS guard is untouched (pad appended after the seven-band build), so committed floors are undisturbed. Option (a) rejected: it needs an engine no-exposure fallback and imports an NCHS level as the hazard, violating the aligned_rate = psid_rate anchor contract (PSID levels, NCHS inert).

F2 (should-fix) — the version gate does not bind the parameter directory

assert_pe_us_version reads importlib.metadata, but load_ssa_parameters reads YAML from POPULACE_DYNAMICS_PE_US_DIR (ss/params.py:187-227) — a mismatched dir passes the gate (referee-demonstrated: 1.752.2 pip + env→1.690.7 checkout). Pinned resolution: a factory-side assert_pe_us_param_dir() binding (_resolve_pe_us(None)/_SSA) to distribution("policyengine-us").locate_file("policyengine_us")/parameters/gov/ssa — verified against the real 1.752.2 site-packages install.

N1 (nit)

Soften _era_mapping's docstring's phantom "verified in build()" (era_map is provenance-only; no assert added).

Patch pointers (zero-discovery for the patch lane)

  • scripts/registered_m6_inputs.py: add _pad_below_25_projection_coverage(...), call in build_inputs step (4); add assert_pe_us_param_dir(), call in step (1).
  • scripts/extract_ssa_claim_ages_2014.py: soften the _era_mapping docstring.
  • Add the missing fit_mortality_model bridge test the referee flagged.

Stays DRAFT pending the amendment-3e referee round.

🤖 Generated with Claude Code

…binding (§2.8.10.5, closes factory-referee F1/F2)

Docs-only amendment (§2.8.10 + §10 JSON revision 11) closing the two run-gating
findings the factory referee filed against the merged spec/engine pair (PR #191
comment 4969829131); the factory itself (67e7fad) is faithful with zero build
choices, so both are findings, not build defects.

F1 (blocking): the pinned seven-band mortality_external_rates shape is rejected by
AgeSexMortalityModel.__post_init__ (bands must start at age 0, contiguous through
120) at the run's first phase, fit_mortality_model (m6_runner.py:348), before any
pre-flight/score/write. Pinned resolution = option (b): an input-side inert (0,24)
projection-coverage pad on BOTH fit inputs -- external central_rate is outcome-inert
(aligned_rate = psid_rate cancels it), exposure death=0 gives a <25 hazard of exactly
0 (no modeled mortality below the age-25 PSID exposure floor); the engine is
untouched. Verified by execution on the merged refit.py: unpadded raises; padded
yields a valid 8-band 0->120 model with the fourteen 25+ cells byte-identical and the
<25 hazard 0. Option (a) rejected: it needs an engine no-exposure fallback and imports
an NCHS level, violating the aligned_rate=psid_rate anchor contract.

F2 (should-fix): the version gate reads importlib.metadata but load_ssa_parameters
reads YAML from POPULACE_DYNAMICS_PE_US_DIR (ss/params.py:187-227), so a mismatched
dir passes the gate. Pinned resolution = a factory-side assert binding the resolved
parameter dir to distribution('policyengine-us').locate_file('policyengine_us') /
parameters/gov/ssa, verified against the real 1.752.2 install.

N1: soften the _era_mapping docstring's phantom "verified in build()" (era_map is
provenance-only; no assert added).

Pins the one-line factory/adapter patch pointers for the patch lane. Edits no
gates.yaml cell, moves no threshold, writes no test.

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

vercel Bot commented Jul 14, 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 14, 2026 2:50pm

Request Review

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Referee — amendment 3e (d798a57), verified against code + execution

Verdict: VERIFIED (ready to merge). Both factory-referee findings are resolved by executable, pinned designs that I reproduced myself against the merged engine (real refit.py / steps.py, real pe-us 1.752.2, committed NCHS-2010); N1 is sound; every code pointer is accurate for zero-discovery; revision 11 parses; the 39 doc-bound tests pass; CI is green and the PR is MERGEABLE. This is a docs-only design pin (1 file, +178/−4) — the verdict covers the design; the F1/F2/N1 code patch + the missing bridge test remain a separate lane per the pinned sequence.

F1 — the inert (0,24) projection-coverage pad (reproduced end-to-end)

I implemented the pad exactly as §2.8.10.5 pins it (external {0,24,"0-24",sex, (l₀−l₂₅)/(T₀−T₂₅)}; exposure {event_year=2014, required_interview_year=2014, "0-24", sex, start_weight=1, exposure=1, death=0}) and routed both through the real prepare_mortality_refit_inputs + fit_mortality_model:

check result
NCHS 0-24 pad central_rate male 0.00076582, female 0.00045150 — matches the doc's "≈0.000766 / ≈0.000451"
unpadded fit (7-band) raises ValueError: mortality bands must start at age zero — the run's first phase (m6_runner.py:348)
padded fit valid 8-band model ((0,24),(25,34),…,(85,120))
14 twenty-five-plus cells byte-identical (==) to the unpadded arithmetic
pad value/weight invariance 25+ cells unchanged across external 0.00077 ↔ 0.5 and weight 1.0 ↔ 17.3 (and NCHS/3.14)
<25 hazard exactly 0.0 (both sexes; psid_rate=0 → −expm1(0)=0), invariant to every pad value/weight
option (a) rejection external 0-24 row without exposure → ValueError: undefined mortality fit cell 0-24|female (the weighted_exposure ≤ 0 guard, refit.py:1070-1082)
T*-dating is load-bearing the 2014-dated pad survives ≤T* truncation (2 rows kept); a 2015-dated pad drops wholesale

The inertness is not just arithmetic — it clears AgeSexMortalityModel.__post_init__'s band/coverage/cell/[0,1] checks (0 ∈ [0,1]) without perturbing a single fitted value, because the pad's "0-24" age_band never matches a 25+ row's cell selection (refit.py:1071-1074). The option-(a) level-import objection is real in code: aligned_rate = central_rate·(psid_rate/central_rate) = psid_rate (refit.py:1083-1088) cancels the external rate on every cell, so consuming a real NCHS <25 level would require an engine no-exposure fallback and would break the aligned_rate = psid_rate anchor contract — option (b) touches zero engine surface. Confirmed MORTALITY_BANDS == mf.BANDS (registered_m6_inputs.py:238) is untouched (pad appended after the seven-band build). The (0,24) band feeds only apply_mortality as a zero hazard: _projected_mortality_cells maps to the seven bands and drops <25 (m6_runner.py:708-722), and both the scored and the report-only mortality_drift/shock surfaces route through it (m6_runner.py:855-874; certifies_nothing_about_mortality_drift: True, :1244) — no scored or disclosed cell sees <25.

F2 — assert_pe_us_param_dir (reproduced, both directions)

Ran the pinned assert verbatim in the 1.752.2 venv:

  • claim confirmed: distribution("policyengine-us").locate_file("policyengine_us") == Path(policyengine_us.__file__).parent.
  • passes when POPULACE_DYNAMICS_PE_US_DIR → the metadata-versioned site-packages install (resolved == versioned; load_ssa_parameters() then loads it, pe_us_revision="unknown" as the doc's provenance note predicts).
  • fires when the env var → the local 1.690.7 checkout while metadata still reads 1.752.2 (resolved = checkout …/ssaversioned = site-packages …/ssa), the exact F2 gap. Reproduced the root cause: without the assert, load_ssa_parameters() silently loads the checkout YAML (pe_us_revision="2542e38bc0" — a real hash, not "unknown"). The mechanism is sound; note it also fires when the env var is unset (default → the checkout), so it forces the operator to point at the 1.752.2 install — a strengthening, not just a check.

Pointers, N1, scope, CI

  • Patch pointers accurate (zero-discovery): build_inputs step (1) is assert_pe_us_version() (F2 assert lands right after); step (4) is nchs_2010_external_rates() + mortality_exposure_adapter(...) (F1 pad appended on their outputs before load_m6_inputs). Every cited line resolves at this head: steps.py:57-68/:98-109, refit.py:1061-1068/:1083-1088/:1089, m6_runner.py:348/:708-722/:855-874/:1244, build_mortality_floors.py:223-224, ss/params.py:52-54/:187-193/:226, extract_ssa_claim_ages_2014.py:327-328.
  • N1 sound — agree. Soften the docstring, no assert: era_map is provenance-only (read by nothing — re-confirmed), build() has no such assert, and an assert would guard a non-load-bearing property that could spuriously fail on a valid future edition with a legitimately sex-specific era boundary. The proposed replacement clause is accurate.
  • Docs-only at the git level: docs/design/m6_projection_engine.md only, +178/−4. The m6-design-parameters JSON block parses; revision == 11; amendment_3e_section present; next pointer threads 3e → patch → referee → merge → third registration.
  • 39 doc-bound tests pass (test_gate_m6_floors 23 + test_gate_m6_derivations 16); no test hardcodes the design revision, so 10→11 is safe. CI green (pytest 3.11 + 3.13, lint, build — gh pr checks exit 0), MERGEABLE / CLEAN at d798a57.

Observation (non-blocking): the zero <25 hazard is a slight downward bias vs the true NCHS ~7.7e-4/yr, but it is ungated, disclosed, deterministic, and — per the §2.8.4 F1 adjudication — reaches no gated tolerance; the honest alternative (option a) is correctly rejected. The design is internally consistent and the resolution minimal.

Verified in a throwaway worktree at d798a57 + a fresh pe-us 1.752.2 venv (proof scripts run against the merged engine, no repo edits). No merge/ready action taken; worktree/venv removed.

@MaxGhenis
MaxGhenis marked this pull request as ready for review July 14, 2026 15:08
@MaxGhenis
MaxGhenis merged commit 30a1bd4 into master Jul 14, 2026
7 checks passed
MaxGhenis added a commit that referenced this pull request Jul 14, 2026
…(3e patch: F1/F2/N1 + bridge test) (#197)

* M6 factory: (0,24) projection-coverage pad + pe-us param-dir binding (amendment 3e patch, F1/F2/N1)

The zero-discovery patch pinned by ratified amendment 3e (§2.8.10.5, PR #196),
verified against the merged engine by the 3e referee (#196 comment 4970729902):

- F1: `_pad_below_25_projection_coverage(external_rates, exposure, *,
  boundary_year)` appends the inert (0,24) band per sex to BOTH mortality fit
  inputs after the seven-band construction — external pad = the NCHS 2010
  (l_0−l_25)/(T_0−T_25) central rate (outcome-inert; aligned_rate = psid_rate
  cancels it), exposure pad dated event_year = required_interview_year = T*
  with death = 0 so the fitted <25 hazard is exactly 0. Called in
  build_inputs step (4). MORTALITY_BANDS and the floors artifacts untouched.
- F2: `assert_pe_us_param_dir()` binds the directory load_ssa_parameters will
  read (env var / default resolution) to the metadata-versioned
  policyengine-us install's own on-disk location; called in step (1) right
  after assert_pe_us_version(). Closes the silent-wrong-load seam the referee
  demonstrated with a 1.690.7 checkout behind a 1.752.2 install.
- N1: the extractor's era-mapping docstring no longer claims a build()
  verification that does not exist (era records are provenance-only).
- The missing fit_mortality_model bridge test the factory referee flagged:
  factory-shaped rates now route through prepare → fit in committed tests —
  unpadded seven-band inputs raise "start at age zero"; padded inputs fit an
  8-band 0→120 model whose <25 hazard is exactly 0 and whose fourteen 25+
  cells equal the PSID arithmetic and are invariant to pad value and weight.
  Plus param-dir gate tests via an injected fake distribution (no real
  policyengine-us needed; the module stays in the always-run unit tier, with
  the env var referenced through the loader's own constant so the
  tier classifier does not demote 21 existing always-run factory tests).
- tier_counts.json: unit 410 → 420 (full-collection recount).

No gates.yaml change, no floor, no scored surface touched.

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

* Mark registration 2 stale in the runner guard

4967241464 is a publicly graded pre-scoring stop (grading 4967433717) and
must not authorize a PSID-touching run; only a fresh third registration can.
Disclosed in the PR body as an addition beyond the pinned 3e patch pointers
(guard hygiene from the public record, no design choice). tier_counts:
artifact 1010 -> 1011 (the added parametrize case; full-collection recount).

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

* Apply black formatting (CI formatter)

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