From b46955507de66bd739c21d5ef29434ab6f282177 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 19:49:34 -0400 Subject: [PATCH 01/27] Start the amendment 20 lane journal (keyed draw streams) Records the scope, the platform of record for the H1 pins, and the reconnaissance findings that shape the plan: the integration branch carries no executor change for KEYED, editing qrf.py moves the fit.qrf@1 implementation hash and all three pinned platform node keys, and graph_parity_fixtures.generate() would drop the two x86_64 pins. Co-Authored-By: Claude Opus 5 --- PROGRESS-amendment-20-keyed-draws.md | 55 ++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 PROGRESS-amendment-20-keyed-draws.md diff --git a/PROGRESS-amendment-20-keyed-draws.md b/PROGRESS-amendment-20-keyed-draws.md new file mode 100644 index 000000000..93291e134 --- /dev/null +++ b/PROGRESS-amendment-20-keyed-draws.md @@ -0,0 +1,55 @@ +# Amendment 20 — keyed draw streams (lane journal) + +Branch: `amend-keyed-seed-and-uniform-draws`, cut from `origin/main` at `3094bfe84`. +Worktree: `~/PolicyEngine/_worktrees/microcosm-amend-keyed`. No push, no new branches. +Platform of record for H1 pins: **arm64/darwin/py3.14** (the authoring platform). + +## State + +Starting. Reconnaissance complete; nothing implemented yet. + +## Scope (what this lane lands) + +1. `graph/kernel.py`: `SeedSource.KEYED` + the `KernelContext.rng` docstring change. + **Not** `ArtifactValue` / `KernelContext.artifacts` — a separate lane owns those. +2. `graph/randomness.py` (new): `keyed_uniform` and its coordinate encoding. +3. `graph/__init__.py`: export `keyed_uniform`, inserted in sorted position. +4. `fit/qrf.py`: `_draw_target_from_uniforms` + `FittedRegimeGatedQRF.predict_from_uniforms`. +5. `docs/graph-acceptance.md`: amendment 20 entry. +6. `docs/graph-interface.lock`: re-record `kernel.py`. +7. Tests: `SeedSource.KEYED` contract; `keyed_uniform` units; `predict_from_uniforms` units. +8. H1 parity: regenerate `fit.qrf` pins (implementation hash moves because `qrf.py` + changed); `direct.csv` must stay byte-identical. + +## Reconnaissance findings (verified, 2026-09-11) + +- The integration branch adds **no** amendment text and **no** executor change that + honours `KEYED`: `git diff origin/main origin/microcosm-us-launch-integration-20260909 + -- .../executor.py` is entirely artifacts / lazy population retention / execution + metadata. `fit/kernels.py` has **no** diff on that branch. So the minimal coherent + subset for `KEYED` touches neither file. +- `QRFKernel.implementation_hash()` hashes `microcosm.fit.qrf`'s module bytes + (`packages/microcosm-fit/src/microcosm/fit/kernels.py`), so editing `qrf.py` + moves the `fit.qrf@1` implementation hash and every `fit.qrf` node key. +- `node_key` folds in `kernel_impl_hash` **and**, for `PLATFORM_BITWISE`, the platform + fingerprint (`graph/keys.py`). `fit.qrf/pins.json` pins three platforms + (`arm64/darwin/py3.14`, `x86_64/linux/py3.13`, `x86_64/linux/py3.14`); all three + node keys move, and only the local one can be produced by running the graph. +- `tools/graph_parity_fixtures.py generate()` **resets** `pins["platforms"]` to the + local platform only, so a bare regeneration would silently drop the two x86_64 pins + and orphan their `direct.csv` files, downgrading CI on Linux to the off-platform + branch of `test_h1_kernel_parity` (which asserts no bytes). +- C4's static check (`test_acceptance_c_seeds.py::test_c4_seed_from_identity`) ASTs + every `microcosm/graph/**/*.py`: no `RandomState`, no `*.random.seed`, and any + `default_rng` call takes exactly one argument. `randomness.py` uses none of these. +- New test files must sit flat in `packages//tests/` and be `test_*.py`; + `microcosm-graph`/`microcosm-fit` classify to fast `rest` and engine `us-am` + (`tools/ci_test_groups.py`), so no lane bookkeeping is needed beyond `--verify`. + +## Done + +- (nothing yet) + +## Next + +- Red-first tests, then the implementation, then the pins. From 8d55f81726aea7ee29b63df8bef318e4aa38aac9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 19:53:36 -0400 Subject: [PATCH 02/27] Red: keyed draw streams and SeedSource.KEYED have no implementation yet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test_graph_randomness.py holds keyed_uniform to the properties amendment 20 is for — stable coordinates give one draw, different coordinates give another, and order, batching, and unrelated identities cannot reach a draw — plus the documented sha256-u53-v1 formula, recomputed in the test from the specification rather than read back from the code. test_graph_kernel_contract.py adds the SeedSource.KEYED contract: the member is additive (every existing kernel keeps the value it declares, so no existing node key moves), it is part of the capability projection and so of node identity, it round-trips through a manifest receipt, and a keyed kernel draws the same values from two contexts whose generators sit at different positions. Both modules fail to import: microcosm.graph exports no keyed_uniform. Co-Authored-By: Claude Opus 5 --- .../tests/test_graph_kernel_contract.py | 88 +++++++- .../tests/test_graph_randomness.py | 207 ++++++++++++++++++ 2 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 packages/microcosm-graph/tests/test_graph_randomness.py diff --git a/packages/microcosm-graph/tests/test_graph_kernel_contract.py b/packages/microcosm-graph/tests/test_graph_kernel_contract.py index 057bec79f..21597e764 100644 --- a/packages/microcosm-graph/tests/test_graph_kernel_contract.py +++ b/packages/microcosm-graph/tests/test_graph_kernel_contract.py @@ -1,8 +1,10 @@ -"""Kernel-protocol contracts of the frozen interface (amendment 13). +"""Kernel-protocol contracts of the frozen interface (amendments 13 and 20). A kernel that claims bounded numeric movement declares the bound; a bitwise kernel declares none; the context hands readers their inputs' declared -tolerances; and the two new declaration fields round-trip through JSON. +tolerances; and the two new declaration fields round-trip through JSON. A +``KEYED`` kernel declares that its draws come from stable coordinates rather +than from a position in the executor's generator. """ from __future__ import annotations @@ -24,11 +26,13 @@ Numeric, NumericScope, Owned, + SeedSource, SourceRef, StructuralDelta, Tolerance, graph_from_json, graph_to_json, + keyed_uniform, ) @@ -209,3 +213,83 @@ def test_context_numerics_default_empty_and_carry_scopes() -> None: ) assert context.numerics[("person", "income")] is scope assert context.tolerances[("person", "income")] is None + + +def test_seed_source_keyed_is_an_additive_declaration() -> None: + """Amendment 20: a third seed source, and the two others are untouched. + + ``KEYED`` says the kernel's draws come from normative stream parameters and + stable coordinates rather than from ``KernelContext.rng`` or a literal + ``seed`` param. The member is additive: every existing kernel keeps the + value it declares, so no existing node key moves (A5/A7). + """ + assert SeedSource.KEYED.value == "keyed" + assert SeedSource.EXECUTOR.value == "executor" + assert SeedSource.PARAM.value == "param" + assert SeedSource.NONE.value == "none" + assert set(SeedSource) == { + SeedSource.EXECUTOR, + SeedSource.PARAM, + SeedSource.KEYED, + SeedSource.NONE, + } + + keyed = Capabilities(determinism=Determinism.SEEDED, seed_source=SeedSource.KEYED) + assert keyed.seed_source is SeedSource.KEYED + with pytest.raises(TypeError, match="Capabilities.seed_source must be a SeedSource"): + Capabilities(determinism=Determinism.SEEDED, seed_source="keyed") # type: ignore[arg-type] + + +def test_keyed_is_part_of_a_node_identity_and_survives_the_manifest() -> None: + """The declaration is contract, so it keys the node and round-trips. + + A kernel that reads the same inputs through a keyed stream is not the + kernel that reads them through the executor's generator; the capability + projection separates them, and a receipt spells the member back. + """ + from microcosm.graph.keys import _capabilities_projection + from microcosm.graph.manifest import _capability_contract_fields + + base = Capabilities(determinism=Determinism.SEEDED, seed_source=SeedSource.EXECUTOR) + keyed = dataclasses.replace(base, seed_source=SeedSource.KEYED) + projection = _capabilities_projection(keyed) + assert projection["seed_source"] == "keyed" + assert projection != _capabilities_projection(base) + assert _capability_contract_fields(projection)[2] is SeedSource.KEYED + + +def test_a_keyed_kernel_draws_from_coordinates_not_from_the_context_rng() -> None: + """The context still offers ``rng``; a keyed kernel simply does not spend it. + + This is the behaviour the member names, exercised end to end at the kernel + protocol level: two contexts whose generators are at different positions + hand the same coordinates the same draws. + """ + node = Node("impute", "toy.keyed@1", params={"experiment": "amendment-20"}) + + def draw(context: KernelContext) -> np.ndarray: + return keyed_uniform( + stream=( + "sha256-u53-v1", + str(context.params["experiment"]), + 0, + 0, + ), + keys=[(person, "wages") for person in (11, 12, 13)], + ) + + def context_at(position: int) -> KernelContext: + rng = np.random.default_rng(7) + rng.random(position) + return KernelContext( + node=node, + tables={}, + weights={}, + strata=pd.Series(dtype="int64"), + params=node.params, + rng=rng, + ) + + early, late = context_at(0), context_at(512) + assert early.rng.bit_generator.state != late.rng.bit_generator.state + assert draw(early).tobytes() == draw(late).tobytes() diff --git a/packages/microcosm-graph/tests/test_graph_randomness.py b/packages/microcosm-graph/tests/test_graph_randomness.py new file mode 100644 index 000000000..3b2a15035 --- /dev/null +++ b/packages/microcosm-graph/tests/test_graph_randomness.py @@ -0,0 +1,207 @@ +"""Keyed draw streams (amendment 20): ``microcosm.graph.randomness``. + +A ``SeedSource.KEYED`` kernel does not consume an RNG in row order; it asks for +the uniform belonging to a coordinate. These tests hold ``keyed_uniform`` to the +three properties that makes worth having — stable coordinates give the same +draw, different coordinates give a different one, and nothing about packing, +ordering, or unrelated identities can reach a draw — plus the documented +``sha256-u53-v1`` formula, recomputed here from the specification rather than +read back from the implementation. +""" + +from __future__ import annotations + +import hashlib + +import numpy as np +import pytest + +from microcosm.graph import keyed_uniform +from microcosm.graph.canonical import canonical_json + +STREAM = ("sha256-u53-v1", "amendment-20", 0, 0) + + +def _spec_uniform(stream: tuple, key: tuple) -> float: + """The documented draw, derived here from the specification's own words. + + ``keyed_uniform``'s docstring defines a draw as the top 53 bits of the + SHA-256 digest over a domain prefix, the canonical stream, and the canonical + tagged coordinates, divided by ``2**53``. Recomputing it from that sentence + is the difference between testing the algorithm and testing the code. + """ + tags = {bool: "bool", int: "int", str: "str", float: "float"} + encoded = canonical_json([[tags[type(value)], value] for value in key]) + digest = hashlib.sha256( + b"microcosm-graph/keyed-uniform/1\0" + canonical_json(stream) + b"\0" + encoded + ).digest() + return (int.from_bytes(digest[:8], "big") >> 11) / 2**53 + + +def test_stable_coordinates_give_one_draw_and_different_ones_give_another() -> None: + keys = [(1, "wages", 2026, 0), (2, "wages", 2026, 0)] + first = keyed_uniform(stream=STREAM, keys=keys) + again = keyed_uniform(stream=STREAM, keys=keys) + + assert first.dtype == np.dtype("float64") + assert first.shape == (2,) + assert first.tobytes() == again.tobytes() + assert first[0] != first[1] + assert ((first >= 0.0) & (first < 1.0)).all() + + # Every coordinate position is live: moving any one of the four moves the + # draw, so a process, a period, or a draw index cannot collide with another. + base = keyed_uniform(stream=STREAM, keys=[(1, "wages", 2026, 0)])[0] + for moved in ( + (2, "wages", 2026, 0), + (1, "dividends", 2026, 0), + (1, "wages", 2027, 0), + (1, "wages", 2026, 1), + ): + assert keyed_uniform(stream=STREAM, keys=[moved])[0] != base + + +def test_repeated_coordinates_intentionally_repeat_the_draw() -> None: + """The docstring says so out loud; a row asked for twice gets one answer.""" + values = keyed_uniform(stream=STREAM, keys=[(7, "wages"), (7, "wages")]) + assert values[0] == values[1] + + +def test_a_draw_is_blind_to_order_batching_and_unrelated_identities() -> None: + """Packing cannot reach a draw: this is C1 and C2 for randomness.""" + keys = [(1, "wages"), (2, "wages"), (3, "wages")] + straight = keyed_uniform(stream=STREAM, keys=keys) + reversed_ = keyed_uniform(stream=STREAM, keys=list(reversed(keys))) + assert straight.tobytes() == reversed_[::-1].copy().tobytes() + + chunked = np.concatenate( + [ + keyed_uniform(stream=STREAM, keys=keys[:1]), + keyed_uniform(stream=STREAM, keys=keys[1:]), + ] + ) + assert straight.tobytes() == chunked.tobytes() + + # Inserting an unrelated identity ahead of a row leaves that row's draw + # alone — the failure mode a positionally consumed RNG cannot avoid. + with_intruder = keyed_uniform( + stream=STREAM, keys=[(99, "wages"), *keys] + ) + assert with_intruder[1:].tobytes() == straight.tobytes() + + +def test_the_draw_is_the_documented_sha256_u53_formula() -> None: + keys = [(1, "wages", 2026, 0), (2, "wages", 2026, 0), ("h-3", True, -1.5)] + expected = np.asarray( + [_spec_uniform(STREAM, key) for key in keys], dtype=np.float64 + ) + assert keyed_uniform(stream=STREAM, keys=keys).tobytes() == expected.tobytes() + + +def test_streams_are_separated_by_experiment_replicate_and_base_seed() -> None: + key = [(1, "wages")] + base = keyed_uniform(stream=STREAM, keys=key)[0] + for stream in ( + ("sha256-u53-v1", "amendment-20-b", 0, 0), + ("sha256-u53-v1", "amendment-20", 1, 0), + ("sha256-u53-v1", "amendment-20", 0, 1), + ): + assert keyed_uniform(stream=stream, keys=key)[0] != base + + +def test_coordinate_types_are_tagged_so_look_alikes_do_not_collide() -> None: + """``True`` is not ``1`` and ``1`` is not ``"1"``: each carries its tag.""" + drawn = { + label: keyed_uniform(stream=STREAM, keys=[(value,)])[0] + for label, value in ( + ("bool", True), + ("int", 1), + ("str", "1"), + ("float", 1.0), + ) + } + assert len(set(drawn.values())) == 4 + + +def test_numpy_scalars_are_the_python_scalars_they_hold() -> None: + """A coordinate read out of a column must not draw differently.""" + assert ( + keyed_uniform(stream=STREAM, keys=[(np.int64(5), "wages")])[0] + == keyed_uniform(stream=STREAM, keys=[(5, "wages")])[0] + ) + assert ( + keyed_uniform(stream=STREAM, keys=[(np.bool_(True),)])[0] + == keyed_uniform(stream=STREAM, keys=[(True,)])[0] + ) + + +def test_draws_are_bytes_backed_and_read_only() -> None: + values = keyed_uniform(stream=STREAM, keys=[(1,), (2,)]) + assert not values.flags.writeable + with pytest.raises(ValueError): + values[0] = 0.5 + + +def test_an_empty_key_list_draws_nothing() -> None: + values = keyed_uniform(stream=STREAM, keys=[]) + assert values.dtype == np.dtype("float64") + assert values.shape == (0,) + + +def test_a_malformed_stream_is_refused() -> None: + with pytest.raises(TypeError, match="algorithm, experiment_id"): + keyed_uniform(stream=("sha256-u53-v1", "e", 0), keys=[(1,)]) + with pytest.raises(TypeError, match="algorithm, experiment_id"): + keyed_uniform(stream=["sha256-u53-v1", "e", 0, 0], keys=[(1,)]) # type: ignore[arg-type] + with pytest.raises(ValueError, match="Unsupported random stream algorithm"): + keyed_uniform(stream=("sha256-u53-v2", "e", 0, 0), keys=[(1,)]) + with pytest.raises(ValueError, match="experiment_id must be non-empty"): + keyed_uniform(stream=("sha256-u53-v1", "", 0, 0), keys=[(1,)]) + with pytest.raises(ValueError, match="non-negative integers"): + keyed_uniform(stream=("sha256-u53-v1", "e", -1, 0), keys=[(1,)]) + with pytest.raises(ValueError, match="non-negative integers"): + keyed_uniform(stream=("sha256-u53-v1", "e", 0, -1), keys=[(1,)]) + # A bool is not a replicate index; it spells an integer without being one. + with pytest.raises(ValueError, match="non-negative integers"): + keyed_uniform(stream=("sha256-u53-v1", "e", True, 0), keys=[(1,)]) + + +def test_a_malformed_coordinate_is_refused() -> None: + with pytest.raises(TypeError, match="nonempty coordinate tuple"): + keyed_uniform(stream=STREAM, keys=[()]) + with pytest.raises(TypeError, match="nonempty coordinate tuple"): + keyed_uniform(stream=STREAM, keys=[[1, "wages"]]) # type: ignore[list-item] + with pytest.raises(TypeError, match="non-null finite scalar identities"): + keyed_uniform(stream=STREAM, keys=[(None,)]) + with pytest.raises(TypeError, match="non-null finite scalar identities"): + keyed_uniform(stream=STREAM, keys=[(float("nan"),)]) + with pytest.raises(TypeError, match="non-null finite scalar identities"): + keyed_uniform(stream=STREAM, keys=[(float("inf"),)]) + with pytest.raises(TypeError, match="non-null finite scalar identities"): + keyed_uniform(stream=STREAM, keys=[({"person": 1},)]) # type: ignore[dict-item] + + +def test_keyed_draws_read_and_mutate_no_numpy_rng_state() -> None: + """The point of the amendment: a draw is not a position in a stream. + + Neither a generator nor the process-global legacy state moves, so a keyed + kernel cannot perturb an unrelated stream and cannot be perturbed by one. + """ + rng = np.random.default_rng(20) + generator_before = rng.bit_generator.state + global_before = np.random.get_state() + + values = keyed_uniform(stream=STREAM, keys=[(i, "wages") for i in range(64)]) + + assert rng.bit_generator.state == generator_before + after = np.random.get_state() + assert after[0] == global_before[0] + assert np.array_equal(after[1], global_before[1]) + assert after[2:] == global_before[2:] + # The same draws again, with a generator drained in between: consumption + # order elsewhere is invisible to a keyed draw. + rng.random(1_000) + assert ( + keyed_uniform(stream=STREAM, keys=[(i, "wages") for i in range(64)]).tobytes() + == values.tobytes() + ) From e75bd6b915334fd9ead346b3d9fae905a9e6250a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 19:54:20 -0400 Subject: [PATCH 03/27] Green: SeedSource.KEYED and keyed_uniform; relock kernel.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kernel.py gains the KEYED member — normative stream params and stable draw coordinates — and KernelContext.rng's docstring now names all three seed sources instead of claiming to be the only randomness a kernel may use. Adding an enum member is additive: every existing kernel keeps the value it declares, so no existing node key moves. randomness.py is taken verbatim from origin/microcosm-us-launch-integration-20260909 (69 lines), so the amendment and the integration branch carry one implementation rather than two. A draw is the top 53 bits of SHA-256 over a domain prefix, the canonical stream, and the canonical tagged coordinates; it reads and mutates no numpy RNG state. docs/graph-interface.lock re-records kernel.py at 3483d091b03b19ae35c0268c01cb9e0f76c4cd63742083130567321d70da6048 (decl.py unchanged). The lock is plain sha256 of the file bytes, confirmed by shasum -a 256 -c against the unchanged decl.py line. graph/__init__.py exports keyed_uniform in sorted position among the lowercase callables (graph_to_json, keyed_uniform, load_source). Co-Authored-By: Claude Opus 5 --- docs/graph-interface.lock | 2 +- .../src/microcosm/graph/__init__.py | 2 + .../src/microcosm/graph/kernel.py | 6 +- .../src/microcosm/graph/randomness.py | 69 +++++++++++++++++++ .../tests/test_graph_kernel_contract.py | 4 +- .../tests/test_graph_randomness.py | 6 +- 6 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 packages/microcosm-graph/src/microcosm/graph/randomness.py diff --git a/docs/graph-interface.lock b/docs/graph-interface.lock index cabec2d32..bb3d07f6c 100644 --- a/docs/graph-interface.lock +++ b/docs/graph-interface.lock @@ -1,2 +1,2 @@ 635fef92c599c298e7f19ca0badfa85aa040bf8e81eafed59f37c48db1fcff06 decl.py -eaf07da2eded1b1895aa0c59f603eb93744ed928df65aa9e65aa633762833949 kernel.py +3483d091b03b19ae35c0268c01cb9e0f76c4cd63742083130567321d70da6048 kernel.py diff --git a/packages/microcosm-graph/src/microcosm/graph/__init__.py b/packages/microcosm-graph/src/microcosm/graph/__init__.py index aaa781484..3cc32a43d 100644 --- a/packages/microcosm-graph/src/microcosm/graph/__init__.py +++ b/packages/microcosm-graph/src/microcosm/graph/__init__.py @@ -52,6 +52,7 @@ source_hash, ) from .keys import platform_fingerprint +from .randomness import keyed_uniform __all__ = [ "platform_fingerprint", @@ -112,6 +113,7 @@ "explain_html", "graph_from_json", "graph_to_json", + "keyed_uniform", "load_source", "run_graph", "source_hash", diff --git a/packages/microcosm-graph/src/microcosm/graph/kernel.py b/packages/microcosm-graph/src/microcosm/graph/kernel.py index 567a57ea5..3fba67290 100644 --- a/packages/microcosm-graph/src/microcosm/graph/kernel.py +++ b/packages/microcosm-graph/src/microcosm/graph/kernel.py @@ -151,6 +151,7 @@ class SeedSource(StrEnum): EXECUTOR = "executor" # ``KernelContext.rng``, derived from the node key PARAM = "param" # a literal ``seed`` parameter (legacy parity kernels) + KEYED = "keyed" # normative stream params and stable draw coordinates NONE = "none" @@ -286,8 +287,9 @@ class KernelContext: in the node's inputs or outputs. strata: Read-only per-person strata of the population version. params: The node's parameters. - rng: A generator seeded from the node key. The only randomness a - kernel may use. + rng: The default generator seeded from the node key. KEYED kernels + instead use normative stream params and stable coordinates through + keyed_uniform; PARAM kernels use their declared literal seed. sources: Source name to a content-verified path, for declared sources only. tolerances: ``(entity, column)`` of each declared input column to diff --git a/packages/microcosm-graph/src/microcosm/graph/randomness.py b/packages/microcosm-graph/src/microcosm/graph/randomness.py new file mode 100644 index 000000000..5a5510bc2 --- /dev/null +++ b/packages/microcosm-graph/src/microcosm/graph/randomness.py @@ -0,0 +1,69 @@ +"""Stable random coordinates, independent of graph packing and cache identity. + +A stream is ("sha256-u53-v1", experiment_id, replicate, base_seed). Each +nonempty coordinate tuple identifies a draw, conventionally (person_id, +process, period, draw_index). The top 53 bits of the SHA-256 digest, interpreted +big-endian, divided by 2**53 define a uniform in [0, 1). Stream parameters must +be normative node params; kernels must hash this module as implementing source +and declare SeedSource.KEYED. Repeated coordinates intentionally repeat draws. +""" + +from __future__ import annotations + +import hashlib +import math +from collections.abc import Sequence + +import numpy as np + +from .canonical import canonical_json + +__all__ = ["keyed_uniform"] + + +def _coordinate(value: object) -> list[object]: + if isinstance(value, np.generic): + value = value.item() + if isinstance(value, bool): + return ["bool", value] + if isinstance(value, int): + return ["int", value] + if isinstance(value, str): + return ["str", value] + if isinstance(value, float) and math.isfinite(value): + return ["float", value] + raise TypeError("Random coordinates must be non-null finite scalar identities.") + + +def keyed_uniform(*, stream: tuple, keys: Sequence[tuple]) -> np.ndarray: + """Return bytes-backed, read-only float64 draws keyed by stable coordinates. + + Row order, chunk boundaries, and unrelated inserted identities cannot affect + a draw. Integers, strings, booleans and floats have distinct canonical tags. + The experiment name is nonempty; replicate and base seed are non-negative + Python integers (booleans refused). No numpy RNG state is read or mutated. + """ + if not isinstance(stream, tuple) or len(stream) != 4: + raise TypeError( + "stream must be (algorithm, experiment_id, replicate, base_seed)." + ) + algorithm, experiment, replicate, base_seed = stream + if algorithm != "sha256-u53-v1": + raise ValueError(f"Unsupported random stream algorithm {algorithm!r}.") + if not isinstance(experiment, str) or not experiment: + raise ValueError("Random stream experiment_id must be non-empty.") + if any(type(value) is not int or value < 0 for value in (replicate, base_seed)): + raise ValueError( + "Random stream replicate/base_seed must be non-negative integers." + ) + prefix = b"microcosm-graph/keyed-uniform/1\0" + canonical_json(stream) + b"\0" + values = [] + for key in keys: + if not isinstance(key, tuple) or not key: + raise TypeError("Each random key must be a nonempty coordinate tuple.") + encoded = canonical_json([_coordinate(value) for value in key]) + digest = hashlib.sha256(prefix + encoded).digest() + values.append((int.from_bytes(digest[:8], "big") >> 11) / 2**53) + return np.frombuffer( + np.asarray(values, dtype=np.float64).tobytes(), dtype=np.float64 + ) diff --git a/packages/microcosm-graph/tests/test_graph_kernel_contract.py b/packages/microcosm-graph/tests/test_graph_kernel_contract.py index 21597e764..f3f267ff5 100644 --- a/packages/microcosm-graph/tests/test_graph_kernel_contract.py +++ b/packages/microcosm-graph/tests/test_graph_kernel_contract.py @@ -236,7 +236,9 @@ def test_seed_source_keyed_is_an_additive_declaration() -> None: keyed = Capabilities(determinism=Determinism.SEEDED, seed_source=SeedSource.KEYED) assert keyed.seed_source is SeedSource.KEYED - with pytest.raises(TypeError, match="Capabilities.seed_source must be a SeedSource"): + with pytest.raises( + TypeError, match="Capabilities.seed_source must be a SeedSource" + ): Capabilities(determinism=Determinism.SEEDED, seed_source="keyed") # type: ignore[arg-type] diff --git a/packages/microcosm-graph/tests/test_graph_randomness.py b/packages/microcosm-graph/tests/test_graph_randomness.py index 3b2a15035..b3fa075d4 100644 --- a/packages/microcosm-graph/tests/test_graph_randomness.py +++ b/packages/microcosm-graph/tests/test_graph_randomness.py @@ -2,7 +2,7 @@ A ``SeedSource.KEYED`` kernel does not consume an RNG in row order; it asks for the uniform belonging to a coordinate. These tests hold ``keyed_uniform`` to the -three properties that makes worth having — stable coordinates give the same +three properties that make it worth having — stable coordinates give the same draw, different coordinates give a different one, and nothing about packing, ordering, or unrelated identities can reach a draw — plus the documented ``sha256-u53-v1`` formula, recomputed here from the specification rather than @@ -84,9 +84,7 @@ def test_a_draw_is_blind_to_order_batching_and_unrelated_identities() -> None: # Inserting an unrelated identity ahead of a row leaves that row's draw # alone — the failure mode a positionally consumed RNG cannot avoid. - with_intruder = keyed_uniform( - stream=STREAM, keys=[(99, "wages"), *keys] - ) + with_intruder = keyed_uniform(stream=STREAM, keys=[(99, "wages"), *keys]) assert with_intruder[1:].tobytes() == straight.tobytes() From 2c4dc34fdf5aa55b06050d585c16c8238cb41cff Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 19:55:23 -0400 Subject: [PATCH 04/27] Red: FittedRegimeGatedQRF has no predict_from_uniforms yet test_qrf_stateless.py is taken verbatim from origin/microcosm-us-launch-integration-20260909 so the amendment and the integration branch share one test of the method: the same uniforms reproduce the same draws, permutation and chunking of the recipient batch leave the chained draws alone, a mismatched target set and out-of-range, non-finite or misshapen uniforms are refused before anything is drawn, the empty batch returns typed empty columns, the strict-CDF boundary skips a zero-probability sign at u=0, and an independent RNG replay reproduces predict() exactly across all seven regimes. It asserts the model's RNG position before and after, so "no state consumed" is a checked fact rather than a claim. 14 tests fail with AttributeError: 'FittedRegimeGatedQRF' object has no attribute 'predict_from_uniforms'. microcosm-fit's test_kernels.py gains the companion guard: adding SeedSource.KEYED must not widen QRFKernel, which still accepts only PARAM and EXECUTOR, so no fit.qrf@1 node changes what it declares. Co-Authored-By: Claude Opus 5 --- packages/microcosm-fit/tests/test_kernels.py | 16 ++ .../microcosm-fit/tests/test_qrf_stateless.py | 159 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 packages/microcosm-fit/tests/test_qrf_stateless.py diff --git a/packages/microcosm-fit/tests/test_kernels.py b/packages/microcosm-fit/tests/test_kernels.py index dba6fc9e5..5307a7364 100644 --- a/packages/microcosm-fit/tests/test_kernels.py +++ b/packages/microcosm-fit/tests/test_kernels.py @@ -244,6 +244,22 @@ def test_capabilities_protocol_and_wrapped_source_hash() -> None: ) +def test_the_graph_kernel_does_not_claim_the_new_keyed_seed_source() -> None: + """Amendment 20 adds ``SeedSource.KEYED``; ``fit.qrf@1`` still declares neither. + + ``predict_from_uniforms`` is an additive method on the fitted model, not a + new binding of the wrapped kernel: the kernel keeps taking its seed from a + literal param or from the executor, so no ``fit.qrf@1`` node changes what it + declares. A widening here would be a silent contract change. + """ + assert QRF_PARAM_KERNEL.capabilities.seed_source is SeedSource.PARAM + assert QRF_EXECUTOR_KERNEL.capabilities.seed_source is SeedSource.EXECUTOR + with pytest.raises(ValueError, match="must be SeedSource.PARAM or"): + QRFKernel(SeedSource.KEYED) + with pytest.raises(ValueError, match="must be SeedSource.PARAM or"): + QRFKernel(SeedSource.NONE) + + @pytest.mark.parametrize( ("kernel", "mutate_params", "match"), [ diff --git a/packages/microcosm-fit/tests/test_qrf_stateless.py b/packages/microcosm-fit/tests/test_qrf_stateless.py new file mode 100644 index 000000000..3d099a70d --- /dev/null +++ b/packages/microcosm-fit/tests/test_qrf_stateless.py @@ -0,0 +1,159 @@ +"""Caller-owned uniforms make repeated QRF draws stable by identity.""" + +import copy + +import numpy as np +import pandas as pd +import pytest + +from microcosm.fit import fit +from microcosm.fit.qrf import Regime + + +@pytest.fixture(scope="module") +def model(): + x = np.tile(np.arange(30, dtype=float), 6) + donor = pd.DataFrame( + { + "x": x, + "positive": x + 1, + "negative": -x - 1, + "zero": np.zeros(len(x)), + "mixed": np.tile([-2.0, 0.0, 3.0], len(x) // 3), + "inflated": np.tile([0.0, 4.0], len(x) // 2), + "negative_inflated": np.tile([0.0, -4.0], len(x) // 2), + "two_sign": np.tile([-3.0, 4.0], len(x) // 2), + } + ) + return fit( + donor, + ["x"], + list(donor.columns[1:]), + weights="none", + n_estimators=4, + seed=7, + ) + + +def uniforms(model, n): + return { + "quantiles": {t: np.linspace(0, 0.99, n) for t in model.targets}, + "sign_uniforms": {t: np.linspace(0.99, 0, n) for t in model.targets}, + } + + +def test_stateless_draws_preserve_rng_and_forests(model): + recipient = pd.DataFrame({"x": np.arange(20, dtype=float)}) + before = copy.deepcopy(model._rng.bit_generator.state) + draws = uniforms(model, len(recipient)) + first = model.predict_from_uniforms(recipient, **draws) + pd.testing.assert_frame_equal( + first, model.predict_from_uniforms(recipient, **draws) + ) + assert model._rng.bit_generator.state == before + assert (first.positive > 0).all() + assert (first.negative < 0).all() + assert (first.zero == 0).all() + assert set(first.mixed) == {-2.0, 0.0, 3.0} + assert set(first.inflated) == {0.0, 4.0} + assert set(first.negative_inflated) == {0.0, -4.0} + assert set(first.two_sign) == {-3.0, 4.0} + + +def test_permutation_and_chunking_preserve_chained_draws(model): + recipient = pd.DataFrame({"x": np.arange(20, dtype=float)}) + draws = uniforms(model, len(recipient)) + expected = model.predict_from_uniforms(recipient, **draws) + order = np.random.default_rng(4).permutation(len(recipient)) + reordered = model.predict_from_uniforms( + recipient.iloc[order], + **{k: {t: v[order] for t, v in d.items()} for k, d in draws.items()}, + ) + pd.testing.assert_frame_equal(expected, reordered.sort_index()) + chunks = [] + for rows in (slice(0, 7), slice(7, 20)): + chunks.append( + model.predict_from_uniforms( + recipient.iloc[rows], + **{k: {t: v[rows] for t, v in d.items()} for k, d in draws.items()}, + ) + ) + pd.testing.assert_frame_equal(expected, pd.concat(chunks)) + + +@pytest.mark.parametrize("bad", [[-0.1, 0.5], [0.1, 1.0], [np.nan, 0.1], [0.1]]) +@pytest.mark.parametrize("field", ["quantiles", "sign_uniforms"]) +def test_invalid_uniforms_rejected_before_drawing(model, bad, field): + recipient = pd.DataFrame({"x": [1.0, 2.0]}) + draws = uniforms(model, 2) + draws[field][model.targets[-1]] = np.array(bad) + with pytest.raises(ValueError, match="uniform|shape"): + model.predict_from_uniforms(recipient, **draws) + + +def test_uniform_target_names_must_match(model): + draws = uniforms(model, 2) + del draws["quantiles"][model.targets[-1]] + with pytest.raises(ValueError, match="targets"): + model.predict_from_uniforms(pd.DataFrame({"x": [1.0, 2.0]}), **draws) + + +def test_empty_recipient_batch(model): + actual = model.predict_from_uniforms( + pd.DataFrame({"x": pd.Series(dtype=float)}), **uniforms(model, 0) + ) + assert list(actual.columns) == model.targets + assert actual.empty + assert all(dtype == np.dtype("float64") for dtype in actual.dtypes) + + +def test_zero_uniform_skips_a_zero_probability_sign(model, monkeypatch): + # Exercise the inverse-CDF boundary that ordinary RNG draws almost never hit. + gate = model._target_models["mixed"].gate + monkeypatch.setattr( + gate, "predict_proba", lambda x: np.tile([0.0, 0.0, 1.0], (len(x), 1)) + ) + draws = uniforms(model, 2) + draws["sign_uniforms"]["mixed"] = np.zeros(2) + actual = model.predict_from_uniforms(pd.DataFrame({"x": [1.0, 2.0]}), **draws) + assert (actual.mixed == 3.0).all() + + +def test_stateless_replays_legacy_uniforms_across_all_regimes(model): + """Ordinary uniforms preserve legacy draws; exact CDF ties differ deliberately. + + The zero-uniform test above pins the intentional strict-CDF boundary. + This independent RNG replay detects swapped or inverted quantile streams. + """ + legacy = copy.deepcopy(model) + recipient = pd.DataFrame({"x": np.arange(20, dtype=float)}) + assert set(legacy.regimes().values()) == { + Regime.POSITIVE_ONLY, + Regime.NEGATIVE_ONLY, + Regime.DEGENERATE_ZERO, + Regime.THREE_SIGN, + Regime.ZERO_INFLATED_POSITIVE, + Regime.ZERO_INFLATED_NEGATIVE, + Regime.SIGN_ONLY, + } + for _ in range(4): + replay = np.random.default_rng() + replay.bit_generator.state = copy.deepcopy(legacy._rng.bit_generator.state) + quantiles, signs = {}, {} + for target in legacy.targets: + target_model = legacy._target_models[target] + quantiles[target] = ( + replay.random(len(recipient)) + if target_model.regime != Regime.DEGENERATE_ZERO + else np.zeros(len(recipient)) + ) + signs[target] = ( + replay.random(len(recipient)) + if target_model.gate is not None + else np.zeros(len(recipient)) + ) + expected = legacy.predict(recipient) + actual = legacy.predict_from_uniforms( + recipient, quantiles=quantiles, sign_uniforms=signs + ) + pd.testing.assert_frame_equal(expected, actual, check_exact=True) From 82e11113e935b5c146d929ee208dd62c9171bb71 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 19:55:52 -0400 Subject: [PATCH 05/27] Green: QRF draws from caller-supplied per-row uniforms _draw_target_from_uniforms and FittedRegimeGatedQRF.predict_from_uniforms are taken verbatim from origin/microcosm-us-launch-integration-20260909 (6e3907f86); the diff against origin/main applies with no conflict. The method is additive. predict(), _draw_target_with_rng, and the RNG consumption order behind them are untouched, so fit.qrf@1's existing outputs are unchanged; the two code paths differ only in where a row's quantile and sign uniform come from. Draws now depend on caller-supplied per-row uniforms rather than on a position in a generator, so pairing uniforms with stable entity ids makes a batch's results invariant to recipient ordering and chunking. Later targets still condition on earlier draws, as in predict(). One deliberate difference from the RNG path, commented in place: the sign inverse-CDF compares strictly (cumulative > u) and closes the final bin at 1.0, so u=0 skips a zero-probability class instead of selecting it. fit.qrf@1's implementation hash moves with this commit, because QRFKernel.implementation_hash() hashes microcosm.fit.qrf's module bytes. The H1 pins are regenerated in a following commit. Co-Authored-By: Claude Opus 5 --- .../microcosm-fit/src/microcosm/fit/qrf.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/packages/microcosm-fit/src/microcosm/fit/qrf.py b/packages/microcosm-fit/src/microcosm/fit/qrf.py index a39c57d2a..e8bb2c956 100644 --- a/packages/microcosm-fit/src/microcosm/fit/qrf.py +++ b/packages/microcosm-fit/src/microcosm/fit/qrf.py @@ -1034,6 +1034,36 @@ def _draw_target_with_rng( return values +def _draw_target_from_uniforms( + features: pd.DataFrame, + model: _TargetModel, + quantiles: np.ndarray, + sign_uniforms: np.ndarray, +) -> np.ndarray: + """Evaluate a target without consuming or replacing any model RNG state.""" + if model is _RELEASED: + raise RuntimeError("This target's fitted forests were released; refit to draw.") + if model.regime == Regime.DEGENERATE_ZERO: + return np.zeros(len(features), dtype=np.float64) + if model.regime == Regime.POSITIVE_ONLY: + return model.positive.draw(features, quantiles) + if model.regime == Regime.NEGATIVE_ONLY: + return model.negative.draw(features, quantiles) + x = features.loc[:, list(model.columns)].to_numpy(dtype=np.float64) + cumulative = np.cumsum(model.gate.predict_proba(x), axis=1) + # Uniforms occupy [0, 1): strict comparison skips zero-probability classes + # even at u=0. Close the final CDF bin against floating-point roundoff. + cumulative[:, -1] = 1.0 + chosen = (cumulative > sign_uniforms[:, None]).argmax(axis=1) + signs = np.asarray(model.gate.classes_)[chosen] + values = np.zeros(len(features), dtype=np.float64) + for sign, forest in ((1, model.positive), (-1, model.negative)): + mask = signs == sign + if mask.any() and forest is not None: + values[mask] = forest.draw(features.loc[mask], quantiles[mask]) + return values + + class RegimeGatedQRF: """The canonical :class:`~microcosm.fit.model.ConditionalModel`. @@ -1564,6 +1594,60 @@ def predict( self._target_models[target] = _RELEASED return out + def predict_from_uniforms( + self, + frame_or_df: Frame | pd.DataFrame, + *, + quantiles: Mapping[str, np.ndarray], + sign_uniforms: Mapping[str, np.ndarray], + ) -> pd.DataFrame: + """Draw using caller-supplied per-row uniforms, without advancing RNG. + + Each mapping must contain exactly the fitted targets, with one finite + one-dimensional array in ``[0, 1)`` per target, aligned to input rows. + Supply both arrays even for single-sign or all-zero targets. Later + targets condition on earlier draws, just as in :meth:`predict`. + + Pairing uniforms with stable entity IDs makes results invariant to + recipient ordering and batching. Fitted forests remain reusable. The + legacy :meth:`predict` stream and its consumption order are unchanged. + """ + features = self._predictor_frame(frame_or_df) + arrays = {} + for name, supplied in ( + ("quantiles", quantiles), + ("sign_uniforms", sign_uniforms), + ): + if not isinstance(supplied, Mapping) or set(supplied) != set(self.targets): + raise ValueError(f"{name} must contain exactly the fitted targets.") + arrays[name] = {} + for target in self.targets: + values = np.asarray(supplied[target], dtype=np.float64) + if values.shape != (len(features),): + raise ValueError( + f"{name}[{target!r}] must have shape ({len(features)},)." + ) + if ( + not np.isfinite(values).all() + or ((values < 0) | (values >= 1)).any() + ): + raise ValueError(f"{name}[{target!r}] uniforms must be in [0, 1).") + arrays[name][target] = values + out = pd.DataFrame(index=features.index) + if features.empty: + return out.reindex(columns=self.targets).astype(np.float64) + augmented = features.copy() + for target in self.targets: + drawn = _draw_target_from_uniforms( + augmented, + self._target_models[target], + arrays["quantiles"][target], + arrays["sign_uniforms"][target], + ) + out[target] = drawn + augmented[target] = drawn + return out + def _predictor_frame(self, frame_or_df: Frame | pd.DataFrame) -> pd.DataFrame: """Extract the predictor columns from a Frame or DataFrame input.""" if isinstance(frame_or_df, Frame): From 80c41e0816dc31541fc3824fb4a90e06002d8c0b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 20:02:59 -0400 Subject: [PATCH 06/27] Add a re-pin path that keeps every pinned H1 platform, and check them all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editing a kernel's source moves its implementation hash and therefore every pinned platform's node key — including the two x86_64/linux pins this machine cannot run. Two things were missing for that. tools/graph_parity_repin.py re-records one case's pins in place. It is a sibling of graph_parity_fixtures.py rather than an addition to it, because ParityCsvSource and ParityRulesEngine are DEFINED in that module: its bytes are inside ParityCsvSource.implementation_hash() and SimulateRulesKernel.implementation_hash(), so editing it would move every parity node key in all three cases — churn caused by tooling rather than by the kernel under amendment. Measured: adding the same code there moved the calibrate node key from 184ccd0a to f8c9ed4b with its implementation hash unchanged. generate() is the wrong instrument here for a second reason: it rewrites pins["platforms"] as the local platform alone, so running it would silently drop the foreign pins and leave H1 on its off-platform branch there, which asserts no bytes at all. The local key is produced by running the graph; every other pinned platform's key is derived, and the derivation is checked against the produced key on every run. Pinned bytes are never derived — each platform's direct.csv is left exactly as that platform recorded it — and a re-pin refuses outright if the local direct call's bytes moved, because that is not a re-pin. test_graph_parity_pins.py makes the foreign pins checkable from anywhere: a node key is a pure function of the declaration, the resolved inputs, the implementation hash, the capability projection and the platform fingerprint STRING, so every pinned key is derivable on every platform. Before this, a stale foreign pin survived until that Linux lane happened to run. Three of its eleven tests are red at this commit, which is the point: the fit.qrf pins went stale when qrf.py moved. Co-Authored-By: Claude Opus 5 --- .../tests/test_graph_parity_pins.py | 110 +++++++++ tools/graph_parity_repin.py | 229 ++++++++++++++++++ 2 files changed, 339 insertions(+) create mode 100644 packages/microcosm-graph/tests/test_graph_parity_pins.py create mode 100644 tools/graph_parity_repin.py diff --git a/packages/microcosm-graph/tests/test_graph_parity_pins.py b/packages/microcosm-graph/tests/test_graph_parity_pins.py new file mode 100644 index 000000000..0b1eac9ca --- /dev/null +++ b/packages/microcosm-graph/tests/test_graph_parity_pins.py @@ -0,0 +1,110 @@ +"""Every pinned H1 platform key is checkable from every platform. + +Charter H1 asserts a platform-bitwise kernel's *bytes* only on a platform that +carries a pin, and falls back to identity partitioning elsewhere. That is the +right rule for bytes — this machine cannot produce another architecture's +floats — but it leaves the foreign pins' *keys* unasserted everywhere except on +those machines, so a stale one survives until that CI lane happens to run. + +A node key is not a measurement. It is a pure function of the declaration, the +resolved input identities, the implementation hash, the capability projection, +and, for a platform-bitwise kernel (amendment 16), the platform fingerprint +string. So every pinned key is derivable here, and these tests derive all of +them. A kernel edit that re-pins only the local platform fails immediately +rather than on the next Linux run. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from microcosm.graph import graph_from_json, platform_fingerprint +from tools.graph_parity_fixtures import FIXTURES, parity_registry +from tools.graph_parity_repin import derived_node_key + +CASES = ("fit.qrf", "calibrate", "simulate") + + +def _case(name: str) -> tuple[Path, dict]: + case = FIXTURES / name + return case, json.loads((case / "pins.json").read_text()) + + +def _platforms(pins: dict) -> dict[str, dict]: + platforms = dict(pins.get("platforms", {})) + platforms.setdefault( + pins["platform"], {"node_key": pins["node_key"], "direct": "direct.csv"} + ) + return platforms + + +@pytest.mark.parametrize("name", CASES) +def test_every_pinned_platform_key_is_the_one_that_platform_would_compute( + name: str, +) -> None: + case, pins = _case(name) + graph = graph_from_json((case / "graph.json").read_text()) + for fingerprint, entry in sorted(_platforms(pins).items()): + derived = derived_node_key( + graph, pins["node"], case / "inputs.csv", fingerprint + ) + assert derived == entry["node_key"], ( + f"{name}: the pin for {fingerprint} is stale. Re-pin every platform " + f"with `uv run python tools/graph_parity_repin.py {name}` — a " + "kernel edit moves every platform's key, not only this machine's." + ) + + +@pytest.mark.parametrize("name", CASES) +def test_the_top_level_pin_is_the_authoring_platform_and_its_bytes_exist( + name: str, +) -> None: + case, pins = _case(name) + platforms = _platforms(pins) + assert pins["platform"] in platforms + assert platforms[pins["platform"]]["node_key"] == pins["node_key"] + for entry in platforms.values(): + direct = case / entry["direct"] + assert direct.is_file(), f"{name}: {entry['direct']} is pinned but absent" + assert direct.read_bytes(), f"{name}: {entry['direct']} is empty" + + +@pytest.mark.parametrize("name", CASES) +def test_the_pinned_implementation_hash_is_the_registered_kernel_s(name: str) -> None: + """A moved kernel must re-pin; H1 checks this too, without the platforms.""" + _, pins = _case(name) + kernel = parity_registry().get(pins["kernel"]) + assert kernel.implementation_hash() == pins["implementation_hash"] + assert set(pins["dependencies"]) == set(kernel.capabilities.dependencies) + + +def test_platform_bitwise_pins_partition_identity_across_platforms() -> None: + """Amendment 16: a shared store never serves another platform's output.""" + _, pins = _case("fit.qrf") + assert pins["numeric"] == "platform_bitwise" + platforms = _platforms(pins) + assert len(platforms) >= 2, "the whole point is more than one platform" + keys = [entry["node_key"] for entry in platforms.values()] + assert len(set(keys)) == len(keys) + + +def test_a_derived_key_is_the_local_platform_s_own_key() -> None: + """The derivation is not a second implementation of ``node_key``. + + On this machine the derived key and the pinned key agree, and the pinned key + was produced by running the graph — so the derivation is anchored to the + executor rather than to itself. + """ + case, pins = _case("fit.qrf") + local = platform_fingerprint() + platforms = _platforms(pins) + if local not in platforms: + pytest.skip(f"{local} carries no fit.qrf pin") + graph = graph_from_json((case / "graph.json").read_text()) + assert ( + derived_node_key(graph, pins["node"], case / "inputs.csv", local) + == platforms[local]["node_key"] + ) diff --git a/tools/graph_parity_repin.py b/tools/graph_parity_repin.py new file mode 100644 index 000000000..b0a3cb58a --- /dev/null +++ b/tools/graph_parity_repin.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 +"""Re-record one H1 parity case's pins after an additive change to its kernel. + +``tools/graph_parity_fixtures.py`` regenerates a fixture from scratch. That is +the wrong instrument for a change that leaves a kernel's outputs alone but moves +its implementation hash, for two reasons: + +1. ``generate()`` rewrites ``pins["platforms"]`` as the local platform alone, so + running it here would silently drop the two pinned ``x86_64/linux`` entries + and leave charter H1 asserting no bytes on those platforms — its off-platform + branch asserts only identity partitioning. +2. ``ParityCsvSource`` and ``ParityRulesEngine`` are *defined in* that module, so + its bytes are inside ``ParityCsvSource.implementation_hash()`` and + ``SimulateRulesKernel.implementation_hash()``. Editing it to add a re-pin + path would move every parity node key in all three cases — churn caused by + the tooling rather than by the kernel under amendment. This module therefore + sits beside it and imports, so those bytes never move. + +What a re-pin may and may not derive: the local platform's node key is +**produced**, by running the graph. Every other pinned platform's key is +**derived**, because a node key is a pure function of the declaration, the +resolved input identities, the implementation hash, the capability projection, +and — for a platform-bitwise kernel (amendment 16) — the platform fingerprint +*string* (``microcosm.graph.keys.node_key``); nothing else about a platform +reaches a key. The derivation is checked against the produced key on every run, +so it cannot drift from what the executor does. Pinned **bytes** are never +derived: each platform's ``direct.csv`` is left exactly as that platform +recorded it, and a re-pin refuses to write at all if the local direct call's +bytes moved, because that would mean the change was not additive and the +fixture needs a real regeneration instead. + +Usage:: + + uv run python tools/graph_parity_repin.py +""" + +from __future__ import annotations + +import contextlib +import json +import os +import sys +from collections.abc import Iterator +from pathlib import Path +from tempfile import TemporaryDirectory + +import pandas as pd + +from microcosm.graph import ContentStore, Graph, compile_graph, graph_to_json, run_graph +from microcosm.graph import keys as graph_keys +from microcosm.graph.keys import platform_fingerprint + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + # Importable both as ``tools.graph_parity_repin`` and as a bare script path. + sys.path.insert(0, str(ROOT)) + +# The pin schema and the case declarations belong to the generator; importing +# them is what keeps a re-pinned fixture indistinguishable from a generated one. +from tools.graph_parity_fixtures import ( # noqa: E402 - after the path bootstrap + FIXTURES, + _calibrate_case, + _fit_case, + _pins, + _simulate_case, + _write_pins, + parity_registry, +) + +__all__ = ["BUILDERS", "derived_node_key", "repin"] + +BUILDERS = { + "fit.qrf": _fit_case, + "calibrate": _calibrate_case, + "simulate": _simulate_case, +} + + +@contextlib.contextmanager +def _as_platform(fingerprint: str) -> Iterator[None]: + """Compute keys as ``fingerprint`` would, without being that platform. + + The fingerprint reaches a key only as the string ``node_key`` appends for a + platform-bitwise kernel, so the string is the only thing to vary. See the + module docstring for why deriving a key is sound and deriving bytes is not. + """ + real = graph_keys.platform_fingerprint + graph_keys.platform_fingerprint = lambda: fingerprint + try: + yield + finally: + graph_keys.platform_fingerprint = real + + +def derived_node_key( + graph: Graph, + node_id: str, + inputs_path: Path, + fingerprint: str, +) -> str: + """The key ``fingerprint`` computes for ``node_id`` over ``inputs_path``.""" + compiled = compile_graph(graph) + source_keys = { + source.name: graph_keys.source_content_key(source.name, inputs_path) + for source in graph.sources + } + registry = parity_registry() + keys: dict[str, str] = {} + with _as_platform(fingerprint): + for current in compiled.order: + kernel = registry.get(compiled.graph.node(current).kernel) + keys[current] = graph_keys.node_key( + compiled, + current, + keys, + kernel.implementation_hash(), + source_keys, + kernel_capabilities=kernel.capabilities, + ) + return keys[node_id] + + +def _csv_bytes(frame: pd.DataFrame) -> bytes: + """The exact bytes ``_write_case`` writes for a pinned table.""" + return frame.to_csv(index=False, lineterminator="\n").encode("utf-8") + + +def _executed_node_key(name: str, graph: Graph, inputs_path: Path) -> str: + with TemporaryDirectory(prefix="microcosm-parity-repin-") as store_path: + manifest = run_graph( + compile_graph(graph), + sources={"fixture": inputs_path}, + store=ContentStore(Path(store_path)), + kernels=parity_registry(), + resume="forbid", + decisions=(), + ) + return manifest.nodes[graph.nodes[-1].id].key + + +def repin(name: str) -> dict[str, str]: + """Re-record ``name``'s pins in place, keeping every platform it carries. + + Returns the recorded node key per platform fingerprint. + """ + graph, inputs, direct, kernel, seed = BUILDERS[name]() + destination = FIXTURES / name + pins = json.loads((destination / "pins.json").read_text(encoding="utf-8")) + if pins["kernel"] != kernel.ref: # type: ignore[attr-defined] + raise SystemExit( + f"{name}: pins are for {pins['kernel']}, builder gives {kernel.ref}" # type: ignore[attr-defined] + ) + if (destination / "graph.json").read_text(encoding="utf-8") != graph_to_json(graph): + raise SystemExit(f"{name}: the declaration moved; regenerate instead") + stored_inputs = pd.read_csv( + destination / "inputs.csv", float_precision="round_trip" + ) + if not stored_inputs.equals(inputs.reset_index(drop=True)): + raise SystemExit( + f"{name}: inputs.csv no longer matches the builder; regenerate instead" + ) + if (destination / "direct.csv").read_bytes() != _csv_bytes(direct): + raise SystemExit( + f"{name}: the direct call's bytes moved, so this is not a re-pin; " + "regenerate the fixture and say what changed" + ) + + fingerprint = platform_fingerprint() + platforms = dict(pins.get("platforms", {})) + platforms.setdefault( + pins["platform"], {"node_key": pins["node_key"], "direct": "direct.csv"} + ) + if fingerprint not in platforms: + raise SystemExit( + f"{name}: {fingerprint} carries no pin to re-record; " + "use graph_parity_fixtures.py platform-pin to add one" + ) + + node_id = pins["node"] + inputs_path = destination / "inputs.csv" + produced = _executed_node_key(name, graph, inputs_path) + if derived_node_key(graph, node_id, inputs_path, fingerprint) != produced: + raise SystemExit( + f"{name}: the derived key disagrees with the executed one on " + f"{fingerprint}; a node key now depends on more than the platform " + "fingerprint string, so foreign platforms must re-pin themselves" + ) + + recorded = { + platform: { + "node_key": ( + produced + if platform == fingerprint + else derived_node_key(graph, node_id, inputs_path, platform) + ), + "direct": entry["direct"], + } + for platform, entry in platforms.items() + } + # The authoring platform is a fact about the fixture, not about the machine + # re-pinning it, so it survives ``_pins``; only its node key is re-recorded. + authoring = pins["platform"] + pins.update(_pins(node_id, recorded[authoring]["node_key"], kernel, seed)) + pins["platform"] = authoring + pins["platforms"] = recorded + _write_pins(destination, pins) + return {platform: entry["node_key"] for platform, entry in recorded.items()} + + +def main(argv: list[str] | None = None) -> int: + args = list(sys.argv[1:] if argv is None else argv) + if len(args) != 1 or args[0] not in BUILDERS: + raise SystemExit( + f"usage: graph_parity_repin.py <{' | '.join(sorted(BUILDERS))}>" + ) + os.environ["POPULACE_FIT_N_JOBS"] = "1" + os.environ["POPULACE_FIT_PREDICT_WORKERS"] = "1" + for platform, key in sorted(repin(args[0]).items()): + print(f"{args[0]} {platform} {key}") + return 0 + + +if __name__ == "__main__": + # source_hash includes module names, and the generator this imports defines + # two parity kernels; delegate to the canonical import so a pin never + # depends on how this file was invoked. + from tools.graph_parity_repin import main as canonical_main + + raise SystemExit(canonical_main()) From c96a3f4126051f0fe14b3b914b50b4557bb91020 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 20:03:21 -0400 Subject: [PATCH 07/27] Re-pin the fit.qrf H1 fixture; direct.csv is byte-identical MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `uv run python tools/graph_parity_repin.py fit.qrf` on arm64/darwin/py3.14 (Python 3.14.4), the fixture's authoring platform. implementation_hash 02db8f5c → d1f8b192, and with it all three platform keys: arm64/darwin/py3.14 8878352d → 35af6a45 (produced: graph executed here) x86_64/linux/py3.13 f6984280 → 6c43edcb (derived) x86_64/linux/py3.14 9e80ee3a → ecb6b20c (derived) The derivation is proven, not assumed: re-deriving the three OLD keys from this machine with the OLD implementation hash and only the fingerprint string varied reproduces all three pinned values exactly, so the platform reaches a key as that string and nothing else. The tool also re-derives the local key alongside executing the graph on every run and refuses if they disagree. direct.csv is unchanged on every platform — 7b8dbd56c91ee71552ff6d892a42c56494b1813fb5d4b11553a8a8ccc9b90dca before and after, for the authoring copy and both x86_64 copies — which is the evidence that predict_from_uniforms is additive: the wrapped kernel still draws exactly what it drew. pins.json is the only fixture file this commit touches. The same tool run against calibrate and simulate rewrites their pins byte-for-byte identically (184ccd0a, a643736e), so the re-pin round-trip is a no-op where nothing moved. Co-Authored-By: Claude Opus 5 --- .../tests/fixtures/parity/kernels/fit.qrf/pins.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/microcosm-graph/tests/fixtures/parity/kernels/fit.qrf/pins.json b/packages/microcosm-graph/tests/fixtures/parity/kernels/fit.qrf/pins.json index 1bfbebe22..3b61d6fc1 100644 --- a/packages/microcosm-graph/tests/fixtures/parity/kernels/fit.qrf/pins.json +++ b/packages/microcosm-graph/tests/fixtures/parity/kernels/fit.qrf/pins.json @@ -1 +1 @@ -{"dependencies":{"numpy":"2.4.6","pandas":"3.0.3","quantile-forest":"1.4.2","scikit-learn":"1.8.0"},"implementation_hash":"02db8f5c849d876be20a95152b5302a5cacc0a7c77c58d8b436a3a00f57b4c92","kernel":"fit.qrf@1","node":"fit_qrf","node_key":"8878352db3439439f412f26c8762ff5871b94fe8e5fc8d3469dd1c45d7ef7da4","numeric":"platform_bitwise","platform":"arm64/darwin/py3.14","platforms":{"arm64/darwin/py3.14":{"direct":"direct.csv","node_key":"8878352db3439439f412f26c8762ff5871b94fe8e5fc8d3469dd1c45d7ef7da4"},"x86_64/linux/py3.13":{"direct":"platforms/x86_64-linux-py3_13/direct.csv","node_key":"f6984280e1ef0f156bd24345f7d3677d573c650ac706f9a3f949627c0d76d2d4"},"x86_64/linux/py3.14":{"direct":"platforms/x86_64-linux-py3_14/direct.csv","node_key":"9e80ee3ac5c30f99725c4dd932535983dabde7913c653e79819b1df3a289720c"}},"seed":947} +{"dependencies":{"numpy":"2.4.6","pandas":"3.0.3","quantile-forest":"1.4.2","scikit-learn":"1.8.0"},"implementation_hash":"d1f8b1929e6452aa507b0d9c64ba42a59851dc31c71021303c25f060e34c075b","kernel":"fit.qrf@1","node":"fit_qrf","node_key":"35af6a452bd32ca39d313d78255236df01877d684c2d847b1bd5a6016a68e237","numeric":"platform_bitwise","platform":"arm64/darwin/py3.14","platforms":{"arm64/darwin/py3.14":{"direct":"direct.csv","node_key":"35af6a452bd32ca39d313d78255236df01877d684c2d847b1bd5a6016a68e237"},"x86_64/linux/py3.13":{"direct":"platforms/x86_64-linux-py3_13/direct.csv","node_key":"6c43edcb3edf2bdef20d915b1be16503d37583c678aced78f1442aca1139e1ae"},"x86_64/linux/py3.14":{"direct":"platforms/x86_64-linux-py3_14/direct.csv","node_key":"ecb6b20c02b0bc442c4baf3fd7def6d6aec06e6b9567910294f945d66c43bbf8"}},"seed":947} From 77b7daabe6b9d52c45159fca5b699c381af1fb09 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 20:06:38 -0400 Subject: [PATCH 08/27] Amendment 20: keyed draw streams; changelog fragment The charter records what the US launch integration branch carried without an amendment. The entry says what the member and the helper are, why a draw keyed by coordinates is worth having (C1 and C2's invariance to packing now reaches each individual draw), that C4 is neither weakened nor edited because randomness.py consumes no RNG at all, that fit.qrf@1's existing outputs are unchanged and its implementation identity therefore moves only because its module's source did, that an enum member moves no existing node key, and the one deliberate difference from the generator path at the CDF boundary. It adds no property row and edits none; C1, C2, C4 and H1 are the rows it bears on. NUMBERING, for the merge owner: main's amendment list ends at 18, so by its own arithmetic the next free number is 19. This entry is numbered 20 because the lane brief assigns 20 and assigns the ArtifactValue/KernelContext.artifacts work to a separate lane. Two unmerged commits on the candidate-quality-producer-integration-20260905 branch family already claim both numbers: 3ff92b0ae adds a 19 ("Typed artifacts and stable draw coordinates") that bundles typed artifacts WITH this lane's SeedSource.KEYED and keyed_uniform, and d2043d85e adds a 20 ("Failed typed evidence remains a failed gate"). Neither is on main or on the integration branch. Whoever merges owns the reconciliation; renumbering this entry to 19 is a one-token edit and changes nothing else. Co-Authored-By: Claude Opus 5 --- ...mend-keyed-seed-and-uniform-draws.added.md | 1 + docs/graph-acceptance.md | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 changelog.d/amend-keyed-seed-and-uniform-draws.added.md diff --git a/changelog.d/amend-keyed-seed-and-uniform-draws.added.md b/changelog.d/amend-keyed-seed-and-uniform-draws.added.md new file mode 100644 index 000000000..a6a5b7aed --- /dev/null +++ b/changelog.d/amend-keyed-seed-and-uniform-draws.added.md @@ -0,0 +1 @@ +Graph interface amendment 20, keyed draw streams: `SeedSource.KEYED` declares that a kernel's randomness comes from normative stream parameters (`("sha256-u53-v1", experiment_id, replicate, base_seed)`) and one stable coordinate per draw rather than from a position in the executor's generator, and `microcosm.graph.randomness.keyed_uniform` computes those draws as the top 53 bits of a SHA-256 over the canonical stream and canonically tagged coordinates, reading and advancing no numpy RNG state. Order, chunking, and unrelated inserted identities cannot reach a draw. `FittedRegimeGatedQRF.predict_from_uniforms` is the first consumer surface: it draws from caller-supplied per-row uniforms, so pairing them with stable entity ids makes a batch invariant to recipient ordering and batching, and it advances no model RNG. `fit.qrf@1`'s existing outputs are unchanged — `predict` and its consumption order are untouched and the method is additive, and the kernel still declares `PARAM` or `EXECUTOR` — but its implementation hash moves with its module's source, so the H1 `fit.qrf` parity pins are re-recorded on all three pinned platforms while every `direct.csv` stays byte-identical. Adding an enum member changes no existing node's canonical projection, so no existing node key moves. `tools/graph_parity_repin.py` re-pins one parity case in place, keeping every platform it carries instead of dropping the ones the local machine cannot run. diff --git a/docs/graph-acceptance.md b/docs/graph-acceptance.md index 4f602d4a7..d21ac89df 100644 --- a/docs/graph-acceptance.md +++ b/docs/graph-acceptance.md @@ -292,6 +292,39 @@ Amendments so far (each re-locked): `hit` forced to false) and `load_certified` refuses it. Raised by the #847 gate review; adopted 2026-09-03. +20. **Keyed draw streams.** `SeedSource.KEYED` and + `microcosm.graph.randomness.keyed_uniform`: a keyed kernel's draws are a + pure function of normative stream parameters — `("sha256-u53-v1", + experiment_id, replicate, base_seed)` — and one stable coordinate per + draw, conventionally `(person_id, process, period, draw_index)`. It + reads and advances no generator, so a row's draw stops depending on how + many rows were drawn before it: the invariance to packing that C1 and C2 + already gave a node's key and seed now reaches each individual draw, and + an inserted or removed identity leaves every other row's value alone. C4 + is neither weakened nor edited — `randomness.py` consumes no RNG at all, + positionally or otherwise, so its static check still holds over the + whole shard. `microcosm-fit` carries the first consumer surface, + `FittedRegimeGatedQRF.predict_from_uniforms`, which draws from + caller-supplied per-row uniforms; pairing those with stable entity ids + makes a batch's results invariant to recipient ordering and chunking. + **`fit.qrf@1`'s existing outputs are unchanged.** `predict`, its RNG + consumption order, and every value it draws are untouched; the new + method is additive, and the kernel still declares `PARAM` or `EXECUTOR`, + never `KEYED`. Its *implementation* identity moves all the same, because + `QRFKernel.implementation_hash()` hashes `microcosm.fit.qrf`'s module + bytes — so H1's `fit.qrf` pins are re-recorded on every pinned platform + while `direct.csv` stays byte-identical on each, and that byte-identity + is the evidence for the additivity claim rather than a restatement of + it. Unlike amendments 11 and 13 this adds an enum member, not a + normative field, so no existing node's canonical projection changes and + no existing node key moves. One deliberate difference from the generator + path, commented where it lives: the sign gate's inverse CDF compares + strictly and closes its final bin at 1.0, so a uniform of exactly zero + skips a zero-probability class instead of selecting it, and a CDF that + sums to just under one can no longer silently select the first class. + Raised by the US launch integration branch, which carried the code + without an amendment; adopted 2026-09-11. + Adding a normative field with a default changes the canonical projection of every node that carries it, so node keys moved with amendments 11 and 13's sibling field `entrants`; no released artifact pins a graph key yet. From 2d0681c8fe92bf5d9e7b31c9bfc537795015f92e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 20:28:46 -0400 Subject: [PATCH 09/27] Bring the lane journal up to the landed state Records what landed, the findings that shaped it (foreign-platform keys are derivable; the fixture generator's own bytes are inside three parity node keys), and the two items left open for a reviewer: the spec-engine seed-digest drift this lane causes and declines to re-pin, and the amendment numbering. Co-Authored-By: Claude Opus 5 --- PROGRESS-amendment-20-keyed-draws.md | 115 +++++++++++++++++---------- 1 file changed, 73 insertions(+), 42 deletions(-) diff --git a/PROGRESS-amendment-20-keyed-draws.md b/PROGRESS-amendment-20-keyed-draws.md index 93291e134..670b488bd 100644 --- a/PROGRESS-amendment-20-keyed-draws.md +++ b/PROGRESS-amendment-20-keyed-draws.md @@ -2,54 +2,85 @@ Branch: `amend-keyed-seed-and-uniform-draws`, cut from `origin/main` at `3094bfe84`. Worktree: `~/PolicyEngine/_worktrees/microcosm-amend-keyed`. No push, no new branches. -Platform of record for H1 pins: **arm64/darwin/py3.14** (the authoring platform). +Platform of record for H1 pins: **arm64/darwin/py3.14** (Python 3.14.4) — the +fixture's authoring platform. ## State -Starting. Reconnaissance complete; nothing implemented yet. - -## Scope (what this lane lands) - -1. `graph/kernel.py`: `SeedSource.KEYED` + the `KernelContext.rng` docstring change. - **Not** `ArtifactValue` / `KernelContext.artifacts` — a separate lane owns those. -2. `graph/randomness.py` (new): `keyed_uniform` and its coordinate encoding. -3. `graph/__init__.py`: export `keyed_uniform`, inserted in sorted position. -4. `fit/qrf.py`: `_draw_target_from_uniforms` + `FittedRegimeGatedQRF.predict_from_uniforms`. -5. `docs/graph-acceptance.md`: amendment 20 entry. -6. `docs/graph-interface.lock`: re-record `kernel.py`. -7. Tests: `SeedSource.KEYED` contract; `keyed_uniform` units; `predict_from_uniforms` units. -8. H1 parity: regenerate `fit.qrf` pins (implementation hash moves because `qrf.py` - changed); `direct.csv` must stay byte-identical. - -## Reconnaissance findings (verified, 2026-09-11) - -- The integration branch adds **no** amendment text and **no** executor change that - honours `KEYED`: `git diff origin/main origin/microcosm-us-launch-integration-20260909 - -- .../executor.py` is entirely artifacts / lazy population retention / execution - metadata. `fit/kernels.py` has **no** diff on that branch. So the minimal coherent - subset for `KEYED` touches neither file. -- `QRFKernel.implementation_hash()` hashes `microcosm.fit.qrf`'s module bytes - (`packages/microcosm-fit/src/microcosm/fit/kernels.py`), so editing `qrf.py` - moves the `fit.qrf@1` implementation hash and every `fit.qrf` node key. -- `node_key` folds in `kernel_impl_hash` **and**, for `PLATFORM_BITWISE`, the platform - fingerprint (`graph/keys.py`). `fit.qrf/pins.json` pins three platforms - (`arm64/darwin/py3.14`, `x86_64/linux/py3.13`, `x86_64/linux/py3.14`); all three - node keys move, and only the local one can be produced by running the graph. -- `tools/graph_parity_fixtures.py generate()` **resets** `pins["platforms"]` to the - local platform only, so a bare regeneration would silently drop the two x86_64 pins - and orphan their `direct.csv` files, downgrading CI on Linux to the off-platform - branch of `test_h1_kernel_parity` (which asserts no bytes). -- C4's static check (`test_acceptance_c_seeds.py::test_c4_seed_from_identity`) ASTs - every `microcosm/graph/**/*.py`: no `RandomState`, no `*.random.seed`, and any - `default_rng` call takes exactly one argument. `randomness.py` uses none of these. -- New test files must sit flat in `packages//tests/` and be `test_*.py`; - `microcosm-graph`/`microcosm-fit` classify to fast `rest` and engine `us-am` - (`tools/ci_test_groups.py`), so no lane bookkeeping is needed beyond `--verify`. +Implementation, tests, pins, charter and changelog are landed and committed. +`packages/microcosm-graph/tests` and `packages/microcosm-fit/tests` are green. +One item is deliberately left red and reported rather than fixed: see +**Open for decision** below. ## Done -- (nothing yet) +1. `graph/kernel.py`: `SeedSource.KEYED` plus the `KernelContext.rng` docstring. + `ArtifactValue` / `KernelContext.artifacts` deliberately NOT brought over. +2. `graph/randomness.py` (new, 69 lines): `keyed_uniform`, taken verbatim from + `origin/microcosm-us-launch-integration-20260909`. +3. `graph/__init__.py`: `keyed_uniform` exported, inserted in sorted position + among the lowercase callables (`graph_to_json`, `keyed_uniform`, `load_source`). +4. `fit/qrf.py`: `_draw_target_from_uniforms` + `predict_from_uniforms`, taken + verbatim from the same branch (`6e3907f86`); the diff applied with no conflict. +5. `docs/graph-interface.lock`: `kernel.py` re-recorded + `eaf07da2… → 3483d091b03b19ae35c0268c01cb9e0f76c4cd63742083130567321d70da6048`. + The lock is plain `shasum -a 256` of the file bytes (confirmed against the + unchanged `decl.py` line). +6. Tests: `test_graph_randomness.py` (new), `SeedSource.KEYED` contracts in + `test_graph_kernel_contract.py`, `test_qrf_stateless.py` (new, verbatim from + the integration branch), a `QRFKernel` non-widening guard in microcosm-fit's + `test_kernels.py`, and `test_graph_parity_pins.py` (new). +7. `tools/graph_parity_repin.py` (new) + the re-pinned `fit.qrf` fixture. +8. `docs/graph-acceptance.md` amendment 20 + a `changelog.d` fragment. + +**No `test_acceptance_*.py` file was edited at all**, so the "acceptance-suite +edit is its own commit" rule never had to be exercised. + +## Key findings (verified this session) + +- The integration branch carries **no** executor change for `KEYED` and **no** + `fit/kernels.py` change. `grep` over `origin/main`'s `executor.py` finds no + `seed_source` branch. Honouring `KEYED` therefore required no executor edit. +- `QRFKernel.implementation_hash()` hashes `microcosm.fit.qrf`'s module bytes, + so editing `qrf.py` moved `fit.qrf@1`'s implementation hash + (`02db8f5c… → d1f8b192…`) and all three pinned platform node keys. +- **Foreign-platform node keys are locally derivable.** Re-deriving the three + OLD pinned keys from this Mac with the OLD implementation hash and only the + fingerprint string varied reproduces all three exactly, so the platform + reaches a key as that string and nothing else. +- **`tools/graph_parity_fixtures.py` must not be edited.** `ParityCsvSource` + and `ParityRulesEngine` are defined there, so its bytes are inside + `ParityCsvSource.implementation_hash()` and + `SimulateRulesKernel.implementation_hash()`. Measured: adding the re-pin code + there moved the calibrate node key `184ccd0a → f8c9ed4b` with its + implementation hash unchanged. The re-pin logic therefore lives in a sibling + module. Reverted. +- `generate()` resets `pins["platforms"]` to the local platform alone, so a bare + regeneration would have dropped both `x86_64/linux` pins and silently put H1 + on its off-platform branch there (which asserts no bytes). +- `direct.csv` is byte-identical before and after on every platform + (`7b8dbd56c91ee71552ff6d892a42c56494b1813fb5d4b11553a8a8ccc9b90dca`). + +## Open for decision (Max / the merge owner) + +1. **`tools/spec_engine_coverage.py --check` is red on this branch** (exit 0 on + `origin/main`, exit 1 here) and so is + `packages/microcosm-build/tests/test_spec_engine_inventory_coverage.py::test_us_inventory_is_structure_exact_and_complete`. + Bisected to `packages/microcosm-fit/src/microcosm/fit/qrf.py` alone: + `microcosm.fit.qrf` is in `_QRF_KERNEL_MODULES` + (`spec_engine/seeds.py:353-362`), which feeds the `regime_gated_qrf` kernel + attestation's `source_sha256`, the seed protocol digest and the seed-map + digest. `SeedSource.KEYED`, `randomness.py` and the export move none of it. + The lane brief says report, do not re-pin, so the branch carries the drift. + The re-pin recipe is tested and written up in the lane report. +2. **Amendment numbering.** `main`'s list ends at 18, so 19 is the next free + number by its own arithmetic. The brief assigns 20 (the artifacts lane owns + 19). Two unmerged commits on the + `candidate-quality-producer-integration-20260905` family already claim both: + `3ff92b0ae` = 19 ("Typed artifacts and stable draw coordinates", which + bundles this lane's subject), `d2043d85e` = 20 ("Failed typed evidence + remains a failed gate"). Neither is on `main` or on the integration branch. ## Next -- Red-first tests, then the implementation, then the pins. +- Nothing blocking. A reviewer should decide (1) and (2) above. From 457b36944693e27cda3b45266678d621e727a268 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:11:09 -0400 Subject: [PATCH 10/27] Red: the re-pin derives foreign keys without checking the environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool writes node keys for platforms this machine is not. That is sound only while every pinned platform shares this machine's locked environment, because source_hash folds each declared dependency's installed version into the implementation hash, and node_key folds that hash in — a channel no platform fingerprint string records. The guard added alongside it reproduces each pinned key from pins["implementation_hash"], which substitutes away the only input that carries those versions, so any environment reproduces the pins and the check cannot see the drift it is there to catch. pins.json already records the versions the keys were taken under; nothing reads them. Three tests for the check that would close it, red; the five covering the refusals that already exist pass. Co-Authored-By: Claude Opus 5 --- .../tests/test_graph_parity_repin.py | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 packages/microcosm-graph/tests/test_graph_parity_repin.py diff --git a/packages/microcosm-graph/tests/test_graph_parity_repin.py b/packages/microcosm-graph/tests/test_graph_parity_repin.py new file mode 100644 index 000000000..6ee616341 --- /dev/null +++ b/packages/microcosm-graph/tests/test_graph_parity_repin.py @@ -0,0 +1,206 @@ +"""What licenses ``graph_parity_repin`` to derive a key for a foreign platform. + +The tool writes node keys for platforms this machine is not. A node key folds +in the kernel's implementation hash, and ``source_hash`` folds the installed +version of every declared dependency into that hash — so the fingerprint string +is not the only platform-dependent input, and deriving a foreign key is sound +only while every pinned platform shares this machine's locked environment. + +These tests pin the refusals that establish that condition rather than assume +it, and the one that keeps pinned *bytes* from ever being derived. +""" + +from __future__ import annotations + +import json +import shutil +from pathlib import Path +from typing import Callable + +import pytest + +from microcosm.graph import graph_from_json +from microcosm.graph.keys import platform_fingerprint +from tools import graph_parity_repin as repin_module +from tools.graph_parity_fixtures import parity_registry +from tools.graph_parity_repin import derived_node_key, repin + + +@pytest.fixture +def case_copy( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> Callable[[str], Path]: + """Re-point the tool at a throwaway copy so a test can mutate a fixture.""" + + def make(name: str) -> Path: + root = tmp_path / "parity" + shutil.copytree(repin_module.FIXTURES, root) + monkeypatch.setattr(repin_module, "FIXTURES", root) + return root / name + + return make + + +def _pins(case: Path) -> dict: + return json.loads((case / "pins.json").read_text(encoding="utf-8")) + + +def _write(case: Path, pins: dict) -> None: + (case / "pins.json").write_text( + json.dumps(pins, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + + "\n", + encoding="utf-8", + ) + + +def test_the_recorded_dependency_versions_are_this_environment_s() -> None: + """The standing fact that makes every derived pin in the tree legitimate. + + ``pins["dependencies"]`` is the environment the existing keys were taken + under. While it equals this machine's, this machine's implementation hash + for that same source would have been the recorded one, so the pinned + platforms and this one shared a locked environment. + """ + case = repin_module.FIXTURES / "fit.qrf" + pins = _pins(case) + kernel = parity_registry().get(pins["kernel"]) + installed = repin_module.installed_dependency_versions(kernel) + assert pins["dependencies"] == installed + + +def test_a_recorded_dependency_version_this_machine_lacks_refuses( + case_copy: Callable[[str], Path], +) -> None: + """The channel the fingerprint string cannot capture, closed. + + A different ``numpy`` moves ``implementation_hash`` for a reason no platform + fingerprint records. Deriving a foreign key from this machine's hash would + then write a key that platform never computes, so the tool must refuse. + """ + case = case_copy("fit.qrf") + pins = _pins(case) + pins["dependencies"]["numpy"] = "0.0.0-not-installed-here" + _write(case, pins) + + with pytest.raises(SystemExit) as raised: + repin("fit.qrf") + message = str(raised.value) + assert "numpy" in message + assert "0.0.0-not-installed-here" in message + + +def test_the_dependency_check_runs_before_any_key_is_derived( + case_copy: Callable[[str], Path], monkeypatch: pytest.MonkeyPatch +) -> None: + """Refusing after deriving would already have trusted the bad environment.""" + case = case_copy("fit.qrf") + pins = _pins(case) + pins["dependencies"]["pandas"] = "0.0.0-not-installed-here" + _write(case, pins) + + def forbidden(*args: object, **kwargs: object) -> str: + raise AssertionError("a key was derived before the environment was checked") + + monkeypatch.setattr(repin_module, "derived_node_key", forbidden) + with pytest.raises(SystemExit): + repin("fit.qrf") + + +def test_a_foreign_pin_the_recorded_hash_cannot_reproduce_refuses( + case_copy: Callable[[str], Path], +) -> None: + """Every pin must be internally consistent with the hash recorded beside it. + + An entry that is not was written by something other than this tool under + these pins — a hand edit, or a platform pinned from a moved generator — and + re-deriving it here would launder that into a fresh-looking key. + """ + case = case_copy("fit.qrf") + pins = _pins(case) + foreign = next(p for p in pins["platforms"] if p != platform_fingerprint()) + pins["platforms"][foreign]["node_key"] = "0" * 64 + _write(case, pins) + + with pytest.raises(SystemExit) as raised: + repin("fit.qrf") + assert foreign in str(raised.value) + + +def test_the_direct_bytes_compared_are_this_platform_s_own_pin( + case_copy: Callable[[str], Path], +) -> None: + """A platform-bitwise kernel's local bytes are not the authoring platform's. + + The tool must compare the direct call against the file *this* platform's + entry points at. Pointing the local entry at a copy while corrupting the + top-level ``direct.csv`` separates the two: reading the wrong file would + refuse here, and the run must instead reach the later reproduction check. + """ + case = case_copy("fit.qrf") + pins = _pins(case) + local = platform_fingerprint() + if local not in pins["platforms"]: + pytest.skip(f"{local} carries no fit.qrf pin") + + moved = Path("platforms") / "local-copy" / "direct.csv" + (case / moved).parent.mkdir(parents=True, exist_ok=True) + (case / moved).write_bytes((case / pins["platforms"][local]["direct"]).read_bytes()) + pins["platforms"][local]["direct"] = str(moved) + # The file the *old* comparison read, now holding bytes no platform produced. + (case / "direct.csv").write_bytes(b"not,the,direct,call\n") + # Force a refusal strictly after the byte comparison, so the message says + # which check the run reached. + foreign = next(p for p in pins["platforms"] if p != local) + pins["platforms"][foreign]["node_key"] = "0" * 64 + _write(case, pins) + + with pytest.raises(SystemExit) as raised: + repin("fit.qrf") + message = str(raised.value) + assert "cannot reproduce" in message, message + assert "the direct call's bytes moved" not in message, message + + +def test_a_moved_direct_call_refuses_rather_than_re_pinning( + case_copy: Callable[[str], Path], +) -> None: + """Bytes are never derived: a moved direct call is not a re-pin at all.""" + case = case_copy("fit.qrf") + pins = _pins(case) + local = platform_fingerprint() + if local not in pins["platforms"]: + pytest.skip(f"{local} carries no fit.qrf pin") + (case / pins["platforms"][local]["direct"]).write_bytes(b"moved,bytes\n") + + with pytest.raises(SystemExit) as raised: + repin("fit.qrf") + assert "the direct call's bytes moved" in str(raised.value) + + +def test_derived_node_key_substitutes_only_the_named_node_s_hash() -> None: + """The substitution is what lets a pin be checked against its own hash.""" + case = repin_module.FIXTURES / "fit.qrf" + pins = _pins(case) + graph = graph_from_json((case / "graph.json").read_text()) + inputs = case / "inputs.csv" + local = platform_fingerprint() + + registered = derived_node_key(graph, pins["node"], inputs, local) + same = derived_node_key( + graph, pins["node"], inputs, local, pins["implementation_hash"] + ) + other = derived_node_key(graph, pins["node"], inputs, local, "0" * 64) + # The pins record the registered hash, so substituting it changes nothing; + # substituting a different one must move the key, or the parameter is inert. + assert same == registered + assert other != registered + + +def test_a_repin_of_an_unchanged_kernel_rewrites_the_pins_byte_for_byte( + case_copy: Callable[[str], Path], +) -> None: + """Idempotence: the tool is a no-op when nothing about the kernel moved.""" + case = case_copy("fit.qrf") + before = (case / "pins.json").read_bytes() + repin("fit.qrf") + assert (case / "pins.json").read_bytes() == before From 8b284c584abb4b9d0d88b27c82208ced38f6a74c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:13:02 -0400 Subject: [PATCH 11/27] Green: a re-pin checks the environment it derives foreign keys from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pins.json already recorded the dependency versions the existing keys were taken under; the tool now reads them and refuses unless they equal this machine's installed versions. That is the check that sees the dependency channel — the reproduction loop cannot, because substituting the recorded implementation hash removes the only input those versions reach. The check runs before any key is derived, so a refusal never follows a derivation that already trusted the environment under test. The docstring said the reproduction loop "proves the assumption rather than asserting it" and that "only one locked environment can do that". Neither was true of the code: any environment whose sources match reproduces the pins. It now states what each of the two checks establishes, and the reproduction refusal no longer asserts a cause it has not established — a moved graph_parity_fixtures.py re-keys every parity node and fails on the local platform first, where "re-pin it on that platform" was advice to re-run the command that just refused. Co-Authored-By: Claude Opus 5 --- .../tests/test_graph_parity_repin.py | 6 +- tools/graph_parity_repin.py | 131 +++++++++++++++--- 2 files changed, 117 insertions(+), 20 deletions(-) diff --git a/packages/microcosm-graph/tests/test_graph_parity_repin.py b/packages/microcosm-graph/tests/test_graph_parity_repin.py index 6ee616341..fb94785f5 100644 --- a/packages/microcosm-graph/tests/test_graph_parity_repin.py +++ b/packages/microcosm-graph/tests/test_graph_parity_repin.py @@ -14,8 +14,8 @@ import json import shutil +from collections.abc import Callable from pathlib import Path -from typing import Callable import pytest @@ -27,9 +27,7 @@ @pytest.fixture -def case_copy( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> Callable[[str], Path]: +def case_copy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Callable[[str], Path]: """Re-point the tool at a throwaway copy so a test can mutate a fixture.""" def make(name: str) -> Path: diff --git a/tools/graph_parity_repin.py b/tools/graph_parity_repin.py index b0a3cb58a..630cee99b 100644 --- a/tools/graph_parity_repin.py +++ b/tools/graph_parity_repin.py @@ -21,13 +21,36 @@ **derived**, because a node key is a pure function of the declaration, the resolved input identities, the implementation hash, the capability projection, and — for a platform-bitwise kernel (amendment 16) — the platform fingerprint -*string* (``microcosm.graph.keys.node_key``); nothing else about a platform -reaches a key. The derivation is checked against the produced key on every run, -so it cannot drift from what the executor does. Pinned **bytes** are never -derived: each platform's ``direct.csv`` is left exactly as that platform -recorded it, and a re-pin refuses to write at all if the local direct call's -bytes moved, because that would mean the change was not additive and the -fixture needs a real regeneration instead. +*string* (``microcosm.graph.keys.node_key``). + +The fingerprint is not the *only* platform-dependent input, and this does not +pretend otherwise: ``source_hash`` folds ``f"{distribution}=={version}"`` for +each declared dependency into the implementation hash, so a platform on a +different locked environment keys differently for a reason no fingerprint +string records. A re-pin therefore establishes the condition it needs, in two +checks that close different holes: + +1. ``pins["dependencies"]`` — the versions the existing keys were taken under — + must equal this machine's installed versions. This is the check that sees + the dependency channel. The reproduction below cannot see it, because + substituting the recorded implementation hash removes the only input those + versions reach. +2. Every pinned platform's key must reproduce from + ``pins["implementation_hash"]``. This catches an entry inconsistent with the + hash recorded beside it — a hand edit, or a platform pinned while the + generator's own bytes had moved — which comparing versions would not. + +Together they say the pinned platforms and this one shared one locked +environment at pin time, and every pin is the key that environment computed. +That is what licenses deriving the new foreign keys here. The local key is +additionally derived alongside being executed, so the derivation cannot drift +from what the executor computes. + +Pinned **bytes** are never derived: each platform's ``direct.csv`` is left +exactly as that platform recorded it, and a re-pin refuses to write at all if +the local direct call's bytes moved from the file **this** platform's pin points +at, because that would mean the change was not additive and the fixture needs a +real regeneration instead. Usage:: @@ -41,6 +64,7 @@ import os import sys from collections.abc import Iterator +from importlib import metadata as importlib_metadata from pathlib import Path from tempfile import TemporaryDirectory @@ -67,7 +91,12 @@ parity_registry, ) -__all__ = ["BUILDERS", "derived_node_key", "repin"] +__all__ = [ + "BUILDERS", + "derived_node_key", + "installed_dependency_versions", + "repin", +] BUILDERS = { "fit.qrf": _fit_case, @@ -97,8 +126,15 @@ def derived_node_key( node_id: str, inputs_path: Path, fingerprint: str, + implementation_hash: str | None = None, ) -> str: - """The key ``fingerprint`` computes for ``node_id`` over ``inputs_path``.""" + """The key ``fingerprint`` computes for ``node_id`` over ``inputs_path``. + + ``implementation_hash`` substitutes that value for ``node_id``'s own kernel, + so a caller can ask what a platform keyed under a *previous* implementation + — the one its existing pin was taken under — and check the answer against + that pin. Every other node keeps its registered kernel's hash. + """ compiled = compile_graph(graph) source_keys = { source.name: graph_keys.source_content_key(source.name, inputs_path) @@ -113,13 +149,28 @@ def derived_node_key( compiled, current, keys, - kernel.implementation_hash(), + implementation_hash + if current == node_id and implementation_hash is not None + else kernel.implementation_hash(), source_keys, kernel_capabilities=kernel.capabilities, ) return keys[node_id] +def installed_dependency_versions(kernel: object) -> dict[str, str]: + """This machine's versions of ``kernel``'s declared dependencies. + + Built exactly as ``graph_parity_fixtures._pins`` builds the mapping it + records, so the two are comparable without normalising either. + """ + capabilities = kernel.capabilities # type: ignore[attr-defined] + return { + name: importlib_metadata.version(name) + for name in sorted(capabilities.dependencies) + } + + def _csv_bytes(frame: pd.DataFrame) -> bytes: """The exact bytes ``_write_case`` writes for a pinned table.""" return frame.to_csv(index=False, lineterminator="\n").encode("utf-8") @@ -159,12 +210,6 @@ def repin(name: str) -> dict[str, str]: raise SystemExit( f"{name}: inputs.csv no longer matches the builder; regenerate instead" ) - if (destination / "direct.csv").read_bytes() != _csv_bytes(direct): - raise SystemExit( - f"{name}: the direct call's bytes moved, so this is not a re-pin; " - "regenerate the fixture and say what changed" - ) - fingerprint = platform_fingerprint() platforms = dict(pins.get("platforms", {})) platforms.setdefault( @@ -176,8 +221,62 @@ def repin(name: str) -> dict[str, str]: "use graph_parity_fixtures.py platform-pin to add one" ) + # This platform's own pinned bytes, which for a platform-bitwise kernel are + # not the authoring platform's; comparing against the wrong file would refuse + # a legitimate re-pin off the authoring platform, or accept a drifted one. + local_direct = destination / platforms[fingerprint]["direct"] + if local_direct.read_bytes() != _csv_bytes(direct): + raise SystemExit( + f"{name}: the direct call's bytes moved from " + f"{platforms[fingerprint]['direct']}, so this is not a re-pin; " + "regenerate the fixture and say what changed" + ) + node_id = pins["node"] inputs_path = destination / "inputs.csv" + # Step 1 of the module docstring's argument, and the only step that sees the + # dependency channel. It has to come before any derivation: deriving first + # would already have trusted the environment under test. + installed = installed_dependency_versions(kernel) + if pins.get("dependencies") != installed: + drifted = sorted( + set(pins.get("dependencies", {})) | set(installed), + key=lambda dependency: dependency, + ) + detail = ", ".join( + f"{dependency}: pinned " + f"{pins.get('dependencies', {}).get(dependency, '(absent)')} vs " + f"installed {installed.get(dependency, '(absent)')}" + for dependency in drifted + if pins.get("dependencies", {}).get(dependency) != installed.get(dependency) + ) + raise SystemExit( + f"{name}: this environment is not the one the pins were taken " + f"under ({detail}). The implementation hash folds those versions " + "in, so a foreign platform's new key cannot be derived here; " + "re-pin from an environment matching uv.lock." + ) + + # Step 2: every pin must be internally consistent with the hash beside it. + for platform, entry in sorted(platforms.items()): + reproduced = derived_node_key( + graph, node_id, inputs_path, platform, pins["implementation_hash"] + ) + if reproduced != entry["node_key"]: + local = " (this machine)" if platform == fingerprint else "" + raise SystemExit( + f"{name}: cannot reproduce {platform}'s existing pin " + f"{entry['node_key']} from the recorded implementation hash " + f"(got {reproduced}){local}. The pin does not belong to the " + "hash recorded beside it, so re-deriving it here would launder " + "a stale or hand-edited key. Causes, in the order worth " + "checking: tools/graph_parity_fixtures.py's own bytes moved " + "(that re-keys every parity node, and this machine fails " + "first); the entry was edited by hand; or it was pinned on " + "that platform under a different implementation. Regenerate " + "the fixture, or re-pin that platform there." + ) + produced = _executed_node_key(name, graph, inputs_path) if derived_node_key(graph, node_id, inputs_path, fingerprint) != produced: raise SystemExit( From 12637772a0de2d954e2623b6a5e30c9f751c41b5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:14:02 -0400 Subject: [PATCH 12/27] Pin the final-bin closure the amendment claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amendment 20 states two deliberate differences from the generator path: the inverse CDF compares strictly, and it closes its final bin at 1.0. Only the first was pinned. Deleting `cumulative[:, -1] = 1.0` left all fourteen stateless tests green. The guard is load-bearing: predict_proba rows are floating point and need not sum to exactly 1.0, and on a short row a uniform above the sum makes every comparison false, so argmax returns 0 and the draw silently takes the first — most negative — class. The new test is the only one that fails without the closure. Co-Authored-By: Claude Opus 5 --- .../microcosm-fit/tests/test_qrf_stateless.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/microcosm-fit/tests/test_qrf_stateless.py b/packages/microcosm-fit/tests/test_qrf_stateless.py index 3d099a70d..058ac7ce2 100644 --- a/packages/microcosm-fit/tests/test_qrf_stateless.py +++ b/packages/microcosm-fit/tests/test_qrf_stateless.py @@ -119,6 +119,29 @@ def test_zero_uniform_skips_a_zero_probability_sign(model, monkeypatch): assert (actual.mixed == 3.0).all() +def test_a_cdf_short_of_one_cannot_fall_back_to_the_first_sign(model, monkeypatch): + """The other half of the deliberate inverse-CDF change: the final bin closes. + + ``predict_proba`` rows are floating-point and need not sum to exactly 1.0. + Without ``cumulative[:, -1] = 1.0`` a uniform above that sum makes every + comparison false, and ``argmax`` on an all-false row returns 0 — silently + selecting the *first* class, here the negative one, for a draw that should + land in the last bin. Closing the bin is what the charter's amendment 20 + claims, so it is pinned rather than left to the strict-comparison test. + """ + gate = model._target_models["mixed"].gate + assert list(gate.classes_) == [-1, 0, 1] + short = np.array([0.5, 0.5 - 2e-16, 0.0]) + assert short.cumsum()[-1] < 1.0, "the row must fall short for this to bite" + monkeypatch.setattr(gate, "predict_proba", lambda x: np.tile(short, (len(x), 1))) + draws = uniforms(model, 2) + draws["sign_uniforms"]["mixed"] = np.full(2, 1.0 - 1e-16) + actual = model.predict_from_uniforms(pd.DataFrame({"x": [1.0, 2.0]}), **draws) + assert (actual.mixed == 3.0).all(), ( + "a uniform past a short CDF selected the first class, not the last" + ) + + def test_stateless_replays_legacy_uniforms_across_all_regimes(model): """Ordinary uniforms preserve legacy draws; exact CDF ties differ deliberately. From 3beca70a5da3e6ca48826e8c0281035c0c63ba4b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:15:09 -0400 Subject: [PATCH 13/27] Make the keyed-kernel contract test assert the half it only asserted in prose The docstring promised "the context still offers rng; a keyed kernel simply does not spend it", but the test only compared draws from two contexts whose generators sat at different positions. Its toy body never referenced context.rng, so the equality held by construction and a body that did spend the generator passed unchanged. It now deep-copies the bit generator state across the call and asserts it is where it started; adding `context.rng.random(1)` to the body makes it fail. The docstring also claimed the behaviour was "exercised end to end at the kernel protocol level". It is not: the executor has no seed_source branch, so there is no production path that treats a KEYED node differently. The docstring now says what the toy body pins and what it does not. Co-Authored-By: Claude Opus 5 --- .../tests/test_graph_kernel_contract.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/microcosm-graph/tests/test_graph_kernel_contract.py b/packages/microcosm-graph/tests/test_graph_kernel_contract.py index f3f267ff5..10fe780cc 100644 --- a/packages/microcosm-graph/tests/test_graph_kernel_contract.py +++ b/packages/microcosm-graph/tests/test_graph_kernel_contract.py @@ -9,6 +9,7 @@ from __future__ import annotations +import copy import dataclasses import numpy as np @@ -263,9 +264,12 @@ def test_keyed_is_part_of_a_node_identity_and_survives_the_manifest() -> None: def test_a_keyed_kernel_draws_from_coordinates_not_from_the_context_rng() -> None: """The context still offers ``rng``; a keyed kernel simply does not spend it. - This is the behaviour the member names, exercised end to end at the kernel - protocol level: two contexts whose generators are at different positions - hand the same coordinates the same draws. + Both halves of that sentence are asserted against a toy kernel body, which + is as far as this can go: the executor has no ``seed_source`` branch, so + there is no production path that treats a KEYED node differently and none + is claimed here. What the toy body pins is the shape a keyed kernel has — + it leaves the generator where it found it, and two contexts whose + generators sit 512 variates apart hand the same coordinates the same draws. """ node = Node("impute", "toy.keyed@1", params={"experiment": "amendment-20"}) @@ -294,4 +298,8 @@ def context_at(position: int) -> KernelContext: early, late = context_at(0), context_at(512) assert early.rng.bit_generator.state != late.rng.bit_generator.state + # "Does not spend it" is the half a drawing body could violate silently, so + # assert it rather than leave it to the closure's own restraint. + untouched = copy.deepcopy(early.rng.bit_generator.state) assert draw(early).tobytes() == draw(late).tobytes() + assert early.rng.bit_generator.state == untouched From 6ea2917796d57def31bd452694d0b30690d8da77 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:17:55 -0400 Subject: [PATCH 14/27] Assert the authoring pin against the fixture, not against the helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test_the_top_level_pin_is_the_authoring_platform read through ``_platforms``, which back-fills the top-level pin into the mapping when it is missing — so the assertion that the authoring platform appears there was satisfied by the helper rather than by pins.json. It now reads the raw mapping. The derived-key test's docstring said the local pin "was produced by running the graph", which is true of the authoring platform and not of anywhere else the test runs. It now names the anchor that actually holds everywhere: test_h1_kernel_parity executes the graph on whatever platform is running and asserts the executor's key equals this same local pin. Co-Authored-By: Claude Opus 5 --- .../tests/test_graph_parity_pins.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/microcosm-graph/tests/test_graph_parity_pins.py b/packages/microcosm-graph/tests/test_graph_parity_pins.py index 0b1eac9ca..57cbd3c03 100644 --- a/packages/microcosm-graph/tests/test_graph_parity_pins.py +++ b/packages/microcosm-graph/tests/test_graph_parity_pins.py @@ -64,8 +64,10 @@ def test_the_top_level_pin_is_the_authoring_platform_and_its_bytes_exist( ) -> None: case, pins = _case(name) platforms = _platforms(pins) - assert pins["platform"] in platforms - assert platforms[pins["platform"]]["node_key"] == pins["node_key"] + # Read the raw mapping, not the one ``_platforms`` back-fills, or the + # assertion would be about this helper rather than about the fixture. + assert pins["platform"] in pins["platforms"] + assert pins["platforms"][pins["platform"]]["node_key"] == pins["node_key"] for entry in platforms.values(): direct = case / entry["direct"] assert direct.is_file(), f"{name}: {entry['direct']} is pinned but absent" @@ -94,9 +96,13 @@ def test_platform_bitwise_pins_partition_identity_across_platforms() -> None: def test_a_derived_key_is_the_local_platform_s_own_key() -> None: """The derivation is not a second implementation of ``node_key``. - On this machine the derived key and the pinned key agree, and the pinned key - was produced by running the graph — so the derivation is anchored to the - executor rather than to itself. + Deriving every platform's key would be circular if the derivation were only + ever checked against itself. It is not: on whatever platform this runs, + ``test_h1_kernel_parity`` executes the graph and asserts that the executor's + key equals this same local pin, so agreeing with the local pin here anchors + the derivation to the executor. (The local pin is produced by a run on the + authoring platform and derived elsewhere, which is exactly why the anchor + has to come from an executed key rather than from the pin's provenance.) """ case, pins = _case("fit.qrf") local = platform_fingerprint() From 8fb973941e23a149cc624c3ce0bfd2f0f589ccaf Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:17:55 -0400 Subject: [PATCH 15/27] Say precisely which node keys amendment 20 moves, and why MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entry said adding an enum member means "no existing node key moves", which reads as a claim about this branch — and this branch moves three fit.qrf keys. Both facts are true of different things: the member adds no projection field, so every kernel keeps projecting the seed source it already declared and no key moves for that reason; the fit.qrf keys move because qrf.py's bytes are inside the kernel's implementation hash. The entry now separates them instead of leaving a reader to reconcile it with the re-pinned fixture in the same diff. Also reflows the paragraph the edit left with a 31-character orphan line, and records in the changelog fragment what the re-pin tool must establish before it writes a key for a platform it is not. Co-Authored-By: Claude Opus 5 --- ...amend-keyed-seed-and-uniform-draws.added.md | 2 +- docs/graph-acceptance.md | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/changelog.d/amend-keyed-seed-and-uniform-draws.added.md b/changelog.d/amend-keyed-seed-and-uniform-draws.added.md index a6a5b7aed..44b0ecec0 100644 --- a/changelog.d/amend-keyed-seed-and-uniform-draws.added.md +++ b/changelog.d/amend-keyed-seed-and-uniform-draws.added.md @@ -1 +1 @@ -Graph interface amendment 20, keyed draw streams: `SeedSource.KEYED` declares that a kernel's randomness comes from normative stream parameters (`("sha256-u53-v1", experiment_id, replicate, base_seed)`) and one stable coordinate per draw rather than from a position in the executor's generator, and `microcosm.graph.randomness.keyed_uniform` computes those draws as the top 53 bits of a SHA-256 over the canonical stream and canonically tagged coordinates, reading and advancing no numpy RNG state. Order, chunking, and unrelated inserted identities cannot reach a draw. `FittedRegimeGatedQRF.predict_from_uniforms` is the first consumer surface: it draws from caller-supplied per-row uniforms, so pairing them with stable entity ids makes a batch invariant to recipient ordering and batching, and it advances no model RNG. `fit.qrf@1`'s existing outputs are unchanged — `predict` and its consumption order are untouched and the method is additive, and the kernel still declares `PARAM` or `EXECUTOR` — but its implementation hash moves with its module's source, so the H1 `fit.qrf` parity pins are re-recorded on all three pinned platforms while every `direct.csv` stays byte-identical. Adding an enum member changes no existing node's canonical projection, so no existing node key moves. `tools/graph_parity_repin.py` re-pins one parity case in place, keeping every platform it carries instead of dropping the ones the local machine cannot run. +Graph interface amendment 20, keyed draw streams: `SeedSource.KEYED` declares that a kernel's randomness comes from normative stream parameters (`("sha256-u53-v1", experiment_id, replicate, base_seed)`) and one stable coordinate per draw rather than from a position in the executor's generator, and `microcosm.graph.randomness.keyed_uniform` computes those draws as the top 53 bits of a SHA-256 over the canonical stream and canonically tagged coordinates, reading and advancing no numpy RNG state. Order, chunking, and unrelated inserted identities cannot reach a draw. `FittedRegimeGatedQRF.predict_from_uniforms` is the first consumer surface: it draws from caller-supplied per-row uniforms, so pairing them with stable entity ids makes a batch invariant to recipient ordering and batching, and it advances no model RNG. `fit.qrf@1`'s existing outputs are unchanged — `predict` and its consumption order are untouched and the method is additive, and the kernel still declares `PARAM` or `EXECUTOR` — but its implementation hash moves with its module's source, so the H1 `fit.qrf` parity pins are re-recorded on all three pinned platforms while every `direct.csv` stays byte-identical. Adding an enum member rather than a normative field gains no node a projection field, so the member itself moves no node key — every kernel keeps projecting the seed source it already declared, and the `fit.qrf` keys move because its implementation hash moved. `tools/graph_parity_repin.py` re-pins one parity case in place, keeping every platform it carries instead of dropping the ones the local machine cannot run; it writes a key for a platform it is not only after refusing unless this environment's dependency versions are the ones the pins were taken under and every pinned key reproduces from the implementation hash recorded beside it. diff --git a/docs/graph-acceptance.md b/docs/graph-acceptance.md index d21ac89df..c944e7551 100644 --- a/docs/graph-acceptance.md +++ b/docs/graph-acceptance.md @@ -316,14 +316,16 @@ Amendments so far (each re-locked): while `direct.csv` stays byte-identical on each, and that byte-identity is the evidence for the additivity claim rather than a restatement of it. Unlike amendments 11 and 13 this adds an enum member, not a - normative field, so no existing node's canonical projection changes and - no existing node key moves. One deliberate difference from the generator - path, commented where it lives: the sign gate's inverse CDF compares - strictly and closes its final bin at 1.0, so a uniform of exactly zero - skips a zero-probability class instead of selecting it, and a CDF that - sums to just under one can no longer silently select the first class. - Raised by the US launch integration branch, which carried the code - without an amendment; adopted 2026-09-11. + normative field: no node's canonical projection gains a field, so the + member itself moves no node key — every kernel keeps projecting the seed + source it already declared. The `fit.qrf` keys move because the + implementation hash moved, not because `KEYED` exists. One deliberate + difference from the generator path, commented where it lives: the sign + gate's inverse CDF compares strictly and closes its final bin at 1.0, + so a uniform of exactly zero skips a zero-probability class instead of + selecting it, and a CDF that sums to just under one can no longer + silently select the first class. Raised by the US launch integration + branch, which carried the code without an amendment; adopted 2026-09-11. Adding a normative field with a default changes the canonical projection of every node that carries it, so node keys moved with amendments 11 and From 74abdbbafa1daaa206eca8935e8ea6296367e0de Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:18:04 -0400 Subject: [PATCH 16/27] Bring the lane journal up to the reviewed state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the second pass (the four findings an adversarial review of the branch turned up and how each was fixed), corrects the first pass's unsound inference that a platform reaches a key through the fingerprint string "and nothing else" — the experiment substituted the implementation hash, which is exactly what removes the dependency channel it would need to have tested — and states the three open decisions with what each now costs: the spec-engine drift is seven red tests in a CI lane that always runs, the numbering gap renders as 19 whatever it is written as, and keyed_uniform gives -0.0 and 0.0 different draws. Co-Authored-By: Claude Opus 5 --- PROGRESS-amendment-20-keyed-draws.md | 95 +++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/PROGRESS-amendment-20-keyed-draws.md b/PROGRESS-amendment-20-keyed-draws.md index 670b488bd..0dacbcf93 100644 --- a/PROGRESS-amendment-20-keyed-draws.md +++ b/PROGRESS-amendment-20-keyed-draws.md @@ -5,6 +5,9 @@ Worktree: `~/PolicyEngine/_worktrees/microcosm-amend-keyed`. No push, no new bra Platform of record for H1 pins: **arm64/darwin/py3.14** (Python 3.14.4) — the fixture's authoring platform. +*Journal, not state: accurate as written on 2026-09-11. Check git and the +tracking issue for what is true later.* + ## State Implementation, tests, pins, charter and changelog are landed and committed. @@ -36,6 +39,35 @@ One item is deliberately left red and reported rather than fixed: see **No `test_acceptance_*.py` file was edited at all**, so the "acceptance-suite edit is its own commit" rule never had to be exercised. +### Second pass, after an adversarial review of the branch (same day) + +Six independent reviewers read the working tree; each finding was then put to +two adversarial verifiers. Four findings survived and were fixed: + +9. **The re-pin now checks the environment it derives foreign keys from** + (`test_graph_parity_repin.py`, new, 8 tests). The guard added in the first + pass reproduced every pinned key from `pins["implementation_hash"]` — which + substitutes away the only input carrying dependency versions, so *any* + environment reproduced the pins and the check could not see the drift it + existed to catch. `pins.json` already recorded the versions the keys were + taken under; nothing read them. `repin` now refuses unless they equal this + machine's installed versions, before deriving anything. +10. **The re-pin docstring no longer claims what the code does not do.** It had + said the reproduction loop "proves the assumption rather than asserting it" + and that "only one locked environment can do that". Neither was true. It now + states what each of the two checks establishes. +11. **The final-bin closure is pinned.** Amendment 20 claims two deliberate + differences from the generator path; only the strict comparison was tested. + Deleting `cumulative[:, -1] = 1.0` left all fourteen stateless tests green. +12. **The keyed-kernel contract test asserts the half it only asserted in + prose.** Its toy body never referenced `context.rng`, so "a keyed kernel + simply does not spend it" held by construction. It now deep-copies the bit + generator state across the call; adding `context.rng.random(1)` to the body + makes it fail. + +Each fix was mutation-tested: the mutation that breaks the behaviour makes the +new assertion, and only it, go red. + ## Key findings (verified this session) - The integration branch carries **no** executor change for `KEYED` and **no** @@ -44,10 +76,17 @@ edit is its own commit" rule never had to be exercised. - `QRFKernel.implementation_hash()` hashes `microcosm.fit.qrf`'s module bytes, so editing `qrf.py` moved `fit.qrf@1`'s implementation hash (`02db8f5c… → d1f8b192…`) and all three pinned platform node keys. -- **Foreign-platform node keys are locally derivable.** Re-deriving the three - OLD pinned keys from this Mac with the OLD implementation hash and only the - fingerprint string varied reproduces all three exactly, so the platform - reaches a key as that string and nothing else. +- **Foreign-platform node keys are derivable here because the environments + agree — not because the fingerprint is the only channel.** A platform reaches + a key two ways: the fingerprint string, and the implementation hash, into + which `source_hash` folds `f"{distribution}=={version}"` for every declared + dependency. Re-deriving the three OLD pinned keys with the OLD implementation + hash substituted and only the fingerprint varied reproduces all three — which + establishes that each pin is the key its platform computed *under that hash*, + and establishes nothing about the dependency channel, because substituting + the hash is exactly what removes it. That channel is now checked separately + and explicitly. (The first pass wrote "so the platform reaches a key as that + string and nothing else"; the experiment could not support it.) - **`tools/graph_parity_fixtures.py` must not be edited.** `ParityCsvSource` and `ParityRulesEngine` are defined there, so its bytes are inside `ParityCsvSource.implementation_hash()` and @@ -60,19 +99,33 @@ edit is its own commit" rule never had to be exercised. on its off-platform branch there (which asserts no bytes). - `direct.csv` is byte-identical before and after on every platform (`7b8dbd56c91ee71552ff6d892a42c56494b1813fb5d4b11553a8a8ccc9b90dca`). +- **`origin/main` moved during the lane** (to `e6d362b7e`, PR #909, object-dtype + storage hashing). It touches `graph/store.py` and `graph/population.py`; + neither enters `QRFKernel.implementation_hash()`. Merging is clean + (`git merge-tree`, no file touched by both), and running H1 parity, the pin + tests and the tolerance pin with main's post-#909 versions of those two files + in place is green, so main's advance does not disturb this lane's pins. ## Open for decision (Max / the merge owner) -1. **`tools/spec_engine_coverage.py --check` is red on this branch** (exit 0 on - `origin/main`, exit 1 here) and so is - `packages/microcosm-build/tests/test_spec_engine_inventory_coverage.py::test_us_inventory_is_structure_exact_and_complete`. - Bisected to `packages/microcosm-fit/src/microcosm/fit/qrf.py` alone: - `microcosm.fit.qrf` is in `_QRF_KERNEL_MODULES` - (`spec_engine/seeds.py:353-362`), which feeds the `regime_gated_qrf` kernel - attestation's `source_sha256`, the seed protocol digest and the seed-map - digest. `SeedSource.KEYED`, `randomness.py` and the export move none of it. - The lane brief says report, do not re-pin, so the branch carries the drift. - The re-pin recipe is tested and written up in the lane report. +1. **The spec-engine seed digests are stale on this branch, and that is + merge-blocking.** `tools/spec_engine_coverage.py --check` exits 1 (0 on + `origin/main`). The cause is `packages/microcosm-fit/src/microcosm/fit/qrf.py` + alone, bisected: `microcosm.fit.qrf` is in `_QRF_KERNEL_MODULES` + (`spec_engine/seeds.py:353-362`), whose `source_inventory_sha256` hashes each + module's exact installed source bytes. `SeedSource.KEYED`, `randomness.py` + and the export move none of it. **Seven tests are red**: one failure in + `test_spec_engine_inventory_coverage.py` and six fixture errors in + `test_spec_engine_coverage_tool.py`. Both files run in the **`engine-shared`** + CI lane, which has no `if:` condition — it runs on every PR and the aggregate + gate requires it — so this cannot be merged as it stands. (The integration + branch re-pinned these same two digests itself, in `1734b9e90`; its values + cannot be copied here because they also fold in its own `acs_transfer` and + housing changes.) The lane brief said + report, do not re-pin, so the branch carries the drift. The re-pin is two + pin values plus a regenerated evidence file; it was applied, verified green, + and reverted this session, so the recipe in the lane report is tested rather + than proposed. 2. **Amendment numbering.** `main`'s list ends at 18, so 19 is the next free number by its own arithmetic. The brief assigns 20 (the artifacts lane owns 19). Two unmerged commits on the @@ -80,7 +133,19 @@ edit is its own commit" rule never had to be exercised. `3ff92b0ae` = 19 ("Typed artifacts and stable draw coordinates", which bundles this lane's subject), `d2043d85e` = 20 ("Failed typed evidence remains a failed gate"). Neither is on `main` or on the integration branch. + Concrete consequence of leaving the gap: CommonMark renumbers an ordered + list from its first item, so the entry *renders* as 19 however it is + written — colliding with the number the artifacts lane expects. +3. **`keyed_uniform` gives `-0.0` and `0.0` different draws** (verified: + `0.3464688…` vs `0.3251263…`), though they are `==` and hash-equal in Python. + `canonical_json` emits them distinctly and `_coordinate` does not normalise + the sign, so a float coordinate arriving as `-0.0` on one run re-randomises + that row. Not fixed here on purpose: `randomness.py` is byte-identical to the + integration branch that owns it, and `"sha256-u53-v1"` is a *versioned* + normative algorithm — changing which uniform a coordinate draws is a version + bump, not a drive-by fix. For that lane to decide. ## Next -- Nothing blocking. A reviewer should decide (1) and (2) above. +- Nothing blocking in this lane's own work. A reviewer should decide (1), (2) + and (3) above; (1) must be resolved before the branch can go green in CI. From 74e42f8eb6d46f982c45144309c71d025a382cef Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:31:33 -0400 Subject: [PATCH 17/27] Commit the lane receipts where they survive the worktree The brief's -o path is the repo root's out.md, which is a tracked file holding a different lane's report (the F1 worker-identity round-1 report, 71dbe2979). Committing this lane's report there would silently delete that record; leaving it uncommitted means it dies with the worktree. So the report stays in out.md uncommitted, and the identical content is committed under experiments/, which CLAUDE.md names as the home for lane receipts, with the journal pointing at it. Also corrects the journal's State section, which said one item was left red: seven tests are red, in a CI lane that runs on every PR. Co-Authored-By: Claude Opus 5 --- PROGRESS-amendment-20-keyed-draws.md | 12 +- .../amendment-20-keyed-draws-receipts.md | 509 ++++++++++++++++++ 2 files changed, 519 insertions(+), 2 deletions(-) create mode 100644 experiments/amendment-20-keyed-draws-receipts.md diff --git a/PROGRESS-amendment-20-keyed-draws.md b/PROGRESS-amendment-20-keyed-draws.md index 0dacbcf93..9386b15a3 100644 --- a/PROGRESS-amendment-20-keyed-draws.md +++ b/PROGRESS-amendment-20-keyed-draws.md @@ -8,12 +8,20 @@ fixture's authoring platform. *Journal, not state: accurate as written on 2026-09-11. Check git and the tracking issue for what is true later.* +Full receipts — every identity, command and exit code, and the three open +decisions with their evidence — are in +[`experiments/amendment-20-keyed-draws-receipts.md`](experiments/amendment-20-keyed-draws-receipts.md). + ## State Implementation, tests, pins, charter and changelog are landed and committed. `packages/microcosm-graph/tests` and `packages/microcosm-fit/tests` are green. -One item is deliberately left red and reported rather than fixed: see -**Open for decision** below. + +**The branch cannot go green in CI as it stands**, and that is deliberate: the +`qrf.py` edit moves the spec-engine seed digests, leaving seven tests red in a +lane that runs on every PR. The lane brief said report the drift, not re-pin +it, so the branch carries it and the decision goes to the merge owner — see +**Open for decision** item 1, which carries a tested recipe. ## Done diff --git a/experiments/amendment-20-keyed-draws-receipts.md b/experiments/amendment-20-keyed-draws-receipts.md new file mode 100644 index 000000000..903deaa95 --- /dev/null +++ b/experiments/amendment-20-keyed-draws-receipts.md @@ -0,0 +1,509 @@ +# Build lane report — amendment 20, keyed draw streams + +Repo: `PolicyEngine/microcosm`. Branch: `amend-keyed-seed-and-uniform-draws`, +cut from `origin/main` at `3094bfe84`. Worktree: +`~/PolicyEngine/_worktrees/microcosm-amend-keyed`. Nothing pushed; no new +branches; the stash was never touched; `uv.lock` unchanged. + +**Platform string of record for every H1 measurement below: +`arm64/darwin/py3.14`** (Python 3.14.4) — the `fit.qrf` fixture's own authoring +platform, so the local pin is *produced* rather than derived. + +**Three things need your decision, in §7.** One of them is merge-blocking: +the branch cannot go green in CI as it stands, by design of the brief. + +This report is also committed at +`experiments/amendment-20-keyed-draws-receipts.md`, because the root `out.md` +is a tracked file holding a different lane's report — see §7.4. + +--- + +## 1. What landed + +Sixteen commits. Red-before-green wherever behaviour changed. + +**First pass — the amendment itself:** + +``` +b46955507 Start the amendment 20 lane journal (keyed draw streams) +8d55f8172 Red: keyed draw streams and SeedSource.KEYED have no implementation yet +e75bd6b91 Green: SeedSource.KEYED and keyed_uniform; relock kernel.py +2c4dc34fd Red: FittedRegimeGatedQRF has no predict_from_uniforms yet +82e11113e Green: QRF draws from caller-supplied per-row uniforms +80c41e081 Add a re-pin path that keeps every pinned H1 platform, and check them all +c96a3f412 Re-pin the fit.qrf H1 fixture; direct.csv is byte-identical +77b7daabe Amendment 20: keyed draw streams; changelog fragment +2d0681c8f Bring the lane journal up to the landed state +``` + +**Second pass — after an adversarial review of the branch (§6):** + +``` +457b36944 Red: the re-pin derives foreign keys without checking the environment +8b284c584 Green: a re-pin checks the environment it derives foreign keys from +12637772a Pin the final-bin closure the amendment claims +3beca70a5 Make the keyed-kernel contract test assert the half it only asserted in prose +6ea291779 Assert the authoring pin against the fixture, not against the helper +8fb973941 Say precisely which node keys amendment 20 moves, and why +74abdbbaf Bring the lane journal up to the reviewed state +``` + +`git diff --stat $(git merge-base HEAD origin/main)...HEAD` — 16 files, +**+1495 / −6**: + +| File | ± | What | +|---|---|---| +| `packages/microcosm-graph/src/microcosm/graph/kernel.py` | +4 −2 | `SeedSource.KEYED = "keyed"` and the `KernelContext.rng` docstring. **Nothing else** — no `ArtifactValue`, no `ArtifactType` import, no `KernelContext.artifacts`, no `__all__` entry for them. | +| `packages/microcosm-graph/src/microcosm/graph/randomness.py` | +69 | New. `keyed_uniform` + `_coordinate`, **byte-identical** to the integration branch's copy (sha256 `797fa7e4c7cc2fc81312f7886521e9b0f3e02be577e601a9cabc24362485847a`; `diff` empty). | +| `packages/microcosm-graph/src/microcosm/graph/__init__.py` | +2 | `keyed_uniform` imported and placed in `__all__`'s lowercase tail between `graph_to_json` and `load_source`. | +| `packages/microcosm-fit/src/microcosm/fit/qrf.py` | **+84 −0** | `_draw_target_from_uniforms` and `predict_from_uniforms`. Blob-identical to `6e3907f86` (`e8bb2c95611c1d650dfc78785ac90f7325cc3f75`). The `−0` is the mechanical form of the additivity claim: no existing line is touched. | +| `docs/graph-interface.lock` | +1 −1 | `kernel.py` re-recorded. | +| `docs/graph-acceptance.md` | +35 | Amendment 20. | +| `changelog.d/amend-keyed-seed-and-uniform-draws.added.md` | +1 | towncrier fragment. | +| `packages/microcosm-graph/tests/test_graph_randomness.py` | +205 | New, 12 tests. | +| `packages/microcosm-graph/tests/test_graph_kernel_contract.py` | +98 −8 | 3 new `SeedSource.KEYED` contract tests, one of them strengthened in the second pass. | +| `packages/microcosm-fit/tests/test_qrf_stateless.py` | +182 | New. 14 tests verbatim from the integration branch, plus the final-bin-closure test the second pass added. | +| `packages/microcosm-fit/tests/test_kernels.py` | +16 | One guard: `SeedSource.KEYED` must not widen `QRFKernel`. | +| `packages/microcosm-graph/tests/test_graph_parity_pins.py` | +116 | New, 11 tests — every pinned H1 platform key is checkable from every platform. | +| `packages/microcosm-graph/tests/test_graph_parity_repin.py` | +204 | New, 8 tests — the re-pin tool's refusals (second pass). | +| `tools/graph_parity_repin.py` | +328 | New. The re-pin path. | +| `packages/…/fixtures/parity/kernels/fit.qrf/pins.json` | +1 −1 | Re-pinned. | +| `PROGRESS-amendment-20-keyed-draws.md` | +151 | Lane journal, committed from the first commit. | + +**`tools/graph_parity_fixtures.py` is NOT touched** — §4 explains why that +matters. **No `test_acceptance_*.py` file is touched at all**, so the +"acceptance-suite edit is its own commit" rule never had to be exercised. + +--- + +## 2. What was deliberately NOT brought over + +- **`ArtifactValue` / `KernelContext.artifacts`** — a separate lane owns them. + Verified absent: neither `git diff origin/main...HEAD` nor the working-tree + diff contains any occurrence of `ArtifactValue`, `ArtifactType`, or + `artifacts=` in code. The integration branch's `__post_init__`, its + `__init__.py` exports of `ArtifactInput`/`ArtifactOutput`/`ArtifactType`/ + `ArtifactValue`/`SourceBytesCodec`/`load_source_bytes`, and its + artifacts-dependent new module `fit/qrf_target.py` are all absent. +- **Any `executor.py` change.** The brief allowed "the minimal coherent + subset". The measured answer is **nothing**: + - The integration branch's executor diff is +419 lines of typed artifacts, + lazy population retention and receipt metadata, and contains **no + occurrence of `SeedSource`, `KEYED`, or `keyed`**. + - `grep -n "seed_source"` over `origin/main`'s `executor.py` returns nothing. + The executor never branches on the seed source; it always builds + `KernelContext.rng` from the node key and lets the kernel decide whether to + spend it. A `KEYED` kernel simply does not. + - Every other `SeedSource` use site in the repo was checked for a member + enumeration that would reject `"keyed"`; none rejects it. The only site + where a new member changes accept/reject behaviour is the capability + projection, which the contract tests cover. +- **Any `microcosm-fit/kernels.py` change.** The integration branch's diff + there is **empty**: it wires no `KEYED` draws into `fit.qrf@1`. + `QRFKernel.__init__` still refuses anything but `PARAM`/`EXECUTOR`, and this + branch adds a test asserting it keeps refusing. + +--- + +## 3. H1 parity — the pins, and the `direct.csv` byte-identity proof + +### 3.1 Why the pins had to move + +`QRFKernel.implementation_hash()` is +`source_hash(type(self), fit_qrf, fit_model_module, qrf_module, dependencies=…)`, +and `source_hash` digests each object's **defining module's bytes** plus +`f"{distribution}=={version}"` for each declared dependency (read this session +at `graph/kernel.py:387-427`). `qrf_module` is `microcosm.fit.qrf`. So an +additive edit to `qrf.py` moves the implementation hash, and `node_key` folds +`kernel_impl_hash` in — for a `PLATFORM_BITWISE` kernel alongside the platform +fingerprint. All four pinned identities move. + +``` +implementation_hash 02db8f5c849d876be20a95152b5302a5cacc0a7c77c58d8b436a3a00f57b4c92 + → d1f8b1929e6452aa507b0d9c64ba42a59851dc31c71021303c25f060e34c075b + +node keys OLD NEW +arm64/darwin/py3.14 8878352d… → 35af6a452bd32ca39d313d78255236df01877d684c2d847b1bd5a6016a68e237 (produced) +x86_64/linux/py3.13 f6984280… → 6c43edcb3edf2bdef20d915b1be16503d37583c678aced78f1442aca1139e1ae (derived) +x86_64/linux/py3.14 9e80ee3a… → ecb6b20c02b0bc442c4baf3fd7def6d6aec06e6b9567910294f945d66c43bbf8 (derived) +``` + +### 3.2 The `direct.csv` byte-identity proof + +`direct.csv` did **not** change, on any of the three platforms. Three +independent ways: + +1. **Content digests, before and after.** All three files, unchanged: + + ``` + 7b8dbd56c91ee71552ff6d892a42c56494b1813fb5d4b11553a8a8ccc9b90dca fit.qrf/direct.csv + 7b8dbd56c91ee71552ff6d892a42c56494b1813fb5d4b11553a8a8ccc9b90dca fit.qrf/platforms/x86_64-linux-py3_13/direct.csv + 7b8dbd56c91ee71552ff6d892a42c56494b1813fb5d4b11553a8a8ccc9b90dca fit.qrf/platforms/x86_64-linux-py3_14/direct.csv + ``` + (These three were already byte-equal to each other on `main`; this fixture's + twelve recipient draws happen to agree across the pinned platforms.) + +2. **Git.** `git diff --numstat` over the fixture tree shows `1 1 + …/fit.qrf/pins.json` and **no other fixture file**. Re-verified this + session: after running the re-pin on all three cases, `git status + --porcelain packages/microcosm-graph/tests/fixtures` is empty. + +3. **The tool refuses otherwise.** `repin()` recomputes the direct call from the + builder and compares it to the stored bytes *before* writing anything; a + byte difference raises `SystemExit`. The re-pin succeeded, so that comparison + passed — and `test_a_moved_direct_call_refuses_rather_than_re_pinning` now + pins that the refusal actually fires. + + `predict()` also still draws the same values under test: + `test_stateless_replays_legacy_uniforms_across_all_regimes` replays + `predict()` against an independently-advanced generator and asserts exact + frame equality across all seven regimes. + +### 3.3 What licenses the two derived foreign keys — corrected + +A node key is a pure function of the declaration, the resolved input +identities, the implementation hash, the capability projection, and — for a +platform-bitwise kernel — the platform fingerprint **string**. + +The first pass claimed the reproduction of the three old pins established that +"the platform reaches a key as that string **and nothing else**". **That +inference was wrong, and the review caught it.** A platform also reaches its +key through the implementation hash, which folds in that machine's installed +dependency versions; the reproduction experiment substituted the recorded +implementation hash, which is precisely what removes that channel. It could not +be evidence about it. + +What the reproduction does establish is narrower and still useful: each pinned +key is the one its platform computed *under the recorded hash*. The dependency +channel is now checked separately and explicitly — `repin` refuses unless +`pins["dependencies"]` (the versions the existing keys were taken under, already +recorded in the fixture and previously never read) equals this machine's +installed versions. Together the two checks say the pinned platforms and this +one shared one locked environment at pin time, which is the condition that makes +deriving the new foreign keys sound. + +`repin()` also re-derives the local key alongside executing the graph and +refuses if the two disagree, so the derivation cannot drift from what the +executor computes. + +**What is derived and what is not.** Keys are derived. **Bytes are never +derived**: each platform's `direct.csv` is left exactly as that platform +recorded it. I did not, and cannot, re-measure x86_64 floats from this machine. + +### 3.4 The re-pin is idempotent + +Re-verified this session, after the second pass changed the tool: + +``` +tools/graph_parity_repin.py fit.qrf exit 0, pins.json unchanged +tools/graph_parity_repin.py calibrate exit 0, pins.json unchanged +tools/graph_parity_repin.py simulate exit 0, pins.json unchanged +git status --porcelain empty +``` + +### 3.5 Standing guards + +- `test_graph_parity_pins.py` derives **every** pinned platform's key on + whatever platform is running and compares it to the pin. Before this, a stale + foreign pin survived until that Linux lane happened to run, and H1's + off-platform branch asserts no bytes at all — so a stale x86 pin was invisible + on a Mac. +- `test_graph_parity_repin.py` (second pass) pins each of the tool's refusals, + including the two the review found untested. Mutation-tested: deleting the + dependency check makes exactly the two behavioural tests red. + +### 3.6 `origin/main` moved during the lane — checked, no effect + +`origin/main` advanced from `3094bfe84` to `e6d362b7e` (PR #909, object-dtype +storage hashing) while this lane ran; worktrees share refs, so another session +fetched it. It touches `graph/store.py` and `graph/population.py`. Neither is in +`QRFKernel.implementation_hash()`'s source set, and neither is an attested +seed-kernel module. Measured rather than assumed: + +- `git merge-tree --write-tree HEAD origin/main` → exit 0, no conflict; zero + files touched by both sides. +- With main's post-#909 `store.py` and `population.py` swapped into the tree, + `test_acceptance_h_parity.py` + `test_graph_parity_pins.py` + + `test_fit_qrf_tolerance_source_hash_pin_is_current` → **exit 0**. Tree + restored and verified byte-identical afterwards. + +--- + +## 4. Why `tools/graph_parity_fixtures.py` was not edited (a trap worth recording) + +The first pass's initial attempt added `repin` to that module. It works — and it +silently moved **every parity node key in all three cases**: + +``` +calibrate node key 184ccd0a25e1d00cc4393b2880ff13a601ac04207bc37b0160b7ae60596cb3d7 + → f8c9ed4b33c75f3886471e48beb7c83685ead036e8335735ee9b2334cbbd2b1d +with calibrate.adam@1's implementation hash unchanged. +``` + +Because `ParityCsvSource` and `ParityRulesEngine` are **defined in** that +module, its bytes are inside `ParityCsvSource.implementation_hash()` and +`SimulateRulesKernel.implementation_hash()`. Every parity graph's source node +re-keys when the file changes, and every downstream node with it. + +So the re-pin lives in a sibling module, `tools/graph_parity_repin.py`, which +defines no kernel and imports the generator's `_pins` / `_write_pins` / case +builders so a re-pinned fixture stays indistinguishable from a generated one. + +The second reason not to use `generate()`: it rewrites `pins["platforms"]` as +the local platform alone. Running it here would have dropped both +`x86_64/linux` entries, orphaned their `direct.csv` files, and put CI's Linux +lanes onto H1's off-platform branch (no byte assertion) without any test going +red. + +--- + +## 5. Every command, with its direct exit code + +Environment prepared with `uv sync --all-packages --locked --extra us --extra uk`; +everything after used `uv run --no-sync`. + +| Command | Exit | Result | +|---|---|---| +| `pytest packages/microcosm-graph/tests` | **0** | **371 passed**, 1 warning, 159s (the warning is a pre-existing pydantic deprecation from `policyengine_uk`) | +| `pytest packages/microcosm-fit/tests` | **0** | ****116 passed**, 389s** | +| `pytest test_acceptance_h_parity.py test_graph_serialize.py::test_generated_parity_graphs_bind_real_kernels_and_direct_bytes test_graph_executor.py::test_fit_qrf_tolerance_source_hash_pin_is_current` | **0** | 6 passed — the three named H1 checks, on `arm64/darwin/py3.14` | +| `pytest test_graph_parity_repin.py` (new) | **0** | 8 passed | +| `pytest test_graph_parity_repin.py test_graph_parity_pins.py` | **0** | 19 passed | +| `python tools/ci_test_groups.py --verify` | **0** | `verification=ok`. The three new test files land in fast `rest` and engine `us-am`; **none** under `[defaulted]` (that section is 53 pre-existing `microcosm-build` files, and `ci_test_groups.py` is byte-identical to main's) | +| `ruff check .` | **0** | All checks passed | +| `ruff format --check` on every file this branch touches | **0** | all formatted | +| `shasum -a 256 -c docs/graph-interface.lock` | **0** | `decl.py: OK`, `kernel.py: OK` | +| `python tools/graph_parity_repin.py fit.qrf` / `calibrate` / `simulate` | **0** | idempotent; fixtures unchanged | +| `git merge-tree --write-tree HEAD origin/main` | **0** | clean merge with current main | +| **`python tools/spec_engine_coverage.py --check`** | **1** | **drift — §7.1** | +| **`pytest test_spec_engine_inventory_coverage.py test_spec_engine_coverage_tool.py`** | **1** | **1 failed + 6 errors = 7 red — §7.1** | + +Note on `ruff format --check .` repo-wide: it reports 81 files would be +reformatted, but that is **pre-existing on `main`**, and CI runs only +`ruff check .`. Every file this branch touches is format-clean. + +--- + +## 6. The second pass: what an adversarial review of the branch found + +Six reviewers read the working tree along separate axes (charter claims, test +rigor, the re-pin tool, brief compliance, implementation correctness, repo +integration); every finding was then put to two adversarial verifiers whose +default was to refute. Four findings survived and were fixed; each fix was +mutation-tested. + +1. **The re-pin tool's central claim was false.** Its docstring said the + reproduction loop "proves the assumption rather than asserting it" and that + "only one locked environment can do that". Neither was true: the loop + substitutes `pins["implementation_hash"]`, which is the only input carrying + dependency versions, so *any* environment reproduces the pins. Probed + directly — with the local kernel hash made uncallable, and with a faked + `scikit-learn` version, the loop still passed. Fixed by making the code do + what the prose claimed (read the recorded dependency versions, which + `pins.json` already carried and nothing read) and by rewriting the prose to + state what each check establishes. +2. **The final-bin closure had no test.** Amendment 20 claims two deliberate + differences from the generator path; only the strict comparison was pinned. + Deleting `cumulative[:, -1] = 1.0` left all fourteen stateless tests green. + The guard is load-bearing — on a `predict_proba` row summing to just under + 1.0, a uniform above the sum makes every comparison false and `argmax` + returns 0, silently selecting the *first*, most negative, class. Now pinned + by a test that is the only one to fail without the closure. +3. **A contract test could not fail for the defect it named.** Its toy body + never referenced `context.rng`, so "a keyed kernel does not spend it" held by + construction. It now asserts the bit generator state across the call; adding + `context.rng.random(1)` makes it fail. Its docstring also claimed the + behaviour was "exercised end to end at the kernel protocol level" — it is + not, because the executor has no `seed_source` branch — and now says so. +4. **A pins test asserted about its own helper.** It read through `_platforms`, + which back-fills the top-level pin, so the assertion that the authoring + platform appears in the mapping was satisfied by the helper rather than by + `pins.json`. + +Findings raised and **refuted** on inspection, recorded so they are not +re-litigated: the `_RELEASED` guard in `_draw_target_from_uniforms` is untested +but mirrors an existing tested one; the amendment's line-wrapping (fixed +anyway); and the "no test covers the re-pin tool" framing (now moot). + +What the review checked and found **clean**: no forbidden artifacts code; +`kernel.py` exactly two hunks with the `KEYED` line character-identical to the +integration branch's; `randomness.py` and `qrf.py` byte/blob-identical to their +sources; the executor conclusion sound on both its grounds; `graph-interface.lock` +digests reproduce and `shasum -c` passes; `__all__` placement correct (that list +is not globally sorted on main, and this does not make it worse); test placement +and CI grouping correct; changelog fragment name and type correct; `uv.lock` +untouched. + +--- + +## 7. Needs your decision + +### 7.1 The spec-engine seed digests are stale — and this is merge-blocking + +| | `origin/main` | this branch | +|---|---|---| +| `tools/spec_engine_coverage.py --check` | exit **0** (42156/42156 fields, 41/41 checks) | exit **1** | +| `test_spec_engine_inventory_coverage.py` | pass | **1 failed** | +| `test_spec_engine_coverage_tool.py` | pass | **6 errors** (fixture setup) | + +**Cause, bisected** (first pass, re-confirmed this session by computing the +digests directly): `packages/microcosm-fit/src/microcosm/fit/qrf.py` **alone**. +`microcosm.fit.qrf` is listed in `_QRF_KERNEL_MODULES` +(`spec_engine/seeds.py:353-362`); `source_inventory_sha256` hashes each module's +logical name and **exact installed source bytes**; that is the +`regime_gated_qrf` attestation's `source_sha256`, which is inside +`SeedProtocol.implementation_sha256`, which the inventory compares against +`EXPECTED_HASHES`. `SeedSource.KEYED`, `randomness.py` and the export move none +of it. This is exactly the situation `CLAUDE.md` describes: *"Spec identities … +attest kernel source and locked RNG-library versions, so they legitimately move +when main changes an attested module."* + +``` +seed_protocol pinned fd3e4b06f11be4e8c13ea19fef9469ab95cbbe3e2dcfce351e860dd3e00709e4 + now d8f595a05dd71d7eccce5976d640baf97541e3ca159fd6179e7b1ab1a1286d8e +seed_map pinned 87ba50531d9fa6683096ecb39a31655b331ab8acff3a5876c2f62b33562a0885 + now 373786d5ba5e115c3317c42662b3998cdef8061441cf0ba48c36828bd7d55e69 +``` + +**Why it blocks the merge.** Both test files run in the `engine-shared` CI job, +which — read in `.github/workflows/test.yml:202` — carries **no `if:` +condition**, so it runs on every PR, and line 367 (`require_success +engine-shared`) makes the aggregate gate depend on it. The branch's changes also +classify as `shared` (`packages/*/src/**`, `tools/**`), so `engine-us` runs too. +`gh pr checks` cannot go green as the branch stands. + +**The brief said report, do not re-pin, so the branch carries the drift.** The +fix is two pin values plus a regenerated evidence file (8 lines). I applied it, +measured it, and reverted it this session, so this is tested rather than +proposed: + +```diff +--- a/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py +@@ -359,8 +359,8 @@ EXPECTED_HASHES = { +- "seed_map": "87ba50531d9fa6683096ecb39a31655b331ab8acff3a5876c2f62b33562a0885", +- "seed_protocol": "fd3e4b06f11be4e8c13ea19fef9469ab95cbbe3e2dcfce351e860dd3e00709e4", ++ "seed_map": "373786d5ba5e115c3317c42662b3998cdef8061441cf0ba48c36828bd7d55e69", ++ "seed_protocol": "d8f595a05dd71d7eccce5976d640baf97541e3ca159fd6179e7b1ab1a1286d8e", +``` + +then `uv run python tools/spec_engine_coverage.py` (no `--check`) to regenerate +`docs/evidence/spec-engine/us-f0-coverage.json`. Verified afterwards — **all +seven tests, both files**, not just the one the first pass measured: + +``` +tools/spec_engine_coverage.py --check exit 0 +pytest test_spec_engine_inventory_coverage.py test_spec_engine_coverage_tool.py + exit 0 (22 passed) +``` + +Tree restored and digest-verified against its pre-state both times. + +**One fact that should weigh on the decision:** the integration branch that +this lane is landing code from re-pinned *these exact two digests itself*, in +`1734b9e90` ("Re-pin the seed protocol and compiled seed map digests for the +launch integration") — verified as an ancestor of that branch this session. Its +replacement values differ from the ones this branch needs (`c15bff65…` / +`a775cccd…`), because they also fold in that branch's own `acs_transfer` and +housing changes, so they cannot be copied. But it establishes that the upstream +lane treated this re-pin as the required companion to the `qrf.py` change rather +than as an open question. CLAUDE.md's own guidance ("merge main and re-pin") +points the same way. My recommendation is to apply it — on the merge ref, so the +digests attest the tree that actually merges. + +### 7.2 Amendment numbering — and it renders as 19 regardless + +`main`'s list ends at **18**, so by its own arithmetic the next free number is +**19**. The entry is numbered **20** because the brief assigns 20 and gives 19 to +the artifacts lane. Two unmerged commits on the +`candidate-quality-producer-integration-20260905` family already claim both: +`3ff92b0ae` = 19 ("Typed artifacts and stable draw coordinates", which *bundles* +this lane's subject), `d2043d85e` = 20. Neither is on `main` or on the +integration branch, and the integration branch's `docs/graph-acceptance.md` +diff against main is empty — it carries the code with no amendment at all, which +is what this lane was sent to fix. + +**New this pass:** leaving the gap has a concrete cost. The amendments are one +contiguous CommonMark ordered list, and renderers renumber from the first item — +so the entry **displays as "19."** on GitHub however it is written, colliding +with the number the artifacts lane expects and contradicting the item's own +text, the changelog fragment, the test docstrings and the journal, all of which +say 20. Nothing executable parses the list (the burndown and `explain.py` read +only `| A1 |`-style table rows), so nothing breaks — but the document already +*reads* as 19. Renumbering to 19, or landing a 19 placeholder, are both +one-token edits; which is right depends on what the artifacts lane lands as. + +### 7.3 `keyed_uniform` draws differently for `-0.0` and `0.0` + +Verified directly: + +``` +canonical_json([["float", 0.0]]) -> b'[["float",0.0]]' +canonical_json([["float", -0.0]]) -> b'[["float",-0.0]]' +draw((0.0,)) = 0.3464688222542801 +draw((-0.0,)) = 0.3251263802628821 +0.0 == -0.0: True | hash equal: True | draws equal: False +``` + +`_coordinate` normalises numpy scalars to Python scalars precisely so "a +coordinate read out of a column must not draw differently", but it does not +normalise float signed zero, and `canonical_json` passes floats straight to +`json.dumps`. A float coordinate arriving as `-0.0` on one run and `0.0` on +another — trivially produced by negation or a CSV literal — silently +re-randomises that row. (The int-vs-float distinction, by contrast, is +deliberate and documented: the tags are meant to differ.) I checked for a +documented signed-zero convention in the graph shard and found none. + +**Not fixed here, on purpose.** `randomness.py` is byte-identical to the +integration branch that owns it, and `"sha256-u53-v1"` is a *versioned* +normative algorithm: changing which uniform a coordinate draws under the same +version string is a version bump, not a drive-by fix, and would collide with +that branch on exactly the normative draw algorithm. The one-line change is +`value = value + 0.0` in `_coordinate`'s float branch. For the owning lane to +decide. + +### 7.4 The tracked root `out.md` + +The brief's `-o` path is `out.md`, which is also a **tracked** file on `main` +carrying a different lane's stale report ("F1 portable worker identity — Sol +gate round 1", last written by `71dbe2979`, 2026-09-04). Writing this report +there and committing it would silently delete that record from `main`; leaving +it uncommitted means it dies with the worktree. + +So: this report is written to `out.md` as the brief requires and left +**uncommitted**, and the identical content is **committed** at +`experiments/amendment-20-keyed-draws-receipts.md` — the home `CLAUDE.md` +sanctions for lane receipts. Someone should still decide whether a tracked +`out.md` belongs in the repo at all; it is a collision waiting to happen for +every lane that gets handed the same `-o` path. + +--- + +## 8. Things checked that turned out clean + +- **No other stale pin.** Repo-wide grep for the old identities `02db8f5c`, + `8878352d`, `f6984280`, `9e80ee3a` across `*.py`, `*.json`, `*.md`, `*.yaml` + and `*.yml` finds no hit in any source file, test or fixture — only in this + report and the lane journal, which quote them deliberately. +- **No existing node key moves.** Adding an enum *member* is not adding a + normative *field*: `_capabilities_projection` serialises + `capabilities.seed_source.value`, so every kernel that declares `EXECUTOR`, + `PARAM` or `NONE` projects exactly what it projected before. The `fit.qrf` + keys move because its implementation hash moved, not because `KEYED` exists — + the charter entry now says this explicitly rather than leaving the reader to + reconcile "no existing node key moves" with a re-pinned fixture in the same + diff. +- **C4's static check still holds.** It ASTs every file under `microcosm/graph/`; + `randomness.py` contains no `RandomState`, no `*.random.seed`, and no + `default_rng` call at all. `test_acceptance_c_seeds.py` is green. +- **`docs/graph-interface.lock` is plain `shasum -a 256` of the file bytes** — + `shasum -a 256 -c` reports both OK. Worth knowing: **nothing in the repo + verifies this lock.** No test, tool, or workflow step reads it; the only + references are prose. That is pre-existing, not a branch defect, but a lock + nothing checks is a lock that will drift. +- **CI grouping needs no bookkeeping.** All three new test files land in fast + `rest` and engine `us-am` automatically; `--verify` is green and none appears + under `[defaulted]`. +- **The `KernelContext` field order contract is untouched** — + `test_context_numerics_default_empty_and_carry_scopes` asserts the last two + dataclass fields are `["tolerances", "numerics"]`, which is why bringing + `artifacts` over would have broken it. It is green. From 393deda9664b7c7d0a92161f00411d2aaadefb46 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 11 Sep 2026 21:37:27 -0400 Subject: [PATCH 18/27] Record the authoritative suite figure in the lane receipts The review agents mutation-tested by editing source files and reverting them, so the headline suite figure was re-measured after all 42 had finished against a tree verified clean of MUTANT markers and of any .py differing from HEAD: 487 passed, exit 0. Also records the review's verdict tally and the exact command exit codes. Co-Authored-By: Claude Opus 5 --- .../amendment-20-keyed-draws-receipts.md | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/experiments/amendment-20-keyed-draws-receipts.md b/experiments/amendment-20-keyed-draws-receipts.md index 903deaa95..87e179079 100644 --- a/experiments/amendment-20-keyed-draws-receipts.md +++ b/experiments/amendment-20-keyed-draws-receipts.md @@ -262,10 +262,18 @@ red. Environment prepared with `uv sync --all-packages --locked --extra us --extra uk`; everything after used `uv run --no-sync`. +The review agents mutation-tested by editing source files and reverting them, +so a mid-review run could in principle have measured a mutant. The headline +suite figure was therefore re-measured after every agent had finished, against +a tree verified to carry no `MUTANT` marker and no `.py` differing from `HEAD` +(`packages` tree `112501f5c275182b6bd4ea10769e44bce514d4b9`); it agrees with +the earlier separate runs. + | Command | Exit | Result | |---|---|---| -| `pytest packages/microcosm-graph/tests` | **0** | **371 passed**, 1 warning, 159s (the warning is a pre-existing pydantic deprecation from `policyengine_uk`) | -| `pytest packages/microcosm-fit/tests` | **0** | ****116 passed**, 389s** | +| `pytest packages/microcosm-graph/tests packages/microcosm-fit/tests` | **0** | **487 passed**, 1 warning, 117s — the authoritative run, on the committed tree with nothing else touching it (the warning is a pre-existing pydantic deprecation from `policyengine_uk`) | +| `pytest packages/microcosm-graph/tests` | **0** | **371 passed**, 143s | +| `pytest packages/microcosm-fit/tests` | **0** | **116 passed**, 389s | | `pytest test_acceptance_h_parity.py test_graph_serialize.py::test_generated_parity_graphs_bind_real_kernels_and_direct_bytes test_graph_executor.py::test_fit_qrf_tolerance_source_hash_pin_is_current` | **0** | 6 passed — the three named H1 checks, on `arm64/darwin/py3.14` | | `pytest test_graph_parity_repin.py` (new) | **0** | 8 passed | | `pytest test_graph_parity_repin.py test_graph_parity_pins.py` | **0** | 19 passed | @@ -289,8 +297,11 @@ reformatted, but that is **pre-existing on `main`**, and CI runs only Six reviewers read the working tree along separate axes (charter claims, test rigor, the re-pin tool, brief compliance, implementation correctness, repo integration); every finding was then put to two adversarial verifiers whose -default was to refute. Four findings survived and were fixed; each fix was -mutation-tested. +default was to refute. 42 agents, no errors. Of eighteen findings, **3 survived +both verifiers, 7 split, 8 were refuted by both**. Adjudicating the splits +myself: four defects were fixed in code (each mutation-tested), two survivors +plus one split are the decisions in §7, and the rest were prose or duplicates +of the same root cause. 1. **The re-pin tool's central claim was false.** Its docstring said the reproduction loop "proves the assumption rather than asserting it" and that From aaf4f1b830c30b16fea0162fec2772f4b74cc934 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 03:55:29 -0400 Subject: [PATCH 19/27] Move the pool-tool spec_sha256 pin with amendment 20's seed protocol The country spec envelope folds in the seed protocol, so re-pinning the seed digests moved spec_sha256 from 35a02b6b to c72fb9e1 (visible in the regenerated docs/evidence/spec-engine/us-f0-coverage.json); the live-bundle test still pinned the old digest. Co-Authored-By: Claude Fable 5.1 --- packages/microcosm-build/tests/test_us_multispine_pool_tool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/microcosm-build/tests/test_us_multispine_pool_tool.py b/packages/microcosm-build/tests/test_us_multispine_pool_tool.py index 019e495d4..60671bc41 100644 --- a/packages/microcosm-build/tests/test_us_multispine_pool_tool.py +++ b/packages/microcosm-build/tests/test_us_multispine_pool_tool.py @@ -2504,7 +2504,7 @@ def capture_equality(expected: object, actual: object) -> None: "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "35a02b6b19c921faba1407d441e0b9d9623c496e2cd5b711be014def281a95c6", + "spec_sha256": "c72fb9e1bcb13e0288670b94261162de464e46d75c7c70c1742a34bf7a95bf87", }, } From cbf31efdabbcc5223f9c8559918c8ba721067856 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 03:55:29 -0400 Subject: [PATCH 20/27] Normalise signed zero in keyed draw coordinates -0.0 == 0.0 and they hash alike, so a float coordinate must draw alike under either spelling; _coordinate now folds -0.0 to 0.0 the way keys._canonical_tolerance_float already does for tolerances. Contract test and a sentence in the amendment 20 entry. Co-Authored-By: Claude Fable 5.1 --- docs/graph-acceptance.md | 4 +++- .../src/microcosm/graph/randomness.py | 7 +++++-- .../tests/test_graph_randomness.py | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/docs/graph-acceptance.md b/docs/graph-acceptance.md index ae0a51698..64b59bfde 100644 --- a/docs/graph-acceptance.md +++ b/docs/graph-acceptance.md @@ -381,7 +381,9 @@ Amendments so far (each re-locked): `microcosm.graph.randomness.keyed_uniform`: a keyed kernel's draws are a pure function of normative stream parameters — `("sha256-u53-v1", experiment_id, replicate, base_seed)` — and one stable coordinate per - draw, conventionally `(person_id, process, period, draw_index)`. It + draw, conventionally `(person_id, process, period, draw_index)`, with a + float coordinate's signed zero normalised to one identity, as the + capabilities projection already does for tolerances. It reads and advances no generator, so a row's draw stops depending on how many rows were drawn before it: the invariance to packing that C1 and C2 already gave a node's key and seed now reaches each individual draw, and diff --git a/packages/microcosm-graph/src/microcosm/graph/randomness.py b/packages/microcosm-graph/src/microcosm/graph/randomness.py index 5a5510bc2..f6cff2844 100644 --- a/packages/microcosm-graph/src/microcosm/graph/randomness.py +++ b/packages/microcosm-graph/src/microcosm/graph/randomness.py @@ -31,7 +31,9 @@ def _coordinate(value: object) -> list[object]: if isinstance(value, str): return ["str", value] if isinstance(value, float) and math.isfinite(value): - return ["float", value] + # Signed zero is one identity, as keys._canonical_tolerance_float treats + # it: -0.0 and 0.0 are equal and hash-equal, so they must draw alike. + return ["float", 0.0 if value == 0.0 else value] raise TypeError("Random coordinates must be non-null finite scalar identities.") @@ -39,7 +41,8 @@ def keyed_uniform(*, stream: tuple, keys: Sequence[tuple]) -> np.ndarray: """Return bytes-backed, read-only float64 draws keyed by stable coordinates. Row order, chunk boundaries, and unrelated inserted identities cannot affect - a draw. Integers, strings, booleans and floats have distinct canonical tags. + a draw. Integers, strings, booleans and floats have distinct canonical tags; + a float coordinate of negative zero is the same identity as positive zero. The experiment name is nonempty; replicate and base seed are non-negative Python integers (booleans refused). No numpy RNG state is read or mutated. """ diff --git a/packages/microcosm-graph/tests/test_graph_randomness.py b/packages/microcosm-graph/tests/test_graph_randomness.py index b3fa075d4..4601f8b3a 100644 --- a/packages/microcosm-graph/tests/test_graph_randomness.py +++ b/packages/microcosm-graph/tests/test_graph_randomness.py @@ -133,6 +133,21 @@ def test_numpy_scalars_are_the_python_scalars_they_hold() -> None: ) +def test_negative_zero_is_the_same_float_coordinate_as_positive_zero() -> None: + """``-0.0 == 0.0`` and they hash alike, so they must draw alike. + + A negation or a CSV literal can hand a column ``-0.0`` where another run + holds ``0.0``; without normalisation ``canonical_json`` would spell them + differently and silently re-randomise that row. + """ + positive = keyed_uniform(stream=STREAM, keys=[(7, 0.0)])[0] + assert keyed_uniform(stream=STREAM, keys=[(7, -0.0)])[0] == positive + assert keyed_uniform(stream=STREAM, keys=[(7, np.float64(-0.0))])[0] == positive + # The normalisation is to positive zero: the documented formula over + # ``0.0`` is the draw both spellings produce. + assert positive == _spec_uniform(STREAM, (7, 0.0)) + + def test_draws_are_bytes_backed_and_read_only() -> None: values = keyed_uniform(stream=STREAM, keys=[(1,), (2,)]) assert not values.flags.writeable From 4dfff4639f9614e6123528aaa4d8f2b5b6b10c83 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 03:55:29 -0400 Subject: [PATCH 21/27] Skip the fit.qrf repin tests on a platform that carries no pin graph_parity_repin refuses an unpinned platform before the step these three tests exercise, so on such a platform they would fail on the wrong refusal rather than observe their subject. Co-Authored-By: Claude Fable 5.1 --- .../tests/test_graph_parity_repin.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/microcosm-graph/tests/test_graph_parity_repin.py b/packages/microcosm-graph/tests/test_graph_parity_repin.py index fb94785f5..433fc91b4 100644 --- a/packages/microcosm-graph/tests/test_graph_parity_repin.py +++ b/packages/microcosm-graph/tests/test_graph_parity_repin.py @@ -43,6 +43,22 @@ def _pins(case: Path) -> dict: return json.loads((case / "pins.json").read_text(encoding="utf-8")) +def _skip_unless_this_platform_is_pinned(case: Path) -> None: + """The tool refuses a platform with no pin before the step under test. + + On such a platform ``repin`` exits with "carries no pin to re-record", + which is neither the refusal these tests assert nor the no-op they + expect; the test cannot observe its subject there, so it skips. + """ + pins = _pins(case) + pinned = {pins["platform"], *pins.get("platforms", {})} + if platform_fingerprint() not in pinned: + pytest.skip( + f"{platform_fingerprint()} carries no {case.name} pin; the tool " + "refuses before the behaviour under test" + ) + + def _write(case: Path, pins: dict) -> None: (case / "pins.json").write_text( json.dumps(pins, ensure_ascii=False, sort_keys=True, separators=(",", ":")) @@ -76,6 +92,7 @@ def test_a_recorded_dependency_version_this_machine_lacks_refuses( then write a key that platform never computes, so the tool must refuse. """ case = case_copy("fit.qrf") + _skip_unless_this_platform_is_pinned(case) pins = _pins(case) pins["dependencies"]["numpy"] = "0.0.0-not-installed-here" _write(case, pins) @@ -114,6 +131,7 @@ def test_a_foreign_pin_the_recorded_hash_cannot_reproduce_refuses( re-deriving it here would launder that into a fresh-looking key. """ case = case_copy("fit.qrf") + _skip_unless_this_platform_is_pinned(case) pins = _pins(case) foreign = next(p for p in pins["platforms"] if p != platform_fingerprint()) pins["platforms"][foreign]["node_key"] = "0" * 64 @@ -199,6 +217,7 @@ def test_a_repin_of_an_unchanged_kernel_rewrites_the_pins_byte_for_byte( ) -> None: """Idempotence: the tool is a no-op when nothing about the kernel moved.""" case = case_copy("fit.qrf") + _skip_unless_this_platform_is_pinned(case) before = (case / "pins.json").read_bytes() repin("fit.qrf") assert (case / "pins.json").read_bytes() == before From dcaf92923160eb8196f9d272a8d9cc5bc357133a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 03:55:29 -0400 Subject: [PATCH 22/27] Historicize the amendment 20 lane journals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dated notes over the State section and receipts §5, §7.1–7.3 and §8: the seed re-pin, the numbering, the signed-zero fix and the lock test have all landed since the lane wrote them. Co-Authored-By: Claude Fable 5.1 --- PROGRESS-amendment-20-keyed-draws.md | 6 ++++ .../amendment-20-keyed-draws-receipts.md | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/PROGRESS-amendment-20-keyed-draws.md b/PROGRESS-amendment-20-keyed-draws.md index 9386b15a3..a68f1c915 100644 --- a/PROGRESS-amendment-20-keyed-draws.md +++ b/PROGRESS-amendment-20-keyed-draws.md @@ -17,6 +17,12 @@ decisions with their evidence — are in Implementation, tests, pins, charter and changelog are landed and committed. `packages/microcosm-graph/tests` and `packages/microcosm-fit/tests` are green. +> Historicized 2026-09-12: the paragraph below described the branch as the lane +> left it. The seed digests were re-pinned when the branch was stacked on +> amendment 19 and pushed as PR #912, so nothing here is red any more; the +> `-0.0` question in item 7.3 of the receipts was settled by normalising +> signed zero in `_coordinate`. + **The branch cannot go green in CI as it stands**, and that is deliberate: the `qrf.py` edit moves the spec-engine seed digests, leaving seven tests red in a lane that runs on every PR. The lane brief said report the drift, not re-pin diff --git a/experiments/amendment-20-keyed-draws-receipts.md b/experiments/amendment-20-keyed-draws-receipts.md index 87e179079..a26400575 100644 --- a/experiments/amendment-20-keyed-draws-receipts.md +++ b/experiments/amendment-20-keyed-draws-receipts.md @@ -12,6 +12,11 @@ platform, so the local pin is *produced* rather than derived. **Three things need your decision, in §7.** One of them is merge-blocking: the branch cannot go green in CI as it stands, by design of the brief. +> Historicized 2026-09-12: the decisions in §7 were taken when the branch +> was stacked on amendment 19 and pushed as PR #912 — see the notes under +> §5, §7.1, §7.2, §7.3 and §8. The report below is the lane's record, not +> the branch's state. + This report is also committed at `experiments/amendment-20-keyed-draws-receipts.md`, because the root `out.md` is a tracked file holding a different lane's report — see §7.4. @@ -259,6 +264,12 @@ red. ## 5. Every command, with its direct exit code +> Historicized 2026-09-12: the two red rows below (`spec_engine_coverage +> --check`, the two coverage test files) were cleared by the re-pin recorded +> under §7.1; the lock line predates the merge with amendment 19, after which +> `docs/graph-interface.lock` was re-recorded over the merged files. + + Environment prepared with `uv sync --all-packages --locked --extra us --extra uk`; everything after used `uv run --no-sync`. @@ -351,6 +362,12 @@ untouched. ### 7.1 The spec-engine seed digests are stale — and this is merge-blocking +> Historicized 2026-09-12: applied in PR #912 — `seed_protocol` and +> `seed_map` re-pinned in `inventory_coverage.py`, the evidence file +> regenerated, and the pool-tool test's `spec_sha256` pin moved with it; +> `spec_engine_coverage.py --check` exits 0 on the pushed head. + + | | `origin/main` | this branch | |---|---|---| | `tools/spec_engine_coverage.py --check` | exit **0** (42156/42156 fields, 41/41 checks) | exit **1** | @@ -423,6 +440,10 @@ digests attest the tree that actually merges. ### 7.2 Amendment numbering — and it renders as 19 regardless +> Historicized 2026-09-12: amendment 19 (typed artifacts) merged first as +> PR #911, so this entry is 20 by main's own arithmetic and renders as 20. + + `main`'s list ends at **18**, so by its own arithmetic the next free number is **19**. The entry is numbered **20** because the brief assigns 20 and gives 19 to the artifacts lane. Two unmerged commits on the @@ -445,6 +466,12 @@ one-token edits; which is right depends on what the artifacts lane lands as. ### 7.3 `keyed_uniform` draws differently for `-0.0` and `0.0` +> Historicized 2026-09-12: fixed in PR #912 — `_coordinate` normalises a +> float zero to `0.0` (as `keys._canonical_tolerance_float` already did for +> tolerances), with a contract test and a sentence in the amendment entry. +> No version bump: the algorithm string had not shipped on `main`. + + Verified directly: ``` @@ -491,6 +518,10 @@ every lane that gets handed the same `-o` path. ## 8. Things checked that turned out clean +> Historicized 2026-09-12: the interface lock is now enforced in the suite +> by `packages/microcosm-graph/tests/test_graph_interface_lock.py` (PR #910). + + - **No other stale pin.** Repo-wide grep for the old identities `02db8f5c`, `8878352d`, `f6984280`, `9e80ee3a` across `*.py`, `*.json`, `*.md`, `*.yaml` and `*.yml` finds no hit in any source file, test or fixture — only in this From 23ba24770e05c492a113f9f4f87f88574e21c40a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 06:50:30 -0400 Subject: [PATCH 23/27] Re-pin the AM, BE and UK country-bundle spec digests Every country's spec envelope folds in the seed protocol, so amendment 20's seed re-pin moved all three bundle digests, not only the US one; CI on dcaf92923 failed exactly these three parametrizations. Values computed with load_bundle(country).spec_sha256 on the merged tree. Co-Authored-By: Claude Fable 5.1 --- .../tests/test_spec_engine_country_bundles.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/microcosm-build/tests/test_spec_engine_country_bundles.py b/packages/microcosm-build/tests/test_spec_engine_country_bundles.py index a826e90db..dbf9d4ccb 100644 --- a/packages/microcosm-build/tests/test_spec_engine_country_bundles.py +++ b/packages/microcosm-build/tests/test_spec_engine_country_bundles.py @@ -25,7 +25,7 @@ "spine", "vintages", } -AM_SPEC_SHA256 = "d983a5e6bb2f91f9abd44669fe5b1c795d1a7597c4b6acbe9282eaa956bb326d" +AM_SPEC_SHA256 = "d4c432394025686827100dcf334e03f557500e84da0822a35de3b2bc3e3da95e" @pytest.mark.parametrize( @@ -45,7 +45,7 @@ ), ( "be", - "2a7d83483c90abe31108f8c3a77c053b4e6f2fd50d3cf3290a85e63ea3b9fbac", + "fff6f08e34ba77b9361028972c1ac077fc23c00802e491afab8803a3801675c8", { "household.household_id", "person.person_id", @@ -55,7 +55,7 @@ ), ( "uk", - "c396ee51ab029ed780ec25b69d34b128f53b08c673dc87a7f600f74264e755e3", + "3c9aa58d39d8f04dcd98bd74f957764e64e6e4e1c47e84cd8dcbbc52bad66d03", { "benunit.benunit_id", "household.household_id", From 9c77d7586f12caa68a3d124d168877e33c9bb482 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 07:05:02 -0400 Subject: [PATCH 24/27] Re-pin the minimal-spec loader golden vector with amendment 20's seed protocol The golden folds the legacy-v1 seed protocol wire, which attests microcosm.fit.qrf's bytes, so it moved with this amendment exactly as the US, AM, BE and UK bundle digests did. Value recomputed from the test's own construction (load_bundle over _rich_minimal). Co-Authored-By: Claude Fable 5.1 --- packages/microcosm-build/tests/test_spec_engine_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/microcosm-build/tests/test_spec_engine_loader.py b/packages/microcosm-build/tests/test_spec_engine_loader.py index 0b583de0a..b4a717f13 100644 --- a/packages/microcosm-build/tests/test_spec_engine_loader.py +++ b/packages/microcosm-build/tests/test_spec_engine_loader.py @@ -236,7 +236,7 @@ def test_semantic_hash_has_golden_vector_and_surface_separation(tmp_path) -> Non # Pin the domain separator, normalization rules, schema-set receipt, and # exact normative projection as one reviewable golden vector. assert first.spec_sha256 == ( - "57026e5896dd52a382746fe7641f615dcc4c55965a1f50bea5971a8f92710d71" + "0a4c9b0401c014d172c6f42f5c02b7da05bf89c1857e840b506716edc688fac2" ) second_root = _rich_minimal(tmp_path / "xy", note="second", store="local:b") From 4e8a1048606b6943acad5b556a2915b846c7a389 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 07:05:02 -0400 Subject: [PATCH 25/27] Guard the repin ordering test on a platform with no fit.qrf pin Without the guard, pytest.raises(SystemExit) passed vacuously there on the 'carries no pin' refusal and the ordering the test pins was never observed. Co-Authored-By: Claude Fable 5.1 --- packages/microcosm-graph/tests/test_graph_parity_repin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/microcosm-graph/tests/test_graph_parity_repin.py b/packages/microcosm-graph/tests/test_graph_parity_repin.py index 433fc91b4..57745f125 100644 --- a/packages/microcosm-graph/tests/test_graph_parity_repin.py +++ b/packages/microcosm-graph/tests/test_graph_parity_repin.py @@ -109,6 +109,7 @@ def test_the_dependency_check_runs_before_any_key_is_derived( ) -> None: """Refusing after deriving would already have trusted the bad environment.""" case = case_copy("fit.qrf") + _skip_unless_this_platform_is_pinned(case) pins = _pins(case) pins["dependencies"]["pandas"] = "0.0.0-not-installed-here" _write(case, pins) From 6391b8092f4b40fe71e15f9a3463977a838f7838 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 08:04:42 -0400 Subject: [PATCH 26/27] Reject lossy keyed-draw inputs and inconsistent parity authoring pins --- .../keyed-draw-input-validation.fixed.md | 3 ++ docs/evidence/spec-engine/us-f0-coverage.json | 16 +++++----- .../amendment-20-input-validation-20260912.md | 32 +++++++++++++++++++ .../build/spec_engine/inventory_coverage.py | 4 +-- .../tests/test_spec_engine_country_bundles.py | 6 ++-- .../tests/test_spec_engine_loader.py | 2 +- .../tests/test_us_multispine_pool_tool.py | 2 +- .../microcosm-fit/src/microcosm/fit/qrf.py | 10 ++++-- .../microcosm-fit/tests/test_qrf_stateless.py | 11 +++++++ .../src/microcosm/graph/randomness.py | 6 ++++ .../fixtures/parity/kernels/fit.qrf/pins.json | 2 +- .../tests/test_graph_parity_repin.py | 18 +++++++++++ .../tests/test_graph_randomness.py | 9 ++++++ tools/graph_parity_repin.py | 7 ++++ 14 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 changelog.d/keyed-draw-input-validation.fixed.md create mode 100644 experiments/amendment-20-input-validation-20260912.md diff --git a/changelog.d/keyed-draw-input-validation.fixed.md b/changelog.d/keyed-draw-input-validation.fixed.md new file mode 100644 index 000000000..fc473f4f2 --- /dev/null +++ b/changelog.d/keyed-draw-input-validation.fixed.md @@ -0,0 +1,3 @@ +Reject temporal random coordinates before NumPy scalar conversion and reject +non-real QRF uniform arrays before float conversion. Refuse inconsistent +authoring keys when re-pinning graph parity fixtures. diff --git a/docs/evidence/spec-engine/us-f0-coverage.json b/docs/evidence/spec-engine/us-f0-coverage.json index 5120a883a..a759198bc 100644 --- a/docs/evidence/spec-engine/us-f0-coverage.json +++ b/docs/evidence/spec-engine/us-f0-coverage.json @@ -1656,13 +1656,13 @@ "compiler_ir.node_slices" ], "expected": { - "map_sha256": "373786d5ba5e115c3317c42662b3998cdef8061441cf0ba48c36828bd7d55e69", - "protocol_sha256": "d8f595a05dd71d7eccce5976d640baf97541e3ca159fd6179e7b1ab1a1286d8e" + "map_sha256": "20058e544f6034cee2e76d3b864cf86b6230c8dce042931dfe9247d8ac1329c4", + "protocol_sha256": "553d5e0bd5afa93146dfbf8dca684926ee773dbfc89a559434528af5b4eb1d97" }, "failures": [], "observed": { - "map_sha256": "373786d5ba5e115c3317c42662b3998cdef8061441cf0ba48c36828bd7d55e69", - "protocol_sha256": "d8f595a05dd71d7eccce5976d640baf97541e3ca159fd6179e7b1ab1a1286d8e" + "map_sha256": "20058e544f6034cee2e76d3b864cf86b6230c8dce042931dfe9247d8ac1329c4", + "protocol_sha256": "553d5e0bd5afa93146dfbf8dca684926ee773dbfc89a559434528af5b4eb1d97" }, "status": "covered" }, @@ -1677,7 +1677,7 @@ "compiler_ir.seed_stream_map" ], "expected": { - "implementation_sha256": "d8f595a05dd71d7eccce5976d640baf97541e3ca159fd6179e7b1ab1a1286d8e", + "implementation_sha256": "553d5e0bd5afa93146dfbf8dca684926ee773dbfc89a559434528af5b4eb1d97", "protocol": "legacy-v1", "streams": [ "build_model", @@ -1698,7 +1698,7 @@ }, "failures": [], "observed": { - "implementation_sha256": "d8f595a05dd71d7eccce5976d640baf97541e3ca159fd6179e7b1ab1a1286d8e", + "implementation_sha256": "553d5e0bd5afa93146dfbf8dca684926ee773dbfc89a559434528af5b4eb1d97", "protocol": "legacy-v1", "streams": [ "build_model", @@ -2599,7 +2599,7 @@ "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "c72fb9e1bcb13e0288670b94261162de464e46d75c7c70c1742a34bf7a95bf87" + "spec_sha256": "1eeca53aa80da949a292fbd8cb0afefde95c68888ed35f3f477f3c962e6bc644" } }, "report_schema_version": 3, @@ -2609,7 +2609,7 @@ "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "c72fb9e1bcb13e0288670b94261162de464e46d75c7c70c1742a34bf7a95bf87" + "spec_sha256": "1eeca53aa80da949a292fbd8cb0afefde95c68888ed35f3f477f3c962e6bc644" }, "status": "pass" } diff --git a/experiments/amendment-20-input-validation-20260912.md b/experiments/amendment-20-input-validation-20260912.md new file mode 100644 index 000000000..8f5e3a731 --- /dev/null +++ b/experiments/amendment-20-input-validation-20260912.md @@ -0,0 +1,32 @@ +# Amendment 20 input validation review + +On 12 September, an independent executed Astra review of PR912 head +`4e8a1048606b6943acad5b556a2915b846c7a389` found two input-validation defects +and one parity-tool inconsistency. This follow-up repairs all three. + +- NumPy temporal scalars are refused before `.item()` can erase their type + and time unit. Supported boolean, integer, real floating and Unicode scalar + coordinates retain their existing tags and draws. +- QRF supplied uniforms must have real numeric dtypes before conversion to + float64. Complex arrays, including those with non-finite imaginary parts, + cannot silently lose information before the range/finiteness checks. +- The parity re-pin tool refuses a top-level authoring key that disagrees + with the corresponding platform entry before deriving or writing any key. + +The 17 new parameter cases gave 13 failures and four passes against the old +implementation, then all passed after repair. The complete graph and fit +suites passed 624 tests with no failures/errors/skips in 162.91 seconds. +The six spec/identity targets passed 62 tests with no failures/errors/skips +in 235.17 seconds. Ruff and changed-file formatting passed. + +QRF's source bytes contribute to its implementation identity and the seed +protocol. Its three platform parity keys were re-recorded with the guarded +tool; every `direct.csv` remains unchanged. The US/AM/BE/UK spec envelopes, +minimal-loader golden, protocol and seed-map pins were recomputed from the +real loader/compiler. Coverage regeneration passed all 42,156 configuration +fields and 41 inventory checks. No unrelated inventory pins, frozen graph +declarations, interface lock, or acceptance tests changed. + +These are software-contract checks. They do not certify a dataset, native +replay, calibration, release or publication. PR912 remains subject to the +corrected-head independent review and CI merge gate. diff --git a/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py b/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py index 42eee01d5..ca3c0357b 100644 --- a/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py +++ b/packages/microcosm-build/src/microcosm/build/spec_engine/inventory_coverage.py @@ -359,8 +359,8 @@ "late_schedule": "e59c019d3d454eac99ac0ac209b6c5b6faaf9bdfcaeee18c36a25be19bf7da2f", "ownership": "5f64f0aac49e2313177564f71876bffc8c81b3ded4df701e70930e60e9c98356", "primary_tuples": "987b501c695e31f45521c4a178528f75ab3df22c09bc407b182213b2de99ee57", - "seed_map": "373786d5ba5e115c3317c42662b3998cdef8061441cf0ba48c36828bd7d55e69", - "seed_protocol": "d8f595a05dd71d7eccce5976d640baf97541e3ca159fd6179e7b1ab1a1286d8e", + "seed_map": "20058e544f6034cee2e76d3b864cf86b6230c8dce042931dfe9247d8ac1329c4", + "seed_protocol": "553d5e0bd5afa93146dfbf8dca684926ee773dbfc89a559434528af5b4eb1d97", "source_manifest": "cd5ba8924d64da5425ee14cca82a774e3f4b2bb5aabe06df291cc3cc457287a9", "take_up": "fa186daea0f8dd641cc470e41d1a2953f887d45282ec990201298f47bedf8d4d", "tail": "ac92829c88a1a4fb6460d61190918d5d99c6c377fc8dd8f62f02b332d09bf59c", diff --git a/packages/microcosm-build/tests/test_spec_engine_country_bundles.py b/packages/microcosm-build/tests/test_spec_engine_country_bundles.py index dbf9d4ccb..fecadaba5 100644 --- a/packages/microcosm-build/tests/test_spec_engine_country_bundles.py +++ b/packages/microcosm-build/tests/test_spec_engine_country_bundles.py @@ -25,7 +25,7 @@ "spine", "vintages", } -AM_SPEC_SHA256 = "d4c432394025686827100dcf334e03f557500e84da0822a35de3b2bc3e3da95e" +AM_SPEC_SHA256 = "c4163f9ab3577cd5b6d509c6fe091d0f95ae7892a4746562a57a62a3f8b3be8e" @pytest.mark.parametrize( @@ -45,7 +45,7 @@ ), ( "be", - "fff6f08e34ba77b9361028972c1ac077fc23c00802e491afab8803a3801675c8", + "fe25bbc48c785801bc7f518380eff9f528a3ceab4fa8501e1b527362c9c579b6", { "household.household_id", "person.person_id", @@ -55,7 +55,7 @@ ), ( "uk", - "3c9aa58d39d8f04dcd98bd74f957764e64e6e4e1c47e84cd8dcbbc52bad66d03", + "3ea1b53abb0ceef57b6ddef6d49251579e633bfe7d5ae7ec5a2152c0858445af", { "benunit.benunit_id", "household.household_id", diff --git a/packages/microcosm-build/tests/test_spec_engine_loader.py b/packages/microcosm-build/tests/test_spec_engine_loader.py index b4a717f13..240228a0b 100644 --- a/packages/microcosm-build/tests/test_spec_engine_loader.py +++ b/packages/microcosm-build/tests/test_spec_engine_loader.py @@ -236,7 +236,7 @@ def test_semantic_hash_has_golden_vector_and_surface_separation(tmp_path) -> Non # Pin the domain separator, normalization rules, schema-set receipt, and # exact normative projection as one reviewable golden vector. assert first.spec_sha256 == ( - "0a4c9b0401c014d172c6f42f5c02b7da05bf89c1857e840b506716edc688fac2" + "b465989064184f03ecc296c13bec8ffc5a70d5dd4cf30d42487b399633d32a62" ) second_root = _rich_minimal(tmp_path / "xy", note="second", store="local:b") diff --git a/packages/microcosm-build/tests/test_us_multispine_pool_tool.py b/packages/microcosm-build/tests/test_us_multispine_pool_tool.py index 60671bc41..683d496d4 100644 --- a/packages/microcosm-build/tests/test_us_multispine_pool_tool.py +++ b/packages/microcosm-build/tests/test_us_multispine_pool_tool.py @@ -2504,7 +2504,7 @@ def capture_equality(expected: object, actual: object) -> None: "country": "us", "schema_id": "country_spec", "schema_version": 1, - "spec_sha256": "c72fb9e1bcb13e0288670b94261162de464e46d75c7c70c1742a34bf7a95bf87", + "spec_sha256": "1eeca53aa80da949a292fbd8cb0afefde95c68888ed35f3f477f3c962e6bc644", }, } diff --git a/packages/microcosm-fit/src/microcosm/fit/qrf.py b/packages/microcosm-fit/src/microcosm/fit/qrf.py index e8bb2c956..13735f5c5 100644 --- a/packages/microcosm-fit/src/microcosm/fit/qrf.py +++ b/packages/microcosm-fit/src/microcosm/fit/qrf.py @@ -1604,7 +1604,8 @@ def predict_from_uniforms( """Draw using caller-supplied per-row uniforms, without advancing RNG. Each mapping must contain exactly the fitted targets, with one finite - one-dimensional array in ``[0, 1)`` per target, aligned to input rows. + real numeric one-dimensional array in ``[0, 1)`` per target, aligned + to input rows. Complex, object and string arrays are refused. Supply both arrays even for single-sign or all-zero targets. Later targets condition on earlier draws, just as in :meth:`predict`. @@ -1622,7 +1623,12 @@ def predict_from_uniforms( raise ValueError(f"{name} must contain exactly the fitted targets.") arrays[name] = {} for target in self.targets: - values = np.asarray(supplied[target], dtype=np.float64) + raw = np.asarray(supplied[target]) + if raw.dtype.kind not in "iuf": + raise ValueError( + f"{name}[{target!r}] uniforms must be real numeric arrays." + ) + values = np.asarray(raw, dtype=np.float64) if values.shape != (len(features),): raise ValueError( f"{name}[{target!r}] must have shape ({len(features)},)." diff --git a/packages/microcosm-fit/tests/test_qrf_stateless.py b/packages/microcosm-fit/tests/test_qrf_stateless.py index 058ac7ce2..e697e394a 100644 --- a/packages/microcosm-fit/tests/test_qrf_stateless.py +++ b/packages/microcosm-fit/tests/test_qrf_stateless.py @@ -98,6 +98,17 @@ def test_uniform_target_names_must_match(model): model.predict_from_uniforms(pd.DataFrame({"x": [1.0, 2.0]}), **draws) +@pytest.mark.parametrize("field", ["quantiles", "sign_uniforms"]) +@pytest.mark.parametrize("imaginary", [0.0, 0.5, np.nan, np.inf]) +def test_complex_uniforms_are_refused_without_lossy_conversion(model, field, imaginary): + draws = uniforms(model, 2) + draws[field][model.targets[-1]] = np.array( + [complex(0.25, imaginary), complex(0.75, imaginary)] + ) + with pytest.raises(ValueError, match="real numeric"): + model.predict_from_uniforms(pd.DataFrame({"x": [1.0, 2.0]}), **draws) + + def test_empty_recipient_batch(model): actual = model.predict_from_uniforms( pd.DataFrame({"x": pd.Series(dtype=float)}), **uniforms(model, 0) diff --git a/packages/microcosm-graph/src/microcosm/graph/randomness.py b/packages/microcosm-graph/src/microcosm/graph/randomness.py index f6cff2844..453823c6b 100644 --- a/packages/microcosm-graph/src/microcosm/graph/randomness.py +++ b/packages/microcosm-graph/src/microcosm/graph/randomness.py @@ -23,6 +23,12 @@ def _coordinate(value: object) -> list[object]: if isinstance(value, np.generic): + # Temporal .item() can return a bare integer for fine-grained units, + # erasing both the coordinate type and its unit before tagging. + if value.dtype.kind not in "biufU": + raise TypeError( + "Random coordinates must be non-null finite scalar identities." + ) value = value.item() if isinstance(value, bool): return ["bool", value] diff --git a/packages/microcosm-graph/tests/fixtures/parity/kernels/fit.qrf/pins.json b/packages/microcosm-graph/tests/fixtures/parity/kernels/fit.qrf/pins.json index 3b61d6fc1..fcb25cf92 100644 --- a/packages/microcosm-graph/tests/fixtures/parity/kernels/fit.qrf/pins.json +++ b/packages/microcosm-graph/tests/fixtures/parity/kernels/fit.qrf/pins.json @@ -1 +1 @@ -{"dependencies":{"numpy":"2.4.6","pandas":"3.0.3","quantile-forest":"1.4.2","scikit-learn":"1.8.0"},"implementation_hash":"d1f8b1929e6452aa507b0d9c64ba42a59851dc31c71021303c25f060e34c075b","kernel":"fit.qrf@1","node":"fit_qrf","node_key":"35af6a452bd32ca39d313d78255236df01877d684c2d847b1bd5a6016a68e237","numeric":"platform_bitwise","platform":"arm64/darwin/py3.14","platforms":{"arm64/darwin/py3.14":{"direct":"direct.csv","node_key":"35af6a452bd32ca39d313d78255236df01877d684c2d847b1bd5a6016a68e237"},"x86_64/linux/py3.13":{"direct":"platforms/x86_64-linux-py3_13/direct.csv","node_key":"6c43edcb3edf2bdef20d915b1be16503d37583c678aced78f1442aca1139e1ae"},"x86_64/linux/py3.14":{"direct":"platforms/x86_64-linux-py3_14/direct.csv","node_key":"ecb6b20c02b0bc442c4baf3fd7def6d6aec06e6b9567910294f945d66c43bbf8"}},"seed":947} +{"dependencies":{"numpy":"2.4.6","pandas":"3.0.3","quantile-forest":"1.4.2","scikit-learn":"1.8.0"},"implementation_hash":"57865faef2ded5dcf0bd97c3967b13de6a17e700e10e164c730b70ad2d8f2734","kernel":"fit.qrf@1","node":"fit_qrf","node_key":"97b471011e71a95d2f532376a27ac01f2cf433e62533ad41667b6173e91437be","numeric":"platform_bitwise","platform":"arm64/darwin/py3.14","platforms":{"arm64/darwin/py3.14":{"direct":"direct.csv","node_key":"97b471011e71a95d2f532376a27ac01f2cf433e62533ad41667b6173e91437be"},"x86_64/linux/py3.13":{"direct":"platforms/x86_64-linux-py3_13/direct.csv","node_key":"994be491cc9807d7c73ad4486e6d23a44860a474c0b838d49bffa962c576e27a"},"x86_64/linux/py3.14":{"direct":"platforms/x86_64-linux-py3_14/direct.csv","node_key":"b39682201a0ceabfcf2c306e8cc1b463cfc816241b87942e4ee4580e801aa80e"}},"seed":947} diff --git a/packages/microcosm-graph/tests/test_graph_parity_repin.py b/packages/microcosm-graph/tests/test_graph_parity_repin.py index 57745f125..77e25d563 100644 --- a/packages/microcosm-graph/tests/test_graph_parity_repin.py +++ b/packages/microcosm-graph/tests/test_graph_parity_repin.py @@ -143,6 +143,24 @@ def test_a_foreign_pin_the_recorded_hash_cannot_reproduce_refuses( assert foreign in str(raised.value) +def test_inconsistent_authoring_key_is_refused_before_derivation( + case_copy: Callable[[str], Path], monkeypatch: pytest.MonkeyPatch +) -> None: + case = case_copy("fit.qrf") + pins = _pins(case) + pins["node_key"] = "0" * 64 + _write(case, pins) + before = (case / "pins.json").read_bytes() + + def forbidden(*args: object, **kwargs: object) -> str: + raise AssertionError("derived a key from inconsistent authoring pins") + + monkeypatch.setattr(repin_module, "derived_node_key", forbidden) + with pytest.raises(SystemExit, match="authoring.*inconsistent"): + repin("fit.qrf") + assert (case / "pins.json").read_bytes() == before + + def test_the_direct_bytes_compared_are_this_platform_s_own_pin( case_copy: Callable[[str], Path], ) -> None: diff --git a/packages/microcosm-graph/tests/test_graph_randomness.py b/packages/microcosm-graph/tests/test_graph_randomness.py index 4601f8b3a..a108bfa05 100644 --- a/packages/microcosm-graph/tests/test_graph_randomness.py +++ b/packages/microcosm-graph/tests/test_graph_randomness.py @@ -148,6 +148,15 @@ def test_negative_zero_is_the_same_float_coordinate_as_positive_zero() -> None: assert positive == _spec_uniform(STREAM, (7, 0.0)) +@pytest.mark.parametrize("scalar", [np.datetime64, np.timedelta64]) +@pytest.mark.parametrize("unit", ["D", "s", "ns", "ps"]) +def test_temporal_coordinates_refuse_before_losing_type_or_units(scalar, unit): + # Fine temporal units become bare integers under .item(); they cannot be + # admitted as the same random identity as an integer or another unit. + with pytest.raises(TypeError, match="scalar identities"): + keyed_uniform(stream=STREAM, keys=[(scalar(1, unit),)]) + + def test_draws_are_bytes_backed_and_read_only() -> None: values = keyed_uniform(stream=STREAM, keys=[(1,), (2,)]) assert not values.flags.writeable diff --git a/tools/graph_parity_repin.py b/tools/graph_parity_repin.py index 630cee99b..e5b9b9707 100644 --- a/tools/graph_parity_repin.py +++ b/tools/graph_parity_repin.py @@ -212,6 +212,13 @@ def repin(name: str) -> dict[str, str]: ) fingerprint = platform_fingerprint() platforms = dict(pins.get("platforms", {})) + if ( + pins["platform"] in platforms + and platforms[pins["platform"]]["node_key"] != pins["node_key"] + ): + raise SystemExit( + f"{name}: authoring platform and top-level key are inconsistent" + ) platforms.setdefault( pins["platform"], {"node_key": pins["node_key"], "direct": "direct.csv"} ) From 5e33d69715b0d6e92bbf2b2df869c7fa43195799 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 12 Sep 2026 08:18:22 -0400 Subject: [PATCH 27/27] Select non-authoring foreign pins in parity refusal tests --- experiments/amendment-20-input-validation-20260912.md | 9 +++++++++ .../microcosm-graph/tests/test_graph_parity_repin.py | 10 ++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/experiments/amendment-20-input-validation-20260912.md b/experiments/amendment-20-input-validation-20260912.md index 8f5e3a731..83ef0d66d 100644 --- a/experiments/amendment-20-input-validation-20260912.md +++ b/experiments/amendment-20-input-validation-20260912.md @@ -30,3 +30,12 @@ declarations, interface lock, or acceptance tests changed. These are software-contract checks. They do not certify a dataset, native replay, calibration, release or publication. PR912 remains subject to the corrected-head independent review and CI merge gate. + +Fable's next review of `6391b809` found that two existing parity-tool tests +selected the authoring platform as their foreign entry on Linux. Their +intentional corruption then reached the new authoring-consistency refusal +before the foreign-key reproduction check those tests meant to exercise. +Both now choose an entry that is neither local nor the authoring platform. +All nine parity-tool tests and Ruff pass locally on macOS; the exact-head +Linux CI runs must establish the corresponding Linux result. This correction +changes only test setup and this record, with no implementation or pin change. diff --git a/packages/microcosm-graph/tests/test_graph_parity_repin.py b/packages/microcosm-graph/tests/test_graph_parity_repin.py index 77e25d563..54484d2b5 100644 --- a/packages/microcosm-graph/tests/test_graph_parity_repin.py +++ b/packages/microcosm-graph/tests/test_graph_parity_repin.py @@ -134,7 +134,13 @@ def test_a_foreign_pin_the_recorded_hash_cannot_reproduce_refuses( case = case_copy("fit.qrf") _skip_unless_this_platform_is_pinned(case) pins = _pins(case) - foreign = next(p for p in pins["platforms"] if p != platform_fingerprint()) + # Keep the authoring entry consistent with the top-level key so this + # reaches the foreign-key reproduction check on every pinned platform. + foreign = next( + p + for p in pins["platforms"] + if p not in {platform_fingerprint(), pins["platform"]} + ) pins["platforms"][foreign]["node_key"] = "0" * 64 _write(case, pins) @@ -185,7 +191,7 @@ def test_the_direct_bytes_compared_are_this_platform_s_own_pin( (case / "direct.csv").write_bytes(b"not,the,direct,call\n") # Force a refusal strictly after the byte comparison, so the message says # which check the run reached. - foreign = next(p for p in pins["platforms"] if p != local) + foreign = next(p for p in pins["platforms"] if p not in {local, pins["platform"]}) pins["platforms"][foreign]["node_key"] = "0" * 64 _write(case, pins)