Skip to content

Add composable Social Security benefit levers - #10400

Merged
MaxGhenis merged 66 commits into
mainfrom
ss-benefit-levers
Oct 27, 2026
Merged

MaxGhenis merged 66 commits into
mainfrom
ss-benefit-levers

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Public Social Security components compose benefit levers over reported sources. Later writes to a benefit source refresh formula-derived results at or after Core's earliest dependency number, so subsequent calculations use the surviving current source. The read-time source model preserves independent public and explicit reported inputs across loading, copying and extraction.

Design

The formulas compose joint COLA, a neutral PIA-ratio extension point, across-the-board changes and supplied payable-share factors. Historical effective/payment months, rounding and bend points remain covered. Rates and payable shares are validated; OASI-only cuts protect DI and require a fund designation for positive dependent benefits. Reserved calculated-source modes fail explicitly. Exact compound totals are retained, with separately supplied annual amounts required when annual factors differ.

Round 4 chose read-time source resolution after the eager state machine found that dataset public input → raw reported write → extraction/reload could erase the original registered source. This removes automatic twin arrays, automatic-origin/stamp maps and replay retagging. Initial public inputs remain registered Core inputs on reserved public-source branches, hidden from public formula reads so scored-year levers can apply. Later public writes remain final overrides in their own periods. Annual reported reads use an explicit twin at or before the request; otherwise they read and uprate the surviving public source. Exact compound requests retain exact-input precedence.

Source snapshots serialize actual public values and initial/final placement. Legacy origin metadata remains readable. Initial routing and later source writes follow RULING-10400-refresh-scope-2026-10-26.md, rules 1–5 and the Round 7 addendum: since is the minimum StoreHistory.earliest_dependency(name, period) over the public component and its _reported twin. Formula-derived physical arrays and fast-only calculated returns dated at or after since are eligible. A fast-only calculated return whose date was lost is intentionally treated as eligible when a cutoff exists; an unknown date cannot establish that the return predates the cutoff. since=None means no drop. Supplied inputs, raw nonderived values, physical values excluded from the fast-only rule, and derived returns numbered before since retain their protections. An unrelated formula stored at or after since can be dropped: this is Core's intended conservative approximation, not an exact dependency graph.

The bounded refresh uses Simulation._get_store_history, StoreHistory.earliest_dependency, storage get_known_branch_periods, is_derived and _sequence_numbers, and _delete_exact/Holder._evict_fast_cache. It bumps _input_epoch and taints _calculation_frames or calls _taint_waiting_frames as Simulation._drop_computed does. The whole fast cache is cleared only after an eligible physical derived array or identity-matching fast-only calculated return is dropped; an undated fast-only return counts as a drop when since is non-null. Without a drop, the adapter leaves the cache, epoch and frames unchanged. For skipped-storage returns, _cache_result / Holder._record_store captures a pending number, and a dict subclass commits that date only on actual fast-cache insertion. A helper store without insertion cannot redate a live memo. StoreHistory’s first-store record supplies the cutoff, not the current return’s date. Core's bulk Simulation.delete_arrays(name) replaces the memo dictionary while keeping other names' arrays, so an enabled adapter hook carries forward each surviving date only when the key still holds the identical array object. This retains pre-cutoff dates rather than inventing new ones. History is retained: pruning all records as Core's broader drop does would forget raw seeds that this adapter preserves. These paths were read in Core 3.33.1, installed 3.33.2 and tag 3.33.12 (037960caba89d089bb42a1ffc911afb7f0c4abb3).

A dataset rebuild detaches Core's old holders while retaining their calculation fast-cache entries. Loading therefore snapshots the old derived returns’ numbers and weak references. The same cutoff removes eligible returns still held by the fast cache; this metadata retains no old holder, population or array and no old holder changes the new input registry. Rebuild snapshots end with loading. For skipped-storage returns, the fast-cache dict subclass keeps only numeric dates as additional committed metadata, keyed by actual requested cache keys. Pending formula stores use weak simulation keys and weak array references with cleanup; the cache owner is weak. Native clone/drop paths reset the dictionary, so independent branches share no mutable metadata. Pickle preserves memo values and numeric dates, omits pending/weak ownership state, and advances Core’s sequence counter past restored dates. Capture is restricted to enabled skipped-storage policies; ordinary physical stores keep using storage metadata. Snapshot selection uses Holder._branch_storing, storage has/is_derived and sequence keys, without decoding disk arrays. The synthetic backend can memoize an exact reported-source read without creating a derived array. Native Core 3.33.2 and 3.33.12 return the tested exact source before memo insertion; real-Core tests now assert that no memo existed. The narrow reported-source eviction through Holder._evict_fast_cache remains defensive for backends that memoize that view. It does not clear unrelated fast-cache entries or bump the drop epoch when no derived value is removed.

The adapter no longer calls Simulation._drop_values_that_may_depend_on from low-level _set or from representation-only migration. Core's own Holder.set_input call remains on branch setters and retains Core's raw-seed removal behavior on that route. The preservation guarantee concerns the adapter's additional refresh; it does not change Core's own cleanup.

The Core floor remains >=3.33.1, and the committed lock retains 3.33.2. The legacy differential reference remains pinned to main 8986e05cab65d56435a23df1910e2d35f2c5f5bf; pinned-reference fetches, the three-process guard, separate JUnit output and failure propagation remain in place.

Input-route audit

The source audit covers Core 3.33.2 and 3.33.12. Their recorded-input helpers distinguish supplied inputs from derived caches and unregistered raw stores. Core 3.33.12 additionally performs overlapping-period cleanup after an ordinary Holder.set_input; that can remove a raw annual cache after a compound input is stored. The reference model follows actual source survival rather than requiring a virtual twin to remain physically stored.

Route Handling and coverage
US Simulation/Microsimulation, raw Core datasets, fresh/existing builders and YAML inputs Nested loading collects successful stores, routes initial public inputs at the outer boundary and finalizes in finally, including stores completed before an exception. The machine covers datasets, builders and YAML; country tests cover both wrappers and public-column H5 ingress.
Simulation.set_input, direct Holder.set_input, delegating setters Actual stores reach the same adapter. Public overrides and explicit twins retain their roles. Runtime refresh uses the bounded public/twin dependency cutoff described above.
_set in every is_input mode, inside/outside input contexts and with branch arguments Core determines registration and effective branch. Registered stores survive input export/replay; nonregistered, nonderived values can serve live reads while they survive Core cleanup.
Nonderived put_in_cache, raw handler stores and stores followed by an exception Raw values are live sources, excluded from input-only extraction/replay. Successful stores remain effective before the same exception is propagated; exact exception identity is checked.
Dispatch/divide helpers and custom period handlers Actual stores use the same adapter. Core's helpers.get_stored_input sees a registered initial source on its reserved branch without making that source visible to public calculations. Real-Core tests, generated machine rules and 24 new equality routes cover initial-source allocations; month/rolling compounds and partial handlers also have country-free coverage.
Derived _set/put_in_cache and formula caches Derived arrays never become fallback public inputs or explicit sources. The machine calculates reported and public values through a component base after every step.
Dump/restore Source sidecars retain public values and placement; legacy public-only dumps and older metadata remain readable. Restoration follows Core's promotion of raw values to inputs.
Clone, branch, reform replay and cache dropping Copies retain independent sources and registrations. Every retained copy is checked after subsequent operations; replay/drop follows surviving registrations.
Extraction/reload and complete source snapshots Registered inputs and final overrides round-trip with independent public source data. A raw reported write cannot erase a separately registered public source. Both routes are generated and have country regressions.
Subsampling Rebuilt source values follow retained person IDs. Country tests and the differential cover this outside the generated machine.

The compatibility adapter is restricted to the four benefit components and their reported twins. Arbitrary external storage mutation, foreign snapshot formats and all possible Core callers are outside the finite generated coverage.

Invariants and backend scope

After every generated step, on each live simulation and retained copy, reported readers must select the surviving explicit source at or before the request, or the surviving public source when fallback is allowed. Exact compound totals keep their own precedence. Public final overrides stay final, derived caches cannot become sources, and later writes cannot alter earlier copies. Registered sources and raw nonderived values are modeled separately; the model accounts for Core's own branch-setter raw cleanup. No automatic-origin map exists.

All four components are read through reported → base → public formulas with a nonneutral synthetic factor. New rules compare helper allocations with Core without hooks, retain caller/raw values and formula arrays stored before a write's cutoff, and label the post-cutoff unrelated-formula drop as intended. They compose initial inputs with divide/dispatch/default FLOW month helpers, month/rolling compounds, real calculate_add, custom and store-then-raise handlers, and physical source deletion/fallback. Existing branch-builder, restoration, extraction/reload, explicit priority, raw/derived writes, failure identity and copy rules remain.

The backend now uses Core periods, InMemoryStorage, StoreHistory, next_sequence_number, Holder._record_store, _record_inputs, _input_unchanged, _has_unnumbered_values, _evict_fast_cache, and Core's _get_store_history, _drop_values_that_may_depend_on, _drop_computed, frame context/taint functions and period helpers. It does not replace the dependency drop with pass.

Remaining substitutions (listed in the backend docstring): Holder construction, default arrays and disk-storage construction create one-person float arrays and JSON disk bytes; array conversion, validation and storage lookup now use Core. Population/entity/system/parameter descriptors, builder explicit_singular_entities, register_variable, add_person_entity, add_group_entity, add_default_group_entity, declare_person_entity, declare_entity and join_with_persons, dataset dictionary loading and entity dump serialization are fixture plumbing. Simulation construction and population lookup attach these tiny entities; the single-thread calculation entry point, acyclic cycle/period checks, formula chain and result cast replace full-country calculation setup. Macro-cache reads are disabled and one-person subsampling is unsupported. Public write/copy/replay/extraction/dump bodies, memory/input tiers and source restoration remain production Core/US paths. The fixture does not claim full-country or threaded execution coverage; standalone real-Core tests use actual TaxBenefitSystem, Simulation, periods, branches and storage without these substitutions.

Each example now generates a blacklist with opt-out caching or variables-to-drop policy over a nonempty subset of the four components’ public/base/reported names and one unrelated formula (13 names). Configuration remains deterministic: max_examples=60, stateful_step_count=25, deadline=None, derandomize=True, report_multiple_bugs=False. Amounts span zero to $48,000 for general rules; helper totals span $24,000–$48,000. All four components, annual 2023–2025, exact multiyear periods, month/rolling helper compounds and 2023–2026 reader requests remain covered. Runtime and actual generated/invalid example counts are recorded below.

Intended differences from main

The difference is scoped to a benefit source written after values were calculated. Both audited Core versions return immediately from _drop_values_that_may_depend_on for an ordinary simulation without a parent branch. Main therefore retains previously calculated benefit components or downstream totals after these writes. This PR refreshes formula-derived arrays numbered at or after the public/twin dependency cutoff for the four components' source writes, as required by the latest-source invariant and the refresh-scope ruling. Ordinary set-then-calculate use and reform/lever paths remain subject to the current-law differential and existing lever assertions.

The refresh cutoff intentionally over-approximates dependencies across holders: an unrelated formula numbered at or after since is also dropped and recalculated, as Core does on branches (test_bounded_refresh_intentionally_drops_derived_value_after_since). A formula numbered before since survives. The reviewer's cached 77 had no source dependency (since=None) and now survives untouched. A changed value from an unrelated refreshed formula would require its own earlier source change without invalidation, the same latent main-cache issue addressed by the ruling.

The shared differential probe normalizes only formula-derived arrays after the source changes. Identical cleanup runs in both checkouts; it drops no supplied input or raw nonderived seed, and no value comparison is removed. Separate intended-difference tests assert the head's fresh results before normalization, using arithmetic expectations and comments recording main's stale values. The state machine and input regressions likewise check runtime invalidation before any normalization.

Here U_y is the country model's per-capita Social Security uprating index in year y. The independent 2026/2024 ratio is (1,665,942,000,000 / 352,304,000) / (1,454,424,000,000 / 346,588,000) = 1.1268465847171525, represented as 1.1268465518951416 in float32. The 2026/2023 ratio for the compound-setter route is (1,665,942,000,000 / 352,304,000) / (1,379,268,000,000 / 342,835,000), represented as 1.1753813028335571 in float32. Values below are the Core 3.33.12 float32 results recorded for these routes. Each component is multiplied and the components are added in float32, matching Core; the two-person rows use component-by-component sums rather than multiplying a combined total.

Intended route Hand-computed fresh expectation Main's recorded stale value
extraction_rewrite_simulation social_security@2024 = 36,000 + 0 + 0 + 0 = 36,000 24,000
extraction_rewrite_microsimulation social_security@2024 = 36,000 + 0 + 0 + 0 = 36,000 24,000
complete_export_rewrite First-person social_security@2026 = 36,000 + 1,800 × U_2026/U_2024 + 600 × U_2026/U_2024 ≈ 38,704.43359375; the second person's componentwise float32 sum 6,000 × U_2026/U_2024 + 1,200 × U_2026/U_2024 + 300 × U_2026/U_2024 ≈ 8,451.3486328125 is unchanged [26,704.431640625, 8,451.3486328125]
dataset_builder_extraction_rewrite social_security_retirement@2026 = 36,000 × U_2026/U_2024 ≈ 40,566.4765625 27,044.31640625
setter_failed_after_loading Nonzero raw-cache/compound sequence: social_security_retirement@2026 = 24,000 × U_2026/U_2023 ≈ 28,209.150390625 after the setter stores and raises 14,104.5751953125, from the earlier 12,000 × U_2026/U_2023 source
dump_restore_legacy_public_override social_security_retirement@2026 = 24,000 × U_2026/U_2024 ≈ 27,044.31640625 13,522.158203125
dump_restore_both_names_override social_security_retirement@2026 = 24,000 × U_2026/U_2024 ≈ 27,044.31640625 13,522.158203125
clone First-person social_security@2026 = 24,000 + 1,800 × U_2026/U_2024 + 600 × U_2026/U_2024 ≈ 26,704.431640625; the second person's same componentwise float32 sum remains 8,451.3486328125 [16,226.58984375, 8,451.3486328125]

Retained round-4 coverage

Round 4's builder replacement and store-then-raise fixes remain: an initial builder replacement reports $24,000, and a successful store followed by an exception retains $24,000 and the original exception object. Legacy, both-name and raw dumps restore $12,000 and follow a later $24,000 public override; an explicit $6,000 twin keeps priority. Read-time resolution rejects formula caches and removed tiers. Prior head 582625618d5d88f1fec8aef7d392cc584c1d5698 passed 32/32 CI jobs in run 40292085299, including the original 50 routes/953 arrays and eight intended differences. The current changes leave .github/, pinned reference fetches, the three-process guard, the committed Core 3.33.2 lock and >=3.33.1 floor unchanged. Main has not been merged because GitHub reports the PR mergeable.

Round 5

  • 2a289b5347: P2-1 routes the existing hidden initial source through Core's helpers.get_stored_input. Divide, dispatch and ordinary FLOW month allocation retain Core's existing-input precedence, without exposing the source as a final public value.

  • 2a289b5347: P2-2 removes the additional low-level Core dependency drop. The review sequence (warm branch future value, raw independent seed 99, direct public _set(is_input=True)) retains 99.

  • 2a289b5347: P2-3 replaces the whole-simulation purge with the ruling's sequence-bounded, derived-only drop. Tests cover pre-cutoff and post-cutoff formulas, raw/input values, minimum twin history, and no dependency. The reviewer's cached 77 is the no-source-dependency case: nothing is dropped.

  • 2a289b5347: P3 stores physical originals on the installed Holder.get_array wrapper and recovers them on reinstall after importlib.reload. Reload → reinstall → write → read retains the correct value.

  • a439daee95: extend the same bounded rule to dataset rebuild returns using transient weak metadata; preserve new supplied-input registration/export and defensively evict an exact reported source memo for backends that create one, without a global cache purge. Three dataset regressions and two exact-source read cases use real Core (Round 6 adds explicit no-memo assertions).

  • 82d52dbfea: replace the machine's stubbed Core bookkeeping and add helper, rolling/month, real compound calculation, custom/partly failing handler, deletion and preservation compositions.

  • 2e7cccd799: add 24 helper equality routes to the unchanged original 50 routes and eight intended-difference tests.

  • 57dc1952ae: move the unchanged 126 synthetic input cases from microsimulation/ to core/, isolated in the social-security-inputs Rest group. Their prior CI process took 6:20 (312.62s pytest). The local 4 GiB-capped run reached its loaded-host timeout at 1801.01s with 2.385 GiB peak process-group RSS; CI at reviewed head 57dc1952ae passed all 126 on Rest shard 5 in 5:24.92 process wall (287.657s pytest), with 2.741 GiB peak RSS. Keep the three-process partition/report/failure guard across Rest inputs and Microsimulation base/current law. Restrict differential conftest discovery so its parent does not load a third country system (prior CI startup to pytest session: 26.85s), and raise the per-probe hang guard to 900s against about 290s per probe in prior CI. At reviewed head 57dc1952ae Microsimulation passed in 52:42, leaving 7:18 below its 60-minute limit; SPM took 21:28 and the microsimulation step 30:39 (base process about 21:32, differential about 9:07). The differential is the only added process left in that job and stays below the 10-minute allowance. Startup to its pytest session was 0.41s, versus 26.85s at 82d52dbfea. No route, assertion, normalization, workflow or job limit changes.

Round-5 validation

Final source head: 57dc1952aeb1b92fd2db8db831cbc5a19f7914d1.

Check Result
Real-Core regressions, prior head 82d52dbfea, no country import 54 passed on Core 3.33.2 in 24.95s and 54 passed on 3.33.12 in 56.62s. These include all four components, five helper/compound routes with/without initial inputs, bounded refresh, raw/input preservation, twin-only history, the reviewer's 77, reload, dataset rebuilds/input export and exact-source read values with unrelated-cache preservation; native reads do not insert a memo.
Machine, prior head 82d52dbfea, Core 3.33.2 60 passing examples, 3 invalid; 47.58s generation, 69.05s pytest.
Machine, prior head 82d52dbfea, Core 3.33.12 60 passing examples, 4 invalid; 35.59s generation, 109.32s pytest. Both runs keep the 60 × 25 deterministic settings and finish below five minutes.
Existing Social Security input file, historical head 82d52dbfea All 126 existing cases passed locally on Core 3.33.2 in 574.10s, under the 4 GB/1800s cap; starting load1 was 67.34.
Moved input file, the round-5 build core/test_social_security_inputs.py: one local run under the required 4 GiB/1800s cap; timed out on the loaded host, peak process-group RSS 2.385 GiB. Start load1 was 79.44. The file is byte-for-byte unchanged. At reviewed head 57dc1952ae CI passed all 126 on Rest shard 5 in 287.657s pytest / 5:24.92 process wall, peak RSS 2.741 GiB.
Local differential, the round-5 build One run under the 4 GiB/1800s cap, starting at load1 86.02. The main probe hit its 900s timeout on the loaded host; all 82 setup errors share that fixture timeout, so this run produced no equality result. Total wall 1269.90s, peak process-group RSS 2.100 GiB. CI supplies the result below.
Normalized equality differential and intended differences At head 57dc1952aeb1b92fd2db8db831cbc5a19f7914d1, run 40354752593 passed all 82 tests: 74 normalized equality routes against pinned main 8986e05cab65d56435a23df1910e2d35f2c5f5bf and 8 intended differences, in 546.02s pytest and about 9:07 process wall including startup and shutdown.
Routing and Makefile alignment, the round-5 build test_rest_python_groups.py: 59 passed in 588.02s; test_make_test_matches_ci.py: 5 passed in 20.26s. The no-history dry-run plan assigns the moved file once to isolated social-security-inputs on shard 6, estimated at 30s/file + 60s/process. A new one-file group uses the global median when timing history is available.
Format, lock and scope make format passed before every commit; final uv lock --check --offline passed with the same lock hash. No .github/, pyproject.toml or lock changes.
CI timing at reviewed head 57dc1952ae Microsimulation passed in 52:42 against main's measured 38–42 minutes. Its sole added process is the differential at about 9:07 including startup/shutdown, about 53s below the 10-minute allowance. SPM: 21:28; microsimulation step: 30:39; base process: about 21:32. 34 checks passed; one check was skipped.
CI at reviewed head 57dc1952ae Pull request run 40354752593 at 57dc1952aeb1b92fd2db8db831cbc5a19f7914d1: 33 of 33 jobs passed. Full Suite - Microsimulation took 52:42 (SPM 21:28, microsimulation step 30:39), against 38-42 minutes on main; the input checks ran in Rest shard 5 (126 passed). The Documentation workflow's build passed; its Pages deploy job was skipped.

Country-free runs use --confcutdir=policyengine_us/tests/core with pytest's default import mode, preventing the parent country conftest import. Core 3.33.12 verification uses the existing separate environment; no relock was needed. Python tests exercise API/storage/cache compositions that YAML cannot express, using one-person systems. The real-Core file and machine run in the existing Rest core group. The 126 synthetic input tests run in their own Rest Python group, with no national dataset. The normalized differential and intended-difference tests run in their own pytest process on the existing Microsimulation runner and reuse one loaded system per probe. No CI jobs or workflow steps are added.

The prior 50-route/953-array differential took 446.71s in Core 3.33.12 CI. Historical local timings use Core 3.33.2 on a shared host and are not a controlled comparison with CI. Their peak RSS was not separately recorded. This build records capped process-group RSS and wall time below. Historical CI measurements above are labelled with their original head. At reviewed head 57dc1952ae the existing test_microsim.py cases totalled 1058.715s, versus the supplied main range of 797–993s and prior PR range of 1015–1019s. These are separate runners rather than a controlled performance comparison; the round-5 build changes no simulation logic. The sandbox denied the sysctl CLI, so load gates use os.getloadavg, reading the same Darwin load averages.

Round-5 mutation checks

These mutation measurements are from prior head 82d52dbfea; the round-5 build changes no production source or targeted mutation regression.

All four independent round-5 mutants reached their targeted regression assertions (exit 1); none survived or needed a cap kill. Each ran in a scratch copy with the final production source/test snapshot, separately under the required 2 GB/900-second cap. The pushed branch was never mutated. These are deterministic targeted-test failure sequences; no Hypothesis shrinking was needed.

Reverted fix Failing sequence Pytest time
p2_1_hidden_helper_source Build initial public 12000@2024; divide 36000 over 2024-2025; expect Core 12000/24000, mutant gives 18000/18000. 7.78s
p2_2_raw_branch_seed Build public 12000@2024; branch; calculate public@2026; raw independent_input@2025=99; direct public _set 24000@2024; mutant removes the raw 99. 10.31s
p2_3_unbounded_refresh Calculate unrelated formula@2024=77; first public write 12000@2024; since is None, but mutant removes the cached 77. 8.99s
p3_reload_originals Build public 12000@2024; reload production module; reinstall; write 24000@2024; mutant raises KeyError('set'). 24.27s

All local test files run separately in the foreground with the six required thread settings and the memory/time caps. Country-importing files are started only below the host's load threshold; no population microsimulation is run. No PR/issue comments and no CI jobs are added.

Round 6

Final source head: 5c0e93ecd68afb2e6fe95fe9d894087437c3bb8d.

  • 13216f9be9 closes the remaining P2 for formula returns whose holders keep no array under opt_out_cache plus cache_blacklist, or memory_config.variables_to_drop. Rebuilds and writes on existing simulations collect these returns as additional bounded-drop witnesses. Inputs, raw seeds, pre-cutoff returns and since=None retain the ruling's protections. Exact reported-read tests now assert the absence of a native memo; their narrow eviction remains defensive.

  • fb4d238831 generates either cache policy over a nonempty subset of 13 names: all four components' public, base and reported names, plus an unrelated formula. Every existing invariant still runs after every step on all retained simulations/copies, and the unrelated reader is also checked. Settings remain deterministic at 60 examples × 25 steps. The model follows Core's ignored opted-out raw put_in_cache writes while retaining low-level raw _set coverage.

  • 06092f17bf extends real-Core coverage to both public and reported source writes, including ordinary setters and direct branch _set(is_input=True) without rebuild.

  • 871ee0eeab commits a return's actual _record_store number only when Core inserts that return into the fast cache. A lazily installed dict subclass observes bracket assignment; numeric dates are keyed by the actual requested memo key. Pending stores have weak simulation keys and weak array references, and the cache has a weak owner. A helper's numbered return without memo insertion cannot replace a live memo's date, even when it reuses the same ndarray. Clone/drop paths still start with an empty Core dictionary; pickle retains values and numeric dates without serializing weak ownership or pending stores.

  • d55a67302f advances Core's process-wide sequence counter past restored memo dates. The two pickle regressions simulate a fresh counter below a later recalculated memo, whose number is newer than StoreHistory's retained first record.

  • 5c0e93ecd6 preserves a memo’s existing date when a backend repeats the identical-array insertion without another store. A matching new pending store still supplies the new date. The real-Core preservation cases exercise duplicate assignments too; the machine’s backend naturally performs them.

StoreHistory supplies only the public/twin dependency cutoff; it never supplies the current return's date. The collector excludes physical nonderived inputs/raw values and read-time source views. An eligible physical deletion or identity-matching fast-only formula return is required before clearing the whole fast cache, advancing _input_epoch and tainting frames. Without a witness, those states remain unchanged. The adapter adds no Core dependency-drop call on low-level _set; Core's own branch-setter cleanup remains intact.

Review reproducer, real Core 3.33.2 Reviewed head 57dc1952ae Fixed source
Initial retirement $12,000; public/base not kept; rebuild with $24,000 Registered source/reported $24,000, public stale at $12,000 (since=3). Source/reported/public all $24,000.
No initial source; public/base/reported not kept; calculate zero, then rebuild with $24,000 Registered source $24,000, reported/public stale at $0 (since=28 locally; 22 in the review's process). Source/reported/public all $24,000.

Manual memo clearing makes both baseline reproducers return $24,000, confirming the stale-return mechanism. The 20-case source-write matrix covers both configurations, both initial values, dataset public loading, public/reported setters and public/reported low-level branch writes. Baseline ordinary public writes already return $24,000 because the physical override masks stale intermediates; they fail the required memo-drop/epoch assertions. Rebuild and reported-write cases also expose stale numerical values.

The real-Core file adds 46 cases to the retained 54: the 20 source-write combinations, pre-cutoff survival (2), intended post-cutoff unrelated deletion (2), a later recalculation of the same key (2), physical raw/input preservation on main and branch (8), nonmemoized fresh/reused arrays before/after the cutoff (8), unread-source loading (2), and pickle/counter restoration (2). The final 100-case file against the unchanged 57dc1952ae module in a scratch tree produced 38 failures and 62 passes in 137.61s, including the strengthened pickle/counter and duplicate-assignment checks. The failures are the 20 source-write cases, 2 intended unrelated drops, 2 later recalculations, 8 caller-preservation cases' missing-drop/epoch assertions, 4 post-cutoff nonmemoized-store cases and 2 pickle cases. The 8 new negative/preservation cases pass there.

The generated machine itself catches 57dc1952ae in 458.26s under the 2 GB/900-second cap. Its returned shrunk example selects the blacklist policy for all 12 component names (leaving the unrelated formula outside that subset), warms the invariant readers, then runs:

state = SocialSecurityInputsMachine()
state.configure_cache(cache_configuration=('blacklist', {
    'social_security_retirement', 'social_security_retirement_base',
    'social_security_retirement_reported',
    'social_security_disability', 'social_security_disability_base',
    'social_security_disability_reported',
    'social_security_survivors', 'social_security_survivors_base',
    'social_security_survivors_reported',
    'social_security_dependents', 'social_security_dependents_base',
    'social_security_dependents_reported',
}))
state.caller_values_and_earlier_derived_survive()
state.reported_values_and_provenance_follow_intent()
state.branch_initial_rewrite(amount=1,
    component='social_security_retirement', period='2023',
    replacement=0, slot=0)
state.teardown()

The assertion is the reported 2024 reader returning 0 instead of 2. Hypothesis stopped further shrinking after five minutes of slow progress; this was an assertion failure, not a cap kill or proof of a globally minimal example. The final-head machine passed all 60 examples (7 invalid) in 140.77s locally; the measured CI time is below.

Native Core 3.33.2 and tag 3.33.12 return the tested straight-line exact reported source from get_array before memo insertion. Both real-Core exact-read cases now assert no reported memo existed before the write. They establish correct values and unrelated-cache/epoch preservation, rather than a native need for eviction. The synthetic calculation entry memoizes exact reads, so the narrow source-view eviction is retained and labelled defensive. This does not claim coverage of arbitrary concurrent mutation or every custom backend.

Validation Result
Real-Core file, Core 3.33.2, final source 100 passed in 88.54s, under the 4 GB/1800-second cap.
Machine, final source, Core 3.33.2 1 passed in 140.77s, 60 passing examples and 7 invalid, deterministic 60 × 25 settings; 2 GB/900-second cap.
Moved core/test_social_security_inputs.py One local 4 GB/1800-second run at fb4d238831, before insertion-observer refinements, timed out at 1800.2s after 94 passing progress dots, with no displayed failure or completed JUnit result. Start load1 64.400. Final-head CI supplies the full 126 passed result (Rest shard 6).
test_rest_python_groups.py, final source 59 passed in 854.92s; 4 GB/1800-second cap.
Format, lock and scope make format and uv lock --check --offline passed; no relock. No .github/, differential, normalization, intended-difference test or job-timeout changes. No local folders, suites, differential or national microsimulation runs.
Previous-head machine CI recovery Reviewed head 57dc1952ae, run 40354752593: 7.402s, recovered from artifact 12666967148, rest-python-test-timings-shard-3/rest-python-core-1.xml. This is historical, not the expanded machine's final-head time.
CI at 5c0e93ecd68afb2e6fe95fe9d894087437c3bb8d run 40436852400 completed successfully: 34 checks passed, 1 skipped, no failures. Core 3.33.12 real-Core 100 passed; moved input 126 passed (Rest shard 6, artifact 12704963668 / rest-python-social-security-inputs.xml). Machine 1 passed in 5.766s, from Rest shard 2 artifact 12705501880 / rest-python-core-1.xml; below the required 300s. Grouping guard 59 passed (Rest shard 3, artifact 12705022672 / rest-python-remaining.xml). Full Suite - Microsimulation total 44m49s (02:10:38Z–02:55:27Z, including setup and cleanup); 74 equality routes and 8 intended differences all passed (82 cases, artifact 12705244572 / simulation-social-security-current-law.xml).

All local checks ran one file per foreground process with the required four-thread limits and OMP_WAIT_POLICY=PASSIVE. Country-importing files were started only below load1 100. Local checks use installed Core 3.33.2; final-head CI supplies 3.33.12 validation. The real-Core file and machine remain in their existing Rest group, and the moved input file retains its existing isolated Rest group. No PR or issue comments were posted.

Round 7

Final source head: a6dcc92217ae60127edd588295684f332713ff1a.

  • 6498687a0d closes the remaining P2: Core's bulk Simulation.delete_arrays(name) keeps unrelated fast-cache arrays while replacing the dictionary and losing its date metadata. The adapter snapshots only dates and weak array references, calls native deletion, and transfers a date only if its key still contains the identical array. Reusing the existing cache subclass preserves the established insertion, removal and pickle semantics; the hook handles the sole supported native replacement that keeps values. It does not redate returns from StoreHistory's first-store number.

  • The same commit adds the intended fail-safe from the ruling's addendum: with non-null since, an undated fast-only calculated return is treated as post-cutoff, dropped and counted. Normal cache clearing, _input_epoch advance and frame tainting follow. Source-view exclusions run first, physical holder values use their separate path, and supplied inputs/raw seeds remain untouched. With since=None, the adapter still does nothing.

  • df0ec80c7e exposes installed Core's actual Simulation.delete_arrays body in the existing country-free backend and adds two generated rules. bulk_delete_formula_cache selects a component's public/base/reported name or the unrelated formula on a retained state, avoiding deletion of caller source values. bulk_delete_then_reported_write composes a fresh initial source, a public read, bulk deletion of a generated name and a reported write before unrelated invariant reads can mask a lost fast-only date. Both existing invariants still run after every generated step on every retained state. Settings remain deterministic at max_examples=60, stateful_step_count=25, derandomize=True, deadline=None and report_multiple_bugs=False.

  • a6dcc92217 fixes a reference-fixture interaction exposed by the first merged-head CI run. The calculator-off fixture had removed gov.contrib.ssa.benefit_levers and gov.ssa.cola_history, although both already belonged to the retained reported-benefit formulas before the main merge. It now removes only the four calculator-only parameter namespaces, retaining all calculator-variable removal and all eight existing comparison cases. The earlier run at df0ec80c7e had six missing-parameter failures and two passes in this unit file. No local differential was run; final-head CI supplies validation. The protected 74-route microsimulation differential is unchanged.

The hub's main merge a7cf9ad820 kept both sides of the Makefile Rest-group conflict. ss-calculator-engine, ss-calculator-differential and social-security-inputs retain their separate files; this round changes no Makefile groups, .github/ files, job limits, microsimulation tests, differential routes, normalization or intended differences.

The six reproductions first load retirement $12,000, omit public/base holder storage, read public retirement and bulk-delete an unrelated formula. After the later $24,000 source refresh:

Cache policy Refresh route 5c0e93ecd6: public / reported Fixed source: public / reported Drop epoch
opt_out_cache + blacklist Dataset rebuild $12,000 / $24,000 $24,000 / $24,000 Unchanged before; advances after
opt_out_cache + blacklist Same-simulation reported write $12,000 / $24,000 $24,000 / $24,000 Unchanged before; advances after
opt_out_cache + blacklist Branch reported _set(is_input=True) $12,000 / $24,000 $24,000 / $24,000 Unchanged before; advances after
variables_to_drop Dataset rebuild $12,000 / $24,000 $24,000 / $24,000 Unchanged before; advances after
variables_to_drop Same-simulation reported write $12,000 / $24,000 $24,000 / $24,000 Unchanged before; advances after
variables_to_drop Branch reported _set(is_input=True) $12,000 / $24,000 $24,000 / $24,000 Unchanged before; advances after

Before writing the fail-safe, source inspection confirmed the premise at Core 3.33.1, installed 3.33.2 and 3.33.12. There are only two fast-cache entry assignments: the converged fixed-point path (calculate, line 1502 in 3.33.12) and the calculated-result tail (_calculate, line 1971). Exact physical holder reads return before the latter insertion. A newly supplied input substituted by _cache_result requires a visible physical input slot through _input_set_meanwhile; it cannot be mistaken for a fast-only return. Raw cache writes either store physically or return without inserting a memo. “Calculated return” includes defaults, uprating/carry-over and the SpiralError fallback. Existing reported-source-view exclusions remain in force.

Cache route Date behavior after this change
Bulk Simulation.delete_arrays(name) Carries each surviving date only for the identical array; deleted-name values and dates disappear.
Period Simulation.delete_arrays(name, period) Holder eviction and native exact-key pop remove affected values and dates together; survivors retain their dates.
purge_cache_of_invalid_values and Simulation.set_input Native single-key pops use the subclass's date-removing pop.
Holder._evict_fast_cache and input-helper eviction Native del/pop removes each affected value and its date; invisible-branch writes preserve both.
Core _drop_computed Replaces the cache with an empty dictionary; no values survive needing dates.
Core clone Starts the clone with an empty cache; the source's cache/dates are unchanged.
PE-US period_branch.drop_inherited_values Clears the entire fast cache; no surviving values need dates.
Adapter _purge_derived_arrays Clears the entire fast cache only after a counted drop.
Adapter _cache_result re-wrap Keeps available numeric dates and records actual new insertions; the intended fail-safe handles any undated fast-only calculated return left by another replacement.

The real-Core file adds 20 cases to the retained 100: the six refresh reproductions, pre-cutoff date preservation across unrelated bulk deletion (2), identical-array-only witness transfer (2), bulk deletion of the component itself with a surviving base (2), period deletion (4), the intended plain-dict-copy fail-safe with supplied-input/raw-seed preservation on not-kept names (2), and an unread-source rebuild after bulk deletion with since=None (2). The independent probe_details.py passes again: retained caches do not retain simulations/populations/holders, popped arrays collect, pending metadata contains only integer dates and weak references, old rebuild objects collect, and dating/serialization/helper/restore checks retain their behavior.

Using only 5c0e93ecd6's production module in a scratch tree, the new-case harness reaches 12 intended assertion failures, 6 passes and 2 hook-only skips, with no unexpected statuses. All six refresh combinations fail at the $24,000 public/reported assertion with public $12,000; both pre-cutoff cases fail at the missing original-date assertion; both component-delete cases fail at the public $24,000 assertion; and both plain-dict-copy fail-safe cases fail because the stale public memo remains. The four period cases and two no-cutoff cases already pass. The two identity-transfer tests require the new adapter hook and are skipped against the old module rather than reported as baseline regressions.

Machine catch against 5c0e93ecd6 under the required 2 GB / 900-second cap: assertion failure in 317.18s, without a cap kill; shrinking stopped after slow progress. Shrunk failing example and assertion: blacklist {social_security_retirement}; dataset public retirement $24,000 at 2023; then two bulk_delete_then_reported_write(amount=12001, component="social_security_retirement", name=...) steps deleting social_security_retirement and social_security_retirement_base, respectively. Both invariants run after initialization and each operation. The last public 2024 invariant returns 24,000 instead of 24,002 (the reported source is 12,001 with the fixture's factor 2). This is the returned shrunk example, not a claim of global minimality. The primitive deletion rule alone did not expose the bug in its deterministic run: warmed unrelated physical formulas could be dropped by the subsequent write and clear the stale memo. The composite rule keeps the critical operations together before those reads while retaining every invariant after every generated step.

Round 7 validation Result
Real-Core file, installed Core 3.33.2 120 passed in 17.82s, one foreground file under the 4 GB / 1800-second cap.
Independent probe_details.py Passed all lifetime, date, serialization, helper and directory-restore checks.
New tests against 5c0e93ecd6 module 12 intended assertion failures / 6 passes / 2 hook-only skips; zero unexpected statuses.
Machine at final source, local 36.868 testcase seconds from JUnit; 47.54s pytest total; deterministic 60 × 25, 2 GB / 900-second cap.
Existing core/test_social_security_inputs.py 126 passed in 642.54s; starting load1 73.797; 4 GB / 1800-second cap.
Existing code_health/test_rest_python_groups.py 61 passed in 303.74s; starting load1 29.214; 4 GB / 1800-second cap.
Format, lock and scope make format passed; lock check passed with the lockfile unchanged; .github/, Makefile, pyproject.toml, lock and microsimulation diffs are empty; env -u UV_FROZEN uv lock --check without relocking.
CI at a6dcc92217ae60127edd588295684f332713ff1a Run 40457717625: 34 checks passed, 1 expected documentation-deploy skip; all 33 workflow jobs succeeded. Core 3.33.12: real-Core 120 passed, machine 1 passed, inputs 126 passed, grouping guard 61 passed, calculator reference 8 passed, and protected differential 74 equality routes + 8 intended differences passed. Microsimulation total 42:19 (05:53:35Z–06:35:54Z, including setup/cleanup; SPM 20:03, microsimulation step 21:34), leaving 17:41 under the unchanged 60-minute limit. Machine JUnit testcase 11.134s, compared with local JUnit testcase 36.868s; CI remains below 300s.

Current-head artifact sources: Rest shard 3, rest-python-core-1.xml, records the real-Core cases and machine testcase; its decoded Hypothesis statistics show 60 passing / 0 failing / 6 invalid examples and 11.10s generation. The same artifact's rest-python-ss-calculator-differential.xml records all eight calculator comparisons. Rest shard 5, rest-python-social-security-inputs.xml, records the 126 input cases; Rest shard 1, rest-python-remaining.xml, records the 61 grouping cases. Simulation artifact, simulation-social-security-current-law.xml, records the 82 protected differential cases.

The four required local files ran at df0ec80c7e; the subsequent commit changes only the calculator reference fixture. All local files run separately in the foreground with the required four-thread limits and OMP_WAIT_POLICY=PASSIVE. Country-importing files start only at load1 ≤ 100. No local folders, suites, differential or microsimulation runs; no CI jobs, workflow steps or timeout changes. No PR or issue comments are posted.

impact: Current-law equality on the 74 normalized routes against pinned main 8986e05cab65d56435a23df1910e2d35f2c5f5bf, with 8 intended differences limited to benefit sources written after values were calculated. Hub microsimulation at this head (default dataset, 2026 and 2027, main at the merge base ee6c1c5166 against a6dcc92217, one side at a time under the shared heavy-job lock): social_security, its four components, taxable_social_security, ssi, state_income_tax, income_tax, household_net_income and household_benefits are unchanged in both years, with 0 records changed. The 2026 Social Security total is $1.68tn on both sides.

axiom: n/a: composable reform options and input compatibility; no current-law legal provision is added or changed.

@MaxGhenis MaxGhenis changed the title WIP: Add composable Social Security benefit levers Add composable Social Security benefit levers Oct 23, 2026
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 23, 2026 10:05
Run the unchanged input checks in a dedicated Rest group and preserve three isolated processes, distinct reports, and failure propagation. Avoid the differential parent's redundant country import and allow slower probes within a bounded hang guard.
@MaxGhenis
MaxGhenis merged commit 9969420 into main Oct 27, 2026
35 checks passed
@MaxGhenis
MaxGhenis deleted the ss-benefit-levers branch October 27, 2026 14:48
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

US + core hub merge audit, us#10400 at a6dcc92217ae60127edd588295684f332713ff1a (squash): Add composable Social Security benefit levers.

  • Reviews: GPT-6.1 Sol, eight rounds. Round 8 APPROVE at a6dcc92217 with no findings (Subfleet job 20261027-002820-hub-us10400-r8delta-g, ~/reviews/us-hub/reviews-gen/us-10400/review-r8delta.md). It closes round 7's P2: a bulk delete_arrays erased the surviving formula dates, so a stale benefit survived.
    • All six reproductions now give $24,000, keep their dates through a bulk delete, and advance the epoch after a refresh. Its own GC, dating and frame probes pass.
    • The real-Core file passes (120). The state machine catches round 7's bug on the old module (24,000 instead of 24,002) and passes 60 examples at the head.
    • It checked the hub's main merge a7cf9ad820: both parents' Makefile additions are kept.
    • It checked the calculator reference change a6dcc92217, which keeps the two namespaces this PR's existing formulas now read. The reference still removes every calculator-only namespace and variable prefix, and rejects a calculator-only dependency mutant.
  • Ruling: which cached values a source write may drop follows the hub's ruling and its 10/27 addendum (~/reviews/us-hub/assembly-ss/RULING-10400-refresh-scope-2026-10-26.md). That is an engine design choice; no legal provision is read.
  • Gates checked live at merge:
    • gh pr checks exits 0;
    • the latest Pull request run at the head (40457717625) concluded success (Microsimulation 42:19);
    • MERGEABLE;
    • not a draft;
    • no reviewer's latest review requests changes;
    • head pinned.
  • Overlap with main: main moved past the CI base ee6c1c5166 through 2ef53c03c0. The overlap guard's one shared area is tests/policy/baseline/parameters/test_uprating_extensions.py, which Uprate poverty guidelines over the 9902(2) interval and fix the Trustees wage base #10453 extended and this PR edits in one assertion. The hub merged 2ef53c03c0 with the head and ran that file and tests/core/test_per_capita_uprating.py: 165 and 51 passed (~/reviews/us-hub/fixes/verify-10400-combo/result.txt). It lands with that verified main.
  • Impact: measured by the hub. A microsimulation on the default dataset for 2026 and 2027 compared main at the merge base ee6c1c5166 with the head.
    • All 13 outputs are unchanged in both years, with 0 records changed: social_security, its four components, taxable_social_security, ssi, state_income_tax, income_tax, household_net_income, household_benefits, and the household and person ids.
    • The 2026 Social Security total is $1.68tn on both sides.
    • Files: ~/reviews/us-hub/fixes/10400-impact/.
  • Axiom: n/a. Composable reform options and input compatibility; no current-law legal provision is added or changed.
  • Evidence: ~/reviews/us-hub/assembly-ss/evidence-10400-r7/, evidence-10400-r7fix/ and evidence-10400-r8/.

MaxGhenis added a commit that referenced this pull request Oct 27, 2026
Main added tools/social_security_preflight.py (#10400), which checks a
stored dataset's SHA-256 before a Social Security reform is scored, as
system.py checks the default dataset's. Its hashlib import and
_file_sha256 are exempt like system.py's, so the calls that reach
_file_sha256 are no draw helper calls. The package scan of this head is
clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 27, 2026
Main raised pe-us's core floor to 3.33.1 (#10400). From 3.33.1 core's
subsample rebuilds from recorded inputs and never calls spm.py's
_get_exportable_input_periods override (policyengine-core#573), so the
override only changed to_input_dataframe and to_input_dict with
include_computed_variables=True, which nothing calls. It goes, with
PRESENCE_TESTED_INPUTS. section_911's call passes
include_computed_variables=False and is unaffected.

The lint job's start-month step ran the tests on the locked core because
that core's subsample reached the override. The lock is now 3.33.2, so
it no longer does; the PR test job's microsimulation group runs the same
file on the latest core. The step goes.

The subsample tests stay: a calculated default must still not come back
as a supplied column. They pass, with the other start-month tests, on
cores 3.33.1, 3.33.2 and 3.33.12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 28, 2026
#10400 was squash-merged as 9969420, so Git's merge base is the
pre-#10400 commit d453360 and every file #10400 added and this branch
then changed shows as an add/add conflict. Each was resolved against
#10400's head a6dcc92:

- The four social_security_*_pia_ratio_factor.py variables, Makefile and
  tests/code_health/test_rest_python_groups.py: main's copy equals
  #10400's head, so this branch's filled version (built on that head) is
  kept.
- tests/core/_social_security_state_backend.py: this branch's copy equals
  #10400's head and main changed it afterwards (#10504, Core 3.33.13
  names), so main's version is kept.
- changelog.d/ss-benefit-levers.added.md: main's release compiled and
  removed it; removed here too.

A merge-tree run with a6dcc92 as the base gives the same tree apart
from the version files (CHANGELOG.md, pyproject.toml, uv.lock), which
this merge takes from main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 28, 2026
Merges main 8ea4342 (which carries #10400) into 1aeb9e4.

Resolved hunks:
- Makefile, test-yaml-structural-other: the one conflict. Took main's
  recipe unchanged. SSA stays excluded from the per-subdir run and has
  its own per-file run with POLICYENGINE_BATCH_WORKERS=1, so this PR's
  three SSA YAML files run one file per process (review P2 #2).
- policyengine_us/system.py: merged without a conflict. #10400 adds an
  import, a module-level install of its input hooks, a class attribute
  and a migrate call in each simulation constructor. This PR adds the
  current-law cap snapshot inside CountryTaxBenefitSystem.__init__.
  The hunks do not overlap.

make format changed nothing after the merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant