Repository navigation
Unify the SPM isolation, CPUC and dataset guard fixes - #9467
Merged
Merged
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Carries PR #9463 (head d8cbd4b) as-is: detach_parameters, _rebind_method_aliases, bind_private_entities and isolate_parameter_tracing in policyengine_us/spm.py, the county FIPS text contract, the microsimulation documentation correction, and all 28 regression tests (21 isolation plus 7 county). The tools/pinned_tbs.py cache key gains the parameter tree because a shared-policy system now keeps its identity while detaching its tree; test_pinned_systems_are_rebuilt_when_their_source_tree_detaches covers it and the New York EITC and CTC variables consume it, so it is production code rather than lane bookkeeping. PR #9463's PROGRESS.md is not carried. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
From #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 #9448's release-candidate wheel tooling and the isolated build backend pin. Dropped from #9448: its `spm.py` and `system.py` isolation hunks (#9463 owns the mechanism), its default-dataset hash and batch-runner hunks (#9464 owns those), its `docs/usage/microsimulation.md` edit (#9463's is more specific), its `reforms/reforms.py` change (no consumer) and its `tests/core/test_spm_system.py` edits (they invert #9463's contract). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tatus From #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 #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 #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 #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>
Folds #9464's citation research into #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>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Found while adjudicating whether #9448's `test_spm_policy_family.py` could be carried onto #9463's mechanism, and reproduced here with public-API calls. `clone_spm_system` hands core an ordinary instance to clone through - `plain_policy_copy` -> `SharedParameterPolicy.unshared_copy()` resets `__class__` to the base class - and never put the barrier back. So `Simulation.clone()` returned a plain `CountryTaxBenefitSystem`, and `apply_reform` on anything reading that tree took the unguarded path: clone = Simulation(situation=...).clone() clone.get_branch("probe").apply_reform({SINGLE standard deduction: 100_000}) # clone's own SINGLE standard deduction moved 14,600 -> 100,000 That is the defect `SharedParameterPolicy` exists to prevent, and it contradicts `test_a_branch_reform_does_not_rewrite_its_parents_policy`, which only passed because it never cloned. `with_parameter_barrier`'s own docstring already states the rule: every simulation's system carries the barrier, because a tree it starts out owning can acquire readers later. Bounded: a clone gets its own tree, so neither the shipped module-level system nor the simulation the clone came from was reachable this way; the leak stayed inside one cloned simulation's branch family. Verified before and after. `test_cloning_a_shared_policy_system_stops_sharing_its_tree` asserted `"shared_parameters" not in cloned.__dict__`, which pinned the defective implementation rather than the invariant. It now asserts that the clone holds its own tree behind the barrier; its two substantive assertions are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
#9448's `test_spm_policy_family.py` cannot be carried whole: run unchanged against #9463's mechanism, 16 of its 27 cases fail. They split four ways. Nine assert #9448's own internals - the `_parameter_view` / `_parameter_source` / `parameters.modified` provenance protocol and `get_parameters_at_instant` - which this branch does not implement, because #9463 owns the mechanism. Six turn on `Simulation.tracer` being a plain attribute in core, so a bare `simulation.tracer = X` re-primes nothing and X records no parameter receipts. #9463 hooks the `trace` setter, `__init__`, `get_branch`, `apply_reform`, `_adopt_detached_parameters` and `clone`, but not a bare `tracer` write. No production caller does one that matters, so these are dropped rather than fixed here. Four assert that a child branch's reform propagates up to its parent and siblings. Stated plainly: that is stock policyengine-core behaviour - `Simulation.clone` hands a `clone_system=False` branch the SAME tax-benefit system object and `apply_reform` mutates it in place - so #9463 is a deliberate deviation from core here, not a restoration of it. It is still the right deviation to ship: every branch in this repository that actually mutates policy already passes `clone_system=True` (`tob_revenue_oasdi.py:42`, `behavioral_response_measurements.py:49`), while the `clone_system=False` branches only set inputs, and a formula-internal branch that silently moved its parent's answer would be order-dependent. But it is a design decision, not something core's contract compels. One depends on a `reforms/reforms.py` hunk this branch drops, which returns `None` from `create_structural_reforms_from_parameters` when no structural reform is active. That case is unreachable - `create_mi_surtax_reform` always returns a reform, so the filtered tuple is never empty - and carrying the hunk verbatim is unsafe: `CountryTaxBenefitSystem.__init__` packs the result into a reform tuple, and `CountryTaxBenefitSystem(reform=((), None))` raises `AttributeError: 'NoneType' object has no attribute 'apply'` (verified). The eleven cases kept here are the mechanism-independent remainder: a nested branch clone's inherited inputs and private reform, a construction-time structural reform detaching before it mutates parameters, the warm-cache contract for a caller-supplied system across clone hops and a reform wrapper, an at-instant view refreshing after an in-place leaf update, and a reform reading back its own edit before it returns. All eleven pass. Also pins the boundary of the barrier in `test_spm_simulation_isolation.py`: it covers the parameter tree, not the variable registry, so a variable-only reform on a shared-policy branch does still reach its parent and siblings. #9463 never claimed otherwise, but nothing recorded the limit. 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>
The review's F1: `spm_unit_allocated_housing_subsidy` prorates a household's modeled `housing_assistance` to every co-resident SPM unit, and `spm_unit_capped_housing_subsidy` masks on a positive allocated share, so `masked_policyengine_amount` now demands a classified measurement adult from units that received no award and held zero housing resource under 2.0.1. A co-resident unit whose members are all under 15, or 15 to 17 without a source-backed independence role, would fail the resource chain for the entire population array. Nothing in the suite ruled that out on the certified build: `test_default_dataset_loads_and_runs` subsamples 1,000 units. This adds a full-population probe with no subsample that computes the allocation, the measurement adult count and `spm_unit_net_income` for 2024, asserts the resource chain completes, and reports the overlap count in the assertion message so a nonzero result names how many units are affected. The same probe runs over `enhanced_cps_2024`, already exercised by this suite and the one population file known to carry units that classify no measurement adult. Only the allocation is probed there: its threshold fails closed on those 18 lone minors whatever the allocation does. No guard is added here. Whether the allocation should be restricted to units that classify an adult is a decision for a reviewer holding the count. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The review's F2. Two of the carried changes move published numbers, and both fragments described only the mechanism. The member-share allocation shifts SPM resources between co-resident SPM units, so SPM poverty and deep poverty change for multi-unit households where any unit receives housing assistance - and only for those; a single-unit household's allocation is its own award (`test_single_unit_allocation_preserves_program_amount`). An awarded family's tenant payment can now zero a co-resident unit's housing resource (`test_independent_unit_caps_do_not_redistribute_unused_housing_value`), which the fragment names as an assumption. Counting actual `housing_assistance` in `household_benefits` and `cbo_means_tested_transfers` raises household benefits, household net income, marginal tax rates and CBO transfer aggregates for every assisted household by whatever the SPM cap withheld, without moving SPM poverty, and CARE/FERA countable income loses the housing entry from 2015 for annual calculations. Both are retyped `changed` rather than `fixed`: they are not defect repairs to a caller, they are moved numbers. No `breaking` fragment is added. `spm-supplied-policy-caches.fixed.md` returns, dropped from #9448 while `test_supplied_prepared_system_retains_warm_policy_children` and its neighbours were carried. It describes the behaviour this branch ships, not #9448's mechanism for it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The review's F3: `DEFAULT_DATASET_SHA256` was the only occurrence of that digest in the repository, and both tests that exercise the check substitute their own value for it, so an edited constant would pass a green suite and every default `Microsimulation()` would then fail closed against bytes nobody could compare with a record. `docs/spm.md` now carries the hash beside the URI it belongs to, with the date it was read from the producer's build, and states where the documented value meets the published bytes: `Microsimulation()` digests whatever the tag resolves to, so the microsimulation CI job that downloads the tag and constructs the default is the check against the Hub. `test_documented_default_build_matches_the_shipped_constants` reads the URI and the hash back out of that section and compares them with the shipped constants, with no monkeypatching, so editing one side alone fails. It skips outside a source checkout, where `docs/` is not present; deleting the section inside one fails. The artifact itself was not downloaded here. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The review's F4. `docs/spm.md` said the published wheel requires exactly `spm-calculator==1.0.0`; `pyproject.toml` requires `>=1.0.0,<=1.0.0.post1`, the range #9466 opened so a pinned development consumer resolves either version, with `uv.lock` pinning 1.0.0. `.github/release-lock.md` said the country lock was intentionally blocked until `spm-calculator==1.0.0` reached PyPI. It reached PyPI on 2026-09-11 and the committed lock has resolved it from `https://pypi.org/simple` with sdist and wheel hashes since; the committed-lock check passes. That passage now says so, and records that the guard validates sources, URLs and hashes and never a version string - `.github/release_lock.py` contains no calculator reference, so nothing there enforces the requirement `pyproject.toml` and review enforce. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The review's F6. The pre-2014-08-14 list counts `housing_assistance`, which is modelled rather than reported: HUD's payment where the unit is eligible - `receives_housing_assistance | (is_renter & is_income_eligible)` - times a take-up flag whose default is true. 2.0.1 counted the SPM-capped valuation in this list, and the modelled amount is at least that, so countable income rises for years before the decision and a household near the limit can lose CARE or FERA eligibility there. The parameter already flagged the inclusion as unrevalidated without saying which quantity it now counts, and the fragment said the same. Both say it now. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The review's F5. Both were run against this branch's mechanism before deciding:
test_cached_formula_trace_binds_replacement_tracer[False] FAILED
test_cached_formula_trace_binds_replacement_tracer[True] FAILED
test_system_parameter_lookup_does_not_cache_tracer FAILED
One root cause, observed directly. Core memoises the
`TracingParameterNodeAtInstant` in the root parameter node's
`_at_instant_cache`, bound to whichever tracer built it. #9463 isolates a
traced request by giving it its own root with an empty at-instant cache, keyed
on the `trace` setter; replacing a tracer in place afterwards - a bare
`simulation.tracer = FullTracer()`, or a `parameters.tracer` write on a system -
reaches none of that, and core's per-formula soft recast in
`Simulation._run_formula` rewrites only the root's own `trace`/`tracer` fields,
not the memo. So the second tracer records nothing: for the system-lookup case,
iteration 0 records one parameter and iteration 1 records zero, with the access
going to the first tracer's closed tree.
Passing them means rebinding that memo at lookup, which is #9448's mechanism
rather than this one, so they are not forced. The boundary is recorded on
`test_traced_simulations_record_their_own_parameter_accesses`, which covers the
supported path, with the note that the only bare tracer write left in this
package is `tools/branched_simulation.py`, which nothing constructs.
The policy-family docstring said #9463 reverses child-to-parent propagation,
full stop. It reverses it for parameter reforms; a variable-only reform on a
shared-policy branch still reaches its parent and siblings, and
`test_a_branch_variable_reform_is_not_isolated_from_its_parent` pins that. The
correction also records what the dropped cases actually carried: #9448's
`test_child_reform_updates_warm_parent_and_sibling` parametrizes a parameter
dictionary against a root-replacing `modify_parameters` reform, so those cases
are contradicted rather than merely duplicated, and the drop stands.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mory The Full Suite microsimulation job was killed twice at this test: the full-population allocation and measurement-adult arrays are the fact the probe exists to establish, but computing the whole SPM resource chain on top of them, in a process that has already loaded three other populations, exceeds the runner. The count stays full-population; the chain-completion check now runs on a 10,000-unit subsample after the arrays are released. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
September 15, 2026 04:02
This was referenced Sep 15, 2026
Closed
This was referenced Oct 6, 2026
MaxGhenis
added a commit
that referenced
this pull request
Oct 7, 2026
PROGRESS.md was the working log for the second-pass review of #9467 ("Second-pass review work on #9467"). Its own text says it is "Removed before the branch is handed back", but the last commit that touched it (0925187, "Record second-pass verification results", 2026-09-14) left it in place, and it reached main with the #9467 merge (5115fb8). Nothing references the file: git grep finds no mention, MANIFEST.in and the hatch wheel config include only policyengine_us, and no workflow or Makefile target reads it. No code or data change. Co-authored-by: Claude Opus 5.5 <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.
One candidate composed from four overlapping SPM drafts, so the pieces that
belong together can be reviewed and released once instead of four times.
Base:
upstream/main@2cc6f045ab(version 2.1.0). Source heads, each readwith
gh pr view <n> --json headRefOid:9f9510b735b3b9b2e375854d66fd315202e31d35d8cbd4b7a0b292beafad37cfc72813e1d710fc3e8f74a5152d1e52c33bd7a7cba6340346bd7fe004cafcd9443df7f638a5ad547a92c6e0bcfd784fbb47e94fa491bbf65ae4b6bd5667f054445b26de36Supersedes #9463, #9464 and the reduced parts of #9448; #9462 (measurement universe) is deliberately not included and stays gated on Microcosm #925.
Where each hunk came from
spm-calculatorversion range,uv.lock, fragmentspm.py(detach_parameters,_rebind_method_aliases,bind_private_entities,isolate_parameter_tracing)tools/pinned_tbs.pycache key,docs/usage/microsimulation.md,test_microsim.pytest_spm_simulation_isolation.py(22 tests) +test_spm_system.pyisolation additionshousing_assistancetest_cpuc_spm_independence.py,test_spm_measurement_boundary.py,test_spm_housing_allocation.py,test_spm_source_input_contract.py, CPUC YAML cases, Tlaib/household fixtures.github/release_build.py,release-lock.md,tests/test_release_build.py,pr.yaml,push.yaml,[build-system]pinstest_spm_policy_family.pyREJECTED_DATASET_INPUTSderived-poverty-alias guard inset_inputDEFAULT_DATASET_SHA256+_verify_default_datasetand its four testsBATCH_EXIT_MARKER,batch_status) and its suite.ymlenumerationtest_batched_per_subdir_budget.py(incl.test_auto_path_routes_keep_root_yml_files)What was dropped, and why
spm.pyandsystem.pyisolation hunks — Isolate shared policy from later reforms and repair clone routing #9463 owns themechanism. Keep SPM valuation out of program benefits and isolate simulation policy #9448's
system.pycontains nothing else; its default-datasethash is superseded by Count housing subsidies as CPUC does, and tighten the dataset, default and batch guards #9464's.
test_batched_process_results.py— written against a
timeout_seconds=API Count housing subsidies as CPUC does, and tighten the dataset, default and batch guards #9464 does not have, and one testasserts behaviour Count housing subsidies as CPUC does, and tighten the dataset, default and batch guards #9464 deliberately inverts.
test_spm_system.pyedits — three invert Isolate shared policy from later reforms and repair clone routing #9463's contract(
policy.parameters is not system.parameterswhere Isolate shared policy from later reforms and repair clone routing #9463 pinsis), fiveduplicate
test_spm_simulation_isolation.py, one pins the dropped hash path.docs/usage/microsimulation.mdedit — Isolate shared policy from later reforms and repair clone routing #9463's is more specific(names the test, the 18 units, and
use_weights=False).reforms/reforms.pyhunk — returnsNonefromcreate_structural_reforms_from_parameterswhen no structural reform isactive. Unreachable (
create_mi_surtax_reformalways returns one), andunsafe as written:
CountryTaxBenefitSystem(reform=((), None))raisesAttributeError: 'NoneType' object has no attribute 'apply'(verified).reported_housing_assistance, its CPUC entry, its YAML test, itschangelog fragment and the three tests that pin housing assistance as
countable — the exclusion verdict below leaves nothing to gate on receipt, and
the variable had no other consumer anywhere in the tree.
The CPUC contradiction: exclude
#9448 and #9464 disagreed. #9464 counted housing subsidies, citing PG&E Form
01-9077 and the CPUC-filed tariff sheet Revised Cal. P.U.C. Sheet No. 61313-E
(Advice 7905-E, effective 1 June 2026). Both documents are genuine and both say
what #9464 said they say. They are also outranked.
CPUC D.14-08-030 §6.2 (physical p.75), read from the PDF:
Ordering Paragraph 40(3) (physical p.124):
The decision is effective 14 August 2014 ("Dated August 14, 2014, San
Francisco, California"). A Commission decision defining income outranks a
utility sample form filed by advice letter — and the filing utility has since
told the Commission the form is wrong. PG&E's reply brief of 5 June 2026 in
A.26-01-003:
So the entry is excluded, not revalued. #9464's citation research is kept: the
~25 non-housing sources now cite Form 01-9077 (Rev. 6.26) and Sheet 61313-E,
annotated that their housing-and-military line is the line being corrected.
FERA rests on the 2026 brief, not on the decision (review item R8). Verified
directly against the PDF: D.14-08-030 contains zero case-sensitive occurrences
of "FERA", "Family Electric" or "739.12". The brief's §B answers the income
definition under Public Utilities Code 739.1(a) and 739.12(a) — 739.12(a) is
FERA — for CARE and FERA alike. That reasoning is now recorded in the parameter.
The break is keyed at
2014-08-14. Annual values resolve at January 1, socalendar 2014 keeps the earlier inclusion (using the actual benefit, not the SPM
cap) and calendar 2015 first applies the exclusion. Verified against the live
parameter tree: 2013/2014/2014-08-13 → 27 entries including
housing_assistance; 2014-08-14/2015/2026 → 26 without it, the only setdifference being
housing_assistance.The pre-2014 inclusion is carried forward from the existing model and was not
revalidated as historical law.
The housing allocation: included
It needs no new dataset input, so it goes in. Both new variables are pure
formulas over
housing_assistance,hud_ttpand SPM-unit membership, and bothare added to
REJECTED_DATASET_INPUTSso a dataset cannot supply either. I readthe released default H5 directly: its
spm_unittable has 11 columns, and theonly housing-related ones are the booleans
receives_housing_assistanceandtakes_up_housing_assistance_if_eligible. There is no housing-dollar input, sothe dollars stay country-formula-owned.
A bug found in #9463, and fixed here
clone_spm_systemhands core an ordinary instance to clone through —plain_policy_copy→SharedParameterPolicy.unshared_copy()resets__class__to the base — and never put the barrier back, soSimulation.clone()returned a plainCountryTaxBenefitSystem:That is the defect
SharedParameterPolicyexists to prevent, and it contradicts#9463's own
test_a_branch_reform_does_not_rewrite_its_parents_policy, whichpassed only because it never cloned. Live callers reach it:
tools/branched_simulation.py:19and everyget_branch(..., clone_system=True).Bounded: a clone gets its own tree, so neither the shipped module-level system
nor the simulation the clone came from was reachable this way.
Fixed by ending
clone_spm_systemwithwith_parameter_barrier(cloned), whichis what that function's own docstring already prescribes. Two regression tests
added.
test_cloning_a_shared_policy_system_stops_sharing_its_treeasserted"shared_parameters" not in cloned.__dict__, pinning the defectiveimplementation rather than the invariant; it now asserts the clone holds its own
tree behind the barrier, with its two substantive assertions unchanged.
test_spm_policy_family.py: 11 of 27 casesRun unchanged against #9463's mechanism, 16 of #9448's 27 cases fail. Nine
assert #9448's own internals; six turn on
Simulation.tracerbeing a plain coreattribute that nothing re-primes; one needs the dropped
reforms.pyhunk; andfour assert that a child branch's reform propagates up to its parent and
siblings.
On that last group, stated plainly: that is stock policyengine-core
behaviour.
Simulation.clonehands aclone_system=Falsebranch the sametax-benefit system object and
apply_reformmutates it in place. #9463 is adeliberate deviation here, not a restoration. It is still the right deviation —
every branch in this repository that actually mutates policy already passes
clone_system=True, theclone_system=Falsebranches only set inputs, and aformula-internal branch that silently moved its parent's answer would be
order-dependent — but it is a design decision, and reviewers should take it as
one.
The 11 kept cases are the mechanism-independent remainder and cover axes #9463's
suite leaves open: a nested branch clone's inherited inputs and private reform, a
construction-time structural reform detaching before it mutates parameters, the
warm-cache contract for a caller-supplied system across clone hops and a reform
wrapper, an at-instant view refreshing after an in-place leaf update, and a
reform reading back its own edit before returning.
Boundary now pinned: the barrier covers the parameter tree, not the variable
registry. A variable-only reform on a
clone_system=Falsebranch does stillreach its parent and siblings, because they share one
variablesdict. #9463never claimed otherwise, but nothing recorded it; a new test does.
Review items
R1 (fourth ECPA fixture) and R2 (the three Tlaib fixtures discriminating actual
12,000 from capped 1,000) were already fixed in #9448's head — verified, not
assumed. R8 is recorded in the parameter. R9 is covered by #9448's CPUC Cases 4
and 5 plus
test_cpuc_housing_exclusion_effective_date.Verification
Python 3.14.4, policyengine-core 3.30.2, spm-calculator 1.0.0.
ruff format --check .ruff check .pytest tests/core tests/unit tests/code_healthpolicy/baseline/gov/states/ca/cpuc)policy/contrib/congress/tlaib)policy/reform).github/tests/test_release_*.py.github/release_lock.py --committedNo population simulation was run here; the microsimulation suites run in CI.
Please look at
CandidateWheelinpr.yamlrunsrelease_lock.py --refreshon every pullrequest, twice (rc1 and final), and that step runs a live
uv lockagainst PyPIand fails closed on any root-version drift. Today that resolver step runs only at
release time in
push.yaml. This is #9448's design and is carried as-is, but itmeans a PyPI hiccup can turn every open PR red, including PRs that touch only a
YAML fixture. Worth a maintainer's call before this merges.
Review, second pass
An adversarial review of this branch requested changes on six findings. Each is
addressed below, one commit per finding. Nothing in the first-pass body above is
retracted.
F1 (high) — the housing allocation extends
SPM_COMPOSITION_REQUIREDtoco-resident non-recipient units. The allocation prorates a household's modelled
housing_assistanceto every SPM unit by member share, and the capped subsidymasks on a positive allocated share, so
masked_policyengine_amountnow demandsa classified measurement adult from units that held zero housing resource under
2.0.1. The only default-dataset microsimulation test subsampled 1,000 units, so
nothing ruled the pattern out.
test_microsim.pynow carries a full-populationprobe with no subsample over the certified default build: it computes
spm_unit_allocated_housing_subsidy,spm_measurement_adultsandspm_unit_net_incomefor 2024, asserts the resource chain completes withoutSPMInputError, and asserts that no unit holds an allocated share whileclassifying no measurement adult - with the count in the message, so a nonzero
result names how many units are affected. The same allocation probe runs over
enhanced_cps_2024, which this suite already exercises and which is the onepopulation file known to carry units that classify no adult; each assertion
names its dataset. No guard was added. Whether the allocation should be
restricted to units that classify an adult, leaving the remainder with the
awarded unit, is a decision for a reviewer holding the count.
F2 (high) — two changes move published numbers, and no fragment said so.
spm-housing-allocationandcare-housing-incomeare retypedchangedandrewritten around consequence rather than mechanism. The allocation moves SPM
resources between co-resident units, so SPM poverty and deep poverty change for
multi-unit households where any unit receives housing assistance and only for
those; an awarded family's tenant payment can zero a co-resident unit's housing
resource, named as an assumption. Counting actual
housing_assistanceinhousehold_benefitsandcbo_means_tested_transfersraises household benefits,household net income, marginal tax rates and CBO transfer aggregates for every
assisted household by whatever the SPM cap withheld, without moving SPM poverty,
and CARE/FERA countable income loses the housing entry from 2015 for annual
calculations. No
breakingfragment. #9448'sspm-supplied-policy-cachesfragment is restored, describing the behaviour this branch ships rather than
#9448's mechanism for it.
F3 (medium) —
DEFAULT_DATASET_SHA256was unverifiable inside the repo. Itwas the only occurrence of that digest in the tree, and both tests that exercise
the check substitute their own value for it.
docs/spm.mdnow has a Defaultpopulation build section carrying the hash beside the URI, with the date it was
read from the producer's build, and stating where the documented value meets the
published bytes:
Microsimulation()digests whatever the tag resolves to, so themicrosimulation CI job that downloads the tag and constructs the default is the
check against the Hub.
test_documented_default_build_matches_the_shipped_constantsreads the URI and the hash back out of that section and compares them with the
shipped constants, with no monkeypatching. The artifact was not downloaded here.
F4 (low/medium) — docs contradicted shipped metadata.
docs/spm.mdsaid thepublished wheel requires exactly
spm-calculator==1.0.0;pyproject.tomlrequires
>=1.0.0,<=1.0.0.post1, withuv.lockpinning 1.0.0..github/release-lock.mdsaid the country lock was blocked untilspm-calculator==1.0.0reached PyPI; it reached PyPI on 2026-09-11 and thecommitted lock has resolved it from
https://pypi.org/simplewith sdist andwheel hashes since. Both passages now describe what ships, and the release-lock
note records that the guard validates sources, URLs and hashes and never a
version string -
.github/release_lock.pycontains no calculator reference.F5 (low) — carried-test accounting. #9448's two tracer tests were run against
this branch's mechanism before deciding, and all three cases fail:
One root cause, observed directly: core memoises the
TracingParameterNodeAtInstantin the root parameter node's_at_instant_cache, bound to whichever tracer built it. #9463 isolates a tracedrequest by giving it its own root with an empty at-instant cache, keyed on the
tracesetter, so replacing a tracer in place afterwards reaches none of that,and core's per-formula soft recast in
Simulation._run_formularewrites only theroot's own
trace/tracerfields rather than the memo. In the system-lookupcase the first tracer records one parameter access and the second records zero.
Passing them means rebinding that memo at lookup, which is #9448's mechanism
rather than this one, so they are not forced; the boundary is recorded on
test_traced_simulations_record_their_own_parameter_accesses.The policy-family docstring is corrected: #9463 reverses child-to-parent
propagation for parameter reforms, and a variable-only reform on a
shared-policy branch still reaches its parent and siblings, pinned by
test_a_branch_variable_reform_is_not_isolated_from_its_parent. The correctionalso records that every propagation case dropped from #9448 carried a parameter
reform -
test_child_reform_updates_warm_parent_and_siblingparametrizes aparameter dictionary against a root-replacing
modify_parametersreform - sothose cases are contradicted rather than merely duplicated, and the drop stands.
F6 (low) — the retained pre-2014 CPUC inclusion counts a modelled benefit.
housing_assistanceis HUD's payment where the unit is eligible(
receives_housing_assistance | (is_renter & is_income_eligible)) times atake-up flag defaulting to true, and it is at least the capped valuation 2.0.1
counted in this list, so countable income rises for years before the decision and
a household near the limit can lose CARE or FERA eligibility there. Both the
parameter comment and the fragment now say so.
🤖 Generated with Claude Code
Full-population probe result
The whole-population probe over the certified default build (
populace_us_2024) passed in CI at head6c9d4ab5: zero SPM units hold an allocated housing share while classifying no measurement adult, so the member-share allocation raises noSPM_COMPOSITION_REQUIREDon the shipped data, and the resource chain completes on the subsample check. The same probe overenhanced_cps_2024also passed. The hosted runner was killed twice while the probe computed the full-population resource chain on top of the allocation arrays; the count is still taken at full population and only the completion check runs on a 10,000-unit subsample. All 35 checks pass.