Skip to content

Count all child care subsidies in household benefits - #9503

Open
RajveerKapoor wants to merge 14 commits into
PolicyEngine:mainfrom
RajveerKapoor:fix/household-child-care-subsidies
Open

RajveerKapoor wants to merge 14 commits into
PolicyEngine:mainfrom
RajveerKapoor:fix/household-child-care-subsidies

Conversation

@RajveerKapoor

@RajveerKapoor RajveerKapoor commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #9405.

Problem

household_state_benefits listed only 23 of the 51 state child care subsidy variables in the CCDF aggregate. Implemented subsidies in the other states therefore never reached household benefits or household net income.

Fix

Replace the per-state child care entries in household_state_benefits with a new variable, household_child_care_subsidies, in every period. It reads the existing child_care_subsidies aggregate, so the CCDF list stays the single source of truth and no subsidy counts twice. It counts each family's subsidy up to its pre-subsidy bill for child care and after-school care, since Colorado pays its subsidy for both. Vermont pays the state rate "regardless of the provider's established rates" and pays it "directly to the family's child care provider(s)" (CCFAP Understanding Payments), so the amount above the bill doesn't reach the family. With no bill recorded, the whole subsidy counts. child_care_subsidies itself stays the full payment. When we ran one family in every state (a $1,000 bill for 2024 to 2026, a $12,000 bill for 2021 to 2027), only Vermont paid above the bill. Both data builds fill the bill from parent-paid ASEC child care (see the SPM section below). So in a microsimulation, Vermont's payment counts up to what parents reported paying, which can fall short of the provider's charge.

We don't subtract Vermont's family share, which providers may waive, so a family that pays the share gains up to that much less than we count. In the $10,000 case below, we count $10,000, and that family's $5,200 share would leave it $4,800 better off.

Tests

  • Added an Alaska regression showing a previously omitted subsidy reaches household state benefits.

  • Added a North Carolina regression showing that household state benefits count a previously direct-listed subsidy once.

  • Verified that each of the 51 programs in the 2025 CCDF list reaches child_care_subsidies and household_state_benefits once.

  • Vermont cases for a 2025 family (parent with $52,875 of earnings, child aged 3 at a licensed center 5 days a week, 8 hours a day). child_care_subsidies comes to $17,628 in each, from ($439 − $100) × 52:

    Case spm_unit_pre_subsidy_childcare_expenses household_child_care_subsidies household_state_benefits
    Bill below the payment 10,000 10,000 10,000
    Small bill 1,000 1,000 1,000
    Bill above the payment 20,000 17,628 17,628
    No bill recorded not set 17,628 17,628
    Bill set on the child (pre_subsidy_childcare_expenses 10,000) not set 10,000 10,000
  • A Connecticut case with ct_child_care_subsidies of $5,000 and a $12,000 bill: household_child_care_subsidies returns $5,000.

  • A Colorado case with co_child_care_subsidies of $12,000, a $6,000 child care bill and $6,000 of after-school expenses: household_child_care_subsidies returns $12,000.

SPM side

This PR changes household totals only. spm_unit_benefits still lists the subsidy for Colorado, Washington and West Virginia alone, and the SPM's child care subtraction still starts from the pre-subsidy bill. #10569 covers that side and waits on a data decision, because both data builds copy parent-paid ASEC child care into the pre-subsidy input.

Invariants

policyengine_us/tests/test_child_care_subsidies_household_benefits.py checks these for every dated block:

  • household_state_benefits lists household_child_care_subsidies once, doesn't list child_care_subsidies, and no state's own subsidy sits beside it, so no subsidy counts twice.
  • On each run, Hypothesis checks 15 sampled (year, program, amount) cases from 2023 to 2026 that, with no bill recorded, a dollar of subsidy adds a dollar to household_benefits and household_net_income. The block test checks every year structurally.
  • On each run, Hypothesis checks 15 sampled (year, program, amount, bill) cases from 2023 to 2026. In each, household_child_care_subsidies never exceeds the subsidy or a positive bill, equals the subsidy when the inputs hold no bill or the subsidy fits within the bill, and never falls when a positive bill rises. Three explicit examples pin a zero bill, a subsidy equal to the bill, and a subsidy a dollar above it.
  • test_latest_blocks_list_every_state_program checks that the CCDF list's 2025 and later blocks name every one of the 51 *_child_care_subsidies variables under variables/gov/states, so dropping a program from the list fails a test.

Impact

We estimate that this PR adds $834 million to household benefits and household net income in 2026 (pair). We simulated the whole Populace US file (populace-us-2024-spm-20260909) at 8ce831b6dc and at its merge base with main, 0d933918a8.

National measure Main This PR Change
household_benefits $2,059.79B $2,060.63B +$834M
household_net_income $17,161.43B $17,162.26B +$834M
spm_unit_net_income $14,653.60B $14,653.60B none
child_care_subsidies $1.79B $1.79B none

Households in the 10 states below gain. Main computes these states' subsidies in child_care_subsidies but leaves them out of household benefits.

State child_care_subsidies Added to household benefits
Connecticut $405.60M $405.60M
Michigan $180.23M $180.23M
Utah $67.09M $67.09M
Maine $48.37M $48.37M
New Mexico $41.89M $41.89M
Oregon $35.43M $35.43M
Vermont $45.95M $29.79M
New Hampshire $16.95M $16.95M
Alaska $6.45M $6.45M
Rhode Island $2.60M $2.60M

In Vermont, 12 of the 17 records with a subsidy (1,494 of 5,124 weighted SPM units) get more than their bill. We leave the $16.16 million above those bills, 35% of the state's $45.95 million, out of household benefits. One of them, with a weight of 1,306, gets $10,608 against a $1,054 bill and accounts for $12.48 million of that. No household's benefits fall by a cent or more.

child_care_subsidies already held each dollar, so apart from Vermont's amounts above the bill, this PR only stops dropping it from household totals. State rows rest on few records (Vermont's effective sample comes to 156 households), so they show exposure, not state spending. Vermont's $46M in subsidies falls short of the state's own $142M to $173M, and #10584 explains why.

This PR makes household_net_income run every state's subsidy formula. With this PR and #10584 merged, household_net_income calculates in all 51 jurisdictions for each year from 2021 to 2027, for a family with care and a family without.

test_child_care_subsidies_household_benefits.py has 7 tests: the block test (4 cases), the list-completeness test, and two property tests of 15 examples each. The cap property runs 1 simulation per example, or 2 when the bill is positive. On a 4 vCPU Linux cloud container, one process each, the old tests alone (-k "not up_to_the_bill and not latest") took 61.5s in pytest, 106.8s wall with imports, at 2.06 GB peak RSS; the whole file took 67.8s in pytest, 113.6s wall, at 2.19 GB peak. An earlier run of the whole file took 84.0s in pytest, so timings vary by about 20s between runs. We timed these outside CI. The Makefile doesn't name the file, so it runs in the rest Python group. The YAML cases pin single families, while this file reads every dated block of the list and generates programs, years, amounts and bills.

impact: we estimate that household benefits and household net income rise $834 million in 2026. The gain goes to households in 10 states whose child care subsidies main leaves out of household benefits. In Vermont, we count $29.8 million of $45.9 million in subsidies, up to each family's bill (pair).

