Skip to content

Add Minnesota EAP Primary Heat benefit - #10433

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

hua7450 wants to merge 5 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 PolicyEngine input rule (heating_type is required, #9754), not a Minnesota rule heating_type UNSPECIFIED or NONE pays $0, including for own-payers and subsidized tenants, whose cost does not depend on the fuel
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), 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, railroad retirement and pensions Manual FFY26 p.96, 100, 110-111; RRB Medicare Coverage Net of medicare_part_a_premium + medicare_part_b_premium, subtracted once from each person's combined amount and floored at zero (PERA example). Part A is $0 with 40 or more quarters of coverage (the default), so it lowers income only for an enrollee who buys Part A, such as a PERA retiree without Social Security
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. A steam or district-heat tenant enters OTHER and gets $0, although the matrix has Steam and District Heating rows
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; job-related expenses of employees who pay business expenses comparable to self-employment; DWP, MFIP housing grants, Housing Support, tribal payments, irregular income, jury pay, inheritances, lump sums Manual FFY26 p.89-108 No existing inputs
Life insurance and endowment payments Manual FFY26 p.94 The manual excludes them, but pension_income carries them and counts them with pensions
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

Plan item 1.9 (pages 6-7) and the manual differ on three income boxes. The plan checks funds received for the care of a foster child, which the manual excludes (p.92). It leaves earned income of a child under 18 unchecked, while the manual counts minors' earnings unless they are K-12 students aged 19 or under (p.91). It also leaves insurance payments made directly to the insured unchecked, while the manual counts workers' compensation and wage-replacement disability pay (p.91, 99). The model follows the manual, and source_conflicts/candidates.yaml records the difference as us-mn:liheap-plan-income-checklist.

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

41 YAML cases in 7 files: mn_eap.yaml 9, mn_eap_household_size.yaml 1, eligibility/mn_eap_eligible.yaml 6, eligibility/mn_eap_income_limit.yaml 2, income/mn_eap_countable_income.yaml 11, payment/mn_eap_energy_cost.yaml 6 and payment/mn_eap_pay_share.yaml 6. The five cases added in this round (countable income Cases 10 and 11, eligibility Case 6, energy cost Case 6 and mn_eap Case 9) were hand-derived and not run locally; CI validates them. The cases cover:

  • 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, railroad retirement net of Part B, a PERA pension net of Part A and Part B, every earned and unearned list source with a distinct amount, 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, the sum of fuel oil, coal and other fuel bills, backup-matrix cells for both years, kerosene on the oil row, subsidized heat in rent, public housing with heat in rent and no electric bill (ineligible), wood with heat in rent;

  • pay share at zero income, above the slope end, Appendix 4A Calculator row 2 for both years, Qualified Eligibility for SSI-only, General Assistance-only and MFIP-only households;

  • the $200 floor, the $1,400 ceiling, half-up rounding (including a $658.50 tie that rounds to $659), 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.

hua7450 and others added 2 commits October 28, 2026 20:10
Apply DTrim99's round 1 review: tests that pin every income source, the
rule and documentation fixes, and source-conflict candidates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…into mn-liheap-wave1d

# Conflicts:
#	policyengine_us/programs.yaml
#	source_conflicts/candidates.yaml
@hua7450
hua7450 requested a review from DTrim99 October 29, 2026 13:37
@hua7450

hua7450 commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, David. Round 1 is in 4b335140ad; 137d454a7a merges current main. The merge resolves the LIHEAP coverage line, which keeps MN, and changes no Minnesota EAP file.

  • S1. Fixed. income/mn_eap_countable_income.py now counts railroad retirement net of Medicare together with Social Security and pensions: max_(social_security + pension + railroad - premiums, 0), with each source floored at zero first. railroad_benefits leaves income/sources/unearned.yaml, which now has 11 entries, and the comment there names railroad retirement. The variable's comment now covers railroad retirement and Part A. Its references add the RRB Medicare Coverage page and manual page 110 (FFY27 page 112). Interpretation 9 of the Axiom issue no longer says "other annuities are not netted". New income/mn_eap_countable_income.yaml Case 10 is your example (a). The $38,000 annuity less $2,434.80 of Part B counts as $35,565.20, so the retiree is eligible and gets $216 (was $0). Your example (b) moves from $483 to $531.
  • S2. Fixed with four new cases. These and Case 10 take the suite from 36 to 41 cases.
    • Countable income Case 11. Each earned and unearned list entry gets a distinct amount, $1,000 to $13,000, for $91,000 in total ($21,000 earned plus $70,000 unearned). Dropping any entry lowers the total by its own amount. The case sends leaf inputs such as military_retirement_pay and partnership_income, so it does not override computed aggregates.
    • mn_eap.yaml Case 9. A household with only $4,200 of General Assistance and a parent and child with only $7,800 of MFIP both get the 0.439 share, which pays $659 and $746. With general_assistance dropped from the Qualified Eligibility list, the first household would get 0.3975 ($596); with tanf dropped, the second would get 0.3801 ($646).
    • Energy cost Case 6. An oil-heated home sums $1,600 of fuel oil, $400 of coal and $200 of other fuel to $2,200.
    • Eligibility Case 6. A public-housing tenant with heat in the rent, natural gas and no electric bill has an energy cost of $0 and is ineligible. Without the is_in_public_housing term in mn_eap_eligible, the case would turn eligible.
  • S3. Fixed. eligibility/smi_rate.yaml now reads "Minnesota limits income to this share of state median income under the Energy Assistance Program."
  • G1. Declined as a model change. heating_type is a required input for the LIHEAP programs, and UNSPECIFIED pays $0 because callers must send it; Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754 tracks the default. Eligibility Case 2 therefore stays at $0 (your $411), and so does the subsidized $650 tenant (your $209). As you suggested, the PR body now calls the gate a PolicyEngine input rule, not a Minnesota rule, for own-payers and subsidized tenants. Its Not modeled table also says that a steam or district-heat tenant enters OTHER and gets $0, although the matrix has Steam and District Heating rows.
  • G2. Fixed in the same edit as S1. The netted term subtracts medicare_part_a_premium and medicare_part_b_premium once from the person's combined Social Security, railroad retirement and pension. Part A is $0 with the default 40 quarters of coverage, so existing cases do not change; countable Case 3 stays at $43,165.20. Countable Case 10's second unit is your PERA example: $30,000 less $2,434.80 of Part B and $3,732 of Part A counts as $23,833.20 and pays $408 (was $334).
  • G3. Recorded. source_conflicts/candidates.yaml adds us-mn:liheap-plan-income-checklist. The entry covers the three plan 1.9 boxes: foster-care funds, a child's earnings and insurance payments made directly to the insured. It records the 2026-10-06 ruling to follow the manual (follows: gap-fill, state: ruled, code: [] until merge). The PR body's Not modeled table now lists employees' job-related expenses (manual p.94; no input) and the life insurance and endowment payments that pension_income carries and the manual excludes. Interpretation 15 of the Axiom issue adds the insurance-payments box and the candidate id.
  • G4. Declined. Every LIHEAP state defaults the dwelling type to house (a standing decision since 2026-10-01). The variable documentation already states the default. A caller with an apartment sets mn_eap_housing_type and gets the Apartment column (your $352 instead of $597).
  • G5. Fixed by mn_eap.yaml Case 9: 0.439 x $1,500 = $658.50, which pays $659 under half-up rounding (half to even would give $658).
  • G6. Declined. The Wayback availability API and CDX return 2025-10-26 as the latest capture of phb-calculator.xlsx. That capture is the FFY26 edition, because the FFY27 workbook is dated 2026-10-14, so no capture of the cited edition exists to link. The Axiom issue records the date and hash of the workbook as served on 2026-10-21, so ingest can pin that file.

The PR body and TheAxiomFoundation/rulespec-us#1894 now describe this head. The Axiom issue adds the RRB page to its sources, rewrites interpretation 9, adds companion test 14 (railroad $216, PERA with Part A $408) and gives the new case counts.

I did not run the tests locally; CI passed every check at 137d454a7a.

@DTrim99

DTrim99 commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 2): Add Minnesota EAP Primary Heat benefit

PR #10433 (author: hua7450), head 137d454a7a. Round 1 reviewed 8ef6ef80e5 on 2026-10-26 and requested changes: 0 critical, 3 should and 6 suggestions. Since then the author has pushed:

  • one fix commit, 4b335140ad (356 lines: 1 variable, 2 parameters, 4 test files and source_conflicts/candidates.yaml);
  • a merge of main at c55b806541, which hand-resolves two blocks and changes no Minnesota EAP file.

The PR still 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/

Summary

All three should-address items are fixed, and a test pins each fix.

  • S1. Railroad retirement is now counted net of Medicare, together with Social Security and pensions. Countable Case 10(a) fails if railroad is counted gross again ($0 instead of $216).
  • S2. Five new cases give every earned, unearned, Qualified Eligibility and energy-cost source a positive value, and they pin the public-housing term in eligibility.
  • S3. The smi_rate description now follows the template. All 24 descriptions end "under the Energy Assistance Program."

Suggestions.

  • Fixed: G2 (Part A netted, with a case), G3 (candidate us-mn:liheap-plan-income-checklist, body and Axiom interpretation 15) and G5 (a $658.50 tie that rounds to $659).
  • Declined, and the reasons hold: G1 (heating_type is a required input, as settled on Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303 and Add Arizona LIHEAP Standard benefit #10293; the body now says so) and G4 (the house default follows the wave convention and is documented).
  • Still open: G6. The author is right that no Wayback capture of the FFY27 workbook exists. Save Page Now would create one.

New findings. No critical or should. One small housekeeping suggestion (N-G1). The fix adds no unpinned branch and removes no test. Its comments trip neither the #10287 ratchet nor any other guard on the tree merged with current main.

Status.

  • Merge resolutions: both hand-resolved blocks are correct.
  • Axiom: rulespec-us#1894 is open, labelled pe-parity, and describes this head.
  • CI: 33 of 33 checks pass at this head. All 41 Minnesota cases passed.
  • Branch (note): Alaska Add Alaska LIHEAP regular heating assistance #10341 merged to main after the author's merge. The head's LIHEAP coverage line now conflicts with main's, and GitHub shows the PR as CONFLICTING. The fix is a one-line resolution that keeps AK and adds MN.

0 critical, 0 should and 2 suggestions are open.

Prior findings

Finding Status Evidence
S1. Railroad annuities counted gross of Medicare Fixed Code. V/income/mn_eap_countable_income.py:52-60 now computes max_(social_security + pension + railroad - (medicare_part_a_premium + medicare_part_b_premium), 0), with each source floored first. railroad_benefits has left P/income/sources/unearned.yaml, which now has 11 entries. The comment at lines 47-51 and the reference at line 16 (RRB Medicare Coverage) are updated. RRB's page says "You do not pay a monthly premium for your Hospital Insurance (Part A)" and that Part B "will usually be deducted from your monthly annuity rate".
Test. T/income/mn_eap_countable_income.yaml:299 (Case 10, unit 1) is round 1's example (a): 35,565.20, eligible, $216.
Mutation. Counted gross again, the unit has 38,000 > 37,428, so it is ineligible at $0 and the case fails.
S2. 20 untested income and cost entries and the public-housing branch Fixed Every entry round 1 listed is now pinned:
• Earned (6). Countable Case 11 (:347) gives each a distinct amount ($1,000 to $6,000) through leaf inputs. partnership_income reaches partnership_s_corp_income through its adds, and military_retirement_pay reaches military_retirement_income the same way. Dropping any entry lowers the 91,000 total by its own amount.
• Unearned (9). Seven are in Case 11 ($7,000 to $13,000). railroad_benefits is in Case 10. general_assistance is in mn_eap.yaml Case 9, which asserts countable income 4,200.
• Qualified Eligibility. mn_eap.yaml Case 9 (:256) asserts the share 0.439 for a General Assistance-only unit and an MFIP-only unit. Dropping general_assistance from the list gives 0.3975 (and $596). Dropping tanf gives 0.3801 (and $646).
• Energy cost. T/payment/mn_eap_energy_cost.yaml:180 (Case 6): 1,600 + 400 + 200 = 2,200.
• Public housing. T/eligibility/mn_eap_eligible.yaml:195 (Case 6). receives_housing_assistance is a plain input with default false. So if is_in_public_housing were dropped from V/eligibility/mn_eap_eligible.py:117-119, the unit would be vulnerable through heat in unsubsidized rent and turn eligible, and the case would fail.
S3. smi_rate description off-template Fixed P/eligibility/smi_rate.yaml:1 now reads "Minnesota limits income to this share of state median income under the Energy Assistance Program." All 24 descriptions now end with the program name.
G1. UNSPECIFIED fuel pays $0 for own-payers and subsidized tenants Declined (accepted) Reason. heating_type is a required input; #9754 tracks the default. That matches the settled rule on #10303 and #10293.
Documentation. As round 1 offered, the body's Eligibility table now calls the gate "PolicyEngine input rule ..., not a Minnesota rule". Its Not modeled table adds that a steam or district-heat tenant enters OTHER and gets $0. Axiom interpretation 8 says the same.
G2. Part A on the pension side Fixed Code. Part A is netted in the same term as S1.
Test. Case 10, unit 2 is round 1's PERA example: 30,000 − 2,434.80 − 3,732 = 23,833.20, which pays $408. Without Part A it would be $334, so the case fails.
Social Security. Subtracting Part A from the combined amount is harmless for consistent inputs. A beneficiary aged 65 or over has premium-free Part A (42 U.S.C. 426(a)), and medicare_part_a_premium is $0 at the default 40 quarters, so countable Case 3 is unchanged at 43,165.20.
G3. Record the plan-vs-manual income choices Fixed Candidate. source_conflicts/candidates.yaml adds us-mn:liheap-plan-income-checklist. It covers all three plan 1.9 boxes, with follows: gap-fill, state: ruled (follow the manual) and the PR in prs. gap-fill is a valid FOLLOWS value, and main's candidate checks pass on the merged tree.
Body. It lists the job-related expense deduction and the life insurance and endowment payments carried by pension_income.
Axiom. Interpretation 15 adds the insurance-payments box and the candidate id.
G4. HOUSE default for heat-in-rent renters Declined (accepted) The wave's dwelling-type inputs default to house or single-family (UT, IN, NE, NH, KS, AK; DC is the exception). V/payment/mn_eap_housing_type.py:21-25 says "If omitted, the model assumes a house".
G5. Add a rounding tie case Fixed mn_eap.yaml Case 9, unit 1: 0.439 × 1,500 = 658.50, giving $659. I checked that the float32 product is exactly 658.5. np.floor(x + 0.5) therefore gives 659 and np.round (half to even) gives 658, so the case does tell the two apart.
G6. Wayback capture for the reused FFY27 workbook URL Declined, still open (suggestion) The author's facts are right. Wayback CDX shows the latest capture of phb-calculator.xlsx is 2025-10-26, which is the FFY26 edition. The live file is served with Last-Modified 2026-10-14. The Axiom issue records the sha256 of the copy served on 2026-10-21.
Still open. A capture can be made now with Save Page Now (web.archive.org/save/<url>), and the FFY27 workbook references in 17 PR files (13 parameters and 4 variables) would still drift when FFY28 overwrites the URL.
Axiom note: update rulespec-us#1894 for the fixes Fixed The issue names head 137d454a7a. It rewrites interpretation 9, notes the G1 input rule in interpretation 8, extends interpretation 15, adds the RRB page as a supporting source and adds companion test 14 ($216 and $408). It gives 41 cases and 11 unearned entries.

New findings

Critical: none.

Should address: none.

  • No new unpinned terms. The two terms the fix adds, railroad in the netted sum and the Part A premium, are each pinned by Case 10. No test was removed, and the merge changed no Minnesota file.
  • Floor. The new max_(railroad, 0) floor matches the existing Social Security and pension floors. It guards a nonnegative input, so it is not a policy branch.

Suggestions:

  • N-G1. List the code in the new candidate. us-mn:liheap-plan-income-checklist has code: [], with the note "Code not yet on main".
    • The guard needs only paths that git tracks in the tree under test. ND Add North Dakota LIHEAP regular heating assistance #10342 listed its PR files (nd_liheap_countable_earned_income.py and a parameter) before it merged.
    • Fix: listing V/income/mn_eap_countable_income.py and the two P/income/sources/ lists now would tie the ruling to its deciding code without waiting for a follow-up edit.

Verified

Hand-checks of every new expectation (2026, FFY26 size-1 limit 37,428):

  • Countable Case 10:
    • Unit 1. 38,000 − (0 + 2,434.80) = 35,565.20, which is ≤ 37,428, so eligible. 35,565.20 / 37,428 = 0.9502 > 0.896 gives the share 0.108, and × (1,000 + 1,000) = 216.00, so $216.
    • Unit 2. 23,833.20, so eligible. 23,833.20 / 37,428 = 0.6367746. 0.439 − 0.331 × 0.6367746 / 0.896 = 0.2037630, and × 2,000 = 407.53, so $408.
    • Mutations. Railroad counted gross gives $0, and no Part A gives 27,565.20, which pays $334.
  • Countable Case 11.
    • Earned: 1,000 + 2,000 + 3,000 + 4,000 + 5,000 + 6,000 = 21,000.
    • Unearned: 7,000 + … + 13,000 = 70,000.
    • Total 91,000. Both members are adults who are not students, so no exclusion applies, and they have no SSI.
  • Eligibility Case 6: $18,000 of wages, under the limit. Public housing with heat in the rent and no electric bill gives cost 0, so the unit is not vulnerable and false.
  • Energy cost Case 6: 2,200.
  • mn_eap Case 9:
    • Unit 1. Countable income is 4,200. The Qualified Eligibility income is also 4,200, so the percentile is 0 and the share 0.439. Cost 800 + 700 = 1,500, so 658.50 gives $659.
    • Unit 2. Countable income is 7,800, the share 0.439. Cost 900 + 800 = 1,700, so 746.30 gives $746.
    • Alternatives without Qualified Eligibility. 4,200 / 37,428 gives 0.3975448 and $596. The size-2 limit is 143,998 × 0.68 × 0.5 / 12 = 4,079.94, truncated to 4,079, so 12 × 4,079 = 48,948. Then 7,800 / 48,948 gives 0.3801319 and $646.
  • Case count: 9 + 1 + 6 + 2 + 11 + 6 + 6 = 41, matching the body, the Axiom issue and the CI dots.
  • Existing cases. No existing expectation changed, and the 36 round-1 cases still pass.

Merge resolutions (both blocks checked):

  • programs.yaml coverage line.
    • At the merge, main's line (c55b806541) had 31 entries: AR, AL, AZ, DC, GA, Riverside County, MA, FL, IL, IA, IN, KS, KY, MD, MS, NC, ND, NE, NH, NJ, NY, OH, OK, OR, SD, RI, TX, UT, VA, WA, WY.
    • The resolved line is exactly that list plus MN, between MD and MS. No sibling was dropped or duplicated.
    • The diff against main at the merge is only that line plus the MN state_implementations entry (between MD and MS).
  • source_conflicts/candidates.yaml.
    • The conflict hunk paired the PR's new MN entry with us-mn:msa-guardian-fee-cap. main deleted that entry in Mark nine source conflicts now in the shared register #10441 ("Mark nine source conflicts now in the shared register").
    • The resolution keeps the new entry and accepts the deletion, which is correct.
    • The diff against main is the one 29-line MN entry.

Guards on the tree merged with current main (1976c4b600):

Housekeeping:

  • PR body. It describes this head: the railroad and Part A row, the G1 input-rule wording, the G3 omissions and 41 cases.
  • Changelog. Unchanged and top level.
  • Test cases. Every new case sends heating_type, and every period is plain.

Axiom

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

  • State: OPEN, labelled pe-parity, updated 2026-10-29 13:37 UTC.
  • It matches this head. It names head 137d454a7a and the merge with main at c55b806541. It describes fix commit 4b335140ad. Its required countable-income output lists 11 unearned entries and the railroad and Part A netting. It gives the new test counts (41 cases).
  • Interpretations and tests:
    • interpretation 9 is rewritten (railroad, Part A, RRB);
    • interpretation 8 adds the input-rule note;
    • interpretation 15 adds the insurance box and the candidate id;
    • companion tests 7, 8, 11 and 12 add the new cases;
    • new companion test 14 derives $216 and $408 from the manual pages and the RRB page.
  • Dispatch-ready. It still meets the rulespec-us#1467 format.
  • Notes.
    • Its Tests line still says CI ran 36 cases at 8ef6ef80e5, while CI has now passed all 41 at 137d454a7a.
    • The head and merge-base lines will need a refresh after the coverage-line merge.

CI

33 pass, 0 fail, 0 pending. I re-checked gh pr checks 10433 at the end of this review.

  • The run. Run 40792574064 (pull_request, head 137d454a7a, created 2026-10-29 00:12 UTC) includes main at c55b806541. That is after Tie source-conflict markers to the register shared with Axiom #10287 and the Check Montana's pension exemption in the Montana AGI pooling test #10496 and core fixes.
  • Minnesota tests. States-shard-1, batch 7/14, ran all 7 files, 6 + 2 + 11 + 9 + 1 + 6 + 6 = 41 cases, and all passed.
  • Guards (Rest jobs and PR validation): test_source_conflicts.py, the unemployment and military reader guards, test_programs_registry and test_members_are_state_gated.
  • Also passing: Microsimulation, Household API Partners, and every baseline and contrib shard.
  • Locally: I ran no YAML tests or model probes. The only local scripts were:
    • main's ratchet and candidate checks on the merged tree;
    • a float32 check of the 658.50 tie.

Branch

0 critical, 0 should and 2 suggestions (G6 and N-G1) are open at this head. The coverage-line conflict must be resolved before merge, keeping AK and adding MN.

Verdict: APPROVE

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving round 2: 0 critical, 0 should. All three round-1 shoulds are fixed and pinned. Full report in the comment above. Branch note: AK #10341 merged, so programs.yaml conflicts again; keep AK and add MN in the coverage line before landing.

# Conflicts:
#	policyengine_us/programs.yaml

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