Skip to content

Add Iowa LIHEAP regular heating assistance - #10292

Open
hua7450 wants to merge 14 commits into
mainfrom
ia-liheap-wave1c
Open

hua7450 wants to merge 14 commits into
mainfrom
ia-liheap-wave1c

Conversation

@hua7450

@hua7450 hua7450 commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Adds Iowa's LIHEAP Regular Assistance heating benefit for FY2026. The model counts members with citizenship or Social Security number documentation for household size, counts the gross income of members 18 and over less child support paid, tests it against 200% of the 2025 poverty guideline, scores the Appendix I matrix (poverty-level points, targeting factors and fuel points) and pays $40 a point between $80 and $800, with a flat $800 for households that buy liquid propane, fuel oil or kerosene themselves. Income counts Social Security net of the Medicare Part B premium and floors each source at zero. In FY2026, a 65-year-old living alone on $18,000 of Social Security in a detached house heated with natural gas receives $480: $18,000 / $15,650 = 115% of poverty gives 5 points, fixed income, age 60 or over and a detached dwelling add 3, natural gas adds 4, and 12 points x $40 = $480. In the model a Medicare Savings Program pays this retiree's Part B premium, so nothing is subtracted.

Fixes #10289

Regulatory authority

Eligibility

Requirement Source How modeled
Iowa residence Manual section 5.00 defined_for = StateCode.IA
Household size counts members with citizenship or SSN documentation; excluded members' income still counts Manual sections 7.00 (page 31) and 7.30 (page 34) ia_liheap_household_size adds is_citizen_or_legal_immigrant; a household with no counted member is ineligible
Income at or below 200% of the poverty guideline; the FY26 table equals 2.0 times the 2025 guideline Manual section 5.10 and Appendix B (page 89); plan section 2.1 ia_liheap_fpg reads the guideline with a one-year lag, ia_liheap_income_limit applies the 2.0 rate, and the limit is inclusive
Energy burden: the household pays a heating vendor, or heat is in unsubsidized rent, or heat is in subsidized rent and the household has an electric account Manual sections 5.41, 5.42 and 5.50 (pages 20 to 22) and Appendix D ia_liheap_has_energy_burden from heat_expense_included_in_rent, receives_housing_assistance and pre_subsidy_electricity_expense
Heating fuel Appendix I item 3 (page 137); Appendix D (page 123) The model requires a known heating_type: UNSPECIFIED is ineligible, including when heat is in the rent, because the fuel row cannot be scored without it. NONE and SOLAR are ineligible.

Countable income

Item Source How modeled
Members 18 and over Manual section 6.20 (page 26), Appendix C (pages 110 to 121), plan section 1.9 ia_liheap_countable_earned_income and ia_liheap_countable_unearned_income count members aged 18 or older. Section 6.00 (page 24) counts wages of minors working full time; the model follows section 6.20, Appendix C and the plan, and the conflict is recorded in a comment.
Earned income Appendix C Wages, military basic pay, self-employment (including SSTB), partnership or S corporation, farm operations and farm rental income
Social Security Plan section 1.9 (page 6); manual section 6.10 (page 25) and Appendix C (page 114) Each member's social_security less that member's medicare_part_b_premium, floored at zero. The plan checks "Excluding MediCare deduction", and the manual uses the amount "paid by check or direct deposit" on the SSA-1099 when the gross amount would put the household over income. The premium is the out-of-pocket amount: zero for a person not enrolled, and net of what a Medicare Savings Program pays. A 66-year-old with $33,000 of Social Security who pays the full 2026 premium (12 x $202.90 = $2,434.80) has countable income of $30,565.20, under the $31,300 limit, and receives $440 (4 + 3 + 4 = 11 points).
Other unearned income Appendix C SSI, pensions, military retirement and survivor pay, retirement distributions (which include IRA distributions), railroad retirement, unemployment (total_unemployment_compensation), workers' compensation, strike benefits, disability benefits, veterans benefits, alimony, rental income, interest, ordinary dividends and gambling winnings. Child support received, capital gains and FIP are not counted.
Losses Section 6.20 (page 26); plan section 1.8 (page 5); Appendix C Rental Income (page 110) Every earned and unearned source is floored at zero per person, so a loss in one source offsets no other income. Appendix C defines rental income as "Income received from rental properties, such as farmland or dwellings." A 40-year-old with $34,000 of wages and a $6,000 rental loss counts $34,000, over the limit, and receives $0.
Child support paid Manual section 6.10 Subtracted once per household, floored at zero
Fixed income only (targeting factor) Definitions (page 81); Appendix C (pages 92, 98, 109-110, 114); section 4.40(3)(a) ia_liheap_fixed_income_only is true when a member receives Social Security, SSI, veterans benefits or a pension, including railroad retirement and military retirement or survivor pay, and no member of any age has another recorded source. Non-countable receipts that Appendix C says are not fixed income, child support received and FIP, also cancel the point. Each other source is floored at zero first, so a rental loss cannot cancel interest or a self-employment profit. The test keys on whether a fixed source is present, so the Medicare premium does not affect it. A minor's Social Security identifies a fixed source while its amount stays excluded.

Benefit

  • Poverty-level points (Appendix I item 1): 8 at or below 75% of poverty, 6 up to 100%, 5 up to 125%, 4 up to 200%, with inclusive band tops and the ratio formed in float64 before the lookup.
  • Targeting factors (Appendix I item 2; Definitions pages 79 to 85): +1 each for fixed income only, a member aged 60 or older, a disabled member (is_disabled), a child under 6 and a detached dwelling; -1 for five or more units; -2 for subsidized housing; -4 for heat in rent; -4 for savings over $50,000 (spm_unit_cash_assets). The new ia_liheap_dwelling_type input (detached, 2 to 4 units, 5 or more units) defaults to detached.
  • Fuel points (Appendix I item 3): natural gas 4, electric 4, liquid propane 5, fuel oil 5, solid fuel 2. Kerosene follows fuel oil, and wood and coal take the solid fuel points.
  • Payment (Appendix I award calculation; manual section 11.00; plan section 2.6): $40 a point, at least $80 and at most $800, so 17 points, the most a natural gas or electric household can score, pay $680. Households that buy liquid propane, fuel oil or kerosene themselves receive a flat $800. A household whose heat is included in its rent takes the matrix amount, with the -4 heat-in-rent factor, whatever the building's fuel (an inference; sources below). A 40-year-old tenant with $28,000 of wages and heat in unsubsidized rent in a propane-heated building of two to four units scores 4 - 4 + 5 = 5 points and receives $200. The bill does not cap the award.

Years covered

Model year Program year Guideline One-person limit
2026 FY2026, heating season October 1, 2025 to April 30, 2026 2025 $31,300

The registry entry records verified_years: "2026", and its note says the flat amount goes to households that buy propane or fuel oil. To keep the three wave 1C PRs from editing the same line, #10303 adds IA, AZ and VA to the LIHEAP coverage line, so it merges last. Earlier periods use parameter backfilling and are unverified historical estimates. Later periods carry the FY2026 rules forward on the lagged guideline: period 2027 reproduces the FY27 limit of $31,920 for one person but keeps the FY26 fixed-income wording, so its results are carried-forward estimates.

Not modeled and source conflicts

  • Disability is self-declared in Iowa; is_disabled stands in for it. Savings use spm_unit_cash_assets.
  • SSA repayments and Medicare Part D premiums withheld from the check have no input; the model subtracts only the Part B premium. No Part A premium is withheld, because a Social Security beneficiary aged 65 or older is entitled to Part A under 42 U.S.C. 426(a). Bank interest and the $120 dividend rule are not separable from the model's inputs. All interest counts, so any interest also cancels the fixed-income point.
  • Self-employment and farm income are the model's net inputs floored at zero, which follows Appendix C's filer route; the non-filer gross route is not represented.
  • Kerosene has no matrix row. The model treats it as fuel oil for both the 5 points and the flat $800, an inference: Appendix I and the award rule use the same term "Fuel Oil", the FY26 draft plan (page 48) ties the flat amount to deliverable fuels, and section 12.20 (page 47) lists kerosene as a deliverable fuel. Other fuels have no published points.
  • The flat $800 goes only to households that buy the deliverable fuel themselves, an inference. The draft plan (page 48) ties it to "deliverable fuel customers", section 11.00 (page 42) raises "the deliverable fuel benefit", and Appendix D (page 122) pays a household whose heat is in its rent by "Direct pay to the LIHEAP customer" or to the electric vendor "because gas usage is indeterminate". Such a tenant buys no fuel, so the model pays the matrix amount.
  • The fixed-income factor follows the FY26 wording, "no other sources of income" (Appendix I) and "the ONLY source(s) of income" (Definitions), so a working minor's wages and non-countable child support or FIP cancel it. The FY27 manual and Program Notice 27-01, effective October 1, 2026, reword the factor to "no other countable sources of income"; that change is not modeled because only FY2026 is verified. Railroad retirement counts as a pension for the factor: Appendix C defines pensions as regular payments for past service or age (page 109) and labels RRB-1099-R as annuities or pensions (page 110).
  • Dual-household children, roomers, special dwellings, payment routing and application timing are not modeled.
  • The two railroad retirement test households are aged 62 because the SSI module's unearned income list omits railroad_benefits, so an older retiree would receive imputed SSI that Iowa counts. That SSI gap is a separate fix.