axiom: n/a: household benefit measurement (child care subsidies counted up to the family's bill), no legal provision changes

🤖 Generated with Claude Code

Added test cases for Massachusetts TAFDC and EAEDC benefits, as well as Alaska and North Carolina child care subsidies. Updated household state benefits calculations to ensure accurate representation of state programs.
@mikewolfd

Copy link
Copy Markdown
Contributor

Independent confirmation of the problem this fixes, and one observation about its size.

We (HotGap, a benefits-cliff calculator on PolicyEngine) hit #9405 from the outside on 2026-09-15 against the hosted API (policyengine-us 1.764.6). The omission is worse than a missing line item: because childcare_expenses is defined net of child_care_subsidies, an omitted state's household looks poorer for having the subsidy — SNAP's dependent-care deduction shrinks, and the subsidy that ate it never arrives in household_net_income.

Connecticut, single parent, one 3-year-old, $9,600/yr of care, $25,000 of pay, meets_ccdf_activity_test: true:

ct_child_care_subsidies modeled forced to 0
child_care_subsidies 8,850 0
household_state_benefits 0 0
snap 2,577 6,559
household_net_income 34,121 38,102

Colorado (in the list) is the control: the same household at $25,000 shows household_state_benefits 8,913 = the subsidy to the dollar, and net income moves by the subsidy when co_child_care_subsidies is forced to 0 (45,226 → 39,944).

On the diff itself: I pulled the branch and checked the parameter — the aggregate is in all four periods (2023–2026) and none of the 84 per-state entries survive, so the double-count the PR description guards against can't happen. Swapping to child_care_subsidies also matches what childcare_expenses already subtracts, so the two sides of the household stay on one variable.

Downstream note for anyone else adding the subsidy back in the omitted states: this PR moves it inside household_net_income everywhere, so that workaround has to stand down on the release that carries it.

@MaxGhenis MaxGhenis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for picking up #9405, and sorry this sat for four days without a review.

Replacing the hand-listed state entries with child_care_subsidies is the right fix. There are 51 *_child_care_subsidies variables and gov.hhs.ccdf.child_care_subsidy_programs lists all 51 for 2025, so the aggregate cannot drift the way the hand list did.

One thing blocks merge. Full Suite - Rest fails in test_aggregate_list_integrity.py::test_members_are_state_gated:

gov/household/household_state_benefits.yaml members without a StateCode gate in their defined_for chain (dc_ctc-style leak risk): ['child_care_subsidies']

That guard is recent (#9497) and only walks defined_for, so it cannot see that every member of the aggregate's adds list is state-gated. The problem is the guard, not your change. Either teach it to accept a member with no defined_for when everything in its adds list is itself state-gated, or add an explicit exemption for child_care_subsidies with a comment saying why. The first is better because the next national aggregate will hit the same wall.

Three things to know about, none blocking:

  • Vermont. vt_ccfap pays the state rate whatever the provider charges, which is Vermont's rule since December 2023, so it can exceed the family's child care cost. With this PR that amount reaches household_net_income for the first time. A Vermont case that pins it would make the number a decision instead of a surprise.
  • SPM side. spm_unit_benefits still lists only co_ccap_subsidy, wa_child_care_subsidies and wv_child_care_subsidies, so household and SPM net income will disagree on child care in most states. Fine as a follow-up, but worth a line in the PR body.
  • Tests. The two new cases set the state variable as an input, so they check list membership only. One case that computes a real subsidy in a newly connected state would be stronger.

After the guard fix, please merge main so CI runs on a current base.

test_members_are_state_gated failed on household_state_benefits because child_care_subsidies is itself an aggregate with no defined_for gate. An aggregate whose every summed member is state-gated cannot leak across states, so the guard now recurses through a member's adds list (parameter path or literal list, depth-capped at 3) and accepts it only when every addend is gated. Requested in review of PolicyEngine#9503.
Merge PolicyEngine main into fix/household-child-care-subsidies
MaxGhenis and others added 2 commits November 1, 2026 17:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vermont pays the state rate whatever the provider charges, so a computed case
now records that the payment above the bill reaches household benefits. The
property tests state that no subsidy counts twice and that a dollar of subsidy
adds a dollar to household benefits and net income.

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

Copy link
Copy Markdown
Contributor

I merged main into this branch, because main had added state supplement entries to the same list and the PR conflicted.

  • Conflict: I kept main's list, removed each *_child_care_subsidies entry and added child_care_subsidies to each block. Every year's list now equals main's list without those entries, plus the aggregate.
  • No state drops: every direct entry on main sits inside the aggregate for that year, so the swap only adds states.
  • Vermont: a computed case now pins a subsidy above the bill. With a $10,000 bill, Vermont pays $17,628, all of it counts as a benefit, and childcare_expenses stops at zero.
  • Invariants: test_child_care_subsidies_household_benefits.py checks that no state's own subsidy sits beside the aggregate, and that a dollar of subsidy adds a dollar to household benefits and net income.

The SPM side stays with #10569. I'll add the state impact here once the runs finish.

Thanks for the fix, @RajveerKapoor.

MaxGhenis and others added 3 commits November 2, 2026 02:39
Vermont's family still owes its share, so the zero that childcare_expenses
returns isn't a result worth pinning. State why the household test is Python and
halve its examples.

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

Copy link
Copy Markdown
Contributor

Handing this PR to the US hub to land once CI passes.

@MaxGhenis

Copy link
Copy Markdown
Contributor

Ready for hub review
VERDICT: REQUEST_CHANGES
100a1ae
METHODOLOGY_CHOICE: yes

The head hasn't moved since 100a1aed40. Main has moved to f9dcb5e794 (railroad retirement in SSI, a Trustees test, two version bumps). None of those commits touch child care, the household lists or partner tests, and the PR still merges cleanly with main (git merge-tree --write-tree origin/main pr-9503 exits 0).

The list swap is correct. Every block now carries the aggregate once, nothing counts twice, CI is green, and household_net_income calculates everywhere. One finding blocks: the PR makes Vermont's payment above the family's bill reach household_net_income in full, and for a family with a small bill that is worse than main. The PR picks that reading on purpose and pins it in a test, so the hub has to rule on it. If the hub rules the full payment counts, P1 drops to a documentation note and the rest is P2/P3.

Findings

P1. Vermont's payment above the bill now counts in full in household_net_income, and for small bills that is worse than main

Where: policyengine_us/parameters/gov/household/household_state_benefits.yaml:5,111,217,323 (the aggregate), reached through vt_ccfap.py:37-41. It is pinned by tests/policy/baseline/household/income/household/household_state_benefits.yaml:234-265.

What the code does. From 2023-12-17, pays_above_provider_charge is true and vt_ccfap pays the full state rate whatever the bill is (vt_ccfap.py:37-41). I couldn't fetch Vermont's own documents (see "What I couldn't check"). A web search returned a DCF provider memo on tuition rates and fees: "The State pays a CCFAP state rate regardless of your program's 'usual and customary rate'". So Vermont appears to pay above the charge on purpose, and the payment goes to the provider.

Which states pay above the bill. I ran a family with a $1,000 annual bill, 40 hours of care a week and $25,000 of earnings in every jurisdiction for 2024, 2025 and 2026. Only Vermont paid more than the bill (156 runs, 0 errors):

[('VT', 2024, 22828.0), ('VT', 2025, 22828.0), ('VT', 2026, 22828.0)]

A $12,000 bill across 2021–2027 found the same: Vermont alone in 2024–2027, at $22,828 = $439 × 52.

Why counting the excess is wrong. The family's resources rise by at most the bill it no longer pays. The excess is money paid to the provider. On main, Vermont isn't in the household list, so the family's net income is understated by the bill. At this head it is overstated by the subsidy minus the bill. For any bill under half the subsidy, the error grows:

BILL=1000, VT 2025, one parent ($25,000) and a 3-year-old in 40h/week center care
head: child_care_subsidies 22828.0  household_benefits 25437.06  household_net_income 56707.01
main: child_care_subsidies 22828.0  household_benefits  2609.06  household_net_income 33879.01

At this head, a family with a $1,000 bill gains $22,828 of net income. On main it gains $0. The value to the family is at most $1,000. The same error reaches equiv_household_net_income, household_income_decile, household_net_income_including_health_benefits and marginal_tax_rate. Under the hub's scope rule this is the PR's defect: the PR makes an unchanged reader (vt_ccfap) reach household_net_income and, for these families, makes it worse. The body's impact run can't show how much of Vermont's $45.95M is above-bill money.

Fix, unless the hub rules the full payment counts. Count the subsidy up to the bill on the household side. With no bill recorded, the full payment still counts, so Vermont's hours-only families and the property test's direct inputs don't change. Add policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py:

from policyengine_us.model_api import *


class household_child_care_subsidies(Variable):
    value_type = float
    entity = SPMUnit
    definition_period = YEAR
    label = "Child care subsidies counted in household benefits"
    documentation = (
        "Child care subsidies up to the family's pre-subsidy bill. A payment "
        "above the bill goes to the provider, not the family. With no bill "
        "recorded, the whole subsidy counts."
    )
    unit = USD

    def formula(spm_unit, period, parameters):
        subsidies = spm_unit("child_care_subsidies", period)
        bill = spm_unit("spm_unit_pre_subsidy_childcare_expenses", period)
        return where(bill > 0, min_(subsidies, bill), subsidies)

Then replace - child_care_subsidies with - household_child_care_subsidies in all four blocks. I applied exactly that on a scratch copy and ran the same family, then restored the copy:

bill=10000 VT: child_care_subsidies 22828.0  household_state_benefits 10000.0
bill=1000  VT: child_care_subsidies 22828.0  household_state_benefits  1000.0
bill=0     VT: child_care_subsidies 22828.0  household_state_benefits 22828.0
bill=10000 CT: child_care_subsidies  9250.0  household_state_benefits  9250.0   (unchanged)

With this fix, the Vermont YAML case expects household_state_benefits: 10_000, and its comment should say the household measure counts the payment up to the bill. The block test would check for household_child_care_subsidies. If the hub instead rules that the full payment counts, keep the code, but have the YAML comment and the body say the household measure counts payments above the bill as household income, and why.

P2. Dropping a state from the CCDF list survives every changed test

Where: policyengine_us/tests/test_child_care_subsidies_household_benefits.py:40-43,63-69.

The PR's case rests on the CCDF list being the single source of truth. No test checks that the list holds every state program. I ran each mutant on a scratch worktree, ran the three changed test files one per process, then restored the worktree (git status --short empty after each):

Mutant test_child_care_subsidies_household_benefits.py test_aggregate_list_integrity.py household_state_benefits.yaml
Delete ct_child_care_subsidies from every CCDF block 5 passed 29 passed 10 passed
Delete it from the 2025 block only 5 passed killed by test_no_silent_removals_between_year_blocks 10 passed
Re-add vt_child_care_subsidies to the 2026 household block (mine) killed by test_each_block_counts_only_the_aggregate[2026-01-01]; the property passed 29 passed 10 passed
Delete the aggregate from the 2023 household block (mine) killed by test_each_block_counts_only_the_aggregate[2023-01-01] and the property 29 passed 10 passed

No Connecticut test asserts child_care_subsidies or a household output either: grep -rln ct_child_care_subsidies policyengine_us/tests finds nothing. Add this to test_child_care_subsidies_household_benefits.py:

def test_latest_blocks_list_every_state_program():
    defined = {
        path.stem
        for path in (PARAMETERS.parent / "variables/gov/states").rglob(
            "*_child_care_subsidies.py"
        )
    }
    assert len(defined) == 51
    for start, programs in blocks(PROGRAMS).items():
        if start >= "2025":
            assert set(programs) == defined, start

Run as a script, the same check passes at head with 51 programs. With Connecticut deleted it fails with {'2025-01-01': ['ct_child_care_subsidies']}. (#10569 carries a stricter version from 2024, which only holds once #10569 adds DC, Tennessee and Texas to the 2024 block.)

P2. The property test is weaker than the body says

Where: test_child_care_subsidies_household_benefits.py:63-69; the body's "Invariants" section.

  • The test can't pass trivially. It sets the program's value directly and checks a dollar-for-dollar change, and it killed the aggregate-deleted mutant.
  • It samples programs only from the CCDF list, so it can never notice a program missing from that list (the first mutant above).
  • It draws 15 (year, program) pairs from about 190, with no fixed seed, so any one pair is checked with low probability. In my run it missed the re-added Vermont entry in 2026, and only the block test killed that mutant.
  • It samples 2023–2026. The 2023 household block also governs 2021 and 2022, and the 2026 block governs 2027.

The body says "A dollar of subsidy adds a dollar … in every state with a program that year. Hypothesis samples the year, program and amount." Replace that with: "On each run, Hypothesis checks 15 sampled (year, program, amount) cases from 2023 to 2026 that a dollar of subsidy adds a dollar to household_benefits and household_net_income. The block test checks every year structurally." Or make the test check every program for one fixed year, which costs 51 × 2 simulations.

P3. The body has no impact: line

The body has an "## Impact" section and an axiom: line (axiom: n/a: household benefit aggregate list, no rule changes), but no impact: line. Add one, for example impact: +$0.85B household_benefits and household_net_income, 2026, populace-us-2024-spm-20260909, 469ad12 vs this branch with #10584. It should be restated once the hub runs the landing pair.

P3 (follow-up, the same on main). Programs that compute in years their CCDF block leaves out

The family below (one parent, a 3-year-old in full-time care, $12,000 bill) gets a subsidy from the state variable, but child_care_subsidies and household_state_benefits are 0:

NC 2021-2023  nc_child_care_subsidies 7632.0    child_care_subsidies 0.0  household_state_benefits 0.0
OR 2021-2023  or_child_care_subsidies 11940.0   child_care_subsidies 0.0  household_state_benefits 0.0
DC 2021-2024  dc_child_care_subsidies 12000.0   child_care_subsidies 0.0  household_state_benefits 0.0
TX 2021-2024  tx_child_care_subsidies 5298.66-5396.45  child_care_subsidies 0.0  household_state_benefits 0.0
TN 2024       tn_child_care_subsidies 2704.0    child_care_subsidies 0.0  household_state_benefits 0.0

On main, none of these was directly listed in those years, so this PR doesn't make it worse. But the changelog line "Count every implemented state child care subsidy in household benefits" is too broad. Either change it to "Count every state child care subsidy in the CCDF program list in household benefits", or open a follow-up deciding whether these programs belong in their earlier blocks. Several of them run on rates copied back from later years, so adding them isn't automatically right. #10569 adds DC, Tennessee and Texas to 2024.

P3 (follow-up, the same on main). childcare_expenses loses the Vermont family's share

As the new YAML comment says, childcare_expenses = max(bill − subsidy, 0) is 0 for the Vermont case, although the family pays a $100 weekly share ($5,200 a year). childcare_expenses is unchanged by this PR, so this belongs in a follow-up, not here.

Checked and sound

  • The list, at head and main. I parsed every dated block of household_state_benefits.yaml at head and at main, and of child_care_subsidy_programs.yaml (identical at both).

    • Each head block (2023, 2024, 2025, 2026) contains child_care_subsidies exactly once and no *_child_care_subsidies entry.
    • Each head block equals main's block with its direct child care entries removed and the aggregate added.
    • Every direct entry on main sits in the CCDF block for that year (none dropped), so the swap only adds states. Newly counted: 30 states for 2023 (and earlier), 26 for 2024, and 28 for 2025 and 2026: AK CT FL HI IA IL KS KY LA MA ME MI MN MS ND NH NJ NM OR PA RI SD TN TX UT VT WI WY.
    • The code defines 51 *_child_care_subsidies variables. The 2025 CCDF block lists all 51, the 2024 block lacks DC, TN and TX, and the 2021 block also lacks NC and OR (see the follow-up above).
  • Double counting. The paths from a state subsidy to the outputs:

    • household_net_income adds household_benefits (which adds household_state_benefits → child_care_subsidies) and doesn't read spm_unit_benefits.
    • spm_unit_benefits lists co_ccap_subsidy (= co_child_care_subsidies), wa_child_care_subsidies and wv_child_care_subsidies. It feeds only spm_unit_net_income.
    • household_local_benefits holds only co_denver_property_tax_relief.
    • No other household list names a child care program.
    • Family runs (2025; one parent at $25,000 working 40 hours, a 3-year-old in 40h/week care, 20 attending days a month, $12,000 bill), identical at head and main:
      CO: co_child_care_subsidies 11249.52  household_state_benefits 11249.52  household_benefits 13386.58  spm_unit_benefits 13386.58
      WA: wa_child_care_subsidies 11220.00  household_state_benefits 11220.00  household_benefits 13360.06  spm_unit_benefits 13360.06
      WV: wv_child_care_subsidies  7500.00  household_state_benefits  7500.00  household_benefits 10756.06  spm_unit_benefits 10756.06
      
      Each subsidy counts once in each output. Connecticut shows the fix: household_net_income goes from 33,222.62 at main to 44,472.62 at head (+11,250 = the subsidy), and spm_unit_net_income stays at 21,222.62.
  • The hand values. Vermont case: age 3 = 36 months, so PRESCHOOL. 40 hours is FULL_TIME (more than 25, up to 50). The 2025 licensed-center rate is $439 a week. $52,875 is 250% of the 2025 guideline for two ($21,150), which falls in the 2.2501–2.5001 band, so the share is $100 a week. ($439 − $100) × 52 = $17,628, matching the test. The Alaska and North Carolina cases set the program value directly.

  • Calculation cost. I ran household_net_income (with child_care_subsidies, household_state_benefits, household_benefits and spm_unit_benefits) at head for every StateCode except the territories and military codes: 50 states, DC and PW. That was each year 2021–2027, for the family above with care and for one without care or a bill: 364 runs each, 0 errors. Mean 1.9s per run after the first, maximum 15.0s (the first run loads the system). No state needed an input without a default. With no care, no state paid a subsidy in any year.

  • Tests. I ran each changed file once, one per process, on a 4-vCPU Linux VM shared with my sweep, so these are contended local numbers, not CI:

    • test_child_care_subsidies_household_benefits.py: 5 passed in 65.7s, peak RSS 2.08 GB.
    • test_aggregate_list_integrity.py: 29 passed.
    • household_state_benefits.yaml: 10 passed.
    • The hand-off's "92 pytest cases and 318 YAML cases" rest on a local run I didn't repeat.
    • The test_aggregate_list_integrity.py change (is_state_gated looks through an aggregate whose members are all state-gated) is sound. child_care_subsidies itself has no gate, and every CCDF member has a StateCode gate.
  • CI at 100a1aed40: run 41460582333, all 34 jobs success, including Household API Partners and every baseline, contrib and Python shard.

  • Readers. Readers of household_state_benefits:

    • household_benefits (and the BOOST, ECPA and EDAA overrides of it);
    • the tx_rebate reform, which copies the 2027 list and appends tx_rebate, so it now carries the aggregate.

    Readers of household_benefits: household_net_income.

    Readers of household_net_income: household_net_income_including_health_benefits, equiv_household_net_income, household_income_decile, marginal_tax_rate (and its helpers), behavioral_response_measurements and capital_gains_responses.

    These move at baseline for families with a subsidy in a newly counted state: household_state_benefits, household_benefits, household_net_income, household_net_income_including_health_benefits, equiv_household_net_income, household_income_decile and marginal_tax_rate. For example, Connecticut's equiv_household_net_income goes from 23,491.94 to 31,446.89, and the summed marginal_tax_rate from −6.28 to −6.25. Behavioral responses move only under a reform with nonzero elasticities. spm_unit_net_income, SPM poverty and childcare_expenses don't move. Nothing in a state tax or benefit formula reads these aggregates.

  • Impact provenance. 469ad12 is on main. Of main's commits since then:

    The landing pair needs these variables, by state and nationally: child_care_subsidies, each newly counted *_child_care_subsidies, household_state_benefits, household_benefits, household_net_income, equiv_household_net_income / household_income_decile (deciles and Gini), spm_unit_net_income and SPM poverty (expected unchanged). Also, for P1: Vermont's max(child_care_subsidies − spm_unit_pre_subsidy_childcare_expenses, 0).

  • Overlap.

    • Count state child care subsidies in the SPM once #10569 (open, held) changes the CCDF list by folding DC, Tennessee and Texas into the 2024 block, and changes spm_unit_benefits. It merges cleanly with this PR. Its subsidy_counted_by_household_measure handles either the direct entry or the aggregate, so landing this PR breaks nothing it assumes. Once both land, DC, Tennessee and Texas reach household benefits in 2024 as well.
    • Count Illinois AABD cash in household and SPM benefits #10086 (Illinois AABD) also edits household_state_benefits.yaml. It merges cleanly with main but conflicts textually with this PR.
  • Scope and partners. The PR changes no file under .github/ or tests/policy/baseline/partners/. No partner test asserts a household aggregate, child_care_subsidies or marginal_tax_rate, and the Household API Partners job passed. No partner value moves.

  • Changelog: changelog.d/9405.fixed.md is top-level with a valid type.

What I couldn't check

  • Vermont's sources. This session's network blocks outside.vermont.gov and legislature.vermont.gov, so I couldn't read the CCFAP regulations, "Understanding Payments" or the capped-rates guidance. The statement that Vermont pays above the provider's charge rests on the parameter, the code comment and a search-result excerpt of a DCF memo. That includes whether the July 13, 2025 "Capped State Rates" guidance limits payment to the charge.
  • The body's population figures. The +$0.85B and the state rows come from a dataset I can't reach. I ran no microsimulation.
  • The hand-off's local logs and counts.
  • A main-side copy of the full sweep. The main-worktree sweep stopped partway, so I compared main and head only for the CO, WA, WV, CT and VT families shown.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor

Picking this up for the US hub. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor

Ready for hub review

Head: c1483a095e6e93a40fefef5a9c5c76c016c5721e on hub/pr9503-r1 in PolicyEngine/policyengine-us. The git proxy refused the fork (RajveerKapoor/policyengine-us isn't in this session's repository set, HTTP 403), so the commits aren't on this PR's head, which is still 100a1aed40534b6e50a791fcc92cee8c191b23b7. No PR was opened for the hub branch. The hub needs to move two commits onto fix/household-child-care-subsidies: e8a51741eb (merge of origin/main) and c1483a095e (the fix).

Ownership check: PR open, not draft, head matched the item, last push 2026-11-02T14:11:56Z (over 8 hours before pick-up).

What the item asked for

  • Merge main. e8a51741eb merges origin/main with a merge commit. No conflicts. household_state_benefits.yaml had no new entries on main, so nothing needed resolving there.

  • Finding 1, variable. policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py:23 returns where(bill > 0, min_(subsidies, bill), subsidies). The bill is add(spm_unit, period, ["pre_subsidy_childcare_expenses"]), which is how childcare_expenses.py reads it.

    • Judgment call: the reference is one href (...Understanding-Payments.pdf#page=1) with a # PDF pages 1-2 comment above it, not the two #page= entries in the brief. CLAUDE.md says URLs that differ only by #page= are one source, and that variable tuples use a comment for several pages.
  • 1.1: household_state_benefits.yaml:5,111,217,323 now list household_child_care_subsidies in all four blocks. No block lists child_care_subsidies.

  • 1.2: test_aggregate_list_integrity.py:42 adds CAPPED_AGGREGATES. aggregate_members (:115-116) returns the mapping's value first.

  • 1.3: test_child_care_subsidies_household_benefits.py:43-47: each block lists household_child_care_subsidies once, doesn't list child_care_subsidies, and lists no state program. The docstring (:1-16) restates invariant 2 for the no-bill case and adds invariant 3, the cap.

  • 1.4: test_household_benefits_count_the_subsidy_up_to_the_bill (:115-137) runs with max_examples=15 and deadline=None. It samples year, program, an amount from 0 to 30,000, a bill from 0 to 30,000, and extra from 1 to 30,000 for the monotonicity check, then asserts all four properties.

    • The family has a parent and a child aged 3. With only an adult, pre_subsidy_childcare_expenses is zero for everyone (the person formula prorates over is_child), so a bill on the SPM unit would never cap anything.
    • The program's value is set directly on the SPM unit, and the bill is set as spm_unit_pre_subsidy_childcare_expenses.
  • 1.5: the Vermont cases are at household_state_benefits.yaml (test) :234-381, under the brief's comment plus a second comment with the ($439 − $100) × 52 derivation. Every computed value matched the table:

    Case bill child_care_subsidies household_child_care_subsidies household_state_benefits
    Bill below the payment 10,000 17,628 10,000 10,000
    Small bill 1,000 17,628 1,000 1,000
    Bill above the payment 20,000 17,628 17,628 17,628
    No bill recorded not set 17,628 17,628 17,628
    Person-level bill (pre_subsidy_childcare_expenses 10,000 on the child, no SPM input) n/a 17,628 10,000 10,000

    The Connecticut case (:383) sets ct_child_care_subsidies to 5,000 and a 12,000 bill, and gets child_care_subsidies 5,000 and household_child_care_subsidies 5,000.

  • Finding 2: test_latest_blocks_list_every_state_program is at test_child_care_subsidies_household_benefits.py:50-60, as written in the brief. It finds 51 definitions, and the 2025 block matches them.

  • Finding 3: changelog.d/9405.fixed.md now has the brief's sentence.

  • Finding 4: I PATCHed the body from a file, with every listed edit and a replaced test-cost paragraph. GitHub's readback matched my file except the last line: the server swapped the closing 🤖 Generated with [Claude Code](https://claude.com/claude-code) line for a --- / _Generated by [Claude Code](https://claude.ai/code)_ footer. The impact: and axiom: lines are there. The body describes code that is on hub/pr9503-r1, not yet on this PR's head.

Sources

outside.vermont.gov is outside this session's network allowlist. curl got a proxy 403, WebFetch got DNS failure, and archive.org was also blocked. So I could not read either Vermont PDF, and the two quotes in the body ("regardless of the provider's established rates", "directly to the family's child care provider(s)") are the brief's, not checked by me.

Tests (one file per process, Linux container)

  • test_child_care_subsidies_household_benefits.py: 7 passed.
  • test_aggregate_list_integrity.py: 29 passed.
  • tests/policy/baseline/household/income/household/household_state_benefits.yaml: 15 passed.
  • tests/policy/baseline/gov/states/vt/dcf/ccfap/vt_ccfap.yaml: 17 passed.
  • tests/policy/baseline/gov/states/vt/dcf/ccfap/integration.yaml: 13 passed.

Cost of the Python file (local, not CI):

  • Old tests only: 61.5s in pytest, 106.8s wall, 2.06 GB peak RSS.
  • Whole file: 67.8s in pytest, 113.6s wall, 2.19 GB peak RSS.
  • An earlier whole-file run took 84.0s in pytest.

Hand computation

The subsidy is ($439 − $100) × 52 = $17,628. Each capped value is min(17,628, bill): 10,000, 1,000 and 17,628. With no bill it is 17,628.

Mutants (each in its own process, restored with git checkout after each; tree clean at the end)

  • (a) return subsidies: killed. YAML failed the 10,000, 1,000 and person-level Vermont cases, and the cap property failed.
  • (b) min_ with no where: killed. YAML failed the NC 2024, DC, Alaska, NC-once and Vermont no-bill cases. Both property tests failed.
  • (c) max_ for min_: killed. YAML failed all four Vermont cases with a bill, plus Connecticut. The cap property failed.
  • (d) child_care_subsidies back in the 2026 block: killed by test_each_block_counts_only_the_aggregate[2026-01-01] and test_a_dollar_of_subsidy_adds_a_dollar. The YAML passed.
  • (e) ct_child_care_subsidies deleted from all 3 CCDF blocks: killed by test_latest_blocks_list_every_state_program and the Connecticut YAML case (child_care_subsidies 0 vs 5,000). My first attempt matched no lines, because of the trailing comments, so I reran it with a fixed regex. The rerun ran the Python file with -k "latest or each_block", without the Hypothesis tests.
  • (f) bill from spm_unit_pre_subsidy_childcare_expenses: killed only by the person-level Vermont case (17,628 vs 10,000).

test_aggregate_list_integrity.py passed under every mutant.

CI

None at c1483a095e. Its branch has no PR, and the PR's head hasn't moved. CI will run once the hub moves the commits onto the fork branch.

Judgment calls

  • The reference is one href with a page comment (see Finding 1).
  • The cap property uses a parent and a child, and samples an extra amount for the monotonicity check.
  • The test-cost paragraph in the body now gives my Linux numbers in place of the earlier macOS ones.
  • I ran no microsimulation.

Follow-up issue I'd propose

Subtract Vermont's family share from the household child care cap. household_child_care_subsidies caps at the full bill, so a Vermont family that pays its weekly share counts up to that share more than it gains. Capping at bill minus the family share, where it's paid, would need data on whether providers waive the share.


Generated by Claude Code

MaxGhenis and others added 2 commits November 2, 2026 19:10
Vermont pays its state rate to the provider whatever the provider charges, so
its subsidy can exceed the family's bill, and the amount above the bill never
reaches the family. household_child_care_subsidies caps child_care_subsidies at
the pre-subsidy bill (whole subsidy with no bill recorded), and every
household_state_benefits block counts it in place of the uncapped aggregate.
child_care_subsidies stays the full payment.

Tests: Vermont cases at bills below, at and above the payment, with no bill and
with the bill set on the child; a Connecticut case; a Hypothesis property of the
cap; and a check that the 2025+ CCDF blocks list every state program.

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

Copy link
Copy Markdown
Contributor

Hub review in progress

Reviewing head 94534943141314d7f000b2bac774952aa0e27b10, round 2. These findings are interim.

  • P1 (firming up). The cap leaves out Colorado's after_school_expenses, so it understates Colorado's subsidy compared with main. co_ccap_subsidy pays pre_subsidy_childcare_expenses + after_school_expenses - parent_fee, but household_child_care_subsidies caps at pre_subsidy_childcare_expenses alone. For a 2025 CO family with a $6,000 bill and $6,000 of after-school expenses, household_state_benefits is 6,000 at head and 12,000 at main, while spm_unit_benefits counts 13,909.06 at both. With a $1 bill, head counts $1 of a $6,001 subsidy.
  • Network: outside.vermont.gov, ecfr.gov and web.archive.org are all refused from this session.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor

Ready for hub review
VERDICT: REQUEST_CHANGES
9453494
METHODOLOGY_CHOICE: yes

This is round 2, a delta review. The head hasn't moved past 9453494314. Since round 1's head 100a1aed40, the branch has one merge of main (a2396d3146, no conflicts in the PR's files) and one fix commit (9453494314). The fix commit adds household_child_care_subsidies, swaps it into the four household_state_benefits blocks, adds CAPPED_AGGREGATES to the list-integrity test, and adds two tests (the list-completeness test and the cap property), five Vermont/person-level YAML cases and a Connecticut case. It also rewrites the changelog. I read git diff 100a1aed40 9453494314 for the six PR files.

Round 1's findings are resolved as the hub ruled, and the Vermont values are right. One new finding blocks: the cap's bill leaves out after_school_expenses, which Colorado's subsidy pays for. So for a Colorado family with after-school costs, this commit lowers household_net_income below main's value, and the household side now disagrees with spm_unit_benefits.

Findings

P1. The cap leaves out after-school expenses, so Colorado's counted subsidy falls below main

Where: policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py:22.

co_ccap_subsidy.py:13-19 pays add(spm_unit, period, ["pre_subsidy_childcare_expenses", "after_school_expenses"]) - co_ccap_parent_fee. The cap's bill is add(spm_unit, period, ["pre_subsidy_childcare_expenses"]) alone. On main, co_child_care_subsidies was listed directly in household_state_benefits and counted in full. At this head, the part of Colorado's subsidy that pays for after-school care is cut off. spm_unit_benefits still lists co_ccap_subsidy uncapped (spm_unit_benefits.py:61), so the two measures now disagree. A $1 bill is worse than no bill at all: with no bill the whole subsidy counts, and with a $1 bill only $1 counts.

The witness family is 2025 Colorado: one parent with $25,000 of earnings working 40 hours, and a 3-year-old in 40 hours a week of care, 20 days a month. after_school_expenses is set on the child.

                        co_child_care_subsidies  household_state_benefits  spm_unit_benefits  household_net_income
bill 6,000, after 6,000  head 12000.00            head  6000.00            13909.06           head 42662.02
                         main 12000.00            main 12000.00            13909.06           main 48662.02
bill 1,   after 6,000    head  6001.00            head     1.00             7910.06           head 36663.02
                         main  6001.00            main  6001.00             7910.06           main 42663.02
no bill,  after 6,000    6000.00 at both          6000.00 at both           7909.06           42662.02 at both
bill 12,000, no after    12000.00 at both         12000.00 at both         13909.06           48662.02 at both

This reaches the household calculator, because after_school_expenses is a user input. I couldn't check whether the population data fills it (see below). No other program reads after_school_expenses (grep -rn after_school_expenses policyengine_us/variables finds only co_ccap_subsidy.py and the input). No test catches this, because the cap property sets each program's value directly and no YAML case uses after-school expenses.

Fix. Count what the payment covers in the bill:

        bill = add(
            spm_unit,
            period,
            ["pre_subsidy_childcare_expenses", "after_school_expenses"],
        )

I applied exactly that on a scratch copy and reran everything, then restored it (git status --short empty):

  • Colorado's four cases above now equal main's values: 12000.00 / 6001.00 / 6000.00 / 12000.00 in household_state_benefits.
  • household_state_benefits.yaml: 15 passed.
  • vt/dcf/ccfap/integration.yaml: 13 passed.

Add a YAML case beside Connecticut's, and update the docstring and documentation to say "pre-subsidy child care and after-school bill":

# Colorado's subsidy pays for after-school care too, so the cap counts that bill.
- name: Colorado's subsidy for child care and after-school care counts in full
  period: 2025
  input:
    people:
      parent:
        age: 30
      child:
        age: 3
        after_school_expenses: 6_000
    spm_units:
      spm_unit:
        members: [parent, child]
        co_child_care_subsidies: 12_000
        spm_unit_pre_subsidy_childcare_expenses: 6_000
    households:
      household:
        members: [parent, child]
        state_code: CO
  output:
    household_child_care_subsidies: 12_000

(I didn't run this exact case. My witness above computes the subsidy rather than setting it.)

P2. On population data, the "pre-subsidy bill" is what parents reported paying after the subsidy

Where: the body's "## Fix" ("counts each family's subsidy up to its pre-subsidy bill"), and the variable's documentation.

#10569 adds this documentation to spm_unit_pre_subsidy_childcare_expenses: "Datasets fill it from the ASEC's SPM_CHILDCAREXPNS, which holds what parents reported paying after any subsidy." If that holds, then in a microsimulation the cap binds at the family's reported spending after the subsidy, not at the provider's charge. A Vermont record with $2,000 of reported spending counts $2,000 of a $17,628 payment, even if the provider charges $10,000. In every other state, the round 1 sweeps and mine (below) found no subsidy above the bill, so this mostly affects Vermont's row. I didn't verify the data build myself, since it lives outside this repository. The body should say this before the hub reads Vermont's landing-pair row. For example, after the "Limit" paragraph:

In the population data, spm_unit_pre_subsidy_childcare_expenses holds what parents reported paying, which the ASEC records after any subsidy (see #10569). So in a microsimulation, Vermont's payment counts up to that reported spending, which can understate the provider's charge.

P3. The cap property doesn't reliably reach its bill == 0 and amount == bill boundaries

Where: policyengine_us/tests/test_child_care_subsidies_household_benefits.py:115-137.

The hand-off says mutant (b) (min_(subsidies, bill) with no where) failed both property tests. In my run of (b), test_household_benefits_count_the_subsidy_up_to_the_bill passed (1 failed, 6 passed: only test_a_dollar_of_subsidy_adds_a_dollar failed). With 15 draws and no seed, Hypothesis didn't draw bill == 0. Pin the boundaries:

from hypothesis import example, given, settings
...
@settings(max_examples=15, deadline=None)
@example(year=2025, index=0, amount=5_000, bill=0, extra=1)
@example(year=2025, index=0, amount=5_000, bill=5_000, extra=1)
@example(year=2025, index=0, amount=5_001, bill=5_000, extra=1)
@given(

On a scratch copy, with these three examples: -k up_to_the_bill gives 1 passed at head, and 1 failed under mutant (b). I restored the copy afterwards.

P3. CAPPED_AGGREGATES is a hand-written claim that nothing checks against the formula

Where: policyengine_us/tests/test_aggregate_list_integrity.py:42,115-116.

aggregate_members returns the mapping in place of what the variable reads. So test_members_are_state_gated checks the mapping, not the formula. Two of my mutants show this, each run in its own process and restored:

  • (h) Map household_child_care_subsidies to {"de_ssp"}: test_aggregate_list_integrity.py 29 passed, and the household YAML 15 passed. Nothing kills it, and it changes no behavior, so it's harmless today.
  • (i) Make the formula read spm_unit("snap", period), a national, ungated program: test_aggregate_list_integrity.py 29 passed. The household YAML killed it (10 failed).

So the gate test would let an ungated read inside a capped variable through. YAML caught this one only because the cases assert values. A cheap guard is to assert that each capped variable's formula source names its mapped aggregate:

def test_capped_aggregates_read_their_aggregate(variables):
    import inspect
    for name, members in CAPPED_AGGREGATES.items():
        source = inspect.getsource(variables[name].formulas[min(variables[name].formulas)])
        for member in members:
            assert f'"{member}"' in source, (name, member)

(I didn't run this sketch. Treat it as a suggestion.)

On the hub's question about a state program listed directly beside the aggregate: test_aggregate_list_integrity.py never checked for that, and still doesn't (29 passed under round 1's re-add mutant). test_each_block_counts_only_the_aggregate in the other file is the guard. It also now fails if child_care_subsidies is listed beside the new variable (the hand-off's mutant (d)).

P3. The axiom: line's reason no longer fits the diff

axiom: n/a: household benefit aggregate list, no rule changes was written when the PR only edited a list. The PR now adds a variable with a formula. The formula is a PolicyEngine measurement choice, not a legal provision, so n/a still holds, but the reason should say what changed:

axiom: n/a: household benefit measurement (child care subsidies counted up to the family's bill), no legal provision changes

P3 (follow-up, the same on main). childcare_expenses also ignores after-school expenses

childcare_expenses.py:12-16 subtracts the whole child_care_subsidies from pre_subsidy_childcare_expenses alone. So for the Colorado family above, it is max(6,000 − 12,000, 0) = 0 at both head and main. The family's after-school cost (net of the parent fee) never shows up. This PR doesn't change it.

Checked and sound

  • The defect at main. At origin/main (0d933918a8), for the 2025 family above in Connecticut with a $12,000 bill: ct_child_care_subsidies 11250.00, household_state_benefits 0.00, household_net_income 33222.62. At head: 11250.00 / 11250.00 / 44472.62. spm_unit_net_income is 21222.62 at both. With ak_child_care_subsidies set to 4,000 in Alaska, household_state_benefits is 0.00 at main and 4000.00 at head.

  • The hand values (computed before reading the cases). I took these from round 1's derivation and the parameters, not from the Vermont source, which I couldn't reach.

    • The subsidy is ($439 − $100) × 52 = $339 × 52 = $17,628.
    • min(17,628, 10,000) = 10,000; min(17,628, 1,000) = 1,000; min(17,628, 20,000) = 17,628.
    • No bill gives 17,628.
    • The child-level bill of 10,000 gives 10,000.
    • Connecticut: min(5,000, 12,000) = 5,000.

    The YAML expects these values and passes.

  • How the bill is read. household_child_care_subsidies and childcare_expenses read the bill the same way: add(spm_unit, period, ["pre_subsidy_childcare_expenses"]). The person formula prorates the SPM input over is_child (age < 18). Runs at head, Vermont 2025, same family:

    SPM bill 10,000                          child_care_subsidies 17628  household_child_care_subsidies 10000  childcare_expenses 0
    SPM bill 10,000 + child-level input 3,000                 17628                                   3000                     0
    SPM bill 10,000, child aged 18                                0                                      0                     0
    unit A: no bill; unit B (same household): SPM bill 10,000
                                             A 17628 / B 0                        A 17628 / B 0          A 0 / B 10000
    
    • An SPM-level bill flows through the person read. A person-level input overrides the SPM input, as it does in childcare_expenses.
    • A unit with a bill and no child under 18 reads a bill of 0, so its whole subsidy counts. Vermont pays nothing for an 18-year-old, so nothing real reaches this case.
    • A subsidy on one SPM unit isn't capped by a bill on another unit in the same household, since each unit's bill is its own. That matches the documentation ("with no bill recorded, the whole subsidy counts").
    • Mutant (f), reading spm_unit_pre_subsidy_childcare_expenses, is killed only by "Vermont's subsidy counts up to a bill set on the child" (1 failed, 14 passed).
  • Other readers of child_care_subsidies:

    • childcare_expenses uses the full payment (intended, and the known Vermont family-share follow-up).
    • ri_liheap_income.py:71 only tests subsidies == 0, so it's unaffected.
    • household_child_care_subsidies.
    • Comments in several state income-source parameters.

    spm_unit_benefits lists co_ccap_subsidy, wa_child_care_subsidies and wv_child_care_subsidies uncapped. It agrees with the household side wherever the cap doesn't bind. It disagrees for Colorado with after-school costs (P1), and nowhere else I found. Vermont isn't in spm_unit_benefits. TAXSIM's generator reads tax_unit_childcare_expenses only. In the API, the new variable appears in metadata and household outputs. Nothing in a tax or benefit formula reads household_state_benefits; state_benefit_cost.py and the WA ECEAP files name it only in comments.

  • Above-bill sweep, 2021–2023. I ran every state in the CCDF list with a $1,000 bill, the family above with 5 days × 8 hours, and meets_ccdf_activity_test set: 153 runs, 0 errors. No subsidy exceeded the bill. Round 1 covered 2024–2026 at $1,000 and 2021–2027 at $12,000. Together these leave Vermont from December 2023 as the only state whose computed payment exceeds the bill, apart from Colorado's after-school case above.

  • The list. All four household_state_benefits blocks (2023, 2024, 2025, 2026) list household_child_care_subsidies once. None lists child_care_subsidies or a *_child_care_subsidies program. test_each_block_counts_only_the_aggregate enforces this.

  • Tests at head, one file per process, on a 4-vCPU Linux VM (local numbers, not CI):

    • test_child_care_subsidies_household_benefits.py: 7 passed in 81.3s (149.8s wall, peak RSS 2.26 GB).
    • test_aggregate_list_integrity.py: 29 passed.
    • household_state_benefits.yaml: 15 passed.
    • vt/dcf/ccfap/vt_ccfap.yaml: 17 passed.
    • vt/dcf/ccfap/integration.yaml: 13 passed.

    These counts match the hand-off's.

  • Mutants, each in its own process with the code restored after each one (git status --short empty every time):

    Mutant Household YAML Python file List integrity
    (b) min_ with no where (hand-off) 5 failed test_a_dollar_of_subsidy_adds_a_dollar failed; the cap property passed (P3) 29 passed
    (f) bill from the SPM input (hand-off) 1 failed, the child-level case 7 passed 29 passed
    (g) no bill counts 0 (mine) 5 failed both property tests failed 29 passed
    (h) CAPPED_AGGREGATES → {"de_ssp"} (mine) 15 passed 5 passed (-k "each_block or latest") 29 passed: survives
    (i) formula reads snap (mine) 10 failed 5 passed (-k) 29 passed
  • The reference is one href (...Understanding-Payments.pdf#page=1) with a # PDF pages 1-2 comment above it and no title. That follows CLAUDE.md's rule for variable tuples. I couldn't open the PDF to confirm the two pages.

  • Changelog: changelog.d/9405.fixed.md is top-level, has a valid type, and carries round 1's wording.

  • Body: it has both the impact: and axiom: lines.

    • The test-cost count (7 tests: 4 block cases, the list test and two properties) matches the file.
    • "The file isn't named in the Makefile" holds: grep test_child_care Makefile finds nothing.
  • Baseline movement. The diff moves outputs at baseline:

    • household_state_benefits, household_benefits, household_net_income and their readers (equiv_household_net_income, household_income_decile, household_net_income_including_health_benefits, marginal_tax_rate) rise in the 28 newly counted states.
    • Vermont's change is now capped.
    • With P1 unfixed, Colorado's change falls wherever after_school_expenses > 0.

    spm_unit_net_income and SPM poverty don't move. The landing pair should report:

    • these by state;
    • Vermont's child_care_subsidies − household_child_care_subsidies;
    • Colorado's household_state_benefits against main (zero change once P1 is fixed);
    • the count of records with after_school_expenses > 0.
  • Overlap. Open PRs touching household_state_benefits: Count Illinois AABD cash in household and SPM benefits #10086, Fix four statute-first benefit bugs: MO TANF rounding, SNAP GU/VI limits, HI CCAP TANF income, SSI food ISM date #10332, Guard county-keyed lookups in VT renter credit, MN CCAP, WI Shares and MD CCS #10116 and Count state child care subsidies in the SPM once #10569. Of these, only Count Illinois AABD cash in household and SPM benefits #10086 changes household_state_benefits.yaml; Fix four statute-first benefit bugs: MO TANF rounding, SNAP GU/VI limits, HI CCAP TANF income, SSI food ISM date #10332 and Guard county-keyed lookups in VT renter credit, MN CCAP, WI Shares and MD CCS #10116 don't change any of this PR's files.

    • Count state child care subsidies in the SPM once #10569: its subsidy_counted_by_household_measure (policyengine_us/tests/test_spm_child_care_subsidy_invariants.py:331-332 at 652e968a94) checks for "child_care_subsidies" or the state's own variable. After this PR, neither is listed, so it returns 0. That's the known issue. One thing beyond the textual fix: that helper should return household_child_care_subsidies, not child_care_subsidies. Its strategy draws Vermont with expenses from 0 to 25,000, where the two differ.
    • Count Illinois AABD cash in household and SPM benefits #10086: the known textual conflict in household_state_benefits.yaml. The new variable adds nothing to it.
  • Scope: no file under .github/ or policyengine_us/tests/policy/baseline/partners/.

  • CI at 9453494314: run 41550947303 is still in progress. Household API Partners, PR validation, Quick Feedback, Package and compatibility, every Baseline shard and every Python shard (including Microsimulation) succeeded. Contrib other-shard-2a succeeded; the other eight Contrib shards were still running when I posted, so this is not a green run yet.

What I couldn't check

  • Sources. This session's network refuses outside.vermont.gov (both the Understanding Payments PDF and the Capped Rates guidance), web.archive.org and ecfr.gov. I couldn't confirm the body's two Vermont quotes, the "Jane Smith" example, or that the PDF has two pages. The hub's ruling and round 1's search excerpt are the only support.
  • Population data. I couldn't check whether the data fills after_school_expenses, so the population size of P1 is unknown. The same goes for whether spm_unit_pre_subsidy_childcare_expenses is post-subsidy spending in the current build (P2). I ran no microsimulation.
  • The remaining hand-off mutants (a), (c), (d) and (e). I reran (b) and (f) and added three of my own.
  • The final CI result for the Contrib shards.

Generated by Claude Code

Colorado's CCAP subsidy pays for pre-subsidy child care and after-school expenses
(co_ccap_subsidy), so a cap at the child care bill alone cut part of it and lowered
household_net_income below main for families with after-school costs. Add a Colorado
YAML case and pin the cap property's zero, equal and one-dollar-over boundaries.

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

Copy link
Copy Markdown
Contributor

Hub impact pair, progress

The base side of the impact pair finished. The head side runs next.

  • base 0d933918a82827b92243c0a43d0d8d03571604fd (the merge base of the head with main)
  • head 8ce831b6dcee3deda178a9a8aeb7fdf118c8e6ac (not run yet)
  • Year 2026, 8 household chunks through calculate_chunked from Add household-chunked microsimulation calculations #10561, on Microsimulation.default_dataset (the local file's SHA-256 matches DEFAULT_DATASET_SHA256)
  • Wall time 9:46 by /usr/bin/time -v (551 s inside the script)
  • Maximum resident set size 3,415,964 kB (3.26 GiB)
  • Both sides drop household_income_decile, because the helper rejects it: "Cannot calculate household_income_decile in household chunks: it aggregates across households. Calculate it on the full dataset."
Variable Base weighted sum, 2026
child_care_subsidies 1,786,291,922.61
household_state_benefits 8,201,643,950.43
equiv_household_net_income 10,834,823,468,521.64
spm_unit_net_income 14,653,599,190,798.56
spm_unit_pre_subsidy_childcare_expenses 83,421,131,008.02
vt_child_care_subsidies 45,948,444.55
ct_child_care_subsidies 405,603,760.80
ma_child_care_subsidies 0.00
ca_child_care_subsidies 0.00
ny_child_care_subsidies 0.00
income_tax 2,595,105,975,379.62
state_income_tax 578,044,599,361.57
household_net_income 17,161,428,039,502.38
household_benefits 2,059,793,926,050.13
Base weighted sums as JSON
{
 "ca_child_care_subsidies": 0.0,
 "child_care_subsidies": 1786291922.607019,
 "ct_child_care_subsidies": 405603760.7995658,
 "equiv_household_net_income": 10834823468521.645,
 "household_benefits": 2059793926050.1262,
 "household_net_income": 17161428039502.375,
 "household_state_benefits": 8201643950.43103,
 "income_tax": 2595105975379.617,
 "ma_child_care_subsidies": 0.0,
 "ny_child_care_subsidies": 0.0,
 "spm_unit_net_income": 14653599190798.559,
 "spm_unit_pre_subsidy_childcare_expenses": 83421131008.018,
 "state_income_tax": 578044599361.5671,
 "vt_child_care_subsidies": 45948444.54842574
}

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor

Hub impact pair, progress

The base side of the impact pair finished. The head side runs next.

  • base 0d933918a82827b92243c0a43d0d8d03571604fd (the merge base of the head with main)
  • head 8ce831b6dcee3deda178a9a8aeb7fdf118c8e6ac (the PR's current head; not run yet)
  • Year 2026, 8 household chunks through calculate_chunked from Add household-chunked microsimulation calculations #10561 (c545c47877c70b367a6fab2322037e453364997e), on Microsimulation.default_dataset. The local file's SHA-256 is 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84, which matches DEFAULT_DATASET_SHA256.
  • Wall time 12:13.07 by /usr/bin/time -v (688 s inside the script), exit status 0
  • Maximum resident set size 3,412,940 kB (3.25 GiB)
  • The side imported policyengine_us from the base worktree, as PYTHONPATH set it.
  • Both sides drop household_income_decile. On the first base attempt, which ran the brief's full list, the helper rejected it: "Cannot calculate household_income_decile in household chunks: it aggregates across households. Calculate it on the full dataset." This run and the head's use the other 14 variables.
  • All 14 base sums equal the base sums in the earlier progress comment exactly (the same float64 values), at the same base SHA.
Variable Base weighted sum, 2026
ca_child_care_subsidies 0.00
child_care_subsidies 1,786,291,922.61
ct_child_care_subsidies 405,603,760.80
equiv_household_net_income 10,834,823,468,521.64
household_benefits 2,059,793,926,050.13
household_net_income 17,161,428,039,502.38
household_state_benefits 8,201,643,950.43
income_tax 2,595,105,975,379.62
ma_child_care_subsidies 0.00
ny_child_care_subsidies 0.00
spm_unit_net_income 14,653,599,190,798.56
spm_unit_pre_subsidy_childcare_expenses 83,421,131,008.02
state_income_tax 578,044,599,361.57
vt_child_care_subsidies 45,948,444.55
Base weighted sums as JSON
{
 "ca_child_care_subsidies": 0.0,
 "child_care_subsidies": 1786291922.607019,
 "ct_child_care_subsidies": 405603760.7995658,
 "equiv_household_net_income": 10834823468521.645,
 "household_benefits": 2059793926050.1262,
 "household_net_income": 17161428039502.375,
 "household_state_benefits": 8201643950.43103,
 "income_tax": 2595105975379.617,
 "ma_child_care_subsidies": 0.0,
 "ny_child_care_subsidies": 0.0,
 "spm_unit_net_income": 14653599190798.559,
 "spm_unit_pre_subsidy_childcare_expenses": 83421131008.018,
 "state_income_tax": 578044599361.5671,
 "vt_child_care_subsidies": 45948444.54842574
}

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor

Ready for hub review
IMPACT PAIR: differs
head 8ce831b
base 0d93391

  • Dataset: hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909 (Microsimulation.default_dataset, the same in both trees). The local file's SHA-256 is 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84, and DEFAULT_DATASET_SHA256 is 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84: they match. The file hashed the same after the head side.
  • Year 2026, 8 household chunks. Both sides ran the same calculate_chunked file from Add household-chunked microsimulation calculations #10561 (c545c47877c70b367a6fab2322037e453364997e), which pair_side.py loads by path.
  • The PR's head is still 8ce831b6dcee3deda178a9a8aeb7fdf118c8e6ac, the commit this run tested.
  • Package check: PYTHONPATH pointed each side at its own worktree, and each side imported that worktree's package (base/policyengine_us and head/policyengine_us). pyproject.toml and uv.lock don't differ between base and head, so both sides ran in the head's environment. Microsimulation accepted dataset_end_year=2026 on both sides.
  • The base sums equal the 2026 base sums in the earlier progress comment exactly, all 14 of them (the same float64 values, at the same base SHA). This session's progress comment gives them too.

Variables that move (records that differ by more than 1e-6): household_state_benefits, equiv_household_net_income, household_net_income, household_benefits.
Variables that don't move: child_care_subsidies, spm_unit_net_income, spm_unit_pre_subsidy_childcare_expenses, vt_child_care_subsidies, ct_child_care_subsidies, ma_child_care_subsidies, ca_child_care_subsidies, ny_child_care_subsidies, income_tax, state_income_tax.

Variable Entity Base sum Head sum Change Rows that differ Weighted rows up Weighted rows down Largest abs. difference
child_care_subsidies spm_unit 1,786,291,922.61 1,786,291,922.61 0 0 0 0 0
household_state_benefits household 8,201,643,950.43 9,036,055,227.82 834,411,277.39 163 207,250.78 8,289.96 20,176.00
equiv_household_net_income household 10,834,823,468,521.64 10,835,263,037,428.23 439,568,906.59 147 207,250.78 1,884.59 9,022.98
spm_unit_net_income spm_unit 14,653,599,190,798.56 14,653,599,190,798.56 0 0 0 0 0
spm_unit_pre_subsidy_childcare_expenses spm_unit 83,421,131,008.02 83,421,131,008.02 0 0 0 0 0
vt_child_care_subsidies spm_unit 45,948,444.55 45,948,444.55 0 0 0 0 0
ct_child_care_subsidies spm_unit 405,603,760.80 405,603,760.80 0 0 0 0 0
ma_child_care_subsidies spm_unit 0 0 0 0 0 0 0
ca_child_care_subsidies spm_unit 0 0 0 0 0 0 0
ny_child_care_subsidies spm_unit 0 0 0 0 0 0 0
income_tax tax_unit 2,595,105,975,379.62 2,595,105,975,379.62 0 0 0 0 0
state_income_tax tax_unit 578,044,599,361.57 578,044,599,361.57 0 0 0 0 0
household_net_income household 17,161,428,039,502.38 17,162,262,450,796.17 834,411,293.80 147 207,250.78 1,884.59 20,176.00
household_benefits household 2,059,793,926,050.13 2,060,628,337,292.83 834,411,242.71 151 207,250.78 1,884.87 20,176.00

Each sum multiplies values by the simulation weights that calculate_chunked returns. "Rows that differ" counts records whose value moves by more than 1e-6, as compare.py counts them; the weighted columns add up those records' weights. Every weighted row down is a sub-cent change (see Sub-cent rows).

What moves. In the 2026 block of gov.household.household_state_benefits, the head replaces the base's 23 per-state *_child_care_subsidies entries (AL, AR, AZ, CA, CO, DC, DE, GA, ID, IN, MD, MO, MT, NC, NE, NV, NY, OH, OK, SC, VA, WA, WV) with one entry, the new household_child_care_subsidies, and keeps the list's other entries in the same order. household_child_care_subsidies takes each SPM unit's child_care_subsidies (the CCDF aggregate, which this PR doesn't change) and counts min_(subsidies, bill) when the unit's bill (the sum of its members' pre_subsidy_childcare_expenses and after_school_expenses) is positive, and the whole subsidy otherwise. So household_state_benefits gains the subsidies of states the base's list left out, up to each bill, and in the listed states it loses any subsidy above a positive bill. household_benefits lists household_state_benefits, household_net_income adds household_benefits, and equiv_household_net_income divides household_net_income by the square root of household_size, so all four move, by a cent or more in the same 144 households. Every one of those households gains; none falls by a cent or more. child_care_subsidies and the five state subsidy variables don't move. Nor does spm_unit_net_income: outside reforms, only the household_benefits list reads household_state_benefits, and only the household_state_benefits list reads household_child_care_subsidies. spm_unit_net_income adds spm_unit_market_income, spm_unit_benefits, acp and ebb and subtracts spm_unit_taxes and spm_unit_spm_expenses, and spm_unit_benefits sums its own list, which this PR doesn't touch. spm_unit_is_in_spm_poverty compares spm_unit_net_income with spm_unit_spm_threshold, so the SPM measures stay put. spm_unit_pre_subsidy_childcare_expenses, income_tax and state_income_tax don't move.

State rows. The 144 households are in the 10 states below, the only states outside the base's list with any child_care_subsidies in this 2026 run. Every dollar was already in child_care_subsidies; the head counts it up to each family's bill. household_benefits and household_net_income change by the same amounts to within float32 rounding (see Sub-cent rows).

State child_care_subsidies Added to household_state_benefits Above the bill (left out) Households that differ Weighted households up
Connecticut 405,603,760.80 405,603,760.80 0.00 21 80,759.41
Michigan 180,234,888.08 180,234,876.95 11.13 16 43,153.99
Utah 67,088,167.97 67,088,167.97 0.00 7 24,244.61
Maine 48,371,147.01 48,371,147.01 0.00 16 12,070.04
New Mexico 41,894,338.04 41,894,338.04 0.00 32 14,654.04
Oregon 35,433,022.92 35,433,019.64 3.28 15 15,415.79
Vermont 45,948,444.55 29,786,190.33 16,162,254.22 17 5,123.96
New Hampshire 16,945,702.42 16,945,702.42 0.00 8 6,299.39
Alaska 6,450,302.04 6,450,302.04 0.00 7 1,413.09
Rhode Island 2,603,773.27 2,603,773.27 0.00 5 4,116.47
Total 850,573,547.10 834,411,278.47 16,162,268.63 144 207,250.78

Rows may not add to the totals because of rounding.

"Above the bill" is each state's child_care_subsidies less what the head adds. Only Vermont's comes from subsidies that exceed the bill by a cent or more (below). Michigan's $11.13 and Oregon's $3.28 are weighted sums of tiny excesses: in a few SPM units the subsidy equals the bill except for float32 rounding and exceeds it by at most $0.00098. The other states' amounts are under half a cent.

The base already listed CO, DE, GA, IN, MD, MT, NC, NE, NV and WA, the other states with child_care_subsidies in this run. No SPM unit there gets more than its positive bill by a cent or more, so the cap moves each household there by less than a cent (at most $0.001953). Weighted, 19 households in CO, DE, IN, NC and WA lose $1.09 in all: $0.92 in Delaware, $0.16 in North Carolina and less than a cent in each of the other three (see Sub-cent rows). Nationally, household_state_benefits rises $834,411,277.39: the head adds $834,411,278.47 in the 10 states and trims $1.09 in the listed ones (unrounded, $834,411,278.4718 less $1.0866).

Vermont's amount above the bill. vt_child_care_subsidies totals $45,948,444.55 in 2026, all of it in Vermont households. The head counts $29,786,190.33 of it in household_state_benefits, so $16,162,254.22 (35.2%) sits above the families' bills and stays out of household benefits. Of Vermont's SPM units, 17 get the subsidy (weighted 5,123.96). Every one has a positive bill, and 12 of them (weighted 1,493.54) get more than their bill. One record carries most of the amount: SPM unit 60751 (weight 1,306.08) gets $10,608.00 against a $1,054.21 bill, $12,477,981.87 weighted, or 77.2% of Vermont's amount above the bill. The next, SPM unit 1119424 (weight 167.89), gets $23,088.00 against $3,985.14, $3,207,184.05 weighted. Computing each unit's excess directly from spm_unit_pre_subsidy_childcare_expenses gives the same $16,162,254.22.

The other nine states' additions match the PR body's pre-cap state table to the $0.01M it shows. Vermont's falls from the $45.95M there, its whole vt_child_care_subsidies, to $29,786,190.33.

Record check. household_child_care_subsidies exists only in the head, so the pair doesn't measure it. Instead, analysis.py predicts each SPM unit's change from the two lists and the cap: the head's count (the subsidy up to spm_unit_pre_subsidy_childcare_expenses when that is positive, else the whole subsidy) less the base's (the whole subsidy in the 23 listed states, nothing elsewhere). The head's bill adds the person-level pre_subsidy_childcare_expenses, which splits the unit's bill evenly across its children, and after_school_expenses, which the population file doesn't store, so in a unit with a child the two bills agree, up to float32 rounding of that split. Each SPM unit's weight equals its household's. When analysis.py sums the prediction by household, it matches the measured change in household_state_benefits in every household to within $0.000244, and the weighted totals differ by $0.0023. State comes from each household's stored state_fips.

Sub-cent rows. In CO, DE, IN, NC and WA, states the base already listed, 19 households lose less than a cent of household_state_benefits (the largest loss is $0.001953); they make up the table's 8,289.96 weighted rows down. In each, an SPM unit's subsidy exceeds its bill by less than a cent, and the head's min_(subsidies, bill) trims exactly that excess: each household's change equals minus its units' excess, and is one or two float32 steps of its stored household_state_benefits (Core stores float variables as float32, in policyengine_core/variables/config.py). The larger float32 totals keep only some of these trims: 7 reach household_benefits, and 3 reach household_net_income and equiv_household_net_income, which is why those variables differ in 151, 147 and 147 households rather than 163. In the 144 households that gain, the change in household_net_income equals the change in household_state_benefits to within $0.015625, one float32 step at that household's base-side net income of $254,813.20 (half a step at its head-side $263,081.19). Rounding like this is why the national changes in household_state_benefits, household_benefits and household_net_income differ by up to $51.09.

Resources (4 vCPU, 16 GB VM, one side at a time; beside each side ran only a log-watching loop and a few short file reads and API calls):

  • Base: 11.5 min (688 s) by pair_side.py's timer, which starts after the imports and covers calculate_chunked and writing the entity pickles; the whole process took 12:13.07 by GNU time; maximum resident set size 3,412,940 KiB (3.25 GiB); exit status 0.
  • Head: 11.2 min (669 s) by pair_side.py's timer, which starts after the imports and covers calculate_chunked and writing the entity pickles; the whole process took 11:50.34 by GNU time; maximum resident set size 3,410,884 KiB (3.25 GiB); exit status 0.

Dropped variables: household_income_decile, on both sides. The first base attempt ran the hub's full list of 15, and the helper rejected it in the first chunk: "Cannot calculate household_income_decile in household chunks: it aggregates across households. Calculate it on the full dataset." (exit status 1 after 3:02.41). Both sides then ran the other 14.

Notes:

  • The first, rejected base attempt downloaded the dataset, so neither full side's wall time includes the download.
  • Neither side's maximum resident set size (3.25 and 3.25 GiB) came near 14 GB, the level at which the hub reruns both sides with 16 chunks, so neither side needed a rerun.
  • The VM had no GNU time, so the run installed it with apt-get. The clone is blobless (--filter=blob:none): it holds every commit, so the merge base is the same, and git fetches file contents as the worktrees need them.

Commands:

# in an empty directory (it ends up holding pe, base, head, out and the scripts)
curl -sI https://huggingface.co/api/datasets/policyengine/populace-us | head -1   # HTTP/1.1 200 Connection Established (the proxy's line)
curl -sS -o /dev/null -w 'HF api status: %{http_code}\n' https://huggingface.co/api/datasets/policyengine/populace-us   # 200
apt-get install -y time || (apt-get update && apt-get install -y time)   # the VM had no GNU time
git clone --filter=blob:none -q https://github.com/PolicyEngine/policyengine-us.git pe && cd pe
git fetch -q origin pull/9503/head:pr-9503 pull/10561/head:pr-10561
HEAD_SHA=$(git rev-parse pr-9503); BASE_SHA=$(git merge-base origin/main $HEAD_SHA); echo $HEAD_SHA $BASE_SHA
# 8ce831b6dcee3deda178a9a8aeb7fdf118c8e6ac 0d933918a82827b92243c0a43d0d8d03571604fd (a rerun should pin these)
git worktree add -q --detach ../head $HEAD_SHA
git worktree add -q --detach ../base $BASE_SHA
git show pr-10561:policyengine_us/tools/calculate_chunked.py > ../calculate_chunked.py   # pr-10561 was c545c47877c70b367a6fab2322037e453364997e
git diff --stat $BASE_SHA $HEAD_SHA -- pyproject.toml uv.lock   # empty
git diff --stat $BASE_SHA $HEAD_SHA   # 6 files, not system.py
grep -n DEFAULT_DATASET ../head/policyengine_us/system.py ../base/policyengine_us/system.py   # the same in both trees
cd ../head && (pip install uv >/dev/null 2>&1; uv sync > ../uv-sync.log 2>&1; echo "uv sync exit: $?" >> ../uv-sync.log)   # uv was already installed; exit 0

# the pair, one side at a time (pair_side.py and compare.py below, saved next to calculate_chunked.py)
cd ../pe; mkdir -p ../out
H=$PWD/../calculate_chunked.py; PY=$PWD/../head/.venv/bin/python
export OMP_NUM_THREADS=4 OPENBLAS_NUM_THREADS=4 MKL_NUM_THREADS=4
# first base attempt: pair_side.py's VARIABLES held the hub's 15 names, the 14 below plus household_income_decile
# after equiv_household_net_income; the helper rejects household_income_decile (exit status 1)
date -u +"start %FT%TZ" > ../out/base.when
PYTHONPATH=$PWD/../base /usr/bin/time -v $PY ../pair_side.py $H 8 2026 ../out/base > ../out/base.log 2> ../out/base.err
echo "exit $?" >> ../out/base.when; date -u +"end %FT%TZ" >> ../out/base.when
(cd ../out && for f in base.log base.err base.when; do mv $f try1-15vars-$f; done)
# then household_income_decile comes out of VARIABLES in pair_side.py, for both sides
F=$($PY -I -c "from huggingface_hub import hf_hub_download; print(hf_hub_download(repo_id='policyengine/populace-us', filename='populace_us_2024.h5', repo_type='dataset', revision='populace-us-2024-spm-20260909', local_files_only=True))")
sha256sum "$F"   # before the base side
echo "$F" > ../out/dataset_path.txt
date -u +"start %FT%TZ" > ../out/base.when
PYTHONPATH=$PWD/../base /usr/bin/time -v $PY ../pair_side.py $H 8 2026 ../out/base > ../out/base.log 2> ../out/base.err
echo "exit $?" >> ../out/base.when; date -u +"end %FT%TZ" >> ../out/base.when
# the earlier progress comment's base sums (fetched while the base side ran), and the check against them
gh api repos/PolicyEngine/policyengine-us/issues/comments/6596150484 --jq '.body' > ../out/prev_progress_6596150484.md
$PY -I -c "import json, re; body = open('../out/prev_progress_6596150484.md').read(); m = re.search(r'\`\`\`json\n(.*?)\n\`\`\`', body, re.S); json.dump(json.loads(m.group(1)), open('../out/prev_base_sums.json', 'w'), indent=1, sort_keys=True)"
$PY -I -c "import json; s = json.load(open('../out/base/summary.json'))['weighted_sums']; p = json.load(open('../out/prev_base_sums.json')); print(set(s) == set(p), all(s[k] == p[k] for k in p))"   # True True
sha256sum "$F" | awk '{print $1}' | tee ../out/dataset.sha256   # between the sides
date -u +"start %FT%TZ" > ../out/head.when
PYTHONPATH=$PWD/../head /usr/bin/time -v $PY ../pair_side.py $H 8 2026 ../out/head > ../out/head.log 2> ../out/head.err
echo "exit $?" >> ../out/head.when; date -u +"end %FT%TZ" >> ../out/head.when
sha256sum "$F" | awk '{print $1}' | tee ../out/dataset.sha256.after   # after the head side
$PY ../compare.py ../out/base ../out/head ../out/compare.json

# state rows, amounts above the bill and the record check (analysis.py below: it reads the pickles,
# the dataset's stored state_fips and entity links, and both trees' household_state_benefits parameter files)
$PY -I ../analysis.py ../out/base ../out/head "$F" ../base ../head ../out/analysis.json > ../out/analysis.stdout
Scripts: pair_side.py, compare.py, analysis.py

pair_side.py:

"""One side of the pair: python pair_side.py <helper.py> <chunks> <year> <out_dir>."""
import importlib.util
import json
import sys
import time
from pathlib import Path

helper, chunks, year, out = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), Path(sys.argv[4])
VARIABLES = (
    "child_care_subsidies",
    "household_state_benefits",
    "equiv_household_net_income",
    "spm_unit_net_income",
    "spm_unit_pre_subsidy_childcare_expenses",
    "vt_child_care_subsidies",
    "ct_child_care_subsidies",
    "ma_child_care_subsidies",
    "ca_child_care_subsidies",
    "ny_child_care_subsidies",
    "income_tax",
    "state_income_tax",
    "household_net_income",
    "household_benefits",
)
spec = importlib.util.spec_from_file_location("hub_calculate_chunked", helper)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

import policyengine_us  # noqa: E402
from policyengine_us import Microsimulation  # noqa: E402

start = time.monotonic()
result = module.calculate_chunked(
    lambda dataset: Microsimulation(dataset=dataset, dataset_end_year=year),
    VARIABLES,
    year,
    chunks,
    dataset=Microsimulation.default_dataset,
)
out.mkdir(parents=True, exist_ok=True)
for entity, table in result.entities.items():
    table.to_pickle(out / f"{entity}.pkl")
summary = {
    "package": str(Path(policyengine_us.__file__).parent),
    "chunks": chunks,
    "year": year,
    "weighted_sums": result.weighted_sums,
    "wall_seconds": time.monotonic() - start,
}
(out / "summary.json").write_text(json.dumps(summary, indent=1, sort_keys=True))
print(json.dumps(summary, sort_keys=True), flush=True)

compare.py:

"""Compare the two sides: python compare.py <base_dir> <head_dir> <out.json>."""
import json
import sys
from pathlib import Path

import numpy as np
import pandas as pd

base, head, out = Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3])
compare = {}
for base_path in sorted(base.glob("*.pkl")):
    entity = base_path.stem
    left = pd.read_pickle(base_path)
    right = pd.read_pickle(head / base_path.name)
    key = f"{entity}_id"
    if not np.array_equal(left[key].to_numpy(), right[key].to_numpy()):
        sys.exit(f"{entity}: the two sides hold different IDs or a different order")
    if not np.array_equal(left["weight"].to_numpy(), right["weight"].to_numpy()):
        sys.exit(f"{entity}: the two sides hold different weights")
    if set(left.columns) != set(right.columns):
        sys.exit(f"{entity}: the two sides hold different columns")
    ids = left[key].to_numpy()
    weight = left["weight"].to_numpy(dtype=float)
    for name in left.columns.drop([key, "weight"]):
        a = left[name].to_numpy(dtype=float)
        b = right[name].to_numpy(dtype=float)
        if np.isnan(a).any() or np.isnan(b).any():
            sys.exit(f"{name}: a side holds NaN")
        difference = b - a
        moved = np.abs(difference) > 1e-6
        largest = np.argsort(-np.abs(difference))[: min(5, int(moved.sum()))]
        compare[name] = {
            "entity": entity,
            "base_sum": float((a * weight).sum()),
            "head_sum": float((b * weight).sum()),
            "change": float((b * weight).sum() - (a * weight).sum()),
            "records_changed": int(moved.sum()),
            "weighted_changed": float(weight[moved].sum()),
            "weighted_up": float(weight[difference > 1e-6].sum()),
            "weighted_down": float(weight[difference < -1e-6].sum()),
            "largest_abs": float(np.abs(difference).max()),
            "rows": [
                {"id": int(ids[i]), "base": float(a[i]), "head": float(b[i])}
                for i in largest
            ],
        }
out.write_text(json.dumps(compare, indent=1))
print(json.dumps({k: v["records_changed"] for k, v in compare.items()}, indent=1))

analysis.py:

"""State rows and amounts above the bill from the pair's per-record outputs.

python analysis.py <base_dir> <head_dir> <dataset.h5> <base_tree> <head_tree> <out.json>

Reads only the two sides' pickles, the dataset's stored household state and
entity links, and the household_state_benefits parameter files of both trees.
"""
import json
import sys
from pathlib import Path

import numpy as np
import pandas as pd
import yaml

base, head, dataset_path, base_tree, head_tree, out = (
    Path(sys.argv[1]),
    Path(sys.argv[2]),
    sys.argv[3],
    Path(sys.argv[4]),
    Path(sys.argv[5]),
    Path(sys.argv[6]),
)
YEAR_BLOCK = "2026-01-01"
TOL = 1e-6  # compare.py's threshold
CENT = 0.01
FIPS = {
    1: "AL", 2: "AK", 4: "AZ", 5: "AR", 6: "CA", 8: "CO", 9: "CT", 10: "DE",
    11: "DC", 12: "FL", 13: "GA", 15: "HI", 16: "ID", 17: "IL", 18: "IN",
    19: "IA", 20: "KS", 21: "KY", 22: "LA", 23: "ME", 24: "MD", 25: "MA",
    26: "MI", 27: "MN", 28: "MS", 29: "MO", 30: "MT", 31: "NE", 32: "NV",
    33: "NH", 34: "NJ", 35: "NM", 36: "NY", 37: "NC", 38: "ND", 39: "OH",
    40: "OK", 41: "OR", 42: "PA", 44: "RI", 45: "SC", 46: "SD", 47: "TN",
    48: "TX", 49: "UT", 50: "VT", 51: "VA", 53: "WA", 54: "WV", 55: "WI",
    56: "WY",
}


def load(side, entity):
    return pd.read_pickle(side / f"{entity}.pkl")


def listed_block(tree):
    path = tree / "policyengine_us/parameters/gov/household/household_state_benefits.yaml"
    values = yaml.safe_load(path.read_text())["values"]
    return list({str(k): v for k, v in values.items()}[YEAR_BLOCK])


base_list = listed_block(base_tree)
head_list = listed_block(head_tree)
base_ccs_states = sorted(
    name[:2].upper() for name in base_list if name.endswith("_child_care_subsidies")
)
head_ccs_items = [
    name
    for name in head_list
    if name.endswith("_child_care_subsidies") or name == "household_child_care_subsidies"
]
other_base = [n for n in base_list if not n.endswith("_child_care_subsidies")]
other_head = [n for n in head_list if n != "household_child_care_subsidies"]
list_check = {
    "base_child_care_entries": len(base_ccs_states),
    "head_child_care_entries": head_ccs_items,
    "other_entries_identical": other_base == other_head,
}

# The dataset's stored state and entity links (inputs, the same on both sides).
with pd.HDFStore(dataset_path, mode="r") as store:
    keys = store.keys()

    def table(entity):
        if f"/{entity}" in keys:
            return store[f"/{entity}"]
        return store[sorted(k for k in keys if k.startswith(f"/{entity}/"))[0]]

    household_table = table("household")
    person_table = table("person")
household_state = pd.Series(
    household_table["state_fips"].to_numpy().astype(int),
    index=household_table["household_id"].to_numpy(),
)
spm_household = (
    pd.DataFrame(
        {
            "spm": person_table["person_spm_unit_id"].to_numpy(),
            "household": person_table["person_household_id"].to_numpy(),
        }
    )
    .groupby("spm", sort=False)
    .household.first()
)

hb, hh = load(base, "household"), load(head, "household")
sb, sh = load(base, "spm_unit"), load(head, "spm_unit")
for left, right, key in ((hb, hh, "household_id"), (sb, sh, "spm_unit_id")):
    assert np.array_equal(left[key].to_numpy(), right[key].to_numpy())
    assert np.array_equal(left["weight"].to_numpy(), right["weight"].to_numpy())

household_ids = hb["household_id"].to_numpy()
hw = hb["weight"].to_numpy(dtype=float)
sw = sb["weight"].to_numpy(dtype=float)
h_state = household_state.reindex(household_ids).to_numpy()
s_household = spm_household.reindex(sb["spm_unit_id"].to_numpy()).to_numpy()
s_state = household_state.reindex(s_household).to_numpy()
assert not pd.isna(h_state).any() and not pd.isna(s_state).any()
h_code = np.array([FIPS[int(f)] for f in h_state])
s_code = np.array([FIPS[int(f)] for f in s_state])
household_position = pd.Series(np.arange(len(household_ids)), index=household_ids)
s_position = household_position.reindex(s_household).to_numpy()

# Each SPM unit's weight equals its household's weight?
weights_match = bool(np.array_equal(sw, hw[s_position]))

delta = {
    name: hh[name].to_numpy(dtype=float) - hb[name].to_numpy(dtype=float)
    for name in hb.columns.drop(["household_id", "weight"])
}
d_hsb = delta["household_state_benefits"]
consistency = {
    "max_abs(d_household_benefits - d_household_state_benefits)": float(
        np.abs(delta["household_benefits"] - d_hsb).max()
    ),
    "max_abs(d_household_net_income - d_household_benefits)": float(
        np.abs(delta["household_net_income"] - delta["household_benefits"]).max()
    ),
    "households_where_net_income_moves_without_state_benefits": int(
        ((np.abs(delta["household_net_income"]) > TOL) & (np.abs(d_hsb) <= TOL)).sum()
    ),
    "households_where_state_benefits_move_without_net_income": int(
        ((np.abs(d_hsb) > TOL) & (np.abs(delta["household_net_income"]) <= TOL)).sum()
    ),
}
spm_records_changed = {
    name: int((np.abs(sh[name].to_numpy(dtype=float) - sb[name].to_numpy(dtype=float)) > TOL).sum())
    for name in sb.columns.drop(["spm_unit_id", "weight"])
}

ccs = sb["child_care_subsidies"].to_numpy(dtype=float)
bill = sb["spm_unit_pre_subsidy_childcare_expenses"].to_numpy(dtype=float)
listed = np.isin(s_code, base_ccs_states)
# The head counts min(subsidy, bill) when the bill is positive, else the whole
# subsidy; the base counted the whole subsidy in listed states, none elsewhere.
# The head's bill is the person-level sum (the unit's bill split across its
# children, plus after-school expenses), which equals this SPM-unit bill in a
# unit with children and no after-school expenses.
counted_head = np.where(bill > 0, np.minimum(ccs, bill), ccs)
counted_base = np.where(listed, ccs, 0.0)
expected_spm = counted_head - counted_base
expected_household = np.bincount(s_position, weights=expected_spm, minlength=len(household_ids))
deviation = d_hsb - expected_household
mechanism = {
    "max_abs(actual - expected) per household": float(np.abs(deviation).max()),
    "households_off_by_a_cent_or_more": int((np.abs(deviation) >= CENT).sum()),
    "weighted_sum(actual - expected)": float((deviation * hw).sum()),
}

rows = []
for code in sorted(set(h_code.tolist())):
    hm = h_code == code
    sm = s_code == code
    d = d_hsb[hm]
    full = float((ccs[sm] * sw[sm]).sum())
    change = float((d * hw[hm]).sum())
    was_listed = code in base_ccs_states
    above_spm = np.where(bill[sm] > 0, np.maximum(ccs[sm] - bill[sm], 0), 0)
    rows.append(
        {
            "state": code,
            "base_listed_2026": was_listed,
            "child_care_subsidies": full,
            "change_household_state_benefits": change,
            "change_household_benefits": float((delta["household_benefits"][hm] * hw[hm]).sum()),
            "change_household_net_income": float((delta["household_net_income"][hm] * hw[hm]).sum()),
            "households_changed": int((np.abs(d) > TOL).sum()),
            "households_changed_cent_or_more": int((np.abs(d) >= CENT).sum()),
            "households_changed_sub_cent": int(((np.abs(d) > TOL) & (np.abs(d) < CENT)).sum()),
            "largest_sub_cent": float(np.abs(d[(np.abs(d) > TOL) & (np.abs(d) < CENT)]).max()) if ((np.abs(d) > TOL) & (np.abs(d) < CENT)).any() else 0.0,
            "weighted_households_up": float(hw[hm][d > TOL].sum()),
            "weighted_households_down": float(hw[hm][d < -TOL].sum()),
            # Previously listed: the change is minus the amount above the bill.
            # Previously omitted: the change is the subsidy up to the bill.
            "amount_above_bill": -change if was_listed else full - change,
            "amount_above_bill_from_spm_bill": float((above_spm * sw[sm]).sum()),
            "spm_units_with_subsidy": int((ccs[sm] > 0).sum()),
            "weighted_spm_units_with_subsidy": float(sw[sm][ccs[sm] > 0].sum()),
            "spm_units_subsidy_above_positive_bill": int(((bill[sm] > 0) & (ccs[sm] - bill[sm] >= CENT)).sum()),
            "weighted_spm_units_subsidy_above_positive_bill": float(sw[sm][(bill[sm] > 0) & (ccs[sm] - bill[sm] >= CENT)].sum()),
            "spm_units_subsidy_no_bill": int(((ccs[sm] > 0) & (bill[sm] <= 0)).sum()),
            "weighted_spm_units_subsidy_no_bill": float(sw[sm][(ccs[sm] > 0) & (bill[sm] <= 0)].sum()),
        }
    )

for row in rows:
    sm = s_code == row["state"]
    excess = np.where(bill[sm] > 0, np.maximum(ccs[sm] - bill[sm], 0), 0)
    row["spm_units_any_excess"] = int((excess > 0).sum())
    row["largest_spm_unit_excess"] = float(excess.max()) if sm.any() else 0.0
    row["spm_units_subsidy_equals_bill_within_cent"] = int(((ccs[sm] > 0) & (np.abs(ccs[sm] - bill[sm]) < CENT)).sum())

# Size of each household variable's changes, against float32 spacing.
by_size = {}
for name, d in delta.items():
    stored_base = hb[name].to_numpy()
    stored_head = hh[name].to_numpy()
    cent = np.abs(d) >= CENT
    sub = (np.abs(d) > TOL) & ~cent
    spacing = np.spacing(np.maximum(np.abs(stored_base), np.abs(stored_head)).astype(np.float32)).astype(float)
    steps = np.abs(d[sub]) / spacing[sub]
    by_size[name] = {
        "stored_dtype": str(stored_base.dtype),
        "households_cent_or_more": int(cent.sum()),
        "households_cent_or_more_down": int((d[cent] < 0).sum()),
        "weighted_change_cent_or_more": float((d[cent] * hw[cent]).sum()),
        "households_sub_cent": int(sub.sum()),
        "sub_cent_states": sorted(set(h_code[sub].tolist())),
        "largest_sub_cent": float(np.abs(d[sub]).max()) if sub.any() else 0.0,
        "sub_cent_float32_steps": sorted(set(np.round(steps, 6).tolist())),
        "weighted_change_sub_cent": float((d[sub] * hw[sub]).sum()),
        "weighted_households_sub_cent": float(hw[sub].sum()),
    }
real = np.abs(d_hsb) >= CENT
gap = np.abs(delta["household_net_income"][real] - d_hsb[real])
at = np.argmax(gap)
net_income_at = float(hb["household_net_income"].to_numpy()[real][at])
net_income_head_at = float(hh["household_net_income"].to_numpy()[real][at])
by_size["net_income_vs_state_benefits_where_cent_or_more"] = {
    "households": int(real.sum()),
    "max_abs_gap": float(gap.max()),
    "household_id_at_max_gap": int(household_ids[real][at]),
    "net_income_at_max_gap": net_income_at,
    "float32_spacing_there": float(np.spacing(np.float32(net_income_at))),
    "head_net_income_at_max_gap": net_income_head_at,
    "head_float32_spacing_there": float(np.spacing(np.float32(net_income_head_at))),
}

vt = next(r for r in rows if r["state"] == "VT")
vt_mask = s_code == "VT"
vt_records = pd.DataFrame(
    {
        "spm_unit_id": sb["spm_unit_id"].to_numpy()[vt_mask & (ccs > 0)],
        "subsidy": ccs[vt_mask & (ccs > 0)],
        "bill": bill[vt_mask & (ccs > 0)],
        "weight": sw[vt_mask & (ccs > 0)],
    }
)
vt_records["above_bill"] = np.where(
    vt_records.bill > 0, np.maximum(vt_records.subsidy - vt_records.bill, 0), 0
)
vt_records["weighted_above_bill"] = vt_records.above_bill * vt_records.weight
vt_records = vt_records.sort_values("weighted_above_bill", ascending=False)
vt_full = float((sb["vt_child_care_subsidies"].to_numpy(dtype=float)[vt_mask] * sw[vt_mask]).sum())
vt_elsewhere = float(np.abs(sb["vt_child_care_subsidies"].to_numpy(dtype=float)[~vt_mask]).sum())
result = {
    "year_block": YEAR_BLOCK,
    "base_listed_child_care_states_2026": base_ccs_states,
    "list_check": list_check,
    "spm_weight_equals_household_weight": weights_match,
    "consistency": consistency,
    "mechanism_check": mechanism,
    "spm_unit_records_changed": spm_records_changed,
    "national": {
        "child_care_subsidies": float((ccs * sw).sum()),
        "change_household_state_benefits": float((d_hsb * hw).sum()),
        "amount_above_bill": float(sum(r["amount_above_bill"] for r in rows)),
        "amount_above_bill_from_spm_bill": float(sum(r["amount_above_bill_from_spm_bill"] for r in rows)),
    },
    "vermont": {
        "vt_child_care_subsidies": vt_full,
        "vt_child_care_subsidies_outside_vermont_abs": vt_elsewhere,
        "child_care_subsidies_in_vermont": vt["child_care_subsidies"],
        "added_to_household_benefits": vt["change_household_state_benefits"],
        "amount_above_bill": vt_full - vt["change_household_state_benefits"],
        "amount_above_bill_from_spm_bill": vt["amount_above_bill_from_spm_bill"],
        "spm_units_with_subsidy": vt["spm_units_with_subsidy"],
        "weighted_spm_units_with_subsidy": vt["weighted_spm_units_with_subsidy"],
        "spm_units_subsidy_above_positive_bill": vt["spm_units_subsidy_above_positive_bill"],
        "weighted_spm_units_subsidy_above_positive_bill": vt["weighted_spm_units_subsidy_above_positive_bill"],
        "spm_units_subsidy_no_bill": vt["spm_units_subsidy_no_bill"],
        "weighted_spm_units_subsidy_no_bill": vt["weighted_spm_units_subsidy_no_bill"],
        "records": vt_records.to_dict(orient="records"),
    },
    "by_size": by_size,
    "states": rows,
}
out.write_text(json.dumps(result, indent=1))
moving = [r for r in rows if r["households_changed"]]
print(
    json.dumps(
        {
            "list_check": list_check,
            "weights_match": weights_match,
            "consistency": consistency,
            "mechanism_check": mechanism,
            "vermont": result["vermont"],
            "moving_states": [
                (r["state"], round(r["change_household_state_benefits"], 2), r["households_changed_cent_or_more"], r["households_changed_sub_cent"])
                for r in moving
            ],
        },
        indent=1,
    )
)
compare.json
{
 "household_state_benefits": {
  "entity": "household",
  "base_sum": 8201643950.43103,
  "head_sum": 9036055227.81625,
  "change": 834411277.3852196,
  "records_changed": 163,
  "weighted_changed": 215540.7354988549,
  "weighted_up": 207250.77999245364,
  "weighted_down": 8289.955506401253,
  "largest_abs": 20176.0,
  "rows": [
   {
    "id": 1001496,
    "base": 0.0,
    "head": 20176.0
   },
   {
    "id": 56848,
    "base": 0.0,
    "head": 18975.783203125
   },
   {
    "id": 1115989,
    "base": 0.0,
    "head": 16737.326171875
   },
   {
    "id": 1664,
    "base": 0.0,
    "head": 15288.0
   },
   {
    "id": 3590,
    "base": 2368.199951171875,
    "head": 17437.783203125
   }
  ]
 },
 "equiv_household_net_income": {
  "entity": "household",
  "base_sum": 10834823468521.645,
  "head_sum": 10835263037428.23,
  "change": 439568906.5859375,
  "records_changed": 147,
  "weighted_changed": 209135.37153056182,
  "weighted_up": 207250.77999245364,
  "weighted_down": 1884.5915381081868,
  "largest_abs": 9022.98046875,
  "rows": [
   {
    "id": 1001496,
    "base": 65052.94140625,
    "head": 74075.921875
   },
   {
    "id": 723,
    "base": 22973.876953125,
    "head": 31223.115234375
   },
   {
    "id": 1664,
    "base": 42014.109375,
    "head": 49658.109375
   },
   {
    "id": 1110560,
    "base": 36983.69140625,
    "head": 44401.98828125
   },
   {
    "id": 56848,
    "base": 59436.4765625,
    "head": 66608.6484375
   }
  ]
 },
 "household_net_income": {
  "entity": "household",
  "base_sum": 17161428039502.375,
  "head_sum": 17162262450796.174,
  "change": 834411293.7988281,
  "records_changed": 147,
  "weighted_changed": 209135.37153056182,
  "weighted_up": 207250.77999245364,
  "weighted_down": 1884.5915381081868,
  "largest_abs": 20176.0,
  "rows": [
   {
    "id": 1001496,
    "base": 145462.796875,
    "head": 165638.796875
   },
   {
    "id": 56848,
    "base": 157254.140625,
    "head": 176229.921875
   },
   {
    "id": 1115989,
    "base": 102615.234375,
    "head": 119352.5625
   },
   {
    "id": 1664,
    "base": 84028.21875,
    "head": 99316.21875
   },
   {
    "id": 3590,
    "base": 64153.34765625,
    "head": 79222.9296875
   }
  ]
 },
 "household_benefits": {
  "entity": "household",
  "base_sum": 2059793926050.1262,
  "head_sum": 2060628337292.8345,
  "change": 834411242.708252,
  "records_changed": 151,
  "weighted_changed": 209135.64504279534,
  "weighted_up": 207250.77999245364,
  "weighted_down": 1884.8650503417011,
  "largest_abs": 20175.999267578125,
  "rows": [
   {
    "id": 1001496,
    "base": 2239.199951171875,
    "head": 22415.19921875
   },
   {
    "id": 56848,
    "base": 5743.4521484375,
    "head": 24719.234375
   },
   {
    "id": 1115989,
    "base": 7712.703125,
    "head": 24450.029296875
   },
   {
    "id": 1664,
    "base": 1843.3277587890625,
    "head": 17131.328125
   },
   {
    "id": 3590,
    "base": 14287.232421875,
    "head": 29356.81640625
   }
  ]
 },
 "child_care_subsidies": {
  "entity": "spm_unit",
  "base_sum": 1786291922.607019,
  "head_sum": 1786291922.607019,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "spm_unit_net_income": {
  "entity": "spm_unit",
  "base_sum": 14653599190798.559,
  "head_sum": 14653599190798.559,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "spm_unit_pre_subsidy_childcare_expenses": {
  "entity": "spm_unit",
  "base_sum": 83421131008.018,
  "head_sum": 83421131008.018,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "vt_child_care_subsidies": {
  "entity": "spm_unit",
  "base_sum": 45948444.54842574,
  "head_sum": 45948444.54842574,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "ct_child_care_subsidies": {
  "entity": "spm_unit",
  "base_sum": 405603760.7995658,
  "head_sum": 405603760.7995658,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "ma_child_care_subsidies": {
  "entity": "spm_unit",
  "base_sum": 0.0,
  "head_sum": 0.0,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "ca_child_care_subsidies": {
  "entity": "spm_unit",
  "base_sum": 0.0,
  "head_sum": 0.0,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "ny_child_care_subsidies": {
  "entity": "spm_unit",
  "base_sum": 0.0,
  "head_sum": 0.0,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "income_tax": {
  "entity": "tax_unit",
  "base_sum": 2595105975379.617,
  "head_sum": 2595105975379.617,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "state_income_tax": {
  "entity": "tax_unit",
  "base_sum": 578044599361.5671,
  "head_sum": 578044599361.5671,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 }
}

Generated by Claude Code

@MaxGhenis

MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Hub review in progress

Reviewing head 8ce831b6dcee3deda178a9a8aeb7fdf118c8e6ac, round 3 (delta 9453494314..8ce831b6dc). These findings are interim. So far I have no P1 or P2.

  • Round 2's P1 is fixed. The head's household_state_benefits.yaml gives 16 passed at head and 1 failed, 15 passed against 9453494314's code. The failure is the new Colorado case: household_child_care_subsidies@2025: [6000.] differs from 12000.0.
  • Colorado witness. With round 2's 2025 family, every household_state_benefits and household_net_income value at this head equals main's, both at the merge base 0d933918a8 and at current main db83eb5ab6. Round 2's table reproduces to the cent when the child's childcare_hours_per_week is set to 40 directly (parent fee 0).
  • Round 2's boundary P3 is fixed. Mutant (b), min_ with no where, now fails the cap property through the explicit bill=0 example (2 failed, 5 passed).
  • P3 (firming up): the cap property never sets after-school costs. Mutant (j) drops after_school_expenses from the bill, which is 9453494314's formula. It passes the Python file (7 passed), and only the new Colorado YAML case kills it.
  • P3 (firming up): Vermont's after-school boundary. In Vermont 2025, a family with no child care bill and $1,000 of after-school costs on the child gets 17,628 at 9453494314 and 1,000 at head, because bill > 0 now holds. With a $1,000 child care bill plus $6,000 of after-school costs, it gets 1,000 at 9453494314 and 7,000 at head. Main counts 0 in both, so neither falls below main. No test pins this.
  • Below main? I swept 51 states × 2023–2026 × a child aged 3 or 7, with no bill or a $1,000 bill, plus $1,000 of after-school costs. No household's household_state_benefits falls below main's.
  • Tests at head. vt/dcf/ccfap/integration.yaml 13 passed, vt_ccfap.yaml 17 passed, test_aggregate_list_integrity.py 29 passed, and every Colorado CCAP file so far passed. CI run 41577574038 concluded success, 34 of 34 jobs.
  • Sources. The egress proxy refuses (HTTP 403 on CONNECT) the Vermont, Colorado, govinfo, eCFR, uscode.house.gov, archive.org and Cornell hosts.
  • Still running: the rest of the mutants, and the head's Python test file.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor

Ready for hub review
VERDICT: APPROVE
8ce831b
METHODOLOGY_CHOICE: yes

This is round 3, a delta review of 9453494314..8ce831b6dc. The PR's head is still 8ce831b6dc. Its one new commit changes four files (git diff --stat 9453494314 8ce831b6dc):

  • the cap's bill in household_child_care_subsidies now adds after_school_expenses;
  • the variable's documentation, the test module's docstring and the changelog say "child care and after-school";
  • the cap property gains round 2's three @examples;
  • household_state_benefits.yaml (tests) gains a Colorado case.

Since round 2 the body has also gained the hub's landing pair, round 2's P2 sentence and round 2's axiom wording.

Round 2's P1 is fixed: every Colorado value I ran equals main's, and the new YAML case fails at 9453494314. Round 2's P2 and its boundary P3 are resolved. I found no P1 or P2. The three P3s below don't block landing. Two are about the after-school extension (a test gap and an unpinned boundary), and one is about stale test-cost text in the body.

Findings

P3. The cap property never sets after-school costs, so one YAML case is the only guard on this round's fix

Where: policyengine_us/tests/test_child_care_subsidies_household_benefits.py:97-113 (counted_subsidy) and :116-143.

The module docstring's invariant 3 now says the cap uses "the family's pre-subsidy bill for child care and after-school care". But counted_subsidy sets only spm_unit_pre_subsidy_childcare_expenses, so no generated or explicit case carries an after-school cost. Two of my mutants show the gap (full table under "Checked and sound"):

  • (j) Drop after_school_expenses from the bill, which is 9453494314's formula. The Python file passes (7 passed). Only the YAML case "Colorado's subsidy for child care and after-school care counts in full" kills it (1 failed, 15 passed).
  • (k) Switch on the child care bill alone: where(care > 0, min_(subsidies, care + after_school), subsidies). Every file passes (16 passed, 7 passed, 29 passed).

Fix. Draw how much of the bill is after-school care. This adds no simulations:

-def counted_subsidy(state, year, program, amount, bill):
+def counted_subsidy(state, year, program, amount, bill, after_school=0):
     members = ["parent", "child"]
     spm_unit = {
         "members": members,
         program: {year: amount},
-        "spm_unit_pre_subsidy_childcare_expenses": {year: bill},
+        "spm_unit_pre_subsidy_childcare_expenses": {year: bill - after_school},
     }
+    child = {"age": {year: 3}, "after_school_expenses": {year: after_school}}
     situation = {
-        "people": {"parent": {"age": {year: 30}}, "child": {"age": {year: 3}}},
+        "people": {"parent": {"age": {year: 30}}, "child": child},
@@
-@example(year=2025, index=0, amount=5_000, bill=0, extra=1)
-@example(year=2025, index=0, amount=5_000, bill=5_000, extra=1)
-@example(year=2025, index=0, amount=5_001, bill=5_000, extra=1)
+@example(year=2025, index=0, amount=5_000, bill=0, extra=1, after_school_share=0)
+@example(year=2025, index=0, amount=5_000, bill=5_000, extra=1, after_school_share=0)
+@example(year=2025, index=0, amount=5_001, bill=5_000, extra=1, after_school_share=0)
+# Half the bill is after-school care, and then all of it.
+@example(year=2025, index=0, amount=5_000, bill=5_000, extra=1, after_school_share=50)
+@example(year=2025, index=0, amount=5_001, bill=5_000, extra=1, after_school_share=100)
 @given(
@@
     extra=st.integers(min_value=1, max_value=30_000),
+    after_school_share=st.integers(min_value=0, max_value=100),
 )
 def test_household_benefits_count_the_subsidy_up_to_the_bill(
-    year, index, amount, bill, extra
+    year, index, amount, bill, extra, after_school_share
 ):
     programs = programs_in(year)
     program = programs[index % len(programs)]
     state = program[:2].upper()
-    counted = counted_subsidy(state, year, program, amount, bill)
+    after_school = bill * after_school_share // 100
+    counted = counted_subsidy(state, year, program, amount, bill, after_school)
@@
     if bill > 0:
-        bigger = counted_subsidy(state, year, program, amount, bill + extra)
+        bigger = counted_subsidy(
+            state, year, program, amount, bill + extra, after_school
+        )
         assert bigger >= counted - TOLERANCE

I applied this on a scratch copy and ran -k up_to_the_bill in one process per tree:

  • At head it passes (1 passed, 6 deselected).
  • Under mutant (j) it fails on both new examples. At the 50% split it counts 2,500 against an expected 5,000; at the 100% split it gives assert 5001.0 <= (5000 + 0.02).
  • Under mutant (k) it fails on the 100% example: assert 5001.0 <= (5000 + 0.02).

ruff format --check and ruff check pass on the patched file. The last @example pins the boundary in the next finding, so drop it if the hub picks the other reading there.

P3. After-school costs now open or raise the cap for a subsidy that doesn't pay for them, and no test pins which reading holds

Where: policyengine_us/variables/gov/hhs/ccdf/household_child_care_subsidies.py:24-29.

The bill adds after_school_expenses in every state, but only Colorado's subsidy pays for after-school care. grep -rn after_school_expenses policyengine_us/variables finds only co_ccap_subsidy.py:16, the input and this file. vt_ccfap.py pays the state rate for each eligible child in care, meaning care hours or a child care bill, and from 2023-12-17 it pays regardless of the bill. It never reads after_school_expenses, so Vermont's payment now meets an after-school bill it doesn't pay. Take Vermont 2025 and the YAML cases' family: a parent with $52,875 of earnings, a child aged 3 at a licensed center 5 days a week for 8 hours a day, and the activity test met. vt_child_care_subsidies is 17,628 in every run.

Inputs main 0d933918a8 round 2 9453494314 head
Child care bill 1,000, after-school 6,000 on the child 0 1,000 7,000
No child care bill, after-school 1,000 on the child 0 17,628 1,000

Values are household_state_benefits; at round 2 and at head, household_child_care_subsidies equals it.

  • Row 1. The cap counts $6,000 of Vermont's payment against after-school care that Vermont's formula doesn't pay for.
  • Row 2. bill > 0 now holds, so the "no bill recorded" branch no longer applies, and Vermont's count drops from round 2's 17,628 to 1,000.

Neither row falls below main, which leaves Vermont out. The documentation states the head's reading ("up to the family's pre-subsidy bill for child care and after-school care"), so the code matches its docs. My sweeps found Vermont the only state where this binds. The pair says the population file doesn't store after_school_expenses, so only household calculations reach it.

This is the hub's call:

  1. Keep the head's reading (a recorded after-school bill is part of the family's bill in any state) and pin it. This case passes at head (1 passed). Under mutants (j) and (k) it fails, because they count 17,628:
    # After-school costs count toward the bill in every state, so with no child care
    # bill recorded they still cap Vermont's payment.
    - name: Vermont's subsidy counts up to an after-school bill when no child care bill is recorded
      period: 2025
      absolute_error_margin: 0.01
      input:
        people:
          parent:
            age: 30
            employment_income: 52_875
          child:
            age: 3
            is_tax_unit_dependent: true
            vt_ccfap_provider_type: LICENSED_CENTER
            childcare_days_per_week: 5
            childcare_hours_per_day: 8
            after_school_expenses: 1_000
        spm_units:
          spm_unit:
            members: [parent, child]
            meets_ccdf_activity_test: true
        households:
          household:
            members: [parent, child]
            state_code: VT
      output:
        child_care_subsidies: 17_628
        household_child_care_subsidies: 1_000
        household_state_benefits: 1_000
  2. Add after-school costs only where a subsidy pays them. Today that is Colorado alone, so the variable would need a per-program cap.

I'd take option 1. It is the documented rule, it never counts less than main, and option 2 puts state-specific logic in a federal variable for a case the population data doesn't carry. Mutant (k) switches on the child care bill alone. That is a halfway reading I wouldn't pick: it brings back round 2's 17,628 in row 2 but leaves row 1 at 7,000.

P3. The body's test-cost paragraph predates the three @examples

Where: the body, the ## Impact paragraph that starts "test_child_care_subsidies_household_benefits.py has 7 tests".

  • "two property tests of 15 examples each": the cap property now runs its 3 explicit examples, then 15 drawn ones. --hypothesis-show-statistics reports "15 passing" in the generate phase, and mutant (b)'s failure names a "Failing explicit example".
  • The timings (61.5s and 67.8s in pytest, 2.19 GB peak) come from round 1's build, before the examples. They still hold roughly. At 8ce831b6dc, on this session's 4-vCPU Linux VM, the whole file took 57.9s in pytest and 122.1s wall, at 2.06 GiB peak RSS. That is a local run with three other test processes going, not CI.

Replace the paragraph's first sentence with: "test_child_care_subsidies_household_benefits.py has 7 tests: the block test (4 cases), the list-completeness test, and two property tests. The dollar property draws 15 examples. The cap property runs 3 explicit examples and draws 15 more, with 1 simulation per example, or 2 when the bill is positive." Then say which commit the timings come from.

Checked and sound

  • The defect at main. I used a 2025 family: a parent with $25,000 of earnings, a child aged 3 in care 5 days a week for 8 hours a day, a $12,000 bill and the activity test met. At current main (db83eb5ab6):

    State Subsidy household_state_benefits household_net_income
    CT 11,250 0 33,222.62
    UT 10,584 0 31,183.89
    CO (already listed) 11,249.52 11,249.52 48,208.29
    AK, ak_child_care_subsidies set to 4,800 4,800 0 5,548.00

    At head, CT gives 11,250 and 44,472.62, UT 10,584 and 41,767.89, AK 4,800 and 10,348.00, and CO doesn't change. spm_unit_net_income doesn't move (CT 21,222.62, UT 19,183.89 at both).

  • Round 2's P1: the new YAML case. I ran the head's household_state_benefits.yaml against 9453494314's code. It gives 1 failed, 15 passed, the Colorado case: household_child_care_subsidies@2025: [6000.] differs from 12000.0. At head the file gives 16 passed.

  • Check 1: round 2's Colorado witness. The family is 2025 Colorado: a parent aged 30 with $25,000 of earnings and 40 work hours, and a child aged 3, a dependent, with 20 attending days a month. Round 2's table reproduces to the cent when the child's childcare_hours_per_week is set to 40 directly, which gives a parent fee of 0.

    Case co_child_care_subsidies household_state_benefits spm_unit_benefits household_net_income
    bill 6,000, after-school 6,000 12,000.00 12,000.00 (r2: 6,000.00) 13,909.06 48,662.02 (r2: 42,662.02)
    bill 1, after-school 6,000 6,001.00 6,001.00 (r2: 1.00) 7,910.06 42,663.02 (r2: 36,663.02)
    no bill, after-school 6,000 6,000.00 6,000.00 7,909.06 42,662.02
    bill 12,000, no after-school 12,000.00 12,000.00 13,909.06 48,662.02
    bill 6,000, no after-school 6,000.00 6,000.00 7,909.06 42,662.02
    no bill, no after-school 0.00 0.00 1,909.06 36,662.02
    • These are head's values. The merge base 0d933918a8 and current main db83eb5ab6 give the same in every cell. 9453494314 differs only where the table shows "r2".
    • I also set the care as 5 days × 8 hours a day, which gives a parent fee of $750.48 a year. In all six cases, household_state_benefits and household_net_income again equal main's at head and at main merged with head (b8515cd2b4, a local merge only). For example, a 6,000 bill plus 6,000 after-school gives 11,249.52 and 47,911.54 there, against 6,000.00 and 42,662.02 at 9453494314.
  • Hand values, computed from the parameters before I read the cases.

    • Colorado's parent fee. The 2025 FPG for two is $15,650 + $5,500 = $21,150 (gov/hhs/fpg.yaml). Monthly income of $2,083.33 is 1.1820 times the monthly FPG of $1,762.50. The base fee rate is 1% up to 1.0 and 14% above (parent_fee/base.yaml), so the fee is (0.01 + 0.14 × 0.1820) × $1,762.50 = $62.54 a month. At 8 hours a day the hours factor is 1 (parent_fee_rate_by_child_care_hours.yaml: 5 < 8 ≤ 12). That makes $750.48 a year, and the subsidy is $11,249.52 for any $12,000 split. The model agrees.
    • The cap. min(11,249.52, 6,000 + 6,000) = 11,249.52; min(5,250.52, 1 + 6,000) = 5,250.52; min(5,249.52, 6,000) = 5,249.52. At 9453494314: min(11,249.52, 6,000) = 6,000 and min(5,250.52, 1) = 1.
    • The new YAML case. min(12,000, 6,000 + 6,000) = 12,000. At 9453494314 it is min(12,000, 6,000) = 6,000, the failure above.
    • Vermont. Age 3 is 36 months, so PRESCHOOL. 40 hours is FULL_TIME (25 < 40 ≤ 50). The licensed-center rate has been $439 a week since 2024 (rates/licensed_center.yaml). $52,875 / $21,150 = 2.50 falls in the 2.2501 band, a $100 weekly share (family_share/scale.yaml). ($439 − $100) × 52 = $17,628, and each Vermont expectation is min(17,628, bill), or 17,628 with no bill.
  • The rule in every year and entity. The formula has no date logic, so it applies in every year the four list blocks govern (2021 to 2027). The bill sums two things over the SPM unit's own members: pre_subsidy_childcare_expenses (which spreads the SPM input over the unit's children) and after_school_expenses (a person input with no formula). One unit's bill therefore never caps another unit's subsidy.

  • Check 2: the other readers of the bill. The PR changes no bill variable.

    • childcare_expenses.py:12-16 still reads add(spm_unit, period, ["pre_subsidy_childcare_expenses"]) alone. git diff 0d933918a8 8ce831b6dc -- policyengine_us/variables/household is empty.
    • after_school_expenses gains one reader, this variable. Its only other reader is co_ccap_subsidy.
    • spm_unit_capped_work_childcare_expenses reads spm_unit_pre_subsidy_childcare_expenses and is unchanged.
  • Can adding after-school costs lower a counted subsidy below main? Not for any state in anything I ran. The bill only grows, but the zero-bill boundary can still lower Vermont's count below round 2's head (not below main):

    • With a positive child care bill, adding after-school costs only raises the cap, so the count can't fall below round 2's either.
    • With no child care bill, a positive after-school cost moves the unit from the uncapped branch to the cap (the second P3).
    • I swept 51 states × 2023–2026 × a child aged 3 or 7 (408 households a run). Each family has a parent with $25,000, 5 × 8 hours of care and the activity test met.
      • No child care bill, $1,000 of after-school costs. Only Colorado and Vermont pay. Colorado pays the after-school cost less the fee (63.64 to 313.24), which is under the bill. No household's household_state_benefits falls below the merge base's. Head differs from 9453494314 only in Vermont: 1,000 against 14,300 to 22,828.
      • A $1,000 child care bill plus $1,000 of after-school costs. 34 states pay. Only Vermont, in 2024 to 2026, pays above the $2,000 bill (22,828 at age 3, 19,292 at age 7), and the cap counts 2,000. No household falls below the merge base.
  • Check 3: round 2's two follow-ups. Neither blocks landing.

    • CAPPED_AGGREGATES still checks a mapping, not the formula. Mutant (h) survives every file but changes no behavior. Mutant (i), whose formula reads snap, still gets past the gate test, though 11 YAML cases fail. It's harmless today.
    • childcare_expenses still ignores after-school costs, the same as on main. For the fee-paying Colorado family, a $12,000 child care bill gives childcare_expenses 750.48. A $6,000 bill plus $6,000 of after-school costs gives 0, at head and at main. This PR doesn't change it.
  • Check 4: tests at head, one file per process, local:

    • household_state_benefits.yaml: 16 passed.
    • vt/dcf/ccfap/integration.yaml: 13 passed.
    • vt/dcf/ccfap/vt_ccfap.yaml: 17 passed.
    • test_child_care_subsidies_household_benefits.py: 7 passed (57.9s in pytest, 122.1s wall, 2.06 GiB peak RSS).
    • test_aggregate_list_integrity.py: 29 passed.
    • Colorado CCAP, every file under gov/states/co/ccap/ (14 files, of which co_ccap_subsidy.yaml sets after_school_expenses): 65 passed across the 14 files, none failed (co_ccap_subsidy.yaml: 2 passed).
    • household/expense/childcare/childcare_expenses.yaml: 2 passed.
    • Other readers of household_state_benefits that use child care cases: mt/dphhs/ccap/integration.yaml 10 passed, nv/dwss/ccdp/integration.yaml 9 passed, household_benefits.yaml 7 passed and household_net_income.yaml 6 passed.
    • Main has moved to db83eb5ab6, which includes a refactor of Colorado CCAP's parameter instants. I merged the head into it locally (git merge-tree exits 0). On that tree, household_state_benefits.yaml gives 16 passed, test_child_care_subsidies_household_benefits.py 7 passed and test_aggregate_list_integrity.py 29 passed.

    My venv first lacked hypothesis, because uv sync skips the dev extra. I installed it with uv sync --extra dev and reran the Python files.

  • Check 5: CI at 8ce831b6dc. The Pull request run 41577574038 concluded success on attempt 1, with all 34 jobs success, none cancelled or skipped. They include Household API Partners, PR validation and every Baseline, Contrib and Python shard. The shard plan put both changed Python test files in shard 5's remaining-1 process, which reported 1389 passed, 1 skipped.

  • Mutants. Each ran on a scratch worktree at 8ce831b6dc, one process per test file. I restored the worktree with git checkout -- . after each one, and git status --short was empty every time. The main checkout stayed clean.

    Mutant household_state_benefits.yaml test_child_care_subsidies_household_benefits.py test_aggregate_list_integrity.py
    (a) return subsidies 3 failed: Vermont at 10,000, at 1,000, bill on the child 1 failed: cap property, explicit example (5,001 against 5,000) 29 passed
    (b) min_ with no where 5 failed: NC 2024, DC 2025, Alaska, NC once, Vermont with no bill 2 failed: dollar property; cap property, explicit example bill=0 29 passed
    (c) max_ for min_ 5 failed: Vermont at 10,000, 1,000 and 20,000, bill on the child; Connecticut 1 failed: cap property, explicit example (5,001 against 5,000) 29 passed
    (d) child_care_subsidies back in the 2026 block 16 passed 2 failed: test_each_block_counts_only_the_aggregate[2026-01-01]; the dollar property (it drew 2026) 29 passed
    (e) ct_child_care_subsidies out of all 3 CCDF blocks 1 failed: Connecticut (child_care_subsidies 0 against 5,000) 1 failed: test_latest_blocks_list_every_state_program 29 passed
    (f) SPM input for the child care bill 1 failed: Vermont, bill on the child (17,628 against 10,000) 7 passed 29 passed
    (g) no bill counts 0 (round 2's) 5 failed, as (b) 2 failed: dollar property; cap property, bill=0 29 passed
    (h) CAPPED_AGGREGATES maps to {"de_ssp"} (round 2's) 16 passed 7 passed 29 passed: survives
    (i) formula reads snap (round 2's) 11 failed 2 failed 29 passed
    (j) bill without after_school_expenses (mine) 1 failed: Colorado 7 passed 29 passed
    (k) switch on the child care bill alone (mine) 16 passed 7 passed 29 passed: survives
    (l) after-school costs alone as the bill (mine) 4 failed: Vermont at 10,000 and 1,000, bill on the child; Colorado 1 failed: cap property (5,001 against 5,000) 29 passed

    Round 2's boundary fix holds: (a), (b), (c), (g) and (l) now fail the cap property through its explicit examples, not by chance draws. (b), the brief's check, fails on bill=0 with assert 0.0 == 5000 ± 0.02.

  • Readers, unchanged from round 2's list and checked with grep at head:

    • household_child_care_subsidies feeds the four household_state_benefits blocks.
    • Those feed household_benefits, the Tlaib ECPA, BOOST and EDAA copies of it, and tx_rebate's 2027 copy of the list.
    • household_benefits feeds household_net_income.
    • household_net_income feeds equiv_household_net_income, household_income_decile, household_net_income_including_health_benefits, marginal_tax_rate, behavioral_response_measurements and capital_gains_responses.

    No tax or benefit formula reads them, and spm_unit_net_income doesn't either.

  • Body and docs. I checked each sentence against the code and the pair comment.

    • Problem. "23 of the 51" holds for main's 2025 and 2026 blocks. Main's 2023 block lists 16 and its 2024 block 22.

    • Fix. Every statement about the code holds. I couldn't fetch the source of the Vermont quotes (see below). "Both data builds fill the bill from parent-paid ASEC child care" rests on Count state child care subsidies in the SPM once #10569's body, which names the two build files; I can't read those repositories. "Only Vermont paid above the bill" agrees with my two sweeps for 2023–2026.

    • Limit. $100 × 52 = $5,200, and 10,000 − 5,200 = 4,800.

    • Tests. Every case listed exists and passes, and each expected value matches my hand values.

      • "Each of the 51 programs in the 2025 CCDF list reaches child_care_subsidies and household_state_benefits once": at head, I set each program in each CCDF block (46 for 2023, 48 for 2024, 51 for 2025 and 2026) to a distinct amount on its own household with no bill. Each gives exactly that amount in child_care_subsidies, household_child_care_subsidies and household_state_benefits.
    • SPM side. spm_unit_benefits.py:61,65,73 lists co_ccap_subsidy, wa_child_care_subsidies and wv_child_care_subsidies and no other subsidy. spm_unit_capped_work_childcare_expenses starts from spm_unit_pre_subsidy_childcare_expenses.

    • Invariants. Each bullet matches a test. The cap bullet's "15 sampled cases" plus three explicit examples is right.

    • Impact. Every figure matches the pair comment:

      • $834M (household_benefits +834,411,242.71; household_net_income +834,411,293.80);
      • the four national rows;
      • all ten state rows, ordered by the amount added;
      • Vermont's 12 of 17 records and 1,494 of 5,124 weighted units;
      • $16.16M, which is 35.2% of $45.95M;
      • the 1,306-weight record, $10,608 against $1,054 and $12.48M;
      • "no household's benefits fall by a cent or more".

      Two figures come from Pay Vermont and New Hampshire child care subsidies only for care #10584's body, not the pair: "an effective sample of 156 households" and "$141.6M … $172.6M", which the body rounds to $142M to $173M.

    • "household_net_income calculates in all 51 jurisdictions for each year from 2021 to 2027." At head I ran one simulation a year holding 102 households: 50 states and DC, each with one family in care with a $12,000 bill and one family without care. Every year gave finite household_net_income and no error, and no state paid a subsidy without care.

    • Lines. The impact: and axiom: lines exist. The axiom line uses round 2's wording and still fits.

    • Changelog. changelog.d/9405.fixed.md is top-level, has a valid type and describes the change.

    • Docs. The variable's documentation, the module docstring and the formula comment all match the code.

  • Baseline movement and the population check. This round's delta moves household_child_care_subsidies, and through it household_state_benefits, household_benefits, household_net_income and their readers, only for units with after_school_expenses above 0.

    • Colorado. It restores main's values.
    • Vermont. See the second P3.

    The pair at 8ce831b6dc already measures the whole PR. The population check this delta needs is that the file stores no after_school_expenses. The pair says it doesn't, so the delta moves nothing there.

  • The author's brief. The PR does everything it asked for. It departs from the brief in two places, and both are justified:

    • the reference is one href with a # PDF pages 1-2 comment, CLAUDE.md's rule for variable tuples;
    • the bill now adds after-school costs, round 2's P1.

    The hub later replaced the body text the brief prescribed.

  • Scope. The PR changes six files, none under .github/ or tests/policy/baseline/partners/. ruff format --check and ruff check pass on its three Python files.

  • Overlap. I searched open PRs for the changed names.

What I couldn't check

  • Sources. The session's egress proxy refused every source host I tried, each with HTTP 403 on CONNECT:

    • outside.vermont.gov (both Vermont PDFs) and dcf.vermont.gov;
    • legislature.vermont.gov;
    • www.sos.state.co.us (Colorado's 8 CCR 1403-1);
    • www.govinfo.gov, www.ecfr.gov and uscode.house.gov;
    • web.archive.org and www.law.cornell.edu.

    I confirmed no source, so I can't say whether the body misreads one. The two Vermont quotes and "which providers may waive" still rest on the hub's ruling. The Vermont and Colorado values above come from the parameter files, not from the agencies' documents.

  • Population data. I ran no microsimulation. Two claims rest on others' runs: that the file stores no after_school_expenses (the pair), and that both data builds fill the bill from ASEC spending (Count state child care subsidies in the SPM once #10569's body).

  • "Waits on a data decision" (SPM side). Count state child care subsidies in the SPM once #10569's body doesn't mention a hold, and I can't see the hub's hold list.

  • Round 1's $12,000 sweep for 2021 to 2027. I didn't rerun it. My sweeps cover 2023 to 2026, with no child care bill or a $1,000 one, plus after-school costs.

  • CI timing for the changed Python file. My timings are local.


Generated by Claude Code

This branch has not been deployed

No deployments
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.

household_state_benefits omits 28 of 51 implemented state child care subsidy variables (child care assistance never reaches net income in those states)

3 participants