Repository navigation
Fix duplicate 2024 Arkansas low-income table threshold and guard scale thresholds - #9599
Conversation
…esholds The 2024 Arkansas low-income tax table for head of household and surviving spouse filers with two or more dependents keyed its $92 row at $24,200, the threshold of the $104 row. ParameterScale._get_at_instant passes each bracket to add_bracket, which sums the amounts of equal thresholds, so the 2024 scale became [(0, 0), (24200, 196), (24300, 116), ...]: AGI of $24,177-$24,200 got $0 instead of $92 and $24,201-$24,300 got $196 instead of $104. The official table (2024 AR1000F/AR1000NR instructions, PDF page 28) reads $0-$24,176 -> $0 and $24,177-$24,200 -> $92. The tables are evaluated with right=True, so the threshold is the upper bound of the $0 row: 24_176. The 2024 reference anchor now points at PDF page 28. calibration/gov/irs/soi/agi/total_agi repeated its $10,000,000-or-more bracket, which doubled the top bucket when loaded. Its 18 values match IRS SOI Table 1.1 for tax year 2020 (20in11si.xls, column 3), where the top bucket appears once. No code reads this parameter. Add YAML tests for AGI of $24,100-$24,401 for both filing statuses and a repo-level test that fails when any parameter scale has two brackets with the same finite threshold at any instant. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Program Review — PR #9599"Fix duplicate 2024 Arkansas low-income table threshold and guard scale thresholds" (author MaxGhenis). PR head Path shorthand: Source Documents
Scope: the PR's changes, plus all five 2024 Arkansas low-income tables (7 parameter files, since HoH and SS are kept as separate files). Branch StatusThe branch is 1 commit ahead of and 10 behind SummaryThe PR fixes a 2024 data error in the Arkansas low-income tax tables for head of household and surviving spouse filers with two or more dependents. The $92 row was keyed at $24,200, the threshold of the $104 row, and policyengine-core sums brackets that share a threshold. So AGI of $24,177–$24,200 got $0 and AGI of $24,201–$24,300 got $196. The PR also:
The value fix, the tests and the calibration deletion are all correct and match the official sources and the statute. There are no critical issues. Two should-address items remain: the guard exempts Arkansas table fix — verification
Scale-threshold guard — verificationFile:
Critical (Must Fix)None. Should Address
Suggestions
Pre-existing issues (outside this PR — candidates for follow-up issues)
PDF Audit Summary
Validation Summary
Review Severity: COMMENTNo critical issues. The two should-address items are non-blocking: tighten the guard's Next Steps
|
DTrim99
left a comment
There was a problem hiding this comment.
Requesting two small changes per the program review above (#9599 (comment)).
The Arkansas fix is correct: the 2024 head-of-household / surviving-spouse 2+ dependents threshold of 24,176 matches the booklet (#page=28) and the statute's 3% cap on 23,472. The PDF audit confirmed 317 values with 0 mismatches across 2021–2025. The new guard runs in CI over all 867 scales with no allowlist, and on main it flags exactly the three scales this PR fixes.
Two should-address items:
test_parameter_scale_thresholds.py:55skips duplicate-infthresholds as well as+inf. Core silently adds duplicate-infrows together, so the guard should exempt only+inf, with a small self-test.- Line 17 of both edited Arkansas files still links the 2023 booklet at
#page=25, which opens a worksheet; it should be#page=29. This PR already fixes the same mistake for the 2024 link two lines below.
…uplicate-threshold
…dd invariants Review fixes for #9599: - The guard now reads values with core's own Parameter._get_at_instant, so entries sharing a date resolve first-match as core does (an uprating start_instant can write a second entry for a date), and it counts only the brackets ParameterScale._get_at_instant passes to add_bracket (threshold and value child both non-None, value child chosen by scale type). - It checks every date any bracket child changes, not only threshold dates, since a collision can start when an amount starts. - Only +inf duplicates are exempt; -inf is a reachable bottom row and core sums duplicates there too. - Failures print one line per collision with its instant range. - Drop the self-test assertions that pinned core's summing, which policyengine-core#550 would change. - New differential test: for all 867 scales at every calendar-valid change date, the thresholds the guard sees match the scale core builds. - New exhaustive invariant tests for the Arkansas low-income tables (2021-2030): tax is non-decreasing in integer AGI up to the cutoff and infinite above it, and the head of household and surviving spouse tables agree. On main the monotonicity test fails for exactly the two 2024 tables fixed here. - Point the 2023 booklet anchor in both edited Arkansas files at PDF page 29 (the low-income tables) instead of page 25 (a worksheet). - Add YAML cases for the last 2024 rows, the $28,200 cutoff, and tax before credits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @DTrim99, that's a thorough review. 24de218 addresses both requested changes, plus several suggestions and two findings from a separate Opus review. The branch now also merges current Requested changes
Suggestions taken
From the independent review (Opus 5.5)
New invariant tests
On Not in this PR: fractional AGI at row edges. The independent review found that $24,176.25 now reaches the $92 row, while the instructions say to round every amount to the nearest dollar (2024 instructions, PDF page 12). That affects every row edge of every table on Locally: the guard tests pass 6/6, the invariant tests 91/91, and |
…uplicate-threshold
- The differential test now records every threshold core passes to add_bracket (repeats included) and compares that multiset with the guard's list. Comparing the built scale's de-duplicated thresholds could not see a disagreement about a bracket at an already-taken threshold, which is the case the guard decides. - The tied-date self-test skips instead of failing if core's uprating ever stops writing the tied entry it reproduces, since CI upgrades core. - The Arkansas table invariants now start in 2015, now that main has the 2015-2020 tables (#9615); they hold for every year 2015-2030. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Update at e237a74: merged current
@DTrim99, both of your requested changes (the |
Program Review — Round 2This re-review covers the author's changes since round 1: commits 24de218 and e237a74. Merges 6cb224c and c25a41a are clean. They brought in main's #9494 and #9615, which are not the author's work.
Path shorthand:
Round-1 items
Round 1's 11 pre-existing items were not re-audited: they are outside the diff and unchanged. The author deferred them to a separate PR, together with the fractional-AGI rounding gap at row edges that the author disclosed. The SOI vintage item moved to #9604. New findingsCriticalNone. Should addressNone. Suggestions
Guard verificationThis section uses policyengine-core 3.32.7, the version CI upgrades to. Its scale-building code is byte-identical to the local 3.26.0.
Value audit
CIAll 35 checks pass on
Partner tests ( Branch statusPR branch is 4 commit(s) behind main. Consider rebasing before merging. Review was scoped to the PR's actual changes — staleness did not affect findings. Review Severity: APPROVEBoth round-1 should-address items are resolved, no new critical or should-address issues were found, and the three open round-1 suggestions and three new suggestions are optional. |
DTrim99
left a comment
There was a problem hiding this comment.
Approving per the round-2 review (#9599 (comment)). Both round-1 should-address items are resolved:
- The guard now mirrors core: it exempts only duplicate
+infthresholds, and its self-test covers both cases. Checked against policyengine-core 3.32.7's bracket construction. With a bug planted, it flags a duplicate at 24,200 and a duplicate-inf, and lets a duplicate+infthrough as intended. - The 2023 booklet anchor now points to #page=29.
All values hold: the 317 from round 1 are unchanged, the newly covered 2015 tables match the booklet (347/347, plus 1,644/1,644 across 2016–2020), and all 35 checks pass.
Optional suggestions: the differential test keeps about 61 MB of cache alive; the shortened failure message implies an end date; and the changelog says the test fails when a table "stops rising", but it only fails when tax falls.
…uplicate-threshold Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged on gates under Max's 2026-10-02 rule that obvious bug fixes merge without his approval, whatever they do to aggregates (cos card closed with "merge on gates").
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fused-URL half of this branch (0ccab71) landed on main as #9786, with its own test_no_concatenated_reference_urls.py. Every file that commit touched now carries main's version: 189 were already identical, 46 differ and take main's. The only file kept from it is this branch's runtime test, tests/code_health/test_reference_urls.py, which also covers reform modules and parameter references. The Arkansas DFA anchor commit (e11a9d2) and the parameter-metadata commit (6a7b430) are re-applied on top of main: - tuition weighted-average files: main's new 2015-2020 values and references, plus this branch's "Tuition"/"education" title fixes. - low-income tables (two or more dependents): main (#9599) already has the 2023 p29 and 2024 p28 anchors; re-applied the 2025 TaxTables_FI title. - rates/main/{rate,reduction}.yaml: main already points the 2025 booklet at p30; main's version. - ar_misc_deduction_{indiv,joint}.py: main (#9625) already replaced the AR1075 citation with the statute and the 2025 AR3 Lines 20-25 instructions; main's version. - MA HECS prior-year cost threshold: moved main's reference block, with main's archived FY2023/FY2025 URLs (#9765), under metadata. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
calibration/gov/irs/soi/agi/total_agi.yaml.+inf) at any instant, plus YAML tests covering AGI from $24,100 to $24,401 for both filing statuses.The bug
ParameterScale._get_at_instantpasses each bracket toadd_bracket, andAmountTaxScaleLike.add_bracketadds the amount to an existing row when the threshold is already present (read in policyengine-core 3.30.2, the version inuv.lock). Before this PR, the 2024 scale was:ar_low_income_tax_jointevaluates it withright=True, so:The official 2024 AR1000F/AR1000NR instructions (PDF page 28, Rev 09/23/2024) list $0–$24,176 → $0 and $24,177–$24,200 → $92. With
right=True, each threshold is the inclusive upper bound of the preceding published row, so the first bracket's 2024 threshold is 24_176. This matches the 2024 single (14_265) and HoH/surviving spouse 0–1 dependent (20_282) tables. The duplicate predates 875b73a (#9068) and d8a4317, which both edited this line without catching it. This PR also moves the 2024 reference anchor from#page=24(now the self-employed health insurance worksheet in DFA's current PDF) to#page=28, and the 2023 anchor in both files from#page=25(the Inflationary Relief worksheet) to#page=29.Verification against the official tables
I parsed every Arkansas low-income table from the DFA PDFs for 2021 (PDF pp. 26–27), 2022 (pp. 29–30), 2023 (pp. 29–30), 2024 (pp. 28–29), and 2025 (pp. 28–29), plus the standalone
TaxTables_FI_2024.pdfandTaxTables_FI_2025.pdf. I parsed deterministically with pdfplumber and checked that each column is contiguous from $0 to its "Above $X" cutoff. Then I comparedscale.calc(agi, right=True)from the loaded model against the published tax for every integer AGI from $0 to cutoff + $200:Microsimulation impact
Default dataset (
populace_us_2024.h5@populace-us-2024-spm-20260909), tax year 2024,main(615a0f2) against this branch withmainmerged in. One real run of each, on policyengine-core 3.32.12, compared record by record.ar_income_tax): $2.694B on both. No tax unit changes (0 of 79,729 records).ar_low_income_tax_joint: identical for all 166,321 person records.This is a household-accuracy fix. It does not move the aggregate in this dataset. The
total_agicalibration parameter has no reader, so removing its duplicate bracket changes no output.soi/agi/total_agitriage20in11si.xls, column 3, "Adjusted gross income less deficit"). The "$10,000,000 or more" row appears once, so the second bracket is a copy.soi.long_term_capital_gains), policyengine.py, and policyengine-api, and ran an org-wide GitHub code search. The only reference is an inventory CSV in ai-inequality. The duplicate has been in the file since ca1bbd0 (2023-09-19).The new tests
Threshold guard.
policyengine_us/tests/test_parameter_scale_thresholds.pywalks everyParameterScalein the loaded system (869 scales). At every date on which any bracket child changes (including uprated values), it lists the brackets thatParameterScale._get_at_instantwould pass toadd_bracketand flags two with the same threshold. A bracket counts only when its threshold and its value child are both non-None, with the value child chosen by scale type exactly as core chooses it. Values come from core's ownParameter._get_at_instant, so entries sharing a date resolve first-match as in core. Duplicate+infthresholds are exempt, because tables that change length park unused brackets there;-infis a reachable bottom row, so ties there are flagged. Onmainit fails with exactly three lines, one per affected scale.+infexempt,-infflagged; a bracket core skips because its amount has not started yet; and an upratingstart_instanttie reproduced throughuprate_parameters, in both directions.add_bracket.add_bracketbisect-inserts brackets, so out-of-order YAML (for examplegov.states.la.tax.income.credits.school_readiness.rate, listed 5→1) evaluates correctly.Arkansas table invariants.
.../states/ar/tax/income/low_income/test_ar_low_income_tax_tables.pychecks every integer AGI for all 7 tables and every year from 2015 (the first yearmainnow has tables for, via #9615) to 2030.Invariants
These hold for every input and are tested exhaustively rather than by sampling:
+infat any instant. Core would silently sum the two rows. Enforced over all 869 scales at every change date.add_bracket. The differential test records those calls rather than comparing the built scale, whose de-duplicated thresholds would hide a disagreement over a bracket at a threshold that is already taken.mainthis fails for exactly the 2024 HoH and SS 2+ tables, where $196 was followed by $116.Tests
mainfail with exactly the three scales.main, 2 fail.policyengine-core test .../states/ar/tax/income/low_income(35 YAML cases, including Give Arkansas 2015-2020 income tax parameters their own values #9615's 2015–2020 cases, plus the 145 invariant cases it also collects): 180/180 pass after mergingmain, on policyengine-core 3.32.7, the version CI upgrades to.policyengine-core test policyengine_us/tests/policy/baseline/gov/states/ar -c policyengine_uspassed 498/498, andtest_parameter_files.pyplustest_system_import.pypassed.main, Cases 19, 20 and 23 fail; Cases 21 and 22 (the last 2024 rows and the cutoff) pass there too.Out of scope
total_agi.yamlkeys the tax year 2020 values at2015-01-01, and its reference reads "IRS SOI PUF 2015 (20in11.xls)" with no href. Rekeying changes the uprated values, so it is in its own PR, Key IRS SOI AGI-by-size calibration amounts to tax year 2020 and fix SOI citations #9604.ar_low_income_tax_jointlooks up unrounded AGI. So a fractional AGI just past any row edge gets the next row: onmain, head of household with 0–1 dependents in 2024, $20,282.25 gets $65 where the rounded $20,282 gets $0. That affects every table and year, so it is a separate fix.gov.states.ny.nyserda.drive_clean.amounthas two entries dated2021-06-31, which is not a real date; the differential test skips it. That is also a separate fix.ParameterScale, not inadd_bracket, becauseMarginalRateTaxScale.combine_bracketrelies on the summing: Reject duplicate finite thresholds within a single parameter scale policyengine-core#550.axiom: TheAxiomFoundation/rulespec-us#1425 queued
🤖 Generated with Claude Code