Assign SNAP take-up from reported receipt and the FNS participation rate#294
Conversation
The two data-seeded take-up programs whose participation rate clears the provenance bar, assigned by calibrated Bernoulli at the administrative rate: - TANF (takes_up_tanf_if_eligible, spm_unit): 21.9% (HHS ASPE 24th Welfare Indicators Report, 2022, Table 10 Indicator 4). - EITC (takes_up_eitc, tax_unit): per-child rates (IRS National Taxpayer Advocate 2020, TY2016: 0=65%, 1=86%, 2=85%, 3+=82%). Child count is approximated from tax-unit member ages under 19; the rate is nearly flat above zero children so the coarse count picks the right bin. with_us_take_up_inputs seeds every program the take-up contract marks `seed` (and only those), keying draws on stable source identity so support-channel clones agree and reruns are bit-reproducible (the SNAP #294 keying). A frame already carrying a non-constant column passes through untouched; a missing or constant column is recomputed, so the published all-True landmine is healed. No reported-receipt column is threaded through the base spine, so assignment is calibrated Bernoulli rather than the SNAP reported-receipt anchor. us_take_up_summary and us_take_up_signal_gate expose the per-program participation-vs-administrative surface: weighted share, target rate, source, and a plausibility band, failing on a missing/constant column or an out-of-band share. Validated at n=8000: TANF share 0.210 (target 0.219); EITC per-bin 0.648/0.866/0.842/0.832 (targets 0.65/0.86/0.85/0.82); EITC overall 0.797 (IRS ~0.78). 17 seeding tests plus the gate failure modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… take-up contract (#312) (#315) * Add engine-asserted take-up contract inventory (#312 step 1) The blocking first deliverable: a checked-in table classifying every policyengine-us take-up flag, asserted against the installed engine. - PolicyEngineUSEngine.take_up_variables() / take_up_contract() derive, from engine metadata, each takes_up_* flag's entity, default, whether the engine computes it (formula / adds / start-date formula), its consumers, and thus an engine_class (model_simulated / data_seeded / dead). Same metadata-derivation doctrine as the #301 formula-owned guard. - us/take_up_contract.json records the engine facts plus the curated populace treatment (seed / rate_unsourced / model_simulated / out_of_scope / near_universal) with each rate's administrative provenance. - assert_take_up_contract_current() fails the build when the table drifts from the pinned engine (new/renamed flag, a flag gaining a formula, a changed default); assert_take_up_treatments_consistent() catches treatments that contradict the engine class. The loader enforces the provenance rule: a program marked seed must carry a sourced administrative rate. Finding against pinned policyengine-us 1.752.2: all 13 take-up flags are data_seeded (default True, no formula, all consumed); there are no *_seed variables and no model-simulated take-up. The chip_take_up_seed / aca_take_up_seed model-side migration the issue references has not landed in the pinned engine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Seed TANF and EITC take-up flags from administrative rates (#312 step 2) The two data-seeded take-up programs whose participation rate clears the provenance bar, assigned by calibrated Bernoulli at the administrative rate: - TANF (takes_up_tanf_if_eligible, spm_unit): 21.9% (HHS ASPE 24th Welfare Indicators Report, 2022, Table 10 Indicator 4). - EITC (takes_up_eitc, tax_unit): per-child rates (IRS National Taxpayer Advocate 2020, TY2016: 0=65%, 1=86%, 2=85%, 3+=82%). Child count is approximated from tax-unit member ages under 19; the rate is nearly flat above zero children so the coarse count picks the right bin. with_us_take_up_inputs seeds every program the take-up contract marks `seed` (and only those), keying draws on stable source identity so support-channel clones agree and reruns are bit-reproducible (the SNAP #294 keying). A frame already carrying a non-constant column passes through untouched; a missing or constant column is recomputed, so the published all-True landmine is healed. No reported-receipt column is threaded through the base spine, so assignment is calibrated Bernoulli rather than the SNAP reported-receipt anchor. us_take_up_summary and us_take_up_signal_gate expose the per-program participation-vs-administrative surface: weighted share, target rate, source, and a plausibility band, failing on a missing/constant column or an out-of-band share. Validated at n=8000: TANF share 0.210 (target 0.219); EITC per-bin 0.648/0.866/0.842/0.832 (targets 0.65/0.86/0.85/0.82); EITC overall 0.797 (IRS ~0.78). 17 seeding tests plus the gate failure modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * WIP checkpoint: agent stopped mid-task; lead salvaged state Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * WIP checkpoint: agent killed (session limit / stop); lead salvaged Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Register take-up contract resource; fake take-up stages in the builder test; diagnostics test class Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Sort imports (ruff --fix) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
751491d to
5c9f905
Compare
|
Rebased onto current main ( Conflict resolutions were the same 11 additive anchor points as #293 — main's TANF/EITC take-up (#315) and eCPS-parity (#316) wiring interleaved with this PR's SNAP wiring. In Intended changes beyond conflict resolution:
Verified the rebased net diff against the original with interdiff — no other semantic drift. Full Note for merge order with #293: both PRs insert at the same manifest/builder anchor points; whichever merges second gets the same-shape trivial conflict both descriptions anticipate. 🤖 Generated with Claude Code |
|
Heads-up from the medicaid take-up workstream (#334/#337): once #337 lands, parity_gate fails on stale known-gap exemptions — any register entry whose layer the candidate now populates. This branch's SNAP stage will make |
MaxGhenis
left a comment
There was a problem hiding this comment.
Approving (agent-assisted review; I verified the load-bearing pieces). The logic is right and the tests are thorough — reported-receipt anchor, weighted fill to the FNS rate, blake2b seeding keyed on source identity for clone-consistency, constant-True healing, and a gate that mirrors the immigration one. 0.82 checks out as the FY2018 FNS national participation rate.
One thing I fixed on the branch rather than bouncing back: this stage makes takes_up_snap_if_eligible a produced layer, but the parity register still exempted it with reason "Populace does not yet produce it." #337 removes the four entries rotted by already-merged work but not this one (it only rots once this merges), and because SNAP stays out_of_scope, it sits outside #337's seeded_take_up_programs() static guard — so the failure would only surface on the first release build after both land. I pushed a commit deleting the entry here; it's safe under current CI (the register only shrinks, nothing asserts presence) and under the pre-#337 gate (the candidate populates the column). Same pattern applies to #293's hours entries — cleanest if each producing PR removes its own register line.
Two nits, neither blocking:
- The manifest labels the FNS artifact vintage "latest_available" but 0.82 is FY2018 (latest published is FY2022 at 88%) — either pin the vintage to FY2018 in the notes or refresh the rate.
- The fill target is computed on person "first" weights per SPM unit while the gate measures on
resolve_weights("spm_unit"); they coincide today (0.821 achieved vs 0.820 target) but computing the target on the spm_unit basis would keep them locked together if weight resolution ever changes.
Max's review of #294 (SNAP take-up) spotted that the static guard covered immigration outputs plus seeded_take_up_programs() only, so a program whose stage produces its flag under a different contract treatment sat outside it — SNAP (out_of_scope) was hand-fixed on #294, and count_calibrated programs (Medicaid in #334, CHIP next per the contract followup) had the same exposure: the staleness failure would only surface on the first release build, not in CI. The guard now derives the produced set from contract treatments ({seed, count_calibrated}) instead of the seeded helper, making it the CI enforcement of the rule that each producing PR removes its own register line. Keyed on the treatment string rather than #334's count_calibrated_take_up_programs() helper so the test imports nothing that does not exist on main yet; it picks up the Medicaid flip (and any future CHIP flip) automatically at merge, in either merge order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Max's #294 review pattern (he independently audited the FNS rate's vintage against newer publications) says the sharpest domain question on this stage is the one thing left implicit: the ASEC anchor is interview-point (~Feb-Apr) while the CMS target is the December snapshot. Record in the module docstring and the contract calibration block that both are point-in-time stocks with deliberately unaligned months, and that the gate's anchor-floor rule absorbs the declining-2024 direction (spring reporters exceeding a December count) rather than reading it as a calibration miss. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a snap_take_up source stage (manifest entry, runtime handler, frame transform, release gate, builder wiring) following the immigration and hours templates. Reported SPM_SNAPSUB recipients always take up; non-reporters fill via seeded stable-identity draws to the cited FNS 0.82 participation rate. Heals the published all-True landmine. Closes #243; second half of #248 alongside #293.
…duces it
The register reason ('Populace does not yet produce it') becomes false once
this PR's SNAP stage populates the layer, and the entry would fail the
stale-exemption check when #337 lands. #337 removes the four entries rotted
by already-merged work; each producing PR removes its own.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
85f331d to
d2f2fa2
Compare
Max's #294 review pattern (he independently audited the FNS rate's vintage against newer publications) says the sharpest domain question on this stage is the one thing left implicit: the ASEC anchor is interview-point (~Feb-Apr) while the CMS target is the December snapshot. Record in the module docstring and the contract calibration block that both are point-in-time stocks with deliberately unaligned months, and that the gate's anchor-floor rule absorbs the declining-2024 direction (spring reporters exceeding a December count) rather than reading it as a calibration miss. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Max's review of #294 (SNAP take-up) spotted that the static guard covered immigration outputs plus seeded_take_up_programs() only, so a program whose stage produces its flag under a different contract treatment sat outside it — SNAP (out_of_scope) was hand-fixed on #294, and count_calibrated programs (Medicaid in #334, CHIP next per the contract followup) had the same exposure: the staleness failure would only surface on the first release build, not in CI. The guard now derives the produced set from contract treatments ({seed, count_calibrated}) instead of the seeded helper, making it the CI enforcement of the rule that each producing PR removes its own register line. Keyed on the treatment string rather than #334's count_calibrated_take_up_programs() helper so the test imports nothing that does not exist on main yet; it picks up the Medicaid flip (and any future CHIP flip) automatically at merge, in either merge order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ies (#337) * Fail the eCPS parity gate on stale exemptions; remove four rotted entries parity_gate skipped known-gap names unconditionally, unlike its sibling gates (default_valued_columns_gate at gates.py:707 and source_stage_input_coverage_gate at gates.py:1499), which both fail on stale reviewed exclusions. That let register entries rot silently once the candidate started producing the exempted layer (the takes_up_medicaid_if_eligible case surfaced in PR #334). The gate now fails on any exemption whose candidate nonzero share is positive, reports exemptions the reference never populates as dormant, and records both in the gate details. Auditing the register against the current release pipeline found four entries stale on arrival — the pipeline deterministically materializes them before the gate runs: takes_up_tanf_if_eligible and takes_up_eitc (contract-seeded take-up, PR #315), ssn_card_type and immigration_status_str (immigration stage, #266). A new register test pins that statically-knowable subset so a stale entry is caught at test time, not first at release-build time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Extend the register staleness guard to count-calibrated take-up programs Max's review of #294 (SNAP take-up) spotted that the static guard covered immigration outputs plus seeded_take_up_programs() only, so a program whose stage produces its flag under a different contract treatment sat outside it — SNAP (out_of_scope) was hand-fixed on #294, and count_calibrated programs (Medicaid in #334, CHIP next per the contract followup) had the same exposure: the staleness failure would only surface on the first release build, not in CI. The guard now derives the produced set from contract treatments ({seed, count_calibrated}) instead of the seeded helper, making it the CI enforcement of the rule that each producing PR removes its own register line. Keyed on the treatment string rather than #334's count_calibrated_take_up_programs() helper so the test imports nothing that does not exist on main yet; it picks up the Medicaid flip (and any future CHIP flip) automatically at merge, in either merge order. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Remove the two hours-stage register entries #293 left behind; widen the guard The #293 hours stage merged without removing its parity register exemptions, so hours_worked_last_week and weekly_hours_worked_before_lsr carried false "not yet on the candidate frame" reasons — the stage runs before the parity gate and its signal gate requires both non-constant, so the first release build after the staleness enforcement lands would have failed on them. Both confirmed as engine input variables (its third output, weeks_worked, has no register entry). hourly_wage stays: it is an org_wages output and that stage is not executed anywhere yet. The static register guard now also pins the deterministic stage output constants (hours, SNAP take-up, eligibility inputs) alongside the immigration outputs and contract-produced take-up flags, so the next producing stage that forgets its register line fails in CI rather than on a release build — #293 slipped through exactly because the guard did not know stage outputs, only take-up programs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…ate snapshots (#334) * Materialize Medicaid take-up via anchored count-calibration to CMS state snapshots Add a count_calibrated take-up contract treatment (no administrative participation rate exists for Medicaid, but administrative enrollment counts do) and a medicaid_take_up source stage following the native ACA pattern: eligible reporters of has_medicaid_health_coverage_at_interview always take up, everyone else draws at an in-build state fill rate, and the assignment is greedily calibrated to CMS December 2024 state enrollment snapshots among eligible non-anchored persons. Point-in-time (average-month) semantics per #332, HIPSM precedent recorded in the contract doctrine. Builder wiring mirrors the ACA stage: batched engine simulation materializes person-level is_medicaid_eligible, the medicaid_enrollment target role compiles the state target table, and a release gate fails on the all-True landmine while classifying saturated states (CMS count at or above modeled eligible weight) as the eligibility-undercount symptom they are. Removes takes_up_medicaid_if_eligible from the reviewed degenerate-input exclusions; heals the #170 enrollment==eligibility degeneracy and stops weight calibration from absorbing take-up error into the eligible population. CHIP is deliberately deferred behind the #321 M-CHIP/separate-CHIP ledger concept split; its contract entry records that dependency. Closes #331; doctrine half of #332. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Harden the Medicaid take-up gate and builder wiring from adversarial review Findings from an 8-angle review with per-finding verification; the confirmed defects and their fixes: - Bump TARGET_FRAME_CHECKPOINT_MATERIALIZER_VERSION to 2: the checkpoint identity hashes the on-disk base dataset, not the staged frame, so a pre-stage checkpoint would silently calibrate weights against all-True medicaid_enrolled columns while the export ships the new flag. - Rework the gate landmine check: full enrollment explained by anchor mass or by a floor within granularity (2% or one person weight) of eligibility is a legitimate calibrated outcome, not a spurious build abort; the count-miss check gains the same granularity allowance. - Make anchor preservation a person-level invariant checked in every state, saturated or not (anchored_not_taking_up_count in diagnostics); aggregate comparisons cannot see a dropped anchor once calibration back-fills to the count. - Delete the now-false eCPS parity exemption for takes_up_medicaid_if_eligible (parity_gate has no staleness check, so the stale entry would exempt the layer forever); the anchored column clears the presence-based gate on its own. - Refuse duplicate per-state target rows in the builder table: the calibrate op is last-row-wins while rate/diagnostics/gate sum them. - Re-check count-calibrated columns on the export frame: a downstream transform dropping the column can no longer ship the engine-default landmine with only an observational JSON field recording it. - Keep the debt-ledger pointer (scope_owner/followup) on count_calibrated participation rows and label the share's universe (off-domain propensity included) so it cannot be read as enrollment. - Refuse empty state targets up front; record weights_basis and max_person_weight in diagnostics; reuse take_up's stable-draw keying and Frame.broadcast instead of private near-copies; drop dead knobs. Refuted in verification (no change): NaN targets cannot reach the stage (ledger compilation enforces math.isfinite at both entry points). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Rename the module state-FIPS normalizer; refuse missing values The module helper shared a name with the builder's _state_fips_text but not its semantics (regex strip vs int coercion; silent 'nan' strings vs raising) — the same-name trap the #334 review flagged. Rename to _normalize_state_fips and refuse NaN input rather than letting a missing code surface later as a confusing states-without-targets gate failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Document the anchor-month vs snapshot-month offset explicitly Max's #294 review pattern (he independently audited the FNS rate's vintage against newer publications) says the sharpest domain question on this stage is the one thing left implicit: the ASEC anchor is interview-point (~Feb-Apr) while the CMS target is the December snapshot. Record in the module docstring and the contract calibration block that both are point-in-time stocks with deliberately unaligned months, and that the gate's anchor-floor rule absorbs the declining-2024 direction (spring reporters exceeding a December count) rather than reading it as a calibration miss. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fail the take-up gate when a state has a CMS count but zero eligible weight A state whose eligibility feed collapsed to zero classifies as saturated (any positive count exceeds zero eligible weight), which skipped the count checks and would ship anchored-only enrollment silently. Treat that saturation as a division artifact and fail the gate instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
Closes #243. Second half of the SNAP work-requirements input surface (#248), alongside #293 (hours).
The published dataset ships
takes_up_snap_if_eligibleconstantTrue(or omits it — same engine default), so PolicyEngine-US pays SNAP to 100% of eligible units. FNS measures participation among the eligible at ~82%. Universal take-up misstates who receives SNAP and overstates the reach of any eligibility-side reform.This adds a
snap_take_upsource stage following the #266/#293 template: manifest entry + shared runtime handler + frame transform + release gate + builder wiring.Semantics (identical to the retired enhanced-CPS pipeline)
SPM_SNAPSUBreported receiving SNAP; survey measurement wins unconditionally.Draws are blake2b hashes keyed by stable source identity (
source_year/source_household_id/minsource_person_id), so support-channel clones of one source unit always agree and reruns are bit-reproducible. The rate lives in the manifest with its FNS citation — a rate without a citation refuses to run.Healing + gate
snap_take_up_signal): column nonconstant, every reporting unit takes up (anchor preserved), weighted share within [0.70, 0.95]. Threaded through release gate failures, calibration diagnostics, and both manifests, mirroring the immigration and hours gates.Verification
Follow-up (not in this PR)
State-calibrated take-up (greedy fill against FNS state household counts, the
calibrate_binary_assignmentmachinery) is deliberately deferred: FNS state counts are average-monthly while the flag is annual-ever, and that bridge deserves its own reviewed decision — see the discussion on #292. National-rate + reported-anchor matches the retired pipeline's published behavior.Merge-order notes
takes_up_snap_if_eligiblereviewed exclusion in Gate releases on input columns stuck at the engine default #286's degenerate-input gate becomes stale and should be removed.🤖 Generated with Claude Code