Repository navigation
CPU becomes an admission axis, and the widths the DIMM upgrade invalidated are re-derived - #8976
Conversation
…dated are re-derived Main is red. #8947 replaced srv3/srv4's usable-RAM row -- a 125 GiB conservative lower bound borrowed from a 2026-07-16 srv3 reading -- with measured /proc/meminfo MemTotal of about 502 GiB, and left nine floor claims asserting the widths the old bound produced. This fixes all nine plus two more that would have broken next, and lands the CPU axis they cannot be honestly closed without. WHY THE CPU AXIS IS IN THIS DIFF AND NOT A FOLLOW-UP. host_memory_admitted_width answers what MEMORY admits, and 29 is now the true answer on srv3/srv4. But gunbc_runner_slots_per_host took a minimum over memory and disk only, and disk is DiskWidthUnconstrained on both -- so the minimum degenerated to memory alone and a corrected 29 would have COMMITTED 29 runners on a 128-core machine. The honest literal is only safe beside a CPU term. Either both land or neither does. host_cpu_admitted_width = cited cores / cores per slot = 128 / 6 = 21 Cores are read from extdeps.cpu.ampere altra_max_m12830_catalog rather than written down, so a host that is not an M128-30 cannot silently inherit the width; that the fleet IS uniform is checked by fleet_processor_verdicts rather than assumed here. CORES PER SLOT NOW DERIVES INSTEAD OF BEING AUTHORED. It was a literal 8 with no stated relation to the 16 GiB MemoryMax beside it, so the two could drift and only an author's memory tied them together. It is now ceil(memory_max / 3 GB) = 6, at the 1 core : 3 GiB shape the operator adopted 2026-08-23 -- the arm64 ratio Ubicloud publishes across its whole standard profile, adopted because a published provider catalog is a real reference and an invented number is not. Rounding is UP because that yields FEWER slots (128/6 = 21, not 128/5 = 25) and over-admitting is the harmful error. The wall's cores conjunct changes with it: it was `== 8`, a literal restating a literal twenty lines above -- the measure() == measure() shape that module's own oracle note says it purged -- and is now the relation cores * per_core >= memory_max, which a wrong rounding direction fails. SRV1 AND SRV2 GET THEIR FIRST MEASURED MemTotal, which is what puts srv1 at 21. srv1 reads 502.24 GiB and srv2 134314512384 bytes, both over SSH 2026-08-23, srv2 corroborated by its own docker daemon. srv2's lands about 96 MiB ABOVE the bound it replaces -- the bound was not merely loose there, it was on the wrong side, and a lower bound that is too high is not conservative at all. That retires the last consumer of gunbc_ci_conservative_lower_bound_usable_ram, so it is DELETED and its scaffold disposition discharged to Terminal: no arm remains in which one host's measurement answers for another's. The HostUsableRamConservativeLowerBound VARIANT stays -- nothing constructs it today, and an arm nothing currently occupies is a quiet guard rather than a dead one. host usable memory cpu committed srv1 502.24 GiB 27 21 21 srv2 125.09 GiB 5 21 5 srv3 502.25 GiB 29 21 21 srv4 502.25 GiB 29 21 21 Memory bound every host in this fleet's history until yesterday and now binds only srv2, whose 64 GiB install failed training and was reverted. THE TWO REAL DEFECTS, as against the stale claims. installed_bom_reconcile's two live-population rows routed srv1's observation through a local uniform_expectation() that hardcoded the Hynix catalog, so when #8947 moved srv1's production expectation to Samsung the row moved and the copy did not. Fixed by removing the seat rather than the copy: the four per-host expectations are now named data in fleet_physical_inventory and the witness imports srv1_dimm_expectation. uniform_expectation survives for the constructed fixtures, which legitimately author their own. WHAT THE STALE CLAIMS TAUGHT, since five of them needed rewriting rather than renumbering. The control-plane placement pair's answer INVERTED: the 2 GiB probe now costs srv3/srv4 a memory slot and srv1 nothing, and reaches the COMMITMENT only on srv2, because everywhere else CPU binds well below memory. The sessions-charge demonstration moved from srv1 to srv2 -- both its arms conserve on srv1 now, so the discrimination was gone, and relaxing the assertion until it passed would have silently retired a live claim whose example had grown out of it. And admitted_width_charging searched 1..8 under a comment calling that "wider than any host can reach", which stopped being true at 502 GiB: an outgrown search bound returns its own ceiling, so every host would have reported exactly 8 and the rows would have compared the bound to itself. Two witnesses that were passing on main are updated because this change breaks them, and one is renamed for cause: runner_slot_allocation_committed_widths_MATCH_MEMORY_ADMISSION stated a coincidence as a law. Committed equalled admitted only while memory bound every host, and a name that encodes a coincidence teaches the next reader the wrong invariant. CORRECTING THE LITERALS DOES NOT DISSOLVE THE SECOND REPRESENTATION. host_memory_admitted_width still authors four numbers that derived_width_reproduces_every_authored_literal then re-derives and joins to them -- validation standing where construction was available. The construction fix is for the function to CALL that derivation, and it is blocked on an import cycle: gunbc.ci_floor_measurement imports runner_slot_allocation for the slot shape, so runner_slot_allocation cannot import the measurement. Breaking it means moving the slot shape to its own authority across roughly twenty-five files, and stays a separate change. This leaves a corrected copy where an uncorrected one stood. Six samsung_dram_module rows are removed from floor_expected_red -- an edit to a nine-element list, not a deletion of it; the three still-red rows stay. The witnesses themselves are kept as permanent regression controls. No CPU reservation is modeled. Every core is offered to slots, so the host, its kernel and the session containers on srv1/srv2 are charged nothing on that axis -- a real gap, confined to CPU, where oversubscription degrades throughput rather than triggering the OOM kill that makes memory unforgiving. Dissolve-on is recorded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e roster edit handed off Three things, in the order they were found. THE PARSE ERROR IS MINE AND IT IS THE PLAINEST FORM OF THE FAILURE. The four per-host DIMM expectations were written as `data X: HostDimmExpectation =` with the initializer on the NEXT line, which the grammar does not take. CI refused the whole module index. I pushed .dag I had never compiled -- typecheck-by-eye is not a consumer, and DESIGN says so in as many words. Initializers are now on one line and this branch is verified locally before pushing rather than after. THE SATURATING SEARCH BOUND IS AN ABSORBING FALLBACK, AND RAISING 8 TO 40 WOULD HAVE LEFT IT. admitted_width_charging folded a bounded range and returned the last admissible element, so a host admitting MORE than the bound got the bound back as a number indistinguishable from a real measurement. That is not hypothetical: it fired. From the 2026-08-22 upgrade until now, srv3 and srv4 admitted 29 against a bound of 8 and reported exactly 8, so two of tonight's six failures were rows comparing the bound to itself. The deficit never surfaced as itself -- it arrived as an unrelated inequality, which is exactly the frequency-zeroing DESIGN 5 describes. The fold now answers a typed verdict: WidthSearchFound when the largest admissible width is strictly inside the range, WidthSearchSaturated naming the host and the bound when the top is still admissible -- at which point the search has not measured a width, it has run out of room to look. The Int projection converts saturation to -1 rather than to the bound, so an assertion fails loudly instead of passing at the ceiling. THE RANGE IS A PARAMETER SO THE REFUSAL HAS AN AUTHORABLE RED. No real host admits 40, so a saturation arm written only against the standard range could never be driven red by any input -- permanently green by construction, and worse than absent because it would be cited as coverage. DESIGN 4b says to ask whether the RED is authorable BEFORE writing the check, and to judge that against what a FIXTURE may author rather than what the corpus contains. admitted_width_search_within takes the range and its top, so saturation_refuses_rather_than_reporting_the_ceiling hands srv3 a range of 1..3 and gets the refusal, with a positive control on srv2 that stops the arm being satisfied by refusing always. no_enrolled_host_saturates_the_standard_search_bound is the quiet guard beside it: mechanism exists, reachable from this operation's own denominator, currently unoccupied, and red the day a host outgrows 40. THE SIX-ROW EXPECTED-RED UN-ENROLMENT IS DROPPED AND HANDED TO #8959, which needs the same nine-element list edited to present anything but a red and needs none of this lane's memory knowledge. Two lanes editing one shared row is a conflict neither wanted; the witnesses stay put in both diffs and only the enrolment moves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Two changes since the approving review, plus a blocker note. 1. The saturating search bound is now a typed refusal, not a bigger literal. It now returns 2. The six-row expected-red un-enrolment is dropped, handed to #8959 which needs the same nine-element list and shares nothing else with this lane. Witnesses stay put; only the enrolment moves. Correction to the PR body's census: there were SIX Also mine, and caught by CI rather than by me: the first push carried a parse error — the four DIMM expectation rows had their initializer on the line after Blocker, not this PR's: main is deterministically red until #8959 merges — #8909 landed the grammar half of the floor's terminal ledger and the seed's collapsing — sent from warm-tern-755 |
…tead of reasoning about it A local floor run on this branch found five failures. Four are consequences of this lane's own change and are repaired here. The fifth is #8947's and is diagnosed below rather than guessed at. THE RETIRED-SLOT FIXTURE WAS PINNED TO AN INDEX THE FLEET GREW PAST. build_cache_endpoint_observe's witness_retired_runner_slot_owner_refuses fed the decoder a cgroup path naming srv1-10 and asserted the lifecycle came back Retired. That held while srv1 committed five slots. It commits twenty-one now, so slot ten is DESIRED and the fixture silently stopped being a specimen of the thing it is named for. Bumping it to a larger literal would have the identical defect on the next width change -- the same trap this branch already hit in a different costume, where a search bound's comment called 8 "wider than any host can reach". The index is now computed as one past what the host actually commits, and the assertion checks the PROPERTY the decoder decides -- slot_index > committed width, which is what retired MEANS -- rather than an equality that has to be retyped whenever the fleet moves. TWO BOUND-ARM CONTROLS LOST THEIR SUBJECT WHEN srv2 WAS PROMOTED. Both reached HostUsableRamConservativeLowerBound through srv2, and every enrolled host now carries its own measured MemTotal, so nothing in the fleet sits on that arm. They are re-pointed at a constructed fixture, NOT deleted. Three questions decide whether a guard should exist and only the first two matter: the mechanism exists, and it is reachable from what a fixture may author. Only current occupancy is zero, which is a quiet guard rather than a dead one -- a fifth host enrolled tomorrow with no read of its own lands on exactly this arm. A new row asserts the complementary fact the fixtures can no longer carry: every enrolled host is on the measured arm, which reds if one is ever demoted back to a borrowed figure. ONE ROW MIXED A FIXTURE HOST WITH A LIVE POOL. allocation_conserves_when_pools_fit passed a 125 GiB literal as the host while drawing runner_pool from srv1's live budget. Those agreed only while srv1 happened to be a 125 GiB machine; after the upgrade the literal described a different computer than the pool did. It reds for the mismatch, not for anything about conservation. The host figure now comes from the host the row names. AND ONE FORK THAT WOULD HAVE SILENTLY EATEN THIS WHOLE CHANGE, found while reading the provisioning path rather than by any test. RunnerHostDeploy.runner_count was a hand-authored 6 on srv3 and srv4, answering the same question as gunbc_runner_slots_per_host. With the literal in place, allocation would say 21 and PROVISIONING WOULD STILL HAVE STOOD UP SIX -- the model and the materialization path disagreeing about the fleet with no diagnostic anywhere, which is precisely the roster-versus-prose failure width_is_a_minimum_over_axes_note records for srv4. It now derives from the width authority. Width is decided by the admission axes and materialization CONSUMES that decision; a deploy row is not a place to re-decide it. Recorded in the same note, because a reader seeing two derived rows will assume the fleet is covered: srv1 and srv2 have NO RunnerHostDeploy row at all. srv1's move from five slots to twenty-one still has no provisioning path. Pre-existing, not closed here, and not silent any more. THE FIFTH FAILURE IS NOT REPAIRED AND NOT GUESSED AT. fleet_intent_memory.srv2_population_matches_bmc_memory_summary is #8947's, and it has a measured diagnosis rather than a theory: run directly against its own entry it returns TRUE, and it fails only under the whole-corpus floor. Both sides compute exactly 137438953472 in isolation. That localizes it to cross-module name resolution under the full module population -- the class this very test file's ambiguous_type_name_import_note already documents, where a targeted compile resolves a closure that never loads the homonym and reports clean while the floor loads both and refuses. My earlier hypothesis -- that extdeps.memory.sk_hynix and extdeps.memory.samsung both defining stick_capacity_bytes was the mechanism -- is REFUTED as stated: a probe importing both still reads the Hynix capacity as 16 GiB. Right class, wrong reproduction; two modules is too small a population to trigger it. Left for its own change rather than repaired blind, and it is not a regression of this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…olumn it was authored for is now live Resolved the one conflict in runner_slot_allocation as a UNION rather than a choice. Both sides added functions at the same point and neither supersedes the other: main (#8976) added host_cpu_admitted_width and made the width a minimum over three axes; this branch added the model that says WHICH axis bound it. The union is not merely both texts. host_width_axes had to gain the CPU axis, because a binding-axis fold that enumerated only memory and disk would report "bound by memory" for a host whose width came from cores -- naming a cause that did not produce the number, which is worse than reporting no cause at all. THE COLUMN WAS AUTHORED FOR A FUTURE THAT ARRIVED DURING THE MERGE. Its own annotation said the axes would diverge once the CPU ruling landed; it landed. Measured on the merged tree: srv1/srv3/srv4 are core-bound at 21, srv2 is memory-bound at 5 on its reverted DIMMs, and the panel now reads 68 slots committed against 20 running. That is two unrelated stories behind one total, which is exactly what the column exists to keep an operator from misreading. One witness was FALSIFIED by the merge and was fixed rather than relaxed: an_unmeasured_axis_is_not_reported_as_binding asserted srv1 was memory-bound, which stopped being true. Its axis-naming half moved into a new, stronger witness -- srv1_and_srv2_are_bound_by_different_axes -- which is the discriminating one: a label function naming a single axis for the whole fleet passes every other assertion in the file and fails this one. CSS digest re-derived on the MERGED tree per that file's own resolution note (a digest pins emitted bytes, and a merge is the one moment when the bytes belong to a tree neither side built). It holds at b6df98adc93f7077 -- the longest metric label is still "bound by memory · disk unmeasured" at 35ch, so the derived column reservation did not move. NOTE, NOT MINE: the corpus-wide parse gate now refuses on dag/test/claim/build_cache_endpoint_observe_test.dag -- a section 4c annotation inside a declaration body, last touched by #8976, which this branch does not modify. Reported to that lane; recorded here so a reader does not attribute it to this merge. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012q31BK3okLA8vG4kdTWtBf
…ole floor at preparation (#8989) Main is parse-broken and it is mine. #8976 landed a four-line // block INSIDE the match body of witness_retired_runner_slot_owner_refuses. DESIGN §4c admits standalone leading blocks attached to MODULE-SCOPE declarations and nothing else, so an annotation indented into a declaration body is a located parse refusal. WHAT IT COSTS IS OUT OF ALL PROPORTION TO WHAT IT IS. It refuses at PREPARATION, before any witness runs, so the floor reports: required-ci: floor refused: subject=949e339137b087a8 modules_resolved=3827 modules_excluded=4 required-ci: FAILED PHASE floor Not one failing row -- every witness in the corpus taken down by a comment's indentation. It also breaks the local corpus-wide parse gate several lanes use before pushing, which is how silent-bear-842 found it: their branch went rc=0 to rc=1 across a merge in which they changed nothing near it. The annotation is hoisted above the declaration unchanged in substance, and extended to record where the check does and does not live -- because that is the part worth keeping. --required-cited-symbol parses the whole corpus and passed this file CLEAN, twice, before I pushed #8976. Annotation grain is enforced in the floor's preparation, not in that census, so a green parse is not evidence that an annotation is placed legally. That is the second time in one night this lane shipped on a local check that could not see the defect class it was being trusted for; the first was a line break after `data X: Type =`, which cited-symbol DID catch. Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ts 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 13db52a 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
…ndex instead of writing one down (#8998) The CPU-axis change (#8976) turned seven witnesses red. Seven of the eight failures on main are this, and they are ONE defect rather than seven, which is what makes retyping the numbers the wrong repair. EVERY FAILING FIXTURE PICKED AN INDEX THAT WAS OUTSIDE THE COMMITTED POPULATION AT WIDTH 5-6 AND IS INSIDE IT AT 21. a_width_above_the_committed_ceiling_is_refused_not_silently_unfulfilled asked for width 7 against a ceiling of 6. At a ceiling of 21, seven is an ordinary in-range request the plan correctly fulfils. a_github_runner_in_a_fabric_slot_refuses_instead_of_reading_converged required srv1 [1, 6] to refuse. Slot 6 was outside srv1's five-wide population; at twenty-one it is an ordinary committed member. an_identity_outside_the_committed_population_is_refused_not_classified read srv1-06 and srv3-07 as outside. Both are now inside. introducing_the_fabric_slot_deregisters_no_live_runner pinned github_count == 5, which was 6 committed less 1 fabric. srv4_enables_six_named_runner_instances pinned the roster at six AND asserted srv4-07 is absent -- two measurements of the same day, falsified together. So the refusal arms did not move. The fixtures silently stopped discriminating: each chose an out-of-range index by writing down a number that happened to be out of range at the width of the day. Retyping 7 as 22 re-arms the identical landmine one width later -- a fixture whose RED depends on a number nobody derived is a change detector wearing a property's name (DESIGN 5: a measurement copied from the same current tree is not an oracle). Each now derives its discriminator: committed + 1 for an out-of-range index, committed - 1 for the GitHub count after the fabric carve, runner_count for the roster length. They discriminate at any width. TWO CLAUSES DELIBERATELY UNTOUCHED, because they are the informative half. srv3-99 is outside at any plausible width. And every srv3-06 clause passed through the width move without noticing: srv3-06 refuses because it is the AUTHORED fabric identity, not because of where it sits relative to a width. runner_slot_allocation was built that way precisely so a width change could not silently relabel a slot that may be running work, and this is that defence observed surviving a real width change rather than only asserted. ONE DELIBERATE NON-GENERALIZATION. introducing_the_fabric_slot_deregisters_no_live_runner says committed - 1, not committed - host_fabric_slot_count. The general form is true for ANY number of fabric members including zero, and that conservation is already asserted by slot_purposes_partition_every_host_committed_width. THIS row is about the specific carve -- srv3 gives up exactly one slot -- and writing it generally would leave that claim asserted nowhere. Two further rows of this class are already repaired in #8992 (witness_srv3_deploy_row_names_six_slots, witness_srv4_runner_count_six_materialization_target). Co-authored-by: Brian Searls <briansearls1@gmail.com>
…plan and apply (#8992) * Hoist a §4c-illegal annotation out of a match body: it refused the whole floor at preparation Main is parse-broken and it is mine. #8976 landed a four-line // block INSIDE the match body of witness_retired_runner_slot_owner_refuses. DESIGN §4c admits standalone leading blocks attached to MODULE-SCOPE declarations and nothing else, so an annotation indented into a declaration body is a located parse refusal. WHAT IT COSTS IS OUT OF ALL PROPORTION TO WHAT IT IS. It refuses at PREPARATION, before any witness runs, so the floor reports: required-ci: floor refused: subject=949e339137b087a8 modules_resolved=3827 modules_excluded=4 required-ci: FAILED PHASE floor Not one failing row -- every witness in the corpus taken down by a comment's indentation. It also breaks the local corpus-wide parse gate several lanes use before pushing, which is how silent-bear-842 found it: their branch went rc=0 to rc=1 across a merge in which they changed nothing near it. The annotation is hoisted above the declaration unchanged in substance, and extended to record where the check does and does not live -- because that is the part worth keeping. --required-cited-symbol parses the whole corpus and passed this file CLEAN, twice, before I pushed #8976. Annotation grain is enforced in the floor's preparation, not in that census, so a green parse is not evidence that an annotation is placed legally. That is the second time in one night this lane shipped on a local check that could not see the defect class it was being trusted for; the first was a line break after `data X: Type =`, which cited-symbol DID catch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Runner slot membership becomes fleet-converge's third member family, plan and apply Slot standup had its own path: a provisioning plan nobody reviewed, no lease, no member-set fingerprint, no CAS against an observed baseline. Timers and caps already flowed through membership_reconcile and slots did not, so the fleet had two ways to change a host and only one of them was gated. A width change landing through the ungated one is unreviewed by construction. This routes slots through the same spine: - observed_slots joins host, timers, caps and spark in observed_baseline_text, the member-set fingerprint and the plan subject, so a slot roster that drifts between plan and apply now fails the CAS instead of applying a stale plan. - fleet_converge_slot_family resolves the deploy row ONCE and both the review lines and the apply script derive from it. Resolving twice would let a reviewer approve one membership while apply executed another, with both halves internally consistent and the bundle hash unable to see the difference. - slot ADD is emitted into apply.sh. Slot directories live under the runner user's own /opt/actions-runner, which is the principal converge already runs as -- unlike cap drop-ins under /etc/systemd, which stay plan-output-only because they belong to the deploy principal. - REMOVE and CHANGE emit nothing, because runner_slot_member_ownership returns Absent for every observed slot: no provenance signal yet distinguishes a gunbc-placed directory from a stranger's. Reconcile yields MemberRemovalRefused, the refusals are rendered for the reviewer, and nothing deletes a directory whose ownership was never established. - An unmodeled host refuses in both projections: a REFUSED line in plan.txt, and an exit 1 in apply.sh. Silence would be indistinguishable from "this host had nothing to do", which is the fabricated plausible output at the actuation seam. - It counts as one refusal, not zero. Zero reads as "nothing was refused on this axis", so the most severe state would have looked like the cleanest one. The CLI observes slots for real. RunnerSlotsUnobserved refuses the plan and the apply rather than being rendered as an empty roster -- an empty roster reconciles to ADD EVERY SLOT against a machine whose population nobody read. srv1 and srv2 gain deploy rows, which is what made the width authority actionable on half the fleet: gunbc_runner_slots_per_host could say srv1 carries twenty-one and there was no provisioning target to hand it to. Addresses were read over SSH, not assumed -- the lane started with .183/.184, which are the BMC addresses, and an ssh_target pointing at a management controller fails looking like an unreachable host rather than a wrong row. Two witnesses stop pinning six. They asserted a literal copied from the tree the day they were written, so the CPU-axis change turned them red without anything about the property they name having changed -- a change detector, not a check (DESIGN 5). What survives is the relation: the instance roster is exactly as long as the declared count, at any width. The fleet-wide row asserts an INEQUALITY against the memory budget rather than equality, because runner_count is the minimum over memory, CPU and disk while declared_runner_count_for is the memory axis alone -- since the CPU axis binds first on srv1, srv3 and srv4 the two legitimately differ, and equality would have been both wrong and vacuous. Also carries the section 4c annotation hoist from #8989, without which the floor refuses at preparation and no witness in this PR can execute. * The slot observation stops naming a host it cannot know Four defects the local required-floor caught, three mechanical and one that is the interesting kind. RunnerSlotsUnobserved carried a host field. observe_runner_slot_members_wet runs LocalExec against whatever machine it is on and has NO WAY TO NAME THAT MACHINE, so the only thing that field could ever hold at the observation site was a fabricated value -- and what I wrote was "" as HostIdentity. That is a fabrication at a REFUSAL seam, which is the worst place for one: the arm exists to say "I could not read this host", and it was answering with an identity it had invented. It did not typecheck, and the reason is worth recording rather than just fixing. HostIdentity is a branded NonEmptyStr, so the empty string is not one -- the carrier's own shape refused the fabrication that a reviewer would otherwise have had to catch by reading. Construction over validation, working as specified. The field is REMOVED rather than filled. The observation reports only the cause it actually established; the caller, which knows which host it is converging, supplies the identity. runner_slot_converge_verdict already took host as a parameter, so nothing downstream loses information. The module's own note claimed "RunnerSlotsUnobserved carries the host and the cause". That is now false, so it is corrected in place rather than left standing -- a note describing a field that no longer exists is the stale-citation class, and it would have been the first thing the next author trusted. Mechanical, same run: - trim was called unqualified and unimported; it is std.algebra trim(s:). - spark_serving_fleet_converge_apply_shell was a second call site of fleet_converge_apply_shell that the signature change missed. Threaded rather than defaulted -- passing [] there would have made the spark path silently emit no slot section while looking like it had one. * The fabric slot stops being a provisioning target desired_runner_slot_members mapped runner_instance_names straight through, so desired was 1..runner_count and nothing asked what any of those slots was FOR. gunbc.runner_slot_allocation authors srv3-06 as SlotServesFabricExecution, so the desired set proposed installing a GitHub Actions runner ON TOP OF THE FABRIC EXECUTION SLOT -- and reconcile would have classified it as an ordinary missing member, so apply would have done it with no refusal anywhere. That is the count-as-authority failure runner_slot_allocation exists to prevent, reintroduced one layer above it. Its own note records the first occurrence: purpose was derived by comparing index against count, so the fabric slot MOVED WITH THE WIDTH -- at width 7, srv3-07 would have become fabric and srv3-06 would have been silently handed back to Actions, relabelling a slot that may be running work. The carve was made an AUTHORED identity so a width change could not do that. This module consumed the width and ignored the carve, arriving at the same outcome by the other road. THE FILTER IS ON THE FABRIC ROLE, NOT ON COMMITTED MEMBERSHIP, and the first version of this fix got that wrong in a way worth recording. Filtering on runner_slot_membership asks the allocation authority whether each index falls within the host's committed width. That is the question runner_count already answered, and it returns ZERO desired members for any host the fleet model does not know. The reconcile fixtures in this module's witness file deliberately plant a synthetic deploy row (host_label "wfix") precisely so their expected counts are literals authored beside a known input rather than a restatement of the live fleet. Routing desired through the global width authority would have silently emptied those fixtures and turned four exact-count assertions GREEN BY VACUITY -- manufacturing the change-detector failure this lane spent the night removing, in the same session. So width still comes from the deploy row, which already derives it from gunbc_runner_slots_per_host, and this function removes exactly the identities that are spoken for. Two questions, two answers: how many slots does this host carry, and which of them are Actions runners. It works on INDICES rather than rendered names, so the fabric identity is compared as an identity and never by parsing a slot number back out of a string it was just formatted into. Two witnesses: srv3-06 is absent from the desired set while srv3-05 and srv3-07 are present, and the positive control that only the fabric host loses a target -- srv3 desires committed - 1, every other host desires its full committed width. The control matters because the first assertion alone is satisfied by a desired set that is simply too small. * The converge plan passed a release where an arch-bound artifact was required runner_slot_provision_plan takes an ActionsRunnerBinaryArtifact -- a release PLUS a published Linux arch -- and fleet_converge_slot_family handed it actions_runner_release_2_334_0, which is the bare ActionsRunnerRelease. Six witnesses errored with `no field 'arch' on type 'ActionsRunnerRelease'`, including four of the five converge witnesses this branch added. THE ARCH IS DERIVED, NOT AUTHORED. The fix is not a literal Aarch64 in a production module: the fleet's architecture is a fact of the cited CPU catalog, so it reads altra_max_m12830_catalog.architecture -- the same authority host_cpu_admitted_width already reads for core count. A host that is not an M128-30 therefore cannot silently inherit this arch, because the premise it rests on is named. actions_runner_binary_artifact_for_arch returns an option, since an arch with no published runner binary is a real state. That gets its own refusal arm -- SlotFamilyArtifactUnpublished -- rendered as a REFUSED line in plan.txt, counted as a refusal rather than as zero, and emitted as an exit 1 in apply.sh. It does not fall through to a default artifact, which would install some other architecture's binary. The two witness fixtures made the same mistake and are corrected the same way, matching the pattern the older witnesses in that file already used. WORTH RECORDING ABOUT HOW THIS SURFACED. A wrong argument type on a cross-module call produced NO diagnostic at parse and none at typecheck; it surfaced only as a runtime error inside the floor fold. That is the ordinary compiler floor DESIGN 4b requires -- applications bind in exact bijection, fields exist -- and it did not hold here. Filed as a separate finding; this commit only repairs the caller. It also vindicates the advice not to keep pushing over an in-flight run (fierce-hawk-734): this branch had two cancelled runs and zero completed ones, so the only thing supporting it was my argument that it should pass, and the argument was wrong. * A length is not a roster: distinctness, and an authored count that pins the indexing contract Both replacements in this file assert list_length(names) == runner_count and nothing else. A GENERATOR EMITTING ONE NAME N TIMES SATISFIES THAT PERFECTLY while deploying a single runner N ways -- N units all pointing at one slot directory, one registration name contending with itself. The count check cannot see it, because the count is right. Found by silent-bear-842, who independently rewrote these rows and asked what a length assertion does NOT cover before shipping rather than after. It is a hole in my rows, not a refinement of them. roster_names_are_distinct is a named function rather than an inlined clause because it belongs beside every length claim in this file, not just the one that prompted it. Applied to all three rosters. THE INDEXING CONTRACT IS BACK, ON GROUND THAT CANNOT MOVE. The rows this file used to carry asserted srv3-01 and srv3-06 by name: one-based, zero-padded, last index equals count. Those were real claims and I deleted them along with the width literals they were entangled with, because every index expectation was a function of a width the CPU axis then moved. Re-deriving them from the width would have made them vacuous -- the enumerator checked against itself. An AUTHORED runner_count breaks that circle. Three is a number the allocation authority never produced and never will, so the fixture pins one-basedness, zero-padding and the count-to-last-index relation without consulting the fleet at all. That is the planted-fixture side of the split ruled by swift-badger-524: a fixture authoring its own input SHOULD carry literals, and only a probe aimed at a live authority must derive its discriminator. MEASURED DISCRIMINATING RATHER THAN ASSUMED. silent-bear-842 planted a one-off in runner_instance_names (mapping over runner_count - 1) and both length rows returned false, so the chain from width to count to names crosses a real enumerator these rows do notice. That planted-defect measurement is the method I named as this class's next-rung trigger in my own census and then did not run. * Six expected-red rows were held green by a throw, not by the failure they assert The floor reported stale_quarantine=6 on this branch and 0 on main, and the counters name the whole move: known_red_runtime_errored goes 170 -> 164 exactly as known_red_now_passing goes 0 -> 6. On main all six materialized_secret witnesses throw `no such function: gunbc_ci_github_app` -- they never return a verdict at all, so their expected-red enrolment was satisfied by an exception rather than by the red it claims. The floor's own diagnostic already says this ("a claim that threw produced none"); nothing gates on it. The control decides the repair rather than the coincidence that this branch surfaced it. Evaluated directly against a detached origin/main worktree, witness_enabled_but_unprobed_refuses returns true ON MAIN TOO. The witness passes on both trees. What differs is only whether the fold's prepared subject carries the name: that file has ZERO import lines, so it reads gunbc_ci_github_app ambiently, and bare resolution is scoped to closure membership -- an outcome decided by what else got pulled in, never by this module. (gunbc_ci_github_app is a `data` row, not a `fn`, so "no such function" is a resolution failure wearing existence vocabulary.) So two halves, and they must not be separated. The import converts the pass from a closure accident into a module property; unenrolling alone would leave the six flapping the next time closure composition shifts, rather than failing honestly. Chunk cut by set difference, not by string removal: floor_expected_red_chunk_04 held THIRTEEN names, so unenrolling the chunk would have been wrong by seven. Exactly six removed, nothing added, Cons chain rebuilt from the survivors and brace balance verified (Cons=7, heads=7). What this does NOT claim: the local floor could not confirm it -- the run was OOM-killed at 11.3 GB RSS in the shared slice, so the verdict is CI's. The corpus parse gate is clean (0 diagnostics). Named and not fixed here: the expected-red roster cannot distinguish "failed as predicted" from "never produced a verdict". The floor counts the difference already and does not gate on it, which on main today is 164 enrolled rows whose stated red nobody has verified. --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… behind them refuses at twenty-one (#9001) The CPU-axis change (#8976) moved srv4 from 6 to 21 and left three prose rows asserting the old width. Prose cannot refuse, so nothing surfaced them -- they were found by review (fierce-hawk-734) rather than by any gate. gunbc_runner_slot_allocation_srv4_admission_note said "srv4 commits 6 slots at the memory axis" and that runner_count and the roster "both name 6 as the materialization target". Replaced, not annotated: two accounts of one width is what this module exists to prevent. srv4 memory-admits 29 and commits 21, the CPU axis binding, and neither number is authored -- runner_count derives from gunbc_runner_slots_per_host, so the note is a reading of one authoring. width_is_a_minimum_over_axes_note carried the same six in passing. Corrected. gunbc_runner_slot_width_ruling_note is deliberately NOT edited. It is labelled SUPERSEDED AND RETAINED AS METHOD with "PRIOR TEXT FOLLOWS UNEDITED", and its own header warns that reading on for current widths will mislead. Editing preserved historical text to agree with the present would destroy the only thing it is for. THE DISK ARITHMETIC IS THE PART THAT IS NOT COSMETIC. srv4_runner_count_disk_cap_note sized the per-slot charge at width 6 and read as reassurance. At width 21 the same charge is 21 x 40.96 GB, about 860 GB, against a Samsung 970 EVO 500GB -- so runner_width_disk_preflight is EXPECTED TO REFUSE srv4 at its committed width. The allocation axis cannot see this: disk is DiskWidthUnconstrained at allocation by the 2026-08-06 ruling. So the model commits a width the actuator is expected to stop, which is the fail-closed arm working and also means srv4 has a committed width it cannot currently reach. Recorded rather than resolved by lowering the commitment, because absorbing to a width the disk happens to permit is the fallback that ruling removed. AND THE CHARGE IS CIRCULAR, which is recorded at the authority rather than only in the rows citing it. runner_slot_disk_budget_per_slot_note claimed provenance: "derived from srv4_runner_count_disk_cap_note arithmetic (~38GB per slot at width 7 on 500GB class disk)". That is not a provenance -- disk size divided by the width of the day IS the derivation, so the charge was computed FROM a desired width and is now used to CONSTRAIN one. Nothing was measured. A live observation under an active reclaimer came in near 2.5 GB, two orders of magnitude below it. So a circular charge is refusing a derived width: two unmeasured numbers meeting, and neither the refusal nor a pass would be evidence about srv4's real disk. The refusal stays, because refusing on an unreliable charge is fail-closed and lowering the charge to make the width fit would be fitting the measurement to the answer. What is NOT claimed is that srv4 cannot hold 21 slots -- every ephemeral runner keeps a _work tree plus caches, so the true figure is not 2.5 GB either and the question is open in both directions. Each row carries the same dissolve-on: a measured per-slot disk series on srv4. Co-authored-by: Brian Searls <briansearls1@gmail.com>
…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>
…(carrier-exactness recut) (#8990) * Split the encode refusal domain out of the decode one, so the load classifier cannot name a state the loader cannot produce Recut item 1 of 7. This is a correctness fix, not carrier tidying. THE DEFECT. ClosureDocIncompleteWithoutAdmission is produced by encode_closure_document_checked and by nothing else -- no decode path reaches it. It nevertheless sat in ClosureDocRefusal, which closure_document_load_standing matches EXHAUSTIVELY. So the load classifier was obliged to assign a standing to a state loading cannot produce, and it answered LoadDocumentMalformed -- which standing_may_supersede_generation makes the ONE standing permitted to supersede a newer document. An encode-only state had a route to "may overwrite". This is a closed match over a dishonest domain: exhaustiveness is satisfied, the compiler is content, and the arm answers for something that cannot occur. Nothing was miswritten; the TYPE was wider than the operation's domain. THE FIX is not a new guard. ClosureDocEncodeRefusal now carries that arm and ClosureDocEncodeOutcome refers to it, so the classifier's parameter can no longer express the cause. The question stops being answerable rather than being answered correctly -- DESIGN section 4b's top rung, unrepresentable rather than validated. EVIDENCE, and the control is the half that makes it evidence. A temporary paired probe, both files staged so they reached the remote runner: probe closure_document_load_standing(cause: ClosureDocIncompleteWithoutAdmission{..}) -> error: type mismatch: expected 'Coproduct(ClosureDocRefusal)', got 'Coproduct(ClosureDocEncodeRefusal)' control closure_document_load_standing(cause: ClosureDocNotAnObject) -> typechecks PAST the same call; fails only at the ProcessExit boundary, which is the host's return-type rule, not a typecheck Without the control the probe's failure would have been satisfied by any breakage at all -- a typo, a bad import, a wrong module name. The control proves the module loaded, the imports resolved and the call typechecked, so what the probe refuses is the domain split and nothing else. Both probe files are deleted in this commit: they declare no test fn and must never enrol, since a file designed to fail compilation would red the floor for everyone. Runtime suites green after the split: commit_closure_witness_main exit=0, load_standing_witness_main exit=0. ONE CONSEQUENCE STATED RATHER THAN HIDDEN. cause_is_incomplete_without_admission is now total by construction -- ClosureDocEncodeRefusal has one arm, so the match can only answer true, and by this stack's own standard that is a decoration. It is kept, because it is the correct residue of a climb: the check did not get stronger, it became unnecessary, and section 4b(4) keeps the evidence enrolled while the obsoleted discrimination goes. What replaced it is a compile-time property no Bool-returning witness can express, which is why the probe above is recorded here rather than enrolled as a claim. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Bind the partial-closure admission to its subject, so a token for A cannot authorize encoding B Recut item 7, and the one the design thread called the highest-priority correction. This closes the authority-substitution hole #8965 declared as honest rung debt rather than fixed. THE DEFECT. PartialClosureAdmission { reason } carried no subject. The encoder took a closure PLUS an optional admission and checked only that one was PRESENT: admit closure A -> token T encode closure B with T -> ACCEPTED sole_constructor did not prevent it: .dag has no module privacy, so it blocks the record literal while the public mint stays freely callable. Nor could the existing mutation have caught it -- deleting the check proves the check is READ, which a bearer token satisfies perfectly. That is why this sat at mitigatable with the rung declared instead of claimed. THE REPAIR IS A SHAPE, NOT A CHECK. AdmittedPartialCommitClosure holds the closure it admits, and encode_admitted_partial_closure_document takes ONLY that carrier. There is no second closure to disagree with it, so "a token for A used on B" is not refused at runtime -- it has no spelling. The partial encode entry performs no validation because nothing is left to validate. `unresolved` is DERIVED at the mint from the closure it is given. A caller-supplied population would reintroduce the same substitution one field down: an admission truthfully about A, carrying B's missing objects. DISCRIMINATOR, and it had to be re-derived rather than copied. The thread specified "admission for A used with B -> refuses or cannot be constructed", but after the reshape the mismatch CANNOT BE PASSED -- one parameter, closure is a field -- so a probe passing a second closure would only be an arity error. The single remaining forgery route is hand-assembling the carrier: probe AdmittedPartialCommitClosure { closure: <never minted>, .. } -> error: sole_constructor type 'AdmittedPartialCommitClosure' cannot be constructed outside its defining module (exit 1) TWO CLAIMS ADDED, both executing: an_admission_names_the_objects_it_admits_as_missing -- the population is the closure's own, not a caller's assertion the_mint_refuses_a_complete_closure -- admitting a partial write for something with nothing missing is a category error, and this is what keeps the mint honest about deriving rather than trusting Suites: commit_closure_witness_main exit=0 (12 claims), load_standing_witness_main exit=0 (6 claims). THREE DEVIATIONS FROM THE PROPOSED SHAPE, each deliberate. NO NonEmptyList. The corpus has none, and minting one for a single field would grow net concepts to buy a guarantee the mint's refusal already provides. So the SUBJECT BINDING is structural while the emptiness exclusion stays mitigatable -- `unresolved: List` can represent an empty admitted population even though this mint cannot produce one. Next-rung trigger: a NonEmptyList authority earning its place from more than one consumer. NO one-member refusal coproduct. `type X = OnlyArm` does not declare a nullary variant, it reads as a type alias and fails to resolve. The refusal is an arm of PartialClosureAdmissionOutcome instead; a second genuine refusal joins that coproduct and every match fails to compile at the match, which is what the nesting was for. TWO ENCODE ENTRIES rather than one with an optional token: encode_complete_closure_document refuses anything uncontained; encode_admitted_partial_closure_document is total because the mint settled it. COVERAGE OWED, NOT CLAIMED. scm_commit_closure_json_v2_witness_test.dag has no ProcessExit driver, so the three call sites retargeted there are typechecked but NOT executed. The floor is the only thing that runs them and it is currently refusing for an inherited reason, so that execution is owed once main reopens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Narrow the repository encode refusal to the one cause encoding can produce, and give that arm its first witness Recut item 6. Same dishonest-domain class as item 1, one layer up. THE DEFECT. RepositoryEncodeClosureDocRefusal wrapped the WHOLE ClosureDocRefusal decode population -- fifteen causes -- while encode_repository_checked produces exactly ONE of them (ClosureDocEdgeTargetUnresolved) from exactly one place. A consumer matching this arm had to handle format-tag and unknown-connective causes that no encode path can raise, and a reader could not tell from the type which were real. The type answered for a domain it does not own. It also round-tripped an identity through text: the arm carried a rendered key while its two sibling arms carry ObjectId directly. An identity left as a string is one nobody can resolve back. Both are fixed by RepositoryEncodeUncontainedTarget { target: ObjectId }, with first_uncontained_target returning the domain type instead of a key. THE ARM HAD NO WITNESS, AND THE GREEN SUITE IS HOW I ALMOST MISSED IT. All three suites passed after the change. But the encode-cause helper enumerates three tags and the claims asserted only two -- "commit_root" and "checked_out". Nothing drove "uncontained_target". The arm was REACHABLE (a grafted store whose root's children were never copied produces it) and merely unoccupied, so changing its payload type would have compiled green with nothing establishing that the identity survives. Reachable-and-empty is a quiet guard, not a dead one: the answer is to occupy it. scm_env_an_uncontained_target_refuses_to_encode_and_names_it now drives it, and asserts TWO things on purpose. The tag alone would pass whether the arm carried a resolvable ObjectId or a stringified one, so it also checks the CARRIED target against the store's own uncontained population -- which is the property the type change was for. Both halves measured rather than argued: 24 claims, membership vs the grafted store exit=0 membership vs the COMPLETE store (empty set) exit=1 The second is what proves the identity check is not vacuous. I could have reasoned that a fold over an empty list returns false; that is the substitution this stack keeps catching, so it was run instead. A DIRECT DRIVER IS ADDED TO THIS WITNESS, and it is scaffold with a stated end. The floor discovers `test fn` itself and never calls it; it exists because the floor is currently refusing before subject preparation for a reason this branch does not own, and these 24 claims otherwise had NO execution path -- this module's change would have been typechecked and never run. `gunbc run --function` cannot drive a Bool-returning `test fn`. Delete it once the floor executes these identities again; the comment on it says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Say only what the evidence establishes: an uncontained target, not the first Two corrections from design review of the item-6 landing. Both are the class this stack keeps producing -- a name or a tool promising more than anything verifies -- so they are fixed rather than argued. (1) THE HELPERS PROMISED AN ORDERING NOTHING CHECKS. first_uncontained_target and first_uncontained_key say FIRST. The witness establishes MEMBERSHIP: the carried target belongs to the store's uncontained population. With a single uncontained object every member is also the first, so the observation cannot distinguish "actually first" from "some legitimate member" -- the name was the stronger claim and it had no discriminator. Renamed to an_uncontained_target / an_uncontained_key. The refusal needs one ACTIONABLE EXAMPLE and no consumer depends on which; that is the real contract, so the name now states it. Deliberately NOT fixed by adding a two-target ordering fixture. Order is not an interface fact here, and pinning it would freeze an incidental traversal order that a later keyed or canonical representation of uncontained_targets should not have to preserve. This is the opposite decision from two_uncontained_children_are_named_in_order, where the reverse IS load-bearing because positions are the encoding -- the difference is whether anything downstream depends on the order, not whether an order exists. (2) THE DRIVER'S OWN COVERAGE WAS UNGUARDED. A hand-sequenced ProcessExit driver that omits a claim turns "driver green" into a subset run that reads as a full pass -- the nothing-ran-versus-nothing-failed trap, inside the tool added to avoid it. The invariant is that every declared `test fn` appears exactly once in its driver. Measured: scm_commit_closure_witness_test declared=13 dispatched=13 scm_load_standing_witness_test declared=6 dispatched=6 scm_repository_envelope_witness_test declared=24 dispatched=24 And the check discriminates -- planting a claim with no driver entry gives declared=7 dispatched=6 -- verified rather than assumed. NO GATE WAS COMMITTED FOR IT, and that is a decision rather than an omission. Durable enforcement machinery for an artifact with a scheduled deletion is scaffold protecting scaffold; the real dissolution is the floor executing these identities, which removes the driver and the invariant together. The rung is recorded on the driver as MITIGATABLE, enforced by hand. Suites after both changes: envelope_witness_main exit=0 (24), commit_closure_witness_main exit=0 (13), load_standing_witness_main exit=0 (6). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Correct the driver's coverage invariant: an identity set, not a count The invariant recorded on the repository witness driver was declared == dispatched. That is the weaker check it sounds like, and recording it as the guarantee made this comment the fourth instance in this stack of prose asserting a wall stronger than the mechanism behind it -- this time inside the artifact added to prevent exactly that failure. WHAT CARDINALITY CANNOT SEE: declared A B C D dispatched A B C C Both populations are four and D never executes. Demonstrated rather than argued, by duplicating one dispatch and dropping another on a sibling witness: count check declared=6 dispatched=6 -> PASS reality an_honest_collision_is_its_own_standing_and_never_supersedes never ran The invariant is now exact SET EQUALITY of declared `test fn` names against dispatched reason strings, plus uniqueness in both populations. Measured across every witness carrying a driver: scm_commit_closure_witness_test 13 identities, sets equal, no dups scm_load_standing_witness_test 6 identities, sets equal, no dups scm_repository_envelope_witness_test 24 identities, sets equal, no dups and falsified by the planted case above, which the previous check passed. NO GATE IS COMMITTED, unchanged from before and for the same reason: durable enforcement machinery for an artifact with a scheduled deletion is scaffold protecting scaffold. The driver's dissolution trigger stands -- the required floor executing these identities removes the driver and the invariant together. What changed is only that the recorded invariant now matches the check that was actually run. Design review also resolved the fork left open in the previous commit, against the premise I offered: targeted mutation runs DO stay valuable after the floor returns, but the answer is to generate an ephemeral driver from the current roster at mutation time, not to keep a hand-maintained one. Two durable rosters -- floor discovery and ProcessExit dispatch -- would be two authorities for which claims belong to a witness, and the drift is predictable (a new test never dispatched, a renamed test leaving a stale entry). That changes nothing in the tree today; it settles what happens to this driver later. envelope_witness_main exit=0 (24 claims) after the edit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * v1 inference: generic instantiation must reach record-literal field expectations (>=2 seams) — plus the adjacent below-floor fail-open where a generic field admits the wrong type silently (#8922) * A generic record literal admits the wrong field type silently at six seams: locate the fail-open, and record two repairs that do NOT close it DESIGN 4b names "values inhabit declared types" as the ordinary compiler floor. A record literal of a GENERIC type does not hold it: measured on eleven single-module probe roots, a wrongly-typed field value is accepted AND EMITTED at six positions -- fn return, let annotation, record field, list element, direct-call argument, and a module-scope data annotation -- while the non-generic control refuses with a located mismatch and the conforming generic control compiles clean. The field PRESENCE axis is unaffected (a generic literal missing a required field still refuses), which rules out "generic declarations are not processed" and confines the class to the field TYPE axis. Mechanism, by execution rather than by reading: the instantiation does reach the literal and the substitution is keyed correctly on "T", but the declaration's field type node carries no name to key on, so the parameter is never substituted and the expectation reaching the judgment is a NAMELESS node -- whereupon kernel_value_declared_type_mismatch returns false on formal_name == "". A second, independent fail-open sits beside it: the substitution value is read with resolved_type, whose Absent arm is the equally nameless error_type. Two repairs were built and run against the full arm table and moved NOTHING; both are recorded because they are the cost of the next attempt. What is still open is where the type-parameter reference loses its name, which is a modelling question in a stage DESIGN names load-bearing -- so no code changes here, and the probe states the exact next question rather than leaving it to be re-derived. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Correct the mechanism: substitution is innocent, the class is any type declared WITH PARAMETERS, and the paired nonzero makes every zero a reading The first revision of this probe named the type-parameter reference losing its name as the cause. Two no-build discriminators falsify that, and a knowingly stale mechanism claim in a finding other lanes plan against is premise contamination -- so the doc is rewritten in one pass rather than annotated. WHAT CHANGED. A generic declaration whose parameter is UNUSED and whose field is a plain kernel type still fails open, so the trigger is that the declaration carries type parameters at all, not that a field mentions one. And forcing the instantiation to bail out with a wrong arity brings the field judgment back on the SAME declaration -- so record_lit_instantiated_fields does not fail to add an expectation, it preempts a working one. Instrumentation then showed authored_fte="" BEFORE substitution: substitution faithfully returns the nameless node it was given, and the declaration reached by the ident-keyed lookup is already identity-stripped where the name-keyed lookup's is not. PAIRED NONZERO. Every fail-open arm now carries a matched non-generic twin at the same seam, same run, same binary: six zeros, six reds. Plus an undeclared-name arm proving the generic module is compiled and its body judged. The twin design also rules out "that seam is unchecked for any type", which a bare perturbation would have left open. FOUR DEAD ENDS, ONE CAUSE, established by reading the construction site rather than by another build: ResolvedModule.module is the raw parsed node, build_type_env folds THOSE items into the bindings, and resolve_item_types runs later feeding resolved_item -- never the binding. ResolvedModule means import-resolved, not type-resolved. Also recorded: resolve_field is correct and has zero callers while its wired sibling resolve_field_init does not, which makes it an incomplete migration rather than dead scaffolding -- and a cleanup sweep deleting it would leave the lossy hand-rolled copy as the only authority. Claim staked on the PR. Still no code change: the remaining question is an ident-versus-intern address-space read, and a fifth blind repair would repeat the pattern the first four established. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The class is two rows, and the corpus reds the closed one: report it rather than narrow the wall (#8901) * The 8 and the 4 have different dispositions: the census rows encode the old answer (#8901) * LexMatchThunk is not generic, so it is none of the three rows: a fourth mechanism, bounded by three baseline arms (#8901) * Withdrawn: the generic carrier is the algebra, not the thunk -- one-variable pair puts the tokenize row in row (b) (#8901) * WIP: (a) fork dissolution — field_declared_type_node, authority + mirror * (a) mirror half restored: field_declared_type_node in v1_compiler_infer.rs * Drop stray backup file * (a) fork dissolution + measured (c) exposure; placeholder-carrier hypothesis refuted by execution * (c) an UNESTABLISHED return type must not become a lambda's body expectation * Install the emitted mirror for v1_compiler_infer.rs (regen candidate, not hand-tuned) * Delete four dissolved frontier rows (observed=0), fix two ContentHash construction defects the wall caught, admit list literals at FreeMonoid * (c) sibling: an UNESTABLISHED substituted param type must not bind a lambda parameter as an error type * Install emitted mirror for v1_compiler_emit_rust.rs (clone elision from the established-type fix) * BISECT ARM (not a landing state): revert (a) field_substitution_carrier, keep (c) Diagnostic push on a draft PR to separate (a) from (c) by execution. The floor caught 9 claims that pass on main and fail on this branch, every one of them against an independently authored oracle, so main's pass was not vacuous and this branch computes wrong values. Local floor OOMs (137) in a session container, so CI is the only instrument at whole-corpus scope. This arm reverts (a) only. It deliberately re-opens the row-(a) defect -- the kb2 RED will stop refusing -- and is NOT proposed for merge. Read the floor line, not the arms. Predicts: if (a) is the culprit, failed goes 9 -> 0 and the 6 samsung_dram stale-quarantine rows stay unmasked. If (c) is, failed stays 9. * Revert "BISECT ARM (not a landing state): revert (a) field_substitution_carrier, keep (c)" This reverts commit 18b5ddc6261acd3c5398a5fc5429fd9ea2e48f63. --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Transport binding spine: one target-neutral semantic binding for all four transports, then Filesystem bindings + Rust renderer to restore the 03_ingest board (#8957) * WIP: Bind the file-transport realization handler AND migrate rest/shell/local * WIP: Transport binding spine: one target-neutral semantic binding for all fou * Regenerate the stage0 mirror for the transport binding spine review 54885 and deep-ant-102 both found the same thing: the de-fork existed in the .dag authority and not in the mirror v1 actually runs from, which is specification-without-execution in its textbook form -- the exact failure this cut exists to close. Produced by claim_executor --required-regen; the candidate tree drifted in exactly the four emit files this change re-typed. Also moves an annotation to module-item grain (§4c refused it at body grain) and records the fabricated-empty-base_url marker dependency beside classify_transport: kind is discriminated by marker-field PRESENCE, so making base_url refusable deletes the rest tag and reclassifies every rest transport as local. No binding arm requires a base_url value; that repair owes an explicit kind tag in the same change and is deliberately not taken here. * Drop the dead classify_transport import from the rust emitter Zero call sites since the de-fork: the rust backend consumes a BoundOperation and no longer classifies anything. A live import of the classifier is what a reader grepping 'does the target still classify?' finds first, so it reads as the fork surviving. Found in re-review by smart-ram-730. --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> * Classify rustc mechanisms across diagnostic codes (#8978) * Classify rustc mechanisms across diagnostic codes * Record cross-code classifier provenance * Bind mechanism population to its measured ref --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> * Locate the LexMatchThunk apply receiver-type loss (#8983) * Locate LexMatchThunk apply receiver type loss * Record the bounded pre-descent ordering null * Reclassify the apply root as a representation gap --------- Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> * Refuse per-code board shares for emitter roots (#8979) * Refuse per-code board shares for emitter roots * Audit shared-types membership authority consumers --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Brian Searls <11205878+briansrls@users.noreply.github.com> * Make impossible fn-field derives unselectable through aliases (#8985) Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> * Bind mock-totality witnesses to published corpora (#9006) Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> * The .dag parser fabricated an empty path and silently ate unknown fields: five refusal arms, one live specimen repaired (#8949) * The parser fabricated an empty path and silently ate unknown fields: five refusal arms, one live specimen repaired `parse_file_fields` substituted an empty string literal when `path:` was omitted, so `transport file { }` and `transport file { path: "" }` produced byte-identical nodes. That is not merely an unchecked state: `is_file_transport` is DEFINED as "carries a base_path", so the fabrication made the absence unobservable to every downstream consumer -- an emit-side "declares no path" refusal is permanently green by construction. The rust realization duly emitted a filesystem write against "" with zero diagnostics. The refusal belongs at parse, where an absent path is decidable from the tokens alone, and that is where it now sits. CENSUS of every parser field that defaults rather than refuses (132 `Absent =>` arms in 02_parse.dag; all but these are legitimate token-absence handling): * parse_file_fields base_path -- omitted path fabricated as "". DEFECT, refused here. * parse_rest_fields base_url -- omitted url fabricated as "". NOT a defect: omitting `url:` is the norm (the base comes from the service config) and an empty base plus a full-URL path template is the authored absolute-form idiom recorded in extdeps.transports.rest. It is a state-space conflation with its own lane, not a refusal decidable from the tokens. * parse_config_fields endpoint -- omitted endpoint fabricated as "". NOT a defect: `config { }` is legal and shell services have no endpoint at all. * four `_` fallthrough arms (config, rest, shell, file) -- an unrecognized field was parsed and THROWN AWAY. Same fail-open reflex one layer over, and it had a live specimen: this repo authored `transport file { op: READ, path: ... }` in extdeps.cloud.gcp and the `op: READ` was swallowed whole, never resolved, never reported. All four refuse; the gcp site is repaired (read is the default verb, so the semantics are unchanged). MEASURED, not assumed. The corpus-wide parse gate against a binary rebuilt from the regenerated mirror indexes 3880 modules from 2 source roots, exit 0 -- so outside the one gcp.dag site nothing in the corpus was relying on a dropped field or an omitted file path. Regen produced exactly one drifted file, v1_compiler_parse.rs, across the 132-file mirror. EVIDENCE, enrolled: dag/test/claim/transport_field_refusal_witness_test.dag carries three positive controls and five discriminating REDs, all 8 PASS under claim_batch. The controls reach compile.emit; the five reds stop at compile.analyses, so the refusal is real and the harness is discriminating rather than false-for-everything. Per DESIGN §4b(4) these stay enrolled as the evidence the rung holds, not deleted with the machinery they replaced. v1 admission: this serves the v2 self-host program -- the file-transport realization lane (#8929) is exactly the consumer whose emitted write the fabrication corrupted. Semantics stay frozen; this is a defect repair, not growth. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Name the stage in the assertion, not just the outcome: the pathless-path RED could not tell parse from emission Folding in a finding from #8937 (sleek-fox-685, relayed by deep-ant-102) that is correct and that this PR's own oracle could not have caught. Every RED here asserted `!compiles(source)` -- ONE BOOLEAN, which is green whether PARSE refused the declaration or the parser fabricated "" and the EMISSION wall caught it downstream. Those are exactly the two states this change separates, so the row watching it could not see the thing it was watching: revert the parse arm, let the emitter catch the pathless case, and every `!compiles` red in this file stays green. Three rows, not the one that was asked for, because a single row could pass for the wrong reason: * w_red_pathless_file_transport_refuses_at_parse_not_emission asserts the parse class POSITIVELY (blocking `ParseError` >= 1) rather than by excluding the emission class. Naming a stage by exclusion still passes if some third, unrelated class is what refused. * w_control_unmodeled_verb_refuses_at_emission_not_parse runs the same two counters the other way, over a source the EMISSION wall refuses. Without it, `parse_blocking_count >= 1` is satisfiable by a counter that is nonzero for everything and `not_modeled == 0` by one that is always zero. * w_control_valid_file_transport_is_clean_at_both_stages reads zero from both on a clean source. Both counters answer -1 on CensusNotRunnable, so could-not-measure fails the `>= 1` AND the `== 0` assertions instead of silently satisfying one (DESIGN §5: top-as-ignorance is not top-as-answer). MEASURED: 11/11 PASS under claim_batch on the merged tree. The open question before running was whether a parse refusal reaches compile_dag_diagnostic_census as an observed blocking ParseError row or as CensusNotRunnable -- if the latter, the -1 arm would have failed the row for a reason unrelated to the wall. It is observed, so the stage assertion is real rather than accidentally green. ALSO: the discriminator fact recorded where the next author will hit it, as a `//` annotation on v1.compiler.core is_rest_transport. Transport KIND is discriminated by marker-property PRESENCE, so `rest_transport_node`'s always-written base_url -- filled from the "" that parse substitutes when `url:` is omitted, which is the NORM -- is load-bearing structure, not a lazy default: removing it reclassifies every rest transport in the corpus as `local`. It is also why is_local_transport is defined negatively. Regen confirms the annotation adds no mirror drift. Merged origin/main. Regen against the merged tree drifts ONE file, v1_compiler_emit_rust.rs, which is main's own red (#8691 landed without its second regen pass) and is #8953's to repair -- not regenerated here, because installing another lane's fix from this tree would give the corpus two producers for one file. v1_compiler_parse.rs is byte-identical to a fresh emit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * An annotation cannot fail: guard the kind-discrimination invariant the 00_core comment only described deep-ant-102 measured what I did not: ZERO witness rows asserted the classification my annotation documents. DESIGN §4c is explicit -- an annotation is never evidence a machine claim holds, because no Accepted program can read one. Prose is the right home for the RATIONALE and cannot be the guard for the INVARIANT, and a comment that reads as coverage to the next reader is worse than none. That reading is not hypothetical: a review of this very PR called the comment "a nice defense against a future 'consistency' edit". It is not a defense. These two rows are. w_red_rest_transport_classifies_as_rest_not_local w_control_shell_transport_emits_no_rest_client Asserted through EMISSION SHAPE rather than by calling is_rest_transport, and that is a reachability fact rather than a preference: CI's source roots are `dag` and `src/v2`, so v1.compiler.core is not in the witness pool and the predicate cannot be named from a witness at all. The consequence is the better subject anyway -- it runs the real pipeline instead of the predicate in isolation. The control supplies the other answer so the first row is not satisfied by an oracle that matches everything, which is the same defect the stage counters had before their inverse row. MUTATION-TESTED RATHER THAN ASSERTED, because "delete the fabricated base_url and this row fails" was a claim about a RED I had not executed. Scratch build with the always-written url_field removed from rest_transport_node -- the exact "tidy the lazy default" edit the annotation warns against: FAIL w_red_rest_transport_classifies_as_rest_not_local PASS w_control_shell_transport_emits_no_rest_client The mutation reds the specific claim and not the harness. Reverted; `git diff` on the mirror is empty, so nothing from the scratch build is in this commit. 13/13 PASS on the restored tree. Kept here rather than routed to #8954's roster witness: this PR introduces the annotation, so it should land with its guard rather than ship prose-only coverage and depend on another lane to close it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> * End an unbraced arm body at a QUALIFIED pattern, not only a bare one (#8999) parse_match_arm_stmts consumes statements until looks_like_arm_start reports that the next tokens open a new arm. That predicate recognised a bare `_` and an UPPERCASE-start leaf, and nothing else. A namespace- qualified pattern begins with its lowercase module head, so it answered false: the body kept consuming, swallowed the next arm's pattern as one more statement, and the parse died on the FatArrow that followed. The reported span is that ARROW -- several lines below the arm that actually ended -- which is why this had to be bisected rather than read off the diagnostic. Four separate reproductions of the neighbouring shapes all parsed before the real one was found. MINIMAL REPRODUCTION, every clause load-bearing: Kind { f: _ } => let a = "p" <- unbraced arm body containing a `let` a mod.path.Other => "d" <- next pattern is DOTTED Drop the `let` and the body is a single expression that never enters the statement loop. Make the following pattern `_` or an uppercase leaf and the predicate already answered true. Both are needed. MEASURED. On the namespace-cut branch, where qualifying every pattern turns this from rare into ordinary, exactly one corpus file of 3875 reaches it: src/v1/05_emit.dag. That file is invalid under the parser its own branch carries -- it survives there only because the built binary predates its own committed mirror, so the defect is latent and would surface at that branch's first successful rebuild. This is therefore a grammar gap the cut made REACHABLE, not an accommodation for it, and it fails loudly at preparation rather than silently downstream. DISCRIMINATING RED, BY EXECUTION: the witness returns false against a parser with this one decision reverted to `false`, and true with it. Both runs were performed. ZERO-DRIFT, STRUCTURALLY: the new scan runs only where the old predicate already answered false, and it requires the TERMINAL segment to be uppercase with the arrow following the path or its brace group -- so no previously-accepted parse changes, and a lowercase dotted expression ending a body is unaffected. An expression statement genuinely followed by a FatArrow was never a legal parse. Receipt: required-regen over the 133-module subject reports first_generation_equal=true with only this repair's own mirror changed. 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> * Bind a qualified pattern head from the scrutinee, as the bare spelling already does (#9004) * Bind a qualified pattern head from the scrutinee, as the bare spelling already does Two spellings of one pattern name the same declaration, so they must bind the same node. lookup_variant_in_type forks on whether the head contains a dot: the bare branch answers from the SCRUTINEE, which carries the instantiation; the dotted branch answered from the SYMBOL INDEX, which returns the coproduct's DECLARATION. So the payload bound to the declaration's type PARAMETER instead of the scrutinee's type ARGUMENT, and every field read off it reported "no field 'root' on type 'T'" -- measured, not inferred, on a two-function probe whose only difference is the spelling of the head. Admission is unchanged: the index lookup still runs first and still decides whether the head names a variant of this coproduct at all. Only the bound node's source changes once admission succeeds, and the fallback arm reproduces the previous answer exactly. RECEIPTS. Discriminating RED proven in both directions on the same corpus: on the pre-fix binary the qualified arm returns false with the diagnostic above and the bare control returns true; after regen and rebuild both return true. One generated file drifted -- v1_compiler_infer_patterns.rs, this repair -- and the second pass reports first_generation_equal=true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * De-confound the witness pair: the arms differed in imports, not only in head spelling The floor reported the qualified arm at 58800ms CPU against a 5000ms budget with 1.21GB RSS growth while the bare arm passed under budget, and I read that as a cost of the qualified pattern head. It is not yet evidence of that. The qualified probe imported two names and the bare probe imported four, so the arms could differ in source-closure construction, import binding, symbol-index use and cache temperature as well as in the spelling under test. The pair was a controlled experiment for the SEMANTIC discriminator and not for the cost one -- a control must name its adversary, and cost was an adversary these arms never excluded. Both probes now import all four names, leaving the two pattern heads as the only difference. The semantic RED is unchanged and was re-proven in both directions after the edit, against binaries built from the pre-fix and post-fix mirrors: pre-fix qualified=false bare=true post-fix qualified=true bare=true No generated file changes; the seed is untouched by this commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Move the witness to the census grain: it was measuring emission for a claim about resolution The subject is a BINDING fact and a binding fact is decided at typecheck. The witness asked it through compile_dag_rust_emit_check, which parses, resolves, typechecks, EMITS RUST, and then -- per the compiler's own compile_dag_diagnostic_census_row_note -- collapses the whole result to a Bool, discarding which judgment fired. The enrolled claim duly cost 58579ms CPU against a 5000ms budget with 1.21GB RSS growth while its bare control passed under budget. compile_dag_diagnostic_census reports the causal judgment directly as typed rows. That makes this witness narrower in subject, MORE discriminating -- it names the diagnostic instead of collapsing to false -- and cheaper for a principled reason rather than a convenient one: emission is downstream of the fact being tested, so removing it removes work, not evidence. CensusNotRunnable is a failure carrying its own cause and is never the expected red. Could-not-measure and measured-nothing are different states and only one of them is evidence. MEASURED, one fixture, both probe sources carrying identical imports so the only difference is the two pattern heads: pre-fix qualified OBSERVED[1] InternalError | no field 'root' on type 'T' | blocking=true | n=1 (enrolled fn returns false) pre-fix bare OBSERVED[0] post-fix qualified OBSERVED[0] (enrolled fn returns true) post-fix bare OBSERVED[0] (enrolled fn returns true) THE COST OBSERVATION IS NOT REPAIRED BY THIS CHANGE AND IS NOT CLAIMED TO BE. It is carried forward in the pull request body with its two ruled-out causes, its unattributed owners, and its next discriminator. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * Three prose rows still said srv4 commits six, and the disk arithmetic behind them refuses at twenty-one (#9001) The CPU-axis change (#8976) moved srv4 from 6 to 21 and left three prose rows asserting the old width. Prose cannot refuse, so nothing surfaced them -- they were found by review (fierce-hawk-734) rather than by any gate. gunbc_runner_slot_allocation_srv4_admission_note said "srv4 commits 6 slots at the memory axis" and that runner_count and the roster "both name 6 as the materialization target". Replaced, not annotated: two accounts of one width is what this module exists to prevent. srv4 memory-admits 29 and commits 21, the CPU axis binding, and neither number is authored -- runner_count derives from gunbc_runner_slots_per_host, so the note is a reading of one authoring. width_is_a_minimum_over_axes_note carried the same six in passing. Corrected. gunbc_runner_slot_width_ruling_note is deliberately NOT edited. It is labelled SUPERSEDED AND RETAINED AS METHOD with "PRIOR TEXT FOLLOWS UNEDITED", and its own header warns that reading on for current widths will mislead. Editing preserved historical text to agree with the present would destroy the only thing it is for. THE DISK ARITHMETIC IS THE PART THAT IS NOT COSMETIC. srv4_runner_count_disk_cap_note sized the per-slot charge at width 6 and read as reassurance. At width 21 the same charge is 21 x 40.96 GB, about 860 GB, against a Samsung 970 EVO 500GB -- so runner_width_disk_preflight is EXPECTED TO REFUSE srv4 at its committed width. The allocation axis cannot see this: disk is DiskWidthUnconstrained at allocation by the 2026-08-06 ruling. So the model commits a width the actuator is expected to stop, which is the fail-closed arm working and also means srv4 has a committed width it cannot currently reach. Recorded rather than resolved by lowering the commitment, because absorbing to a width the disk happens to permit is the fallback that ruling removed. AND THE CHARGE IS CIRCULAR, which is recorded at the authority rather than only in the rows citing it. runner_slot_disk_budget_per_slot_note claimed provenance: "derived from srv4_runner_count_disk_cap_note arithmetic (~38GB per slot at width 7 on 500GB class disk)". That is not a provenance -- disk size divided by the width of the day IS the derivation, so the charge was computed FROM a desired width and is now used to CONSTRAIN one. Nothing was measured. A live observation under an active reclaimer came in near 2.5 GB, two orders of magnitude below it. So a circular charge is refusing a derived width: two unmeasured numbers meeting, and neither the refusal nor a pass would be evidence about srv4's real disk. The refusal stays, because refusing on an unreliable charge is fail-closed and lowering the charge to make the width fit would be fitting the measurement to the answer. What is NOT claimed is that srv4 cannot hold 21 slots -- every ephemeral runner keeps a _work tree plus caches, so the true figure is not 2.5 GB either and the question is open in both directions. Each row carries the same dissolve-on: a measured per-slot disk series on srv4. Co-authored-by: Brian Searls <briansearls1@gmail.com> * Make the hand-built argv unwritable at the call site, and land the positive examples that show what to write instead (#8919) * Make the hand-built argv unwritable: seal ArgvCommand, and land the builders that show what to write instead extdeps.exec.command ArgvCommand becomes sole_constructor with one admitted mint (argv_command), caller-sealed by name to typed builders homed in each tool's own extdeps module beside its cited upstream authority. All 58 record constructions on main are converted in this change, because a partial seal is a dual-authority interval rather than a weaker seal (DESIGN section 3). The carrier splits program from arguments, which makes the empty argv unrepresentable and dissolves two runtime refusal arms in gunbc.command_runner (DESIGN section 4b: structurally impossible over mechanically preventable). The seal surfaced nine hand-spelled argvs that were never record literals and four List<String> laundering seams; all are converted or closed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * Program spelling is an observation claim: one authority paragraph, one line per row still-seal-394's correction: the rule is not 'PATH-resolved is the safe default'. An absolute path is a claim that this repository observed the target's filesystem, and it is the STRONGER form wherever that claim is true -- a sudoers rule can name it and a preflight can test for it. PATH resolution is correct only where the host is unobserved. The reasoning, the climb criterion and the roadmap_dashboard_instance_apply counter-example live once at extdeps.exec.command program_spelling_is_an_observation_claim; the ten rows it governs carry one line pointing at it instead of ten copies of the paragraph. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * Five more hand-spelled argvs the seal surfaced: /proc reads and the fd walk The refusal census does not stop at the record literal. build_cache_endpoint_observe still spelled four argvs as bare word lists (two cat, one stat -c %U, one readlink) and host_effect_realize spelled the /proc fd walk as a fifth; all five now derive from cited authorities, with find's -lname glob fact homed in a new extdeps.tools.findutils beside the flags rather than in the caller that met it. stat's name row gains the -- end-of-options guard its sibling rows already carried: one module was answering one question two ways, and these operands come from observed host state, which is exactly where a leading-dash path appears. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * Cast the two NonEmptyStr literals at their call sites Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * Admit two builders the wall caught, and measure the NonEmptyStr refinement the climb rests on The seal refused readlink_command and the findutils fd-walk builder -- two admit-list omissions of mine, caught by execution rather than by review. The empty-argv climb claims program: NonEmptyStr makes an empty program unwritable. NonEmptyStr is a refinement, not a mint, so that claim is only as good as its enforcement at construction. Measured with a discriminating pair on a freshly built gunbc: data p: NonEmptyStr = "" is REFUSED at the declaration, data p: NonEmptyStr = "mkdir" is accepted and evaluates. Recorded beside the deleted test, with the path boundary and the reason it is not enrolled. Two NonEmptyStr literals become data rows, because a cast of a String literal at a call site is refused by the same judgment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * Count the sh -c residual instead of estimating it: eleven, not six Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * The nbd programs do not both run on the BMC: correct the note that said so Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * State the two privileged word changes at the builder that makes them Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * Name the two concrete products the hub still carries, and why the rule admits them Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * The transport-argv population is blocked by a floor defect, not this lane's residue Preliminary ruling relayed from the shell -> dag lane: an identifier in a transport argv position is never name-resolved, so no citation-based conversion of those sites is possible by any author until gunbc#8916 closes. Naming it as residue would read as a conversion that stopped short and would put the obligation on the wrong lane. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * State the empty-program climb as four facts, not one claim Operator ruling relayed: a completed climb retains executing evidence, and this one has a recorded receipt with no enrolled route. The arms stay deleted -- the empty argv is unrepresentable regardless of enrolment -- but the claim is evidenced-but-not-floor-enrolled on the source path and unmeasured on the emission path, stated as separate facts so they cannot collapse into 'done'. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * The bracket-form regression control follows its builder's rename socket_inode_holder_argv became socket_inode_holder_command when the /proc fd walk stopped being a hand-spelled argv, and this witness -- the control that pins the ? wildcards against the bracketed glob that silently matches nothing -- still called the old name. CI caught it; I had not. It asserts exactly what it asserted before: the emitted pattern is socket:?3894042? and contains no brackets. Only the route to the words changed, from a List<String> return to argv_words over the ArgvCommand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * One name, two operations: qualify the ten live-deploy references the merge made ambiguous THE MERGE PRODUCED A COLLISION NEITHER SIDE COULD SEE ALONE. main's #8845 added `gunbc.live_deploy.operations.rm_force_command(paths: List<String>) -> String` -- privileged shell TEXT for removing several paths. This branch added `extdeps.tools.gnu_coreutils.rm_force_command(path: String) -> ArgvCommand` -- the cited tool operation for one path. Different layers, different carriers, different arity, same spelling. Each was unambiguous in its own tree. THE COMPILER REFUSED RATHER THAN PICKING, which is the outcome worth recording: dag/gunbc/live_deploy/emit.dag:818:29: error: ambiguous reference 'rm_force_command': 2 candidates: extdeps.tools.gnu_coreutils.rm_force_command, gunbc.live_deploy.operations.rm_force_command — qualify by containment path, alias, or rename Ten references, six in `live_deploy/emit.dag` and four in its test. All ten want the shell-text one -- they pass `paths:` and feed `deploy_raw` -- so they are now spelled `gunbc.live_deploy.operations.rm_force_command`. Qualification is the diagnostic's own first suggestion and the least invasive of the three: it edits references, not authorities, and renames nobody's landed symbol. NEITHER NAME IS WRONG, which is why this is not a §3 nicknaming repair. §3 forbids two names for one concept; this is one name for two concepts, and both are honest in their own module -- `extdeps/` keeps the tool's real operation name, and the product layer names the privileged-text form it owns. If either should move it is a question for the live-deploy lane, not something to settle inside a merge. ONE LATENT INSTANCE LEFT DELIBERATELY, named rather than silently passed over: `dag/gunbc/build_cache_endpoint_path.dag:130` calls the unqualified `rm_force_command(path:)` and did NOT error, because `gunbc.live_deploy.operations` is not in that module's import closure. It is correct today and one import edge away from the same refusal -- the same closure-scoped resolution class this session filed as gap-analysis row 30 (gunbc#8943) and repaired four sites of in gunbc#8944. Left unqualified because this PR is otherwise complete and a speculative edit buys a 40-minute CI cycle; recorded here so the next reader finds it by search rather than by breakage. Regen passed on this head (first_generation_equal=true), so the inherited #8691 red is gone and this floor refusal was the only remaining failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * Merge main (§4c annotation fix, #8989) and align the one over-indented admit row THE MERGE closes the last inherited red. #8989 hoisted the §4c-illegal annotation out of `witness_retired_runner_slot_owner_refuses`'s match body; while it stood, floor preparation died before planning a single witness, so every "failing" floor verdict recorded anywhere in the repository during that window was uninformative rather than negative. This branch's own runs in that window say nothing about this branch. THE NIT, from review 54984 and held deliberately until now: one `decl_ref` row in `argv_command`'s `admit_callers` list sat at 8 spaces where its 39 siblings sat at 4. Now 40 of 40 at 4. It is cosmetic, which is exactly why it waited — pushing it alone would have cancelled an in-flight run, dropped five approvals (scored per head), and spent a ~45-minute CI cycle in a fleet completing about one run in six, all to correct four spaces. Riding the merge costs nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 * Two witnesses caught the spelling migration going both ways, and only one of them was the witness's fault THE FLOOR RAN FOR THE FIRST TIME ON THIS BRANCH and reported failed=10 against main's failed=8 on the same head (13db52a25, run 32621117917). The two extra are mine. They are opposite defects and the difference is the whole entry. BUSYBOX -- MY CODE WAS WRONG, THE WITNESS WAS RIGHT. `busybox_build_is_modeled_and_grounded` asserts `'mkdir' '-p'` and got `/usr/bin/mkdir`, because this migration routed the call through `mkdir_parents_command`, whose wrapper cites the recorded absolute path. `gunbc.busybox_bmc_build` runs its whole sequence under `LocalExec` on whichever machine the operator invoked the cross-build from -- it demands an `arm-linux-gnueabi-` toolchain, not a host this repository has ever read. So the absolute path was a claim about an unobserved filesystem: the exact §5 fabrication `extdeps.tools.mkdir`'s own rows warn against, committed by me while quoting the rule. Now routed through `mkdir_parents_command_at` with `mkdir_path_resolved_program`, with the reason stated at the call site as that module asks its PATH-resolved callers to do. The mkdir note is corrected in the same commit rather than left standing: it said the BMC applet was the one such caller and "every other caller gets the recorded absolute path". That became false the moment this second caller landed, and a note asserting a population it no longer has is the stale-recital class DESIGN §3 exists to stop. SUDO -- MY CODE WAS RIGHT, THE WITNESS PINNED THE OLD SPELLING. `srv4_runner_installer_command_cites_installer_with_env` asserts `'sudo' '-E' 'bash' ...` and now renders `/usr/bin/sudo`. That change is deliberate: sudoers matches on the absolute path, so a PATH-resolved spelling would let the caller's own PATH decide which binary crosses the privilege boundary, and `sudo_elevate` two functions above already used this row -- the alternative was two spellings of one binary in one module. The assertion is updated to the new words. THE ASYMMETRY IS THE POINT. Same migration, same kind of diff, two witnesses red, and the correct repair ran in opposite directions -- one edits the code, one edits the oracle. Deciding by which side is easier to change would have got one of them backwards; deciding by whether the host was OBSERVED gets both right. Also lands the annotation I owed at `sudo_binary_path` (review 55022): the -E row documented its own `-n` delta while the path delta was left implicit. Both are now stated at their rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhAfyPkuxnjZaTzPn5PyE1 --------- 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> * A cost-shape defect that measured linear: file the per-step emit constant, and the refuted hypothesis (#8988) * A cost-shape defect that measured linear: file the per-step emit constant, and the refuted hypothesis I proposed fixing a quadratic accumulator in the orchestration emit path, measured it, and it does not exist. The refuted hypothesis is the more useful half of this row, so it is filed with the measurement rather than dropped. WHAT WAS HYPOTHESISED, and every sentence of it is true. v2.compiler.emit_orchestration orch_emit_steps_from folds left-linearly, carrying the accumulated script as a String and calling orch_emit_join2(left: everything_so_far, right: next_step) once per step. That join is not a concat: it reaches orch_emit_from_registry, builds a TargetModel whose binding spellings carry both operands verbatim, and runs the whole grammar emit pass over it. Read that way it is n-1 emit passes over payloads growing to the full script length -- the copied accumulator DESIGN section 6 names. WHAT IT MEASURES. A scaling probe -- identical trivial steps, only the count varying, release claim_batch, thread CPU with wall within 1-5ms on every row: n 8 16 32 64 128 | 128 256 512 1024 cpu 48 61 94 161 308 | 239 448 894 1863 ratio 1.27 1.54 1.71 1.91 | 1.87 2.00 2.08 Linear across two decades, no knee. A quadratic converges to 4.0 per doubling; this converges to 2.0, and the early sub-2.0 ratios are the fixed intercept washing out. Fit: about 14ms + 1.8-2.3ms per step, the slope moving between processes on one box with ambient contention -- the same 2x spread gunbc.witness_row_cost already records. SO THE CONSUMER'S COST IS ARITHMETIC. test.claim.live_deploy.emit twin_and_production_configure_disjoint_tailscale_endpoints budget-refused a required floor run at "at least 5008ms" against v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms. Run alone it PASSES at cpu=5489ms wall=5504ms -- 5008 was the interrupt point, not the cost, so the row is ~10% over the fail-stop rather than ~0.2%. At ~2ms/step its four scripts are roughly 2750 rendered lines. WHY THIS IS NOT A SECTION 6 ALWAYS-FIX. That rule fires on a PROVEN cost shape; a plausible one is a hypothesis. There is no wrong complexity class here, only a large constant, and reducing it means memoizing the emit path on declared-input content -- the Realization/content-hash carrier section 2 holds up as canonical and section 6 records v2 as still hand-rolling. Compiler-wide work on load-bearing files, filed rather than improvised. THE CLASS. A mechanism reading is not a cost measurement. The hypothesis named the right function, the right call and the right reason it is expensive, and was still the wrong complexity class, because whether a real mechanism DOMINATES depends on constants the source does not show. The tell is that the fix would have looked principled: orch_construct_seq2 is left "\n" right over two raw operand tokens, wrapping and escaping nothing, so it is associative and a rebalanced join tree renders byte-identical output. The rewrite was available, correct, byte-safe and pointless -- and merged, nothing afterwards would have distinguished it from a real fix. WHAT WAS DELIBERATELY NOT DONE. The witness was not split: it would zero this row's failure frequency while leaving sixteen siblings at ~8x the 500ms wall migration threshold, which is the absorbing fallback executed at authoring time, and the disclosure gate exists to keep that population visible. The fail-stop was not touched. No declared-ceiling lane was built -- the diagnostic advises one and no such mechanism was found in tree, which is recorded as an observation about the diagnostic. The row stays on the disclosure roster and will intermittently budget-refuse on main until the memoization lands. Named as a real intermittent red, not an acceptable one. Every symbol and module cited here was grep-verified against the tree; no positional citations (DESIGN section 3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * "Two scripts each" was an unverified quantifier, and correcting it moved the finding The approving review found nothing; re-reading my own row did. Two defects, and the second one changes what this row says is worth fixing. THE QUANTIFIER. I wrote that the sixteen live_deploy.emit siblings emit "two scripts each". Counted per witness, twelve emit ONE, three emit two, one emits three, and the refused row emits four. I had counted the family and not the scripts, and asserted the second as though I had. WHAT CORRECTING IT EXPOSED. With the real counts the seventeen observed costs regress cleanly on script count: ~2235ms fixed per witness + ~773ms per emitted script scripts rows observed predicted 1 12 2830-3178ms 3008ms 2 3 3841-3979ms 3781ms 3 1 4109ms 4555ms 4 1 5504ms 5328ms The ~773ms marginal matches the probe's ~2ms/step over roughly 400 lines per script, so the emit constant explains the SLOPE. It does not explain the INTERCEPT -- and the intercept is the larger term for twelve of the sixteen. About 2.2 seconds is spent before the first script is emitted, outside orch_emit_pipeline entirely, and this probe did not measure what it is. WHY THAT MATTERS RATHER THAN BEING A REFINEMENT. The previous revision took 5504ms, divided by ~2ms/step, and reported "roughly 2750 rendered lines, about 690 per script". The slope only accounts for about 800. I had folded an unmeasured fixed cost into a measured per-step figure and published the quotient as though the whole row were emit -- the same class of error as the hypothesis this document exists to record, arrived at one layer further in. So the row now names the fixed term as unmeasured and unowned instead of absorbing it, and says plainly that it, not the emit constant, is the bigger target for most of the family. The split-the-witness paragraph now cites the regression's ~3781ms for a two-script witness instead of "about two scripts and under the fail-stop". The refusal to split is unchanged and unaffected. 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: Claude Opus 5 <noreply@anthropic.com> * Supplier bindings, per-supplier billing quantum, and an acquisition simulation (#8960) * wip: ubicloud supplier binding (verifying) * Billing quantum is a per-supplier fact, and continuous capacity is the modeled advantage of owning * The acquisition question: does demand shape make a commitment worth holding * fix: named fold args, no block lambda, no next-line field values * refactor: qualify Offer/SelectionPolicy as SupplierOffer/SupplySelectionPolicy 'Provider', 'Offer' and 'Selection' each name two unrelated things in this corpus: gunbc.dispatch_selection's ProviderOffer is an agent-runtime credential binding carrying no price, and product.fabric.supply's Offer is a priced compute-supply ask. They do not unify -- there is no proven coincidence to bundle, only a shared English word -- so the generic name goes to neither. dispatch_selection already qualifies its side; this qualifies ours. * fix: bill the quantum against each job's duration, not the slot's concurrency Executing the witnesses caught a units error in the simulation's billing core. quantum_billed_slots(used_slots: taken, ...) passed a JOB COUNT into a parameter meaning a DURATION, so a 60-slot minimum increment rounded a slot's concurrency up to sixty instead of billing each of its jobs for sixty. Both magnitudes are Nat, so it typechecked; only running it disagreed. Replaced by billed_slots_per_job, which states the model's standing assumption -- one slot is one job's runtime, and jobs of differing duration are not modeled until arrivals carry a duration -- rather than leaving it implicit. The shape witnesses were reworked in the same pass because their fixture moved two axes at once: a 60-slot increment against one-slot jobs is so dominant that owning wins under every arrival shape, which is a real effect but not the one those tests claim to measure. Renting now bills per slot there, so demand shape is the only thing varying, and the increment gets its own witness. The flip is now demonstrated at EQUAL MEAN -- flat ten every slot versus six hundred once in sixty, the same 6000 job-slots -- where it previously compared unequal means and asserted the …
… it (#9027) `gunbc compile --entry src/v2/compiler/03_ingest.dag` REFUSES on current main (faf6583) with `EMIT_REFUSE` and no cargo log at all. Measured this morning at 907f19c the same entry emitted 177 files and 316 coded errors, so the board's subject stopped being producible at some point in today's merges. The cause is one file. §4c admits an annotation only as a standalone leading `//` block attached to a module-scope declaration; #8919 left a 62-line block at end-of-file with no item following it, and the compiler correctly refuses every span of it -- "source annotation names no subject: no module item follows it". A corpus-wide scan finds exactly one such file, so the population is closed. The repair moves the block above the file's final declaration. Every annotation line is preserved verbatim (diff of sorted `//` lines is one added `//` separator); no prose is rewritten, dropped, or summarised, because a block deleted for one reason takes everything in it unless its contents are enumerated first. WHAT THIS SAYS ABOUT THE GATE, which matters more than the fix. This is the SECOND instance of this class today: #8976 landed an indented `//` inside a match arm this morning and broke the floor corpus-wide for 74 minutes. That one was caught because the file was floor-enrolled. THIS one sits in dag/test/manual/, which no required phase parses -- the required run's three phases are the src/v1 `.dag` parse sweep, required-regen, and the witness floor, and none of them compiles a v2 entry. So an emission-breaking change landed on main and every gate stayed green. RUNG, honestly: this change is a repair of one instance and NOT a climb. The class remains writable and undetected. Its next-rung trigger is a required phase that compiles at least one v2 entry -- the emission path has no gate at all today, which is why the only instrument that found this was a hand-run probe. 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>
… break reached main and no gate could see it (#9035) * Main emission is refusing on a trailing annotation, and CI cannot see it `gunbc compile --entry src/v2/compiler/03_ingest.dag` REFUSES on current main (faf6583) with `EMIT_REFUSE` and no cargo log at all. Measured this morning at 907f19c the same entry emitted 177 files and 316 coded errors, so the board's subject stopped being producible at some point in today's merges. The cause is one file. §4c admits an annotation only as a standalone leading `//` block attached to a module-scope declaration; #8919 left a 62-line block at end-of-file with no item following it, and the compiler correctly refuses every span of it -- "source annotation names no subject: no module item follows it". A corpus-wide scan finds exactly one such file, so the population is closed. The repair moves the block above the file's final declaration. Every annotation line is preserved verbatim (diff of sorted `//` lines is one added `//` separator); no prose is rewritten, dropped, or summarised, because a block deleted for one reason takes everything in it unless its contents are enumerated first. WHAT THIS SAYS ABOUT THE GATE, which matters more than the fix. This is the SECOND instance of this class today: #8976 landed an indented `//` inside a match arm this morning and broke the floor corpus-wide for 74 minutes. That one was caught because the file was floor-enrolled. THIS one sits in dag/test/manual/, which no required phase parses -- the required run's three phases are the src/v1 `.dag` parse sweep, required-regen, and the witness floor, and none of them compiles a v2 entry. So an emission-breaking change landed on main and every gate stayed green. RUNG, honestly: this change is a repair of one instance and NOT a climb. The class remains writable and undetected. Its next-rung trigger is a required phase that compiles at least one v2 entry -- the emission path has no gate at all today, which is why the only instrument that found this was a hand-run probe. 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> * 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> * A v2-emission phase behind its own entry point: one entry compiled, refusing only where no tree comes out The 2026-08-23 break: `gunbc compile --source-root dag --source-root src/v2 --entry src/v2/compiler/03_ingest.dag` refused outright on main for hours while every required phase stayed green. The required run parses src/v1 .dag, compares the regen mirrors and folds the witness floor; none of the three compiles a v2 entry, so the emission path had no observer. The specimen was a trailing `//` annotation block with no declaration after it, authored under dag/test/manual/, which no required phase reads either. `claim_executor --required-v2-emission` compiles the entries named by `gunbc.ci_layer_roots` `required_v2_emission_entries` and stops the line on the compiler's own refusal authority (`v1_compiler_compile` `stage0_self_compile_refusal_message`) — a blocking diagnostic, or an empty emitted file set. Advisory diagnostics are counted and never refused on: 03_ingest carries 503 of them today, so a gate on any diagnostic would be permanently red. `--required-v2-emission-selftest` is the phase's executing evidence: two controlled fixture roots differing in one thing, the red one carrying the real specimen and the green one the same block moved above its declaration. Red must refuse on the annotation cause; green must emit. Mutation-confirmed — moving the red block above a declaration makes the selftest FAIL. NOT enrolled in `--required-ci` or witnesses.yml: changing what every PR must pass is an operator decision, and the enrolment question goes up with its measured cost attached. * 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. * One emission producer for the board and the gate, a three-state disposition, and enrolment as a required phase Finding 1 (the gate must consume the same producer as the board): it did not. The phase was a second caller reproducing the closure load, the census assembly, the refusal check and the silent-pick gate beside `gunbc compile`'s — and had already drifted on one of the three parameters, resolving under the strict pool index where the board runs primary-precedence. The transaction now lives in `cli_run` `compile_entry_emission`, and BOTH the CLI's `--entry` single-target arm and the required phase call it. Verified by execution that the CLI path is unchanged: abi 4/3847/9, and the board's own invocation on 03_ingest emits 177 by the `compiled:` line with 0 blocking / 503 advisory, exit 0. Finding 2 (a preparation refusal must not render as emitted=0 blocking=0): `Option<refusal>` was a two-state carrier expressing three. `EntryEmissionDisposition` is now Completed / Refused { phase, cause } / NotExecuted { earlier_phase, cause }, the tag is ON the counts line, and the count fields read `n/a` — deliberately not 0 — on any path that never ran. All three arms confirmed by execution. The invariant is emission COMPLETED, never a file count: a legitimate compiler change may alter closure size, so `emitted == 177` would be a change detector. ENROLLED as required-ci phase 3, ordered ahead of the floor, on the relayed operator ruling. The phases stay independent — every one runs after an earlier failure — so this is report order, not an early prerequisite; making it one is a scheduling decision left to the operator with the number attached. Dissolution trigger carried on `gunbc.ci_layer_roots` `required_v2_emission_dissolution`. * Rewrite the standalone-entry-point comment that still said NOT ENROLLED, and name the enrolment authority beside the dissolution row The comment beside --required-v2-emission-selftest was written before the enrolment and survived the revision that added phase 3, so the diff carried prose asserting the phase is opt-in beside code making it required. Rewritten, not annotated: two accounts of one fact is what produced the contradiction. The ci_layer_roots row now states the enrolment authority separately from the dissolution condition — how the gate ends is not the admission that created it. * 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. * Ground the selftest's expected refusal on the typed variant's message authority, not a spelled literal Review 55128 named the coupling: matching `cause.contains("source annotation names no subject")` is a second spelling of a sentence `std_source_annotation` `annotation_attachment_refusal_message` owns, and a rewording silently invalidates it. The expected sentence is now ASKED FOR from that authority, keyed on the typed variant `UnattachedAtScopeEnd` the fixture provokes, so a rewording moves both sides together. Confirmed by execution in both mutation directions: a legal red fixture fails with 'did not refuse', and a fixture refusing for an unrelated cause fails with the grounded sentence quoted in the message. * Restore an unrelated function my splice deleted, and build the module index once instead of twice TWO REVIEW ITEMS FROM review 55167, and the first is a defect in this PR rather than a suggestion. `declared_import_closure_live_paths` — a `#[cfg(feature = "test_hooks")]` host twin for the Class B declared-import controls — was DELETED BY ACCIDENT: the edit that inserted the emission transaction spliced over a region that ended past it, and nothing failed because it is behind a feature gate with no current caller. The review read it as intentional and rationalised it. It is restored verbatim; the diff against the merge base now removes zero lines. The second: `compile_entry_emission` built the source index twice — once inside `load_sources_for_entry_with_pool_index` and once for the census fill — parsing ~3,800 modules per invocation for the second copy. It now builds one `MultiEntryIndex` and hands it to both the closure loader and the census, which also removes the possibility that the two are computed against different populations. Strict routes through `process_shared_index` so a second `--entry` compile in one process is a cache hit. Behaviour identical by execution: abi still 4 closure / 3847 census / 9 emitted, selftest green. NO SPEEDUP IS CLAIMED — measured 134s against 126s before, inside this host's noise; the change stands on removing the duplicate parse and the second population, not on a number I did not observe. --------- Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Brian Searls <briansearls1@gmail.com>
… whose producing computation may never have run (#9002) State-space conflation is already listed, and this is not it. There the carrier cannot represent the distinction and the repair is to split the states; here the state exists and the consumer cannot tell whether it was reached, so the repair is to bind an execution receipt to the value. Two different laws, one shared symptom, and the night that produced this row cost three hours to the difference. The receipts are all measured on main. #8976 put four source-annotation lines inside a match body; DESIGN 4c admits only standalone leading blocks on module-scope declarations, so the compiler refused at strict preparation -- upstream of the fold and of emission -- from 04:30 until #8989 landed at 05:44. Downstream measurements kept producing valid-looking values throughout. One lane read a post-#8976 run and reported TerminalLedgerUnrenderable closed; another cited the same run as evidence it was still firing; it contained zero occurrences because the fold never reached the ledger. Three A/B comparisons reported perfect agreement -- one with both arms broken identically, one with a missing flag so no arm executed, one where the defect was fixed between runs so both arms were the same bytes. All of it follows from a masked run and a clean run rendering identically. The construction move is stronger than naming known masks: any stage that can prevent a downstream observation from executing must participate in the observation carrier, so "refused before stage N" cannot inhabit the same carrier as "stage N executed with zero findings". A named mask is a hole someone found; execution provenance is what stops an unfound hole from masquerading as a measurement. Not touching line 147's known_red_held=206, which this row cites as a specimen: that account is being rewritten in #8982 by the lane that owns the disposition census, and two edits to one figure is the fork this document exists to refuse. 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>
…ody form CI's parse phase refused five sites across two witness files: source annotation sits inside a declaration body. Only module-item grain is modeled; move it above the declaration it describes. Both were mine, added in this PR. §4c is explicit that the initial .dag realization admits only standalone leading // blocks attached to module-scope declarations, and that trailing, body, unattached and block-comment forms refuse until separately modeled. I wrote body-form comments because they read better next to the expression they explain, which is exactly the temptation the rule exists to refuse. WHY THIS CLASS IS WORTH A COMMIT MESSAGE rather than a silent fix: DESIGN records that an in-body annotation on #8976 refused strict preparation for three hours while downstream measurements kept producing valid-looking values, and two lanes drew opposite conclusions from runs where the defect could not appear at all. A comment is not a small edit in this substrate -- it is the one edit class that can mask an entire phase. The content is preserved, hoisted above the declaration it describes. Both witnesses re-run green. LOCAL GAP THIS EXPOSED, recorded rather than left: this session's gunbc binary predates --required-ci, so the parse sweep could not be run before pushing and the class was caught by CI instead. A grep for indented // inside declaration bodies is a usable local proxy and is clean across every .dag this branch touches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s stop being binary paths (#9152) * The install shape stops naming the verbs: privileged binaries are supplied by the principal's roster Re-applied onto current main after #9091 squash-merged. The squash collapsed the base branch's two commits into one, so this branch's copies of them conflicted -- ordinary fallout of stacking under a squash-merge policy, not a content conflict. Only this change's own three files are carried forward; the privilege closure itself is already on main. WHAT THIS CLOSES. extdeps.github.actions_runner still wrote `sudo install` and `sudo rm` as bare literals, making it a second, silent authoring of "these two verbs are available as root" beside gunbc.fleet_posix_accounts. Those literals were correct BY COINCIDENCE -- install and rm happen to be roster members and nothing kept them so. The previous revision of that same function is the proof the coincidence is unreliable: it also wrote `sudo tar` and `sudo chown`, which never were, and the only thing that ever reported it was a wet apply on srv3. ActionsRunnerPrivilegedBinaries carries the two paths in. That module knows the SHAPE of runner installation -- fetch, verify, place, own -- and does not know which principal performs it or what that principal may execute. THE ACCESSOR IS DELIBERATELY NOT IMPORTED THERE. Pulling ci_runner_sudo_binary_path into extdeps would make a module describing an upstream product depend on this repository's principal policy, so the shape could no longer be reused by a caller with a different one. gunbc.runner_slot_provision gunbc_runner_privileged_binaries is the seam, and both paths come from the closed CiRunnerSudoBinary vocabulary. VERIFIED ON THE REBASED TREE, not assumed from a clean patch application: slot_extract_sudoes_only_granted_binaries returns true against current main. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * One argv grounds both the command and its grant: privileged operations stop being binary paths srv3's converge apply refused at its first privileged command with zero slots mutated, and the cause was two authorities answering one question. The roster granted BINARY PATHS (fleet_posix_ci_runner_sudo_grants: install, rm, systemctl, apt-get, visudo, tailscale) while extdeps.github.actions_runner independently rebuilt an argv from those paths. Nothing joined them, so a grant for /usr/bin/install did not imply authorization for the argv the script actually ran. The disagreement was discoverable only by a wet apply on a real host. THE CONSTRUCTION. gunbc.executor_privileged_operation declares the operation once -- EnsureOwnedDirectory / RemoveDirectoryTree / SystemdDaemonReload, parameterized rather than a nullary enum of binaries -- and DERIVES both renderings from one argv projection: executor_privileged_operation_argv the words ..._sudoers_command = join(argv, " ") what authorizes ..._shell_command = sudo + quoted argv what executes A grant that fails to authorize its own command now has no spelling. Quoting is shell-level and invisible to sudo, which receives argv after the shell has parsed it, so one argv grounds both renderings without either knowing the other's escaping. WHAT WAS DELETED, not accumulated beside. ActionsRunnerPrivilegedBinaries and gunbc_runner_privileged_binaries() are gone; extdeps no longer rebuilds argv at all, it splices commands it is handed and stays agnostic. That is a net removal of a parallel rendering. TWO NARROWINGS the antagonist review of 2026-08-24 surfaced, both real reductions in authority: run_as is now root rather than ALL (ALL=(ALL) authorized the grantee to run as ANY user, including other service accounts), and no wildcard reaches the rendered file. /opt/actions-runner/* would match `..` -- fnmatch's * declines to cross `/`, and `..` contains none -- so it would authorize `rm -rf /opt/actions-runner/..`. Every managed path is named in full instead. TWO MODULES, and the split is structural rather than tidy: gunbc.runner_host_grants joins the operation authority to a host's slot roster, because an operation module that also read the roster would close an import cycle with runner_slot_provision, and acyclicity is the one structural law the import graph has. A §3 RESIDUAL IS RECORDED WITH A TRIGGER, not only prose: the fleet-converge state directory was spelled three times in fleet_converge_plan and named nowhere -- once as a bare literal in the lease guard, twice more as the leading segment of the generation store and lock paths. This declares it once; the row dissolves when fleet_converge_plan derives all three from it. EVIDENCE, six witnesses green by execution with RED measured rather than asserted. The load-bearing one is witness_executed_command_is_the_granted_command: it reaches the command the provisioning script RUNS through runner_slot_provision's real seam and the command the sudoers file GRANTS through the renderer, and asserts they are one word vector. Mutation receipt: dropping the quoting from the shell projection returns false, restoring returns true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Add read-only grants_observe mode: measure srv3's privilege instead of inferring it Every claim about srv3's grant state so far rests on ONE observed refusal -- a single argv under /usr/bin/install -- generalized into statements about six binaries and two principals. That is the denominator error this repo keeps paying for, and it is cheap to close: sudo -n -l is read-only, needs no privilege, and answers exactly what is unknown. The mode reports three things and mutates nothing: which account the converge apply actually runs as, what that account may sudo without a password, and whether the in-run fleet key authenticates as the deploy row's ssh principal and what THAT principal may sudo. The third is the one that decides whether a bootstrap path already exists -- if the fleet key reaches a sudo-capable briansrls, convergence can install its own grants with no new credential, and the grantee is never the source of its own authority. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Split the read-only grants_observe mode out of this PR Review 55610 is right that a hand-written shell observation step in the workflow is a DESIGN section 6 scaffold tell -- raw shell implementing semantics expressible in .dag, with no carrier and no dissolution trigger -- and it is a separate concern from the model change this PR makes. It is preserved on session/warm-tern-755-grants-observe and will land with a .dag authority rather than as hand-authored workflow shell. Also fixes the static break review 55610 found: fleet_converge_plan_witness_test still called the deleted gunbc_runner_privileged_binaries and passed the removed job_user argument. Its assertions additionally spelled the sudo commands as UNQUOTED literals, which would have gone permanently red against the posix-quoted rendering -- so they are now built from the authority (executor_privileged_operation_shell_command) rather than restated, which is the drift this PR exists to remove. Verified green by execution. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Encode the bootstrap principal: the fleet key's sudo lives under ubuntu, not the deploy row's ssh_target srv3's grants are installed and ghrunner now holds 44 of them, but the FACT THAT MADE THAT POSSIBLE lived only in a chat transcript, which is where knowledge goes to be lost. This commits it. WHAT WAS UNKNOWN AND IS NOW A ROW. A grantee cannot install its own sudoers grant, so the first install on any host needs a different principal -- and on the executor fleet that principal is NOT the deploy row's ssh_target. Probed against the live host with the fleet key (fleet-automation-ssh-key, fingerprint verified before contacting anything): briansrls Permission denied -- the ssh_target the deploy row NAMES does not accept it fleet-automation Permission denied root Permission denied ghrunner authenticates, nologin -- the grantee, a service account ubuntu full shell, (ALL) NOPASSWD: ALL -- the bootstrap principal WHY IT IS A ROW RATHER THAN A RUNBOOK LINE. The operator did not know the briansrls password and reasonably believed the fleet credential already carried sudo. It does -- under ubuntu, not under the account the deploy row names. That one fact was the entire blocker on going 5 -> 21 runners, it is not derivable from anything else in the tree, and recovering it cost probing five accounts against a live host. Its half-life in prose is one session. THE LAW IS WALLED, NOT NARRATED. bootstrap_principal_is_not_the_grantee is a predicate with a witness, so a future edit pointing both at one account fails rather than quietly creating an account that authorizes itself. RED is authorable: set the bootstrap login to job_user and it goes false. WHAT THIS STILL DOES NOT DO, stated so the gap is tracked rather than rediscovered: the install itself was performed BY HAND over ssh -- out-of-band actuation, the DESIGN section 6 tell. The staged/validate/install/read-back transaction is modeled nowhere for the executor path; Spark has one (gunbc.spark.managed_grant_install) and the executor does not. This row makes the missing piece namable. Next: an executor grant-install transaction consuming these rows, so no host is ever again bootstrapped from a terminal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Drop the duplicated actions_runner_base_dir import and the now-dead fleet_posix_accounts import Both from review 55614, both mine, both introduced by this PR's own edits. The duplicate is the more interesting one: my scripted import insertion added actions_runner_base_dir to a block that already carried it, so one import list named one symbol twice. Whether the ingestor refuses it or dedupes it silently, a symbol that is its own second name is the section 3 violation this PR is otherwise about removing. The fleet_posix_accounts import (ci_runner_sudo_binary_path, SudoInstall, SudoRm) had zero usages outside its own import line after the refactor -- the argv is now derived inside executor_privileged_operation, which is the whole point, so this file no longer reads binary paths at all. Verified by counting usages with the import line excluded rather than by eye. Join witness re-run green after both removals. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Pin actions/runner v2.336.0: 2.334.0 is deprecated server-side and provisions dead slots MEASURED, not inferred. All 13 slots newly provisioned on srv3 came up, authenticated, reached 'Listening for Jobs', and were then refused by GitHub: An error occurred: Runner version v2.334.0 is deprecated and cannot receive messages. Runner listener exit with terminated error, stop the service, no retry needed. ExecStart exits 0/SUCCESS, systemd restarts it, and the unit sits in activating/auto-restart forever. So the slot count went 8 -> 21 while the RUNNING count stayed at 5: the provisioning path is fine and the artifact it installs cannot take work. THE ROW IS REPLACED, NOT ADDED BESIDE. A deprecated pin is not a version that merely lags; keeping it selectable would only preserve a way to build dead slots. Digests are the upstream's own, published in actions/runner's v2.336.0 release body, and the emitted installer verifies with sha256sum -c, so a wrong value fails at the host instead of installing an unverified tarball. NOT TOUCHED: gunbc.fleet_runner_connectivity incident_runner_version and the capacity-plan fixture keep 2.334.0. Those record a PAST incident and a fixed test input; rewriting them would falsify history to satisfy a grep. STANDING RISK THIS EXPOSES, worth stating because it is fleet-wide rather than srv3-local: the five runners still serving on srv3 hold agent registrations minted BEFORE the deprecation. They are grandfathered, not healthy, and will not survive a restart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Add srv1/srv4 grant renderers: three call sites of one derivation, not three lists srv4 had zero ghrunner grants, so the converge apply could not run there at all. Rather than hand-write a second sudoers file, this adds two call sites of runner_host_sudoers_content -- the population follows each host's own slot roster and job_user, so srv1 and srv4 derive their grants from the same function srv3 already uses. Measured: srv4 renders 46 grants, zero wildcards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Split runner activation into three axes: materialize != enable != start THE FUSION WAS DELIBERATE AND ITS NOTE ASKED FOR THIS CHANGE. extdeps.systemd.systemctl already says enable and start are 'SEPARATE OPERATIONS, deliberately not enable --now', and then fuses them in systemctl_enable_now_command for exactly one caller -- runner slot activation -- with a note saying the split 'is its own change with its own witness'. This is that change. WHY THE TWO ACTIONS ARE NOT INTERCHANGEABLE. start runs the unit now, under prerequisites that hold now. enable declares it WANTED PERSISTENTLY, so it starts again after a reboot, at a moment when the cache instance and compile pool that gunbc.runner_activation gates on may not hold. Fused, one admission decision is spent on both, and the post-reboot start inherits an admission measured against a machine state that no longer exists. So both actions consume the admission here; enable is not the harmless half. WHAT THE MISSING SECOND CUT COST, measured rather than argued. The emitted apply.sh carries daemon-reload and disable and nothing that starts a runner -- enable count 0, start count 0. Fleet convergence therefore called a host converged when its slots existed on disk and served no traffic. srv1 held FIFTY slot directories with five runners running and nothing in the pipeline could notice. Convergence was measuring filesystem state while the subject is SERVICE CAPACITY. THREE ORTHOGONAL AXES, NOT ONE LIFECYCLE ENUM, because systemd holds every combination: a unit can be installed-but-disabled-and-running or enabled-but-inactive. A single ordered enum would have to pick an order that does not exist and would make the off-diagonal states unrepresentable rather than merely unusual. The fused spelling now has no representation: MaterializeRunner, EnableRunnerUnit and StartRunnerUnit are separate values, so 'enable and start' cannot be written as one opaque step. UNOBSERVED REFUSES RATHER THAN DEFAULTING on every axis. An axis nobody measured is not a disabled unit and not an inactive one; rendering it as either is the empty-observation narrow -- silently uncovered rather than merely expensive. EVIDENCE: 13 witnesses covering the full truth table, including the off-diagonal rows (enabled-but-inactive starts only; disabled-but-active enables only, which matters because fusing would restart a runner mid-job) and the three unobserved-axis refusals. Green by execution. Discriminating RED measured by mutation, not asserted: removing the admission check returns false, restoring returns true. NOT YET WIRED to a production actuator, and that is the next change rather than a claim made here -- runner_activate_command still has no production caller, and the workflow still has no activation mode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The sudoers header names the host's own reproducing entry point Review 55653. The header has now been wrong twice in two different ways, which is why it takes a parameter rather than a constant. It named executor_privileged_operation.dag until the module split moved the renderer (review 55626); it then named srv3's entry point for EVERY host once srv1 and srv4 rendered through the same function -- so srv1's installed file told a reader to reproduce it with srv3's command, on a production host, in the one string a human reads while deciding whether a sudoers file is safe to regenerate. A constant cannot be right here: the reproducing entry point is a fact about the HOST, not about the renderer, so it is supplied by the caller that knows which host it is rendering. Verified by execution -- srv1, srv3 and srv4 each now name their own entry point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Split the runner activation model out of this PR: it has no reachable consumer here Review 55653 is right that gunbc.runner_service_standing has no production consumer, and the reason it cannot get one in THIS PR is a measurement rather than an omission: gunbc.fleet_converge_plan observes nothing about build-cache or compile-pool readiness -- zero matches for BuildCacheInstanceReady, HostCompilePoolReady or admit_runner_activation anywhere in that module. The activation admission's inputs are receipts that path does not produce. So wiring it into slot_apply_lines here would leave two options, and both are worse than splitting. Fabricating the readiness receipts to make the gate open is the fabricated-plausible-output failure this repository refuses outright. Emitting a gate that can never open makes activation strictly worse than today, where at least a human starts the units and knows they did. The model, its 13-row truth table and its mutation receipt are preserved on session/warm-tern-755-runner-activation and land with the observation layer that feeds the admission -- observe enabled and active per slot, mint the readiness receipts, then a reviewed activation plan with its own subject and fresh observations. That is the change the side-chat analysis of 2026-08-25 describes, and it is a family, not a line appended to the install script. WHAT THIS PR STILL DOES NOT FIX, stated so it is not read as closed: apply.sh emits daemon-reload and disable and nothing that starts a runner, so convergence still calls a host converged when its slots serve no traffic. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Hoist two source annotations to module-item grain: §4c admits no in-body form CI's parse phase refused five sites across two witness files: source annotation sits inside a declaration body. Only module-item grain is modeled; move it above the declaration it describes. Both were mine, added in this PR. §4c is explicit that the initial .dag realization admits only standalone leading // blocks attached to module-scope declarations, and that trailing, body, unattached and block-comment forms refuse until separately modeled. I wrote body-form comments because they read better next to the expression they explain, which is exactly the temptation the rule exists to refuse. WHY THIS CLASS IS WORTH A COMMIT MESSAGE rather than a silent fix: DESIGN records that an in-body annotation on #8976 refused strict preparation for three hours while downstream measurements kept producing valid-looking values, and two lanes drew opposite conclusions from runs where the defect could not appear at all. A comment is not a small edit in this substrate -- it is the one edit class that can mask an entire phase. The content is preserved, hoisted above the declaration it describes. Both witnesses re-run green. LOCAL GAP THIS EXPOSED, recorded rather than left: this session's gunbc binary predates --required-ci, so the parse sweep could not be run before pushing and the class was caught by CI instead. A grep for indented // inside declaration bodies is a usable local proxy and is clean across every .dag this branch touches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Mark the shell renderer as the transport scaffold it is Review 55676. A modeled argv must not launder an unmarked string realization: shell is one transport among N, and a function that concatenates a program is a realization detail the canonical emitter should eventually own. The marker was missing and now binds to the same dissolution target this module's sibling already uses -- gunbc.host_effect host_effect_apply -- because the two dissolve together or not at all. ONE CORRECTION TO THE FINDING'S FRAMING, which does not change the verdict. 'Introduces a hand-built shell program' overstates what the diff did to the string surface: extdeps.github.actions_runner on main already carries THREE inline join(["sudo ", ...]) concatenations, already under a Scaffold disposition (actions_runner_slot_extract_script_scaffold), and this PR replaced them with ONE derived rendering. The debt predates the diff and the diff reduces its authorings from three to one. What was genuinely missing was the marker on the new home, which is what this row supplies -- so the gate fires correctly even though the population it names got smaller rather than larger. Join witness re-run green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Write down the runner service-capacity design before it evaporates The operator's own words when I explained the bootstrap principal by hand: 'this knowledge is all supposed to be encoded in the workflows/models? because i will honestly forget at this point.' That applies with more force to a design worked out across a chat thread, which no future session can read. WHAT IT RECORDS, and why each part is not obvious from the tree: The incident chain in order, because each blocker MASKED the next and the record is otherwise unrecoverable -- privilege blocked materialization; repairing it exposed that install never activates; activating exposed that the pinned release was provider-deprecated. The privilege work was necessary and could never have been sufficient: it proved the host could run the installer, never that GitHub would accept what was installed. Three open defects with their receipts: membership compares only slot_dir, so a release bump repairs nothing that already exists -- the receipt is 29 slot directories deleted BY HAND to force re-provisioning, replacement being inducible only by turning a member into an absence. apply.sh carries enable count 0, start count 0. And ProviderAdmissibility was never modeled beside integrity and availability, so an exact digest proved we faithfully installed the wrong operational version. Serving vs restartable capacity as a carried COUNT rather than a caveat -- srv3 was ServingButNotRestartable and nothing could say so. The compatibility probe question, settled: the deprecation class is answered by a fresh incarnation reaching the broker's message endpoint, because the broker maps RunnerVersionTooOld to exactly the access-denied failure measured on srv3. A canary JOB is only needed to prove scheduler handoff, which is a different question and not what deprecated us. That distinction is the difference between a probe this repo can build and one it cannot. And two rules that are counter-intuitive enough to be worth writing: do not make 'latest' the authority, and do NOT restart the old runners first -- restart is precisely what converts latent old capacity into visible failure. This is a plan document, not a carrier. It claims nothing is implemented that is not, and its final section separates landed from built-not-landed from open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Brian Searls <briansearls1@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es in prose Both are mine, both are the compiler correctly refusing, and both are error classes DESIGN names explicitly. Recording what they were rather than just fixing them, because I walked into each with the relevant rule already written down. FIRST, §4c. I put explanatory `//` blocks INSIDE func bodies in bmc_first_contact and bmc_onboard -- thirteen of the run's errors. §4c admits only standalone leading blocks attached to module-scope declarations and says trailing, body and unattached forms refuse; I wrote body forms. The content was worth keeping, so each block is hoisted above the declaration it describes and rewritten to read at that grain -- a note above `func bmc_probe_credential_phase` has to speak about the whole probe rather than about the statement it used to sit on, and the post-rotation one now names the func's own subject. This is the exact failure mode DESIGN records against #8976: an illegal annotation inside a match body made the compiler refuse at strict preparation while everything downstream kept producing valid-looking output. Here it refused before any witness executed, which is why the floor reported `modules_resolved` and then nothing about my witness at all. SECOND, brace interpolation in prose. I quoted two argv TEMPLATES inside `data` strings -- `-u {username}:{password}` and `systemd-run --unit={unit} --collect -- {command_argv}` -- and the compiler read the braces as interpolation and refused on four undefined variables. The escape is `\{`, already used in this repo for `%\{http_code}`. The irony is not lost: this carrier is about a value being interpolated into a string by a mechanism that had no business interpolating it, and its own prose was interpolated by a mechanism that had no business interpolating it either. I swept both classes across every file this branch touches rather than fixing only the lines the run named -- no indented `//` remains, and the only unescaped braces left are the argv templates in extdeps.bmc.http where interpolation is the point. WHAT I COULD NOT DO: validate this locally. The system gunbc predates annotation support and errors on line 6 of any file carrying a `//` block, including unmodified std/algebra.dag, std/decl_ref.dag and std/types.dag -- an older compiler against a newer corpus, so it cannot answer the question. The static sweep above is what I have; CI is the executor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…and on this fleet that is measured (#9226) * A credential in a command line is read by every process on the host, and on this fleet that is measured Three facts taken on srv1 on 2026-08-25 decide this class: /proc carries no hidepid, /proc/PID/cmdline is mode 0444, and the CI runner slots are ordinary host systemd services in the host pid namespace rather than containers. So an argv word is not readable by "other processes owned by the same user" -- it is readable by any job payload executing on a runner slot, in a repository whose workflows a pull request can influence. The usual reason this class gets deprioritized is an assumption that the reader must already be privileged; here the reader is unprivileged and arrives through the front door. The observation that opened the incident is NOT in this repository -- it is the session container entrypoint projecting a forwarded credential roster into `tmux new-session -e NAME=VALUE`. That is reported to its owner rather than edited around, with a verified repair shape recorded in the new carrier. What this commit does is ask the same question of our own tree, and it had three answers. extdeps.bmc.http declared `password: Secret` on GetSystem, GetPower and GetThermal and then interpolated it into `-u {username}:{password}`. bmc_onboard called GetSystem with the FRESHLY ROTATED credential, so the new password entered argv at the moment the old one was retired. This was a half-finished replacement migration rather than a new defect: every mutating operation already used --netrc-file and a GetSystemWithNetrc sat directly beside the leaking GetSystem answering the identical question. The correct shape was present and understood; what kept the leak alive is that the old form was left standing next to the new one, and five call sites duly reached for it. So the cut is at the root -- the three argv-borne arms are deleted, and GetSystemWithNetrc goes too, because once GetSystem takes no password the `WithNetrc` suffix distinguishes nothing. `Secret` no longer appears anywhere in that module, which is the receipt. Moving every BMC call onto the netrc made that file's MODE load-bearing, so write_netrc now uses WriteOwnerOnly -- 0600 at creation via O_EXCL, not write-then-chmod -- with a shred first, since O_EXCL refuses an existing path and this tool legitimately rewrites the netrc within one run. Otherwise the repair would have moved the exposure from the process table to the filesystem, where the audience is identical. gunbc.ci_spec built `curl -H "Authorization: Bearer $WIF_ACCESS_TOKEN"` in two runner preludes. The shell expands before exec, so the GCP access token was a curl argv word on exactly the hosts measured above. The header is now written by the `printf` BUILTIN -- which forks no process and so creates no second command line to leak into -- and read back with `-H @FILE`. Executed both ways with a planted canary: present in curl's argv under the old form, absent under the new, so the detector is discriminating rather than merely quiet. The third instance is DECLARED AND NOT REPAIRED, named in the new carrier rather than left for rediscovery. nbd_proxy_serve concatenates a live BMCWEB session token into a `--header=` argv word, and because that argv is launched via systemd-run the value also reaches the unit's ExecStart and the journal. Its repair is not the same move as the other two -- websocat has no @file header form and systemd-run performs no shell expansion, so it needs an EnvironmentFile plus an sh -c wrapper, on the srv3 OS-install path, and two witnesses that currently ASSERT the token's presence in argv would have to be inverted with it. A probe written to prove the value was materialized rather than left a dead shell reference has, as a side effect, frozen the argv placement as a contract. bmc_auth_runtime_secret_seam_test is deleted, and not as collateral: it asserts that a Secret survives a locally-defined IDENTITY function. It has no red arm and never had one, so it proved nothing about the transport, and with basic auth gone its name would leave a reader grepping for coverage that does not exist. RUNG, stated at the honest grain: mitigatable, not mechanically preventable. Nothing refuses the next author who interpolates a credential into a command string, and the reason is structural rather than an oversight -- v2.std.operation_argv already refuses undeclared names and a non-literal argv[0], but ShellTransportOperationRow carries declared_inputs as NAMES WITHOUT TYPES, so a Secret-typed input bound into an argv token is invisible to the binder. That missing type on the row is the next-rung trigger, and it is the difference between cannot-climb and can-climb-after-one-grounding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Give the argv repair a witness with a red arm, over the emission rather than the committed file The repair in the previous commit is two call-site edits, and a call-site edit is exactly the kind of thing a later author undoes by copying a neighbouring curl invocation. This enrolls the property instead of trusting the diff. TWO ASSERTIONS, AND EITHER ALONE WOULD BE A DECORATION. The absence of the leaking spelling is the safety property; the presence of the file-backed one is what makes that absence mean the credential still ARRIVES rather than that the step quietly stopped authenticating. A prelude that had simply dropped the header satisfies an absence-only check perfectly -- an argv that is clean because it is broken. THE SUBJECT IS THE EMITTED SHELL, NOT THE COMMITTED WORKFLOW FILE, deliberately. The committed yml is a projection of these functions; asserting against it would test whether somebody remembered to regenerate, while asserting against the emission tests the authority that decides what any future regeneration produces. The third test is the one most likely to be lost in a later edit because it looks like a formatting preference. It is not. `printf` runs inside the shell and forks nothing, so the token exists only in the shell's memory and in the file; any EXTERNAL writer of the same bytes -- echo as a binary, a here-doc through tee, a python one-liner -- would leave the value in ITS OWN argv, relocating the exposure rather than removing it. Someone swapping printf for tee has not made a cosmetic change, and this test is what says so. One thing checked rather than assumed, because it would have made the whole witness non-discriminating: the emitted printf line contains the text "Authorization: Bearer" and the text "$WIF_ACCESS_TOKEN", so a naive forbidden pattern could have matched the REPAIR and gone red on correct code -- or, worse, been weakened until it matched nothing. It does not match: "%s\n' \"" sits between the two halves in the emitted line, while the old spelling was contiguous. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Instance 3 occupies three argv positions, not one, and the next item is scoped rather than started TWO CORRECTIONS AND ONE SCOPING, none of which change the repairs already landed. FIRST, THE DECLARED-NOT-REPAIRED INSTANCE IS WORSE THAN THIS CARRIER SAID. It recorded the BMCWEB session token as reaching the transient unit's ExecStart and the journal "in addition to the process table", which undercounted by reading only the websocat argv. extdeps.systemd.systemd_run RunTransient's own argv is `systemd-run --unit={unit} --collect -- {command_argv}`, so the token sits in THREE positions: systemd-run's own argv while it runs, websocat's argv for the whole lifetime of the served ISO, and systemd's recorded ExecStart. The middle one is the long-lived one and is the identical world-readable cmdline surface as the incident that opened this carrier -- so the instance I deferred is not merely a journal problem with a process-table footnote, it is the same primary defect plus two further surfaces. The disposition does not change; the severity statement did, and an understated severity on the one instance left open is the wrong direction to be wrong in. SECOND, THE QUERY THAT WOULD CONFIRM THE JOURNAL HALF IS RECORDED WITH ITS CONTROL, because the obvious sweep does not terminate -- srv1's journal is ~3.4M lines per 24h and a regex alternation over all of it exceeded a measurement budget without returning. The unit name is derivable rather than guessable, so the query is one glob and a fixed string. The control matters more than the query: if no such unit has ever run on the host, a zero match means the path is UNEXERCISED, not clean, and the leak is latent rather than absent. A zero against no history distinguishes nothing -- which is the same non-result this lane has now produced twice, once on my own curl measurement and once on a container-runtime sweep whose control was also zero. THIRD, THE CONSTRUCTION ITEM IS SCOPED AND NOT STARTED, on direction. What landed is two repairs and a declaration; the class is held by review diligence, which is exactly what failed for however long three instances stood. The missing fact is specific: ShellTransportOperationRow carries declared_inputs as NAMES, not types, so the refusal a binder would need to raise is not merely unwritten but unexpressible against the row it would read. With the type present, Secret in an argv token becomes a typed cause and instance 1 stops being a thing an author rewrites. The scope note also says what that trigger does NOT reach, so nobody picking it up overclaims: ci_spec's hand-authored shell is not declared operation argv, so no binder change touches it under any version. That half stays at diligence plus the emission witness until shell command construction is itself modeled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Say plainly that the instance left open is the most severe, because the intuitive order is backwards A reader deciding what to pick up next reads the ranking line, and the natural assumption -- the two REPAIRED instances were the serious ones, the deferred one was the leftover -- is exactly wrong. Instances 1 and 2 were argv-only on SHORT-LIVED processes: a curl fetching one secret, a curl authenticating one Redfish call. Both closed. Instance 3 is argv PLUS A DURABLE RECORD, three surfaces with three lifetimes -- systemd-run's transient argv, websocat's argv FOR THE LIFETIME OF THE SERVED ISO, and the recorded ExecStart which persists in the journal and `systemctl show` after the process exits. The middle surface is the same one the incident was originally reported against, held for as long as an install is served. The third outlives the process, so revoking the session token stops the credential working without removing it from the record of what ran. This does not reverse the decision to declare rather than repair -- inverting two witnesses that ASSERT the token is in the argv still belongs with the repair, and doing it inside a security change on the srv3 install path is how a leak fix becomes an outage. It does mean the open instance outranks the binder trigger when someone picks up next work, and leaving that to be reconstructed from three separate paragraphs was the wrong way to record it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * The journal half is measured: unexercised, which is not clean — and the detector that nearly said otherwise Q3 ran on all four fleet hosts against the derivable unit glob `nbd-proxy-*-ws`: units=0, journal_lines=0, grep_journal=0, execstart_now=0, live websocat=0, everywhere. BOTH CONTROLS ARE ZERO, so the greps distinguish nothing. No such unit has ever existed and the journal holds no lines for that selection, which means the srv3 OS-install route has not run on this fleet. The verdict is UNEXERCISED PATH: the leak is LATENT, not absent. The code path is real and exposes the token on all three surfaces the moment it runs, and recording a zero-against-no-history as safety would be the same non-result this lane has produced repeatedly. THE RANKING NOW CARRIES TWO AXES, because collapsing them mis-ranks this in either direction. Instance 3 is worst by SURFACE COUNT AND LIFETIME and is the only one NOT CURRENTLY FIRING; instances 1 and 2 were LIVE and are closed. Severity alone over-ranks it against work leaking today; liveness alone under-ranks a three-surface exposure that fires the moment the install path runs. The latency also STRENGTHENS the declare-rather-than-repair call rather than merely permitting it: nothing is leaking right now, so no emergency forces a rushed change to the srv3 install path, and the repair can land properly with the two token-asserting witnesses inverted alongside it. AND THE DETECTOR RULE IS ENCODED, because the first pass of this very measurement reported live token-bearing processes on all four hosts and it was FALSE: every hit was `grep` matching its own command line, since the search pattern is itself an argv word that ps can see. Excluding the searcher returns zero. That is the third instance of this artifact in one day -- it also inflated an unrelated orphan census, and it made this module's own curl repair read as a non-repair until the probe was narrowed to the subject's own cmdline. None of the three was caught by reading. So: any ps- or /proc-based detector must exclude its own process, and the receipt for a ZERO must be a PLANTED POSITIVE rather than an absence of hits. That is not hygiene advice sitting in a security carrier by coincidence -- it is this carrier's own defect turned on the instrument: a value in a command line read by something with no business reading it, which here was the measurement itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Record why the auth-header payload is shell text, instead of marking it as debt it is not review 55857 asked for a bash-emit scaffold marker on the header-write command. Declining the marker and recording the reasoning where the next reader will hit it, because the marker would assert something false. The gate is real and this module has two instances of it -- but both mark `*_script` functions that CONCAT-BUILD a whole script as a String, and their own dissolution text names that shape: "medium-as-string concat scaffold", dissolving when bash-emit realizes the runner "through orchestration emit ... without a medium-as-string concat". gunbc_ci_fleet_key_agent_script does not concat-build. It emits a typed Pipeline of PipelineStep through v2.workflow.gunbc_invoke_step_emit -- which IS the terminal state those triggers name -- and gunbc.fleet_ssh_credential_verify's supersession note records that migration having already happened. The prelude carries zero scaffold markers across ~20 command steps for that reason. So marking one payload while its twenty peers carry none would claim this line is unmigrated debt when it sits in the migrated construction: a false rung on the carrier, and a scaffold verdict written by its own author to clear a review, which DESIGN forbids in the same breath as it says a dissolution condition does not authorize the debt it describes. What the review is right about is kept rather than argued away: a Run's `command` IS still shell text. That is corpus-wide and tracked by bash-emit, not by a per-line marker. And the reason no modeled actuator was available here is a BOUNDARY, not a preference -- this prelude runs as raw shell in a foreign executor before gunbc runs at all, so Filesystem.WriteOwnerOnly, which this same change uses for the netrc on the BMC path, is unreachable at that point in a job. printf is not a modeled route declined; it is the absence of one on that side of the boundary, which is what bash-emit would change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fix the two things CI refused: in-body annotations and unescaped braces in prose Both are mine, both are the compiler correctly refusing, and both are error classes DESIGN names explicitly. Recording what they were rather than just fixing them, because I walked into each with the relevant rule already written down. FIRST, §4c. I put explanatory `//` blocks INSIDE func bodies in bmc_first_contact and bmc_onboard -- thirteen of the run's errors. §4c admits only standalone leading blocks attached to module-scope declarations and says trailing, body and unattached forms refuse; I wrote body forms. The content was worth keeping, so each block is hoisted above the declaration it describes and rewritten to read at that grain -- a note above `func bmc_probe_credential_phase` has to speak about the whole probe rather than about the statement it used to sit on, and the post-rotation one now names the func's own subject. This is the exact failure mode DESIGN records against #8976: an illegal annotation inside a match body made the compiler refuse at strict preparation while everything downstream kept producing valid-looking output. Here it refused before any witness executed, which is why the floor reported `modules_resolved` and then nothing about my witness at all. SECOND, brace interpolation in prose. I quoted two argv TEMPLATES inside `data` strings -- `-u {username}:{password}` and `systemd-run --unit={unit} --collect -- {command_argv}` -- and the compiler read the braces as interpolation and refused on four undefined variables. The escape is `\{`, already used in this repo for `%\{http_code}`. The irony is not lost: this carrier is about a value being interpolated into a string by a mechanism that had no business interpolating it, and its own prose was interpolated by a mechanism that had no business interpolating it either. I swept both classes across every file this branch touches rather than fixing only the lines the run named -- no indented `//` remains, and the only unescaped braces left are the argv templates in extdeps.bmc.http where interpolation is the point. WHAT I COULD NOT DO: validate this locally. The system gunbc predates annotation support and errors on line 6 of any file carrying a `//` block, including unmodified std/algebra.dag, std/decl_ref.dag and std/types.dag -- an older compiler against a newer corpus, so it cannot answer the question. The static sweep above is what I have; CI is the executor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: gunbc-ci-auto-heal <gunbc-ci-auto-heal@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Main is red. #8947 replaced srv3/srv4's usable-RAM row -- a 125 GiB conservative
lower bound borrowed from a 2026-07-16 srv3 reading -- with measured /proc/meminfo
MemTotal of about 502 GiB, and left nine floor claims asserting the widths the old
bound produced. This fixes all nine plus two more that would have broken next, and
lands the CPU axis they cannot be honestly closed without.
WHY THE CPU AXIS IS IN THIS DIFF AND NOT A FOLLOW-UP. host_memory_admitted_width
answers what MEMORY admits, and 29 is now the true answer on srv3/srv4. But
gunbc_runner_slots_per_host took a minimum over memory and disk only, and disk is
DiskWidthUnconstrained on both -- so the minimum degenerated to memory alone and a
corrected 29 would have COMMITTED 29 runners on a 128-core machine. The honest
literal is only safe beside a CPU term. Either both land or neither does.
host_cpu_admitted_width = cited cores / cores per slot = 128 / 6 = 21
Cores are read from extdeps.cpu.ampere altra_max_m12830_catalog rather than written
down, so a host that is not an M128-30 cannot silently inherit the width; that the
fleet IS uniform is checked by fleet_processor_verdicts rather than assumed here.
CORES PER SLOT NOW DERIVES INSTEAD OF BEING AUTHORED. It was a literal 8 with no
stated relation to the 16 GiB MemoryMax beside it, so the two could drift and only
an author's memory tied them together. It is now ceil(memory_max / 3 GB) = 6, at the
1 core : 3 GiB shape the operator adopted 2026-08-23 -- the arm64 ratio Ubicloud
publishes across its whole standard profile, adopted because a published provider
catalog is a real reference and an invented number is not. Rounding is UP because
that yields FEWER slots (128/6 = 21, not 128/5 = 25) and over-admitting is the
harmful error. The wall's cores conjunct changes with it: it was
== 8, a literalrestating a literal twenty lines above -- the measure() == measure() shape that
module's own oracle note says it purged -- and is now the relation
cores * per_core >= memory_max, which a wrong rounding direction fails.
SRV1 AND SRV2 GET THEIR FIRST MEASURED MemTotal, which is what puts srv1 at 21.
srv1 reads 502.24 GiB and srv2 134314512384 bytes, both over SSH 2026-08-23, srv2
corroborated by its own docker daemon. srv2's lands about 96 MiB ABOVE the bound it
replaces -- the bound was not merely loose there, it was on the wrong side, and a
lower bound that is too high is not conservative at all.
That retires the last consumer of gunbc_ci_conservative_lower_bound_usable_ram, so
it is DELETED and its scaffold disposition discharged to Terminal: no arm remains in
which one host's measurement answers for another's. The HostUsableRamConservativeLowerBound
VARIANT stays -- nothing constructs it today, and an arm nothing currently occupies
is a quiet guard rather than a dead one.
host usable memory cpu committed
srv1 502.24 GiB 27 21 21
srv2 125.09 GiB 5 21 5
srv3 502.25 GiB 29 21 21
srv4 502.25 GiB 29 21 21
Memory bound every host in this fleet's history until yesterday and now binds only
srv2, whose 64 GiB install failed training and was reverted.
THE TWO REAL DEFECTS, as against the stale claims. installed_bom_reconcile's two
live-population rows routed srv1's observation through a local uniform_expectation()
that hardcoded the Hynix catalog, so when #8947 moved srv1's production expectation
to Samsung the row moved and the copy did not. Fixed by removing the seat rather than
the copy: the four per-host expectations are now named data in fleet_physical_inventory
and the witness imports srv1_dimm_expectation. uniform_expectation survives for the
constructed fixtures, which legitimately author their own.
WHAT THE STALE CLAIMS TAUGHT, since five of them needed rewriting rather than
renumbering. The control-plane placement pair's answer INVERTED: the 2 GiB probe now
costs srv3/srv4 a memory slot and srv1 nothing, and reaches the COMMITMENT only on
srv2, because everywhere else CPU binds well below memory. The sessions-charge
demonstration moved from srv1 to srv2 -- both its arms conserve on srv1 now, so the
discrimination was gone, and relaxing the assertion until it passed would have
silently retired a live claim whose example had grown out of it. And
admitted_width_charging searched 1..8 under a comment calling that "wider than any
host can reach", which stopped being true at 502 GiB: an outgrown search bound
returns its own ceiling, so every host would have reported exactly 8 and the rows
would have compared the bound to itself.
Two witnesses that were passing on main are updated because this change breaks them,
and one is renamed for cause: runner_slot_allocation_committed_widths_MATCH_MEMORY_ADMISSION
stated a coincidence as a law. Committed equalled admitted only while memory bound
every host, and a name that encodes a coincidence teaches the next reader the wrong
invariant.
CORRECTING THE LITERALS DOES NOT DISSOLVE THE SECOND REPRESENTATION. host_memory_admitted_width
still authors four numbers that derived_width_reproduces_every_authored_literal then
re-derives and joins to them -- validation standing where construction was available.
The construction fix is for the function to CALL that derivation, and it is blocked
on an import cycle: gunbc.ci_floor_measurement imports runner_slot_allocation for the
slot shape, so runner_slot_allocation cannot import the measurement. Breaking it means
moving the slot shape to its own authority across roughly twenty-five files, and stays
a separate change. This leaves a corrected copy where an uncorrected one stood.
Six samsung_dram_module rows are removed from floor_expected_red -- an edit to a
nine-element list, not a deletion of it; the three still-red rows stay. The witnesses
themselves are kept as permanent regression controls.
No CPU reservation is modeled. Every core is offered to slots, so the host, its
kernel and the session containers on srv1/srv2 are charged nothing on that axis --
a real gap, confined to CPU, where oversubscription degrades throughput rather than
triggering the OOM kill that makes memory unforgiving. Dissolve-on is recorded.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
This unblocks main, which is red for every branch in the fleet. Proof it is main's state and not any PR's: the last completed
witnessesrun withfailed=0is 32609950903 at544b0feda, and96cb36285(#8947, mine) is not an ancestor of it. Every main run since is still queued.Nine floor failures, split by cause:
installed_bom_reconcile.srv1_live_population_confirms_the_expectationinstalled_bom_reconcile.confirmation_predicate_separates_the_two_live_hostshost_allocation_conservation.*fleet_intent_memory.srv2_population_matches_bmc_memory_summaryPlus two witnesses that pass on main and that this change breaks (
runner_slot_allocation,runner_lifecycle), updated here.The srv2 row is not yet explained and I am not guessing at it. srv2's population is untouched by #8947 (8 × Hynix 16 GiB = 128 GiB) and it is compared against
gibibyte(128), so it should pass by inspection and does not. #8947 splitsrv1_srv2_populations_match_bmc_memory_summaryinto per-host claims and this is the new srv2 half, so the defect is most likely in the split rather than in either half. If CI still shows it red, that is expected from this diff and I will fix it in the next push rather than assert a cause I have not measured.🤖 Generated with Claude Code