Skip to content

Add New York HEAP regular heating assistance - #9744

Merged
hua7450 merged 12 commits into
mainfrom
ny-heap
Oct 8, 2026
Merged

hua7450 merged 12 commits into
mainfrom
ny-heap

Conversation

@hua7450

@hua7450 hua7450 commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Adds New York's regular HEAP heating benefit for FY2024 to FY2026, including income/categorical eligibility, household composition, fuel and heat-in-rent payments, and Tier I/vulnerability supplements. A Tier I oil-heating household receives $961, or $996 with a qualifying vulnerable member.

Fixes #9936.

Implementation

  • Reuses the federal poverty guideline helper with a one-year lag and federal SMI size factors. The published size 1–13 income limits reconcile for FY2024, FY2025 and FY2026; the two-stage SMI flooring order is the one ACF IM 2025-02 Attachment 4 prints. For size 14+, the stated percentage rule controls despite the matrix's inconsistent additional-person shortcut.
  • The base amounts, the $35 vulnerable add-on and the heat-included levels are unchanged across the FY2024–FY2026 matrices; only the Tier I add-on moved, from $41 (FY2023–FY2024) to $61 (FY2025 on), and both values are dated.
  • Excludes foster members and federal living-arrangement Code C SSI recipients using existing inputs. One derived person flag supplies the exclusion consistently to household size, income, vulnerability and SSI categorical eligibility; it introduces no new question/input. 18 NYCRR 393.4(b) would count a Code C child; the model follows the manual and records the conflict. Nonqualified members are excluded from size and vulnerability, but their income counts in full. No SNAP immigration bars are imported.
  • SNAP, TANF and Code A SSI establish categorical income eligibility and Tier I treatment. Code A follows 393.4(c)(1)(iii) and the manual glossary: an SSI individual or couple living alone in the federal own-household arrangement, so the SPM unit may hold no one but the recipient or couple (foster members aside). Uses annual tanf receipt, with take-up, instead of monthly New York entitlement. TANF is not counted as income because plan item 1.9 leaves its box unchecked and the manual counts a TA grant only for a roomer-budgeted minor (D.11(c)(1)).
  • Uses parameter lists for earnings and other income (including survivor benefits, farm rent and miscellaneous income under D.12(a)(13), (14), (27) and (38)), the current ordinary-dividend input, and existing net business income without extra business/work-expense deductions. Social Security and railroad benefits count net of the computed Medicare Part B premium (D.12(a)(6) and (8), D.14(b)(4)); Part D has no variable. Every source is floored at zero, so a rental or estate loss does not offset other income (D.14(b)(7)). Dependent child/student earnings are excluded; monthly household income is rounded down.
  • Uses the existing USDA disability calculation for the supplement. A bare disability flag is insufficient. The state dwelling input covers eligible group and ineligible residence categories; ordinary housing treatment reuses existing inputs.
  • Uses undated formulas and verified_years: "2024-2026". Earlier results are unverified estimates, with a test of the requested-year guideline lag.

Remaining coverage

Medicare Part D premiums are not deducted. Other gaps include recurring receipt at application, Safety Net Assistance receipt, roomer/employee/fleeing-felon distinctions, certification pathways not identified by USDA flags, and detailed income exclusions. January living arrangements approximate application-month status.

Market-rent voucher exceptions, payment timing and previously advanced nominal awards remain partial. An unknown direct-heating fuel returns zero as a coverage gap (#9754). Crisis, cooling and equipment assistance are excluded. The official 2021 manual supplies structural rules; the FY2024–FY2026 documents supply amounts. Archived official copies were used where live OTDA downloads failed.

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

54 program YAML cases cover all fuel mappings, the Tier II limits at the odd sizes and the Tier I lines at the even sizes plus the size-14 boundary, the FY2024 and FY2025 limits and Tier I lines, the $41 FY2024 add-on, mixed-status households, foster/Code C exclusions (including reported receipt), Code A living alone (an aged parent living with family, an SSI couple, a recipient outside the own-household arrangement), categorical receipt versus entitlement, disability pathways, the Medicare deduction, a rental loss, the ineligible-residence and vulnerable-heat-in-rent branches and integrated benefit amounts. Every case specifies heating_type.

Official sources

axiom: TheAxiomFoundation/rulespec-us#1480 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 (fb96daf).
⚠️ Report is 296 commits behind head on main.

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

Count survivor_benefits, farm_rent_income and miscellaneous_income under HEAP Manual D.12(a)(13), (14), (27) and (38); stop adding TANF to countable income (state plan item 1.9 leaves it unchecked; TANF receipt still confers categorical eligibility); point the Tier I links at plan page 10; name the Medicare premium variables that are not wired in; place New York between IL and TX in programs.yaml. Rewrite the tests to the shared conventions with fuel bills, derivation comments and end-to-end coverage of categorical, over-income, heat-in-rent and public-housing paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Write parameter date keys unquoted, wrap each variable's documentation
string across lines, and split the household-size sweeps between files
so each size is asserted once: odd sizes in the income-limit test, even
sizes in the Tier I maximum test.

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: New York HEAP regular heating assistance

PR #9744: Add New York HEAP regular heating assistance (author: hua7450, head 194a3b97bc)

Scope: New York's LIHEAP program, run by OTDA as the Home Energy Assistance Program (HEAP). The PR covers the regular heating benefit only. Direct heaters get a base amount by fuel ($400 utility, $900 oil/kerosene/propane, $635 other), plus $61 for Tier I and $35 for a vulnerable member. Heat-in-rent households get $50 (Tier I) or $45 (Tier II), or the $21 nominal amount in subsidized housing and listed group residences. Crisis, cooling, HERR and clean-and-tune are out of scope. The PR changes 39 files: 15 parameters, 12 variables, 10 test files (41 cases), programs.yaml and a changelog fragment. Every parameter has a single 2025-10-01 value (FY2026), and the registry lists verified_years: "2026".

Overall: the FY2026 values are right. Every amount matches the FY2026 matrix and plan, and all 13 published size 1-13 income limits and Tier I lines reproduce. The FY2025 and FY2024 limits also reproduce from the same formula. Five problems remain:

  • One categorical-eligibility rule is wrong for a common household type in 2026. Any SSI recipient counts as "Code A", even when they do not live alone (C1).
  • Two income rules depart from the manual: Social Security is counted gross, and losses offset other income (S1, S2).
  • The Tier I add-on is backfilled at the wrong amount for 2023-2024 (S3).
  • The remaining should-address items cover tests and parameter descriptions (S4, S5).

Path prefixes used below:

  • P/ = policyengine_us/parameters/gov/states/ny/otda/heap/
  • V/ = policyengine_us/variables/gov/states/ny/otda/heap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/ny/otda/heap/

Line numbers refer to head 194a3b97bc. The benefit amounts below were reproduced by running the PR's code on single households.

Source Documents

# Document Link Use
1 FY2026 NY LIHEAP State Plan (52 pp) NY_Plan_2026.pdf 1.4a CE (p5); 1.7b, 1.8, 1.9 income (p6-7); 2.1 limits, vulnerable add-on (p9); Tier I, min/max (p10)
2 OTDA HEAP Manual, edition dated 7/1/2021 (113 pp; the current posted edition) HEAP-manual.pdf Ch. 8 D.4 (p36), D.9-D.12 (p37-39), D.13 (p39-40), D.14 (p40-43), F (p45-47); Ch. 9 B.3 (p49); payments (p84-88); glossary (p110). OTDA blocks scripted downloads, so the audit used the Wayback copy
3 FY2026 benefit matrix and income guidelines (.docx) NY_BenefitMatrix_2026.docx All FY2026 amounts and the size 1-13 limits
4 FY2025 benefit matrix (.docx) NY_BenefitMatrix_2025.docx Not cited in the PR. Same amounts as FY2026; limits reproduce for period 2025
5 FY2024 and FY2023 benefit matrices NY_BenefitMatrix_2024.pdf; NY_BenefitMatrix_2023.pdf Not cited in the PR. They govern the backfilled periods 2023-2024 (S3)
6 FY2025 NY LIHEAP State Plan NY_Plan_2025.pdf Same $35 / $61 / 130% / $21-$996 terms as FY2026
7 ACF LIHEAP IM 2025-02, Attachment 4 (FY2026 SMI table) Att. 4 NY SMI4 $133,609 and the floored 60% amounts (G6)
8 18 NYCRR 393.4 (LII; no official online NYCRR copy) 18 NYCRR 393.4 393.4(b) household; 393.4(c)(1) categorical eligibility; (c)(3)-(4) living arrangements

Critical (Must Fix)

C1. Categorical eligibility treats every SSI recipient in the federal own-household arrangement as "Code A". Code A requires the recipient to live alone.

  • Where:

    • V/eligibility/ny_heap_categorically_eligible.py:25-33. Line 26 is code_a = arrangement == arrangement.possible_values.OWN_HOUSEHOLD, and lines 27-33 OR it into the SNAP/TANF test.
    • The same flag sets Tier I in V/payment/ny_heap_tier_one.py:23-25.
  • Sources:

    • 18 NYCRR 393.4(c)(1)(iii): categorically eligible households include "households, any member of which is in receipt of SSI as a couple or individual living alone (SSI Living Arrangement Code A)".
    • Manual glossary, p. 110: "SSI Code A – Supplemental Security Income (SSI) recipients who 'live alone' and one member of SSI couples 'living alone' (Code A) on the SDX file only. To be categorically eligible, an individual must be Code A for both State and Federal SSI".
    • FY2026 plan 1.4a, p. 5: "Code A Supplemental Security Income (SSI) (state and federal living arrangement code of 'A')".
    • rulespec-us#1480 quotes 14-ADM-07 Attachment 2: "Living alone means living in a private household composed of one eligible individual or one eligible couple." Under the same attachment, an individual who "lives with an ineligible spouse", lives with a child, or "resides in a dwelling with others and prepares food in common" is not living alone.
  • Why it is wrong:

    • Federal code A means "own household". State code A means "living alone". The PR's test checks only the federal code.
    • ssi_federal_living_arrangement defaults to OWN_HOUSEHOLD and returns it as the residual (policyengine_us/variables/gov/ssa/ssi/eligibility/status/ssi_federal_living_arrangement.py:17, 70-78). It returns another value only for a medical facility, a child with an ineligible parent, or when the three another-household inputs are all set.
    • So code_a is true for almost every adult SSI recipient, including those who live with their children, siblings or other relatives.
  • Impact in period 2026 (reproduced by running the PR's code): a 72-year-old mother on SSI lives with her daughter and son-in-law, who earn $84,000 a year ($7,000 a month), and the household heats with gas.

    PR 393.4(c)(1)(iii)
    Categorical Yes (the model computes $11,928 of SSI, own household) No (not living alone), so the household is income tested
    Countable income n/a $95,928 = $7,994 a month, over the $5,611 size-3 limit
    Benefit $400 + $61 (Tier I via CE) + $35 (vulnerable) = $496 $0

    Households within the limit but above 130% FPL also get the $61 Tier I add-on they should not get, or $50 instead of $45 when heat is included in rent.

  • Why this is not a documented gap: the PR documents "State SSI Code A ... approximated", but the approximation never tests "living alone". That condition can be determined from household composition. rulespec-us#1480 gap 2 says so too: conditions 1-3 "are determinable from existing inputs".

  • Fix:

    1. Treat an SSI recipient as Code A only when the SPM unit (the PR's household proxy) contains no one besides one SSI individual or one SSI couple. A foster child does not count against this. For example:
      receives = (person("ssi", period) > 0) | (add(person, period, ["receives_ssi"]) > 0)
      foster = person("is_in_foster_care", period.first_month)
      head_or_spouse = person("is_tax_unit_head_or_spouse", period)
      # State Code A: one eligible individual or one eligible couple.
      living_alone = spm_unit.all(foster | (receives & head_or_spouse))
      qualifying_ssi = living_alone & spm_unit.any(
          receives & code_a & qualified & ~excluded
      )
    2. Keep the federal OWN_HOUSEHOLD check, since the glossary requires both codes.
    3. Add tests:
      • The aged-parent household above: ny_heap_categorically_eligible: false, ny_heap: 0.
      • An SSI couple living alone: categorical.
      • A federal ANOTHER_PERSONS_HOUSEHOLD recipient living alone: not categorical (see S4b).
    4. Keep T/eligibility/ny_heap_categorically_eligible.yaml:23-37 (Case 2). It is a single person, so it stays correct.
    5. Update the documentation at lines 15-21 and rulespec-us#1480 gap 2.

Should Address

S1. Social Security and railroad benefits count gross, but the manual and the plan count them net of Medicare premiums

  • Where:
    • P/income/sources/unearned.yaml:4-8. The comment says "medicare_part_b_premium and medicare_part_b_premiums_reported are not wired in yet, so both count gross".
    • V/income/ny_heap_countable_income.py:17-18, 41-45.
    • T/income/ny_heap_countable_income.yaml:8-39 (Case 1) pins the gross treatment, with a medicare_part_b_premium: 1_200 input that must not reduce the total.
  • Sources:
    • Manual D.12(a)(6) and (8), p. 38: "Social Security payments after deduction of Medicare premium, Part B and D" and "Railroad Retirement benefits after deduction of applicable Medicare premium, Part B and D".
    • D.13(a)(6), p. 39 excludes the "Medicare premium Part B deducted from monthly Social Security benefit and Part D Medicare premium".
    • D.14(b)(4), p. 41: "The gross monthly benefit amount after the deduction of the Medicare Part B and Medicare Part D premiums is counted as income."
    • FY2026 plan 1.9, p. 6: the SSA benefits box is ticked with "Excluding MediCare deduction" checked.
  • Why "not wired in yet" does not hold:
    • medicare_part_b_premium exists and is computed. It is the out-of-pocket premium, net of Medicare Savings Program coverage.
    • Two merged batch-1 programs already subtract it: policyengine_us/variables/gov/states/ky/dcbs/liheap/ky_liheap_income.py:36-45 and policyengine_us/variables/gov/states/in/ihcda/eap/in_eap_income.py:40-47.
  • Impact in period 2026 (reproduced): a single 70-year-old with $3,600 a month of Social Security, gas heat. The model's premium is $2,434.80 a year ($202.90 a month), and MSP does not apply at this income.
    • PR: $3,600 is over the $3,473 limit, so $0.
    • Manual: $3,600 − $202.90 = $3,397 (rounded down), which is within the limit, so $400 + $35 = $435.
    • The eligibility band covers every enrolled Social Security recipient within one premium of the limit: $203 a month for a single person, $406 for a couple.
  • Fix:
    • Per person, count max_(social_security + railroad_benefits - medicare_part_b_premium, 0), as KY and IN do. Remove the two sources from the plain list, or keep them there and subtract the premium in the formula.
    • Keep the Part D gap documented, since there is no Part D variable.
    • Change Case 1 to expect 29,041 − 1,200 = 27,841, so floor(27,841 / 12) × 12 = 27,840.
    • Update rulespec-us#1480 companion test 14(a), which encodes "Social Security gross (settled decision)".

S2. A loss in one unearned source offsets wages and other income

  • Where:
    • V/income/ny_heap_countable_income.py:31-37: add(person, period, p.sources.unearned) sums signed values. Only the household total is floored at line 45.
    • Line 20 says "Rental income is assumed nonnegative".
    • P/income/sources/unearned.yaml:25-28 lists rental_income, farm_rent_income and estate_income. All three are signed inputs (estate_income is documented as "Net income or loss").
  • Sources:
    • Manual D.14(b)(7), p. 41: the three-month net profit is averaged "with negative income converting to '0' for the purposes of averaging". The same page lists, under "not allowable deductions for HEAP": "(ii) Depreciation (iii) Net loss".
    • D.12(a)(27), p. 38: "Income from rents after allowable deductions".
  • Impact in period 2026 (reproduced): a single adult with $43,200 of wages and a −$6,000 rental loss, gas heat.
    • PR: countable income is $37,200 ($3,100 a month), so $400.
    • Manual: $3,600 a month is over $3,473, so $0.
  • Inconsistency: the earned loop already floors each source (V/income/ny_heap_countable_earned_income.py:22-23).
  • Fix:
    • Loop over p.sources.unearned with max_(person(source, period), 0), matching the earned loop and ky_liheap_income.py:33-35 ("a loss in one source cannot offset another").
    • Replace the sentence at line 20, and add a rental-loss test like the example above.

S3. The Tier I add-on is backfilled at $61 to periods 2023-2024, where the published matrices pay $41

  • Where:
    • P/payment/tier_one_supplement.yaml:2-3 has a single 2025-10-01: 61 entry, so every earlier period gets $61.
    • V/ny_heap.py:19-20 says "Earlier years are unverified backfilled estimates."
  • Sources:
    • FY2024 matrix, p. 1 ("2023-2024 HEAP Direct Heating Regular Base Benefit Levels"): "Tier I $41 Add-on"; "Utility $400 ... $441".
    • FY2023 matrix, p. 1 prints the same $41.
    • The FY2025 matrix and the FY2026 matrix print $61.
    • Every other amount is identical in the FY2023-FY2026 matrices: base $400/$900/$635, vulnerable $35, heat included $21/$45/$50.
  • Impact (reproduced): in periods 2023 and 2024 a zero-income gas heater gets $461, against the matrix's $441. Every Tier I and categorical direct heater is overpaid by $20.
  • What is already correct:
    • Period 2025 carries back correctly.
    • The PR's formula reproduces all 13 FY2025 limits and Tier I lines from the FY2025 SMI and the 2024 guidelines.
    • It also reproduces all 13 FY2024 rows (e.g. size 1: 1,579 / 3,035; size 13: 8,261 / 9,532, where 150% FPL binds).
  • Why should, not critical: the claimed year is 2026, and no test asserts a pre-2025 benefit (the period-2022 test checks only the guideline lag). The values are now confirmed wrong against official matrices, though, and the fix is two lines.
  • Fix:
    • Add 2022-10-01: 41 and 2024-10-01: 61 to tier_one_supplement. Cite both matrices.
    • Add a period-2024 case expecting $441.
    • verified_years can then honestly read "2024-2026". It could already read "2025-2026" today.
    • Values before 2022-10-01 stay unverified. The 2021 manual prints heat-included amounts of "$21, $40 and $45" (p. 88), against the PR's $21/$45/$50.

S4. Four branches have no test that would fail if they broke

Coverage is otherwise strong: all 11 fuels, both income boundaries, ages 59/60 and 5/6, every disability pathway, mixed-status households, and foster and Code C exclusions. These branches are not pinned:

  • (a) Ineligible residence.
    • T/ny_heap.yaml:147-160 (Case 9) expects [21, 0], but household 2 has no gas_expense. So has_heating_expense is false, and ny_heap_eligible is false at V/eligibility/ny_heap_eligible.py:23-27 whatever the dwelling type. It duplicates Case 7.
    • Deleting & (dwelling != types.INELIGIBLE_RESIDENCE) at line 36 would pass all 41 cases.
    • Source: Manual F.4, p. 46.
    • Fix: set gas_expense: [0, 1_500], keeping [21, 0]. Household 1 still shows that a group residence needs no bill.
  • (b) SSI outside the own-household arrangement.
    • No test sets ANOTHER_PERSONS_HOUSEHOLD or MEDICAL_TREATMENT_FACILITY. The only non-own-household SSI case is already removed by the Code C exclusion.
    • Deleting & code_a at V/eligibility/ny_heap_categorically_eligible.py:29 would pass all tests. Add the negative case with the C1 fix.
  • (c) Vulnerable member with heat included in rent.
    • V/ny_heap.py:45-54 correctly gives heat-included and group households no add-ons (plan p. 9: the $35 goes "to their base Regular heater benefit").
    • Every heat-included test household has members aged 8-58 with no disability: T/ny_heap.yaml Cases 3, 9 and 14, and T/integration.yaml Cases 7-8. Adding + vulnerable * p.vulnerable_supplement to heat_included would pass.
    • Add a 65-year-old with heat in rent: $50 in Tier I and $45 in Tier II, not $85/$80.
  • (d) Code C exclusion through reported receipt.
    • The add(person, period, ["receives_ssi"]) > 0 arm of V/eligibility/ny_heap_is_excluded_person.py:19-21 is never exercised. Case 2 of T/eligibility/ny_heap_household_size.yaml:59-74 sets receives_ssi: false, and Case 1 uses an ssi amount.
    • Add a CHILD_IN_PARENTAL_HOUSEHOLD child with receives_ssi: true and ssi: 0, expecting household size 1.

S5. Parameter descriptions use verbs outside the template

The same pattern was flagged in #9728 (S5) and #9726 (S8). Merged LIHEAP parameters use provides/sets/limits/uses/excludes.

  • "pays": P/payment/heat_included/nominal.yaml:1, P/payment/heat_included/tier_one.yaml:1, P/payment/heat_included/tier_two.yaml:1. Use "provides".
  • "adds": P/payment/tier_one_supplement.yaml:1, P/payment/vulnerable_supplement.yaml:1. Use "provides".
  • "treats ... as vulnerable": P/payment/elderly_age.yaml:1, P/payment/young_child_age.yaml:1. Use "sets".
  • "assigns": P/payment/tier_one_fpg_rate.yaml:1. Use "limits Tier I to".
  • "counts": P/income/sources/unearned.yaml:1. Use "uses", as earned.yaml already does.
  • P/payment/base.yaml:1 ends "...under the Home Energy Assistance Program, by primary heating fuel." Move "by primary heating fuel" before "under" so that the sentence ends with the program name.
  • Source: repository parameter-description convention; there is no regulatory page.

Suggestions

G1. Safety Net Assistance is not a categorical program here (documented)

  • Where: V/eligibility/ny_heap_categorically_eligible.py:19-20 documents it.
  • Sources: 393.4(c)(1)(ii) and Manual D.10(c), p. 37 include SNA. The FY2026 plan, p. 5 says "federally funded Public Assistance (PA)", which matches the PR's TANF-only test.
  • There is no NY SNA variable, and many SNA households also receive SNAP. Acceptable as documented.

G2. Point the unknown-fuel $0 at #9754 in code and tests

  • Where: P/payment/base.yaml:20-21 (UNSPECIFIED: 0); V/ny_heap.py:22-23, 33-39; T/ny_heap.yaml:215-227 (Case 12), which pins $0 for a household with has_heating_expense: true.
  • The matrix pays every direct heater at least $400. The PR body defers this to Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754, which the batch accepts, but neither the code nor Case 12 mentions it.
  • Add a # See #9754 note in base.yaml, ny_heap.py and Case 12, so that the pinned $0 reads as provisional.

G3. The Code C exclusion conflicts with 393.4(b) (kept as a suggestion: the PR follows the manual literally)

  • Where: V/eligibility/ny_heap_is_excluded_person.py:17-22.
  • Supporting the PR: Manual D.4(a)(5), p. 36 says "Federal living arrangement code C SSI recipients" are "not considered household members". Federal code C is the child-in-parental-household code.
  • Against:
    • 393.4(b) presumes relatives to be household members and lists only "adults in foster care, foster care children and nonrelated roomers and/or boarders" as exclusions.
    • Manual D.3(a)(2), p. 35: "There is no exception for children under the age of 21".
  • Effect: an SSI child's vulnerability (+$35) and size are dropped. T/eligibility/ny_heap_household_size.yaml:6-57 expects $400, where the regulation reading gives $435.
  • Record the tension and all three citations in the variable. Link the owner decision that rulespec-us#1480 gap 1 leaves pending.

G4. Use one definition of SSI receipt (kept as a suggestion: outcomes rarely differ today)

  • The categorical test uses only person("ssi", period) > 0 (V/eligibility/ny_heap_categorically_eligible.py:28). The exclusion (ny_heap_is_excluded_person.py:19-21) and vulnerability (through is_usda_disabled) also count receives_ssi, following the repo convention ("Reported SSI receipt, for people the model gives a $0 payment").
  • In practice a reported recipient living alone is SNAP categorically eligible in the model, so snap > 0 already confers HEAP categorical eligibility.
  • Apply the convention when rewriting the line under C1, as the sketch there does.

G5. Published thresholds are tested at only half the sizes, and size 14 Tier I has no test

  • T/eligibility/ny_heap_income_limit.yaml:17-18 and T/payment/ny_heap_tier_one.yaml:14-15 each say the other file covers the remaining sizes. In fact:
    • Tier II limits are tested at odd sizes 1-13 and size 14.
    • Tier I lines are tested at even sizes 2-12.
  • The untested halves reproduce: Tier II 6,680 / 8,818 / 9,218 / 9,619 / 10,020, and Tier I 1,695 / 2,887 / 4,078 / 5,270 / 6,462 / 7,653 / 8,845.
  • Size 14+: the PR applies the plan rule (plan 2.1, p. 9: "HHS Poverty Guidelines 150.00%"), giving 10,893. The matrix's "+687" shortcut gives 10,908. That is a documented, reasonable choice. For Tier I, the formula gives 9,441 against the matrix's 8,845 + 595 = 9,440, with no test.
  • Add the missing halves (one vector line per file) and a size-14 Tier I boundary case, then reword the two comments.

G6. The SMI flooring order has a source and is not "inferred"

  • Where: V/eligibility/ny_heap_income_limit.py:23-24, and the PR body ("inferred from the numbers").
  • ACF IM 2025-02 Att. 4, p. 2 prints NY "$133,609" with 60% columns "$41,685 / $54,512 / $67,338 / $80,165 ...". So 0.6 × SMI4 is floored first, then each size factor is applied.
  • Cite Att. 4 in the references and drop "Inferred".

G7. The fpg_year_lag reference is empirical only

  • Where: P/eligibility/fpg_year_lag.yaml:8-10 cites only the matrix.
  • The lag reproduces: the FY2026 Tier I lines equal 130% of the 2025 guidelines (floor(1.3 × 15,650 / 12) = 1,695), and FY2025/FY2024 reproduce from the 2024/2023 guidelines.
  • Add the 2025 HHS poverty guideline notice (or the LIHEAP IM naming the guideline year). Add a comment that model period Y uses FFY Y.

G8. miscellaneous_income is counted although plan 1.9 leaves "Other" unchecked

  • Where: P/income/sources/unearned.yaml:29-30.
  • Sources: the plan, p. 7 leaves "Other" unchecked, and p. 6 leaves "Cash gifts" and "One-time lump-sum payments" unchecked. Manual D.12(a)(38), p. 39 counts "All other income not specifically excluded"; D.13 excludes non-recurring lump sums.
  • Counting under the manual is defensible. Note the plan/manual divergence in the YAML comment.

G9. Reference titles, anchors and citations (checklist #6)

  • No comma #page=a,b anchors, which is good. Remaining items:
  • Page text in titles:
    • "PDF pages 9, 10" in P/eligibility/smi_rate.yaml:9, P/payment/elderly_age.yaml:9, P/payment/young_child_age.yaml:9 and P/payment/vulnerable_supplement.yaml:9. Page 9 alone carries the 60% rule, the ages and the $35.
    • "PDF page 10" in P/payment/tier_one_fpg_rate.yaml:9 and P/payment/tier_one_supplement.yaml:9.
    • "PDF pages 37, 38, 42, 43" in P/income/sources/earned.yaml:14.
    • "PDF pages 37, 38, 39, 40, 41, 42, 43" in P/income/sources/unearned.yaml:36.
    • Use the section in the title, and give each page its own entry, e.g. D.12(a) at #page=37 and D.14(b) at #page=41.
  • Explanatory prose in a title: P/payment/base.yaml:31 ("other fuels include fuels outside ..."). Use the document name, and move the SOLAR/OTHER-to-$635 mapping into a YAML comment.
  • Citation format: P/income/dependent_child_age.yaml:9 "Chapter 8 D.13(4)-(5)" should read "D.13(a)(4)-(5)".
  • Anchors:
    • V/payment/ny_heap_vulnerable.py:11 opens #page=44 (citizenship). The vulnerability rule, Ch. 9 B.3(b), is at #page=49.
    • V/ny_heap.py:14 opens plan #page=5 (categorical eligibility). The amounts are on #page=9 and #page=10.

G10. Missing references and the manual edition

  • 18 NYCRR 393.4 is cited nowhere in the PR. Add it to ny_heap_eligible, ny_heap_categorically_eligible, ny_heap_household_size/ny_heap_is_excluded_person and ny_heap_dwelling_type.
  • P/payment/heat_included/nominal.yaml cites only the matrix. Add Manual F.2(c)/F.3, p. 45 and plan 1.7b, p. 6.
  • Every manual anchor depends on the 7/1/2021 edition. Add "(edition dated 7/1/2021)" to the manual titles, so that a new OTDA edition does not silently shift the pages.

G11. PR body and registry notes

  • The body still says "PR remains draft" (the PR is ready for review) and "not re-run locally after the review fixes" (CI is now green).
  • It also claims tests at the "published size thresholds", which G5 qualifies.
  • After S3, update verified_years and the programs.yaml notes ("earlier years are unverified estimates").

Verified correct

  • Income limit (Manual D.9, p. 37; plan 2.1, p. 9):
    • The rule is "the higher of 60% State Median Income (SMI) or 150% of the Federal Poverty Level", compared "at or below".
    • All 13 FY2026 limits reproduce: 3,473 / 4,542 / 5,611 / 6,680 / 7,749 / 8,818 / 9,018 / 9,218 / 9,419 / 9,619 / 9,820 / 10,020 / 10,221. The FY2025 and FY2024 rows also reproduce.
    • gov.hhs.smi.amount NY 2025-10-01 = $133,609 matches Att. 4.
  • Tier I (plan p. 10): at or below 130% FPL, inclusive, with a monthly floor and a one-year guideline lag. Categorical households get Tier I ("This add-on is also applied to Regular heater benefits received by CE households"). All 13 lines reproduce.
  • Amounts: $400 / $900 / $635 base; +$61 and +$35 to direct heaters only; heat included $50 / $45 / $21 (Manual F.2(c), F.3). These match the plan's maximum of $996 and minimum of $21.
  • Vulnerability (Ch. 9 B.3(b), p. 49): under 6, 60 or older, or disabled by benefit receipt. Reusing is_usda_disabled is right, since B.3(c)(2) adopts the SNAP criteria, and a bare disability flag correctly does not qualify.
  • Household and immigration:
    • Nonqualified members are excluded from size and vulnerability, but their income counts (D.4(a)(7), E.9).
    • The foster exclusion is correct (D.4(a)(3); 393.4(b)).
    • No benefit is paid when no member is qualified.
  • Income:
    • Gross income (plan 1.8).
    • Earned sources are floored per source.
    • Dependent-minor and dependent-student earnings are excluded (D.13(a)(4)-(5)).
    • TANF is not counted (the plan 1.9 box is unchecked).
    • There is no double counting among the listed sources.
    • Monthly amounts are rounded down (D.14(c), p. 43).
  • Other rules:
    • No asset test.
    • Ineligible residences and the no-heat-responsibility rule follow F.4.
    • Subsidized direct heaters get the ordinary fuel payment.
  • Code patterns:
    • No hard-coded values; formulas are vectorized.
    • defined_for is correct throughout.
    • Periods are right: MONTH inputs are read at period.first_month, and receives_ssi through add(...) > 0.
    • All 15 parameters are read.
    • All 11 heating_type keys exist.
    • There is no SNAP-to-HEAP cycle.
    • Helpers are reused: smi(), fpg(year_lag=), is_usda_disabled, has_heating_expense.
    • The SNAP income lists are not reused.
  • Documented simplifications accepted as written:
    • Net self-employment and rental income without the depreciation add-back.
    • No Part D variable.
    • Railroad, FECA and Medicaid disability pathways.
    • Roomers, employees and fleeing felons.
    • The Section 8 market-rent exception and application-month timing.
  • Tests: 41 cases, with plain 2026/2022 periods and no underscore literals. 14 cases were hand-checked, including countable income 29,040 and integration Cases 1, 4-7.

Checklist results (LIHEAP batch 2)

  1. SNAP income-list reuse: pass on reuse. The NY lists are self-contained, so main's snap_rental_income change cannot double count. Fail on source treatment: Social Security is counted gross (S1), and losses are not floored per source (S2).
  2. Backfill: 2026 is correct and 2025 carries back correctly. Periods 2023-2024 get $61 against the published $41 (S3).
  3. Operative state rule: fail on 393.4(c)(1)(iii) "living alone" (C1). The Code C versus 393.4(b) tension is G3, and SNA is a documented gap (G1).
  4. UNSPECIFIED fuel: $0 by construction. The PR body defers to Default heating_type to electricity and remove the UNSPECIFIED-gated state heating adapters #9754, which is acceptable. Add the pointer in code and in Case 12 (G2).
  5. Axiom line: valid queued line pointing to a dispatch-ready pe-parity issue (see below).
  6. References: no comma anchors. There is page text in titles, one miscitation, two weak anchors and no 18 NYCRR 393 citation (G9, G10). The manual is the current posted edition.
  7. programs.yaml: the NY entry is well formed and sorted between NE and TX. The coverage-line conflict with main is expected (see Branch status). Not a finding.
  8. Rounding and boundaries: monthly floors follow D.14(c), boundaries are inclusive and tested, and the periods are plain. Gaps: size-14 Tier I and half the published sizes (G5).

CI status

37/37 checks pass at 194a3b97bc (gh pr checks 9744, 2026-10-06).

Branch status

Axiom line

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

  • rulespec-us#1480 is open and labelled pe-parity.
  • It has module paths, corpus citations (18 NYCRR 393, the manual, the plan), verbatim law, required outputs, and companion tests built from the matrix, plan, SMI and FPG figures. It is dispatch-ready.
  • After the fixes, update it:
    • Companion test 14(a) encodes gross Social Security (S1).
    • Gap 2 describes the Code A approximation (C1).
    • Gap 1 holds the Code C decision (G3).
    • It still describes head 2ee8a99173 and calls the PR an "open draft".

Next steps

  1. C1: require "living alone" (one SSI individual or couple in the SPM unit) for SSI categorical eligibility. Add the aged-parent, SSI-couple and non-own-household tests.
  2. S1-S2: net Social Security and railroad benefits of medicare_part_b_premium, and floor each unearned source at zero. Update Case 1 to 27,840 and add a rental-loss case.
  3. S3: add the 2022-10-01: 41 / 2024-10-01: 61 Tier I entries and a period-2024 test, then widen verified_years.
  4. S4-S5: fix Case 9, add the three missing branch tests, and align the parameter descriptions.
  5. Rebase on main (resolve the coverage line as above), refresh the PR body and rulespec-us#1480, and re-run CI.

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: 1 critical (SSI categorical eligibility treats every SSI recipient as Code A "living alone") and 5 should-address items (Social Security/RRB counted gross of Medicare premiums, income losses offsetting other income, Tier I $61 backfilled to FY2023-24 where the matrices pay $41, missing branch tests, parameter descriptions). FY2026 amounts and income limits all match the sources.

hua7450 and others added 2 commits October 6, 2026 17:07
- Require Code A SSI recipients to live alone (one individual or one
  couple) for categorical eligibility and Tier I.
- Count Social Security and railroad benefits net of the Medicare
  Part B premium; floor every unearned source.
- Date the Tier I supplement ($41 FY2023-FY2024, $61 FY2025 on) and the
  base amounts from the FY2024 matrix; verified_years "2024-2026".
- Add cases for the living-alone arrangements, FY2024 and FY2025 limits
  and Tier I lines, a rental loss, an ineligible residence with a bill,
  heat in rent at 65 and a reported Code C child.
- Re-template descriptions, cite 18 NYCRR 393.4 and Attachment 4, and
  fix the reference anchors.

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): New York HEAP regular heating assistance

PR #9744: Add New York HEAP regular heating assistance (author: hua7450). Round 1 reviewed head 194a3b97bc. This round reviews head f3971348a5: fix commit 0478fcd90a ("Address review round 1 on New York HEAP") plus a merge of main.

Summary: the fix commit addresses every round-1 critical and should-address item.

  • C1: SSI categorical eligibility now requires the recipient to live alone.
  • S1: Social Security and railroad benefits are counted net of the Part B premium.
  • S2: each unearned source is floored at zero.
  • S3: the Tier I add-on is $41 for FY2023-FY2024 and $61 from FY2025.
  • S4: four new tests now fail if their branch breaks.
  • S5: the parameter descriptions follow the template.

I re-ran the round-1 examples on the new head with computed SSI and Medicare premiums, and each now gives the manual's result. The wider verified_years: "2024-2026" claim holds:

  • The FY2023 and FY2024 matrices print $41 and the same amounts as FY2025-FY2026.
  • All 13 FY2024 and FY2025 income limits and Tier I lines reproduce.

There are four new suggestions, the largest a residual gap in the living-alone test (G12). Two round-1 suggestions are partly done. CI did not run on this head: the workflow was cancelled before any job started. It needs a re-run (see CI status).

Path prefixes as in round 1:

  • P/ = policyengine_us/parameters/gov/states/ny/otda/heap/
  • V/ = policyengine_us/variables/gov/states/ny/otda/heap/
  • T/ = policyengine_us/tests/policy/baseline/gov/states/ny/otda/heap/

Line numbers refer to head f3971348a5. I reproduced the amounts by running the PR's code locally.

Round-1 items

Item Status Evidence
C1 SSI "Code A" must live alone ADDRESSED V/eligibility/ny_heap_categorically_eligible.py:31-45: living_alone = spm_unit.all(foster | (receives_ssi & head_or_spouse)) (line 39), ANDed with the federal own-household test (lines 40-45). The documentation (lines 16-25) cites 393.4(c)(1)(iii) and the glossary. Tests at T/eligibility/ny_heap_categorically_eligible.yaml: aged parent (Case 7, line 146), SSI couple (Case 8, line 190), another-person's-household (Case 9, line 220). Re-run results below. A narrower gap remains (G12).
S1 Social Security and railroad benefits net of Medicare ADDRESSED V/income/ny_heap_countable_income.py:39-48 counts max_(social_security + railroad_benefits - medicare_part_b_premium, 0). P/income/sources/unearned.yaml:4-5 drops the two sources from the list. T/income/ny_heap_countable_income.yaml Case 1 (line 8) is now 27,840, and Case 5 (line 133) gives $435. Re-run with the computed premium: $2,434.80, then 40,764 countable, then $435 (was $0). The FY2024 and FY2025 plans (1.9, p. 6 of each) also check "Excluding MediCare deduction", so the net treatment holds across the claimed years.
S2 Losses offset other income ADDRESSED V/income/ny_heap_countable_income.py:33-38 floors each unearned source, citing D.14(b)(7). Dropping the household floor at line 56 is safe, because every component is now non-negative. Test: Case 4 (line 112). Re-run: wages $43,200 with a −$6,000 rental loss gives 43,200 countable and $0 (was $400).
S3 Tier I $41 for FY2023-FY2024 ADDRESSED P/payment/tier_one_supplement.yaml:3-4 is 2022-10-01: 41, 2024-10-01: 61, citing the FY2023 and FY2024 matrices. Both print "Tier I Household Member $41 Add-on", with base $400/$900/$635, vulnerable $35 and heat included $21/$45/$50. Re-run for a zero-income gas heater: 2023 $441, 2024 $441, 2025 $461, 2026 $461. All 13 FY2024 rows reproduce (1,579/3,035 … 8,261/9,532), and so do all 13 FY2025 rows (1,631/3,322 … 8,625/9,952). Tests: T/ny_heap.yaml Case 16 (line 318), T/eligibility/ny_heap_income_limit.yaml Cases 3-4, T/payment/ny_heap_tier_one.yaml Cases 4-5. programs.yaml:885 reads "2024-2026".
S4a Ineligible residence ADDRESSED T/ny_heap.yaml:158 sets gas_expense: [0, 1_500] and keeps [21, 0].
S4b SSI outside own household ADDRESSED T/eligibility/ny_heap_categorically_eligible.yaml Case 9 (ANOTHER_PERSONS_HOUSEHOLD, expects false).
S4c Vulnerable member, heat in rent ADDRESSED T/ny_heap.yaml Case 15 (line 299): a 65-year-old gets [50, 45].
S4d Code C by reported receipt ADDRESSED T/eligibility/ny_heap_household_size.yaml Case 3 (line 76): receives_ssi: true, ssi: 0, gives size 1.
S4, mutation check Confirmed On a scratch copy, I removed the dwelling guard and the own-household test, removed the receives_ssi exclusion arm, and added the $35 to heat-included payments. Each change fails exactly its new case (4 failed, 50 passed).
S5 Description verbs ADDRESSED Line 1 of heat_included/{nominal,tier_one,tier_two}.yaml, tier_one_supplement.yaml and vulnerable_supplement.yaml now says "provides". elderly_age.yaml and young_child_age.yaml say "sets". tier_one_fpg_rate.yaml says "limits Tier I to". income/sources/{earned,unearned}.yaml say "uses these sources as …". base.yaml now ends with the program name.
G1 SNA not categorical ACKNOWLEDGED Still documented (ny_heap_categorically_eligible.py:22-24). This was accepted in round 1.
G2 #9754 pointers ADDRESSED P/payment/base.yaml:22, V/ny_heap.py:23-24, 33, T/ny_heap.yaml:229-230.
G3 Code C vs 393.4(b) ACKNOWLEDGED V/eligibility/ny_heap_is_excluded_person.py:16-23 records D.4(a)(5), 393.4(b) and D.3(a)(2). rulespec-us#1480 gap 1 records that the owner kept the manual-literal reading on 2026-10-06.
G4 One SSI-receipt definition ADDRESSED ny_heap_categorically_eligible.py:31-33 adds receives_ssi.
G5 Half the published sizes untested; size-14 Tier I PARTIALLY Size-14 Tier I is added (T/payment/ny_heap_tier_one.yaml Case 3, line 40: 113,292 / 113,304). The two comments now say the other halves "are not asserted" (ny_heap_income_limit.yaml:17-19, ny_heap_tier_one.yaml:14-16). The halves remain untested, but they reproduce. Optional.
G6 SMI flooring "inferred" ADDRESSED V/eligibility/ny_heap_income_limit.py:16 cites Att. 4, and the comment at lines 24-26 says so. The PR body is updated.
G7 fpg_year_lag source ADDRESSED P/eligibility/fpg_year_lag.yaml:2-4 adds the comment, and lines 14-15 add the HHS notice. The Federal Register page number is wrong (G13).
G8 miscellaneous_income vs plan ADDRESSED P/income/sources/unearned.yaml:26-27, with plan 1.9 cited at lines 36-37.
G9 Titles, anchors, citations PARTIALLY Fixed: the anchors (V/payment/ny_heap_vulnerable.py:11 now #page=49; V/ny_heap.py:14 now #page=9), the D.13(a)(4)-(5) citation, and the prose in the base.yaml title (now a comment). Page text remains in 11 titles. Four are carried over: eligibility/smi_rate.yaml:9, payment/elderly_age.yaml:9, payment/young_child_age.yaml:9, payment/vulnerable_supplement.yaml:15. Four more: payment/tier_one_fpg_rate.yaml:9, payment/tier_one_supplement.yaml:16, income/sources/earned.yaml:15, income/sources/unearned.yaml:34. Three are new: income/sources/unearned.yaml:36, payment/heat_included/nominal.yaml:15, 17. Still a suggestion.
G10 393.4, nominal sources, manual edition ADDRESSED 18 NYCRR 393.4 is cited in ny_heap_eligible, ny_heap_categorically_eligible, ny_heap_household_size, ny_heap_is_excluded_person and ny_heap_dwelling_type. heat_included/nominal.yaml:15-18 adds Manual F.2(c)/F.3 and plan 1.7b. Every manual title now carries "(edition dated 7/1/2021)".
G11 PR body and registry ADDRESSED The body no longer says draft. It describes 54 cases and the actual threshold coverage. programs.yaml:885-886 has the new range and notes.

Re-run of the C1 households (period 2026, computed SSI, gas heat):

Household Categorical Countable / limit Benefit
Mother 72 on SSI ($11,928) with daughter and son-in-law earning $84,000 (mother as her own tax unit, or as their dependent) No 95,928 / 67,332 $0 (was $496)
SSI couple 68/66 living alone ($8,946 each) Yes 17,892 / 54,504 $496
Single SSI recipient living alone Yes 11,928 / 41,676 $496
SSI recipient 70 with an ineligible spouse earning $30,000 (SNAP set to 0) No 33,396 / 54,504 $435 by the income test. With the model's own SNAP ($291), the household is categorical through SNAP, as it should be

Critical (Must Fix)

None.

Should Address

None.

Suggestions

G12. The living-alone test still lets two SSI recipients who are not a couple count as "living alone"

  • Where: V/eligibility/ny_heap_categorically_eligible.py:36-39. The test requires every non-foster member to be an SSI recipient who is a tax-unit head or spouse. Two recipients who each head their own tax unit pass it. The round-1 sketch had the same gap.
  • Source: rulespec-us#1480 quotes 14-ADM-07 Attachment 2: "Living alone means living in a private household composed of one eligible individual or one eligible couple." 18 NYCRR 393.4(c)(1)(iii) says "a couple or individual living alone".
  • Impact in period 2026 (reproduced):
    • Household: an SSI mother aged 72 ($11,928) lives with her disabled adult son. He earns $18,000, receives $3,438 of SSI and files his own return. SNAP is set to 0, and the household heats with gas.
    • PR: the household is categorical, so Tier I: $400 + $61 + $35 = $496.
    • Rule: two eligible individuals are not one individual or one couple, so the household is income tested. Countable income is $33,360, or $2,780 a month, above the $2,291 size-2 Tier I line. Tier II pays $400 + $35 = $435.
    • Two unmarried SSI cohabitants pass the same way.
    • The effect is capped at the $61 Tier I add-on. Two SSI recipients' combined income stays under the size-2 limit, so eligibility itself does not flip. That is why this is a suggestion.
  • Fix:
    • Allow at most one non-foster member, or two who form one marital unit. For example:
      non_foster = ~foster
      count = spm_unit.sum(non_foster)
      couple = person.marital_unit.nb_persons() == 2
      living_alone = spm_unit.all(foster | receives_ssi) & (
          (count == 1) | ((count == 2) & spm_unit.all(foster | couple))
      )
    • Add the mother-and-son case expecting ny_heap_categorically_eligible: false and ny_heap: 435.
    • Add the 14-ADM-07 Attachment 2 URL to the reference tuple. The comment at line 37 cites it, but the tuple does not.
    • Mirror the change in rulespec-us#1480 gap 2.

G13. The HHS poverty guideline notice is miscited as 90 FR 5733

  • Where: P/eligibility/fpg_year_lag.yaml:14, and the "Official sources" list in the PR body.
  • Source: the linked FR Doc. 2025-01377 is headed "Federal Register / Vol. 90, No. 11 / Friday, January 17, 2025 / Notices 5917".
  • Fix: use "90 FR 5917". The href is correct.

G14. A Tier I test name claims two years but tests one

  • Where: T/payment/ny_heap_tier_one.yaml:58. Case 4 is named "the FY2025 and FY2024 one-person Tier I maximums are inclusive", but it runs only period: 2025. Case 5 (line 70) covers FY2024.
  • Fix: rename it "the FY2025 one-person Tier I maximum is inclusive".

G15. The rule parameters still show only FY2026 support, although the registry claims FY2024-FY2026

  • Where: these parameters keep a single 2025-10-01 value and cite only the FY2026 plan or the 2021 manual:

    • P/eligibility/{smi_rate,fpg_rate,fpg_year_lag}.yaml
    • P/payment/{tier_one_fpg_rate,elderly_age,young_child_age}.yaml
    • P/income/dependent_child_age.yaml
    • the two P/income/sources/ lists

    The amounts, by contrast, were re-dated to 2023-10-01 and cite all three matrices.

  • Source: the FY2024 plan, p. 8 prints the same rules:

    • Tier I at "130% of federal poverty level" with "an additional $41".
    • The vulnerable ages: "under the age of six, age 60 or older".
    • The 60% SMI / 150% FPL limits.

    The FY2025 plan (pp. 9-10) matches too.

  • The values are the same in every year, and backward extrapolation already gives the right result. So this is documentation only: date these parameters 2023-10-01 and add the FY2024 and FY2025 plan pages, as was done for the amounts.

Carried over: G5 (untested halves) and G9 (page text in 11 titles), above.

New code in 0478fcd90a: other checks

  • Code patterns:
    • Nothing is hard-coded; the formulas are vectorized.
    • receives_ssi is read through add(...) > 0, and MONTH inputs through period.first_month (is_in_foster_care, ssi_federal_living_arrangement).
    • medicare_part_b_premium follows the merged KY and IN LIHEAP pattern. The computed premium runs without a cycle in a full household calculation.
    • No double counting: survivor_benefits and disability_benefits are the non-Social Security inputs, and the comment at unearned.yaml:12-14 says so.
  • Period logic:
    • Model year Y reads Y-01-01, so tier_one_supplement gives $41 for 2022-2024 and $61 from 2025. The 2023-10-01 amounts cover period 2024.
    • Main's Backdate parameters before applying reforms #9628 backdates each parameter's earliest value to 2015 before reforms apply. That matches what the period-2022 test already relies on.
  • Tests: 54 cases, all passing locally. I hand-checked these new or changed expectations:
    • Countable income: 27,840 (12,001 + 12,600 − 1,200 + 4,440 = 27,841, floored by month), 40,764 / $435, and 43,200 / $0.
    • Categorical Case 7: 95,928 against 67,332, so $0. Case 8: the couple rate of $1,491 a month gives $8,946 each.
    • ny_heap Case 15: [50, 45]. Case 16: [441, 941], matching the FY2024 matrix.
    • Tier I boundaries: 113,292 / 113,304 (130% × 87,150), 19,572 / 19,584 (FY2025, 1,631) and 18,948 / 18,960 (FY2024, 1,579).
    • Income limits: 39,864 / 76,680 / 119,424 (FY2025) and 36,420 / 70,056 / 114,384 (FY2024). Both match the matrices.
  • References:
    • Every new href is a single URL with at most one #page=N. The FY2023/FY2024 .pdf links resolve (the .docx versions do not exist for those years).
    • The new 393.4 and Att. 4 links are correct.
    • Page text in titles remains (G9), and one Federal Register page number is wrong (G13).
  • Descriptions: all 15 now use the template verbs and end with the program name.
  • Changelog: changelog.d/ny-heap.added.md is a top-level fragment: "Add New York HEAP regular heating assistance for FY2024 to FY2026."
  • Main merge:
    • Among the PR's files, the merge changed only programs.yaml.
    • The LIHEAP coverage line resolves to main's line plus NY: DC, Riverside County, MA, IL, IN, KS, KY, MS, NC, NE, NY, TX (programs.yaml:811).
    • The NY entry (programs.yaml:879-886) is intact and sits between NE and TX.
  • Main's guards: each passes locally on this head:

CI status

No CI results exist for f3971348a5.

  • The Pull request workflow (run 37531685026) queued at 21:08 UTC and was cancelled at 21:56 UTC before any job ran.
  • gh pr checks 9744 therefore lists all 13 jobs as failed, but every one was cancelled with no step executed.
  • I re-ran the workflow. This approval assumes that run passes; please confirm it is green before merging.

Local runs on this head:

  • The 54 NY HEAP YAML cases pass.
  • Main's guard tests pass: 769 passed, 10 skipped.

The last completed CI run, on 194a3b97bc, passed.

Branch status

Axiom line

axiom: TheAxiomFoundation/rulespec-us#1480 queued is still valid.

  • The issue is open and labelled pe-parity.
  • It was refreshed on 2026-10-06 at 21:38 UTC:
    • It now describes head f3971348a5, "open and under review"; it no longer calls the PR a draft.
    • It covers FY2024-FY2026.
    • Companion test 14(a) now nets the Part B premium (27,840).
    • Gap 2 records the living-alone rule, and gap 1 records the owner's Code C decision.
    • New companion tests 15, 16 and 19 cover the new cases.
  • It is still dispatch-ready. If G12 lands, update gap 2's wording ("every non-foster SPM member … tax-unit head or spouse").

Next steps

  1. Re-run CI on f3971348a5, or on the next push.
  2. Optional:
    • G12: limit living alone to one recipient or one couple.
    • G13: fix the Federal Register page number.
    • G14: fix the test name.
    • G15: date the rule parameters for FY2024-FY2025.
    • G9: drop the page text from titles.

Verdict: APPROVE

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Round 2: all round-1 critical and should items are addressed and verified (Code A living-alone gate, Social Security/RRB net of Part B, per-source loss floors, Tier I $41 for FY2023-24 with verified_years 2024-2026, new branch tests, descriptions). The remaining items are suggestions only. The CI run on f397134 was cancelled before any job ran; I re-ran it, so please confirm it is green before merging.

# Conflicts:
#	policyengine_us/programs.yaml
@hua7450
hua7450 merged commit 918619d into main Oct 8, 2026
28 checks passed
@hua7450
hua7450 deleted the ny-heap branch October 8, 2026 03:47
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 New York HEAP (Home Energy Assistance Program) regular heating benefit

2 participants