Skip to content

Model the IRS local sales tax worksheet with official locality rates - #9879

Open
MaxGhenis wants to merge 23 commits into
mainfrom
local-sales-tax-official-rates
Open

MaxGhenis wants to merge 23 commits into
mainfrom
local-sales-tax-official-rates

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Replace 0.2 × state_sales_tax with the IRS State and Local General Sales Tax Deduction Worksheet, lines 2–6: local tables/selector and the ratio method. Official SST, New York and Virginia files cover 21 states; elsewhere the local amount defaults to $0 unless a rate is entered, under Max's ruling d984 (2026-10-06). No Tax Foundation-derived data are included, enforced by test_l0_official_sources_only.

The hub's population impact, by state, is under "Hub microsimulation impact". It was rerun at the current head 76ebba54fa against main at 6a92d6a983. The national totals are within 0.4% of the earlier run at 724a8fbe11; Mississippi's rows moved by less than 0.1%. Mississippi methodology is resolved under Max's ruling d1590, reading B: absent local tables use the ratio method; Jackson and Tupelo retain their tables.

The law

What changes

Variables (variables/gov/local/tax/sales/):

  • combined_sales_tax_rate (household, input-able): place → county remainder with known place/block → county population average → state average; heading rate without official data.
  • local_sales_tax_rate (household, input-able): line 3 in percentage points, max(combined − official_state_component, 0) / 0.01; heading fallback and CA/NV thresholds.
  • local_sales_tax_table (household): place → county → state default.
  • local_sales_tax (tax unit): worksheet line 6.

Both lookups share tools/local_sales_tax_geography.py: normalize codes, reject out-of-state GEOIDs, resolve county FIPS → block county → supplied county enum → place principal county; supplied NYC/Yonkers flags infer missing places. Calculated defaults are ignored across branches. The generated crosswalk covers all Census places in the 21 rate states and single-county places elsewhere: 30,924 places, including 13,550 population-weighted places in the 21 states.

Parameters (gov.irs.deductions.itemized.salt_and_real_estate): Tables A–D, 2022–2025 (all 1,824 cells independently checked), selector state/default/county/place lists with named Census codes, and heading rates. Tables uprate after 2025. The 2022–2025 references identify all supporting PDF pages once per annual document, including state_sales_tax_table/tax.yaml.

Data/build: data/local_sales_tax/rates.csv: 7,720 step-function rows/4,448 county-place keys/21 states; populations.csv: matching Census weights; state_rates.csv: dated state components; place_counties.csv: generated crosswalk; SOURCES.md: sources/method/limits. scripts/local_sales_tax_rates/build.py checks ny_pub718.csv against the publication and vt_local_option.csv against Vermont dates. tools/local_sales_tax_rates.py lazily caches annual rates. The manifest records source/output hashes, logs and parser audits: 19 retained boundary files, 20,414,368 accepted records, 0 malformed rows skipped; 489 omitted empty final types padded and retained.

Sources and licences

States Source Terms
AR GA IA KS MN NC ND NE NV OH OK SD TN UT VT WA WI WV WY Each state's SST rate and boundary files (latest posted on 2026-10-06), which states provide under SSUTA sections 305-307 No licence on the files. The SST Technology Guide summarizes SSUTA section 307: "Database must be provided at no cost to the user of the database." The site footer reads "© Copyright 2018 Streamlined Sales Tax Governing Board, Inc."
IN KY MI NJ RI The same files No local tax; the IRS sends these residents to line 6 = 0, so the files are not used.
NY Publication 718 (2/25) and 718-A (12/25), NYS Department of Taxation and Finance No licence. The disclaimer offers the site "as a public service" and states no reuse restriction.
VA Sales and Use Tax Rates by Locality by Date, Virginia Tax No licence. The disclaimer offers it "as a public service"; the footer reads "Copyright © 2019 Virginia Department of Taxation. All rights reserved."
Places and populations 2020 Census P.L. 94-171 block records U.S. Government work, not subject to copyright (17 U.S.C. 105).

None of the rate sources carries an open licence. The PR ships only the rates and Census codes derived from them, which are facts set by state and local law, with attribution, never the source files.

Method and known limits

  • SST: sum state/county/place/up to 20 district components; average the finest records (address → ZIP+4 → ZIP). Districts retain coverage weight: Wichita 7.506%, ordinary state-plus-county 7.5%; unit averages need not equal address rates.
  • Dates/weights: quarterly and other observed 2022–2026 changes, including Nashville 2025-02-01. Blocks use place → subdivision → county; population-weight place/remainder rates and prune identical places. Principal county = largest population slice, deterministic FIPS ties.
  • 2025 examples: Seattle 10.35%, Tacoma 10.3%, Spokane 9.1%; Minneapolis 9.025%, St. Paul 9.875%; Memphis/Nashville 9.75%; Atlanta 8.9%, Tulsa 8.517%, Oklahoma City 8.625%; Salt Lake City 8.45%, Fayetteville AR 9.75%, Lincoln 7.25%; Milwaukee 7.9%, Las Vegas 8.375%, NYC/Yonkers 8.875%.
  • Rutland: recode 2022 town records 61225 → 61300 despite the resolving city code; synthetic 6% city unit has zero record weight. City tax begins July 2023, town April 2009. Validate Vermont dates, including Montgomery's October 2025 rescission; log tolerances/missing units.
  • Retained limits: Iowa lacks place codes after its 2024 rebuild (Des Moines 6.83% versus 7%); Vermont taxing towns outside places use county-remainder averages and uncoded records limit precision.
  • Historical staging: 2015–2021 backdate 2022 IRS tables/selector/headings and first locality rates. Add the IRS 2015-2021 local sales tax tables, selector and heading rates #9911 handles historical IRS data/instructions, including CA's 2015–2016 threshold; pre-2022 locality history is separate.
  • CSV state components: MN 6.875% versus 6.88% heading; SD 2023 4.348767% versus 4.35%; NY/VA 4%/4.3%. Line 4 retains the heading parameter; parameter reforms/API queries cannot independently alter/expose CSV components.
  • Geography: nationwide county/block fallback; principal counties include pruned places in the 21 states, only single-county places elsewhere. Other multi-county places need county/block input.
  • Rebuilds: posted files change; --download may alter historical outputs. Exact reproduction needs retained hash-matching inputs. Validate 89-field records, allow empty trailing fields, normalize omitted unused final types, log malformed-width counts/examples, fail broken quoting. V3 caches pin source/data hashes and audits; compare/explain historical CSV changes.
  • Mississippi: under Max's ruling d1590 (reading B), localities other than Jackson and Tupelo select NONE for an absent table: worksheet line 2 = 0, and line 6 uses the ratio method with Mississippi's 7% state rate. The 2025 Schedule A instructions, p. 18 list "City of Jackson only" for Table C and "City of Tupelo only" for Table A; those city tables are retained.

Invariants

The worksheet/locality Python tests cover:

  • Method partition/disjoint selectors; non-negativity, zero without rates and state-component thresholds (Hypothesis), including zero-tax official localities.
  • Line-3 linearity/monotonicity and equivalent combined/local inputs (Hypothesis); monotone rows/family sizes, capped at six.
  • Raw-YAML differential: all states/rows/tables, sizes 1–8, five rates, 2022–2026/2030; Table D = 25% of NY (2025 within $1).
  • FIPS/weights/key equality, every covered county, day-by-day proration (Hypothesis), average bounds/component floors with heading-rounding tolerance.
  • Shared geography/precedence/provenance/branch/crosswalk checks; lightweight parser/cache audit/hash tests without model construction.

Tests

Reviewed baseline 944686e0d4: 15/8/25/26 YAML cases (combined/local rate/selector/local amount), 40 worksheet/23 locality/89 state-table Python tests and 4,448 data keys, correcting the original body's counts.

New cases: Rutland dates/backdating/remainder; NYC seven-digit GEOID, Atlanta short place and DeWitt/Arapahoe short counties; invalid geography and block/enum/flag/place-only precedence; Vermont aliases/boundaries, Core provenance/branches, generated crosswalks and CSV/ZIP parser/cache failures.

Hand calculations:

  • Rutland city: 2022 6%, $572 × 0/6 = $0 (was $95.33); 2019 $525 × 0/6 = $0 (was $87.50). 2023 (181×6% + 184×7%)/365 = 6.50410959%; $766 × (184/365)/6 = $64.36. 2024 7%.
  • Rutland remainder 2022: (273×6.2167% + 92×6.2192%)/365 = 6.21733014% after town records move out of the city.
  • NYC 3651000/alternate inputs: 8.875%/A, $120 × 4.875 = $585; Yonkers flag/place-only 8.875%/B, $142 × 4.875 = $692.25.
  • Short counties: DeWitt 5001 → 05001, 6.5% + 1% + 3.5% = 11%; Arapahoe 8005 → 08005, Table B in 2022 versus Colorado's default A.
  • Atlanta/DeKalb 4000, 2024: 8.9%/B, the other-locality table. Westchester without city 8.375%/B, $142 × 4.375 = $621.25; no location retains NY 8.5411545063%/D, $135 × 4.5411545063 = $613.06.
  • Pruned/crosswalk places: Savannah 7%/B, $142 × (7 − 4) = $426; Baton Rouge 5%/B, local rate 0 without an entered rate. An out-of-state place GEOID leaves NY at 8.5411545063%/D. Out-of-state enum/block inputs and NY city flags leave TX at 6.25%/A, with local rate 0.

