Skip to content

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

Open
MaxGhenis wants to merge 6 commits into
mainfrom
md-capital-gains-surtax-net-capital-gain
Open

MaxGhenis wants to merge 6 commits into
mainfrom
md-capital-gains-surtax-net-capital-gain

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Change

Maryland's capital gains surtax now uses IRC § 1222(11) net capital gain and the strict federal AGI threshold in Md. Code, Tax-General § 10-105(a)(3)–(4). A positive net short-term gain does not enlarge the surtax base; a net short-term loss reduces it. Capital gain distributions reported without Schedule D are included, qualified dividends are excluded, and only the head and spouse contribute capital income.

  • md_net_capital_gain computes max(0, min(L15, L16)), where L15 is the filers' net long-term capital gain including distributions reported without Schedule D, and L16 adds their net short-term gain or loss. Distributions are floored per person; distributions and carryovers already inside Schedule D net long-term gain are not counted twice.
  • md_capital_gains_surtax_applies tests federal AGI strictly above the parameterized threshold ($350,000 under current law), regardless of filing status. Maryland additions or subtractions do not move a filer across that threshold.
  • md_capital_gains_surtax_excepted_gain is a tax-unit input for the total excepted gain on Form 502CG line 8. Negative supplied amounts are treated as zero.
  • The surtax is the parameterized rate (currently 2%) times max(0, md_net_capital_gain - max(0, excepted_gain)). It remains part of state income tax before credits. The county tax base is unchanged.

For example, long-term gain of $100,000 plus short-term gain of $50,000 yields a $2,000 surtax for an eligible filer; short-term gain alone yields none. Technical Bulletin 58's example gives (610,000 - 442,000) × 0.02 = 3,360.

Statute and form interpretation

The controlling base is Md. Code, Tax-General § 10-105(a)(3), which incorporates the Internal Revenue Code's definition of net capital gain. 26 U.S.C. § 1222(11) and Technical Bulletin 58, section I define that amount as net long-term capital gain less net short-term capital loss. Capital gain distributions receive long-term treatment under 26 U.S.C. § 852(b)(3)(B). Qualified dividends are added only for the separate federal § 1(h) calculation.

Form 502CG's instructions instead direct residents to Form 502 line 1c, which uses federal Form 1040 line 7a (resident booklet, page 13). That figure also includes positive net short-term gain. This implementation follows the statute and TB-58; the choice is explicit in md_net_capital_gain documentation and the witness tests.

Further references:

Validation

Final head 49558d1f4d93accf947a0df3b7d80bca16c47cdf merges upstream/main 7545b2c92ffb30d18615ff64d5e573627777733a in a real merge commit and was pushed fast-forward. This final merge includes release metadata and #10003's federal mortgage-deduction changes. Maryland's income-tax variables, parameters, YAML tests and both Maryland Python test files are byte-identical to tested head 6e3b0d0605ee867b3bd02172e331d49b07c1cb81. The complete policy tree differs, however, and mortgage deductions can affect Maryland itemized deductions indirectly. Post-merge functional validation is pending; the results below belong to the pre-merge tested head.

  • CI Maryland YAML: run 39518028733, states shard 2, passed all 50 income-tax files / 266 cases: 3 capital-gains files / 39 cases and 47 other income-tax files / 227 cases. Its complete Maryland batch passed 640 cases. This was batch execution, distinct from the prescribed local file-by-file run.
  • CI Quick Feedback: the same run passed the three capital-gains YAML files in separate sequential foreground CLI processes (27 + 5 + 7 = 39 cases, clean exits), plus 3 Maryland Python tests: two deterministic invariants and one optional Hypothesis property. Measured Python call times were 21.79 seconds, 0.22 seconds, and 11.53 seconds, totaling 33.54 seconds, excluding shared imports, model startup, setup and teardown.
  • Earlier CI guard failure: run 39507064256 reported 2,499 passed, 11 skipped, 1 failed in Python Rest's remaining group. Its sole failure, test_source_conflicts.py::test_conflict_sounding_comments_do_not_grow, names four Florida LIHEAP paths and one South Dakota LIHEAP path. Those five paths and the guard are unchanged in final head; no Maryland path was named. The newer pre-merge Python Rest job had not completed at the final check, and final-head CI remains pending.
  • Local YAML: on tested head 6e3b0d0, one file per foreground CLI process with all required thread caps set to four: 7 files / 61 cases passed, including all 39 capital-gains cases and 22 other income-tax cases. 5 CLIs exited cleanly. The surtax and applicability files reported all 27 + 5 cases passed, then required cleanup interruption and reaping (actual child exits −2). The controller reached its cutoff and reaped a subsequent incomplete file, exiting 124. 43 files / 205 cases remain incomplete locally. No assertion failure was reported in the completed files.
  • Local Python: both deterministic tests passed without Hypothesis, with plugin autoload disabled and a shadow module raising ModuleNotFoundError: 2 passed in 606.73 seconds, clean exit 0; total process elapsed time 1,673.00 seconds. The normal Hypothesis test was verified by CI rather than a completed local run.
  • Other checks: after the final merge, make format left 7,148 files unchanged and Ruff lint passed. The authored diff's whitespace check against merged main and the unstaged whitespace check passed. No local microsimulation was run.

