Skip to content

Add North Dakota LIHEAP regular heating assistance - #10342

Open
hua7450 wants to merge 10 commits into
mainfrom
nd-liheap-wave2b
Open

hua7450 wants to merge 10 commits into
mainfrom
nd-liheap-wave2b

Conversation

@hua7450

@hua7450 hua7450 commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Adds North Dakota's LIHEAP regular heating benefit for FY2026. The model counts citizens and qualified noncitizens for household size and counts all members' income less a 27% earned-income deduction, reported medical, support and child care costs, and each member's computed out-of-pocket Medicare Part A and Part B premiums. It places the household in one of five bands from the published FY2026 income table (20%, 30%, 40%, 50% and 60% of state median income), with every band capped at the federal maximum of 42 U.S.C. 8624(b)(2)(B), and pays 100%, 95%, 85%, 65% or 45% of the annual bill for the primary heating fuel. Unsubsidized renters whose heat is included in rent receive a monthly amount by fuel, up to a yearly cap of $700 to $2,200; subsidized tenants whose heat is included in rent receive the share of an electric bill in their own name. In FY2026, a 40-year-old living alone with $20,000 of wages and a $1,000 natural gas bill receives $950: $20,000 x (1 - 0.27) = $14,600 of countable income, which is above the one-person 20% limit of $13,518 and at or below the 30% limit of $20,274, so the share is 95%, and $1,000 x 0.95 = $950.

Fixes #10336

Regulatory authority

Eligibility

Requirement Source How modeled
North Dakota residence Manual 415-20-10 defined_for = StateCode.ND
Household size counts citizens and qualified noncitizens; nonqualified members' income and deductions still count Manual 415-20-10-50 nd_liheap_household_size adds is_citizen_or_legal_immigrant; a household with no counted member is ineligible
Income at or below 60% of state median income, using the published FY2026 limits for 1 to 28 members, capped at the federal maximum Plan section 2.1 (page 9); FY2026 income table; manual 415-30-10; 42 U.S.C. 8624(b)(2)(B) and 8622(11); HHS IM 2025-02 Attachment 4 (pages 2 and 5) nd_liheap_share looks up inclusive upper limits in income_limits by household size and band; above 28 members it applies the 20% to 60% band rates to the size-adjusted state median income from the shared smi helper. It then caps every band's limit at the federal maximum: the greater of 60% of HHS's size-adjusted state median income (liheap_smi_limit; the FY2026 four-person median is $129,911) and 150% of the poverty guideline with a one-year lag (new federal parameter gov.hhs.liheap.fpg_limit, 1.5). The table's 60% limits for 2 to 9, 11 and 12 members exceed HHS's amounts by $2 to $12, so the cap lowers them to the Attachment 4 figures (four people: $77,958 to $77,946). Size 1 ($40,530 against $40,531), size 10 (equal) and sizes 13 and up (below 150% of the guideline, $122,475 at 13) keep the table.
Federal protection below 110% of the poverty guideline 42 U.S.C. 8624(b)(2)(B); 45 CFR 96.85(a); manual 415-30-05 A household the table excludes receives the 45% minimum share when its income is strictly below 110% of the poverty guideline with a one-year lag (the 2025 guideline for FY2026), rounded to cents, reusing gov.hhs.liheap.fpg_floor. Within the published table this reaches households of 21 to 28 members, whose 60% limits fall below the floor; a 21-person household's floor is 1.1 x ($15,650 + 20 x $5,500) = $138,215 against a table limit of $137,970.
Heating responsibility: a bill for the primary heating fuel, or heat included in rent Manual 415-20-05-05 heating_expense > 0 for the household's heating_type, or heat_expense_included_in_rent
A subsidized household with heat included in rent is eligible only for an electric bill in its name; subsidized households that pay their own heating bill follow the ordinary rules Manual 415-20-10 and 415-20-10-30; plan section 2.3 (page 9) Subsidized means receives_housing_assistance or the household's is_in_public_housing. A subsidized household with heat in rent is eligible only when its pre_subsidy_electricity_expense is positive, and it is paid the share of that bill (see Benefit)
No asset test Plan section 2.3 (page 9) No resource condition
Heating fuel Manual 415-20-05-05 and 415-30-30 heating_type NONE is ineligible; UNSPECIFIED has no bill in heating_expense and no renter cap, so it pays nothing, including on a subsidized heat-in-rent household's own electric bill; callers must send heating_type (#9754)

Countable income

Item Source How modeled
Earned income Manual 415-25-05-20-05 and 415-25-05-27-15 Wages, military basic pay (military_basic_pay), self-employment (including SSTB), farm operations and partnership income, each floored at zero so that a business loss does not offset other income
Earned income deduction Manual 415-25-05-05 27% of countable earned income
Dependent students' earnings Manual 415-25-05-50 Excluded for a tax dependent younger than 20 enrolled in K-12 or secondary school; tax dependency and enrollment stand in for the manual's dependent-child and half-time attendance conditions. Children's unearned income still counts.
Unearned income Manual 415-25-05-20-20, 415-25-05-27-15 and 415-25-05-30 Social Security, SSI, total unemployment compensation, workers' compensation, pensions, military retirement pay and military survivor annuities (military_retirement_income), retirement distributions, general assistance, railroad benefits, veterans benefits, disability benefits, survivor benefits, strike benefits, child support and alimony received, rental and farm rental income, S corporation income and estate income, each floored at zero. TANF received (tanf) counts once per household. Investment interest and ordinary dividends are not counted.
Cash contributions Manual 415-25-05-20-20 financial_assistance counts when it reaches $360 a year
Deductions Manual 415-25-05-05 and 415-25-05-05-05; plan section 1.9 (page 6) Health insurance premiums: the reported total (health_insurance_premiums) when positive; otherwise reported premiums other than Part B, floored at zero, plus each member's computed out-of-pocket medicare_part_a_premium and medicare_part_b_premium. Those premiums are zero for a member who is not enrolled, has premium-free Part A or has the premium paid by a Medicare Savings Program; that last condition stands in for the manual's bar on Part A and B premiums of QMB, SLMB, QI-1 and Buy-In members. Also deducted: pretax premiums, other medical expenses, child support paid, alimony paid and child care expenses. The medical deduction has no age or disability condition. Countable income is floored at zero.
Annual amounts Manual 415-25-05-15 Annual inputs stand in for the manual's annualization of application-month income

Benefit

  • Share (manual 415-30-10 and the FY2026 income table): 100% at or below 20% of state median income, 95% up to 30%, 85% up to 40%, 65% up to 50% and 45% up to 60%, with inclusive upper limits, each capped at the federal maximum. Income above the capped 60% limit pays nothing unless the federal floor applies; the floor pays the 45% minimum from manual 415-30-05. A four-person household with $77,950 of pension income and a $3,000 fuel oil bill receives $0: the income is within the printed $77,958 limit but above the $77,946 federal maximum (150% of the 2025 guideline is $48,225) and not below the $35,365 floor. At $77,946 the household receives 45%, or $1,350.
  • Direct bills (manual 415-20-05-05 and 415-30-05): the share times the annual heating_expense for the primary heating_type, with no dollar cap. Electric heat uses the same share on the pre-subsidy electricity bill; the 75% electric adjustment in 415-20-05-05-05 was repealed June 1, 2025 and does not apply.
  • Subsidized heat included in rent (manual 415-20-10): the household receives the share of its own annual electric bill (pre_subsidy_electricity_expense), with no dollar cap and nothing for the heat in its rent. A 40-year-old with $15,000 of pension income and a $1,200 electric bill receives $1,140: $1,200 x 0.95.
  • Unsubsidized heat included in rent (manual 415-30-30 and 415-30-15-01): the monthly amount is the yearly cap times the share divided by 12, rounded to the nearest dollar with halves rounded up, and the annual amount is 12 times the monthly amount, at most the yearly cap. Yearly caps are $2,200 for fuel oil, $1,750 for propane, $1,500 for electricity and $700 for natural gas and other fuels; kerosene, wood, coal, solar and other heating_type values take the $700 cap because 415-30-15-01 codes every fuel other than fuel oil, propane, electricity and natural gas, such as kerosene, wood and coal, as "other". A single renter with $15,000 of pension income and oil heat receives $2,088: $2,200 x 0.95 / 12 = $174.17, rounded to $174, and 12 x $174 = $2,088. Propane at the 100% share gives $146 a month, or $1,752, capped at $1,750.

Years covered

Model year Program year Guideline One-person limit
2026 FY2026, October 1, 2025 to September 30, 2026 (plan section 1.1, page 4) 2025, for the federal floor; the income bands come from the FY2026 published table $40,530 (60% band)

The registry entry records verified_years: "2026". Other years reuse the FY2026 income table, shares and renter caps through parameter backfilling and carry-forward, with the federal floor and maximum following the lagged guideline and that year's state median income, and are unverified estimates. For model years 2015 to 2025 the backfilled values depart from the rules then in force: the 75% electric adjustment in 415-20-05-05-05 applied until June 1, 2025, and Manual Letter 3750 (October 1, 2023) describes an earlier benefit method, while the model pays electric heat at the full share and applies the FY2026 method in every year.

Not modeled and source conflicts

  • The FY2026 income table's 60% limits for 21 to 28 members fall below 110% of the poverty guideline, which 42 U.S.C. 8624(b)(2)(B) protects. The model follows the statute and pays the 45% minimum share (see Eligibility); source_conflicts/candidates.yaml lists this as us-nd:liheap-income-floor-vs-smi-limit.
  • The FY2026 income table's 60% limits for 2 to 9, 11 and 12 members exceed HHS's FY2026 60% amounts (IM 2025-02 Attachment 4) by $2 to $12, and at those sizes 60% of state median income is the ceiling of 42 U.S.C. 8624(b)(2)(B). The model follows the statute and caps every band at the federal maximum (see Eligibility); source_conflicts/candidates.yaml lists this as us-nd:liheap-table-above-federal-ceiling.
  • The FY2026 plan says no eligible household pays more than 6% of its total income toward heating costs (section 2.5, page 10) and lists a $7,500 maximum benefit (section 2.6, page 11). The manual pays a fixed share of actual bills (415-20-05-05, 415-30-05 and 415-30-10) and sets neither a 6%-of-income limit nor a $7,500 maximum. The model follows the manual and applies neither; source_conflicts/candidates.yaml lists this as us-nd:liheap-plan-income-share-design.
  • Manual 415-30-30-10 says to subtract the share-based amount from the monthly estimated cost of heating, which would pay the lowest-income renters nothing; the table in 415-30-30 pays the share-based amount. The model follows the table; source_conflicts/candidates.yaml lists this as us-nd:liheap-renter-table-vs-subtraction-prose. The nearest-dollar, halves-up rounding reproduces all 20 published monthly amounts, but the manual does not state it.
  • Manual 415-25-05-10 prints a six-person annual limit of $102,984 next to a monthly limit of $8,574.50. The FY2026 income table prints $102,894, which equals 12 x $8,574.50, and the model uses $102,894; source_conflicts/candidates.yaml lists this as us-nd:liheap-six-person-limit-workbook-vs-manual. Both figures exceed the $102,888 federal maximum, so the choice does not change results.
  • Plan item 1.9 (page 6) leaves "Earned income of a child under the age of 18" unchecked, which would exclude every minor's earnings. Manual 415-25-05-50 excludes only the earnings of dependent children 19 and under attending elementary or high school half time or more. The model follows the manual; source_conflicts/candidates.yaml lists this as us-nd:liheap-child-earnings-exclusion.
  • Households larger than 28 use 20% to 60% of state median income from the smi helper, capped at the federal maximum; North Dakota's rounding for those sizes is unverified.
  • The program page lists 2026-2027 heating season limits effective October 1, 2026 for 1 to 12 members. No complete FY2027 band table was found, so the model applies FY2026 values throughout 2026.
  • The SPM unit stands in for the household that buys energy in common. Foster persons, roomers, institutional and transient residences, and qualified-noncitizen statuses outside the immigration_status values are not represented.
  • Native American households on the Standing Rock, Turtle Mountain, Spirit Lake and Fort Berthold reservations are generally served by tribal LIHEAP programs, and mixed households may elect either program (415-20-10-15). The model assumes every household uses the state program.
  • Income inputs are annual. Net business income comes from the supplied net amounts without depreciation add-backs, and losses within one aggregated business input cannot be separated. Military basic pay counts as earned income; the subsistence and housing allowances that 415-25-05-20-05 also counts as military pay have no input. Employer-paid disability is not added separately because employment_income may already include it. Disability benefits are counted as unearned because the payer is not recorded, and rental income is counted as unearned. financial_assistance is treated as recurring and unconditional. Nonrecurring lump sums, educational aid, royalties, tribal payments, recoupments and military allotments have no separate inputs. Manual 415-25-05-20-20 counts recurring gambling winnings, but gambling_winnings does not separate recurring from nonrecurring winnings, so the model does not count it.
  • Wage garnishments, representative-payee fees, away-from-home student and worker costs, and employment or education expenses approved by the State Program Administrator have no inputs. The model deducts reported medical, support and child care amounts without checking payment plans, legal obligation or the work, training or education purpose of care. No Part D premium variable exists, so the out-of-pocket Part D premium that 415-25-05-05-05 allows counts only when reported among other premiums.
  • Manual 415-20-05-05 pays a primary and a secondary heat source, and incidental charges such as propane tank rental, wireless monitors, hook-up, leak-check and same-day delivery charges, at the share. The model pays only the bill for the primary heating_type: a secondary fuel's bill can sit in its own expense input but is not paid, and incidental charges have no input. The nonresidential-usage caps in 415-20-05-05-15 ($4,300 fuel oil, $3,500 propane, $3,000 electricity, $1,100 natural gas and other) need facts the inputs lack.
  • Annual amounts assume a full eligible year; application timing, partial-year service and vendor payment routing are not modeled.
  • An UNSPECIFIED heating_type has no bill and no renter cap, so it pays $0, including on a subsidized heat-in-rent household's own electric bill; callers must send heating_type, and Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754 tracks its default.
  • Manual 415-30-30 also prints a Renter Electric Utility Benefit Table ($125 to $56 a month, $1,500 yearly cap) for renters whose electric utility is included in rent. No input marks electricity included in rent, so the model does not pay it.
  • This PR models regular heating assistance only. Crisis assistance, cooling, weatherization and heating-equipment repair are out of scope.

Test plan

  • 4 YAML files with 73 cases under policyengine_us/tests/policy/baseline/gov/states/nd/dhs/liheap/ (nd_liheap.yaml 48, nd_liheap_countable_income.yaml 21, nd_liheap_household_size.yaml 2, nd_liheap_share.yaml 2); expected values are hand computed from the manual, the FY2026 income table, HHS's FY2026 state median income amounts and the CMS 2026 Medicare premiums. One share case checks all 140 published upper limits and the dollar after each; the ten printed 60% limits above the federal maximum (sizes 2 to 9, 11 and 12) expect 0. nd_liheap.yaml Case 43 pins the four-person federal maximum: $77,946 of pension income pays $1,350 and $77,950 pays $0. Case 35 deducts the computed Part B premium: $34,000 - $2,434.80 = $31,565.20, in the 65% band, so a $1,000 gas bill pays $650. Case 44 deducts nothing when a Medicare Savings Program pays the premium ($950), and Case 45 deducts a $3,732 Part A premium and the Part B premium from a $38,000 pension ($31,833.20, $650). Case 46 pays a subsidized heat-in-rent tenant 95% of a $1,200 electric bill, $1,140 (Case 48 pays $0 when that tenant's heating_type is UNSPECIFIED), and Case 47 pays an unsubsidized kerosene renter at the 95% share $660, under the $700 cap. Countable income Case 19 gives all 6 earned and 19 unearned sources distinct amounts ($16,060 + $16,850 = $32,910), Case 20 keeps a $3,000 rental loss from offsetting a $20,000 pension, and Case 21 pins the dependency and school conditions of the student earnings exclusion. nd_liheap.yaml Case 41 counts a $30,000 military survivor annuity less the $2,434.80 Part B premium of the 70-year-old recipient: $27,565.20, 65% of a $3,000 oil bill, $1,950. Case 42 puts a 29-person household on the state median income path: 2.01 x $129,911 = $261,121.11, so $130,000 of pension income falls between the 40% limit of $104,448.44 and the 50% limit of $130,560.56, and 65% of a $3,000 oil bill is $1,950. The review-fix cases were not run locally; CI runs the full suite.
  • YAML tests whose expected values come from an external source (statute, official form or worksheet, official calculator)
  • Source-conflict register entry and # Source conflict <jurisdiction>:<id> marker: not yet. The six us-nd:liheap-* readings named under source conflicts are in source_conflicts/candidates.yaml until they are registered in rulespec-us; this PR adds no marker.
  • make format
  • Changelog fragment in changelog.d/

axiom: TheAxiomFoundation/rulespec-us#1902 queued

🤖 Generated with Claude Code

hua7450 and others added 5 commits October 3, 2026 19:19
One added changelog fragment for the new program and a one- or two-sentence programs.yaml note; limitations stay in code comments and the PR body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hua7450 and others added 3 commits October 23, 2026 22:46
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	policyengine_us/programs.yaml
@hua7450
hua7450 marked this pull request as ready for review October 24, 2026 03:00
@codecov

codecov Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff            @@
##              main    #10342   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         7    +3     
  Lines           76       119   +43     
  Branches         2         2           
=========================================
+ Hits            76       119   +43     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

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

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

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

@DTrim99

DTrim99 commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Add North Dakota LIHEAP regular heating assistance

PR #10342 (author: hua7450), head 62c5fa449b. This is a first review. The PR adds North Dakota's LIHEAP regular heating benefit for FY2026 (model year 2026). It includes:

  • 11 parameter files and 7 variables;
  • 4 test files (64 cases), one of which checks all 140 published upper limits and the dollar after each;
  • a programs.yaml entry (coverage line and state block);
  • three source_conflicts/candidates.yaml entries;
  • a changelog fragment.

The PR has no reviews and no comments other than Codecov. It touches no partner test file.

Path prefixes:

  • P/ = policyengine_us/parameters/gov/states/nd/dhs/liheap/
  • V/ = policyengine_us/variables/gov/states/nd/dhs/liheap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/nd/dhs/liheap/

Sources read for this review:

Summary

What is right. The tables and mechanics are encoded carefully:

There is one critical finding:

  • C1. Federal ceiling. North Dakota's printed 60% column exceeds HHS's mandatory FY2026 60%-of-SMI figures by $2 to $12 at sizes 2-9, 11 and 12. At those sizes 60% of SMI is also the statute's ceiling. The head follows the table over 42 U.S.C. 8624(b)(2)(B), although it applies the statute at the floor. A four-person household at $77,950 gets $1,350 where the statute gives $0, and 10 share-test expectations pin the head.

There are three should-address items:

  • S1. Medicare premiums. They are deducted only when reported. Manual 415-25-05-05-05 allows Medicare premiums (except those MSP pays), and plan 1.9 checks "Excluding MediCare deduction". main computes both premiums net of MSP. Case 35 pins $450 where the rule gives $650.
  • S2. Subsidized tenants with an electric bill. Subsidized heat-in-rent households who pay their own electric bill are excluded. Manual 415-20-10 makes them "eligible for only the ... electric bill in their name": $0 instead of $1,140 in the example. The body omits this clause.
  • S3. Untested entries. 16 of the 24 income-source entries are never positive in a test. The unearned zero floor, the dependency and school conditions of the student exclusion, the K-12 arm and five renter-cap fuel keys are also unpinned (the mutation rule).

There are also six suggestions:

  • G1: record the remaining source choices in the backlog and relabel two candidates.
  • G2: UNSPECIFIED heat-in-rent households.
  • G3: military pay.
  • G4: secondary heat and incidental charges.
  • G5: years outside FY2026.
  • G6: archive captures of the manual pages.

Status.

  • CI: 34 pass and 3 skipping.
  • Branch: 8 commits ahead of main and 59 behind. The only conflict is programs.yaml (branch note).

Critical (must fix)

C1. The 60% column exceeds the federal ceiling of 60% of HHS's state median income at sizes 2-9, 11 and 12, and the head follows the table over the statute

  • Where:
    • P/income_limits.yaml, band 5 for sizes 2-9, 11 and 12.
    • V/nd_liheap_share.py:31-38 uses the published limit at every size up to 28. Nothing caps it at the federal maximum.
    • T/nd_liheap_share.yaml Case 1 expects 0.45 for the ten households at those 60% limits (for example the size-4 household at line 803, with $77,958).
  • Sources:
    • The statute. 42 U.S.C. 8624(b)(2)(B) allows payments only to "households with incomes which do not exceed the greater of— (i) an amount equal to 150 percent of the poverty level for such State; or (ii) an amount equal to 60 percent of the State median income".
    • The definition. 42 U.S.C. 8622(11) defines "State median income" as "the State median income promulgated by the Secretary ... adjusted, in accordance with regulations prescribed by the Secretary, to take into account the number of individuals in the household".
    • The regulation. 45 CFR 96.85(a) requires grantees to adjust their criteria by October 1 to the most recently published estimates.
    • HHS figures. HHS LIHEAP IM 2025-02 Attachment 4 is "Mandatory Use in LIHEAP for FY26". Page 2 (sizes 1-6) and page 5 (sizes 7-12) print North Dakota (four-person SMI $129,911, the value on main). main's liheap_smi_limit helper reproduces them.
    • North Dakota's own standard. It is the same 60% of SMI. Plan 2.1 (page 9) gives "State Median Income 60.00%". Manual 415-25-05-10 says "The income eligibility limits for heating assistance are based on 60% of the North Dakota median income" and "Amounts shown are based on the upper limit of 60% SMI".
  • The overshoot. The workbook's 60% column compared with Attachment 4:
Size Workbook (head) HHS 60% SMI Over 150% FPG (2025)
2 53,010 53,003 7 31,725
3 65,478 65,474 4 39,975
4 77,958 77,946 12 48,225
5 90,426 90,417 9 56,475
6 102,894 102,888 6 64,725
7 105,234 105,227 7 72,975
8 107,574 107,565 9 81,225
9 109,914 109,903 11 89,475
11 114,582 114,580 2 105,975
12 116,922 116,919 3 114,225
  • The ceiling binds here. At these sizes 60% of SMI is above 150% of the guideline, so 60% of SMI is the statute's ceiling. The overshoot holds even against the unrounded amount (four people: 0.6 × 129,911 = 77,946.60).
  • Sizes outside the finding. Size 1 ($40,530 against $40,531) is stricter, which the statute allows, and size 10 is equal. From size 13 up, 150% of the guideline (size 13: $122,475) is above the table, so the table is within the ceiling.
  • The PR knew of the gap. The axiom issue (interpretation 5) notes that "North Dakota's own table differs slightly from the HHS amounts", but the PR treats the gap as a rounding curiosity rather than a statute conflict.
  • Inconsistent with the PR's own approach. At the bottom of the table the PR applies the statute over the workbook (the 110% floor, candidate us-nd:liheap-income-floor-vs-smi-limit, follows: statute). At the top it lets the workbook override the same subsection without recording the choice. CLAUDE.md ("Conflicting sources") requires the statute over a manual or stated agency practice.
  • Effect (FY2026; four citizens; countable income $77,950 from a pension; fuel oil, bill $3,000):
    • Head: $77,950 ≤ $77,958 gives the 45% share: $1,350.
    • Statute: $77,950 is above 60% of SMI ($77,946) and above 150% of the guideline ($48,225), so the household is not eligible: $0.
    • The same window exists at every size in the table, up to $12 wide. It is tiny in dollars, but it gives a wrong result in the claimed year, as Wyoming Add Wyoming LIHEAP regular heating assistance #10346 C2 did ($1 above HHS's mandatory figures).
  • Severity: critical. The head gives wrong results in the claimed year against the governing statute, and 10 test expectations pin them.
  • Fix (recommended):
    • Cap the top band at the federal maximum: min_(published 60% limit, max_(liheap_smi_limit(size, state, gov.hhs.liheap.smi_limit, period, parameters), 1.5 × lagged guideline)). That needs a 150% parameter, since gov.hhs.liheap has only fpg_floor and smi_limit. The 150% term keeps the table at sizes 13 and up, where it is lawful.
    • Keep the 110% floor as is.
    • Add a follows: statute candidate (for example us-nd:liheap-table-above-federal-ceiling).
    • Re-pin the ten Case 1 households to 0 and add the $77,950 four-person case.
    • The six-person candidate then no longer changes results, because both printed figures exceed $102,888.
    • If the team prefers North Dakota's table, record the conflict and take it to the project lead, as CLAUDE.md requires for a departure from the statute.

Should address

S1. Medicare premiums are deducted only when reported, although manual 415-25-05-05-05 allows Medicare premiums and main computes them

  • Where:
    • V/nd_liheap_countable_income.py:35-46 deducts health_insurance_premiums (if positive) or health_insurance_premiums_without_medicare_part_b + medicare_part_b_premiums_reported.
    • The computed medicare_part_b_premium and medicare_part_a_premium are never read.
    • T/nd_liheap.yaml Case 35 (line 795) pins "Medicare enrollment alone does not create a premium deduction".
  • Sources:
    • Manual 415-25-05-05-05 (web) lists among allowable medical expenses "Medicare premiums, deductibles, and co-pays. Part A and B Medicare premiums for an individual who is QMB/SLMB, QI-1 or Buy-In eligible are not allowable medical expenses. The actual out-of-pocket Part D Medicare premium is an allowable medical expense."
    • Manual 415-25-05-20-20: "If unearned income is withheld for: ... the gross amount must be counted". Social Security counts gross, so the Part B premium withheld from it is relieved only through this deduction.
    • Plan item 1.9 (page 6) checks "Excluding MediCare deduction", by the PR's axiom issue's reading (interpretation 11).
    • On main. medicare_part_b_premium and medicare_part_a_premium are the enrollee's out-of-pocket premiums net of Medicare Savings Program coverage. That is the manual's rule, including its QMB/SLMB/QI-1 bar.
      • medicare_part_b_premiums_reported is an input that nothing else on main reads, so a household calculation for a Medicare enrollee gets no deduction unless the user finds it.
      • Every merged LIHEAP state that relieves Medicare premiums reads the computed premium (AZ and VA read A + B; AR, GA, IA, KY, NJ, OH, RI and SD read B).
  • Effect (T/nd_liheap.yaml Case 35 as written: FY2026; one person aged 68; Medicare enrolled; pension $34,000; natural gas, bill $1,000; MSP coverage 0, which the case asserts):
    • Head: countable $34,000 is above the 65% top of $33,786, so 45%: $450.
    • Rule: the Part B premium is $202.90 × 12 = $2,434.80, below the IRMAA threshold. $34,000 − $2,434.80 = $31,565.20 is within the 65% band (27,031-33,786): $650.
    • A Part A buyer also loses the Part A premium. The rule's QMB/SLMB/QI-1 bar is not applied to reported amounts (axiom interpretation 11).
  • Severity: should, as in Add Tennessee LIHEAP regular energy assistance #10430 S2, Add Montana LIHEAP regular heating assistance #10435 S1 and the wave's Medicare precedents (VA Add Virginia EAP Fuel Assistance (LIHEAP regular heating) #10303, AZ Add Arizona LIHEAP Standard benefit #10293, IA Add Iowa LIHEAP regular heating assistance #10292). The body documents the choice, but the head relieves less than the state's rule does.
  • Fix:
    • In the component branch, deduct medicare_part_a_premium + medicare_part_b_premium, keeping the aggregate's precedence. If medicare_part_b_premiums_reported is kept, use it in place of the computed Part B, not in addition.
    • Re-pin Case 35 to $650 and add a QMB case (no deduction) and a Part A case.
    • Update the body and axiom interpretation 11 and companion test 12(f).

S2. Subsidized heat-in-rent households who pay their own electric bill get $0, although manual 415-20-10 makes them eligible for that bill

  • Where:
    • V/nd_liheap_eligible.py:32 excludes every heat_in_rent & subsidized household, whatever other bill it pays.
    • The body's eligibility table says only "Subsidized housing with heat included in rent is ineligible".
    • Axiom interpretation 15 calls the electric benefit "outside regular heating scope", but neither the body nor the code says so.
  • Sources:
    • Manual 415-20-10 (web, revised June 1, 2025), the section the PR cites: "Tenants of subsidized housing in which heat is included in the rent are not eligible. If a subsidized household has heat or electricity included in their rent, but they have the opposite (heat or electricity) bill in their name, the household is eligible for only the heat or electric bill in their name." The same section describes the two eligible groups as those who pay a vendor and those "whose cost of heating or electric utility is included" in rent.
    • Manual 415-20-05-05: the allowable energy sources include electricity, and "Allowable heating-related costs are paid at the client's established LIHEAP Share percentage". The 75% electric adjustment (415-20-05-05-05) was repealed June 1, 2025.
    • Manual 415-30-30 also prints a "Renter Electric Utility Benefit Table" ($125 to $56 a month, $1,500 yearly cap) for renters. The electric benefit is part of the regular benefit chapter, not a separate component.
  • Effect (FY2026; one person aged 40; pension $15,000; receives_housing_assistance; heat included in rent, building fuel NATURAL_GAS; own electric bill pre_subsidy_electricity_expense $1,200):
    • Head: not eligible: $0.
    • Manual: eligible for the electric bill at the 95% share: 0.95 × 1,200 = $1,140.
    • The other direction (electricity in rent, heating bill in the tenant's name) is already right, because heat_expense_included_in_rent is false.
  • Severity: should, as in Add New Mexico LIHEAP regular heating assistance #10431 S1 (subsidized heat-in-rent tenants who pay the electric bill got $0) and the wave's heat-in-rent precedents (Add Tennessee LIHEAP regular energy assistance #10430 S1, Add Wyoming LIHEAP regular heating assistance #10346 S2). A household the manual expressly serves gets nothing, and the PR body misstates the rule it cites.
  • Fix:
    • For subsidized heat-in-rent households, pay the share of max_(pre_subsidy_electricity_expense, 0) instead of excluding them, and add a case.
    • If the team keeps this out of scope, state that in the body's eligibility table and the code, citing the 415-20-10 sentence. Say the same for the Renter Electric Utility Benefit Table.

S3. 16 of the 24 income-source entries and several branches have no test that would fail if they were dropped

  • Where: P/income/earned_sources.yaml, P/income/unearned_sources.yaml, V/nd_liheap_countable_unearned_income.py:21, V/nd_liheap_countable_earned_income.py:26-36 and P/renter_annual_cap.yaml.
  • Untested entries. No case gives these a positive value:
    • Earned (3 of 5):
      • farm_operations_income and partnership_income never appear.
      • sstb_self_employment_income appears only as −$5,000 (countable Case 4), which floors to 0, the same result as dropping it.
    • Unearned (13 of 19):
      • total_unemployment_compensation, workers_compensation, retirement_distributions, general_assistance and railroad_benefits;
      • veterans_benefits (only $0 in Case 36), disability_benefits, survivor_benefits and strike_benefits;
      • rental_income, farm_rent_income, s_corp_income and estate_income.
  • Tested:
    • Earned: employment_income and self_employment_income.
    • Unearned: social_security, ssi, pension_income, military_retirement_income (through both inputs), child_support_received and alimony_income.
    • Outside the lists: financial_assistance, tanf, every deduction input and the earned per-source floor.
  • Unpinned branches:
    • Unearned floor. No case has a negative unearned input, so removing max_(…, 0) at line 21 fails nothing. That floor is what stops a rental loss from offsetting a pension.
    • Student exclusion. Countable Cases 12 and 13 vary only age. Removing is_tax_unit_dependent or the school condition fails nothing, and is_in_k12_school is never true.
    • Renter caps. The KEROSENE, WOOD, COAL, SOLAR and OTHER caps ($700) and UNSPECIFIED (0) are never exercised. Only the four table fuels are.
  • Severity: should, as in Add Nevada Energy Assistance Program Fixed Annual Credit #10432 S1, Add Wisconsin WHEAP regular heating benefit #10434 S1, Add Tennessee LIHEAP regular energy assistance #10430 S3, Add Wyoming LIHEAP regular heating assistance #10346 S3 and Add Montana LIHEAP regular heating assistance #10435 S3 (the mutation rule).
  • Fix:
    • Add one countable-income case that gives every listed source a distinct positive amount. Enter unemployment_compensation and military_retirement_pay_survivors so the totals are exercised.
    • Add a negative rental_income beside a pension.
    • Add a non-dependent 17-year-old in school with wages (counted), a dependent 17-year-old not in school (counted) and a K-12 student (excluded).
    • Add a kerosene heat-in-rent case ($700 cap).
    • Mirror the new cases in the rulespec-us#1902 companion tests.

Suggestions

  • G1. Record the remaining source choices, and relabel two candidates.
    • Unrecorded choices. The plan's 6%-of-income design and $7,500 maximum (items 2.5-2.6, pages 10-11) against the manual's fixed share. Plan item 1.9's unchecked "Earned income of a child under the age of 18" against 415-25-05-50's narrower exclusion. Both are documented in the body or the axiom issue, but neither is in candidates.yaml. The code comment in V/nd_liheap.py ("the plan's conflicting 6%-income and $7,500 narrative") names one of them.
    • Labels. us-nd:liheap-renter-table-vs-subtraction-prose and us-nd:liheap-six-person-limit-workbook-vs-manual are labelled follows: conflicting-source. The backlog defines that label as "against the statute; the 2026-10-19 rule calls for a model change". Neither involves a statute (both readings say "No statute is supplied"), so gap-fill describes them.
    • Precedent: Add Wyoming LIHEAP regular heating assistance #10346 G1, Add New Mexico LIHEAP regular heating assistance #10431 G2 and Add Montana LIHEAP regular heating assistance #10435 G2.
  • G2. UNSPECIFIED heat-in-rent households get $0. main's HeatingType documentation makes UNSPECIFIED "the right value when heat is included in rent and the tenant does not know the building's fuel". In North Dakota that common case passes eligibility and is paid $0 (P/renter_annual_cap.yaml). Every fuel outside the four named columns is "Other" (415-30-15-01), and that $700 column is the smallest. Using it as a floor would be a documented lower bound (Vermont Add Vermont Seasonal Fuel Assistance #10436 G1 precedent).
  • G3. Military pay. 415-25-05-20-05 counts "Military Pay, including" BAS and BAH as earned income. military_basic_pay exists on main, and Iowa's earned list names it. Consider adding it, or citing why employment_income covers it (Nevada Add Nevada Energy Assistance Program Fixed Annual Credit #10432 G2 precedent).
  • G4. Secondary heat and incidental charges. 415-20-05-05 pays "a primary and secondary heat source" and incidental charges (tank rental, hook-up, leak checks) at the share. The program page says LIHEAP "will continue to pay for approved primary and secondary heat sources".
    • The body says secondary heating needs "distinctions the inputs do not carry". But main's heating_type documentation keeps secondary fuel bills "in their own expense inputs". A wood, coal or fuel-oil bill in a home with another primary fuel is identifiable; electricity and propane are ambiguous.
    • Consider paying the unambiguous secondary fuels, or correcting the body's wording, and mention incidental charges.
  • G5. Years outside FY2026 (unclaimed, documented).
    • Backfill. Model years 2015-2025 use FY2026 limits and the full electric share. But the 75% electric adjustment applied until June 1, 2025, and the axiom issue notes an earlier benefit method (Manual Letter 3750).
    • FY2027. The program page publishes FY2027 60% limits for 1-12 members effective October 1, 2026 ($3,446 a month for one person).
    • End-dating the values or adding the FY2025 table would avoid silent estimates.
  • G6. Archive captures.
    • The web manual is now version 26.1 (published October 1, 2026), and 415-25-05-10 will be revised for FY2027. It "is the controlling current source", and copies are "point-in-time references".
    • Wayback captures of 415-25-05-10, 415-30-10 and 415-30-30 would keep the FY2026 references corroborating (MT Add Montana LIHEAP regular heating assistance #10435 G3 precedent).

Verified correct

  • Income limits. All 140 workbook upper limits match P/income_limits.yaml (checked cell by cell). The workbook's lower limits are always the prior upper limit plus $1, so the inclusive <= lookup matches the printed rows at every edge. The six-person $102,894 equals 12 × $8,574.50 (415-25-05-10's monthly figure).
  • Shares. P/shares.yaml and P/smi_band_limits.yaml match 415-30-10 (0-10 and 11-20: 100%; 21-30: 95%; 31-40: 85%; 41-50: 65%; 51-60: 45%). The 45% minimum and 100% maximum match 415-30-05.
  • Renter table. Half-up(cap × share / 12) reproduces all 20 cells of 415-30-30: oil 183/174/156/119/83, propane 146/139/124/95/66, electricity 125/119/106/81/56, gas 58/55/50/38/26. The yearly caps are $2,200, $1,750, $1,500 and $700, and the propane 1,752 → 1,750 cap applies. The monthly renter check for unsubsidized renters matches 415-30-05.
  • Federal floor. Strictly below 1.1 × the 2025 guideline (45 CFR 96.85(a) timing), with the 45% minimum. It binds only at sizes 21 and up (size 20: $132,165 is below $135,630). The above-28 SMI extension follows 45 CFR 96.85(b).
  • Electric heat. The full pre-subsidy bill is paid at the share. 415-20-05-05-05 (the 75% adjustment) was repealed June 1, 2025.
  • Income (manual 415-25-05-05, -20-05, -20-20, -30 and -50; 415-20-10-50):
    • The 27% deduction applies to earnings only.
    • The dependent-student exclusion applies through age 19.
    • Nonqualified members' income and deductions count, but the members do not.
    • Bank interest and dividends are excluded.
    • Contributions count from $360 a year, and TANF counts once.
    • Each source is floored at zero.
    • Deductions cover premiums (including pre_tax_health_insurance_premiums, item 14), medical, support paid and child care.
  • Housing and assets. Subsidized households that pay their own heating bill follow the ordinary rules (415-20-10-30). is_in_public_housing is honoured even without receives_housing_assistance (Case 40). There is no asset test (plan 2.3).
  • Code and docs.
    • Parameter descriptions follow the house template.
    • References link the manual pages directly, one entry per source, and variable reference tuples have no titles.
    • The changelog fragment is top-level added.
    • Test periods are plain, and there are no parameter overrides.
  • Wiring. nd_liheap is not added to a household aggregate. That is consistent with every merged LIHEAP state on main (OR, NY, NJ, MD, VA, AZ, IA and others are not aggregated either).
  • Guards (merged tree PolicyEngine/main + head):
  • NH Add New Hampshire state supplementation for 2026 #10156 pattern: safe. The size is clipped to 1-28 before the income_limits lookup, so a non-resident's size of 0 reads key 1. renter_annual_cap covers all 11 HeatingType values. Case 11 (a South Dakota household) passes.
  • Checklist items:
    • 12 (IIJA): no supplement, so not applicable.
    • 16: military_retirement_income ✓.
    • 17: North Dakota has no SSP variable on main or in an open PR, so not applicable.

Hand-checked expectations (16):

Case Arithmetic Expected
nd_liheap 1 20,000 × 0.73 = 14,600; 13,519-20,274 → 95%; 1,000 × 0.95 950 ✓
nd_liheap 3 33,787 > 33,786 → 45% 450 ✓
nd_liheap 5 40,531 > 40,530 → 0 0 ✓
nd_liheap 13 2,200 × 0.95 / 12 = 174.17 → 174; × 12 2,088 ✓
nd_liheap 16 2,200 × 0.45 / 12 = 82.50 → 83 (table $83); × 12 996 ✓
nd_liheap 17 1,750 / 12 = 145.83 → 146; 1,752 capped 1,750 ✓
nd_liheap 31 700 × 0.45 / 12 = 26.25 → 26; × 12 312 ✓
nd_liheap 33 34,000 − 1,200 − 2,400 − 600 = 29,800 → 65% 650 ✓
nd_liheap 34 37,000 − 1,200 − 600 = 35,200 → 45% 450 ✓
nd_liheap 37 unit 2: 9,600 + 3,600 = 13,200 ≤ 17,670 → 100% 3,000 ✓
nd_liheap 38 20,440 − 1,200 = 19,240 (95%); 18,040 (95%); 20,440 (85%) 2,850 / 2,850 / 2,550 ✓
nd_liheap 39 floor 1.1 × (15,650 + 20 × 5,500) = 138,215; 138,000 and 138,214.99 → 45%; 138,215 → 0 1,350 / 1,350 / 0 / 0 ✓
nd_liheap 42 0.52 + 5 × 0.16 + 23 × 0.03 = 2.01; 129,911 × 2.01 = 261,121.11; 40% 104,448.44 < 130,000 ≤ 50% 130,560.56 → 65% 1,950 ✓
countable 13 25,000 + 8,000 × 0.73 30,840 ✓
countable 15 14,600 − 1,000 − 600 − 2,000 11,000 ✓
household size 1 size 1; 10,000 + 10,000 × 0.73 = 17,300 → 95% 950 ✓

The share-test Case 1 also follows the workbook at all 280 households. Sizes 21-28 at the dollar after the 60% limit get 0.45 through the floor (for example 137,971 < 138,215).

Axiom line assessment

axiom: TheAxiomFoundation/rulespec-us#1902 queued.

  • Valid. The issue is OPEN and labelled pe-parity (updated 2026-10-24). It is dispatch-ready:
    • module paths;
    • corpus manifests and the missing-source blocker;
    • verbatim manual, workbook and statute text;
    • required outputs by variable;
    • companion tests whose expected values come from those sources, not from PolicyEngine output.
  • Matches the head. It describes head 62c5fa449b with 64 cases, total_unemployment_compensation and military_retirement_income.
  • After the fixes, refresh:
    • interpretation 5 and companion tests 2 and 4 (C1's ceiling);
    • interpretation 11 and companion test 12(f) (S1);
    • interpretation 15 (S2);
    • the companion tests for the new cases (S3).

CI status

gh pr checks 10342 at head 62c5fa449b: 34 pass and 3 skipping (the umbrella jobs), with 0 failures and 0 pending. The run is not affected by either known main-caused failure.

Branch status

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.

Requesting changes: 1 critical (the 60% income limits exceed the federal ceiling of 60% of HHS's state median income, 42 USC 8624(b)(2)(B), by $2-$12 at sizes 2-9, 11 and 12), 3 should. Full report in the comment above.

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

hua7450 commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review, David. The fixes are in 14a5193, and 1f953ba merges current main.

  • C1 (federal ceiling). Fixed. New federal parameter gov.hhs.liheap.fpg_limit is 1.5 (42 U.S.C. 8624(b)(2)(B)(i)), and nd_liheap_share.py caps every band at the greater of liheap_smi_limit and 150% of the lagged guideline, citing 8624(b)(2)(B), 8622(11) and IM 2025-02 Attachment 4 (pages 2 and 5). The lower bands never reach the cap, so this matches capping the top band; the 110% floor is unchanged. The capped limits for sizes 2-9, 11 and 12 equal Attachment 4, and the ten nd_liheap_share.yaml Case 1 households at those printed limits now expect 0. New nd_liheap.yaml Case 43 is your example: $77,946 pays $1,350 and $77,950 pays $0. New candidate us-nd:liheap-table-above-federal-ceiling follows statute, and the six-person candidate now notes that both printed figures exceed $102,888.
  • S1 (Medicare premiums). Fixed. nd_liheap_countable_income.py deducts each member's computed medicare_part_a_premium and medicare_part_b_premium (out of pocket and net of Medicare Savings Program coverage, which applies the QMB/SLMB/QI-1 bar) plus non-Medicare premiums floored at zero. A positive health_insurance_premiums total still takes precedence, and medicare_part_b_premiums_reported is no longer read. Case 35 now pays $650 (34,000 - 2,434.80 = 31,565.20). New Case 44: a Medicare Savings Program pays Part B, so nothing is deducted ($950). New Case 45: with 35 quarters, Part A ($3,732) and Part B ($2,434.80) come off a $38,000 pension (31,833.20, $650). Cases 32-34 and 41 and countable Cases 17 and 18 are re-pinned (Case 41: 27,565.20, still $1,950); countable Case 18 drops its second household, which tested the reported Part B input.
  • S2 (subsidized tenants with an electric bill). Fixed. Following 415-20-10, nd_liheap_eligible.py admits a subsidized heat-in-rent household whose pre_subsidy_electricity_expense is positive, and nd_liheap.py pays it the share of that bill, uncapped like any billed household, with nothing for the heat in its rent. New Case 46 is your example: $1,140. As in every state, this path needs a known heating_type, so new Case 48 (UNSPECIFIED) pays $0. Case 10 (no electric bill) stays at $0, and the PR body's eligibility row now states the 415-20-10 rule.
  • S3 (untested entries). Fixed. New countable Case 19 gives all 6 earned and 19 unearned sources distinct positive amounts, entering unemployment_compensation and military_retirement_pay_survivors: earned 22,000 x 0.73 = 16,060, unearned 16,850, total 32,910. Countable Case 20: a -$3,000 rental loss beside a $20,000 pension leaves 20,000. Countable Case 21 has three households, each with a 17-year-old earning $8,000 beside a $25,000 pension: a non-dependent in school is counted (30,840), a dependent not in school is counted (30,840) and a dependent in K-12 school is excluded (25,000). nd_liheap.yaml Case 47: kerosene heat in unsubsidized rent at the 95% share pays $660 from the $700 cap. LIHEAP wave 2b: encode North Dakota LIHEAP regular heating assistance TheAxiomFoundation/rulespec-us#1902 mirrors the new cases.
  • G1 (candidates). Adopted. New candidates us-nd:liheap-plan-income-share-design (plan items 2.5-2.6 against the manual's fixed share) and us-nd:liheap-child-earnings-exclusion (plan item 1.9 against 415-25-05-50) both follow gap-fill, and us-nd:liheap-renter-table-vs-subtraction-prose and us-nd:liheap-six-person-limit-workbook-vs-manual are relabelled gap-fill.
  • G2 (UNSPECIFIED heat in rent). Declined. Callers must send heating_type, and UNSPECIFIED paying $0 is by design (Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754), so no $700 floor is added.
  • G3 (military pay). Adopted. military_basic_pay is on the earned list in income/earned_sources.yaml, so it takes the 27% deduction (countable Case 19). BAS and BAH have no input; the list comment and the body say so.
  • G4 (secondary heat and incidental charges). Documented. The body now says that 415-20-05-05 pays a secondary heat source and incidental charges at the share, and that the model pays only the primary fuel's bill: a secondary fuel's bill can sit in its own expense input but is not paid, and incidental charges have no input.
  • G5 (years outside FY2026). Documented. The body now labels 2015-2025 as backfilled estimates, notes that the 75% electric adjustment applied until June 1, 2025 and that Manual Letter 3750 describes an earlier benefit method, and states that the FY2027 limits effective October 1, 2026 are not added.
  • G6 (archive captures). No change. The Wayback CDX API returns no capture of 415-25-05-10, 415-30-10 or 415-30-30, in the FY2026 window or at any date, so no archived references were added.

The PR body and TheAxiomFoundation/rulespec-us#1902 are updated for 1f953ba. I did not run the tests locally, so CI will check them.

@hua7450
hua7450 requested a review from DTrim99 October 27, 2026 19:43
@DTrim99

DTrim99 commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

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

PR #10342 (author: hua7450), head 1f953ba83b. Round 1 reviewed 62c5fa449b and requested changes (1 critical, 3 should, 6 suggestions). Since then the author has pushed:

  • one fix commit, 14a5193a94;
  • a merge of main, 1f953ba83b, with one hand-resolved block in programs.yaml.

The PR still touches no partner test file.

Path prefixes:

  • P/ = policyengine_us/parameters/gov/states/nd/dhs/liheap/
  • V/ = policyengine_us/variables/gov/states/nd/dhs/liheap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/nd/dhs/liheap/

Summary

All four blocking findings are fixed, and a test would fail without each fix:

  • C1. Federal ceiling. Every band is now capped at the greater of HHS's 60% of state median income and 150% of the poverty guideline, which is the 42 U.S.C. 8624(b)(2)(B) ceiling. The ten printed limits above it now pay 0. A follows: statute candidate records the choice.
  • S1. Medicare premiums. The deduction now uses each member's computed out-of-pocket Part A and Part B premiums, net of Medicare Savings Program coverage.
  • S2. Subsidized tenants. A subsidized household with heat in its rent and an electric bill in its name is now paid the share of that bill.
  • S3. Tests. All 25 income sources, the unearned floor, the student conditions and the kerosene renter cap are now tested.

The suggestions. Five of the six were adopted or documented, and the author declined G2 (UNSPECIFIED) with a reason I accept.

The merge. The hand-resolved programs.yaml block is correct.

The rulespec-us issue. rulespec-us#1902 has been updated to describe the new head.

One new should-address item:

  • N1. Unpinned public-housing condition. The fix adds a new payment selector in V/nd_liheap.py. Its public-housing condition is not pinned: deleting is_in_public_housing from that line fails no test. If it were deleted, a public-housing tenant with heat in rent and its own electric bill would get $660 instead of $1,140.

Four new suggestions:

  • N-G1: the UNSPECIFIED gate on the electric-bill path.
  • N-G2: tighter pins on the federal maximum and the remaining "Other" renter caps.
  • N-G3: a possible double count of a reported Part A premium.
  • N-G4: a stale code comment.

Status.

  • CI: 31 pass and 2 pending, both Contrib shards. Both state baseline shards that run these YAML files are green at the head.
  • Branch: 10 commits ahead of main and 2 behind, with a clean merge.

Prior findings

Finding Status Evidence
C1. The 60% column exceeds the 8624(b)(2)(B) ceiling at sizes 2-9, 11 and 12 Fixed Code: V/nd_liheap_share.py:43-59 caps every band at max_(liheap_smi_limit(size, state, 0.6), 1.5 × lagged guideline). The 1.5 is the new gov.hhs.liheap.fpg_limit, citing 42 U.S.C. 8624(b)(2)(B)(i).
Caps: they match Attachment 4 at all ten sizes (for example size 4: floor(0.6 × 129,911) = 77,946).
Tests: T/nd_liheap_share.yaml Case 1 now expects 0 for the ten printed limits (for example line 3417). T/nd_liheap.yaml Case 43 (line 1338) is the round-1 example: $77,946 pays $1,350 and $77,950 pays $0.
Mutations: removing the cap fails 10 share households and Case 43. Dropping the 150% term fails Case 1 at sizes 13-20, where the printed 60% limits sit between Attachment 4 and 150% of the guideline (size 13: 119,257 < 119,262 ≤ 122,475). Turning max_ into min_ fails Cases 4 and 43.
Records: candidate us-nd:liheap-table-above-federal-ceiling (follows: statute), and the six-person candidate is updated.
S1. Medicare premiums are deducted only when reported Fixed Code: V/nd_liheap_countable_income.py:41-50 deducts the non-Medicare premiums, floored at zero, plus the computed medicare_part_a_premium and medicare_part_b_premium. A positive health_insurance_premiums total still takes precedence. medicare_part_b_premiums_reported is no longer read.
Tests: Case 35 now gives $650 (34,000 − 2,434.80). Case 44 (MSP pays Part B) gives $950. Case 45 (Part A of $3,732) gives $650.
Mutations: dropping Part B fails Cases 33, 35 and 41 and countable Cases 17-18. Dropping Part A fails Case 45. Dropping the precedence fails Cases 32 and 34. Dropping the floor on non-Medicare premiums fails countable Case 18 (31,865.20 instead of 31,565.20).
S2. Subsidized heat-in-rent households with their own electric bill are excluded Fixed (see N1) Code: V/nd_liheap_eligible.py:31-34 admits a subsidized heat-in-rent household when pre_subsidy_electricity_expense > 0, and V/nd_liheap.py:50-57 pays max_(bill, 0) × share.
Tests: Case 46 (line 1441) is the round-1 example: eligible, $1,140. Case 10 (no electric bill) still asserts nd_liheap_eligible: false, so the remaining exclusion is pinned.
Mutations: reverting the eligibility change fails Case 46. Removing the electric branch also fails Case 46, because the renter amount is $660.
Body: the eligibility row now states the 415-20-10 rule.
S3. 16 of 24 income entries and several branches are untested Fixed Sources: countable Case 19 (line 447) gives all 6 earned and 19 unearned sources distinct positive amounts, so dropping any entry changes a total. unemployment_compensation and military_retirement_pay_survivors exercise the totals.
Floor and students: Case 20 pins the unearned floor (a rental loss against a pension). Case 21 pins dependency, school attendance and the K-12 arm.
Renter cap: T/nd_liheap.yaml Case 47 pins the kerosene $700 cap, which is the case round 1 asked for. WOOD, COAL, SOLAR and OTHER share the same untested $700 entry (N-G2).
G1. Record source choices and relabel candidates Adopted us-nd:liheap-plan-income-share-design and us-nd:liheap-child-earnings-exclusion are added as gap-fill, and the renter-table and six-person candidates are relabelled gap-fill. The ids are sorted and unique, and CI's source-conflict checks pass.
G2. UNSPECIFIED heat-in-rent pays $0 Declined; reason accepted #9754 keeps UNSPECIFIED as a value that callers send explicitly, and every new state LIHEAP program expects it to pay $0. The body and axiom interpretation 14 document this.
G3. Military pay Adopted military_basic_pay is on P/income/earned_sources.yaml, as in Iowa's merged list. Countable Case 19 includes $2,000 of it in the 22,000 × 0.73. The allowances have no input, and the comment and body say so.
G4. Secondary heat and incidental charges Documented The body now states the 415-20-05-05 rule and what is not paid. The code comment at V/nd_liheap.py:38-39 was not updated (N-G4).
G5. Years outside FY2026 Documented The "Years covered" section now labels 2015-2025 as backfilled estimates and names the 75% electric adjustment, Manual Letter 3750 and the unadded FY2027 limits.
G6. Archive captures Declined; reason accepted A Wayback CDX query for 415-25-05-10, 415-30-10 and 415-30-30 returns no capture, as the author says. A Save Page Now capture is optional.

New findings

Critical

None.

Should address

N1. The new payment selector's public-housing condition is not pinned by any test
  • Where: V/nd_liheap.py:43-45:
    subsidized = spm_unit(
        "receives_housing_assistance", period
    ) | spm_unit.household("is_in_public_housing", period)
    The fix copies the subsidy definition from V/nd_liheap_eligible.py:20-22 and uses it to route subsidized heat-in-rent households to the electric-bill payment (select([heat_in_rent & subsidized, heat_in_rent], ...)).
  • Why no test fails:
    • Only Case 40 (line 1184) sets is_in_public_housing.
      • Its heat-in-rent household has no electric bill, so nd_liheap_eligible makes it ineligible whatever nd_liheap.py does.
      • Its other household pays its own oil bill and takes the default branch.
    • Case 46, the only electric-bill case, uses receives_housing_assistance.
    • So deleting | spm_unit.household("is_in_public_housing", period) from nd_liheap.py fails no test. (The eligibility copy is pinned by Case 40.)
  • Effect of that mutation (FY2026; one person aged 40; pension $15,000; is_in_public_housing true and receives_housing_assistance false; heat in rent, NATURAL_GAS; own electric bill $1,200):
    • Head: eligible, 0.95 × 1,200 = $1,140. This is correct under manual 415-20-10.
    • Mutant: the household passes eligibility through nd_liheap_eligible.py but takes the unsubsidized renter branch: 700 × 0.95 / 12 = 55.42 → $55; 12 × 55 = $660. No test notices.
  • Severity: should.
    • Rule: the mutation rule (checklist item 13; the wave's "any new branch with no test").
    • Utah round-2 precedent: a gap in test pins that a review fix itself introduces is a new should-address item.
    • Not critical: the head's result is correct today.
  • Fix: either of these works.
    • Add a second household to Case 46 with is_in_public_housing: true and receives_housing_assistance: false, expecting nd_liheap_eligible: true and $1,140.
    • Or compute the subsidy flag once (one variable read by both formulas), so that Case 40 pins both uses.
    • Either way, mirror the case in rulespec-us#1902 companion test 9.

Suggestions

  • N-G1. The UNSPECIFIED gate on the electric-bill path.
    • V/nd_liheap_eligible.py:31-33 admits a subsidized heat-in-rent household only when heating_type != UNSPECIFIED, and Case 48 pins $0.
    • The payment is the share of pre_subsidy_electricity_expense, which does not depend on the building's fuel. So the gate denies a household whose bill the inputs already carry.
    • It is documented (body, axiom interpretation 15) and follows the convention accepted for G2, so I rate it a suggestion. Consider dropping the gate and re-pinning Case 48 to $1,140.
  • N-G2. Tighter pins.
    • Federal maximum. Case 43's ineligible household is $4 above the cap ($77,950). $77,947 would pin the exact Attachment 4 amount. At the other nine capped sizes only "printed limit → 0" is tested, so a cap-edge pair at one more size (for example size 2: $53,003 → 0.45 and $53,004 → 0) would pin the cap there as well.
    • Renter caps. Turning Case 47 into a vector over WOOD, COAL, SOLAR and OTHER would pin the remaining $700 "Other" entries in P/renter_annual_cap.yaml. Axiom companion test 7 already lists them.
  • N-G3. Possible double count of a reported Part A premium.
    • main documents health_insurance_premiums_without_medicare_part_b as premiums "excluding Medicare Part B". A reported amount there can therefore include a paid Part A premium, which V/nd_liheap_countable_income.py:46 then deducts again through medicare_part_a_premium.
    • main's own medical_expense_health_insurance_premiums adds only Part B to that input.
    • Premium-paying Part A is rare. Say in the code comment and the body that the non-Part-B input must exclude Part A, or note the overlap among the not-modeled items.
  • N-G4. Stale code comment. V/nd_liheap.py:38-39 still says secondary heating needs "distinctions absent from current inputs". The body (after G4) now says that a secondary fuel's bill can sit in its own input but is not paid. Align the comment.

Verified

Hand-checked expectations. Every new or changed expectation is hand-checked. The 2026 guideline, Medicare and SMI figures are those in the cases' comments and on main.

Case Arithmetic Expected
nd_liheap 10 (renamed) subsidized, heat in rent, no electric bill → ineligible false, 0 ✓
nd_liheap 32 total 2,400 replaces Part B 2,434.80: 34,000 − 2,400 = 31,600 → 65% band (27,031-33,786) 650 ✓
nd_liheap 33 34,000 − (1,200 + 2,434.80) − 600 = 29,765.20 → 65% 650 ✓
nd_liheap 34 37,000 − 1,200 − 600 = 35,200 → 45% (33,787-40,530); with components it would be 32,765.20 (65%) 450 ✓
nd_liheap 35 Part B 202.90 × 12 = 2,434.80, Part A 0 (40 quarters); 34,000 − 2,434.80 = 31,565.20 → 65% 650 ✓
nd_liheap 41 30,000 − 2,434.80 = 27,565.20, above 27,030 and at most 33,786 → 65%; 3,000 × 0.65 1,950 ✓
nd_liheap 43 cap = max(floor(0.6 × 129,911) = 77,946, 1.5 × 32,150 = 48,225) = 77,946. 77,946 → 45%, 3,000 × 0.45; 77,950 → no band and not below 1.1 × 32,150 = 35,365 1,350 / 0 ✓
nd_liheap 44 MSP: 15,000 / 12 − 20 = 1,230, under the QMB limit, so MSP pays 2,434.80 and Part B is 0; 15,000 → 95% (13,519-20,274) 950 ✓
nd_liheap 45 Part A 311 × 12 = 3,732 (35 quarters); MSP 3,146.67 is above the QI limit of 1,795.50; 38,000 − 3,732 − 2,434.80 = 31,833.20 → 65% (45% without Part A: 35,565.20) 650 ✓
nd_liheap 46 15,000 → 95%; 1,200 × 0.95 1,140 ✓
nd_liheap 47 700 × 0.95 / 12 = 55.42 → 55; × 12 = 660 ≤ 700 660 ✓
nd_liheap 48 UNSPECIFIED → no own-bill path → ineligible false, 0 ✓
countable 17 −1 is not a positive total: 34,000 − (1,200 + 2,434.80) − 600 29,765.20 ✓
countable 18 −300 floored to 0: 34,000 − 2,434.80 31,565.20 ✓
countable 19 earned 12,000 + 3,000 + 2,500 + 1,500 + 1,000 + 2,000 = 22,000 × 0.73 = 16,060; unearned (19 distinct amounts) 16,850 32,910 ✓
countable 20 pension 20,000, rental −3,000 floored to 0 20,000 ✓
countable 21 non-dependent in school: 25,000 + 5,840; dependent not in school: 25,000 + 5,840; dependent in K-12: 25,000 30,840 / 30,840 / 25,000 ✓
share 1 (ten changed) printed 60% limit above the cap → no band; caps 53,003, 65,474, 77,946, 90,417, 102,888, 105,227, 107,565, 109,903, 114,580, 116,919 (floor(77,946 × size factor)), each above 150% of the guideline 0 × 10 ✓

Unchanged behaviour. The other share households keep their round-1 values:

  • size 1 (40,530 is under the 40,531 cap);
  • size 10 (112,242 equals the cap);
  • sizes 13-28 (150% of the guideline, 122,475 at size 13, is above the table);
  • the 110% floor at sizes 21-28 (the cap there is 188,475 or more).

Merge resolution. I checked the one hand-resolved block in policyengine_us/programs.yaml:

  • The coverage line is main's line (with AZ, IA and VA) plus ND.
  • The state block keeps VA and appends ND once.
  • LIHEAP state_implementations has 25 entries on main and 26 at the head, so no sibling entry was dropped or duplicated.
  • source_conflicts/candidates.yaml merged with additions only: six us-nd:liheap-* ids, sorted and unique.

Guards on the merged tree (PolicyEngine/main + head, tree 62e8f593c6):

Other checks.

  • NH Add New Hampshire state supplementation for 2026 #10156 pattern: still safe. liheap_smi_limit and fpg use the same state and size keys as round 1, and Case 11 (South Dakota) passes in CI.
  • New federal parameter: gov.hhs.liheap.fpg_limit follows the house template and has one statute reference.
  • Siblings: no open LIHEAP PR adds a file under gov/hhs/liheap/.
  • References: the share variable's Attachment 4 entry uses # PDF pages 2, 5 above the #page=2 href, and the plan reference is a single page.
  • Periods: test periods are plain.
  • Body: the counts (73 cases: 48, 21, 2, 2) match the files.

Axiom

axiom: TheAxiomFoundation/rulespec-us#1902 queued: valid and current.

  • Status: the issue is OPEN, labelled pe-parity, and was updated 2026-10-27 at 19:43 UTC.
  • Head: it describes head 1f953ba83b (14a5193a94 on 62c5fa449b) with 73 cases.
  • Updates: interpretation 5 (the federal maximum via fpg_limit), 11 (computed Part A/B net of MSP; the reported Part B input unused) and 15 (the subsidized electric bill, with the UNSPECIFIED condition) are updated.
  • Companion tests cover the new cases: 9(e)-(f), 12(f)/(h)/(i), 13(j)/(k) and 14. Their expected values come from the manual, the workbook, Attachment 4 and CMS's 2026 premiums.
  • After N1: add the public-housing electric-bill household to companion test 9.

CI

gh pr checks 10342 at head 1f953ba83b (run 40571552875): 31 pass and 2 pending.

  • Pending: Contrib other-shard-3 and Contrib states-shard-2, still queued. They run contributed-reform tests, not these files.
  • Passing: all Baseline shards (including the four state shards that run the ND YAML files), Rest Python shards 1-6, Microsimulation, Household API Partners, PR validation and Quick Feedback.
  • Known failure: Rest (Python shard 3) passed, so the known main-caused test_social_security_input_state_machine failure did not occur on this run.
  • Failures: none.

Branch

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.

Requesting changes, round 2: 0 critical, 1 should. The critical and all three shoulds from round 1 are fixed and pinned. One new should: the public-housing condition in the new payment selector (nd_liheap.py) can be deleted without any test failing; add a public-housing heat-in-rent household with its own electric bill to Case 46. Full report in the comment above.

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 North Dakota LIHEAP regular heating assistance

2 participants