You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
CA SSI state supplement pays $0 to disabled recipients after meets_ssi_disability_criteria migration (payment amount still keys on raw is_disabled) #8549
After #8364 separated meets_ssi_disability_criteria from the broad is_disabled input (so is_ssi_disabled = meets_ssi_disability_criteria & ~ssi_engaged_in_sga), the California SSI State Supplement pays $0 to non-aged disabled recipients who are modeled with the new canonical input. Eligibility and the payment amount now read disability from different sources, so the SPM unit comes back eligible but with a $0 benefit.
This was found while comparing the PolicyEngine household APIcurrent vs frontier releases (period 2026). It is a concrete, confirmed instance of the broad-is_disabled risk tracked in #8431.
Reproduction (household API, /us/calculate, period 2026, CA, single adult, $0 income & resources)
Inputs
ca_state_supplement_eligible_person
ca_state_supplement
aged 70 (control)
True
4101.72
age 45, meets_ssi_disability_criteria: true only
True
0.00⚠️
age 45, meets_ssi_disability_criteria: trueandis_disabled: true
True
4101.72 ✅
So eligibility correctly recovers with the migrated input, but the amount only appears if the (now legacy) is_disabled flag is also sent.
Root cause
ca_state_supplement_eligible_person correctly derives eligibility from is_ssi_aged_blind_disabled (which flows through meets_ssi_disability_criteria post-Add SSI disability criteria variable #8364). ✅
But the payment standard's aged/disabled count still uses raw is_disabled:
# ca_state_supplement/payment_standard/ca_state_supplement_aged_disabled_count.pyis_disabled=person("is_disabled", period)
is_aged=age>=p.aged_or_disabled.age_thresholdaged_or_disabled=is_aged|is_disabled# <-- raw is_disabledeligible=person("ca_state_supplement_eligible_person", period)
returnspm_unit.sum(aged_or_disabled*eligible)
For a disabled SSI recipient modeled via meets_ssi_disability_criteria, is_disabled is False, so the count is 0 → ca_state_supplement_aged_disabled_amount is defined_for a count > 0 and returns 0 → payment standard 0 → max(0, payment_standard - ssi - countable_income) = 0.
The same raw is_disabled pattern appears in ca_state_supplement_dependent_amount.py.
Suggested fix
In the CA payment-amount path, derive disability from the same SSI source used for eligibility — is_ssi_disabled (consistent with is_ssi_aged_blind_disabled) — instead of raw is_disabled:
ca_state_supplement_aged_disabled_count.py
ca_state_supplement_dependent_amount.py
Add a regression test asserting that a non-aged disabled SSI recipient modeled with meets_ssi_disability_criteria receives the same supplement that is_disabled: true produced pre-#8364.
Scope — checked against programs.yaml (all implemented SSPs)
programs.yaml registers SSP variables for 18 states + DC: AK, AL, CA, CO, CT, DC, DE, GA, IN, KY, MA, ME, MI, MO, NM, SC, TX, WA (FL/HI/IA/KS/LA/MN are listed but not yet implemented).
I scanned all of them for how they derive disability. CA is the only implemented SSP that reads the raw is_disabled input in its payment path:
ca_state_supplement_aged_disabled_count.py
ca_state_supplement_dependent_amount.py
Every other implemented SSP already derives disability from the SSI-correct source (is_ssi_disabled / ssi_category / is_ssi_aged_blind_disabled) — e.g. CO co_state_supplement_eligible, MA ma_maximum_state_supplement, WA wa_ssp_payment_category all use is_ssi_disabled. This matches empirical household-API testing: MA SSP recovers cleanly with meets_ssi_disability_criteria; only CA stays at $0. So the fix is localized to CA.
Note on blindness: raw is_blind is still read directly by several SSPs (WA wa_ssp_payment_category, MA ma_maximum_state_supplement, CO co_state_supplement_eligible, CT ct_ssp_*, MO mo_ssp_category_eligible, CA ca_state_supplement_blind_amount/_dependent_amount). That's fine today (blindness has no separate SSI-criteria input), but if it ever gets a meets_ssi_*-style split like disability did, these would need the same treatment.
All code references verified against up-to-date policyengine-us (post-#8364, is_ssi_disabled = meets_ssi_disability_criteria & ~ssi_engaged_in_sga).
Summary
After #8364 separated
meets_ssi_disability_criteriafrom the broadis_disabledinput (sois_ssi_disabled = meets_ssi_disability_criteria & ~ssi_engaged_in_sga), the California SSI State Supplement pays $0 to non-aged disabled recipients who are modeled with the new canonical input. Eligibility and the payment amount now read disability from different sources, so the SPM unit comes back eligible but with a $0 benefit.This was found while comparing the PolicyEngine household API
currentvsfrontierreleases (period 2026). It is a concrete, confirmed instance of the broad-is_disabledrisk tracked in #8431.Reproduction (household API,
/us/calculate, period 2026, CA, single adult, $0 income & resources)ca_state_supplement_eligible_personca_state_supplementTruemeets_ssi_disability_criteria: trueonlyTruemeets_ssi_disability_criteria: trueandis_disabled: trueTrueSo eligibility correctly recovers with the migrated input, but the amount only appears if the (now legacy)
is_disabledflag is also sent.Root cause
ca_state_supplement_eligible_personcorrectly derives eligibility fromis_ssi_aged_blind_disabled(which flows throughmeets_ssi_disability_criteriapost-Add SSI disability criteria variable #8364). ✅is_disabled:For a disabled SSI recipient modeled via
meets_ssi_disability_criteria,is_disabledisFalse, so the count is 0 →ca_state_supplement_aged_disabled_amountisdefined_fora count> 0and returns 0 → payment standard 0 →max(0, payment_standard - ssi - countable_income) = 0.The same raw
is_disabledpattern appears inca_state_supplement_dependent_amount.py.Suggested fix
In the CA payment-amount path, derive disability from the same SSI source used for eligibility —
is_ssi_disabled(consistent withis_ssi_aged_blind_disabled) — instead of rawis_disabled:ca_state_supplement_aged_disabled_count.pyca_state_supplement_dependent_amount.pyAdd a regression test asserting that a non-aged disabled SSI recipient modeled with
meets_ssi_disability_criteriareceives the same supplement thatis_disabled: trueproduced pre-#8364.Scope — checked against
programs.yaml(all implemented SSPs)programs.yamlregisters SSP variables for 18 states + DC: AK, AL, CA, CO, CT, DC, DE, GA, IN, KY, MA, ME, MI, MO, NM, SC, TX, WA (FL/HI/IA/KS/LA/MN are listed but not yet implemented).I scanned all of them for how they derive disability. CA is the only implemented SSP that reads the raw
is_disabledinput in its payment path:ca_state_supplement_aged_disabled_count.pyca_state_supplement_dependent_amount.pyEvery other implemented SSP already derives disability from the SSI-correct source (
is_ssi_disabled/ssi_category/is_ssi_aged_blind_disabled) — e.g. COco_state_supplement_eligible, MAma_maximum_state_supplement, WAwa_ssp_payment_categoryall useis_ssi_disabled. This matches empirical household-API testing: MA SSP recovers cleanly withmeets_ssi_disability_criteria; only CA stays at $0. So the fix is localized to CA.Note on blindness: raw
is_blindis still read directly by several SSPs (WAwa_ssp_payment_category, MAma_maximum_state_supplement, COco_state_supplement_eligible, CTct_ssp_*, MOmo_ssp_category_eligible, CAca_state_supplement_blind_amount/_dependent_amount). That's fine today (blindness has no separate SSI-criteria input), but if it ever gets ameets_ssi_*-style split like disability did, these would need the same treatment.Related
is_disabledusage (this is a concrete, confirmed SSP instance with a reproduction)meets_ssi_disability_criteria