Skip to content

Keep itemized sales taxes in Utah's taxpayer credit base - #10597

Merged
MaxGhenis merged 7 commits into
mainfrom
claude/state-itemized-misc-sales-tax-rozit6
Nov 10, 2026
Merged

MaxGhenis merged 7 commits into
mainfrom
claude/state-itemized-misc-sales-tax-rozit6

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Utah's taxpayer credit now keeps itemized sales taxes in its federal-deduction base. Under a sales-tax election, the base equals the federal itemized deductions after section 68, with nothing removed. Pull request run 42767666376 at ac417e0352 passed all 36 jobs on GitHub's merge with main 4a3ddefcc7.

Scope

We planned one PR for four backlog items, and the other three landed in other PRs:

This PR covers only us-ut-taxpayer-credit-sales-tax.

Utah: sales taxes stay in the taxpayer credit base

Source

Three editions of Utah Code §59-10-1018 say what comes out of the federal itemized deductions. The 2026-11-09 round read each edition at le.utah.gov:

We also read TC-40 line 14 for 2019, 2023 and 2025, and each agrees: a filer doesn't include any sales tax they itemized. The 2025 instructions are at https://files.tax.utah.gov/tax/forms/2025/tc-40inst.pdf#page=10.

Ruling for the sales branch. A sales-tax elector reports and deducts no state or local income tax on the federal return, so no edition removes anything for them. The change carries no date and applies in every year.

The 6% rate comes from §59-10-1018(2)(a)(ii) and stays in gov.states.ut.tax.income.credits.taxpayer.rate; this PR doesn't change it.

What changed

ut_federal_deductions_for_taxpayer_credit used to subtract salt_deduction − capped real-estate tax from the federal itemized total. That amount is whatever SALT remains, so under a sales-tax election the sales tax came out of the base. The variable now does the following:

  • It identifies the election from the amount the federal SALT deduction uses. A unit elects sales taxes when state_and_local_sales_or_income_tax equals state plus local sales tax and not state plus local income tax.
    • When the formula supplies that amount (the larger of the two), this means the sales tax exceeds the income tax by more than np.isclose's tolerance, about 0.001% of the income tax. A smaller gap counts as a tie.
    • A supplied amount that equals the sales tax counts as a sales-tax election even when the sales tax falls short of the income tax (Case 15). A filer can choose that: it costs the federal difference times the marginal rate, and Utah removes nothing.
    • A tie counts as an income-tax election, since the amount doesn't show which tax the return deducted. So does a supplied amount that matches neither component (Case 12). Both leave main's result unchanged.
  • Sales-tax election. The variable removes no tax. The base equals itemized_taxable_income_deductions, the federal itemized deductions after section 68. When that equals zero (no federal total), the variable falls back to non-SALT itemized + salt_deduction.
  • Income-tax election. The formula matches main's.
  • Standard-deduction filers. Unchanged.

The income-tax branch has separate problems, which this PR leaves alone; a follow-up PR will change it after this one lands. Its floor (non-SALT itemized + capped real-estate tax) uses a total before section 68. It caps real-estate tax at the statutory maximum rather than salt_cap. And it removes the residual federal SALT, while the statute, as we read it, removes the income tax paid and reported, capped at $10,000 for 2018 through 2025 and also at the amount deducted from 2026.

For example, a 2024 joint return with $10,000 of real-estate tax, $8,000 of state income tax and $1,000 of sales tax has a $10,000 SALT deduction. The branch takes all of it as real-estate tax and removes nothing, for a $10,000 base. The 2023 edition removes $8,000, so the branch overstates the base by $8,000, $480 of credit before the phase-out. The 2025 TC-40 line 14 has filers enter Schedule A line 5e less lines 5b and 5c, $0 here, as the branch does, so that fix needs its own source-conflict entry.

Witnesses

The YAML comments give the arithmetic for each case.

Case Year Main Branch
6: $6,000 other deductions + $9,000 sales tax 2025 base $6,000 base $15,000, credit $900
7: $4,000 real-estate tax + sales tax, SALT capped at $10,000 2024 base $10,000 base $16,000, credit $960
10: fallback, no federal total supplied 2025 $6,000 $15,000
11: supplied federal total $14,000, below $6,000 + $9,000 2026 $6,000 $14,000, credit $840
13: single, $1.5M long-term gains, $100,000 cash charity, $20,000 sales tax 2026 $92,500 $96,959.46
14: the Case 13 filer 2017 $100,000 $82,845
8: income tax larger than sales tax 2025 $6,000 $6,000
9: income tax equal to sales tax 2025 $6,000 $6,000
12: supplied combined amount $9,000 matching neither component 2023 $6,000 $6,000
15: supplied $3,600, equal to the sales tax and below the $3,700 income tax 2025 $36,000, credit $1,593.54 $39,600, credit $1,809.54

In Cases 13 and 14, section 68 leaves the federal itemized deductions at $96,959.46 and $82,845. The branch's earlier max(itemized, non-SALT + SALT) returned the totals before section 68 ($102,500 and $120,000), above main in both years.

Case 15 comes from the 2026-11-04 review: before this round the branch gave main's $36,000, $216 of credit short.

Case 5, the existing test for removing income-or-sales tax, now names the income-tax components explicitly and still expects $6,000. Case 12 keeps its original inputs, which supply only the combined variable.

Years affected: every year in which a unit itemizes with a sales-tax election the model can identify: its federal SALT amount equals its sales tax and not its income tax.

Invariants

policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py holds the property test, which uses Hypothesis.

  1. Under a sales-tax election, the base equals itemized_taxable_income_deductions, or the fallback when that equals zero.
  2. Under an income-tax election, non-SALT + min(real-estate tax, cap) ≤ base ≤ max(itemized, non-SALT + min(real-estate tax, cap)).
  3. Raising sales tax above income tax, with every other input fixed, never lowers the base, when the supplied total isn't cut below the reconstruction.

The test also asserts that the sales-tax base never exceeds the supplied federal itemized total. Its draws cut the supplied total by up to 80%, which brings section 68 into the test. An explicit example pins a cut below the reconstruction under both elections. Main's formula fails property 1, and so does the earlier max sales base. Property 3 leaves out pairs with a cut, because the income-tax branch still floors its base at a total before section 68 (the follow-up above).

What it can reach that YAML doesn't: its strategy draws sales-tax elections for SEPARATE and HEAD_OF_HOUSEHOLD filers and in 2018, which no YAML case has, and it bounds randomized amounts in both branches. It runs six fixed examples (max_examples=6, derandomize=True), so one run doesn't cover every combination. Measured at ac417e0352 with /usr/bin/time -v on a 4-vCPU VM (local, not CI), the file passed its 1 test in 7.64 s of pytest time, 51.96 s of wall time for the cold process, and 1,011,648 KB peak RSS. In CI at ac417e0352 (run 42655090497, timing artifact rest-python-test-timings-shard-4), it took 0.58 s of test time in Rest shard 4, group core-1. That group's process ran 22:09 and peaked at 7,395,764 KB, under the Makefile's target of about 10 GB per process; the peak isn't this file's alone. Run alone in Quick Feedback, the file passed in 15.80 s, 3.76 s of it test time.

Files

  • policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py
  • policyengine_us/tests/policy/baseline/gov/states/ut/tax/income/credits/ut_federal_deductions_for_taxpayer_credit.yaml
  • policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py (new). The spec doesn't name this file; we added it because the item asks for a property test. It runs in the Makefile's core group.
  • changelog.d/ut-taxpayer-credit-sales-tax.fixed.md

Readers: ut_taxpayer_credit_max reads this variable; ut_taxpayer_credit and the contrib ut_hb210 reform read ut_taxpayer_credit_max; ut_income_tax_before_non_refundable_credits reads ut_taxpayer_credit, then on to ut_income_tax and household_net_income.

Overlap with #10038. #10038 merged on 2026-11-07 and touches none of these files. It adds claim_of_right_deduction to gov.irs.deductions.itemized_deductions, so that deduction joins this variable's non-SALT fallback_itemized. Run 42767666376 tested this PR merged with a main that includes it.

Microsimulation impact

We estimate that the change moves almost nothing on the default dataset. It reaches 6 sampled Utah tax units in 2025 and 5 in 2026, whose weights sum to about 1 in each year. We compared main at 89a15b72f5 with this branch at ac417e0352.

For those tax units, the federal deductions base rises by at most $1,693 in 2025 and $1,791 in 2026, and the taxpayer credit by at most $102 and $107. Weighted, the credit rises by $99 and $112, and Utah income tax falls by under $1. state_and_local_sales_or_income_tax totals $746.4bn and $780.9bn and doesn't change. Federal income tax and household benefits don't change.

Output 2025 main 2025 change 2025 records changed (weighted) 2026 main 2026 change 2026 records changed (weighted)
ut_federal_deductions_for_taxpayer_credit $43.63bn +$1,648 6 (about 1) $44.83bn +$1,860 5 (about 1)
ut_taxpayer_credit_max $2.77bn +$99 6 (about 1) $2.84bn +$112 5 (about 1)
ut_taxpayer_credit $1.58bn +$99 6 (about 1) $1.61bn +$112 5 (about 1)
ut_income_tax $6.64bn −$1 2 (under 1) $6.92bn −$1 2 (under 1)
state_and_local_sales_or_income_tax $746.4bn $0 0 (0) $780.9bn $0 0 (0)
state_income_tax $548.8bn −$1 2 (under 1) $578.0bn −$1 2 (under 1)
income_tax $2,421.9bn $0 0 (0) $2,595.1bn $0 0 (0)
household_net_income $16,345.8bn +$1 2 (under 1) $17,162.3bn +$1 2 (under 1)
household_benefits $1,955.4bn $0 0 (0) $2,060.6bn $0 0 (0)

impact: near zero on the default dataset: Utah's taxpayer credit +$99 in 2025 and +$112 in 2026 (6 and 5 sampled tax units, combined weight about 1); federal income tax and household benefits don't change
axiom: us-ut:statutes/59-10-1018 encoded-correct

🤖 Generated with Claude Code

Utah Code 59-10-1018(1)(f)(i) removes state or local income tax from the
federal itemized total, and the TC-40 line 14 instructions say not to
remove itemized sales tax. The model removed the combined income-or-sales
amount, so a sales-tax election lost its sales tax from the credit base.
A strictly larger sales amount (the federal election) now keeps the whole
allowed SALT deduction; the income-tax branch is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JnxodpPD6TQDSxpKU
Checks, for drawn households in 2018 and 2024-2026, that a sales-tax
election keeps the whole allowed SALT deduction in the base, that the
income-tax branch stays within its bounds, and that moving to a sales-tax
election never lowers the base. Main's formula fails the first property.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6JnxodpPD6TQDSxpKU
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: b4ca92ba03f1545bd1956499ba0b1150a4913cc8. Branch: claude/state-itemized-misc-sales-tax-rozit6.

The brief named the branch state-itemized-misc-and-ut-sales-tax. This session's environment fixes the development branch, so I used claude/state-itemized-misc-sales-tax-rozit6 instead. I checked first that neither branch existed (both returned 404).

Items

Item Result Why
us-al-misc-deduction-double-floor SKIPPED Open draft #10098 (branch al-misc-deduction-own-floor, last push 2026-10-11) makes the same change. One difference from the spec: #10098 starts from total_misc_deductions, the federal source list. The spec asks for a separate Alabama source list, so that future federal categories aren't imported automatically, and asks for the rate's coverage to go back to 2017. The hub should reconcile that on #10098.
us-mo-misc-duplicate-2017 SKIPPED Covered by open draft #10096 (branch ms-mo-misc-deduction-double-count, last push 2026-10-11).
us-ms-misc-double-count-2017 SKIPPED Also covered by #10096.
us-ut-taxpayer-credit-sales-tax FIXED This PR.

Utah: what changed

  • policyengine_us/variables/gov/states/ut/tax/income/credits/taxpayer_credit/ut_federal_deductions_for_taxpayer_credit.py
    • Line 37: a sales-tax election is sales_tax > income_tax. Each side is state plus local, the same components the federal state_and_local_sales_or_income_tax compares.
    • Lines 64-65: under a sales-tax election the base is max(itemized, non-SALT + salt_deduction), and no tax is removed.
    • Under an income-tax election the branch is unchanged from main. The standard-deduction branch is also unchanged.
    • Added references to §59-10-1018 (PDF page 1) and the 2025 TC-40 instructions (PDF page 10).
  • Tests, in ut_federal_deductions_for_taxpayer_credit.yaml:
    • Case 5 (line 62) is the existing sales-tax-removal test. It now names the income-tax components explicitly, and its expected value is still $6,000. Without that change, state_sales_tax's table formula would make Case 5 a sales-tax election.
    • New cases 6-10 start at lines 81, 103, 125, 144 and 164.
  • Every quoted source comes from the spec's excerpt fields. le.utah.gov and files.tax.utah.gov returned 403 through this VM's proxy.

Tests

Each file ran in its own process.

  • On main before the fix, ut_federal_deductions_for_taxpayer_credit.yaml: 7 passed, 3 failed (cases 6, 7 and 10). After the fix: 10 passed.

  • The reader chain all passes on the branch:

    File Passed
    ut_taxpayer_credit_max.yaml 2
    ut_taxpayer_credit.yaml 1
    ut_taxpayer_credit_reduction.yaml 3
    ut_non_refundable_credit_order.yaml 1
    ut/tax/income/integration.yaml 2
    ut_income_tax_before_credits.yaml 1
    ut_income_tax_before_non_refundable_credits.yaml 3
    ut_income_tax_before_refundable_credits.yaml 2
  • The new property test, policyengine_us/tests/core/test_ut_taxpayer_credit_sales_tax_properties.py, passed: 1 test, 6 Hypothesis examples with derandomize=True.

    • Local measurement (Linux VM, Python 3.14): 8.9 s inside pytest, 52.9 s for the whole process including model import, 909 MiB peak RSS for the process.
    • These are local numbers, not CI. The test runs in the Makefile's core group, which CI run 38141803955 recorded at 13.3 GB. The model is already loaded in that process, so the added cost should be roughly the in-test time, but CI hasn't measured it.
    • Hypothesis was declared in the dev extras but wasn't installed in the venv; uv sync --extra dev installed it.
  • code_health/test_rest_python_groups.py: 62 passed.

  • Partner contract tests: no file under tests/policy/baseline/partners/ contains a Utah household, and none were edited.

Hand computations

Case Year Arithmetic Main
6 2025 $6,000 charitable + $9,000 sales tax = $15,000; credit 6% = $900 $6,000 base, $360 credit
7 2024 allowed SALT $10,000 + $6,000 = $16,000; credit $960 $10,000 base, $600 credit
10 2025 fallback $6,000 + $9,000 = $15,000 $6,000
8 2025 income election: $15,000 − $9,000 = $6,000 same
9 2025 tie: $15,000 − $9,000 = $6,000 same

Mutants

Each mutant ran in its own process, and the code was restored afterwards (checked with git status and a clean diff).

  1. Main's formula restored. YAML cases 6, 7 and 10 failed, with values $6,000, $10,000 and $6,000. The property test failed on invariant (1).
  2. Tie rule changed from > to >=. Case 9 failed.

CI

None has run. The PR is a draft, and CI skips drafts.

Judgment calls

  • Ties. When income and sales tax are equal, the unit counts as an income-tax election. That matches the spec's "strictly larger" and leaves main unchanged.
  • Income-tax branch left as main has it. The spec notes say the 2018 and 2023 statutory editions measure the addback as income tax paid and reported, capped at $10,000, while main uses residual federal SALT. That is out of scope here.
  • The sales branch trusts the supplied total. It takes max(itemized, fallback), so a supplied itemized_taxable_income_deductions above the reconstruction is kept whole.
  • axiom: line. I wrote axiom: needed. The item asked for axiom: pending: hub files parity, but the brief's general rules say never to write that, and needed is one of CLAUDE.md's allowed values. The hub should set the final value.
  • No conflict with Model the claim of right repayment deduction and credit (26 U.S.C. 1341) and North Carolina's rules that depend on it #10038. It touches none of this PR's files.
  • Unverified. The 2017 statutory edition and the intervening statutory gap remain unverified, as the spec says.

Proposed follow-up

Title: Utah taxpayer credit: use the statutory income-tax addback before 2026.

The 2018-2025 editions of §59-10-1018(1)(f) measure the income tax removed as the amount paid and reported, capped at $10,000. Main removes residual federal SALT, as the TC-40 instructions do. This needs a dated parameter or formula branch, with a source-conflict entry.


Generated by Claude Code

@MaxGhenis
MaxGhenis marked this pull request as ready for review November 2, 2026 15:44
@codecov

codecov Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff            @@
##              main    #10597   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            1         1           
  Lines           54        36   -18     
=========================================
- Hits            54        36   -18     
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

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: REQUEST_CHANGES
b4ca92b
METHODOLOGY_CHOICE: yes

The head hasn't moved since b4ca92ba03. No earlier **Hub review in progress** comment exists, so this review covers the whole diff.

The defect is real and the fix points the right way. Two things block it. First, the sales branch's max lets the base exceed the federal itemized deductions in any year when §68 reduces them (2013-2017 and 2026 on). In the 2017 witness below, that makes the base worse than main's. Second, the election is recomputed from components, so it can disagree with a supplied state_and_local_sales_or_income_tax, the variable the federal side actually deducts.

Findings

P1. The sales branch ignores the §68 limitation and can exceed federal itemized deductions

ut_federal_deductions_for_taxpayer_credit.py:64-65

sales_fallback_base = fallback_itemized + salt_deduction
sales_claimed_base = max_(itemized, sales_fallback_base)

In a computed simulation, fallback_itemized + salt_deduction is exactly total_itemized_taxable_income_deductions. That's the same gov.irs.deductions.itemized_deductions list, which still contains salt_deduction. itemized_taxable_income_deductions is that total minus itemized_taxable_income_deductions_reduction. The reduction is nonzero whenever gov.irs.deductions.itemized.limitation.applies (true 2013-2017 and from 2026). So for any sales-tax elector whose itemized deductions are reduced, the max returns the unreduced total. The statute starts from the federal itemized deductions and, under a sales-tax election, removes no income tax, so the correct base is itemized.

Failure scenario. A single Utah filer with $1.5M long-term gains and $100,000 cash charity, state_withheld_income_tax: 0 and state_sales_tax: $20,000 supplied, itemizing:

Year federal itemized (after §68) total before §68 main this PR correct (itemized − 0)
2026 96,959.46 102,500 92,500 102,500 96,959.46
2017 82,845 120,000 100,000 120,000 82,845

In 2017 the PR overstates the base by $37,155, against main's $17,155, so this PR makes that unit's base worse. Command: a Simulation(situation=...) script run once with sys.path on this head and once on origin/main. It printed, for example, C_pease 2017 {'ut_federal_deductions_for_taxpayer_credit': 120000.0, 'itemized_taxable_income_deductions': 82845.0, 'total_itemized_taxable_income_deductions': 120000.0, 'itemized_taxable_income_deductions_reduction': 37155.0, ...}.

At default inputs this is rare. ut_withheld_income_tax is 4.55% of person AGI above the single standard deduction, so a filer above the §68 thresholds is almost never a sales-tax elector unless their withholding is supplied. It is still a wrong result, and invariant (1) in the body and the property test write it down as intended.

Replacement for line 65:

        # Sales-tax election: nothing is removed from the federal itemized
        # deductions (after section 68); itemized_base falls back when zero.
        sales_claimed_base = itemized

I applied that line on a scratch copy. The YAML file passed 10/10, the property test passed, and the two witnesses gave 96,959.46 and 82,845. The code was restored and git status was clean afterwards. No current test pins the max (mutant M3 below survives), so please add a YAML case that supplies itemized_taxable_income_deductions below non-SALT + salt_deduction under a sales election and expects itemized.

P2. The election test can disagree with the SALT amount the federal side actually used

ut_federal_deductions_for_taxpayer_credit.py:33-37

The flag is recomputed as state_sales_tax + local_sales_tax > state_withheld_income_tax + local_income_tax. That equals the federal choice only while state_and_local_sales_or_income_tax comes from its formula. When that variable is supplied, the components come from ut_withheld_income_tax's estimate and state_sales_tax's table, and they need not match it. policyengine_us/data/economic_assumptions.py:64 uprates state_and_local_sales_or_income_tax as a microdata variable, which suggests datasets supply it.

Witness: the old Case 5 inputs (2023, JOINT, itemized_taxable_income_deductions: 15_000, state_and_local_sales_or_income_tax: 9_000, no income):

  • main: base 6,000, ut_taxpayer_credit_max 360;
  • this head: base 15,000, credit 900, because the table gives state_sales_tax 424 against withholding of 0.

The hand-off changed Case 5's inputs to avoid exactly this, so the YAML no longer exercises it. Suggested replacement for line 37:

        # Sales-tax election only where the SALT amount the federal side used
        # is the sales tax; a supplied amount that matches neither keeps main's
        # income-tax treatment.
        sales_tax_elected = (sales_tax > income_tax) & np.isclose(
            raw_state_income_or_sales_tax, sales_tax
        )

Then restore the old Case 5 inputs as an extra case expecting 6,000. The hub's population run should confirm whether the dataset supplies this variable. If it does, this finding decides the population impact.

P2. 2018-2025 sales-tax electors: under the hand-off's own reading of those editions, the PR widens the gap (methodology choice)

ut_federal_deductions_for_taxpayer_credit.py:62-69; body "Years affected".

The body cites only the 2026 edition. The hand-off's follow-up says the 2018-2025 editions measure the income tax removed as the amount "paid and reported, capped at $10,000", not the amount deducted. If that is right, a 2018-2025 sales-tax elector who paid Utah income tax still has that tax removed. The PR removes nothing in every year. Witness: 2025 joint retirees with $40,000 Social Security, $30,000 pension, $25,000 mortgage interest, $8,000 cash charity and $3,000 real-estate tax, all other inputs default. Withholding is 1,140.75 and sales tax 1,292.05, so they count as a sales-tax election.

  • main: base 37,357.50;
  • this head: 38,649.55;
  • the paid-and-reported reading: 38,649.55 − 1,140.75 = 37,508.80.

Under that reading main is off by 151.30 and the PR by 1,140.75. I could not reach le.utah.gov to check the 2018 or 2023 text (see below). So this is a dated choice the hub should make against the statute: either confine the sales-branch change to 2026 on and record the earlier years in the source-conflict register, or confirm that the earlier editions also measure the income tax deducted. The PR should not claim "every year" until that's settled.

P3. The PR body has inaccurate or stale statements

  • "Draft. … CI doesn't run on drafts." The PR is no longer a draft (draft: false), and Pull request run 41474534138 completed with every job success.
  • "Only ut_taxpayer_credit_max reads this variable." That's true of the variable, but the chain is incomplete: policyengine_us/reforms/states/ut/ut_hb210.py:31 also reads ut_taxpayer_credit_max. Its tests pass (below).
  • Invariant 1 ("the base equals max(itemized, non-SALT + salt_deduction)") restates the P1 defect. Replace it with "Under a sales-tax election, the base equals itemized_taxable_income_deductions, or the fallback when that is zero."
  • Suggested replacement for the "Readers" sentence: "Readers: ut_taxpayer_credit_max reads this variable; ut_taxpayer_credit and the contrib ut_hb210 reform read ut_taxpayer_credit_max; ut_income_tax_before_non_refundable_credits reads ut_taxpayer_credit, then on to ut_income_tax and household_net_income."

P3. The property test restates the formula and can't detect P1

test_ut_taxpayer_credit_sales_tax_properties.py:63-70. The test always supplies itemized = charitable + salt + extra ≥ other + salt, so max(itemized, other + salt) always equals itemized. The §68 reduction is never exercised, because itemized_taxable_income_deductions is an input. Property (1) fails on main, so it isn't vacuous, but it compares the code to its own formula, and mutant M3 (dropping the max) passes it. Add an assertion that the sales-election base is at most itemized where itemized > 0, and draw some cases with extra_itemized negative relative to the reconstruction, which is how §68 enters.

Follow-ups (same on main, not made worse here)

  • Income branch also ignores §68. Line 59 has max_(itemized - claimed, fallback_itemized + capped_real_estate_tax), and that fallback is pre-reduction. With a reduction it wins, and main returns the unreduced total less the income tax. Witness: any 2026 income-tax elector with itemized_taxable_income_deductions_reduction > 0.
  • Real-estate cap uses the statutory maximum, not salt_cap. Lines 45-47 use p.itemized.salt_and_real_estate.cap, ignoring the 2025+ phase-down. For an income elector whose cap is phased down to $10,000 with real-estate tax at or above that, salt_deduction − capped_real_estate_tax is 0 and no income tax is removed. The same is true on main.
  • Pre-2026 income-tax addback. This is the hand-off's proposed follow-up. I could not verify it (statute unreachable).

Checked and sound

  • Defect reproduces at main. I ran the PR's YAML with PYTHONPATH on an origin/main worktree (confirmed policyengine_us.__file__ points there): 7 passed, 3 failed (Cases 6, 7, 10). At head: 10 passed.

  • Election test against the federal formula. state_and_local_sales_or_income_tax (state_and_local_sales_or_income_tax.py:13-17) is max_(withheld + local income, state + local sales), with a single formula and no dated branches. The SALT sources list has been unchanged since 2013, and salt_deduction = min_(salt_cap, salt). So while the formula runs, line 37 matches the federal choice in every year from 2017, including when the cap binds. In that case the federal amount doesn't depend on the election, and the PR keeps the whole capped SALT, which is right when no income tax was deducted. On ties the federal amount is identical, and the PR takes the income branch (Case 9), which is the methodology choice flagged above.

  • Itemizer with zero SALT. itemized_taxable_income_deductions: 15_000, all taxes zero, 2025 JOINT: base 15,000 on both head and main. Nothing is removed and no sales branch is taken.

  • Hand values, before reading the expected outputs (rate 6%, ut_personal_exemption 0):

    • Case 6: sales 9,000 > income 0, so nothing is removed; 15,000 → credit 900.
    • Case 7: SALT = min(4,000 + 9,000, 10,000) = 10,000; 6,000 + 10,000 = 16,000 → 960.
    • Case 8: income 9,000 > sales 2,000; removes 9,000 − 0 = 9,000; max(6,000, 6,000) = 6,000.
    • Case 9: tie → income branch → 6,000.
    • Case 10: itemized 0 → fallback 6,000 + 9,000 = 15,000.

    All match the YAML.

  • Case 5 and the table formula. Default UT households become sales-tax electors at low wage incomes. Scanning 0-200k employment income, the cutoff is $20k single / $25k joint (2018), $30k/$35k (2023, 2025) and $35k/$40k (2026). Joint retirees living on Social Security are electors at every level scanned up to $200k, because ut_withheld_income_tax uses AGI. None of the wage-only scan points itemized. No other Utah YAML supplies state_and_local_sales_or_income_tax, and every reader test passes at head (below). The old behaviour Case 5 encoded is the supplied-combined-variable path in P2.

  • Tests at head, one file per process. All passed:

    File Passed
    ut_federal_deductions_for_taxpayer_credit.yaml 10
    ut_taxpayer_credit_max 2
    ut_taxpayer_credit 1
    ut_non_refundable_credit_order 1
    ut_taxpayer_credit_reduction 3
    ut/tax/income/integration 2
    ut_income_tax_before_credits 1
    ut_income_tax_before_non_refundable_credits 3
    ut_income_tax_before_refundable_credits 2
    contrib hb210/ut_hb210_s2_marriage_credit 7
    contrib ut_hb210_reform_test 8
    property test 1 (6.8 s inside pytest, local)
  • Mutants. Each ran on the working file in its own process, restored afterwards; cmp against a saved copy matched and git status was clean.

    • M1, main's formula: YAML Cases 6, 7 and 10 fail, and the property test fails.
    • M2, >= tie: Case 9 fails; the property test passes.
    • M3 (mine), sales_claimed_base = itemized: survives everything. That's the P1 fix, and it shows nothing pins the max.
    • M4 (mine), sales fallback uses capped_real_estate_tax: Case 10 fails, and so does the property test.
  • Readers moved at baseline. These change at baseline: ut_federal_deductions_for_taxpayer_credit → ut_taxpayer_credit_max → ut_taxpayer_credit → ut_income_tax_before_non_refundable_credits → ut_income_tax_before_refundable_credits → ut_income_tax → state_income_tax → household_net_income, plus the contrib ut_hb210 reform. In the retiree witness the base rises 1,292.05 and ut_taxpayer_credit rises 77.52, but ut_income_tax is already 0, so household_net_income doesn't move. Outside P1's unit, the sales base is never below the income base, because salt_deduction ≥ min(real_estate, cap) unless the taxable-income limit clips it. So the base and credit weakly rise, and Utah income tax weakly falls.

  • Scope.

  • CI. Pull request run 41474534138 at this head completed success, with every listed job success. The skipped run 41472971525 was the draft-time run.

  • impact: and axiom: lines. Both are present (impact: pending (hub), axiom: us-ut:statutes/59-10-1018 encoded-correct).

Population check

Variables beyond COMMON:

  • inputs: state_sales_tax, local_sales_tax, state_withheld_income_tax, local_income_tax, state_and_local_sales_or_income_tax, real_estate_taxes;
  • federal intermediates: salt_deduction, total_itemized_taxable_income_deductions, itemized_taxable_income_deductions, itemized_taxable_income_deductions_reduction, tax_unit_itemizes;
  • Utah: ut_federal_deductions_for_taxpayer_credit, ut_taxpayer_credit_max, ut_taxpayer_credit, ut_income_tax.

Expected direction: the Utah credit total rises and ut_income_tax falls, or stays flat. Check whether state_and_local_sales_or_income_tax is a dataset input (P2). If it is, the size and even the existence of the effect depend on that.

What I couldn't check

  • Utah sources. le.utah.gov and files.tax.utah.gov are blocked from this VM (proxy 403; DNS refused through WebFetch). I didn't read any edition of §59-10-1018, the 2018/2023 "paid and reported" text, or the TC-40 line 14 instructions. Everything above about the statute's wording rests on the brief's quotes.
  • Federal sources. uscode.house.gov, govinfo.gov and ecfr.gov were also blocked, so §§164 and 68 are described from the model's code, not the statute.
  • Axiom parity. This session's GitHub access doesn't include TheAxiomFoundation/rulespec-us, so I couldn't read us-ut/statutes/59-10-1018.yaml or test it on Cases 6-10. The encoded-correct claim is unverified here.
  • Population data. Whether datasets supply state_and_local_sales_or_income_tax, and the population impact, need the hub's data.
  • Memory. I didn't measure CI memory for the property test.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

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


Generated by Claude Code

1 similar comment
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

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


Generated by Claude Code

Bring the branch up to date with main (76 commits) before the review
fixes, so CI tests them against current code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FzNbZSacUp8TcfjZi836q
… base

Under a sales-tax election the base was max(itemized, non-SALT + SALT).
The second term is the total before section 68, so in 2013-2017 and from
2026 the base could exceed the federal itemized deductions. The statute
starts from those deductions and removes no income tax under a sales-tax
election, so the base is now the federal itemized total itself (Cases 11,
13 and 14; the 2017 witness had a base of $120,000 instead of $82,845).

The election was recomputed from its components, so it could disagree
with a supplied state_and_local_sales_or_income_tax, the amount the
federal SALT deduction uses. A unit now counts as a sales-tax election
only when that amount is the sales tax. A supplied amount matching
neither component keeps the income-tax treatment (Case 12, the earlier
Case 5 inputs, back at $6,000).

Also cite the 2023 edition of 59-10-1018, the text for 2018 through 2025.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FzNbZSacUp8TcfjZi836q
The property test always supplied a federal itemized total at or above
the reconstruction (non-SALT deductions plus allowed SALT), so section 68
never entered and putting max(itemized, reconstruction) back as the
sales-tax base passed it. The draws now cut the supplied total by up to
80%, an explicit example pins a cut below the reconstruction under both
elections, and the sales-tax property asserts the base is never above
the supplied total. 2017 joins the years drawn.

Pairs with a cut are left out of the property that a sales-tax election
never lowers the base: the income-tax branch still floors its base at the
reconstruction before section 68, which this change does not touch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FzNbZSacUp8TcfjZi836q
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6KGcTTH9bwvB1ny1VV

# Conflicts:
#	CHANGELOG.md
#	policyengine_us/tests/policy/baseline/gov/states/tx/twc/ccs/tx_ccs.yaml
#	pyproject.toml
#	uv.lock
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub. I'll post a hand-off comment here when it's ready. This continues an earlier session's work on this brief: its commits 7d387e2, 0d2afbb and 35d7e73 are on the branch, and I'm checking each against the review before building on them.


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 97aab82e35171c24894181c791391de0765a649e

This continues an earlier session on this brief. It pushed 7d387e25fa (merge main), 0d2afbb31f (P1 line 65, P2 election, Cases 11-14, 2023 reference) and 35d7e732e8 (property-test §68 draws), then stopped without a hand-off. I checked each of those commits against the review and kept all three unchanged. My only commit is 97aab82e35, a merge of current origin/main. The branch was 10 commits behind again. The only conflicts were in the bot-managed version files CHANGELOG.md, pyproject.toml and uv.lock, plus tx_ccs.yaml. The branch doesn't touch any of them, so each took main's side. The diff against main is still the four PR files.

No commit or comment from anyone else is dated after 2026-11-03T16:20Z.

Findings

Finding What was done Where
P1: sales branch max ignores §68 Kept the earlier session's replacement: sales_claimed_base = itemized, with the review's comment ut_federal_deductions_for_taxpayer_credit.py:74-76
P1: YAML pin for the max Case 11 (2026 JOINT): supplied itemized_taxable_income_deductions 14,000 is below non-SALT 6,000 + salt_deduction 9,000, under a sales election. Expects 14,000, credit 840 ut_federal_deductions_for_taxpayer_credit.yaml:183
P1: 2026 witness Case 13: single filer, $1.5M LTCG, $100,000 cash charity, $20,000 sales tax. Expects 96,959.46 yaml:221
P1: 2017 witness Case 14: the same filer in 2017. Expects 82,845 yaml:257
P2: election vs a supplied combined amount Kept the review's replacement: (sales_tax > income_tax) & np.isclose(raw_state_income_or_sales_tax, sales_tax) .py:41-46
P2: old Case 5 inputs Case 12 (2023 JOINT, itemized 15,000, supplied combined 9,000, no income). Expects 6,000 yaml:206
P2: 2018-2025 methodology question Closed by the hub's ruling. The change stays undated, and the body cites the 2017, 2023 and 2026 editions plus H.B. 77 with links PR body "Source"
2023 edition in reference Added, with a comment that it covers May 8, 2018 through tax year 2025. The 2026 link's comment says it covers tax years from 2026 .py:14-20
P3: body "Draft" line, readers sentence, invariant 1 All three are in the body with the review's wording. I only added this session's link PR body
P3: property test New assertion: the sales-election base ≤ supplied itemized where itemized > 0 (test_ut_taxpayer_credit_sales_tax_properties.py:179). Draws now cut the supplied total by 0-80% (:50, :82), so it can fall below the reconstruction. An @example pins a 10% cut under both elections (:131). 2017 is added to the years drawn test file

Hand computations (before comparing with the YAML)

  • Case 11: sales 9,000 > income 0, and the combined amount from its formula is 9,000 = sales, so this is a sales election. The base is the supplied 14,000 (6% → credit 840). Main removes salt_deduction − 0 = 9,000: 14,000 − 9,000 = 5,000, floored at the 6,000 fallback → 6,000.
  • Case 12: no income, so state_sales_tax comes from the table (not 9,000) and isclose fails, giving the income branch. 15,000 − 9,000 = 6,000.
  • Case 13, 2026:
    • Charitable: 100,000 − 0.5% × 1,500,000 = 92,500.
    • SALT: the 40,400 cap phases down by 30% × (1,500,000 − 505,000), which hits the 10,000 floor. min(20,000, 10,000) = 10,000.
    • Total 102,500. §68: 2/37 × min(102,500, excess over the 37% threshold, which is far above 102,500) = 5,540.54.
    • Base: 102,500 − 5,540.54 = 96,959.46. Main gives 92,500 and the old max 102,500.
  • Case 14, 2017:
    • No SALT cap or charitable floor: 100,000 + 20,000 = 120,000.
    • Pease: min(3% × (1,500,000 − 261,500), 80% × 120,000) = min(37,155, 96,000) = 37,155.
    • Base: 120,000 − 37,155 = 82,845. Main gives 100,000 and the old max 120,000.

All four match the YAML, and Cases 13 and 14 also pin total_itemized_taxable_income_deductions, itemized_taxable_income_deductions_reduction and itemized_taxable_income_deductions.

Tests at head (one file per process, local)

File Result
ut_federal_deductions_for_taxpayer_credit.yaml 14 passed
tests/core/test_ut_taxpayer_credit_sales_tax_properties.py 1 passed (12 s in pytest)
ut_taxpayer_credit.yaml 1 passed
ut_taxpayer_credit_max.yaml 2 passed
ut_taxpayer_credit_reduction.yaml 3 passed
contrib hb210/ut_hb210_s2_marriage_credit.yaml 7 passed
contrib ut_hb210_reform_test.yaml 8 passed

ruff format --check and ruff check pass on both changed Python files. I ran no full suites or microsimulations.

Mutants

Each mutant ran in its own process on the working file. Afterwards I restored it, and cmp against a saved copy plus git diff --quiet confirmed the restore.

Mutant YAML cases that fail Property test Killed by
M1: main's formula 6, 7, 10, 11, 13, 14 fails both
M2: >= on ties 9 passes Case 9
M3: sales_claimed_base = max_(itemized, sales_fallback_base) 11, 13, 14 fails both. It survived everything at b4ca92ba03
M4: sales fallback uses capped_real_estate_tax 10 fails both
M5: drop the np.isclose condition 12 passes Case 12

state_and_local_sales_or_income_tax supplied, matching neither component

I read this at .py:41-71. sales_tax_elected is false, so the unit takes the income branch, the same as on main:

  • with salt_deduction > 0, it removes salt_deduction − min(real-estate tax, statutory cap);
  • with zero salt_deduction and a supplied itemized total above the fallback, it removes the supplied combined amount;
  • the result is floored at non-SALT + capped real-estate tax.

Case 12 exercises this: 6,000, the same as main.

Income-branch witness (not fixed here; for the hub's follow-up)

The case is 2025 JOINT in Utah: $8,000 state_withheld_income_tax, $6,000 real-estate tax, and $150,000 wages. I added the wages because with no income the model clips salt_deduction to 0. I ran it at head with Simulation(situation=...):

  • Model's own itemizing choice: it doesn't itemize. Itemized is 14,000 against a standard deduction of 31,500, so the base is 31,500 and nothing is removed.
  • Itemizer (tax_unit_itemizes: true, plus $30,000 home_mortgage_interest): salt_deduction 14,000 and itemized 44,000. The code removes 8,000, giving 14,000 − min(6,000, cap) and a base of 36,000. (1)(e) of the 2023 edition removes min($10,000, $8,000 paid and reported) = 8,000. They agree.
  • Forced itemizer with no other deductions: the code also removes 8,000 (14,000 → 6,000).
  • Contrast, the same itemizer with $15,000 of income tax: salt_deduction 21,000 and itemized 51,000. The code removes 15,000 (base 36,000), while (1)(e) removes min(10,000, 15,000) = 10,000 (base 41,000). The gap is 5,000 of base, or 300 of credit at 6%.

Judgment calls

  • I kept all three of the earlier session's commits, because each one does what the review and this brief ask. I re-ran its YAML and property tests and its mutants rather than relying on its message.
  • In the property test, invariant 3 (the sales election never lowers the base) leaves out pairs whose supplied total is cut. That's because the income branch still floors at a total before §68, which is one of the review's follow-ups and isn't fixed here.
  • Merge conflicts took main's side. They were all files outside the PR's change.

CI

The Pull request run at this head, https://github.com/PolicyEngine/policyengine-us/actions/runs/41710758293, was still in progress 60 minutes after the push (checked at about 21:14Z). Every job that had finished was success, including:

  • PR validation;
  • Household API Partners;
  • Package and compatibility;
  • Quick Feedback;
  • Python tests: all 9 Rest shards and the Microsimulation job;
  • Baseline tests: all 12 shards;
  • Contrib tests: congress, other-shard-2a, other-shard-2b and states-shard-3.

Five Contrib shards were still running: other-shard-1, other-shard-3, states-shard-1, states-shard-2 and states-shard-4. The hub should read the run's conclusion before landing.

Not checked

  • I didn't read le.utah.gov or the TC-40 instructions from this VM. The statute readings in the body are the hub's, as the brief gave them.
  • I didn't verify the Axiom parity line (encoded-correct), because rulespec-us is outside this session's access.
  • I didn't measure the population impact. impact: pending (hub) stays in the body.

Proposed follow-up

Utah taxpayer credit: measure the removed income tax by the statute, and apply §68 to the income-branch floor. For 2018-2025, remove the lesser of $10,000 and the income tax paid and reported, rather than residual SALT. From 2026, also cap it at the amount deducted. The income-branch floor should start from the federal itemized deductions after §68, and its real-estate cap should use salt_cap, not the statutory maximum.


Generated by Claude Code

@MaxGhenis

MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Hub review in progress

Done. The final review is #10597 (comment), at head 97aab82e35171c24894181c791391de0765a649e.


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: APPROVE
97aab82
METHODOLOGY_CHOICE: yes

The head hasn't moved past 97aab82e35. Since round 1 (b4ca92ba03), it adds a merge of main (7d387e25fa), the P1, P2 and reference fixes with Cases 11 to 14 (0d2afbb31f), the property test's §68 draws (35d7e732e8), and a second merge of main (97aab82e35). The diff against main is still the four PR files.

Every round 1 finding is fixed as round 1 asked, and every mutant is killed. CI is green. No finding below changes a computed result for a household that supplies the components. The P2 below concerns what the change can reach in microdata, and the body's claim about it. The hub's population run should settle it.

The methodology choices are these: a tie goes to the income-tax branch; a supplied state_and_local_sales_or_income_tax that matches neither component goes to the income-tax branch; and, per the hub's ruling, the sales branch applies undated across the 2017, 2023 and 2026 editions.

Findings

P2. In microdata the election test probably never fires, so the body's "Years affected" overstates the reach

ut_federal_deductions_for_taxpayer_credit.py:44-46; PR body line 59.

state_and_local_sales_or_income_tax is in policyengine_us/tools/default_uprating.py INPUT_VARIABLES, and it also has an entry in policyengine_us/data/economic_assumptions.py:64. real_estate_taxes is on the input list too. state_sales_tax, local_sales_tax and state_withheld_income_tax are not; they come from formulas (the sales-tax table, and ut_withheld_income_tax's estimate). So where the dataset supplies the combined amount, a unit counts as a sales-tax elector only when the reported amount equals the table's sales tax to np.isclose tolerance. Otherwise it gets main's income-tax treatment, which removes the residual SALT, sales tax included.

Witness (2025 JOINT Utah, $50,000 wages, $3,000 real-estate tax, itemizing). The supplied combined amount is $4,000, as if the return deducted $4,000 of sales tax. Model components: state_sales_tax 799, state_withheld_income_tax 1,541.25. I ran a Simulation(situation=...) script on this head and on origin/main. Both print:

W1 supplied 4,000 2025 {'ut_federal_deductions_for_taxpayer_credit': 3000.0, 'itemized_taxable_income_deductions': 7000.0, 'salt_deduction': 7000.0, 'state_and_local_sales_or_income_tax': 4000.0, 'state_sales_tax': 799.0, 'state_withheld_income_tax': 1541.25, ...}

The $4,000 matches neither component, so the head removes it (7,000 − 3,000 real estate), the same as main. If that $4,000 was a sales-tax deduction, the statute as the hub reads it keeps it, for a base of 7,000.

This is the conservative reading. A reported amount that differs from the model's withholding estimate is more often income tax than sales tax in an income-tax state, so I don't ask for a code change. The body should say it, though. Replace the "Years affected" line (body line 59) with:

Years affected: every year in which a unit itemizes with a sales-tax election that the model can identify: the sales tax strictly exceeds the income tax, and state_and_local_sales_or_income_tax equals the sales tax. Where a dataset supplies state_and_local_sales_or_income_tax and it matches neither component, the unit keeps main's result. So the population effect depends on whether the dataset supplies that variable.

Population check: count the Utah itemizers with state_sales_tax + local_sales_tax > state_withheld_income_tax + local_income_tax and isclose(state_and_local_sales_or_income_tax, sales). Do it once with the dataset's supplied value and once with the variable's formula. If the first count is about zero, the change moves no baseline aggregate.

P3. A supplied combined amount equal to the sales tax but below the income tax is treated as an income-tax election

ut_federal_deductions_for_taxpayer_credit.py:44-46.

The strict sales_tax > income_tax term duplicates what isclose already implies when the variable comes from its formula (max_). When the variable is supplied, it decides against the supplied amount. Witness: 2025 JOINT, $100,000 wages, state_withheld_income_tax 5,000, state_sales_tax 2,000, supplied state_and_local_sales_or_income_tax 2,000 and charitable_deduction 6,000. The federal SALT is 2,000, the sales tax. Head and main both give base 6,000, because 2,000 is removed. Run output at head: W2 raw=sales<income 2025 {'ut_federal_deductions_for_taxpayer_credit': 6000.0, 'itemized_taxable_income_deductions': 8000.0, 'salt_deduction': 2000.0, ...}. The supplied amount says the federal side used the sales tax, so the sales branch would keep 8,000. The input is unusual, which is why I rank it P3. Replacement that identifies the election only from the amount used:

        # Sales-tax election only where the SALT amount the federal side used
        # is the sales tax and not the income tax; a tie or a supplied amount
        # that matches neither keeps the income-tax treatment.
        sales_tax_elected = np.isclose(
            raw_state_income_or_sales_tax, sales_tax
        ) & ~np.isclose(raw_state_income_or_sales_tax, income_tax)

I applied it on a scratch copy as mutant "ALT". The YAML passed 14/14 and the property test passed. When the variable comes from its formula, this matches the current line, because ties fail the second term (Case 9). It also still sends Case 12 to the income branch. Afterwards the code was restored, and git status was clean.

P3. Two adjacent "Sales-tax election" comments

ut_federal_deductions_for_taxpayer_credit.py:71-75. The first comment ("the whole allowed SALT deduction stays in the base") describes only the fallback on line 73. The second describes line 76. Suggested merge:

        # Sales-tax election: 59-10-1018 removes no sales tax. The base is the
        # federal itemized deductions after section 68; when that is zero, the
        # fallback is the non-SALT deductions plus the allowed SALT deduction.
        sales_fallback_base = fallback_itemized + salt_deduction
        sales_claimed_base = itemized

Round 1 findings at this head

Round 1 finding Fixed? Where Rerun
P1: sales branch max ignores §68 Yes, as written .py:76 sales_claimed_base = itemized 2026 witness 96,959.46 (main 92,500, round 1 head 102,500); 2017 witness 82,845 (main 100,000, round 1 head 120,000)
P1: YAML pin for the max Yes Case 11, yaml:183 head 14,000 / credit 840; main 6,000 / 360
P1: two witnesses as YAML Yes Case 13 yaml:221, Case 14 yaml:257 both also pin the total, the §68 reduction and the federal itemized amount
P2: election vs supplied combined amount Yes, as written .py:44-46 old Case 5 inputs give 6,000 (credit 360) at head, as on main; round 1's head gave 15,000
P2: restore old Case 5 inputs Yes Case 12, yaml:206 6,000. Case 5 (yaml:62) keeps the income-tax components it gained in round 1 and still expects 6,000
P2: 2018-2025 methodology Closed by the hub's ruling body lines 16-24; 2023 edition added to reference with its years, .py:18-20 not verifiable here (sources blocked, see below)
P3: body ("Draft" line, readers sentence, invariant 1) Yes body lines 1, 65, 78 readers checked by grep, see below
P3: property test can't see §68 Yes assertion test_...properties.py:179; cuts drawn 0-80% at :50, :82; @example at :131 M3 now fails the property test

Checked and sound

  • Defect at main. I copied the PR's YAML into an origin/main worktree (6145ad45) and ran it there: 6 failed (Cases 6, 7, 10, 11, 13, 14), 8 passed. At head: 14 passed.

  • Hand values, computed before running. For each year I name the §68 rule, using the model's parameters (limitation/applies is true 2013-2017 and from 2026; obbb/applies from 2026):

    • Case 11 (2026 JOINT, §68 inputs supplied): sales 9,000 > income 0, and the formula's combined amount 9,000 = sales, so it's a sales election. The base is the supplied 14,000, and 6% gives 840. Main: 14,000 − (9,000 − 0) = 5,000, floored at 6,000 + 0, so 6,000.
    • Case 13 (2026 SINGLE, OBBBA §68, 2/37 rule):
      • charitable 100,000 − 0.5% × 1,500,000 = 92,500;
      • SALT cap 40,400 − 30% × (1,500,000 − 505,000) falls below the 10,000 floor, so min(20,000, 10,000) = 10,000;
      • total 102,500;
      • the excess over the top bracket's start (640,600) is 859,400, so the reduction is 0.05405405 × min(102,500, 859,400) = 5,540.54;
      • base 96,959.46.
    • Case 14 (2017 SINGLE, Pease):
      • no SALT cap or charitable floor, so the total is 120,000;
      • reduction min(3% × (1,500,000 − 261,500), 80% × 120,000) = min(37,155, 96,000) = 37,155;
      • base 82,845.
    • Case 12 (2023 JOINT, no §68): with no income, salt_deduction is 0 and state_sales_tax comes from the table (492 in my two-person run), so isclose(9,000, 492) fails and the unit takes the income branch. The override path removes the supplied 9,000, giving 15,000 − 9,000 = 6,000.

    The Simulation script printed each of these at head (Case 13: total 102,500, reduction 5,540.54, base 96,959.46; Case 14: 120,000, 37,155, 82,845).

  • The code across years, filing statuses and entities. The sales branch returns itemized_taxable_income_deductions. That's the federal amount after §68 in every year, whether §68 is off, Pease applies, or the 2/37 rule applies. The filing-status-dependent pieces (caps, thresholds) all sit in the federal variables. The income branch computes the same thing as main's formula, with renamed variables (.py:61-70 against main's lines 37-44). Standard-deduction filers are unchanged (.py:82-86). The variable is TaxUnit-level, with defined_for = StateCode.UT.

  • Matching neither component (asked by the brief). sales_tax_elected is false, so the unit takes main's income branch. With salt_deduction > 0 that removes salt_deduction − min(real estate, statutory cap) (W1 above). With salt_deduction == 0 and a supplied itemized total above the fallback, it removes the supplied amount (Case 12). The result is floored at non-SALT + capped real estate. This matches the hand-off.

  • Tests at head, one file per process, all passed:

    File Passed
    ut_federal_deductions_for_taxpayer_credit.yaml 14
    ut_taxpayer_credit.yaml 1
    ut_taxpayer_credit_max.yaml 2
    ut_taxpayer_credit_reduction.yaml 3
    ut_non_refundable_credit_order.yaml 1
    ut/tax/income/integration.yaml 2
    ut_income_tax_before_credits.yaml 1
    ut_income_tax_before_non_refundable_credits.yaml 3
    ut_income_tax_before_refundable_credits.yaml 2
    contrib hb210/ut_hb210_s2_marriage_credit.yaml 7
    contrib ut_hb210_reform_test.yaml 8
    tests/core/test_ut_taxpayer_credit_sales_tax_properties.py 1

    For the property test, the test call took 3.5 s, pytest 11.7 s in all, and the cold process 54 s, with about 990 MB peak RSS. That's local; I didn't read the CI core group's memory report. ruff format --check and ruff check pass on both Python files.

  • Mutants. Each ran on the working file in its own process, one process per test file. After each, I restored the file and checked it with cmp against a saved copy and git diff --quiet, and git status was clean at the end.

    Mutant YAML fails Property test Killed by
    M1: main's formula Cases 6, 7, 10, 11, 13, 14 fails both
    M2: >= on ties Case 9 passes Case 9
    M3: max_(itemized, sales_fallback_base) Cases 11, 13, 14 fails both (it survived everything in round 1)
    M4: sales fallback uses capped_real_estate_tax Case 10 fails both
    M5: drop the np.isclose term Case 12 passes Case 12
    M6 (mine): drop the sales_tax > income_tax term, keeping isclose alone Case 9 passes Case 9
  • Property test strength. Assertion (1) checks equality with the supplied total, and line 179 adds the upper bound. Draws reach both ends of the cut (0 and 80%) and both supply_itemized values. The paired half never ties, since _with_sales_election adds 1. The drawn half ties only when two draws coincide. Case 9 is the only reliable pin for ties, which is why M2 and M6 pass the property test. itemized is an input there, so computed §68 enters only through YAML Cases 13 and 14. The test doesn't supply state_and_local_sales_or_income_tax, so M5 passes it; Case 12 kills M5. SURVIVING_SPOUSE and 2019-2023 aren't drawn; the model's rules for those years match 2018 and 2024.

  • Readers. The only formula reader of ut_federal_deductions_for_taxpayer_credit is ut_taxpayer_credit_max. Its readers are ut_taxpayer_credit and the contrib reforms/states/ut/ut_hb210.py:31. ut_taxpayer_credit is read by ut_income_tax_before_non_refundable_credits. The body's readers sentence is accurate. At baseline the change can only raise the base, and so the credit, for an identified sales elector (Case 11: 6,000 → 14,000), so Utah tax weakly falls. No reader is made worse.

  • Income-branch witness (follow-up, not this PR). I reran the hand-off's 2025 JOINT case at head, with $150,000 wages, $6,000 real-estate tax, $30,000 mortgage interest and forced itemizing:

    • $8,000 of state income tax: the code removes 8,000 (base 36,000), and min($10,000, $8,000) also gives 8,000.
    • $15,000 of state income tax: the code removes 15,000 (base 36,000), against 10,000 under the hub's reading of the 2023 edition's (1)(e).

    This matches the hand-off. The gap is the same on main, since the income branch is unchanged.

  • Scope.

  • CI. Pull request run 41710758293 at this head completed success, with 36 of 36 jobs success and none cancelled or skipped. The five Contrib shards the hand-off listed as still running finished success.

  • impact: and axiom: lines. Both are present (impact: pending (hub), axiom: us-ut:statutes/59-10-1018 encoded-correct). I left them alone.

What I couldn't check

  • Utah sources. le.utah.gov and files.tax.utah.gov refused connections from this VM (proxy 403; DNS failure through the fetch tool), and so did web.archive.org. So I read none of the 2017, 2023 or 2026 editions of §59-10-1018, H.B. 77, or the TC-40 line 14 instructions. The "(1)(f)(i)" pin cite in the code comments, the changelog and the body's readings of each edition rest on the hub's reading.
  • Federal sources. govinfo.gov, uscode.house.gov, ecfr.gov and law.cornell.edu were blocked too. §§68 and 164 appear above only as the model's parameters and code implement them.
  • Population data. I couldn't check whether the dataset supplies state_and_local_sales_or_income_tax (P2), or the population impact.
  • Axiom parity. rulespec-us is outside this session's GitHub access, so encoded-correct is unverified.
  • CI memory. I didn't read the CI core group's memory report for the new property test.

Generated by Claude Code

@DTrim99

DTrim99 commented Nov 4, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Keep itemized sales taxes in Utah's taxpayer credit base

Summary

This PR fixes a real error. For an itemizer who deducted sales tax instead of income tax on the federal return, ut_federal_deductions_for_taxpayer_credit removed the leftover SALT, which was the sales tax, from the Utah taxpayer credit base. I read every edition of Utah Code § 59-10-1018 directly: the pre-2018 text, the 2018, 2021 and 2023 editions, and the 2026 edition. Each one removes only state or local income tax. The TC-40 line 14 instructions for 2017, 2018, 2019, 2023, 2024 and 2025 all say not to include sales tax.

The sales branch now returns the federal itemized deductions after § 68. That matches the law in every year the PR claims. Ten YAML expectations check out by hand. A small check at this head also matches: a sales-tax itemizer's credit is $222 (6% × $3,700) higher than an income-tax itemizer's with the same amounts.

One should-fix remains. The election test ignores a supplied sales-tax election when the sales tax is below the income tax. A Utah filer can rationally make that election, and the statute then keeps the sales tax in the base. The income-tax branch hasn't changed from main and still departs from the 2018-2025 statute. The body already flags that as a follow-up.

Critical

None.

Should address

1. A supplied sales-tax election is overridden when the sales tax is below the income tax. ut_federal_deductions_for_taxpayer_credit.py:44-46

sales_tax_elected = (sales_tax > income_tax) & np.isclose(
    raw_state_income_or_sales_tax, sales_tax
)

When state_and_local_sales_or_income_tax comes from its formula (max_(income_tax, sales_tax)), isclose(raw, sales_tax) already implies sales ≥ income. The strict term then only decides ties (Case 9).

When the amount is supplied, the strict term overrides it. A unit whose supplied SALT amount equals its sales tax, and not its income tax, goes to the income-tax branch, and its sales tax is removed. That election is rational for a Utah filer. Choosing sales tax costs the federal difference times the marginal rate. In return, Utah removes nothing from the credit base, and a later state income tax refund isn't federally taxable.

Witness: I ran this at this head. 2025 JOINT, Utah, $80,000 wages, $36,000 charitable deduction, $3,700 state income tax paid, $3,600 state sales tax, supplied state_and_local_sales_or_income_tax $3,600, itemizing.

  • Head:
    • salt_deduction 3,600; federal itemized 39,600.
    • ut_federal_deductions_for_taxpayer_credit 36,000.
    • Credit max 2,160, reduction 566.46, ut_taxpayer_credit 1,593.54.
    • Main gives the same.
  • Law: the filer reported $3,600 of sales tax on Schedule A line 5a and no income tax.
    • § 59-10-1018(1)(e)-(f) (2023 edition) removes only "state or local income tax" that the claimant "pays for the taxable year; and ... reports on the claimant's federal individual income tax return".
    • The 2025 TC-40 instructions (line 14, PDF page 10) say "Do not include any sales tax that you itemized."
    • So the base is 39,600. Credit max 0.06 × 39,600 = 2,376; credit 2,376 − 566.46 = 1,809.54.
    • The head understates the credit by $216.
  • Federal cost of the election: $100 less SALT × 12% bracket = $12, against a $216 Utah gain.

Why this is a should-fix rather than critical: the model has no input for the election, so which election a unit made is an inference, and this case only arises when the combined amount is supplied. When it does arise, the result contradicts the statute. It also contradicts the PR's own comment at lines 41-43, which says the election comes from "the SALT amount the federal side used".

Fix: the replacement proposed in the 2026-11-03 review comment. That comment reports that all 14 YAML cases and the property test pass with it.

        sales_tax_elected = np.isclose(
            raw_state_income_or_sales_tax, sales_tax
        ) & ~np.isclose(raw_state_income_or_sales_tax, income_tax)

When the formula supplies the amount, this gives the same result as the current line: ties fail the second term, so Case 9 still gives 6,000. Case 12 still goes to the income branch. Please also add a YAML case with the witness inputs, expecting 39,600 (credit max 2,376).

Suggestions

  1. Income-tax branch: file the follow-up now and link it. This branch is unchanged from main and outside this PR's scope. But the references now cite the 2023 edition, which this branch doesn't follow, and the gap is larger than the body's $15,000 example shows.
    • Witness (traced by hand): 2024 JOINT, $10,000 real-estate tax, $8,000 state income tax, $1,000 sales tax, enough AGI.
      • salt_deduction = min(18,000, 10,000) = 10,000.
      • Removed = 10,000 − min(10,000, 10,000) = 0.
    • The 2023 edition removes min($8,000, $10,000) = $8,000. The 2024 TC-40 line 14 agrees ("enter that amount on line 14, up to $10,000"). The 2018 and 2019 instructions say "enter that amount", meaning the income tax on Schedule A line 5a.
    • So the head overstates the base by $8,000, or $480 of credit before phase-out.
    • The 2025 instructions switch to "Schedule A, line 5e, minus any amounts on lines 5b and 5c". That matches main's leftover-SALT method but conflicts with the 2023 edition's (1)(e). The follow-up therefore needs a source-conflict register entry.
  2. References.
    • The change applies in 2017 (Case 14), but the variable cites only the 2023 and 2026 editions. Please add the pre-2018 edition: https://le.utah.gov/xcode/Title59/Chapter10/C59-10-S1018_1800010118000101.html. There the rule sits in § 59-10-1018(2)(a)(ii)(A)(II) ("any amount of state or local income taxes the claimant deducts as allowed as an itemized deduction"), not in (1)(f)(i) as the changelog and docstrings say.
    • The comment "the text in force from May 8, 2018 through tax year 2025" is accurate for (1)(e)-(f), which read the same in the 5/8/2018, 5/5/2021 and 5/3/2023 editions. Other subsections changed in between, so "(1)(e)-(f) text" would be more precise.
    • The first reference (§ 59-10-114, additions to AGI) was already there before this PR and doesn't address this credit.
  3. Comments.
    • Lines 71-75 have two "Sales-tax election" comments next to each other. The first describes only the fallback on line 73.
    • Lines 35-36 say "a strictly larger sales amount is a sales election". After fix 1, that should read: the federal SALT amount equals the sales tax and not the income tax.
  4. Ties (Case 9). With equal amounts, a Utah filer would report sales tax: the federal deduction is the same, Utah keeps it in the base, and a later state refund isn't federally taxable. Treating a tie as an income-tax election is conservative, and the PR documents it as a methodology choice. A one-line reason in the Case 9 comment would help.
  5. Property test cost. CLAUDE.md "Test design and CI cost" asks a new Python test that builds simulations to report elapsed time and peak memory, and to check the affected CI group's report.
    • The hand-off comments give local figures: about 54 s cold and about 990 MB peak RSS. The body doesn't.
    • The YAML cases alone kill every mutant in the author's table.
    • Please put the measurements and the test's distinct coverage (SEPARATE and HEAD_OF_HOUSEHOLD filers, 2018 and 2024 draws) in the body. Property tests under tests/core/ are an established pattern, so this is housekeeping.
  6. Impact. The body still says impact: pending (hub). The 2026-11-03 review worried that a combined amount supplied by the dataset would rarely match the table sales tax. In the current microdata build, the PUF tax-detail outputs include real_estate_taxes but not state_and_local_sales_or_income_tax. So the formula's max() should supply it, and the election test can fire in microdata. Please confirm in the impact run before rewording "Years affected".

Verified correct

  • Statute, read directly from le.utah.gov:
    • Pre-2018 edition (superseded 5/8/2018), § 59-10-1018(2)(a)(ii)(A): the base is the federal itemized deduction minus "(II) any amount of state or local income taxes the claimant deducts as allowed as an itemized deduction". It has no sales-tax term.

    • 5/8/2018, 5/5/2021 and 5/3/2023 editions: (1)(e) and (1)(f) read the same in all three. "State or local income tax" is the lesser of:

      • (i) the amount the claimant "(A) pays for the taxable year; and (B) reports on the claimant's federal individual income tax return ..., regardless of whether the claimant is allowed an itemized deduction ... for the full amount"; and
      • (ii) $10,000.

      (1)(f)(i): the federal itemized deduction "minus any amount of state or local income tax for the taxable year".

    • 2026 edition (effective 1/1/2026; amended by ch. 299, 2026 General Session): (1)(e)(ii) becomes "the amount of state or local income tax that is included and allowed as an itemized deduction". (2)(a)(ii) gives 6% of the Utah itemized deduction. The PDF anchor #page=1 holds (1)(e)-(f).

    • No edition removes sales tax, and a sales-tax elector reports and deducts no income tax.

  • TC-40 instructions, read directly:
    • Line 14 says "Do not enter any sales tax from Schedule A, line 5" (2017) or "line 5a" (2018, 2019). 2023 and 2024 say "Do not include any sales tax that you itemized". 2025 says the same, on PDF page 10, so the anchor is right.
    • Line 12 is the federal itemized total after § 68 and the Pease limitation (2017 Form 1040 line 40; 2025 line 12e). So itemized_taxable_income_deductions is the right sales-branch base.
    • Statute and instructions agree for sales-tax electors, so this branch has no source conflict.
  • Hand checks:
# Case Arithmetic Expected Match
1 Case 5 (2023 JOINT) No AGI, so salt_deduction is 0. Income branch; the supplied 15,000 > fallback 0, so the supplied 9,000 is removed: 15,000 − 9,000 6,000 yes
2 Case 6 (2025) Sales 9,000 > income 0 and raw = 9,000, so sales election. Base 15,000; 6% = 900. Main: max(15,000 − 9,000, 6,000) = 6,000 15,000 / 900 yes
3 Case 7 (2024) Sales election, base 16,000; 6% = 960. Main: 16,000 − (10,000 − 4,000) = 10,000 16,000 / 960 yes
4 Case 8 (2025) Income 9,000 > sales 2,000; removes 9,000. 2023 edition: min(9,000, 10,000) = 9,000. 15,000 − 9,000 6,000 yes
5 Case 9 (tie) Treated as income: 15,000 − 9,000 6,000 yes
6 Case 10 No itemized total, so fallback 6,000 + 9,000 15,000 yes
7 Case 11 (2026) Sales election; supplied 14,000 kept; 6% = 840. 2026 (1)(e) removes min(0, 0) = 0 14,000 / 840 yes
8 Case 12 (2023) Table sales tax ≠ 9,000, so income branch: 15,000 − 9,000 6,000 yes
9 Case 13 (2026 SINGLE) Charitable 100,000 − 0.5% × 1,500,000 = 92,500. SALT cap 40,400 − 30% × 995,000 < 0, so the 10,000 floor; SALT 10,000. Total 102,500. § 68: 2/37 × min(102,500, 1,500,000 − 640,600) = 5,540.54. Base 102,500 − 5,540.54 96,959.46 yes
10 Case 14 (2017 SINGLE) 100,000 + 20,000 = 120,000. Pease: min(3% × 1,238,500, 80% × 120,000) = min(37,155, 96,000) = 37,155. Base 120,000 − 37,155 82,845 yes
11 Sales-tax itemizer (run at head; 2025 JOINT, $80k wages, $36k charity, sales 3,700 > income 3,600) Itemized 39,700; base 39,700; credit max 0.06 × 39,700 = 2,382; reduction 0.013 × (80,000 − 36,426) = 566.46; credit 2,382 − 566.46 1,815.54 yes (run)
12 Income-tax itemizer (same, income 3,700 > sales 3,600) Itemized 39,700; removes 3,700 (2023 edition: min(3,700, 10,000)); base 36,000; credit 2,160 − 566.46 1,593.54 yes (run)
13 Supplied sales election, sales < income See should-fix 1 head 1,593.54; law 1,809.54 finding

Rows 11 and 12 differ by 2,382 − 2,160 = 222 = 6% × 3,700, the sales tax the PR now keeps. Main gives 1,593.54 for row 11 as well.

  • Code:
    • Fully vectorized (where, max_, np.isclose), TaxUnit / YEAR, and defined_for = StateCode.UT is unchanged.
    • No new state-gated int or enum key, so the NH Add New Hampshire state supplementation for 2026 #10156 pattern doesn't apply.
    • The income branch is main's formula with variables renamed. Standard-deduction filers are unchanged.
  • Readers: only ut_taxpayer_credit_max reads this variable. ut_taxpayer_credit and the contrib ut_hb210 reform read ut_taxpayer_credit_max. At baseline the change can only raise a sales-tax elector's base.
  • Tests: they're in the variable-named YAML file. Case 5 still expects 6,000 with explicit components, and Case 12 keeps the old inputs. The changelog fragment is top-level (changelog.d/ut-taxpayer-credit-sales-tax.fixed.md, type fixed).
  • Source-conflict guard: the new comments use none of its trigger phrases, CI ran the guard green, and main hasn't changed tests/code_health since the merge base.
  • Scope: no partner-test files and no workflow files are touched.

Axiom line assessment

axiom: us-ut:statutes/59-10-1018 encoded-correct is valid for what this PR changes.

The rulespec-us module us-ut/statutes/59-10-1018.yaml exists, with a signed encoding manifest generated 2026-07-08. It encodes:

  • utah_itemized_deduction = max(0, itemized_deductions_allowed_on_federal_return - state_or_local_income_tax);
  • state_or_local_income_tax = min(paid_and_reported, included_and_allowed_as_itemized_deduction).

It removes only income tax, never sales tax. For a sales-tax elector, the income tax included and allowed is 0, so the module returns the federal itemized deductions after § 68, which is exactly the PR's sales branch.

Caveat for the follow-up, not this PR: the module has a single version, effective from 0001-01-01, that encodes the 2026 text in every year. It doesn't carry the 2018-2025 measure ("regardless of whether ... allowed", capped at $10,000) or the pre-2018 text. All editions agree for sales-tax electors, so this PR is unaffected. The income-branch fix in suggestion 1 will need a dated rulespec version, so its axiom line can't be encoded-correct against this module.

CI status

All 38 checks pass at 97aab82 (re-checked with gh pr checks 10597 at the end of the review).

Branch status

The branch is 8 commits behind main. git merge-tree --write-tree PolicyEngine/main 97aab82e35 merges cleanly. Since the merge base, main hasn't changed the PR's files, the Utah credit files, the federal itemizing variables or the code-health tests.

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.

Requesting changes: 0 critical, 1 should. The sales-tax branch matches § 59-10-1018 in every claimed year, and the axiom line is valid. Open: the strict sales_tax > income_tax term overrides a supplied sales-tax election when sales tax is below income tax (2025 example: credit 1,593.54 where the law gives 1,809.54); use the isclose test the report suggests and pin it. Full report in the comment above.

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

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


Generated by Claude Code

…come tax

The review found that the election test required sales tax above income
tax, so a supplied federal SALT amount equal to the sales tax was treated
as an income-tax election and the sales tax was removed. A filer can make
that election, and 59-10-1018 then keeps the sales tax in the base. The
test now asks only whether the amount equals the sales tax and not the
income tax. A tie still takes the income-tax branch (Case 9), and so does
a supplied amount that matches neither (Case 12).

Case 15 is the review's witness: a 2025 joint return with $3,600 of sales
tax, $3,700 of income tax and a supplied $3,600. Its base is now $39,600
and its credit $1,809.54, where the old test gave $36,000 and $1,593.54.

The references add the pre-2018 edition, whose rule sits in
(2)(a)(ii)(A)(II), and drop 59-10-114, which covers additions to and
subtractions from income and not this credit. The changelog and the
property test's docstring no longer put the pre-2018 rule in (1)(f)(i).
Two adjacent comments merge into one, and Case 9 gives its reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N2tyHT12N9L3WVu8rJokf1
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: ac417e0352fb20d6c0303892b2bbf0b2954adce3

I built on 97aab82e35, the head the item named. No other commit landed on the branch while I worked. The round adds one commit, ac417e0352.

Items

  1. Main. No merge. GitHub reports the PR mergeable: true. git merge-tree against main at ba9bfdea75 is clean, and main changed none of the PR's four files; its Utah changes are the homeowner and renter relief files. The review names no failure on the merged tree and no main test to run.
  2. Findings.
    • Should-address 1, a supplied sales-tax election. I reproduced the witness at 97aab82e35 first: a 2025 joint return with $80,000 of wages, $36,000 of charity, $3,700 of income tax, $3,600 of sales tax and a supplied $3,600. It gives salt_deduction 3,600, federal itemized 39,600, base 36,000, credit maximum 2,160, reduction 566.46 and credit 1,593.54. The fix:
      • ut_federal_deductions_for_taxpayer_credit.py:47 applies the review's replacement: isclose(raw, sales) & ~isclose(raw, income). The comment at :43 says what it tests.
      • ut_federal_deductions_for_taxpayer_credit.yaml:292 adds the witness as Case 15: base 39,600, credit maximum 2,376, reduction 566.46, credit 1,809.54.
      • Case 9 (the tie) still gives 6,000, and Case 12 still takes the income-tax branch. All 15 cases pass.
    • Suggestion 1, the income-tax branch. Left as it is, as the brief says. I reran the review's witness at the final head: a 2024 joint return with $10,000 of real-estate tax, $8,000 of income tax and $1,000 of sales tax gives salt_deduction 10,000 and base 10,000. The 2023 edition removes $8,000, so the credit maximum is $600 where it should be $120. The body's follow-up note now carries this example. It also notes that the 2025 TC-40's line 5e method agrees with the branch, so that fix needs its own source-conflict entry. No issue is linked.
    • Suggestion 2, references.
      • Added the pre-2018 edition at .py:16, with its rule at (2)(a)(ii)(A)(II); I read that edition at le.utah.gov.
      • Dropped § 59-10-114. I read its 2022 and current versions: it covers additions to and subtractions from AGI and never mentions § 59-10-1018, itemized deductions, or income or sales tax.
      • The changelog no longer cites (1)(f)(i).
      • The property test's docstring (:3-7) gives each edition's subsection.
      • Case 14's comment (.yaml:264) cites (2)(a)(ii)(A)(II).
      • The 2023 reference's comment (.py:17-20) says the 2021 edition has the same (1)(e)-(f), and the edition effective May 8, 2018 has them as (1)(g)-(h). I fetched all three.
    • Suggestion 3, comments. The two adjacent "Sales-tax election" comments are one (.py:74-76). The "strictly larger" comment is gone, and :43-46 says what the test now asks.
    • Suggestion 4, the tie's reason. Case 9 (.yaml:145-146) now says the amount equals both taxes, so it doesn't show which one the return deducted, and the conservative reading removes it.
    • Suggestion 5, the property test's cost. In the body, with the test's distinct coverage. The figures are under "Tests".
    • Suggestion 6, impact. The body reads impact: pending a comparison at the final head.; the hub fills it.
  3. Source conflicts. None for this branch. For sales-tax electors the statute's editions and the TC-40 instructions agree; I read the 2017, 2018, 2021, 2023 and 2026 editions and the 2025 TC-40 line 14. The income-tax branch's conflict with the 2025 TC-40 belongs to the hub's separate PR. No candidates.yaml entry.
  4. Axiom line. The review finds it valid for what this PR changes, so I kept it and drafted nothing.
  5. Mutants. See "Mutants".
  6. Tests. See "Tests".
  7. Body. Updated and read back:
    • the Source section now says this round read each edition, with the subsections;
    • the election bullet describes the new test;
    • the follow-up note has the review's witness;
    • Case 15 is in the witness table;
    • "Years affected" names identifiable elections;
    • the property test paragraph gives its distinct coverage and cost;
    • the impact line and the first line's CI pointer match this head.

Rulings for the hub

No new ruling. One change reverses a methodology choice that the earlier body and the hub's 2026-11-03 approval recorded. A supplied combined amount equal to the sales tax now counts as a sales-tax election even when the sales tax is below the income tax. Before, that took the income-tax branch. Both reviews asked for it, and it follows § 59-10-1018(1)(e)-(f) and the TC-40 line 14, which I read. The two other recorded choices stand: a tie, and a supplied amount that matches neither component, still take the income-tax branch.

Tests

One process per file, at ac417e0352:

File Result
credits/ut_federal_deductions_for_taxpayer_credit.yaml 15 passed
tests/core/test_ut_taxpayer_credit_sales_tax_properties.py 1 passed
code_health/test_source_conflicts.py 103 passed
credits/ut_taxpayer_credit.yaml 1 passed
credits/ut_taxpayer_credit_max.yaml 2 passed
credits/ut_taxpayer_credit_reduction.yaml 3 passed
ut/tax/income/integration.yaml 2 passed
contrib hb210/ut_hb210_s2_marriage_credit.yaml 7 passed
contrib ut_hb210_reform_test.yaml 8 passed

ruff format and ruff check pass on both Python files.

The property test's cost (suggestion 5).

  • Local, with /usr/bin/time -v on a 4-vCPU VM with nothing else running: 7.64 s of pytest time, 0:51.96 of wall time for the cold process, and 1,011,648 KB peak RSS.
  • CI, from the timing artifact of the run at 97aab82e35 (41710758293): 6.48 s of test time in Rest shard 3, group core-1 (49 files). That process ran 28:25 and peaked at 10,900,036 KB, above the Makefile's target of about 10 GB per process. The peak isn't this file's alone; run by itself, the file peaks at about 1 GB.
  • The run at this head hadn't reached its timing artifacts at hand-off.

Distinct coverage: the YAML's sales-tax cases are all JOINT or SINGLE filers in 2017, 2024, 2025 and 2026. The property test adds SEPARATE and HEAD_OF_HOUSEHOLD filers and 2018, and it checks both branches' bounds on randomized amounts.

Hand computations

  • Case 15.
    • Federal SALT = min(0 + 3,600, $40,000 cap) = 3,600. Itemized = 36,000 + 3,600 = 39,600, with no § 68 reduction in 2025.
    • The supplied amount equals the sales tax and not the income tax, so nothing is removed, and the base is 39,600. Line 16: 0.06 × 39,600 = 2,376.
    • Lines 17 to 19: line 9 is $80,000 and the 2025 joint base amount is $36,426 (TC-40 instructions, page 10), so 0.013 × 43,574 = 566.462.
    • Line 20: 2,376 − 566.46 = 1,809.54. Under the old test, 0.06 × 36,000 − 566.46 = 1,593.54, so the difference is 216 = 0.06 × 3,600.
  • Suggestion 1 witness. SALT = min(10,000 + 8,000, 10,000) = 10,000. The branch removes 10,000 − min(10,000, 10,000) = 0. The 2023 (1)(e) removes min(8,000, 10,000) = 8,000. 0.06 × 8,000 = 480.

Mutants

Each ran in its own process, in a scratch worktree. I restored the file after each one, and git status was clean afterwards.

Mutant Result Killed by
U1: the old test, (sales > income) & isclose(raw, sales) (undoes the fix) killed, 1 of 15 Case 15 (36,000 vs 39,600)
U2: drop & ~isclose(raw, income) killed, 1 of 15 Case 9 (15,000 vs 6,000)
U3: drop isclose(raw, sales) & killed, 1 of 15 Case 12 (15,000 vs 6,000)

U1 is the old head's code, so Case 15 fails without the fix.

CI

Pull request run 42655090497 at ac417e0352 was in progress at hand-off. 2 of its 26 jobs had succeeded and 24 were running, with none skipped or cancelled. The hub reads the result.

Judgment calls

  • Drop § 59-10-114. It doesn't address this credit (see suggestion 2). The TC-40 cites § 59-10-114(2)(c) for the state tax refund line, line 7, not this base.
  • Letter differences between editions. The review says (1)(e)-(f) read the same in the 2018, 2021 and 2023 editions. The text is the same, but in the edition effective May 8, 2018 it sits at (1)(g)-(h), so the comments say that. I couldn't fetch the edition effective July 21, 2018 (the file name I tried returned 404), so no comment claims anything about it.
  • "Years affected." I reworded it to identifiable elections, which holds whatever the dataset supplies. The two reviews disagree on whether the microdata supplies state_and_local_sales_or_income_tax: the hub's 2026-11-03 review finds it in the uprating input list, and DTrim99 finds it missing from the PUF outputs. The hub's impact run settles that.
  • The body's session links. Earlier sessions' two links at the end of the body stay as they were; I added none.

Follow-ups

None new. The income-tax branch is the hub's tracked PR, and the body's note carries the review's witness.

DISMISSAL MAP

  • Critical: the review lists none.
  • Should-address 1 (a supplied sales-tax election overridden when sales < income): FIXED. ut_federal_deductions_for_taxpayer_credit.py:47; Case 15 at ut_federal_deductions_for_taxpayer_credit.yaml:292; U1 fails it; Cases 9 and 12 unchanged.
  • Suggestion 1 (file and link the income-branch follow-up): ANSWERED. The branch is unchanged, per the brief; the hub tracks the fix as its own PR. The body's note carries the review's witness ($8,000 base, $480 credit) and the TC-40 conflict, and links no issue.
  • Suggestion 2 (references: pre-2018 edition, (1)(e)-(f) wording, § 59-10-114): FIXED. Pre-2018 edition at .py:16; § 59-10-114 dropped; edition wording at .py:17-20; docstring and changelog corrected; Case 14 cites (2)(a)(ii)(A)(II) at .yaml:264.
  • Suggestion 3 (two adjacent comments; the "strictly larger" comment): FIXED. .py:43-46 and .py:74-76.
  • Suggestion 4 (the tie's reason): FIXED. Case 9 at .yaml:145-146.
  • Suggestion 5 (the property test's cost and coverage in the body): FIXED. Local and CI figures, and the distinct coverage, in the body's Invariants section.
  • Suggestion 6 (impact): FOR THE HUB. impact: pending a comparison at the final head.

Generated by Claude Code

@DTrim99

DTrim99 commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 2): Keep itemized sales taxes in Utah's taxpayer credit base

Summary

The round-1 should-fix is fixed as proposed. ut_federal_deductions_for_taxpayer_credit now treats a unit as a sales-tax elector when the federal SALT amount equals its sales tax and not its income tax. The old extra test, sales tax above income tax, is gone. New Case 15 is the round-1 witness and expects a base of $39,600 and a credit of $1,809.54. It fails on the round-1 code, which gives $36,000. Ties (Case 9) and a supplied amount that matches neither tax (Case 12) still take the income-tax branch. When the formula supplies the amount, results don't change.

Five of the six suggestions are fixed. One is declined with a documented reason, and one is still open as housekeeping. I re-read every edition of § 59-10-1018 and the 2025 TC-40 instructions, and they support the new references and comments.

The encoded-correct axiom line holds: the rulespec-us module gives the PR's answer for every companion case. CI is green on GitHub's merge with current main (38/38). That merge is the same tree a local merge-tree gives. No critical or should-fix findings are open.

Prior findings

Finding Status Evidence
Should 1: a supplied sales-tax election was overridden when the sales tax is below the income tax Fixed .py:47-49 is now np.isclose(raw, sales_tax) & ~np.isclose(raw, income_tax), the replacement round 1 proposed. New Case 15 (2025 JOINT, $80,000 wages, $36,000 charity, income tax $3,700, sales tax $3,600, supplied $3,600) expects SALT 3,600, federal itemized 39,600, base 39,600, credit maximum 2,376, reduction 566.46 and credit 1,809.54. Mutation check: under the round-1 line, 3,600 > 3,700 is false, so the unit takes the income branch. It removes 3,600 − min(0, cap) = 3,600, so the base is max(39,600 − 3,600, 36,000 + 0) = 36,000 and Case 15 fails. Dropping either new term is also caught: Case 9 kills dropping ~isclose(raw, income) (15,000 vs 6,000), and Case 12 kills dropping isclose(raw, sales) (15,000 vs 6,000).
Suggestion 1: file and link the income-branch follow-up Declined with reason (acceptable) The branch is unchanged from main and outside this PR. The body's follow-up note now has round 1's 2024 witness: a $10,000 base where the 2023 edition gives $2,000 ($10,000 − $8,000), so $480 of credit. It also notes that the 2025 TC-40's line 5e method agrees with the branch, so that fix needs its own source-conflict entry, and that the fix will be filed after this PR lands. No issue is linked yet. Carried below as an optional suggestion.
Suggestion 2: references Fixed The pre-2018 edition was added with its rule at (2)(a)(ii)(A)(II) (verified below). § 59-10-114 was dropped. The changelog and the property-test docstring no longer put the pre-2018 rule in (1)(f)(i). The 2023 reference's comment says the edition effective May 8, 2018 has the text at (1)(g) and (1)(h)(i). I fetched that edition, and it does. This corrects round 1's statement that the letters were the same in that edition.
Suggestion 3: comments Fixed .py:43-46 describes the new test. The two adjacent "Sales-tax election" comments are merged into one (.py:74-76).
Suggestion 4: the reason for ties Fixed The Case 9 comment now says the amount equals both taxes, so it doesn't show which one the return deducted, and the conservative reading removes it as income tax.
Suggestion 5: the property test's cost Fixed The body gives local figures, labelled as local: 7.64 s pytest, 51.96 s cold wall time and 1,011,648 KB peak RSS. It gives the CI timing from the 97aab82 run and the test's distinct coverage (SEPARATE and HEAD_OF_HOUSEHOLD filers, 2018). Figures for this head are now available (suggestion 1).
Suggestion 6: impact Open (housekeeping) The line still reads impact: pending a comparison at the final head. "Years affected" now says "a sales-tax election the model can identify: its federal SALT amount equals its sales tax and not its income tax", which matches the new test.

New findings

Critical

None.

Should address

None.

Suggestions

  1. Refresh the body's status lines. The first line and the Invariants paragraph say run 42655090497 was still running and quote the core group's 10,900,036 KB peak from the earlier 97aab82 run.
    • Run 42655090497 has now finished success, with 36 of 36 jobs passing.
    • In its timing artifacts, the property test took 0.58 s in Rest group core-1.
    • That process ran 22:09 and peaked at 7,395,764 KB, below the Makefile's target of about 10 GB per process.
    • Please replace the old figure. Please also fill in the impact: line (round-1 suggestion 6).
  2. Income-tax branch: link the follow-up when it's filed (round-1 suggestion 1). This is optional. The body's note is enough for review, but a link keeps the $480 witness and the 2025 TC-40 conflict from getting lost after merge.
  3. Optional, reference comment (.py:17-20). The comment names the 2021 and May 8, 2018 editions. The edition effective July 21, 2018 was in force until May 5, 2021, so it covers tax years 2018 to 2020. It has the same text at (1)(e) and (1)(f)(i): https://le.utah.gov/xcode/Title59/Chapter10/C59-10-S1018_2018051620180721.html. The hand-off says that edition couldn't be fetched. It loads at this URL.

Verified

  • Statute, read this round from le.utah.gov:
    • Edition superseded May 8, 2018, § 59-10-1018(2)(a)(ii)(A). The base is "(I) the amount the claimant deducts as allowed as an itemized deduction ... and (II) any amount of state or local income taxes the claimant deducts as allowed as an itemized deduction". The rule sits at (2)(a)(ii)(A)(II), as the new reference comment and Case 14 say.
    • Edition effective 5/8/2018 (superseded 7/21/2018). "State or local income tax" is at (1)(g), and "Utah itemized deduction" is at (1)(h)(i).
    • Editions effective 7/21/2018, 5/5/2021 and 5/3/2023. The same text is at (1)(e) and (1)(f)(i). (1)(e)(i) covers income tax the claimant "(A) pays for the taxable year; and (B) reports on the claimant's federal individual income tax return ..., regardless of whether the claimant is allowed an itemized deduction". (1)(e)(ii) is $10,000.
    • 2026 edition (effective 1/1/2026). (1)(e)(ii) becomes "the amount of state or local income tax that is included and allowed as an itemized deduction". (1)(f)(i) is unchanged.
    • No edition removes sales tax. A sales-tax elector reports no income tax on Schedule A, so nothing is removed in any year. The property-test docstring's subsection cites ((2)(a)(ii)(A) pre-2018; (1)(f)(i) in the 2021, 2023 and 2026 editions) are accurate.
  • 2025 TC-40 instructions, PDF page 10, so the #page=10 anchor is right.
    • Line 14: "If you took state and local income tax as an itemized deduction on your federal return (1040 Schedule A, line 5a), enter the amount from federal form 1040, Schedule A, line 5e, minus any amounts on lines 5b and 5c. ... Do not include any sales tax that you itemized."
    • Line 17 gives a $36,426 joint base phase-out amount. That matches the existing phase_out/threshold.yaml value for 2025, which Case 15 uses.
  • Hand checks of every new or changed expectation, and of the pins tied to the fix:
# Case Arithmetic Expected Match
1 Case 15, salt_deduction min(0 real estate + 3,600 supplied, $40,000 2025 joint cap; MAGI $80,000 means no phase-down) 3,600 yes
2 Case 15, federal itemized 36,000 charity + 3,600 SALT; no § 68 reduction in 2025 and no 2026 charitable floor 39,600 yes
3 Case 15, base isclose(3,600, 3,600) is true and isclose(3,600, 3,700) is false, so sales election. Nothing removed 39,600 yes
4 Case 15, credit maximum 0.06 × (39,600 + 0 personal exemption, no dependents) 2,376 yes
5 Case 15, reduction 0.013 × (80,000 − 36,426) = 0.013 × 43,574 = 566.462 566.46 yes
6 Case 15, credit 2,376 − 566.46. The round-1 code gives 2,160 − 566.46 = 1,593.54, so the gap is 216 = 6% × 3,600 1,809.54 yes
7 Case 9 (tie; comment changed) Both isclose terms are true, so income branch. 15,000 − (9,000 − 0) = 6,000, floored at 6,000 6,000 yes
8 Case 12 (supplied amount matches neither tax) isclose(9,000, table sales tax) is false, so income branch. SALT is 0 with no AGI. The supplied total beats the fallback, so 15,000 − 9,000 6,000 yes
9 Case 14 (comment changed) 120,000 − min(3% × 1,238,500, 80% × 120,000) = 120,000 − 37,155. The pre-2018 (II) removes only income taxes 82,845 yes
  • Behavior when the formula supplies the amount. With raw = max(income, sales), the new test agrees with the old sales > income test except at near-ties. A gap within np.isclose's default tolerance now counts as a tie and goes to the income branch. That tolerance is relative 1e-5, so the gap must be under $1 at $100,000 of tax. Such near-ties are negligible, and treating them as ties is consistent with Case 9.
    • The property test doesn't supply raw. Its paired draws set sales = income + 1 + a drawn amount, with amounts capped at 60,000. A $1 gap falls within tolerance only from about $100,000, so the test's sales_tax > income_tax classification still matches the formula's. It passed in CI.
    • When the amount is supplied, the result now follows it: equal to the sales tax only means the sales branch, equal to the income tax or to both means the income branch, and matching neither means the income branch.
  • Code. The formula is vectorized, with TaxUnit / YEAR and defined_for = StateCode.UT unchanged. No new state-gated int or enum key, so the NH Add New Hampshire state supplementation for 2026 #10156 pattern doesn't apply. The income branch and the standard-deduction path are unchanged. The only reader is still ut_taxpayer_credit_max.
  • Source-conflict ratchet. I ran main's CONFLICT_COMMENT_RE over every line of the three changed files at this head and got no matches. None of them is in KNOWN_UNMARKED.
  • References follow the CLAUDE.md rules: one entry per document, no title in the variable tuple, and single-page anchors (#page=1, #page=10). All four URLs resolve.
  • Tests and changelog. Case 15 is in the variable-named YAML file. The changelog fragment is still top-level, type fixed, and no longer gives a pin cite that is wrong for 2017.
  • Scope. The PR still changes four files. No partner-test or workflow files are touched.

Axiom

axiom: us-ut:statutes/59-10-1018 encoded-correct is valid.

  • The module. us-ut/statutes/59-10-1018.yaml exists in rulespec-us, with 59-10-1018.test.yaml beside it. It hasn't changed since 2026-07-09. It encodes:
    • state_or_local_income_tax = min(paid_and_reported_on_federal_return, included_and_allowed_as_itemized_deduction);
    • utah_itemized_deduction = max(0, itemized_deductions_allowed_on_federal_return - state_or_local_income_tax);
    • a credit of 0.06 × that amount, less 0.013 × max(0, state taxable income − threshold). The joint threshold is twice the CPI-adjusted single threshold.
  • Sales-tax companion cases. For these, the income tax included and allowed is 0, so nothing is removed and the module returns the federal itemized deductions after § 68:
Case Year Module base PR base
6 2025 15,000 15,000
7 2024 16,000 16,000
10 2025 15,000 15,000
11 2026 14,000 14,000
13 2026 96,959.46 96,959.46
14 2017 82,845 82,845
15 2025 39,600 39,600
  • Case 15 end to end: the deduction component is 2,376. With CPI inputs that give the published single threshold of $18,213, the reduction is 0.013 × (80,000 − 36,426) = 566.462 and the credit allowed is 1,809.54. Both match the PR.
  • Income-tax companion cases (Cases 8 and 9, and Cases 5 and 12 with income tax deducted): the module removes min(9,000, 9,000) = 9,000, giving 6,000, as the PR does.
  • Caveat (unchanged from round 1, not this PR's problem). The module has one version, effective from 0001-01-01, that encodes the 2026 text in every year. No companion case has income tax above the allowed amount or above $10,000, so the 2018-2025 text gives the same results here. The income-branch follow-up will need a dated rulespec version.

CI

gh pr checks 10597, re-checked at the end of the review: 38 of 38 pass.

Pull request run 42655090497 at ac417e0352 completed success, with 36 of 36 jobs passing and none skipped or cancelled. It ran on GitHub's merge with main, which I confirmed in two ways:

  • It executed the Rest groups ss-history-engine and ss-history-differential, which exist only in main's Makefile.
  • GitHub's merge commit ea173d0c has tree 1406a8d2db, the tree that git merge-tree gives against current main 61d9e8520c. Main hasn't moved since that run started.

So the main-side guards passed on the merged tree: the #10287 source-conflict ratchet with its current KNOWN_UNMARKED, #9558, #10317, #10408, the programs registry and the Rest Python group checks.

Branch

Verdict: APPROVE

@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: the supplied sales-tax election fix is in and pinned (new Case 15 fails on the round-1 code), the reference and comment suggestions are fixed, the encoded-correct axiom claim holds, and CI is green on the merged tree. Three optional suggestions remain.

@MaxGhenis MaxGhenis closed this Nov 10, 2026
@MaxGhenis MaxGhenis reopened this Nov 10, 2026
@MaxGhenis MaxGhenis closed this Nov 10, 2026
@MaxGhenis MaxGhenis reopened this Nov 10, 2026
@MaxGhenis
MaxGhenis merged commit 96f384f into main Nov 10, 2026
77 of 81 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#10597 at ac417e0352fb20d6c0303892b2bbf0b2954adce3 (squash): Keep itemized sales taxes in Utah's taxpayer credit base.

  • Reviews:
    • An independent review requested changes at b4ca92ba03, and a second approved 97aab82e35.
    • DTrim99 requested changes at 97aab82e35: a supplied election whose sales tax falls short of the income tax lost the sales tax from the base, for a credit of $1,593.54 where the statute gives $1,809.54. ac417e0352 changes the election test and adds that case, and DTrim99 approved ac417e0352.
    • An independent review of that commit agreed at ac417e0352. It recomputed the new case by hand and read six editions of Utah Code 59-10-1018 and the 2025 TC-40 instructions.
  • Gates checked live at merge:
  • Impact (default dataset, ac417e0352 against main at 89a15b72f5): we estimate that the change moves almost nothing. It reaches 6 sampled Utah tax units in 2025 and 5 in 2026, with a combined weight of about 1, and raises the weighted taxpayer credit by $99 and $112.
  • Next: a follow-up PR changes the income tax branch, which removes the residual federal deduction where the statute removes the income tax paid and reported.
  • Axiom: us-ut:statutes/59-10-1018 encoded-correct.

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis deleted the claude/state-itemized-misc-sales-tax-rozit6 branch November 10, 2026 07:44
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.

3 participants