Skip to content

Add pre-OBBBA SNAP ABAWD work requirements counterfactual reform#8944

Closed
daphnehanse11 wants to merge 12 commits into
PolicyEngine:mainfrom
daphnehanse11:snap-pre-obbba-work-requirement
Closed

Add pre-OBBBA SNAP ABAWD work requirements counterfactual reform#8944
daphnehanse11 wants to merge 12 commits into
PolicyEngine:mainfrom
daphnehanse11:snap-pre-obbba-work-requirement

Conversation

@daphnehanse11

@daphnehanse11 daphnehanse11 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Fixes #8943

Summary

Adds a contributed counterfactual-baseline reform modeling 2026/2027 SNAP rules as if the OBBBA (P.L. 119-21, §10102) ABAWD work requirement exemption and age provisions had not passed.

Since the OBBBA provisions are now encoded in the current-law baseline, this provides the inverse lever of the pre-passage gov/contrib/reconciliation/snap_abawd_work_requirement reform: toggling gov.contrib.snap.pre_obbba_abawd_work_requirements.in_effect restores prior law against the post-OBBBA baseline.

Implementation

The baseline ABAWD formulas (meets_snap_abawd_work_requirements, meets_snap_work_requirements) already branch on the person-level is_snap_abawd_hr1_in_effect variable, using a parameters("2025-06-01") pre-HR1 snapshot for the prior-law path. The reform overrides is_snap_abawd_hr1_in_effect with a formula that returns false for periods where the contrib toggle is true and otherwise mirrors the baseline logic, restoring in every state (including CA/HI/AK delayed adoption):

  • exempt ages 18–54 subject / 55+ exempt (vs 18–64 subject post-OBBBA)
  • dependent-child age threshold of 18 (vs 14)
  • homeless, veteran, and former foster youth exemptions (removed by OBBBA)

The per-period override (rather than neutralize_variable) ensures a future-dated toggle does not apply retroactively to earlier simulated years. Follows the standard contrib factory pattern (in_effect toggle, 5-year lookahead, module-level bypass=True instance, registration in reforms.py).

Review round (multi-angle, findings addressed here)

An 8-angle adversarial review surfaced and this PR addresses:

  1. Retroactivity bug (fixed): the original neutralize_variable approach ignored the toggle's start date — a toggle dated 2028 reverted 2026 results. Now period-gated and covered by a test.
  2. Medicaid interaction (documented + tested): medicaid_community_engagement_pass_through_eligible reads is_snap_abawd_hr1_in_effect, so the pass-through follows the counterfactual SNAP rules. This is the intended behavior (in a no-OBBBA-SNAP-WR world, the Medicaid pass-through references actual SNAP rules); documented in the module docstring and covered by a test (57-year-old work-program participant flips eligible).
  3. Scope narrowed (documented): OBBBA §10102 also rewrote area waivers; post-OBBBA waiver geography (waived_county_fips) is not reverted. Parameter description, changelog, and docstring now claim only the exemption and age provisions.
  4. Data-layer caveat (documented): discretionary exemption flags are assigned at microdata construction among the post-OBBBA covered population and are not re-drawn under the counterfactual.
  5. FRA sunset caveat (documented): the pre-OBBBA path is a frozen 2025-06 snapshot, so the FRA's scheduled 2030-10-01 sunset is not modeled; the reform targets pre-2030 windows (e.g., 2026/2027).
  6. Bypass fragility (mitigated): the baseline is_snap_abawd_hr1_in_effect now documents that it must remain the sole access point for the underlying federal/state parameters.

Verification

  • 12 YAML tests pass (tests/policy/contrib/snap/pre_obbba_abawd_work_requirements.yaml): age exemption in 2026/2027, non-exempt control, parent of a 15-year-old, homeless/veteran/former-foster-youth exemptions, CA after delayed adoption, period gating (reform applied, toggle off ⇒ current law), baseline-contrast cases (no reform ⇒ post-OBBBA rules), and the Medicaid pass-through interaction. 5 pytest unit tests cover the autoloader (bypass, default-inactive, and the 5-year lookahead window edges). Federal cases pin state_code: TX since the default state (CA) is pre-HR1 through 2026-05 in baseline.
  • All 111 existing baseline work-requirement and Medicaid CE pass-through tests pass.
  • End-to-end via structural registration: TX 60-year-old non-worker in 2026-01 is ineligible in baseline, eligible under the reform; a 2028-dated toggle leaves 2026 at current law.

