Skip to content

Add Georgia SB 520 contributed reform#7690

Open
DTrim99 wants to merge 8 commits intoPolicyEngine:mainfrom
DTrim99:ga-sb520-tax-reform
Open

Add Georgia SB 520 contributed reform#7690
DTrim99 wants to merge 8 commits intoPolicyEngine:mainfrom
DTrim99:ga-sb520-tax-reform

Conversation

@DTrim99
Copy link
Collaborator

@DTrim99 DTrim99 commented Mar 3, 2026

Summary

  • Implements Georgia SB 520 as a contributed reform with two phases:
    • 2026 provisions: Increased standard deduction ($34k joint/$17k other) with 25% phase-out above AGI thresholds, enhanced CTC ($1,250, fully refundable), new Georgia EITC (20% of federal, refundable)
    • 2027 provisions: Progressive income tax brackets (2%/4%/6%) replacing the flat rate
  • Separate in_effect switches for 2026 and 2027 provisions
  • Filing status-specific brackets for all 5 statuses

Test plan

  • Integration tests verify standard deduction amounts
  • Integration tests verify CTC calculations
  • Integration tests verify standard deduction phase-out
  • Integration tests verify EITC match calculation
  • Integration tests verify 2027 progressive bracket calculations

🤖 Generated with Claude Code

Implements progressive income tax brackets (2027), increased standard
deduction with phase-out, enhanced child tax credit ($1,250 refundable),
and new Georgia EITC (20% of federal).

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 3, 2026

Implementation Notes

This PR implements Georgia SB 520 with the following structure:

Parameters (gov/contrib/states/ga/sb520/)

  • in_effect_2026.yaml - Switch for 2026 provisions
  • in_effect_2027.yaml - Switch for 2027 provisions
  • deductions/standard/amount.yaml - Increased standard deduction amounts by filing status
  • deductions/standard/phase_out/threshold.yaml - AGI phase-out thresholds by filing status
  • deductions/standard/phase_out/rate.yaml - 25% phase-out rate
  • credits/ctc/amount.yaml - $1,250 per child
  • credits/eitc/match.yaml - 20% of federal EITC
  • tax/{single,joint,separate,head_of_household,surviving_spouse}.yaml - Progressive brackets (2%/4%/6%)

Reform Variables

  • ga_standard_deduction - Overrides with phase-out calculation
  • ga_ctc - Enhanced CTC amount
  • ga_eitc - New state EITC (20% match)
  • ga_refundable_credits - Makes CTC and EITC fully refundable
  • ga_non_refundable_credits - Removes CTC from non-refundable
  • ga_income_tax_before_non_refundable_credits - 2027 progressive brackets

Known Issue

There may be parameter access issues to debug - the in_effect_2026 and in_effect_2027 parameters may need path adjustments.

Rename in_effect_2026.yaml to active2026.yaml to fix parameter
access issues with underscore-separated filenames.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 3, 2026

Fix Applied

Renamed parameter files to fix the parameter access issue:

  • in_effect_2026.yamlactive2026.yaml
  • in_effect_2027.yamlactive2027.yaml

Updated all references in:

  • ga_sb520_reform.py - Changed p_sb520.in_effect_2026 to p_sb520.active2026
  • integration.yaml - Changed parameter paths accordingly

The underscore in in_effect_2026 was causing PolicyEngine to interpret it as a path separator rather than a single parameter name. Using active2026 (no underscore before the year) should resolve the CI failures.

@DTrim99 DTrim99 marked this pull request as draft March 3, 2026 16:17
@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 3, 2026

PR Review

🔴 Critical (Must Fix)

  1. CI Failure - Test Case 2: ga_ctc returns 1250 but expected 2500 for 2 children

    • File: integration.yaml:46
    • Issue: The CTC formula may have a bug - it should multiply $1,250 × 2 children = $2,500
    • Check: ga_sb520_reform.py:77 - verify eligible_children * amount_per_child is working correctly
  2. CI Failure - Test Case 4: Invalid test syntax ga_eitc__gte: 0

    • File: integration.yaml:95
    • Issue: __gte suffix is not valid for output assertions in PolicyEngine
    • Fix: Replace with ga_eitc: X where X is the calculated expected value, or use a simple ga_eitc: 0 assertion
  3. CI Failure - Test Case 5: Invalid test syntax ga_income_tax_before_non_refundable_credits__gte: 0

    • File: integration.yaml:119
    • Issue: Same issue - __gte suffix is not valid
    • Fix: Replace with actual expected value (calculation shows ~$1,080)

🟡 Should Address

  1. Period usage: In ga_ctc formula (line 64), age = person("age", period) should use period.this_year since age is a YEAR variable accessed from a YEAR formula

    • File: ga_sb520_reform.py:64
  2. Filing status in test cases 2 & 5: Tests don't explicitly set filing_status - this relies on defaults

    • File: integration.yaml:21-46, 97-119
    • Suggestion: Add explicit filing_status: JOINT for Case 2 and filing_status: SINGLE for Case 5
  3. Parameter date value: active2026.yaml and active2027.yaml use 0000-01-01 for the default false value

    • Files: active2026.yaml:4, active2027.yaml:4
    • Issue: While functional, using a more reasonable date like 2020-01-01 is cleaner

🟢 Suggestions

  1. Consider adding more test cases:
    • Zero income case
    • Income at exact phase-out threshold boundary
    • Head of household filing status

Validation Summary

Check Result
Regulatory Accuracy Appears correct per SB 520
Reference Quality Good - all params have references with page numbers
Code Patterns Good - uses proper where(), select(), parameter access
Test Coverage 3 test syntax errors causing failures
CI Status Failing (3 test failures)

Next Steps

To auto-fix issues: /fix-pr 7690

Primary fixes needed:

  1. Fix CTC calculation or test expectation
  2. Replace __gte assertions with actual expected values
  3. Consider adding explicit filing_status to tests

- Reorganize parameter folder structure:
  - in_effect.yaml at sb520/ for 2026 provisions
  - brackets/ folder with in_effect.yaml for 2027 tax brackets
  - Rename tax/ to brackets/
- Fix CTC to use baseline GA age threshold (under 6)
- Fix test expected values for CTC (1 child qualifies, not 2)
- Add edge case tests (fully phased out deduction, no qualifying children, high income)
- Update all parameter path references in reform code and tests

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@DTrim99 DTrim99 marked this pull request as ready for review March 3, 2026 18:52
@DTrim99 DTrim99 requested a review from PavelMakarchuk March 3, 2026 19:01
@PavelMakarchuk
Copy link
Collaborator

Program Review: PR #7690 -- Georgia SB 520 Contributed Reform

Source Documents

  • PDF: Georgia SB 520 (26 LC 44 3458) (9 pages)
  • Year: 2026-2027
  • Scope: PR changes only (12 parameter YAML files, 1 reform Python module, 1 test file, reform registration, changelog)
  • Bill Status: Not yet enacted -- read and referred to Senate Finance Committee. Acceptable as a contributed reform.

Critical (Must Fix)

  1. Wrong document URL in ALL references (232863 is HB 169, not SB 520)
    Every href across all 12 parameter YAML files and all 4 Python variable reference lines points to document ID 232863, which is HB 169 (a solar energy/conservation land bill). The correct document ID for SB 520 is 242809. Clicking any reference link in the PR takes the reader to an entirely unrelated bill.

    • Affected files (16 references total):
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/in_effect.yaml
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/brackets/in_effect.yaml
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/brackets/joint.yaml
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/brackets/single.yaml
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/brackets/head_of_household.yaml
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/brackets/separate.yaml
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/brackets/surviving_spouse.yaml
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/credits/ctc/amount.yaml
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/credits/eitc/match.yaml
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/deductions/standard/amount.yaml
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/deductions/standard/phase_out/rate.yaml
      • policyengine_us/parameters/gov/contrib/states/ga/sb520/deductions/standard/phase_out/threshold.yaml
      • policyengine_us/reforms/states/ga/sb520/ga_sb520_reform.py (4 variable reference attributes)
    • Fix: Replace 232863 with 242809 in every URL.
  2. Wrong page numbers for bracket parameter files (6 YAML files)
    After correcting the document ID, the #page= anchors for all bracket-related parameter files would land on the wrong page. The bracket rate/threshold tables in 48-7-20(a.1) are on PDF page 3 (lines 48-62), not page 2. The brackets/in_effect.yaml references page 1 (bill preamble), but Section 1 starts on page 2.

    • Files and corrections:
      • brackets/in_effect.yaml: #page=1 -> #page=2 (Section 1 heading starts on page 2)
      • brackets/joint.yaml: #page=2 -> #page=3 (48-7-20(a.1)(1), lines 48-54)
      • brackets/single.yaml: #page=2 -> #page=3 (48-7-20(a.1)(2), lines 55-62)
      • brackets/head_of_household.yaml: #page=2 -> #page=3
      • brackets/separate.yaml: #page=2 -> #page=3
      • brackets/surviving_spouse.yaml: #page=2 -> #page=3
  3. Wrong page numbers in Python variable references (3 of 4 wrong)
    In policyengine_us/reforms/states/ga/sb520/ga_sb520_reform.py:

    • ga_ctc reference: #page=7 -> #page=6 (page 7 is the tobacco excise tax section; CTC is Section 6 on page 6, line 121)
    • ga_eitc reference: #page=8 -> #page=6 (page 8 is the scholarship organization section; EITC is Section 7 on page 6, lines 128-130)
    • ga_income_tax_before_non_refundable_credits reference: #page=2 -> #page=3 (bracket tables are on page 3)
    • ga_standard_deduction reference: #page=5 -- CORRECT (no change needed)

Should Address

  1. Use if/else instead of where() for scalar parameter toggles
    p_sb520.in_effect and p_sb520.brackets.in_effect are scalar parameter booleans (the entire population uses the same code path for a given period). The idiomatic pattern is if p.in_effect: / else:, not where(sb520_active, ...). Using where() is not incorrect (NumPy broadcasts the scalar), but if/else avoids computing both branches unnecessarily and communicates intent.

    • File: policyengine_us/reforms/states/ga/sb520/ga_sb520_reform.py
    • Scope: 6 locations (ga_standard_deduction, ga_ctc, ga_eitc, ga_refundable_credits, ga_non_refundable_credits, ga_income_tax_before_non_refundable_credits)
  2. Trailing zero in EITC match rate parameter

    • File: policyengine_us/parameters/gov/contrib/states/ga/sb520/credits/eitc/match.yaml
    • Current: 2026-01-01: 0.20
    • Fix: 2026-01-01: 0.2
    • Per parameter conventions: remove trailing zeros.
  3. Missing end-to-end tax pipeline test (refundable/non-refundable credit reclassification)
    The most complex logic in the reform -- moving the CTC from non-refundable to refundable -- is untested. No test case asserts values for ga_refundable_credits, ga_non_refundable_credits, or ga_income_tax (final tax after all credits). If CTC were double-counted or lost in the transition, existing tests would not catch it.

    • File: policyengine_us/tests/policy/contrib/states/ga/sb520/integration.yaml
    • Recommended: Add a test checking ga_income_tax, ga_refundable_credits, and ga_non_refundable_credits simultaneously for a qualifying household.
  4. Missing joint filer 2027 bracket test
    Case 5 tests 2027 brackets for SINGLE only. Joint/surviving spouse filers have materially different thresholds (30k/60k vs 15k/30k). An error in the select() filing-status dispatch would go undetected.

    • File: policyengine_us/tests/policy/contrib/states/ga/sb520/integration.yaml
    • Recommended: Add a JOINT filer case with 2027 brackets to verify the 30k/60k thresholds.
  5. Missing multiple qualifying children CTC test
    Case 2 has 2 children but only 1 qualifies (age 5 < 6, age 8 >= 6). No test verifies the multiplication eligible_children * amount_per_child with count > 1 (e.g., 3 children under 6 -> $3,750).

    • File: policyengine_us/tests/policy/contrib/states/ga/sb520/integration.yaml
  6. Parameter description verbs not from approved list

    • deductions/standard/phase_out/rate.yaml: uses "reduces" (not in approved list: limits, provides, sets, excludes, deducts, uses)
    • deductions/standard/phase_out/threshold.yaml: uses "phases out" (not in approved list)
    • credits/eitc/match.yaml: uses "EITC" acronym instead of spelling out "Earned Income Tax Credit"
    • deductions/standard/amount.yaml: has unnecessary trailing phrase "depending on filing status"

Suggestions

  1. Add code comment for surviving spouse inference
    The bill does not explicitly mention "surviving spouse" as a filing status. The PR assigns surviving spouse the joint-equivalent values ($34,000 deduction, $280,000 phase-out threshold, 30k/60k brackets), which is a reasonable interpretation but is not directly stated in SB 520. A brief code comment noting this inference would improve traceability.

    • Affected files: brackets/surviving_spouse.yaml, deductions/standard/amount.yaml, deductions/standard/phase_out/threshold.yaml
  2. Improve in_effect description patterns

    • in_effect.yaml: "Georgia applies SB 520 2026 provisions..." could follow the standard toggle pattern: "Georgia uses this indicator to determine whether SB 520 standard deduction, child tax credit, and EITC provisions apply."
    • brackets/in_effect.yaml: similar improvement for consistency.
  3. Improve reference titles on in_effect parameters

    • brackets/in_effect.yaml reference title: "Georgia SB 520 Section 1" -- could add subsection detail: "48-7-20(a.1)(1)-(2), progressive income tax rates effective January 1, 2027"
    • in_effect.yaml reference title: "Georgia SB 520 Sections 4, 6, 7" -- could reference Section 11 effective date on page 9 instead of page 1
  4. Missing baseline regression test (reform off)
    No test verifies that when both in_effect toggles remain false (the default), all GA tax variables return baseline values. While implicitly tested by existing baseline tests, an explicit test would protect against accidental behavior changes when the reform is not enabled.

  5. Missing head-of-household and separate filing status tests
    HEAD_OF_HOUSEHOLD, SEPARATE, and SURVIVING_SPOUSE are completely untested across all components (standard deduction, phase-out, CTC, EITC, brackets). While they share parameters with tested statuses, the select() dispatch mapping for brackets is a potential point of error.

  6. EITC test uses approximate expected value
    Case 4 tests ga_eitc: 848 with comment "Federal EITC approximately $4,242." The expected value should be computed precisely rather than approximated, to ensure the test is verifiable without running the simulation.

  7. Top-level in_effect.yaml page reference could be more specific
    Currently #page=1 (bill preamble). The effective date provision is in Section 11 on page 9. Linking to #page=9 would be more useful for tracing the effective date.


PDF Audit Summary

Category Count
Confirmed correct (values match bill text) 11 parameters
Value mismatches 0
Surviving spouse interpretation (not in bill) 3 parameters
Missing from repo (out-of-scope provisions) 9 provisions (corporate tax, tobacco tax, scholarship repeal, etc.)

All modeled parameter values are numerically correct. The surviving spouse assignments use joint-equivalent values, which is a reasonable interpretation but not explicitly stated in SB 520.


Validation Summary

Check Result
Regulatory Accuracy CORRECT -- all provisions accurately implemented; 1 documented limitation (EITC NOL carryforward not modeled)
Reference Quality 3 critical issues -- wrong document URL (all 16 refs), wrong page numbers (6 YAML + 3 Python)
Code Patterns 2 issues -- where() vs if for scalar toggles (6 locations), trailing zero in parameter value
Test Coverage 4 gaps -- no end-to-end tax test, no joint bracket test, no multi-child CTC test, only 2 of 5 filing statuses tested
PDF Value Audit 0 mismatches / 11 confirmed correct / 3 surviving spouse inferred
CI Status All 7 checks passing

Review Severity: REQUEST_CHANGES

Rationale: Every reference URL in the PR points to the wrong bill entirely (HB 169 about solar energy instead of SB 520 about tax reform). This is a fundamental traceability failure -- no reviewer or auditor can verify any parameter value by clicking the current links. Additionally, 9 of 16 page number anchors are wrong. While all parameter values are numerically correct and the implementation logic is sound, the reference issues must be fixed before merge.


What Is Done Well

  • All parameter values match the bill text exactly (0 mismatches).
  • The reform architecture follows established patterns (two independent toggles, proper update_variable overrides, baseline fallback).
  • The tax pipeline correctly handles the CTC transition from non-refundable to refundable.
  • All 9 integration tests pass with clear calculation comments.
  • No hard-coded values, no reinvented variables, proper entity levels, correct period handling.
  • Changelog entry present and correctly typed as added.
  • Clean __init__.py exports and proper reform registration in reforms.py.

Next Steps

To auto-fix issues: /fix-pr 7690

- Replace incorrect document ID 232863 (HB 169) with correct ID 242809 (SB 520) in all 16 references
- Fix page numbers: bracket files now correctly reference page 3 (48-7-20(a.1))
- Fix page numbers in Python: CTC and EITC now reference page 6, brackets reference page 3
- Remove trailing zero in EITC match rate (0.20 → 0.2)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 3, 2026

Fixes Applied for PR #7690

🔴 Critical Issues Fixed

Issue Fix Applied
Wrong document URL in all 16 references ✅ Replaced 232863 (HB 169 - solar energy bill) with 242809 (SB 520 - tax reform bill)
Wrong page numbers in 6 bracket YAML files ✅ Fixed #page=2#page=3 for all bracket files (48-7-20(a.1) is on page 3)
Wrong page numbers in 3 Python variable refs ✅ Fixed: CTC #page=7#page=6, EITC #page=8#page=6, Brackets #page=2#page=3

🟡 Should-Address Issues Fixed

Issue Fix Applied
Trailing zero in EITC match rate ✅ Changed 0.200.2 in credits/eitc/match.yaml

Summary of Changes

  • 13 files modified with reference corrections
  • All 16 reference URLs now point to the correct SB 520 document
  • All page anchors now land on the correct page in the PDF

Verification

  • ✅ All source files have correct document ID (242809)
  • ✅ No trailing zeros in parameter values
  • ✅ Code formatting verified

Not Fixed (Out of Scope for Auto-Fix)

The following suggestions from the review are architectural/style choices that may need manual review:

  • where() vs if/else for scalar parameter toggles (6 locations) - current code works correctly
  • Additional test coverage for filing statuses (HEAD_OF_HOUSEHOLD, SEPARATE, SURVIVING_SPOUSE)
  • End-to-end tax pipeline test for refundable/non-refundable credit reclassification

🤖 Generated with Claude Code

@PavelMakarchuk
Copy link
Collaborator

Itemized Deduction Repeal Not Modeled

SB 520 Section 4 amends 48-7-27(a)(1) to eliminate the option to itemize nonbusiness deductions — it strikes the "At the taxpayer's election, either:" language and subsection (A), leaving only the standard deduction.

The reform overrides ga_standard_deduction (with new amounts and phase-out) but does not override ga_deductions. The baseline ga_deductions variable applies where(itemizes, itemized, sd), so filers who itemize federally will still incorrectly itemize for Georgia even with SB 520 active.

Impact: For filers whose federal itemized deductions exceed the SB 520 standard deduction, Georgia taxable income will be too low (deductions too high).

Suggested fix: Add ga_deductions to the reform's update_variable list with a formula that always returns the standard deduction when p_sb520.in_effect is true:

class ga_deductions(variable_cls):
    def formula(tax_unit, period, parameters):
        p_sb520 = parameters(period).gov.contrib.states.ga.sb520
        if p_sb520.in_effect:
            return tax_unit("ga_standard_deduction", period)
        # baseline logic
        itemizes = tax_unit("tax_unit_itemizes", period)
        sd = tax_unit("ga_standard_deduction", period)
        p = parameters(period).gov.irs.deductions
        itemized = add(tax_unit, period, p.itemized_deductions)
        return where(itemizes, itemized, sd)

🤖 Generated with Claude Code

SB 520 Section 4 amends 48-7-27(a)(1) to eliminate the option to itemize
nonbusiness deductions. When SB 520 is active, all filers must use the
standard deduction regardless of federal itemization status.

Without this fix, filers who itemize federally would incorrectly itemize
for Georgia even with SB 520 active, resulting in too-low taxable income.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@DTrim99
Copy link
Collaborator Author

DTrim99 commented Mar 4, 2026

Additional Fix: Itemized Deduction Repeal

Issue

SB 520 Section 4 amends 48-7-27(a)(1) to eliminate the option to itemize nonbusiness deductions. The reform was overriding ga_standard_deduction but not ga_deductions, which meant filers who itemize federally would incorrectly continue to itemize for Georgia even with SB 520 active.

Fix Applied

Added ga_deductions override to the reform:

  • When p_sb520.in_effect is true: Always returns the standard deduction
  • When inactive: Falls back to baseline behavior (itemize if federal itemizes)

This ensures filers who would benefit more from federal itemization don't incorrectly get higher Georgia deductions under SB 520.

🤖 Generated with Claude Code

DTrim99 and others added 3 commits March 6, 2026 16:27
Include both ga_sb520 and watca reforms after merging main.

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Co-Authored-By: Claude Opus 4.5 <[email protected]>
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