Skip to content

The planner minted an empty Spark schedule unconditionally: the observation carries its projection, the rows become an axis product, and the artifact stops answering for a plan it never made (P0-C3, dry) - #9763

Merged
gunbai-bot[bot] merged 110 commits into
mainfrom
session/eager-pike-541-p0-c3
Aug 31, 2026

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

Worker attestation

  • Title describes the change rather than the session or branch.
  • This body summarizes what changed and why.
  • Tests and their results are recorded below.
  • This PR closes the SPARK-PAIR-0 P0-C3a program step; it does not close a GitHub issue.
  • The branch commit history is accounted for, including two merges from main and the regenerated projections.
  • No secrets, credential material, or large binaries are committed.

Summary

SPARK-PAIR-0 P0-C3a makes one sealed observation transaction own every Spark serving probe capture, typed acquisition identity, provenance, projection, and acquisition-coverage receipt. Planning privilege is separated from pure row computation: only an acquired host observation may mint a bound Spark axis, while hermetic projections may exercise computation without crossing into an executable plan.

The generic FleetConverge artifact now consumes that bound axis and is the sole artifact assembler. Its rendered body carries the Spark typed-action marker required by its own apply reader. Post-apply verification acquires a distinct PostApply transaction and refuses unobserved linger rather than planning from a fabricated Boolean.

Two production defects the cut surfaced, both repaired here:

  1. Two artifact assemblers, with the correctness wall on only one. spark_serving_fleet_converge_plan_artifact_from_rows assembled a whole FleetConvergePlanArtifact beside the generic fleet_converge_plan_artifact. The only thing it did that the generic one did not was write the # spark-serving-typed-actions: line that fleet_converge_plan_spark_typed_actions_consistency_admits reads and spark_serving_apply_reads_frozen_typed_rows depends on. Once production moved to the generic route, the shipping path emitted plans its own apply reader refuses whenever a Spark row existed. Nothing was red, because every claim reading the marker entered through the Spark-shaped door that had it. The line now lives in the generic assembler; the second assembler is deleted with no callers left.
  2. A conjoined slice claim whose red named none of its four facts. Split into four; the split is what localized (1).

The hermetic acquisition fixture is transitively sealed: every function returning a production-accepted capture, request, transaction, provenance or observation carries admit_callers naming its exact consumers. Sealing proved eight functions dead and they are deleted. The wall is proven by a control pair that makes the same call with the same arguments and differs only in whether the roster names the caller.

Fixture states are earned from probe output rather than declared: the unit read echoes the desired unit wire so the content member converges by bytes, the runtime receipt echoes the identity digest the projector compares, the required model digest is the joint across four store legs, and linger's third state comes from a loginctl leg that ran and exited nonzero.

Test plan

Candidate head: 3add770ce1418c530c064b5d5f2d7f7f4155d5ee
Tree: 60f44f57ae343f9f227a22621f676a9547f72009

Eight-entry srv6 matrix, run on the earlier head a44a6a1b (tree 29b5962a): 305/305 PASS. That matrix has NOT been re-run on the current head; what covers the current head is CI, which executed all five jobs on the composed merge-preview tree (below). The matrix is recorded as the historical targeted run it was, not as evidence about 744d69e9.

Each entry is scored as PASS-count against that file's own declared test fn roster, so a truncated or killed entry reads as PASS < roster rather than as silence. rc=0 on all eight.

  • observation transaction: 23/23
  • full plan: 63/63
  • observe: 68/68
  • converge slice: 35/35
  • model store: 15/15
  • cell role: 27/27
  • fleet converge plan: 72/72
  • fixture admission: 2/2

Generated-artifact assessor (generated_artifact_gate.main): clean, rc=0. The zero is discriminating, not assumed: a control in an isolated detached worktree that perturbs .githooks/pre-commit returns rc=1 with the located finding committed content differs from authority.

This PR performs no fleet mutation. Its canary is one read-only observation of srv5 and srv6, re-executed on this exact head 744d69e9, and again on the composed merge-preview tree, rather than carried. The 34de783 execution below is retained as the historical record it is; the applicability section that follows states why its carry was withdrawn.

CANARY EXECUTION

attempt_identity =
  UnrecoverableInSession {
    missing_field: CanaryRunId,
    reason:
      "The ephemeral harness-generated canary run ID was not retained with
       the surviving execution receipt. Repository, PR-comment, workflow,
       and retained-session searches found no value bound to this exact
       source/tree/binary/report tuple."
  }

receipt_identity =
  ExactExecutionTuple {
    source_commit:
      34de783a9a822d324df54d4cc7dfb516020d38cd,
    source_tree:
      86a9ace9e3b8f40e48e5042b7845587f204b9705,
    observer_entry:
      spark_serving_observe_ci_wet,
    binary_sha256:
      92a695df93aff60e089030ef33923b4b6bb278471939266074678ddf51a41edc,
    report_body_sha256:
      d7951e922ef8fb12a338a0a94051dcd5f90a79571f9382c33b5184b69e756c2b
  }

The observation itself: srv5 and srv6 each observed exactly once with no foreign host; acquired provenance and phase=report on both; one shared fleet attempt with distinct observation-attempt identities; a single credential identity and locus across both hosts; required=23 captured=23 missing=0 duplicated=0; executed=23, transport-refused=0, prerequisite-refused=0; process exit 0.

CANARY APPLICABILITY

carry_required = false

reason =
  NoCarryNeeded {
    note:
      "The observation was re-executed on the exact candidate head, so no
       applicability relation carries a prior execution forward. The block
       this replaces asserted CarriedByStructuralIdentity from 34de783 to
       a44a6a1b; that verdict did not survive the two subsequent merges
       from main and is withdrawn below rather than repaired."
  }

execution_receipt =
  ExactExecutionTuple {
    canary_run_id:
      canary-20260831T202616Z-47796f96a950,
    source_commit:
      3add770ce1418c530c064b5d5f2d7f7f4155d5ee,
    source_tree:
      60f44f57ae343f9f227a22621f676a9547f72009,
    observer_entry:
      spark_serving_observe_ci_wet,
    binary_sha256:
      3a26295c9c269aace078bdeb4aa3505fb562a4ca8ff9fe05ef593ade9c58da06,
    report_body_sha256:
      ae78ec903cc4781af5df54d541eb60cd2f0749e861e9a0341ab309c99201fd6c,
    probed_at:
      2026-08-31T18:55:00Z
  }

verdict = ExactHeadExecution

Second receipt: the COMPOSED tree, not just the head. A head-only receipt
proves the observe route for the branch, not for what actually lands. The wet
observation was therefore re-run a second time against GitHub's merge preview —
the same tree CI adjudicated, confirmed from the job log's checkout line
(HEAD is now at 59cd9388d6 Merge 744d69e9fda... into 0b4366efb8f...).

SUPERSEDED — the composed-tree receipt below was taken against merge preview
59cd9388 whose head-parent was the previous head. main advanced to eced9d24,
which conflicted DESIGN.md; that merge is now in this branch and the head moved,
so the preview changed and this block is retained as history rather than as current
evidence. The exact-head receipt above IS current. Re-running the composed-tree
observation costs no head move and no CI restart, so it can be re-taken on request
against whatever preview is current at the moment of merge.

CANARY, COMPOSED TREE (SUPERSEDED — see note above)

canary_run_id = canary-20260831T195020Z-7083b85f17f9
subject       = merge-preview refs/pull/9763/merge
merge_commit  = 59cd9388d65ebd3e18e77569c9892d1d8b63cf1d
merge_tree    = 4440edfaebc1db85da03e8ed802c405f116b338c
base_parent   = 0b4366efb8f4e6f3d2224e94d210fe6500323376
head_parent   = 3add770ce1418c530c064b5d5f2d7f7f4155d5ee
binary_sha256 = 4f1618f523cb5caaf988b8169a1b9c952ecd67375e4fe84d3d3423d60597f02f
exit          = 0

verdict = ComposedTreeExecution

Same predeclared criteria, all passed: srv5=1 srv6=1 with 0 foreign; acquired and
phase=report on both; required=23 captured=23 missing=0 duplicated=0 and
executed=23 transport-refused=0 prerequisite-refused=0 on both; one fleet-attempt
with distinct per-host observation-attempt; one credential identity; model_tag=20b
digest 17052f91… on both. The harness additionally asserts both parents, so a
refreshed merge preview refuses rather than silently scoring a different tree.

This closes the gap a head-only receipt leaves: main commits reachable only through
the base parent — including #9813, which touches the evaluation substrate that runs
this entry — are inside the tree this observation executed.

The honest boundary, stated rather than implied. The composed tree is a function
of main, so this receipt is exact for 4440edfa and is retired by any main advance
that changes the merge preview. It is not, and cannot be, a claim about a tree that
does not exist yet. A per-merge-preview wet receipt cannot be made permanently
current while main advances faster than the merge cycle; what it can do, and does
here, is pair the wet evidence to the exact tree the dry evidence adjudicated.

This receipt is bound to the head this PR currently carries, so it is not a carry
argument and has no intersection premise to be invalidated by a moving main. Any
further merge from main produces a new head and a new tree, which retires this
receipt and re-opens the same obligation.

Withdrawn: the previous applicability block. It asserted
canary_execution_component_intersection = [] and
verdict = CarriedByStructuralIdentity for candidate a44a6a1b. Measured at the
current head, that intersection is not empty. Changed since a44a6a1b and on or
beside the wet route: dag/extdeps/posix/shell_command_language.dag (remote-command
quoting), src/v2/std/text.dag, src/v2/extdeps/languages/bash.dag and
bash_command_fold.dag, dag/extdeps/ollama/capability.dag (support catalog
[20b] to [20b, 120b]), dag/gunbc/ollama_model_resolution.dag, and in the seed
src/v1/stage0/src/cli_run/entry_resolve.rs and resolved_graph_cache.rs. The
block is replaced rather than corrected, because re-running removes the need for a
carry instead of arguing one.

Withdrawn: the grain label. That block named
dag_comparison_grain = ResolvedDeclarationDependencyClosure. The instrument was
not a resolved dependency closure — it was a conservative lexical declaration
reachability over-approximation over fn/func/data/type with an
identifier-names-declaration edge rule. The label overstated the instrument. It is
recorded here as withdrawn rather than silently dropped, since the mislabel is
independent of the staleness and would otherwise recur.

Subject unchanged, checked at this head.
spark_serving_desired_model_ref is still ollama_gpt_oss_20b_model_ref, and
ollama_model_support_row_for selects by model identity rather than catalog
position, so appending the 120b row cannot move the selection. The appended row is
therefore not a subject change. A later change that rewires the desired ref or its
resolution to 120b would be one, and would require its own wet execution.

Acceptance, measured on this receipt — every criterion below was written down
before the run produced output, and a partial pass was pre-committed to count as a
failure:

criterion result
process exit 0
binary sha256 pre/post invocation identical
host sections srv5=1, srv6=1, 0 foreign
provenance acquired x2
phase phase=report x2
coverage, both hosts required=23 captured=23 missing=0 duplicated=0
outcome, both hosts executed=23 transport-refused=0 prerequisite-refused=0
fleet attempt one value across both
observation-attempt distinct per host
credential identity one value, 01c65fe99aeb7b24
model model_tag=20b digest=17052f91… on both

This PR still performs no fleet mutation; the canary remains one read-only
observation of srv5 and srv6.

SupersededHistoricalExecution: an earlier canary ran on 05c0dddaac400d49ef78ea677634cc50564344b9 (tree bcb150be…, attempt canary-20260831T075026Z-cab81106e375, binary-sha256=45ab012e…, report-body-sha256=b7ba142f…) and passed, as did one on 689a3c38ce2787c28a9aaf892303d132c1c89fe5. None of them is the authoritative receipt for this candidate, and the 34de783 tuple is now superseded too: the authoritative receipts are the exact-head execution canary-20260831T202616Z-47796f96a950 on 744d69e9 and the composed-tree execution canary-20260831T195020Z-7083b85f17f9 on merge-preview 59cd9388, both recorded above. Full receipt and ephemeral harness text for the earlier run: issue comment 5475049785.

@gunbai-bot
gunbai-bot Bot force-pushed the session/eager-pike-541-p0-c3 branch from e320cf5 to f7adb42 Compare August 30, 2026 17:16
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 30, 2026 17:17
@gunbai-bot gunbai-bot Bot changed the title Sparks Pair (P0->P5) The planner minted an empty Spark schedule unconditionally: the observation carries its projection, the rows become an axis product, and the artifact stops answering for a plan it never made (P0-C3, dry) Aug 30, 2026
@gunbai-bot

gunbai-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

SPARK-PAIR-0 P0-C3, the dry observation-to-plan bridge. No host mutation.

The two independent breaks

fleet_converge_plan_cli passed observed_spark: [], and the generic artifact set spark_serving_typed_actions: scheduled_rows_empty() unconditionally. Fixing either alone changes nothing, which is why the Spark axis was inert in production even though the planner already knew how to plan it.

This PR closes the second and makes the first possible. It does not close the first — see the end.

One observation, one producer

SparkServingHostObservation carried only the reporting facts, so the planner could not be fed from it. The two routes available instead were both wrong the same way: parse the human receipt back into members, or run a second observation for planning. The second is the one that looks harmless — it's only more probes — and it's the one that silently joins a unit's content read at one moment with its enablement read at another. Two facts about one machine taken from two observations are not an observation of that machine.

The carrier now holds the projection, produced by the same probe transaction. Because it is sole_constructor, there is no way to obtain one whose projection came from a different observation than its readback.

The mint derives the observation from the readback but takes the projection. That asymmetry is deliberate: the observation is a pure function of the readback, so deriving it removes an authorable disagreement; the projection is a function of four more probes this type does not carry and cannot re-run, so a mint claiming to derive it would be fabricating them.

Rows are a fact about an observation, not about an artifact

Row production and artifact assembly were fused, so reaching Spark rows meant calling the Spark-specific artifact builder — which brings its own timers, caps, observed_slots: [] and an unobserved fabric population. Grafting that beside the generic artifact would have created two subjects, two baselines, two leases and an arbitrary winner. spark_serving_scheduled_rows_for is extracted instead, and both the slice helper and the generic artifact derive from it.

Every refusal is preserved. The four unobserved guards — unit, enablement, activity, linger — each name a probe whose silence would otherwise read as the member is absent, and absence is what makes membership_reconcile plan an addition. An extraction keeping only the happy path would have converted four fail-closed walls into four writes against states nobody measured.

The substrate refused my first design, and it was right

I first had the artifact call the row producer directly. That closed an import cycle — gunbc.fleet_converge_plan → gunbc.spark.serving_converge_apply → gunbc.spark.serving_converge_plan already runs the other way. Artifact assembly consumes already-planned axis products (timer, cap, slot, fabric, activation plans); the Spark schedule is one more of them, not an exception this module reaches down to compute. The rows are now a parameter.

The artifact's Optional carried a fact that cannot happen

It answered Absent when content_hash_of_value didn't return the Fnv1a64 arm — an absorbing arm in the narrowing direction, since an unrecognised hash became "there is no plan artifact", which every caller reads as nothing to converge. The Spark builder repaired exactly this in its own copy and this one was left behind. content_hash_atom dissolves it, and the outcome becomes a typed sum carrying the refusal that actually exists.

Two smaller repairs found on the way

  • The linger probe argv was authored twice, once per transport — a §3 fork, free to drift the first time loginctl's interface moves. One home now.
  • serving_converge_slice_wet collapses the three-valued SparkServingObservedLingerState into a Bool, turning "the probe did not answer" into "linger is off". The new reader keeps LingerUnobservedState distinct — which is what stops an unmeasured property entering the observed member list.

Receipts (srv6)

spark_serving_full_plan_witness_test        EXPECTED=49  PASSED=49  FAILED=0
fleet_converge_plan_witness_test            EXPECTED=66  PASSED=66  FAILED=0
spark_serving_observe_witness_test          EXPECTED=68  PASSED=68  FAILED=0

Control — restoring scheduled_rows_empty() in the artifact:

PASS the_extracted_row_producer_plans_real_work_from_an_observation
PASS the_extracted_row_producer_still_refuses_an_unobserved_linger
PASS extraction_did_not_change_what_the_slice_artifact_plans
FAIL the_generic_artifact_carries_the_schedule_it_is_handed

Only the claim exercising that path reds. The other three stay green because they test the row producer, whose behaviour the mutation doesn't touch — reported that way rather than trimmed, because a control that reds everything usually means the claims are one claim wearing four names.

What this does not close

fleet_converge_plan_cli still passes an empty schedule. Wiring the real Spark observation into the plan CLI needs the fleet SSH credential context, which the plan CLI does not currently materialise. That is the remaining C3 break, alongside the canonical typed persistence (encode/decode byte-identity, tamper refusal) which is not started. Both are named rather than implied.

— sent from eager-pike-541

@gunbai-bot
gunbai-bot Bot force-pushed the session/eager-pike-541-p0-c3 branch from a8a05ba to a02bff2 Compare August 30, 2026 17:45
@gunbai-bot

gunbai-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in a02bff23261. The finding was correct — and it caught a stale intermediate state rather than a design mistake, which is worth being precise about since it changes what the fix is.

What happened. The mint went through two shapes in this branch. It first took a finished projection parameter, and I changed it mid-work to take the four probe observations instead, because the projection-parameter form reopened the exact class #9754 had just closed one field over: nothing stopped a caller pairing srv5's readback with a projection built for srv6. Two hosts, one carrier, disagreeing — same defect, different field. The fixture was left on the old shape when the mint moved, which is what you found.

The fix is the one you named, with one refinement. ho_from now threads honest *Unobserved sentinels:

runtime: RuntimeMaterializationAbsent
store: ModelStoreAbsent
observed_unit: { state: UserUnitStateUnobserved,
                 enablement: UserUnitManagerUnobserved,
                 activity: UnitActivityUnobserved }
linger: LingerUnobservedState

and projection_unobserved() is dropped, as you said. The refinement: the fixture can no longer state a host for the projection at all, because the mint derives it from readback.host. That is the provenance property the reshape was for — a fixture that could still name a host would have preserved the disagreement the change exists to remove.

Receipts — srv6, a8a05baa1f6101ddfaf318e05bfa996adc74761f, clean tree, 0 untracked, denominators derived from source:

spark_serving_full_plan_witness_test    EXPECTED=49  TERMINAL=49  PASSED=49  FAILED=0
fleet_converge_plan_witness_test        EXPECTED=66  TERMINAL=66  PASSED=66  FAILED=0
spark_serving_observe_witness_test      EXPECTED=68  TERMINAL=68  PASSED=68  FAILED=0

expected = terminal = passed on all three.

One thing landed since you reviewed, from a parallel read: the artifact took observed_spark and the schedule as two independent parameters, so a caller could hand it members from one observation and rows planned from another. observed_spark feeds the observed baseline, and the baseline is what apply re-checks to decide the plan still applies — so that disagreement would have been invisible at exactly the moment it mattered. They are now one SparkServingAxisProduct, sole_constructor, minted only by the row producer. Same class as the two above; third instance in this area.

— sent from eager-pike-541

@gunbai-bot
gunbai-bot Bot marked this pull request as draft August 30, 2026 17:52
@gunbai-bot

gunbai-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Back to draft despite the approval. A parallel review found four things the approval did not, and one of them is an overstatement in my own commit message. Recording them here rather than merging.

1 — my "one observation, one producer" claim is not structural yet (blocking). The mint now takes probes rather than a projection, which closes the type-level splice. But the production path underneath does not actually run one producer:

  • in spark_serving_observe_host_via_locus, the reporting readback goes through spark_serving_probe_leg_via_locus while the projection helpers invoke the other spark_serving_probe_leg path — one returned carrier combining two executor paths;
  • is-active is read once for the readback and again inside the user-unit projection;
  • the runtime materialization receipt is read for the readback and again by the runtime-root observer.

They may reach the same account in a given run, but nothing establishes it. The honest wording for what this PR currently does is "the readback and projection are constructed during one enclosing function call" — not "one typed probe transaction owns every capture." My commit message claims the latter. That is rung inflation and I am not shipping it under that description. The repair is a per-host probe transaction carrier owning host, locus, and one capture per probe, with reporting, account, observation and projection all projecting from it.

2 — the axis product needs to be a disposition, not a pair (blocking). SparkServingAxisProduct binds members to rows, which was the right direction, but it cannot distinguish a genuinely non-Spark subject from an unobserved one, and it has no arm for a Spark-axis refusal. spark_serving_axis_product_unobserved() currently launders all three into empty-members-plus-empty-rows. It needs NotApplicable / Refused / Planned arms with the host bound from the observation.

3 — the artifact's Refused arm is now inert. After the content_hash_atom repair, fleet_converge_plan_artifact always constructs Ready. Nothing produces Refused, because the function receives concrete ScheduledRows rather than the row-planning verdict. That is a permanently-green arm carrying no information — worse than absent, since it will be cited as coverage. It gets a real producer via finding 2, or it goes.

4 — my linger claim is not established. The argv fork is genuinely closed. But the slice still contains spark_serving_linger_enabled_from_stdout -> Bool? and I added the three-valued reader on the observation path without replacing the slice's Boolean path. So "the slice collapsed unobserved to disabled" is a claim I have not demonstrated. I will run a discriminating control through the actual slice — absent probe, nonzero exit, malformed stdout — and either fix the slice or retract the defect claim and describe what actually changed.

What stands: the row-production extraction, keeping all four unobserved walls, the content_hash_atom dissolution, the argv centralization, and the scheduled_rows_empty() mutation control.

This PR becomes P0-C3a and does not claim the C3 exit receipt.

— sent from eager-pike-541

@gunbai-bot

gunbai-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Retracting finding 4 — the defect I claimed does not exist.

I checked the slice's actual flow rather than reasoning from the presence of -> Bool?, and all three unobserved cases already refuse before reaching the Boolean translation:

  • TypedArgvExecRefused → wet-applied-readback-refused <reason>
  • post.success == false (nonzero exit) → wet-applied-readback-refused exit=… stderr=…
  • malformed successful stdout → spark_serving_linger_enabled_from_stdout returns Absent, and the observation arm sets observed_linger: LingerUnobserved, typed_row_count: 0, verdict: "observation-refused stdout=…"

So the slice never turns "the probe did not answer" into "linger is off". It refuses with zero planned rows, which is the correct behaviour and was already the behaviour on pre-PR main. The Absent arm is disposed of as a refusal, not translated.

What I got wrong and why. I saw a three-valued type (SparkServingObservedLingerState) being fed from a Bool? decoder and inferred the collapse from the shape, without following the control flow to see that the third value is handled earlier by refusing outright. That is the same mistake I have been calling out in reviews — asserting a property from a type signature instead of from execution — applied to my own diff.

The honest description of what this PR actually changes on the linger axis is therefore narrower than my commit message says:

  • the linger probe argv had two spellings, one per transport, and now has one home (this part stands — it is a real §3 fork);
  • a three-valued reader is added for the unified observation producer, which is a new consumer that needs it, not a repair of the slice.

The commit message and PR body will be corrected to say that, and the "turning 'the probe did not answer' into 'linger is off'" sentence removed. It described a defect that isn't there.

— sent from eager-pike-541

…e rows derived from them (P0-C3a, dry)

SPARK-PAIR-0 P0-C3a, part of the dry observation-to-plan bridge. No host
mutation. This does NOT claim the C3 exit receipt.

TWO INDEPENDENT BREAKS make P0-WET unreachable: the CLI passes an unobserved
Spark axis, AND the generic artifact set spark_serving_typed_actions to
scheduled_rows_empty() unconditionally. Repairing either alone changes nothing.
This closes the second and makes the first possible; the first is still open.

ROWS ARE A FACT ABOUT AN OBSERVATION, NOT ABOUT AN ARTIFACT. Row production and
artifact assembly were fused, so reaching Spark rows meant calling the
Spark-specific artifact builder, which brings its own timers, caps,
observed_slots: [] and an unobserved fabric population -- two subjects, two
baselines, two leases and an arbitrary winner. spark_serving_scheduled_rows_for
is extracted; both the slice helper and the generic artifact derive from it. All
four unobserved guards are preserved verbatim: each names a probe whose silence
would read as "the member is absent", and absence is what makes
membership_reconcile plan an ADDITION.

MEMBERS AND ROWS ARE ONE CARRIER. The artifact took observed_spark and the
schedule as two independent parameters, so a caller could hand it members from
one observation and rows planned from another. observed_spark feeds the observed
BASELINE, and the baseline is what apply re-checks to decide the plan still
applies -- so that disagreement would have been invisible at exactly the moment
it mattered. SparkServingAxisProduct is sole_constructor and only the row
producer mints it.

THE MINT TAKES PROBES, NOT A FINISHED PROJECTION. A first version accepted the
projection as a parameter, which reopened the class #9754 closed one field over:
nothing stopped a caller pairing srv5's readback with a projection built for
srv6. The mint now takes the four probe observations and builds the projection
from readback.host, so the host is not a caller's to supply.

THE SUBSTRATE REFUSED MY FIRST DESIGN AND IT WAS RIGHT. Having the artifact call
the row producer closed an import cycle -- gunbc.fleet_converge_plan ->
spark.serving_converge_apply -> spark.serving_converge_plan already runs the
other way. Artifact assembly consumes already-planned axis products.

THE ARTIFACT'S OPTIONAL CARRIED A FACT THAT CANNOT HAPPEN. It answered Absent
when content_hash_of_value did not return the Fnv1a64 arm -- an absorbing arm in
the narrowing direction, since an unrecognised hash became "there is no plan
artifact", read by every caller as nothing to converge. content_hash_atom
dissolves it.

THE LINGER ARGV FORK IS CLOSED: it had two spellings, one per transport, free to
drift the first time loginctl moves, and now has one home. The three-valued
reader added beside it serves the unified observation producer. It is NOT a
repair of serving_converge_slice_wet: an earlier version of this commit claimed
the slice collapsed "unobserved" into "disabled", and that claim was FALSE. The
slice refuses on all three unobserved cases -- transport refusal, nonzero exit,
malformed stdout -- before reaching any boolean translation, setting
LingerUnobserved with zero planned rows. The claim was inferred from a Bool? in
a signature rather than from the control flow, and is retracted here rather than
left standing in history.

KNOWN NOT DONE, so this does not overstate its rung: the production path does
not yet run ONE probe transaction. In spark_serving_observe_host_via_locus the
readback uses the via-locus leg while the projection helpers use the other leg,
and is-active and the runtime receipt are each probed twice. The honest wording
is that readback and projection are constructed during one enclosing call, not
that one transaction owns every capture. The axis carrier is also a pair rather
than a disposition: it cannot distinguish a non-Spark subject from an unobserved
one and has no refusal arm, which is why fleet_converge_plan_artifact's Refused
arm currently has no producer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018dHefbXnLiW1XNs2vVxhwj
@gunbai-bot
gunbai-bot Bot force-pushed the session/eager-pike-541-p0-c3 branch from a02bff2 to d4257b6 Compare August 30, 2026 18:00
@gunbai-bot

gunbai-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

This branch was silently reverting #9704, and the review credited that as a clean deletion. Rebuilt in d4257b68630.

What happened. I squashed with git reset --soft origin/main && git add -A. My working tree was based on an older main. reset --soft moved HEAD to the newer origin/main while keeping the older tree, so git add -A staged the deletion of every file main had gained in the interim. Main had advanced to 2afe322f774 — FABRIC-CI: add shared wet-gate evidence interlock (#9704) — so my commit deleted that PR's files and reverted two more:

dag/gunbc/instruments/fabric_ci_evidence.dag                    -265
dag/gunbc/instruments/fabric_ci_evidence_projection_fixture.dag  -14
tools/fabric_ci_evidence_calibration.sh                          -82
tools/fabric_ci_evidence_driver.sh                               -27
dag/gunbc/witness/witness_floor_workflow.dag                      -84
src/v2/workflow/floor_cost_debt.dag                           reverted

None of that is mine. The git grep fabric_ci_evidence check reported empty precisely because the deletion was complete and self-consistent — it confirmed the revert was tidy, not that it was intended. A diff that reverts someone else's merged work is coherent by construction, which is why "drops cleanly" could not distinguish it from the real thing.

Rebuilt from current main, taking only the nine files I actually authored:

9 files changed, 456 insertions(+), 99 deletions(-)

No fabric_*, no witness_floor_workflow, no floor_cost_debt.

Receipt — srv6, d4257b68630dbaf4e9d855daadf868d5a8487742, tree aabfc6e1a67, clean, 0 untracked:

spark_serving_full_plan_witness_test    EXPECTED=49  TERMINAL=49  PASSED=49  FAILED=0
fleet_converge_plan_witness_test        EXPECTED=66  TERMINAL=66  PASSED=66  FAILED=0
spark_serving_observe_witness_test      EXPECTED=68  TERMINAL=68  PASSED=68  FAILED=0

One correction to this review. It repeats "The linger three-valued fix removes a real Bool→enum truncation." That claim is mine and I retracted it in the comment above before this review ran. The slice already refuses on all three unobserved cases — transport refusal, nonzero exit, malformed stdout — before any boolean translation, setting LingerUnobserved with zero planned rows. The argv de-duplication is real; the truncation was not. Flagging it so the retraction doesn't get overwritten by a review quoting my own error back at me.

This stays draft: the probe-transaction and axis-disposition findings from the parallel review are still open, and the commit message now states them as known-not-done rather than claiming the rung.

— sent from eager-pike-541

@gunbai-bot

gunbai-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

Axis disposition landed, three annotations corrected — and I hit the recut bug a second time

First, the process failure, because it happened again and the protocol is the point. Preparing this update I typed git reset --soft origin/main && git add -A out of habit — the exact primitive I had just been told to stop using. Main had advanced from 2afe322f774 to b60fc7dd61e, and the index immediately staged 1,725 paths. I caught it before committing, reset back to c31cc43c8e4, and redid it properly.

That near-miss produced a useful measurement: six of my nine files overlap with paths main changed since my base. So even a correct nine-path allowlist would not have been enough — copying my snapshots over those six would have silently reverted main's edits inside files that are legitimately mine. The safe primitive is a real 3-way merge, not a file copy, which is also this repo's stated preference for integrating main.

Recut receipt

old_base    2afe322f774        old_head  c31cc43c8e4
new_base    b60fc7dd61e5bae6b8c2e493e87815a116d4e7a5
result      5942842fc01e5bea718806c18ec2c3d8db939b49
tree        72c2a757a789239b51746ee8ba242e5497ae7a6f
paths main changed since old base: 1725
overlap with my authored set:      6   (resolved by 3-way merge, not copy)
unexpected paths: 0     deletions: 0     clean: yes     untracked: 0

Complete name-status: nine M, nothing else.

Three annotations corrected

The commit message told the truth while the source still told the superseded story — which is the "annotation as evidence" trap this codebase is otherwise careful about.

  1. serving_observe.dag claimed "one physical observation attempt owns the probe results". Replaced with the honest interim standing: readback and projection are assembled during one enclosing per-host call, but their captures are not yet owned by one typed probe transaction — is-active and the runtime receipt are each probed twice, and the via-locus path mixes executor adapters.
  2. The linger annotation still asserted the slice collapse I had retracted. Now states what the decoder actually is (the canonical three-valued reader for the new consumer) and records explicitly that it does not repair the slice, which already refuses all three unobserved cases.
  3. serving_execution_schedule.dag claimed only the row producer could mint the axis — while the module exposed a standalone spark_serving_axis_product_unobserved() that falsified it.

The axis is now a disposition, and the empty mint is deleted

SparkServingAxisNotApplicable { host }
SparkServingAxisRefused      { host, cause }
SparkServingAxisPlanned      { host, observed_members, rows }

A pair of lists could not distinguish a non-Spark subject from a refused observation from an observed host owing no writes — it answered all three with empty-members-plus-empty-rows, which is an absorbing fallback in the input to a converge. The host travels in the arm, so it cannot disagree with the observation that produced it.

The artifact's Refused arm now has a real producer. A SparkServingAxisRefused propagates instead of being laundered into an empty schedule. That arm was permanently green before — worse than absent, since it reads as coverage.

Receipts — srv6, 5942842fc01e, tree 72c2a757a78, clean, 0 untracked

spark_serving_full_plan_witness_test    EXPECTED=55  TERMINAL=55  PASSED=55  FAILED=0
fleet_converge_plan_witness_test        EXPECTED=66  TERMINAL=66  PASSED=66  FAILED=0
spark_serving_observe_witness_test      EXPECTED=68  TERMINAL=68  PASSED=68  FAILED=0

Two controls, killing different claims:

A — a refusal launders into an empty plan (the old pair behaviour): only a_refused_axis_refuses_the_artifact_instead_of_planning_nothing reds.

B — baseline reads an empty population regardless of the axis: only the_artifact_baseline_moves_with_the_observed_spark_population reds.

Neither substitutes for the other.

Still open, still draft

The probe-transaction carrier is not built, so "one transaction owns every capture" remains false and the source now says so. That is the next piece.

— sent from eager-pike-541

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

observed_linger: LingerObserved { enabled: linger_enabled },

P2 Badge Preserve unobserved linger in refused reports

When the ProbeLinger capture exists but its transport is refused, exits nonzero, or returns malformed output, the transaction still reaches this function and spark_serving_projection_linger_enabled returns false; wrapping that value in LingerObserved therefore renders observed_linger=no even though linger was never observed. This defeats the existing distinction between disabled and unobserved on artifact-refusal paths and can mislead consumers of the wet receipt; derive a typed observed/unobserved state rather than converting the projection to a Boolean here.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

gunbc-ci-auto-heal added 2 commits August 31, 2026 14:03
…1-p0-c3

# Conflicts:
#	DESIGN.md
#	docs/design-ledgers.md
#	src/v1/stage0/src/namespace_wave_admission.rs
…1-p0-c3

# Conflicts:
#	src/v1/stage0/src/namespace_wave_admission.rs
@gunbai-bot

gunbai-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

HOLD — do not merge yet. The CANARY APPLICABILITY block in this body is stale and now asserts a verdict that is not established at this head. This is an author-raised defect, not a review finding; the five approvals were all cast against this body.

The block states:

candidate_commit = a44a6a1b12a37b104e14de0b3fefce8ec746db86
candidate_tree   = 29b5962a618e8fcacea1ca2ff73a65dfef1efa0c
canary_execution_component_intersection = []
verdict = CarriedByStructuralIdentity

Head is now 32e2dcc — two further merges from main (ce01e18, 32e2dcc) since that candidate. Measuring the delta a44a6a1..32e2dcc, the intersection is not empty. Files changed since the asserted candidate that lie on or beside the canary route:

dag/extdeps/posix/shell_command_language.dag   remote-command quoting
src/v2/std/text.dag                            text carriers used by that quoting
src/v2/extdeps/languages/bash.dag              bash conversion
src/v2/extdeps/languages/bash_command_fold.dag
dag/extdeps/ollama/capability.dag              support catalog, [20b] -> [20b, 120b]
dag/gunbc/ollama_model_resolution.dag          desired-model resolution
src/v1/stage0/src/cli_run/entry_resolve.rs     entry-resolution substrate
src/v1/stage0/src/resolved_graph_cache.rs      serving representation the old route could consult

So canary_execution_component_intersection = [] is not true at this head, and verdict = CarriedByStructuralIdentity does not hold. The receipt still correctly identifies the binary that ran (92a695df…, on source 34de783 / tree 86a9ace9); what is false is the applicability claim carrying that execution to the current candidate.

A third defect, independent of the staleness: dag_comparison_grain = ResolvedDeclarationDependencyClosure names a grain the instrument does not have. It is a conservative lexical declaration-reachability over-approximation — identifier-names-declaration edges over fn/func/data/type — not a resolved dependency closure. Under DESIGN section 4b that mislabel is rung inflation on the instrument itself.

Disposition I am taking: re-establish the receipt at this exact head rather than repair the carry argument. Re-running the read-only two-host observation on 32e2dcc discharges the whole carry question by removing the need for a carry, and — decisively — a body edit does not change the head, so the new receipt stays valid against this SHA, no CI re-run and no review staleness. Building the extensional-equivalence proof instead would require committing witness code, which moves the head and re-opens the same gap against the new SHA.

I will replace the applicability block with the execution receipt for 32e2dcc, or report here if the observation cannot be obtained.

@gunbai-bot

gunbai-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

HOLD LIFTED — the applicability gap is discharged by re-execution, not by a repaired carry argument.

The read-only two-host observation was re-run on this exact head. Head did not move: a body edit changes no SHA, so this receipt is bound to the commit the PR actually carries.

canary-run-id       canary-20260831T173013Z-114592d57094
source-commit       32e2dcc7bcbf80d6438ab03cde30354a17e537ca
source-tree         5ffaab022248fadd01eca2a31a7dbddab3ecb037
observer-entry      spark_serving_observe_ci_wet
binary-sha256       a432efaa778f09f38b361329b08852028b6060672441c8fb29310e842ce620cd
report-body-sha256  a30f7631447fb721757270419e6c15823c007a25043a829cadfa81a472b50d60
probed-at           2026-08-31T17:33:27Z
exit                0

Acceptance criteria were written down before the run produced any output, with a partial pass pre-committed to count as a failure. All of them pass: srv5=1 srv6=1 sections and 0 foreign; acquired and phase=report on both; required=23 captured=23 missing=0 duplicated=0 and executed=23 transport-refused=0 prerequisite-refused=0 on both; one fleet-attempt across both with distinct per-host observation-attempt; one credential identity 01c65fe99aeb7b24; model_tag=20b digest=17052f91 on both; binary sha256 identical before and after invocation.

Body updated in three places: the applicability block is replaced by the exact-head receipt, the summary sentence no longer says the canary was carried, and the SupersededHistoricalExecution note no longer names 34de783 as authoritative.

Two things withdrawn on the record rather than silently dropped:

  1. The carry verdict. canary_execution_component_intersection = [] was false at this head.
  2. The grain label. dag_comparison_grain = ResolvedDeclarationDependencyClosure named a grain the instrument did not have; it was a conservative lexical declaration-reachability over-approximation. That mislabel was independent of the staleness and would have recurred.

One fact checked at this head and worth stating because it decides whether this was a refactor or a subject change: spark_serving_desired_model_ref is still ollama_gpt_oss_20b_model_ref, and ollama_model_support_row_for selects by model identity rather than catalog position, so appending the 120b row cannot move the selection. A later change rewiring the desired ref to 120b would be a subject change and would need its own wet execution.

Standing limitation, stated so it is not discovered later: this receipt is bound to 32e2dcc / tree 5ffaab02. Any further merge from main produces a new head and retires it. If main advances before merge, the observation must be re-run rather than re-argued.

No source churn was involved — no commit, no new SHA, no CI re-run, and the existing approvals are still against the code they reviewed.

gunbc-ci-auto-heal added 2 commits August 31, 2026 17:54
…1-p0-c3

# Conflicts:
#	docs/design-ledgers.md
…s imported

Two repairs, both surfaced by composing with main rather than by any defect
in either side alone.

The merge with main brought #9824, which added a `target` field to
AdmissionSubject::Binding: the module the admission's own PR moved the name
TO, and the referent of the consumption proof. Our two SPARK-PAIR-0 P0-C3a
rows predate that field, so git merged both sides cleanly and the tree did
not compile. The value is verified rather than copied from our own label --
spark_serving_linger_probe_words is declared in dag/gunbc/spark/serving_observe.dag,
whose module line is gunbc.spark.serving_observe.

serving_execution_schedule.dag matched SparkServingUserUnitActivityAddress at
four sites without naming it in its import block. Peer modules list it
explicitly, so it is now listed here. This is import hygiene, not a
resolution repair: the module resolved and typechecked before the change --
measured, by reaching evaluation and failing on a call-contract mismatch,
which occurs after resolution.
@gunbai-bot

gunbai-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Addressing review 57915 (dashboard-only, REQUEST_CHANGES). One finding fixed, one respectfully disputed with a measurement.

FINDING 1 — missing import. FIXED in 0599a8a, but the mechanism claim is wrong and I want that on the record rather than quietly accepting it.

The observation is correct: dag/gunbc/spark/serving_execution_schedule.dag matched SparkServingUserUnitActivityAddress at four sites without naming it in the import block, and peer modules list it explicitly. It is now listed.

The claim that this "breaks name resolution for four binding rows" is not correct. I measured it before changing anything, by running the module through the real compiler:

gunbc run --source-root dag --source-root src/v2 \
  --entry dag/gunbc/spark/serving_execution_schedule.dag \
  --function spark_serving_address_establishes

Before the fix, that reached EVALUATION and failed with CallContractMismatch: missing required argument address. A call-contract mismatch is raised after resolution and typechecking, so reaching it is positive evidence that the module resolved. Variant names resolve corpus-wide in this substrate; the import block is hygiene and convention, not the binding mechanism. After adding the import the same command produces the identical diagnostic, which is the control: the change moved hygiene, not resolution.

That distinction matters beyond this line. If a missing import really did break resolution, the compiler floor would already be red on main for every module in this shape, and the remedy would be a corpus-wide census rather than one line. Recording the mechanism honestly is what keeps the next reader from deriving the wrong remedy.

FINDING 2 — advisory, empty ScheduledRows mint. This does not exist in the current file. ScheduledRows is sole_constructor with exactly one mint, scheduled_rows_from_frozen_schedule, which takes a frozen schedule. There is no empty mint beside it. The file records its own history on this point:

"So the arms are distinct and the empty mint is deleted rather than kept beside them."

The empty mint the review describes was removed earlier in this same PR; the advisory appears to be reading a superseded state. The nearby empty-looking construction is SparkServingScheduleRowsProjected { rows: [], direction: SparkServingSchedulableEmpty }, which is a different type and a deliberate typed empty-disposition arm, not a ScheduledRows bypass. No change made.

ALSO IN 0599a8a, and not from this review — a defect that only exists because of composition with main. #9824 added a target field to AdmissionSubject::Binding. Our two SPARK-PAIR-0 P0-C3a admission rows predate it, so git merged both sides with no conflict and the tree did not compile: E0063 missing field target, twice. The value is verified rather than copied from our own row label — spark_serving_linger_probe_words is declared in dag/gunbc/spark/serving_observe.dag, whose module line is gunbc.spark.serving_observe.

Wet canary re-executed on the new head, all predeclared criteria passed:

canary-run-id       canary-20260831T181650Z-f156688c9802
source-commit       0599a8a21a82f350faa2b0b426b79eb1adc66085
source-tree         579a314cfb00a26103ac04a0cc4fb3f974e55f14
report-body-sha256  b4ebc223494ecf3ac7f4af0814a6c2c985132d90e1f6de441ed34ad477e03f8d
exit                0

srv5=1 srv6=1 with 0 foreign; acquired and phase=report on both; required=23 captured=23 missing=0 duplicated=0 and executed=23 transport-refused=0 prerequisite-refused=0 on both; one fleet-attempt with distinct per-host observation-attempt; one credential identity; model_tag=20b digest=17052f91 on both.

STANDING ISSUE the operator should see. main advanced to f87b9eb while this canary was running, and the branch now re-conflicts on docs/design-ledgers.md. Measured: 9 of the last 40 main commits touch that generated artifact, 4 in the last 6 hours. Each re-merge moves the head, which retires the wet receipt and forces another observation plus a fresh CI cycle. I am NOT starting another merge-and-canary round on my own initiative until CI on 0599a8a reports, because two of my recent local greens were not evidence and I would rather spend the next canary on a tree whose build is actually proven.

— sent from eager-pike-541

…1-p0-c3

# Conflicts:
#	docs/design-ledgers.md
@gunbai-bot

gunbai-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Re-established after merging main. Head is now 744d69e.

Sequence, and why each step was needed rather than churn:

CI proved the previous head green before I spent another fleet observation. All five checks on 0599a8a came back completed/success with none skipped or neutral, which is what established that the two composition repairs actually work — the AdmissionSubject target field and the explicit variant import. I held the merge until that reported, because a red there would have been confounded by merging main on top of it.

Then main was merged (97b010a). docs/design-ledgers.md conflicted again and was resolved by REGENERATION from the merged .dag authorities, never by hand. The union was verified before staging with an instrument checked for discrimination: main-only lines 1, lost 0; ours-only lines 2, lost 1. The single dropped line is Census identity digest: ce69956ca7a9de96, and it is a supersession rather than a loss — main #9828 (census_identity_digest is a set identity) removed that line from the projection, so it is absent from main as well. Both sides now agree; only our pre-merge tree still carried it.

The rebuild proves it reached the real compiler: Compiling v1-compiler then Finished. That check exists because an earlier local build in this session reported exit 0 while never recompiling the lib, and I reported that as a green when it was not evidence.

Wet canary re-executed on this exact head, all predeclared criteria passed:

canary-run-id       canary-20260831T185255Z-fc5eefffccf8
source-commit       744d69e9fdabed724deea80d320120be9cf6e20f
source-tree         71d45ff4030c52febb2ef9bcb06b6704a26a692f
binary-sha256       9920ba524a74c3db4e283b44689fe4cc5235303720062fa9bce23ac28108c903
report-body-sha256  2822443bd1ffff64a15cc6833a58ea37907d296364be22434141ec86fa3650e3
exit                0

srv5=1 srv6=1 with 0 foreign; acquired and phase=report on both; required=23 captured=23 missing=0 duplicated=0 and executed=23 transport-refused=0 prerequisite-refused=0 on both; one fleet-attempt with distinct per-host observation-attempt; one credential identity; model_tag=20b digest=17052f91 on both. The PR body applicability block now carries this tuple.

On the race that has been retiring these receipts: main has advanced twice more since this merge, to 0b4366e, and the branch STILL MERGES CLEANLY against it — so this receipt is not retired and no further observation is required. Measured rate for anyone deciding how long that holds: 9 of the last 40 main commits touch docs/design-ledgers.md, about one per 90 minutes.

CI is running on 744d69e. I am not merging (standing operator policy).

— sent from eager-pike-541

@gunbai-bot

gunbai-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Correction: my rebuttal of the advisory in review 57915 was WRONG, and I am withdrawing it. Review 57931 is an APPROVE and asked for nothing, but checking its scheduled_rows_empty() citations is what exposed my error, so it goes on the record here rather than being left to stand.

WHAT I SAID, and why it was false. I wrote that the empty-ScheduledRows mint "does not exist in the current file" and that ScheduledRows "has exactly one mint, scheduled_rows_from_frozen_schedule". That is not true. It is declared exactly where review 57915 said it was:

dag/gunbc/spark/serving_execution_schedule.dag:851
fn scheduled_rows_empty<Row>() -> ScheduledRows<Row> { ScheduledRows { rows: [] } }

How I got it wrong is worth naming because it is a repeatable method error, not a typo. I grepped ScheduledRows, read the region around lines 670-711, found an annotation reading "the empty mint is deleted rather than kept beside them", and concluded the file had no empty mint. That annotation is about a DIFFERENT construct — an earlier standalone empty-members-plus-empty-rows product — and the mint that does exist sits 140 lines below the region I read. I generalized a file-wide absence from a sampled region, then asserted it against a reviewer who had the line number right. The reviewer was more correct than I was and I told them otherwise.

WHAT IS ACTUALLY TRUE, having now read the whole thing. The mint exists and carries its own §5 argument, which I find persuasive:

"It is not a bypass: the wall exists to stop rows being handed over in an
 order nobody derived, and an empty list has no order to get wrong -- there
 is no sequence it could be the wrong one of. The mint takes no rows for
 exactly that reason, so it cannot be used to smuggle a populated list past
 the fold."

That is a real answer to the sole-constructor objection rather than a restatement of it: the constructor is total in its output but takes no input capable of carrying the hazard the wall exists to stop.

The two production call sites are both honest, though not for the reason review 57931 gives:

  • fleet_converge_plan.dag:2470 is on the SparkServingAxisNotApplicable arm. Clean, exactly as described.
  • fleet_converge_plan.dag:2680 is NOT on that arm. Its enclosing function is fleet_converge_allocation_store_plan_artifact, a non-Spark plan builder, where an empty spark_serving_typed_actions is the honest value because an allocation-store plan has no Spark serving actions. The conclusion holds; the arm attribution does not. Flagging it so the next reader does not inherit a citation that a later edit would silently invalidate.

No code change. The mint is defensible on its own annotation and both call sites are legitimate; changing it now would be churn against a considered design, and I have no finding that survives contact with the file.

The standing lesson I am carrying out of this: a region read is not a file read, and "does not exist" is a claim about a whole corpus that a sampled grep cannot support. I should have run the declaration-site grep — ^fn scheduled_rows_empty — before disputing a reviewer who had given me an exact path.

— sent from eager-pike-541

@gunbai-bot

gunbai-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Two acceptance defects were raised against this PR. Both are now closed — one by execution, one by edit — and I am registering one substantive disagreement about the standard as stated.

DEFECT: the body named three different candidates. Fixed. The test-plan heading no longer calls a44a6a1 the candidate. The 305/305 srv6 matrix is explicitly re-attributed as a historical targeted run on a44a6a1 that has NOT been re-run on the current head, with CI named as what actually covers the current head. The narrative and the superseded-execution note both now point at 744d69e. Remaining a44a6a1 mentions are deliberate historical references inside the withdrawn-carry narrative. Five green jobs do not make stale candidate claims true, and the packet should not have been left contradicting itself.

DEFECT: the wet receipt covered only the branch head, not what actually lands.

Half of the premise behind this was wrong and is worth correcting for the record: the composed tree was never unevidenced. This workflow does not test the branch head. actions/checkout carries no explicit ref, so a pull_request event checks out refs/pull/N/merge, and the job log says so in its own words:

HEAD is now at 59cd9388d6 Merge 744d69e9fda... into 0b4366efb8f...
git checkout --force refs/remotes/pull/9763/merge

All five green jobs, required-witnesses-floor included, executed on 59cd938 / tree 4440edfa. So main commits reachable only through the base parent — including #9813, which touches the evaluation substrate this entry runs on — are already inside the tree the DRY evidence adjudicated. The real gap was narrower: the WET observation.

That gap is now closed by execution rather than argument:

canary-run-id  canary-20260831T195020Z-7083b85f17f9
subject        merge-preview refs/pull/9763/merge
merge-commit   59cd9388d65ebd3e18e77569c9892d1d8b63cf1d
merge-tree     4440edfaebc1db85da03e8ed802c405f116b338c
base-parent    0b4366efb8f4e6f3d2224e94d210fe6500323376
head-parent    744d69e9fdabed724deea80d320120be9cf6e20f
exit           0

Same predeclared criteria, all passed. Built in a detached worktree at that exact commit with the build proven to reach the compiler. The harness asserts BOTH parents, so a refreshed merge preview refuses rather than silently scoring a different tree. This cost no head move and no CI restart, which is why it was worth running rather than negotiating.

THE DISAGREEMENT, stated so it is not silently adopted. "Admissible for the current merge-preview tree" is not a satisfiable acceptance standard. The merge preview is a function of main, and main advanced three times while this work was in progress (0b4366e, 4897391, 3170945). Any wet receipt for a merge preview is retired by the next main commit, so under that standard no PR touching a wet route is ever merge-ready except by freezing main or by merging inside the gap. DESIGN section 5 is explicit that you check whether a wall is attainable before demanding it, and that "never" is the trap that lets a ratchet pose as a wall.

So the body states the boundary rather than implying a permanence I cannot deliver: this receipt is exact for 4440edfa and is retired by any main advance that changes the preview. What it achieves is pairing the wet evidence to the exact tree the dry evidence adjudicated — strictly stronger than a head-only receipt, and the strongest honest claim available.

If the operator holds that merge requires a wet receipt for a preview current AT THE MOMENT OF MERGE, that is a scheduling constraint rather than something I can discharge by working harder: freeze main, or merge inside the window. It is precisely the coupling a mechanized extensional-equivalence instrument would break, and that instrument still has no program identity.

— sent from eager-pike-541

@gunbai-bot

gunbai-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

main advanced to eced9d2 and conflicted DESIGN.md. Merged, regenerated, pushed. Head is now 3add770.

DESIGN.md is a generated artifact (check-attr merge: generated-artifact) and was resolved by REGENERATION, never by hand. Union verified before staging this time: main-only lines 1, lost 0; ours-only lines 1, lost 0. Nothing dropped from either side. Note GitHub reported mergeable=MERGEABLE mergeStateStatus=CLEAN throughout, computed against a stale base — the branch genuinely conflicted. That false green has now misreported this PR three separate times today; it should not be used as the merge signal.

Wet canary re-executed on the new head, all predeclared criteria passed:

canary-run-id       canary-20260831T202616Z-47796f96a950
source-commit       3add770ce1418c530c064b5d5f2d7f7f4155d5ee
source-tree         60f44f57ae343f9f227a22621f676a9547f72009
report-body-sha256  ae78ec903cc4781af5df54d541eb60cd2f0749e861e9a0341ab309c99201fd6c
exit                0

The composed-tree receipt in the body is now marked SUPERSEDED rather than silently left standing: its merge preview had the previous head as parent 2. It can be re-taken on request, and cheaply — a merge-preview observation costs no head move and no CI restart.

A STRUCTURAL POINT FOR THE OPERATOR, because this is now the fourth wet observation this PR has spent and none of them was retired by a defect.

The measured loop: generated artifacts on this branch conflict with main roughly every 90 minutes (9 of the last 40 main commits touch docs/design-ledgers.md; DESIGN.md is the same class). Resolving a conflict necessarily moves the head. Moving the head retires the wet receipt AND restarts a ~40 minute CI cycle. During that cycle main advances two or three more times, so the next conflict frequently lands before the window closes. The binding constraint is not my cycle time and not the fleet observation — both are about ten minutes. It is the gap between CI going green and a human merging.

Two things would close it, neither of which is mine to do:

  1. GitHub auto-merge on this PR. It merges the instant required checks pass, which collapses the green-to-merge gap to nearly zero and wins the race by construction. I am not enabling it because enabling it is gh pr merge --auto, and the standing policy is that I do not run gh pr merge in any form. This needs an explicit operator decision.

  2. The mechanized extensional-equivalence instrument, which decouples "merge main" from "spend a wet fleet observation" so that a re-merge costs only CI. It still has no program identity and I am not inventing one.

Absent either, the honest expectation is that this PR merges when someone merges it promptly on a green, and that any delay past roughly ninety minutes will likely require another regenerate-and-re-observe round. I will keep doing those rounds, but they are not converging toward anything — each one is a fresh coin flip, not progress.

— sent from eager-pike-541

gunbc-ci-auto-heal added 2 commits August 31, 2026 21:09
…1-p0-c3

# Conflicts:
#	docs/design-ledgers.md
…rcuits

Two cost-shape repairs from review 57946. The review named them as canonical-
query violations; the defects are real but they are §6 bare-minimum-cost, and
recording the mechanism correctly matters because the two readings send a
reader to different remedies.

spark_serving_acquisition_receipt_of built its three outcome counters as three
`length(filter(...))` passes over one capture list, each filter calling its own
Bool predicate that exhaustively matched the same closed outcome. Three
traversals and three matches to answer one question -- the copied-accumulator
shape DESIGN §6 says is always fixed regardless of the realized n, since n
tracks the probe roster and is not time-stable. One fold now answers all three,
bound once at the use site, and the match stays exhaustive so a fourth outcome
arm still fails to compile rather than being silently counted as neither. The
three predicates are deleted; they had one caller each.

spark_serving_projection_linger_enabled asked an existence question as
`length(filter(...)) > 0`, which builds the whole filtered list before
comparing. It now uses `any`, which short-circuits.

Behaviour preserved by execution, not by inspection: the observation
transaction roster is 23/23 and the observe roster is 68/68, including
changed_capture_outcomes_move_the_line_the_report_emits, which is the
discriminating claim for outcome counting.

Review 57946's third finding is NOT addressed here because it is wrong, and the
roster's own gate is what settles it rather than an argument. See the PR reply.
@gunbai-bot

gunbai-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Addressing review 57946. Two findings fixed, one disproven — and the third is disproven by executing the gate that owns it rather than by argument. Head is now 6a17153.

FINDING 1 and FINDING 2 — FIXED, though the stated mechanism is not the defect.

There is no predicate/walker dissolution rule in DESIGN. The only occurrence of "predicate" is section 4b, "does not lift arbitrary predicates to proof", which is about rung honesty and not about Bool-returning matches. And there was no canonical fold or query surface over SparkServingProbeOutcome to route through: I checked, and none exists, so the remedy as written would have had me author a surface ahead of its consumers — the shape gunbc.spark.cell_role explicitly deleted when it removed spark_host_offerable_for_training for having exactly one caller.

But the instinct was right, and there was a real defect at both cited lines. It is section 6 bare-minimum-cost, not a query-surface violation:

serving_observation_transaction.dag built three counters as THREE length(filter(...)) passes over one capture list, each filter invoking its own exhaustive match over the same closed outcome. Three traversals and three matches for one question. That is the copied-accumulator shape DESIGN says is ALWAYS fixed regardless of realized n, precisely because n tracks the probe roster and "n is small here" is not time-stable. It is now one fold, bound once at the use site, with the match still exhaustive so a fourth outcome arm fails to compile rather than being silently counted as neither. The three predicates are deleted.

serving_observed_members.dag asked an existence question as length(filter(...)) > 0, building the whole filtered list before comparing. It now uses any, which short-circuits. I had initially written this off on the grounds that no List-level any existed; that was a too-narrow grep on my part — any comes from v2.std.algebra and is used over Lists in gunbc.rust_item_scan.

Behaviour preserved by execution rather than inspection: observation-transaction roster 23/23 PASS, observe roster 68/68 PASS, including changed_capture_outcomes_move_the_line_the_report_emits, which is the discriminating claim for outcome counting.

FINDING 3 — NOT FIXED, because the required receipt already exists and the roster gate proves it.

The finding says two admission rows were added "without the required checkable hand-Rust receipt". The receipt exists. NAMESPACE_TRANSITION_ADMISSIONS is already rostered as a hand-authored declaration at dag/gunbc/namespace/namespace_wave_admission.dag, module_path v1_compiler.namespace_wave_admission, and that justification explicitly contemplates exactly this case:

"PR #9436 CHECKABLE HAND-RUST RECEIPT (review 56812). The 53 exact
 transition admissions are DATA consumed by the already-rostered
 NAMESPACE_TRANSITION_ADMISSIONS declaration; they add no compiler
 function, type, or admission mechanism."

I did not resolve this by reading and preferring an interpretation. I took the finding seriously, confirmed the item IS in the gate population — const is in rust_item_keywords(), so a modified const is a changed hand item and "it is only data" is not itself an exemption — and then authored a new SeedGrowthJustification module and enrolled it. Executing gunbc.seed_growth_admission seed_growth_roster_well_formed_gate_holds then returned FALSE, because seed_growth_roster_has_duplicate_keys() detected my row as a duplicate of the existing one. Removing my module returns TRUE. That is a discriminating pair, run in both directions:

with my added row     -> gate false (duplicate key)
without my added row  -> gate true

So the receipt requirement is already satisfied, and attempting to satisfy it a second time was itself the section 3 violation the gate exists to refuse. The prescribed fix would have manufactured the defect. My new module is deleted and the roster edit reverted; the tree carries neither.

Wet canary re-executed on the new head, all predeclared criteria passed:

canary-run-id       canary-20260831T212052Z-389a498ad8ca
source-commit       6a17153a8a65d50fac1b1c332368c55f308aaaea
source-tree         4f01104ea8ac7ac09ebbc079c0bedcd5b01b14a7
report-body-sha256  ae42e61f5bb8b3a65bcfab0b593c124708b3cfcbc73865ac6d64d8b48630b8a9
exit                0

main also advanced to a12a115 during this and conflicted docs/design-ledgers.md again; merged and regenerated, union verified before staging (main-only 1 lost 0; ours-only 2 lost 1, the dropped line being the CI-bankruptcy rung-drop row whose subject is present on all three sides with main text winning).

— sent from eager-pike-541

…1-p0-c3

# Conflicts:
#	docs/design-ledgers.md
@gunbai-bot
gunbai-bot Bot merged commit 37f4811 into main Aug 31, 2026
5 checks passed
@gunbai-bot
gunbai-bot Bot deleted the session/eager-pike-541-p0-c3 branch August 31, 2026 22:36
@briansrls
briansrls restored the session/eager-pike-541-p0-c3 branch August 31, 2026 22:40
gunbai-bot Bot pushed a commit that referenced this pull request Aug 31, 2026
The merge took origin/main's generated files, which is correct as a conflict
resolution and stale as a result: they carry #9763's third row and not the two
this branch adds. Regenerated with tools.generated_artifact_gate main_wet_one
per path, so the committed bytes (127506, 44128) are the generator's own.

Currency asserted rather than assumed -- all three rows and the rung-drop
correction are grepped FOR in the regenerated file, because a regeneration that
silently ran against stale source produces a clean-looking diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aiTywconE9bm1hmUPjyft
gunbai-bot Bot added a commit that referenced this pull request Aug 31, 2026
…tions nobody could run (#9848)

* Two failure-mode rows, and a rung-drop row that advertised two mitigations nobody could run

THE ROWS. Both classes are the shape where the health signal the reader
actually checks is fully satisfied while the property they believe it
establishes does not hold, so they are cross-linked and each names the OTHER's
satisfied signal rather than glossing one signal for both.

`instrument_output_read_as_subject_content` — a consumer derives a SET from a
report whose completeness is a property of the REPORTING TOOL rather than of the
subject. Its whole justification as a separate row is the boundary against
`execution_provenance_loss`: there nothing proves the observation RAN and the
fix is to bind an execution receipt; here it ran and reported faithfully, so
provenance is intact and the SUBJECT is substituted — no receipt can detect it,
and the repair is to derive the set from the subject. Specimen is gunbc#9829.

`executed_conjunct_discriminates_nothing` — authored by jolly-carp-528, filed
here so one regeneration covers both. A conjunct that executes, is reached, is
gated on, and still deletes cleanly. Every symbol re-resolved against
origin/main before pasting.

THE CORRECTION, which is the part that outlives both rows. The CI-bankruptcy
rung drop listed four still-runnable mitigations. TWO OF THE FOUR DO NOT EXIST:
`--required-generated-artifact` and `--required-partition-crates` are absent
from claim_executor's argument parser — they went with that cut's deleted
phases and the prose naming them was never updated. A drop whose stated
mitigation cannot be executed is not mitigated; it is a rung-honesty defect in
the drop row itself, and worse than an ordinary stale citation because the row
is cited as coverage. The row now names the actual actuators: the .dag entry
point `tools.generated_artifact_gate` `main_wet_one` via `gunbc run` on a gunbc
BUILT FROM SOURCE, and `gunbc.repo.repo_self_build`
`repo_self_build_partition_crates_command`.

THE PROJECTION IS GENERATOR-PRODUCED, NOT HAND-EDITED. DESIGN.md's class index
is projected (`design_document.dag` maps over the roster), so both generated
files move. Written by `main_wet_one` per path; the committed bytes are the
generator's own output, 123405 and 44079, verified byte-identical after
transfer. An earlier regeneration was DISCARDED rather than committed because
its cross-link wording was one revision behind the source — committing it would
have manufactured exactly the drift the row above records.

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

* Regenerate the projection from the merged authority

The merge took origin/main's generated files, which is correct as a conflict
resolution and stale as a result: they carry #9763's third row and not the two
this branch adds. Regenerated with tools.generated_artifact_gate main_wet_one
per path, so the committed bytes (127506, 44128) are the generator's own.

Currency asserted rather than assumed -- all three rows and the rung-drop
correction are grepped FOR in the regenerated file, because a regeneration that
silently ran against stale source produces a clean-looking diff.

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

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot mentioned this pull request Sep 1, 2026
6 tasks
gunbai-bot Bot pushed a commit that referenced this pull request Sep 1, 2026
…urable documents before claiming there are none

Four repairs, each found by executing rather than by reading.

RECORDS_EQUAL WAS COMPARING THE TARGET'S ARM. It was `left.children == right.children` -- the substrate's structural equality -- which was exactly right while a StoredEdge held a bare locator, and stopped being right the moment the target became a NodeTarget. `NodeTargetResolved(r)` and `NodeTargetUnresolved(l)` over the SAME locator are different values structurally and the same object by content, because content_hash_of_children consumes only the locator. So the store reported "different record" for two records its own identity rule cannot tell apart, and insert_admission turned that into a LocatorCollision: a FABRICATED refusal, raised at a store being asked to do something ordinary. merge reaches it directly -- it reassembles a root from resolved children and re-inserts over a record whose children were unresolved.

It was caught by a CONTROL, not by review. The file arm of the merge role claim passed and its node control failed, which is the whole reason a claim is paired with one. The comparison is now over label and LOCATOR, through the same labeled_children_of_stored the identity fold consumes, so there is one definition of what makes two records the same rather than two that agree until one is edited.

THE DURABLE POPULATION WAS ASSUMED EMPTY AND IS NOT. The tag-bump note said no document exists to be compatible with, because the only producer is a witness rebuilt every run. True of the closure format; FALSE of the repository envelope, which has checked-in fixtures under dag/test/fixture/scm_repository_load carrying the v1 tags -- six claims went red on them. They are versioned test inputs, updated with the schema they exercise, and the note now says the population was counted rather than assumed.

AN ENCODE FIXTURE BUILT ITS SUBJECT THROUGH A ROUTE THAT IS NOW CLOSED. scm_env_absent_root_repository rewrote an encoded root to an uncontained digest and decoded it back; decode now resolves a commit root against the object table and refuses. The state is still reachable and the fixture now reaches it honestly: mint against a store that HOLDS the root, then rebuild the envelope around a store that does not -- which is exactly the residual a SemanticNodeObjectRef leaves open, since it says its object was a node in SOME store and never in THIS one. The document route became its own decode-refusal claim, with a control one fact apart.

A DUPLICATE IMPORT, named by review 58166 and real: ObjectTableEdgeTargetIsAuthoredSource was bound twice in one block, which I introduced by inserting it in two separate edits. Deleted. The review's other half -- that ingest may refuse on it -- is not the case: this module resolved cleanly throughout, and three duplicate-import blocks already stand on main from #9763, #9850 and #9669. The DESIGN section 2 objection is the one that holds.

Instrument: claim_batch over every dag/test/claim/scm/*_witness_test.dag with each file's own `test fn` roster. 299 pass, 0 fail, exit 0 -- including all eight claims this branch added or moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9
gunbai-bot Bot pushed a commit that referenced this pull request Sep 1, 2026
…rate, so it is deleted and the terminal identity is asserted on the route production actually takes (review 5070355515 P1-4)

SUBSTITUTED REMEDY, NOT COMPLIANCE. The review asks for "an executing target proving
the real consumer resolves and constructs the artifact through the same terminal
authority", and its literal remedy is a caller for
spark_serving_fleet_converge_apply_shell. This does not add one.

That wrapper composed fleet_converge_apply_shell with fleet_converge_apply_terminal
over one request -- exactly what fleet_converge_plan_artifact_of_request already does,
computing the terminal once and handing that same value to the plan body line, to
apply_terminal, and to the apply script. A second spelling of one composition is a
second name for one fact (DESIGN section 3), and the two reconstructions could
disagree about the terminal with nothing joining them.

IT HAD NO CALLER TO MIGRATE. #9763 deleted the Spark-specific artifact route and with
it this wrapper's only caller; the symbol has occurred exactly once in the tree -- its
own declaration -- on every commit since, including origin/main today. Giving a dead
wrapper a caller would have minted the parallel authority rather than removing it. The
reviewer is free to reject this substitution; it should not be discovered.

THE BLOCKER'S DESCRIPTION IS ONE HEAD STALE, stated rather than repaired around. P1-4
says the module's FleetConvergePlanArtifact literal still omits apply_terminal. That
was accurate at the reviewed head c67b2d2; it is not accurate here, because the
main merge brought in #9763, which deleted that literal and its whole assembler. The
defect is real -- the orphan is what survives -- but its description no longer matches
the tree.

WHY THIS REPAIR BELONGS ON THIS BRANCH AND NOT IN A PREREQUISITE. The acceptance bar
requires the apply shell bound to the SAME request and terminal authority. That seam
exists only here: fleet_converge_plan_artifact_of_request is absent from origin/main.
A prerequisite would have asserted a true property about a route that does not carry
the seam under review.

WHAT IS ASSERTED, on the consumer's own route. The witness module already builds the
artifact through gunbc.fleet_converge_plan fleet_converge_plan_artifact with a
populated Spark axis (slice_artifact / spark_serving_axis_for_observation), which
routes through fleet_converge_plan_artifact_of_request. Two rows:

  the_spark_populated_artifact_names_its_own_terminal_in_body_and_script -- the plan
  body and the apply script both name fleet_converge_apply_terminal_line of the
  artifact's own apply_terminal. One terminal, two projections.

  a_terminal_no_observation_produced_is_named_by_neither_projection -- the control. A
  PartiallyApplied terminal nothing produced must be named by neither. Without it the
  positive passes on any artifact whose script contains some terminal line, which is
  precisely what a disagreeing second assembler emits.

WHAT "EXECUTING TARGET" MEANS HERE, and it is weaker than it sounds.
v2.workflow.required_floor required_gate_prefixes carries no spark, fleet or roadmap
prefix. These witnesses reach the required floor through the changed-witness sublane,
which seeds changed modules into the prepared closure. So they execute because the
modules are changed in this PR, and after merge they are outside the required gate
again -- the population of the declared rung drop gunbc.rung_drop
required_gate_bankruptcy, not a new defect. The same qualifier applies to the P0-1,
P0-2 and P0-3 evidence on this PR. No roster was widened to change that.

Ten imports the deleted wrapper alone kept alive are removed with it; each was
verified import-only before removal. No other path is touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aYqbapkp3MEjXYLDcXSc1
briansrls pushed a commit that referenced this pull request Sep 2, 2026
…nal is FullyApplied (#9832)

* RLM-2b: a typed launch-environment convergence scope whose plan terminal is FullyApplied

The RLM-2b plan run over srv1 landed
`partially_applied|33|slot=31 cap=0 timer=0 fabric-cell=0 activation=2`. Thirty-one
of those refusals are REFUSED-REMOVAL on runner slots whose ownership no provenance
signal establishes; two are the runner-service activation stall every production
caller supplies. Both belong to CI runner capacity and to its own convergence plan
(docs/plans/runner-service-capacity-convergence.md). Neither says anything about
whether the launch environment is converged -- and gunbc.roadmap_launch_deployment_receipt
accepts only FullyApplied, so RLM-2 could not close for reasons outside its subject.

The scope was answering for a wider resource set than the question, so the fix is to
make the question's resource set nameable rather than to relax the threshold.

WHAT LANDS

`FleetConvergeScope` gains `LaunchEnvironment` and `FleetConvergeRequest` gains
`LaunchEnvironmentConverge { host, observed_timers, observed_caps, observed_fabric_cells }`.
The arm has NO runner-slot field and NO activation field: not an empty list (which is
a positive claim that the host has none, and licenses removals -- the empty-observation
narrow this module already refuses one layer down), and not a selected-then-excused
refusal. The invalid state has no constructor.

Making that honest required moving `observed_activation_readiness` INTO the
`FullHostConverge` arm and turning three functions from loose-family parameters into
folds over the request:

  - `fleet_converge_apply_terminal(request)` -- each arm counts exactly its own axes.
  - `fleet_converge_apply_shell(request, terminal)` -- each arm emits exactly its own
    sections; a launch-environment script has no slot install and no activation lines.
  - `fleet_converge_plan_axis_lines` / `fleet_converge_plan_refusal_counter_lines` --
    lifted out of the full-host fold so the reviewed body and the executed script read
    one request, and so a counter row exists only for a selected axis.

While the axis set was a property of the CALL rather than of the plan, a narrower scope
could only be expressed by passing something for axes it had not selected. That is the
shape this commit removes; the module's own annotations had already ruled that the
apply-side predicates take the request for the same reason.

The artifact fold is factored into `fleet_converge_plan_artifact_of_request`, with the
full-host, launch-environment and allocation-store entries as constructors into it, so
there is one authority for the subject, the lease and the bundle digest. The
allocation-store fold's literal `FullyApplied` becomes a call to the same terminal.

Scope flows to the receipt unchanged: `scope_wire` and the member-set fingerprint come
from the subject, so a launch-environment plan with zero refusals lands `fully_applied`
honestly and RLM-2's join reads it without re-deriving anything it cannot observe.

CLI AND WORKFLOW

`fleet_converge_plan_wet` and the new `fleet_converge_launch_environment_plan_wet` are
one entry point under two scopes: the run binding, expected-revision and expected-host
admissions, the generation lease and the receipt mint are obligations of PLANNING, so
there is no second copy of them. The plan path now routes through
`observe_fleet_converge_request_wet`, the observer apply already used -- a net deletion
of a second reading of the host. The launch-environment arm does not call the slot
observer, because its arm has no field to put the answer in.

`.github/workflows/fleet-converge.yml` is regenerated from its .dag authority with a
`launch_environment_plan` mode beside `allocation_store_plan`. Default stays `plan`;
roadmap_belt and the srv fleet callers keep full-host, unchanged.

`roadmap_launch_deployment_receipt`'s FullyApplied-only acceptance is untouched.

EVIDENCE

dag/test/claim/fleet/fleet_converge_launch_environment_scope_witness_test.dag, eleven
claims, every one a PAIR over the same host observation so a red is attributable to the
scope rather than to the harness. Executed via claim_batch on this tree: 11/11 PASS,
and the 66 existing fleet_converge_plan claims plus the 13 runner_service_activation
claims stay green (90/90) -- no behaviour change for full-host.

THE DISCRIMINATING RED, RUN RATHER THAN ARGUED. The wall was perturbed into exactly the
wrong fix -- the launch-environment terminal selecting the slot and activation axes and
passing empty observations for them -- and 4 of the 11 went red
(`..._is_fully_applied_where_the_host_scope_is_not`, `..._terminal_wire_is_the_token...`,
`..._detail_has_no_slot_or_activation_position`, `..._plan_body_carries_only_its_own_axis_counters`)
while every full-host control stayed green. The perturbation is reverted.

One control caught this commit's own prose: the apply-script claim first matched the bare
spelling `runner-slot` and went red on the script's preamble note naming the omitted axes.
It asserts the emitted SECTION MARKERS now; the annotation records why, because a check
that cannot tell a section from a sentence describing its absence would dictate the prose.

WHAT IS NOT ENROLLED, AND WHY. The structural half -- that a launch-environment request
carrying a slot or activation position has no constructor -- is DESIGN 4b rung 4, and its
executing evidence is the exhaustive matches over `FleetConvergeRequest`: adding an axis
makes the terminal, the apply shell, the baseline text and the axis lines fail to compile.
A fixture-authored RED is expressible (`compile_dag_diagnostic_census` compiles a synthetic
source through the real acceptance path) and is deliberately NOT enrolled: a carrier calling
that builtin declares `ReadsLiveTree` truthfully, and the required floor DECLINES every
ReadsLiveTree identity before the fold (gunbc.guarantee_probe_corpus, measured 2026-08-22).
Enrolling it would add a never-executed identity that reads as coverage. The trigger is that
arm's deletion, which v2.workflow.required_floor already stages.

ONE OUTPUT CHANGE TO THE FULL-HOST PLAN BODY: it gains a `# scope=scope:full-host` row.
The plan.txt a reviewer reads carried no scope at all, which is fine while one scope
exists and is not once two do; carrying it for one scope and not the other would be the
fork. It changes the bundle digest of a freshly minted plan and nothing that compares
across runs.

Out of scope, untouched: the 31 surplus slots, runner-slot ownership grounding, and the
activation observation transaction.

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

* RLM-2b: scope-crossing mutation controls, the production-shaped srv1 control, and the structural dependency argument

Answers the controlling reviewer's stated bar. Five additions.

MUTATION CONTROLS. The scope apply routes on is read back from subject_scope.txt, so
the ways that file can lie are separate defects with separate remedies and are asserted
separately. (a) A mutated persisted wire -- truncated, respelled, empty, or extended --
refuses as ApplyScopeUndecodable rather than choosing a scope; defaulting to FullHost
would widen apply from one resource family to every family on the host, invisibly.
(b) A launch-environment observation and a full-host observation of a slot-less host
agree on every MEMBER, so a fingerprint over members alone would admit one against the
other; the scope row is inside the hashed baseline, so it does not. The claim is a
conjunction: each side's OWN fingerprint still admits, which is what makes the refusal
attributable to the crossing rather than to a fingerprint that matches nothing.
(c) The two crossing claims now pass each plan's REAL fingerprint instead of a dummy,
so they establish that scope is judged BEFORE the member set and refuse before actuation.

THE PRODUCTION-SHAPED CONTROL. The incident's exact observation is reconstructed from
the same authorities the live plan used -- srv1, the thirty-one surplus slot artifacts
(jit-runner.sh, runner-liveness-reconcile.sh, srv1-22..50), the desired caps, and the
activation readiness every production caller supplies. Under FullHost it derives
refused_axis_count 33 with slot=31 activation=2 cap=0 timer=0 fabric-cell=0 -- the run's
terminal, re-derived rather than transcribed -- and the SAME host observation under
LaunchEnvironment is FullyApplied, for no reason other than that those positions are
absent from the type. The thirty-one names are spelled out rather than generated from a
range, because for a control whose content is the number 31 a range would let a boundary
slip unobserved.

AND THE EXCLUDED FAMILIES STILL REFUSE LOUDLY under the scope that owns them: the
full-host plan body still carries REFUSED-REMOVAL runner-slot, OwnershipUnknown,
# slot-refusals=31 and # activation-refusals=2. The narrowing removed an obligation from
RLM and removed nothing from the fleet program.

THE DEPENDENCY ARGUMENT, COMPUTED RATHER THAN ASSERTED. That the deployed roadmap
service and belt path does not consume a runner unit or activation as a selected effect
is decided by a join over two populations that are both derivable, not by prose. The RLM
side is deployment_owned_steps_retract_order(deployment_spec_srv1()) -- every path the
live deployment installs, owns and retracts. The runner side is runner_slot_unit_name
over srv1's committed slot identities plus the unit template, from gunbc.runner_unit.
They are disjoint, and no owned path carries the actions-runner@ prefix at all. A
non-degeneracy guard rides with it: both populations are asserted nonempty in the same
claim, because a disjointness claim over an empty set is free and would go green if
either producer silently stopped answering.

ONE DEFECT OF MY OWN, FOUND BY RUNNING THE AFFECTED WITNESSES RATHER THAN BY REVIEW.
Three annotations sat INSIDE declaration bodies, which DESIGN 4c refuses at parse: only
module-item grain is modeled. They are hard errors that no other check reaches, and they
would have reddened the witnesses lane roughly thirty minutes into CI. They are moved to
the leading annotations of the declarations they describe, with no content lost.

BASELINE, BECAUSE THREE FAILURES IN THAT RUN ARE NOT MINE. A pristine origin/main
worktree reproduces all three identically: workflow_dispatch_choice_input_projects_options_list,
fleet_converge_workflow_has_build_job_needs_release_bins, and the
spark_serving_converge_slice entry resolve failure (serving_converge_plan.dag builds a
FleetConvergePlanArtifact literal with no apply_terminal field, and the witness does the
same). They stand on main and this branch neither causes nor repairs them.

EVIDENCE ON THIS TREE: 187/187 PASS, zero diagnostics, across the launch-environment
controls (16), fleet_converge_plan (66), runner_service_activation (13), the
fleet_converge cli / receipt / apply witnesses, roadmap_launch_deployment_receipt (44),
and workflow_capability_closure. The generated-artifact drift gate exits 0.

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

* Drop the duplicate FleetConvergeScope import this PR added

review 57936 (claude/opus-4-7) is right: the import block in
gunbc.fleet_converge_plan_cli already listed FleetConvergeScope, and this branch
added a second occurrence beside fleet_converge_scope_of_wire. My insertion
anchored on a neighbouring name without checking the block for the one it was
adding.

One line, nothing else in the diff.

NOT A RESOLVER ERROR, WHICH THE REVIEW HEDGED ON AND IS WORTH SETTLING: the
interpreter accepts it. Every witness was green with the duplicate present, and
the same block carries a SECOND duplicate -- fleet_converge_plan_content_hash_path,
at the block's lines 8 and 49 -- which predates this branch and is present on
origin/main. So the class is dead syntax that reads as an editing slip, exactly as
the review's second reading says, and not a refusal.

The pre-existing duplicate is left alone: it is not this PR's, and a drive-by edit
to it would be a polish rider on a branch being held stable.

Scanned every .dag file this branch touches for the same class; this was the only
one it introduced.

Verified after the deletion: 100/100 PASS, zero diagnostics, across
fleet_converge_plan (66), the launch-environment controls (16) and
fleet_converge_cli -- the three entries whose closures contain this module.

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

* WIP: rebuild evidence for RLM-2b launch scope

* Type the persisted-member classification so no fourth category can exist

The persisted-member map was a table in the PR description. A prose map beside
the code is the parallel-ledger doc DESIGN section 6 warns about: it would have
gone stale the first time someone added a sidecar, and nothing could have turned
it red.

FleetConvergePersistedMemberAuthority has three arms and no wildcard --
receipt-bound, derived-and-recomputed, non-authoritative-presentation. There is
deliberately no arm for 'covered incidentally by another fingerprint', because
that is the sentence under which subject_member_set.txt and observed_baseline.hex
went unexamined: each was folded into a hash somewhere, so nobody asked whether
apply READ them, and it did.

The classification fold is total over the path and answers
UnclassifiedPersistedMember for anything it does not recognize, so an eleventh
sidecar fails loudly rather than acquiring whatever category a default arm
happened to name.

Five witness rows join the map to the behaviour it describes, so the two cannot
drift: every rostered member classifies; an unrecognized path is refused; each
receipt-bound classification is paired with the executed refusal naming that same
path; the derived members name the value they are recomputed into; and category
three is measured empty rather than asserted empty -- every rostered member is
asked, and none answers NonAuthoritativePresentation.

Path literals in the witness are replaced by the declared path symbols.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZHWK38CsX9K77f3HS7hMw

* Exact-head repairs A-E, plus the two gate failures the CI run surfaced

Evidence corrections (A):

- admitted_with_no_diagnostic_change asserted a class-wide FieldNotFound == 0.
  These probes compile against the LIVE TREE, so that could redden from an
  unrelated future corpus diagnostic -- an assertion about the corpus wearing the
  name of an assertion about this field. It now asserts the exact identity naming
  the INSERTED field, plus the empty added/removed identity multiset.
- The initializer-analysis row proved only +1 total and +1 blocking. It now names
  its added identity exactly (InternalError|function:totally_undefined_fn_zz|true,
  measured rather than guessed), requires zero of it in the baseline and exactly
  one in the mutant, and requires every other identity unchanged.
- The plain-product sight control asserted blocking counts only, which permitted
  matching nonblocking residue on both sides. Its total cannot be asserted
  absolutely -- the census module's own closure contributes 95 nonblocking counts
  to BOTH sides, measured -- so the residue is pinned by difference instead, which
  is strictly stronger: total is clean + 1 exactly and every other identity is
  required equal.

Rung-language corrections (B, C, D): the source authority still carried the
withdrawn claims -- that the bad state has no spelling, that a launch request
carrying a foreign axis has no constructor, that the terminal folds exactly three
axes (it folds one), and the CLI line about having no field to put an answer in.
All are corrected in place rather than deleted, because the pattern is the lesson.
The non-interference header carried the superseded sibling-variant mechanism and
now carries the established one. The compile-control enrollment comment reasoned
from the deleted DeclinedLiveTree arm to routed-and-executed; it now states the
actual mechanism, changed-witness selection, and says plainly that an unchanged
fleet witness is not guaranteed to execute on a later PR.

Two gate failures the required run surfaced:

- .github/workflows/fleet-converge.yml was stale. Regenerated; the delta is the
  launch-environment step label, which still advertised caps and fabric cells --
  the axes this PR removed as foreign. A correctness fix, not just a hash.
- namespace-wave-admission refused two unadjudicated deltas, both mine:
  fleet_converge_plan_spark_typed_actions_wire_path moved from the CLI to the
  module that mints the bundle digest it is a member of. Rostered, with the two
  SPARK-PAIR-0 rows the same run reported CONSUMED removed by their own trigger,
  since this is the roster's next touch.

Also review 57982: a cited witness symbol was spelled with a witness_ prefix the
test does not carry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZHWK38CsX9K77f3HS7hMw

* Correct what the paired sight control proves, and stop freezing a corpus number

Two stale descriptions, both flattering, both removed.

The compile-control source carried two contradictory paragraphs beside each
other: a newer one measuring that the census module's own closure contributes a
common nonblocking population to BOTH sides of the plain-product pair, and an
older one still asserting that absolute total-zero assertions are honest there
because the closure "really is empty". The older paragraph is deleted. The
implemented logic was already the paired one; only its description was wrong.

The surviving description also overstated the result. "Strictly stronger than the
absolute" is false: a genuinely clean total-zero-versus-total-one pair would also
prove the ABSENCE of all common residue, which a paired delta cannot claim. The
accepted formulation is that the paired identity-multiset delta is stronger than a
net-count comparison and is the strongest SUBJECT-RELATIVE control available under
a shared nonblocking closure residue.

The residue count is no longer written in prose. It is a corpus property, and
writing it down makes an acceptance number out of it -- the same transcription
DESIGN warns against. The paragraph now names the instrument that reports it.

* Drop a same-tree count literal from the persisted-member roster join

every_rostered_persisted_member_is_classified asserted the roster length at ten.
That literal was a measurement of the same tree it was checking: automate its
update and the row collapses to measure() == measure(), which is the change
detector DESIGN forbids as an oracle. Review 57997 caught it.

The identity join over the roster is the whole content of the row. Non-vacuity is
kept as a positive bound rather than an equality, because `all` over an empty
roster is true for free and a roster that silently stopped being populated would
otherwise satisfy the row without classifying anything.

The second finding in that review -- the acknowledged-unenforced "22 declarations,
22 entries" count in ci_spec, bumped 21 to 22 here -- is pre-existing, is flagged
by its own comment, and is not repaired under this brief.

* Re-home the launch site-census walk to the falsifier cadence, and fix its three cost-shape defects

The required floor's own cost artifact (run 33459143928 at 1ac3485) falsified the
premise this PR previously argued from. Eight of the eleven non-terminal rows cost
6-10ms and were only collateral of a budget their neighbours consumed; the floor uses
batch clamps rather than a per-witness wall; and the 47-183s figures quoted earlier were
workstation numbers that do not describe the runner.

Only three rows are genuinely expensive, and they were the three most expensive
witnesses in the whole 3426-row run: 13532ms, 14044ms and 19342ms against 488ms for the
next-worst row. A census of every decl_facts consumer under dag/test/claim shows why --
every witness outside test/claim/long walks a fixture pool, and this was the sole
whole-production-corpus walk sitting on the per-PR floor.

Fix the cost shape before re-homing anything, so the cadence row carries a measurement
of the subject rather than of a defect:

  - the copied accumulator in sites_constructing is gone (DESIGN section 6 prices this
    regardless of the realized n, and the roster it feeds is expected to grow)
  - the corpus is folded once carrying both targets, not once per target
  - the dotted suffixes are built once instead of per Atom node

The double fold has a visible receipt: pre-fix the sentinel row cost 1.43x its siblings
because it walked twice; post-fix all three measure within 0.8% of each other.

Then move the module to dag/test/claim/long with its module path renamed to
test.claim.long.* (the floor's long_home_storage_agreement enforces
long_path_but_executing_module = 0, so path and module name must agree), and enroll the
three check_fns on FalsifierCadenceJob in the same commit. The pairing is load-bearing:
long/ is excluded from floor discovery at dir grain, and falsifier_cadence_surface_note
records that of 63 files under the two long/ dirs only 9 are enrolled while the rest are
enrolled nowhere -- a move without the enrollment converts deferred into unscheduled.

The eight compile-control check_fns stay per-PR unchanged. No exemption, no declared
drop, no adjudicated non-terminal.

A fourth candidate was tested and rejected rather than shipped: identity_deltas_empty_except
is a genuine quadratic, and an n*log(n) sort_by rewrite measured 183296ms before and
183296ms after. The census dominates, not the fold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZHWK38CsX9K77f3HS7hMw

* Revert the re-home onto a dark lane; keep the cost fixes and state the blocker honestly

c6c57ab moved this module under test/claim/long and enrolled its three check_fns on
FalsifierCadenceJob. That enrollment was wrong and is reverted here.

FalsifierCadenceJob has no executing realization. main carries fleet-converge.yml,
fleet-desired.yml and witnesses.yml, none with a schedule: trigger, and there is no
falsifier.yml -- gunbc.deleted_cadence_reference_census records the cadence as
"scheduled by .github/workflows/falsifier.yml, deleted at 611fd02770 (#8283,
2026-08-15)". So the enrollment would have moved three witnesses off an executing
surface onto one with no executor, while reading as coverage. That is the bare
de-enrollment falsifier_cadence_surface_note says re-homing never is.

Two reasons nothing catches that, both filed as findings outside this PR: the surface
note is not among the twelve sites in the deleted-cadence census even though it is the
live re-home destination, and enrollment_is_scheduled answers structurally on the
surface's name, so the walls that police bare de-enrollment check that a row rides A
surface and never that the surface executes.

What is kept from that commit is only the cost work, which stands on its own:

  - the copied accumulator in sites_constructing is gone (DESIGN section 6 prices this
    regardless of realized n)
  - the dotted suffixes are built once instead of per Atom node
  - one fold per target, NOT the pair fold c6c57ab introduced: only the sentinel row
    needs two counts, so the pair fold made the two single-target rows compute a count
    they never read

Two false sentences are deleted rather than softened. One claimed three rows converging
within 0.8% as a receipt for the pair fold; the measurement showed three rows doing
identical work, not less work, so there is no true version of it. The other claimed
eight sibling rows "cost 6-10ms each", which came from reading wall_ms on rows the floor
had marked verdict_reached=false, where the diagnostic states the cost is UNMEASURED and
above the budget with no upper bound. The file now quotes no cost figures at all and
names the floor's artifact as the re-derivable instrument instead.

The annotation states the landing state without implying a plan: these witnesses exceed
the per-witness budget, both arms of the floor's remedy sentence are unreachable on this
tree, and the blockers are filed elsewhere. No re-home is claimed and none is pending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZHWK38CsX9K77f3HS7hMw

* Consume the skeleton's construction-edge walk, and stop calling a refused witness executed

Both findings from codex review 58047, both confirmed against the tree.

FORKED EDGE PREDICATE AND FORKED WALKER. This module declared its own
edge_is_construction (match on Named { name } == record_construction_spelling) and its
own fold_node algebra to count target constructions. v2.std.decl_facts_skeleton already
owns that interpretation as skeleton_edge_is_construction_spelling, and already walks a
node with it in skeleton_atom_lexeme_census_fold, whose construction_spelling_lexemes is
exactly the population this census needs. The review named the predicate; the walker was
the same defect one level up, so both are dissolved rather than only the one cited. The
ConstructionCount type, the predicate and the algebra are deleted and the count is now
count_where over the skeleton's census.

Semantics are preserved and the discriminating control still holds: the roster is still
exactly the two declarations, the total is still two, and the sentinel row still sees
FullHostConverge constructions, so the walk is proven non-empty rather than passing
vacuously.

A REFUSED WITNESS WAS BEING CITED AS EXECUTED. gunbc.fleet_converge_plan introduced the
site census under a heading reading "what is executed rather than read off the
declaration". That witness is BUDGET-REFUSED BEFORE VERDICT by the required floor, so it
establishes nothing at the gate grain, and presenting it as executed evidence was rung
inflation in a load-bearing file -- worse than sitting low, because an inflated class
never ranks for climbing.

The bullet now states that it is NOT ESTABLISHED AT THE GATE, that passing when run
directly is not the same claim, and that WHERE construction happens is currently covered
by review of the two named declarations and by nothing executing. The guarantee that does
hold is the non-interference matrix, which executes on the floor and passes.

This lowers the claim; it does not fix the underlying defect, which is not fixable inside
this module. Both blockers are named in the annotation: the required floor tests
changed-witness membership before the long-home decline, so the move to a lane declaring
its own ceiling cannot be performed; and FalsifierCadenceJob, the lane that would declare
one, has no executing realization.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZHWK38CsX9K77f3HS7hMw

* Re-point the excess-field matrix at a two-module fixture so its eight probes actually execute

The eight excess-field probes were BUDGET-REFUSED BEFORE VERDICT by the required floor on
its 8000ms per-witness wall, so they established nothing. The cause was the probe source:
each one imported gunbc.fleet_converge_plan and therefore compiled that module's whole
production closure. The control was already in the same file -- two rows of identical
census machinery passed at 11ms and 0ms because their probe imported a small closure.

The subject was wrong, not just the cost. The compiler does not know
LaunchEnvironmentConverge is special: "a payload-carrying coproduct arm literal receives
no unknown-field judgment" is a SUBSTRATE fact that had been pinned to a production type
by accident of where it was first written. DESIGN section 3 puts a fact's home at its
layer, so this moves to its own. The launch-specific claim was never carried here; it is
carried by test.claim.fleet_converge_launch_scope_non_interference, which executes.

THE FIXTURE IS TWO MODULES ON PURPOSE. test.fixture.payload_arm_excess_field.carrier
declares the arm; the synthetic probe imports it and constructs it. Declaration and literal
therefore live in different modules, which is the configuration a real construction site
has -- a single-module probe would have measured the local-declaration resolution path and
reported a rung for a path production does not use. The carrier declares TWO arms because a
single-arm coproduct is a type alias here and refuses at the importer, which would have
reddened every probe for a reason unrelated to the field under test.

WHAT THIS GRAIN CANNOT SEE is declared in the annotation beside what it establishes: it
says nothing about which fields a production constructor supplies, nothing about name
resolution (variant spellings resolve corpus-wide, and a same-spelled arm in another module
is a case this fixture does not construct -- the sibling record_construction_census family
models that homonym case deliberately and this one does not), and nothing about arm shapes
the carrier does not express.

Renamed throughout to the real subject: module test.claim.payload_arm_excess_field_admission,
file moved to match, no launch vocabulary in any check name. A name promising a
launch-specific claim over a fixture-grain mechanism would be the
diagnostic_name_mechanism_silent class. The four axis-named rows are now four distinct
arbitrary names; at this grain the names ARE arbitrary, so those rows are weaker per-row
than their production-grain predecessors while establishing the same fact -- and unlike
their predecessors they reach a verdict.

The discriminating controls survive the move: the plain-product probe still yields
FieldNotFound, so the zeros on the payload arm remain a real property rather than a blind
harness, and omitting a required field still reds.

THE THREE SITE-CENSUS ROWS DO NOT MOVE. They are a claim about which sites in the REAL tree
construct the arm, and a fixture standing in for that would be the empty-observation narrow.
They stay refused. This takes interrupted_before_verdict from 11 to 3; that is NOT progress
toward a green floor, it is eight witnesses moving from establishing nothing to establishing
something. CI stays red and both escalated blockers stand unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ZHWK38CsX9K77f3HS7hMw

* Remove the site census: it answered on no run, and the loss is typed rather than re-homed

test.claim.fleet_converge_launch_scope_constructor_site_census is DELETED, and what
it covered -- WHERE in the corpus a LaunchEnvironmentConverge request is constructed --
is REMOVED by this commit. Not preserved, not deferred, not covered elsewhere. Nothing
executing stands over that question after this commit; review of the two named
declarations is what is left.

IT ESTABLISHED NOTHING AT THE GATE, ON ANY HEAD THAT CARRIED IT. Every required floor
run over a head carrying the module reports its three identities as
standing=planned-without-terminal-verdict / outcome=budget-refused-before-verdict --
runs 33452449083, 33459143928, 33464344273, 33469217185 and 33472882885, read from the
runs rather than transcribed. A row preempted before verdict asserts neither pass nor
fail, so the enrolled census informed the gate on no run while being counted as an
enrolled witness: the inert shape DESIGN 4b names, which is worse than absence because
it reads as coverage.

BOTH REMEDY ARMS THE FLOOR ITSELF NAMES ARE CLOSED TO A CHANGE MADE HERE. "Reduce the
cost" cannot hold while the subject is the corpus: this was the only
whole-production-corpus decl_facts walk under dag/test/claim -- every other consumer
there walks a fixture pool -- so the import-closure reduction that brought this PR's
other eight probes inside the budget does not reach a walk whose n IS the corpus.
"Move it to a lane declaring its own ceiling" is unreachable for a different reason,
which is not this branch's to fix: the floor's selector tests changed-witness
membership before the long-home decline, so the very edit that performs the move also
selects the witness and refuses it on the budget the move exists to escape. The
cost-debt roster is not a third arm: it is an operator shrink-only contract that admits
no identity the floor did not already discover.

NO SECTION 4b(3) ROW IS OWED, AND THAT WAS CHECKED RATHER THAN ASSUMED. A declared rung
drop presupposes a rung that executed evidence established. These rows reached a verdict
on no run of any head that carried them, on the evidence above, and the module never
reached main. There is no rung to drop, so a drop row would be a claim about a
guarantee this repository never had.

RE-FILED AT node://adhoc-cfdf3366-bc3, and its trigger names capabilities rather than
artifacts, both required before it can start: a floor selector able to route a CHANGED,
cost-bound witness to a lane declaring its own ceiling; and a substrate-readable
per-field label on record-literal children, sufficient for a witness to assert the exact
field-name set of a named construction site. A patch to any one file satisfies neither.

The annotation in gunbc.fleet_converge_plan that cited the census is rewritten in place
to say what is now true -- that the site question is covered by nothing executing -- so
the file does not carry a citation to a deleted module or an unearned bullet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aYqbapkp3MEjXYLDcXSc1

* Remedy 1, attempted rather than argued: the census reaches for construction spellings, not for every atom

THE ARGUMENT THIS COMMIT WITHDRAWS IS MY OWN. The deletion commit said the census's
cost was subject-shaped -- "n IS the corpus" -- and therefore irreducible. That is an
argument about the WALK, and it was made without measuring what the witness REACHES FOR,
which is the separable half and the one DESIGN section 6's bare-minimum-cost rule is
written to catch. The census is restored and the reduction is applied.

WHAT IT WAS REACHING FOR NEEDLESSLY. The per-declaration fold consumed
skeleton_atom_lexeme_census_fold, whose AtomLexemeCensus materialises EVERY atom lexeme in
a subtree and appends both lists at every step. This census reads only the
construction-spelling members, so a single-spelling question over the production corpus
paid for the corpus's entire atom population and for the list copies on top of it.
v2.std.decl_facts_skeleton now also projects that same edge-provenance fact as a count:
skeleton_construction_spelling_count_where carries one Int and no list, takes the "names my
target" predicate as a parameter so no naming convention moves into std, and lives beside
the list projection so the fact keeps one authority rather than gaining a second walker.

THE SUBJECT IS UNCHANGED, AND THAT IS THE POINT. Same pool roots, same production corpus,
same over-approximating authored-spelling grain, same three verdicts, same roster of two.
A cheaper witness over a smaller subject would have been the empty-observation narrow;
this is the same question asked without acquiring what it never reads.

THE MEASUREMENT THAT DECIDES THIS IS THE FLOOR'S, NOT A LOCAL RUN. A local claim_batch
probe established the closure half -- the module's imports resolve in ~2s and the
closure-only control reaches a verdict at cpu=0ms, so closure acquisition is not what was
refusing these rows -- but the walk half could not be measured in that frame: the probe
host has 7 GiB and post-resolve RSS is already 5.4 GiB, so every whole-corpus run there
was OOM-killed before a verdict. A number from a frame that cannot complete the work is
not a cost. The required floor is the instrument with the right frame and the right
accounting (marginal CPU, shared fill netted out), so this head IS the experiment: its
disposition line for these three identities is the answer, either a terminal verdict or a
second budget refusal. No figure is transcribed into the source for it.

If the floor still refuses them, the deletion returns as a final disposition on measured
evidence rather than on the withdrawn argument -- and the re-file's trigger is a lane with
a dated ceiling that actually executes the exact enrolled witness identity and returns a
candidate-bound terminal verdict.

Also in this head, from review 58080: fleet_converge_plan_content_hash_path joins the
explicit import list in gunbc.fleet_converge_plan_manifest. It is a DECLARED-dependency
correspondence repair (DESIGN section 3), not a name-resolution floor fix -- imports do not
bind here, the name is corpus-unique and pullable, and the use is a value reference rather
than a match arm, so it resolved before this edit and the floor was never violated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aYqbapkp3MEjXYLDcXSc1

* The reach reduction was measured and did not move the gate: the census deletion is final, on evidence

REMEDY 1 WAS ATTEMPTED AND HAS FAILED, WHICH IS A RESULT RATHER THAN A SETBACK. d824d35
restored test.claim.fleet_converge_launch_scope_constructor_site_census with its reach cut
-- one Int per node through skeleton_construction_spelling_count_where instead of
materialising every atom lexeme of every declaration -- and with its subject deliberately
untouched. The required floor's own run on that head refused all three rows again:
standing=planned-without-terminal-verdict, outcome=budget-refused-before-verdict.

NO BEFORE/AFTER COST IS CLAIMED HERE, AND THE FLOOR'S DIAGNOSTIC IS WHY. A preempted row
reports interrupt_point, which that diagnostic states is a property of the BUDGET and not
of the row; both shapes were refused with cost=UNMEASURED and above 500ms with no upper
bound, so the honest comparison is of OUTCOMES, which are identical, and there is no
measured figure on either side to compare. What the run did measure is memory: the
census's worst row grew the run's resident set by 2.01GB, the largest single claim in the
run, so this walk is not only over the CPU line.

WHAT WAS TRIED AND WHAT REMAINS, so the record does not have to be reconstructed:
closure acquisition was NOT the cause -- a probe carrying the census's exact imports
resolves in about two seconds and its witness reaches a verdict at cpu=0ms, so the
import-closure reduction that rescued this PR's eight excess-field probes does not apply
to this witness. The walk could not be measured off-gate at all: the probe host has 7 GiB
and post-resolve RSS is already 5.4 GiB there, so every whole-corpus run was OOM-killed
before verdict, and a frame that cannot finish the work does not produce a cost.
The floor's other remedy arm is a lane declaring its own ceiling, and none exists that
EXECUTES anything; moving the source into a non-executing home is the bare de-enrollment
the 2026-08-04 admission ruling forbids, deleting coverage while retaining the source.

SO THE DELETION RETURNS AS FINAL, ON MEASURED EVIDENCE. The coverage -- WHERE in the
corpus a LaunchEnvironmentConverge request is constructed -- is REMOVED. Not preserved,
not deferred, not covered elsewhere; what stands over that question is review of the two
named declarations and nothing executing.

skeleton_construction_spelling_count_where GOES WITH IT. Its only consumer was the census,
and a std projection with no consumer is dead weight that would also have owed a
corpus-scale agreement check against the list projection it sits beside -- two projections
of one edge-provenance fact are two authorities the moment they can disagree. Removing the
consumer removes the obligation rather than deferring it; the fact keeps one projection and
one authority.

NO SECTION 4b(3) ROW IS OWED. A declared drop presupposes a rung that executed evidence
established. These rows reached a verdict on no run of any head, in either shape, and the
module never reached main. There is no rung to drop.

RE-FILED AT node://adhoc-cfdf3366-bc3, and it cannot start until BOTH hold: a lane with a
dated ceiling that actually executes the exact enrolled witness identity and returns a
candidate-bound terminal verdict; and a substrate-readable per-field record-literal label
sufficient for a witness to assert the exact field-name set of a named construction site.
The first clause is deliberate: a lane that runs the row but reaches no verdict does not
satisfy it, which is exactly the state this census has been in throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aYqbapkp3MEjXYLDcXSc1

* The re-file trigger gains the memory term the experiment discovered, so it cannot be satisfied in form alone

THE TRIGGER AS WRITTEN WAS SATISFIABLE WHILE THE CAPABILITY STAYED DEAD, which is the
exact §4b(3) failure the rule about triggers naming capabilities exists to prevent. "A
lane with a dated ceiling that actually executes the exact enrolled witness identity and
returns a candidate-bound terminal verdict" can be met in FORM by standing up a lane with
a generous dated CPU ceiling -- and that lane would then die the way the off-gate probe
host died, leaving the trigger reading as fired while the witness still reached no verdict.

The reduction experiment measured a constraint the earlier trigger did not imply: this
census's worst row was the LARGEST single claim in its floor run by resident-set growth,
in gigabytes, while its CPU stayed UNMEASURED and above the per-claim ceiling with no
upper bound. Those are two different resources and only one of them was named. The clause
now requires a candidate lane to afford the row's measured resource cost rather than
merely a looser CPU number, and both facts are cited by naming the producing run rather
than by transcribing figures into an annotation no Accepted program can read.

This also makes the two halves of the evidence corroborate instead of merely coexist: the
gigabyte-scale growth is why every off-gate probe was OOM-killed on a 7 GiB host before
reaching a verdict, rather than two unrelated observations about the same witness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aYqbapkp3MEjXYLDcXSc1

* Two defects I reported as fixed and had not verified: the dead projection is really gone, and the roster gains a uniqueness wall

BOTH FOUND BY READERS, NOT BY THE GATE, AND ONE OF THEM CONTRADICTED MY OWN REPORT.

1. skeleton_construction_spelling_count_where AND ConstructionSpellingMatchCount WERE
STILL IN THE TREE. The previous commit's message says the projection goes with the census
and names that census as its only consumer; the source disagreed with it, which is worse
than either state alone. The cause was mechanical and worth recording: the addition had
been COMMITTED on the experiment head, so `git checkout -- <path>` restored HEAD's version
-- the one carrying it -- rather than reverting the addition, and I reported the intent
instead of reading the result. They are now actually deleted, leaving one projection of
the construction-spelling fact and no unconsumed second implementation.
This is exactly the residue the deletion existed to avoid: a dead projection introduced
for a refuted experiment, with no consumer and therefore no possible agreement check --
and no green can see it, because an unconsumed projection cannot make a disagreement
observable.

2. fleet_converge_persisted_member_paths CARRIED fleet_converge_plan_content_hash_path
TWICE. Review 58080 asked for one addition, the missing import; the edit that made it also
inserted the path into the roster below, where it was already present. A roster whose own
annotation calls it "a closed map rather than a lookup nobody enumerates", joined at
identity grain, cannot hold an identity twice and still be what it says it is.

WHY EVERY WITNESS STAYED GREEN OVER IT, which is the part that needed fixing rather than
the line. every_rostered_persisted_member_is_classified requires nonemptiness and that
every occurrence classifies, so a duplicate of an ALREADY-CLASSIFIED path satisfies it BY
CONSTRUCTION. The row passed for a reason unrelated to the property it names.

So the duplicate is removed AND the hole is closed: no_persisted_member_path_is_rostered_twice
asserts multiplicity PER IDENTITY over the real roster -- each member occurs exactly once --
with no length literal, because a count copied from the tree it checks collapses to
measure() == measure(). Its mutation control,
a_repeated_existing_identity_is_refused_by_the_uniqueness_wall, runs the same predicate over
the real roster with one existing identity repeated, which is precisely the defect committed
here, and requires it to refuse. Without that control the new green would be
indistinguishable from a check that cannot see a duplicate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aYqbapkp3MEjXYLDcXSc1

* State the uniqueness wall's rung honestly: mechanically enforced, and the word "wall" was inviting the higher reading

The new row asserts that every rostered persisted-member identity occurs exactly once,
and nothing about that makes a duplicate UNWRITABLE. The roster is a List<String>: a
repeated identity is still perfectly expressible, and safety depends on this row executing
and staying enrolled, which is DESIGN 4b rung 2 and not rung 3 or 4. Naming a raw list "a
closed map" does not elevate it -- a name is not a constructor -- and the annotation now
says so at the point where a reader would otherwise infer more from the word "wall".

The next-rung trigger is stated as the CAPABILITY rather than as an artifact: a
keyed-roster carrier whose construction admits at most one member per identity, so the
duplicate this row catches has nowhere to be written. It is deliberately NOT adopted here.
Generalizing on a single site trades a proven local wall for an admission problem, and the
bar for lifting the law into a shared carrier is at least two genuine closed-identity
populations; this repository has one today. Ordered sequences, bags, retry histories and
evidence transcripts may legitimately repeat a projected identity, so an all-distinct
helper applied by list-shaped resemblance would strengthen their semantics incorrectly.

Annotation only. No check, roster or predicate changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aYqbapkp3MEjXYLDcXSc1

* Delete the XL-0T cohort: its trigger had already fired when the previous merge imported it

THE PREVIOUS MERGE PRESERVED SEVENTEEN ROWS WHOSE DECLARED LIFETIME HAD ENDED. The
paragraph declaring them states their dissolve-on trigger as "#9907 merging". #9907 merged
to main at 14:02:39. The merge commit that imported them was made at 14:12:13 -- ten
minutes after the trigger fired -- and carried them across anyway. They are removed here,
by their own trigger, which is the only thing that retires a row on this ledger.

THE ROSTER IS LIFECYCLE-MANAGED, NOT APPEND-ONLY, and this file says so twice about earlier
cohorts: rows are "removed by their own dissolve-on trigger, exactly as the seven shrinks
above". Base and head both carry the XL-0T qualification now, so no run can produce those
deltas, all seventeen report stale, and a stale row refuses EVERY unrelated PR in the
repository. That is why deletion is owed on the roster's next touch rather than whenever
convenient, and this merge is that touch.

THE MISTAKE WAS ASKING THE TRIGGER QUESTION OF ONE SIDE ONLY. The check was run carefully
against the rows being KEPT -- this PR's two, whose trigger is this PR merging, which has
not fired -- and it was never run against the cohort being IMPORTED. Both resolution
recipes this file has been given were therefore wrong for this merge: union-then-shrink
because it shrinks only what the previous merge taught us to shrink, and plain-union
because "preserve both sides" is not a safe default for a ledger with dissolution rules.
The rule that survives, and it is now recorded in the file itself: the resolved roster is
the old cohort, UNION newly live main cohorts, MINUS every cohort whose trigger has fired
as of the base being merged -- asked of each side independently.

NOTHING ELSE MOVES. The roster is this PR's two RLM-2b rows and nothing else; the other
sixteen files in this PR are byte-identical to defb324f, verified per file by hashing each
against that head rather than by reading the diff. `cargo check --release -p v1-compiler
--lib` is clean on the resolved tree.

ONE THING DELIBERATELY NOT DONE: no explanation is offered for why the consumed-at-base
path did not refuse the previous head. I did not establish the mechanism and will not
invent one -- a stale baseline is a plausible cause and plausibility is not evidence. The
source-level lifecycle contradiction is sufficient to owe this deletion on its own, and if
the mechanism turns up it belongs in its own finding rather than folded into this fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aYqbapkp3MEjXYLDcXSc1

* The Spark consumer's second apply-shell spelling had no caller to migrate, so it is deleted and the terminal identity is asserted on the route production actually takes (review 5070355515 P1-4)

SUBSTITUTED REMEDY, NOT COMPLIANCE. The review asks for "an executing target proving
the real consumer resolves and constructs the artifact through the same terminal
authority", and its literal remedy is a caller for
spark_serving_fleet_converge_apply_shell. This does not add one.

That wrapper composed fleet_converge_apply_shell with fleet_converge_apply_terminal
over one request -- exactly what fleet_converge_plan_artifact_of_request already does,
computing the terminal once and handing that same value to the plan body line, to
apply_terminal, and to the apply script. A second spelling of one composition is a
second name for one fact (DESIGN section 3), and the two reconstructions could
disagree about the terminal with nothing joining them.

IT HAD NO CALLER TO MIGRATE. #9763 deleted the Spark-specific artifact route and with
it this wrapper's only caller; the symbol has occurred exactly once in the tree -- its
own declaration -- on every commit since, including origin/main today. Giving a dead
wrapper a caller would have minted the parallel authority rather than removing it. The
reviewer is free to reject this substitution; it should not be discovered.

THE BLOCKER'S DESCRIPTION IS ONE HEAD STALE, stated rather than repaired around. P1-4
says the module's FleetConvergePlanArtifact literal still omits apply_terminal. That
was accurate at the reviewed head c67b2d26e83; it is not accurate here, because the
main merge brought in #9763, which deleted that literal and its whole assembler. The
defect is real -- the orphan is what survives -- but its description no longer matches
the tree.

WHY THIS REPAIR BELONGS ON THIS BRANCH AND NOT IN A PREREQUISITE. The acceptance bar
requires the apply shell bound to the SAME request and terminal authority. That seam
exists only here: fleet_converge_plan_artifact_of_request is absent from origin/main.
A prerequisite would have asserted a true property about a route that does not carry
the seam under review.

WHAT IS ASSERTED, on the consumer's own route. The witness module already builds the
artifact through gunbc.fleet_converge_plan fleet_converge_plan_artifact with a
populated Spark axis (slice_artifact / spark_serving_axis_for_observation), which
routes through fleet_converge_plan_artifact_of_request. Two rows:

  the_spark_populated_artifact_names_its_own_terminal_in_body_and_script -- the plan
  body and the apply script both name fleet_converge_apply_terminal_line of the
  artifact's own apply_terminal. One terminal, two projections.

  a_terminal_no_observation_produced_is_named_by_neither_projection -- the control. A
  PartiallyApplied terminal nothing produced must be named by neither. Without it the
  positive passes on any artifact whose script contains some terminal line, which is
  precisely what a disagreeing second assembler emits.

WHAT "EXECUTING TARGET" MEANS HERE, and it is weaker than it sounds.
v2.workflow.required_floor required_gate_prefixes carries no spark, fleet or roadmap
prefix. These witnesses reach the required floor through the changed-witness sublane,
which seeds changed modules into the prepared closure. So they execute because the
modules are changed in this PR, and after merge they are outside the required gate
again -- the population of the declared rung drop gunbc.rung_drop
required_gate_bankruptcy, not a new defect. The same qualifier applies to the P0-1,
P0-2 and P0-3 evidence on this PR. No roster was widened to change that.

Ten imports the deleted wrapper alone kept alive are removed with it; each was
verified import-only before removal. No other path is touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014aYqbapkp3MEjXYLDcXSc1

---------

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>
gunbai-bot Bot added a commit that referenced this pull request Sep 2, 2026
…e to live (#9891)

* Give the SCM object store two kinds of object, so a file has somewhere to live

WHY: `gunbc scm add <file>` could not be built, and the blocker was the model rather
than the wiring. `ObjectStore` held `List<ObjectRecord>` -- a NodeKind plus labelled
children -- so a file's bytes had exactly one route in: intern them as an Atom's
Symbol. That makes the symbol table a content store and gives one name two materially
different meanings, which is DESIGN §3's meaning fork, and it is the same wrong model
I already rejected for the srv2 mirror.

WHAT: `ScmObject = SemanticNodeObject(ObjectRecord) | AuthoredSourceObject(AuthoredSourceRecord)`,
under ONE content identity family, because a store holding two identity kinds would
make `store_contains` ambiguous about what it contains. The authored identity is
derived from the bytes by `content_hash_atom` under a family tag, so a caller supplies
content and never an identity -- the same wall `store_node` and `insert_from_structure`
already stand behind, reached by a third producer rather than a second identity
authority.

THE EMPTY FILE HAS ITS OWN CONSTRUCTOR. `content_hash_atom` takes a NonEmptyStr, and
hashing "" through some other path would give the empty file an identity minted by a
different rule than every other file. `AuthoredSourceContent = AuthoredSourceEmpty |
AuthoredSourceText(NonEmptyStr)` makes the degenerate case unwritable rather than
admitted and checked.

REFUSALS ADDED, EACH BECAUSE A CONSUMER CAN NOW REACH A KIND IT CANNOT ACT ON. Every
site that needs a node asks `find_node_record`, whose three arms separate "nothing is
here" from "a file is here where a program was required" -- folding those into one
absent arm would send an operator looking for bytes that are right there.
  - checkout: `ObjectIsAuthoredSource`
  - merge: `TargetRootIsAuthoredSource`
  - insert: `objects_equal` answers false ACROSS arms, so a file landing on a node's
    locator is a LocatorCollision rather than silently satisfying a checkout.

THE CODEC IS A STRICT EXTENSION, NOT A FORMAT VERSION. A node entry keeps
`{kind, children}` byte for byte, so every document the predecessor accepted still
decodes identically; an authored entry is `{source}`. The two member sets are disjoint,
which is what makes discriminating on shape a partition rather than a guess -- and an
entry matching neither still refuses by member set. The decoder routes bytes through
`store_authored_source`, so an image cannot assert an identity for a file any more than
it can for a node.

EXECUTED, five new claims enrolled in test.claim.scm.scm_object_store_collision_witness
(bodies in the defining module, where sole_constructor lets the impostor fixture exist):
identity follows the bytes AND differs for different bytes; storing one file twice
stores it once; the empty file is an object and is not the same object as " "; a file
under a node's locator refuses; and the control that the same file stores cleanly under
its own derived identity. All five evaluate `true`, as does the migrated
both_insertion_orders_produce_the_same_canonical_record and the checkout witness's
lost-object arm.

Not in this commit: the `add` and `commit` verbs. They now have somewhere to write, and
they need the one modeled ScmWriteOutcome the plan names -- that is the next increment,
not a wiring afterthought.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wy8wRfzTK2kvFzj9nAEbyT

* Apply the authored-source tagging rule to both arms, and make the invariant executable

Review 58068's nit was right and is worth more than a reply. `AuthoredSourceEmpty`
hashed its tag directly through `content_hash_atom` while `AuthoredSourceText` went
through `content_hash_tagged_structural`, so the tagging invariant stated in the comment
directly above held for one arm and not the other -- an untagged digest sitting inside a
family whose whole claim is that its members are tagged. A stated invariant with one
exception is worse than no invariant, and the exception was the arm a reader is least
likely to check.

Both arms now apply the same combinator under different tags. The empty arm's payload is
its own tag, which is degenerate deliberately: what the payload must NOT be is some
file's content, because then an empty file and a file holding that content would collide
BY CONSTRUCTION rather than by accident. Distinct tags keep the arms apart; the shared
combinator is what makes "tagged" true of the family rather than of most of it.

The invariant is now executed rather than asserted in prose:
`no_authored_identity_is_an_untagged_digest` checks that neither arm's identity equals
the bare atom digest of its own tag. It is discriminating -- against the revision this
commit replaces it goes red, which is what separates it from a restatement of the
comment. Green, along with re-runs of the two identity claims the change could have
disturbed.

This changes the empty file's identity. Nothing writes authored-source objects yet -- the
format landed one commit ago and has no producer outside the witnesses -- so there is no
document population to migrate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wy8wRfzTK2kvFzj9nAEbyT

* Execute the authored-source checkout refusal instead of arguing it from the call graph

Side-chat review cautioned that my reachability answer might not hold: authored
objects decode, but a CHECKOUT might never be able to LAND on one, because every edge
the encoder emits was derived from a node record. That caution is right about the
encoder and wrong about the decoder, and the difference is worth executing rather than
reasoning about -- a call-graph argument is not evidence that two halves meet.

The decoder resolves {"target": {"at": "<n>"}} through the position map, and that map
now holds authored entries alongside node entries. So a document may name a file as a
node's CHILD -- an input no encoder would ever emit, and exactly the kind this
repository does not control.

The witness builds that document by hand: position 0 is the file, position 1 is a node
whose only child targets position 0, root is position 1. Children-before-parents is the
order the decoder requires, so the document is well-formed by the format's own rules --
it is not malformed, it is legitimate and says something the model must refuse to
reconstruct. The document LOADS (which is what makes this a checkout claim rather than
another decoder claim) and the checkout then refuses naming the authored source.

It is red against a checkout reporting MissingObject for a file that is present -- the
two have opposite remedies -- and red against one that reconstructs the node with the
child dropped.

Paired with the control that makes the claim about the FILE rather than the document
shape: the identical shape with a node at position 0 checks out. Without it, a decoder
refusing this shape for any reason would satisfy the claim.

One fixture bug found and fixed while writing this: the parent node initially targeted
position 1 (itself) rather than position 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wy8wRfzTK2kvFzj9nAEbyT

* Refuse for the right owner: split MemberFailed, and stop reporting a present file as missing

Three defects the side-chat review located, each now carrying an executed
discriminating claim plus a control:

- `root_edge_for` mapped EVERY `CheckoutRefused` to `TargetChildMissing`, so a
  merge role naming a present authored-source object was reported as absent.
  `MergeRefusal` gains `TargetChildIsAuthoredSource { role, identity }` and the
  arm is selected by matching the checkout cause.

- `decode_object_step` treated every `MemberFailed` as source-absent, so
  `{source: null}`, a duplicated `source`, and a non-object subject were all
  reported as a missing `kind` or an unexpected member. Only
  `MemberReadMissing` now selects the node arm; the other three refuse in their
  own right.

- Two comment blocks claimed the empty payload and the arm tags carry a
  property they do not. `content_hash_tagged_structural` already
  domain-separates the arms by outer tag, and separating preimages is not
  separating 64-bit digests. The prose now states the weaker true claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wy8wRfzTK2kvFzj9nAEbyT

* Put every annotation above a subject, and ask the store for a NODE where a node is required

Two things, both refusals the corpus already owns.

PARSE. The floor refused this branch at the parse phase, not at a claim: gunbc.scm.commit_closure_json_v2 carried a nine-line rationale INSIDE decode_object_step's body, and test.claim.scm.scm_merge_witness_test ended with an eight-line block naming no subject at all. DESIGN 4c models annotations at module-item grain only, so both are unparseable rather than untidy -- and because parse refuses whole, the refusal took every SCM claim in the lane down with it, which is why nothing else in this slice was measured. The first block moves above decode_object_step. The second's subject is merge_refusal_tag: it explains why that function's TargetChildIsAuthoredSource arm has no claim in this file and where the claim that reaches it does live, so it sits above the enumeration it is about.

THE ROOT ADMISSION. mint_repository_commit and gunbc.scm.log both asked store_contains, which is kind-agnostic by design -- it answers about the LOCATOR. So an authored-source object under a root locator minted a commit cleanly and then refused at checkout, and log reported it as `LoggedCommitRootHeld`: a row claiming a checkoutable root where checkout refuses. Both now ask find_node_record, whose three arms separate "nothing is here" from "a file is here"; the remedies differ (fetch the object vs. repair a wrong-kind reference), so they are separate arms rather than one Bool. LoggedCommitStanding grows LoggedCommitRootIsAuthoredSource and render says so in words.

The claim pair is executed, not argued: a store holding exactly ONE object -- a file -- so RootMissing is unreachable and a kind-agnostic check mints; the refusal must carry the file's own identity back. Its control is one fact apart, a stored node in the same shape, and mints. Both PASS under claim_batch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Brand a node reference so a file cannot inhabit a program position, and move the tag the schema outgrew

TWO REVIEWS, ONE CUT. The designated SCM re-review's central finding was that ObjectId was ONE untyped locator standing in every position that requires a program -- StoredEdge.target, a commit's root -- so an authored-source identity inhabited those positions perfectly well and the model's answer was to notice afterwards: find_node_record at a mint, at a log row, at a checkout. That is validation standing exactly where construction was available (DESIGN 5). codex/gpt-5.6-sol, independently, refused the same PR for continuing to write the v2 closure format tag over a schema that grew a `source` member. Both are fixed here.

THE REFERENCE TYPES. SemanticNodeObjectRef and AuthoredSourceObjectRef are sole_constructor over one ObjectId, so only the store mints one and no caller can brand a locator it merely holds; ScmObjectRef is the join for consumers that genuinely handle both. store_node hands back the node brand, store_authored_source the source brand -- two outcome types, because one shared outcome would force store_node's caller to match an arm store_node cannot produce. The admission rule is NOT duplicated: both route through insert_admission.

A CHILD EDGE HAS TWO HONEST STATES, and one brand cannot carry both. A partial closure deliberately references objects it does not carry, so a decoder holds edges whose kind nothing has established. Branding those anyway would need a mint that records a CLAIM rather than evidence, and the only place to put it is an exported function -- the importable forge factory this module already recorded once. So NodeTarget names the state: NodeTargetResolved(SemanticNodeObjectRef), or NodeTargetUnresolved(ObjectId). Neither arm reads as the other.

WHAT THE BRAND DOES NOT CLAIM: that the object is a node in THIS store. It does not carry which store, so checkout's two refusal arms remain as the honest boundary between stores. What it removes is the authoring mistake.

CONSEQUENCES THE REVIEW DERIVED, each now structural rather than checked: closure completeness resolves a child edge instead of asking the kind-agnostic store_contains, so a file no longer counts as a contained child; mint_repository_commit takes a NodeTarget and stores the reference THE STORE handed back, so a RepositoryCommit's root cannot name a file; the decoder's position map carries ScmObjectRef, so a contained edge target or root resolving to an authored entry refuses at decode, owned by the layer reading the document, instead of building the store and failing at checkout.

EVIDENCE MOVED WITH THE WALL rather than being deleted with it (DESIGN 4b(4)). The child-edge claim was a CHECKOUT claim; the same document now refuses at decode, so it asserts ObjectTableEdgeTargetIsAuthoredSource -- red against a decoder that resolves without discriminating, and equally red against one reporting the not-present cause for an entry that is right there. A root naming a file has its own claim and cause. merge's TargetChildIsAuthoredSource is still reachable, through an UNRESOLVED target, and its claim moves to the merge witness where insert_from_structure can build that store; its node control moves with it.

THE TAGS. This module's own policy is that a schema growing a member grows a new format tag, so the closure document is v3 and the repository envelope v2 -- the latter for two reasons, the embedded table's new entry shape and root resolution at decode. The identity rule is a separate axis and it moved too: v1 named one derivation, and an authored source's identity comes from a different one. ONE tag each, not two: no durable v2 document exists, the only producer is a witness rebuilt every run, and a compatibility arm over an empty population is machinery with no consumer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Compare records by what the identity is derived from, and count the durable documents before claiming there are none

Four repairs, each found by executing rather than by reading.

RECORDS_EQUAL WAS COMPARING THE TARGET'S ARM. It was `left.children == right.children` -- the substrate's structural equality -- which was exactly right while a StoredEdge held a bare locator, and stopped being right the moment the target became a NodeTarget. `NodeTargetResolved(r)` and `NodeTargetUnresolved(l)` over the SAME locator are different values structurally and the same object by content, because content_hash_of_children consumes only the locator. So the store reported "different record" for two records its own identity rule cannot tell apart, and insert_admission turned that into a LocatorCollision: a FABRICATED refusal, raised at a store being asked to do something ordinary. merge reaches it directly -- it reassembles a root from resolved children and re-inserts over a record whose children were unresolved.

It was caught by a CONTROL, not by review. The file arm of the merge role claim passed and its node control failed, which is the whole reason a claim is paired with one. The comparison is now over label and LOCATOR, through the same labeled_children_of_stored the identity fold consumes, so there is one definition of what makes two records the same rather than two that agree until one is edited.

THE DURABLE POPULATION WAS ASSUMED EMPTY AND IS NOT. The tag-bump note said no document exists to be compatible with, because the only producer is a witness rebuilt every run. True of the closure format; FALSE of the repository envelope, which has checked-in fixtures under dag/test/fixture/scm_repository_load carrying the v1 tags -- six claims went red on them. They are versioned test inputs, updated with the schema they exercise, and the note now says the population was counted rather than assumed.

AN ENCODE FIXTURE BUILT ITS SUBJECT THROUGH A ROUTE THAT IS NOW CLOSED. scm_env_absent_root_repository rewrote an encoded root to an uncontained digest and decoded it back; decode now resolves a commit root against the object table and refuses. The state is still reachable and the fixture now reaches it honestly: mint against a store that HOLDS the root, then rebuild the envelope around a store that does not -- which is exactly the residual a SemanticNodeObjectRef leaves open, since it says its object was a node in SOME store and never in THIS one. The document route became its own decode-refusal claim, with a control one fact apart.

A DUPLICATE IMPORT, named by review 58166 and real: ObjectTableEdgeTargetIsAuthoredSource was bound twice in one block, which I introduced by inserting it in two separate edits. Deleted. The review's other half -- that ingest may refuse on it -- is not the case: this module resolved cleanly throughout, and three duplicate-import blocks already stand on main from #9763, #9850 and #9669. The DESIGN section 2 objection is the one that holds.

Instrument: claim_batch over every dag/test/claim/scm/*_witness_test.dag with each file's own `test fn` roster. 299 pass, 0 fail, exit 0 -- including all eight claims this branch added or moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Name the identity rule for its subject, and freeze a predecessor document to prove the version refuses as a protocol gap

Two items from the designated SCM reviewer's bar that the tag bump left half-done.

THE RULE'S NAME SAID `node` WHILE ITS TABLE HELD NON-NODE IDENTITIES. `repository_node_identity_rule_tag` is the one authority in the document whose whole job is to say WHICH derivation reconstructed these identities, and it asserted something false about its own subject the moment the object table grew an authored-source arm -- a meaning fork in the worst possible place. It is now `repository_object_identity_rule_tag`, and its value names the rule rather than one branch's hash function: `scm-object-identity-v1`, a total law over the object coproduct whose semantic-node branch delegates to v2.std.node content_hash. A manifest branch joins this tag when it lands rather than minting a third.

THE OLD-WRITER EVIDENCE, RESHAPED BY THE VERSIONING RATHER THAN DROPPED. The re-review asked for a frozen predecessor document -- bytes not produced by the current encoder -- to prove old documents still read. Bumping the tag changed what that evidence has to show: a v2 document is now a version this build does not implement, so the required behaviour is a REFUSAL. Specifically the unsupported-protocol one and never malformed, because those have opposite remedies and malformed is the ONE standing that permits supersession (gunbc.scm.supersession standing_permission_refusal) -- a reader that filed a predecessor document as damaged could discard a generation it merely cannot read.

So three claims, not one: the cause is the format's own, the STANDING is the protocol gap, and the identical document under this build's tag loads. The document is a hand-authored literal precisely so nothing in the file can quietly re-derive it from the encoder under test, which is what made the existing round trips unable to answer this question.

Instrument: claim_batch over every dag/test/claim/scm/*_witness_test.dag with each file's own `test fn` roster. 302 pass, 0 fail, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* The module names the codec and the tag names the version: retire a suffix that had started to fork meaning

`gunbc.scm.commit_closure_json_v2` announced v2 in its name while emitting `gunbc-scm-commit-closure-v3` in its bytes. A reader has no way to decide which of the two is the contract, and that is the meaning fork DESIGN section 3 forbids sitting in the one authority whose whole job is to say which schema this build writes.

The suffix was not wrong when it was authored: it named the second CODEC GENERATION, distinguishing this module from the predecessor that encoded the root positionally. That predecessor is deleted, so the suffix had already stopped distinguishing anything, and bumping the format tag turned a harmless fossil into an active contradiction. The fix is the ordinary one -- one fact per authority: the module names the codec, the tag names the version, and the version lives in a value the encoder actually emits rather than in a spelling in a path.

Renamed with it: the module, its file, the witness module and file, and every prose citation across the corpus. A citation naming a symbol that no longer exists is the positional-citation failure by another route -- it decays silently and nothing refuses.

Found by the designated SCM reviewer while tracing whether retired version names still fork meaning after the tag bump. They did.

Instrument: claim_batch over every dag/test/claim/scm/*_witness_test.dag with each file's own `test fn` roster. 302 pass, 0 fail, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Restore the rung-drop row an auto-merge deleted, and revert a module rename whose admission wave every other lane would have paid for

Three CI findings, and the first is the one that matters.

A DECLARED SAFETY REGRESSION DISAPPEARED IN A MERGE, WITH NO CONFLICT. `direct_call_arg_seam_v2_exemption` landed on main today: a §4b(3) row declaring that one of the two direct-call argument judgments is switched off for every `v2.*` module. Taking main into this branch deleted it -- the declaration and its roster entry -- and git reported a clean auto-merge. That is exactly the class main filed this morning as `merge_region_excludes_shared_tail`, whose own text names `gunbc.rung_drop` as the second carrier with the vulnerable shape: multi-line rows sharing a `}` tail, so the three-way merge factors the tail out of the conflict region and the region's natural resolution drops what lived in it.

I resolved the file git DID conflict on and never looked at the one it resolved for me. The recognition rule that row states is about the shared suffix; the operational half I learned here is that the same carrier class also produces SILENT deletions on the files that never conflict at all. `gunbc.rung_drop` and `docs/design-ledgers.md` are restored to main's content, and the ledger is REGENERATED from the authorities rather than hand-restored, so what is committed is what the projection derives.

THE MODULE RENAME IS REVERTED, AND THE REASON IS AN EXTERNALITY. `commit_closure_json_v2` -> `commit_closure_json` was right on its merits: after the tag bump the module announced v2 in its name and v3 in its bytes. It is reverted because of what it costs everyone else. A rename re-targets every binding naming it, which the required namespace-wave-admission phase classifies `TargetChanged` -- not auto-admitted, one roster row each, 41 here. That roster's own header records the receipt: `stale_admissions` is computed PER RUN, so rows sitting on main are inherited by every open pull request, and a change touching no namespace can never match them; the phase then refuses every unrelated lane until they are dissolved. Spending every other lane's CI to fix one module's name is the externalized cost DESIGN §5 names, and doing it inside a pull request about the object model compounds it.

So the fork is REPORTED where a reader meets it: the module header now says which of the two versions is the contract (the emitted tag), that the suffix is a dead codec-generation fossil, what the rename costs, and a dissolve-on naming the shape that can pay it -- its own change, rows authored and deleted in one wave, at a moment the open-PR population can absorb. A comment does not make the name honest and this one does not claim to; it stops the two from looking equally authoritative.

AND ONE REAL DEFECT THE FLOOR CAUGHT THAT LOCAL RUNS DID NOT. `NewUnresolvedness binding gunbc.scm.log::CommitLogRow ObjectId` -- the row still named `ObjectId` after that import was dropped, resolving locally through bare-reference closure while the namespace wall measured it against the base and saw a name that had lost its declarer. The field is now `SemanticNodeObjectRef`, which is what it actually holds: `RepositoryCommit.root` is branded, so the log row carries the reference rather than a bare address.

Instrument: claim_batch over every dag/test/claim/scm/*_witness_test.dag with each file's own `test fn` roster. 302 pass, 0 fail, exit 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Execute the authored-source wire round trip in both arms

The branded-reference cut gave AuthoredSourceContent two arms and gave the
codec two encode branches, but nothing executed the claim that the arms
survive the wire and stay distinct. A codec branch with no discriminating
consumer is specification-without-execution (DESIGN.md §5).

Two claims now encode a closure holding an empty and a non-empty authored
source, decode it, and read the content back off the rebuilt store: one
asserts each arm round-trips, the other asserts the two do not collapse
into each other.

Proven discriminating rather than decorative by mutation: collapsing the
text branch of encode_authored_source_content to json_string(s: "") takes
both claims red (PASS 0 FAIL 2, exit 1); the codec was restored byte-clean
afterwards. Full SCM sweep: 304 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Make a node requirement a proposition, and let only the store answer it

The branded-reference recut left StoredEdge.target a two-armed sum, Resolved
beside Unresolved, on the reasoning that a partial closure must reference an
object it does not carry. The premise was right and the carrier was wrong, and
the designated SCM reviewer is what made the difference visible.

A child edge states a PROPOSITION -- a semantic node is required at locator L.
Whether a store satisfies it is an OBSERVATION, and observations do not belong
inside an object whose identity is derived from its content. The four facts
offered as evidence the resolved arm was harmless are the proof it never
belonged: the hash ignores the arm, records_equal must ignore it, the wire has
no spelling for it, and every consumer re-resolves it anyway.

Carrying it there was not merely redundant. store_contains_node_target answered
Bool, and "false" meant BOTH "nothing is here" and "a file is here" -- states
whose remedies are incompatible, since no fetch repairs an occupied locator.
Completeness put wrong-kind locators into the fetchable population, and the
encoder's position index was over OBJECTS, so it found the file and emitted the
CONTAINED spelling. An admitted partial closure produced bytes this same build
refuses at decode.

  - SemanticNodeTarget { locator } is canonical requirement content, plain and
    publicly constructible, because stating a requirement forges nothing. The
    sum is deleted, not deprecated: keeping both would leave a second authoring
    route that bypasses the census.
  - resolve_node_target projects it into find_node_record, which ALREADY carries
    the three arms. No peer NodeTargetResolution type is minted; one concept
    keeps one authority.
  - The Bool is deleted rather than kept beside the fix.
  - ClosureCensus separates absent from source_occupied in one traversal and
    keeps them apart all the way up. unresolved_identities projects absent only;
    closure_is_complete requires both empty; admission grew
    ClosureRequiresNodeAtAuthoredSource naming the whole population, so a caller
    repairing one does not discover the rest one round at a time.
  - The encoder now carries TWO indexes. Targets resolve against a node-only
    map, so a contained reference to a file has no entry to find. This is the
    part that makes the class unwritable rather than checked: .dag has no module
    privacy, so guarding the two intended entry points would leave the bad
    publication representable through the importable raw encoder.

Red first, and the claim FORBIDS the state rather than describing the
emit-then-refuse contradiction, which would have been green on the defect.
Three cells one fact apart: source-occupied FAILED while both controls passed;
all three pass now. Mutation: routing NodeRecordIsAuthoredSource back into
absent takes exactly one claim red.

The two authored-source claims now run through serialize_json and
parse_json_document, reading content only from the reparsed value. The reviewer
proposed a newline payload; mutating the newline escape left both GREEN,
because this parser tolerates a raw newline in a string -- the durable leg would
have looked rigorous and discriminated nothing. With a quote-bearing payload,
mutating the cp==34 escape takes both red. An unreadable parse answers Absent
rather than a manufactured ClosureDocRefusal, which would report a decoder
verdict for something the decoder never saw.

SCM suite 307 pass, 0 fail.

Still open on the reviewer's bar: records_equal compares labeled children in
supplied order while content_hash_of_children canonicalizes, so the permutation
collision is LIVE; parent-first/child-later graft ordering; repository-v1 names
under the v2 tag; a frozen v1 document refusing as unsupported protocol.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Canonicalize on the other route too, and stop fabricating a collision

object_store already records why the stored record must be the canonical one:
content_hash_of_children SORTS named edges, so keying on the canonical identity
while persisting the authored order stores an arbitrary first-observed
representative of an equivalence class. store_node does that. insert_from_
structure, the other public way to make a record, did not -- it derived the
locator from the canonicalized children and then persisted the children AS
SUPPLIED.

The consequence was a FABRICATED REFUSAL, which is why it survived: writing
[alpha, beta] and then [beta, alpha] derives ONE locator, so the second write
found the first record, records_equal compared labeled children in supplied
order, they differed, and insert_admission answered LocatorCollision. Nothing
had collided. The store refused a write it should have accepted, and every
existing claim stayed green because none of them wrote one object two ways.

Found by external review reading the identity rule against the persistence,
then executed here rather than argued:

  re_writing_the_same_bindings_in_another_order_is_idempotent   FAIL -> PASS
  swapping_which_target_each_label_names_is_a_different_object  PASS throughout

The control is the half that matters: without it the claim is satisfied by
collapsing genuinely different label-to-target associations into one object,
which would trade a fabricated refusal for a fabricated equality.

The canonicalization reuses std's canonicalize_labeled_for_content_hash rather
than sorting locally. The ordering rule belongs to the identity authority, and a
second sort in this module would be that rule written twice, free to drift from
the one the hash actually uses.

SCM suite 309 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Derive dependency order instead of trusting insertion order

store_records_in_dependency_order returned store.objects, and the note beside it
argued that insertion order IS dependency order because store_node recurses into
a target before appending its parent. True for store_node, false for the graft
route: copy_object appends one record without its children, so copying a parent
and then its child leaves the parent EARLIER than the object it references.

Completeness never noticed -- both objects are present. The encoder did. It
assigns positions in this order, so it reached the parent before the child had
one and spelled the target UNCONTAINED while the document carried that very
object a few entries later. That contradicts the declared wire meaning of the
arm and let a complete closure emit a partial spelling.

A post-order walk emits every referenced record before its referent, for any
insertion history. The store is acyclic by construction -- a locator is derived
from content that already includes its children's locators, so a cycle would
have to hash itself -- which is what makes the walk terminate without a separate
descent argument.

THE FIRST VERSION OF THE CLAIM WAS NOT DISCRIMINATING, and the comment says so
because the failure mode is instructive. Encoding, round-tripping through text
and requiring the reloaded closure to be complete PASSES ON THE DEFECT: decode
re-stores every object in the table, so the child arrives whichever spelling its
parent used. The defect is in the BYTES, and an assertion that reads only the
rebuilt store cannot see it.

The claim now compares IMAGES. The grafted-then-completed store and the
naturally-built store are the same closure, so they must serialize to the same
document; under the insertion-order encoder they did not.

  one_closure_has_one_image_however_its_store_was_assembled   FAIL -> PASS
  a_genuinely_partial_store_does_not_encode_as_the_complete_one  PASS throughout

The control is load-bearing: without it the claim is satisfied by an encoder that
never spells a target uncontained at all, which would delete the partial-closure
format this branch exists to add.

ON COST, since DESIGN section 6 refuses "n is small here": emitted_holds is a
linear scan, so the walk is quadratic in object count. That is the same shape
find_object already has -- the carrier is a List -- and is bounded by the same
dissolve-on already recorded for that carrier. A keyed collection retires both
at once; fixing it here alone would leave the store's own lookup quadratic while
adding a second index to maintain.

SCM suite 311 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Delete the two node-presence predicates the census replaced

Review 58294 found store_contains_node has no caller anywhere in the tree. That
is correct and it is mine: this PR added it, it is absent from main, and the
census split then removed the need for it without removing it.

The same deletion catches a second one the finding did not name.
store_holds_node_at's ONLY caller was store_contains_node, so with that gone it
is dead too. Deleting just the reported symbol would have left a dead function
whose sole consumer had been deleted in the same commit -- fixing the finding
rather than the fact.

Both are superseded by resolve_node_target, which answers the same question
three ways instead of two. That is the whole point of the census: a Bool at a
node position cannot separate "nothing is here" from "a file is here", so
nothing should be asking for one.

I do not accept the rule the finding cites. There is no corpus-wide prohibition
on Boolean projections over a coproduct in DESIGN.md -- the same claim was made
in review 58182 and answered there with a census of 2,523 such functions across
908 files. What makes these two deletable is that they are UNUSED, which is
DESIGN section 2: unnecessary work is redundant work, and dead API surface on a
sole_constructor carrier is surface a later caller can reach for.

SCM suite 311 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Name the repository codec for the tag it implements, and freeze a real v1 document

TWO ITEMS FROM THE SCM REVIEWER'S BAR, and they are the same seam from opposite
sides: what this build CALLS its repository format, and what it DOES with the
format it replaced.

repository_envelope_format_tag has read "gunbc-scm-repository-v2" since the
protocol cut, while the dispatch arm was RepositoryFormatV1 and the codec
functions were encode_repository_v1 and decode_repository_v1. That is the
meaning fork DESIGN section 3 names: one spelling over two materially different
contracts, with the wrong one standing where a reader looks first. Nothing about
the schema changes here -- the names were lying about which schema they were.

The frozen-predecessor claim needed a REAL document. The existing unknown-format
claim uses "gunbc-scm-repository-v99", a version that never existed: it proves
the dispatch refuses a tag it does not know, which is a different question from
whether this build handles the format it actually superseded. This branch moved
the tag from -v1 to -v2, so every repository written before it is a real artifact
a real caller can hand to this build today. The fixture is main's v1 document
verbatim, carrying both v1 axes -- the schema tag and the
fnv1a64-structural-canonical-v1 identity rule this branch also replaced.

THE ASSERTION IS THE STANDING, NOT THE REFUSAL. Refusing is easy and the wrong
refusal is worse than none: "these bytes are damaged" sends a reader hunting
corruption in a perfectly intact file, while "I do not speak this protocol" names
a version gap with a real remedy. A refusal-only assertion cannot separate them,
because both arms refuse. Mutation receipt -- mapping
RepositoryFormatUnrecognized to LoadDocumentMalformed:

  scm_rl_a_frozen_v1_document_is_a_protocol_gap_not_damage  FAIL
  scm_rl_a_damaged_document_is_not_a_protocol_gap           PASS (control held)

The control is what stops a classifier that answers UnsupportedProtocol for
everything from satisfying the claim.

SCM suite 313 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Read both census populations everywhere, not just the absent one

The SCM reviewer re-anchored at 837115a and found the previous fix had MOVED the
leak rather than closed it. They are right on all three counts.

Splitting the census into absent and source_occupied is only worth what its
consumers read. Three of them read the absent half alone:

  - encode_repository_checked guarded on an_uncontained_target and NOTHING else,
    so a requirement whose locator is occupied by an authored source walked
    through and got PUBLISHED. It now refuses first, with its own cause:
    RepositoryEncodeTargetIsAuthoredSource. The two refusals stay separate
    because their remedies are -- one says fetch an object that is not here, the
    other says something IS here and is the wrong kind, which no fetch repairs.
  - loaded_closure_standing answered LoadPartial for every not-complete closure.
    LoadPartial promises "fetch the named identities and this completes", which
    is FALSE for an occupied locator. LoadStanding grows
    LoadRequiresNodeAtAuthoredSource, and standing_loaded answers false for it:
    the document read fine, but what it asserts is unsatisfiable.
  - The raw encoder spells a source-occupied target uncontained, which is
    indistinguishable from absent on the wire. Both callers now refuse before
    reaching it.

THE THIRD FINDING IS THE ONE I MOST NEEDED. My byte-equality claim --
one_closure_has_one_image_however_its_store_was_assembled -- catches a
DISAGREEMENT between assembly routes but not a wrong spelling both routes share.
Proven, not conceded: under an encoder that spells EVERY target uncontained,

  one_closure_has_one_image_however_its_store_was_assembled   PASS
  a_complete_closure_spells_its_child_contained               FAIL
  a_partial_closure_spells_its_missing_child_uncontained      PASS

Agreement is consistency, not correctness. The new claims read the arm out of
the document through decode_target_reference -- the module's own reader, not a
second parser written to grade the first.

Item 3 also lacked its positional control. Canonicalizing named children is
satisfied by sorting every child list unconditionally, which is wrong under
Cardinality and Instantiation where PositionalEdges makes position the meaning.
The canonicalizer already dispatches correctly on kind_edge_discipline; nothing
executed it, so the rule was true and unwitnessed -- and an unwitnessed rule is
one a later local sort can quietly replace.

SCM suite 317 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Seal the raw closure encoder behind a carrier the bad state cannot inhabit

The SCM reviewer's exact-head ruling on 837115a listed five required closures.
54cd2e4 had already landed three of them (the repository source-occupied
refusal, LoadRequiresNodeAtAuthoredSource, and the contained-spelling witness
with the mutation receipt they asked for). These are the remaining two, plus the
part of the first that their reading was right about and mine was not.

encode_complete_closure_document asked unresolved_identities, which projects the
ABSENT population. For a requirement whose locator is occupied by an authored
source that population is EMPTY -- so the FRONT DOOR returned bytes for the
identical state partial admission refused at the side door. My previous claim
that "both entry points are closed" was true of admission and false of this one.

THE REPAIR IS A CARRIER, NOT A THIRD CHECK. Writing the judgment at each entry
point is what produced this bug twice: `.dag` has no module privacy, so
encode_closure_document stayed importable and the wrong-kind state stayed
representable through it no matter how many front doors were guarded. It now
takes a WellKindedClosure -- sole_constructor, mintable only through
admit_well_kinded_closure, which runs the census. There is no argument a caller
can build that carries the state, so the raw writer needs no claim of its own.

An ABSENT population is fine in that carrier and is the partial-closure case the
format exists for: it seals KIND, not completeness.
encode_admitted_partial_closure_document re-seals rather than re-judges, because
admit_partial_commit_closure already refused source_occupied before minting.

Also: unresolved_commit_root_step asked position_of, which is the GENERIC object
index, so a commit root pointing at a file found a position and passed. It asks
node_position_of now. Repository roots are established against node positions,
not object occupancy.

The seal is not decorative -- it broke three fixture call sites that had been
handing the raw encoder a store and a root, which is exactly the shape a
production caller could have used.

  the_complete_writer_refuses_a_wrong_kind_closure_and_names_it
  the_complete_writer_still_encodes_a_complete_closure          (control)

SCM suite 319 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Separate a wrong-kind commit root from an absent one, and execute the new standing

Four blockers from the SCM reviewer's exact-head ruling on 9bdd976.

1. COMMIT ROOT. Asking node_position_of was the right first question, but every
   node-index miss became RepositoryCommitRootNotInStore -- reporting ABSENCE for
   an object that is present and wrong-kind, the same conflation one layer later.
   commit_root_standing now answers three ways and the encoder grew
   RepositoryEncodeCommitRootIsAuthoredSource.

   HONESTY NOTE ON ITS EVIDENCE, because the arm ships without a discriminating
   RED and that must not pass silently. I could not author one. RepositoryCommit.root
   is a SemanticNodeObjectRef, sole_constructor, obtainable only by storing a node;
   reaching this arm needs a store where that same LOCATOR holds a file, which is a
   64-bit digest collision. No fixture can construct it without the forge factory
   this module refuses to export. Per DESIGN section 4b the arm is therefore
   mitigatable-with-unauthorable-RED, and its next-rung trigger is a fixture
   capability that can seed a store at a chosen locator. I am not claiming coverage
   for it.

2. THE NEW STANDING IS NOW EXECUTED. LoadRequiresNodeAtAuthoredSource had an
   implementation, exhaustive plumbing, and no evidence any external document
   reaches it rather than LoadPartial -- a typed carrier and a tag are not a
   consumer. The document is authored BY HAND because the encoders now refuse to
   produce it, which is exactly section 4b's case for authoring a RED as source
   handed to the decoder.

     scm_image_a_requirement_over_a_carried_file_is_not_partial
     scm_image_a_requirement_over_nothing_is_partial_and_loaded   (control)

   Mutation, the one the reviewer named: routing source_occupied back through the
   absent branch takes the first red and leaves the control green.

3. THE MANIFEST TRANSITION RULE was wrong. It said a manifest branch "joins this
   same tag; it does not mint a third one". The first half is right, the second
   does not follow: joining one authority is the section 3 point, but adding an arm
   changes the authority's TOTAL LAW, and this tag names the law. The tag is not
   pre-bumped for a branch that does not exist; the rule is corrected so the next
   author does not read it as permission to grow the law under a fixed name.

4. TWO SOURCE COMMENTS ASSERTED THINGS THAT ARE FALSE.

   object_store said "every node-requiring position takes the node brand; a file in
   a child edge is then not refused, it is UNWRITABLE." That was true of a design
   this module no longer has. It is the worst kind of stale comment -- one asserting
   a SAFETY property, which a reader plans against -- and an external review restated
   it back as the module's guarantee, which is how a false comment becomes a false
   shared premise. The honest statement is rung 3, not rung 4: the state is writable
   and every consumer refuses it by name. Three real defects were found in this seam
   AFTER the brands landed; had it been unwritable, none could have existed.

   commit_closure_json_v2 still called the schema addition "a strict extension, not
   a format version". The module's own policy refutes it -- a schema that grows a
   member grows a new format tag -- and this codec emits v3 and refuses the frozen
   predecessor outright.

SCM suite 321 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Make the commit-root fold exhaustive instead of rostering its wildcard

CI red on 103f2d5, reproduced locally rather than assumed flaky:

  nfr_roster_receipt: unrostered=1 stale=0 live=187
  unrostered: ["gunbc.scm.repository_envelope::unresolved_commit_root_step"]

The new fold carried `_ => found` over CommitRootStanding, a closed coproduct.
The roster exists to make exactly that visible, and the lens caught it on the
first commit that introduced one.

There are two ways to satisfy it and only one is right. Rostering the wildcard
would record a residue that does not need to exist; the arms are enumerable, so
the fold is written out. That also restores what the coproduct is for: a fourth
standing would now break this match rather than being silently absorbed by the
wildcard into "keep whatever we had" -- which, for a fold whose whole job is to
report the FIRST bad root, would have quietly kept reporting the wrong one.

  nfr_roster_receipt  1 passed
  SCM suite           321 pass, 0 fail

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Carry the standing to its destination, scope the image claim, sweep the stale text

Three of the four items from the SCM reviewer's ruling on 103f2d5. The fourth is
terminal exact-head CI, which follows this push. They accepted the unwitnessed
commit-root arm's disposition: keep it, export no forge, claim no coverage.

1. THE NEW STANDING NOW REACHES AN ACTUATOR. The hand-authored document proved
   LoadRequiresNodeAtAuthoredSource is produced; nothing proved it CHANGED
   anything. A standing no actuator consumes is a value with a name and no
   consequence, and the exhaustive arm in supersession was plumbing, not
   execution. The claim now drives the standing THAT DOCUMENT ACTUALLY PRODUCES
   -- not a literal handed to the actuator -- through observe_generation into
   supersede_head_slot with every other condition admitting, so the only thing
   that can refuse is the standing, and requires exactly
   StandingDoesNotPermit { standing: LoadRequiresNodeAtAuthoredSource }.

   The third one-fact cell was also missing: a SEMANTIC NODE carried at that
   locator must resolve the SAME uncontained spelling to LoadComplete. Without
   it the pair is about presence rather than kind, and satisfiable by a decoder
   that treats every uncontained target as unresolved -- which would make
   LoadComplete unreachable through that spelling.

2. THE IMAGE CLAIM IS RENAMED to a_completed_parent_first_graft_has_the_naturally_
   built_image. The old name claimed GLOBAL image invariance across every
   assembly history -- more than the fixture builds and more than the repository
   authority grants, since insertion history still decides tie order among
   INDEPENDENT components. A claim whose name promises more than its body
   executes is the same inflation as a rung reported above its evidence.

3. THE STALE AUTHORITY TEXT IS ONE SWEEP, not another round of single-paragraph
   repairs, because that is how the previous two got missed:
   - repository_envelope said emission order is INSERTION order. It is a derived
     post-order walk; what survives is the narrower true claim that insertion
     history decides TIE ORDER among independent components.
   - "THIS FABRICATES NO v2" sat above an encoder emitting v2. The number moved
     and the prose did not; it is the v3 statement now.
   - The repository encode header and its witness header both said "the three
     causes" / "one claim per cause". The coproduct is larger and one cause is
     deliberately unwitnessed, so both now say which one and why.
   - commit_closure_json_v2 credited the index split alone with closing the raw
     route. It never did -- the carrier does. Both facts are needed and the note
     now says which does what.
   - "A missing position is UNREACHABLE" was true only while every closure was
     complete. For an ADMITTED PARTIAL closure a missing node position IS the
     uncontained arm, and reading it as unreachable would invite an author to
     delete the format's reason for existing.
   - supersession's producer census said loaded_closure_standing answers "only
     LoadComplete or LoadPartial", and that repository_envelope classifies
     NOTHING into LoadStanding. Both premises are obsolete; a stale count is
     exactly what corrupts a census. The conclusion is unchanged and broader.

SCM suite 323 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* State the populations that exist, not the ones these notes were written against

The reviewer's hold on 689a2b5 was text-only but authority-bearing: three
source notes described populations their own subjects had outgrown, and one
asserted a rule in the present tense directly above its retraction.

The supersession census is recomputed from the live tree rather than patched.
It said "PRODUCERS, TOTAL: two" beside "the producer count grew"; both cannot
hold at the declared denominator. There are five, across three modules.

Recounting surfaced a second, load-bearing falsehood the review did not name.
The census also claimed closure_document_load_standing was the ONLY declaration
able to return LoadDocumentMalformed, and concluded that the producer count grew
while the permitting surface did not. LoadDocumentMalformed is the sole
permitting standing, and repository_envelope_load_standing returns it for its
member-read and target-reference modes while object_table_load_standing returns
it for six of its seven causes. The permitting surface grew with the producer
count. Recording the growth of one while denying the other is exactly the stale
count this census exists to catch, so the conclusion is replaced rather than the
number. What survives is stated narrowly: the success path stays outside the
relation by construction, because none of loaded_closure_standing's three arms
permits.

RepositoryEncodeRefusal's note described "the single producible cause" and "the
two sibling arms" above an eight-arm coproduct, and the checked encoder's note
named three ways a repository can be unrepresentable above the same eight. Both
now enumerate what exists, grouped by remedy, keeping the domain rule that was
each paragraph's actual content.

The witness header opened by asserting one claim per cause and retracted it two
lines later. The heading now carries the exception it always had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Exclude witness helpers from the census denominator, by name

The recount stated its denominator as "declarations whose return type is
LoadStanding". That is too wide: witness modules declare their own, and
test.claim.scm_commit_closure_json_v2_witness scm_image_standing_of is one. A
fixture helper is not a producer this destination can be reached from, so the
denominator is now production classifiers under gunbc.scm.*, and the witness
helper is excluded by name rather than by being overlooked.

The withdrawn claim -- that the producer count grew while the permitting surface
did not -- is removed rather than qualified. The account states positively what
holds: LoadDocumentMalformed is the one permitting standing, three of the five
producers can answer with it, and no declaration or module owns it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Regenerate the Rust source-type bindings from their authority

main carries an updated `proof` row in gunbc.rust_source_type_bindings whose
committed projection was never regenerated, so origin/main is itself in a
drifted state and every branch that merges it inherits a failing regen phase.
Measured here rather than assumed: both the .dag authority and the .rs
projection on this branch are byte-identical to origin/main, and regen still
reports first_generation_equal=false for this one file.

The fix is the projection, not the authority: the generated file now carries
what the row says. The build lane is green at this head -- regen
first_generation_equal=true, generated-artifact 35/35 matched, 0 drifted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

* Two claims quantified over a population they did not check

The census repair left its neighbours stating the population as it stood when
each sentence was written. Two of those neighbours are claims, and that is the
part that matters: their names quantify universally while their bodies
enumerated a subset, so the standing this branch introduces could have collapsed
into the permitting arm with both green.

every_non_permitting_standing_refuses_and_says_which checked four of the five
non-permitting standings. malformed_is_the_only_standing_that_permits_replacement
checked two of the three it owns (LoadComplete and LoadPartial belong to
a_partial_load_is_never_supersedable, so the five are covered between the pair).
Both now check the whole population.

Executed rather than argued: routing LoadRequiresNodeAtAuthoredSource to the
permitting arm takes BOTH claims red, and both are green with the arm restored.
Before this change that mutation was invisible to both.

The prose repairs are the same defect without the teeth. load_standing said
"four refusals ... for four different reasons" and listed four of five, so the
new standing had no stated reason; it has one now, and it is its own -- the
document decoded and what it asserts is unsatisfiable, so no fetch repairs it
and superseding would destroy a generation over a defect not in its bytes.
object_table_json called collision "the five"; it is the seventh cause against
the other six. The census said "one producer today" after counting five, and
"a sixth standing" above a six-arm coproduct.

The census partition is restated at the grain the review asked for: three
declarations DECIDE LoadDocumentMalformed from a refusal cause, one RELAYS it
without deciding, and one cannot return it -- a fourth return route is not a
fourth decision authority. The surviving consequence is stated at arm grain
(the ClosureDocLoaded branch) rather than as "no load that succeeded", because
this module deliberately separates a document that decoded from one whose
standing answers standing_loaded true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ4iThiZ1B9LPB9ePr8qa9

---------

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <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.

0 participants