Skip to content

Add Minnesota EAP Primary Heat benefit - #10433

Open
hua7450 wants to merge 2 commits into
mainfrom
mn-liheap-wave1d
Open

hua7450 wants to merge 2 commits into
mainfrom
mn-liheap-wave1d

Conversation

@hua7450

@hua7450 hua7450 commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

Adds Minnesota's Energy Assistance Program (EAP) Primary Heat benefit for FFY2026 and FFY2027, administered by the Minnesota Department of Commerce. The model counts citizens and eligible noncitizens in household size (ineligible members' income still counts), tests countable income against 50% of state median income with the federal 110% poverty floor, and pays a share of the household's total annual energy cost that falls linearly as income rises. The result is rounded and held between $200 and $1,400. Primary Heat is Minnesota's single regular benefit and is based on all home energy costs, including non-heating electricity. Crisis, Energy Related Repair and weatherization are out of scope.

Fixes #10423

Regulatory authority

Eligibility

Requirement Source How modeled
Minnesota residence Manual ch. 3 defined_for = StateCode.MN
At least one citizen or eligible noncitizen Manual FFY26 p.80-81 mn_eap_household_size sums is_citizen_or_legal_immigrant; size 0 is ineligible
Income at or below the one-month maximum Manual FFY26 p.88-89, FFY27 p.90 mn_eap_income_limit = 12 x max(floor(50% of size-adjusted SMI / 12), floor(110% of prior-year FPG / 12)); reproduces all 40 published monthly cells (FFY26, FFY27)
Federal 110% poverty floor 42 U.S.C. 8624(b)(2)(B) Built into the limit; binds at sizes 19-20 and continues above size 20, where the table stops
Vulnerable to rising energy costs Manual FFY26 p.72, 84, 126 Pays an energy vendor (positive energy cost), or unsubsidized heat in rent; subsidized heat in rent needs an electric bill; subsidized with heat and electricity included is ineligible
Heating fuel reported Brief convention heating_type UNSPECIFIED or NONE pays $0
No categorical eligibility, no asset test Plan 1.4, 2.3 Not applied

Countable income

Item Source How modeled
Earned and self-employment income Manual FFY26 p.89, 91, 96, 107-108 Wages, net self-employment, partnership/S-corp, farm operations, farm rent and rental income, each floored at zero per person (a loss on one return is not subtracted from another)
Student exclusion Manual FFY26 p.91, 99 Earnings and unemployment insurance of members 19 and younger in K-12 (is_in_k12_school) are excluded
Unearned income Manual FFY26 p.89-99 SSI, general assistance, military retirement (with survivor annuities), railroad, veterans, workers' compensation, disability, alimony, strike benefits, trust income, survivor benefits, unemployment
Minnesota Paid Leave Manual FFY26 p.88; FFY27 p.96, 116 pfml_benefits counted from FFY2026: the FFY26 manual counts gross cash receipts with no Paid Leave exclusion, Paid Leave began January 1, 2026, and the FFY27 manual lists it as countable
Social Security and pensions Manual FFY26 p.96, 111 Net of medicare_part_b_premium (subtracted once from Social Security plus pension; PERA example)
Retirement account withdrawals Manual FFY26 p.96, 111 Counted from age 59 1/2 only
Dividends and interest; cash gifts Manual FFY26 p.90, 92 Counted only when the monthly equivalent is at least $50 (dividends, interest) or $100 (gifts)
MFIP and other TANF cash Manual FFY26 p.89 tanf
Not counted Manual FFY26 p.87-97 Child support, capital gains, student aid, Minnesota Supplemental Aid, foster care, tax refunds, SNAP

Benefit

Constant (Appendix 4A Formula Parameters) FFY2026 FFY2027
Maximum pay share (C4) 43.9% 45.2%
Minimum pay share (C5) 10.8% 11.3%
Slope start / end percentile (C6, C7) 0 / 0.896 0 / 0.891
Minimum / maximum benefit (C2, C3; Manual p.124 / p.126) $200 / $1,400 $200 / $1,400
  • Income percentile = countable income / income limit. Pay share = maximum share minus (maximum - minimum share) x percentile / slope end, held at the minimum share from the slope end (Manual FFY26 p.124-125; Appendix 4A Calculator).
  • Benefit = pay share x annual energy cost, rounded half up to whole dollars, then held between $200 and $1,400. The floor applies at any positive cost.
  • Annual energy cost: households paying their own energy use the total of all energy bills (pre_subsidy_electricity_expense, gas, fuel oil, propane, wood, coal, other fuel). Unsubsidized heat-in-rent households use the backup matrix (average annual energy cost by main fuel and housing type, 7 fuels x 8 housing types per year). Subsidized heat-in-rent households use their electric cost (Manual FFY26 p.117-119, 125-126).
  • New input mn_eap_housing_type (Apartment, Duplex, Triplex, Fourplex, Townhouse, House, Mobile home, Other; default House) selects the backup-matrix column. Oil (fuel oil and kerosene), propane, natural gas and electricity rows are selected through heating_type; the steam, district heating and metered propane rows are stored as published, but no existing input selects them.
  • Qualified Eligibility: households whose only income is DWP, General Assistance, MFIP or SSI are "eligible at the lowest level of income" (Manual FFY26 p.102, FFY27 p.103); the model reads this as an income percentile of zero, which gives the maximum share.

Not modeled

Item Source Treatment
Prior-year 12-month consumption window Manual FFY26 p.118 Current-year expense inputs stand in
One calendar month of income Manual FFY26 p.88 Annual income is compared with 12 times the monthly limit
eHEAT larger-of (partial-year data) and lower-of (business use, shared meter, outbuildings) rules; fuel-change invalidation; biofuel worksheet; $1 extended-absence entry Manual FFY26 p.120-127 Not represented
Backup matrix for an own-payer without consumption data; $200 minimum for a subsidized heat-in-rent household with no electric use Manual FFY26 p.120, 126 A zero expense input is treated as no cost
Steam and district heating fuels; wood, coal and other fuels with heat in rent Backup matrix No heating_type value or matrix row; benefit $0
Unpenalized retirement withdrawals before age 59 1/2 Manual FFY26 p.111, FFY27 p.113 No input shows whether a withdrawal was penalized; the age cut-off stands in for the penalty test, so these withdrawals are not counted
50% gross-receipts self-employment method; Medicare Part D deductions; DWP, MFIP housing grants, Housing Support, tribal payments, irregular income, jury pay, inheritances, lump sums Manual FFY26 p.89-108 No existing inputs
SSN and documentation rules, dwelling-eligibility tool, institutions, application deadline, one grant per year Manual ch. 3 Not represented
Crisis, Energy Related Repair, weatherization Plan 1.1 Out of scope

Earlier years are unverified estimates through parameter backfilling. The pay-share constants appear only in the Appendix 4A calculator; the FFY27 manual calls the calculator an optional estimating tool, while eHEAT, the system of record, is not public. The size-21+ limit follows the federal floor because the manual table stops at size 20.

The stale note on the shared pfml_benefits input saying it is used only for Massachusetts CCFA is removed, since Minnesota now reads it.

Microsimulation: heating_type, the per-fuel bills and mn_eap_housing_type are not in current datasets; callers must supply them.

Test plan

36 YAML cases in 7 files (not re-run locally after the review fixes; CI validates them):

  • income limits at sizes 0, 1, 4, 18, 19 and 21 for both years, and the exact-limit boundary;

  • countable income: a self-employment loss, an ineligible member's wages, the student exclusion at 19 and 20, Social Security and pension net of Part B, retirement withdrawals at age 59 and exactly 59 1/2, the $50 and $100 thresholds, SSI, veterans benefits, TANF, child support excluded, Paid Leave counted in FFY2026;

  • energy cost across fuels, backup-matrix cells for both years, kerosene on the oil row, subsidized heat in rent, wood with heat in rent;

  • pay share at zero income, above the slope end, Appendix 4A Calculator row 2 for both years, Qualified Eligibility;

  • the $200 floor, the $1,400 ceiling, half-up rounding, and two end-to-end households (2026 and 2027).

  • YAML tests with expected values from the manuals and Appendix 4A

  • ruff format

  • CI passes (program tests, registry and code-health checks)

  • Changelog fragment in changelog.d/

axiom: TheAxiomFoundation/rulespec-us#1894 queued

🤖 Generated with Claude Code

hua7450 and others added 2 commits October 23, 2026 18:11
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	policyengine_us/programs.yaml
@DTrim99

DTrim99 commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Add Minnesota EAP Primary Heat benefit

PR #10433 (author: hua7450), head 8ef6ef80e5. This is a first review. The PR adds Primary Heat, the single regular benefit of Minnesota's Energy Assistance Program (EAP, Department of Commerce), for FFY2026 and FFY2027. It includes:

  • 24 parameters and 8 variables (7 computed, plus the new input mn_eap_housing_type);
  • 7 test files (36 cases);
  • a programs.yaml entry (coverage line and state list);
  • the removal of a stale comment in the shared pfml_benefits input;
  • a changelog fragment.

The PR has no comments or reviews yet, and it touches no partner test file.

Path prefixes:

  • P/ = policyengine_us/parameters/gov/states/mn/commerce/eap/
  • V/ = policyengine_us/variables/gov/states/mn/commerce/eap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/mn/commerce/eap/

Sources read for this review:

  • Manuals:
  • State plan: the FY2026 Detailed Model Plan. I rendered pages 5-9 to read the boxes and read page 4 as text. No FY2027 plan is posted.
  • Appendix 4A, the Primary Heat Benefit Calculator:
    • the FFY26 workbook (Clearinghouse; modified 2025-12-01);
    • the FFY27 workbook (state URL; served with Last-Modified 2026-10-14).
    • I parsed every cell of Formula Parameters, Income Guidelines and Backup Matrix Values, plus the Calculator formulas.
  • Federal: 42 U.S.C. 8624(b)(2)(B), 42 U.S.C. 426(a), LIHEAP IM 2025-02 Attachment 2, and the Railroad Retirement Board's Medicare Coverage page.
  • Law: no Minnesota statute or rule governs EAP income or benefits. Minn. Stat. § 216C.266, the only EAP section the manual cites, covers data privacy. So the plan and the manual are the governing sources.

Summary

What is right. The published FFY2026 and FFY2027 values are encoded correctly:

  • Income limits. 12 × max(floor(round(0.5 × size-adjusted SMI, 2) / 12), floor(round(1.1 × prior-year guideline, 2) / 12)) reproduces all 40 published one-month cells (FFY26 manual pages 88-89, FFY27 page 90, and both workbooks' Income Guidelines!B2:B21). The 110% floor binds from size 19 and carries past the table's size 20, as 42 U.S.C. 8624(b)(2)(B) requires.
  • Benefit. These match Formula Parameters!C2:C7 and the Calculator formulas (F2, G2, H2 = ROUND(G2*C2, 0), I2) of each edition:
    • the pay-share line (0.439 / 0.108 / 0 / 0.896 in FFY26; 0.452 / 0.113 / 0 / 0.891 in FFY27);
    • half-up rounding;
    • the $200 floor and the $1,400 ceiling.
  • Backup matrix. All 112 cells (7 fuels × 8 housing types × 2 years) match the workbooks, and every cited cell range is the right one.
  • Energy cost. Own-payers use total energy cost including non-heat electricity (manual page 117). Heat in unsubsidized rent uses the backup matrix, and heat in subsidized rent uses the electric bill (page 126).
  • Countable income. It follows the manual's table: gross receipts; per-source loss floors; the K-12 student exclusion at 19; retirement withdrawals from 59½; the $50 and $100 thresholds; Social Security and pensions net of Part B (page 96, the PERA note on page 111, and plan 1.9 "Excluding MediCare deduction"); MSA and child support excluded.
  • Tests. I hand-checked 22 expectations below, and CI passed all 36 cases.
  • Batch checks. The guards all pass: Integrate modeled state unemployment insurance into income flows #9558, Count military SBP annuities in state benefit income lists #10317, the Count each Social Security benefit once in Pennsylvania CCW countable income #10408 pattern and the Tie source-conflict markers to the register shared with Axiom #10287 ratchet (on the merged tree too). Item 17 passes: the manual excludes Minnesota Supplemental Aid, and the PR leaves mn_msa out on purpose.

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

  • S1. Railroad annuities are counted gross of Medicare. Social Security and pensions are counted net of the Part B premium, but railroad retirement annuities are counted gross, although RRB withholds Part B from them in the same way. A railroad retiree with $38,000 gets $0 instead of $216.
  • S2. Untested sources. 6 of the 7 earned sources, 9 of the 12 unearned sources, 2 of the 3 Qualified Eligibility sources and 3 of the 7 energy-cost sources never take a positive value in any test. The public-housing branch of eligibility is also unpinned.
  • S3. One description is off-template. P/eligibility/smi_rate.yaml puts the program name mid-sentence.

There are also six suggestions:

  • G1: own-payers and subsidized tenants with an UNSPECIFIED fuel get $0, although their amount does not depend on the fuel.
  • G2: Part A on the pension side (the Social Security side is right).
  • G3: record the plan-vs-manual income choices.
  • G4: the HOUSE default for heat-in-rent renters.
  • G5: add a rounding tie case.
  • G6: add a Wayback capture for the reused FFY27 workbook URL.

Status.

  • CI: 27 pass and 3 skipping. All 36 Minnesota cases passed in states-shard-1.
  • Branch: 2 commits ahead of main and 41 behind. It merges cleanly.
  • Siblings: this PR adds MN to the LIHEAP coverage line, so it conflicts with 16 open LIHEAP siblings in programs.yaml only (branch note).

Critical (must fix)

None.

Should address

S1. Railroad retirement annuities are counted gross of the Medicare premium, while Social Security and pensions are counted net

  • Where:
    • V/income/mn_eap_countable_income.py:50-53 sets max_(social_security + pension - premium, 0).
    • railroad_benefits is summed gross in the unearned loop (P/income/sources/unearned.yaml:11, read at V/income/mn_eap_countable_income.py:35).
    • The comment at lines 46-49 and Axiom interpretation 9 say "other annuities are not netted".
  • Sources:
    • Manual, Income Inclusions and Exclusions (FFY26 p. 96; FFY27 p. 98): Social Security Benefits count as the "Net amount of the check (gross amount minus amount deducted for Medicare and Medicare Part D)". The List of Excluded Income adds "Medicare deductions from Social Security" (p. 100).
    • Manual, Unearned Income Documentation. Its heading reads "Pensions, Railroad Retirement, Annuities and other Retirement Plans such as PERA, 401s, 403Bs, IRAs ..." (p. 110). Its note on p. 111 reads: "Just as the amount taken out of a Social Security check for the Medicare benefit is not included in a household's gross income for the EAP purposes, neither is the amount paid out of a household's PERA."
    • RRB. "If you receive an annuity, the Medicare Part B premium will usually be deducted from your monthly annuity rate" (Medicare Coverage).
  • Why it is a gap. The PR already reads the PERA note as a general rule and nets the premium from every pension_income. Railroad retirement sits in the same documentation section, and its premium is taken out of the check exactly as Social Security's is. As written, a railroad retiree's premium is subtracted from nothing, because they have no Social Security or pension.
  • Effect (hand-traced, 2026, a single retiree aged 70 with medicare_part_b_premium $2,434.80, natural gas, gas $1,000 and electricity $1,000):
    • (a) railroad_benefits $38,000.
      • Head: 38,000 > 37,428, so $0.
      • Netted: 35,565.20 ≤ 37,428, so eligible. The percentile is 0.9502, past the slope end, so the share is 0.108 and 0.108 × 2,000 = 216, giving $216.
    • (b) railroad_benefits $20,000.
      • Head: 0.5344 gives a share of 0.2415972, so $483.
      • Netted: 17,565.20 gives 0.4693 and a share of 0.2656290, so $531.
  • Severity: should. This is the wave's Medicare precedent (Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303, Add Arizona LIHEAP Standard benefit #10293, Add Iowa LIHEAP regular heating assistance #10292, Add Missouri LIHEAP Energy Assistance #10438 S3): a benefit counted gross where the state nets the premium, with a wrong result in a claimed year. The input exists, and it is enrollee-only and net of MSP.
  • Fix:
    • Move railroad_benefits out of the unearned list and into the netted term: max_(social_security + pension + railroad - premium, 0).
    • Add a railroad-only case like (a).
    • Update the comment, the body's "Social Security and pensions" row and Axiom interpretation 9 and its countable-income output.

S2. Twenty income and cost entries, and one eligibility branch, have no test that would fail if they were dropped

  • Where:
    • P/income/sources/earned.yaml (7 entries), P/income/sources/unearned.yaml (12) and P/income/qualified_eligibility_sources.yaml (3);
    • P/payment/energy_cost_sources.yaml (7);
    • V/eligibility/mn_eap_eligible.py:36-38.
  • Untested. No case gives these a positive value:
    • Earned (6 of 7):
      • sstb_self_employment_income, partnership_s_corp_income, farm_operations_income, farm_rent_income and rental_income;
      • self_employment_income, which is set only to −$4,000 in countable Case 1. That pins the floor, but dropping the source changes nothing.
    • Unearned (9 of 12): general_assistance, military_retirement_income, railroad_benefits, workers_compensation, disability_benefits, alimony_income, strike_benefits, estate_income and survivor_benefits.
    • Qualified Eligibility (2 of 3): tanf and general_assistance. Countable Case 8 is MFIP-only but asserts only countable income, not the share. Example: a household with only $4,200 of General Assistance gets the 0.439 share at the head, but 0.3975 if general_assistance were dropped from the list.
    • Energy cost (3 of 7): fuel_oil_expense, coal_expense and other_heating_fuel_expense.
    • The public-housing branch of eligibility. Eligibility Case 3 uses receives_housing_assistance only. Energy-cost Case 4 pins is_in_public_housing only inside mn_eap_energy_cost, which computes subsidized separately. If is_in_public_housing were dropped from mn_eap_eligible, a public-housing tenant with heat in the rent and no electric bill would turn eligible (at $0), and no case would fail.
  • Tested:
    • Income: wages, reported unemployment (through total_unemployment_compensation), SSI, VA benefits, Paid Leave, retirement withdrawals, Social Security, pensions, the Part B premium, interest, ordinary dividends, cash gifts and tanf.
    • Energy cost: gas, electricity, wood and bottled gas.
    • Backup matrix: all four selected rows (oil for fuel oil and kerosene, propane, natural gas and electricity) and the HOUSE default.
    • Branches: size 0, the inclusive limit, UNSPECIFIED, NONE, zero cost, the floor, the ceiling, the slope end, SSI-only Qualified Eligibility, the student exclusion at 19 and 20, and the age-59½ cut-off.
  • Severity: should, as in Add Missouri LIHEAP Energy Assistance #10438 S5, Add Washington LIHEAP regular heating assistance #10345 S4, Add New Mexico LIHEAP regular heating assistance #10431 S3 and Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303 S4 (the mutation rule).
  • Fix:
    • Add one countable-income case that gives every untested income source a distinct positive amount for an adult. S1's railroad case covers railroad_benefits.
    • Add GA-only and MFIP-only pay-share cases that expect the Pay Max share.
    • Add one energy-cost case with fuel oil, coal and other-fuel bills.
    • Add a public-housing heat-in-rent eligibility case with no electric bill that expects false.

S3. The smi_rate description puts the program name mid-sentence

  • Where: P/eligibility/smi_rate.yaml:1: "Minnesota limits Energy Assistance Program income to this share of state median income."
  • The rule. The house template ends the sentence with the program name ("... under the [program]."), with nothing after it (checklist item 6; NJ S6, Add Washington LIHEAP regular heating assistance #10345 S5). The other 23 descriptions end "under the Energy Assistance Program."
  • Severity: should (off-template).
  • Fix: "Minnesota limits income to this share of state median income under the Energy Assistance Program."

Suggestions

G1. Own-payers and subsidized tenants with an UNSPECIFIED fuel get $0, although their amount does not depend on the fuel.

  • The model. V/eligibility/mn_eap_eligible.py:25-27 requires a heating type that is not UNSPECIFIED or NONE in every branch. Eligibility Case 2 pins an own-payer with $900 of gas and $800 of electricity as ineligible.
  • Why Minnesota differs from the siblings.
    • The own-payer amount is "based on all household energy costs, including non-heat electric costs" (manual p. 117): mn_eap_energy_cost sums every bill whatever the fuel.
    • The subsidized heat-in-rent amount is the electric bill (p. 126).
    • Only the unsubsidized heat-in-rent path needs a fuel, to choose the backup-matrix row.
  • Examples (2026, wages $20,000):
    • The Case 2 own-payer. It gets $0 at the head, against 0.2415972 × 1,700 = $411 without the gate.
    • A subsidized tenant with heat in the rent and a $650 electric bill (eligibility Case 3's income). It gets $0 with UNSPECIFIED, against $209 with natural gas.
  • The precedent. On Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303 and Add Arizona LIHEAP Standard benefit #10293 the UNSPECIFIED rule was settled as "heating_type is a required input", and main's documentation says so too. That is why this is a suggestion.
  • Fix: apply the fuel test only where the backup matrix is used, i.e. heating_reported | ~(heat_in_rent & ~subsidized), with NONE still excluded. Otherwise, say in the body that the gate is an input rule, not a Minnesota rule. Either way, note that OTHER with heat in rent (where a steam or district-heat tenant would land) pays $0 although the matrix has Steam and District Heating rows.

G2. Part A on the pension side; the Social Security side is right.

  • Social Security. Not subtracting medicare_part_a_premium from Social Security is correct. A beneficiary aged 65 or over is entitled to premium-free Part A (42 U.S.C. 426(a)), so no Part A premium is withheld from the check that page 96 nets (the Add Iowa LIHEAP regular heating assistance #10292 G8 reasoning).
  • Pensions. The PERA note on page 111 covers retirees who "may not receive Social Security" and "are able to receive the Medicare benefit if they pay for it out of pocket". For a retiree with fewer than 40 quarters, that includes premium Part A.
    • Example: PERA $30,000, Part B $2,434.80 and Part A 12 × $311 = $3,732, with $2,000 of energy cost. The head gives $334; netting Part A as well gives $408.
  • Fix: subtract medicare_part_a_premium together with Part B for members with pension income, with a case. Otherwise, add a line to the comment and the body explaining why only Part B is netted.

G3. Record the plan-vs-manual income choices.

  • The conflicts. Plan item 1.9 (pp. 6-7, rendered) disagrees with the manual on three boxes:
    • it checks "Funds received by household for the care of a foster child", while the manual excludes foster care payments (p. 92);
    • it leaves "Earned income of a child under the age of 18" unchecked, while the manual counts minors' earnings unless they are K-12 students aged 19 or under (p. 91);
    • it leaves "Insurance payments made directly to the insured" unchecked, while the manual counts workers' compensation and wage-replacement disability pay (pp. 91, 99).
  • The PR's choice. It follows the manual, which is the right call: the manual is more specific and is the operative document.
  • Where it is recorded. Axiom interpretation 15 records the first two boxes. The body and source_conflicts/candidates.yaml record none.
  • Other omissions. The body's not-modeled list leaves out two manual items:
    • the deduction of "job-related expenses from gross income for employed individual who pays business expenses comparable to self-employment" (p. 94; no input);
    • that pension_income carries life insurance payments, which the manual excludes (p. 94; Axiom interpretation 12).
  • Fix:

G4. The HOUSE default for heat-in-rent renters.

  • The default. V/payment/mn_eap_housing_type.py:19 defaults to HOUSE. The variable is read only for unsubsidized heat-in-rent households, who are mostly apartment renters. HOUSE is the highest-cost column in most rows.
  • Example. A natural-gas heat-in-rent renter with $20,000 of wages gets 0.2415972 × 2,472 = $597 with the default, against $352 with APARTMENT.
  • Fix: consider APARTMENT or OTHER as the default. Otherwise, add the effect to the variable documentation and the body.

G5. Add a rounding tie case.

  • The claim. The test plan says "half-up rounding". The only rounding case, mn_eap Case 2 (666.58 → 667), does not tell half-up from half-even.
  • A tie case. A zero-income household with a $1,500 cost in 2026 gives 0.439 × 1,500 = 658.5. That is $659 under floor(x + 0.5) and Excel ROUND, but $658 under banker's rounding.

G6. Add a Wayback capture for the reused FFY27 workbook URL.

  • The URL. https://mn.gov/commerce-stat/xls/phb-calculator.xlsx held the FFY26 cells until Commerce replaced them with FFY27 (served with Last-Modified 2026-10-14). The FFY28 workbook will overwrite it, and the FFY27 manual says the percentages are set only when funds are awarded (p. 126).
  • What drifts. The FFY27 references in the 4 share/ files, minimum, maximum and the 7 backup_cost/ files would then point at other values.
  • Fix: add a dated Wayback capture beside each FFY27 workbook reference, as Add Missouri LIHEAP Energy Assistance #10438 G5 asked for Missouri's application.

Verified correct

Values against sources:

  • Income limit (manual FFY26 pp. 88-89, FFY27 p. 90; plan 2.1 "State Median Income 50.00%", p. 8):
    • SMI. FFY26 uses $143,998 for four (gov.hhs.smi.amount at 2025-10-01) and FFY27 uses $147,939, with the 45 CFR 96.85(b) factors.
    • All 40 cells reproduce, for example FFY26 3,119 / 5,999 / 10,079 / 10,509 / 11,013 and FFY27 3,205 / 6,164 / 10,355 / 10,835 / 11,355.
    • The 110% floor. It is 2025 guidelines for FFY26 and 2026 guidelines for FFY27 (fpg_year_lag 1, IM 2025-02 Att. 2). It binds from size 19, as both table headers say ("110% of Federal Poverty Guidelines for household sizes 19-20"). Above 20 it continues: size 21 gives 138,204 (FFY26) and 142,512 (FFY27).
    • The comparison. Comparing annual income with 12 × the one-month cell (not the annual column) follows page 88's note that a household "may be over-income ... based on 'One Month Max. Guidelines' even though the household's yearly income is less than the 'Annual Income'". The truncation leaves sizes 19 and up a few dollars a year below 110% of the guideline, exactly as the published cells do.
  • Pay share and benefit (FFY26 workbook Formula Parameters!C2:C7 = 200, 1400, 0.439, 0.108, 0, 0.896; FFY27 = 200, 1400, 0.452, 0.113, 0, 0.891):
    • The formulas match. Calculator F2 = C4 − (C4 − C5) × (E2 − C6)/(C7 − C6); G2 is ineligible above 1, otherwise clipped to [C5, C4]; H2 = ROUND(G2*C2, 0); I2 floors at C2 and caps at C3. mn_eap_pay_share and mn_eap match this.
    • Bounds. The $200 and $1,400 match manual p. 124 / FFY27 p. 126 and plan 2.6 (p. 9).
    • Qualified Eligibility. Reading "eligible at the lowest level of income" (p. 102 / FFY27 p. 103) as percentile 0 is a documented and reasonable reading.
  • Backup matrix. All 56 cells per year match Backup Matrix Values!C2:C57 in both workbooks. The row ranges are Oil A2:C9, Propane A10:C17, Steam A18:C25, Natural Gas A26:C33, Electricity A34:C41, District Heating A42:C49 and Metered Propane A50:C57, as cited. Kerosene on the Oil row is a documented inference.
  • Energy cost (pp. 117-126):
    • Own-payers. The sum of all seven bills, electricity included.
    • Heat in unsubsidized rent. The matrix ("they can still receive a grant using the backup matrix", p. 126).
    • Heat in Section 8, public or tribal subsidized rent. The electric cost ("Households who have heat included in rent and pay electric costs have their benefit determined by their electric costs").
    • Heat and electricity both in subsidized rent. Ineligible.
    • Wood or coal with heat in rent. $0, with page 123 ("The backup matrix does not include biofuels").
    • Documented simplifications:
      • an own-payer with zero bills gets no matrix fallback, which follows main's rule that a known fuel with no bill is a zero cost;
      • a subsidized tenant who pays electricity but has no recorded use is not given the $200 minimum.
  • Eligibility:
    • no categorical eligibility (plan 1.4 "No", p. 5, rendered);
    • no asset test (2.3, p. 8);
    • the vulnerability rules (p. 72; plan 2.3 and p. 9);
    • size counts citizens and eligible non-citizens, while every member's income counts (pp. 80-81, 93; plan p. 9);
    • a size of 0 is ineligible.
  • Countable income (plan 1.8 "Gross Income"; manual pp. 88-99):
    • the student exclusion for earnings and UI at 19 and under (pp. 91, 99);
    • retirement withdrawals from 59½ (pp. 96, 111);
    • the $50 and $100 thresholds (pp. 90, 92);
    • ordinary dividends only (p. 90);
    • per-source loss floors (p. 108 bars offsetting across returns; flooring within a return is a documented, stricter simplification);
    • Paid Leave counted from FFY2026 (FFY27 pp. 96, 116; reasoned in the body);
    • excluded: child support, capital gains, MSA, foster care, EITC and refunds (pp. 89-97);
    • tanf includes MFIP cash only, because mn_mfip already subtracts the food portion, so no SNAP-funded amount is counted.
  • Years:
    • 2026 and 2027 are verified.
    • Earlier years. All parameters start 2025-10-01, so period 2025 and earlier take the FFY26 values: a documented, unclaimed backfill (verified_years: "2026-2027"; NJ S3 pattern).

Hand-checked expectations (22). All are 2026 unless noted, with a size-1 FFY26 limit of 37,428:

  • Eligibility Case 1: 37,428 is eligible; 37,429 is not.
  • Eligibility Case 3: with no electric bill, the cost is 0, so false. With $650: 14,400 − 2,434.80 = 11,965.20 ≤ 37,428, so true.
  • Eligibility Case 4: an own-payer with no bills is false. Heat in unsubsidized rent (apartment) is true.
  • Eligibility Case 5: undocumented, so size 0 and false.
  • Income-limit Case 1:
    • Size 4: 143,998 × 0.5 / 12 = 5,999.92, giving 5,999 and 71,988.
    • Size 18: 120,958.32 / 12 gives 10,079 and 120,948.
    • Size 19: 1.1 × 114,650 = 126,115, / 12 gives 10,509 and 126,108.
    • Size 21: 138,204.
    • Size 0: 0.
  • Income-limit Case 2 (2027):
    • Size 1: 147,939 × 0.52 × 0.5 = 38,464.14, giving 3,205 and 38,460.
    • Others: 73,968, 124,260, 130,020 (1.1 × 118,200) and 142,512.
  • Countable Case 1: 23,733 + max(−4,000, 0) + 18,000 = 41,733, with size 1.
  • Countable Case 2: 31,200 + 6,000 + 1,200 = 38,400 (the member aged 19 is excluded; the member aged 20 counts).
  • Countable Case 3: 21,600 + 6,000 − 2,434.80 + 18,000 = 43,165.20.
  • Countable Case 4: 6,000 (only the withdrawal at 59.5 counts).
  • Countable Cases 5-6: 600 / 12 = 50 gives 20,600; 599 gives 20,000. 1,200 / 12 = 100 gives 21,200; 1,199 gives 20,000.
  • Countable Cases 7-9: 26,328; 7,800; 39,000, above 37,428, so ineligible.
  • mn_eap Case 1: 12,000 / 37,428 = 0.3206156, so the share is 0.3205583 (workbook F2 0.320558306). × 2,000 = 641.12, giving 641.
  • mn_eap Case 2 (2027): 12,000 / 38,460 gives 0.3332882 (workbook F2 0.333288181). × 2,000 = 666.58, giving 667.
  • mn_eap Case 3: 0.9351 > 0.896, so 0.108 × 1,100 = 118.80, giving 119 and then 200.
  • mn_eap Case 4: 14,000 / 60,468 gives 0.3534692. × 4,200 = 1,484.57, giving 1,485 and then 1,400.
  • mn_eap Case 5: wood with heat in the rent is eligible, with cost 0 and $0.
  • mn_eap Case 7: 51,480, 71,988 and 2,050. 0.7151192 gives 0.1748209, and × 2,050 = 358.38, so 358.
  • mn_eap Case 8 (2027): 27,000, 50,292 and 3,342 (Propane, Mobile Home). 0.5368647 gives 0.2477383, and × 3,342 = 827.94, so 828.
  • Energy cost Cases 1-5: 2,300; 1,458 / 3,293; 2,566 / 2,566 / 1,547; 720 / 650; 2027 1,554 / 2,634.
  • Pay share Cases 1-6: 0.439; 0.452; 0.108 (0.9351); 0.113 (36,000 / 38,460 = 0.9360); 0.439 (SSI only); and 12,168 / 37,428 = 0.3251042, giving 0.3189001.
  • Household size Case 1: 2.
  • Case count: 5 + 2 + 9 + 8 + 1 + 5 + 6 = 36, as the body says.

Batch checks:

Wiring and housekeeping:

  • Registry. The MN entry sits between MD and MS with verified_years: "2026-2027", and MN is added to the coverage line. test_programs_registry passed.
  • Aggregates. There is no aggregate wiring, which matches every merged LIHEAP state.
  • Gating. defined_for = StateCode.MN is on 7 variables, and mn_eap is gated by mn_eap_eligible. test_members_are_state_gated passed.
  • Changelog. changelog.d/mn-liheap-wave1d.added.md is top level and typed added.
  • Shared input. The pfml_benefits comment removal is accurate: MA CCFA and MN EAP are now its only readers.
  • Descriptions (item 6): 23 of 24 pass (S3).
  • References (item 7):
    • Every manual anchor shows its value:
      • FFY26 pp. 88-89, 90, 91, 92, 96, 99, 102, 111, 117-119, 124-126;
      • FFY27 pp. 90, 92-94, 98, 100, 103, 113, 119-121, 126-128.
    • Plan pp. 8-9 also show their values.
    • Multi-page titles use the quoted #page 88-89 / #page 96,111 form, and variables use # PDF pages comments.
    • Workbook cell ranges in the titles are correct (G6 covers the reused URL).
  • Tests. The files are variable-named, every period is plain, and every case sends heating_type.

Checklist results (LIHEAP batch):

  1. State plan and manual: every limit cell, constant and matrix cell matches.
  2. Income definition: gross income, per-source floors and Social Security and pensions net of Part B are right. Railroad netting is S1; Part A is G2.
  3. Backdating: pass for 2026-2027. Earlier years are a documented, unclaimed backfill.
  4. Household and arrangement rules: heat in unsubsidized and subsidized rent are both modeled and tested. The UNSPECIFIED gate is G1; the HOUSE default is G4.
  5. Tests: 36 cases, 22 hand-checked, all periods plain. Source coverage is S2.
  6. Descriptions: 1 off-template (S3).
  7. References: pass (Wayback capture is G6).
  8. Wiring: pass.
  9. Axiom: valid (Add NYC taxes and refundable credits to relevant household variables #1894).
  10. Branch and CI: a clean merge with main; 27 pass and 3 skipping.
  11. Integrate modeled state unemployment insurance into income flows #9558: pass.
  12. IIJA / FY2027: not applicable / FY2027 from the manual and workbook.
  13. Mutation check: 20 entries and one branch are unpinned (S2).
  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 / Count military SBP annuities in state benefit income lists #10317: pass.
  17. Merged state SSPs: pass (MSA is excluded by the manual).

Axiom line assessment

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

  • State: OPEN, labelled pe-parity, created 2026-10-23 22:16 UTC.

  • It matches the head. It names head 8ef6ef80e5 and merge base 6a92d6a983. It lists the 24 parameters, the 8 variables and the 36 cases by file. Its required outputs (size, limit, countable income, energy cost, eligibility, pay share and benefit) describe the head's code.

  • It is dispatch-ready in the rulespec-us#1467 format:

    • proposed us-mn/ module paths, with corpus status per source;
    • verbatim text of both manuals, the plan (boxes read from renders) and both workbooks;
    • 13 companion tests derived from the sources, not from PE output;
    • 17 interpretations;
    • its blockers: sources missing from the corpus, an unchecked signed release, billed-run approval and no reviewed oracle revision.
  • Note. The fixes change these interpretations and outputs:

    • S1 changes interpretation 9 and the countable-income output;
    • S2 adds cases;
    • G1 changes interpretation 8 and the eligibility output;
    • G2 changes interpretation 9;
    • G3 changes interpretation 15;
    • G4 changes interpretation 7.

    Update the issue with the head that resolves them, and add the RRB page to its sources.

CI status

27 pass, 0 failed, 3 skipping, none pending. I re-checked gh pr checks 10433 at the end of this review.

  • The runs. Run 40032582202 tested head 8ef6ef80e5 (pull_request, created 2026-10-23 22:16 UTC, after Tie source-conflict markers to the register shared with Axiom #10287, the 2026-10-21 cutoff and Count military SBP annuities in state benefit income lists #10317). The three skipped checks belong to an earlier trigger on the same head (run 40032356504).
  • Minnesota tests. States-shard-1, batch 7/14 (states/mn), ran all 7 files: 5 + 2 + 9 + 8 + 1 + 5 + 6 = 36 passed.
  • Guards (Rest jobs):
    • the four test_unemployment_compensation_readers.py tests;
    • the four test_military_retirement_readers.py tests;
    • the test_source_conflicts.py tests, including test_conflict_sounding_comments_do_not_grow;
    • test_members_are_state_gated.
  • 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 S1, S2 and G1-G5 examples are hand traces of the code at the head. The only local scripts were the merged-tree comment scan, the workbook cell parse, the limit arithmetic and the plan page renders.

Branch status

0 critical, 3 should and 6 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.

All 40 limit cells (50% SMI, with the 110% FPG floor from size 19), both workbooks' pay-share constants and formulas, all 112 backup-matrix cells and the heat-in-rent and subsidized paths check out, 22 expectations hand-check, and MSA is correctly excluded. Three should-address items: (1) railroad_benefits are counted gross of Part B while SS and pensions are netted, though RRB withholds Part B (a $38k railroad retiree gets $0 instead of $216); (2) many entries are unpinned (6/7 earned, 9/12 unearned, tanf/GA, 3/7 energy-cost sources, the public-housing branch); (3) the smi_rate description is off-template. Six suggestions. Full report in the comment above.

@DTrim99 DTrim99 mentioned this pull request Oct 27, 2026
2 of 3 tasks

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 Minnesota EAP regular heating assistance

2 participants