Skip to content

Require state-level SNAP benefit hard targets#256

Open
daphnehanse11 wants to merge 2 commits into
mainfrom
snap-state-hard-targets
Open

Require state-level SNAP benefit hard targets#256
daphnehanse11 wants to merge 2 commits into
mainfrom
snap-state-hard-targets

Conversation

@daphnehanse11

Copy link
Copy Markdown
Collaborator

Summary

Closes #255.

SNAP currently calibrates to a single national hard target. This PR makes the 51 state-level SNAP benefit totals (50 states + DC) a required part of the US target profile, so a build can no longer silently ship national-only SNAP calibration.

Why the change is this small

Investigation for the 2026-07-01 SNAP research readout found the pipeline is already almost fully wired:

  • The Ledger package usda-snap-fy69-to-current already ships per-state total_benefits facts (usda_snap.fy2024.state_benefits.<region> record sets, geography_level: state, all 53 state agencies).
  • _direct_reference_from_fact already accepts state-geography facts for mapped families and stamps state_fips metadata — the same path per-state TANF targets use today.
  • The release builder already materializes state_fips-scoped targets by masking households to the state.
  • Territories (GU/VI) resolve no PolicyEngine state FIPS and are skipped automatically.

So with a current consumer_facts.jsonl, state SNAP hard targets activate with zero mapping changes. The only gap was enforcement.

Changes

  • gates.py: add required_metadata_keys to TargetCoverageRequirement — presence-of-key matching, so a requirement can demand state-dimensioned rows (state_fips present) without enumerating 50 FIPS values. Validated non-empty in __post_init__.
  • fiscal_targets.py: add the snap_state_benefits coverage requirement (family usda_snap, role snap_total, state_fips key required, min_matches=51).
  • Tests: gate-level tests for required_metadata_keys (present/absent/empty-key rejection); compile test proving a state-level usda_snap fact becomes a state_fips-scoped spec while a Guam row is skipped; profile tests proving 50 state rows or a national-only surface fail the gate; state SNAP rows added to the complete-profile fixture.

Draft status / rollout note

This gate makes builds fail if the consumer-facts artifact lacks the state SNAP record sets. Before marking ready:

  • Confirm the production consumer_facts.jsonl includes the state_benefits record sets (the source package parses them from the FNS FY24 workbook, but the deployed artifact vintage needs checking).
  • Run a fiscal refresh build against a current artifact and review the per-state SNAP calibration diagnostics (zero-support states would need reviewed exclusions, TN-style).

Follow-up (not in this PR)

State participation counts (average_monthly_households/average_monthly_persons) as indicator targets — needs an explicit average-monthly vs annual-ever bridge before becoming a hard constraint.

Test plan

  • uv run pytest packages/populace-build/tests — full suite passes (269 tests).
  • ruff check / ruff format --check clean on changed files.

🤖 Generated with Claude Code

@daphnehanse11 daphnehanse11 force-pushed the snap-state-hard-targets branch from 42d651f to 19f77c6 Compare July 2, 2026 14:17
@daphnehanse11 daphnehanse11 marked this pull request as ready for review July 2, 2026 16:53
@daphnehanse11

Copy link
Copy Markdown
Collaborator Author

@PavelMakarchuk one open item for merge timing (not code review): this gate makes the 51 state SNAP rows mandatory, so it should land only once the production consumer_facts.jsonl is confirmed to include the usda_snap.fy2024.state_benefits.* record sets (they're in the Ledger source package, but I can't check the deployed artifact vintage from my side). One-line check against the artifact:

grep -c 'state_benefits' consumer_facts.jsonl   # expect >= 51

If the artifact predates the state record sets, merging this will (correctly) fail the next build until the artifact is refreshed — that's the gate doing its job, but better to sequence it deliberately.

@MaxGhenis

Copy link
Copy Markdown
Contributor

Independent review verdict: the mechanism is sound (presence-of-key matching validated non-empty; state SNAP already maps to an active hard target on main, so this enforces an existing surface; orthogonal to #245's CD posture) and local gate/target tests pass. Two things block merge, matching the PR's own unchecked boxes: (1) run one fiscal-refresh build on a current artifact to confirm the deployed consumer_facts carries the state_benefits record sets, and (2) record any zero-support states as reviewed exclusions (TN-style). With per-state diagnostics from that run attached, this is merge-ready.

@MaxGhenis

Copy link
Copy Markdown
Contributor

Gate-review evidence (independent review pass, 2026-07-06):

Code review: clean. The required_metadata_keys mechanism is presence-only as claimed (gates.py:297), validation rejects empty keys, and all touched suites pass locally (test_gates.py 84/84, test_us_fiscal_targets.py 86/86, spec-only enforcement 5/5, full package green, ruff clean).

Your first unresolved checklist item is now resolved with evidence: the current curated feed (the v6 surface used in tonight's Build F staging run) carries 53 state-level usda_snap total_benefits rows — 50 states + DC + GU/VI, with the territories dropping at reference compilation via the pre-existing _state_fips skip — so the compiled registry meets min_matches=51.

Second checklist item lands tomorrow: tonight's Build F staging run produces exactly the per-state SNAP calibration diagnostics you flagged as needed. Merge sequencing: once those diagnostics confirm no zero-support state (the TN-style case), this merges.

One design note for a follow-up (non-blocking): the failure message reports only the count ("has 50 match(es), needs 51") without naming the missing state, and the only exemption mechanism is requirement-wide — a per-state diagnostic in the failure line would make future breakage self-explaining.

@daphnehanse11 daphnehanse11 force-pushed the snap-state-hard-targets branch from e5ec306 to 9ce73ed Compare July 8, 2026 16:38
@daphnehanse11 daphnehanse11 requested a review from MaxGhenis July 8, 2026 16:38

@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 — still valuable; do not close. Needs a rebase and its two rollout items resolved before it's merge-ready.

I checked whether the merged SNAP train (#350/#352/#353) or the state suites (#345/#319) already cover this. They don't:

  • SNAP train (#350/#352/#353) is entirely about SNAP input columns — carrying eligibility/exemption inputs through outputs, seeding pregnancy, seeding ABAWD exemptions. Nothing about calibration-target coverage.
  • State suites (#345 7 state-program benchmarks + EITC-by-state cross-check; #319 legislative-reform validation) are backtest/validation suites, not target-profile coverage requirements.
  • Current main has neither piece of this PR: gates.py has no required_metadata_keys (only key-value required_metadata, line ~357), and fiscal_targets.py has no snap_state_benefits requirement — SNAP is still enforced only at the national snap_total. A build can still silently ship national-only SNAP calibration, which is exactly the gap this PR closes (#255).

The mechanism is genuinely needed, not redundant with the existing state coverage. The existing state_income_tax requirement gets away with bare min_matches=44 because that family has no national row. SNAP is different: the usda_snap/target_role=snap_total family carries both a national row and the per-state rows, so a plain count would let the national row (or a partial state surface) pass. required_metadata_keys=("state_fips",) is the right primitive — it demands state-dimensioned rows (state_fips present) without enumerating 50 FIPS values, and the matching logic is correct (AND of value-matches then key-presence; __post_init__ rejects empty keys). The two profile tests pin exactly the failure modes: a national row and a 50-of-51 surface both fail. min_matches=51 is right (50 states + DC; GU FIPS 66 / VI resolve no PE state FIPS and are skipped — the compile test proves CA→06 maps and Guam is dropped).

What remains before this can go ready (the PR's own checkboxes, still open, and correct):

  1. Rebase — CONFLICTING/DIRTY. fiscal_targets.py now carries ~30 TargetCoverageRequirement blocks and has moved a lot since this branch's base; the changes here are additive (one dataclass field, one match clause, one requirement), so the rebase should be mechanical, but it's required.
  2. Confirm the deployed consumer_facts.jsonl carries the usda_snap.fy2024.state_benefits.* record sets. This gate makes the build fail if they're absent, so this is a hard data dependency (same class as #334's Ledger-fact dependency) — merging it against an artifact that lacks the state rows would red the release build.
  3. Run a fiscal-refresh build and review per-state SNAP diagnostics — any zero-support state needs a reviewed exclusion (the TN pattern) rather than a gate failure.

The deferred follow-up (state participation counts as indicator targets, pending an average-monthly vs annual-ever bridge) is correctly out of scope here.

Recommendation: keep open and pursue — rebase, tick the two data/rollout boxes, then it's a clean, self-contained hardening. Not closing (and not merging) — review-only per request.

daphnehanse11 and others added 2 commits July 9, 2026 09:45
The Ledger's usda-snap-fy69-to-current package ships per-state
total_benefits facts, and the direct-reference path already compiles
state-geography usda_snap rows into state_fips-scoped hard targets.
Nothing required them, though: a build consuming a facts artifact that
predates the state record sets silently ships national-only SNAP
calibration.

Add a snap_state_benefits coverage requirement (50 states + DC) so the
target-profile gate fails loudly when the compiled registry lacks the
state SNAP surface, and extend TargetCoverageRequirement with
required_metadata_keys so a requirement can demand state-dimensioned
rows by metadata-key presence instead of enumerating FIPS values.
Territory rows (GU, VI) resolve no PolicyEngine state FIPS and are
skipped at reference compilation, so they never count toward the gate.

Closes #255

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Main's profile-coverage tests (added since this branch's base) assert a
complete target surface passes the requirements; the new
snap_state_benefits requirement makes 51 state-dimensioned SNAP rows
part of completeness, so the fixtures gain complete_snap_state_rows().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@daphnehanse11 daphnehanse11 force-pushed the snap-state-hard-targets branch from 9ce73ed to e1e0911 Compare July 9, 2026 13:51
@daphnehanse11

Copy link
Copy Markdown
Collaborator Author

Addressing the three items:

1. Rebase — done (e1e0911, CI running). The rebase surfaced one real interaction beyond the mechanical conflict: the complete-profile coverage tests that landed with the Build G work (test_soi_income_tax_liability_satisfies_total_tax and its seven siblings) synthesize a complete target surface and assert it passes — so snap_state_benefits immediately made 51 state-dimensioned SNAP rows part of "complete." Added complete_snap_state_rows() to the fixtures at all eight sites, mirroring complete_state_income_tax_rows. Full suite green.

2. Deployed-artifact confirmation — verified at the Ledger declaration level; the artifact-pin check remains. The usda_snap fy69-to-current source package in PolicyEngine/ledger declares the usda_snap.fy2024.state_benefits.{nero,maro,sero,mwro,swro,mpro,wro} record sets with per-state rows — 53 total: 50 states + DC + Guam (wro) + USVI (nero), which independently grounds both min_matches=51 and the requirement's territory-skip note. The state-level ETL (db/etl_snap.py, snap_states strata from the SNAP State Summary with state_fips constraints) produces exactly the rows this gate demands. What I could not check from here is the deployed consumer_facts.jsonl behind the current pin — that needs someone with artifact access to grep it for usda_snap.fy2024.state_benefits. before merge, per your point that this gate makes absence a build-red.

3. Per-state diagnostics / zero-support exclusions — open, needs a build run. Noting that the Build G per-run extra_support_exclusions mechanism (which landed in the same wave) is exactly the right vehicle if any state shows zero support: a reviewed, reason'd, per-artifact exclusion rather than a registry edit — arguably cleaner than the standing TN pattern for what may be artifact-specific gaps.

So: item 1 closed, item 2 four-fifths closed (declaration + ETL verified; pin grep outstanding), item 3 gated on the next fiscal-refresh run.

@daphnehanse11 daphnehanse11 requested a review from MaxGhenis July 9, 2026 17:49
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.

Add state-level SNAP benefit hard targets from USDA FNS administrative data

2 participants