Skip to content

Tax Maryland's capital gains surtax on net capital gain as the Internal Revenue Code defines it - #9898

Merged
MaxGhenis merged 8 commits into
mainfrom
md-capital-gains-surtax-net-capital-gain
Nov 8, 2026
Merged

MaxGhenis merged 8 commits into
mainfrom
md-capital-gains-surtax-net-capital-gain

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

From tax year 2025, Maryland charges filers whose federal adjusted gross income exceeds $350,000 an additional 2% of the net capital gain in their Maryland adjusted gross income. This PR computes that gain as the Internal Revenue Code defines it and tests federal AGI against the threshold, as the statute says:

  • A net short-term capital gain no longer adds to the base, and a net short-term capital loss still reduces it.
  • Capital gain distributions that filers report without Schedule D now count as long-term gain.
  • Only the head's and spouse's gains count, so a dependent's gains no longer raise the filers' surtax.
  • The model tests federal AGI against the $350,000 threshold, where main tests Maryland AGI.
  • A new input, md_capital_gains_surtax_excepted_gain, takes the gain from assets the statute excepts (Form 502CG line 8). It defaults to $0.

For example, a filer over the threshold with $100,000 of long-term gain and $50,000 of short-term gain owes a $2,000 surtax, where main charges $3,000.

What the sources say

  • Md. Code, Tax-General § 10-105(a)(3) charges "an additional 2% of the amount of net capital gain included in the individual's Maryland adjusted gross income". It takes net capital gain "as defined and determined under the Internal Revenue Code", and (a)(4) limits the tax to individuals "with a federal adjusted gross income in excess of $350,000".
  • 2025 Md. Laws ch. 604 struck "for taxable years 2025 through 2028" from (a)(4), and its section 34 applies the tax to taxable years beginning after December 31, 2024.
  • 26 U.S.C. § 1222(11) defines net capital gain as "the excess of the net long-term capital gain for the taxable year over the net short-term capital loss for such year". Technical Bulletin No. 58, effective December 29, 2025, quotes that definition and applies the tax above $350,000 of federal AGI regardless of filing status.
  • 26 U.S.C. § 852(b)(3)(B) treats a capital gain dividend as gain from a capital asset held more than a year, so distributions count as long-term gain.
  • The Form 502CG instructions tell residents to enter Form 502 line 1c on line 1, and the resident booklet fills line 1c from federal Form 1040 line 7a. Schedule D adds net short-term and net long-term gain on its line 16 and carries a gain there to line 7a. So a filer who follows the forms counts a net short-term gain.
  • In the bulletin's example, the filer enters $610,000 of gain on Form 502CG line 1 and $442,000 from excepted assets on line 8, which leaves $168,000 on line 9. The filer carries line 9 to Form 502 line 20a and multiplies it by 2% on line 21b: $3,360.

Statute or form

The statute and the forms part ways only when the filers have a net short-term gain: the § 1222(11) base leaves it out, and the line 7a figure counts it. For $100,000 of long-term and $50,000 of short-term gain, the statute gives a $2,000 surtax and the forms give $3,000. For $80,000 of short-term gain alone, they give $0 and $1,600.

This PR follows the statute, which the bulletin's definition matches. source_conflicts/candidates.yaml records the conflict as us-md:capital-gains-surtax-short-term-gains, and a comment on the deciding line of md_net_capital_gain.py names it. Its register entry in rulespec-us and the # Source conflict marker come after this PR lands.

The model

Each sum runs over the head and spouse, and each person's distributions reported without Schedule D count only when positive.

  • md_net_capital_gain (new) computes Schedule D line 15 as long-term gain plus those distributions, line 16 as line 15 plus short-term gain, and Form 502CG line 1 as max(0, min(line 15, line 16)).
  • md_capital_gains_surtax_excepted_gain (new input) holds Form 502CG line 8.
  • md_capital_gains_surtax_applies tests whether adjusted_gross_income exceeds surtax_threshold ($350,000) for every filing status, from 2025.
  • md_capital_gains_surtax charges surtax_rate (2%) on max(0, net capital gain − max(0, excepted gain)), the amount on Form 502CG line 9. md_income_tax_before_credits adds it, as on main.

Main adds every tax unit member's short-term and long-term gains, subtracts limited_capital_loss, floors the result at zero and tests Maryland AGI. The three parameters keep their values.

Worked cases

Each household files in Maryland for 2025. The "Before" column runs this PR's YAML cases on main ba14b81f7b.

Household Before After
A single filer with federal AGI of $500,000, $100,000 of long-term gain and $50,000 of short-term gain $3,000 $2,000
A single filer with federal AGI of $450,000 and $80,000 of short-term gain only $1,600 $0
A single filer with $400,000 of wages and $20,000 of capital gain distributions reported without Schedule D $0 $400
A head with $400,000 of wages and $10,000 of long-term gain, and a dependent aged 10 with $300,000 of long-term gain $6,200 $200
A single filer with federal AGI of $360,000, Maryland AGI of $340,000 and $100,000 of long-term gain $0 $2,000
The bulletin's example: federal AGI of $900,000 and $610,000 of gain, $442,000 of it from excepted assets $12,200 $3,360

Main has no excepted-gain input, so its value for the bulletin's example taxes all $610,000.

Invariants

test_md_capital_gains_surtax_invariants.py checks these on a grid of households, and test_md_capital_gains_surtax_properties.py checks them on 25 households that Hypothesis generates:

  1. md_net_capital_gain equals max(0, min(line 15, line 16)) computed from the inputs.
  2. For a return with no dependents and no negative distributions, md_net_capital_gain equals the federal net_capital_gain, a separate § 1222(11) path, less qualified dividends, which § 1(h)(11) adds only for § 1(h).
  3. md_net_capital_gain never exceeds the positive part of the filers' Form 1040 line 7a gain, so federal AGI includes all of it.
  4. The surtax applies when federal AGI exceeds $350,000 and not otherwise, and then equals 2% of max(0, base − max(0, excepted gain)). It never falls below $0 or exceeds 2% of the base.
  5. A dependent's gains, losses and distributions change neither the base nor the surtax.
  6. Raising a filer's long-term gain, short-term gain or distributions by x raises the base by between 0 and x, and a net short-term gain adds nothing. More excepted gain never raises the surtax.
  7. md_income_tax_before_credits adds the surtax once: neutralizing the surtax lowers that tax by the surtax amount.

Tests

  • CI: the Pull request run 41733019138 at 3427c3acd0 passed all 36 jobs.
  • On a scratch merge of 3427c3acd0 with main ba14b81f7b, one process each:
    • the three capital-gains YAML files pass 27, 5 and 7 cases, and the other 49 Maryland tax YAML files pass 236 in five processes;
    • taxsim_outputs.yaml passes 22;
    • test_md_capital_gains_surtax_invariants.py passes 2, test_md_capital_gains_surtax_properties.py 1 and test_capital_gain_distributions_loss_limit.py 9;
    • test_source_conflicts.py passes 103 and test_no_group_variables_in_person_adds.py 10.
  • On main, 16 of the 32 cases in the two surtax YAML files fail, and the "Before" column shows main's values.
  • Both new Python files run in Rest Python's remaining shards. In that CI run, the invariants file's two tests took 28.4 s and the properties test 11.8 s, in shards that peaked at 7.4 and 7.2 GiB.
  • Locally (macOS, Python 3.13), a process that imports the model and runs one household takes 21.1 s and peaks at 1,057 MiB. The invariants file takes 31.0 s and peaks at 2,339 MiB, so it adds about 10 s and 1.3 GiB. The properties file takes 26.5 s at 1,523 MiB, adding about 5 s and 470 MiB.

Not modeled

  • Users enter the excepted gain as one total. The model doesn't check each asset class's conditions, such as a residence's sale price under $1,500,000 or the farming share of gross income for livestock.
  • The model builds the base from the filers' federal gains, so a Maryland subtraction that removes a capital gain from Maryland AGI doesn't reduce it.

Overlap with open PRs

Microsimulation impact

