Skip to content

Add South Carolina fully refundable EITC contrib reform#8683

Merged
PavelMakarchuk merged 3 commits into
PolicyEngine:mainfrom
DTrim99:sc-fully-refundable-eitc-reform
Jun 23, 2026
Merged

Add South Carolina fully refundable EITC contrib reform#8683
PavelMakarchuk merged 3 commits into
PolicyEngine:mainfrom
DTrim99:sc-fully-refundable-eitc-reform

Conversation

@DTrim99

@DTrim99 DTrim99 commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

What this does

Adds a Child Poverty Impact Dashboard contrib reform that converts South Carolina's nonrefundable EITC to fully refundable for all filers, activated by gov.contrib.states.sc.child_poverty_impact_dashboard.eitc.in_effect. This is the SC half of #8682.

SC's EITC is 125% of the federal EITC, capped per filer at $200 from 2026, and is nonrefundable by default (applied only up to remaining state income tax liability), so zero-liability filers receive nothing today.

Approach

Mirrors the corrected Utah/Missouri/Ohio refundability reforms (#8645 / #8642 / #8657) — same principles, no reintroduced bugs:

  • Pay the potential, not the capped credit. sc_fully_refundable_eitc returns sc_eitc_potential (uncapped at liability), so the full credit is paid as a refund. sc_eitc is capped at liability and zeroes out for exactly the low-liability filers refundability helps.
  • Rebuild the non-refundable bucket correctly. sc_non_refundable_credits re-runs ordered_capped_state_non_refundable_credits over the ordered list with sc_eitc filtered out — no add("<param-path string>") bug, and no sum − sc_eitc overstatement when the bucket binds.
  • Clear inherited adds before adding a formula. sc_refundable_credits sets adds = None; subtracts = None (the baseline computes via adds) and returns the standard refundable credits plus sc_fully_refundable_eitc.

The statutory per-filer cap (gov.states.sc.tax.income.credits.eitc.max) is preserved and remains separately adjustable — the dashboard exposes it as its own lever.

New files

  • policyengine_us/reforms/states/sc/child_poverty_eitc/sc_fully_refundable_eitc_reform.py (+ __init__.py)
  • policyengine_us/parameters/gov/contrib/states/sc/child_poverty_impact_dashboard/eitc/in_effect.yaml
  • policyengine_us/tests/policy/reform/sc_fully_refundable_eitc.yaml
  • registration in policyengine_us/reforms/reforms.py; changelog fragment

Verification

  • 4 YAML cases pass: full potential paid as refundable at zero liability; no double-count when liability is present; the $200 (2026) cap is respected; no effect outside SC.
  • End-to-end: setting …child_poverty_impact_dashboard.eitc.in_effect = true on a 2026 SC filer with a federal EITC of $100 and zero SC liability yields sc_fully_refundable_eitc = $125, sc_refundable_credits = $125, and +$125 household net income (baseline pays $0).

Part of #8682.

DTrim99 and others added 2 commits June 18, 2026 14:34
Adds a Child Poverty Impact Dashboard contrib reform that converts South
Carolina's nonrefundable EITC (125% of the federal EITC, capped per filer at
$200 from 2026) to fully refundable for all filers, activated by
gov.contrib.states.sc.child_poverty_impact_dashboard.eitc.in_effect.

