From d710b14cab4a87635f8425c2d50a4d0ddc2a1d1d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 07:39:04 +0000 Subject: [PATCH 1/3] Compare completed years in the childless EITC age ceiling 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 --- .../eitc-age-ceiling-completed-years.fixed.md | 1 + .../irs/credits/eitc/eligibility/age/max.yaml | 2 +- ...c_demographic_eligible_fractional_age.yaml | 143 ++++++++++++++++++ .../test_eitc_age_ceiling_completed_years.py | 81 ++++++++++ .../eligibility/eitc_demographic_eligible.py | 7 +- 5 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 changelog.d/eitc-age-ceiling-completed-years.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible_fractional_age.yaml create mode 100644 policyengine_us/tests/test_eitc_age_ceiling_completed_years.py diff --git a/changelog.d/eitc-age-ceiling-completed-years.fixed.md b/changelog.d/eitc-age-ceiling-completed-years.fixed.md new file mode 100644 index 00000000000..03b09444c03 --- /dev/null +++ b/changelog.d/eitc-age-ceiling-completed-years.fixed.md @@ -0,0 +1 @@ +Compare completed years in the childless EITC age ceiling, so a filer aged 64.5, who has not attained 65, meets IRC 32(c)(1)(A)(ii)(II). diff --git a/policyengine_us/parameters/gov/irs/credits/eitc/eligibility/age/max.yaml b/policyengine_us/parameters/gov/irs/credits/eitc/eligibility/age/max.yaml index 1a26635ef5e..236d40c8685 100644 --- a/policyengine_us/parameters/gov/irs/credits/eitc/eligibility/age/max.yaml +++ b/policyengine_us/parameters/gov/irs/credits/eitc/eligibility/age/max.yaml @@ -1,4 +1,4 @@ -description: The US limits EITC eligibility for filers without children to those below this age. +description: The US limits EITC eligibility for filers without children to those whose completed age is at most this value. metadata: unit: year label: EITC maximum childless age diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible_fractional_age.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible_fractional_age.yaml new file mode 100644 index 00000000000..bdba8ce11d2 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible_fractional_age.yaml @@ -0,0 +1,143 @@ +# IRC 32(c)(1)(A)(ii)(II): a filer without a qualifying child must have +# "attained age 25 but not attained age 65 before the close of the taxable +# year". gov.irs.credits.eitc.eligibility.age.max is the highest completed age +# that qualifies (64; no limit in 2021 under ARPA's 32(n)(2)), so the test +# compares completed years. + +- name: Childless filer aged 64.0 meets the age test, 2024 + period: 2024 + input: + people: + filer: + age: 64.0 + 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: + # Completed age 64 <= 64. + eitc_demographic_eligible: true + # 7.65% x $8,000 = $612, under the $632 maximum and the $10,330 phase-out start. + eitc: 612 + +- name: Childless filer aged 64.5 has not attained 65 and meets the age test, 2024 + period: 2024 + input: + people: + filer: + age: 64.5 + 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: + # Completed age 64 <= 64; main tested 64.5 <= 64 and failed. + eitc_demographic_eligible: true + # 7.65% x $8,000 = $612, under the $632 maximum and the $10,330 phase-out start. + eitc: 612 + +- name: Childless filer aged 65.0 has attained 65 and fails the age test, 2024 + period: 2024 + input: + people: + filer: + age: 65.0 + 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: + # Completed age 65 > 64. + eitc_demographic_eligible: false + eitc: 0 + +- name: Childless filer aged 64.5 meets the age test, 2026 + period: 2026 + input: + people: + filer: + age: 64.5 + 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: + # Completed age 64 <= 64. + eitc_demographic_eligible: true + +- name: Childless filer aged 64.5 meets the age test in 2021, with no upper limit + period: 2021 + input: + people: + filer: + age: 64.5 + 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: + # ARPA 32(n)(2) removed the upper limit for 2021. + eitc_demographic_eligible: true + +- name: Childless filer aged 80.5 meets the age test in 2021, with no upper limit + period: 2021 + input: + people: + filer: + age: 80.5 + 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: + # ARPA 32(n)(2) removed the upper limit for 2021. + eitc_demographic_eligible: true + +- name: Childless filer aged 24.5 has not attained 25 and fails the age test, 2024 + period: 2024 + input: + people: + filer: + age: 24.5 + 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: + # The minimum is unchanged: 24.5 < 25. + eitc_demographic_eligible: false diff --git a/policyengine_us/tests/test_eitc_age_ceiling_completed_years.py b/policyengine_us/tests/test_eitc_age_ceiling_completed_years.py new file mode 100644 index 00000000000..7b53b6342e9 --- /dev/null +++ b/policyengine_us/tests/test_eitc_age_ceiling_completed_years.py @@ -0,0 +1,81 @@ +"""The childless EITC age test compares completed years. + +IRC 32(c)(1)(A)(ii)(II) requires a filer without a qualifying child (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 holds +the highest completed age that qualifies, so for any age a: + +1. The age test holds exactly when min <= a and floor(a) <= max, where min + is the non-student minimum (no filer here is a student). +2. It is monotone above the minimum: once raising age fails the test, a + higher age never passes it again. + +Each example builds one simulation of single childless filers who are not +dependents, so eitc_demographic_eligible is the age test alone. +""" + +import math + +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_us import CountryTaxBenefitSystem, Simulation + +SYSTEM = CountryTaxBenefitSystem() +YEARS = [2018, 2020, 2021, 2022, 2024, 2025, 2026] +AGES = st.floats(min_value=25, max_value=80, allow_nan=False).map(lambda a: round(a, 2)) + + +def _eligible(year, ages): + people, tax_units, households = {}, {}, {} + for i, age in enumerate(ages): + people[f"p{i}"] = { + "age": {year: age}, + "is_full_time_student": {year: False}, + } + tax_units[f"t{i}"] = { + "members": [f"p{i}"], + "filing_status": {year: "SINGLE"}, + } + households[f"h{i}"] = {"members": [f"p{i}"], "state_code": {year: "TX"}} + sim = Simulation( + situation={ + "people": people, + "tax_units": tax_units, + "households": households, + } + ) + return sim.calculate("eitc_demographic_eligible", year) + + +SETTINGS = settings( + max_examples=10, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) + + +@SETTINGS +@given(year=st.sampled_from(YEARS), ages=st.lists(AGES, min_size=1, max_size=20)) +@example(year=2024, ages=[63.99, 64.0, 64.5, 64.99, 65.0, 65.5]) +@example(year=2021, ages=[64.5, 65.0, 80.0]) +def test_age_test_equals_completed_years_comparison(year, ages): + p = SYSTEM.parameters.gov.irs.credits.eitc.eligibility.age + instant = f"{year}-01-01" + min_age, max_age = p.min(instant), p.max(instant) + eligible = _eligible(year, ages) + for age, result in zip(ages, eligible): + assert bool(result) == (min_age <= age and math.floor(age) <= max_age) + + +@SETTINGS +@given(year=st.sampled_from(YEARS), ages=st.lists(AGES, min_size=2, max_size=20)) +@example(year=2024, ages=[64.0, 64.5, 65.0, 66.0]) +def test_age_test_is_monotone_past_the_ceiling(year, ages): + ages = sorted(ages) + eligible = _eligible(year, ages) + failed = False + for result in eligible: + if failed: + assert not result + failed = failed or not result diff --git a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py index 9cb742b764f..d41b90088ea 100644 --- a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py +++ b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py @@ -26,7 +26,12 @@ def formula(tax_unit, period, parameters): student = person("is_full_time_student", period) min_age = where(student, min_age_student, min_age_non_student) max_age = parameters.gov.irs.credits.eitc.eligibility.age.max(period) - meets_age_requirements = (age >= min_age) & (age <= max_age) + # IRC 32(c)(1)(A)(ii)(II): "attained age 25 but not attained age 65 + # before the close of the taxable year". The parameter is the highest + # completed age that qualifies, so compare completed years: a filer + # aged 64.5 has not attained 65. The minimum already works for + # fractional ages. + meets_age_requirements = (age >= min_age) & (np.floor(age) <= max_age) # IRC § 32(c)(1)(A)(ii)(II) applies the age test to the filer or, # on a joint return, either spouse — never to dependents. is_filer_or_spouse = ~person("is_tax_unit_dependent", period) From 395d8cd68daeab392bf17244f91b5222340f46a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 21:27:35 +0000 Subject: [PATCH 2/3] Replace EITC age-ceiling Python property test with YAML cases 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 --- ...c_demographic_eligible_fractional_age.yaml | 76 +++++++++++++++++ .../test_eitc_age_ceiling_completed_years.py | 81 ------------------- .../eligibility/eitc_demographic_eligible.py | 3 +- .../eitc/me_childless_eitc_age_eligible.py | 2 +- 4 files changed, 79 insertions(+), 83 deletions(-) delete mode 100644 policyengine_us/tests/test_eitc_age_ceiling_completed_years.py diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible_fractional_age.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible_fractional_age.yaml index bdba8ce11d2..9780664fe36 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible_fractional_age.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_demographic_eligible_fractional_age.yaml @@ -141,3 +141,79 @@ output: # The minimum is unchanged: 24.5 < 25. eitc_demographic_eligible: false + +- 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: + # Completed age 64 <= 64; rounding 64.99 to 65 would fail it. + eitc_demographic_eligible: true + # 7.65% x $8,000 = $612, under the $632 maximum and the $10,330 phase-out start. + eitc: 612 + +- 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 + # 7.65% x $8,000 = $612, under the $632 maximum and the joint phase-out start. + eitc: 612 + +- name: Joint filers aged 70 and 65 don't meet the age test, 2024 + period: 2024 + input: + people: + head: + age: 70 + is_tax_unit_head: true + ssn_card_type: CITIZEN + employment_income: 8_000 + spouse: + age: 65.0 + 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: + # Neither spouse's completed age (70, 65) is at most 64. + eitc_demographic_eligible: false + eitc: 0 diff --git a/policyengine_us/tests/test_eitc_age_ceiling_completed_years.py b/policyengine_us/tests/test_eitc_age_ceiling_completed_years.py deleted file mode 100644 index 7b53b6342e9..00000000000 --- a/policyengine_us/tests/test_eitc_age_ceiling_completed_years.py +++ /dev/null @@ -1,81 +0,0 @@ -"""The childless EITC age test compares completed years. - -IRC 32(c)(1)(A)(ii)(II) requires a filer without a qualifying child (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 holds -the highest completed age that qualifies, so for any age a: - -1. The age test holds exactly when min <= a and floor(a) <= max, where min - is the non-student minimum (no filer here is a student). -2. It is monotone above the minimum: once raising age fails the test, a - higher age never passes it again. - -Each example builds one simulation of single childless filers who are not -dependents, so eitc_demographic_eligible is the age test alone. -""" - -import math - -from hypothesis import HealthCheck, example, given, settings -from hypothesis import strategies as st - -from policyengine_us import CountryTaxBenefitSystem, Simulation - -SYSTEM = CountryTaxBenefitSystem() -YEARS = [2018, 2020, 2021, 2022, 2024, 2025, 2026] -AGES = st.floats(min_value=25, max_value=80, allow_nan=False).map(lambda a: round(a, 2)) - - -def _eligible(year, ages): - people, tax_units, households = {}, {}, {} - for i, age in enumerate(ages): - people[f"p{i}"] = { - "age": {year: age}, - "is_full_time_student": {year: False}, - } - tax_units[f"t{i}"] = { - "members": [f"p{i}"], - "filing_status": {year: "SINGLE"}, - } - households[f"h{i}"] = {"members": [f"p{i}"], "state_code": {year: "TX"}} - sim = Simulation( - situation={ - "people": people, - "tax_units": tax_units, - "households": households, - } - ) - return sim.calculate("eitc_demographic_eligible", year) - - -SETTINGS = settings( - max_examples=10, - deadline=None, - suppress_health_check=[HealthCheck.too_slow], -) - - -@SETTINGS -@given(year=st.sampled_from(YEARS), ages=st.lists(AGES, min_size=1, max_size=20)) -@example(year=2024, ages=[63.99, 64.0, 64.5, 64.99, 65.0, 65.5]) -@example(year=2021, ages=[64.5, 65.0, 80.0]) -def test_age_test_equals_completed_years_comparison(year, ages): - p = SYSTEM.parameters.gov.irs.credits.eitc.eligibility.age - instant = f"{year}-01-01" - min_age, max_age = p.min(instant), p.max(instant) - eligible = _eligible(year, ages) - for age, result in zip(ages, eligible): - assert bool(result) == (min_age <= age and math.floor(age) <= max_age) - - -@SETTINGS -@given(year=st.sampled_from(YEARS), ages=st.lists(AGES, min_size=2, max_size=20)) -@example(year=2024, ages=[64.0, 64.5, 65.0, 66.0]) -def test_age_test_is_monotone_past_the_ceiling(year, ages): - ages = sorted(ages) - eligible = _eligible(year, ages) - failed = False - for result in eligible: - if failed: - assert not result - failed = failed or not result diff --git a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py index d41b90088ea..311e62c1bf7 100644 --- a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py +++ b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_demographic_eligible.py @@ -30,7 +30,8 @@ def formula(tax_unit, period, parameters): # before the close of the taxable year". The parameter is the highest # completed age that qualifies, so compare completed years: a filer # aged 64.5 has not attained 65. The minimum already works for - # fractional ages. + # fractional ages. This reads `age` as the age at the close of the + # taxable year. meets_age_requirements = (age >= min_age) & (np.floor(age) <= max_age) # IRC § 32(c)(1)(A)(ii)(II) applies the age test to the filer or, # on a joint return, either spouse — never to dependents. diff --git a/policyengine_us/variables/gov/states/me/tax/income/credits/eitc/me_childless_eitc_age_eligible.py b/policyengine_us/variables/gov/states/me/tax/income/credits/eitc/me_childless_eitc_age_eligible.py index 07a398bb53f..4458a540ad8 100644 --- a/policyengine_us/variables/gov/states/me/tax/income/credits/eitc/me_childless_eitc_age_eligible.py +++ b/policyengine_us/variables/gov/states/me/tax/income/credits/eitc/me_childless_eitc_age_eligible.py @@ -52,7 +52,7 @@ def formula(tax_unit, period, parameters): ) meets_federal_age = ( (age >= federal_min_age) - & (age <= federal_age.max(period)) + & (np.floor(age) <= federal_age.max(period)) & is_head_or_spouse ) From 6e7f76ef5b540b83e4d954160e826652fb0dcb61 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 19:39:56 -0500 Subject: [PATCH 3/3] Test Maine's EITC against the completed-age ceiling 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 --- .../states/me/tax/income/credits/me_eitc.yaml | 28 +++++++++++++++++++ .../tests/test_me_childless_eitc_age_gate.py | 2 +- ...est_me_childless_eitc_age_gate_property.py | 6 ++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/me_eitc.yaml b/policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/me_eitc.yaml index 244cf86f664..140d6af1e00 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/me_eitc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/me/tax/income/credits/me_eitc.yaml @@ -606,3 +606,31 @@ eitc: 0 me_pro_forma_childless_eitc: 765 me_eitc: 153 + +- name: 2024 federally eligible couple aged 23 and 64.5 that does not take up the federal EITC gets no Maine credit + # The 64.5-year-old has not attained 65, so the couple fails the Maine cohort + # and takes the federal path. If the federal test goes back to the exact age, + # the cohort flag and the pro forma credit return, and me_eitc pays 306. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + head: + age: 64.5 + spouse: + age: 23 + employment_income: 8_000 + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + takes_up_eitc: false + households: + household: + members: [head, spouse] + state_code: ME + output: + me_childless_eitc_age_eligible: false + eitc: 0 + me_pro_forma_childless_eitc: 0 + me_eitc: 0 diff --git a/policyengine_us/tests/test_me_childless_eitc_age_gate.py b/policyengine_us/tests/test_me_childless_eitc_age_gate.py index 0697b02c1f2..ba3a7cf3143 100644 --- a/policyengine_us/tests/test_me_childless_eitc_age_gate.py +++ b/policyengine_us/tests/test_me_childless_eitc_age_gate.py @@ -125,7 +125,7 @@ def filers(h): def meets_federal_age(h, year): minimum, student_minimum, maximum = federal_age_limits(year) return any( - (student_minimum if student else minimum) <= age <= maximum + (student_minimum if student else minimum) <= age < maximum + 1 for age, student in filers(h) ) diff --git a/policyengine_us/tests/test_me_childless_eitc_age_gate_property.py b/policyengine_us/tests/test_me_childless_eitc_age_gate_property.py index 167b27c8ab3..b679dc0c5cf 100644 --- a/policyengine_us/tests/test_me_childless_eitc_age_gate_property.py +++ b/policyengine_us/tests/test_me_childless_eitc_age_gate_property.py @@ -13,10 +13,8 @@ # Filers are 18 or older: the model makes a married 17-year-old a dependent, # not a spouse. The YAML cases and the under-18 witness cover age 17. The -# fractional ages test "has not attained 25". None falls between 64 and 65: -# the federal test (eitc_demographic_eligible, unchanged here) compares the -# exact age with 64, and the oracle copies it. -fractional_ages = st.sampled_from([18.5, 23.5, 24.5, 24.99]) +# fractional ages test "has not attained 25" and "not attained age 65". +fractional_ages = st.sampled_from([18.5, 23.5, 24.5, 24.99, 64.5, 64.99]) spouse_strategy = st.fixed_dictionaries( { "age": st.one_of(st.integers(18, 80), fractional_ages),