Skip to content

Carry SNAP eligibility and exemption inputs through Populace US outputs#350

Merged
MaxGhenis merged 4 commits into
mainfrom
eligibility-inputs-stage
Jul 8, 2026
Merged

Carry SNAP eligibility and exemption inputs through Populace US outputs#350
MaxGhenis merged 4 commits into
mainfrom
eligibility-inputs-stage

Conversation

@daphnehanse11

Copy link
Copy Markdown
Collaborator

Closes #244; advances #248.

Summary

Adds an eligibility_inputs source stage that carries the SNAP eligibility and exemption inputs through Populace US outputs, following the hours_worked (#293) template: manifest entry, runtime handler, frame transform, release signal gate, builder wiring, and parity-register cleanup.

Five PolicyEngine-facing person inputs, each a direct mapping of measured CPS ASEC variables — the same mappings the retired enhanced-CPS pipeline used, so parity with the retired surface is by construction:

Output Source Rule
is_disabled PEDISDRS/PEDISEAR/PEDISEYE/PEDISOUT/PEDISPHY/PEDISREM, SSI_VAL, A_AGE any difficulty item == 1, then the retired align_reported_ssi_disability rule (under-65 reported-SSI recipients are disabled)
is_blind PEDISEYE == 1
is_full_time_college_student A_HSCOL, A_FTPT college enrollment (== 2) and full-time (== 1)
own_children_in_household PEPAR1/PEPAR2 within PH_SEQ count of household members whose parent pointers resolve to the person (the retired children_per_parent construction)
veterans_benefits VET_VAL direct, floored at zero

Nothing is imputed. Without this stage every input takes its engine default (False/0), silently erasing the SNAP disability, student, parent/child, and veteran exemption channels — the #244/#248 failure mode.

Ownership notes

Gates

us_eligibility_inputs_signal_gate fails the release when any column is missing or constant, or when weighted shares leave plausibility bands: disabled share [0.05, 0.20] (ASEC six-item battery ≈ 10% of all persons), full-time college share [0.01, 0.08], parent share [0.12, 0.40], positive veterans'-payment share [0.002, 0.04]. Healing mirrors the hours stage: a surface carrying constant engine defaults is recomputed from raw columns rather than trusted.

Testing

  • New test_us_eligibility_inputs.py: 20 tests over manifest declaration, each mapping rule (including the SSI-alignment age boundary, both parent pointers, cross-household pointer isolation, negative-value flooring), frame integration (idempotence, constant-default healing, missing-raw-column errors), and gate pass/fail behavior.
  • Builder test patches the new stage alongside hours/snap; l0-refit export fixture extended with the five columns (they join US_RELEASE_REQUIRED_PERSON_SOURCE_COLUMNS).
  • Full populace-build suite passes locally.

🤖 Generated with Claude Code

Add an eligibility_inputs source stage (manifest entry, runtime handler,
frame transform, release signal gate, builder wiring) following the
hours_worked template. Direct-maps measured ASEC variables: is_disabled
(PEDIS* difficulty battery plus the retired align_reported_ssi_disability
rule), is_blind (PEDISEYE), is_full_time_college_student
(A_HSCOL/A_FTPT), own_children_in_household (PEPAR1/PEPAR2 parent
pointers within PH_SEQ), and veterans_benefits (VET_VAL). is_veteran is
formula-owned in PolicyEngine-US and deliberately not exported.
Removes the five now-produced entries from ecps_parity_known_gaps.json.
Closes #244; advances #248.

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

@MaxGhenis MaxGhenis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — column persistence verified; the "parity by construction" claim is overstated on two of five mappings

Reviewed with a column-manifest lens (does the surface actually reach the persisted H5, not just pass a weights-side gate) plus a source check on each mapping. The persistence machinery is right. Two mappings are refinements over the retired pipeline rather than reproductions of it, which is worth documenting explicitly rather than folding into a blanket parity claim.

Persistence is enforced end-to-end — this is the right fix for the #340/#244 mechanism

Confirmed all five outputs are genuine PolicyEngine-US person input variables with no formula (is_disabled, is_blind, is_full_time_college_student bool; own_children_in_household int; veterans_benefits float). Input variables with no formula fall back to their broadcast default (False/0) when absent from the H5 — exactly the #244/#340 silent-erasure path. So persistence is what makes this stage matter, and it is enforced at two independent points:

  • The stage produces the columns into the base frame (with_us_eligibility_inputsderive_us_eligibility_inputs_from_manifest), and the build tool runs us_eligibility_inputs_signal_gate, which raises RuntimeError if any column is missing/constant or its weighted share leaves the plausibility band.
  • US_ELIGIBILITY_INPUTS_NONCONSTANT_PERSON_COLUMNS (all five) is spliced into US_RELEASE_REQUIRED_PERSON_SOURCE_COLUMNS, so assert_required_us_release_source_columns(export_frame) — called in export_us_l0_refit_h5 immediately before write_dataset — hard-fails the export if any of the five is missing or constant in the frame that gets written.

That is precisely #340's requested action ("fail release when a PolicyEngine input variable consumed by any validation-suite reform/backtest is absent from the persisted H5"), and it is invisible to weights-side gates by construction. The _disabled_carries_signal idempotence guard (recompute a constant-default surface rather than trust it) is the correct handling of the healing case. Good.

Mapping sources — three are exact parity, two are refinements presented as parity

Checked each mapping against the retired enhanced-CPS cps.py:

  • is_blindPEDISEYE == 1: matches retired cps.py exactly. Parity. ✓
  • own_children_in_household: the PEPAR1/PEPAR2-within-PH_SEQ parent-pointer count matches the retired children_per_parent construction. Parity. ✓
  • veterans_benefitsmax(VET_VAL, 0): direct mapping, floored. Parity in substance. ✓
  • is_full_time_college_student(A_HSCOL == 2) & (A_FTPT == 1): the retired pipeline used A_HSCOL == 2 alone and never read A_FTPT. This is a divergence, not parity. I verified against the Census ASEC 2024 data dictionary that A_FTPT is school enrollment intensity ("Is ... enrolled in school as a full-time or part-time student"; 1 = full time, 2 = part time; universe A_ENRLW=1) — not employment — so the added filter is a legitimate refinement that makes the flag truer to its name (it correctly drops part-time college students the retired mapping swept in). But it is a behavioral change to the retired surface: PolicyEngine-US reads is_full_time_college_student in is_snap_higher_ed_student and meets_snap_parent_exception, so narrowing it moves who is subject to the SNAP higher-ed-student restriction.
  • is_disabled ← any PEDIS* == 1 OR (under-65 reported SSI_VAL > 0): the retired cps.py construction is the six-item battery alone. The added SSI alignment is attributed to a "retired align_reported_ssi_disability rule", but I couldn't find that named rule in the retired code (local clone or archived-repo code search). Either it was ported faithfully from a retired stage I couldn't locate, or it's a new (defensible) refinement — please confirm which, and cite it, rather than asserting parity.

Recommendation

The persistence design is solid and I'd be comfortable with the mappings on their merits — both refinements arguably improve accuracy. The single ask is honesty about scope: #352 documents its one pregnancy-rate divergence from the retired pipeline cleanly in the module and manifest notes; do the same for the is_full_time_college_student full-time filter and the is_disabled SSI alignment, so "parity by construction" isn't claimed for mappings that are supersets/refinements. That also gives the SNAP student-surface change an audit trail.

Tests are thorough (the full/part-time split, both SSI-alignment paths, cross-household pointer isolation, negative flooring, gate pass/fail, idempotent pass-through). CI is green on test/wheels for 3.13 and 3.14. Not merging — review only.

@MaxGhenis

Copy link
Copy Markdown
Contributor

Follow-up on the is_veteran / #24 boundary and the downstream plumbing, since both are load-bearing for this stage's purpose.

The is_veteran boundary is correct. Confirmed is_veteran is formula-owned in PolicyEngine-US (formula returns person("veterans_benefits", period) > 0), so exporting veterans_benefits (the measured input dollars) and not is_veteran is exactly right — persisting a formula-owned flag is the #24 over-export failure, because the engine would then read the stale persisted value instead of recomputing. Exporting the input that drives the formula is the correct lever.

And that lever actually reaches the work requirement. veterans_benefitsis_veteran is read directly in meets_snap_abawd_work_requirements (the pre-HR1 exemption branch: is_veteran = person("is_veteran", period)), and is_disabled is read at exemption (B) (7 U.S.C. 2015(o)(3)(B)). So the two channels this stage most affects are live in the pinned pe-us ABAWD formula, not just nominally present. Before this stage they default (False/0) and those exemptions never fire, over-stating ABAWD ineligibility — which is the concrete reason the persistence fix matters for the pre-OBBBA §10102 counterfactual becoming scoreable.

This doesn't change the review above: the persistence design is sound and the #293 template is followed cleanly. The one open item remains documenting the two mapping refinements (is_full_time_college_student's A_FTPT == 1 filter and is_disabled's SSI alignment) as deliberate divergences rather than parity.

daphnehanse11 and others added 2 commits July 8, 2026 11:37
Replace the blanket parity claim with per-mapping citations to
policyengine-us-data @ 42ed5d45c5: the A_FTPT full-time filter is the
retired primary branch (census_cps.py loads A_FTPT, so the published
surface ran the strict form; the A_HSCOL-only fallback is deliberately
not reproduced), and the SSI alignment is the retired
align_reported_ssi_disability rule from datasets/cps/takeup.py applied
in cps.py's take-up block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the retired package in stage text

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

Copy link
Copy Markdown
Collaborator Author

Thanks — the persistence-lens review caught exactly the right question, and chasing it turned up receipts: both mappings are retired-pipeline constructions, so I've replaced the blanket parity claim with per-mapping citations rather than reframing them as divergences (pushed in 0371324, plus a follow-up: the manifest's incumbent-dependency guard forbids naming the retired package in stage text, so the file/line citations live in the stage module docstring).

is_disabled SSI alignmentalign_reported_ssi_disability is defined in the retired repo at datasets/cps/takeup.py#L38-L45 (is_disabled | (reported_ssi & (ages < 65))), imported in cps.py#L33-L37 and applied to is_disabled in the take-up block at cps.py#L751-L757. History reaches back to at least 2026-03-19 ("Fix PR 607 SSI alignment"), well before the 2026-07-02 archival — it likely hid from the search because it lives in takeup.py, not cps.py.

is_full_time_college_student A_FTPT filter — the retired cps.py applies (A_HSCOL == 2) & (A_FTPT == 1) whenever A_FTPT is loaded (cps.py#L1222-L1228), and census_cps.py includes A_FTPT in the raw person load (census_cps.py#L323) — so the published surface ran the strict form, and I agree your data-dictionary read confirms it's also the semantically correct one. The one true divergence, now documented in the docstring: the retired A_HSCOL-only fallback (for loads without A_FTPT) is deliberately not reproduced — this stage requires A_FTPT and fails loudly.

CI is re-running on the citation commits.

The behavioral contract suite forbids the retired package name in build
sources outside the pinned parity-reference allowlist; cite the repo
snapshot sha and file paths instead (full permalinks live in the PR
review thread).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis merged commit 34dd3a2 into main Jul 8, 2026
4 checks passed
@daphnehanse11

Copy link
Copy Markdown
Collaborator Author

Addendum for the final state of the branch: the citation form went through one more revision — the incumbent-independence rule turns out to be enforced at two layers, and the receipts comment above only reflects the first.

  1. The source-manifest loader rejects stage JSON containing the retired package name (caught locally, fixed in 0371324 by keeping citations out of the manifest).
  2. The behavioral-contract suite (test_us_plan.py) sweeps all build source text for the literal, allowlisting only the pinned parity-reference machinery (parity_reference.py / ecps_parity_reference.json) — which is why the docstring citation failed CI even after the manifest fix.

Final form (0c3d890): the module docstring cites "the retired enhanced-CPS repo @ 42ed5d45c5" plus in-repo file paths (datasets/cps/takeup.py, cps.py's take-up block, census_cps.py's raw load list) per mapping, and the full GitHub permalinks live in the receipts comment above for the review record. All four checks green on 0c3d890.

Net diff vs what you reviewed: docstring citation wording only — no behavioral change to any mapping, gate, or test.

daphnehanse11 added a commit that referenced this pull request Jul 8, 2026
Add a snap_abawd_discretionary_exemption source stage seeding
is_snap_abawd_discretionary_exempt with stable blake2b draws across
potentially covered adults 18-64 at the 7 U.S.C. 2015(o)(6) cap rate
(8% from FY2024); the engine intersects with modeled coverage so the
exempt share among covered individuals lands at the statutory rate in
expectation. Full-cap seeding is documented as an explicit upper bound
(USDA reports usage typically runs well under the allotment; cap
exceeded in only five state-year instances across FY2016-19), with
usage-report refinement tracked under #323.

Rebuilt on main after #350/#352 merged; stage order runs
eligibility_inputs -> pregnancy -> snap_abawd_discretionary_exemption.

Addresses the SNAP half of #323; advances #248.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MaxGhenis pushed a commit that referenced this pull request Jul 8, 2026
Add a snap_abawd_discretionary_exemption source stage seeding
is_snap_abawd_discretionary_exempt with stable blake2b draws across
potentially covered adults 18-64 at the 7 U.S.C. 2015(o)(6) cap rate
(8% from FY2024); the engine intersects with modeled coverage so the
exempt share among covered individuals lands at the statutory rate in
expectation. Full-cap seeding is documented as an explicit upper bound
(USDA reports usage typically runs well under the allotment; cap
exceeded in only five state-year instances across FY2016-19), with
usage-report refinement tracked under #323.

Rebuilt on main after #350/#352 merged; stage order runs
eligibility_inputs -> pregnancy -> snap_abawd_discretionary_exemption.

Addresses the SNAP half of #323; advances #248.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Jul 9, 2026
…age smoke (#368 Deliverable 1) (#369)

* Add input-column coverage gate primitive + derived release coverage manifest (#368)

Generalizes the #350 required-source-columns mechanism to the full reference
eCPS input surface. input_column_coverage_gate() fails a required column that is
absent OR present-but-degenerate, with #286 cannot-rot reviewed exclusions.

Manifest derived from the pinned eCPS parity reference + known-gaps register:
58 required (incl. the 3 SSI countable-resource assets with NO exclusion per
#368) + 100 reviewed exclusions = the 158 populated eCPS input layers.

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

* Add release input-coverage loader/gate + reform-coverage smoke modules (#368)

release_input_coverage.py: manifest loader, us_release_input_coverage_gate over
an export frame, and assert_release_input_coverage_manifest_current anti-rot
(manifest must equal the pinned eCPS surface; SSI assets must stay required;
every declared column a live PE-US input leaf).

reform_coverage_smoke.py: us_reform_coverage_smoke_gate — a pinned bound reform
scoring ~$0 fails the release. First probe: SSI $10k/$20k asset limits.

Registered in us_runtime __init__ and country_package.json resources.

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

* Wire the input-coverage + reform-coverage smoke gates into the US release tool (#368)

Complete Deliverable 1's release wiring: the coverage gate and reform-coverage
smoke now run in tools/build_us_fiscal_refresh_release.py on BOTH the dense and
sparse default paths, aborting the release exactly like the export-mass parity
gate.

- Preflight: assert_release_input_coverage_manifest_current() runs before the
  expensive calibration, so a manifest that has drifted from the pinned eCPS
  surface (or quietly demoted the SSI assets) fails fast.
- After the calibrated export frame is built and before write_dataset:
  us_release_input_coverage_gate scores every required eCPS input column for
  presence + non-degenerate signal, writes input_coverage.json, and raises on
  failure (--allow-input-coverage-gaps is a diagnostic-only escape hatch;
  release builds leave it unset).
- After write_dataset: us_reform_coverage_smoke_gate scores the pinned bound
  reforms on the written H5 (first probe: SSI asset limits $10k/$20k), writes
  reform_coverage_smoke.json, and raises on a ~$0 bound reform.

The SSI countable-resource assets ship as hard requirements with no reviewed
exclusion, so both gates are RED on today's asset-less artifacts by design;
Deliverable 2 (asset-stage restoration) turns them green.

Also re-export input_column_coverage_gate from populace.build to match every
sibling gate, and drop an unused import the loader carried.

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

* Stop the coverage manifest naming the retired data package (#368)

The generated coverage manifest copied the eCPS parity reference's full source
block into its own "reference" field, which carried the retired data package's
repo id. The manifest is not on the incumbent-reference allow-list, so that
tripped the live-tree guard (test_us_plan.test_no_incumbent_data_package_
references_in_live_tree) and would have failed CI.

Nothing reads the manifest's reference field — the gate and the anti-rot check
derive the column surface from ecps_parity_reference.json directly — so the
generator now records only the sha-locked coordinates that do not name a package
(filename, content sha256, revision, vintage, period) plus a derived_from
pointer to that allow-listed parity reference. Column set is unchanged (58
required incl. the 3 SSI assets, 100 reviewed exclusions).

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

* Add release input-coverage + reform-coverage smoke gate tests (#368)

The five acceptance cases from the brief, plus red-by-design and anti-drift
guards, all isolated from policyengine-us (real Frame, a stub engine exposing
only default_values, an injected simulate, and a monkeypatched reform builder):

- full required set present with signal passes;
- a missing required column fails, named;
- a required column present but all-engine-default fails without an exclusion;
- a reviewed exclusion whose column has caught up (present with signal) is stale
  and fails (#286 cannot-rot);
- a bound reform scoring ~$0 fails the reform-coverage smoke (and a nonzero
  effect passes; a probe-less gate is refused).

Shipped-manifest guards: the SSI assets stay required with no exclusion, the SSI
probe binds through them, demoting an asset to a reviewed exclusion is rejected,
and the committed manifest matches regeneration and names no retired data
package — so neither the manifest nor the generator can silently rot.

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

* Merge origin/main; regenerate coverage manifest against advanced gaps register (65 required / 93 exclusions / 1 probe) (#368)

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.

Carry SNAP eligibility and exemption inputs through Populace US outputs

2 participants