Mirrors the corrected Utah/Missouri/Ohio refundability reforms (PolicyEngine#8645/PolicyEngine#8642/
PolicyEngine#8657): pays sc_eitc_potential (uncapped at liability) so zero-liability
filers receive the credit, rebuilds sc_non_refundable_credits via the ordered
cap walk with sc_eitc filtered out (no add() path-string bug), and clears the
inherited adds/subtracts on sc_refundable_credits before giving it a formula.
The statutory per-filer cap (gov.states.sc.tax.income.credits.eitc.max) is
preserved and remains separately adjustable.

Tested: 4 YAML cases (zero-liability refund, no double-count with liability,
$200 cap respected, SC-only) plus an end-to-end check that the in_effect param
activates the reform and raises a zero-liability filer's net income.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…llowlist

The fully refundable SC EITC reform filters sc_eitc out of the ordered
non-refundable list by name, which the applied-credit downstream-consumer
code-health guardrail flags as a new consumer. Add the reviewed entry, as
done for the UT/OH refundability reforms.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@DTrim99 DTrim99 requested a review from PavelMakarchuk June 19, 2026 20:17

@PavelMakarchuk PavelMakarchuk 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.

Program Review — Add South Carolina fully refundable EITC contrib reform

Scope: Contrib reform (Child Poverty Impact Dashboard), SC EITC, 2026 · CI: ✅ 25/25 passing

The implementation is correct and faithfully mirrors the corrected OH/MO refundability pattern (filtered ordered-cap re-walk; adds=None cleared before the formula; no double-counting), avoiding the older UT baseline − ut_eitc bug. The statute is verified and references are sound. Requesting changes to tighten test coverage and fix misleading cap terminology before merge.

Requested changes

  1. Clarify "per-filer" → "per-tax-unit" terminology. The docstring, PR description, and test header describe a "per-filer $200 cap," but the implementation (sc_eitc_potential.py, max.yaml) applies one flat $200 clamp per tax unit — a married-joint couple gets $200, not $400. The statute (SC Code 12-6-3632 / H.4216 §7) says "not to exceed two hundred dollars" with no per-filer multiplier, so the model is correct — but the wording is misleading and should say "per return / per tax unit." (This cap is pre-existing baseline behavior, not introduced here.)

  2. Add missing edge-case tests. The 4 YAML cases all use 2026 + the always-on reforms: bypass and pin liability to 0 or a fully-absorbing value. Please add:

    • Partial liability — liability between 0 and the credit, confirming the full potential is paid (not the residual).
    • household_net_income assertion — every current case checks credit variables only; none confirms the refund flows end-to-end to net income. This is the highest-value missing test.
    • Married (MFJ) — pin the per-tax-unit cap behavior for 2 filers.
    • in_effect = false gating — the 5-year activation look-ahead is currently untested (all cases use the bypass).
    • Lower priority: a pre-2026 (infinite-cap) case and a case with another SC non-refundable credit present, to exercise the re-walk the docstring says prevents overstatement.

Suggestions (non-blocking)

  • Inline aggregate-override variables omit reference fields (consistent with baseline SC aggregates / UT sibling — cosmetic).
  • for i in range(5)for _ in range(5) (unused loop var).

Validation Summary

Check Result
Regulatory Accuracy ✅ Statute verified (125%, nonrefundable, $200 from 2026); reform logic correct, no double-counting
Reference Quality ✅ Toggle param matches 12 sibling conventions; statutory claims trace to H.4216 §7
Code Patterns ✅ Mirrors corrected OH/MO pattern; adds=None cleared; no hard-coding; changelog + registration present
Test Coverage ⚠️ Gaps: partial liability, net-income assertion, MFJ, gating-off
CI Status ✅ Passing (25/25)

To auto-fix: /fix-pr 8683

🤖 Generated with Claude Code

- Fix misleading "per-filer" wording in reform docstring/comments and
  tests: the $200 refundable cap is per return / per tax unit (MFJ also
  $200, not $400)
- Add edge-case tests: end-to-end refund flow through net income,
  partial-liability split, MFJ cap, and in_effect=false no-op gating
- Add reference to in_effect.yaml; use range(_) for the unused loop index

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@DTrim99

DTrim99 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator Author

Fixes Applied

Addressed the review (no critical issues; CI was green throughout).

Should-address

  • Clarified the cap is per tax unit, not per filer — corrected misleading "per-filer" wording in the reform docstring/comments and the test file. The model was already correct: the $200 refundable cap is a single flat clamp per return (MFJ also $200, not $400).
  • Added edge-case tests — end-to-end refund flowing through sc_income_tax/net income, partial-liability split, MFJ cap ($200), and the in_effect=false no-op default (baseline non-refundable behavior).

Suggestions

  • Added a reference (SC H.4216 §7 / SC Code 12-6-3632) to in_effect.yaml.
  • for i in range(5)for _ in range(5) (unused index).

Verification

  • Reform test: 8/8 pass; regression (baseline sc_eitc + h3492 contrib): 12/12 pass.
  • No expected-value corrections were needed.
  • ruff format + ruff check: clean.

@DTrim99

DTrim99 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

CI note: "Full Suite - Rest" failure was infrastructure, not a test failure

The failed Full Suite - Rest job did not contain any test failures. The logs show:

  • policyengine_us/tests/policy/reform/sc_fully_refundable_eitc.yaml ........ — all 8 cases (including the new edge cases) passed
  • 392 passed, 5 skipped and 67 passed across the shard's batches
  • The job then ended with ##[error]The runner has received a shutdown signal ... The operation was canceled.

That is a runner shutdown / cancellation (CI infrastructure), unrelated to this PR's changes. The job is now re-running and should go green.

@PavelMakarchuk PavelMakarchuk 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.

Re-reviewed after "Address review: clarify per-tax-unit cap wording, add edge-case tests" — all requested changes addressed:

  • Wording: docstring + comments now say "per return / per tax unit" (statute caps the credit at $200 per return, not per filer).
  • Edge-case tests added: end-to-end net SC tax refund (sc_income_tax: -125 at zero liability), partial liability (capped $200 credit leaves $100 owed on $300 liability), MFJ capped at $200 per tax unit (not $400), and in_effect=false gating no-op.

Verified all 8 YAML cases pass locally. The failing "Full Suite - Rest" check is a transient runner shutdown (infra), not a test failure — recommend re-running. Approving. 🤖

@PavelMakarchuk PavelMakarchuk merged commit c4fb9c4 into PolicyEngine:main Jun 23, 2026
75 of 78 checks passed
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