Skip to content

Verify native sources once per run, not once per access and per node - #935

Closed
MaxGhenis wants to merge 46 commits into
microcosm-us-launch-integration-20260909from
native-verify-once
Closed

MaxGhenis wants to merge 46 commits into
microcosm-us-launch-integration-20260909from
native-verify-once

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Native source authentication and identity verification ran once per accessor
use and once per executed node
. On the 19-node financial graph at
Fraction(1, 1000) — 3,168 expanded households, 5,362 s wall — node execution
was 1,143 s and the other ~79 % was admission and verification of the 3.48 GiB
of staged source, repeated (~/PolicyEngine/_recovered/pilot-runs/native45-v5/out.md §2).
This makes each of those checks happen once per run, without weakening a
refusal and without moving a digest.

docs/us-native-verification-once.md
is the design note — per mechanism: the guarantee today, the change, why the
guarantee survives, and the new cost class. It was written and committed before
any capsule changed. The lane report, with every measurement and every
re-derived pin, is
experiments/native-verify-once/out.md.

Measurement (2026-09-16, real 1/1000 sample, same arguments as the accepted v4 cold pilot)

run before (v4 receipt / before-probe) after (this branch)
9-node source-admission prefix probe, 1,800 s CPU cap did not complete: ceiling reached at 1,803.9 CPU-s, 8.6 GB completed in 1,445 CPU-s / 1,452 s wall, 12.3 GB
19-node financial graph, cold 5,362 s wall / 5,279 s CPU / 9.31 GB 2,016 s wall / 2,010 s CPU / 13.31 GB (2.6x less CPU)

Files: .measure/repeated-verification-before.json, .measure/after/probe/repeated-verification-measurement.json, .measure/after/harness19/repeated-verification-measurement.json in the branch worktree (measurement artifacts, not committed). Peak RSS rose because memoised authentication keeps validated state resident; the remaining CPU sits in the ACS archive streaming (zipfile read/peek/readline, ~380 s), ASEC coverage _capture (~175 s), _series_digest/_frame_identity (~74 s) and the per-node _population_stamp (~61 s), which are the next lane's targets (whole-roster receipt transport and per-node population retention).

The contract

  • Verified before first use — nothing is memoised until it has been verified
    in full, exactly as today.
  • Proven unchanged at the end — every memo is re-verified in full,
    unconditionally, before the run hands anything back.
  • A signature mismatch is a memo miss, not a refusal: the complete
    validation runs and raises exactly what it would have raised.
  • Opt-in and scoped. Outside a verification_epoch() nothing is memoised,
    which is why every existing capsule test — including the two that trace
    _producer and _file_identity firing inside a validation — keeps its
    exact meaning, and why _validate and _validate_state are untouched.

No digest, key, seal or receipt value moves. _update_series is the one place
bytes are genuinely rebuilt, and it is proven byte-identical against a verbatim
copy of the pre-change body.

The five mechanisms

# what after
1 AuthenticatedSurveyPopulationPreparation._checked() re-ran the whole authentication on every borrow — and the graph registers it as a per-node population observer scoped by verification_epoch(); cheap tier still every borrow
2 AuthenticatedAsec2024NativePopulation.frame re-hashed all seven ASEC source files on every access same epoch; its epoch closes first so the owner's final validation reaches the real file check
3 executor.py re-derived every declared source's content key after every cold node — a 3.48 GiB tree re-read per node per-run _SourceIdentities cache keyed on stat signatures, plus a full cache-bypassed re-derivation before the manifest is built
4 acs_person_coverage_authentication._records was a byte-at-a-time loop over 2.4 GB of ACS person CSV bytes.find/bytes.count with cached terminator cursors — 226.9 s → 4.6 s on the real archive, digests identical
5 executor._update_series boxed every value of a plain float/int/bool column built with numpy, byte for byte — 0.47 s → 0.09 s on a 6,928 × 240 frame

What gained coverage

test_graph_executor_source_identity pins a refusal that had no test anywhere
in the repository
before this branch: a source mutated while a node runs must
make the run refuse before that node's artifacts reach the content store.
Mechanism 3's cache is measured against that pinned contract rather than against
silence.

Main-only hunks

These graph-shard changes depend on nothing in this branch and can be split into
a PR against main as they stand:

  • packages/microcosm-graph/src/microcosm/graph/executor.py
  • packages/microcosm-graph/src/microcosm/graph/manifest.py
  • packages/microcosm-graph/src/microcosm/graph/codecs.py (a docstring that
    described the per-node check as "post-run")
  • packages/microcosm-graph/tests/test_graph_executor_series_stream.py
  • packages/microcosm-graph/tests/test_graph_executor_source_identity.py

Everything else is microcosm-build and belongs on this stack.

Reviewing the two big diffs

graph_atomic_survey_population.py and graph_atomic_survey_financial.py are
an indentation plus one with line. Proven mechanically in the commit
message: the statement lists of both function bodies parse to identical ASTs
before and after (50 and 102 statements), argument lists unchanged, docstrings
outside the wrapper, and both files were ruff format clean at the base so
every hunk lies inside the wrapped function. The body has to stay in its own
function because a build test asserts
caller.f_code is runner.run_atomic_survey_population.__code__.

Pins re-derived

pin old → new
acs_native_coverage_binding._ACCEPTED["acs_person_coverage_authentication.py"] 475aa795…fe85bcff → 9ec68721…d88e8e49f
inventory survey_population_preparation.py unbound_uses_sha256 29c09f6f…d296ef91 → d114117c…dd4005910
inventory asec_2024_native_population.py unbound_uses_sha256 71463df4…d7f287608 → 7bb20439…40d58cdf4

Every other contract in graph_implementation_inventory.json was recomputed
through the module's own generator and is unchanged; all ten stage manifests
build.

Before/after native source-verification cost

All numbers below were read from the files named in the "file" column; nothing was re-run and no source file was touched (worktree /Users/maxghenis/PolicyEngine/_worktrees/microcosm-verify-once clean at 284bc6e9624d211ba9c16dcdf04df63b9e87b10c).

1. Run-level numbers, as recorded

run status in file wall s CPU s peak RSS (bytes / decimal GB) file
9-node prefix, before (base f7bb88525, source tree .measure/source-before) CEILING_REACHED_PARTIAL_MEASUREMENT 1,853.894 1,803.870 (cap 1,800) 8,612,429,824 / 8.61 A
9-node prefix, after (284bc6e96, source tree .measure/source-after) COMPLETED_PREFIX 1,451.762 1,444.776 12,285,526,016 / 12.29 B
19-node financial graph, after (284bc6e96) COMPLETED_NINETEEN_NODE 2,016.079 2,010.072 13,309,181,952 / 13.31 C
19-node financial graph, before = accepted v4 cold receipt result: 0, one test passed 5,362.050 5,278.611 9,313,337,344 / 9.31 (8.67 GiB) D
— of which the v4 run_atomic_survey_financial call alone (timing.case_phases) — 4,685.862 4,605.351 — E

Files. A /Users/maxghenis/PolicyEngine/_worktrees/microcosm-verify-once/.measure/repeated-verification-before.json · B /Users/maxghenis/PolicyEngine/_worktrees/microcosm-verify-once/.measure/after/probe/repeated-verification-measurement.json · C /Users/maxghenis/PolicyEngine/_worktrees/microcosm-verify-once/.measure/after/harness19/repeated-verification-measurement.json · D /Users/maxghenis/PolicyEngine/_recovered/pilot-runs/native19-v2/run/us-native-postclone-financial19-v4-cold.json · E /Users/maxghenis/PolicyEngine/_recovered/pilot-runs/native19-v2/run/native-financial-manifest.json (timing.case_phases: before_financial_runner wall 0.00085 s / CPU 0.00084 s → after_financial_runner wall 4,685.862 s / CPU 4,605.351 s).

Every figure quoted in the task statement is confirmed by the files; the task rounded, the files carry more digits (see caveats for the one genuine discrepancy — a second, differently-numbered before-probe JSON).

2. The 9-node probe: what can and cannot be compared

The before probe is partial, so this is not a measured ratio.

comparable before after
Completed the nine-node prefix inside the 1,800 CPU-s cap? No — stopped at the ceiling, 1,803.87 CPU-s Yes — 1,444.78 CPU-s, 355.2 CPU-s of headroom left
Graph store written under the probe dir none — .measure/probe-before/graph-store does not exist 4,363 object files under .measure/after/probe/graph-store/objects
Statement the two runs jointly support the after does the whole prefix in 0.80× of the CPU the before burned without finishing — a lower bound of ≥ 1.25× on the CPU reduction, true ratio unknown
Peak RSS 8.61 GB (partial — a floor, not a peak) 12.29 GB (whole prefix) — 1.43× the before's floor, and not a like-for-like peak

Top chains by CPU, sampled_cpu_ledger (0.25 s stack sampler; chains print innermost → outermost; the ledger lists the top 60 chains only).

# before — CPU s (share of its partial 1,803.87) after — CPU s (share of 1,444.78)
1 760.23 (42.1%) _read1 <- read <- acs_person_coverage_authentication.py:_records <- _inventory 195.96 (13.6%) _read1 <- read <- peek <- readline
2 242.02 (13.4%) _read1 <- read <- peek <- readline 100.36 (6.9%) string_arrow.py:isin <- algorithms.py:isin <- series.py:isin <- acs_pums.py:_read_archive
3 119.33 (6.6%) string_arrow.py:isin <- … <- acs_pums.py:_read_archive 84.39 (5.8%) _read1 <- read1 <- acs_person_coverage_columns.py:lines <- _literal_csv_records
4 88.36 (4.9%) asec_coverage_authentication.py:_capture <- _reconstruct <- authenticate_asec_coverage <- asec_population_catalogue.py:issue_asec_source_catalogue 80.19 (5.6%) asec…_capture <- … <- asec_2024_native_population.py:load_authenticated_asec_2024_native_population
5 77.31 (4.3%) _read1 <- read1 <- acs_person_coverage_columns.py:lines <- _literal_csv_records 75.38 (5.2%) asec…_capture <- … <- asec_population_catalogue.py:issue_asec_source_catalogue
6 26.22 (1.5%) canonical.py:_json_value ×3 <- canonical_json 68.40 (4.7%) read <- _read2 <- _read1 <- read
7 25.77 (1.4%) read <- _read2 <- _read1 <- read 29.56 (2.0%) asec_coverage_authentication.py:_require <- feed <- _capture <- _reconstruct
8 22.03 (1.2%) encoder.py:_iterencode <- acs_person_coverage_authentication.py:_json <- acs_population_catalogue.py:reserve <- consume_row 28.07 (1.9%) asec_current_money_source.py:_series_digest <- _frame_signature <- asec_2024_native_population.py:_frame_identity <- survey_population_preparation.py:_nested_seals
9 17.61 (1.0%) asec…_require <- feed <- _capture <- _reconstruct 24.12 (1.7%) canonical.py:_json_value ×3 <- canonical_json
10 16.15 (0.9%) c_parser_wrapper.py:read <- readers.py:read <- get_chunk <- __next__ 20.80 (1.4%) encoder.py:_iterencode <- … <- acs_population_catalogue.py:reserve <- consume_row

The one chain-level movement the partial run still supports: the ACS record fence, rank 1 before at 760.23 s and still running when the ceiling hit, is 10.80 s in the after probe (mechanism 4). Chains that rank higher in the after than the before (the two asec…_capture callers, _series_digest) must not be read as regressions: the before never reached that work.

3. The 19-node harness: like-for-like at identical arguments

.measure/harness19_verify_once.py is .measure/probe_verify_once.py with only the docstring, the ceilings (7,200 CPU / 9,000 wall / 32 GiB), one import, the scope and status strings, and the call changed; the call is run_atomic_survey_financial(RUN/"sources", snapshot_root=…, store_root=…, fraction=Fraction(1,1000), seed=20260908, geography_config=geography(seed 20260908), demographic_conditioning=True, n_estimators=2, resume="auto", return_values=True) — the same arguments as the v4 test's own call (/Users/maxghenis/PolicyEngine/_recovered/pilot-runs/native19-v2/run/test_native_nineteen_node_financial.py:130-137), cold store.

comparison wall CPU peak RSS
v4 whole process (D) → after harness (C) 5,362.05 s → 2,016.08 s (2.66×) 5,278.61 s → 2,010.07 s (2.63×, −3,268.5 s, −61.9%) 9.31 GB → 13.31 GB (1.43× higher)
v4 runner call only (E) → after harness total (C) 4,685.86 s → 2,016.08 s (≥2.32×) 4,605.35 s → 2,010.07 s (≥2.29×) —

The second row is the conservative framing: the after number is a whole-process total (imports included) while E excludes everything outside the runner call. The v4 process spent 676.2 s wall / 673.3 s CPU outside the runner (imports and collection 10.4 s, artifact export and readback 210.8 s wall, pending report and final checks 465.4 s wall, guard postcheck 1.8 s), none of which the harness runs.

Corroboration that the after harness did the same amount of work: 5,869 object files under .measure/after/harness19/graph-store/objects against 5,866 under the v4 run's graph-store/objects (comparable volume, not identical keys — every node key moves because each stage's implementation_hash covers the module roster).

4. Top 10 remaining chains, 19-node after harness (file C)

Process CPU 2,010.07 s; these ten are 864.1 s = 43.0% of it; the 60 listed chains sum to 1,464.8 s of the 2,007.83 s sampled total, so ~543 s sits in chains below the ledger cut.

# CPU s samples share of process CPU chain (innermost → outermost)
1 209.72 817 10.4% zipfile/__init__.py:_read1 <- read <- peek <- readline
2 103.04 360 5.1% string_arrow.py:isin <- algorithms.py:isin <- series.py:isin <- acs_pums.py:_read_archive
3 98.17 308 4.9% zipfile/__init__.py:_read1 <- read1 <- acs_person_coverage_columns.py:lines <- _literal_csv_records
4 87.86 338 4.4% asec_coverage_authentication.py:_capture <- _reconstruct <- authenticate_asec_coverage <- asec_population_catalogue.py:issue_asec_source_catalogue
5 86.66 332 4.3% asec_coverage_authentication.py:_capture <- _reconstruct <- authenticate_asec_coverage <- asec_2024_native_population.py:load_authenticated_asec_2024_native_population
6 73.71 285 3.7% asec_current_money_source.py:_series_digest <- _frame_signature <- asec_2024_native_population.py:_frame_identity <- survey_population_preparation.py:_nested_seals
7 71.78 276 3.6% zipfile/__init__.py:read <- _read2 <- _read1 <- read
8 60.80 235 3.0% survey_population_preparation.py:update_cell <- _frame_identity <- survey_atomic_geography.py:_population_stamp <- <genexpr>
9 38.93 151 1.9% acs_housing_universe_source.py:_persisted_sha <- acs_native_coverage_binding.py:_verify_sources <- _verify <- verify_acs_native_coverage
10 33.38 130 1.7% c_parser_wrapper.py:read <- readers.py:read <- _read <- read_csv

This reproduces the PR body's remaining-cost list exactly: ACS archive streaming (rows 1+3+7 = 379.7 s, "~380 s"), ASEC _capture (rows 4+5 = 174.5 s, "~175 s"), _series_digest/_frame_identity (row 6 = 73.7 s, "~74 s"), per-node _population_stamp (row 8 = 60.8 s, "~61 s").

DISCREPANCY (the only one): a SECOND before-probe result exists on disk with different numbers, not mentioned in the task. /Users/maxghenis/PolicyEngine/_worktrees/microcosm-verify-once/.measure/before/probe/repeated-verification-measurement.json, written 2026-09-16 01:34:11, records verbatim: "measurement": "before f7bb885", "status": "CEILING_REACHED_PARTIAL_MEASUREMENT", "source_tree": "/Users/maxghenis/PolicyEngine/_worktrees/microcosm-verify-once-baseline", process {"cpu_seconds": 1801.100405, "wall_seconds": 1821.7746500000358, "peak_rss_bytes": 8304001024}, "sampled_ledger_total_seconds": 1799.2456869999999, and its rank-1 chain is the same ['init.py:_read1','init.py:read','acs_person_coverage_authentication.py:_records','acs_person_coverage_authentication.py:_inventory'] at 777.4474699999996 s. So the before probe was run twice, on two different copies of the same baseline code (the second worktree is at f7bb885, verified by git rev-parse). The two agree on the finding (neither completed; the record fence dominates) and differ by 0.15% in CPU, which is expected because BOTH were stopped by the 1,800 CPU-s ceiling — the CPU figure of a ceilinged run measures the cap, not the work. The task named the earlier file (.measure/repeated-verification-before.json, written 00:47:24), which is the one I report. Its machine context is also weaker: it carries no "machine" block at all, while the second before run recorded loadavg_at_start [54.72, 66.75, 68.97]. Note also that the two before runs were produced by different scripts: the task-named one by .measure/probe_repeated_verification.py (the v5 pilot copy, LABEL "before" -> BASE .measure/source-before, PROBE .measure/probe-before), the second by .measure/probe_verify_once.py, which is also the script both after runs used. Both scripts call run_atomic_survey_population with the same nine-node arguments and the same 0.25 s sampler.

NO OTHER DISCREPANCY. Every figure the task stated is in the files: before 1,803.8695539999999 CPU-s and 8,612,429,824 B ("1,803.9 CPU-s, 8.6 GB"); after probe 1,444.775832 / 1,451.761529374984 / 12,285,526,016 ("1,445 CPU-s / 1,452 s wall / 12.3 GB"); harness19 2,016.0785499999765 / 2,010.072336 / 13,309,181,952 ("2,016 s wall / 2,010 s CPU / 13.31 GB"); v4 receipt 5,362.050427666982 / 5,278.6107919999995 / 9,313,337,344 ("5,362 s wall / 5,279 s CPU / 9.31 GB"). The before probe's wall time (1,853.894 s, under its 2,400 s wall ceiling) was not in the task statement; it confirms the run was stopped by the CPU ceiling, not the wall ceiling. RSS units are decimal GB throughout, matching the task and the PR body; the v4 peak is 8.67 GiB in the harness docstring and 9.31 GB here — same 9,313,337,344 bytes.

WHAT THE BEFORE/AFTER PROBE PAIR CANNOT SUPPORT. The before run is partial, so there is no before/after ratio for the nine-node prefix. The defensible statements are (a) completion inside the same 1,800 CPU-s cap: before no, after yes with 355.2 CPU-s spare; (b) a lower bound of >=1.25x on the CPU reduction (1,803.87 / 1,444.78), with the true ratio unknown and certainly larger; (c) per-chain absolute CPU at the moment each run ended. Share-of-process percentages on the before side are shares of a truncated run and are not comparable with the after's shares. The before peak RSS (8.61 GB) is a floor, so "peak RSS rose 1.43x" on the probe is an upper bound on the rise, not a measurement of it; the harness/v4 RSS comparison (9.31 -> 13.31 GB, 1.43x) is the one that is like-for-like, and it is a real regression by design (memoised authentication keeps validated state resident), well inside that run's 32 GiB ceiling.

SAMPLER, NOT COUNTERS. In all three JSONs every one of the 16 rows in "checks" reads calls=0, cpu_seconds=0.0, status "not-wrapped: runtime refuses instrumented producers (PRODUCER_CHANGED); see sampled_cpu_ledger". All attribution therefore comes from the 0.25 s stack sampler, is statistical, and truncates each chain to four frames and each ledger to the top 60 chains (harness19 cut at 5.00 s; after probe at 2.83 s; before at 2.11 s). A chain "absent" from a list only means it fell below that cut.

INFERENCE, FLAGGED AS SUCH. Neither before-probe directory (.measure/probe-before, .measure/before/probe) contains a graph-store, while both probe scripts pass store_root=PROBE/"graph-store", and no ledger chain in either before file names executor.py. The file evidence is that no store was ever written; the reading that the before runs exhausted the cap inside source admission before any node artifact was produced is an inference from that plus the chain contents, not something the receipts state.

