Repository navigation
UK #931 prep: count floors in true sources, fold geographic clones in the FRS fits, ready the conditioned area draw - #1519
juaristi22 wants to merge 21 commits into
Conversation
Every UK floor now counts the survey household each copy and clone keeps in source_household_id, through one country-neutral reader: microcosm.build.source_lineage.record_sources reads any entity's true source through household membership on a declared copy lineage, and the UK declares its lineage once (household_lineage.UK_COPY_LINEAGE). receipt_counts.SOURCE_HOUSEHOLD_COLUMN is the shared source_household_id, not household_source_id, the support channel's immediate parent, which a copy made from a clone sets to the clone. The reader refuses a frame without the column and a null source; the stages before the SPI support channel say so, and their households are their own sources. The receipts of stages 10 to 12 and 15, the SPI pay floor and pension coherence, company cars, SPI benefit coherence, UC take-up's pools, the legacy closure, salary sacrifice and its bonus donor floor, Carer's Allowance, the trading allowance, the support floor, the sampled-rung private-rent region floor and the single-year age, dataset-size, council-tax, BRMA, weighted-integrity, benefit-cap and UC-relationship reports count through it. The five finite stages that declared household_source_id declare source_household_id, and UC take-up's pooling text and two notes name it. A static test refuses a distinct count of household_id, person_household_id or household_source_id in the UK runtime and tools outside two named structural checks, and the counting modules are bound into every stage hash that imports them. The two columns agree on every household row of today's spine, so no spine output moves. uk_spine.json and the coverage manifest are regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#931) The fits that train on the frame's FRS rows now fold a record's geographic clones into its first training row, with their weights summed exactly, before they fit: the SPI stage-2 and reference forests, the housing shell, the UC reporter redraw, the UC capital donor cells and the salary-sacrifice forest. SPI, high-value and CGT copies stay rows of their own. Today's spine has no geographic clones, so every fit's rows and weights are unchanged. The machinery is country-neutral. microcosm.build.source_lineage gains copy_paths, which reads every household's ordered copy path from a declared lineage, and replica_keys and collapse_replicas, which keep one row per record and path without the named branches. household_lineage.collapse_geographic_clones is the UK's entry, and uk_household_clone_paths now reads through copy_paths on UK_COPY_LINEAGE with identical paths. The housing shell and UC capital stages declare the lineage live where they run, and a test refuses a collapsing stage whose surface would hide one. Replicated-fixture tests hold each fit to the rows and weights it fits without clones, and fail without the fold. The SPI income stage binds spi_rank_transport into its hash. uk_spine.json is regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sampling stage of the shared atomic-geography declaration can declare weight_by (input, map, default) in place of its one weight column: each household samples by the weight column map names for its input code, or by default where the code is missing, and a code the map does not name is refused. The UK geography-first draw needs it to weight a household's output area by the dwellings in its own council-tax band. Declarations without weight_by keep their canonical bytes, definition digest and draws: New Zealand's committed declaration and today's UK one are unchanged, and the invented fixture's draws are pinned. Only the geography kernels' implementation hashes, and with them their cache keys, move. The graph builder adds the weight_by input to the assignment's household slice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each of the three UK atomic-area supports gains a conditioned form that only the geography-first move binds; today's build binds the registered supports, so no output moves. A conditioned support extends its registered support byte for byte and adds: - imd_decile, an exact-match constraint code: the area's decile within its nation at the 2019 vintage the FRS 2024-25 deciles follow (IoD 2019, WIMD 2019, SIMD 2020v2, NIMDM 2017), the plurality of its live residential postcodes in the May 2026 NSPL, with ties and postcode-less areas resolved by the parent area; - households by council-tax band times 10^6, exact integers for a weight_by stage: England and Wales from the area's LSOA mix in VOA CTSOP1.1 (band I only in Wales), Scotland from its council's CTAXBASE 2025 chargeable dwellings, so there the band moves mass only between councils, and none in Northern Ireland, which has domestic rates. tools/build_uk_conditioned_area_supports.py builds them from chronicle#412's raw-only registrations, read at PR chronicle#413's head and pinned by sha256. The committed provenance records every input, the decile rule counts, the rounding and a precomputed table of zero-mass draw cells. sources.yaml declares them under role uk_atomic_area_conditioned_support with five new geography vintages; spine.yaml declares the FRS channel's observed_constraints [imd_decile], an optional field the shared spine schema now admits; validate_uk_country_source_projection refuses a spec in which the supports, their conditioned forms and that declaration disagree. The building blocks are country-neutral, in microcosm.build.atomic_support_conditioning: modal_area_codes, apportion_weights, extend_atomic_support and zero_mass_cells. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chronicle#413 merged as c51f40d2b. Each of its five registrations the conditioned supports read pins the same file at the merge as at the PR head (df468b8) they were built from: the same filename, URL, sha256 and size. The tool's CHRONICLE block and the provenance now name the merge commit, and the three support files regenerate byte for byte. The provenance note also no longer claims DWP has been asked to confirm the FRS decile vintage; it says the vintage is still to be confirmed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review of 7f74d4c. I read the PR body, #931 and all five commits, focusing on the shared lineage reader ( The design holds up. Should
Nit
Checked and fine
CI. There is no run at 7f74d4c or at cad323b. The last Tests run (41954535023) was at 6e7a256 (commits 1 to 3), and all 11 jobs passed. Commit 4 and the re-pin have only the local runs described in the body, so a CI run at head is needed before this leaves draft. Verdict: commits 1 to 3 do what they say for floors and geographic clones. Items 1 and 2 need an answer, and item 2 should be settled before the move relies on the fold. 🤖 Generated with Claude Code |
Two generated files conflicted; both are regenerated on the merged tree with the venv synced to main's lock (policyengine-uk 2.174.1): - release_input_coverage_manifest.json differs from main's only in the 25 source-manifest pins this branch's stage-manifest edits move; - the H2 uk_spine.json differs from main's only in the seven stages whose consumed columns commit 1 changed, UC take-up's stage contract hash, and the two stages below. Main's two new stages, spi_employment_shell and aps_occupation (#1490), count support through receipt_counts, which on this branch reads the true source: - they join _COUNTING_STAGES, binding the counting and lineage modules into their hashes; - their household slices declare source_household_id in place of household_source_id, as commit 1 did for five stages; the strict reader refuses a slice without it; - their test fixtures carry each copy's true source and support clone index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SPI employment shell (#1490) trains its occupation classifier and draws its industry donors on the frame's FRS adults. As in the six fits of commit 2, it now folds each survey record's geographic clones into the record's first training row with their summed weight, and every other copy stays a row of its own. Today's spine has no geographic clones, so its rows and weights are unchanged, value for value. - Its household slice declares the support clone index its fold reads. - It joins the clone-collapse sweep's stages, which checks that slice. - A replicated-fixture test runs it on two production geographic clones: the fit, its training shares and clone 0's draws equal the frame's own, and the test fails with the fold switched off. - uk_spine.json moves only in its household slice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round 2 at 668e13f. Since 7f74d4c there are two commits: the merge of main (cd15bca) and the SPI employment fold (668e13f). I read both, along with the files behind each round-1 item. I read the code only and rely on CI for the tests. There are no replies on the PR yet, so all five items carry over. Round 1 items
A reasoned won't-fix on any of these is fine. Items 1 and 2 still need an answer before the move. New code No new findings. Checked and fine:
CI. Tests run 42075566346 at 668e13f: all 11 jobs passed (lint, engine-free and engine-uk on 3.13 and 3.14, engine-us, integration-uk, wheels, installed-emitter, select-countries). This closes round 1's point that CI had not run at head. Verdict: the new fold is consistent with the six earlier ones and CI is green. Items 1 and 2 remain the ones to settle before the move relies on the fold. 🤖 Generated with Claude Code |
|
Notes at 668e13f, on top of Vahid's two rounds. The SPI employment fold (668e13f) already covers the seventh FRS-trained fit, so it isn't listed here. Items 1 to 4 change no output. Item 5 would, so it belongs in a follow-up. 1. A concrete case for Vahid's item 2 (fold agreement).
This column would make a natural replicated fixture with clones that differ. The same question applies to any id-keyed draw that sits between the clone step and a folded fit. 2. The legacy
3. The US sections of
This PR already edits the doc, so a one-line note at the head of those sections, saying they describe #893's unmerged work, would cover it. Alternatively, a follow-up could trim them. The body could also say "never merged" rather than "no longer exists". 4. Cache keys after commit 3.
One sentence in the body would stop a full recompute being read as a regression. 5. Follow-up, not for this PR: group gate folds by true source household.
For the move (nothing to change here)
🤖 Generated with Claude Code |
The H2 uk_spine.json conflicted. It is regenerated on the merged tree and differs from main's only in the slices this PR changes: commit 1's seven stages, the two occupation stages, and UC take-up's contract hash. Main re-pins the UK feed onto chronicle#411's merge, which rewrites the feed block of scotgov_chargeable_dwellings_by_band.json; its counts are unchanged. The Scottish conditioned support names that file's digest in its band columns' source, so the tool rebuilds it with the same weights and new metadata. sources.yaml and the provenance pin its new bytes (a5e994ae, 1,028,760 bytes). The England and Wales and Northern Ireland supports rebuild byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) From Vahid's review (item 4), the scan now also sees: - `len(df.drop_duplicates(key))`, with the key as an argument or `subset=`; - `groupby(key).ngroups` and `len(df.groupby(key))`; - a `.loc[rows, "column"]` tuple slice; - a column named through a module constant, such as `HOUSEHOLD_ID_COLUMN = "household_id"`. Snippet tests pin each form and a pass case for other keys. The wider scan found one more count, a structural row check in tools/verify_uk_identity_stability.py that every SPI band donor holds one carrier. It is named in `_KEPT_ROW_COUNTS` beside the two existing ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From Vahid's review (item 5): `derive_is_apprentice`, `derive_frs_person_draws` (with and without its receipt) and `derive_frs_annual_income_spells` fell back to each row's household when `person_sources` was missing. On a cloned person table that counts K clones as K sources. `person_sources` is now a required keyword. The spine stages already pass true sources (`record_source_households`). The callers that did lean on the fallback now pass `own_source_households(person)` themselves: - the tests, which build raw frames where each household is its own source; - `tools/verify_uk_identity_stability.py`, which compares only the draws, never the receipt's counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the planning review (item 2): the stage filtered FRS training rows on `clone_index == 0`, the artifact's column name. No spine stage writes it, since the spine names it `household_clone_index` and `expand_uk_geographic_pool` drops a bare `clone_index`, so the filter did nothing. But a frame that carried it would train clone 0 at clone 0's own weight, not at the fold's summed weight. The stage now refuses such a table, and a test checks it. The `_dead_support` fixture drops the column after its support rebuild. Its four households then all train, so: - the engine tests count four and three reference rows, not two and one; - the engine tests count the reference forest's fits from the transport's fold receipts, one per fold that scores a recipient; - the replicated test no longer drops the column itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ext (#931) Three review notes, none of which changes code behaviour: - **UC capital (Vahid, item 1).** The note says `UC_CAPITAL_MINIMUM_CELL_DONORS` counts a cell's donor benefit-unit rows after the geographic fold, so a high-value copy counts as a donor of its own. It is a row threshold, not a disclosure floor. Whether it should count true sources is for the move's audit of K-sensitive constants, since switching it can move outputs. - **Scotland (Vahid, item 3).** The conditioned supports' provenance now notes that Scotland's band mix is its council's, so within a council a household is placed in proportion to households. It names a mix by 2022 data zone as the way to lift that, once one is registered in Chronicle. The support bytes are unchanged. - **The US doc passages (planning review, item 3).** `docs/geography-assignment.md` notes, at the head of each, that the US passages describe microcosm#893, which closed unmerged on 9 October 2026. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From Vahid's review (item 2) and the planning review (item 1), with María's ruling to keep each variant. The fold kept a record's first clone at the clones' summed weight even when the clones differ on what the fit reads. Once the move gives clones their own ids, they will differ: a landlord's finance costs are drawn on uniforms keyed by person id, and reach the UC reporter redraw through benefit-unit investment income. The fold dropped every clone's value but the first. How the fold works now: - `collapse_replicas` takes the fit's `inputs`, one row per record, and folds replicas only where they agree on every input; a missing value agrees only with a missing value. - Replicas that differ stay rows of their own. Each distinct variant keeps its first record, with the summed weight of the replicas that share it, so the fit sees every clone's value at its share and nothing is dropped. - `collapse_geographic_clones` requires `inputs`, so no fit folds blind. The seven fits pass what they read: - the housing shell its predictors and targets; - the UC reporter redraw its predictors and the reported amount; - UC capital its cell keys and the capital it donates; - salary sacrifice its predictors and sacrifice; - the employment shell its predictors, occupation and donated industry; - the SPI stage the raw columns its two forests derive from at fold time. Today's spine has no geographic clones, so every fit's rows and weights are unchanged, value for value. Tests: - shared tests pin the variant rule, missing values and refusals; - the sweep test adds a record whose clones differ; - a replicated fixture moves one clone's property income by 500, and the UC reporter redraw then trains on both variants at half the unit's weight each, while every other unit folds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks both. The answers are in five commits, on top of a second merge of main ( Vahid
María (planning review)
For the move. I've passed both notes to the session implementing the stacked move PR:
The second merge. Main's re-pin of the UK feed onto chronicle#411 rewrote the feed block of the Scottish band resource; its counts are unchanged. So the Scottish conditioned support is rebuilt with the same weights and new metadata ( None of this moves an output: the fold changes nothing without clones, no spine stage writes 🤖 Generated with Claude Code |
Scotland's conditioned support took each council's CTAXBASE 2025 band mix, so within a council the band said nothing about where a household lives (Vahid's item 3 on #1519). It now takes the mix of the area's 2022 data zone from NRS's dwelling estimates by data zone, December 2024 sheet. The bytes are the ones Chronicle already registers as nrs-dwellings-data-zone-2025, pinned here by sha256. Chronicle emits only the workbook's total as facts; PolicyEngine/chronicle#419 asks for the band columns. - The tool pins the workbook beside chronicle#412's files. It checks the header, the zone count and that each zone's bands sum to its total, and keys each area to its zone through the support's own lsoa_code; all 7,392 zones match. - The Scottish support is rebuilt (0c7d7826, 1,607,702 bytes). England and Wales and Northern Ireland rebuild byte for byte, and the zero-mass cells are unchanged. - The band vintage uk_ctaxbase_2025 becomes uk_nrs_dz_dwellings_2024 in sources.yaml and vintages.yaml. - Tests cover the reader and its refusals, the band source naming the workbook, and the mounted check that each zone's areas share its mix. No output moves: only the geography-first move binds these supports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Vahid's item 3, the Scottish band weights, now come by data zone ( First, a correction to my reply above. I said no data-zone source was registered in Chronicle. In fact Chronicle already registers NRS's dwelling estimates by 2022 data zone (
PolicyEngine/chronicle#419 asks Chronicle to emit the band columns as facts. Once it does, the support can read them from the feed instead, and the numbers will be the same. No output moves, since only the move binds these supports. The PR body is updated. |
|
Round 3 at 3778eb2. Since round 2 (668e13f) there are seven commits: the second merge of main (f9421ea) and six review fixes (fd902f6, 193870e, bb68b57, e9d557c, 77b1eb1, 3778eb2). I read the six fix commits and the merge's file list, with María's two replies. In a scratch clone I ran these engine-free test files, and all pass: the shared lineage reader, the clone-collapse sweep, the UC reporter redraw and the SPI income stage in full, plus the receipt-count scan, the conditioned supports and their tool, the apprentice draw and annual income. I also ran one extra SPI test (item 6) and checked the NRS workbook with the tool's own reader. Earlier items
María's planning items 2 to 4 (the bare New finding Should
Checked and fine
CI. Tests run 42209756229 at 3778eb2 was still running at 12:05Z. lint, wheels, select-countries, installed-emitter, integration-uk and engine-uk (3.13 and 3.14) have passed. engine-free and engine-us (3.13 and 3.14) were still running; on round 2's run engine-free took about 1 hour 40 minutes. The run at 77b1eb1 was cancelled. Verdict: items 1, 3, 4 and 5 are closed. Item 2 is closed apart from its SPI consequence in item 6, which is the one thing to settle before the move relies on the SPI fold. It moves no output today. 🤖 Generated with Claude Code |
run_graph kept every structural population version until the run ended and attached all of them to the manifest. Nothing serializes them, and the spine reads only its final version, but holding every one is what keeps a 15-clone spine from fitting in 32 GiB. A caller can now name the versions it reads, and the run frees every other one as soon as its last reader has run. - run_graph takes retain_populations. None, the default, keeps today's behaviour: every version is kept and attached. - Given a collection, a name that is not a version of the graph, or is not a name, is refused before any source is read. Each version's last reader comes from the compiled graph: its ordinary members read it as their incumbent, and each structural node built on it reads it as its base. Once that node has run, reached or not, the version leaves the run. Only the named versions and their mass ledgers are attached. - The UK opts in. The spine names its final version. Each dense and national phase names what it reads from its own manifest: the export the installed population, the national checkpoint and solve their own populations, the numerical phase the pool only when it writes a size checkpoint from a graph with a size search, and every other phase nothing. The atomic-assignment measurement names uk.full.expand. The transport CLI and the parity tools keep the default. No output moves. The keyword enters no key, receipt or cache record, and each node reads the same population as before. The new graph tests run a structural chain cold, warm and under resume="require", with and without the keyword: the store objects are byte-identical and the manifests agree on every field but the attached populations. Weak references show each dead version's tables freed right after its last reader and not before, for a version read by two members and two boundaries too, and when the last reader is unreached. The synthetic spine and dense builds keep only the versions they read. No kernel source hash binds an edited module, so no node key moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each UK stage keeps its last result, output Frame included, in last_result, and the stage transforms live for the whole run. Only the evidence snapshot each kernel takes right after its stage runs reads that result, so on a 15-clone spine the retained frames are memory nothing reads (an upper bound of about 45-50 GiB). - stage_evidence gains release_stage_results. It walks the same wrappers as the fit-weight probe, now through one shared walk, and clears last_result wherever an object holds one itself. It writes through object.__setattr__, so frozen stages are cleared too. It never writes on a wrapper that only proxies the attribute, and it keeps the wrapped objects, fit-weight records, receipts and sampling. - The UK create, stage and expand kernels call it right after they encode their evidence, when the registry asks. build_uk_registry and uk_registry take release_stage_results, False by default, so a caller or test that reads last_result after a run is unchanged. - prepare_uk_spine_execution turns it on. That covers the spine and the full build's --spine-request route, which prepares its spine through it. The full build's --input-h5 route and the national line bind no stage transforms. Nothing reads a live transform after its snapshot. Inside the stages, last_result is read only by checkpoint_metadata and by the SPI income stage's fit_weight_records, both reached only from the snapshot. The spine driver, its gates, the sidecar, the weights audit and the dense and national lines read stored evidence. The graph source transform keeps the stage it built. Without its result that stage holds only small evidence, and a late read still reaches it: most stages' checkpoint hooks then refuse, where a dropped stage would read as no evidence at all. The release changes no stored byte. A truncated spine run, with stages of every shape that keeps a result, stores the same objects under the same keys, byte for byte, with and without it. Each result it frees dies at once, by reference counting alone, so no wrapper still holds one. A synthetic run of the spine driver's own graph, with and without main's observation wrappers, leaves no stage result held, the expanding SPI support stage's included. Fit-weight records read from a result are still captured. Against the previous code, the same truncated run stores identical column and stage-evidence payloads and kernel receipts, node for node. Keys move, though no payload does. Every UK create and stage kernel's hash binds stage_evidence, so an edit there re-keys the whole spine, wherever the switch lives. Every kernel in graph_kernels.py hashes that file, so its claim and identity kernels move too. All 79 declared spine node keys move, and the gate nodes' with them. So does every record that carries a key: store metadata and cache records, receipt keys and kernel hashes, the manifests, the graph evidence and the spine sidecar. In-graph spine caches go cold. A full build re-keys from its spine on its --spine-request route, and from uk.full.expand.owned onward when it starts from an --input-h5 checkpoint. The national graph binds none of these kernels, and no committed pin binds their hashes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chronicle main 80a3482 (PolicyEngine/chronicle#421) brings NRS's dwellings by council-tax band A to H for each 2022 data zone at December 2024 and 2025 (118,272 rows), which the Scottish conditioned support reads from the next commit on. It also brings what Chronicle merged since 6475380: PolicyEngine/chronicle#415's UK land-value inputs (326,495 rows), which nothing reads yet; PolicyEngine/chronicle#413's raw-only registrations, no rows; and PolicyEngine/chronicle#414, which writes the bundle one package at a time with the same rows. The feed has 2,030,062 rows, and its row contract and schema digest are unchanged. The merge tree is the pull request head's, so the artifact was built from the head. No compiled value moves on either surface: both reference files and both compile-parity receipts are byte-identical. The national membership moves its feed label and the matched-fact counts of the nine MHCLG council-tax stock targets, whose selectors also match the council-tax rows #415 adds; every candidate resolves to the same fact and value. The local membership moves its feed label and the 1,263 DESNZ constituency keys, whose derived keys hash the feed identity. The vendored resources and the DESNZ constituency resource carry the same rows under the new feed identity, the census and the validation register restate the pin, and the H2 fixture moves the contract hashes of the 19 stages that bind vendored resources. The high-value home stock's inputs are unchanged, so it is not rebuilt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The conditioned-support tool now reads NRS's dwellings by council-tax band for each 2022 data zone at December 2024 from the pinned UK feed (record set nrs.dwellings_data_zone_2025.2024, PolicyEngine/chronicle#421) instead of parsing the registered workbook. It streams the feed and checks the whole file against the pin. It refuses a row of that record set that is not a zone's total or band, a zone without all nine, and bands that miss the total. The provenance records the feed read and a fingerprint of the counts, and the Scottish band columns' source names the record set and that fingerprint. The facts equal the workbook's bands in all 7,392 zones; they come from the same bytes (5d61db5f). So the Scottish support's arrays are unchanged, and only the eight band columns' source moves (a8ca096b, 1,607,718 bytes). England and Wales and Northern Ireland rebuild byte for byte, and every receipt is unchanged. The tool's Chronicle block names #421's merge, where the five raw-only registrations pin the same bytes. The facts are streamed rather than vendored: their 66,528 rows would make a resource of over 80 MB. As for the high-value home stock, a feed-gated test checks on each re-pin that the feed still carries the facts the supports were built from, and the re-pin runbook says to rebuild them if not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A structural node stores every column of its version, then the whole frame. _write_node first built a copy of every column, each with its own copy of the entity's ids, and only then wrote the first one. On a late 15-clone spine frame that is a second frame plus one id array per column, about 10 GiB held at every structural write on top of the version itself. A cache hit on a structural node did the same the other way round: it loaded every stored column, each with its own ids, and held them all while it loaded the frame, though it returns only the frame. The spine runs on a persistent node store with resume="auto", so a spine restarted after a failure meets that at every structural node it already stored. - The structural write now streams. Every dtype token is still taken first, so an unsupported dtype is refused before anything is written. Then each entity's ids are copied once, each column is copied only when it is written, and each copy goes before the next is made. The order is the same sorted (entity, column) order, so the record lists the same entries. - put_column only reads the series and its ids. It wraps the ids in a new index without copying them, writes them and keeps nothing once it returns, so one id array shared by an entity's columns writes the same bytes as one copy per column. - A structural hit still loads every column, in the record's order, and still refuses a damaged or repeated one, but it keeps none of them: its version comes from the frame. An ordinary hit still returns the columns it loads. - The ordinary-node write and the frame, weight, opaque and record writes are unchanged. Integration layer 02 (69b94b4) once wrote this per column too, taking each dtype token only when it reached the column, so an unsupported dtype would have been refused after earlier columns were stored. The #893 reconciliation (80fef22) dropped it for want of a consumer. This version takes every token first and shares one id index per entity, and the 15-clone UK spine is its consumer. No output moves. The new graph tests keep the old structural column copies verbatim but for their names, and only those stand in for the new ones, so the rest of the write is shared. A toy graph with CREATE, FILTER, REWEIGHT and EXPAND nodes stores byte-identical objects under the same keys with either, cold, warm and rewritten under resume="forbid", every node's artifacts match in order, and the warm run, where every node hits, reports what the cold run did. A direct write of a frame with every graph dtype, nulls, and entities and columns out of sorted order returns the same artifacts and record, and an unsupported dtype still leaves the store empty. Weak references show each column copy gone before the next is made, and each entity's columns share one id index. They show a structural hit loading each column in the record's order and holding none once it loads the next one or the frame, and a damaged or repeated column is still refused. On a synthetic frame with 10 MB of columns, tracemalloc puts the write's peak at 2.1 MB, against 22.3 MB before, and a structural hit's at 34.9 MB, against 55.1 MB before. What remains of the hit is the frame load itself. No kernel source hash or committed pin binds the executor, so no node key moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) Review follow-ups to keeping only the versions a caller reads. A version left the run only once its last reader had finished, and the executor's own locals kept the dead base and the node's copies alive until the next node rebound them. - A version now leaves once its last reader has applied its result, before that node is observed or written. An unreached last reader still drops it at its turn. - The executor lets go of what a node no longer needs: the incumbent once the node's result is admitted, the context and decoded artifacts once the kernel has run, and the node's population, result and opaque payloads once its receipt is built. A structural node's dead base is gone before its own frame is written, and nothing a node held is left for the run-end source pass. - The dense build drops the numerical manifest before the export run, and the national build drops its loaded checkpoint before the targets run. The size-checkpoint pool name lives in one constant read by both helpers. - Charter amendment 29 records retain_populations. The graph explorer notes that a retaining run attaches only the versions it names. No output moves. The graph tests run a FILTER chain and the toy country's REWEIGHT, gate, release and entrant EXPAND graphs cold, warm and under resume="require", with and without the keyword. The store objects are byte-identical and the manifests agree on every portable field. With the cyclic collector off, weak references show each dead version freed by its last reader before that reader writes. That holds for a version read by two members and three boundaries, for one nothing reads, and for one whose last reader is unreached. No kernel source hash binds an edited module, so no node key moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part of #931. This is the prep PR of the geography-first spine plan. It carries all four of the plan's prep commits, and none of them changes an output of today's build.
Commit 4 builds the conditioned area supports from the registrations in PolicyEngine/chronicle#413. It was built at that PR's head (
df468b8) before the merge, then re-pinned to the merge commit (c51f40d2b), where every registration pins the same bytes. Two commits re-pin the UK feed onto the merge of PolicyEngine/chronicle#421 and read Scotland's band weights from its NRS facts. No compiled value moves. Four more commits cut what the graph executor holds in memory, so that the move's 15-clone spine can fit in 32 GiB; they move no output either.Why
The move, a later PR, clones every FRS household K times at the start of the spine. Each clone then gets its own small area, and every later stage runs per clone. Four things have to hold before that, and all four hold today without moving an output:
household_idorperson_household_idreads K clones as K sources.household_source_id, the support channel's immediate parent, does too, because a copy made from a clone names the clone there.The machinery is country-neutral
Per María's ask, nothing new names a UK column. The new
microcosm.build.source_lineagetakes a country's copy lineage as data:CopyLineage: the entity copies are made of and its true-source column (default: the sharedsource_household_id).CopyBranch: each copying step, in path order, with its marker and ordinal columns.Its readers are:
record_sources: every record's true source, for any entity, through household membership.copy_paths: every household's ordered copy path.replica_keysandcollapse_replicas: one row per record and path without the named branches, with the weights summed exactly.The UK declares its lineage once, as
household_lineage.UK_COPY_LINEAGE.weight_byis added to the sharedatomic_geographyoperator itself. The newmicrocosm.build.atomic_support_conditioningbuilds a support's conditioning columns from any country's point records, shares and supports (commit 4), and the shared spine schema gains an optionalobserved_constraintsfield.Commit 1: every floor counts true source households (246b33a)
receipt_counts.SOURCE_HOUSEHOLD_COLUMNis now the sharedsource_household_id, read throughrecord_source_households, the UK's entry on the shared reader.before_copies. Their households are then their own sources, and a frame that already carries copy lineage is still refused.household_source_iddirectly and skipped the null check;household_source_idnow declaresource_household_id. UC take-up's pooling text and two notes name it too. Updating that wording was María's choice; it is the one evidence digest that moves (see "The proof").household_id,person_household_idorhousehold_source_id, outside two named structural checks that each benefit unit sits in one household.receipt_counts,household_lineageandsource_lineageare bound into the hash of every stage whose module or helpers import them. A test derives that set from the imports.Commit 2: the FRS-trained fits fold geographic clones (f00739e)
The six fits:
A seventh, the SPI employment shell from main's UK: carry the FRS occupation (SOC 2020 major group), and redraw it on the SPI channel #1490, joins after the merge (
668e13f51, below).What each fit does now. It folds a record's geographic clones into the record's first training row, summing their weights exactly with
math.fsum. SPI, high-value and CGT copies stay rows of their own, as now. Today's spine has no geographic clones, so every fit's rows and weights are unchanged, value for value.Only clones that agree fold (
77b1eb151, from review). Each fit passes what it reads, and clones fold only where they agree on it. Clones that differ stay rows of their own, each variant with its own clones' summed weight, so the fit sees every clone's value at its share. That will happen once the move gives clones their own ids: through a draw keyed on those ids, or a stage run per area.The clone path.
uk_household_clone_pathsnow reads throughcopy_pathsonUK_COPY_LINEAGE, as María asked. The paths are identical on the 3 November spine (24 distinct path shapes) and on a two-clone pool of it, so the dense identity keys and draws don't move. Three identity tests now match the shared reader's branch names in their error messages.Declarations. The housing shell and UC capital stages declare the lineage live where they run. A test refuses any collapsing stage whose surface would hide a lineage column; in the move that includes the geographic clone index.
Replicated-fixture tests. One per fit: the stage runs on its fixture and on two production-expanded geographic clones of it (
expand_uk_geographic_pool), and the fit must receive the same rows and weights, or clone 0 must draw what it draws alone. Each test fails with the fold switched off.The SPI income stage now binds
spi_rank_transportinto its hash.Commit 3: the shared operator's
weight_by(6e7a256)weight_by(input,map,default) in place of its singleweight. Each household samples by the columnmapnames for its code, or bydefaultwhen the code is missing. A code the map does not name is refused.weight_bykeep their canonical bytes, definition digest and draws. That covers New Zealand's committed declaration and today's UK one, and the invented fixture's draws are now pinned. Only the geography kernels' implementation hashes move, in the UK and New Zealand alike.resume="require"refuses at preflight instead of recomputing.geography.assign_atomic@1.Commit 4: the conditioned area supports (cad323b)
What they are. Each of the three UK supports gains a conditioned form, declared beside it in
sources.yamlunder the new roleuk_atomic_area_conditioned_support. It keeps its support's bytes column for column:uk_conditioned_support_baserecovers them exactly, and the tests compare them with the registered digest. It adds the columns the move conditions on. Only the move binds them; today's build reads the registered supports.imd_decile. The area's deprivation decile within its nation, an exact-match code for the shared operator.Vintage. The 2019 vintage of each index: IoD 2019, WIMD 2019, SIMD 2020v2 and NIMDM 2017. The FRS 2024-25 documentation lists no change to
IMDE,IMDW,IMDSorIMDNsince 2023-24, which predates the 2025 indices. An internal comparison (no figures quoted) also fits England's FRS decile shares by region to IoD 2019 better than to IoD 2025, as it does for the 2023-24 FRS. The vintage is still to be confirmed with DWP. If it turns out to be 2025, only the England and Wales column is rebuilt.Rule. The May 2026 NSPL is the last edition to carry the 2019-vintage ranks. It gives every live residential (small-user) postcode its small area and rank.
inferred_modal.How each area's decile was set:
Band weights. Each band column is the area's households in that council-tax band times 10^6, an exact integer, for the move's
weight_bystage.voa_council_tax_band_mix_lsoa.json, the mix UK: data fixes to model the High Value Council Tax Surcharge (mansion tax) correctly, and the related housing and council-tax gaps #1240's band node reads). The mix is stated to 6 dp and ends at exactly 1, so nothing is rounded. Only Wales has band I, and the assembler refuses any in England.3778eb2c6), rounded half to even. Sincec8066be53they are read from the facts Chronicle emits for them. The largest per-area deviation is 3 millionths of a household. Every area in a zone has the zone's mix, and a test checks this. NRS counts dwellings on the valuation list, as VOA does in England and Wales.Zero-mass cells. For each support and each weight column (households and every band), the provenance lists the FRS region × decile cells and region cells with no mass. So the move can choose its fallback (band weight → household weight, then IMD → region) before it draws. Today every region has households in every decile. The only empty cells are band H in seven of England's region × decile cells, and band I across England.
Inputs.
tools/build_uk_conditioned_area_supports.pyreads three sets of inputs:c8066be53);uk/uk_atomic_area_conditioned_supports.provenance.jsonrecords every input's identity, the Chronicle commit and feed read, a fingerprint of the NRS facts, each nation's decile rule counts, households by decile, the rounding and the zero-mass cells. It holds published area data only. The support bytes are local inputs, as today's are.Declarations.
sources.yamlgains three rows pinning the bytes. Each keeps its support's vintages and adds the decile vintage and, outside Northern Ireland, the band vintage. The five new geography vintages are reciprocally compatible with every other.spine.yaml, the FRS channel declaresobserved_constraints: [imd_decile]. The shared spine schema now admits this optional field.validate_uk_country_source_projectionnow requires three things: each system's support and its conditioned form, the conditioned vintages extending the support's, and the spine declaration.uk_atomic_support_register(conditioned=True)reads the conditioned pins for the move's preflight.Country-neutral.
microcosm.build.atomic_support_conditioningholds the pieces any country can use:modal_area_codes,apportion_weights,extend_atomic_supportandzero_mass_cells. A test draws on added columns with the shared operator unchanged. The code column is an exact-match constraint, andweight_bychooses the weight column.Re-pinned to chronicle#413's merge (
7f74d4c32). At the merge commitc51f40d2b, each of the five registrations pins the same filename, URL, sha256 and size as at the PR head. The tool'sCHRONICLEblock and the provenance now name the merge, and a test holds the two together. Rerunning the tool regenerates the three support files byte for byte.Review. Max: this commit adds a shared module and an optional field to the shared spine schema. No other country declares the field, and their specs resolve as before.
Merging main (cd15bca, f9421ea) and the occupation stages (668e13f)
uk_spine.json. Each is regenerated on the merged tree, with the environment synced to main's lock (policyengine-uk 2.174.1).spi_employment_shellandaps_occupation(UK: carry the FRS occupation (SOC 2020 major group), and redraw it on the SPI channel #1490) count their receipts' support throughreceipt_counts, which here readssource_household_id. So in the merge:_COUNTING_STAGES;source_household_idin place ofhousehold_source_id, since the strict reader refuses a slice without it, as for commit 1's five stages;668e13f51). The employment shell trains its occupation classifier and industry donors on the frame's FRS adults, so it now folds geographic clones like commit 2's six fits.aps_occupationtrains on APS donors, not on the frame, so it needs no fold.f9421ea47, main9d080e793). Only the H2 fixture conflicted, and it is regenerated. Main's re-pin of the UK feed onto chronicle#411 rewrote the feed block ofscotgov_chargeable_dwellings_by_band.json; its counts are unchanged.a5e994ae, 1,028,760 bytes). The data-zone mix in3778eb2c6has since replaced that file as Scotland's band source.Review fixes
From Vahid's two rounds and the planning review:
77b1eb151). Commit 2 above has the details. A replicated fixture with differing clones uses the planning review's case: a landlord's property income, drawn apart per clone, reaches the UC reporter redraw as two rows at half the unit's weight each.3778eb2c6). Chronicle already registers NRS's dwelling estimates by 2022 data zone, band columns included, but emits only the total as facts. The tool now reads bands A to H from those bytes, pinned by sha256. It uses the December 2024 sheet, which is nearest the FRS 2024-25 year and VOA's stock at 31 March 2025.lsoa_code. All 7,392 zones match NRS's.0c7d7826, 1,607,702 bytes). England and Wales and Northern Ireland rebuild byte for byte, and the zero-mass cells are unchanged.uk_ctaxbase_2025is renameduk_nrs_dz_dwellings_2024.nrs-dwellings-data-zone-2025) chronicle#419 asked Chronicle to emit the band columns as facts. Emit NRS's council tax bands by 2022 data zone chronicle#421 does, and the support now reads them from the feed with the same numbers (below).fd902f63a). It now catchesdrop_duplicates(key),groupby(key).ngroups,len(df.groupby(key)),.loc[rows, "column"]and columns named through a module constant. It found one more structural row check intools/verify_uk_identity_stability.py, now named.193870e6f). The person draws and annual income no longer fall back to each row's household.clone_indexfilter is gone (bb68b570e). The SPI income stage refuses a table carrying the artifact's bareclone_index; no spine stage writes it.e9d557c39):3778eb2c6has since lifted;docs/geography-assignment.mddescribe Consolidate the US survey graph build and enrichment pipeline #893.Picking up chronicle#421 (f086e01, c8066be)
f086e0169). The UK feed moves from6475380to80a3482, the merge of Emit NRS's council tax bands by 2022 data zone chronicle#421 (2,030,062 rows; the row contract and schema digest are unchanged).c8066be53). The tool streams NRS's December 2024 record set from the pinned feed and checks the whole feed against the pin. It still refuses a zone without all nine counts, or whose bands miss its total.5d61db5f).a8ca096b, 1,607,718 bytes). England and Wales and Northern Ireland rebuild byte for byte, and every receipt is unchanged.The executor keeps less in memory (a0e31d8, 5dd5e1d, 13ce7a3, 2fe3341)
Why. At 15 clones, a spine run would hold more than this 32 GiB machine has:
last_result;The move needs these fixes before its f100 15-clone spine, not before its f010 arms.
E1: keep only the versions the caller reads (
a0e31d886,2fe33410e).run_graph(..., retain_populations=...)lives in the shared microcosm-graph.None, the default, keeps today's behaviour.uk.full.expand.docs/graph-acceptance.mdrecords it.E2: release each stage's result once its evidence is captured (
5dd5e1d62). The sharedstage_evidence.release_stage_resultsclearslast_resultafter the snapshot, walking the same wrappers as the fit-weight probe. The UK kernels call it when the registry asks. The spine turns it on, and so does the full build's--spine-requestroute, which prepares its spine the same way. Inside the stages, only the snapshot readslast_result.E3: write structural versions one column at a time (
13ce7a3a3). Each entity's ids are copied once and shared, and each column is copied only when written, in the same order. A structural cache hit still checks each stored column, but keeps none of them.Other releases (
2fe33410e). The executor also lets go of each node's context, decoded artifacts, incumbent and result once nothing reads them.Review. Max: this changes the shared executor and adds amendment 29, which you may want to renumber or reword.
The proof: no output change
The proof predates the merge and the review fixes, and carries over. It wasn't re-run, because everything after it is of a kind already shown to move nothing:
source_household_idinstead ofhousehold_source_id, the switch commit 1 made, and the real spine shows the two columns agree on every household row;spi_income.py,uc_capital_coherence.py, and the counting module's docstring) merged without conflicts;clone_index), pass the same sources explicitly, or are notes and tests;Real spine, before and after. Each spine is a K=1 full spine from the licensed sources, with a fresh checkpoint store and
--staging-local-only. The base isfc5bb9014, the merge base before main was merged in. There are two heads:cad323b79(all four commits) and, earlier,6e7a25628(commits 1–3). Both heads give every result below.uk_frame_content_identityis equal:3939a72ea5693441c3af4efea61ccc1ad8ae5d11f02ce91574cf61bae1cb003bon all three.tools/compare_uk_h5_payload.pyexits 0).release_id, which names each build's start time.The executor's memory fixes, before and after. Base
c8066be53, headcf8432c9b, both K=1 full spines from the same launcher and inputs. The launcher now passes--aps-tab, which the spine requires since UK: carry the FRS occupation (SOC 2020 major group), and redraw it on the SPI channel #1490.uk_frame_content_identityis equal:b19e3f90daf4b1a69c66d4d2e6c4c6c3d05198e08255cc6d03917e5b1f826d07.release_id.Precondition. On the 3 November spine and on both spines here,
household_source_idandsource_household_idagree on every household row, with every copy type present.Commit 4 binds nothing today. No stage reads the conditioned supports, and the spine above is unchanged with them declared. The commit after it,
7f74d4c32, re-pins the provenance to chronicle#413's merge and rewords one provenance note. The support files regenerate byte for byte.H2.
uk_spine.jsonis regenerated. Only the inputs of the seven stages whose declarations changed move, plus UC take-up's stage contract hash.fixture.jsonmoves only in the two texts. The coverage manifest moves only in its source-manifest pins. Commit 4 moves none of the three: they regenerate byte for byte.Tests run locally:
test_uk_cgt_projection's check that the UK engine extra is absent, which fails wherever the extra is installed.3778eb2c6: the conditioned-support and tool tests, the UK country adapter, the atomic-support provenance and the shared spec tests pass with the rebuilt supports mounted.c8066be53: with the pinned feed available and the rebuilt supports mounted, 21 files pass, with 3 skips: the feed pin, full-targets, reference, local-target, census, validation-level, vendoring, ledger-target, local-parity, DESNZ, high-value home, council-tax, conditioned-support and country-adapter tests; the shared epoch, ledger-artifact and spec tests; and the engine coverage-manifest and H2 parity tests.c8066be53) pass. The new tests run the toy graphs cold, warm and underresume="require", with and withoutretain_populations.ci_test_plan.py verify, on each state.The full suites are left to CI.
What moves without being an output
5dd5e1d62).stage_evidence.pyandgraph_kernels.pyare bound into every UK stage kernel's implementation hash.--spine-request, and fromuk.full.expand.ownedonward with--input-h5.f086e0169): the vendored resources' feed headers, both memberships, the census, the validation register, the DESNZ constituency keys and 19 H2 contract hashes.Found in passing
receipt_countswithout binding it into their hash, so an edit to it alone left their cache keys in place. With the lineage modules, 24 stages were missing a binding; all are now bound, and the test above derives the set.docs/geography-assignment.mddescribes the US work of Consolidate the US survey graph build and enrichment pipeline #893, including a block adapter (us_runtime/atomic_block_support.py). Consolidate the US survey graph build and enrichment pipeline #893 closed unmerged on 9 October 2026, so that code never reached main. The doc now says so at the head of each US passage (e9d557c39).inferred_modal; the move may choose to relax IMD in Northern Ireland.uv run --no-sync --with odfpy, which leaves the lockfile alone; it is documented in the tool.🤖 Generated with Claude Code