Skip to content

Reviewed CMS Medicaid enrollment substitution register for the take-up gate (#386)#387

Merged
MaxGhenis merged 5 commits into
mainfrom
medicaid-gate-substitution
Jul 10, 2026
Merged

Reviewed CMS Medicaid enrollment substitution register for the take-up gate (#386)#387
MaxGhenis merged 5 commits into
mainfrom
medicaid-gate-substitution

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

For the Medicaid stage owner's review

This implements the conductor recommendation on #386: a reviewed-substitution register at the Medicaid take-up gate, rather than relaxing the gate. It is deliberately staged for the Medicaid stage owner's review before merge — the substitution touches what the release calibrates state Medicaid enrollment to.

The problem (#386)

The #334 Medicaid take-up gate fails closed on the first full-release run (#368 Build J): Rhode Island's December 2024 CMS snapshot is unreported at source. Both RI 202412 rows in the pinned pi-dataset-april-2026-release.csv carry Total Medicaid Enrollment = 0 under the footnote "Unable to Provide Data due to System Limitations", so the point-in-time snapshot compiles to no FIPS-44 medicaid_enrollment spec and the count-calibration gate fails on states without CMS enrollment targets: ['44'].

The fix

A reviewed-substitution register maps RI to its nearest-prior reported month. No Ledger fact is synthesized — the substitution lives at the registry-compile/gate layer, where it is auditable per release:

  • Substituted fact: cms_medicaid.month2024_11.state_enrollment.ri.total_medicaid_enrollment
  • Value: 273,400 (RI November 2024 Total Medicaid Enrollment)

Verified against the pinned ledger source: the RI 202411 final (U/Y) row reports Total Medicaid Enrollment = 273400 with no footnote (and 273,400 Medicaid + 32,761 CHIP = 306,161 total, internally consistent), while RI 202412 is the footnoted 0.

The register injects a medicaid_enrollment TargetSpec for RI, cloning an existing state spec's calibration/materialization shape and re-pointing its identity and value, stamping the substitution provenance (the CMS footnote, the substituted-for month, the tracking issue) into the spec metadata.

Wiring

  • Applied once after the target-registry compile, before the dense/sparse split, so the injected spec flows through target_specs into the materialized calibration registry that both the dense (calibrate) and sparse (calibrate_l0_refit) arms consume, and into the take-up target table.
  • Take-up diagnostics: the substitution records ride with_us_medicaid_take_up into the take-up diagnostics and the us_medicaid_take_up.json release artifact.
  • Manifest: the applied records land in build_manifest.json alongside the target-registry version that carries the injected spec's provenance metadata, and the metadata survives compilation into the calibration TargetSet (the diagnostics target surface). So release artifacts show exactly which point-in-time counts were substituted, and why.

Cannot-rot semantics

The register cannot outlive its justification. A substitution applies only while its substituted-for month is genuinely absent. The moment CMS backfills a real RI December-2024 count, the natural FIPS-44 spec reappears, the register marks the entry stale and injects nothing, and the take-up gate fails — forcing the entry's removal (the #286 stale-exclusion doctrine, applied to a substitution register).

Alternatives considered

  • T-MSIS cross-source for the same month: heavier lift, different concept basis — better as the durable fix if CMS never backfills.
  • Ship RI anchored-only with a caveat: rejected — exactly the silent-degradation class the gate exists to stop.

Tests

packages/populace-build/tests/test_us_medicaid_take_up.py::TestReviewedSubstitutionRegister:

  • test_pass_with_substitution — the gate fails closed on FIPS 44 without the register and passes once RI gains its substituted target.
  • test_fails_without_substitution_on_a_genuinely_missing_state — a genuinely-missing state with no register entry still fails, so the register is not a blanket bypass.
  • test_cannot_rot_fails_when_the_backfilled_ri_fact_appears — a backfilled RI December fact marks the entry stale, injects nothing, and fails the gate.
  • test_substitution_metadata_reaches_the_compiled_spec — the provenance survives into the compiled TargetSet the solver and diagnostics consume.
  • test_register_pins_rhode_island_to_the_ledger_value — pins the single reviewed entry against silent drift.
  • test_registry_without_medicaid_family_is_a_no_op — a registry with no medicaid_enrollment family at all records every entry inapplicable and injects nothing (the register is not the owner of Medicaid-absence; the take-up stage's empty-target guard is).

uv run pytest is clean — 1,622 passed / 54 skipped in a CI-identical venv (no us extra), and 37 passed in test_us_medicaid_take_up.py with policyengine-us installed — and ruff check / ruff format --check are clean on the changed files.

Refs #386, #334, #368.

🤖 Generated with Claude Code

MaxGhenis and others added 5 commits July 10, 2026 00:48
Apply the reviewed CMS Medicaid enrollment substitutions once after the
target-registry compile, before the dense/sparse split, so the injected
RI spec flows through target_specs into the materialized calibration
registry that both the dense (calibrate) and sparse (calibrate_l0_refit)
arms consume, into the take-up target table, and into the build manifest.

Thread the substitution records into _with_medicaid_take_up_outputs ->
with_us_medicaid_take_up so they ride the take-up diagnostics: the gate
rot-checks a backfilled (stale) substitution and the us_medicaid_take_up
release artifact records which states shipped a substituted point-in-time
count. Surface the applied records in build_manifest.json alongside the
target-registry version that carries the injected spec's provenance
metadata, so release artifacts show the substitution explicitly.

Refs #386, #334, #368.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cover the four register behaviours against faithful CMS medicaid_enrollment
state specs:

- pass-with-substitution: a registry missing RI fails the take-up gate
  closed on FIPS 44 (the Build J failure); applying the register injects
  RI's nearest-prior-month count (November 2024 = 273,400), RI gains a
  target, and the same assignment passes.
- fail-without: a genuinely-missing state with no register entry still
  fails the gate, so the register cannot become a silent blanket bypass.
- cannot-rot: when a backfilled RI December-2024 fact appears, the register
  marks the entry stale, injects nothing, and the gate fails so the
  substitution cannot outlive its justification (#286 doctrine).
- metadata-reaches-compiled-spec: the injected spec carries the full
  substitution provenance, which survives compilation into the calibration
  TargetSet the solver and diagnostics target surface consume.

Also pins the register's single reviewed entry (RI 2024-11 = 273,400,
populace#386) against silent drift.

Refs #386, #334, #368.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…caid family (#386)

The end-to-end main() ordering test drives the release tool with a synthetic
registry that carries no medicaid_enrollment state specs; the register raised
"no ... spec to derive the substituted target's calibration shape from" and
broke main() for any target surface without the CMS family (the CI failure on
all four jobs).

A registry with no medicaid_enrollment state family has nothing for the
register to stand in: record every entry applied=False, stale=False and inject
nothing, instead of raising. Medicaid-absence is owned by the take-up stage's
own empty-target guard, not the register. Covered by
test_registry_without_medicaid_family_is_a_no_op, and the main() ordering
test's _with_medicaid_take_up_outputs stub now mirrors the substitutions
kwarg.

Refs #386, #334, #368.

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