Test plan

  • 16 YAML files with 164 cases under policyengine_us/tests/policy/baseline/gov/states/ia/hhs/liheap/; expected values are hand computed from the manual in case comments. All 164 cases pass locally, as do the registry, unemployment-reader and source-conflict pytest checks.
  • YAML tests whose expected values come from an external source (statute, official form or worksheet, official calculator)
  • make format
  • Changelog fragment in changelog.d/

axiom: TheAxiomFoundation/rulespec-us#1834 queued

🤖 Generated with Claude Code

hua7450 and others added 4 commits October 3, 2026 23:37
Encode Iowa's FY2026 regular heating benefit from the FY26 LIHEAP Policy
and Procedures Manual: household size of citizens and qualified
noncitizens, gross countable income of members aged 18 and over less
child support paid, a 200 percent federal poverty guideline limit on the
prior-year guideline through the shared fpg() helper, the energy burden
and housing rules of Appendix D, and the Appendix I payment matrix
(poverty-level points, nine targeting factors, fuel points, $40 per
point between $80 and $800, flat $800 for liquid propane and fuel oil).
Adds the ia_liheap_dwelling_type input the matrix requires and registers
the program with verified_years "2026". Crisis assistance, cooling and
weatherization are out of scope.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Count railroad retirement benefits as unearned income per Appendix C,
score zero poverty-level points when a household has no counted member
and so no guideline, and test the fixed-income-only factor on the
household's recorded income sources across all members so a minor's
Social Security still identifies a fixed-income household while staying
excluded from the countable amount. Reword the self-employment and
Medicare comments to match the manual's two-route and net-of-premium
rules.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The SSI unearned income list does not include railroad_benefits, so a
railroad retiree aged 65 or older with no other income receives the full
SSI federal benefit, which Iowa then counts. Age 62 keeps the two cases on
the railroad retirement rule they test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
hua7450 and others added 2 commits October 20, 2026 01:09
- Fixed income only: recorded non-countable sources that Appendix C says
  are not fixed income (child support received, FIP) now cancel the
  point, through a new other_recorded source list; railroad retirement
  counts as a pension; the test no longer depends on float summation order.
- Kerosene follows fuel oil for both the fuel points and the flat $800.
- References follow the repo convention for PDF pages.
- Replace internal workflow wording with the policy reasons.
- Tests: new fixed-income, child support and kerosene cases (153 total).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hua7450
hua7450 marked this pull request as ready for review October 20, 2026 05:31
Main's code-health check rejects the reported-only unemployment_compensation
input in income source lists; the total uses the reported amount when present
and otherwise the modeled state unemployment insurance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hua7450
hua7450 marked this pull request as draft October 20, 2026 15:16
hua7450 and others added 2 commits October 20, 2026 11:16
Add main's military_retirement_income (military retirement pay and survivor
benefit payments) to Iowa's unearned and fixed income lists: Appendix C
counts pensions and retirement income, and the fixed income definition
includes pensions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hua7450
hua7450 marked this pull request as ready for review October 21, 2026 04:45
@DTrim99

DTrim99 commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Add Iowa LIHEAP regular heating assistance

PR #10292 (author: hua7450), head 5a164d58ea. This is a first review. The PR adds Iowa's LIHEAP Regular Assistance heating benefit for FY2026: 25 parameters, 17 variables, 16 test files (156 cases), a programs.yaml entry and a changelog fragment. The PR has no comments or reviews yet. It touches no partner test file.

Path prefixes:

  • P/ = policyengine_us/parameters/gov/states/ia/hhs/liheap/
  • V/ = policyengine_us/variables/gov/states/ia/hhs/liheap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/ia/hhs/liheap/

Sources read for this review:

  • FY26 manual: the Iowa LIHEAP Policy and Procedures Manual, effective October 1, 2025 (media/17262, 139 pages). I read pages 13–14, 18–26, 31, 34, 42–43, 46–47, 79–85, 89–123 and 137–138.
  • Benefit matrix: the Clearinghouse FY2026 copy of Appendix I. It is identical to manual page 137.
  • FY2026 state plan: the Clearinghouse copy, pages 5–9. The checkboxes are images, so I decoded each one: a checked box carries a green check mark and an unchecked box does not.
  • Draft plan: the FY26 public comment draft, page 48.
  • Program Notice 26-01: effective October 1, 2025.
  • FY2027: the FY27 manual (pages 89, 96–97 and 147) and the agency LIHEAP page, both fetched 2026-10-21.

Summary

What is right. The published FY26 rules are encoded faithfully:

  • Income limits. 2.0 × the 2025 guideline, with a one-year lag, reproduces all eight Appendix B sizes and the +$11,000 step (p. 89). The limit is inclusive.
  • Matrix and award. The poverty bands, with the ".01% rounded up" note, all nine targeting factors, the five fuel rows, $40 a point, the $80 minimum, the $800 maximum and the flat $800 for liquid propane and fuel oil all match Appendix I (p. 137), section 11.00 (p. 42), the draft plan (p. 48) and plan item 2.6.
  • Eligibility. The household-size rule, the energy-burden rules (Appendix D) and the treatment of income under 18 follow the manual. The under-18 conflict between sections 6.00 and 6.20 is resolved toward 6.20, Appendix C and the plan, and the PR says so.
  • Tests. I hand-checked 15 cases. CI passed all 156.
  • Batch checks. The PR reads total_unemployment_compensation (Integrate modeled state unemployment insurance into income flows #9558) and military_retirement_income (Wire military retirement pay and survivors into federal and state tax (#10181) #10182), with the survivor input pinned. Its comments pass the source-conflict ratchet on the merged tree.

There are no critical findings and four should-address items:

  • S1. Medicare premiums. Social Security counts gross. Plan item 1.9 checks "Excluding MediCare deduction", and the manual counts the amount "paid by check or direct deposit". The PR defers netting "as in Kansas", but Kansas's own rule includes the deduction. medicare_part_b_premium exists, and eight merged LIHEAP states subtract it. Example: a 66-year-old with $33,000 of Social Security gets $0 at the head and $440 under the rule.
  • S2. Rental losses. A rental loss offsets wages. The PR floors self-employment, farm-operations and farm-rent losses per source, but not rental_income. Example: $34,000 of wages and a $6,000 rental loss get $360 instead of $0.
  • S3. Stale Axiom issue. rulespec-us#1834 is open, labelled pe-parity and dispatch-ready, but it describes head 189af398b8. It still lists raw unemployment_compensation, omits military_retirement_income from both source lists, and counts 153 cases where the head has 156.
  • S4. An unpinned source. self_employment_income is never positive in any test. Every case gives it a loss, which floors to zero, so no test fails if it is removed from the earned list.

There are also seven suggestions. They include the flat $800 for heat-in-rent households in propane or oil-heated buildings, the published FY2027 rewording of the fixed-income factor, and Iowa's state supplement (item 17), which is not on main yet.

Status.

  • CI: 17 checks pass and 10 are still pending (nine contrib shards and Rest (Python + variables)); none has failed. The 156 Iowa cases passed in states-shard-2.
  • Branch: 11 commits ahead of main and 2 behind, and it merges cleanly.
  • Siblings: the 11 open LIHEAP sibling PRs conflict only in programs.yaml.

Critical (must fix)

None.

Should address

S1. Medicare premiums are not subtracted: Social Security counts gross, although the plan and the manual count the amount paid

  • Where:
    • P/income/sources/unearned.yaml:11-17: "The model counts social_security gross as a documented approximation: netting the Medicare Part B premium (the model has medicare_part_b_premium) is deferred, as in Kansas".
    • The documentation of V/income/ia_liheap_countable_unearned_income.py, the header of T/income/ia_liheap_countable_unearned_income.yaml and the PR body ("Social Security is counted gross; Iowa nets Medicare premiums") say the same.
  • Sources:
    • FY2026 plan item 1.9 (p. 6, decoded): "Social Security Administration (SSA) benefits" is checked, with "Excluding MediCare deduction" checked and "Including MediCare deduction" unchecked.
    • Manual section 6.10 (p. 25): when the 1040 amount "would cause the household to be over income for eligibility, agencies can request the SSA 1099 from the customer and utilize the 'paid by check or direct deposit' as shown on the SSA 1099". Appendix C repeats this on p. 114.
    • Appendix C, Social Security Benefits (p. 114): "Count the net amount as income". The same row adds: "If a LIHEAP customer receives social security and is being reimbursed for Medicare Part B that was previously deducted, only count the amount received that applies to the verification period."
    • The one gross route. The SSA-1099 Box 5 route includes the premium. The manual replaces it with the net "paid by check or direct deposit" amount exactly when it would make the household ineligible, and the household picks its most favourable verification period.
  • Why the stated precedent does not hold:
    • Kansas counts Social Security gross because its own rule says so: "SSA benefits are counted gross, including the Medicare deduction". Iowa's plan checks the opposite box.
    • medicare_part_b_premium (person, annual) is on main. It holds the out-of-pocket Part B premium, net of Medicare Savings Program (MSP) coverage, and it is zero for non-enrollees.
    • Merged IN, KY, MD, NJ, NY, OH, RI and SD subtract it.
  • Effect (hand-traced from the code for 2026). Take a single person aged 66 with $33,000 a year of Social Security, natural gas heat with a bill, and the default detached dwelling. Their income of $2,750 a month is well above the MSP limit for Qualifying Individuals (QI, 135% of the guideline), so MSP pays nothing. The premium is 12 × $202.90 = $2,434.80.
    • Head: 33,000 > 31,300, so $0.
    • Rule: 33,000 − 2,434.80 = 30,565.20 ≤ 31,300, so the household is eligible. 195.3% of the guideline gives 4 points. Fixed income, elderly and detached add 3, and natural gas adds 4. 11 points gives $440.
    • Existing cases. Retirees under the QI limit, such as integration Case 1 ($18,000), appear to be MSP-covered in the model, so their awards should not change. Countable-unearned Case 1 ($24,000 at age 67) is above that limit, so its expected value would fall to 21,565.20. CI will confirm.
  • Severity: should, as in Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303 S1 (Virginia), Add Arizona LIHEAP Standard benefit #10293 S1 (Arizona) and wave 1 (Add New Jersey LIHEAP regular heating assistance #9745 S1, Add New York HEAP regular heating assistance #9744 S1).
  • Fix:
    • Subtract each member's medicare_part_b_premium from that member's Social Security, floored per person. Use a deductions list like Ohio's or subtract it in the formula like New Jersey's.
    • Replace the three comments and the body row. Keep Part D and SSA repayments as not modeled, since neither has a separate input.
    • Add a case like the example above, and update countable-unearned Case 1.

S2. Rental losses offset wages and other income

  • Where:
    • V/income/ia_liheap_countable_unearned_income.py:41 sums p.sources.unearned with add(), keeping signs. That list includes rental_income (P/income/sources/unearned.yaml:49).
    • V/income/ia_liheap_countable_income.py floors only the household total.
    • The PR floors every earned source, including farm_rent_income ("Proceeds from land rental", p. 98), "so a loss does not offset other income". A loss on a rented dwelling still offsets other income, while a loss on rented farmland does not.
  • Sources:
    • Rental Income (p. 110): "Income received from rental properties, such as farmland or dwellings." A loss is not income received.
    • Section 6.20 (p. 26) counts "the gross income of all household members", and plan item 1.8 checks "Gross Income". Plan item 1.9 checks "Rental income". No source lets a loss in one source reduce another.
  • Effect (hand-traced for 2026). A single person aged 40 has $34,000 of wages and a $6,000 rental loss, with natural gas heat, a bill and a detached house.
    • Head: countable income is 28,000, or 178.9% of the guideline: 4 + 1 + 4 = 9 points, $360.
    • With the loss floored: 34,000 > 31,300, so $0.
  • Same flaw in the fixed-income test. V/income/ia_liheap_fixed_income_only.py sums the other unearned sources with signs, so a rental loss can cancel another source. For example, Social Security plus a $500 rental loss and $500 of interest scores as fixed income only.
  • Tests: no case sets a negative rental_income.
  • Severity: should, as in Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303 S2, Add Arizona LIHEAP Standard benefit #10293 S2 and Add New Jersey LIHEAP regular heating assistance #9745 S2.
  • Fix:
    • Floor rental_income per person, either by moving it into the floored loop or by flooring each unearned entry.
    • Apply the same floor in the fixed-income test.
    • Add a rental-loss case like the one above.

S3. rulespec-us#1834 no longer matches the head

  • What the issue gets right. It is open and labelled enhancement and pe-parity, and it is dispatch-ready. It has:
    • module paths and corpus status;
    • verbatim law, including plan item 1.9's "Excluding MediCare deduction" and Appendix C's "SSA (State Supplementary Assistance)" line;
    • required outputs, 14 companion tests derived from the sources, and 16 interpretations;
    • its blockers: sources missing from the corpus, an unchecked signed release, billed-run approval and no reviewed oracle revision.
  • Where it is stale. It describes head 189af398b8 (2026-10-20). Two later commits changed the code:
    • 8490f2e587 switched the unearned list to total_unemployment_compensation.
    • 410a0a0241 added military_retirement_income to the unearned and fixed lists. It also added countable-unearned Case 10 and fixed-income Cases 16–17.
  • The resulting gaps:
    • The required-outputs row for countable unearned income still names raw unemployment_compensation, which the Integrate modeled state unemployment insurance into income flows #9558 guard now rejects. It lists 15 sources; the head has 16.
    • The fixed-income row lists five fixed sources; the head has six.
    • The "Parameters" and "Tests" lines say 15 unearned sources, five fixed sources and 153 cases. The head has 16, six and 156 (countable_unearned_income 10, fixed_income_only 17).
    • No companion test covers military retirement pay or the Survivor Benefit Plan (SBP) annuity.
  • Severity: should, as in Add Arizona LIHEAP Standard benefit #10293 S4 (checklist item 9: stale companion values).
  • Fix: update the issue to the final head after S1 and S2.
    • Name total_unemployment_compensation, military_retirement_income, medicare_part_b_premium and the rental floor in the required outputs.
    • Drop the companion-test assumption "no Medicare premium ... withheld", or add a premium case.
    • Add the military-retirement and SBP rows, and refresh the head SHA and case counts.

S4. self_employment_income has no test that would fail if it were dropped

  • Where: P/income/sources/earned.yaml lists self_employment_income, the main self-employment input.
  • Why it is unpinned. It appears in two cases, each time as a loss: earned Case 4 (−$5,000) and countable-income Case 6 (−$12,000).
    • The per-source floor turns both losses into 0, which is the same result as leaving the input out.
    • No case gives it a positive amount, either in the countable totals or in the fixed-income test, where a profit should cancel the fixed-income point.
  • The other entries are covered. All 22 other income-source entries take a nonzero positive value in some case. total_unemployment_compensation is pinned through unemployment_compensation in countable-unearned Case 5 and countable-income Case 7.
  • Severity: should, as in Add Arizona LIHEAP Standard benefit #10293 S5 and Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303 S4 (checklist item 13).
  • Fix: give earned Case 4, or a new case, a positive self_employment_income, for example $4,000 beside the wages. Add a fixed-income case in which a self-employment profit cancels the point. The S2 rental-loss case can sit beside it.

Suggestions

G1. The flat $800 reaches heat-in-rent households in propane or oil-heated buildings.

  • The head. V/ia_liheap.py pays deliverable_fuel_amount whenever the fuel category is PROPANE or FUEL_OIL, including when heat is in the rent. The −4 heat-in-rent factor then has no effect.
  • The sources point the other way:
    • The draft plan (p. 48) explains the flat amount by "deliverable fuel customers ... not being eligible for winter shutoff moratorium protection".
    • Section 11.00 (p. 42) raises "the deliverable fuel benefit".
    • Appendix D (p. 122) pays a household with heat in its rent through the electric vendor ("because gas usage is indeterminate") or directly. Such a tenant does not buy deliverable fuel.
  • Example. A single person aged 40 with $28,000 of wages lives in a two-to-four-unit building, with heat in unsubsidized rent and a propane-heated building.
    • The head: $800.
    • The matrix: 4 − 4 + 5 = 5 points, or $200.
  • Tests. No test combines heat in rent with a deliverable fuel.
  • Fix: consider limiting the flat amount to households that buy the fuel themselves (has_heating_expense, or not heat in rent). Otherwise, record the reading as an inference and pin it with a test.

G2. FY2027 is published and rewords the fixed-income factor.

  • What changed. The FY27 manual, effective October 1, 2026, keeps the matrix and award lines. Its limits are $31,920 plus $11,360 (pp. 96–97), which period 2027 reproduces through the one-year lag.
  • The reworded factor. It reads "Fixed Income Only (no other countable sources of income)" (p. 147), and the Definitions say "countable income types" (p. 89). The head's comment notes this as not modeled.
  • The effect in period 2027. Child support received, FIP and a minor's wages still cancel the point. Example: ia_liheap.yaml Case 15 (a grandparent and a grandchild who receives child support), run for 2027:
    • The rule: 18,000 / 21,640 = 83.2% gives 6, the targeting factors give 3 with the fixed-income point, and natural gas gives 4. 13 points is $520.
    • The head: $480.
  • Severity. 2027 is not claimed (verified_years: "2026"), so this is a suggestion, as in Add Arizona LIHEAP Standard benefit #10293 G5.
  • Fix: consider a 2026-10-01 entry that limits the other-source test to countable sources. For example, empty other_recorded and age-gate the other sources, and add a 2027 case. Otherwise, keep the note and say in the body that 2027 is a carried-forward estimate.

G3. Interest and the fixed-income factor.

  • The head. All interest_income counts as countable income, a documented approximation. As a result, fixed-income Case 9 pins that $200 of interest cancels the fixed-income point.
  • The rule. Appendix C excludes "Interest Bearing Bank Accounts" (p. 103), the usual interest of a low-income retiree, and FY27 limits the factor to countable sources.
  • The effect. In microsimulation, many retirees who live only on Social Security but report some interest lose the point (−$40).
  • Fix: consider leaving interest_income out of the fixed-income other-source test, or record the effect beside the approximation in P/income/sources/unearned.yaml.

G4. The agency-page references now show FY27 (live URL).

  • Where. P/eligibility/fpg_rate.yaml, P/eligibility/fpg_year_lag.yaml, the V/eligibility/ia_liheap_fpg.py and ia_liheap_income_limit.py reference tuples and the T/eligibility/ia_liheap_income_limit.yaml header cite the agency page.
  • What the titles claim: "200% of the 2025 federal poverty guidelines for October 1, 2025 to September 30, 2026".
  • What the page shows. On 2026-10-21 it read "200% of the 2026 federal poverty guidelines. These guidelines are for October 1, 2026 - September 30, 2027", with $31,920 for one person. The closest Wayback capture is from 2025-07-14 (FY25), so no FY26 capture exists.
  • Why only a suggestion. Every value keeps a stable primary reference: the FY26 manual is a fixed file (media/17262), and plan item 2.1 is on p. 9. That makes this milder than Add Arizona LIHEAP Standard benefit #10293 S3, where the primary anchors were broken.
  • Fix: drop the agency-page references, or retitle them to describe what the page shows. Add the FY27 manual if G2 is taken.

G5. Record the under-18 conflict as a source-conflict candidate.

  • The conflict. Section 6.00 (p. 24) counts the income of minors "working full-time". Section 6.20, Appendix C (p. 102) and plan item 1.9 do not.
  • Where the PR records it. P/income/countable_min_age.yaml ("that conflict is resolved in favor of Sections 6.20 and Appendix C") and V/income/ia_liheap_countable_earned_income.py ("a recorded conflict").
  • Why nothing forces it. Neither wording matches the ratchet's pattern, so the guard does not prompt for an entry.
  • Precedent. source_conflicts/candidates.yaml already lists Alabama LIHEAP plan-versus-manual conflicts (for example us-al:liheap-insurance-payments).
  • Fix: consider adding a us-ia: LIHEAP candidate with the three sources.

G6. Iowa's state supplement (item 17, forward-looking branch note).

  • The rule. Appendix C counts "SSA (State Supplementary Assistance) - Fully state-funded" in the Social Security row (p. 114).
  • The state of main. It has no Iowa supplement variable today. Add Iowa state supplementation for 2026 #8017 (ia_ssa, with SNAP and HUD countable partitions) is approved but not merged, so the head is correct against its base.
  • Fix: once Add Iowa state supplementation for 2026 #8017 merges, decide whether ia_liheap should count ia_ssa, probably in both the unearned and the fixed lists. Update the Axiom issue's interpretation 8, which says no supplement is counted.

G7. Small test and polish items.

  • A mixed-state case. No case runs an Iowa and a non-Iowa SPM unit in one simulation (item 15). It is safe today: ia_liheap_fuel_category defaults to OTHER, a valid key in p.fuel_points (0). A mixed case would keep it pinned.
  • A size-9 limit case. Sizes 1, 2, 3, 4 and 8 are pinned. A size-9 case would pin Appendix B's "Add $11,000 for each additional member" line.
  • UNSPECIFIED with heat in rent. V/eligibility/ia_liheap_eligible.py calls ineligibility "the all-state convention". The merged AL, FL, MS, OH and OK formulas admit that household, and main's heating_type documentation names UNSPECIFIED as the right value when the tenant does not know the fuel (Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303 G2, Add Arizona LIHEAP Standard benefit #10293 G2).
    • In Iowa the fuel drives 2–5 points. Either admit the household with a documented default fuel, or reword the comment as Iowa's own choice.
  • The NONE exclusion. NONE in fuel_known is unpinned. NONE without heat in rent already fails the burden test. This is optional.

Verified correct

Values against sources:

  • Income limit (section 5.10, p. 18; Appendix B, p. 89; plan 2.1 note, p. 9).
    • 2.0 × (15,650 + 5,500 × (n − 1)) gives 31,300, 42,300, 53,300, 64,300, 75,300, 86,300, 97,300 and 108,300, plus 11,000 a member. That is all of Appendix B.
    • The plan's printed 150.00% is explained on p. 9 as an OLDC form limit, and the PR follows the note.
    • The test is at or below the limit (<=), matching "200% or below" and "Over 200%" ineligible.
  • The lag. fpg_year_lag 1 is required: period 2026 reads the 2025 guideline. The same lag gives FY27's $31,920 and $11,360 for period 2027.
  • Effective date. 2025-10-01 on every parameter matches Program Notice 26-01 ("Effective Date: October 1, 2025").
  • Matrix (Appendix I, p. 137; Clearinghouse copy; draft p. 48):
    • Poverty bands. 8 / 6 / 5 / 4 at band tops of 75 / 100 / 125 / 200%. The -.inf first threshold and right=True reproduce "rounded up to the next percentage", and the float64 ratio keeps exact band tops in band.
    • Targeting factors. +1 each for fixed income only, elderly (60+), disabled, a child under 6 and a detached dwelling. −2 for subsidized housing, −1 for five or more units, −4 for heat in rent and −4 for savings over $50,000. Each applies once.
    • Fuel rows. Natural gas 4, electric 4, liquid propane 5, fuel oil 5, solid fuel 2.
  • Award. $40 a point, $80 minimum and $800 maximum (p. 137; plan 2.6, p. 9). The flat $800 for liquid propane and fuel oil follows Appendix I, section 11.00 (p. 42) and the draft's "both a minimum and maximum". There is no bill cap (section 5.41, "does not depend on the actual amount owed").
  • Eligibility:
    • Household size. Members with citizenship or SSN documentation count, and an ineligible member's income still counts (7.00, p. 31; 7.30, p. 34). An all-ineligible household is ineligible.
    • Energy burden (5.41–5.42, pp. 20–21; Appendix D, pp. 122–123). Paying a vendor directly qualifies, as do heat in unsubsidized rent and subsidized heat in rent with an electric account. Subsidized housing with everything in the rent does not qualify, and neither does solar.
    • Not required. There is no categorical eligibility (plan 1.4 "No", decoded) and no asset test (plan 2.3).
  • Income:
    • Gross income (plan 1.8, decoded). Gross wages follow the pay-stub route, "gross amount before any pre-tax deductions" (p. 97).
    • Under 18. Members under 18 are excluded (6.20; Appendix C, pp. 102 and 114; plan 1.9 box unchecked).
    • Child support paid is deducted (p. 92).
    • Not counted: child support received, FIP, general assistance and capital gains (Appendix C, pp. 92, 98 and 99; plan 1.9 boxes unchecked).
    • Gambling winnings count ("Count only winnings subject to tax", p. 99; plan 1.9 checks lottery winnings).
    • IRA distributions are counted once through retirement_distributions.
  • Fixed-income factor (Definitions, p. 81; Appendix C, pp. 90, 92, 98, 109–110 and 114; 4.40(3)(a), p. 14). SSA, SSI, SSDI, VA and pensions count, with railroad and military retirement as pensions. A minor's Social Security identifies a fixed source. Child support received and FIP cancel the point under the FY26 wording.

Hand-checked expectations (15 cases). The 2025 guideline is 15,650 + 5,500 a member.

  • ia_liheap.yaml Case 1: 18,000 / 15,650 = 1.1502 gives 5; fixed, elderly and detached give 3; gas gives 4. 12 points, $480.
  • ia_liheap.yaml Case 2: 34,000 / 26,650 = 1.2758 gives 4. +1 − 1 − 2 − 4 = −6, plus 4, is 2 points, $80.
  • ia_liheap.yaml Case 3: 28,000 / 15,650 = 1.789 gives 4. −11 plus 4 is −3 points, raised to $80.
  • ia_liheap.yaml Case 4: 4 + 1 + 5 = 10 points, $400, raised to the flat $800.
  • ia_liheap.yaml Case 11: 30,000 / 15,650 = 1.917 gives 4. Detached and wood give 4 + 1 + 2 = 7 points, $280.
  • ia_liheap.yaml Case 13: 60,000 / 32,150 = 1.866 gives 4. Child and detached give 2; electric gives 4. 10 points, $400.
  • ia_liheap.yaml Case 15: 18,000 / 21,150 = 85.1% gives 6. Elderly (63) and detached give 2, with no fixed-income point because of the child support; gas gives 4. 12 points, $480.
  • Integration Case 3: 22,000 + 6,000 = 28,000; 28,000 / 21,150 = 1.324 gives 4. +1 − 4 plus propane 5 is 6 points, $240, raised to $800.
  • Integration Case 4: size 2 with the undocumented parent excluded, limit 42,300. 38,000 / 21,150 = 1.797 gives 4. 4 + 2 + 4 = 10 points, $400.
  • Integration Case 5: 26,000 − 4,800 = 21,200, and 21,200 / 15,650 = 1.355 gives 4. Disabled +1 and electric 4 make 9 points, $360.
  • Integration Case 7: 15,000 / 21,150 = 0.709 gives 8. 8 + 5 + 5 = 18 points, the matrix maximum, and $720 is raised to the flat $800 (kerosene as fuel oil).
  • Poverty-points Cases 2–3 and 6–7: 11,737.5 / 15,650 = 0.75 exactly gives 8, and 11,738 gives 0.750032, so 6. 40,187.5 / 32,150 = 1.25 exactly gives 5, and 40,500 / 32,150 = 1.2597 gives 4.
  • Income-limit Case 3: 2 × (15,650 + 7 × 5,500) = 108,300, Appendix B size 8.
  • Income-eligible Cases 1–2: 31,300 is eligible and 31,301 is not.
  • Fixed-income Case 11: a child's $9,000 of Social Security is the only source, so the factor is true and countable income is 0 (p. 114; 4.40(3)(a)).

Batch checks:

  • Integrate modeled state unemployment insurance into income flows #9558 (item 11): the list reads total_unemployment_compensation, pinned by countable-unearned Case 5 and countable-income Case 7. The four guard tests passed in CI.
  • Wire military retirement pay and survivors into federal and state tax (#10181) #10182 (item 16): military_retirement_income is in the unearned and fixed lists. Fixed-income Case 17 pins the survivor input (countable 14,400).
  • Document disjoint health premium inputs and correct consumer treatment #10046 (item 14): not applicable. Iowa counts wages "before any pre-tax deductions" on the pay-stub route and deducts no health premium; Medicare is S1. Box 1 wages on the elected tax-return route are an alternative the household may choose, not the rule the model has to follow.
  • State-gated keys (item 15): safe.
    • ia_liheap_fuel_category is an enum gated to Iowa whose default, OTHER, is a key in p.fuel_points.
    • ia_liheap_household_size (int) feeds only fpg() arithmetic and where().
    • ia_liheap_dwelling_type is an ungated input that is compared, never indexed.
    • The Microsimulation job passed. A mixed-state case is optional (G7).
  • Federal 110% floor: never binds. The limit is 2.0 × the same lagged guideline that gov.hhs.liheap.fpg_floor (1.1) would use. For size 1 that is $31,300 against $17,215 (2025 guideline) or $17,556 (2026). Not wiring the floor matches the merged states whose limits are far above it.
  • IIJA (item 12): not applicable. Iowa's matrix has no separate supplement, and FY2027 is G2.
  • Source-conflict ratchet (Tie source-conflict markers to the register shared with Axiom #10287):
    • The PR's conflict comments ("that conflict is resolved in favor of", "a recorded conflict") do not match CONFLICT_COMMENT_RE, and there are no marker attempts.
    • In CI, test_conflict_sounding_comments_do_not_grow passed on the merge with a6fafb06c2. main's two newer commits touch no code-health file.
  • Wiring:
    • Registry. The programs.yaml IA entry is present (verified_years: "2026"), and IA is added to the coverage line. test_programs_registry passed.
    • Aggregates. None, which matches every merged LIHEAP state.
    • Gating. defined_for = StateCode.IA is on 16 variables, and ia_liheap is gated by ia_liheap_eligible. test_members_are_state_gated passed.
    • Take-up. FIP is read through the take-up-gated tanf.
    • Changelog. changelog.d/ia-liheap-wave1c.added.md is top level and typed added.
  • Descriptions (item 6): all 25 end "under the Low Income Home Energy Assistance Program".
  • References (item 7): every manual and plan anchor I opened shows its value:
    • manual pp. 14, 18, 20, 24–26, 31, 34, 42, 46–47, 79–85, 89, 90–121 (the Appendix C rows cited) and 137;
    • plan pp. 5, 6, 8 and 9;
    • draft p. 48.
    • Multi-page titles are quoted and use the #page 24,26,102,114 form, and variable tuples carry # PDF pages comments. The agency-page references are G4.

Checklist results (LIHEAP batch):

  1. State plan and manual: pass for every matrix cell, limit and award line.
  2. Income definition: fail on two items. The Medicare premium is not subtracted (S1), and rental losses net (S2).
  3. Backdating: pass for 2026. Earlier years are documented backfill. FY2027 is published and its fixed-income rewording is not used (G2).
  4. Household and arrangement rules: pass, with every eligibility branch tested. Heat in rent with a deliverable fuel is G1, and heat in rent with UNSPECIFIED fuel is G7.
  5. Tests: 156 cases, 15 hand-checked, periods plain. One source is unpinned (S4).
  6. Descriptions: pass.
  7. References: pass, apart from the live agency page (G4).
  8. Wiring: pass.
  9. Axiom: valid but stale (S3).
  10. Branch and CI: a clean merge; 17 pass and 10 pending, none failed.
  11. Integrate modeled state unemployment insurance into income flows #9558: pass.
  12. IIJA / FY2027: IIJA not applicable; FY2027 is G2.
  13. Mutation check: every eligibility, limit, points and award branch is pinned except self_employment_income (S4) and the NONE exclusion (G7).
  14. Document disjoint health premium inputs and correct consumer treatment #10046: not applicable.
  15. State-gated index defaults: safe.
  16. Wire military retirement pay and survivors into federal and state tax (#10181) #10182: pass.
  17. Merged state SSPs: not applicable at this head; main has no Iowa supplement. G6 is the branch note for Add Iowa state supplementation for 2026 #8017.
  • Federal 110% floor: never binds.

Axiom line assessment

Valid but stale: axiom: TheAxiomFoundation/rulespec-us#1834 queued (S3).

  • What is in order. The issue is open and labelled enhancement and pe-parity.
    • Blockers. Its "Why queued" section records the sources missing from the corpus: the FY26 manual, Program Notice 26-01, the Clearinghouse matrix, the draft plan, the 2025 guidelines, a dated agency-page capture and IAC 421-22. It also records the unchecked signed release, billed-run approval and the lack of a reviewed oracle revision.
    • Content. Its verbatim text matches the sources I read, including the decoded plan checkboxes. Its 14 companion tests are derived from the sources, not from PolicyEngine output.
  • What needs updating:
    • The head SHA, the unearned and fixed source lists (total_unemployment_compensation, military_retirement_income) and the case count (156).
    • After S1 and S2: the Medicare subtraction and the rental floor in the required outputs and companion tests.
    • If taken, G1, G2 and G6.

CI status

17 pass, 10 pending, 0 failed. Run 39635435060 tests merge commit 57ef08f560, which merges head 5a164d58ea into a6fafb06c2 (pull_request, started 2026-10-21 16:30 UTC). I re-checked gh pr checks 10292 at 19:44 UTC, at the end of this review.

  • Passed: all four states shards; the household, irs, ssa-usda, rest-a, rest-b, reform and contrib-hhs baseline shards; Rest (Python, shard-1 + variables); Microsimulation; Household API Partners; PR validation; Package and compatibility; and Quick Feedback.
  • Still pending: nine contrib shards (congress, other 1, 2a, 2b and 3, and states 1–4) and Rest (Python + variables). None of them runs the Iowa LIHEAP files.
  • Where the Iowa tests ran. States-shard-2 (batch 4/14, states/ia) ran all 16 files: 12 + 4 + 9 + 6 + 5 + 15 + 11 + 5 + 6 + 8 + 10 + 17 + 8 + 10 + 11 + 19 = 156 passed.
  • Guards. Rest shard-1 passed:
  • Known main-caused failure. Not applicable: the run started after 13:24 UTC, and the ratchet passed.
  • Locally: I ran no tests or model probes. The examples in S1, S2, G1 and G2 are hand traces of the code at the head.

Branch status

0 critical, 4 should and 7 suggestions are open at this head.

Verdict: REQUEST_CHANGES

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The FY26 limits, every Appendix I matrix cell, $40 a point, the $80/$800 bounds and the energy-burden rules check out, and all 156 Iowa cases pass. Four should-address items, the same pattern as VA and AZ: (1) Social Security counts gross though plan 1.9 excludes the Medicare deduction (age 66 with $33k SS: $0 vs $440); (2) rental losses offset wages; (3) rulespec-us#1834 is stale; (4) self_employment_income is never positive in a test. Seven suggestions. Full report in the comment above.

hua7450 and others added 2 commits October 23, 2026 19:57
…uel in rent

- Count Social Security net of the Medicare Part B premium (plan item 1.9)
- Floor rental income per source, including the fixed-income test
- Pay the flat deliverable-fuel amount only when heat is not in the rent
- Pin positive self-employment income; reword the UNSPECIFIED comment

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hua7450

hua7450 commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, David. The fixes are in 5108878, and cc2347f merges main. All 164 Iowa cases pass locally, as do the registry, source-conflict and unemployment-reader checks.

Should address

  • S1, Medicare premium: fixed. Each member's Social Security now counts net of that member's medicare_part_b_premium, floored at zero (plan item 1.9; manual pages 25 and 114). Your 66-year-old with $33,000 of Social Security now receives $440 (ia_liheap Case 16). Without the premium as an input, the model computes the same $2,434.80 premium and $440. Countable unearned Case 11 pins the floor when the premium exceeds the benefit.
  • S2, rental losses: fixed. Every unearned source is now floored at zero per person, in countable income and in the fixed-income test. $34,000 of wages with a $6,000 rental loss now gets $0 (countable income Case 9). Countable unearned Case 12 separates the per-source floor from a floor on the total. Fixed-income Case 18 covers a rental loss beside interest.
  • S3, Axiom issue: updated. LIHEAP wave 1c: encode Iowa LIHEAP regular heating assistance TheAxiomFoundation/rulespec-us#1834 now describes cc2347f. It covers total_unemployment_compensation, military_retirement_income in both lists, the Medicare subtraction, the rental floor, the $800 limit and 164 cases.
  • S4, self-employment: fixed. Earned income Case 7 adds a $4,000 profit beside wages. Fixed-income Case 19 shows that a profit cancels the fixed-income point.

Suggestions

  • G1, flat $800: taken. The flat amount now goes only to households that buy propane or fuel oil themselves. A tenant with heat in the rent takes the matrix amount, so your propane-building example now gets $200 (ia_liheap Case 17). The code comment labels this an inference and cites pages 48, 42 and 122.
  • G7, UNSPECIFIED with heat in rent: declined. heating_type is a required input for every state LIHEAP program, and callers send the fuel. The default is tracked in Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754. I reworded the comment so it no longer calls this an all-state convention.
  • G2, FY2027 wording: not taken. 2027 is not claimed. The body now says 2027 results are carried-forward estimates.
  • G3, interest: kept as a documented approximation. The body now notes that any interest cancels the fixed-income point.
  • G4, G5, G6 and the other G7 items: not taken in this round. G6 waits for Add Iowa state supplementation for 2026 #8017.

@hua7450
hua7450 requested a review from DTrim99 October 24, 2026 00:50
@DTrim99

DTrim99 commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 2): Add Iowa LIHEAP regular heating assistance

PR #10292 (author: hua7450). This round reviews head cc2347fb3b; round 1 reviewed 5a164d58ea. Two commits came in between:

  • 5108878aac, "Fix issues from review: Medicare premium, rental floor, deliverable fuel in rent" (13 files, +317/−60). It answers S1–S4, G1 and part of G7.
  • cc2347fb3b, a merge of main at 6a92d6a983. It had one hand-resolved conflict, the LIHEAP coverage line in programs.yaml (checked below). It changes no Iowa LIHEAP file.

Ziming replied to every finding on 2026-10-24. The PR body still ends axiom: TheAxiomFoundation/rulespec-us#1834 queued, and the issue has been rewritten for this head. The PR still touches no partner test file.

Path prefixes, as in round 1: P/ = policyengine_us/parameters/gov/states/ia/hhs/liheap/, V/ = policyengine_us/variables/gov/states/ia/hhs/liheap/, T/ = policyengine_us/tests/policy/baseline/gov/states/ia/hhs/liheap/.

Summary

Ziming fixed all four round-1 should items:

  • S1, Medicare premiums. V/income/ia_liheap_countable_unearned_income.py:56-57 counts each member's Social Security net of that member's medicare_part_b_premium, floored at zero. ia_liheap.yaml Case 16 reproduces the round-1 example exactly: $440 instead of $0.
  • S2, rental losses. Every unearned source is now floored at zero per person (lines 45-48), and the fixed-income test floors each other source the same way. Countable income Case 9 reproduces the round-1 example: $0 instead of $360.
  • S3, Axiom issue. rulespec-us#1834 now describes this exact head, including the Medicare subtraction, the per-source floors, the purchase condition on the flat $800 and 164 cases.
  • S4, self-employment. Earned Case 7 adds a $4,000 profit beside wages, and fixed-income Case 19 shows that a profit cancels the fixed-income point.

Of the seven suggestions, G1 (the flat $800 with heat in rent) is taken, and so is the G7 comment rewording. G2 and G3 are declined but now documented in the body, and I accept both. G4, G5 and the other G7 items were optional and are declined. G6 still waits on #8017, which is open with changes requested.

There are no new critical or should findings, and two new suggestions:

  • G8. Part A. This is the item raised as G7 on Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303 (Virginia) and Add Arizona LIHEAP Standard benefit #10293 (Arizona), with the same severity. In Iowa the head is right not to subtract Part A: the state nets only what is withheld from the Social Security check, and no Part A premium can be withheld from it (42 U.S.C. 426(a)). The suggestion is to document that, so nobody "fixes" it to match the siblings.
  • G9. The registry note still says the flat amount goes to all propane and fuel oil households. This is optional polish left over from G1.

Batch checks:

I hand-checked every new or changed expectation: 8 new cases and 1 changed case. CI is 27/27 green at the head, and states-shard-2 passed all 164 Iowa LIHEAP cases. The branch is 36 commits behind main and merges cleanly.

Prior findings status

Finding Status Evidence
S1: Social Security counted gross, although plan item 1.9 checks "Excluding MediCare deduction" Fixed The code. V/income/ia_liheap_countable_unearned_income.py:56 reads medicare_part_b_premium. Line 57 sets social_security = max_(person("social_security", period) - premium, 0), and line 61 adds it to the floored sources for members 18 and over. social_security left P/income/sources/unearned.yaml (15 entries remain), so it is not counted twice. It stays in P/income/sources/fixed.yaml, gross, which is right because the fixed-income test only asks whether a fixed source is present. The scope. The subtraction applies to Social Security only. That matches plan item 1.9, where the box sits beside "SSA benefits", and section 6.10 and Appendix C, which use the SSA-1099 amount "paid by check or direct deposit" (p. 25, p. 114). It also matches the Railroad Retirement row, which lists "Medicare" among the "Deductions not allowed from gross income" (p. 110). The variable. medicare_part_b_premium is person-level, annual and "paid out of pocket by the enrollee, net of Medicare Savings Program coverage", with defined_for = "medicare_enrolled". A premium that an MSP pays is not withheld from the check, so it is correctly not subtracted. The tests. ia_liheap.yaml Case 16 is the round-1 example: $440, against $0 counted gross. Countable-unearned Case 11 pins the per-person floor and the Social-Security-only scope. Countable-unearned Case 1 now pins the premium and expects $21,565.20, as round 1 predicted. The comments. The three "deferred, as in Kansas" comments are gone. The documentation, the list comment, the test header and the body describe the subtraction. Part D and SSA repayments are still listed as not modeled; Part A is G8.
S2: a rental loss offset wages Fixed The code. V/income/ia_liheap_countable_unearned_income.py:45-48 floors each source: max_(person(source, period, options=[ADD]), 0). ADD sums the monthly ssi. V/income/ia_liheap_fixed_income_only.py:55-58 applies the same per-source floor to the earned and other unearned sources before checking for any other income. The tests. Countable income Case 9 is the round-1 example: wages of $34,000 with a $6,000 rental loss give unearned $0 and countable $34,000, which is over $31,300, so the award is $0. Countable-unearned Case 12 tells a per-source floor apart from a floor on the total (18,000 against 14,000). Fixed-income Case 18 pins the floor in the fixed-income test: a −$500 rental loss no longer cancels $500 of interest. The comment. P/income/sources/unearned.yaml cites Rental Income (p. 110): "Income received from rental properties, such as farmland or dwellings."
S3: rulespec-us#1834 described an older head Fixed The issue was updated 2026-10-24 00:14 UTC and names head cc2347fb3b and commit 5108878aac. Its countable-unearned output is max(social_security − medicare_part_b_premium, 0) plus the 15 listed sources, including total_unemployment_compensation and military_retirement_income, each floored per person. It lists 164 cases by file, which matches CI file by file. Companion tests 15–19 cover the Medicare premium, the loss floors, the self-employment profit, military retirement and the SBP annuity, and the heat-in-rent propane building. Details are under Axiom line assessment.
S4: self_employment_income never positive Fixed Earned Case 7: 26,000 + max(4,000, 0) = 30,000. Fixed-income Case 19: Social Security of $18,000 plus a $3,000 profit is not fixed income only. Dropping the entry from P/income/sources/earned.yaml now fails both cases.
G1: the flat $800 reached heat-in-rent households in propane or oil-heated buildings Fixed (taken) V/ia_liheap.py:63-64 sets buys_deliverable_fuel = deliverable_fuel & ~heat_in_rent, and line 70 pays the flat amount only then. The comment at lines 55-62 labels the reading an inference and cites the draft plan (p. 48), section 11.00 (p. 42) and Appendix D (p. 122). Appendix D (p. 122) pays a heat-in-rent household by "Direct pay to the LIHEAP customer" or to the electric vendor "because gas usage is indeterminate", which I re-read for this round. ia_liheap.yaml Case 17 is the round-1 example: $200, not $800. The documentation, the deliverable_fuel_amount comment, the test headers, the body and Axiom interpretation 10 all describe the condition.
G2: FY2027 rewords the fixed-income factor Declined, documented (accepted) 2027 is not claimed (verified_years: "2026"). The body's "Years covered" paragraph now says period 2027 reproduces the $31,920 limit but keeps the FY26 wording, "so its results are carried-forward estimates". The code comment and Axiom interpretation 6 record the FY27 wording.
G3: interest cancels the fixed-income point Declined, documented (accepted) The body's "Not modeled" list now says "All interest counts, so any interest also cancels the fixed-income point", and Axiom interpretation 6 says the same. Fixed-income Case 9 keeps pinning the current reading.
G4: agency-page references show FY27 Declined (optional) The four references and the test header are unchanged. The live page still serves FY27, but every value keeps its stable FY26 manual reference (media/17262, p. 89) and plan item 2.1 (p. 9), as round 1 noted.
G5: record the under-18 conflict as a source-conflict candidate Declined (optional) main's source_conflicts/candidates.yaml has no us-ia: LIHEAP entry. The comments still do not match the ratchet's pattern, so nothing forces one.
G6: count ia_ssa once #8017 merges (item 17) Still waiting (branch note) #8017 is OPEN with changes requested (last updated 2026-10-23), and main has no Iowa supplement variable. The head is correct against its base. Axiom interpretation 8 ("the model has no Iowa state supplement at this head") is still accurate. If #8017 merges first, add ia_ssa to the unearned and fixed lists and update interpretation 8.
G7: small test and polish items Partly taken; the rest declined (optional) UNSPECIFIED. The comment in V/eligibility/ia_liheap_eligible.py:38-43 and the test header no longer call this "the all-state convention". They now describe it as this implementation's input rule, with #9754 tracking the default. That is the same decision I accepted on #10303 and #10293. Declined. The mixed-state case, the size-9 limit case and the NONE pin are declined. The state-gated keys are still safe: ia_liheap_fuel_category defaults to OTHER, a key in p.fuel_points, and the Microsimulation job passed.

New findings

Critical (must fix)

None.

Should address

None.

Suggestions

G8. Part A: record why only the Part B premium is subtracted (the sibling of #10303 G7 and #10293 G7).

  • Why the question comes up.
    • Plan item 1.9 checks "Excluding MediCare deduction", which is not limited to Part B (p. 6).
    • main has medicare_part_a_premium: person-level, annual, "paid out of pocket by the enrollee, net of Medicare Savings Program coverage". In 2026 it is $311 a month with 30–39 quarters of coverage, $565 with fewer than 30, and $0 at the default of 40.
    • The code documentation and the body name only SSA repayments and Part D as not modeled. On the Virginia and Arizona PRs, the same gap led to a suggestion to add Part A.
  • Why Iowa differs.
    • Iowa nets only what is deducted from the Social Security check: the SSA-1099 amount "paid by check or direct deposit" (section 6.10, p. 25; Appendix C, p. 114). The Railroad Retirement row disallows a Medicare deduction (p. 110).
    • A Social Security beneficiary aged 65 or over "shall be entitled to hospital insurance benefits under part A" (42 U.S.C. 426(a)), and SSDI beneficiaries become entitled after 24 months (426(b)). Premium Part A is only for a person who "is not otherwise entitled to benefits under this part" (42 U.S.C. 1395i-2(a)).
    • So no Part A premium is ever withheld from an SSA check. The head's Part B-only subtraction is right, and adding Part A, as on the siblings, would be wrong here.
  • Where the model could mislead (hand trace, 2026). The model's premium-free test reads only medicare_quarters_of_coverage. Take a single person aged 66 with $34,000 of Social Security, 35 quarters, natural gas heat with a bill and a detached house.
    • The model's Part A premium: 12 × $311 = $3,732.
    • Head: 34,000 − 2,434.80 = 31,565.20 > 31,300, so $0. This is correct in law.
    • If Part A were subtracted: 27,833.20, or 177.9% of the guideline, gives 4 + 3 + 4 = 11 points and $440.
  • Why only a suggestion. The head is correct. This is documentation, and it keeps the three sibling PRs from being made consistent in the wrong direction. It has the same severity as Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303 G7 and Add Arizona LIHEAP Standard benefit #10293 G7.
  • Fix:
    • Add one clause to the variable documentation, the P/income/sources/unearned.yaml comment and the body's "Not modeled" line, for example: "Part A is not subtracted: Social Security beneficiaries are entitled to premium-free Part A (42 U.S.C. 426(a)–(b)), so no Part A premium is withheld from the check."
    • Add the same sentence to Axiom interpretation 5.
    • Optionally, pin it with the case above (35 quarters, $0).

G9. The registry note could carry the G1 condition (polish). The IA entry in programs.yaml says "with a flat amount for propane and fuel oil". Since G1, the flat amount goes only to households that buy the fuel themselves. Changing the note to "for households that buy propane or fuel oil" would keep the coverage page in line with the code and the body.

Verified

The merge resolution (cc2347fb3b, programs.yaml).

  • The coverage line. It now reads AR, AL, DC, GA, Riverside County, MA, FL, IL, IA, IN, KS, KY, MD, MS, NC, NE, NJ, NY, OH, OK, OR, SD, RI, TX. That is main's line (which gained AR and GA) plus IA, with nothing dropped or duplicated.
  • The registry entry. state_implementations holds the IA entry once, after IL.
  • Against main. The PR's only programs.yaml changes are IA on the coverage line and the IA entry. main's 36 newer commits change programs.yaml only to add student-loan entries, so the merged tree's coverage line is identical.
  • No other changes. git diff 5108878aac cc2347fb3b over the Iowa LIHEAP paths and the changelog is empty. Against 6a92d6a983, the head changes 60 files: the 59 PR files and programs.yaml.
  • CI. test_programs_registry passed.

Hand-checks of every new or changed expectation. All use period 2026, the 2025 guideline of $15,650 for one person, the $31,300 limit and the Part B premium of 12 × $202.90 = $2,434.80 (the 2026 base_premium).

New cases:

  • ia_liheap.yaml Case 16 (age 66, Social Security $33,000, premium input $2,434.80, gas $1,200, detached).
    • Income. 33,000 − 2,434.80 = 30,565.20 ≤ 31,300, so the household is income eligible.
    • Fixed income only: true, since Social Security is the only source.
    • Points. 30,565.20 / 15,650 = 1.9531 gives 4. Fixed income, elderly and detached give 3. With gas 4, that is 11 points and $440.
    • Without the input the result is the same. $2,750 a month is far above the QI limit (135% of the guideline), and IRMAA starts far higher, so the model charges the full premium.
  • ia_liheap.yaml Case 17 (age 40, wages $28,000, PROPANE, bottled_gas_expense 0, heat in unsubsidized rent, 2–4 units).
    • Eligibility. The energy burden is true, because heat is in unsubsidized rent (5.41; Appendix D). The fuel category is PROPANE.
    • Points. 28,000 / 15,650 = 1.7891 gives 4. Targeting is −4: heat in rent −4, two to four units 0, and no fixed source. 4 − 4 + 5 = 5 points.
    • Benefit. 5 × $40 = $200, inside [$80, $800]. Heat is in the rent, so the flat amount does not apply.
  • Earned Case 7: 26,000 + max(4,000, 0) = 30,000.
  • Countable income Case 9 (wages $34,000, rental −$6,000). Unearned max(−6,000, 0) = 0, so countable income is 34,000 > 31,300. The household is not income eligible, and the award is $0.
  • Countable-unearned Case 11 (age 67, Social Security $2,000, pension $24,000, premium $2,434.80). max(2,000 − 2,434.80, 0) + 24,000 = 24,000. An unfloored premium, or one taken from all of the person's unearned income, would give 23,565.20.
  • Countable-unearned Case 12 (age 62, pension $18,000, rental −$4,000). 18,000. A signed sum or a floor on the total would give 14,000. At 62 there is no Medicare premium and no aged SSI.
  • Fixed-income Case 18 (age 67, Social Security $18,000, rental −$500, interest $500). The other sources are 0 + 500 > 0, so the result is false. A signed sum would give 0 and true.
  • Fixed-income Case 19 (age 67, Social Security $18,000, self-employment $3,000). The other sources total 3,000, so the result is false.

Changed expectation:

  • Countable-unearned Case 1 (age 67, Social Security $18,000, pension $6,000, premium now pinned at $2,434.80). 18,000 − 2,434.80 + 6,000 = 21,565.20. $2,000 a month is above the QI limit, so the pinned premium equals the computed one.

Comment-only edits: the headers of T/eligibility/ia_liheap_eligible.yaml, T/ia_liheap.yaml, T/integration.yaml, T/income/ia_liheap_countable_unearned_income.yaml and T/income/ia_liheap_fixed_income_only.yaml now describe the new rules accurately.

Unchanged retiree cases.

  • ia_liheap.yaml Case 1 and integration Case 1 (age 65, Social Security $18,000) keep $480. Had the model charged the premium, 15,565.20 / 15,650 = 99.5% would score 6 points and $520. CI passes at $480, which confirms that the model's Medicare Savings Program pays the premium at $1,500 a month.
  • Countable-unearned Cases 2 ($12,168) and 8 ($19,000) are likewise under the MSP limits.
  • Integration Case 7 scores 8 poverty points either way.

Case count. 12 + 4 + 9 + 6 + 5 + 17 + 11 + 5 + 7 + 9 + 12 + 19 + 8 + 10 + 11 + 19 = 164, as the body and the Axiom issue say. CI's per-file results match file by file.

New-code checks:

  • No cycle. medicare_part_b_premium reads the gross premium (base and IRMAA) and MSP coverage, neither of which reads LIHEAP. ia_liheap feeds no aggregate.
  • Mutations:
    • Removing the premium subtraction fails ia_liheap.yaml Case 16 and countable-unearned Case 1.
    • Removing the Social Security floor, or taking the premium from all unearned income, fails countable-unearned Case 11.
    • Removing the per-source unearned floor fails countable income Case 9 and countable-unearned Case 12.
    • Removing the floor in the fixed-income test fails fixed-income Case 18.
    • Removing ~heat_in_rent fails ia_liheap.yaml Case 17.
    • Dropping self_employment_income fails earned Case 7 and fixed-income Case 19.
  • Documentation. V/income/ia_liheap_countable_unearned_income.py:10-24, V/ia_liheap.py:10-22, the fixed-income comments, the list comments and the body match the code. The one gap is the Part A clause (G8).
  • References. The new tuples follow CLAUDE.md: # PDF pages 20, 42, 47, 122, 137 above an href on page 20, and # PDF pages 5-6 above an href on plan page 5.
  • Descriptions. All 25 parameter descriptions still end "under the Low Income Home Energy Assistance Program."; none changed.
  • Changelog. changelog.d/ia-liheap-wave1c.added.md is unchanged and still correct.

Checklist results (LIHEAP batch):

  1. State plan and manual: pass. The FY26 matrix, limits and award lines are unchanged from round 1. The flat amount now follows the deliverable-fuel reading (G1).
  2. Income definition: pass.
    • Social Security counts net of the Part B premium, per person and floored, as plan item 1.9 and the manual require (S1).
    • Railroad retirement stays gross, as p. 110 requires.
    • Every source is floored (S2), wages count gross, and under-18 income is excluded.
    • Part A is documentation only (G8).
  3. Backdating: pass for 2026. Earlier years are documented backfill, and 2027 is now labelled a carried-forward estimate (G2).
  4. Household and arrangement rules: pass. Heat in rent with a deliverable fuel is now pinned (Case 17). The UNSPECIFIED choice is documented as an input rule (G7).
  5. Tests: 164 cases, all periods plain. All 9 new or changed expectations check by hand.
  6. Descriptions: pass.
  7. References: pass. The new anchors show their provisions; the agency page is G4 (declined, optional).
  8. Wiring: pass. The registry and the resolved coverage line are correct, gating is unchanged, and there is no aggregate wiring, as in every merged LIHEAP state.
  9. Axiom: valid (Too many EITCs #1834, now current).
  10. Branch and CI: a clean merge with main and 27/27 green.
  11. Integrate modeled state unemployment insurance into income flows #9558: pass. The guard passes.
  12. IIJA / FY2027: IIJA is not applicable. FY2027 is reproduced for the limit, not claimed, and documented (G2).
  13. Mutation check: every new branch and every source entry is pinned. The NONE exclusion is still optional (G7).
  14. Document disjoint health premium inputs and correct consumer treatment #10046: not applicable. Wages count before pre-tax deductions, and the only premium subtracted is Medicare Part B from Social Security.
  15. State-gated index defaults: safe and unchanged.
  16. Wire military retirement pay and survivors into federal and state tax (#10181) #10182 / Count military SBP annuities in state benefit income lists #10317: pass. military_retirement_income is in both lists, and the Count military SBP annuities in state benefit income lists #10317 guard passes.
  17. Merged state SSPs: not applicable. main has no Iowa supplement, and Add Iowa state supplementation for 2026 #8017 is open (G6).

Federal 110% floor: never binds. The limit is 2.0 times the same lagged guideline, against the 1.1 floor.

Axiom line assessment

Valid: axiom: TheAxiomFoundation/rulespec-us#1834 queued.

  • State: OPEN, labelled enhancement and pe-parity, created 2026-10-20 and updated 2026-10-24 00:14 UTC.
  • It matches the head:
    • It names head cc2347fb3b and commit 5108878aac.
    • It lists the 25 parameters (unearned with 15 entries plus Social Security net of the premium; fixed with 6), the 17 variables and 164 cases by file.
    • Required outputs. Countable unearned income is max(social_security − medicare_part_b_premium, 0) plus 15 sources, each floored per person. Fixed income only floors the other sources. The benefit pays $800 to PROPANE or FUEL_OIL only when heat_expense_included_in_rent is false. That is the head's code.
  • It is dispatch-ready in the rulespec-us#1467 format:
    • module paths with corpus status;
    • verbatim text of the FY26 manual, including the Social Security row, section 6.10 and the Appendix D Type of Payment column;
    • plan items 1.1–2.6, the draft page 48, Program Notice 26-01 and IAC 421-22;
    • a required-outputs table;
    • 19 companion-test rows derived from the sources, not from PE output;
    • 16 interpretations, including 5 (Medicare), 7 (loss floors) and 10 (the purchase condition, as an inference);
    • its blockers: sources missing from the corpus, an unchecked signed release, billed-run approval, and no reviewed oracle revision.
  • Note. If G8 is adopted, add the Part A sentence to interpretation 5. If Add Iowa state supplementation for 2026 #8017 merges first, update interpretation 8.

CI status

27 pass, 0 pending, 0 failed. Run 40042877423 ran on head cc2347fb3b (pull_request, created 2026-10-24 00:01 UTC, after #10287 and the 2026-10-21 cutoff). I re-checked gh pr checks 10292 at the end of this review.

  • Iowa LIHEAP tests. States-shard-2, batch 4/14 (states/ia, exit 0), ran all 16 files: 12 + 4 + 9 + 6 + 5 + 17 + 11 + 5 + 7 + 9 + 12 + 19 + 8 + 10 + 11 + 19 = 164 passed.
  • Guards (Rest shard-1):
  • Also passing: Microsimulation, Household API Partners, PR validation, Package and compatibility, Quick Feedback, and every baseline and contrib shard.
  • Known main-caused failure: not applicable.
  • Locally: I ran no tests or model probes. The new expectations, G8 and the mutation claims are hand traces of the code at the head.

Branch status

0 critical, 0 should and 2 suggestions are open at this head.

Verdict: APPROVE

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Round 2: all four should items are fixed. Social Security now counts net of the Part B premium (Case 16 gives $440), every source is floored per person (Case 9 gives $0), rulespec-us#1834 matches the head, and self-employment is pinned. G1 is taken too (Case 17 gives $200). The programs.yaml hand-merge is correct, and all 164 IA cases pass. Two small suggestions. Full report in the comment above.

…0303

- Explain why Iowa subtracts no Part A premium (42 U.S.C. 426(a))
- Registry note: the flat amount goes to households that buy propane or fuel oil
- Keep the LIHEAP coverage line as on main; PR 10303 adds IA there

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hua7450

hua7450 commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, David. 157a10d takes both suggestions, with comment and registry changes only:

  • G8: the code and the body now say why only Part B comes off. Under 42 U.S.C. 426(a), a Social Security beneficiary aged 65 or older is entitled to Part A, so no Part A premium is withheld from the check.
  • G9: the registry note now says the flat amount goes to households that buy propane or fuel oil.

The commit also stops this PR from editing the LIHEAP coverage line. #10303 now adds IA, AZ and VA there, so the three wave 1C PRs merge without conflicts. Merge this PR and #10293 in either order, and #10303 last. TheAxiomFoundation/rulespec-us#1834 is updated to this head.

@DTrim99

DTrim99 commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 3): Add Iowa LIHEAP regular heating assistance

Summary

Head 157a10db6d, one commit after our round-2 approval (cc2347fb3b). It addresses our round-2 suggestions G8 and G9: documentation only, plus the shared coverage line.

The change

  • G8, Part A: documented, and the reasoning is right. IA subtracts only Part B. The variable now explains that a Social Security beneficiary aged 65 or older is entitled to Part A without a premium under 42 U.S.C. 426(a), so no Part A premium is withheld from the check that plan 1.9 nets. No formula change.
  • G9: the registry note now says the flat amount goes to households that buy propane or fuel oil.
  • Coverage line: IA is removed from the coverage line; Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303 adds it.

No logic changed and no expectation moved.

CI and branch

At review time, 7 checks passed and 15 were pending. Let CI finish green, including test_programs_registry, before merging. Merge before #10303.

Verdict: APPROVE

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Round 3: documentation only. The Part B-only netting is now justified under 42 U.S.C. 426(a), the registry note is fixed, and IA is removed from the coverage line, which #10303 adds. No logic changed. Let CI finish green, including the registry test, and merge before #10303.

This branch has not been deployed

No deployments
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.

Implement Iowa LIHEAP regular heating assistance

2 participants