Repository navigation
Let transport specs declare pre-calibration stages - #1392
Merged
Merged
Conversation
An optional precalibration_stages resource lists the rules bindings whose outputs may feed calibration. A listed unit-entity stage admits its own nodes, engine reference and outputs; every listed stage, person or unit, lets only its declared outputs flow on; a stage's module and outputs must stay clear of the hold-out measures. With the resource absent or empty, the calibration ancestry guard is unchanged, byte for byte. Built from S5-SPEC revision 2 on a GPT-6.1 Sol build lane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The build checked the ancestry of every node of a listed stage, including nodes that run after calibration. Extension checkpoints run after calibration and may sit beside hold-out data or unit rules, so listing a binding that also runs there would have refused a sound graph. A stage node upstream of calibration already brings its ancestors into the calibration closure, so that closure alone carries rule 4. The property test now compares the guard with the frozen guard without stages (equal with no stages; person stages only restrict) and checks the corrected monotonicity: dropping a person stage never refuses what it admitted, and dropping a unit stage never admits what it refused. Both new example tests and the property fail against the old roots. Documents the resource in docs/transport-precalibration-stages.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes the four findings of the GPT-6.1 Sol review of 7b933e9: - Recognise the prepared engine_refs inventory by decoded structure, so any JSON spelling (spaced, nested, embedded) refuses as "prepared engine_refs map". - Restrict rule 3 to columns a stage's own nodes produce upstream of calibration, so another binding that declares the same variable name is unaffected. - Cover hold-out sources on stage nodes in the property, with a rules-only domain that exercises producer attribution in every run. - Compare benefit-unit entities without hashing, so absent or empty stages give today's outcome for malformed entities too. Adds optional derived_outputs (NZ's option b) for columns a stage node computes that are not binding variables, such as a solve_zero gross-up. Each must be produced by a stage node upstream of calibration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NZ's Schedule 1 stage keeps the tax inside its gross-up solve, so only the solve's own column flows on. A stage may now list "outputs": [] when it declares derived outputs; a stage with neither still refuses. The derived tests use that shape, and the property draws derived-only declarations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Roots are every calibrate.* node and the calibration endpoint, so a node after the central fit that a variant calibration reads is in that variant's ancestry. Add the regression the NZ hub asked for: a hold-out source or an unlisted unit rules node there refuses in both passes, and the same node feeding no calibration is accepted. The doc now says "outside the ancestry of every calibration node". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage outputs are columns, so rule 3 checked only column reads, and a node that names a listed stage could pass an artifact straight to calibration. For a unit stage, today's guard refuses that graph and the stage path admitted it. Now a node outside the stage that reads one of its artifacts upstream of calibration refuses as "undeclared pre-calibration stage output", in both passes; the composed rows carry artifact edges for this. The property now draws artifact edges apart from column reads. The guard without stages follows column reads only in its compiled predecessors, and a resource selection leaves no marker there, so the no-stage reference is the frozen guard. The doc scopes unit-stage I2 to drops that leave a stage listed, and says the no-stage guard still misses a hold-out selection reached only through column reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 24, 2026
With stages listed, the declaration pass now validates every field it reads, on every declared node and rules binding, before either pass uses it. A malformed field refuses as "node 'x' has malformed <field>" instead of raising KeyError or TypeError, or being read character by character. Read character by character, three spellings were accepted that their list form refuses: - a string `columns` hid a read of an undeclared stage output; - a string selection `path` hid selected data embedding an engine ref; - a string `sources` hid a hold-out source. A repeated node id hid the first node's reads, and a string binding `variables` matched stage outputs by substring. Resource selections follow activation's rules: text resource, list path, known encoding. Binding selections (binding and field) name both as text; engine-reference documents carry no field and are untouched. The guard without stages is unchanged, so I1 identity holds, malformed declarations included. Adds an example test that isolates the composed pass's artifact edges, which only the property test covered before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lections Fixes the five findings of the round-2 review of c4e23bd. - Rule 3 follows each read to its producer, as the compiler resolves it: the owner in the reader's population version, or the nearest base version for an inherited column. A structural node reads its base, and a rewrite reads its incumbent from below its version. A second stage's own column of the same name in a later version no longer refuses as the first stage's private output. The composed pass resolves exactly; the declaration pass falls back to every declared producer where a version is unknown (no population, an undeclared version, a base cycle). - The composed pass reads entitlement rows' declared selections (entitlement._rows), so a variant target compiler selecting holdout_references refuses. Those rows need distinct text ids and well-formed params. - The prepared engine_refs inventory is detected in every string (escaped braces included) and in selected resource data. - An explicit null calibration endpoint refuses. - The doc states that shape refusals come first, and that the guard without stages misses entitlement-row hold-out selections. The P1 generator now draws population versions with shared column names. All 32 mutants are killed by the example tests alone, including rule 3 without version attribution. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lines From the NZ hub's review of c4e23bd (m2), the two doc points still open at 20dfb3e: - A non-text or repeated node id now refuses naming it ("node id None is not text", "node 'x' is declared twice"), in the declaration and in the entitlement rows, so "a malformed field refuses, naming the node" holds. Rules binding ids refuse the same way. - The doc's validation list maps each list to its own fields, and its intro states the guard without stages checks selections only in the rows its declaration pass reaches through population, base and artifact links. - The guard's docstring says which view the derived-output check uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A property-only mutant pass at 20dfb3e killed 9 of 32 mutants, and missed rule 3 without version attribution: the generator drew a graph that tells it from the reference about once in 2,000 examples. It never drew a rewrite or a structural node's inputs. The generator now draws: - plain nodes, outside any stage, that may own a shared column; - a random single column for a one-column read, not the first; - rewrites by person nodes in a filter version of a person column visible from below, which the reference counts as reads of the incumbent; - filter inputs, resolved in the filter's base; - a third domain, "versions" (person, filter and plain nodes), where shared names, rewrites and stage listings are likely, so version attribution decides most outcomes. The property alone now kills 14 of 32 mutants. In ten property-only runs it killed the version-blind rule 3, a compiled rewrite resolved at its own version and compiled structural reads at their own version every time, and rewrites ignored in either pass nine times. The example tests still kill all 32. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The NZ hub's sign-off at 3cb8172 noted that the doc's "every field the checks read is validated first" overstates the code. Three values get no shape check, because the checks read them exactly as composition does: an output's rewrite flag, by truthiness like the compiler; a selection path, through mappings only, which composition also refuses otherwise; and a prepared value, which counts only as "engine_refs", the one prepared parameter composition admits. The doc now says so. No code changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… passes Fixes the five findings of the round-3 review of 3cb8172. - Rule 3 follows each read to every writer whose values it receives, as the executor's writer chain does (executor._writers_of). An owner that keeps earlier values passes on the writers below it: a rewrite, a masked owner (an output with a rows mask) and an EXPAND claim. An EXPAND writes the cells it installs, and a claim reads them without a Slice. A masked owner of a stage's private column no longer hides the values it leaves outside its mask, and a claim forwarding an EXPAND's private cell refuses. Both passes walk the chain; the composed pass reads EXPAND cells and claims from the compiled params as the executor does, and a malformed one refuses. The declaration reads literal expand_cells and materialized_expand_outputs and otherwise falls back: unknown cells write every column and produce none; non-literal claims claim every non-rewrite output. - A derived output may be produced by an EXPAND's cells. - With stages listed, a node's structural kind and its outputs' rows masks are shape-checked. - The prepared engine_refs inventory is a quoted "engine_refs" key and a colon, found after decoding JSON escapes layer by layer. Each layer is one linear pass, so a long harmless string costs one pass, not the quadratic suffix scan; the scan no longer mistakes plain text naming engine_refs for the map. Decoding stops after 32 layers, and a string still escaped then counts as carrying the map, so a crafted chain of escaped backslashes (a pass per layer, quadratic) refuses in at most 33 passes. JSON encoding doubles the backslashes per layer, so no encoded spelling comes near 32. - The non-text extension-row id refusal has a test. - I4 says which message is not fixed: with several dangling links, which one a refusal names follows set iteration, as without stages. The composed pass's writers are checked against the executor's own static writer lists (executor._input_writers): on every P1 graph, the writer and EXPAND examples, and the composed NZ and entitlement graphs. The P1 generator gains a "writers" domain: masked owners, EXPAND stage nodes whose cells a claim owns, and reads that name every writer. A focused mode keeps refusals to about one cause: in 3,000 draws, about 0.6% refuse only through a masked owner's later writer, and 5% only through a claim's read of its cell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-4 mutant sweep at ebe6f3d killed 53 of 54 mutants. The survivor, I5, put the quadratic suffix scan back into _carries_engine_ref_inventory. At the test's 32 KB it took 0.47 s on an idle host, under the 1 s bound. Its cost quadruples per doubling (7.5 s at 128 KB), while the layered decoding takes 7 ms at 128 KB. The test also timed more than the inventory scan. Its spec listed a benefit-unit stage, so the guard's engine-reference scan (_embeds_engine_ref, on main before the stages) decoded JSON from every quote of the parameter too: 0.34 s at 32 KB and 5.2 s at 128 KB of the guard's time. That scan is filed as a separate task, because rewriting it changes the guard without stages. The test now runs at 128 KB in two cases: - the helper alone; - the declaration pass on a person-only stage spec, where the engine-reference scan has no reference to look for. Its four calls return at once and decode nothing. Both take 0.01 s at the head and about 5 s with I5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-4 review (partial: the run stopped on a provider filter after about an hour) found that a stage could change the population. Kernels receive ids, memberships, weights, strata, the frame metadata and the mass log without a Slice, and the executor's writer lists, which the composed pass matches, hold no read for them. The reviewer's case: a stage EXPAND overlays person_household_id, calibration declares no Slice and receives [10, 20], and both passes accepted it. Only structural nodes write those values. The executor refuses a NONE node that owns an id or membership column or declares a weight transition, and a kernel's frame, keep, expand, weights and strata results are for CREATE, FILTER, EXPAND and REWEIGHT nodes. A stage declares only columns, so a node of a listed stage upstream of calibration may not be structural. Both passes now refuse such a node before their other checks, naming the node, its kind and its stage binding, as NZ asked. The compiled rows carry each node's kind for this. NZ checked that none of its stage nodes is structural (517 guard calls over 13 probes). A stage's EXPAND now always refuses, so its cells can no longer be a stage's production: the writes map is gone and a node produces the columns it owns. A stage claim that owns a cell still produces it. An EXPAND still counts as a writer of its cells in both passes, as the executor's lists do. Tests: - Each kind (CREATE, FILTER, EXPAND, REWEIGHT) refuses as a stage node, declared and compiled-only. Unbound structural nodes, and bound ones that are no ancestor of calibration, pass. - The reviewer's three-node graph, executed: calibration receives the stage's membership, the writer lists hold no read of it, and both passes refuse at the EXPAND. - _expand_case: the EXPAND is outside the stage and overlays A's cell. The claim chain is tested through it, including a read-first shape in which calibration is the first node that receives A's values. stage_expand refuses. - P1: EXPANDs are mostly binding-free. A structural node is a person binding's node one time in eight. A claim with a binding owns a new cell as that binding's derived output; otherwise the cell may overlay a person column the EXPAND sees. The reference refuses a listed structural stage node and counts owned columns only. Docs: the new rule and the refusal order. The writer-chain claims now say column reads. The 32-layer rationale is corrected: an encoder that writes \u005c grows linearly, and 33 layers of the key take 344 characters, so harmless text that deep refuses, as does prose that quotes the key and a colon. Binding refusals name the binding, post-calibration nodes still get shape checks, and surrogate pairs decode as two code points. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NZ noted (review of d42fbcb) that the calibration ancestry includes its roots, so a calibrate.* node or the calibration endpoint that carries a stage binding refuses as structural, while the doc said only "upstream of calibration". The code is unchanged: the doc now says each calibration node is in its own ancestry, so "upstream of calibration" includes the calibration nodes, and the structural-stage rule names a REWEIGHT calibration that carries the stage's binding. A new test pins it. Calibration is a REWEIGHT reading A's public output; carrying A's engine reference, it refuses naming cal in both passes (the composed pass alone when only cal is declared); without it, it passes. Under a mutant that exempts the calibration nodes from the rule (ancestors - roots), the two bound cases fail. Run in memory, as the pytest file waits for the disk-hold lift. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round 5 (Sol, at 9959390) found three things. 1. A non-structural stage node can pass an undeclared value to calibration through the frame mass log. A receipt's frame_mass_log_append records reach every later structural node of the node's lineage without a Slice: population.patch appends them, and the executor gives a structural node its base version's cumulative log. No declaration names the log, so neither guard pass can follow it. The UK runtime appends from non-structural nodes, so the graph package cannot refuse appends in general. The transport runner (cli._run) now gives run_graph a population observer. It refuses any node whose admitted mass log differs from the one it received, whether the node ran or was restored from the store, before the node is persisted. No transport kernel appends, so no transport run changes. The doc's premise that only structural nodes write the mass log is corrected. 2. The composed pass read a node's stage bindings by calling _embeds_engine_ref once per binding. That call decodes JSON from every quote of every parameter, so a harmless parameter full of escaped quotes took quadratic time. _embedded_engine_refs now decodes escapes layer by layer, as the inventory scan does: one linear pass per layer, for all bindings at once. A parameter still escaped after 32 layers refuses. The shared decoder now decodes a surrogate pair to one character, as json.loads does. A Hypothesis property checks that the layered scan finds every reference _embeds_engine_ref finds. 3. No test had an EXPAND claim a second cell. The two_claims shape claims a public cell first and a private one second. It is added to the outcome test and to the static executor differential. The changelog's "unaffected" wording now says post-calibration nodes still face the shape checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tches Fixes the round-6 Sol review of 1d1801e (APPROVE, one minor and a nit). The composed pass refused a node with a parameter still JSON-escaped after 32 layers before it knew the ancestry, so a node after calibration, which faces only the shape checks, refused on a harmless note. The ancestry is now computed first, and the bound refuses only upstream of calibration, the calibration nodes included. No check reads a non-ancestor's bindings. The scan's escaped-text match had no test that needed it. A reference of a newline, a backslash and "n", written as its escaped text after a stray backslash, is lost by decoding, which pairs that backslash with the text's first escape; without the match the node left its stage and its derived output refused as unproduced. New tests: that guard case, a property that a reference's escaped text counts in any surrounding text (with the review's case and a non-ASCII one as explicit examples), and the decoding-scan differential over random references with quoteless and stray-backslash spellings. The bound's test now covers a stage node, the calibration node and a post-calibration node. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-7 Sol review of 973c674 (APPROVE, no findings) noted two wording points. The structural rule's loop computes a non-ancestor's bindings and discards them, so the checks below the escape bound use only an ancestor's bindings rather than reading only them. A reference's escaped text counts wherever it appears only within the 32-layer bound: a parameter with any string still escaped after it gives no membership, and upstream of calibration it refuses. The docs bullet and the property's docstring say so. No code changes: compose.py and cli.py parse to the same AST, and the test file differs only in one docstring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round-8 Sol review of 3b97bea (APPROVE) found the round-7 wording point in two more places: the docs' membership paragraph and the bound test's docstring said a post-calibration node's bindings are not read. The composed pass scans every node, and the structural rule computes every node's bindings, but only an ancestor's affect the checks. Both now say the bindings are not used. No code changes: the test file differs only in that docstring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uv.lock pins ruff 0.15.16, which wraps the multi-line lambda in _is_rows in parentheses. This is a layout-only change: the module's AST is identical before and after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WIP. Do not merge before 2026-10-29 12:00Z (UK hold on shared compile modules), and land it before NZ's G8-r2c, which depends on it.
What this does
Adds an optional
precalibration_stagestransport resource: the rules bindings whose declared outputs may feed calibration. Both countries' income producers enter calibration through it (the joint d1952 ruling).Spec: S5-SPEC revision 2, reviewed by the NZ hub. The repo doc is docs/transport-precalibration-stages.md.
"stages": []: the existing guard runs unchanged, and outcomes and messages match a frozen copy of it.executor._writers_of). That is the owner in the reader's population version, then, while that owner keeps earlier values, the writers below it. A rewrite, a masked owner (an output with arowsmask) and an EXPAND claim keep them. The version's EXPAND counts as a writer of its cells.materialized_expand_outputs) reads its cell, both without aSlice.executor._input_writers): on every property graph, the writer and EXPAND examples, and the composed NZ and entitlement graphs.expand_cellsis not a literal list writes every column read through its version;materialized_expand_outputsclaims every non-rewrite output.Slice(ids, memberships, weights, strata, the frame metadata and the mass log), and the executor lets no other node write those. A structural node outside every stage is checked like any other node.derived_outputs): columns a stage node computes that are not binding variables, such as NZ's w2bsimulate.solve_zero@1gross-up output. Each must be owned by a stage node upstream of calibration (a stage claim that owns an EXPAND cell counts). A stage may list"outputs": []and only derived outputs, which is NZ's Schedule 1 shape: the tax stays inside the solve. This is option (b) of the NZ hub's ask.transport_graphrow or the entitlement row the extension instantiated it from (entitlement._rows), whose params are resolved before the graph exists. A code extension's node has no declaration, so it gets the engine-reference and inventory checks only.engine_refsinventory refuses upstream of calibration, on stage nodes too: a mapping with anengine_refskey, at any depth, or a string holding the quoted key"engine_refs"and a colon, as written or after decoding JSON escapes layer by layer. That covers spaced or nested JSON, braces or the key written as escapes (\u007b,\u0065ngine_refs), JSON embedded in a string, and the data a resource selection names. Plain text naming engine_refs is not the map.\\doubles them per layer, but one that writes\u005cgrows linearly: 33 layers of the quoted key take 344 characters. So the bound is reachable, and harmless text that deep refuses, as does prose that quotes the key and a colon. None of the 1.65 million strings in the 179 build JSON files carries the map, and none holds an escape after parsing.calibrate.*node and the calibration endpoint) are checked. A stage binding's nodes outside the ancestry of every calibration, such as extension checkpoints, are unaffected. A node after the central fit that a variant calibration reads is checked.id,kernel, links,structural, params selections,inputs,outputs(with anyrowsmask),artifact_inputsandsources; bindingid,entity,rulespec_pathandvariables; and the calibration endpoint (text when present; an explicit null refuses). In the composed pass, entitlement rows need distinct text ids, none a skeleton id, and well-formed params. A malformed one refuses as "node 'x' has malformed " instead of being read character by character. The guard without stages is unchanged.Invariants (tested)
transport_graphor entitlement row) is upstream of calibration, and no stage output or module carries a hold-out measure.holdout_referencesselection that calibration reaches only through column reads, and any hold-out selection in an entitlement row.Slicecome, upstream of calibration, only from nodes outside every stage. The executor's writer lists cover column reads only, which is why this needs its own rule.Slice. The guard can't see that, so the runner enforces it. No transport kernel appends._embeds_engine_ref) finds, tested over fixed and random references. A reference's JSON-escaped text counts wherever it appears. The scan is linear in the parameter's length. A parameter still escaped after 32 layers refuses upstream of calibration only, where bindings are read.The Hypothesis property (2,000 examples in each of four domains) compares the guard with three things in both passes, and checks the composed pass's writers against the executor's on every graph:
The
mixeddomain draws encoders, unit and person rules, selectors and hold-out nodes, with hold-out sources on stage nodes, variable names shared between bindings, and derived outputs. Therules_onlydomain drops hold-out data, so most graphs pass the other checks and producer attribution is exercised in every run. Since 20dfb3e the domains also draw population versions: filter nodes, a version closed once a filter branches from it, andshared_<entity>columns owned in different versions, with each read naming the producer it resolves to. Since 3cb8172 they also draw plain non-stage owners of a shared column, person rewrites in a filter version (read of the incumbent) and filter inputs from the base. A third domain,versions, makes those likely, so version attribution decides most outcomes. Since ebe6f3d a fourth,writers, draws masked owners and EXPANDs whose cell the next node claims, with reads naming every writer. Its focused mode keeps refusals to about one cause. Since commit 14, EXPANDs are mostly outside every stage, and any structural node is a person binding's node one time in eight, so listed structural stage nodes refuse in the reference too. A claim with a binding owns a new cell as that binding's derived output. Otherwise the cell may overlay a person column the EXPAND sees, so the claim receives every writer below it.Commits
derived_outputs.rules_onlydomain was added.TypeErrorwith no stages. Entities are now compared, never hashed."outputs": []), since otherwise NZ's Schedule 1 stage would have to list the tax variable. A stage with neither still refuses.artifact_inputsentry does; at 3d8e9f9 it raised KeyError or TypeError, as today's guard and_nodestill do. Probing it found three spellings the stage path accepted where their list form refuses, because a string was read character by character:columnshid a read of an undeclared stage output;pathhid selected data embedding an engine reference;sourceshid a hold-out source.A repeated node id hid the first node's reads, and a string binding
variablesmatched stage outputs by substring.bindingwithfield) name both as text. Engine-reference documents and object-valuedprepareddata are untouched.\u007bescapes hid the inventory, and selected resource data was never checked for it. Every string is now decoded, and the selection loop refuses selected data carrying the inventory, in both passes.transport_graph, so a variant target compiler in the real entitlement fixture could selectholdout_referencesand reach calibration through artifact edges. The composed pass now reads the entitlement rows' declared selections; that graph failscompose_transport_graph, and the unmodified fixture still composes (92 nodes).person.sharedin versionv2refused as another stage's private column of the same name increate. Rule 3 is now attributed through version lineage, as above.rewriteflag (by truthiness, as the compiler reads it;Owneddoesn't type-check it), a selectionpath(through mappings only; composition refuses anything else), and apreparedvalue (only"engine_refs"counts, the one prepared parameter composition admits). The doc now says so. A bool check onrewritewould refuse a shape composition accepts.Slice, again executed. Claims are now reads, and EXPANDs are writers of their cells.structuraland outputrowsshape checks, the executor differential, and thewritersproperty domain.jobs/s5-review-4/partial-review.txtof the Belgium hub.Slice, and the executor's writer lists hold no read for them. The reviewer executed it: a stage EXPAND overlaysperson_household_id, calibration declares noSliceand receives[10, 20], and both passes accepted. Only structural nodes write those values: the executor refuses a NONE node that owns an id or membership column or declares a weight transition, and a kernel'sframe,keep,expand,weightsandstrataresults are for CREATE, FILTER, EXPAND and REWEIGHT nodes. So a listed stage's node upstream of calibration may not be structural (above). NZ checked that none of its stage nodes is structural (517 guard calls over 13 probes)._embeds_engine_ref): unchanged here; see "Known limitation". The test the reviewer saw fail was fixed in 339445f.run_graph(calibration receives the stage's membership, the writer lists hold no read of it, and both passes refuse)._expand_casemoved the EXPAND outside the stage, overlaying A'scell, so the claim chain is still tested, with a read-first shape for claims that keep the values below them. P1 is reworked as above.9959390 (doc and test only): NZ noted that the calibration ancestry includes its roots, so a calibration node that carries a stage binding is checked as the stage's node (a REWEIGHT calibration refuses as structural), while the doc said only "upstream of calibration". The doc now says each calibration node is in its own ancestry. A new test pins it: a REWEIGHT calibration carrying A's engine reference refuses naming
calin both passes (the composed pass alone when onlycalis declared), and without it passes.1d1801e: fixes the round-5 Sol review of 9959390 (REQUEST_CHANGES: one major, two minors, a nit).
frame_mass_log_appendrecords reach every later structural node of its lineage without aSlice, because the executor gives a structural node its base version's cumulative log. The reviewer executed it: stage A's node appends a conserving record whose reason is its private value, calibration sets its weight from it, and both passes accepted.UKStageKernel), so the graph package can't refuse appends in general.cli._run) passesrun_grapha population observer. It refuses any node whose admitted mass log differs from the log it received, cold or restored, before the node is persisted.uk_runtimeappends (git grep frame_mass_log_append), so no transport run changes._embeds_engine_refcall per binding, which decodes JSON from every quote. A 16 KB harmless parameter took 1.8 s._embedded_engine_refs: escapes are decoded layer by layer, as in the inventory scan, in one linear pass per layer for all bindings, up to 32 layers.json.loadsdoes.two_claimsshape claims a public cell first and a private one second. It is in the outcome test and the executor differential.raw_decodecalls at 64,000 escaped quotes;_embeds_engine_reffinds (nestedjson.dumps, ASCII escaping with surrogate pairs, objects, lists, text embedding).n, written as its escaped text after a stray backslash. Decoding pairs that backslash with the text's first escape, so no layer holds the reference. Without the match, the node left its stage and its derived output refused as unproduced. My own 40,000-case fuzz had not found it. Tests only:_embeds_engine_refdifferential now draws random references too, with quoteless and stray-backslash spellings. It passed 20,000 examples at 1d1801e.compose.pyandcli.pyparse to the same AST as at 973c674, and the test file differs only in that docstring.compose.pywith the ruff thatuv.lockpins (0.15.16). That version wraps the multi-line lambda in_is_rowsin parentheses. The module's AST is identical before and after. An independent review of Belgium's OK for microcosm#1397 found the drift. CI runs onlyruff check, which passed at 91d46e1.CompiledGraphimport, socompose.py's added import becomes context. This PR differs from main on exactly six paths.Tests run
test_transport_composed_graph.py, single file,-n 0, with the normal conftest: 210 passed at 293bec1, and the 58 pre-calibration tests pass at 7b933e9.At 062d89c:
test_transport_composed_graph.pyplustest_transport_graph.py,-n 4, 259 passed.At ead5c9c:
test_transport_composed_graph.py,-n 4, 245 passed. At 16c35df: 249 passed.At 3d8e9f9:
test_transport_composed_graph.pyplustest_transport_graph.py,-n 4, 268 passed. The property passes on six fixed Hypothesis seeds.At c4e23bd, single process (
-p no:xdist) because of the host reservation: every pre-calibration and ancestry test intest_transport_composed_graph.py, the property included, 149 passed. The full required files are queued behind the reservation and the fleet disk hold.Mutation check, now sixteen mutants. At c4e23bd every one is killed by the example tests alone, with the property excluded. They are:
outputsagain;calibrate.*nodes in the declaration pass (killed by the variant-calibration regression);At 3d8e9f9 the property test, run by itself, also killed six of the first eleven: rule 3 by name, the hold-out source check, two of the derived-output mutants, and both artifact mutants.
At 20dfb3e, single process (
-p no:xdist) because of the host reservation: the 173 pre-calibration and ancestry tests intest_transport_composed_graph.pypass, the property at 2,000 examples per domain included.Mutation check at 20dfb3e, 32 mutants, every one killed by the example tests alone (property excluded). Results are in
jobs/s5-build/mutants-r3-examples/andmutants-r3-n8/of the Belgium hub. Beyond the sixteen above (one now split into the version-blind rule 3 of c4e23bd), they are:A property-only pass over the 32 mutants is running.
At c05685c, single process: 177 pre-calibration and ancestry tests pass. The NZ hub's probe scripts (
q1_nzfit,q1_real_compose,q3_branches,q2_probes,q2e_nz), run unmodified, give the same outputs as at 20dfb3e. Against the hub's c4e23bd outputs:holdout_referencescase on the real entitlement fixture now refuses namingnz.v.V1.targets(no stages: still accepts);b.calcases now accept.The 32-mutant example sweep at c05685c kills all 32 (
jobs/s5-build/mutants-c056-examples/).At 3cb8172, single process: 178 pre-calibration and ancestry tests pass (the property now has three domains). Property-only mutant results:
jobs/s5-build/mutants-c056-property-v2/(all 32) and-v4/(ten repeats of the version mutants).At ebe6f3d, single process: all 363 tests in
test_transport_composed_graph.pypass (111 s), 225 of them pre-calibration and ancestry tests, the property at 2,000 examples in each of four domains included.At ebe6f3d, the S2-D item 6 required files, one file at a time with
-n 4(jobs/s5-build/required-ebe6f3d1a/). All pass, and the worktree was clean throughout:test_country_spec136;test_nz_spec_package1,192;test_spec_engine_country_bundles21;test_spec_only_country_packages8;test_transport_population_kernels30;test_transport_dependent_children24;test_transport_graph15;test_transport_composed_graph363;test_transport_target_kernels15.Mutation sweep at ebe6f3d, 54 mutants (
jobs/s5-mutants-r4/summary.txt, all pre-calibration and ancestry tests): 53 killed. Beyond the 32 of 20dfb3e, the new ones are:structuralandrowsshape checks off;\uescapes, deep strings passing, and no layer bound.The property's
writersdomain kills W1–W12. The executor differential kills every compiled writer mutant (W2, W4, W6, W8, W10, the compiled rewrites, no compiled base walk). The survivor, I5, is fixed in 339445f and killed there (jobs/s5-mutants-r4/at-339445fbc/).At 339445f: all 364 tests in
test_transport_composed_graph.pypass (-n 4). The only change from ebe6f3d is that test.At d42fbcb, under Fleet ops' Waiver 3 for DISK HOLD populace-data: registry-driven loader for published populations #4 (one file, then a short mutant sweep): all 382 tests in
test_transport_composed_graph.pypass (-n 4, 78 s).Mutants of the new rule (
jobs/s5-mutants-r5/). The run stopped at the disk floor after two of eight; the rest, and a rerun of the 52 round-4 mutants that still apply, follow the lift. (W11 and W12 mutated the cell production this commit removes.)stage_expand, the executed membership test, P1 in all four domains).structural: killed by 8 tests (each kind compiled-only, P1 in all four domains).The round-5 mutant sweep finished at 9959390, once DISK HOLD populace-us-2024-9f1260b-20260611: eCPS-free build provenance #6 lifted (
jobs/s5-mutants-r5/summary.txt). It ran in place with-n 2while load1 > 60, restoringcompose.pybyte for byte after each mutant. Of 63 mutants, the 9 structural-rule mutants (S1–S9) and the 54 round-4 mutants that still apply, 61 are killed. W9 and W13 survive as equivalent; the round-5 reviewer confirmed W9.At 9959390 (DISK HOLD populace-data: registry-driven loader for published populations #4 still in force, so no pytest): the new test's four cases pass, called in memory. Under a mutant that exempts the calibration nodes from the rule (
ancestors - roots), its two bound cases fail.CI at 9959390: all 10 jobs pass (run 40181956868). Both engine-free jobs (3.13, 3.14) run
test_transport_composed_graph.py, the new test included, with 21,356 passed each.At 1d1801e: the new and touched tests pass (30 selected,
-n 2). They are the runner, two-claim, linear-scan, 32-layer and property tests, the executor differentials and the EXPAND outcome test. The reviewer's mass-log leak, run throughcli._run, refuses cold and restored; a plainrun_graphstill delivers it.At 1d1801e, locally: all 399 tests in
test_transport_composed_graph.pypass (-n 2, rc 0). Of the round-6 mutant sweep (jobs/s5-mutants-r6/), the reviewer's O3 and O4 (only the first claimed cell read, compiled and declared) ran before Fleet ops' 22:24Z focus mode. Both are killed, by the two-claim writer differential and the two-claim outcome test. I stopped the sweep between mutants for the focus; its other 16 mutants rerun at 973c674.CI at 1d1801e: all 10 jobs pass (run 40230656382). The round-6 reviewer read the engine-free 3.14 log: the full file, no keyword selector, 21,391 passed.
At 973c674: the new and touched tests pass (24 selected,
-n 2): the 32-layer, escaped-text, scan, linear, mass-log and two-claim tests.At 973c674, locally: all 403 tests in
test_transport_composed_graph.pypass (-n 2, rc 0).Mutant sweep at 973c674 (
jobs/s5-mutants-r7/summary.txt): in place,-n 2, restoring both files byte for byte after each run. 19 of 20 are killed:raw_decodecalls, and the bound;Noneas empty (M6): the bound, on the stage node and calibration;aandbappenders, cold and restored.test_ci_integration_job.py: 9 passed (it reads the agent guide).ruff checkandruff format --checkwithuv.lock's ruff 0.15.16: clean at 6197d74. Earlier rounds used ruff 0.14.10, which doesn't make the lambda wrapping that commit 20 adds.Known limitation (predates this PR)
The guard's engine-reference scan (
_embeds_engine_ref) is on main unchanged: this PR only threadsallowed_referencesthrough it. Since 1d1801e, S5's own composed membership scan no longer uses it (I7); the unit-binding calls below are main's. Like the old inventory scan, it decodes JSON from every quote of a parameter that is not JSON.Rewriting it would change the guard without stages, which this PR keeps identical to main (I1), so it is a separate task. That task keeps every outcome identical, checked by a differential against a frozen copy, and lands after this PR.
Plan
jobs/s5-review-5/review/of the Belgium hub.jobs/s5-review-6/review/. It executed the composed skeleton (27 nodes), extended (31) and entitlement (92) graphs throughexecute_transport_graphwith the mass-log observer: all pass. It found no other undeclared carrier.jobs/s5-review-7/review/.NZ-VERDICT-973c.md, covering 9959390..973c674).node_bindingsread incheck()is ancestry-guarded, and no NZ node carries a parameter escaped past 32 layers.frame_mass_log_appendwriters are only inuk_runtimeat main and at NZ's three heads, and no transport module imports them, so no NZ node appends. NZ takes on that its w2b, w3 and r2c nodes never change the mass log.compose.py's AST is identical, the test AST is identical with docstrings masked, and the docs change is one bullet.git merge-treeof either head with each of NZ's three heads is clean.NZ-VERDICT-973c.md, addendum 15:4xZ). Onecompose.pyhunk (+4/−2) wraps the_is_rowslambda;ast.dumpis identical at 91d46e1 and 6197d74. The merge-tree is clean against all six current NZ heads: Allocate Census residuals through shared reference profiles #1375 840d933, Add feed-derived NZ reference ageing factors (G8-r2a) #1382 c4ddcb4, Add exact income-band intervals and boundary diagnostics #1394 fca69c1, Normalize donor money with exact declared factors #1395 db02ae8, Add receipt contract v2 and selection-aware reference preflight #1397 84c2082 and Reconcile regional Census family-type counts under d1932 #1398 9c7c4a2.jobs/s5-review-8/review/.jobs/s5-review-9/review/.jobs/s5-review-10/review/.CompiledGraphimport is the only difference.test_transport_composed_graph.pygave 432 passed and 1 failed on a loaded host (-n 2). The failure istest_precalibration_ancestry_composed_stage_scan_is_linearatassert elapsed < 1, which saw 1.75 s. Its acceptance and decoder-count assertions passed, an isolated rerun passed, and CI passes.test_country_spec.pygave 136 passed andtest_spec_only_country_packages.py28.executor._input_writersdifferential: 161 calls on 18 NZ graphs, 0 mismatches. No NZ stage node is structural. A new probe, q10, makes NZ's rates filter, JS gross-up EXPAND, V1 rates REWEIGHT and entitlement CREATE structural in turn; each refuses with this wording. A filter bound only through an embedded engine reference is refused by the composed pass alone, and post-calibration and unread nodes pass. Its note on the calibration roots is answered in 9959390, and the sign-off extends there: the source is unchanged, and the new test matches its q10 run onnz.calibrate.executor._input_writerscovers 161 calls on 18 NZ graphs, with 0 mismatches. The old and new inventory detectors give no hits across the S5, Add exact income-band intervals and boundary diagnostics #1394 and Normalize donor money with exact declared factors #1395 NZ JSON.build/nzJSON string is 1,361 characters, with at most 6 quotes, so NZ is far from the engine-reference scan's slow case.build/nzJSON files, and I1 holds. Its one optional nit is fixed in bfd5e85. A non-stage node that rewrites a stage's private column upstream of calibration now refuses.test_transport_composed_graph.pypasses again at 339445f.🤖 Generated with Claude Code