Skip to content

Add South Dakota LIEAP regular heating assistance - #9974

Merged
hua7450 merged 16 commits into
mainfrom
sd-liheap
Oct 20, 2026
Merged

hua7450 merged 16 commits into
mainfrom
sd-liheap

Conversation

@hua7450

@hua7450 hua7450 commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Adds a partial South Dakota LIHEAP regular heating model, which the state runs as the Low Income Energy Assistance Program (LIEAP), using FY2024 through FY2026 schedules. Mixed-status households are eligible: only citizens and eligible noncitizens count toward household size. The model currently counts ineligible members' income in full, overstating it because the operative citizenship proration worksheet is unavailable. The model counts the household's income, tests it against the published limits or all-member SNAP receipt, assigns the county to one of four heating regions, and reads the benefit matrix by region, fuel and income tier. Natural gas, electricity, propane, fuel oil and kerosene awards go to the supplier, up to the household's unpaid heating charges; wood and coal pay the full amount to the household; heat included in rent pays up to 30% of seven months of tenant rent. In FY2026, a 70-year-old in Custer County who heats with natural gas and receives $32,000 a year of Social Security has $29,565.20 of countable income after the $2,434.80 Medicare Part B premium. That income is within the $31,300 one-person limit and above the $19,564 first-tier threshold, so the household receives the $792 region 1, tier 2 natural gas amount against $2,000 of unpaid gas charges. Counting gross Social Security would put the household over the limit, and it would receive $0.

Fixes #9965

No complete benefit year is verified: the published FY2024 and FY2026 benefit amounts are tested, while the citizenship income worksheet and the FY2026 "Mid" thresholds remain source gaps.

Regulatory authority

Eligibility

Requirement Source How modeled
South Dakota residence — defined_for = StateCode.SD
Household size and citizenship Manual pages 9 and 18; FY2026 plan section 17.3, page 34 Manual page 9: "Mixed households are eligible for assistance. Only citizens and eligible aliens are counted as actual HH members." sd_liheap_household_size counts members who meet is_citizen_or_legal_immigrant, with no SNAP-specific waiting periods or bars; the income limit and first-tier threshold use that size. A household needs at least one counted member.
Income limit ARSD 67:15:01:10; FY2026 plan section 2.1, page 9; FY2024 matrix pages 1–5; DSS income-limit pages The annual limit is four times the three-month limit. The rule takes the smaller of 200% of the prior year's poverty guideline and the larger of 150% of that guideline and 60% of state median income from the shared liheap_smi_limit helper (#10019), which floors the four-person amount and the size-scaled amount to the dollar, then floors the three-month amount to the dollar. It reproduces every printed FY2024 limit (sizes 1–15) and FY2026 limit (sizes 1–10); the rule is a reconciled pattern, not an express instruction. FY2025 uses the printed table for sizes 1–10. Larger households follow the rule. The limit is inclusive: $29,160 for one person in FY2024, $30,120 in FY2025 and $31,300 in FY2026.
Categorical income eligibility ARSD 67:15:01:01(1) and 67:15:01:10; FY2026 plan section 1.4a, page 5 The income test passes when every member receives SNAP: a positive January SNAP benefit and a January snap_unit_size equal to the number of SPM unit members, eligible or not. A benefit for only some members does not qualify.
Heating responsibility ARSD 67:15:01:09; manual page 32 A direct heating obligation (has_heating_expense) qualifies. Heat included in rent qualifies with positive tenant rent and no separate bill; zero tenant rent does not. heating_type NONE is ineligible.

Countable income

Item Source How modeled
Earned income ARSD 67:15:01:18(1) and 67:15:01:18.01; FY2026 plan section 1.9, page 6 Counts wages and net self-employment, SSTB, farm operations and partnership/S-corporation income, each floored at zero, with no further business deduction. Excludes the earnings of members under 18, or under 19 while in secondary school.
Unearned income FY2026 plan section 1.9, pages 6–7; ARSD 67:15:01:01(4) and 67:15:01:18(17); manual pages 25, 27–28; energy assistance application, page 5 Counts SSI, pensions, military retirement pay, railroad retirement, survivor benefits, unemployment (total_unemployment_compensation: the reported amount, or modeled state unemployment insurance when none is reported), workers' compensation, veterans benefits, child support received, alimony, rental and farm rental income, strike benefits, general assistance, estate and trust income (estate_income, the manual's "Trusts"), gambling winnings and regular cash help from family or friends (financial_assistance) for members of every age. Floors each source at zero, so a rental, farm-rent or trust loss offsets nothing. Excludes interest, dividends and capital gains.
Social Security ARSD 67:15:01:18(12); manual page 28; FY2026 plan section 1.9, page 6 ("Excluding MediCare deduction") Counts each person's Social Security minus medicare_part_b_premium, floored at zero. The model counts railroad retirement gross.
TANF Manual page 28 Counts the take-up-gated tanf aggregate, which includes sd_tanf.
Ineligible members' income Manual pages 17 and 26 Counted in full. The manual counts only a portion through its Citizenship Income Calculator, whose proration formula is unpublished and unclear: its one example turns $3,000 of eligible members' income and $4,000 of an ineligible member's income into $6,200, matching neither full counting ($7,000) nor SNAP pro rata ($6,000). Counting it in full overstates income for mixed-status households.
Child support paid ARSD 67:15:01:18(20); manual page 25 Subtracts child_support_expense and floors countable income at zero.

Benefit

Each cell lists tier 1 / tier 2 for FY2024 through FY2026.

Schedule Fuels mapped to it Region 1 Region 2 Region 3 Region 4
Natural gas Natural gas $880 / $792 $938 / $844 $1,007 / $907 $1,058 / $953
Electricity Electricity $1,331 / $1,198 $1,410 / $1,270 $1,516 / $1,369 $1,626 / $1,461
Propane Propane $2,499 / $2,249 $2,658 / $2,394 $2,834 / $2,552 $3,066 / $2,762
Fuel oil Fuel oil, kerosene $2,858 / $2,573 $3,018 / $2,715 $3,236 / $2,910 $3,437 / $3,093
Coal and wood Coal, wood $742 / $668 $770 / $694 $812 / $730 $854 / $768
Heat in rent (maximum) Any fuel except NONE $800 $800 $850 $850
  • Tier 1 applies when countable income is at or below the first-tier threshold ("Mid"): 125% of the prior year's guideline, with the three-month amount rounded half up to the dollar, times four. That rule reproduces every printed FY2022 and FY2024 threshold. The FY2026 matrix names Mid without printing it, so the model carries the rule forward: $19,564 for one person in FY2026.
  • Natural gas, electricity, propane, fuel oil and kerosene awards go to the supplier and pay the smaller of the cell and heating_expense floored at zero. Wood and coal pay the full cell to the household (plan item 9.1; ARSD 67:15:01:40).
  • Heat in rent pays the smaller of the regional maximum and 30% of monthly tenant rent for seven months, October through April; $100 a month of rent yields $210.
  • ARSD 67:15:01:35 assigns all 66 counties to the four regions; former Shannon County maps to Oglala Lakota County. An unknown or out-of-state county pays $0. With no county input, the shared county default assigns Aurora County (region 2), so callers should send county.
  • The matrices print the same cells for every household size; size enters only through the limit and the first-tier threshold.
  • The FY2022 and FY2023 matrices print lower cells for three fuels, regions 1–4, tier 1 / tier 2: electricity $1,210 / $1,089, $1,281 / $1,154, $1,378 / $1,244, $1,478 / $1,328; fuel oil $1,905 / $1,715, $2,012 / $1,810, $2,157 / $1,940, $2,291 / $2,062; propane $1,666 / $1,499, $1,772 / $1,596, $1,889 / $1,701, $2,044 / $1,841. The FY2022, FY2023, FY2024 and FY2026 matrices print the same coal and wood, natural gas and heat-in-rent amounts.
  • The PR adds no new inputs.

Years covered

Model year Program year Income limit One-person limit First-tier threshold Direct-payment cells
2024 FY2024, from October 1, 2023 Rule, 2023 guideline $29,160 $18,224 FY2024 matrix
2025 FY2025, from October 1, 2024 Printed table for sizes 1–10; rule above $30,120 $18,824 FY2024 matrix
2026 FY2026, from October 1, 2025 Rule, 2025 guideline $31,300 $19,564 FY2026 matrix

A calendar-year period reads the parameters in force on January 1. Formulas are undated. The programs.yaml entry, South Dakota LIEAP, has status partial and no verified_years; the table lists the schedules the model represents, and no year is verified.

  • FY2025 limits. The FY2025 plan, section 2.1 (page 8), lists 60% of state median income for sizes 1–9 and 150% of the guideline for size 10. The published 2024-2025 limits for sizes 1–3 instead equal 200% of the 2024 guideline, sizes 4 and 5 exceed that cap, and sizes 5–7 round differently. No single rule reproduces all ten, so income/limits/table/amount.yaml prints them and table/in_effect.yaml applies them from October 1, 2024 to September 30, 2025. For four people, the table gives $15,858 a quarter, or $63,432 a year; the rule would give $62,400.
  • FY2026 limits for sizes 4–6. The FY2026 plan, section 2.1 (page 9), sets 60% of state median income for sizes 4–9; applied to the $111,183 FY2026 median, that rate gives three-month limits of $16,677, $19,345 and $22,013 under the model's flooring. DSS publishes $16,075, $18,825 and $21,575, which equal 200% of the 2025 guideline. The model follows DSS. The plan's size 1–3 rates (54.14%, 55.95% and 57.07% of state median income) equal 200% of the 2025 guideline.
  • Direct-payment cells. payment/direct.yaml carries the FY2022 and FY2023 cells from October 1, 2021 and the FY2024 cells, which the FY2026 matrix repeats, from October 1, 2023. No FY2025 matrix is published; the FY2025 plan's $668 minimum and $3,437 maximum (section 2.6, page 9) match the FY2024 cells.
  • Earlier and later years. Region lists, income source lists and heat-in-rent amounts carry one October 2025 date and backfill to earlier years. Model years 2022 and 2023 use the FY2022 and FY2023 cells, but the income rule does not reproduce every printed FY2022 and FY2023 limit, which used different rounding, so those years stay unverified. The FY2021 plan, section 2.6, prints a $334 minimum benefit against $668 in every later plan, so years before 2022 are unverified backfilled estimates. The FY2027 draft plan (status "Saved") keeps the $668–$3,437 range (section 2.6, page 10) and lists 60% of state median income for sizes 4–10 (section 2.1, page 9); its size 1–3 rates (52.02%, 53.94% and 55.13%) match 200% of the 2026 guideline to within 0.01 point. The model carries the rule forward, so in model year 2027 sizes 4–6 follow the 200% cap ($66,000, $77,360 and $88,720), where the draft's 60% of the $118,003 median gives $70,800, $82,128 and $93,456. FY2027 is not claimed; sizes 4–6 need another check once DSS posts its 2026-2027 limits.

Not modeled and source conflicts

Item Source Model treatment
Proration of ineligible members' income Manual page 17 Not modeled; the income is counted in full (see Countable income).
Medicare premiums beyond Part B on Social Security Manual pages 25 and 28 The manual subtracts Part A and Part D premiums from pensions and Part B and Part D premiums from VA benefits and Social Security. The model counts pensions and VA benefits gross and has no Part D premium input.
Jury duty pay and long-term care insurance Manual page 27 Not counted (no input).
Overpayment recoveries withheld by other programs and expense reimbursements ARSD 67:15:01:18(4) and (21) Not separated from the counted sources.
Self-employment income from the latest tax return ARSD 67:15:01:10 and 67:15:01:18.01 Uses existing net business inputs.
Income in the three months before application ARSD 67:15:01:10 Annual inputs approximate it.
Seasonal unpaid charges FY2026 plan section 1.1, page 4; manual page 30 heating_expense is treated as unpaid charges from gas and electric meter reads between October 1 and May 15, or propane and fuel oil fills between July 1 and April 30. The model does not observe charge dates or balances.
Heat-in-rent months Manual pages 31–32 Assumes the same tenant rent in all seven months and a signed landlord agreement; rent changes and months already paid are not tracked.
Tribal service areas, institutional residents, duplicate awards and verification ARSD 67:15:01:08.01 and 67:15:01:39 The model assumes an independent, noninstitutional household that the state serves, with verification complete and no earlier award.
OTHER (including corn or pellet stoves), SOLAR and unspecified direct fuels Manual page 31; FY2026 matrix No matrix price; pay $0 as a coverage gap (#9754 for unspecified fuel).
Heat included in condominium association fees or hotel charges Manual page 30 No separate input; a caller can report a year of association fees or hotel charges as rent with heat_expense_included_in_rent, and the heat-in-rent payment then applies the 30% share.
Crisis, cooling, weatherization and furnace repair FY2026 plan section 1.1, page 4 Out of scope.

Where sources differ or are silent, the model reads them as follows:

Topic Other source Source followed and model
Gambling winnings FY2026 plan section 1.9 (page 6) leaves "One-time lump-sum payments, such as rebates/credits, winnings from lotteries" unchecked Manual page 27 counts lottery or gaming winnings in full; the model counts gambling_winnings.
Cash gifts FY2026 plan section 1.9 (page 6) leaves "Cash gifts" unchecked ARSD 67:15:01:18(17) excludes only "Cash gifts for recurring occasions such as birthdays and Christmas"; manual page 25 counts "Cash Gifts (Monthly)" listed for all three months, and the application (page 5) lists "Money from Family or Friends" as gross income. The model counts financial_assistance and reads an annual amount as regular help.
Kerosene payment method Manual page 31 (2018) pays wood, coal and kerosene households "the entire benefit amount" by check. Plan item 9.1 (FY2024 and FY2025 page 21, FY2026 and the FY2027 draft page 22) pays heating assistance directly to energy suppliers and lists the only exceptions: "If household uses coal or wood as it's primary heat source or a household's heat is included in rent, a payment is issued directly to the household." ARSD 67:15:01:40 names only wood or coal; ARSD 67:15:01:43.01 continues payment "to the energy supplier or landlord"; the application (page 4) asks for a supplier fill ticket for "Fuel Oil/Kerosene" and pays only "Heat in Rent, Wood, or Coal" to the household; the DSS page says "the payment is made directly to the energy supplier." The model follows the plan and rules: kerosene takes the fuel oil cell, capped at unpaid charges like fuel oil.
Categorical eligibility Manual page 12 also covers members whose income SNAP considered, Medicare Savings Program households and SSI recipients ARSD 67:15:01:01(1) and FY2026 plan section 1.4a (page 5) require every application member to receive SNAP; the model follows them.
Categorical benefit level Manual page 13: "The reported income counted by the SNAP case in the month of LIEAP application will be multiplied by 3 to determine the benefit level." The model sets the tier from its own countable income.
Self-employment and rental losses ARSD 67:15:01:18.01 counts "any net profit or loss" and treats a self-employment loss as zero, without saying whether a member's businesses or rental income are netted first The model floors each source at zero, so a loss in one source does not offset a gain in another.

Microsimulation: the microsimulation datasets carry no heating inputs, so population totals are about $0 until #9754 lands. Callers must send heating_type with the matching fuel bill.

Test plan

78 YAML cases in 10 files cover:

  • all 40 FY2026 direct-payment cells, nine FY2023 cells, kerosene on the fuel oil schedule, and coal and wood on one schedule;
  • FY2024 limits and first-tier thresholds for sizes 1–15, the printed FY2025 limits for sizes 1–10 plus the size-11 rule, FY2026 limits for sizes 1–15, and FY2026 thresholds;
  • inclusive limits and thresholds at the dollar and one cent above, including the FY2025 four-person limit of $63,432 and quarter rounding against the printed annual column;
  • all 66 county assignments including former Shannon County, and unknown or foreign counties;
  • SNAP categorical eligibility for all members and for only some, permanent residents and refugees (each paid the $938 region 2 gas amount), an undocumented member and another state;
  • household size counting only citizens and eligible noncitizens (sizes 3, 2 and 0), a mixed-status household with an undocumented adult that is eligible at size 1 ($880), and one whose undocumented member's $9,000 of wages counts in full (size 2, $27,000, tier 2, $792);
  • heating responsibility through positive rent with no bill; heat-in-rent maximums, the $210 cap on $100 a month of rent, rent summed across members, a subsidized positive share, a zero share, unspecified fuel accepted and NONE rejected;
  • the earnings exclusions at ages 17, 18 and 19 by school status, business loss floors, unearned income at every age, workers' compensation, excluded interest, dividends and capital gains, counted regular cash help from family ($12,000 of wages plus $2,400 of help counts $14,400), unemployment insurance alongside wages ($20,800 of wages plus $6,400 of modeled unemployment insurance with no reported amount counts $27,200; reading the raw input would give $20,800), SSI and TANF, child support paid and its floor, railroad retirement, Social Security net of the Part B premium and a premium larger than the benefit, military retirement pay, trust income and a trust loss, rental and farm-rent losses, gambling winnings, and TANF with and without take-up;
  • five end-to-end households in integration.yaml, four of which carry a fuel bill through heating_expense: gas in Minnehaha County ($1,007), kerosene in Brown County paid to the supplier and capped at $900 of fill tickets, wood in Harding County paid in full ($854 on a $500 bill) and propane in Lincoln County with a teenager's excluded earnings ($1,800); the fifth has heat in rent in Pennington County ($800);
  • final awards for vendor caps, full cash payments for wood and coal, kerosene capped at a $1 bill, a tier change, negative bills, unpriced fuels, the FY2025 four-person household, 2023 and 2024 matrix cells, a retiree with the Part B premium, military retirement pay above the limit, a retiree's rental loss and $6,000 a year of regular help from family that puts $27,000 of wages over the limit ($0).

Every case specifies heating_type except the four sd_liheap_household_size cases; that variable does not read it. Expected values come from hand arithmetic on the sources. The 72 cases at cd74fd52d6 passed CI; the new and changed cases since then were not run locally, and CI validates them. Cases for citizenship income and FY2026 Mid thresholds exercise the documented assumptions, not independently verified policy rules.

  • YAML tests whose expected values come from an external source (statute, official form or worksheet, official calculator)
  • make format
  • Changelog fragment in changelog.d/

axiom: TheAxiomFoundation/rulespec-us#1539 queued

🤖 Generated with Claude Code

hua7450 and others added 5 commits October 3, 2026 19:49
- Encode the published FY2025 three-month income limits (sizes 1-10) as a
  dated table, since no single rule reproduces them, while FY2024 and
  FY2026 keep the rule-based limits.
- Date the direct-payment matrix from October 2021 (FY2022/FY2023 cells)
  and October 2023 (FY2024-FY2026 cells).
- Count Social Security net of the Medicare Part B premium (ARSD
  67:15:01:18(12)), military retirement pay, trust income (manual p25)
  and gambling winnings (manual p27, followed over the plan); floor each
  unearned source at zero.
- Make mixed-status households eligible: only citizens and eligible aliens
  count toward household size (manual p9, p18), and ineligible members'
  income counts in full because the manual's citizenship income
  calculator does not publish its proration formula.
- Count TANF through the take-up-gated tanf variable instead of sd_tanf.
- Put SD on the LIHEAP coverage line; verified_years 2024-2026; short note.
- Add and update tests for each change.

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

codecov Bot commented Oct 7, 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 (f88aa84).
⚠️ Report is 541 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9974   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         9    +5     
  Lines           76       168   +92     
  Branches         2         3    +1     
=========================================
+ Hits            76       168   +92     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

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

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

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

hua7450 and others added 6 commits October 7, 2026 22:56
Add workers_compensation to the unearned sources (EA-297 application page 5; ARSD 67:15:01:01(4)) and document the FY2026 plan-versus-DSS income-limit conflict for sizes 4-6.

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
One reference entry per source; single-page citations keep the page only in the href; multi-page titles end with a quoted #page list and the href opens the first cited page; variable references use '# PDF pages ...' comments above multi-page hrefs (CLAUDE.md, PolicyEngine/policyengine-us PR 10009). Reference lines only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calls liheap_smi_limit from PR 10019 instead of repeating the HHS
floor-then-floor calculation. Limits are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DTrim99

DTrim99 commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Add South Dakota LIEAP regular heating assistance

PR #9974: Add South Dakota LIEAP regular heating assistance (author: hua7450, head ee24e71ea5)

Scope: South Dakota's Low Income Energy Assistance Program (LIEAP), the state's LIHEAP regular heating benefit, run by the DSS Office of Energy Assistance, with the FY2024-FY2026 schedules. The PR covers:

  • household size, which counts citizens and qualified noncitizens (mixed-status households are eligible), and countable income: gross earnings of members 18 and over (19 for high school students), each member's unearned income with every source floored at zero, Social Security net of the Medicare Part B premium, take-up-gated TANF, less child support paid;
  • the income limit: an inferred rule, the lesser of 200% of the prior year's poverty guideline and the greater of 150% of it and 60% of state median income (SMI), floored to the quarter, plus the printed FY2025 table for sizes 1-10; and categorical income eligibility when every member receives SNAP;
  • the benefit matrix: five fuel rows by four county regions by two income tiers, with the tier break ("Mid") at 125% of the prior year's guideline;
  • vendor payments up to the household's heating bill, the full award paid to wood, coal and kerosene households, and heat-in-rent payments of 30% of monthly rent for seven months, up to $800 or $850.

Crisis (ECIP), weatherization, furnace repair and tribal service areas are out of scope. The PR changes 39 files: 19 parameters, 9 variables, 9 test files (71 cases), programs.yaml and a changelog fragment. The direct-payment cells are dated 2021-10-01 and 2023-10-01; the limit and tier rule parameters 2024-01-01; the FY2025 table 2024-10-01, switched on by table/in_effect for that year only; everything else 2025-10-01. The tests use periods 2023-2026, and the registry lists South Dakota as partial with no verified_years.

Overall: the policy values are right where the sources print them.

  • All 40 FY2026 direct-payment cells and the four heat-in-rent amounts match the FY2026 matrix, and the FY2024 matrix prints the same cells for every household size. The 24 lower FY2022/FY2023 cells match those matrices.
  • The inferred limit rule reproduces all 15 printed FY2024 limits and all 10 published FY2026 limits. The printed FY2025 table matches figures that each reproduce from HHS's FFY2025 60% SMI amounts and the 2024 guideline. The Mid rule reproduces all 15 printed FY2024 three-month thresholds, and sizes 1-10 for FY2022 and FY2023.
  • All 66 county assignments match ARSD 67:15:01:35 and the manual's region map.
  • The income rules follow ARSD 67:15:01:18 and the manual's income chart. Social Security is counted net of the Medicare premium, which ARSD 67:15:01:18(12) requires and plan item 1.9 confirms ("Excluding MediCare deduction" checked).
  • Every one of the 21 listed income sources is nonzero in at least one case, and each of 16 mutations of the eligibility, income, limit, tier and payment logic breaks at least one case. This is the first PR in the batch with no untested logic.

There are no critical findings. The four should-address items:

  • S1: a merge blocker from main's Integrate modeled state unemployment insurance into income flows #9558. The unearned list reads the raw unemployment_compensation, so main's code-health guard fails on the merged tree. South Dakota has no modeled unemployment insurance yet, so no value changes today.
  • S2: regular cash help from family or friends (financial_assistance) is excluded. ARSD 67:15:01:18 excludes only gifts for recurring occasions, and the manual and the current application count regular help; only plan item 1.9's unchecked "Cash gifts" box supports the PR. Reproduced: $792 at head, $0 with the help counted.
  • S3: twelve parameter descriptions fall outside the house template, and five of them continue with sentences after the program name.
  • S4: the eligibility variable links the 49-page FY2026 plan without a page, two variables list incomplete pages, and the rate parameters do not cite plan item 2.1, which prints the rates.

All 71 cases pass locally at the head and on the merged tree. The model outputs quoted below were reproduced by running the head code, or the merged tree for S1. Values that a proposed fix or another reading would produce are hand computations and are labelled as such. The mutation results come from mutating the code and rerunning the PR's tests.

Path prefixes used below:

  • P/ = policyengine_us/parameters/gov/states/sd/dss/liheap/
  • V/ = policyengine_us/variables/gov/states/sd/dss/liheap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/
  • M = LIEAP Policy and Procedures Manual (60 pp, the Clearinghouse's 2018 copy; PDF pages cited)

Line numbers refer to head ee24e71ea5.

Source Documents

# Document Link Use
1 FY2026 South Dakota LIHEAP State Plan (49 pp, report period 10/01/2025-09/30/2026, "Submission Accepted by CO") SD_Plan_2026.pdf 1.1 dates and claim windows (p4); 1.4 Yes (SNAP Yes for heating; TANF and SSI No), 1.4a, 1.5 (p5); 1.8 Gross Income, 1.9 (p6-7); 2.1 thresholds, 2.3 (p9); 2.5, 2.6 $668 / $3,437 (p10); 17.3 (p34). The text layer lacks the checkbox states, so the rendered pages were read
2 FY2025 plan (47 pp, Revision #1) SD_Plan_2025.pdf 1.1 dates (p4); 2.1 60% SMI for sizes 1-9 and 150% of the guideline for size 10 (p8); 2.6 $668 / $3,437 (p9)
3 FY2021-FY2024 plans (accepted) FY2021, FY2022, FY2023, FY2024 2.6 ranges (p9): $334 / $2,291 (FY2021); $668 / $2,291 (FY2022, FY2023); $668 / $3,437 (FY2024)
4 Draft FY2027 plan (48 pp, report period 10/01/2026-09/30/2027, status "Saved") DSS hearing copy 2.1 sizes 1-3 at 52.02%, 53.94% and 55.13% of SMI, sizes 4-10 at 60% (p9); 2.6 $668 / $3,437 (p10). No IIJA text
5 FY2026 Benefit Matrix (1 p) SD_BenefitMatrix_2026.pdf "Own and Rent Cases" (five fuel rows, Reg 1-4, "Income Tier 1 0-Mid" and "Income Tier 2 Mid-Max") and "Heat in Rent Cases"
6 FY2024, FY2023 and FY2022 Benefit Matrices (5 pp each) FY2024, FY2023, FY2022 Cells and "Maximum Income" (Annual, 3-Month), both tiers, sizes 1-15
7 M (60 pp; chapter pages marked "Rev. 8/2012") M Eligibility (p2); household (p3); citizenship (p7-9); time frame and categorical eligibility (p12-13); ineligible members (p17-18); income chart (p24-28); payment (p30-31); heat in rent (p30-32); region map (p52)
8 Application EA-297 (08/25, 6 pp) EA-297 Claim windows and "already been paid" (p2); heating-source categories and the fuels paid to the household (p4); gross income list (p5)
9 DSS Energy and Weatherization Assistance page (read 2026-10-14) dss.sd.gov "2025-2026 Heating Season Income Limits" ($7,825 ... $24,431); "If eligible, the payment is made directly to the energy supplier." No 2026-2027 table yet. The January 14, 2025 capture the PR cites for 2024-2025 could not be fetched (the Wayback Machine blocks automated requests), so its ten values were checked against item 10 and the 2024 guideline
10 HHS LIHEAP IM 2024-02 and IM 2025-02, Attachment 4 FFY2025, FFY2026 South Dakota four-person SMI $105,723 and $111,183 and the 60% amounts for sizes 1-6 (p3) and 7-12 (p6)
11 ARSD chapter 67:15:01 67:15:01 :01(1) categorical eligibility and (4) gross income; :09; :10; :11; :18 exclusions; :18.01; :20; :35 regions; :39 tribal areas; :40 wood and coal; :41-:41.02 heat in rent; :43.01 payment limits

Critical (Must Fix)

None.

Should Address

S1. Merge blocker: after merging main, the unearned list reads the reported-only unemployment_compensation, so main's guard fails

  • Where: P/income/sources/unearned.yaml:14 (- unemployment_compensation).
  • What changed on main: Integrate modeled state unemployment insurance into income flows #9558 (merged 2026-10-13, after this PR's last push on 2026-10-11) added total_unemployment_compensation, which uses the reported amount when present and otherwise the modeled state unemployment insurance programs. It moved every income-source list under parameters/ to the total, including the merged LIHEAP lists (for example nj/dca/liheap/income/sources/unearned.yaml:4), and added policyengine_us/tests/code_health/test_unemployment_compensation_readers.py, which fails on any raw list entry.
  • Reproduced on the git merge-tree result of main 8ae01aa3cb and this head: test_parameter_source_lists_do_not_read_raw_unemployment_compensation fails with one violation, parameters/gov/states/sd/dss/liheap/income/sources/unearned.yaml:14. The file's other three tests pass, and the PR's 71 cases still pass on that tree.
  • Source: plan item 1.9, p. 6 checks "Unemployment insurance"; M p28: "All UI payments received are countable."; EA-297 p5 lists "*Unemployment" as gross income. None separates reported from modeled amounts.
  • Impact: main models unemployment insurance for AL, NJ, NY, OK, PA and UT only (gov.states.household.modeled_state_unemployment_compensation), so for South Dakota households the total equals the reported amount and no value changes today. The guard still blocks the merge, and the raw entry would silently drop South Dakota UI once it is modeled.
  • Why should, not critical: the head is correct against its base and CI is green. git merge-tree shows a clean textual merge, but this has to change before merge (as Add Alabama LIHEAP regular heating assistance #9967 S1, Add Rhode Island LIHEAP regular heating assistance #9972 S1, Add Georgia LIHEAP regular heating assistance #9968 S1 and Add Ohio HEAP regular heating assistance #9975 S1).
  • Fix: replace the entry with total_unemployment_compensation. T/income/sd_liheap_countable_income.yaml Case 7 reports $1,200 of unemployment compensation, so it keeps covering the entry. Update rulespec-us#1539's required-outputs list, which names unemployment_compensation.

S2. Regular cash help from family is excluded, although ARSD 67:15:01:18 excludes only occasion gifts and the manual and application count it

  • Where: P/income/sources/unearned.yaml leaves out financial_assistance ("Cash financial assistance from friends or relatives outside the household"), and T/income/sd_liheap_countable_income.yaml Case 8 pins its exclusion (line 239: "Current planpp6-7 excludes ... gifts").
  • Source:
    • ARSD 67:15:01:01(4) defines gross income as "earned and unearned income before deductions", and ARSD 67:15:01:18 lists the exclusions. For gifts it excludes only "(17) Cash gifts for recurring occasions such as birthdays and Christmas".
    • M p25 counts "Cash Gifts (Monthly)": "If applicant lists gifts for all three months on the application, that will count toward the households income" (Y). One-time gifts are N.
    • EA-297 p5 lists "*Money from Family or Friends" among the gross income to report.
    • The PR's reading rests on plan item 1.9 (p. 6, rendered), which leaves "Cash gifts" unchecked.
  • Impact (reproduced at head): one adult with $27,000 of wages and $6,000 of financial_assistance, Custer County gas, counts $27,000, is eligible and gets $792. Counting the $6,000 gives $33,000, above the $31,300 limit, so $0 (reproduced with $33,000 of wages).
  • The PR follows the manual over plan 1.9 for gambling winnings, but the plan over the rule and the manual here. Its body lists "Recurring monthly cash gifts" as "Not counted" under "Not modeled", although the input exists and Case 8 asserts the exclusion.
  • Why should, not critical: plan item 1.9, an accepted state submission, supports the exclusion, so this is the plan-versus-rule class of Add Oklahoma LIHEAP regular heating assistance #9970 S2.
  • Fix: add financial_assistance to the unearned list (an annual input of regular help is not an occasion gift) and change Case 8's expectation to $14,400 ($12,000 of wages plus $2,400 of help), with a conflict-table row. Or, if DSS confirms it follows the plan box, keep the exclusion and correct the "not modeled" wording and the test comment.

S3. Twelve parameter descriptions fall outside the house template, five with sentences after the program name

All 19 descriptions use South Dakota's own program name, "Low Income Energy Assistance Program", which is fine (Ohio's review accepted "Home Energy Assistance Program", as merged New York HEAP uses). The template's verbs are limits, provides, sets, excludes, deducts and uses, and nothing may follow the program name (#9745 S6). The explanatory sentences can move into YAML comments, as the PR already does elsewhere.

File:line Problem Suggested
P/income/fpg_year_lag.yaml:1 "...for the Low Income Energy Assistance Program. The date records the FY2024 schedule in the annual model, not policy commencement; earlier backfilled years are unverified." ("for", trailing sentence) "South Dakota uses poverty guidelines from this many years before the heating year under the Low Income Energy Assistance Program."
P/income/limits/fpg_lower_rate.yaml:1 "...under the Low Income Energy Assistance Program. This numerical rule reproduces the FY2024 schedule..." (trailing sentence) "South Dakota uses this poverty guideline rate as the lower bound of its income limit under the Low Income Energy Assistance Program."
P/income/limits/fpg_upper_rate.yaml:1 "caps the income limit at this federal poverty guideline rate ... This numerical rule..." (verb, trailing sentence) "South Dakota limits household income to at most this share of the poverty guideline under the Low Income Energy Assistance Program."
P/income/limits/smi_rate.yaml:1 "applies this rate to four-person state median income ... Flooring before size adjustment ... The date is an FY2024 annual-model observation..." (verb, two trailing sentences) "South Dakota limits household income to this share of state median income, within the poverty guideline bounds, under the Low Income Energy Assistance Program."
P/payment/income_threshold_rate.yaml:1 "...under the Low Income Energy Assistance Program. Rounding its three-month equivalent half up reproduces ... The date is the latest explicit FY2024..." (two trailing sentences) "South Dakota uses this poverty guideline rate as the first income tier limit under the Low Income Energy Assistance Program."
P/income/sources/earned.yaml:1 "counts these earned income sources subject to its age and school exclusions" "South Dakota uses these sources as earned income under the Low Income Energy Assistance Program."
P/income/sources/unearned.yaml:1 "counts these unearned income sources for members of all ages" "South Dakota uses these sources as unearned income for members of all ages under the ..."
P/payment/heat_in_rent/months.yaml:1 "covers this many months of tenant-paid heat in rent, from October through April" "South Dakota provides heat-in-rent payments for this many months, October through April, under the ..."
P/payment/regions/region_1.yaml:1 to region_4.yaml:1 (4 files) "assigns these counties to heating region N" "South Dakota uses these counties as heating region N under the ..."

The other seven fit: earned_income_min_age and secondary_school_age_limit (excludes), table/amount and heat_in_rent/rent_share (limits), table/in_effect (uses), direct and heat_in_rent/amount (provides). Consistency: #9967 S4, #9972 S4, #9968 S3, #9975 S5 and #9745 S6.

S4. References: the 49-page plan linked without a page, two incomplete page lists, and the rate parameters do not cite the plan item that prints the rates

File:line Problem Fix
V/eligibility/sd_liheap_eligible.py:13-14 The FY2026 plan link has no anchor, so it opens the cover page. The comment names sections 1.4a and 17.3, which are on p. 5 and p. 34 #page=5, with # PDF pages 5, 34 above it
V/income/sd_liheap_countable_income.py:14 #page=6 for item 1.9, which runs onto p. 7, where the counted "Veterans Administration (VA) benefits" box and the unchecked insurance and child-earnings boxes sit. There is no # PDF pages line # PDF pages 6-7
V/payment/sd_liheap_heat_in_rent_payment.py:14-15 # PDF pages 31-32 leaves out M p30, which states the payment the formula caps: "a LIEAP payment of up to 30% of the monthly rent is paid to the client for unpaid rental obligations until the benefit is exhausted or the heating season ends" #page=30, # PDF pages 30-32
P/income/limits/smi_rate.yaml:8-12, fpg_lower_rate.yaml:8-12, fpg_upper_rate.yaml:8-12 Each cites the FY2024 matrix's dollar limits and the live DSS page. Neither prints a rate. Plan item 2.1 does: "State Median Income ... 60.00%" for sizes 4-9 and "HHS Poverty Guidelines 150.00%" for size 10 (FY2026, p. 9; FY2025, p. 8), and its size 1-3 entries (54.14%, 55.95%, 57.07% of SMI) equal 200% of the 2025 guideline ($31,300, $42,300, $53,300). The DSS page is overwritten each season, so the "FY2026" reference will stop showing FY2026 figures once DSS posts 2026-2027 Add the two plan references (p9 and p8), note the 54.14% equivalence in fpg_upper_rate.yaml, and cite an archived copy of the 2025-2026 DSS table

Consistency: #9972 S5 and #9975 S6 rated missing anchors and incomplete page lists SHOULD.

Suggestions

G1. Kerosene is paid the full award, but the current application pays only heat-in-rent, wood and coal awards to the household

  • V/sd_liheap.py:24-28 pays KEROSENE the full Fuel Oil cell whatever the bill, citing M p31: "LIEAP eligible households whose primary heat source is wood, coal, kerosene and other alternative heat sources, such as corn or pellet stoves, will receive a check directly to them for the entire benefit amount."
  • The current application (EA-297 p4, 08/25) lists "Fuel Oil/Kerosene - Include a fill ticket, monthly statement or a note from your supplier" and says "The payment for Heat in Rent, Wood, or Coal will be either direct deposited or put on an EPC card for you." ARSD 67:15:01:40 names wood and coal for direct payment without barring other fuels, and the DSS page says "the payment is made directly to the energy supplier".
  • Reproduced at head: with $1,500 of unpaid charges and no income in Custer County, fuel oil gets $1,500 and kerosene $2,858.
  • Severity: a suggestion, because the rule does not address kerosene, the DSS manual (p31) expressly pays it the entire benefit by check, and the PR documents the conflict in its conflict table, so the reading is genuinely ambiguous rather than contrary to the rule.
  • The current application points to the vendor cap, as for fuel oil. Either switch to it, or cite the application's wording beside the manual in the variable comment.

G2. Self-employment and rental results may net before the zero floor

  • ARSD 67:15:01:18.01: "the department shall include depreciation and any net profit or loss" and "shall consider that the income earned through self-employment is zero if the self-employment income calculations result in a loss". M p25 computes rental income on "the self-employment rental sheet", from the tax return (ARSD 67:15:01:10).
  • V/income/sd_liheap_countable_income.py:31-37 floors each source separately, so a business gain and a rental loss count the gain in full. Reproduced at head: $15,000 of wages, $6,000 of self-employment income and a $6,000 rental loss count $21,000, above the $19,564 Mid, so tier 2 and $792 for gas. Netting the two gives $15,000, tier 1 and $880 (hand-computed; reproduced with $15,000 of wages alone). Wages are never offset under either reading. Main's Offset SSTB and non-SSTB Schedule C results in federal AGI #9995 now offsets SSTB and non-SSTB Schedule C results in federal AGI.
  • Severity: a suggestion, because ARSD 67:15:01:18.01 does not say whether a member's businesses, or rental income (its own row on M p25, from its own sheet), are netted before the zero floor, and the PR documents its per-source reading in the code comment and the PR body; Add Ohio HEAP regular heating assistance #9975 G4 rated the same point a suggestion.
  • The per-source reading is conservative. Either net a member's self_employment_income, sstb_self_employment_income, farm_operations_income, partnership_s_corp_income, rental_income and farm_rent_income before one floor, or add the reading to the PR body's conflict table (as Add Ohio HEAP regular heating assistance #9975 G4).

G3. Ineligible members' income counted in full (documented)

  • The PR's description of M p17 is accurate: the "Citizenship Income Calculator" example (4 members, 3 eligible, $3,000 and $4,000, adjusted income $6,200) states no formula, and p26 ("using the following formula") gives none either.
  • Reproduced at head: a citizen adult with $30,000 of wages, an undocumented adult with $24,000 and two citizen children (Custer County, gas) have size 3 and count $54,000, above the $53,300 limit, so they get $0. Counting the undocumented adult's wages pro rata (3 of 4 members, $18,000) gives $48,000, eligible at tier 2 for $792; the manual example's ratio ($3,200 of $4,000) gives $49,200, also $792 (both hand-computed).
  • Full counting is $800 above the manual's example, and SNAP-style pro rata ($6,000) is $200 below it. Keeping the conservative reading is defensible; if it stays, the registry note already names the gap. Consider the pro rata reading or ask DSS for the worksheet formula.

G4. Backfilled years and FY2027

  • FY2022 and FY2023 (model years 2022 and 2023, unclaimed): the cells match, and the Mid rule reproduces every printed three-month Mid for sizes 1-10. The limit rule differs from the printed three-month limits by $1 a quarter ($4 a year) for sizes 5-7, 9 and 10 in FY2022 and sizes 5-9 in FY2023, where those years rounded instead of flooring (FY2023 size 7: 73,938 / 4 = 18,484.50, printed 18,485; reproduced period 2023: 73,936). The PR documents this.
  • FY2021 and earlier: the 2021-10-01 cells carry back, but the FY2021 plan, p. 9 prints "Minimum Benefit $334", against $668 in every later plan, so the FY2021 schedule differed at least at the low end. Unclaimed and documented as "unverified backfilled estimates"; record the $334 in the parameter comment or gate pre-FY2022 payments.
  • FY2027: the draft plan (status "Saved") keeps $668 / $3,437 (p10). Its item 2.1 (p9) prints 52.02%, 53.94% and 55.13% of SMI for sizes 1-3, which equal 200% of the 2026 guideline, and 60% of SMI for sizes 4-10. Reproduced at head, period 2027: limits $31,920, $43,280, $54,640, $66,000, $77,360, $88,720, $95,580, $97,704, $99,828, $101,952. Sizes 1-3 and 7-10 match the draft (size 10 switches to 60% of SMI, as the rule does). Sizes 4-6 follow the 200% cap, while the draft's 60% of SMI ($118,003 four-person median) gives $70,800, $82,128 and $93,456 (hand-computed, quarter-floored). That is the same plan-versus-DSS split as FY2026, where DSS published the 200% figures, so revisit when DSS posts the 2026-2027 table (the page still shows 2025-2026 on 2026-10-14). 2027 is not claimed.

G5. Fuels and locations that are eligible but paid $0

  • Reproduced at head (period 2026, no income, Custer County): SOLAR with an electric bill and OTHER with an other-fuel bill are sd_liheap_eligible: true with sd_liheap: 0, and so is an explicit county: UNKNOWN (region 0). T/sd_liheap.yaml Cases 5 and 6 pin these zeros.
  • M p31 pays "other alternative heat sources, such as corn or pellet stoves" "the entire benefit amount", and heating_expense treats SOLAR homes as grid-tied with electric heat, so the Coal & Wood and Electric rows are natural columns. Either pay those, or make the eligibility flag match the $0. Add a # See #9754 note for UNSPECIFIED (Add New Jersey LIHEAP regular heating assistance #9745 G1, Add Alabama LIHEAP regular heating assistance #9967 G2, Add Georgia LIHEAP regular heating assistance #9968 G2, Add Ohio HEAP regular heating assistance #9975 G3).
  • With no county input, the shared formula assigns Aurora County, so the household is priced as region 2 (reproduced: one adult, no income, gas bill: $938). V/payment/sd_liheap_region.py:18-19 says so in a comment; merged New Jersey and Oregon state their first-county fallback in the variable's documentation, which would help callers here.

G6. The base payment's size gate reads spm_unit_size

V/payment/sd_liheap_base_payment.py:49-50 returns 0 when spm_unit_size is 0, which no real SPM unit is, while the limit and tier read sd_liheap_household_size. Only T/payment/sd_liheap_base_payment.yaml Case 5's seventh unit, which sets spm_unit_size: 0 directly, reaches it. Eligibility already requires a counted member, so read sd_liheap_household_size or drop the gate.

G7. PR body, changelog and issue housekeeping

  • The changelog says "regular heating estimates for supported wholly qualified households", but the PR now models mixed-status households (Case 13 pays one).
  • The body says "this PR remains a draft"; the PR is open and ready for review.
  • "Every case specifies heating_type": the four T/eligibility/sd_liheap_household_size.yaml cases do not (harmless, since that variable does not read it).
  • Add conflict-table rows for cash gifts (S2) and for M p13, which sets a categorically eligible household's benefit level from "The reported income counted by the SNAP case ... multiplied by 3"; the model uses its own countable income (rulespec-us#1539 interpretation 8 records it).
  • The registry entry is named "South Dakota LIHEAP" ("South Dakota Low Income Home Energy Assistance Program"), while the labels and descriptions say LIEAP. Merged New York uses its program's own name ("New York HEAP"), which would fit here.
  • rulespec-us#1539 still calls the PR "open as a draft" and says "The tests were not run locally; CI validates them". All 71 pass locally at this head.

G8. Test and comment hygiene

  • About 25 comment lines lost the spaces around their numbers, for example:
    • T/eligibility/sd_liheap_income_limit.yaml:3 ("labels2025-2026season and prints quarterly7825,10575,..."), lines 15 and 28;
    • T/income/sd_liheap_countable_income.yaml:3 ("approximation:12000+600+600+1200+600=15000"), lines 33, 62, 97, 132, 167, 239, 301 and 328;
    • T/payment/sd_liheap_base_payment.yaml:3 ("FY2026matrix physicalp1:5fuelgroups x2income tiers x4regions=40literalcells"), lines 26, 39 and 54;
    • T/payment/sd_liheap_heat_in_rent_payment.yaml:3, 18, 45, 84, 127 and 184; T/payment/sd_liheap_income_threshold.yaml:3 and 16; T/payment/sd_liheap_region.yaml:3 and 41.
  • Comments refer to internal decisions: "approved net-input approximation" (T/income/sd_liheap_countable_income.yaml:3), "the approved eligible-unpaid-expense assumption" (T/sd_liheap.yaml:2), "Approved input assumption" (V/sd_liheap.py:29) and "the approved assumption" (V/payment/sd_liheap_base_payment.py:45), as in Add Rhode Island LIHEAP regular heating assistance #9972 G7. T/payment/sd_liheap_region.yaml:3 cites "sources/sd_county_regions.json", which is not in the repo.
  • T/income/sd_liheap_countable_income.yaml:1 and T/payment/sd_liheap_region.yaml:1 open with "supported income region and heat-in-rent components only", copied across files.
  • The isolated limit, threshold and base-payment tests set heating_type that those variables never read.

Verified correct

  • Matrix (FY2026, rendered):
    • Tier 1 / tier 2 by region 1-4: Coal & Wood 742 / 770 / 812 / 854 and 668 / 694 / 730 / 768; Electric 1331 / 1410 / 1516 / 1626 and 1198 / 1270 / 1369 / 1461; Fuel Oil 2858 / 3018 / 3236 / 3437 and 2573 / 2715 / 2910 / 3093; Natural Gas 880 / 938 / 1007 / 1058 and 792 / 844 / 907 / 953; Propane 2499 / 2658 / 2834 / 3066 and 2249 / 2394 / 2552 / 2762. Heat in rent 800 / 800 / 850 / 850 in every row and tier.
    • The FY2024 matrix prints the same cells for sizes 1-15. The FY2023 and FY2022 matrices print Electric 1210 / 1281 / 1378 / 1478 and 1089 / 1154 / 1244 / 1328, Fuel Oil 1905 / 2012 / 2157 / 2291 and 1715 / 1810 / 1940 / 2062, Propane 1666 / 1772 / 1889 / 2044 and 1499 / 1596 / 1701 / 1841, and the same other rows, as the 2021-10-01 entries hold.
    • Plan 2.6 matches the cells: $668-$2,291 in FY2022-FY2023 and $668-$3,437 in FY2024-FY2026 and the FY2027 draft. No FY2025 matrix is published; carrying the FY2024 cells fits FY2025's identical range.
    • Coal and wood share a row; kerosene is priced from Fuel Oil (EA-297 p4 "Fuel Oil/Kerosene"); household size does not change the cells.
  • Limits:
    • FY2026: all 10 DSS figures reproduce. Sizes 1-6 are 200% of the 2025 guideline ($7,825 ... $21,575 a quarter); sizes 7-9 are HHS's FFY2026 60% SMI amounts ($90,057, $92,058, $94,059), floored to $22,514, $23,014 and $23,514 a quarter; size 10 is 150% ($97,725, so $24,431). The rule's sizes 11-15 continue at 150% (105,972 ... 138,972).
    • FY2024: all 15 printed Max values (29,160 ... 129,808) reproduce with the FFY2024 median ($96,351).
    • FY2025: the printed table holds. Sizes 1-3 are 200% of the 2024 guideline ($7,530, $10,220, $12,910); sizes 4-9 are HHS's FFY2025 60% SMI amounts ($63,433, $73,582, $83,731, $85,634, $87,537, $89,440) divided by 4 and rounded half up; size 10 is 150% ($23,805). Size 11 follows the rule (103,288). The table exists because sizes 4 and 5 exceed the 200% cap and the quarters rounded differently, as the PR says.
    • Federal bounds (42 USC 8624(b)(2)(B)): the rule's inner term is the federal maximum, the greater of 150% of poverty and 60% of SMI, and the 200% cap only lowers it, so every rule-based limit lies between 150% of the guideline and that maximum. The 110%-of-poverty floor cannot bind at any size or year (the lowest printed figure, FY2025 size 10, is 150%).
    • The FY2026 plan-versus-DSS split for sizes 4-6 is documented in V/eligibility/sd_liheap_income_limit.py:38-44 and the PR body, and following DSS's published table is reasonable. Limits are inclusive ("may not exceed").
  • Mid: 125% of the prior year's guideline, rounded half up to the quarter: all 15 FY2024 three-month values (4556 ... 27044, so 18,224 rather than the annual column's 18,225), FY2023 and FY2022 sizes 1-10, and $19,564 for one person in FY2026 (inferred and documented; the FY2026 matrix names "0-Mid" without a number).
  • Timing: FY N runs October 1 of N-1 to September 30 of N (plan 1.1, p4), so period N reads FY N on January 1, with the prior year's guideline and the FFY N median (dated October 1).
  • Income (ARSD 67:15:01:18; M p24-28; plan 1.8-1.9 rendered):
    • Gross income ("earned and unearned income before deductions", ARSD 67:15:01:01(4)); three months approximated by annual income (ARSD 67:15:01:10).
    • Earnings of members under 18, or under 19 in high school, are excluded (:18(1)); their unearned income counts (M p26: "SSI or Social Security are considered the income of the child").
    • Counted, each with a basis: wages and self-employment (1.9; :18.01), SSI, pensions including military retirement, railroad retirement (plan 1.5, p5), non-Social Security survivor benefits, unemployment, workers' compensation (EA-297 p5; not excluded by :18), VA benefits, child support received, alimony, rental and farm rent (M p25), strike pay and general assistance (1.9), trusts as estate_income (M p25) and gambling winnings (M p27, documented against 1.9).
    • Excluded: interest and dividends (1.9 unchecked; M p25 "interest/dividend income from a bank or credit union account" N; M p28 "Stocks and bonds are NOT countable"), capital gains (:18(14)), SNAP.
    • Social Security net of medicare_part_b_premium, floored at zero (:18(12); M p28; 1.9 "Excluding MediCare deduction" checked). TANF through the take-up-gated tanf (M p28). Child support paid deducted (:18(20)) with a final floor.
  • Household: size counts is_citizen_or_legal_immigrant (M p9: "Mixed households are eligible for assistance. Only citizens and eligible aliens are counted as actual HH members"; p18 codes I and Q); the limit and Mid use that size; a unit with no counted member is ineligible.
  • Eligibility: South Dakota residence; categorical income eligibility only when every member receives SNAP (ARSD 67:15:01:01(1), :10; plan 1.4 and 1.4a, p5); responsibility through a vendor bill or heat paid in rent (ARSD 67:15:01:09; M p2); heat in rent with a $0 tenant payment ineligible (M p32); NONE ineligible; no asset test (plan 2.3).
  • Payment: vendor awards go to unpaid charges "until the benefit is exhausted" (M p30; plan 1.1; ARSD 67:15:01:43.01), so min(cell, bill); wood and coal paid directly (ARSD 67:15:01:40); heat in rent "up to 30 percent of the household's monthly rent" (ARSD 67:15:01:41.02) for October-April (M p31), with a positive subsidized tenant share allowed (M p32; plan 2.3).
  • Regions: the four ARSD 67:15:01:35 columns (19, 19, 15 and 13 counties), with Shannon as Oglala Lakota, match the region files and M p52's map.
  • Code patterns: no hard-coded values; defined_for = StateCode.SD on eight variables and sd_liheap gated by sd_liheap_eligible; vectorized formulas; all 19 parameters read; the shared fpg() and liheap_smi_limit() helpers reused; mixed-state rows return 0.
  • Mutation check: each of these, applied alone, fails at least one PR case: dropping the SNAP receipt or SNAP unit-size conjunct, the size gate, the NONE check, either heating-responsibility branch, kerosene's cash route, the expense floor, the base-payment gate, the minor-earnings exclusion, the Social Security premium netting (control), the limit's size mask, the Mid's half-up rounding, the heat-in-rent region and NONE gates, or the FY2025 table's size gate.
  • Tests:
    • 71 of 71 pass locally at head (policyengine-core 3.32.29) and on the merged tree. Periods are plain.
    • Hand-checked against the matrices, plans, DSS tables, HHS tables, ARSD and M:
      • T/sd_liheap.yaml Cases 1-4, 7-10, 12 and 13;
      • T/payment/sd_liheap_base_payment.yaml Cases 1-4, 6 and 7 (all 40 FY2026 cells and nine FY2023 cells);
      • T/eligibility/sd_liheap_income_limit.yaml Cases 1, 2, 3 and 5 (all 41 limits);
      • T/payment/sd_liheap_income_threshold.yaml Cases 1 and 2;
      • T/income/sd_liheap_countable_income.yaml Cases 1-11, 13, 14, 16 and 22;
      • T/payment/sd_liheap_heat_in_rent_payment.yaml Cases 1-5 and 7; T/eligibility/sd_liheap_eligible.yaml Cases 1, 4 and 7; T/payment/sd_liheap_region.yaml Case 1.
  • Changelog: changelog.d/sd-liheap.added.md is top level and typed added (its wording is in G7).

Checklist results (LIHEAP wave 2a)

  1. State plan and manual first: pass. I read the accepted FY2026 plan (rendered checkboxes) and FY2025 plan, item 2.6 of the FY2021-FY2024 plans, the FY2027 draft, the FY2022-FY2026 matrices, M, EA-297, the DSS page, ARSD 67:15:01 and the HHS SMI tables, and verified every encoded cell, limit, threshold, region and branch.
  2. Income definition: pass at head on gross income, the minor-earnings exclusion, Social Security net of Medicare (ARSD 67:15:01:18(12); plan 1.9), per-source loss floors and annual income for the three-month test. Excluding regular family help contradicts ARSD 67:15:01:18 (S2); Schedule-level netting is a reading point (G2). After rebase the list must read total_unemployment_compensation (S1).
  3. Backdating: FY2024-FY2026 reproduce. FY2022-FY2023 cells and Mid reproduce, with $4-a-year limit differences for five sizes (unclaimed, documented). FY2021 and earlier carry FY2022 cells although the FY2021 minimum was $334 (G4). The FY2027 draft plan is published, with no FY2027 DSS table or matrix yet (G4).
  4. Household and arrangement rules: pass on subsidized housing (positive tenant share), heat in rent, renters, categorical eligibility (the rule followed over the manual, documented), no priority, and the $668-$3,437 range. Eligible-but-$0 fuels and locations (G5); kerosene (G1).
  5. Tests: 71 cases, more than 8 hand-checked. FY2024 limits and Mid for sizes 1-15, FY2025 and FY2026 limits for sizes 1-10 (and 11-15 by rule), the limit and tier edges at the dollar and one cent, and plain periods are covered.
  6. Parameter descriptions: twelve off-template, five with trailing sentences (S3).
  7. References: one missing anchor, two incomplete page lists and uncited plan rates (S4).
  8. Wiring: pass. The programs.yaml entry and coverage line are well formed. No aggregate wiring, which matches OR, NY, NJ and MD on main (no LIHEAP program is in the household benefit aggregates). defined_for gating is on every variable. No take-up beyond TANF's.
  9. Axiom: valid queued line. rulespec-us#1539 is open, labelled pe-parity, dispatch-ready and current to ee24e71ea5. It needs updates for S1-S2 (see below).
  10. Branch and CI: all checks are green at head. 11 ahead and 82 behind main. No textual conflicts, but main's UI guard fails on the merged tree (S1).
  11. Integrate modeled state unemployment insurance into income flows #9558: S1. No modeled South Dakota UI exists, so the effect today is the guard failure only.
  12. IIJA and FY2027: no IIJA supplement; none of the FY2021-FY2026 plans or the FY2027 draft mentions one, and the matrices print one amount per cell. The FY2027 draft keeps $668-$3,437 and changes item 2.1 (G4).
  13. Mutation check: pass. All 16 mutations fail at least one case (see Verified correct).
  14. Health premiums after Document disjoint health premium inputs and correct consumer treatment #10046: not applicable. South Dakota deducts only Medicare premiums from benefit income; wages count gross ("REPORT GROSS (amount before deductions) INCOME", EA-297 p5; ARSD 67:15:01:01(4)), so pre_tax_health_insurance_premiums plays no role. medicare_part_b_premium, which the formula reads, is unchanged on main.
  15. State-gated index defaults: pass. sd_liheap_region (int, default 0 outside South Dakota) is clipped to 1-4 before it indexes payment/direct and heat_in_rent/amount, the tier key is always 1 or 2, and sd_liheap_household_size feeds only bracket and helper calls that accept 0. Mixed-state rows (ND and CA beside SD) run in one simulation in five of the PR's test files without error.

CI status

28 of 28 checks pass at ee24e71ea5 (gh pr checks 9974, run of 2026-10-12). The three "skipping" entries belong to a superseded run on the same SHA.

Branch status

Axiom line

axiom: TheAxiomFoundation/rulespec-us#1539 queued: valid format, and the issue meets the bar.

  • rulespec-us#1539 is open and labelled pe-parity (plus enhancement), with no comments, and was last updated 2026-10-12.
  • It is dispatch-ready under the Add AMT parameters for 2020-2023 #1467 standard. It has:
    • proposed us-sd/ module paths with corpus status;
    • verbatim ARSD, plan, matrix, manual, application and DSS text;
    • required outputs for all 9 variables;
    • 14 companion tests derived from the DSS tables, matrices, ARSD, plan, manual and EA-297 ("none is taken from policyengine-us output").
  • It describes head ee24e71ea5 correctly, including the plan-versus-DSS limit conflict (interpretation 1), the inferred Mid (2), kerosene (4), the unpriced fuels (5), the mixed-status gap (7) and the categorical-eligibility conflict (8).
  • Updates needed when the fixes land:
    • the UI input in the required outputs (S1);
    • the cash-help decision for interpretation 9 and the countable-income definition (S2);
    • the FY2027 draft's item 2.1 for interpretation 1 and the FY2021 $334 minimum for interpretation 13 (G4);
    • any change from G1-G3;
    • the stale "open as a draft" and "The tests were not run locally".

Next steps

  1. S1: rebase onto main and switch P/income/sources/unearned.yaml:14 to total_unemployment_compensation.
  2. S2: count financial_assistance and update Case 8, or document the plan-box reading with DSS confirmation.
  3. S3: align the twelve descriptions and move the trailing sentences into comments.
  4. S4: fix the eligibility anchor and the two page lists, and cite plan item 2.1 for the rates.
  5. Optionally address G1-G8, then refresh the PR body, the changelog and rulespec-us#1539 and re-request review.

0 critical and 4 should-address findings, plus 8 suggestions.

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 for 4 should items; details in the full report above. Verified: all 40 FY2026 benefit cells, the heat-in-rent amounts, the limit rule against every published limit, all 66 county regions and the ARSD income rules. All 71 tests pass and CI is green. (1) Merge blocker from main's #9558: unearned.yaml:14 reads the raw unemployment_compensation, so the guard fails on the merged tree. (2) Regular cash help from family (financial_assistance) is excluded. ARSD 67:15:01:18 excludes only gifts for recurring occasions, and the manual (p25) and current application count regular help; only plan item 1.9's unchecked box supports the PR. Example: $792 at head, $0 with the help counted. (3) Twelve parameter descriptions are off the house template. (4) References: the FY2026 plan link lacks #page=5, two page lists are incomplete, and the rate parameters don't cite plan item 2.1.

hua7450 and others added 2 commits October 15, 2026 15:13
- S1: Unearned list reads the raw unemployment_compensation (main's #9558 guard fails)
- S2: Regular cash help from family (financial_assistance) is excluded
- S3: Twelve parameter descriptions are off the house template (five have trailing sentences)
- S4: References: plan link without a page, incomplete page lists, rate parameters missing plan item 2.1

Also applies the in-scope suggestions. Tests not run locally; CI validates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hua7450

hua7450 commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Fixes pushed in cd74fd5.

The branch also merges upstream/main (PRs 9558 and 10046). PR 10046's pre_tax_health_insurance_premiums does not apply, because South Dakota counts wages gross. No limit falls below 149% of the guideline, so the shared 110% floor gov.hhs.liheap.fpg_floor (42 U.S.C. 8624(b)(2)(B)) never binds here, and this PR does not add it. The suite now has 72 cases; the new and changed expected values are hand-computed, and CI validates them.

  • S1: income/sources/unearned.yaml now lists total_unemployment_compensation, so no raw entry is left for main's guard. South Dakota has no modeled unemployment insurance, so no value changes. New income Case 23 counts $20,800 of wages and $6,400 of benefits as $27,200 ($400 a week, 1/26 of a $10,400 high quarter under the DLR rule, for 16 weeks).
  • S2: done as you proposed. financial_assistance is in the unearned list, with a comment that follows ARSD 67:15:01:18(17), manual page 25 and EA-297 page 5 over plan item 1.9's unchecked box. Income Case 8 now expects $14,400 ($12,000 of wages plus $2,400 of help). The PR body adds a cash-gifts conflict row and drops cash gifts from "Not modeled".
  • S3: the twelve descriptions use your suggested text, and the trailing sentences are now YAML comments. All 19 descriptions are one sentence ending in "under the Low Income Energy Assistance Program."
  • S4: sd_liheap_eligible opens the FY2026 plan at page 5 with # PDF pages 5, 34, sd_liheap_countable_income adds # PDF pages 6-7, and sd_liheap_heat_in_rent_payment opens manual page 30 with # PDF pages 30-32. smi_rate and fpg_lower_rate cite plan item 2.1 (FY2026 page 9, FY2025 page 8). fpg_upper_rate cites FY2026 page 9 only, since FY2025 prints 60% of SMI for sizes 1-3, and its comment shows that 54.14%, 55.95% and 57.07% equal 200% of the 2025 guideline. The live DSS page is replaced in all five places by the January 1, 2026 Wayback capture of the 2025-2026 table.
  • G1: kerosene keeps the manual's full payment. Manual page 31 is the only source that names kerosene in a payment rule, and neither ARSD 67:15:01:40 nor the application bars a check. The sd_liheap comment now quotes EA-297 page 4 beside the manual and the rule, and the PR body's kerosene row adds the application and DSS page wording.
  • G2: documented in the PR body's conflict table. The per-source floors stay, since this batch treats rental income as non-negative and makes no self-employment netting changes.
  • G3: kept full counting. The Citizenship Income Calculator formula is unpublished, and its one example matches neither full counting nor pro rata; the code comment, PR body and registry note record the gap.
  • G4: partly done. History is out of scope for this PR, so FY2021-FY2023 are not gated or re-encoded; payment/direct.yaml now notes the FY2021 plan's $334 minimum. For FY2027, a sd_liheap_income_limit comment cites the draft's item 2.1 and flags sizes 4-6 for a recheck when DSS posts its 2026-2027 limits. The PR body gives the model year 2027 values: sizes 4-6 follow the 200% cap ($66,000, $77,360, $88,720) where the draft's 60% gives $70,800, $82,128 and $93,456.
  • G5: partly done. Fuels without a matrix row stay eligible at $0 as a documented coverage gap: the FY2026 matrix prices five fuel rows, no South Dakota source maps SOLAR or OTHER to one, and manual page 31 sets only the payment method for corn and pellet stoves. The base-payment comment now points to Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754 for UNSPECIFIED. sd_liheap_region gained a documentation string, as in New Jersey, stating that a household with no county input takes Aurora County (region 2).
  • G6: done. sd_liheap_base_payment drops the spm_unit_size gate, because sd_liheap_eligible already requires a counted member. Base-payment Case 5 loses its size-0 column, and Cases 1-4 and 7 lose the unused spm_unit_size inputs.
  • G7: done. The changelog now names mixed-status households. The registry entry is South Dakota LIEAP (South Dakota Low Income Energy Assistance Program) with a two-sentence note. The PR body drops the draft sentence and says every case except the four household-size cases sends heating_type. It also adds conflict rows for cash gifts and for manual page 13's SNAP income times three.
  • G8: partly done. The listed comments are rewritten with spaced, hand-checked arithmetic, the "approved" wording and the sources/sd_county_regions.json reference are gone, and the copied file headers are replaced. heating_type stays in the isolated limit, threshold and base-payment tests: sd_liheap_base_payment reads it, and the batch sends it in those fixtures so none depends on the default.
  • LIHEAP wave 2a: encode South Dakota LIEAP regular heating assistance TheAxiomFoundation/rulespec-us#1539 is refreshed to this head.

@hua7450
hua7450 requested a review from DTrim99 October 15, 2026 21:08

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

Program Review

IDs: C = critical, S = should address, G = suggestion. S1-S4 and G1-G8 keep the prior review's numbers; C1 and G9-G17 are new.

Source Documents

Paths: P/, V/, T/ = gov/states/sd/dss/liheap/ under policyengine_us/parameters, policyengine_us/variables and policyengine_us/tests/policy/baseline. Pages are PDF file pages.

Branch Status

13 commits ahead of main and 42 behind; merges cleanly. Informational only: the staleness did not affect any finding.

Critical (Must Fix)

C1. Kerosene is paid the full award, but the state plan pays only coal, wood and heat in rent to the household

  • Defect: V/sd_liheap.py:29-33 puts KEROSENE in cash_payment, so line 39 pays the full Fuel Oil cell whatever the unpaid charges. Other vendor fuels get min(cell, heating_expense). The comment at :23-28 says "Neither bars a check for kerosene, so the manual is followed." T/sd_liheap.yaml Case 2 pins $2,858 on a $1 bill.
  • Evidence:
    • Plan item 9.1 asks "Do you make payments directly to home energy suppliers?" Heating: Yes. Exceptions: Yes, and it lists them: "If household uses coal or wood as it's primary heat source or a household's heat is included in rent, a payment is issued directly to the household." Same text in FY2026 (p. 22), FY2025 (p. 21), FY2024 (p. 21) and the FY2027 draft (p. 22).
    • ARSD 67:15:01:40 names only "wood or coal" for direct payment. ARSD 67:15:01:43.01 makes payment "to the energy supplier or landlord" the default.
    • EA-297 lists "Fuel Oil/Kerosene" with a supplier fill ticket and pays only "Heat in Rent, Wood, or Coal" to the household (p. 4). The DSS page: "the payment is made directly to the energy supplier."
    • Only the manual pays kerosene "the entire benefit amount" by check (p. 31).
    • The PR prices kerosene from the Fuel Oil row because EA-297 groups "Fuel Oil/Kerosene" (V/payment/sd_liheap_base_payment.py:31), but does not follow that grouping for the payment method.
  • History: round 1 raised this as G1, a suggestion, on EA-297 p. 4, ARSD :40 and the DSS page, and called the reading ambiguous. The author declined because "neither ARSD 67:15:01:40 nor the application bars a check" and documented the conflict, a fair answer to that evidence. Round 1 did not cite plan item 9.1, the state's own list of exceptions to supplier payment for every claimed year. With the supplier default in ARSD :43.01, it answers the author's reason.
  • Impact (reproduced at head; one adult, no income, Custer County): kerosene gets $2,858 on a $1 bill (vendor rule: $1) and on a $1,500 bill (fuel oil: $1,500), in 2024, 2025 and 2026. Microsimulation impact is about $0 (no heating inputs, #9754).
  • Severity: critical, because it passed code-path and 600-DPI visual verification, changes awards in every claimed year and rests on evidence round 1 did not have (as in #9973); round-1-accepted items on unchanged evidence are not escalated (as in #9972).
  • Fix:
    1. Remove | (heating_type == fuels.KEROSENE) from cash_payment. Kerosene then takes min_(base_payment, expense), as fuel oil does.
    2. Rewrite V/sd_liheap.py:23-28 to cite plan 9.1, ARSD :40 and :43.01 and EA-297 p. 4, and say manual p. 31 is superseded for kerosene. Add plan 9.1 (#page=22) and ARSD 67:15:01:40 to reference.
    3. T/sd_liheap.yaml Case 2: KEROSENE 2858 becomes 1; rename the case. Case 8 (2023) stays $1,905 (min($1,905, $3,000)); fix only its comment at :168 and the header at :2-3.
    4. Update the PR body's kerosene row and rulespec-us#1539 interpretation 4.

Should Address

None. Round-1 S1-S4 are resolved.

Suggestions

  • G9. Case 23 (T/income/sd_liheap_countable_income.yaml:696-700) and Case 22 (:666-669) cite DLR with no URL; link SDCL 61-6-2 and the DLR fact sheet.
  • G10. Case 23 passes with the raw or total UI input; add unemployment_compensation: 0 and modeled_state_unemployment_compensation: 6_400 (total $27,200, raw $20,800, hand-computed).
  • G11. T/payment/sd_liheap_base_payment.yaml:67-70 calls the NONE zero a coverage gap, but NONE is ineligible (ARSD 67:15:01:09); the name still says "component placeholders".
  • G12. V/eligibility/sd_liheap_income_limit.py:42: $22,014 should be $22,013 under the model's flooring. Note in fpg_upper_rate.yaml that plan p. 9 prints 60% SMI for sizes 4-6.
  • G13. P/payment/direct.yaml:8-9: use the full FY2021 plan URL (/sites/default/files/webfiles/docs/SD_Plan_2021.pdf#page=9).
  • G14. Optional: cite 42 USC 8624(b)(2)(B) on fpg_lower_rate.yaml and the archived DSS table on fpg_year_lag.yaml.
  • G15. Optional end-to-end pins: sd_liheap: [938, 938, 938, 0] in T/eligibility/sd_liheap_eligible.yaml Case 3 (hand-computed); a $27,000 wages + $6,000 financial_assistance case giving $0.
  • G16. Consider a T/integration.yaml, as the merged state LIHEAP folders have.
  • G17. Style: drop absolute_error_margin from boolean-only cases; use 20_000 consistently.

Prior Findings

ID Finding Status
S1 Raw UI input fails #9558 guard RESOLVED; Case 23 added
S2 financial_assistance excluded RESOLVED; Case 8 = $14,400
S3 Off-template descriptions RESOLVED
S4 Anchors, page lists, plan 2.1, live DSS link RESOLVED
G1 Kerosene paid in full STILL OPEN: declined by author; superseded by C1
G2 Self-employment and rental netting RESOLVED (documented)
G3 Ineligible members' income in full RESOLVED (documented)
G4 Backfill and FY2027 RESOLVED (documented)
G5 SOLAR, OTHER, UNSPECIFIED at $0 STILL OPEN (partial): documented coverage gap, acceptable
G6 Base-payment size gate RESOLVED
G7 Changelog, body, registry RESOLVED
G8 Hygiene STILL OPEN (partial): T/income/sd_liheap_countable_income.yaml:369-370; isolated-test heating_type declined by author

PDF Audit Summary

Category Count
Documents audited 4 PDFs (162 pages at 300 DPI), DSS page, 4 external PDFs
Changed values and claims matched 36
Mismatches queued 11
Confirmed (code path and 600-DPI check) 1 (C1)
Investigated and cleared 10
References checked 44: 38 correct, 0 wrong, 6 unverifiable

Investigated and cleared ("R1" = round 1 accepted the same evidence):

  • FY2026 sizes 4-6 follow the DSS table over plan 2.1 (R1).
  • financial_assistance counted over plan 1.9's box, per ARSD :18 (R1's S2 fix).
  • Ineligible members' income counted in full; no published formula (R1, G3).
  • Manual's "all three months" gift test; annual input cannot carry it (R1).
  • gambling_winnings counted; ARSD :18 has no exclusion (R1).
  • OTHER, SOLAR, UNSPECIFIED at $0; no source prices them (G5).
  • Categorical tier from SNAP income × 3; ARSD :20 uses one measure (R1).
  • FY2027 sizes 4-6; unclaimed draft year (R1, G4).
  • Heat in rent for condo fees and hotels (manual p. 30); no input; optional "Not modeled" row.
  • Pre-FY2022 cells vs the FY2021 $334 minimum; unclaimed (R1, G4).

Validation Summary

Check Result
Regulatory accuracy 1 critical (C1); incremental fixes correct
References Changed anchors correct; Wayback unverifiable here
Code patterns No hard-coded policy values; descriptions on template
Test coverage 72 cases pass; Cases 8 and 23 hand-checked; all branches covered
CI 29/29 pass at cd74fd52d6
Main merge #9558 guard passes; #10046 not applicable
Federal 110% floor Never binds (lowest limit about 150% of the lagged guideline)
Changelog Present, top level, added

Review Severity: REQUEST_CHANGES

One confirmed value mismatch (C1) changes kerosene awards in every claimed year.

Next Steps

To auto-fix issues: run the fix-pr workflow for this PR.

@hua7450

hua7450 commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Fixes pushed in f6a8bbf.

The suite now has 78 cases in 10 files. The new and changed expected values are hand-computed, and CI validates them.

  • C1: Fixed as you listed. sd_liheap drops KEROSENE from cash_payment, so the model now pays kerosene to the supplier at the fuel oil cell, capped at unpaid charges as fuel oil is. The comment cites plan item 9.1, ARSD 67:15:01:40 and 67:15:01:43.01 and EA-297 page 4, and says manual page 31 is superseded for kerosene. reference adds plan 9.1 (#page=22) and both rules. sd_liheap.yaml Case 2 is renamed and now expects [742, 742, 1]. Case 8 keeps $1,905 (min($1,905, $3,000)); its comment and the file header are corrected. Integration Case 2 caps kerosene at $900 of fill tickets, and the PR body's kerosene row now follows the plan and rules.
  • G9: Case 22 now links SDCL 62-4-3 and the DLR weekly workers' compensation rate table (page 2). Case 23 links SDCL 61-6-2 and the DLR benefits fact sheet (page 2). Expected values are unchanged.
  • G10: Case 23 now sends unemployment_compensation: 0 and modeled_state_unemployment_compensation: 6_400, and asserts total_unemployment_compensation: 6_400. Reading the raw input would give $20,800, so the $27,200 expectation fails on that regression.
  • G11: Base-payment Case 5 is renamed to "unknown regions, unpriced fuels, no heat and another state return a zero base payment." Its comment and the sd_liheap_base_payment comment now list only an unknown region and SOLAR, OTHER and UNSPECIFIED as coverage gaps, and both say that NONE fails sd_liheap_eligible; the test comment cites ARSD 67:15:01:06 and 67:15:01:09. Values are unchanged.
  • G12: The sd_liheap_income_limit comment now gives $22,013 under the model's flooring: floor(0.6 × $111,183) = $66,709, floor($66,709 × 1.32) = $88,055, and $88,055 / 4 floors to $22,013. fpg_upper_rate.yaml notes that plan page 9 prints 60% of state median income for sizes 4-6. DSS publishes 200% of the 2025 guideline instead ($16,075, $18,825 and $21,575 a quarter), and the model follows DSS.
  • G13: payment/direct.yaml now cites the full FY2021 plan URL at page 9. The FY2025 plan citation in the same comment also gets its full URL.
  • G14: Done. fpg_lower_rate.yaml adds 42 U.S.C. 8624(b)(2)(B) with the #b_2_B anchor, and fpg_year_lag.yaml adds the archived DSS 2025-2026 income-limit table.
  • G15: Done. Eligible Case 3 now sends Aurora County and asserts sd_liheap: [938, 938, 938, 0] (region 2, tier 1 gas, min($938, $2,000)). New sd_liheap.yaml Case 14 counts $27,000 of wages plus $6,000 of financial_assistance as $33,000. That is over the $31,300 one-person limit, so the award is $0.
  • G16: Added integration.yaml with five FY2026 households. Four of them carry fuel bills through heating_expense: Minnehaha gas ($1,007), Brown kerosene capped at $900 of fill tickets, Harding wood ($854 on a $500 bill), and Lincoln propane ($1,800, with the teenager's earnings excluded). The fifth is a Pennington renter with heat in rent ($800).
  • G17: Done. Eligible Case 2, which asserts only booleans, drops absolute_error_margin. Every literal of four or more digits in the SD test files now uses underscores. No values change.
  • G1: Closed by C1. The model now pays kerosene to the supplier.
  • G5: No change. SOLAR, OTHER and UNSPECIFIED stay eligible at $0 as a documented coverage gap because no South Dakota source prices them, and Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754 tracks the UNSPECIFIED default. G11 removes NONE from that list.
  • G8: Partly done. The countable-income Case 12 comment now reads "FY2026 plan pages 5 and 10 process railroad retirement as counted fixed income." heating_type stays in the isolated limit, threshold and base-payment tests: sd_liheap_base_payment reads it, and the state LIHEAP tests send it in every eligibility and payment case so that no case depends on the default.
  • LIHEAP wave 2a: encode South Dakota LIEAP regular heating assistance TheAxiomFoundation/rulespec-us#1539 is refreshed to match.

Thanks for checking the ten cleared items. The PR body adds your optional "Not modeled" row for heat included in condominium fees or hotel charges (manual page 30). The Wayback capture 20260101052007 loads through Wayback's raw id_ URL and shows the same 2025-2026 table, including "the payment is made directly to the energy supplier."

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

Program Review

IDs: C = critical, S = should address, G = suggestion. C1, S1-S4 and G1-G17 keep the prior review's numbers; G18-G21 are new and optional.

Source Documents

Paths: P/, V/, T/ = gov/states/sd/dss/liheap/ under policyengine_us/parameters, policyengine_us/variables and policyengine_us/tests/policy/baseline. Pages are PDF file pages.

Branch Status

15 commits ahead of main and 17 behind; merges cleanly. Informational only. The merge of main at 1661d715bd changed no SD file, and no shared input that SD reads (heating_expense, has_heating_expense, total_unemployment_compensation, FPG, SMI, Part B).

Prior Findings

ID Finding Status
C1 Kerosene paid the full award by check RESOLVED. V/sd_liheap.py:36 is WOOD or COAL only; kerosene takes min_(base_payment, expense) at :42. Comment :26-35 and reference :13-15 cite plan 9.1, ARSD :40 and :43.01. T/sd_liheap.yaml:45 = [742, 742, 1]; Case 8 stays $1,905 (:170-174); T/integration.yaml:129 = $900. Putting KEROSENE back fails exactly these 2 cases.
G1 Kerosene paid in full (round-1 form) RESOLVED (closed by C1)
G5 SOLAR, OTHER, UNSPECIFIED at $0 STILL OPEN (accepted): documented gap at V/payment/sd_liheap_base_payment.py:44-47, #9754; NONE correctly removed from the list
G8 Hygiene STILL OPEN (accepted, partial): Case 12 comment fixed (T/income/sd_liheap_countable_income.yaml:369-371); heating_type in isolated limit and threshold tests kept by the author, inert
G9 Cases 22 and 23 cite DLR with no URL RESOLVED: SDCL 62-4-3 and DLR rate table p. 2 ($554-$1,108 for 7-1-25 to 6-30-26); SDCL 61-6-2 and DLR fact sheet p. 2
G10 Case 23 cannot tell raw UI from total UI RESOLVED: :721-722 reported 0, modeled 6,400; :735-736 assert 6,400 and 27,200. The raw-reader mutation gives 20,800 and fails.
G11 NONE described as a coverage gap RESOLVED (T/payment/sd_liheap_base_payment.yaml:67-71; ARSD :06, :09)
G12 $22,014 comment; sizes 4-6 note RESOLVED: $22,013 recomputed (V/eligibility/sd_liheap_income_limit.py:41-43); note at P/income/limits/fpg_upper_rate.yaml:7-9 matches plan p. 9
G13 Short FY2021 plan URL RESOLVED (P/payment/direct.yaml:9-12; p. 9 shows the $334 minimum)
G14 42 U.S.C. and archived DSS references RESOLVED (P/income/limits/fpg_lower_rate.yaml:20-21, anchor b_2_B exists; P/income/fpg_year_lag.yaml:13-14, capture verified)
G15 End-to-end pins RESOLVED: T/eligibility/sd_liheap_eligible.yaml:55-58 and T/sd_liheap.yaml:346-365. Each fails its mutation (drop has_qualified_member; drop financial_assistance, which also fails countable-income Case 8).
G16 Integration file RESOLVED: T/integration.yaml, 5 FY2026 households, 11 outputs each; all 55 values hand-checked
G17 Style RESOLVED: no boolean-only case keeps a margin; every 4+ digit value in the 10 SD test files uses underscores
S1-S4, G2-G4, G6, G7 Round-1 items RESOLVED in round 2; no regression

Critical (Must Fix)

None.

Should Address

None.

Suggestions

All four are optional and do not block approval.

  • G18. Pin heat in rent over the wood, coal and kerosene payment rules. V/sd_liheap.py:46-50 checks heat in rent before the wood/coal branch at :36 and :42. That order is correct: plan 9.1 lists coal/wood and heat in rent as separate exceptions (FY2026 p. 22), and ARSD 67:15:01:41.02 prices heat in rent from rent, not fuel (manual p. 30). But no case combines heat in rent with WOOD, COAL or KEROSENE, so a refactor that tests cash_payment first passes all 78 cases (mutation run). Append this to T/sd_liheap.yaml after :365. It passes at head; under the mutation it gives [742, 742, 800] and fails. Custer is region 1: min($800, $6,000 / 12 x 7 x 0.30 = $1,050) = $800.
    - name: Case 15, heat in rent takes precedence over the wood, coal and kerosene payment rules.
      period: 2026
      absolute_error_margin: 0.01
      input:
        state_code: [SD, SD, SD]
        age: [40, 40, 40]
        immigration_status: [CITIZEN, CITIZEN, CITIZEN]
        heating_type: [WOOD, COAL, KEROSENE]
        heating_expense: [0, 0, 0]
        employment_income: [0, 0, 0]
        ssi: [0, 0, 0]
        tanf: [0, 0, 0]
        snap:
          2026-01: [0, 0, 0]
        heat_expense_included_in_rent: [true, true, true]
        rent: [6_000, 6_000, 6_000]
        county_str: [CUSTER_COUNTY_SD, CUSTER_COUNTY_SD, CUSTER_COUNTY_SD]
      output:
        sd_liheap_eligible: [true, true, true]
        sd_liheap: [800, 800, 800]
  • G19. Optional: wood with no purchased-fuel bill. V/sd_liheap.py:42 pays wood and coal the full cell whatever heating_expense is, and eligibility rests on has_heating_expense. Every wood and coal case has a positive bill, so a mutation that requires expense > 0 for the cash branch survives. If $742 is the intended award when the user sets has_heating_expense: true with a $0 bill (that variable's documentation names free gathered wood), add a two-column Case 16: WOOD, Custer, heating_expense: [0, 0], has_heating_expense: [true, false], expecting sd_liheap_eligible: [true, false] and sd_liheap: [742, 0] (passes at head). Check this against the proof of heating liability in ARSD 67:15:01:09 and manual p. 31 first.
  • G20. Style. Add a blank line before the new comment blocks at T/sd_liheap.yaml:25 and T/eligibility/sd_liheap_eligible.yaml:38; each starts right after the previous case's output, so it reads as part of that case. Optionally add #page=1 to the one-page matrix URL at V/sd_liheap.py:12 and T/integration.yaml:4 (also the unchanged V/payment/sd_liheap_heat_in_rent_payment.py:13 and P/payment/heat_in_rent/amount.yaml:19), as V/payment/sd_liheap_base_payment.py:12 does.
  • G21. Comments. T/integration.yaml:82 and :113-114 give the $202.90 Part B premium without a source; add the CMS 2026 fact sheet. In T/income/sd_liheap_countable_income.yaml:707-709, say that South Dakota UI is not modeled (modeled_state_unemployment_compensation lists only AL, NJ, NY, OK, PA and UT), so the input stands in for reported UI to test the fallback (manual p. 28: "All UI payments received are countable").

PDF Audit Summary

Category Count
Documents audited No new primary source. Round-2 300-DPI renders reused (4 PDFs, 162 pages), plus the FY2026 matrix and FY2021 plan p. 9. New this round: FY2024 plan p. 21 (300 DPI), DLR rate table (2 pages), DLR fact sheet p. 2, and the web sources above (DSS, Wayback, CMS, SSA, DLR wage page, ARSD, SDCL, U.S.C.).
Changed page anchors and page claims 23 checked (7 #page= hrefs, 16 page claims): 23 correct
Hand-computed expected values 55 integration outputs, T/sd_liheap.yaml Cases 2, 8, 14 and eligible Case 3: all match
Value mismatches queued 0
Verification items 14 (5 external, 9 code-path): 5/5 external verified, 9/9 code-path claims confirmed
Investigated and cleared 7 (below)
References checked 18 added or changed: 18 correct, 0 wrong, 0 unverifiable. The 5 unchanged hrefs to the same Wayback capture are now verified too.

Investigated and cleared:

  • Wayback 20260101052007 (7 places): loads in both forms and shows the 2025-2026 LIEAP table ($7,825 ... $24,431) and "the payment is made directly to the energy supplier". The weatherization table on the same page differs for sizes 7-10; the PR uses the LIEAP table.
  • FY2024 plan item 9.1 on p. 21 (cited at V/sd_liheap.py:26): Heating Yes, exceptions Yes, same coal/wood/heat-in-rent text.
  • SSA $2,071 average and 2.8% COLA (T/integration.yaml:79-81): match the archived fact sheet.
  • CMS $202.90 Part B premium: matches the fact sheet and gov.hhs.medicare.part_b.base_premium.
  • Case 23 and main's total_unemployment_compensation (reported amount, else modeled): the right "received" concept for manual p. 28; also what the #9558 guard requires.
  • Case 22 spans two DLR rate years ($554-$1,108 and $576-$1,152); $600 a week is inside both.
  • Heat in rent: plan 9.1 pays the household, ARSD :41 the landlord; the amount is the same, so the model is unaffected.

Validation Summary

Check Result
Regulatory accuracy C1 fix follows plan 9.1, ARSD :40 and :43.01 and EA-297 p. 4 in every claimed year; wood, coal and heat in rent unchanged
References 18 added or changed, all correct; Wayback capture verified
Code patterns One formula line changed (V/sd_liheap.py:36); no hard-coded values; other variable edits are comments
Test coverage 78 cases; C1 revert fails 2; G10 and G15 pins each fail their mutation; heat-in-rent order unpinned (G18)
CI 29/29 pass at f88aa84a78
Local tests 78/78 pass at f88aa84a78 (SD folder)
Main merge No SD file changed; no regression
Federal 110% floor (#10279) Not wired for SD; never binds (lowest limit about 150% of the lagged guideline)
Changelog Present, top level, added

Review Severity: APPROVE

No critical or should-address item is open. C1 is fixed and covered by tests, and the round-2 suggestions G9-G17 are done. G5 and G8 stay open as accepted. G18-G21 are optional.

Next Steps

Nothing blocks merge. To apply the optional suggestions, run the fix-pr workflow for this PR.

@DTrim99

DTrim99 commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 2): Add South Dakota LIEAP regular heating assistance

PR #9974 (author: hua7450). This round reviews head f88aa84a78; round 1 reviewed ee24e71ea5. Four commits and two outside reviews came in between:

  • 91e39fd448, a merge of main at 9560e7f183. It brought in Integrate modeled state unemployment insurance into income flows #9558 (total_unemployment_compensation and its guard) and Document disjoint health premium inputs and correct consumer treatment #10046 (pre_tax_health_insurance_premiums). It touches no South Dakota LIEAP file.
  • cd74fd52d6, "Address review round 1 on South Dakota LIHEAP" (30 files, +222/−103). It answers S1–S4 and G1–G8 and brings the suite to 72 cases.
  • PavelMakarchuk requested changes on cd74fd52d6 (2026-10-19). He raised:
    • C1: kerosene was paid the full award by check, although plan item 9.1 pays every fuel except coal, wood and heat in rent to the supplier;
    • suggestions G9–G17.
  • f6a8bbf432, "Address Pavel's review on South Dakota LIHEAP" (14 files, +435/−84). It:
    • removes KEROSENE from the cash route in sd_liheap, so kerosene is capped at unpaid charges like fuel oil;
    • adds sd_liheap.yaml Case 14, award outputs on eligibility Case 3 and a new integration.yaml (5 cases), for 78 cases in 10 files;
    • pins modeled UI in income Case 23 and polishes comments and references.
  • f88aa84a78, a merge of main at 1661d715bd (Apply the federal 110% poverty floor to state LIHEAP programs (DC, MA, MS, NJ, OR) #10279, the shared 110% poverty floor). It touches no South Dakota LIEAP file.
  • PavelMakarchuk approved f88aa84a78 on 2026-10-20 (19:27 UTC). He marked C1 and G9–G17 resolved and left four optional suggestions, G18–G21.

Neither merge had a hand-resolved conflict.

Path prefixes, as in round 1:

  • P/ = policyengine_us/parameters/gov/states/sd/dss/liheap/
  • V/ = policyengine_us/variables/gov/states/sd/dss/liheap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/sd/dss/liheap/
  • "Manual" = the LIEAP Policy and Procedures Manual (2018 Clearinghouse copy, 60 pages). Page numbers are file pages.

Line numbers refer to f88aa84a78.

Summary

Ziming fixed all four round-1 should items:

  • S1. The unearned list reads total_unemployment_compensation. Main's guard passes at the head, and income Case 23 now enters the benefit as modeled UI with no reported amount, so reading the raw input would fail it ($20,800 instead of $27,200).
  • S2. He took the fix I proposed. financial_assistance is in the unearned list, with a comment that follows ARSD 67:15:01:18(17), manual page 25 and EA-297 page 5 over plan item 1.9's unchecked box. Income Case 8 now expects $14,400, and sd_liheap.yaml Case 14 pins my round-1 example: $27,000 of wages plus $6,000 of regular help is $33,000, over the $31,300 limit, so $0. The body has a cash-gifts conflict row, and the "not modeled" wording and the old test comment are gone.
  • S3. All 19 descriptions are one sentence ending at "under the Low Income Energy Assistance Program." The trailing sentences are now YAML comments.
  • S4. The FY2026 plan link opens page 5 with # PDF pages 5, 34, and the income and heat-in-rent variables carry # PDF pages 6-7 and #page=30 with # PDF pages 30-32. The rate parameters cite plan item 2.1. The live DSS page is replaced by the January 1, 2026 Wayback capture, which I loaded: it shows the 2025-2026 table ($7,825 ... $24,431).

Of the eight suggestions:

  • G1 (kerosene) is fixed through Pavel's C1;
  • G6 and G7 are fixed;
  • G2, G3 and G4 are documented, and G5 and G8 are partly done.

I accept the reasons for each.

Pavel's C1 fixes my round-1 G1 with evidence I did not have: plan item 9.1, which I confirmed on FY2026 page 22. All of his G9–G17 are done. His G18–G21 are open and optional, and I agree with them.

There are no new critical or should findings. Two new suggestions come from main commits merged after this head's last merge:

Batch checks:

I checked every new or changed expectation by hand: 7 new cases, 2 cases with changed values, 1 case with added award outputs and the cases whose unread inputs were removed. CI is 29/29 green at the head. The branch is 15 commits ahead of main and 18 behind, and merges cleanly.

Prior findings status

Finding Status Evidence
S1: the unearned list read the raw unemployment_compensation, so main's #9558 guard failed after the merge Fixed The list. P/income/sources/unearned.yaml:14 reads total_unemployment_compensation, and no South Dakota LIEAP file reads the raw input. The guard. All four test_unemployment_compensation_readers.py tests pass at the head (Rest shard-1). The case. Income Case 23 (line 710) sends unemployment_compensation: 0 and modeled_state_unemployment_compensation: 6_400 and asserts total_unemployment_compensation: 6_400 and 20,800 + 6,400 = 27,200 (Pavel's G10). The total uses the reported amount when nonzero and otherwise the modeled one, so a raw reader gives 20,800. The issue. rulespec-us#1539's countable-income output names the total.
S2: regular cash help from family (financial_assistance) was excluded, although ARSD 67:15:01:18 excludes only occasion gifts Fixed The list. P/income/sources/unearned.yaml:33-40 adds financial_assistance. The comment quotes ARSD 67:15:01:18(17) ("Cash gifts for recurring occasions such as birthdays and Christmas"), manual page 25 ("Cash Gifts (Monthly)") and EA-297 page 5 ("Money from Family or Friends"), notes plan item 1.9's unchecked box, and reads an annual input as regular help. I re-read the rule's 22 exclusions this round; (17) is the only gift item. The closing comment (lines 41-42) no longer lists cash gifts as excluded. The tests. Income Case 8 (line 253): 12,000 + 2,400 = 14,400; interest, dividends, capital gains and SNAP still excluded. sd_liheap.yaml Case 14 (line 346): 27,000 + 6,000 = 33,000 > 31,300, so [false] and [0]; without the help, $27,000 is tier 2 and $792. Disclosure. The body's conflict table has a "Cash gifts" row, the unearned row lists the help, and rulespec-us#1539 interpretation 9 records the choice.
S3: twelve descriptions off the template, five with trailing sentences Fixed The twelve. All use my suggested wording ("uses these sources as earned income", "uses these counties as heating region N", "provides heat-in-rent payments for this many months", and so on). All 19. Each is one sentence ending "under the Low Income Energy Assistance Program." The rule notes moved into comments (for example P/income/limits/smi_rate.yaml:2-5).
S4: the plan linked without a page, two incomplete page lists, rate parameters not citing plan item 2.1 Fixed Anchors. V/eligibility/sd_liheap_eligible.py:14-15 (# PDF pages 5, 34, #page=5); V/income/sd_liheap_countable_income.py:14-15 (# PDF pages 6-7); V/payment/sd_liheap_heat_in_rent_payment.py:14-15 (# PDF pages 30-32, #page=30). Rates. smi_rate.yaml and fpg_lower_rate.yaml cite plan item 2.1 (FY2026 #page=9, FY2025 #page=8). fpg_upper_rate.yaml cites FY2026 page 9 only, which is right: FY2025 prints 60% of median income for sizes 1–3, not a 200% cap. Its comment shows that 54.14%, 55.95% and 57.07% equal 200% of the 2025 guideline. DSS. All five live-page references now point to the 20260101052007 capture; it returned HTTP 200 (id_ form) on 2026-10-20 with the 2025-2026 table and "the payment is made directly to the energy supplier".
G1: kerosene paid the full award Fixed (through Pavel's C1) See Pavel's C1 below.
G2: self-employment and rental results floored per source Documented (accepted) The body's conflict table has a "Self-employment and rental losses" row, and #1539 interpretation 9 says the same. Per-source floors stay, which is the conservative reading of ARSD 67:15:01:18.01 (as #9975 G4).
G3: ineligible members' income counted in full Declined, documented (accepted) V/income/sd_liheap_countable_income.py:53-60, the body's "Ineligible members' income" row and the registry note record that the Citizenship Income Calculator's formula is unpublished and that its one example matches neither full counting nor pro rata.
G4: backfilled years and FY2027 Partly done (accepted) Done. P/payment/direct.yaml:9-12 cites the FY2021 plan's $334 minimum (full URL, #page=9). V/eligibility/sd_liheap_income_limit.py:46-50 cites the FY2027 draft's item 2.1 and flags sizes 4–6. The body gives the model year 2027 values and the draft's 60% figures. Declined. FY2021–FY2023 stay ungated and unclaimed, as in #9967–#9973. Status today. The live DSS page still shows the 2025-2026 table, and the Clearinghouse returns 404 for a FY2027 South Dakota plan and matrix.
G5: fuels and locations eligible but paid $0 Partly done (accepted) Done. V/payment/sd_liheap_base_payment.py:44-47 names an unknown region and SOLAR, OTHER and UNSPECIFIED as coverage gaps, points to #9754 and says NONE fails eligibility. V/payment/sd_liheap_region.py:11-15 adds a documentation string for the Aurora fallback, as New Jersey does. Kept. The eligible-at-$0 treatment stays, pinned by sd_liheap.yaml Case 5, as in the siblings.
G6: the base payment's size gate read spm_unit_size Fixed sd_liheap_base_payment returns where(supported_region, amount, 0) with no size gate. Base-payment Case 5 lost its size-0 column, and Cases 1–4 and 7 lost the unused spm_unit_size inputs. A unit with no counted member stays at $0 through has_qualified_member, which eligibility Case 3's undocumented row now pins with sd_liheap: 0 (Pavel's G15).
G7: PR body, changelog and issue housekeeping Fixed Changelog. "Add South Dakota LIEAP regular heating assistance with FY2024-FY2026 income limits, county heating regions, direct and heat-in-rent payments, and mixed-status households." Registry. The entry is "South Dakota LIEAP" ("South Dakota Low Income Energy Assistance Program"), partial, with a two-sentence note. Body. The draft sentence is gone; it says every case except the four household-size cases sends heating_type; it has conflict rows for cash gifts and for manual page 13's SNAP income times three. Issue. #1539 says "open, not merged". It keeps "not run locally; CI validates them", which matches the team's convention (as accepted in #9970).
G8: test and comment hygiene Mostly fixed; one point declined (accepted) Fixed. A grep finds no squashed numbers, no "approved" wording and no sources/sd_county_regions.json. The copied file headers are replaced. Declined. heating_type stays in the isolated limit and threshold tests. That is harmless, and the author is right that sd_liheap_base_payment reads it.

Pavel's review of cd74fd52d6 (2026-10-19). Every item is fixed:

Pavel's item Status Evidence
C1: kerosene paid the full Fuel Oil cell by check Fixed The rule. Plan item 9.1, FY2026 page 22 (which I read this round): "Do you make payments directly to home energy suppliers?" Heating: Yes; exceptions: "If household uses coal or wood as it's primary heat source or a household's heat is included in rent, a payment is issued directly to the household." ARSD 67:15:01:40 names only "wood or coal", and ARSD 67:15:01:43.01 pays "the energy supplier or landlord". EA-297 page 4 asks for a supplier fill ticket for "Fuel Oil/Kerosene" and pays only "Heat in Rent, Wood, or Coal" to the household. The code. V/sd_liheap.py:36 is (heating_type == fuels.WOOD) | (heating_type == fuels.COAL), so kerosene takes min_(base_payment, expense) at line 42. The comment (lines 26-35) cites plan 9.1, both rules and EA-297 page 4, and says manual page 31 is superseded for kerosene. reference adds plan #page=22 and both rules. The tests. sd_liheap.yaml Case 2 (line 29) is [742, 742, 1]. Integration Case 2 caps Brown County kerosene at $900 of fill tickets. Case 8 (2023) keeps min(1,905, 3,000) = 1,905. Severity. My round-1 G1 rated this a suggestion on EA-297, ARSD :40 and the DSS page alone, and I called the reading ambiguous. Plan item 9.1 is the state's own list of exceptions for every claimed year, and with ARSD :43.01's supplier default it settles the question. Pavel's escalation was right, and the fix follows the plan and rules.
G9: DLR citations without URLs Fixed Case 22 cites SDCL 62-4-3 and the DLR weekly rate table at #page=2; Case 23 cites SDCL 61-6-2 and the DLR fact sheet at #page=2.
G10: Case 23 could not tell the raw from the total UI input Fixed See S1.
G11: NONE described as a coverage gap Fixed Base-payment Case 5 (line 71) is renamed, and its comment cites ARSD 67:15:01:06 and :09 for NONE.
G12: $22,014 in the limit comment; the sizes 4–6 note Fixed floor(0.6 × 111,183) = 66,709; floor(66,709 × 1.32) = 88,055; 88,055 / 4 floors to 22,013, as V/eligibility/sd_liheap_income_limit.py:41-43 now says. Sizes 4 and 5 give 16,677 and 19,345. P/income/limits/fpg_upper_rate.yaml:7-9 notes that plan page 9 prints 60% for sizes 4–6 and that DSS publishes 200% of the guideline.
G13: short FY2021 plan URL Fixed P/payment/direct.yaml:9-12.
G14: 42 U.S.C. and the archived DSS table Fixed P/income/limits/fpg_lower_rate.yaml:20-21 (#b_2_B) and P/income/fpg_year_lag.yaml:13-14.
G15: end-to-end pins Fixed Eligibility Case 3 (line 41) adds Aurora County and sd_liheap: [938, 938, 938, 0]. sd_liheap.yaml Case 14 adds the cash-help case.
G16: no integration file Fixed T/integration.yaml, five FY2026 households with 11 outputs each (hand-checked below).
G17: style Fixed Eligibility Case 2 (booleans only) drops absolute_error_margin. Every literal of four or more digits outside case names uses underscores.

Pavel's review of this head (f88aa84a78, 2026-10-20). He approved. His four new suggestions are open and optional:

Pavel's item Status Evidence
G18: heat in rent over the wood, coal and kerosene routes is unpinned Open; concur (suggestion) V/sd_liheap.py:46-50 checks heat in rent last, overriding the direct route, which is right: plan 9.1 lists heat in rent as its own exception and ARSD 67:15:01:41.02 prices it from rent. No case combines heat in rent with WOOD, COAL or KEROSENE (the heat-in-rent units in sd_liheap.yaml Case 4, eligibility Case 2 and integration Case 4 are UNSPECIFIED, NATURAL_GAS, ELECTRICITY or NONE), so a refactor that tests cash_payment first would pass. His proposed Case 15 expects [800, 800, 800] (Custer: min(800, 6,000 / 12 × 7 × 0.3 = 1,050)).
G19: wood or coal with no purchased-fuel bill Open; concur, check the rule first The cash route pays the full cell whatever heating_expense is. If the case is added, check it against ARSD 67:15:01:09 first. That rule's proof of liability is "A bill or statement from the energy supplier that a household has a heating expense account" or a landlord statement. So a household that gathers its own wood may have no proof, and documenting the input override may fit better than pinning $742.
G20: blank lines before two comment blocks; #page=1 on the one-page matrix URL Open; concur (style) The comments at T/sd_liheap.yaml:25 and T/eligibility/sd_liheap_eligible.yaml:38 follow the previous case's output with no blank line.
G21: sources for $202.90 and the UI fallback comment Open; concur T/integration.yaml Case 2 states the 2026 Part B premium without a link. The Case 23 comment could say that South Dakota UI is not modeled, so the input stands in for the fallback.

New findings

Critical (must fix)

None.

Should address

None.

Suggestions

1. After #10182, count military SBP survivor annuities through military_retirement_income.

  • Where: P/income/sources/unearned.yaml:11 lists military_retirement_pay, with pension_income at line 7 and survivor_benefits at line 13. The comment at lines 8-10 says military retirement pay is listed separately from the pension inputs.
  • What changed on main: Wire military retirement pay and survivors into federal and state tax (#10181) #10182 merged on 2026-10-20, after this head's last merge at 1661d715bd.
    • At this head, military_retirement_pay is documented as "The benefits received under a United States military retirement plan, including survivor benefits."
    • On main it is taxable retirement pay of former members. SBP annuities go in military_retirement_pay_survivors ("Military retirement survivor benefit payments").
    • military_retirement_income adds both. pension_income does not include the new input.
  • The rule counts these payments.
    • Manual page 25 counts "Any non-lump sum gross annuity, pension, and retirement payments" (Y).
    • FY2026 plan item 1.9 checks "Retirement / pension benefits".
    • EA-297 page 5 lists "*Retirement, *Pensions, *Annuities".
  • Effect on the merged tree (worked out from the code, not run). A surviving spouse aged 62 in Custer County has $24,000 a year of SBP annuity entered in military_retirement_pay_survivors, natural gas heat with a $1,200 bill and no other income, in 2026:
  • Why a suggestion, not should:
  • Fix, if taken here:
    • Replace the entry with military_retirement_income, and update the lines 8-10 comment.
    • Add a military_retirement_pay_survivors amount to income Case 15, or add a separate case. Case 15's 35,000 is unchanged by the swap itself.
    • Update the countable-income input list in rulespec-us#1539.

2. After #10169, count South Dakota's State Supplemental Payment (sd_ssp).

  • Where: P/income/sources/unearned.yaml lists ssi (line 6) but not sd_ssp. Unlike Add Oklahoma LIHEAP regular heating assistance #9970 and Add Rhode Island LIHEAP regular heating assistance #9972, no comment, body row or test comment says the supplement is unmodeled, so the fix is the list entry and a test.
  • What changed on main: Add South Dakota state supplementation for 2026 #10169 merged on 2026-10-20 (2f9c830f15), after this head's last merge.
    • It adds sd_ssp, a monthly, person-level payment gated to South Dakota. It is the sum of sd_ssp_independent and sd_ssp_care.
    • sd_ssp_independent pays $15 a month to an SSI recipient aged 18 or over, in their own or another person's household, not a student and not in care, with no other income. An eligible couple gets $8 and $7. ARSD 67:12:14:04 sets the amounts.
    • sd_ssp_care covers certified assisted living and adult foster care residents. Most of them fall outside LIEAP's independent-household scope, so the $15 payment is the case that matters.
    • Add South Dakota state supplementation for 2026 #10169 lists sd_ssp in SNAP's and HUD's unearned sources, household_state_benefits and spm_unit_benefits.
  • The rule counts these payments:
    • EA-297 page 5, which the unearned list already cites, says: "REPORT GROSS (amount before deductions) INCOME *Wages, *Self-employment, *Child Support, *Alimony, *Social Security, *SSI, *SSI State Supplement, *BIA GA, *TANF, ..."
    • ARSD 67:15:01:01(4) defines gross income as "earned and unearned income before deductions".
    • None of ARSD 67:15:01:18's 22 exclusions covers a recurring state supplement. (16) excludes only "One-time payments from a welfare agency", and manual page 27 lists "one time payments from welfare agency" among the excluded lump sums.
    • Manual page 28 counts "Social Security/SSI" gross, less Medicare premiums.
    • rulespec-us#1539 already quotes the EA-297 line, including "*SSI State Supplement".
  • Effect on the merged tree (worked out from the code, not run). Take a Custer County household with natural gas heat and a $2,000 bill, in 2026:
    • The household. A parent aged 50 earns $30,300. Her disabled daughter, 25, receives SSI of $994 a month ($11,928, the 2026 federal rate, entered as an input) and has no other income of her own.
    • The daughter's sd_ssp. She is in the default own-household arrangement, not a student and not in care, so sd_ssp_independent is $15 a month, or $180 a year.
    • The thresholds. Size 2: limit $42,300 (200% of $21,150), Mid $26,436.
    • Merged tree: 30,300 + 11,928 = 42,228, at most 42,300, so eligible in tier 2. Region 1 gas: min(792, 2,000) = $792.
    • Counting sd_ssp: 42,228 + 180 = 42,408, above 42,300, so $0.
    • No existing case changes. Income Case 9's SSI adult has $6,000 of wages, and its SSI child is 8, so neither gets sd_ssp.
  • Why a suggestion, not should:
  • Fix, if taken here:

Verified

  • Mutation check (item 13), reasoned from the tests and code at the head:

    • Kerosene's supplier route. Putting KEROSENE back in cash_payment fails sd_liheap.yaml Case 2 (2,858 instead of 1) and integration Case 2 (3,093 instead of 900). Pavel confirmed this by a mutation run.
    • financial_assistance. Dropping it fails income Case 8 (12,000) and sd_liheap.yaml Case 14 (eligible at $27,000, $792).
    • total_unemployment_compensation. A raw reader fails income Case 23 (20,800).
    • has_qualified_member (the only size guard since G6). Without it, eligibility Case 3's undocumented adult would be eligible: limit 0, income 0, threshold 0, tier 1, $938.
    • Not pinned: heat-in-rent precedence over the wood and coal route (Pavel's G18) and wood or coal with a $0 bill (G19). Both are orderings or added conditions, not removed branches.
    • Unchanged since round 1: the other branches. Round 1 confirmed them by mutation runs.
  • State-gated keys (item 15, NH Add New Hampshire state supplementation for 2026 #10156).

    • No new keys. sd_liheap_region gained only a documentation string.
    • Index safety. The region is still clipped to 1–4 before it indexes payment/direct and heat_in_rent/amount, and the tier is always 1 or 2.
    • Mixed-state tests. The ND and CA rows still run beside SD rows.
  • Integrate modeled state unemployment insurance into income flows #9558 (item 11). The guard's four tests pass at the head, and no South Dakota LIEAP file reads raw unemployment_compensation.

  • Document disjoint health premium inputs and correct consumer treatment #10046 (item 14) and the wage lessons (OK Add Oklahoma state supplementation for 2026 #10165, NH Add New Hampshire state supplementation for 2026 #10156 round 3).

    • Gross wages. Wages come from employment_income, matching EA-297's "REPORT GROSS (amount before deductions)" and ARSD 67:15:01:01(4). They do not come from ssi_earned_income.
    • Premiums. The only premium netted is medicare_part_b_premium, against Social Security (ARSD 67:15:01:18(12)). So Document disjoint health premium inputs and correct consumer treatment #10046 does not reach South Dakota LIEAP.
  • Federal 110% floor (42 U.S.C. 8624(b)(2)(B)). It never binds.

  • IIJA and FY2027 (item 12).

    • IIJA. No supplement, as in round 1.
    • FY2027 sources. On 2026-10-20 the DSS page still shows "2025-2026 Heating Season Income Limits", and the Clearinghouse returns 404 for a South Dakota FY2027 plan and matrix. The draft FY2027 plan (status "Saved") remains the only FY2027 source, documented and unclaimed.
  • Other main commits.

  • New and changed expectations, checked by hand.

    • FY2026 inputs. The 2025 guideline is $15,650 plus $5,500 per added person. Limits: 31,300 / 42,300 / 53,300 / 64,300 for sizes 1–4 (DSS three-month maxima × 4). Mid: 19,564 / 26,436 / 33,312 / 40,188 (125% of the guideline over 4, rounded half up, × 4).
    • Regions under ARSD 67:15:01:35, confirmed in the region files: Custer and Lincoln 1; Aurora and Pennington 2; Minnehaha 3; Brown and Harding 4.
    • sd_liheap.yaml:
      • Case 2 (changed): income 0, so tier 1 in region 1. Coal & Wood 742 is paid in full, twice. Kerosene reads Fuel Oil 2,858, then min(2,858, 1) = 1. Result: [742, 742, 1].
      • Case 8 (comment only): kerosene min(1,905, 3,000) = 1,905, unchanged.
      • Case 14 (new): 27,000 + 6,000 = 33,000 > 31,300, so [33,000], [false], [0].
    • eligibility/sd_liheap_eligible.yaml:
      • Case 3 (award outputs added): Aurora is region 2. Income 0 gives tier 1 gas 938, and min(938, 2,000) = 938 for the citizen, permanent resident and refugee. The undocumented adult has size 0, so ineligible and 0. Result: [938, 938, 938, 0].
      • Case 2: margin removed; booleans unchanged.
    • income/sd_liheap_countable_income.yaml:
      • Case 8 (changed): 14,400.
      • Case 23 (new): total UI 6,400 and 27,200. The DLR arithmetic: 13 × 800 = 10,400; 10,400 / 26 = 400 a week; 16 weeks gives 6,400.
    • payment/sd_liheap_base_payment.yaml Case 5 (column removed): region 0, region 5, NONE, SOLAR, OTHER, UNSPECIFIED and ND give [0 ×7].
    • integration.yaml (new):
      • Case 1: gas bill 1,200; size 3. Wages 11.85 × 2,080 = 24,648, within 53,300 and at most Mid 33,312, so tier 1. Minnehaha region 3 gas 1,007, and min(1,007, 1,200) = 1,007. Heat in rent 0.
      • Case 2: KEROSENE reads fuel_oil_expense (900). Countable 24,852 − 2,434.80 = 22,417.20 (2,071 × 12 less 202.90 × 12), within 31,300 and above Mid 19,564, so tier 2. Brown region 4 Fuel Oil 3,093, and min(3,093, 900) = 900.
      • Case 3: size 2. 24,648 is within 42,300 and at most Mid 26,436, so tier 1. Harding region 4 Coal & Wood 854 is paid in full on a 500 bill.
      • Case 4: heat in rent with UNSPECIFIED fuel. heating_expense is 0 and has_heating_expense false, but positive rent makes the household responsible. 12,324 (11.85 × 1,040) is within 31,300. Base 0. Heat in rent: 9,000 / 12 × 0.3 × 7 = 1,575, so Pennington region 2 caps it at 800.
      • Case 5: size 4. 41,600 + 12,324 = 53,924, with the 17-year-old's 4,000 excluded (ARSD 67:15:01:18(1)). That is within 64,300 and above Mid 40,188, so tier 2. Lincoln region 1 Propane 2,249, and min(2,249, 1,800) = 1,800.
    • Changed inputs, unchanged outputs:
      • The cases: base-payment Cases 1–4 and 7 drop spm_unit_size, which no South Dakota formula reads now.
      • Comment and formatting edits. Every other test file changed only comments and digit separators. I compared the old and new values.
  • Inputs not checked. I didn't re-derive these amounts; they are inputs only, and every expectation follows from them:

    • the $11.85 minimum wage;
    • SSA's $2,071 average benefit;
    • the $202.90 Part B premium;
    • the DLR weekly ranges.

    Pavel checked each of them against its source.

  • Wiring.

    • Registry. The programs.yaml SD entry is partial with no verified_years, and test_programs_registry passes.
    • Aggregates. No aggregate wiring, which matches every merged LIHEAP state.
    • Gating. defined_for = StateCode.SD is on eight variables, and sd_liheap is gated by sd_liheap_eligible.
    • Changelog. changelog.d/sd-liheap.added.md is top level and typed added.
  • Partner tests. The PR touches none, and Household API Partners passes.

Checklist results (LIHEAP wave 2a)

  1. State plan and manual: pass. Encoded matrix, limit, threshold and region values are unchanged since round 1. I read plan item 9.1 (FY2026 page 22), ARSD 67:15:01:01, :09, :18 and :40, manual pages 12, 24-28 and 31, EA-297 pages 4-5 and the DSS page (live and the January 1, 2026 capture) for the fixes and Suggestions 1–2.
  2. Income definition: pass.
  3. Backdating: pass for the claimed years (FY2024–FY2026). Earlier years and FY2027 are documented, unclaimed estimates (G4).
  4. Household and arrangement rules: pass. Kerosene now follows the supplier rule. Wood and coal are paid in full, heat in rent is priced from rent, and the unpriced fuels stay eligible at $0 as documented. The heat-in-rent ordering against the wood and coal route is unpinned (Pavel's G18).
  5. Tests: pass. 78 cases in 10 files; every new or changed expectation was checked by hand. Periods are plain.
  6. Descriptions: pass. All 19 follow the template.
  7. References: pass. One entry per source, # PDF pages comments over multi-page hrefs, the plan 2.1 and 9.1 anchors and the archived DSS capture. #page=1 on the one-page matrix is optional (G20).
  8. Wiring: pass. Registry, no aggregate (as in the siblings), defined_for gating, and no take-up beyond tanf's.
  9. Axiom: valid queued line. Presence of a complex reform causes US income tax inaccuracies #1539 matches the head (see below).
  10. Branch and CI: 29/29 green. A clean merge with main; the siblings conflict only in programs.yaml.
  11. Integrate modeled state unemployment insurance into income flows #9558: fixed. The guard passes, and Case 23 pins the modeled path.
  12. IIJA / FY2027: not applicable (no supplement). No FY2027 matrix, final plan or DSS table is posted.
  13. Mutation check: pass. Every new branch is pinned; Pavel's G18 and G19 are optional.
  14. Document disjoint health premium inputs and correct consumer treatment #10046 health premiums: not applicable. Gross wages, and Part B netting only.
  15. State-gated index defaults: guarded. The region is clipped, and mixed-state rows still run.
  16. Wire military retirement pay and survivors into federal and state tax (#10181) #10182 survivor annuities: Suggestion 1.
  17. Merged state SSPs: Suggestion 2. EA-297 page 5 lists "*SSI State Supplement" as gross income, and sd_ssp is missing from the list.
  • Federal 110% floor: never binds (at least 1.4999 times the lagged guideline), so no floor parameter is needed.

Axiom line assessment

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

  • Issue state.
    • Presence of a complex reform causes US income tax inaccuracies #1539 is open and labelled pe-parity and enhancement. It was last updated 2026-10-19 21:03 UTC and has no comments.
    • rulespec-us main (5796861e58, 2026-10-20) has no South Dakota DSS or LIEAP module: us-sd/policies/ holds only cms and income_tax. So queued is still the right form.
    • It records its blockers: sources that need ingest, an unchecked signed release, encoder approval and no reviewed PE revision.
  • Dispatch-ready. It has:
    • module paths with corpus status;
    • the verbatim ARSD, plan, matrix, manual, application and DSS text;
    • required outputs for every variable;
    • companion tests hand-derived from the sources and mapped to PE cases.
  • Matches the head f88aa84a78:
    • Cases: 78 cases in 10 files, listed per file.
    • Countable income: it names total_unemployment_compensation and financial_assistance.
    • Kerosene: interpretation 4 and companion tests 3, 7 and 15(b) cap kerosene at unpaid charges under plan 9.1 and ARSD :40 and :43.01.
    • Cash gifts: interpretation 9 records the rule-over-plan reading.
    • Integration: companion test 15 covers integration.yaml.
  • Updates it would need if the suggestions are taken: the countable-income inputs (military_retirement_income, sd_ssp). The issue already quotes EA-297's "*SSI State Supplement".

CI status

29 pass, 0 pending, 0 failed (run 39324256958 on head f88aa84a78, pull_request, 2026-10-19 21:02 UTC to 2026-10-20 02:24 UTC). I re-checked gh pr checks 9974 at the end of this review.

  • Quick Feedback ran all ten South Dakota LIEAP files at the head: 7 + 4 + 5 + 23 + 5 + 7 + 8 + 3 + 2 + 14 = 78 passed.
  • Rest (Python, shard-1) passed:
  • Also passed: Household API Partners, Microsimulation, both Rest jobs, all 11 Baseline shards, all 9 Contrib shards, PR validation, Package and compatibility, and codecov patch and project.
  • Locally: no tests or probes were run. Both suggestions are hand traces of the code on main.

Branch status

Next steps

Nothing blocks merge. Optionally:

  1. Pavel's G18–G21 (the heat-in-rent precedence case, the wood-without-a-bill check against ARSD 67:15:01:09, style and comment sources).
  2. Suggestion 1 (military_retirement_income) and Suggestion 2 (sd_ssp), with rulespec-us#1539's input list.

0 new critical and 0 new should findings, plus 2 new suggestions. Open at this head: 0 critical, 0 should and 6 suggestions (Pavel's G18–G21 and Suggestions 1–2).

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.

Round 2: all four should-address items from round 1 are fixed, including counting regular cash help from family per ARSD 67:15:01:18. Six open suggestions: #10182 survivor annuities, sd_ssp now that #10169 has merged, and Pavel's optional G18-G21. Full report in the comment above.

# Conflicts:
#	policyengine_us/programs.yaml
@hua7450
hua7450 merged commit 8ed1375 into main Oct 20, 2026
1 check failed
@hua7450
hua7450 deleted the sd-liheap branch October 20, 2026 21:21
MaxGhenis added a commit that referenced this pull request Oct 21, 2026
…seline (#10334)

The guard (#10287) took its KNOWN_UNMARKED baseline at 0f21a63. FL LIHEAP
(#9969) and SD LIHEAP (#9974) merged before the guard did, each with
comments the ratchet reads as unmarked source conflicts, so
test_conflict_sounding_comments_do_not_grow fails on main from ce9377e.
These five files were on main when the guard landed, so they belong in the
baseline, one comment each.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 22, 2026
…10349)

* Point the FL and SD LIHEAP conflict candidates at their code on main

#10287 wrote these candidates while #9969 (FL) and #9974 (SD) were open, so
each said its code was not on main. Both have merged. Each candidate now
lists the files that decide it, and its note says which source that code
follows, as read on main. One note changes substance: SD's merged code caps
kerosene at unpaid charges like fuel oil, following the plan and rules, not
the 2018 manual's full payment by check. The SD 2025 table candidate gets the
archived DSS page as its source.

Two FL conflicts recorded in the merged code had no candidate: the FY2026
plan's payment estimate against the October 2025 matrix, and manual part
900.01D against parts 1100.03D and 1200.01A and contract G(7)d on the income
limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add the FL floor and minors' income candidates; reframe the SD limits

From DTrim99's review and a Sol review of #10349.

- Two more conflicts the merged FL code records had no candidate. The first is
  the federal 110% poverty floor against the 60% SMI limit from household
  size 15 (statute against guidance; PE-US follows the statute). The second is
  minors' income: the current application and portal FAQ against manual part
  1200.01D. Every value is checked against the merged code and its tests: the
  floor case's $98,320 limit and $101,915 floor are fl_liheap.yaml's Case 27.
- The SD income-limit entries credited ARSD 67:15:01:10 with a limit formula
  it doesn't contain; it sets only the three-month income period. FY2025 is
  now the plan's 60% of SMI against the published table's 200% of the 2024
  guideline for sizes 1-3: $32,984 against $30,120 for one person, as
  table/amount.yaml records. FY2026 is DSS's 200% of the 2025 guideline
  against the plan's 60% of SMI. Its $22,013 is the model's flooring, and
  $22,014 is unrounded.
- Anchors and sources:
  - FY2026 plan item 2.1 is on page 8, not 4.
  - Contract G(8)c is on page 12, not 9.
  - The null URLs are filled from the merged code's references.
  - The FL manual-sections entry gets the plans, the matrix and the manual's
    "current matrix" page.
  - Kerosene gets plan item 9.1 and ARSD 67:15:01:43.01.
  - The FL size-13 and SD 2026 entries list the remaining deciding files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Cite the FY2025 FL LIHEAP matrix; list the floor candidate's lag and band files

Review nits from the round-2 review of #10349:
- us-fl:liheap-income-limit-manual-sections now cites the FY2025 payment
  matrix the code follows.
- us-fl:liheap-income-floor-vs-smi-limit lists fpg_year_lag.yaml and
  fl_liheap.py, which its numbers rest on, and says the band 4 base is
  the model's estimate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement South Dakota LIEAP regular heating assistance

3 participants