Repository navigation
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Four simulation-isolation defects in the shared-policy path: 1. A later apply_reform mutated the shared parameter tree, so a reform on one simulation changed unrelated ones. The shared system now holds its tree copy-on-write: reform application arms a read barrier and the first read inside it clones the tree, leaving the lender's tree and warm at-instant caches untouched. Cloning a 130,000-parameter tree takes seconds, so it stays lazy: the structural reform every simulation re-applies only rebinds variables and never reads parameters. 2. Core keeps calc/df as bound methods in the instance dict and copies them verbatim when cloning, so clone.calc() calculated the original simulation under the original's policy and recorded the original's SPM receipts. clone() now rebinds every copied bound-method alias to the clone. 3. Entities resolve variable names through the system they are bound to, so a shared system's entities sent holder lookups to the lending system's registry and set_input could not find a variable a reform had added to this simulation. Shared policy now gets private entities bound to itself. 4. Core writes a traced request's tracer onto the root parameter node and caches the tracing at-instant node there, so a shared tree mixed up whose trace a parameter access belonged to. A traced simulation now gets a private root node - the same children, its own at-instant cache and trace fields - primed as traced, because _calculate reads gov.abolitions before _run_formula marks the tree and no access was recorded at all. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
county_fips is documented as a five-digit string, but core maps a str variable onto the numpy object dtype, so the model stores an integer column as integers and both readers stringify before asking the forecast provider. An integer county code for a state without a leading zero was therefore accepted silently as that county, while the identical mistake for California (6037 for Los Angeles County 06037) was reported as an absent county. The provider now records which counties the model was handed as something other than text - at the one point that still sees the type - and rejects them when a county measurement needs them, so both fail as SPM_GEOGRAPHY_REQUIRED naming the same fix, and a caller who never asks for a county is still never asked for one. is_county_fips itself now enforces the text contract for direct callers instead of stringifying whatever arrives. Hardening from the isolation change: - A shared-policy system clones through an ordinary copy, because core's clone writes the cloned tree into the instance dictionary where the copy-on-write property would shadow it. - The shared-policy class is published under its own name so systems built from it still pickle. - The pinned tax-benefit-system cache keys on the parameter tree as well as the system, since a shared system keeps its identity while detaching a private tree. Regression tests reproduce each finding: every one fails on the parent commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The weight sanity check summed a weight variable through a weighted series, which multiplies each value by its own weight: with household weights of 2 and 3 the documented snippet reports 13 households rather than 5, and on the default population it reports trillions. Read the weights unweighted, and say why, naming count() as the alternative. The legacy enhanced-CPS bullet claimed every resource output over the file fails closed. The test it points at asserts county formatting, the count of unclassified units and the threshold only; and since the capped housing subsidy began asking for the SPM housing portion only for units receiving housing assistance - which none of the 18 unclassified units does - household net income and benefits compute over the file (household_net_income sums to 6,944,802,734,080 over its 41,314 households). Say what the test checks, and assert the housing-assistance premise in the test so the rationale is enforced rather than asserted in prose. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
modify_parameters, load_extension and add_abolition_parameters all edit the live parameter tree rather than a copy of it, and none of them goes through the reform path that arms the copy-on-write barrier. Detach outright in each; it costs nothing on a system that already owns its tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Sharing a system whose parameter tree is None no longer leaves the property with nothing to read. - Two bases with the same class name get distinct published names, so pickle cannot restore one shared-policy class as the other. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The country's __init__ resumes after core's and now reads self.trace to decide whether this simulation needs its own parameter-tree root. Core's real __init__ sets it; the stand-in has to as well. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
An adversarial review of the copy-on-write design found five ways it could still hand a simulation the wrong parameter values. - A system's own history says nothing about who else holds the tree it points at, so a system that took a private copy and then lent it to a second simulation went on writing to it in place. Sharing is now recorded on the tree, one-way: a tree that has ever had a second reader is never written to in place again. Core's ParameterNode.clone copies the instance dictionary, so a fresh private clone is explicitly unmarked. - Every simulation's own system now carries the barrier, not only the ones borrowing the shipped policy, because the tree a simulation starts out owning acquires readers later - a branch copies the system, and another simulation can be built on it. - Branches were moved onto a detached tree by root identity, which a traced branch can never match because tracing gives it a shallow copy of the root. A traced simulation therefore stranded every shared-policy branch on the unreformed tree - including the branches the model's own formulas create, so a traced calculate/apply_reform/calculate sequence reported pre-reform numbers. Branches are matched on the children of their root instead, and an adopting branch keeps sharing, so its own reform detaches rather than rewriting its parent's policy. - A traced root is now only copied when the tree is shared. A shallow copy keeps the original's children, whose parents still point at the original root, so an in-place edit cleared that root's at-instant cache and left the traced copy serving pre-reform values. - Core recurses into apply_reform for each member of a reform tuple. Reading parameters in the nested call fell inside the outer call's armed window and detached unconditionally, so a tuple of variable-only reforms paid for a full tree clone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The review found the record was a monotone, unkeyed blacklist: correcting the input left the rejection in place, a period's mistake rejected every period, and a snapshot taken for a brand-new simulation carried the lender's rejection into a simulation that supplied nothing wrong. It is now keyed on (year, county) and replaced per year, so re-setting county_fips for a period re-derives that period's record from what the model holds. Only a clone - which keeps this simulation's holders - inherits it; a new simulation reads its own inputs. A reform simulation's baseline arm, which core hands a provider that never sees an input, is recorded alongside its reform arm. Nothing is recorded or scanned at all when the selection is not county-based, and each write scans only the period it wrote rather than every period known so far. The rejection is still scoped to a county code rather than a row, because the county reaching a provider has already been stringified by the variable that read the column; the message now says which county and which year. Also tighten the enhanced-CPS assisted-unit assertion from "more than none" to the count the file actually holds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The borrowing simulation sent a different county, so it passed even while a new simulation inherited its lender's rejection. Send the same county as text, which is the case the leak rejected. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Sep 15, 2026
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>
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
Carries PR PolicyEngine#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 PolicyEngine#9463's PROGRESS.md is not carried. 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
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
Found while adjudicating whether PolicyEngine#9448's `test_spm_policy_family.py` could be carried onto PolicyEngine#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>
MaxGhenis
added a commit
to DTrim99/policyengine-us
that referenced
this pull request
Sep 15, 2026
…mily tests PolicyEngine#9448's `test_spm_policy_family.py` cannot be carried whole: run unchanged against PolicyEngine#9463's mechanism, 16 of its 27 cases fail. They split four ways. Nine assert PolicyEngine#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 PolicyEngine#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. PolicyEngine#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 PolicyEngine#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. PolicyEngine#9463 never claimed otherwise, but nothing recorded the limit. 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.
Why
The Astra review of country 2.0.1 reproduced four simulation-isolation defects on the shipping runtime, plus a county input-type inconsistency and two documentation errors. Ordinary simulations deliberately share the default policy state:
share_spm_policyhands each one the shipped parameter tree, its warm at-instant caches and the shared instance's variable objects, because rebuilding a 130,000-parameter tree per household request is not affordable (a clone takes 1.5–2.0 s; a warm household build takes 0.03 s). What was shared was not protected.1 (P1). A later parameter reform contaminated every other simulation. Core's
Simulation.apply_reformcallsreform.apply(system)directly instead of constructing the reform. That bypasses the defensive parameter clone inReform.__init__and edits whatever tree the system hands it. Reproduction: two ordinary single-person simulations on $50,000 both calculate $4,016 of income tax. Aftera.apply_reform({"gov.irs.deductions.standard.amount.SINGLE": {"2024": 100_000}}), a newly built, unreformed simulation calculates $3,718, and the module-level system's ownSINGLEstandard deduction has moved from 14,600 to 100,000. The same contamination leaks between tests in one process: running this PR's tests against the old code also breaks five unrelated pre-existing tests.2 (P1). A reformed clone's
calc()calculated the original. Core keepsself.calc = self.calculateandself.df = self.calculate_dataframeas bound methods in the instance dictionary (simulations/simulation.py:223-224), and itsclonecopies that dictionary verbatim (:1381-1416). So every alias on the clone still called the original. With income tax neutralized on the clone,clone.calculate("income_tax", 2024)returned[0.0]whileclone.calc("income_tax", 2024)returned[4016.0].clone.calc("spm_unit_spm_threshold", 2025)also filed its SPM receipt in the original simulation's provenance.3 (P2). A variable a reform added could not take input. A population asks its entity for a variable before it builds a holder, and an entity resolves names through the system it is bound to. The shared system's entities still belonged to the shared instance, so
set_input("clone_only_income", 2024, [123])afterAddedInputReformraisedVariableNotFoundErrorfor a variable the simulation's own registry held.4 (P2). Traced parameter accesses were misattributed. Core writes the requesting simulation's
tracer,traceandbranch_nameonto the root parameter node and caches the resultingTracingParameterNodeAtInstantthere. On a shared tree, the second traced request reuses the first request's node. On core 3.30.2 it is worse than Astra found on 3.32.5: a traced simulation recorded zero parameter accesses._calculatereadsgov.abolitionsand caches a non-tracing at-instant node before_run_formulaever marks the tree. The shared tree was also left holding a finished request's tracer for every later reader.5 (low). The county FIPS text contract was not enforced.
county_fipsis documented as a five-digit string. Core maps astrvariable onto the numpyobjectdtype, so the model stores an integer column as integers, and both readers of the column stringify before they ask the forecast provider. As a result, integer36061was silently accepted as New York County, while integer6037(Los Angeles County06037with its leading zero dropped) was reported as an absent county. Tighteningis_county_fipsalone would have changed nothing, because no model path hands it anything but text.Documentation. The weight sanity check summed a weight variable through a weighted series, which multiplies each value by its own weight. With household weights of 2 and 3 it reported 13 households instead of 5, and trillions on the default population. The legacy enhanced-CPS bullet claimed that every resource output over the file fails closed. The test it cites checks county formatting, the count of unclassified units and the threshold, nothing else. Since the capped housing subsidy began requesting the SPM housing portion only for assisted units, and none of the 18 unclassified units is assisted, household net income and benefits do compute over the file (
household_net_incomesums to 6,944,802,734,080 across its 41,314 households).What
Changes are in
policyengine_us/spm.pyunless noted otherwise.SharedParameter<Base>subclass whoseparametersis a property. Whether anyone else reads a tree is recorded on the tree, and the mark is one-way.share_spm_policy, a branch copying its parent's system, and tracing's shallow root copy all mark it. Reform application arms the barrier. The first read of a marked tree inside that window clones it through core, which leaves the lender's tree and warm caches untouched.modify_parameters,load_extensionandadd_abolition_parameters, the three core methods that edit the live tree in place, detach outright. A variable-only reform never readsparameters, so the structural reform every simulation re-applies at its own start instant still pays nothing. Branches created withclone_system=Falseshare their parent's policy deliberately, so they follow a parent's detach, matched on the children of their root. A branch's own reform detaches rather than rewriting its parent's tree.clone()rebinds every bound-method alias it copied to the clone, by method name, so an alias core adds later is repaired as well.share_spm_policyshallow-copies the entities and binds them to the private system, keeping one object per key acrossentities,person_entityandgroup_entities.clone_spm_systemnow removes the second object per group-entity key that core's clone creates.(year, county), which counties the model received as something other than text, at the one point that still sees the type. It rejects them before its own memo when a county measurement needs them. Re-setting the input re-derives that year's record, so a corrected input goes through. Only a clone inherits the record, and it also covers a reform simulation's baseline arm. Nothing is scanned when the selection is not county-based.is_county_fipsenforces the text contract for direct callers. Anyone who never asks for a county is still never asked for one:geography_kind="national"computes over the same population.tools/pinned_tbs.pykeys its cache on the parameter tree as well as the system, because a system now keeps its identity while detaching a tree. NY's decoupled EITC and CTC would otherwise have gone on reading pre-reform federal parameters.docs/usage/microsimulation.mdis corrected, along with the docstring oftest_legacy_enhanced_cps_lacks_source_backed_spm_independence_roles. That test now asserts its premise instead of stating it in prose: none of the 18 unclassified units receives housing assistance, out of 870 assisted units.Adversarial review of this change
Before asking for review, I ran a four-lens adversarial review of the first pushed head (
c7e2f9a179), covering copy-on-write, clones and entities, tracing and county, and performance and test quality. Every raised finding was then verified independently. 27 were raised and 18 confirmed, including real regressions in that first pass:calculate → apply_reform → calculatereturned pre-reform numbers. The verifier's constructed case was off by $2,860.All 18 are fixed. Each has a test that fails against
c7e2f9a179and passes here, confirmed by running them against that head'sspm.py. Two residuals are documented rather than fixed:holder.set_inputbypasses the country'sset_input, so that one channel behaves exactly as before.Verification
Run locally on macOS with Python 3.14, policyengine-core 3.30.2 and spm-calculator 1.0.0, rebased on
upstream/mainat2cc6f045ab.uv run ruff format . && uv run ruff check .pytest policyengine_us/tests/ --ignore=…/policy/contrib --ignore=…/microsimulation(thetest-other-pythonjob; coverstests/coreandtests/unit)pytest policyengine_us/tests/microsimulation(thetest-microsimulationjob)make test-yaml-reformmake test-yaml-structuraltest_batched.py …/baseline/gov/states/ny(exercises the pinned NY systems)test_batched.py …/baseline/householdupstream/main'sspm.pytest_spm_system.pyalone: 9 failed, 45 passed, where the failures are exactly the new county and entity tests and every pre-existing test passes; the isolation file: 19 of 25 failedThe microsimulation count is one lower than before the rebase because upstream's MA CCFA revert (#9458) collapsed a two-case parametrized test into one.
Performance is unchanged for ordinary simulations. A warm household
Simulation(situation=…)build takes 0.02–0.05 s and a warmcalculateabout 0.2 s. Tests pin that an ordinary simulation, a variable-only reform and a tuple of them all keep sharing the tree. A parameter reform pays the detach once. Traced calculations are about 13% slower, because tracing now actually records parameter accesses, which it previously never did on this core.Not addressed here
These Astra findings need model or release decisions rather than runtime isolation, and are left for PR B:
.ymlfiles.🤖 Generated with Claude Code