Skip to content

Make Riverside General Relief eligible at exactly $500 of property - #10582

Merged
MaxGhenis merged 4 commits into
mainfrom
riv-gr-property-boundary-500
Nov 3, 2026
Merged

MaxGhenis merged 4 commits into
mainfrom
riv-gr-property-boundary-500

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Ruling

Riverside County General Relief now counts a unit holding exactly $500 of total combined countable property as property-eligible.

The DPSS General Assistance fact sheet has two relevant lines:

  • its lead-in: "Being below the income and property limits set by Riverside County";
  • its property bullet: "Total combined property cannot exceed $500".

The lead-in is the general sentence covering both limits. The property bullet is the specific rule for property, and the specific governs, so $500 meets the limit. The county manual (DP 90-001 p.22) isn't public.

Source: the fact sheet archived 2021-07-13, https://web.archive.org/web/20210713051521/http://dpss.co.riverside.ca.us/files/pdf/dpss-general-assistance.pdf#page=1 (SHA-256 5760c47280bfb42f8f60cd2d1243f4a77a2443edf6a0a28cf8e1b4c9c103f130).

Change

  • ca_riv_general_relief_property_eligible.py: np.round(total_property_value, 2) <= p.limit (was total_property_value < p.limit). The variable now cites the fact sheet.
    • Why the rounding. Inputs are stored as float32. Two accounts that total $500.00 sum exactly, but three or more can land one float32 step above it: $265.92 + $133.82 + $100.26 sums to 500.00003, which a bare <= reads as over the limit. Rounding the total to whole cents first fixes this, as is_optional_senior_or_disabled_asset_eligible.py already does. $500.01 stays ineligible: float32 500.01 is 500.0100098, which rounds to 500.01.
  • property/limit.yaml: the description now reads "Riverside County limits General Relief to units whose total combined countable property does not exceed this amount." (The limit applies to the SPM unit's combined property, not to a tax filer.) The fact sheet is added as a reference (title, then href with #page=1), and the manual reference is kept.
  • tests/core/test_riverside_general_relief_attribution_invariants.py: the property-eligibility oracle is now property_value <= PROPERTY_LIMIT and the docstring says "at or below $500". It still encoded the old < rule and passed only because no draw totalled exactly $500.
  • Changelog fragment changelog.d/riv-gr-property-boundary-500.fixed.md.

Cases

The property is cash held by the first adult, with no vehicle and no personal property.

eligibility/ca_riv_general_relief_property_eligible.yaml: the existing Case 2 ($500 given directly as countable property) flips from ineligible to eligible. Cases 6-11 cover a single adult and a two-adult unit at $499.99 (eligible), $500 (eligible) and $500.01 (ineligible). Case 12 spreads exactly $500.00 over bank ($265.92), stock ($133.82) and bond ($100.26) accounts: eligible.

integration.yaml (TY2024-01, $300 monthly housing cost, no income):

Case Unit Cash Eligible Grant Hand computation
11 single, 64, incapable of self-care $0 yes 328.50 min(300, 223) + 92 + 11 = 326, + 30/12 = 328.50
12 single $499.99 yes 328.50 same as Case 11
13 single $500 yes 328.50 same as Case 11
14 single $500.01 no 0
15 two adults $499.99 yes 438.50 same as Case 1
16 two adults $500 yes 438.50 min(300, 257) + 160 + 19 = 436, + 2.50 = 438.50 (Case 1)
17 two adults $500.01 no 0

At $500 the grant equals the $0-property grant: Case 13 equals Case 11, and Case 16 equals Case 1.

Tests run (one process per file)

  • All 13 Riverside GR YAML files pass, 62 cases: the property-eligibility file 12, integration.yaml 17, the other 11 files 33.
  • test_riverside_general_relief_attribution_invariants.py: 2 passed.
  • test_source_conflicts.py: 103 passed.
  • Partner contract files, run read-only and not edited: analytics_coverage/.../ca/riv_general_relief.yaml 3 passed, amplifi/2025.yaml 1 passed, amplifi/2026.yaml 3 passed.

Mutants (not committed, code restored after each):

  • (a) total_property_value < p.limit (main's rule): the property-eligibility file fails Cases 2, 7, 10 and 12 (4 of 12), and integration.yaml fails Cases 13 and 16 (2 of 17). The invariant test still passes (2 of 2), since its draws are whole dollars and never total exactly $500.
  • (b) total_property_value <= p.limit (no rounding): the property-eligibility file fails Case 12 only (1 of 12); integration.yaml passes 17 of 17.
  • (c) np.round(total_property_value, 2) <= p.limit + 1: the property-eligibility file fails Cases 8 and 11, integration.yaml fails Cases 14 and 17, and the invariant test fails test_seeded_population.

CI

At head 9fb39dd72dedc061a37e33289851075163fac9ae: Pull request run 41536094536 completed with conclusion success, all 35 jobs green, including Household API Partners, every Baseline, Contrib and Python shard, and Microsimulation.

Refs #10232

impact: no output moves in 2026. No Riverside SPM unit in the default dataset holds from $500 to $500.005 of countable property, where the new limit test and the old one disagree. No record in it receives General Relief in 2026, before or after this change (pair).

axiom: TheAxiomFoundation/rulespec-us#1983 queued


Generated by Claude Code

The DPSS General Assistance fact sheet's property bullet says total
combined property cannot exceed $500, so a unit holding exactly $500
meets the limit. The model required less than $500. Compare with <=,
cite the fact sheet on the limit, and add cases at $499.99, $500 and
$500.01 for one and two adults.

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

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 1de21757f90f4ff7419b0bf383d3465d98702b14 (draft)

Precondition: #9901 merged on 2026-10-22, and no open PR or branch covered this change.

What the item asked, and how I resolved it

  • Comparison: policyengine_us/variables/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_property_eligible.py:19 is now total_property_value <= p.limit. The variable also cites the fact sheet (:10).
  • policyengine_us/parameters/gov/local/ca/riv/general_relief/property/limit.yaml:1: the description now reads "...whose total combined countable property does not exceed this amount". The fact sheet is added at :13-14 (title, then href with #page=1), and the manual reference is kept.
  • Cases: eligibility/ca_riv_general_relief_property_eligible.yaml Cases 6-11, plus the existing Case 2 flipped from false to true at $500. integration.yaml Cases 11-17. Hand computations are in the PR body: single 223 + 92 + 11 + 30/12 = 328.50; two adults 257 + 160 + 19 + 2.50 = 438.50. At $500 the grant equals the $0-property grant (Case 13 = Case 11, Case 16 = Case 1).
  • Changelog: changelog.d/riv-gr-property-boundary-500.fixed.md.
  • The body says "Refs Riverside GR: resolve eligibility at exactly $500 of combined property #10232", never "Fixes" or "Closes".

Tests (one process per file): all 13 Riverside GR files pass, 61 cases in all (property eligibility 11, integration 17, the other 11 files 33). test_source_conflicts.py: 103 passed.

Mutant (not committed): restoring < fails 3 of the 11 eligibility cases and 2 of the 17 integration cases, all at exactly $500.

CI: not run, because the PR is a draft.

Judgment calls

  • web.archive.org was unreachable from this VM, so I could not check the PDF or its hash. Both quotes in the body come from the hub's ruling.
  • Item 2 says both to write axiom: (hub files pe-parity before review) and that a reviewer has ruled that line invalid. I wrote axiom: needed; the hub should replace it.
  • The single-adult cases use age 64 with is_incapable_of_self_care, so the person meets the work rule and is under the SSI age.
  • I changed the existing Case 2 of ca_riv_general_relief_property_eligible.yaml ($500 → eligible). It encoded the old reading.
  • The API rewrote the body's closing line into the standard "Generated by Claude Code" footer.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: REQUEST_CHANGES
1de2175
METHODOLOGY_CHOICE: yes

Independent review, round 1. The head has not moved past 1de21757f9. No earlier **Hub review in progress** comment exists on this PR, so this review starts from the beginning.

The comparison change is right, and the new YAML cases pin it. Two things in the same rule are still wrong at this head. The Python invariant test still encodes the old strict rule, and float32 sums can push an exact $500.00 total over the limit. Both fixes are one or two lines.

P2-1. The Riverside invariant test still asserts the old < $500 rule

policyengine_us/tests/core/test_riverside_general_relief_attribution_invariants.py:239 (oracle) and :18 (docstring)

The test's oracle is property_value < PROPERTY_LIMIT, and its docstring says "Property eligibility is that sum below $500." Both now contradict the model. The test passes in CI only because its derandomized Hypothesis draws and its seeded population never land on a unit total of exactly $500. Any change to the seed, to the strategies or to the Hypothesis version that produces such a total will fail CI against correct code. A reader of the docstring is also told the wrong rule. The PR body's list of "13 Riverside GR test files" leaves this file out.

Witness: one unit with bank_account_assets 250 and stock_assets 250, passed to the module's own _check:

# m._check([unit]) from the test module, at 1de21757
FAILED: ['Arrays are not equal', ..., ' [0]: True (ACTUAL), False (DESIRED)']

Replacement:

-   is that sum below $500.
+   is that sum at or below $500.
-        property_value < PROPERTY_LIMIT,
+        property_value <= PROPERTY_LIMIT,

With both edits on a scratch copy, the file passes (2 passed) and the same witness passes.

P2-2. Cash spread over several accounts that totals exactly $500.00 can still be ineligible

policyengine_us/variables/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_property_eligible.py:19

The inputs are float32. ca_riv_general_relief_countable_property_value adds spm_unit_cash_assets (bank + stock + bond), the countable vehicle value and personal property, and that sum can come out one float32 step above 500 (500.00003) when the cents-exact total is 500.00. The <= comparison then reports ineligible, in exactly the case this PR's title says it fixes. Two-way splits are safe: I checked all 49,999 cent splits of $500.00 between two float32 values, and none drifts. Three-way splits are not. Of 200,000 random three-way cent splits summed in numpy float32, 13,260 came out above 500.

Model witness at this head: one adult with bank_account_assets 265.92, stock_assets 133.82 and bond_assets 100.26, a total of $500.00:

countable_property_value = np.float32(500.00003), property_eligible = False

I also ran 40 such units through Simulation. All 40 came out 500.00003 and ineligible. As a YAML case expecting ca_riv_general_relief_property_eligible: true, it fails at the head (1 failed).

The repo already handles this pattern in is_optional_senior_or_disabled_asset_eligible.py:21-22. Replacement for line 19:

        # Inputs are stored as float32, so compare whole cents with the limit.
        return np.round(total_property_value, 2) <= p.limit

Then add this case to eligibility/ca_riv_general_relief_property_eligible.yaml:

- name: Case 12, cash spread over three accounts totalling exactly $500.00.
  period: 2024
  input:
    people:
      person1:
        age: 30
        bank_account_assets: 265.92
        stock_assets: 133.82
        bond_assets: 100.26
    households:
      household:
        members: [person1]
        in_riv: true
    spm_units:
      spm_unit:
        members: [person1]
  output:
    # 265.92 + 133.82 + 100.26 = 500.00, which does not exceed the $500 limit.
    ca_riv_general_relief_property_eligible: true

With the rounding on a scratch copy, the property-eligibility file passes 11 of 11, integration.yaml passes 17 of 17, the witness passes, and all 40 drifted units become eligible. $500.01 stays ineligible: float32 500.01 is 500.0100098, which rounds to 500.01.

On main the defect is wider, since every $500 unit is ineligible there. This PR doesn't make it worse, but it leaves its own claim incomplete.

P3-1 (follow-up, identical on main). Income eligibility uses <= while the lead-in says "below"

policyengine_us/variables/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_income_eligible.py:14: countable_income <= needs_standards.

The fact sheet's lead-in ("Being below the income and property limits") covers income too. The hub's ruling settles property through the specific property bullet. I couldn't fetch the sheet, so I can't say whether an income bullet exists that settles income the same way. If none does, the lead-in's "below" argues for <. Witness at both the head and main: a single adult (64, incapable of self-care, $300 rent) with $326 a month of interest_income has countable income equal to the $326 needs standard. That unit is income-eligible and gets a $2.50 grant (the transportation special need):

countable_income 326, needs_standards 326, income_eligible true, ca_riv_general_relief 2.5: 1 passed at the head, 1 passed at main

This is a follow-up for the hub's register, not a change for this PR.

P3-2. The parameter description says "filers"

policyengine_us/parameters/gov/local/ca/riv/general_relief/property/limit.yaml:1. The limit applies to the SPM unit's combined property (entity = SPMUnit), not to a tax filer. The old text also said "filers", and the brief asked for "filer", so this is optional. Suggested text: Riverside County limits General Relief to units whose total combined countable property does not exceed this amount.

Checked and sound

  • Defect reproduces on main. I ran the PR's two changed YAML files against origin/main code. The property file had 3 failures (Cases 2, 7, 10) and integration.yaml had 2 (Cases 13, 16), all at exactly $500. All 11 and 17 cases pass at the head.
  • Hand values, computed from the parameters before reading the cases. Single adult: min(300, 223) + 92 + 11 = 326, plus 30 / 12 = 2.50, giving 328.50. Two adults: min(300, 257) + 160 + 19 = 436, plus 2.50, giving 438.50. Both match Cases 11-17. At $500 the grant equals the $0-property grant (Case 13 = Case 11, Case 16 = Case 1).
  • Single-adult setup. Age 64 is under the 65 work-exemption age (work_exempted_age/older.yaml), so ca_riv_general_relief_meets_work_requirements is true only through is_incapable_of_self_care. The unit is otherwise eligible: age ≥ 18, ssi 0, ca_tanf 0. Cases 11-14 differ only in cash, and Case 14 flips on property alone. The cases pass because of the rule they mean to test.
  • Generated values reach the boundary. The cases sit at $499.99, $500 and $500.01, for both a single adult and a two-adult unit, through real bank_account_assets and not only the direct input.
  • Mutants. < fails property-eligibility Cases 2, 7 and 10 and integration Cases 13 and 16 (matches the hand-off). My own mutant, <= p.limit + 1, fails property-eligibility Cases 8 and 11 and integration Cases 14 and 17. git status was clean after each run.
  • Every year, entity and filing status. The parameter has one value from 2020-01-01. The formula has no year branch and runs on the SPM unit. Filing status doesn't enter.
  • Other comparisons under gov/local/ca/riv/general_relief. Age ≥ 18 matches "this age or older". Work exemption age ≥ 65 matches "this age or older". The student exemption age < 19 matches "younger than this age". ssi > 0, tanf > 0 and budget_unit_size > 0 are receipt flags, not limits. The vehicle exemption uses max_(value − 4,650, 0), which has no boundary effect. Income is in P3-1.
  • Readers. ca_riv_general_relief_property_eligible is read only by ca_riv_general_relief_eligible, which gates ca_riv_general_relief. The property.limit parameter is read only by the changed variable. ca_riv_general_relief isn't in household_state_benefits.yaml or any other aggregator; only programs.yaml names it. So the change can't move household net income or any other program's output. It can move only ca_riv_general_relief and its eligibility, for units whose countable property is exactly 500 in float32.
  • Partner contract tests (run read-only, not edited). partners/analytics_coverage/edge_cases/state/ca/riv_general_relief.yaml passes 3 of 3. partners/amplifi/2025.yaml passes 1 of 1, and 2026.yaml passes 3 of 3. The PR touches nothing under .github/ or partners/.
  • Riverside YAML files, one per process at the head: all 13 pass, 61 cases (property eligibility 11, integration 17, the other 11 files 33), which matches the body. test_source_conflicts.py: 103 passed.
  • CI at this head. The pull_request run 41403111044 succeeded, with every job green, including Household API Partners, the full Baseline, Contrib and Python suites, and Microsimulation. An earlier run (41401468903, while the PR was a draft) was skipped and doesn't count. The hand-off's "CI: not run" is out of date.
  • Body and docs. The ruling, both quotes, the case table, the mutant, Refs #10232 (no Fixes or Closes), impact: the hub will measure, and axiom: TheAxiomFoundation/rulespec-us#1983 queued are present. The axiom: line has the right form; I can't open that repository from this session. The changelog fragment is a top-level changelog.d/riv-gr-property-boundary-500.fixed.md. The new references follow the house format: one page, title then href with #page=1. The countable-property variable already cited the same URL on main.
  • Assignment. Stop projecting group amounts onto every Riverside General Relief member #9901 merged 2026-10-22T04:13:33Z. The branch is riv-gr-property-boundary-500. The comparison, description, reference, the six boundary cases with grant equality, and the changelog are all done. The assignment didn't name the Python invariant test, but its oracle encodes the rule the assignment changes (P2-1).
  • Overlap. Open-PR searches for ca_riv_general_relief_property_eligible, limit.yaml riv and riverside general relief return only this PR (and the unrelated Count a Montana casualty loss once per joint return and only for the spouse who had it #9888, which touches no Riverside file).
  • Baseline impact. The hub should count Riverside SPM units whose baseline ca_riv_general_relief_countable_property_value equals 500 and report the change in ca_riv_general_relief. I expect it near zero, since imputed assets are rarely cent-exact.

What I couldn't check

  • The fact sheet. web.archive.org and rivcodpss.org were refused by this VM's egress proxy (403), and dpss.co.riverside.ca.us doesn't resolve. So I couldn't refetch the PDF, match its SHA-256 or read both quotes myself, or see whether it has a specific income bullet (P3-1). Per the brief, I rely on the hub's logged fetch for the reading and don't reargue it.
  • The county manual (DP 90-001 p.22) isn't public.
  • Population impact needs microdata, which the hub measures.
  • rulespec-us#1983 is outside this session's repository scope.

Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Picking this up for the US hub. I'll post a hand-off comment here when it's ready.


Generated by Claude Code

Bring the branch up to date before the round-1 fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt test

Inputs are float32, so three accounts totalling exactly $500.00 can sum
to 500.00003 and fail the <= limit check. Round the total to whole cents
before comparing, as is_optional_senior_or_disabled_asset_eligible does,
and add the three-account case.

The Python invariant test still encoded the old strict < rule and passed
only because no draw hit exactly $500; its oracle and docstring now say
at or below. The parameter description now says units, not filers, since
the limit applies to the SPM unit's combined property.

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

Copy link
Copy Markdown
Contributor Author

Ready for hub review

Head: 9fb39dd72dedc061a37e33289851075163fac9ae

Fix round for the round-1 review (#10582 (comment)). The starting head was 1de21757f90f4ff7419b0bf383d3465d98702b14, as named. Nobody had pushed since 06:40 UTC.

Merge of main

origin/main (f8142b0223e36f78736dac73b868f2a4e68841c1) merged with merge commit 8194b03d. No conflicts. After the merge, the diff against main touches only this PR's five files.

Findings

  1. P2-1, invariant test. policyengine_us/tests/core/test_riverside_general_relief_attribution_invariants.py:18 now says "is that sum at or below $500.", and :239 is now property_value <= PROPERTY_LIMIT,. Witness (one unit, bank_account_assets 250 + stock_assets 250, through the module's _check): before, it FAILED with DESIRED: array([False]); after, it passes. The oracle sums whole-dollar float64 draws, so it needs no rounding of its own.
  2. P2-2, float32 rounding. policyengine_us/variables/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_property_eligible.py:19-20 now reads # Inputs are stored as float32, so compare whole cents with the limit. / return np.round(total_property_value, 2) <= p.limit. This matches is_optional_senior_or_disabled_asset_eligible.py:20-21. Witness (bank 265.92, stock 133.82, bond 100.26): before, countable_property_value = np.float32(500.00003) and property_eligible = False; after, the value is still 500.00003 and property_eligible = True. Case 12 was added verbatim at eligibility/ca_riv_general_relief_property_eligible.yaml:201. $500.01 stays ineligible: a direct run gives np.float32(500.01) → False, and Cases 8 and 11 pass.
  3. P3-2, description. policyengine_us/parameters/gov/local/ca/riv/general_relief/property/limit.yaml:1 now reads "Riverside County limits General Relief to units whose total combined countable property does not exceed this amount."
  4. Body. Rewritten from a file with the REST PATCH call and checked against GitHub's readback: it matches line for line, except that the server replaced the closing "🤖 Generated with Claude Code" line with its own "Generated by Claude Code" footer. It adds the float32 rounding and why, Case 12, the invariant test, this round's counts and mutants, and the CI result. Refs #10232, impact: the hub will measure and the axiom: line are kept.

Tests (one process per file, at 9fb39dd7)

File Result
ca_giv_general_relief_countable_income.yaml 2 passed
ca_riv_general_relief_countable_property_value.yaml 4 passed
ca_riv_general_relief_countable_vehicle_value.yaml 3 passed
ca_riv_general_relief_earned_income_deductions.yaml 3 passed
ca_riv_general_relief_excluded_income.yaml 2 passed
ca_riv_general_relief_needs_standards.yaml 2 passed
eligibility/ca_riv_general_relief_eligible.yaml 2 passed
eligibility/ca_riv_general_relief_immigration_status_eligible.yaml 3 passed
eligibility/ca_riv_general_relief_income_eligible.yaml 2 passed
eligibility/ca_riv_general_relief_ineligible_person.yaml 6 passed
eligibility/ca_riv_general_relief_meets_work_requirements.yaml 4 passed
eligibility/ca_riv_general_relief_property_eligible.yaml 12 passed
integration.yaml 17 passed
test_riverside_general_relief_attribution_invariants.py 2 passed
code_health/test_source_conflicts.py 103 passed
partners analytics_coverage/edge_cases/state/ca/riv_general_relief.yaml (read-only) 3 passed
partners amplifi/2025.yaml (read-only) 1 passed
partners amplifi/2026.yaml (read-only) 3 passed

That is 62 Riverside YAML cases (61 last round, plus Case 12). No partner file moved or was edited. make format: clean.

Mutants (each in its own processes; git status clean after each)

  • (a) total_property_value < p.limit. The property file fails Cases 2, 7, 10 and 12 (4 of 12). integration.yaml fails Cases 13 and 16 (2 of 17). The invariant test passes (2 of 2), because its draws are whole dollars that never total exactly $500, so it isn't a guard for this mutant.
  • (b) total_property_value <= p.limit (no rounding). The property file fails Case 12 only (1 of 12). integration.yaml passes 17 of 17, and the invariant test passes 2 of 2.
  • (c) np.round(total_property_value, 2) <= p.limit + 1. The property file fails Cases 8 and 11 (2 of 12). integration.yaml fails Cases 14 and 17 (2 of 17). The invariant test fails test_seeded_population.

CI

Pull request run 41536094536 at 9fb39dd7 completed at 23:19 UTC with conclusion success: all 35 jobs succeeded, including PR validation, Household API Partners, Quick Feedback, Package and compatibility, every Baseline, Contrib and Python shard, and Microsimulation. It is the only workflow run at this head.

Judgment calls

  • I used the review's exact replacement text for the formula, docstring, oracle, Case 12 and description.
  • I left the invariant oracle unrounded. Its inputs are whole-dollar draws summed in float64, so it can't drift the way the model's float32 sum does.
  • P3-1 (income <= against the lead-in's "below") is untouched, as the review marked it a follow-up for the hub's register.
  • I didn't refetch the fact sheet; the ruling and quotes are unchanged from the hub's logged fetch.

Proposed follow-up

Riverside GR income limit: settle <= vs "below" at the needs standard. ca_riv_general_relief_income_eligible uses countable_income <= needs_standards, while the fact sheet's lead-in says "below". Decide from the source (or record a source conflict) and pin the boundary with a YAML case.


Generated by Claude Code

@MaxGhenis

MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Hub review in progress

Superseded: the round-2 review is posted at #10582 (comment).


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
VERDICT: APPROVE
9fb39dd
METHODOLOGY_CHOICE: yes

Independent review, round 2. The head has not moved past 9fb39dd72d. The range since round 1 is the merge 8194b03d and the fix commit 9fb39dd7. Both round-1 P2 findings and the P3 wording finding are closed at this head. The income question stays a follow-up, and it is identical on main. I found no P1 or P2. One new P3: a test comment that this PR makes stale, with a one-line assertion that would close #10232's multi-member criterion.

P3-1. A test comment still says it avoids choosing eligibility at exactly $500

policyengine_us/tests/policy/baseline/gov/local/ca/riv/general_relief/ca_riv_general_relief_countable_property_value.yaml:92

Case 4 builds a two-person unit whose combined property is exactly $500: cash 300 + 100, countable vehicle 25, and member property 50 + 25. Its comment reads "Assert the stock amount without choosing eligibility at exactly $500." This PR makes that choice, so the comment now describes a rule that no longer exists. #10232's acceptance criterion 3 also asks for "a multi-member combined-property input" at the boundary. The PR's boundary cases put all the cash with the first adult, and Case 12 has a single person, so no case asserts eligibility for property combined across members at exactly $500. Case 4 already has that input, and one line pins it.

Replacement:

     # Cash 300 + 100, countable vehicles 25, member property 50 + 25.
-    # Assert the stock amount without choosing eligibility at exactly $500.
+    # The total does not exceed the $500 limit, so the unit is property-eligible.
     ca_riv_general_relief_countable_property_value: 500
+    ca_riv_general_relief_property_eligible: true

On a scratch copy at this head, the file passes 4 of 4. With mutant (a) (total_property_value < p.limit), it fails Case 4:

4 passed, 1 warning
with mutant (a): Test 'Case 4 ... 1 failed, 3 passed

This file is outside the five the round-1 fix was limited to, so whether to add it now is the hub's call. The PR says Refs #10232, not Fixes, which fits: criteria 1 (the manual) and 4 (Axiom) are still open.

P3-2 (follow-up, identical on main). Income test: <= against the lead-in's "below"

policyengine_us/variables/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_income_eligible.py:14: countable_income <= needs_standards.

I still can't read the fact sheet (see below), so I can't tell whether it has a bullet specific to income, the way the property bullet is specific to property. The reasoning depends on that bullet:

  • If the sheet has no specific income rule, the only text is the lead-in, "Being below the income and property limits". The canon the hub used for property (specific over general) then has nothing specific to apply, and "below" argues for countable_income < needs_standards.
  • If it has an income bullet in "cannot exceed" form, <= stands, by the same ruling as property.

What it moves: ca_riv_general_relief adds needs_standards and special_needs_amount and subtracts countable_income (ca_riv_general_relief.py). So at income exactly equal to the needs standard, the choice moves only the special-needs amount, $30 / 12 = $2.50 a month. The round-1 witness (a single adult with $326 of income against a $326 needs standard: eligible, grant $2.50) gives the same result at main. Proposed issue: Riverside GR: settle the income comparison at exactly the needs standard. Fetch the fact sheet's income text and choose < or <=. Pin income = needs and needs + $0.01 in ca_riv_general_relief_income_eligible.yaml.

Checked and sound

Round-1 findings, each confirmed closed at this head

  • P2-1 (invariant oracle). The test now uses property_value <= PROPERTY_LIMIT (test_riverside_general_relief_attribution_invariants.py:239), and the docstring says "at or below $500" (:18). The round-1 witness (bank 250 + stock 250) passes the module's own _check at this head. The file passes 2 of 2.
  • P2-2 (float32 sum). ca_riv_general_relief_property_eligible.py:19-20 is np.round(total_property_value, 2) <= p.limit, with the comment. Case 12 was added verbatim.
  • P3-2 (description). property/limit.yaml:1 now says "units".

The merge. 8194b03d has parents 1de21757 and main f8142b02. git diff 8194b03d^2 8194b03d touches only the five round-1 files, and its diff matches round 1's diff against its old base byte for byte. So the merge adds nothing beyond main. The PR now touches six files: those five plus the invariant test. The REST files list agrees, and nothing is under .github/ or partners/. Main has moved to d4d39178 (6 commits, none touching Riverside), and the PR still merges cleanly (git merge-tree).

The rounding.

  • Margins. I enumerated every float32 from 499.98 to 500.02 (1,311 values). Every value within 0.4 cents of 499.99 or 500.00 passes, and every value within 0.4 cents of 500.01 fails. The largest passing float32 is 500.005, and the smallest failing one is 500.00504. float32 500.01 is 500.0100098, which rounds to 500.01 and fails.
  • Multi-member sums through the model. I ran 1,200 random units through Simulation: 1-4 members, each with bank, stock, bond and personal property, plus a vehicle excess, with cent splits totalling 499.99, 500.00 and 500.01 (400 each). At 500.00, 176 of 400 raw values sit above 500 (largest drift 0.00027), and all 400 are eligible. All 400 at 499.99 are eligible, and none of the 400 at 500.01 is. The drift is about 20 times smaller than the half-cent margin. Every input is non-negative, so the sum's error stays near the float32 step at 500.
  • Two- and three-account sums. 265.92 + 133.82 + 100.26 in float32 is 500.00003, as the body says. None of the 49,999 two-way cent splits of 500.00 differs from 500, which matches the body's "two accounts ... sum exactly".
  • Where the rounding sits. is_optional_senior_or_disabled_asset_eligible.py:21-22 (the hand-off says 20-21; the file has a line more) rounds the summed assets into a local before comparing. This PR does the same thing at the comparison. ca_riv_general_relief_countable_property_value has no reader other than this variable and the invariant test, so rounding in the sum would change only the reported value. Keeping it at the comparison is fine.
  • The one consequence. A total with fractions of a cent, in (500, 500.005], now passes. Whole-cent inputs can't produce one, but imputed microdata could (see baseline impact below).

Mutants, each run in its own process on a scratch worktree, with git status clean after each:

Mutant Property file integration.yaml Invariant test
(a) total < p.limit fails Cases 2, 7, 10, 12 fails 13, 16 passes 2/2
(b) total <= p.limit fails Case 12 passes 17/17 passes 2/2
(c) round(total, 2) <= p.limit + 1 fails Cases 8, 11 fails 14, 17 fails test_seeded_population
(d, mine) round(total, 0) <= p.limit fails Cases 8, 11 fails 14, 17 passes 2/2

(a) to (c) match the hand-off and the body exactly. YAML cases kill all four mutants. The invariant test can't kill (a), (b) or (d) because its draws are whole dollars. Its strategies can reach $500 (one to four members with four items each, up to $400 apiece), but the derandomized draws and the seeded population don't land on it.

A boundary-hitting draw in the invariant test doesn't belong in this PR. The YAML cases already pin $499.99, $500, $500.01 and the three-account drift, and the repo's guidance puts policy boundaries in YAML rather than in more simulation-heavy Python. One caveat for anyone who later widens the draws to cents: the oracle sums float32-read values in float64 without rounding. It would then need np.round(property_value, 2), or it would disagree with the model at exactly $500. With whole-dollar draws, the hand-off is right that it needs no rounding.

Defect at main. I ran the PR's two YAML files against main's code (d4d39178). The property file fails Cases 2, 7, 10 and 12 (4 failed, 8 passed), and integration.yaml fails Cases 13 and 16 (2 failed, 15 passed).

Hand values, computed from the parameter files before reading the cases:

  • single adult: housing min(300, 223) + food 92 + personal needs 11 = 326, plus transportation 30 / 12 = 2.50, giving 328.50;
  • two adults: 257 + 160 + 19 = 436, plus 2.50, giving 438.50.

Both match the body's table. The parameter has one value from 2020-01-01, and the formula has no year branch. It runs on the SPM unit, and filing status doesn't enter.

Tests at this head, one file per process:

  • Riverside YAML: 13 of 13 files pass, 62 cases. The property file has 12, integration.yaml 17, and the other 11 files 2+4+3+3+2+2+2+3+2+6+4 = 33.
  • test_riverside_general_relief_attribution_invariants.py: 2 passed. test_source_conflicts.py: 103 passed.
  • Partner files, run read-only and not edited: analytics_coverage/.../ca/riv_general_relief.yaml 3 passed, amplifi/2025.yaml 1 passed, amplifi/2026.yaml 3 passed.
  • ruff format --check on the two changed Python files: already formatted.

Readers. ca_riv_general_relief_property_eligible is read only by ca_riv_general_relief_eligible, which gates ca_riv_general_relief. property.limit is read only by the changed variable. ca_riv_general_relief appears in no aggregator; only programs.yaml names it. So the diff can move only ca_riv_general_relief and its eligibility, and no household net income.

Body. Each number in the tests and mutants section matches my runs, and so do the float claims, the case table and the hand computations. Refs #10232 (no Fixes or Closes), impact: the hub will measure and axiom: TheAxiomFoundation/rulespec-us#1983 queued are present and in the right form. The closing line is the server's footer, which the hub restores at landing.

CI at this head. Pull request run 41536094536, the only workflow run at 9fb39dd7, finished with conclusion success. All 35 jobs succeeded: PR validation, Quick Feedback, Package and compatibility, Household API Partners, 11 Baseline shards, 9 Contrib shards, Microsimulation, the 8 Rest Python shards, and their plan and check jobs. None was skipped or cancelled. No reviews are on the PR.

Overlap. Open-PR searches for ca_riv_general_relief_property_eligible, test_riverside_general_relief_attribution_invariants, ca_riv_general_relief_income_eligible and "riverside general relief" return this PR plus #10607 and #9888. Neither of those touches a Riverside file.

Baseline impact. At baseline, units whose float32 ca_riv_general_relief_countable_property_value lies in [499.995, 500.005] can flip from ineligible to eligible for ca_riv_general_relief. Nothing else can move. The hub's population check should count Riverside SPM units in that band and report the change in ca_riv_general_relief. I expect it near zero.

What I couldn't check

  • The fact sheet. web.archive.org and dpss.co.riverside.ca.us were refused through this VM's proxy (403), and rivcodpss.org is outside this session's allowed domains. So I couldn't read either quote, match the SHA-256, or look for an income bullet (P3-2). I rely on the hub's logged fetch for the property ruling and don't reargue it.
  • The county manual (DP 90-001 p.22) isn't public.
  • Population impact needs microdata, which the hub measures.
  • rulespec-us#1983 is outside this session's repository scope.
  • Source-conflict register. The ruling settles a tension inside one document (lead-in against bullet), and the PR adds no # Source conflict marker. Whether that needs a register entry is the hub's call; test_source_conflicts.py passes.

Generated by Claude Code

Case 4 combines property across two members to exactly $500. Its comment said it avoided
choosing eligibility there, which this PR now does; assert it (round 2 review P3-1, kills
the strict-limit mutant).

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

Copy link
Copy Markdown
Contributor Author

Hub impact pair, progress

The base side of the impact pair for this PR finished. The head side runs next, on the same household chunks.

  • Base: f8142b0223e36f78736dac73b868f2a4e68841c1, the merge base of the head with main. The run imported policyengine_us from the base tree.
  • Head, next: faec8bd7d846ef5e2ca41fd6216cb6b21132c344.
  • Dataset: hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909. The local file's SHA-256 is 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84, which matches DEFAULT_DATASET_SHA256.
  • Year 2026, 8 chunks, through calculate_chunked from Add household-chunked microsimulation calculations #10561 at c545c47877c70b367a6fab2322037e453364997e.
  • Wall time: 14:46.50 (886.5 s) for the whole process, 839.0 s of it inside calculate_chunked. I downloaded the dataset before the run, so this excludes the download.
  • Maximum resident set size: 3,819,576 kB (3.64 GiB), from /usr/bin/time -v.

Weighted sums at the base:

Variable Weighted sum As summary.json records it
ca_riv_general_relief 0.00 0.0
ca_riv_general_relief_property_eligible 119,676.03 119676.02529076242
ca_riv_general_relief_eligible 0.00 0.0
income_tax 2,595,105,852,713.51 2595105852713.508
state_income_tax 578,044,599,346.72 578044599346.724
household_net_income 17,161,428,162,125.83 17161428162125.828
household_benefits 2,059,793,926,050.13 2059793926050.1262

ca_riv_general_relief for 2026 is the sum of its twelve monthly amounts. ca_riv_general_relief_eligible is a monthly boolean, so Core reads it for 2026 as its December value. ca_riv_general_relief_property_eligible is yearly.


Generated by Claude Code

@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Ready for hub review
IMPACT PAIR: zero-diff
head faec8bd
base f8142b0

No variable moves. All seven outputs are identical, row by row, at the two SHAs. Every largest absolute difference is 0.0, and pandas DataFrame.equals holds for each of the three entity tables (57,240 households, 59,900 SPM units, 79,729 tax units).

The head's test, np.round(total_property_value, 2) <= p.limit, gives a different answer from the base's total_property_value < p.limit only for countable property from $500 up to about $500.005. So no Riverside SPM unit in the data falls in that band. At both SHAs, 20 SPM unit records (119,676.03 weighted) are property-eligible. No record is eligible for General Relief in December 2026, and no record receives any in 2026.

  • Dataset: hf://datasets/policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260909 (Microsimulation.default_dataset, the same DEFAULT_DATASET in both trees). The local file's SHA-256 is 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84. It matches DEFAULT_DATASET_SHA256, 6496cc4393d4d3c6574f76eca231de5898c803b9067645591fd5c4d3e65aee84, which is also the same in both trees. I hashed the file before the base side and again after the head side.
  • Year and chunks: 2026, 8 chunks. Both sides used the same balanced household chunks from calculate_chunked, which I took from Add household-chunked microsimulation calculations #10561 at c545c47877c70b367a6fab2322037e453364997e.
  • Environment: git diff $BASE_SHA $HEAD_SHA -- pyproject.toml uv.lock is empty, so both sides ran in the head's uv sync environment (Python 3.14.6, policyengine-core 3.33.2). Each side's summary.json shows that it imported policyengine_us from its own tree.
Variable Entity Base sum Head sum Change Rows that differ Weighted rows up Weighted rows down Largest absolute difference
ca_riv_general_relief spm_unit 0.00 0.00 0.0 0 0.00 0.00 0.0
ca_riv_general_relief_property_eligible spm_unit 119,676.03 119,676.03 0.0 0 0.00 0.00 0.0
ca_riv_general_relief_eligible spm_unit 0.00 0.00 0.0 0 0.00 0.00 0.0
income_tax tax_unit 2,595,105,852,713.51 2,595,105,852,713.51 0.0 0 0.00 0.00 0.0
state_income_tax tax_unit 578,044,599,346.72 578,044,599,346.72 0.0 0 0.00 0.00 0.0
household_net_income household 17,161,428,162,125.83 17,161,428,162,125.83 0.0 0 0.00 0.00 0.0
household_benefits household 2,059,793,926,050.13 2,059,793,926,050.13 0.0 0 0.00 0.00 0.0

ca_riv_general_relief for 2026 is the sum of its twelve monthly amounts. ca_riv_general_relief_eligible is a monthly boolean, so Core reads it for 2026 as its December value. ca_riv_general_relief_property_eligible is yearly.

Side SHA Wall time, whole process Inside calculate_chunked Maximum resident set size
base f8142b0223e36f78736dac73b868f2a4e68841c1 14:46.50 (886.5 s) 839.0 s 3,819,576 kB (3.64 GiB)
head faec8bd7d846ef5e2ca41fd6216cb6b21132c344 14:24.41 (864.4 s) 822.9 s 3,818,036 kB (3.64 GiB)

The whole-process wall time and the maximum resident set size come from /usr/bin/time -v; the time inside calculate_chunked comes from summary.json. I downloaded the dataset before the base side, so neither wall time includes the download. Both sides peaked well under the brief's 14 GB limit, so 8 chunks stood and I didn't rerun with 16.

Dropped variables: none. All seven exist on both sides, on the same entity, and the helper accepted each one. Microsimulation accepted dataset_end_year, so both sides kept it.

Commands. pair_side.py and compare.py are the brief's scripts, unchanged. I departed from the brief's commands in four places:

  • The VM already had uv, so I skipped pip install uv.
  • I ran the helper's own download call once before the base side, so both sides read the cached file.
  • GNU time was missing from the VM, and apt-get found it only after apt-get update.
  • A wrapper, run_side.sh, ran each side's command detached from the session. It also read the process's resident set size from /proc once a minute.

In run order, from a work folder that holds pe, head, base and the three scripts. The block leaves out the -q flags I gave git clone, git fetch and git worktree add, and the redirections that sent setup output to log files.

curl -sI https://huggingface.co/api/datasets/policyengine/populace-us | head -1   # the proxy's CONNECT line
curl -s -o /dev/null -w '%{http_code}\n' https://huggingface.co/api/datasets/policyengine/populace-us   # 200
git clone https://github.com/PolicyEngine/policyengine-us.git pe && cd pe
git fetch origin pull/10582/head:pr-10582 pull/10561/head:pr-10561
HEAD_SHA=$(git rev-parse pr-10582); BASE_SHA=$(git merge-base origin/main $HEAD_SHA); echo $HEAD_SHA $BASE_SHA
git diff $BASE_SHA $HEAD_SHA -- pyproject.toml uv.lock   # empty
git worktree add --detach ../head $HEAD_SHA
git worktree add --detach ../base $BASE_SHA
git show pr-10561:policyengine_us/tools/calculate_chunked.py > ../calculate_chunked.py
cd ../head && uv sync
.venv/bin/python -c "from huggingface_hub import hf_hub_download; print(hf_hub_download(repo_id='policyengine/populace-us', filename='populace_us_2024.h5', repo_type='dataset', revision='populace-us-2024-spm-20260909'))"
sha256sum "$(readlink -f <the path printed above>)"
apt-get install -y -q time || (apt-get update -q && apt-get install -y -q time)   # the first install could not locate the package
cd ..
nohup setsid bash run_side.sh base 8 > out_base_runner.log 2>&1 < /dev/null &
nohup setsid bash run_side.sh head 8 > out_head_runner.log 2>&1 < /dev/null &   # after the base side finished
cd pe; ../head/.venv/bin/python ../compare.py ../out/base ../out/head ../out/compare.json
sha256sum "$(readlink -f <the path printed above>)"   # again after the head side: the same hash
run_side.sh

The script's first line held the work folder's absolute path; here it reads cd pe. With side set to base or head, the script runs the brief's run lines for that side.

#!/bin/bash
# Usage: run_side.sh <base|head> <chunks>. Runs the brief's command for one side.
side=$1
chunks=$2
cd pe || exit 1
mkdir -p ../out
H=$PWD/../calculate_chunked.py; PY=$PWD/../head/.venv/bin/python
export OMP_NUM_THREADS=4 OPENBLAS_NUM_THREADS=4 MKL_NUM_THREADS=4
rm -f ../out/$side.exit
date -u +%FT%TZ > ../out/$side.start
# Once a minute, record the side's RSS and peak RSS (kB) and the VM's free memory.
(
  while true; do
    pid=$(pgrep -f "^$PY \.\./pair_side\.py" | head -1)
    if [ -n "$pid" ]; then
      echo "$(date -u +%T) $(grep -E 'VmRSS|VmHWM' /proc/$pid/status 2>/dev/null | tr -s ' \t' ' ' | tr '\n' ' ')$(grep MemAvailable /proc/meminfo | tr -s ' ')"
    fi
    sleep 60
  done
) > ../out/$side.mem 2>&1 &
sampler=$!
PYTHONPATH=$PWD/../$side /usr/bin/time -v $PY ../pair_side.py $H $chunks 2026 ../out/$side > ../out/$side.log 2> ../out/$side.err
status=$?
kill $sampler 2>/dev/null
date -u +%FT%TZ > ../out/$side.end
echo $status > ../out/$side.exit
compare.json
{
 "household_net_income": {
  "entity": "household",
  "base_sum": 17161428162125.828,
  "head_sum": 17161428162125.828,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "household_benefits": {
  "entity": "household",
  "base_sum": 2059793926050.1262,
  "head_sum": 2059793926050.1262,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "ca_riv_general_relief": {
  "entity": "spm_unit",
  "base_sum": 0.0,
  "head_sum": 0.0,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "ca_riv_general_relief_property_eligible": {
  "entity": "spm_unit",
  "base_sum": 119676.02529076242,
  "head_sum": 119676.02529076242,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "ca_riv_general_relief_eligible": {
  "entity": "spm_unit",
  "base_sum": 0.0,
  "head_sum": 0.0,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "income_tax": {
  "entity": "tax_unit",
  "base_sum": 2595105852713.508,
  "head_sum": 2595105852713.508,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 },
 "state_income_tax": {
  "entity": "tax_unit",
  "base_sum": 578044599346.724,
  "head_sum": 578044599346.724,
  "change": 0.0,
  "records_changed": 0,
  "weighted_changed": 0.0,
  "weighted_up": 0.0,
  "weighted_down": 0.0,
  "largest_abs": 0.0,
  "rows": []
 }
}

Generated by Claude Code

@MaxGhenis
MaxGhenis merged commit bd1d774 into main Nov 3, 2026
34 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#10582 at faec8bd7d846ef5e2ca41fd6216cb6b21132c344 (squash): Count a Riverside County General Relief unit with $500 of countable property as within the property limit.

  • Review: an independent cloud review at 9fb39dd72d approved with one note (review). The hub applied it at faec8bd7d8, where Case 4 of ca_riv_general_relief_countable_property_value.yaml now asserts property eligibility at $500. No formula or parameter changed after the review.
  • Source conflict: for us-ca-riv:general-relief-property-boundary, we read the property bullet in the DPSS fact sheet, "Total combined property cannot exceed $500", as the specific rule (fact sheet). We apply it over the sheet's general lead-in, "Being below the income and property limits set by Riverside County", so a unit with $500 meets the limit.
  • Gates the hub checked live at merge:
    • gh pr checks exits 0;
    • the latest Pull request run at the head concluded success;
    • MERGEABLE;
    • not a draft;
    • no reviewer's latest review requests changes;
    • the semantic-overlap guard;
    • head pinned.
  • Impact (pair at faec8bd7d8 against main f8142b0223): no output moves in 2026. No Riverside SPM unit in the default dataset holds from $500 to $500.005 of countable property, where the new limit test and the old one disagree. In 2026, 20 records (119,676 weighted SPM units) meet the property limit on both sides, and none receives General Relief.
  • Axiom: Riverside County General Relief property limit queued (TheAxiomFoundation/rulespec-us#1983)

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis deleted the riv-gr-property-boundary-500 branch November 3, 2026 10:47
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