HARNESS-VS-V4 DIFFERENCES THAT ARE NOT THIS CHANGE (from .measure/harness19_verify_once.py lines 12-17, verbatim): "it ran on source snapshot 2ca11c85a, and its person-income-attachment.h5 was the pre-restoration 5996dcdd... rather than the staged 9ebc0ef2.... Both files are exactly 666,333,922 B". I confirmed the v4 receipt's native_before/native_after both carry 5996dcdd... at 666,333,922 B and that the staged file at /Users/maxghenis/PolicyEngine/_recovered/pilot-runs/native45-v5/run/sources/asec/person-income-attachment.h5 is 666,333,922 B; I did not hash the staged file, so the 9ebc0ef2 value is the lane's claim, not something I verified. Further: the v4 receipt is a pytest process (5,362 s covers imports, collection, the runner, artifact export and readback, the pending report and final checks, and the guard postcheck) while the harness is a bare script that ends when the runner returns — hence the two framings in the table. Load context differs and runs against the after: v4's receipt records no loadavg; the after harness started at loadavg [7.34, 7.54, 8.09] and ended at [7.83, 9.33, 11.38], i.e. it shared the machine with other lanes.

PROVENANCE I VERIFIED. .measure/source-before/head.txt = f7bb885 and its acs_person_coverage_authentication.py hashes to 475aa795c8a5b49a0dd3405a0877866dddd03012a1f2b9743447fab1fe85bcff (the OLD _ACCEPTED pin); .measure/source-after/head.txt = 284bc6e and the same module hashes to 9ec68721a4cf480ef412c51ab354db9000eb7a7989e35e6e09b1574d88e8e49f (the NEW pin recorded in the lane report and PR body). So the before run measured unchanged code and the after runs measured the branch. Both after JSONs record source_tree .measure/source-after and staged_run_inputs /Users/maxghenis/PolicyEngine/_recovered/pilot-runs/native45-v5/run; the task-named before JSON records no source_tree field (older script), but .measure/probe_repeated_verification.py derives BASE = ROOT/f"source-{LABEL}" and OUT = ROOT/f"repeated-verification-{LABEL}.json", so the file name fixes LABEL="before" and hence BASE=.measure/source-before.

SCOPE. These are descriptive measurements. Every JSON carries "release_eligible": false and a scope line ending "not a build, not a certification, not release eligible". Nothing here says anything about dataset quality, calibration or release eligibility. I re-ran nothing, edited no source file, and the worktree is clean at 284bc6e. I did not verify the contract claims themselves (no digest/key/seal/receipt moves; fail-closed stays fail-closed; a mutation test per memoised path) — the design note, lane report and PR body assert them and name the tests, and the lane report's "Test summaries, verbatim" section is incomplete as the task said, ending "(Remaining runs and the probe measurement follow.)"; confirming them would need the test suites re-run, which this task forbids.

Tests

1. uv lock --check -> Resolved 125 packages in 5ms (rc 0)
2a. ruff check (CI lint lane's actual command) -> All checks passed! (rc 0)
2b. ruff format --check (NOT run by CI) -> 77 files would be reformatted, 1110 files already formatted (rc 1)
2c. ruff format --check, branch-touched .py files only -> 17 files already formatted (rc 0)
3. graph suite (packages/microcosm-graph/tests) -> 766 passed, 1 skipped in 46.66s (rc 0)
4. microcosm-build tests naming/importing a touched module (62 files) -> 2350 passed, 3 skipped, 438 warnings in 11678.22s (3:14:38) (rc 0)
4b. the two new graph test files, standalone -> 49 passed in 0.41s (rc 0)
5a. engine-free environment sync -> Installed 73 packages in 405ms (rc 0)
5b. graph suite in the engine-free environment -> 765 passed, 2 skipped in 48.63s (rc 0)
6a. CI lint lane extra: test-group partition verify -> verification=ok (tracked_test_files=551) (rc 0)
6b. CI lint lane extra: stdlib matrix/selection contract -> Ran 15 tests in 0.298s -- OK (rc 0)
6c. CI lint lane extra: graph acceptance burndown -> verification=ok (rc 0)

Main-only subset

PR: #938 — draft, base main, head graph-verify-once-main, 6 files, +988/−8. Not marked ready, not merged. CI is running (13 checks pending at time of writing, run 35133268135) — which the stacked PR #935 never gets.

Head sha: 8ea48447cf3245a8ac4acc0242cc9178d50acb13

Worktree: /Users/maxghenis/PolicyEngine/_worktrees/microcosm-verify-once-main (created from origin/main = d69a543e5d7f0ac01e73c9c6eb4645b0d4b8b9b6)

Verbatim test summary lines (all run in that worktree, .venv Python 3.14.4):

$ uv sync --all-packages --locked
Resolved 125 packages in 5ms
Checked 73 packages in 21ms
$ uv lock --check
Resolved 125 packages in 3ms
$ .venv/bin/python -I -B -m pytest packages/microcosm-graph/tests -p no:randomly
585 passed, 2 skipped in 20.68s
$ .venv/bin/python -I -B -m pytest packages/microcosm-graph/tests -p no:randomly \
    --ignore=packages/microcosm-graph/tests/test_graph_executor_series_stream.py \
    --ignore=packages/microcosm-graph/tests/test_graph_executor_source_identity.py
536 passed, 2 skipped in 19.28s
$ .venv/bin/python -I -B -m pytest packages/microcosm-graph/tests/test_graph_executor_series_stream.py packages/microcosm-graph/tests/test_graph_executor_source_identity.py -p no:randomly
49 passed in 0.56s
$ uv run --no-sync ruff check .
All checks passed!
$ uv run --no-sync ruff format --check <the five touched files>
5 files already formatted
$ python3 tools/ci_test_groups.py --verify
verification=ok
$ uv run --no-sync python tools/graph_acceptance_burndown.py --verify
verification=ok

536 + 49 = 585: every pre-existing graph test passes unchanged and all 49 new tests are additions. The count differs from the stacked branch's 766 because main does not carry test_graph_executor_scalar_float.py or test_graph_storage_parts_whole_series.py. Both new files land in fast and engine, neither under [defaulted]. tracked_test_files=400 here (main's inventory), vs 551 on the stack. Note packages/microcosm-build/tests/test_ci_test_groups.py does not exist on main, so the stdlib matrix/selection contract check has nothing to run.

Conflicts resolved against main by hand (two, both in packages/microcosm-graph/src/microcosm/graph/executor.py and its test):

  1. git apply --3way conflicted only on the import block. native-verify-once's base carries an exact-Python-float short circuit in _update_scalar (payload = b"f" + struct.pack("=d", value)) that main does not have, so main's executor does not import struct. I took import stat alone and verified struct. appears nowhere in the resulting file.
  2. Not a git conflict but a false claim if left alone: test_graph_executor_series_stream.py documents _reference_update_scalar as "a verbatim copy of the pre-change body". On main that body has no type(value) is float branch, so I dropped that branch and the file's import struct. I then checked mechanically that all three reference helpers (_reference_update_scalar, _reference_update_array, _reference_update_series) are textually identical to origin/main's _update_scalar, _update_array, _update_series — all three reported IDENTICAL. Both resolutions are inert for emitted bytes (np.asarray([v], dtype=np.float64).tobytes() == struct.pack("=d", v) for an exact Python float), so the byte-level parity the tests prove is the same contract on either base.

Three things worth your attention:

  • A previous unfinished attempt at this exact task existed. Local branch graph-verify-once-main already held 4 commits made today 00:54–00:57, based on a since-superseded origin/main (51c3143), never pushed, no PR, no live process, clean tree. I did not delete it — I renamed it to graph-verify-once-main-stale-20260916-0057 (still checked out at /Users/maxghenis/PolicyEngine/_worktrees/microcosm-graph-verify-once-main) and built this branch fresh from current origin/main. After finishing, I compared: all five graph files in my branch are byte-identical (sha256) to that attempt's, including both resolutions above — an independent confirmation, not a copy. That stale worktree and branch are now safe to delete whenever you want.
  • Commits are split by mechanism so each reviews on its own: a904fa41d (numpy digest stream + its parity test), 873b0e964 (per-run source identity cache, run-end re-derivation, RunManifest.source_identities, refusal tests), 3f38fc315 (codecs docstring correction), 8ea48447c (changelog fragment changelog.d/graph-verify-once.changed.md, following the repo's towncrier convention). I checked a904fa41d's executor parses and contains no stat. or _SourceIdentities, so it stands alone.
  • One inherited detail I deliberately did not change, so this stays a faithful subset of Verify native sources once per run, not once per access and per node #935: _object_stream's float path casts with dtype="<f8" (explicit little-endian) while the comment above it and _update_scalar say native order. They agree on every little-endian machine, which is the only kind the tests run on; on a big-endian host they would diverge. Worth raising on Verify native sources once per run, not once per access and per node #935 rather than fixing here.

Verified independently: RunManifest.source_identities cannot move the manifest identity — content_addressed reads only nodes and tier, to_json never references it, and run_graph is the single construction site and passes it by keyword.

🤖 Generated with Claude Code

MaxGhenis and others added 13 commits September 16, 2026 14:43
Records the lane's base, goal, evidence base and the two local constraints:
root out.md belongs to another lane, and the v5 cold run holds the machine.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
_update_series boxed every value of a plain float, integer or boolean column
into a Python object and framed them one at a time; on pandas 3.0.3 a plain
column is a NumpyExtensionArray with no _data/_mask, so the fast masked path
never applied to it. _object_stream now builds the identical framed bytes --
the same 'object' dtype header, the same shape header, the same per-value
length prefixes and payloads -- with vectorised numpy, and returns None for
every column kind it cannot reproduce exactly, including a Categorical whose
values are an integer-coded view.

No digest value moves. test_graph_executor_series_stream compares the live
helper against a verbatim copy of the pre-change body at the byte level over
every column kind, the float specials, non-canonical NaN payloads, both int64
endpoints and a 200-frame random sweep. Measured 0.47 s -> 0.09 s on a
6,928 x 240 frame.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
docs/us-native-verification-once.md states, per mechanism, the guarantee
today, the change, why the guarantee survives and the new cost class -- and
the two residuals the epoch's unconditional final re-validation exists to
close.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
_records read the 2.4 GB of ACS person CSV inside csv_pus.zip one Python byte
at a time; with peek and columns.lines it was 57-65% of the admission phase in
every measured native run. It now finds terminators with bytes.find, counts
quote parity with bytes.count, and keeps each terminator cursor until the scan
passes it -- so an absent carriage return costs one search per 1 MiB block
rather than one per record, which is where the bulk of the win is.

The two ceilings are charged by _fence. A record no longer than the smaller of
the live ceilings cannot have violated either, because the token counter resets
at every record start; anything longer is replayed byte by byte, so the refusal
code and the byte it fires on stay the fence's own -- including the LF that
closes a CRLF record, which the fence charges against the record ceiling only.

test_us_acs_record_fence_scan compares the shipped fence against a verbatim
copy of the byte loop: every boundary case, every block-boundary case, every
ceiling on both sides, 4,500 randomised strings, 300 row-shaped inputs, and all
9,331 strings up to five bytes over the branching alphabet under each of five
ceiling settings -- 46,655 exhaustive comparisons. Measured on the real staged
archive: 10.8 MB/s -> 522.8 MB/s, digests identical.

Re-pins acs_native_coverage_binding._ACCEPTED for the changed module:
  475aa795c8a5b49a0dd3405a0877866dddd03012a1f2b9743447fab1fe85bcff
  -> 9ec68721a4cf480ef412c51ab354db9000eb7a7989e35e6e09b1574d88e8e49f
  (shasum -a 256 .../acs_person_coverage_authentication.py, after formatting)
The module's graph_implementation_inventory contract is unchanged: the rewrite
adds no _RESOURCE_CALLS name and no non-stdlib import.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
executor.py re-derived source_content_key for every source a cold-executed
node declared, and keys._directory_identity read_bytes()es every file of the
tree: O(executed source-declaring nodes x source bytes), which on the native
graph is the 3.48 GiB source tree re-read per node.

run_graph now carries a _SourceIdentities cache in its own locals, populated by
the run-start pass as it reads. A cached key is reused only while the path's
stat signature -- (dev, ino, size, mtime_ns, ctime_ns) for a file, and for a
directory its own identity plus the relative name, type and identity of every
entry _directory_identity would walk -- is identical to the signature taken
both immediately before and immediately after the read that produced the key.
A derivation whose two signatures disagree is not cached at all.
source_content_key itself stays pure and uncached: three key and codec tests
call it twice on one path with the bytes changed in between.

Before the manifest is built, every source is re-derived in full with the cache
bypassed, written inline rather than through _source_paths_and_keys because a
build test counts calls to that code object. That closes the two cases a stat
signature cannot decide, and gives the executor a guarantee it never had: a
source changed during a node that declares none is now caught.

RunManifest gains source_identities, attached exactly like populations and
mass_ledgers -- repr=False, compare=False, outside content_addressed, outside
to_json, outside every node receipt and cache record.

test_graph_executor_source_identity pins the contract that had no test at all:
a source rewritten by its own node refuses and writes nothing to the store, a
file added to or removed from a directory source refuses, a same-length rewrite
with mtime restored refuses, a change during a source-free node refuses at run
end, and an unchanged source is read twice per run however many nodes declare
it. The graph suite falls from 108 s to 72 s.

Also records the real-archive record-fence parity receipt: both ACS person
members, 2.4 GB, 3,422,890 records, identical digests, 226.9 s -> 4.6 s.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Every borrow of AuthenticatedSurveyPopulationPreparation re-ran the whole
authentication: both source catalogues, the ACS native coverage binding (eight
full zip reads), the nested ASEC native population (seven files), the ten-file
roster re-hash and every pure seal -- once per executed node through the
population observer, and again inside every kernel that borrows it. The ASEC
native capsule did the same on every .frame access.

survey_population_preparation.verification_epoch() makes that happen once.
Inside an epoch each borrow still pays a cheap tier in full -- the live
authority and attached owner payloads, _encode(_producer()), and _file_stats
over the whole roster -- and the expensive tier is skipped only while a
signature is unchanged: the stat identity of every path the four foreign
verifications and _source_files re-read, the identity and length of every
borrowed payload, and a read-free witness of every live Frame's storage
(buffer address, shape, strides, dtype and writeable flag per column, index and
weight). A signature that moved is a memo miss, not a refusal: the complete
validation runs and raises exactly what it would have raised.

Leaving the epoch, at every nesting level, re-validates every memoised capsule
in full with the memo bypassed, so a change no signature can see -- an in-place
write into a live buffer, or a value written into a frozen plan row -- still
makes the run refuse before the caller receives anything. Outside an epoch
nothing is memoised and every existing borrow, refusal and trace-based mutation
test keeps its exact meaning; _validate and _validate_state are untouched.

The three foreign owners gain only a _verified_source_stats() helper naming the
paths their own verification re-reads. Their inventory contracts do not move.
The two capsule modules' unbound_uses digests do, because the new functions use
already-imported names in new scopes; both are re-derived through
graph_implementation._dependency_contract and recorded in PROGRESS.md.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
run_atomic_survey_population and run_atomic_survey_financial now wrap their
whole body in survey_population_preparation.verification_epoch(). Every native
capsule the run borrows is authenticated in full once, reused while its sources
and live storage are provably unchanged, and re-authenticated in full before
the run returns. The financial run's prefix opens a nested epoch; each closes
with its own full re-validation.

Both commits are an indentation plus one 'with' line. Proven mechanically: the
statement lists of both function bodies parse to identical ASTs before and
after (50 statements and 102 statements), the argument lists are unchanged, the
docstrings stay outside the wrapper, and both files were ruff-format clean at
the base so every hunk lies inside the wrapped function. Keeping the body in
its own function matters: a build test asserts
caller.f_code is runner.run_atomic_survey_population.__code__.

The epoch's exit closes the nested capsule's epoch first, so this owner's final
validation reaches the nested population's complete file check rather than its
memo, and collects both refusals so neither can be lost to the other in a
finally block. A foreign owner's error raised by a deferred borrow is
translated exactly as _checked translates it, instead of escaping raw.

test_us_native_verify_once_epoch: 17 tests. Validated once and reused (one
_source_files pass for six borrows, five memo hits); the producer encoding
never memoised; every on-disk mutation -- append, truncate, same-length rewrite
with mtime restored, a file added to a source directory, a touch -- refusing at
the borrow that follows it AND again when the epoch declines to close over it;
a signature miss outside the roster re-running without refusing; a change no
signature can see refusing at the close; a failing body finalizing nothing; and
nested epochs finalizing at every level.

Also adopts the change in docs/validation-cost-and-borrow-boundaries.md, which
recorded this optimization as proposed rather than adopted, and corrects the
codecs docstring that called the executor's check post-run.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Co-Authored-By: Claude Opus <noreply@anthropic.com>
The frame-witness bridge into the preparation owner is module-internal; naming
it without the underscore made it look like an owner API. Renaming it moves the
module's unbound_uses digest, because that digest keys each use site by its
enclosing scope, so the inventory contract is re-derived through the module's
own generator:
  71463df4f645cd2a98f537a212187b4d1e52bffcc4d2317defe6968d7f287608
  -> 7bb20439da26f75b48551e21e304c4bb7f3983ee07ef78ed804411640d58cdf4
All ten stage manifests still build.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Co-Authored-By: Claude Opus <noreply@anthropic.com>
The split branch, the measurement staging and the remaining work, so the
journal is accurate while the probes are running.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The measurement staging itself stays out of the tree (.measure/ is ignored):
this is the script, parameterised so the identical file measures the base and
this branch, with the import-origin assertion that makes that comparison mean
something.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tions

Finishes experiments/native-verify-once/out.md per the lane brief's report
section, on top of 284bc6e (the head at which every number in it was
measured).

- Status first: no test group failed. `ruff format --check .` exits 1 on 77
  pre-existing files; the intersection with this branch's diff is empty, the
  branch does not touch the ruff config, its own 17 .py files are clean, and
  CI's lint job runs `ruff check .` only. Stated, not softened.
- Per mechanism, a probe-level paragraph beside the unit-level numbers, each
  saying what the runs can and cannot support. The one chain-level before/after
  the partial before probe supports is the ACS record fence: rank 1 at 760.23 s
  (still running at the ceiling) against 10.80 s in the after probe and 11.98 s
  in the 19-node run.
- A consolidated digest-equality section: the two mechanisms that rebuild bytes
  and are compared against verbatim copies of the pre-change code, the three
  that rebuild nothing, and the whole-inventory recomputation.
- Pins re-derived, each half re-checked against the tree while writing this
  (shasum on both sides, and the inventory diff is exactly two lines).
- Test summaries verbatim: the lane's own runs plus the 2026-09-16 CI-shaped
  battery, with the 62-file selection, the three expected skips, the
  engine-free skip delta and the load contention that inflates the wall times.
- Measurement: files A-E, run-level table, the 9-node probe's bound (>= 1.25x,
  not a ratio), the 19-node like-for-like (5,278.61 -> 2,010.07 CPU s, 2.63x;
  peak RSS 9.31 -> 13.31 GB), the top ten remaining chains, and the caveats,
  including the second before-probe JSON on disk.
- PR URL, base, head sha, and seven open questions for Max as questions with
  options.

Also corrects the experiment README, which named two probe result files that do
not exist under those names, and updates the lane's journal section.

No claim about dataset quality, calibration or release eligibility.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
MaxGhenis and others added 12 commits September 16, 2026 14:55
…ules

Every mutation test the lane had for the preparation capsule targets a roster
file or a roster directory, and the cheap tier compares `_file_stats` over
exactly those before any memo lookup, so all of them refused with
SOURCE_STAT_CHANGED without reaching the memo at all. The branch of the new
code the design leans on hardest -- a signature miss running the complete
validation and raising whatever it would have raised today -- was untested for
that capsule.

Both new tests change file content while keeping the length, the inode and the
modification time, and both pick a path the cheap tier cannot compare:

* the preparation capsule's test writes into an ACS catalogue private snapshot
  copy, which is in the memo signature and outside `_file_stats`, asserts the
  roster stats are byte-identical across the mutation, and asserts the borrow
  refuses with the same code an unmemoised borrow of the same mutation raises
  (PREPARATION_VERIFICATION_REFUSED, from the ACS catalogue's own SOURCE_CHANGED);
* the ASEC capsule's cheap tier compares no file stat at all, so its test
  counts the file loop to prove the refusing borrow ran the complete
  `_validate_state` rather than a cheap check, and pins SOURCE_FILE_CHANGED at
  the borrow and again at the close.

The existing cheap-tier tests are unchanged.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The design note promised that a signature mismatch is a memo miss rather than
a refusal, so the complete validation runs and raises "whatever refusal it
would have raised today ... with the same code, at the same access". The cheap
tier broke that promise for one comparison: it compared `_file_stats` before
the memo lookup, while `_validate` compares it last, after both catalogues,
both native verifications and the roster re-hash. An appended `acs/csv_pus.zip`
therefore refused with SOURCE_STAT_CHANGED inside an epoch and with
PREPARATION_VERIFICATION_REFUSED outside one, for the same mutation.

The fix is behavioural rather than documentary, because it is smaller than the
divergence it removes and it deletes a redundant `_file_stats` walk per borrow:
the roster's stat identities are read into the signature (where
`_verified_source_stats` already reads them) and no longer compared in the
cheap tier. A moved stat field is now a miss, `_validate` runs in full and
compares the same roster stats it always compared, and the borrow raises the
code an unmemoised borrow raises. A signature that cannot be taken at all --
a roster entry removed, or no longer a regular file -- defers to `_validate`
first for the same reason, and re-raises only as the fail-closed remainder.

`test_an_in_epoch_refusal_carries_the_code_it_carries_today` refuses four
mutations twice each, once inside an epoch and once with no memo at all, and
asserts the codes are equal and are SOURCE_CHANGED, PREPARATION_VERIFICATION_REFUSED,
PREPARATION_VERIFICATION_REFUSED and SOURCE_STAT_CHANGED. The existing
cheap-tier tests, including the touch test that pins SOURCE_STAT_CHANGED, are
unchanged and still pass.

The note's two tier paragraphs are corrected with the same change: Tier A also
keeps the live authority and the attached owner payloads (the code was already
stricter than the note there), and the signature is named as input, never a
verdict.

Recomputed after the edit: all 124 inventory contracts through
`graph_implementation._dependency_contract` give `moved: 0`, and all ten stage
manifests build.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The brief asked for the run-end re-verification to be recorded. The epoch
already yielded a record, but both call sites opened it bare, so hits, misses,
final validations and the capsule count were dropped the moment the run ended,
and nothing anywhere said how many full validations a real run performed. The
design note named a `verification_epoch.last_record()` API that does not exist.

Both runners now bind the record and hand it to `run_graph`, which attaches it
to the manifest as `RunManifest.verification_epoch` -- a live view of the
caller's own mapping, kept rather than copied because such a record is
finalised when the epoch closes, which happens after `run_graph` returns and
before the caller receives the manifest. It carries counts and labels only, and
like `source_identities` it is `repr=False`, `compare=False`, outside
`content_addressed`, outside `to_json`, and outside every node receipt and
cache record. No existing receipt field moves.

`test_a_real_run_records_its_epoch_in_the_manifest` runs the nine-node atomic
survey population graph over invented sources and reads the record off the
manifest: 20 memo hits, 3 signature misses, one unconditional final
re-validation, one capsule. The report now quotes those counts and states
plainly that "once per run" is the shape rather than the literal count -- that
run paid four complete validations for twenty-three borrows.

The design note's nonexistent API is replaced by what the code actually
exposes.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
`_source_stat_signature` lstat()ed every entry of a directory source, while
`keys._directory_identity` selects members with `is_file()` and reads them with
`read_bytes()` -- both of which follow symlinks. A member link's target was
therefore inside the source's content key and outside the signature over it, so
rewriting that target left all five of the link's stat fields untouched, the
per-node check answered from cache, and the refusal was deferred to the run-end
re-derivation, after intervening nodes had written store records. That residual
was in neither the design note nor the report.

The signature now contributes the resolved identity of every member that is a
symlink, so both sides walk the same members. A link that resolves to nothing
contributes that fact and raises nothing, matching `_directory_identity`
skipping it.

`test_a_symlinked_member_refuses_at_the_node_that_changed_its_target` runs a
graph whose kernel rewrites the target and asserts the node refuses and the
store gained no object; two unit tests pin the signature itself, including the
broken-link case. The note and the report state the follow on both sides.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
`_object_stream`'s float branch cast to an explicit `<f8` while `_update_scalar`
packs `struct.pack("=d", value)` and its array fallback uses `np.float64`, both
native order. The two agree on every little-endian host -- which is why the
byte-parity tests pass here -- and disagree on any other, so the helper's
"byte-for-byte identical output is the contract" would not hold on a big-endian
build. The cast is now `np.float64`.

The length prefixes stay explicitly little-endian on purpose: the loop writes
them with `int.to_bytes(8, "little")`, so `_framed`'s `<u8` and the integer
branch's `<u8` lengths are the correct match. The comment now says which is
which. Byte-identity tests unchanged: 34 passed.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Five places described the code inaccurately, and one comment described a
behaviour the code did not have:

* the design note said the ASEC memo is keyed by the identity of the `_ISSUED`
  entry tuple and torn down by the weakref callback that clears `_ISSUED`; it
  is keyed by `id(owner)` with a weak reference beside the signature, answers
  only while that reference resolves to the same object, and is cleared when
  the outermost epoch exits;
* the epoch finalizer's comment, the note and the report all said the owner's
  final validation reaches the nested population's complete file check. That
  is true only at the outermost close, where the nested memo has been cleared.
  At an inner nested close the depth stays above zero, `_epoch_exit` refreshes
  the nested signature, and the owner's validation is a memo hit -- with
  nothing skipped in net, because the nested capsule's exit has just
  re-validated it in full. All three now say that;
* the report said each mechanism 1 mutation test asserts its refusal twice;
  the last one asserts it once by design, because the mutation it makes is the
  one a signature cannot see, so the borrow after it is a hit and the close is
  the only refusal;
* `test_a_rewritten_source_still_refuses_when_only_its_bytes_moved` implied it
  exercised a stat-preserving rewrite. It does not: st_ctime_ns moves. Its
  docstring now says so and points at the test that reaches the run-end
  re-derivation, which is where the genuinely stat-preserving case is closed;
* the finalizer's comment promised "a refusal is never dropped, only ordered",
  while a nested refusal raised alongside this owner's was discarded. The
  owner's refusal is still the one raised -- its class is what every borrow
  through it raises -- and the nested one is now chained onto it as its cause.

Tier A already kept the live authority and the attached evidence on every
borrow, which the note listed under Tier B; that correction landed with the
refusal-code fix.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
`_stat_or_absent` in `survey_population_preparation.py` had no caller anywhere
in the repository; it is deleted.

`_path_stat` stays duplicated verbatim in the four capsule modules, and the
report now says why instead of leaving a reader to find out.
`graph_implementation._dependency_details` records every relative import as an
unbound use and every scope-qualified use of the alias it binds, and
`_dependency_contract` hashes that set into the `unbound_uses_sha256` the
inventory pins per module. Running a shared-helper import through the module's
own generator confirms it: `imports` does not move, `unbound_uses_sha256` does.
Consolidating would re-pin four authentication capsules in the file that binds
what each capsule may reach, on top of the two pins this lane already moves --
a larger and more security-relevant diff than the duplication it removes. The
cross-package pair (`survey_population_preparation._stat_identity` and
`executor._stat_identity`) would additionally move `imports` and require a new
`import_classifications` entry.

All 124 contracts recomputed after the deletion: `moved: 0`; all ten stage
manifests build.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
…misses

The report's mechanism 5 section named byte-parity proofs and no mutation test,
which left the impression that the "did the kernel mutate its context" guard
was unexercised through the new numpy path. It is exercised:
`test_executor_detects_mutation_even_when_pandas_replaces_a_buffer` mutates an
int64 column inside a kernel and asserts the refusal, and that column now
streams through `_object_stream`'s integer branch. The section says so, and
says the float and bool branches have byte parity only -- no test mutates a
float or bool column in a kernel -- so the coverage is stated rather than
implied.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Two stale claims in the report, both now dated rather than deleted, per the
repo's journal rule.

The eight "lane's own runs" summary lines were pasted as each mechanism landed,
at that day's head and machine state, and none of them was reproduced in the
CI-shaped battery; several of their test counts have since moved because the
verification findings added tests. The section now says so up front, and points
at the battery and the post-findings section as what stands for the branch.

Open question 4 asked whether to open the main-only split. It exists: draft PR
#938, base main, head 8ea4844, 6 files +988/-8, MERGEABLE (checked with
`gh pr view 938`). The question is marked answered, and the four graph-shard
changes this branch has made since -- the member-symlink follow, the
native-order float cast, `RunManifest.verification_epoch` and `run_graph`'s
`_verification_epoch` parameter -- are named as the mirroring that is still
open. PROGRESS.md's "Next" says the same.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The verification findings changed the code under three of the report's sections
and rebased the branch under its diff stat, and the report still described the
state before both.

* Mechanism 1's guarantee said every borrow pays `_file_stats` as a refusal.
  Since the refusal-code fix it reads the roster stats into the signature
  instead, so a moved stat field is a miss whose complete validation raises
  today's code. The section now says that, names the two tests that reach the
  memo-miss branch at all, and states what each one pins.
* Mechanism 2 gains the rewrite-through-the-memo test beside the one it had.
* The five-copies section claimed a cross-package `_stat_identity` import
  "would require a new entry in `import_classifications`". It would not:
  `microcosm.graph.executor` is already classified there. What it does move is
  that module's pinned `imports` list, while `unbound_uses_sha256` stays put,
  because every stage carrying the file declares the graph package and
  `_covered_imports` binds everything beneath it -- the mirror image of the
  four `_path_stat` modules, where `unbound_uses_sha256` moves and `imports`
  does not. Both halves were re-derived through the modules' own generator
  rather than argued, and the section now says they were.
* The header, the head-sha rows and the diff stat are re-stated for the rebase
  onto `363a9033b`: 26 files, +5,422 / -819, with two-dot and three-dot now
  agreeing because no base drift is left. The superseded 27 files / +4,399 /
  -849 is kept with the reason it was wrong.

