Repository navigation
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The dataset guard rejected only the calculator's FORMULA_OWNED_INPUTS, so a dataset supplying in_poverty, in_deep_poverty, deep_poverty_line, deep_poverty_gap or person_in_poverty was accepted unchanged. Those are country-side functions of the same measurement, so the documented poverty calculation and spm_unit_is_in_spm_poverty could publish different poverty rates for one population. Add DERIVED_POVERTY_OUTPUTS beside the calculator contract and reject the union. A closure test asserts no further pure function of the poverty chain can be stored. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
DEFAULT_DATASET pins a Hugging Face tag, which is a mutable pointer: re-tagging the repository, or a truncated or substituted download, would hand the model another schema-valid H5 and change every standalone-country result with nothing rejecting it anywhere. Record the certified build's sha256 beside the URI and check it after resolution, for the country default only - an explicit dataset= argument names the caller's own artifact and stays the caller's responsibility. A mismatch raises with the URI, the resolved path, and both hashes. The ~830 MB digest is memoised on (path, size, mtime), so a default Microsimulation pays it once per process rather than a third of a second per construction. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The runner decided a batch by parsing pytest's summary line: with no "N failed" count it called the batch passed, then terminated the child without reading its exit status. pytest prints no failure count for an errored run, so a real child producing "1 passed, 1 error in 0.01s" and exiting 1 was reported as passing, and make test-yaml-* exited 0 on it. Reproduced end to end against both revisions: old status='passed' returncode=None, new status='failed' returncode=1. Wait for the child to exit after it reports, and read its status. Only 0 passes. A child that stops responding is killed and reported rather than credited a pass, and a summary that claimed no failures alongside a nonzero exit is called out by name in the batch log. Exit 5 (no tests collected) is a failure here, unlike in run_selective_tests.py: that runner selects paths from a changed-file list, where a changed helper legitimately collects nothing, while this one enumerates the YAML suite itself, so an empty batch means the suite or the enumeration is broken. Every batch line now carries the exit status as release evidence. Enumerate both suffixes policyengine-core collects. Listing only "*.yaml" would drop a .yml test from any file-level batch - the heavy-subdirectory split among them - while the batch it belonged to still reported green. No .yml test files ship today, so this changes no current batch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
gov.states.ca.cpuc.income_sources counted spm_unit_capped_housing_subsidy: the Census SPM's housing subsidy capped at the housing portion of the SPM threshold less the tenant payment. That is a poverty-measurement construct, and it made a utility discount depend on which SPM geography a request selected. A Los Angeles renter earning $40,700 with $23,790 of housing assistance had a $0 capped subsidy under national selection and $310.88 under county selection, moving countable income from $40,700 to $41,010.88, across the CARE limit, and flipping ca_care_eligible from true to false. Requesting CARE with no SPM configuration raised SPM_GEOGRAPHY_REQUIRED, though no SPM output had been asked for. The parameter's own cited source does not support the cap. PG&E Form 01-9077, the CARE/FERA application, footnotes its income tables: "Total gross annual household income includes all taxable and non-taxable revenues from all people living in the home, from whatever sources derived, including, but not limited to, wages, salaries, interest, dividends, spousal and child support payments, public assistance payments, Social Security and pensions, housing and military subsidies, rental income, income from self-employment and all employment-related, non-cash income." The same footnote appears on the CPUC-filed tariff sheet (Revised Cal. P.U.C. Sheet No. 61313-E). Both were fetched and read; neither contains any netting, cap, tenant-share or poverty-measurement language. Count housing_assistance instead, and replace the dead berkeleyca.gov mirror with PG&E's own URL and the tariff sheet. Countable income for that renter is now $64,490 under national selection, county selection and no SPM configuration alike, and computes with no county input. Three contract tests pin that, all failing on the old parameter, and two YAML cases pin the arithmetic. The partner CARE and FERA fixtures pass unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Waiting for the batch subprocess to exit turned every large green batch red. policyengine-core takes tens of seconds to tear down once pytest.main() returns, freeing the tax-benefit systems the run cached: measured here, a single small state took 18.2s to exit after printing its summary, and the first two state batches of make test-yaml-no-structural-states blew through a 30s grace and were reported as killed by signal 15 despite 1,708 and 2,177 passing tests. Terminating the child promptly is why the previous code never had a status to read in the first place. Ask for the status where it is both known and free instead. The command now runs through a shim that writes pytest's return value to stdout the instant pytest.main() hands it back, before any teardown; measured, that marker arrives in the same read as the summary line, so it costs no wall clock. The runner reads the marker, terminates the child exactly as promptly as before, and decides the batch on the reported status. A child that dies before reporting is judged by its process status, which is what an OOM kill or an import error leaves behind. The reproduction still holds against both revisions: for a child printing "1 passed, 1 error" and finishing 1, old status='passed' returncode=None, new status='failed' returncode=1. A new end-to-end case runs the real policyengine-core against an empty directory and asserts the runner reports pytest's exit 5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Three findings from an adversarial review of this branch. The batch runner's monitoring loop is unbounded, and was before this branch too: the 30-minute budget sits below the loop, reachable only once the child has already exited, so the wait it guards returns instantly and its TimeoutExpired never fires. Reading the status from the runner rather than terminating a second after the summary removed the only bound run_batch ever enforced in practice, leaving pytest's session-finish window open as well. Enforce both inside the loop. A/B on a child that prints a summary and then sleeps: the previous revision was still running after 40s, this one returns with a status. batch_status's docstring claimed pytest exit 2 prints no summary count. It does. Under policyengine-core's argv, a session whose second test raises KeyboardInterrupt exits 2 beneath "1 passed in 0.10s" - measured - and the old parse read that as success by matching the summary, not by falling through to a status it had discarded. Corrected; it is the clearest example of the defect. _static_variable_reads discarded any variable whose class body held no direct entity call. That dropped 884 of the system's 6,173 variables, among them poverty_line, which is `adds = ["spm_unit_spm_threshold"]` and no formula at all - a live member of the chain the guard protects. The closure test was blind to the idiom the next alias would most likely use, so "no further alias can be stored" claimed more than it checked. Trust a formula-less variable's adds/subtracts, and the helpers that name their reads as string literals: 5,665 variables now resolve, the whole poverty chain among them, and the closure still passes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An adversarial review found the entry counts a voucher the household may not hold. `housing_assistance` is not a report of receipt: is_eligible_for_housing_assistance is `receives_housing_assistance | (is_renter & is_income_eligible)` and takes_up_housing_assistance_if_eligible defaults to true, so a California renter sending receives_housing_assistance: false is still modelled as holding one. Reproduced: a 40-year-old Los Angeles renter, 2026 earnings $35,000, rent $24,000, no voucher, is modelled with $11,580 of assistance, which takes CPUC countable income from $35,000 to $46,580 and ca_care from $585 to $0. That is how the rest of the list already works - snap, tanf, ssi and wic are this model's amounts, not an applicant's reported ones - and the entry this replaced consulted the same modelled amount, merely capped. But the cap incidentally suppressed it: that household's capped subsidy is $0. So the change is real and user-visible, and it deserves to be a named decision rather than an incidental one. Add a test that pins it with the mechanism spelled out, so a reviewer who wants the entry gated on receipt has something to change, and say "the modelled housing subsidy" in the parameter comment and changelog rather than "the housing subsidy the household receives", which the variable does not mean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`gov.states.ca.cpuc.income_sources` counted `housing_assistance`, which is eligibility times take-up rather than a report of receipt: `is_eligible_for_housing_assistance` is `receives_housing_assistance | (is_renter & is_income_eligible)` and `takes_up_housing_assistance_if_eligible` defaults to true in the household path, so every income-eligible California renter who supplied a rent was modelled as holding a voucher and carried it into CARE/FERA income. A Los Angeles renter earning $35,000 against $24,000 of rent, sending `receives_housing_assistance: false`, picked up $11,580 of imputed assistance and lost the discount. PG&E Form 01-9077 asks an applicant what the household receives, so the entry now counts receipt: `reported_housing_assistance` is `housing_assistance` gated on `receives_housing_assistance`, an input defaulting to false. The form's valuation still holds for a household that reports the voucher - the subsidy counts in full, with no netting against rent and no cap. Drops PROGRESS.md; lane bookkeeping does not ship. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An adversarial review of the receipt gate raised no defect in it, but three things it could not tell from the files: that `reported_housing_assistance` is the model's amount gated on a reported flag rather than an amount the applicant reports, so a caller who sends `housing_assistance` without `receives_housing_assistance: true` has nothing counted; that `receives_housing_assistance` is already this model's report of Section 8 receipt, read by FCC Lifeline, MA LIHEAP, CT TFA, RI Works and Orange County general relief; and which of the two housing variables a new consumer wants - `housing_assistance` carried the documentation string "Housing assistance". Also pins the worked example in the parameter comment to its inputs: $11,580 is the payment standard the model derives for Los Angeles County, $13,500 the supplied $36,000 standard the contract test uses, and the comment now says so rather than reading as one household with two answers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9464 +/- ##
===========================================
- Coverage 100.00% 92.30% -7.70%
===========================================
Files 17 2 -15
Lines 264 26 -238
Branches 10 1 -9
===========================================
- Hits 264 24 -240
- Misses 0 1 +1
- Partials 0 1 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
Author
|
Superseded by #9467, which carried this work to main (see its provenance table). |
MaxGhenis
added a commit
to DTrim99/policyengine-us
that referenced
this pull request
Sep 15, 2026
From PolicyEngine#9448, reduced. General household benefits, CBO means-tested transfers and the three Tlaib household-benefit overrides now add actual `housing_assistance` instead of the Census SPM's capped housing subsidy, so a change to an SPM threshold no longer moves a non-SPM result. SPM resources keep the capped valuation, now allocated across SPM units by member share. CARE/FERA countable income excludes housing subsidies from the August 14, 2014 parameter date of CPUC D.14-08-030; annual values resolve at January 1, so calendar 2014 retains the earlier modelled inclusion (using the actual benefit) and calendar 2015 first applies the exclusion. Also carries PolicyEngine#9448's release-candidate wheel tooling and the isolated build backend pin. Dropped from PolicyEngine#9448: its `spm.py` and `system.py` isolation hunks (PolicyEngine#9463 owns the mechanism), its default-dataset hash and batch-runner hunks (PolicyEngine#9464 owns those), its `docs/usage/microsimulation.md` edit (PolicyEngine#9463's is more specific), its `reforms/reforms.py` change (no consumer) and its `tests/core/test_spm_system.py` edits (they invert PolicyEngine#9463's contract). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MaxGhenis
added a commit
to DTrim99/policyengine-us
that referenced
this pull request
Sep 15, 2026
…tatus From PolicyEngine#9464, reduced. `REJECTED_DATASET_INPUTS` extends the calculator's `FORMULA_OWNED_INPUTS` with the country-side poverty aliases (`in_poverty`, `in_deep_poverty`, `deep_poverty_line`, `deep_poverty_gap`, `person_in_poverty`) and with the two SPM housing-allocation outputs PolicyEngine#9448 adds, so a dataset can no longer store a formula-owned measurement under a name the calculator does not police. `DATASET_SOURCE_INPUTS` keeps `is_spm_independent_minor_role` declarable by a producer, and the two sets are asserted disjoint. `DEFAULT_DATASET_SHA256` pins the certified build the mutable Hugging Face tag must resolve to, verified once per process and only for the country default; an explicit `dataset=` argument remains the caller's own artifact. The batch runner now decides a batch on the child's real exit status, reported through a marker written before interpreter teardown, and enumerates `.yml` alongside `.yaml` everywhere it lists files. Dropped from PolicyEngine#9464: `reported_housing_assistance` and the CPUC entry that counted it, plus the three tests that pinned housing assistance as countable - CPUC D.14-08-030 excludes housing subsidies outright, so there is nothing to gate on receipt and the variable had no other consumer. Its citation research is folded into the CPUC parameter instead. Adaptations: the dataset guard is grafted onto PolicyEngine#9463's `set_input`; `test_spm_system.py`'s import block is the union of both PRs' needs; `test_care_eligibility_is_the_same_under_every_spm_geography` expects `(40_700, True)` because $40,700 of countable income is inside the 2024 one-person CARE limit of $40,880 once the subsidy is excluded; and the two counting non-vacuity tests are replaced by their exclusion analogues so the suite still fails if the housing entry is silently dropped rather than excluded. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MaxGhenis
added a commit
to DTrim99/policyengine-us
that referenced
this pull request
Sep 15, 2026
Folds PolicyEngine#9464's citation research into PolicyEngine#9448's dated exclusion list. The ~25 non-housing entries regain a source: PG&E Form 01-9077 (Rev. 6.26) at PG&E's own URL, plus Revised Cal. P.U.C. Sheet No. 61313-E (Electric Sample Form 79-1055, Advice 7905-E, effective June 1, 2026), which carries the same income footnote as a CPUC filing. Both are annotated as being corrected: PG&E's June 5, 2026 reply brief in A.26-01-003 states it does not use housing and military subsidies in its income analysis and will revise the application. The dead bha.berkeleyca.gov href is gone; all nine references return HTTP 200. Records review item R8: FERA coverage of the exclusion rests on the 2026 brief, not on D.14-08-030. Verified first-hand against the decision PDF - it contains zero case-sensitive occurrences of "FERA", "Family Electric" or "739.12", while the brief's Section B answers the income definition under Public Utilities Code 739.1(a) and 739.12(a) for CARE and FERA alike. Verified against the live parameter tree: 2013-01-01, 2014-01-01 and 2014-08-13 resolve to 27 entries including housing_assistance; 2014-08-14, 2015-01-01 and 2026-01-01 resolve to 26 without it, the only set difference being housing_assistance, and all 27 names resolve to model variables. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four fixes from an external review of the SPM integration on
upstream/main, plus three more found by an adversarial review of this branch.1. Count housing subsidies as the CPUC form does
gov.states.ca.cpuc.income_sourcescountedspm_unit_capped_housing_subsidy— the Census SPM's housing subsidy capped at the housing portion of the SPM threshold less the tenant payment. That is a poverty-measurement construct, and it made a utility discount depend on which SPM geography the request selected.Reproduced. A Los Angeles renter, 2024 earnings $40,700, rent and PHA payment standard $36,000, receiving housing assistance. Changing only
spm.geography_kind:ca_care_eligibleRequesting CARE with no SPM configuration raised
SPM_GEOGRAPHY_REQUIRED, though no SPM output had been asked for.The source does not support the cap. PG&E Form 01-9077, the CARE/FERA application, footnotes its income guideline tables:
— PG&E Form 01-9077, CARE/FERA Program Application (Rev. 6.26). The same footnote appears on the CPUC-filed tariff sheet, Revised Cal. P.U.C. Sheet No. 61313-E (Electric Sample Form 79-1055, Advice 7905-E, effective 1 June 2026). Both PDFs were fetched and read; grepping either for
net of|capped|cap the|tenant share|rent portion|poverty measure|deduct|minus|offsetreturns nothing substantive. The form names the category and stops — it never says "gross" or "amount received", and it states no valuation rule at all.So the entry becomes
housing_assistance. Countable income for that renter is now $64,490 under national selection, county selection and no SPM configuration alike, and computes with no county input. The deadbha.berkeleyca.govmirror (DNS no longer resolves) is replaced by PG&E's own URL plus the tariff sheet.Please weigh this before merging.
housing_assistanceis not a report of receipt:is_eligible_for_housing_assistanceisreceives_housing_assistance | (is_renter & is_income_eligible)andtakes_up_housing_assistance_if_eligibledefaults to true. A 40-year-old Los Angeles renter, 2026 earnings $35,000, rent $24,000, sendingreceives_housing_assistance: false, is modelled with $11,580 of assistance, taking CPUC countable income from $35,000 to $46,580 andca_carefrom $585 to $0. That is consistent with how the rest of the list already works —snap,tanf,ssiandwicare this model's amounts, not an applicant's reported ones — and the capped entry consulted the same modelled amount. But the cap incidentally suppressed it (that household's capped subsidy is $0), so the change is user-visible. It is pinned intest_cpuc_counts_modelled_housing_assistance_for_a_non_recipient_renterso anyone who wants the entry gated on receipt has a named test to change.In microsimulation the change is immaterial — the effect above is specific to the household/API path. Measured over the certified default build (2026, weighted, CA), running the parameter file swapped on disk in three separate processes:
ca_care_income_eligibleca_care_eligiblehousing_assistance(this PR)spm_unit_capped_housing_subsidy(before)Income eligibility is unchanged to the person, and mean countable income moves $10. The certified build supplies
receives_housing_assistanceandtakes_up_housing_assistance_if_eligibleas inputs, so take-up is not imputed there, and for genuinely assisted units the SPM cap seldom binds — the capped figure was already close to the full subsidy. Dropping the entry altogether would move income eligibility by 31,286 (+1.15%), so the entry does carry weight; it is the valuation that microsimulation barely notices.Other consumers of
spm_unit_capped_housing_subsidy, left as they are. The form says nothing about any of them.spm_unit_benefits→spm_unit_net_incomehousehold_benefits(gov.household.household_benefits) →household_net_incomegov.household.cbo_means_tested_transfers→ CBO household incometx_rebatereforms2. Reject derived poverty aliases from stored datasets
The dataset guard rejected only the calculator's
FORMULA_OWNED_INPUTS, so a dataset supplyingin_poverty,in_deep_poverty,deep_poverty_line,deep_poverty_gaporperson_in_povertywas accepted unchanged, and the documented poverty calculation could publish a different rate fromspm_unit_is_in_spm_povertyfor the same population.DERIVED_POVERTY_OUTPUTSnow sits beside the calculator contract and the union is rejected, with a closure test asserting no further pure function of the poverty chain can be stored.3. Verify the default dataset resolves to the certified build
DEFAULT_DATASETpins a Hugging Face tag, a mutable pointer: re-tagging, or a truncated download, would hand the model another schema-valid H5 and change every standalone-country result with nothing rejecting it.DEFAULT_DATASET_SHA256now sits beside the URI and is checked after resolution — for the country default only, since an explicitdataset=names the caller's own artifact. A mismatch raises with the URI, the resolved path and both hashes. The ~830 MB digest is memoised on (path, size, mtime), so a defaultMicrosimulationpays it once per process.4. The batch runner's exit status is authoritative
test_batched.pydecided a batch by parsing pytest's summary line: absent an"N failed"count it called the batch passed, then terminated the child without reading its status. pytest prints no failure count for an errored run, so1 passed, 1 errorwith child exit 1 was reported as passing andmake test-yaml-*exited 0 on it. An interrupted run is worse: with policyengine-core's argv, a session whose second test raisesKeyboardInterruptexits 2 beneath1 passed in 0.10s— measured — which the old parse also scored a pass.The status cannot simply be waited for. Once
pytest.main()returns, interpreter teardown frees the tax-benefit systems the run cached, and that is slow: measured here, one small state took 18.2s to exit after printing its summary, and the large state batches took over 30s. Waiting turned every big green batch red. So the command now runs through a shim that writes pytest's return value to stdout the instant it has one, before teardown — measured, that marker arrives in the same read as the summary line, so it costs no wall clock. The runner reads the marker, terminates the child exactly as before, and decides the batch on the reported status. A child that dies before reporting is judged by its process status, which is what an OOM kill leaves behind.Exit 5 (no tests collected) is a failure here, unlike in
run_selective_tests.py: that runner selects paths from a changed-file list, where a changed test helper legitimately collects nothing, while this one enumerates the YAML suite itself, so an empty batch means the suite or the enumeration is broken. Every batch line now carries its exit status as release evidence..ymlis enumerated alongside.yamleverywhere the runner lists files — policyengine-core collects both (tools/test_runner.py:413), so listing only*.yamlwould drop a.ymltest from any file-level batch, the heavy-subdirectory split among them, while the batch still reported green. No.ymltest files ship today, so no current batch changes.Found reviewing this branch
batch_status's docstring claimed pytest exit 2 prints no summary count. It does (above). Corrected._static_variable_readsdiscarded any variable whose class body held no direct entity call — 884 of the system's 6,173, among thempoverty_line, which isadds = ["spm_unit_spm_threshold"]and no formula at all, a live member of the chain the guard protects. The closure test was blind to the idiom the next alias would most likely use. It now trusts a formula-less variable'sadds/subtractsand the helpers that name their reads as string literals: 5,665 variables resolve, and the closure still passes.Verification
Every suite below was run in this worktree on Python 3.14 with the pinned
.venv; exit codes are the runner's own.tests/core+tests/unit+tests/code_healthcare/ferafixturespolicy/baseline/gov/states, 16 batches (test-yaml-no-structural-states)test-yaml-no-structural-other-partners), all five invocationspolicy/baseline/household+tests/variablespolicy/contrib/congressper-subdir +policy/reformper-filemake format(ruff format + check)Partner contract fixtures under
tests/policy/baseline/partners/**pass unchanged and were not edited (git diff upstream/main -- .../partners/is empty).Not addressed here
Astra's P1 #2 and #3 and P2 #4 and #5 — later
apply_reformmutating a shared parameter tree, a reformed clone'scalc()routing to the original, private-variable registration through sharedEntityobjects, and shared parameter trace caches — arespm.py/policyengine-core concerns outside this branch.The CPUC ESA Policy & Procedures Manual §2.2.2 lists housing subsidies among receipts not counted as household income, footnoted to D.14-08-030 OP 40. Its own scope line reads "the ESA Program", the Attachment Q footnote naming that rule is written for "the ESA and CARE Programs" jointly, and OP 40 itself could not be retrieved (docs.cpuc.ca.gov's search endpoint returns HTTP 000; the APEX proceeding page needs a browser session). So whether the exclusion reaches CARE is genuinely unresolved. This PR keeps the application form's treatment — housing subsidies count — and only corrects how they are valued. Settling the include-versus-exclude question needs OP 40.
🤖 Generated with Claude Code