Skip to content

Drive the artifact axis with a real cargo build and a real digest - #8176

Merged
briansrls merged 4 commits into
mainfrom
session/sharp-hawk-59-artifact-wet
Aug 12, 2026
Merged

briansrls merged 4 commits into
mainfrom
session/sharp-hawk-59-artifact-wet

Conversation

@gunbai-bot

@gunbai-bot gunbai-bot Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

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 ArtifactNotMaterialized and every axis-invalidation control was synthetic.

What lands

  • cargo.Build.BuildManifestMessages — cargo build --message-format=json. Its own operation rather than a flag on BuildManifest, because the flag is what makes stdout a 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.
  • A wet execution witness — writes a real minimal crate to a real mktemp dir, runs a real cargo build, decodes cargo's own stream, digests the file cargo named with real sha512sum. Excluded from hermetic discovery, enrolled in bin_witness_wet_entries at 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: digest manifest_path instead of the reported executable.

Claim Under the mutation
a_real_cargo_build_materializes_through_the_real_digest still PASS
the_digest_is_of_the_file_cargo_named_not_of_some_other_real_file FAIL

That 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.Dir projects path: FilePath straight from stdout, so it carries mktemp'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 reported manifest 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

gunbc-ci-auto-heal and others added 2 commits August 12, 2026 03:11
… 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>
@gunbai-bot gunbai-bot Bot changed the title SH-C: Bootstrap artifact materialization Drive the artifact axis with a real cargo build and a real digest Aug 12, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 12, 2026 04:09
gunbc-ci-auto-heal and others added 2 commits August 12, 2026 04:13
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>
@briansrls
briansrls merged commit 5f14c43 into main Aug 12, 2026
5 checks passed
@briansrls
briansrls deleted the session/sharp-hawk-59-artifact-wet branch August 12, 2026 10:02
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>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 12, 2026
This branch's ci was red only because main carried a broken import between
#8176 and #8187; the witness corpus here ran fully green.
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>
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