Skip to content

Add Kentucky LIHEAP regular cooling assistance - #10752

Merged
hua7450 merged 18 commits into
mainfrom
ky-liheap-cooling
Nov 15, 2026
Merged

hua7450 merged 18 commits into
mainfrom
ky-liheap-cooling

Conversation

@hua7450

@hua7450 hua7450 commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Adds Kentucky's Summer Cooling Subsidy, a one-time electric benefit of $41 to $250 that equals the heating points with the fuel set to electric.

Fixes #10751

Builds on the national LIHEAP total from #10730, merged on 2026-11-12: this PR adds ky_liheap_cooling to gov.hhs.liheap.regular_payments.

What and why

The FY2026 plan says "Cooling assistance provides electric benefits only" (item 3.4), with a 150% FPG limit (3.1) and a $41 to $250 range (3.6). In the FY2026 heat-cool workbook the cooling Electric column equals the heating Electricity column in all 64 rows. The FFY 2026 Summer Cooling Subsidy fact sheet prints monthly limits of $1,995 for one person, plus $710 for each added person: 150% of the 2026 guidelines, while heating uses 2025. 921 KAR 4:116 Section 4(3)(a) pays the electric provider or the landlord.

  • The points calculation, the income-limit check and the documentation check move from heating code into shared functions, so cooling reuses them without copying. Heating results are unchanged.
  • New cooling/fpg_year_lag.yaml (0 from FY2026): the cooling income limit uses the current year's guidelines, as the fact sheet prints. The income-point bands use heating's guideline (ky_liheap_fpg, lag 1), which reproduces every income range the FY2026 workbook prints beside the cooling column (for one person, 0-979, 979.01-1,305, 1,305.01-1,631 and 1,631.01-1,957).
  • ky_liheap_cooling_eligible: monthly countable income at or below the cooling limit; has_cooling_expense with a positive electricity bill, or electricity paid through the rent (fact sheet: "pay utility costs as an undesignated portion of rent"; 921 KAR 4:116 Section 4(3)(a)2 pays the landlord); and heating's citizenship and documentation rules.
  • ky_liheap_cooling: the heating points amount with electric fuel points, ceil(points × 16.66), and the subsidized-housing share. No bill cap.
  • The cooling subsidy has no heating-fuel condition: every heating_type, including NONE and UNSPECIFIED, can be paid, since the amount always uses the electric points.
  • has_cooling_expense is required with a positive electricity bill, except for renters whose utilities are included in rent. This is a PolicyEngine convention for cooling costs, not a Kentucky condition: the fact sheet asks for the current electric bill and sets no air-conditioning test. Renters whose utilities are in the rent are identified by the existing tenant_pays_utilities input (false) with positive rent; no input is added.

Test plan

Tests were not run locally. This PR now targets main, so CI runs them.

  • YAML tests with expected values worked from the sources above. 12 cases in 2 files: workbook cells, the plan minimum and maximum, the fact-sheet limits, the band edge at 75% of the 2025 guideline, subsidized and unsubsidized renters paying electricity through the rent, a renter paying utilities apart from the rent and a rent-free household (neither eligible without an electric bill), no cooling cost, an UNSPECIFIED fuel (paid), and the national liheap total.
  • Source-conflict register: the cooling subsidized amounts reuse the heating subsidized income points, which the open candidate us-ky:liheap-subsidized-income-points tracks. This PR extends that candidate to cooling: it adds ky_liheap_cooling.py, plan item 3.4, 921 KAR 4:116 Section 4(3)(c)2 and the workbook's cooling cells K52-K55 to the entry.
  • make format (ruff)
  • Changelog fragment in changelog.d/

Not modeled

  • One cooling payment per year. FY2026 ran a spring round (April 14 to May 22 in the plan; Community Action Kentucky announced April 20) and a summer round (August 3 to September 11), and the regulation allows one benefit per round. The model pays one because plan item 1.1 offers cooling and summer components only "if additional money is received", and both FY2026 rounds were first come, first served, so a second round depends on funding.
  • No summer 2026 benefit table was published, so the bands follow the FY2026 workbook. Incomes above the workbook's last printed range but within the cooling limit (above $1,957 and up to $1,995 for one person) get the top band.
  • Only FY2026 is verified; the FY2027 plan has no benefit table.

axiom: TheAxiomFoundation/rulespec-us#2187 queued

🤖 Generated with Claude Code

hua7450 and others added 12 commits November 8, 2026 22:01
Sum each state's regular LIHEAP payment in a new liheap variable, add an
Illinois regular heating payment without crisis assistance, add a Michigan
Home Heating Credit registry entry, and record each state's cooling
treatment in its registry note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add regular_payments.yaml to the aggregate-list integrity test, which checks
that each member exists, is state-gated and is not dropped between year
blocks. Say that Rhode Island plans no regular cooling payment, since its
FY2027 plan still allows a leftover-funds cooling grant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
921 KAR 4:116 Section 4(3)(a)2 pays the landlord for these renters. The
income-point bands now use heating's guideline year, which reproduces
the FY2026 workbook; the income limit keeps the 2026 guideline. Updates
shared parameter descriptions, test comments and margins, and checks
the national liheap total.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The national `liheap` total evaluates every state's formula over a
mixed-state population, which no household test does. Two patterns on main
raised errors there:

- 26 formula files passed options=[ADD] to an entity call. Core's
  Microsimulation.calculate_add rejects the keyword an entity call passes.
  The option is redundant: a plain read already sums a monthly variable over
  the year through the same Core function.
- The DC and Illinois payment formulas look up a matrix by an income level
  that is 0 for households outside the state. Add the floor Maryland and
  Kansas already use.

Also:
- Reword the North Dakota registry note: the state reserves an optional
  direct cooling payment, so its cooling component is not only equipment.
- Add a `liheap` case with Texas, DC and Illinois households in one
  simulation.
- Add a code-health check against options=[ADD] and options=[DIVIDE].
- Check that every state payment in the LIHEAP registry entry is in the
  list `liheap` sums.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
PolicyEngine/policyengine-core#647 fixes the cause in Core: it lets a
Microsimulation accept the keyword an entity call passes with
options=[ADD] or options=[DIVIDE]. Once that is released the option works
again, so a check that bans it here would be wrong. The 26 formula files
stay without the option, which was redundant and lets `liheap` run on
today's Core.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…ogram prefix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from liheap-aggregate to main November 12, 2026 19:06
hua7450 and others added 2 commits November 12, 2026 14:08
PR 10730 was squash-merged into main as f67bb1e, so this merge uses its
final head 4625f4e, already merged here, as the merge base.

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

DTrim99 commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Add Kentucky LIHEAP regular cooling assistance

Summary

The PR adds Kentucky's one-time electric cooling subsidy (ky_liheap_cooling, ky_liheap_cooling_eligible), a cooling guideline-year parameter, a second Kentucky registry entry and a new line in gov.hhs.liheap.regular_payments. It also moves the heating points calculation, income-limit check and documentation check into shared functions. I checked it against 921 KAR 4:116 (current text, effective 11-23-2021), the FY2026 and FY2027 plans, the FY2026 heat-cool workbook, the FFY 2026 Summer Cooling Subsidy fact sheet and Community Action Kentucky's spring and summer 2026 announcements.

The amounts are right. The cooling Electric column (K) equals the heating Electricity column (E) in all 64 workbook rows, and every cell I checked follows min(size, 5) + income points + 6, at 16.66 a point rounded up, with 30% rounded up in subsidized housing. The fact sheet limits are exactly 150% of the 2026 guidelines. Listing heating and cooling together in the national total does not double count: Kentucky pays both in one program year (quoted below). The refactor leaves heating unchanged line by line, and no existing expectation changed. CI is 36/36 green at the head, and both new test files ran (6 and 4 cases passed).

One should-fix: the new rent route has two terms, and no test pins either one on its own. The suggestions are disclosure and labelling points: the second FY2026 round, the cooling-cost input, UNSPECIFIED fuel for utilities-in-rent renters, the existing subsidized-points candidate, the band extension above $1,957, reference anchors, and a stale line in the axiom issue.

Critical (must fix)

None.

Should address

  1. The rent route's two terms are not pinned on their own (mutation rule). policyengine_us/variables/gov/states/ky/dcbs/liheap/cooling/ky_liheap_cooling_eligible.py lines 36-38:

    utilities_in_rent = ~spm_unit.household("tenant_pays_utilities", period) & (
        add(spm_unit, period, ["rent"]) > 0
    )

    The governing text is the FFY 2026 Summer Cooling Subsidy fact sheet (one page), eligibility item 1: "Must be responsible for home utility costs or pay utility costs as an undesignated portion of rent". 921 KAR 4:116 Section 4(3)(a)2 adds that the payment goes to the "Landlord, if the cost of cooling is included as an undesignated portion of the rent". Only one test reaches this route: ky_liheap_cooling_eligible.yaml Case 4 (line 116), where both households have tenant_pays_utilities false and positive rent. Every other case has rent 0 and the default tenant_pays_utilities true. Reading the tests, neither of these mutations would fail any case:

    • (a) Drop ~tenant_pays_utilities. Any renter with positive rent then qualifies without an electric bill. Example: one adult in Kentucky, wages $7,540, rent $12,564, tenant_pays_utilities true (default), pre_subsidy_electricity_expense 0, has_cooling_expense false. Expected: not eligible, $0, because the household pays neither an electric bill nor utilities through the rent. Under the mutation: eligible, $184. No case catches it.
    • (b) Drop rent > 0. A household that does not pay rent then qualifies whenever tenant_pays_utilities is false. Example: the same adult with tenant_pays_utilities false, rent 0, no electric bill, has_cooling_expense false (living rent-free with utilities paid by someone else). Expected: not eligible, $0. Under the mutation: eligible, $184. No case catches it.

    Fix: add these two households to ky_liheap_cooling_eligible.yaml, each expecting ky_liheap_cooling_eligible: false and ky_liheap_cooling: 0.

