Skip to content

Add Pennsylvania LIHEAP regular heating assistance - #9971

Merged
hua7450 merged 17 commits into
mainfrom
pa-liheap
Oct 30, 2026
Merged

hua7450 merged 17 commits into
mainfrom
pa-liheap

Conversation

@hua7450

@hua7450 hua7450 commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Adds Pennsylvania's LIHEAP regular heating assistance for FY2025 through FY2027. The model tests income against 150% of the federal poverty guideline, computes the heating payment from a fuel base for the county or heating region, a multiplier for each $1,000 bracket of benefit income and $5 for each added member, and adds the $100 vulnerable-household supplements that Pennsylvania issued in August 2025 and July 2026. The payment formula reproduces every printed cell for the fuels it prices in the FY2025 and FY2026 benefit charts and the current benefit lookup. In 2026, a Philadelphia family of three with $4,836 of TANF and natural gas heat receives the $664 printed in the FY2026 chart plus the $100 supplement, $764.

Fixes #9962

Regulatory authority

Eligibility

Requirement Source How modeled
Pennsylvania residence — defined_for = StateCode.PA
Household size excludes nonqualified noncitizens FY2026 plan section 601.41(1), page 41 Counts members with is_citizen_or_legal_immigrant. A household with no qualified member is ineligible. Nonqualified members' income counts in full.
Income at or below 150% of the poverty guideline in effect when the season opens FY2025 plan page 10; FY2026 plan page 10; FY2027 plan page 9 Previous year's guideline × 1.5, inclusive. One person: $22,590 (FY2025), $23,475 (FY2026), $23,940 (FY2027). Eligibility uses income before the benefit-only wage disregard.
Primary heating responsibility FY2026 plan pages 38–39 A household qualifies with its own primary heating bill or obligation (has_heating_expense), or with heat included in unsubsidized rent. A household with housing assistance or in public housing whose rent includes heat needs its own primary heating obligation; a secondary-fuel bill does not establish it. heating_type NONE is ineligible.
No asset test and no categorical eligibility FY2026 federal plan page 8 No resource test applies. SNAP receipt does not waive the income limit.

Countable income

Item Source How modeled
Wages FY2026 plan sections 601.82(1) and 601.84(21), pages 49–53 Counts employment_income and military_basic_pay, since 601.82(1) counts wages for services on behalf of an employer. Excludes wages of children under 18 who are tax dependents, the model's proxy for dependency; their business and unearned income still count.
Net business income Section 601.82(2), pages 49–50 Counts self-employment, SSTB, farm operations and partnership/S-corporation income. Floors each source at zero under 601.82(2)(ii): "A loss from one source of income cannot be used to offset another source of income." Adds no further business-expense deduction.
Unearned income Section 601.82(4), pages 50–51; federal plans section 1.9, page 6 Counts SSI, Pennsylvania's state supplement to SSI (pa_ssp), pensions, military retirement income (retiree pay and Survivor Benefit Plan annuities), railroad retirement annuities, survivor benefits, unemployment compensation (the reported amount, or modeled state unemployment compensation when none is reported), strike benefits, workers' compensation, disability benefits, veterans benefits, General Assistance, cash help from relatives (financial_assistance), child support, alimony, rental income, farm rental income, retirement distributions, interest and dividends. Floors each source at zero, so a rental or farm-rent loss offsets nothing.
Interest and dividends 55 Pa. Code 601.82(4)(ix); section 601.82(4)(ix), page 51 Counts interest and ordinary dividends in full, with no threshold.
Social Security net of the Medicare premium 55 Pa. Code 601.84(10); section 601.84(10), page 52; FY2025 and FY2026 federal plans check "Excluding MediCare deduction" Subtracts the premium from Social Security, capped at the benefit, since SSA withholds the whole benefit when it is smaller (42 CFR 408.43); the premium never reduces other income. For an enrollee with railroad retirement benefits nothing is subtracted, because the Railroad Retirement Board deducts the premium from those benefits (42 CFR 408.42). Railroad retirement counts in full as a pension.
Public assistance grants Section 601.82(4)(i), page 50 Counts the take-up-gated tanf aggregate, which includes pa_tanf.
Child and spousal support exclusions Section 601.84(15), pages 52–53 Excludes up to $100 a month of child support for one child under 18 or $200 for two or more, and up to $50 a month of spousal support. Only the larger annual exclusion applies.
Benefit income for the payment Section 601.41(2), page 42 Subtracts 20% of countable wages from countable income. Business income receives no disregard.

Benefit

Handbook 638.3 describes the calculation. Benefit income falls in $1,000 brackets: "Each bracket is associated with a different benefit multiplier." Lower-income brackets carry higher multipliers. Each fuel's value depends "on the cost of the fuel in the household's county of residence." Household sizes run from 1 to 20. The model computes

payment = min(max(round_half_up(multiplier × (fuel base + $5 × (min(size, 20) − 1))), $200), $1,000)

DHS prints neither the multipliers nor the fuel bases. The model reads them from the printed tables, and the parameter comments say so.

Parameter under gov.states.pa.dhs.liheap.payment Values Varies by
income_multiplier 2.1, 1.9, 1.75, 1.4, 1.25 and 1.0 for $0–$5,999; 0.8727 at $6,000–$6,999, falling to 0.1486 at $19,000–$19,999; 0 from $20,000 21 brackets of benefit income; the same in all three seasons
base/electricity FY2027: $670, $640, $610, $580 and $550 for regions 1–5 Heating region and season; also prices propane and solar heat
base/wood_other One value per region Heating region and season
base/natural_gas, base/fuel_oil, base/coal, base/kerosene One value per county County and season; natural gas takes one value per heating region in FY2025 and FY2026
additional_member_amount $5 Each member after the first
maximum_household_size 20 Larger households use size 20
minimum, maximum $200 and $1,000 Cash benefit limits in the state plans and handbook Appendix B

The six base files set 834 season values: 5 regions × 2 fuels and 67 counties × 4 fuels, for 3 seasons. Centre and Fayette coal keep one base for two seasons, so the files hold 832 dated entries. For the Philadelphia family above, benefit income of $4,836 takes the 1.25 multiplier and the FY2026 Philadelphia natural gas base is $521.05, so the payment is 1.25 × (521.05 + 5 × 2) = 663.81, which rounds to the printed $664.

The formula and these parameters reproduce all 355,971 printed cells for the seven priced fuel rows (67 counties × 7 rows × 23 income bands × 11 sizes × 3 seasons) with 0 mismatches. The tables round half up: the current lookup prints 763 for Adams County electricity, size 7, $4,000–$4,999, where 1.25 × (580 + 5 × 6) = 762.50. With the same parameters, rounding half to even would change 600 cells (125 in FY2025, 92 in FY2026 and 383 in the lookup). The eighth printed row, Blended Fuel, has no model input.

  • Sizes 12 to 20 continue the $5 member amount, and larger households use size 20. The printed tables stop at size 11, so no published amount confirms these. At FY2026 Adams County electricity, $5,000–$5,999, the chart prints $602 for size 11; the model pays $607 for size 12 and $647 for sizes 20 and 21.
  • From $20,000 of benefit income the multiplier is 0, so the payment is the $200 minimum. Every printed cell from $20,000 to $22,999 is $200; the tables print no band above $22,999, and the model pays $200 there too.
  • Each multiplier bracket starts at a multiple of $1,000, so $999.99 falls in the $0–$999 band.
  • Solar heat uses the electricity base. Handbook 638.3 fuel codes 701–708 include no solar code, and a grid-tied solar home pays the electricity bill heating_expense assigns it. Propane uses the electricity base because the printed Propane row equals the Electric row in every county, band and season.
  • The reported bill does not cap the payment. An eligible tenant with heat in rent receives the full amount.
  • The supplement adds $100 once per household in 2025 and in 2026 for an eligible household with a member aged 60 or older, a child under 5, or a member receiving disability cash assistance (Social Security disability, disability_benefits, or SSI with a disability or blindness flag). Operations Memorandum 25-08-01 paid the 2024–2025 season's households on August 15, 2025, and Operations Memorandum 26-07-01 paid the 2025–2026 season's households on July 16, 2026; each payment is recorded in its year of issuance (pages 1–2 of each memo). It is $0 in 2027 because no 2026–2027 directive exists as of October 6, 2026.

Years covered

Model year Season Fuel bases read from One-person limit Supplement
2025 FY2025, opened November 4, 2024 FY2025 county benefit chart $22,590 (2024 guideline) $100 (August 2025)
2026 FY2026, opened November 3, 2025 FY2026 county benefit chart $23,475 (2025 guideline) $100 (July 2026)
2027 FY2027, opens November 2, 2026 Current benefit lookup $23,940 (2026 guideline) $0

A calendar-year period reads the season open on January 1. The lookup prints no season label; the FY2027 plan (page 39) points to it through the DHS LIHEAP page, and the current handbook links to it, which supports dating its amounts to FY2027. Formulas are undated, verified_years is "2025-2027", and earlier years are unverified backfilled estimates.

Not modeled and source conflicts

Item Source Model treatment
Blended fuel Benefit lookup fuel option; handbook 638.3 fuel code 708 No input.
Midseason poverty-guideline update FY2026 plan page 38 (each January); FY2027 plan page 35 (each February) Uses the guideline in effect at season opening for the whole year. For one person in calendar 2026 the updated limit is $23,940, $465 above the modeled $23,475.
Prior-month income election, utility allowances, certificate-of-deposit and stock-sale proceeds, support or rent paid within the household, refunded support Sections 601.82–601.84 Existing inputs do not identify them.
Medicare Part D premiums Section 601.84(10) No variable; not deducted.
Institutional residence, prior awards, agency-paid bills, custody and temporary-resident exceptions Sections 601.31 and 601.41 Existing inputs do not identify them; the SPM unit approximates the household.
Supplement receipt facts Operations Memoranda 25-08-01 and 26-07-01, page 2 Annual eligibility approximates actual Cash grant receipt in the season; annual age approximates age at application; the household is assumed not to have moved after its last payment. Aggregate veterans benefits do not establish disability assistance. The supplement also pays when the table pays $0 for an unknown county or an unpriced fuel, since such a household would receive a Cash grant.
Unknown county — An explicitly unknown county gets a $0 payment amount. A missing county falls back to Adams County, the repository default.
Unspecified fuel — Pays $0 as a coverage gap (#9754).
Crisis, cooling and weatherization assistance — Out of scope.
Topic State plan or announcement Operating source and model
Medicare premium on railroad retirement Section 601.84(10) of the plan (page 52) and of 55 Pa. Code excludes the premium deducted from Social Security only Handbook 650.6(10) also excludes premiums deducted from Railroad Retirement and other benefits. The model follows the regulation and the plan.
Interest and dividends Section 601.82(4)(ix) of the plan (page 51) and of 55 Pa. Code counts them with no threshold Handbook 650.3(9) counts only amounts over $25 a month. The model follows the regulation and the plan.
Child age for the $100 supplement FY2026 plan pages 34 and 43 define a vulnerable household by a member "age five and under"; the July 30, 2026 announcement says under six Operations Memoranda 25-08-01 and 26-07-01, page 2, issue the payment for a child under five. The model follows the memoranda, so a household whose only vulnerable member is five receives no supplement.

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

Test plan

84 YAML cases in 10 files cover:

  • all FY2026 and FY2027 income limits for sizes 1–11 and FY2025 limits for sizes 1, 8 and 11;
  • payment amounts across regions, counties, fuels and sizes in all three seasons, cents at band edges, the $200 minimum in the final printed band and above it, sizes 12, 20 and 21, half-up rounding (the lookup's $763 for size 7, where half to even gives $762, and $738 for size 3), solar heat, and unspecified and no heat;
  • heat in rent with and without a housing subsidy, the benefit-only wage disregard, dependent-child wages, nonqualified members, and the support exclusions;
  • Social Security net of the Medicare premium, capped at the benefit, with railroad retirement counted in full and no Social Security netting for a railroad annuitant, interest from the first dollar, military basic pay, military retirement and a Survivor Benefit Plan annuity, modeled unemployment compensation, rental and farm-rent losses, and TANF with and without take-up;
  • each supplement category with its age edges, and the supplement's timing: paid in 2025 and 2026, not in 2027;
  • seven end-to-end households: a Philadelphia TANF family (gas), a Bradford County retired couple (fuel oil), an Elk County SSI recipient (kerosene), a Lackawanna County parent with child support (electricity), a Chester County mixed-status family (gas), an FY2025 Schuylkill County family (coal) and an FY2027 Allegheny County household with a military survivor annuity and a rental loss (gas).

Every case specifies heating_type; mixed-state arrays include New Jersey and Ohio households. The tests were not run locally; CI validates them.

  • For policy changes: YAML tests whose expected values come from an external source (statute, official form or worksheet, official calculator); otherwise mark not applicable.
  • Source-conflict register entry and marker: not applicable. Pennsylvania's three questions in source_conflicts/candidates.yaml are ruled. For us-pa:liheap-interest-dividend-threshold and us-pa:liheap-railroad-retirement-medicare-premium, the model follows 55 Pa. Code and the state plan over the handbook. For us-pa:liheap-supplement-child-age, no regulation sets the age, and the model follows the operations memoranda. No statute is on either side, so no register entry or # Source conflict marker applies.
  • make format
  • Changelog fragment in changelog.d/

axiom: TheAxiomFoundation/rulespec-us#1536 queued

🤖 Generated with Claude Code

hua7450 and others added 4 commits October 3, 2026 19:49
…tion tests

- Encode the published FY2025 benefit chart (from 2024-11-04) in all six
  matrix files alongside FY2026 and FY2027.
- Count Social Security and railroad retirement net of the Medicare Part B
  premium, taken once and floored at zero (handbook 650.6(10), followed
  over 55 Pa. Code 601.84(10)); count military retirement; floor each
  income source at zero (601.82(2)(ii)); read take-up-gated tanf.
- Count interest and dividends only above $25 a month (handbook 650.3(9),
  followed over the plan).
- Pay SOLAR heat from the electricity row.
- Pay the $100 vulnerable-household supplement for the 2024-2025 season in
  2025 (Operations Memorandum 25-08-01, issued August 15, 2025).
- Shorten the registry note, add PA to the LIHEAP coverage line, and set
  verified_years to 2025-2027.
- Add integration and period-2025 cases; tidy test conventions.

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

# Conflicts:
#	policyengine_us/programs.yaml
@codecov

codecov Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (b0a8c09) to head (87aaae3).
⚠️ Report is 69 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##              main     #9971    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files            1         9     +8     
  Lines           15       156   +141     
  Branches         0         1     +1     
==========================================
+ Hits            15       156   +141     
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 3 commits October 21, 2026 12:37
The printed tables follow multiplier x (fuel base + $5 per extra member),
rounded half up and held to the $200-$1,000 Cash range. Income multipliers,
regional and county fuel bases, the member amount and the limits reproduce
all 355,971 printed cells for FY2025, FY2026 and the current lookup.
Sizes 12-20 now continue the per-member amount (handbook 638.3 range).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	policyengine_us/programs.yaml
Use the repository's page-range title style for multi-page references, add
PDF page comments to multi-page variable references, and say how the FY2027
plan and handbook reach the benefit lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hua7450 and others added 2 commits October 28, 2026 10:58
# Conflicts:
#	policyengine_us/programs.yaml
- Add pa_ssp to the LIHEAP unearned income sources (state plan section
  601.82(4)); integration Case 3 now pays 349 + 100.
- Pin pa_ssp to 0 in tests whose households do not receive SSI.
- Start eligibility, income and region parameters on 2024-11-04 with the
  FY2025 state plan and the October 2024 handbook edition as references.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hua7450 and others added 2 commits October 28, 2026 19:05
Case 3 described an SSI recipient with a disability but set only
is_disabled, which SSI's disability test does not read. Without
meets_ssi_disability_criteria the person was not SSI-eligible, so pa_ssp
paid $0 and the case failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	policyengine_us/programs.yaml
@hua7450
hua7450 requested a review from DTrim99 October 28, 2026 23:06
@DTrim99

DTrim99 commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Add Pennsylvania LIHEAP regular heating assistance

PR #9971 (author: hua7450), head 6a15c9fd2c. This is the first review. The PR has no earlier reviews or review comments; the only comment is Codecov's. It touches no partner test file.

Path prefixes:

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

Summary

The payment model is very good. Pennsylvania prints only the resulting tables, and the PR recovers them as fuel bases × income multipliers + $5 a member, rounded half up and held between $200 and $1,000. I checked it against the printed FY2025 and FY2026 county charts and the live DHS lookup, and every cell I tried matched, including the cells that show half-up rounding (Allegheny coal $339; Adams electric size 7, $763).

The rest of the model also matches the sources:

  • the 150% FPG limits for three seasons;
  • the 20% benefit-only wage disregard;
  • the heat-in-rent and subsidized-housing rules;
  • per-source loss flooring;
  • the support exclusions;
  • the July 2026 and August 2025 supplements.

The tests are thorough (84 cases), and every income source is nonzero in at least one test.

Critical (1):

  • C1. CI fails on two main-side guards, and both name the PR's own P/income/sources/unearned.yaml. Raw military_retirement_pay drops military Survivor Benefit Plan annuities. Raw unemployment_compensation drops the modeled pa_uc.

Should address (5):

  • S1. Military basic pay is not counted as wages.
  • S2. The handbook is followed over the state plan and 55 Pa. Code in two places (the $25 interest threshold and the railroad Medicare premium), with no reviewed record. The three backlog candidates are stale.
  • S3. The FY2026 state plan URL that 24 references cite now returns 404.
  • S4. Three supplement parameter descriptions are off the house template.
  • S5. rulespec-us#1536 describes an older head.

Suggestions (3): the documented midseason guideline update, re-capturing the live lookup after the season opens, and housekeeping.

Status:

  • CI: 33 of 35 checks pass. The 2 failures are C1.
  • Branch: 0 behind main, and it merges cleanly.

Critical

C1. CI fails because the PR's unearned list names two raw inputs that main's guards reject.

  • Where: P/income/sources/unearned.yaml:18 (- military_retirement_pay) and :20 (- unemployment_compensation).
  • Failing tests (Rest Python shard 3):
    • test_military_retirement_readers.py::test_benefit_source_lists_do_not_read_raw_military_retirement_pay: "List military_retirement_income, not the retiree-only military_retirement_pay input ... Raw entries: ['parameters/gov/states/pa/dhs/liheap/income/sources/unearned.yaml:18']".
    • test_unemployment_compensation_readers.py::test_parameter_source_lists_do_not_read_raw_unemployment_compensation: "... Raw entries: ['parameters/gov/states/pa/dhs/liheap/income/sources/unearned.yaml:20']".
    • These are the shard's only 2 failures. The branch is 0 behind main, and main's own push run at c55b806541 passed, so the failures are this PR's.
  • Effect 1: SBP annuities drop out.
    • Since Wire military retirement pay and survivors into federal and state tax (#10181) #10182, survivor annuities go in military_retirement_pay_survivors. On main, military_retirement_pay is documented as pay "received by former members of the uniformed services".
    • So the comment at unearned.yaml:14-17 ("It includes military survivor benefits") is stale. So is integration Case 7 (T/integration.yaml:345-347), which enters a "1,000 a month military Survivor Benefit Plan annuity" as military_retirement_pay.
    • If that grandparent's annuity is entered where main now puts it, the head counts $0 of income. Benefit income is then $0, and the payment is 2.1 × (573.25 + 5) = 1,214.33, capped at $1,000, instead of $223.
  • Effect 2: modeled unemployment compensation drops out.
    • total_unemployment_compensation falls back to modeled_state_unemployment_compensation, whose list includes pa_uc from 2025.
    • A Pennsylvania claimant whose UC comes from the modeled pa_uc (UC wage and weeks inputs, with no reported amount) has that UC counted as $0. Plan 601.82(4)(v) counts "Unemployment compensation".
  • Severity: critical. This is a real CI failure caused by the PR (Alaska Add Alaska LIHEAP regular heating assistance #10341 C1 precedent), and each guard protects a counted source.
  • Fix:
    • List military_retirement_income and total_unemployment_compensation, and correct the comment.
    • Move Case 7's annuity to military_retirement_pay_survivors; it still expects $223.
    • Add one case that pins the UC fallback, for example modeled_state_unemployment_compensation with no reported UC.
    • Update the body and the axiom issue (S5).

Should address

S1. Military basic pay is not counted as wages.

  • Where: V/income/pa_liheap_countable_employment_income.py:23 reads only employment_income. On main, military_basic_pay is a separate input; employment_income adds only employment_income_before_lsr and the behavioral response.
  • Source: plan 601.82(1) (FY2026 p. 49, FY2027 p. 47): "Employee earnings are money, including wages, salaries, bonuses, commissions, and tips, before taxes or other deductions, that a person receives for providing services on behalf of an employer."
  • Effect (2026): take a Cumberland County family of three: a service member with $42,000 of basic pay, a spouse, a child aged 3, and natural gas heat.
    • Correct: $42,000 is above the $39,975 limit, so $0.
    • Head: income is $0, so the family is eligible. The payment is 2.1 × (521.05 + 10) = 1,115.21, capped at $1,000, plus the $100 supplement: $1,100.
    • With $30,000 of basic pay, the correct benefit income is 30,000 − 6,000 = $24,000, which pays the $200 minimum plus $100. The head again pays $1,000 + $100.
  • Precedent: Utah and Alaska (should). Utah's HEAT and Iowa's and North Dakota's LIHEAP on main list military_basic_pay as earned income.
  • Fix: add military_basic_pay to the countable wages, so that the dependent-child exclusion and the 20% disregard apply to it, and add a test.

S2. The handbook is followed over the state plan and 55 Pa. Code without a reviewed record.

  • What the code does:
    • V/income/pa_liheap_income.py:36-48 nets the Medicare premium from Social Security and railroad retirement.
    • :50-66 and P/income/monthly_interest_dividend_disregard.yaml count interest and dividends only above $25 a month.
    • Both comments say "the handbook governs".
  • What the plan and the regulation say: they agree with each other, and both are narrower than the handbook.
    • Plan 601.84(10) (FY2026 p. 52, FY2027 p. 50): "Medicare premiums deducted from Social Security benefit payments." 55 Pa. Code § 601.84(10): "Deductions for Medicare premiums deducted from Social Security benefit payments."
    • Plan 601.82(4)(ix) (FY2026 p. 51, FY2027 p. 48): "Interest and/or dividends from investments or bank accounts", with no threshold. 55 Pa. Code § 601.82(4)(ix): "Interest from investments or bank accounts."
  • What the handbook says:
    • 650.6(10): "Medicare premiums deducted from Social Security, Railroad Retirement, and other benefit payments."
    • 650.3(9): "of more than $25 per month".
  • Effect (2026):
    • Railroad. One person aged 67 with a $24,000 railroad annuity and the $2,434.80 Part B premium, in Adams County with electric heat. The head nets the premium: $21,565.20, eligible, $200 + $100 = $300. Under the plan, $24,000 is above $23,475, so $0.
    • Interest. T/income/pa_liheap_income.yaml Case 4 gives $9,660 at the head; under the plan it is $9,960.
  • The record:
    • The three candidates on main (us-pa:liheap-interest-dividend-threshold, us-pa:liheap-railroad-retirement-medicare-premium, us-pa:liheap-supplement-child-age) have follows: unknown, code: [] and state: unreviewed.
    • Their notes name a variable that does not exist (pa_liheap_countable_income) and still ask to "obtain the plan link".
    • The body's checkbox says no register entry applies because "None has a statute side". CLAUDE.md, however, puts the legal rule over a manual and asks that every such choice be recorded.
  • Precedent: Montana, Utah and Alaska rated a manual followed over the administrative rule, with no reviewed record, as a should.
  • Recommendation: follow the plan and the regulation. Count interest and dividends in full, and net Medicare premiums only from Social Security. If you want to keep the handbook readings, get the project lead's ruling. Either way, update the three candidates with follows and the real code paths (V/income/pa_liheap_income.py, P/income/monthly_interest_dividend_disregard.yaml, V/payment/pa_liheap_supplement.py, P/payment/supplement/child_age_limit.yaml), and correct the checkbox.
  • The supplement's child age is reasonable as it stands. The memoranda are the issuance directives for a discretionary payment that the plan says "may" be issued. It needs only the candidate update.

S3. The FY2026 state plan URL that 24 references cite now returns 404.

  • Where: https://www.pa.gov/.../heating-assistance_liheap/2026-liheap-state-plan.pdf (HTTP 404 today, a pa.gov "Page not found" page). It appears in 24 references:
    • 17 parameter files: every file under P/eligibility and P/payment/base, every P/income file except the interest disregard, plus maximum, minimum and regions;
    • 7 variable files: every one except the matrix amount and supplement variables.
    • The PR body links it too.
  • Comparison: the FY2025 plan already cites a Wayback capture for the same reason.
  • Replacement: http://web.archive.org/web/20251016232745/ + the same URL, the only capture. It is the "FISCAL YEAR 2026 FINAL STATE PLAN". I checked that its file pages match the PR's anchors: p. 5 (opening date, $200 and $1,000), p. 10 (limits), p. 31 (Appendix A), pp. 38-43 (601.31-601.42) and pp. 49-53 (601.81-601.84).
  • Severity: should, the same as the wave's live-URL rule.
  • Fix: swap all 24 hrefs and the body links to the capture, keeping the anchors.

S4. Three parameter descriptions are off the house template.

  • P/payment/supplement/amount.yaml: "... issues this one-time Low Income Home Energy Assistance Program supplement to vulnerable Cash recipient households, recorded in the calendar year of issuance." The program name sits mid-sentence and a clause trails it.
  • P/payment/supplement/child_age_limit.yaml and elderly_age.yaml: both end "... for the one-time Low Income Home Energy Assistance Program supplement."
  • The template: end at "under the Low Income Home Energy Assistance Program." (checklist item 6). New Mexico Add Washington LIHEAP regular heating assistance #10345 and Missouri Add New Hampshire LIHEAP regular heating assistance #10343 rated this a should.
  • Fix: for example, "Pennsylvania issues this one-time supplement to vulnerable Cash recipient households under the Low Income Home Energy Assistance Program." Move the issuance-year note to a comment.
  • Optional: P/eligibility/fpg_year_lag.yaml ("Pennsylvania's modeled opening-season income limit uses ...") could start "Pennsylvania uses ...".

S5. rulespec-us#1536 describes an older head.

  • Status: the issue is OPEN, labelled pe-parity, and otherwise dispatch-ready. It has module paths, corpus citations, verbatim law and companion tests from the charts, the plans and the memoranda.
  • Problem: it was last updated 2026-10-21 for a76331a0c4, before the head's 2026-10-28 commits:
    • its unearned-source list lacks pa_ssp;
    • it says the eligibility, income and region parameters "start at 2025-11-03 and backfill to 2025" (the head dates them 2024-11-04);
    • it says "The FY2025 final State Plan was not read" (the head now cites the archived FY2025 plan).
  • Fix: refresh it for the new head. After C1 and S1, also add military_retirement_income, total_unemployment_compensation and military_basic_pay, and whatever S2 decides.

Suggestions

G1. Midseason guideline update.

  • What the plans say:
    • FY2026 plan 601.31(1), p. 38: "the FPIG will update each January when the annual guidelines are posted."
    • FY2027 plan p. 35: "each February".
  • The gap: the model applies the season-opening limit all year. Calendar-2026 applicants after the update faced 150% of the 2026 guideline ($23,940 for one person), not $23,475.
  • Why a suggestion: the body and fpg_year_lag.yaml document the choice, and it is consistent with dating each season's tables to January 1. Consider stating the size of the gap in the parameter comment.

G2. The live lookup.

  • What I checked: the FY2027 bases come from the undated DHS lookup. I re-queried it today:
    • Adams electric: 812, 725, 738, 763 and 580;
    • Adams gas: 659, 709, 575, 618, 254 and 273;
    • Allegheny gas: 573, 623, 500, 544, 221, 223 and 240.
    • All of these still match the parameters. OPS 26-08-01 (August 13, 2026) and the plan's pre-season mailings support the FY2027 dating.
  • Suggestion: as rulespec-us#1536 also notes, re-capture after the November 2 opening, and consider keeping the raw responses so the 2027 values can be re-verified if the lookup changes midseason.

G3. Housekeeping.

  • V/pa_liheap.py cites OPS 26-07-01 but not OPS 25-08-01.
  • The chart references cite #page 1-469 (the whole document). Representative page lists would be easier to verify.
  • Optionally, add one supplement or income case that leaves medicare_part_b_premium to its formula (net of MSP coverage), since every current case pins it as an input.

Verified correct

Payment cells (formula vs. printed source):

Source Cells Head Printed
FY2026 chart p. 352 (Philadelphia) $4,000-4,999 gas, sizes 3/4/5/6/10/11: 1.25 × (521.05 + 5(n−1)) 664, 670, 676, 683, 708, 714 same
FY2026 chart p. 352 (Philadelphia) electric size 3: 1.25 × 532.10 665 665
FY2026 chart p. 352 (Philadelphia) coal size 3: 1.25 × 416.90 521 521
FY2026 chart p. 2 (Adams) $5,000-5,999, size 11: electric/oil/coal/gas/kerosene/wood 602, 811, 504, 601, 795, 657 same
FY2026 chart p. 2 (Adams) $6,000-6,999 electric size 3: 562.10 × 0.8727 491 491
FY2026 chart p. 2 (Adams) $4,000-4,999 oil size 3 / size 10 964 / 1,000 (cap) same
FY2026 chart p. 9 (Allegheny) $5,000-5,999 coal size 1: 338.50 rounded half up 339 339
FY2026 chart p. 9 (Allegheny) $4,000-4,999 oil size 1: 1.25 × 686.35 858 858
FY2026 chart p. 165 (Elk) $12,000-12,999 kerosene size 1: 905.80 × 0.3855 349 349
FY2026 chart p. 241 (Lackawanna) $10,000-10,999 electric size 3: 652.10 × 0.5061 330 330
FY2026 chart p. 102 (Chester) $12,000-12,999 gas size 3: 531.05 × 0.3855 205 205
FY2026 charts pp. 7, 56 $22,000-22,999, every fuel row 200 200
FY2025 chart p. 375 (Schuylkill) $12,000-12,999 coal size 4: 571.46 × 0.3855 220 220
FY2025 chart p. 353 (Philadelphia) $10,000-10,999 gas size 3: 469.26 × 0.5061 237 237
FY2025 chart p. 2 (Adams) electric $4,000-4,999 / $5,000-5,999 sizes 1 and 11 613 / 490, 540 same
FY2025 and FY2026 charts p. 93 (Centre) coal unchanged between seasons 439 439 both
Lookup (Adams electric) $4,000-4,999 sizes 3 and 7: 737.50 → 738 and 762.50 → 763 738, 763 same

Hand-checked expectations (all match the head):

  1. T/integration.yaml Case 1: TANF $4,836 → band $4,000; Philadelphia gas size 3 → 664; + 100 (child aged 3) = 764.
  2. Case 2: (22,800 − 2,434.80) + (12,000 − 2,434.80) = 29,930.40 ≤ 31,725 → multiplier 0 → 200 + 100 = 300.
  3. Case 3: SSI 11,928 + pa_ssp 22.10 × 12 = 265.20 → 12,193.20 → Elk kerosene 349 + 100 = 449.
  4. Case 4: 11,310 + 4,200 − min(4,200, 2,400) = 13,110; − 0.2 × 11,310 = 10,848 → 330.
  5. Case 5: the undocumented parent is not counted (size 3) but his $15,080 counts; benefit income 12,064 → 205 + 100 = 305.
  6. Case 6 (2025): limit 31,200 × 1.5 = 46,800; coal 220.
  7. Case 7 (2027): limit 21,640 × 1.5 = 32,460; the rental loss is floored; Allegheny gas size 2: 578.25 × 0.3855 = 222.92 → 223 (the lookup prints 223).
  8. T/pa_liheap.yaml Case 3: 24,000 − 2,434.80 = 21,565.20 ≤ 23,475 → 200 + 100 = 300.
  9. T/pa_liheap.yaml Case 1: heat in unsubsidized rent → 552; age 60 → 652; subsidized heat in rent → 0; $23,475.01 → 0; NONE → 0.
  10. T/income/pa_liheap_income.yaml Case 4: 3,600 + 600 + 600 + 1,200 × 4 = 9,600, + max(360 − 300, 0) = 9,660.
  11. Cases 9 and 10: 12,000 + 3,600 + 1,200 − max(1,200, 600) = 15,600; with two children, − max(2,400, 600) = 14,400.
  12. T/eligibility/pa_liheap_income_limit.yaml: FY2026 23,475 … 97,725 (+ 8,250 = 105,975) and FY2027 23,940 … 100,620 (+ 8,520 = 109,140) match FY2026 plan p. 10 and FY2027 plan p. 9. FY2025: 22,590, 79,080 and 103,290.

Checklist (one line each):

  1. Plan and matrix:
    • limits, opening dates, the $200 minimum and $1,000 maximum (FY2026 and FY2027 plans, p. 5), regions (Appendix A) and the matrix cells all verified;
    • the $1,000 bands and half-up rounding are pinned;
    • the FY2025 plan provisions were taken from the FY2026 plan's FY2025 column and the FY2025 chart.
  2. Income: gross wages; per-person, per-source loss floors (601.82(2)(ii)); the support exclusions (601.84(15)); dependent-child wages (601.84(21)); and nonqualified members' income counted (601.81(1)). Military basic pay is missing (S1), and the two handbook readings are S2.
  3. Backdating: three dated seasons; earlier years are documented as unverified backfill; supplement $0 in 2027.
  4. Arrangements:
  5. Tests: 84 cases; sizes 1-11 plus 12, 20 and 21; band edges in cents; every branch has a case.
  6. Descriptions: 21 of 24 follow the template (S4).
  7. References: the anchors show their values; the dead FY2026 URL is S3.
  8. Wiring: programs.yaml adds PA to the coverage line, keeps every main entry (ND, NH, UT, VA, WA and WY included) and adds a state_implementations block. No merged LIHEAP state feeds a household aggregate, so none is expected here. defined_for gates the state.
  9. Axiom: see S5.
  10. Branch: clean (see Branch).
  11. Integrate modeled state unemployment insurance into income flows #9558: fails (C1).
  12. IIJA: no IIJA supplement is encoded. The $100 supplements follow the memoranda's dates.
  13. Mutations:
    • Size > 0, the income limit, the responsibility test, the NONE override (eligibility Case 8), known_county (UNKNOWN → 0), the priced-fuel test, the size cap (21 → 647), the minimum, the maximum and half-up rounding are each pinned.
    • So are each supplement route, its age edges (4/5, 59/60) and the veterans exclusion.
  14. Document disjoint health premium inputs and correct consumer treatment #10046: no health-premium deduction is involved.
  15. NH Add New Hampshire state supplementation for 2026 #10156: no state-gated key lookups.
    • The region comes from np.isin with default 0, then max_(region, 1).
    • Counties outside the region lists map to ADAMS_COUNTY_PA before the keyed lookups.
    • Matrix Case 9 (an Ohio household) and eligibility Case 10 (New Jersey) pass.
  16. Wire military retirement pay and survivors into federal and state tax (#10181) #10182: see C1.
  17. State SSP: pa_ssp is listed (601.82(4)(iv) counts SSI, and nothing excludes the supplement) and pinned by integration Case 3. Without it the result is the $11,000 band, 400, not 349.

Other guards and checks:

  • Tie source-conflict markers to the register shared with Axiom #10287: test_source_conflicts.py passes in shard 3.
  • Count each Social Security benefit once in Pennsylvania CCW countable income #10408: social_security is read once, with no component also listed.
  • Medicare netting: it uses the computed medicare_part_b_premium (out of pocket, net of MSP coverage), always, not only when reported. That matches the handbook note "The CAO will not enter the premium as a medical expense if DHS pays it under the buy-in program." Part B only is accepted for a check-withheld rule (Wyoming and Iowa precedent).
  • Supplement memoranda: OPS 26-07-01 pp. 1-2 verified: "$100 supplemental payment to all vulnerable households that received a LIHEAP Cash grant during the 2025-2026 heating season", issued July 16, 2026, for "a member that has a disability, a member age 60 or older, or has a child under the age of five".
  • Federal limits: 150% of the guideline is within the 42 U.S.C. 8624(b)(2)(B) range (at least 110% FPG, at most max(150% FPG, 60% SMI)). The prior-year guideline only lowers the limit.

Axiom line assessment

axiom: TheAxiomFoundation/rulespec-us#1536 queued.

  • Status: OPEN, labelled pe-parity, and dispatch-ready in form.
  • Problem: it matches a76331a0c4, not the head (S5).
  • Next: once it is refreshed for the SSP, FY2025 dating, C1 and S1 changes and the S2 decision, the line is valid.

CI status

gh pr checks 9971 for run 40784941918 at 6a15c9fd2c, re-checked at the end of this review: 33 pass and 2 fail.

  • Failures: "Python tests / Full Suite - Rest (Python shard 3)" and its aggregator "Check Rest Python shards".
  • Log: the shard ran 2 failed, 3,080 passed and 11 skipped. Both failures are the guards in C1, and each names parameters/gov/states/pa/dhs/liheap/income/sources/unearned.yaml. This is a real failure caused by the PR, not infrastructure.
  • Known main-caused failure: test_social_security_input_state_machine does not appear, and main's push run at c55b806541 succeeded.
  • Passing: all baseline state shards (which run the 84 PA cases), Quick Feedback, test_programs_registry.py and test_source_conflicts.py.

Branch status

Verdict: REQUEST_CHANGES

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes: 1 critical (CI fails on the #10317 and #9558 guards: the unearned list uses raw military_retirement_pay and unemployment_compensation; use military_retirement_income and total_unemployment_compensation), 5 should. The payment formula reproduces every chart cell checked. Full report in the comment above.

# Conflicts:
#	policyengine_us/programs.yaml
hua7450 and others added 2 commits October 29, 2026 17:51
- Count military_retirement_income and total_unemployment_compensation,
  so Survivor Benefit Plan annuities and modeled state unemployment
  compensation count; Case 7 enters its annuity as a survivor payment.
- Count military basic pay as wages (section 601.82(1)).
- Follow 55 Pa. Code and the state plan over the handbook: count interest
  and dividends from the first dollar and net the Medicare premium from
  Social Security only, with railroad retirement counted in full. Mark
  the three Pennsylvania conflict candidates ruled.
- Point FY2026 state plan references at the Wayback capture, since the
  pa.gov copy returns 404.
- Align supplement descriptions with the template, document the size of
  the midseason guideline gap and cite Operations Memorandum 25-08-01.

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

hua7450 commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, David. All five should-fix items and the critical are addressed at 79acdea636 (fixes in 752ce6f1a2, then a main merge).

C1. The unearned list now names military_retirement_income and total_unemployment_compensation, and the stale survivor-benefit comment is gone. Integration Case 7 enters its annuity as military_retirement_pay_survivors and still expects $223. Income Case 20 pins the fallback: $5,980 of modeled state unemployment compensation with no reported amount counts as $5,980.

S1. pa_liheap_countable_employment_income adds military_basic_pay to wages (601.82(1)), so the dependent-child exclusion and the 20% benefit disregard apply to it. Employment-income Case 2 pins an adult's $30,000 and a dependent 17-year-old's $6,000 (excluded).

S2. We follow 55 Pa. Code and the plan.

  • Interest and dividends count from the first dollar (601.82(4)(ix)); the $25 parameter is deleted. Income Case 4 is now $9,960, Case 16 $14,900, and Case 18 counts $300 of interest.
  • The Medicare premium is netted from Social Security only (601.84(10)); railroad retirement counts in full as a pension. Case 15's railroad annuitant is now $22,200, and the old Case 21 (now Case 19) gives $10,600. The two cases that only tested the handbook readings are removed.
  • The three Pennsylvania candidates are state: ruled with sources, quotes and real code paths: interest and Medicare follow the regulation (follows: statute); the supplement child age follows the memoranda as a gap-fill, since no regulation sets it. The body's checkbox is corrected.

S3. All 24 references and the body now cite the 20251016232745 Wayback capture of the FY2026 plan, with the same page anchors.

S4. The three supplement descriptions end with "under the Low Income Home Energy Assistance Program."; the issuance-year note moved to a comment. fpg_year_lag now starts "Pennsylvania uses ...".

S5. rulespec-us 1536 is refreshed for 79acdea636.

Suggestions. G1: fpg_year_lag.yaml now states the gap (one person in calendar 2026: $23,940 after the update, $465 above the modeled $23,475). G3: pa_liheap cites OPS 25-08-01. I kept the charts' full page range because the fuel bases draw on every county's pages. G2, re-capturing the lookup, has to wait for the November 2 opening.

@hua7450
hua7450 requested a review from DTrim99 October 29, 2026 22:09
@DTrim99

DTrim99 commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator

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

PR #9971 (author: hua7450), head 79acdea636. Round 1 reviewed 6a15c9fd2c and requested changes (posted 2026-10-29 UTC). Since then the author pushed one fix commit (752ce6f1a2) and two merges of main (8449e009bc, which has one hand-resolved block, and 79acdea636, which merged cleanly). The PR still touches no partner test file.

Path prefixes:

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

Summary

All round-1 findings are fixed: the critical, the five shoulds and suggestion G1. For each critical and should fix, at least one test would fail if the fix were reverted.

  • C1: the unearned list now names military_retirement_income and total_unemployment_compensation. Integration Case 7 enters its annuity as military_retirement_pay_survivors and still pays $223. A new income case pins modeled unemployment compensation. Both main-side guards pass in CI.
  • S1: military basic pay now counts as wages, so the dependent-child exclusion and the 20% disregard apply to it.
  • S2: the model now follows 55 Pa. Code and the plan:
    • interest and dividends count from the first dollar, and the $25 parameter is deleted;
    • the Medicare premium is netted from Social Security only, and railroad retirement counts in full;
    • the three Pennsylvania candidates are state: ruled, with sources, quotes and code paths that exist.
  • S3: all 24 FY2026 plan references point to the Wayback capture.
  • S4: every description follows the template.
  • S5: rulespec-us#1536 describes the new head.

The hand-resolved programs.yaml block was right when it was made. Since then, six sibling LIHEAP PRs have merged, so the coverage line conflicts again (branch note).

Open: 0 critical, 0 should, 3 suggestions. All three are optional:

  • re-capture the lookup after the November 2 opening;
  • add one case that leaves the Part B premium to its formula;
  • handle a premium larger than the Social Security check.

Status:

  • CI: 36 of 36 checks pass at 79acdea636.
  • Branch: 35 behind main, with one conflicting line in programs.yaml.

Prior findings

Finding Status Evidence
C1. Raw military_retirement_pay and unemployment_compensation in the unearned list; CI failed on the #10317 and #9558 guards Fixed P/income/sources/unearned.yaml:16 lists military_retirement_income and :22 lists total_unemployment_compensation. The stale comment saying survivor benefits sit in retiree pay is gone. T/integration.yaml:346 enters military_retirement_pay_survivors: 12_000, and the case expects $223 (:382). With raw military_retirement_pay it would count $0 and pay $1,000. New T/income/pa_liheap_income.yaml Case 20 (:615) enters modeled_state_unemployment_compensation: 5_980 with no reported amount and expects total_unemployment_compensation 5,980 and income 5,980. With raw unemployment_compensation it would expect 0. In Rest Python shard 3, test_benefit_source_lists_do_not_read_raw_military_retirement_pay and test_parameter_source_lists_do_not_read_raw_unemployment_compensation both PASSED.
S1. Military basic pay not counted as wages (601.82(1)) Fixed V/income/pa_liheap_countable_employment_income.py:25-28 adds max_(military_basic_pay, 0) to wages before the where(dependent_child, 0, ...). So 601.84(21) excludes a dependent child's basic pay, and pa_liheap_benefit_income takes 20% off it. New T/income/pa_liheap_countable_employment_income.yaml Case 2: an adult's 30,000 counts → 30,000; a dependent 17-year-old's 6,000 is excluded → 0. Both fail if the term is dropped or moved outside the where. military_basic_pay is a standalone input on main (employment_income adds only employment_income_before_lsr and the behavioral response), so nothing is counted twice. Round-1 example, Cumberland family of three: $42,000 > $39,975 → $0 (was $1,100). With $30,000: benefit income 30,000 − 6,000 = 24,000 → $200 + $100 = $300 (was $1,100).
S2. Handbook followed over the plan and 55 Pa. Code ($25 interest threshold; railroad Medicare netting) with no reviewed record Fixed Interest. P/income/monthly_interest_dividend_disregard.yaml is deleted. interest_income and ordinary_dividend_income are in the unearned list (unearned.yaml:37-38), floored per person and source. Case 4 → 9,960, Case 16 → 14,900, Case 18 → 300. Medicare. V/income/pa_liheap_income.py:43-45 nets medicare_part_b_premium from social_security only, floored at zero. railroad_benefits is listed as a pension (unearned.yaml:18). Case 15 → 22,200, Case 19 → 10,600. The two cases that pinned only the handbook readings (combined interest disregard; one premium against Social Security and railroad combined) are removed, which is right because those branches no longer exist. Text check today: 55 Pa. Code § 601.82(4)(ix) reads "Interest from investments or bank accounts." and § 601.84(10) reads "Deductions for Medicare premiums deducted from Social Security benefit payments." Record. us-pa:liheap-interest-dividend-threshold and us-pa:liheap-railroad-retirement-medicare-premium are now follows: statute and state: ruled. us-pa:liheap-supplement-child-age is follows: gap-fill and state: ruled; no regulation sets that age, as round 1 accepted. All three entries have regulation or plan sources with https links, verbatim readings, examples and tracked code paths, and the stale pa_liheap_countable_income name is gone. The body's source-conflict checkbox now matches the code. Railroad round-1 example: $24,000 > $23,475 → $0 (was $300).
S3. FY2026 plan pa.gov URL 404s in 24 references Fixed All 24 hrefs (17 parameter files, 7 variable files) and the body use web.archive.org/web/20251016232745/ + the original URL, with the same anchors. The head has no bare FY2026 pa.gov href left. The pa.gov URL still returns 404 today. The Wayback CDX index lists capture 20251016232745 as application/pdf, status 200, 442,075 bytes. A direct fetch was rate-limited today (HTTP 429); round 1 checked the capture's pages.
S4. Three supplement descriptions off the template Fixed P/payment/supplement/amount.yaml: "Pennsylvania issues this one-time supplement to vulnerable Cash recipient households under the Low Income Home Energy Assistance Program." The issuance-year note moved to a comment. child_age_limit.yaml and elderly_age.yaml now end "... for the one-time supplement under the Low Income Home Energy Assistance Program." fpg_year_lag.yaml now starts "Pennsylvania uses ...". All 23 descriptions follow the template.
S5. rulespec-us#1536 stale Fixed Updated 2026-10-29 21:57 UTC for 79acdea636; see Axiom.
G1. Midseason guideline update Fixed The P/eligibility/fpg_year_lag.yaml comment and the body state the gap: 15,960 × 1.5 = 23,940, $465 above the modeled $23,475 for one person in calendar 2026.
G2. Re-capture the live lookup after the opening Deferred (accepted) It cannot be done before the November 2, 2026 opening. Still open as a suggestion (below).
G3. Housekeeping Partly fixed V/pa_liheap.py now cites OPS 25-08-01. The author kept the charts' #page 1-469 range because the fuel bases draw on every county's pages, which is a reasonable answer. Still not done: a case that leaves medicare_part_b_premium to its formula. All 10 premium entries in the tests are inputs (suggestion below).

New findings

Critical: none.

Should address: none.

Suggestions:

  • G1 (carried, G2). The FY2027 lookup. After the November 2 opening, re-query the DHS lookup and keep the raw responses, so the 2027 bases can be re-checked if the table changes midseason.
  • G2 (carried, G3). A formula-driven premium case. Add one income or integration case with an enrollee aged 65 or older whose medicare_part_b_premium is left to its formula (net of MSP coverage) rather than entered as an input.
  • G3 (new). A premium larger than the Social Security check.
    • The issue: Case 19 nets a $2,434.80 premium from $600 of Social Security and excludes all $600. In practice SSA withholds the Part B premium only when the monthly benefit covers it, and Medicare bills the enrollee directly otherwise. So on a strict reading of "deducted from Social Security benefit payments", nothing is excluded, and the case would give 11,200 rather than 10,600.
    • Why only a suggestion: it affects only people with less than about $203 a month of Social Security, and the floored netting is the pattern the wave accepted.
    • Options: net the premium only when Social Security covers it, or note the approximation in the pa_liheap_income.py comment.

Verified

Hand-checked expectations (every new or changed one matches the head):

  1. Employment Case 2: adult, not a dependent: 30,000 of basic pay → 30,000. Age 17 < 18 and a tax dependent → 0.
  2. Income Case 4: 3,600 + 600 + 600 + 1,200 × 4 = 9,600; + 120 interest + 240 dividends = 9,960.
  3. Income Case 15:
    • 2,500 × 12 = 30,000 of military retirement pay (through military_retirement_income), > 23,475 → ineligible.
    • 1,850 × 12 = 22,200 of railroad annuity. The 2,434.80 premium is not netted because there is no Social Security. 22,200 ≤ 23,475 → eligible.
  4. Income Case 16: 26,000 + max(−4,000, 0) = 26,000 (ineligible). 14,400 + 500 + max(−1,000, 0) = 14,900 (eligible).
  5. Income Case 18: 25 × 12 = 300.
  6. Income Case 19: max(600 − 2,434.80, 0) + 600 railroad + 10,000 pension = 10,600.
    • Without the floor: 8,765.20.
    • Netting from Social Security and railroad combined: 10,000.
    • With no netting: 11,200.
    • So the case pins the floor and the Social-Security-only rule.
  7. Income Case 20: reported UC 0 → total_unemployment_compensation = modeled 5,980 → income 5,980.
  8. Integration Case 7 (2027):
    • income: 12,000 survivor annuity + max(−1,800, 0) = 12,000;
    • limit: 21,640 × 1.5 = 32,460;
    • payment: Allegheny gas size 2, (573.25 + 5) × 0.3855 = 222.92 → 223, the lookup's printed value. There is no 2027 supplement.
  9. Unchanged neighbours still hold under the new formula:
    • T/pa_liheap.yaml Case 3 (Social Security, not railroad): 24,000 − 2,434.80 = 21,565.20 → $300;
    • integration Case 2: (22,800 − 2,434.80) + (12,000 − 2,434.80) = 29,930.40 → $300.

Mutation check of the fix commit. Every new term or branch fails at least one test if it is removed:

Term or branch Pinned by
military_basic_pay and its place inside the dependent-child where Employment Case 2
military_retirement_income Integration Case 7 and income Case 15
railroad_benefits Cases 15 and 19
total_unemployment_compensation Case 20
interest_income Cases 4, 16 and 18
ordinary_dividend_income Case 4 (9,720 without it)
The zero floor on net Social Security Case 19

No removed test pinned logic that still exists.

Merge resolution (8449e009bc, programs.yaml).

  • The hand-resolved coverage line is main's line at the time (which had just added AK) plus PA. It keeps all 32 main entries and adds PA, with nothing dropped or duplicated (33 entries).
  • 79acdea636 merged cleanly.
  • Against the merge base, the PR's programs.yaml diff is only PA on the coverage line and one PA state_implementations block (between OK and RI).
  • Its candidates.yaml diff is only the three PA entries.

Other checks:

Axiom

axiom: TheAxiomFoundation/rulespec-us#1536 queued. The line is valid.

  • Status: OPEN, labelled pe-parity, last updated 2026-10-29 21:57 UTC.
  • Head: it describes 79acdea636 and names 752ce6f1a2 as the answer to round 1.
  • It reflects the fixes:
    • the wage formula with military_basic_pay;
    • the unearned list with military_retirement_income (retiree pay plus military_retirement_pay_survivors), railroad_benefits, total_unemployment_compensation, interest_income and ordinary_dividend_income;
    • Social Security net of the Part B premium only;
    • interpretations 10 and 11, which record that the regulation and plan are followed over handbook 650.3(9) and 650.6(10) (ruled 2026-10-29);
    • the FY2025 dating from 2024-11-04;
    • 84 cases in 10 files.
  • Companion cases: they come from the plans, the regulation and the charts, and cover the changed tests: employment Case 2, income Cases 4, 15, 16, 18, 19 and 20, and integration Case 7.

CI

gh pr checks 9971, re-checked at the end of this review: 36 of 36 pass (run 40961299855 at 79acdea636, on the PR merge ref with main as of 2026-10-29).

  • Rest Python shards 3 and 4: shard 3 (1,306 passed, 1 skipped) runs the four guards above; shard 4 runs test_programs_registry.py. Both pass.
  • Baseline state shards: all pass. They run the PA YAML cases.
  • Failures: none. Round 1's two guard failures are gone, and neither known main-caused failure appears.
  • Next run: the PR now shows CONFLICTING, so CI will not re-run until the branch is updated.

Branch

Verdict: APPROVE

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving round 2: 0 critical, 0 should. The guard failure (C1) and all five shoulds are fixed and pinned; CI is 36/36. Full report in the comment above. Branch note: more LIHEAP states merged, so re-resolve the coverage line (main's line plus PA) before landing.

hua7450 and others added 2 commits October 30, 2026 10:48
# Conflicts:
#	policyengine_us/programs.yaml
42 CFR 408.42 has the Railroad Retirement Board deduct the Part B premium
from railroad benefits for an enrollee entitled to them, so section
601.84(10)'s exclusion of premiums deducted from Social Security does not
apply. For other enrollees, 42 CFR 408.43 withholds the whole benefit when
it is smaller than the premium, so the exclusion stays capped at Social
Security. Income Case 19 now expects 11,200.

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

hua7450 commented Oct 30, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, David. Main is merged at ad816444d3: the coverage line is main's states plus PA, as you listed.

On G3, the federal rules split the case in two, so I made one change in 87aaae3718:

  • Social Security smaller than the premium. 42 CFR 408.43 says SSA deducts the premium from Social Security, and "If the benefit is less than the monthly premium, the benefit is withheld and the enrollee is required to pay the balance through direct remittance." The whole benefit is still deducted, so capping the exclusion at the benefit is right. Income Case 14's second household ($1,800 of Social Security) still pins that cap.
  • Railroad annuitants. 42 CFR 408.42 has the Railroad Retirement Board deduct the premium from railroad benefits "even though he or she is also entitled to social security benefits", so nothing is deducted from Social Security, and 601.84(10) excludes nothing. pa_liheap_income now subtracts the premium from Social Security only for a person without railroad_benefits. Income Case 19 ($600 Social Security, $600 railroad, $10,000 pension) now expects 11,200.

The PR body and rulespec-us 1536 describe 87aaae3718. G1 (re-capturing the lookup after November 2) and G2 (a computed-premium case) are still open as optional suggestions.

@hua7450
hua7450 merged commit f051731 into main Oct 30, 2026
1 check failed
@hua7450
hua7450 deleted the pa-liheap branch October 30, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Pennsylvania LIHEAP regular heating assistance

2 participants