Add San Francisco County Adult Assistance Program (CAAP)#8591
Open
hua7450 wants to merge 17 commits into
Open
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ier (ref PolicyEngine#8590) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8591 +/- ##
============================================
+ Coverage 77.77% 100.00% +22.22%
============================================
Files 1 23 +22
Lines 9 260 +251
============================================
+ Hits 7 260 +253
+ Misses 2 0 -2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
- age_threshold: re-cite to CAAP Manual Div 91-4.5 Age (#page=144) - qualified_immigration_status: re-cite to Div 91-4 Citizenship/PRUCOL (#page=123/125) - unearned income sources: repoint #page=316 (earned) to Div 92-1 Unearned (#page=190) - add size-11 in-kind and disregard 1/5-tier coverage tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ca_sf_caap: cap the SEC. 20.7-24 $59 floor at grant_after_income so in-kind support can no longer increase the net benefit (over-pay fix) - add floor boundary tests (over-pay guard, genuine floor, cash_grant==59) - drop "Div" prefix in param reference titles to match manual labeling - repoint unearned income sources to Manual 92-40 (#page=229) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stry - unearned income sources: drop social_security_disability/retirement components (the social_security aggregate already adds them) to avoid double-counting SSDI/retirement - move in-kind-exercising tests to 2026-01 so they fall within the in-kind chart's 2025-11-01 effective era (no backward-extrapolated future values) - register ca_sf_caap in programs.yaml (status: partial, GA tier) alongside the other CA county General Assistance programs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gate The CAAP Manual (Div 91-4.5 Age, p.144) admits some under-18 applicants who are emancipated minors (married/divorced/widowed/domestic-partnered/court- emancipated). PolicyEngine has no emancipation input, so this rare exception is documented as an unmodeled pathway rather than partially modeled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion The $59 floor reference titled SEC. 20.7-24 (ordinance) was linked to the Manual PDF. Split into two references so each title matches its href: the amlegal ordinance for SEC. 20.7-24, and the Manual page 47 where the value appears. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- immigration: add DACA + TPS to the eligibility whitelist (manual lists deferred-action/TPS/PRUCOL as eligible); document COFA/VAWA/U/T/SIV (no enum member) as not modeled - countable unearned income: add workers_compensation and railroad_benefits (both counted by the manual) - other-aid bar: new ca_sf_caap_other_aid_eligible bars CalWORKs (ca_tanf) and CAPI (ca_capi) recipients; RCA/ECA not modeled (no PE variable) - citations: extra-person grant -> Manual SEC. 20.7-21(e) ($35; amlegal shows a superseded $14); swap dead Westlaw Title 22 links to Cornell LII; add #page=47 to the main variable reference - document CA SDI/DIB and mid-month proration as not modeled Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t param - remove alimony_income and strike_benefits from countable unearned income: the manual treats alimony only as a client-paid deduction (not received income) and has no strike-benefit income type (strike is a work-availability rule) -- both were generic-template carryover - rename personal_property/limit/married.yaml -> couple.yaml and p.married -> p.couple (label/description already said "couple"); document that units of 3+ apply the 2-person reserve Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ligible Replace ca_sf_caap_ineligible_person with a positive ca_sf_caap_eligible_person (not an SSI recipient and has a qualified immigration status). budget_unit_size now adds eligible persons directly (dropping the spm_unit_size dependency), and countable_income keeps eligible persons' income. Behavior is identical (count(eligible) == spm_unit_size - count(ineligible)). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PAES (Personalized Assisted Employment Services) recipients get a higher grant than the base General Assistance table (SEC. 20.7-21(b) vs (a); Manual "Maximum PAES Grant Amount", single $714 ... $3,107 for size 10, verified at the source). ca_sf_caap_max_grant selects the PAES table when any unit member is in a work program, captured by the shared is_in_work_program input (byte-identical to PR PolicyEngine#8360, which also adds it). The +$35/person-beyond-10 rule (SEC. 20.7-21(e)) moves to a tier-neutral amount/extra_person.yaml. CALM/SSIP higher tiers remain not modeled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CAPI is an individual SSI-type program, so a CAPI-eligible person is now excluded from the budget unit per-person (in ca_sf_caap_eligible_person, alongside the SSI exclusion) rather than barring the whole unit -- a non-CAPI member of the same unit still qualifies. CAPI uses ca_capi_eligible_person (categorical eligibility), faithful to the manual's "eligible to or a recipient of" bar. The single-condition ca_sf_caap_other_aid_eligible wrapper is removed; CalWORKs (ca_tanf, a family grant) is checked directly at the unit level in ca_sf_caap_eligible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tests no longer set the computed variables ssi, ca_tanf, or ca_capi directly -- they drive them from underlying inputs (age 70 -> ssi; parent+child -> CalWORKs; aged non-qualified-noncitizen -> CAPI), matching the real household-API flow. Adds a CAPI-isolation case (aged DACA -> eligible_person false) and an explicit no-eligible-person case (budget unit size 0 -> ca_sf_caap 0); removes the deleted other_aid_eligible test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ax size - Mask the is_in_work_program check to ca_sf_caap_eligible_person, so an ineligible member (SSI/CAPI/immigration, already dropped from the budget unit) who is in a work program no longer pushes the eligible unit onto the higher PAES table (a one-person GA unit was becoming PAES). Adds a regression test. - Parameterize the largest charted family size (amount/max_family_size = 10) and use it in ca_sf_caap_max_grant and ca_sf_caap_income_in_kind instead of the literal 10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add interest_income and rental_income to CAAP countable unearned income (Manual #page=190 enumerates both; neither is exempt under SEC. 20.7-14, which uses a catch-all "all unearned income not specifically exempt" rule) - Fix TPS immigration reference: split PRUCOL (#page=125) from TPS (#page=133) - Reframe earned/unearned source citations to note SEC. 20.7-14 lists exemptions and income not exempted under it is counted - Align extra_person.yaml reference title with the Div 99.3-1 table at #page=487 - Add absolute_error_margin to ca_sf_caap_max_grant test cases - Add tests: isolated unit-level CalWORKs bar, mixed-unit per-person CAPI exclusion with income exclusion, and PAES tier flowing end-to-end Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the San Francisco County Adult Assistance Programs (CAAP) — General Assistance (GA) tier — in PolicyEngine. CAAP is San Francisco's locally funded cash-assistance program for indigent adults, administered by the SF Human Services Agency under SF Administrative Code Ch. 20 Article VII and CA Welfare & Institutions Code §17000. This PR models the grant (fill-the-gap: max grant minus countable income) for both the General Assistance and PAES tiers, asset and immigration eligibility, the 5-tier earned-income disregard (recipients only), the Title 22 §50511 in-kind valuation chart with the $59 minimum-cash floor, per-person exclusion of SSI and CAPI recipients, and a unit-level CalWORKs bar. PAES enrollment is captured by the shared
is_in_work_programinput.Closes #8590
Regulatory Authority
Citation split: structural / eligibility rules are cited to the amlegal ordinance; dollar values (grant tables, in-kind chart, asset reserve) are cited to the CAAP Manual / Title 22, because the ordinance does not print the rate tables. All
#page=anchors on the Manual use the PDF's physical page numbers (the Manual's printed page numbers run ~+424 ahead of physical pages).Program Overview
is_in_work_programinput. The CALM and SSIP grant tiers and all sub-program activity-qualification gates are documented as not modeled (see Scope and Not Modeled).Scope
ca_sf_caap_max_grantselects the PAES table when any CAAP-eligible member is in a work program (is_in_work_program, masked to budget-unit members), else the GA table; the +$35/person-beyond-the-max-charted-size rule (SEC. 20.7-21(e)) is shared, and the max charted family size is a parameter (amount/max_family_size, 10). The CALM and SSIP grant tiers and the activity-qualification gates (PAES job-readiness, CALM Medi-Cal linkage, SSIP disability/pending-SSI) are documented as not modeled.gov/local/ca/sf/caap/, consistent with the sibling CA county GA programs (Alamedagov/local/ca/ala/ga/, Riversidegov/local/ca/riv/general_relief/, LAgov/local/ca/la/general_relief/).ca_sf_caapis registered inprograms.yaml(status: partial— GA + PAES grant tiers modeled; CALM/SSIP and activity gates not yet), alongside those siblings. Like them, it is not added to the household benefit aggregator (county GA does not flow into the household state-benefit total).Income Eligibility & Resource Tests
in_san_franciscolocator (county_str == "SAN_FRANCISCO_COUNTY_CA"); every CAAP variable chainsdefined_forthrough the eligibility variableca_sf_caap_age_eligible(monthly_age >= 18); a child in the unit fails the test. The CalWORKs-ineligible-parents split is not tracked at the momentca_sf_caap_personal_property_eligible(spm_unit_cash_assets <= limit); single/couple limits ineligibility/personal_property/limit/{single,couple}.yamlca_sf_caap_income_eligible(ca_sf_caap_countable_income < ca_sf_caap_max_grant, strict<). Uses cash income only — in-kind value is handled at the grant level, not the income testca_sf_caap_eligible_person(~(ssi > 0) & ~ca_capi_eligible_personand a qualified immigration status) is summed intoca_sf_caap_budget_unit_size. SSI and CAPI are individual programs, so each recipient drops out of the unit per person — a non-recipient member still qualifies — and their (exempt) income is excludedca_tanf > 0checked directly inca_sf_caap_eligible; CalWORKs is a family grant, so the bar applies at the unit level. RCA / ECA also bar in the manual but have no PolicyEngine variable (not modeled)ca_sf_caap_immigration_status_eligibleagainst thequalified_immigration_status.yamlwhitelist (CITIZEN, LEGAL_PERMANENT_RESIDENT, REFUGEE, ASYLEE, CUBAN_HAITIAN_ENTRANT, DEPORTATION_WITHHELD, CONDITIONAL_ENTRANT, PAROLED_ONE_YEAR, DACA, TPS), reusing the existingimmigration_statusenum. PRUCOL / SF-specific humanitarian categories are approximated by this setIncome Deductions & Exemptions
Counted income — earned income (less the disregard below) plus unearned income (Social Security, unemployment compensation, veterans' benefits, pensions, child support, workers' compensation, railroad retirement), from the positive
countable_income/sources/{earned,unearned}.yamllists. Only the income of persons counted in the budget unit is included.5-tier earned-income disregard (recipients only) —
ca_sf_caap_earned_income_disregard, a marginal-rate scale on gross monthly earned income (SEC. 20.7-21(j) / Div 94-14):The disregard caps at $455. It is gated on
ca_sf_caap_is_recipient(a SPMUnit boolean input defaulting to False): applicants receive no disregard.~9 exempt income types (SEC. 20.7-14: SSI/SSP, tax refunds incl. EITC, foster care / adoption assistance / KinGAP, relocation payments, training/job-readiness payments, student grants/loans paid to the school, locally-funded work-incentive payments, Guaranteed Income Pilot payments, rent grants/loans at application) are modeled by omission from the positive
countable_income/sources/{earned,unearned}.yamllists.In-kind value (Title 22 §50511 chart, below) is deducted from the grant, not from countable income.
Benefit Calculation
Fill-the-gap, computed monthly at the SPM-unit level:
written as an explicit
base − max_(countable, 0)(not anadds/subtractsaggregation) so that negative income sources (e.g. self-employment losses) cannot inflate the benefit above the max grant.The in-kind value of any housing / utilities / food / clothing provided is deducted from the grant (SEC. 20.7-22). When the in-kind value meets or exceeds the grant, the $59 minimum-cash floor (SEC. 20.7-24) tops the otherwise-eligible recipient back up to $59/month — capped at the cash entitlement (
min_(grant_after_income, $59)), so in-kind support never raises the net benefit above what cash income alone leaves. The full grant is treated as received — the Mandatory Direct Rent Payment routing (landlord-direct vs. cash) is not split out, since it does not change the total benefit value.Income / Grant Standards
GA maximum monthly grant, effective 2024-10-01 (unchanged in both the 2025 and 2026 manuals), from
amount/ga/by_family_size.yaml(Div 99.3-1, Manual #page=487):PAES maximum monthly grant (the higher tier, SEC. 20.7-21(b), Manual #page=43), effective 2024-10-01, from
amount/paes/by_family_size.yaml— applied whenis_in_work_program:In-kind value chart (Title 22 §50511, Div 99-1, Manual #page=467), effective 2025-11-01:
(Each person beyond 10 adds $35 to food and clothing. The Manual prints only sizes 1–2 — see Not Modeled.)
Other standards: cash-asset limit $2,000 single / $3,000 couple (Title 22 §50420); $59 minimum cash floor (SEC. 20.7-24); age threshold 18.
Requirements Coverage
18 in-scope requirements: 13 covered, 5 by-design, 0 missing.
in_san_francisco(+defined_forchain)in_san_francisco.yaml,eligible.yamleligibility/age_threshold.yamlca_sf_caap_age_eligibleca_sf_caap_age_eligible.yaml,eligible.yamleligibility/personal_property/limit/{single,couple}.yamlca_sf_caap_personal_property_eligiblepersonal_property_eligible.yamlca_sf_caap_income_eligibleincome_eligible.yamlqualified_immigration_status.yamlca_sf_caap_immigration_status_eligibleimmigration_status_eligible.yamlca_sf_caap_eligible_person,ca_sf_caap_budget_unit_size,ca_sf_caap_eligibleca_sf_caap_eligible_person.yaml,budget_unit_size.yaml,ca_sf_caap.yamlca_sf_caap_countable_income,_countable_income_person,_income_in_kindcountable_income.yaml,ca_sf_caap.yamlcountable_income/sources/{earned,unearned}.yamlca_sf_caap_earned_income,_unearned_incomecountable_income.yamlearned_income_disregard.yamlca_sf_caap_earned_income_disregard,_net_earned_income,_is_recipientearned_income_disregard.yaml,net_earned_income.yamlincome_in_kind/{housing,utilities,food,clothing,extra_person}.yamlca_sf_caap_income_in_kind+ 4*_provided_in_kindflagsincome_in_kind.yaml,ca_sf_caap.yamlamount/ga/{by_family_size,extra_person}.yamlca_sf_caap_max_grantca_sf_caap_max_grant.yamlca_sf_caapca_sf_caap.yamlspecial_allowance/floor.yamlca_sf_caapca_sf_caap.yamlamount/ga/by_family_size.yaml(size 2)ca_sf_caap_max_grantca_sf_caap_max_grant.yaml,ca_sf_caap.yamlspm_unit_cash_assets; documented in docstringsincome_in_kind.yamlNot Modeled
We don't track the following at the moment:
is_in_work_program); what is not modeled is which sub-program a recipient qualifies for and the separate CALM/SSIP grant amounts.ca_sf_caap_age_eligible.py).ca_tanf; CAPI per person viaca_capi_eligible_person, alongside the SSI exclusion).ImmigrationStatusenum member — COFA permanent residents (FSM / Marshall Islands / Palau), VAWA self-petitioners, U-visa, T-visa, and SIV holders are listed as eligible by the manual but cannot be expressed with the current enum (documented inca_sf_caap_immigration_status_eligible.py).social_security_disabilityis federal SSDI, a different program, and is not substituted).Historical Notes
Files Added
Test plan
sf/tree, including the pre-existingsf/wftctests).make format(ruff format) andruff checkclean.