We compared main at 6145ad4513 with 3427c3acd0 on the default dataset for 2025 and 2026. We estimate that Maryland's capital gains surtax falls by $30.0 million in 2025, from $250.1 million to $220.1 million, and by $31.2 million in 2026, from $259.9 million to $228.7 million. Maryland income tax falls by the same amounts, and federal income tax and benefits don't change.

The table applies the PR's changes to main's surtax one at a time, in this order. Each cell gives the change, then the sample tax units it moves and their weight.

Change 2025 2026
Count only the head's and spouse's gains $0 $0
Leave out net short-term gain, under § 1222(11) −$30.1 million: 36 units, 75,543 weighted −$31.3 million: 38 units, 76,082 weighted
Count distributions reported without Schedule D +$134,000: 16 units, 11,325 weighted +$139,000: 16 units, 11,406 weighted
Test federal AGI against the threshold +$13: 2 units, weight under 0.1 +$31: 3 units, weight under 0.1

In the dataset, every tax unit that pays the surtax on main has a net short-term gain, so each pays less. In 2026 one of them stops paying, because it has no net long-term gain. The units that start paying report distributions without Schedule D, or have federal AGI over $350,000 and Maryland AGI under it.

impact: on the default dataset, Maryland's capital gains surtax falls by $30.0 million in 2025 and $31.2 million in 2026 (54 and 57 tax units; 86,867 and 87,488 weighted), and Maryland income tax falls by the same amounts. Federal income tax and benefits don't change.

axiom: TheAxiomFoundation/rulespec-us#1518 queued for Md. Code, Tax-General § 10-105(a)(3)–(4). us-md/statutes/gtg/10-105.yaml encodes the 2% rate and the $350,000 federal AGI threshold and defers additional_capital_gain_tax. Because md_capital_gains_surtax_applies tests federal AGI alone, the issue maps its output 2 to md_capital_gains_surtax_applies & (md_net_capital_gain > 0). rulespec-us already encodes the § 1222(11) base as us:statutes/26/1222#net_capital_gain, and TheAxiomFoundation/rulespec-us#1490 queues § 852(b)(3)(B).

🤖 Generated with Claude Code

Md. Code, Tax-General 10-105(a)(3) taxes an additional 2% of "net capital
gain, as defined and determined under the Internal Revenue Code" included
in Maryland AGI, less gain from excepted assets; (a)(4) applies it when
federal AGI is over $350,000. Technical Bulletin No. 58 gives the IRC
1222(11) definition: net long-term capital gain over net short-term
capital loss.

The model summed every tax unit member's long- and short-term gains,
subtracted the capital loss deduction again, left out capital gain
distributions, and tested Maryland AGI against the threshold.

- md_net_capital_gain (new, Form 502CG line 1): the head and spouse's
  max(0, min(Schedule D line 15, line 16)), with distributions reported
  without Schedule D floored per person. A net short-term gain and
  qualified dividends are not part of it.
- md_capital_gains_surtax_excepted_gain (new input, Form 502CG line 8).
- md_capital_gains_surtax: 2% of max(0, line 1 - line 8) (line 9).
- md_capital_gains_surtax_applies: federal AGI over the threshold.
- Parameter references to the statute, ch. 604 and the 2025 forms.

Tests: YAML cases with Schedule D and Form 502CG line comments, including
the Technical Bulletin 58 example (line 9 = $168,000); property tests
with a differential against the federal net_capital_gain.

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

codecov Bot commented Oct 6, 2026

Copy link
Copy Markdown

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #9898   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files            4         4           
  Lines           76        58   -18     
  Branches         2         1    -1     
=========================================
- Hits            76        58   -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.

@DTrim99

DTrim99 commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Tax Maryland's capital gains surtax on net capital gain as the Internal Revenue Code defines it

PR #9898 (author: MaxGhenis, head 963ae33e10, merge-base b79705b4bd). 12 files, 1 commit.

Summary

The PR rebuilds Maryland's 2% capital gains surtax (Form 502 line 21b) on the statutory base.

  • New base. md_net_capital_gain is IRC § 1222(11) net capital gain over the head and spouse: max(0, min(Schedule D line 15, line 16)), with capital gain distributions reported without Schedule D floored per person.
  • New input. md_capital_gains_surtax_excepted_gain holds the excepted-asset gain (Form 502CG line 8).
  • Threshold. md_capital_gains_surtax_applies now tests federal AGI instead of Maryland AGI.
  • Parameters. The parameter values don't change.

The law and the code agree:

  • Md. Code, Tax-General § 10-105(a)(3)–(4), 2025 Md. Laws ch. 604, Technical Bulletin No. 58 and Form 502CG all support the rate, the threshold, the years, the § 1222(11) base, the treatment of qualified dividends and distributions, the exclusion of dependents and the line 9 arithmetic.
  • All 34 YAML cases pass at the head.
  • The property tests pass, and CI is green (37/37).
  • The only reader of the changed variables is md_income_tax_before_credits, which is the right place. The local tax base is untouched.

Two documentation-level items need a fix before approval:

  1. Two new variable references use PDF anchors that don't show what their titles cite.
  2. A test-file comment, repeated in the PR body, says the deterministic grid "runs without Hypothesis". In fact the module-level importorskip skips the whole file.

Neither changes a result.

Critical (must fix)

None.

Should address

1. Two new variable references point to pages that don't show what the title cites.

  • policyengine_us/variables/gov/states/md/tax/income/capital_gains/md_capital_gains_surtax.py:31-32
  • policyengine_us/variables/gov/states/md/tax/income/capital_gains/md_capital_gains_surtax_excepted_gain.py:31-32
  • PR body. It has the same off-by-one: it cites resident-booklet.pdf#page=21 for "line 21b is line 20a × .02".
  • Fix. Use one reference per page.
    • Booklet: "2025 Maryland Resident Income Tax Instructions, line 20a" at #page=21 and "... line 21b" at #page=22. Or keep only the #page=22 "line 21b" entry.
    • TB-58: "Technical Bulletin No. 58, section II" at #page=1 and "Technical Bulletin No. 58, section III.A example" at #page=3.
  • Impact: references only. No result changes.