The Python grid and generated properties check netting identities, dependent invariance, monotonicity, the federal differential, and the surtax's single insertion into state tax; the YAML files hold policy witnesses. This revision retains the existing two deterministic checks and moves the existing generated property into its own optional-dependency module, with 25 Hypothesis examples unchanged. The split does not increase the Python test count or add CI runners. Generation is derandomized with the slow-generation and data-size health checks suppressed.

Coverage includes the statute-first witnesses (positive short-term gain, short-term gain only, long-term loss with short-term gain, federal versus Maryland AGI, and TB-58's example), negative and excessive excepted gain, a head-of-household filer, and a 2029 case.

Response to @DTrim99

  • S1 — reference anchors: split booklet references into line 20a at page 21 and line 21b at page 22; separate TB-58 exception and example references by page. Corrected the PR body's anchors too.
  • S2 — optional Hypothesis: moved the Hypothesis property test and strategies to their own module, so the deterministic grid and federal-line-7a witnesses remain collectable without that dependency. Corrected the sibling's misleading comment.
  • Suggestion 1 — effective date: added ch. 604 § 34, page 200, to surtax_applies references.
  • Suggestion 2 — RuleSpec oracle: the correct mapping for output 2 is md_capital_gains_surtax_applies & (md_net_capital_gain > 0). The PE applicability variable intentionally tests the federal AGI threshold alone. Corrected Step 2 in the existing rulespec-us#1518 issue body to use this conjunction; all other issue-body text was preserved.
  • Suggestion 3 — property settings: enabled derandomize=True and suppressed HealthCheck.too_slow and HealthCheck.data_too_large.
  • Suggestion 4 — negative excepted gain: floor the supplied excepted amount at zero before subtraction, with a regression witness.
  • Suggestion 5 — documentation constants: use rate and threshold terminology in the surtax variable documentation rather than hard-coded parameter values.
  • Suggestion 6 — statute/form choice: explain the § 1222(11) interpretation and Form 502 line 1c discrepancy in md_net_capital_gain.documentation.
  • Suggestion 7 — rate description: identify net capital gain included in Maryland AGI and the federal AGI threshold explicitly.
  • Suggestion 8 — witnesses: added a post-2028 case and a head-of-household case.
  • Suggestion 9 — PR housekeeping: removed the unfinished microsimulation section and historical run claims; preserved the axiom declaration as a single line.

Limits and parity follow-up

Excepted-asset categories are supplied as one aggregate input; their sale-price, holding-period and other qualifications are not inferred from household data. Maryland subtractions that specifically remove capital gain from Maryland AGI (including the listed U.S.-obligation, MDOT-acquisition and race-course provisions), nonresident allocation, and fiduciary returns remain outside this change.

In rulespec-us#1518, Step 2 now maps additional_capital_gain_tax_applies to md_capital_gains_surtax_applies & (md_net_capital_gain > 0) and the tax output directly to md_capital_gains_surtax. This avoids false mismatches for the zero-net-capital-gain cases B, C and E.

The source-conflict registration required by current contributor guidance needs a separate rulespec-us PR. This assignment permits one PR, so a concrete registration handoff has been prepared for the hub. Registration remains pending: the existing candidate is unreviewed and is not registration, and this revision changes neither source-conflict markers nor the vendored pin.

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 but defers additional_capital_gain_tax. The issue is dispatch-ready: verbatim law, required outputs, a pasteable review_finding, and 11 companion tests from the statute, TB-58 and Form 502CG. It is blocked only because the OpenAI-billed signed re-encode needs approval. The base it imports, us:statutes/26/1222#net_capital_gain (§ 1222(11)), is already encoded; capital gain distributions as long-term gain (852(b)(3)(B)) remain TheAxiomFoundation/rulespec-us#1490, queued.

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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants