Repository navigation
Drive the artifact axis with a real cargo build and a real digest - #8176
Merged
Merged
Conversation
… named #8158 landed the decode: which executable cargo REPORTED for a target, read from its --message-format=json stream rather than assembled from target dir, profile and binary name. #8153 landed the carrier: ArtifactIdentity whose materialized arm holds a sole_constructor ObservedArtifactDigest over a validated Sha512Digest, so an artifact identity cannot be authored without an observation. This joins them. The chain is build -> the path cargo named -> a digest of THAT path -> ArtifactIdentity, and its load-bearing property is that one path VALUE flows through the digest and out to any later invocation. A convention-derived path breaks that at both ends simultaneously — digesting one file while executing another, each operation individually honest and the composition silently wrong — and it also makes the artifact axis a function of the build-configuration axis, collapsing two of the six axes #8153 exists to keep independent. DECOMPOSED SO THE DECISIONS ARE WITNESSED PER PR. Every decision sits in one of two pure folds — select_artifact_path decides what the build reported, materialization_from_digest decides what the observation means — with only a thin wet composition needed between them. One wet function carrying the decisions would put every refusal arm behind a cadence that does not run per PR, which is how a decision nobody executes reads as a decision that holds. Arms kept separate on purpose: a build that exited nonzero never reaches the stream decode at all, because "the candidate did not build" and "the candidate built but we cannot say what it produced" have different remedies and a later behavioural comparison must not read one as the other. No arm yields ArtifactMaterialized without a digest in hand; every failure projects ArtifactNotMaterialized, which promotion admission already refuses by name rather than treating as equivalent. 10 witnesses green by execution, compile-clean on both frontends. Mutation-probed: collapsing the build-failure arm reds exactly the three build claims, and making a path refusal fall back to a conventional path — the precise defect this lane exists to prevent — reds exactly the two claims that guard it. One receipt worth keeping: the fixture digest was hand-typed at first and was 130 hex characters instead of 128. The refinement rejected it, the observation became a refusal, and three positive claims went red rather than a malformed digest flowing into the carrier. The fixture is now a real sha512, reproducible with printf and sha512sum. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds cargo.Build.BuildManifestMessages (--message-format=json), the wet composition that runs it and digests the path cargo reported, and a wet execution witness that builds a real crate end to end. The digest is taken only for a reported path, and the match that produces the observation is what makes that structural rather than a discipline. 4 wet claims green by execution against a real cargo build; the 10 pure folds stay green. Mutation probe: digesting a different REAL file still materializes and only the new digest-identity claim catches it, which is the "a real digest of a real thing that is not the artifact" trap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The claim was added after the roster rows were written, so it was not in bin_witness_wet_entries. The file is excluded from hermetic discovery, so the ONE claim that catches a digest of the wrong real file had zero executing consumers — green with no coverage. Also marks it non-retiring: when the mint climbs to bind digest to subject, this becomes the executed evidence the higher rung stays real, not redundant machinery to delete alongside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The note forward-referenced a mint change that has since been measured and withdrawn, so it described a climb nobody is making. Replaced with the measured position: mechanically preventable, this claim IS the mechanism, three candidate walls abandoned with the reason each failed, and the next-rung trigger named as DESIGN 4b unforgeable construction rather than a locally coined one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 12, 2026
gunbai-bot Bot
pushed a commit
that referenced
this pull request
Aug 12, 2026
main is broken, not this branch. #8176 added dag/gunbc/self_host_artifact_materialization.dag importing ArtifactIdentity from v2.compiler.self_host.generation; #8177 then renamed that declaration to GeneratedArtifactIdentity without updating this consumer. My copy of the file is byte-identical to main's, so the compile-clean failure here is inherited rather than introduced. Swept the import, the return type, and both witness files together. THIS IS THE THIRD MAIN BREAKAGE OF THIS CLASS TODAY — Memory (#8129), ArtifactIdentity (#8153), and now the incomplete repair of that same rename. Two were new bare names colliding with existing ones, and this one is a rename that moved a declaration without its consumers. All three share a mechanism: a change is checked against the module it edits, while the breakage appears in modules it does not. Under namespace-only resolution that is mechanically decidable from the containment tree — a symbol that resolved before an edit and does not resolve after it is a whole-tree fact a lens can compute. Worth a lens rather than a fourth repair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls
added a commit
that referenced
this pull request
Aug 12, 2026
main is red. #8177 renamed `v2.compiler.self_host.generation`'s `ArtifactIdentity` to `GeneratedArtifactIdentity` and swept the consumers that existed when it was written. #8176 had added another one two minutes earlier, so the rename left it importing a name that no longer exists: dag/gunbc/self_host_artifact_materialization.dag:32:3: error: name 'ArtifactIdentity' not found in module 'v2.compiler.self_host.generation' Three lines in one file: the import, `artifact_identity_of`'s return type, and one prose mention that cited the retired symbol (a stale citation is the class DESIGN §3 names -- the name no longer resolves, so leaving it would rot silently). The two witness files need no change: they call `artifact_identity_of` qualified and match on `ArtifactMaterialized` / `ArtifactNotMaterialized`, which are VARIANT names a type rename does not touch. Discriminating control, both directions: pre-sweep -> exit 1, name 'ArtifactIdentity' not found in module post-sweep -> exit 0, PASS a_failed_build_does_not_materialize_even_with_a_good_digest Corpus scan: no other reference to the renamed symbol remains. The surviving `ArtifactIdentity` uses are `std.cache_interface`'s unrelated `ArtifactIdentity<T>` and `spark_serving_release`'s own `RuntimeArtifactIdentity` / `ModelArtifactIdentity` -- distinct concepts that share a word, which is what allowed the original collision. Not fixed here: #8176 and #8177 were each correct against their own base and each passed their own CI. The break exists only in their composition, so no affected set either author could compute would have shown it. Claude-Session: https://claude.ai/code/session_01XxLzhuMisV3GAPpP2mBtSG Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
6 tasks
briansrls
added a commit
that referenced
this pull request
Aug 12, 2026
…ssions (#8132) * PRESS-0 step 3: dispatch admission asks whether a PROCESS is running, not whether a tmux name exists A retained remain-on-exit pane — the exact artifact the spawn path creates — answered "live" to belt_node_session_live, so a click returned DispatchAlreadyLive through the accepted band: a positive acknowledgement, no agent, and no refusal anywhere to count. That is worse than the selection refusal beside it, because an accepted no-op has zero observable frequency by construction (DESIGN §5, the absorbing fallback wearing an idempotence label). No new observer was built. The pane probe, the pane-to-evidence projection (worker_process_from_attempt_pane) and the per-session join (worker_process_for_session) already existed and already carried the refusal semantics, and the sessions UI route was already using them via belt_sessions_observe_for_instance. The defect was that the two ACTUATION sites — belt_tick_for_instance and belt_dispatch_node_for_instance — read the ls-only observation whose worker evidence is the placeholder whose own reason reads "tmux name presence does not observe pane liveness or exit". One concept had two answers in one module (§3); this routes both actuation sites through the answer that observes. Liveness is four-valued, not Boolean: Running may suppress a spawn; StaleTerminal is refused and never suppresses the new turn; LivenessUnobserved refuses rather than spawning blind, because a spawn under unknown liveness is how one node acquires two workers; Absent is the ordinary spawn path. Collapsing Unobserved into either pole is the state-space conflation that produced this defect one level down. Witnesses: the prior witness_node_session_live_true asserted exactly the behaviour that is now wrong, so it is replaced rather than kept beside the new arms. The discriminating red is witness_retained_dead_pane_is_not_live, which returned true under the old check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix: render the exit code with to_string, the idiom this module and its sibling already use int_to_string exists only in src/v1/02_parse.dag, takes value: rather than n:, and no module under dag/gunbc/ calls it — the sole occurrence of int_to_string(n:) in that directory was the line this fixes. dispatch_process_cleanup_separation_note's own retained-pane message renders the same field as to_string(exit_code). Also restructures the liveness filter to the let-then-first form worker_process_for_session uses, rather than an inline parenthesized pipe-then-method that appears nowhere in the corpus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Admit the two new dispatch arms into the wire-roster size pin dispatch_button_terminal_states maps over belt_dispatch_all_status_labels, so button totality and the terminal count both follow the roster by derivation and needed no edit. What failed is the third clause of witness_dispatch_button_states_total_over_wire: the absolute roster size, pinned so a new arm cannot enter the wire vocabulary without a conscious update to the surface that renders it. The subject genuinely grew by two (stale_session, session_liveness_unobserved), so the pin moves 8 -> 10. The pin is doing exactly its job here and is not being loosened: it is a controlled fixture over a hand-authored closed roster, and it stopped a wire-contract change from landing silently. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Announce a population budget refusal before the work that can swallow it A budget exhaustion killed the CI floor and said nothing. Run 31474198106 on PR #8132 ran the floor step for exactly 55m01s against gunbc_ci_ordinary_floor_budget_minutes = 55, exited 1, and emitted no OVER-BUDGET line anywhere in the attempt log. The step's own output stops mid-measurement three minutes before the process dies. The wall itself is correct and is not being weakened here — only its announcement is. A fail-closed mechanism whose diagnostic is missing is worse than a loud one: the refusal still fires, but readers cannot see why, so they reach for whatever number is nearby. In this incident that was a cgroup peak RSS reading sitting next to the silence, and the failure was diagnosed as a memory kill that never happened — by me, in the PR thread, before the operator corrected it. That is the cost being paid: the silence does not just withhold a cause, it manufactures a wrong one. TWO CANDIDATE LOSS MECHANISMS, both closed by this ordering without having to decide between them. The receipt write ran first and its path was interpolated into the message, so a stalled write under a loaded runner delays the only announcement past process death. And the message took an explicit stderr lock guard held across the write — a lock the main thread holds while streaming its own receipt phases, which is precisely what the floor was doing in the window this fired in. A watchdog must never wait on a resource held by the thread it polices. So the announcement now goes first: a ::error:: annotation on stdout so the cause reaches the run summary rather than only line ~7000 of a step log, the detail line on stderr, both flushed, and the receipt write afterwards where failing to write it can no longer suppress the diagnosis. NOT ESTABLISHED, stated so the next reader does not inherit a guess: which of the two mechanisms actually lost the message. Neither was reproduced. The fix is ordering that makes both harmless, not a repair of a located defect. Also merges origin/main (d3203de). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Heal main's two §3 name collisions from #8129 (compute_board) #8129 landed `Memory` as a `BoardCapability` variant and `unsatisfied_capabilities` in `product.compute_board.composition`. Both names already existed corpus-wide — `std.measure` `Memory` and `gunbc.auth.github_credential` `unsatisfied_capabilities` — so under namespace-only resolution the bare references stopped resolving uniquely and the whole-tree compile went red in four files: dag/gunbc/ci_budget_tree.dag unresolved type 'Memory' dag/product/budget_tree.dag unresolved type 'Memory' dag/test/claim/budget_tree_witness_test.dag dag/test/claim/github_app_registry_witness_test.dag 'unsatisfied_capabilities' not found It was invisible on main because every push run there was cancelled, so no floor run completed on the merge commit. The newer, narrower names move: `Memory` -> `MemoryCapability`, `unsatisfied_capabilities` -> `unsatisfied_board_capabilities`. The long-standing authorities keep their names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * PRESS-0 step 1: dispatch reads back the deployed revision before it acts gunbc.fleet_desired_observe fleet_revision_standing already decided the revision cell three ways, and its only consumer was gunbc.fleet_converge_cli. So convergence knew srv1 was drifted while the served page dispatched against the drifted tree anyway and said nothing — a worker spawned from code the fleet never admitted, reported as an ordinary spawn. The standing is now consulted FIRST in belt_dispatch_node_for_instance, before node lookup and before session observation, because the roster, the signoff and the command all come from that tree; refusing later would mean refusing after acting on it. Both non-converged arms refuse: DispatchRevisionDrifted carries BOTH revisions (the membership-diff `from` ruling — a catch-up needs the prior) and DispatchRevisionUnobserved carries its cause. Neither reaches the ok band. Serve maps drift to 409 and unobserved to 503. The repo path is instance.repo_root, not the srv1_gunbc_repo_root constant converge uses — hardcoding srv1 would mint a second path authority and answer for the wrong host on any other instance. SCOPE: the new witnesses are projection-grain — never-ok, loud band, drift names both revisions, unobserved names its cause. Whether the gate FIRES on a live drifted srv1 is a wet fact and is owed a wet receipt; it is not established here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Extend the band-fold vocabulary pin to the four new dispatch labels Run 31528394784 red batch 4: witness_band_fold_pins_operator_vocabulary pinned belt_dispatch_label_band_rows().length() == 8, and dashboard_instance_dispatch_contract_keystone_holds failed only as its conjunct. The four arms added by step 3 (stale_session, session_liveness_unobserved) and step 1 (deployed_revision_drifted, deployed_revision_unobserved) each get their band assertion; all four are loud, so the ok set is unchanged and witness_ok_labels_derive_from_band_fold still holds. This is the second roster I missed after adding arms — I fixed d4_wire_contract_labels and the button wire pin but not this one. Swept the class this time: the only other pin over these rosters is roadmap_sandbox_witness_test line 75, which is relative (all_status_labels().length() - 1) and self-adjusts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Extract the revision-admission fold, refuse duplicate sessions, delete the Boolean collapse Handback items 2, 3 and 5 from review artifact 51237. EXTRACT THE GATE (item 4). The revision check lived inline in dispatch, so every witness constructed DispatchRevisionDrifted directly and asserted its label, band and JSON — which would stay green if the fleet_revision_standing match were deleted outright. A test that survives removal of the thing it tests is not testing it. belt_revision_admission is now a pure FleetRevisionStanding -> BeltRevisionAdmission fold with the effectful read left at the call site, driven through all three arms by witness. It is also the shared seam the tick path will consume. 0/1/MANY (item 3). belt_node_session_liveness filtered by node id and took first(), so two sessions claiming one node resolved to whichever tmux listed first: [running, stale] answered running, [stale, running] answered stale. Same observation, two orders, two verdicts, no signal a choice was made. It now refuses with BeltSessionMultiplicityConflict naming every session, routed to a new dispatch arm, loud band, 409. The discriminating witness asserts order-independence directly. DELETE THE BOOLEAN (item 5). belt_node_session_live answered Bool over a four-state lifecycle, mapping StaleTerminal, LivenessUnobserved and Absent all to false though the module's own note says those three have different remedies. It had no production caller. Its four witnesses are re-pointed at belt_node_session_liveness and are STRONGER for it: each now asserts the exact arm rather than mere falsity. Rosters swept together — exemplars, d4_wire_contract_labels, the band-fold pin and the button wire pin all read 13. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Make the scheduled belt tick lifecycle-aware and revision-gated Handback P0s 1 and 2 from review artifact 51237: the dead-pane fix and the revision gate reached the click path only, and the timer is a dispatch actuator exactly as much as the button is. The tick passed the whole observed session list into belt_reconcile, which maps EVERY present row to an observed member and counts live.length() as occupied. So a retained dead pane stayed a member (desired + present = Unchanged, no respawn) AND consumed a capacity slot. It also never consulted the deployed revision, so a drifted host refused a click while spawning on a timer. Both paths now consume belt_revision_admission. A refused revision blocks spawn and reap ONLY — verification and publication continue, because they read independently bound receipts and already run when tmux observation refuses; stopping them would widen a spawn-admission refusal into a work stoppage. Cleanup precedes start, deferred one tick. A stale pane leaves observed membership, so its node becomes a spawn candidate — but spawning it this tick would collide with the tmux session name that still exists, because spawn runs before teardown. So the stale node's spawn is withheld and its teardown planned; the next tick observes it Absent and spawns normally. That is why this is a partition plus a deferral, not a filter. LivenessUnobserved and MultiplicityConflict are neither cleaned nor spawned. An unobserved pane might be running, so teardown could kill live work and spawning beside it could double-run the node. Witnessed at the belt grain rather than on a helper Boolean: the partition the tick feeds to belt_reconcile puts running, stale and unobserved in three different buckets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix the witness import block I split in the wrong place (review 51277) CONFIRMED, and it was the worst possible failure for this PR. My edit inserted the fleet_desired_observe import in the MIDDLE of the roadmap_belt_actuate import list, closing that block one line early. So DispatchRevisionDrifted, DispatchRevisionUnobserved, belt_dispatch_result_ok, belt_dispatch_status_label, belt_dispatch_result_json_value, the label/band/exemplar roster accessors and the preflight symbols all sat inside `import gunbc.fleet_desired_observe { … }` while being defined in roadmap_belt_actuate.dag. The module could not resolve its imports, so NONE of the new evidence would have run — the revision-admission fold, the multiplicity conflict, the tick partition, all of it. A PR whose entire claim is "these gates are proven by execution" would have merged with zero executing witnesses. That is specification-without-execution exactly, and it is the failure mode I have been quoting at other people all session. fleet_desired_observe now exports only RevisionConverged / RevisionDrifted / RevisionUnobserved. The belt_actuate symbols are back in their own block, and I merged the two rather than leaving a split import authority for one module. Every imported symbol re-verified against its defining module by name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Make the tick honor its own lifecycle note (review 51285) BOTH FINDINGS CONFIRMED, and the first is the worse kind: the note described behaviour the code did not have. `classified.refused` was partitioned and never read again, so only stale node ids were withheld from spawn. A ready node with an unobserved-but-present session therefore read as ABSENT to belt_reconcile and could be spawned beside a pane that might be running — the exact double-run the click path's DispatchSessionLivenessUnobserved arm exists to prevent, and exactly what my own note said was prevented here. The deferral set is now every non-running class. MULTIPLICITY IS A NODE FACT, NOT A SESSION FACT, which is why the first cut missed it: belt_tick_classify_sessions read each row's process state in isolation, so two rows for one node (one running, one stale) landed in two different buckets and the reconciler acted on an ambiguity dispatch refuses. The partition is now node-aware — more than one row for a node is a conflict whatever the rows say individually — so both actuation paths refuse the same ambiguity. Witnessed: an unobserved session yields no running row and no spawn candidate; two rows for one node classify neither as running. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Heal main's ArtifactIdentity collision (#8153), inherited via the main merge CI on press-0 failed with `unresolved type 'ArtifactIdentity'` in three files this branch never touched — realize_kernel_test, hermetic_fixture_realization_test, reconcile_in_process_cache_test. The cause is a §3 name collision, the same class as #8129's `Memory`. `dag/std/cache_interface.dag` has carried `ArtifactIdentity<T>` for a long time; #8153 added a bare `ArtifactIdentity` in `src/v2/compiler/self_host/generation.dag`. Under namespace-only resolution the long-standing bare references stopped resolving uniquely, so the consumers of the ORIGINAL type broke while the new declaration compiled fine. main still carries it and no open PR is fixing it, so this is not a duplicate of someone else's repair — I checked first, having duplicated #8151 earlier today. The newer, narrower name moves: `SelfHostArtifactIdentity`. The cache-interface authority keeps its name and its consumers resolve again. Swept its three files together — the declaration, both self_host witnesses, and the field and function signatures that referenced it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Sweep GeneratedArtifactIdentity into the consumer #8177 left behind main is broken, not this branch. #8176 added dag/gunbc/self_host_artifact_materialization.dag importing ArtifactIdentity from v2.compiler.self_host.generation; #8177 then renamed that declaration to GeneratedArtifactIdentity without updating this consumer. My copy of the file is byte-identical to main's, so the compile-clean failure here is inherited rather than introduced. Swept the import, the return type, and both witness files together. THIS IS THE THIRD MAIN BREAKAGE OF THIS CLASS TODAY — Memory (#8129), ArtifactIdentity (#8153), and now the incomplete repair of that same rename. Two were new bare names colliding with existing ones, and this one is a rename that moved a declaration without its consumers. All three share a mechanism: a change is checked against the module it edits, while the breakage appears in modules it does not. Under namespace-only resolution that is mechanically decidable from the containment tree — a symbol that resolved before an edit and does not resolve after it is a whole-tree fact a lens can compute. Worth a lens rather than a fourth repair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Pin HTTP status for all five new dispatch arms (review 51368) CONFIRMED. roadmap_serve.dag maps the five new arms explicitly — stale session, revision drift and multiplicity conflict to 409, liveness unobserved and revision unobserved to 503 — but witness_dispatch_status_total still pinned only the original eight. A regression mapping any new arm to 200 would have stayed green. Worse than an absent pin, because status_map_red_note claims each refusal variant maps to ITS status so a collapse reds exactly one row. A reader checking whether the mapping is guarded found a sentence saying yes while the assertion had stopped covering half the arms. WHY THIS ONE WAS MISSED when the label, band, exemplar and button rosters were all swept: it lives in a different witness module and is keyed by CONSTRUCTED variants rather than by a roster the compiler counts, so nothing goes non-exhaustive when an arm appears. That asymmetry is the real defect and it is recorded on the note — until this assertion derives from the exemplar roster the way the label set does, it has to be swept by hand beside it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Brian Searls <11205878+briansrls@users.noreply.github.com>
briansrls
pushed a commit
that referenced
this pull request
Aug 12, 2026
#8177 renamed the self-host coproduct to GeneratedArtifactIdentity to end its collision with std.cache_interface's record. #8176 merged an hour earlier and added a consumer importing the old name, so neither PR could see the other and main went red on whole-tree compile-clean: dag/gunbc/self_host_artifact_materialization.dag:32:3: error: name 'ArtifactIdentity' not found in module 'v2.compiler.self_host.generation' Rename at the import, the return type, and the note that names the chain. Both witness families over the affected module pass by execution. Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #8172 (the pure folds). This is the half that makes the axis derived rather than derivable: before it, nothing in the tree digested a built artifact, so every generation carried
ArtifactNotMaterializedand every axis-invalidation control was synthetic.What lands
cargo.Build.BuildManifestMessages—cargo build --message-format=json. Its own operation rather than a flag onBuildManifest, because the flag is what makesstdouta message stream instead of human text: it is constitutive of the output contract, not a caller preference.materialize_artifact_wet— the only function in the module that reaches the host. Three service calls, no decision the pure folds don't already make, so the hermetic corpus keeps every arm under per-PR execution and the wet cadence only has to prove the real tools reach them.bin_witness_wet_entriesat function grain.The digest is taken only for a path cargo reported
The match producing the observation is what makes this structural. The non-reported arms have no executable to hand sha512sum, so they yield a refusal saying exactly that — discarded by the mapper, but true as written, so a later edit that did consult it stays fail-closed instead of naming a file nothing observed.
Evidence
4 wet claims green against a real cargo build (~0.5–0.8s each); the 10 pure folds stay green.
Mutation probe, and it found a real gap first. My initial three wet claims did not discriminate whether the digest was of the reported file — they checked the path the outcome carried, which the mutation didn't move. So I added
the_digest_is_of_the_file_cargo_named_not_of_some_other_real_file, then planted the defect: digestmanifest_pathinstead of the reported executable.a_real_cargo_build_materializes_through_the_real_digestthe_digest_is_of_the_file_cargo_named_not_of_some_other_real_fileThat top row is the point. A wrong-but-real file produces a real digest of a real thing that is not the artifact, and a materialization claim cannot tell. Only the identity claim can.
Two receipts kept in-file rather than fixed silently
shell.Mktemp.Dirprojectspath: FilePathstraight from stdout, so it carriesmktemp's trailing newline. Interpolating it puts a newline inside the path and every later write refuses — this witness first failed at the fixture write, nowhere near its subject. Trimmed at the call site with the modeling gap named; the shared operation is consumed raw elsewhere, so changing it is its own change, not a rider.\{is the escape for a literal brace, so"\{root}/Cargo.toml"is the literal text{root}/Cargo.toml. It wrote a file by that name and cargo reportedmanifest path {root}/Cargo.toml does not exist. Both halves were individually honest.Explicitly not claimed
Each claim builds its own crate in its own temp dir, so this does not witness one-build-one-artifact — that control is about reusing an artifact across invocations, and every claim here deliberately starts from an empty target dir. Also outstanding: direct invocation via
WitnessBin.Run, the warm-marker control, the per-axis invalidation RED owed to #8153, and migrating the four convention-derived callsites.Lane: SH-C.
🤖 Generated with Claude Code