Repository navigation
Guard federal-style bracket thresholds and NaN parameter values; fix the contrib MFS 33% bracket - #9934
Guard federal-style bracket thresholds and NaN parameter values; fix the contrib MFS 33% bracket#9934MaxGhenis wants to merge 3 commits into
Conversation
…racket test_parameter_scale_thresholds only checked ParameterScale brackets, so federal-style schedules (a `thresholds` node mapping bracket numbers to a value per filing status, as gov.irs.income.bracket does) could repeat or invert a threshold unnoticed, and nothing checked parameter values for NaN, which YAML loads silently in scalars and inside list values. The new mapped-threshold guard found one live inversion: the additional_tax_bracket contrib reform's SEPARATE bracket-5 top for 2026-2035 copied the JOINT values (541,550 in 2026, above bracket 6's 305,875). #6235 fixed the same copy in gov.irs.income.bracket; the contrib file kept it. The values are now half the joint thresholds, as in every other bracket and year of the file and in #6235. Under the reform, a separate filer's income from 270,775 to 305,875 was taxed at 33% instead of 35% in 2026. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_shipped_additional_tax_bracket_reform_is_clamped relied on the shipped SEPARATE inversion, which the previous commit removed, so a zero-income filer owed nothing with or without the clamp. The test now recreates the old 541,550 bracket-5 top itself. The mapped-threshold guard now requires a `rates` sibling, the node tax_at_main_rates reads with `thresholds`, so a household-size table named `thresholds` cannot be swept in. New unit cases show it flags a repeated -inf and a NaN threshold, and the NaN test asserts it visited parameters. A second reform case covers the 39.6% band. The old data also taxed a separate filer's income from 305,875 to 541,550 at 33% instead of 39.6% (not only 270,775 to 305,875 at 33% instead of 35%, as the previous commit said): taxable income of 400,000 owed 115,896.25, not 122,810.50. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-2 review: a zero-income filer owes nothing under any loop that is never negative per bracket, so the clamp test could not tell today's unclamped form from the clamped one, nor notice if the recreated inversion failed to apply. A separate filer with 400,000 of taxable income now must owe exactly 115,896.25 under the recreated inversion: 122,810.50 if the inversion did not apply, other figures if the loop did not clamp. (The previous commit's "owed nothing with or without the clamp" held only for the old clip form.) The table finder also skips an empty `thresholds` node, and the 2035 SEPARATE line loses a trailing space. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR Review: Guard federal-style bracket thresholds and NaN parameter values; fix the contrib MFS 33% bracketPR #9934 (author: MaxGhenis, head SummaryThe PR does three things:
The substance is right:
Two should-fix items remain:
Critical (must fix)None. Should address1. The branch conflicts with
2. The PR body still has placeholder sections and asks to be used as the squash commit body.
Suggestions
Verified correct
Axiom line assessmentValid. The line reads
CI statusAll 35 checks pass (
Branch status
Verdict: REQUEST_CHANGES |
DTrim99
left a comment
There was a problem hiding this comment.
The ten new MFS values match the CBO Jan 2025 Table 1 'Sixth rate begins' row and are exact halves of JOINT. The guard catches injected inversions and NaN, all expectations check by hand, and the axiom n/a is valid. Two should items (see the comment above): (1) the branch conflicts with main in test_parameter_scale_thresholds.py and additional_tax_bracket_reform.yaml; both are additive, and a union resolution passes locally, so main's newer parameters violate neither guard; (2) the PR body still has the queued 'Tests' and 'Impact' placeholders and asks to be the squash message, so fill in the verified results. Also worth fixing: the stale 2022 SINGLE 539,000 in the same contrib file (should be 539,900).
Summary
The 2026-10-06 invariants audit's verifier showed that two kinds of malformed parameters escaped the existing checks:
test_parameter_scale_thresholds.pyonly examinesParameterScalebrackets. Schedules such asgov.irs.income.bracketkeep athresholdsnode mapping bracket numbers 1..N to a value per filing status, so a repeated or inverted threshold there went unchecked.tax_at_main_ratesreads bracket i's threshold as its top and clamps an inverted one (income_tax_main_rates returns large negative values (−$81.7k on $8.2k taxable income) under TCJA-expiration parameters; NE/CO/OR conformity imports it into state tax #9084), so either defect silently taxes a band at the wrong rate..nanloads as a float, alone or inside a list value, and every comparison with it is false.This PR extends
test_parameter_scale_thresholds.pyrather than adding a separate raw-file scan. The new checks walk the loaded parameter tree the module already builds, so they also see uprated values.Guards added
test_mapped_bracket_thresholds_strictly_increase: for every rate schedule with athresholdsnode of brackets 1..N beside aratesnode (whattax_at_main_ratesreads; todaygov.irs.income.bracket,gov.irs.capital_gains, and theadditional_tax_bracketandharris.capital_gainscontrib schedules), each filing status's thresholds must strictly increase at every instant any of them changes, uprated years included. Repeated+inf(parked top brackets) is allowed, as in the scale guard. It asserts the two federal tables are found, so it cannot pass vacuously. Unit cases cover a repeat, a later inversion, a repeated-inf, a NaN threshold, parked+inf, and a finite value after+inf.test_parameter_values_are_not_nan: no parameter value may hold NaN, as a scalar or at any depth of a list (core accepts only float, int, bool, None and list values). Unit cases cover[1.0, [2.0, nan]], a scalar NaN and[nan]; the test also asserts it visited parameters.The guard's first catch: the contrib MFS bracket
The mapped-threshold guard fails on main. The
additional_tax_bracketcontrib reform'sSEPARATEbracket-5 top from 2026 copies theJOINTvalues (541,550 in 2026, above bracket 6's 305,875; authored through 2035 and uprated after). #6235 fixed the same copy ingov.irs.income.bracketin 2025; this file kept it. Every other bracket and year in the file hasSEPARATE == JOINT / 2, as 26 U.S.C. 1 sets for married filing separately. The values are now exactly #6235's: 270,775; 276,525; 282,100; 287,700; 293,375; 299,175; 305,100; 311,150; 317,325; 323,625.SINGLEbracket 5 also reads 541,550 in 2026, and that is correct: before TCJA the single and joint 33% bracket tops were equal.With the copied threshold,
tax_at_main_rates' clamp (#9084) emptied bracket 6 and started 39.6% at 541,550, so under the reform a separate filer's income from 270,775 to 305,875 was taxed at 33% instead of 35%, and from 305,875 to 541,550 at 33% instead of 39.6% (up to $16,256.55 less tax per filer in 2026, rising with the uprated thresholds to $19,429.55 in 2035).New YAML cases (
additional_tax_bracket_reform.yaml), 2026 separate filers under the reform, at 10/15/25/28/33/35/39.6% up to 12,150 / 49,300 / 99,500 / 151,625 / 270,775 / 305,875:test_negative_main_rates_inverted_brackets.py::test_shipped_additional_tax_bracket_reform_is_clampedrelied on the shipped inversion to exercise the reform's clamp. It now recreates the old 541,550 top itself and also checks a separate filer inside the inverted band: $400,000 of taxable income must owe exactly $115,896.25, a figure that differs if the inversion fails to apply ($122,810.50) or the loop stops clamping.Note for the merge: the repo squashes with the commits' messages, and the first commit's body understates the old error (it names only the 270,775–305,875 band). Please use this PR's description for the squash body.
Tests (fail before, pass after)
Fail-before and pass-after runs are queued behind the shared test lock; this section is updated when they finish.
Impact
The baseline is unchanged: only a contrib reform parameter changes, and the new tests add no model behaviour. With the
additional_tax_bracketreform applied, 2026, default dataset, main vs this branch:The reform microsimulation is queued behind the shared lock; this section is updated when it finishes.
2025 is unaffected by construction: only
SEPARATEbracket-5 entries dated 2026-01-01 and later changed, and 2025 lookups read the 2025-01-01 entry.axiom: n/a: test guards plus a contrib (hypothetical reform) parameter; no enacted-law provision changes.
Independent review: in progress (in-session Opus reviewers; Subfleet lanes are unavailable today).
🤖 Generated with Claude Code