Skip to content

Add North Dakota LIHEAP regular heating assistance - #10342

Merged
hua7450 merged 12 commits into
mainfrom
nd-liheap-wave2b
Oct 28, 2026
Merged

hua7450 merged 12 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. Reported premiums other than Part B should exclude Part A, which would otherwise be deducted twice. 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,947 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 74 cases under policyengine_us/tests/policy/baseline/gov/states/nd/dhs/liheap/ (nd_liheap.yaml 49, 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,947 pays $0; Case 49 does the same for two people ($53,003 pays $1,350 and $53,004 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 two heat-in-rent tenants, one receiving housing assistance and one in public housing, 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 unsubsidized kerosene, wood, coal, solar and other renters 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.

hua7450 and others added 2 commits October 28, 2026 10:42
Pin the public-housing branch of the subsidized heat-in-rent payment with
a second Case 46 household, test the federal maximum at the exact four- and
two-person cap edges, cover every Other-column fuel in the renter cap case,
note that the non-Part-B premium input should exclude Part A, and align the
secondary-heat comment with the manual.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	policyengine_us/programs.yaml
@DTrim99

DTrim99 commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

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

PR #10342 (author: hua7450), head 5cea524422. Round 2 reviewed 1f953ba83b and requested changes (0 critical, 1 should, 4 suggestions). Since then the author has pushed:

  • one commit, 705f8cec8d, which changes tests and two code comments but no formula or parameter;
  • a merge of main, 5cea524422, with one hand-resolved block in programs.yaml.

The PR still touches no partner test file.

Path prefixes:

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

Summary

N1 is fixed. Case 46 now has a second household that lives in public housing and gets no other housing assistance. If the public-housing condition is deleted from the payment selector in V/nd_liheap.py, that household's expected $1,140 becomes $660 and the test fails.

The suggestions:

  • N-G2 (tighter pins) is adopted.
  • N-G3 (Part A double count) is adopted as a code comment.
  • N-G4 (stale comment) is fixed.
  • N-G1 (UNSPECIFIED gate on the electric-bill path) was not taken up. It is still documented and still optional.

The round-1 fixes still hold. The commit changes no formula or parameter, so C1 and S1-S3 are as round 2 verified them.

The merge is correct. The hand-resolved programs.yaml block keeps every sibling entry. It also adds NH and UT to the LIHEAP coverage line, which main had left out even though both states have entries.

CI: 24 pass and 9 pending (eight Contrib shards and Microsimulation), with no failures. The state shard that runs the ND files (states-shard-2) passes all 74 ND cases, and so does Quick Feedback.

Branch: 12 commits ahead of main and 0 behind. The merge is clean.

One new should-address item:

  • A1. Stale rulespec-us issue. rulespec-us#1902 has not been updated since round 2. It still describes 1f953ba83b with 73 cases. Its companion test 9 lacks the public-housing electric-bill household that round 2 asked to mirror with the N1 fix.

One new suggestion:

  • R3-G1. Refresh the PR body's test plan, which still describes the round-2 tests.

Prior findings

Finding Status Evidence
N1. The payment selector's public-housing condition was unpinned Fixed Code: V/nd_liheap.py:45-47 is unchanged: subsidized = receives_housing_assistance | is_in_public_housing, used by select([heat_in_rent & subsidized, heat_in_rent], [electric_payment, renter_payment], ...) at lines 55-59.
Test: T/nd_liheap.yaml Case 46 (line 1445) has two households. The first gets housing assistance; the second has is_in_public_housing: true and receives_housing_assistance: false. Each has heat in rent (NATURAL_GAS), a $15,000 pension and a $1,200 electric bill. Both expect eligible and $1,140. Both subsidy inputs have no formula on main.
Mutation: delete the public-housing term from V/nd_liheap.py. The second household still passes nd_liheap_eligible, because the eligibility formula has its own copy of the flag. It then takes the renter branch: 700 × 0.95 / 12 = 55.42 → 55; 55 × 12 = $660 ≠ $1,140, so the case fails.
Other term: deleting the receives_housing_assistance term fails the first household the same way.
Eligibility copy: Case 40 still pins the copy in V/nd_liheap_eligible.py:20-22 (a public-housing tenant with heat in rent and no electric bill is ineligible).
N-G1. The UNSPECIFIED gate on the electric-bill path Not taken up; still documented V/nd_liheap_eligible.py:31-33 and Case 48 are unchanged. The PR body (eligibility row and "Not modeled" list) and axiom interpretations 14-15 document it. The author posted no reply on it. It remains an optional suggestion.
N-G2. Tighter pins on the federal maximum and the "Other" renter caps Adopted Four-person cap: Case 43's second household moved from $77,950 to $77,947, one dollar above the Attachment 4 figure of $77,946. A cap one dollar too high now fails the case.
Two-person cap: new Case 49 (line 1591) tests the cap edge: $53,003 pays $1,350 and $53,004 pays $0.
Renter caps: Case 47 is now a vector over KEROSENE, WOOD, COAL, SOLAR and OTHER, each $660. Every $700 "Other" entry in the renter cap parameter is now pinned.
N-G3. Possible double count of a reported Part A premium Adopted (code comment) V/nd_liheap_countable_income.py:40-42 now says the non-Part-B input "should not include a Part A premium; one entered there is deducted twice."
Wording: the comment calls that input "the non-Medicare component". The body still says an out-of-pocket Part D premium counts "only when reported among other premiums", which is that same input. "Premiums other than Medicare Part B" would match both.
Body: it has no matching note. Both points are optional (R3-G1).
N-G4. Stale code comment on secondary heat Fixed V/nd_liheap.py:38-41 now says manual 415-20-05-05 pays a secondary heat source and that a secondary fuel's bill "can sit in its own expense input but is not paid here". It also names the nonresidential-use caps (415-20-05-05-15). This matches the body.

New findings

Critical

None.

Should address

A1. rulespec-us#1902 has not been updated for this head
  • State:
    • The issue is OPEN and labelled pe-parity.
    • It was last updated 2026-10-27 at 19:43 UTC, before the round-3 commit (2026-10-28 14:42 UTC).
    • I checked again at 15:25 UTC.
  • What is stale:
    • Head and counts: the header and PR line still say head 1f953ba83b, and the tests line says "73 YAML cases ... (nd_liheap.yaml 48)". The head has 74 cases, 49 of them in nd_liheap.yaml.
    • Companion test 9 (subsidized housing) lists households (a)-(f). None is a public-housing tenant with heat in rent and its own electric bill. That is the household N1's fix adds to Case 46. Round 2 asked for it to be mirrored in test 9, because it is the only case that exercises the selector's public-housing arm.
    • Companion test 14 (federal maximum) still gives Case 43's second household as $77,950, which is now $77,947. It does not include Case 49's two-person cap edge.
  • What is still right:
    • Interpretations 5 and 15, and the eligibility row (subsidized means receives_housing_assistance or is_in_public_housing), still describe the code.
    • So the encoder's reading of the law is unaffected. What is out of date is the companion tests and the record of the head.
  • Severity: should.
    • The wave treats an axiom issue that does not match the head (including its case counts) as should.
    • N1's fix asked for this mirror explicitly.
  • Fix: update Add formula for CA AGI subtractions variable #1902 to head 5cea524422 with 74 cases (49 / 21 / 2 / 2). Then:
    • Test 9: add (g): pension $15,000; is_in_public_housing true and receives_housing_assistance false; heat in rent; NATURAL_GAS; pre_subsidy_electricity_expense $1,200. It is eligible for only the electric bill in its name (415-20-10): 1,200 × 0.95 = $1,140.
    • Test 14: change Case 43's second household to $77,947. Add the two-person pair: M = max(53,003, 1.5 × 21,150 = 31,725) = 53,003, so $53,003 gets the 45% share ($1,350) and $53,004 gets $0 (not below 1.1 × 21,150 = 23,265).

Suggestions

  • N-G1 (carried). See the table. Drop the UNSPECIFIED gate on the electric-bill path, or keep it documented as it is.
  • R3-G1. Refresh the PR body's test plan. It still describes the round-2 tests:
    • Counts: it says "73 cases (nd_liheap.yaml 48)". The files now hold 74 cases, with 49 in nd_liheap.yaml. CI's Quick Feedback run counted 49 + 21 + 2 + 2.
    • Case 43: it says "$77,950 pays $0", but the case now uses $77,947. The Benefit section's $77,950 example is still arithmetically right.
    • Cases 46, 47 and 49: each is described as it was in round 2 or not at all. Case 46 is described as one tenant, Case 47 as kerosene only, and Case 49 is missing.
    • Part A note: a "Not modeled" line on the reported Part A overlap (N-G3) would round this out.
    • Comment wording: V/nd_liheap_countable_income.py:41 calls the non-Part-B input "the non-Medicare component". "Premiums other than Medicare Part B" would agree with the body's statement that a Part D premium is reported there.

Verified

Hand-checked expectations. Every new or changed expectation is checked below. The 2025 guidelines are $15,650 + $5,500 per extra person. North Dakota's FY2026 four-person SMI is $129,911, and the size factors are HHS's (1.00 for four people, 0.68 for two).

Case Arithmetic Expected
nd_liheap 43, household 2 (changed) Cap = max(floor(floor(0.6 × 129,911) × 1.00) = 77,946; 1.5 × 32,150 = 48,225) = 77,946. 77,947 > 77,946, so no band. Not below 1.1 × 32,150 = 35,365, so no floor. Share 0, ineligible countable 77,947; share 0; false; $0 ✓
nd_liheap 46, household 2 (new) 15,000 is in the one-person 95% band (13,519-20,274). The household is subsidized through public housing, has heat in rent and a $1,200 electric bill, and its fuel is NATURAL_GAS, not UNSPECIFIED, so it is eligible. 1,200 × 0.95 0.95; true; $1,140 ✓
nd_liheap 46, household 1 (unchanged value) as round 2, with is_in_public_housing: false now explicit 0.95; true; $1,140 ✓
nd_liheap 47, WOOD / COAL / SOLAR / OTHER (new) Each renter cap is 700. floor(700 × 95 / 1,200 + 0.5) = floor(55.92) = 55; 55 × 12 = 660 ≤ 700. Unsubsidized heat in rent, so eligible 0.95; true; $660 × 4 ✓
nd_liheap 47, KEROSENE (unchanged) as above $660 ✓
nd_liheap 49, household 1 (new) Size 2. Cap = max(floor(77,946 × 0.68) = floor(53,003.28) = 53,003; 1.5 × 21,150 = 31,725) = 53,003. The printed 45% band is 44,179-53,010, so the capped band is 44,179-53,003. 53,003 → 0.45; 3,000 × 0.45 0.45; true; $1,350 ✓
nd_liheap 49, household 2 (new) 53,004 > 53,003, so no band. Not below 1.1 × 21,150 = 23,265, so no floor 0; false; $0 ✓

Merge resolution (the one hand-resolved block, policyengine_us/programs.yaml):

  • Coverage line: it is main's line (which added WA and WY) plus ND, NH and UT, in alphabetical position.
  • State block: the ND entry moved from after VA to between NC and NE. It appears exactly once.
  • Counts: main has 29 state_implementations and the head has 30. The only difference is ND, and there are no duplicates.
  • programs.yaml diff against main: it is just the coverage line and the ND block.
  • source_conflicts/candidates.yaml merged on its own with additions only. It has 277 ids on main and 283 at the head; the six extra are the us-nd:liheap-* ids. None is dropped, and the list is sorted and unique.

Merged tree. git merge-tree --write-tree PolicyEngine/main 5cea524422 gives 2eeec57b94, which is the head's own tree, because main is already an ancestor.

Guards.

Other checks.

  • NH Add New Hampshire state supplementation for 2026 #10156 pattern: unchanged and safe. No new state-gated key variable was added.
  • Source-conflict wording: the new test comments are in tests, which the ratchet does not scan, and they use no source-conflict wording.
  • Changelog: the fragment is unchanged (changelog.d/nd-liheap-wave2b.added.md).

Axiom

axiom: TheAxiomFoundation/rulespec-us#1902 queued: valid but stale (A1).

  • Valid: the issue is OPEN, labelled pe-parity and dispatch-ready. Its interpretations still describe the code at the head.
  • Stale: it still records head 1f953ba83b with 73 cases. It lacks the public-housing electric-bill household in companion test 9, the $77,947 edge in test 14 and the Case 49 two-person cap edge.

CI

gh pr checks 10342 at head 5cea524422 (run 40708627242), checked at 15:25 UTC: 24 pass and 9 pending.

  • Pending:
    • Contrib congress, other-shard-1, other-shard-2a, other-shard-2b and states-shard-1 to 4. They started at 15:16 UTC and run contributed-reform tests, not these files.
    • Microsimulation, which is still running.
  • Passing: all Baseline shards, Rest Python shards 1-6, Contrib other-shard-3, Household API Partners, Package and compatibility, PR validation and Quick Feedback.
    • ND files: states-shard-2 runs them in the full suite and passes them all.
    • Quick Feedback: passes nd_liheap.yaml 49, nd_liheap_countable_income.yaml 21, nd_liheap_household_size.yaml 2 and nd_liheap_share.yaml 2.
  • Known failure: Rest (Python shard 3) passed, so the known main-caused test_social_security_input_state_machine failure did not occur.
  • Failures: none.

Branch

  • Position: 12 commits ahead of main (bf108e931a) and 0 behind.
  • Merge: git merge-tree is clean (tree 2eeec57b94, the head's own tree).
  • Siblings: the coverage line now names ND, NH and UT. Any open LIHEAP PR that edits the same line will conflict with it. That is a branch note only.

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 3: 0 critical, 1 should. N1 is fixed and pinned, the merge is correct, and the code is done. The one open item needs no code: rulespec-us#1902 still describes the round-2 head. Add the public-housing electric-bill household to companion test 9 (asked for in round 2), move test 14 to $77,947 plus the two-person edge, and update the case count. Full report in the comment above.

@hua7450
hua7450 requested a review from DTrim99 October 28, 2026 18:20
@DTrim99

DTrim99 commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

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

PR #10342 (author: hua7450), head 5cea524422. This head is the one round 3 reviewed. Round 3 requested changes for one should-address item (A1, the stale rulespec-us issue) and left two suggestions. Since then the author has updated rulespec-us#1902 (2026-10-28 18:20 UTC), refreshed the PR body and re-requested review. The code and tests have not changed. The PR still touches no partner test file.

Path prefixes:

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

Summary

A1 is fixed. rulespec-us#1902 is still OPEN, labelled pe-parity and dispatch-ready. It now:

  • names head 5cea524422 and its commits correctly;
  • gives 74 cases (49 / 21 / 2 / 2);
  • adds the public-housing electric-bill household as companion test 9(g);
  • moves Case 43 to the $77,946 / $77,947 pair in test 14;
  • adds Case 49's two-person cap edge ($53,003 pays $1,350; $53,004 pays $0).

I checked every new expected value by hand, and each matches the YAML at the head. The rest of the issue agrees with the head on four points: Medicare netting, the 8624(b)(2)(B) cap, the subsidized heat-in-rent electric-bill rule and the 25 income sources.

R3-G1 is addressed in the PR body. The test plan now says 74 / 49 and describes Case 43 at $77,946 / $77,947, Case 46's two tenants, Case 47's five fuels and Case 49. The Deductions row now carries the Part A double-count note. One optional sub-point remains: the code comment still calls the non-Part-B input "the non-Medicare component". The head did not change, so this was not expected.

N-G1 (UNSPECIFIED gate on the electric-bill path) was not taken up. It is still documented and still optional.

One new suggestion (R4-G1): two small wording fixes in #1902. Neither blocks.

CI: 33 of 33 checks pass, Microsimulation and all Contrib shards included.

Branch: 12 commits ahead of main and 0 behind. main has not moved since round 3, and the merge is clean.

No critical or should-address item is open.

Prior findings

Finding Status Evidence
A1 (should). rulespec-us#1902 was not updated for this head Fixed State: OPEN, labelled pe-parity, updated 2026-10-28 18:20:56 UTC. I checked at 19:10 UTC. It has no comments.
Head: "Head 5cea524422: review fixes 14a5193a94 and 705f8cec8d on 62c5fa449b, merged with current policyengine-us main." The commit history gives round 3 as 705f8cec8d (tests, comments) plus merge 5cea524422. This matches the branch.
Counts: "74 YAML cases in 4 files ... at 5cea524422 (nd_liheap.yaml 49, nd_liheap_countable_income.yaml 21, nd_liheap_household_size.yaml 2, nd_liheap_share.yaml 2)". A - name: count at the head gives 49 / 21 / 2 / 2.
Test 9(g): "as (e) with is_in_public_housing instead", where (e) is pension $15,000, heat in rent, NATURAL_GAS and pre_subsidy_electricity_expense $1,200. It expects eligible and $1,140, and it cites Case 46. This matches T/nd_liheap.yaml Case 46, household 2: age 40, pension $15,000, receives_housing_assistance: false, is_in_public_housing: true, NATURAL_GAS, heat in rent, $1,200 → 0.95, true, $1,140. Hand check: 15,000 is in the one-person 95% band (13,519-20,274), and 1,200 × 0.95 = 1,140.
Test 14, Case 43: pension $77,946, then $77,947 (four citizens aged 45, 45, 12 and 9; FUEL_OIL $3,000). Hand check: M = max(floor(floor(0.6 × 129,911) × 1.00) = 77,946; 1.5 × 32,150 = 48,225) = 77,946. 77,946 ≤ M, so 45% and $1,350. 77,947 > M and is not below 1.1 × 32,150 = 35,365, so $0. This matches Case 43.
Test 14, Case 49: two adults aged 45, pension $53,003, then $53,004, as Case 43. Hand check: M = max(floor(77,946 × 0.68) = floor(53,003.28) = 53,003; 1.5 × 21,150 = 31,725) = 53,003. $53,003 → 0.45 → $1,350. $53,004 > M and is not below 1.1 × 21,150 = 23,265 → $0. This matches Case 49.
Other test 14 rows: these are unchanged and still right. Size 10 = M = 112,242. At size 13, M = 1.5 × 81,650 = 122,475, which is above floor(77,946 × 1.53) = 119,257 and above the printed 119,262.
R3-G1 (suggestion). The PR body's test plan was stale Addressed (one optional sub-point open) Test plan: it now says "74 cases ... (nd_liheap.yaml 49, ... 21, ... 2, ... 2)". It describes Case 43 ("$77,946 of pension income pays $1,350 and $77,947 pays $0"), Case 49 ("$53,003 pays $1,350 and $53,004 pays $0"), Case 46 ("two heat-in-rent tenants, one receiving housing assistance and one in public housing ... $1,140") and Case 47 ("kerosene, wood, coal, solar and other renters ... $660").
Benefit example: it now uses $77,947 → $0 and $77,946 → $1,350.
Part A note: the Deductions row adds "Reported premiums other than Part B should exclude Part A, which would otherwise be deducted twice."
Still open (optional): V/nd_liheap_countable_income.py still calls health_insurance_premiums_without_medicare_part_b "the non-Medicare component". The body says "reported premiums other than Part B", and the "Not modeled" list says a Part D premium is reported there. A code change would rerun CI, so leave it for a later touch.
N-G1 (suggestion). UNSPECIFIED gate on the electric-bill path Not taken up; still documented Code: V/nd_liheap_eligible.py is unchanged: own_electric_bill = (pre_subsidy_electricity_expense > 0) & (heating_type != UNSPECIFIED). Case 48 pins it.
Body: the Heating fuel row and the "Not modeled" list both say UNSPECIFIED pays $0 "including on a subsidized heat-in-rent household's own electric bill".
#1902: interpretations 14 and 15 and companion test 9(f) say the same. See R4-G1 for the one place they do not.
Still optional.

New findings

Critical

None.

Should address

None.

Suggestions

  • R4-G1. Two wording fixes in rulespec-us#1902 (optional; either can be made with N-G1 or at merge):
    • Eligible row. The Required outputs row for nd_liheap_eligible says "not (heat in rent and subsidized) unless pre_subsidy_electricity_expense > 0". It omits the UNSPECIFIED condition that the code applies (own_electric_bill requires heating_type != UNSPECIFIED). Interpretation 15 and companion test 9(f) both carry the condition, so an encoder would meet it in the tests. The row alone, though, makes 9(f) eligible.
      • Fix: add "and heating_type is not UNSPECIFIED" after "> 0".
      • If N-G1 is adopted instead: drop the gate from the code and from interpretation 15 and test 9(f).
    • "Why queued" bullet. The "No reviewed oracle revision" bullet mentions only the two 2026-10-27 reviews and says "the fixes at 5cea524422 await re-review". Round 3 reviewed 5cea524422 on 2026-10-28 and asked only for this issue to be updated. This is a factual nit; the next update (the merged commit) can refresh it.

Verified

#1902 against the head. Beyond the A1 items, I spot-checked the issue's Required outputs, interpretations and companion tests against the code at 5cea524422:

  • Medicare netting.
    • Countable income row: "per member, health_insurance_premiums if positive, otherwise max(health_insurance_premiums_without_medicare_part_b, 0) + medicare_part_a_premium + medicare_part_b_premium (out of pocket, net of Medicare Savings Program coverage), plus max(pre_tax_health_insurance_premiums, 0)", then other medical, child support paid, alimony paid and child care.
    • Code: this matches V/nd_liheap_countable_income.py (where(direct_premiums > 0, direct_premiums, component_premiums) and the same add list).
    • Other sections: interpretation 11 and companion tests 12(f), (h) and (i) and 13(h) carry the computed Part A and B premiums net of MSP (QMB member → Part B 0 → $950; 12 × 311 = 3,732 Part A). Interpretation 11 now also says "A Part A premium in the non-Part-B input counts twice", which matches N-G3.
  • 8624(b)(2)(B) cap.
    • Share row: M = max(floor(floor(0.6 × four-person amount) × 96.85(b) factor), 1.5 × the lagged guideline), applied to every band, with the 45% floor below round(1.1 × guideline, 2).
    • Code: this matches V/nd_liheap_share.py (federal_maximum = max_(liheap_smi_limit(...), guideline * p_hhs.fpg_limit), min_(..., federal_maximum) per band, np.round(guideline * p_hhs.fpg_floor, 2)).
    • Parameter: gov.hhs.liheap.fpg_limit is 1.5 from 1981-10-01.
    • Helpers: smi and liheap_smi_limit are in variables/gov/hhs/hhs_smi.py, and fpg is in tax_unit_fpg.py, as the issue says.
  • Subsidized heat in rent.
    • Benefit row: "with heat in rent and subsidized, max(pre_subsidy_electricity_expense, 0) x share". Subsidized means receives_housing_assistance or is_in_public_housing.
    • Code: this matches V/nd_liheap.py: select([heat_in_rent & subsidized, heat_in_rent], [electric_payment, renter_payment], default=direct_payment).
    • Eligibility: apart from R4-G1, the Eligible row matches V/nd_liheap_eligible.py.
  • 25 income sources.
    • Earned (6): employment_income, self_employment_income, sstb_self_employment_income, farm_operations_income, partnership_income and military_basic_pay.
    • Unearned (19): social_security through estate_income, including total_unemployment_compensation and military_retirement_income.
    • Parameters: these match income/earned_sources.yaml and income/unearned_sources.yaml at the head (6 + 19 = 25). financial_assistance is handled separately at $360.
    • Companion test 13(j): it still gives each source a distinct amount: 16,060 + 16,850 = 32,910.
  • Parameter count: 11 files under gov.states.nd.dhs.liheap, as stated.

PR body. Its eligibility, countable-income and benefit descriptions are unchanged in substance from round 3 and still describe the code. The axiom line still reads axiom: TheAxiomFoundation/rulespec-us#1902 queued.

Code and tests. The head is unchanged, so round 3's verification of C1, S1-S3, N1 and N-G2 to N-G4 stands. That includes the mutation check showing that Case 46 household 2 fails if the public-housing term is removed from the payment selector.

Axiom

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

  • Status: OPEN, labelled pe-parity.
  • Dispatch-ready:
    • Module paths: proposed us-nd/ and us/ module paths, with corpus citation paths and status per row.
    • Law: verbatim text from the manual 415 sections, the FY2026 plan, the FY2026 workbook, 42 U.S.C. 8624(b)(2)(B) and 8622(11), 45 CFR 96.85, IM 2025-02 Attachment 4, 90 FR 5917, and 90 FR 52060 and 52063.
    • Outputs: seven required outputs, with entities, periods and definitions.
    • Tests: 14 companion tests derived by hand from those sources, none from policyengine-us output.
    • Interpretations: 17 interpretations, with source-conflict candidate ids.
    • Blockers: recorded under "Why queued": sources missing from the corpus, the signed release not checked, billed runs not approved and no reviewed oracle revision.
  • Current: it names head 5cea524422 with 74 cases (49 / 21 / 2 / 2). Test 9(g) and the test 14 Case 43 and Case 49 pairs match the head's YAML and my hand arithmetic.
  • Optional nits: see R4-G1.

CI

gh pr checks 10342 at head 5cea524422 (run 40708627242), checked at 19:10 UTC: 33 of 33 pass, 0 pending, 0 failing.

  • Newly passing: the nine checks that were pending in round 3 now pass. These are Contrib congress, other-shard-1, other-shard-2a, other-shard-2b, states-shard-1 to 4, and Microsimulation (50m34s).
  • ND files: Baseline states-shard-2 (which runs the ND files) and Quick Feedback still pass.
  • Known failure: Rest (Python shard 3) passed, so the known main-caused test_social_security_input_state_machine failure did not occur.
  • Failures: none to classify.

Branch

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 4: 0 critical, 0 should. rulespec-us#1902 now matches the head (74 cases, the public-housing electric-bill household and both cap edges), and CI is 33/33. Full report in the comment above.

@hua7450
hua7450 merged commit 92ce321 into main Oct 28, 2026
33 checks passed
@hua7450
hua7450 deleted the nd-liheap-wave2b branch October 28, 2026 19:18
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