Skip to content

Compare completed years in the childless EITC age ceiling - #10585

Merged
MaxGhenis merged 5 commits into
mainfrom
eitc-age-ceiling-completed-years
Nov 3, 2026
Merged

MaxGhenis merged 5 commits into
mainfrom
eitc-age-ceiling-completed-years

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

26 U.S.C. 32(c)(1)(A)(ii)(II) requires a childless filer (or either spouse) to have "attained age 25 but not attained age 65 before the close of the taxable year". gov.irs.credits.eitc.eligibility.age.max is 64, the highest completed age that qualifies. But eitc_demographic_eligible tested age <= max_age, so a filer aged 64.5, who has not attained 65, failed.

Witness at main (97b6d5135c, 2024, single, $8,000 wages): age 64.0 gives eitc 612; age 64.5 gives 0 (wrong); age 65.0 gives 0.

Change

eitc_demographic_eligible now tests np.floor(age) <= max_age, following #10526 for Maine. The minimum side (age >= min_age) already handles fractional ages and is unchanged. The parameter keeps its meaning; only its description was reworded ("completed age is at most this value"). The formula comment now also says it reads age as the age at the close of the taxable year, which the statute's "before the close of the taxable year" requires.

me_childless_eitc_age_eligible.py:55 applies the same completed-years federal test (np.floor(age) <= federal_age.max(period)), so its comment ("read as in eitc_demographic_eligible") holds. me_eitc takes max_(eitc, me_pro_forma_childless_eitc). A Maine couple aged 23 and 64.5 with $8,000 wages in 2024 moves to the federal path: against main, eitc goes from 0 to 612, the cohort flag from true to false and the pro forma credit from 612 to 0, and me_eitc stays 306. me_eitc moves only where eitc pays less than the pro forma. With takes_up_eitc: false, eitc stays 0 and me_eitc falls from 306 to 0, the result me_eitc.yaml already pins for a federally eligible couple that doesn't take up the credit. Inputs that zero eitc for non-filing or a section 911 election fall the same way.

Every year of age/max.yaml:

  • 2013-2020: 64.
  • 2021: .inf (ARPA 32(n)(2)). Floor of any age is at most inf.
  • 2022 on: 64.
  • There are no pre-2013 values; the file starts in 2013. The comparison holds in each year.

Invariant

For each filer and a whole-number maximum m, the age test holds exactly when min <= age < m + 1, so raising age past the ceiling never passes it again. With m = .inf (2021) only the minimum binds. The YAML cases pin the boundary at 64.0, 64.5, 64.99 and 65.0.

Tests (eitc_demographic_eligible_fractional_age.yaml)

  • 2024, age 64.0: eligible, eitc 612. Hand check: 7.65% x $8,000 = $612, under the $632 maximum and the $10,330 phase-out start.
  • 2024, age 64.5: eligible, eitc 612.
  • 2024, age 65.0: ineligible, eitc 0.
  • 2026, age 64.5: eligible.
  • 2021, ages 64.5 and 80.5: eligible (no ceiling).
  • 2024, age 24.5: ineligible (minimum unchanged).
  • 2024, age 64.99: eligible, eitc 612 (rounding would fail it).
  • 2024, joint, ages 70 and 64.5: eligible through the spouse (32(c)(1)(A)(ii)(II) lets either spouse meet the test), eitc 612. $8,000 falls short of the joint phase-out start.
  • 2024, joint, ages 70 and 65.0: ineligible, eitc 0.

Test counts at the head, one file per process: eitc_demographic_eligible_fractional_age.yaml 10 passed; eitc_demographic_eligible.yaml 21; eitc_eligible.yaml 11; earned_income_tax_credit.yaml 3; me_eitc.yaml 27; me_childless_eitc_age_eligible.yaml 16; test_me_childless_eitc_age_gate.py 3; test_me_childless_eitc_age_gate_property.py 1; test_dependent_elsewhere_head_spouse_swap_invariance.py 8; test_source_conflicts.py 103. All passed.

Mutants (one process each, code restored after each; run against eitc_demographic_eligible_fractional_age.yaml)

  • age <= max_age: killed, 4 failures (64.5 in 2024 and 2026, 64.99, joint 70/64.5).
  • age < max_age: killed, 5 failures (those four plus 64.0).
  • np.round(age) <= max_age: killed, 1 failure (64.99).
  • Age test on the head only: killed, 1 failure (joint 70/64.5).

Sweep

Other variables/gov/irs/ upper bounds compare raw age with a strict < against "under N" / "not attained N", which is right for fractional ages:

Site Law (unverified wording) Verdict
credits/ctc/maximum/ctc_qualifying_child_ignoring_filer_identification.py:31 age < age_limit 24(c)(1), not attained age 17 right
credits/cdcc/cdcc_eligible.py:36 age < child_age (13) 21(b)(1)(A), not attained age 13 right
tax/federal_income/alternative_minimum_tax/kiddie_tax/amt_kiddie_tax_applies.py:44,47 age < 19/24 1(g)(2)(A) via 152(c)(3), not attained 19 / 24 right
IRS lower bounds (aged_head, aged_spouse, is_irs_aged, IRA and 401(k) catch-up, savers credit, elderly credit, senior deduction) age >= N attained age N right

No other federal site was wrong, so eitc_demographic_eligible is the only federal fix.

Outside gov/irs: same pattern, likely wrong. These are recomputations of the federal childless EITC age ceiling (or state copies of it) using age <= 64. Proposed fix: np.floor(age) <= max_age. State law wasn't read here.

  • states/nm/tax/income/credits/eitc/nm_eitc_demographic_eligible.py:35 (federal max)
  • states/ok/tax/income/credits/eitc/federal_credit/eligible/ok_federal_eitc_demographic_eligible.py:46
  • states/dc/tax/income/credits/eitc/dc_eitc_without_qualifying_child.py:36
  • states/co/tax/income/credits/eitc/co_eitc.py:49
  • states/md/tax/income/credits/eitc/federal_eitc_without_age_minimum.py (below_max_age)
  • states/wa/tax/income/credits/wa_working_families_tax_credit.py:74 and tools/state_eitc_helpers.py:60 (frozen federal max)
  • states/in/tax/income/credits/earned_income_credit/in_eitc_eligible.py:47-48 (IN max 64)
  • reforms/states/ny/wftc/ny_working_families_tax_credit.py:374 ((age >= min_age_student) & (age <= max_age), reading gov.irs.credits.eitc.eligibility.age.max)
  • states/mn/tax/income/credits/mn_wfc_eligible.py:23. MN's own maximum; value and law unread, so unclear.

Outside gov/irs: unclear. These are benefit and state age <= p.<limit> sites whose law wasn't read. Each is right if the parameter is the last qualifying completed age and the law says "N or younger" / "through age N". Each is wrong for fractional ages if the law says "under N" and the parameter is N-1 (then floor). Examples: hhs/medicaid/costs/is_medicaid_slcsp_dependent_child.py:31, aca/slspc/is_aca_family_tier_dependent_child.py:14, aca/eligibility/aca_child_index.py:12, usda/snap/income/snap_excluded_child_earner.py:18, usda/snap/eligibility/work_requirements/is_snap_abawd_exempt.py:75, hhs/medicaid/eligibility/medicaid_work_requirement_eligible.py:105, RI CTC and rebate, VT CTC, CT rebate, DC KCCATC, IN dependent exemption, OR WFHDC, IL HBI, TX FPP and others. The full list: grep -rnE "\bage\w*\s*<=" policyengine_us/variables (63 hits).

Impact

impact: none at baseline. The production dataset stores whole-year ages (none of its 166,321 people has a fractional age), so no head or spouse falls strictly between 64 and 65.
axiom: TheAxiomFoundation/rulespec-us#1776 queued

🤖 Generated with Claude Code

IRC 32(c)(1)(A)(ii)(II) requires a childless filer not to have attained
age 65 before the close of the year. The parameter holds 64, the highest
qualifying completed age, but the test compared the raw age, so a filer
aged 64.5 failed. Compare floor(age), as Maine's cohort test already does.

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

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: d710b14cab4a87635f8425c2d50a4d0ddc2a1d1d (one commit on origin/main 97b6d5135c). The PR is still a draft, so CI hasn't run.

What the item asked, and how it was resolved

  • Bug reproduced at main. 2024, single, $8,000: age 64.0 gives eitc 612; age 64.5 gives 0; age 65.0 gives 0.
  • Fix. policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py:29-34 now reads (age >= min_age) & (np.floor(age) <= max_age), with the 32(c)(1)(A)(ii)(II) wording quoted from the brief. The parameter's meaning is kept; only its description was reworded (parameters/gov/irs/credits/eitc/eligibility/age/max.yaml:1).
  • Every year of age/max.yaml. 64 for 2013-2020, .inf for 2021 (ARPA), 64 from 2022. There are no pre-2013 values. Floor-comparison is right in each year.
  • Sweep. The table is in the PR body. Only eitc_demographic_eligible was wrong in gov/irs. CTC (age < 17), CDCC (age < 13) and kiddie tax (age < 19/24) use strict "under N" and are right. The aged, catch-up, savers and elderly tests are lower bounds and are right. Nine state copies of the federal ceiling use the same age <= 64 pattern: NM, OK, ME:55, DC, CO, MD, WA plus tools/state_eitc_helpers.py:60, and IN. MN is unclear. The benefit and state child-age <= sites are unclear because their law wasn't read.

Tests (one file per process)

  • New YAML. eitc_demographic_eligible_fractional_age.yaml, 7 cases, all passing:
    • 2024 at ages 64.0, 64.5 and 65.0: eitc 612, 612 and 0. Hand check: 7.65% x $8,000 = $612, under the $632 maximum and the $10,330 phase-out start.
    • 2026 at 64.5.
    • 2021 at 64.5 and 80.5 (ARPA, no ceiling).
    • 24.5 fails the minimum.
  • New Hypothesis. test_eitc_age_ceiling_completed_years.py, 2 tests passing: the age test equals min <= age and floor(age) <= max, and it is monotone past the ceiling. It includes explicit examples around 64-65.
  • Affected YAML. 23 files passed, 184 cases, 0 failures: the whole gov/irs/credits/earned_income/ folder, the DC EITC files, and every YAML naming eitc_demographic_eligible.
  • Python. test_me_childless_eitc_age_gate.py 3 passed, test_me_childless_eitc_age_gate_property.py 1 passed, test_eitc_qualifying_child_invariants.py 3 passed.
  • Partner tests: none move. No partner file has a fractional age near the ceiling (grep), so none can move.

Mutants (each in its own process, code restored after)

  • age <= max_age: killed (2 YAML failures, 1 Hypothesis failure).
  • age < max_age: killed (3 YAML failures, 1 Hypothesis failure).

Judgment calls

Proposed follow-up issues

  • Floor completed years in state copies of the federal childless EITC ceiling. NM, OK, ME:55, DC, CO, MD, WA (and tools/state_eitc_helpers.py:60) and IN compare raw age with 64, so a 64.5-year-old fails. Each follows the federal 32(c)(1)(A)(ii)(II) wording or its own statute.
  • Audit benefit-program child-age upper bounds for fractional ages. About 65 age <= p.<limit> sites (ACA/Medicaid child tiers, SNAP, CCAP, state CTCs) need each law's wording ("under N" vs "N or younger") checked against the parameter.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: REQUEST_CHANGES
d710b14
METHODOLOGY_CHOICE: no

Independent review, round 1. No earlier Hub review in progress comment exists on this PR, so this review starts from the beginning. The head is still d710b14cab. Main has moved 14 commits past the merge base 97b6d5135c, but none of them touch gov/irs/credits/earned_income, the EITC parameters or age.py.

The fix is correct and CI at the head is green. I'm asking for two small test and documentation changes (P2) before landing; nothing changes in the formula.

Findings

P2-1. The new Python property test restates the formula, and the PR doesn't report its cost

policyengine_us/tests/test_eitc_age_ceiling_completed_years.py:69 asserts bool(result) == (min_age <= age and math.floor(age) <= max_age). That is the changed line of eitc_demographic_eligible.py:34, rewritten in Python. CLAUDE.md ("Test design and CI cost") says not to "duplicate a policy formula in Python merely to test it against itself", and asks that a new simulation-building Python test report its elapsed time and peak memory. The body reports neither.

  • Cost, measured locally (not Linux CI): pytest -q test_eitc_age_ceiling_completed_years.py gives 2 passed in 34.29s, 79 s wall including import, and 1,625 MB peak RSS (from getrusage(RUSAGE_CHILDREN)).
  • What it adds over YAML: one mutant only. np.round(age) <= max_age (my M3, below) passes all 7 new YAML cases and all 21 cases in eitc_demographic_eligible.yaml. Only the property's explicit @example at 64.99 kills it. A single YAML case would kill it just as well.
  • Fix: delete the Python file and add these cases to eitc_demographic_eligible_fractional_age.yaml. Then every mutant below dies in YAML. If you keep the file instead, put the time and memory figures in the body and say what failure it catches that YAML can't.
- name: Childless filer aged 64.99 has not attained 65 and meets the age test, 2024
  period: 2024
  input:
    people:
      filer:
        age: 64.99
        is_tax_unit_head: true
        ssn_card_type: CITIZEN
        employment_income: 8_000
    tax_units:
      unit:
        members: [filer]
        filing_status: SINGLE
        would_file_taxes_voluntarily: true
        takes_up_eitc: true
  output:
    eitc_demographic_eligible: true
    eitc: 612

P2-2. No test covers the spouse side of the new comparison

Under 32(c)(1)(A)(ii)(II), on a joint return either spouse can meet the age test. The code handles this through tax_unit.any(meets_age_requirements & is_filer_or_spouse), and my run confirms it works. But every new case, YAML and Hypothesis alike, uses a single filer.

  • Mutant M4 applies the age test to the head only (is_filer_or_spouse replaced by person("is_tax_unit_head", period)). It survives all 7 new YAML cases, both Hypothesis tests and all 21 cases in eitc_demographic_eligible.yaml. Only tests/core/test_dependent_elsewhere_head_spouse_swap_invariance.py kills it (1 failed, 7 passed under M4; 8 passed at the head). That file only exercises whole ages.
  • Witness (my run, 2024, joint, $8,000 head wages):
Head / spouse ages eitc at main eitc at this head
70 / 64.5 0 612
64.5 / 70 0 612
70 / 65.0 0 0
  • Fix: add the pair below to eitc_demographic_eligible_fractional_age.yaml, plus the same case with the spouse aged 65.0, expecting eitc_demographic_eligible: false and eitc: 0.
- name: Joint filers aged 70 and 64.5 meet the age test through the spouse, 2024
  period: 2024
  input:
    people:
      head:
        age: 70
        is_tax_unit_head: true
        ssn_card_type: CITIZEN
        employment_income: 8_000
      spouse:
        age: 64.5
        is_tax_unit_spouse: true
        ssn_card_type: CITIZEN
    tax_units:
      unit:
        members: [head, spouse]
        filing_status: JOINT
        would_file_taxes_voluntarily: true
        takes_up_eitc: true
    marital_units:
      mu:
        members: [head, spouse]
  output:
    # Either spouse may meet 32(c)(1)(A)(ii)(II); the spouse's completed age is 64.
    eitc_demographic_eligible: true
    eitc: 612

P3-1. The comment at me_childless_eitc_age_eligible.py:44-45 is now false

The comment says the federal test is "read as in eitc_demographic_eligible", but line 55 still tests raw age <= federal_age.max(period).

Output today: none moves. I read me_eitc.py: it takes max_(eitc, me_pro_forma_childless_eitc), and the pro forma is the same federal computation without the age gate. Take a Maine couple aged 23 and 64.5 in 2024:

  • on main, federal 0 and pro forma X, so Maine gets X;
  • at this head, federal X and pro forma X, so Maine still gets X.

The defect is identical on main, so it belongs with the follow-up. Changing line 55 here would still be output-neutral and would make the comment true:

            & (np.floor(age) <= federal_age.max(period))

P3-2. The follow-up list in the body misses one copy, and the body and the hand-off disagree

  • A missing copy. policyengine_us/reforms/states/ny/wftc/ny_working_families_tax_credit.py:374 ((age >= min_age_student) & (age <= max_age), reading gov.irs.credits.eitc.eligibility.age.max) is another raw-age copy of the federal ceiling. Add it to the "same pattern, likely wrong" list.
  • Axiom line. The hand-off says axiom: n/a, but the body says axiom: TheAxiomFoundation/rulespec-us#1776 queued. The body is the one that counts. I can't reach rulespec-us from this session, so I couldn't check that Add Illinois and Missouri income taxes to net income tree #1776 exists, is labelled pe-parity or is dispatch-ready.
  • Stale hand-off. It says the PR is a draft and CI hasn't run. Both are now out of date: the PR is not a draft, and CI is green (see below).

P3-3. The fix assumes age is the age at the close of the year

age.py defines age as a float with default_value = 40 and no documentation of when it's measured. The statute asks about age "before the close of the taxable year". So np.floor(age) <= 64 is right only if age holds the age at year end. If a dataset ever stored the age at mid-year, a 64.5 would have turned 65 by December 31.

This PR doesn't create that question: Maine's #10526 already uses the same convention. I'm not counting it as a methodology choice by this PR. One sentence in the new code comment would make the assumption explicit: "age is the age at the close of the taxable year."

Mutants

Each mutant ran in its own process on a scratch worktree, with outbound HTTP pointed at a dead proxy. After each run the code was restored and git status --short | wc -l gave 0.

Mutant New YAML (7) eitc_demographic_eligible.yaml (21) Hypothesis (2) Killed by
M1 age <= max_age 2 fail 21 pass 1 fail 64.5 cases (2024 and 2026); property
M2 age < max_age 3 fail 1 fail 1 fail 64.0 / 64.5 cases; "Childless 64-year-old"; property
M3 np.round(age) <= max_age (mine) 7 pass 21 pass 1 fail property @example 64.99 only (see P2-1)
M4 age test on head only (mine) 7 pass 21 pass 2 pass only test_dependent_elsewhere_head_spouse_swap_invariance.py (see P2-2)

Merge with #10583

#10583's head has moved from 7d651dee54 to 5d09b58583 since the brief was written. Both heads merge into this PR without conflict. The setup is a head-of-household filer with $8,000 wages and an 8-year-old qualifying child with ssn_card_type: NONE, so eitc_child_count is 0.

Tree 2023, filer aged 64.5 2023, filer aged 65.5 2020, filer aged 64.5 2020, filer aged 65.5
main 0 0 0 0
#10583 alone (7d651dee) 600 600 0 0
this PR alone 600 0 538 0
merge with 7d651dee 600 600 538 0
merge with 5d09b585 600 600 538 0

What each should get, assuming #10583's reading (ARPA struck former 32(c)(1)(F) from 2021, so a child without an SSN still makes the filer an eligible individual under (c)(1)(A)(i)):

  • 2023, either age: the zero-child amount, $600, regardless of age. The merge gives 600 for both, so the two changes compose correctly.
  • 2020: the merge keeps this PR's age-only result. At 64.5 that is 538, the same as main gives at whole ages: main pays 538 at 40.0 and 64.0 and 0 at 64.5. So this PR only makes 64.5 match 64.0.

Whether pre-2021 law should pay a filer whose only qualifying child lacks an SSN at all is #10583's question. The answer is the same on main, so it's not this PR's defect.

Checked and sound

  • Bug at main (origin/main 4bfc548d), 2024 single, $8,000: eitc is 612, 0 and 0 at ages 64.0, 64.5 and 65.0; at 64.999 it is 0.

  • Same cases at the head: 612, 612 and 0; 612 at 64.999. In 2023: main gives 600, 0 and 0; the head gives 600, 600 and 0.

  • Is np.floor(age) <= max_age right? For "not attained age 65" with max = 64, floor(a) <= 64 is equivalent to a < 65 for every real a. It includes an integer 64 and excludes an integer 65 (both confirmed by run).

    • The minimum side age >= min_age matches "attained age 25" exactly, so it doesn't need a floor.
    • For an integer max, np.floor(age) <= max is identical to age < max + 1. Either form would do.
  • age/max.yaml: 64 from 2013, .inf in 2021, 64 from 2022. There are no pre-2013 values.

    • np.floor(80.5) <= inf is true; the 2021 run gives 1,224 at both 64.5 and 80.5.
    • The new description ("completed age is at most this value") is accurate for this variable.
    • No reform, contrib parameter or YAML test sets the parameter, so no fractional value exists anywhere (grep of reforms/, parameters/gov/contrib, tests).
  • The hand values hold, computed from the repo's parameters before reading the YAML:

    • 2024: phase-in rate 0.0765, maximum $632, phase-out start $10,330, so 0.0765 × 8,000 = 612.
    • 2023: maximum $600, start $9,800, so 600 (612 capped).
    • 2021: 1,224 matches the ARPA childless schedule in the parameters.
  • Federal sweep. I grepped every age comparison in variables/gov/irs/. Each upper bound is a strict <:

    • CTC at ctc_qualifying_child_ignoring_filer_identification.py:31: age < thresholds[-1], where the threshold is 17 (18 in 2021).
    • CDCC at cdcc_eligible.py:36: age < child_age, where child_age is 13.
    • Kiddie tax at amt_kiddie_tax_applies.py:44,47: age < 19 / age < 24, from gov.irs.dependent.ineligible_age.
    • The EITC qualifying-child age test runs through is_qualifying_child_dependent.py:23, also age < 19/24.
    • For "not attained N", a strict < against N is right for fractional ages.
    • Every other site is a lower bound age >= threshold (aged standard deduction, senior deduction, IRA and 401(k) catch-up, Saver's Credit, elderly credit, TCE). For "attained N", that is also right.
    • eitc_demographic_eligible was the only wrong federal site, which matches the body.
  • Readers of eitc_demographic_eligible:

    • eitc_eligible gets better.
    • dc_eitc_with_qualifying_child.py:35 gets better: a DC child without a TIN plus a filer aged 64.5 now gets the federal-like childless amount.
    • Readers of eitc that take a max with a state path don't double-count: CO co_eitc.py:91 and ME me_eitc.py.
  • State copies. These give the same output on main and at this head, so they go to the follow-up the body already lists:

    • NM nm_eitc_demographic_eligible.py:35;
    • OK ok_federal_eitc_demographic_eligible.py:46, frozen at 2020 law;
    • ME me_childless_eitc_age_eligible.py:55;
    • DC dc_eitc_without_qualifying_child.py:36;
    • CO co_eitc.py:49: its ITIN path is still raw-age, but max_ with the now-correct federal EITC improves the result for SSN filers;
    • MD federal_eitc_without_age_minimum.py:26: the childless MD credit uses this instead of eitc, so it stays at 0 at 64.5 on both sides;
    • WA wa_working_families_tax_credit.py:74 and tools/state_eitc_helpers.py:60, both on frozen 2022 parameters;
    • IN in_eitc_eligible.py:47-48, with its own parameter;
    • plus the NY WFTC reform from P3-2.

    None of them reads eitc_demographic_eligible, so this PR makes no state output worse. The body already carries the limit; it only needs the NY line added.

  • Tests, one process each, all at the head:

    • eitc_demographic_eligible_fractional_age.yaml: 7 passed;
    • eitc_demographic_eligible.yaml: 21 passed;
    • eitc_eligible.yaml: 11 passed;
    • test_eitc_age_ceiling_completed_years.py: 2 passed;
    • test_me_childless_eitc_age_gate.py: 3 passed;
    • test_me_childless_eitc_age_gate_property.py: 1 passed;
    • test_dependent_elsewhere_head_spouse_swap_invariance.py: 8 passed;
    • earned_income_tax_credit.yaml: 3 passed, run under M4 only (I did not run it at the unmutated head).
  • Hypothesis ranges:

    • Ages are drawn from [25, 80] and rounded to 2 decimals, so the 2021 minimum of 19 is never exercised; YAML covers the minimum at 24.5.
    • The boundary is reached only through explicit @examples: 63.99, 64.0, 64.5, 64.99, 65.0 and 65.5.
    • max_examples=10.
    • The monotonicity test checks the stated claim.
  • CI at d710b14cab: the "Pull request" run 41414877820 succeeded. All 31 of its jobs report success, including Household API Partners, every baseline shard, Microsimulation and the seven Python shards. One earlier run (41407680853) shows skipped; it was from the draft period and doesn't count.

  • Scope:

    • The PR changes 5 files and none sits under .github/ or tests/policy/baseline/partners/.
    • The changelog fragment changelog.d/eitc-age-ceiling-completed-years.fixed.md is top-level and matches the branch name.
    • The body has both an impact: line and an axiom: line.
  • Open-PR overlap:

  • Baseline impact:

    • The diff can move a baseline output only for a head or spouse whose age lies strictly between 64 and 65 in a year with max = 64, and who has no counted qualifying child.
    • If the enhanced CPS stores whole-year ages, the change can't move any output at baseline.

What I couldn't check

  • Statute text. uscode.house.gov, govinfo.gov, law.cornell.edu and irs.gov were all refused by this session's network proxy, so I couldn't re-read 26 U.S.C. 32(c)(1)(A)(ii)(II), 32(n), 24(c), 21(b) or 152(c)(3) here. The quotations above come from the brief and the code comments, and I haven't verified them against the source. The same goes for former 32(c)(1)(F) (the 2020 row of the Let a qualifying child without an SSN establish EITC eligibility from 2021 #10583 table).
  • Population data. I couldn't check whether any dataset (enhanced CPS or another) carries a fractional age, so the population impact is unverified. The hub's impact: pending (hub) run should confirm a zero change, or count the units with 64 < age < 65.
  • rulespec-us#1776, its labels and whether it is dispatch-ready; I can't reach that repo from this session.
  • Linux CI time and memory for the new Python file. The figures in P2-1 are local.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

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


Generated by Claude Code

The Python test restated the formula line it checked and only killed the
np.round mutant beyond YAML; a 64.99 YAML case covers that. Add joint
cases so an age test applied to the head alone fails. Note that the
comparison reads age as the age at the close of the taxable year, and
apply the same completed-years federal test in Maine's cohort flag so
its comment holds (me_eitc unchanged: the max of federal and pro forma).

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

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 395d8cd68daeab392bf17244f91b5222340f46a8

Merge. Fast-forwarded to d710b14c, then merged origin/main with a merge commit (240c5c10). No conflicts.

Finding 1. Deleted policyengine_us/tests/test_eitc_age_ceiling_completed_years.py; no other file references it. Added the 64.99 case to eitc_demographic_eligible_fractional_age.yaml:145. Hand value 7.65% × $8,000 = $612, under the $632 max and the $10,330 phase-out start. I checked all three against phase_in_rate.yaml, max.yaml and phase_out/start.yaml (2024, 0 children). age.max is 64.

Finding 2. Added "Joint filers aged 70 and 64.5 meet the age test through the spouse, 2024" (true, 612) and "Joint filers aged 70 and 65 don't meet the age test, 2024" (false, 0) at eitc_demographic_eligible_fractional_age.yaml:166 and :194. Mutant (d) now fails the 70/64.5 case.

Finding 3. me_childless_eitc_age_eligible.py:55 is now & (np.floor(age) <= federal_age.max(period)). me_eitc.py:136-139 takes max_(eitc, me_pro_forma_childless_eitc). Witness: Maine joint couple aged 23 (with $8,000 wages) and 64.5, 2024:

before after
eitc_demographic_eligible true true
eitc 612 612
me_childless_eitc_age_eligible true false
me_pro_forma_childless_eitc 612 0
me_eitc 306 306

me_eitc doesn't move, so the edit stays.

Finding 4. age.py has no documentation beyond the label "age" (YEAR, default 40). Added "This reads age as the age at the close of the taxable year." to the comment at eitc_demographic_eligible.py:33-34. The formula is unchanged.

Finding 5. The body now adds the NY WFTC reform line (ny_working_families_tax_credit.py:374, confirmed). Maine is gone from the "likely wrong" list and described under Change. I removed the Invariant/Hypothesis section and added the three new cases, this round's counts and mutants, the CI result and the given impact line. The axiom: line is kept. GitHub's readback matches my file except the closing line: the session's GitHub proxy replaced 🤖 Generated with [Claude Code](...) with its own _Generated by [Claude Code](...)_ footer. I couldn't keep the original closing line through this path.

Tests (one file per process, at the head):

  • eitc_demographic_eligible_fractional_age.yaml: 10 passed
  • eitc_demographic_eligible.yaml: 21 passed
  • eitc_eligible.yaml: 11 passed
  • earned_income_tax_credit.yaml: 3 passed
  • me_eitc.yaml: 27 passed
  • me_childless_eitc_age_eligible.yaml: 16 passed
  • test_me_childless_eitc_age_gate.py: 3 passed
  • test_me_childless_eitc_age_gate_property.py: 1 passed
  • test_dependent_elsewhere_head_spouse_swap_invariance.py: 8 passed
  • test_source_conflicts.py: 103 passed

hypothesis is in the dev extras but wasn't in the synced env. Without it the property file skipped and the swap-invariance file errored at import. I ran both with uv run --with hypothesis.

Mutants (each in its own process against the fractional-age YAML; restored after each, cmp verified):

  • (a) age <= max_age: killed, 4 fail (64.5/2024, 64.5/2026, 64.99, joint 70/64.5)
  • (b) age < max_age: killed, 5 fail (those plus 64.0)
  • (c) np.round(age) <= max_age: killed, 1 fail (64.99)
  • (d) head-only age test: killed, 1 fail (joint 70/64.5)

CI. Pull request run 41528556634 at 395d8cd6: success.

Judgment calls

  • make format also reformatted docs/usage/social-security-pia-ratio.md, which came from main and is out of scope. I reverted it.
  • I added a short # comment to each new YAML case giving its hand value or reason, matching the existing cases.

Proposed follow-up: "Apply completed-years EITC age ceiling in state recomputations". NM, OK, DC, CO, MD, WA, IN and the NY WFTC reform recompute the federal childless ceiling with raw age <= 64. Each should floor the age after its state law is read.


Generated by Claude Code

MaxGhenis and others added 2 commits November 2, 2026 19:39
The property test's oracle and comment still described the exact-age federal test. Its ages now reach 64.5 and 64.99, and a new YAML case fails if me_childless_eitc_age_eligible.py goes back to the exact age.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit 0a87d1f into main Nov 3, 2026
34 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#10585 at a9ab6fef0222e9e7600d46a2faadaf14290e43e7 (squash): Compare completed years in the childless EITC age ceiling.

  • Review: cloud review r1 asked for changes; the fix round's delta review (Opus 5.5, independent) approved at 395d8cd with P3 notes only, which the hub applied at the head (a test oracle, one YAML case, body text).
  • Branch update: GitHub's branch update merged main 6fa4eaf6d3 into the reviewed head 6e7f76ef5b; each file's changes match the reviewed head's.
  • Gates checked live at merge:
    • gh pr checks exits 0;
    • the latest Pull request run at the head concluded success;
    • MERGEABLE;
    • not a draft;
    • no reviewer's latest review requests changes;
    • the semantic-overlap guard;
    • head pinned.
  • Impact: none at baseline: age in populace_us_2024.h5 at populace-us-2024-spm-20260909 holds whole numbers for all 166,321 people (read directly), so no head or spouse falls strictly between 64 and 65.
  • Axiom: [pe-parity] IRC 32(c)(1)(A)(ii)(II): derive the childless EITC age test from the taxpayer and spouse instead of a free input TheAxiomFoundation/rulespec-us#1776 queued

@MaxGhenis
MaxGhenis deleted the eitc-age-ceiling-completed-years branch November 3, 2026 04:29
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.

2 participants