Skip to content

Add Oregon LIHEAP regular heating assistance - #9743

Merged
hua7450 merged 18 commits into
mainfrom
or-liheap
Oct 9, 2026
Merged

hua7450 merged 18 commits into
mainfrom
or-liheap

Conversation

@hua7450

@hua7450 hua7450 commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Adds Oregon's regular LIHEAP heating assistance for program years 2025 to 2027, including income eligibility, county/fuel payment tables, heat-in-rent treatment and the half payment for separate roomer/boarder/owner applications. In FY2026 a one-person Multnomah County household with $12,000 countable income and gas heat receives $520.

Fixes #273.

Implementation

  • Encodes each year's manual matrix: PY2025 (effective October 1, 2024), the PY2026 matrix as corrected by temporary order OHCS 44-2025 (October 9, 2025) and PY2027 (effective October 1, 2026). The Clearinghouse's "2026" file reprints the PY2025 amounts under FY2026 income ranges. Fuel oil, propane and wood vary by region in PY2025, so all five tables carry the region level.
  • Reuses hhs_smi. Ceiling of 60%, applied to the product rounded to cents in float64, reproduces all 36 published limits for 2025 to 2027. Payment-band boundaries remain published parameters because a single formula does not reproduce them. The final band extends to the eligibility ceiling where the tables differ by $1.
  • Counts the SPM unit as the economic household, assuming identity/SSN verification. No SNAP immigration test or asset test is introduced. TANF income uses the annual tanf receipt variable, which applies take-up, rather than Oregon's monthly entitlement.
  • Earned and unearned source lists are parameters. Earned income uses the existing net business inputs (including partnership and S-corporation income) without further self-employment or work-expense deductions; unearned income includes farm rents, military retirement pay and non-Social-Security survivor benefits (manual pages 44 and 48). Every source is floored at zero for each member, so rental and estate losses are treated like business losses and one member's loss does not offset another's income: the manual sends landlords to the self-employment form (page 35). The $200 interest test is applied per member.
  • Private-disability income represents the insurer's actual payment after its SSA offset. Social Security is then counted separately; both official examples are tested without an extra deduction or input.
  • The program is fuel blind (page 5): kerosene is paid from the heating oil column and a grid-tied solar home from the electricity column, the pairing heating_expense uses. Coal, other and unspecified fuels have no matrix column and pay $0 as a coverage gap; the unspecified default is tracked in Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754.
  • Subsidized heat-in-rent households are excluded under the page 93 energy burden table and OAR 813-200-0020(1)(b). Other heat-in-rent households use the building's known fuel. The sole new empty input is the state dwelling arrangement.
  • Uses undated formulas and verified_years: "2025-2027"; earlier years are unverified backfilled estimates.

Remaining coverage

A county reported as UNKNOWN and sizes above 12 do not make or_liheap_eligible false: the benefit is zero because the payment tables do not cover them. A missing county input falls back to Baker County (Region 2), the repo-wide county default. Institutional residence, tribal duplicate awards and detailed income exclusions remain partial.

Minimum-delivery Fuel Payments and agency supplements are regular-assistance gaps, not crisis components. Their amounts depend on delivery requirements and local decisions that existing annual expenses do not identify. No uniform amount is invented. Cooling, crisis and equipment assistance remain excluded.

Microsimulation: the datasets carry no heating inputs, so population totals are $0 until the default heating type changes. That change affects API partners and is tracked separately in #9754; callers must send heating_type with the matching fuel bill.

Validation

40 program YAML cases. The FY2026 matrices are swept at every band for the even household sizes, the odd sizes 1, 3 and 5 are tested at their printed band tops, and the FY2026 limits are asserted for every size; PY2025 and PY2027 cases pin the regional heating oil split, the size-1 and size-12 rows and band tops, and the whole-dollar FY2027 limits. Other cases cover boundaries, housing and public housing, the age-18 boundary, mixed households, TANF take-up, both private-insurance examples, rental and estate losses including a two-member rental offset, per-member interest, the missing-county fallback and farm rental and survivor income over the limit. All cases specify heating_type. A simulation that mixes an eligible Oregon household with households in other states does not raise (two-state case).

Official sources

axiom: TheAxiomFoundation/rulespec-us#1479 queued

@codecov

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

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9743   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         8    +4     
  Lines           76       126   +50     
  Branches         2         2           
=========================================
+ Hits            76       126   +50     
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 7 commits October 3, 2026 01:52
…tic tests

Floor the income band before the payment-table lookup so units outside Oregon index a valid key in mixed-state simulations, and add a two-state case. Place Oregon between IL and TX in programs.yaml. Give the integration cases plausible wages and heating bills with derivations from the PY2026 manual.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Write parameter date keys unquoted, wrap each variable's documentation
string across lines, add digit separators to test values, name and
comment every test case against the manual, and split the household-size
sweeps so each size is asserted once: odd sizes in the income-limit test,
even sizes in the benefit-matrix tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@hua7450
hua7450 marked this pull request as ready for review October 6, 2026 12:51
@hua7450
hua7450 requested a review from DTrim99 October 6, 2026 12:51
@DTrim99

DTrim99 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Oregon LIHEAP regular heating assistance

PR #9743: Add Oregon LIHEAP regular heating assistance (author: hua7450, head 06b38abd60)

Scope: Oregon LIHEAP, run by OHCS, regular (standard) heating payment only. Cooling, crisis, the Fuel Payment, equipment repair and OEAP are out of scope. The PR changes 32 files: 16 parameters, 8 variables (7 formulas and the new or_liheap_dwelling_type input), 6 test files (29 cases), a changelog fragment and programs.yaml. Every parameter has a single 2025-10-09 entry (the OHCS 44-2025 correction filing; the manual itself took effect 2025-10-01). Every test uses period: 2026, and the registry lists verified_years: "2026".

Overall: the FY2026 implementation is accurate. All 480 FY2026 matrix lookups match the PY2026 manual, and so do the 36 band tops, 12 income limits and 36 county assignments. The countable-income lists are self-contained, so there is no SNAP-list reuse and no #9672 exposure. Two problems are critical:

  • C1: period 2025 carries the FY2026 matrix and bands back over the published PY2025 matrix. This is the same pattern as Add Indiana EAP regular heating assistance #9728 C1.
  • C2: kerosene and solar households are eligible but receive $0 by construction, although the manual is fuel blind and the PR's own test comment says these fuels are billed as heating oil and electricity.

The six should-address items cover:

  • the FY2027 carry-forward;
  • a floating-point ceil error that makes six FY2027 limits $1 too high;
  • two missing income sources;
  • asymmetric treatment of rental and self-employment losses;
  • tests that cover less than the PR body claims;
  • four wrong page anchors.

No regulatory or code finding was downgraded after re-verification. One proposed fix was corrected: rounding to cents on the float32 hhs_smi array does not repair S2 (see S2). Duplicate findings from the regulatory and code passes are merged.

Path prefixes used below:

  • P/ = policyengine_us/parameters/gov/states/or/ohcs/liheap/
  • V/ = policyengine_us/variables/gov/states/or/ohcs/liheap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/or/ohcs/liheap/

Line numbers refer to head 06b38abd60.

Source Documents

# Document Link Use
1 PY2026 Intake, Operations & Policy Manual (95 pp; "Effective October 1, 2025 – September 30, 2026", p. 2; PDF page = printed page). The PR's only cited source M26 Limits p. 77, matrices pp. 78-81, income chart pp. 37-54, payment types pp. 61-62, 67, energy burden table p. 93
2 PY2025 manual ("Effective October 1, 2024 – September 30, 2025"; limits and matrices on PDF pp. 57-59, printed 5.1-5.3) M25 Not cited in the PR. It governs the period the PR backfills (C1)
3 PY2027 manual ("Effective Oct.1, 2026 – Sept. 30, 2027", p. 2; adopted by temporary rule OHCS 29-2026) M27 Limits p. 71, matrices pp. 72-75 (S1, S2)
4 FFY2026 LIHEAP state plan (OR) OR_Plan_2026.pdf §1.9 countable income (pp. 5-6), §2.1 60% SMI (p. 8), §2.6 $250/$750 (p. 9)
5 FFY2025 LIHEAP state plan (OR) OR_Plan_2025.pdf Same 60% SMI and $250/$750
6 Clearinghouse "2026" heating/cooling matrix (dated 2025-08-22) OR_BenefitMatrix_Heat-Cool_2026.pdf FY2026 bands with PY2025 amounts; the PR correctly uses the manual instead
7 OAR 813-200-0005 and -0020 0005, 0020, SOS division 3649 Definitions, the energy-burden requirement, institutions; manual adoption history (0008)

The OHCS 41-2025 and 44-2025 filing attachments could not be retrieved, so the "October 9, 2025 rule filing" cited in the PR body is not verified directly. Nothing below depends on it.

Critical (Must Fix)

C1. Period 2025 carries the FY2026 matrix and income bands back over the published PY2025 matrix

  • Code: every amount and band parameter has one 2025-10-09 entry:

    • P/payment/amount/electricity.yaml:2-111 and natural_gas.yaml:2-111 (region × size × band)
    • P/payment/amount/fuel_oil.yaml:2-55, propane.yaml:2-55 and wood.yaml:2-55 (size × band, no region level)
    • P/payment/income_band.yaml:4-78
  • What happens: period 2025 resolves at 2025-01-01, which falls inside FFY2025. policyengine-core extends a parameter's first value backward, so period 2025 gets the FY2026 amounts and band tops. The income limit is the exception: hhs_smi has its own FFY2025 value, so period 2025 pairs the correct FY2025 limits with FY2026 bands and amounts.

  • What the PR says about 2025:

    • V/or_liheap.py:13-15: "Earlier years use parameter backfilling and are unverified historical estimates".
    • programs.yaml:876-877 gives verified_years: "2026" with the note "earlier years are unverified estimates". The PR body says the same.
    • Labelling these values as estimates does not help, because the governing PY2025 schedule is published and differs.
  • Sources:

    • M25, PDF p. 58, "2025 Benefit Matrix for Region 1", size 1:
      • Row "$0 $9,203": Electricity "$380", Heating Oil "$750", Liquid Gas "$750", Natural Gas "$500", Wood/Pellets "$750".
      • Row "$9,204 $18,406": "$295 $750 $715 $390 $685".
      • Row "$18,407 $27,609": "$250 $680 $595 $325 $570".
    • M25, PDF p. 59, Region 2, size 1, row "$18,407 $27,609": "$270 $745 $650 $355 $625". Heating Oil, Liquid Gas and Wood/Pellets differ between the regions in PY2025.
    • The PR's band-1 values for Region 1, size 1 are $465 / $750 / $715 / $670 / $715, and its band-1 top is $9,597 (M26 p. 78).
  • Extent: 310 of 480 PY2025 amount lookups (2 regions × 12 sizes × 4 bands × 5 fuels) and all 72 band-top lookups differ from the PR's values.

  • Head trace, period 2025, Multnomah, size 1:

    Case PR (period 2025) PY2025 manual
    Electricity, income $9,400 band 1, $465 band 2 ($9,204-$18,406), $295
    Electricity, income $0 $465 $380
    Natural gas, income $0 $670 $500
    Income limit $36,811 $36,811 (M25 PDF p. 57)
  • Precedent: Add Indiana EAP regular heating assistance #9728 C1 had the same setup: single-dated FY2026 values, verified_years: "2026", and a published prior-year matrix that the backfilled values contradicted. It was rated critical and fixed by adding prior-year entries. This PR is consistent with that precedent, and the PY2025 gap here is larger.

  • Fix, either option is acceptable:

    1. Encode PY2025 (preferred).
      • Add 2024-10-01 entries from M25 pp. 58-59 to the five amount tables and to income_band.yaml.
      • The structure has to change. In PY2025 all five fuel columns vary by region, so the region-free fuel_oil, propane and wood files cannot hold the PY2025 values. Give them the same leading region level that electricity and natural_gas have, with breakdown range(1, 3). Repeat the identical FY2026 values in both regions, and index them as p.amount.fuel_oil[region][size][band] and so on at V/or_liheap.py:40,41,43.
      • The size-6 payment cap, the 12-size table, the region county lists and the 60% SMI rate are the same in PY2025 (M25 pp. 57-59), and the age-18 and roomer rules appear unchanged. Those parameters need no new entry.
      • Add a period-2025 test. Suitable cases: Multnomah, size 1, electricity, $9,400 gives $295; natural gas at $0 gives $500; Deschutes, size 1, heating oil at $20,000 gives $745 against Multnomah's $680, which pins the regional split.
      • Update V/or_liheap.py:13-15, the registry note, and verified_years (for example "2025-2026").
    2. Explicitly start the program in FY2026. Add a dated start-date gate, for example an in_effect boolean that is false before and true from 2025-10-01, and apply it in or_liheap_eligible. Periods before FY2026 then return $0 instead of FY2026 amounts. Say so in the variable documentation and the registry notes, and add a period-2025 test that expects 0.

C2. Kerosene and solar households are eligible but receive $0 by construction, though the manual is fuel blind and the PR's own test says these fuels are billed as heating oil and electricity

  • Code: V/or_liheap.py:30-47 selects only ELECTRICITY, FUEL_OIL, PROPANE, NATURAL_GAS and WOOD, with default=0. The comment at :45 reads "No published column for unspecified, other, coal, kerosene or solar."
  • Why these households are eligible: the repo's canonical heating_expense reads fuel_oil_expense for FUEL_OIL | KEROSENE and pre_subsidy_electricity_expense for ELECTRICITY | SOLAR, with the comment "Solar-primary homes are assumed grid-tied with electric supplemental heat". So has_heating_expense is true, or_liheap_eligible is true, and or_liheap is $0.
  • The PR's own test contradicts its expected values:
    • T/or_liheap.yaml:265-266 says "Kerosene is billed as heating oil and a grid-tied solar home pays an electricity bill". The inputs give kerosene a $1,200 fuel_oil_expense and solar a $1,200 electric bill.
    • :283 asserts that both are eligible, but :290 expects 0 for both.
  • Sources:
    • M26 p. 5: "LIHEAP is a fuel blind energy assistance program".
    • OAR 813-200-0005(4): "'Home energy' means the type of energy or fuel, including but not limited to fuel oil, natural gas, electricity, wood or propane, used to power the household's primary source of heating or cooling equipment."
    • OAR 813-200-0020(4): "Households in similar circumstances must receive similar benefits to the extent of LIHEAP funding."
    • M26 p. 67: "The standard benefit must be based on the energy type where the majority of assistance will be applied."
    • M26 p. 65 lists "kerosene" among the combustion fuels the program serves.
    • rulespec-us#1479, interpretation 8, concedes that these households get $0 "because the matrix prints only five heating columns, not because the manual denies them".
  • Head trace, period 2026, Multnomah, size 1, income $0:
    • KEROSENE with a $1,200 fuel-oil bill: eligible, $0. FUEL_OIL gives $750.
    • SOLAR with a $1,200 electric bill: eligible, $0. ELECTRICITY gives $465.
  • Why this is critical: batch-2 checklist Implement Section 8 Project-Based Rental Assistance #4 says an unknown-fuel household that the state would pay must not get $0 by construction. Here the fuel is known, and the column it is billed under is fixed. The batch-1 analogue, Add North Carolina LIEAP regular heating assistance #9726 C1 (an eligible household zeroed by its fuel value), was critical. Merged code already maps these fuels the same way: ne_liheap.py pays KEROSENE from the oil column, and ks_liheap_fuel_category.py, il_liheap_heating_type.py and dc_liheap_heating_type.py treat SOLAR as electric.
  • Fix:
    • Use (fuel == types.FUEL_OIL) | (fuel == types.KEROSENE) for Heating Oil and (fuel == types.ELECTRICITY) | (fuel == types.SOLAR) for Electricity, matching heating_expense.
    • Update the comment at V/or_liheap.py:45.
    • Change Case 13's expected values (T/or_liheap.yaml:290, Region 2) to [515, 750, 715, 560, 715, 750, 0, 515, 0, 0, 0].

Should Address

S1. Period 2027 pairs FY2027 limits with the FY2026 matrix and bands, but the PY2027 manual is in force from 2026-10-01

  • What happens: the single-dated parameters carry forward, while hhs_smi moves to FFY2027 (Oregon $129,250 from 2026-10-01). The limits update; the bands and amounts stay at FY2026.

  • Source: M27 p. 72, Region 1, size 1:

    • Row "$0 $10,082": "$475 $750 $715 $660 $715".
    • Row "$10,083 $20,163": "$370 $675 $560 $515 $560".
  • What changes in PY2027: all 72 band-top lookups and 95 of 480 amount lookups differ from the PR. The changes are in Electricity and Natural Gas; the Heating Oil, Liquid Gas and Wood/Pellets columns checked (sizes 1-2, both regions, M27 pp. 72 and 74) are unchanged and still identical across regions.

  • Head trace, period 2027, Multnomah, size 1, electricity:

    • Income $10,000: the PR gives band 2 and $360. PY2027 gives band 1 and $475.
    • Income $0: $465 against $475.
    • Natural gas, band 1: $670 against $660.
  • Why should and not critical:

    • Carrying values forward is the normal state until a parameter update.
    • The registry claims only 2026.
    • M27 carries a published date of 2026-10-01, the day this PR was opened.

    Even so, FFY2027 is the current program year and the PR is not merged yet.

  • Fix: add 2026-10-01 entries from M27 pp. 72-75, at least for electricity, natural_gas and income_band. Add one period-2027 case, such as the $10,000 trace above giving $475. If this is deferred, say in the registry notes and the PR body that 2027 uses FY2026 tables.

S2. np.ceil on a float32 product makes six FY2027 income limits $1 too high

  • Where: V/eligibility/or_liheap_income_limit.py:15-17 computes np.ceil(spm_unit("hhs_smi", period) * p.smi_rate). The comment says "Ceiling reproduces all 12 published limits", and the PR body repeats it.

  • Head output, period 2027: 40,326, 52,735, 65,143, 77,550, 89,958, 102,367, 104,693, 107,020, 109,346, 111,673, 113,999, 116,326.

  • Source: M27 p. 71 prints 40,326, 52,734, 65,142, 77,550, 89,958, 102,366, 104,693, 107,019, 109,346, 111,672, 113,999, 116,325.

  • Cause:

    • For sizes 2, 3, 6, 8, 10 and 12 the exact product is a whole dollar. For example, 129,250 × 0.68 × 0.6 = 52,734.
    • hhs_smi is a float32 array, and float32 0.6 is slightly above 0.6, so the product lands just above the integer and ceil adds $1.
    • FY2025 and FY2026 happen to be unaffected (12 of 12 each).
    • The error is formula-driven, so the S1 parameter update will not fix it.
  • Fix: round to cents in float64 before the ceiling:

    smi = spm_unit("hhs_smi", period).astype(np.float64)
    return np.ceil(np.round(smi * p.smi_rate, 2))
    • Checked on the head tree: this reproduces all 36 published FY2025, FY2026 and FY2027 limits.
    • Rounding the float32 product directly, np.ceil(np.round(hhs_smi * p.smi_rate, 2)), still leaves sizes 6, 8, 10 and 12 $1 too high in FY2027.
    • Add a period-2027 case for an exact-integer size, for example size 2 = 52,734, and correct the comment and the PR body.

S3. The countable-income lists omit two sources that the manual counts

The lists are self-contained, which is good. Two sources marked "Yes" are missing:

  • partnership_s_corp_income (earned).
    • M26 p. 45: "Self-Employment Income | Income from a business, less business expenses | Yes". FFY2026 plan §1.9, p. 5 checks "Self - Employment Income".
    • Partnership and S-corporation income is business income that sits outside self_employment_income, sstb_self_employment_income and farm_operations_income.
    • P/income/sources/earned.yaml:3-7 omits it.
  • military_retirement_pay (unearned).
    • M26 p. 44: "Retirement | A monthly payment made to someone who is retired from work | Yes". FFY2026 plan §1.9, p. 6 checks "Retirement / pension benefits".
    • pension_income adds only taxable_pension_income and tax_exempt_pension_income.
    • P/income/sources/unearned.yaml:3-21 omits it.
  • Bias: countable income for these households is understated, so eligibility and band placement are biased upward.
  • Precedent: batch-1 KY S1 raised the same omissions. Merged KS and KY count partnership_s_corp_income, and KY lists military_retirement_pay next to pension_income with a note that a household reports the amount in one or the other.
  • Fix:
    • Add both sources. Put partnership_s_corp_income in the floored earned loop.
    • military_retirement_pay's documentation says it includes survivor benefits, so add a note against double counting with survivor_benefits.
    • Add one test case per source.

S4. Rental and estate losses reduce countable income, but self-employment losses are floored at zero, although the manual sends landlords to the self-employment form

  • Code:
    • V/income/or_liheap_countable_earned_income.py:23 floors each earned source: max_(person(source, period), 0).
    • V/income/or_liheap_countable_income.py:35 adds the unearned list unfloored. That list includes rental_income (P/income/sources/unearned.yaml:4), farm_rent_income (:5) and estate_income (:21).
    • rental_income is an input with no floor. estate_income's own documentation calls it "Net income or loss from an interest in an estate or trust".
    • So a rental or estate loss offsets wages, Social Security and every other source, but a self-employment loss cannot. T/income/or_liheap_countable_earned_income.yaml:28 (person4) pins the self-employment floor.
  • Source: M26 p. 35, "Self-Employed Clients":
    • "All self-employed applicants (including those landlords who own rental properties) should complete the self-employment form."
    • "Previous year's losses or expenses are not allowed to be carried forward."
    • p. 44 gives the proof for rental income as the "Self-Employment form".
    • The manual treats rental income as self-employment, so treating the two differently is not supported. The PR documents its floor as a modeling convention (V/income/or_liheap_countable_earned_income.py:13-17).
  • Head trace, period 2026, Multnomah, size 1, natural gas, $45,000 wages:
    • With rental_income: -8_000 (or estate_income: -8_000): countable income is $37,000, within the $38,385 limit, so band 4 and $310.
    • With self_employment_income: -8_000 instead: countable income is $45,000, so ineligible and $0.
  • Fix:
    • Apply one rule to both. The PR's own convention points to flooring rental_income, farm_rent_income and estate_income at zero. Either move rental and farm rent into the floored loop as self-employment-form income, or floor each unearned source.
    • Add a case with a negative rental_income alongside wages.

S5. Tests cover less than the PR body claims, and two eligibility branches have no test that would fail if they were removed

  • The claim: the PR body says "29 program YAML cases cover all published heating cells and income limits". At head they do not:
    • Payment cells.
      • Cases 1-10 sweep only sizes 2, 4, 6, 8, 10 and 12.
      • T/or_liheap.yaml:7-9 says "the odd sizes are covered through the income-limit test", but that test (T/eligibility/or_liheap_income_limit.yaml:5-14) asserts limits only.
      • No case exercises payment size 3 or 5, which leaves 56 distinct amount cells untested. Also untested: electricity bands 2-4 at size 1 in both regions, Region 2 natural gas bands 2-4, and oil, propane and wood bands 2-4. Those last include the only cell where Wood/Pellets differs from Liquid Gas: size 1, band 4, $350 against $335 (M26 p. 78, p. 80).
    • Band tops.
      • The 15 tops for sizes 3, 5, 7, 9 and 11 are never exercised.
      • The even-size sweeps test the first dollar of each row, not the printed top.
      • Exact tops are tested only for size 1 (Case 12) and the size-2 top band (Case 11).
    • Limits. Sizes 4, 6, 8, 10 and 12 are never asserted. T/eligibility/or_liheap_income_limit.yaml:12-13 says even sizes are "exercised through the payment matrix sweeps", but every sweep income is below its limit.
    • Where the coverage went: commit 5a40a829cc ("Tidy Oregon LIHEAP parameters and tests") cut the coverage in half. At dfb2b8230b the matrix cases swept sizes 1-12, and the limit test asserted all 12 limits.
  • Untested branches:
    1. Public-housing heat-in-rent exclusion.
      • V/eligibility/or_liheap_eligible.py:23-25,30 excludes heat_in_rent & (receives_housing_assistance | is_in_public_housing).
      • The exclusion tests (T/eligibility/or_liheap_eligible.yaml:21) set only receives_housing_assistance.
      • is_in_public_housing appears only in T/integration.yaml:92-119 (Case 3), where heat is not in rent, so that household is eligible either way.
      • Deleting | spm_unit.household("is_in_public_housing", period) would pass every test.
    2. The age-18 boundary.
      • V/income/or_liheap_countable_earned_income.py:24 uses age >= p.earned_income_min_age.
      • The only 18-year-old in the tests (T/income/or_liheap_countable_earned_income.yaml:14-18) is in secondary school, so the person is excluded under either >= or >.
      • The rule is on M26 p. 41 and p. 53: earnings of members "eighteen (18) and over" count unless they are enrolled in high school.
  • Fix:
    • Restore the size 1-12 sweeps and the 12-limit assertion from before 5a40a829cc, keeping the new formatting.
    • Test printed top and top + 1 for a few odd sizes.
    • Add a household with is_in_public_housing: true and heat_expense_included_in_rent: true that expects ineligible and $0.
    • Add an 18-year-old who is not in secondary school and whose earnings count.
    • Correct the PR-body claim.

S6. Four single-page anchors open a page without the cited content, and four more are partial

All 30 anchors are single #page=N values within 1-95. These four open the wrong page:

File:line Anchor Page that opens Where the content is
P/payment/regions/region_2.yaml:28 #page=78 Region 1 county list and matrix p. 80 ("Baker, Crook, Deschutes, ... Wheeler Counties")
V/eligibility/or_liheap_eligible.py:11 #page=11 SSN and identity fields Energy burden p. 61 and p. 93; limit p. 77
P/payment/max_household_size.yaml:10 #page=78 Sizes 1-7 only Size 12 is on p. 79 and p. 81
P/payment/roomer_rate.yaml:10 #page=61 Start of the Roomer/Boarder/Owner bullet "50% of a regular payment" is on p. 62

These open a page that holds only part of the cited values:

  • P/payment/income_band.yaml:88 and V/payment/or_liheap_income_band.py:11 open p. 78, which prints band tops for sizes 1-7 only. The 15 tops for sizes 8-12 (for example 25,467 and 83,043) are only on p. 79 and p. 81.
  • P/payment/amount/electricity.yaml:122 and natural_gas.yaml:122 open p. 78, but the Region 2 values are on p. 80.
  • V/payment/or_liheap_region.py:11 opens p. 78, but the Region 2 list is on p. 80.

Fix: retarget the four wrong anchors, and add separate single-page entries for the partial ones: p. 79 for the bands; p. 80 for Region 2 amounts and counties.

Suggestions

G1. A missing county silently pays Region 2 rates, which contradicts the documentation and the PR body

  • V/payment/or_liheap_region.py:12-15 says "Zero denotes an unknown or out-of-state county; a county is required". The PR body says an unknown county leaves the benefit at zero.
  • With no county input, county falls back to first_county_in_state, which is BAKER_COUNTY_OR, so the region is 2. Head trace: electricity $515, natural gas $560; a Multnomah household gets $465 and $670.
  • Only an explicit county: UNKNOWN gives $0. Even so, the Case 15 comment at T/or_liheap.yaml:345 says "Without a county".
  • The fallback is repo-wide, and rulespec-us#1479 interpretation 4 documents it. Microsim stores county, so it is unaffected.
  • Fix the docstring, the PR body and the test comment. Add a case that omits county and expects the Baker/Region 2 result.

G2. COAL, OTHER and UNSPECIFIED households get $0

  • The matrix has no clear column for these fuels. Because of the manual's fuel-blind rule (p. 5), the state would still pay them.
  • The PR body defers UNSPECIFIED to Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754, which is acceptable under the batch-2 guidance.
  • Add a # see #9754 pointer at V/or_liheap.py:45-46.
  • Reword T/or_liheap.yaml:286-288 ("heating_type is a required input"; it defaults to UNSPECIFIED) and V/or_liheap.py:16-17 so the heat-in-rent UNSPECIFIED $0 reads as a coverage gap, not a legal result.

G3. Parameter start dates

  • Every parameter starts on 2025-10-09, the OHCS 44-2025 correction filing. The manual took effect on 2025-10-01 (M26 p. 2). Annual periods are unaffected.
  • Consider 2025-10-01 for the non-matrix parameters: smi_rate, earned_income_min_age, interest_threshold, the source lists, the regions, roomer_rate and the size limits.
  • If 2025-10-09 is kept for the matrix and band files, cite the correction filing in their references.

G4. Page lists in reference titles and in variable comments

  • 14 parameter titles embed page lists, for example P/income/sources/unearned.yaml:27 ("PDF pages 37, 38, ... 52"), P/payment/amount/*.yaml ("PDF pages 78, 79, 80, 81") and P/payment/regions/region_{1,2}.yaml:27.
  • Seven variables carry a # PDF pages ... comment above a single-URL reference: V/or_liheap.py:11, V/eligibility/or_liheap_eligible.py:10, V/income/or_liheap_countable_income.py:11-12, V/income/or_liheap_countable_earned_income.py:11, V/payment/or_liheap_income_band.py:10, V/payment/or_liheap_region.py:10 and V/or_liheap_dwelling_type.py:18.
  • Use section names in titles, with one title/href entry per operative page. In the variables, use a tuple of single-page hrefs, as merged KS does. For example, or_liheap: pp. 61, 62, 67, 78, 80; or_liheap_region: pp. 78, 80.

G5. rulespec-us#1479 is stale

See the Axiom line section. Update the pin, the "open draft" wording and companion tests 5 and 7 once S5 lands. After C2, update interpretation 8 and test 8.

G6. Households of 13 or more are income-eligible but get $0

  • The limit extends per member (M26 p. 77, "Each Additional Member"). Sizes 7-12 all print the size-6 amounts.
  • Consider paying size-6 amounts with the size-12 band tops instead of $0 at V/or_liheap.py:52-55. Otherwise, keep the documented gap. Few households are affected.

G7. Cite the operative rule for the subsidized heat-in-rent exclusion

  • The hard exclusion at V/eligibility/or_liheap_eligible.py:30 is supported. p. 61 says "not typically eligible", but the p. 93 Energy Burden Table is categorical: "Applicant lives in Subsidized Housing and heat is included in rent" gives Energy Burden "No". OAR 813-200-0020(1)(b) also makes "a demonstrated energy burden" a baseline requirement.
  • Cite both. No YAML or variable currently cites an OAR or the state plan.
  • T/eligibility/or_liheap_eligible.yaml:37-39 (household 6: subsidized, heat in rent, separate electric bill, ineligible) is one reading. The p. 93 row "Subsidized Housing and pays a vendor directly for utility costs" gives "Yes". Document the choice.

G8. PR-body hygiene

  • "PR remains draft" is stale; isDraft is false.
  • "Ceiling of 60% reproduces the 12 published limits" holds for FY2026 but not FY2027 (S2).
  • "Remaining coverage" misstates the missing-county behavior (G1).

G9. The interest threshold is applied per person

  • V/income/or_liheap_countable_income.py:28-31 applies the $200 test per person and counts all of the interest once it is exceeded.
  • M26 p. 50 ("Only to be counted if over $200.00 per year and is withdrawn") does not say per person or per household. The per-person reading is reasonable.
  • Say so in the documentation, and pin it with a test: two members with $150 each count $0.

G10. Digit separators

P/payment/income_band.yaml:6-78 writes all 36 values without underscores (9597 ... 83043). Merged KS and NE use 9_597.

G11. Parameter descriptions

  • P/income/sources/earned.yaml:1 ("counts these existing earnings inputs, without further self-employment expense deductions under the ...") is implementation language with a misplaced comma. Prefer "Oregon counts these sources as earned income under the Low Income Home Energy Assistance Program." and move the expense note to a YAML comment.
  • P/payment/amount/*.yaml:1 end with ", by region, household size and income band" after the program name.
  • Verbs outside the template list appear in interest_threshold (counts), the source lists, roomer_rate (pays), max_household_size (publishes) and the regions (assigns).

G12. or_liheap_dwelling_type is not a dwelling type

  • V/or_liheap_dwelling_type.py:4-17 records the application arrangement: a standard household, or a roomer, boarder or owner applying separately.
  • On main, ks_liheap_dwelling_type and ne_liheap_dwelling_type describe the physical structure.
  • Consider or_liheap_application_arrangement, or a boolean such as or_liheap_separate_roomer_application.

G13. Test hygiene

  • T/income/or_liheap_countable_income.yaml:6 is named "every listed unearned source counts", but ssi, general_assistance and estate_income are 0 and are never nonzero in any OR test. Give them values or rename the case.
  • Most cases set snap: 0 and immigration_status: CITIZEN, which no OR variable reads. The eligibility documentation says there is no immigration test, so these inputs suggest dependencies that do not exist.

G14. Registry notes

  • programs.yaml:877 reads "Regular heating income, county/fuel schedules and housing treatment. Some income, residence and discretionary-payment rules remain partial; earlier years are unverified estimates."
  • The sibling entries (IN, KS, KY, MS, NC, NE) use the form "FY2026 regular heating only, with ... . Crisis, cooling and weatherization assistance are out of scope."
  • Align it, for example: "FY2026 regular heating only, with the two county regions, five fuel columns and the roomer half payment. Crisis, cooling, Fuel Payment, equipment and OEAP assistance are out of scope."

Verified correct

Check Result
FY2026 amounts (M26 pp. 78-81): 2 regions × 12 sizes × 4 bands × 5 fuels 480 lookups (168 distinct parameter cells), 0 mismatches. Sizes 7-12 print the size-6 amounts, so the cap of 6 is right. Oil, propane and wood are identical across regions in FY2026
FY2026 band tops 36 distinct (72 lookups), 0 mismatches. Where the last printed top is $1 below the p. 77 limit (sizes 2, 4, 6 and 8-12), band 4 correctly extends to the limit (Case 11)
Income limits, ceil(hhs_smi × 0.6) 12 of 12 FY2026 (M26 p. 77) and 12 of 12 FY2025 (M25 PDF p. 57). FY2027: S2
County lists (M26 pp. 78, 80) 36 of 36. Each county appears once in the County enum, and together they are exactly Oregon's 36 counties
Scalars: 0.6 SMI, age 18, $200 interest, 0.5 roomer, size 12, payment cap 6 0 mismatches
Income sources 22 listed, 0 wrong (2 omitted, S3). Social Security is gross, matching the plan's "Including MediCare deduction" (p. 6) and the M26 p. 58 worksheet. Private disability, entered net of the SSA offset, reproduces the pp. 55-56 examples. Child earnings are excluded and child unearned income counted
Test arithmetic 261 of 261 or_liheap expectations recomputed from the head parameters. The integration, roomer ($465 × 0.5 = $232.50), mixed-state and countable-income cases were hand-checked
Code patterns defined_for chain (or_liheap → or_liheap_eligible → StateCode.OR). Entities and periods correct. Matrix keys are floored or clipped before lookup, so mixed-state simulations do not raise (Case 18). No hard-coded policy values, no unused parameters, and no aggregate feeds
Not findings after verification The roomer half payment of $232.50 is below the plan's $250 minimum, but p. 62 is explicit ("50% of a regular payment based on the Benefit Matrix"), and plan §2.6 says the minimum and maximum "must be shown in the payment matrix". The subsidized heat-in-rent hard exclusion is supported (G7). The Clearinghouse "2026" matrix carries PY2025 amounts; the PR correctly uses the manual
Microsimulation The datasets carry no heating inputs, so the Oregon total is about $0 until #9754 lands. The PR body says so. or_liheap feeds no household aggregate, so net income and poverty are unaffected

The sources also disagree with each other in one place, which is not a finding against the PR: p. 77's "Each Additional Member $2,115" does not match its own monthly figure ($184.58 × 12 = $2,214.96).

Checklist results

  1. SNAP list reuse: none. The lists are self-contained and use rental_income, not snap_rental_income, so there is no Stop rental losses from offsetting other income in SNAP #9672 double count. Social Security is gross per the plan, and child earnings are excluded. Gaps: S3 and S4.
  2. Backfill and carry-forward: C1 (period 2025 uses FY2026 tables) and S1 (period 2027).
  3. Operative state rules: the energy-burden test and the subsidized heat-in-rent exclusion match OAR 813-200-0020(1)(b) and M26 p. 93. The sources have no immigration test. Institutions are not modeled, which is documented (G7).
  4. UNSPECIFIED and microsim defaults: the Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754 deferral is acceptable for UNSPECIFIED, COAL and OTHER (G2). Kerosene and solar are known fuels zeroed by construction (C2).
  5. Axiom line: queued with a dispatch-ready pe-parity issue. The pin and test pointers are stale (G5).
  6. References: every anchor is a single valid #page=N. Four open the wrong page and four are partial (S6). Page text appears in 14 titles (G4). There are no OAR or plan references (G7).
  7. programs.yaml: entry format and sort are fine. The coverage-line conflict with Fix LIHEAP program registry entries and add a registry integrity test #9722 is expected (Branch status).
  8. Rounding, boundaries and periods: FY2027 ceil error (S2). Band edges are tested for sizes 1-2 only (S5). All periods are 2026, with no 2_026-style literals.

CI status

37/37 checks pass (gh pr checks 9743, 2026-10-06) on head 06b38abd60, which is unchanged since this review began.

Branch status

  • Divergence: 11 commits ahead of main and 12 behind (merge base b812bd46a0). GitHub reports CONFLICTING.
  • Expected conflict in programs.yaml (not a finding):
  • Other main commits: none of them touch Oregon LIHEAP, hhs_smi, heating_expense or the county logic.

Axiom line

axiom: TheAxiomFoundation/rulespec-us#1479 queued

  • The form is valid and the issue is dispatch-ready. It is open and labelled pe-parity (plus enhancement). It carries module paths, corpus citations, verbatim manual, OAR and plan text, required outputs, and companion tests sourced from manual values.
  • Stale points:
    • It pins dfb2b8230b, which predates 5a40a829cc (the tidy commit that changed 28 Oregon LIHEAP files and halved the test sweeps) and two main merges. Head is 06b38abd60.
    • It calls the PR "open draft".
    • Companion tests 5 ("sizes 1 to 12" limits) and 7 ("480 cells") point to sweeps that 5a40a829cc removed. Restoring them under S5 makes those pointers true again.
    • Interpretation 8 and test 8 need the kerosene and solar amounts once C2 lands, and the scope and period notes need updating after C1 and S1.
  • None of this blocks the PR by itself. Update the issue when the fixes land.

Next Steps

  1. C1: either add PY2025 (2024-10-01) entries and give fuel_oil, propane and wood a region level, or gate the program to start in FY2026. Add a period-2025 test either way.
  2. C2: map KEROSENE to Heating Oil and SOLAR to Electricity, and update Case 13.
  3. S1-S2: add PY2027 (2026-10-01) electricity, natural gas and band entries. Round the limit in float64 before the ceiling. Add period-2027 cases.
  4. S3-S4: add partnership_s_corp_income and military_retirement_pay, and treat rental, farm-rent and estate losses the same way as self-employment losses.
  5. S5-S6: restore the size 1-12 sweeps and the 12-limit test, add the public-housing and age-18 cases, retarget the four wrong anchors, and add the partial-page entries.
  6. Rebase on main (coverage line ... NC, NE, OR, TX), update rulespec-us#1479, and re-request review.

2 critical and 6 should-address findings, plus 14 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 per the review comment above: 2 critical (period 2025 carries the FY2026 matrix and bands back over the published PY2025 matrix; kerosene and solar households are eligible but paid $0 despite the fuel-blind manual) and 6 should-address items (2027 pairs FY2027 limits with FY2026 tables, float ceil makes six FY2027 limits $1 high, two missing income sources, rental vs self-employment loss treatment, test coverage below the PR body's claim, wrong page anchors). All FY2026 values match the sources.

hua7450 and others added 2 commits October 6, 2026 17:07
- Encode the PY2025 (2024-10-01) and PY2027 (2026-10-01) payment
  matrices, bands and limits beside FY2026; fuel oil, propane and wood
  carry the region level; verified_years "2025-2027".
- Pay kerosene from the heating oil column and solar from electricity.
- Apply the 60% SMI ceiling to the product rounded to cents.
- Add partnership/S-corp income to the earned sources and military
  retirement pay to the unearned list; floor every unearned source.
- Add cases for PY2025 and PY2027, odd household sizes, public-housing
  heat in rent, age 18, per-member interest and the Region 2 fallback.
- Retarget references, re-template descriptions and align the registry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts:
#	policyengine_us/programs.yaml
@hua7450
hua7450 requested a review from DTrim99 October 6, 2026 21:10
@DTrim99

DTrim99 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 2): Oregon LIHEAP regular heating assistance

PR #9743: Add Oregon LIHEAP regular heating assistance (author: hua7450, head 5d4e3a5b15)

Round 1 reviewed head 06b38abd60 and requested changes for 2 critical and 6 should-address findings, plus 14 suggestions. Since then the author has pushed one fix commit, 7d4079fe85 ("Address review round 1 on Oregon LIHEAP"), and merged main into the branch. The author left no reply comment; the commit message and the rewritten PR body serve as the response.

Overall: the substantive findings are fixed, and the new values check out against the sources.

  • C1 (PY2025): the PY2025 matrix is now encoded. All 552 PY2025 amount and band lookups match the PY2025 manual.
  • C2 (kerosene and solar): kerosene is now paid from the Heating Oil column and solar from the Electricity column.
  • S1 (PY2027): the PY2027 tables are now encoded. All 480 amount lookups and 72 band-top lookups match the PY2027 manual.
  • S2 (rounding): the float64 fix makes the formula reproduce all 36 published limits for 2025 to 2027.
  • S3 and S4 (income): the two missing sources were added, and losses are now treated the same way in the earned and unearned income.
  • S5 (tests): the two untested eligibility branches now have tests, and the PR body's claim about test coverage has been corrected.

One should-address item stays open:

  • S6 (anchors), partially addressed: the four wrong anchors were fixed. The partial-page anchors were not split into one entry per page, and the new PY2025 and PY2027 references repeat that pattern.

Three round-1 suggestions are also still open (G4, G5, G12), and there are three new suggestions. There are no new critical or should-address findings.

Path prefixes used below:

  • P/ = policyengine_us/parameters/gov/states/or/ohcs/liheap/
  • V/ = policyengine_us/variables/gov/states/or/ohcs/liheap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/or/ohcs/liheap/

Line numbers refer to head 5d4e3a5b15.

Source Documents

# Document Link Use in this round
1 PY2025 Intake, Operations & Policy Manual ("Effective October 1, 2024 – September 30, 2025") M25 Limits p. 57, matrices pp. 58-59, scalars pp. 29-47
2 PY2026 Intake, Operations & Policy Manual M26 Limits p. 77, matrices pp. 78-81, retargeted anchors
3 PY2027 Intake, Operations & Policy Manual ("Effective Oct.1, 2026 – Sept. 30, 2027") M27 Limits p. 71, matrices pp. 72-75, scalars pp. 36-59
4 OHCS 44-2025 temporary order (cited by the PR this round) SOS filing receipt Verified this round; see "Verified correct"
5 OAR 813-200-0020 Cornell LII Energy-burden citation added to or_liheap_eligible

Round-1 findings

Item Status Evidence
C1 Period 2025 carried the FY2026 matrix back ADDRESSED
  • 2024-10-01 entries were added to all five amount tables and to P/payment/income_band.yaml.
  • fuel_oil, propane and wood now carry the region level, indexed at V/or_liheap.py:43-46. In PY2025, 13 oil, propane and wood cells differ between the regions, so the region level is needed.
  • All 552 PY2025 lookups match M25 pp. 58-59: 480 amounts and 72 band tops. Sizes 7-12 print the size-6 amounts.
  • A period-2025 case was added (T/or_liheap.yaml:529, Case 23): Multnomah electricity at $9,400 → $295, natural gas at $0 → $500, heating oil at $20,000 → $680, and Deschutes heating oil at $20,000 → $745.
  • verified_years: "2025-2027", and the documentation is updated.
C2 Kerosene and solar were eligible but paid $0 ADDRESSED
  • V/or_liheap.py:31-36 pays ELECTRICITY | SOLAR from the Electricity column and FUEL_OIL | KEROSENE from the Heating Oil column, matching heating_expense.
  • Case 13 now expects [515, 750, 715, 560, 715, 750, 0, 515, 0, 0, 0] (T/or_liheap.yaml:292), the values round 1 proposed.
S1 Period 2027 used the FY2026 tables ADDRESSED
  • 2026-10-01 entries were added to all five fuels and to the band tops, not only to electricity, natural gas and the bands.
  • All 480 amount lookups and 72 band-top lookups match M27 pp. 72-75.
  • Case 24 (T/or_liheap.yaml:556): $10,000 → $475, $10,083 → $370, natural gas $660, Deschutes electricity $510.
S2 The float32 ceil raised six FY2027 limits by $1 ADDRESSED
  • V/eligibility/or_liheap_income_limit.py:20-21 now casts hhs_smi to float64 and rounds to cents before the ceiling, the formula round 1 proposed.
  • Recomputed from the repo's SMI parameters, it reproduces all 36 published limits (M25 p. 57, M26 p. 77, M27 p. 71). The old formula reproduced 6 of 12 for FY2027.
  • New T/eligibility/or_liheap_income_limit.yaml:30-40 (Case 3) asserts the six whole-dollar FY2027 sizes, and Case 2 asserts PY2025.
  • The comment and the PR body are corrected.
S3 Two countable sources were missing ADDRESSED
  • partnership_s_corp_income is in the floored earned list (P/income/sources/earned.yaml:9).
  • military_retirement_pay is in the unearned list with the survivor double-count note (P/income/sources/unearned.yaml:14-16).
  • Tests: T/income/or_liheap_countable_earned_income.yaml:31-36,60 (person5, $2,500 + $700 = $3,200) and T/income/or_liheap_countable_income.yaml:33,47 (military $90 → $3,690 / $3,891).
S4 Rental and estate losses offset other income ADDRESSED
  • V/income/or_liheap_countable_income.py:38-40 floors each unearned source at zero, and the documentation at :18-19 says so.
  • T/income/or_liheap_countable_income.yaml:86-101 (Case 4): a −$8,000 rental loss and a −$1,000 estate loss count as 0, giving $45,000.
  • A residual cross-member offset is noted as new suggestion N1.
S5 Tests covered less than claimed; two branches untested ADDRESSED
  • Public housing with heat in rent is now tested (T/eligibility/or_liheap_eligible.yaml:23,45, household 7 → ineligible). So is an 18-year-old who is not in school (person5 above).
  • Odd sizes 3 and 5 are now tested at each printed top and top + 1 (Case 21, T/or_liheap.yaml:484). That case also pins the size-1 band-4 cell where Liquid Gas ($335) and Wood/Pellets ($350) differ.
  • The PR body now describes the coverage accurately ("swept at every band for the even household sizes, and the odd sizes 1, 3 and 5 are tested at their printed band tops").
  • The remaining gaps are not claimed and are listed as new suggestion N2.
S6 Four wrong anchors and four partial anchors PARTIALLY ADDRESSED: open
  • The four wrong anchors are fixed:
    • P/payment/regions/region_2.yaml:30 → p. 80
    • V/eligibility/or_liheap_eligible.py:12 → p. 61, plus the OAR (:13)
    • P/payment/max_household_size.yaml:12 → p. 79
    • P/payment/roomer_rate.yaml:12 → p. 62
  • The partial-page anchors were not split, and the new PY2025 and PY2027 entries copy the pattern. See S6 below.
G1 Missing county pays Region 2 ADDRESSED
  • The docstring is fixed (V/payment/or_liheap_region.py:12-16), as are the PR body and the Case 15 comment.
  • New Case 22 (T/or_liheap.yaml:508) omits the county and expects Region 2, $515 / $560.
G2 COAL, OTHER, UNSPECIFIED pay $0 ADDRESSED
  • A #9754 pointer is at V/or_liheap.py:48-49, and the documentation (:16-18) calls the $0 a coverage gap.
  • The Case 13 comment is reworded.
G3 Parameter start dates ADDRESSED
  • The scalars and lists now start on 2024-10-01.
  • The FY2026 matrix entries keep 2025-10-09 and cite the OHCS 44-2025 filing.
G4 Page lists in titles and variable comments NOT ADDRESSED
  • 48 parameter titles now carry "PDF page(s) …" text, up from 14, because each new manual entry repeats the pattern. Example: P/income/sources/unearned.yaml:30-34.
  • Seven variables keep # PDF pages … comments above single-URL references (e.g. V/or_liheap.py:11, V/payment/or_liheap_income_band.py:10).
  • This is folded into the S6 fix below.
G5 rulespec-us#1479 is stale NOT ADDRESSED See the Axiom line section.
G6 Sizes 13+ get $0 ACKNOWLEDGED-DEFERRED The PR body ("Remaining coverage") and V/eligibility/or_liheap_eligible.py:23-24 document it as unsupported schedule coverage, not a denial. This is a sound deferral.
G7 Cite the operative rule for subsidized heat-in-rent ADDRESSED
  • OAR 813-200-0020(1)(b) is in the reference tuple and the documentation (V/eligibility/or_liheap_eligible.py:11-20).
  • The test comment cites row 3 of the p. 93 table and records the choice that a separate non-heating bill does not create an energy burden.
G8 PR-body hygiene ADDRESSED
  • The "remains draft" line is gone.
  • The ceiling claim now covers 36 limits.
  • The missing-county wording is fixed.
G9 Per-person interest test ADDRESSED
  • Documented at V/income/or_liheap_countable_income.py:16-17.
  • New Case 5 (T/income/or_liheap_countable_income.yaml:103-127): $150 + $150 → 0.
G10 Digit separators ADDRESSED All 108 band tops use 9_597 style (P/payment/income_band.yaml:6-150).
G11 Parameter descriptions ADDRESSED
  • P/income/sources/earned.yaml:1 now uses the proposed wording, with the expense note moved to a comment (:2-3).
  • The amount descriptions no longer trail the program name, and the verbs are templated.
G12 or_liheap_dwelling_type is not a dwelling type NOT ADDRESSED The file is unchanged and there is no response. rulespec-us#1479 interpretation 2 raises the same point.
G13 Test hygiene ADDRESSED
  • ssi, general_assistance and estate_income now have values (T/income/or_liheap_countable_income.yaml:30-33).
  • No Oregon LIHEAP test sets snap or immigration_status any more.
G14 Registry notes ADDRESSED programs.yaml now reads "PY2025-PY2027 regular heating only, with the two county regions, five fuel columns and the roomer half payment. Crisis, cooling, Fuel Payment, equipment and OEAP assistance are out of scope."

Critical (Must Fix)

None.

Should Address

S6 (round 1, still open). Multi-page tables are cited with one anchor that opens only part of the cited values

  • What round 1 asked for: retarget the four wrong anchors, which is done. It also asked to "add separate single-page entries for the partial ones: p. 79 for the bands; p. 80 for Region 2 amounts and counties", which is not done.

  • The fix commit repeats the pattern in each new PY2025 and PY2027 entry. Each table cites one href, with a title that lists pages the href does not open:

    File:line Anchor What it opens Values it misses
    P/payment/amount/electricity.yaml:185, fuel_oil.yaml:148, propane.yaml:156, natural_gas.yaml:190, wood.yaml:156 M25 #page=58 Region 1 matrix All Region 2 amounts, which are on M25 p. 59. In PY2025, 13 oil, propane and wood cells differ by region, so p. 59 is operative
    electricity.yaml:187, fuel_oil.yaml:150, propane.yaml:158, natural_gas.yaml:192, wood.yaml:158; P/payment/income_band.yaml:162 M26 #page=78 Region 1, sizes 1-7 Sizes 8-12 (p. 79) and Region 2 (p. 80, p. 81). This is unchanged from round 1
    electricity.yaml:191, fuel_oil.yaml:154, propane.yaml:162, natural_gas.yaml:196, wood.yaml:162; income_band.yaml:166 M27 #page=72 Region 1, sizes 1-8 Sizes 9-12 (M27 p. 73) and Region 2 (p. 74, p. 75)
    V/payment/or_liheap_income_band.py:11 M26 #page=78 Sizes 1-7 Band tops for sizes 8-12 on p. 79. Unchanged from round 1
    V/payment/or_liheap_region.py:11 M26 #page=78 Region 1 list The Region 2 list on p. 80. Unchanged from round 1
  • What is not affected: the PY2025 band citation (income_band.yaml:160, M25 p. 58) is complete, because the bands are the same in both regions and p. 58 prints all 12 sizes. max_household_size and max_payment_size also open on their values.

  • Fix: split each multi-page citation into one title/href entry per operative page:

    • M25: pp. 58, 59
    • M26: pp. 78, 79, 80, 81
    • M27: pp. 72, 73, 74, 75

    Give each entry a section title, such as "PY2027 Energy Assistance Intake Operations and Policy Manual, Region 2 benefit matrix, sizes 9 to 12". This also removes the page lists from the titles (G4). In the two variables, use a tuple of single-page hrefs (pp. 78, 79 and pp. 78, 80), as merged KS does.

  • Severity: this stays a should because it is the unfinished part of round-1 S6. It is mechanical and changes no values.

Suggestions

N1. The unearned floor is applied to the unit total of each source, so one member's loss still offsets another member's income from the same source

  • V/income/or_liheap_countable_income.py:38-40 computes max_(add(spm_unit, period, [source]), 0), which sums each source across members and then floors the total.
  • The earned formula floors each source per person (V/income/or_liheap_countable_earned_income.py:22-23).
  • Example: two spouses with separate rentals of +$6,000 and −$6,000 count $0 rental income. The same split in self_employment_income counts $6,000.
  • The documentation at :18-19 says unearned sources are floored "like the earned sources".
  • Fix: floor the person-level sources per member before summing. tanf is SPM-level, so it keeps the unit sum. Alternatively, document the unit-level reading and add a two-member case.

N2. Residual test coverage, now matched to the claims

  • FY2026 limits for sizes 4, 6, 8, 10 and 12 are still never asserted. The odd sizes, size 2 and the PY2025/PY2027 limit cases are.
  • The band tops for sizes 7, 9 and 11 are not exercised.
  • The PY2025 and PY2027 tables (960 new amount lookups) are pinned by eight size-1 payment cells and one printed top at its edge (PY2027 size 1, $10,082 / $10,083).
  • The PR body says the PY2025 and PY2027 cases "pin ... the new band tops". It would be more accurate to say they pin the size-1 tops.
  • I checked every cell independently (see "Verified correct"), so this is not blocking. A size-12 period-2027 row and an even-size FY2026 limit case would close most of it.

N3. The variables cite only the PY2026 manual although they are verified for 2025-2027

  • V/eligibility/or_liheap_income_limit.py:11 cites M26 p. 77 only, while the comment above the formula (:15-16) claims the PY2025 limits (M25 p. 57) and the PY2027 limits (M27 p. 71).
  • V/or_liheap.py:12, V/payment/or_liheap_income_band.py:11 and V/payment/or_liheap_region.py:11 likewise cite M26 only.
  • Add the PY2025 and PY2027 pages as tuple entries alongside the S6 split.

G4, G5, G12 (round 1, still open)

See the round-1 table. G4 is resolved by the S6 fix. G5 is covered in the Axiom line section. G12 (rename to an application-arrangement input) remains optional.

Verified correct

Check Result
PY2025 amounts and bands (M25 pp. 58-59) 480 amount lookups (2 regions × 12 sizes × 4 bands × 5 fuels) and 72 band-top lookups, 0 mismatches. Sizes 7-12 print the size-6 amounts. The printed last top equals the p. 57 limit for every size, and the bands have no gaps
PY2027 amounts and bands (M27 pp. 72-75) 480 amount lookups and 72 band-top lookups, 0 mismatches. The PDF text layout shifts the Liquid Gas and Natural Gas columns by one row, so the values were read by column position and confirmed against the page images: 120 Region 2 cells for sizes 1-6, plus the size 1 and 6 band tops and the first size-9 top, read directly from p. 74. Sizes 7-12 print the size-6 amounts
FY2026 cells after restructuring 276 lookups (240 amounts, with oil, propane and wood counted in both regions, plus 36 band tops) are identical to round-1 head, which round 1 verified against M26 pp. 78-81
OHCS 44-2025 filing (not retrievable in round 1) Filed and effective 10/09/2025; caption "Correcting the payment matrices in the LIHEAP and OEAP Program Manual". Its attached manual's size-1 rows match the PR's FY2026 cells: Region 1 $465 / $750 / $715 / $670 / $715 and $360 / $675 / $560 / $520 / $560; Region 2 $515 / … / $560 and $400 / … / $435
Income limits ceil(round(float64(hhs_smi) × 0.6, 2)) reproduces 12 of 12 limits in each of FY2025, FY2026 and FY2027
Period logic An annual period Y reads 1 January of Y:
  • 2025 reads the 2024-10-01 tables with FFY2025 SMI ($117,983).
  • 2026 reads 2025-10-09 with FFY2026 ($123,027).
  • 2027 reads 2026-10-01 with FFY2027 ($129,250).
So every claimed period pairs its own manual's tables with its own limits. Periods before 2025 backfill PY2025 values, which are not claimed and are documented as unverified estimates
Scalars and lists for the new years The values are the same in all three manuals:
  • 60% SMI (M25 p. 57, M27 p. 71)
  • age 18 (M25 p. 33, M27 p. 42)
  • $200 interest (M25 p. 34, M27 p. 43)
  • landlords on the self-employment form (M25 p. 29, M27 p. 36)
  • 50% roomer payment (M25 p. 47, M27 p. 59)
  • 36 counties in the same two regions (M25 pp. 58-59, M27 pp. 72, 74)
  • a 12-size table and a size-6 cap
The PY2025 income chart (pp. 31-41) has the same categories as PY2026, so the 2024-10-01 source lists hold
New anchors Every new or retargeted page opens on its cited content:
  • M25 pp. 29, 31, 33, 34, 47, 57, 58, 59
  • M26 pp. 61, 62, 79, 80
  • M27 pp. 36, 38, 42, 43, 59, 71, 72, 73, 74
  • the SOS filing and the OAR 813-200-0020 page
The partial-page caveat for multi-page tables is S6
New test expectations (hand-checked)
  • Cases 21-24 of T/or_liheap.yaml: 24 payment, 22 band, 2 region and 8 limit expectations.
  • T/eligibility/or_liheap_eligible.yaml household 7.
  • Earned-income person5.
  • Countable-income Cases 1, 4 and 5.
  • Income-limit Cases 2 and 3.
All correct. The PR body's example ($12,000, gas, Multnomah, size 1 → $520) also matches
Code The kerosene/solar mapping matches heating_expense. All five tables are indexed [region][size][band], with region and band floored at 1, so mixed-state simulations still do not raise (Case 18). No hard-coded values, no unused parameters, and changelog changelog.d/or-liheap.added.md is updated ("program years 2025 to 2027")
Merge of main Of the PR files, only programs.yaml changed in the merge, and only by main's own entries. The coverage line is now DC, Riverside County, MA, IL, IN, KS, KY, MS, NC, NE, OR, TX, and the OR block sits after NE, so the round-1 conflict is resolved
Main's new guards

CI status

gh pr checks 9743 on head 5d4e3a5b15 (2026-10-06, 21:31 UTC) shows 3 pending, 0 passed and 0 failed. The first-stage jobs (changelog fragment, registry lock, release lock guard) have been queued since 21:08 UTC, and the YAML and unit shards have not started. I did not run the YAML suite locally; every new expectation listed above was recomputed by hand from the head parameters and the manuals. Round 1's head passed 37/37.

Branch status

  • 13 commits ahead of main and 0 behind. The merge base is main's tip, 9eab717e56.
  • GitHub reports MERGEABLE, with merge state BLOCKED (changes requested, CI pending).

Axiom line

axiom: TheAxiomFoundation/rulespec-us#1479 queued. The line is still in the PR body.

  • The form is still valid. The issue is open and labelled pe-parity, with module paths, corpus citations, verbatim text and companion tests.
  • The issue has not been refreshed. It was last updated 2026-10-03, so it predates both round-1 fixes and the round-2 head. It now describes behavior that differs from the PR:
    • It pins dfb2b8230b and calls the PR "open draft".
    • The policy period is "one season" (FY2026), and it says a 2027 period "is not encoded". The PR now encodes PY2025 and PY2027, and the issue has no PY2025 or PY2027 sources, outputs or companion tests.
    • Interpretation 8 and companion test 8 expect 515, 750, 715, 560, 715, 0, 0, 0, 0, 0, 0. The PR now pays kerosene $750 and solar $515.
    • It does not cover partnership_s_corp_income, military_retirement_pay, the per-source loss floor or the float64 rounding.
    • Companion test 7 still describes a 480-cell sweep, which the PR does not have.
    • Interpretation 1 (parameter dates) is now resolved.
  • A rulespec encoder dispatched from the issue as written would diverge from the PR on kerosene and solar and on two of the three program years. This stays a suggestion, consistent with round 1, but please update the issue before merge so that the queued parity claim matches the merged behavior.

Next Steps

  1. S6: split the M25, M26 and M27 table citations into one entry per page (M25 pp. 58-59; M26 pp. 78-81; M27 pp. 72-75), drop the page lists from the titles, and use single-page tuples in or_liheap_income_band and or_liheap_region.
  2. Optional: N1 (per-member unearned floor), N2 (an even-size FY2026 limit case and a size-12 PY2027 row), N3 (PY2025 and PY2027 references on the variables), G12.
  3. Refresh rulespec-us#1479 to the new head: scope 2025-2027, kerosene and solar, the new sources and floor, and the companion tests. Then re-request review once CI is green.

0 critical and 1 should-address finding open (S6, partially addressed), plus 3 new suggestions and 3 open round-1 suggestions (G4, G5, G12).

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.

Round 2: both criticals and S1-S5 are addressed (PY2025 and PY2027 tables encoded and verified, kerosene/solar paid, limit rounding fixed, income sources and loss flooring added, tests expanded). One should item remains (S6): multi-page tables are still cited with a single anchor that misses Region 2 / larger-size values; please add one entry per operative page (details in the comment above). CI on the new head was still queued at review time.

hua7450 and others added 2 commits October 7, 2026 15:20
- Floor each member's unearned sources before the household sum and add
  the unit-level TANF amount in the formula.
- Add the even-size FY2026 limit case, the PY2027 size 12 Region 2 row
  and a two-member rental case.
- Cite the PY2025 and PY2027 manuals on the limit, payment, band and
  region variables.
- Write consecutive pages as #page ranges in reference titles and drop
  single pages from titles.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@hua7450

hua7450 commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for round 2. Fixes pushed in 34bf42c.

S6 — not splitting the table citations into one link per page. Those pages are consecutive and linear: someone who clicks the link and starts verifying the values against our parameters will scroll to the next page by nature. The split would also put ten links to the same three PDFs, differing only in page number, into each of the six table files, which I don't want. We keep one entry per source and have settled the title form instead, now in AGENTS.md: a single page is cited by the href alone, consecutive pages end the title with #page 72-75, nonconsecutive with #page 29,32-33,36,41; the variable comments follow the same shape. Applied across the Oregon files.

  • N1: each member's unearned sources are floored before the household sum (the TANF aggregate, a unit-level amount, is added in the formula); countable-income Case 6 pins spouses with +$6,000 and −$6,000 of rent at $6,000.
  • N2: income-limit Case 4 asserts the even FY2026 sizes 2 to 12, and or_liheap.yaml Case 25 pins the PY2027 size-12 Region 2 rows (page 75) at each band and the last-row gas and oil cells. 40 cases.
  • N3: the PY2025 and PY2027 pages are on or_liheap, or_liheap_income_limit, or_liheap_income_band and or_liheap_region.
  • G12: name kept, as in round 1.
  • LIHEAP wave 1b: encode Oregon LIHEAP regular heating assistance TheAxiomFoundation/rulespec-us#1479 was refreshed at 21:38 UTC on 2026-10-06, two minutes after your review (head, PY2025 and PY2027 sources and tests, kerosene and solar, the added sources and floor, interpretation 1 closed), and again now for N1 to N3.

@hua7450
hua7450 requested a review from DTrim99 October 7, 2026 19:21
@DTrim99

DTrim99 commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 3): Oregon LIHEAP regular heating assistance

PR #9743: Add Oregon LIHEAP regular heating assistance (author: hua7450, head 34bf42ca56)

Round 2 reviewed head 5d4e3a5b15 and requested changes for one open should-address finding (S6), with three new suggestions (N1-N3) and three open round-1 suggestions (G4, G5, G12). Since then the author has pushed one fix commit, 66cea76d64 ("Address review round 2 on Oregon LIHEAP"), and merged main (b7e4229d86). The author also replied in a PR comment (2026-10-07, 19:21 UTC).

Overall: the code and test changes are correct, and every new expectation matches the manuals.

  • N1 (per-member floor): each member's unearned sources are now floored before the household sum. A two-member rental case pins the behavior.
  • N2 (coverage): the even-size FY2026 limits and the PY2027 size-12 Region 2 rows are now tested.
  • N3 (variable references): the four variables now cite all three manuals.
  • G5 (rulespec-us#1479): the issue is refreshed to this head.
  • Tests: all 40 Oregon cases pass locally on this head.

One should-address item stays open:

  • S6 (anchors), not addressed: the author declined the split. Each table still has one href per manual, and the page lists moved into the titles as #page 58-59. No href in the five amount files opens a Region 2 matrix. Below I propose a smaller fix that answers the author's concern about adding ten links per file.

There are no new critical or should-address findings, and there is one new optional suggestion (N4).

Path prefixes used below:

  • P/ = policyengine_us/parameters/gov/states/or/ohcs/liheap/
  • V/ = policyengine_us/variables/gov/states/or/ohcs/liheap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/or/ohcs/liheap/

Line numbers refer to head 34bf42ca56. M25, M26 and M27 are the PY2025, PY2026 and PY2027 Intake, Operations & Policy Manuals (links in the table below).

Source Documents

# Document Link Use in this round
1 PY2025 Intake, Operations & Policy Manual M25 Limits p. 57; Region 1 p. 58; Region 2 p. 59
2 PY2026 Intake, Operations & Policy Manual M26 Limits p. 77 (Case 4); matrices pp. 78-81
3 PY2027 Intake, Operations & Policy Manual M27 Limits p. 71; matrices pp. 72-75 (Case 25 is on p. 75)
4 rulespec-us#1479 issue Axiom refresh (G5)

Round-2 open items

Item Status Evidence
S6 Multi-page tables cited with one anchor NOT ADDRESSED (declined): open
  • The reply says the pages are consecutive, a reader "will scroll to the next page", and splitting would put "ten links to the same three PDFs" in each table file.
  • Each table still has one href per manual (P/payment/amount/electricity.yaml:185,187,191 and the same lines in the other four amount files; P/payment/income_band.yaml:160,162,166).
  • The titles now end in #page 58-59, #page 78-81 or #page 72-75, page ranges the href does not open.
  • The new M25 and M27 tuple entries in V/payment/or_liheap_region.py:12,16 and V/payment/or_liheap_income_band.py:16 repeat the pattern. See S6 below.
N1 Unit-level unearned floor ADDRESSED
  • V/income/or_liheap_countable_income.py:38-40 now computes spm_unit.sum(max_(person(source, period), 0)).
  • All 18 listed sources are Person variables, so person(...) cannot hit an entity mismatch.
  • tanf (SPM unit) left the list (P/income/sources/unearned.yaml:2-3) and is added at :41-44.
  • The documentation (:17-19) is updated.
  • New Case 6 (T/income/or_liheap_countable_income.yaml:129-153): +$6,000 and −$6,000 of rent → $6,000.
N2 Residual test coverage ADDRESSED
  • Income-limit Case 4 (T/eligibility/or_liheap_income_limit.yaml:42-50) asserts FY2026 sizes 2, 4, 6, 8, 10 and 12, so all 12 FY2026 limits are now tested.
  • T/or_liheap.yaml:577-600 (Case 25) pins the PY2027 size-12 Region 2 rows at every band, plus the last-row gas and oil cells.
  • The two additions round 2 named are both in. Band tops for sizes 7, 9 and 11 remain untested, which round 2 did not ask for.
N3 Variables cited only M26 ADDRESSED
  • M25 and M27 entries were added to V/eligibility/or_liheap_income_limit.py:11-15 (pp. 57, 77, 71, all complete), V/or_liheap.py:11-18, V/payment/or_liheap_income_band.py:10-17 and V/payment/or_liheap_region.py:10-17.
  • The partial anchors in the last two are part of S6.
G4 Page lists in titles and variable comments PARTIALLY ADDRESSED
  • Page text is gone from the 11 single-page titles: smi_rate, region_1, region_2, and roomer_rate for PY2025 and PY2027.
  • 37 of 54 titles still carry page text, now as #page a-b or #page 29,32-33,36,41 (e.g. P/income/sources/earned.yaml:16).
  • Fourteen # PDF pages … comment lines remain in seven variables. See G4 below.
G5 rulespec-us#1479 is stale ADDRESSED The issue was updated on 2026-10-07 at 19:21 UTC and pins 34bf42ca56. See the Axiom line section.
G12 or_liheap_dwelling_type is not a dwelling type ACKNOWLEDGED (declined) The reply says "name kept, as in round 1". This was optional, so I consider it closed.

Critical (Must Fix)

None.

Should Address

S6 (round 1, still open). The Region 2 matrices, and some band tops, have no href that opens them

  • The author's response. One entry per source is kept, and the title form is "now in AGENTS.md": a single page is cited by the href alone, and multi-page citations end the title in #page a-b.

    • Neither main's nor this branch's AGENTS.md contains that rule.
    • PolicyEngine's parameter-reference guidance keeps page numbers in href and out of title.
    • I treat the title form itself as the G4 suggestion, not as blocking. The blocking part is that some operative values cannot be reached from any href.
  • What each table href opens (I reopened every one):

    Entries Anchor Page shows Values no href opens
    P/payment/amount/{electricity,fuel_oil,propane,natural_gas,wood}.yaml, PY2025 entry (e.g. electricity.yaml:185) M25 #page=58 Region 1, all sizes, and the Region 1 counties The whole Region 2 matrix (M25 p. 59). In PY2025, 13 oil, propane and wood cells differ between the regions
    The same five files, PY2026 entry (e.g. electricity.yaml:187) M26 #page=78 Region 1, sizes 1-7 The Region 2 matrix (M26 p. 80)
    The same five files, PY2027 entry (e.g. electricity.yaml:191) M27 #page=72 Region 1, sizes 1-8 The Region 2 matrix (M27 p. 74)
    P/payment/income_band.yaml:162; V/payment/or_liheap_income_band.py:14 M26 #page=78 Band tops for sizes 1-7 Sizes 8-12 (M26 p. 79)
    P/payment/income_band.yaml:166; V/payment/or_liheap_income_band.py:16 M27 #page=72 Band tops for sizes 1-8 and the first size-9 row The rest of size 9, and sizes 10-12 (M27 p. 73)
    V/payment/or_liheap_region.py:12,14,16 M25 #page=58, M26 #page=78, M27 #page=72 The Region 1 county lists All three Region 2 county lists (M25 p. 59, M26 p. 80, M27 p. 74). Their own comments (:13, :15) name pp. 78, 80 and 72, 74
  • Not affected: the PY2025 bands (M25 p. 58 prints all 12 sizes, and the bands are the same in both regions), region_1/region_2, smi_rate, roomer_rate, max_household_size, max_payment_size and the or_liheap_income_limit tuple. Each of these opens on its values.

  • A smaller fix than round 2 asked for, which answers the "ten links" concern. max_payment_size caps the amount tables at size 6, so each amount file's size axis is range(1, 7). In every manual, each region prints sizes 1-6 on its first page. So no file needs ten links:

    • Five amount files: add one Region 2 entry per manual (M25 p. 59, M26 p. 80, M27 p. 74). That gives two entries per manual, seven in all with the OHCS filing, and every cell is then one click away.
    • income_band.yaml: add M26 p. 79 and M27 p. 73. M25 p. 58 is already complete.
    • or_liheap_region: add the three Region 2 pages.
    • or_liheap_income_band: add M26 p. 79 and M27 p. 73.
    • Titles: name the section, for example "PY2027 Energy Assistance Intake Operations and Policy Manual, Region 2 benefit matrix" or "…, standard payment income bands, sizes 10 to 12". Leave the page text out of the title.
  • Severity: this stays a should, consistent with rounds 1 and 2, because the facts are unchanged. The Region 2 values, which in PY2025 differ from Region 1, still have no link that opens them. The fix is mechanical and changes no values.

Suggestions

N4. TANF now sits outside the unearned-source parameter (optional)

  • P/income/sources/unearned.yaml no longer lists tanf. V/income/or_liheap_countable_income.py:44 adds it in code instead. The YAML comment (:2-3) explains this, but the description (:1) still reads like the complete list, and TANF can no longer be removed through the parameter.
  • This is a valid way to do what round 2 proposed, and the arithmetic is correct. If you want TANF back under parameter control, use a one-item unit-level list (for example sources/unearned_unit: [tanf]) summed with add(). Alternatively, say "person-level" in the description.

G4 (round 1, partially addressed)

  • The table titles are covered by the S6 fix.
  • For the text-page titles, prefer section names over the #page suffix: earned_income_min_age, interest_threshold, both source lists, and roomer_rate PY2026.
  • P/payment/max_household_size.yaml:11,13 titles say #page 78-81 and #page 72-75, but the hrefs open p. 79 and p. 73. That is correct for the size-12 value, but it does not match even the new title form.
  • The # PDF pages … comments in the variables are harmless. They would go away with single-page tuple entries.

Verified correct

Check Result
Income-limit Case 4 (T/eligibility/or_liheap_income_limit.yaml:42-50) M26 p. 77 prints $50,196, $73,817, $97,438, $101,867, $106,296 and $110,725, matching all six. The formula agrees: ceil(123,027 × {0.68, 1.00, 1.32, 1.38, 1.44, 1.50} × 0.6)
or_liheap Case 25 (T/or_liheap.yaml:577-600)
  • The M27 p. 75 page image, Region 2 size 12, has rows "$0 $29,082", "$29,083 $58,163", "$58,164 $87,244" and "$87,245 $116,325"; Electricity $750, $615, $515, $370; and, in the last row, Natural Gas $380 and Heating Oil $535.
  • The head parameters at 2026-10-01 give the same values: Region 2, size 6, band tops 29,082 / 58,163 / 87,244.
  • The limit is 129,250 × 1.5 × 0.6 = $116,325 (p. 71), so band 4 at $87,245 is eligible, and every fuel has a positive bill.
Countable-income Case 6 +6,000 is floored at 6,000 and −6,000 at 0, so the unit counts $6,000. No other source is nonzero (ssi and tanf are set to 0)
Values since round 2 The diff of the Oregon parameters from 5d4e3a5b15 touches only titles and hrefs, plus the removal of tanf from the unearned list. No table, band or scalar value moved, so the round-2 checks still hold: 552 PY2025 lookups, 552 PY2027 lookups and 276 FY2026 lookups, 0 mismatches
tanf move p.sources.unearned is read only at V/income/or_liheap_countable_income.py:39. max_(spm_unit("tanf"), 0) equals the old unit sum, and Case 3 (take-up) still expects $0 / $1,200
Anchors reopened
  • M25: p. 57 limits, p. 58 Region 1 and counties, p. 59 Region 2 and counties.
  • M26: p. 77 limits, p. 78 Region 1 sizes 1-7 and counties, p. 79 Region 1 sizes 8-12, p. 80 Region 2 sizes 1-6 and counties, p. 81 Region 2 sizes 7-12.
  • M27: p. 71 limits, p. 72 Region 1 sizes 1-8 and counties, p. 73 Region 1 sizes 9-12, p. 74 Region 2 sizes 1-8 and counties, p. 75 Region 2 sizes 9-12.
  • Every title containing # is quoted, so YAML does not truncate it as a comment.
Local run All 40 Oregon YAML cases pass on an export of 34bf42ca56 (25 + 6 + 4 + 3 + 1 + 1, matching the PR body's "40"). The local environment had policyengine-core 3.32.6, just below the >=3.32.8 floor, so CI remains authoritative
Main's guards I ran main's reference-URL, concatenated-URL, known-dead-URL, built-in-sum and program-registry tests against the head: 740 passed, 10 skipped. The built-in-sum test needed UTF-8 mode on Windows, an environment issue, not a PR one
Merge of main
  • This is a two-parent merge of b7e4229d86. No PR-scope file differs between 66cea76d64 and 34bf42ca56.
  • All 32 changed files against main are under the Oregon paths, plus programs.yaml and changelog.d/or-liheap.added.md.
  • main's only county-related change since round 2 (Read stored county and has_tin inputs with the branch's name #9748) affects the microsimulation over-dataset path, not these YAML cases.
programs.yaml The coverage line is DC, Riverside County, MA, IL, IN, KS, KY, MS, NC, NE, OR, TX. The OR block sits between NE and TX, with verified_years: "2025-2027" and the round-2 notes. The registry test passes
PR body and changelog "40 program YAML cases" is accurate, and "the FY2026 limits are asserted for every size" is now true. The fragment (changelog.d/or-liheap.added.md, "program years 2025 to 2027") is unchanged and accurate

CI status

At 20:53 UTC on 2026-10-07, gh pr checks 9743 on head 34bf42ca56 shows 3 pending, 0 passed and 0 failed. The re-run (run 37673782871, attempt 2) has been queued since 20:32 UTC. Its first-stage jobs (changelog fragment, registry lock, release lock guard) have not started, and the YAML and unit shards have not been created. Round-2 head 5d4e3a5b15 passed 35/35 on its second attempt. As noted above, all 40 Oregon cases and main's reference and registry guards pass locally on this head.

Branch status

  • 15 commits ahead of main and 0 behind. The merge base is main's tip, b7e4229d86.
  • GitHub reports MERGEABLE, with merge state BLOCKED (changes requested, CI pending).

Axiom line

axiom: TheAxiomFoundation/rulespec-us#1479 queued. The line is unchanged in the PR body.

  • G5 is addressed. The issue is open and labelled pe-parity, and was last updated 2026-10-07 at 19:21 UTC. It now:
    • pins 34bf42ca56;
    • scopes three seasons (PY2025-PY2027) with their sources and outputs;
    • expects kerosene $750 and solar $515 (test 8: 515, 750, 715, 560, 715, 750, 0, 515, 0, 0, 0);
    • covers partnership_s_corp_income, military_retirement_pay, the per-member floor (test 27), the even-size limits (test 25) and the size-12 row (test 26);
    • closes interpretation 1.
  • A cosmetic leftover: its PR line still says "Everything below describes head 5d4e3a5b15", although the body now includes the 34bf42ca56 changes.

Next Steps

  1. S6: add the Region 2 entries to the five amount files (M25 p. 59, M26 p. 80, M27 p. 74), M26 p. 79 and M27 p. 73 to income_band.yaml and or_liheap_income_band, and the three Region 2 pages to or_liheap_region. Use section titles without page text. This is two links per manual for the amount files, not ten.
  2. Optional: N4 (TANF back under parameter control, or a "person-level" description), the rest of G4, and the stale sentence in rulespec-us#1479.
  3. Re-request review once CI is green.

0 critical and 1 should-address finding open (S6, declined), plus 1 new suggestion (N4) and 1 partially addressed round-1 suggestion (G4).

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.

Round 3: N1-N3 and the axiom refresh are addressed, and every value still matches the manuals. One should item remains (S6), now with a smaller fix (see the comment above). The Region 2 matrices (M25 p59, M26 p80, M27 p74) and the larger-size band pages (M26 p79, M27 p73) still have no href that opens them, so those values can't be checked from the references. Because max_payment_size caps the amount tables at six, adding one Region 2 entry per manual (two per manual, not ten) and the two band pages fixes it. I'm treating the page text in titles as an optional suggestion, not a blocker. The repo's AGENTS.md doesn't contain the title convention you mentioned.

hua7450 and others added 2 commits October 7, 2026 22:48
Add the Region 2 benefit matrix and the larger-household-size pages to
the amount, income band and region references so every modeled value is
reachable from a href; drop page text from single-page titles; describe
the unearned source list as person-level.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts:
#	policyengine_us/programs.yaml
@hua7450

hua7450 commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for round 3. Fixes pushed in 9872ebb.

  • S6: done as proposed. The five amount files now carry a Region 1 and a Region 2 entry per manual (M25 pp. 58 and 59, M26 pp. 78 and 80, M27 pp. 72 and 74). Each is a single page, since the tables stop at size 6. income_band.yaml and or_liheap_income_band add M26 p. 79 and M27 p. 73, and or_liheap_region adds the three Region 2 pages. Titles name the section and carry no page text. Size 7 straddles M26 pp. 78 and 79, and size 9 straddles M27 pp. 72 and 73, so those titles read "household sizes 1 to 7" / "7 to 12" and "1 to 9" / "9 to 12".
  • G4: the max_household_size titles no longer carry page text. Each href opens the page that prints size 12.
  • N4: the unearned-source description now says "person-level". TANF stays in the formula.
  • Convention: you are right that the repo does not carry the title rule. I filed Document the reference page-citation convention in AGENTS.md #10008 to add it to AGENTS.md.
  • LIHEAP wave 1b: encode Oregon LIHEAP regular heating assistance TheAxiomFoundation/rulespec-us#1479 is refreshed to this head, and the sentence that still named 5d4e3a5b15 is fixed.

No values changed.

# Conflicts:
#	policyengine_us/programs.yaml
@DTrim99

DTrim99 commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 4): Oregon LIHEAP regular heating assistance

PR #9743: Add Oregon LIHEAP regular heating assistance (author: hua7450, head bbb8ce05c7)

Round 3 reviewed head 34bf42ca56 and requested changes for one open should-address finding (S6). Since then the author has pushed one fix commit, ea08858e80 ("Address review round 3 on Oregon LIHEAP"), merged main twice (9872ebbdf5, bbb8ce05c7) and replied on 2026-10-08 at 02:49 UTC.

Overall: S6 is fixed as proposed. Every table value now has an href that opens the page printing it, and no value, formula or test changed. N4, G4 and the stale rulespec-us#1479 sentence are also resolved. There are no new critical or should-address findings and one informational note (N5). CI is green.

Path prefixes: P/ = policyengine_us/parameters/gov/states/or/ohcs/liheap/, V/ = policyengine_us/variables/gov/states/or/ohcs/liheap/. M25, M26 and M27 are the PY2025, PY2026 and PY2027 Intake, Operations & Policy Manuals: M25, M26, M27. All three URLs return 200 today, with the same byte counts as the copies I audited.

Round-3 open items

Item Status Evidence
S6 Region 2 matrices and larger-size band pages had no href ADDRESSED
  • Five amount files: each now has a Region 1 and a Region 2 entry per manual: M25 #page=58/59, M26 #page=78/80, M27 #page=72/74 (e.g. P/payment/amount/electricity.yaml:183-197).
  • income_band.yaml (:158-170) and or_liheap_income_band (V/payment/or_liheap_income_band.py:10-16) add M26 #page=79 and M27 #page=73.
  • or_liheap_region (V/payment/or_liheap_region.py:10-17) adds M25 p. 59, M26 p. 80 and M27 p. 74.
  • Every new href opens the right page. M25 p. 59 shows the Region 2 matrix, all sizes, and the Region 2 counties. M26 p. 80 shows Region 2, sizes 1-6, and its counties. M27 p. 74 shows Region 2, sizes 1-8, and its counties. M26 p. 79 shows the rest of size 7 and sizes 8-12. M27 p. 73 shows the rest of size 9 and sizes 10-12.
  • The size ranges in the titles are right. M26 p. 78 ends on size 7's first row ($0-$24,913), and M27 p. 72 ends on size 9's first row ($0-$27,337). So "sizes 1 to 7 / 7 to 12" and "1 to 9 / 9 to 12" are accurate.
  • Spot check against the new pages: the head's Region 2 size-2 electricity cells are 650/505/420/305 (PY2025), 645/505/420/300 (PY2026) and 635/495/415/295 (PY2027). These match p. 59, p. 80 and p. 74.
N4 TANF outside the unearned-source list ADDRESSED P/income/sources/unearned.yaml:1 now says "person-level unearned income sources". TANF stays in the formula, one of the two options round 3 offered.
G4 Page text in titles CLOSED
  • The max_household_size titles no longer carry page text. Their hrefs open M26 p. 79 and M27 p. 73, which print size 12.
  • The remaining #page a-b titles and # PDF pages comments now follow the convention main adopted in Document the reference page-citation convention #10009. No further ask.
rulespec-us#1479 stale sentence ADDRESSED Updated 2026-10-08 at 04:41 UTC. It pins bbb8ce05c7, and the sentence naming 5d4e3a5b15 is gone. The issue is open and labelled pe-parity.

Critical (Must Fix)

None.

Should Address

None.

Suggestions

N5. The new entries depart from the convention main adopted after the fix (informational, no change requested)

  • The rule. Document the reference page-citation convention #10009, merged 2026-10-08 at 03:29 UTC (41 minutes after ea08858e80), adds a "Parameter and variable references" section to CLAUDE.md. Its first bullet says: "One reference entry per source document; URLs that differ only by #page= are one source. Do not split a multi-page table into per-page entries."
  • Where the PR differs. The S6 entries are URLs that differ only by #page=. V/or_liheap.py:11-18 follows the rule instead: one href per manual with # PDF pages comments. No guard test enforces the rule.
  • Why I don't ask for a change. The split was made at my request. Each region's matrix is a separate table with its own county list. Every href now opens its values.
  • My round-3 premise is superseded. Round 3 said PolicyEngine's guidance keeps page numbers out of titles. Document the reference page-citation convention #10009 now documents the title-range form, so I would also accept the round-3 layout under that rule. I'd keep the current one.

Verified correct

Check Result
Values unchanged
  • I parsed all 16 Oregon parameter files at 34bf42ca56 and bbb8ce05c7 with metadata and description stripped: 0 value changes.
  • ea08858e80 touches only reference blocks, one description and two variable reference tuples.
  • No formula or test changed, so the round-2 lookups (552 + 552 + 276 cells, 0 mismatches) and the 40 YAML cases still hold.
Every table href
  • I reopened each href in the five amount files, income_band.yaml, max_household_size.yaml, region_1/region_2, or_liheap_income_band and or_liheap_region.
  • Each opens a page that prints the values its title names.
  • The OHCS 44-2025 entry is unchanged.
Merges of main
  • No Oregon file and no changelog fragment differs between ea08858e80 and bbb8ce05c7.
  • Against the merge base, the PR still changes 32 files: the Oregon paths, programs.yaml and changelog.d/or-liheap.added.md.
programs.yaml
  • The coverage line is DC, Riverside County, MA, IL, IN, KS, KY, MD, MS, NC, NE, NJ, NY, OR, TX. MD, NJ and NY came from main.
  • The OR block now sits between NY and TX, with unchanged fields (verified_years: "2025-2027").
main's guards
PR body "40 program YAML cases" is still accurate. The axiom line is unchanged: axiom: TheAxiomFoundation/rulespec-us#1479 queued.

CI status

gh pr checks 9743 shows 28 passed, 0 failed, 0 pending on head bbb8ce05c7 (run 37728677240, attempt 1, 2026-10-08 04:41-05:25 UTC). That run predates the 48 commits main has gained since, including the new reference-key guard (#9849). The local guard and Oregon runs on the merged tree cover that gap.

Branch status

  • 18 commits ahead of main and 48 behind; the merge base is e19f75c510.
  • git merge-tree against main (0d93495806) is clean.
  • GitHub reports MERGEABLE, with merge state BLOCKED on the round-3 change request.

Next Steps

  1. None required. Optionally merge main before merging, so CI runs once against the new guards.

0 critical and 0 should-address findings open, plus 1 informational note (N5).

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 4: S6 is addressed. Every amount file now cites the Region 1 and Region 2 matrix pages for each manual, and the band and region references open the larger-size and Region 2 pages. I reopened every href, and each shows what its title claims. No values changed, CI is 28/28 green, and the merge-tree against main is clean.

@hua7450
hua7450 merged commit efac95f into main Oct 9, 2026
28 checks passed
@hua7450
hua7450 deleted the or-liheap branch October 9, 2026 15:10
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.

Oregon LIHEAP

2 participants