Suggestions

  1. One cooling payment per year, although FY2026 ran two rounds. Section 4(3)(a) allows "A one (1) time subsidy payment during each component period". Community Action Kentucky ran a spring round (April 20 to May 22, 2026; plan item 1.1, page 4 prints 04/14/2026 to 05/22/2026) and a summer round (August 3 to September 11, 2026, fact sheet). Both announcements say "Assistance received during previous LIHEAP components does not affect household eligibility" (summer announcement; the spring announcement carries the same sentence, reprinted here). So a one-person household with $628 a month that applied in both rounds received $184 twice; the model pays $184. The body and registry note disclose this, but they give no reason. Add one sentence giving it (for example: plan 1.1 makes cooling contingent, "We will offer cooling components and/or summer components if additional money is received", and each round was first-come, first-served), or add a rounds-per-year parameter.

  2. has_cooling_expense on the direct route is a convention. Lines 29-35 require has_cooling_expense with a positive electric bill. The fact sheet asks for "Most current electric (utility) bill" and responsibility "for home utility costs". It has no air-conditioning or cooling-equipment test. Section 3(2)(b) does name "Cooling costs". An adult with a $130 monthly electric bill and no cooling cost gets $0 here (Case 2, line 43); Kentucky's printed rule would pay $184. The rent route does not require the input (Case 4), so the two routes differ. The body labels this as the all-state cooling convention, but the code comment presents it as the rule ("must be responsible for its home electric costs"). Say in the comment that has_cooling_expense is a modeling convention, not a Kentucky condition.

  3. The UNSPECIFIED exclusion catches the PR's own utilities-in-rent renters. Lines 40-43 return false for heating_type UNSPECIFIED. This matches Kentucky heating: ky_liheap.py line 79 pays $0 whenever the fuel carries zero points, and UNSPECIFIED has 0 in payment/fuel_points.yaml. So the body's statement is accurate for Kentucky. But the cooling amount does not depend on the heating fuel. The heating_type documentation says UNSPECIFIED "is also the right value when heat is included in rent and the fuel is unknown". Example: a renter whose utilities are in the rent and who does not know the building's fuel (UNSPECIFIED, tenant_pays_utilities false, rent $12,564, wages $7,540). Kentucky pays the landlord $184 (Section 4(3)(a)2); the model pays $0. Consider exempting the rent route from the fuel check, or say in the comment that this case is excluded.

  4. Extend the subsidized-points candidate to cooling. source_conflicts/candidates.yaml (main, line 4390) us-ky:liheap-subsidized-income-points lists only income_points/subsidized.yaml and ky_liheap.py, and cites only Section 4(1)(c). Its paths are not stale: the subsidized lookup still sits in ky_liheap.py, inside ky_liheap_points_amount. But cooling now inherits the reversed ordering, and the workbook's subsidized cooling rows rise with income (41, 45, 51, 56 for one person, cells K52-K55). Two cooling sources bear on the question:

    • Plan item 3.4, page 10: "The highest benefits are awarded to the lowest income household size".
    • Section 4(3)(c)2: subsidized ceilings rise with income ("up to one hundred (100) dollars" for 0-100%, "up to $200" for 101-150%), while nonsubsidized ceilings fall ($400, then $300).

    Under the regulation's ordering, one subsidized person in the lowest band would get ceil(184 × 0.3) = $56, not $41. Add cooling/ky_liheap_cooling.py to pe_us.code and those two sources to the entry. In the body, "adds no new conflict" would be more accurate as "extends the existing conflict to cooling".

  5. Income between the workbook's top band end and the cooling limit. For one person, the workbook's last row ends at 1,957 (cell D8, 150% of the 2025 guideline). The cooling limit is 1,995. Between them the model gives the top-band amount by extension: Case 5 (line 159), $1,976 → 8 points → $134. The body says no summer table was published. Add one line to the formula comment (ky_liheap_cooling.py lines 30-35) saying that incomes above the printed range get the top band.

  6. Reference anchors and a shared comment.

    • ky_liheap_cooling.py line 21 says "PDF pages 4,10-11: Sections 1.1, 3.4-3.6", but only #page=4 is linked. Add separate #page=10 (3.4) and #page=11 (3.6) entries.
    • The comment in the now-shared ky_liheap_meets_income_limit (ky_liheap_income_eligible.py lines 27-32) explains only heating's $688. A clause noting that the cooling fact sheet's $710 is exact (5,680 × 1.5 / 12 = 710.00) would help.
  7. Axiom issue wording. rulespec-us#2187 still says CI "has not yet run its tests at this head" and the tests "have not yet passed in CI at this head". Both new files have now passed at 799669b (6 and 4 cases). Refresh that line.

Verified correct

  • Heating and cooling in the same program year (no double count).
    • Section 1(15) defines the subsidy component as "the heating or cooling component", and Section 4(3)(a) gives cooling its own "one (1) time subsidy payment during each component period".
    • Community Action Kentucky's FY2026 summer and spring announcements say "Assistance received during previous LIHEAP components does not affect household eligibility". The 2025 summer announcement says "Prior LIHEAP assistance does not affect eligibility".
    • So ky_liheap and ky_liheap_cooling both belong in regular_payments.
  • A regular cooling benefit exists in FY2026.
    • Plan item 1.1 (page 4): cooling 04/14/2026-05/22/2026, contingent "if additional money is received". Item 1.2: cooling 7.00% (the PDF text layer misaligns this table by one row).
    • The summer fact sheet is titled "Summer Cooling Subsidy Component: FFY 2026" and runs August 3 to September 11, 2026.
    • The FY2027 plan repeats cooling (04/13/2027-09/10/2027), 150%, "electric benefits only" and $41-$250 (pages 4, 10 and 11). No FY2027 matrix is on the Clearinghouse, so verified_years: "2026" is accurate.
  • Fact-sheet limits. 15,960 × 1.5 / 12 = 1,995.00 and 5,680 × 1.5 / 12 = 710.00 exactly. Sizes 2-8: 21,640 → 2,705; 27,320 → 3,415; 33,000 → 4,125; 38,680 → 4,835; 44,360 → 5,545; 50,040 → 6,255; 55,720 → 6,965. All match the fact sheet, so the code's rounding up changes nothing. The federal 110% floor and the 150%/60%-SMI ceiling hold.
  • Guideline-year split.
    • The lag 0 (cooling) / lag 1 (heating) split rests on the two fact sheets' amounts only. Plan items 2.1 and 3.1 print "HHS Poverty Guidelines 150.00%" with no year, and Section 3(1) says "updated annually"; the parameter comments say the lag is inferred.
    • Calendar 2026 reads FFY2026: limit at the 2026 guidelines, bands at the 2025 guidelines.
    • Calendar 2025 returns backfilled values: limit at the 2025 guidelines, bands at the 2024 guidelines (942/1,255/1,569 for one person), same points.
    • Calendar 2027 uses the projected 2027 guideline for the limit and the 2026 guidelines for the bands (998/1,330/1,663 for one person).
    • Backdating to the first modeled year means the 2025-10-01 start date raises no missing-parameter error before FY2026.
  • Points table against workbook column K ("rev benefit income range"):
    • Size 1, unsubsidized, each band: 1+4+6 = 11 → ceil(183.26) = 184 (K5); 1+3+6 = 10 → 167 (K6); 1+2+6 = 9 → ceil(149.94) = 150 (K7); 1+1+6 = 8 → ceil(133.28) = 134 (K8).
    • Other sizes, unsubsidized: size 2 lowest band 12 → ceil(199.92) = 200 (K10); size 3 third band 11 → 184 (K17); size 4 top band 11 → 184 (K23); size 5 lowest band 15 → ceil(249.9) = 250 (K25); size 6 top band 5+1+6 = 12 → 200 (K33); size 8 second band 5+3+6 = 14 → ceil(233.24) = 234 (K41).
    • Subsidized: size 1 lowest 1+1+6 = 8 → 134 → ceil(40.2) = 41 (K52); size 1 top 1+4+6 = 11 → 184 → ceil(55.2) = 56 (K55); size 4 second 4+2+6 = 12 → 200 → 60 (K68); size 5 top 15 → 250 → 75 (K75); size 7 third 5+3+6 = 14 → 234 → ceil(70.2) = 71 (K84).
    • The cooling formula uses 16.66 (L5; N5 "1 pt= 16.66 (roundup)"). The "if no funding FFY26" sheet (33.33, up to $367) is correctly not used, since plan item 3.6 prints $41-$250.
  • Band ends.
    • One person: ceil(1,304.17 × 0.75) = 979, ceil(1,304.17) = 1,305, ceil(1,630.21) = 1,631 (D5-D7).
    • Two people: 1,322 / 1,763 / 2,204 (D10-D12).
    • Four people: 2,010 / 2,680 / 3,349 (D20-D22).
    • Eight people: 3,385 / 4,513 / 5,641 (D40-D42).
  • Minimum and maximum. Neither is clipped, and both are structural. The fewest possible points are 1 + 1 + 6 = 8 → $134, or $41 subsidized (the plan minimum, Case 2). The most are 5 + 4 + 6 = 15 → $250 (the plan maximum, Case 3). Both are reachable and neither can be exceeded. All amounts are within the Section 4(3)(c)2 ceilings.
  • Subsidized share. The workbook's subsidized cooling cells are ROUNDUP(K(reversed row) × 0.3, 0), the same as heating's E52 = ROUNDUP(E8×0.3,0). Section 4(1)(e) applies to "a subsidy component", which Section 1(15) defines to include cooling.
  • Refactor, line by line against main (cc48633).
    • ky_liheap_points_amount reproduces main's income, band, subsidized, points, rounding and subsidized-share lines. Heating passes fuel_points + coal_adjustment and keeps where(fuel_points > 0, amount, 0). Sums of integer-valued floats are exact, so regrouping changes nothing.
    • ky_liheap_members_documented returns main's has_qualified_member & all_documented. ky_liheap_meets_income_limit is main's income test with year_lag=p.fpg_year_lag for heating.
    • Readers: ky_liheap_income_eligible is read only by ky_liheap_eligible (and the cooling helper import); ky_liheap_eligible only by ky_liheap's defined_for; ky_liheap only by regular_payments and programs.yaml.
    • The only test changes are the two new files, and the heating tests ran green in the full baseline shards.
    • The four re-described parameters are read only by Kentucky LIHEAP.
  • Documentation and citizenship rule. The fact sheet requires "Proof of Social Security Number or Permanent Residence card (Green Card) for each member of the household", matching Section 2(1)(d) and the reused heating function. Case 3 pins it.
  • Rent route and subsidized renter. The fact sheet accepts a "statement from your landlord if electric is included in your rent". Plan item 3.3 (page 10, read from the page image) answers No to differing policies for renters, subsidized renters and renters with utilities in rent. Case 4 (184 unsubsidized, 41 subsidized) is right by hand. GA and NV LIHEAP read tenant_pays_utilities the same way.
  • Mutation coverage of the other new terms.
    • Cooling limit and guideline lag: eligible Case 1 (1,995 / 1,995.01) and cooling Case 5 (1,976).
    • has_cooling_expense and electric bill > 0: eligible Case 2, households 1 and 2.
    • Rent route as a whole: eligible Case 4. UNSPECIFIED: cooling Case 6. Documentation: eligible Case 3. Kentucky gate: eligible Case 2, household 3.
    • Electric points for a gas home: cooling Case 1. Band guideline: cooling Case 4 (979 / 979.01; under 2026 bands 979.01 would pay $184). Subsidized share: cooling Case 2.
    • The regular_payments line: cooling Case 1 (liheap: 318) and test_liheap_total_sums_every_registered_state_payment. That test needs every float registry variable in the list, and ky_liheap_cooling is now one. Kentucky is not in LIHEAP_PAYMENT_STAND_INS.
  • All 10 test expectations hand-traced from the fact sheet, the workbook and the 2025/2026 guidelines, including Case 3 (22,880 / 12 = 1,906.67 ≤ ceil(3,137.50 × 0.75) = 2,354 → $250) and Case 5 (1,976 > 1,957 heating limit, ≤ 1,995 cooling limit, > 1,631 → $134).
  • National total and registry. The list line is a dollar amount on SPMUnit/YEAR, gated to Kentucky through ky_liheap_cooling_eligible (defined_for = StateCode.KY), and is not a component of ky_liheap. The second registry entry and the reworded heating note match the code.
  • Mixed-state and periods. No state-gated key: fuel_points.ELECTRICITY is a scalar; fuel_points[fuel] covers all 11 heating types; the state_group breakdown covers every group. Non-residents evaluate without error (masked ky_liheap_fpg and ky_liheap_income are 0), and the Microsimulation job passed. Both variables are YEAR, monthly tests divide annual income by 12, and nothing uses options=[ADD].
  • Hygiene. Labels, units, defined_for and references are on both variables. The parameter descriptions follow the template. Both fact sheets are one page, so no anchor is needed. Tests are in variable-named files, and the changelog fragment is changelog.d/ky-liheap-cooling.added.md. The source-conflict ratchet is unaffected: ky_liheap.py keeps its one counted line, and the new comments contain none of the counted patterns.

Axiom line assessment

axiom: TheAxiomFoundation/rulespec-us#2187 queued. The issue is OPEN and labelled pe-parity, and it names the current head 799669b. It has proposed module paths, corpus citations with manifest status, verbatim source text, the required outputs for both variables, and 12 companion tests derived from the fact sheet, workbook and guidelines. It also lists interpretation gaps, including the second round, has_cooling_expense, the unpinned positive-rent term and the subsidized ordering. It depends on rulespec-us#1465 (Kentucky heating) and on ingest of the workbook and fact sheet, which it states. The line is valid. The only staleness is the CI sentence (Suggestion 7).

CI status

gh pr checks 10752: 36 of 36 pass at head 799669b (run 43287798487); none pending or failed. The Quick Feedback job ran both new files: ky_liheap_cooling.yaml 6 passed and ky_liheap_cooling_eligible.yaml 4 passed. The full baseline state shards (existing Kentucky heating tests), the Python shards and the Microsimulation job are all green.

Branch status

The branch is 15 commits ahead of main and 12 behind (cc48633). git merge-tree --write-tree against main is clean. Test merges against all eight sibling heads are clean: AR #10744, GA #10746, OK #10748, LA #10750, SC #10754, AL #10756, NE #10758 and VA #10760. That includes the adjacent la_liheap lines in regular_payments.yaml and programs.yaml from LA #10750. This PR does not touch source_conflicts/candidates.yaml.

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 one change, two test cases. The amounts are right: the cooling Electric column equals the heating Electricity column in all 64 workbook rows, every cell I checked follows min(size, 5) + income points + 6 at 16.66 a point rounded up, and the fact-sheet limits are exactly 150% of the 2026 guidelines. Kentucky pays both the winter subsidy and the summer cooling subsidy in one program year, so the national total doesn't double count, and the refactor leaves heating unchanged line by line.

The rent route has two terms, ~tenant_pays_utilities and rent > 0, and only Case 4 reaches it, with both true. By my reading, dropping either term fails no case: a renter with positive rent who pays utilities separately, or a rent-free household with the flag false, would then get $184 with no electric bill. Two households, each expecting not eligible and $0, are in the report above (Should Address 1).

Seven optional suggestions follow; the most useful are labelling has_cooling_expense as a convention in the comment, and extending us-ky:liheap-subsidized-income-points to cooling. CI is green at 799669b167.

@hua7450

hua7450 commented Nov 13, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, David. This push, head 7c47ff923e, adds your two rent-route cases and removes the cooling formula's heating-fuel condition (fix commit 27080d37a2); upstream main is merged in.

Should address

  • 1. Rent-route terms. ky_liheap_cooling_eligible.yaml now ends with your two households, each expecting ky_liheap_cooling_eligible false and ky_liheap_cooling 0. Case 5 pays rent of $12,564 with tenant_pays_utilities true, heats with gas and has no electric bill or cooling cost. Case 6 pays no rent, has tenant_pays_utilities false and has no bill or cooling cost. By hand, dropping ~tenant_pays_utilities turns Case 5 into $184, and dropping rent > 0 does the same to Case 6.

Suggestions

  • 1. Two FY2026 rounds. The ky_liheap_cooling formula comment now gives the reason: Section 4(3)(a) allows one payment per component period, and plan item 1.1 offers cooling and summer components only "if additional money is received", so a second round depends on funding. The body's "Not modeled" bullet says the same and adds that both rounds were first come, first served. No rounds parameter. The registry note is unchanged: registry notes stay one or two sentences on years and scope.
  • 2. has_cooling_expense. The comment in ky_liheap_cooling_eligible.py now says the fact sheet asks for the current electric bill and sets no air-conditioning test, and that requiring has_cooling_expense with the bill is a PolicyEngine convention for cooling costs, not a Kentucky condition. The body says the same. The input stays on the direct route, and the rent route still does not need it.
  • 3. UNSPECIFIED for utilities-in-rent renters. The cooling eligibility formula no longer reads heating_type, so neither route has a fuel condition. This follows the project rule that a separate cooling payment adds no heating-fuel condition of its own. ky_liheap_cooling.yaml Case 6 keeps its inputs and now expects [true, true] and [184, 184] for the NONE and UNSPECIFIED homes, with "Was" comments; ky_liheap stays [0, 0]. By hand, your utilities-in-rent example now gets $184; no separate case was added for it. Heating code is unchanged. The body's UNSPECIFIED sentence is replaced.
  • 4. Subsidized-points candidate. us-ky:liheap-subsidized-income-points now cites Sections 4(1)(c) and 4(3)(c)2, plan item 3.4 (page 10) and workbook cells K52-K55. It adds the cooling amounts ($41, $45, $51 and $56 for one person) and your example ($41 under the workbook, $56 under the regulation's ordering). It also lists cooling/ky_liheap_cooling.py in pe_us.code and this PR in prs. The cooling formula comment names the candidate, and the body now says the PR extends it to cooling.
  • 5. Incomes above the printed range. The ky_liheap_cooling formula comment now says incomes above the printed ranges but within the cooling limit take the top band. The body's "Not modeled" bullet gives the one-person range: above $1,957 and up to $1,995.
  • 6. Anchors and the shared comment. The ky_liheap_meets_income_limit comment in ky_liheap_income_eligible.py now names the heating fact sheet as the source of the $688, and says the cooling fact sheet's $710 is exact (5,680 × 1.5 / 12 = 710). Separate #page=10 and #page=11 entries were not added: each source is listed once, with its pages in the comment above the link, and the existing # PDF pages 4,10-11 comment has that shape.
  • 7. Axiom issue wording. LIHEAP cooling: encode Kentucky LIHEAP cooling subsidy eligibility and one-time electric payment (PE-US PR 10752) TheAxiomFoundation/rulespec-us#2187 now points at 7c47ff923e. Both "not yet" CI lines are gone; it says both files passed at 799669b167. It also updates the scope and the eligibility output (no fuel condition), the test counts (12 cases) and companion test 10 ($184 for both homes). It adds companion tests 13 and 14 for your two cases, updates gaps 1 and 5-8 and the checklist, and drops the UNSPECIFIED gap.

CI is running at 7c47ff923e.

@hua7450
hua7450 requested a review from DTrim99 November 13, 2026 20:35
@DTrim99

DTrim99 commented Nov 15, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 2): Add Kentucky LIHEAP regular cooling assistance

Re-reviewed at head 7c47ff923e. Since round 1 (2026-11-12, head 799669b167) the branch has one fix commit, 27080d37a2, and two merges of main, 8bb7c15bef and 7c47ff923e, all from 2026-11-13. I ran no tests and no Python. I ran test merges, read the CI logs at this head, and opened again the regulation, the FY2026 plan, the FY2026 workbook, the summer fact sheet and both 2026 Community Action Kentucky announcements. Each claim below says whether it rests on hand tracing, on reading the tests, on the source documents or on the CI logs.

Summary

The round-1 should finding is fixed. New eligible Cases 5 and 6 each pin one of the two rent-route terms, and both ran green in CI at this head.

The fix also removes the heating-fuel condition from cooling eligibility. A household whose heating_type is UNSPECIFIED can now be paid, through either route. Kentucky's sources support this: cooling "provides electric benefits only" (plan item 3.4), and neither the fact sheet nor 921 KAR 4:116 Section 4(3) ties the cooling payment to the heating fuel. Cooling Case 6 pins the change, and the body and the Axiom issue disclose it. Heating results are unchanged.

Suggestions 1-5 and 7 were taken. Suggestion 6 was partly taken. The author declined the separate page entries, and CLAUDE.md's reference rule supports the decline.

The reply credits the fuel change to a project rule. I found no dated record of that rule on this PR or on the sibling PRs (see "Ruling cited in the reply"). The change rests on Kentucky's own sources, and no ruled candidate depends on it, so it does not affect the verdict.

All 36 CI checks pass at this head. The branch merges cleanly with current main and with each of the eight open sibling PRs.

Counts: 0 critical, 0 should, 1 suggestion.

Prior findings

Finding Status Evidence
Should 1: the rent route's two terms are not pinned on their own Fixed New ky_liheap_cooling_eligible.yaml Case 5: rent $12,564, tenant_pays_utilities true, no electric bill, no cooling cost; expects false and $0. New Case 6: rent 0, tenant_pays_utilities false, no bill, no cooling cost; expects false and $0. These are the two households round 1 proposed. By my reading (hand trace): without ~tenant_pays_utilities, Case 5 passes the rent route. Its $628.33 a month is under the $1,995 limit and in the 0-979 band, so it gets 1 + 4 + 6 = 11 points, ceil(183.26) = $184, and eligible true. Without rent > 0, Case 6 gets the same $184. Neither household can pass the bill route (has_cooling_expense false), so each case isolates one term. CI ran the file green at this head (6 passed).
Suggestion 1: one payment a year, although FY2026 ran two rounds Taken ky_liheap_cooling.py lines 39-42 now give the reason: one payment per component period (Section 4(3)(a)), and plan item 1.1 offers cooling only "if additional money is received". The body's "Not modeled" bullet says the same and adds that both rounds were first come, first served. Sources: plan page 4 prints "We will offer cooling components and/or summer components if additional money is received" (text extraction). The spring announcement says applications "will be accepted from April 20 to May 22 on a first-come, first-served basis". The summer announcement says "on a first-come, first-served basis, or until funds are depleted". The registry note is unchanged, with a stated reason; it already discloses the single payment.
Suggestion 2: has_cooling_expense on the direct route is a convention Taken ky_liheap_cooling_eligible.py lines 32-35 now say the fact sheet "sets no air-conditioning test" and that requiring has_cooling_expense with the bill "is a PolicyEngine convention for cooling costs, not a Kentucky condition". The body and Axiom gap 6 say the same.
Suggestion 3: the UNSPECIFIED exclusion blocks utilities-in-rent renters Taken, more broadly The heating_type term is gone from both routes, not just the rent route. See "Verified".
Suggestion 4: extend the subsidized-points candidate to cooling Taken us-ky:liheap-subsidized-income-points now cites Sections 4(1)(c) and 4(3)(c)2, plan item 3.4 (page 10) and workbook cells K52-K55. It adds the cooling amounts and the $41/$56 example, and lists cooling/ky_liheap_cooling.py and this PR. I checked the record field by field (see "Verified"). The body now says the PR "extends that candidate to cooling".
Suggestion 5: incomes between $1,957 and $1,995 Taken ky_liheap_cooling.py lines 35-37: "incomes above the printed ranges but within that limit take the top band". The body's "Not modeled" bullet gives the one-person range. Cooling Case 5 ($1,976, $134) already pins the behavior.
Suggestion 6: reference anchors and the shared comment Partly taken; the decline is sound The $710 clause is added (ky_liheap_income_eligible.py lines 32-34), and the $688 is now attributed to the heating fact sheet. 5,680 × 1.5 / 12 = 710 exactly (by hand). The author declined separate #page=10 and #page=11 entries. CLAUDE.md says: "One reference entry per source document; URLs that differ only by #page= are one source", and "Variable reference tuples have no title: put a # PDF pages 61-62, 67 comment above a multi-page href". The PR's # PDF pages 4,10-11 comment above the #page=4 link has that form, so the decline closes the point.
Suggestion 7: stale CI sentence in the Axiom issue Taken rulespec-us#2187 no longer says CI "has not yet run". It says both files passed at 799669b167 and that CI at this head "was running on 2026-11-13". See "Axiom".

New findings

Critical

None.

Should address

None.

Suggestions

  1. No record of the fuel-condition decision. The reply says the change "follows the project rule that a separate cooling payment adds no heating-fuel condition of its own". This PR has no record of that rule, and none of the dated project-lead ruling comments on the sibling PRs covers it. The PR doesn't need one, because the change rests on plan item 3.4 and the workbook, not on a ruling. The code, the body and the Axiom issue cite those sources. If the rule is meant to be cited as a ruling, post a dated record and link it, as was done for the Delaware, Georgia, Oklahoma and Virginia rulings.

Ruling cited in the reply

Verified

What the fix commit changes (files read in full at the head):

  • ky_liheap_cooling_eligible.py: the heating_type read and the UNSPECIFIED term are gone. The formula returns income_eligible & responsible & documented. The comment adds the convention sentence.
  • ky_liheap_cooling.py and ky_liheap_income_eligible.py: comments only. Neither formula changed.
  • Tests: cooling Case 6's expectations changed; eligible Cases 5 and 6 are new.
  • source_conflicts/candidates.yaml: the Kentucky subsidized-points entry.
  • The fix commit does not touch ky_liheap.py, ky_liheap_eligible.py, any parameter, regular_payments.yaml or programs.yaml. The programs.yaml lines that differ from round 1 came in from main (sibling notes for DC, DE and HI).
  • Both merges are identical, tree for tree, to git's automatic merge of their parents (a comparison I ran), so nothing was resolved by hand.

Removing the fuel condition is right for Kentucky (source documents and code):

  • Plan item 3.4, page 10 (text extraction): "Cooling assistance provides electric benefits only."
  • 921 KAR 4:116 Section 4(3)(a) (current text): the payment goes to the household's "Electric utility provider" or to the "Landlord, if the cost of cooling is included as an undesignated portion of the rent". Section 4(3) names no heating fuel.
  • The summer fact sheet (text extraction) names no heating fuel. Its purpose is "One-time bill payment assistance for home electric services", and eligibility item 1 is "Must be responsible for home utility costs or pay utility costs as an undesignated portion of rent."
  • Workbook column K equals the Electricity column in all 64 rows (round 1), so the amount never needs the heating fuel. ky_liheap_cooling reads the scalar fuel_points.ELECTRICITY, so NONE and UNSPECIFIED reach no missing key.
  • Who changes: only households with heating_type UNSPECIFIED, which go from $0 to the electric amount. The removed term was true for every other fuel, so every other household that was right at round 1 is unchanged. Heating code is unchanged, so heating results are too. The body ("every heating_type, including NONE and UNSPECIFIED, can be paid") and the test's "Was" comments disclose the change.
  • Pinned (by my reading): putting the UNSPECIFIED term back makes cooling Case 6 household 2 return false and $0, against expected true and $184. An exemption for the rent route only would fail the same household, which pays by bill.

Mutation coverage of the fix (by my reading of the tests; not run):

Mutation Case that catches it Result against expected
Drop ~tenant_pays_utilities ky_liheap_cooling_eligible.yaml Case 5 true, $184 against false, $0
Drop rent > 0 ky_liheap_cooling_eligible.yaml Case 6 true, $184 against false, $0
Restore the UNSPECIFIED fuel term ky_liheap_cooling.yaml Case 6, household 2 false, $0 against true, $184

Hand checks of every new or changed expectation. The limit uses the 2026 guidelines ($15,960 plus $5,680 a person). The bands use the 2025 guidelines ($15,650, or $1,304.17 a month, for one person).

  • Eligible Case 5: 7,540 / 12 = 628.33 ≤ 1,995. The bill route is closed (has_cooling_expense false), and the rent route is closed (tenant_pays_utilities true). Not eligible, $0. Matches.
  • Eligible Case 6: income passes. The bill route is closed, and the rent route is closed (rent 0). Not eligible, $0. Matches.
  • Cooling Case 6, both households: 628.33 ≤ 1,995. The bill route is open ($1,560 bill and a cooling cost). 628.33 ≤ ceil(1,304.17 × 0.75) = 979, so the lowest band and 4 income points. 1 + 4 + 6 = 11 points; ceil(11 × 16.66 = 183.26) = 184; not subsidized. Eligible [true, true], amounts [184, 184]. Matches. ky_liheap stays [0, 0], because NONE and UNSPECIFIED carry 0 heating fuel points.

The candidate record (us-ky:liheap-subsidized-income-points):

  • Fields: id, program, years (2025-2026, program year), question, three sources, readings, example, pe_us (follows: unknown; three code paths, all tracked at the head; this PR under prs), state: unreviewed and note. plan is an allowed source kind, and 326 entries on main use example. CI's test_candidate_fields and test_candidates_are_not_yet_registered pass at this head (Python shard 5). The id is not in the pinned register on main.
  • Sources (opened again):
    • Section 4(1)(c): "A household with the lowest income and highest heating season fuel cost shall receive highest benefits." Section 4(1) applies to "a subsidy component", which Section 1(15) defines to include cooling.
    • Section 4(3)(c)2 prints the ceilings as the entry gives them: subsidized "up to one hundred (100) dollars" (0-100%) and "up to $200" (101-150%); nonsubsidized "up to $400" and "up to $300".
    • Plan page 10 prints "The highest benefits are awarded to the lowest income household size" under item 3.4, so the #page=10 link shows the quote.
    • The workbook's "rev benefit income range" worksheet caches K52-K55 as 41, 45, 51 and 56 (read from the file's XML), beside E52-E55, which are ROUNDUP(E80.3,0) through ROUNDUP(E50.3,0) = 41, 45, 51 and 56.
  • Example (by hand): 1 + 1 + 6 = 8 points, ceil(133.28) = 134, ceil(40.2) = $41. With 4 income points, 184 × 0.3 = 55.2, rounded up to $56. The four amounts are 134, 150, 167 and 184 times 0.3, rounded up: 41, 45, 51 and 56. All match.
  • The readings also record that Section 4(3)(c)2's subsidized ceilings rise with income. Those are ceilings, and both orderings stay under $100, so the entry states the fact without picking a side. The body's register line and Axiom gap 8 say the same as the entry.

New comments against their sources. "if additional money is received" is on plan page 4 (item 1.1). Both 2026 rounds were first come, first served (both announcements). 5,680 × 1.5 / 12 = 710 exactly, so rounding the increment and rounding the whole limit give the same result. The $688 attribution to the heating fact sheet matches the variable's first reference.

Source-conflict ratchet. By reading the ratchet's patterns on main, none of the new comment lines matches: there is no "says" or "states" after form, instructions, worksheet or manual, and no "conflict with", "in practice" or "discrepancy". ky_liheap.py keeps its one counted line. CI's test_conflict_sounding_comments_do_not_grow passes at this head. Since the merge base, main's only change to that test file adds an unrelated KNOWN_UNMARKED line (medical_care_c_cpi_u.yaml).

Registry and body. The fix commit leaves both Kentucky registry notes alone. The cooling note does not mention the fuel, so it still matches the code. The body's test plan (12 cases in 2 files) matches the files (6 and 6).

Axiom

axiom: TheAxiomFoundation/rulespec-us#2187 queued is still valid. The issue is OPEN, labelled pe-parity, last updated 2026-11-13 20:35 UTC, and names head 7c47ff923e. It matches the fix:

  • Scope: "The payment has no heating-fuel condition: every heating_type, including NONE and UNSPECIFIED, can be paid."
  • Required outputs: the eligibility definition has no fuel term, and the inputs line says "The cooling module does not read heating_type".
  • Tests line: 12 cases (6 and 6), naming eligible Cases 5 and 6 and the changed cooling Case 6.
  • Companion tests 10 ($184 for both homes), 13 and 14 (the two new households, false and $0), derived from the fact sheet and the regulation, not from PolicyEngine output.
  • Gaps 5 (electric column for every fuel), 6 (convention wording), 7 (each rent-route term pinned) and 8 (the candidate now covers cooling). The UNSPECIFIED gap is gone, and the checklist names both rent-route conditions and "an unspecified heating type (both paid)".

The CI sentence says CI at this head "was running on 2026-11-13". That was true when written. The run has since passed (below), and the sentence can say so the next time the issue is edited.

CI

All 36 checks pass in run 43501529124 (pull_request event at 7c47ff923e, started 2026-11-13 20:26 UTC, finished 21:40 UTC). None is pending or failed.

  • Quick Feedback ran both of the PR's test files at this head: ky_liheap_cooling.yaml 6 passed and ky_liheap_cooling_eligible.yaml 6 passed.
  • Python shard 5 ran the source-conflict checks, including test_candidate_fields and test_conflict_sounding_comments_do_not_grow. All passed.
  • The four baseline state shards, the Microsimulation job and the other Python and contrib shards are green. The Kentucky heating code is unchanged by the fix commit apart from one comment.

Branch

The branch is 18 commits ahead of main and 54 behind (7c516c74bb, 2026-11-14). git merge-tree --write-tree against current main is clean. Since the merge base, main changed two of the PR's files, programs.yaml and source_conflicts/candidates.yaml, and both merge automatically. The merged tree has one Kentucky subsidized-points entry with the cooling path, both Kentucky lines in regular_payments.yaml and both Kentucky registry entries. Siblings #10730, #10732, #10734, #10736, #10738, #10740, #10742 and #10762 have merged into main, so the main check covers them. Test merges against the current heads of the eight open siblings are all clean: AR #10744 (c984f475b7), GA #10746 (0046a70a0a), OK #10748 (b3b87c9204), LA #10750 (d4215baf78), SC #10754 (5f487892d2), AL #10756 (f3dbfc7056), NE #10758 (a497a51822) and VA #10760 (bd1f8a61d8).

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). The round-1 should is fixed: eligible Cases 5 and 6 each pin one rent-route term. Dropping the heating-fuel condition from cooling eligibility rests on Kentucky's own sources (plan 3.4, "electric benefits only"; neither the fact sheet nor 921 KAR 4:116 names a fuel), and cooling Case 6 pins it. CI is green at 7c47ff923e, and the branch merges cleanly with main and the open siblings. The one suggestion: the reply cites a project rule for the fuel change with no dated record. The change doesn't need one, but a citation of a rule should point to its record.

@hua7450
hua7450 merged commit aaf4ac8 into main Nov 15, 2026
36 checks passed
@hua7450
hua7450 deleted the ky-liheap-cooling branch November 15, 2026 01:25
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 Kentucky LIHEAP regular cooling assistance

2 participants