Skip to content

Guard against person variables that add group variables (every member gets the whole group amount) - #9897

Merged
MaxGhenis merged 3 commits into
mainfrom
code-health-person-adds-group-projection
Oct 10, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
code-health-person-adds-group-projection

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds policyengine_us/tests/code_health/test_no_group_variables_in_person_adds.py to reject unreviewed group variables in a Person variable's adds or subtracts list. This is a code-health guard; it changes no policy calculation.

Review round

Merged main at 05907577140ce06f38b45777a05f479181161164 with a plain merge. Removed three stale known violations already fixed by Mississippi HSA #9865, West Virginia government interest #9856, and Ohio medical expenses #9925. Seven pairs in five variables remain: 10 - 3 = 7 pairs; 8 - 3 = 5 variables.

Narrowed the three boolean exceptions to their 11 existing reviewed (person variable, attribute, group variable) triples. New sources and all subtraction edges require review. Added the Kansas SNAP-subtraction regression and bounded Hypothesis coverage for registry, history, and projection invariants. The original merge-order discussion and open-PR census have been replaced by this verified merge snapshot.

Mechanism and sources

Verified the installed policyengine-core 3.32.15 implementation against the tagged source (the two files' SHA-256 hashes match):

  • Without a formula for the period, _run_formula calculates listed variables with map_to=variable.entity.key.
  • map_result projects a group amount unchanged to each member. Three members each receiving 100 total 100 + 100 + 100 = 300. Person-to-group mapping sums; mapping between different groups splits across members before summing.
  • The add() formula helper rejects person-from-group aggregation, while adds and subtracts reach automatic projection.
  • _cast_formula_result casts the numeric result to the target's dtype. For a boolean flag, 100 - 100 = 0 becomes false.

No statute, regulation, or form determines static exception granularity or whether a recorded source edge still exists. Those findings are decided from the merged model and Core's implementation. The existing Kansas participation criterion is documented in the Kansas CSFP State Plan, PDF page 4, and 7 CFR 247.9(b)(1) and (b)(3) address adjunctive income eligibility. This PR retains that calculation and its existing attribution interpretation.

What the guard checks

It scans the shared baseline system, rather than constructing another US model. Both literal lists and every dated value of a parameter-backed list are checked. A removed historical source remains detectable at its earlier dates; parameters and group targets are skipped. A path to a parameter folder fails explicitly.

ALLOWED contains only these positive receipt edges, with a reason for each:

  • Kansas CSFP categorical flag: snap, receives_snap, fdpir.
  • MBTA applicable-program enrollment flag: ma_eaedc, ma_tafdc, snap, receives_snap.
  • DART program enrollment flag: snap, receives_snap, tanf, receives_tanf.

KNOWN_VIOLATIONS records seven existing defects in five variables. Fixing a defect makes its precise entry stale and requires deleting it. This is the same maintenance approach as the dead-URL guard.

Invariants

  • Every found (Person variable, attribute, group source) triple is explicitly reviewed or recorded as a known violation.
  • Reviewed and known registries are disjoint, and every recorded triple remains live. Deleting one reviewed source makes that exact entry stale even if another source on the same variable remains.
  • Every reviewed target is a boolean Person variable, and every reviewed attribute is adds. Adding a new source or a subtraction on an allowed name is rejected.
  • Scanning preserves the union of group sources across all parameter-history values, with sorted, unique dates. It excludes Person sources, parameter names, and group targets.
  • Core projection repeats the full group amount for any positive member count; reaggregation yields member count times that amount; add() rejects the same group-to-Person request.

Five bounded Hypothesis tests exercise these invariants using synthetic registries/parameter trees and Core's small template country. They do not construct or copy a full US model for each example. The Kansas regression temporarily mutates one shared variable and restores it with monkeypatch; a one-person simulation reuses the existing US system and confirms that 100 - 100 = 0 becomes false before checking guard rejection. Python is necessary to mutate the declaration and exercise the static guard; YAML cannot test that interaction.

Methodology

Existing positive program-receipt edges remain exempt as exact (person variable, attribute, group variable) triples. The alternative is a whole-variable exemption, which permits new sources or subtraction without review. We preserve main's existing SPM-unit-to-member interpretation and change no modeled result relative to main.

Validation and CI cost

Reproductions ran before fixes, one file at a time in the foreground:

  • P2: on the plain merge of the original head with main, the original file had 4 passed, 1 failed. test_known_violations_only_shrink named exactly the MS, OH, and WV triples above. That existing regression passes after their removal.
  • P3: on the original head, a Kansas recipient with computed SNAP 100, reported receipt false, and all other qualifying inputs zero became false after adding subtracts = ["snap"], while all original guard checks accepted the edge. The rejection regression failed before the fix. The same runtime scenario is now in the committed regression and passes because the guard rejects the subtraction.
  • Final changed file: 10 passed, including five Hypothesis properties with 220 passing generated cases and two explicit examples; no failing generated cases. Full-repository Ruff formatting/lint and git diff --check on the changed files and the diff against main pass.

Local macOS / CPython 3.13.9 / Core 3.32.15, including fresh US model loading:

File revision Result Process elapsed Peak RSS
Merged tree before fixes 4 passed, 1 expected failure 367.97 s 774,520,832 bytes (0.72 GiB)
Final guard 10 passed 404.96 s 847,921,152 bytes (0.79 GiB)

The final tests' call time totals about 20.85 s: the generated Core premise takes 10.39 s, the one-person Kansas regression 4.84 s, and registry/history properties about 5.62 s. Peak process RSS increased by 70 MiB between these local runs. These measurements include setup and host scheduling and are not Linux CI timings. The initial sandbox attempt failed collection because Core's template-country import creates storage beside its installed package; subsequent runs used an unchanged workspace copy of installed Core so that storage was writable. Core implementation files match the tagged source linked above.

The file remains in the Rest job's remaining group; no runner or matrix was added.

The make -n test-other-python-rest-remaining selector includes this code-health file. I downloaded the existing Linux CI timing artifact, run 37462569599: remaining completed in 13:10.63 with maximum RSS 10,228,880 KiB (about 9.76 GiB), exit 0. These are historical whole-group measurements, not measurements of this revision. No extra CI job or full-suite run was started to benchmark this change.

Not covered

The scan covers the baseline system. Contrib reform variables are outside its scope.

Hub microsimulation impact

None to measure. Against current main, this PR changes only the code-health guard test and a changelog fragment: no variable, parameter or formula changes (US + core hub, 2026-10-10; confirmed by the independent delta review).

axiom: n/a: tooling (code-health test only; no policy change)

policyengine-core evaluates adds/subtracts with calculate(..., map_to=<the
variable's entity>), and map_result projects a group value onto every member
with no split. A person variable that lists a tax unit or SPM unit variable
therefore gives each member the whole group amount, and a group total counts
it once per member (MT casualty loss #9888, MS HSA #9865, WV senior #9856,
and OH, IL, CA Riverside and LA cases found the same day).

The new code-health test scans the shared baseline system, resolving
parameter-path lists across their whole history. Three boolean enrollment
flags are allowed; the 10 current cases are known bugs that may only be
removed. A premise test pins the projection on core's template country.

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

Copy link
Copy Markdown
Contributor Author

Merge order: eight fix PRs now remove entries from KNOWN_VIOLATIONS, not three

This PR's body names #9888, #9865 and #9856. Five fixes opened after it remove the other seven entries:

Combined merge. I merged this PR and the eight fixes onto upstream/main.

Why order matters. main doesn't require branches to be up to date, so whichever of this PR and a fix lands second breaks this file on main:

  • If this PR lands first, every later fix must delete its own entry here and re-run CI.
  • If a fix lands first, this PR must drop that entry before it lands.

Suggested order. Land this PR last.

  1. After the eight fixes merge, empty KNOWN_VIOLATIONS. You could also remove the shrink-only test, since nothing will be left to shrink.
  2. Re-run CI, then land.

That end state is the guard as specified: it fails on any monetary Person variable that lists a group variable, with an allowlist for boolean receipt flags. If this PR has to land earlier, delete the entries for fixes that have already merged. After that, each fix that lands deletes its own line.

#9925 / #9939 overlap. Both PRs edit the same lines of oh_unreimbursed_medical_care_expense_deduction_person.py, so whichever lands second has a textual conflict. The adds change is identical (oh_insured_unreimbursed_medical_care_expenses_person); the PRs differ only in the comment and #9925's updated reference URLs. On the combined merge, with #9925's side of that file, these all pass:

  • oh_unreimbursed_medical_care_expense_deduction.yaml (4), oh_insured_unreimbursed_medical_care_expenses.yaml (5), oh_deductions.yaml (4) and integration_2025.yaml (9);
  • test_oh_unreimbursed_medical_deduction_invariants.py and test_dependent_state_subtractions_invariants.py.

#9939's filer_amounts_recorded_on_dependents keeps the medical deduction on dependents, so #9925's prorated dependent share still reaches Ohio AGI.

Each fix fails on main with a household test. I ran each PR's new household cases against main 2d0815ae23. I dropped any case that needs a variable new in that PR, and any output that names one. Every remaining failure is a wrong value, not an error:

PR File Fails on main Example on main vs expected
#9865 MS ms_health_savings_account_adjustment.yaml 2 of 4 runnable ms_agi_adjustments [3,000, 3,000] vs [3,000, 0]
#9888 MT mt_itemized_deductions_joint.yaml / _indiv.yaml 2 of 2 / 1 of 1 runnable joint with child: [60,000, 0, 0] vs [19,900, 0, 0]
#9856 WV wv_senior_citizen_disability_deduction_total_modifications.yaml 2 of 2 (failure messages not captured)
#9902 IL il_aabd_expense_exemption_person.yaml 4 of 4 [299.11, 299.11] vs [211.55, 211.55]
#9901 Riverside ca_riv_general_relief_earned_income_deductions.yaml / ca_riv_general_relief_countable_property_value.yaml 3 of 3 / 2 of 3 deductions [31.80, 62.40] vs [31.80, 30.60]
#9916 LA integration.yaml 2 of 3 net income 5,400 vs 1,800
#9925 OH oh_unreimbursed_medical_care_expense_deduction.yaml 4 of 4 [2,250, 2,250, 2,250] vs [2,250, 0, 0]

ALLOWED. I read the three consumers, and all three treat the flag as the person's own enrollment, as this PR says:

  • commodity_supplemental_food_program_eligible ORs ks_dcf_csfp_categorically_eligible into income eligibility.
  • ma_mbta_income_eligible_reduced_fare_eligible uses ma_mbta_enrolled_in_applicable_programs as defined_for.
  • tx_dart_reduced_fare_eligible ORs tx_dart_reduced_fare_program_eligible.

None of them sums a dollar amount over members.

@MaxGhenis MaxGhenis closed this Oct 10, 2026
@MaxGhenis MaxGhenis reopened this Oct 10, 2026
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Reopened by the US + core hub to start a fresh CI run at the same head. The previous run failed on test_spm_system.py, which broke on every pull request when policyengine-core 3.33.1 was released; #10069 fixed it on main at 20:42Z on 2026-10-10. Nothing in this pull request changed.

@MaxGhenis
MaxGhenis merged commit 0984c83 into main Oct 10, 2026
51 of 52 checks passed
@MaxGhenis
MaxGhenis deleted the code-health-person-adds-group-projection branch October 10, 2026 22:45
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#9897 at 2bd81fd1fd38a4ee801ca28b559df5aa9b8fb1a9 (squash): a code-health guard against person variables that add group variables.

  • Adopted from the open-PR backlog on 2026-10-10: ready and green since 10/06, with no hand-off.
  • Review: two GPT-6.1 Sol rounds. Delta review r2 approves this exact head, with no open findings and METHODOLOGY_CHOICE: no. The guard now exempts exact (variable, source, period) triples, not whole variables, and the three entries main has since resolved are removed.
  • Scope: against current main, the head changes only the guard test and a changelog fragment. The branch merged main in its fix round, so its diff against the old head carries main's changes; the partner subtree matches main. No model change, so there is no microsimulation impact to measure.
  • Gates checked live at merge:
    • gh pr checks exits 0;
    • the latest Pull request run at the head concluded success;
    • MERGEABLE, not a draft, no CHANGES_REQUESTED;
    • the semantic-overlap guard;
    • head pinned.
  • Axiom: n/a: tooling (code-health test only; no policy change).
  • --admin: used only because a required approving review is the sole block.

MaxGhenis added a commit that referenced this pull request Oct 11, 2026
#9902 made il_aabd_expense_exemption_person count only the person's own
withholding, so it no longer adds the tax-unit state_withheld_income_tax.
test_known_violations_only_shrink (#9897) then fails on main because the
entry is still listed in KNOWN_VIOLATIONS. Remove it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant