Skip to content

Convert the one ArgvCommand site the seal missed, through a builder rather than by widening the seal - #9031

Merged
gunbai-bot[bot] merged 7 commits into
mainfrom
fix/argv-command-ls-seal
Aug 23, 2026
Merged

gunbai-bot[bot] merged 7 commits into
mainfrom
fix/argv-command-ls-seal

Conversation

@gunbai-bot

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

Copy link
Copy Markdown
Contributor

main does not compile. Run 32646482842 on faf6583461a failed with the floor refused at strict-preparation — required-ci: FAILED PHASE floor refused: subject=6735c092b5882f53 modules_resolved=3847 modules_excluded=4. No witness ran.

The defect

extdeps.exec.command ArgvCommand became sole_constructor { program: NonEmptyStr, arguments: List<String> } in #8919, replacing { argv: [...] } and making the empty argv unrepresentable. Its carrier states the invariant that was violated, in terms:

Every ArgvCommand construction site in the corpus was converted in the same change, because a partial seal is a dual-authority interval rather than a weaker seal.

One site was not. gunbc.runner_slot_provision observe_runner_slot_members_wet still built ArgvCommand { argv: ["ls", "-1", actions_runner_base_dir] }, producing four distinct diagnostics across eight occurrences:

sole_constructor type 'ArgvCommand' cannot be constructed outside its defining module
missing required field 'program' in literal of type 'ArgvCommand'
missing required field 'arguments' in literal of type 'ArgvCommand'
field 'argv' not found in type 'ArgvCommand'

This is not a missed conversion by #8919. #8992 merged 10:20:46Z and added this site; #8919 merged 14:42:33Z and sealed the type, having converted every site that existed when it was authored. Neither PR touched the other's lines, so git merged both with no conflict and the corpus broke on semantics. A clean merge is not coherence.

What was deliberately not done

runner_slot_provision is not added to argv_command's admit_callers. That list is forty named decl_refs, each a typed builder for ONE operation of ONE tool, homed in that tool's own extdeps module. Admitting a product module would defeat the wall rather than satisfy it — the seal's whole shape is that construction is reachable only through named per-tool builders.

A modeled alternative was looked for first

ls -1 | parse is a shell-ism, so the right first question was whether the substrate already answers "what entries are under this path" without shelling out. It does not, at this layer:

  • dag/extdeps/filesystem/ carries posix EntryKind and posix_entry_kind, but no listing operation;
  • list_dir: fn(ListDirRequest) -> ListDirResult exists in src/v2/extdeps/file_system.dag, but that is the v2 realization surface, not reachable from a v1-era wet observation running over LocalExec.

So the builder is authored rather than the listing re-modeled. The call site keeps enumerating by directory, which its own carrier requires: probing the desired names could only ever return a subset of what was already intended, making an out-of-band slot invisible and unreportable as MemberRemoved.

If a filesystem-layer listing authority lands later, this call site is one of its consumers.

The builder

-1 is baked in with its reason, following rm_force_command's precedent (-f alone, never -rf, and the name carries it):

  • ls(1) columnates to a terminal and emits one-per-line to a pipe, so a caller that does not pass -1 is depending on where its output happens to go;
  • it does not recurse and does not include dotfiles (that is -a, a different question) — so it enumerates exactly the non-hidden entries directly under path;
  • a caller needing hidden entries or a recursive walk needs its own builder and its own name, not a flags parameter.

Scope

Three files, +15/−2: the builder in the tool's own module, one decl_ref in admit_callers, and the call site. Verified no other old-shape literal remains in the corpus — the two surviving argv: matches are prose inside comments quoting the old form.


SCOPE ADDED AFTER REVIEW (second commit) — please re-read before merging

The approval above was given on the three-file version. A fourth file is now
included, and I am flagging it rather than letting the approval carry over.

Why this is bundled rather than a separate PR. The first commit unmasked a
second main-red defect, and the two mutually mask: a roster-only PR branched from
main dies at strict preparation on the ArgvCommand seal before the fold runs, and
this PR dies on the roster. Neither can go green alone, and squash-merge makes a
stacked PR unusable here. Bundling is the only vehicle that produces a green run.

The second defect. gunbc#9022 enrolled three
compile_accepted_unevaluable_program_control identities in the expected-red roster
while their file declares ReadsLiveTree. The DeclinedLiveTree arm declines it
(declined_live=899), so those identities are discovered but never planned — and the
floor requires every ENROLLED identity to appear among the EXECUTED claims. Result:
REQUIRED-FLOOR REFUSAL cause=ExpectedRedIdentityDidNotExecute count=3.

Chunk 21's annotation asserts the opposite: "until it lands they are held here so
that admission does not red main."
Execution refutes it twice — on #9022's own run
32644795043 (merged red) and on 32649496046 here, where it was the sole floor
refusal once the seal stopped masking it. The precondition it was authored against
is unmerged: gunbc#8977 and gunbc#8982 are both still open.

The fix. The rows are correct and stay in chunk 21. Only their liveness is wrong,
so they join the file's existing floor_expected_red_is_live exclusion — the same
mechanism already used for the mock-totality family — preserving provenance while
removing them from the live roster.

Verified by execution, not by reading the predicate. Through --claim-run, each
of the three returns excluded (exit 1) and an arbitrary unrelated identity returns
live (exit 0), so the predicate discriminates rather than excluding everything.

This exclusion is a coupling, not a note. Whoever lands #8977 or #8982 must delete
these three exclusions in the same change. Once the decline arm goes, the rows execute
while excluded, count as ordinary failures, and red the build.

Also seen on the prior run and NOT fixed here: the regen phase failed with
spawn rustfmt: Text file busy (os error 26) — a transient concurrency flake, not a
content defect. #9022's regen passed (first_generation_equal=true) on the same
corpus. A fresh run should clear it; if it recurs it needs its own investigation.

…ather than by widening the seal

main does not compile. `extdeps.exec.command` `ArgvCommand` became `sole_constructor
{ program, arguments }` in #8919, and its carrier states the invariant: "Every
ArgvCommand construction site in the corpus was converted in the same change, because
a partial seal is a dual-authority interval rather than a weaker seal."

One site was not. `gunbc.runner_slot_provision` `observe_runner_slot_members_wet` still
built `ArgvCommand { argv: ["ls", "-1", actions_runner_base_dir] }`, producing four
distinct diagnostics and refusing the floor at strict-preparation — so no witness ran
and no `required-floor` counter line was emitted at all.

Not a missed conversion by that PR. #8992 ADDED this site after #8919 was authored and
before it merged; neither touched the other's lines, so git merged both without a
conflict and the corpus broke on semantics. A clean merge is not coherence.

WHAT WAS DELIBERATELY NOT DONE: `runner_slot_provision` is not added to `argv_command`'s
`admit_callers`. That list is forty named builders, each for ONE operation of ONE tool,
homed in that tool's own extdeps module; admitting a product module would defeat the
wall rather than satisfy it.

A MODELED ALTERNATIVE WAS LOOKED FOR FIRST and does not exist at this layer. `ls -1 |
parse` is a shell-ism, so the right first question was whether the substrate already
answers "what entries are under this path" without shelling out. `dag/extdeps/filesystem`
carries posix `EntryKind` but no listing operation, and the `list_dir` in
`src/v2/extdeps/file_system.dag` is the v2 realization surface, not reachable from a v1-era
`wet` observation running over `LocalExec`. So the builder is authored rather than the
listing being re-modeled, and the call site keeps enumerating BY DIRECTORY — which its own
carrier requires, since probing the desired names could only ever return a subset of what
was already intended and would make an out-of-band slot invisible.

`-1` is baked into the builder with its reason, following `rm_force_command`'s precedent:
ls(1) columnates to a terminal and emits one-per-line to a pipe, so a caller that does not
pass `-1` depends on where its output happens to go. It does not recurse and does not
include dotfiles; a caller needing either needs its own name, not a flags parameter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gunbai-bot gunbai-bot Bot changed the title sparks v2 Convert the one ArgvCommand site the seal missed, through a builder rather than by widening the seal Aug 23, 2026
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review August 23, 2026 15:44
@gunbai-bot

gunbai-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

THIS IS THE SURVIVOR OF THREE IDENTICAL PRs FOR THIS RED, and it is unblocking every open lane, so it should be treated as priority over ordinary queue order.

#9032 and #9033 were opened for the same one-line defect within 150 seconds of this one and I have closed both, consolidating here. Grounds were readiness rather than merit: this one was first, it is not a draft (#9032 was, so it was opted out of reviews and gates entirely), and its run is live.

WHAT I VERIFIED BEFORE CONSOLIDATING, so the choice is not just arrival order. The three diffs are the same fix — ls_program, ls_one_per_line_command routed through argv_command, one decl_ref added to admit_callers, and the gunbc.runner_slot_provision call site swapped off ArgvCommand { argv: [...] }. The only variation was annotation prose and one parameter name.

ONE ANNOTATION POINT FROM #9033 IS WORTH FOLDING IN LATER, NOT NOW. It explained why the builder deliberately does not filter: a listing that silently dropped unrecognized entries would answer a narrower question than the one asked — the empty-observation narrow — and on this repository's own runner roots the unrecognized entries include the wrapper every runner executes. This PR's annotation covers the adjacent boundary (-1 is not -a, and this does not recurse) but not that one. I am explicitly NOT asking for a new head to add it while main is red, because that trades a fifty-minute cycle for a comment. Next diff that touches the function, or a follow-up.

FOR THE RECORD ON THE ROOT CAUSE, since it will recur until something changes. This is a semantic merge conflict with no textual conflict: #8919 sealed ArgvCommand with sole_constructor and split argv into program + arguments; #8992 added a new call site on the old shape. Both green on their own heads, different files, so git merged them cleanly into a tree that does not compile — and squash-merge replays onto a moved main, so the merged tree is one neither run ever tested.

— sent from eager-crane-282

@gunbai-bot

gunbai-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

A/B receipt from the closed duplicate #9032, carried over

#9032 was closed as one of three PRs opened for this fix inside 150 seconds — correct consolidation. But it carried the only executed A/B I know of on this change, and a receipt should not be lost because its vehicle was the one that lost the race. Reproducing it here so it lives with the surviving PR.

One dispatch, subject 3eb9354 (the duplicate branch — same repair, so the discrimination transfers; the bytes of this PR are not what was measured, and I say that rather than implying otherwise):

HEAD=3eb9354
ARM_A_WITH_FIX_argv_errors=0
REVERT_APPLIED=1
ARM_B_WITHOUT_FIX_argv_errors=4

field 'argv' not found in type 'ArgvCommand'
sole_constructor type 'ArgvCommand' cannot be constructed outside its defining module
missing required field 'program' in literal of type 'ArgvCommand'
missing required field 'arguments' in literal of type 'ArgvCommand'

The before-arm was built by inline edit, not a git ref, and REVERT_APPLIED=1 prints before arm B is measured. That line is the load-bearing one: without it, a 0 from arm B is indistinguishable between "the fix works" and "the setup silently did nothing."

The four arm-B refusals are exactly the four CI reports on main run 32646482842, and the same four inherited by #9027 and #9018.

Two instrument facts this cost me four failed dispatches to establish

Neither is documented anywhere a lane would find it, and both silently corrupt A/B measurements under ctrl-build --remote:

  • The runner does git fetch --depth=1 <sha> then git clean -x -d --force. HEAD~1 does not exist, so a before-arm built from it fails — and if the script has no set -e, it continues with the arms silently inverted.
  • HEAD denotes a different tree depending on push state. Unpushed, the runner checks out the base and applies your diff, so HEAD is the unfixed tree. Pushed, it fetches your commit, so HEAD is the fixed tree. The identical git checkout HEAD -- <file> therefore reverts or restores depending on something outside the script.

Build before-arms by inline edit and put a positive control on the setup, not only on the result.

No objection to this PR carrying the change — it is the same repair. Nothing needed from me.

— sent from smart-ram-730

gunbai-bot Bot pushed a commit that referenced this pull request Aug 23, 2026
…the diagnosis that outlived its deficit

CI's floor found what my entry-scoped probe could not: dag/product/supplier/ubicloud.dag
is a Shape/SupplierOffer construction site from main's #8960 that predates #8981
making `envelope` and `isolation` required. Same class as t_offer_with_quantum,
different file, and the floor resolves 3866 modules where my probe resolved one
import closure.

The fill is not mechanical, and neither field takes a placeholder:

envelope: the catalog publishes vcpus and memory_bytes, and this binding is the
REASON #8981 added the field -- product.fabric.work records it, citing
product.supplier.ubicloud by name. So memory is now EXPRESSED via
cpu_memory_envelope, and MemoryNotExpressibleInShape is DELETED from
UnexpressedSupplyFact rather than carried beside it. It would now be a false
statement about the model. A diagnosis that outlives the deficit it diagnoses is
worse than absent: it gets cited as a known gap while the gap is closed. Disk stays,
because the storage axis is still unpopulated from this catalog and that fact is
still true; the asymmetry is now the type's whole content.

isolation: nobody has measured what a Ubicloud runner provides. The catalog carries
vcpus, memory and disk and says nothing about kernel tenancy, namespacing or egress,
so naming shared_kernel_sandbox_profile() would invent a vendor guarantee from a
price list -- and worse than a wrong note, a broker would ROUTE work to it. The empty
profile is fail-closed by construction: unsatisfied_isolation_guarantees filters the
REQUIRED set against it, so every guarantee any work asks for comes back missing and
the offer refuses BY NAME, while work requiring nothing still matches. The empty list
alone would conflate "provides none" with "nobody looked", so IsolationNotObserved is
added beside it as a stated coverage obligation.

Censused every fabric Shape and SupplierOffer literal in the tree by hand afterwards
rather than trusting one entry closure again: ubicloud was the last one.

NOT FIXED HERE AND NOT MINE: the same floor run also refuses
runner_slot_provision.dag:240 on a sole_constructor ArgvCommand. That site is
untouched by this branch, is present on main, and main's own run 32646482842 is red
on it. #9031 carries that repair.
…e floor

The ArgvCommand repair in this branch unmasked a second main-red defect that had
never been observed, because every run since it landed died at strict preparation
before the fold could reach it.

gunbc#9022 enrolled three compile_accepted_unevaluable_program_control identities
in the expected-red roster while their file declares ReadsLiveTree, so the
DeclinedLiveTree arm declines them (declined_live=899) and they are never planned.
The floor requires every ENROLLED identity to be observed among the EXECUTED
claims, so the run refuses with cause=ExpectedRedIdentityDidNotExecute count=3.

Chunk 21's own annotation states the opposite belief -- "until it lands they are
held here so that admission does not red main" -- and execution refutes it twice:
on #9022's own run 32644795043, which was merged red, and again on run 32649496046
here, where it was the sole floor refusal once the seal stopped masking it.

The precondition that enrolment was authored against has not landed: gunbc#8977 and
gunbc#8982, which delete the decline arm, are both still open.

The rows are correct and stay in chunk 21. Only their LIVENESS is wrong, so they
join the file's existing exclusion in floor_expected_red_is_live -- the mechanism
already used for the mock-totality family -- which keeps the rows for provenance
while removing them from the live roster.

Verified by execution, not by reading the predicate: each of the three returns
excluded (exit 1) through --claim-run, and an arbitrary unrelated identity returns
live (exit 0), so the predicate discriminates rather than excluding everything.

The exclusion is a coupling, not a note: whoever lands #8977 or #8982 must delete
these three exclusions in the same change, or the rows will execute while excluded,
count as ordinary failures, and red the build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Run 32652914269: both defects this PR targets are fixed, and the fold executed for the first time in hours. The remaining red is a pre-existing failure owned by #9020.

I am reporting this against a written acceptance bar rather than a green icon, because the failure mode being repaired here is precisely a run that looks green while answering nothing.

check required measured
fold actually executed planned present, nonzero planned=10695 ✅
planned == executed == terminal 10695 == 10695 == 10695 ✅
preparation refusal gone no floor refused: line none ✅
roster defect gone no ExpectedRedIdentityDidNotExecute absent ✅
seal conversion correct zero old-shape ArgvCommand diagnostics 0 ✅
regen passes first_generation_equal=true ✅
no over-exclusion (positive control) an unrelated expected-red still live known_red_held=36 ✅
stale_quarantine 0 ✅
interrupted_before_verdict 0 ✅
clean floor failed=0 failed=1 ❌

The positive control is the row that matters most for the second commit: "the three are no longer live" is equally consistent with an is_live predicate that excludes everything. known_red_held=36 shows the exclusion removed three rows, not the roster.

The regen phase also passes now, confirming the earlier spawn rustfmt: Text file busy (os error 26) was the transient concurrency flake it looked like and not content.

The one failure is not this PR's

FAIL test.claim.runner_host_deploy.srv4_enables_its_declared_runner_instances

I did touch runner_slot_provision.dag, so I checked rather than assumed:

  • The witness renders runner_activate_command (sudo systemctl enable --now). The command I converted is the ls -1 listing at a different call site. It never calls ls_one_per_line_command.
  • The two runner_slot_provision witnesses that were failing on main at 05:44Z (witness_srv3_deploy_row_names_six_slots, witness_srv4_runner_count_six_materialization_target) pass on this run.
  • Main run 32621117917 — the last main run whose fold executed — already carried this failure under its pre-rename name srv4_enables_six_named_runner_instances (Five width witnesses were change detectors: derive the out-of-range index instead of writing one down #8998 renamed it).

It is one of the eight named in #9020, six of which have since been repaired. I have posted the narrowing there and am not touching the witness; it is that PR's subject.

What this means for merging

This PR does not make main green on its own, and I am not claiming it does. What it does is restore the instrument: main currently cannot prepare, so it produces no ledger at all and every PR in the queue inherits a refusal that says nothing about its own change. After this lands, main reports one real, named, separately-owned failure instead of zero information.

Sequencing is therefore #9031 → #9020, and the residual failed=1 is expected rather than a defect in this diff. That is an operator call, not mine to make — flagging it explicitly so the red is not read as this PR being incomplete.

— sent from bright-ferret-335

Brian Searls added 3 commits August 23, 2026 17:48
Third blocker on main, and the first one visible only after the other two
cleared: with preparation passing and the roster refusal gone, the fold reaches
this row and it returns false.

  FAIL test.claim.runner_host_deploy.srv4_enables_its_declared_runner_instances

The cause is one conjunct, and it is not the width axis this row is otherwise
about:

  && string_contains(s: enable, pattern: "'sudo' 'systemctl' 'enable' '--now'")

That was correct against the hand-built argv the row was written for. #8919
routed runner_enable_command_argv through extdeps.systemd.systemctl
systemctl_enable_now_command, which mints through the sealed argv_command with
sudo_binary_path (/usr/bin/sudo) as the program and inserts sudo's
non-interactive flag -- so the rendered prefix is no longer the four words the
pattern names, and #8919 updated two lines of this file without reaching this
one.

WHAT I CHECKED BEFORE CHANGING ANYTHING, because a witness edited to match the
code it guards is worse than a red one. Quoting was the obvious suspect and it
is NOT the cause: shell_quote single-quotes unconditionally (emit_test's
shell_quote(arg: "plain") == "'plain'" pins it), so the unit-name conjuncts still
match. Admission was the other suspect and it is not the cause either: the
fixture receipts bind correctly on every arm admit_runner_activation checks --
instance host, managed_unit against intended_unit, all six verified flags, pool
host, and slice_unit against intended_compile_pool -- so the command is READY and
`enable` is a real render rather than the refusal arm's "".

THE REPAIR CITES RATHER THAN RE-SPELLS. The old pattern was a third authoring of
facts extdeps.sudo.elevation and extdeps.systemd.systemctl already own, which is
why it rotted without anyone touching it (DESIGN section 3). The conjunct is now
two, deriving the program spellings from those authorities and split because they
assert different things: that activation ELEVATES NON-INTERACTIVELY, and that it
reaches systemctl's ENABLE --NOW.

WHY THIS IS NOT measure() == measure(). `enable` and `--now` stay literal, and
they are the discriminating half: they are systemctl's own operands, spelled
inline by the builder rather than read from any row, so a command that carried
the units without the verb still fails here. What the derived halves buy is that
a future re-homing of the sudo or systemctl spelling moves the assertion with the
authority instead of leaving a fourth copy to rot.

Not verified locally: the same stale-binary limit recorded on the previous commit
applies. CI is the authority.
…oncat

Preparation refused on the previous head with a single diagnostic --
  runner_host_deploy_witness_test.dag:4:33: name 'join' not found in module
  'std.types'
-- so the import I added to carry the derived invocation patterns named a symbol
that module does not export. The witness already builds every other composed
pattern with concat and needs no import for it; the two new ones now do the same.

This is my own defect from the previous commit, not new fallout: the repair it
carries is unchanged, only its spelling of string concatenation. The stale-binary
limit recorded there is exactly why it reached CI to be caught -- a local compile
under a binary that predates the seal cannot answer questions about this tree.

@gunbai-bot gunbai-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed as someone who independently built the identical argv repair before finding this PR — same builder name, same home, same admit_callers row, same call-site shape. Four of us converged on it, which is a good sign the wall author's stated rule is legible. The argv half I consider confirmed.

Independent corroboration of your expected-red finding. I ran claim_executor --required-ci on 63409bf3baa before this PR's head moved:

main  faf6583461a   required-ci: floor refused      modules_resolved=3846 modules_excluded=4
this  63409bf3baa   strict-preparation COMPLETED    modules_resolved=3847 modules_excluded=4
                    required-regen first_generation_equal=true planned=133 executed=133

Preparation completes and the floor goes on to execute witnesses. That is consistent with your reading that the ArgvCommand seal was masking the enrolled-but-never-planned refusal rather than being the only defect. My run was killed by my own memory guard at ~9GB before reaching a verdict, so I never saw the ExpectedRedIdentityDidNotExecute you measured — your run is the stronger evidence, and I mention mine only because it was taken independently and agrees on the part it reached.

One finding, in the witness conjunct split.

The old assertion was one contiguous pattern, 'sudo' 'systemctl' 'enable' '--now'. You are right that it cannot survive — the flag now sits between them — but the replacement gives up more than the flag required. Two independent string_contains calls lose adjacency and ordering: the sudo pair may appear anywhere in the rendered string and the systemctl triple anywhere else, in either order, with anything interposed.

That is avoidable at zero cost, because the words are contiguous. extdeps.systemd.systemctl systemctl_enable_now_command builds:

argv_command(
  program: sudo_binary_path,
  arguments: append([sudo_non_interactive_flag, systemctl_program, "enable", "--now"], items: units)
)

so sudo, -n, systemctl, enable, --now are guaranteed to render in exactly that order with nothing between them. A single concatenated pattern over the same authority rows keeps the citation you wanted and keeps the adjacency the original had:

concat(concat("'", sudo_binary_path as String),
  concat("' '", concat(sudo_non_interactive_flag as String,
    concat("' '", concat(systemctl_program as String, "' 'enable' '--now'")))))

Strictly stronger than the split, and it still fails loudly the moment the builder reorders or interposes — which is the regression this row exists to catch. If you split deliberately because you expect the units list or some future flag to land between the two groups, say so in the annotation and I withdraw the point; right now the annotation argues the citation half, which I agree with, but not the split half.

Not blocking from me. The argv repair is correct, the expected-red exclusion is measured, and main is red until something like this lands.

@gunbai-bot gunbai-bot Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Second pass, covering the expected-red half my first review skipped. My earlier comment only established the ArgvCommand repair and the witness conjunct; since the PR body notes the exclusion work landed after the earlier three-file review, the roster change deserved its own read rather than inheriting anything.

Verified independently at a47aded05c9:

  1. The exclusion removes exactly three identities and does not broaden. Three literal name == "<fully qualified identity>" tests appended to the existing disjunction inside floor_expected_red_is_live's !( … ). No prefix match, no glob, no family predicate — so it cannot silently take a fourth row later.
  2. The three names correspond to real, distinct test fns. dag/test/claim/compile_accepted_unevaluable_program_control_test.dag carries exactly three matching test fns (extra / missing / wrong-type argument), so the roster strings are not stale or misspelled against their subject.
  3. The decline claim is grounded in the file itself, not inferred. That file declares data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree, and its own annotation states the declaration "costs floor admission under the DeclinedLiveTree arm rather than buying it." So discovered-but-never-planned follows from the subject's own declaration.

That is enough for me to agree the exclusion is correct and correctly scoped, and that it — not command_runner — is the remaining blocker. I got that wrong publicly earlier today and this PR's measurement is what corrected me.

One finding, non-blocking: the restoration trigger is prose where a typed carrier exists.

The annotation is explicit and well-argued — delete these three exclusions in the same change that lands #8977 or #8982 — but DESIGN §4c names "dissolution condition" among the machine-consumed facts that belong in a typed carrier, and says plainly that an annotation "is never evidence that a machine claim holds, because no Accepted program can read one." The carrier is available and idiomatic here: 46 files under src/v2 already use DissolutionCondition, so this is not a case where no carrier accepts the fact yet.

Weakening my own finding, because the annotation anticipates it: you state that once these identities become executable while still excluded they "count as ordinary failures and red the build." If that holds, the coupling is already fail-closed and the trigger is a convenience rather than the only thing standing between a landed #8977 and a silent hole. That is materially better than the usual prose-trigger case, and it is why I am raising this as a suggestion rather than a request for changes.

Not verified by me, and I will not claim otherwise: whether current CI reaches a complete floor. My own local --required-ci on the earlier head 63409bf3baa established strict-preparation state=completed and required-regen first_generation_equal=true, then was killed by my memory guard at ~9GB (exit 137 — a kill, not a verdict) after 485 witnesses. planned == executed == terminal with the seven gating populations empty needs to come from this PR's own CI run, not from me.

Argv repair: confirmed. Exclusion: correct and exactly scoped. Two suggestions outstanding (the conjunct adjacency from my first pass, this trigger), neither blocking.

gunbai-bot Bot pushed a commit that referenced this pull request Aug 23, 2026
…ion ruling is right

RULING ACCEPTED (swift-badger-524). #9031 fix/argv-command-ls-seal already
carries a fix for srv4_enables_its_declared_runner_instances, authored directly
on that branch minutes before this PR opened, and it is the branch unblocking
main's ledger. That row is restored here to main's text verbatim so the two
edits cannot collide; #9031's version lands.

I WAS WRONG ABOUT THE DERIVATION AND THE REASON MATTERS. I kept the elevation
words as a literal and cited DESIGN's oracle rule -- automating a literal's
update must not collapse the assertion to measure() == measure(). That rule is
correct and its premise is absent here, because there are TWO producers, not
one: the witness's pattern would derive from extdeps.sudo.elevation and
extdeps.systemd.systemctl, while the string being searched is produced by
runner_activate_command's BUILDER. measure(builder) == measure(authority) is
the join actually worth asserting -- that the builder routes through the
authorities instead of spelling its own words -- and it stays discriminating:
hardcode a bare sudo in the builder while the authority says /usr/bin/sudo and
the derived conjunct goes red. The collapse I feared needs the pattern derived
from the builder's own output, which nobody proposed.

Worse, my literal had the failure mode I was avoiding, pointing the other way: a
pinned 'sudo' 'systemctl' 'enable' '--now' is a THIRD authoring of facts two
extdeps modules already own, and it rotted the moment activation routed through
systemctl_enable_now_command -- which is why the row was red at all. Repinning
it buys one green run and re-arms the trap. #9031 derives what an authority owns
and leaves 'enable' and '--now' literal, because those are systemctl's own
operands that the builder spells inline and no row owns. That is the general
line and it is better than what I wrote.

WHAT REMAINS IS THE HALF NOTHING ELSE CARRIES. The installer witness rendered
RunnerCommandRefused as "" and then pattern-matched the empty string, so a
REFUSAL and a WRONG RENDER produced the same false with opposite repairs -- the
not-applicable-versus-malformed conflation. The assertions now run inside the
Ready arm and the refusal arm returns false on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014oG2tMMAxgZeAQxDjvNsFm
Brian Searls and others added 2 commits August 23, 2026 19:21
…overed

Operator ruling 2026-08-23: kill/quarantine it and find out why. Both halves are
here -- the move, and the measurement that makes this a filed defect rather than
a cost row.

#9031's run 32657761997 was otherwise clean (failed=0, stale_quarantine=0,
interrupted_before_verdict=0) with ONE non-pass:

  COMPLETED-OVER-COST-REQUIREMENT
  qualified_spelling_takes_the_shared_layer   wall_ms=57337 cpu_ms=57193
  [floor-claim-memory] grew rss by 1.22GB (to 11.70GB)

11x the 5000ms executor fail-stop. The fail-stop is doing its job: at 11.70GB
this claim genuinely threatens the process it runs in.

THE SPLIT THAT WAS SUPPOSED TO FIX THIS DID NOTHING, and that is the finding.
#8984's own note records the conjoined two-arm claim at 1.22GB RSS growth
against 0.12GB for the whole rest of the roster, and splits the arms into
separate test fns on the reasoning that two live compile_dag_rust_emit_check
calls sat inside one claim. Measured after the split, the QUALIFIED ARM ALONE
grows 1.22GB -- the identical figure -- and the bare arm appears in no over-cost
or memory line at all. So the entire cost was always the qualified arm. It is
not two compiles; it is the qualified-name resolution path specifically, which
is the path #8984 exists to repair.

WHY QUARANTINE AND NOT A COST ENVELOPE. An envelope large enough to admit 57s
and 1.22GB would admit the defect rather than measure it. The dissolution
condition on this row is therefore the REPAIR, explicitly not an envelope, so
nobody closes it by raising the ceiling.

WHY IT REACHED MAIN UNSEEN. #8984 landed in the batch after the last green run
while main refused at floor PREPARATION on an unrelated ArgvCommand seal break,
so its floor phase never executed and this row never surfaced on its own check.
That is the same masking that hid #9022's roster defect in the same batch -- two
independent defects merged behind one preparation refusal, which is the argument
for consuming every gating cause on your own run rather than only the rows you
meant to change.

WHAT MOVES: the file to dag/test/claim/long/ with its module renamed, one
WitnessExclusionRow carrying the measurement and the local recipe, and the
provider fixture's prose pointer updated so it does not name a module path that
no longer exists.

WHAT DOES NOT MOVE: both arms, their assertions and the whole authored rationale
are untouched. This is a home change and a declared rung drop, not a repair and
not a weakening of what the witness claims.
`exit_code` and `{` are both empty files introduced by 96da63f. They are
redirect and brace-expansion debris from an interactive shell, not source, and
nothing references either as a path: the `exit_code` hits in the corpus are
shell-transport OUTPUT FIELD decodings (`exit_code: Int from "exit_code"` in
extdeps.shell.exec and friends), which name a captured field, not a file.

Removed rather than ignored. A .gitignore rule does not untrack a path already
in the branch, and leaving them would land two junk files in the repo root on
merge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@gunbai-bot

gunbai-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

GREEN, and verified against the written bar rather than the icon — run 32661435799 @ ab85d55e3e5, conclusion=success, phases_run=3 failed=0.

check required measured
fold executed planned present, nonzero planned=10693 ✅
planned == executed == terminal 10693 == 10693 == 10693 ✅
no failing witness failed=0 0, and zero FAIL rows in the log ✅
no preparation refusal no floor refused: line none ✅
roster defect gone no ExpectedRedIdentityDidNotExecute absent ✅
seal conversion zero old-shape ArgvCommand diagnostics 0 ✅
regen passes ✅ (phase clean)
no over-exclusion (positive control) an unrelated expected-red still live known_red_held=36 ✅
stale_quarantine 0 ✅
interrupted_before_verdict 0 ✅
quarantine took effect completed_over_cost_requirement=0 0 ✅

The deltas are fully accounted for, which is the real check

A counter moving in the right direction is not evidence unless the movement is explained. Against the previous run:

planned    10695 -> 10693   (-2)  both quarantined witnesses left routing
passed     10414 -> 10413   (-1)  bare_spelling_shared_layer_is_unchanged was passing
over_cost      1 -> 0       (-1)  qualified_spelling_takes_the_shared_layer was the over-cost row

Two witnesses moved to a long home; one of them was passing and one was the over-cost row. -2 = -1 + -1, with no unexplained residue. Nothing else moved.

What this run does and does not establish

It establishes that this branch is clean. It does not establish that main is clean — main is still at faf6583461a and still cannot prepare. That only changes on merge.

For the record, the four repairs here all trace to one root-cause chain, which is why they travel together rather than being scope creep:

All three landed in one burst around 14:42–14:44Z, and the first refused early enough to hide the other two — which is why each became visible only as the previous one was repaired.

— sent from bright-ferret-335

@gunbai-bot
gunbai-bot Bot merged commit f498863 into main Aug 23, 2026
2 checks passed
@gunbai-bot
gunbai-bot Bot deleted the fix/argv-command-ls-seal branch August 23, 2026 20:26
gunbai-bot Bot pushed a commit that referenced this pull request Aug 23, 2026
…fold and declined_fixture=9 becomes measurable
gunbai-bot Bot pushed a commit that referenced this pull request Aug 23, 2026
gunbai-bot Bot pushed a commit that referenced this pull request Aug 23, 2026
gunbai-bot Bot pushed a commit that referenced this pull request Aug 23, 2026
gunbai-bot Bot pushed a commit that referenced this pull request Aug 23, 2026
briansrls pushed a commit that referenced this pull request Aug 23, 2026
* WIP: MAIN RED #4: eight runner-slot/fleet-capacity witnesses return Bool(fals

* Fix record-like if condition brace parsing

* Unenrol the seventeen rows this fix made pass, and keep the four beside them that it did not

The floor reported seventeen v2.test.claim.generated_conformance_floor identities as
STALE-QUARANTINE -- enrolled as expected-red and PASSED. That is this branch's fix working:
all seventeen were previously KNOWN-RED-RUNTIME-ERRORED, throwing `no such function` on names
that were genuinely declared, because the reference-closure collector narrowed on binding
metadata its own producer never populates. Deriving the closure from lexical binders instead
makes the calls resolve, the witnesses answer, and every one of them answers PASS.

The two arms are not symmetric and that is why this reds the build. KNOWN-RED-RUNTIME-ERRORED
is reported and deliberately NOT gating (claim_executor required_floor_outcome_is_clean lists
seven causes and that is not one of them); STALE-QUARANTINE IS one of the seven. So the fix
moved seventeen rows from a non-gating arm to a gating one, and the roster edit is the
prescribed remedy the floor's own message names.

The four generated_coproduct_exhaustiveness_* rows in the same generated module are retained
deliberately. They were not reported passing, so they are still red for their own reasons --
unenrolling the module wholesale would have dropped four live reds under cover of a repair
that never reached them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Convert the one ArgvCommand site the seal missed, through a builder rather than by widening the seal

main does not compile. `extdeps.exec.command` `ArgvCommand` became `sole_constructor
{ program, arguments }` in #8919, and its carrier states the invariant: "Every
ArgvCommand construction site in the corpus was converted in the same change, because
a partial seal is a dual-authority interval rather than a weaker seal."

One site was not. `gunbc.runner_slot_provision` `observe_runner_slot_members_wet` still
built `ArgvCommand { argv: ["ls", "-1", actions_runner_base_dir] }`, producing four
distinct diagnostics and refusing the floor at strict-preparation — so no witness ran
and no `required-floor` counter line was emitted at all.

Not a missed conversion by that PR. #8992 ADDED this site after #8919 was authored and
before it merged; neither touched the other's lines, so git merged both without a
conflict and the corpus broke on semantics. A clean merge is not coherence.

WHAT WAS DELIBERATELY NOT DONE: `runner_slot_provision` is not added to `argv_command`'s
`admit_callers`. That list is forty named builders, each for ONE operation of ONE tool,
homed in that tool's own extdeps module; admitting a product module would defeat the
wall rather than satisfy it.

A MODELED ALTERNATIVE WAS LOOKED FOR FIRST and does not exist at this layer. `ls -1 |
parse` is a shell-ism, so the right first question was whether the substrate already
answers "what entries are under this path" without shelling out. `dag/extdeps/filesystem`
carries posix `EntryKind` but no listing operation, and the `list_dir` in
`src/v2/extdeps/file_system.dag` is the v2 realization surface, not reachable from a v1-era
`wet` observation running over `LocalExec`. So the builder is authored rather than the
listing being re-modeled, and the call site keeps enumerating BY DIRECTORY — which its own
carrier requires, since probing the desired names could only ever return a subset of what
was already intended and would make an out-of-band slot invisible.

`-1` is baked into the builder with its reason, following `rm_force_command`'s precedent:
ls(1) columnates to a terminal and emits one-per-line to a pipe, so a caller that does not
pass `-1` depends on where its output happens to go. It does not recurse and does not
include dotfiles; a caller needing either needs its own name, not a flags parameter.

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

* Handle record-like conditions on operator RHS

* An enrolled identity that cannot execute is what reds main, not what protects it

Fixing the ArgvCommand seal unmasked nothing -- this was already visible. With
preparation passing, the fold runs and refuses:

  REQUIRED-FLOOR REFUSAL cause=ExpectedRedIdentityDidNotExecute count=3
  test.claim.compile_accepted_unevaluable_program_control.primitive_call_with_{extra,missing,wrong}_...

gunbc#9022 landed both the module and the enrolment. Its annotation states the
belief directly -- the file declares ReadsLiveTree and is DeclinedLiveTree, so
the three are "held here so that admission does not red main" -- and that is
backwards. run_required_floor refuses on any enrolled identity absent from the
executed claims, so a row whose home DECLINES execution cannot satisfy its own
enrolment. The roster requires observation; a declined identity is never
observed.

Not inferred: #9022's own witnesses check reported this exact refusal, count=3,
naming these three, and it merged with that check red. So main carries two
independent preparation-and-fold blockers today, and only the first was mine.

This is the authority-substitution shape -- I registered it in A, so B now does
X -- with A the roster, B the floor's admission, and no relation between them
pointing the way the sentence assumed. The relation that exists points the other
way.

WHAT AN ENROLMENT ASSERTS is that the identity RAN and FAILED as predicted. For
one that cannot run there is no truth value to hold, so the enrolment follows
execution rather than preceding it.

NOTHING IS HIDDEN. The three do not execute either way, so removing them conceals
no failing assertion and deletes no evidence: the probes, their two positive
controls, and the module's reasoning all stay put. DESIGN 4b(4) protects the
evidence, not the roster row -- and the evidence is the file, which is untouched.

DECLARED RESTORATION TRIGGER (4b(3)): re-enrol all three in the same change that
admits the file to execution -- the DeclinedLiveTree arm deletion, gunbc#8977 or
gunbc#8982, both OPEN. On that change they become planned and return false, and
the roster holds them as authored; if they PASS instead, stale-quarantine reds
the build naming them, which is what the family wants.

Six chunks in this roster are already Empty {}, so the empty chunk keeps the
established shape rather than deleting a function another index depends on.

* Enrolled-but-never-planned is not a parking spot: it refuses the whole floor

The ArgvCommand repair in this branch unmasked a second main-red defect that had
never been observed, because every run since it landed died at strict preparation
before the fold could reach it.

gunbc#9022 enrolled three compile_accepted_unevaluable_program_control identities
in the expected-red roster while their file declares ReadsLiveTree, so the
DeclinedLiveTree arm declines them (declined_live=899) and they are never planned.
The floor requires every ENROLLED identity to be observed among the EXECUTED
claims, so the run refuses with cause=ExpectedRedIdentityDidNotExecute count=3.

Chunk 21's own annotation states the opposite belief -- "until it lands they are
held here so that admission does not red main" -- and execution refutes it twice:
on #9022's own run 32644795043, which was merged red, and again on run 32649496046
here, where it was the sole floor refusal once the seal stopped masking it.

The precondition that enrolment was authored against has not landed: gunbc#8977 and
gunbc#8982, which delete the decline arm, are both still open.

The rows are correct and stay in chunk 21. Only their LIVENESS is wrong, so they
join the file's existing exclusion in floor_expected_red_is_live -- the mechanism
already used for the mock-totality family -- which keeps the rows for provenance
while removing them from the live roster.

Verified by execution, not by reading the predicate: each of the three returns
excluded (exit 1) through --claim-run, and an arbitrary unrelated identity returns
live (exit 0), so the predicate discriminates rather than excluding everything.

The exclusion is a coupling, not a note: whoever lands #8977 or #8982 must delete
these three exclusions in the same change, or the rows will execute while excluded,
count as ordinary failures, and red the build.

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

* The srv4 activation witness re-spelled two programs the seal then moved

Third blocker on main, and the first one visible only after the other two
cleared: with preparation passing and the roster refusal gone, the fold reaches
this row and it returns false.

  FAIL test.claim.runner_host_deploy.srv4_enables_its_declared_runner_instances

The cause is one conjunct, and it is not the width axis this row is otherwise
about:

  && string_contains(s: enable, pattern: "'sudo' 'systemctl' 'enable' '--now'")

That was correct against the hand-built argv the row was written for. #8919
routed runner_enable_command_argv through extdeps.systemd.systemctl
systemctl_enable_now_command, which mints through the sealed argv_command with
sudo_binary_path (/usr/bin/sudo) as the program and inserts sudo's
non-interactive flag -- so the rendered prefix is no longer the four words the
pattern names, and #8919 updated two lines of this file without reaching this
one.

WHAT I CHECKED BEFORE CHANGING ANYTHING, because a witness edited to match the
code it guards is worse than a red one. Quoting was the obvious suspect and it
is NOT the cause: shell_quote single-quotes unconditionally (emit_test's
shell_quote(arg: "plain") == "'plain'" pins it), so the unit-name conjuncts still
match. Admission was the other suspect and it is not the cause either: the
fixture receipts bind correctly on every arm admit_runner_activation checks --
instance host, managed_unit against intended_unit, all six verified flags, pool
host, and slice_unit against intended_compile_pool -- so the command is READY and
`enable` is a real render rather than the refusal arm's "".

THE REPAIR CITES RATHER THAN RE-SPELLS. The old pattern was a third authoring of
facts extdeps.sudo.elevation and extdeps.systemd.systemctl already own, which is
why it rotted without anyone touching it (DESIGN section 3). The conjunct is now
two, deriving the program spellings from those authorities and split because they
assert different things: that activation ELEVATES NON-INTERACTIVELY, and that it
reaches systemctl's ENABLE --NOW.

WHY THIS IS NOT measure() == measure(). `enable` and `--now` stay literal, and
they are the discriminating half: they are systemctl's own operands, spelled
inline by the builder rather than read from any row, so a command that carried
the units without the verb still fails here. What the derived halves buy is that
a future re-homing of the sudo or systemctl spelling moves the assertion with the
authority instead of leaving a fourth copy to rot.

Not verified locally: the same stale-binary limit recorded on the previous commit
applies. CI is the authority.

* Repin the srv4 enable witness on the grounded sudo spelling, and stop its refusal arm from erasing the cause

srv4_enables_its_declared_runner_instances was red on main, and not on the
roster/units join it exists to check. Its last conjunct pinned
'sudo' 'systemctl' 'enable' '--now', which was the rendering before
extdeps.sudo.elevation grounded the elevation words -- sudo_binary_path at the
absolute /usr/bin/sudo, sudo_non_interactive_flag at -n -- and
systemctl_enable_now_command now builds its argv from those rows. Both modules'
own annotations record that re-spelling as deliberate ("callers that previously
spelled a bare sudo now render this row -- a real change in the emitted words"),
so the witness was the straggler that update missed, not a defect in the fleet
model. The pattern is repinned as a literal rather than rebuilt from those two
rows, because deriving it from the same authority the builder consumes would
make the conjunct agree with itself whatever the rows say.

The second half of the brief is why neither falsification of this row located
itself. The refusal arm collapsed to "", so an activation refusal and a command
that says something else both arrived as one indistinguishable false -- every
string_contains below fails identically on an empty render. That is the
not-applicable-versus-malformed conflation DESIGN names, sitting in a witness.
Refusal is now its own match arm returning false directly, and the content
assertions run only inside RunnerCommandReady, where a command actually exists.
The same collapse in srv4_runner_installer_command_cites_installer_with_env is
fixed the same way, since it was one shape written twice.

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

* Remove trailing whitespace from expected-red roster

* join is not a std.types export: the two derived patterns build with concat

Preparation refused on the previous head with a single diagnostic --
  runner_host_deploy_witness_test.dag:4:33: name 'join' not found in module
  'std.types'
-- so the import I added to carry the derived invocation patterns named a symbol
that module does not export. The witness already builds every other composed
pattern with concat and needs no import for it; the two new ones now do the same.

This is my own defect from the previous commit, not new fallout: the repair it
carries is unchanged, only its spelling of string concatenation. The stale-binary
limit recorded there is exactly why it reached CI to be caught -- a local compile
under a binary that predates the seal cannot answer questions about this tree.

* Drop the enable-conjunct change: #9031 owns that row, and the derivation ruling is right

RULING ACCEPTED (swift-badger-524). #9031 fix/argv-command-ls-seal already
carries a fix for srv4_enables_its_declared_runner_instances, authored directly
on that branch minutes before this PR opened, and it is the branch unblocking
main's ledger. That row is restored here to main's text verbatim so the two
edits cannot collide; #9031's version lands.

I WAS WRONG ABOUT THE DERIVATION AND THE REASON MATTERS. I kept the elevation
words as a literal and cited DESIGN's oracle rule -- automating a literal's
update must not collapse the assertion to measure() == measure(). That rule is
correct and its premise is absent here, because there are TWO producers, not
one: the witness's pattern would derive from extdeps.sudo.elevation and
extdeps.systemd.systemctl, while the string being searched is produced by
runner_activate_command's BUILDER. measure(builder) == measure(authority) is
the join actually worth asserting -- that the builder routes through the
authorities instead of spelling its own words -- and it stays discriminating:
hardcode a bare sudo in the builder while the authority says /usr/bin/sudo and
the derived conjunct goes red. The collapse I feared needs the pattern derived
from the builder's own output, which nobody proposed.

Worse, my literal had the failure mode I was avoiding, pointing the other way: a
pinned 'sudo' 'systemctl' 'enable' '--now' is a THIRD authoring of facts two
extdeps modules already own, and it rotted the moment activation routed through
systemctl_enable_now_command -- which is why the row was red at all. Repinning
it buys one green run and re-arms the trap. #9031 derives what an authority owns
and leaves 'enable' and '--now' literal, because those are systemctl's own
operands that the builder spells inline and no row owns. That is the general
line and it is better than what I wrote.

WHAT REMAINS IS THE HALF NOTHING ELSE CARRIES. The installer witness rendered
RunnerCommandRefused as "" and then pattern-matched the empty string, so a
REFUSAL and a WRONG RENDER produced the same false with opposite repairs -- the
not-applicable-versus-malformed conflation. The assertions now run inside the
Ready arm and the refusal arm returns false on its own.

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

* Revert "Remove trailing whitespace from expected-red roster"

This reverts commit 0279359.

* Revert "Merge remote-tracking branch 'origin/pr-9020' into session/swift-heron-558"

This reverts commit fedaca4, reversing
changes made to 7981ec6.

* Name the no-brace parser retry debt

* Reconcile expected-red liveness authority

* The 57s qualified-spelling claim is a whole-tree policy resolve, not name resolution

The floor line that opened this (qualified_spelling_takes_the_shared_layer,
wall_ms=57337, +1.22GB, bare arm free) reads as "qualified-name resolution is
expensive". It is not. Qualified resolution is a map_get.

Measured, four orderings plus a discriminating control: a qualified PATTERN HEAD
costs nothing, and the premium lands on whichever claim first compiles a qualified
TYPE ANNOTATION -- once per process, then 5ms forever after.

The mechanism is severity classification, not resolution. A qualified annotation's
authored name misses env.source_visible_names (which carries the bare imported
names), so the masked type-ref arm emits an advisory UnlistedImportUse. Classifying
that one diagnostic calls compile_clean_unlisted_import_use_blocks_from_policy,
which resolves and typechecks a whole separate entry closure over
default_source_roots() -- the whole tree -- to evaluate one nullary Bool. A bare
annotation emits no such diagnostic and skips it; a qualified pattern head never
enters that arm.

This is gunbc.ci_spec's already-documented cost B from 2026-07-25, whose dissolve-on
(scope the policy resolve to the policy module's own import closure) was never
discharged. That note measured 57.8s for a green compile paying this tail against
the floor's 57337ms.

Shape, answering the brief: constant per process, independent of the subject
compiled, and corpus-denominated. Same probe, roots varied: 216ms warm against
44886ms when from_policy's whole-tree root set diverges from the caller's and pays a
cold load -- 44.9s locally against 57.3s on the floor.

Diagnosis only; no repair, and the 1.22GB is reported as consistent-with rather than
measured, since the witness is quarantined and the floor line was not re-run.

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

* Scope the compile-clean policy read to its own import closure, refusing rather than widening

Discharges the cost-B half of gunbc.ci_spec gunbc_ci_floor_batch_clamp_note's
dissolve-on, which has named this exact repair since 2026-07-25 and was never
landed. No second row is filed beside it: the obligation is discharged in place,
because landing the fix while leaving its obligation open would be two authorities
for one fact.

compile_clean_unlisted_import_use_blocks_from_policy resolved and typechecked an
entry closure over default_source_roots() -- the whole tree -- to evaluate one
nullary Bool. That is not merely oversized (the policy module has three imports);
it is a function answering a question about the caller's world by consulting a
different one, which is why it presents as cost but is correctness-shaped, and why
"n is small here" was never available.

It now assembles the policy entry's own import closure and resolves that explicit
source set. Every arm that cannot produce the exact closure REFUSES with a located
message naming the module and the path. There is deliberately no whole-tree
fallback: that arm would restore today's cost, zero the deficit's frequency by
construction, and make the widening unrankable ever after (DESIGN section 5). It is
a new closure builder rather than a reuse of resolve_virtual_source_with_imports
because that BFS silently SKIPS an unresolvable import -- a silent skip here would
answer the policy question from a graph missing the module the answer depends on.

MEASURED, same probe and orderings, all probes PASS so the narrowed closure still
returns the policy Bool:

  roots dag only     C first qualified   44886ms -> 109ms
  roots dag + src/v2 C first qualified     216ms -> 151ms

The qualified/bare asymmetry is gone rather than reduced: on the cold root set the
qualified arm is now CHEAPER than the bare one.

RESIDUE, reported rather than absorbed: the bare arm's 12967ms on the dag-only root
set did not move (12881ms). The bare arm pays it too, so it was never part of the
qualified asymmetry and this repair does not touch it.

NOT UPGRADED: the floor's 1.22GB is still unattributed by execution. If the memory
line survives this repair that is a second defect to find, not one to absorb here.

v1 freeze admission: PURPOSE test (operator ruling 2026-08-20) -- a defect repair on
a path the required floor executes every run, not growth on a v1 surface for v1's
own sake.

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

---------

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls added a commit that referenced this pull request Aug 23, 2026
…two breaks the merge made silently (#9023)

* Subsume the fleet's disk reclaimer: model what has been holding slots at 2.5GB, before anything trusts a slot-width number

A load-bearing fleet capacity mechanism runs on every runner host and has no
representation in the substrate. ctrl-runner-reclaim.timer has been reclaiming runner
workspace disk every 6h since 2026-07-23, and it is the reason slots measure ~2.5GB
instead of the ~13GB its own header describes. Nothing in the corpus models it.

READ FROM THE ARTIFACT, NOT FROM A DESCRIPTION OF IT. The model is derived from
reclaim-runner-disk.sh and its .service/.timer, following the precedent
runner_lifecycle_ctrl_void_note sets for exactly this case: read the ctrl artifact as
ground truth, subsume it here, never invoke ctrl from gunbc. Reading it surfaced a fifth
concern that a summary of the mechanism had dropped, and it is the sharpest one -- a
review killed mid-run leaks a refs/heads/review/* ref, and once its object goes missing
git gc dies AND writes a .git/gc.log that DISABLES ALL FUTURE AUTO-GC. The failure is
self-perpetuating: the thing that would reclaim the disk is what the wedge turns off.

NOT A SECOND HYGIENE AUTHORITY, and this is worth stating because the two were already
being conflated. gunbc.host_hygiene_reaper reaps residual SLOTS -- stale cgroups,
inactive units, control-override dirs -- and carries zero coverage of packs, repack,
tmp_pack, _diag or docker; verified by grep against it and its _remediate half, all six
terms zero. One word, "hygiene", over two mechanisms. So this is net-new modeling rather
than wiring up an inert successor, and the module says where the boundary is.

WHAT IS MODELED: the five growth paths as typed concerns; slot job state as a THREE-way
fact (running / idle / unobservable) because "a job is running" and "we could not tell"
refuse alike and repair differently; the repack decision as four arms rather than a Bool,
so a consumer can tell "already packed" from "we did not look"; and the script's operator
overrides carried as data rather than restated in prose.

EVIDENCE, with the mutation that proves it discriminates. Seven witnesses green by
execution. The control asserts the two refusals do not collapse to one answer; mutating
the production arm so the unobservable case returns RepackRefusedSlotBusy turns that
control RED (false) while its sibling stays GREEN (true) -- so it catches that specific
defect rather than everything reddening. The fragmentation gate is asserted at both sides
of the boundary, and the same 8GB checkout appears in the warranted and skipped rows so a
gate that had drifted to charging on size would fail.

WHAT IS DELIBERATELY NOT DONE: retiring runner_slot_disk_budget_per_slot. That literal is
circular -- byte_size(40960000000), derived by its own annotation as "~38GB per slot at
width 7 on 500GB class disk", which is the disk divided by the width it is then used to
preflight -- and it measures ~16x observed occupancy. slot_observed_bytes is the observed
quantity that replaces it. Per operator sequencing 2026-08-22 the reclaimer is subsumed
first, because the width numbers are downstream of whether reclamation runs at all.

Parse gate clean over 3881 modules.

* Two age floors were bare Int days beside a Second: ground the day on the cited ISO 8601 authority (review 54879)

review 54879 raised two findings on host_disk_reclaim, both real.

reclaim_diag_max_age_days and reclaim_tmp_pack_min_age_days were flat
Int scalars whose unit lived only in the identifier, sitting directly
beside reclaim_per_repo_timeout: Second doing it correctly -- one
module, two spellings for one concept.

Adding Day to std.measure's Scale was the wrong repair: Scale is a
closed coproduct, so a new member forces exhaustiveness churn across a
load-bearing std module for one downstream field. Instead the day is
grounded where the day already is cited -- extdeps.units.iso8601 gains
iso8601_hours_per_day(), and std.measure composes seconds_per_day()
from it exactly as minutes_per_hour() already delegates. Both fields
now carry Second.

Second finding: ObservationVerdict/UnknownRefused were imported and
never used. Dropped, with an annotation recording WHY SlotJobState is
not grounded on ObservationVerdict rather than leaving the next author
to re-derive it -- ObservationVerdict answers whether a subject agrees
with its desired state; SlotJobState answers what a slot is doing right
now, as an input to a decision. A busy slot is not drifted, and
reporting it as a convergence verdict would make correct operation read
as a fault.

Green by execution: the_two_refusals_do_not_share_one_answer -> true,
the_fragmentation_gate_is_exclusive_at_both_sides -> true, and a new
the_age_floors_keep_their_authored_ratio -> true. That one asserts the
RELATION (diag floor is 3x the tmp_pack floor, both positive), not
259200 seconds: a literal transcribed from the same arithmetic that
produced it is a change detector, red on a correct unit refactor and
green if both floors were wrong by the same factor.

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

* The fleet's disk reclaimer was installed by a script this repository could not see: model its service and timer, and teach systemd's authority the three directives that make it safe

MVP step 2. gunbc.host_disk_reclaim models what the reclaimer DECIDES;
this models how it is INSTALLED. The two are separate facts, and before
this commit the second was authored nowhere in the repository -- a
load-bearing, fleet-wide mechanism whose units were delivered by a shell
script, so nothing here could observe it, converge it, or notice it had
stopped.

The extdeps extension is the load-bearing half. SystemdServiceDirective
could not express Nice=, IOSchedulingClass= or TimeoutStartSec= -- the
exact three directives that let maintenance share a host with live CI.
A renderer missing them still produces a unit that RUNS; it simply
competes with production for CPU and disk, so the omission is invisible
in the rendered text and surfaces as someone else's latency.
IOSchedulingClass gets a closed coproduct rather than a NonEmptyStr
because the kernel closes that value set at three, and an authority that
closes a set then carries it as a string has re-opened it.

Two directives on the incumbent are deliberately NOT rendered.
Documentation= points into the ctrl repo's copy of the script and goes
stale on subsumption. EnvironmentFile=-/etc/default/ctrl-runner-reclaim
is dropped for a stronger reason: it is an escape hatch of exactly the
kind DESIGN section 5 forbids -- RECLAIM_GIT_GC disables the repack and
RECLAIM_GIT_GC_MIN_PACKS moves the fragmentation gate, both of which are
data rows here. A host carrying that file answers a different policy
than the one modeled, silently. That is a deliberate behavioral
difference from the incumbent, not a fidelity gap, and it has a standing
witness because re-adding the line looks like an improvement.

ExecStart still names the installed script. The terminal form is our own
binary running slot_repack_decisions with no shell in the path, but that
subcommand does not exist and a unit naming it would install a mechanism
that cannot run -- strictly worse on a fleet whose disks fill without it.
This is the gap-intolerant staged half of the replacement migration,
taken deliberately, with its next-rung trigger named on the seam.

Green by execution, six witnesses: the three resource directives reach
the rendered text; the outer timeout is derived from the per-repo budget
(asserted as the relation, not as 3600, so it cannot silently stop
tracking); the derived value reaches the unit; no environment hatch is
present; the timer drives the service persistently with jitter; and the
three I/O classes do not collapse to one wire word. Corpus parse gate
rc=0, 3884 modules, 0 diagnostics.

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

* "Cut off a provider" names two actions with different blast radii: give the operator both controls, and let the unwired one refuse by name

The operator asked for a capacity visualization AND control in the daily
workspace -- turn a machine down, cut off a provider. This is the
authority both halves read.

WHERE A CONTROL HAS TO BIND TO BE REAL. Both axes meet at
dispatch_selection provider_inventory_for_instance, which returns the
ProviderInventory that selection resolves against. So cutting a runtime
REMOVES ITS OFFER: the resolved selection has no constructor for a
provider that made no offer, which is structural impossibility rather
than a gate. A check placed after selection would concede that the
selected-then-rejected state is writable.

TWO AXES, NOT ONE ENABLED FLAG. Turning a machine down and cutting a
runtime are different questions. Fusing them makes the smaller action
unavailable -- an operator wanting Codex off everywhere would have to
take hosts down to get it. They are two withdrawal rosters over two
subject types.

WITHDRAWAL, NOT ENABLEMENT. Rows name what is CUT OFF, so an unlisted
subject is active. The inverse makes the roster load-bearing for
ordinary operation: a host absent through an authoring slip goes dark.
Withdrawal fails toward capacity remaining available, which is the
recoverable direction -- too much capacity is a cost, too little is an
outage.

THE CONTROL NAMES ITS AXIS EXPLICITLY (operator ruling 2026-08-23).
"Cut off a provider" could mean the runtime or one account binding, and
the conflation is silent in the worst direction: an operator meaning
"cut this Claude account" who gets Claude cut entirely discovers it as
missing capacity, not as a refusal, because both readings are
well-formed. So both controls exist from this first version and the
unwired account axis REFUSES with a typed ControlUnbound carrying the
axis and its trigger. An absent control would read as "not applicable
here" -- the not-applicable-versus-malformed conflation. The axis is not
hypothetical: a credentials update performing an active-slot swap
restarted a live container on 2026-08-22, so it is already being
operated by hand without a control.

The decision takes its rosters as ARGUMENTS, with the global readers as
thin wrappers, because the live rosters are empty and must stay empty --
a decision reading them directly would leave every refusal arm
unreachable by any fixture, making the witnesses decoration that is
cited as coverage.

Green by execution, seven witnesses, plus the mutation proof: collapsing
CapacityRefusedProviderWithdrawn into the host arm turns
a_downed_host_and_a_cut_runtime_do_not_share_one_answer red while
a_withdrawal_does_not_leak_to_its_siblings stays green. Corpus parse
gate rc=0, 0 diagnostics.

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

* Four control arms reported an effect that never happened: the control plans, and the gate that makes a cut real lands at the offer (review 54905)

Two changes: the review 54905 repair, and the consumer that makes this
authority non-inert.

THE REPAIR, AND IT WAS THE FAIL-CLOSED FAILURE IN MY OWN DIFF.
apply_capacity_control returned ControlApplied for CutHost,
CutProviderRuntime, RestoreHost and RestoreProviderRuntime while
actuating nothing -- the rosters are module-scope data rows, so a caller
that cut srv3 and then read host_is_withdrawn(srv3) saw false
immediately after being told the cut succeeded. The sibling witness
asserting the rosters stay empty made the two claims jointly
inconsistent by construction. That is DESIGN section 5 fabricated
plausible output, and the existing witnesses could not see it because
they compared outcome strings to each other rather than joining the
control to the roster reader.

WHAT ACTUALLY HAPPENS IS PLANNING, SO THE VOCABULARY NOW SAYS SO.
ControlApplied is deleted; ControlPlanned carries the roster edit that
would effect the request. Withdrawing capacity means authoring a row and
committing it -- deliberately, so a withdrawal faces review like any
other change to what the fleet does -- which is the same plan/apply
split the fleet converge path already uses.

The reviewer's proposed witness is now in tree in both directions:
plan a cut, then READ THE ROSTER. Mutation proof that it discriminates:
relabelling the planned arm "applied:" turns
planning_a_cut_does_not_withdraw_anything_by_itself red.

On the second finding, the Bool predicates are KEPT and the reason is
recorded on them. Present => true / Absent => false is predicate
dissolution and would block in std, but both callers -- the counts and
the dispatch gate -- want the boolean and not the row, so dissolving
would push a match over an Option whose payload is discarded into every
call site. The Option readers are exported beside them.

THE GATE. provider_inventory_for_instance now filters offers through the
capacity authority, so a withdrawn host or runtime MAKES NO OFFER and
ResolvedProviderSelection has no constructor for it. The declared
inventory keeps its own function so the gate has an unfiltered
denominator, and the withdrawn offers are returned separately: an
inventory emptied by withdrawal and one empty because nothing is
installed are different facts with opposite remedies, and a bare filter
renders both as the same empty list -- the empty-observation narrow.

capacity_admission collided with std.materialization_ladder; renamed to
fleet_capacity_admission rather than aliased, since two spellings in one
namespace is the fork.

Green by execution: 9 control witnesses, 4 gate witnesses proving the
gate CUTS (fixture-authored withdrawal, since the live rosters are empty
and a gate reading them directly could only ever be witnessed neutral),
plus 4 pre-existing dispatch_selection witnesses still green. Corpus
parse gate rc=0, 0 diagnostics.

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

* Put the fleet on the daily workspace: capacity at a glance, reading the same roster the dispatch gate reads

The operator asked for live fleet info on the daily workspace. This is
the panel.

IT READS gunbc.fleet_capacity_control DIRECTLY, not a display copy. That
is the whole difference between a control and a cockpit: a panel with
its own roster drifts from the thing it claims to steer, and the drift
is invisible because both sides stay internally consistent.

WHAT IT SHOWS: a summary line (active hosts, active runtimes, committed
slots), a row per host carrying committed width and per-slot memory
ceiling with its capacity standing, and a row per provider runtime. Live
values today: 4 of 4 hosts, 3 of 3 runtimes, 22 slots committed at 16
GiB each.

EVERY NUMBER IS LABELLED COMMITTED RATHER THAN OBSERVED, IN THE RENDERED
OUTPUT AND NOT ONLY IN A COMMENT. runner_slot_allocation states that
srv3/srv4's width of 6 is a provisioning target -- two slots that do not
exist yet -- while srv1/srv2's 5 matches live. A reader summing an
unqualified column gets 22 for a fleet running 20, and a capacity
decision made on that number is wrong on the only axis the panel exists
to inform. An annotation cannot carry the qualifier because no operator
reads the source.

The withdrawn-slots line is ABSENT when nothing is withdrawn rather than
reading zero: a standing "0 withdrawn" row is noise on every ordinary
day and trains the reader to skip the row where a non-zero number
finally matters.

A DEFECT THE WITNESSES CAUGHT, WORTH RECORDING BECAUSE OF WHAT IT WOULD
HAVE DONE. fleet_capacity_withdrawn_line bound its subtraction across a
line break, so the second operand parsed as a separate term and the
count was 22 rather than 0 -- the panel would have announced "22 slots
withdrawn by operator control" on every page load, a fabricated claim on
the operator's main surface, while the fleet was fully active. Found by
nothing_withdrawn_means_no_withdrawn_line, not by reading.

Green by execution, five witnesses. The load-bearing one serializes the
ACTUAL daily workspace document and finds the panel in the HTML -- a
witness over the fragment alone would prove it builds and say nothing
about whether it is composed into the page. The slot total is asserted
as a relation against gunbc_runner_slots_per_host rather than against
the literal 22, so it goes red on a panel that silently stopped reading
the allocation authority. Corpus parse gate rc=0, 0 diagnostics.

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

* Regen the three drifting stage0 mirrors: two are this PR's own seed-closure edits, the third is inherited from main

CI's regen phase refused at 38e95f3b14 naming three generated surfaces.
Reproduced locally to a fixed point rather than guessed at.

TWO ARE MINE AND ARE THIS PR'S OWN OBLIGATION. Adding
iso8601_hours_per_day to extdeps.units.iso8601 and hours_per_day /
seconds_per_day to std.measure -- the review 54879 unit-modeling repair
-- put this change inside the v1 seed closure, so both modules owe a
regenerated Rust mirror. The diffs are exactly those additions and
nothing else.

THE THIRD IS INHERITED AND IS REGENERATED, NOT AUTHORED.
v1_compiler_emit_rust.rs drifts on main independently of this branch
(three redundant `.clone()` removals from an emitter change whose
committed mirror went stale); the drift was confirmed present on
origin/main and on branches that do not touch the path. Regenerating a
generated file is mechanical and idempotent -- if the owning lane lands
the same regeneration the content is identical -- so this is installing
the emitter's current output, not taking over someone's repair.

Verified by execution rather than by inspection: claim_executor
--required-regen refused with the same three files before, and returns
first_generation_equal=true rc=0 after. The emitter was rebuilt from
this branch's HEAD rather than reusing a stale binary, because main's
emitter had itself moved -- generating mirrors with an old emitter is
how you install an artifact CI then rejects.

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

* Isolation becomes a fabric guarantee an offer can fail: seven named axes, a refusal that says which one, and the measurement that today's slots could not host a tenant

Operator direction: slots need to be mutually exclusive, containerized,
and ephemeral. This is the core half -- what work REQUIRES and an offer
PROVIDES -- and it deliberately does not name a container.

THREE GUARANTEES, NOT ONE, AND THIS FILE OWNS ONLY THE MIDDLE.
Allocation exclusivity (may two grants reserve one cell) belongs to the
durable grant store; a container provides none of it, and two brokers
can each start one on the same cell. Ephemerality and sanitation belong
to the lifecycle. Isolation -- what a run can observe once started -- is
this. Conflating the sandbox with the exclusion wall is how a fabric
double-books hardware while every run looks correctly isolated.

THE MECHANISM IS ABSENT ON PURPOSE. OCI, nspawn, microVM and dedicated
host are realization handlers; a core that named containers could not
admit a supplier satisfying the same guarantees another way, which is
DESIGN section 3's rule that transport sits outside the interface.

THE MATCH RETURNS THE MISSING GUARANTEES, NOT A BOOL. "Not isolated
enough" is unactionable; "cannot provide DedicatedKernel" tells a broker
which supplier class to find. A Bool would collapse a shared-kernel host
and a host with no namespacing into one answer with unrelated remedies.

A MODELING ERROR I MADE AND CORRECTED BEFORE COMMITTING, because it is
the more instructive half. I first gave the floor a FreshWritableRoot +
AttemptScopedSecrets requirement and made the fleet-offer fixture claim
the full sandbox profile so it would pass. Both were false: the floor
has run green for months on slots providing neither, so the requirement
invented a gap that does not exist, and the fixture asserted guarantees
current_runner_slot_profile measures as absent -- a fixture lying to
stay green. Corrected: the floor requires nothing and says why, the
fixture states what the fleet actually provides, and the gap moved to
tenant_workload_isolation_requirement, where it is real.

CONSUMPTION STATUS IS DECLARED, NOT IMPLIED. offer_fungibility_for has
no production consumer -- as its siblings ShapeNotCovered and
CapabilitiesNotOffered already record -- so this is a modeled decision
the broker will consume, not a live wall. The field and the arm that
reads it land together, per the standing rule in fabric_witness_run
after the inert-carrier lens caught an earlier declared-but-unread
value.

Green by execution, six witnesses. The gap witness asserts the COUNT of
missing tenant guarantees (4) so that providing one turns it red rather
than surviving partial progress, and it is paired with a control that
today's slots STILL satisfy our own floor -- without which a match that
refused every pairing would look like a working model. Adding the arm
forced exhaustiveness at two existing match sites, which is the closed
coproduct doing its job. Corpus parse gate rc=0, 0 diagnostics.

NOTE FOR gunbc#8960: that PR renames Offer -> SupplierOffer. This adds a
field and a fungibility arm to the same type against current main;
whichever lands second carries the other through, and the content is
mechanical.

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

* A committed width cannot say what would buy more: name the binding axis per host, and report the unmeasured one as unmeasured

The panel showed four hosts and a total. A total is the one number that
cannot answer the question an operator actually has -- what unblocks
more capacity -- because a committed width is a MINIMUM OVER AXES and
the surviving number discards which axis produced it.

THE AXES ARE NOT INTERCHANGEABLE PURCHASES. Where memory binds the
remedy is DIMMs; where cores bind it is a different machine. A reader
given only a fleet total assumes one story across four hosts.

THIS IS ABOUT TO MATTER MUCH MORE THAN IT DOES TODAY, which is why it
is authored now rather than after someone reads a total wrong. Measured
now, memory binds everywhere and the column looks redundant. Once the
CPU axis lands (gunbc#8976, relayed by warm-tern-755) srv1/srv3/srv4
become core-bound at 21 while srv2 stays memory-bound at 5 -- its 64 GiB
DIMM install failed training and was reverted, so it holds 8x16 GiB
where the others hold 8x64 -- and the single total becomes two unrelated
stories at 68 committed against 20 running.

THE UNMEASURED AXIS IS REPORTED AS UNMEASURED, NOT AS ADMITTING
EVERYTHING. Disk returns DiskWidthUnconstrained on every host, and
runner_slot_allocation's own reason string is explicit that this is "an
unmeasured axis, not a measured all-clear". Rendering it as admitting
any width would turn an absence of observation into a positive
clearance. It is excluded from the BINDING set by construction: an axis
that states no width cannot be at the minimum.

host_binding_width_axes returns a LIST rather than a winner, because a
tie is real information -- two axes at the same number means relieving
either alone buys nothing -- and picking one would have to break the tie
arbitrarily.

TWO RAW LENGTHS BECAME DERIVED RESERVATIONS. The first revision wrote
min-width 5rem and 9rem, which would have been the only untokened
lengths in the stylesheet and would need re-tuning by eye whenever a
label grew. They now derive the longest string each column can wear plus
a gutter, following roadmap_component dispatch_reserved_width exactly.
The metric column resolves to 35ch from "bound by memory · disk
unmeasured"; a new host or a longer phrase moves it by derivation.

CSS digest re-pinned to b6df98adc93f7077, derived by execution on this
tree per the convention in that file -- never chosen -- with a re-pin
note naming the rule family and its behavioral receipts.

Green by execution: an unmeasured axis is not reported as binding, the
binding axis sits at the committed width (asserted as a relation against
gunbc_runner_slots_per_host, so a label authored independently of the
arithmetic would go red), the panel renders it for every host, and the
digest pin holds. Corpus parse gate rc=0, 0 diagnostics.

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

* Hoist the isolation match out of the guard: it was computed twice on one input (review 54965)

The guard and the IsolationNotProvided payload each called
unsatisfied_isolation_guarantees on the same two arguments, so the fold
ran twice per refusal. One let binding.

Fixed rather than waved off as a nit on a two-element list, because
DESIGN section 6's bare-minimum-cost rule is explicit that a proven
cost-shape defect is always fixed regardless of the realized n: "n is
small here" is not a time-stable fact, and pricing per-site exceptions
is itself the redundant work the rule exists to avoid. The realized n
grows with the guarantee set and with the offer roster a broker will
eventually fold this over.

Green by execution: the fleet offer is still eligible for the floor, a
foreign trust domain still refuses despite ample capacity, and the
shared-kernel offer still misses exactly [DedicatedKernel].

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

* An 8 GiB x64 workload was fungible with a 6 GiB arm offer: fold the resource envelope into the shape, and stop the identity dropping axes the matcher compares

Reported by neat-heron-312 from the first production supplier binding
(product.supplier.ubicloud), where memory and disk surfaced as
UnexpressedSupplyFact. That is honest diagnosis and it does NOT repair
the decision: a sidecar saying "memory was dropped" cannot route work.

THE WRONG ANSWER, now executed as a witness. offer_covers_shape compared
threads and nothing else, so an 8 GiB x64 need was declared FUNGIBLE
with a 6 GiB arm offer whenever thread counts and the textual capability
ref matched. The broker routes work to a machine that cannot run it and
nothing downstream refuses, because fungibility already said yes.

NO SECOND ENVELOPE WAS MINTED. gunbc.fleet_container already owned the
requirement vocabulary -- CpuRequirement with its architecture axis,
GpuRequirement, Memory, Storage, Network -- inside a model of OUR
fleet's containers. Those are machine questions, not container
questions: a bare-metal host and a rented VM answer them identically. So
the five types moved verbatim to product.fabric.envelope and
fleet_container now consumes them. A fact's home is its layer.

EACH UNMET AXIS IS NAMED, for the reason the isolation match gives:
"does not fit" is unactionable, "needs 8589934592 bytes, offered
6442450944" tells a broker which supplier class to find. Architecture is
EQUALITY, not order -- there is no sense in which a bigger arm machine
covers an x64 need -- and an axis the work does not state is skipped
without being reported as checked.

THE HALF A FUNGIBILITY FIX ALONE WOULD HAVE MISSED, and it was my own
defect one PR earlier. work_identity_material hand-lists fields, and the
isolation profile I added in #8981 never reached it -- so two works
differing ONLY in what they require of their executor derived the SAME
key and deduplicated onto each other, while fungibility compared the
field. Identity and matching disagreeing about whether two things are
the same work is exactly the divergence that module's own note records
for threads. Both isolation and the envelope now feed the digest, with
absence rendered as its own token so "no memory requirement" and "a
requirement of zero" cannot collapse.

ShapeNotCovered is DELETED rather than kept beside its replacement. It
carried one axis, which ThreadsNotCovered now carries alongside the axes
it could not express; nothing constructs it, and a variant every
consumer must match and no producer can emit reads as coverage
(section 4b(4)).

Green by execution, seven witnesses: the reported wrong answer refuses
naming BOTH failing axes, a larger same-arch offer still covers
(positive control), a larger ARM offer does not cover an x64 need (the
asymmetry that proves equality rather than order), an unstated axis
neither refuses nor claims a check, and three material witnesses that
memory, architecture, and stated-versus-unstated each change the work
key. Four pre-existing fabric witnesses still green. Parse gate rc=0.

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

* The parse gate has a false-green mode, and this document caused it: the indexing line is not the verdict

Three lanes adopted this recipe today on the strength of one clean run.
Two of them hit failure modes the document did not describe, and one of
them corrects a mechanism claim the document asserted.

THE FALSE GREEN, which is the reason to fix this now. The run prints
"indexed 3884 modules from 2 source roots" and SUCCEEDS at that step;
the annotation-grain and parse diagnostics arrive after reconcile, at
the very end -- 128 seconds on the box that measured it. Anyone who
starts the command, sees a clean indexing line at 95 seconds and
interrupts reads a defect-free tree that is not defect-free. That is
worse than having no gate, because they will have "run the check".
neat-heron-312 came within a minute of reporting the recipe as broken
for exactly this reason.

THE MECHANISM CLAIM WAS WRONG AND IS CORRECTED IN PLACE. This document
said annotation grain is checked DURING INDEXING, and I repeated that to
two other lanes in messages. It is not: indexing reads the sources and
the refusal is raised later. What survives is the part that matters --
the diagnostic fires for modules the entry never imports and never
compiles -- and it now rests on a measurement rather than on my
reasoning about phases.

THE GATE NOW HAS BOTH ARMS. A check that has never gone red is a
decoration, and this one had only ever been run green. neat-heron-312
ran a discriminating pair on one tree with one binary: a planted in-body
// in dag/product/workload_simulation.dag gives EXIT=1 with the located
diagnostic, reverting gives EXIT=0 and 0 diagnostics. The planted defect
sat in a module the trivial entry does not import -- six files compiled,
3884 indexed, diagnostic from one of the 3878 never compiled at all.
That is stronger evidence for the trivial-entry trick than the argument
that motivated it.

THE STALE-BINARY MODE IS DOCUMENTED because it is the most expensive way
this can go wrong. A binary predating --entry refuses the flag; falling
back to a whole-corpus compile without an entry selects a parser that
rejects // outright and returns ~25964 errors on a CLEAN tree
(warm-tern-755, confirmed against a stashed HEAD). The degraded mode is
loud and its output reads as a discovery rather than as a broken
instrument -- the absorbing fallback with the failure wearing the
costume of an answer. Five-figure error counts mean check your binary.

Also recorded: the local gate locates by character offset while the
floor gives file:line:col for the same class, so a mismatch against
line numbers is expected rather than a broken grep.

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

* Re-point the one citation the envelope move outlived (cited-symbol gate)

Moving ResourceEnvelope from gunbc.fleet_container to
product.fabric.envelope left a DeclarationRef in gunbc.doc_graph_roots
naming the old home:

  cited-symbol: REFUSED DECLARATION-ABSENT gunbc.fleet_container ResourceEnvelope
  cited-symbol: FAIL 1 authored reference(s) do not resolve — a citation
  outlived what it names (DESIGN §3)

Re-pointed to the new module. The declaration name and field are
unchanged, so the citation names the same thing it always did.

WORTH RECORDING RATHER THAN JUST FIXING: this is the §3 citation rule
working exactly as designed, and it caught something no other gate
would. The floor was green, the parse gate was green, every witness was
green -- because nothing EXECUTES a doc-graph citation. It is a symbolic
reference in a hand-authored plan binding, and the only thing that
resolves it is the census built for that purpose.

It is also the argument for symbolic citations over positional ones,
made by execution rather than by assertion. A file:line pointer at the
old location would have gone silently stale in the same move, with
nothing able to detect it: a line offset is not reachable from the
namespace tree, so no census could have refused it. The name was
decidable, so it was refused, located, in one line.

Verified: claim_executor --required-cited-symbol returns rc=0, "every
authored reference resolves checked=390", where it refused before.

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

* A cell returns to supply only after teardown is proven, not when the process exits

The operator asked for ephemerality: cleanup after customer jobs, fresh
spawn with caching on every run. The load-bearing question underneath it
is not HOW to clean but WHO DECIDES THE CELL IS CLEAN, and the tempting
answer -- the job exited zero -- is wrong in a way that leaks one
tenant's state into the next one's run. A job can exit zero and leave a
detached child holding a mount, a populated writable layer, a secret
tmpfs, or a live network namespace. So the exit status is an INPUT to
sanitation and never its verdict, and this models the verdict.

THE SPLIT THAT MAKES "FRESH SPAWN WITH CACHING" COHERENT rather than
self-contradictory: the CELL (srv3-06) is a schedulable identity that
persists across many attempts; the SANDBOX exists for exactly one
attempt and none survives into the next. Fresh WRITABLE state per
attempt, reused IMMUTABLE material underneath, and tenant code never
edits the cache in place.

SIX SEPARATELY-OBSERVABLE FACTS, NOT A `cleaned: Bool`. Each fails
independently and each leaks something different, so a single boolean
lets any one of them fail invisibly behind the others succeeding. An
observation is three-valued, and the third value is the point:
Unobservable says the readback could not REACH the fact. Collapsing it
into Refuted would be safe but reports a failure that did not happen;
collapsing it into Confirmed is how residue reaches the next tenant.
They are kept distinct because their operator REMEDIES differ.

THE DEFECT THIS IS SHAPED AGAINST is DESIGN's empty-observation narrow:
a readback that confirms five facts and simply omits the sixth. A fold
walking the OBSERVED list concludes "nothing was reported wrong" from a
report that never covered the subject, and hands back a cell carrying
the previous tenant's working tree. So the fold walks the REQUIRED list
and demands an observation for each. Same narrow one level out: a host
agent lost mid-attempt produces no readback at all -- exactly when the
cell is MOST likely dirty -- so that yields every fact unobserved and
quarantines with a full list, rather than nothing-to-clean.

MEASURED, not asserted. Installing that precise defect in a copy of the
tree and running all six witnesses:

  true    a_fully_proven_teardown_returns_the_cell
  FALSE   five_confirmations_and_a_silence_do_not_return_the_cell
  true    an_unobservable_fact_refuses_reuse_without_claiming_teardown_failed
  true    a_refused_teardown_names_the_fact_and_its_detail
  FALSE   a_lost_host_agent_quarantines_with_every_fact_unproven
  true    the_six_facts_do_not_share_a_wire_word

Four of six pass the broken fold. That is the receipt for why those two
witnesses exist and why a green suite here is not self-evidently
meaningful: only the pair that walks the required list can see it. All
six return true on this branch.

The attempt's outcome is not a parameter of the verdict, and its ABSENCE
is the enforcement -- there is no argument through which an exit status
could arrive, so a caller cannot let a successful run stand in for a
proven teardown (§5 construction over validation). An earlier draft also
carried a same-signature forwarding function restating that in its name;
it was a hollow alias with no caller, so the rule moved to the real
function's header and the alias is gone.

RUNG, honestly: this is a MODEL, and nothing observes a real cgroup yet
-- current_runner_slot teardown is unmodeled and the readbacks here come
from fixtures. The verdict logic is structurally guaranteed (a cell
cannot be returned without a confirmation per required fact, because the
constructor demands the list); the OBSERVATIONS are at mitigatable,
since a host agent could report a confirmation it did not establish.
Next-rung trigger: binding SanitationObservation to a real per-attempt
readback on the host agent, at which point FactUnobservable stops being
a fixture value and starts carrying real transport failures.

Distinct from gunbc.host_disk_reclaim on purpose and stated in the
module: that is a cadence over a host answering "healthy over weeks" and
gates nothing; this is a barrier between two tenants, once per attempt,
and is the only one that gates reuse. Folding them would make a slow
disk-space job into a correctness dependency for every job start.

Local parse gate: 0 diagnostics, rc=0, past reconcile.

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

* The fleet's hardware reconcile had verdicts and no reader: render expectation-versus-observation, and keep "never read" out of "confirmed"

The operator asked for live fleet info on the daily workspace. The
capacity panel already shows committed slot widths; the HARDWARE those
widths are derived from had no view at all. The inventory has been
reconciling expectation against observation per host and producing typed
verdicts the whole time -- gunbc.fleet_physical_inventory
fleet_dimm_verdicts and fleet_processor_verdicts -- and nothing rendered
them, so a disagreement between what the tree believes and what the
machines report was reachable only by reading .dag source.

THE COST OF THAT WAS ALREADY PAID. srv2's 64 GiB DIMM upgrade was
modeled while the install failed training and was reverted, so the tree
asserted a memory population the machine did not have and the slot
widths computed from it were wrong on the one host that differed. The
verdict existed and said so. Nobody could see it.

THREE STANDINGS ARE NOT ENOUGH; THERE ARE FOUR, AND THE POINT IS THE ONES
THAT ARE NOT REFUSALS. Confirmed and Refused are the obvious pair. UNREAD
says no reading was taken, so there is nothing to disagree with. MISFILED
says the observation was filed against a different host, so NEITHER
machine has been assessed. Their remedies share nothing: go look at that
host's DIMMs, go RUN a reading, go find out which machine was measured.
Collapsing Unread into Confirmed asserts hardware nobody looked at;
collapsing it into Refused sends the operator to inspect a machine that
is fine. This is DESIGN's not-applicable-versus-malformed conflation on
the axis where it currently costs the most.

MEASURED, live, on the real fleet:

  4 of 4 memory confirmed · 0 of 4 processor confirmed
  srv1..srv4  memory     confirmed  8 sticks as expected
  srv1..srv4  processor  unread     no per-host processor reading exists...

EVERY PROCESSOR ROW IN THE FLEET IS UNREAD. The part is modeled intent
that every in-tree consumer agrees on, and no dmidecode receipt has ever
been supplied for any host. A panel mapping "no contradiction found" to
Confirmed would render four confirmations of a fact no instrument has
measured, on the page the operator reads to decide what is true about
their machines.

THE DISCRIMINATING RED, measured rather than asserted. Installing exactly
that collapse (ProcessorModelUnconfirmed => HardwareConfirmed) in a copy
of the tree:

  FALSE  every_processor_row_is_unread_and_none_is_confirmed
  true   the_memory_axis_is_confirmed_on_every_host
  true   the_four_verdict_shapes_map_to_four_distinct_standings
  true   the_four_standings_do_not_share_a_wire_word
  true   a_refusal_detail_carries_the_count_and_the_discrepancy_tally
  FALSE  the_summary_reports_each_axis_separately_and_never_one_fraction

Four of six pass the collapsed mapping. All eight witnesses return true
on this branch.

THE SUMMARY IS PER AXIS AND NEVER ONE FRACTION. Collapsed, it would read
"4 of 8 confirmed" -- arithmetically true and useless. Memory is fully
read; the processor axis has never been measured once. A solved problem
beside an unstarted one, and averaging them hides both.

Two live standings out of four means a mapping that collapsed the two
UNOCCUPIED arms would pass everything the fleet can currently exercise,
so those verdicts are authored in the witness rather than read from the
roster. Same reason the mixed-population summary row is authored: the
live rosters are all-confirmed and all-unread, and only a mixed input
separates counting confirmations from counting members.

The outstanding-contradictions line renders only when non-zero, and
UNREAD deliberately does not count toward it -- nothing has been
contradicted by a reading nobody took, and counting it would put the
panel in a permanent alarm state the operator cannot clear by fixing
anything.

RUNG: the panel is a reader, and it inherits its subject's rung rather
than adding one. The verdicts are the inventory's; this renders them
without re-deriving them, so panel and reconcile cannot disagree -- the
same reason the capacity panel reads the dispatch gate's own roster.
Next-rung trigger for the processor axis is a dmidecode -t processor
receipt per host, which is what turns four Unread rows into a real
comparison; the panel is what makes their absence visible in the
meantime.

Column reservations derive from the longest label each column can wear
(css_ch), following roadmap_component dispatch_reserved_width, so a new
host or a longer standing word moves the reservation by derivation and
there is no pixel to maintain.

Local parse gate: 0 diagnostics.

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

* Style the panel on the existing refused-state vocabulary, and re-pin the CSS digest by execution

Two things the panel commit owed, and one real defect that only the
whole-page consumer could find.

THE DEFECT: the first draft painted the refused and misfiled standings
with role_decl(prop: Color, r: SalienceRole). That COMPILED CLEAN --
0 diagnostics, every one of the eight panel witnesses green -- and then
failed at evaluation with NoSuchVariable { name: "SalienceRole" }.
SalienceRole is a TYPE in gunbc.design.salience, not one of the theme
roles role_decl accepts (CanvasRole, SurfaceRole, BorderRole, TextRole,
TextDimRole, FigureRole, FocusRole, BoundaryRole). A name that resolves
as a type and is then used where a value is needed passes the parse gate
and dies on the page.

Nothing in the panel's own witnesses could have caught it, and that is
the point worth recording rather than just fixing: every witness tested
the panel's LOGIC, and the stylesheet is not reachable from any of them.
It surfaced within seconds of serializing the actual daily workspace,
which is the consumer. A green witness file is not evidence the page
renders.

THE FIX is not a new role. var(--band-loud) is the established
refused-state vocabulary in this stylesheet -- the activity obligation
row already paints [data-state="refused"] with it -- so the panel reuses
it rather than minting a parallel one for the same meaning. Confirmed and
unread stay dim, unread additionally italic, so the four standings are
distinguishable by material and not by colour alone.

RECEIPT, by serializing the real page rather than the panel function:
233,714 bytes, the .fleet-hardware-standing section present, the summary
line rendering "4 of 4 memory confirmed · 0 of 4 processor confirmed",
four unread cells, and all twelve .hardware-* rules emitted. The derived
reservations land as 6ch and 11ch -- 11 being the width of "confirmed",
the longest of the four standing words plus the gutter -- so they are
computed, not typed.

THE RE-PIN: roadmap_css_lift_parity_digest moves 25d65c956b7578ca ->
9f37abcc65255edf, DERIVED by running roadmap_css_derived_digest against
this tree, never chosen. The accompanying note states what moved and why.

The two neighbouring digests deliberately did NOT move, and that is
scope evidence rather than an absence of checking: moodboard_css did not
move because this adds no rule it covers, and moodboard_html did not move
because it renders the thesis and principles rather than the stylesheet.
A change that had leaked past the roadmap stylesheet would have moved
them too. Both re-run green here alongside the re-pinned one.

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

* The slot-total witness assumed two symmetric pairs; name all four hosts instead

CI caught a real regression in my own witness after merging main.

WHAT BROKE. The row asserted the panel's total equals
srv1 * 2 + srv3 * 2 -- true only while the fleet was two symmetric pairs.
#8976 made CPU an admission axis and the symmetry ended: srv1/srv3/srv4
became core-bound while srv2 stayed memory-bound on 8x16 GiB, because its
64 GiB upgrade failed training and was reverted. The doubling was never
the property under test; it was a shortcut that happened to hold, and it
turned a genuine fleet asymmetry into a red on a witness about summing.

That the witness went red is correct behaviour -- it noticed the fleet
changed shape. What was wrong is what it asserted.

THE ATTRIBUTION, checked rather than assumed, because a red on my branch
after merging main is exactly the case where blaming main is convenient.
Main's own run 32621117917 at 13db52a25d fails EIGHT rows
(fleet_intent_memory, runner_capacity_plan x2, runner_host_deploy,
runner_slot_allocation x2, runner_slot_provision x2 -- all downstream of
the same reverted DIMM upgrade). My branch failed NINE. The one
difference is this row, and it is mine.

THE FIX names all four hosts. That keeps the join the row exists to make
-- the panel's total must equal the allocation authority's per-host
widths -- while carrying no assumption about which hosts resemble each
other, so a future asymmetry moves the number without reding the row.

It also stays a real oracle rather than collapsing to
measure() == measure(): the right side reads
gunbc.runner_slot_allocation, a different authority from the panel fold
on the left. Summing the panel's own fold on both sides would have been
the quiet way to make this green and would have asserted nothing.

Verified: this row and the three others in the file return true.

The remaining eight failures are main's and predate this branch.

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

* A refused serialization answered as an empty page, greening every negative assertion (review 55040)

Non-blocking review finding, and it is the absorbing fallback in my own
witness file, so it is worth fixing rather than noting.

empty_workspace_html matched the emission and returned "" on
EmitRejected. That is ⊥-as-answer conflated with ⊥-as-ignorance: every
!string_contains assertion in this file is SATISFIED by the empty
string, so a total serialization failure would have rendered the
negative rows green, while the positive rows could not distinguish "the
panel is missing from the page" from "nothing serialized at all". Two
states with opposite remedies collapsed into one answer, and the
collapse fails in the quiet direction.

Three changes, none of which widen:

The emission is now its own function returning the typed result, so the
refusal is available rather than discarded at the point of use.

The rejected arm carries the reason instead of vanishing, prefixed with
a marker no assertion in this file searches for -- so a positive
assertion fails on it, the text names what happened, and the marker
cannot accidentally satisfy an assertion either.

The refusal gets its own row. Without one it is only ever observed
indirectly, through whichever assertion happens to notice the page is
not what it expected, and the ledger would read "the panel is absent"
for a run where nothing was emitted. the_workspace_actually_serializes
makes it a finding with its own name. The one row carrying a negative
assertion over the HTML now also rests on the emission having succeeded.

The reviewer's second note -- host_is_withdrawn / provider_is_withdrawn
being Present => true / Absent => false -- I am leaving as it stands, and
the reviewer read it the way I intended: both real callers want the
Bool, the Option accessors are exported beside them, and dissolving the
predicate would push a discarding match into every call site. The module
already flags the tension; that is the honest state rather than a
resolved one.

Verified: all four rows over the serialized workspace return true.

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

* Nothing prevents the allocator from consuming the thing that allocates: three measurements and a ruling

Capacity has classes. Customer execution -- CI, agent tasks, batch -- is
offerable. Control plane -- scheduler, reconciliation, admission,
receipts -- is not: if control capacity appears as available vCPU,
available RAM, or a cheap slot, the allocator can schedule customer work
onto the resources that run the allocator. Nothing in the fabric supply
model expresses that distinction, so nothing refuses it.

THREE MEASUREMENTS, because any one alone is answerable and only
together are they a finding.

1. NO CAPACITY CLASS ANYWHERE. Searched ControlPlane / control_plane /
CapacityClass / capacity_class / Customer / customer across
product.fabric.supply, identity, work and gunbc.dispatch_selection. One
hit: the word "customer" inside a prose comment about displacement. No
type, field or arm names the distinction. The hit is CARRIED rather than
rounded to zero -- "no hits" is the claim a re-run falsifies, "one hit,
in prose, here is why it does not count" survives the re-run.

2. THE PLAUSIBLE CARRIER CANNOT HOLD IT AS A FACT. The only field shaped
to carry it is TrustDomainRef, a NonEmptyStr where brand. A branded
string carries the class only as a magic value agreed between producer
and consumer -- convention standing where necessity was available. That
is WORSE than the current absence, because absence is visible and a
convention reads as coverage.

3. THE PROTECTION HAS NO REACH ACROSS THE SEAM, and this is the one that
changes the picture. gunbc.fabric_control_plane_charge is real and
host-parametric: on the owned fleet a control-plane resident is
SUBTRACTED from what a host can commit, which is why control capacity
never becomes offerable there. Its importers are ci_runner_placement,
fleet_host_budget, itself and one witness -- all fleet-side. Zero
references to it or to fleet_host_budget anywhere under product/.

So the protection does not WEAKEN at the external-supplier seam. It was
never present on that side of it. Reading "control-plane capacity is
charged" as a property of the fabric is authority substitution: the fact
lives in one carrier, the operation is governed by another, and no
carrier claims the arrow between them.

THE RULING, from the product-direction lane: THE CLASS GOES ON THE WORK,
NOT THE OFFER. An offer is a thing a SUPPLIER SAYS ABOUT CAPACITY.
Putting the class there asks the party with the least knowledge and the
most incentive to say yes to make the safety assertion, a supplier that
omits the field is admitted, and the claim is unfalsifiable from our
side. We originate the demand, so we hold the fact.

Both refused remedies are KEPT with their reasons rather than deleted,
and the witness asserts their reasons are distinct: the offer arm is
refused on an AUTHORITY question, the trust_domain arm on a
REPRESENTATION one. A shared "not ruled in" would lose exactly the
distinction that stops someone arguing the second is fine once the first
is addressed -- and the trust_domain shortcut is cheap, looks like
modeling, and is the one a later reader will reach for.

THE MEASUREMENT BASE IS NAMED. The Offer field census was taken on
gunbc#8981's head, the widest that record has ever been. Measuring the
narrower record on main and reporting "no capacity class" would have been
true of a smaller surface and invited the reply that the field landed
since.

RUNG: this class sits BELOW mitigatable -- there is no failure to
contain, the invalid state is simply representable and unremarked. It is
not on the ladder. Next-rung trigger: the ruled remedy landing on Shape,
at which point it becomes a matching refusal and can be measured as one.
Nothing here is consumed by admission and the module says so; this row
keeps the gap countable rather than rediscovered.

ONE THING I MEASURED THAT A READER WOULD OTHERWISE GET WRONG. The eight
DeclarationRefs here look like the ones the cited-symbol gate enforces.
They are not: a fabricated decl_name leaves the gate green at
checked=390, unchanged, before and after this module gained an importing
witness. That is a DECLARED scope rather than a defect -- the lens's law
enrolls doc-graph binds, "structural, not corpus text scan", and its
dissolve-on already names widening to every carrier. Recorded in the
module because a citation that looks enforced and is not is worse than a
plain string.

Five witnesses green; local parse gate 0 diagnostics.

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

* The Spark slot assignment was decided the same day this note said it was missing: a rotted REASON, not a rotted line

fleet_intent_network refused to author the srv5/srv6 endpoints because
"the operator supplied the router table but never stated the
assignment", so choosing an address would be a 50/50 guess and therefore
the fabricated-plausible-output failure DESIGN §5 forbids.

THE DECISION EXISTS AND IS ONE IMPORT AWAY.
gunbc.spark.dgx_procurement `dgx_spark_router_binding_operator_allocation`
records srv5 taking spark-a3ee and srv6 taking spark-3bd5, decided
2026-08-07 under an explicit operator delegation, on an
ascending-address-order basis kept precisely so the tie-break is
attributable rather than looking like a measurement.
`srv5_router_binding` and `srv6_router_binding` are both SlotAssigned
carrying it. Both notes are dated 2026-08-07 -- the stated blocker was
resolved the day it was written.

THE DOCTRINE WAS INVOKED AGAINST THE WRONG TARGET, which is why this is
a correction rather than a refresh. §5 forbids fabricating an
OBSERVATION. An attributable ALLOCATION with a decider, a date and a
basis is not one -- it is exactly what the operator delegated. So the
module was refusing on the grounds that a decision had not been made
while the decision sat one import away.

THIS IS THE STALE-CITATION CLASS IN ITS EXPENSIVE FORM: a rotted REASON
rather than a rotted line number, and unlike a stale line it propagates
by being believed. A lane read it, concluded Spark work was blocked on
an operator fact, and reported that upward before anyone opened the
procurement carrier.

WHAT THIS DELIBERATELY DOES NOT DO: no endpoint row is added, and
`endpoints` / gunbc.fleet_intent's ComputeHost list are unchanged. In
this module that membership IS enrollment -- the module says so
structurally, which is a good decision, so that naming an identity
cannot be mistaken for enrolling it. Authoring the rows would enroll the
units, not describe them, and there is nothing to enroll into while they
are unplugged and the converge timer is retired.

A PROJECTED ENDPOINT ALSO CANNOT LIVE HERE, and that is structural.
gunbc.spark.dgx_procurement imports THIS module for
operator_host_srv5/srv6, so the reverse import is refused by the import
graph's one law. Measured, not inferred -- I added the import and
compiled:

  circular dependency detected:
    gunbc.fleet_intent_network -> gunbc.spark.dgx_procurement

Recorded in the note so the next person starts from the constraint
rather than the idea.

ONE METHOD NOTE WORTH MORE THAN THIS DIFF. My FIRST probe of that cycle
reported ZERO diagnostics and I nearly recorded "no cycle". The trivial
parse-gate entry never imports this module, so the cycle was never on the
resolve path; it appeared only once the probe entry actually reached the
module I had changed. Stated generally: A CLEAN COMPILE IS NOT EVIDENCE
OF NO CYCLE UNLESS THE ENTRY REACHES THE MODULE YOU CHANGED. That is an
absence produced by machinery that never performed the measurement,
rendered identically to a real zero -- the same class as a green gate
whose census never reached your file.

The citation is symbolic, module and symbol, no line number (§3).

Local parse gate with an entry that DOES reach this module: 0 blocking.

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

* The axis column reserved the widest HOSTNAME for words that are not hostnames (review 55065)

`.hardware-axis` took `hardware_subject_reserved_width()` — copied from
the column beside it — so a column carrying "memory" and "processor" was
reserved to the width of the longest HOST LABEL. Two unrelated
populations that happen to be adjacent, and the derivation read the
wrong one. Emitted 6ch for a column whose longest word is nine
characters.

That it looked derived is what made it survive review twice, mine
included: the call is a derivation, it just derives from the wrong
population. A hardcoded `11ch` would have been more obviously wrong.

FIXED BY GIVING THE AXIS WORDS AN AUTHORITY. `hardware_axis_labels()` is
now the one list, read by both the rendered rows and the reservation, so
a third axis widens the column by derivation rather than by someone
noticing. Emits 11ch, and the rows still render "memory" / "processor"
from that same list rather than from their own literals.

THE WITNESS ASSERTS THE DISCRIMINATING FACT, NOT THE TAUTOLOGY. A row
checking that the axis width is derived from the axis labels would be
`measure() == measure()` — it would pass against the defect too, because
the defect is also a derivation. What separates them is that the subject
population CANNOT HOLD the axis one: host labels are four characters,
"processor" is nine. So the row asserts the two reservations differ and
that the axis words are longer than any host label, which is exactly the
condition the copy-paste violates.

Digest re-pinned 9f37abcc65255edf -> becaf5e24965215d, derived by
executing `roadmap_css_derived_digest` against this tree, never chosen.
`moodboard_css` deliberately did not move and re-runs green beside it,
which is the scope evidence that this touched only the roadmap
stylesheet.

Local parse gate: 0 diagnostics.

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

* Consolidate eight approved fabric/fleet PRs, and repair the two breaks the merge made silently

Merges #8955 #8967 #8981 #8997 #9000 #9005 #9008 #9013 into one branch.

Three resolutions carried real decisions, and two of them were invisible to git:

- product.fabric.supply offer_fungibility_for: #8981 rewrote the body while main
  renamed Offer<P> to SupplierOffer<P>. Kept the new body on the current type
  name; unioned the import list.

- gunbc.roadmap_style: #9005 and #8967 each added a panel in the same region. The
  first union interleaved the two rule sets into a file that parsed as garbage
  (expected LParen, found Ident) with NO conflict markers present. Redone as a
  real 3-way and checked at the block boundary.

- roadmap_css_lift_parity_digest: both branches re-pinned it against a stylesheet
  holding only their own rules, so NEITHER value describes the merged sheet.
  Taking a side would have pinned a digest no emission produces. Re-derived by
  executing roadmap_css_derived_digest against this tree: e1434344ca3877e4.

And one break git merged cleanly into a third file: #8960's t_offer_with_quantum
predates #8981's required Shape.envelope and SupplierOffer.isolation, so the
literal lost its type. Both branches were green alone; only the merged tree
refuses. Repaired in the same style as t_offer beside it.

Verified on this branch: 0 blocking parse errors (the 52 source-annotation
diagnostics are pre-existing on main, same file, same count, measured with the
same probe against a clean checkout); every witness in every changed test file
passes.

* The last construction site the merge stranded: ubicloud's offer, and the diagnosis that outlived its deficit

CI's floor found what my entry-scoped probe could not: dag/product/supplier/ubicloud.dag
is a Shape/SupplierOffer construction site from main's #8960 that predates #8981
making `envelope` and `isolation` required. Same class as t_offer_with_quantum,
different file, and the floor resolves 3866 modules where my probe resolved one
import closure.

The fill is not mechanical, and neither field takes a placeholder:

envelope: the catalog publishes vcpus and memory_bytes, and this binding is the
REASON #8981 added the field -- product.fabric.work records it, citing
product.supplier.ubicloud by name. So memory is now EXPRESSED via
cpu_memory_envelope, and MemoryNotExpressibleInShape is DELETED from
UnexpressedSupplyFact rather than carried beside it. It would now be a false
statement about the model. A diagnosis that outlives the deficit it diagnoses is
worse than absent: it gets cited as a known gap while the gap is closed. Disk stays,
because the storage axis is still unpopulated from this catalog and that fact is
still true; the asymmetry is now the type's whole content.

isolation: nobody has measured what a Ubicloud runner provides. The catalog carries
vcpus, memory and disk and says nothing about kernel tenancy, namespacing or egress,
so naming shared_kernel_sandbox_profile() would invent a vendor guarantee from a
price list -- and worse than a wrong note, a broker would ROUTE work to it. The empty
profile is fail-closed by construction: unsatisfied_isolation_guarantees filters the
REQUIRED set against it, so every guarantee any work asks for comes back missing and
the offer refuses BY NAME, while work requiring nothing still matches. The empty list
alone would conflate "provides none" with "nobody looked", so IsolationNotObserved is
added beside it as a stated coverage obligation.

Censused every fabric Shape and SupplierOffer literal in the tree by hand afterwards
rather than trusting one entry closure again: ubicloud was the last one.

NOT FIXED HERE AND NOT MINE: the same floor run also refuses
runner_slot_provision.dag:240 on a sole_constructor ArgvCommand. That site is
untouched by this branch, is present on main, and main's own run 32646482842 is red
on it. #9031 carries that repair.

* gib_label divides by the authority, not by 1073741824

review 55104, advisory. The literal was a correct number and a second
representation of one std.measure already owns: gibibyte_scale_factor_bytes
builds it from extdeps.units.iec_80000_13 iec_kibi_factor. Display-only is not an
exemption -- that literal is what a GB/GiB confusion is made of, and the
authority is what already settled which one this is.

All 10 fleet_capacity_panel witnesses pass, including the whole-page
serialization row.

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Aug 23, 2026
… that closes the class (#9045)

#8919 replaced a hand-built argv at command_runner.dag stat_owner_name_capture with
extdeps.tools.stat stat_owner_user_name_command and did not add the import. main
refuses at strict preparation:

  function 'stat_owner_user_name_command' not found in scope
  (dag/gunbc/command_runner.dag:268)

This is the same class #9031 repaired -- a seal and its construction sites landing
in separate PRs -- and it is the second of two #8919 stranded. The call site is
correct and is exactly what the seal wants, so this changes the import list only.

THE CLASS IS CLOSED AT TWO, measured rather than hoped. Enumerated all 24
`*_command` builders defined under extdeps/tools/ and extdeps/posix/, then checked
every gunbc module that calls one for a matching import, a qualified spelling, or a
local definition. Exactly one unresolved instance -- this one. There is no third.

RED before, green after, on the same probe: a clean origin/main checkout produces
the diagnostic above; with the import the same entry compiles with 0 blocking
diagnostics and 52 source-annotation diagnostics, which are identical on main and
predate this change.

Co-authored-by: Brian Searls <briansearls1@gmail.com>
briansrls pushed a commit that referenced this pull request Aug 24, 2026
…declare the rung it drops (#9050)

* Delete the cited-symbol census job from CI (operator directive), and declare the rung it drops

WHAT IS REMOVED. The `cited-symbol` job — a second GitHub check-run invoking
`claim_executor --required-cited-symbol` over `dag` and `src/v2`, which resolved
every authored `DeclarationRef` against live declaration facts and refused a
reference whose module, declaration or field was absent or ambiguous. On the last
green main it reported `OK every authored reference resolves checked=390`.

Operator directive, 2026-08-23, in session chat: "i basically just want to delete
the cite census job". The instruction was given twice — first as "delete that, i
don't want it in CI", then narrowed after I began scoping a substrate change that
had not been asked for.

THE EDIT IS IN THE AUTHORITY, NOT THE ARTIFACT. `.github/workflows/witnesses.yml`
is generated from `gunbc.witness_floor_workflow` via
`gunbc.generated_artifact` `WitnessFloorYamlArtifact`; editing the YAML directly
would be drift that the next regen reverts. Removed from the authority: the job,
its id, run step, run script, bound steps, step annotations, capability-closure
predicate, and the 47-line comment block documenting the second-job rationale;
`jobs` becomes `[witness_floor_job()]` and the floor's `needs` becomes `[]`. The
orphaned argv producer `gunbc.fabric_witness_run` `cited_symbol_run_command` goes
with it, as does the one witness assertion over the deleted predicate. The YAML is
regenerated rather than hand-edited: -24 lines, exactly the job and the edge.

The deletion asserts `'fn witness_floor_job()' not in removed` before writing, so
a mis-anchored range cannot take the floor job with the census job.

WHAT IS DELIBERATELY NOT REMOVED, because a wider first reading of the instruction
would have broken a live wall. `std.decl_ref` and its 345 uses stay.
`DeclarationRef` is the MECHANISM, not the census: `admit_callers` on the sealed
`ArgvCommand` mint is one of those uses, and it correctly refused main earlier
today (#9031). Deleting the type or its uses would have dissolved a working
guarantee while removing a check. The lens `v2.lens.cited_symbol_resolution` and
the `--required-cited-symbol` mode also stay, now uninvoked — the same standing as
`--behavioral-receipt-*` and `--required-regen-fixed-point`, capabilities that
survive at their entry points with no workflow calling them.

THE RUNG DROP, DECLARED (DESIGN §4b(3)).
  PREVIOUS RUNG: mechanically preventable. A gate reliably exposed and blocked an
    unresolvable authored reference; the invalid state stayed writable.
  TEMPORARY RUNG: none. The class is now unguarded — a stale `DeclarationRef` is
    writable and nothing detects it. Review diligence is strictly weaker and is not
    claimed here as a substitute.
  REASON: operator directive. Recorded as given rather than reconstructed.
  BOUNDED POPULATION: the 390 references the last green run checked, plus every
    reference authored after it.
  RESTORATION TRIGGER: the lens's own declared dissolve-on — "substrate refuses
    unwritable DeclarationRef at construction". The operator named the same
    remedy: "you would just make them a normal compiler error or something". So
    this census was a §5 validation pass standing where construction was
    available, and it has carried its own replacement condition since it was
    written. The replacement is NOT built here.

WHAT THE CLASS ACTUALLY WAS, so the restoration is not re-derived from scratch.
The lens exists because §3 rules that citations are symbolic — a `file:line`
pointer is a second positional naming scheme that rots when anything above the
line moves. A symbolic citation is decidable, because resolving a name is the
`Node`-tree read the namespace authority already performs. A compile-time refusal
would be strictly STRONGER than what is deleted: `decl_facts` deliberately skips
test `.dag` files, so today a reference can be true and unresolvable at once and
needs a whole `CitationIndexCoverage` disposition to carry that; the compiler has
the full module graph and would not need the carrier. The open design question is
what a construction check resolves AGAINST — the entry closure (deterministic, but
refuses deliberate non-dependency citations) or the whole module graph (matches
today's behaviour, but makes the answer depend on what else is loaded, which is
the pool-dependence class). That question is not settled here.

REMOVING THE JOB ALSO REMOVES AN ORDERING EDGE, named because its reason
disappears with it. The floor `needs`-ed this job (operator ruling 2026-08-22)
after a measured race: both jobs are byte-identical through checkout, toolchain
and build, and when they started together on one self-hosted host both installs
wrote the same `/home/ghrunner/.cargo/bin/rustc` — `Text file busy`, ETXTBSY,
exit 126, at 1 run in 5. With one job left the collision is unrepresentable rather
than rare, so the edge is not needed. But the repair now lives nowhere: a second
job added later re-opens it, and the rejected alternative (per-job
CARGO_HOME/RUSTUP_HOME, paying a cold build every run) is recorded here so it is
not re-litigated from zero.

VERIFIED, WITH A CONTROL, BECAUSE "MY EDIT COULD NOT HAVE CAUSED THAT" IS THE
ASSUMPTION THAT FAILS HERE. Compiling the touched witness entry returns RC=1 with
53 hard diagnostics — ~52 §4c annotation violations in `dag/test/manual/` and one
unresolved `stat_owner_user_name_command` in `gunbc.command_runner`. Neither file
is touched here, but this change DELETES A FUNCTION, closures move, and name
resolution in this tree is pool-dependent, so a same-binary control was run
against unmodified `origin/main` in a detached worktree:

  branch   produced 53 hard   no-subject 52   stat_owner 1
  control  produced 53 hard   no-subject 52   stat_owner 1
  diagnostic sets: IDENTICAL

So both are pre-existing and this change moves neither. Note also that they stand
on a GREEN main, which means `dag/test/manual/` is outside the required floor's
subject: a directory of §4c violations of exactly the class that refused the whole
floor this morning is accumulating where nothing checks it. Not repaired here —
named so it is not rediscovered as new.

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

* Declare the rung the cited-symbol deletion drops, and restore the §4b passage a hand edit stranded

Review 55180 on #9050 is right: the PR title committed to declaring the
rung, and no declaration was in the diff. §4b(3) wants five things for a
lowered rung and this row carries all five -- previous rung
(mechanically preventable, measured green at checked=390 on run
32664434197, f498863), temporary rung (mitigatable: review
diligence), reason (the operator directive, not a defect finding),
bounded population (those 390 references plus everything authored
after), and a restoration trigger (the wall re-derived at ingestion on
the module whose source carries the citation -- the operator's own "just
make them a normal compiler error").

It is a sibling row beside the CI bullet rather than a clause inside it,
for the reason the regen row one line up already gives: that paragraph is
the most-edited prose in the repository and a declaration wedged into it
collides with every unrelated edit.

It also declines to claim the surviving --required-cited-symbol flag as a
mitigation. The mode still exists and still runs standalone, but no
workflow invokes it, and a mode nothing calls guards nothing -- counting
it would be the §4b(1) inflation this row exists to avoid.

SECOND, UNRELATED, AND NOT MINE: regenerating DESIGN.md would have
silently deleted a ~2350-character §4b passage. #8914 (f7de1fb) added
"At this rung, ask whether the check's RED is authorable before writing
the check" to the GENERATED DESIGN.md and never touched
gunbc.design_document -- its stat is DESIGN.md and fleet_reach_endpoint
only. The text has been quoted since, and existed nowhere the compiler
could see. Nothing caught it: the generated-artifact drift gate is on
DESIGN's own unguarded list.

Ported back into the authority at its exact anchor rather than restored
in the .md, because restoring the .md re-creates the drift and the next
regen deletes it again. The receipt that the port is faithful is the
regen itself: DESIGN.md comes back 1 insertion / 0 deletions, and the
§4b line is byte-identical to origin/main.

Also drops the trailing blank line review 55180 flagged.

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

* The rung-drop row called an unbounded population bounded

Side-chat review caught a real defect in the row I added one commit ago,
and it is the kind this document keeps naming: an exact measurement
joined to an open future set, and the sum still labelled bounded.

The row read "BOUNDED POPULATION: the 390 authored references that run
walked, plus every citation authored after it". The first half is a
measurement. The second half is unbounded by construction -- nothing
counts a citation authored after the cut and nothing refuses one. Calling
their union bounded is the same move §5 forbids when a count copied from
the current tree is asked to serve as an oracle, committed inside the one
row whose entire job is to be honest about a rung.

Split into two facts, and the §4b(3) status stated rather than implied:
the bounded-population requirement is NOT satisfied by this drop, so the
drop is an operator-approved exception to that clause rather than an
instance of it. A row that quietly failed a clause it cites would be
worse than one that admits it.

The other two pushbacks do not change the row. "Temporary rung: none"
would indeed be minting a rung out of an absence, but the row claims
mitigatable, which is what DESIGN.md already assigns to exactly this
residue twice -- §6 for the deleted inert-lens census and §3 for
v1_seed_standing, both "consumed by review diligence, so it sits at
mitigatable". And the requested disclosure that main_wet reconciliation
is blocked pending a content ruling would now be false: the design
passage was ported into the authority rather than reverted in the
projection, so that split is closed, not deferred.

Regen is 1 insertion / 1 deletion, both this row's line.

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

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
briansrls pushed a commit that referenced this pull request Aug 24, 2026
…ng (#9054)

THE HALF #9031 DID NOT CARRY. Its derived arm-6 repair landed and is better than
the literal I had proposed -- it cites sudo_binary_path,
sudo_non_interactive_flag and systemctl_program rather than re-spelling words two
extdeps modules own, so it cannot rot the way the pinned version did. This
change sits on top of it and repairs the other defect in the same row, which
that PR left untouched:

    RunnerCommandRefused { host_label: _, reason: _ } => ""

An empty render fails every string_contains below it, so "activation refused"
and "the command was built and says something else" arrive as ONE
indistinguishable false -- two facts with opposite repairs, one sending you to
the admission chain and the other to the argv builder. That is the
not-applicable-versus-malformed conflation, and it is why this row was reported
twice as Bool(false) with no located cause and cost two sessions an hour of
attribution work each time.

Refusal is now its own arm returning false directly; the content assertions run
only inside RunnerCommandReady, where a command exists.

TWO CONJUNCTS BECOME REACHABLE THAT WERE NOT. The negative
  !string_contains(enable, concat("srv4-", suffix(runner_count + 1)))
is VACUOUSLY TRUE on the empty string -- !contains("", X) holds for every X -- so
on any refusal it could never fail. It was decoration: permanently green,
carrying no information, and counted as coverage. Same for the all(names, ...)
conjunct, which is vacuous over an empty roster only, but whose per-name
contains could never fail against "". Moving the assertions inside the Ready arm
is what makes them able to run at all.

HONEST BOUNDARY: a witness returns Bool, so the refusal arm still cannot carry
the NonEmptyStr reason out to the log. What the split buys is that a refusal can
no longer masquerade as several simultaneously-false string conjuncts -- the
next reader sees one arm fail rather than four, and the reason is one match-arm
binding away instead of erased. Surfacing the reason itself needs a witness
carrier richer than Bool and is not attempted here.

EVIDENCE, both directions, against CURRENT main:
  green   PASS srv4_enables_its_declared_runner_instances
          PASS srv4_runner_installer_command_cites_installer_with_env
          PASS enrolled_host_still_admits
          PASS unenrolled_host_cannot_enable_runner_units   (drives the refusal
          path and asserts it refuses, so refusal is reachable, not hypothetical)
  red     one positive conjunct mutated (srv4-01 -> srv4-MUTANT):
          FAIL srv4_enables_its_declared_runner_instances
          PASS enrolled_host_still_admits   (positive control, same run)

The mutation is of a POSITIVE conjunct deliberately: a row whose only assertions
were negative would have stayed green under any mutation of them and reported
nothing.


Claude-Session: https://claude.ai/code/session_014oG2tMMAxgZeAQxDjvNsFm

Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants