Skip to content

Derive Arkansas 2026+ minus adjustments from the indexed statutory tables - #9629

Merged
MaxGhenis merged 3 commits into
mainfrom
fix-ar-reduction-uprating
Sep 27, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
fix-ar-reduction-uprating

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

From 2027, Arkansas income tax projections were internally inconsistent:

  • rates/main/rate.yaml thresholds uprate (gov.irs.uprating, rounded down to $100).
  • rates/main/reduction.yaml, the minus-adjustment scale in tax = rate × TI − reduction, had no uprating, so its thresholds and amounts stayed at their 2026 values.

As a result, 2027+ tax fell as income crossed each 2026 bound: −$111.98 at $11,200, −$63.97 at $16,000 and −$79.17 at $26,400 (and −$112 at $5,600 before the $0 floor). Tax was also wrong between the old and uprated bounds.

This PR stops storing the 2026+ minus adjustments and derives them from the tables that A.C.A. § 26-51-201(a)(4) (Act 1 of 2026) sets out and that § 26-51-201(a)(5) and (d) index each year:

  • (A) table (net income ≤ the high-income threshold): rate.yaml, taxed at marginal rates, each from its row's "From" bound.
  • (B) table (above the threshold): new high_income/rate.yaml, 2% to $4,700 and 3.7% above, applied to all of the filer's net income.
  • (C) bracket adjustment: new high_income/bracket_adjustment.yaml, $290 falling $10 per $100 row to $0. It is subtracted from (B).
  • High-income threshold: new high_income/threshold.yaml, $94,700.

ar_main_income_tax (new, in ar_income_tax_helpers.py) now computes the schedule for ar_income_tax_before_non_refundable_credits_indiv, …_joint and ar_uses_low_income_tax_tables, which each used to repeat rate × TI − reduction.

  • 2014–2025 keep the published DFA-card path behind a new switch, use_published_reduction (true through 2025). Results are unchanged.
  • 2026 results are unchanged at every whole-dollar income. The derived schedule equals Act 1 of 2026 to within 1e-12, including the −$2.66 step at $94,700 → $94,701 that the Act itself creates. The one change is for fractional taxable income strictly between a row's upper bound and the next row's start, such as $94,800.50, a gap under $1 that the statute's whole-dollar rows skip. Such income now falls in the higher row. That is the ceiling convention Fix Arkansas 2025 and 2024 income tax minus-adjustment schedules against the DFA cards #9624's own reference implementation uses.
  • 2027+ is consistent by construction. Because the reduction is derived, it also follows parameter reforms. A reform that moves a rate-table bound no longer makes tax jump at the old bound.

What § 26-51-201(d) indexes, and how it rounds

Quoted from the enacted Acts. Act 1 of 2026 changed only (a)(4) and added (a)(5); it left (d) as amended by Act 532 of 2023.

(d)(1) … The secretary shall increase the minimum and maximum dollar amounts for each rate bracket, rounding to the nearest one hundred dollars ($100), for which a tax is imposed under the table by the cost-of-living adjustment for each calendar year and by not changing the rate applicable to any rate bracket as adjusted.
— Act 532 of 2023, § 2

(5) The tables set forth in subdivisions (a)(1)-(4) of this section shall be adjusted annually in accordance with the method set forth in subsection (d) of this section.
— Act 1 of 2026 (1st Ext. Sess.), § 1

What that means for each amount, and what this PR does:

Amount Indexed? Basis
(A) row bounds Yes Bracket minimums and maximums in an (a)(4) table
$94,700 threshold Yes It is the (A) top bracket's maximum. DFA indexed it on every card: 89,600 → 92,300 (2024), 92,300 → 94,700 (2025)
(B) $4,700 2% top Yes Bracket bound. The cards' final minus adjustments imply 4,500 → 4,600 → 4,700 (87.40 = 1.9% × 4,600; 89.30 = 1.9% × 4,700)
(C) row bounds Yes, each row DFA's 2023 and 2024 cards: "Ark. Code Ann. 26-51-201(d)(1) requires each bracket to be increased annually by the cost-of-living adjustment, rounding to the nearest $100", which produced their asterisked $200 rows
(C) dollar amounts ($290 … $10) No Not bracket bounds. Every 2022–2025 card kept the enacting Act's $10 steps ($460, $430, $310 start)
Rates No "by not changing the rate"

Rounding. The statute says nearest $100, but rate.yaml rounded down, so this PR switches it to nearest. DFA's cards follow the statute. Act 532 of 2023's TY2023 table starts its rows at $5,100 / $10,300 / $14,700 / $24,300 and tops out at $87,000. The 2023 card has $5,300 / $10,600 / $15,100 / $25,000 / $89,600. That is each amount × 1.03 (the COLA cap) rounded to the nearest $100: 5,100 × 1.03 = 5,253 → 5,300. Rounding down would need a 3.9% COLA, above the 3% cap. rate.yaml brackets 5 and 6, which are .inf in every year since 2021/2022, drop their uprating metadata: core rounds nearest with Python's round, which overflows on infinity.

Row keys. Each (C) row is keyed on the round "Less Than or Equal To" bound of the row before it (94,700, 94,800, …, 97,600) and evaluated with calc(right=True). Rounding those round numbers to the nearest $100 keeps every row starting exactly one dollar above the prior bound. The first row always shares the high-income threshold's value, and a test checks this for every year.

Before and after (2027)

ar_income_tax_before_non_refundable_credits_indiv, 2027:

Taxable income Base This PR Change Why
$5,750 3.00 0.00 −3.00 The 2% row now starts at $5,800 (5,600 × 1.0298 = 5,767, nearest $100)
$11,300 2.00 110.00 +108.00 Base took 2% × 11,300 − the frozen $224
$16,200 198.00 255.00 +57.00 Base took 3% × 16,200 − the frozen $288
$26,800 544.00 614.20 +70.20 Base took 3.4% × 26,800 − the frozen $367.20
$50,000 1,482.80 1,471.40 −11.40 Derived (A) reduction is $378.60, not the frozen $367.20
$97,000 3,439.10 3,210.40 −228.70 Below the indexed $97,500 threshold, so table (A) applies; base applied the un-indexed 2026 ladder
$97,550 3,519.45 3,237.75 −281.70 First indexed (C) row ($290 adjustment); base was in the frozen ladder's last row ($10)
$150,000 5,470.10 5,468.40 −1.70 The (B) 2% top indexes to $4,800

Every integer taxable income from $0 to $200,000:

Year Incomes that change This PR higher by up to This PR lower by up to Incomes where tax falls, base …this PR
2027 194,301 $108.00 $281.70 4 (worst −$111.98) 0
2030 193,901 $102.00 $320.50 4 (worst −$111.98) 0
2035 193,301 $90.00 $361.70 4 (worst −$111.98) 0

"Base" is main before this PR: the 2027+ rate thresholds rounded down, with the minus adjustments frozen at 2026.

Invariants

policyengine_us/tests/test_ar_income_tax_schedule.py (extended from #9624) checks, at every integer taxable income:

  1. Differential. The model matches an independent transcription of each source:
    • 2021–2025: the DFA cards.
    • 2026: Act 1 of 2026.
    • 2027–2035: Act 1's tables with every bound indexed from 2026 and rounded to the nearest $100, written in the test from the Act's numbers.
    • Tolerance is half a cent.
  2. Non-negative in 2021–2040, 2050, 2075 and 2100.
  3. Never falls as income rises, same years, except where the statute's (A) and (B)–(C) tables meet. In 2026 that step is −$2.66, pinned exactly. Once indexing raises the (A) bounds, the step becomes a rise (+$7.04 in 2027).
  4. Structure, 2026–2040, 2050, 2075 and 2100:
    • the (C) ladder starts at the threshold;
    • its bounds rise in $100 multiples;
    • its amounts stay $290…$10, $0;
    • the (B) top rate equals the (A) top rate.
  5. Property tests on 200 seeded random table sets. The derived (A) tax equals the card form (rate × TI − Σ rate step × row start), is continuous at every row start, and never falls within either regime. Hypothesis is not a dependency of this repo, and uv.lock is committed, so these use seeded numpy generators instead.
  6. Reform robustness. A 2027 reform moves the 2% row to $7,000 and cuts the top rates to 3.5%. Tax still never falls except where (A) meets (B)–(C). Continuity holds at every (A) and (B) row bound; the statute's own steps remain: the seam, and +$10 at each (C) row.

The 2027 YAML cases use only incomes whose tax survives a one-step change in the projected uprating factor. The (B)–(C) rows next to the indexed threshold sit within about 0.03% of a nearest-$100 flip, and a routine C-CPI-U refresh could move them. The index-relative Python tests cover those rows instead, and test_state_uprating_rounding.py now derives its expected 2027–2035 bounds from the loaded index.

Verification

  • Base vs this branch, parameter level. Every integer taxable income from 0 to 200,000, plus 20,000 random fractional incomes, bound±0.5 points, −5, 1e6, 1e7 and 1e9:
    • 2014–2025: identical at every point.
    • 2026: identical at every whole-dollar income. The only differences are 40 fractional incomes inside the sub-$1 gaps described above.
    • 2012–2013: $0 on both sides. The switch starts at 0000-01-01, so years before rate.yaml's first value still take the published path. An independent reviewer caught that the first version raised ParameterNotFoundError there; a test now pins it.
  • Local runs on the final head:
    • test_ar_income_tax_schedule.py plus tests/core/test_state_uprating_rounding.py: 309 passed.
    • The whole tests/policy/baseline/gov/states/ar YAML folder: 718 passed.
  • Mutation testing of the new tests: the pytest suite kills all 14 mutants. The 2027 YAML cases also kill 10 of them; the three they miss only touch mid-ladder rows, which the YAML leaves to the index-relative pytest.
    • Rounding back to downwards.
    • An unindexed threshold, (B) top, ladder, or 3% row.
    • right=False on the ladder, and < for <= at the threshold.
    • DFA's "Less Than or Equal To" convention in (A).
    • One ladder amount changed, or one ladder row dropped.
    • A different (B) rate, threshold, or 2% top.
    • Restoring the old frozen-2026 reduction.
  • Adversarial review in three lenses (statute and claims, numerical, conventions and CI), with a skeptic re-running each finding:
    • Three findings confirmed, all wording: the helper docstring overclaimed continuity, the changelog said the new bounds were previously "rounded down", and the (B) description. All fixed.
    • Three more, unverified when the skeptics hit a usage limit, were checked by hand and also fixed: the withholding side effect, four 2027 YAML cases one C-CPI-U refresh from a rounding flip, and the rate-table description.
    • Five were refuted:
  • Independent review (Opus, Subfleet): APPROVE at cc053d30ff, then APPROVE on the final head's delta (e00c01eba7, the pre-2014 fix). Both reviews were read-only, so they didn't run anything; the runs above are mine.
  • Rebase. Rebased onto main after Fix Arkansas 2025 and 2024 income tax minus-adjustment schedules against the DFA cards #9624, Fix Arkansas 2014-2020 income tax rate schedules #9611, Give Arkansas 2015-2020 income tax parameters their own values #9615, Use Arkansas's own medical expense deduction floor #9612 and Use Arkansas's own miscellaneous and casualty loss itemized deduction rules #9625 merged. Main's 2025–2027 AR schedule values are the same as Fix Arkansas 2025 and 2024 income tax minus-adjustment schedules against the DFA cards #9624's head, so the before/after tables above describe main.

API note. From 2026, gov.states.ar.tax.income.rates.main.reduction has no effect: its 2025 values carry forward but go unused. Reform rates.main.rate and rates.main.high_income.* instead.

Not changed (follow-ups)

axiom: TheAxiomFoundation/rulespec-us#1418 debt (the rulespec-us AR pilot is bounded to TY2026 "because section 26-51-201(a)(5) provides for later annual adjustment"; TY2027+ indexing under (d) is unencoded)

🤖 Generated with Claude Code

@codecov

codecov Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (909176a) to head (e00c01e).
⚠️ Report is 67 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9629   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         4           
  Lines           76        67    -9     
  Branches         2         3    +1     
=========================================
- Hits            76        67    -9     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

MaxGhenis and others added 2 commits September 27, 2026 01:50
…bles

reduction.yaml had no uprating, so from 2027 its thresholds and amounts
stayed at 2026 values while rate.yaml's thresholds uprated, and tax fell by
up to $111.98 as income crossed a 2026 bound.

From 2026 (no DFA Indexed Tax Brackets card yet), compute the regular tax
from A.C.A. 26-51-201(a)(4) as amended by Act 1 of 2026: table (A) at
marginal rates up to the high-income threshold; above it, table (B) less
the (C) bracket adjustment. The threshold, the (B) 2% row top and each (C)
row bound are new parameters indexed like the rate thresholds. The
reduction is derived from the same tables, so it moves with them (and with
reforms). 2021-2025 keep the published-card schedule behind a new
use_published_reduction switch; 2026 results are unchanged.

26-51-201(d)(1) indexes bracket amounts "rounding to the nearest one
hundred dollars ($100)", so rounding moves from downwards to nearest. The
(C) dollar amounts stay fixed, since (d) indexes only bracket bounds.

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

- Helper docstring: marginal taxing keeps tax continuous at every (A) and (B)
  row bound; the statute's (A)/(B)-(C) seam and (C) row steps remain.
- (B) and rate-table descriptions say which income each rate applies to.
- Changelog: the threshold, (B) top and (C) bounds were not indexed before
  (not rounded down); the rounding change also moves withholding's bounds.
- Drop the four 2027 YAML cases next to the indexed threshold, which sit
  within ~0.03% of a nearest-$100 flip; the parameter-relative differential
  and ladder tests cover them.
- The AR rounding regression test now derives expected bounds from the loaded
  index for 2027-2035 and asserts they differ from rounding down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the fix-ar-reduction-uprating branch from f6b111e to cc053d3 Compare September 27, 2026 05:52
…ompute

The switch started in 2014, so for earlier years it was undefined and the AR
tax variables raised ParameterNotFoundError, where main returned $0.
Independent review flagged the risk; reproduced for 2012 and 2013 and pinned
with a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review September 27, 2026 06:42
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Admin-merged (squash, --match-head-commit e00c01e) under Max's 2026-09-26 ruling (d399: obvious improvements need no permission; admin-merge when the only block is a required review only Max could give). Gates on head e00c01e: gh pr checks exit 0 (37 pass), MERGEABLE, no CHANGES_REQUESTED. Approvals: an independent Opus 5.5 review on Subfleet (APPROVE at cc053d3; a delta review of the pre-2014 fix, APPROVE at e00c01e), and an in-session adversarial review with statute, numerical and conventions lenses; all its confirmed findings are fixed in cc053d3 and e00c01e. Local runs on the final head: 309 pytest, 718 AR YAML; all 14 mutants killed; 2014-2025 identical to main at every tested income.

@MaxGhenis
MaxGhenis merged commit 08d9c8f into main Sep 27, 2026
37 checks passed
@MaxGhenis
MaxGhenis deleted the fix-ar-reduction-uprating branch September 27, 2026 06:43
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.

1 participant