Microsimulation note

On the default CPS-based dataset, this reform (and the baseline OBBBA work requirement itself) produces no population-level SNAP delta, because weekly_hours_worked_before_lsr defaults to 40 for every person when the dataset lacks hours data — everyone trivially satisfies the 20-hour ABAWD test. Population-level analysis of SNAP work requirements requires microdata with real hours and exemption assignment (tracked under the #8820 data work / PolicyEngine/populace#24).

🤖 Generated with Claude Code

daphnehanse11 and others added 2 commits July 6, 2026 15:38
Models 2026/2027 SNAP rules as if the P.L. 119-21 (OBBBA) Section 10102
ABAWD work requirement changes had not passed, by neutralizing
is_snap_abawd_hr1_in_effect so the baseline formulas take their
pre-HR1 snapshot path in every state.

Fixes PolicyEngine#8943

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Replace neutralize_variable with a per-period formula override so a
  future-dated toggle no longer applies pre-OBBBA rules retroactively
  (verified: 2028-dated toggle leaves 2026 at current law).
- Document scope and limitations in the reform module: waiver geography
  and data-assigned discretionary exemptions are not reverted, the
  FRA 2030 sunset is not modeled, and the Medicaid community engagement
  pass-through intentionally follows the counterfactual SNAP rules.
- Narrow the parameter description and changelog to the Section 10102
  exemption and age provisions.
- Add tests: period gating (toggle off => current law) and the Medicaid
  pass-through interaction (57-year-old work-program participant).
- Note on the baseline is_snap_abawd_hr1_in_effect that it must stay the
  sole access point for the underlying parameters.

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

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (f9e58e7) to head (de896b6).
⚠️ Report is 166 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #8944   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            3         1    -2     
  Lines           55        14   -41     
=========================================
- Hits            55        14   -41     
Flag Coverage Δ
unittests 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

daphnehanse11 and others added 3 commits July 6, 2026 18:28
Covers the bypass flag, the default-inactive return-None path, and the
5-year lookahead window (activation at the simulation year, at the
window edge, and non-activation just beyond it) using a lightweight
parameter stub, addressing the codecov/project gap on the factory's
non-bypass path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two current-law cases (no reforms key) contrast post-OBBBA behavior
against the counterfactual and exercise the baseline
is_snap_abawd_hr1_in_effect formula, restoring codecov project
coverage for the file the selective runner scopes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ian exemption behavior

Verified via a 672-cell monotonicity sweep (ages x hours x states x
exemption flags): the reform only loosens the requirement except for
the OBBBA-created Indian exemption, which correctly does not exist in
the counterfactual — now documented in the module docstring. A
month-by-month sweep over 2025-2028 confirms pre-toggle months match
baseline and each state flips at its true adoption date. End-to-end,
an age-60 non-worker with no income goes from $0 to $298/month SNAP
under the reform; codified as is_snap_eligible contrast tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@daphnehanse11 daphnehanse11 marked this pull request as ready for review July 7, 2026 13:20
daphnehanse11 and others added 7 commits July 7, 2026 09:43
The case fails on Linux CI only (computed [0,1], expected [1,1]) while
passing on macOS with identical package versions; asserting
is_snap_abawd_hr1_in_effect isolates whether the reform override or the
dependent-child linkage diverges.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The case failed only on Linux CI with explicit spm_units/tax_units
blocks while the equivalent baseline cases (people + households only)
pass on all platforms; instrumented CI runs showed the hr1 switch was
correct and the parent/dependent-child linkage was what diverged.
Mirror the proven baseline structure.

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both parent cases fail on Linux CI regardless of dependent threshold,
so the parent/qualifying-child linkage is what diverges; asserting
is_parent, is_tax_unit_dependent, and monthly_age per person will name
the component that drops.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Input assertions passed on Linux, so the remaining suspect is the SPM
unit aggregation: assert spm_unit_size in the diagnostic case and add
an explicit spm_units block (without tax_units) to the 15-year-old
case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream PolicyEngine#8942 (merged after this branch was cut) removed the
exempt_parent path from meets_snap_abawd_work_requirements and made
meets_snap_work_requirements_person the single source of truth for the
7 CFR 273.24(c)(4) household-child exception. CI tests the merge with
main, which is why the parent cases failed there while passing locally.

Replace the person-level parent cases with a routing test at
meets_snap_work_requirements_person: an adult who complies with general
work requirements via work program participation but fails the 20-hour
ABAWD test is routed around the ABAWD test by a 15-year-old household
member under the pre-OBBBA threshold (18) and not under the post-OBBBA
threshold (14), plus a baseline-contrast case. Drop the diagnostic
scaffolding and update household-child wording.

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

Copy link
Copy Markdown
Collaborator

This can be a pure parametric reform

is_snap_abawd_hr1_in_effect is the sole reader of the four baseline booleans it consults (gov.usda.snap.work_requirements.abawd.in_effect plus the CA/HI/AK hr1_in_effect params — confirmed by grep), and all three downstream consumers (meets_snap_abawd_work_requirements, meets_snap_work_requirements_person, medicaid_community_engagement_pass_through_eligible) branch on nothing but that variable, taking the frozen parameters("2025-06-01") pre-OBBBA snapshot on the false path.

So setting those four booleans to False collapses the select() to all-false — identical to the reform's where(in_effect, False, baseline) override, with no side effects since nothing else reads them.

Empirical proof of equivalence

I ran the PR's structural reform (variable override + contrib toggle ON) against a pure parametric reform (the four booleans → False) across 30 scenarios × 6 outputs = 180 comparisons — all identical:

Dimension Cases Result
State paths (TX/CA/HI/AK — the four select() branches) 4
Delayed-adoption timing (2026-01, 2027-01) 2
Age threshold (55+ pre vs 65+ post) ages 30/55/60/70
Child-age threshold (14 post vs 18 pre) child 13/15/17
Restored exemptions (homeless, veteran, former foster youth) 3
Indian-exemption tightening (the asymmetric case in the docstring) 1

Outputs compared: is_snap_abawd_hr1_in_effect, meets_snap_abawd_work_requirements, meets_snap_work_requirements_person, meets_snap_work_requirements, medicaid_community_engagement_pass_through_eligible, and end-to-end is_snap_eligible.

The parametric form:

Reform.from_dict({
    "gov.usda.snap.work_requirements.abawd.in_effect":               {"2026-01-01.2100-12-31": False},
    "gov.states.ca.cdss.snap.work_requirements.abawd.hr1_in_effect": {"2026-01-01.2100-12-31": False},
    "gov.states.hi.dhs.snap.work_requirements.abawd.hr1_in_effect":  {"2026-01-01.2100-12-31": False},
    "gov.states.ak.dpa.snap.work_requirements.abawd.hr1_in_effect":  {"2026-01-01.2100-12-31": False},
}, "policyengine_us")

Why reconsider the structural approach

The structural version produces bit-for-bit identical results while adding ~115 lines of reform code, a duplicated select() block that must be kept in sync by hand (flagged with # keep in sync in the diff — a maintenance hazard if a fifth delayed-adoption state is ever added), and a 5-year-lookahead autoloader with its own unit tests. It also introduces the retroactivity bug the PR then had to fix — which only existed because of the structural neutralize_variable approach; a parametric reform gets period-gating for free from the reform-dict date range.

The one thing the structural approach buys is the single gov.contrib.* toggle (consistent with the inverse pre-passage reform). If a named single lever is desired, a lighter middle option is to have baseline is_snap_abawd_hr1_in_effect return False when the contrib in_effect param is set (~2-line edit) rather than overriding the whole variable — this preserves the one-lever UX and drops the duplicated logic + autoloader.

Recommendation: convert to the pure parametric reform above, or the ~2-line contrib-gated variant if the single toggle is a hard requirement.

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 pre-OBBBA SNAP ABAWD work requirement counterfactual reform (2026/2027 rules as if P.L. 119-21 §10102 had not passed)

2 participants