Skip to content

Apply North Carolina Work First's 27.5% earnings exclusion and $25 minimum payment - #10260

Merged
PavelMakarchuk merged 6 commits into
mainfrom
fix-10210-nc-work-first-earnings
Oct 27, 2026
Merged

PavelMakarchuk merged 6 commits into
mainfrom
fix-10210-nc-work-first-earnings

Conversation

@PavelMakarchuk

@PavelMakarchuk PavelMakarchuk commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

North Carolina Work First had three errors.

  1. Earnings. nc_tanf_countable_earned_income added every member's gross earnings. The TANF State Plan FFY 2026-2028 Financial Rules has two rules here, and the 2022-2025, 2019-2022 and 2016-2019 plans have the same wording. Rule 6 says "27.5% of gross earned income is excluded". Rule 3 says all income is counted "except the earnings of children, and the income of SSI recipients". The Work First Manual, section 114 I also says "Do not count a child's earned income". The variable now excludes earnings of members under the program's child age limit (18) and of SSI recipients, and then excludes 27.5% of the rest. The rate is a new parameter, gov.states.nc.ncdhhs.tanf.income.earned_income_exclusion_rate.
  2. SSI recipients' income. Financial Rules 3 and 4 ("no income of SSI recipients is counted"), 10A NCAC 71W .0408(a)(1) and the WF-114 chart ("Do not count any of their income") exclude all income of SSI recipients. nc_tanf_household_size already drops them from the need standard. nc_tanf_countable_earned_income and nc_tanf_countable_gross_unearned_income now drop their earnings and unearned income, with the same ssi > 0 | receives_ssi check. Child support still counts when an SSI-recipient parent receives it, because WF-114 section XII and 71W .0408(a)(2) treat it as the dependent child's income; only an SSI-recipient child's own child support is excluded.
  3. $25 minimum. nc_tanf_income_eligible divided the reduced need standard by the household size and required $25 a month per person. Financial Rules 8 (FFY 2026-2028, p. 19) says "A family remains eligible if the result of the payment calculation is twenty-five dollars ($25.00) or more", and 10A NCAC 71W .0601(a) says payments "shall not be made for less than twenty five dollars ($25.00)". The payment is 50% of the difference between countable income and the need standard. The test is now 0.5 × reduced need standard ≥ $25 a month, with no division by household size. The existing household_size > 0 guard stays.

Tests

  • nc_tanf_countable_earned_income.yaml: three existing cases now expect 72.5% of earnings ($1,000 gives $725, $500 gives $362.50). New cases: a 16-year-old's earnings are excluded (6,000 × 0.725 = $4,350); an 18-year-old not in high school has earnings counted ($2,175, the PolicyEngine approximation; see North Carolina Work First: non-parent adults, 18-year-olds in high school, and all-SSI children #10492); an SSI-recipient parent's $4,800 is excluded and the other parent's $2,000 counts ($1,450).
  • nc_tanf_countable_gross_unearned_income.yaml: an SSI-recipient parent's $6,000 of Social Security is excluded, and the $1,000 of child support they receive for the child counts with the other parent's $1,200 pension and $500 of child support ($2,700). Child support that an SSI-recipient child receives is excluded ($1,800).
  • nc_tanf_income_eligible.yaml: a size-6 unit with a $50 payment is eligible (main: 100 / 6 < 25, ineligible). A size-1 unit with a $20 payment is ineligible (main: eligible). Two controls test exactly $25 (eligible) and $24.50 (ineligible).
  • integration.yaml: the issue's case is a parent of five with $7,176 a year of earnings. Countable income is $5,202.60 and nc_tanf = 0.5 × (8,376 − 5,202.60) = $1,586.70 (main: $0). A parent of two with $600 a month gets 0.5 × (544 − 435) = $54.50 a month (main: $0). Two parents and a child, where the parent with $400 a month of earnings receives SSI, get 0.5 × 472 = $236 a month (previous head: $91; main: $36). A parent on SSI who receives $300 a month of child support for a child aged 5 gets 0.5 × (362 − 300) = $31 a month (main: $31). Seven existing cases with earnings were recomputed by hand with the exclusion. Each has the arithmetic in a comment.
  • partners/analytics_coverage/edge_cases/state/nc/tanf.yaml: four fixtures were recomputed for the 27.5% exclusion and the $25 rule. The two cliff fixtures moved to the new cliff: $8,176 a year of earnings gives a $300.20 payment (eligible), and $8,177 gives $299.84 (ineligible). The SSI change moves no partner fixture. These edits need @hua7450's approval (see the PR comments).

With this PR, gov/states/nc passes (402 tests), the NC partner fixtures pass (32 cases in 10 files), gov/hhs/tanf/cash passes (36), and the code-health and parameter checks pass (450).

The average_reduced_need_standard_threshold parameter keeps its name to keep the change small. It now has the State Plan and 10A NCAC 71W .0601(a) references, and its label already says "monthly minimum benefit".

Follow-up for the older gaps (non-parent adults, 18-year-olds in high school, all-SSI children): #10492.

Fixes #10210

axiom: TheAxiomFoundation/rulespec-us#1947 queued

🤖 Generated with Claude Code

…nimum payment

North Carolina excludes 27.5% of gross earned income and does not count
children's earnings (TANF State Plan, Financial Rules 3 and 6), and pays a
family when the payment, 50% of the difference between countable income and
the need standard, is $25 or more (Financial Rules 8). PE counted all earnings
and required $25 of the reduced need standard per household member.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PavelMakarchuk
PavelMakarchuk marked this pull request as ready for review October 23, 2026 17:03
@codecov

codecov Bot commented Oct 23, 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 (faf0bf8).
⚠️ Report is 712 commits behind head on main.

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

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

DTrim99 commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

PR Review: Apply North Carolina Work First's 27.5% earnings exclusion and $25 minimum payment

Summary

The PR fixes two errors in North Carolina Work First (#10210):

  • Countable earned income. nc_tanf_countable_earned_income was a plain adds of every member's earnings. It is now a formula. It drops the earnings of members under eligibility.age_limit (18), then excludes 27.5% of the rest. The 27.5% is a new parameter, income/earned_income_exclusion_rate (0.275 from 1997-01-01).
  • $25 minimum. nc_tanf_income_eligible used to divide the reduced need standard by household size and require $25 a month per person. It now requires 0.5 × reduced need standard ≥ $25 × 12 and keeps the household_size > 0 guard.

It also recomputes 7 integration cases, 3 earned-income cases and 4 partner fixtures, adds 2 + 4 + 2 cases, and adds a fixed changelog fragment.

  • Law. Both fixes are right in every claimed year:
    • Financial Rules 6, "27.5% of gross earned income is excluded", appears in all four State Plans from October 2016 through September 2028.
    • The payment is "50% of the difference between the total countable income and the need standard" (Financial Rules 8).
    • Payments "will not be made for less than twenty-five dollars" (Financial Rules 8). 10A NCAC 71W .0601(a) says the same: "shall not be made for less than twenty five dollars ($25.00)".
    • No source divides the $25 by household size.
  • Order of operations. NC has no separate gross or net income test, because eligibility is the payment test.
    • Children's earnings are left out first, then 27.5% comes off the rest.
    • Unearned income is added in full, and the total is subtracted from the need standard.
    • The result is halved, and the $25 floor is applied to the payment.
    • A flat percentage gives the same result per person or per unit, and the plans apply it to applicants and recipients alike.
  • Payment formula. The existing nc_tanf (50% × (need standard − countable income), capped at the need standard) matches Financial Rules 8 and WF-114 section III. Unlike Apply Mississippi TANF's $90 work expense and dependent care deductions and $10 minimum payment #10258, the new expectations don't pin a payment formula that the rule contradicts.
  • Code. The code is vectorized. It adds no state-gated key lookups, so the NH Add New Hampshire state supplementation for 2026 #10156 pattern doesn't apply.
  • Tests. I hand-checked 27 expectations, and all are right under the PR's formula.
  • CI and branch. All 30 checks pass, and the merge with main is clean.

Three things need fixing:

  1. The PR edits a partner contract test file without the required Ziming Hua (@hua7450) sign-off.
  2. The new exclusion carries out only half of the sentence it cites. Financial Rules 3 also excludes "the income of SSI recipients", but an SSI-recipient parent's earnings still count. The new 18-year-old test also presents a PolicyEngine approximation as the rule.
  3. The axiom: line is missing.

Critical (must fix)

None.

Should address

1. The partner contract test edits need Ziming Hua's three-question sign-off before merge.

The PR changes policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/state/nc/tanf.yaml, which is under partners/**. CLAUDE.md, at this head and on main since #10035 (2026-10-09), says:

  • "Ziming Hua (@hua7450) approves every edit to files in this folder".
  • The author must "Request his review (gh pr edit <number> --add-reviewer hua7450) and post a comment ... that names each changed case, its old and new expected values, and the model change and law behind it".
  • Do "not merge a PR that changes them until he has approved a head that contains the edits".

At head 8044240552, the only requested reviewer is DTrim99, and the PR has no comment except Codecov's. The "Partner API impact notice" workflow sent its automatic Slack alert on 2026-10-23, but the alert doesn't meet the gate.

The edits look right to me (see "Verified correct"), but the sign-off is Ziming's, not the reviewer's. The changed cases for the comment are:

Fixture Input change nc_tanf / tanf
tanf_mid_phaseout_earned_income (nc) none 1,764 → 2,176.50
tanf_self_employment_income_counted_as_earned (nc) none 1,632 → 1,962
tanf_income_at_min_benefit_boundary_eligible (nc) earnings 5,628 → 8,176 450 → 300.20
tanf_income_just_above_min_benefit_boundary (nc) earnings 5,629 → 8,177 0 → 0

The file's header comment also changes.

Partner impact: NC Work First amounts rise for every earner (each dollar of earnings now cuts the benefit by $0.3625, not $0.50). The size-3 cliff moves from $5,628 to $8,176 a year of earnings.

Fix:

  • Run gh pr edit 10260 --add-reviewer hua7450.
  • Post a comment with the table above, the model change (Financial Rules 6 and 8) and the three questions: (1) are you sure you want to edit this test file, (2) have you notified a team member, and (3) have you notified the API partner.
  • Merge only after Ziming approves a head that contains the edits.

2. The new exclusion carries out half of Financial Rules 3. SSI recipients' earnings still count, and the 18-year-old test presents an approximation as the rule.

nc_tanf_countable_earned_income.py:20-22 cites "State Plan Financial Rules 3: the earnings of children are not counted" and zeroes out members under 18. The full rule is broader:

  • Financial Rules 3 (FFY 2026-2028 plan, file page 18): "all income of Work First participants is counted, except the earnings of children, and the income of SSI recipients, Foster Care Assistance, Adoption Assistance, and the Earned Income Tax Credit".
  • Financial Rules 4 (same page): "Income of parents who live in the household and are excluded from the case is counted as if they are included in the case. The exception is that no income of SSI recipients is counted."
  • 10A NCAC 71W .0408(a) (file page 7, readopted 2021-10-01):
    • "(1) Parents' incomes ... shall be counted unless the parent, adoptive parent, or stepparent receives SSI".
    • "(2) Countable net unearned income of a child who is included in the assistance unit shall be counted."
  • WF-114 section I (the PR's own file page 2): for a parent who is an SSI recipient, "Do not count any of their income".

The two parts of this finding:

(a) SSI recipients' earnings. The formula still counts 72.5% of an SSI recipient's earnings, although nc_tanf_household_size already drops that person from the need standard, using (ssi > 0) | receives_ssi.

  • Example: two parents and a child aged 5. Parent A receives SSI and earns $400 a month, and parent B has no income. The household size is 2, so the need standard is $472.
  • The PR gives (472 − 290) × 0.5 = $91 a month. The rule gives 472 × 0.5 = $236 a month, a gap of $1,740 a year.
  • Main also counts these earnings (in full), so the gap isn't new. But the PR rewrites this formula to apply Financial Rules 3 and applies only the first exclusion in its list.

(b) The 18-year-old test (nc_tanf_countable_earned_income.yaml:58, "Earnings of an 18-year-old member are counted.") pins 2,175 for a non-parent 18-year-old. Two sources disagree:

  • 71W .0408(a)(1) counts parents' earnings, not those of other adults.
  • WF-109 (Age Rule for Children, 2026-10-01) and the plan's Non-Financial Rule 1 (file page 12) say "A child who is 18, enrolled in high school and expected to graduate by the month of their 19th birthday, may remain eligible through the month of graduation, provided the case remains active." That makes them a child, whose earnings aren't counted.

PolicyEngine includes every non-SSI member in the need standard, so counting their earnings is a consistent approximation. But the test name and comment state it as North Carolina's rule.

Fix (either):

  • (a) Preferred: exclude SSI recipients here, the same way the household size does:
    is_ssi_recipient = (person("ssi", period) > 0) | person("receives_ssi", period)
    counted = ~is_child & ~is_ssi_recipient
    adult_earned = spm_unit.sum(where(counted, gross_earned, 0))
    • No new dependency cycle: nc_tanf_household_size already reads ssi in the same chain. IL, KS and MO TANF use the same check.
    • Add a case: the two-parent example above with receives_ssi: true on parent A gives countable earnings 0 and nc_tanf 236 × 12 = 2,832.
    • Then rename the 18-year-old case to say what it pins, for example "Earnings of an 18-year-old not in high school are counted (PolicyEngine includes every non-SSI member in the unit)". Optionally treat 18-year-olds with is_in_secondary_school as children (the federal and IL TANF pattern).
    • Applying the SSI exclusion to unearned income too, in nc_tanf_countable_gross_unearned_income (71W .0408(a)(1) covers all of an SSI parent's income), can be part of this fix or a follow-up.
  • (b) Keep the scope and document the deferral.
    • Change the comment at line 20 to quote all of Financial Rules 3 and say that SSI recipients' income is not yet excluded.
    • Open a follow-up issue and link it in the body.
    • Reword the 18-year-old test name and comment as in (a).

3. Add the axiom: line to the PR body.

The body (head 8044240552, last edited 2026-10-23) has no line mentioning axiom in any format. Pavel is staff and this is a policy fix, so the line is required, and axiom: needed isn't available.

encoded-correct isn't available either: rulespec-us has no North Carolina Work First module. See "Axiom line assessment" for the queued line and the issue contents.

Suggestions

  1. Cite the regulation and close the year gap in the references.
    • In average_reduced_need_standard_threshold.yaml, add 10A NCAC 71W .0601(a) (file page 10): "Payments, including prorated payments for the month of application or termination, shall not be made for less than twenty five dollars ($25.00)." It is the codified rule, and it settles the ambiguous WF-114 wording (Suggestion 2).
    • In earned_income_exclusion_rate.yaml, add the ACF-approved 2019-2022 plan, Financial Rules 6 (file page 16). The 2016, 2022 and 2026 references skip FFY 2020-2022, and that plan has the same 27.5% text.
  2. The WF-114 wording for the $25. WF-114 section III (file page 5) says "If the difference is $.01 to $24.99, the household is ineligible", and "difference" there could mean need standard less income, before halving. The PR applies the $25 to the payment. That is right, given Financial Rules 8, 71W .0601(a), and the manual's own next sentence ("If a recoupment results in a payment of less than $25.00 ..."). If you add a comment about it, avoid phrasing such as "the manual says ..." or "conflicts with". The source-conflict ratchet would count it unless the conflict is registered and marked.
  3. Rename average_reduced_need_standard_threshold to something like eligibility/minimum_payment in a follow-up. The body explains why the name was kept, and the label and description already say "minimum benefit", so this is optional.
  4. Pre-existing items for a follow-up issue (outside this diff):
    • Non-parent adults. 71W .0408(a)(1) counts parents' income, and WF-114 section I says not to count a non-parent caretaker's income. PolicyEngine counts, and includes in the need standard, every non-SSI member of the SPM unit.
    • Two SSI children. The two-SSI-children integration cases now expect $36 and $61.375 a month (a one-person payment for the parent). Non-Financial Rule 2 (file page 12) gives a one-person payment when a parent "only have one child, and that child receives SSI". Check the manual for a parent whose children all receive SSI.
    • payment_percentage.yaml. Its 2016-2019 plan anchor #page=35 shows the need standard table but not the "50% of the difference" text, which is on file page 34.

Verified correct

  • Rule text, with file pages:
    • FFY 2026-2028 plan (effective 2025-10-01 to 2028-09-30):
      • Financial Rules 3, 4 and 6 are on file page 18: "For earned income, percentages are subtracted from the total monthly gross income as follows: 27.5% of gross earned income is excluded."
      • Financial Rules 8 is on file page 19: "The Work First monthly payment is 50% of the difference between the total countable income and the need standard ... A family remains eligible if the result of the payment calculation is twenty-five dollars ($25.00) or more. Payments ... will not be made for less than twenty-five dollars."
    • 2022-2025 plan: the same text on file page 17 (Financial Rules 3 and 6) and file page 18 (Financial Rules 8).
    • 2019-2022 plan: the same text on file pages 15-16.
    • 2016-2019 plan (effective 2016-10-01): Financial Rules 3, 6 and 8's 50% on file page 34, and the $25 on file page 35.
    • Every cited #page= anchor in the new parameter and both reference tuples shows its text, and no title carries a page number.
  • Years. The 27.5%, the 50% and the $25 are unchanged from October 2016 through September 2028.
    • The 1997-01-01 start predates the earliest cited plan, but it matches every other NC TANF parameter, and parameters backfill earlier years anyway.
    • The tests use 2024 and 2026.
  • The $25 test. It is (household_size > 0) & (reduced_need_standard × 0.5 ≥ 25 × 12).
    • Exactly $25 is eligible ("$25.00 or more").
    • It is annual, so it equals the monthly test when monthly income is level.
    • There's no division by household size.
  • Wage definition (OK Add Oklahoma state supplementation for 2026 #10165 / Document disjoint health premium inputs and correct consumer treatment #10046 family). WF-114 defines gross earned income as earnings "before taxes, social security, and other deductions are made by the employer". The earned list reads employment_income and self-employment income, with no netting of pre-tax premiums, which is consistent with that definition.
  • Code.
    • add(person, period, p.income.earned), where(is_child, 0, …) and spm_unit.sum are vectorized.
    • age defaults to 40, so the single-person earned-income cases count the earnings, which CI confirms.
    • No state-gated variable is used as a parameter key.
  • Callers. Only nc_tanf_reduced_need_standard reads the earned income variable, and only nc_tanf_eligible reads nc_tanf_income_eligible. nc_tanf feeds tanf_if_takes_up and the programs registry. No other parameter or reform references the changed parameters.
  • Hand-checked expectations (27), all correct. Need standards are $362 / $472 / $544 / $698 / $1,042 for sizes 1 / 2 / 3 / 6 / 14.
    • nc_tanf_countable_earned_income.yaml:
      • 1,000 × 0.725 = 725.
      • 500 × 0.725 = 362.50 (two cases).
      • Parent 6,000 and a 16-year-old's 3,000: 6,000 × 0.725 = 4,350.
      • An 18-year-old's 3,000 × 0.725 = 2,175 (Should 2(b) is about the name, not the arithmetic).
    • nc_tanf_income_eligible.yaml:
      • Size 6, $100 reduced: 50 ≥ 25, so true. Main gives false (100 / 6 < 25).
      • Size 1, $40: 20 < 25, so false. Main gives true.
      • $50: 25 ≥ 25, so true.
      • $49: 24.50, so false.
      • The existing $72 / size 2 case: 36 ≥ 25, so true.
      • The size-0 cases stay false.
    • integration.yaml:
      • $422: 305.95 countable, 0.5 × 166.05 = 83.025, so 996.30 a year.
      • $400 with one SSI child (size 2): 0.5 × (472 − 290) = 91 a month.
      • $400 with two SSI children (size 1): 0.5 × (362 − 290) = 36 a month.
      • $330 (size 1): 0.5 × (362 − 239.25) = 61.375 a month.
      • $430: 0.5 × (472 − 311.75) = 80.125, so 961.50 a year.
      • 14 people with $692: 0.5 × (1,042 − 501.70) = 270.15, so 3,241.80 a year.
      • 14 people with $1,000: 0.5 × (1,042 − 725) = 158.50, so 1,902 a year.
      • Parent of five, $7,176: countable 5,202.60; reduced 8,376 − 5,202.60 = 3,173.40; eligible true (132.225 a month); nc_tanf 1,586.70.
      • Parent of two, $600 a month: 0.5 × (544 − 435) = 54.50 a month.
    • Partner fixtures:
      • 3,000 → 2,175, and 0.5 × (6,528 − 2,175) = 2,176.50.
      • Self-employment 2,400 → 1,740, and 0.5 × (5,664 − 1,740) = 1,962.
      • 8,176 → 5,927.60, and 0.5 × 600.40 = 300.20 ≥ 300.
      • 8,177 → 0.5 × 599.675 = 299.84 < 300, so 0.
      • tanf equals nc_tanf in all four.
  • PR body. "The 16 new or changed assertions fail" on main counts test cases outside the partner file: 5 earned-income cases, 2 income-eligibility cases and 9 integration cases. The two $25 controls ($50 and $49) give the same result on main, as the body implies by calling them controls. The issue's example ($1,586.70 against main's $0) is reproduced in the new integration case.
  • Source-conflict ratchet (Tie source-conflict markers to the register shared with Axiom #10287).
  • Changelog. changelog.d/fix-10210-nc-work-first-earnings.fixed.md is a top-level fragment with the fixed type.
  • Parameter. The description follows the house template, and the label, unit: /1 and three single-page references are in place.
  • Issue closure. Fixes #10210 is right: the issue asks for exactly the 27.5% exclusion, the children's-earnings exclusion and the $25 payment test.

Axiom line assessment

The line is missing (Should 3), and encoded-correct isn't available:

  • No module. rulespec-us us-nc/ holds only the FNS manual pages (policies/dhhs/fns/...), 10A NCAC 71U (FNS: .0210, .0212, .0215, .0302), CMS CHIP/Medicaid and income tax modules. programs/us-nc/ has only SNAP. There is no Work First module.
  • No issue. Searches of rulespec-us issues for "North Carolina Work First", "us-nc TANF", "Work First", "10210" and "10260" find no parity issue.

Recommended line. File a dispatch-ready pe-parity issue and use axiom: TheAxiomFoundation/rulespec-us#<n> queued. The issue should have:

  • Modules. Follow the us-ny/policies/otda/tanf-state-plan-2024-2026/... layout:

    • us-nc/policies/dhhs/work-first/tanf-state-plan-2026-2028/financial-rules for Financial Rules 3, 4, 6 and 8;
    • optionally us-nc/policies/dhhs/work-first/wf-114-income-and-budgeting for section I (income by member) and section III (payment calculation).
  • Corpus. Both sources are already ingested in axiom-corpus:

    • us-nc/policy/2026-09-13-tanf-state-plan (the FY2026-2028 plan, 75 pages);
    • us-nc/manual/2026-07-03-nc-work-first-manual, whose WF-114 PDF is byte-identical to the one the PR cites (sha256 d7327de7…).

    10A NCAC 71W isn't ingested yet: the NC regulation ingest is 10A NCAC 10. It can be added if the module should cite .0408 and .0601. The only blocker is approval of the billed encoder run.

  • Law. The verbatim Financial Rules 3, 4, 6 and 8 text (file pages 18-19), and WF-114 section III (file page 5).

  • Required outputs. Countable earned income (children's and SSI recipients' earnings excluded, then 27.5% off), the monthly payment (50% of the need standard less countable income), and payment issuance at $25 or more.

  • Divergence note. Until Should 2 is fixed, PolicyEngine counts SSI recipients' earnings.

  • Companion cases. Compute them by hand from the plan text, not from PolicyEngine output:

    • Need standard $698 (6 people) and $598 of monthly earnings give countable $433.55 and a $132.23 payment.
    • $600 of earnings with need standard $544 gives countable $435 and a $54.50 payment.
    • A computed payment of $25 is paid, and $24.50 is not.
    • A child's earnings are not counted.
    • An SSI-recipient parent's earnings are not counted.

CI status

All 30 checks pass at 8044240552 (gh pr checks 10260). The workflow run on that SHA was created 2026-10-23 18:44 UTC. That includes:

  • the 11 Baseline shards (incl. states shards 1-4);
  • the 9 Contrib shards;
  • Household API Partners;
  • PR validation (lock, guard tests, lint, changelog);
  • Microsimulation;
  • the two Full Suite - Rest jobs;
  • Package and compatibility;
  • Quick Feedback;
  • codecov patch and project (100%).

None is pending or failing. The "Partner API impact notice" workflow also ran and posted its Slack alert (see Should 1).

Branch status

The branch is 2 commits ahead of main (the change on 2026-10-16 and a merge of main on 2026-10-23) and 48 behind. git merge-tree --write-tree PolicyEngine/main 8044240552 merges cleanly.

  • Main hasn't changed any NC TANF file or the NC partner fixture since the merge base.
  • Main's only change to test_source_conflicts.py edits unrelated KNOWN_UNMARKED rows (CA premium subsidy, QBI, local sales tax).
  • No other open PR touches NC Work First files.

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 law is right in every year (the State Plans, Financial Rules 6 and 8, and 10A NCAC 71W .0601(a)), the 50% payment formula matches, and all 27 expectations hand-check. Three should-address items: (1) the PR edits partners/analytics_coverage/.../nc/tanf.yaml (4 fixtures, 2 inputs) without @hua7450 as reviewer, and under CLAUDE.md his approval of a head with those edits is needed before merge; (2) Financial Rules 3-4 and 71W .0408(a) exclude SSI recipients' income, but an SSI parent's earnings still count ($91 vs $236/month); (3) there's no axiom line. Four suggestions. Full report in the comment above.

State Plan Financial Rules 3 and 4, 10A NCAC 71W .0408(a)(1) and the
WF-114 income chart exclude all income of SSI recipients. The household
size already drops them; the earned and unearned income variables now
drop their income too, with the same ssi/receives_ssi check.

Rename the 18-year-old earnings test so that it describes the
PolicyEngine approximation, add SSI cases, cite 10A NCAC 71W .0601(a)
and the 2019-2022 State Plan, and fix the 2016-2019 payment anchor.

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

Copy link
Copy Markdown
Collaborator Author

Thanks @DTrim99. New head: 589543dd03 ("Exclude SSI recipients' income from North Carolina Work First"). Each item from the 2026-10-26 review and its disposition:

Should address

1. Partner contract test edits: review requested from @hua7450.

@hua7450, this PR edits policyengine_us/tests/policy/baseline/partners/analytics_coverage/edge_cases/state/nc/tanf.yaml. The changed cases (no change since head 8044240552; the new SSI commit moves no partner fixture, and all 32 NC partner cases in 10 files pass at 589543dd03):

Fixture Input change nc_tanf / tanf (annual)
tanf_mid_phaseout_earned_income (nc) none 1,764 → 2,176.50
tanf_self_employment_income_counted_as_earned (nc) none 1,632 → 1,962
tanf_income_at_min_benefit_boundary_eligible (nc) employment_income_before_lsr 5,628 → 8,176 450 → 300.20
tanf_income_just_above_min_benefit_boundary (nc) employment_income_before_lsr 5,629 → 8,177 0 → 0

The file's header comment also changes to describe the 27.5% exclusion and the $25 payment test.

  • Model change. nc_tanf_countable_earned_income now excludes 27.5% of gross earnings (State Plan FFY 2026-2028, Financial Rules 6, file page 18), and nc_tanf_income_eligible applies the $25 minimum to the payment, not to the reduced need standard per person (Financial Rules 8, file page 19; 10A NCAC 71W .0601(a)).
  • The first two fixtures rise because each dollar of earnings now cuts the benefit by $0.3625, not $0.50: 0.5 × (6,528 − 3,000 × 0.725) = 2,176.50 and 0.5 × (5,664 − 2,400 × 0.725) = 1,962.
  • The two cliff fixtures keep their purpose (one dollar on each side of the size-3 cliff), so their earnings move to the new cliff: 8,176 gives 0.5 × (6,528 − 5,927.60) = 300.20 ≥ 300, and 8,177 gives 299.84 < 300, so 0.
  • Partner impact. NC Work First amounts rise for every earner, and the size-3 cliff moves from $5,628 to $8,176 a year of earnings.

Before merge, Ziming, please answer:

  1. Are you sure you want to edit this test file?
  2. Have you notified a team member about this change?
  3. Have you notified the API partner about this change?

This PR will not merge until you approve a head that contains these edits.

2. SSI recipients' income: fixed with option (a).

  • nc_tanf_countable_earned_income now leaves out the earnings of SSI recipients, with the same (ssi > 0) | receives_ssi check that nc_tanf_household_size uses. The comment quotes Financial Rules 3 in full, and the variable cites 10A NCAC 71W .0408(a) (file page 7).
  • I also applied it to nc_tanf_countable_gross_unearned_income (unearned sources and child support), since Financial Rules 4 ("no income of SSI recipients is counted"), 71W .0408(a)(1) and the WF-114 chart ("Do not count any of their income") cover all of an SSI recipient's income.
  • New tests:
    • integration.yaml: your example (two parents and a child aged 5; parent A has receives_ssi: true and earns $400 a month) gives household size 2, countable earnings 0 and nc_tanf 236 × 12 = 2,832.
    • nc_tanf_countable_earned_income.yaml: the SSI parent's $4,800 is excluded and the other parent's $2,000 counts: 2,000 × 0.725 = 1,450.
    • nc_tanf_countable_gross_unearned_income.yaml: the SSI parent's $6,000 of Social Security and $1,000 of child support are excluded; the other parent's $1,200 pension and $500 of child support count: 1,700.
  • (b) The 18-year-old case is renamed "Earnings of an 18-year-old not in high school are counted (PolicyEngine counts every non-SSI member who is not a child)". Its comment says that this approximates the rule and cites 71W .0408(a)(1) and Non-Financial Eligibility Rules 1.
  • Declined here: treating 18-year-olds in high school as children. The same age test drives nc_demographic_tanf_eligible, so the change belongs with the demographic rule. It is item 2 of the follow-up issue North Carolina Work First: non-parent adults, 18-year-olds in high school, and all-SSI children #10492.

No partner fixture changes because of this commit.

3. axiom: line: added.

I filed TheAxiomFoundation/rulespec-us#1947 (pe-parity). It has the module path (us-nc/policies/dhhs/work-first/tanf-state-plan-2026-2028/financial-rules.yaml), the corpus citations (us-nc/policy/2026-09-13-tanf-state-plan; us-nc/manual/2026-07-03-nc-work-first-manual; 10A NCAC 71W is not ingested), the verbatim Financial Rules 3, 4, 5, 6 and 8 and Non-Financial Eligibility Rules 1 and 2, the required outputs, and nine hand-computed companion cases. The blocker is approval of the billed encoder run. The body now ends with axiom: TheAxiomFoundation/rulespec-us#1947 queued.

Suggestions

  1. References: done. average_reduced_need_standard_threshold.yaml cites 10A NCAC 71W .0601(a) (file page 10), and earned_income_exclusion_rate.yaml cites the ACF-approved 2019-2022 plan, Financial Rules 6 (file page 16). I checked both pages.
  2. WF-114 "difference" wording: no change. I added no comment about it, so there is nothing for the source-conflict ratchet to count. The rulespec-us issue records the reading under "Interpretations and gaps".
  3. Rename average_reduced_need_standard_threshold: declined in this PR. A rename changes a public parameter path, so it should be its own change. The label and description already say "minimum benefit".
  4. Pre-existing items:

Tests at 589543dd03 (local)

  • gov/states/nc (test_batched, per-subdir): 400 passed, including 59 in gov/states/nc/ncdhhs/tanf.
  • NC partner fixtures (10 files, read-only): 32 passed.
  • gov/hhs/tanf/cash: 36 passed.
  • tests/code_health, test_parameter_files.py, test_parameter_scale_thresholds.py: 450 passed, 1 skipped.

Re-requesting review from @DTrim99 and requesting review from @hua7450.

@DTrim99

DTrim99 commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 2): Apply North Carolina Work First's 27.5% earnings exclusion and $25 minimum payment

Summary

Round 1 (head 8044240552, 2026-10-26) asked for three changes. The new commit 589543dd03 ("Exclude SSI recipients' income from North Carolina Work First") deals with all three:

  • Partner gate (Should 1). Pavel requested @hua7450's review and posted the changed-case table, the model change, the law and the three questions. Ziming hasn't answered yet, so the merge still waits on his approval. That is a merge gate, not an open item for the author.
  • SSI recipients (Should 2).
    • Earnings are now excluded with the same (ssi > 0) | receives_ssi check that the household size uses.
    • The exclusion now also covers unearned income.
    • The 18-year-old test is renamed so it describes the PolicyEngine approximation.
  • Axiom line (Should 3). The line is added. rulespec-us#1947 is OPEN, labelled pe-parity, dispatch-ready, and matches the head.
  • Suggestions.

One new problem came in with the unearned-income change, and it is critical. The commit also drops child support that an SSI-recipient parent receives. North Carolina treats child support as income of the dependent child, so support that an SSI parent receives for a child in the unit still counts.

  • Main counted that support, and this head doesn't.
  • The new unearned-income test pins the wrong value: 1,700 where the rule gives 2,700.
  • My round-1 suggestion to extend the SSI exclusion to unearned income didn't carve out child support, so part of this traces back to that suggestion.

CI has 34 passing checks and 2 failing. Both failures come from the main-caused Montana AGI test, which #10496 fixes. The merge with main is clean.

Prior findings

Finding Status Evidence
Should 1. The partner contract test edits need Ziming Hua's three-question sign-off. Done on the author's side. The merge is still gated. hua7450 is now a requested reviewer. Pavel's comment (2026-10-27 15:04 UTC) names all four fixtures with old and new values, cites Financial Rules 6 and 8 and 10A NCAC 71W .0601(a), states the partner impact and asks the three questions. The fixture values haven't changed since round 1, and I re-checked them (below). Ziming hasn't replied. Under CLAUDE.md, the PR can't merge until he approves a head that contains the edits. I don't count this as open against the author.
Should 2(a). SSI recipients' earnings were counted. Fixed. nc_tanf_countable_earned_income.py:23-25 sets is_ssi_recipient = (person("ssi", period) > 0) | person("receives_ssi", period) and counted = ~is_child & ~is_ssi_recipient. The round-1 example is now an integration case: household size 2, countable earnings 0, and nc_tanf 236 × 12. A second case excludes the SSI parent's $4,800 and counts the other parent's $2,000 (1,450).
Should 2(a), the optional extension to unearned income Fixed for the recipient's own income, but it goes too far for child support. The SSI recipient's own unearned income (for example, Social Security) is now excluded, which is right. Child support that the SSI recipient receives is also excluded, which is wrong. See New Critical 1.
Should 2(b). The 18-year-old test presented an approximation as the rule. Fixed. The optional part was declined, and the reason holds. The test is renamed "Earnings of an 18-year-old not in high school are counted (PolicyEngine counts every non-SSI member who is not a child)". Its comment now cites 71W .0408(a)(1) and Non-Financial Eligibility Rules 1. Treating 18-year-olds in high school as children was optional in round 1, and the author declined it. The same age test drives nc_demographic_tanf_eligible, so the change belongs with that rule. It is item 2 of #10492.
Should 3. The axiom: line was missing. Fixed. The body ends with axiom: TheAxiomFoundation/rulespec-us#1947 queued, and the issue meets every requirement (see "Axiom line assessment").
Suggestion 1. Cite 71W .0601(a) and the 2019-2022 plan. Fixed. Both pages are checked (see "Verified correct").
Suggestion 2. The WF-114 "difference" wording Handled as advised. The author added no comment, so the ratchet has nothing to count. The reading is recorded under "Interpretations and gaps" in rulespec-us#1947.
Suggestion 3. Rename average_reduced_need_standard_threshold Declined, with a reason. A rename changes a public parameter path, so it should be its own change. That is reasonable, and the suggestion was optional.
Suggestion 4. Pre-existing items Fixed or filed. The 2016-2019 anchor in payment_percentage.yaml is now #page=34, which shows "50% of the difference". #10492 is OPEN and covers non-parent adults, 18-year-olds in high school, and a parent whose children all receive SSI.

New findings

Critical (must fix)

1. This head no longer counts child support that an SSI-recipient parent receives for a child in the unit.

nc_tanf_countable_gross_unearned_income.py:25-29 zeroes child_support_received for every SSI recipient:

child_support = person("child_support_received", period)
...
countable_child_support = spm_unit.sum(
    where(is_ssi_recipient, 0, child_support)
)

The new test nc_tanf_countable_gross_unearned_income.yaml:110-136 pins this. The SSI parent receives $1,000 of child support, a child aged 5 is in the unit, and the expected value is 1,700.

North Carolina counts child support as the child's income, not the income of the parent who receives it:

  • WF-114 section XII (file page 45): "Child support is considered unearned income for the dependent child." Support counts as the participant's own income only "for an adult child (over age 18) or a child who no longer lives in the household".
  • 10A NCAC 71W .0408 (file page 7):
    • "(a)(2) Countable net unearned income of a child who is included in the assistance unit shall be counted."
    • (d) lists "(7) support payments and contributions" as countable unearned income.
    • (e)(3) excludes only "child support being routed to IV-D Accounting", which is the existing is_tanf_enrolled branch.
  • State Plan FFY 2026-2028 (file page 35): "In lieu of counting child support as unearned income for dependent child(ren) included in the Work First Cash Assistance budget unit, the state may opt to implement a temporary pass through ..."

These sources exclude the SSI recipient's own income:

  • Financial Rules 3 excludes "the income of SSI recipients".
  • Financial Rules 4 says "no income of SSI recipients is counted".
  • 10A NCAC 71W .0408(a)(1) excludes a parent who "receives SSI".
  • The WF-114 section I chart says "Do not count any of their income".

Child support paid for a non-SSI child in the unit is that child's income, and (a)(2) counts it. No source conflict is involved, because all three sources agree.

child_support_received is a person-level input. The CPS-based data fill it from the person-level CPS item CSP_VAL, which the adult who receives the support reports. So the exclusion hits the usual case: a custodial parent on SSI who receives support for the children.

Example: 2024, a unit not enrolled in TANF. A single parent receives SSI and has one child aged 5. The parent receives $300 a month of child support.

Household size Countable unearned Payment
Rule (WF-114 XII; 71W .0408(a)(2)) 1 $300 a month 0.5 × (362 − 300) = $31 a month ($372 a year)
Main 1 $300 a month $31 a month
This head 1 $0 0.5 × 362 = $181 a month ($2,172 a year)

This head pays $1,800 a year too much, and main gets it right. In the PR's own test, the rule gives 1,200 + 500 + 1,000 = 2,700.

Fix:

  • Code. Count child support whoever receives it, as main does. Alternatively, exclude it only when an SSI-recipient child receives it, since that is the one case PolicyEngine can identify where the support is the SSI recipient's own income:
    p = parameters(period).gov.states.nc.ncdhhs.tanf
    person = spm_unit.members
    is_ssi_recipient = (person("ssi", period) > 0) | person("receives_ssi", period)
    gross_unearned = add(person, period, p.income.unearned)
    countable_unearned = spm_unit.sum(where(is_ssi_recipient, 0, gross_unearned))
    # WF-114 section XII: child support is unearned income of the dependent child.
    is_child = person("age", period) < p.eligibility.age_limit
    child_support = person("child_support_received", period)
    countable_child_support = spm_unit.sum(
        where(is_ssi_recipient & is_child, 0, child_support)
    )
  • Comment wording. Cite "WF-114 section XII" plainly, as above. Avoid phrasing such as "the manual says" or "conflicts with", which the source-conflict ratchet counts.
  • Test. Change the expectation to 2,700 and rename the case, for example "An SSI recipient's unearned income is not counted; child support they receive for a child in the unit is."
  • New integration case. Use the example above: nc_tanf_household_size: 1, nc_tanf_countable_gross_unearned_income: 3_600, nc_tanf: 31 * 12. Optionally add a case where an SSI-recipient child receives child support, which is excluded.
  • PR body. Update the bullet for the unearned-income test.
  • rulespec-us#1947.
    • Update the nc_tanf_countable_gross_unearned_income bullet ("plus their child support when the unit is not enrolled") and the "Countable unearned income" output row.
    • Add a companion case from WF-114 XII and Financial Rules 8: an SSI parent and a child aged 5 with $300 of child support get a $31 payment.

Should address

None.

Suggestions

  1. Cite the approved 2019-2022 plan in payment_percentage.yaml. It still cites the 2019-2022 plan through the draft URL (.../2019-2022-tanf-state-plan-draft/download#page=18). That page does show the 50% text. The PR now cites the ACF-approved 2019-2022 plan in earned_income_exclusion_rate.yaml, and that plan has Financial Rules 8 on file page 16. Switching would put both files on the approved plan. This is optional, since that line predates the PR.

Verified correct

  • SSI earnings exclusion.

    • It matches Financial Rules 3 and 4 (file page 18), 71W .0408(a)(1) and the WF-114 section I chart (file page 2).
    • It uses the same (ssi > 0) | receives_ssi test as nc_tanf_household_size, so the member dropped from the need standard is the member whose income is dropped.
    • It adds no new dependency, because the household size already reads ssi in the same chain.
  • The SSI recipient's own unearned income (Social Security, pensions and so on) is now excluded. That's right: the WF-114 chart says "Do not count any of their income" for SSI-recipient parents and children.

  • Entity change. Every source in income/unearned.yaml and income/earned.yaml is a Person variable. The switch from add(spm_unit, …) to add(person, …) with spm_unit.sum(where(...)) is therefore safe and vectorized.

  • References. I checked every new or changed anchor in the downloaded PDFs:

    • 2019-2022 ACF-approved plan, file page 16: "27.5% of gross earned income is excluded" (Financial Rules 6).
    • 10A NCAC 71W, file page 10: .0601 "shall not be made for less than twenty five dollars ($25.00)".
    • 10A NCAC 71W, file page 7: .0408(a)(1) and (a)(2). Both variables now cite this page.
    • 2016-2019 plan, file page 34: "The Work First monthly payment is 50% of the difference ...".
    • No title carries a page number.
  • Rounding. No source sets a rounding rule. I searched the FFY 2026-2028 plan, WF-114 and 10A NCAC 71W for "round", "cents" and "nearest". So the unrounded expectations, such as $83.025 and $61.375 a month, are consistent with the sources.

  • Hand-checked expectations. These are every new or changed expectation since round 1, plus the $25 boundary and 27.5% cases. Need standards are $362 / $472 / $544 / $698 for sizes 1 / 2 / 3 / 6.

    Case Hand computation Expected OK
    Integration: two parents and a child; the parent with $400 a month of earnings receives SSI Size 2 (parent B and the child). Countable earnings 0. 0.5 × 472 = 236 a month size 2, earnings 0, nc_tanf 2,832 yes
    Earned: SSI parent's $4,800 and other parent's $2,000 2,000 × 0.725 1,450 yes
    Earned: 18-year-old's $3,000 (renamed) 3,000 × 0.725 2,175 yes (the name is now accurate)
    Unearned: SSI parent's $6,000 Social Security and $1,000 child support; other parent's $1,200 pension and $500 child support Rule: 1,200 + 500 + 1,000 = 2,700 1,700 no (Critical 1)
    Eligibility: $100 reduced, size 6 (above) 0.5 × 100 = 50 ≥ 25 true yes
    Eligibility: $40 reduced, size 1 20 < 25 false yes
    Eligibility: $50 reduced (at $25) 25.00 ≥ 25 true yes
    Eligibility: $49 reduced (just below) 24.50 < 25 false yes
    Partner: earnings 8,176 (just above the cliff) 8,176 × 0.725 = 5,927.60; 0.5 × (6,528 − 5,927.60) = 300.20 ≥ 300 300.20 yes
    Partner: earnings 8,177 (just below the cliff) 8,177 × 0.725 = 5,928.325; 0.5 × 599.675 = 299.84 < 300 0 yes
    Earned: $1,000 and $500 × 0.725 725, 362.50 yes
    Integration: parent of five, $7,176 a year 5,202.60 countable; 0.5 × (8,376 − 5,202.60) 1,586.70 yes
    Integration: parent of two, $600 a month 435 countable; 0.5 × (544 − 435) 54.50 a month yes
    • The eligibility boundary tests take the reduced need standard as an input, and 600 × 0.5 = 300 is exact in binary.
    • The partner cliff cases have margins of $0.20 and $0.16, far above floating-point error.
    • The other round-1 expectations are unchanged at this head.
  • Changelog. The fragment is still a top-level fixed file, and it now mentions SSI recipients' income.

  • PR body. "previous head: $91; main: $36" for the SSI case is right:

    • Previous head: 0.5 × (472 − 290) = 91.
    • Main: 0.5 × (472 − 400) = 36, which passes main's per-person test (72 / 2 ≥ 25).
  • Partner fixtures. No NC partner file uses receives_ssi or child_support_received. The SSI commit moves no fixture, and the Critical 1 fix shouldn't either.

  • Source-conflict ratchet (Tie source-conflict markers to the register shared with Axiom #10287) on the merged tree.

Axiom line assessment

Valid. The line is axiom: TheAxiomFoundation/rulespec-us#1947 queued.

  • State. The issue is OPEN, labelled pe-parity, and was created 2026-10-27 15:02 UTC.
  • Dispatch-ready (the rulespec-us#1467 model):
    • Module path: us-nc/policies/dhhs/work-first/tanf-state-plan-2026-2028/financial-rules.yaml, with optional WF-114 and 71W modules.
    • Corpus citations: us-nc/policy/2026-09-13-tanf-state-plan (locked) and us-nc/manual/2026-07-03-nc-work-first-manual, byte-identical to the WF-114 PDF the PR cites. The issue says 71W isn't ingested.
    • Verbatim law: Financial Rules 3-6 and 8, Non-Financial Eligibility Rules 1 and 2, 71W .0408(a) and .0601(a).
    • Required outputs: a table of the outputs.
    • Companion cases: nine, computed by hand from the plan text, not from PolicyEngine output. I re-checked cases 1-5, 7 and 9: 132.225, 54.50, 25.00 (paid), 24.50 (not paid), 236, 163.50 and 0.
    • Blocker: approval of the billed encoder run.
  • queued is right. rulespec-us has no NC Work First module.
  • Match with the head. The issue matches head 589543dd03, including the child-support handling in Critical 1. It also notes that the corpus copy of the plan has a different sha256 from today's download, and it tells the encoder to confirm pages against the corpus copy, which is right.
  • After the fix. Update the unearned-income bullet and output row and add the child-support companion case, as listed under Critical 1.

CI status

At 589543dd03, gh pr checks 10260 shows 34 passing checks, 2 failing and none pending. I re-checked near the end of the review. The workflow run was created 2026-10-27 15:00 UTC on this head.

This PR doesn't touch Montana AGI, so this is a CI note, not a finding. Merge main and re-run once #10496 lands.

All the jobs that exercise this PR pass: Baseline states shards 1-4, Household API Partners, PR validation (lock, guard tests, lint, changelog), Quick Feedback, Microsimulation, and codecov patch and project.

Branch status

The branch is 3 commits ahead of main (the original change, a merge of main on 2026-10-23, and the SSI commit) and 70 behind.

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.

Requesting changes, round 2: 1 critical, 0 should. All round-1 items are handled. The new commit also stops counting child support an SSI-recipient parent receives for a child in the unit, which WF-114 XII and 10A NCAC 71W .0408(a)(2) count as the child's income ($1,800/yr overpayment in the report's example; the new test pins 1,700 where the rule gives 2,700). The partner-test edits still need Ziming's approval before merge. Full report in the comment above.

WF-114 section XII and 10A NCAC 71W .0408(a)(2) count child support as
the dependent child's income. The SSI exclusion now removes only the SSI
recipient's own unearned income, and child support only when an
SSI-recipient child receives it. The unearned test now expects 2,700,
and new cases pin the SSI-parent and SSI-child child-support paths.

Also cite the ACF-approved 2019-2022 State Plan in payment_percentage.

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

Copy link
Copy Markdown
Collaborator Author

Thanks @DTrim99. Round-2 fixes are in c711e7034b ("Count child support an SSI-recipient parent receives for a child").

Critical 1, child support received by an SSI-recipient parent: fixed.

  • nc_tanf_countable_gross_unearned_income still excludes the SSI recipient's own unearned income (Financial Rules 3 and 4). Child support is now excluded only when an SSI-recipient child receives it. Support that an SSI-recipient parent receives counts as the dependent child's income (WF-114 section XII, file page 45; 10A NCAC 71W .0408(a)(2)). The variable cites WF-114 pages 2 and 45.
  • nc_tanf_countable_gross_unearned_income.yaml: the case is renamed "An SSI recipient's unearned income is not counted; child support they receive for a child in the unit is." and now expects 2,700 (1,200 + 500 + 1,000). A new case pins the other path: child support that an SSI-recipient child receives is excluded (1,200 + 600 = 1,800).
  • integration.yaml: your example (a parent on SSI, a child aged 5, $300 a month of child support, not enrolled) gives nc_tanf_household_size 1, nc_tanf_countable_gross_unearned_income 3,600 and nc_tanf 31 × 12.
  • The PR body's unearned-income bullet is updated. Encode North Carolina Work First countable income and payment (State Plan Financial Rules 3, 4, 6, 8) (PE parity, policyengine-us#10260) TheAxiomFoundation/rulespec-us#1947 now has the WF-114 XII text, the corrected unearned-income bullet and output row, and companion case 10 ($31 payment).

Suggestion 1: done. payment_percentage.yaml now cites the ACF-approved 2019-2022 plan, Financial Rules 8 (file page 16). I checked the page.

Partner fixtures. This commit changes no partner file, and all 32 cases in the 10 NC partner files pass at c711e7034b. The merge still waits for @hua7450's approval of the four fixture edits.

Local tests at c711e7034b: gov/states/nc/ncdhhs/tanf 61 passed; gov/states/nc (test_batched, per-subdir) both batches passed; NC partner files 32 passed; test_source_conflicts.py, the reference-URL and parameter-file checks 225 passed.

@DTrim99

DTrim99 commented Oct 27, 2026

Copy link
Copy Markdown
Collaborator

PR Review (round 3): Apply North Carolina Work First's 27.5% earnings exclusion and $25 minimum payment

Summary

Round 2 (head 589543dd03, 2026-10-27) had one critical finding: the SSI-recipient exclusion also dropped child support that an SSI-recipient parent receives for a child in the unit. The new commit c711e7034b ("Count child support an SSI-recipient parent receives for a child") fixes it.

  • Code. nc_tanf_countable_gross_unearned_income still excludes the SSI recipient's own unearned income. Child support is now excluded only when an SSI-recipient child (under 18) receives it, so support an SSI parent receives counts as the dependent child's income.
  • Tests. The unearned case now expects 2,700 and has a new name that describes it. A new unit case covers support that an SSI-recipient child receives (excluded, 1,800). The round-2 example is a new integration case (household size 1, countable unearned 3,600, nc_tanf 31 × 12). I hand-checked all three.
  • Suggestion 1 (cite the approved 2019-2022 plan in payment_percentage.yaml) is done, and I checked the page.
  • PR body and rulespec-us#1947 both match the head.

No new critical or should findings. One optional suggestion: record how child support is attributed to members.

CI at the last check: 27 passing, 2 failing and 7 still pending (the Contrib shards and Microsimulation). Neither failure comes from this PR. policyengine-core 3.33.13, released today, breaks a Social Security test in the Rest Python shard 3 job, and main's own push run fails the same test. The Montana AGI failure from round 2 is gone, because #10496 has merged and CI tests the PR merged with main. Every job that runs the NC Work First tests passed.

The merge with main is clean, and main hasn't changed any NC Work First file since the merge base. Merge gate: the PR still edits the NC partner fixtures, and @hua7450 hasn't reviewed yet. Under CLAUDE.md the PR can't merge until he approves a head that contains those edits. This approval doesn't clear that gate.

Prior findings

Finding Status Evidence
Round 2, Critical 1. Child support an SSI-recipient parent receives for a child in the unit was excluded; the test pinned 1,700 where the rule gives 2,700. Fixed. nc_tanf_countable_gross_unearned_income.py:29-34 now has is_child = person("age", period) < p.eligibility.age_limit and where(is_ssi_recipient & is_child, 0, child_support). The SSI recipient's other unearned sources are still dropped (where(is_ssi_recipient, 0, gross_unearned)). This matches WF-114 section XII (file page 45): "Child support is considered unearned income for the dependent child", and 10A NCAC 71W .0408(a)(2). The test expects 2,700 and is renamed "An SSI recipient's unearned income is not counted; child support they receive for a child in the unit is." The round-2 example is now an integration case.
Round 2, Suggestion 1. Cite the ACF-approved 2019-2022 plan in payment_percentage.yaml. Fixed. The href is now .../acf-approved-2019-2022-tanf-state-plan/download#page=16. I downloaded the PDF: file page 16 has Financial Rules 8, "The Work First monthly payment is 50% of the difference between the total countable income and the need standard."
Round 1, Should 1. The partner fixture edits need Ziming Hua's three-question sign-off. Done on the author's side. The merge is still gated. hua7450 is still a requested reviewer and hasn't submitted a review or replied. This commit changes no partner file (git diff 589543dd03 c711e7034b touches four non-partner files), and the Quick Feedback job ran partners/.../state/nc/tanf.yaml green at this head (7 passed). This is a merge gate, not an author finding.
Round 1, Should 2 and 3; other round-1 suggestions Fixed or declined with a reason (round 2). Unchanged since round 2. The SSI earnings exclusion, the renamed 18-year-old case, the axiom line, the references, and the #10492 follow-up all stand.

New findings

Critical (must fix)

None.

Should address

None.

Suggestions

  1. Say how child support is attributed. child_support_received sits on the member who receives the support, but WF-114 section XII (file page 45) follows the child it is paid for. The new rule (an SSI-recipient child's support is excluded, an SSI adult's is counted) is the split I proposed in round 2, and it gets the common cases right. Three rare cases still fall outside it:

    • A non-SSI parent receives support for a child on SSI. WF-114 XII: "the income of non-assistance unit members is not countable income". The code counts it, as main does.
    • An SSI parent receives support "for an adult child (over age 18) or a child who no longer lives in the household". WF-114 XII makes that "countable unearned income to the participant", which is the SSI recipient's own income, so it is excluded. The code counts it, as main does.
    • An SSI-recipient parent under 18 receives support for her own child. The rule counts it as the baby's income. The code excludes it.

    In household inputs, entering the support on the child it is paid for gives the right answer in the first and third cases. A sentence in the code comment, or a line under "Interpretations and gaps" in rulespec-us#1947, would record this. Optional.

Verified correct

  • The fix matches the law.

    • WF-114 section XII (file page 45, checked in the downloaded PDF): "Child support is considered unearned income for the dependent child."
    • 10A NCAC 71W .0408(a)(2): "Countable net unearned income of a child who is included in the assistance unit shall be counted."
    • Financial Rules 3 and 4 and 71W .0408(a)(1) still exclude the SSI recipient's own income, and the code still drops every income/unearned.yaml source for SSI recipients.
    • The IV-D routing exclusion (71W .0408(e)(3)) is still the is_tanf_enrolled branch, now with its own comment.
  • The code is sound.

    • is_child uses the same age < gov.states.nc.ncdhhs.tanf.eligibility.age_limit (18) test as nc_tanf_countable_earned_income and nc_demographic_tanf_eligible, so all three variables draw the child line in the same place.
    • p now points at gov.states.nc.ncdhhs.tanf, and both uses were updated (p.income.unearned, p.eligibility.age_limit).
    • It is vectorized (where on person arrays, then spm_unit.sum) and adds no new dependency beyond age.
    • The WF-114 href carries a # PDF pages 2, 45 comment, as CLAUDE.md asks for a multi-page variable reference.
  • Hand-checked expectations. These are every new or changed expectation since round 2. All amounts are annual unless marked. The need standard for one person is $362 a month.

    Case Hand computation Expected OK
    Unearned (renamed): SSI parent has $6,000 Social Security and $1,000 child support; other parent has $1,200 pension and $500 child support; child 5; not enrolled SSI parent's Social Security excluded. The parent is 35, so is_child is false and the $1,000 counts. 1,200 + 500 + 1,000 2,700 yes
    Unearned (new): parent $1,200 pension; SSI child aged 8 with $2,400 support; child 5 with $600 support; not enrolled SSI child: is_ssi_recipient & is_child, so the $2,400 is excluded. 1,200 + 600 1,800 yes
    Integration (new): SSI parent with $300 a month support, child 5, not enrolled Size 1 (the child) nc_tanf_household_size 1 yes
    same 300 × 12; the parent is not a child, so the support counts countable unearned 3,600 yes
    same Need 362 × 12 = 4,344; 4,344 − 3,600 = 744; 0.5 × 744 = 372. 372 ≥ 25 × 12 = 300, so it is paid. A child is present and size 1 > 0, so the unit is demographically eligible nc_tanf 372 = 31 × 12 yes
    • The integration result equals main's ($31 a month): main counted the support, and its per-person test passes (62 / 1 ≥ 25). The previous head paid $181 a month.
    • The $25 margin is $72 a year, so there's no floating-point risk.
    • All other expectations are unchanged since round 2, where I checked them.
  • Test names. The renamed unearned case and the new SSI-child case describe what they test. The integration case gives the arithmetic in comments.

  • Reference. payment_percentage.yaml now cites the ACF-approved 2019-2022 plan at file page 16, which shows Financial Rules 8 (see the table above). Its title has no page text.

  • Source-conflict ratchet (Tie source-conflict markers to the register shared with Axiom #10287). I ran main's CONFLICT_COMMENT_RE over every comment in the NC Work First variables, parameters and tests at this head, and nothing matched. The new comments ("WF-114 section XII: child support is unearned income of the dependent child ...", "Child support counts only if the unit is not currently enrolled.") use none of the counted phrasings. No NC file is in KNOWN_UNMARKED. The PR validation job (guard tests) passed on this head's run, created 2026-10-27 17:09 UTC.

  • PR body.

    • The summary's item 2 now says child support still counts when an SSI-recipient parent receives it, citing WF-114 XII and 71W .0408(a)(2), and that only an SSI-recipient child's own support is excluded.
    • The unearned test bullet gives $2,700 and $1,800.
    • The integration bullet adds the $31 case ("main: $31"), which is correct.
    • "gov/states/nc passes (402 tests)" matches the Quick Feedback run at this head (402 passed).
  • Changelog. The fragment is unchanged: a top-level fixed file that mentions SSI recipients' income. That is still accurate.

  • Partner fixtures. This commit changes none, and no NC partner fixture uses receives_ssi or child_support_received.

Axiom line assessment

Valid. The line is axiom: TheAxiomFoundation/rulespec-us#1947 queued.

  • State. The issue is OPEN and labelled pe-parity. It was last updated 2026-10-27 17:10 UTC.
  • It matches head c711e7034b (the head it names):
    • The nc_tanf_countable_gross_unearned_income bullet now reads "Child support is excluded only when an SSI-recipient child receives it; support that an SSI-recipient parent receives counts as the dependent child's income (WF-114 section XII)."
    • The "Countable unearned income" output row adds "child support received for a child in the unit, including support that an SSI-recipient parent receives (Financial Rules 3, 4, 5; WF-114 section XII; 71W .0408(a)(2))".
    • The verbatim law now quotes WF-114 section XII.
    • The pasteable review_finding carries the same rule.
  • Dispatch-ready. It still has the module path, the corpus citations (the WF-114 corpus copy is byte-identical to the cited PDF), the verbatim law, the required outputs, and the blocker (approval of the billed encoder run).
  • Companion case 10 is "Parent receives SSI and $300 of child support for a child aged 5": number on Work First 1, countable 300.00, payment 0.5 × (362 − 300) = 31.00. It is computed by hand from the source text, and I re-checked it.
  • Optional. The attribution note in Suggestion 1 would fit under "Interpretations and gaps".

CI status

At c711e7034b, my last check of gh pr checks 10260 (2026-10-27 17:50 UTC) showed 27 passing, 2 failing and 7 pending. The workflow run tests the PR merged into main 4c65c82191.

  • Full Suite - Rest (Python shard 3) fails one test: policyengine_us/tests/core/test_social_security_input_state_machine.py::TestSocialSecurityInputStateMachine::runTest, with NameError: name 'operator' is not defined (1 failed, 1,945 passed).
    • Cause. The job upgrades policyengine-core from 3.33.2 to 3.33.13 (released today, from policyengine-core#624, merged 16:42 UTC). That release's SimulationBuilder.build_default_simulation calls operator.index(count). The test's helper, tests/core/_social_security_state_backend.py, recompiles that method into its own namespace, which doesn't import operator.
    • It's main-side. Main's own push run at 4c65c82191 (run 40546990445) fails the same test with the same error. This PR touches no Social Security or core code.
    • So this is a CI note, not a finding. Re-run once main has a fix; I found no open fix PR yet.
  • Check Rest Python shards. This aggregator fails only because shard 3 failed.
  • Montana AGI. The round-2 failure (test_mt_state_agi_invariants) is gone: Check Montana's pension exemption in the Montana AGI pooling test #10496 merged 2026-10-27 16:47 UTC, CI tests the merged tree, and test_drawn_batches passed in shard 3.
  • Still pending: Contrib states shards 1-4, Contrib other-shard-1 and other-shard-2b, and Microsimulation. This PR touches no contrib reform.
  • Passing jobs that exercise this PR:
    • Baseline states shards 1-4.
    • Household API Partners.
    • PR validation (lock, guard tests, lint, changelog).
    • Quick Feedback. It ran gov/states/nc (402 passed), each changed NC Work First test file, and the NC partner file (7 passed).
    • codecov patch and project.

Branch status

The branch is 4 commits ahead of main (the original change, a merge of main on 2026-10-23, the round-2 SSI commit and this commit) and 80 behind.

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.

Approving round 3: 0 critical, 0 should. The child support fix is correct. Full report in the comment above. Merge gate: the partner fixture edits still need @hua7450's approval of a head that contains them; this approval doesn't clear that. CI note: the Rest shard 3 failure (test_social_security_input_state_machine, NameError 'operator' under policyengine-core 3.33.13) also fails on main's own push run.

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

Approved at c711e7034b for the fixes in #10210. The 27.5% earnings exclusion, exclusion of ordinary dependent children's earnings, and $25 minimum applied to the calculated family payment correctly address the reported problems. The SSI-income exclusion and latest child-support correction also handle the reviewed ordinary cases correctly.

Validation: all 40 existing tests in the four changed non-partner NC test files passed locally. Both original issue examples match the expected results: $1,586.70/year for the parent with five children and $7,176 annual earnings, and $54.50/month for the parent with two children and $600 monthly earnings.

The broader child-status and assistance-unit cases belong in the #10492 follow-up. Please explicitly include emancipated minor parents: the new age-only filter exempts their earnings, although Work First treats them as adults. This newly affected case is being deferred to that follow-up as discussed.

The partner-test communication requirements remain as documented in the existing PR discussion.

@PavelMakarchuk
PavelMakarchuk merged commit b21b894 into main Oct 27, 2026
36 checks passed
@PavelMakarchuk
PavelMakarchuk deleted the fix-10210-nc-work-first-earnings branch October 27, 2026 23:37
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.

North Carolina Work First counts all earnings (no 27.5% exclusion) and applies the $25 minimum per household member

3 participants