Skip to content

Separate the dependent portion of 11 states' personal exemptions#8696

Open
DTrim99 wants to merge 5 commits into
PolicyEngine:mainfrom
DTrim99:separate-state-dependent-exemptions
Open

Separate the dependent portion of 11 states' personal exemptions#8696
DTrim99 wants to merge 5 commits into
PolicyEngine:mainfrom
DTrim99:separate-state-dependent-exemptions

Conversation

@DTrim99

@DTrim99 DTrim99 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

What this does

Adds Child Poverty Impact Dashboard contrib reforms that separate the dependent portion of each state's per-person personal exemption (or credit) into its own variable + parameters, for 11 states: AR, HI, IN, MD, MI, NE, OH, OK, VT, WI, WV. This mirrors the existing RI/DE/OR/VA reforms and lets the per-dependent amount be adjusted or eliminated independently of the head/spouse exemption (which most of these states bundle together).

Each reform:

  • Defaults to a no-opin_effect=false, and when on, the default amount reproduces current law.
  • Exposes gov.contrib.states.{st}.dependent_exemption.{in_effect, amount, age_limit/*} (AR uses dependent_credit; MD/OH also get a phaseout group).

How each separates

Group States Approach
Bundled per-person exemption → head/spouse + dependent HI, IN, MI, NE, OK, VT, WI, WV Recompute the personal count excluding dependents; add a separate {st}_dependent_exemption. HI preserves its aged add-on; IN leaves the existing per-child additional exemption untouched.
AGI-stepped per-person amount MD, OH Dependent amount defaults to the baseline income-stepped schedule via a negative-amount sentinel (so default = no-op); a non-negative value applies a flat per-dependent amount, plus an optional income phase-out.
Shared credit base AR Redirects the existing person-level dependent slice of the personal credit to a contributed amount (its base is otherwise shared with head/spouse). No age limit — it's summed via adds, which only reflects uniform per-dependent amounts.

Indiana's module directory is the Python keyword in, so it is imported dynamically in reforms.py and uses getattr for parameter access.

Tests

Each state has YAML tests (tests/policy/contrib/states/{st}/) covering: default no-op for a family with children, amount reduction / elimination (amount: 0), childless-filer no-op, and (exemption states) age-limit fallback. All pass. The system smoke-imports cleanly with all 11 registered in create_structural_reforms_from_parameters.

Follow-up

Once released, the dashboard flips these states to amount-editable (separate PR). Companion: #8691 (SC dependent-exemption parameter).

🤖 Generated with Claude Code

DTrim99 and others added 2 commits June 19, 2026 16:09
Adds Child Poverty Impact Dashboard contrib reforms that split the dependent
portion of each state's per-person personal exemption (or credit) into its own
variable + parameters, mirroring the existing RI/DE/OR/VA reforms. Each defaults
to a no-op (in_effect=false; default amount reproduces current law) and lets the
per-dependent amount be adjusted or zeroed independently of the head/spouse
exemption.

States and how they separate:
- Bundled per-person exemption split into head/spouse + dependent: HI (preserves
  the aged add-on), IN (base; the additional per-child exemption is untouched),
  MI, NE, OK, VT, WI, WV.
- AGI-stepped per-person amount (MD, OH): the dependent amount defaults to the
  baseline income-stepped schedule via a negative-amount sentinel, with an
  optional flat override and income phase-out group.
- AR: redirects the existing person-level dependent slice of the personal credit
  to a contributed amount (its base is otherwise shared with head/spouse). No
  age limit, since it is summed via `adds`.

Each reform exposes in_effect + amount (+ age_limit for the exemption states;
+ phaseout for MD/OH). Indiana's module dir is the Python keyword `in`, so it is
imported dynamically in reforms.py and uses getattr for parameter access.

Parameters under gov.contrib.states.{st}.dependent_exemption (AR:
dependent_credit). Registered in reforms.py. Each state has YAML tests covering
the default no-op, amount reduction/elimination, childless-filer no-op, and
age-limit fallback; all pass. System smoke-imports cleanly.

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

- Remove accidentally committed SEP_MSG.tmp scratch file from repo root
- HI dependent-exemption reform: use `reference` field instead of `documentation`
- Add MD/OH flat-amount and phaseout test cases (previously untested paths)
- Add joint-filing test cases (MD, OH, MI, AR, HI, IN) covering the
  head/spouse-vs-dependent count split in 2-adult units
- Strengthen MD/OH no-op case documentation

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

@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 — Separate the dependent portion of 11 states' personal exemptions

Scope: Child Poverty Impact Dashboard contrib reforms, 11 states (AR, HI, IN, MD, MI, NE, OH, OK, VT, WI, WV) · three separation patterns (bundled split; AGI-stepped sentinel + phaseout; shared credit-base redirect)

A large but disciplined PR. All four review dimensions cleared the implementation: every state defaults to a verified no-op, cross-state isolation holds (no copy-paste/wrong-state references — the main risk in a multi-state PR), variable overrides match their baselines exactly, and all 55 YAML cases pass. Requesting changes to tighten the test suite before merge — the gaps are in what the tests prove, not in the code.

Requested changes

  1. Add an end-to-end tax/net-income assertion (systemic — affects all 11 states). Every test currently asserts the reform's own exemption/credit-total variable (e.g. md_total_personal_exemptions, hi_regular_exemptions, ar_personal_credits) — one level above the per-dependent variable, which does prove the head/spouse split arithmetic. But no test asserts state taxable income, state tax, or net income, so the exemption→tax wiring is unproven end-to-end. AR is most exposed (it asserts only a credit total, never a resulting AR tax/net-income figure). Please add at least one downstream-outcome case — ideally for AR plus one bundled-split state and MD/OH.

  2. Add the joint 2-adult case to NE, OK, VT, WI, WV. These five states have 4 cases and omit the joint two-adult + dependents case that HI/IN/MI/MD/OH/AR include. That case is precisely what catches off-by-one / sign-flip errors in the personal_count = size − dependents + over-age dependents formula — the risk the PR description itself flags. Add a joint 2-adult + 2-dependent case to each, mirroring HI/IN/MI.

Suggestions (non-blocking)

  • Re-run the canceled "Quick Feedback (Selective Tests + Coverage)" job — it was canceled by an infra runner-shutdown, not a real failure (the Full Suite - Contrib (states-shard-*) jobs that run these tests all passed).
  • Several count variables are typed float / unit = USD (should be int/count); unused instant import in ~7 reform files.
  • Most contrib params omit reference — consistent with the merged DE/RI/VA/OR siblings, so optional; a one-line provenance comment on each amount.yaml noting it mirrors the state baseline would make the derivation explicit.
  • Minor consistency: inconsistent state-level __init__.py presence (works via namespace packages); WI/WV test values lack numeric underscores; effective-date style varies (0000-01-01 vs 2025-01-01) for inert defaults.

Validation Summary

Check Result
Regulatory Accuracy ✅ No-op fidelity verified for all 11; bundled-split arithmetic, MD/OH sentinel, and state-specific preservations (HI aged add-on, IN per-child exemption, AR credit base, MI stillborn, OK extras, WV size==0) all correct; isolation confirmed
Reference Quality ✅ All 11 default amounts trace to state baselines; metadata complete & consistent with siblings
Code Patterns ✅ All update_variable overrides match baseline name/entity/period/value_type; non-negative fallback counts; IN dynamic import correct; 5-year gating loop references each state's own parameter — no cross-state leakage
Test Coverage ⚠️ No-op + elimination + special mechanics covered; gaps: no downstream-tax assertion; 5 states lack joint case
CI Status ⚠️ One job canceled by infra (runner shutdown) — not a code failure; contrib state shards passed

Review Severity: REQUEST_CHANGES (approve-leaning — gaps are test-completeness only)

To auto-fix: /fix-pr 8696

🤖 Generated with Claude Code

Resolves PavelMakarchuk review on PR PolicyEngine#8696:
- Add downstream-tax/net-income assertions (AR, MI, MD, OH) so tests
  no longer stop at intermediate exemption/credit variables
- Add married-filing-joint 2-adult cases for NE, OK, VT, WI, WV
- Add age-17 (eligible) / age-18 (excluded) boundary cases for all 10
  age-limit states
- Enrich AR dependent credit reference (AR1000F Line 7A + statute
  subsections); fix WI/WV test literal underscores
- Remove unused `instant` imports (7 reform modules); add ne/wi
  re-export __init__.py; type dependent-count helpers as int (drop USD)

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

DTrim99 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Fixes Applied (full scope)

Addresses PavelMakarchuk's review. CI: the prior "Full Suite - Rest" failure was a runner cancellation, not a test failure.

Blocking review items

  • Downstream-outcome assertions — tests no longer stop at intermediate exemption/credit variables; added state taxable-income / tax-reducing assertions for AR (ar_non_refundable_credits), MI (mi_taxable_income), MD (md_exemptions), and OH (oh_taxable_income), confirming the dependent-exemption change flows through.
  • Joint 2-adult cases — added married-filing-joint cases for NE, OK, VT, WI, WV (HI/IN/MI/MD/OH/AR already had them), verifying the head/spouse-vs-dependent count split has no off-by-one.

Suggestions

  • Age-limit boundary tests — age-17 (eligible) / age-18 (excluded) cases for all 10 age-limit states.
  • AR reference — added AR1000F instructions (Line 7A, where $29 appears) plus statute subsections, mirroring the AR baseline parameter.
  • WI/WV test literals — added missing numeric underscores.
  • Module cleanups — removed unused instant imports (7 reform modules); added the missing ne/wi re-export __init__.py; typed the dependent-count helper variables as int (dropped the spurious USD unit).

Verification

  • Reform modules + reforms.py import cleanly (py3.11).
  • Local contrib test run for all 11 states was passing (no failures); CI on this PR is the authoritative gate.
  • No duplicate test cases; no parameter/reform-logic changes beyond the cleanups above.

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

Thanks for the updates — the joint 2-adult cases for NE/OK/VT/WI/WV (plus the age-limit boundary cases) and the downstream taxable_income assertions for MI and OH are exactly what was needed, and all 11 suites pass.

One remaining item from the downstream-assertion request: AR still asserts only ar_personal_credits (a credit total) — no resulting AR income-tax / net-income figure. AR is the one state using the shared-credit-base-redirect pattern (the credit is consumed via adds), so it's the most exposed to a wiring issue between the separated dependent credit and the actual tax — and it's the only pattern with no end-to-end assertion.

Requested change: add one downstream AR case asserting ar_income_tax (or net income) so the dependent-credit → tax wiring is proven end-to-end, mirroring the MI/OH taxable_income cases. A no-op default case plus an amount: 0 case showing AR tax rises would be ideal.

Everything else is good to go — this is the last blocker from my side.

Note: the failing "Quick Feedback" check is a transient runner-shutdown (infra), not a real failure; recommend re-running. 🤖

AR uses the shared-credit-base-redirect pattern (credit consumed via
adds), so it was the one state lacking a downstream tax assertion. Add a
default vs amount:0 pair on ar_income_tax proving the dependent credit is
wired through to the actual tax: removing the dependent portion raises AR
income tax by exactly 58 (1346.67 -> 1404.67), matching the credit drop
from 87 to 29.

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

DTrim99 commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

AR end-to-end income-tax assertion added

Good call — AR was the one state (shared-credit-base-redirect pattern, credit consumed via adds) lacking a downstream tax assertion. Added a default vs amount: 0 pair on ar_income_tax (mirroring the MI/OH taxable-income cases):

  • Default (no-op, credit 87): ar_income_tax = 1346.67
  • amount: 0 (credit 29): ar_income_tax = 1404.67

The tax rises by exactly $58 when the dependent portion is removed — matching the credit drop from 87 → 29, proving the dependent-credit → tax wiring end-to-end. All 9 AR cases pass locally (py 3.11).

On the "Quick Feedback" check — agreed, that was a runner-shutdown/cancellation (infra), not a real test failure; it should go green on re-run.

@DTrim99 DTrim99 requested a review from PavelMakarchuk June 24, 2026 13:23

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

Review: Separate dependent portion of 11 states' personal exemptions

Thanks for this — the separation approach is sound and faithfully mirrors the RI/VA/DE precedent. The split math (head/spouse slice + dependent slice = original per-person total) checks out algebraically and against the no-op tests for all 11 states, and the reform correctly defaults OFF. Requesting changes on a few items before merge, the first being a genuine correctness gap.

Must address

  1. MI & NE: the in_effect=true default stops reproducing current law outside 2025.
    mi/dependent_exemption/amount.yaml and ne/dependent_exemption/amount.yaml freeze the per-dependent amount at the single 2025 value (5_800 / 171), but the baseline per-person amounts (mi/.../exemptions/personal.yaml, ne/.../exemptions/amount.yaml) carry uprating: gov.irs.uprating. So when the reform is on, the personal slice uses the live uprated baseline while the dependent slice stays frozen — the "default = no-op" invariant silently breaks for 2026+ and pre-2025. A dashboard sweeping years will hit this. Fix by mirroring VT's uprating on the contrib param (vt/dependent_exemption/amount.yaml), or by adopting the MD/OH -1 sentinel. (AR/HI/IN/OK/WI/WV are safe — flat, non-uprated baselines.)

  2. Add references to the 8 value params that lack them.
    hi/in/mi/ne/ok/vt/wi/wv .../amount.yaml have no reference field. Each value exactly matches a baseline parameter that already carries a full authoritative reference — please copy those over so the new file is independently traceable to current law.

  3. Cover the reform-activation branch + add downstream tax assertions.
    Tests apply each reform via the bypass=True object, so the non-bypass reform_active = True / return create_…() path never executes (the per-test in_effect: true input is inert — formulas only read p.age_limit.in_effect). Add a parametrized pure-Python test calling each create_<st>_…_reform_fn(parameters, period) with in_effect true and asserting a Reform (not None) is returned. Separately, only AR has an end-to-end income-tax assertion; HI/IN/NE/OK/VT/WI/WV assert the reform variable in isolation. Add a downstream state-tax assertion for those states so the separated dependent portion is shown to reach the tax base.

Should address

  1. Missing __init__.py in the ar, md, ne, oh, wi, wv reform dirs — works via namespace packages, but the precedent and the other states ship explicit ones; add for consistency.
  2. MD/OH amount.yaml descriptions are two sentences (sentinel explanation) — split to one sentence per the parameter style rule.
  3. Inconsistent value-from dates — age threshold uses 2025-01-01 in 8 states but 0000-01-01 in MD/OH. Harmless (gated by in_effect=false) but worth normalizing.

Suggestions

  • Adopt the MD/OH where(amount < 0, baseline_per, amount) sentinel across all flat-amount states — it auto-tracks the baseline and removes the MI/NE bug class entirely.
  • Consider a boolean toggle instead of the -1 currency sentinel (like the existing age_limit/in_effect), since -1 overloads a currency-USD param.
  • No-op tests assert a hand-computed constant; a baseline-vs-reform equality assertion would be more robust.
  • Factor out the repeated range(5) lookahead loop (appears 11×).
  • AR statute href uses commercial casetext.com; an official source is preferable.

CI note (not blocking)

The failing Quick Feedback (Selective Tests + Coverage) check was an infrastructure runner-shutdown cancel at 10m01s while the selective suite was still passing (only ~27 of 90 tests had run, all green; the coverage step never started and runs --fail-under=0). Re-run the job. If it recurs, the suite is genuinely slow — each structural reform forces a full system rebuild per YAML test — and would benefit from an explicit timeout-minutes or sharding.


🤖 Review 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.

2 participants