2. The test file says the deterministic grid "runs without Hypothesis", but without Hypothesis the whole file is skipped.

  • Where: policyengine_us/tests/test_md_capital_gains_surtax_invariants.py:232 ("# A deterministic grid (runs without Hypothesis).") and :291-292 (module-level pytest.importorskip("hypothesis")).
  • What happens. pytest.importorskip at module level raises Skipped during import, so pytest skips every test in the module. That includes test_grid and test_cases_where_the_form_1040_line_7a_gain_differs, which sit above the call.
  • Reproduced at the head in an environment without Hypothesis: pytest --collect-only reports "SKIPPED [1] ...test_md_capital_gains_surtax_invariants.py:291: could not import 'hypothesis'" and "no tests collected".
  • The PR body says the same: "The file's importorskip makes the Hypothesis test skip where Hypothesis isn't installed."
  • CI is unaffected. CI installs the dev extra (pyproject.toml:57, hypothesis>=6.100.0), so all three tests run there. The claim misleads only someone who runs the file without dev extras.
  • Fix (either):
    • Make the claim true. Move test_properties and its strategies into a separate module (for example test_md_capital_gains_surtax_properties.py) that keeps the module-level importorskip. Then test_grid and the line-7a test always run.
    • Or drop "(runs without Hypothesis)" and correct the PR-body sentence to "the file is skipped where Hypothesis isn't installed".
  • Sibling with the same comment: test_capital_gain_distributions_loss_limit.py:165 (pre-existing, Net capital gain distributions inside the capital loss limit in AGI #9788). Fix both if convenient.

Suggestions

  1. Cite the effective-date section for surtax_applies.
    • Where: policyengine_us/parameters/gov/states/md/tax/income/capital_gains/surtax_applies.yaml:9-10.
    • 2025-01-01: true with no end date rests on two provisions of ch. 604:
    • The cited #page=163 shows neither. Add 2025 Md. Laws ch. 604, § 34 at #page=200. The 2025 booklet reference already corroborates 2025, so this is optional.
  2. Fix the oracle mapping in rulespec-us#1518.
    • The issue defines output 2, additional_capital_gain_tax_applies, as "federal adjusted gross income is more than ... threshold, and net capital gain is more than zero". It marks cases B, C and E not_holds for that reason.
    • "Step 2. Oracle coverage" then says to "Map outputs 2 and 4 to" md_capital_gains_surtax_applies and md_capital_gains_surtax.
    • PE's md_capital_gains_surtax_applies tests federal AGI only, so it returns true for B, C and E. The result is a spurious parity mismatch.
    • Fix: map output 2 to md_capital_gains_surtax_applies & (md_net_capital_gain > 0), or compare outputs 3 and 4 only.
    • This doesn't affect whether the axiom line is valid.
  3. Harden the Hypothesis settings.
    • Where: test_md_capital_gains_surtax_invariants.py:321, @hypothesis.settings(max_examples=25, deadline=None).
    • Evidence. In three local runs at the head with a cold .hypothesis cache, one failed with FailedHealthCheck: Input generation is slow: Hypothesis only generated 2 valid inputs after 72.54 seconds. The other two passed. CI passed, and the local Windows environment likely inflated the first-draw time.
    • The test is also not derandomized, so each CI run draws new examples, and a rare failure on an unrelated PR would not be reproducible.
    • Most siblings use derandomize=True and suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large]: test_capital_gain_rate_group_netting.py:426-433, test_form_4952_section_911_interaction.py:958-963 and test_ctc_credit_limit_worksheets.py:217-221. Match them.
  4. Floor the excepted gain at zero.
    • Where: md_capital_gains_surtax.py:42-44.
    • The statute excepts "any amount of capital gain from the sale or exchange of the following assets" (§ 10-105(a)(3)(ii)). Form 502CG lines 2–7 are each "Net capital gain income from the sale or exchange of ...", and line 9 is "Net capital gain income subject to additional tax (subtract Line 8 from Line 1 ...)" (https://www.marylandcomptroller.gov/content/dam/mdcomp/tax/forms/2025/502cg.pdf#page=1). So line 9 cannot exceed line 1.
    • A negative input currently raises the surtax. Probe at the head: LT 100,000, wages 400,000, excepted −50,000 gives 3,000, which is 3% of line 1.
    • Fix: excepted = max_(0, tax_unit("md_capital_gains_surtax_excepted_gain", period)). The PR already gives the same "never negative" reason for flooring non_sch_d_capital_gains.
  5. Don't hard-code parameter values in documentation strings.
    • Where: md_capital_gains_surtax.py:11-15 ("additional 2% tax", "over $350,000", "times 2%").
    • These go stale if the rate or threshold parameters change. md_capital_gains_surtax_applies already says "more than the threshold".
    • Suggested wording: "the additional tax rate" and "federal adjusted gross income over the threshold".
    • The statutory descriptions in md_capital_gains_surtax_excepted_gain.py ("$1,500,000", "12 months") describe the exception categories, aren't used in logic, and are fine.
  6. State the form-versus-statute choice in md_net_capital_gain's documentation, not only in the code comment.
    • Where: md_net_capital_gain.py:9-20; the code comment is at :62-69.
    • The documentation calls the variable "Form 502CG line 1" without qualification.
    • The form's line 1 instruction says "Residents use Line 1c of Form 502" (502CG #page=2). Line 1c is "the amount of net capital gain or loss reported as taxable income on Line 7a of the federal Form 1040" (booklet #page=13), which also counts a net short-term gain.
    • Suggested addition: "This follows the statute and Technical Bulletin 58 section I rather than the Form 502 line 1c figure, which also counts a net short-term capital gain."
  7. Make the surtax_rate.yaml:1 description specific (existing text in a file the PR touches).
    • Current: "applies this surtax rate to net capital gains for high earners".
    • Suggested: "Maryland applies this additional rate to net capital gain included in Maryland adjusted gross income for filers with federal adjusted gross income above the threshold."
  8. Optional tests.
    • A post-2028 year, such as 2029, to pin the struck "2025 through 2028" sunset. Probe at the head: LT 100,000 plus wages 400,000 gives 2,000 in 2028, 2029 and 2030.
    • A head-of-household case. Probe: head with wages 380,000 and LT 30,000 plus a child gives FAGI 410,000 and a surtax of 600.
  9. PR body housekeeping.
    • The "Microsimulation" section still says "Results will be added here". Fill it in or remove it.
    • The axiom line runs on into a paragraph; keep it to one line.

Verified correct

  • The base is § 1222(11) and nothing else.
    • For L16 = L15 + net ST, max(0, min(L15, L16)) equals max(0, L15) when net ST ≥ 0 and max(0, L15 + ST) when net ST < 0. That is "the excess of the net long-term capital gain ... over the net short-term capital loss" (26 U.S.C. 1222(11)).
    • It matches Schedule D Tax Worksheet line 7, "the smaller of line 15 or line 16" (https://www.irs.gov/pub/irs-prior/i1040sd--2025.pdf#page=15), and TB-58 § I (#page=1).
  • Qualified dividends are excluded. § 1(h)(11)(A) applies "For purposes of this subsection" only.
  • Capital gain distributions are included, with the same per-person floor as filer_loss_limited_net_capital_gains and irs_gross_income.
  • Nothing is double counted.
    • The Schedule D line 13 distributions and the long-term loss carryover are memo items inside long_term_capital_gains. YAML cases cover both.
    • The old second subtraction of limited_capital_loss is gone.
  • Dependents are left out. The PR uses tax_unit_non_dep_add plus a filer mask, consistent with federal AGI.
  • Threshold.
    • It is federal AGI, strictly "in excess of" $350,000, with one value for every filing status (§ 10-105(a)(4); TB-58 p. 1 "regardless of filing status"; 502CG p. 2).
    • Tests pin $350,000 → 0 and $350,001 → 2,000, a joint return at $351,000 → 800, and Maryland additions and subtractions moving Maryland AGI across the threshold while federal AGI does not.
  • Excepted gain and line 9.
    • Line 9 = max(0, line 1 − line 8) (502CG #page=1).
    • The TB-58 example reproduces exactly: (610,000 − 442,000) × 0.02 = 3,360.
  • Callers (grep at the head).
    • md_capital_gains_surtax is read only by md_income_tax_before_credits.py:38.
    • md_capital_gains_surtax_applies is only its defined_for.
    • The two new variables feed only the surtax.
    • No other state or federal variable uses the redefined variables. The federal net_capital_gain and schedule_d_tax_worksheet_form_4952_lines keep their own § 1222(11) logic and are untouched.
  • Credits and local tax.
    • The MD EITC, poverty-line, CDCC and senior credits read md_income_tax_before_credits. This matches Form 502 line 27 and the booklet's credit worksheets ("sum of Lines 21 through 21b", booklet #page=22).
    • The county taxes read md_taxable_income, so the surtax never reaches the local base. This matches the DLS fiscal note's "Local Effect: None".
  • Code patterns.
    • The code is vectorized (max_, min_, masked sums) and contains no hard-coded policy values.
    • Both new variables use defined_for = StateCode.MD. Entities are TaxUnit and periods are YEAR.
    • References are lists of dicts with no page text in titles. Parameter values use 350_000 formatting, and the threshold description follows the house template.
    • The changelog fragment changelog.d/md-capital-gains-surtax-net-capital-gain.fixed.md is top-level and fixed.
  • Parameter values against the sources.
    • 0.02: ch. 604 #page=163; Form 502 line 21b ".02".
    • 350,000: ch. 604 #page=164; 502CG #page=2.
    • true from 2025 with no end date: ch. 604 § 34 #page=200 and the struck sunset at #page=164.
  • Tests.
    • All 34 YAML cases pass at the head (22 + 5 + 7). All periods are plain (2024, 2025).
    • The property file passes 3/3 on warm runs: the grid takes 67 s and the Hypothesis test 153 s.
    • The five changed expectations are each the § 1222(11) amount (3,000 → 2,000; 1,600 → 0; 14,000 → 10,000; 1,440 → 0 with the contradictory capital_losses input restated as ST −55,000; and 6,000 → 2,000 via the excepted-gain input).
  • Hand checks:
    • LT 100,000 + ST 50,000: L15 100,000, L16 150,000, so 100,000 × 0.02 = 2,000.
    • LT 50,000, ST −55,000: L16 = −5,000, so 0.
    • Distributions 10,000, ST −4,000: L15 10,000, L16 6,000, so 120.
    • LT 120,000, ST −70,000, excepted 80,000: line 1 50,000, line 9 = max(0, −30,000) = 0.
    • Spouses A LT −30,000 and B LT 100,000 / ST −10,000: L15 70,000, L16 60,000, so md_net_capital_gain = 60,000.
    • Joint FAGI 351,000, LT 40,000: 800.
    • FAGI 360,000 with Maryland subtractions of 20,000: Maryland AGI 340,000, but the surtax applies, so 2,000.
    • TB-58: 3,360.

Axiom line assessment

Valid queued.

CI status

All green: 37/37 checks pass (gh pr checks 9898). These include all 22 Full Suite jobs (Microsimulation among them), Household API Partners, Lint, and codecov patch/project (100%). The PR touches no partner test files.

Branch status

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.

The surtax base is right: net capital gain under IRC § 1222(11), the treatment of capital gain distributions and qualified dividends, the strict federal AGI threshold, and line 9 (TB-58 gives 3,360) all check out, local tax is untouched, and rulespec-us#1518 is dispatch-ready. Two should items (see the comment above): S1, two reference anchors don't show what their titles cite (booklet line 21b is on #page=22, and the TB-58 example is on p. 3), so use one reference per page and fix the PR body's anchor too; and S2, the invariants file says the grid runs without Hypothesis, but the module-level importorskip skips the whole file, so split the module or correct the comment and the PR body.

@DTrim99

DTrim99 commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 2): Tax Maryland's capital gains surtax on net capital gain as the Internal Revenue Code defines it

PR #9898 (author: MaxGhenis). Head 49558d1f4d; round 1 reviewed 963ae33e10. Since then there are two author commits ("Address Maryland capital gains surtax review and statute witnesses" and "Check optional Hypothesis before loading simulation helpers") and three merges of main, none of which needed hand resolution. Every change to the Maryland capital gains files since round 1 comes from the two author commits.

Summary

Both round-1 should-address findings and all nine suggestions are fixed.

  • References. The anchors now land on the pages their titles cite.
  • Hypothesis split. The Hypothesis property test is in its own module, which skips before it loads the model. The deterministic grid and the line-7a witnesses now run without Hypothesis, as their comment says.
  • New tests. Five YAML witnesses cover:
    • short-term gain alone in 2026;
    • federal versus Maryland AGI;
    • a negative excepted gain;
    • no sunset in 2029;
    • a head of household.
      Each hand-checks to its expected value.
  • Law. The law still matches the code: § 10-105(a)(3)–(4), the § 1222(11) base, § 852(b)(3)(B) distributions, the TB-58 and Form 502CG exceptions, and taxable years 2025 onward.

There are no new critical or should-address findings. Three suggestions remain:

  1. Reference format. The per-page reference entries we asked for in round 1 conflict with the house rule of one entry per document.
  2. Source-conflict backlog. The backlog item for this statute-versus-form conflict will be stale once this merges.
  3. PR body. The validation section is out of date.

CI. 25 of 27 checks pass. The two failures are one ratchet failure on main's side, which #10334 has since fixed. Every file it lists is a Florida or South Dakota LIHEAP file, and none is from Maryland. Merge main and re-run.

Prior findings

Finding (round 1) Status Evidence
S1. Two variable references cite pages that don't show the cited lines Fixed Two files changed (md_capital_gains_surtax.py:30-37 and md_capital_gains_surtax_excepted_gain.py:31-42), and the PR-body anchors are corrected (see the first table below).
S2. "Deterministic grid (runs without Hypothesis)" was false: a module-level importorskip skipped the whole file Fixed Five changes make the comment true (see the second table below).
Suggestion 1. Cite the effective-date section for surtax_applies Fixed surtax_applies.yaml:11-14 adds ch. 604 § 3 at #page=164 (the struck sunset) and § 34 at #page=200. Page 200 reads: "Section 3 of this Act shall take effect July 1, 2025, and shall be applicable to all taxable years beginning after December 31, 2024."
Suggestion 2. Oracle mapping in rulespec-us#1518 Fixed Step 2 of the issue now reads: "Map output 2 to md_capital_gains_surtax_applies & (md_net_capital_gain > 0) and output 4 to md_capital_gains_surtax".
Suggestion 3. Harden the Hypothesis settings Fixed The test now uses derandomize=True, deadline=None and suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large]. The example count is still 25, matching the sibling tests.
Suggestion 4. Floor the excepted gain at zero Fixed Four changes (see the third table below).
Suggestion 5. No parameter values in documentation strings Fixed The surtax, net-capital-gain and excepted-gain documentation now says "the additional tax", "over the threshold" and "the additional tax rate".
Suggestion 6. State the form-versus-statute choice in the documentation Fixed md_net_capital_gain.py:19-22: "This follows the statute and Technical Bulletin 58 section I rather than the Form 502 line 1c figure specified by the Form 502CG instructions, which also counts a net short-term capital gain."
Suggestion 7. Make the surtax_rate.yaml description specific Fixed It now uses the suggested text word for word.
Suggestion 8. Optional tests for 2029 and head of household Fixed Both tests were added. Their expected values (2,000 and 600) match the round-1 probes.
Suggestion 9. PR body housekeeping Fixed The unfinished Microsimulation section is gone, and the axiom declaration is on one line.

S1 details

Location Entry Page
md_capital_gains_surtax.py:30-37 line 20a 21
md_capital_gains_surtax.py:30-37 line 21b 22
md_capital_gains_surtax_excepted_gain.py:31-42 TB-58 section II 1
md_capital_gains_surtax_excepted_gain.py:31-42 TB-58 exceptions A–F 2
md_capital_gains_surtax_excepted_gain.py:31-42 TB-58 section III.A example 3

I checked the page text for each: Line 20a is on booklet p. 21 and Line 21b on p. 22. TB-58 p. 2 has "the dwelling sold for less than $1,500,000", and the Jane McDonald example is on p. 3.

S2 details

Change Where
The property test and its strategies moved to the new module test_md_capital_gains_surtax_properties.py
pytest.importorskip("hypothesis") and importorskip("hypothesis.strategies") now run before the helper import lines 9–10
The helper import, which loads policyengine_us.Simulation, comes after them line 12
The invariants module no longer imports pytest or hypothesis invariants module
The sibling comment now reads "(this module requires Hypothesis)", which is accurate because its importorskip is at line 313 test_capital_gain_distributions_loss_limit.py:165

Suggestion 4 details

Change Where
excepted = max_(0, tax_unit("md_capital_gains_surtax_excepted_gain", period)) md_capital_gains_surtax.py:46
New YAML witness: excepted −50,000 gives 2,000 YAML tests
The invariant now uses max(0, NCG - max(0, excepted)) invariants module
The property strategy now draws negative excepted amounts properties module

New findings

Critical (must fix)

None.

Should address

None.

Suggestions

1. Merge the per-page reference entries into one entry per document, following the house convention.

  • Where:
    • md_capital_gains_surtax.py:30-37: two booklet entries.
    • md_capital_gains_surtax_excepted_gain.py:31-42: three TB-58 entries.
    • surtax_applies.yaml:9-14: three ch. 604 entries.
  • The rule. CLAUDE.md ("Parameter and variable references", Document the reference page-citation convention #10009) says:
    • "One reference entry per source document; URLs that differ only by #page= are one source."
    • "Several cited pages: the href opens the first, and the title ends with #page 72-75 (consecutive) or #page 29,32-33,36,41 (nonconsecutive). Quote the title".
  • Our part. Our round-1 fix text asked for "one reference per page". That conflicts with this convention, which landed at about the same time, so the split is our doing. Sorry for the churn.
  • Fix:
    • Booklet: 2025 Maryland Resident Income Tax Instructions, lines 20a and 21b #page 21-22 with #page=21.
    • TB-58: Maryland Comptroller Technical Bulletin No. 58 (Dec. 29, 2025), sections II and III.A #page 1-3 with #page=1.
    • ch. 604 in YAML: '2025 Md. Laws ch. 604 (HB 352), §§ 3 and 34 #page 163-164,200', quoted, with #page=163.
  • Impact: citation format only. Every anchor is now correct.

2. Update the source-conflict backlog item this PR makes stale.

  • The item. source_conflicts/candidates.yaml on main lists us-md:capital-gains-surtax-short-term-gains, which is exactly this PR's choice. The question it asks: "Does Maryland's surtax exclude net short-term gains under the statutory IRC definition or include them through Form 502CG's federal-line transfer?"
  • What it says now:
    • pe_us.follows: conflicting-source
    • code: [.../md_capital_gains_surtax_applies.py]
    • a note that md_net_capital_gain is "not yet on main".
  • Why it goes stale. After this merges:
    • The deciding line is md_net_capital_gain.py:66-73, which follows the statute.
    • md_capital_gains_surtax_applies.py only tests federal AGI and makes no form choice.
  • What CONTRIBUTING.md asks. Its "Source conflicts" section (from Tie source-conflict markers to the register shared with Axiom #10287) asks a PR that takes such a reading to do three things:
    1. add the entry in rulespec-us, with an open PR listed under proposed;
    2. mark the deciding line with # Source conflict <jurisdiction>:<id>;
    3. refresh the pin and delete the backlog item.
  • Status. The PR body says the registration is pending and handed off.
  • Why it is not a blocker:
    • The ratchet passes.
    • The conflict is already in the backlog that exists for "every known conflict not yet registered", and that entry names this PR.
    • A marker cannot be added until the entry is pinned, because the guard rejects markers for unregistered ids.
    • No file on main carries a marker yet.
  • Fix in this PR: set the item's code to policyengine_us/variables/gov/states/md/tax/income/capital_gains/md_net_capital_gain.py and follows to statute, and update the note.
  • Then:
    1. Open the rulespec-us register entry, listing this PR under proposed.
    2. Once the entry is on rulespec-us main, refresh the pin.
    3. Add # Source conflict us-md:capital-gains-surtax-short-term-gains above the return at md_net_capital_gain.py:73.
    4. Delete the backlog item.

3. Refresh the PR body's Validation section after the next merge with main.

  • What is out of date. It says "Post-merge functional validation is pending" and "final-head CI remains pending". CI has now finished at 49558d1f4d, and its one failure is the main-caused ratchet listing described under CI status.
  • Fix: after merging main and re-running, replace the section with the final-head results.

Verified

Law (re-checked against the sources)

  • Base. Net capital gain "as defined and determined under the Internal Revenue Code" (§ 10-105(a)(3)(i)) is 26 U.S.C. § 1222(11), "the excess of the net long-term capital gain for the taxable year over the net short-term capital loss for such year". TB-58 section I gives the same definition (tb-58.pdf p. 1). max(0, min(L15, L16)) is that amount. The documentation and the code comment state the departure from Form 502CG line 1, which points to Form 502 line 1c and Form 1040 line 7a.
  • Threshold. The threshold is federal AGI "in excess of $350,000" (§ 10-105(a)(4)), "regardless of filing status" (TB-58 p. 1). Form 502CG says "Reported a federal adjusted gross income of more than $350,000 on Line 1 of Form 502" (502cg.pdf p. 2). The code tests adjusted_gross_income > surtax_threshold, with one value for every filing status. The new head-of-household witness pins that.
  • Exceptions:
    • Primary residence: the dwelling must be the primary residence, one of the listed dwelling types, and "sold for less than $1,500,000". See TB-58 p. 2 and Form 502CG's "Do not include net capital gain income attributable to a sale totaling $1,500,000 or more".
    • Retirement accounts: 401(k), 403(b), 457(b), IRA, Roth IRA, and "defined contribution plans, defined benefit plans, or similar retirement savings plans" (TB-58 p. 2; 502CG line 3).
    • The excepted-gain input carries these as one Form 502CG line 8 amount. Line 9 is "Subtract Line 8 from Line 1 ... If less than 0 enter 0" (502cg.pdf p. 1). With the new floor on line 8, line 9 can no longer exceed line 1.
  • Years:
    • Ch. 604 § 34: "applicable to all taxable years beginning after December 31, 2024" (CH_604_hb0352e.pdf p. 200).
    • The enrolled bill strikes "FOR TAXABLE YEARS 2025 THROUGH 2028" in (a)(4) (p. 164).
    • surtax_applies is false from 2013 and true from 2025 with no end date. The rate (0.02) and the threshold (350,000) have single 2025 values. This matches, and the 2026 and 2029 witnesses pin it.
  • Capital gain distributions. A capital gain dividend "shall be treated by the shareholders as a gain from the sale or exchange of a capital asset held for more than 1 year" (26 U.S.C. § 852(b)(3)(B)). non_sch_d_capital_gains is added to L15 as long-term gain, floored per filer. Schedule D line 13 amounts stay inside long_term_capital_gains. Nothing here changed since round 1.

Hand checks of every new or changed expectation

Case Calculation Expected
2026: ST 80,000 only, federal AGI 500,000 L15 = 0, L16 = 80,000, so NCG = max(0, min(0, 80,000)) = 0 and the surtax is 0 md_net_capital_gain 0, surtax 0 ✓
2025: LT 100,000, federal AGI 360,000, Maryland AGI 340,000 360,000 > 350,000, so it applies; NCG 100,000 × 0.02 applies true, NCG 100,000, surtax 2,000 ✓
2025: LT 100,000, federal AGI 500,000, excepted −50,000 Line 8 is floored to 0, so line 9 is 100,000 and 100,000 × 0.02 = 2,000. Without the floor it would be 3,000. NCG 100,000, surtax 2,000 ✓
2029: LT 100,000, federal AGI 500,000 surtax_applies keeps its 2025 value (true); 100,000 × 0.02 applies true, surtax 2,000 ✓
2025 head of household: wages 380,000, LT 30,000, child aged 10 Federal AGI = 380,000 + 30,000 = 410,000 > 350,000; NCG 30,000 × 0.02 = 600 HEAD_OF_HOUSEHOLD, AGI 410,000, applies true, NCG 30,000, surtax 600 ✓
Invariant 4 (changed) RATE * max(0, ncg - max(0, excepted)) when federal AGI > 350,000, otherwise 0. This equals the formula at md_capital_gains_surtax.py:46-49. ✓
Property strategy (changed) excepted is drawn from [0, 0, 10_000, draw(GAIN)] with GAIN in [−300,000, 300,000], so negative inputs exercise the floor. The grid's excepted values (0 and 20,000) are unchanged. ✓

The optional-Hypothesis change

  • Without Hypothesis. The properties module raises Skipped at line 9, before line 12 imports the invariants helpers (and with them policyengine_us.Simulation). So it skips at collection without loading the model. The invariants module imports neither pytest nor hypothesis, so test_grid and test_cases_where_the_form_1040_line_7a_gain_differs are collected and run. I checked this statically. The PR body also reports a local run that passed both tests with Hypothesis shadowed.
  • With Hypothesis, coverage is not weakened:
    • In CI (Rest shard-1 at this head), all three tests pass: test_grid in 24.69 s, test_properties in 13.16 s and the line-7a witnesses in 0.40 s.
    • Each run still uses 25 examples. The strategy is wider than before, because it now includes negative excepted gain.
    • derandomize=True makes runs reproducible, at the cost of fresh draws on each run, which is what round 1 asked for.
    • The number of Python tests is unchanged (3), and the module stays in the same CI group.
  • Imports. Cross-module imports of test helpers are an established pattern here (for example test_section_1h_regular_tax.py and test_form_4952_section_911_interaction.py). Only non-test helpers are imported, so no test is collected twice. Lint passes, and pyproject.toml ignores E402.

Other checks

Axiom line assessment

Valid queued. The PR body has a single axiom: TheAxiomFoundation/rulespec-us#1518 queued ... line.

rulespec-us#1518 meets every requirement:

  • State. It is OPEN and labelled pe-parity. It was last updated 2026-10-21 00:11 UTC, after the author commits.
  • Dispatch-ready. It has:
    • the module path us-md/statutes/gtg/10-105.yaml, a re-encode that removes the additional_capital_gain_tax deferral;
    • the corpus citation us-md/statute/gtg/10-105;
    • the verbatim text of (a)(3)–(4) and § 1222(11);
    • four required outputs;
    • a pasteable review_finding;
    • 11 companion tests whose expected values come from the statute, TB-58 and Form 502CG, not from PolicyEngine output.
  • Matches the head. The rate, the threshold, the line-9 floor, the TB-58 example (3,360), the threshold cases (350,000 and 350,001) and the excepted-gain-above-net-capital-gain case all agree with the head. The corrected Step 2 mapping avoids false mismatches in cases B, C and E. The issue's excepted inputs are per-category gain amounts, so the new floor on negative input is not at stake.
  • Blocker recorded. As CONTRIBUTING requires, it says: "Signed encoder only: axiom-encode targeted-signed-reencode.yml (production-signing environment, OpenAI backend), which needs Max's approval. Hand-written RuleSpec is rejected by the generated-guard." This matches the PR body's "blocked only because the OpenAI-billed signed re-encode needs approval".

rulespec-us#1490 (the § 852(b)(3)(B) dependency) is OPEN and labelled pe-parity.

CI status

25 of 27 checks pass; 2 fail (run 39531581561 at 49558d1f4d). Both failures trace to one main-caused test failure, so this is a branch note, not a finding.

  • Full Suite - Rest (Python, shard-1 + variables). 1 failed, 2,499 passed, 11 skipped.
    • The sole failure is test_source_conflicts.py::test_conflict_sounding_comments_do_not_grow.
    • The full grown dict from the job log has five entries:
      • variables/gov/states/fl/commerce/liheap/eligibility/fl_liheap_eligible.py
      • variables/gov/states/fl/commerce/liheap/eligibility/fl_liheap_income_limit.py
      • variables/gov/states/fl/commerce/liheap/fl_liheap.py
      • parameters/gov/states/fl/commerce/liheap/payment/base_amount.yaml
      • parameters/gov/states/sd/dss/liheap/income/limits/table/in_effect.yaml
    • Every entry is a Florida or South Dakota LIHEAP file. No Maryland or other PR file is listed.
    • The Maryland Python tests in the same job passed.
  • Full Suite - Rest (Python + variables). This is the aggregator. All of its own test groups passed (60, 55, 1,767 and 980 passed). It fails only with "Rest shards did not all succeed: shard-1=failure, shard-2=success".
  • Timing. The run was created at 2026-10-21 02:11 UTC, and the shard-1 job ran from 03:19 to 04:09 UTC. Both are before the 13:24 UTC fix on main (Count the FL and SD LIHEAP comments in the source-conflict ratchet baseline #10334, which adds those five files to KNOWN_UNMARKED).
  • Merged tree (checked statically).
    • The four variable files have no comment matching the ratchet's CONFLICT_COMMENT_RE.
    • The three parameter files have no YAML comments.
    • None of the PR's files is in KNOWN_UNMARKED.
    • main's only change to the guard since this head adds the five Florida and South Dakota entries.
    • So the ratchet should pass once main is merged.
  • Everything else passes:
    • all Baseline shards, including the Maryland YAML (39 capital-gains cases);
    • Contrib;
    • Microsimulation;
    • Household API Partners;
    • PR validation (lint, changelog);
    • Package and compatibility;
    • Quick Feedback.
  • Partner tests. The PR touches no partner test files.
  • Pending. No checks are pending.

Branch status

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: all prior findings are fixed. The reference anchors are corrected, and the Hypothesis split means the grid runs without it. The law re-checks against TB-58, Form 502CG and ch. 604 s. 34, and the new witnesses hand-check. The two red checks are only the known FL/SD LIHEAP ratchet failure (no MD file listed; the run predates #10334), so merge main and re-run. Three suggestions, including merging the per-page reference split I asked for in round 1 back into one entry per document, per CLAUDE.md. 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

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

…idate

The candidate still described the form-based reading the PR replaced. It now
records the 1222(11) base the code follows, points at md_net_capital_gain,
and the deciding line carries a conflict comment counted in KNOWN_UNMARKED.
References that differed only by page are consolidated into one entry per
document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LwQq6FjKTA223kkKGWSie6
Main carries the source-conflict ratchet counts for the Florida and South
Dakota LIHEAP files whose absence failed this branch's final-head CI.

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

# Conflicts:
#	source_conflicts/candidates.yaml
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 3427c3acd0c02adfa2ef641af51c854177121937

What the statute review asked, and how it is resolved

P2: complete the source-conflict registration (as far as this repository can; the register entry itself is a rulespec-us change).

  • policyengine_us/variables/gov/states/md/tax/income/capital_gains/md_net_capital_gain.py:70-75: the deciding line (return max_(0, min_(long_term, combined))) now has a conflict comment naming both sources and the candidate id us-md:capital-gains-surtax-short-term-gains. It can't be a # Source conflict <id> marker yet, because the guard only accepts ids that are in the vendored register.
  • policyengine_us/tests/code_health/test_source_conflicts.py: that file's count (1) is added to KNOWN_UNMARKED in sorted order.
  • source_conflicts/candidates.yaml (us-md:capital-gains-surtax-short-term-gains): rewritten to the statute reading the PR follows: pe_us.follows: statute, code path md_net_capital_gain.py (was md_capital_gains_surtax_applies.py), TB-58 cited with its URL (was url: null), the form cited as the Form 502CG instructions, readings and example corrected to $2,000 under the statute against $3,000 under the form for $100,000 long-term plus $50,000 short-term gain, and $0 against $1,600 for $80,000 of short-term gain alone. State is ruled (statute-first rule of 2026-10-19).

P3: CI claims. The body's Validation section now reports the final-head result: run 39531581561 at 49558d1f4d completed with 25 jobs passed and 2 failed. Both failures were test_conflict_sounding_comments_do_not_grow on four Florida LIHEAP files and one South Dakota LIHEAP file, none in this PR. The stale "registration remains pending" sentence is reworded.

P3: duplicate PDF references. One entry per document:

  • md_capital_gains_surtax.py:30-33: resident booklet lines 20a and 21b, #page=21, title ending #page 21-22.
  • I applied the same rule to the PR's two other duplicates: md_capital_gains_surtax_excepted_gain.py (TB-58, pages 1-3, three entries → one) and parameters/gov/states/md/tax/income/capital_gains/surtax_applies.yaml (ch. 604, pages 163-164 and 200, three entries → one).

Main merge (judgment call)

The item said to push as a fast-forward. The previous head's CI failed only on the source-conflict ratchet for the Florida and South Dakota LIHEAP files, and the same test failed locally on the branch. Main already carries those files' counts, so I merged main (3427c3acd0) to clear the failure instead of pushing a head that would fail the same way. candidates.yaml conflicted: I kept this PR's rewritten Maryland entry and main's new us-md:ccs-copay-flat-schedule entry after it.

Tests (local, one file per process, at 3427c3acd0)

File Result
md/tax/income/capital_gains/md_capital_gains_surtax.yaml 27 passed
md/tax/income/capital_gains/md_capital_gains_surtax_applies.yaml 5 passed
md/tax/income/capital_gains/md_net_capital_gain.yaml 7 passed
tests/test_md_capital_gains_surtax_invariants.py 2 passed
tests/test_md_capital_gains_surtax_properties.py 1 passed (25 generated examples)
tests/code_health/test_source_conflicts.py 103 passed (102 passed, 1 failed before the merge)

No tax result changed this round: only comments, references, the backlog entry and the merge.

CI: the Pull request run 41733019138 at 3427c3acd0 completed with 36 of 36 jobs passing.

Not done

  • No statute or form text is quoted in the candidate or code: this session's network couldn't reach the Maryland or Comptroller sites. The candidate's readings paraphrase the PR's existing description and the review; unverified here.
  • The register entry, the # Source conflict marker and the pin refresh wait for a rulespec-us change once this is on main.

Generated by Claude Code

@MaxGhenis

MaxGhenis commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor Author

Hub review in progress

Head under review: 3427c3acd0c02adfa2ef641af51c854177121937 (unchanged since the hand-off).

So far:

  • CI run 41733019138 at this head: 36 of 36 jobs success.
  • The three Maryland capital-gains YAML files pass locally, one per process: 27 + 5 + 7. test_source_conflicts.py passes 103; the invariants file 2; the properties file 1; test_capital_gain_distributions_loss_limit.py 9.
  • The merge 3427c3acd0 holds the PR's Maryland candidate exactly as on 22ee7937c7 and main's us-md:ccs-copay-flat-schedule exactly as on main's parent 6145ad4513. No other candidate differs from main. The head also merges cleanly with current main.
  • The Maryland legislature and Comptroller sites and govinfo are blocked from this session, so I couldn't check the statute, TB-58 or the Form 502CG text myself.

Complete: see the final review comment below.


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: REQUEST_CHANGES
3427c3a
METHODOLOGY_CHOICE: yes

The head hasn't moved since the hand-off. This round's code and register changes are sound. The surtax results, the merge, CI and every mutant I ran check out. What blocks is the PR body: it has no impact: line, and one paragraph still describes the reference layout this round removed. METHODOLOGY_CHOICE is yes because the PR follows § 10-105(a)(3)'s IRC § 1222(11) base over the Form 502CG instructions' Form 502 line 1c figure, which is the registered conflict us-md:capital-gains-surtax-short-term-gains. I couldn't fetch any Maryland source or govinfo from this session, so the statute, TB-58 and form readings are still unconfirmed by a source fetch (see "What I couldn't check").

Findings

P2: the body has no impact: line, and this diff moves baseline outputs

  • Where: the PR body. Only the axiom: line exists.

  • Failure scenario: the hub can't tell from the body which baseline outputs move or what population check is needed. The diff moves md_capital_gains_surtax, and so md_income_tax_before_credits and md_income_tax, for Maryland filers from 2025.

  • Witness: I ran the same scratch YAML on main (origin/main worktree) and on this head. Expected values are my hand computations from § 1222(11) as the PR states it: L15 = long-term gain plus distributions; L16 = L15 plus short-term gain; base = max(0, min(L15, L16)); tax = 2% of the base.

    Case (2025, single, MD) Hand value main head
    W1: wages 400,000, LT 100,000, ST 50,000 100,000 × 0.02 = 2,000 3,000 2,000
    W2: wages 450,000, ST 80,000 only min(0, 80,000) → 0 1,600 0
    W3: head wages 400,000 and LT 10,000; dependent LT 40,000 10,000 × 0.02 = 200 1,000 200
    W4: MFJ, head LT 60,000, spouse ST −20,000, wages 500,000 min(60,000, 40,000) × 0.02 = 800 800 800
    W5: wages 400,000, non-Schedule D distributions 30,000, ST −5,000 min(30,000, 25,000) × 0.02 = 500 0 500
    W6: 2024 · W7: Virginia 0 · 0 0 · 0 0 · 0

    policyengine-core test witness.yaml -c policyengine_us: head 7 passed; main 4 failed, 3 passed (W1 [3000.] differs from 2000.0, W2 [1600.], W3 [1000.], W5 [0.]). The body's YAML also pins federal AGI rather than Maryland AGI for the threshold.

  • Replacement: add this line next to the axiom: line:
    impact: baseline changes for Maryland filers with federal AGI over $350,000 from 2025: a net short-term gain no longer enlarges the surtax base, dependents' gains no longer count, capital gain distributions reported without Schedule D now count, the threshold tests federal rather than Maryland AGI, and excepted gain is a new input (default 0). Needs a hub microsimulation of md_capital_gains_surtax and md_income_tax for 2025 and 2026, before and after.

P3: the body's "Response to @DTrim99" S1 bullet describes references that no longer exist

  • Where: PR body, "Response to @DTrim99", bullet S1: "split booklet references into line 20a at page 21 and line 21b at page 22; separate TB-58 exception and example references by page."
  • Failure scenario: commit 22ee7937c7 merged those entries back into one per document, as DTrim99's round-2 suggestion 1 asked. The Validation section's References bullet already says so, so the body now contradicts itself.
  • Check: git show 22ee7937c7 -- '*md_capital_gains_surtax.py' '*excepted_gain.py' '*surtax_applies.yaml' removes the per-page entries: 1 booklet, 3 TB-58 and 3 ch. 604 entries become 1 each.
  • Replacement: "S1 — reference anchors: the booklet, TB-58 and ch. 604 references each now open at the first cited page, with their pages in the title (#page 21-22, #page 1-3, #page 163-164,200). Round 1 split them by page; round 2 merged them back to one entry per document, per DTrim99's round-2 suggestion 1. The PR body's anchors are corrected too."

P3: the candidate paraphrases its sources, and no capture backs TB-58 or the 502CG instructions

  • Where: source_conflicts/candidates.yaml, item us-md:capital-gains-surtax-short-term-gains (readings, sources).
  • What the build asked for and didn't get: "Cite both sources with quotes of 25 words or fewer." The readings are paraphrases, which the hand-off admits ("No statute or form text is quoted"). CONTRIBUTING.md step 1 also asks the ruling PR to add a capture under source_conflicts/sources/ when the Axiom corpus lacks a source's text. source_conflicts/sources/us-md/ holds only resident-booklet-2025-p23.txt. I can't see whether rulespec-us already has TB-58 or Form 502CG in its corpus.
  • Failure scenario: step 2 in rulespec-us can't pin a capture of TB-58 or the 502CG instructions if the corpus lacks them. That would need another PolicyEngine US PR first.
  • A smaller point in the same item: the third source's citation names both the 502CG instructions and resident booklet p. 13, but its url points only to 502cg.pdf#page=2. The booklet page, which carries the Form 1040 line 7a link, has no URL.
  • Fix: when a session can reach marylandcomptroller.gov, put short quotes in the readings, for example the 502CG instruction "Residents use Line 1c of Form 502" and the booklet's line 1c text. Split the booklet into its own kind: instructions source at resident-booklet.pdf#page=13. If the corpus lacks them, add captures of TB-58 p. 1 and 502CG p. 2. This can also land as a follow-up before step 2; it changes no result.

The statute review's items (round 1 at 49558d1f4d)

  • P2, register item: answered as far as one PR can. Checked against candidates.yaml's header and test_source_conflicts.py:
    • follows: statute and state: ruled are valid values. "ruled" means "PolicyEngine has picked", and the code does follow the statute.
    • code lists only md_net_capital_gain.py, a tracked file that holds the deciding line.
    • There are three sources with valid kinds (statute, guidance, instructions) and https URLs.
    • prs lists Tax Maryland's capital gains surtax on net capital gain as the Internal Revenue Code defines it #9898.
    • The examples recompute by hand. $100,000 LT and $50,000 ST: statute min(100,000, 150,000) × 0.02 = 2,000; form line 7a 150,000 × 0.02 = 3,000. $80,000 ST alone: statute min(0, 80,000) = 0, so 0; form 80,000 × 0.02 = 1,600. The model at main gives exactly the form values (W1, W2 above), and the head gives the statute values.
    • The note's claim that md_capital_gains_surtax.yaml pins both witnesses holds: "A net short-term capital gain is not net capital gain" gives 2,000, and "Short-term gains only" and the 2026 statute-first witness give 0.
    • test_source_conflicts.py: 103 passed.
  • The deciding line: md_net_capital_gain.py:70-75 sits directly above return max_(0, min_(long_term, combined)). The comment says the 502CG instructions use Form 502 line 1c, the Form 1040 line 7a gain, which also counts a net short-term gain, while § 10-105(a)(3) and TB-58 use the § 1222(11) amount, and it names the candidate. That matches the variable's documentation, the candidate, and the page text DTrim99 quoted in the round-1 review ("Residents use Line 1c of Form 502"; line 1c is "the amount of net capital gain or loss reported as taxable income on Line 7a of the federal Form 1040"). I couldn't fetch those pages myself.
    • KNOWN_UNMARKED gains md_net_capital_gain.py: 1, in sorted order, and the guard passes. No # Source conflict marker is added, which is correct: CONTRIBUTING.md says the marker waits for the pinned register entry.
  • P3, CI claims: answered. The body now gives run 39531581561 at 49558d1f4d (25 passed, 2 failed on the Florida/South Dakota LIHEAP ratchet) and run 41733019138 at the head.
  • P3, duplicate references: answered, and extended to md_capital_gains_surtax_excepted_gain.py and surtax_applies.yaml. Each changed file now has one entry per document, with an href to the first page and a title ending in a page list (quoted in the YAML).

DTrim99's review

The brief says DTrim99's CHANGES_REQUESTED still stands. It doesn't: DTrim99 APPROVED at 49558d1f4d on 2026-10-21, after round 2 found all 11 round-1 items fixed (S1, S2 and suggestions 1–9). That approval is on an earlier head. Round 2 left three suggestions:

  1. One entry per document: answered in 22ee7937c7.
  2. Update the backlog item: answered. code is now md_net_capital_gain.py, follows is statute, and the note is updated. The rulespec-us entry, marker and pin refresh are post-merge steps.
  3. Refresh the Validation section: answered. The S1 bullet in the response section is stale (P3 above).

Checked and sound

  • Head and merge:

    • The PR head is 3427c3acd0, unchanged.
    • git diff 6145ad4513 3427c3acd0 --stat (main parent → head) shows only the PR's 16 files.
    • Parsing candidates.yaml at each commit: the Maryland item at the head equals 22ee7937c7's; us-md:ccs-copay-flat-schedule equals main's side (not in the merge base); no other id differs from main's side; ids are sorted (357).
    • git merge-tree --write-tree origin/main HEAD is clean against current main, which is 6 commits past the merged parent.
  • CI: run 41733019138 ("Pull request", head 3427c3acd0) completed success; jobs API: 36 success, none cancelled or skipped.

  • Local tests, one file per process:

    • md_capital_gains_surtax.yaml 27 passed; md_capital_gains_surtax_applies.yaml 5; md_net_capital_gain.yaml 7;
    • test_md_capital_gains_surtax_invariants.py 2; test_md_capital_gains_surtax_properties.py 1 (25 examples); test_capital_gain_distributions_loss_limit.py 9; test_source_conflicts.py 103.
  • Mutants (the hand-off lists none; all five are mine). Each ran in its own process, the code was restored after each, and git status was clean afterwards.

    Mutant Killed by
    M1: return max_(0, combined) (the form reading) md_net_capital_gain.yaml "Spouses' short-term amounts net to a gain, which adds nothing"; md_capital_gains_surtax.yaml 7 failures, including "A net short-term capital gain is not net capital gain" and "Short-term gains only"
    M2: drop the max_(0, …) on excepted gain "A negative excepted gain cannot increase the surtax"
    M3: >= threshold "Federal AGI equal to the threshold"; "Federal AGI at the threshold is not over it"
    M4: count dependents' distributions (filer mask always true) md_net_capital_gain.yaml "A dependent's capital gain distributions are on the dependent's own return" (survives md_capital_gains_surtax.yaml)
    M5: threshold on md_agi md_capital_gains_surtax_applies.yaml "Maryland subtractions do not take federal AGI under the threshold" and "Maryland additions do not put federal AGI over the threshold"; md_capital_gains_surtax.yaml "Statute-first witness - federal AGI controls…"
  • Readers (grep of non-test code):

    • md_capital_gains_surtax is read only by md_income_tax_before_credits.py:38, behind surtax_applies.
    • md_capital_gains_surtax_applies is only the surtax's defined_for.
    • md_net_capital_gain and md_capital_gains_surtax_excepted_gain feed only the surtax.
    • No other reader exists, so no reader is newly reachable and worse.
  • Rule across entities and years:

    • Head and spouse only: the YAML pins tax_unit_non_dep_add and the per-filer distribution mask, and M4 is killed. MFJ netting is checked by W4.
    • One threshold for every filing status: HOH is in YAML.
    • 2024 gives 0, and 2025, 2026 and 2029 apply (the parameter has no end date).
    • Non-Maryland households give 0 (W7).
  • Scope: no file under .github/ or policyengine_us/tests/policy/baseline/partners/.

  • Overlap: open PR Leave dependents' capital income out of state lines on the filers' return #9894 touches md_capital_gains_surtax.py and its YAML, as DTrim99 noted; whichever lands second should keep this PR's Maryland files. Search hits for candidates.yaml (Follow state statutes where forms differ: NM medical exemption, VT CDCC, NY/NYC household credit, PR EITC #10300, Follow the statute for the NYC school tax rate reduction, IN supplemental homestead, VA 529 and WA senior/disabled property tax exemption #10330, Add Colorado LEAP regular heating assistance #10609 and others) touch other ids.

  • TB-58 arithmetic: (610,000 − 442,000) × 0.02 = 3,360, matching the YAML. The inputs come from the PR; I couldn't fetch TB-58.

What I couldn't check

  • Sources: mgaleg.maryland.gov, www.marylandcomptroller.gov and govinfo.gov all returned a proxy policy denial (CONNECT 403 / DNS refusal) from this session. So I confirmed none of the following against its text:

    • § 10-105(a)(3)–(4);
    • ch. 604 pp. 163–164 and 200;
    • TB-58 pp. 1–3;
    • Form 502CG pp. 1–2;
    • the resident booklet pp. 13 and 21–22;
    • 26 U.S.C. §§ 1222(11) and 852(b)(3)(B).

    This includes whether each consolidated reference's page list holds what its title cites. The page assignments agree with DTrim99's quoted page text, but that is their reading, not a fetch of mine.

  • Population impact: I ran no microsimulation. That is the hub's check under the impact: line above.

  • rulespec-us: out of this session's GitHub scope. I didn't check whether its corpus holds TB-58 or Form 502CG text, or the current state of rulespec-us#1518.


Generated by Claude Code

@MaxGhenis
MaxGhenis merged commit e7bcd59 into main Nov 8, 2026
36 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#9898 at 3427c3acd0c02adfa2ef641af51c854177121937 (squash): Tax Maryland's capital gains surtax on net capital gain as the Internal Revenue Code defines it.

  • Reviews:
    • DTrim99 requested changes at 963ae33e10 and approved 49558d1f4d on October 21, after their second round found all eleven first-round items fixed. 22ee7937c7 answers that round's two code suggestions: one reference per document, and the conflict record's new reading.
    • An independent statute review of 49558d1f4d asked to record the Maryland conflict and to fix the body's CI line and duplicate references. 22ee7937c7 and the body answer it.
    • The independent cloud review of 3427c3acd0 checked the code, the conflict record, the merge and five mutants, and asked only for an impact: line, which the body now carries. Its two smaller notes concerned the body's reply section, which the body no longer has, and short quotes and source captures for the conflict record, which follow separately.
  • Sources: each reading in the body matches its official source. The check covered § 10-105(a)(3)-(4), chapter 604's page 164 and section 34, Technical Bulletin 58 (pages 1 and 3), the Form 502CG and resident booklet instructions, 2025 Schedule D, and 26 U.S.C. §§ 1222(11) and 852(b)(3)(B).
  • Main merge and the overlap guard: the head merges main at 6145ad4513; main has since taken 85 commits. The two sides share only test_source_conflicts.py and candidates.yaml, and they merge without conflict. On a scratch merge with main ba14b81f7b, one process each:
  • Worked cases on main: the PR's surtax YAML cases, run on main ba14b81f7b with only the outputs main defines and without the excepted-gain input, give the body's "Before" column.
  • Gates checked live at merge:
    • gh pr checks exits 0;
    • the latest Pull request run at the head, 41733019138, passed all 36 jobs;
    • MERGEABLE;
    • not a draft;
    • no reviewer's latest review requests changes;
    • the semantic-overlap guard, with the combination verified at main ba14b81f7b;
    • the merge pins the head.
  • Impact (default dataset, the head against main at 6145ad4513): we estimate that Maryland's capital gains surtax falls by $30.0 million in 2025 and $31.2 million in 2026, and Maryland income tax falls by the same amounts. Federal income tax and benefits don't change. Leaving out net short-term gain accounts for −$30.1 million and −$31.3 million, distributions without Schedule D add $134,000 and $139,000, and the federal AGI test adds $13 and $31. A step-by-step rebuild at the head matches main's surtax total within $2 in each year. The body's impact section has the table.
  • Follow-ups:
  • Axiom: TheAxiomFoundation/rulespec-us#1518 queued.

🤖 Generated with Claude Code

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