Required file checks for d1fabb8: each changed file separately, foreground; OMP_NUM_THREADS, OPENBLAS_NUM_THREADS, MKL_NUM_THREADS, VECLIB_MAXIMUM_THREADS, NUMEXPR_NUM_THREADS = 4, OMP_WAIT_POLICY=PASSIVE.

File Passing count Wall time
combined_sales_tax_rate.yaml 26 583.6 s
local_sales_tax_rate.yaml 8 414.1 s
local_sales_tax_table.yaml 28 253.0 s
local_state_tax.yaml 29 1058.9 s
test_local_sales_tax_worksheet.py 40 638.7 s
test_locality_sales_tax_rates.py 33 409.1 s
test_state_sales_tax_table.py 89 1000.1 s
test_local_sales_tax_rate_build.py 9 227.2 s

Rate, selector and provenance files passed after the resolver/crosswalk changes; parser tests passed after the source-audit change. The remaining worksheet/state/amount files passed at the final head. After the runtime checks, edits affected documentation and the builder.

Formatting/lint: uvx ruff@0.15.5 format and check, and make format, passed.

Local cost for the expanded locality file: 409.1 s wall including setup; peak RSS was not collected. The previous attempt's 23-test baseline took 483.9 s; host conditions differ, and no baseline RSS was recorded. The added Core provenance/branch checks reuse the existing model; parser tests construct no model. These Python files remain in Rest's existing remaining group; no CI runner was added. These are local macOS figures, not Linux CI measurements.

CI cost, from this PR's CI run artifacts as read in the round-2 review: in Rest shard-1's remaining group the sales tax Python files take 49.4 s of test time (worksheet 40 tests in 23.1 s, state table 89 in 20.4 s, locality 33 in 5.5 s, build 9 in 0.4 s), and the group took 14:55 wall at a peak RSS of 7.93 GB, against 14:46–19:19 and 7.84–7.89 GB for three sibling PRs that day. In the microsimulation group the county-carryover test takes 0.8 s for 4 tests; the group's peak RSS is 14.16 GB against 14.07–14.09 GB on siblings (about +75 MB). No CI job was added.

Original main 4c900b6d3d failed 11/26 local-amount cases (NYC $107.80, Tacoma/Texas $200); new rate variables were absent. Original checks also passed unchanged state-sales-tax, CTC liability, Hawaii SALT and NY itemized-deduction files.

Hub microsimulation impact (current head)

A real microsimulation on the default dataset, main at the merge base 6a92d6a983 against this PR's head 76ebba54fa, for 2025 and 2026. The runs went one at a time under the shared heavy-job lock. Sums are weighted.

Output 2025 change 2026 change 2025 records changed (weighted) 2026 records changed (weighted)
local_sales_tax -1.27bn -1.10bn 57,960 (134,662,101) 57,947 (135,616,664)
state_sales_tax +0 +0 0 (0) 0 (0)
salt_deduction -3.00bn -3.02bn 23,302 (62,032,566) 23,490 (63,104,051)
state_income_tax -2,913 -2,418 31 (4,338) 25 (5,943)
income_tax +49.74M +48.62M 892 (2,300,928) 838 (2,197,320)
household_net_income -49.73M -48.62M 886 (2,243,567) 832 (2,145,770)
household_benefits +0 +0 0 (0) 0 (0)

Every total in the table matches the state breakdown's "ALL" row, which is computed separately from household sums. Local sales tax changes in 36 states and moves both ways. In 2025 New York gains $5.6bn, Georgia $2.1bn and Washington $1.1bn, while California loses $3.8bn, Texas $2.7bn and Florida $2.1bn. The SALT deduction falls most in Texas (−$2.5bn) and Florida (−$2.0bn). Federal income tax rises most there too (+$59M and +$44M) and falls most in Washington (−$44M). State income tax moves in 4 states, by less than $10,000 in total.

By state: weighted change, summed to households, states with any change
State local_sales_tax 2025 local_sales_tax 2026 salt_deduction 2025 salt_deduction 2026 income_tax 2025 income_tax 2026
NY +5.65bn +5.91bn +744.36M +770.08M 0 0
CA -3.83bn -4.00bn -960.20M -991.14M +539,713 +577,435
TX -2.69bn -2.81bn -2.54bn -2.66bn +58.99M +58.19M
GA +2.13bn +2.29bn +340.40M +432.75M -4 -25
FL -2.11bn -2.20bn -1.96bn -2.04bn +44.42M +41.69M
WA +1.12bn +1.24bn +1.04bn +1.14bn -43.90M -42.37M
IL -1.10bn -1.15bn -62.90M -70.54M 0 0
NC +1.07bn +1.16bn +195.58M +217.77M 0 0
PA -1.02bn -1.06bn -79.10M -81.91M 0 0
OK +834.52M +873.85M +162.93M +190.79M -92,629 0
AZ -578.87M -604.85M -159.16M -161.75M +28 +15
TN +548.48M +573.29M +514.02M +537.39M -9.31M -9.53M
SC -427.72M -447.25M -109.28M -169.31M 0 +159,789
MO -371.78M -390.77M -70.78M -73.47M 0 0
VA +368.56M +385.62M +38.51M +35.46M 0 0
MS -361.45M -377.36M -123.20M -135.55M +35,292 +91,145
OH +358.80M +395.74M +115.56M +125.57M -54,459 -62,510
UT +348.14M +376.52M +67.27M +70.09M 0 0
AL -320.96M -335.18M -34.21M -35.67M 0 0
LA -305.63M -320.26M -96.28M -100.27M +27,808 0
CO -255.40M -268.11M -24.48M -26.63M 0 0
AR +231.65M +238.18M +48.09M +51.62M 0 0
ID -215.34M -225.42M -50.93M -52.73M 0 0
KS +205.74M +219.87M +16.70M +18.89M 0 0
NM -169.23M -177.14M -53.64M -56.14M +5 +5
HI -137.48M -143.47M -19.45M -24.16M 0 0
WI -128.97M -132.24M -12.68M -10.62M 0 0
WV -119.69M -124.95M -19.70M -22.21M 0 0
IA -70.14M -69.24M -16.23M -15.89M 0 0
MN -53.33M -48.70M -2.55M -1.77M 0 0
ND +49.11M +54.46M +35.58M +39.50M -169,488 -97,021
SD +43.83M +45.85M +42.49M +44.48M +89,522 +365,223
WY +33.67M +29.05M +32.10M +27.94M -642,541 -364,355
VT -28.69M -28.73M -2.88M -2.92M 0 0
NE +24.06M +25.69M +4.62M +4.16M 0 0
NV +1.74M +1.93M +1.93M +1.66M -190,075 -38,718
ALL -1.27bn -1.10bn -3.00bn -3.02bn +49.74M +48.62M

Comparison files are in fixes/9879-impact/ on the hub host.

Coverage and household examples

The 16 local-tax states without an official locality file here are AL, AK, AZ, CA, CO, FL, HI, ID, IL, LA, MO, MS, NM, PA, SC and TX. Without an entered rate their local amount becomes $0, replacing main's 20% of the state amount. This is a coverage regression under d984, particularly relevant in no-income-tax Texas/Florida; it does not mean their localities impose no tax.

Houston, 2025, single/$85,000: state amount $849; main local $169.80, PR default $0, entered 8.25% worksheet $849 × 2 / 6.25 = $271.68. Houston FY2025 adopted budget, PDF p. 8 confirms that combined rate. Actual SALT/tax effects depend on itemization, the income-tax election and the SALT limit.

Controlled household probes use one unweighted single person per state, age 35, $85,000 wages, $5,000 property tax and $20,000 deductible interest. The comparison substitutes main's old local formula on the same merged model; these examples do not estimate population impacts.

Household Old local formula PR default local SALT change Income-tax change
Houston, TX (48201/35000) $169.80 $0.00 $-169.80 $+37.36
Miami, FL (12086/45000) $167.20 $0.00 $-167.20 $+36.78
Tacoma, WA (53053/70000) $189.40 $553.63 $+364.23 $-80.13

Official follow-up sources include Florida DOR's DR-15DSS county surtaxes and CDTFA's rates/downloads.

Follow-ups

  • Hub population/state impact table for S1: done; see "Hub microsimulation impact", including the by-state table.
  • Historical IRS tables/instructions/California thresholds in stacked Add the IRS 2015-2021 local sales tax tables, selector and heading rates #9911; pre-2022 locality-rate histories separately.
  • Iowa place-level local option taxes when an official place-coded file is available; Vermont municipality resolution outside Census places/uncoded records.
  • Official rate files for the 16 missing states, beginning with Florida/CDTFA; exposing state components as parameters and broader principal-county coverage.
  • Mississippi methodology: resolved under Max's ruling d1590, reading B; absent tables use the ratio method, while Jackson and Tupelo retain their tables.

Response to David Trimmer's review

  • C1 — fixed: recode Rutland town/city, validate Vermont dates, regenerate rates; test city/remainder/transition/backdating and builder boundaries.

  • S1 — fixed: documented Houston's regression and the TX/FL/WA household probes; the hub's state SALT and income tax microsimulation is in "Hub microsimulation impact", with a by-state table.

  • S2 — fixed: 2022–2025 heading/state-table parameters and worksheet variables cite each annual document once with all supporting PDF pages; pre-2022 metadata is unchanged.

  • S3 — paths/imports specified: rulespec-us#1402 identifies worksheet targets and same-year table imports; d236, signed-import and corpus/release/approval blockers remain.

  • S4 — fixed: both lookup YAML files cover NYC 3651000 → 8.875%/A and Atlanta 4000 in DeKalb → 8.9%/B in 2024, plus short/invalid geography coverage.

  • 1 — resolved under Max's ruling d1590 (reading B): Mississippi localities other than Jackson and Tupelo select NONE for an absent table, so worksheet line 2 = 0 and line 6 uses the ratio route with the 7% state rate: $1,243 × 1 / 7 = $177.57 (2025/$85,000/family 1/1% local), replacing the $179 Jackson proxy. Jackson retains Table C and Tupelo retains Table A, as specified by the 2025 Schedule A instructions, p. 18. Without an entered rate the local amount remains $0.

    d1590 validation at b35e82324a1d9c81865d18546937279f70ccc1a4: 31 selector, 32 local amount and 40 worksheet tests passed in separate foreground processes with four-thread caps. The worksheet grids now include NONE without additional model constructions or CI jobs and remain in the existing remaining group. Local worksheet wall time including setup: 188.85 s; peak RSS: 1.64 GiB. The prior body reports 638.7 s for 40 worksheet tests; host conditions differ and prior peak RSS was not recorded.

  • 2 — fixed: shared supplied-input geography, generated principal counties, precedence/invalid/branch tests and coverage limits.

  • 3 — retained deferrals: Iowa averages (Des Moines 6.83% versus 7%) and Vermont outside-place/uncoded limits; Rutland fixed separately.

  • 4 — retained staging: backdate 2022 IRS/first locality rates to 2015–2021; Add the IRS 2015-2021 local sales tax tables, selector and heading rates #9911 covers historical IRS data/instructions, locality history separately.

  • 5 — documented: weighted small districts retained, Wichita 7.506% versus ordinary 7.5%; no new cutoff.

  • 6 — documented: unrounded dated CSV components, MN/SD differences, parameter/API limitation; no migration.

  • 7 — fixed: audit all 19 files/20,414,368 records; retain 489 SD records by padding their omitted unused final type, with full collapsed-input equality to the legacy parser; log malformed skips, reject broken quoting, preserve audits in hash-pinned caches, and test failures/normalization; document current downloads/historical-output review.

  • Hub review P2 — fixed: when a dataset supplies a county for an earlier year and the county formula carries it into a later year, the resolver now treats that county as supplied, so it uses the county's rate and table instead of the state's average. Regression test: test_local_sales_tax_county_carryover.py.

  • P2 test fix: handle NONE in the state-table test lookup and masks so absent local tables use line 1 × line 3 / line 4; Jackson C and Tupelo A stay covered. All 11 requested files pass (171 Python + 272 YAML = 443 tests).

Response to round 2 (David Trimmer, 2026-10-23)

  • N1 — fixed: rulespec-us#1402's two worksheet comments (2022–2025, 2015–2021) now carry the no-table outcome. The selector returns A–D or no table; a line-2-state locality that no selector row covers has line 2 = 0; line 6 is then line 1 × line 5. The TY2015–2021 Jackson-proxy sentence is replaced, and companion case 10 is Mississippi 2025, $85,000, family size 1, line 3 = 1: 1,243 × 1 / 7 = 177.57 (1,243 and 7.00% from the 2025 instructions, p. 14). Each year's instructions from 2015 to 2025 carry "If there is no table for your locality, the table amount is considered to be zero" (checked in each PDF).
  • N2 — fixed: main is merged (5a1896f844), and 7003e15c8b deletes the local_sales_tax.py entry from KNOWN_UNMARKED. The test's own comment scanner, run on the merged tree's copies of the PR's 21 files under variables/ and parameters/, finds no unmarked conflict-sounding comment in local_sales_tax.py and none gained in any other file.
  • N3 — fixed: default_table/{a,b,c,d}.yaml read "The IRS has residents of these states use Optional Local Sales Tax Table A unless the selector names their city or county" (B, C, D likewise), and none.yaml reads "The IRS provides no Optional Local Sales Tax Table for residents of these states unless the selector names their city."
  • Suggestion 1 — done: the impact section's title names the head it measured, and the S1 line reads fixed. The rerun at the current head replaces the table.
  • Suggestion 2 — done: CI cost figures are under "Tests".
  • Suggestion 3 — done (76ebba54fa): build.py records output_sha256 for all four shipped CSVs, manifest.json carries them (the place_counties.csv hash is unchanged), and test_manifest_hashes_every_shipped_csv checks that the manifest names exactly the shipped CSVs and that each hash matches. It fails against the previous manifest.
  • Suggestion 4 — done: the changelog fragment says the local amount is now $0 in the 16 uncovered local-sales-tax states without an entered rate, where it was 20% of the state table amount.
  • Suggestion 5 — done: SOURCES.md, none.yaml and the two YAML test comments cite the 2025 instructions' p. 5 sentence instead of decision ids; the ids stay in this body.
  • Suggestion 6 — deferred to a separate PR: moving the stricter _stored_input into tools/general.py changes a shared helper that main uses elsewhere.
  • Add the IRS 2015-2021 local sales tax tables, selector and heading rates #9911 (stacked) still gives Mississippi's other localities the City of Jackson row's table for 2015–2021; that PR will move them to no table, as here.

axiom: TheAxiomFoundation/rulespec-us#1402 queued

TheAxiomFoundation/rulespec-us#1402 tracks the tables/worksheet with IRS companion cases: us/form/irs/ty<YYYY>/i1040sca-line-5a-worksheet.yaml (TY2018–2025), .../i1040sca-line-5-worksheet.yaml before 2018. Same-year imports await d236/signed bindings and corpus/release/re-pin/gated promotion. Locality rates are line-3 data inputs. No billed encoder run is authorized.

🤖 Generated with Claude Code

Replace the 20%-of-state-amount approximation in local_sales_tax with the
State and Local General Sales Tax Deduction Worksheet: the Optional Local
Sales Tax Tables (with the selector's named counties and cities) in the
line-2 states and the ratio method elsewhere, with line 3 the combined rate
above the state table heading rate.

Combined rates come only from official state files: the Streamlined Sales
Tax rate and boundary files, New York Publication 718 and 718-A, and
Virginia Tax's rates by locality, weighted to Census 2020 places. Where no
official file exists the combined rate defaults to the heading rate, so the
local amount is 0 unless the user enters a combined or local rate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 6, 2026 11:10
…ne 3

Address the independent review of #9879:
- Recode SST place codes that match no Census place or county subdivision of
  their county by the records' postal city, or by the code the same ZIP
  codes carry after the records end. This fixes the DeKalb County part of
  Atlanta (Georgia code 05000; 8.0% before, 8.9% now) and Vermont's 2022
  town taxes (older town codes, and town-wide taxes under CDP codes). In
  Vermont codes resolve to towns. The build logs every recoding and every
  code left unmatched.
- Write each covered state's general sales tax rate (state_rates.csv) and
  take worksheet line 3 as the combined rate less it, so rounded or averaged
  headings (Minnesota 6.88%, South Dakota 2023 4.35%) no longer shift
  line 3. California and Nevada keep the IRS rule: the combined rate above
  the heading.
- List New York City's five counties as Table A counties.
- Accept 7-digit place GEOIDs; document the county_fips/place_fips keys,
  the pre-2022 backdating and the remaining limits (Iowa, Vermont towns
  outside places, a few Utah codes).

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

DTrim99 commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Model the IRS local sales tax worksheet with official locality rates

PR #9879 (author: MaxGhenis, head 944686e0d4). The PR is still a draft, so treat this review as early feedback. It covers the three commits from 2026-10-06 (082ee8c9c1, aa8499986b, 944686e0d4).

Summary

The PR replaces PolicyEngine's placeholder local general sales tax (0.2 × state_sales_tax) with lines 2–6 of the IRS State and Local General Sales Tax Deduction Worksheet. It adds:

  • three household variables:
    • combined_sales_tax_rate, the locality rate lookup;
    • local_sales_tax_rate, worksheet line 3;
    • local_sales_tax_table, the "Which Optional Local Sales Tax Table Should I Use?" selector;
  • Optional Local Sales Tax Tables A–D for 2022–2025, the selector's named counties and cities by FIPS code, and the state-table heading rates (line 4);
  • official combined rates for 4,448 county-place keys in 21 states, from the SST rate and boundary files, NY Publication 718 and Virginia Tax's locality workbook. scripts/local_sales_tax_rates/build.py builds them.

The worksheet logic and the IRS tables are right.

  • All 1,824 local-table cells, every selector entry and all 47 × 4 heading rates match the PDFs.
  • More than 40 locality spot checks against official sources found one mismatch, C1.
  • The code is vectorized and light. The CSVs (7,721 rate rows) load lazily, once per process, and are cached per year.
  • On the extracted head tree, all 74 YAML cases in the four touched files pass, as do 40 worksheet property tests, 23 data tests and 89 state-table tests. Eleven expectations check by hand.
  • CI is 35/35 green, and the merge with main is clean.

What blocks approval:

  • C1 (critical). Rutland city, Vermont, gets a 1% local option tax in 2022, and through backdating in 2015–2021. The city first levied it in July 2023.
  • Four should-address items:
    • S1: the unquantified drop to $0 in the 16 local-tax states without official files, Texas and Florida among them;
    • S2: page anchors that don't show the values they cite;
    • S3: no module path for the worksheet module in the axiom issue;
    • S4: untested input-normalization branches.

There are seven optional suggestions.

Critical (must fix)

C1. Rutland city, Vermont, is at 7% for 2022 (and 2015–2021) instead of 6%.

  • Where: policyengine_us/data/local_sales_tax/rates.csv:6285-6287:

    VT,50021,61225,2022-01-01,0.070000
    VT,50021,61225,2023-01-01,0.060000
    VT,50021,61225,2023-07-01,0.070000
    
  • Source: Vermont Department of Taxes, Local Option Tax. Under "Municipalities that have a 1% Local Option Sales Tax" it lists "Rutland City July 2023" and "Rutland Town April 2009".

    • The city had no local option sales tax before July 1, 2023, so its combined rate in 2022 was 6%.
    • The 2023 rows are right: 6%, then 7% from 2023-07-01.
  • Likely cause:

    • The 2022 boundary records for Rutland Town seem to carry a code that resolves to the 2020 Census Rutland city subdivision (61225).
    • Because the code resolves, the alias and successor logic in build.py never runs, and manifest.json logs no recoding for Rutland.
    • The data fit this. The Rutland County remainder, which contains Rutland Town (rates.csv:6237-6240), is 6.087% in 2022 and 6.212% from 2023-01-01. Yet the Department's list shows no town in that county starting a tax in January 2023.
  • Head-tree reproduction (single filer, $85,000 wages, county_fips 50021, place_fips 61225):

    Year combined_sales_tax_rate Line 3 state_sales_tax local_sales_tax Correct
    2022 7.000% 1.000 572 95.33 0
    2019 (backdated) 7.000% 1.000 525 87.50 0
    2023 6.504% = (181 × 6 + 184 × 7) / 365 0.504 766 64.36 64.36 ✓
  • Reach:

    • About 15,800 people live in Rutland city (populations.csv).
    • Vermont has an income tax, so only households that elect the sales tax are affected.
    • The same mechanism understates Rutland Town residents in 2022.
    • Even so, this is a confirmed mismatch with the official source in a year the PR models.
  • Fix:

    • Correct the 2022 Rutland mapping. Either recode by successor or postal city even when the old code resolves, or add an explicit 2022 override.
    • Add a build-time cross-check of Vermont units against the Department's list and its effective dates. The list is official and short.
    • Add YAML cases: Rutland city (50021/61225) gives 0.06 in 2022 and 0.07 in 2024. The only 2022 Vermont rate pinned by a test today is East Middlebury's.

Should address

S1. In the 16 local-tax states without official files, the local amount is now $0. The PR doesn't quantify that change, and the impact section is still a placeholder.

  • Where:
    • combined_sales_tax_rate.py:52-55: where(official.isna(), heading_rate, ...);
    • local_sales_tax.py:58-75.
  • Law: worksheet line 3 is the household's local general sales tax rate. Line 6 is "line 2 × line 3" in the line-2 states and line 1 × line 5 elsewhere (2025 Instructions for Schedule A, p. 4).
  • What changes:
    • Sixteen states have local general sales taxes but no official file here: AL, AK, AZ, CA, CO, FL, HI, ID, IL, LA, MO, MS, NM, PA, SC and TX.
    • Unless the user enters a rate, local_sales_tax there is 0. On main it was 20% of the state amount.
  • Head-tree probe (Houston, 48201/35000, 2025, single filer, $85,000):
    • state_sales_tax is 849, and local_sales_tax is 0.
    • main gives 169.80.
    • The worksheet at Houston's 8.25% gives 849 × 2 / 6.25 = 271.68.
  • Why it matters: this follows Max's ruling d984, and the PR documents it. But Texas and Florida have no income tax, so many households there elect the sales tax, and the PR moves them further from the IRS amount than main did. Nothing in the PR says how large that effect is.
  • Fix:
    • Before marking the PR ready, run the microsimulation. Report the change in salt_deduction and income tax by state (at least TX, FL and WA), and state the regression against main in the body.
    • Optionally, note official follow-up sources that d984 would admit:
  • PR body nits: the test counts at the head are 15 combined_sales_tax_rate cases (not 13), 8 local_sales_tax_rate cases (not 7), 25 local_sales_tax_table cases (not 24) and 23 test_locality_sales_tax_rates.py tests (not 22). The rates cover 4,448 county-place keys (not 4,431).

S2. Several #page= anchors don't show what they cite.

  • Heading rates, state_sales_tax_table/rate.yaml:9-16. Each year cites only the first state-table page, which shows the headings for Alabama through Illinois. The other jurisdictions' headings are on:

    • 2022: pp. 14–16;
    • 2023: pp. 14–16;
    • 2024: pp. 13–15;
    • 2025: pp. 14–17.

    For example, Washington's "6.50%" is on 2025 p. 17, and Texas's is on 2025 p. 16.

  • local_sales_tax.py:33-37, "Worksheet line 2 and 6 instructions":

    • In 2023, "Line 2. If you checked the 'No' box, enter -0- on line 2…" is on p. 4. The cited p. 6 has lines 3 and 6.
    • In 2024 and 2025, p. 5 has lines 2 and 3. "Line 6. If you lived in more than one locality…" is on 2024 p. 6 and 2025 p. 6.
  • Fix: use the reference convention main adopted after this branch was cut (Document the reference page-citation convention #10009, CLAUDE.md "Parameter and variable references"): one entry per source document.

    • In rate.yaml, merge each year's heading entry and line-4 entry into one. The href opens the first cited page, and the quoted title ends with the page list, for example "... #page 5, 13-16" for 2022.
    • In local_sales_tax.py and local_sales_tax_rate.py, keep one href per year, with a # PDF pages 4-6 comment above it.
    • main's state_sales_tax_table/tax.yaml (Correct the IRS Optional State Sales Tax Table and add 2022-2025 values #9616) uses the same first-page pattern and could be fixed in a follow-up.

S3. Axiom: rulespec-us#1402 gives no module path for the worksheet module this PR mirrors.

  • What's there: Adding Oregon taxable income deductions #1402 is OPEN and labelled pe-parity. It has:

    • the verbatim § 164(b)(5)(H)(i);
    • corpus citations;
    • required outputs;
    • IRS-sourced companion tables.

    The 2026-10-06 12:42 UTC comment adds the worksheet's required outputs, a pasteable review_finding and nine companion cases. They match this PR's YAML: NYC 585.00, Yonkers 692.25, Chicago 480.00, WA 584.62, NV 222.63, CA 0 / 310.34, MA 0 and 1.189.

  • What's missing: the issue gives a path only for the per-state table modules ("option A … Adjust the path if d236 picks another option"). It gives none for the worksheet module (plan step 3), and the module shape (d236) is still open.

  • Fix: add the worksheet module's path and imports to Adding Oregon taxable income deductions #1402 or to the comment, for example us/form/irs/ty<YYYY>/i1040sca-line-5a-worksheet.yaml, so the item is dispatch-ready on the Add AMT parameters for 2020-2023 #1467 model. Otherwise the line matches the PR's behavior. Treating the locality rates as data inputs to line 3 is a reasonable carve-out.

S4. The input-normalization branches have no tests.

  • Where: combined_sales_tax_rate.py:35-39 and local_sales_tax_table.py:47-54. Both variables:
    • accept a 7-digit place GEOID (state FIPS code plus place code), stripping or adding the state prefix;
    • zero-pad short codes, for example county "6037" or place "4000".
  • The gap: every YAML and Python test passes 5-digit, zero-padded strings, so a regression in either branch would go unnoticed. That would send households silently to the state default rate or table.
  • Fix: add two cases to each file:
    • NYC with place_fips: "3651000", which should give 0.08875 and Table A;
    • Atlanta in DeKalb with county_fips: "13089" and place_fips: "4000", which should give 0.089 in 2024 and the state's other table.

Suggestions

All are optional.

1. Mississippi localities outside Jackson and Tupelo default to Jackson's table, but the IRS logic points to the ratio method.

  • Where: default_table/a.yaml:19-27 and default_table/c.yaml:40.
  • Law:
    • The 2025 selector names only "City of Tupelo only" (A) and "City of Jackson only" (C) (p. 18).
    • The worksheet instructions say: "If there is no table for your locality, the table amount is considered to be zero" (p. 5, multi-locality paragraph).
    • With line 2 at 0, line 4 sends the household to line 1 × line 3 / line 4.
  • Reach: only a user-entered Mississippi rate, since no Mississippi locality rates are modeled. The YAML comment documents the choice.
  • Fix: drop MS from the default lists. Then the default_table descriptions could name the IRS instead of "PolicyEngine assigns…".

2. Some location inputs are ignored by one or both lookups.

  • Where: combined_sales_tax_rate.py:44-52 and local_sales_tax_table.py:44-57.

  • Head-tree probes (NY, 2025, single filer, $85,000). With NYC's county and place codes, the result is 120 × 4.875 = 585.00. Other ways of locating the same household give:

    Input Combined rate Table local_sales_tax
    county: KINGS_COUNTY_NY (enum only) NY average, 8.5412% D 135 × 4.5412 = 613.06
    in_nyc: true only 8.5412% D 613.06
    place_fips: "51000" with no county 8.5412% A 120 × 4.5412 = 544.94
  • The inconsistency: with a place but no county, the table lookup resolves the place (state FIPS code plus place code), but the rate lookup falls back to the state average.

  • Reach: Populace always carries county_fips, place_fips and block_geoid, so the microsimulation is unaffected. These are household-calculator and API inputs.

  • Fix: when county_fips is empty:

    • take the county from block_geoid[:5];
    • otherwise use county if it was entered as an input;
    • otherwise treat in_nyc and in_yonkers as places 3651000 and 3684000;
    • resolve a place with no county through its principal county.

3. Two data limits are documented deferrals. Both are reasoned and documented in SOURCES.md ("Known limits"):

  • Iowa: from mid-2024, local option taxes are averaged over each county. Des Moines gets 6.83% instead of 7%, so local_sales_tax is 120.14 instead of 144.83 for the probe household. Places without the tax get part of it.
  • Vermont: a household in a taxing town but outside every Census place takes its county's remainder rate.

4. 2015–2021 use the 2022 tables, selector, heading rates and locality rates (local_sales_tax.py:16-18). This is documented and handled by stacked PR #9911. Two known differences:

  • California's line 3 threshold was 7.5% in 2015–2016.
  • Several headings differ, for example Kansas 6.3264% in 2015 and Louisiana 4.7227% in 2018.

5. Record-weighted unit averages pick up small special districts (build.py:290, :385-389). For example, Wichita (20173/79000) is 7.506%, against the statutory 6.5% + 1% Sedgwick County = 7.5%. The effect is tiny. Either document it, or drop districts that cover only a small share of a unit's records.

6. The state general rates live in a CSV rather than a parameter.

  • Where: local_sales_tax_rate.py:48-51 reads line 3's base from state_rates.csv. The New York and Virginia values are typed into build.py:80 (STATE_RATES = {"NY": 0.04, "VA": 0.043}).
  • Why it matters: these are statutory rates, but they sit outside the parameter tree, so reforms and the API can't see them.
  • Scale: they differ from the heading parameter only for Minnesota (6.875% against 6.88% from 2023) and South Dakota in 2023 (day-weighted 4.3488% against 4.35%).
  • Fix: a state-breakdown parameter with statutory references, or a line in SOURCES.md explaining why it's a CSV.

7. The build can't be reproduced once the posted files change.

  • Where:
    • --download (build.py:131-143) takes whatever SST files are posted, and the SST site replaces them each quarter.
    • The sources are deliberately not shipped, for licensing reasons, so the manifest's SHA-256 values can verify a set of inputs but can't recover them.
    • on_bad_lines="skip" (build.py:255) drops malformed boundary rows without a trace.
  • Fix:
    • Log the number of skipped rows per file in manifest.json.
    • Say in SOURCES.md that a rebuild picks up newer files.
    • Consider a check that pre-existing dates' rates don't move between builds unless the log says why.

Verified correct

  • Law and worksheet (from the stage-A source audit, re-spot-checked here):

    • 26 U.S.C. 164(b)(5)(H)(i) (LII): "(II) the amount determined under tables prescribed by the Secretary".
    • Line 6 of the 2022–2025 worksheet (2025 p. 4; 2022 p. 5) is:
      • 0 in the ten no-local jurisdictions;
      • line 2 × line 3 in the line-2 states (15 in 2022; AL and KS added from 2023);
      • line 1 × line 3 / line 4 elsewhere.
    • The family-size column is capped at 6 ("Over 5"), and the state table's income row is reused.
  • Line 3 (local_sales_tax_rate.py):

    • California and Nevada count only the part above 7.25% and 6.85% (2025 p. 5).
    • Rates are day-prorated, with 366 days in 2024. The 1.189 example reproduces.
    • Using the official state rate gives the true local rate where the heading is rounded (MN) or averaged (SD 2023).
  • IRS data:

    • All 1,824 local-table cells match the PDFs, checked by stage A with an independent word-box parse.
    • I re-read the 2025 row "$80,000–$90,000" on p. 18: A 120, B 142, C 179, D 135 for family size 1.
    • Every selector entry, all 47 × 4 heading rates, and the 2024 selector on p. 16 and 2025 selector on p. 18 match.
    • Chicago is Table A in 2025 ("…Chicago, … or any other locality that imposes a local sales tax A").
  • Locality rates: more than 40 checks against official sources found one mismatch, C1. They cover:

    • NY Pub. 718;
    • all 133 Virginia localities × 6 periods;
    • Washington, Tennessee, Utah, Minnesota and Wisconsin change notices;
    • Georgia's quarterly charts;
    • the Vermont list.

    populations.csv sums to each state's 2020 Census total.

  • Code patterns:

    • Household entity for the rate and table variables, TaxUnit for local_sales_tax, all YEAR.
    • Vectorized pandas maps over FIPS keys, the same pattern as hud_fair_market_rent.
    • No policy values in the formulas. The / 0.01 converts line 3 to percentage points, and TAX_UNIT_SIZE_CAP = 6 mirrors state_sales_tax.py.
    • Parameter metadata (unit, period, label, breakdown, uprating: gov.irs.uprating for the post-2025 tables) is complete.
    • Every new function is used.
    • Lint passes.
  • Performance, memory and packaging:

    • rates.csv (7,721 rows), populations.csv (4,448) and state_rates.csv (21) load on the first formula call, are cached per year by lru_cache, and are read through importlib.resources.
    • They are tracked and not git-ignored, so the hatch wheel (packages = ["policyengine_us"]) and MANIFEST.in (recursive-include policyengine_us *) include them. The "Unpublished release wheel" and smoke-import jobs pass.
    • The microsimulation check passes. Populace assigns block_geoid, county_fips and place_fips as strings, with "" for no place (microcosm geography_ladder.py), which is the form the lookup expects.
  • Callers at the head: only state_and_local_sales_or_income_tax and hi_salt_deduction read local_sales_tax. Nothing outside the sales tax folder reads the three new variables. No partner test is touched, and the partner suite passes.

  • Tests run on the extracted head tree, one file per process (local policyengine-core 3.26):

    • combined_sales_tax_rate.yaml 15/15;
    • local_sales_tax_rate.yaml 8/8;
    • local_sales_tax_table.yaml 25/25;
    • local_state_tax.yaml 26/26;
    • test_local_sales_tax_worksheet.py 40/40 (86 s);
    • test_locality_sales_tax_rates.py 23/23 (84 s);
    • test_state_sales_tax_table.py 89/89 (53 s).
  • Test quality:

    • Hypothesis runs with derandomize=True and no example database.
    • The differential test (I6) covers every state, family size 1–8, income row, table and five rates for 2022–2026 and 2030.
    • L6 covers the fallback order: place, then the area outside places (with the place or block known), then county average, then state average. It also covers out-of-state counties.
    • The CA/NV threshold is tested at 7.25% / 6.85% and one basis point above.
    • Test periods are plain, and the YAML has no parameter overrides.
  • Hand checks (11):

    1. NYC 2025: Table A 120 × (8.875 − 4) = 585.00.
    2. Yonkers 2025: Table B 142 × 4.875 = 692.25.
    3. Chicago 2025: Table A 120 × (10.25 − 6.25) = 480.00.
    4. Tacoma 2025: 1,000 × (10.3 − 6.5) / 6.5 = 584.62.
    5. Clark County 2025: 1,000 × (8.375 − 6.85) / 6.85 = 222.63.
    6. Texas with a user rate: 1,000 × 2 / 6.25 = 320.00.
    7. Minneapolis 2024 (probe): 820 × (9.025 − 6.875) / 6.88 = 256.25.
    8. Sioux Falls 2023, line 3: (181 × 6.5 + 184 × 6.2) / 365 − (181 × 4.5 + 184 × 4.2) / 365 = 2.000.
    9. Suffolk 2025: (59 × 8.625 + 306 × 8.75) / 365 = 8.72979%.
    10. Rutland city 2023: (181 × 6 + 184 × 7) / 365 = 6.5041%, and 766 × 0.5041 / 6 = 64.36 (the head matches).
    11. Kings County via the enum only: Table D 135 × 4.5412 = 613.06 (the head matches; see Suggestion 2).
  • Changelog: changelog.d/local-sales-tax-official-rates.added.md is a top-level file of type added, and its text matches the change.

Axiom line assessment

The PR body has axiom: TheAxiomFoundation/rulespec-us#1402 queued.

CI status

  • All 35 checks pass at 944686e0d4 (run 37485646654, completed 2026-10-06 19:34 UTC). That covers every Baseline and Contrib shard, Rest (Python + variables), Microsimulation, Household API Partners, Lint, the changelog check, the smoke imports for 3.11–3.14 and the release-wheel builds.
  • The run predates the 195 main commits since the merge base. The merge is clean (below), and none of those commits touch the sales tax deduction code.

Branch status

  • The branch is 195 commits behind main and 3 ahead. The merge base is 4c900b6d3d.
  • git merge-tree --write-tree PolicyEngine/main 944686e0d4 is clean, with no conflicts.
  • Since the merge base, no main change touches:
    • the sales tax variables or parameters;
    • county_helpers.py;
    • the tests that compute local_sales_tax.
  • The main tests that mention local_sales_tax set it as an input. Changes to main's SALT phase-out and its new § 911 salt_deduction case don't involve the sales tax.
  • main's reference convention (Document the reference page-citation convention #10009) landed in that window, which is why S2's fix follows it.
  • Before merge: fix C1, address S1–S4, fill in the impact section, rebase, and let CI re-run.

Verdict: REQUEST_CHANGES

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Early feedback on the draft (details in the comment above). The worksheet logic matches § 164(b)(5)(H)(i), and all 1,824 local-table cells, the selector and the heading rates match the IRS PDFs. One critical: rates.csv:6285-6287 puts Rutland city, VT at 7% in 2022, and by backdating in 2015-2021, but its 1% local option tax began in July 2023, so 2022 local_sales_tax should be 0, not 95.33. Four should items: the 16 local-tax states without official files now get $0 local (Houston 2025: 0 here vs 169.80 on main and 271.68 per the IRS), so quantify the regression; the rate.yaml and local_sales_tax.py page anchors; the worksheet module path missing from rulespec-us#1402; and tests for the 7-digit place GEOID and zero-padding branches.

@MaxGhenis
MaxGhenis marked this pull request as ready for review October 16, 2026 07:15
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Thanks, David. Every item from your 10-13 review is answered at af4958ea8f. The body's "Response to David Trimmer's review" section lists each one.

  • Fixed: C1, S2, S4 and items 2 and 7. S3's paths and imports are specified in Encode 26 USC 164(b)(5)(H) optional sales tax tables, TY2015-2025 TheAxiomFoundation/rulespec-us#1402.
  • Item 1 (Mississippi's other localities): resolved, reading B. Localities other than Jackson and Tupelo now select NONE, so worksheet line 2 is 0 and line 6 uses the ratio route with the 7% state rate. For 2025, $85,000, family size 1 and a 1% local rate, that is $1,243 × 1 / 7 = $177.57, replacing the $179 Jackson proxy. Jackson keeps Table C and Tupelo keeps Table A (2025 Schedule A instructions, p. 18). Without an entered rate the local amount stays $0.
  • S1: the by-state microsimulation in the body is at 724a8fbe11. In 2025 the SALT deduction falls $3.01bn and federal income tax rises $49.9M, most of it in Texas and Florida. A rerun at af4958ea8f, which includes the Mississippi change and a main merge, is queued behind a shared host reservation. I'll replace the table when it finishes.
  • Items 3–6 are documented or kept as staged follow-ups.
  • County carryover: a county that a dataset supplies for an earlier year now carries into later years, with a regression test.
  • CI passes (27/27).

Could you take another look?

@MaxGhenis
MaxGhenis requested a review from DTrim99 October 23, 2026 18:17
@DTrim99

DTrim99 commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 2): Model the IRS local sales tax worksheet with official locality rates

PR #9879 (author: MaxGhenis), head af4958ea8f. The last review was at 944686e0d4. Since then there are 15 author commits (Rutland fix, references, geography resolver, parser audits, county carryover, Mississippi ratio method, test fixes) and two clean merges of main.

Summary

All five prior blockers are fixed.

  • C1: Rutland city is 6% before July 2023.
  • S1: the hub microsimulation now quantifies the change by state.
  • S2: the references follow the one-entry-per-document convention.
  • S3: rulespec-us#1402 names the worksheet module path.
  • S4: the normalization branches are tested.

The Mississippi suggestion was adopted in its IRS reading: localities without a table now take the ratio method. The geography suggestion was implemented in full.

I checked the data again by sampling, against official sources, with no mismatches:

  • 18 localities across NY, VT, WA, TN, OH and AR;
  • 15 place-to-county mappings;
  • the population totals of 7 states.

I recomputed the NY state average and the Rutland remainder from the CSVs. The Python tests are justified and cheap in CI. The data adds about 240 KB to the wheel. CI is 27/27 green.

Three new should-address items remain. None changes a household result.

  • N1: the axiom issue doesn't match the head's absent-table (Mississippi) behavior.
  • N2: on the tree merged with main, the source-conflict ratchet fails, because the PR removes the one comment that main's KNOWN_UNMARKED entry for local_sales_tax.py counts.
  • N3: the default-table parameter descriptions, including the new none.yaml, don't follow the house template, and "PolicyEngine assigns" is no longer accurate.

There are six optional suggestions.

Prior findings

Finding Status Evidence
C1 Rutland city 7% in 2022 (and backdated) Fixed rates.csv now has VT,50021,61225,2022-01-01,0.060000 and …,2023-07-01,0.070000. The 2022 town records are recoded 61225 → 61300 (manifest log line), and the Rutland County remainder and place 49450 now agree with their 2023 values. vt_local_option.csv lists 38 municipalities. Each name and date matches the Vermont Department of Taxes Local Option Tax list, including "Rutland City July 2023", "Rutland Town April 2009" and "Montgomery October 2022 (Rescinded as of 10/1/2025)". The build checks 480 unit/date rates against it. YAML pins 2022 at 6%, 2023 at 6.5041%, 2024 at 7% and 2019 (backdated) at $0. Builder tests cover the alias and the date bounds.
S1 $0 regression in 16 states unquantified Fixed The body has a hub microsimulation for 2025/2026 with a by-state table. In 2025 the SALT deduction falls $3.01bn and income tax rises $49.9M, most of it in TX (+$59.0M) and FL (+$44.6M); WA is −$43.9M. It also has Houston/Miami/Tacoma household probes. SOURCES.md opens with "Coverage regression against main". The run is at 724a8fbe11, before the Mississippi change and the last merge. Neither affects these totals, because Mississippi has no rate data and its local amount stays $0 (Suggestion 1).
S2 #page= anchors Fixed rate.yaml has one entry per annual PDF with quoted page lists: #page 5,13-16 (2022), 5,13-16 (2023), 4,12-15 (2024), 4,13-17 (2025). state_sales_tax_table/tax.yaml titles carry the table page ranges. The variables keep one href per year with # PDF pages 4-6. I checked the 2025 PDF: worksheet on p. 4, line 2/3 instructions on p. 5, line 6 instructions on p. 6, and state headings on pp. 13–17 (Washington's 6.50% on p. 17). In the 2022 PDF the worksheet is on p. 5 and the line instructions on p. 6.
S3 No worksheet module path in #1402 Fixed, but see N1 The issue body (updated 2026-10-15) adds us/form/irs/ty<YYYY>/i1040sca-line-5a-worksheet.yaml (TY2018–2025) and …-line-5-worksheet.yaml (TY2015–2017). It also adds the import table for the state and local table modules and records d236 as an explicit dispatch blocker.
S4 Untested normalization branches Fixed Both lookup files now cover NYC 3651000 → 8.875% / A and Atlanta 4000 in DeKalb (2024) → 8.9% / B. combined_sales_tax_rate.yaml adds DeWitt 5001 → 11%, and local_sales_tax_table.yaml adds Arapahoe 8005 → B (2022). Both cover out-of-state GEOIDs, counties and blocks, and invalid FIPS.
Sugg. 1 Mississippi default to Jackson's table Fixed default_table/none.yaml lists MS. local_sales_tax sets line 2 to 0 for NONE and routes to line 1 × line 3 / line 4. This follows the 2025 instructions, p. 5: "If there is no table for your locality, the table amount is considered to be zero." Jackson keeps C and Tupelo A from 2024; Jackson keeps A and Tupelo C in 2022–2023, which I checked by word position on 2022 p. 17. The description wording is now N3.
Sugg. 2 Ignored location inputs Fixed The shared tools/local_sales_tax_geography.py resolves location in this order: county FIPS, then block, then a supplied county enum, then the place's principal county. Supplied in_nyc/in_yonkers fill a missing NY place, and calculated defaults are ignored. A 12-household YAML case and the L9 tests pin the precedence. The county-carryover regression test covers dataset years.
Sugg. 3 Iowa / Vermont outside-place limits Deferred with reason Documented in SOURCES.md. For example, a Rutland Town household located only by block gets the county remainder, 6.217% in 2022, instead of 7%.
Sugg. 4 2015–2021 backdating Deferred with reason Documented and staged in #9911 (see Branch status).
Sugg. 5 Special-district weighting Declined with reason Documented. Wichita is 7.506% against the ordinary 7.5%.
Sugg. 6 State rates in a CSV Declined with reason Documented. build.py still types NY 4% and VA 4.3%.
Sugg. 7 Build reproducibility Fixed The parser audits 19 files and 20,414,368 records and logs malformed rows (0 skipped). It pads the omitted final type on 489 SD records, rejects broken quoting and keeps hash-pinned audits. Nine parser tests build no model. See also Suggestion 3.

New findings

Should address

N1. Axiom: rulespec-us#1402 doesn't match the head's absent-table rule.

  • What the head does: local_sales_tax_table can return NONE. For Mississippi localities other than Jackson and Tupelo, line 6 is line 1 × line 3 / line 4, as in the YAML case 1,243 × 1 / 7 = 177.57.
  • What Adding Oregon taxable income deductions #1402 specifies:
    • The 2026-10-06 comment, to which the 2026-10-15 worksheet section defers, gives the selector output as "optional_local_sales_tax_table_letter (A-D)". It gives line 6 as "line 2 × line 3 in the line-2 states", with no case for a locality without a table.
    • The 2026-10-06 16:52 comment for TY2015–2021 still says "Mississippi has no other-locality row. PE uses the City of Jackson row's table."
    • An encoder following the issue would reproduce the Jackson proxy that this PR removed (commit a1df044684, 2026-10-20, after the issue's last update).
  • Law: the 2025 Instructions for Schedule A, p. 5 say "If there is no table for your locality, the table amount is considered to be zero". Worksheet line 6 on p. 4 reads "Did you enter -0- on line 2? … Yes. Multiply line 1 by line 5."
  • Fix: update Adding Oregon taxable income deductions #1402.
    • Add a no-table outcome to the selector's required outputs, and route line 6 to line 1 × line 5 when line 2 is 0 because the locality has no table.
    • Replace the TY2015–2021 Jackson-proxy sentence.
    • Add an IRS-sourced companion case: MS, 2025, income $85,000, family size 1, a locality other than Jackson or Tupelo, line 3 = 1. Line 1 is 1,243 (2025 p. 14), line 4 is 7.00%, and line 6 is 177.57.

N2. On the tree merged with main, the source-conflict ratchet fails (merge blocker).

  • Background: main's policyengine_us/tests/code_health/test_source_conflicts.py (Tie source-conflict markers to the register shared with Axiom #10287) was merged on 2026-10-20 at 22:02 UTC, after this PR's CI run started at 15:04 UTC. It isn't on the branch.
  • The entry: KNOWN_UNMARKED lists "policyengine_us/variables/gov/local/tax/sales/local_sales_tax.py": 1. That count is for main's comment "# Elsewhere the worksheet uses the Optional Local Sales Tax Tables", which matches the worksheet … uses pattern.
  • What the PR does: it rewrites that file. I ran the test's comment scanner (Python tokens and the PyYAML scanner) on the 22 scanned files the PR adds or changes in the merged tree:
    • local_sales_tax.py now has 0 conflict-sounding comments;
    • no other PR file gains one.
  • The failure: test_conflict_sounding_comments_do_not_grow fails on its "shrunk" assertion ("lower or delete their KNOWN_UNMARKED counts").
  • Fix: after merging main, delete the local_sales_tax.py entry from KNOWN_UNMARKED, and let CI re-run. This is the same class of failure as Follow state statutes where forms differ: NM medical exemption, VT CDCC, NY/NYC household credit, PR EITC #10300.

N3. The default-table descriptions don't follow the house template, and "PolicyEngine assigns" is no longer accurate.

  • Where:
    • local_sales_tax_table/default_table/{a,b,c,d}.yaml read "PolicyEngine assigns residents of these states to Optional Local Sales Tax Table A unless their locality's table is given."
    • The new default_table/none.yaml reads "Residents of these states have no Optional Local Sales Tax Table unless the IRS selector names their locality."
  • Why it's wrong now: with the Mississippi proxy gone, every default comes from an IRS selector row such as "Any (other) locality that imposes a local sales tax". The sibling lists already use the IRS-led form ("The IRS has residents of these counties use …").
  • Fix:
    • For a–d: "The IRS has residents of these states use Optional Local Sales Tax Table A unless the selector names their city or county."
    • For none.yaml: "The IRS provides no Optional Local Sales Tax Table for residents of these states unless the selector names their city."

Suggestions

All are optional.

  1. Refresh the hub table and its heading. The section is titled "Hub microsimulation impact (current head)", but it was measured at 724a8fbe11. When the queued rerun at af4958ea8f finishes, replace the table. Until then, retitle it, for example "(measured at 724a8fb)". The "Response" S1 bullet still opens "S1 — incomplete" while the follow-up list says "done".
  2. Add the CI cost figures (CLAUDE.md "Measure simulation-heavy additions"). The body gives local macOS figures only. From this run's artifacts:
    • Rest shard-1 "remaining" group:
      • The sales tax Python files take 49.4 s of test time: worksheet 40 tests in 23.1 s, state table 89 in 20.4 s, locality 33 in 5.5 s, build 9 in 0.4 s.
      • The group took 14:55 wall time at a peak RSS of 7.93 GB.
      • Three sibling PR runs the same day took 14:46–19:19 at 7.84–7.89 GB.
    • Microsimulation group:
      • The new carryover test takes 0.8 s for 4 tests.
      • The group's peak RSS is 14.16 GB, against 14.07–14.09 GB on siblings, about +75 MB. This group already has the least headroom.
    • No job was added.
  3. Record output hashes for every shipped CSV. manifest.json's output_sha256 covers only place_counties.csv. Add rates.csv, populations.csv and state_rates.csv, so that a reviewer can tie the shipped rates to the logged inputs. The Rutland fix regenerated rates.csv without a recorded hash.
  4. Say in the changelog fragment that the local amount changes. Without an entered rate, it now falls to $0 in the 16 uncovered local-tax states (TX, FL, CA, IL and others), replacing 20% of the state amount. Users in no-income-tax Texas and Florida will see this change.
  5. Cite sources instead of internal decision IDs in shipped text. SOURCES.md ships in the wheel, and it and two YAML test comments cite "Max's ruling d1590" and "d984". Cite the IRS sentence instead ("If there is no table for your locality, the table amount is considered to be zero", 2025 p. 5) and keep the decision IDs in the PR body.
  6. Consider sharing the provenance helper. _stored_input in local_sales_tax_geography.py reimplements most of main's has_input_for_period (tools/general.py), adding Core's _is_input check. Moving the stricter version into tools/general.py would keep one copy of the private-Core logic (_get_visible_branch_names, _user_input_keys, _get_array_from_storage), which main already relies on elsewhere.

Verified

  • Worksheet logic (new code), against the 2025 Instructions for Schedule A, pp. 4–6:
    • The ten no-local jurisdictions "skip lines 2 through 5, enter -0- on line 6".
    • The line-2 states (AL, AK, AZ, AR, CO, GA, IL, KS, LA, MS, MO, NY, NC, SC, TN, UT, VA in 2025) use line 2 × line 3 when a table is selected. With no table (NONE), they use line 1 × line 3 / line 4.
    • The other states use line 1 × line 5.
    • California and Nevada count only the part above 7.25% and 6.85%.
    • Line 3 is combined − official state rate, day-weighted (MN 6.875% against the 6.88% heading; SD 2023). Line 4 is the heading.
    • Fallbacks: place → county remainder when the place or block is known → county population average → state population average → heading rate (local 0) outside the 21 states.
    • No state-gated key variables: the local_sales_tax_table default is A, and the rate.yaml breakdown is state_code.
    • Callers are unchanged: state_and_local_sales_or_income_tax and hi_salt_deduction.
  • Locality rates against official sources (18 localities, no mismatches):
    • NY Pub. 718 (effective 3/1/2025):
      • NYC 8⅞, Yonkers 8⅞;
      • Westchester outside cities 8⅜. Mount Vernon, New Rochelle and White Plains are also 8⅜, so they are pruned to the remainder;
      • Suffolk 8¾ (8⅝ before 2025-03-01), Erie 8¾, Albany 8, Chautauqua 8.
    • Vermont list:
      • Burlington 7%, Winooski 7%;
      • Montpelier 7% from 2025-07-01;
      • Barre City 7% from October 2022 (the 2022 Q4 record-weighted 6.9977% is logged);
      • Rutland City from July 2023.
    • WA DOR quarterly rate files:
      • Seattle 10.25% → 10.35% (Q2 2024) → 10.55% (Q1 2026);
      • Tacoma 10.3% → 10.4% (Q2 2026) → 10.5% (Q3 2026);
      • Spokane 9.0% → 9.1% (Q2 2025).
    • TN Notice 24-11: Davidson County 2.75% local, so Nashville is 9.75%, from 2025-02-01.
    • Ohio: Franklin County 7.5% → 8.0% on 2025-04-01.
    • Arkansas DFA city/county table: DeWitt 3.5% + Arkansas County 1% + state 6.5% = 11%.
    • CA, IL, TX, CO, AL, LA, AZ and FL have 0 rows in rates.csv. Those households get the heading rate (e.g. TX 6.25%, LA 5.00%, CA 7.25%), so line 3 is 0 unless a rate is entered. This is the documented and quantified S1 regression.
  • Data schema and weights:
    • The CSV schemas are state_code,county_fips,place_fips,effective_from,rate, county_fips,place_fips,population, state_code,effective_from,rate and place_geoid,county_fips. The 21 covered states are as listed.
    • Population sums equal the 2020 Census totals: NY 20,201,249; GA 10,711,908; WA 7,705,281; VT 643,077; TN 6,910,840; OH 11,799,448; VA 8,631,393. They weight only the county and state fallbacks.
    • Recomputed independently from the CSVs: NY 2025 average 8.54115450633%, Rutland remainder 2022 6.21733%.
  • Place-to-county crosswalk (15 places):
    • Mapped: NYC → Kings (the largest slice, 2,736,074), Yonkers → Westchester, Savannah → Chatham, Baton Rouge → East Baton Rouge, Nashville → Davidson, Atlanta → Fulton, Oklahoma City → Oklahoma, Tulsa → Tulsa, Minneapolis → Hennepin, Seattle → King, Des Moines → Polk, Tupelo → Lee, Phoenix → Maricopa, Denver → Denver.
    • As documented, multi-county places outside the 21 states (Houston, Chicago, Birmingham, Jackson MS, Kansas City MO) are not inferred.
  • Provenance:
    • build.py downloads only the SST rate and boundary files, NY Pub. 718/718-A, the Virginia locality workbook, the Vermont list and 2020 Census files.
    • manifest.json pins the SHA-256 of 80 inputs and has boundary audits for 19 files and a 907-line recode log.
    • test_l0_official_sources_only guards against non-official sources.
  • Python tests are justified. They cover data-table invariants (schema, coverage, day-by-day proration, bounds, floors), the selector lists, the parser and builder, and Core input provenance across branches and dataset years, none of which YAML can express. L6 overlaps YAML but sweeps all 21 states in one simulation. The tests reuse the existing system, and the parser tests build no model.
  • Packaging:
    • The data ships through hatch packages = ["policyengine_us"] and MANIFEST.in: about 760 KB uncompressed (place_counties.csv 433 KB, rates.csv 258 KB, populations.csv 68 KB).
    • The release-wheel artifact is 14.44 MB, against 14.19–14.20 MB on sibling runs (+~240 KB, 1.7%).
    • scripts/ is not shipped. The workflows set no wheel size limit, and "Package and compatibility" and the 3.11–3.14 smoke imports pass.
  • Hand checks: every new or changed expectation, all matching.
    • combined_sales_tax_rate.yaml:
      • Rutland city: 2022 6%; 2023 (181 × 6 + 184 × 7) / 365 = 6.50410959%; 2024 7%.

      • Rutland remainder block 2022: (273 × 6.2167 + 92 × 6.2192) / 365 = 6.21733014%.

      • NYC 3651000: 8.875%. Atlanta 4000/DeKalb 2024: 8.9%. DeWitt 5001: 6.5 + 1 + 3.5 = 11%.

      • Precedence case, rate and table for each household:

        Household Rate Table
        Kings enum 8.875% A (Kings is a listed NYC county)
        NYC flag 8.875% A
        Yonkers flag 8.875% B
        Place 51000 only 8.875% A
        Westchester block 8.375% B
        Block over enum 8.375% B
        County FIPS over block 8.375% B
        Place over Yonkers flag 8.875% A
        No location NY average 8.5411545063% D
        Savannah, pruned to Chatham remainder 7% B
        Invalid county and block, place 51000 8.875% A
        Baton Rouge via the crosswalk to East Baton Rouge LA heading 5.00% B
      • The out-of-state GEOID 0651000 gives NY's average and D. TX households with NY enum, block or flags get 6.25% and the default A.

    • local_sales_tax_table.yaml:
      • The Mississippi default is NONE in 2022–2025. The selector has only the Jackson and Tupelo rows: 2022 p. 17, 2025 p. 18.
      • NYC GEOID A. Atlanta short code B (Atlanta is listed in place/b.yaml, overriding DeKalb's A). Arapahoe 8005 (2022) B.
      • Jackson: 2022 A, 2025 C. Clinton in Hinds County: NONE.
    • local_state_tax.yaml:
      • Rutland city: 2022 572 × 0 / 6 = 0; 2019 525 × 0 = 0; 2023 766 × 0.504110 / 6 = 64.36.
      • Mississippi at $85,000, size 1, line 1 = 1,243 (2025 p. 14, heading 7.00%):
        • other and unknown localities: 1,243 × 1 / 7 = 177.57;
        • Jackson: C 179 × 1 = 179;
        • Tupelo: A 120 × 1 = 120;
        • entered local rate 1: 177.57; entered rate 0: 0;
        • no rate entered: 0.
      • Unchanged no-local case: MA 2022 → 0.
  • Python test edits: test_state_sales_tax_table.py and the worksheet I1/I6/I7 tests now include NONE, and the reference worksheet sends NONE to the ratio. The L9 provenance tests and the carryover tests check calculated-versus-supplied county and city inputs across branches and dataset years.
  • Changelog: changelog.d/local-sales-tax-official-rates.added.md is a top-level file of type added.

Axiom line assessment

The body has axiom: TheAxiomFoundation/rulespec-us#1402 queued. #1402 is OPEN, labelled pe-parity, and was last updated 2026-10-15 04:03 UTC.

  • What it has:
  • Why queued is valid: d236, the corpus/release re-pin and approval are recorded as explicit blockers.
  • Mismatch (N1): the issue predates the 2026-10-20 Mississippi change. It still describes an A–D selector, line 2 × line 3 in all line-2 states, and the Jackson proxy for TY2015–2021, so it doesn't match the head.

The line becomes valid once the absent-table rule and a Mississippi companion case are added.

CI status

gh pr checks 9879 shows 27/27 pass at af4958ea8f, rechecked at the end of this review (run 39445050057, started 2026-10-20 15:04 UTC). It covers:

  • all Baseline and Contrib shards;
  • Microsimulation;
  • both Rest shards;
  • Household API Partners;
  • PR validation (lock, guard tests, lint, changelog);
  • Package and compatibility;
  • Quick Feedback.

The run predates the source-conflict ratchet (#10287), so N2 shows up only on the merged tree.

Branch status

  • The branch is 20 ahead and 73 behind main. The merge base is f19877374a.
  • git merge-tree --write-tree PolicyEngine/main af4958ea8f is clean.
  • Since the merge base, no main change touches the sales tax variables or parameters, SALT or county_helpers.py. main added get_itemization_branch to tools/general.py, and the period_branch helpers this PR imports (_input_keys, _is_known) are unchanged.
  • The one merged-tree problem is the ratchet entry (N2).
  • Sibling note: stacked PR Add the IRS 2015-2021 local sales tax tables, selector and heading rates #9911 (open, last updated 2026-10-06) still adds MS to default_table/a.yaml/b.yaml and Jackson to the place lists for 2015–2021. Merged, that would put MS in both A/B and NONE, and select picks A or B first. Add the IRS 2015-2021 local sales tax tables, selector and heading rates #9911 needs rebasing onto this head and should adopt the absent-table rule.

Verdict: REQUEST_CHANGES

@DTrim99 DTrim99 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Round 2: all round-1 findings are fixed (Rutland 2022, the state-by-state impact, references, the #1402 module path, normalization tests), and the data checks out: 18 localities across NY/VT/WA/TN/OH/AR match official sources, 15 crosswalk places and 7 state population totals verify, and every new or changed expectation hand-checks. Three should-address items: (1) rulespec-us#1402 doesn't match the head (add the 'no table = zero' rule and a Mississippi 2025 case); (2) merge blocker: on the merged tree, the #10287 ratchet fails because local_sales_tax.py's KNOWN_UNMARKED count drops to 0, so delete that entry after merging main; (3) the default_table and none.yaml descriptions are off the house template. Six suggestions. Full report in the comment above.

MaxGhenis and others added 3 commits October 23, 2026 19:06
- Drop local_sales_tax.py from KNOWN_UNMARKED: on the tree merged with main it has no unmarked conflict-sounding comment, so the #10287 ratchet's shrunk check failed.
- Default-table descriptions follow the IRS-led house form (a-d, none.yaml).
- Shipped text cites the 2025 Schedule A instructions (p. 5) instead of decision ids.
- The changelog fragment states the $0 local amount in the 16 uncovered local-sales-tax states (was 20% of the state amount).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…round 2, suggestion 3)

build.py now records output_sha256 for rates.csv, populations.csv, place_counties.csv and state_rates.csv, and manifest.json carries the current files' hashes (place_counties.csv unchanged). test_manifest_hashes_every_shipped_csv checks that the manifest names exactly the shipped CSVs and that each hash matches.

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

Copy link
Copy Markdown
Contributor Author

Round 2 is addressed at 76ebba54fa; details are under "Response to round 2" in the body.

  • N1: the two worksheet comments on rulespec-us#1402 now carry the no-table outcome, the replaced 2015–2021 Jackson sentence and the Mississippi 2025 case (177.57).
  • N2: main is merged, and the local_sales_tax.py entry is gone from KNOWN_UNMARKED. The test's scanner finds no unmarked conflict-sounding comment left in that file and none gained elsewhere.
  • N3: the descriptions now follow your wording.

Suggestions 1–5 are done. For suggestion 3, the manifest now hashes all four CSVs, with a test that ties the hashes to the shipped files. Suggestion 6 goes to a separate PR. The by-state impact rerun at this head is queued for when the host's current reservation lifts, and its table will replace the 724a8fbe11 one.

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis requested a review from DTrim99 October 23, 2026 23:13

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.

2 participants