Skip to content

Rename the self-host ArtifactIdentity: two concepts shared one name, and it only broke whole-tree - #8177

Merged
briansrls merged 2 commits into
mainfrom
session/eager-cat-841-artifactidentity
Aug 12, 2026
Merged

briansrls merged 2 commits into
mainfrom
session/eager-cat-841-artifactidentity

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

Main is red on whole-tree compile-clean. This is the root fix, and it is not caused by the PRs currently reporting it.

The defect

ArtifactIdentity is declared twice, as two unrelated concepts:

  • std.cache_interface ArtifactIdentity<T> — a long-standing record {subject_digest, artifact_kind} the cache and realization corpus is built on.
  • v2.compiler.self_host.generation ArtifactIdentity — a coproduct ArtifactMaterialized | ArtifactNotMaterialized, introduced by SH-A: Behavioral bootstrap and promotion authority #8153.

One name, two authorities: the DESIGN §3 nicknaming violation in its collision form.

What it breaks

Three import-less witnesses reference the name bare — dag/test/claim/realize_kernel_test.dag, reconcile_in_process_cache_test.dag, hermetic_fixture_realization_test.dag. Once both declarations share a pool the reference is ambiguous and resolves to nothing:

dag/test/claim/hermetic_fixture_realization_test.dag:44:12: error: unresolved type 'ArtifactIdentity'
dag/test/claim/realize_kernel_test.dag:15:15: error: unresolved type 'ArtifactIdentity'
dag/test/claim/reconcile_in_process_cache_test.dag:44:12 (and :53, :62)

Why it stayed hidden — the part worth keeping

It manifests only whole-tree. #8153's own PR run and main's post-merge run both carried .dag-only diffs, so compile-clean ran scoped to affected shard-entry closures, where only the cache declaration is reachable and the name is still unique.

Executed evidence for that reading, rather than inference: on a branch that does contain the new declaration, realize_kernel_test witness_verified_hit_reuses resolves and returns true at entry-closure scope. The failure appeared the moment two unrelated PRs touched a .rs file and thereby forced the whole-tree baseline. One of them (#8173) changes no .dag at all — which is what proves neither PR caused it.

The fix is at the root

The three witnesses did nothing wrong; patching them would leave the collision live for the next bare reference. Renaming the newly-introduced type restores global uniqueness — 7 sites across 3 files, all landed today.

Green by execution

The renamed self_host_generation_identity_witness_test.dag generation_identity_agrees_with_itself returns true.

Not proven by this PR's own CI

This diff is .dag-only, so its compile-clean runs scoped and does not exercise the whole-tree pool where the collision lives. The whole-tree control is any PR touching a .rs file — #8167 and #8173 both qualify and are both currently red on exactly these five errors. They are the proof once this lands.

🤖 Generated with Claude Code

https://claude.ai/code/session_018ZrxLJrt8ASGLegiK72fAi

gunbc-ci-auto-heal and others added 2 commits August 12, 2026 04:26
…and it only broke whole-tree

`ArtifactIdentity` is declared twice in the corpus as two unrelated concepts:
`std.cache_interface` `ArtifactIdentity<T>`, a long-standing record
`{subject_digest, artifact_kind}` that the cache and realization corpus is built
on, and `v2.compiler.self_host.generation` `ArtifactIdentity`, a coproduct
`ArtifactMaterialized | ArtifactNotMaterialized` introduced by #8153. That is the
§3 nicknaming violation in its collision form — one name, two authorities.

WHAT IT BREAKS. Three import-less witnesses reference the name bare:
`dag/test/claim/realize_kernel_test.dag`, `reconcile_in_process_cache_test.dag`,
and `hermetic_fixture_realization_test.dag`. Once both declarations sit in one
pool the reference is ambiguous and resolves to nothing, so compile-clean refuses
with five `unresolved type 'ArtifactIdentity'` errors.

WHY IT STAYED HIDDEN, which is the part worth keeping. It manifests ONLY
whole-tree. #8153's own PR run and main's post-merge run both carried `.dag`-only
diffs, so compile-clean ran scoped to the affected shard-entry closures — where
only the cache declaration is reachable and the name is still unique. Executed
evidence for that reading rather than inference: on a branch that DOES contain the
new declaration, `realize_kernel_test` `witness_verified_hit_reuses` resolves and
returns `true` at entry-closure scope. The failure appeared the moment two
unrelated PRs touched a `.rs` file and thereby forced the whole-tree baseline; one
of them (gunbc#8173) changes no `.dag` at all, which is what proves neither PR
caused it.

THE FIX IS AT THE ROOT, not the call sites. The three witnesses did nothing wrong,
so patching them would leave the collision live for the next bare reference.
Renaming the newly-introduced type restores global uniqueness: 7 sites across 3
files, all landed today.

Green by execution: the renamed
`self_host_generation_identity_witness_test.dag`
`generation_identity_agrees_with_itself` returns `true`.

NOT PROVEN BY THIS PR'S OWN CI: this diff is `.dag`-only, so its compile-clean runs
SCOPED and does not exercise the whole-tree pool where the collision lives. The
whole-tree control is any PR touching a `.rs` file — gunbc#8167 and gunbc#8173 both
qualify and both currently red on exactly these five errors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ZrxLJrt8ASGLegiK72fAi
`git add -u` swept in a lockfile regeneration that has nothing to do with this
change: main's `Cargo.lock` lists `serde`/`serde_json` under `v1-compiler-tests`
and cargo removes them whenever it rewrites the file in a clean worktree. That is
pre-existing drift on main, not a consequence of renaming a type, and a fix for a
red tree should be exactly the fix — nothing bundled that a reviewer has to
separately reason about (review 51314 spotted it).

Reverted to main's `Cargo.lock`. If the drift is real it deserves its own change
with its own justification.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ZrxLJrt8ASGLegiK72fAi
@briansrls
briansrls merged commit 9b79271 into main Aug 12, 2026
5 checks passed
@briansrls
briansrls deleted the session/eager-cat-841-artifactidentity branch August 12, 2026 10:04
gunbai-bot Bot pushed a commit that referenced this pull request Aug 12, 2026
All three conflicts are the ArtifactIdentity collision I healed in
d3e42a1, which main healed independently in #8177. Main chose
GeneratedArtifactIdentity where I chose SelfHostArtifactIdentity, so all
three files resolve to main's side and my rename dissolves.

That is the second time today my repair of a main breakage raced a
parallel fix (the first was #8151's Memory rename). Both times the
collision was real and unfixed when I checked, and both times someone
else was fixing it in the same window — the check that would actually
prevent the duplicate work is looking for an in-flight PR touching the
same symbol, not just the state of main.

Verified after resolution: the belt work is intact (revision admission,
tick partition, multiplicity arm all present), and the three generated
artifacts main touched resolved to main's side byte-for-byte.

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
classify_source returns FrontendStage; p_hex_escape and its five siblings still declared
-> String, so every one of them was a type error waiting for the next compile-clean run. I
changed the classifier's result type and updated its structured consumers without checking the
six one-line probes at the bottom of the same file.

Found by the planning side reading the file, not by me re-reading what I had edited.

Also merges origin/main, which now carries #8177 — the rename of the OTHER ArtifactIdentity.
That PR touches self_host_generation_identity_witness_test, self_host_promotion_admission_
witness_test and self_host/generation.dag: exactly the files I retracted imports from on #8149,
and none of the three I kept. It confirms the homonym split rather than superseding those edits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBBegUkJygQyr1zMHiv2eK
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
Brings in the ArtifactIdentity collision repair (#8177 + #8187) that main's
whole-tree compile-clean was red on, plus the intervening merges. This PR's red was
that inherited defect, not its own content: the identical five `unresolved type
'ArtifactIdentity'` errors appeared on a sibling PR containing no `.dag` change at
all, and then on main itself at #8146.

DESIGN.md was the only conflict, and it is resolved to main's copy deliberately:
it is a PROJECTION of `dag/gunbc/design_document.dag`, which merged cleanly. Hand
-merging a generated artifact would author bytes no authority produced; the
`heal_generated_artifacts` job re-projects it from the merged authority, as it
already did on this branch in f7ddaef.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ZrxLJrt8ASGLegiK72fAi
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>
gunbai-bot Bot pushed a commit that referenced this pull request Aug 12, 2026
Four conflicts, all expected from the stack.

The three self_host files are my SelfHostArtifactIdentity rename, which
main healed independently as GeneratedArtifactIdentity in #8177 — main's
name wins and mine dissolves, same resolution press-0 took.

roadmap_belt_actuate.dag is the real one: #8132 carried the string-arm
partition helper to main, and this branch replaces it with the coproduct
version (review artifact 51309). Kept this branch's side, after checking
that every main-only line it drops is something deliberately removed or
corrected here rather than main content going missing — the dead
belt_observe entry points and the stale "8th variant" roster note, both
from the dissolve commit that landed on this branch rather than press-0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Aug 12, 2026
* wip: exact-head frontier probe + durable classification witness

* Re-cut the native-selected-witness-bundle first_slice on exact-head re-observation

* Home the reproduction instruments outside the test module, and stop relabelling genuine body-lowering rejections as retention

* Wire the production '//' annotation channel into dag_lex_rules, with LexArtifact-grain controls

* Record the two repairs against the exact-head observation they were measured from

* Close both evidence gaps: a constructible rejection-propagation pair, and comment newline termination

* Close the review findings: complete semantic-erasure controls, honest partial receipt, one consistent roadmap sequence, scratch residue deleted

* chore: regenerate drifted generated artifacts (ci auto-heal)

* Report the retention population as 4-at-observation and 3-after-repair, and render the rejection arm

* Close the second review round: classifier controls, Rejected-arm assertion, read contract recorded not fabricated, roadmap by time grain, resolve-ready naming

* Delete rejected_reasons, dead since the assertion moved onto the Rejected arm

* Fix all three floor refusals: brief budget via cited note, doc-graph link, and over-budget stage claims deleted with the gap recorded

* Bring the boundary brief back under its 100-word budget, and attribute two walls by minimal pair

* chore: regenerate drifted generated artifacts (ci auto-heal)

* Record the refuted attributions where they were asserted, not only where they were measured

* Make two latent bare-reference dependencies explicit (Class B, exposed by this diff's compile-clean closure)

* Complete the ArtifactIdentity bare-reference population: six files across four lanes

* Retract three of six imports: two homonyms and a suffix match, not one semantic population

* Complete the three partial import lists that a single named import had left half-bound

The 13 witness failures at eff3eaa are caused by the repair, not exposed by it. All three
modules were fully bare — every name resolved by pool-membership coincidence (DESIGN Class B).
Naming ONE dependency changed the closure that gets assembled, and the extdeps catalog rows two
of the witnesses read stopped resolving: `no such function: hermetic_fixture_file_facts`, etc.

A partial import list is the worst of the two states: it neither binds the module's dependencies
nor leaves the coincidence intact. So the repair is the whole list, and every symbol home below
was verified at its declaration rather than by name search — std.cache_identity for the artifact
kind ids and the receipt digest, std.cache_interface for the kernel types and route/write folds,
extdeps.cache + extdeps.cache.types for the catalog projections, and the per-family extdeps
realization modules for the rows themselves.

realize_kernel_test stayed GREEN under the partial list its two siblings went red under, and is
completed here anyway. Green under a bare reference is not evidence the reference is bound; it
is evidence that this run's closure happened to contain it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBBegUkJygQyr1zMHiv2eK

* Re-home three key-derivation variants I had imported from a module that only re-imports them

ContentAddressedByValue, HandAuthoredString and NativeInternalHash are declared on
std.cache_interface KeyDerivationClass; extdeps.cache.types merely imports them at the top of
its own file. I read those lines as declarations because a name-ranked search returned them,
which is the third instance in this PR of the same failure — a name-level observation standing
where a bound declaration was required, the exact class the PR's subject is about.

Every one of the 54 imported names across the three files is now checked against a declaration
form (col-0 type/fn/data, or a variant on its coproduct) rather than against an occurrence.
StructuredArtifact is declared inline on `type ValueShape` and is correct as imported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBBegUkJygQyr1zMHiv2eK

* Move two receipt-digest names into the block that declares them, and check the written file

ExecutionReceiptDigest and execution_receipt_digest_of_value are declared on std.cache_identity;
I wrote them into the std.cache_interface block. The previous commit's verification did not catch
it because it checked a list of (name, intended module) pairs I held in mind, not the pairs the
file actually contains — so it confirmed the homes I had already re-derived and said nothing
about where the import statement put them.

The check now parses the three files' import blocks and tests each (module, name) pair as
written against a declaration in that module's source. 71 pairs, no remaining mismatch; the two
StructuredArtifact flags are the checker's own blind spot for inline variant lists
(`type ValueShape = RawBytes | StructuredArtifact | ...`), confirmed correct by direct read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBBegUkJygQyr1zMHiv2eK

* Close the frontend probe's result space and make census completeness an identity join

Increment 2 of the frontier lane, delivering the two structural halves and NOT the third.

CLOSED RESULT SPACE. classify_source answered in String, so every consumer compared against a
literal and a typo produced a verdict rather than a refusal. It now returns FrontendStage =
LexRefused | ParseRefused | NormalizeGraftRefused | NormalizeRetained | NormalizeOther |
NormalizeAccepted, with frontend_stage_label the single rendering authority.

THE EMPTY-REASON-SET ARM IS GONE BECAUSE ITS INPUT IS. The classifier took List<Symbol> and had
to answer NORM_REASON_SET_INVALID_EMPTY for the empty list. It now takes NonEmptyDiagnostics,
whose head is not optional, so that input has no representation. The RED that fed it an empty
list retires with the state rather than with the wall — the dissolution rule preserves a control
when the invalid state stays writable, and this one cannot be authored at all without re-widening
the parameter to admit the value it tests. Recorded in the file rather than left to be inferred
from a deleted test.

COMPLETENESS IS A JOIN. Fifteen hand-written m_* fns had no denominator: drop one and the
remaining fourteen still answer, so 'all fifteen observed' only ever meant 'fifteen fns were
present'. They are now rows on frontier_roster, and census_is_complete asks per roster label
whether a matching observation exists. Three controls, including one that a count equality would
pass — fifteen observations, none of them a roster member.

NOT DELIVERED, and it is the increment's headline item: the live fifteen still cannot gate a PR.
dag_grammar() construction alone exceeds the fast-lane budget before any member is read, so
observe_roster stays a reproduction instrument in a non-test module and the durable claims run on
synthetic specimens. The two stage-separation claims deleted last increment are still owed to the
batched or realized instrument; what changed is that it now has a total verdict type and a
denominator to join against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBBegUkJygQyr1zMHiv2eK

* Type the five empty list literals that fold_list's generic accumulator cannot infer

compile-clean refused all five `empty: []` arguments: fold_list's empty parameter is the generic
accumulator A, so a bare literal has no expected type to be a collection of. The corpus idiom is
an explicit cast, used the same way in gunbc.package_delivery.

Diagnostic returns to the witness import list — it was dropped as unused when the specimen helper
stopped naming it, and the cast names it again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBBegUkJygQyr1zMHiv2eK

* Reconcile the census in both directions: coverage was only the superset half

census_is_complete asked, per roster label, whether SOME observation carried it. That is the
covering direction alone, and I described it as an identity join, which it was not. A census of
all fifteen members PLUS a stray sixteenth passed it, and so did one carrying a member twice —
neither condition can make a per-label existence check fail.

reconcile_census now joins both directions and at multiplicity, returning CensusReconciliation =
CensusExact | CensusMissingMember | CensusDuplicateMember | CensusUnrosteredObservation with the
offending label. The three failures are not one state: a missing member means coverage is short,
an unrostered observation means the roster and instrument have drifted apart, and a duplicate
silently doubles that member's contribution to every count_stage figure derived from the census.
Collapsing them into one Bool is what would have made those figures unfalsifiable.

census_is_complete is now DERIVED from the verdict rather than computed beside it, so there is no
second Bool fold that could disagree with the coproduct.

Four controls, two of which the previous version passed: the stray and the doubled census each
contain every roster member. The notes claiming an identity join are corrected at the point of
assertion rather than left for the next reader to discover.

Found by a question from the planning side asking whether the census proved exact reconciliation
or mere coverage. It proved coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBBegUkJygQyr1zMHiv2eK

* Give the six minimal-pair probes the return type their callee now has

classify_source returns FrontendStage; p_hex_escape and its five siblings still declared
-> String, so every one of them was a type error waiting for the next compile-clean run. I
changed the classifier's result type and updated its structured consumers without checking the
six one-line probes at the bottom of the same file.

Found by the planning side reading the file, not by me re-reading what I had edited.

Also merges origin/main, which now carries #8177 — the rename of the OTHER ArtifactIdentity.
That PR touches self_host_generation_identity_witness_test, self_host_promotion_admission_
witness_test and self_host/generation.dag: exactly the files I retracted imports from on #8149,
and none of the three I kept. It confirms the homonym split rather than superseding those edits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBBegUkJygQyr1zMHiv2eK

* Validate the denominator, make stage equality structural, and delete the unadmitted stage count

Four repairs from an external review of the census increment. Three are defects I introduced.

THE ROSTER WAS NEVER VALIDATED. reconcile_census checked observations against the roster and
assumed the roster itself was sound. Two malformed rosters defeat that with CensusExact still
reachable: a duplicated LABEL is satisfied by one observation counted once for BOTH rows, so
fifteen rows with fourteen labels reconcile exactly against fourteen observations; a duplicated
PATH under two labels is invisible at label grain entirely, and the live census would read one
file twice and report it as two members. verify_roster now runs first and CensusExact is
unreachable without FrontierRosterValid. It takes the rows rather than reading frontier_roster,
because a validator that can only see the one live roster is unfalsifiable — it would report
valid and nothing could establish it reports anything else.

STAGE EQUALITY WAS THE RENDERER. frontend_stage_eq compared frontend_stage_label results, making
presentation the semantic authority: a label rename would change which stages compare equal. Now
structural; the pairwise-distinctness claim is about rendering only, which is what it should
always have been.

count_stage IS DELETED. It took the unreconciled population, so it would total a census missing a
member, carrying a duplicate, or built on an invalid roster — the exact corruption the note
beside it describes. It also had no consumer. Counting returns on a carrier only a reconciled
census can construct, with the executing census that gives it a producer.

The six manual probes now render through frontend_stage_label rather than returning FrontendStage.
Their only consumer is a person invoking one by hand, so label text is the right interface; the
typed vocabulary is for structured consumers.

Four roster controls, including reordering, since row order is not a fact about the roster and
must not become one about the verdict.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBBegUkJygQyr1zMHiv2eK

* Enumerate frontend_stage_eq's arms: the wildcard form was unrostered non-fold residue

The floor refused non_fold_residue_no_unrostered_or_stale. v2.lens.non_fold_residue counts a
match on a FUNCTION PARAMETER whose type is a closed coproduct and whose body carries a top-level
wildcard arm, in a non-test .dag. The structural equality I landed last push is exactly that
shape — `match b { LexRefused => true  _ => false }`, six times — so making equality structural
introduced the residue in the same motion that removed the renderer from the equality path.

Both facts are real and the resolution is neither a roster row nor a revert: enumerate the arms.
All thirty-six positions are written out.

The verbosity is the point rather than a cost being tolerated. Under wildcards, adding a seventh
stage leaves this function compiling and quietly answering false for every comparison involving
it — the same silent-widening the lens exists to catch. Enumerated, the addition fails to
typecheck at all thirty-six positions, so a new stage cannot be introduced without deciding its
equality against every existing one.

Verified no match-on-parameter in the file carries a wildcard: the remaining ones scrutinise
let-bound values or closure parameters, which the census does not count and which cannot hide a
missing coproduct case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBBegUkJygQyr1zMHiv2eK

---------

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
Co-authored-by: gunbai-bot[bot] <289086189+gunbai-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.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