Re-verified at this head while writing it: all 124 inventory contracts
recomputed through `graph_implementation._dependency_contract` give `moved: 0`,
all ten stage manifests build, `shasum -a 256` on
`acs_person_coverage_authentication.py` is the `9ec68721...d88e8e49f` that
`acs_native_coverage_binding.py:38` holds, and the only 16+-hex changes in the
three-dot `packages/*/src` diff are that pin and the two `unbound_uses_sha256`
lines.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The nine commits that resolve the verification findings changed three source
files and two runner call sites, so the groups that reach them were run again at
this head, on the rebased base. All rc 0, pasted verbatim into a new report
section and summarised in the journal:

    769 passed, 1 skipped in 44.56s                        packages/microcosm-graph/tests
    128 passed in 188.39s (0:03:08)                        the four new test files together
    24 passed in 168.66s (0:02:48)                         test_us_native_verify_once_epoch.py
    52 passed in 0.48s                                     test_us_acs_record_fence_scan.py
    34 passed in 0.27s                                     test_graph_executor_series_stream.py
    18 passed in 0.32s                                     test_graph_executor_source_identity.py
    1811 passed, 1 skipped, 38 warnings in 11200.54s (3:06:40)   46 build test files
    All checks passed!                                     ruff check .
    17 files already formatted                             ruff format --check, changed .py only
    Resolved 125 packages in 4ms                           uv lock --check
    verification=ok                                        ci_test_groups.py --verify
    Ran 15 tests in 0.347s / OK                            the stdlib matrix check

The 46 files are every build test naming `survey_population_preparation`,
`graph_atomic_survey_financial`, `graph_atomic_survey_population` or
`verification_epoch`, or importing `microcosm.graph.executor` or
`microcosm.graph.manifest` by dotted path -- a subset of the 62-file battery.
The single skip is `test_uk_uc_capital_coherence.py:345`, "requires
policyengine-uk extra", confirmed by re-running that file alone with `-rs`.

Three counts moved and the section says why: the graph suite 766 -> 769 and
`test_graph_executor_source_identity` 15 -> 18 for the member-symlink follow,
and the epoch file 17 -> 24 for the two memo-miss tests, the four
refusal-code parametrisations and the real-run record test.

Open question 5 is re-counted while here: 35 build test files reach the graph
shard only through the package re-export, and `--collect-only` over them reports
`992 tests collected in 20.03s`. Collection is all that was run against them.

Nothing above the new section was re-measured, and the journal's third-session
state is marked historical rather than rewritten.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The whole-branch insertion count moves with every commit to this report, so
quoting one number as a standing fact makes the row wrong as soon as the row is
written -- which is how it was wrong before. The row now gives the three parts
that do not move while only this file changes: `packages/*/src` 12 files,
+1,683 / -818; `packages/*/tests` 4 files, +1,780; docs, journal, changelog
fragment, `.gitignore` and `experiments/` 10 files, +2,184 / -1. The
whole-branch total is kept, anchored to the head it was read at.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Sep 17, 2026
A country runtime that scopes source verification around a whole run yields a
record of what that scope did -- memo hits, signature misses, final validations,
capsule count. Nothing carried it out of the scope, so it was dropped the moment
the run ended and nothing anywhere said how many full validations a real run
performed.

`run_graph` takes a private `_verification_epoch` mapping and attaches it to the
manifest as `RunManifest.verification_epoch`. It is a live view of the caller's
own mapping, kept rather than copied, because such a record is finalised when
the scope closes -- which happens after `run_graph` returns and before the
caller receives the manifest. `__post_init__` checks it is a mapping of string
keys to integer or string values, so it carries counts and labels only: no key,
digest or path.

Like `source_identities` it is `repr=False`, `compare=False`, and outside the
addressed surface: `content_addressed` projects `nodes` and `tier` alone, and
`to_json` names its fields explicitly, so neither reaches it. No existing
receipt field moves and no digest, key, seal or receipt value changes.

The callers that bind such a record are the US native runners on #935's stack
and are not on this branch; this is the graph-package half, which is the half
that reaches CI here.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Sep 17, 2026
`_object_stream`'s float branch cast to an explicit `<f8` while the
`_update_scalar` loop it reproduces packs
`np.asarray([value], dtype=np.float64).tobytes()`, which is native order. The
two agree on every little-endian host -- which is why the byte-parity tests
pass here -- and disagree on any other, so the helper's "byte-for-byte
identical output is the contract" would not hold on a big-endian build. The
cast is now `np.float64`.

The length prefixes stay explicitly little-endian on purpose: the loop writes
them with `int.to_bytes(8, "little")`, so `_framed`'s `<u8` and the integer
branch's `<u8` lengths are the correct match. The comment now says which is
which.

The rationale is stated against `main`'s `_update_scalar`. #935's base carries
an exact-Python-float short circuit there (`struct.pack("=d", value)`) that
`main` does not; both are native order, so the defect and the fix are the same
on either base, and only the code this comment cites differs.

No emitted byte changes on a little-endian host, so no digest, key, seal or
receipt value moves here.

  $ uv run --no-sync pytest \
      packages/microcosm-graph/tests/test_graph_executor_series_stream.py \
      -p no:cacheprovider
  34 passed in 0.65s

Co-Authored-By: Claude Opus <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Sep 17, 2026
`test_a_rewritten_source_still_refuses_when_only_its_bytes_moved` read as though
it exercised a stat-preserving rewrite. It does not: it restores st_mtime_ns and
holds the length, but st_ctime_ns still moves, and st_ctime_ns is one of the
five fields `_stat_identity` binds -- so the signature moves and the node
refuses on the ordinary path. That is the strongest rewrite an unprivileged
process can perform here.

The genuinely stat-preserving case, all five fields identical, is the residual
the run-end re-derivation exists for. The docstring now says which of the two it
is and points at
`test_a_source_changed_during_a_source_free_node_refuses_at_run_end`, which
reaches that re-derivation by the other route.

Docstring only: no assertion, no behaviour and no emitted byte changes.

The rest of #935's docstring pass corrects the design note, the report and the
epoch finalizer's nested-refusal chaining, none of which are on this branch.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
MaxGhenis and others added 20 commits September 17, 2026 03:08
…probe

`RunManifest.verification_epoch` is bound and passed by both native runners,
but only the nine-node population runner had a test that read the record back:
`test_a_real_run_records_its_epoch_in_the_manifest`. Deleting
`_verification_epoch=verification` from `run_atomic_survey_financial` failed
nothing -- the runner this record was asked for, and the one the 19-node
measurement uses.

`test_nineteen_node_financial_cold_and_required_replay` now reads the record
off both manifests that runner returns: its own epoch's on the outer manifest,
and the nested nine-node population epoch's on the prefix's. For the cold run
and the required replay alike it asserts the epoch's protocol label, that every
capsule either epoch memoised was re-validated in full as it closed
(`final_validations == capsules >= 1`), and that `to_json` still cannot see any
of it. The module-scoped fixture already holds both runs, so this costs no
additional run. It asserts presence and closure, not counts: literal hit and
miss numbers there would bind an unrelated test's call pattern, and the counts
are the nine-node test's.

No measured run carries the record either, because the committed probe never
read it. `experiments/native-verify-once/probe_verify_once.py` now copies the
returned manifest's record into its output JSON and its console summary as
`verification_epoch` -- `null` until the runner returns, so every mid-run flush
and every ceiling exit carries `null`, and the before tree has no such record
at all. The run values are dropped again immediately so peak RSS stays
comparable with the runs that predate the line. The three measurement files
`out.md` quotes were written before it existed and none of them carries the
field; the uncommitted `.measure/` scripts are left exactly as they ran, and
the report, the README and the design note all say so. Nothing was re-measured.

No digest, key, seal or receipt value moves: the record is outside the manifest
key, outside `to_json`, and outside every node receipt and cache record, and
the probe only reads it.

  $ uv run --no-sync pytest \
      packages/microcosm-build/tests/test_us_graph_atomic_survey_financial.py \
      -p no:cacheprovider
  7 passed in 119.98s (0:01:59)

  $ uv run --no-sync pytest \
      packages/microcosm-build/tests/test_us_native_verify_once_epoch.py \
      -p no:cacheprovider
  24 passed in 157.61s (0:02:37)

Co-Authored-By: Claude Opus <noreply@anthropic.com>
Open question 4 and this journal's "Next" were written at 06:40 UTC on
2026-09-17 and overtaken eight minutes later. Both said #938 was at head
`8ea48447c`, 6 files +988/-8, and that four post-findings graph-shard changes
were "not in #938 yet, and mirroring them is the open item".

`gh pr view 938` at 07:03 UTC reports head
`1884d7f2a3af96799457dd42021cd111220e1ffc`, 6 files +1,124/-8, draft,
MERGEABLE, base `main`, with four commits dated 06:45-06:46 UTC that carry
exactly those changes: the verification epoch on the manifest with the
`_verification_epoch` parameter on `run_graph` (`6206b4a75`), the member-symlink
follow in `_source_stat_signature` (`d87196ce9`), the native-order float cast in
`_object_stream` (`284252e56`) and the stat-preserving-rewrite docstring fix
(`1884d7f2a`).

Diffed against this branch at 07:00 UTC: `manifest.py`, `codecs.py` and
`test_graph_executor_source_identity.py` are identical, and `executor.py` and
`test_graph_executor_series_stream.py` differ only by the documented `import
struct` and exact-`float` short-circuit hunks, which belong to #893's base. So
the split carries the whole graph shard as this branch has it, and the open item
handed to Max is closed. Both documents now say that, with the dates and the
shas, and the opening figures are kept as the history they are.

Documentation only: no code, no test and no measured number changed.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The second fix pass added 19 lines to
`packages/microcosm-build/tests/test_us_graph_atomic_survey_financial.py`, an
existing file this branch had not touched, so the PR table's parts moved: the
tests part is 5 files / +1,799 rather than 4 / +1,780, the file count is 27
rather than 26, and the docs-and-experiments part rises with each edit to the
report itself. Each part is now dated to the sha it was measured at
(`db7a3a0ba`), the `packages/*/src` part is marked as the one no edit to this
report can move, and the 06:40 figures are kept beside them as what they were,
with the one line that explains the difference.

`python3 tools/ci_test_groups.py --verify` is `verification=ok`: the pass added
assertions to an existing file and no new test file, so the partition is
untouched and "all four new test files" is still exactly right.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
`_finalize_epoch` validated a memoised capsule and only then recorded the
signature that answers the next borrows. At an inner nested close that memo
survives into the outer epoch -- `_MEMO` is cleared only at the outermost
close -- so a roster file whose stat moved after `_validate`'s own
`SOURCE_STAT_CHANGED` comparison and before the recording was absorbed as the
new normal, and every outer borrow up to the outermost close was a memo hit.
The refusal this capsule owes at the borrow arrived at the end of the run
instead, after intervening nodes had written store records. Before the
2026-09-16 fix the cheap tier's own `_file_stats` comparison anchored that
borrow; moving the roster stats into the signature removed the anchor without
replacing it, so this is a refusal that moved later because of a fix pass.

The signature is now taken before validating and again after, and when the two
differ no memo answer is recorded at all: the next borrow is a miss and pays
the complete validation, which raises the code it raises today. The entry
itself stays, because the outermost close re-validates every capsule the memo
still holds and dropping it here would drop that pass for a capsule nothing
borrows again. Nothing is compared per borrow, so the per-borrow `_file_stats`
walk the fix pass removed does not come back.

`asec_2024_native_population._epoch_exit` had the same ordering, present since
the original head and never anchored by a cheap-tier stat check, and is fixed
the same way.

Both are pinned by a test that reproduces the shape: an outer epoch, an inner
epoch, and a profile hook that moves a source as the inner close's own
validation returns -- the only way in, since rebinding a runtime callable
refuses with PRODUCER_CHANGED. Against the previous ordering both tests fail
with DID NOT RAISE; against this one the next borrow refuses with
`SOURCE_STAT_CHANGED` (preparation) and `SOURCE_FILE_CHANGED` (native).

`_memoized_validate`'s signature handler also now catches `Exception` rather
than `BaseException`, which is what its own comment describes: an interrupt or
a process exit landing in `_memo_signature` is not a memo miss and must not buy
a complete re-validation -- minutes of SHA-256 over real sources -- before it
propagates.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
…hing reached

`RunManifest.verification_epoch` and `run_graph(_verification_epoch=...)` had
no test anywhere in `packages/microcosm-graph/tests`, on this branch or on
#938's head, which is the PR that ships them to `main`. `source_identities`
beside them has
`test_the_manifest_records_the_run_end_identities_without_moving_anything`; the
new field had only a `"verification_epoch" not in manifest.to_json()` line in
the build shard, which #938 does not carry.

`test_graph_verification_epoch.py` asserts, on a synthetic toy graph: that a
record moves no node key, no manifest key, no `to_json` and no
content-addressed body, compared both against a bare manifest built from the
same fields and against a separate cold run with no record at all; that no
record is an empty mapping rather than a missing attribute; that the manifest
holds the caller's own mapping as a live view and not a copy -- the property
the design leans on, since the record is finalised when the caller's scope
closes, after `run_graph` has returned -- while staying read-only through the
manifest; that a mapping proxy handed in is kept as given rather than wrapped
twice; and that each of the three `__post_init__` refusals fires.

In the build shard, two branches of the memo had no test either. A roster file
removed mid-epoch is the case where no signature can be taken at all, so
`_memoized_validate`'s handler runs the complete validation before re-raising:
the refusal a caller sees is asserted equal to the one an unmemoised borrow
raises for the same removal, on its own fixture. And an epoch whose nested
capsule and whose own close both refuse is asserted to raise this owner's error
class with the nested one attached as its cause, which is the `raise own from
nested` branch.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
… old one

`cba9d6c3a`'s tests and comments were written while the cheap tier still
compared `_file_stats` ahead of the memo lookup. `e36f975d9` moved the roster
stats into the signature and removed that comparison, and the prose around it
was not revisited, so five places in
`test_us_native_verify_once_epoch.py` -- the module docstring and four test
docstrings and comments -- and the block comment at
`survey_population_preparation.py:1005` still called the roster stats part of
the cheap tier, contradicting the `_cheap_checks` docstring twenty-five lines
below. None of it affected behaviour; all of it now says what the code does:
the cheap tier refuses on live authority, attached owner payloads and the
producer encoding, and the roster stats are read into the signature.

The design note and the report gain the third pass's own rule -- a close
records a signature only when nothing moved while it validated -- in the epoch
section, in mechanism 1, in mechanism 2 and in both mutation-test lists, with
the two new tests named. The report also: records that no pin moves in this
pass and how that was re-derived; replaces "the one new recorded thing" with
both recorded things, now that `verification_epoch` has a graph-shard test;
corrects the `_stat_identity` citation from `executor.py:2100` to `:2105`; and
restates #938 at head `33f3150bb` (7 files, +1,430/-8, draft, MERGEABLE,
`gh pr view 938` 2026-09-17 07:43 UTC) with the third pass's mirror commit.

Open question 5 now records that the 35 build test files reaching the graph
shard through the package re-export (992 tests collected) were not run on this
stacked branch in this pass either, and that for the graph-shard changes
specifically they are covered by #938's CI on `main` -- with the actual bucket
counts at two heads and no green verdict claimed, since that matrix is still
running.

One line of the committed probe changed and was not run: it read
`manifest.verification_epoch` unguarded, which would label an otherwise
complete before-tree run `STOPPED_AttributeError`. It now reads with a
`getattr(..., None)` default. No quoted figure is affected.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The third pass added one file the PR table had not counted --
`packages/microcosm-graph/tests/test_graph_verification_epoch.py` -- so the row
that said 27 files at `db7a3a0ba` is 28 at `3b63b8504`, and every part moved
with it: `packages/*/src` +1,750/-818 (the two epoch closes and their
comments), `packages/*/tests` 6 files +2,304, docs and `experiments/` 10 files
+2,497/-1, whole branch +6,551/-819. Re-derived against the merge-base
`363a9033b`, where two-dot and three-dot agree. Both earlier readings are kept
beside it as what they were at their own heads, with the one line that explains
each file-count step.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
A close that cannot take its post-validation signature records no memo
answer, and the borrow that follows pays the complete validation and
raises the refusal. The outermost close has no borrow after it -- the
memo is cleared as it returns -- so a roster file removed after that
close's own validation had returned ended the run clean. Before the
close took its signature through a guarded helper the same removal
escaped as a raw FileNotFoundError and failed the run, which the
guard swallowed.

The outermost close now pays that deferred validation itself, inside
the same translation block, whenever its signature moved or could not
be taken. The refusal is the one that validation raises -- SOURCE_ROSTER
for a removed roster file, the nested capsule's NATIVE_BINDING_REFUSAL
for a removed source -- never a code of the close's own, and a clean
signature costs nothing extra. asec_2024_native_population._epoch_exit
had the same shape and is fixed the same way; it reaches the window by
the other route, because _path_stat never raises, so a removed source
moves its signature rather than making it unavailable.

Both are pinned by a test per capsule that removes the file from a
profile hook at the return of the outermost close's own validation and
asserts the close raises exactly what an unmemoised borrow raises for
the same removal. Against the swallowing behaviour both fail with
DID NOT RAISE.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
…he old one

The adoption note in the boundary ledger still listed `_file_stats` over
the whole source roster inside the cheap tier each borrow pays, which is
the behaviour from before the roster stats moved into the signature. It
was the one sentence the previous sweep did not reach, because it lives
in a file that pass did not otherwise touch. The stats are read on every
borrow, into the signature, and what refuses is the complete validation
a moved signature runs. The same sentence now also says what a close
does when its own signature moved, including at the outermost one.

Nothing else on the branch still describes the cheap tier as comparing
roster or file stats: the design note's Tier B and its "comparison of
those roster stat identities" are the complete validation, which does
compare them, and the journal's line is historicized in place.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The two-run test asserted the node keys, the manifest key and the
content-addressed body, but not the JSON document, so a record that
leaked into `to_json` was caught only by the bare-manifest test beside
it. It could not assert the document outright: two runs differ in the
three run-level manifest fields and in the two `NodeReceipt` already
names as run-level, which is why the manifest key hashes receipts
without them.

It now compares the whole document with exactly those five removed,
taken by name and from `RUN_LEVEL_FIELDS` rather than from a list
written in the test, so a field that stopped being serialized raises
instead of quietly widening what the comparison hides. With the record
written into `to_json`, this test now fails on that assertion alone.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
(cherry picked from commit ec2b7b4)
… sit

The helper's note said all five removed fields are what the manifest's
own docstring calls the ones a run may change without changing what was
computed, and that this is why the key hashes each receipt without them.
That is the argument for `hit` and `wall_time` only. `started_at`,
`finished_at` and `host` are not receipt fields at all: they are absent
from the content-addressed body the key hashes, which is `nodes` and
`tier` alone.

The note now gives the two routes separately -- outside
`content_addressed` for the three, dropped by `_content_payload` for the
two -- so a reader checking it against manifest.py finds each claim where
the note says it is. No assertion changes.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
… describe it

The design note and the report both stopped at the inner close: a close
that cannot take its post-validation signature records no memo answer,
and the borrow that follows pays the complete validation and refuses.
Neither said what happens where no borrow follows. The outermost close
is that place -- the memo is cleared as it returns -- and until the
commit before this one a roster file removed after that close's own
validation ended the run clean, where the head before the guard failed
it with a raw FileNotFoundError.

Both now say that the outermost close pays the deferred validation
itself, in the same place and with the same error translation, that the
refusal is the one validation raises rather than a code the close
invents, that a clean signature costs nothing extra, and that the two
new tests assert the close raises exactly what an unmemoised borrow
raises for the same removal. The report also records the discrimination:
against the swallowing behaviour restored in place in this worktree,
both fail with DID NOT RAISE.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The report said the graph-shard file asserts the same three identities
-- the key, the JSON and the content-addressed body -- both against a
bare manifest and against a separate cold run. Against the separate cold
run it asserted the key and the body and not the JSON, and it could not
have asserted the document outright: two cold runs differ in the three
run-level manifest fields and in the two the receipt names as run-level.

The preceding commit makes the sentence true rather than trimming it:
the two-run test now compares the whole document with exactly those five
removed. The report says which five, why each is outside the key -- the
timestamps and the host are not in the content-addressed body at all,
and `_content_payload` drops the other two from every receipt -- and
that the test now asserts, rather than assumes, that two cold runs
differ in nothing else. It also records the mutant: with the record
written into `to_json`, this assertion is the one that fails, and the
report quotes what it prints.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The report's two mutation-test inventories, one per capsule, name every
test that pins an in-epoch refusal, and the pass that closed the
outermost-close window described its two tests in prose without adding
them there. Both inventories now carry them.

Each also says why these two assert the refusal once rather than twice.
The others assert it at the borrow after the mutation and again at the
close; a mutation made inside the outermost close has no borrow after
it, so the test asserts instead that the close raises exactly what a
later unmemoised borrow raises for the same removal -- which is the
identity the twice-asserting tests get from asserting both.

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The per-node source check answers from a stat signature, so the one
rewrite that signature cannot see -- same length, in place, every field
identical -- is a cache hit. The node proceeds, `_write_node` persists
its columns, frame, weights and cache record under the source identity
the run started with, and only the run-end re-derivation refuses. The
ContentStore had no way to remove an object, so that record survived the
refusal and the next `resume="auto"` run over the same store hit it: the
run returned values the source on disk does not imply. The inline
comments said the refusal precedes every `_write_node`, which is true
only of the rewrites the signature does see.

`ContentStore.recording_writes()` opens a ledger that collects the key
of every object published while it is open -- publications only, so an
object an earlier run already satisfied is not this caller's to remove,
while a `verify_existing=False` replacement is. Blocks nest and close by
identity. `ContentStore.evict(key)` renames the object out of the tree
before deleting it, so a reader sees the whole object or none, returns
whether one was there, and evicting twice leaves the same store as
evicting once. `run_graph` holds one ledger around the run and spends it
on the run-end refusal: everything the run published is evicted before
the refusal propagates. The eviction never replaces the refusal -- an
object that will not go is named in the refusal text, and the run still
refuses.

Both comments now state the guarantee the two checks actually make
together: a rewrite that moves any stat field refuses before the node
that would have persisted work derived from it; a rewrite that moves
none of them is refused at run end, after persistence, and everything
the run persisted is taken back.

Four executor tests and nine store tests. The stat-identical rewrite is
modelled by dropping `st_ctime_ns` from `_stat_identity`, because no
unprivileged process can hold that field still here -- which is exactly
why `test_a_rewritten_source_still_refuses_when_only_its_bytes_moved`
refuses at the node instead. The other four fields do in the test what
they do anywhere: an in-place same-length rewrite leaves device, inode
and size alone and `os.utime(ns=)` puts the modification time back.

Against the pre-fix executor and store, the two tests that matter fail
on the defect itself rather than on a missing attribute:

  packages/.../test_a_stat_identical_rewrite_refuses_at_run_end_...
  E   AssertionError: assert {'11/116c8180...ues.npy', ...} == set()

  packages/.../test_the_record_a_stat_identical_rewrite_left_is_not_reused
  E   AssertionError: assert True is False
  E    +  where True = NodeReceipt(key='92ade67d...', hit=True, ...).hit

No digest, key, seal or receipt value moves: eviction removes
content-addressed objects that are all re-derivable, and the refusal
path gains no `except` that could swallow it.

  $ uv run --no-sync pytest packages/microcosm-graph/tests
  610 passed, 2 skipped in 19.16s

Co-Authored-By: Claude Opus <noreply@anthropic.com>
`_object_stream`'s integer branch built its eight-byte length prefixes as
`filled.sum(axis=1).astype("<u8")` and then emitted `(lengths + 1).tobytes()`.
The cast is explicit but it is not what reaches `tobytes()`: `+ 1` is a ufunc,
and a ufunc's output carries the host's byte order whatever its operands
carried, so the array that is serialized is native-order again. On a
big-endian host this branch writes big-endian prefixes while the
`_update_scalar` loop it reproduces writes `int.to_bytes(8, "little")` and
`_framed` writes `<u8` -- three constructions, two orders. The comment above
the float branch already says the prefixes stay explicitly little-endian
because the loop writes them that way; the integer branch did not.

`lengths = (filled.sum(axis=1) + 1).astype("<u8")`, serialized directly. The
cast is now the last step, so it pins the order of the bytes that are actually
written.

Three cases pin it against a reference assembled in pure Python --
`len(payload).to_bytes(8, "little") + payload` -- for the integer, boolean and
float branches. Every parity assertion in this file compares one numpy
construction against another, including `_reference_update_series`, which is
itself a copy of the loop; a byte order both sides take from the host cancels
out of that comparison. A reference that takes its order from `int.to_bytes`
and its payloads from Python `bytes` cannot cancel that way.

What these cases do not do is discriminate here. This host is little-endian,
so `<u8` and native agree and the pre-fix code passes all 42 cases in this
file, the three new ones included -- which is the defect's whole shape. Forcing
the cast to `>u8` fails the new integer cases along with the existing parity
ones, so they are wired to the prefix bytes; on a big-endian host the pre-fix
code would fail them for the reason the fix addresses.

No emitted byte changes on a little-endian host, so no digest, key, seal or
receipt value moves here.

  $ uv run --no-sync pytest packages/microcosm-graph/tests
  618 passed, 2 skipped in 18.58s

Co-Authored-By: Claude Opus <noreply@anthropic.com>
`RunManifest.verification_epoch` is documented as a live view of the caller's
own mapping, because the record is finalised when the caller's verification
scope closes -- after `run_graph` has returned and before the caller reads the
manifest. `__post_init__` honoured that for a `dict` and for a
`MappingProxyType` and copied everything else through `dict(...)`. A caller
whose record is its own `Mapping` type -- the natural shape for an object a
scope keeps writing into -- therefore got a snapshot taken mid-run, reporting
a final validation count of zero, with nothing to distinguish it from a field
that was working: the same type, the same reads, silently stale.

`MappingProxyType` takes any object with the mapping protocol, so every
mapping is now wrapped rather than copied, and an already read-only proxy is
still passed through untouched. A type registered as a `Mapping` without
implementing that protocol cannot be held live; it is refused with a message
that says why, rather than accepted as the copy the contract forbids. The
class docstring says both.

Two tests. A `Mapping` subclass backed by a dict is mutated after `run_graph`
returns and the manifest follows it, exactly as the dict case beside it does,
and stays read-only through the proxy. The unwrappable type is refused. Against
the pre-fix constructor the first fails on the snapshot itself --

  E   assert 0 == 18

-- and the second on the opaque error a copy attempt raises instead:

  E   TypeError: '_NotSubscriptable' object is not iterable

Values are still checked once at construction, which is what a live view can
check; no key, digest, JSON field or receipt is touched, and the field remains
outside `content_addressed`, `key` and `to_json`.

  $ uv run --no-sync pytest packages/microcosm-graph/tests
  620 passed, 2 skipped in 17.97s

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The run-end block justified being inlined partly by "a build test counts calls
to that exact code object", naming no test. Nothing at this head references
`_source_paths_and_keys` outside executor.py, so a reader checking that claim
here finds nothing and has no way to tell whether the constraint is real or
left over.

The test is real and lives on the stacked branch `native-verify-once`:
`test_authenticated_cold_and_materialized_warm_clones_both_sources` in
`packages/microcosm-build/tests/test_us_graph_survey_population.py` registers a
`sys.setprofile` hook over a table of code-object ids that includes
`_source_paths_and_keys.__code__` under the label `source_key`, and times one
source-key pass per call/return pair. It profiles rather than asserts a count,
which the old wording also got wrong.

The comment now gives the file, what the hook does, and a grep that finds it,
and says plainly that nothing at this head references the helper outside this
module. It also leads with the reason that stands on its own here -- the
helper answers from the per-run cache this pass exists to bypass, and
re-resolves paths and re-checks codec availability that this pass has no
reason to repeat -- with the stacked shard's expectation second.

Comment only: no behaviour, no assertion and no emitted byte changes.

  $ uv run --no-sync pytest packages/microcosm-graph/tests
  620 passed, 2 skipped in 19.09s

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The mirrored comment arrived from the graph-only branch, where no test
references `_source_paths_and_keys` and the note therefore sends the reader to
the branch stacked on it. Here that branch is this one: the profiling test is
in this tree, and so are two more build tests that call the helper directly.
Left as it was, the comment would send a reader looking elsewhere for
something at their feet -- the same defect on this branch that naming the test
fixed on the other.

It now names
`test_authenticated_cold_and_materialized_warm_clones_both_sources` in
`packages/microcosm-build/tests/test_us_graph_survey_population.py`, says what
its `sys.setprofile` hook does, notes the two direct callers, and records that
the graph-only branch has none of them. Everything else in the four mirrored
commits is verbatim.

The changelog fragment also differs by necessity: this branch carries
`changelog.d/native-verify-once.changed.md`, not the graph-only branch's
fragment, so the eviction sentence was added to this branch's own fragment
rather than a second file being introduced.

Comment only: no behaviour, no assertion and no emitted byte changes.

  $ uv run --no-sync pytest packages/microcosm-graph/tests
  801 passed, 1 skipped in 47.22s

Co-Authored-By: Claude Opus <noreply@anthropic.com>
The run-end re-derivation is the only check that sees a rewrite leaving
every stat field identical, and a run that raises never reaches it: a
later kernel failure, a per-node refusal, a store error or an interrupt
unwound through the write ledger with the poisoned columns, frame,
weights and cache record still in the store under the pre-rewrite
identity, where the next resume="auto" run would hit them.

run_graph now runs the same cache-bypassing re-derivation on the way out
of any exception. Sources that still match leave the store alone, so a
failed or interrupted run stays resumable. A source that moved, a
re-derivation that cannot finish, or a failure before the run had source
keys evicts everything the run wrote. The original error is never
replaced; what happened is added to it as a note. The run-end pass marks
the run settled either way, so its own refusal is not re-derived twice.

What no in-process check covers is a process that stops without
unwinding after such a rewrite. That residual is now stated on run_graph,
at the per-node check, at the run-end pass and in the changelog fragment.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ve-verify-once (0d04abf)

The integration branch now carries main through 8c44daa, which includes
PR #938. This branch already mirrored #938 commit for commit, so the graph
package conflicts were wording only: two comment hunks in executor.py, where
this branch's text names the US build tests that exist here and the scalar
float packing this tree really uses, and the series-stream test's reference
body, which keeps the struct branch that matches it. Both sides were
compared file against file first: no non-comment line differs. PROGRESS.md
keeps both journals.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Closing as superseded: this head (c6e38d6) is an ancestor of #1011's head (3de7348, branch native-integration-20260923), so nothing here is lost. The branch is kept. Whether the graph-native US line itself continues is decision US-2 in the 2026-10-09 consolidation review.

@MaxGhenis MaxGhenis closed this Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant