Skip to content

Add Indiana EAP regular heating assistance - #9728

Merged
hua7450 merged 13 commits into
PolicyEngine:mainfrom
hua7450:in-eap
Oct 5, 2026
Merged

hua7450 merged 13 commits into
PolicyEngine:mainfrom
hua7450:in-eap

Conversation

@hua7450

@hua7450 hua7450 commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Adds Indiana EAP regular heating assistance using FY2026 rules, with a state-specific dwelling-type input and existing income and utility inputs. In the lowest income band, a household with gas heat and electric service receives $450 in a single-family home, $425 in a mobile home, or $400 in a multi-unit dwelling. A vulnerable single-family household using bulk fuel receives $625. The winter electric allowance is included because manual Section 8.7 ties it to operating the heating system.

Year convention: undated formulas follow the model's existing parameter backfilling. The registry records verified_years: "2026". FY2025 uses the PY2024-25 benefit matrix for the six values that differ from FY2026 (income points 6 and 4, electric payments $100 and $75, and zero fuel points for natural gas and electric heat); its other values and all earlier years are unverified historical estimates.

  • Income thresholds. Reuses the shared federal SMI helper, truncating household-adjusted SMI before applying the 30%, 45% and 60% rates. This reproduces all 90 monthly, quarterly and annual amounts printed in the PY2026 manual and the 48 cells of the PY2024-25 matrix. The rounding is a reconciled numerical pattern, not an express instruction. The annual eligibility ceiling follows the manual's annualization rule; payment bands use the published three-month limits.
  • Income. Counts wages, net self-employment (ordinary and SSTB), farm operations, partnership and S corporation, estate, farm rental, strike, pension, railroad and survivor benefits and the other Section 6.1 sources, each floored at zero per person so a loss does not offset wages, with the adult and student exclusions, child Social Security benefits and child support paid. Excludes TANF, child support received, capital gains and refunds. Social Security is counted net of the Medicare Part B premium the person pays, as Section 6.1 directs, using the existing medicare_part_b_premium, which is zero for a person who is not enrolled and excludes the share a Medicare Savings Program pays; the model has no Part D premium variable, so that part of the deduction is not modeled. Military retirement pay is counted beside pension income, because the two are separate inputs.
  • Benefit. Applies income, dwelling, fuel and vulnerability points at $25 per point, the $50/$100/$125 winter electric allowance, and the heat-in-rent rules including the $1 monthly out-of-pocket rent requirement. The fuels that receive the heating award are a list parameter, separate from the fuel points, because the PY2024-25 matrix gave natural gas and electric heat zero fuel points but still paid the award. Kerosene receives no heating award because Section 1.1 bars EAP funds for it. in_eap_dwelling_type selects SINGLE_FAMILY (3 points), MOBILE_HOME (2) or MULTI_UNIT (1). Includes reported pregnancy in household size and vulnerability.

Limitations

  • Indiana calls for Schedule C gross profit and other gross business amounts; the existing net inputs can understate countable income.
  • SPM units approximate residence-based households, which include unrelated roommates, and annual income approximates the preceding three months. Foster and temporary members, emancipated minors, SSN and document categories, residency duration, some income exclusions and disability evidence are incomplete. Lump-sum retirement withdrawals cannot be separated from recurring payments.
  • OTHER fuels receive the bulk-fuel points, and solar heat receives none; both are assumptions the manual does not address.
  • The monthly SSI-receipt flag is read for December when the period is a year.
  • heating_type must be sent. An unspecified fuel receives no heating award.
  • Microsimulation: the datasets carry no heating inputs, so population totals are close to $0 until the default heating type changes. That change affects API partners and is tracked separately in Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754.

Regular heating and its winter electric allowance only. Crisis, cooling, weatherization and equipment payments are excluded.

Sources:

Validation: 39 Indiana YAML cases cover annual eligibility and three-month payment boundaries through size 11, dwelling and fuel points, the disability proxies, utility and rent treatment, household composition, income sources and exclusions, and an FY2025 household for each changed value. All 90 printed FY2026 income-table values were reconciled against the manual. CI runs the test suite.

axiom: TheAxiomFoundation/rulespec-us#1468 queued

@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9728   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         6    +2     
  Lines           76       115   +39     
  Branches         2         1    -1     
=========================================
+ Hits            76       115   +39     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

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

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

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

hua7450 and others added 6 commits September 30, 2026 19:59
…tilities in rent

- Count farm, partnership and S corporation, estate, farm rental, strike and
  military retirement income (manual section 6.1)
- Give kerosene no bulk-fuel points (manual sections 1.1 and 8.5) and give
  other fuels the bulk-fuel points
- Treat all utilities in rent as heat in rent as well as electricity in rent
- Floor each income source at zero per person
- Replace hardcoded month and quarter literals; add income/calculation_months
- Add metadata.period to parameters
- Add tests for the new sources, fuel points, the single tenant flag, per-source
  floors and raw-input integration households; send heating_type explicitly in
  every eligibility or payment case

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replace the deprecated dividend_income alias. Results are unchanged because the alias only adds ordinary_dividend_income.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@DTrim99

DTrim99 commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Program Review

PR #9728: Add Indiana EAP regular heating assistance (author: hua7450, head c9febf1930)

Scope: Indiana LIHEAP, which Indiana runs as the Energy Assistance Program (EAP) through IHCDA. The PR covers the regular heating benefit and the winter electric allowance. Crisis, cooling, weatherization and repair are out of scope. It changes 32 files: 20 parameters, 6 variables, 4 test files (34 cases), a changelog fragment and programs.yaml. Every parameter starts on 2025-10-01 (FFY/PY2026), and the registry lists verified_years: "2026".

Path prefixes used below:

  • P/ = policyengine_us/parameters/gov/states/in/ihcda/eap/
  • V/ = policyengine_us/variables/gov/states/in/ihcda/eap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/in/ihcda/eap/

Source Documents

# Document Link Use
1 IHCDA PY2026 Intake and Operations Program Manual (122 pp; modified 2025-09-03; PDF page = printed page + 2) Manual Primary source; cited in every parameter
2 FFY2026 Detailed Model Plan, Revision 1 (accepted 2025-10-28) IN_Plan_2026.pdf Heating rules on pp. 8-9; countable income on pp. 5-7
3 FFY2026 Benefit Matrix (PY2025-26, revised 2025-08-05) IN_BenefitMatrix_Heat-Crisis_2026.pdf One-page matrix of all FY2026 points and dollar amounts (sizes 1-8)
4 FFY2025 Detailed Model Plan IN_Plan_2025.pdf Pre-FY2026 income rules and the 60% SMI test
5 FFY2025 Benefit Matrix (PY2024-25, revised 2024-07-24) IN_BenefitMatrix_2025.pdf Not cited in the PR. It governs the period that the PR backfills (C1)
6 PY2027 Detailed Model Plan, draft dated 2026-06-26 Draft plan Corroborates the $125 electric maximum; the structure looks unchanged
7 IHCDA EAP partner page in.gov/ihcda EAP Index of the current manual and plans

The PY2025 manual is not retrievable (404), and no PY2027 manual has been posted.

Branch Status

Item Status
Head c9febf1930, the same commit that was reviewed
CI 37/37 checks pass (gh pr checks 9728, 2026-10-01)
Divergence from main 8 commits ahead and 12 behind (merge base d254a343d4). None of the 12 commits touch programs.yaml, the SMI helper, is_citizen_or_legal_immigrant, the immigration parameters or any Indiana path
Mergeability MERGEABLE, but merge state is BLOCKED because a review is required
Sibling PRs Open LIHEAP PRs #9663 (KS), #9723 (NE), #9725 (KY), #9726 (NC) and #9727 (MS) edit the same programs.yaml lines (see G13)
Axiom line axiom: n/a: rulespec-us does not have this program yet; it should be added as a follow-up.

Critical (Must Fix)

C1. Periods before 2025-10-01 backfill the FY2026 schedule, which contradicts the official PY2024-25 benefit matrix

Each of these parameters has a single 2025-10-01 entry. Any earlier period, including 2025 (which resolves at 2025-01-01), therefore gets the FY2026 value. Six of those values differ from the PY2024-25 schedule:

File:line Item PR value (backfilled) PY2024-25 matrix
P/benefit/income/lower_points.yaml:3 Band 1 income points 9 6
P/benefit/income/middle_points.yaml:3 Band 2 income points 5 4
P/benefit/electricity/lower_income.yaml:3 Band 1 electric benefit $125 $100
P/benefit/electricity/middle_income.yaml:3 Band 2 electric benefit $100 $75
P/benefit/fuel_points.yaml:5 ELECTRICITY fuel points 1 0
P/benefit/fuel_points.yaml:7 NATURAL_GAS fuel points 1 0

Other places that depend on the backfill:

  • T/integration.yaml:75-94 tests the backfilled values. Its 2022 case asserts in_eap: 325, which is (9 + 3 + 1) × $25.
  • V/in_eap.py:10-13 describes the backfill.
  • The comment at T/integration.yaml:75-76 says 2022 uses "the earliest available state benefit schedule". The PY2024-25 schedule is available, and its values differ.

Sources:

  • PY2024-25 matrix, p. 1: "Matrix Points 6 | 4 | 2"; "Electric Benefit $100.00 | $75.00 | $50.00"; fuel source "Natural Gas 0", "Electric 0".
  • PY2024-25 matrix, p. 2: "Award zero (0) points if the household heats with natural gas or electricity." Also: "The maximum number of points is six (6)."
  • Corroborating sources:
    • FFY2025 plan, p. 9: "The electric benefit will be $50-$100".
    • Manual, p. 71 prints the PY2026 natural-gas and electric value of "1" in red change markup.
    • Manual, p. 77 prints the income maximum "9", the total "20", "$125" and "$625" in red.

How this was verified:

  • Code path: every Indiana EAP parameter starts on 2025-10-01, so policyengine-core extends the FY2026 value backward. The PR's own 2022 test confirms this behavior.
  • 600-dpi re-renders of PY2024-25 matrix pages 1 and 2 confirm the quoted values.
  • SMI thresholds are not affected. The shared smi() helper reproduces all 48 PY2024-25 threshold cells.

Impact in period 2025 for a one-person household in a single-family home that pays both bills:

Case PR PY2024-25 matrix
Band 1, gas heat (9+3+1) × $25 + $125 = $450 (6+3+0) × $25 + $100 = $325
Band 2, gas heat $325 $250
Band 1, propane $575 $475
Maximum regular benefit $625 $525

Fix:

  1. Add 2024-10-01 entries to the six parameters above: 6, 4, $100, $75, ELECTRICITY 0 and NATURAL_GAS 0. Cite IN_BenefitMatrix_2025.pdf#page=1, and #page=2 for the fuel rule.
  2. Leave every other parameter alone. These values are the same in PY2024-25, so they need no new entry:
    • $25 per point
    • dwelling points 3/2/1/0
    • 2 income points in band 3 or with heat in rent
    • $50 electric in band 3 or with electricity in rent
    • 6 points for bulk fuels
    • 2 vulnerability points, with ages 60 and 5
    • band rates of 30% and 45%
    • the 60% SMI limit (FFY2025 plan, p. 8)
  3. Replace the 2022 test with a 2025 test against the PY2024-25 matrix, and update the comments at V/in_eap.py:10-13 and T/integration.yaml:75-76.
  4. Decide whether to extend verified_years. The PY2025 manual is unavailable, but FFY2025 plan pp. 5-9 corroborate the income rules.

C2. 23 of 26 #page= anchors are comma lists, and 6 of them open a page that does not hold the cited value

PDF open parameters accept a single page number, so a viewer opens only the first page of a comma list. These six references open the wrong page:

File:line Anchor Page that opens Where the cited value is
P/benefit/fuel_points.yaml:37 10,71 p. 10 (Section 1.1, which has only the kerosene bar) Manual 8.5, p. 71 (fuel table)
P/benefit/income/middle_points.yaml:10 69,70 p. 69 Manual 8.3, p. 70 (5-point chart)
P/benefit/income/middle_rate.yaml:10 69,70 p. 69 Manual 8.3, p. 70 (30-45% chart)
P/benefit/income/higher_points.yaml:10 69,70 p. 69 (only the heat-in-rent rule) Manual 8.3, p. 70 (2-point chart), plus p. 69
P/benefit/vulnerability/points.yaml:10 71,72 p. 71 (the 8.6 heading) Manual 8.6, p. 72 ("Award two (2) points")
V/in_eap.py:15 15-page list p. 10 Manual 8.11, p. 77 (benefit formula)

The other 17 comma lists happen to open a usable first page, but they are still invalid and should be split. Recommended single pages:

File:line Current Recommended
P/benefit/dwelling_points.yaml:16 70,71 #page=71
P/benefit/electricity/lower_income.yaml:10 72,73 #page=72
P/benefit/electricity/middle_income.yaml:10 72,73 #page=72
P/benefit/electricity/higher_income.yaml:10 72,73 #page=72 and #page=73
P/benefit/income/lower_points.yaml:10 69,70 #page=69
P/benefit/income/lower_rate.yaml:10 69,70 #page=69
P/benefit/vulnerability/elderly_age.yaml:10 26,27,71,72 #page=26 and #page=27
P/benefit/vulnerability/young_child_age_limit.yaml:10 26,71,72 #page=26 and #page=27
P/eligibility/adult_age.yaml:10 47,57,58 #page=47, plus #page=29 (see G4)
P/eligibility/income_limit.yaml:10 23,69,70 #page=23
P/income/calculation_months.yaml:10 47,69 #page=47
P/income/sources.yaml:36 47-61 separate #page=49, 51, 52, 53, 54, 55, 56
V/in_eap_dwelling_type.py:23 70,71 #page=70
V/in_eap_eligible.py:10 23,29,... #page=23 and #page=29
V/in_eap_household_size.py:10 23,24,25,29,30 #page=23, #page=24 and #page=29
V/in_eap_income.py:11 47-61 split by topic, as for sources.yaml
V/in_eap_smi.py:12 69,70,72,73 #page=69

Three anchors are already correct: P/benefit/amount_per_point.yaml:10 (#page=77), P/eligibility/minimum_monthly_rent.yaml:10 (#page=32) and P/income/student_age_limit.yaml:10 (#page=58).

Fix: give each page its own - title: / href: entry, and put the manual section number in the title.

Should Address

S1. SSA netting uses the raw reported Part B input instead of the model's computed premium

  • Where: V/in_eap_income.py:40-47. The comment at lines 40-41 says "Only reported Part B premiums are available."
  • Sources:
    • Manual 6.1, p. 55: "The net amount of the Social Security check, after the deduction for Medicare Part B premiums and/or Part D ... is to be used to compute income."
    • FFY2026 plan, p. 6 counts Social Security "Excluding MediCare deduction".
  • What the code does now: medicare_part_b_premiums_reported is a bare input that defaults to 0, and nothing else in variables/ reads it.
  • Why the comment is inaccurate: the model already computes medicare_part_b_premium (policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium.py). That variable is the out-of-pocket premium, including IRMAA and net of Medicare Savings Program coverage. It has no dependency on in_eap.
  • Bias: unless the premium is entered by hand, countable income for each Medicare-enrolled Social Security recipient is overstated by the full premium. At the 2026 standard premium that is $202.90 a month, or $2,434.80 a year, and more with IRMAA. Eligibility and band placement are therefore biased downward.
  • The elderly-widow integration case passes only because it hand-enters 2,434.80.
  • Fix:
    • Read medicare_part_b_premium and keep the per-person floor against social_security.
    • Keep the hand-entered case only as an override test, if at all.
    • Add a case where the premium exceeds the benefit, to exercise the floor at V/in_eap_income.py:43-47.

S2. survivor_benefits is missing from the countable income sources

  • Where: P/income/sources.yaml:4-29.
  • Sources:
    • Manual 6.1, p. 54: "Life insurance payments issued on a regular basis to a surviving household member should be counted as income." The same page says retirement funds are "also available ... to their surviving spouse and surviving minor children".
    • Manual, p. 53 counts the Black Lung survivor benefit.
    • FFY2026 plan, p. 6 checks "Retirement / pension benefits" and "Insurance payments made directly to the insured".
  • Gap: survivor_benefits ("Survivor benefits other than Social Security survivor benefits") is a separate input from pension_income. The federal TANF source list and several state lists already count it.
  • Bias: survivors' countable income is understated by the full annual survivor benefit, which biases eligibility and points upward for those households.
  • Fix: add survivor_benefits. Check that it does not double-count with military_retirement_pay, whose documentation says it includes survivor benefits.

S3. Reading receives_ssi for a YEAR period returns only December

  • Where: V/in_eap.py:69.
  • Cause: receives_ssi is a MONTH boolean. policyengine-core treats booleans as stocks, so a YEAR request returns the last month.
  • Effect: a reported SSI recipient whose receipt ends before December loses the disability proxy for vulnerability points. The ssi > 0 term on line 68 sums over the year and is unaffected.
  • Source: Manual 3.4, p. 26 (the disability benefits that qualify a household as vulnerable).
  • Bias: downward, by at most 2 points ($50), and only for households with no other vulnerability factor.
  • Fix: use add(person, period, ["receives_ssi"]) > 0, as in policyengine_us/variables/gov/hhs/medicaid/eligibility/categories/is_ssi_recipient_for_medicaid.py:30.

S4. 21 test cases write the period as 2_026 or 2_022

  • Where:
    • T/in_eap.yaml:2,21,40,59,84,102,123,140,157
    • T/in_eap_eligible.yaml:2,18,34,49
    • T/in_eap_income.yaml:2,18,37,56
    • T/integration.yaml:2,20,60,78
  • The other 13 cases in these files use period: 2026, and no other test file in the repo uses an underscored period.
  • The format parses today only because PyYAML follows YAML 1.1, which allows digit separators. The YAML 1.2 core schema does not.
  • Source: repository test convention; there is no regulatory page.
  • Fix: write period: 2026. The 2_022 case at line 78 is replaced under C1.

S5. Parameter descriptions depart from the template

  • P/income/sources.yaml:1 joins two clauses with a semicolon and includes an implementation note ("Social Security and SSI are handled separately because benefits paid for children also count"). It also does not end with "under the Energy Assistance Program."
    • Suggested wording: "Indiana uses these income sources to compute countable income for adult members who are not exempt students under the Energy Assistance Program."
    • Move the Social Security and SSI note into a YAML comment or the variable.
  • 15 of the 20 descriptions use verbs outside the standard list (limits, provides, sets, excludes, deducts, uses):
    • "pays": P/benefit/amount_per_point.yaml:1 and P/benefit/electricity/{lower,middle,higher}_income.yaml:1. Use "provides".
    • "awards": P/benefit/dwelling_points.yaml:1, P/benefit/fuel_points.yaml:1, P/benefit/income/{lower,middle,higher}_points.yaml:1 and P/benefit/vulnerability/points.yaml:1. Use "provides".
    • "awards ... beginning at / through this age": P/benefit/vulnerability/elderly_age.yaml:1 and P/benefit/vulnerability/young_child_age_limit.yaml:1. Use "sets".
    • "counts": P/eligibility/adult_age.yaml:1 and P/income/sources.yaml:1.
    • "requires": P/eligibility/minimum_monthly_rent.yaml:1. Use "sets".
  • Source: repository parameter-description convention; there is no regulatory page.

S6. Missing edge-case tests

Coverage is otherwise strong. The tests check both band boundaries for sizes 1-10, the 60% limit for sizes 1-11, all 11 fuel types and all 3 dwelling types. They also check the vulnerability ages at 59/60 and 5/6, the $12-a-year rent boundary, the maximum and the floor paths, and nine integration cases. These cases are missing:

  • (a) Bands above size 10. At size 11 only eligibility is tested (T/in_eap_eligible.yaml:33-47). Source: Manual 8.3, p. 69 (the method for larger households). At size 11:
    • SMI = floor(107,555 × 1.47) = 158,105.
    • Band 1 three-month ceiling = 11,857. Annual income of 47,428 should give $450, and 47,432 should give $325.
    • Band 2 three-month ceiling = 17,786. Annual income of 71,144 should give $325, and 71,148 should give $200.
  • (b) Disability proxies (V/in_eap.py:67-72). Source: Manual 3.4, p. 26.
    • social_security_disability > 0 on its own is never tested.
    • receives_ssi: true on its own is never tested.
    • There is no negative case. With is_disabled: true but no Medicaid, SSI or SSDI, the propane band-1 household at T/in_eap.yaml:101 should get $575, not $625.
  • (c) Heat in rent, electricity billed separately, rent below $12 a year (V/in_eap.py:47-49, 93). The expected result is no heating award and only the electric payment, for example $125 in band 1. Source: Manual 4.7, p. 32.

Suggestions

G1. Default inputs bias simulated in_eap, and some fuel mappings have no source

The table gives each default's bias direction and size; no particular values are prescribed.

Input (default) Effect Bias
heating_type (UNSPECIFIED) fuel_points = 0 (P/benefit/fuel_points.yaml:3), so heating_burden is false (V/in_eap.py:47-49). Households without heat in rent get no heating award Downward, by the whole heating component ($50-$500)
pre_subsidy_electricity_expense (0) No electric payment unless a bill is supplied Downward, $50-$125
heat_expense_included_in_rent (false), tenant_pays_utilities (true) Renters whose utilities are included in rent go down the bill-based path with no bill Downward, up to the $100 in-rent award ($50 in points plus $50 electric)
in_eap_dwelling_type (SINGLE_FAMILY, V/in_eap_dwelling_type.py:14) Apartments and mobile homes get the maximum 3 dwelling points Upward, at most $50, and only where a heating award is paid
  • The local policyengine-us-data and populace checkouts have no imputation of heating_type or heat_expense_included_in_rent. Simulated in_eap will therefore be close to $0 or electric-only. Consider saying so in the programs.yaml notes and the in_eap documentation.
  • Fuel mappings with no printed matrix value:
    • KEROSENE = 0 is supported by Manual 1.1, p. 10.
    • OTHER = 6 is inferred from the bulk-fuel list on Manual 8.5, p. 71.
    • SOLAR = 0 has no source. It denies the heating award to solar-primary homes, which heating_expense models as grid-tied with electric backup. Document the assumption.

G2. Business, farm and rental income are counted net, but the manual specifies gross

  • Where: V/in_eap_income.py:27-35 documents the gap. No gross input exists.
  • Source: Manual 6.1, p. 51 ("gross income and profits before allowable business expenses") and p. 52 (Schedule C line 5, Schedule E line 3, Schedule F line 9).
  • Bias: countable income is understated, which biases eligibility and points upward. Acceptable as documented.

G3. retirement_distributions includes lump-sum and principal withdrawals

  • Where: P/income/sources.yaml:19.
  • Source: Manual, p. 55 and p. 59 exclude lump-sum pension or retirement payments of contributions and interest. p. 54 says principal "is never considered as income".
  • Bias: countable income is overstated, which biases eligibility downward. A comment in the file would help.

G4. eligibility/adult_age does two jobs but its description covers one

  • P/eligibility/adult_age.yaml:1,10 is the income-counting age, cited to p. 47.
  • V/in_eap_eligible.py:22 also uses it as the applicant-age test, which is not cited: Manual 4.4, p. 29 ("An applicant must only be eighteen (18) to apply").
  • Fix: split it into two parameters, or widen the description and add a #page=29 reference.

G5. is_in_k12_school is redundant

  • Where: V/in_eap_income.py:20.
  • The variable is an age 5-17 imputation, so it is never true for the members aged 18 and over that this test applies to.
  • Source: Manual 6.2, p. 57.

G6. In-rent amounts share parameters with the band 3 amounts

  • P/benefit/income/higher_points.yaml:3 (used at V/in_eap.py:53,57) holds both the band 3 points and the heat-in-rent points.
  • P/benefit/electricity/higher_income.yaml:3 (used at V/in_eap.py:98,102) holds both the band 3 electric amount and the electricity-in-rent amount.
  • The heat-in-rent zeros for dwelling and fuel points are literals at V/in_eap.py:61-62.
  • The sources print the in-rent values as separate rows: FFY2026 matrix, p. 1, Manual 8.3, p. 69 and Manual 8.7, p. 72. A single parameter cannot represent a year in which the two values differ.
  • Consider separate parameters, such as benefit/income/heat_in_rent_points and benefit/electricity/in_rent.

G7. Draft and cross-PR wording in comments

  • V/in_eap_smi.py:20-21 compares the code with Mississippi's truncation order. That refers to ms_liheap, which exists only in open PR Add Mississippi LIHEAP regular heating assistance #9727 and not on main. Keep the Indiana-only explanation in lines 17-19. Source: Manual 8.3, p. 69.
  • V/in_eap_income.py:33-34 says "no new gross inputs ... are introduced in this project". "Are not modeled" is clearer.

G8. A test comment cites the wrong section

  • T/integration.yaml:173 cites "Section 6.2 (page 55)" for the rule that Social Security is counted net of Part B.
  • The page is right, but the rule is in Section 6.1 (Manual, p. 55). Section 6.2 starts on p. 57.

G9. PR body anchors

  • State plan #page=9,10,11: use #page=8 and #page=9.
    • p. 8 holds heating eligibility and the rent rule.
    • p. 9 holds the vulnerability points, the electric benefit and the minimum and maximum.
    • Pages 10-11 are Section 3, Cooling, which Indiana does not operate.
  • Matrix #page=1,2: use #page=1, plus a separate #page=2 if the instructions are cited.
  • Manual: split the 33-page anchor list into single-page links.

G10. Cite the FY2026 matrix as a corroborating reference

  • Add FFY2026 matrix, p. 1 to the income-points, electricity, dwelling, fuel and vulnerability parameters. It prints all of them on one page and is what intake workers use.
  • Thirteen parameter reference titles also lack a manual section number.

G11. Roommates in the household unit

  • Manual 3.2, p. 23 counts roommates as household members "even if they are not related". SPM units separate unrelated roommates.
  • The approximation note at V/in_eap_household_size.py:13-17 does not mention roommates. Consider naming them. The bias direction is ambiguous.

G12. spm_unit.any on the citizenship flag

  • V/in_eap_eligible.py:21 can use add(spm_unit, period, ["is_citizen_or_legal_immigrant"]) > 0, following the repository pattern.
  • The age comparison on line 22 and the vulnerability test at V/in_eap.py:73 do need spm_unit.any.
  • Source: Manual 4.4, p. 29.

G13. programs.yaml merge conflicts and ordering

G14. A shared dwelling-type input across the sibling PRs

G15. Changelog wording

  • The fragment changelog.d/in-eap.added.md:1 is valid.
  • Its text describes internals ("with remaining income and administrative gaps documented"). A user-facing line reads better, for example: "Added Indiana Energy Assistance Program (EAP) regular heating assistance and the winter electric allowance."
  • Source: Manual 8.11, p. 77.

PDF Audit Summary

Check Result
FY2026 values (2025-10-01) 31 scalar entries in 19 files. 26 have a printed source value, and all 26 match the manual and the FFY2026 matrix, p. 1. The other 5 are fuel mappings with no printed value (G1)
FY2026 SMI thresholds 90 of 90 manual cells reproduced (pp. 69-70, 72-73). The size-11 limit of $94,863 follows 45 CFR 96.85
PY2024-25 SMI thresholds 48 of 48 PY2024-25 matrix cells reproduced
Values backfilled to 2025-01-01, checked against the PY2024-25 matrix and FFY2025 plan 22 comparable entries: 16 match and 6 do not (C1)
Mismatches that survive the code-path trace and the 600-dpi re-render 6, all in C1
FY2026 mismatches None
Income sources list 22 entries, all countable under manual Section 6.1 and FFY2026 plan pp. 5-7. One omission (S2) and two caveats (G2, G3)
#page= anchors 3 of 26 valid. 23 are comma lists, and 6 of those open the wrong page (C2)

The sources also disagree with each other in places. None of these produces a finding against the PR:

  • FFY2026 plan, p. 9 gives the electric benefit as "$50-$100". The manual (p. 72, p. 77) and the FFY2026 matrix pay $125 in band 1, and the PY2027 draft, p. 9 corrects the range to "$50-$125". The PR's $125 is correct.
  • The FFY2025 plan, p. 9 lists a $675 maximum, but the PY2024-25 matrix arithmetic gives $525.
  • In 28 of the 30 FY2026 cells, the manual's 12-month column is not 4 times its 3-month column. The 12-month cell is floor(SMI × r) and the 3-month cell is floor(SMI × r / 4).
    • The PR reproduces both columns and uses each where the manual does: annual for eligibility (p. 47) and the 3-month chart for points (p. 69).
    • The largest boundary effect is $4 a year.
  • Some manual text is stale:
    • Section 8.3 says "Points range from 2-6" (p. 69), a holdover from PY2025. The chart, Section 8.11 and the FFY2026 matrix all give 9/5/2.
    • The Section 8.3 SMI link points to IM 2022-04, but the printed FY2026 four-person SMI of $107,555 matches gov.hhs.smi.amount for Indiana on 2025-10-01.

Validation Summary

Check Result
CI 37/37 pass
Head moved since review No (c9febf1930)
Hard-coded values None. Formulas use only 0 and MONTHS_IN_YEAR
Naming and duplicates Pass. All names follow in_eap_*, and the shared hhs_smi.smi() helper is reused
Aggregation Pass. The per-source loop must floor each source, so adds cannot be used there
Periods One formula issue (S3) and one test-format issue (S4)
Entity levels Pass
Vectorization and microsim safety Pass. Every HeatingType and dwelling key exists in its enum, and in_eap_smi guards household size 0
defined_for Pass: in_eap → in_eap_eligible → StateCode.IN
References C2 (anchors), G9 (PR body), G10 (corroboration)
Parameter descriptions S5
Changelog Valid top-level fragment changelog.d/in-eap.added.md
TODO/FIXME None
Test coverage 34 cases in 4 files. 8 hand calculations match: band 1 and band 2 boundaries for sizes 1-10, 60% limits for sizes 1-10, size 11, a two-earner household of 3, an elderly widow, a disabled worker, and vulnerability counted once. Gaps are listed in S6
programs.yaml Entry complete. Conflict risk with 5 sibling PRs (G13)
Axiom parity line Present: n/a: rulespec-us does not have this program yet; it should be added as a follow-up.

Review Severity: REQUEST_CHANGES

2 critical and 6 should-address findings. There are also 15 suggestions.

Next Steps

  1. C1: add 2024-10-01 PY2024-25 entries to the six income-point, electric and fuel-point parameters. Replace the 2022 integration case with a 2025 case against the PY2024-25 matrix, and update the backfill comments.
  2. C2: split every comma-list #page= anchor into single-page title/href entries, fix the six wrong pages, and add section numbers to the titles.
  3. S1-S3: net Social Security using medicare_part_b_premium, add survivor_benefits to the sources, and read receives_ssi through add(...) > 0.
  4. S4-S5: change the test periods to 2026, and align the parameter descriptions with the template.
  5. S6: add the size-11 band, disability-proxy (including a negative case), heat-in-rent with rent under $12, and Part B floor tests.
  6. Rebase on main, coordinate the programs.yaml ordering with Add Kansas LIEAP regular heating assistance #9663, Add Nebraska LIHEAP regular heating assistance #9723, Add Kentucky LIHEAP regular heating assistance #9725, Add North Carolina LIEAP regular heating assistance #9726 and Add Mississippi LIHEAP regular heating assistance #9727, and re-run CI.

@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.

Requesting changes per the program review above (#9728 (comment)): 1 critical (each benefit parameter has a single 2025-10-01 entry, so the PY2024-25 period backfills FY2026 values that contradict the official PY2024-25 matrix: income points 9/5 vs 6/4, electric $125/$100 vs $100/$75, fuel points 1 vs 0), plus the should-address items in the report. Run /fix-pr 9728 or address the list.

hua7450 and others added 2 commits October 2, 2026 12:35
Add the PY2024-25 values for the income points, electric payments and fuel
points that differ from FY2026, and list the fuels that receive the heating
award in a parameter so natural gas and electric households are paid in
FY2025. Count survivor benefits, drop military retirement pay as a separate
source, and count Social Security gross with the Medicare premium deduction
left as a documented gap. Use single-page references with section numbers,
fix test periods and names, rewrite descriptions, and add size-11,
disability, heat-in-rent and FY2025 cases.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@hua7450

hua7450 commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Fixes are in 2cfdf94d64, followed by a merge of main (head 524a6ddfcb). Nothing was run locally; CI validates.

Critical

  • C1. The six PY2024-25 values are added from 2024-10-01. That exposed a second problem: the heating award was gated on fuel_points > 0, so a 2025 gas or electric household would have lost the award once its fuel points became 0. The fuels that receive the award are now a list parameter (benefit/heating_award_fuels.yaml), separate from the points. No FY2026 result changes. The 2022 case is replaced by 2025 households at $325 (gas), $475 (propane), $325 (electric) and $250 (band 2). verified_years stays "2026".
  • C2. Fixed: one entry per source with one page in the link and the section and pages in the title, or in a comment above the URL for variables. The six links now open the page that holds the value. The PR body cites plan pages 8-9.

Should-address

  • S1. Not wired. Social Security is counted gross, and a comment records that Section 6.1 nets out the Part B and Part D premiums and that neither medicare_part_b_premium nor medicare_part_b_premiums_reported is read yet. The two affected expectations are updated.
  • S2. survivor_benefits added, with a case. military_retirement_pay is removed from the list: the manual lists pensions and retirement payments only ("Military Allotments" is deployment pay), and pension_income covers it, so there is no overlap to check.
  • S3. Applied as you wrote it, but it does not change the result. For a MONTH-defined boolean requested for a year, add() passes no ADD option, so core returns the last month (bool is a stock), the same as the direct call and the Medicaid precedent. The December-only reading remains and is listed as a limitation in the PR body.
  • S4, S5. Periods and descriptions fixed.
  • S6. Size-11 bands, the disability proxies with a negative case, and heat in rent with rent under $12 are added. The "premium exceeds the benefit" case is not, given S1.

Suggestions

@DTrim99

DTrim99 commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Program Review — Round 2

PR #9728: Add Indiana EAP regular heating assistance (author: hua7450). This round reviews head 524a6ddfcb. Since the round-1 head c9febf1930, the branch has added fix commit 2cfdf94d64 ("Address Indiana EAP review") and a merge of main.

Path prefixes used below:

  • P/ = policyengine_us/parameters/gov/states/in/ihcda/eap/
  • V/ = policyengine_us/variables/gov/states/in/ihcda/eap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/in/ihcda/eap/

Status of Round-1 Findings

ID Round-1 finding Verdict Evidence
C1 Periods before 2025-10-01 backfilled FY2026 income points, electric amounts and fuel points FIXED
  • Six 2024-10-01 entries now match PY2024-25 matrix, p. 1: P/benefit/income/lower_points.yaml:3 (6), P/benefit/income/middle_points.yaml:3 (4), P/benefit/electricity/lower_income.yaml:3 ($100), P/benefit/electricity/middle_income.yaml:3 ($75) and P/benefit/fuel_points.yaml:6,9 (ELECTRICITY 0, NATURAL_GAS 0).
  • The new P/benefit/heating_award_fuels.yaml keeps the heating award for gas and electric heat when those fuels earn 0 points. For FY2026 it lists exactly the fuels the old fuel_points > 0 gate covered, so FY2026 results do not change.
  • The 2022 case is replaced by four 2025 households (T/integration.yaml:80-112). Each matches a hand calculation.
  • The backfill documentation at V/in_eap.py:10-15 is updated.
C2 Comma-list #page= anchors, six of which opened the wrong page FIXED
  • All 21 parameter references and 6 variable references now use a single page per href. Section and page ranges are in the title or in a comment.
  • The six wrong anchors now open p. 71 (fuel), p. 70 (middle points, middle rate, higher points), p. 72 (vulnerability points) and p. 77 (V/in_eap.py:22). Each of those pages holds the cited value.
  • The PR body anchors are also fixed (see G9).
S1 Social Security netting read the raw reported Part B input instead of medicare_part_b_premium NOT FIXED V/in_eap_income.py:39-44 now counts Social Security gross and reads neither premium variable. No tracking issue exists. Details under Should Address.
S2 survivor_benefits missing from the income sources FIXED Added at P/income/sources.yaml:31, with a source comment at lines 27-30 and a test at T/in_eap_income.yaml:151-165 ($20,160). The same commit removed military_retirement_pay (Suggestion 2).
S3 receives_ssi read only for December ACCEPTED-AS-RATIONALE
  • The prescribed add(...) > 0 is applied at V/in_eap.py:79.
  • The author is right that this changes nothing. In policyengine-core, for_each_variable passes options=None. The default quantity_type for a bool is STOCK, and a STOCK MONTH variable requested for a YEAR returns the last month (see the MONTH-to-YEAR branch of Simulation._calculate). The round-1 prescription was therefore wrong, and so was the Medicaid precedent it cited.
  • The remaining effect is at most $50. It reaches only households whose sole vulnerability evidence is a receives_ssi flag that varies by month. The PR body lists it as a limitation.
  • Suggestion 1 gives a one-line fix that works.
S4 21 test cases wrote the period as 2_026 or 2_022 FIXED All 38 cases now use period: 2026 or period: 2025.
S5 Parameter descriptions departed from the template FIXED
  • All 21 descriptions now use one of provides, sets, limits, uses or excludes, and each ends with "under the Energy Assistance Program."
  • The Social Security and SSI note moved to a YAML comment at P/income/sources.yaml:2-3.
S6 Missing edge-case tests FIXED
  • Size-11 bands: T/in_eap.yaml:268-292.
  • Disability proxies: SSDI alone, receives_ssi alone, and a negative case with only the disability flag, at T/in_eap.yaml:294-321.
  • Heat in rent with rent under $12: T/in_eap.yaml:323-343.
  • The Part B floor case no longer applies after the S1 change and belongs with the S1 fix.

Suggestions the author reports as taken, each verified at the new head:

ID Verified at
G1 (comments)
G2 P/income/sources.yaml:7-9 and V/in_eap_income.py:26-34
G3 P/income/sources.yaml:22-25
G4 P/eligibility/adult_age.yaml:1-4,12. The description and comment cover both uses. The title names Section 4.4 (page 29), but the href opens p. 47
G5 is_in_k12_school removed (V/in_eap_income.py:21-24)
G7 Mississippi comment removed from V/in_eap_smi.py. "Are not modeled" wording at V/in_eap_income.py:32-33
G8 T/integration.yaml Case 7 cites Section 6.1 (page 55)
G9 PR body links plan p. 8 (pages 8-9), matrix p. 1 and manual p. 69
G10 FY2026 matrix p. 1 added to the income-point, electric, dwelling, fuel and vulnerability parameters. Every title carries a section number
G11 Roommates named at V/in_eap_household_size.py:16-18
G12 V/in_eap_eligible.py:25
G13 policyengine_us/programs.yaml:805 lists IL, IN, TX
G15 changelog.d/in-eap.added.md:1

G6 and G14 were not taken. Both were optional, and no change is requested.

Value Verification (delta)

PY2024-25 values were checked against the PY2024-25 matrix, p. 1 and its instructions, p. 2 (revised 2024-07-24). FY2026 values were checked against the FFY2026 matrix, p. 1 (revised 2025-08-05) and Manual pp. 69-73 and 77. Both matrices were read from the page images.

Item PR value from 2024-10-01 PY2024-25 source PR value from 2025-10-01 FY2026 source Result
Income points, bands 1 / 2 / 3 6 / 4 / 2 (band 3 backfilled) 6 / 4 / 2 9 / 5 / 2 9 / 5 / 2 Match
Income points, heat in rent 2 (backfilled) 2 (p. 2) 2 2 Match
Electric, bands 1 / 2 / 3 $100 / $75 / $50 (band 3 backfilled) $100 / $75 / $50 $125 / $100 / $50 $125 / $100 / $50 Match
Electric, electricity in rent $50 (backfilled) $50 (p. 2) $50 $50 Match
Fuel points, natural gas / electric 0 / 0 0 / 0 1 / 1 1 / 1 Match
Fuel points, FUEL_OIL, PROPANE, COAL, WOOD, OTHER 6 (backfilled) Bulk fuels 6, biofuel 6 6 6 Match. OTHER is a documented assumption
Dwelling, single-family / mobile / multi-unit / heat in rent 3 / 2 / 1 / 0 (backfilled) 3 / 2 / 1 / 0 3 / 2 / 1 / 0 3 / 2 / 1 / 0 Match
Vulnerability points; elderly and child ages 2; 60; 5 (backfilled) 2; 60+; 5 or younger 2; 60; 5 2; 60+; 5 or younger Match
Amount per point $25 (backfilled) $25 $25 $25 Match
Maximum regular benefit (6+3+6+2) × $25 + $100 = $525 $525 by matrix arithmetic. The FFY2025 plan prints $675, a source inconsistency already noted in round 1 20 × $25 + $125 = $625 Manual 8.11, p. 77 and FFY2026 plan, p. 9: $625 Match
Minimum regular benefit 2 × $25 + $50 = $100 FFY2025 plan, p. 9: $100 2 × $25 + $50 = $100 Manual 8.11 and FFY2026 plan: $100 Match. The Section 8.11 table derives the minimum and maximum from the point and electric ranges, so neither needs its own floor or cap parameter
Effective date 2024-10-01, the start of PY2024-25 (FFY2025) — 2025-10-01, the start of FFY2026 (plan p. 4 runs heating from 10/01/2025 to 04/20/2026) — Correct

FY2025 state median income, size 1: floor($103,324 × 0.52) = $53,728. The three-month ceilings are floor($53,728 × 0.075) = $4,029 and floor($53,728 × 0.1125) = $6,044, both matching matrix p. 1.

Hand checks of the new and changed tests:

  • T/integration.yaml:80-112 (period 2025).
    • Households 1-3 earn $12,480, so three-month income is $3,120, within $4,029: band 1.
    • Gas: (6 + 3 + 0) × $25 + $100 = $325. Propane: (6 + 3 + 6) × $25 + $100 = $475. Electric heat: $325.
    • Household 4 earns $20,800, so three-month income is $5,200: band 2. (4 + 3 + 0) × $25 + $75 = $250.
    • All four match.
  • T/in_eap.yaml:268-292 (size 11). SMI = floor($107,555 × 1.47) = $158,105. The three-month ceilings are $11,857 and $17,786, giving $450, $325, $325 and $200. Match.
  • T/in_eap.yaml:294-321.
    • SSDI of $14,400 gives three-month income of $3,600, band 1: (9 + 3 + 6 + 2) × $25 + $125 = $625.
    • Reported SSI receipt: $625.
    • Disability flag alone: (9 + 3 + 6) × $25 + $125 = $575.
    • All match.
  • T/in_eap.yaml:323-343. Rent of $0 or $11 a year pays the $125 electric benefit only. Rent of $12 pays 2 × $25 + $125 = $175. Match.
  • T/in_eap_income.yaml:151-165. $10,800 + $9,360 = $20,160. Match.
  • Re-based to gross Social Security. T/in_eap_income.yaml:35-55 now expects $22,800. T/integration.yaml:188-226 now expects $21,600, which is still band 2 and $500. The arithmetic matches the code; the rule itself is S1.

Critical (Must Fix)

None.

Should Address (remaining)

S1. Social Security is counted gross, contrary to Section 6.1, and the reported-premium netting has been removed

  • Where: V/in_eap_income.py:39-44, which sets ssa = person("social_security", period) + person("ssi", period).

  • Rule:

    • Manual 6.1, p. 55: "The net amount of the Social Security check, after the deduction for Medicare Part B premiums and/or Part D ... is to be used to compute income."
    • FFY2026 plan, p. 6 counts Social Security "Excluding MediCare deduction".
  • Change since round 1: the round-1 code netted medicare_part_b_premiums_reported when a user supplied it. The fix commit removed that netting instead of switching to medicare_part_b_premium. The premium is now ignored even when it is reported.

  • Author reply: "Not wired." The reply names no technical obstacle, and none is apparent:

    • medicare_part_b_premium already exists: policyengine_us/variables/gov/hhs/medicare/eligibility/part_b/medicare_part_b_premium.py (Person, YEAR, defined_for = "medicare_enrolled", net of Medicare Savings Program coverage).
    • in_eap is not referenced outside its own folder, so reading the premium cannot create a cycle.
  • Tracking: policyengine-us has no tracking issue. rulespec-us#1468 records the gap as "settled; not modeled yet".

  • Effect. Hand calculation for 2026:

    • Household: one Medicare-enrolled person aged 70 with Social Security only, natural gas heat, a single-family home and both bills.
    • Premium: the 2026 standard premium of $202.90 × 12 = $2,434.80. Income is above the Qualifying Individual limit, so there is no Medicare Savings Program coverage.
    Social Security PR (gross) Section 6.1 (net of Part B)
    $25,500 Three-month $6,375, band 3: (2+3+1+2) × $25 + $50 = $250 $23,065.20, three-month $5,766.30, band 2: (5+3+1+2) × $25 + $100 = $375
    $34,000 Above the $33,556 limit: $0 $31,565.20, eligible, band 3: $250
  • Bias: countable income is overstated by the out-of-pocket Part B premium for every Medicare-enrolled Social Security recipient. Eligibility and band placement are therefore biased downward for elderly households, which Section 3.4 names as a priority group.

  • Fix (either option resolves this item):

    1. Per person, set ssa = max_(social_security - medicare_part_b_premium, 0) + ssi. Restore an elderly test that uses the model's computed premium, and add a case where the premium exceeds the benefit to exercise the floor.
    2. Or open a policyengine-us tracking issue for the Part B and Part D netting. Link it from the comment at V/in_eap_income.py:39-43 and from the PR body limitation.

Suggestions

  1. receives_ssi across the year (V/in_eap.py:79). add(person, period, ["receives_ssi"], options=[ADD]) > 0 sums the 12 monthly flags, and Simulation.calculate_add has no value-type restriction. This is optional, given the effect of at most $50.
  2. Removal of military_retirement_pay (P/income/sources.yaml:21; T/in_eap_income.yaml:82-99).
    • The reply says pension_income covers military retirement. It does not: pension_income adds only the taxable and tax-exempt public and private pension inputs, and none of them includes military_retirement_pay, which is a separate input.
    • Repository practice is split. The federal TANF and SSI unearned-income lists count pension_income without military_retirement_pay. Twenty-nine state benefit source lists count both.
    • The manual counts "Ongoing pension payments" (p. 54) and "Any ongoing, recurring retirement payments" (p. 55).
    • A household that reports military retirement only in military_retirement_pay now has it excluded.
    • Fix: restore the input, or add a comment in sources.yaml saying that military retirement must be reported as a public pension.
  3. Wrong test pointer in rulespec-us#1468. Companion test 14 cites "in_eap.yaml Case 4", but the period-2025 households are in integration.yaml Case 4. in_eap.yaml Case 4 is the FY2026 band 2 boundary.

Branch and Axiom Status

Item Status
Head 524a6ddfcb (fix commit 2cfdf94d64 plus a merge of main)
Mergeability MERGEABLE. Merge state is BLOCKED because the review decision is CHANGES_REQUESTED
Axiom line axiom: TheAxiomFoundation/rulespec-us#1468 queued
rulespec-us#1468
  • Open and labelled pe-parity. It follows the same section structure as Add AMT parameters for 2020-2023 #1467.
  • Module paths: each provision has a corpus citation and ingest status. The provisions cover the PY2026 manual sections, both benefit matrices, the FY2026 plan, the IM SMI tables and 45 CFR 96.85.
  • It also has verbatim source text, required outputs for all five computed variables, and interpretations and gaps.
  • Companion tests: 14 cases, with hand derivations from the manual and the matrices. They include the FY2025 households.
  • Dispatch-ready, apart from one wrong file pointer (Suggestion 3).

CI Status

35 of 37 checks pass. The two failures are both runner kills unrelated to this PR.

Job Classification Evidence
Full Suite - Baseline (states-shard-3) Runner kill Batches 1 and 2 passed (1,505 and 1,901 tests). Batch 3 stalled in the Oklahoma tests, then: "The runner has received a shutdown signal", exit code 143. No test failed. No Indiana test runs in this shard
Full Suite - Baseline (ssa-usda) Runner kill Every printed batch passed (14, 10, 88, 3 and 165 tests), and every printed file shows only dots through usda/wic/wic.yaml. After about 880 seconds with no output: "make: *** [Makefile:142: test-yaml-no-structural-other-usda] Terminated", exit code 143. No test failed

Review Severity: REQUEST_CHANGES

There are 0 critical findings, 1 remaining should-address finding (S1) and 3 suggestions.

  • Resolved: both critical items and five of the six should-address items. Every new PY2024-25 value matches the official matrix, and every FY2026 value is unchanged and still matches.
  • Remaining blocker: S1. Either net Social Security by medicare_part_b_premium, or track the deferral in an issue linked from the code comment and the PR body.

@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 (#9728 (comment)): the critical PY2024-25 backfill and the other should items are resolved. One item remains: Social Security is now counted gross, but Manual 6.1 (p. 55) and the FFY2026 plan (p. 6) use the net check after the Medicare Part B/D deduction. The fix commit removed the reported-premium netting instead of switching to the existing medicare_part_b_premium variable.

hua7450 and others added 2 commits October 2, 2026 22:08
…itary retirement pay

Manual Section 6.1 counts the Social Security check net of the Medicare Part B premium, so the premium the person pays, from the existing medicare_part_b_premium variable, is subtracted; there is no Part D premium variable. military_retirement_pay is a separate input from the pension inputs that pension_income adds, so it is listed beside pension_income.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…into in-eap

# Conflicts:
#	policyengine_us/programs.yaml
@hua7450

hua7450 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for round 2. Head is now 2a04f374f4: fix commit 661fff1bc3 plus a merge of main (which brings in #9758).

S1, Social Security and the Medicare premium. Fixed with your first option. in_eap_income now counts max_(social_security - medicare_part_b_premium, 0) + ssi for every member. medicare_part_b_premium is zero for a person who is not enrolled and excludes the share a Medicare Savings Program pays, which is not withheld from the check; the model has no Part D premium variable, so that part of Section 6.1 stays a documented gap. integration.yaml Case 7 is re-based so the deduction is visible: the widow now has $21,600 of Social Security and a $5,000 pension; her countable income is above the MSP limit, so she pays the $2,434.80 premium, her income is $24,165.20, three-month $6,041.30, band 2, and she still gets $500 (counted gross she would be band 3 and get $375). New in_eap_income.yaml Case 9 covers an MSP-covered retiree ($12,000 counted in full), the floor (a $2,000 benefit with a larger premium counts $0) and a 40-year-old on SSDI who is not enrolled.

Suggestion 2, military retirement pay. You are right that pension_income does not include military_retirement_pay; my reply was wrong on that. military_retirement_pay is restored beside pension_income, with a comment that the two are separate inputs, and the $18,000 military pension in in_eap_income.yaml Case 5 now enters through it.

Suggestion 3. The companion test pointer in rulespec-us#1468 now reads integration.yaml Case 4. The issue is also updated for the S1 change and for military retirement pay.

Suggestion 1. Not taken in this round.

Tests were not run locally; CI runs them.

@hua7450
hua7450 requested a review from DTrim99 October 3, 2026 02:10
@DTrim99

DTrim99 commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Program Review — Round 3

PR #9728: Add Indiana EAP regular heating assistance (hua7450). This round reviews head 2a04f374f4, which adds fix commit 661fff1bc3 ("Deduct the Medicare Part B premium from Social Security and count military retirement pay") and a merge of main (merge base 7bc185b0cc, which includes #9663 and #9758). Round 2 reviewed 524a6ddfcb.

Path prefixes (same as earlier rounds):

  • P/ = policyengine_us/parameters/gov/states/in/ihcda/eap/
  • V/ = policyengine_us/variables/gov/states/in/ihcda/eap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/in/ihcda/eap/

Scope at this head: 33 files: 21 parameters, 6 variables, 4 test files with 39 cases (in_eap.yaml 16, in_eap_eligible.yaml 5, in_eap_income.yaml 9, integration.yaml 9; round 2 had 38), a changelog fragment and programs.yaml. Line numbers below refer to head 2a04f374f4.

Status of Round-2 Findings

ID Round-2 finding Verdict Evidence
S1 Social Security counted gross, contrary to Manual 6.1 (p55) and the FFY2026 plan (p6) FIXED
  • V/in_eap_income.py:46-48: for every member, social_security = max_(social_security - medicare_part_b_premium, 0), then ssa = social_security + ssi.
  • The comment at :39-45 cites Section 6.1 (page 55) and records that Part D, withholding, overpayment recovery and garnishment are not modeled.
  • T/integration.yaml Case 7 is re-based so the deduction changes the band ($500 net against $375 gross).
  • New T/in_eap_income.yaml Case 9 covers MSP coverage, the floor and a non-enrollee.
  • The T/in_eap_income.yaml Case 3 comment now explains why no premium applies.
Suggestion 1 receives_ssi read only for the last month Not taken (optional) V/in_eap.py:79 is unchanged. Carried over.
Suggestion 2 Removal of military_retirement_pay FIXED P/income/sources.yaml:22-26 restores it beside pension_income, with a source comment. T/in_eap_income.yaml Case 5 enters the $18,000 military pension through military_retirement_pay. Source support is below.
Suggestion 3 Wrong test pointer in rulespec-us#1468 FIXED Companion test 14 now cites "integration.yaml Case 4", the period-2025 households.

Verification of the Medicare Fix

Years. The deduction is unconditional, so it applies in period 2025 (FFY2025) and period 2026 (FFY2026). Both plans support that, read from the page images:

Source Text or checkbox Applies to
FFY2025 plan, §1.9, p6 Social Security: "Excluding MediCare deduction" checked; "Including" unchecked Period 2025
FFY2026 plan, §1.9, p6 Same: "Excluding MediCare deduction" checked Period 2026
PY2026 Manual 6.1, p55 "The net amount of the Social Security check, after the deduction for Medicare Part B premiums and/or Part D, overpayment recovery, tax withholdings, and child support garnishments, is to be used to compute income." Period 2026
PY2026 Manual 6.2, p61 "The premium for Medicare Part B is not included as income." Period 2026

No PY2025 manual was found (the analogous IHCDA ...Manual-PY2025.pdf URL returned 404 on 2026-10-01), so the FFY2025 plan is the period-2025 source. Unlike Kentucky, where the FY2025 plan checked "Including", Indiana checks "Excluding" in both years. A dated toggle is therefore not needed.

Variable. medicare_part_b_premium (Person, YEAR, defined_for = "medicare_enrolled") is max_(gross_medicare_part_b_premium - msp_part_b_premium_coverage, 0).

  • What it covers: the standard premium plus the Part B IRMAA, less the standard premium in each month the enrollee meets the MSP income (135% of the guideline after the $20 exclusion) and asset tests.
  • Why that matches Section 6.1: a premium paid by a Medicare Savings Program is not withheld from the check, so this matches the manual's "net check" rule for Part B.
  • Non-enrollees: under 65 without 24 months of SSDI (months_receiving_social_security_disability defaults to 0), or not taking up Medicare, they get $0.

Part D. Section 6.1 names "Part B premiums and/or Part D". Plan-specific Part D premiums have no model variable. The model does have income_adjusted_part_d_premium_surcharge (the Part D IRMAA, also withheld from the check). It starts at a two-year-prior MAGI above $109,000 (single, 2026), against a one-person EAP limit of $33,556, so leaving it out has a negligible effect. The code comment and PR body say "the model has no Part D premium variable", which is imprecise (Suggestion 1).

Hand checks (2026 standard premium $202.90 × 12 = $2,434.80; 2026 MSP limit 1.35 × $15,960 / 12 = $1,795.50 a month):

Test Inputs Hand calculation Expected Result
T/integration.yaml Case 7 (2026) Age 72; Social Security $21,600; private pension $5,000; wood; single-family MSP countable (26,600 − 240) / 12 = $2,196.67 > $1,795.50, so the premium is paid. Income (21,600 − 2,434.80) + 5,000 = $24,165.20 ≤ $33,556 (floor(floor(107,555 × 0.52) × 0.6)). SSI $0, since $2,196.67 > $994. Three-month $6,041.30 is above $4,194 and within $6,291: band 2, 5 points, $100. (5 + 3 + 6 + 2) × $25 + $100 = $500. Gross three-month $6,650 is band 3: 13 × $25 + $50 = $375 premium 2,434.8; income 24,165.2; $500 Match; the case discriminates
T/in_eap_income.yaml Case 9, household 1 Age 70, Social Security $12,000 $980 < $1,795.50, so the MSP pays: $0 deducted 0; 12,000 Match
Case 9, household 2 Age 70, $2,000 + $24,000 private pension $2,146.67 > limit; max(2,000 − 2,434.80, 0) + 24,000 2,434.8; 24,000 Match
Case 9, household 3 Age 40, SSDI $12,000 Not Medicare-eligible (0 months of SSDI): $0 0; 12,000 Match
T/in_eap_income.yaml Case 3 Age 40, social_security $12,000, SSI $1,200, wages $12,000, support paid $2,400 Not eligible for Medicare: 12,000 + 12,000 + 1,200 − 2,400 22,800 Match
T/in_eap_income.yaml Case 5, household 6 military_retirement_pay $18,000 18,000 18,000 Match

Other existing tests. No other case has a Social Security recipient aged 65 or older:

  • T/in_eap.yaml Case 15 (SSDI $14,400 at age 40) is not Medicare-eligible.
  • The age-60 members in T/in_eap.yaml Cases 6 and 13 are under 65.

Their expectations are unaffected.

Microsimulation note (informational). msp_part_b_premium_coverage assumes every income- and asset-eligible enrollee is covered by an MSP. In microsimulation, therefore, the deduction reaches only enrollees above the MSP limits. This is a property of the shared variable, not of this PR.

Military Retirement Pay

  • Sources:
    • Manual 6.1, p47: "military allotments, pensions, self-employment and other retirement payments".
    • p54: "Ongoing pension payments are counted as income."
    • p55: "Any ongoing, recurring retirement payments are counted as income."
    • Both plans check "Retirement / pension benefits" on p6.
  • No double counting:
    • pension_income adds only tax_exempt_pension_income and taxable_pension_income.
    • retirement_distributions adds only the retirement-distribution inputs.
    • veterans_benefits and survivor_benefits are separate inputs.
    • None of them contains military_retirement_pay.

Axiom Issue (rulespec-us#1468)

Compared against the structure of #1467:

Requirement Status at 2026-10-03 update
pe-parity label Present (with enhancement). The issue is open.
PR link #9728, head 2a04f374f4, with scope and policy periods.
Module paths and corpus citations Each PY2026 manual section (including 6-income-computations and 6-2-what-is-not-counted-as-income), the FY2026 plan sections (in manifest us-in-liheap-state-plan-fy2026.yaml), both benefit matrices, the SMI tables and 45 CFR 96.85, each with an ingest status.
Verbatim law Manual Sections 3.4, 4.4, 6.1 (pension and counted income) and 6.2 (including "The premium for Medicare Part B is not included as income."), plus plan Sections 1.9 and 2.4-2.6.
Required outputs All five computed variables. The in_eap_income definition now states Social Security net of medicare_part_b_premium (floored) and lists military retirement pay among the inputs.
Companion tests 14 cases with hand derivations from the manual charts and both matrices. Test 14 now points to integration.yaml Case 4.

The issue remains dispatch-ready. Refresh items are in Suggestion 3.

Main-Merge Check (#9672, #9671, #9727)

Programs.yaml Merge Resolution

Diffed against the merge base 7bc185b0cc:

  • The only changes are the coverage: line (..., IL, IN, KS, TX) and the new IN block before KS.
  • The Kansas entry from Add Kansas LIEAP regular heating assistance #9663 is byte-identical to main.
  • The file parses with yaml.safe_load: 103 programs, no duplicate ids. LIHEAP state_implementations order is OR, DC, MA, IL, IN, KS, TX.

Regression Pass over the Delta

  • Formula: the premium is subtracted per person before members are summed, so one member's premium cannot reduce another member's Social Security. The floor keeps the subtraction within the Social Security benefit, and SSI is not reduced.
  • Comments: V/in_eap_income.py:39-45 and P/income/sources.yaml:22-25 match the code.
  • Tests: changed and new expectations use absolute_error_margin: 0.01 where cents occur.
  • Code patterns: no hardcoded policy values were added, and the entities and defined_for are unchanged.
  • PR body: the Income bullet describes the Medicare deduction and military retirement pay. The validation line states 39 cases, which matches the head.
  • No regressions found.

Critical (Must Fix)

None.

Should Address (remaining)

None. S1 is resolved.

Suggestions

New in this round:

  1. Part D wording (V/in_eap_income.py:43-45, the PR body Income bullet and the rulespec-us#1468 gaps). "The model has no Part D premium variable" is accurate for plan premiums only. income_adjusted_part_d_premium_surcharge (the Part D IRMAA, withheld from the check) exists. Either subtract it alongside the Part B premium, or reword to "no Part D plan-premium variable; the Part D IRMAA surcharge is not deducted". The practical effect is negligible: the surcharge starts at a two-year-prior MAGI above $109,000.
  2. Period-2025 source for the deduction. V/in_eap_income.py cites only the PY2026 manual (p47). Adding the FFY2025 plan, p6 and the FFY2026 plan, p6 ("Excluding MediCare deduction") would document why the deduction also applies to period 2025.
  3. rulespec-us#1468 refresh:
    • "38 YAML cases … in_eap_income.yaml (8)" should read 39 and 9.
    • The Medicare deduction appears only under "Interpretations and gaps". A companion row for integration.yaml Case 7 would put it in the oracle set: CMS 2026 premium $202.90 a month, income $24,165.20, band 2, $500, against $375 gross.
    • The FFY2025 plan is not cited; its p6 supports the period-2025 treatment.

Carried over from round 2, optional:

  1. receives_ssi across the year (V/in_eap.py:79). add(person, period, ["receives_ssi"], options=[ADD]) > 0 would read all 12 monthly flags. The effect is at most $50.

CI and Branch Status

  • Checks: gh pr checks 9728 shows 37 of 37 checks passing on head 2a04f374f4 (run 37088747228, pull_request). They include:
    • Lint and the changelog check;
    • Baseline states shards 1-4, ssa-usda and contrib-hhs;
    • Microsimulation and Household API Partners;
    • Quick Feedback and codecov patch/project.
  • No failures: the two runner kills from round 2 did not recur.
  • Merge state: CONFLICTING / DIRTY, with review decision CHANGES_REQUESTED from round 2.

Review Severity: APPROVE

0 critical, 0 should-address and 4 suggestions (3 new, 1 carried over).

  • S1 is fixed: Social Security is counted net of medicare_part_b_premium in both modeled years, as Manual 6.1 and both plans direct. The re-based integration case and the new income case match hand calculations, and the integration case changes the band.
  • Military retirement pay is restored, with manual and plan support.
  • The Add AMT integration test #1468 pointer is corrected.
  • Before merge: the programs.yaml conflict with main needs to be resolved.

@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 (#9728 (comment)): the Medicare Part B deduction and military retirement pay now match the state sources, the axiom issue is dispatch-ready, and the programs.yaml merge is correct; 0 critical, 0 should. The income lists are self-contained, so main's #9672 (snap_rental_income) does not cause double counting. #9727's merge reopened a programs.yaml conflict with main to resolve before merging.

…into in-eap

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

2 participants