From 3e54d851b88c1a9d7fa58b72a1dde0480ad21995 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 01:26:15 +0000 Subject: [PATCH 1/8] WIP: self host/namespace/type check --- dag/gunbc/roadmap_authority.dag | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dag/gunbc/roadmap_authority.dag b/dag/gunbc/roadmap_authority.dag index 3041a287c0e..c391f5a5012 100644 --- a/dag/gunbc/roadmap_authority.dag +++ b/dag/gunbc/roadmap_authority.dag @@ -529,13 +529,13 @@ fn declared_roadmap_nodes() -> List { owner: "namespace", path: "docs/plans/namespace-unique-on-chain-operational-plan.md", t: fields( - headline: "One decision function, handed the candidates rather than finding them", - boundary: "Given a list of things a name could refer to, decide: none, exactly one, or more than one. Checking that the name and the declarations are well formed and of a kind that could match. NOT working out which declarations are in view — that is the next step.", + headline: "One canonical decision authority, handed the candidates rather than finding them", + boundary: "Given a list of things a name could refer to, decide: none, exactly one, or more than one. Checking that what it was handed is well formed and of a kind that could match. NOT working out which declarations are in view — that is the next step. Owns the canonical decision itself; does NOT own removing the older mechanisms that still decide it their own way.", displaced_cost: "Several lookup mechanisms exist today and they can disagree about the same name, which is how a program silently ends up meaning something other than what was written. Separating the decision from the search makes the decision reviewable on its own.", - first_slice: "Validate the name and its declarations, then call the one decision function exactly once on a candidate list supplied from outside.", - red_control: "A malformed name refuses; a declaration of the wrong kind is excluded; supplied lists of nothing, one thing, and two things give unresolved, resolved, and ambiguous respectively; and no second place in the compiler makes that same decision.", - out_of_scope: "Does not work out which declarations are visible, does not order them by where they were written, does not build an index, and does not claim the wider naming work is finished.", - handback: "The decision function plus worked none/one/many examples actually run, and evidence that nothing else decides the same question." + first_slice: "Validate what was handed in, then call the one decision function exactly once on a candidate list supplied from outside.", + red_control: "Malformed occurrence transport refuses; a declaration of the wrong kind is excluded; a candidate given in the wrong role refuses, distinctly from one that is absent; the same declaration supplied twice refuses as a malformed population rather than counting as two; and supplied lists of nothing, one thing, and two distinct things give unresolved, resolved, and ambiguous respectively.", + out_of_scope: "Does not work out which declarations are visible, does not order them by where they were written, does not build an index, and does not claim the wider naming work is finished. Does not require the older deciding mechanisms to be gone — that is the retirement node's job, and holding it here would block the next step on work the next step does not need.", + handback: "The decision function plus worked none/one/many examples actually run, with the malformed-population and wrong-role refusals executing." ) ), active_large( @@ -620,10 +620,10 @@ fn declared_roadmap_nodes() -> List { path: "docs/plans/namespace-unique-on-chain-operational-plan.md", t: fields( headline: "Delete every other way of deciding what a name means", - boundary: "Once all ordinary consumers use the one rule, remove the older mechanisms: name-string matching, walking up the parent chain, the global pool of bare names, and the import retry.", + boundary: "Once all ordinary consumers use the one rule, remove the older mechanisms: name-string matching, walking up the parent chain, the global pool of bare names, the import retry, and the hand-written seed mirror that counts none/one/many for itself inside the bootstrap compiler. Owning that last one HERE, rather than in the node that defines the canonical decision, is deliberate — see namespace_kernel_authority_amendment_note.", displaced_cost: "Leaving them in place means one name can still get different answers from different code paths.", first_slice: "Inventory every remaining place that decides what a name refers to, and remove one complete family.", - red_control: "The inventory refuses anything it cannot classify and must reach zero before this closes.", + red_control: "The inventory refuses anything it cannot classify and must reach zero before this closes; the seed mirror is one of its rows and is not closed by anything upstream of it.", out_of_scope: "Code that merely stores where things are written, without choosing meanings, stays.", handback: "The zero-remaining inventory plus proof the compiler still behaves the same." ) From d51241542aa5d226a3c1b91cb586b990bf13183b Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sat, 1 Aug 2026 01:29:51 +0000 Subject: [PATCH 2/8] chore: regenerate drifted generated artifacts (ci auto-heal) --- ROADMAP.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 8bf6b3632de..e6815b3f8a6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -16,14 +16,14 @@ The graph has eleven lanes: SCM compatibility · namespace · P-derive · observ - [ ] **Have the new compiler write out Rust source itself** — Read the source, work out the types, translate, and write real Rust to disk without the old generator deciding any of it. The old runner may still be what executes this; it just stops making the decisions. Why: Until the new compiler can write Rust on its own, every improvement has to go through the generator we are trying to delete. [authority](src/v2/compiler/self_host/candidate_generation.dag) - [ ] **Regenerate one already-tested module with the new generator** — Take a module that already has working tests, change only which generator produces it, and confirm it still passes the same tests including the deliberately broken one. Why: Proves the whole approach works on something real before anyone builds more on the assumption that it does. [authority](src/v2/compiler/self_host/frontier.dag) - [ ] **Build the new generator with itself, then do it again** — The generator, and only the generator: it writes and builds a native version of itself, that native version writes and builds another, and the second one needs nothing from the old compiler. The other parts of the compiler are still produced the old way at this point. Why: This is the moment the new generator stops being something we produce and becomes something that produces itself. [authority](src/v2/compiler/self_host/frontier.dag) — requires all: v2-emitter-first-behavioral-module, pderive-error-movement-receipt -- [ ] **Write the closing check for: One decision function, handed the candidates rather than finding them** — One executable check states when this is done, transcribed from the node's own bar: A malformed name refuses; a declaration of the wrong kind is excluded; supplied lists of nothing, one thing, and two things give unresolved, resolved, and ambiguous respectively; and no second place in the compiler makes that same decision. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **One decision function, handed the candidates rather than finding them** — Given a list of things a name could refer to, decide: none, exactly one, or more than one. Checking that the name and the declarations are well formed and of a kind that could match. NOT working out which declarations are in view — that is the next step. Why: Several lookup mechanisms exist today and they can disagree about the same name, which is how a program silently ends up meaning something other than what was written. Separating the decision from the search makes the decision reviewable on its own. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) +- [ ] **Write the closing check for: One canonical decision authority, handed the candidates rather than finding them** — One executable check states when this is done, transcribed from the node's own bar: Malformed occurrence transport refuses; a declaration of the wrong kind is excluded; a candidate given in the wrong role refuses, distinctly from one that is absent; the same declaration supplied twice refuses as a malformed population rather than counting as two; and supplied lists of nothing, one thing, and two distinct things give unresolved, resolved, and ambiguous respectively. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **One canonical decision authority, handed the candidates rather than finding them** — Given a list of things a name could refer to, decide: none, exactly one, or more than one. Checking that what it was handed is well formed and of a kind that could match. NOT working out which declarations are in view — that is the next step. Owns the canonical decision itself; does NOT own removing the older mechanisms that still decide it their own way. Why: Several lookup mechanisms exist today and they can disagree about the same name, which is how a program silently ends up meaning something other than what was written. Separating the decision from the search makes the decision reviewable on its own. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Work out what each name can see, and let that be what a file depends on** — From where a name is written, work outward to every declaration actually in view — including ordinary neighbours in the same file — respecting where things were written relative to each other. Then record, for each name, exactly which declaration answered it, and derive what each file depends on from those answers. Why: What a file depends on is currently written out by hand as a separate statement, so it can disagree with what the file actually refers to. Hundreds of files were patched by hand to keep those two in step, which is work that should never have existed: what a file depends on is a consequence of what it refers to, not a second thing to maintain. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Settle the names that genuinely could mean two things** — Using the real answers from the step before — not a count taken by searching text — go through every name that genuinely has more than one candidate and either refuse it on purpose or say which one is meant, by renaming or by writing it out in full. Why: Counting these by searching for text gives a number that is wrong in both directions, so it cannot tell anyone how much work is left or whether it is finished. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Make name lookup itself produce that one answer, and delete the old way** — The places that WORK OUT what a name refers to — type names, value names, and calls — produce their answer from the mechanisms built in the earlier steps, and the old habit of searching everything currently loaded is deleted rather than left beside them. This is the changeover of name lookup itself. What the rest of the compiler then does with that answer is the next step, not this one. Why: Until the old lookup is deleted the new mechanism is only advisory: both run, they can disagree, and the compiler can still quietly mean something other than what was written. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Make the rest of the compiler use only that one answer** — Type checking, call lookup, and argument-count checks all read the single decided answer. When the answer is 'unresolved' or 'ambiguous', they stop rather than carrying on with a guess. Why: While parts of the compiler still do their own lookup, the one shared rule is advisory rather than authoritative. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Do the same for imports** — Every place the newer compiler resolves an import or a dotted name reads the single decided answer, before the old mechanisms are deleted. Why: If imports are left behind, they become a second source of truth about what a name means — exactly the problem this whole chain exists to remove. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Delete every other way of deciding what a name means** — Once all ordinary consumers use the one rule, remove the older mechanisms: name-string matching, walking up the parent chain, the global pool of bare names, and the import retry. Why: Leaving them in place means one name can still get different answers from different code paths. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Delete every other way of deciding what a name means** — Once all ordinary consumers use the one rule, remove the older mechanisms: name-string matching, walking up the parent chain, the global pool of bare names, the import retry, and the hand-written seed mirror that counts none/one/many for itself inside the bootstrap compiler. Owning that last one HERE, rather than in the node that defines the canonical decision, is deliberate — see namespace_kernel_authority_amendment_note. Why: Leaving them in place means one name can still get different answers from different code paths. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Delete the import lines, and the grammar that allows them** — Every import line goes, and import stops being something the language can express at all: writing one becomes a parse error. What a file depends on is worked out from the names it actually mentions. Why: While import still parses, a file can declare one set of dependencies while its references need another, and the two can disagree. That is the second source of truth this whole chain exists to remove, and it survives every earlier step. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Attach those requirements to the right type, not just the right position** — Connect each recorded slot to the exact declaration it belongs to, using the name resolution work above. Why: Matching by position or by short name attaches a requirement to the wrong type whenever two types share a name or the order shifts. [authority](docs/plans/v2-self-hosting.md) - [ ] **Generate those requirements from one place in the new generator** — The new Rust generator reads the record and the mapping through its existing path, rather than needing a special case per shape. Teaching the old generator the same thing is allowed only where the build-itself path is genuinely stuck without it, and even then it reads the same answer rather than working it out again. Why: Every shape currently needing its own hand-written patch is a shape someone has to fix again next time — and doing it in the generator we are deleting means doing it twice. [authority](src/v2/compiler/05_emit.dag) — requires all: pderive-source-slot-binding, v2-emitter-direct-rust-door From 79813e5db87e23502e6d1df42125979e8a6546fa Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 01:49:54 +0000 Subject: [PATCH 3/8] wip: rehome + regen --- ROADMAP.md | 6 +++--- dag/gunbc/roadmap_authority.dag | 2 ++ dag/std/occurrence_identity.dag | 12 +++++++----- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 8bf6b3632de..e6815b3f8a6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -16,14 +16,14 @@ The graph has eleven lanes: SCM compatibility · namespace · P-derive · observ - [ ] **Have the new compiler write out Rust source itself** — Read the source, work out the types, translate, and write real Rust to disk without the old generator deciding any of it. The old runner may still be what executes this; it just stops making the decisions. Why: Until the new compiler can write Rust on its own, every improvement has to go through the generator we are trying to delete. [authority](src/v2/compiler/self_host/candidate_generation.dag) - [ ] **Regenerate one already-tested module with the new generator** — Take a module that already has working tests, change only which generator produces it, and confirm it still passes the same tests including the deliberately broken one. Why: Proves the whole approach works on something real before anyone builds more on the assumption that it does. [authority](src/v2/compiler/self_host/frontier.dag) - [ ] **Build the new generator with itself, then do it again** — The generator, and only the generator: it writes and builds a native version of itself, that native version writes and builds another, and the second one needs nothing from the old compiler. The other parts of the compiler are still produced the old way at this point. Why: This is the moment the new generator stops being something we produce and becomes something that produces itself. [authority](src/v2/compiler/self_host/frontier.dag) — requires all: v2-emitter-first-behavioral-module, pderive-error-movement-receipt -- [ ] **Write the closing check for: One decision function, handed the candidates rather than finding them** — One executable check states when this is done, transcribed from the node's own bar: A malformed name refuses; a declaration of the wrong kind is excluded; supplied lists of nothing, one thing, and two things give unresolved, resolved, and ambiguous respectively; and no second place in the compiler makes that same decision. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **One decision function, handed the candidates rather than finding them** — Given a list of things a name could refer to, decide: none, exactly one, or more than one. Checking that the name and the declarations are well formed and of a kind that could match. NOT working out which declarations are in view — that is the next step. Why: Several lookup mechanisms exist today and they can disagree about the same name, which is how a program silently ends up meaning something other than what was written. Separating the decision from the search makes the decision reviewable on its own. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) +- [ ] **Write the closing check for: One canonical decision authority, handed the candidates rather than finding them** — One executable check states when this is done, transcribed from the node's own bar: Malformed occurrence transport refuses; a declaration of the wrong kind is excluded; a candidate given in the wrong role refuses, distinctly from one that is absent; the same declaration supplied twice refuses as a malformed population rather than counting as two; and supplied lists of nothing, one thing, and two distinct things give unresolved, resolved, and ambiguous respectively. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **One canonical decision authority, handed the candidates rather than finding them** — Given a list of things a name could refer to, decide: none, exactly one, or more than one. Checking that what it was handed is well formed and of a kind that could match. NOT working out which declarations are in view — that is the next step. Owns the canonical decision itself; does NOT own removing the older mechanisms that still decide it their own way. Why: Several lookup mechanisms exist today and they can disagree about the same name, which is how a program silently ends up meaning something other than what was written. Separating the decision from the search makes the decision reviewable on its own. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Work out what each name can see, and let that be what a file depends on** — From where a name is written, work outward to every declaration actually in view — including ordinary neighbours in the same file — respecting where things were written relative to each other. Then record, for each name, exactly which declaration answered it, and derive what each file depends on from those answers. Why: What a file depends on is currently written out by hand as a separate statement, so it can disagree with what the file actually refers to. Hundreds of files were patched by hand to keep those two in step, which is work that should never have existed: what a file depends on is a consequence of what it refers to, not a second thing to maintain. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Settle the names that genuinely could mean two things** — Using the real answers from the step before — not a count taken by searching text — go through every name that genuinely has more than one candidate and either refuse it on purpose or say which one is meant, by renaming or by writing it out in full. Why: Counting these by searching for text gives a number that is wrong in both directions, so it cannot tell anyone how much work is left or whether it is finished. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Make name lookup itself produce that one answer, and delete the old way** — The places that WORK OUT what a name refers to — type names, value names, and calls — produce their answer from the mechanisms built in the earlier steps, and the old habit of searching everything currently loaded is deleted rather than left beside them. This is the changeover of name lookup itself. What the rest of the compiler then does with that answer is the next step, not this one. Why: Until the old lookup is deleted the new mechanism is only advisory: both run, they can disagree, and the compiler can still quietly mean something other than what was written. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Make the rest of the compiler use only that one answer** — Type checking, call lookup, and argument-count checks all read the single decided answer. When the answer is 'unresolved' or 'ambiguous', they stop rather than carrying on with a guess. Why: While parts of the compiler still do their own lookup, the one shared rule is advisory rather than authoritative. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Do the same for imports** — Every place the newer compiler resolves an import or a dotted name reads the single decided answer, before the old mechanisms are deleted. Why: If imports are left behind, they become a second source of truth about what a name means — exactly the problem this whole chain exists to remove. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Delete every other way of deciding what a name means** — Once all ordinary consumers use the one rule, remove the older mechanisms: name-string matching, walking up the parent chain, the global pool of bare names, and the import retry. Why: Leaving them in place means one name can still get different answers from different code paths. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Delete every other way of deciding what a name means** — Once all ordinary consumers use the one rule, remove the older mechanisms: name-string matching, walking up the parent chain, the global pool of bare names, the import retry, and the hand-written seed mirror that counts none/one/many for itself inside the bootstrap compiler. Owning that last one HERE, rather than in the node that defines the canonical decision, is deliberate — see namespace_kernel_authority_amendment_note. Why: Leaving them in place means one name can still get different answers from different code paths. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Delete the import lines, and the grammar that allows them** — Every import line goes, and import stops being something the language can express at all: writing one becomes a parse error. What a file depends on is worked out from the names it actually mentions. Why: While import still parses, a file can declare one set of dependencies while its references need another, and the two can disagree. That is the second source of truth this whole chain exists to remove, and it survives every earlier step. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - [ ] **Attach those requirements to the right type, not just the right position** — Connect each recorded slot to the exact declaration it belongs to, using the name resolution work above. Why: Matching by position or by short name attaches a requirement to the wrong type whenever two types share a name or the order shifts. [authority](docs/plans/v2-self-hosting.md) - [ ] **Generate those requirements from one place in the new generator** — The new Rust generator reads the record and the mapping through its existing path, rather than needing a special case per shape. Teaching the old generator the same thing is allowed only where the build-itself path is genuinely stuck without it, and even then it reads the same answer rather than working it out again. Why: Every shape currently needing its own hand-written patch is a shape someone has to fix again next time — and doing it in the generator we are deleting means doing it twice. [authority](src/v2/compiler/05_emit.dag) — requires all: pderive-source-slot-binding, v2-emitter-direct-rust-door diff --git a/dag/gunbc/roadmap_authority.dag b/dag/gunbc/roadmap_authority.dag index c391f5a5012..fd35c8c628f 100644 --- a/dag/gunbc/roadmap_authority.dag +++ b/dag/gunbc/roadmap_authority.dag @@ -70,6 +70,8 @@ fn signed(by: String, works: RedControlEvidence, scope_equivalent: HandbackEvide SignedOff { by: by, works: works, scope_equivalent: scope_equivalent, as_expected: as_expected } } +data namespace_kernel_authority_amendment_note: String = "THE KERNEL BAR AS FIRST WRITTEN DEADLOCKED ITS OWN SUCCESSOR, and this amendment is the fix (operator ruling, 2026-08-01). The recut landed namespace-binding-kernel with a red_control ending 'no second place in the compiler makes that same decision' and a handback demanding 'evidence that nothing else decides the same question'. That bar cannot be met while the bootstrap seed keeps its own hand-written none/one/many mirror inside the v1 adapter, and that mirror cannot be deleted until generic emission carries the canonical carrier with its required bounds. But namespace-reference-derived-closure DEPENDS on the kernel. So on the day the recut merged, the next step was already unstartable for a reason that had nothing to do with what the next step needs: deriving candidate populations does not require the seed mirror to be gone, it requires the canonical decision to EXIST and be callable.\n\nTHE CUT IS AUTHORITY VERSUS RETIREMENT. Owning the canonical decision and removing every rival implementation of it are separate achievements with separate evidence, and fusing them made an acceptance bar that no amount of correct kernel work could satisfy. The kernel now owns being the one canonical authority; namespace-legacy-choice-retirement owns deleting the older mechanisms, and the seed mirror is named there as one of its rows. A new node was deliberately NOT minted: that node already reads 'delete every other way of deciding what a name means' and already inventories every remaining place that decides, so the mirror is a row it should always have carried, and minting a sibling would have been a second home for one concept.\n\nTHE ADAPTER IS NOT THEREBY BLESSED. It remains a declared scaffold with its dissolution trigger on its own carrier, counted and visible; moving WHERE its retirement is tracked does not lower what is owed. What changes is only that a temporary bootstrap realization no longer gates work that does not depend on it.\n\nTWO ACCEPTANCE SENTENCES WERE ALSO WRONG AND ARE CORRECTED HERE. The red_control said 'a malformed name refuses', but this boundary accepts occurrence identities and derives no name spelling at all, so it named a check no implementation executes — it now reads malformed occurrence TRANSPORT. And the bar now states the two refusals that distinguish a malformed supplied population from a real verdict: a candidate handed in the wrong role refuses distinctly from one that is absent, and the same declaration supplied twice refuses as malformed rather than counting as two. Only two DISTINCT declaration identities are ambiguity. Collapsing either of those into an ordinary verdict would let a defective candidate producer read as a legitimate naming answer, which is the state-space conflation this whole lane exists to remove." + data namespace_node_recut_note: String = "THE BROAD NAMESPACE NODE WAS TOO COARSE TO CLOSE HONESTLY, and the recut is the fix (operator ruling, 2026-07-31). namespace-canonical-binding previously combined two separable things in one boundary: working out which declarations a name can see, and calling one decision function on them. A check proving only the none/one/many decision could therefore have marked the whole node accepted while candidate derivation, reference-derived dependency closure, and the production changeover all remained unbuilt — an acceptance hazard, and the exact shape of rung inflation DESIGN 4b forbids, where a carrier named for the top rung occupies the bottom one.\\n\\nThree prerequisites now stand in front of it. namespace-binding-kernel owns the decision alone, on a candidate population SUPPLIED to it. namespace-reference-derived-closure owns structural exposure, authored order, the candidate index, exact reference-to-declaration binding, and the derived dependency edges. namespace-ambiguity-discharge settles the genuinely-many cases from real compilation. namespace-canonical-binding KEEPS ITS DURABLE IDENTITY and is now the final production changeover only, so historical links resolve and no identity is reused.\\n\\nWhy the split falls exactly there: dependency edges are a PROJECTION OF THE BINDING, not a second authored statement. A program that instead authored the missing edges by hand reached 400 of 771 files at roughly one import per CI floor before it was stopped; the unresolved population was evidence that binding and module discovery are coupled at the wrong boundary, not a backlog to grind down.\\n\\nTHE CHANGEOVER AND ITS SUCCESSOR ARE DISJOINT, and an earlier revision of this recut broke that (review 45649). It retitled namespace-canonical-binding as 'switch the whole compiler over' with a boundary reading 'every place that resolves a name', which swallowed namespace-bound-only-consumers underneath it — that node's own boundary is type checking, call lookup, and argument-count checks reading the decided answer. Overlapping boundaries are the §2 redundancy this document exists to remove, and they are worse on a roadmap than in code: completing the parent would already entail most of the child, so either the child can never be honestly accepted or the parent was silently read narrower than it was written. The cut is PRODUCER versus READER. The changeover node owns the sites that WORK OUT what a name refers to, and the deletion of the search-everything fallback behind them. Its successor owns the READERS of that answer and their obligation to stop on unresolved or ambiguous. Neither entails the other, and each can be accepted on its own evidence.\\n\\nONE EDGE WAS DELIBERATELY NOT CHANGED. pderive-source-slot-binding still depends on namespace-canonical-binding, which now means it waits for the whole production changeover. It plausibly needs only occurrence transport plus the binding kernel, but that is unproven: the dependency moves only once the focused P-derive consumer demonstrates it can operate on an exact supplied candidate population. Loosening it on the strength of the argument alone would be asserting a fact nobody has measured." data roadmap_execution_contract_frontier_note: String = "Every node below is ExecutionContractUnspecified: the declared, fail-closed migration frontier for gunbc.roadmap_execution_contract. A node states the exact validation operations that close it before the actuator will spawn for it, so dispatch refuses with a typed environment-admission diagnostic rather than guessing validations or dependencies. The prior binding lived on a pre-refresh operator-TODO node deleted by the 2026-07-27 authority refresh; re-binding is per-node roadmap authoring, never a merge-time default." diff --git a/dag/std/occurrence_identity.dag b/dag/std/occurrence_identity.dag index de23394635e..b7ecfb6cd51 100644 --- a/dag/std/occurrence_identity.dag +++ b/dag/std/occurrence_identity.dag @@ -83,29 +83,31 @@ type OccurrenceIdentityConsumerRedDebt { dissolve_on: String } +data occurrence_identity_consumer_red_debt_rehome_note: String = "THE SIX ROWS BELOW ALL NAMED namespace-canonical-binding AS OWNER, which was correct while that node was the single broad namespace node and is wrong now that the recut split it (operator ruling, 2026-08-01). A roadmap split leaves every row that pointed at the old node pointing at the wrong boundary, and a debt row whose owner cannot accept it is untracked in the way that matters: nobody discovering the node can tell whether the row is theirs.\n\nCLASSIFIED BY ACTUAL BOUNDARY, not by which node the row prose happens to mention. Five are parser or collector identity facts: rebuilt-reference identity preservation, one-occurrence collector dedupe, structurally-equal distinct occurrences, pattern-declaration reachability, and same-spelling parser declaration isolation. Each is decidable at the structural producer that builds candidate populations, so each is owned by namespace-reference-derived-closure. Note that one-occurrence-collector-dedupe is the SAME obligation the operator assigned to P2 candidate construction, collapsing repeated discovery of one identical full identity before the cardinality fold is called; it was already written here and simply pointed at the wrong node.\n\nONE ROW STAYS. inferred-target-anchor-identity requires an inferred target projection to stay anchored to the exact authored occurrence rather than a rebuilt shell Node, spelling, or SourceSpan. That is a property of the production projection path, not of building the candidate population, so it stays with namespace-canonical-binding, the production changeover. It is the row most likely to be re-argued, so the reasoning is written here rather than left implicit: the question that decides it is whether the receipt can execute before the production sites are switched over. For the other five it can; for this one it cannot.\n\nNo row was deleted, weakened, or given a new dissolution trigger by this rehome. Each dissolve_on still names the RED that retires it, and each still deletes in the PR that lands its receipt." + data occurrence_identity_consumer_red_debts: List = [ OccurrenceIdentityConsumerRedDebt { id: "rebuilt-reference-identity-preservation", required_receipt: "A production reference rebuild preserves the exact sidecar occurrence identity and containment path; dropping or reminting the occurrence must make the consumer RED.", - owner: "namespace-canonical-binding", + owner: "namespace-reference-derived-closure", dissolve_on: "DISSOLVE-ON: the rebuilt-reference production RED executes through the sidecar-consuming canonical binding boundary; delete this row in the same PR.", }, OccurrenceIdentityConsumerRedDebt { id: "one-occurrence-collector-dedupe", required_receipt: "Two observations of one authored occurrence enter the collector once by exact occurrence identity, never by spelling, SourceSpan, or Node structure.", - owner: "namespace-canonical-binding", + owner: "namespace-reference-derived-closure", dissolve_on: "DISSOLVE-ON: the occurrence-safe collector dedupe RED executes against the sidecar consumer; delete this row in the same PR.", }, OccurrenceIdentityConsumerRedDebt { id: "structurally-equal-distinct-occurrences", required_receipt: "Structurally equal and equally spelled authored occurrences with distinct IDs remain two collector entries.", - owner: "namespace-canonical-binding", + owner: "namespace-reference-derived-closure", dissolve_on: "DISSOLVE-ON: the distinct-authored-occurrences collector RED executes against the sidecar consumer; delete this row in the same PR.", }, OccurrenceIdentityConsumerRedDebt { id: "pattern-declaration-reachability", required_receipt: "Authoritative collection reaches every parser-minted pattern declaration occurrence, including nested pattern binders.", - owner: "namespace-canonical-binding", + owner: "namespace-reference-derived-closure", dissolve_on: "DISSOLVE-ON: the pattern-declaration reachability RED executes through the production occurrence collector; delete this row in the same PR.", }, OccurrenceIdentityConsumerRedDebt { @@ -117,7 +119,7 @@ data occurrence_identity_consumer_red_debts: List Date: Sat, 1 Aug 2026 01:58:44 +0000 Subject: [PATCH 4/8] Fix the rehome's two consumers: pin per-row owners in the witness, regen the seed --- .../occurrence_identity_witness_test.dag | 24 ++++++++++++------- src/v1/stage0/src/std_occurrence_identity.rs | 11 ++++++++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/dag/test/claim/occurrence_identity_witness_test.dag b/dag/test/claim/occurrence_identity_witness_test.dag index ab908809d18..9e6c7ddf492 100644 --- a/dag/test/claim/occurrence_identity_witness_test.dag +++ b/dag/test/claim/occurrence_identity_witness_test.dag @@ -127,11 +127,13 @@ test fn occurrence_identity_v2_shared_constructor_equivalence_holds() -> Bool { } } -fn occurrence_identity_consumer_red_debt_has(id: String) -> Bool { +data occurrence_identity_consumer_red_debt_owner_witness_note: String = "The expected owner is a PARAMETER, not a constant, because the roadmap recut split the single broad namespace node and the six rows no longer share one owner. Pinning each row to its own expected node is strictly stronger than the previous form, which only asserted that all six agreed with each other and would have stayed green if every row had been rehomed to the same wrong node." + +fn occurrence_identity_consumer_red_debt_has(id: String, owner: String) -> Bool { occurrence_identity_consumer_red_debts |> any(row => row.id == id && - row.owner == "namespace-canonical-binding" && + row.owner == owner && string_contains(row.dissolve_on, "DISSOLVE-ON:") ) } @@ -139,21 +141,27 @@ fn occurrence_identity_consumer_red_debt_has(id: String) -> Bool { test fn occurrence_identity_consumer_red_debt_is_counted_holds() -> Bool { (occurrence_identity_consumer_red_debts |> count) == 6 && occurrence_identity_consumer_red_debt_has( - id: "rebuilt-reference-identity-preservation" + id: "rebuilt-reference-identity-preservation", + owner: "namespace-reference-derived-closure", ) && occurrence_identity_consumer_red_debt_has( - id: "one-occurrence-collector-dedupe" + id: "one-occurrence-collector-dedupe", + owner: "namespace-reference-derived-closure", ) && occurrence_identity_consumer_red_debt_has( - id: "structurally-equal-distinct-occurrences" + id: "structurally-equal-distinct-occurrences", + owner: "namespace-reference-derived-closure", ) && occurrence_identity_consumer_red_debt_has( - id: "pattern-declaration-reachability" + id: "pattern-declaration-reachability", + owner: "namespace-reference-derived-closure", ) && occurrence_identity_consumer_red_debt_has( - id: "inferred-target-anchor-identity" + id: "inferred-target-anchor-identity", + owner: "namespace-canonical-binding", ) && occurrence_identity_consumer_red_debt_has( - id: "same-spelling-parser-declaration-isolation" + id: "same-spelling-parser-declaration-isolation", + owner: "namespace-reference-derived-closure", ) } diff --git a/src/v1/stage0/src/std_occurrence_identity.rs b/src/v1/stage0/src/std_occurrence_identity.rs index 5a5a5cf540f..afd9c0f3319 100644 --- a/src/v1/stage0/src/std_occurrence_identity.rs +++ b/src/v1/stage0/src/std_occurrence_identity.rs @@ -144,10 +144,19 @@ pub struct OccurrenceIdentityConsumerRedDebt { pub dissolve_on: String, } +pub fn occurrence_identity_consumer_red_debt_rehome_note() -> String { + thread_local! { + static CACHED: String = { + "THE SIX ROWS BELOW ALL NAMED namespace-canonical-binding AS OWNER, which was correct while that node was the single broad namespace node and is wrong now that the recut split it (operator ruling, 2026-08-01). A roadmap split leaves every row that pointed at the old node pointing at the wrong boundary, and a debt row whose owner cannot accept it is untracked in the way that matters: nobody discovering the node can tell whether the row is theirs.\n\nCLASSIFIED BY ACTUAL BOUNDARY, not by which node the row prose happens to mention. Five are parser or collector identity facts: rebuilt-reference identity preservation, one-occurrence collector dedupe, structurally-equal distinct occurrences, pattern-declaration reachability, and same-spelling parser declaration isolation. Each is decidable at the structural producer that builds candidate populations, so each is owned by namespace-reference-derived-closure. Note that one-occurrence-collector-dedupe is the SAME obligation the operator assigned to P2 candidate construction, collapsing repeated discovery of one identical full identity before the cardinality fold is called; it was already written here and simply pointed at the wrong node.\n\nONE ROW STAYS. inferred-target-anchor-identity requires an inferred target projection to stay anchored to the exact authored occurrence rather than a rebuilt shell Node, spelling, or SourceSpan. That is a property of the production projection path, not of building the candidate population, so it stays with namespace-canonical-binding, the production changeover. It is the row most likely to be re-argued, so the reasoning is written here rather than left implicit: the question that decides it is whether the receipt can execute before the production sites are switched over. For the other five it can; for this one it cannot.\n\nNo row was deleted, weakened, or given a new dissolution trigger by this rehome. Each dissolve_on still names the RED that retires it, and each still deletes in the PR that lands its receipt.".to_string() + }; + } + CACHED.with(|c: &String| c.clone()) +} + pub fn occurrence_identity_consumer_red_debts() -> Rc>> { thread_local! { static CACHED: Rc>> = { - serde_json::from_value(serde_json::json!([{"id": "rebuilt-reference-identity-preservation", "required_receipt": "A production reference rebuild preserves the exact sidecar occurrence identity and containment path; dropping or reminting the occurrence must make the consumer RED.", "owner": "namespace-canonical-binding", "dissolve_on": "DISSOLVE-ON: the rebuilt-reference production RED executes through the sidecar-consuming canonical binding boundary; delete this row in the same PR."}, {"id": "one-occurrence-collector-dedupe", "required_receipt": "Two observations of one authored occurrence enter the collector once by exact occurrence identity, never by spelling, SourceSpan, or Node structure.", "owner": "namespace-canonical-binding", "dissolve_on": "DISSOLVE-ON: the occurrence-safe collector dedupe RED executes against the sidecar consumer; delete this row in the same PR."}, {"id": "structurally-equal-distinct-occurrences", "required_receipt": "Structurally equal and equally spelled authored occurrences with distinct IDs remain two collector entries.", "owner": "namespace-canonical-binding", "dissolve_on": "DISSOLVE-ON: the distinct-authored-occurrences collector RED executes against the sidecar consumer; delete this row in the same PR."}, {"id": "pattern-declaration-reachability", "required_receipt": "Authoritative collection reaches every parser-minted pattern declaration occurrence, including nested pattern binders.", "owner": "namespace-canonical-binding", "dissolve_on": "DISSOLVE-ON: the pattern-declaration reachability RED executes through the production occurrence collector; delete this row in the same PR."}, {"id": "inferred-target-anchor-identity", "required_receipt": "An inferred target projection remains anchored to the exact authored occurrence instead of a rebuilt shell Node, spelling, or SourceSpan.", "owner": "namespace-canonical-binding", "dissolve_on": "DISSOLVE-ON: the inferred-target anchor RED executes through the production sidecar consumer; delete this row in the same PR."}, {"id": "same-spelling-parser-declaration-isolation", "required_receipt": "Same-spelling declarations and references in sibling match arms, nested lets, lambdas, and parameters retain distinct authored identities and containment paths without overwrite.", "owner": "namespace-canonical-binding", "dissolve_on": "DISSOLVE-ON: the full same-spelling parser-to-binding RED matrix executes at the canonical binding boundary; delete this row in the same PR."}])) + serde_json::from_value(serde_json::json!([{"id": "rebuilt-reference-identity-preservation", "required_receipt": "A production reference rebuild preserves the exact sidecar occurrence identity and containment path; dropping or reminting the occurrence must make the consumer RED.", "owner": "namespace-reference-derived-closure", "dissolve_on": "DISSOLVE-ON: the rebuilt-reference production RED executes through the sidecar-consuming canonical binding boundary; delete this row in the same PR."}, {"id": "one-occurrence-collector-dedupe", "required_receipt": "Two observations of one authored occurrence enter the collector once by exact occurrence identity, never by spelling, SourceSpan, or Node structure.", "owner": "namespace-reference-derived-closure", "dissolve_on": "DISSOLVE-ON: the occurrence-safe collector dedupe RED executes against the sidecar consumer; delete this row in the same PR."}, {"id": "structurally-equal-distinct-occurrences", "required_receipt": "Structurally equal and equally spelled authored occurrences with distinct IDs remain two collector entries.", "owner": "namespace-reference-derived-closure", "dissolve_on": "DISSOLVE-ON: the distinct-authored-occurrences collector RED executes against the sidecar consumer; delete this row in the same PR."}, {"id": "pattern-declaration-reachability", "required_receipt": "Authoritative collection reaches every parser-minted pattern declaration occurrence, including nested pattern binders.", "owner": "namespace-reference-derived-closure", "dissolve_on": "DISSOLVE-ON: the pattern-declaration reachability RED executes through the production occurrence collector; delete this row in the same PR."}, {"id": "inferred-target-anchor-identity", "required_receipt": "An inferred target projection remains anchored to the exact authored occurrence instead of a rebuilt shell Node, spelling, or SourceSpan.", "owner": "namespace-canonical-binding", "dissolve_on": "DISSOLVE-ON: the inferred-target anchor RED executes through the production sidecar consumer; delete this row in the same PR."}, {"id": "same-spelling-parser-declaration-isolation", "required_receipt": "Same-spelling declarations and references in sibling match arms, nested lets, lambdas, and parameters retain distinct authored identities and containment paths without overwrite.", "owner": "namespace-reference-derived-closure", "dissolve_on": "DISSOLVE-ON: the full same-spelling parser-to-binding RED matrix executes at the canonical binding boundary; delete this row in the same PR."}])) .expect("valid data definition") }; } From 13ff522094e43283aff140d9d92445995d00e8c8 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 03:31:22 +0000 Subject: [PATCH 5/8] WIP: self host/namespace/type check --- dag/tools/kernel_digest_probe.dag | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 dag/tools/kernel_digest_probe.dag diff --git a/dag/tools/kernel_digest_probe.dag b/dag/tools/kernel_digest_probe.dag new file mode 100644 index 00000000000..43adb9fb4a0 --- /dev/null +++ b/dag/tools/kernel_digest_probe.dag @@ -0,0 +1,18 @@ +module tools.kernel_digest_probe + +import std.types { ContentHash } +import gunbc.roadmap_authority { active_roadmap_nodes } +import gunbc.roadmap_model { RoadmapNode, node_criteria_digest } + +fn digest_for(id: String) -> List { + active_roadmap_nodes() + |> filter(n => n.node == id) + |> map(n => match node_criteria_digest(rn: n) { + Present { value } => value + Absent => "ABSENT" + }) +} + +fn main() -> List { + digest_for(id: "namespace-binding-kernel") +} From c07bf925d1c8b9295c26b3177ac9c7c88a758085 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 03:40:33 +0000 Subject: [PATCH 6/8] Accept the namespace binding kernel, so P2a is honestly startable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #7508 merged the supplied-candidate binding kernel, resolving P2a's implementation dependency — but no RoadmapAcceptanceReceipt existed, so the roadmap still said its own prerequisite was unaccepted. node_is_startable reads exactly that, so P2a was being worked on while not startable under the roadmap's own definition. Two truths for one fact. The criteria digest 2eba4715cdef42eb is the post-#7530 value and was DERIVED BY EXECUTION, not transcribed: #7530 rewrote this node's boundary, red_control and out_of_scope to cut authority from retirement, and roadmap_criteria_digest covers brief + red_control + handback, so any pre-amendment digest is stale by construction. node_criteria_digest was run against the live node on this tree. Proven end-to-end rather than by inspection: namespace-binding-kernel goes 2 -> 0 occurrences in the regenerated ROADMAP.md, which only happens if the receipt matches the node and the node leaves the active set. A wrong digest leaves the row in place. witness_stale_criteria_digest_refuses_acceptance returns true. Also removes dag/tools/kernel_digest_probe.dag, a throwaway probe that the autosave committed while it was being used to compute the digest. Co-Authored-By: Claude Opus 5 (1M context) --- ROADMAP.md | 40 +++++++++++++++---------------- dag/gunbc/roadmap_authority.dag | 20 ++++++++++++++++ dag/tools/kernel_digest_probe.dag | 18 -------------- 3 files changed, 40 insertions(+), 38 deletions(-) delete mode 100644 dag/tools/kernel_digest_probe.dag diff --git a/ROADMAP.md b/ROADMAP.md index 8b567b1b708..ca2e00d9b61 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -10,31 +10,31 @@ Parked context (non-dispatchable): [compiler algorithm survey](docs/plans/compil The graph has eleven lanes: SCM compatibility · namespace · P-derive · observation · placement · v1 exit (four finish lines: compiler fixed point, interpreter deleted, products v1-free, zero hand-maintained Rust) · shell · roadmap runtime · fleet/hygiene · hermetic toolchain · compiler-guarantee (the DESIGN §4b ladder climbs; rung STATE lives in the guarantee claims carrier and is emitted, never restated in tickets — [gap analysis](docs/plans/compiler-guarantee-recovery-gap-analysis.md)). The three independent SCM R0 models, the separate P−1 evidence carrier, and R1 compatibility-shape extraction are landed prerequisites; P0's user-contract and layered landing spine is the active SCM slice, while the P1 semantic kernel and later product lanes remain parked. The toolchain pin model is an independent root. The P-derive receipt feeds the emitter fixed point, so the v1 chain nests under it. Node fields define boundary, first slice, RED control, exclusions, owner, and handback. -**Focused view — the v1 deletion lane + the guarantee-ladder climbs.** 38 active deliverable(s) in other lanes are declared in the authority and hidden here; nothing is deleted or parked by focusing. Clear `roadmap_focus_selection` in `dag/gunbc/roadmap_authority.dag` to restore the full page. 11 of the rows below are NOT lane deliverables — they are prerequisites pulled in from other lanes because lane work is blocked on them: namespace-canonical-binding, namespace-ambiguity-discharge, namespace-reference-derived-closure, namespace-binding-kernel, namespace-binding-kernel-closing-contract, v1-seed-honesty-decision, v1-seed-honesty-decision-closing-contract, namespace-legacy-choice-retirement, namespace-v2-import-completion, namespace-bound-only-consumers, namespace-import-deletion. Hidden lanes remain readable through their carriers: [docs/plans/dag-scm-design.md](docs/plans/dag-scm-design.md) · [dag/gunbc/plans/cli_run_hollowing_plan.dag](dag/gunbc/plans/cli_run_hollowing_plan.dag) · [docs/plans/progress-observation-design.md](docs/plans/progress-observation-design.md) · [docs/plans/inner-cost-lanes-scoping.md](docs/plans/inner-cost-lanes-scoping.md) · [docs/plans/ci-two-tier-placement-redesign.md](docs/plans/ci-two-tier-placement-redesign.md) · [docs/plans/shell-to-dag-residual-census-and-arc-completion.md](docs/plans/shell-to-dag-residual-census-and-arc-completion.md) · [docs/plans/roadmap-workspace-ux-plan.md](docs/plans/roadmap-workspace-ux-plan.md) · [docs/plans/host-converge-inventory.md](docs/plans/host-converge-inventory.md) · [docs/runbooks/bmc-redfish-operator-access.md](docs/runbooks/bmc-redfish-operator-access.md) · [docs/plans/host-effect-orchestration.md](docs/plans/host-effect-orchestration.md) · [docs/plans/gunbc-served-dashboard-design.md](docs/plans/gunbc-served-dashboard-design.md) · [docs/plans/fleet-acceptance-criteria.md](docs/plans/fleet-acceptance-criteria.md) · [docs/plans/hermetic-tool-provisioning-design.md](docs/plans/hermetic-tool-provisioning-design.md). +**Focused view — the v1 deletion lane + the guarantee-ladder climbs.** 38 active deliverable(s) in other lanes are declared in the authority and hidden here; nothing is deleted or parked by focusing. Clear `roadmap_focus_selection` in `dag/gunbc/roadmap_authority.dag` to restore the full page. 10 of the rows below are NOT lane deliverables — they are prerequisites pulled in from other lanes because lane work is blocked on them: namespace-canonical-binding, namespace-ambiguity-discharge, namespace-reference-derived-closure, namespace-reference-derived-closure-closing-contract, v1-seed-honesty-decision, v1-seed-honesty-decision-closing-contract, namespace-legacy-choice-retirement, namespace-v2-import-completion, namespace-bound-only-consumers, namespace-import-deletion. Hidden lanes remain readable through their carriers: [docs/plans/dag-scm-design.md](docs/plans/dag-scm-design.md) · [dag/gunbc/plans/cli_run_hollowing_plan.dag](dag/gunbc/plans/cli_run_hollowing_plan.dag) · [docs/plans/progress-observation-design.md](docs/plans/progress-observation-design.md) · [docs/plans/inner-cost-lanes-scoping.md](docs/plans/inner-cost-lanes-scoping.md) · [docs/plans/ci-two-tier-placement-redesign.md](docs/plans/ci-two-tier-placement-redesign.md) · [docs/plans/shell-to-dag-residual-census-and-arc-completion.md](docs/plans/shell-to-dag-residual-census-and-arc-completion.md) · [docs/plans/roadmap-workspace-ux-plan.md](docs/plans/roadmap-workspace-ux-plan.md) · [docs/plans/host-converge-inventory.md](docs/plans/host-converge-inventory.md) · [docs/runbooks/bmc-redfish-operator-access.md](docs/runbooks/bmc-redfish-operator-access.md) · [docs/plans/host-effect-orchestration.md](docs/plans/host-effect-orchestration.md) · [docs/plans/gunbc-served-dashboard-design.md](docs/plans/gunbc-served-dashboard-design.md) · [docs/plans/fleet-acceptance-criteria.md](docs/plans/fleet-acceptance-criteria.md) · [docs/plans/hermetic-tool-provisioning-design.md](docs/plans/hermetic-tool-provisioning-design.md). - [ ] **Record which generator actually produced each result** — Every result says which generator made it and how far it got, as two separate facts. Claiming the new generator made something, without the evidence that it did, has to be impossible to write down. Why: Right now everything is made by the old generator, and the count we track reads as if the new one made it. We are measuring the wrong thing, so we cannot tell how far along we are. [authority](src/v2/compiler/self_host/frontier.dag) - [ ] **Have the new compiler write out Rust source itself** — Read the source, work out the types, translate, and write real Rust to disk without the old generator deciding any of it. The old runner may still be what executes this; it just stops making the decisions. Why: Until the new compiler can write Rust on its own, every improvement has to go through the generator we are trying to delete. [authority](src/v2/compiler/self_host/candidate_generation.dag) - [ ] **Regenerate one already-tested module with the new generator** — Take a module that already has working tests, change only which generator produces it, and confirm it still passes the same tests including the deliberately broken one. Why: Proves the whole approach works on something real before anyone builds more on the assumption that it does. [authority](src/v2/compiler/self_host/frontier.dag) - [ ] **Build the new generator with itself, then do it again** — The generator, and only the generator: it writes and builds a native version of itself, that native version writes and builds another, and the second one needs nothing from the old compiler. The other parts of the compiler are still produced the old way at this point. Why: This is the moment the new generator stops being something we produce and becomes something that produces itself. [authority](src/v2/compiler/self_host/frontier.dag) — requires all: v2-emitter-first-behavioral-module, pderive-error-movement-receipt -- [ ] **Write the closing check for: One canonical decision authority, handed the candidates rather than finding them** — One executable check states when this is done, transcribed from the node's own bar: Malformed occurrence transport refuses; a declaration of the wrong kind is excluded; a candidate given in the wrong role refuses, distinctly from one that is absent; the same declaration supplied twice refuses as a malformed population rather than counting as two; and supplied lists of nothing, one thing, and two distinct things give unresolved, resolved, and ambiguous respectively. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **One canonical decision authority, handed the candidates rather than finding them** — Given a list of things a name could refer to, decide: none, exactly one, or more than one. Checking that what it was handed is well formed and of a kind that could match. NOT working out which declarations are in view — that is the next step. Owns the canonical decision itself; does NOT own removing the older mechanisms that still decide it their own way. Why: Several lookup mechanisms exist today and they can disagree about the same name, which is how a program silently ends up meaning something other than what was written. Separating the decision from the search makes the decision reviewable on its own. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Work out what each name can see, and let that be what a file depends on** — From where a name is written, work outward to every declaration actually in view — including ordinary neighbours in the same file — respecting where things were written relative to each other. Then record, for each name, exactly which declaration answered it, and derive what each file depends on from those answers. Why: What a file depends on is currently written out by hand as a separate statement, so it can disagree with what the file actually refers to. Hundreds of files were patched by hand to keep those two in step, which is work that should never have existed: what a file depends on is a consequence of what it refers to, not a second thing to maintain. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Settle the names that genuinely could mean two things** — Using the real answers from the step before — not a count taken by searching text — go through every name that genuinely has more than one candidate and either refuse it on purpose or say which one is meant, by renaming or by writing it out in full. Why: Counting these by searching for text gives a number that is wrong in both directions, so it cannot tell anyone how much work is left or whether it is finished. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Make name lookup itself produce that one answer, and delete the old way** — The places that WORK OUT what a name refers to — type names, value names, and calls — produce their answer from the mechanisms built in the earlier steps, and the old habit of searching everything currently loaded is deleted rather than left beside them. This is the changeover of name lookup itself. What the rest of the compiler then does with that answer is the next step, not this one. Why: Until the old lookup is deleted the new mechanism is only advisory: both run, they can disagree, and the compiler can still quietly mean something other than what was written. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Make the rest of the compiler use only that one answer** — Type checking, call lookup, and argument-count checks all read the single decided answer. When the answer is 'unresolved' or 'ambiguous', they stop rather than carrying on with a guess. Why: While parts of the compiler still do their own lookup, the one shared rule is advisory rather than authoritative. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Do the same for imports** — Every place the newer compiler resolves an import or a dotted name reads the single decided answer, before the old mechanisms are deleted. Why: If imports are left behind, they become a second source of truth about what a name means — exactly the problem this whole chain exists to remove. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Delete every other way of deciding what a name means** — Once all ordinary consumers use the one rule, remove the older mechanisms: name-string matching, walking up the parent chain, the global pool of bare names, the import retry, and the hand-written seed mirror that counts none/one/many for itself inside the bootstrap compiler. Owning that last one HERE, rather than in the node that defines the canonical decision, is deliberate — see namespace_kernel_authority_amendment_note. Why: Leaving them in place means one name can still get different answers from different code paths. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Delete the import lines, and the grammar that allows them** — Every import line goes, and import stops being something the language can express at all: writing one becomes a parse error. What a file depends on is worked out from the names it actually mentions. Why: While import still parses, a file can declare one set of dependencies while its references need another, and the two can disagree. That is the second source of truth this whole chain exists to remove, and it survives every earlier step. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Attach those requirements to the right type, not just the right position** — Connect each recorded slot to the exact declaration it belongs to, using the name resolution work above. Why: Matching by position or by short name attaches a requirement to the wrong type whenever two types share a name or the order shifts. [authority](docs/plans/v2-self-hosting.md) - - [ ] **Generate those requirements from one place in the new generator** — The new Rust generator reads the record and the mapping through its existing path, rather than needing a special case per shape. Teaching the old generator the same thing is allowed only where the build-itself path is genuinely stuck without it, and even then it reads the same answer rather than working it out again. Why: Every shape currently needing its own hand-written patch is a shape someone has to fix again next time — and doing it in the generator we are deleting means doing it twice. [authority](src/v2/compiler/05_emit.dag) — requires all: pderive-source-slot-binding, v2-emitter-direct-rust-door - - [ ] **Count the compiler errors before and after, to prove it helped** — Re-run the same set of modules and compare the error counts and kinds. Why: A fix that makes one example compile can leave the real number untouched; without the count we would not know. [authority](docs/plans/v2-self-hosting.md) - - [ ] **Get the rest of the compiler to rebuild itself too** — With the generator already building itself, extend the same two-round test to everything else the compiler does — reading source, working out names, working out types, translating — until the whole thing generates its own source, that source is built, and the result behaves the same. Behaviour has to match; the bytes do not. Why: This is the whole point of the project: until the compiler can build itself end to end, there is nothing to replace the old one with. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: pderive-error-movement-receipt, v2-emitter-native-bootstrap - - [ ] **Switch the build over to the new compiler** — The real build runs on the new path, with cold comparisons against the old one first. Why: Moves the thing that actually has to work every day, instead of proving a replacement nobody uses. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-emitter-fixed-point, v1-honest-frontier, v1-verification-ledger - - [ ] **Run everything with the old generator taken away** — The checks and the build use what the new generator produced, nothing falls back to the old generator, and nothing is left calling it. Why: Separates deleting the old generator from deleting the old runner, so the generator can go first instead of waiting on everything else. [authority](src/v1/05_emit_rust.dag) - - [ ] **Delete the old Rust generator** — The old generator itself is removed, along with the one Rust file it generated to run itself, the Rust option in the old build path, and the fixtures, helpers and measurements that existed only for it. What produced a file in the past does not decide whether it goes: any Rust the new generator now produces stays exactly where it is. Why: Removes the second set of Rust-generation rules for good — the split the shared rules were meant to end. [authority](src/v1/05_emit_rust.dag) - - [ ] **Build the products with the old runner left out entirely** — A rehearsal: build without the old runner included at all, and run the products and the build both cold and warm. Why: Proves nothing still reaches for it, which is the thing that actually makes deletion safe. [authority](dag/gunbc/v1_deletion_plan.dag) - - [ ] **Delete the old runner** — Delete it and everything now unreachable, in one change, once the rehearsal above holds. This is about the runner only — not the rest of the old code, and not hand-written Rust generally. Why: Removes the second, slower way of running everything, and with it the possibility of the two disagreeing. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-interpreter-quarantine, v1-emitter-delete - - [ ] **Build and run every product without the old code present** — With the old runner gone and the hand-written queue worked through, the products build and run with the old directory absent. Anything that must stay gets an explicit reason. Why: Separates 'the products no longer need the old code' from 'the old runner is gone', so neither claim waits on the other's leftovers. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-interpreter-delete, v1-hand-queue-drain +- [ ] **Write the closing check for: Work out what each name can see, and let that be what a file depends on** — One executable check states when this is done, transcribed from the node's own bar: A neighbour in the same file is in view; something inside a different branch of a decision is not; something written further down is not; the same name written twice in genuinely different places stays ambiguous; repeated mentions of one thing collapse to a single dependency; and nothing depends on an unrelated file happening to be loaded at the same time. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Work out what each name can see, and let that be what a file depends on** — From where a name is written, work outward to every declaration actually in view — including ordinary neighbours in the same file — respecting where things were written relative to each other. Then record, for each name, exactly which declaration answered it, and derive what each file depends on from those answers. Why: What a file depends on is currently written out by hand as a separate statement, so it can disagree with what the file actually refers to. Hundreds of files were patched by hand to keep those two in step, which is work that should never have existed: what a file depends on is a consequence of what it refers to, not a second thing to maintain. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Settle the names that genuinely could mean two things** — Using the real answers from the step before — not a count taken by searching text — go through every name that genuinely has more than one candidate and either refuse it on purpose or say which one is meant, by renaming or by writing it out in full. Why: Counting these by searching for text gives a number that is wrong in both directions, so it cannot tell anyone how much work is left or whether it is finished. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Make name lookup itself produce that one answer, and delete the old way** — The places that WORK OUT what a name refers to — type names, value names, and calls — produce their answer from the mechanisms built in the earlier steps, and the old habit of searching everything currently loaded is deleted rather than left beside them. This is the changeover of name lookup itself. What the rest of the compiler then does with that answer is the next step, not this one. Why: Until the old lookup is deleted the new mechanism is only advisory: both run, they can disagree, and the compiler can still quietly mean something other than what was written. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Make the rest of the compiler use only that one answer** — Type checking, call lookup, and argument-count checks all read the single decided answer. When the answer is 'unresolved' or 'ambiguous', they stop rather than carrying on with a guess. Why: While parts of the compiler still do their own lookup, the one shared rule is advisory rather than authoritative. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Do the same for imports** — Every place the newer compiler resolves an import or a dotted name reads the single decided answer, before the old mechanisms are deleted. Why: If imports are left behind, they become a second source of truth about what a name means — exactly the problem this whole chain exists to remove. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Delete every other way of deciding what a name means** — Once all ordinary consumers use the one rule, remove the older mechanisms: name-string matching, walking up the parent chain, the global pool of bare names, the import retry, and the hand-written seed mirror that counts none/one/many for itself inside the bootstrap compiler. Owning that last one HERE, rather than in the node that defines the canonical decision, is deliberate — see namespace_kernel_authority_amendment_note. Why: Leaving them in place means one name can still get different answers from different code paths. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Delete the import lines, and the grammar that allows them** — Every import line goes, and import stops being something the language can express at all: writing one becomes a parse error. What a file depends on is worked out from the names it actually mentions. Why: While import still parses, a file can declare one set of dependencies while its references need another, and the two can disagree. That is the second source of truth this whole chain exists to remove, and it survives every earlier step. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Attach those requirements to the right type, not just the right position** — Connect each recorded slot to the exact declaration it belongs to, using the name resolution work above. Why: Matching by position or by short name attaches a requirement to the wrong type whenever two types share a name or the order shifts. [authority](docs/plans/v2-self-hosting.md) + - [ ] **Generate those requirements from one place in the new generator** — The new Rust generator reads the record and the mapping through its existing path, rather than needing a special case per shape. Teaching the old generator the same thing is allowed only where the build-itself path is genuinely stuck without it, and even then it reads the same answer rather than working it out again. Why: Every shape currently needing its own hand-written patch is a shape someone has to fix again next time — and doing it in the generator we are deleting means doing it twice. [authority](src/v2/compiler/05_emit.dag) — requires all: pderive-source-slot-binding, v2-emitter-direct-rust-door + - [ ] **Count the compiler errors before and after, to prove it helped** — Re-run the same set of modules and compare the error counts and kinds. Why: A fix that makes one example compile can leave the real number untouched; without the count we would not know. [authority](docs/plans/v2-self-hosting.md) + - [ ] **Get the rest of the compiler to rebuild itself too** — With the generator already building itself, extend the same two-round test to everything else the compiler does — reading source, working out names, working out types, translating — until the whole thing generates its own source, that source is built, and the result behaves the same. Behaviour has to match; the bytes do not. Why: This is the whole point of the project: until the compiler can build itself end to end, there is nothing to replace the old one with. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: pderive-error-movement-receipt, v2-emitter-native-bootstrap + - [ ] **Switch the build over to the new compiler** — The real build runs on the new path, with cold comparisons against the old one first. Why: Moves the thing that actually has to work every day, instead of proving a replacement nobody uses. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-emitter-fixed-point, v1-honest-frontier, v1-verification-ledger + - [ ] **Run everything with the old generator taken away** — The checks and the build use what the new generator produced, nothing falls back to the old generator, and nothing is left calling it. Why: Separates deleting the old generator from deleting the old runner, so the generator can go first instead of waiting on everything else. [authority](src/v1/05_emit_rust.dag) + - [ ] **Delete the old Rust generator** — The old generator itself is removed, along with the one Rust file it generated to run itself, the Rust option in the old build path, and the fixtures, helpers and measurements that existed only for it. What produced a file in the past does not decide whether it goes: any Rust the new generator now produces stays exactly where it is. Why: Removes the second set of Rust-generation rules for good — the split the shared rules were meant to end. [authority](src/v1/05_emit_rust.dag) + - [ ] **Build the products with the old runner left out entirely** — A rehearsal: build without the old runner included at all, and run the products and the build both cold and warm. Why: Proves nothing still reaches for it, which is the thing that actually makes deletion safe. [authority](dag/gunbc/v1_deletion_plan.dag) + - [ ] **Delete the old runner** — Delete it and everything now unreachable, in one change, once the rehearsal above holds. This is about the runner only — not the rest of the old code, and not hand-written Rust generally. Why: Removes the second, slower way of running everything, and with it the possibility of the two disagreeing. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-interpreter-quarantine, v1-emitter-delete + - [ ] **Build and run every product without the old code present** — With the old runner gone and the hand-written queue worked through, the products build and run with the old directory absent. Anything that must stay gets an explicit reason. Why: Separates 'the products no longer need the old code' from 'the old runner is gone', so neither claim waits on the other's leftovers. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-interpreter-delete, v1-hand-queue-drain + - [ ] **Get hand-written Rust in this repository down to zero** — Every Rust file we track is either generated from a description or deleted. Hand-maintaining Rust is a stage we are passing through, not where we stop. Why: Every hand-written file is a permanent tax: the behaviour and its description have to be kept in step by hand, forever. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-products-v1-free, v1-test-migration, v1-seed-honesty-decision, namespace-legacy-choice-retirement, namespace-import-deletion - [ ] **Write the closing check for: Keep an honest list of what still depends on the old compiler** — One executable check states when this is done, transcribed from the node's own bar: An unlisted module, an unlisted dependency, or a listed test that never actually runs makes the list red. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](dag/gunbc/v1_deletion_plan.dag) - [ ] **Keep an honest list of what still depends on the old compiler** — Every module is either generated by the new compiler or explicitly kept on the old one with a reason and a plan to move. Why: Turns a vague sense of 'mostly done' into a finite, countable list someone can work through. [authority](dag/gunbc/v1_deletion_plan.dag) - [ ] **Write the closing check for: Compute things once and reuse the result** — One executable check states when this is done, transcribed from the node's own bar: Wrong content, a missing entry, and a full store each refuse rather than quietly falling back to recomputing. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/witness-realization-plan.md) diff --git a/dag/gunbc/roadmap_authority.dag b/dag/gunbc/roadmap_authority.dag index 10bd69bd9de..b71a7032dce 100644 --- a/dag/gunbc/roadmap_authority.dag +++ b/dag/gunbc/roadmap_authority.dag @@ -1552,6 +1552,8 @@ fn declared_roadmap_edges() -> List { data criteria_digest_reword_note: String = "Both digests below moved on 2026-07-29 because every row was rewritten in plain language, and the digest covers the brief, the pass/fail test, and what gets handed back. Worth being blunt about the limit this exposes: the digest cannot tell a rewording from a genuine change to the bar — it only knows the words moved. Nothing about what these two rows had to prove changed, and the evidence each names is the same run and the same files as before, so re-pinning is correct here. But that judgement was a person's, not the mechanism's, and a reviewer should treat any digest change as a prompt to re-read the row rather than as a formality. This is the same gap roadmap_acceptance_receipt_note already names: until a row's pass/fail test points at the test that discharges it, the link between words and evidence is asserted rather than derived." +data namespace_binding_kernel_acceptance_note: String = "STATUS RECONCILIATION, NOT NEW WORK (operator ruling 2026-08-01). PR #7508 merged the supplied-candidate binding kernel, so the IMPLEMENTATION dependency for namespace-reference-derived-closure was resolved, but no acceptance receipt existed — leaving the roadmap saying its own prerequisite was unaccepted while the code depended on it. Two different truths for the same fact, which is exactly what node_is_startable reads, so P2a was being worked on while not honestly startable under the roadmap's own definition. The digest 2eba4715cdef42eb is the POST-#7530 value: #7530 rewrote this node's boundary, red_control and out_of_scope to cut authority from retirement, and roadmap_criteria_digest covers brief plus red_control plus handback, so the pre-amendment digest is stale by construction. It was DERIVED BY EXECUTION rather than transcribed — node_criteria_digest was run against the live node on this tree, not copied from a prior receipt or computed by hand. The red control names the malformed-transport refusal because that is the first arm #7530's amended row requires; the sibling arms (absent versus wrong-role identity distinct, duplicate supplied identity refused, wrong-category excluded, zero/one/two giving unbound/bound/ambiguous, exact declaration identity surviving, canonical fold invoked once) all execute in the same witness module and were green on the exact-head run that merged #7508." + data scm_prerequisite_acceptance_note: String = "The operator explicitly accepted the three independent R0 models and the separate P−1 evidence carrier, then accepted R1 after merging its three-realization compatibility extraction. R1 depends only on the three R0 receipts; P0 is the first source-intent slice that consumes both accepted R1 and accepted P−1. These receipts preserve every prerequisite and handback as durable facts. Replacing accepted rows with their child would erase history and violate roadmap_acceptance_receipt_note." fn chash(s: String) -> ContentHash { s } @@ -1670,6 +1672,24 @@ fn roadmap_acceptance_receipts() -> List { accepted_by: "operator", accepted_on: "2026-07-28", }, + RoadmapAcceptanceReceipt { + node: nid(s: "namespace-binding-kernel"), + criteria_digest: chash(s: "2eba4715cdef42eb"), + red_control: RedControlExecuted { + witness_module: "test.claim.occurrence_binding_resolve_witness_test", + witness_fn: "occurrence_binding_resolve_malformed_transport_refuses_holds", + executed_on: "2026-08-01", + }, + handback: HandbackDelivered { + first_artifact: "dag/std/occurrence_binding_resolve.dag", + further_artifacts: [ + "dag/test/claim/occurrence_binding_resolve_witness_test.dag", + "src/v1/04_occurrence_binding.dag", + ], + }, + accepted_by: "operator", + accepted_on: "2026-08-01", + }, RoadmapAcceptanceReceipt { node: nid(s: "pderive-typesafe-nullary-reflection"), criteria_digest: chash(s: "dac83172a613e659"), diff --git a/dag/tools/kernel_digest_probe.dag b/dag/tools/kernel_digest_probe.dag deleted file mode 100644 index 43adb9fb4a0..00000000000 --- a/dag/tools/kernel_digest_probe.dag +++ /dev/null @@ -1,18 +0,0 @@ -module tools.kernel_digest_probe - -import std.types { ContentHash } -import gunbc.roadmap_authority { active_roadmap_nodes } -import gunbc.roadmap_model { RoadmapNode, node_criteria_digest } - -fn digest_for(id: String) -> List { - active_roadmap_nodes() - |> filter(n => n.node == id) - |> map(n => match node_criteria_digest(rn: n) { - Present { value } => value - Absent => "ABSENT" - }) -} - -fn main() -> List { - digest_for(id: "namespace-binding-kernel") -} From b8f3ec45b57a75e2516c014756a1a6d384f11975 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 04:54:49 +0000 Subject: [PATCH 7/8] Merge origin/main; regenerate ROADMAP.md from the authority Six commits behind. roadmap_authority.dag auto-merged cleanly this time; only ROADMAP.md conflicted, and it is a generated artifact, so it was regenerated from the authority rather than hand-resolved. Hand-resolving a generated file makes the committed bytes disagree with their source and detonates on the next .dag PR. Verified after: the kernel row stays absent from ROADMAP.md so the acceptance receipt remains effective, and no conflict markers survive. Co-Authored-By: Claude Opus 5 (1M context) --- ROADMAP.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d9166752e76..99bef0538e6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -10,31 +10,31 @@ Parked context (non-dispatchable): [compiler algorithm survey](docs/plans/compil The graph has eleven lanes: SCM compatibility · namespace · P-derive · observation · placement · v1 exit (four finish lines: compiler fixed point, interpreter deleted, products v1-free, zero hand-maintained Rust) · shell · roadmap runtime · fleet/hygiene · hermetic toolchain · compiler-guarantee (the DESIGN §4b ladder climbs; rung STATE lives in the guarantee claims carrier and is emitted, never restated in tickets — [gap analysis](docs/plans/compiler-guarantee-recovery-gap-analysis.md)). The three independent SCM R0 models, the separate P−1 evidence carrier, R1 compatibility-shape extraction, and P0 user-contract/landing spine are accepted; the operator-authored P1 proof-kernel node is active and fail-closed pending its first discriminating closing validation, while P2 and later product lanes remain parked. The toolchain pin model is an independent root. The P-derive receipt feeds the emitter fixed point, so the v1 chain nests under it. Node fields define boundary, first slice, RED control, exclusions, owner, and handback. -**Focused view — the v1 deletion lane + the guarantee-ladder climbs.** 38 active deliverable(s) in other lanes are declared in the authority and hidden here; nothing is deleted or parked by focusing. Clear `roadmap_focus_selection` in `dag/gunbc/roadmap_authority.dag` to restore the full page. 11 of the rows below are NOT lane deliverables — they are prerequisites pulled in from other lanes because lane work is blocked on them: namespace-canonical-binding, namespace-ambiguity-discharge, namespace-reference-derived-closure, namespace-binding-kernel, namespace-binding-kernel-closing-contract, v1-seed-honesty-decision, v1-seed-honesty-decision-closing-contract, namespace-legacy-choice-retirement, namespace-v2-import-completion, namespace-bound-only-consumers, namespace-import-deletion. Hidden lanes remain readable through their carriers: [docs/plans/dag-scm-design.md](docs/plans/dag-scm-design.md) · [dag/gunbc/plans/cli_run_hollowing_plan.dag](dag/gunbc/plans/cli_run_hollowing_plan.dag) · [docs/plans/progress-observation-design.md](docs/plans/progress-observation-design.md) · [docs/plans/inner-cost-lanes-scoping.md](docs/plans/inner-cost-lanes-scoping.md) · [docs/plans/ci-two-tier-placement-redesign.md](docs/plans/ci-two-tier-placement-redesign.md) · [docs/plans/shell-to-dag-residual-census-and-arc-completion.md](docs/plans/shell-to-dag-residual-census-and-arc-completion.md) · [docs/plans/roadmap-workspace-ux-plan.md](docs/plans/roadmap-workspace-ux-plan.md) · [docs/plans/host-converge-inventory.md](docs/plans/host-converge-inventory.md) · [docs/runbooks/bmc-redfish-operator-access.md](docs/runbooks/bmc-redfish-operator-access.md) · [docs/plans/host-effect-orchestration.md](docs/plans/host-effect-orchestration.md) · [docs/plans/gunbc-served-dashboard-design.md](docs/plans/gunbc-served-dashboard-design.md) · [docs/plans/fleet-acceptance-criteria.md](docs/plans/fleet-acceptance-criteria.md) · [docs/plans/hermetic-tool-provisioning-design.md](docs/plans/hermetic-tool-provisioning-design.md). +**Focused view — the v1 deletion lane + the guarantee-ladder climbs.** 38 active deliverable(s) in other lanes are declared in the authority and hidden here; nothing is deleted or parked by focusing. Clear `roadmap_focus_selection` in `dag/gunbc/roadmap_authority.dag` to restore the full page. 10 of the rows below are NOT lane deliverables — they are prerequisites pulled in from other lanes because lane work is blocked on them: namespace-canonical-binding, namespace-ambiguity-discharge, namespace-reference-derived-closure, namespace-reference-derived-closure-closing-contract, v1-seed-honesty-decision, v1-seed-honesty-decision-closing-contract, namespace-legacy-choice-retirement, namespace-v2-import-completion, namespace-bound-only-consumers, namespace-import-deletion. Hidden lanes remain readable through their carriers: [docs/plans/dag-scm-design.md](docs/plans/dag-scm-design.md) · [dag/gunbc/plans/cli_run_hollowing_plan.dag](dag/gunbc/plans/cli_run_hollowing_plan.dag) · [docs/plans/progress-observation-design.md](docs/plans/progress-observation-design.md) · [docs/plans/inner-cost-lanes-scoping.md](docs/plans/inner-cost-lanes-scoping.md) · [docs/plans/ci-two-tier-placement-redesign.md](docs/plans/ci-two-tier-placement-redesign.md) · [docs/plans/shell-to-dag-residual-census-and-arc-completion.md](docs/plans/shell-to-dag-residual-census-and-arc-completion.md) · [docs/plans/roadmap-workspace-ux-plan.md](docs/plans/roadmap-workspace-ux-plan.md) · [docs/plans/host-converge-inventory.md](docs/plans/host-converge-inventory.md) · [docs/runbooks/bmc-redfish-operator-access.md](docs/runbooks/bmc-redfish-operator-access.md) · [docs/plans/host-effect-orchestration.md](docs/plans/host-effect-orchestration.md) · [docs/plans/gunbc-served-dashboard-design.md](docs/plans/gunbc-served-dashboard-design.md) · [docs/plans/fleet-acceptance-criteria.md](docs/plans/fleet-acceptance-criteria.md) · [docs/plans/hermetic-tool-provisioning-design.md](docs/plans/hermetic-tool-provisioning-design.md). - [ ] **Record which generator actually produced each result** — Every result says which generator made it and how far it got, as two separate facts. Claiming the new generator made something, without the evidence that it did, has to be impossible to write down. Why: Right now everything is made by the old generator, and the count we track reads as if the new one made it. We are measuring the wrong thing, so we cannot tell how far along we are. [authority](src/v2/compiler/self_host/frontier.dag) - [ ] **Have the new compiler write out Rust source itself** — Read the source, work out the types, translate, and write real Rust to disk without the old generator deciding any of it. The old runner may still be what executes this; it just stops making the decisions. Why: Until the new compiler can write Rust on its own, every improvement has to go through the generator we are trying to delete. [authority](src/v2/compiler/self_host/candidate_generation.dag) - [ ] **Regenerate one already-tested module with the new generator** — Take a module that already has working tests, change only which generator produces it, and confirm it still passes the same tests including the deliberately broken one. Why: Proves the whole approach works on something real before anyone builds more on the assumption that it does. [authority](src/v2/compiler/self_host/frontier.dag) - [ ] **Build the new generator with itself, then do it again** — The generator, and only the generator: it writes and builds a native version of itself, that native version writes and builds another, and the second one needs nothing from the old compiler. The other parts of the compiler are still produced the old way at this point. Why: This is the moment the new generator stops being something we produce and becomes something that produces itself. [authority](src/v2/compiler/self_host/frontier.dag) — requires all: v2-emitter-first-behavioral-module, pderive-error-movement-receipt -- [ ] **Write the closing check for: One canonical decision authority, handed the candidates rather than finding them** — One executable check states when this is done, transcribed from the node's own bar: Malformed occurrence transport refuses; a declaration of the wrong kind is excluded; a candidate given in the wrong role refuses, distinctly from one that is absent; the same declaration supplied twice refuses as a malformed population rather than counting as two; and supplied lists of nothing, one thing, and two distinct things give unresolved, resolved, and ambiguous respectively. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **One canonical decision authority, handed the candidates rather than finding them** — Given a list of things a name could refer to, decide: none, exactly one, or more than one. Checking that what it was handed is well formed and of a kind that could match. NOT working out which declarations are in view — that is the next step. Owns the canonical decision itself; does NOT own removing the older mechanisms that still decide it their own way. Why: Several lookup mechanisms exist today and they can disagree about the same name, which is how a program silently ends up meaning something other than what was written. Separating the decision from the search makes the decision reviewable on its own. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Work out what each name can see, and let that be what a file depends on** — From where a name is written, work outward to every declaration actually in view — including ordinary neighbours in the same file — respecting where things were written relative to each other. Then record, for each name, exactly which declaration answered it, and derive what each file depends on from those answers. Why: What a file depends on is currently written out by hand as a separate statement, so it can disagree with what the file actually refers to. Hundreds of files were patched by hand to keep those two in step, which is work that should never have existed: what a file depends on is a consequence of what it refers to, not a second thing to maintain. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Settle the names that genuinely could mean two things** — Using the real answers from the step before — not a count taken by searching text — go through every name that genuinely has more than one candidate and either refuse it on purpose or say which one is meant, by renaming or by writing it out in full. Why: Counting these by searching for text gives a number that is wrong in both directions, so it cannot tell anyone how much work is left or whether it is finished. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Make name lookup itself produce that one answer, and delete the old way** — The places that WORK OUT what a name refers to — type names, value names, and calls — produce their answer from the mechanisms built in the earlier steps, and the old habit of searching everything currently loaded is deleted rather than left beside them. This is the changeover of name lookup itself. What the rest of the compiler then does with that answer is the next step, not this one. Why: Until the old lookup is deleted the new mechanism is only advisory: both run, they can disagree, and the compiler can still quietly mean something other than what was written. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Make the rest of the compiler use only that one answer** — Type checking, call lookup, and argument-count checks all read the single decided answer. When the answer is 'unresolved' or 'ambiguous', they stop rather than carrying on with a guess. Why: While parts of the compiler still do their own lookup, the one shared rule is advisory rather than authoritative. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Do the same for imports** — Every place the newer compiler resolves an import or a dotted name reads the single decided answer, before the old mechanisms are deleted. Why: If imports are left behind, they become a second source of truth about what a name means — exactly the problem this whole chain exists to remove. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Delete every other way of deciding what a name means** — Once all ordinary consumers use the one rule, remove the older mechanisms: name-string matching, walking up the parent chain, the global pool of bare names, the import retry, and the hand-written seed mirror that counts none/one/many for itself inside the bootstrap compiler. Owning that last one HERE, rather than in the node that defines the canonical decision, is deliberate — see namespace_kernel_authority_amendment_note. Why: Leaving them in place means one name can still get different answers from different code paths. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Delete the import lines, and the grammar that allows them** — Every import line goes, and import stops being something the language can express at all: writing one becomes a parse error. What a file depends on is worked out from the names it actually mentions. Why: While import still parses, a file can declare one set of dependencies while its references need another, and the two can disagree. That is the second source of truth this whole chain exists to remove, and it survives every earlier step. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) - - [ ] **Attach those requirements to the right type, not just the right position** — Connect each recorded slot to the exact declaration it belongs to, using the name resolution work above. Why: Matching by position or by short name attaches a requirement to the wrong type whenever two types share a name or the order shifts. [authority](docs/plans/v2-self-hosting.md) - - [ ] **Generate those requirements from one place in the new generator** — The new Rust generator reads the record and the mapping through its existing path, rather than needing a special case per shape. Teaching the old generator the same thing is allowed only where the build-itself path is genuinely stuck without it, and even then it reads the same answer rather than working it out again. Why: Every shape currently needing its own hand-written patch is a shape someone has to fix again next time — and doing it in the generator we are deleting means doing it twice. [authority](src/v2/compiler/05_emit.dag) — requires all: pderive-source-slot-binding, v2-emitter-direct-rust-door - - [ ] **Count the compiler errors before and after, to prove it helped** — Re-run the same set of modules and compare the error counts and kinds. Why: A fix that makes one example compile can leave the real number untouched; without the count we would not know. [authority](docs/plans/v2-self-hosting.md) - - [ ] **Get the rest of the compiler to rebuild itself too** — With the generator already building itself, extend the same two-round test to everything else the compiler does — reading source, working out names, working out types, translating — until the whole thing generates its own source, that source is built, and the result behaves the same. Behaviour has to match; the bytes do not. Why: This is the whole point of the project: until the compiler can build itself end to end, there is nothing to replace the old one with. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: pderive-error-movement-receipt, v2-emitter-native-bootstrap - - [ ] **Switch the build over to the new compiler** — The real build runs on the new path, with cold comparisons against the old one first. Why: Moves the thing that actually has to work every day, instead of proving a replacement nobody uses. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-emitter-fixed-point, v1-honest-frontier, v1-verification-ledger - - [ ] **Run everything with the old generator taken away** — The checks and the build use what the new generator produced, nothing falls back to the old generator, and nothing is left calling it. Why: Separates deleting the old generator from deleting the old runner, so the generator can go first instead of waiting on everything else. [authority](src/v1/05_emit_rust.dag) - - [ ] **Delete the old Rust generator** — The old generator itself is removed, along with the one Rust file it generated to run itself, the Rust option in the old build path, and the fixtures, helpers and measurements that existed only for it. What produced a file in the past does not decide whether it goes: any Rust the new generator now produces stays exactly where it is. Why: Removes the second set of Rust-generation rules for good — the split the shared rules were meant to end. [authority](src/v1/05_emit_rust.dag) - - [ ] **Build the products with the old runner left out entirely** — A rehearsal: build without the old runner included at all, and run the products and the build both cold and warm. Why: Proves nothing still reaches for it, which is the thing that actually makes deletion safe. [authority](dag/gunbc/v1_deletion_plan.dag) - - [ ] **Delete the old runner** — Delete it and everything now unreachable, in one change, once the rehearsal above holds. This is about the runner only — not the rest of the old code, and not hand-written Rust generally. Why: Removes the second, slower way of running everything, and with it the possibility of the two disagreeing. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-interpreter-quarantine, v1-emitter-delete - - [ ] **Build and run every product without the old code present** — With the old runner gone and the hand-written queue worked through, the products build and run with the old directory absent. Anything that must stay gets an explicit reason. Why: Separates 'the products no longer need the old code' from 'the old runner is gone', so neither claim waits on the other's leftovers. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-interpreter-delete, v1-hand-queue-drain +- [ ] **Write the closing check for: Work out what each name can see, and let that be what a file depends on** — One executable check states when this is done, transcribed from the node's own bar: A neighbour in the same file is in view; something inside a different branch of a decision is not; something written further down is not; the same name written twice in genuinely different places stays ambiguous; repeated mentions of one thing collapse to a single dependency; and nothing depends on an unrelated file happening to be loaded at the same time. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Work out what each name can see, and let that be what a file depends on** — From where a name is written, work outward to every declaration actually in view — including ordinary neighbours in the same file — respecting where things were written relative to each other. Then record, for each name, exactly which declaration answered it, and derive what each file depends on from those answers. Why: What a file depends on is currently written out by hand as a separate statement, so it can disagree with what the file actually refers to. Hundreds of files were patched by hand to keep those two in step, which is work that should never have existed: what a file depends on is a consequence of what it refers to, not a second thing to maintain. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Settle the names that genuinely could mean two things** — Using the real answers from the step before — not a count taken by searching text — go through every name that genuinely has more than one candidate and either refuse it on purpose or say which one is meant, by renaming or by writing it out in full. Why: Counting these by searching for text gives a number that is wrong in both directions, so it cannot tell anyone how much work is left or whether it is finished. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Make name lookup itself produce that one answer, and delete the old way** — The places that WORK OUT what a name refers to — type names, value names, and calls — produce their answer from the mechanisms built in the earlier steps, and the old habit of searching everything currently loaded is deleted rather than left beside them. This is the changeover of name lookup itself. What the rest of the compiler then does with that answer is the next step, not this one. Why: Until the old lookup is deleted the new mechanism is only advisory: both run, they can disagree, and the compiler can still quietly mean something other than what was written. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Make the rest of the compiler use only that one answer** — Type checking, call lookup, and argument-count checks all read the single decided answer. When the answer is 'unresolved' or 'ambiguous', they stop rather than carrying on with a guess. Why: While parts of the compiler still do their own lookup, the one shared rule is advisory rather than authoritative. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Do the same for imports** — Every place the newer compiler resolves an import or a dotted name reads the single decided answer, before the old mechanisms are deleted. Why: If imports are left behind, they become a second source of truth about what a name means — exactly the problem this whole chain exists to remove. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Delete every other way of deciding what a name means** — Once all ordinary consumers use the one rule, remove the older mechanisms: name-string matching, walking up the parent chain, the global pool of bare names, the import retry, and the hand-written seed mirror that counts none/one/many for itself inside the bootstrap compiler. Owning that last one HERE, rather than in the node that defines the canonical decision, is deliberate — see namespace_kernel_authority_amendment_note. Why: Leaving them in place means one name can still get different answers from different code paths. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Delete the import lines, and the grammar that allows them** — Every import line goes, and import stops being something the language can express at all: writing one becomes a parse error. What a file depends on is worked out from the names it actually mentions. Why: While import still parses, a file can declare one set of dependencies while its references need another, and the two can disagree. That is the second source of truth this whole chain exists to remove, and it survives every earlier step. [authority](docs/plans/namespace-unique-on-chain-operational-plan.md) + - [ ] **Attach those requirements to the right type, not just the right position** — Connect each recorded slot to the exact declaration it belongs to, using the name resolution work above. Why: Matching by position or by short name attaches a requirement to the wrong type whenever two types share a name or the order shifts. [authority](docs/plans/v2-self-hosting.md) + - [ ] **Generate those requirements from one place in the new generator** — The new Rust generator reads the record and the mapping through its existing path, rather than needing a special case per shape. Teaching the old generator the same thing is allowed only where the build-itself path is genuinely stuck without it, and even then it reads the same answer rather than working it out again. Why: Every shape currently needing its own hand-written patch is a shape someone has to fix again next time — and doing it in the generator we are deleting means doing it twice. [authority](src/v2/compiler/05_emit.dag) — requires all: pderive-source-slot-binding, v2-emitter-direct-rust-door + - [ ] **Count the compiler errors before and after, to prove it helped** — Re-run the same set of modules and compare the error counts and kinds. Why: A fix that makes one example compile can leave the real number untouched; without the count we would not know. [authority](docs/plans/v2-self-hosting.md) + - [ ] **Get the rest of the compiler to rebuild itself too** — With the generator already building itself, extend the same two-round test to everything else the compiler does — reading source, working out names, working out types, translating — until the whole thing generates its own source, that source is built, and the result behaves the same. Behaviour has to match; the bytes do not. Why: This is the whole point of the project: until the compiler can build itself end to end, there is nothing to replace the old one with. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: pderive-error-movement-receipt, v2-emitter-native-bootstrap + - [ ] **Switch the build over to the new compiler** — The real build runs on the new path, with cold comparisons against the old one first. Why: Moves the thing that actually has to work every day, instead of proving a replacement nobody uses. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-emitter-fixed-point, v1-honest-frontier, v1-verification-ledger + - [ ] **Run everything with the old generator taken away** — The checks and the build use what the new generator produced, nothing falls back to the old generator, and nothing is left calling it. Why: Separates deleting the old generator from deleting the old runner, so the generator can go first instead of waiting on everything else. [authority](src/v1/05_emit_rust.dag) + - [ ] **Delete the old Rust generator** — The old generator itself is removed, along with the one Rust file it generated to run itself, the Rust option in the old build path, and the fixtures, helpers and measurements that existed only for it. What produced a file in the past does not decide whether it goes: any Rust the new generator now produces stays exactly where it is. Why: Removes the second set of Rust-generation rules for good — the split the shared rules were meant to end. [authority](src/v1/05_emit_rust.dag) + - [ ] **Build the products with the old runner left out entirely** — A rehearsal: build without the old runner included at all, and run the products and the build both cold and warm. Why: Proves nothing still reaches for it, which is the thing that actually makes deletion safe. [authority](dag/gunbc/v1_deletion_plan.dag) + - [ ] **Delete the old runner** — Delete it and everything now unreachable, in one change, once the rehearsal above holds. This is about the runner only — not the rest of the old code, and not hand-written Rust generally. Why: Removes the second, slower way of running everything, and with it the possibility of the two disagreeing. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-interpreter-quarantine, v1-emitter-delete + - [ ] **Build and run every product without the old code present** — With the old runner gone and the hand-written queue worked through, the products build and run with the old directory absent. Anything that must stay gets an explicit reason. Why: Separates 'the products no longer need the old code' from 'the old runner is gone', so neither claim waits on the other's leftovers. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-interpreter-delete, v1-hand-queue-drain + - [ ] **Get hand-written Rust in this repository down to zero** — Every Rust file we track is either generated from a description or deleted. Hand-maintaining Rust is a stage we are passing through, not where we stop. Why: Every hand-written file is a permanent tax: the behaviour and its description have to be kept in step by hand, forever. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-products-v1-free, v1-test-migration, v1-seed-honesty-decision, namespace-legacy-choice-retirement, namespace-import-deletion - [ ] **Write the closing check for: Keep an honest list of what still depends on the old compiler** — One executable check states when this is done, transcribed from the node's own bar: An unlisted module, an unlisted dependency, or a listed test that never actually runs makes the list red. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](dag/gunbc/v1_deletion_plan.dag) - [ ] **Keep an honest list of what still depends on the old compiler** — Every module is either generated by the new compiler or explicitly kept on the old one with a reason and a plan to move. Why: Turns a vague sense of 'mostly done' into a finite, countable list someone can work through. [authority](dag/gunbc/v1_deletion_plan.dag) - [ ] **Write the closing check for: Make exact prior results reusable across invocations** — One executable check states when this is done, transcribed from the node's own bar: Wrong content, a missing entry, and a full store each refuse rather than quietly falling back to recomputing. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/witness-realization-plan.md) From bb2363585f587d520a42d59b53a7029a7e8f62d4 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Sat, 1 Aug 2026 20:03:22 +0000 Subject: [PATCH 8/8] WIP: namespace/substrate/determinism/numeric tower --- .github/workflows/ci.yml | 99 +- Cargo.lock | 82 + DESIGN.md | 6 +- ROADMAP.md | 71 +- dag/extdeps/advertising/google_ads.dag | 109 + dag/extdeps/advertising/meta_ads.dag | 216 + dag/extdeps/advertising/types.dag | 19 + dag/extdeps/cache/sccache.dag | 71 + dag/extdeps/crm/hubspot_lifecycle.dag | 117 + dag/extdeps/crm/salesforce_opportunity.dag | 106 + dag/extdeps/git/git.dag | 43 - dag/extdeps/git/inspect.dag | 90 + dag/extdeps/git/mock_corpus.dag | 32 +- dag/extdeps/github/pulls.dag | 69 +- dag/extdeps/github/workflows.dag | 72 + dag/extdeps/languages/lean/overflow.dag | 16 + dag/extdeps/languages/typescript/program.dag | 16 +- dag/extdeps/languages/yaml/gha_workflow.dag | 52 +- dag/extdeps/project_schedule/cpm_pert.dag | 30 + dag/extdeps/tailscale/serve.dag | 89 + dag/extdeps/tmux/tmux.dag | 9 + dag/extdeps/vendor/alphabet.dag | 16 + dag/extdeps/vendor/hubspot.dag | 16 + dag/extdeps/vendor/meta_platforms.dag | 16 + dag/extdeps/vendor/salesforce.dag | 16 + dag/gunbc/build_cache_instance.dag | 210 + dag/gunbc/ci_layer_roots.dag | 19 +- dag/gunbc/ci_materialization_gate.dag | 20 +- dag/gunbc/ci_runner_placement.dag | 61 +- dag/gunbc/ci_spec.dag | 44 +- dag/gunbc/ci_workflow.dag | 98 +- dag/gunbc/compile_diagnostic_census.dag | 53 + dag/gunbc/design/scale.dag | 8 +- dag/gunbc/design_document.dag | 5 +- dag/gunbc/doc_graph_roots.dag | 9 +- dag/gunbc/econ/acquisition.dag | 143 + dag/gunbc/fleet_host_budget.dag | 81 +- dag/gunbc/heal_revalidation.dag | 176 + dag/gunbc/host_build_cache_provision.dag | 167 +- dag/gunbc/host_layout.dag | 3 + dag/gunbc/live_deploy/apply.dag | 123 +- dag/gunbc/live_deploy/readiness.dag | 37 +- dag/gunbc/live_deploy/spec.dag | 51 + dag/gunbc/merge_admission.dag | 4 +- dag/gunbc/merge_admission_produce.dag | 7 +- dag/gunbc/observation_seed_render.dag | 2 +- dag/gunbc/plans/dag_v2_defork_audit.dag | 7 +- dag/gunbc/readback_independence.dag | 2 +- dag/gunbc/roadmap_authority.dag | 528 ++- dag/gunbc/roadmap_belt_actuate.dag | 41 +- dag/gunbc/roadmap_component.dag | 1299 +++--- dag/gunbc/roadmap_dashboard_instance.dag | 37 + dag/gunbc/roadmap_forecast.dag | 826 ++++ dag/gunbc/roadmap_identity.dag | 22 +- dag/gunbc/roadmap_page.dag | 721 ++-- dag/gunbc/roadmap_presentation.dag | 985 +++++ dag/gunbc/roadmap_sandbox.dag | 119 +- dag/gunbc/roadmap_serve.dag | 29 +- dag/gunbc/roadmap_style.dag | 335 +- dag/gunbc/roadmap_workflow_progress.dag | 21 +- dag/gunbc/seed_growth.dag | 16 + dag/gunbc/source_integration_proof_kernel.dag | 582 +++ dag/gunbc/stage0_cargo_manifest.dag | 9 +- dag/gunbc/stage0_emit_model.dag | 1 + dag/gunbc/stage0_rust_host_observation.dag | 274 ++ .../stage0_rust_source_lifecycle_scaffold.dag | 156 +- dag/gunbc/tools/roadmap_spawn_request.dag | 3 +- .../v1_interpreter_primitive_surface.dag | 229 + dag/gunbc/witness_row_cost_basis.tsv | 7 +- dag/std/algebra.dag | 29 +- dag/std/integer.dag | 13 +- dag/std/occurrence_identity.dag | 4 +- dag/std/primitive_identity.dag | 760 ++++ dag/std/primitives.dag | 198 + dag/std/trait_derive_shape.dag | 2 +- .../acquisition_mechanism_witness_test.dag | 193 + .../advertising_interface_witness_test.dag | 88 + .../claim/anomaly_evidence_witness_test.dag | 6 +- ...build_cache_placement_observation_test.dag | 185 + dag/test/claim/ci_deploy_witness_test.dag | 9 +- dag/test/claim/ci_heal_job_witness_test.dag | 95 +- .../claim/ci_materialization_witness_test.dag | 5 + dag/test/claim/ci_workflow_witness_test.dag | 8 + ...compile_diagnostic_census_witness_test.dag | 102 + .../crm_stage_vocabulary_witness_test.dag | 84 + ...sign_register_lift_parity_witness_test.dag | 10 +- dag/test/claim/dispatch_stop_witness_test.dag | 21 +- .../claim/doc_reachability_witness_test.dag | 2 +- .../expectation_frontier_witness_test.dag | 5 +- dag/test/claim/fleet_host_budget_test.dag | 12 +- .../generic_item_clone_bound_witness_test.dag | 33 + ..._revalidation_integration_witness_test.dag | 47 + .../claim/heal_revalidation_witness_test.dag | 194 + .../host_allocation_conservation_test.dag | 9 +- ...ld_cache_provision_design_witness_test.dag | 73 +- .../claim/live_deploy/apply_witness_test.dag | 45 +- dag/test/claim/live_deploy/emit_test.dag | 33 +- .../live_deploy/readiness_witness_test.dag | 11 +- .../claim/no_fake_anomalies_witness_test.dag | 5 +- .../primitive_identity_join_witness_test.dag | 204 + dag/test/claim/roadmap_authority_test.dag | 18 +- .../roadmap_belt_actuate_witness_test.dag | 35 + .../claim/roadmap_forecast_witness_test.dag | 520 +++ .../claim/roadmap_frontier_witness_test.dag | 6 +- .../claim/roadmap_identity_witness_test.dag | 2 +- dag/test/claim/roadmap_page_witness_test.dag | 217 +- .../roadmap_presentation_witness_test.dag | 585 +++ .../claim/roadmap_register_witness_test.dag | 14 +- dag/test/claim/roadmap_row_witness_test.dag | 28 +- .../claim/roadmap_tactile_witness_test.dag | 15 +- .../claim/sessions_panel_witness_test.dag | 45 +- ...tegration_proof_kernel_acceptance_test.dag | 18 + ...ration_proof_kernel_model_witness_test.dag | 356 ++ ...ust_host_observation_live_witness_test.dag | 51 + ...source_lifecycle_scaffold_witness_test.dag | 122 +- .../tailscale_serve_status_witness_test.dag | 18 + .../claim/typed_witness_invocation_test.dag | 5 +- .../typescript_program_serializer_test.dag | 35 + ...rpreter_primitive_surface_witness_test.dag | 180 + .../workflow_dispatch_input_witness_test.dag | 79 + dag/tools/build_step_transport.dag | 3 +- dag/tools/ci_heal_dispatch.dag | 44 + .../dag_compile_clean_seam_transport.dag | 3 +- ...compile_clean_shard_totality_transport.dag | 3 +- .../dag_compile_clean_shard_transport.dag | 3 +- dag/tools/dag_compile_clean_transport.dag | 9 +- dag/tools/host_prelude.dag | 9 +- dag/tools/merge_admission_gate.dag | 3 +- dag/tools/merge_admission_stamp.dag | 5 +- ...ce_import_closure_behavioral_transport.dag | 3 +- dag/tools/self_host_curated_probe_cargo.dag | 3 +- .../self_host_curated_seed_linked_harness.dag | 3 +- .../self_host_logic_behavioral_transport.dag | 3 +- ...elf_host_realized_comparison_transport.dag | 3 +- .../algebra-grounding-unification-design.md | 4 +- docs/plans/build-cache-placement-receipt.md | 135 + ...ompiler-guarantee-recovery-gap-analysis.md | 309 +- docs/plans/dag-v2-defork-audit.md | 8 +- .../entry-graph-union-slice1-measurement.md | 2 + ...raph-union-slice2-typecheck-attribution.md | 197 + .../m2-floor-retention-measurement-receipt.md | 223 + .../plans/nat-grounding-unification-design.md | 49 +- docs/plans/node-subtree-visibility-grants.md | 99 +- .../matrix-governor.log | 24 + .../entry-graph-union-slice2/matrix-run.log | 8 + .../receipt-broad-capped.json | 1 + .../receipt-broad-disjoint.json | 1 + .../receipt-explicit-order-a.json | 1 + .../receipt-explicit-order-b.json | 1 + .../receipt-narrow-disjoint.json | 1 + .../receipt-narrow-production.json | 1 + .../receipt-typical-disjoint.json | 1 + .../receipt-typical-production.json | 1 + .../m2-floor-retention-falsifier-batch1.json | 20 + .../plans/roadmap-presentation-seam-design.md | 179 + .../srvn-buildcache-provisioning-design.md | 8 + docs/plans/v1-run-stability-throughline.md | 1 + src/v1/02_parse.dag | 20 +- src/v1/04_method.dag | 5 + src/v1/05_emit_rust.dag | 42 +- src/v1/stage0/Cargo.toml | 5 + .../measure_repeated_typecheck_attribution.rs | 198 + src/v1/stage0/src/cli_run.rs | 1427 ++++++- src/v1/stage0/src/lib.rs | 1 + src/v1/stage0/src/std_algebra.rs | 58 +- src/v1/stage0/src/std_integer.rs | 34 +- src/v1/stage0/src/std_measure.rs | 2 +- src/v1/stage0/src/std_occurrence_identity.rs | 4 +- src/v1/stage0/src/std_realization_schedule.rs | 2 +- src/v1/stage0/src/std_trait_derive_shape.rs | 2 +- src/v1/stage0/src/v1_compiler_emit_rust.rs | 103 +- src/v1/stage0/src/v1_compiler_infer_method.rs | 24 + src/v1/stage0/src/v1_compiler_parse.rs | 34 +- .../src/v1_compiler_trait_derive_emit.rs | 99 + src/v1/stage0/src/v1_interpreter.rs | 3743 +++++++++-------- ...required_expr_newline_continuation_test.rs | 130 + .../stage0/tests/walk_plan_stage_fixture.rs | 12 +- ...equired_expr_newline_continuation_test.dag | 105 + .../tests/src/union_resolve_receipts_test.rs | 129 + src/v1/trait_derive_emit.dag | 50 + src/v2/extdeps/languages/lean.dag | 276 +- ...n_bit_width_admissibility_witness_test.dag | 2 +- .../lean_overflow_semantics_witness_test.dag | 98 + .../test/fixture/walk_plan_stage/common.dag | 6 +- src/v2/test/fixture/walk_plan_stage/plan.dag | 8 +- .../recursion_refusal_member_test.dag | 5 + .../git_mock_totality_test.dag | 10 +- src/v2/workflow/ci_floor_plan.dag | 4 +- .../ci_heal_revalidation_preflight_emit.dag | 121 + ..._heal_revalidation_preflight_emit_test.dag | 22 + .../compiler_closure_ingest_transport.dag | 3 +- .../frontier_probe_survey_transport.dag | 3 +- .../module_binding_supply_transport.dag | 3 +- .../workflow/source_root_ingest_transport.dag | 3 +- 194 files changed, 18295 insertions(+), 3751 deletions(-) create mode 100644 dag/extdeps/advertising/google_ads.dag create mode 100644 dag/extdeps/advertising/meta_ads.dag create mode 100644 dag/extdeps/advertising/types.dag create mode 100644 dag/extdeps/crm/hubspot_lifecycle.dag create mode 100644 dag/extdeps/crm/salesforce_opportunity.dag create mode 100644 dag/extdeps/git/inspect.dag create mode 100644 dag/extdeps/github/workflows.dag create mode 100644 dag/extdeps/languages/lean/overflow.dag create mode 100644 dag/extdeps/project_schedule/cpm_pert.dag create mode 100644 dag/extdeps/vendor/alphabet.dag create mode 100644 dag/extdeps/vendor/hubspot.dag create mode 100644 dag/extdeps/vendor/meta_platforms.dag create mode 100644 dag/extdeps/vendor/salesforce.dag create mode 100644 dag/gunbc/build_cache_instance.dag create mode 100644 dag/gunbc/compile_diagnostic_census.dag create mode 100644 dag/gunbc/econ/acquisition.dag create mode 100644 dag/gunbc/heal_revalidation.dag create mode 100644 dag/gunbc/roadmap_forecast.dag create mode 100644 dag/gunbc/roadmap_presentation.dag create mode 100644 dag/gunbc/seed_growth.dag create mode 100644 dag/gunbc/source_integration_proof_kernel.dag create mode 100644 dag/gunbc/stage0_rust_host_observation.dag create mode 100644 dag/gunbc/v1_interpreter_primitive_surface.dag create mode 100644 dag/std/primitive_identity.dag create mode 100644 dag/test/claim/acquisition_mechanism_witness_test.dag create mode 100644 dag/test/claim/advertising_interface_witness_test.dag create mode 100644 dag/test/claim/build_cache_placement_observation_test.dag create mode 100644 dag/test/claim/compile_diagnostic_census_witness_test.dag create mode 100644 dag/test/claim/crm_stage_vocabulary_witness_test.dag create mode 100644 dag/test/claim/generic_item_clone_bound_witness_test.dag create mode 100644 dag/test/claim/heal_revalidation_integration_witness_test.dag create mode 100644 dag/test/claim/heal_revalidation_witness_test.dag create mode 100644 dag/test/claim/primitive_identity_join_witness_test.dag create mode 100644 dag/test/claim/roadmap_forecast_witness_test.dag create mode 100644 dag/test/claim/roadmap_presentation_witness_test.dag create mode 100644 dag/test/claim/source_integration_proof_kernel_acceptance_test.dag create mode 100644 dag/test/claim/source_integration_proof_kernel_model_witness_test.dag create mode 100644 dag/test/claim/stage0_rust_host_observation_live_witness_test.dag create mode 100644 dag/test/claim/tailscale_serve_status_witness_test.dag create mode 100644 dag/test/claim/v1_interpreter_primitive_surface_witness_test.dag create mode 100644 dag/test/claim/workflow_dispatch_input_witness_test.dag create mode 100644 dag/tools/ci_heal_dispatch.dag create mode 100644 docs/plans/build-cache-placement-receipt.md create mode 100644 docs/plans/entry-graph-union-slice2-typecheck-attribution.md create mode 100644 docs/plans/m2-floor-retention-measurement-receipt.md create mode 100644 docs/plans/receipts/entry-graph-union-slice2/matrix-governor.log create mode 100644 docs/plans/receipts/entry-graph-union-slice2/matrix-run.log create mode 100644 docs/plans/receipts/entry-graph-union-slice2/receipt-broad-capped.json create mode 100644 docs/plans/receipts/entry-graph-union-slice2/receipt-broad-disjoint.json create mode 100644 docs/plans/receipts/entry-graph-union-slice2/receipt-explicit-order-a.json create mode 100644 docs/plans/receipts/entry-graph-union-slice2/receipt-explicit-order-b.json create mode 100644 docs/plans/receipts/entry-graph-union-slice2/receipt-narrow-disjoint.json create mode 100644 docs/plans/receipts/entry-graph-union-slice2/receipt-narrow-production.json create mode 100644 docs/plans/receipts/entry-graph-union-slice2/receipt-typical-disjoint.json create mode 100644 docs/plans/receipts/entry-graph-union-slice2/receipt-typical-production.json create mode 100644 docs/plans/receipts/m2-floor-retention-falsifier-batch1.json create mode 100644 docs/plans/roadmap-presentation-seam-design.md create mode 100644 src/v1/stage0/src/bin/measure_repeated_typecheck_attribution.rs create mode 100644 src/v1/stage0/src/v1_tests_claim_required_expr_newline_continuation_test.rs create mode 100644 src/v1/tests/claim/required_expr_newline_continuation_test.dag create mode 100644 src/v2/test/claim/lean_overflow_semantics_witness_test.dag create mode 100644 src/v2/test/fixture/walk_plan_stage/recursion_refusal_member_test.dag create mode 100644 src/v2/workflow/ci_heal_revalidation_preflight_emit.dag create mode 100644 src/v2/workflow/ci_heal_revalidation_preflight_emit_test.dag diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 963941652cf..85e8186dcfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,11 @@ name: ci on: workflow_dispatch: + inputs: + expected_healed_sha: + description: Immutable healed commit required by an auto-heal revalidation run + required: false + type: string push: branches: [main] pull_request: @@ -19,12 +24,29 @@ env: jobs: build: runs-on: [self-hosted, linux, arm64] - timeout-minutes: 75 + timeout-minutes: 80 steps: - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 + ref: ${{ inputs.expected_healed_sha || github.sha }} + - name: Refuse a heal revalidation whose run subject or checkout does not name the expected healed SHA + run: | + EXPECTED_HEALED_SHA="${{ inputs.expected_healed_sha }}" + RUN_SUBJECT_HEAD="${{ github.sha }}" + if [ -n "$EXPECTED_HEALED_SHA" ]; then + ACTUAL_HEAD="$(git rev-parse HEAD)" + if ! [ "$RUN_SUBJECT_HEAD" = "$EXPECTED_HEALED_SHA" ]; then + echo "::error::heal revalidation refused: workflow run subject $RUN_SUBJECT_HEAD does not equal expected healed head $EXPECTED_HEALED_SHA" + exit 1 + fi + if ! [ "$ACTUAL_HEAD" = "$EXPECTED_HEALED_SHA" ]; then + echo "::error::heal revalidation refused: checkout head $ACTUAL_HEAD does not equal expected healed head $EXPECTED_HEALED_SHA" + exit 1 + fi + echo "heal revalidation preflight: checkout names expected healed head $EXPECTED_HEALED_SHA" + fi - name: Isolate toolchain dirs run: | rm -rf "$RUNNER_TEMP/rustup" "$RUNNER_TEMP/cargo" @@ -80,7 +102,7 @@ jobs: run: | # 🟡 dissolve-on: ci_v1_compiler_tests_compile_gate_script — foreign-executor (GitHub Actions run:) shell transport for extdeps.cargo_build cargo.Build.Check(extra_args: ci_v1_compiler_tests_compile_gate_extra_args); DISSOLVES WHEN bash-emit (#5828 / ROADMAP 6-shell-slice0 / shell→intent Phase 2) binds the build-job step to orchestration emit over the typed cargo operation — the same de-fork ci_fmt_gate_line and ci_release_build_script await "$CARGO_BIN" 'check' '-p' 'v1-compiler-tests' '--tests' - timeout-minutes: 5 + timeout-minutes: 10 - name: Build release bins (claim_executor + gunbc) run: | # 🟡 dissolve-on: ci_release_build_script — concat-built foreign-executor (GitHub Actions run:) release-build runner (ROOT stamp + verify-artifacts + sccache stats wrapping the orch-emitted EAGAIN retry core); membership of --bin / verify paths is derived from gunbc.ci_release_bins, but the runner transport itself remains hand-shell; DISSOLVES WHEN bash-emit (#5828 / ROADMAP 6-shell-slice0 / shell→intent Phase 2) realizes the release-build runner through orchestration emit or typed host_effect_apply without a medium-as-string concat scaffold @@ -123,6 +145,23 @@ jobs: uses: actions/checkout@v5 with: fetch-depth: 0 + ref: ${{ inputs.expected_healed_sha || github.sha }} + - name: Refuse a heal revalidation whose run subject or checkout does not name the expected healed SHA + run: | + EXPECTED_HEALED_SHA="${{ inputs.expected_healed_sha }}" + RUN_SUBJECT_HEAD="${{ github.sha }}" + if [ -n "$EXPECTED_HEALED_SHA" ]; then + ACTUAL_HEAD="$(git rev-parse HEAD)" + if ! [ "$RUN_SUBJECT_HEAD" = "$EXPECTED_HEALED_SHA" ]; then + echo "::error::heal revalidation refused: workflow run subject $RUN_SUBJECT_HEAD does not equal expected healed head $EXPECTED_HEALED_SHA" + exit 1 + fi + if ! [ "$ACTUAL_HEAD" = "$EXPECTED_HEALED_SHA" ]; then + echo "::error::heal revalidation refused: checkout head $ACTUAL_HEAD does not equal expected healed head $EXPECTED_HEALED_SHA" + exit 1 + fi + echo "heal revalidation preflight: checkout names expected healed head $EXPECTED_HEALED_SHA" + fi - name: Setup Rust (floor gate toolchain — batch-4 emit_host/self_host gates need cargo/rustfmt; no isolated rustup) uses: actions-rust-lang/setup-rust-toolchain@v1.16.0 with: @@ -182,6 +221,23 @@ jobs: uses: actions/checkout@v5 with: fetch-depth: 0 + ref: ${{ inputs.expected_healed_sha || github.sha }} + - name: Refuse a heal revalidation whose run subject or checkout does not name the expected healed SHA + run: | + EXPECTED_HEALED_SHA="${{ inputs.expected_healed_sha }}" + RUN_SUBJECT_HEAD="${{ github.sha }}" + if [ -n "$EXPECTED_HEALED_SHA" ]; then + ACTUAL_HEAD="$(git rev-parse HEAD)" + if ! [ "$RUN_SUBJECT_HEAD" = "$EXPECTED_HEALED_SHA" ]; then + echo "::error::heal revalidation refused: workflow run subject $RUN_SUBJECT_HEAD does not equal expected healed head $EXPECTED_HEALED_SHA" + exit 1 + fi + if ! [ "$ACTUAL_HEAD" = "$EXPECTED_HEALED_SHA" ]; then + echo "::error::heal revalidation refused: checkout head $ACTUAL_HEAD does not equal expected healed head $EXPECTED_HEALED_SHA" + exit 1 + fi + echo "heal revalidation preflight: checkout names expected healed head $EXPECTED_HEALED_SHA" + fi - name: Setup Rust (floor gate toolchain — batch-4 emit_host/self_host gates need cargo/rustfmt; no isolated rustup) uses: actions-rust-lang/setup-rust-toolchain@v1.16.0 with: @@ -239,11 +295,31 @@ jobs: needs: [ci] timeout-minutes: 10 if: github.ref == 'refs/heads/main' && github.event_name == 'push' + concurrency: + group: srv1-dashboard-deploy + cancel-in-progress: false steps: - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 + ref: ${{ inputs.expected_healed_sha || github.sha }} + - name: Refuse a heal revalidation whose run subject or checkout does not name the expected healed SHA + run: | + EXPECTED_HEALED_SHA="${{ inputs.expected_healed_sha }}" + RUN_SUBJECT_HEAD="${{ github.sha }}" + if [ -n "$EXPECTED_HEALED_SHA" ]; then + ACTUAL_HEAD="$(git rev-parse HEAD)" + if ! [ "$RUN_SUBJECT_HEAD" = "$EXPECTED_HEALED_SHA" ]; then + echo "::error::heal revalidation refused: workflow run subject $RUN_SUBJECT_HEAD does not equal expected healed head $EXPECTED_HEALED_SHA" + exit 1 + fi + if ! [ "$ACTUAL_HEAD" = "$EXPECTED_HEALED_SHA" ]; then + echo "::error::heal revalidation refused: checkout head $ACTUAL_HEAD does not equal expected healed head $EXPECTED_HEALED_SHA" + exit 1 + fi + echo "heal revalidation preflight: checkout names expected healed head $EXPECTED_HEALED_SHA" + fi - name: Download release-bins artifact uses: actions/download-artifact@v4 with: @@ -269,6 +345,7 @@ jobs: if: github.event_name == 'pull_request' permissions: contents: write + actions: write steps: - name: Checkout triggering branch head (heal pushes the regeneration back to this branch) uses: actions/checkout@v5 @@ -409,11 +486,25 @@ jobs: if [ -e "docs/plans/fleet-acceptance-criteria.md" ]; then git add "docs/plans/fleet-acceptance-criteria.md"; else echo "chore-heal: registered generated artifact absent on this tree, not staging: docs/plans/fleet-acceptance-criteria.md"; fi if [ -e "docs/plans/structural-quadratic-wall-coverage-audit.md" ]; then git add "docs/plans/structural-quadratic-wall-coverage-audit.md"; else echo "chore-heal: registered generated artifact absent on this tree, not staging: docs/plans/structural-quadratic-wall-coverage-audit.md"; fi if [ -e "docs/plans/ci-performance-tanking-evidence.md" ]; then git add "docs/plans/ci-performance-tanking-evidence.md"; else echo "chore-heal: registered generated artifact absent on this tree, not staging: docs/plans/ci-performance-tanking-evidence.md"; fi + PRIOR_HEAD=$(git rev-parse HEAD) if git diff --cached --quiet; then - echo "chore-heal: no generated-artifact drift on this tree; nothing to commit" + echo "HealNoChange head=$PRIOR_HEAD" + exit 0 else + CHANGED_ARTIFACTS=$(git diff --cached --name-only) git commit -m "chore: regenerate drifted generated artifacts (ci auto-heal)" + HEALED_HEAD=$(git rev-parse HEAD) git push origin HEAD:${{ github.head_ref || github.ref_name }} - echo "chore-heal: pushed regenerated generated artifacts to the triggering branch" + echo "HealProduced prior_head=$PRIOR_HEAD healed_head=$HEALED_HEAD changed_artifacts=$CHANGED_ARTIFACTS" + ROOT=$(git rev-parse --show-toplevel) + if ! GUNBC_HEALED_HEAD="$HEALED_HEAD" GUNBC_HEAL_BRANCH_REF="${{ github.head_ref || github.ref_name }}" "$ROOT/target/release/gunbc" run --source-root "$ROOT/dag" --source-root "$ROOT/src/v2" --entry dag/tools/ci_heal_dispatch.dag --function main + then + echo "::error::HealDispatchRefused healed_head=$HEALED_HEAD cause=CreateWorkflowDispatchFailed" >&2 + exit 1 + fi + echo "SupersededByHealedHead prior_head=$PRIOR_HEAD healed_head=$HEALED_HEAD; revalidation-required" + exit 1 fi + env: + GITHUB_TOKEN: ${{ github.token }} timeout-minutes: 5 diff --git a/Cargo.lock b/Cargo.lock index 1ef3dc9e080..82b6da508b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,6 +202,12 @@ dependencies = [ "syn", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -248,6 +254,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -372,6 +384,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -595,6 +617,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "sha2" version = "0.10.9" @@ -697,6 +728,47 @@ dependencies = [ "zerovec", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "typenum" version = "1.20.1" @@ -775,6 +847,7 @@ dependencies = [ "serde_json", "sha2", "stacker", + "toml", "unicode-ident", "unicode-properties", "ureq", @@ -1032,6 +1105,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/DESIGN.md b/DESIGN.md index ed8f8831311..b9c75826ab5 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -67,6 +67,8 @@ Every class carries an **attainable ceiling — derived, not aspirational**: a d Minimizing cost and complexity (§2–§4) is worthless if a wrong thing passes silently — this is §1's safety axis made concrete. This code is digital: a wrong answer is a **loud error, never a warning** — a bridge collapses, it does not warn. Every path succeeds fully or fails with a typed, located diagnostic; no fabricated plausible output (a bounded "forever" ≠ an "unknown" error). Relax toward application-layer leniency only under protest, and lean to infra so others can build on your work. Stronger than *catching* a wrong state is making it **unwritable** — **correctness by construction, not validation.** A check that re-states a constraint the model already carries is a *second representation* of it (§2/§3): so prefer a single authority from which the realization is *derived* — the bad state cannot be written — over a check that flags it after the fact, which concedes the bad state *is* writable. The tell that a check was validation standing where construction was available: it can be satisfied by editing the *declaration* while the realization still lies (a key-completeness check went green when the spec was edited while the realizer kept faking the cache key). Reserve post-hoc checks for the genuinely **unstructurable** residue (§6 complexity / necessity — you cannot structurally forbid an *unnecessary* loop). Construction makes a class unwritable only when membership is **decidable**, so every class is one of three: a *wall now* (decidable and grounded — nicknaming, dead scaffolds, effect leaks); a *wall after grounding* (decidable but waiting on its single authority — the cross-representation `==` straddle is one only once `Int = GroupCompletion` grounds it); or a *ratchet forever* (undecidable — optimality, by Rice, never reaches "never"). The word **"never" is the trap**: it lets a ratchet masquerade as a wall, so check decidability before claiming one — the undecidable residue is the §6 lens, honestly and permanently. The deepest trap is **specification-without-execution**: a typecheck and a `.contains()` grep are *not* consumers — "done" means a real consumer **green by execution** plus a discriminating input that goes *red* when the behavior is wrong. (For the LLM agent: fluent, type-checking, grep-passing output is precisely the artifact that looks finished without running. Treat your own output as unverified until a consumer runs it green.) +The same demand for an independent referent governs a test's oracle. **A merge-blocking test may compare a live repository population to a numeric literal only when that literal is grounded in a controlled fixture, external/versioned authority, explicit policy budget, or a monotone debt contract over a closed subject universe. A measurement copied from the same current tree is not an oracle. Completeness is an identity join, not a count equality.** A controlled fixture independently authors the input and expected population (for example, one planted duplicate and an assertion of exactly one duplicate). An external or versioned authority names its source — such as a cited protocol arity, ABI field count, or content-addressed prior-release baseline; measured current main is not such an authority. A policy budget states the resource cap it enforces. A monotone debt contract is legitimate only when the subject universe is independently discovered and closed, membership is checked at identity grain rather than by count, the roadmap justifies the direction, and every removal or exception carries a typed disposition. Without all four conditions, a one-sided ratchet over hand-authored rows is only a quieter snapshot: it counts the subjects someone remembered to author. Review tell: if automating the literal's update collapses the assertion to `measure() == measure()`, the manual update was the test's entire content — a change detector, not a check. This rule rejects tree-copied census pins while preserving exact-count fixture tests. + A third named trap, the subtlest because it wears this section's own name: **the absorbing fallback — degradation is disguised fail-open.** When a mechanism cannot compute its precise answer, the tempting failure arm substitutes the *superset* instead: can't compute the affected set → rerun the entire suite; cache key uncertain → scan all keys; provenance unavailable → always run. Nothing is *missed*, so the arm gets labeled fail-closed — but it is the fabricated plausible output one level up: **⊤-as-answer conflated with ⊤-as-ignorance** (state-space conflation on the answer lattice — "everything is affected" and "I could not compute what is affected" are different states whose remedies differ), and it fails open twice. On safety: absorption destroys the only signal that the precise mechanism has a deficit — the failure's frequency is zero *by construction*, so the deficit never ranks for fixing (§6 prices by displaced cost, and a masked cost displaces nothing) and the anemia compounds (the scan-all-keys heuristic in the receipt below was this exact absorption; removing it exposed 8 deficits its silence had hidden). On cost: the fallback is denominated in the *corpus*, not the *change*, so it grows with the repo until the *budget* breaks instead of the build — later, loudly, at interest: §1's safety debt paid in §1's cost currency. And the confidence **threshold** that selects such an arm is a smuggled heuristic — §4 already rules a heuristic never necessary in a closed system — so the threshold's very existence *locates* the anemic modeling it papers over: a solution that must keep degrading to stay green is not a solution being cautious, it is a model being wrong. The rule, and the review tell: **a failure arm must refuse, never widen** — every degradation a typed, located, *countable* diagnostic, so its frequency is observable and prioritizable, never an absorbed rerun. Two neighbors are not this pattern (check the *trigger*: structure vs failure-state): a structural over-approximation computed *as* the answer (a dependency-closure superset is the model's precision frontier, not an absorption), and a deliberate interim fallback that is loud, budget-bounded, and lands with its §6 dissolution trigger. Silent unbounded absorption is never the third. A corollary on the refusal itself: **no escape hatches** — a toggle whose only effect is "proceed as if the refusal had not fired" (an env var, a flag, a silence mode) re-opens the arm §5 just closed, one config edit from silent (operator ruling, 2026-07-05). The operative discipline is the **factory model — and it is a merge requirement, not a preference** (operator ruling, 2026-07-05): a deficit stops the line (typed, located refusal); the stopped line is analyzed before it restarts; the only sanctioned second mode is a *stopped-line audit* that replays the run to ledger every deficit (typed, located, counted) for that analysis — it reports, it does not green. Diagnostic modes read state; no mode writes "keep going." **Review bar:** a diff that lands a non-fail-closed failure arm — a silent widen, a fabricated default, an uncounted degradation, an escape hatch — is a **hard reject** in code review, regardless of what else the PR delivers. The reviewer's three questions: does the line stop? is the stop typed and located? does analysis precede restart? The same arm exists at authoring time: **the workaround — an absorbing fallback executed by the author** (operator ruling, 2026-07-24). When the obstacle is the substrate itself — a parse error you don't understand, a check that won't green, an interface that refuses — the tempting move is to route around it: a different spelling, a dodged codepath, a "for now". The concealment is identical to the runtime absorption above: the deficit's frequency is zeroed (nothing typed, nothing counted, nothing ranks for fixing) — except the concealed deficit is usually in the *language layer*, precisely the class §6 says to root-cause first. So the hard rule, in repo and in review: **noticing you are implementing a workaround IS the line-stop signal** — back up, reassess the problem, root-cause it or flag for help; the only two landing states are the real fix, or a declared scaffold (loud, counted, named dissolution trigger — §6). A workaround is exactly an *unmarked* scaffold, and the marking is the entire difference. Review tell: "workaround" / "for now" / "dodge" in a diff or PR body with no dissolution trigger beside it. Receipt: the `${…}`-in-strings dodge (2026-07-24) — a bare-`$` respelling shipped while the language already had the `\{` escape AND two real parser defects sat beneath the confusing error (a context-free diagnostic and a silent fail-open fallback arm); stopping the line surfaced all three within the hour. @@ -113,10 +115,10 @@ hollow alias (minimality ≠ grounding) · state-space conflation (an `Option`/` - **machine-shape orthogonal scheduling — one realization kernel over faithfully modeled machine facts, with locality (§2).** DRAFT for review; model-before-implement, each phase names its trigger. Its authority transfers to the std/extdeps carriers it names when Phase 1 lands, at which point this md dissolves into a registered `gunbc.plan.Plan` row (§6). → [machine-shape scheduling design](docs/plans/machine-shape-orthogonal-scheduling.md) - **namespace-only name resolution — the containment tree is the single naming authority (operator-signed 2026-07-06).** Supersedes resolver-graph-major §1c's *import-name-universe* ("own declarations ∪ direct import lists" visibility), carrying forward its mechanics unchanged (binding-edge owner, unbound/double-bound = error, patterns-via-scrutinee, no expected-type picker). One structure — **syntactic containment** — induces everything at once: qualified name = nesting position (`T{a}` → `a` *is* `T.a`; `T{a{a}}` → inner *is* `T.a.a`, so Rule-2 no-ambiguity holds by construction), reference = lexical lookup up the ancestor chain (a sibling module is visible, its members projected `node.Symbol`), `.` = projection one level down (field / module-member / variant — one op), and `.` *is* the `std.induction` sub-value relation (descent evidence). So one content-addressed tree grounds THREE consumers — resolution *walks* it, content-addressing *hashes* it, termination reads its *sub-value* edges (§2 Realization: one structure, N consumers). Frontend BUILDS the tree from nesting (pure structure, zero resolution logic); backend WALKS it (lexical up, project down) — no heuristics to tune (the tuning risk = resolving against a flat index instead of the tree). Two governing rules (operator, 2026-07-06): **no dual representation / minimal at every layer; no ambiguity at any layer.** Rides on `SymbolIndex` = "the containment tree materialized" (`type-env-single-authority`, lively-raven lane) — the loyal-heron scaling gate that formerly blocked resolver surgery was **discharged at #6809** (per §7.5 of the design doc: "the lane is no longer blocked on it"); `import-scoped` (§1c, today) and `namespace-only-Y` (position-info uniqueness: expected type filters a variant to one, never picks) are two walk-rules over one *fill-agnostic* index (fill = whole tree; policy gates lookup, never fill). Census: 98% of names globally unique → always bare; the residue is v1-seed forks (resolve by subtree, free) + github-style variants ((Y) context-resolves); no consolidate-now forks (the census's 2 same-name flags proved to be a homonym + whole-file v1-seed duplication, not genuine cross-tree §3 forks). Terminal step: delete the `import` grammar + supporting code → `import` becomes a *parse error*, deps derived from `container.member` references (Rule-1 end-state). → [namespace-only resolution design](docs/plans/namespace-resolution-design.md) - **fleet-reconcile spine — one grain-agnostic membership diff (deploy · fleet · session).** ONE `membership_reconcile` reuses `std.change.keyed_two_way_diff` verbatim (no fork): desired set vs observed set keyed by member IDENTITY — Added/Modified → upsert, Removed → teardown-or-refuse, Unchanged → noop (absence of a hunk). A member may be a service/unit/host OR a Session (operator: 'it should all be the same'), so a new member type is a new instantiation of the same fn with its own `(key_of, key_eq, value_eq, ownership_of)` bundle — zero spine change (a Realization, §2; a forked reconcile means the genericity bought nothing). R5 teardown-owned-only is a CONSTRUCTION WALL: a Removed non-owned member yields `MemberTeardownRefused`, which has no effect arm in any apply dispatch, so the bad state is unwritable — never a post-check, never 'assume owned' (the absorbing fallback §5 forbids); ownership-unknown REFUSES too, typed/located/counted. Ownership is extracted as its own single authority (`gunbc.ownership.Ownership`) and `live_deploy`'s `DeploymentStep` re-grounds onto it (pass 2, a declared dissolution trigger — never a second `Owned|Ensured`). Pass 1 (spine + R5 + a synthetic discriminating witness the degenerate apply/retract poles cannot exercise) landed. → [membership-diff reconcile spine design](docs/plans/membership-diff-reconcile-spine-design.md) -- **node/subtree visibility grants — two verbs (`Reference`, `Publish`) in the same grant algebra, not a parallel mechanism.** `Reference` gates whether a projection/reference edge may be formed within one compiled program; `Publish` gates whether source appears in an audience storage realization. Both reuse `Grant`/`admit_effect_go`; `AudienceScopeTree` converges the existing cache visibility scope, while concrete `std.realization.Placement` remains a different redundant-computation axis. One graph has N storage projections: a private repo is not a second graph. Rule 1 makes publication visibility monotone along references; Rule 2 makes privacy downward-closed on containment, nearest ancestor winning. A World projection renders private content absent by default, never a churn/name-leaking stub; commitment stubs are later opt-in. `Reference` keeps today's permissive `LegacyOpen` frontier, while new undeclared `Publish` content defaults to the narrowest root; Stage 0 bulk-stamps today's public corpus before the guard activates. Stage 0 is file-grain/two Git roots/push guard; Stage 1 checks the composed graph; Stage 2 rides the module-storage binding. A public-only guard reports only local-marker/dangling-reference coverage and never impersonates the composed-root closure. The locked extension is a **publication capability product, not a ladder**: source/interface disclosure, artifact delivery, local/remote execution, integrity, reproducibility, existence/churn disclosure, revocation/erase, and placement/trust are independent policy axes. An emitted artifact is not source disclosure but remains inspectable; client-held key means plaintext access, distinct from remote/attested key custody. Locked blobs use AEAD with subject/interface/version/audience/policy context bound as associated data plus confidentiality-and-integrity-preserving per-audience key wraps; wrong key/context/authentication is typed refusal. Per-statement holes remain refused; interface/realization is the cut; size padding and `Secret`/`Pii`→`SecretRef` construction constraints remain. Commitments never promise zero leakage: length, timing, interface shape, wrap count, access pattern, low-entropy guessing, and churn stay in a named threat model. Remote execution and SCM diagnostics obey information-flow/noninterference + explicit declassification; metering prices an oracle but does not secure it, and unauthorized audiences receive only a non-leaking diagnostic projection. Cryptographic erase is conditional on every key/wrap/backup/cache/derivative/recovery path being sanitized and the result validated, never “delete one key.” → [node/subtree visibility grants design](docs/plans/node-subtree-visibility-grants.md) +- **node/subtree visibility grants — two verbs (`Reference`, `Publish`) in the same grant algebra, not a parallel mechanism.** `Reference` gates whether a projection/reference edge may be formed within one compiled program; `Publish` gates whether source appears in an audience storage realization. Both reuse `Grant`/`admit_effect_go`; `AudienceScopeTree` converges the existing cache visibility scope, while concrete `std.realization.Placement` remains a different redundant-computation axis. One graph has N storage projections: a private repo is not a second graph. Rule 1 makes publication visibility monotone along references; Rule 2 makes privacy downward-closed on containment, nearest ancestor winning. A World projection renders private content absent by default, never a churn/name-leaking stub; commitment stubs are later opt-in. `Reference` keeps today's permissive `LegacyOpen` frontier, while new undeclared `Publish` content defaults to the narrowest root. **Stage 0 per-file placement gate DELETED (operator ruling 2026-08-01):** by operator ruling 2026-08-01: its constant `Publish World` rows duplicated Git's post-cutover path population, four same-day repair rounds raced identical reconstructions and repeatedly redded fleet CI, ordinary modifications were invisible, and CI after a public push was main-admission rather than public-storage admission. New public files require no stamp; closed PR #7588 retains the subtree-default experiment as a re-land seed. Publication enforcement returns only at P-C when private/public composition creates content to guard: a keyed subtree-prefix → disposition `PublicationPolicy` defaults `WithholdPrivate`, refuses unknown roots, and resolves most-specific rule; duplicate-identical rules are idempotent or construction-refused; exceptional decisions bind content identity; a `PublicProjectionPlan` is derived change-locally and consumed only by the sole modeled publisher holding the only public-write capability; a separate whole-projection audit is centrally owned and the pre-publication boundary is a server-side required check. The locked extension is a **publication capability product, not a ladder**: source/interface disclosure, artifact delivery, local/remote execution, integrity, reproducibility, existence/churn disclosure, revocation/erase, and placement/trust are independent policy axes. An emitted artifact is not source disclosure but remains inspectable; client-held key means plaintext access, distinct from remote/attested key custody. Locked blobs use AEAD with subject/interface/version/audience/policy context bound as associated data plus confidentiality-and-integrity-preserving per-audience key wraps; wrong key/context/authentication is typed refusal. Per-statement holes remain refused; interface/realization is the cut; size padding and `Secret`/`Pii`→`SecretRef` construction constraints remain. Commitments never promise zero leakage: length, timing, interface shape, wrap count, access pattern, low-entropy guessing, and churn stay in a named threat model. Remote execution and SCM diagnostics obey information-flow/noninterference + explicit declassification; metering prices an oracle but does not secure it, and unauthorized audiences receive only a non-leaking diagnostic projection. Cryptographic erase is conditional on every key/wrap/backup/cache/derivative/recovery path being sanitized and the result validated, never “delete one key.” → [node/subtree visibility grants design](docs/plans/node-subtree-visibility-grants.md) - **effect grants over namespaces — dissolve `Hermetic | Wet` into (frame × verb × subtree).** The 2-valued `Hermetic | Wet` enum conflates four axes (input closure / replayability, output reach / interference, handler binding, selection eligibility) — a §3 state-space conflation with proto-envelopes already forked (the hand-rolled `workspace_root` containment gate, `std.resources.ResourceHandle`, `AuthScope`, `LiveTreeDisposition`). End shape (§3, cited not minted): an effect target is a position in a containment tree that already exists — filesystem paths, URIs, `/proc`, service-operation paths, and the unifying case **code names themselves** (the containment tree the namespace-resolution lane makes the single naming authority); permission is a grant of (verb × subtree) attached to a `Frame`; admissibility is the namespace prefix relation — the same `⊑` the resolver walks, content-addressing hashes, and termination reads, so effects become the **fourth consumer** of the one containment structure. Dispatch checks the envelope fail-closed (`EffectOutsideGrant`, a typed/located/counted refusal, never a silent widen — §5); *replayable* / *isolated* / `LiveTreeDisposition` become **derived** projections, and `Hermetic | Wet | Record` survive only as named envelope presets, deleted at the end. Frame-containment is graded on the §5 construction/validation axis (`LifecycleByConstruction` vs `LifecycleByConvention`). No code lands from the design doc; the FLAG-A interim is the only near-term consumer. → [effect-namespace-grants design](docs/plans/effect-namespace-grants.md) - **shell → intent: the intent layer is language-blind (operator-aligned 2026-07-17; census/routing consolidated onto sibling authorities 2026-07-19).** §3/§4 restated for the shell case: the `.dag` intent may not name a target language — a workflow is an ordinary dependency graph over modeled operations (§4: a program is `Node`+`Edge`); *the intent IS the `.dag` graph* (operator), and `src/v2/std/orchestration.dag` `Pipeline` is optional sugar, never the authority. Two distinct downstream layers, not to be conflated: **emit** (§4 — `emit(intent, Bash)`, one grammar read backward, target as a *parameter*) and **realization** (§2 — pure-spec → host-effect, N transports `LocalShell`/`SshShell`, `dag/gunbc/host_effect_realize.dag`). **Routing (the load-bearing correction, operator review 2026-07-19): most sites do NOT emit bash.** Runtime-present sites (a gunbc runtime / typed transport is on the path) realize as typed service ops / typed transports / binary-interpreted effect-plan via `host_effect_apply` — no bash emitted (precedent already in tree: `floor_diff_observe` calls `git.Core.DiffUnified0` directly; `host_identity_converge` funnels through `host_effect_apply`); only foreign executors (GHA `run:`, cron, git hooks) and pre-runtime bootstrap windows render through the bounded v2 bash grammar rows. The authority for that routing is `authorize_shell_emission` (provisioning-window lane); the per-site residual census + arc-completion scoping is `shell-to-dag-residual-census-and-arc-completion.md` (witty-ibex-317) — DESIGN carries no parallel copy of either. **Invariant (enforceable):** the intent imports no language-construction vocab — emit-internal; `src/v2/lens/realization_vocabulary_containment.dag` confines it (LANDED #6854). The lens confines by MODULE, and naming what it actually reads is the whole point of §3's cite-the-symbol rule: `target_ast_vocab_module_prefixes` = `v2.extdeps.languages.bash`, `v2.workflow.bash`, plus `target_ast_vocab_modules` = `extdeps.languages.typescript.program`. The bash vocabulary is `v2.extdeps.languages.bash_build`'s `bash_build_*` constructors and the `v2.workflow.bash_command_fold_serialize` backend. *(Corrected 2026-07-31: this sentence previously listed the confined vocab as ``bash_build``, ``bash_command_fold_serialize``, ``the ShellStmt/ShellWord/ShellProgram coproduct``, ``serialize_bash``. The first two are real modules; the last two NAME NOTHING — the bash AST is built as generic `Node` (every `bash_build_*` fn returns `Node`), there is no ShellStmt/ShellWord/ShellProgram coproduct, and no `serialize_bash` is defined anywhere in the tree. Both were dissolved by the typed-WitnessBin migration (Phase D, 2026-07-14), which the `dag/tools/*` transport notes record and this row did not follow. The WALL WAS NEVER AFFECTED — the lens confines module prefixes, not these names, so nothing was unguarded; what rotted was the description of it, in the sentence declaring the invariant. That is the §3 positional/symbolic citation class exactly: a name-level claim in the canonical authority that a grep would have decided, sitting false while the mechanism it describes stayed correct. Found by a corpus citation sweep, not by anyone touching this row.)* Meta-exec confinement is **PARTIAL**: Wave A1 (#6900) module-isolated `shell.Exec.Run/.Check` and the import wall now has an empty exception roster; the still-open construction half is symbol/reference-level visibility that prevents allowed realization-edge modules from forming arbitrary transport values. *(Corrected 2026-07-26: this read "`host_language_transport_script` lens inert, `fail_closed_lockdown.dag`". That lens is **LIVE** — activated by #7184 as a per-PR `ReadsLiveTree` consumer that reds a new raw literal at an enrolled `Run` position — and the transport-script hole it was cited for is **closed by construction on the `ShellOnHost` route only**: `ShellOnHost.script` is the `RetainedShellScript` record and the free minter is deleted — while `shell.Exec.Run` itself still declares a transparent `TransportScript` brand over `String`, so a direct `as TransportScript` cast stays writable from any module and that sink is lens-guarded, not construction-walled (corrected 2026-07-27, `review 43600`; closing it is the meta-exec confinement milestone). The lens stays deliberately green on `ComputedApplication`, so it backstops literal blobs rather than being the wall. What remains open is the construction confinement named above, not the already-landed module-import wall.)* **Carriers → `Do{effect}` (never a command string, host-effect-orchestration.md):** the anemic `String` leaves are `host_effect.ShellCommand{script}`, `std.orchestration.Run.command`, and `host_effect.BootstrapFragment.script` — add-replacement → migrate all consumers → delete at arc close (§5 construction; `host_effect.dag` is load-bearing, the delete escalated). **Phases:** 0 sidecar delete LANDED (#6831) → 1 agnostic `While`/`BoundedPoll`/`Retry` emit PARTIAL (#6832; corrected 2026-07-26, verified against main — `While` framing has exactly one production consumer, `ci_floor_peak_emit`, whose cond and body are still raw shell strings; `BoundedPoll` has zero production construction sites, test-only; #7303 makes Retry refuse multi-step bodies, present redirect/capture, changed template log sources, and three-or-more (N-level) escalation shapes; #7318 additionally refuses every nonempty source `Run.env` while preserving empty-env emission bytes, proven by executing nonempty-refusal and empty-env accepted controls. Phase 1 remains PARTIAL only because the live `While` consumer has two raw command leaves, `BoundedPoll` is test-only, and the modeled scope still needs need-driven adoption or explicit narrowing) → 2 model runtime-present sites as typed effects on `host_effect_apply` + emit the foreign-executor/bootstrap residue → 3 wall green (bash-AST containment + meta-exec confinement). → [shell → intent design](docs/plans/shell-intent-emit-realization-design.md) -- **srvN build-cache provisioning on host-standup subsumption spine.** Legacy `ctrl-sccache.service` is misconfigured; `ci_release_build_script()` absorbing fallback (`CARGO_BUILD_JOBS=1` then `-u RUSTC_WRAPPER`) masks the deficit (§5). Design anchor: provision sccache as a host-effect ensure on the assimilation spine (P1b after `RunnerDeploySlot`), generalizing srv3's `WorkflowEnsureActuatorToolchain` to srvN; `ProvisionBuildCache { catalog_id }` routes through `host_effect_apply`; verdict fold is `ProvisionConverged | ProvisionRefused` only. STEP 2 (fallback removal) operator-sequenced after T4 live read-back. → [srvN build-cache provisioning design](docs/plans/srvn-buildcache-provisioning-design.md) +- **srvN build-cache provisioning on host-standup subsumption spine.** Legacy `ctrl-sccache.service` is misconfigured; `ci_release_build_script()` absorbing fallback (`CARGO_BUILD_JOBS=1` then `-u RUSTC_WRAPPER`) masks the deficit (§5). Design anchor: provision sccache as a host-effect ensure on the assimilation spine (P1b after `RunnerDeploySlot`), generalizing srv3's `WorkflowEnsureActuatorToolchain` to srvN; `ProvisionBuildCache { catalog_id }` routes through `host_effect_apply`; verdict fold is `ProvisionConverged | ProvisionRefused` only. **PLACEMENT IS A VERDICT AXIS (landed):** sccache executes cache-miss compiles as *children of the server*, so the server's cgroup is the cgroup every compile on the host runs in — a responsive daemon proves nothing about where it runs, and collapsing the two into one `DaemonStatsOk` arm is the §5 state-space conflation that let a lazily auto-spawned server (kept alive by `SCCACHE_IDLE_TIMEOUT=0`) strand itself in a dead slot's cgroup and bound all five live slots' compiles at one slot's cap. `BuildCacheServerPlacement = ServerManagedUnitOwned | ServerRunnerOwned{slot,lifecycle} | ServerUnsupervised | ServerPlacementAbsent | ServerPlacementUnknown` gates `ProvisionConverged`; unknown placement refuses rather than assuming owned, and BOTH runner lifecycles refuse (an active slot means birth ordering failed; a retired slot means birth ordering AND retirement both failed). It is derived from an endpoint-owner observation homed with the deployed-instance model in `gunbc.build_cache_instance` (`BuildCacheInstance` = host x consumer class x instance name, distinct from the CATALOG identity `sccache_local_id`), because process name plus cgroup cannot tell a server from the transient clients every compile spawns — the owner is the process holding the LISTEN on the configured endpoint. **LIVE RECEIPT 2026-08-01 (read-only, srv1/srv2/srv3):** NO host has a unit-owned CI cache; the birth defect is fleet-wide. srv1's owner is pid 3975946, 31.2 days old, in RETIRED slot `srv1-10` (inactive+disabled) with `max 229780` / `oom_kill 12` and a live runner's `rustc` resident beside it; srv2's and srv3's owners sit in ACTIVE slots and so self-limit only because ephemeral runners recycle them. srv4 was unreachable and is carried as typed `ReachRefused`, never as absent. A measured correction landed with it: `--show-stats` does NOT auto-start a server (private-endpoint controls, both UDS and TCP, 0 to 0 listeners) — it answers plausibly with NO server in existence, so stats may only ever be supplemental data read after ownership is grounded; a real COMPILE is what spawns a server, inheriting the invoking cgroup. The budget consequence is a first-class `compile_pool` child of the host allocation tree sized by the cited jobserver authority (`jobserver_build_pool_bytes`) — a per-slot memory cap does *not* bound compile RSS — with `CompilePoolPlacement` making the pool's size derive from the declared topology rather than a fudge. STEP 2 (fallback removal) operator-sequenced after T4 live read-back. → [srvN build-cache provisioning design](docs/plans/srvn-buildcache-provisioning-design.md) · [placement step-1 live receipt](docs/plans/build-cache-placement-receipt.md) - **srv4 fleet subsumption — manual-gap receipts (2026-07-22).** srv4 was installed and subsumed through the modeled BMC virtual-media path (PR #7027: 5 runners live, rootless Docker + sccache, seeded-media boot-fidelity fixes root-caused live); every step still performed by hand is typed as a modeling gap in the registered plan `gunbc.plans.fleet_subsumption_manual_gaps` (A1 MaterializedSecret lifecycle, A2 reach-secrets ADC, A3 SecretRef liveness, B1 runner-slot provisioning, B2 runner user, B3 fresh-host prereqs, B4 install actuator), each with an acceptance bar and RED; the plan dissolves item-by-item and fully retires when srv5 subsumes with zero hand-run shell. The BMC credential-rotation gap analysis that seeded the lane: → [srv4 BMC onboarding gap](docs/plans/srv4-bmc-onboarding-gap.md) - **gunb.ai site subsumption — hosting + frontend emitted from `.dag` (operator-directed 2026-07-20).** Subsume `gunb-ai/frontend`: the public site's HTML/CSS/JS become emitted, drift-gated artifacts in this repo, published to GitHub Pages at `gunb.ai` (DNS already points at Pages IPs; cutover is Pages-config-only; the frontend repo archives). Register thesis, developed across nine operator passes and carried as a signable checklist + behavioral-intricacy law: *quiet at arm's length; responsive up close; every response true* — simplicity is a D0/D1 property, intricacy a D2/D3 property of BEHAVIOR (coverage × consistency × timing, the Discord re-read), and every intricate thing shown or answered derives from a real repo fact. Phase-0 slices LANDED under `dag/gunbc/site/` (principles → tokens → interaction → mark → constellation → moodboard → theme_transition → starlight/accent_study), proven by execution with RED controls: themes as total role→material assignments (night = warm starlight on indigo, day = indigo ink on paper, one model, two projections; totality/area/contrast witnessed), the glare envelope (bright OR chromatic, never both — the operator-rejected gamer cyan is the negative control and provably absent from emissions), still-until-touched by construction (MotionTrigger has no ambient variant), the universal theme reframe derived from the same BuildRule rows (role-routed colors cross-fade for free on one token clock; an unthemed color is a counted census row; the data-theme watcher pins the flip so the fade cannot race the UA restyle — measured, snap → 12–16-frame blend), and the accent decision grounded in `extdeps.astronomy.stellar_classification` (cited MK/Harvard table; no green stars exist so green enters only as verdigris material; M self-prunes at the envelope; candidates O/A/G/K rendered from identical fact rows). Remaining: checklist row-by-row sign-off, primitives.html at every depth, mark candidates B–D, craft-floor scales, generated-artifact registration, Pages workflow + cutover receipt (discharges the `2-stateless-frontend` milestone-A shape on the public domain). → [site subsumption design](docs/plans/gunb-ai-site-subsumption-design.md) - **hierarchical design-register library — lift `gunbc.site` L1/L2 into `gunbc.design.*` (DOC-ONLY until #6927 merges).** PR #6927 proves the register on gunb.ai; the follow-on extracts site-independent L1 house walls + L2 role-material machinery (lift-not-fork), re-grounds contrast at L0 (WCAG/OKLCH, dissolving the HSL lightness-gap proxy in `theme_contrast_ok`), and stages L4 archetypes + `DataVizArea` behind second-consumer discipline. Phase A fires on #6927 merge with an emission-identical witness. The **palette catalogue** rode ahead additively (operator 2026-07-21): `extdeps.color.srgb` (CSS Color 4 cite; hex = a serialization of `Srgb8`, HSL→sRGB re-homed out of the WCAG module) → `gunbc.design.palette` (approval IS the computed WCAG grade — ordered figure-on-ground pairings total by construction) → `gunbc.design.catalog` (first entry island-pastel incl. the derived `loam` ink), witnessed by execution; its measured receipts (HSL-S misreads pastel tints as neon — ivory C 0.007 refused while `day_canvas` C 0.008 passes; gamer-cyan C 0.088 < mint C 0.142, so the envelope must be a per-area L×C region) are A.2's discriminating witness — see plan §2.2. → [design-register library](docs/plans/design-register-library.md) diff --git a/ROADMAP.md b/ROADMAP.md index 99bef0538e6..9f4961107cc 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -10,7 +10,7 @@ Parked context (non-dispatchable): [compiler algorithm survey](docs/plans/compil The graph has eleven lanes: SCM compatibility · namespace · P-derive · observation · placement · v1 exit (four finish lines: compiler fixed point, interpreter deleted, products v1-free, zero hand-maintained Rust) · shell · roadmap runtime · fleet/hygiene · hermetic toolchain · compiler-guarantee (the DESIGN §4b ladder climbs; rung STATE lives in the guarantee claims carrier and is emitted, never restated in tickets — [gap analysis](docs/plans/compiler-guarantee-recovery-gap-analysis.md)). The three independent SCM R0 models, the separate P−1 evidence carrier, R1 compatibility-shape extraction, and P0 user-contract/landing spine are accepted; the operator-authored P1 proof-kernel node is active and fail-closed pending its first discriminating closing validation, while P2 and later product lanes remain parked. The toolchain pin model is an independent root. The P-derive receipt feeds the emitter fixed point, so the v1 chain nests under it. Node fields define boundary, first slice, RED control, exclusions, owner, and handback. -**Focused view — the v1 deletion lane + the guarantee-ladder climbs.** 38 active deliverable(s) in other lanes are declared in the authority and hidden here; nothing is deleted or parked by focusing. Clear `roadmap_focus_selection` in `dag/gunbc/roadmap_authority.dag` to restore the full page. 10 of the rows below are NOT lane deliverables — they are prerequisites pulled in from other lanes because lane work is blocked on them: namespace-canonical-binding, namespace-ambiguity-discharge, namespace-reference-derived-closure, namespace-reference-derived-closure-closing-contract, v1-seed-honesty-decision, v1-seed-honesty-decision-closing-contract, namespace-legacy-choice-retirement, namespace-v2-import-completion, namespace-bound-only-consumers, namespace-import-deletion. Hidden lanes remain readable through their carriers: [docs/plans/dag-scm-design.md](docs/plans/dag-scm-design.md) · [dag/gunbc/plans/cli_run_hollowing_plan.dag](dag/gunbc/plans/cli_run_hollowing_plan.dag) · [docs/plans/progress-observation-design.md](docs/plans/progress-observation-design.md) · [docs/plans/inner-cost-lanes-scoping.md](docs/plans/inner-cost-lanes-scoping.md) · [docs/plans/ci-two-tier-placement-redesign.md](docs/plans/ci-two-tier-placement-redesign.md) · [docs/plans/shell-to-dag-residual-census-and-arc-completion.md](docs/plans/shell-to-dag-residual-census-and-arc-completion.md) · [docs/plans/roadmap-workspace-ux-plan.md](docs/plans/roadmap-workspace-ux-plan.md) · [docs/plans/host-converge-inventory.md](docs/plans/host-converge-inventory.md) · [docs/runbooks/bmc-redfish-operator-access.md](docs/runbooks/bmc-redfish-operator-access.md) · [docs/plans/host-effect-orchestration.md](docs/plans/host-effect-orchestration.md) · [docs/plans/gunbc-served-dashboard-design.md](docs/plans/gunbc-served-dashboard-design.md) · [docs/plans/fleet-acceptance-criteria.md](docs/plans/fleet-acceptance-criteria.md) · [docs/plans/hermetic-tool-provisioning-design.md](docs/plans/hermetic-tool-provisioning-design.md). +**Focused view — the v1 deletion lane + the guarantee-ladder climbs.** 35 active deliverable(s) in other lanes are declared in the authority and hidden here; nothing is deleted or parked by focusing. Clear `roadmap_focus_selection` in `dag/gunbc/roadmap_authority.dag` to restore the full page. 10 of the rows below are NOT lane deliverables — they are prerequisites pulled in from other lanes because lane work is blocked on them: namespace-canonical-binding, namespace-ambiguity-discharge, namespace-reference-derived-closure, namespace-reference-derived-closure-closing-contract, v1-seed-honesty-decision, v1-seed-honesty-decision-closing-contract, namespace-legacy-choice-retirement, namespace-v2-import-completion, namespace-bound-only-consumers, namespace-import-deletion. Hidden lanes remain readable through their carriers: [docs/plans/dag-scm-design.md](docs/plans/dag-scm-design.md) · [dag/gunbc/plans/cli_run_hollowing_plan.dag](dag/gunbc/plans/cli_run_hollowing_plan.dag) · [docs/plans/progress-observation-design.md](docs/plans/progress-observation-design.md) · [docs/plans/inner-cost-lanes-scoping.md](docs/plans/inner-cost-lanes-scoping.md) · [docs/plans/ci-two-tier-placement-redesign.md](docs/plans/ci-two-tier-placement-redesign.md) · [docs/plans/shell-to-dag-residual-census-and-arc-completion.md](docs/plans/shell-to-dag-residual-census-and-arc-completion.md) · [docs/plans/roadmap-workspace-ux-plan.md](docs/plans/roadmap-workspace-ux-plan.md) · [docs/plans/host-converge-inventory.md](docs/plans/host-converge-inventory.md) · [docs/runbooks/bmc-redfish-operator-access.md](docs/runbooks/bmc-redfish-operator-access.md) · [docs/plans/host-effect-orchestration.md](docs/plans/host-effect-orchestration.md) · [docs/plans/gunbc-served-dashboard-design.md](docs/plans/gunbc-served-dashboard-design.md) · [docs/plans/fleet-acceptance-criteria.md](docs/plans/fleet-acceptance-criteria.md) · [docs/plans/hermetic-tool-provisioning-design.md](docs/plans/hermetic-tool-provisioning-design.md). - [ ] **Record which generator actually produced each result** — Every result says which generator made it and how far it got, as two separate facts. Claiming the new generator made something, without the evidence that it did, has to be impossible to write down. Why: Right now everything is made by the old generator, and the count we track reads as if the new one made it. We are measuring the wrong thing, so we cannot tell how far along we are. [authority](src/v2/compiler/self_host/frontier.dag) - [ ] **Have the new compiler write out Rust source itself** — Read the source, work out the types, translate, and write real Rust to disk without the old generator deciding any of it. The old runner may still be what executes this; it just stops making the decisions. Why: Until the new compiler can write Rust on its own, every improvement has to go through the generator we are trying to delete. [authority](src/v2/compiler/self_host/candidate_generation.dag) @@ -31,7 +31,7 @@ The graph has eleven lanes: SCM compatibility · namespace · P-derive · observ - [ ] **Switch the build over to the new compiler** — The real build runs on the new path, with cold comparisons against the old one first. Why: Moves the thing that actually has to work every day, instead of proving a replacement nobody uses. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-emitter-fixed-point, v1-honest-frontier, v1-verification-ledger - [ ] **Run everything with the old generator taken away** — The checks and the build use what the new generator produced, nothing falls back to the old generator, and nothing is left calling it. Why: Separates deleting the old generator from deleting the old runner, so the generator can go first instead of waiting on everything else. [authority](src/v1/05_emit_rust.dag) - [ ] **Delete the old Rust generator** — The old generator itself is removed, along with the one Rust file it generated to run itself, the Rust option in the old build path, and the fixtures, helpers and measurements that existed only for it. What produced a file in the past does not decide whether it goes: any Rust the new generator now produces stays exactly where it is. Why: Removes the second set of Rust-generation rules for good — the split the shared rules were meant to end. [authority](src/v1/05_emit_rust.dag) - - [ ] **Build the products with the old runner left out entirely** — A rehearsal: build without the old runner included at all, and run the products and the build both cold and warm. Why: Proves nothing still reaches for it, which is the thing that actually makes deletion safe. [authority](dag/gunbc/v1_deletion_plan.dag) + - [ ] **Build the products with the old runner left out entirely** — A rehearsal: build without the old runner included at all, and run the products and the build both cold and warm. Why: Proves nothing still reaches for it, which is the thing that actually makes deletion safe. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-ci-floor-cutover, v1-interpreter-primitive-roster - [ ] **Delete the old runner** — Delete it and everything now unreachable, in one change, once the rehearsal above holds. This is about the runner only — not the rest of the old code, and not hand-written Rust generally. Why: Removes the second, slower way of running everything, and with it the possibility of the two disagreeing. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-interpreter-quarantine, v1-emitter-delete - [ ] **Build and run every product without the old code present** — With the old runner gone and the hand-written queue worked through, the products build and run with the old directory absent. Anything that must stay gets an explicit reason. Why: Separates 'the products no longer need the old code' from 'the old runner is gone', so neither claim waits on the other's leftovers. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-interpreter-delete, v1-hand-queue-drain - [ ] **Get hand-written Rust in this repository down to zero** — Every Rust file we track is either generated from a description or deleted. Hand-maintaining Rust is a stage we are passing through, not where we stop. Why: Every hand-written file is a permanent tax: the behaviour and its description have to be kept in step by hand, forever. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-products-v1-free, v1-test-migration, v1-seed-honesty-decision, namespace-legacy-choice-retirement, namespace-import-deletion @@ -46,29 +46,52 @@ The graph has eleven lanes: SCM compatibility · namespace · P-derive · observ - [ ] **Make sure deleting the old tests loses no coverage** — Every old test file removed has equivalent coverage that would actually catch the bug it was written for. Why: A guard that stays quiet is not the same as coverage; without this, deletion silently drops real protection. [authority](dag/gunbc/v1_deletion_plan.dag) - [ ] **Write the closing check for: Decide whether the trust-the-compiler check has to happen first** — One executable check states when this is done, transcribed from the node's own bar: Feeding the same thing in as both the claim and the check cannot pass. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](dag/gunbc/v1_deletion_plan.dag) - [ ] **Decide whether the trust-the-compiler check has to happen first** — A decision: does independently rebuilding with a different compiler have to happen before deletion? And first, make that check capable of failing. Why: The check currently passes no matter what, because it compares a thing to itself — so any confidence from it is imaginary. [authority](dag/gunbc/v1_deletion_plan.dag) -- [ ] **Write the closing check for: The expecting-red probe corpus: the floor gap becomes a counted, continuously-measured number** — One executable check states when this is done, transcribed from the node's own bar: A probe that stops expecting red without its class's wall having landed is itself a red (greening is counted); the corpus reds if any probe pair loses its executing consumer. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **The expecting-red probe corpus: the floor gap becomes a counted, continuously-measured number** — One probe pair per floor class (deliberately-bad input expected to refuse; legitimate control expected to accept), enrolled as expecting-red rows via the known-red quarantine mechanism so today's fail-open state is measured, not asserted. Every currently-legitimate map/fold/filter/flat_map/service invocation form appears as a positive control so a future wall cannot green by refusing the language. Why: Replaces anecdote (gunbc#7479's 500) with a counted deficit per class; each wall that lands flips its probes to regression controls loudly. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **The guarantee claims carrier: rung state becomes DERIVED from execution receipts, never stored** — Four carriers, measurement separated from claim: GuaranteeRequirement (class identity, domain, harm, ceiling with typed reason, next_rung_trigger); GuaranteePath (subject_grain, acceptance_boundary, compile_mode, realization_target — one row per in-scope path, population derived from the census or completeness-witnessed); GuaranteeMeasurement (executed receipts naming their consumer); and GuaranteeDisposition DERIVED per path — Unmeasured | BelowFloor | FrontierAccepted | OnLadder | OutsideModeledGuarantee — folded by below-floor dominance, then frontier, then unmeasured, then MINIMUM rung across paths. No stored current_rung field exists, so a transcribed rung is unrepresentable; the five recovered vocabularies stay orthogonal, joined by class identity. Full shape: gap analysis sec 12 Stage 1b. Why: Ends the per-conversation re-derivation of what the compiler guarantees (the operator performed the missing meta-lens by hand three times this month); grounds the enforcement-intent StandingIntent thread instead of minting beside it. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) +- [ ] **Write the closing check for: Make the interpreter's primitive surface enumerable** — One executable check states when this is done, transcribed from the node's own bar: A dispatch arm added without a roster row, and a roster row with no arm, must both be unwritable rather than caught afterwards. A spelling reached by two different arms within one form is unreachable code and has to be named rather than silently accepted. A roster that cannot represent an arm carrying no semantic primitive identity fails this node outright, because that population is exactly what the identity join must discover. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](dag/gunbc/v1_interpreter_primitive_surface.dag) + - [ ] **Make the interpreter's primitive surface enumerable** — Every interpreter dispatch arm -- free call, method, guard-predicated bridge, native special and the one service carve-out -- has one interpreter-local identity, dispatch form and authored spelling, enumerable independently of semantic primitive identity. Adding or deleting an implementation arm without changing the roster refuses. Why: The interpreter's primitive surface had no roster anywhere, so nobody could ask which primitives it provides. That blocks the primitive-identity join at its fifth surface, and it blocks sequencing v1 deletion honestly: you cannot show nothing still needs a capability you cannot enumerate. [authority](dag/gunbc/v1_interpreter_primitive_surface.dag) +- [ ] **Write the closing check for: The guarantee measurement schema: evidence learns to identify its subject before anyone measures** — One executable check states when this is done, transcribed from the node's own bar: A receipt missing any identity component is unwritable (required fields, no Optional arms); a duplicate class id reds the registry witness. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **The guarantee measurement schema: evidence learns to identify its subject before anyone measures** — The stable vocabulary only: GuaranteeClassId, GuaranteePathId, GuaranteeProbeId; GuaranteePath (subject_grain, acceptance_boundary, compile_mode, realization_target); GuaranteeMeasurementReceipt carrying class, path, probe, subject_revision, harness_revision, probe_set_digest, observed disposition. No stored rung, no claims about the compiler — only how a receipt names what it measured. This stage exists to break the probes-need-carrier-identities / carrier-needs-probe-receipts protocol cycle (operator spine verdict, 2026-08-01): probes and claims meet through this contract instead of each minting temporary string keys. Why: Without it the probe child mints throwaway identifiers that immediately require migration, and two carriers of the same fact drift. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **The expecting-red probe corpus: the floor gap becomes a counted, continuously-measured number** — One probe pair per floor class (deliberately-bad input expected to refuse; legitimate control expected to accept), enrolled as expecting-red rows via the known-red quarantine mechanism so today's fail-open state is measured, not asserted. Every currently-legitimate map/fold/filter/flat_map/service invocation form appears as a positive control so a future wall cannot green by refusing the language. Why: Replaces anecdote (gunbc#7479's 500) with a counted deficit per class; each wall that lands flips its probes to regression controls loudly. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Baseline prevalence: the whole-corpus floor rerun bucketed by ladder position, before any wall lands** — A TWO-REVISION execution, never 'run the baseline at commit X' (three-way ambiguous): the anchor's compiler artifacts (subject_revision: anchor_commit 6c6e2dcb8587d73350ac252f5b07a6b50d684485, the gunbc#7489 merge — pre-P0 main) run under today's content-addressed probe set and classifier (harness_revision + probe_set_digest per the measurement schema), outcomes translated into measurement receipts keyed to carrier class ids. Bucket every failure — statically-decidable-should-have-refused, runtime-value-dependent, external-boundary, resource-budget, capability-not-grounded, interpreter-defect. Replaces the carried-but-unverified historical figures (1454/149; the 66/40/6 histogram). Runs parallel with the census emitters, not ahead of them. Why: Denominates the climbs in displaced cost (DESIGN 6) instead of elegance; the honest number for how many programs the gap admits today. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: ladder-probe-corpus, ladder-claims-carrier + - [ ] **Receiver normalization: the measured resolution residue becomes decidable receivers** — The frontier's measured causes, repaired upstream so the wall can decide them: a lambda-parameter receiver whose type never propagates from the declared fn type it is bound under; a coproduct payload bound by destructuring typed as its variant name; the optional receiver whose cardinality drops before lookup; the brand-alias LEAF form the peel cannot reach (qualified-name leaf, measured and declared on the gunbc#7484 frontier roster). Each repair shrinks the roster in both directions — correct calls resolve, bogus methods refuse. Why: Every roster row this discharges converts an admitted-by-key site into a decided one; the frontier ratchet only shrinks. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Zero-resolution refuses generally: enumeration over the union of admissible sources** — Absence is decided by enumerating the union of current admissible sources — no identity join required (the join gates only the more-than-one wall). Delete the resolve_builtin_call_type Absent-to-unit_type fabrication; every call whose callee resolves nowhere refuses with name and locus on every compile/emit path. Why: Closes the callable-existence class fully: the last fabricated-success arm at the call seam dies. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Missing and duplicate arguments: the interpreter-first parity pair** — The interpreter itself does not refuse these today — a missing required arg decays to NoSuchVariable at read (or silence if unread); a duplicate label silently overwrites in bindings.insert — so a compile-only wall would diverge the authorities the other way. Land the refusals in call_function_inner FIRST (default-value-aware: param_node_default_value legitimately fills omissions), then mirror at the compile seam, keeping the two authorities in lockstep exactly as the label/surplus pair did. Why: A missing argument surfacing as NoSuchVariable inside the body blames the wrong site; a duplicate silently taking the last value is unlocatable. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Signature-resolution coverage: the sig-unresolved fallthrough stops being a blind spot** — A call whose callee resolves NO signature bypasses every call-shape judgment (measured: 3 label fossils invisible to the landed wall in dag_collect, found only by grep). Either the callee resolves and is judged, or the failure to resolve is itself a typed, counted fact — never a silent fallthrough to the method bridge. Rides the namespace lane's resolution coverage; this node owns the call-shape consequence. Why: The wall's own measured blind spot: correctness there currently rests on grep. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Conformance grounding: the four representation classes stop being unjudgeable** — The measured false-positive classes that cap the ground fragment, each a §3 grounding defect not a checker defect: brand aliases unpeeled to their base; optionality living in two representations (return_cardinality vs the nominal Optional coproduct); anonymous record literals never grounded against their expected nominal; declaration-vs-body expansion depth. Ground each so the conformance relation widens class by class — the ground-fragment gate exists only because these are unjudgeable today. Why: Each grounded class moves thousands of silent declarations into the judged set (measured: the expansion-depth class alone was half the 3005-row frontier). [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **The general conformance wall: every grounded declared type constrains its producer** — As grounding lands per class, the conformance judgment widens to full return/data coverage — the gate dissolving into the relation until every grounded declared type refuses non-inhabiting producers, with whatever residue remains typed and counted, never silent. Why: Completes the keystone: a declared type is a contract everywhere, not per-fragment. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Argument-type compatibility grounding: adjudicate what the exemption papers over** — module_skips_direct_call_arg_check exempts v2.*/v1.compiler.* from the argument TYPE judgment because of representation-gap false positives — the same four classes the conformance lane grounds, at the argument seam, plus the unlocated historical '104 TypeMismatch false positives' (no receipt found by two audits; adjudicate or refute it by measurement). Ground the argument-position relation so the exemption's reason dissolves. Why: The compiler's own sources are currently un-judged at every argument position — the largest single acceptance hole by module count. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Delete module_skips_direct_call_arg_check: the compiler stops authoring itself under weaker checks** — Gated on the two GROUNDING nodes, not the label wall (dependency corrected per the operator spine verdict: the exemption skips the argument TYPE judgment, whose false positives are representation gaps — labels never gated it, and gunbc#7519's wall already runs exemption-free). Once argument-type-compatibility grounding and declared-conformance grounding land, re-run exemption-free, repair relation or source per the bucketed census, then delete. Its deletion is the terminal Tier-1 acceptance named by the doc-graph bind on this lane's analysis. Why: The dimension contract's one named escape hatch (enforced universally — no escape hatch, no wrapper functions); v1 currently admits unsound v2 source that v2 then self-certifies, and v1 deletion would not retroactively prove it. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: argument-type-compatibility-grounding, declared-conformance-grounding + - [ ] **ExhaustivenessUnknown refuses: the PatternLookupBlocked silent arm closes** — lookup_variant_in_type and lookup_field_in_variant answer PatternLookupBlocked with diagnostics: [] — exhaustiveness-not-established passing as success-adjacent. Split the verdict three ways (Exhaustive | NonExhaustive | ExhaustivenessUnknown) and refuse Unknown into any accepted executable artifact. Why: Closes the fallback-arm shape the census names for blocked lookups — exhaustiveness-not-established passing as success-adjacent; the measured disposition lives in the carrier, not this ticket. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Field access through an unconstrained generic refuses: delete the field_of_type_var fabrication** — field_of_type_var answers a field projection on a bare type variable by fabricating the field type — compile-time plausible output where no fact exists (the census's field-through-generic class). The wall: field projection on a type parameter with no field-carrying bound refuses with the parameter, field name, and locus; concrete receivers and parameters whose bound carries the field stay green. Why: A fabricated field type flows into inference and surfaces as a downstream mismatch or a wrong runtime value far from its cause; a located refusal moves the diagnosis to the projection site. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Parse formation refuses: a dropped separator cannot silently change the program** — Two slices, one wall. Safety: a missing list separator refuses at parse instead of parsing as element juxtaposition (the measured below-floor census row — a dropped comma silently changes list arity). Completeness: a block after a bare identifier in if-condition position parses as the block it is, closing the measured false refusal (expected-RParen on legitimate input). Probe-paired via the corpus's parser pair; grammar-owned, no host parser (census queue items 6-7). Why: The separator specimen survived regen, whole-corpus compile, fixed-point verify, and a 15-case matrix; only a human diff read caught it (review 45347). [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Record construction refuses: missing required fields, bad defaults, ungrounded nominal literals** — A record literal missing a required field refuses at compile with the field named; defaults apply only where declared; a literal claiming a nominal type it does not ground refuses. Owns the census's record-completeness class — the declared-conformance chain owns return, data, and generic instantiation, not this. Probe-paired via the corpus's record-completeness pair. Motivating specimen: HandAuthoredDocBind's primary_work construction wall currently rests on record-completeness enforcement that is itself unmeasured. Why: A construction wall anywhere in the corpus is only as strong as required-field enforcement; this node is what makes primary_work-style walls load-bearing rather than declared. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **The guarantee claims carrier: rung state becomes DERIVED from execution receipts, never stored** — Four carriers, measurement separated from claim: GuaranteeRequirement (class identity, domain, harm, ceiling with typed reason, next_rung_trigger); GuaranteePath (subject_grain, acceptance_boundary, compile_mode, realization_target — one row per in-scope path, population derived from the census or completeness-witnessed); GuaranteeMeasurement (executed receipts naming their consumer); and GuaranteeDisposition DERIVED per path — Unmeasured | BelowFloor | FrontierAccepted | OnLadder | OutsideModeledGuarantee — folded by below-floor dominance, then frontier, then unmeasured, then MINIMUM rung across paths. No stored current_rung field exists, so a transcribed rung is unrepresentable; the five recovered vocabularies stay orthogonal, joined by class identity. Full shape: gap analysis sec 12 Stage 1b. Why: Ends the per-conversation re-derivation of what the compiler guarantees (the operator performed the missing meta-lens by hand three times this month); grounds the enforcement-intent StandingIntent thread instead of minting beside it. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: ladder-measurement-schema, ladder-probe-corpus - [ ] **The operator's seam, end to end: a 0..n producer cannot feed a 1..n consumer** — One path, not a census: bare List = 0..inf, literal [] = 0..0, nonempty literal = n..n; map preserves, filter floors to 0, concat adds; the interval is an interface fact in InterfaceSummary's contract slot, actual-within-required enforced at function boundaries with a located seam refusal. The seam direction (2026-07-04) and candidate lattice exist; the in-tree fold-propagation rows are manual value-level specimens — binding-level propagation starts here. Construction-wall first candidate: type Refined sole_constructor plus deleting refined_vacuous_stub_pack's forged rejection arm, which is why the sole-constructor completeness audit is a declared prerequisite edge. Why: The canonical motivating specimen — simple, structural, explainable without compiler expertise; the first differentiating class an outside reader can verify. [authority](docs/plans/cardinality-refinement.md) — requires all: ladder-claims-carrier, unforgeable-construction-capability - - [ ] **v2 stops treating source structure as inferred fact: the self-grounding slice becomes a typed frontier** — The strict-narrowing slice, carried today by open candidate gunbc#7485 (candidate evidence on an open branch, never main's rung): exact self-evidence leaves solve_constraints' generic arm; underived facts become typed, located, counted GroundingNotDerived diagnostics inside an accepted InferredTree — the FrontierAccepted disposition's live specimen. Identity admission reads the authored declared-inhabitant roster inside the authority, never caller-supplied candidates; Bool/Int literal derivations raised via the canonical literal query. Closes when the slice is on main with its witnesses enrolled. Why: Silent self-certification becomes a counted frontier: the deficit ranks for fixing instead of reading as green (the absorbing-fallback rule applied to inference). [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **Translate propagates the refusal: an underived fact cannot be swallowed downstream** — gunbc#7485 records that Translate can swallow the new GroundingNotDerived rejection and continue — the frontier diagnostic exists but the emission path proceeds. This node makes Translate refuse or thread the typed frontier so Eval and Translate agree: compile-time refusal on both is the class minimum (post-merge verdict grid). The per-path grain is exactly why the carrier models paths separately. Why: Closes the path split where the same underived fact refuses in one phase and ships in another — the subject-grain failure mode measured on the call-shape class. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **InferredTree carries only derived facts: the all-derived carrier lands** — Separate source and inferred carriers so candidate-equals-source is not even well-typed as a grounding proof: every fact on the inferred carrier traces to a derivation or the node refuses. Branch/Match/Loop (the three real inference paths) migrate first; the validate_then_compile door and its lens roster stay. A well-formed node with no derived facts cannot inhabit the carrier. Why: InferredTree becomes a trustworthy proof boundary; every lens consuming it stops reading facts that were never derived; the self-host frontier stops reading greener than it is. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **Every node kind derives or refuses: the generic acceptance arm deletes** — The remaining derivation rules beyond Branch/Match/Loop: each v2 node kind either derives into the inferred carrier or its acceptance is a typed compile refusal — the generic accept-anything arm deletes. Coverage is counted per kind, so the frontier shrinks to zero or to named FrontierAccepted rows each carrying a trigger. Why: The last self-certification residue: any kind without a rule is currently accepted on well-formedness alone, which is the recovered defect class in its most general form. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **Realization per target: an operation cannot compile for T without exactly one realization** — The realized carrier adds target realization: every executable operation in a program accepted for target T has exactly one compatible realization row for T, refused target-relatively otherwise. gunbc#7485 explicitly leaves this open; it consumes the primitive-identity join (realization rows are its per-target facts) and the derivation-coverage node (a realized tree is derived-then-realized, never source-then-realized). Why: The gunbc#7479 shape at its root: a costed-but-unrealized primitive currently compiles and fails at dispatch; the gate moves that to a located target-relative refusal. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: v2-node-kind-derivation-coverage, primitive-identity-join - - [ ] **Accepted means established: the acceptance-completeness door closes the contract** — The terminal P0 wall (the recovered P0.0 meta-guarantee — no exhaustive acceptance contract exists today): for every Required guarantee path in the claims carrier, exactly one live consumer, consumed before the relevant Accepted constructor; acceptance refuses when a required class or path is Unmeasured, BelowFloor, FrontierAccepted, or lacks its live consumer. Depends on the carrier and EVERY required-now climb — the floor walls, the ambiguity wall, and the cardinality seam included (review 45545: a door closing over known-open required judgments would certify its own headline falsely). Why: The original thesis promise — if it compiles, declared intent is sound inside the modeled system — becomes checkable instead of aspirational; the carrier turns from observability into the contract. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: ladder-claims-carrier, floor-method-existence-wall, compiler-source-exemption-removal, floor-record-construction-wall, floor-generic-field-constraint-wall, floor-exhaustiveness-refusal, floor-parse-formation-wall, v2-translate-underived-refusal, v2-target-realization-gate, floor-method-ambiguity-wall, cardinality-vertical-slice - - [ ] **Residual prevalence: the post-climb rerun that prices what remains** — The baseline's bucketing classifier re-run after the declared climbs land, diffed against the anchored baseline: per-class delta receipts showing which buckets each wall emptied, and the residual histogram pricing the next round (DESIGN 6 displaced-cost denomination). Depends on the acceptance-closure door alone — every required climb, ambiguity and the cardinality seam included, now flows through that door (review 45545) — so a partial rerun cannot masquerade as the residual. Why: Turns each wall's landing into a measured before/after instead of an assumed win; the residual table is the priced backlog for the next design round. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **The acceptance door as mechanism: audit first, blocking later** — Split from the monolithic closure node (operator spine verdict: a door gated on the whole program delays the most important new mechanism until nearly done). This node lands the MECHANISM only, depending on the carrier alone: read every Required guarantee path, derive the missing/unmeasured/frontier/no-live-consumer census, run audit-mode and expecting-red — it reports, it does not yet block, and it never claims the open classes closed. The recovered P0.0 meta-guarantee gets its executable form here. Why: The every-Required-path-has-a-live-pre-Accepted-consumer relation becomes executable years before the last wall lands. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Floor activation: the ordinary compiler floor flips to blocking** — The door mechanism flips from audit to refusal over the FLOOR domain only — names, call shape, conformance, fields, exhaustiveness, parse formation, record construction, the exemption gone: acceptance refuses when a floor class or path is Unmeasured, BelowFloor, FrontierAccepted, or lacks its live consumer. Why: Below-baseline safety regressions become unshippable — the floor stops depending on review vigilance. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: accepted-obligation-door-mechanism, call-missing-and-duplicate-wall, call-signature-resolution-coverage, method-zero-resolution-general-wall, method-call-shape-seam, declared-conformance-general-wall, compiler-source-exemption-removal, floor-exhaustiveness-refusal, floor-generic-field-constraint-wall, floor-parse-formation-wall, floor-record-construction-wall + - [ ] **Cardinality joins the contract: the seam class flips to Required-blocking** — Admit the cardinality-seam class into the door's blocking Required set — one class, one flip (per-class admission nodes replace the monolithic extended activation, review 45918: an admission becomes ready the day its OWN climb lands instead of every extended class waiting on the slowest). After the flip, acceptance refuses a program whose cardinality judgment is Unmeasured, BelowFloor, FrontierAccepted, or consumer-less. Why: The first differentiating class stops being observability and becomes contract. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: accepted-floor-obligation-closure, cardinality-vertical-slice + - [ ] **Extended closure: the full contract roster certified complete** — The terminal certification over the per-class admissions (recut per review 45918 — requires-all belongs HERE, on the completion receipt, never on the admission work, which is per-class and ready as each climb lands): every extended admission receipt present, the door's Required roster equal to the declared extended set — nothing climbed-but-unadmitted, nothing admitted-but-unclimbed — the full recovered P0.0 contract in force. Why: The thesis promise — if it compiles, declared intent is sound inside the modeled system — becomes the shipping contract. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: extended-admission-cardinality, extended-admission-ambiguity, extended-admission-v2-realization, extended-admission-v2-translate + - [ ] **Residual prevalence: the post-climb rerun that prices what remains** — The baseline's bucketing classifier re-run after the declared climbs land, diffed against the anchored baseline: per-class delta receipts showing which buckets each wall emptied, and the residual histogram pricing the next round (DESIGN 6 displaced-cost denomination). Depends on the acceptance-closure door alone — every required climb, ambiguity and the cardinality seam included, now flows through that door (review 45545) — so a partial rerun cannot masquerade as the residual. Why: Turns each wall's landing into a measured before/after instead of an assumed win; the residual table is the priced backlog for the next design round. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Method ambiguity joins the contract** — Admit the method-ambiguity class into the door's blocking Required set once its wall lands (per-class admission, review 45918). After the flip, a program with a more-than-one-resolution judgment left Unmeasured, BelowFloor, FrontierAccepted, or consumer-less refuses at acceptance. Why: The silent-first-hit dispatch class becomes contract, not census. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: accepted-floor-obligation-closure, floor-method-ambiguity-wall + - [ ] **Target realization joins the contract** — Admit the target-relative realization class into the door's blocking Required set once its gate lands (per-class admission, review 45918). After the flip, a program accepted for target T with a realization judgment left Unmeasured, BelowFloor, FrontierAccepted, or consumer-less refuses at acceptance. Why: The costed-but-unrealized dispatch failure class becomes contract. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: accepted-floor-obligation-closure, v2-target-realization-gate + - [ ] **Translate containment joins the contract** — Admit the Translate-containment class into the door's blocking Required set once the refusal lands (per-class admission, review 45918). After the flip, a program whose underived-fact judgment is Unmeasured, BelowFloor, FrontierAccepted, or consumer-less refuses at acceptance. Why: The documented Translate fail-open, once closed, stays closed by contract rather than vigilance. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: accepted-floor-obligation-closure, v2-translate-underived-refusal - [ ] **Emit the ladder: per-class rung state renders to ROADMAP.md and the served roadmap page from the carrier** — A projection from the guarantee carrier's derived dispositions to (a) a generated ROADMAP.md state table and (b) a roadmap-page panel via the existing ServedRoute machinery — one authority, two projections, drift-gated like DESIGN.md and ROADMAP.md today. The gunb.ai site inherits it through the site-subsumption lane's rule that every shown intricacy derives from a real repo fact. Why: The operator reads ladder position without asking; the state table cannot rot because it is emitted, and a hand-edit is a drift refusal. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **Baseline prevalence: the whole-corpus floor rerun bucketed by ladder position, before any wall lands** — Re-run the whole-corpus floor and bucket every failure — statically-decidable-should-have-refused, runtime-value-dependent, external-boundary, resource-budget, capability-not-grounded, interpreter-defect — keyed to the carrier's class ids: the honest BEFORE picture. Replaces the carried-but-unverified historical figures (1454/149 and the 66/40/6 histogram) with current receipts. Anchored: anchor_commit 6c6e2dcb8587d73350ac252f5b07a6b50d684485 (the gunbc#7489 merge — pre-P0-implementation main), so the measurement is content-addressed and reproducible after in-flight branches merge; walls sequence after it without racing a live tree (post-merge verdict). Why: Denominates the climbs in displaced cost (DESIGN 6) instead of elegance; the honest number for how many programs the gap admits today. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: ladder-probe-corpus, ladder-claims-carrier - - [ ] **Method existence: an unresolved method refuses at compile on every applicable path** — An unresolved method refuses at compile with name, receiver type, and locus: delete the method_pipe_map_keys_values_fallback else-arm and the resolve_builtin_call_type Absent-to-unit_type arm. Main today: compile-time method existence unenforced outside existing paths. Open candidate gunbc#7484 carries narrow R2 coverage for established receiver surfaces plus a typed, countable MethodExistenceUndecided frontier elsewhere — candidate evidence on an open branch, never main's rung (post-merge verdict). Zero-resolution is decided over the union of current admissible sources — enumeration suffices for absence — so the identity join gates only the more-than-one half, not this node. First: receiver normalization, then zero-resolution refusal. Why: The gunbc#7479 class (typechecked filter_map, HTTP 500 at dispatch) becomes unwritable-to-ship; the compile gate stops being quieter than the interpreter. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **Method ambiguity refuses: more than one resolution is a located error once primitive identity defines sameness** — The method-existence wall closes cardinality zero; this closes more-than-one — two distinct primitive identities (or a primitive and a user fn) resolving for one receiver/method pair refuse with both candidates named. Gated on primitive-identity-join because ambiguity is decidable only when sameness is: the length/count/size dispatch aliases make one method look like three today (the gunbc#7484 measured fork), so an ambiguity check before the join would refuse legitimate corpus or silently pick. Why: The silent-first-hit dispatch class: today arm order decides which of two candidates runs — an unstated tiebreak the census cannot even measure until sameness is defined. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: floor-method-existence-wall, primitive-identity-join - - [ ] **Call shape: the silent positional label bind gets loud, then exact bijection** — Floor LANDED (cool-badger-514): unknown label and surplus positional refuse at the direct-call seam, blocking, exemption-free — the two classes the interpreter's call_function_inner already refuses, mirrored so the authorities agree. Census refused 28 live rename fossils, all fixed. Remaining wall: duplicate and missing land as an interpreter-first parity pair; method-pipe seam with the method wall. ArityMismatch stays type-constructor grain. Why: A misspelled label silently computing the wrong thing is the harm shape DESIGN 5 prices at interest, paid by the end user; the landed floor retires it at the direct-call seam. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **Delete module_skips_direct_call_arg_check: the compiler stops authoring itself under weaker checks** — Run the call-shape and inhabitance probes over v2.* and v1.compiler.*, classify every failure fresh (the historical 104 TypeMismatch figure has no locatable receipt and is neither blocker nor promise), repair the compatibility relation or the source, then delete the exemption. Its deletion is the terminal Tier-1 acceptance named by the doc-graph bind on this lane's analysis. Why: The dimension contract's one named escape hatch (enforced universally — no escape hatch, no wrapper functions); v1 currently admits unsound v2 source that v2 then self-certifies, and v1 deletion would not retroactively prove it. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: floor-call-shape-wall, floor-inhabitance-wall - - [ ] **Declared types constrain produced values: return, data, and generic instantiation refuse non-inhabiting producers** — fn f() -> Int returning a string refuses at compile; data x: Int = "x" refuses; a generic record instantiation whose interior does not inhabit the substituted parameter refuses. The judgment is actual-inhabits-declared at the three unchecked positions gunbc#7481 probed, reusing the argument-position machinery that already refuses correctly. Why: The keystone gap: the recovered thesis's distinctness requirement — without it every declared type is part contract, part documentation, and WalkPlan-class defects surface at first field access. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **ExhaustivenessUnknown refuses: the PatternLookupBlocked silent arm closes** — lookup_variant_in_type and lookup_field_in_variant answer PatternLookupBlocked with diagnostics: [] — exhaustiveness-not-established passing as success-adjacent. Split the verdict three ways (Exhaustive | NonExhaustive | ExhaustivenessUnknown) and refuse Unknown into any accepted executable artifact. Why: Closes the fallback-arm shape the census names for blocked lookups — exhaustiveness-not-established passing as success-adjacent; the measured disposition lives in the carrier, not this ticket. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **Field access through an unconstrained generic refuses: delete the field_of_type_var fabrication** — field_of_type_var answers a field projection on a bare type variable by fabricating the field type — compile-time plausible output where no fact exists (the census's field-through-generic class). The wall: field projection on a type parameter with no field-carrying bound refuses with the parameter, field name, and locus; concrete receivers and parameters whose bound carries the field stay green. Why: A fabricated field type flows into inference and surfaces as a downstream mismatch or a wrong runtime value far from its cause; a located refusal moves the diagnosis to the projection site. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **Parse formation refuses: a dropped separator cannot silently change the program** — Two slices, one wall. Safety: a missing list separator refuses at parse instead of parsing as element juxtaposition (the measured below-floor census row — a dropped comma silently changes list arity). Completeness: a block after a bare identifier in if-condition position parses as the block it is, closing the measured false refusal (expected-RParen on legitimate input). Probe-paired via the corpus's parser pair; grammar-owned, no host parser (census queue items 6-7). Why: The separator specimen survived regen, whole-corpus compile, fixed-point verify, and a 15-case matrix; only a human diff read caught it (review 45347). [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **Record construction refuses: missing required fields, bad defaults, ungrounded nominal literals** — A record literal missing a required field refuses at compile with the field named; defaults apply only where declared; a literal claiming a nominal type it does not ground refuses. Owns the census's record-completeness class — floor-inhabitance-wall owns return, data, and generic instantiation, not this. Probe-paired via the corpus's record-completeness pair. Motivating specimen: HandAuthoredDocBind's primary_work construction wall currently rests on record-completeness enforcement that is itself unmeasured. Why: A construction wall anywhere in the corpus is only as strong as required-field enforcement; this node is what makes primary_work-style walls load-bearing rather than declared. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) -- [ ] **Write the closing check for: One primitive identity: definition, per-target realization, and cost as three facts on one key** — One executable check states when this is done, transcribed from the node's own bar: A costed-but-unrealized primitive refuses target-relatively; a realized-but-undefined name cannot be keyed; existing corpus calls all still resolve. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - - [ ] **One primitive identity: definition, per-target realization, and cost as three facts on one key** — PrimitiveDefinition (invocation forms, receiver domain, parameters, result), PrimitiveRealization per target, PrimitiveCostModel — joined by one identity so a call cannot compile for target T without exactly one compatible definition and realization, and interpreter/emitter dispatch derive from realization rows instead of five independently-writable name sets (registry map, PrimitiveContract rows, algebra templates, interpreter arms, per-target templates). Why: Closes the gunbc#7479 CLASS rather than the specimen: filter_map had a cost contract, no seed method, and a green compile. Unlocks the method ambiguity wall (sameness becomes decidable) and un-narrows the method-existence wall — the measured length/count/size dispatch fork on gunbc#7484 is why per-receiver checking stalls at kernel receivers today. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) +- [ ] **Write the closing check for: Method existence on established receiver surfaces — LANDED (gunbc#7484, merged 2026-08-01)** — One executable check states when this is done, transcribed from the node's own bar: method_existence_wall_witness (REDs + zero-diagnostic controls + frontier-key perturbation) — migrates to the probe corpus as this class's durable pair. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Method existence on established receiver surfaces — LANDED (gunbc#7484, merged 2026-08-01)** — ACCEPTED fragment, recut from floor-method-existence-wall at its actual grain (operator spine verdict). An unresolved method on a kernel-profiled receiver refuses (MethodNotFound, blocking — the gunbc#7479 class, 0 files emitted); undecided receivers carry the rostered, occurrence-budgeted MethodExistenceUndecided/FrontierAdmitted frontier; map_keys/map_values bypasses routed through the one decision; one where-refinement receiver class peeled. What did NOT land stays on the sibling nodes: receiver-resolution residue, the general zero-resolution wall, resolve_builtin_call_type's unit_type fabrication. Why: The compile gate stopped being quieter than the interpreter for every receiver with a complete declared surface. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Argument labels at the method/pipe seam: the call-shape judgment reaches method dispatch** — The direct-call label/surplus wall (landed, gunbc#7519) covers the sig-resolved ExprCall branch only; method-pipe argument binding routes through its own inference arms. Mirror the same two runtime-refused classes there, reusing the landed judgment rather than minting a second relation. Why: A mislabeled method argument silently binding positionally is the same interest-bearing harm as the direct-call case. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) +- [ ] **Write the closing check for: Unknown labels and surplus positionals refuse at the direct-call seam — LANDED (gunbc#7519, merged 2026-07-31)** — One executable check states when this is done, transcribed from the node's own bar: ct_call_shape_wall_witness_test — migrates to the probe corpus as this class's durable pair. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Unknown labels and surplus positionals refuse at the direct-call seam — LANDED (gunbc#7519, merged 2026-07-31)** — ACCEPTED fragment, recut from floor-call-shape-wall at its actual grain (operator spine verdict). CallArgumentNameUnknown + CallPositionalSurplus blocking, exemption-free, mirroring call_function_inner's two refused classes (underscore idiom included); census refused 28 live parameter-rename fossils, all fixed; the silent emit-path reorder retired for Accepted programs. What did NOT land stays on the sibling nodes: missing/duplicate, signature-resolution coverage, the method seam. Why: The mislabeled call that computed the wrong thing silently on the emission path is unwritable in an Accepted program at this seam. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) +- [ ] **Write the closing check for: Declared conformance for the ground fragment — LANDED (gunbc#7484, merged 2026-08-01)** — One executable check states when this is done, transcribed from the node's own bar: declared_type_conformance_witness — migrates to the probe corpus as this class's durable pair. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Declared conformance for the ground fragment — LANDED (gunbc#7484, merged 2026-08-01)** — ACCEPTED fragment, recut from floor-inhabitance-wall at its actual grain (operator spine verdict). fn returns (both arms, plus early returns stopping at lambda boundaries) and data annotations refuse non-conforming producers where BOTH sides are ground: kernel scalars, or Required non-keyed element collections of them — positive establishment, the four measured representation classes never guessed at. The counted unjudged advisory was EXCLUDED in final review scope: everything wider is byte-for-byte main behavior (silent), stated here so the fragment is never read as the class. Why: fn f() -> Int returning a string — the probe-3 specimen — refuses located instead of surfacing at the first consumer. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) +- [ ] **Write the closing check for: v2 self-grounding slice — LANDED (gunbc#7485, merged; acceptance receipt recorded 2026-08-01)** — One executable check states when this is done, transcribed from the node's own bar: The claims carrier's v2 self-grounding path flips BelowFloor-to-FrontierAccepted exactly here; the identity path refuses caller-supplied candidate sets. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **v2 self-grounding slice — LANDED (gunbc#7485, merged; acceptance receipt recorded 2026-08-01)** — ACCEPTED against its handback (operator spine verdict): exact self-evidence left solve_constraints' generic arm; underived facts are typed, located, counted GroundingNotDerived diagnostics inside an accepted InferredTree — the FrontierAccepted disposition's live specimen. Identity admission reads the authored declared-inhabitant roster, never caller-supplied candidates. The documented residue is the NEXT node's whole scope: Translate can still swallow the refusal and proceed. Why: Silent self-certification became a counted frontier: the deficit ranks for fixing instead of reading as green. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Translate propagates the refusal: an underived fact cannot be swallowed downstream** — ACTIVE — the containment workstream, authorized to run parallel with the spine (operator spine verdict 2026-08-01: not another optional wall; it closes the one documented fail-open the merged frontier split created). gunbc#7485 records that Translate can swallow the new GroundingNotDerived rejection and continue — the frontier diagnostic exists but the emission path proceeds. Make Translate refuse or thread the typed frontier so Eval and Translate agree: compile-time refusal on both is the class minimum. The per-path grain is exactly why the carrier models paths separately. Why: Closes the path split where the same underived fact refuses in one phase and ships in another — the subject-grain failure mode measured on the call-shape class. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **InferredTree carries only derived facts: the all-derived carrier lands** — Separate source and inferred carriers so candidate-equals-source is not even well-typed as a grounding proof: every fact on the inferred carrier traces to a derivation or the node refuses. Branch/Match/Loop (the three real inference paths) migrate first; the validate_then_compile door and its lens roster stay. A well-formed node with no derived facts cannot inhabit the carrier. Why: InferredTree becomes a trustworthy proof boundary; every lens consuming it stops reading facts that were never derived; the self-host frontier stops reading greener than it is. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Every node kind derives or refuses: the generic acceptance arm deletes** — The remaining derivation rules beyond Branch/Match/Loop: each v2 node kind either derives into the inferred carrier or its acceptance is a typed compile refusal — the generic accept-anything arm deletes. Coverage is counted per kind, so the frontier shrinks to zero or to named FrontierAccepted rows each carrying a trigger. Why: The last self-certification residue: any kind without a rule is currently accepted on well-formedness alone, which is the recovered defect class in its most general form. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Realization per target: an operation cannot compile for T without exactly one realization** — The realized carrier adds target realization: every executable operation in a program accepted for target T has exactly one compatible realization row for T, refused target-relatively otherwise. gunbc#7485 explicitly leaves this open; it consumes the primitive-identity join (realization rows are its per-target facts) and the derivation-coverage node (a realized tree is derived-then-realized, never source-then-realized). Why: The gunbc#7479 shape at its root: a costed-but-unrealized primitive currently compiles and fails at dispatch; the gate moves that to a located target-relative refusal. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: v2-node-kind-derivation-coverage, primitive-identity-join +- [ ] **Write the closing check for: Interpreter primitive roster: the fifth surface becomes readable from .dag** — One executable check states when this is done, transcribed from the node's own bar: A new dispatch arm without a roster row refuses; a roster row without a matching arm refuses; fold_list via eval_call intercept and lookup dual-realization join through derived census. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Interpreter primitive roster: the fifth surface becomes readable from .dag** — Land interpreter-primitive-roster across all six v1_interpreter dispatch sites (eval_builtin_inner, eval_algebra_method_inner, eval_call, try_v2_std_collection_map_primitive_grounding, try_parse_table_memo_dispatch, eval_method_call lookup short-circuit — 158 arms / 169 spellings). clever-crab-111 macro authority: eval_builtin_inner arms expand from interpreter_free_call_arm_spellings with construction wall (arm without roster entry unwritable). primitive_surface_census_derived absorbs InterpreterDispatch instead of primitive_interpreter_dispatch_measurement_rows. Prerequisite for primitive-identity-join terminal acceptance. Why: Closes the fifth blind spot in D1 mutation control across all six dispatch sites. Dissolves MeasurementOnly specimen rows into derived surfaces. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **One primitive identity: definition, per-target realization, and cost as three facts on one key** — PrimitiveDefinition (identity + PrimitiveSemanticContract), PrimitiveInvocationSurface, PrimitiveRealization per RealizationTarget, PrimitiveCostFact, and PrimitiveTraversalOrderFact — five separate carriers joined by PrimitiveIdentity, never one mega-row. TERMINAL acceptance: every primitive-bearing surface across all five authorities (registry, PrimitiveContract rows, algebra templates, interpreter dispatch, emit handlers) joins exactly one identity; population-wide join and realization refusals are the closing contract. D0 is a first slice only — not the closing contract. Namespace supplies declaration identity; this node supplies semantic primitive identity across invocation and realization. Why: Proves the determinism denominator is open (#7532 honest rung) and begins closing it: four surfaces feed primitive_surface_census_derived at runtime; mutation control is CLOSED on AlgebraTemplate and EmitHandler, PARTIAL on BuiltinRegistry and PrimitiveContractRow (parallel hand rosters — primitive_d0_derived_surface_drift_audit_note), NOT DISCOVERED on InterpreterDispatch. fold_list spelling fork and index_by registry+algebra dual absence are named specimens proving the denominator is not closed. Unlocks method ambiguity wall and D1 determinism consumption through PrimitiveIdentity rather than raw symbol. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) + - [ ] **Method ambiguity refuses: more than one resolution is a located error once primitive identity defines sameness** — The method-existence wall closes cardinality zero; this closes more-than-one — two distinct primitive identities (or a primitive and a user fn) resolving for one receiver/method pair refuse with both candidates named. Gated on primitive-identity-join because ambiguity is decidable only when sameness is: the length/count/size dispatch aliases make one method look like three today (the gunbc#7484 measured fork), so an ambiguity check before the join would refuse legitimate corpus or silently pick. Why: The silent-first-hit dispatch class: today arm order decides which of two candidates runs — an unstated tiebreak the census cannot even measure until sameness is defined. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) — requires all: primitive-identity-join, method-zero-resolution-general-wall + - [ ] **Determinism denominator closure: every primitive carries one traversal-order classification** — v2.std.determinism and v2.lens.determinism consume std.primitive_identity: every PrimitiveDefinition carries exactly one PrimitiveTraversalOrderFact, unclassified refuses, HostUnspecifiedOrder derives a determinism root, aliases resolve through PrimitiveIdentity not raw symbol. Adding a new invokable primitive realization without a traversal-order fact reds before any caller uses it. Why: Dissolves the interim two-root registry (#7532) into a closed denominator — the gate can tell an alias from a distinct operation, and keys/values false-positive class is eliminated by identity not spelling. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - [ ] **Write the closing check for: The capability audit: sole_constructor completeness, then seal the proof-carriers** — One executable check states when this is done, transcribed from the node's own bar: Refined authored outside the sealing module refuses at compile in every probed form; refine remains the only inhabitation path; a probe form that does NOT refuse is a counted audit finding, never silently absorbed. Why: Turns a prose bar into a runnable verdict, so the node it gates can be dispatched and its completion checked rather than judged. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) - [ ] **The capability audit: sole_constructor completeness, then seal the proof-carriers** — The language ALREADY has a construction wall: sole_constructor rejects cross-module construction with SoleConstructorViolation and carries a sealed-record specimen (post-merge review of gunbc#7486 refuted this node's earlier premise that no private/sealed/opaque capability exists). What is UNVERIFIED is its completeness: generic carriers (does type Refined sole_constructor hold across instantiation?), every construction form (literals, casts like as TransportScript, coproduct variants, default values), and behavior under module_skips_direct_call_arg_check. Audit by falsification first; extend sole_constructor or ground the reference-grant algebra (the parked visibility-grants design) only where the audit reds. Why: Every capability-kind ceiling resolves together — either sole_constructor suffices (cheap: seal Refined, delete the stub-pack forge arm) or its named gaps price the visibility-grants work honestly instead of by assumption. [authority](docs/plans/compiler-guarantee-recovery-gap-analysis.md) diff --git a/dag/extdeps/advertising/google_ads.dag b/dag/extdeps/advertising/google_ads.dag new file mode 100644 index 00000000000..81f77e360ea --- /dev/null +++ b/dag/extdeps/advertising/google_ads.dag @@ -0,0 +1,109 @@ +module extdeps.advertising.google_ads + +import std.types { List, NonEmptyStr } + +import extdeps.advertising.types { AdPriceDetermination, AuctionDetermined } +import extdeps.securities { Securities } +import extdeps.vendor { Vendor } +import extdeps.vendor.alphabet { alphabet } + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "support.google.com/google-ads/answer/2472725" + } +} + +data google_ads_subject: Vendor = alphabet + +data google_ads_subject_note: String = "Subject and source are orthogonal axes (operator ruling 2026-07-25, extdeps.pricing.gitlab_subscription precedent): the anchor above is the SOURCE (Google's own product help pages under support.google.com/google-ads), and the subject edge names the ENTITY -- the SEC issuer Alphabet Inc., the filing parent whose income statement reports Google advertising revenue. The consumer-facing product brand is Google Ads; no separate legal-entity row is minted for the operating subsidiary because no fact in this module is about corporate structure." + +data google_ads_provenance_note: String = "Interface facts live-fetched 2026-08-01 from two Google Ads Help pages: 'Determine a bid strategy based on your goals - Google Ads Help' (answer/2472725, the anchor) and 'Auction - Google Ads Help' (answer/142918). Neither page publishes any numeric price, minimum bid, or rate floor -- the pricing interface Google documents is a per-auction mechanism plus an advertiser-set budget cap, which is why the rate row below is AuctionDetermined rather than a money amount." + +type GoogleAdsBillableUnit + = GoogleCostPerClick + | GoogleCostPerThousandImpressions + | GoogleCostPerThousandViewableImpressions + | GoogleCostPerView + +type GoogleAdsBillableUnitRow { + unit: GoogleAdsBillableUnit + unit_wire_label: NonEmptyStr + citation: NonEmptyStr +} + +data google_ads_billable_unit_note: String = "The four cost units the bid-strategy page names, wire labels verbatim: cost-per-click (CPC) for clicks, cost-per-thousand impressions (CPM) and cost-per-thousand viewable impressions (vCPM) for impressions, and cost-per-view (CPV) for video views. Conversions are an OPTIMIZATION target in Google's model (Target CPA, Target ROAS, Maximize Conversions), not a fifth billable unit on this page -- the strategy roster below carries those names; fusing the two axes would misstate the documented interface." + +data google_cpc_unit: GoogleAdsBillableUnitRow = GoogleAdsBillableUnitRow { + unit: GoogleCostPerClick, + unit_wire_label: "CPC", + citation: "support.google.com/google-ads/answer/2472725", +} + +data google_cpm_unit: GoogleAdsBillableUnitRow = GoogleAdsBillableUnitRow { + unit: GoogleCostPerThousandImpressions, + unit_wire_label: "CPM", + citation: "support.google.com/google-ads/answer/2472725", +} + +data google_vcpm_unit: GoogleAdsBillableUnitRow = GoogleAdsBillableUnitRow { + unit: GoogleCostPerThousandViewableImpressions, + unit_wire_label: "vCPM", + citation: "support.google.com/google-ads/answer/2472725", +} + +data google_cpv_unit: GoogleAdsBillableUnitRow = GoogleAdsBillableUnitRow { + unit: GoogleCostPerView, + unit_wire_label: "CPV", + citation: "support.google.com/google-ads/answer/2472725", +} + +fn google_ads_billable_unit_catalog() -> List { + [ + google_cpc_unit, + google_cpm_unit, + google_vcpm_unit, + google_cpv_unit, + ] +} + +data google_ads_bid_strategy_roster_note: String = "Bid-strategy names transcribed verbatim from the anchor page 2026-08-01. Kept as a wire-label roster rather than a closed enum: this roster is product-versioned help prose, not a machine-facing API enum (the machine-facing contrast is extdeps.advertising.meta_ads, where billing_event and bid_strategy ARE published API enums). Strategies select what the auction optimizes; the billable unit stays one of the four rows above." + +data google_ads_bid_strategy_wire_labels: List = [ + "Target cost per action (CPA)", + "Target return on ad spend (ROAS)", + "Maximize Conversions", + "Maximize Conversion Value", + "Enhanced cost per click (ECPC)", + "Maximize Clicks", + "Target CPC", + "Manual CPC bidding", + "Target Impression Share", + "CPM", + "tCPM", + "vCPM", + "CPV bidding", +] + +type GoogleAdsBudgetControl + = | GoogleAverageDailyBudget + +data google_ads_budget_note: String = "The advertiser-side spend cap the page documents, quoted: 'All you have to do is set an average daily budget, and the Google Ads system automatically manages your bids to bring you the most clicks possible within your budget.' The budget is the advertiser's declared cap; the per-event price under it remains auction-determined." + +type GoogleAdsAuctionTimeSignal + = GoogleSignalDevice + | GoogleSignalLocation + | GoogleSignalTimeOfDay + | GoogleSignalLanguage + | GoogleSignalOperatingSystem + +data google_ads_auction_time_signal_note: String = "Coarse-grain targeting dimensions exactly as the anchor page enumerates them, quoted: Smart Bidding 'factors in a wide range of auction-time signals such as device, location, time of day, language, and operating system to capture the unique context of every search.' The page says 'such as' -- this enum models the five signals the vendor names, and is deliberately NOT claimed closed over everything the auction reads." + +data google_ads_media_rate: AdPriceDetermination = AuctionDetermined { + mechanism_citation: "support.google.com/google-ads/answer/142918", +} + +data google_ads_auction_mechanism_note: String = "The cited mechanism page ('Auction - Google Ads Help') describes selection and pricing by Ad Rank, quoted: eligibility considers 'your bid, ad quality, the Ad Rank thresholds, the context of the person's search, and the expected impact of extensions and other ad formats', and 'even if your competition bids higher than you, you can still win a higher position -- at a lower price -- with highly relevant keywords and ads.' The page contains no numeric amounts, which is the fact the AuctionDetermined arm carries." diff --git a/dag/extdeps/advertising/meta_ads.dag b/dag/extdeps/advertising/meta_ads.dag new file mode 100644 index 00000000000..24e5dd0bf7d --- /dev/null +++ b/dag/extdeps/advertising/meta_ads.dag @@ -0,0 +1,216 @@ +module extdeps.advertising.meta_ads + +import std.types { List, NonEmptyStr } + +import extdeps.advertising.types { AdPriceDetermination, AuctionDetermined, RateUnavailable } +import extdeps.securities { Securities } +import extdeps.vendor { Vendor } +import extdeps.vendor.meta_platforms { meta_platforms } + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "developers.facebook.com/docs/marketing-api/reference/ad-campaign" + } +} + +data meta_ads_subject: Vendor = meta_platforms + +data meta_ads_provenance_note: String = "Interface facts live-fetched 2026-08-01 from the Meta Marketing API developer docs: the anchor page (which, per the API's legacy object naming, is the AD SET reference -- billing_event, bid_strategy, and optimization_goal are ad-set-level fields) and the bidding overview at developers.facebook.com/docs/marketing-api/bidding, which states the billing-event concept verbatim: the system 'defines events you want to pay for, including impressions, clicks, or various actions.' Two Business Help Center pages were attempted and are client-rendered (this fetch reads only their titles): 'About Ad Auctions | Meta Business Help Center' (facebook.com/business/help/430291176997542) and 'Best Practices for Minimum Budgets | Meta Business Help Center' (facebook.com/business/help/203183363050448) -- the latter is why the minimum-budget row below is RateUnavailable with that cause, never a remembered number." + +type MetaBillingEvent + = MetaBillAppInstalls + | MetaBillClicks + | MetaBillImpressions + | MetaBillLinkClicks + | MetaBillNone + | MetaBillOfferClaims + | MetaBillPageLikes + | MetaBillPostEngagement + | MetaBillThruplay + | MetaBillPurchase + | MetaBillListingInteraction + +type MetaBillingEventRow { + event: MetaBillingEvent + wire_value: NonEmptyStr + citation: NonEmptyStr +} + +data meta_billing_event_note: String = "billing_event is the published API enum naming what the advertiser is CHARGED for; wire values verbatim from the anchor reference. It is a separate axis from optimization_goal (optimize delivery for one event while being billed on another) -- fusing the two would misstate the documented interface, so the optimization-goal roster lands as its own row below." + +data meta_bill_app_installs: MetaBillingEventRow = MetaBillingEventRow { + event: MetaBillAppInstalls, + wire_value: "APP_INSTALLS", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_bill_clicks: MetaBillingEventRow = MetaBillingEventRow { + event: MetaBillClicks, + wire_value: "CLICKS", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_bill_impressions: MetaBillingEventRow = MetaBillingEventRow { + event: MetaBillImpressions, + wire_value: "IMPRESSIONS", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_bill_link_clicks: MetaBillingEventRow = MetaBillingEventRow { + event: MetaBillLinkClicks, + wire_value: "LINK_CLICKS", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_bill_none: MetaBillingEventRow = MetaBillingEventRow { + event: MetaBillNone, + wire_value: "NONE", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_bill_offer_claims: MetaBillingEventRow = MetaBillingEventRow { + event: MetaBillOfferClaims, + wire_value: "OFFER_CLAIMS", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_bill_page_likes: MetaBillingEventRow = MetaBillingEventRow { + event: MetaBillPageLikes, + wire_value: "PAGE_LIKES", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_bill_post_engagement: MetaBillingEventRow = MetaBillingEventRow { + event: MetaBillPostEngagement, + wire_value: "POST_ENGAGEMENT", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_bill_thruplay: MetaBillingEventRow = MetaBillingEventRow { + event: MetaBillThruplay, + wire_value: "THRUPLAY", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_bill_purchase: MetaBillingEventRow = MetaBillingEventRow { + event: MetaBillPurchase, + wire_value: "PURCHASE", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_bill_listing_interaction: MetaBillingEventRow = MetaBillingEventRow { + event: MetaBillListingInteraction, + wire_value: "LISTING_INTERACTION", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +fn meta_billing_event_catalog() -> List { + [ + meta_bill_app_installs, + meta_bill_clicks, + meta_bill_impressions, + meta_bill_link_clicks, + meta_bill_none, + meta_bill_offer_claims, + meta_bill_page_likes, + meta_bill_post_engagement, + meta_bill_thruplay, + meta_bill_purchase, + meta_bill_listing_interaction, + ] +} + +type MetaBidStrategy + = MetaLowestCostWithoutCap + | MetaLowestCostWithBidCap + | MetaCostCap + | MetaLowestCostWithMinRoas + +type MetaBidStrategyRow { + strategy: MetaBidStrategy + wire_value: NonEmptyStr + citation: NonEmptyStr +} + +data meta_strategy_lowest_cost_without_cap: MetaBidStrategyRow = MetaBidStrategyRow { + strategy: MetaLowestCostWithoutCap, + wire_value: "LOWEST_COST_WITHOUT_CAP", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_strategy_lowest_cost_with_bid_cap: MetaBidStrategyRow = MetaBidStrategyRow { + strategy: MetaLowestCostWithBidCap, + wire_value: "LOWEST_COST_WITH_BID_CAP", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_strategy_cost_cap: MetaBidStrategyRow = MetaBidStrategyRow { + strategy: MetaCostCap, + wire_value: "COST_CAP", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +data meta_strategy_lowest_cost_with_min_roas: MetaBidStrategyRow = MetaBidStrategyRow { + strategy: MetaLowestCostWithMinRoas, + wire_value: "LOWEST_COST_WITH_MIN_ROAS", + citation: "developers.facebook.com/docs/marketing-api/reference/ad-campaign", +} + +fn meta_bid_strategy_catalog() -> List { + [ + meta_strategy_lowest_cost_without_cap, + meta_strategy_lowest_cost_with_bid_cap, + meta_strategy_cost_cap, + meta_strategy_lowest_cost_with_min_roas, + ] +} + +data meta_optimization_goal_note: String = "optimization_goal wire values transcribed verbatim from the anchor reference 2026-08-01. Kept as a wire-value roster rather than a closed enum: this is the coarse-grain record that the axis EXISTS and is distinct from billing_event; per-goal modeling waits for a consumer that needs it (second-consumer discipline), and a 33-variant enum nobody eliminates over would be roster theater." + +data meta_optimization_goal_wire_values: List = [ + "NONE", + "APP_INSTALLS", + "AD_RECALL_LIFT", + "ENGAGED_USERS", + "EVENT_RESPONSES", + "IMPRESSIONS", + "LEAD_GENERATION", + "QUALITY_LEAD", + "LINK_CLICKS", + "OFFSITE_CONVERSIONS", + "PAGE_LIKES", + "POST_ENGAGEMENT", + "QUALITY_CALL", + "REACH", + "LANDING_PAGE_VIEWS", + "VISIT_INSTAGRAM_PROFILE", + "ENGAGED_PAGE_VIEWS", + "VALUE", + "THRUPLAY", + "DERIVED_EVENTS", + "APP_INSTALLS_AND_OFFSITE_CONVERSIONS", + "CONVERSATIONS", + "IN_APP_VALUE", + "MESSAGING_PURCHASE_CONVERSION", + "MESSAGING_DEEP_CONVERSATION_AND_FOLLOW", + "SUBSCRIBERS", + "REMINDERS_SET", + "MEANINGFUL_CALL_ATTEMPT", + "PROFILE_VISIT", + "PROFILE_AND_PAGE_ENGAGEMENT", + "ADVERTISER_SILOED_VALUE", + "AUTOMATIC_OBJECTIVE", + "MESSAGING_APPOINTMENT_CONVERSION", +] + +data meta_ads_media_rate: AdPriceDetermination = AuctionDetermined { + mechanism_citation: "developers.facebook.com/docs/marketing-api/bidding", +} + +data meta_ads_minimum_daily_budget_rate: AdPriceDetermination = RateUnavailable { + cause: "Meta publishes minimum daily budget figures in the Business Help Center page 'Best Practices for Minimum Budgets' (facebook.com/business/help/203183363050448), but that page is client-rendered and this fetch reads only its title (2026-08-01) -- refused, never filled from memory", +} diff --git a/dag/extdeps/advertising/types.dag b/dag/extdeps/advertising/types.dag new file mode 100644 index 00000000000..e2f116ac0cc --- /dev/null +++ b/dag/extdeps/advertising/types.dag @@ -0,0 +1,19 @@ +module extdeps.advertising.types + +import std.types { NonEmptyStr } + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "en.wikipedia.org/wiki/Online_advertising" + } +} + +data ad_price_determination_note: String = "Auction ad platforms publish the pricing MECHANISM, not a rate card: the amount charged per billable event is determined per-auction and no citable numeric rate exists upstream. AuctionDetermined is therefore the true upstream fact (an answer, with the mechanism page as its citation), while RateUnavailable marks a figure the vendor DOES publish but this fetch could not read (ignorance) -- the DESIGN section 5 distinction between top-as-answer and top-as-ignorance, kept as two typed arms so neither can masquerade as the other. A listed-rate arm is deliberately absent: neither modeled vendor publishes a citable numeric rate today, and that arm is minted when the first citable listed figure appears, never before -- a fabricated plausible rate from training data is exactly the wrong answer, and a vendor rate is precisely the kind of fact that goes stale." + +type AdPriceDetermination + = AuctionDetermined { mechanism_citation: NonEmptyStr } + | RateUnavailable { cause: NonEmptyStr } diff --git a/dag/extdeps/cache/sccache.dag b/dag/extdeps/cache/sccache.dag index 9e34e5a8e62..331e11d2c99 100644 --- a/dag/extdeps/cache/sccache.dag +++ b/dag/extdeps/cache/sccache.dag @@ -22,6 +22,7 @@ import std.cache_interface { LocalDiskUs, ReadYourWrites, VendorCitation, + CacheEvidence, RunnerObserved, ManualPurge, ToolchainChange, @@ -198,6 +199,76 @@ fn sccache_release_digest(rel: SccacheBinaryRelease, arch: Architecture) -> Dige } } +data sccache_server_process_model_doc: String = "sccache is a client/server tool: the client auto-spawns a server on first use if none is listening, the server survives the client that spawned it, and CACHE MISSES ARE COMPILED BY THE SERVER — rustc runs as a CHILD OF THE SERVER PROCESS, not of the invoking client. Consequence (load-bearing for host budgeting): the server's cgroup IS the cgroup every cache-miss compile on the host executes in, so a server that was lazily spawned inside some job's cgroup makes that job's memory caps bound EVERY subsequent compile on the host, including compiles issued by other jobs. SCCACHE_IDLE_TIMEOUT=0 disables the idle shutdown, so such a mis-placed server survives indefinitely." + +data sccache_server_endpoint_doc: String = "The SERVER is identified positively as the process HOLDING THE LISTEN on the configured endpoint — never by process name. The sccache binary is both client and server, so under RUSTC_WRAPPER every in-flight compile also runs a process named 'sccache'; selecting by name samples a transient client and reads the WRONG cgroup. Upstream exposes two mutually exclusive endpoint realizations: SCCACHE_SERVER_PORT (loopback TCP, default 4226) and SCCACHE_SERVER_UDS (a Unix-domain socket path). Modeling only the port would be a nickname for one realization (DESIGN section 3): the gunbc fleet configures the UDS form, so a port-only probe finds no listener at all. Observed live on srv1 2026-08-01: no TCP 4226 listener exists, while /var/lib/ctrl/sccache-ci/server.sock is held LISTEN by the CI server and /var/lib/ctrl/sccache/server.sock by the session server — endpoint ownership is what distinguished them." + +data sccache_server_default_tcp_port: Int = 4226 + +type SccacheServerEndpoint + = SccacheServerTcpPort { port: Int } + | SccacheServerUnixSocket { path: NonEmptyStr } + +type SccacheServerSpawn + = ClientAutoSpawned + | UnitStarted + +data sccache_stats_query_does_not_spawn_note: String = "MEASURED CORRECTION, 2026-08-01: `--show-stats` does NOT auto-start a server, and the widely-repeated claim that 'any sccache invocation auto-starts a server, so --show-stats manufactures the evidence it reports' is FALSE for sccache 0.15.0. Two controls on srv2, each with a PRIVATE cache dir AND a private endpoint so the falsifier could not touch production: with SCCACHE_SERVER_UDS the socket did not exist before or after the call; with a private SCCACHE_SERVER_PORT the listener count was 0 before and 0 after. In both the call returned rc=0 and printed a fully-formed table of ZEROES. A third control isolated what DOES spawn: one real compile (`sccache /usr/bin/gcc -c t.c`) through the same private port took the listener count 0 -> 1, and the new server inherited the INVOKING process's cgroup (observed: the caller's own session scope). THE DEFECT IS REAL BUT DIFFERENTLY SHAPED, and the difference changes the remedy's justification rather than the remedy: --show-stats answers plausibly WITHOUT a server existing at all, so a read-back that converges on it greens over a host with no durable daemon — a fabricated plausible output (DESIGN section 5), which is exactly what srv3 exhibited on 2026-07-25 (a full stats table while the unit had been inactive six hours). The observation there was right; the inference drawn from it — that the call had created a server — was wrong. Consequence for the observer: stats is supplemental data gathered AFTER ownership is independently established, never the thing that establishes it; and the auto-spawn wall belongs on the COMPILE path, which is where a client actually creates a server in its own cgroup." + +type SccacheServerProcessFacts { + spawn: SccacheServerSpawn + idle_timeout_zero_disables_shutdown: Bool + compiles_execute_as_server_children: Bool + stats_query_spawns_server: Bool + default_endpoint: SccacheServerEndpoint + evidence: List +} + +data sccache_server_process_facts: SccacheServerProcessFacts = { + spawn: ClientAutoSpawned + idle_timeout_zero_disables_shutdown: true + compiles_execute_as_server_children: true + stats_query_spawns_server: false + default_endpoint: SccacheServerTcpPort { port: sccache_server_default_tcp_port } + evidence: [ + { + field: "spawn" + evidence: RunnerObserved { + receipt: "Private-endpoint control on srv2 (2026-08-01, sccache 0.15.0): with a private SCCACHE_DIR and a private SCCACHE_SERVER_PORT=48223, listeners on that port went 0 -> 1 across a single real compile `sccache /usr/bin/gcc -c t.c -o t.o`, and resolving the new listener's inode through /proc/*/fd showed the server had inherited the INVOKING shell's cgroup (/user.slice/user-1000.slice/session-9219.scope) rather than any managed unit. This is the birth mechanism behind runner-owned servers, reproduced deliberately instead of inferred from the runner hosts." + observed_at: "2026-08-01" + } + }, + { + field: "stats_query_spawns_server" + evidence: RunnerObserved { + receipt: "Two private-endpoint controls on srv2 (2026-08-01, sccache 0.15.0). UDS form: SCCACHE_SERVER_UDS pointed at a fresh temp path; the socket did not exist before the call and did not exist after it; /proc/net/unix LISTEN entries for that path were 0 before and 0 after. TCP form: private SCCACHE_SERVER_PORT=48221; /proc/net/tcp listeners on that port were 0 before and 0 after. Both invocations returned rc=0 and printed a complete stats table reading all zeroes. So `--show-stats` reports plausibly with NO server in existence — it does not manufacture one. Refutes the prior in-tree claim that any sccache invocation auto-starts a server." + observed_at: "2026-08-01" + } + }, + { + field: "idle_timeout_zero_disables_shutdown" + evidence: VendorCitation { + source: "SCCACHE_IDLE_TIMEOUT: seconds the server waits before shutting down; 0 disables shutdown entirely" + url: "https://github.com/mozilla/sccache/blob/v0.15.0/docs/Configuration.md" + } + }, + { + field: "compiles_execute_as_server_children" + evidence: VendorCitation { + source: "sccache server performs the compilation on a cache miss; the compiler subprocess is a child of the server" + url: "https://github.com/mozilla/sccache/blob/v0.15.0/docs/Architecture.md" + } + }, + { + field: "default_endpoint" + evidence: VendorCitation { + source: "SCCACHE_SERVER_PORT sets the loopback TCP port the server listens on (default 4226); SCCACHE_SERVER_UDS selects a Unix-domain socket endpoint instead" + url: "https://github.com/mozilla/sccache/blob/v0.15.0/docs/Configuration.md" + } + }, + ] +} + data sccache_fatal_error_log_fragment: String = "sccache: encountered fatal error" data sccache_rustc_wrapper_exit_254_log_fragment: String = "(exit status: 254)" diff --git a/dag/extdeps/crm/hubspot_lifecycle.dag b/dag/extdeps/crm/hubspot_lifecycle.dag new file mode 100644 index 00000000000..b0d4c54f989 --- /dev/null +++ b/dag/extdeps/crm/hubspot_lifecycle.dag @@ -0,0 +1,117 @@ +module extdeps.crm.hubspot_lifecycle + +import std.nat { Nat } +import std.types { List, NonEmptyStr } + +import extdeps.securities { Securities } +import extdeps.vendor { Vendor } +import extdeps.vendor.hubspot { hubspot } + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "knowledge.hubspot.com/records/use-lifecycle-stages" + } +} + +data hubspot_lifecycle_subject: Vendor = hubspot + +data hubspot_lifecycle_provenance_note: String = "Vocabulary facts live-fetched 2026-08-01 from the HubSpot Knowledge Base page 'Use contact and company lifecycle stages' (the anchor): the eight default lifecycle stages in the vendor's own order, display labels and definition sentences verbatim. The stages apply to both contact and company records upstream (the page title says so). The cited page does NOT publish the internal API values for these stages, so no internal-value field exists on the row -- a plausible internal value from training data would be fabrication, and the field is minted only when a readable API-facing citation lands. These are vocabulary facts about the upstream product; no sales-process opinion lives in this module." + +type HubspotLifecycleStage + = HubspotSubscriber + | HubspotLead + | HubspotMarketingQualifiedLead + | HubspotSalesQualifiedLead + | HubspotOpportunity + | HubspotCustomer + | HubspotEvangelist + | HubspotOther + +type HubspotLifecycleStageRow { + stage: HubspotLifecycleStage + display_label: NonEmptyStr + definition_quote: NonEmptyStr + sort_order: Nat + citation: NonEmptyStr +} + +data hubspot_stage_subscriber: HubspotLifecycleStageRow = HubspotLifecycleStageRow { + stage: HubspotSubscriber, + display_label: "Subscriber", + definition_quote: "a contact that has opted in to hear more from you by signing up for your blog or newsletter", + sort_order: 1, + citation: "knowledge.hubspot.com/records/use-lifecycle-stages", +} + +data hubspot_stage_lead: HubspotLifecycleStageRow = HubspotLifecycleStageRow { + stage: HubspotLead, + display_label: "Lead", + definition_quote: "a contact or company that has converted on your website or through some other interaction with your organization beyond a subscription sign up", + sort_order: 2, + citation: "knowledge.hubspot.com/records/use-lifecycle-stages", +} + +data hubspot_stage_marketing_qualified_lead: HubspotLifecycleStageRow = HubspotLifecycleStageRow { + stage: HubspotMarketingQualifiedLead, + display_label: "Marketing Qualified Lead", + definition_quote: "a contact or company that your marketing team has qualified as ready for the sales team", + sort_order: 3, + citation: "knowledge.hubspot.com/records/use-lifecycle-stages", +} + +data hubspot_stage_sales_qualified_lead: HubspotLifecycleStageRow = HubspotLifecycleStageRow { + stage: HubspotSalesQualifiedLead, + display_label: "Sales Qualified Lead", + definition_quote: "a contact or company that your sales team has qualified as a potential customer", + sort_order: 4, + citation: "knowledge.hubspot.com/records/use-lifecycle-stages", +} + +data hubspot_stage_opportunity: HubspotLifecycleStageRow = HubspotLifecycleStageRow { + stage: HubspotOpportunity, + display_label: "Opportunity", + definition_quote: "a contact or company that is associated with a deal", + sort_order: 5, + citation: "knowledge.hubspot.com/records/use-lifecycle-stages", +} + +data hubspot_stage_customer: HubspotLifecycleStageRow = HubspotLifecycleStageRow { + stage: HubspotCustomer, + display_label: "Customer", + definition_quote: "a contact or company with at least one closed deal", + sort_order: 6, + citation: "knowledge.hubspot.com/records/use-lifecycle-stages", +} + +data hubspot_stage_evangelist: HubspotLifecycleStageRow = HubspotLifecycleStageRow { + stage: HubspotEvangelist, + display_label: "Evangelist", + definition_quote: "a customer that has advocated for your organization", + sort_order: 7, + citation: "knowledge.hubspot.com/records/use-lifecycle-stages", +} + +data hubspot_stage_other: HubspotLifecycleStageRow = HubspotLifecycleStageRow { + stage: HubspotOther, + display_label: "Other", + definition_quote: "a contact or company that does not fit any of the above stages", + sort_order: 8, + citation: "knowledge.hubspot.com/records/use-lifecycle-stages", +} + +fn hubspot_lifecycle_stage_catalog() -> List { + [ + hubspot_stage_subscriber, + hubspot_stage_lead, + hubspot_stage_marketing_qualified_lead, + hubspot_stage_sales_qualified_lead, + hubspot_stage_opportunity, + hubspot_stage_customer, + hubspot_stage_evangelist, + hubspot_stage_other, + ] +} diff --git a/dag/extdeps/crm/salesforce_opportunity.dag b/dag/extdeps/crm/salesforce_opportunity.dag new file mode 100644 index 00000000000..3e05bc1bfd4 --- /dev/null +++ b/dag/extdeps/crm/salesforce_opportunity.dag @@ -0,0 +1,106 @@ +module extdeps.crm.salesforce_opportunity + +import std.measure { BasisPoint } +import std.nat { Nat } +import std.types { List, NonEmptyStr } + +import extdeps.securities { Securities } +import extdeps.vendor { Vendor } +import extdeps.vendor.salesforce { salesforce } + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_opportunitystage.htm" + } +} + +data salesforce_opportunity_subject: Vendor = salesforce + +data salesforce_opportunity_provenance_note: String = "Vocabulary facts live-fetched 2026-08-01 from the Salesforce Platform object reference (en-us, API version 252.0, read through the docs content endpoint get_document_content because the doc site shell is client-rendered): the Opportunity object page for StageName and the OpportunityStage object page (the anchor) for the stage row schema and the ForecastCategory value set. These are vocabulary facts about the upstream product's published surface -- no sales-process opinion lives in this module." + +data salesforce_stage_configurability_note: String = "The load-bearing upstream fact: StageName is a required picklist on Opportunity that the reference says controls ForecastCategoryName, IsClosed, IsWon, and Probability, and the reference directs, quoted: 'To obtain the stage name values in the picklist, query the OpportunityStage object.' The stage VOCABULARY is therefore org-configurable DATA in the vendor's own model, not a published universal roster -- what Salesforce publishes as vocabulary is the stage row SCHEMA and the closed ForecastCategory value set, both modeled here under their real names. OpportunityStage is read-only via the API upstream." + +type SalesforceForecastCategory + = SalesforceForecastBestCase + | SalesforceForecastClosed + | SalesforceForecastForecast + | SalesforceForecastMostLikely + | SalesforceForecastOmitted + | SalesforceForecastPipeline + +type SalesforceForecastCategoryRow { + category: SalesforceForecastCategory + wire_value: NonEmptyStr + citation: NonEmptyStr +} + +data salesforce_forecast_best_case: SalesforceForecastCategoryRow = SalesforceForecastCategoryRow { + category: SalesforceForecastBestCase, + wire_value: "BestCase", + citation: "sforce_api_objects_opportunitystage#ForecastCategory", +} + +data salesforce_forecast_closed: SalesforceForecastCategoryRow = SalesforceForecastCategoryRow { + category: SalesforceForecastClosed, + wire_value: "Closed", + citation: "sforce_api_objects_opportunitystage#ForecastCategory", +} + +data salesforce_forecast_forecast: SalesforceForecastCategoryRow = SalesforceForecastCategoryRow { + category: SalesforceForecastForecast, + wire_value: "Forecast", + citation: "sforce_api_objects_opportunitystage#ForecastCategory", +} + +data salesforce_forecast_most_likely: SalesforceForecastCategoryRow = SalesforceForecastCategoryRow { + category: SalesforceForecastMostLikely, + wire_value: "MostLikely", + citation: "sforce_api_objects_opportunitystage#ForecastCategory", +} + +data salesforce_forecast_omitted: SalesforceForecastCategoryRow = SalesforceForecastCategoryRow { + category: SalesforceForecastOmitted, + wire_value: "Omitted", + citation: "sforce_api_objects_opportunitystage#ForecastCategory", +} + +data salesforce_forecast_pipeline: SalesforceForecastCategoryRow = SalesforceForecastCategoryRow { + category: SalesforceForecastPipeline, + wire_value: "Pipeline", + citation: "sforce_api_objects_opportunitystage#ForecastCategory", +} + +fn salesforce_forecast_category_catalog() -> List { + [ + salesforce_forecast_best_case, + salesforce_forecast_closed, + salesforce_forecast_forecast, + salesforce_forecast_most_likely, + salesforce_forecast_omitted, + salesforce_forecast_pipeline, + ] +} + +type SalesforceOpportunityStageRow { + master_label: NonEmptyStr + api_name: NonEmptyStr + default_probability_bp: BasisPoint + forecast_category: SalesforceForecastCategory + is_closed: Bool + is_won: Bool + sort_order: Nat +} + +data salesforce_stage_row_schema_note: String = "Field-by-field mirror of the upstream OpportunityStage object under its real names: MasterLabel ('the internal label that does not get translated'), ApiName ('uniquely identifies a picklist value so it can be retrieved without using an id or master label'), DefaultProbability ('the default percentage estimate of the confidence in closing a specific opportunity for this opportunity stage value'), ForecastCategory, IsClosed, IsWon, and SortOrder ('number used to sort this value in the opportunity stage picklist'). DefaultProbability is a percent field upstream and is carried here on the std.measure BasisPoint carrier -- 60 percent is basis_point_count 6000 -- so a bare scalar with an implicit scale is unwritable." + +type SalesforceDefaultStageRoster + = DefaultStageRosterCited { rows: List, citation: NonEmptyStr } + | DefaultStageRosterUnavailable { cause: NonEmptyStr } + +data salesforce_default_stage_roster: SalesforceDefaultStageRoster = DefaultStageRosterUnavailable { + cause: "the machine-facing object reference deliberately does not enumerate the shipped default stage values (it directs callers to query OpportunityStage, per the configurability note), and the help article that lists the defaults (help.salesforce.com articleView sales.opportunity_stages) renders as a client-side shell this fetch cannot read (2026-08-01) -- refused, never filled from memory", +} diff --git a/dag/extdeps/git/git.dag b/dag/extdeps/git/git.dag index 914767050c6..cc7b9237aa0 100644 --- a/dag/extdeps/git/git.dag +++ b/dag/extdeps/git/git.dag @@ -543,34 +543,6 @@ service git.Core { } } - operation Toplevel { - input {} - output { path: FilePath from "stdout" } - readonly - transport shell { argv: ["git", "rev-parse", "--show-toplevel"] } - exit { - 0 => Unit - 128 => String "Not a git repository" - } - mock_response { - 0 => { path: "." } "hermetic checkout root: the runner contract binds cwd = repo toplevel, so the relative root composes with checkout-rooted reads exactly as the absolute toplevel does wet" - } - } - - operation HeadCommit { - input {} - output { - sha: CommitSha from "stdout" - success: Bool from "exit_success" - } - readonly - transport shell { argv: ["git", "rev-parse", "HEAD"] } - exit { - 0 => Unit - 128 => String "Not a git repository" - } - } - operation RemoteBranches { input {} output { branches: List from "stdout_lines" } @@ -708,21 +680,6 @@ service git.Core { } } - operation ShowTree { - input { ref: GitRef } - output { - tree: String from "stdout" - success: Bool from "exit_success" - stderr: String from "stderr" - } - readonly - transport shell { argv: ["git", "show", "-s", "--format=%T", "{ref}"] } - exit { - 0 => Unit - 128 => String "Invalid ref or not a git repository" - } - } - operation FetchNoTags { input { remote: String, ref: String } output { diff --git a/dag/extdeps/git/inspect.dag b/dag/extdeps/git/inspect.dag new file mode 100644 index 00000000000..48f52cdd8ed --- /dev/null +++ b/dag/extdeps/git/inspect.dag @@ -0,0 +1,90 @@ +module extdeps.git.inspect + +import std.types { Bool, CommitSha, FilePath, GitRef, String } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "git-scm.com/docs" + } +} + +data git_inspect_service_note: String = "Read-only repository-root, revision, and tree inspection operations are de-fused from the broad git.Core service so consumers pay only the upstream facts they inspect. Toplevel, HeadCommit, and ShowTree move here with their existing shapes; MergeBase and ListTreePathsAtRevision extend this same inspection authority for revision-addressed Rust-source observation. No second Git inspection surface remains in extdeps.git." + +service git.Inspect { + operation Toplevel { + input {} + output { path: FilePath from "stdout" } + readonly + transport shell { argv: ["git", "rev-parse", "--show-toplevel"] } + exit { + 0 => Unit + 128 => String "Not a git repository" + } + mock_response { + 0 => { path: "." } "hermetic checkout root: the runner contract binds cwd = repo toplevel, so the relative root composes with checkout-rooted reads exactly as the absolute toplevel does wet" + } + } + + operation HeadCommit { + input {} + output { + sha: CommitSha from "stdout" + success: Bool from "exit_success" + } + readonly + transport shell { argv: ["git", "rev-parse", "HEAD"] } + exit { + 0 => Unit + 128 => String "Not a git repository" + } + } + + operation MergeBase { + input { left: GitRef, right: GitRef } + output { + sha: CommitSha from "stdout" + success: Bool from "exit_success" + stderr: String from "stderr" + } + readonly + transport shell { argv: ["git", "merge-base", "{left}", "{right}"] } + exit { + 0 => Unit + 1 => String "No merge base" + 128 => String "Invalid ref or not a git repository" + } + } + + operation ListTreePathsAtRevision { + input { ref: GitRef } + output { + paths_nul: String from "stdout" + success: Bool from "exit_success" + stderr: String from "stderr" + } + readonly + transport shell { argv: ["git", "ls-tree", "-rz", "--name-only", "{ref}"] } + exit { + 0 => Unit + 128 => String "Invalid ref or not a git repository" + } + } + + operation ShowTree { + input { ref: GitRef } + output { + tree: String from "stdout" + success: Bool from "exit_success" + stderr: String from "stderr" + } + readonly + transport shell { argv: ["git", "show", "-s", "--format=%T", "{ref}"] } + exit { + 0 => Unit + 128 => String "Invalid ref or not a git repository" + } + } +} diff --git a/dag/extdeps/git/mock_corpus.dag b/dag/extdeps/git/mock_corpus.dag index b76d0ae607e..84a9b32c591 100644 --- a/dag/extdeps/git/mock_corpus.dag +++ b/dag/extdeps/git/mock_corpus.dag @@ -16,6 +16,20 @@ data git_core_mock_case_remote_branches: PublishedMockCase = PublishedMockCase { case_id: 0 } +data git_inspect_mock_case_merge_base: PublishedMockCase = PublishedMockCase { + operation_key: "git.Inspect.MergeBase", + service: "git.Inspect", + operation: "MergeBase", + case_id: 0 +} + +data git_inspect_mock_case_list_tree_paths_at_revision: PublishedMockCase = PublishedMockCase { + operation_key: "git.Inspect.ListTreePathsAtRevision", + service: "git.Inspect", + operation: "ListTreePathsAtRevision", + case_id: 0 +} + data git_core_mock_case_ls_files: PublishedMockCase = PublishedMockCase { operation_key: "git.Core.LsFiles", service: "git.Core", @@ -79,9 +93,9 @@ data git_core_mock_case_show: PublishedMockCase = PublishedMockCase { case_id: 0 } -data git_core_mock_case_show_tree: PublishedMockCase = PublishedMockCase { - operation_key: "git.Core.ShowTree", - service: "git.Core", +data git_inspect_mock_case_show_tree: PublishedMockCase = PublishedMockCase { + operation_key: "git.Inspect.ShowTree", + service: "git.Inspect", operation: "ShowTree", case_id: 0 } @@ -107,17 +121,19 @@ data git_core_mock_case_head_commit_short: PublishedMockCase = PublishedMockCase case_id: 0 } -data git_core_mock_case_toplevel: PublishedMockCase = PublishedMockCase { - operation_key: "git.Core.Toplevel", - service: "git.Core", +data git_inspect_mock_case_toplevel: PublishedMockCase = PublishedMockCase { + operation_key: "git.Inspect.Toplevel", + service: "git.Inspect", operation: "Toplevel", case_id: 0 } data git_published_mock_corpus: List = [ - git_core_mock_case_toplevel, + git_inspect_mock_case_toplevel, git_core_mock_case_current_branch, git_core_mock_case_remote_branches, + git_inspect_mock_case_merge_base, + git_inspect_mock_case_list_tree_paths_at_revision, git_core_mock_case_ls_files, git_core_mock_case_diff, git_core_mock_case_diff_unified0, @@ -127,7 +143,7 @@ data git_published_mock_corpus: List = [ git_core_mock_case_rev_list, git_core_mock_case_rev_list_before, git_core_mock_case_show, - git_core_mock_case_show_tree, + git_inspect_mock_case_show_tree, git_core_mock_case_fetch_no_tags, git_core_mock_case_fetch_prune, git_core_mock_case_head_commit_short diff --git a/dag/extdeps/github/pulls.dag b/dag/extdeps/github/pulls.dag index b837d11a7d1..00ce6ca3257 100644 --- a/dag/extdeps/github/pulls.dag +++ b/dag/extdeps/github/pulls.dag @@ -1,7 +1,7 @@ module extdeps.github.pulls import extdeps.github.github { GitHubUser, default_api_base, default_per_page } -import std.types { CommitSha, FilePath, GitRef, NonEmptyStr, Timestamp, HttpMethod, AuthScheme } +import std.types { CommitSha, EpochMs, FilePath, GitRef, NonEmptyStr, Timestamp, HttpMethod, AuthScheme } import extdeps.github.errors { GitHubErrorShape } import std.credentials { CredentialSource, EnvVar } import std.serialization { WireFormat, Text } @@ -48,6 +48,73 @@ type PullRequest { draft: Bool } +data pull_timing_observation_provenance_note: String = "One-time GitHub REST pull on 2026-08-01 under this module's existing GET-pull authority. Each row is the PullRequest.created_at and PullRequest.merged_at pair returned by github.Pulls.Get, normalized once from the wire Timestamp strings to Unix epoch milliseconds so arithmetic consumers do not pretend Timestamp is subtractable. The source URL remains on every row and fetched_on makes staleness inspectable. PullRequest stays the one wire-response authority; PullTimingObservation is its cited, immutable arithmetic projection, not a second endpoint or response model." + +type PullTimingObservation { + pull_number: Int + opened_at: EpochMs + merged_at: EpochMs + source_url: NonEmptyStr + fetched_on: NonEmptyStr +} + +fn pull_timing_observation( + pull_number: Int, + opened_at: EpochMs, + merged_at: EpochMs, + source_url: NonEmptyStr, +) -> PullTimingObservation { + PullTimingObservation { + pull_number: pull_number, + opened_at: opened_at, + merged_at: merged_at, + source_url: source_url, + fetched_on: "2026-08-01", + } +} + +data gunbc_pr_7393_timing: PullTimingObservation = pull_timing_observation( + pull_number: 7393, + opened_at: 1785284539000, + merged_at: 1785298702000, + source_url: "https://github.com/gunb-ai/gunbc/pull/7393", +) + +data gunbc_pr_7405_timing: PullTimingObservation = pull_timing_observation( + pull_number: 7405, + opened_at: 1785294084000, + merged_at: 1785357921000, + source_url: "https://github.com/gunb-ai/gunbc/pull/7405", +) + +data gunbc_pr_7429_timing: PullTimingObservation = pull_timing_observation( + pull_number: 7429, + opened_at: 1785356295000, + merged_at: 1785358865000, + source_url: "https://github.com/gunb-ai/gunbc/pull/7429", +) + +data gunbc_pr_7431_timing: PullTimingObservation = pull_timing_observation( + pull_number: 7431, + opened_at: 1785358812000, + merged_at: 1785397720000, + source_url: "https://github.com/gunb-ai/gunbc/pull/7431", +) + +data gunbc_pr_7441_timing: PullTimingObservation = pull_timing_observation( + pull_number: 7441, + opened_at: 1785362512000, + merged_at: 1785370233000, + source_url: "https://github.com/gunb-ai/gunbc/pull/7441", +) + +data gunbc_pr_7542_timing: PullTimingObservation = pull_timing_observation( + pull_number: 7542, + opened_at: 1785552700000, + merged_at: 1785557096000, + source_url: "https://github.com/gunb-ai/gunbc/pull/7542", +) + type IssueComment { id: Int html_url: NonEmptyStr diff --git a/dag/extdeps/github/workflows.dag b/dag/extdeps/github/workflows.dag new file mode 100644 index 00000000000..08194798079 --- /dev/null +++ b/dag/extdeps/github/workflows.dag @@ -0,0 +1,72 @@ +module extdeps.github.workflows + +import extdeps.github.github { default_api_base } +import extdeps.github.errors { GitHubErrorShape } +import std.credentials { EnvVar } +import std.types { CommitSha, NonEmptyStr } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "docs.github.com/en/rest/actions/workflows#create-a-workflow-dispatch-event" + } +} + +data workflows_api_version: NonEmptyStr = "2026-03-10" + +data create_dispatch_version_fidelity_note: String = "Create Workflow Dispatch has two live response contracts across GitHub REST versions. Requests without X-GitHub-Api-Version default to 2022-11-28 and return 204 No Content. Version 2026-03-10 returns 200 with workflow_run_id, run_url, and html_url. This service explicitly pins 2026-03-10 in the transport header, so WorkflowDispatchReceipt is the cited response rather than a fabricated body projected from an unversioned 204. Authority: docs.github.com/en/rest/about-the-rest-api/api-versions and the Create a workflow dispatch event response schema, read 2026-08-01." + +type WorkflowDispatchReceipt { + workflow_run_id: Int + run_url: NonEmptyStr + html_url: NonEmptyStr +} + +service github.Workflows { + config { + endpoint: default_api_base + auth: Bearer + auth_source: EnvVar { name: "GITHUB_TOKEN" } + + rate_limit: { requests: 5000, per: hour, scope: core } + + retry: { max_attempts: 1, backoff: none, retry_on: [] } + } + + operation CreateDispatch { + input { + owner: String + repo: String + workflow_id: String + ref: String + expected_healed_sha: CommitSha + } + output { + workflow_run_id: Int + run_url: NonEmptyStr + html_url: NonEmptyStr + } + transport rest { + method: POST, + path: "/repos/\{owner\}/\{repo\}/actions/workflows/\{workflow_id\}/dispatches", + headers: { "X-GitHub-Api-Version": workflows_api_version }, + body: { + ref: ref, + inputs: { expected_healed_sha: expected_healed_sha } + } + } + response { + 200 => WorkflowDispatchReceipt + 401 => GitHubErrorShape + 403 => GitHubErrorShape + 404 => GitHubErrorShape + 422 => GitHubErrorShape + 429 => GitHubErrorShape + 5xx => GitHubErrorShape + } + } +} + +data create_dispatch_retry_policy_note: String = "CreateDispatch is intentionally single-attempt. Once the heal push has created a new subject, a dispatch refusal must be visible as HealDispatchRefused and leave revalidation required; silently retrying a permission, rate-limit, or server refusal into apparent success would erase the classification operators need. The owning workflow may be retriggered deliberately after diagnosis, but this operation never assumes that the GITHUB_TOKEN push created a run." diff --git a/dag/extdeps/languages/lean/overflow.dag b/dag/extdeps/languages/lean/overflow.dag new file mode 100644 index 00000000000..d1d9c7e853b --- /dev/null +++ b/dag/extdeps/languages/lean/overflow.dag @@ -0,0 +1,16 @@ +module extdeps.languages.lean.overflow + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import std.integer { OverflowReturnValueRule, ReturnWrapping } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "lean-lang.org/doc/reference/latest/Basic-Types/Fixed-Precision-Integers/" + } +} + +data lean_fixed_width_overflow_provenance_note: String = "Upstream Lean 4 fixed-precision integer overflow (extdeps_external_authority_anchor, cited 2026-08-01): literals that overflow their type's precision are interpreted modulo the precision; signed types use twos-complement (lean-lang Fixed-Precision-Integers). Init.Data.UInt.Basic documents UInt8.add, UInt8.mul, UInt8.pow, and width conversions as wrapping around on overflow. Catalog fixed-width scalars ground on ReturnWrapping — not saturating, trap, or undefined." + +data lean_upstream_fixed_width_overflow_returns_rule: OverflowReturnValueRule = ReturnWrapping diff --git a/dag/extdeps/languages/typescript/program.dag b/dag/extdeps/languages/typescript/program.dag index eaeb99c1e82..ae6bd05909b 100644 --- a/dag/extdeps/languages/typescript/program.dag +++ b/dag/extdeps/languages/typescript/program.dag @@ -302,19 +302,27 @@ fn serialize_ts_operand(e: TsExpr, parent_op: String, is_right: Bool) -> String ts_group_if(text: serialize_ts_expr(e: e), needed: needed) } +data receiver_grouping_note: String = "A member access, index, or call binds tighter than any operator, so a receiver that IS an operator expression must be grouped or the emitted bytes re-associate: `ts_member(ts_binop(a, '||', b), 'forEach')` serialized as `a || b.forEach(...)`, which is not a syntax error but a SILENT no-op when `a` is truthy — the fabricated-plausible-output shape at the emitter layer, found live when the roadmap client's `(attempt.segments || []).forEach` ledger fill appended nothing and no console error fired. The receiver is grouped exactly when it is not an atom: atoms (identifiers, literals, calls, members) have nothing to re-associate, per atom_operand_never_grouped_note." + +fn serialize_ts_receiver(e: TsExpr) -> String { + ts_group_if( + text: serialize_ts_expr(e: e), + needed: ts_expr_precedence(e: e) != ts_atom_precedence) +} + fn serialize_ts_expr(e: TsExpr) -> String { match e { LitStr { value: v, quote: q } => serialize_string_literal(value: v, quote: q) LitNum { text: t } => t LitRegex { source: s } => concat("/", concat(s, "/")) Ident { name: n } => n - Member { object: o, property: p } => concat(serialize_ts_expr(e: o), concat(".", p)) + Member { object: o, property: p } => concat(serialize_ts_receiver(e: o), concat(".", p)) Index { object: o, index: i } => - concat(serialize_ts_expr(e: o), concat("[", concat(serialize_ts_expr(e: i), "]"))) + concat(serialize_ts_receiver(e: o), concat("[", concat(serialize_ts_expr(e: i), "]"))) Call { callee: c, args: a } => - concat(serialize_ts_expr(e: c), concat("(", concat(join_expr_args(args: a), ")"))) + concat(serialize_ts_receiver(e: c), concat("(", concat(join_expr_args(args: a), ")"))) New { callee: c, args: a } => - concat("new ", concat(serialize_ts_expr(e: c), concat("(", concat(join_expr_args(args: a), ")")))) + concat("new ", concat(serialize_ts_receiver(e: c), concat("(", concat(join_expr_args(args: a), ")")))) ArrowFn { params: ps, body: b } => concat("(", concat(join(ps, ", "), concat(") => ", serialize_ts_stmt_body(s: b)))) BinOp { left: l, op: o, right: r } => diff --git a/dag/extdeps/languages/yaml/gha_workflow.dag b/dag/extdeps/languages/yaml/gha_workflow.dag index a3b4c098d0c..6b27794152e 100644 --- a/dag/extdeps/languages/yaml/gha_workflow.dag +++ b/dag/extdeps/languages/yaml/gha_workflow.dag @@ -11,6 +11,7 @@ import extdeps.cron.schedule_model { render_cron_schedule } import extdeps.github.actions { Workflow, Job, Step, RunStep, UsesStep, ActionRef, WorkflowTrigger, Push, PullRequest, WorkflowDispatch, Schedule, MergeGroup, + DispatchInput, DispatchInputType, InputString, InputBoolean, InputChoice, InputEnvironment, PullRequestActivity, Opened, Synchronize, Reopened, ReadyForReview, Closed, WorkflowPermissions, PermissionLevel, PermRead, PermWrite, PermNone, ConcurrencySpec, ConcurrencyScalar, ConcurrencyMappingQueueMax, ConcurrencyMappingQueueNotMax, @@ -88,10 +89,57 @@ fn push_trigger_yaml(branches: List, paths: List) -> YamlKeyValu kv(key: "push", value: yaml_mapping(entries: entries)) } +fn dispatch_input_type_label(t: DispatchInputType) -> String { + match t { + InputString => "string" + InputBoolean => "boolean" + InputChoice => "choice" + InputEnvironment => "environment" + } +} + +fn dispatch_input_optional_string(key: String, value: String?) -> List { + match value { + Present { value: s } => [kv(key: key, value: yaml_string(s: s))] + Absent => [] + } +} + +fn dispatch_input_yaml(input: DispatchInput) -> YamlKeyValue { + kv( + key: input.name, + value: yaml_mapping(entries: concat( + dispatch_input_optional_string(key: "description", value: input.description), + concat( + [kv(key: "required", value: yaml_bool(b: input.required))], + concat( + dispatch_input_optional_string(key: "default", value: input.default), + [kv(key: "type", value: yaml_string(s: dispatch_input_type_label(t: input.type)))] + ) + ) + )) + ) +} + +fn workflow_dispatch_yaml(inputs: List) -> YamlValue { + if length(inputs) == 0 { + YamlNull + } else { + yaml_mapping(entries: [ + kv( + key: "inputs", + value: yaml_mapping(entries: map(inputs, input => dispatch_input_yaml(input: input))) + ) + ]) + } +} + +data workflow_dispatch_input_projection_note: String = "WorkflowDispatch.inputs was modeled in extdeps.github.actions but this serializer discarded the entire list and emitted workflow_dispatch: null. That specification-without-execution gap is now closed: every modeled input is projected into the workflow syntax, while the empty list deliberately retains the compact null form. The CI heal revalidation input is the first production consumer and its witness reads the projected YamlValue rather than trusting generated text." + fn workflow_trigger_entry(trigger: WorkflowTrigger) -> YamlKeyValue { match trigger { - WorkflowDispatch { inputs: _ } => - kv(key: "workflow_dispatch", value: YamlNull) + WorkflowDispatch { inputs: inputs } => + kv(key: "workflow_dispatch", value: workflow_dispatch_yaml(inputs: inputs)) Push { branches: bs, paths: ps } => push_trigger_yaml(branches: bs, paths: ps) PullRequest { branches: bs, types: ts } => diff --git a/dag/extdeps/project_schedule/cpm_pert.dag b/dag/extdeps/project_schedule/cpm_pert.dag new file mode 100644 index 00000000000..92e4ec1f131 --- /dev/null +++ b/dag/extdeps/project_schedule/cpm_pert.dag @@ -0,0 +1,30 @@ +module extdeps.project_schedule.cpm_pert + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.gao.gov/products/gao-16-89g" + } +} + +data rcpsp_lower_bound_external_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "doi.org/10.1016/S0377-2217(02)00763-4" + } +} + +data schedule_method_grounding_note: String = "GAO-16-89G, Schedule Assessment Guide: Best Practices for Project Schedules, is the cited scheduling authority. Best Practice 6 defines the critical path as the longest continuous sequence of activities and the program's earliest completion/minimum duration. Its schedule-risk guidance calls for three-point duration estimates (minimum, most likely, maximum). This module names those two standard method roles only; the roadmap instantiation supplies its own observed distribution and dependency graph." + +data resource_capacity_lower_bound_grounding_note: String = "Baptiste and Demassey, On linear lower bounds for the resource constrained project scheduling problem, European Journal of Operational Research 149(2), 2003, DOI 10.1016/S0377-2217(02)00763-4, grounds makespan lower bounds for RCPSP. For one declared renewable slot resource, if each activity continuously occupies one slot for its modeled duration, every feasible schedule is bounded below by ceil(total slot-time / slot capacity). Taking the maximum of that work-volume bound and the precedence critical-path bound remains a lower bound; it is not a resource-leveled schedule." + +type CriticalPathMethod {} +type ThreePointEstimateMethod {} +type ResourceCapacityLowerBoundMethod {} + +data critical_path_method: CriticalPathMethod = CriticalPathMethod {} +data three_point_estimate_method: ThreePointEstimateMethod = ThreePointEstimateMethod {} +data resource_capacity_lower_bound_method: ResourceCapacityLowerBoundMethod = ResourceCapacityLowerBoundMethod {} diff --git a/dag/extdeps/tailscale/serve.dag b/dag/extdeps/tailscale/serve.dag index 501e511f0fe..589c62dd4b1 100644 --- a/dag/extdeps/tailscale/serve.dag +++ b/dag/extdeps/tailscale/serve.dag @@ -6,6 +6,17 @@ import extdeps.version.semver { SemVerVersion, semver_compare, semver_version_la import extdeps.external_authority { ExternalAuthority } import extdeps.uri { Uri, Https } +import extdeps.languages.json.emit { + JsonValue, JsonNull, JsonBool, JsonNumber, JsonString, JsonArray, JsonObject, +} +import extdeps.languages.json.parse { + parse_json, + json_object_unique_member, + JsonMemberFound, + JsonMemberAbsent, + JsonMemberDuplicated, + JsonMemberNotAnObject, +} data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { @@ -56,6 +67,73 @@ fn tailscale_serve_off_argv(endpoint: TailscaleServeEndpoint) -> List { ) } +fn tailscale_serve_status_json_argv() -> List { + ["serve", "status", "--json"] +} + +fn tailscale_serve_proxy_url(endpoint: TailscaleServeEndpoint) -> String { + concat("http://127.0.0.1:", endpoint.backend as String) +} + +fn tailscale_serve_mount_status_key(mount: TailscaleServeMount) -> String { + match mount { + ServeRootMount => "/" + ServeSetPath { path } => path as String + } +} + +fn json_unique_member_value(v: JsonValue, key: String) -> JsonValue? { + match json_object_unique_member(v: v, key: key) { + JsonMemberFound { value } => Present { value: value } + JsonMemberAbsent => none + JsonMemberDuplicated { count: _ } => none + JsonMemberNotAnObject => none + } +} + +fn tailscale_status_handler_proxy(handler: JsonValue) -> List { + match json_unique_member_value(v: handler, key: "Proxy") { + Present { value: JsonString { value: proxy } } => [proxy] + Present { value: _ } => [] + Absent => [] + } +} + +fn tailscale_status_web_entry_proxies(entry: JsonValue, mount: TailscaleServeMount) -> List { + match json_unique_member_value(v: entry, key: "Handlers") { + Present { value: handlers } => + match json_unique_member_value(v: handlers, key: tailscale_serve_mount_status_key(mount: mount)) { + Present { value: handler } => tailscale_status_handler_proxy(handler: handler) + Absent => [] + } + Absent => [] + } +} + +fn tailscale_status_web_proxies(web: JsonValue, mount: TailscaleServeMount) -> List { + match web { + JsonObject { members } => flat_map(members, member => tailscale_status_web_entry_proxies(entry: member.value, mount: mount)) + JsonNull => [] + JsonBool { value: _ } => [] + JsonNumber { lexeme: _ } => [] + JsonString { value: _ } => [] + JsonArray { elements: _ } => [] + } +} + +data serve_status_json_projection_note: String = "Tailscale documents `serve status --json` as the machine-readable status surface. For the legacy node Serve configuration used by srv1, Web..Handlers..Proxy names the currently routed backend. DNS is intentionally not treated as identity here: it is a map key derived by tailscaled and the owned endpoint is listener x mount. Duplicate or wrong-shaped members yield no proxy, which the blue/green selector refuses before mutation; they never select a plausible slot." + +fn tailscale_serve_status_proxy_targets(status_json: String, mount: TailscaleServeMount) -> List { + match parse_json(s: status_json) { + Present { value: doc } => + match json_unique_member_value(v: doc, key: "Web") { + Present { value: web } => tailscale_status_web_proxies(web: web, mount: mount) + Absent => [] + } + Absent => [] + } +} + data serve_scoped_removal_capability_note: String = "SCOPED REMOVAL IS A VERSION-OBSERVED CAPABILITY, NOT A CONSTANT — and the constant it replaces was wrong. This module's consumer previously asserted that tailscale serve has no per-port removal and only a node-global reset, built a refusal arm on that premise, and cited nothing. The upstream documents scoped removal via `off`, and separately notes that the Serve CLI CHANGED in client 1.52 (review, 2026-07-30; tailscale.com/kb/1242).\\n\\nSo the capability is a fact about the client installed on a particular host at a particular time, which is why it is a function of an observed version rather than a row. The verdict is three-valued because an unobserved version is neither capable nor incapable, and collapsing that to either pole is what produced the false claim above. A consumer must refuse BEFORE mutating rather than after, because the failure this guards is a teardown that silently removes nothing and reports success.\\n\\nThe boundary is stated as the version the upstream itself names, not as a version this repo picked." data tailscale_scoped_serve_off_minimum: SemVerVersion = SemVerVersion { @@ -98,4 +176,15 @@ service tailscale.Serve { nonzero => String "tailscale serve status failed" } } + + operation StatusJson { + input {} + output { stdout: String from "stdout" } + readonly + transport shell { argv: ["tailscale", "serve", "status", "--json"] } + exit { + 0 => Unit + nonzero => String "tailscale serve status --json failed" + } + } } diff --git a/dag/extdeps/tmux/tmux.dag b/dag/extdeps/tmux/tmux.dag index 76f642bbd41..f09fc59d3da 100644 --- a/dag/extdeps/tmux/tmux.dag +++ b/dag/extdeps/tmux/tmux.dag @@ -11,6 +11,15 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { } } +data tmux_client_connect_error_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "github.com/tmux/tmux/blob/master/client.c" + } +} + +data tmux_client_connect_error_note: String = "Upstream tmux client.c client_connect passes connect(2) failures through errno. The client prints `error connecting to ()` for errors other than ECONNREFUSED; ENOENT is rendered as `No such file or directory` and means the selected Unix socket path does not exist, so no tmux server owns that socket and its session set is empty. ECONNREFUSED is deliberately distinct: tmux renders it as `no server running`, while a literal `error connecting ... (Connection refused)` is not the cited ENOENT fact and must not be classified from this row." + type TmuxServerScope = DefaultTmuxServer | NamedTmuxServer { socket_name: NonEmptyStr } diff --git a/dag/extdeps/vendor/alphabet.dag b/dag/extdeps/vendor/alphabet.dag new file mode 100644 index 00000000000..1963b5f9f73 --- /dev/null +++ b/dag/extdeps/vendor/alphabet.dag @@ -0,0 +1,16 @@ +module extdeps.vendor.alphabet + +import extdeps.vendor { Vendor } +import extdeps.securities { Securities } + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.sec.gov/cgi-bin/browse-edgar?CIK=GOOGL" + } +} + +data alphabet: Vendor = Vendor { legal_name: "Alphabet Inc." } diff --git a/dag/extdeps/vendor/hubspot.dag b/dag/extdeps/vendor/hubspot.dag new file mode 100644 index 00000000000..2c26b1103ce --- /dev/null +++ b/dag/extdeps/vendor/hubspot.dag @@ -0,0 +1,16 @@ +module extdeps.vendor.hubspot + +import extdeps.vendor { Vendor } +import extdeps.securities { Securities } + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.sec.gov/cgi-bin/browse-edgar?CIK=HUBS" + } +} + +data hubspot: Vendor = Vendor { legal_name: "HubSpot, Inc." } diff --git a/dag/extdeps/vendor/meta_platforms.dag b/dag/extdeps/vendor/meta_platforms.dag new file mode 100644 index 00000000000..7e7b377431e --- /dev/null +++ b/dag/extdeps/vendor/meta_platforms.dag @@ -0,0 +1,16 @@ +module extdeps.vendor.meta_platforms + +import extdeps.vendor { Vendor } +import extdeps.securities { Securities } + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.sec.gov/cgi-bin/browse-edgar?CIK=META" + } +} + +data meta_platforms: Vendor = Vendor { legal_name: "Meta Platforms, Inc." } diff --git a/dag/extdeps/vendor/salesforce.dag b/dag/extdeps/vendor/salesforce.dag new file mode 100644 index 00000000000..be428bbc01a --- /dev/null +++ b/dag/extdeps/vendor/salesforce.dag @@ -0,0 +1,16 @@ +module extdeps.vendor.salesforce + +import extdeps.vendor { Vendor } +import extdeps.securities { Securities } + +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.sec.gov/cgi-bin/browse-edgar?CIK=CRM" + } +} + +data salesforce: Vendor = Vendor { legal_name: "Salesforce, Inc." } diff --git a/dag/gunbc/build_cache_instance.dag b/dag/gunbc/build_cache_instance.dag new file mode 100644 index 00000000000..ca1f26d49d8 --- /dev/null +++ b/dag/gunbc/build_cache_instance.dag @@ -0,0 +1,210 @@ +module gunbc.build_cache_instance + +import std.types { String, NonEmptyStr, Bool, List } +import std.nat { Nat } +import std.measure { ByteSize } +import std.cache_identity { CacheInterfaceId } +import std.disposition { Disposition, SingleAuthority } +import product.placement_supply { HostIdentity } +import extdeps.cache.sccache { SccacheServerEndpoint, SccacheServerUnixSocket, sccache_local_id } +import gunbc.fleet_intent_network { operator_host_srv1, operator_host_srv2, operator_host_srv3 } +import gunbc.host_layout { gunbc_managed_compile_pool_slice } + +data build_cache_instance_scope_disposition: Disposition = SingleAuthority + +data build_cache_instance_grain_note: String = "CATALOG IDENTITY IS NOT DEPLOYED-INSTANCE IDENTITY, and conflating them is why a host carrying two differently-configured uses of one implementation could not be described. gunbc.host_build_cache_provision speaks of provisioning `the sccache_local catalog` on a host; extdeps.cache.sccache.sccache_local_id names sccache's cache SEMANTICS and its pinned binary. Neither identifies a running daemon. srv1 and srv2 each run TWO sccache servers from that one catalog row — a CI server (principal ghrunner, /var/lib/ctrl/sccache-ci, endpoint /var/lib/ctrl/sccache-ci/server.sock) and a session server (principal briansrls, /var/lib/ctrl/sccache, 50G, endpoint /var/lib/ctrl/sccache/server.sock) — so 'provision the catalog on this host' names neither of them and cannot express that one is correctly placed while the other is not. BuildCacheInstance is the deployed grain: one row per (host, consumer class, instance name), carrying the endpoint and the INTENDED unit, principal, storage, capacity and compile pool that an observation is checked against. One concept with multiple data rows, never a second provisioning mechanism per use (DESIGN section 2 horizontal)." + +type BuildCacheConsumerClass + = ContinuousIntegrationCompiles + | InteractiveSessionCompiles + +type BuildCacheInstanceId { + host: HostIdentity + consumer_class: BuildCacheConsumerClass + instance_name: NonEmptyStr +} + +type BuildCacheInstance { + identity: BuildCacheInstanceId + implementation: CacheInterfaceId + endpoint: SccacheServerEndpoint + intended_unit: NonEmptyStr + intended_principal: NonEmptyStr + intended_storage: NonEmptyStr + intended_capacity: ByteSize? + intended_compile_pool: NonEmptyStr +} + +data process_identity_note: String = "A BARE PID IS NOT AN IDENTITY: the kernel reuses pids, so a pid recorded in one observation and re-read in the next may name a different process, and a receipt that says 'pid 3975946 is the endpoint owner' is only true relative to a boot and a start time. boot_id distinguishes across reboots; start_time (the process's own start, monotonic within a boot) distinguishes across pid reuse within one. The triple is what makes two consecutive observations comparable, which is exactly what the non-mutation control asserts." + +type ProcessIdentity { + boot_id: NonEmptyStr + pid: Nat + start_time: NonEmptyStr +} + +data runner_slot_lifecycle_note: String = "A RETIRED SLOT IS A CURRENT-INTENT FACT, NOT AN ARCHAEOLOGICAL ONE. Whether srv1-10 is retired is decided by asking whether it appears in the fleet's desired runner slots today — not by explaining how the fleet once had ten. The lifecycle distinction is kept in the receipt because it names WHICH construction wall failed: an ACTIVE runner owning the endpoint means birth ordering failed (a runner ran before a managed cache existed); a RETIRED runner owning it means birth ordering AND retirement both failed (the slot's unit was disabled while its cgroup stayed computationally alive, still owning an endpoint and still parenting new compiler children under its ceiling). Both refuse." + +type RunnerSlotIdentity { + host: HostIdentity + slot_index: Nat +} + +type RunnerSlotLifecycle + = DesiredRunnerSlot + | RetiredRunnerSlot + +type CgroupMemoryLimits { + memory_max: ByteSize + memory_high: ByteSize + memory_current: ByteSize +} + +type CgroupMemoryEvents { + low: Nat + high: Nat + max: Nat + oom: Nat + oom_kill: Nat +} + +data endpoint_ownership_note: String = "ENDPOINT OWNERSHIP IS THE PRIMARY OBSERVATION; PROCESS NAME PLUS CGROUP IS NOT ENOUGH. The sccache binary is both client and server, so under RUSTC_WRAPPER every in-flight compile also runs a process named 'sccache' inside the cgroup of the runner that issued it. Those clients are HEALTHY and expected — srv1 carried three of them (ages 296s, 112s, 86s, under live slots 01/04/05) at the moment the receipt was taken — so a probe that selects sccache processes by name and reads a cgroup samples a client and reports the WRONG placement, and a probe that refuses on 'multiple sccache processes' produces a FALSE refusal in the ordinary case. The server is identified positively and uniquely as the process HOLDING THE LISTEN on the instance's configured endpoint. For the UDS shape the whole chain is non-mutating and needs no sccache invocation: the exact endpoint path in /proc/net/unix yields the listening socket inode, /proc/*/fd resolves that inode to one owning pid, and /proc/pid/\{exe,environ,cgroup,stat\} yields executable, principal, unit and cgroup. It depends on no pgrep, head -1, ss or lsof, and it refuses naturally when the path, the inode, or the owner is not exactly one." + +type EndpointObservationUnavailableReason + = ReachRefused { detail: NonEmptyStr } + | ProcTableUnreadable { detail: NonEmptyStr } + | EndpointPathUnreadable { detail: NonEmptyStr } + +data endpoint_owner_context_note: String = "WHAT THE OWNER'S CGROUP RESOLVES TO IS ONE FACT WITH THREE SHAPES, not three fields that happen to agree. The first version of this carrier spelled it as a bare `unit` beside `owning_runner_slot: RunnerSlotIdentity?` and `runner_slot_lifecycle: RunnerSlotLifecycle?`, which admitted states no observation can produce and no reader can interpret: a slot with no lifecycle, a lifecycle with no slot, or a unit contradicting both. The lifecycle-without-slot case was the dangerous one — the derivation matched on the slot first, so an Absent slot carrying a Present lifecycle fell straight through to the unit comparison and could return ServerManagedUnitOwned, converging on a runner-owned endpoint from inconsistent substrate data (review 46217). Guarding that with a check would have conceded the state is writable; the coproduct makes it unwritable instead (DESIGN section 5, construction over validation). A slot cannot be recorded without deciding its lifecycle against current fleet intent, because deciding it is the same act as finding it. When the chain reaches a cgroup it cannot classify — an unreadable unit, a slot index that does not parse, a desired-slot roster the observer could not read — that is its own positive arm carrying the located reason, never a half-filled record." + +type EndpointOwnerContext + = OwnerInRunnerSlot { slot: RunnerSlotIdentity, lifecycle: RunnerSlotLifecycle } + | OwnerInSystemdUnit { unit: NonEmptyStr } + | OwnerContextUndecided { detail: NonEmptyStr } + +type BuildCacheEndpointObservation + = EndpointAbsent { instance: BuildCacheInstance } + | EndpointOwnerObserved { + instance: BuildCacheInstance + owner: ProcessIdentity + executable: NonEmptyStr + principal: NonEmptyStr + cgroup: NonEmptyStr + owner_context: EndpointOwnerContext + memory_limits: CgroupMemoryLimits? + memory_events: CgroupMemoryEvents? + } + | EndpointOwnershipAmbiguous { instance: BuildCacheInstance, candidate_count: Nat } + | EndpointObservationUnavailable { + instance: BuildCacheInstance + reason: EndpointObservationUnavailableReason + } + +data build_cache_server_placement_authority_note: String = "THE SINGLE PLACEMENT AUTHORITY, homed here rather than in gunbc.host_build_cache_provision because placement is derived from an OBSERVATION of a deployed instance, and the verdict fold is a CONSUMER of it. Keeping the type beside the observation it is derived from means there is exactly one definition of what 'correctly placed' is, and the provisioning fold cannot drift a second opinion. Placement is a verdict axis and not a detail because extdeps.cache.sccache.sccache_server_process_facts cites that cache-miss compiles execute as CHILDREN OF THE SERVER PROCESS: the server's cgroup is the cgroup every compile on the host runs in, so the server's caps bound every compile on the host and not merely the caps of the job that issued them. A responsive daemon proves nothing about where it runs." + +type BuildCacheServerPlacement + = ServerManagedUnitOwned { unit: NonEmptyStr } + | ServerRunnerOwned { slot: RunnerSlotIdentity, lifecycle: RunnerSlotLifecycle } + | ServerUnsupervised { cgroup: NonEmptyStr } + | ServerPlacementAbsent + | ServerPlacementUnknown { detail: NonEmptyStr } + +fn placement_of_owner( + instance: BuildCacheInstance, + cgroup: NonEmptyStr, + owner_context: EndpointOwnerContext, +) -> BuildCacheServerPlacement { + match owner_context { + OwnerInRunnerSlot { slot: s, lifecycle: lc } => + ServerRunnerOwned { slot: s, lifecycle: lc } + OwnerInSystemdUnit { unit: u } => + match (u as String) == (instance.intended_unit as String) { + true => ServerManagedUnitOwned { unit: u } + false => ServerUnsupervised { cgroup: cgroup } + } + OwnerContextUndecided { detail: d } => ServerPlacementUnknown { detail: d } + } +} + +fn placement_of_endpoint_observation( + observed: BuildCacheEndpointObservation, +) -> BuildCacheServerPlacement { + match observed { + EndpointAbsent { instance: _ } => ServerPlacementAbsent + EndpointOwnerObserved { + instance: inst, + owner: _, + executable: _, + principal: _, + cgroup: cg, + owner_context: ctx, + memory_limits: _, + memory_events: _, + } => + placement_of_owner(instance: inst, cgroup: cg, owner_context: ctx) + EndpointOwnershipAmbiguous { instance: _, candidate_count: n } => + ServerPlacementUnknown { + detail: concat( + "the configured endpoint did not resolve to exactly one listening owner; candidate count ", + to_string(n), + ) as NonEmptyStr, + } + EndpointObservationUnavailable { instance: _, reason: r } => + ServerPlacementUnknown { detail: unavailable_reason_detail(reason: r) } + } +} + +fn unavailable_reason_detail(reason: EndpointObservationUnavailableReason) -> NonEmptyStr { + match reason { + ReachRefused { detail: d } => + concat("the host could not be reached, so placement is unobserved rather than absent: ", d as String) as NonEmptyStr + ProcTableUnreadable { detail: d } => + concat("the process table could not be read, so the endpoint owner is unresolved: ", d as String) as NonEmptyStr + EndpointPathUnreadable { detail: d } => + concat("the endpoint path could not be read, so listener ownership is unresolved: ", d as String) as NonEmptyStr + } +} + +data build_cache_desired_instances_note: String = "THE DESIRED DEPLOYED INSTANCES, one row per (host, consumer class). These state INTENT and are deliberately not a description of the live hosts: the whole point of the observer is that intent and observation are different things and their difference is refusable. As of the 2026-08-01 receipt NO host satisfies intended_unit for its CI instance — on srv1, srv2 and srv3 alike the CI endpoint is owned by a runner-slot process, never by a managed unit — so every row below currently refuses, which is the correct and intended reading rather than a defect in the rows. intended_capacity is left Absent for CI on purpose: the live 10 GiB is sccache's built-in default rather than a chosen number, and choosing it is explicitly a later increment, so declaring one here would fabricate a decision nobody has made. The session instances carry the 50G the ctrl unit actually sets." + +fn ci_cache_instance(host: HostIdentity) -> BuildCacheInstance { + BuildCacheInstance { + identity: BuildCacheInstanceId { + host: host, + consumer_class: ContinuousIntegrationCompiles, + instance_name: "sccache-ci" as NonEmptyStr, + }, + implementation: sccache_local_id, + endpoint: SccacheServerUnixSocket { path: "/var/lib/ctrl/sccache-ci/server.sock" as NonEmptyStr }, + intended_unit: "gunbc-build-cache-ci.service" as NonEmptyStr, + intended_principal: "ghrunner" as NonEmptyStr, + intended_storage: "/var/lib/ctrl/sccache-ci" as NonEmptyStr, + intended_capacity: none, + intended_compile_pool: gunbc_managed_compile_pool_slice, + } +} + +fn session_cache_instance(host: HostIdentity) -> BuildCacheInstance { + BuildCacheInstance { + identity: BuildCacheInstanceId { + host: host, + consumer_class: InteractiveSessionCompiles, + instance_name: "sccache-session" as NonEmptyStr, + }, + implementation: sccache_local_id, + endpoint: SccacheServerUnixSocket { path: "/var/lib/ctrl/sccache/server.sock" as NonEmptyStr }, + intended_unit: "gunbc-build-cache-session.service" as NonEmptyStr, + intended_principal: "briansrls" as NonEmptyStr, + intended_storage: "/var/lib/ctrl/sccache" as NonEmptyStr, + intended_capacity: none, + intended_compile_pool: gunbc_managed_compile_pool_slice, + } +} + +data build_cache_desired_instances: List = [ + ci_cache_instance(host: operator_host_srv1), + session_cache_instance(host: operator_host_srv1), + ci_cache_instance(host: operator_host_srv2), + session_cache_instance(host: operator_host_srv2), + ci_cache_instance(host: operator_host_srv3), +] diff --git a/dag/gunbc/ci_layer_roots.dag b/dag/gunbc/ci_layer_roots.dag index 1c80000deb6..3c5d6afa6cf 100644 --- a/dag/gunbc/ci_layer_roots.dag +++ b/dag/gunbc/ci_layer_roots.dag @@ -70,6 +70,12 @@ data excl_install_media_reason: String = "real host-effect execution witness (sh data excl_install_media_dissolve: String = "mock_response coverage lands and assertions are re-checked under a fixed mock; then the fns re-enroll as ordinary hermetic discovery rows and this row deletes (install_media_real_execution_wet_note)" data witness_exclusion_frontier: List = [ + WitnessExclusionRow { + pattern: "stage0_rust_host_observation_live_witness_test.dag", + classification: OfflineLocalRecipe, + reason: "SUBSTANTIATED per-row: every fn in this file calls gunbc.stage0_rust_host_observation current_stage0_rust_source_observation, whose realization reaches git.Inspect.HeadCommit and git.Inspect.MergeBase. Neither carries a mock_response, so the hermetic envelope does not skip these -- it REFUSES, declining to fabricate rather than inventing a revision. Because a refusing discovery member fails its whole batch, leaving them enrolled took down the entire per-PR witness corpus (CI receipt on PR 7562: batch 3 discovery-corpus, 2 roots + 105 explicit). Publishing the mocks was tried and reverted: it only moves the refusal to the next unmocked operation, and mocking the whole revision-addressed chain would let scaffold_host_observation_is_live_and_observed pass against a constant sha -- reflection evidence standing in for the live read the witness exists to prove. Falsifier wet enrollment is not available either: falsifier_self_host_wet_cadence_active is false, so a row there would satisfy a coverage argument while executing nowhere. The honest state is excluded, reason stated, gap counted -- the same class and the same trigger as the wet_integration_offline_note rows.", + dissolve_on: "mock_response coverage lands for the git.Inspect revision-addressed operations AND the assertions are re-checked so that they still discriminate under a fixed mock (a liveness claim that a constant satisfies must be restated or dropped); or the wet lane un-darkens and these fns enroll there as declared execution rows. Either way this row deletes." + }, WitnessExclusionRow { pattern: "claim/long/mandatory_tag_extdeps_corpus", classification: OfflineLocalRecipe, @@ -106,6 +112,12 @@ data witness_exclusion_frontier: List = [ reason: "PAIRED with a known_red_probe_entries row (Phase 0(b) admission: an excluded witness must name an EXECUTING consumer — the exclusion alone is orphaned enrollment, and the discovery corpus refuses it as UnexecutedDeferredWitness). Measured: shipping the exclusion without the probe row reded the floor with exactly that cause, which is the invariant working. The acceptance test for roadmap node observation-raw-print-retirement, red BY DESIGN: it asserts observation_emit_frontier_count() == 0 (that node's handback, 'the classified zero-residue progress census') while the census counts 8 unmigrated emit sites today. It is the discriminating validation on that node's WorkItemExecutionContract, so dispatch's Verify segment has a target that can actually FAIL — without a red validation somewhere, a green Verify proves only that the plumbing ran. Not a broken witness and not relaxable: the only edit that may green it is migrating the sites it counts.", dissolve_on: "the frontier reaches zero — at which point this row is a still-red expectation against a green witness and MUST be deleted in the same change that lands the last migration, promoting the witness to ordinary DiscoverySelection as a standing regression ratchet (the counted un-quarantine event of std.witness_admission)" }, + WitnessExclusionRow { + pattern: "source_integration_proof_kernel_acceptance_test.dag", + classification: QuarantineProbeExpectRed, + reason: "PAIRED with the known_red_probe_entries row below. This is the discriminating closing validation for roadmap node 2-scm-p1-proof-kernel, RED BY DESIGN while the derived KernelAcceptanceCapability frontier is nonzero. The first slice supplies the bounded candidate-generation model check; the one public fold, occurrence dedup, context transport, commuting squares, batch invariance, n-way safety, order honesty, all seven closure certificates, and data-only invariant extension stay missing until executing scenarios establish them. The validation cannot pass on zero progress and is bound directly by scm_p1_proof_kernel_execution_contract.", + dissolve_on: "every required KernelAcceptanceCapability has a passing executing scenario — then this expect-red row and its known_red_probe_entries row delete in the completing change, promoting the unchanged acceptance witness to ordinary DiscoverySelection as the permanent P1 regression wall" + }, WitnessExclusionRow { pattern: "glob_discovery.dag", classification: OfflineLocalRecipe, @@ -742,7 +754,8 @@ data known_red_probe_entries: List = [ probe_red(entry: "src/v2/test/claim/manual/english_emit_add_test.dag", f: "english_emit_add_ingest_round_trip_holds"), probe_red(entry: "src/v2/test/claim/manual/english_emit_add_test.dag", f: "english_emit_add_prose_holds"), probe_red(entry: "src/v2/test/claim/self_host/compiler_closure_emit_from_ingest_test.dag", f: "compiler_closure_scoped_ingest_module_count_ok_holds"), - probe_red(entry: "dag/test/claim/observation_raw_print_retirement_acceptance_test.dag", f: "observation_emit_frontier_is_zero") + probe_red(entry: "dag/test/claim/observation_raw_print_retirement_acceptance_test.dag", f: "observation_emit_frontier_is_zero"), + probe_red(entry: "dag/test/claim/source_integration_proof_kernel_acceptance_test.dag", f: "witness_p1_proof_kernel_acceptance_contract_holds") ] fn known_red_probe_roster() -> List { @@ -759,7 +772,7 @@ This roster must not grow silently: a new known-red admission requires the same Wet assemble-red SelfEmitted module receipts (admitted 2026-07-25, review #7199 / falsifier_self_host_wet_known_red_entries — NOT hermetic known_red_probe_entries): six SelfEmitted wet behavioral receipts (parse_engine_hooks, use_site_verdict, discovery_enumeration, target_carriers, body_producer, 03_normalize) red on assemble (cargo/shim closure refuse — unresolved std_algebra/std_types/std_nat/List and target_carriers type-shape; cadence run 30126573464). Green wet roster stays empty for them (enrollment note: green-only). Phase-0 forbids SelfEmitted-with-zero-executing-consumer, so they run Wet expect_red on the falsifier quarantine batch; still-red is agreement; greening is a counted un-quarantine that restores each SelfHostWetReceiptBinding. Owner: #7199 falsifier lane / self-host assemble. Dissolve-on per row: local wet receipt greens under gunbc_falsifier_self_host_wet_receipt_wall_budget, the known-red entry deletes, the green wet binding restores." -data bin_witness_wet_note: String = "The remaining excluded entries are the BIN-EXECUTION witness class: their test fns run compiled seed witness binaries via tools.host_prelude.run_witness_bin (git.Core.Toplevel to locate the checkout, shell.Test.IsExecutable against target/release build artifacts, then process execution) — host effects on non-commit-deterministic inputs, so the hermetic envelope refuses them by construction (audit 2026-07-12: every refusal typed as IsExecutable / Run / IsNonEmpty / Dir; the pure fns that merely read transport modules pass hermetically and stay discovered). They carry core per-PR compiler coverage, so unlike the offline four they DO keep running on every PR — as the declared bin-witness wet batch (bin_witness_wet_entries below, a Wet-profiled explicit-entry batch in v2.workflow.ci_floor_plan), never as silently-wet discovery rows. EXCEPTION (Wave 2, 2026-07-15): emit->compile->run->equals-seed self-host behavioral receipts (Gate-4 std/logic first) moved to the falsifier nightly Wet follow-on batch (falsifier_self_host_wet_entries) — per-PR stays hermetic/mocked for that class; the falsifier is the proven template lane. Dissolve-on: these files migrate under the execution-corpus scope prefix and enroll as ExecutionWitnessKind rows in gunbc.commit_workflow; then this roster and its exclusion rows delete." +data bin_witness_wet_note: String = "The remaining excluded entries are the BIN-EXECUTION witness class: their test fns run compiled seed witness binaries via tools.host_prelude.run_witness_bin (git.Inspect.Toplevel to locate the checkout, shell.Test.IsExecutable against target/release build artifacts, then process execution) — host effects on non-commit-deterministic inputs, so the hermetic envelope refuses them by construction (audit 2026-07-12: every refusal typed as IsExecutable / Run / IsNonEmpty / Dir; the pure fns that merely read transport modules pass hermetically and stay discovered). They carry core per-PR compiler coverage, so unlike the offline four they DO keep running on every PR — as the declared bin-witness wet batch (bin_witness_wet_entries below, a Wet-profiled explicit-entry batch in v2.workflow.ci_floor_plan), never as silently-wet discovery rows. EXCEPTION (Wave 2, 2026-07-15): emit->compile->run->equals-seed self-host behavioral receipts (Gate-4 std/logic first) moved to the falsifier nightly Wet follow-on batch (falsifier_self_host_wet_entries) — per-PR stays hermetic/mocked for that class; the falsifier is the proven template lane. Dissolve-on: these files migrate under the execution-corpus scope prefix and enroll as ExecutionWitnessKind rows in gunbc.commit_workflow; then this roster and its exclusion rows delete." data falsifier_self_host_wet_note: String = "The nightly falsifier Wet follow-on batch (gunbc_falsifier_batches batch 2 in v2.workflow.ci_floor_plan): explicit ExecutionWitnessKind rows whose fns realize the emit->compile->run->equals-seed self-host behavioral receipt chain (cargo build, process execution, real gunbc emit) — too heavy and fidelity-sensitive for per-PR hermetic discovery. Gate-4 std/logic (self_host_logic_behavioral_witness_test.dag) is the inaugural template migrated out of bin_witness_wet_entries; compiler modules enroll via v2.compiler.self_host.wet_receipt_enrollment when their frontier row flips SelfEmitted (green-only — never known-red). Runs SelectionApplied (always run, not predict-only) after the hermetic affected-set falsifier sweep. Local recipe: claim_batch --wet --source-root dag --source-root src/v2 --entry --functions . Dissolve-on: per-module self-host receipts graduate to continuous CI only when the emit-on-demand / self-host cutover lane proves each module's receipt is cheap enough for per-PR admission — until then this scheduled lane is the sole live consumer." @@ -876,7 +889,7 @@ data falsifier_rehomed_bin_wet_rows: List = [ RehomedBinWetRow { entry: "dag/test/claim/dag_compile_clean_shard_totality_witness_test.dag", function: "compile_clean_shard_totality_holds_on_live_tree", - reason: "Class C corpus-totality claim (docs/plans/test-decomposition-wcf-cut.md §5, criterion 2, session bright-newt-31 2026-07-25): shells out git.Core.Toplevel + shell.Find.Files over the whole live tree to prove the shard roster is total against the real corpus (~15s) — the claim is legitimately corpus-grain, not shrinkable, since the whole point is whether the REAL tree's partition is still total. The mechanism (orphan/duplicate/surplus verdict algebra) is proven per-PR on synthetic glob results by compile_clean_shard_totality_algebra_holds, which stays per-PR", + reason: "Class C corpus-totality claim (docs/plans/test-decomposition-wcf-cut.md §5, criterion 2, session bright-newt-31 2026-07-25): shells out git.Inspect.Toplevel + shell.Find.Files over the whole live tree to prove the shard roster is total against the real corpus (~15s) — the claim is legitimately corpus-grain, not shrinkable, since the whole point is whether the REAL tree's partition is still total. The mechanism (orphan/duplicate/surplus verdict algebra) is proven per-PR on synthetic glob results by compile_clean_shard_totality_algebra_holds, which stays per-PR", dissolve_on: "re-priced only if the shard partition boundary itself changes shape enough that a fixture can stand in for the real tree without losing coverage — until then this is the honest corpus-grain residue, not a defect" }, RehomedBinWetRow { diff --git a/dag/gunbc/ci_materialization_gate.dag b/dag/gunbc/ci_materialization_gate.dag index c262e6d2454..69797e1130d 100644 --- a/dag/gunbc/ci_materialization_gate.dag +++ b/dag/gunbc/ci_materialization_gate.dag @@ -1,11 +1,14 @@ module gunbc.ci_materialization_gate - -data ci_materialization_gate_note: String = "The live binding: the ladder run over the ACTUAL emitted workflow (gunbc.ci_workflow data row), never a fixture copy of it. Demands, exemption identities, and fresh-effect scripts are all resolved against the same authority the ci.yml is emitted from, so a workflow edit that introduces cross-job duplication without a covering provider reds the enrolled witness in the same CI run that would have paid the duplication. Post-fold reality (ci_regen job folded into the ci job 2026-07-11): the workflow now has a SINGLE building job, so the release build is one demand -> AcceptedSingleRecompute (state 5: a single undeclared demand needs no provider, a cache there is dead weight), and the ladder holds WITH AND WITHOUT the sccache provider. The cross-job duplication the ladder refutes is now carried as the discriminating RED control by the synthetic placement_grain witness (two independently-building jobs -> AuthoredDuplication/RefusedNoProvider), which the live witness's demand==1 / discharged==0 pins guard against reintroducing. Fresh effects: the floor cgroup-peak measurement scripts (a memory.peak read is a new fact each run, never memoizable — the rust_tests cgroup-peak measurement was the prior subject; it retired with the rust_tests job 2026-07-11, and the ci-job floor peak pre/post reads are the same measurement category) and every deploy stage script (a deploy is the effect being the use); all resolved by projection, not copied strings." +data ci_materialization_gate_note: String = "The live binding: the ladder run over the ACTUAL emitted workflow (gunbc.ci_workflow data row), never a fixture copy of it. Demands, exemption identities, and fresh-effect scripts are all resolved against the same authority the ci.yml is emitted from, so a workflow edit that introduces cross-job duplication without a covering provider reds the enrolled witness in the same CI run that would have paid the duplication. Post-fold reality (ci_regen job folded into the ci job 2026-07-11): the workflow now has a SINGLE building job, so the release build is one demand -> AcceptedSingleRecompute (state 5: a single undeclared demand needs no provider, a cache there is dead weight), and the ladder holds WITH AND WITHOUT the sccache provider. The cross-job duplication the ladder refutes is now carried as the discriminating RED control by the synthetic placement_grain witness (two independently-building jobs -> AuthoredDuplication/RefusedNoProvider), which the live witness's demand==1 / discharged==0 pins guard against reintroducing. Fresh effects: the floor cgroup-peak measurement scripts (a memory.peak read is a new fact each run, never memoizable — the rust_tests cgroup-peak measurement was the prior subject; it retired with the rust_tests job 2026-07-11, and the ci-job floor peak pre/post reads are the same measurement category), every deploy stage script (a deploy is the effect being the use), and the heal-revalidation exact-head preflight (git HEAD is a new checkout fact in each isolated job and must be observed there, never memoized or discharged from another job); all resolved by projection, not copied strings." fn ci_fresh_effect_scripts() -> List { concat( - [step_run_script(s: ci_floor_peak_pre_step()), step_run_script(s: ci_floor_peak_post_step())], + [ + step_run_script(s: ci_floor_peak_pre_step()), + step_run_script(s: ci_floor_peak_post_step()), + ci_heal_revalidation_preflight_script(), + ], map(gunbc_ci_deploy_stages, s => gunbc_ci_deploy_invoke(stage: s)) ) } @@ -131,6 +134,17 @@ fn ci_exempt_fresh_verdict_count() -> Int { |> count() } +fn ci_heal_revalidation_preflight_is_fresh_effect() -> Bool { + let identity = ci_heal_revalidation_preflight_script() + let demand_count = ci_materialization_demands() + |> filter(d => d.identity == identity) + |> count() + let verdict_count = ci_materialization_verdicts() + |> filter(v => verdict_is_exempt_fresh(v: v, identity: identity)) + |> count() + demand_count > 1 && verdict_count == 1 +} + fn ci_uses_steps_skipped_count() -> Int { workflow_uses_steps_count(w: ci_workflow) } diff --git a/dag/gunbc/ci_runner_placement.dag b/dag/gunbc/ci_runner_placement.dag index 0cf7bb39e0c..b5ca1ae8459 100644 --- a/dag/gunbc/ci_runner_placement.dag +++ b/dag/gunbc/ci_runner_placement.dag @@ -17,7 +17,16 @@ import gunbc.fleet_host_budget { host_allocation_conserves, declared_runner_count, oomd_actuation_floor, + CompilePoolResolution, + CompilePoolDerived, + CompilePoolUnresolved, + CompilePoolPlacement, + CompilePoolSliceDeclared, + CompilePoolInRunnerSlots, + compile_pool_bytes_for, + gunbc_compile_pool_placement, } +import extdeps.jobserver { jobserver_config } import extdeps.cpu { cpu_facts_threads } import gunbc.ci_floor_measurement { HostFixedOverhead, @@ -180,26 +189,42 @@ fn accumulate_conservation_deployment(acc: DeploymentAccumulator, offer: Compute let host_ram = compute_host_ram_bytes_total(host: offer.supply.physical) let overhead = host_fixed_overhead_bytes(o: gunbc_ci_host_fixed_overhead) let headroom = oomd_actuation_floor(host: host_ram) - if host_allocation_conserves( + match compile_pool_bytes_for( + placement: gunbc_compile_pool_placement, + nproc: cpu_facts_threads(cpu: cpu), host: host_ram, - overhead: overhead, - headroom: headroom, - runner_pool: gunbc_runner_pool_budget, - session_pool: byte_size(0) + citation: jobserver_config ) { - let dep = conservation_host_deployment(offer: offer, nproc: cpu_facts_threads(cpu: cpu)) - DeploymentAccumulator { - deployments: concat(acc.deployments, [dep]), - unsound_reason: acc.unsound_reason, - any_unfit: acc.any_unfit || (dep.runner_count == 0), - total: acc.total + dep.runner_count, - } - } else { - DeploymentAccumulator { - deployments: acc.deployments, - unsound_reason: if acc.unsound_reason == "" { concat("host allocation over-committed (runner_pool + session_pool + overhead + headroom exceeds host RAM): ", offer.supply.physical.identity) } else { acc.unsound_reason }, - any_unfit: acc.any_unfit, - total: acc.total, + CompilePoolUnresolved { reason: r } => + DeploymentAccumulator { + deployments: acc.deployments, + unsound_reason: if acc.unsound_reason == "" { concat("compile pool unresolved: ", r) } else { acc.unsound_reason }, + any_unfit: acc.any_unfit, + total: acc.total, + } + CompilePoolDerived { bytes: compile_pool } => + if host_allocation_conserves( + host: host_ram, + overhead: overhead, + headroom: headroom, + runner_pool: gunbc_runner_pool_budget, + session_pool: byte_size(0), + compile_pool: compile_pool + ) { + let dep = conservation_host_deployment(offer: offer, nproc: cpu_facts_threads(cpu: cpu)) + DeploymentAccumulator { + deployments: concat(acc.deployments, [dep]), + unsound_reason: acc.unsound_reason, + any_unfit: acc.any_unfit || (dep.runner_count == 0), + total: acc.total + dep.runner_count, + } + } else { + DeploymentAccumulator { + deployments: acc.deployments, + unsound_reason: if acc.unsound_reason == "" { concat("host allocation over-committed (runner_pool + session_pool + compile_pool + overhead + headroom exceeds host RAM): ", offer.supply.physical.identity) } else { acc.unsound_reason }, + any_unfit: acc.any_unfit, + total: acc.total, + } } } } diff --git a/dag/gunbc/ci_spec.dag b/dag/gunbc/ci_spec.dag index 3bcdf1664d8..9005eb934ab 100644 --- a/dag/gunbc/ci_spec.dag +++ b/dag/gunbc/ci_spec.dag @@ -479,6 +479,24 @@ fn gunbc_ci_heal_regen_invoke() -> String { ) } +data ci_heal_dispatch_entry: String = "dag/tools/ci_heal_dispatch.dag" +data ci_heal_dispatch_function: String = "main" + +fn gunbc_ci_heal_dispatch_invoke() -> String { + concat( + "GUNBC_HEALED_HEAD=\"$HEALED_HEAD\" GUNBC_HEAL_BRANCH_REF=\"${{ github.head_ref || github.ref_name }}\" ", + gunbc_run_shell( + source_roots: witness_layer_roots, + entry: ci_heal_dispatch_entry, + function: ci_heal_dispatch_function, + rooted: true, + claim_run: false + ) + ) +} + +data ci_heal_dispatch_invoke_note: String = "After a successful push the heal invokes tools.ci_heal_dispatch, which binds the modeled github.Workflows.CreateDispatch operation. The moving branch ref and immutable HEALED_HEAD are distinct inputs. The dispatched run binds the branch resolution recorded in github.sha and its checkout head to that immutable input before any witness can count. The workflow RunStep projects github.token into its GITHUB_TOKEN environment because GitHub Actions does not inject the installation token as a process variable by default; CreateDispatch auth_source reads that env var. A transport refusal makes the gunbc invocation nonzero; the surrounding heal script classifies HealDispatchRefused, keeps revalidation required, and fails loudly. There is deliberately no fallback to the ordinary push event and no silent retry." + data ci_heal_binary_source_skew_note: String = "THE PAIRING THE HEAL JOB NEVER MODELED (incident 2026-07-29). A generated artifact's identity is (authority source x compiler), and this job silently pairs TWO DIFFERENT REVISIONS of that product: the release binary comes from `build`, which uses the default pull_request checkout — the MERGE ref (main + PR) — while this job checks out `github.head_ref`, the BRANCH HEAD, because it pushes the regeneration back (ci_heal_branch_checkout_note reasons carefully about the push semantics and never about the binary's provenance; it calls build's output 'the release gunbc binary', treating a projection of one revision as a context-free tool). The skew is latent until a seed change adds a Rust->.dag call: then the merge-ref binary invokes a `.dag` function the branch-head tree does not contain, `lookup_fn` misses, and the run dies mid-regen. Live receipt: #7348 landed the scoped-run observation seed, whose Rust calls gunbc.observation_seed_render.scoped_run_begin_write_measured; every open PR whose branch head predated it went red at once (#7392, #7394, #7395 — identical failure, none of them at fault), while main stayed green because ci_regen_heal_if skips this job on push. Reproduced by execution: ONE binary built from the merge ref, run against a pre-#7348 tree -> the exact CI failure; run against the same tree with origin/main merged in -> ExitSuccess, zero drift. The guard below refuses the unsound pairing instead of discovering it as a mid-regen crash." data ci_heal_binary_source_skew_guard_note: String = "THE COMPARAND IS THE BUILT-FROM REVISION, NOT A BRANCH NAME (operator ruling 2026-07-29). The first cut of this guard compared against `origin/main`, and that was wrong on the operator's own objection: `origin/main` names several different things — the one on this runner, the one on a developer's machine, and the actual source of truth — so a guard keyed on it asserts a relationship between the tree and WHICHEVER main this box last fetched, and can even race an advance mid-run. There is no ambiguity to resolve because the question was never about main: it is 'was this binary compiled from a revision this tree can satisfy'. `github.sha` answers it exactly — on a pull_request it IS the merge commit the build job checked out, so the guard compares the tree against the binary's own provenance, in the same workflow run, with no floating ref anywhere. The guard fails closed if that revision cannot be resolved: an unverifiable provenance is a refusal, never an assumed-fine.\n\nPRECISION IS ALSO THE POINT (DESIGN §5 — refuse, never widen; and never manufacture friction the defect does not warrant). A blanket 'tree must equal the built-from revision' would red nearly every PR to catch a rare pairing — a wall priced in other people's time. The DECIDABLE condition that actually matters is narrower: the binary is unsound against this tree exactly when the built-from revision carries SEED commits (src/v1 — the Rust the binary is compiled from) this tree lacks, because only those can introduce a Rust->.dag expectation the tree cannot satisfy. `git rev-list HEAD..$BUILT_FROM -- src/v1` is exactly that set: empty on the overwhelming majority of PRs (a .dag-only advance cannot skew a compiled binary), non-empty precisely in the #7348 shape. VERIFIED BY EXECUTION on the incident itself: at the pre-#7348 branch point the set is two commits and one of them IS 445bac31c (#7348), so the guard names the responsible commit in its own refusal; at current main and at the same branch after the advance was merged in, the set is empty and the guard is silent — the heal that went green on #7395 is the live receipt for the passing arm. On non-empty the step REFUSES rather than regenerating with a compiler that disagrees with the tree, which is the fabricated-plausible-output arm §5 forbids: main_wet would write whatever artifacts that mismatched pair produces and the commit-push step would push them to the branch. NOT IN SCOPE: preventing a stale branch from MERGING at all. That is merge-admission policy, it needs a source-of-truth model this repo does not have yet (operator: the SCM lane after v1 deletion), and this guard deliberately does not pretend to supply one — it protects the one pairing this job has agency over." @@ -489,7 +507,7 @@ fn gunbc_ci_heal_binary_source_skew_guard_script() -> String { ci_heal_binary_source_skew_guard_script() } -data ci_heal_commit_push_note: String = "The heal COMMIT+PUSH step: a GitHub Actions run: foreign-executor context (the shell-intent design's bootstrap/foreign-executor case that renders through bash), so the git commit/push is a hand-authored shell string like the deploy invoke and the release-bins unpack script beside it. COMMIT SCOPE IS ARTIFACT-ONLY BY CONSTRUCTION (review 41703, DESIGN §5 no-silent-widen): staging is `git add` over EXACTLY committed_generated_artifact_paths() (the registry the drift gate guards and the exact set main_wet writes) — never `git add -A`, so an incidental dirty file on the runner (checkout residue, line-ending normalization, leftover state) can never be swept into an unattended push. Each path is guarded by a `[ -e ]` existence test (ci_heal_git_add_lines): a registered artifact that is absent on this tree — e.g. one a later PR deleted while its registry row lingered (the #7121 FleetConvergeArtifact incident) — is loudly echoed and skipped, never an exit-128 `git add` pathspec error that fails the whole heal; the drift gate remains the wall for a registered-but-missing artifact, the heal just commits what main_wet actually produced. The commit fires ONLY when `git diff --cached --quiet` reports a staged change — i.e. only when main_wet actually healed a registry artifact — so a clean tree is a no-op, and the byte-idempotent fixed point makes the healed commit's own re-run a no-op that terminates the loop. The push target is the TRIGGERING branch, not hardcoded main: `git push origin HEAD:${{ github.head_ref || github.ref_name }}` resolves to the PR source branch on a pull_request and to main on a push (operator-directed 2026-07-23: heal pushes to PRs so drift is repaired on the PR branch before it reaches main). The push authenticates with the job's GITHUB_TOKEN (persisted by actions/checkout, which checked out that same branch head) and needs the job-scoped contents:write grant. Loop termination is NOT a re-run of CI on the healed commit: a push made with the workflow's GITHUB_TOKEN does NOT trigger a new workflow run (GitHub's cascade-prevention is by design), so the healed commit fires no follow-on run — termination is guaranteed instead by the byte-idempotent fixed point itself, PROVEN pre-merge by generated_artifact_drift_test.witness_committed_is_fixed_point (committed bytes == artifact_generate) with its RED control, so a second heal would be a no-op even if one were triggered (review 41782 correction: the earlier 'verified by execution on the healed commit' claim was wrong — no such run exists). The healed artifacts are re-validated by the batch-0 drift gate on the NEXT unrelated PR/push, not on the heal commit, which is correct-by-construction (it is main_wet's own output). A concurrent advance on the target branch makes the push a non-fast-forward that fails LOUDLY (never a force-push), and the next run on that branch retries. A fork PR's branch is not writable by the workflow GITHUB_TOKEN, so a fork heal push fails loudly rather than silently — fork heals are out of scope; the fleet's PR model is same-repo session branches, which are pushable." +data ci_heal_commit_push_note: String = "The heal COMMIT+PUSH+DISPATCH step creates and classifies a new subject. COMMIT SCOPE remains artifact-only: staging is exactly committed_generated_artifact_paths(), never git add -A, and missing registered paths are loudly skipped. Before mutation it captures PRIOR_HEAD; after the artifact-only commit it captures HEALED_HEAD and the changed artifact population, pushes non-force to the triggering PR branch, then explicitly invokes github.Workflows.CreateDispatch with the branch ref plus HEALED_HEAD as expected_healed_sha. The workflow model binds github.token into the step's GITHUB_TOKEN environment because the modeled service resolves its sole credential authority there and Actions does not export the token context automatically. A clean staged diff is HealNoChange and exits successfully. A mutation is HealProduced, makes the prior run SupersededByHealedHead, and the old run exits nonzero even after a successful dispatch: no result from tree A can authorize tree B. If push or dispatch fails, the job fails loudly; dispatch failure emits HealDispatchRefused and never assumes the GITHUB_TOKEN push produced an event. The dispatched workflow excludes this heal job, requires both github.sha (the GitHub run/check subject) and checkout HEAD to equal the immutable expected SHA before witnesses, and refuses either mismatch. The byte-idempotent fixed point still proves loop safety, but it is no longer used as a substitute for executing revalidation on the healed head." data ci_heal_commit_push_shell_emit_scaffold: Disposition = Scaffold { dissolves_to: SingleAuthority, @@ -516,18 +534,34 @@ fn ci_heal_shell_lines() -> List { concat( ci_heal_git_add_lines(), [ + "PRIOR_HEAD=$(git rev-parse HEAD)", "if git diff --cached --quiet; then", - " echo \"chore-heal: no generated-artifact drift on this tree; nothing to commit\"", + " echo \"HealNoChange head=$PRIOR_HEAD\"", + " exit 0", "else", + " CHANGED_ARTIFACTS=$(git diff --cached --name-only)", " git commit -m \"chore: regenerate drifted generated artifacts (ci auto-heal)\"", + " HEALED_HEAD=$(git rev-parse HEAD)", " git push origin HEAD:${{ github.head_ref || github.ref_name }}", - " echo \"chore-heal: pushed regenerated generated artifacts to the triggering branch\"", - "fi" + " echo \"HealProduced prior_head=$PRIOR_HEAD healed_head=$HEALED_HEAD changed_artifacts=$CHANGED_ARTIFACTS\"", + " ROOT=$(git rev-parse --show-toplevel)" ] ) ) } fn gunbc_ci_heal_commit_push_script() -> String { - fold(ci_heal_shell_lines(), init: "", f: fn(acc, line) { concat(acc, concat(line, "\n")) }) + concat( + fold(ci_heal_shell_lines(), init: "", f: fn(acc, line) { concat(acc, concat(line, "\n")) }), + concat( + concat(" if ! ", gunbc_ci_heal_dispatch_invoke()), + concat( + " then\n echo \"::error::HealDispatchRefused healed_head=$HEALED_HEAD cause=CreateWorkflowDispatchFailed\" >&2\n exit 1\n fi\n", + concat( + " echo \"SupersededByHealedHead prior_head=$PRIOR_HEAD healed_head=$HEALED_HEAD; revalidation-required\"\n", + " exit 1\nfi\n" + ) + ) + ) + ) } diff --git a/dag/gunbc/ci_workflow.dag b/dag/gunbc/ci_workflow.dag index 6d300c1e7d1..0bd16b876f1 100644 --- a/dag/gunbc/ci_workflow.dag +++ b/dag/gunbc/ci_workflow.dag @@ -17,6 +17,9 @@ import gunbc.ci_spec { import v2.workflow.ci_merge_admission_gate_emit { ci_merge_admission_gate_script, } +import v2.workflow.ci_heal_revalidation_preflight_emit { + ci_heal_revalidation_preflight_script, +} import gunbc.ci_workflow_expressions { ci_deploy_push_to_main_if, ci_regen_heal_if } import gunbc.commit_workflow { commit_gate_roster, project_ci_floor_gates, project_ci_floor_witness_entries } import gunbc.ci_runner_target { gunbc_ci_selected_runner_spec, ci_runner_target_ram_speed_budget, selected_ci_runner_target } @@ -42,6 +45,7 @@ import gunbc.repo_identity { gunbc_default_branch_name } import extdeps.github.actions { Workflow, Job, Step, RunStep, UsesStep, WorkflowTrigger, Push, PullRequest, WorkflowDispatch, MergeGroup, + DispatchInput, InputString, PullRequestActivity, Opened, Synchronize, Reopened, ReadyForReview, WorkflowPermissions, PermRead, PermWrite, ConcurrencySpec, ConcurrencyMappingQueueNotMax, CancelInProgressBool, CancelInProgressExpression, @@ -64,6 +68,38 @@ fn ci_checkout_step() -> Step { } } +fn ci_heal_revalidation_checkout_step() -> Step { + UsesStep { + name: Present { value: "Checkout" }, + id: none, + uses: checkout_action, + with: Present { + value: [ + kv(key: "fetch-depth", value: yaml_int(n: 0)), + kv(key: "ref", value: yaml_string(s: "${{ inputs.expected_healed_sha || github.sha }}")) + ] + }, + env: none, + if_condition: none, + continue_on_error: none, + timeout_minutes: none + } +} + +fn ci_heal_revalidation_preflight_step() -> Step { + RunStep { + name: Present { value: "Refuse a heal revalidation whose run subject or checkout does not name the expected healed SHA" }, + id: none, + run: ci_heal_revalidation_preflight_script(), + shell: none, + env: none, + working_directory: none, + if_condition: none, + continue_on_error: none, + timeout_minutes: none + } +} + data ci_heal_branch_ref_expr: String = "${{ github.head_ref || github.ref_name }}" data ci_heal_branch_checkout_note: String = "The generated-artifact heal checks out the PR SOURCE branch head (github.head_ref) rather than the default detached PR-merge ref, because the heal pushes a regeneration commit BACK to that branch. The heal runs on pull_request only (ci_regen_heal_if), so github.head_ref is always the PR source branch; the `|| github.ref_name` fallback is retained as a defensive no-op for that expression. push (main), merge_group, and workflow_dispatch are excluded by ci_regen_heal_if — main is not auto-healed (its ci drift gate reds instead) and a queue ref (gh-readonly-queue/*) is not a pushable branch." @@ -132,9 +168,10 @@ fn ci_native_cache_prelude_order_bridge() -> String { data ci_native_cache_prelude_order_bridge_note: String = "Adjacency-as-proxy ordering witness (not a structural Step-order predicate): ci_native_cache_prelude_order_bridge builds the setup-rust HOME env line immediately followed by the native-cache step name at the emitter's six-space yaml indent. witness_prelude_native_cache_root_step_follows_setup_rust asserts expected_ci_yml/falsifier_yml contain that substring. The real invariant is only that segment derivation runs after setup-rust-toolchain installs rustc; this check over-constrains to textual adjacency and can false-RED on a legitimate step inserted between setup-rust and native-cache, emitter yaml reformatting, or setup-rust gaining fields after HOME — but not false-green if ordering regresses. A hand-rolled match on Step would be the structural alternative; non-fold-residue forbids the wildcard arms that predicate would need (receipt: nfr floor on 14bc815db4). Dissolves when prelude ordering is witnessed without coproduct-residue match arms." -fn ci_prelude_steps() -> List { +fn ci_prelude_steps_with_checkout(checkout: Step, exact_head_preflight: List) -> List { [ - ci_checkout_step(), + checkout, + ] |> concat(exact_head_preflight) |> concat([ ci_isolate_toolchain_step(), UsesStep { name: Present { value: "Setup Rust" }, @@ -182,7 +219,18 @@ fn ci_prelude_steps() -> List { continue_on_error: none, timeout_minutes: none } - ] + ]) +} + +fn ci_prelude_steps() -> List { + ci_prelude_steps_with_checkout(checkout: ci_checkout_step(), exact_head_preflight: []) +} + +fn ci_heal_revalidation_prelude_steps() -> List { + ci_prelude_steps_with_checkout( + checkout: ci_heal_revalidation_checkout_step(), + exact_head_preflight: [ci_heal_revalidation_preflight_step()] + ) } data ci_artifact_consumer_prelude_note: String = "Checkout-only prelude for deploy jobs that consume the build job's release-bins artifact. Rust release-bin build + isolated rustup stay on build only — deploy verifies and runs prebuilt binaries." @@ -209,11 +257,11 @@ fn ci_floor_gate_toolchain_step() -> Step { } fn ci_artifact_consumer_prelude_steps() -> List { - [ci_checkout_step()] + [ci_heal_revalidation_checkout_step(), ci_heal_revalidation_preflight_step()] } fn ci_floor_job_prelude_steps() -> List { - [ci_checkout_step(), ci_floor_gate_toolchain_step()] + [ci_heal_revalidation_checkout_step(), ci_heal_revalidation_preflight_step(), ci_floor_gate_toolchain_step()] } fn ci_floor_spec_from_roster() -> CiSpec { @@ -251,7 +299,7 @@ fn ci_v1_compiler_tests_compile_gate_step() -> Step { working_directory: none, if_condition: none, continue_on_error: none, - timeout_minutes: Present { value: gunbc_ci_aux_step_timeout_minutes } + timeout_minutes: Present { value: gunbc_ci_v1_compiler_tests_compile_gate_timeout_minutes } } } @@ -444,6 +492,10 @@ data gunbc_ci_step_timeout_measure_grounding_disposition: Disposition = Scaffold data gunbc_ci_aux_step_timeout_minutes: Int = 5 +data gunbc_ci_v1_compiler_tests_compile_gate_timeout_minutes: Duration = 10 + +data gunbc_ci_v1_compiler_tests_compile_gate_timeout_note: String = "v1-compiler-tests compile gate step budget (operator ruling 2026-08-01, PR #7533 receipt): cargo check -p v1-compiler-tests --tests exceeded the 5m aux cap on cold native-cache hosts (run 30692671758 @ 6f20329: srv1-05, step killed at 5m00s while still compiling deps; warm-cache runs complete in ~50s). 10m is the fleet-pressure envelope for a cold compile-only gate after main growth — not RustMonolithGate re-enrollment." + data gunbc_ci_artifact_transfer_step_timeout_minutes: Int = 10 data gunbc_ci_artifact_transfer_step_note: String = "Cross-job handoff of the release bins (operator job-split ruling 2026-07-11): the build job packs ci_floor_required_artifact_names into one tgz (tar preserves the executable bits a raw artifact zip would drop) and uploads it; the ci floor job downloads, unpacks into target/release, and re-verifies via claim_executor --verify-build-artifacts before any step consumes a bin (fail-closed: a missing or truncated artifact reds the job at the verify, never as a mid-floor bin fallback). 10m is the transfer envelope for a ~200MB archive under fleet pressure; upload uses if-no-files-found: error so an empty pack is loud." @@ -457,9 +509,11 @@ data gunbc_ci_prelude_allowance_minutes: Int = 5 data gunbc_ci_release_build_step_timeout_minutes: Int = 45 data gunbc_ci_deploy_job_backstop_timeout_minutes: Int = 10 +data srv1_dashboard_deploy_concurrency_group: String = "srv1-dashboard-deploy" +data srv1_dashboard_deploy_serialization_note: String = "Blue/green selection is observe-then-mutate, so two deploy jobs may not overlap: both could observe blue active, then the first flips green while the second restarts what has become the active green slot, recreating the outage. Job-grain concurrency serializes the entire cutover with cancel-in-progress=false; QueueNotMax may replace an older pending deployment with the newest main revision, but it never interrupts the one holding the host mutation window." fn gunbc_ci_build_job_backstop_timeout_minutes() -> Int { - gunbc_ci_aux_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_release_build_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_artifact_transfer_step_timeout_minutes + gunbc_ci_prelude_allowance_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_v1_compiler_tests_compile_gate_timeout_minutes + gunbc_ci_release_build_step_timeout_minutes + gunbc_ci_aux_step_timeout_minutes + gunbc_ci_artifact_transfer_step_timeout_minutes + gunbc_ci_prelude_allowance_minutes } data gunbc_ci_receipt_gate_steps_moved_in_executor_note: String = "The resolve-receipt and materialization-receipt gate STEPS are deleted (ruling 2026-07-30): their laws — resolves_total equals the declared count, and the materialization receipt exists/parses/keyed nonzero — now run INSIDE claim_executor as ordinary-floor finalization (run_walk, FloorFinalization), so a receipt-law violation blocks every on-success stage instead of redding a step AFTER admission had already stamped. The two aux terms leave the backstop sum in the same motion, keeping it the exact step-sum + prelude the witness pins. The receipt FILES keep writing — they are observability the falsifier and operators read — only the shell re-validation of them is gone, because keeping it would be a second representation of the floor-completion rule." @@ -493,7 +547,13 @@ fn ci_deploy_job(stage: DeployStage) -> Job { if_condition: Present { value: ci_deploy_push_to_main_if }, timeout_minutes: Present { value: gunbc_ci_deploy_job_backstop_timeout_minutes }, continue_on_error: none, - concurrency: none, + concurrency: Present { + value: ConcurrencyMappingQueueNotMax { + group: srv1_dashboard_deploy_concurrency_group, + cancel_in_progress: Present { value: CancelInProgressBool { value: false } }, + explicit_single: none, + } + }, permissions: none } } @@ -540,7 +600,9 @@ fn ci_heal_commit_push_step() -> Step { id: none, run: gunbc_ci_heal_commit_push_script(), shell: none, - env: none, + env: Present { + value: [kv(key: "GITHUB_TOKEN", value: yaml_string(s: "${{ github.token }}"))] + }, working_directory: none, if_condition: none, continue_on_error: none, @@ -548,7 +610,7 @@ fn ci_heal_commit_push_step() -> Step { } } -data ci_heal_job_note: String = "CI auto-heal for the DERIVABLE red class (ci-chores lane, operator-directed 2026-07-23: 'i would regen push to PRs please — convenience in general, by the time it gets to main this should be green regardless'). A generated-artifact drift is a red whose remedy is a pure function of the tree — regenerate the projection from its .dag authority — so the pipeline heals it without a human. The job runs on pull_request ONLY (ci_regen_heal_if), needs only build (the release gunbc binary), and is deliberately independent of the ci job's verdict: the drift gate lives IN ci, so a heal gated on ci passing would be skipped exactly when it is needed. It checks out the PR SOURCE branch head (ci_heal_branch_head_checkout_step — github.head_ref), downloads+unpacks the release bins, runs main_wet to rewrite drifted artifacts, then commits+pushes the fix back to THAT branch ONLY if git diff shows a change — so a drifted PR is repaired on its own branch before it can reach main. main is deliberately NOT auto-healed (operator-directed 2026-07-23: 'i think we can disable main pushes for now — i don't expect this to be needed on main — we can just fail CI on main if regen somehow fails there'): main is protected by a ruleset requiring the 'ci' check, so a heal push to main is rejected (GH013, receipt 2026-07-23), and it is not wanted anyway — on main the ci job's own drift gate reds honestly if regen ever drifts, which is the correct main behavior since a change reaching main should already be green from its PR heal. contents:write is scoped to THIS job (least privilege, DESIGN §5): the workflow default stays contents:read, so the whole-corpus ci floor never runs with a push-capable token — only this regen+push job does. Fail-closed: main_wet stops on the first write failure, and the git add stages ONLY registered artifact paths that exist on the tree (ci_heal_git_add_lines — a registered-but-absent artifact, e.g. one deleted by a later PR while its registry row lingered, is loudly skipped and counted, never an exit-128 that fails the whole heal). The loop terminates by construction, NOT by a re-run — a GITHUB_TOKEN-pushed commit does not trigger a new workflow (GitHub's cascade-prevention), so there is no self-verifying second run; loop-safety rests on the byte-idempotent fixed point proven pre-merge by witness_committed_is_fixed_point (generated_artifact_drift_test.dag) with its RED control, so a heal that regenerated a still-drifting artifact would already have failed the drift gate before landing. KNOWN CAVEAT (drift gate still in ci floor): because the heal push does not re-trigger CI, the run that FIRST observes drift still shows the ci job's drift gate red on its pre-heal checkout even though the branch is now auto-fixed; the next push/run is green, and main relies on the PR having healed before merge. Making even that first run green requires moving drift from a ci-floor validation gate to construction (regen owns it), a separate merge-admission change. Judgment-class reds (producer bugs, roster staleness) are untouched — this job only re-derives what the registry already declares generatable, never a human decision. Same-repo session branches (the fleet's PR model) are pushable with the job's GITHUB_TOKEN; a fork PR's branch is not writable by GITHUB_TOKEN and the push would fail loudly (never silently), which is correct — fork heals are out of scope." +data ci_heal_job_note: String = "CI auto-heal for the DERIVABLE red class remains pull_request-only and independent of ci, because the drift gate that triggers it can stop ci's dependent witness population. When regeneration changes a registered artifact, the job pushes the new PR head and explicitly workflow_dispatches CI with expected_healed_sha. The old run is SupersededByHealedHead and deliberately remains non-success; the dispatched run requires both its GitHub check subject (github.sha) and checkout HEAD to equal the immutable SHA before any gate. A dispatch or subject refusal leaves revalidation required and fails loudly. contents:write and actions:write are scoped to this job: contents is needed for the repair push, and Actions write is the narrowest GitHub permission that can invoke CreateWorkflowDispatch (it can start workflows, so it is not granted to build/regen/ci or the workflow default). The dispatching step explicitly binds `${{ github.token }}` into GITHUB_TOKEN, the sole environment authority consumed by github.Workflows; GitHub Actions does not implicitly export that context value as a process variable. The heal job is excluded from workflow_dispatch, preventing a dispatch loop. Main is not auto-healed. Judgment-class reds and the separate compile timeout are untouched." fn ci_heal_generated_artifacts_job() -> Job { Job { @@ -571,7 +633,7 @@ fn ci_heal_generated_artifacts_job() -> Job { contents: Present { value: PermWrite }, pull_requests: none, issues: none, - actions: none, + actions: Present { value: PermWrite }, id_token: none } } @@ -583,7 +645,7 @@ fn ci_build_job() -> Job { id: "build", name: none, runner: gunbc_ci_selected_runner_spec(), - steps: concat(ci_prelude_steps(), [ci_fmt_gate_step(), ci_v1_compiler_tests_compile_gate_step(), ci_release_build_step(), ci_release_bins_pack_step(), ci_release_bins_upload_step()]), + steps: concat(ci_heal_revalidation_prelude_steps(), [ci_fmt_gate_step(), ci_v1_compiler_tests_compile_gate_step(), ci_release_build_step(), ci_release_bins_pack_step(), ci_release_bins_upload_step()]), needs: [], env: none, outputs: none, @@ -650,7 +712,17 @@ data ci_merge_group_trigger_note: String = "merge_group trigger: the merge queue data ci_workflow: Workflow = { name: "ci", on: [ - WorkflowDispatch { inputs: [] }, + WorkflowDispatch { + inputs: [ + DispatchInput { + name: "expected_healed_sha", + description: Present { value: "Immutable healed commit required by an auto-heal revalidation run" }, + required: false, + default: none, + type: InputString + } + ] + }, Push { branches: [gunbc_default_branch_name], paths: [] }, PullRequest { branches: [gunbc_default_branch_name], diff --git a/dag/gunbc/compile_diagnostic_census.dag b/dag/gunbc/compile_diagnostic_census.dag new file mode 100644 index 00000000000..108874de94e --- /dev/null +++ b/dag/gunbc/compile_diagnostic_census.dag @@ -0,0 +1,53 @@ +module gunbc.compile_diagnostic_census + +import std.types { String, NonEmptyStr, Bool, Int, List } +import std.disposition { Disposition, Scaffold, SingleAuthority } +import std.decl_ref { DeclarationRef, WholeDeclaration } + +data compile_diagnostic_census_charter_note: String = "THE PROBE OBSERVATION SURFACE for the v1 compile path (roadmap node ladder-probe-corpus; gap analysis sec 12 Stage 1a). The probe corpus needs to observe WHICH judgment a synthetic source provoked, not merely that something refused: the Stage-0 receipt vocabulary gunbc.guarantee_measurement carries RefusedTyped \{diagnostic_class, count} and AcceptedCounted \{diagnostic_class, count}, and neither is inhabitable from a Bool. Before this surface the only .dag-callable v1 compile was compile_dag_rust_emit_check, which counts diagnostics passing compile_clean_diagnostic_is_hard and returns false when that count is nonzero - so class identity, severity, and every advisory were discarded inside the host. Three things that costs, each measured on the tests this corpus migrates: a probe refusing for ANY hard reason (including a typo in the probe source itself) reads as the wall firing; demoting a landed wall from blocking to advisory turns its RED silently GREEN, because the filter IS the severity predicate; and a positive control cannot state zero-diagnostics-of-ANY-severity, which is the assertion codex review 45357 added after an advisory MethodExistenceUndecided passed unnoticed as a green control. MEASUREMENT ONLY: no arm here judges acceptance. The rows carry what the compile decided, severity included AS DATA, and callers filter." + +data compile_diagnostic_census_grounding_note: String = "SECTION 3 - this is a PROJECTION, not a second diagnostic vocabulary. diagnostic_class is exactly the class half of the host's compile_clean_diagnostic_histogram_key, whose total match over v1.std.core CompilerDiagnostic is the single authority for naming a variant; subject_name is that key's name half. Declaring the variants again here as a coproduct would fork the authority and drift the moment a variant lands, so the class arrives as a projected string and the compiler's own exhaustiveness is what keeps it total. blocking is likewise projected, not restated: the host reads it through the existing compile_clean_diagnostic_is_hard delegation to is_interpreter_blocking_diagnostic, so the severity POLICY has one home and this surface reports its verdict rather than re-deciding it." + +data compile_diagnostic_census_path_scope_note: String = "THE ONE PATH THIS SURFACE MEASURES, stated so a receipt cannot claim coverage it does not have (DESIGN 4b: a class's rung is the MINIMUM across its in-scope paths, and citing the strongest path while another stays silent is inflation). The census compiles a synthetic single-module source through the v1 pipeline to the Rust render target - the same compile path compile_dag_rust_emit_check takes - and reports the diagnostics that compile produced. In GuaranteePath axes that is subject_grain SyntheticProgram and acceptance_boundary CompileAccept. The pipeline is v1, and that is stated here as a fact about this surface rather than as a path axis: compile_mode was deleted from the path row (gunbc#7572, review 46308) because every landed control derives its pipeline from its boundary, so storing it was dual representation. If a path is ever measured under two pipelines at ONE boundary, that is the trigger returning the axis as a stored fact - not a reason to keep a copy of it here meanwhile. It observes NOTHING about the interpreter's disposition of the same program, nothing about other emission targets, and nothing about corpus-grain prevalence. A probe row keyed to a path this surface does not measure must not carry a receipt derived from it." + +data compile_diagnostic_census_not_runnable_note: String = "CensusNotRunnable is a separate arm and NOT an empty row list, because an empty census is byte-identical to a clean compile (DESIGN 5: top-as-answer conflated with top-as-ignorance is the absorbing fallback). Could-not-measure must never be readable as the subject passing: the probe layer maps this arm to guarantee_measurement ProbeObservation ProbeNotRunnable, whose observed_is_verdict is false, and it can do so without guessing precisely because the separation is preserved here at the host seam rather than reconstructed downstream. An infrastructural failure of the compile itself - a harness error, an unreadable module index, a panic caught at the boundary - lands here with its cause, never as a green census." + +data compile_diagnostic_census_hand_rust_dissolve_trigger: String = "dissolve-on for the HAND-RUST OBSERVATION SURFACE behind this vocabulary - cli_run compile_dag_diagnostic_census (the host census fold) and v1_interpreter compile_diagnostic_census_value (the substrate projection), together with the v1.compiler.infer_method builtin_function_registry row that types it. The registry row's own trigger is its sibling's, the PrimitiveDefinition identity join; THIS row covers the Rust surface itself, which that trigger does not reach. TRIGGER, one and checkable: the roadmap row v1-zero-hand-maintained-rust, the v1-exit lane's fourth finish line. Its acceptance condition is that no hand-maintained Rust remains in the seed, so this surface is gone when that row is accepted and still owed while it is not - a state readable from the roadmap authority rather than from anyone's judgment about how far v2 has got. An earlier cut of this row offered two open-ended alternatives instead - v2 self-hosting the pipeline 'far enough' that a probe could read modeled diagnostics, or a floor-enrolled diagnostic lens subsuming the projection - and named no row; neither was evaluable, and a trigger nobody can evaluate is indistinguishable from no trigger, which is exactly the untracked-stall shape DESIGN 4b forbids (codex review 46264). EARLIER REMOVAL IS PERMITTED AND EXPECTED, but it is not the trigger: if a diagnostic lens subsumes this projection, or v2 self-hosts the pipeline far enough to read modeled diagnostics directly, this surface should go then. A scaffold may always dissolve early; what a trigger fixes is the point by which it MUST be gone, and stating the early paths as alternatives was what made the obligation unfalsifiable. It does NOT dissolve merely because the registry is replaced: the projection is the scaffold, the registry row is only its type declaration. WHY IT IS A SCAFFOLD AND NOT A PERMANENT SURFACE: everything it computes already exists inside the compiler - compile_clean_diagnostic_histogram_key names the variant and compile_clean_diagnostic_is_hard decides severity - so this adds NO judgment, only a substrate-visible projection of judgments the compiler already made. That is precisely the shape that dissolves when the substrate can read them directly. Receipt while it stands: the surface has exactly one class of consumer, the guarantee probe corpus, and its executing evidence is dag/test/claim/compile_diagnostic_census_witness_test.dag." + +data compile_diagnostic_census_hand_rust_scaffold: Disposition = Scaffold { + dissolves_to: SingleAuthority, + bind: DeclarationRef { + module_path: "gunbc.compile_diagnostic_census", + decl_name: "compile_diagnostic_census_hand_rust_dissolve_trigger", + field: WholeDeclaration + } +} + +type CompileDiagnosticCensusRow { + diagnostic_class: NonEmptyStr + subject_name: String + blocking: Bool + count: Int +} + +type CompileDiagnosticCensus + = CensusObserved { rows: List } + | CensusNotRunnable { cause: NonEmptyStr } + +data census_projection_note: String = "Pure reads over ROWS, never over the census. The selectors take List, so the only way to obtain rows is to MATCH the coproduct - which makes reading a row list out of a CensusNotRunnable structurally unwritable rather than warned against. An earlier cut exported census_rows(c: CompileDiagnosticCensus) answering [] on the NotRunnable arm, with a prose caution telling callers not to read a zero out of it. That was the fail-open this module exists to prevent, one layer up and in its own API: could-not-measure and observed-nothing became the same empty list at the export boundary, and a prose warning is validation standing exactly where construction was available (DESIGN 5; caught by codex review 46144). The caution deleted WITH the accessor rather than surviving beside it - a warning about a state that can no longer be written is dead text. None of these decides acceptance: blocking and advisory partition the SAME rows by the severity the host already reported, and census_total_count sums counts rather than rows so a class occurring twice is two occurrences." + +fn census_blocking_rows(rows: List) -> List { + rows |> filter(r => r.blocking) +} + +fn census_advisory_rows(rows: List) -> List { + rows |> filter(r => !r.blocking) +} + +fn census_rows_of_class(rows: List, wanted: String) -> List { + rows |> filter(r => (r.diagnostic_class as String) == wanted) +} + +fn census_total_count(rows: List) -> Int { + fold(rows, init: 0, f: (acc, r) => acc + r.count) +} diff --git a/dag/gunbc/design/scale.dag b/dag/gunbc/design/scale.dag index d624dc59423..8c68ce2d180 100644 --- a/dag/gunbc/design/scale.dag +++ b/dag/gunbc/design/scale.dag @@ -103,13 +103,10 @@ data font_mono: ScaleToken = font_token(name: "font-mono", families: [ data font_stack: List = [font_sans, font_mono] data radius_3: ScaleToken = length_token(prefix: "radius", px: 3) -data radius_5: ScaleToken = length_token(prefix: "radius", px: 5) data radius_999: ScaleToken = length_token(prefix: "radius", px: 999) data border_1: ScaleToken = length_token(prefix: "border", px: 1) data border_2: ScaleToken = length_token(prefix: "border", px: 2) data measure_page: ScaleToken = named_length_token(name: "measure", px: 960) -data height_mark: ScaleToken = named_length_token(name: "h-mark", px: 24) -data height_bar: ScaleToken = named_length_token(name: "h-bar", px: 10) data height_control_note: String = "The tactile target height (remodel W1c, operator archetype 2026-07-24: the GitHub merge button — a control you press with confidence needs the hand-scale target, ~40px class, not a text-line strip). This token plus radius-999 (the oblong/pill radius) are the first two rows of the control-geometry axis; the axis stays token-grain until a SECOND instrument lands (the archetype library's own second-consumer discipline), at which point the rows lift into a typed ControlGeometry carrier." @@ -119,7 +116,6 @@ data height_row_note: String = "The roadmap row's density token (composition sli data height_row: ScaleToken = named_length_token(name: "h-row", px: 24) data width_status: ScaleToken = named_length_token(name: "w-status", px: 52) -data width_brick_state: ScaleToken = named_length_token(name: "w-brick-state", px: 62) data travel_token_note: String = "A control's TRAVEL is a dimension, so it belongs on the dimension axis with every other length rather than as a literal inside a transform value — otherwise the first raw pixel in the register arrives through the one property nobody thought to look at, and the untokened-length census (which keys on 'px' appearing in a value) would flag it exactly as it flags a stray padding. Two tokens rather than one signed magnitude used twice: travel-lift is NEGATIVE because rising is negative Y in the coordinate system CSS actually has, and hiding that sign inside a calc() at each use site would put arithmetic in the value string where a token belongs. 1 px is deliberately the whole vocabulary — a chip-sized control has a chip-sized throw, and the register earns nothing from a travel-2 with no consumer." @@ -127,8 +123,8 @@ data travel_lift: ScaleToken = named_length_token(name: "travel-lift", px: -1) data travel_press: ScaleToken = named_length_token(name: "travel-press", px: 1) data dimension_scale: List = [ - radius_3, radius_5, radius_999, border_1, border_2, measure_page, - height_mark, height_bar, height_control, height_row, width_status, width_brick_state, + radius_3, radius_999, border_1, border_2, measure_page, + height_control, height_row, width_status, travel_lift, travel_press, ] diff --git a/dag/gunbc/design_document.dag b/dag/gunbc/design_document.dag index a2523802cc2..3cfa97cb3f9 100644 --- a/dag/gunbc/design_document.dag +++ b/dag/gunbc/design_document.dag @@ -105,6 +105,7 @@ fn section_5_blocks() -> List { [ h2(text: "5. Fail-closed (§1's safety axis — harm reduction)"), p(text: "Minimizing cost and complexity (§2–§4) is worthless if a wrong thing passes silently — this is §1's safety axis made concrete. This code is digital: a wrong answer is a **loud error, never a warning** — a bridge collapses, it does not warn. Every path succeeds fully or fails with a typed, located diagnostic; no fabricated plausible output (a bounded \"forever\" ≠ an \"unknown\" error). Relax toward application-layer leniency only under protest, and lean to infra so others can build on your work. Stronger than *catching* a wrong state is making it **unwritable** — **correctness by construction, not validation.** A check that re-states a constraint the model already carries is a *second representation* of it (§2/§3): so prefer a single authority from which the realization is *derived* — the bad state cannot be written — over a check that flags it after the fact, which concedes the bad state *is* writable. The tell that a check was validation standing where construction was available: it can be satisfied by editing the *declaration* while the realization still lies (a key-completeness check went green when the spec was edited while the realizer kept faking the cache key). Reserve post-hoc checks for the genuinely **unstructurable** residue (§6 complexity / necessity — you cannot structurally forbid an *unnecessary* loop). Construction makes a class unwritable only when membership is **decidable**, so every class is one of three: a *wall now* (decidable and grounded — nicknaming, dead scaffolds, effect leaks); a *wall after grounding* (decidable but waiting on its single authority — the cross-representation `==` straddle is one only once `Int = GroupCompletion` grounds it); or a *ratchet forever* (undecidable — optimality, by Rice, never reaches \"never\"). The word **\"never\" is the trap**: it lets a ratchet masquerade as a wall, so check decidability before claiming one — the undecidable residue is the §6 lens, honestly and permanently. The deepest trap is **specification-without-execution**: a typecheck and a `.contains()` grep are *not* consumers — \"done\" means a real consumer **green by execution** plus a discriminating input that goes *red* when the behavior is wrong. (For the LLM agent: fluent, type-checking, grep-passing output is precisely the artifact that looks finished without running. Treat your own output as unverified until a consumer runs it green.)"), + p(text: "The same demand for an independent referent governs a test's oracle. **A merge-blocking test may compare a live repository population to a numeric literal only when that literal is grounded in a controlled fixture, external/versioned authority, explicit policy budget, or a monotone debt contract over a closed subject universe. A measurement copied from the same current tree is not an oracle. Completeness is an identity join, not a count equality.** A controlled fixture independently authors the input and expected population (for example, one planted duplicate and an assertion of exactly one duplicate). An external or versioned authority names its source — such as a cited protocol arity, ABI field count, or content-addressed prior-release baseline; measured current main is not such an authority. A policy budget states the resource cap it enforces. A monotone debt contract is legitimate only when the subject universe is independently discovered and closed, membership is checked at identity grain rather than by count, the roadmap justifies the direction, and every removal or exception carries a typed disposition. Without all four conditions, a one-sided ratchet over hand-authored rows is only a quieter snapshot: it counts the subjects someone remembered to author. Review tell: if automating the literal's update collapses the assertion to `measure() == measure()`, the manual update was the test's entire content — a change detector, not a check. This rule rejects tree-copied census pins while preserving exact-count fixture tests."), p(text: "A third named trap, the subtlest because it wears this section's own name: **the absorbing fallback — degradation is disguised fail-open.** When a mechanism cannot compute its precise answer, the tempting failure arm substitutes the *superset* instead: can't compute the affected set → rerun the entire suite; cache key uncertain → scan all keys; provenance unavailable → always run. Nothing is *missed*, so the arm gets labeled fail-closed — but it is the fabricated plausible output one level up: **⊤-as-answer conflated with ⊤-as-ignorance** (state-space conflation on the answer lattice — \"everything is affected\" and \"I could not compute what is affected\" are different states whose remedies differ), and it fails open twice. On safety: absorption destroys the only signal that the precise mechanism has a deficit — the failure's frequency is zero *by construction*, so the deficit never ranks for fixing (§6 prices by displaced cost, and a masked cost displaces nothing) and the anemia compounds (the scan-all-keys heuristic in the receipt below was this exact absorption; removing it exposed 8 deficits its silence had hidden). On cost: the fallback is denominated in the *corpus*, not the *change*, so it grows with the repo until the *budget* breaks instead of the build — later, loudly, at interest: §1's safety debt paid in §1's cost currency. And the confidence **threshold** that selects such an arm is a smuggled heuristic — §4 already rules a heuristic never necessary in a closed system — so the threshold's very existence *locates* the anemic modeling it papers over: a solution that must keep degrading to stay green is not a solution being cautious, it is a model being wrong. The rule, and the review tell: **a failure arm must refuse, never widen** — every degradation a typed, located, *countable* diagnostic, so its frequency is observable and prioritizable, never an absorbed rerun. Two neighbors are not this pattern (check the *trigger*: structure vs failure-state): a structural over-approximation computed *as* the answer (a dependency-closure superset is the model's precision frontier, not an absorption), and a deliberate interim fallback that is loud, budget-bounded, and lands with its §6 dissolution trigger. Silent unbounded absorption is never the third. A corollary on the refusal itself: **no escape hatches** — a toggle whose only effect is \"proceed as if the refusal had not fired\" (an env var, a flag, a silence mode) re-opens the arm §5 just closed, one config edit from silent (operator ruling, 2026-07-05). The operative discipline is the **factory model — and it is a merge requirement, not a preference** (operator ruling, 2026-07-05): a deficit stops the line (typed, located refusal); the stopped line is analyzed before it restarts; the only sanctioned second mode is a *stopped-line audit* that replays the run to ledger every deficit (typed, located, counted) for that analysis — it reports, it does not green. Diagnostic modes read state; no mode writes \"keep going.\" **Review bar:** a diff that lands a non-fail-closed failure arm — a silent widen, a fabricated default, an uncounted degradation, an escape hatch — is a **hard reject** in code review, regardless of what else the PR delivers. The reviewer's three questions: does the line stop? is the stop typed and located? does analysis precede restart?"), p(text: "The same arm exists at authoring time: **the workaround — an absorbing fallback executed by the author** (operator ruling, 2026-07-24). When the obstacle is the substrate itself — a parse error you don't understand, a check that won't green, an interface that refuses — the tempting move is to route around it: a different spelling, a dodged codepath, a \"for now\". The concealment is identical to the runtime absorption above: the deficit's frequency is zeroed (nothing typed, nothing counted, nothing ranks for fixing) — except the concealed deficit is usually in the *language layer*, precisely the class §6 says to root-cause first. So the hard rule, in repo and in review: **noticing you are implementing a workaround IS the line-stop signal** — back up, reassess the problem, root-cause it or flag for help; the only two landing states are the real fix, or a declared scaffold (loud, counted, named dissolution trigger — §6). A workaround is exactly an *unmarked* scaffold, and the marking is the entire difference. Review tell: \"workaround\" / \"for now\" / \"dodge\" in a diff or PR body with no dissolution trigger beside it. Receipt: the `$\{…}`-in-strings dodge (2026-07-24) — a bare-`$` respelling shipped while the language already had the `\\\{` escape AND two real parser defects sat beneath the confusing error (a context-free diagnostic and a silent fail-open fallback arm); stopping the line surfaced all three within the hour."), ul(items: [ @@ -164,10 +165,10 @@ fn open_threads_blocks() -> List { li(text: "**machine-shape orthogonal scheduling — one realization kernel over faithfully modeled machine facts, with locality (§2).** DRAFT for review; model-before-implement, each phase names its trigger. Its authority transfers to the std/extdeps carriers it names when Phase 1 lands, at which point this md dissolves into a registered `gunbc.plan.Plan` row (§6). → [machine-shape scheduling design](docs/plans/machine-shape-orthogonal-scheduling.md)"), li(text: "**namespace-only name resolution — the containment tree is the single naming authority (operator-signed 2026-07-06).** Supersedes resolver-graph-major §1c's *import-name-universe* (\"own declarations ∪ direct import lists\" visibility), carrying forward its mechanics unchanged (binding-edge owner, unbound/double-bound = error, patterns-via-scrutinee, no expected-type picker). One structure — **syntactic containment** — induces everything at once: qualified name = nesting position (`T\{a\}` → `a` *is* `T.a`; `T\{a\{a\}\}` → inner *is* `T.a.a`, so Rule-2 no-ambiguity holds by construction), reference = lexical lookup up the ancestor chain (a sibling module is visible, its members projected `node.Symbol`), `.` = projection one level down (field / module-member / variant — one op), and `.` *is* the `std.induction` sub-value relation (descent evidence). So one content-addressed tree grounds THREE consumers — resolution *walks* it, content-addressing *hashes* it, termination reads its *sub-value* edges (§2 Realization: one structure, N consumers). Frontend BUILDS the tree from nesting (pure structure, zero resolution logic); backend WALKS it (lexical up, project down) — no heuristics to tune (the tuning risk = resolving against a flat index instead of the tree). Two governing rules (operator, 2026-07-06): **no dual representation / minimal at every layer; no ambiguity at any layer.** Rides on `SymbolIndex` = \"the containment tree materialized\" (`type-env-single-authority`, lively-raven lane) — the loyal-heron scaling gate that formerly blocked resolver surgery was **discharged at #6809** (per §7.5 of the design doc: \"the lane is no longer blocked on it\"); `import-scoped` (§1c, today) and `namespace-only-Y` (position-info uniqueness: expected type filters a variant to one, never picks) are two walk-rules over one *fill-agnostic* index (fill = whole tree; policy gates lookup, never fill). Census: 98% of names globally unique → always bare; the residue is v1-seed forks (resolve by subtree, free) + github-style variants ((Y) context-resolves); no consolidate-now forks (the census's 2 same-name flags proved to be a homonym + whole-file v1-seed duplication, not genuine cross-tree §3 forks). Terminal step: delete the `import` grammar + supporting code → `import` becomes a *parse error*, deps derived from `container.member` references (Rule-1 end-state). → [namespace-only resolution design](docs/plans/namespace-resolution-design.md)"), li(text: "**fleet-reconcile spine — one grain-agnostic membership diff (deploy · fleet · session).** ONE `membership_reconcile` reuses `std.change.keyed_two_way_diff` verbatim (no fork): desired set vs observed set keyed by member IDENTITY — Added/Modified → upsert, Removed → teardown-or-refuse, Unchanged → noop (absence of a hunk). A member may be a service/unit/host OR a Session (operator: 'it should all be the same'), so a new member type is a new instantiation of the same fn with its own `(key_of, key_eq, value_eq, ownership_of)` bundle — zero spine change (a Realization, §2; a forked reconcile means the genericity bought nothing). R5 teardown-owned-only is a CONSTRUCTION WALL: a Removed non-owned member yields `MemberTeardownRefused`, which has no effect arm in any apply dispatch, so the bad state is unwritable — never a post-check, never 'assume owned' (the absorbing fallback §5 forbids); ownership-unknown REFUSES too, typed/located/counted. Ownership is extracted as its own single authority (`gunbc.ownership.Ownership`) and `live_deploy`'s `DeploymentStep` re-grounds onto it (pass 2, a declared dissolution trigger — never a second `Owned|Ensured`). Pass 1 (spine + R5 + a synthetic discriminating witness the degenerate apply/retract poles cannot exercise) landed. → [membership-diff reconcile spine design](docs/plans/membership-diff-reconcile-spine-design.md)"), - li(text: "**node/subtree visibility grants — two verbs (`Reference`, `Publish`) in the same grant algebra, not a parallel mechanism.** `Reference` gates whether a projection/reference edge may be formed within one compiled program; `Publish` gates whether source appears in an audience storage realization. Both reuse `Grant`/`admit_effect_go`; `AudienceScopeTree` converges the existing cache visibility scope, while concrete `std.realization.Placement` remains a different redundant-computation axis. One graph has N storage projections: a private repo is not a second graph. Rule 1 makes publication visibility monotone along references; Rule 2 makes privacy downward-closed on containment, nearest ancestor winning. A World projection renders private content absent by default, never a churn/name-leaking stub; commitment stubs are later opt-in. `Reference` keeps today's permissive `LegacyOpen` frontier, while new undeclared `Publish` content defaults to the narrowest root; Stage 0 bulk-stamps today's public corpus before the guard activates. Stage 0 is file-grain/two Git roots/push guard; Stage 1 checks the composed graph; Stage 2 rides the module-storage binding. A public-only guard reports only local-marker/dangling-reference coverage and never impersonates the composed-root closure. The locked extension is a **publication capability product, not a ladder**: source/interface disclosure, artifact delivery, local/remote execution, integrity, reproducibility, existence/churn disclosure, revocation/erase, and placement/trust are independent policy axes. An emitted artifact is not source disclosure but remains inspectable; client-held key means plaintext access, distinct from remote/attested key custody. Locked blobs use AEAD with subject/interface/version/audience/policy context bound as associated data plus confidentiality-and-integrity-preserving per-audience key wraps; wrong key/context/authentication is typed refusal. Per-statement holes remain refused; interface/realization is the cut; size padding and `Secret`/`Pii`→`SecretRef` construction constraints remain. Commitments never promise zero leakage: length, timing, interface shape, wrap count, access pattern, low-entropy guessing, and churn stay in a named threat model. Remote execution and SCM diagnostics obey information-flow/noninterference + explicit declassification; metering prices an oracle but does not secure it, and unauthorized audiences receive only a non-leaking diagnostic projection. Cryptographic erase is conditional on every key/wrap/backup/cache/derivative/recovery path being sanitized and the result validated, never “delete one key.” → [node/subtree visibility grants design](docs/plans/node-subtree-visibility-grants.md)"), + li(text: "**node/subtree visibility grants — two verbs (`Reference`, `Publish`) in the same grant algebra, not a parallel mechanism.** `Reference` gates whether a projection/reference edge may be formed within one compiled program; `Publish` gates whether source appears in an audience storage realization. Both reuse `Grant`/`admit_effect_go`; `AudienceScopeTree` converges the existing cache visibility scope, while concrete `std.realization.Placement` remains a different redundant-computation axis. One graph has N storage projections: a private repo is not a second graph. Rule 1 makes publication visibility monotone along references; Rule 2 makes privacy downward-closed on containment, nearest ancestor winning. A World projection renders private content absent by default, never a churn/name-leaking stub; commitment stubs are later opt-in. `Reference` keeps today's permissive `LegacyOpen` frontier, while new undeclared `Publish` content defaults to the narrowest root. **Stage 0 per-file placement gate DELETED (operator ruling 2026-08-01):** by operator ruling 2026-08-01: its constant `Publish World` rows duplicated Git's post-cutover path population, four same-day repair rounds raced identical reconstructions and repeatedly redded fleet CI, ordinary modifications were invisible, and CI after a public push was main-admission rather than public-storage admission. New public files require no stamp; closed PR #7588 retains the subtree-default experiment as a re-land seed. Publication enforcement returns only at P-C when private/public composition creates content to guard: a keyed subtree-prefix → disposition `PublicationPolicy` defaults `WithholdPrivate`, refuses unknown roots, and resolves most-specific rule; duplicate-identical rules are idempotent or construction-refused; exceptional decisions bind content identity; a `PublicProjectionPlan` is derived change-locally and consumed only by the sole modeled publisher holding the only public-write capability; a separate whole-projection audit is centrally owned and the pre-publication boundary is a server-side required check. The locked extension is a **publication capability product, not a ladder**: source/interface disclosure, artifact delivery, local/remote execution, integrity, reproducibility, existence/churn disclosure, revocation/erase, and placement/trust are independent policy axes. An emitted artifact is not source disclosure but remains inspectable; client-held key means plaintext access, distinct from remote/attested key custody. Locked blobs use AEAD with subject/interface/version/audience/policy context bound as associated data plus confidentiality-and-integrity-preserving per-audience key wraps; wrong key/context/authentication is typed refusal. Per-statement holes remain refused; interface/realization is the cut; size padding and `Secret`/`Pii`→`SecretRef` construction constraints remain. Commitments never promise zero leakage: length, timing, interface shape, wrap count, access pattern, low-entropy guessing, and churn stay in a named threat model. Remote execution and SCM diagnostics obey information-flow/noninterference + explicit declassification; metering prices an oracle but does not secure it, and unauthorized audiences receive only a non-leaking diagnostic projection. Cryptographic erase is conditional on every key/wrap/backup/cache/derivative/recovery path being sanitized and the result validated, never “delete one key.” → [node/subtree visibility grants design](docs/plans/node-subtree-visibility-grants.md)"), li(text: "**effect grants over namespaces — dissolve `Hermetic | Wet` into (frame × verb × subtree).** The 2-valued `Hermetic | Wet` enum conflates four axes (input closure / replayability, output reach / interference, handler binding, selection eligibility) — a §3 state-space conflation with proto-envelopes already forked (the hand-rolled `workspace_root` containment gate, `std.resources.ResourceHandle`, `AuthScope`, `LiveTreeDisposition`). End shape (§3, cited not minted): an effect target is a position in a containment tree that already exists — filesystem paths, URIs, `/proc`, service-operation paths, and the unifying case **code names themselves** (the containment tree the namespace-resolution lane makes the single naming authority); permission is a grant of (verb × subtree) attached to a `Frame`; admissibility is the namespace prefix relation — the same `⊑` the resolver walks, content-addressing hashes, and termination reads, so effects become the **fourth consumer** of the one containment structure. Dispatch checks the envelope fail-closed (`EffectOutsideGrant`, a typed/located/counted refusal, never a silent widen — §5); *replayable* / *isolated* / `LiveTreeDisposition` become **derived** projections, and `Hermetic | Wet | Record` survive only as named envelope presets, deleted at the end. Frame-containment is graded on the §5 construction/validation axis (`LifecycleByConstruction` vs `LifecycleByConvention`). No code lands from the design doc; the FLAG-A interim is the only near-term consumer. → [effect-namespace-grants design](docs/plans/effect-namespace-grants.md)"), li(text: "**shell → intent: the intent layer is language-blind (operator-aligned 2026-07-17; census/routing consolidated onto sibling authorities 2026-07-19).** §3/§4 restated for the shell case: the `.dag` intent may not name a target language — a workflow is an ordinary dependency graph over modeled operations (§4: a program is `Node`+`Edge`); *the intent IS the `.dag` graph* (operator), and `src/v2/std/orchestration.dag` `Pipeline` is optional sugar, never the authority. Two distinct downstream layers, not to be conflated: **emit** (§4 — `emit(intent, Bash)`, one grammar read backward, target as a *parameter*) and **realization** (§2 — pure-spec → host-effect, N transports `LocalShell`/`SshShell`, `dag/gunbc/host_effect_realize.dag`). **Routing (the load-bearing correction, operator review 2026-07-19): most sites do NOT emit bash.** Runtime-present sites (a gunbc runtime / typed transport is on the path) realize as typed service ops / typed transports / binary-interpreted effect-plan via `host_effect_apply` — no bash emitted (precedent already in tree: `floor_diff_observe` calls `git.Core.DiffUnified0` directly; `host_identity_converge` funnels through `host_effect_apply`); only foreign executors (GHA `run:`, cron, git hooks) and pre-runtime bootstrap windows render through the bounded v2 bash grammar rows. The authority for that routing is `authorize_shell_emission` (provisioning-window lane); the per-site residual census + arc-completion scoping is `shell-to-dag-residual-census-and-arc-completion.md` (witty-ibex-317) — DESIGN carries no parallel copy of either. **Invariant (enforceable):** the intent imports no language-construction vocab — emit-internal; `src/v2/lens/realization_vocabulary_containment.dag` confines it (LANDED #6854). The lens confines by MODULE, and naming what it actually reads is the whole point of §3's cite-the-symbol rule: `target_ast_vocab_module_prefixes` = `v2.extdeps.languages.bash`, `v2.workflow.bash`, plus `target_ast_vocab_modules` = `extdeps.languages.typescript.program`. The bash vocabulary is `v2.extdeps.languages.bash_build`'s `bash_build_*` constructors and the `v2.workflow.bash_command_fold_serialize` backend. *(Corrected 2026-07-31: this sentence previously listed the confined vocab as ``bash_build``, ``bash_command_fold_serialize``, ``the ShellStmt/ShellWord/ShellProgram coproduct``, ``serialize_bash``. The first two are real modules; the last two NAME NOTHING — the bash AST is built as generic `Node` (every `bash_build_*` fn returns `Node`), there is no ShellStmt/ShellWord/ShellProgram coproduct, and no `serialize_bash` is defined anywhere in the tree. Both were dissolved by the typed-WitnessBin migration (Phase D, 2026-07-14), which the `dag/tools/*` transport notes record and this row did not follow. The WALL WAS NEVER AFFECTED — the lens confines module prefixes, not these names, so nothing was unguarded; what rotted was the description of it, in the sentence declaring the invariant. That is the §3 positional/symbolic citation class exactly: a name-level claim in the canonical authority that a grep would have decided, sitting false while the mechanism it describes stayed correct. Found by a corpus citation sweep, not by anyone touching this row.)* Meta-exec confinement is **PARTIAL**: Wave A1 (#6900) module-isolated `shell.Exec.Run/.Check` and the import wall now has an empty exception roster; the still-open construction half is symbol/reference-level visibility that prevents allowed realization-edge modules from forming arbitrary transport values. *(Corrected 2026-07-26: this read \"`host_language_transport_script` lens inert, `fail_closed_lockdown.dag`\". That lens is **LIVE** — activated by #7184 as a per-PR `ReadsLiveTree` consumer that reds a new raw literal at an enrolled `Run` position — and the transport-script hole it was cited for is **closed by construction on the `ShellOnHost` route only**: `ShellOnHost.script` is the `RetainedShellScript` record and the free minter is deleted — while `shell.Exec.Run` itself still declares a transparent `TransportScript` brand over `String`, so a direct `as TransportScript` cast stays writable from any module and that sink is lens-guarded, not construction-walled (corrected 2026-07-27, `review 43600`; closing it is the meta-exec confinement milestone). The lens stays deliberately green on `ComputedApplication`, so it backstops literal blobs rather than being the wall. What remains open is the construction confinement named above, not the already-landed module-import wall.)* **Carriers → `Do\{effect\}` (never a command string, host-effect-orchestration.md):** the anemic `String` leaves are `host_effect.ShellCommand\{script\}`, `std.orchestration.Run.command`, and `host_effect.BootstrapFragment.script` — add-replacement → migrate all consumers → delete at arc close (§5 construction; `host_effect.dag` is load-bearing, the delete escalated). **Phases:** 0 sidecar delete LANDED (#6831) → 1 agnostic `While`/`BoundedPoll`/`Retry` emit PARTIAL (#6832; corrected 2026-07-26, verified against main — `While` framing has exactly one production consumer, `ci_floor_peak_emit`, whose cond and body are still raw shell strings; `BoundedPoll` has zero production construction sites, test-only; #7303 makes Retry refuse multi-step bodies, present redirect/capture, changed template log sources, and three-or-more (N-level) escalation shapes; #7318 additionally refuses every nonempty source `Run.env` while preserving empty-env emission bytes, proven by executing nonempty-refusal and empty-env accepted controls. Phase 1 remains PARTIAL only because the live `While` consumer has two raw command leaves, `BoundedPoll` is test-only, and the modeled scope still needs need-driven adoption or explicit narrowing) → 2 model runtime-present sites as typed effects on `host_effect_apply` + emit the foreign-executor/bootstrap residue → 3 wall green (bash-AST containment + meta-exec confinement). → [shell → intent design](docs/plans/shell-intent-emit-realization-design.md)"), - li(text: "**srvN build-cache provisioning on host-standup subsumption spine.** Legacy `ctrl-sccache.service` is misconfigured; `ci_release_build_script()` absorbing fallback (`CARGO_BUILD_JOBS=1` then `-u RUSTC_WRAPPER`) masks the deficit (§5). Design anchor: provision sccache as a host-effect ensure on the assimilation spine (P1b after `RunnerDeploySlot`), generalizing srv3's `WorkflowEnsureActuatorToolchain` to srvN; `ProvisionBuildCache { catalog_id }` routes through `host_effect_apply`; verdict fold is `ProvisionConverged | ProvisionRefused` only. STEP 2 (fallback removal) operator-sequenced after T4 live read-back. → [srvN build-cache provisioning design](docs/plans/srvn-buildcache-provisioning-design.md)"), + li(text: "**srvN build-cache provisioning on host-standup subsumption spine.** Legacy `ctrl-sccache.service` is misconfigured; `ci_release_build_script()` absorbing fallback (`CARGO_BUILD_JOBS=1` then `-u RUSTC_WRAPPER`) masks the deficit (§5). Design anchor: provision sccache as a host-effect ensure on the assimilation spine (P1b after `RunnerDeploySlot`), generalizing srv3's `WorkflowEnsureActuatorToolchain` to srvN; `ProvisionBuildCache { catalog_id }` routes through `host_effect_apply`; verdict fold is `ProvisionConverged | ProvisionRefused` only. **PLACEMENT IS A VERDICT AXIS (landed):** sccache executes cache-miss compiles as *children of the server*, so the server's cgroup is the cgroup every compile on the host runs in — a responsive daemon proves nothing about where it runs, and collapsing the two into one `DaemonStatsOk` arm is the §5 state-space conflation that let a lazily auto-spawned server (kept alive by `SCCACHE_IDLE_TIMEOUT=0`) strand itself in a dead slot's cgroup and bound all five live slots' compiles at one slot's cap. `BuildCacheServerPlacement = ServerManagedUnitOwned | ServerRunnerOwned\{slot,lifecycle\} | ServerUnsupervised | ServerPlacementAbsent | ServerPlacementUnknown` gates `ProvisionConverged`; unknown placement refuses rather than assuming owned, and BOTH runner lifecycles refuse (an active slot means birth ordering failed; a retired slot means birth ordering AND retirement both failed). It is derived from an endpoint-owner observation homed with the deployed-instance model in `gunbc.build_cache_instance` (`BuildCacheInstance` = host x consumer class x instance name, distinct from the CATALOG identity `sccache_local_id`), because process name plus cgroup cannot tell a server from the transient clients every compile spawns — the owner is the process holding the LISTEN on the configured endpoint. **LIVE RECEIPT 2026-08-01 (read-only, srv1/srv2/srv3):** NO host has a unit-owned CI cache; the birth defect is fleet-wide. srv1's owner is pid 3975946, 31.2 days old, in RETIRED slot `srv1-10` (inactive+disabled) with `max 229780` / `oom_kill 12` and a live runner's `rustc` resident beside it; srv2's and srv3's owners sit in ACTIVE slots and so self-limit only because ephemeral runners recycle them. srv4 was unreachable and is carried as typed `ReachRefused`, never as absent. A measured correction landed with it: `--show-stats` does NOT auto-start a server (private-endpoint controls, both UDS and TCP, 0 to 0 listeners) — it answers plausibly with NO server in existence, so stats may only ever be supplemental data read after ownership is grounded; a real COMPILE is what spawns a server, inheriting the invoking cgroup. The budget consequence is a first-class `compile_pool` child of the host allocation tree sized by the cited jobserver authority (`jobserver_build_pool_bytes`) — a per-slot memory cap does *not* bound compile RSS — with `CompilePoolPlacement` making the pool's size derive from the declared topology rather than a fudge. STEP 2 (fallback removal) operator-sequenced after T4 live read-back. → [srvN build-cache provisioning design](docs/plans/srvn-buildcache-provisioning-design.md) · [placement step-1 live receipt](docs/plans/build-cache-placement-receipt.md)"), li(text: "**srv4 fleet subsumption — manual-gap receipts (2026-07-22).** srv4 was installed and subsumed through the modeled BMC virtual-media path (PR #7027: 5 runners live, rootless Docker + sccache, seeded-media boot-fidelity fixes root-caused live); every step still performed by hand is typed as a modeling gap in the registered plan `gunbc.plans.fleet_subsumption_manual_gaps` (A1 MaterializedSecret lifecycle, A2 reach-secrets ADC, A3 SecretRef liveness, B1 runner-slot provisioning, B2 runner user, B3 fresh-host prereqs, B4 install actuator), each with an acceptance bar and RED; the plan dissolves item-by-item and fully retires when srv5 subsumes with zero hand-run shell. The BMC credential-rotation gap analysis that seeded the lane: → [srv4 BMC onboarding gap](docs/plans/srv4-bmc-onboarding-gap.md)"), li(text: "**gunb.ai site subsumption — hosting + frontend emitted from `.dag` (operator-directed 2026-07-20).** Subsume `gunb-ai/frontend`: the public site's HTML/CSS/JS become emitted, drift-gated artifacts in this repo, published to GitHub Pages at `gunb.ai` (DNS already points at Pages IPs; cutover is Pages-config-only; the frontend repo archives). Register thesis, developed across nine operator passes and carried as a signable checklist + behavioral-intricacy law: *quiet at arm's length; responsive up close; every response true* — simplicity is a D0/D1 property, intricacy a D2/D3 property of BEHAVIOR (coverage × consistency × timing, the Discord re-read), and every intricate thing shown or answered derives from a real repo fact. Phase-0 slices LANDED under `dag/gunbc/site/` (principles → tokens → interaction → mark → constellation → moodboard → theme_transition → starlight/accent_study), proven by execution with RED controls: themes as total role→material assignments (night = warm starlight on indigo, day = indigo ink on paper, one model, two projections; totality/area/contrast witnessed), the glare envelope (bright OR chromatic, never both — the operator-rejected gamer cyan is the negative control and provably absent from emissions), still-until-touched by construction (MotionTrigger has no ambient variant), the universal theme reframe derived from the same BuildRule rows (role-routed colors cross-fade for free on one token clock; an unthemed color is a counted census row; the data-theme watcher pins the flip so the fade cannot race the UA restyle — measured, snap → 12–16-frame blend), and the accent decision grounded in `extdeps.astronomy.stellar_classification` (cited MK/Harvard table; no green stars exist so green enters only as verdigris material; M self-prunes at the envelope; candidates O/A/G/K rendered from identical fact rows). Remaining: checklist row-by-row sign-off, primitives.html at every depth, mark candidates B–D, craft-floor scales, generated-artifact registration, Pages workflow + cutover receipt (discharges the `2-stateless-frontend` milestone-A shape on the public domain). → [site subsumption design](docs/plans/gunb-ai-site-subsumption-design.md)"), li(text: "**hierarchical design-register library — lift `gunbc.site` L1/L2 into `gunbc.design.*` (DOC-ONLY until #6927 merges).** PR #6927 proves the register on gunb.ai; the follow-on extracts site-independent L1 house walls + L2 role-material machinery (lift-not-fork), re-grounds contrast at L0 (WCAG/OKLCH, dissolving the HSL lightness-gap proxy in `theme_contrast_ok`), and stages L4 archetypes + `DataVizArea` behind second-consumer discipline. Phase A fires on #6927 merge with an emission-identical witness. The **palette catalogue** rode ahead additively (operator 2026-07-21): `extdeps.color.srgb` (CSS Color 4 cite; hex = a serialization of `Srgb8`, HSL→sRGB re-homed out of the WCAG module) → `gunbc.design.palette` (approval IS the computed WCAG grade — ordered figure-on-ground pairings total by construction) → `gunbc.design.catalog` (first entry island-pastel incl. the derived `loam` ink), witnessed by execution; its measured receipts (HSL-S misreads pastel tints as neon — ivory C 0.007 refused while `day_canvas` C 0.008 passes; gamer-cyan C 0.088 < mint C 0.142, so the envelope must be a per-area L×C region) are A.2's discriminating witness — see plan §2.2. → [design-register library](docs/plans/design-register-library.md)"), diff --git a/dag/gunbc/doc_graph_roots.dag b/dag/gunbc/doc_graph_roots.dag index 426c9098671..118412d33a9 100644 --- a/dag/gunbc/doc_graph_roots.dag +++ b/dag/gunbc/doc_graph_roots.dag @@ -22,7 +22,7 @@ type HandAuthoredDocBind { dissolution: DissolutionTrigger, } -data hand_authored_works_carrier_note: String = "primary_work + additional_works is the TYPED anchor set: every carrier a doc binds to is a DeclarationRef row, never a name living only in DissolutionTrigger.text (post-merge review of gunbc#7486, 2026-07-31: the merged guarantee-recovery row claimed two anchored carriers while structurally carrying one — the second existed only in prose, so its lifecycle was untracked). CONSTRUCTION, not validation (2026-07-31 operator verdict on gunbc#7489): the first cut typed works: List, which admits [] — a workless bind was writable and a witness stood where a wall was available (DESIGN 5), the exact shape the guarantee ladder's cardinality class names. primary_work is a required scalar, so the zero-anchor row is now unwritable and the nonempty predicates plus their empty-works RED control were DELETED WITH the validation they backed (DESIGN 4b dissolution rule: the RED asserted a predicate over a state that no longer compiles; its discriminating role passes to the type, and the compile-refusal probe proving a missing-primary_work row refuses belongs to the ladder-probe-corpus record-completeness class — refinement/completeness enforcement at data positions is itself an open census class, so the wall is the declared shape today and the probe corpus is its named evidence trigger). EXECUTING CONSUMERS (review 45336 lineage): hand_authored_bind_works projects the full anchor list for the reachability witnesses; refs-carry-symbols stays as honest validation ONLY because NonEmptyStr refinement is not yet enforced at data positions (dissolves when floor-inhabitance-wall lands); bind identity (home+slug) uniqueness is witnessed with a synthetic-duplicate RED — two rows for one doc gave one authority two independently removable halves, caught twice in review (45305, and the 2026-07-31 verdict's residual pair). RESIDUE, named: staleness against the live tree (a ref naming a decl that no longer exists) is NOT witnessed here — v1.compiler.* modules are outside the witness compile pool, so ref-vs-tree resolution is exactly the feature:cited-symbol-resolution lens DESIGN 3 already names; these refs are symbolic citations and inherit that trigger." +data hand_authored_works_carrier_note: String = "primary_work + additional_works is the TYPED anchor set: every carrier a doc binds to is a DeclarationRef row, never a name living only in DissolutionTrigger.text (post-merge review of gunbc#7486, 2026-07-31: the merged guarantee-recovery row claimed two anchored carriers while structurally carrying one — the second existed only in prose, so its lifecycle was untracked). CONSTRUCTION, not validation (2026-07-31 operator verdict on gunbc#7489): the first cut typed works: List, which admits [] — a workless bind was writable and a witness stood where a wall was available (DESIGN 5), the exact shape the guarantee ladder's cardinality class names. primary_work is a required scalar, so the zero-anchor row is now unwritable and the nonempty predicates plus their empty-works RED control were DELETED WITH the validation they backed (DESIGN 4b dissolution rule: the RED asserted a predicate over a state that no longer compiles; its discriminating role passes to the type, and the compile-refusal probe proving a missing-primary_work row refuses belongs to the ladder-probe-corpus record-completeness class — refinement/completeness enforcement at data positions is itself an open census class, so the wall is the declared shape today and the probe corpus is its named evidence trigger). EXECUTING CONSUMERS (review 45336 lineage): hand_authored_bind_works projects the full anchor list for the reachability witnesses; refs-carry-symbols stays as honest validation ONLY because NonEmptyStr refinement is not yet enforced at data positions (dissolves when declared-conformance-general-wall lands — the 2026-08-01 recut retired floor-inhabitance-wall; the accepted ground fragment deliberately excludes refinement classes); bind identity (home+slug) uniqueness is witnessed with a synthetic-duplicate RED — two rows for one doc gave one authority two independently removable halves, caught twice in review (45305, and the 2026-07-31 verdict's residual pair). RESIDUE, named: staleness against the live tree (a ref naming a decl that no longer exists) is NOT witnessed here — v1.compiler.* modules are outside the witness compile pool, so ref-vs-tree resolution is exactly the feature:cited-symbol-resolution lens DESIGN 3 already names; these refs are symbolic citations and inherit that trigger." fn hand_authored_bind_works(b: HandAuthoredDocBind) -> List { concat([b.primary_work], b.additional_works) @@ -343,6 +343,13 @@ data hand_authored_doc_binds: List = [ additional_works: [DeclarationRef { module_path: "v2.std.constraints", decl_name: "solve_constraints", field: WholeDeclaration }], dissolution: HasTrigger { text: "ONE row, TWO anchored carriers — both in the typed works field (post-merge review 2026-07-31: the prior revision carried the second anchor only in this prose), BOTH conditions gate dissolution (merged from a two-row form per review 45305: home+slug is this list's symbolic identity, so two rows gave one doc two independently removable authority rows). Condition zero, the authority: the recovered guarantee statement and tier claims are modeled as .dag claim rows (the guarantee authority of the analysis Sec 11/12, grounding the StandingIntent thread) and PROJECTED into DESIGN.md via gunbc.design_document — never hand-edited into the markdown, which is a projection, so a direct DESIGN.md edit does NOT satisfy this trigger (analysis Sec 7b; wording corrected from 're-homed into DESIGN.md' per review 45299) — and every Tier 1/Tier 2 row in the analysis is either closed on its carrier or migrated to a registered gunbc.plan.Plan row. Carrier (1), the primary_work ref: v1.compiler.infer module_skips_direct_call_arg_check — the one named in-tree violation of the dimension contract's 'no escape hatch' clause (docs/thesis/correctness-dimensions.md): it exempts v2.* and v1.compiler.* from direct-call argument checking, so the compiler being bootstrapped is authored under weaker checks than ordinary source; its deletion is the analysis's terminal Tier 1 acceptance. Carrier (2): v2.std.constraints solve_constraints — passes graph.root as source_facts, algebra AND the sole candidate, and v2.std.constraint_satisfaction_predicate.constraint_satisfaction_preservation_holds requires algebra == source_facts && candidate == source_facts, so the grounding proof reduces to well_formed(root) relabelled CanonicalGrounding; that half dissolves when a candidate is derived rather than supplied, or the generic arm returns a typed compile refusal instead of an accepted self-grounded InferredTree. The row deletes only when BOTH carriers are closed" }, }, + HandAuthoredDocBind { + home: PlanDoc, + slug: "roadmap-presentation-seam-design", + primary_work: DeclarationRef { module_path: "gunbc.roadmap_page", decl_name: "a1_composition_provisional_note", field: WholeDeclaration }, + additional_works: [DeclarationRef { module_path: "gunbc.roadmap_component", decl_name: "roadmap_row_archetype", field: WholeDeclaration }], + dissolution: HasTrigger { text: "the A2 presentation seam lands on its carriers — the view projection module (WorkRowView/ActivityView/ActionView/DailyWorkspaceView/ObservationImpactView), the row archetype's ActivityRegion placed UnderHead with ActuatorRegion shrunk to actions only, and the attention-rule witnesses including the falsifier specimen (active attempt + seven obligations + session-present/process-exited + long title, coherent at three widths) — and the plan registers as a gunbc.plan.Plan row (extends roadmap-workspace-ux-plan and roadmap-workspace-remodel-plan, whose rows dissolve on their own triggers)" }, + }, HandAuthoredDocBind { home: PlanDoc, slug: "nat-grounding-unification-design", diff --git a/dag/gunbc/econ/acquisition.dag b/dag/gunbc/econ/acquisition.dag new file mode 100644 index 00000000000..a225c8c49dc --- /dev/null +++ b/dag/gunbc/econ/acquisition.dag @@ -0,0 +1,143 @@ +module gunbc.econ.acquisition + +import std.algebra { FieldOfFractions } +import std.measure { MoneyOnce, money_once_micros } +import std.nat { Nat } +import std.types { List, NonEmptyStr, list_length } + +data acquisition_mechanism_note: String = "Generic acquisition funnel mechanism, sibling to gunbc.econ.scm_serving_model: stages are PARAMETERS (declared labels on scenario rows, never baked into the mechanism), conversion rates are exact Nat fractions (never floats), and every underivable input refuses with a typed, located cause. This module is mechanism only -- instantiations with actual assumptions carry their own citations and live elsewhere. Vendor stage vocabularies to instantiate stage labels against are cited in extdeps.crm (Salesforce opportunity stages, HubSpot lifecycle stages); channel pricing interfaces for entry unit costs are cited in extdeps.advertising." + +type ConversionRate = FieldOfFractions + +data conversion_rate_note: String = "A conversion rate is an exact fraction and CONSUMES the std.algebra FieldOfFractions carrier at Nat (non-negative fractions): num is the converted count, denom is the cohort count -- 1 of 40 stays 1/40, never 0.025. Composition multiplies num and denom; nothing in the mechanism divides until a consumer explicitly asks for the floor projection, so the exact fraction is the authority and the floor is a declared lossy view. Fixed-scale dimensionless carriers (Percent, BasisPoint) do not absorb this concept -- projecting onto any fixed scale loses the exactness (1 of 3 has no exact basis-point image). The at-most-unity semantics (num at most denom) is a conversion-rate FACT enforced by typed refusal at composition, not a property of the generic fraction carrier. An earlier revision of this note claimed no std exact-rational carrier existed on main and minted a local pair; refuted against the tree by review 45963 (std.algebra FieldOfFractions carries the grounded num/denom body and std.rational.Rational builds on it), so the local pair was dissolved into the authority." + +type AcquisitionStageStep { + stage_label: NonEmptyStr + assumed_rate: ConversionRate +} + +type AcquisitionScenario { + entry_stage_label: NonEmptyStr + entry_unit_cost: MoneyOnce + steps: List +} + +type AcquisitionScenarioInput + = AcquisitionScenarioDeclared { scenario: AcquisitionScenario } + | AcquisitionScenarioUnknown + +type AcquisitionRefusalCause + = ScenarioUnknown + | ZeroEntryUnitCost + | EmptyFunnel + | ZeroConversionNumerator { stage_label: NonEmptyStr } + | ZeroConversionCohort { stage_label: NonEmptyStr } + | ConversionRateExceedsUnity { stage_label: NonEmptyStr } + +type AcquisitionCostForecast { + entry_stage_label: NonEmptyStr + outcome_stage_label: NonEmptyStr + composed_rate: ConversionRate + entry_unit_cost: MoneyOnce +} + +data acquisition_forecast_distinctness_note: String = "AcquisitionCostForecast is a FORECAST carrier: a derived consequence of declared assumptions. It shares no type with any observation or Measured row anywhere in the tree, so a forecast cannot be passed where an actual is required -- the distinction is structural, not a naming convention. Money stays on the std.measure MoneyOnce carrier (currency and micro denomination live there, never as bare scalars beside it), and the cost per outcome is DERIVED, never stored: the exact value is entry_unit_cost scaled by denom over num on the ConversionRate fraction. Every forecast the derivation PRODUCES carries a nonzero num (a zero numerator refuses before composition), but the record itself remains publicly constructible with num zero, so the floor projection below does not trust its caller: it is total by typed refusal, returning none on a zero num rather than dividing." + +type AcquisitionForecastResult + = AcquisitionForecastDerived { forecast: AcquisitionCostForecast } + | AcquisitionForecastRefused { cause: AcquisitionRefusalCause } + +type AcquisitionFoldAcc { + refusal: AcquisitionRefusalCause? + converted_product: Nat + cohort_product: Nat + outcome_label: NonEmptyStr +} + +data acquisition_refusal_first_wins_note: String = "The step fold refuses on the FIRST invalid step in declared order and carries that cause unchanged past later steps -- one typed cause per refusal, never a widened or merged answer. Rate validity is per-step: a zero cohort is an ill-formed fraction, a zero numerator makes the funnel's outcome unreachable (cost per outcome would divide by zero), and converted greater than cohort asserts more conversions than attempts. Each names the stage it refused at." + +fn acquisition_step_apply(a: AcquisitionFoldAcc, step: AcquisitionStageStep) -> AcquisitionFoldAcc { + match a.refusal { + Present { value: _ } => a + Absent => + if step.assumed_rate.denom == 0 { + AcquisitionFoldAcc { + refusal: Present { value: ZeroConversionCohort { stage_label: step.stage_label } }, + converted_product: a.converted_product, + cohort_product: a.cohort_product, + outcome_label: a.outcome_label, + } + } else { + if step.assumed_rate.num == 0 { + AcquisitionFoldAcc { + refusal: Present { value: ZeroConversionNumerator { stage_label: step.stage_label } }, + converted_product: a.converted_product, + cohort_product: a.cohort_product, + outcome_label: a.outcome_label, + } + } else { + if step.assumed_rate.num > step.assumed_rate.denom { + AcquisitionFoldAcc { + refusal: Present { value: ConversionRateExceedsUnity { stage_label: step.stage_label } }, + converted_product: a.converted_product, + cohort_product: a.cohort_product, + outcome_label: a.outcome_label, + } + } else { + AcquisitionFoldAcc { + refusal: none, + converted_product: a.converted_product * step.assumed_rate.num, + cohort_product: a.cohort_product * step.assumed_rate.denom, + outcome_label: step.stage_label, + } + } + } + } + } +} + +fn derive_acquisition_cost_forecast(input: AcquisitionScenarioInput) -> AcquisitionForecastResult { + match input { + AcquisitionScenarioUnknown => AcquisitionForecastRefused { cause: ScenarioUnknown } + AcquisitionScenarioDeclared { scenario: s } => + if money_once_micros(q: s.entry_unit_cost) == 0 { + AcquisitionForecastRefused { cause: ZeroEntryUnitCost } + } else { + if list_length(items: s.steps) == 0 { + AcquisitionForecastRefused { cause: EmptyFunnel } + } else { + let acc = fold(s.steps, init: AcquisitionFoldAcc { + refusal: none, + converted_product: 1, + cohort_product: 1, + outcome_label: s.entry_stage_label, + }, f: fn(a, step) { acquisition_step_apply(a: a, step: step) }) + match acc.refusal { + Present { value: c } => AcquisitionForecastRefused { cause: c } + Absent => + AcquisitionForecastDerived { + forecast: AcquisitionCostForecast { + entry_stage_label: s.entry_stage_label, + outcome_stage_label: acc.outcome_label, + composed_rate: ConversionRate { + num: acc.converted_product, + denom: acc.cohort_product, + }, + entry_unit_cost: s.entry_unit_cost, + }, + } + } + } + } + } +} + +fn acquisition_forecast_cost_floor_micros(forecast: AcquisitionCostForecast) -> Nat? { + if forecast.composed_rate.num == 0 { + none + } else { + Present { + value: (money_once_micros(q: forecast.entry_unit_cost) * forecast.composed_rate.denom) / forecast.composed_rate.num + } + } +} diff --git a/dag/gunbc/fleet_host_budget.dag b/dag/gunbc/fleet_host_budget.dag index d978efa6c5b..1381ea44f7b 100644 --- a/dag/gunbc/fleet_host_budget.dag +++ b/dag/gunbc/fleet_host_budget.dag @@ -26,6 +26,7 @@ import extdeps.jobserver { JobserverAbsent, jobserver_config, derive_jobserver_tokens, + jobserver_build_pool_bytes, } import gunbc.fleet_container { FleetContainerType, @@ -124,6 +125,41 @@ fn gunbc_fleet_oomd_evidence_for(mode: SessionReservationMode) -> OomdEnforcemen data gunbc_fleet_oomd_evidence: OomdEnforcementEvidence = gunbc_fleet_oomd_evidence_for(mode: gunbc_ci_session_reservation_mode) +data gunbc_compile_pool_doc: String = "THE COMPILE POOL IS A CLAIM ON HOST RAM THAT THE PER-SLOT MODEL DID NOT SEE. extdeps.cache.sccache.sccache_server_process_facts cites that cache-miss compiles execute as CHILDREN OF THE SCCACHE SERVER. So compile RSS is charged to the SERVER's cgroup, not to the runner slot that issued the compile, and a per-slot memory cap therefore does NOT bound compile memory. Measured consequence on srv1: the CI server was lazily auto-spawned inside dead slot srv1-10's cgroup and kept alive by SCCACHE_IDLE_TIMEOUT=0, so all five live slots' compiles executed under one dead slot's 15G high / 16G max caps and accumulated 229780 memory.max breach events and 12 oom_kills. Sizing is jobserver_build_pool_bytes (tokens * mib_per_job) — the same cited authority that bounds build concurrency, NOT a fresh literal: the pool must hold exactly the compiles the jobserver admits." + +data gunbc_compile_pool_placement_doc: String = "WHERE COMPILE RSS IS CHARGED IS A TOPOLOGY FACT, AND THE BUDGET IS DERIVED FROM IT — not a fudge factor. CompilePoolInRunnerSlots: no managed compile-pool slice exists, so cache-miss compiles are charged to whichever cgroup the sccache server sits in; the budget's compile_pool child is therefore ZERO because the pool is not a separate claim on host RAM — it is (nominally) already inside runner_pool. That is the honest description of today's topology, NOT an absorbing fallback: it is level-triggered on a declared row, it widens no failure arm, and it fabricates no converged verdict. Note it is also the topology in which a mis-placed server silently exports one slot's caps onto every compile on the host — that deficit is refused by gunbc.host_build_cache_provision's placement verdict, not papered over here. CompilePoolSliceDeclared: the managed unit owns the compiles in its own slice, so the pool becomes a first-class sibling child sized by the admitted jobserver tokens and runner_pool must shrink to keep the host conserving. DISSOLUTION TRIGGER: flip this row to CompilePoolSliceDeclared in the same change that converges the managed ctrl-sccache-ci unit and its slice; conservation then refuses by construction if the slots were not shrunk." + +type CompilePoolPlacement + = CompilePoolSliceDeclared { slice_unit: String } + | CompilePoolInRunnerSlots + +data gunbc_compile_pool_placement: CompilePoolPlacement = CompilePoolInRunnerSlots + +type CompilePoolResolution + = CompilePoolDerived { bytes: ByteSize } + | CompilePoolUnresolved { reason: String } + +fn compile_pool_bytes_for( + placement: CompilePoolPlacement, + nproc: HardwareThreadCount, + host: ByteSize, + citation: JobserverCitation +) -> CompilePoolResolution { + match placement { + CompilePoolInRunnerSlots => CompilePoolDerived { bytes: byte_size(0) } + CompilePoolSliceDeclared { slice_unit: _ } => resolve_compile_pool(nproc: nproc, host: host, citation: citation) + } +} + +fn resolve_compile_pool(nproc: HardwareThreadCount, host: ByteSize, citation: JobserverCitation) -> CompilePoolResolution { + match citation { + JobserverAbsent => + CompilePoolUnresolved { reason: "jobserver config citation absent: the compile pool is sized by the admitted build tokens (tokens * mib_per_job); refuse to size the pool — and therefore to claim the host allocation conserves — without the cited installer knobs" } + JobserverCited { config: cfg, provenance: _ } => + CompilePoolDerived { bytes: jobserver_build_pool_bytes(nproc: nproc, mem_total: host, cfg: cfg) } + } +} + fn appropriation_of(b: ByteSize) -> Appropriation { Appropriation { ceiling: b } } @@ -163,7 +199,8 @@ fn host_allocation_tree( overhead: ByteSize, headroom: ByteSize, runner_pool: ByteSize, - session_pool: ByteSize + session_pool: ByteSize, + compile_pool: ByteSize ) -> BudgetNode { BudgetNode { name: "host_allocation", @@ -172,6 +209,7 @@ fn host_allocation_tree( children: [ child_node(name: "runner_pool", b: runner_pool), child_node(name: "session_pool", b: session_pool), + child_node(name: "compile_pool", b: compile_pool), child_node(name: "headroom", b: headroom), ], } @@ -182,14 +220,16 @@ fn host_allocation_conserves( overhead: ByteSize, headroom: ByteSize, runner_pool: ByteSize, - session_pool: ByteSize + session_pool: ByteSize, + compile_pool: ByteSize ) -> Bool { node_conserves(node: host_allocation_tree( host: host, overhead: overhead, headroom: headroom, runner_pool: runner_pool, - session_pool: session_pool + session_pool: session_pool, + compile_pool: compile_pool )) } @@ -202,10 +242,11 @@ fn max_concurrent_sessions(session_pool: ByteSize) -> Int { } -fn runner_slice_cap(host: ByteSize, overhead: ByteSize, session_slice: ByteSize, headroom: ByteSize) -> ByteSize { +fn runner_slice_cap(host: ByteSize, overhead: ByteSize, session_slice: ByteSize, headroom: ByteSize, compile_pool: ByteSize) -> ByteSize { let after_overhead = byte_size_saturating_sub(total: host, reserved: overhead) let after_session = byte_size_saturating_sub(total: after_overhead, reserved: session_slice) - byte_size_saturating_sub(total: after_session, reserved: headroom) + let after_headroom = byte_size_saturating_sub(total: after_session, reserved: headroom) + byte_size_saturating_sub(total: after_headroom, reserved: compile_pool) } fn runner_slice_node(runner_slice: ByteSize, runner_baseline: ByteSize) -> BudgetNode { @@ -224,7 +265,8 @@ fn fleet_host_budget_tree( runner_baseline: ByteSize, session_slice: ByteSize, headroom: ByteSize, - runner_slice: ByteSize + runner_slice: ByteSize, + compile_pool: ByteSize ) -> BudgetNode { BudgetNode { name: name, @@ -233,6 +275,7 @@ fn fleet_host_budget_tree( children: [ child_node(name: "session_worstcase_reservation", b: session_slice), child_node(name: "headroom", b: headroom), + child_node(name: "compile_pool", b: compile_pool), runner_slice_node(runner_slice: runner_slice, runner_baseline: runner_baseline), ], } @@ -282,18 +325,22 @@ fn fleet_host_plan_for( let floor = oomd_actuation_floor(host: host) match resolve_headroom(policy: policy, floor: floor) { HeadroomUnsound { reason: r } => FleetHostPlanUnsound { reason: r } - HeadroomResolved { bytes: hr } => { - let slice = runner_slice_cap(host: host, overhead: overhead, session_slice: session_slice, headroom: hr) - let tree = fleet_host_budget_tree(name: name, host: host, overhead: overhead, runner_baseline: rb, session_slice: session_slice, headroom: hr, runner_slice: slice) - FleetHostPlanDerived { - name: name, - tree: tree, - runner_slice_cap: slice, - effective_build_tokens: derive_jobserver_tokens(nproc: nproc, mem_total: host, cfg: cfg), - effective_runner_cpu_cap: cpu_cores_available(nproc: nproc, margin: cfg.margin), - build_step: build_realization(cfg: cfg), + HeadroomResolved { bytes: hr } => + match compile_pool_bytes_for(placement: gunbc_compile_pool_placement, nproc: nproc, host: host, citation: citation) { + CompilePoolUnresolved { reason: cr } => FleetHostPlanUnsound { reason: cr } + CompilePoolDerived { bytes: compile_pool } => { + let slice = runner_slice_cap(host: host, overhead: overhead, session_slice: session_slice, headroom: hr, compile_pool: compile_pool) + let tree = fleet_host_budget_tree(name: name, host: host, overhead: overhead, runner_baseline: rb, session_slice: session_slice, headroom: hr, runner_slice: slice, compile_pool: compile_pool) + FleetHostPlanDerived { + name: name, + tree: tree, + runner_slice_cap: slice, + effective_build_tokens: derive_jobserver_tokens(nproc: nproc, mem_total: host, cfg: cfg), + effective_runner_cpu_cap: cpu_cores_available(nproc: nproc, margin: cfg.margin), + build_step: build_realization(cfg: cfg), + } + } } - } } } } diff --git a/dag/gunbc/heal_revalidation.dag b/dag/gunbc/heal_revalidation.dag new file mode 100644 index 00000000000..e97550c0c01 --- /dev/null +++ b/dag/gunbc/heal_revalidation.dag @@ -0,0 +1,176 @@ +module gunbc.heal_revalidation + +import std.types { CommitSha, ContentHash, NonEmptyStr } +import gunbc.merge_admission { + CheckCoverage, + check_coverage_admits +} + +type HealOutcome + = HealNoChange { head: CommitSha } + | HealProduced { + prior_head: CommitSha, + healed_head: CommitSha, + changed_artifacts: List + } + +type HealDispatchOutcome + = HealDispatchPending { healed_head: CommitSha } + | HealDispatchAccepted { + healed_head: CommitSha, + run: NonEmptyStr + } + | HealDispatchRefused { + healed_head: CommitSha, + cause: NonEmptyStr + } + +type PriorRunDisposition + = PriorRunStillCurrent { head: CommitSha } + | SupersededByHealedHead { + prior_head: CommitSha, + healed_head: CommitSha + } + +type HealRevalidationState + = HealRevalidationNotRequired { head: CommitSha } + | HealRevalidationRequired { + prior_head: CommitSha, + healed_head: CommitSha, + changed_artifacts: List, + dispatch: HealDispatchOutcome + } + | HealRevalidationCoverageObserved { + prior_head: CommitSha, + healed_head: CommitSha, + changed_artifacts: List, + coverage: CheckCoverage + } + +type HealRevalidationAdmissionVerdict + = HealRevalidationAdmittedNoChange { head: CommitSha } + | HealRevalidationAdmittedExactCoverage { healed_head: CommitSha } + | HealRevalidationDeniedRequired { healed_head: CommitSha, dispatch: HealDispatchOutcome } + | HealRevalidationDeniedCoverage { + healed_head: CommitSha, + coverage: CheckCoverage + } + +type WorkflowDispatchPreflight + = OrdinaryWorkflowDispatch { + checkout_head: CommitSha, + run_subject_head: CommitSha + } + | HealWorkflowDispatchSubjectMatched { expected_head: CommitSha } + | HealWorkflowDispatchCheckoutMismatch { + expected_head: CommitSha, + checkout_head: CommitSha, + run_subject_head: CommitSha + } + | HealWorkflowDispatchRunSubjectMismatch { + expected_head: CommitSha, + checkout_head: CommitSha, + run_subject_head: CommitSha + } + +data heal_revalidation_subject_universe_note: String = "HEAL REVALIDATION IS A TOTAL CLASSIFICATION OVER EVERY HEAD THE HEAL CAN PRODUCE (operator mandate 2026-08-01). HealNoChange creates no new subject. HealProduced creates an immutable healed_head, immediately supersedes prior_head, and starts in HealRevalidationRequired; Pending, Accepted, and Refused dispatches all remain refusals until CheckCoverageComplete for that exact healed head and the current nonempty roster admits through gunbc.merge_admission. The dispatched run also classifies its GitHub run/check subject separately from checkout HEAD: both must equal healed_head, and either mismatch is a typed refusal. Adding a heal path or preflight subject state means adding a coproduct arm, which turns the exhaustive folds red until that new subject is classified. There is no unknown-head arm and no rerun-everything fallback." + +data heal_revalidation_external_boundary_note: String = "This module and the CI workflow close the in-repository model/realization loop: a produced heal explicitly dispatches a new run and exact-head coverage is required. The external dashboard readout that historically treated an absent run as ok, and its separate aggregate conflation of Cancelled superseded runs with failed runs, live outside this repository and are not fixed or claimed fixed here." + +fn heal_revalidation_initial(outcome: HealOutcome) -> HealRevalidationState { + match outcome { + HealNoChange { head } => HealRevalidationNotRequired { head: head } + HealProduced { prior_head, healed_head, changed_artifacts } => + HealRevalidationRequired { + prior_head: prior_head, + healed_head: healed_head, + changed_artifacts: changed_artifacts, + dispatch: HealDispatchPending { healed_head: healed_head } + } + } +} + +fn prior_run_disposition(outcome: HealOutcome) -> PriorRunDisposition { + match outcome { + HealNoChange { head } => PriorRunStillCurrent { head: head } + HealProduced { prior_head, healed_head, changed_artifacts: _ } => + SupersededByHealedHead { prior_head: prior_head, healed_head: healed_head } + } +} + +fn classify_workflow_dispatch_preflight( + expected_healed_head: CommitSha?, + checkout_head: CommitSha, + run_subject_head: CommitSha, +) -> WorkflowDispatchPreflight { + match expected_healed_head { + Absent => OrdinaryWorkflowDispatch { + checkout_head: checkout_head, + run_subject_head: run_subject_head + } + Present { value: expected } => + if expected != run_subject_head { + HealWorkflowDispatchRunSubjectMismatch { + expected_head: expected, + checkout_head: checkout_head, + run_subject_head: run_subject_head + } + } else if expected == checkout_head { + HealWorkflowDispatchSubjectMatched { expected_head: expected } + } else { + HealWorkflowDispatchCheckoutMismatch { + expected_head: expected, + checkout_head: checkout_head, + run_subject_head: run_subject_head + } + } + } +} + +fn classify_heal_revalidation_admission( + state: HealRevalidationState, + required_roster: ContentHash, + required_gates: List, +) -> HealRevalidationAdmissionVerdict { + match state { + HealRevalidationNotRequired { head } => + HealRevalidationAdmittedNoChange { head: head } + HealRevalidationRequired { + prior_head: _, healed_head, changed_artifacts: _, dispatch + } => + HealRevalidationDeniedRequired { healed_head: healed_head, dispatch: dispatch } + HealRevalidationCoverageObserved { + prior_head: _, healed_head, changed_artifacts: _, coverage + } => + if check_coverage_admits( + coverage: coverage, + required_head: healed_head, + required_roster: required_roster, + required_gates: required_gates + ) { + HealRevalidationAdmittedExactCoverage { healed_head: healed_head } + } else { + HealRevalidationDeniedCoverage { + healed_head: healed_head, + coverage: coverage + } + } + } +} + +fn heal_revalidation_admits( + state: HealRevalidationState, + required_roster: ContentHash, + required_gates: List, +) -> Bool { + match classify_heal_revalidation_admission( + state: state, + required_roster: required_roster, + required_gates: required_gates + ) { + HealRevalidationAdmittedNoChange { head: _ } => true + HealRevalidationAdmittedExactCoverage { healed_head: _ } => true + HealRevalidationDeniedRequired { healed_head: _, dispatch: _ } => false + HealRevalidationDeniedCoverage { healed_head: _, coverage: _ } => false + } +} diff --git a/dag/gunbc/host_build_cache_provision.dag b/dag/gunbc/host_build_cache_provision.dag index fc9c1d81dde..2fb261c1ce5 100644 --- a/dag/gunbc/host_build_cache_provision.dag +++ b/dag/gunbc/host_build_cache_provision.dag @@ -15,8 +15,21 @@ import gunbc.host_build_cache_catalog_gate { import gunbc.host_effect { HostEffect, ProvisionBuildCache } import gunbc.host_toolchain_ensure { host_toolchain_ensure, BuildCache } import product.placement_supply { HostIdentity } +import gunbc.host_layout { gunbc_managed_compile_pool_slice } +import gunbc.build_cache_instance { + RunnerSlotLifecycle, + DesiredRunnerSlot, + BuildCacheServerPlacement, + ServerManagedUnitOwned, + ServerRunnerOwned, + ServerUnsupervised, + ServerPlacementAbsent, + ServerPlacementUnknown, + RunnerSlotIdentity, + RetiredRunnerSlot, +} -data provision_build_cache_widen_sketch_note: String = "RED-control fixture modeling the forbidden absorbing fallback (the -u RUSTC_WRAPPER cache-drop). The live instance ci_retry_escalation_level2 was REMOVED 2026-07-23 (STEP 2): the eagain retry now escalates to CARGO_BUILD_JOBS=1 (keep sccache) only, then fails loud via verify-build-artifacts — no cache-drop widen remains. This fixture stays as the RED control guarding the verdict fold against re-introduction; it must never ship as the authority." +data provision_build_cache_widen_sketch_note: String = "RED-control fixture modeling TWO forbidden widens; it must never ship as the authority verdict fold. (1) The absorbing fallback (the -u RUSTC_WRAPPER cache-drop): the live instance ci_retry_escalation_level2 was REMOVED 2026-07-23 (STEP 2) — the eagain retry now escalates to CARGO_BUILD_JOBS=1 (keep sccache) only, then fails loud via verify-build-artifacts, so no cache-drop widen remains and this fixture guards the fold against its re-introduction. (2) The PLACEMENT widen: its DaemonStatsOk arm discards the placement field, i.e. 'responsive implies converged'. That is precisely the behavior that shipped on srv1 — a lazily auto-spawned server in dead slot srv1-10's cgroup accumulated 229780 memory.max breach events and 12 oom_kills while every provision read reported converged. Re-confirmed live 2026-08-01: pid 3975946, age 31.2d, cgroup actions-runner@srv1-10.service (unit inactive+disabled, slots 01-05 are the live ones), holding LISTEN on /var/lib/ctrl/sccache-ci/server.sock, resident beside an active runner's rustc." data host_build_cache_provision_scope_disposition: Disposition = SingleAuthority @@ -39,10 +52,14 @@ data ci_sccache_opportunistic_detect_scaffold: Disposition = Scaffold { } data build_cache_path_authority_observation_note: String = "CI and ctrl-build sessions consume the cache as bare `sccache` on PATH (RUSTC_WRAPPER=sccache), so provisioning the modeled binary is NOT sufficient — the PATH-visible sccache must BE the modeled one. A second sccache earlier in PATH (a distro package, a stale copy) is precisely the two-authority state this lane exists to remove, and it is invisible to a read-back that probes the absolute path: that read-back greens on the binary we provisioned while the compiler calls a different one. DaemonPathShadowed carries BOTH paths so the refusal is located (which binary won, which one was expected) rather than merely typed. This judgement is modeled HERE, in the verdict fold (proven by design_witness_path_shadow_refuses_fail_closed against a synthetic DaemonPathShadowed observation), not decided by the provisioning shell. Its LIVE observation is STAGED, not emitted: per the shell→dag census this module's script side is an A5 `decompose` row that must not grow its emitted shell surface, so the `command -v sccache` resolve waits for the typed-argv dissolve-on (#5828) rather than landing as hand-shell now (review 42343). The model carries the state and its refusal today; only the live producer is deferred." -data build_cache_supervision_observation_note: String = "A stats read-back CANNOT see whether the daemon it just talked to is supervised — and this is not a gap in the probe, it is the probe's semantics: any sccache invocation AUTO-STARTS a server if none is listening, so `--show-stats` manufactures the very evidence it reports. On srv3 2026-07-25 it printed a full stats table while the modeled unit had been `inactive (dead)` for six hours and no sccache process existed beforehand. Read through the verdict fold as it stood, that host observed DaemonStatsOk and therefore converged: the provisioning act would have reported success over a host with NO durable cache — a fabricated plausible output (§5), and the same shape as DaemonPathShadowed (a read-back that greens on evidence it produced rather than on the state the lane exists to establish). DaemonUnsupervised splits that conflation: 'a daemon answered' and 'the modeled supervisor is running it' are different states with different remedies, so they get different names rather than one Ok. The LIVE observation is STAGED exactly as the PATH-shadow one is — the supervisor read (`systemctl --user is-active`) is not emitted as hand-shell here, because this module's script side is an A5 `decompose` census row that may shrink but must not grow (review 42343); it lands with the typed-argv dissolve-on (#5828). The model carries the state and its refusal today; only the live producer is deferred. Construction covers the case this observation would catch on a host provisioned BY THIS LANE — build_cache_unit_daemon_runs_until_stopped makes the self-exiting unit unwritable — so the observation's standing job is the residue construction cannot reach: a host whose unit was stopped, masked, or replaced out of band." +data build_cache_supervision_observation_note: String = "A stats read-back CANNOT see whether the daemon it just talked to is supervised — and this is not a gap in the probe, it is the probe's semantics: `--show-stats` answers with a fully-formed table WHETHER OR NOT any server exists, so it reports health it has not established. CORRECTED 2026-08-01, and the correction matters because the original sentence named the wrong mechanism: this note previously said the call AUTO-STARTS a server and therefore manufactures the evidence it reports. Measured false for sccache 0.15.0 — two private-endpoint controls (UDS and TCP, each with a private cache dir) took the listener count 0 -> 0 across `--show-stats` while it printed all zeroes and returned rc=0; a third control showed that a real COMPILE is what takes the count 0 -> 1, with the new server inheriting the invoking process's cgroup. See extdeps.cache.sccache sccache_stats_query_does_not_spawn_note. The conclusion is unchanged and if anything sharper: stats establishes nothing about supervision OR existence, so it may only ever be supplemental data read AFTER ownership is independently grounded. On srv3 2026-07-25 it printed a full stats table while the modeled unit had been `inactive (dead)` for six hours and no sccache process existed beforehand. Read through the verdict fold as it stood, that host observed DaemonStatsOk and therefore converged: the provisioning act would have reported success over a host with NO durable cache — a fabricated plausible output (§5), and the same shape as DaemonPathShadowed (a read-back that greens on evidence it produced rather than on the state the lane exists to establish). DaemonUnsupervised splits that conflation: 'a daemon answered' and 'the modeled supervisor is running it' are different states with different remedies, so they get different names rather than one Ok. The LIVE observation is STAGED exactly as the PATH-shadow one is — the supervisor read (`systemctl --user is-active`) is not emitted as hand-shell here, because this module's script side is an A5 `decompose` census row that may shrink but must not grow (review 42343); it lands with the typed-argv dissolve-on (#5828). The model carries the state and its refusal today; only the live producer is deferred. Construction covers the case this observation would catch on a host provisioned BY THIS LANE — build_cache_unit_daemon_runs_until_stopped makes the self-exiting unit unwritable — so the observation's standing job is the residue construction cannot reach: a host whose unit was stopped, masked, or replaced out of band." + +data build_cache_server_placement_doc: String = "WHY PLACEMENT IS A VERDICT AXIS AND NOT A DETAIL: extdeps.cache.sccache.sccache_server_process_facts cites that cache-miss compiles execute as CHILDREN OF THE SERVER PROCESS. So the server's cgroup is the cgroup every compile on the host runs in, and the server's caps bound every compile on the host — not just the caps of the job that issued them. A lazily auto-spawned server therefore silently exports one arbitrary job's memory caps onto the whole host's compile traffic, and SCCACHE_IDLE_TIMEOUT=0 keeps it there indefinitely. Answering --show-stats proves the server is RESPONSIVE; it proves nothing about WHERE it runs. Collapsing those two into one DaemonStatsOk arm is the DESIGN.md §5 state-space conflation, and it fabricates a plausible ProvisionConverged over a host whose compiles are being throttled and killed." + +data build_cache_placement_authority_is_elsewhere_note: String = "BuildCacheServerPlacement is IMPORTED from gunbc.build_cache_instance, not declared here. It used to be declared in this module with three arms (ServerUnitOwned | ServerLazySpawned | ServerPlacementUnknown). It moved because placement is derived from an observation of a DEPLOYED INSTANCE and this fold is a consumer of it; leaving a copy here would be two definitions of 'correctly placed' that can drift (DESIGN section 3). The arms also changed shape with the move, and the renames are honesty rather than churn: ServerLazySpawned named an INFERRED CAUSE (that the server was lazily auto-spawned) while what an observer can actually see is the current owner, so it is now ServerRunnerOwned carrying the slot and its lifecycle; and ServerUnitOwned became ServerManagedUnitOwned to say WHICH unit, since 'a unit owns it' is satisfied by any unit including the wrong one." type BuildCacheDaemonObservation - = DaemonStatsOk { stats_line: String } + = DaemonStatsOk { stats_line: String, placement: BuildCacheServerPlacement } | DaemonAbsent | DaemonUnresponsive { detail: NonEmptyStr } | DaemonMisconfigured { matched_fragment: String, detail: NonEmptyStr } @@ -57,6 +74,8 @@ type BuildCacheProvisionRefusal | RefusalCatalogUnknown | RefusalPathShadowed | RefusalDaemonUnsupervised + | RefusalServerRunnerOwned + | RefusalPlacementUnknown type BuildCacheProvisionVerdict = ProvisionConverged { catalog_id: CacheInterfaceId, stats_line: String } @@ -72,7 +91,7 @@ fn provision_build_cache_absorbing_fallback_widen_sketch( catalog_id: catalog_id, stats_line: "local-build-without-rustc-wrapper-widen-sketch", } - DaemonStatsOk { stats_line: line } => + DaemonStatsOk { stats_line: line, placement: _ } => ProvisionConverged { catalog_id: catalog_id, stats_line: line } DaemonUnresponsive { detail: d } => ProvisionRefused { cause: RefusalDaemonUnresponsive, reason: d } @@ -94,48 +113,81 @@ fn provision_build_cache_absorbing_fallback_widen_sketch( } } -fn build_cache_provision_gate_accepts( - verdict: BuildCacheProvisionVerdict, - observed: BuildCacheDaemonObservation, +data provision_gate_consumes_the_fold_note: String = "THE GATE ASKS THE AUTHORITY; IT DOES NOT HOLD A SECOND OPINION. It used to be a full hand-written traversal of BuildCacheDaemonObservation — and, inside the DaemonStatsOk arm, of BuildCacheServerPlacement — mapping each arm to the verdict shape it expected. That mapping is exactly what provision_build_cache_verdict and placement_verdict already compute, so the same fact was written twice and either copy could drift while both typechecked (review 46217; DESIGN sections 2 and 3). The drift is not hypothetical for a gate whose whole job is to reject widens: a placement arm added to the coproduct and handled in the fold but forgotten here would silently keep accepting whatever the gate's stale arm said. Consuming the fold means a new arm is impossible to forget, because there is nowhere left to forget it. What the gate still owns is the one judgement the fold cannot make for itself: WHICH PART of a verdict is contractual. Two verdicts agree when they agree on converged-versus-refused and, when refused, on the CAUSE — the located reason string is a diagnostic payload, not part of the contract, so comparing it would make every wording edit a gate failure while catching nothing a cause comparison misses." + +fn verdict_shape_matches( + actual: BuildCacheProvisionVerdict, + canonical: BuildCacheProvisionVerdict, ) -> Bool { - match observed { - DaemonAbsent => - match verdict { - ProvisionRefused { cause: _, reason: _ } => true - ProvisionConverged { catalog_id: _, stats_line: _ } => false - } - DaemonStatsOk { stats_line: _ } => - match verdict { + match actual { + ProvisionConverged { catalog_id: _, stats_line: _ } => + match canonical { ProvisionConverged { catalog_id: _, stats_line: _ } => true ProvisionRefused { cause: _, reason: _ } => false } - DaemonUnresponsive { detail: _ } => - match verdict { - ProvisionRefused { cause: RefusalDaemonUnresponsive, reason: _ } => true + ProvisionRefused { cause: actual_cause, reason: _ } => + match canonical { ProvisionConverged { catalog_id: _, stats_line: _ } => false - ProvisionRefused { cause: _, reason: _ } => false + ProvisionRefused { cause: canonical_cause, reason: _ } => + discriminant(v: actual_cause) == discriminant(v: canonical_cause) } - DaemonMisconfigured { matched_fragment: _, detail: _ } => - match verdict { - ProvisionRefused { cause: RefusalDaemonMisconfigured, reason: _ } => true - ProvisionConverged { catalog_id: _, stats_line: _ } => false - ProvisionRefused { cause: _, reason: _ } => false + } +} + +fn build_cache_provision_gate_accepts( + catalog_id: CacheInterfaceId, + verdict: BuildCacheProvisionVerdict, + observed: BuildCacheDaemonObservation, +) -> Bool { + verdict_shape_matches( + actual: verdict, + canonical: provision_build_cache_verdict(catalog_id: catalog_id, observed: observed), + ) +} + +fn placement_verdict( + catalog_id: CacheInterfaceId, + stats_line: String, + placement: BuildCacheServerPlacement, +) -> BuildCacheProvisionVerdict { + match placement { + ServerManagedUnitOwned { unit: _ } => + ProvisionConverged { catalog_id: catalog_id, stats_line: stats_line } + ServerRunnerOwned { slot: s, lifecycle: lc } => + ProvisionRefused { + cause: RefusalServerRunnerOwned, + reason: concat( + concat("host_build_cache_provision: the configured cache endpoint is owned by a RUNNER-SLOT process, not by the managed unit. Slot index ", to_string(s.slot_index)), + concat(runner_slot_lifecycle_refusal_clause(lifecycle: lc), " Cache-miss compiles execute as children of the endpoint owner, so that slot's memory ceiling bounds every compile on this host. Refusing: responsive is not placed."), + ) as NonEmptyStr, } - DaemonPathShadowed { resolved: _, expected: _ } => - match verdict { - ProvisionRefused { cause: RefusalPathShadowed, reason: _ } => true - ProvisionConverged { catalog_id: _, stats_line: _ } => false - ProvisionRefused { cause: _, reason: _ } => false + ServerUnsupervised { cgroup: cg } => + ProvisionRefused { + cause: RefusalDaemonUnsupervised, + reason: concat("host_build_cache_provision: the endpoint owner is not the intended managed unit; observed cgroup ", concat(cg as String, ". Refusing rather than accepting an out-of-band supervisor.")) as NonEmptyStr, } - DaemonUnsupervised { unit: _, unit_state: _ } => - match verdict { - ProvisionRefused { cause: RefusalDaemonUnsupervised, reason: _ } => true - ProvisionConverged { catalog_id: _, stats_line: _ } => false - ProvisionRefused { cause: _, reason: _ } => false + ServerPlacementAbsent => + ProvisionRefused { + cause: RefusalDaemonAbsent, + reason: "host_build_cache_provision: no process holds the LISTEN on the configured endpoint; the instance is not running." as NonEmptyStr, + } + ServerPlacementUnknown { detail: d } => + ProvisionRefused { + cause: RefusalPlacementUnknown, + reason: concat("host_build_cache_provision: sccache server placement could not be observed; refusing rather than assuming unit-owned: ", d as String) as NonEmptyStr, } } } +fn runner_slot_lifecycle_refusal_clause(lifecycle: RunnerSlotLifecycle) -> String { + match lifecycle { + DesiredRunnerSlot => + " on a slot the fleet still desires, so BIRTH ORDERING failed: a runner was able to run before a managed cache endpoint existed, and its client created the server inside its own cgroup." + RetiredRunnerSlot => + " on a RETIRED slot, so birth ordering AND retirement both failed: the slot's unit was disabled while its cgroup stayed computationally alive, still owning the endpoint and still parenting new compiler children under a ceiling nothing recycles." + } +} + fn provision_build_cache_verdict( catalog_id: CacheInterfaceId, observed: BuildCacheDaemonObservation, @@ -148,8 +200,8 @@ fn provision_build_cache_verdict( } ProvisionBuildCacheCatalogRealizable => match observed { - DaemonStatsOk { stats_line: line } => - ProvisionConverged { catalog_id: catalog_id, stats_line: line } + DaemonStatsOk { stats_line: line, placement: p } => + placement_verdict(catalog_id: catalog_id, stats_line: line, placement: p) DaemonAbsent => ProvisionRefused { cause: RefusalDaemonAbsent, @@ -217,6 +269,49 @@ fn daemon_absent_refuses_fail_closed() -> Bool { } } +data managed_compile_pool_slice_fixture: NonEmptyStr = gunbc_managed_compile_pool_slice + +fn stats_ok_lazy_spawned_refuses() -> Bool { + match provision_build_cache_verdict( + catalog_id: sccache_local_id, + observed: DaemonStatsOk { + stats_line: "compile_requests=42", + placement: ServerRunnerOwned { slot: RunnerSlotIdentity { host: "srv1", slot_index: 10 }, lifecycle: RetiredRunnerSlot }, + }, + ) { + ProvisionRefused { cause: RefusalServerRunnerOwned, reason: _ } => true + ProvisionRefused { cause: _, reason: _ } => false + ProvisionConverged { catalog_id: _, stats_line: _ } => false + } +} + +fn stats_ok_placement_unknown_refuses() -> Bool { + match provision_build_cache_verdict( + catalog_id: sccache_local_id, + observed: DaemonStatsOk { + stats_line: "compile_requests=42", + placement: ServerPlacementUnknown { detail: "cgroup read unavailable" }, + }, + ) { + ProvisionRefused { cause: RefusalPlacementUnknown, reason: _ } => true + ProvisionRefused { cause: _, reason: _ } => false + ProvisionConverged { catalog_id: _, stats_line: _ } => false + } +} + +fn stats_ok_unit_owned_converges() -> Bool { + match provision_build_cache_verdict( + catalog_id: sccache_local_id, + observed: DaemonStatsOk { + stats_line: "compile_requests=42", + placement: ServerManagedUnitOwned { unit: managed_compile_pool_slice_fixture }, + }, + ) { + ProvisionConverged { catalog_id: id, stats_line: _ } => id == sccache_local_id + ProvisionRefused { cause: _, reason: _ } => false + } +} + fn misconfigured_matches_cited_fragment() -> Bool { string_contains( s: sccache_fatal_error_log_fragment, diff --git a/dag/gunbc/host_layout.dag b/dag/gunbc/host_layout.dag index 63d14ae57e4..25b910a5aa7 100644 --- a/dag/gunbc/host_layout.dag +++ b/dag/gunbc/host_layout.dag @@ -66,3 +66,6 @@ fn srv2_host_layout() -> GunbcHostLayout { controller_repo: srv2_dashboard_controller_repo, } } +data managed_compile_pool_slice_note: String = "The ONE authority for the managed compile-pool slice unit name, homed here for the same reason as the paths above: it is a fact about how a HOST is laid out, not about any one consumer of it. Its live consumers are gunbc.build_cache_instance `ci_cache_instance` and `session_cache_instance`, which carry it as each deployed instance's `intended_compile_pool`, plus the `managed_compile_pool_slice_fixture` row in gunbc.host_build_cache_provision. STATE, stated so this row is not read as a description of the fleet: NOTHING PROVISIONS THIS SLICE YET. No observed host has it — the 2026-08-01 placement receipt found every CI cache endpoint owned by a runner-slot process, never by a managed unit inside a compile pool — so the name is declared intent that an observation is checked against, and gunbc.fleet_host_budget's `CompilePoolInRunnerSlots` correspondingly derives a ZERO pool today. CORRECTED 2026-08-01 (review 46182, and the review understated it — all three of this note's claims had gone stale in one edit, which is the §3 citation class this repo keeps paying for): it previously cited `ServerUnitOwned.slice_unit` in gunbc.host_build_cache_provision, said the emitted shell in gunbc.host_build_cache_provision_script greps the observed cgroup for this value, and justified its homing by an import cycle between those two consumers. `ServerUnitOwned` no longer exists (it is `ServerManagedUnitOwned { unit }` in gunbc.build_cache_instance, and placement is decided against the instance's `intended_unit` — a service, not this slice); the script carries no compile-pool reference at all, because the branch's emitted placement probe was deliberately not landed (that module is an A5 census row whose emitted shell may shrink but not grow); and with that consumer absent the two-consumer drift hazard and the cycle rationale describe a mechanism that is not in the tree." + +data gunbc_managed_compile_pool_slice: NonEmptyStr = "gunbc-compile-pool.slice" as NonEmptyStr diff --git a/dag/gunbc/live_deploy/apply.dag b/dag/gunbc/live_deploy/apply.dag index 332ed4b8ab9..5b643cf8e51 100644 --- a/dag/gunbc/live_deploy/apply.dag +++ b/dag/gunbc/live_deploy/apply.dag @@ -4,8 +4,12 @@ import gunbc.live_deploy.spec { DeploymentSpec, DeploymentDependencyStep, deployment_spec_srv1, + deployment_spec_srv1_green, + deployment_slot_spec, + deployment_route_spec, deployment_plan_listen_port, } +import std.types { String } import gunbc.ci_deploy_access { ci_deploy_srv1_access, ci_deploy_srv1_operator_access, @@ -47,6 +51,110 @@ import gunbc.live_deploy.readiness { } import std.realization_reconcile { Reconciliation, reconciliation_converged, Converged, NotConverged } import std.process { ProcessExit, ExitSuccess, exit_failure } +import extdeps.tailscale.serve { + ServeRootMount, + tailscale_serve_proxy_url, + tailscale_serve_status_json_argv, + tailscale_serve_status_proxy_targets, +} +import extdeps.tailscale.serve +import gunbc.typed_argv_exec { + typed_argv_exec_over_ssh, + TypedArgvExecConverged, + TypedArgvExecRefused, +} + +type BlueGreenSlotSelection + = BlueGreenSlotsSelected { active: DeploymentSpec, idle: DeploymentSpec } + | BlueGreenSlotSelectionRefused { reason: String } + +type TailscaleServeStatusRead + = TailscaleServeStatusObserved { json: String } + | TailscaleServeStatusReadRefused { reason: String } + +fn srv1_blue_green_slot_selection(status_json: String) -> BlueGreenSlotSelection { + let blue = deployment_spec_srv1() + let green = deployment_spec_srv1_green() + let proxies = tailscale_serve_status_proxy_targets(status_json: status_json, mount: ServeRootMount) + let blue_proxy = tailscale_serve_proxy_url(endpoint: blue.names.tailscale_serve_endpoint) + let green_proxy = tailscale_serve_proxy_url(endpoint: green.names.tailscale_serve_endpoint) + let blue_hits = count(filter(proxies, p => p == blue_proxy)) + let green_hits = count(filter(proxies, p => p == green_proxy)) + if count(proxies) != 1 { + BlueGreenSlotSelectionRefused { + reason: concat("blue/green cutover refused: root Tailscale Serve endpoint did not carry exactly one Proxy; observed-count=", to_string(count(proxies))) + } + } else if blue_hits == 1 && green_hits == 0 { + BlueGreenSlotsSelected { active: blue, idle: green } + } else if green_hits == 1 && blue_hits == 0 { + BlueGreenSlotsSelected { active: green, idle: blue } + } else { + BlueGreenSlotSelectionRefused { + reason: "blue/green cutover refused: root Tailscale Serve Proxy names neither declared srv1 slot" + } + } +} + +fn tailscale_serve_status_read(transport: HostEffectTransport) -> TailscaleServeStatusRead { + let argv = concat(["tailscale"], tailscale_serve_status_json_argv()) + match transport { + LocalShell => { + let result = tailscale.Serve.StatusJson() + TailscaleServeStatusObserved { json: result.stdout } + } + SshShell { ssh_host } => + match typed_argv_exec_over_ssh(host: ssh_host, argv: argv) { + TypedArgvExecConverged { result } => + if result.success { + TailscaleServeStatusObserved { json: result.stdout } + } else { + TailscaleServeStatusReadRefused { reason: concat("tailscale serve status --json over ssh failed: ", result.stderr) } + } + TypedArgvExecRefused { reason } => TailscaleServeStatusReadRefused { reason: reason } + } + EmitArtifactThenThinRun { bootstrap: _, invocation: _ } => + TailscaleServeStatusReadRefused { reason: "blue/green slot observation refused EmitArtifactThenThinRun transport" } + } +} + +data blue_green_cutover_order_note: String = "Operator-signed 2026-08-01: straight to blue/green, no socket-activation interim. Tailscale's currently routed root Proxy selects active and idle from the two declared HostDashboardInstance slots. The candidate mutation is the slot footprint only; its existing readiness path polls /healthz until the digest proves it serves THIS source tree and then performs the deeper roadmap asset readback. Only after both converge does the route-only spec replay the scoped root endpoint toward the candidate. Old-slot teardown is also route-free, because `off` addresses endpoint identity rather than backend and would otherwise remove the route just installed. Every pre-flip refusal returns before route mutation, leaving the old process mapped and serving; post-flip teardown failure leaves the new process mapped, so absence is not a failure arm." + +fn live_deploy_blue_green_cutover_with_selection( + selection: BlueGreenSlotSelection, + transport: HostEffectTransport, + access: DeployAccess, +) -> Reconciliation { + match selection { + BlueGreenSlotSelectionRefused { reason } => + NotConverged { reason: reason, applied: live_deploy_preflight_intent(access: access) } + BlueGreenSlotsSelected { active, idle } => { + let candidate = live_deploy_apply_via_transport( + spec: deployment_slot_spec(spec: idle), + transport: transport, + access: access, + ) + if !reconciliation_converged(r: candidate) { + candidate + } else { + let flip = live_deploy_apply_mutation_via_transport( + spec: deployment_route_spec(spec: idle), + transport: transport, + access: access, + ) + if !reconciliation_converged(r: flip) { + flip + } else { + let teardown = live_deploy_retract_via_transport( + spec: deployment_slot_spec(spec: active), + transport: transport, + access: access, + ) + if reconciliation_converged(r: teardown) { flip } else { teardown } + } + } + } + } +} fn live_deploy_preflight_intent(access: DeployAccess) -> HostEffectIntent { HostEffectIntent { @@ -295,11 +403,16 @@ fn live_deploy_access_preflight() -> Reconciliation Reconciliation { - live_deploy_apply_via_transport( - spec: deployment_spec_srv1(), - transport: access.transport, - access: access - ) + match tailscale_serve_status_read(transport: access.transport) { + TailscaleServeStatusReadRefused { reason } => + NotConverged { reason: reason, applied: live_deploy_preflight_intent(access: access) } + TailscaleServeStatusObserved { json } => + live_deploy_blue_green_cutover_with_selection( + selection: srv1_blue_green_slot_selection(status_json: json), + transport: access.transport, + access: access, + ) + } } fn live_deploy_retract_srv1_with_access(access: DeployAccess) -> Reconciliation { diff --git a/dag/gunbc/live_deploy/readiness.dag b/dag/gunbc/live_deploy/readiness.dag index 31802335707..a6a6f309b4f 100644 --- a/dag/gunbc/live_deploy/readiness.dag +++ b/dag/gunbc/live_deploy/readiness.dag @@ -84,6 +84,13 @@ fn service_ready_apply_effect() -> ServiceReadyApplyEffect { } } +fn service_ready_apply_effect_for_deploy(spec: DeploymentSpec) -> ServiceReadyApplyEffect { + ServiceReadyApplyEffect { + unit: spec.service.unit_name, + endpoint: live_deploy_service_ready_endpoint_authority, + } +} + fn healthz_effective_read_from_get(success: Bool, body: String, error: String) -> HealthzEffectiveRead { if success { HealthzEffectiveRead { body: body, probe_error: "" } @@ -212,17 +219,24 @@ fn show_healthz_read_fixture_healthy() -> ShowEffectiveRead +) -> Reconciliation { + reconcile_service_ready_read_for_effect(read: read, effect: service_ready_apply_effect()) +} + +fn reconcile_service_ready_read_for_effect( + read: ShowEffectiveRead, + effect: ServiceReadyApplyEffect, ) -> Reconciliation { match read { ReadAbsent { reason: why } => reconcile_grounded( - apply_effect: service_ready_apply_effect(), + apply_effect: effect, show_read: read, grounding: DoesNotGround { reason: why } ) ReadObserved { value: observed } => reconcile_grounded( - apply_effect: service_ready_apply_effect(), + apply_effect: effect, show_read: read, grounding: ground_service_ready_from_healthz(read: observed) ) @@ -234,12 +248,13 @@ fn live_deploy_service_ready_once_for_deploy( transport: HostEffectTransport, access: DeployAccess, ) -> Reconciliation { - reconcile_service_ready_read( + reconcile_service_ready_read_for_effect( read: show_healthz_read_for_deploy( spec: spec, transport: transport, access: access, - ) + ), + effect: service_ready_apply_effect_for_deploy(spec: spec), ) } @@ -248,6 +263,7 @@ fn live_deploy_service_ready_once() -> Reconciliation String { @@ -259,7 +275,7 @@ fn capture_unit_diagnosis( unit_status_capture_text( capture: systemctl_status_read( transport: transport, - unit: live_deploy_service_ready_unit_authority, + unit: spec.service.unit_name, ) ) } @@ -269,9 +285,9 @@ type ServiceReadyPollOutcome = PollSettled { result: Reconciliation } | PollRefusedAtBound { refusal: ServiceNotReady } -fn poll_bound_refusal(last_probe: String) -> ServiceNotReady { +fn poll_bound_refusal(spec: DeploymentSpec, last_probe: String) -> ServiceNotReady { ServiceNotReady { - unit: live_deploy_service_ready_unit_authority, + unit: spec.service.unit_name, endpoint: live_deploy_service_ready_endpoint_authority, waited: live_deploy_service_ready_poll_bound, last_probe: last_probe, @@ -282,7 +298,7 @@ fn poll_bound_refusal(last_probe: String) -> ServiceNotReady { fn refusal_to_reconciliation(refusal: ServiceNotReady) -> Reconciliation { NotConverged { reason: service_not_ready_reason(refusal: refusal), - applied: service_ready_apply_effect(), + applied: ServiceReadyApplyEffect { unit: refusal.unit, endpoint: refusal.endpoint }, } } @@ -295,7 +311,7 @@ fn live_deploy_poll_service_ready_outcome_after_attempts( ) -> ServiceReadyPollOutcome { let max_attempts = live_deploy_service_ready_poll_max_attempts() if attempt >= max_attempts { - PollRefusedAtBound { refusal: poll_bound_refusal(last_probe: last_probe) } + PollRefusedAtBound { refusal: poll_bound_refusal(spec: spec, last_probe: last_probe) } } else { let probe = live_deploy_service_ready_once_for_deploy( spec: spec, @@ -316,7 +332,7 @@ fn live_deploy_poll_service_ready_outcome_after_attempts( ) } else { PollRefusedAtBound { - refusal: poll_bound_refusal(last_probe: "sleep between health probes failed") + refusal: poll_bound_refusal(spec: spec, last_probe: "sleep between health probes failed") } } } @@ -369,6 +385,7 @@ fn live_deploy_poll_until_service_ready_for_deploy( refusal: service_not_ready_with_captured_diagnosis( refusal: refusal, text: capture_unit_diagnosis( + spec: spec, transport: transport, access: access, ) diff --git a/dag/gunbc/live_deploy/spec.dag b/dag/gunbc/live_deploy/spec.dag index f0cb686f463..97ba3fa6afe 100644 --- a/dag/gunbc/live_deploy/spec.dag +++ b/dag/gunbc/live_deploy/spec.dag @@ -35,6 +35,7 @@ import gunbc.host_layout { import gunbc.roadmap_dashboard_instance { HostDashboardInstance, srv1_live_dashboard_instance, + srv1_green_dashboard_instance, } import extdeps.http.server { HttpServerListenConfig } import product.network_topology { NetworkEndpoint } @@ -180,6 +181,22 @@ fn srv1_live_deployment_names(instance: HostDashboardInstance) -> DeploymentName } } +data srv1_green_deployment_names_note: String = "The green slot owns distinct unit and tree-sync names, but it deliberately shares production's ROOT Tailscale endpoint. Endpoint identity excludes backend (extdeps.tailscale.serve): changing 8080 to 18080 is a re-point of one owned route, not a second route. That distinction is load-bearing for blue/green — the candidate footprint stays invisible until readiness proves THIS tree, then one scoped endpoint replay performs the cutover." + +fn srv1_green_deployment_names(instance: HostDashboardInstance) -> DeploymentNames { + let derived = derived_deployment_names(instance: instance) + DeploymentNames { + unit_name: derived.unit_name, + tailscale_serve_endpoint: TailscaleServeEndpoint { + https_port: tailscale_default_https_port, + mount: ServeRootMount, + backend: to_string(instance.listen_port) as NonEmptyStr, + }, + tree_sync_unit_name: derived.tree_sync_unit_name, + tree_sync_env_path: derived.tree_sync_env_path, + } +} + data gunbc_tree_sync_note: String = "gunbc-tree-sync.service is the GunbcSourceTree member's sync TRANSPORT, not an independent member: a oneshot unit running AS the service user (the tree's owner writes its own tree — ownership by construction, no chown), installed/started by the deploy through the granted install+systemctl channel because the ci-runner principal holds no rsync grant and no write access to the service-user-owned tree (emit_deploy_principal_note). Its EnvironmentFile carries the runner checkout path (varies per runner slot), written by the apply at run time. Both files are owned host state and are torn down with the tree in the GunbcSourceTree teardown arm." data tree_sync_was_a_host_singleton_note: String = "THE SYNC TRANSPORT WAS ONE SET OF FILES FOR THE WHOLE HOST, and it never appeared in the five-member footprint the twin witness compares. gunbc-tree-sync.service and /etc/gunbc-tree-sync.env were fixed literals, so a twin apply OVERWROTE both with its own tree destination and restarted the shared unit — pointing production's sync at the lab's tree — and a twin retract DELETED both, disabling production's sync entirely. Neither effect is a path in DeploymentArtifactStep, so a claim over declared members could not see either (review, 2026-07-30).\\n\\nThey are derived from the deployment identity now, for the same reason the unit and binary are: the collision becomes unwritable rather than unlikely, because two deployments cannot produce one filename without sharing an identity the registry already forbids. gunbc_tree_sync_note calls these files owned host state torn down with the tree; that description is only true once they are per-deployment, and it was describing an intent the literals contradicted.\\n\\nThe names still derive from the SERVICE unit name rather than from instance_id directly, so srv1-live's migration-debt unit name carries through to its sync files and its emitted paths stay byte-identical." @@ -314,6 +331,40 @@ fn deployment_spec_srv1() -> DeploymentSpec { ) } +fn deployment_spec_srv1_green() -> DeploymentSpec { + deployment_spec_for( + instance: srv1_green_dashboard_instance(), + target: deployment_srv1_target(), + names: srv1_green_deployment_names(instance: srv1_green_dashboard_instance()), + ) +} + +data blue_green_spec_partition_note: String = "Slot footprint and public route are two effects with one required edge between them: candidate footprint -> healthz digest readiness -> route replay. The old DeploymentSpec roster fused them, so LiveDeployApply replayed Tailscale before the post-mutation readiness gate and recreated the 502 window on the candidate port. These projections do not mint new operations; they partition the existing modeled members. Teardown uses the slot projection too, because replaying `off` from the old spec after the flip addresses the SAME root endpoint and would remove the newly installed route." + +fn deployment_slot_spec(spec: DeploymentSpec) -> DeploymentSpec { + DeploymentSpec { + target: spec.target, + steps: filter(spec.steps, s => match s { + ArtifactStep { step: art } => art.kind != TailscaleServeMapping + Dependency { step: _ } => true + }), + service: spec.service, + names: spec.names, + } +} + +fn deployment_route_spec(spec: DeploymentSpec) -> DeploymentSpec { + DeploymentSpec { + target: spec.target, + steps: filter(spec.steps, s => match s { + ArtifactStep { step: art } => art.kind == TailscaleServeMapping + Dependency { step: _ } => false + }), + service: spec.service, + names: spec.names, + } +} + fn deployment_plan_host_identity(spec: DeploymentSpec) -> String { spec.target.host.identity } diff --git a/dag/gunbc/merge_admission.dag b/dag/gunbc/merge_admission.dag index 394c5894e0b..a91781f33dc 100644 --- a/dag/gunbc/merge_admission.dag +++ b/dag/gunbc/merge_admission.dag @@ -183,7 +183,9 @@ type CheckCoverage data check_coverage_subject_universe_note: String = "CHECK COVERAGE IS A TOTAL CLASSIFICATION OVER AN EXPLICIT SUBJECT UNIVERSE (operator mandate 2026-08-01). The subject is (required PR head, required gate-roster identity, nonempty required gate population). No check runs is CheckCoverageAbsent, never an empty list that all() can call green. A run still executing is Running. A dependency stop is Stopped and carries both the completed and UNEXECUTED populations plus a cause, so batch-1 drift stopping the witness batches cannot masquerade as a witness verdict. Complete is the only arm admission examines for success. Stale carries both observed and required identities; it is not repaired by relabeling the observation. Adding a coverage state requires a new coproduct arm and turns every exhaustive consumer red until classified — unclassified is structurally unavailable." -data check_coverage_external_consumer_boundary_note: String = "BOUNDARY, NOT AN END-TO-END CLOSURE CLAIM. This module is the repository's admission authority and now makes the correct semantics representable and executable. The live fail-open observed 2026-08-01 was in the external dashboard readout: it reported no failing or pending checks with ok=true when the healed head had NO run. That consumer lives outside this repository and is not changed here. Until it conforms to classify_check_coverage_admission, the operational rule is stricter than its readout: never merge unless an executing run explicitly names the current PR head and reaches every required witness; an auto-heal head with no subsequent run is untested." +data check_coverage_external_consumer_boundary_note: String = "BOUNDARY, NOT AN END-TO-END CLOSURE CLAIM. This module is the repository's admission authority and now makes the correct semantics representable and executable. The live fail-open observed 2026-08-01 was in the external dashboard readout: it reported no failing or pending checks with ok=true when the healed head had NO run. That consumer is ctrl session-dashboard: scripts/session-dashboard/lib/summarize_checks.mjs supplies the check classification consumed by pr_merge_ready_digest.mjs and server.mjs. It lives outside this repository and is not changed here. Until it conforms to classify_check_coverage_admission, the operational rule is stricter than its readout: never merge unless an executing run explicitly names the current PR head and reaches every required witness; an auto-heal head with no subsequent run is untested." + +data check_coverage_external_consumer_adoption_receipt_in_flight: String = "ADOPTION IN FLIGHT, NOT COMPLETE (observed 2026-08-01). gunb-ai/ctrl PR #1922, Adopt gunbc exact-head CI admission semantics in dashboard checks, is OPEN, non-draft, and mergeStateStatus CLEAN. It maps CANCELLED runs to superseded rather than failed, an empty check rollup to none rather than passing, and permits the merge gate only when checksState is passing. This receipt closes only after #1922 is merged AND the live session-dashboard consumer is observed reading those semantics. gunbc records that external obligation but does not own or claim completion of the ctrl change. The repeated cancelled-run false negatives observed while validating gunbc PR #7544 are the live symptom this adoption removes." type CheckCoverageAdmissionVerdict = CheckCoverageAdmitted diff --git a/dag/gunbc/merge_admission_produce.dag b/dag/gunbc/merge_admission_produce.dag index c51ab474571..12928fd2054 100644 --- a/dag/gunbc/merge_admission_produce.dag +++ b/dag/gunbc/merge_admission_produce.dag @@ -14,6 +14,7 @@ import gunbc.cli_invoke { source_roots_shell_flags, } import extdeps.git +import extdeps.git.inspect import extdeps.github.checks { CheckConclusion, check_conclusion_wire_label, @@ -248,7 +249,7 @@ fn merge_target_tree_hash_show_tree_refusal_cause(stderr: String) -> String { } fn observe_merge_target_tree_hash(ref: GitRef) -> MergeTargetTreeHashObservation { - let result = git.Core.ShowTree(ref: ref) + let result = git.Inspect.ShowTree(ref: ref) if !result.success { MergeTargetTreeHashRefused { ref: ref as String, @@ -259,7 +260,7 @@ fn observe_merge_target_tree_hash(ref: GitRef) -> MergeTargetTreeHashObservation if hash == "" { MergeTargetTreeHashRefused { ref: ref as String, - cause: "git.Core.ShowTree exited 0 but stdout tree hash was empty", + cause: "git.Inspect.ShowTree exited 0 but stdout tree hash was empty", } } else { MergeTargetTreeHashOk { hash: hash as ContentHash } @@ -269,7 +270,7 @@ fn observe_merge_target_tree_hash(ref: GitRef) -> MergeTargetTreeHashObservation fn merge_target_tree_hash_refusal_reason(prefix: String, ref: String, cause: String) -> String { concat( - concat(prefix, ": git.Core.ShowTree refused for ref "), + concat(prefix, ": git.Inspect.ShowTree refused for ref "), concat(ref, concat(": ", cause)) ) } diff --git a/dag/gunbc/observation_seed_render.dag b/dag/gunbc/observation_seed_render.dag index 89992e251f7..86ea5460888 100644 --- a/dag/gunbc/observation_seed_render.dag +++ b/dag/gunbc/observation_seed_render.dag @@ -486,7 +486,7 @@ fn seed_peak_rss_line(label: NonEmptyStr, rss_bytes: Nat, rss_available: Bool, e ) } -data seed_shell_effect_note: String = "Shell host-effect projection (operator live-log 2026-07-25 acceptance): subjects are NAMED INTENTS — the typed service.op key (`git.Core.HeadCommit`, `shell.Env.Get`), never raw `$ argv`. Argv is demoted to Failed.error (`$ (exit=N)`, whitespace-collapsed) and the Verbose/title channel. Routine Begin/Concluded\{Done\} are Ambient (ShellTrace-gated; Suppressed at Normal so scaffolding collapses); failing Concluded\{Failed\} is Anomaly (OutsideGroup by law 4), gated by divergence disposition alone — channel Suppressed must never silence a failure. Hot-path mirrors in v1_interpreter; oracle RED keeps them honest." +data seed_shell_effect_note: String = "Shell host-effect projection (operator live-log 2026-07-25 acceptance): subjects are NAMED INTENTS — the typed service.op key (`git.Inspect.HeadCommit`, `shell.Env.Get`), never raw `$ argv`. Argv is demoted to Failed.error (`$ (exit=N)`, whitespace-collapsed) and the Verbose/title channel. Routine Begin/Concluded\{Done\} are Ambient (ShellTrace-gated; Suppressed at Normal so scaffolding collapses); failing Concluded\{Failed\} is Anomaly (OutsideGroup by law 4), gated by divergence disposition alone — channel Suppressed must never silence a failure. Hot-path mirrors in v1_interpreter; oracle RED keeps them honest." fn seed_shell_subject(intent: NonEmptyStr) -> ObservationSubject { ObservationSubject { segments: [ PhaseSegment { name: intent } ] } diff --git a/dag/gunbc/plans/dag_v2_defork_audit.dag b/dag/gunbc/plans/dag_v2_defork_audit.dag index 1eeb50e4225..2d32a82f789 100644 --- a/dag/gunbc/plans/dag_v2_defork_audit.dag +++ b/dag/gunbc/plans/dag_v2_defork_audit.dag @@ -17,7 +17,7 @@ fn dag_v2_defork_audit_body() -> List { alignments: [AlignNone, AlignNone, AlignNone, AlignNone, AlignNone, AlignNone], rows: [ row(cells: [cell(text: "**algebra**"), cell(text: "16 (13 differ; 3 byte-identical `Lattice`/`Magma`/`Ordering`)"), cell(text: "same concept, divergent **encoding** (dag flat records vs v2 compositional/coproduct)"), cell(text: "**LIVE — 75 entries**"), cell(text: "grounding (Root-A: v2 coproduct authority RULED 2026-06-22)"), cell(text: "cant-unify-yet, **has path** (de-fork-resolved, not exempted)")]), - row(cells: [cell(text: "**nat**"), cell(text: "1 (`Nat`: `= CommutativeSemiring` vs `= Zero \\| Succ`)"), cell(text: "same concept, divergent **model**"), cell(text: "**LIVE — 138 floor entries** (946 scanned; was 4 @ 351, 2026-06-23)"), cell(text: "grounding (#5428 numeric tower; escalated smart-ant-466)"), cell(text: "cant-unify-yet, has path; BLOCKED + LAST · census nat-grounding-unification-design.md")]), + row(cells: [cell(text: "**nat**"), cell(text: "1 (`Nat`: `= CommutativeSemiring` vs `= Zero \\| Succ`)"), cell(text: "same concept, divergent **model** (semiring alias vs Peano coproduct — representational, not nominal)"), cell(text: "**LIVE — 138 floor closures** (946 scanned; was 4 @ 351) — **risk measure, not a task count** (see §2A note)"), cell(text: "grounding (#5428 numeric tower; census nat-grounding-unification-design.md)"), cell(text: "**DESIGN READY** · IMPLEMENTATION LARGE/ATOMIC · SEQUENCED WITH integer+float repoint (not last)")]), row(cells: [cell(text: "**effects**"), cell(text: "3 (`EffectShape`/`KeySource`/`CreateCause`; `EffectShape` body re-modeled on a different axis)"), cell(text: "divergent **axis** (operation-kind vs idempotency-class)"), cell(text: "latent (0)"), cell(text: "grounding"), cell(text: "cant-unify-yet, has path (axis-authority decision)")]), row(cells: [cell(text: "**float**"), cell(text: "3 (2 differ; 1 byte-identical `Float = Float64`)"), cell(text: "same concept, divergent encoding"), cell(text: "latent (0)"), cell(text: "grounding (#5428)"), cell(text: "cant-unify-yet, has path")]), row(cells: [cell(text: "**integer**"), cell(text: "14 (11 differ only on `MachineWidth<8>` vs ``; 3 byte-identical)"), cell(text: "same concept, near-identical (token diff)"), cell(text: "latent (0)"), cell(text: "grounding (#5428)"), cell(text: "cant-unify-yet, has path (mechanical once the tower lands)")]), @@ -27,6 +27,7 @@ fn dag_v2_defork_audit_body() -> List { row(cells: [cell(text: "**node**"), cell(text: "**0** (dag v1-only `compiler_inductive_fields` vs v2 Node substrate)"), cell(text: "divergent concept, **no name overlap**"), cell(text: "none (0 shared)"), cell(text: "v1-artifact name-collision"), cell(text: "dissolve-on v1-delete; **0 type-name collisions → exemption vacuous for the guard**")]), ], }, + p(text: "**Co-occurrence closure counts (algebra 75, nat 138):** each integer counts floor `*_test.dag` **import closures** where both `std.` and `v2.std.` are reachable — a **risk and closure-population measure** for the LIVE fail-open pair, **not** a migration task count or work estimate. New floor entries and tests can grow these numbers legitimately; **do not** ratchet on the integer (gate the unsafe facts instead — nat census §7.6 in [nat-grounding-unification-design.md](nat-grounding-unification-design.md)). Until the fork dissolves, mixed closures silently shadow record-with-record (benign today for nat; coproduct-variant-drop for algebra)."), p(text: "**Confirms parent's derived distribution; cant-unify-yet == \{node, coercion\} exactly; flag-ANY +\n\{node,coercion\} HOLDS.** Two scope refinements (strengthen, do not refute the ruling):"), ol(items: [ li(text: "**The \{node,coercion\} exemption is VACUOUS for the type-name guard.** node/coercion (and logic,\nand now verification) share **zero** type names — only the *basename*. The guard keys on a shared\nunqualified **type name** within one closure, so it never fires on them; their de-fork is a\n*module-basename* rename (Route-C / v1-delete), a different surface. The guard needs no\n\{node,coercion\} roster entry to land green — keeping it would be a dead (non-firing) exemption,\nitself a §5 smell. Recommend marking the roster entry explicitly \"Route-C basename, non-firing\"\nor dropping it from the guard model."), @@ -78,7 +79,7 @@ fn dag_v2_defork_audit_body() -> List { rows: [ row(cells: [cell(text: "**algebra**"), cell(text: "16 abstract structures (`Magma`…`Field`, `FreeMonoid`, `Ordering`)"), cell(text: "no (the structures match)"), cell(text: "dag-only 19 = template/codegen machinery **+ `GroupCompletion`/`FieldOfFractions`**; v2-only 74 = **41 `_node`/`_type_node` substrate projections** + the list/fold framework (213 importers)"), cell(text: "numeric tower (`GroupCompletion` = `Int = GroupCompletion`) **+** model↔realization substrate reflection. Each side owns a different grounding.")]), row(cells: [cell(text: "**logic**"), cell(text: "**0**"), cell(text: "— (no overlap)"), cell(text: "dag-only = `Classical`/`classical_and/or/not`; v2-only = `Bool` + boolean-algebra instance + 6 node-projections + `BoolEncodingFact`/`BoolWidthFact`/`BoolPrimitiveFacts`"), cell(text: "Bool grounded into **bit-width** (model↔realization). Operator picks: genuinely different concepts (→ rename) **or** one boolean modeled twice (→ unify on v2's grounded).")]), - row(cells: [cell(text: "**nat**"), cell(text: "`Nat` (the name)"), cell(text: "yes (thin alias vs coproduct)"), cell(text: "dag = 4-decl `Nat = CommutativeSemiring`; v2 = 12-decl coproduct `Zero/Succ` + `nat_cata`/`nat_add`/`nat_mul`/`is_zero`/`nat_lte`/`nat_gte`/`NatAlgebraLawObligation`"), cell(text: "**#5428** grounded numeric tower (`Zero → Int(0)`, `Succ → Int(k+1)`). Escalated (smart-ant-466).")]), + row(cells: [cell(text: "**nat**"), cell(text: "`Nat` (the name)"), cell(text: "yes (thin alias vs coproduct)"), cell(text: "dag = 4-decl `Nat = CommutativeSemiring`; v2 = 12-decl coproduct `Zero/Succ` + `nat_cata`/`nat_add`/`nat_mul`/`is_zero`/`nat_lte`/`nat_gte`/`NatAlgebraLawObligation`"), cell(text: "**#5428** grounded numeric tower (`Zero → Int(0)`, `Succ → Int(k+1)`). Census complete (2026-08-01): **DESIGN READY** — [nat-grounding-unification-design.md](nat-grounding-unification-design.md); prerequisites discharged (#6341 merged; coproduct keystone green). **IMPLEMENTATION LARGE/ATOMIC**, sequenced with integer+float repoint (not last).")]), row(cells: [cell(text: "**integer**"), cell(text: "14 = the **whole `Int`/`UInt` width tower** (`Int`, `Int8…128`, `UInt`, `UInt8…128`, `IntPlatform`, `UIntPlatform`)"), cell(text: "no"), cell(text: "v2-only +72 = the arithmetic ops grounded **on** the tower"), cell(text: "`Int = GroupCompletion` numeric tower.")]), row(cells: [cell(text: "**float**"), cell(text: "3 (`Float`, `Float32`, `Float64`)"), cell(text: "no"), cell(text: "v2-only +18 = algebraic-vs-bit-level ops"), cell(text: "the algebraic-vs-bit-level layer of the numeric tower + bit-width.")]), row(cells: [cell(text: "**effects**"), cell(text: "6 core (`EffectShape`, `KeySource`, `CreateCause`, +3 fns)"), cell(text: "**YES — the shared `EffectShape` body is re-modeled on a different axis**"), cell(text: "dag = **operation axis** (`ReadEffect`/`UpsertEffect`/`DeleteEffect`/`CreateEffect`/`AppendEffect`) + derivation (`derive_effect_shape`/`OperationEffect`/`compose_effects`); v2 = **idempotency-class axis** (`IsIdempotent(IdempotentShape)`/`IsBreaking(BreakingShape)`) + idempotency machinery + node-projections"), cell(text: "DESIGN §4 \"idempotency dissolved from an `idempotent:Bool` flag into the `EffectShape` variant\" — v2 is that grounding. Unification question: *which axis is the single authority, or are they orthogonal dimensions of one `EffectShape`?*")]), @@ -93,7 +94,7 @@ fn dag_v2_defork_audit_body() -> List { li(text: "**Step-4 data import:** `probe_selector` `Option`/`Optional` + `std.*` collision, so `dag/product/compute_fabric` imports cleanly into v2. **DONE (#5904).**"), ]), p(text: "**Deferred to v1-shrink (the not-a-fork renames):** `coercion`, `node`. v1-seed-coupled; ruled DEFER with the dissolution trigger *\"when v1's consumers of `dag/std/\{node,coercion\}` are removed\"* — at which point `node` self-dissolves (delete the dead file) and `coercion` shrinks to a v1-free disambiguation. Not dispatched until then."), - p(text: "**Grounding cluster — held for the operator** (a single-authority unification *design*, downstream of the numeric tower #5428 and the model↔realization grounding): `algebra`, `logic`, `nat`, `integer`, `float`, `effects`, `verification`. These are **not** dispatched as mechanical PRs. The grounded dag authority for each has to be *designed* before any fan-out can repoint to it (the same shape as the project's \"substrate migration precedes the ratchet, is not a path to it\"). `nat` is already escalated (smart-ant-466) and stays BLOCKED + LAST."), + p(text: "**Grounding cluster — held for the operator** (a single-authority unification *design*, downstream of the numeric tower #5428 and the model↔realization grounding): `algebra`, `logic`, `nat`, `integer`, `float`, `effects`, `verification`. These are **not** dispatched as mechanical PRs. The grounded dag authority for each has to be *designed* before any fan-out can repoint to it (the same shape as the project's \"substrate migration precedes the ratchet, is not a path to it\"). **`nat` census complete (2026-08-01):** per-concept design in [nat-grounding-unification-design.md](nat-grounding-unification-design.md) — **DESIGN READY**; prerequisites discharged (`algebra` `FreeMonoid` shadow #6341 merged; generic-alias coproduct keystone green). **IMPLEMENTATION LARGE/ATOMIC**, **SEQUENCED WITH integer+float repoint** (not last). Operational gate before the wave: bounded P3b PR2 overflow on `std.integer` lands first (#7511 merged; both lanes touch the numeric authority — one writer at a time). **Atomic wave file ownership (one push):** `dag/std/nat.dag` ← coproduct + Peano ops; delete dag semiring alias; `v2.std.nat` ← thin reimport + node-bound law roster only; `integer`/`float` repoint `GroupCompletion` to `std.nat.Nat`; all importer repoints in the same push (§5 auto-committer hazard)."), p(text: "**Hazard (atomicity):** the dashboard auto-committer can snapshot a multi-file rename/collapse mid-edit (a symbol deleted in file A while file B still calls it) → an internally-inconsistent intermediate commit caught by a *frozen* merge-sha CI run → phantom \"not found in scope\" red. `gh run rerun` cannot fix it (it re-merges the broken head); only a fresh consistent push. Stage every file of a rename/collapse together."), ThematicBreak, h2(text: "3b. OPERATOR RULING — grounding-cluster anchor (2026-06-22) — the brief"), diff --git a/dag/gunbc/readback_independence.dag b/dag/gunbc/readback_independence.dag index b7c9108c028..00eb5db2d93 100644 --- a/dag/gunbc/readback_independence.dag +++ b/dag/gunbc/readback_independence.dag @@ -114,7 +114,7 @@ data sccache_show_stats_probe: ReadBackProbe = ReadBackProbe { label: "sccache --show-stats", subject: "sccache daemon is running under the modeled unit", effect_on_subject: ProbeMayEstablish { - mechanism: "any sccache client invocation auto-starts a server when none is listening, so the probe can create the daemon it reports on (#7213 Finding 2, observed live on srv3 2026-07-25)", + mechanism: "`--show-stats` returns a fully-formed table whether or not a server exists, so the probe reports health it has not established (observed live on srv3 2026-07-25: a complete stats table while the modeled unit had been inactive for six hours). CORRECTED 2026-08-01: this row previously said the client invocation auto-starts a server and so creates the daemon it reports on. That mechanism is measured FALSE for sccache 0.15.0 — private-endpoint controls in both the UDS and TCP forms took the listener count 0 -> 0 across --show-stats, while a real compile took it 0 -> 1 (extdeps.cache.sccache sccache_stats_query_does_not_spawn_note). The probe still may-establish in the sense that matters here — it cannot be trusted to have observed an independently existing subject — but the reason is answer-without-subject, not subject-creation", }, } diff --git a/dag/gunbc/roadmap_authority.dag b/dag/gunbc/roadmap_authority.dag index c8674e75228..4b9be1231b9 100644 --- a/dag/gunbc/roadmap_authority.dag +++ b/dag/gunbc/roadmap_authority.dag @@ -6,6 +6,8 @@ import gunbc.roadmap_document { project_roadmap_to_markdown, } import std.types { ContentHash } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.seed_growth { SeedGrowthJustification } import gunbc.roadmap_model { RoadmapNode, RoadmapNodeIdentity, @@ -229,6 +231,15 @@ fn observation_raw_print_retirement_execution_contract() -> WorkItemExecutionCon ) } +data scm_p1_proof_kernel_contract_note: String = "THE SCM P1 CLOSING DISCRIMINATOR, authored only after the first P1 carrier made the node's whole completion condition executable. The validation is deliberately RED: its closed capability population is the roadmap node's own bar, and this first slice supplies only the bounded candidate-generation model check. Missing behavior is derived set difference, never a hand-maintained progress count; duplicate passing rows cannot erase it, and a false row remains missing. The contract therefore cannot green on zero progress and cannot confuse a modeled carrier with an executed proof kernel. It is enrolled QuarantineProbeExpectRed in gunbc.ci_layer_roots. Each later P1 slice adds an executing scenario check without weakening the aggregate. When the final capability greens, that same change removes the expect-red enrollment and promotes the witness to ordinary discovery ratchet." + +fn scm_p1_proof_kernel_execution_contract() -> WorkItemExecutionContract { + gunbc_claim_execution_contract( + entry: "dag/test/claim/source_integration_proof_kernel_acceptance_test.dag" as FilePath, + function: "witness_p1_proof_kernel_acceptance_contract_holds" as NonEmptyStr, + ) +} + data roadmap_with_execution_note: String = "The per-node execution binding, shaped as a COMBINATOR rather than a parameter on every constructor (the `sign` twin below is the precedent, and it already carries `execution: rn.execution` through untouched). Threading an execution argument through active/active_small/active_large/authored/derivable/ticket_row would edit seven constructors and every one of their ~46 call sites to express a fact that today holds for exactly one node — the churn a combinator avoids, and the reason `sign` is a combinator too. A node without this wrapper keeps ExecutionContractUnspecified, which is the fail-closed refusal roadmap_execution_contract_frontier_note declares, not a default anyone chose." data v1_lane_binding_survey_note: String = "A BINDING NAMES THE VALIDATION THAT CLOSES A NODE, so it can only be authored once that validation exists. A node without one stays ExecutionContractUnspecified, which is the honest fail-closed state rather than a gap: dispatch refuses with a typed diagnostic instead of admitting an environment for work whose completion nothing can check. Fabricating contracts to make a lane look dispatchable produces exactly the coverage-by-illusion tier — every node clickable, every Verify vacuous. The closing validation is written as part of the node's first slice; the contract follows the witness, never precedes it.\n\nTHE ROSTER IS NOT TRANSCRIBED HERE. This row previously carried a hand survey — 'two of six startable roots are bindable' plus the four names and why each lacked a validation — and it rotted exactly as a hand-maintained count must: two of the four it named (v1-test-hygiene-producer-retirement, pderive-typesafe-nullary-reflection) were accepted while the sentence still listed them as unbound, and the leading count was edited twice by unrelated changes. startable_nodes_missing_closing_contract() computes the same set from the nodes themselves, so read it there and delete nothing by hand. What stays here is the RULE, which does not change when the roster does.\n\nOne bound node was removed rather than added by this correction: see v1_test_migration_unbound_note. A contract whose validation cannot fail on zero progress is worse than no contract, because it reports done over undone work." @@ -367,15 +378,25 @@ fn edge(child: String, parent: String) -> RoadmapEdge { RoadmapEdge { child: nid(s: child), parent: nid(s: parent) } } -type SeedGrowthJustification { - reason: String - owning_dissolution_lane: RoadmapNodeId - trigger: String - current_boundary: String -} - fn observation_scoped_run_seed_growth_justification() -> SeedGrowthJustification { SeedGrowthJustification { + hand_authored_declarations: [ + DeclarationRef { + module_path: "v1_compiler.cli_run", + decl_name: "handle_run_with_options", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.cli_run", + decl_name: "capture_scoped_run_stderr", + field: WholeDeclaration + }, + DeclarationRef { + module_path: "v1_compiler.cli_run", + decl_name: "replay_scoped_run_stderr", + field: WholeDeclaration + } + ], reason: "Showing progress while a command runs has to reach the point where the old runner starts a program. It needs to know which piece of code was chosen, hold back error output until it can be shown in the right order, and report exactly one ending — and the rules for all of that live in the substrate, not in Rust.", owning_dissolution_lane: nid(s: "v1-hand-queue-drain"), trigger: "Move this reach-through into a described operation, then hollow out the corresponding part of the command-line tool so generated code does the work; the hand-written bridge is deleted at that point.", @@ -509,7 +530,7 @@ fn declared_roadmap_nodes() -> List { handback: "Return the layered landing-spine carrier, the tightened two-or-more-option continuation contract, twenty-two executing journey witnesses, the R1 compatibility projection edge, and the exact P1/P2/P3 work still deliberately absent." ) ), - active_sized( + with_execution(active_sized( identity: "rn_D33CBEADA91C3E8C10D8515CC4", id: "2-scm-p1-proof-kernel", owner: "source-intent", @@ -525,7 +546,7 @@ fn declared_roadmap_nodes() -> List { out_of_scope: "No source parser, quotient-delta edit lens, effect transport, executed target transition, writable SCM realization, command-line product, hosted service, arbitrary synthesis, unbounded proof search, or P2/P3 roadmap node.", handback: "Return the modeled bounded fragment, one public reconciliation fold, data-driven invariant and certificate carriers, the executing discriminating scenario corpus, the closing validation bound and green, and the exact P2 capture and P3 effect work still absent." ) - ), + ), e: scm_p1_proof_kernel_execution_contract()), active( identity: "rn_52ZXVWC7S94RYF4YJHZCN00GCS", @@ -829,15 +850,15 @@ fn declared_roadmap_nodes() -> List { identity: "rn_9WD4YB1TQK6RXA0PJM7N5GHVC2", id: "entry-graph-union-construction", owner: "ci-cost", - path: "docs/plans/inner-cost-lanes-scoping.md", + path: "docs/plans/entry-graph-union-slice2-typecheck-attribution.md", t: fields( - headline: "Prove repeated typechecking exists before building a shared entry graph", - boundary: "BANKED: slice 1 merged as #7483 (b5191426479ddec5f9a80b60dc6264ca168b42b6) and eliminated loading as the union graph's target: the corpus-wide edge index is already paid once per shared index. Selected closures repeat membership heavily, but that does not prove their typecheck computations are reusable. NEXT: attribute typecheck work per entry against one shared environment before choosing any implementation; implementation remains gated on that receipt and on fleet memory.", - displaced_cost: "Across six entries sharing one index, typecheck_compute grew 2.86x while the fixed loading row grew only 1.08x; selected closures repeat membership 35.9–38.4x by module count and 43.0–49.3x by bytes. Those are candidate bounds, not a saving: no receipt yet converts repeated membership into repeated computation.", - first_slice: "Measure per-entry typecheck attribution against one shared environment and report repeated typecheck compute / total typecheck compute. Close or recut the row if that decision ratio does not justify an architecture move.", - red_control: "The attribution must reconcile on one exclusive basis and must not infer computation from closure membership. Any later implementation requires identical verdicts plus fleet receipts showing no material increase in peak, hard backoff, or throttle wall at the real slot budget; local timing is not acceptance.", - out_of_scope: "No eviction redesign, and no forking of the entry-closure memo already in flight — sharing more while holding more live collides with retention, which is the axis the floor already fails on.", - handback: "The repeated-typecheck attribution receipt; an implementation proposal only if that receipt and fleet memory justify one." + headline: "Repeated typecheck attribution adjudicated — union construction no-go at N≤50", + boundary: "CLOSED (operator adjudication 2026-08-01, PR #7533). Slice 1 (#7483) eliminated loading as the union-graph target. Slice 2 measured per-entry typecheck attribution against one shared typed cache at N≤50 across disjoint production windows: decision_ratio=0 at every cell — the existing typed cache already enforces once-per-content-key, so a union/shared typed-computation graph is not justified by this hypothesis. Do not begin union construction. Per-entry assembly decomposition is a separate hypothesis with a new denominator.", + displaced_cost: "Across six entries sharing one index, typecheck_compute grew 2.86x while the fixed loading row grew only 1.08x; selected closures repeat membership 35.9–38.4x by module count and 43.0–49.3x by bytes. Slice 2 converted that membership duplication into a decision ratio and found zero repeated typecheck compute at every measured cell — the priced benefit of union construction is absent at N≤50.", + first_slice: "Delivered: slice-2 attribution matrix, controls in docs/plans/entry-graph-union-slice2-typecheck-attribution.md §C–F, operator no-go verdict §F.", + red_control: "Satisfied by execution: attribution reconciles on exclusive (entry,module_key) basis without inferring computation from closure membership; reorder preserves distinct-compute set; decision_ratio=0 at every measured N≤50 cell (perfect_sharing_yields_zero_decision_ratio).", + out_of_scope: "No union graph implementation, no eviction redesign, and no forking of the entry-closure memo already in flight — sharing more while holding more live collides with retention.", + handback: "docs/plans/entry-graph-union-slice2-typecheck-attribution.md §F verdict plus committed measurement receipts under docs/plans/receipts/entry-graph-union-slice2/; no implementation proposal (no-go)." ) ), active_small( @@ -1129,6 +1150,21 @@ fn declared_roadmap_nodes() -> List { handback: "The deletion, proof nothing was left behind, and a passing build on what the new generator produced." ) ), + active( + identity: "rn_7QVX2K4M9NBTR6HDJ0FZ8SWCPE", + id: "v1-interpreter-primitive-roster", + owner: "self-host", + path: "dag/gunbc/v1_interpreter_primitive_surface.dag", + t: fields( + headline: "Make the interpreter's primitive surface enumerable", + boundary: "Every interpreter dispatch arm -- free call, method, guard-predicated bridge, native special and the one service carve-out -- has one interpreter-local identity, dispatch form and authored spelling, enumerable independently of semantic primitive identity. Adding or deleting an implementation arm without changing the roster refuses.", + displaced_cost: "The interpreter's primitive surface had no roster anywhere, so nobody could ask which primitives it provides. That blocks the primitive-identity join at its fifth surface, and it blocks sequencing v1 deletion honestly: you cannot show nothing still needs a capability you cannot enumerate.", + first_slice: "R0 -- the roster is DERIVED FROM the dispatch. Each dispatch site in the seed expands its match arms and its roster rows from one macro token list, so the two cannot disagree; the sites that are not match arms at all stay declared, counted, each naming its own dissolution trigger. R1, the step after, inverts the direction: the .dag roster GENERATES the interpreter's local dispatch identity and lookup, at which point the denominator leaves Rust, the macro authorities delete, and the declared rows delete with them. R0 is mechanically preventive for the sites it covers and is not claimed as structural: the invalid state is unwritable within a site, but a whole new dispatch site can still be added in Rust without anyone noticing, and only R1 closes that.", + red_control: "A dispatch arm added without a roster row, and a roster row with no arm, must both be unwritable rather than caught afterwards. A spelling reached by two different arms within one form is unreachable code and has to be named rather than silently accepted. A roster that cannot represent an arm carrying no semantic primitive identity fails this node outright, because that population is exactly what the identity join must discover.", + out_of_scope: "Which semantic primitive each arm realizes belongs to the primitive-identity join, not here, and no row may carry an identity field. Generating the dispatch itself from the roster is the step after this one.", + handback: "The roster; three derived denominators -- dispatch sites, distinct arm identities, distinct authored spellings -- rather than one conflated count; row-grain enumeration provenance, since a row's provenance is a fact about that row and not about its dispatch form; a reconciliation of every quoted dispatch line in the seed file into either a row or a named non-primitive residue; a typed disposition for the Rust the scaffold adds, naming every symbol and the exact deletion condition; and witnesses asserting exact values rather than lower bounds, with a discriminating red for each detector." + ) + ), active( identity: "rn_J4Q81A4BJ8TXN6ZE4481RMB3Y9", id: "v1-interpreter-quarantine", @@ -1535,6 +1571,7 @@ fn declared_roadmap_edges() -> List { edge(child: "v1-emitter-quarantine", parent: "v1-ci-floor-cutover"), edge(child: "v1-emitter-delete", parent: "v1-emitter-quarantine"), edge(child: "v1-interpreter-quarantine", parent: "v1-ci-floor-cutover"), + edge(child: "v1-interpreter-quarantine", parent: "v1-interpreter-primitive-roster"), edge(child: "v1-interpreter-delete", parent: "v1-interpreter-quarantine"), edge(child: "v1-interpreter-delete", parent: "v1-emitter-delete"), edge(child: "v1-products-v1-free", parent: "v1-interpreter-delete"), @@ -1570,6 +1607,8 @@ fn declared_roadmap_edges() -> List { data criteria_digest_reword_note: String = "Both digests below moved on 2026-07-29 because every row was rewritten in plain language, and the digest covers the brief, the pass/fail test, and what gets handed back. Worth being blunt about the limit this exposes: the digest cannot tell a rewording from a genuine change to the bar — it only knows the words moved. Nothing about what these two rows had to prove changed, and the evidence each names is the same run and the same files as before, so re-pinning is correct here. But that judgement was a person's, not the mechanism's, and a reviewer should treat any digest change as a prompt to re-read the row rather than as a formality. This is the same gap roadmap_acceptance_receipt_note already names: until a row's pass/fail test points at the test that discharges it, the link between words and evidence is asserted rather than derived." +data entry_graph_union_construction_acceptance_note: String = "CLOSED NO-GO (operator adjudication 2026-08-01). Slice 2 PR #7533 delivered the repeated-typecheck attribution receipt slice 1 required before any union implementation. decision_ratio=0 at every measured cell at N≤50 — priced benefit absent. Digest 4442f95272011c70 derived by execution via node_criteria_digest on this tree after amending boundary/red_control/handback to record the verdict. Red control names perfect_sharing_yields_zero_decision_ratio as the discriminating decision-quantity witness; sibling controls in slice-2 receipt §C execute in the same test modules." + data namespace_binding_kernel_acceptance_note: String = "STATUS RECONCILIATION, NOT NEW WORK (operator ruling 2026-08-01). PR #7508 merged the supplied-candidate binding kernel, so the IMPLEMENTATION dependency for namespace-reference-derived-closure was resolved, but no acceptance receipt existed — leaving the roadmap saying its own prerequisite was unaccepted while the code depended on it. Two different truths for the same fact, which is exactly what node_is_startable reads, so P2a was being worked on while not honestly startable under the roadmap's own definition. The digest 2eba4715cdef42eb is the POST-#7530 value: #7530 rewrote this node's boundary, red_control and out_of_scope to cut authority from retirement, and roadmap_criteria_digest covers brief plus red_control plus handback, so the pre-amendment digest is stale by construction. It was DERIVED BY EXECUTION rather than transcribed — node_criteria_digest was run against the live node on this tree, not copied from a prior receipt or computed by hand. The red control names the malformed-transport refusal because that is the first arm #7530's amended row requires; the sibling arms (absent versus wrong-role identity distinct, duplicate supplied identity refused, wrong-category excluded, zero/one/two giving unbound/bound/ambiguous, exact declaration identity surviving, canonical fold invoked once) all execute in the same witness module and were green on the exact-head run that merged #7508." data scm_prerequisite_acceptance_note: String = "The operator explicitly accepted the three independent R0 models and the separate P−1 evidence carrier, then accepted R1 after merging its three-realization compatibility extraction, and accepted P0 after its layered landing spine and twenty-two executing journey witnesses landed. R1 depends only on the three R0 receipts; P0 consumes both accepted R1 and accepted P−1. These receipts preserve every prerequisite and handback as durable facts. Replacing accepted rows with their child would erase history and violate roadmap_acceptance_receipt_note." @@ -1730,6 +1769,25 @@ fn roadmap_acceptance_receipts() -> List { accepted_by: "operator", accepted_on: "2026-08-01", }, + RoadmapAcceptanceReceipt { + node: nid(s: "entry-graph-union-construction"), + criteria_digest: chash(s: "4442f95272011c70"), + red_control: RedControlExecuted { + witness_module: "v1-compiler.cli_run.repeated_typecheck_attribution_arithmetic", + witness_fn: "perfect_sharing_yields_zero_decision_ratio", + executed_on: "2026-08-01", + }, + handback: HandbackDelivered { + first_artifact: "docs/plans/entry-graph-union-slice2-typecheck-attribution.md", + further_artifacts: [ + "docs/plans/receipts/entry-graph-union-slice2/receipt-broad-disjoint.json", + "src/v1/stage0/src/bin/measure_repeated_typecheck_attribution.rs", + "src/v1/tests/src/union_resolve_receipts_test.rs", + ], + }, + accepted_by: "operator", + accepted_on: "2026-08-01", + }, RoadmapAcceptanceReceipt { node: nid(s: "pderive-typesafe-nullary-reflection"), criteria_digest: chash(s: "dac83172a613e659"), @@ -1882,8 +1940,24 @@ fn closing_contract_task_for(n: RoadmapNode) -> RoadmapNode { } } +data closing_contract_family_claim_note: String = "The family claim, stated once (presentation family-once grouping): every closing-contract task shares this claim, and each generated ticket's fused headline ('Write the closing check for: X') repeats it per row. The pair surface below exposes the DISCRIMINATING SUBJECT (the gated node's own typed ticket headline) beside each generated task so a projection can state the claim once and never recover the subject by parsing the fused headline. The generated ticket itself is unchanged — ROADMAP.md rendering and dispatch briefs keep the full self-contained headline." + +data closing_contract_family_claim: String = "Write the closing check — one executable done-condition for each gated node below" + +type ClosingContractTaskPair { + task: RoadmapNode + subject: String +} + +fn closing_contract_task_pairs() -> List { + startable_nodes_missing_closing_contract() |> map(n => ClosingContractTaskPair { + task: closing_contract_task_for(n: n), + subject: ticket_headline_of(n: n), + }) +} + fn closing_contract_task_nodes() -> List { - startable_nodes_missing_closing_contract() |> map(n => closing_contract_task_for(n: n)) + closing_contract_task_pairs() |> map(p => p.task) } fn closing_contract_task_edges() -> List { @@ -1967,10 +2041,25 @@ fn active_section_for(focus: RoadmapFocus) -> RoadmapSection { } } -data guarantee_ladder_lane_note: String = "STATE vs WORK, and IN-GRAPH vs BESIDE-IT. Rung STATE has one authority — the guarantee claims carrier (ladder-claims-carrier node), its dispositions DERIVED from execution receipts and EMITTED by ladder-census-emitters — never a ticket; a climb node names its target transition and lets the census/carrier say where the class stands today (the 2026-07-31 operator verdict on gunbc#7489 caught headlines transcribing rungs, the third rung-inflation catch in this lane's own artifacts). The WORK rows below are ordinary declared roadmap nodes under lane owner compiler-guarantee: the first cut rendered them from a section-local list beside the graph, which minted a second roadmap universe — uncounted, receipt-unreachable, invisible to the identity/endpoint/acyclicity witnesses and to active_roadmap_nodes() (same verdict; review 45356 independently flagged the owner-less rows the placement forced). Owner here is the LANE's identity, not an assignee: nodes are unbound until dispatch binds a session and a closing contract, exactly like every other lane. Page visibility is the typed focus policy in roadmap_focus_selection (the lane is selected so the unstaffed backlog stays on the page), and rendered-node containment is witnessed — see roadmap_rendered_containment_note. Post-merge verdict (2026-07-31, on the merged gunbc#7489): v2-phase-carriers split into five staged nodes (its identity is the registry's first tombstone, superseded by v2-self-grounding-frontier); floor-parse-formation-wall, floor-record-construction-wall, and the compiler-accepted-obligation-closure terminal added; baseline anchored to the merge commit; and the standing discipline that an OPEN PR is candidate evidence, never main's rung — tickets cite open work as 'open candidate', and the census states main's disposition separately." +data guarantee_ladder_lane_note: String = "STATE vs WORK, and IN-GRAPH vs BESIDE-IT. Rung STATE has one authority — the guarantee claims carrier (ladder-claims-carrier node), its dispositions DERIVED from execution receipts and EMITTED by ladder-census-emitters — never a ticket; a climb node names its target transition and lets the census/carrier say where the class stands today (the 2026-07-31 operator verdict on gunbc#7489 caught headlines transcribing rungs, the third rung-inflation catch in this lane's own artifacts). The WORK rows below are ordinary declared roadmap nodes under lane owner compiler-guarantee: the first cut rendered them from a section-local list beside the graph, which minted a second roadmap universe — uncounted, receipt-unreachable, invisible to the identity/endpoint/acyclicity witnesses and to active_roadmap_nodes() (same verdict; review 45356 independently flagged the owner-less rows the placement forced). Owner here is the LANE's identity, not an assignee: nodes are unbound until dispatch binds a session and a closing contract, exactly like every other lane. Page visibility is the typed focus policy in roadmap_focus_selection (the lane is selected so the unstaffed backlog stays on the page), and rendered-node containment is witnessed — see roadmap_rendered_containment_note. Post-merge verdict (2026-07-31, on the merged gunbc#7489): v2-phase-carriers split into five staged nodes (its identity is the registry's first tombstone, superseded by v2-self-grounding-frontier); floor-parse-formation-wall, floor-record-construction-wall, and the compiler-accepted-obligation-closure terminal added; baseline anchored to the merge commit; and the standing discipline that an OPEN PR is candidate evidence, never main's rung — tickets cite open work as 'open candidate', and the census states main's disposition separately. Spine verdict (2026-08-01, seventh pass): the ladder-measurement-schema node now precedes probes and carrier (the receipt protocol both meet through); the three merged P0 slices are recut at their actual grain with tombstoned identities (floor-call-shape-wall, floor-method-existence-wall, floor-inhabitance-wall each split into an ACCEPTED fragment node plus open siblings); the Accepted door is split mechanism/floor/extended (compiler-accepted-obligation-closure tombstoned) so the audit form lands with the carrier; exemption removal re-grounded on the two grounding nodes; emitters parallel with the baseline; the baseline a two-revision execution. Correction on the split (review 45918): the first extended-activation cut kept requires-all edges on one node while its prose promised class-by-class widening — the graph would have deferred every admission behind the slowest climb; recut as four per-class admission nodes (each ready the day its own climb lands) with accepted-extended-obligation-closure the terminal roster-completeness certification, where requires-all honestly belongs. Sequencing: everything outside the spine's first increment (schema, then probes + carrier) and the authorized parallel containment workstream WAITS." fn guarantee_ladder_nodes() -> List { [ + active_sized(identity: "rn_RAVINFXPMNUT5RP5TAN2PXHATI", id: "ladder-measurement-schema", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyMedium, + volume: VolumeSmall, + t: fields( + headline: "The guarantee measurement schema: evidence learns to identify its subject before anyone measures", + boundary: "The stable vocabulary only: GuaranteeClassId, GuaranteePathId, GuaranteeProbeId; GuaranteePath (subject_grain, acceptance_boundary, compile_mode, realization_target); GuaranteeMeasurementReceipt carrying class, path, probe, subject_revision, harness_revision, probe_set_digest, observed disposition. No stored rung, no claims about the compiler — only how a receipt names what it measured. This stage exists to break the probes-need-carrier-identities / carrier-needs-probe-receipts protocol cycle (operator spine verdict, 2026-08-01): probes and claims meet through this contract instead of each minting temporary string keys.", + displaced_cost: "Without it the probe child mints throwaway identifiers that immediately require migration, and two carriers of the same fact drift.", + first_slice: "The identity and receipt types, with a synthetic-receipt round-trip witness.", + red_control: "A receipt missing any identity component is unwritable (required fields, no Optional arms); a duplicate class id reds the registry witness.", + out_of_scope: "No probes, no dispositions, no rung derivation, no prevalence.", + handback: "The schema module path and the identity-registry witness receipt." + ) + ), active_sized(identity: "rn_T6JTXDKQBI6FDZ742FSGRZRQNA", id: "ladder-probe-corpus", owner: "compiler-guarantee", path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", @@ -1980,7 +2069,7 @@ fn guarantee_ladder_nodes() -> List { headline: "The expecting-red probe corpus: the floor gap becomes a counted, continuously-measured number", boundary: "One probe pair per floor class (deliberately-bad input expected to refuse; legitimate control expected to accept), enrolled as expecting-red rows via the known-red quarantine mechanism so today's fail-open state is measured, not asserted. Every currently-legitimate map/fold/filter/flat_map/service invocation form appears as a positive control so a future wall cannot green by refusing the language.", displaced_cost: "Replaces anecdote (gunbc#7479's 500) with a counted deficit per class; each wall that lands flips its probes to regression controls loudly.", - first_slice: "The floor probes from the census: unknown method, call shape (incl. the silent positional-label bind), return, data annotation, generic instantiation, field-through-generic, exhaustiveness-via-blocked-lookup, record completeness, cardinality seam; plus the parser silent-separator pair (a dropped list comma parsing as element juxtaposition with zero diagnostics — measured specimen from the gunbc#7484 review trail, 2026-07-31).", + first_slice: "MIGRATE, do not duplicate: the landed controls from gunbc#7484, #7519, and #7485 become the durable per-class probes (one probe pair per class/path, keyed by the measurement schema's identities — never a fresh test family beside each PR). Then the unwalled census classes: generic instantiation, field-through-generic, exhaustiveness-via-blocked-lookup, record completeness, cardinality seam, and the parser silent-separator pair (measured specimen, gunbc#7484 review trail 2026-07-31).", red_control: "A probe that stops expecting red without its class's wall having landed is itself a red (greening is counted); the corpus reds if any probe pair loses its executing consumer.", out_of_scope: "No compiler change; probes measure, walls land in the floor-wall nodes.", handback: "Probe roster with per-class expected/actual disposition and the enrollment receipt." @@ -1995,7 +2084,7 @@ fn guarantee_ladder_nodes() -> List { headline: "The guarantee claims carrier: rung state becomes DERIVED from execution receipts, never stored", boundary: "Four carriers, measurement separated from claim: GuaranteeRequirement (class identity, domain, harm, ceiling with typed reason, next_rung_trigger); GuaranteePath (subject_grain, acceptance_boundary, compile_mode, realization_target — one row per in-scope path, population derived from the census or completeness-witnessed); GuaranteeMeasurement (executed receipts naming their consumer); and GuaranteeDisposition DERIVED per path — Unmeasured | BelowFloor | FrontierAccepted | OnLadder | OutsideModeledGuarantee — folded by below-floor dominance, then frontier, then unmeasured, then MINIMUM rung across paths. No stored current_rung field exists, so a transcribed rung is unrepresentable; the five recovered vocabularies stay orthogonal, joined by class identity. Full shape: gap analysis sec 12 Stage 1b.", displaced_cost: "Ends the per-conversation re-derivation of what the compiler guarantees (the operator performed the missing meta-lens by hand three times this month); grounds the enforcement-intent StandingIntent thread instead of minting beside it.", - first_slice: "The four carrier types plus the floor-domain classes' path rows, dispositions derived from the ladder-probe-corpus receipts (the probe corpus precedes this node for exactly that reason).", + first_slice: "The requirement/ceiling/measurement/disposition carriers over the measurement schema's identities (the schema stage precedes both this node and the probes — the protocol cycle is broken there, not here), with the floor-domain path rows populated from the probe receipts and the disposition fold proven over synthetic receipts first.", red_control: "A synthetic measurement-less row folds to Unmeasured, never to a rung; deleting a probe receipt visibly demotes the derived disposition; a path population missing a census axis reds the completeness witness; a row below its ceiling with next_rung_trigger absent reds the stall check.", out_of_scope: "No wall lands here; no DESIGN.md prose edit (the carrier projects); no prevalence run.", handback: "Carrier module path, the disposition-derivation fn, and the per-domain path-row count." @@ -2008,7 +2097,7 @@ fn guarantee_ladder_nodes() -> List { volume: VolumeMedium, t: fields( headline: "Baseline prevalence: the whole-corpus floor rerun bucketed by ladder position, before any wall lands", - boundary: "Re-run the whole-corpus floor and bucket every failure — statically-decidable-should-have-refused, runtime-value-dependent, external-boundary, resource-budget, capability-not-grounded, interpreter-defect — keyed to the carrier's class ids: the honest BEFORE picture. Replaces the carried-but-unverified historical figures (1454/149 and the 66/40/6 histogram) with current receipts. Anchored: anchor_commit 6c6e2dcb8587d73350ac252f5b07a6b50d684485 (the gunbc#7489 merge — pre-P0-implementation main), so the measurement is content-addressed and reproducible after in-flight branches merge; walls sequence after it without racing a live tree (post-merge verdict).", + boundary: "A TWO-REVISION execution, never 'run the baseline at commit X' (three-way ambiguous): the anchor's compiler artifacts (subject_revision: anchor_commit 6c6e2dcb8587d73350ac252f5b07a6b50d684485, the gunbc#7489 merge — pre-P0 main) run under today's content-addressed probe set and classifier (harness_revision + probe_set_digest per the measurement schema), outcomes translated into measurement receipts keyed to carrier class ids. Bucket every failure — statically-decidable-should-have-refused, runtime-value-dependent, external-boundary, resource-budget, capability-not-grounded, interpreter-defect. Replaces the carried-but-unverified historical figures (1454/149; the 66/40/6 histogram). Runs parallel with the census emitters, not ahead of them.", displaced_cost: "Denominates the climbs in displaced cost (DESIGN 6) instead of elegance; the honest number for how many programs the gap admits today.", first_slice: "The bucketing classifier over one batch's failures, validated by hand against ten cases.", red_control: "Bucket counts reconcile to the total (no absorbed remainder); a failure the classifier cannot bucket is a typed Unknown row, never dropped.", @@ -2016,49 +2105,169 @@ fn guarantee_ladder_nodes() -> List { handback: "The bucketed histogram keyed to carrier class ids, and the delta rows for the census." ) ), - active_sized(identity: "rn_A7QX5QZMWKC6STOGGDGD4KMIHQ", id: "floor-method-existence-wall", + active_sized(identity: "rn_IGN7UM5UQQPZ6MNMIX5TVUMNSK", id: "method-established-surface-wall", owner: "compiler-guarantee", path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", intricacy: IntricacyMedium, volume: VolumeSmall, t: fields( - headline: "Method existence: an unresolved method refuses at compile on every applicable path", - boundary: "An unresolved method refuses at compile with name, receiver type, and locus: delete the method_pipe_map_keys_values_fallback else-arm and the resolve_builtin_call_type Absent-to-unit_type arm. Main today: compile-time method existence unenforced outside existing paths. Open candidate gunbc#7484 carries narrow R2 coverage for established receiver surfaces plus a typed, countable MethodExistenceUndecided frontier elsewhere — candidate evidence on an open branch, never main's rung (post-merge verdict). Zero-resolution is decided over the union of current admissible sources — enumeration suffices for absence — so the identity join gates only the more-than-one half, not this node. First: receiver normalization, then zero-resolution refusal.", - displaced_cost: "The gunbc#7479 class (typechecked filter_map, HTTP 500 at dispatch) becomes unwritable-to-ship; the compile gate stops being quieter than the interpreter.", - first_slice: "The two receiver-type resolution repairs (alias peel, pattern payload), probe-paired, widening the landed kernel-receiver wall one receiver family at a time.", - red_control: "The probe corpus's method pair flips red-to-green exactly when the general wall lands; the map/filter/fold/flat_map/service positive controls never refuse (an over-wide wall reds them).", - out_of_scope: "Ambiguity refusal (its own node); deleting seed dispatch; emitter dispatch derivation.", - handback: "The deleted arms, the receiver families widened, and the probe flip receipt." + headline: "Method existence on established receiver surfaces — LANDED (gunbc#7484, merged 2026-08-01)", + boundary: "ACCEPTED fragment, recut from floor-method-existence-wall at its actual grain (operator spine verdict). An unresolved method on a kernel-profiled receiver refuses (MethodNotFound, blocking — the gunbc#7479 class, 0 files emitted); undecided receivers carry the rostered, occurrence-budgeted MethodExistenceUndecided/FrontierAdmitted frontier; map_keys/map_values bypasses routed through the one decision; one where-refinement receiver class peeled. What did NOT land stays on the sibling nodes: receiver-resolution residue, the general zero-resolution wall, resolve_builtin_call_type's unit_type fabrication.", + displaced_cost: "The compile gate stopped being quieter than the interpreter for every receiver with a complete declared surface.", + first_slice: "DONE — acceptance receipt: merged gunbc#7484; whole-tree 0 blocking; regen divergence 0; discriminating REDs enrolled.", + red_control: "method_existence_wall_witness (REDs + zero-diagnostic controls + frontier-key perturbation) — migrates to the probe corpus as this class's durable pair.", + out_of_scope: "Everything the sibling nodes own; the frontier's dissolution (primitive-realization-single-authority).", + handback: "Receipt recorded; residue on method-receiver-normalization and method-zero-resolution-general-wall." ) ), - active_sized(identity: "rn_DYQZBEQKOWIYI776266OWMZREQ", id: "floor-call-shape-wall", + active_sized(identity: "rn_TMC5UFTYNMSBMS7FLXYIVQCSRE", id: "method-receiver-normalization", owner: "compiler-guarantee", path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", intricacy: IntricacyMedium, - volume: VolumeMedium, + volume: VolumeSmall, + t: fields( + headline: "Receiver normalization: the measured resolution residue becomes decidable receivers", + boundary: "The frontier's measured causes, repaired upstream so the wall can decide them: a lambda-parameter receiver whose type never propagates from the declared fn type it is bound under; a coproduct payload bound by destructuring typed as its variant name; the optional receiver whose cardinality drops before lookup; the brand-alias LEAF form the peel cannot reach (qualified-name leaf, measured and declared on the gunbc#7484 frontier roster). Each repair shrinks the roster in both directions — correct calls resolve, bogus methods refuse.", + displaced_cost: "Every roster row this discharges converts an admitted-by-key site into a decided one; the frontier ratchet only shrinks.", + first_slice: "The lambda-parameter receiver typing from the declared fn type of the record field the lambda is stored in (10 of 13 measured residue sites).", + red_control: "Discharged roster rows delete with their occurrence budgets; the frontier count falls monotonically; positive controls stay green.", + out_of_scope: "The zero-resolution general wall (next node); ambiguity.", + handback: "Roster rows discharged, the receiver families now decidable, probe receipts." + ) + ), + active_sized(identity: "rn_M25FZG43IT6MSBIWBQBONOUSZM", id: "method-zero-resolution-general-wall", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyMedium, + volume: VolumeSmall, + t: fields( + headline: "Zero-resolution refuses generally: enumeration over the union of admissible sources", + boundary: "Absence is decided by enumerating the union of current admissible sources — no identity join required (the join gates only the more-than-one wall). Delete the resolve_builtin_call_type Absent-to-unit_type fabrication; every call whose callee resolves nowhere refuses with name and locus on every compile/emit path.", + displaced_cost: "Closes the callable-existence class fully: the last fabricated-success arm at the call seam dies.", + first_slice: "The resolve_builtin_call_type arm becomes a typed refusal, census-adjudicated corpus-wide.", + red_control: "Probe: a call to a nowhere-defined fn refuses at compile; controls: every corpus builtin and user call still resolves.", + out_of_scope: "Ambiguity (identity join's dependent); dispatch-tier unification.", + handback: "The deleted arm, census delta, probe flip receipt." + ) + ), + active_sized(identity: "rn_42Z3Z646SNDZBMVXYDU7RSL4JA", id: "method-call-shape-seam", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyLow, + volume: VolumeSmall, + t: fields( + headline: "Argument labels at the method/pipe seam: the call-shape judgment reaches method dispatch", + boundary: "The direct-call label/surplus wall (landed, gunbc#7519) covers the sig-resolved ExprCall branch only; method-pipe argument binding routes through its own inference arms. Mirror the same two runtime-refused classes there, reusing the landed judgment rather than minting a second relation.", + displaced_cost: "A mislabeled method argument silently binding positionally is the same interest-bearing harm as the direct-call case.", + first_slice: "Label membership at method-call argument positions, probe-paired.", + red_control: "Probe: mislabeled method arg refuses; controls: every corpus pipe/method form compiles.", + out_of_scope: "Method existence (sibling lane); duplicate/missing (parity-pair node).", + handback: "The seam's judgment site and probe receipts." + ) + ), + active_sized(identity: "rn_EDXPW53NQMZVLEPYNYH6X4VNKM", id: "call-label-and-surplus-wall", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyMedium, + volume: VolumeSmall, + t: fields( + headline: "Unknown labels and surplus positionals refuse at the direct-call seam — LANDED (gunbc#7519, merged 2026-07-31)", + boundary: "ACCEPTED fragment, recut from floor-call-shape-wall at its actual grain (operator spine verdict). CallArgumentNameUnknown + CallPositionalSurplus blocking, exemption-free, mirroring call_function_inner's two refused classes (underscore idiom included); census refused 28 live parameter-rename fossils, all fixed; the silent emit-path reorder retired for Accepted programs. What did NOT land stays on the sibling nodes: missing/duplicate, signature-resolution coverage, the method seam.", + displaced_cost: "The mislabeled call that computed the wrong thing silently on the emission path is unwritable in an Accepted program at this seam.", + first_slice: "DONE — acceptance receipt: merged gunbc#7519; pre/post probe pair; suite green; regen divergence 0.", + red_control: "ct_call_shape_wall_witness_test — migrates to the probe corpus as this class's durable pair.", + out_of_scope: "Everything the sibling nodes own.", + handback: "Receipt recorded; residue on call-missing-and-duplicate-wall and call-signature-resolution-coverage." + ) + ), + active_sized(identity: "rn_EXUHLON5V24YU7Z4XLJFWEZO33", id: "call-missing-and-duplicate-wall", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyMedium, + volume: VolumeSmall, + t: fields( + headline: "Missing and duplicate arguments: the interpreter-first parity pair", + boundary: "The interpreter itself does not refuse these today — a missing required arg decays to NoSuchVariable at read (or silence if unread); a duplicate label silently overwrites in bindings.insert — so a compile-only wall would diverge the authorities the other way. Land the refusals in call_function_inner FIRST (default-value-aware: param_node_default_value legitimately fills omissions), then mirror at the compile seam, keeping the two authorities in lockstep exactly as the label/surplus pair did.", + displaced_cost: "A missing argument surfacing as NoSuchVariable inside the body blames the wrong site; a duplicate silently taking the last value is unlocatable.", + first_slice: "The interpreter's duplicate-label refusal (no default interaction, cheapest half), then compile mirror, probe-paired.", + red_control: "Probes: duplicate label and missing non-default arg refuse on both authorities; controls: defaulted omissions and every corpus form stay green.", + out_of_scope: "Label membership (landed sibling); the method seam.", + handback: "Both authorities' refusal sites, census delta, probe receipts." + ) + ), + active_sized(identity: "rn_GVLXWYYRQV47WTJLSSIFX7FLFM", id: "call-signature-resolution-coverage", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyMedium, + volume: VolumeSmall, + t: fields( + headline: "Signature-resolution coverage: the sig-unresolved fallthrough stops being a blind spot", + boundary: "A call whose callee resolves NO signature bypasses every call-shape judgment (measured: 3 label fossils invisible to the landed wall in dag_collect, found only by grep). Either the callee resolves and is judged, or the failure to resolve is itself a typed, counted fact — never a silent fallthrough to the method bridge. Rides the namespace lane's resolution coverage; this node owns the call-shape consequence.", + displaced_cost: "The wall's own measured blind spot: correctness there currently rests on grep.", + first_slice: "Count the fallthrough (typed, per-module) so the blind spot has a size before it has a fix.", + red_control: "The dag_collect specimen class reds if reintroduced once coverage lands; fallthrough count monotonically falls.", + out_of_scope: "The resolution mechanism itself (namespace lane).", + handback: "Fallthrough census, then the coverage receipt." + ) + ), + active_sized(identity: "rn_4ELCVV2EIHCBGHMOG6T4IZEMT3", id: "declared-conformance-ground-fragment", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyMedium, + volume: VolumeSmall, t: fields( - headline: "Call shape: the silent positional label bind gets loud, then exact bijection", - boundary: "Floor LANDED (cool-badger-514): unknown label and surplus positional refuse at the direct-call seam, blocking, exemption-free — the two classes the interpreter's call_function_inner already refuses, mirrored so the authorities agree. Census refused 28 live rename fossils, all fixed. Remaining wall: duplicate and missing land as an interpreter-first parity pair; method-pipe seam with the method wall. ArityMismatch stays type-constructor grain.", - displaced_cost: "A misspelled label silently computing the wrong thing is the harm shape DESIGN 5 prices at interest, paid by the end user; the landed floor retires it at the direct-call seam.", - first_slice: "DONE — direct_call_shape_diags, probe-paired (pre 0 diagnostics, post two located refusals), census-adjudicated.", - red_control: "Landed: ct_call_shape_wall_witness_test — mislabel and surplus REDs, zero-diagnostic controls incl. the underscore idiom; every legitimate corpus form compiles (census dry).", - out_of_scope: "The v2.*/v1.compiler.* exemption (its own node); default-parameter semantics changes; the sig-unresolved fallthrough (closes with resolution coverage).", - handback: "v1.compiler.infer direct_call_shape_diags; CallArgumentNameUnknown + CallPositionalSurplus; receipts in direct_call_shape_wall_note." + headline: "Declared conformance for the ground fragment — LANDED (gunbc#7484, merged 2026-08-01)", + boundary: "ACCEPTED fragment, recut from floor-inhabitance-wall at its actual grain (operator spine verdict). fn returns (both arms, plus early returns stopping at lambda boundaries) and data annotations refuse non-conforming producers where BOTH sides are ground: kernel scalars, or Required non-keyed element collections of them — positive establishment, the four measured representation classes never guessed at. The counted unjudged advisory was EXCLUDED in final review scope: everything wider is byte-for-byte main behavior (silent), stated here so the fragment is never read as the class.", + displaced_cost: "fn f() -> Int returning a string — the probe-3 specimen — refuses located instead of surfacing at the first consumer.", + first_slice: "DONE — acceptance receipt: merged gunbc#7484; discriminating controls run against the prior binary.", + red_control: "declared_type_conformance_witness — migrates to the probe corpus as this class's durable pair.", + out_of_scope: "Everything the sibling nodes own; generic instantiation (record-construction lane).", + handback: "Receipt recorded; residue on declared-conformance-grounding and declared-conformance-general-wall." ) ), - active_sized(identity: "rn_6W5ACS5ZE3ZMTA7BFCQLY76VW4", id: "floor-inhabitance-wall", + active_sized(identity: "rn_WGRZFAYJ5EDVPNEQ42UAK7TD6D", id: "declared-conformance-grounding", owner: "compiler-guarantee", path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", intricacy: IntricacyHigh, volume: VolumeMedium, t: fields( - headline: "Declared types constrain produced values: return, data, and generic instantiation refuse non-inhabiting producers", - boundary: "fn f() -> Int returning a string refuses at compile; data x: Int = \"x\" refuses; a generic record instantiation whose interior does not inhabit the substituted parameter refuses. The judgment is actual-inhabits-declared at the three unchecked positions gunbc#7481 probed, reusing the argument-position machinery that already refuses correctly.", - displaced_cost: "The keystone gap: the recovered thesis's distinctness requirement — without it every declared type is part contract, part documentation, and WalkPlan-class defects surface at first field access.", - first_slice: "Return-position checking, probe-paired; the gunbc#7481 in-tree witnesses (floor finalization) dissolve on landing per their own dissolve-on notes.", - red_control: "The three gunbc#7481 probes refuse; every green corpus module still compiles (the historical 104-class false-positive risk is adjudicated, not exempted).", - out_of_scope: "Where-refinement semantics beyond literals; the exemption removal (depends on this).", - handback: "The judgment site, the three probe flips, and the dissolved-witness list." + headline: "Conformance grounding: the four representation classes stop being unjudgeable", + boundary: "The measured false-positive classes that cap the ground fragment, each a §3 grounding defect not a checker defect: brand aliases unpeeled to their base; optionality living in two representations (return_cardinality vs the nominal Optional coproduct); anonymous record literals never grounded against their expected nominal; declaration-vs-body expansion depth. Ground each so the conformance relation widens class by class — the ground-fragment gate exists only because these are unjudgeable today.", + displaced_cost: "Each grounded class moves thousands of silent declarations into the judged set (measured: the expansion-depth class alone was half the 3005-row frontier).", + first_slice: "The brand-alias peel at conformance positions (the relation the method wall already built, second consumer).", + red_control: "Per class: previously-unjudgeable correct code stays green AND a same-shape violation refuses — decidable in both directions or not claimed.", + out_of_scope: "The general wall flip (next node); argument-position adjudication (its own node).", + handback: "Classes grounded, frontier delta per class, probe receipts." + ) + ), + active_sized(identity: "rn_E6PQ6WQR2J7SLMGBR62P3ECKGY", id: "declared-conformance-general-wall", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyMedium, + volume: VolumeMedium, + t: fields( + headline: "The general conformance wall: every grounded declared type constrains its producer", + boundary: "As grounding lands per class, the conformance judgment widens to full return/data coverage — the gate dissolving into the relation until every grounded declared type refuses non-inhabiting producers, with whatever residue remains typed and counted, never silent.", + displaced_cost: "Completes the keystone: a declared type is a contract everywhere, not per-fragment.", + first_slice: "Widen to the first grounded class corpus-wide, census-adjudicated.", + red_control: "The probe corpus's return/data pairs per class; corpus stays green as each class widens.", + out_of_scope: "Argument positions (exemption lane); generic instantiation (record-construction lane).", + handback: "The widened relation, residue census, probe receipts." + ) + ), + active_sized(identity: "rn_EAOINVVBM732ZXS42ELVZAUPQV", id: "argument-type-compatibility-grounding", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyMedium, + volume: VolumeMedium, + t: fields( + headline: "Argument-type compatibility grounding: adjudicate what the exemption papers over", + boundary: "module_skips_direct_call_arg_check exempts v2.*/v1.compiler.* from the argument TYPE judgment because of representation-gap false positives — the same four classes the conformance lane grounds, at the argument seam, plus the unlocated historical '104 TypeMismatch false positives' (no receipt found by two audits; adjudicate or refute it by measurement). Ground the argument-position relation so the exemption's reason dissolves.", + displaced_cost: "The compiler's own sources are currently un-judged at every argument position — the largest single acceptance hole by module count.", + first_slice: "Measure: run the check exemption-free in census mode, bucket every red by representation class.", + red_control: "Each grounded class's argument-position probe pair; corpus reds adjudicated never exempted.", + out_of_scope: "Deleting the exemption (its own node, gated on this one and conformance grounding).", + handback: "The bucketed census, classes grounded, the 104-claim disposition." ) ), active_sized(identity: "rn_VEY66GYMNYZ24LWJQJGZGCFLRI", id: "floor-exhaustiveness-refusal", @@ -2127,10 +2336,10 @@ fn guarantee_ladder_nodes() -> List { intricacy: IntricacyMedium, volume: VolumeMedium, t: fields( - headline: "v2 stops treating source structure as inferred fact: the self-grounding slice becomes a typed frontier", - boundary: "The strict-narrowing slice, carried today by open candidate gunbc#7485 (candidate evidence on an open branch, never main's rung): exact self-evidence leaves solve_constraints' generic arm; underived facts become typed, located, counted GroundingNotDerived diagnostics inside an accepted InferredTree — the FrontierAccepted disposition's live specimen. Identity admission reads the authored declared-inhabitant roster inside the authority, never caller-supplied candidates; Bool/Int literal derivations raised via the canonical literal query. Closes when the slice is on main with its witnesses enrolled.", - displaced_cost: "Silent self-certification becomes a counted frontier: the deficit ranks for fixing instead of reading as green (the absorbing-fallback rule applied to inference).", - first_slice: "The gunbc#7485 stack merged to main; the self-grounding and identity-admission witnesses enrolled on the floor.", + headline: "v2 self-grounding slice — LANDED (gunbc#7485, merged; acceptance receipt recorded 2026-08-01)", + boundary: "ACCEPTED against its handback (operator spine verdict): exact self-evidence left solve_constraints' generic arm; underived facts are typed, located, counted GroundingNotDerived diagnostics inside an accepted InferredTree — the FrontierAccepted disposition's live specimen. Identity admission reads the authored declared-inhabitant roster, never caller-supplied candidates. The documented residue is the NEXT node's whole scope: Translate can still swallow the refusal and proceed.", + displaced_cost: "Silent self-certification became a counted frontier: the deficit ranks for fixing instead of reading as green.", + first_slice: "DONE — acceptance receipt: gunbc#7485 merged with the self-grounding and identity-admission witnesses enrolled.", red_control: "The claims carrier's v2 self-grounding path flips BelowFloor-to-FrontierAccepted exactly here; the identity path refuses caller-supplied candidate sets.", out_of_scope: "Translate propagation; carrier completeness; target realization (each its own node downstream).", handback: "The merged receipts and the frontier diagnostic counts by class." @@ -2143,7 +2352,7 @@ fn guarantee_ladder_nodes() -> List { volume: VolumeSmall, t: fields( headline: "Translate propagates the refusal: an underived fact cannot be swallowed downstream", - boundary: "gunbc#7485 records that Translate can swallow the new GroundingNotDerived rejection and continue — the frontier diagnostic exists but the emission path proceeds. This node makes Translate refuse or thread the typed frontier so Eval and Translate agree: compile-time refusal on both is the class minimum (post-merge verdict grid). The per-path grain is exactly why the carrier models paths separately.", + boundary: "ACTIVE — the containment workstream, authorized to run parallel with the spine (operator spine verdict 2026-08-01: not another optional wall; it closes the one documented fail-open the merged frontier split created). gunbc#7485 records that Translate can swallow the new GroundingNotDerived rejection and continue — the frontier diagnostic exists but the emission path proceeds. Make Translate refuse or thread the typed frontier so Eval and Translate agree: compile-time refusal on both is the class minimum. The per-path grain is exactly why the carrier models paths separately.", displaced_cost: "Closes the path split where the same underived fact refuses in one phase and ships in another — the subject-grain failure mode measured on the call-shape class.", first_slice: "Translate refusing the existing frontier diagnostic, probe-paired with an underived-generic fixture.", red_control: "A source tree with an underived fact refuses translation naming the fact; every derived corpus module still translates.", @@ -2218,7 +2427,7 @@ fn guarantee_ladder_nodes() -> List { volume: VolumeSmall, t: fields( headline: "Record construction refuses: missing required fields, bad defaults, ungrounded nominal literals", - boundary: "A record literal missing a required field refuses at compile with the field named; defaults apply only where declared; a literal claiming a nominal type it does not ground refuses. Owns the census's record-completeness class — floor-inhabitance-wall owns return, data, and generic instantiation, not this. Probe-paired via the corpus's record-completeness pair. Motivating specimen: HandAuthoredDocBind's primary_work construction wall currently rests on record-completeness enforcement that is itself unmeasured.", + boundary: "A record literal missing a required field refuses at compile with the field named; defaults apply only where declared; a literal claiming a nominal type it does not ground refuses. Owns the census's record-completeness class — the declared-conformance chain owns return, data, and generic instantiation, not this. Probe-paired via the corpus's record-completeness pair. Motivating specimen: HandAuthoredDocBind's primary_work construction wall currently rests on record-completeness enforcement that is itself unmeasured.", displaced_cost: "A construction wall anywhere in the corpus is only as strong as required-field enforcement; this node is what makes primary_work-style walls load-bearing rather than declared.", first_slice: "Missing-required-field refusal, probe-paired.", red_control: "A record literal minus one required field refuses naming it; every corpus record literal compiles.", @@ -2226,19 +2435,109 @@ fn guarantee_ladder_nodes() -> List { handback: "The judgment site, probe flip, corpus receipt." ) ), - active_sized(identity: "rn_J5XDQKBF6J4YPGFKWQXE45KOQM", id: "compiler-accepted-obligation-closure", + active_sized(identity: "rn_6QXWDNSCBUAK6JWU7VSUWBXWMH", id: "accepted-obligation-door-mechanism", owner: "compiler-guarantee", path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", intricacy: IntricacyHigh, - volume: VolumeMedium, + volume: VolumeSmall, + t: fields( + headline: "The acceptance door as mechanism: audit first, blocking later", + boundary: "Split from the monolithic closure node (operator spine verdict: a door gated on the whole program delays the most important new mechanism until nearly done). This node lands the MECHANISM only, depending on the carrier alone: read every Required guarantee path, derive the missing/unmeasured/frontier/no-live-consumer census, run audit-mode and expecting-red — it reports, it does not yet block, and it never claims the open classes closed. The recovered P0.0 meta-guarantee gets its executable form here.", + displaced_cost: "The every-Required-path-has-a-live-pre-Accepted-consumer relation becomes executable years before the last wall lands.", + first_slice: "The closure fold over carrier rows with a synthetic Required-path-without-consumer RED.", + red_control: "A Required path with no live consumer counts; a FrontierAccepted path admitted into Accepted counts; the audit totals reconcile with the carrier's row population.", + out_of_scope: "Blocking (the two activation nodes); new walls.", + handback: "The door site, the audit census, the RED receipts." + ) + ), + active_sized(identity: "rn_5FUBPPKWUGPBAYADIIIR3LDZKQ", id: "accepted-floor-obligation-closure", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyMedium, + volume: VolumeSmall, + t: fields( + headline: "Floor activation: the ordinary compiler floor flips to blocking", + boundary: "The door mechanism flips from audit to refusal over the FLOOR domain only — names, call shape, conformance, fields, exhaustiveness, parse formation, record construction, the exemption gone: acceptance refuses when a floor class or path is Unmeasured, BelowFloor, FrontierAccepted, or lacks its live consumer.", + displaced_cost: "Below-baseline safety regressions become unshippable — the floor stops depending on review vigilance.", + first_slice: "Census-first over the floor domain (count, then flip), whole-tree green at flip.", + red_control: "The mechanism's audit census reads zero over the floor domain before the flip; a planted floor violation refuses after it.", + out_of_scope: "Extended domains (next node).", + handback: "The flip receipt and the zero-census reading.", + ) + ), + active_sized(identity: "rn_FAF4MACL7ZXJMZ3GX6DJIR4E5S", id: "extended-admission-cardinality", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyLow, + volume: VolumeSmall, + t: fields( + headline: "Cardinality joins the contract: the seam class flips to Required-blocking", + boundary: "Admit the cardinality-seam class into the door's blocking Required set — one class, one flip (per-class admission nodes replace the monolithic extended activation, review 45918: an admission becomes ready the day its OWN climb lands instead of every extended class waiting on the slowest). After the flip, acceptance refuses a program whose cardinality judgment is Unmeasured, BelowFloor, FrontierAccepted, or consumer-less.", + displaced_cost: "The first differentiating class stops being observability and becomes contract.", + first_slice: "The Required-roster row plus the planted possibly-empty-into-nonempty violation refusing at acceptance.", + red_control: "The planted seam violation refuses; whole-tree stays green at the flip.", + out_of_scope: "The climb itself (cardinality-vertical-slice); other classes' admissions.", + handback: "The admission receipt and the flip's green whole-tree run.", + ) + ), + active_sized(identity: "rn_7OIWVA5MTJIGEEJOBPJOKJTEQ2", id: "extended-admission-ambiguity", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyLow, + volume: VolumeSmall, + t: fields( + headline: "Method ambiguity joins the contract", + boundary: "Admit the method-ambiguity class into the door's blocking Required set once its wall lands (per-class admission, review 45918). After the flip, a program with a more-than-one-resolution judgment left Unmeasured, BelowFloor, FrontierAccepted, or consumer-less refuses at acceptance.", + displaced_cost: "The silent-first-hit dispatch class becomes contract, not census.", + first_slice: "The Required-roster row plus the planted two-candidate violation refusing at acceptance.", + red_control: "The planted ambiguous-dispatch violation refuses; whole-tree stays green at the flip.", + out_of_scope: "The wall itself (floor-method-ambiguity-wall); other classes' admissions.", + handback: "The admission receipt and the flip's green whole-tree run.", + ) + ), + active_sized(identity: "rn_QXI4FCLYEUMTP66BKPEGYLNPLV", id: "extended-admission-v2-realization", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyLow, + volume: VolumeSmall, + t: fields( + headline: "Target realization joins the contract", + boundary: "Admit the target-relative realization class into the door's blocking Required set once its gate lands (per-class admission, review 45918). After the flip, a program accepted for target T with a realization judgment left Unmeasured, BelowFloor, FrontierAccepted, or consumer-less refuses at acceptance.", + displaced_cost: "The costed-but-unrealized dispatch failure class becomes contract.", + first_slice: "The Required-roster row plus the planted unrealized-operation violation refusing at acceptance.", + red_control: "The planted unrealized-for-T violation refuses; whole-tree stays green at the flip.", + out_of_scope: "The gate itself (v2-target-realization-gate); other classes' admissions.", + handback: "The admission receipt and the flip's green whole-tree run.", + ) + ), + active_sized(identity: "rn_VAKMHRHJ64DJLMKHHSEKTXYLNP", id: "extended-admission-v2-translate", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyLow, + volume: VolumeSmall, + t: fields( + headline: "Translate containment joins the contract", + boundary: "Admit the Translate-containment class into the door's blocking Required set once the refusal lands (per-class admission, review 45918). After the flip, a program whose underived-fact judgment is Unmeasured, BelowFloor, FrontierAccepted, or consumer-less refuses at acceptance.", + displaced_cost: "The documented Translate fail-open, once closed, stays closed by contract rather than vigilance.", + first_slice: "The Required-roster row plus the planted swallowed-refusal violation refusing at acceptance.", + red_control: "The planted underived-fact violation refuses; whole-tree stays green at the flip.", + out_of_scope: "The containment itself (v2-translate-underived-refusal); other classes' admissions.", + handback: "The admission receipt and the flip's green whole-tree run.", + ) + ), + active_sized(identity: "rn_DOFYQJV7FMMCLMNWGE4EUDJSOY", id: "accepted-extended-obligation-closure", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyMedium, + volume: VolumeSmall, t: fields( - headline: "Accepted means established: the acceptance-completeness door closes the contract", - boundary: "The terminal P0 wall (the recovered P0.0 meta-guarantee — no exhaustive acceptance contract exists today): for every Required guarantee path in the claims carrier, exactly one live consumer, consumed before the relevant Accepted constructor; acceptance refuses when a required class or path is Unmeasured, BelowFloor, FrontierAccepted, or lacks its live consumer. Depends on the carrier and EVERY required-now climb — the floor walls, the ambiguity wall, and the cardinality seam included (review 45545: a door closing over known-open required judgments would certify its own headline falsely).", - displaced_cost: "The original thesis promise — if it compiles, declared intent is sound inside the modeled system — becomes checkable instead of aspirational; the carrier turns from observability into the contract.", - first_slice: "The closure check over the floor domain only, census-first (count, then flip).", - red_control: "A Required path with no live consumer reds; a FrontierAccepted path admitted into Accepted reds; the whole-tree compile stays green at flip.", - out_of_scope: "New walls (it consumes them); domains beyond the declared Required set.", - handback: "The door site, the refusal variants, the flip receipt." + headline: "Extended closure: the full contract roster certified complete", + boundary: "The terminal certification over the per-class admissions (recut per review 45918 — requires-all belongs HERE, on the completion receipt, never on the admission work, which is per-class and ready as each climb lands): every extended admission receipt present, the door's Required roster equal to the declared extended set — nothing climbed-but-unadmitted, nothing admitted-but-unclimbed — the full recovered P0.0 contract in force.", + displaced_cost: "The thesis promise — if it compiles, declared intent is sound inside the modeled system — becomes the shipping contract.", + first_slice: "The roster-completeness fold over admission receipts with a planted climbed-but-unadmitted RED.", + red_control: "A climbed-but-unadmitted class reds the roster check; an admitted-but-unclimbed row reds it; whole-tree stays green.", + out_of_scope: "New admissions (the per-class nodes own them); new walls.", + handback: "The contract roster receipt handed to residual prevalence.", ) ), active_sized(identity: "rn_W2KPJ5VLF3RMNDELVJXP6EKL5A", id: "primitive-identity-join", @@ -2248,12 +2547,42 @@ fn guarantee_ladder_nodes() -> List { volume: VolumeMedium, t: fields( headline: "One primitive identity: definition, per-target realization, and cost as three facts on one key", - boundary: "PrimitiveDefinition (invocation forms, receiver domain, parameters, result), PrimitiveRealization per target, PrimitiveCostModel — joined by one identity so a call cannot compile for target T without exactly one compatible definition and realization, and interpreter/emitter dispatch derive from realization rows instead of five independently-writable name sets (registry map, PrimitiveContract rows, algebra templates, interpreter arms, per-target templates).", - displaced_cost: "Closes the gunbc#7479 CLASS rather than the specimen: filter_map had a cost contract, no seed method, and a green compile. Unlocks the method ambiguity wall (sameness becomes decidable) and un-narrows the method-existence wall — the measured length/count/size dispatch fork on gunbc#7484 is why per-receiver checking stalls at kernel receivers today.", - first_slice: "The identity type plus the join for the collection-algebra family (map/filter/fold/flat_map/filter_map), with the registry and contract rows re-keyed onto it.", - red_control: "A costed-but-unrealized primitive refuses target-relatively; a realized-but-undefined name cannot be keyed; existing corpus calls all still resolve.", - out_of_scope: "Deleting the seed dispatch (climbs by self-host deletion); new primitives.", - handback: "The three carrier types, the joined family roster, the derived-dispatch receipt." + boundary: "PrimitiveDefinition (identity + PrimitiveSemanticContract), PrimitiveInvocationSurface, PrimitiveRealization per RealizationTarget, PrimitiveCostFact, and PrimitiveTraversalOrderFact — five separate carriers joined by PrimitiveIdentity, never one mega-row. TERMINAL acceptance: every primitive-bearing surface across all five authorities (registry, PrimitiveContract rows, algebra templates, interpreter dispatch, emit handlers) joins exactly one identity; population-wide join and realization refusals are the closing contract. D0 is a first slice only — not the closing contract. Namespace supplies declaration identity; this node supplies semantic primitive identity across invocation and realization.", + displaced_cost: "Proves the determinism denominator is open (#7532 honest rung) and begins closing it: four surfaces feed primitive_surface_census_derived at runtime; mutation control is CLOSED on AlgebraTemplate and EmitHandler, PARTIAL on BuiltinRegistry and PrimitiveContractRow (parallel hand rosters — primitive_d0_derived_surface_drift_audit_note), NOT DISCOVERED on InterpreterDispatch. fold_list spelling fork and index_by registry+algebra dual absence are named specimens proving the denominator is not closed. Unlocks method ambiguity wall and D1 determinism consumption through PrimitiveIdentity rather than raw symbol.", + first_slice: "D0 first slice (NOT closing contract): std.primitive_identity carriers; primitive_surface_census_derived from four authorities; primitive_interpreter_dispatch_measurement_rows (MeasurementOnly) for specimens; refusal predicate machinery; slice witness receipt. Denominator NOT closed — interpreter blind until interpreter-primitive-roster lands.", + red_control: "TERMINAL (population-wide): every derived census row joins exactly one PrimitiveIdentity; surfaces joining two identities refuse; realizations naming unknown primitives refuse; duplicate semantic definitions refuse. D0 SLICE ONLY: derived census exceeds specimen count; four named specimen findings (map_keys five-surface join, fold_list spelling fork, fold_list registry gap, index_by registry+algebra gap); planted missing-identity and unknown-realization RED controls — these witnesses do not substitute for terminal acceptance.", + out_of_scope: "D1 determinism gate consumption (child determinism-denominator-closure); deleting v1 seed dispatch tables; namespace declaration identity.", + handback: "D0: std.primitive_identity carriers, primitive_surface_census_derived, measurement specimen rows, refusal predicates, slice witness receipt. TERMINAL (after interpreter-primitive-roster): population-wide join closure across all five surfaces." + ) + ), + active_sized(identity: "rn_DETERMINISM_DENOMINATOR_CLOSURE", id: "determinism-denominator-closure", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyHigh, + volume: VolumeMedium, + t: fields( + headline: "Determinism denominator closure: every primitive carries one traversal-order classification", + boundary: "v2.std.determinism and v2.lens.determinism consume std.primitive_identity: every PrimitiveDefinition carries exactly one PrimitiveTraversalOrderFact, unclassified refuses, HostUnspecifiedOrder derives a determinism root, aliases resolve through PrimitiveIdentity not raw symbol. Adding a new invokable primitive realization without a traversal-order fact reds before any caller uses it.", + displaced_cost: "Dissolves the interim two-root registry (#7532) into a closed denominator — the gate can tell an alias from a distinct operation, and keys/values false-positive class is eliminated by identity not spelling.", + first_slice: "Wire determinism_compile_gate to PrimitiveTraversalOrderFact over the primitive_identity census rather than determinism_root_registry symbol matching.", + red_control: "A new primitive realization without traversal-order fact refuses at gate enrollment before corpus reach; map_keys and map_values classify HostUnspecifiedOrder through identity; generic keys/values symbols do not false-positive unrelated functions.", + out_of_scope: "D0 census and join carriers (parent primitive-identity-join); deleting v1 seed dispatch.", + handback: "Identity-keyed determinism classification, closed denominator receipt, deleted interim symbol registry." + ) + ), + active_sized(identity: "rn_INTERPRETER_PRIMITIVE_ROSTER", id: "interpreter-primitive-roster", + owner: "compiler-guarantee", + path: "docs/plans/compiler-guarantee-recovery-gap-analysis.md", + intricacy: IntricacyHigh, + volume: VolumeMedium, + t: fields( + headline: "Interpreter primitive roster: the fifth surface becomes readable from .dag", + boundary: "Land interpreter-primitive-roster across all six v1_interpreter dispatch sites (eval_builtin_inner, eval_algebra_method_inner, eval_call, try_v2_std_collection_map_primitive_grounding, try_parse_table_memo_dispatch, eval_method_call lookup short-circuit — 158 arms / 169 spellings). clever-crab-111 macro authority: eval_builtin_inner arms expand from interpreter_free_call_arm_spellings with construction wall (arm without roster entry unwritable). primitive_surface_census_derived absorbs InterpreterDispatch instead of primitive_interpreter_dispatch_measurement_rows. Prerequisite for primitive-identity-join terminal acceptance.", + displaced_cost: "Closes the fifth blind spot in D1 mutation control across all six dispatch sites. Dissolves MeasurementOnly specimen rows into derived surfaces.", + first_slice: "Roster authority for six sites; eval_builtin_inner via interpreter_free_call_arm_spellings macro; primitive_surface_census_derived absorbs interpreter dispatch; measurement rows deleted.", + red_control: "A new dispatch arm without a roster row refuses; a roster row without a matching arm refuses; fold_list via eval_call intercept and lookup dual-realization join through derived census.", + out_of_scope: "D0 slice carriers and four-surface derivation (parent primitive-identity-join first slice); deleting seed dispatch implementation.", + handback: "Modeled interpreter roster authority, derived fifth surface on primitive_surface_census_derived, deleted measurement scaffold." ) ), active_sized(identity: "rn_JPU4RUV5DNELDCZI4JUQGWHICM", id: "compiler-source-exemption-removal", @@ -2263,7 +2592,7 @@ fn guarantee_ladder_nodes() -> List { volume: VolumeMedium, t: fields( headline: "Delete module_skips_direct_call_arg_check: the compiler stops authoring itself under weaker checks", - boundary: "Run the call-shape and inhabitance probes over v2.* and v1.compiler.*, classify every failure fresh (the historical 104 TypeMismatch figure has no locatable receipt and is neither blocker nor promise), repair the compatibility relation or the source, then delete the exemption. Its deletion is the terminal Tier-1 acceptance named by the doc-graph bind on this lane's analysis.", + boundary: "Gated on the two GROUNDING nodes, not the label wall (dependency corrected per the operator spine verdict: the exemption skips the argument TYPE judgment, whose false positives are representation gaps — labels never gated it, and gunbc#7519's wall already runs exemption-free). Once argument-type-compatibility grounding and declared-conformance grounding land, re-run exemption-free, repair relation or source per the bucketed census, then delete. Its deletion is the terminal Tier-1 acceptance named by the doc-graph bind on this lane's analysis.", displaced_cost: "The dimension contract's one named escape hatch (enforced universally — no escape hatch, no wrapper functions); v1 currently admits unsound v2 source that v2 then self-certifies, and v1 deletion would not retroactively prove it.", first_slice: "The fresh adjudication run with its failure classification, before any relation change.", red_control: "After deletion, a planted arg-type mismatch inside a v2.* module refuses exactly like user source; the whole-tree compile stays green.", @@ -2321,41 +2650,62 @@ fn guarantee_ladder_nodes() -> List { fn guarantee_ladder_edges() -> List { [ + edge(child: "ladder-probe-corpus", parent: "ladder-measurement-schema"), + edge(child: "ladder-claims-carrier", parent: "ladder-measurement-schema"), edge(child: "ladder-claims-carrier", parent: "ladder-probe-corpus"), edge(child: "guarantee-baseline-prevalence", parent: "ladder-probe-corpus"), edge(child: "guarantee-baseline-prevalence", parent: "ladder-claims-carrier"), - edge(child: "floor-method-existence-wall", parent: "guarantee-baseline-prevalence"), - edge(child: "floor-call-shape-wall", parent: "guarantee-baseline-prevalence"), - edge(child: "floor-inhabitance-wall", parent: "guarantee-baseline-prevalence"), + edge(child: "ladder-census-emitters", parent: "ladder-claims-carrier"), + edge(child: "call-missing-and-duplicate-wall", parent: "guarantee-baseline-prevalence"), + edge(child: "call-signature-resolution-coverage", parent: "guarantee-baseline-prevalence"), + edge(child: "method-receiver-normalization", parent: "guarantee-baseline-prevalence"), + edge(child: "declared-conformance-grounding", parent: "guarantee-baseline-prevalence"), + edge(child: "argument-type-compatibility-grounding", parent: "guarantee-baseline-prevalence"), edge(child: "floor-exhaustiveness-refusal", parent: "guarantee-baseline-prevalence"), edge(child: "floor-generic-field-constraint-wall", parent: "guarantee-baseline-prevalence"), edge(child: "floor-parse-formation-wall", parent: "guarantee-baseline-prevalence"), edge(child: "floor-record-construction-wall", parent: "guarantee-baseline-prevalence"), - edge(child: "floor-method-ambiguity-wall", parent: "floor-method-existence-wall"), + edge(child: "method-zero-resolution-general-wall", parent: "method-receiver-normalization"), + edge(child: "method-call-shape-seam", parent: "method-established-surface-wall"), + edge(child: "declared-conformance-general-wall", parent: "declared-conformance-grounding"), edge(child: "floor-method-ambiguity-wall", parent: "primitive-identity-join"), + edge(child: "floor-method-ambiguity-wall", parent: "method-zero-resolution-general-wall"), + edge(child: "compiler-source-exemption-removal", parent: "argument-type-compatibility-grounding"), + edge(child: "compiler-source-exemption-removal", parent: "declared-conformance-grounding"), edge(child: "cardinality-vertical-slice", parent: "ladder-claims-carrier"), edge(child: "cardinality-vertical-slice", parent: "unforgeable-construction-capability"), - edge(child: "v2-self-grounding-frontier", parent: "ladder-claims-carrier"), edge(child: "v2-translate-underived-refusal", parent: "v2-self-grounding-frontier"), edge(child: "v2-inferred-tree-completeness", parent: "v2-translate-underived-refusal"), edge(child: "v2-node-kind-derivation-coverage", parent: "v2-inferred-tree-completeness"), edge(child: "v2-target-realization-gate", parent: "v2-node-kind-derivation-coverage"), edge(child: "v2-target-realization-gate", parent: "primitive-identity-join"), - edge(child: "ladder-census-emitters", parent: "ladder-claims-carrier"), - edge(child: "compiler-source-exemption-removal", parent: "floor-call-shape-wall"), - edge(child: "compiler-source-exemption-removal", parent: "floor-inhabitance-wall"), - edge(child: "compiler-accepted-obligation-closure", parent: "ladder-claims-carrier"), - edge(child: "compiler-accepted-obligation-closure", parent: "floor-method-existence-wall"), - edge(child: "compiler-accepted-obligation-closure", parent: "compiler-source-exemption-removal"), - edge(child: "compiler-accepted-obligation-closure", parent: "floor-record-construction-wall"), - edge(child: "compiler-accepted-obligation-closure", parent: "floor-generic-field-constraint-wall"), - edge(child: "compiler-accepted-obligation-closure", parent: "floor-exhaustiveness-refusal"), - edge(child: "compiler-accepted-obligation-closure", parent: "floor-parse-formation-wall"), - edge(child: "compiler-accepted-obligation-closure", parent: "v2-translate-underived-refusal"), - edge(child: "compiler-accepted-obligation-closure", parent: "v2-target-realization-gate"), - edge(child: "compiler-accepted-obligation-closure", parent: "floor-method-ambiguity-wall"), - edge(child: "compiler-accepted-obligation-closure", parent: "cardinality-vertical-slice"), - edge(child: "guarantee-residual-prevalence", parent: "compiler-accepted-obligation-closure"), + edge(child: "determinism-denominator-closure", parent: "primitive-identity-join"), + edge(child: "primitive-identity-join", parent: "interpreter-primitive-roster"), + edge(child: "accepted-obligation-door-mechanism", parent: "ladder-claims-carrier"), + edge(child: "accepted-floor-obligation-closure", parent: "accepted-obligation-door-mechanism"), + edge(child: "accepted-floor-obligation-closure", parent: "call-missing-and-duplicate-wall"), + edge(child: "accepted-floor-obligation-closure", parent: "call-signature-resolution-coverage"), + edge(child: "accepted-floor-obligation-closure", parent: "method-zero-resolution-general-wall"), + edge(child: "accepted-floor-obligation-closure", parent: "method-call-shape-seam"), + edge(child: "accepted-floor-obligation-closure", parent: "declared-conformance-general-wall"), + edge(child: "accepted-floor-obligation-closure", parent: "compiler-source-exemption-removal"), + edge(child: "accepted-floor-obligation-closure", parent: "floor-exhaustiveness-refusal"), + edge(child: "accepted-floor-obligation-closure", parent: "floor-generic-field-constraint-wall"), + edge(child: "accepted-floor-obligation-closure", parent: "floor-parse-formation-wall"), + edge(child: "accepted-floor-obligation-closure", parent: "floor-record-construction-wall"), + edge(child: "extended-admission-cardinality", parent: "accepted-floor-obligation-closure"), + edge(child: "extended-admission-cardinality", parent: "cardinality-vertical-slice"), + edge(child: "extended-admission-ambiguity", parent: "accepted-floor-obligation-closure"), + edge(child: "extended-admission-ambiguity", parent: "floor-method-ambiguity-wall"), + edge(child: "extended-admission-v2-realization", parent: "accepted-floor-obligation-closure"), + edge(child: "extended-admission-v2-realization", parent: "v2-target-realization-gate"), + edge(child: "extended-admission-v2-translate", parent: "accepted-floor-obligation-closure"), + edge(child: "extended-admission-v2-translate", parent: "v2-translate-underived-refusal"), + edge(child: "accepted-extended-obligation-closure", parent: "extended-admission-cardinality"), + edge(child: "accepted-extended-obligation-closure", parent: "extended-admission-ambiguity"), + edge(child: "accepted-extended-obligation-closure", parent: "extended-admission-v2-realization"), + edge(child: "accepted-extended-obligation-closure", parent: "extended-admission-v2-translate"), + edge(child: "guarantee-residual-prevalence", parent: "accepted-extended-obligation-closure"), ] } diff --git a/dag/gunbc/roadmap_belt_actuate.dag b/dag/gunbc/roadmap_belt_actuate.dag index 1ca2c2c7720..797b6b10b5b 100644 --- a/dag/gunbc/roadmap_belt_actuate.dag +++ b/dag/gunbc/roadmap_belt_actuate.dag @@ -161,14 +161,20 @@ import gunbc.roadmap_validation_oracle { validation_receipt_json, } import extdeps.git { shape_git_rev_parse_head_args } -import gunbc.roadmap_provider_events { ProviderCompleted } +import gunbc.roadmap_provider_events { ProviderCompleted, provider_execution_activity } import extdeps.version { VersionIdentity } import std.types { ContentHash } import gunbc.roadmap_workflow_progress { WorkflowAttemptEvidence, WorkflowAttemptProgress, workflow_attempt_progress, - workflow_attempt_progress_json_value, + workflow_attempt_progress_json_members, +} +import gunbc.roadmap_presentation { + activity_view_from_progress, + activity_view_json_value, + session_facts_view, + session_facts_json_value, } import extdeps.gunbc import extdeps.shell @@ -1333,11 +1339,15 @@ type BeltObserve = Observed { live: List } | ObserveRefused { reason: String } -data belt_observe_note: String = "tmux-present is the FIRST precondition: tmux not on PATH -> ObserveRefused (the belt cannot know what is live). Once tmux is present the observe half must NOT widen an exec failure into a zero-session answer (the §5 absorbing-fallback / ⊤-as-answer-vs-⊤-as-ignorance conflation). So the decision (belt_observe_from_result) gates on exit success: `tmux ls` exit 0 -> parse stdout into the true session set. On FAILURE only tmux's two DOCUMENTED empty signals count as a genuine zero observation — stderr containing 'no server running' or 'no sessions' (grounded in the extdeps tmux.Session.List exit contract `1 => Unit \"No sessions\"`, dag/extdeps/tmux/tmux.dag) -> Observed []. EVERY OTHER failure (socket/permission/transient error, empty-stderr non-zero exit) is an UNKNOWABLE session set -> ObserveRefused (typed, located, carries stderr), never a blind spawn/reap. First-tick bootstrap still works: no server yields the documented 'no server running' signal -> Observed [] -> spawns proceed. The decision is a pure fn so every arm is witnessed (belt_observe_from_result witnesses); belt_observe only threads the live WitnessBin.Run result fields into it." +data belt_observe_note: String = "tmux-present is the FIRST precondition: tmux not on PATH -> ObserveRefused (the belt cannot know what is live). Once tmux is present the observe half must NOT widen an exec failure into a zero-session answer (the §5 absorbing-fallback / ⊤-as-answer-vs-⊤-as-ignorance conflation). So the decision (belt_observe_from_result) gates on exit success: `tmux ls` exit 0 -> parse stdout into the true session set. On FAILURE only tmux's three DOCUMENTED empty spellings count as a genuine zero observation — stderr containing 'no server running', 'no sessions', or the no-server socket ENOENT spelling 'error connecting to ... (No such file or directory)' (grounded in extdeps.tmux tmux.Session.List and tmux_client_connect_error_note) -> Observed []. EVERY OTHER failure (socket permission/transient error, empty-stderr non-zero exit) is an UNKNOWABLE session set -> ObserveRefused (typed, located, carries stderr), never a blind spawn/reap. In particular, 'error connecting to ... (Connection refused)' is not the cited ENOENT fact and keeps refusing. First-tick bootstrap works under either no-server spelling: the empty observation admits the first spawn that creates the instance socket. The decision is a pure fn so every arm is witnessed (belt_observe_from_result witnesses); belt_observe only threads the live WitnessBin.Run result fields into it." fn tmux_ls_stderr_reports_empty(stderr: String) -> Bool { string_contains(s: stderr, pattern: "no server running") || string_contains(s: stderr, pattern: "no sessions") + || ( + string_contains(s: stderr, pattern: "error connecting to ") + && string_contains(s: stderr, pattern: "(No such file or directory)") + ) } fn belt_observe_from_result( @@ -2253,6 +2263,17 @@ fn belt_workflow_attempts_observe_for_instance( } } +data belt_attempt_presentation_note: String = "The wire join of the two authorities the client must never re-derive: the attempt's progress members (gunbc.roadmap_workflow_progress, unchanged) plus the DECIDED activity view (gunbc.roadmap_presentation) appended as one 'activity' member. The seam reads process and current activity from INSIDE the progress carrier (one_progress_authority_note — the belt cannot pass a disagreeing process beside it), so this join is one call on one value. The join lives here because progress cannot import presentation without a cycle; the member-list seam (workflow_attempt_progress_json_members) is what lets the belt extend the object without re-stating or re-matching it." + +fn belt_workflow_attempt_presentation_json( + progress: WorkflowAttemptProgress, +) -> JsonValue { + json_object(members: concat( + workflow_attempt_progress_json_members(progress: progress), + [ json_kv(key: "activity", value: activity_view_json_value( + view: activity_view_from_progress(progress: progress))) ])) +} + fn belt_workflow_attempts_json_value( observed: BeltWorkflowAttempts, ) -> JsonValue { @@ -2271,7 +2292,7 @@ fn belt_workflow_attempts_json_value( key: "attempts", value: json_array(elements: map( attempts, - progress => workflow_attempt_progress_json_value( + progress => belt_workflow_attempt_presentation_json( progress: progress, ), )), @@ -2506,6 +2527,18 @@ fn belt_sessions_json_value(o: BeltObserve) -> JsonValue { b: dispatch_session_cleanup_allowed(cleanup: cleanup))), json_kv(key: "cleanup_detail", value: json_string( s: dispatch_session_cleanup_detail(cleanup: cleanup))), + json_kv(key: "facts", value: session_facts_json_value( + v: session_facts_view( + process: sess.worker.process, + process_detail: worker_process_evidence_detail(evidence: sess.worker.process), + provider: provider_evidence_label(evidence: sess.worker.provider_instance), + configured_provider: provider_kind_label(kind: sess.worker.configured_provider.kind), + provider_readiness: provider_readiness_label(readiness: sess.worker.provider_readiness), + session_name: sess.session_name, + cleanup_action: dispatch_session_cleanup_action(cleanup: cleanup), + cleanup_allowed: dispatch_session_cleanup_allowed(cleanup: cleanup), + cleanup_detail: dispatch_session_cleanup_detail(cleanup: cleanup), + ))), ]) }))), ]) diff --git a/dag/gunbc/roadmap_component.dag b/dag/gunbc/roadmap_component.dag index b739553c1fc..c011c0069f3 100644 --- a/dag/gunbc/roadmap_component.dag +++ b/dag/gunbc/roadmap_component.dag @@ -5,15 +5,6 @@ import std.measure { Millisecond, millisecond, millisecond_count } import extdeps.languages.css.values { CssLength, css_ch, css_length_wire } import gunbc.design.scale { ScaleToken, space_4, space_8, height_row } import gunbc.roadmap_altitude { band_expands } -import gunbc.client_observation_state { - ClientObservationState, - ClientObservationPending, ClientObservationOff, ClientObservationOn, ClientObservationRefused, - client_observation_state_key, -} -import gunbc.roadmap_workflow_stage { - WorkflowSegmentKind, - roadmap_attempt_workflow_segment_kinds, -} import gunbc.design.component { Component, ComponentState, ComponentStateKind, IdleState, RequestedState, TerminalState, @@ -25,6 +16,9 @@ import gunbc.design.theme { day_theme, night_theme, } import gunbc.design.interaction { DispatchBtnElement, Approach, Dispatch } +import gunbc.roadmap_presentation { + activity_state_sep, activity_detail_sep, activity_evidence_kv_sep, activity_disclosure_label, +} import gunbc.roadmap_belt_actuate { belt_dispatch_all_status_labels, belt_dispatch_label_is_ok, belt_dispatch_ok_status_labels, belt_dispatch_path_prefix, @@ -128,7 +122,6 @@ fn dispatch_caption_set() -> List { ], [ concat(dispatch_ack_prefix, dispatch_ack_word), concat(dispatch_ack_prefix, belt_dispatch_disabled_status_label()), - concat(dispatch_session_chip_prefix, dispatch_caption_present_word), ]) } @@ -144,13 +137,12 @@ fn dispatch_reserved_width() -> String { css_length_wire(l: dispatch_reserved_length()) } -data dispatch_session_chip_note: String = "The POST acknowledgement and the session observation are two different captions because they are two different facts. An ok POST renders `dispatch · accepted`: worktree/process creation returned success, or the idempotent request found the desired dispatch already accepted. Only the independent /sessions.json observation may render `session · present`, and that wording claims tmux/process presence only — never provider readiness, execution, or completion. The response's exact spawned/already_live wire label remains in the title channel. +data dispatch_session_chip_note: String = "The POST acknowledgement and the session observation are two different captions because they are two different facts. An ok POST renders `dispatch · accepted`: worktree/process creation returned success, or the idempotent request found the desired dispatch already accepted. Only the independent /sessions.json observation may show the session sentence — and at A2 that sentence is DECIDED by gunbc.roadmap_presentation.session_facts_view and carried on the wire under `facts`; this client paints it verbatim, so no session prose constant lives here anymore (operator blocker 5). The wording claims tmux/process presence only — never provider readiness, execution, or completion. The response's exact spawned/already_live wire label remains in the title channel. The observe-only posture independently renders `dispatch · disabled`; it is neither an accepted dispatch nor session evidence. All three captions stay inside the reserved width derived from the caption set, so re-observation cannot reflow the row. The session chip remains type-distinct from the node's status chip at the head of the row." data dispatch_ack_prefix: String = "dispatch · " data dispatch_ack_word: String = "accepted" -data dispatch_session_chip_prefix: String = "session · " data sessions_route_path: String = "/sessions.json" data dashboard_instance_route_path: String = "/instance.json" @@ -297,86 +289,30 @@ fn dashboard_instance_fetch_statements() -> List { data attempts_route_path: String = "/attempts.json" data workflow_route_path: String = "/workflow.json" -type WorkflowLamp - = SessionWorkflowLamp - | AttemptWorkflowLamp - -data workflow_lamps_v0: List = [ - SessionWorkflowLamp, - AttemptWorkflowLamp, -] - -fn workflow_lamp_key(lamp: WorkflowLamp) -> String { - match lamp { - SessionWorkflowLamp => "session" - AttemptWorkflowLamp => "attempt" - } -} - -fn workflow_lamp_label(lamp: WorkflowLamp) -> String { - match lamp { - SessionWorkflowLamp => "Session" - AttemptWorkflowLamp => "Attempt" - } -} - -fn workflow_lamp_state( - lamp: WorkflowLamp, - session: ClientObservationState, - attempt: ClientObservationState, -) -> ClientObservationState { - match lamp { - SessionWorkflowLamp => session - AttemptWorkflowLamp => attempt - } -} - -fn workflow_lamp_initial_state(lamp: WorkflowLamp) -> ClientObservationState { - workflow_lamp_state( - lamp: lamp, - session: ClientObservationPending, - attempt: ClientObservationPending) -} - data workflow_controls_class: String = "workflow-controls" -data workflow_strip_class: String = "workflow-strip" -data workflow_lamp_class: String = "workflow-lamp" -data workflow_lamp_indicator_class: String = "workflow-lamp-indicator" -data workflow_lamp_label_class: String = "workflow-lamp-label" -data workflow_lamp_data_attribute: String = "data-workflow-lamp" -data workflow_observation_data_attribute: String = "data-observation" -data workflow_lamp_label_data_attribute: String = "data-label" - -data workflow_progress_stages: List = roadmap_attempt_workflow_segment_kinds - -fn workflow_progress_stage_key(stage: WorkflowSegmentKind) -> String { - gunbc.roadmap_workflow_stage.workflow_segment_kind_key(kind: stage) -} -fn workflow_progress_stage_label(stage: WorkflowSegmentKind) -> String { - gunbc.roadmap_workflow_stage.workflow_segment_kind_short_label(kind: stage) -} - -data workflow_progress_class: String = "workflow-progress" -data workflow_stage_class: String = "workflow-stage" -data workflow_activity_class: String = "workflow-activity" -data workflow_stage_data_attribute: String = "data-workflow-stage" -data workflow_segment_state_data_attribute: String = "data-state" +data node_activity_class: String = "node-activity" +data activity_summary_class: String = "activity-summary" +data activity_detail_class: String = "activity-detail" +data activity_ledger_class: String = "activity-ledger" +data activity_obligation_class: String = "activity-obligation" +data activity_evidence_class: String = "activity-evidence" +data activity_evidence_row_class: String = "activity-evidence-row" +data activity_located_reason_class: String = "activity-located-reason" +data session_facts_class: String = "session-facts" -data workflow_progress_note: String = "The seven-obligation attempt pipeline is presentation over /workflow.json evidence, not a stored stage machine and not a replacement for independent Session/Attempt facts. Environment and Worktree are constructive receipts; Agent comes from provider events reconciled with the shared worker-process evidence; Verify, Publish, Review, and Audit remain visibly pending until their own producers exist. Both markup and server desired facts consume roadmap_attempt_workflow_segment_kinds, while the client writes each segment's observed state/detail and makes failed/refused state visible in the stage label rather than relying on border style alone. A provider turn completing cannot move downstream obligations. Workflow observation is single-flight: the next timer is armed only in the settled promise's finally arm, so a slow/refused route cannot accumulate overlapping requests against the interpreted single-threaded server." +data node_activity_region_note: String = "The client realization of the archetype's UnderHead ActivityRegion (A2 — roadmap_row_archetype_note records the falsifier that forced it; the lamp strip, seven pre-rendered stage chips, and 'waiting' text it replaces are deleted, not hidden). The container exists ONLY while the decided /workflow.json activity view says an attempt exists — the attention rule's 'no attempt, no furniture' is realized as DOM absence, not display:none. All content is painted VERBATIM from the decided view (gunbc.roadmap_presentation, single projection authority): activity.summary/outcome/reason are final strings, data-arm and data-attention are the decided keys the stylesheet materializes, the obligation ledger renders the wire's canonical segment rows, and evidence rows are label/value pairs. The client holds NO composition policy — a summary sentence assembled in TypeScript is the two-authorities defect this region dissolved. The detail disclosure auto-opens ONCE per anomaly transition (data-auto-opened guard), honoring both the attention rule's auto-expand and reason_surface_producers_note's ruling that a poll tick must never fight the operator's hands. Session-observation facts (/sessions.json, the independent presence source) render into the row disclosure's session-facts line, never into the actuator cell and never onto the dispatch button — the button is an action, not a status report." -data workflow_lamps_note: String = "Workflow lamps v0 (operator adjudication 2026-07-26, vocabulary corrected 2026-07-28): Session and Attempt are two independent projections over /sessions.json and /attempts.json. workflow_lamps_v0 declares their presentation order for the row strip; workflow_lamp_state selects each axis without collapsing them into one workflow status. Every lamp starts ClientObservationPending and can become Off, On, or Refused only through the shared gunbc.client_observation_state authority. Session means the tmux container is present—not that its process is running—and process lifecycle is rendered separately. The strip is evidence presentation only: its rightmost lit member never gates an action and is never persisted. gunbc.client_observation_state.work_item_workflow_fact_note records the seven-fact join ruling without re-declaring those facts as a parallel roster. Proposed and Landed do not earn lamps until the typed attempt/branch/PR/node join exists; that binding, not more visual states, is the next slice." +data workflow_progress_note: String = "The seven-obligation attempt pipeline is presentation over /workflow.json evidence, not a stored stage machine. Environment and Worktree are constructive receipts; Agent comes from provider events reconciled with the shared worker-process evidence; Verify, Publish, Review, and Audit remain pending until their own producers exist. The server decides the presentation (the 'activity' member on each wire attempt, derived by gunbc.roadmap_presentation from roadmap_attempt_workflow_segment_kinds and the reconcile report); the client paints it into the UnderHead activity region without policy. A provider turn completing cannot move downstream obligations. Workflow observation is single-flight: the next timer is armed only in the settled promise's finally arm, so a slow/refused route cannot accumulate overlapping requests against the interpreted single-threaded server." -data dispatch_caption_live_word_note: String = "The session route observes tmux container presence independently from pane/process lifecycle. Its caption is therefore `present`, never `live` or `executing`; the Session lamp names that same narrow fact. A neighboring process chip renders running, exited, absent, or unobserved from the pane source." - -data dispatch_caption_present_word: String = "present" +data dispatch_caption_live_word_note: String = "The session route observes tmux container presence independently from pane/process lifecycle. Its decided sentence says `present`, never `live` or `executing` (the wording now lives in gunbc.roadmap_presentation.session_facts_view, which composes it from the shared worker-process evidence)." data dispatch_class_requested: String = "dispatch-requested" data dispatch_class_ok: String = "dispatch-ok" data dispatch_class_refused: String = "dispatch-refused" data dispatch_class_loud: String = "dispatch-loud" -data roadmap_row_archetype_note: String = "The RoadmapRow archetype (composition slice 2; refined by workflow lamps v0 without adding a head track): the row's five regions remain status chip, title, meta, disclosure, actuator. The ActuatorRegion is realized by one always-present workflow-controls wrapper containing the workflow strip and the optional dispatch control; dynamic process evidence and its stop/clear/unavailable cleanup action also land inside that wrapper. This preserves the derived auto 1fr auto grid while giving done/superseded rows an observation surface and preventing a cleanup action or lamp strip from becoming an implicit fourth grid item. The title owns the flex track; meta FLOWS inside it; disclosure sits under the head. Density rides roadmap_row_height and roadmap_row_gutter. The full-size instrument specimen remains outside .node-head in the sandbox." +data roadmap_row_archetype_note: String = "The RoadmapRow archetype (composition slice 2; ActivityRegion added at A2 after the live-session falsifier, operator 2026-08-01 — docs/plans/roadmap-presentation-seam-design.md section 1). The prior revision of this note declared the fusion the falsifier refuted: it packed the workflow strip, dynamic process evidence, AND the cleanup action into the one actuator wrapper 'to preserve the derived auto 1fr auto grid' — evidence about an action and the action itself sharing one region because the layout had one cell left. Under the first realistic live state the intrinsic-width actuator track starved the 1fr title on WIDE screens (the stacked realization fires only below the compact breakpoint, so the pathology never reached it). The correction is regional, not spatial: ActivityRegion — what is happening to this work — is its own region placed UnderHead, where width is not contested with the title; ActuatorRegion shrinks back to ACTIONS only (dispatch, stop); evidence rows render inside the activity region's disclosure, never as head-track furniture. The head grid stays the derived auto 1fr auto with a small third track. The activity content itself is the decided ActivityView from gunbc.roadmap_presentation (single projection authority across server and client); the attention rule governs its rendering — no attempt means NO activity DOM at all. The title owns the flex track; meta FLOWS inside it; disclosure sits under the head. Density rides roadmap_row_height and roadmap_row_gutter. The full-size instrument specimen remains outside .node-head in the sandbox." type RowTrack = AutoTrack @@ -398,6 +334,7 @@ type RowRegion = StatusChipRegion | TitleRegion | MetaRegion + | ActivityRegion | DisclosureRegion | ActuatorRegion @@ -406,6 +343,7 @@ fn row_region_key(r: RowRegion) -> String { StatusChipRegion => "status-chip" TitleRegion => "title" MetaRegion => "meta" + ActivityRegion => "activity" DisclosureRegion => "disclosure" ActuatorRegion => "actuator" } @@ -420,6 +358,7 @@ data roadmap_row_archetype: List = [ RoadmapRowRegion { region: StatusChipRegion, placement: HeadTrack { track: AutoTrack } }, RoadmapRowRegion { region: TitleRegion, placement: HeadTrack { track: FlexTrack } }, RoadmapRowRegion { region: MetaRegion, placement: InTitleFlow }, + RoadmapRowRegion { region: ActivityRegion, placement: UnderHead }, RoadmapRowRegion { region: DisclosureRegion, placement: UnderHead }, RoadmapRowRegion { region: ActuatorRegion, placement: HeadTrack { track: AutoTrack } }, ] @@ -440,6 +379,31 @@ fn roadmap_row_grid_template() -> String { " ") } +data row_head_column_note: String = "Explicit head-cell placement, derived from the SAME archetype the grid template folds (A3 routine-state collapse): once the status chip renders only when its lifecycle discriminates, the head can have fewer children than tracks, and implicit grid auto-placement would slide the title into the chip's auto track. Each head cell therefore carries an explicit grid-column derived from its region's HeadTrack position in roadmap_row_archetype — one authority for both the template and the placement, so an archetype reorder moves both together and a hand-drifted column is unwritable. A region not placed in a head track derives 0, which the archetype witness refuses (a 0 column never reaches emission while the witness is enrolled)." + +type RowHeadColumnScan { + col: Int + found: Int +} + +fn roadmap_row_head_column(region_key: String) -> Int { + let scan = fold(roadmap_row_archetype, init: RowHeadColumnScan { col: 0, found: 0 }, f: (acc, r) => match r.placement { + HeadTrack { track: _ } => + if acc.found > 0 { + acc + } else { + if row_region_key(r: r.region) == region_key { + RowHeadColumnScan { col: acc.col + 1, found: acc.col + 1 } + } else { + RowHeadColumnScan { col: acc.col + 1, found: 0 } + } + } + InTitleFlow => acc + UnderHead => acc + }) + scan.found +} + data node_mid_class_note: String = "The title track's cell: the wrapper that carries the title plus the flowing meta regions (badges, carrier links). It exists because the archetype's grid has exactly the head tracks as columns and a node's badge count varies — meta declared InTitleFlow realizes as flex flow inside this cell, never as a column that would ragged the grid." data node_mid_class: String = "node-mid" @@ -1369,16 +1333,6 @@ fn swatch_toggle_bind_statements() -> List { ] } -data js_set_client_observation_fn: String = "gunbcSetClientObservation" - -fn workflow_lamp_selector(lamp: WorkflowLamp) -> String { - join([ - "[", workflow_lamp_data_attribute, "=\"", - workflow_lamp_key(lamp: lamp), - "\"]", - ], "") -} - fn workflow_row_selector_expr(node_id: TsExpr) -> TsExpr { ts_binop( left: ts_binop( @@ -1389,101 +1343,6 @@ fn workflow_row_selector_expr(node_id: TsExpr) -> TsExpr { right: ts_lit_str(value: "\"]")) } -fn workflow_lamp_in_row_expr(row_name: String, lamp: WorkflowLamp) -> TsExpr { - ts_binop( - left: ts_ident(name: row_name), - op: "&&", - right: ts_call( - callee: ts_member(object: ts_ident(name: row_name), property: "querySelector"), - args: [ts_lit_str(value: workflow_lamp_selector(lamp: lamp))])) -} - -fn workflow_observation_key(state: ClientObservationState) -> String { - client_observation_state_key(state: state) -} - -fn workflow_set_observation_call( - lamp_expr: TsExpr, - state: ClientObservationState, - reason_expr: TsExpr, -) -> TsStmt { - ts_expr_stmt(expr: ts_call( - callee: ts_ident(name: js_set_client_observation_fn), - args: [ - lamp_expr, - ts_lit_str(value: workflow_observation_key(state: state)), - reason_expr, - ])) -} - -fn workflow_set_all_observations_stmt( - lamp: WorkflowLamp, - state: ClientObservationState, - reason_expr: TsExpr, -) -> TsStmt { - ts_expr_stmt(expr: ts_call( - callee: ts_member( - object: ts_call( - callee: ts_member(object: ts_ident(name: "document"), property: "querySelectorAll"), - args: [ts_lit_str(value: workflow_lamp_selector(lamp: lamp))]), - property: "forEach"), - args: [ts_arrow(params: ["lamp"], body: workflow_set_observation_call( - lamp_expr: ts_ident(name: "lamp"), - state: state, - reason_expr: reason_expr))])) -} - -fn workflow_observation_preamble_statements() -> List { - [ - ts_const(name: js_set_client_observation_fn, value: ts_arrow( - params: ["lamp", "state", "reason"], - body: ts_block(stmts: [ - ts_if( - cond: ts_ident(name: "lamp"), - then: ts_block(stmts: [ - ts_expr_stmt(expr: ts_binop( - left: ts_member( - object: ts_member(object: ts_ident(name: "lamp"), property: "dataset"), - property: "observation"), - op: "=", - right: ts_ident(name: "state"))), - ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "lamp"), property: "title"), - op: "=", - right: ts_binop( - left: ts_ident(name: "reason"), - op: "||", - right: ts_lit_str(value: "")))), - ts_const(name: "stateLabel", value: ts_cond( - test: ts_ident(name: "reason"), - consequent: ts_binop( - left: ts_binop( - left: ts_ident(name: "state"), - op: "+", - right: ts_lit_str(value: ": ")), - op: "+", - right: ts_ident(name: "reason")), - alternate: ts_ident(name: "state"))), - ts_expr_stmt(expr: ts_call( - callee: ts_member(object: ts_ident(name: "lamp"), property: "setAttribute"), - args: [ - ts_lit_str(value: "aria-label"), - ts_binop( - left: ts_binop( - left: ts_member( - object: ts_member(object: ts_ident(name: "lamp"), property: "dataset"), - property: "label"), - op: "+", - right: ts_lit_str(value: ": ")), - op: "+", - right: ts_ident(name: "stateLabel")), - ])), - ]), - else_stmt: none), - ]))), - ] -} - data sessions_panel_note: String = "U2 plus workflow lamps v0, corrected for lifecycle truth: one named observer per endpoint runs at load, immediately after an accepted dispatch, and on the modeled poll cadence while the page is open. Each observation recomputes its own axis; neither endpoint can suppress the other. /sessions.json proves tmux/process presence only, so it renders `session · present` plus the provider/readiness evidence carried by the wire, never `live` as agent execution. Refusal resolves every Live lamp to Refused and inserts a source-specific loud banner — unknowable never rounds to Off." data js_sessions_observe_fn: String = "gunbcObserveSessions" @@ -1544,10 +1403,6 @@ fn sessions_mark_live_stmts() -> List { callee: ts_member(object: ts_ident(name: "document"), property: "querySelector"), args: [workflow_row_selector_expr( node_id: ts_member(object: ts_ident(name: "sess"), property: "node_id"))])), - workflow_set_observation_call( - lamp_expr: workflow_lamp_in_row_expr(row_name: "sr", lamp: SessionWorkflowLamp), - state: ClientObservationOn, - reason_expr: ts_lit_str(value: "")), ts_const(name: "sb", value: ts_binop( left: ts_ident(name: "sr"), op: "&&", @@ -1563,87 +1418,95 @@ fn sessions_mark_live_stmts() -> List { property: "sessionPresence"), op: "=", right: ts_lit_str(value: "present"))), + ts_const(name: "sdt", value: ts_call( + callee: ts_ident(name: js_row_disclosure_fn), + args: [ts_ident(name: "sb")])), + ts_let(name: "sf", value: ts_binop( + left: ts_ident(name: "sdt"), + op: "&&", + right: ts_call( + callee: ts_member(object: ts_ident(name: "sdt"), property: "querySelector"), + args: [ts_lit_str(value: concat(".", session_facts_class))]))), ts_if( cond: ts_binop( - left: ts_binop( - left: ts_member(object: ts_ident(name: "sb"), property: "textContent"), - op: "===", - right: ts_lit_str(value: dispatch_caption_idle)), - op: "||", - right: ts_binop( - left: ts_member(object: ts_ident(name: "sb"), property: "textContent"), - op: "===", - right: ts_lit_str(value: concat(dispatch_session_chip_prefix, dispatch_caption_present_word)))), + left: ts_ident(name: "sdt"), + op: "&&", + right: ts_prefix(op: "!", expr: ts_ident(name: "sf"))), then: ts_block(stmts: [ ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "sb"), property: "textContent"), + left: ts_ident(name: "sf"), op: "=", - right: ts_lit_str(value: concat(dispatch_session_chip_prefix, dispatch_caption_present_word)))), - ts_expr_stmt(expr: ts_call( - callee: ts_member(object: ts_member(object: ts_ident(name: "sb"), property: "classList"), property: "add"), - args: [ts_lit_str(value: dispatch_class_ok)])), + right: ts_call( + callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), + args: [ts_lit_str(value: "div")]))), ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "sb"), property: "title"), + left: ts_member(object: ts_ident(name: "sf"), property: "className"), op: "=", - right: ts_binop( - left: ts_binop( - left: ts_binop( - left: ts_binop( - left: ts_member(object: ts_ident(name: "sess"), property: "configured_provider"), - op: "+", - right: ts_lit_str(value: " configured · ")), - op: "+", - right: ts_member(object: ts_ident(name: "sess"), property: "provider")), - op: "+", - right: ts_lit_str(value: " · ")), - op: "+", - right: ts_binop( - left: ts_member(object: ts_ident(name: "sess"), property: "provider_readiness"), - op: "+", - right: ts_binop( - left: ts_lit_str(value: " — "), - op: "+", - right: ts_member(object: ts_ident(name: "sess"), property: "session_name")))))), + right: ts_lit_str(value: session_facts_class))), + ts_const(name: "sfsm", value: ts_call( + callee: ts_member(object: ts_ident(name: "sdt"), property: "querySelector"), + args: [ts_lit_str(value: "summary")])), + ts_expr_stmt(expr: ts_binop( + left: ts_ident(name: "sfsm"), + op: "&&", + right: ts_call( + callee: ts_member(object: ts_ident(name: "sfsm"), property: "insertAdjacentElement"), + args: [ts_lit_str(value: "afterend"), ts_ident(name: "sf")]))), + ts_const(name: "sft", value: ts_call( + callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), + args: [ts_lit_str(value: "span")])), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "sft"), property: "className"), + op: "=", + right: ts_lit_str(value: "session-facts-text"))), + ts_expr_stmt(expr: ts_call( + callee: ts_member(object: ts_ident(name: "sf"), property: "append"), + args: [ts_ident(name: "sft")])), ]), else_stmt: none), - ts_let(name: "pc", value: ts_call( - callee: ts_member( - object: ts_member(object: ts_ident(name: "sb"), property: "parentElement"), - property: "querySelector"), - args: [ts_lit_str(value: ".process-chip")])), + ts_const(name: "st", value: ts_binop( + left: ts_ident(name: "sf"), + op: "&&", + right: ts_call( + callee: ts_member(object: ts_ident(name: "sf"), property: "querySelector"), + args: [ts_lit_str(value: ".session-facts-text")]))), + ts_const(name: "sfx", value: ts_binop( + left: ts_member(object: ts_ident(name: "sess"), property: "facts"), + op: "||", + right: ts_ident(name: "null"))), ts_if( - cond: ts_prefix(op: "!", expr: ts_ident(name: "pc")), + cond: ts_binop( + left: ts_ident(name: "st"), + op: "&&", + right: ts_ident(name: "sfx")), then: ts_block(stmts: [ ts_expr_stmt(expr: ts_binop( - left: ts_ident(name: "pc"), + left: ts_member(object: ts_ident(name: "st"), property: "textContent"), op: "=", - right: ts_call( - callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), - args: [ts_lit_str(value: "span")]))), + right: ts_binop( + left: ts_member(object: ts_ident(name: "sfx"), property: "line"), + op: "||", + right: ts_lit_str(value: "")))), ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "pc"), property: "className"), + left: ts_member(object: ts_ident(name: "st"), property: "title"), op: "=", - right: ts_lit_str(value: "process-chip"))), - ts_expr_stmt(expr: ts_call( - callee: ts_member(object: ts_ident(name: "sb"), property: "insertAdjacentElement"), - args: [ts_lit_str(value: "afterend"), ts_ident(name: "pc")])), + right: ts_binop( + left: ts_member(object: ts_ident(name: "sfx"), property: "detail"), + op: "||", + right: ts_lit_str(value: "")))), ]), else_stmt: none), - ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "pc"), property: "textContent"), - op: "=", - right: ts_member(object: ts_ident(name: "sess"), property: "process_state"))), - ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "pc"), property: "title"), - op: "=", - right: ts_member(object: ts_ident(name: "sess"), property: "process_detail"))), - ts_let(name: "cc", value: ts_call( - callee: ts_member( - object: ts_member(object: ts_ident(name: "sb"), property: "parentElement"), - property: "querySelector"), - args: [ts_lit_str(value: ".cleanup-chip")])), + ts_let(name: "cc", value: ts_binop( + left: ts_ident(name: "sf"), + op: "&&", + right: ts_call( + callee: ts_member(object: ts_ident(name: "sf"), property: "querySelector"), + args: [ts_lit_str(value: ".cleanup-chip")]))), ts_if( - cond: ts_prefix(op: "!", expr: ts_ident(name: "cc")), + cond: ts_binop( + left: ts_ident(name: "sf"), + op: "&&", + right: ts_prefix(op: "!", expr: ts_ident(name: "cc"))), then: ts_block(stmts: [ ts_expr_stmt(expr: ts_binop( left: ts_ident(name: "cc"), @@ -1790,30 +1653,59 @@ fn sessions_mark_live_stmts() -> List { ]))])), ]))])), ts_expr_stmt(expr: ts_call( - callee: ts_member(object: ts_ident(name: "pc"), property: "insertAdjacentElement"), - args: [ts_lit_str(value: "afterend"), ts_ident(name: "cc")])), + callee: ts_member(object: ts_ident(name: "sf"), property: "append"), + args: [ts_ident(name: "cc")])), ]), else_stmt: none), - ts_expr_stmt(expr: ts_binop( - left: ts_member( - object: ts_member(object: ts_ident(name: "cc"), property: "dataset"), - property: "cleanupAction"), - op: "=", - right: ts_member(object: ts_ident(name: "sess"), property: "cleanup_action"))), - ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "cc"), property: "textContent"), - op: "=", - right: ts_member(object: ts_ident(name: "sess"), property: "cleanup_action"))), - ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "cc"), property: "title"), - op: "=", - right: ts_member(object: ts_ident(name: "sess"), property: "cleanup_detail"))), - ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "cc"), property: "disabled"), - op: "=", - right: ts_prefix( - op: "!", - expr: ts_member(object: ts_ident(name: "sess"), property: "cleanup_allowed")))), + ts_const(name: "sact", value: ts_binop( + left: ts_binop( + left: ts_ident(name: "sfx"), + op: "&&", + right: ts_member(object: ts_ident(name: "sfx"), property: "action")), + op: "||", + right: ts_ident(name: "null"))), + ts_if( + cond: ts_binop( + left: ts_binop( + left: ts_ident(name: "cc"), + op: "&&", + right: ts_ident(name: "sact")), + op: "&&", + right: ts_binop( + left: ts_member(object: ts_ident(name: "sact"), property: "kind"), + op: "!==", + right: ts_lit_str(value: "none"))), + then: ts_block(stmts: [ + ts_expr_stmt(expr: ts_binop( + left: ts_member( + object: ts_member(object: ts_ident(name: "cc"), property: "dataset"), + property: "cleanupAction"), + op: "=", + right: ts_member(object: ts_ident(name: "sact"), property: "label"))), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "cc"), property: "textContent"), + op: "=", + right: ts_member(object: ts_ident(name: "sact"), property: "label"))), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "cc"), property: "title"), + op: "=", + right: ts_member(object: ts_ident(name: "sact"), property: "detail"))), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "cc"), property: "disabled"), + op: "=", + right: ts_binop( + left: ts_member(object: ts_ident(name: "sact"), property: "availability"), + op: "!==", + right: ts_lit_str(value: "available")))), + ]), + else_stmt: Present { value: ts_block(stmts: [ + ts_expr_stmt(expr: ts_binop( + left: ts_ident(name: "cc"), + op: "&&", + right: ts_call( + callee: ts_member(object: ts_ident(name: "cc"), property: "remove"), + args: []))), + ]) }), ]), else_stmt: none, ), @@ -1834,55 +1726,39 @@ fn remove_all_selector_stmt(selector: String, item_name: String) -> TsStmt { args: [])))])) } +data sessions_reset_note: String = "Each session observation clears the prior projection before repainting: presence datasets flip back and every session-facts disclosure line is removed, so a torn-down session cannot leave a stale 'present' claim. The dispatch button itself is untouched — it is an action and never carried the presence claim after A2 (node_activity_region_note)." + fn sessions_reset_presence_stmt() -> TsStmt { ts_expr_stmt(expr: ts_call( callee: ts_member( object: ts_call( callee: ts_member(object: ts_ident(name: "document"), property: "querySelectorAll"), - args: [ts_lit_str(value: ".dispatch-btn[data-session-presence=\"present\"]")]), + args: [ts_lit_str(value: join([ + ".dispatch-btn[data-session-presence=\"present\"], .", + session_facts_class, + ], ""))]), property: "forEach"), args: [ts_arrow(params: ["pb"], body: ts_block(stmts: [ - ts_expr_stmt(expr: ts_binop( - left: ts_member( - object: ts_member(object: ts_ident(name: "pb"), property: "dataset"), - property: "sessionPresence"), - op: "=", - right: ts_lit_str(value: ""))), ts_if( - cond: ts_binop( - left: ts_member(object: ts_ident(name: "pb"), property: "textContent"), - op: "===", - right: ts_lit_str(value: concat(dispatch_session_chip_prefix, dispatch_caption_present_word))), + cond: ts_call( + callee: ts_member( + object: ts_member(object: ts_ident(name: "pb"), property: "classList"), + property: "contains"), + args: [ts_lit_str(value: session_facts_class)]), then: ts_block(stmts: [ - ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "pb"), property: "textContent"), - op: "=", - right: ts_lit_str(value: dispatch_caption_idle))), ts_expr_stmt(expr: ts_call( - callee: ts_member( - object: ts_member(object: ts_ident(name: "pb"), property: "classList"), - property: "remove"), - args: [ts_lit_str(value: dispatch_class_ok)])), + callee: ts_member(object: ts_ident(name: "pb"), property: "remove"), + args: [])), + ]), + else_stmt: Present { value: ts_block(stmts: [ ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "pb"), property: "title"), + left: ts_member( + object: ts_member(object: ts_ident(name: "pb"), property: "dataset"), + property: "sessionPresence"), op: "=", right: ts_lit_str(value: ""))), - ]), - else_stmt: none), - ts_const(name: "oldProcess", value: ts_call( - callee: ts_member(object: ts_member(object: ts_ident(name: "pb"), property: "parentElement"), property: "querySelector"), - args: [ts_lit_str(value: ".process-chip")])), - ts_expr_stmt(expr: ts_binop( - left: ts_ident(name: "oldProcess"), - op: "&&", - right: ts_call(callee: ts_member(object: ts_ident(name: "oldProcess"), property: "remove"), args: []))), - ts_const(name: "oldCleanup", value: ts_call( - callee: ts_member(object: ts_member(object: ts_ident(name: "pb"), property: "parentElement"), property: "querySelector"), - args: [ts_lit_str(value: ".cleanup-chip")])), - ts_expr_stmt(expr: ts_binop( - left: ts_ident(name: "oldCleanup"), - op: "&&", - right: ts_call(callee: ts_member(object: ts_ident(name: "oldCleanup"), property: "remove"), args: []))), + ]) }, + ), ]))])) } @@ -1951,19 +1827,11 @@ fn sessions_fetch_effect_statements() -> List { concat( [ sessions_reset_presence_stmt(), - workflow_set_all_observations_stmt( - lamp: SessionWorkflowLamp, - state: ClientObservationRefused { reason: "" }, - reason_expr: ts_ident(name: "why")), ], observation_refused_banner_stmts(source_class: "sessions-observe-refused")))), else_stmt: Present { value: ts_block(stmts: [ remove_all_selector_stmt(selector: ".sessions-observe-refused", item_name: "oldBanner"), sessions_reset_presence_stmt(), - workflow_set_all_observations_stmt( - lamp: SessionWorkflowLamp, - state: ClientObservationOff, - reason_expr: ts_lit_str(value: "")), ts_expr_stmt(expr: ts_call( callee: ts_member(object: ts_member(object: ts_ident(name: "sj"), property: "sessions"), property: "forEach"), args: [ts_arrow(params: ["sess"], body: ts_block(stmts: sessions_mark_live_stmts()))])), @@ -1979,10 +1847,6 @@ fn sessions_fetch_effect_statements() -> List { concat( [ sessions_reset_presence_stmt(), - workflow_set_all_observations_stmt( - lamp: SessionWorkflowLamp, - state: ClientObservationRefused { reason: "" }, - reason_expr: ts_ident(name: "why")), ], concat( observation_refused_banner_stmts(source_class: "sessions-observe-refused"), @@ -2001,10 +1865,6 @@ fn attempts_mark_stmts() -> List { callee: ts_member(object: ts_ident(name: "document"), property: "querySelector"), args: [workflow_row_selector_expr( node_id: ts_member(object: ts_ident(name: "att"), property: "node_id"))])), - workflow_set_observation_call( - lamp_expr: workflow_lamp_in_row_expr(row_name: "ar", lamp: AttemptWorkflowLamp), - state: ClientObservationOn, - reason_expr: ts_lit_str(value: "")), ts_const(name: "adt", value: ts_binop( left: ts_ident(name: "ar"), op: "&&", @@ -2065,19 +1925,11 @@ fn attempts_fetch_effect_statements() -> List { concat( [ remove_all_selector_stmt(selector: ".dispatch-attempts", item_name: "staleAttempts"), - workflow_set_all_observations_stmt( - lamp: AttemptWorkflowLamp, - state: ClientObservationRefused { reason: "" }, - reason_expr: ts_ident(name: "why")), ], observation_refused_banner_stmts(source_class: "attempts-observe-refused")))), else_stmt: Present { value: ts_block(stmts: [ remove_all_selector_stmt(selector: ".attempts-observe-refused", item_name: "oldBanner"), remove_all_selector_stmt(selector: ".dispatch-attempts", item_name: "oldAttempts"), - workflow_set_all_observations_stmt( - lamp: AttemptWorkflowLamp, - state: ClientObservationOff, - reason_expr: ts_lit_str(value: "")), ts_expr_stmt(expr: ts_call( callee: ts_member(object: ts_member(object: ts_ident(name: "aj"), property: "attempts"), property: "forEach"), args: [ts_arrow(params: ["att"], body: ts_block(stmts: attempts_mark_stmts()))])), @@ -2093,10 +1945,6 @@ fn attempts_fetch_effect_statements() -> List { concat( [ remove_all_selector_stmt(selector: ".dispatch-attempts", item_name: "staleAttempts"), - workflow_set_all_observations_stmt( - lamp: AttemptWorkflowLamp, - state: ClientObservationRefused { reason: "" }, - reason_expr: ts_ident(name: "why")), ], concat( observation_refused_banner_stmts(source_class: "attempts-observe-refused"), @@ -2150,418 +1998,444 @@ fn observation_fetch_statements() -> List { ] } -fn workflow_progress_in_row_expr(row_name: String) -> TsExpr { +data workflow_paint_no_policy_note: String = "Every branch below selects WHERE a decided string lands, never WHAT it says: arm/attention keys route material and expansion, summary/outcome/reason strings land verbatim, the ledger renders the wire's canonical segment rows in wire order, and counts are never recomputed client-side. The stale sweep realizes attempt disappearance as DOM removal (attention rule: absence earns no area) while keep-and-update preserves the operator's open disclosure between polls." + +fn workflow_activity_in_row_expr(row_name: String) -> TsExpr { ts_binop( left: ts_ident(name: row_name), op: "&&", right: ts_call( - callee: ts_member( - object: ts_ident(name: row_name), - property: "querySelector", - ), - args: [ts_lit_str(value: join([".", workflow_progress_class], ""))], - ), - ) -} - -fn workflow_dynamic_stage_selector_expr() -> TsExpr { - ts_binop( - left: ts_binop( - left: ts_lit_str(value: join([ - "[", - workflow_stage_data_attribute, - "=\"", - ], "")), - op: "+", - right: ts_member(object: ts_ident(name: "seg"), property: "kind"), - ), - op: "+", - right: ts_lit_str(value: "\"]"), - ) + callee: ts_member(object: ts_ident(name: row_name), property: "querySelector"), + args: [ts_lit_str(value: join([".", node_activity_class], ""))])) } fn workflow_clear_rendered_stmt() -> TsStmt { ts_expr_stmt(expr: ts_call( callee: ts_member( object: ts_call( - callee: ts_member( - object: ts_ident(name: "document"), - property: "querySelectorAll", - ), - args: [ts_lit_str(value: join([".", workflow_progress_class], ""))], - ), - property: "forEach", - ), - args: [ts_arrow( - params: ["wp"], - body: ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "wp"), property: "hidden"), - op: "=", - right: ts_ident(name: "true"), - )), - )], - )) + callee: ts_member(object: ts_ident(name: "document"), property: "querySelectorAll"), + args: [ts_lit_str(value: join([".", node_activity_class], ""))]), + property: "forEach"), + args: [ts_arrow(params: ["na"], body: ts_expr_stmt(expr: ts_binop( + left: ts_member( + object: ts_member(object: ts_ident(name: "na"), property: "dataset"), + property: "stale"), + op: "=", + right: ts_lit_str(value: "1"))))])) +} + +fn workflow_stale_sweep_stmt() -> TsStmt { + remove_all_selector_stmt( + selector: join([".", node_activity_class, "[data-stale=\"1\"]"], ""), + item_name: "staleActivity") } fn workflow_refuse_visible_stmt() -> TsStmt { ts_expr_stmt(expr: ts_call( callee: ts_member( object: ts_call( - callee: ts_member( - object: ts_ident(name: "document"), - property: "querySelectorAll", - ), - args: [ts_lit_str(value: join([ - ".", - workflow_progress_class, - ":not([hidden]) .", - workflow_stage_class, - ], ""))], - ), - property: "forEach", - ), - args: [ts_arrow(params: ["stage"], body: ts_block(stmts: [ + callee: ts_member(object: ts_ident(name: "document"), property: "querySelectorAll"), + args: [ts_lit_str(value: join([".", node_activity_class], ""))]), + property: "forEach"), + args: [ts_arrow(params: ["na"], body: ts_block(stmts: [ ts_expr_stmt(expr: ts_binop( left: ts_member( - object: ts_member( - object: ts_ident(name: "stage"), - property: "dataset", - ), - property: "state", - ), + object: ts_member(object: ts_ident(name: "na"), property: "dataset"), + property: "arm"), op: "=", - right: ts_lit_str(value: "refused"), - )), + right: ts_lit_str(value: "unobservable"))), + ts_const(name: "ns", value: ts_call( + callee: ts_member(object: ts_ident(name: "na"), property: "querySelector"), + args: [ts_lit_str(value: join([".", activity_summary_class], ""))])), ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "stage"), property: "title"), - op: "=", - right: ts_ident(name: "why"), - )), - ]))], - )) + left: ts_ident(name: "ns"), + op: "&&", + right: ts_binop( + left: ts_member(object: ts_ident(name: "ns"), property: "textContent"), + op: "=", + right: ts_ident(name: "why")))), + ]))])) } -fn workflow_mark_attempt_stmts() -> List { +fn activity_container_create_stmts() -> List { [ - ts_const(name: "wr", value: ts_call( - callee: ts_member( - object: ts_ident(name: "document"), - property: "querySelector", - ), - args: [workflow_row_selector_expr( - node_id: ts_member( - object: ts_ident(name: "attempt"), - property: "node_id", - ), - )], - )), - ts_const( - name: "wp", - value: workflow_progress_in_row_expr(row_name: "wr"), + ts_expr_stmt(expr: ts_binop( + left: ts_ident(name: "act"), + op: "=", + right: ts_call( + callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), + args: [ts_lit_str(value: "div")]))), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "act"), property: "className"), + op: "=", + right: ts_lit_str(value: node_activity_class))), + ts_const(name: "nh", value: ts_call( + callee: ts_member(object: ts_ident(name: "wr"), property: "querySelector"), + args: [ts_lit_str(value: ".node-head")])), + ts_if( + cond: ts_ident(name: "nh"), + then: ts_block(stmts: [ + ts_expr_stmt(expr: ts_call( + callee: ts_member(object: ts_ident(name: "nh"), property: "insertAdjacentElement"), + args: [ts_lit_str(value: "afterend"), ts_ident(name: "act")])), + ]), + else_stmt: Present { value: ts_block(stmts: [ + ts_expr_stmt(expr: ts_call( + callee: ts_member(object: ts_ident(name: "wr"), property: "append"), + args: [ts_ident(name: "act")])), + ]) }, ), + ts_const(name: "asum", value: ts_call( + callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), + args: [ts_lit_str(value: "span")])), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "asum"), property: "className"), + op: "=", + right: ts_lit_str(value: activity_summary_class))), + ts_expr_stmt(expr: ts_call( + callee: ts_member(object: ts_ident(name: "act"), property: "append"), + args: [ts_ident(name: "asum")])), + ts_const(name: "adet", value: ts_call( + callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), + args: [ts_lit_str(value: "details")])), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "adet"), property: "className"), + op: "=", + right: ts_lit_str(value: activity_detail_class))), + ts_const(name: "asm", value: ts_call( + callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), + args: [ts_lit_str(value: "summary")])), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "asm"), property: "textContent"), + op: "=", + right: ts_lit_str(value: activity_disclosure_label))), + ts_expr_stmt(expr: ts_call( + callee: ts_member(object: ts_ident(name: "adet"), property: "append"), + args: [ts_ident(name: "asm")])), + ts_const(name: "aled", value: ts_call( + callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), + args: [ts_lit_str(value: "div")])), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "aled"), property: "className"), + op: "=", + right: ts_lit_str(value: activity_ledger_class))), + ts_expr_stmt(expr: ts_call( + callee: ts_member(object: ts_ident(name: "adet"), property: "append"), + args: [ts_ident(name: "aled")])), + ts_const(name: "aev", value: ts_call( + callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), + args: [ts_lit_str(value: "div")])), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "aev"), property: "className"), + op: "=", + right: ts_lit_str(value: activity_evidence_class))), + ts_expr_stmt(expr: ts_call( + callee: ts_member(object: ts_ident(name: "adet"), property: "append"), + args: [ts_ident(name: "aev")])), + ts_expr_stmt(expr: ts_call( + callee: ts_member(object: ts_ident(name: "act"), property: "append"), + args: [ts_ident(name: "adet")])), + ] +} + +fn activity_summary_text_expr() -> TsExpr { + ts_cond( + test: ts_binop( + left: ts_member(object: ts_ident(name: "av"), property: "arm"), + op: "===", + right: ts_lit_str(value: "completed")), + consequent: ts_member(object: ts_ident(name: "av"), property: "outcome"), + alternate: ts_cond( + test: ts_binop( + left: ts_member(object: ts_ident(name: "av"), property: "arm"), + op: "===", + right: ts_lit_str(value: "unobservable")), + consequent: ts_binop( + left: ts_lit_str(value: "workflow evidence unobservable — "), + op: "+", + right: ts_member(object: ts_ident(name: "av"), property: "reason")), + alternate: ts_member(object: ts_ident(name: "av"), property: "summary"))) +} + +fn activity_paint_stmts() -> List { + [ + ts_expr_stmt(expr: ts_binop( + left: ts_member( + object: ts_member(object: ts_ident(name: "act"), property: "dataset"), + property: "arm"), + op: "=", + right: ts_member(object: ts_ident(name: "av"), property: "arm"))), + ts_expr_stmt(expr: ts_binop( + left: ts_member( + object: ts_member(object: ts_ident(name: "act"), property: "dataset"), + property: "attention"), + op: "=", + right: ts_binop( + left: ts_member(object: ts_ident(name: "av"), property: "attention"), + op: "||", + right: ts_lit_str(value: "")))), + ts_expr_stmt(expr: ts_binop( + left: ts_member( + object: ts_member(object: ts_ident(name: "act"), property: "dataset"), + property: "stale"), + op: "=", + right: ts_lit_str(value: ""))), + ts_const(name: "asum", value: ts_call( + callee: ts_member(object: ts_ident(name: "act"), property: "querySelector"), + args: [ts_lit_str(value: join([".", activity_summary_class], ""))])), + ts_expr_stmt(expr: ts_binop( + left: ts_ident(name: "asum"), + op: "&&", + right: ts_binop( + left: ts_member(object: ts_ident(name: "asum"), property: "textContent"), + op: "=", + right: activity_summary_text_expr()))), + ts_const(name: "adet", value: ts_call( + callee: ts_member(object: ts_ident(name: "act"), property: "querySelector"), + args: [ts_lit_str(value: join(["details.", activity_detail_class], ""))])), + ts_const(name: "anom", value: ts_binop( + left: ts_binop( + left: ts_member(object: ts_ident(name: "av"), property: "arm"), + op: "===", + right: ts_lit_str(value: "refused")), + op: "||", + right: ts_binop( + left: ts_member(object: ts_ident(name: "av"), property: "attention"), + op: "===", + right: ts_lit_str(value: "anomalous")))), ts_if( - cond: ts_ident(name: "wp"), + cond: ts_binop( + left: ts_binop( + left: ts_ident(name: "adet"), + op: "&&", + right: ts_ident(name: "anom")), + op: "&&", + right: ts_prefix(op: "!", expr: ts_member( + object: ts_member(object: ts_ident(name: "act"), property: "dataset"), + property: "autoOpened"))), then: ts_block(stmts: [ ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "wp"), property: "hidden"), + left: ts_member(object: ts_ident(name: "adet"), property: "open"), op: "=", - right: ts_ident(name: "false"), - )), + right: ts_ident(name: "true"))), ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "wp"), property: "title"), + left: ts_member( + object: ts_member(object: ts_ident(name: "act"), property: "dataset"), + property: "autoOpened"), op: "=", - right: ts_binop( - left: ts_binop( - left: ts_binop( - left: ts_member( - object: ts_ident(name: "attempt"), - property: "workflow_status", - ), - op: "+", - right: ts_lit_str(value: " · "), - ), - op: "+", - right: ts_member( - object: ts_ident(name: "attempt"), - property: "remaining_obligations", - ), - ), - op: "+", - right: ts_binop( - left: ts_lit_str(value: " remaining — "), - op: "+", + right: ts_lit_str(value: "1"))), + ]), + else_stmt: none), + ts_const(name: "aled", value: ts_call( + callee: ts_member(object: ts_ident(name: "act"), property: "querySelector"), + args: [ts_lit_str(value: join([".", activity_ledger_class], ""))])), + ts_if( + cond: ts_ident(name: "aled"), + then: ts_block(stmts: [ + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "aled"), property: "textContent"), + op: "=", + right: ts_lit_str(value: ""))), + ts_expr_stmt(expr: ts_call( + callee: ts_member( + object: ts_binop( + left: ts_member(object: ts_ident(name: "attempt"), property: "segments"), + op: "||", + right: ts_array(elements: [])), + property: "forEach"), + args: [ts_arrow(params: ["seg"], body: ts_block(stmts: [ + ts_const(name: "ob", value: ts_call( + callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), + args: [ts_lit_str(value: "div")])), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "ob"), property: "className"), + op: "=", + right: ts_lit_str(value: activity_obligation_class))), + ts_expr_stmt(expr: ts_binop( + left: ts_member( + object: ts_member(object: ts_ident(name: "ob"), property: "dataset"), + property: "state"), + op: "=", + right: ts_member(object: ts_ident(name: "seg"), property: "state"))), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "ob"), property: "textContent"), + op: "=", right: ts_binop( - left: ts_member( - object: ts_ident(name: "attempt"), - property: "provider_status", - ), + left: ts_binop( + left: ts_binop( + left: ts_member(object: ts_ident(name: "seg"), property: "label"), + op: "+", + right: ts_lit_str(value: activity_state_sep)), + op: "+", + right: ts_member(object: ts_ident(name: "seg"), property: "state")), op: "+", right: ts_cond( - test: ts_member( - object: ts_ident(name: "attempt"), - property: "provider_detail", - ), + test: ts_member(object: ts_ident(name: "seg"), property: "detail"), consequent: ts_binop( - left: ts_lit_str(value: " — "), + left: ts_lit_str(value: activity_detail_sep), op: "+", - right: ts_member( - object: ts_ident(name: "attempt"), - property: "provider_detail", - ), - ), - alternate: ts_lit_str(value: ""), - ), - ), - ), - ), - )), + right: ts_member(object: ts_ident(name: "seg"), property: "detail")), + alternate: ts_lit_str(value: ""))))), + ts_expr_stmt(expr: ts_call( + callee: ts_member(object: ts_ident(name: "aled"), property: "append"), + args: [ts_ident(name: "ob")])), + ts_const(name: "bad", value: ts_binop( + left: ts_binop( + left: ts_member(object: ts_ident(name: "seg"), property: "state"), + op: "===", + right: ts_lit_str(value: "failed")), + op: "||", + right: ts_binop( + left: ts_member(object: ts_ident(name: "seg"), property: "state"), + op: "===", + right: ts_lit_str(value: "refused")))), + ts_const(name: "sd", value: ts_binop( + left: ts_member(object: ts_ident(name: "seg"), property: "detail"), + op: "||", + right: ts_lit_str(value: reason_absent_phrase))), + ts_expr_stmt(expr: ts_call( + callee: ts_ident(name: js_reason_surface_fn), + args: [ + ts_call(callee: ts_ident(name: js_row_disclosure_fn), args: [ts_ident(name: "act")]), + ts_binop( + left: ts_lit_str(value: reason_source_workflow_prefix), + op: "+", + right: ts_member(object: ts_ident(name: "seg"), property: "kind")), + ts_cond( + test: ts_ident(name: "bad"), + consequent: ts_binop( + left: ts_binop( + left: ts_binop( + left: ts_binop( + left: ts_member(object: ts_ident(name: "seg"), property: "short_label"), + op: "+", + right: ts_lit_str(value: " · ")), + op: "+", + right: ts_member(object: ts_ident(name: "seg"), property: "state")), + op: "+", + right: ts_lit_str(value: " — ")), + op: "+", + right: ts_ident(name: "sd")), + alternate: ts_lit_str(value: "")), + ])), + ]))])), + ]), + else_stmt: none), + ts_const(name: "aev", value: ts_call( + callee: ts_member(object: ts_ident(name: "act"), property: "querySelector"), + args: [ts_lit_str(value: join([".", activity_evidence_class], ""))])), + ts_if( + cond: ts_ident(name: "aev"), + then: ts_block(stmts: [ + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "aev"), property: "textContent"), + op: "=", + right: ts_lit_str(value: ""))), ts_expr_stmt(expr: ts_call( callee: ts_member( - object: ts_ident(name: "wp"), - property: "setAttribute", - ), - args: [ - ts_lit_str(value: "aria-label"), - ts_binop( - left: ts_lit_str(value: "Workflow: "), - op: "+", + object: ts_binop( + left: ts_member(object: ts_ident(name: "av"), property: "evidence"), + op: "||", + right: ts_array(elements: [])), + property: "forEach"), + args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ + ts_const(name: "er", value: ts_call( + callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), + args: [ts_lit_str(value: "div")])), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "er"), property: "className"), + op: "=", + right: ts_lit_str(value: activity_evidence_row_class))), + ts_expr_stmt(expr: ts_binop( + left: ts_member(object: ts_ident(name: "er"), property: "textContent"), + op: "=", right: ts_binop( - left: ts_member( - object: ts_ident(name: "attempt"), - property: "workflow_status", - ), - op: "+", - right: ts_binop( - left: ts_lit_str(value: " · "), + left: ts_binop( + left: ts_member(object: ts_ident(name: "row"), property: "label"), op: "+", - right: ts_member( - object: ts_ident(name: "attempt"), - property: "remaining_obligations", - ), - ), - ), - ), - ], - )), - ts_const(name: "wa", value: ts_call( - callee: ts_member( - object: ts_ident(name: "wp"), - property: "querySelector", - ), - args: [ts_lit_str(value: join([ - ".", - workflow_activity_class, - ], ""))], - )), + right: ts_lit_str(value: activity_evidence_kv_sep)), + op: "+", + right: ts_member(object: ts_ident(name: "row"), property: "value")))), + ts_expr_stmt(expr: ts_call( + callee: ts_member(object: ts_ident(name: "aev"), property: "append"), + args: [ts_ident(name: "er")])), + ]))])), ts_if( - cond: ts_ident(name: "wa"), + cond: ts_binop( + left: ts_binop( + left: ts_member(object: ts_ident(name: "av"), property: "arm"), + op: "===", + right: ts_lit_str(value: "refused")), + op: "&&", + right: ts_member(object: ts_ident(name: "av"), property: "located_reason")), then: ts_block(stmts: [ + ts_const(name: "lr", value: ts_call( + callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), + args: [ts_lit_str(value: "div")])), ts_expr_stmt(expr: ts_binop( - left: ts_member( - object: ts_ident(name: "wa"), - property: "textContent", - ), + left: ts_member(object: ts_ident(name: "lr"), property: "className"), op: "=", - right: ts_binop( - left: ts_member( - object: ts_ident(name: "attempt"), - property: "provider_activity", - ), - op: "||", - right: ts_member( - object: ts_ident(name: "attempt"), - property: "provider_status", - ), - ), - )), + right: ts_lit_str(value: activity_located_reason_class))), ts_expr_stmt(expr: ts_binop( - left: ts_member(object: ts_ident(name: "wa"), property: "title"), + left: ts_member(object: ts_ident(name: "lr"), property: "textContent"), op: "=", - right: ts_member( - object: ts_ident(name: "attempt"), - property: "provider_detail", - ), - )), + right: ts_member(object: ts_ident(name: "av"), property: "located_reason"))), + ts_expr_stmt(expr: ts_call( + callee: ts_member(object: ts_ident(name: "aev"), property: "append"), + args: [ts_ident(name: "lr")])), ]), - else_stmt: none, + else_stmt: none), + ]), + else_stmt: none), + ] +} + +fn workflow_mark_attempt_stmts() -> List { + [ + ts_const(name: "wr", value: ts_call( + callee: ts_member( + object: ts_ident(name: "document"), + property: "querySelector", + ), + args: [workflow_row_selector_expr( + node_id: ts_member( + object: ts_ident(name: "attempt"), + property: "node_id", ), - ts_const(name: "wdt", value: ts_call( - callee: ts_ident(name: js_row_disclosure_fn), - args: [ts_ident(name: "wp")])), - ts_expr_stmt(expr: ts_call( - callee: ts_member( - object: ts_member( - object: ts_ident(name: "attempt"), - property: "segments", - ), - property: "forEach", - ), - args: [ts_arrow(params: ["seg"], body: ts_block(stmts: [ - ts_const(name: "stage", value: ts_call( - callee: ts_member( - object: ts_ident(name: "wp"), - property: "querySelector", - ), - args: [workflow_dynamic_stage_selector_expr()], - )), - ts_if( - cond: ts_ident(name: "stage"), - then: ts_block(stmts: [ - ts_expr_stmt(expr: ts_binop( - left: ts_member( - object: ts_member( - object: ts_ident(name: "stage"), - property: "dataset", - ), - property: "state", - ), - op: "=", - right: ts_member( - object: ts_ident(name: "seg"), - property: "state", - ), - )), - ts_const(name: "bad", value: ts_binop( - left: ts_binop( - left: ts_member( - object: ts_ident(name: "seg"), - property: "state", - ), - op: "===", - right: ts_lit_str(value: "failed"), - ), - op: "||", - right: ts_binop( - left: ts_member( - object: ts_ident(name: "seg"), - property: "state", - ), - op: "===", - right: ts_lit_str(value: "refused"), - ), - )), - ts_const(name: "sl", value: ts_binop( - left: ts_binop( - left: ts_member( - object: ts_ident(name: "seg"), - property: "short_label", - ), - op: "+", - right: ts_lit_str(value: " · "), - ), - op: "+", - right: ts_member( - object: ts_ident(name: "seg"), - property: "state", - ), - )), - ts_expr_stmt(expr: ts_binop( - left: ts_member( - object: ts_ident(name: "stage"), - property: "textContent", - ), - op: "=", - right: ts_cond( - test: ts_ident(name: "bad"), - consequent: ts_ident(name: "sl"), - alternate: ts_member( - object: ts_ident(name: "seg"), - property: "short_label", - ), - ), - )), - ts_expr_stmt(expr: ts_binop( - left: ts_member( - object: ts_ident(name: "stage"), - property: "title", - ), - op: "=", - right: ts_member( - object: ts_ident(name: "seg"), - property: "detail", - ), - )), - ts_const(name: "sd", value: ts_binop( - left: ts_member( - object: ts_ident(name: "seg"), - property: "detail", - ), - op: "||", - right: ts_lit_str(value: reason_absent_phrase), - )), - ts_expr_stmt(expr: ts_call( - callee: ts_ident(name: js_reason_surface_fn), - args: [ - ts_ident(name: "wdt"), - ts_binop( - left: ts_lit_str(value: reason_source_workflow_prefix), - op: "+", - right: ts_member(object: ts_ident(name: "seg"), property: "kind"), - ), - ts_cond( - test: ts_ident(name: "bad"), - consequent: ts_binop( - left: ts_binop( - left: ts_ident(name: "sl"), - op: "+", - right: ts_lit_str(value: " — "), - ), - op: "+", - right: ts_ident(name: "sd"), - ), - alternate: ts_lit_str(value: ""), - ), - ], - )), - ts_expr_stmt(expr: ts_call( - callee: ts_member( - object: ts_ident(name: "stage"), - property: "setAttribute", - ), - args: [ - ts_lit_str(value: "aria-label"), - ts_binop( - left: ts_binop( - left: ts_binop( - left: ts_member( - object: ts_ident(name: "seg"), - property: "label", - ), - op: "+", - right: ts_lit_str(value: ": "), - ), - op: "+", - right: ts_member( - object: ts_ident(name: "seg"), - property: "state", - ), - ), - op: "+", - right: ts_cond( - test: ts_member( - object: ts_ident(name: "seg"), - property: "detail", - ), - consequent: ts_binop( - left: ts_lit_str(value: " — "), - op: "+", - right: ts_member( - object: ts_ident(name: "seg"), - property: "detail", - ), - ), - alternate: ts_lit_str(value: ""), - ), - ), - ], - )), - ]), - else_stmt: none, - ), - ]))], - )), + )], + )), + ts_const(name: "av", value: ts_binop( + left: ts_member(object: ts_ident(name: "attempt"), property: "activity"), + op: "||", + right: ts_ident(name: "null"))), + ts_let(name: "act", value: workflow_activity_in_row_expr(row_name: "wr")), + ts_if( + cond: ts_binop( + left: ts_binop( + left: ts_prefix(op: "!", expr: ts_ident(name: "av")), + op: "||", + right: ts_binop( + left: ts_member(object: ts_ident(name: "av"), property: "arm"), + op: "===", + right: ts_lit_str(value: "none"))), + op: "||", + right: ts_prefix(op: "!", expr: ts_ident(name: "wr"))), + then: ts_block(stmts: [ + ts_expr_stmt(expr: ts_binop( + left: ts_ident(name: "act"), + op: "&&", + right: ts_call( + callee: ts_member(object: ts_ident(name: "act"), property: "remove"), + args: []))), ]), - else_stmt: none, + else_stmt: Present { value: ts_block(stmts: concat( + [ + ts_if( + cond: ts_prefix(op: "!", expr: ts_ident(name: "act")), + then: ts_block(stmts: activity_container_create_stmts()), + else_stmt: none), + ], + activity_paint_stmts())) }, ), ] } @@ -2637,6 +2511,7 @@ fn workflow_fetch_request_statements() -> List { body: ts_block(stmts: workflow_mark_attempt_stmts()), )], )), + workflow_stale_sweep_stmt(), ]) }, ), ]))], @@ -2728,22 +2603,20 @@ fn dispatch_client_program_for( concat( dispatch_preamble_statements(), concat( - workflow_observation_preamble_statements(), + dispatch_bind_statements(selector: selector, url_expr: url_expr), concat( - dispatch_bind_statements(selector: selector, url_expr: url_expr), + sound_toggle_bind_statements(), concat( - sound_toggle_bind_statements(), + theme_toggle_bind_statements(), concat( - theme_toggle_bind_statements(), + disclosure_bind_statements(), concat( - disclosure_bind_statements(), + swatch_toggle_bind_statements(), concat( - swatch_toggle_bind_statements(), + observation_fetch_statements(), concat( - observation_fetch_statements(), - concat( - workflow_fetch_statements(), - instance_statements)))))))))), + workflow_fetch_statements(), + instance_statements))))))))), } } diff --git a/dag/gunbc/roadmap_dashboard_instance.dag b/dag/gunbc/roadmap_dashboard_instance.dag index 2ef669f676f..a394cbecb9e 100644 --- a/dag/gunbc/roadmap_dashboard_instance.dag +++ b/dag/gunbc/roadmap_dashboard_instance.dag @@ -101,6 +101,7 @@ type HostDashboardInstance { data dashboard_instance_note: String = "A dashboard is an explicit host instance, not an ambient serve process. One value moves the source repository, local Git origin, attempt-worktree root, semantic execution-capability realizations, provider state binding, tmux server, listen address, handler entrypoint, and actuation posture together. Selection is by the serve entry function that receives this value; no environment variable may replace one projection independently. Production uses the user's default tmux server for compatibility. Labs and previews use named tmux servers and roots disjoint from production. Provider credentials remain pre-existing user-authored state and are referenced, never copied into a deployment payload. Capability realizations name exact executable paths and identity probes; observe-only means absence refuses admission until a separate capability-specific provisioner exists, never that generic Ensure guesses an installer." data srv1_dashboard_live_id: NonEmptyStr = "srv1-live" as NonEmptyStr +data srv1_dashboard_green_id: NonEmptyStr = "srv1-green" as NonEmptyStr data srv1_dashboard_lab_id: NonEmptyStr = "srv1-lab-codex-feedback-v0" as NonEmptyStr data srv2_dashboard_preview_id: NonEmptyStr = "srv2-preview-codex-feedback-v0" as NonEmptyStr @@ -352,6 +353,42 @@ fn srv1_live_dashboard_instance() -> HostDashboardInstance { } } +data srv1_green_dashboard_instance_note: String = "The second production slot for the operator-signed 2026-08-01 blue/green cutover. It is a full HostDashboardInstance rather than a port override: source, binary, dispatch worktrees, attempt evidence, tmux server, unit handler and listen port move together, so preparing this idle slot cannot mutate the instance that Tailscale still serves. Provider credentials are referenced from the same admitted account binding, never copied. The slot is actuating and serves the production handler surface; it is not the pre-existing feedback lab, whose source ref and handler intentionally describe a different product." + +fn srv1_green_dashboard_instance() -> HostDashboardInstance { + let root = "/opt/gunbc-green" as FilePath + let binary = dashboard_instance_child(root: root, child: "bin/gunbc") + HostDashboardInstance { + instance_id: srv1_dashboard_green_id, + host_identity: "srv1" as NonEmptyStr, + provision_parent_root: "/opt" as FilePath, + instance_root: root, + local_origin_root: dashboard_instance_child(root: root, child: "gunbc"), + local_origin_publish_ref: "refs/heads/main" as NonEmptyStr, + repo_root: dashboard_instance_child(root: root, child: "gunbc"), + dispatch_base_ref: "origin/main" as NonEmptyStr, + dispatch_worktree_root: dashboard_instance_child(root: root, child: "dispatch-worktrees"), + dispatch_attempt_state_root: dashboard_instance_child(root: root, child: "attempt-state"), + execution_environment: dashboard_execution_environment(serve_binary: binary), + provider_state: DashboardExistingProviderState { + root: srv1_roadmap_codex_home, + executable: "/usr/bin/codex" as FilePath, + binding: admitted_single_session_binding( + selection: codex_roadmap_account_selection(), + evidence: codex_roadmap_account_evidence, + ), + }, + serve_binary: binary, + serve_function: "roadmap_serve_handle_srv1_green" as NonEmptyStr, + listen_host: "0.0.0.0" as NonEmptyStr, + listen_port: 18080, + tmux_server: dashboard_named_tmux_server(instance_id: srv1_dashboard_green_id), + actuation: DashboardActuating, + provision_transport: DashboardSshProvision { host: "srv1" as NonEmptyStr }, + source_seed: DashboardExistingRepo { anchor_ref: "origin/main" as NonEmptyStr }, + } +} + data isolated_instance_layout_parameter_note: String = "host_identity USED TO BE A PARAMETER OVER A BODY OF srv1 LITERALS, which made a second host expressible without making it representable. Serve binary, provider state root, and source-seed controller repo were pinned to srv1 rows here, so srv2_preview_dashboard_instance below produced a value reading host_identity srv2 while describing srv1's binary, srv1's Codex home, and srv1's controller worktree. Nothing caught it because every consumer that would disagree — preflight, apply, the git and tmux observations — needs the host to actually be actuated, and that instance was declared observe-only and never provisioned.\\n\\nTaking the whole layout row instead of a bare host_identity makes the mismatch unwritable: there is no longer a way to name a host without also supplying the three facts that belong to it, because the name IS a field of the row that carries them. gunbc.host_layout owns those rows." data isolated_instance_own_binary_note: String = "EACH INSTANCE OWNS ITS BINARY, BECAUSE THE PROCESSES ARE MUTUALLY EXCLUSIVE (operator ruling, 2026-07-30). The serve binary used to come from the host layout, so every isolated instance on one machine resolved to the same /opt/gunbc/bin/gunbc that production declares. dashboard_instance_owned_paths does not cover serve_binary — it checks origin, source, attempts and attempt-state — so the existing disjointness check passed while the DEPLOYMENT's owned-artifact set overlapped production on exactly that path.\\n\\nThe consequence was not a stale read. ServeBinary is an Owned artifact and retract tears Owned members down, so retracting a lab would have deleted the binary production is executing. That could not fire before deployments became expressible per instance, which is why it surfaced the moment the twin witness was written and not one commit earlier: the hazard is created by the capability, so it has to be closed in the same change that grants it.\\n\\nDeriving from the instance root rather than the layout also states the isolation the labs exist for — a lab testing a new seed must not overwrite the binary production is running. srv1-live is untouched: it declares /opt/gunbc/bin/gunbc directly rather than through this constructor, which is why its emitted apply and retract scripts stay byte-identical. GunbcHostLayout.gunbc_serve_binary keeps only srv1-live's own declaration as a consumer; it is a per-INSTANCE fact wearing a per-host name, and it dissolves when srv1-live is expressed as an instance row like the rest." diff --git a/dag/gunbc/roadmap_forecast.dag b/dag/gunbc/roadmap_forecast.dag new file mode 100644 index 00000000000..5fd65f7701b --- /dev/null +++ b/dag/gunbc/roadmap_forecast.dag @@ -0,0 +1,826 @@ +module gunbc.roadmap_forecast + +import std.types { EpochMs, NonEmptyStr } +import std.measure { + Millisecond, + PositiveMeasureCount, + millisecond, + millisecond_count, +} +import std.graph { GraphEdge, CallGraph, graph_cycle_members } +import std.algebra { FieldOfFractions } +import extdeps.project_schedule.cpm_pert { + CriticalPathMethod, + ThreePointEstimateMethod, + ResourceCapacityLowerBoundMethod, + critical_path_method, + three_point_estimate_method, + resource_capacity_lower_bound_method, +} +import extdeps.github.pulls { + PullTimingObservation, + gunbc_pr_7393_timing, + gunbc_pr_7405_timing, + gunbc_pr_7429_timing, + gunbc_pr_7431_timing, + gunbc_pr_7441_timing, + gunbc_pr_7542_timing, +} +import gunbc.roadmap_model { + RoadmapNodeId, + RoadmapNode, + RoadmapEdge, + RoadmapAcceptanceReceipt, + Intricacy, + Volume, + NodeSizing, + Unsized, + Sized, + node_accepted, +} +import gunbc.roadmap_belt { BeltCapacity } +import gunbc.roadmap_authority { + declared_roadmap_nodes, + active_roadmap_nodes, + active_roadmap_edges, + roadmap_acceptance_receipts, +} + +data forecast_is_projection_not_observation_note: String = "A forecast projects what may happen; a CycleTimeObservation records what did happen. They are separate nominal record types with no conversion in either direction. ScheduleForecast exposes ForecastInstant and ForecastLowerBoundInterval; its epoch value is sealed inside ForecastInstant and never appears as a bare Timestamp/EpochMs observation, RoadmapAcceptanceReceipt, Signoff, or done/accepted stage. schedule_forecast derives only ScheduleForecastResult, and its refusal arm is exhaustive. Therefore a date can neither accept a roadmap node nor inhabit the observation input used to calibrate later forecasts. Estimates project; only evidence promotes." + +data schedule_method_note: String = "This is a roadmap instantiation of the cited GAO critical-path and three-point-estimate methods plus the cited RCPSP work-volume lower bound, not a new scheduling vocabulary. Dependencies determine the path of longest median duration. Historical minimum/median/maximum remain an interval throughout; the small sample is never collapsed to a point. PR open-to-merge is one undecomposed observation: ReviewCycleCount and OperatorMergeLatencyAssumption remain named and visible, but ScenarioAdjustment is AdjustmentUnavailable until per-review duration and operator merge latency are separately observed. Neither is multiplied into or added onto the baseline. BeltCapacity is session-grain: under EndToEndBeltSlotOccupancyAssumption, each forecast node continuously occupies one belt slot across its modeled baseline interval, so capacity enters through ceil(total slot-time / cap). The output is explicitly conditional lower-bound projection, never a resource-leveled expected date. dissolve-on: separately grained review/merge observations, observed dispatch-to-release slot occupancy, and a grounded RCPSP resource-leveling slice replace the unavailable adjustment, declared occupancy assumption, and lower-bound-only projection; until then no consumer may read these bounds as observed labor or promised landing dates." + +data remaining_cost_relative_sizing_follow_on_note: String = "FOLLOW-ON DERIVED READ, not slice 1 and never an authored field on RoadmapNode or NodeSizing: remaining-cost-to-complete-from-here is a node's own calibrated work plus the unfinished dependency closure, bucketed relative to the historical plus active task population by a separately grounded order-of-magnitude or quantile policy. It must project from this module's TotalWork and dependency-closure facts, so the label recalibrates as history accumulates and shrinks as prerequisites gain acceptance evidence. dissolve-on: a consumer needs relative portfolio sizing and grounds one population-bucketing policy; then one fold derives the label without creating a second sizing authority." + +data critical_path_method_used: CriticalPathMethod = critical_path_method +data three_point_estimate_method_used: ThreePointEstimateMethod = three_point_estimate_method +data resource_capacity_lower_bound_method_used: ResourceCapacityLowerBoundMethod = resource_capacity_lower_bound_method + +type SizingCell { + intricacy: Intricacy + volume: Volume +} + +fn sizing_cell_eq(a: SizingCell, b: SizingCell) -> Bool { + a.intricacy == b.intricacy && a.volume == b.volume +} + +fn sizing_cell_of(sizing: NodeSizing) -> SizingCell? { + match sizing { + Unsized => Absent + Sized { intricacy, volume, repo: _, acceptance: _ } => + Present { value: SizingCell { intricacy: intricacy, volume: volume } } + } +} + +type AcceptedNodePullLink { + node: RoadmapNodeId + pull_number: Int +} + +data accepted_node_pull_link_note: String = "The business join between a durable roadmap acceptance and its implementation/acceptance pull request. Pull wire shape and its normalized timing observations stay under the single extdeps.github.pulls authority; acceptance stays in gunbc.roadmap_authority. This list says only which PR delivered an accepted node. A PR open-to-merge interval is admitted as a calibration sample only when exactly one accepted node links to that PR; a shared delivery is reported once as BundledPullDelivery and excluded because the source does not allocate its elapsed time among node sizing cells." + +fn accepted_node_pull_links() -> List { + [ + AcceptedNodePullLink { node: "2-scm-git-upstream-model", pull_number: 7405 }, + AcceptedNodePullLink { node: "2-scm-mercurial-upstream-model", pull_number: 7405 }, + AcceptedNodePullLink { node: "2-scm-pijul-upstream-model", pull_number: 7405 }, + AcceptedNodePullLink { node: "2-claim-indexed-evidence", pull_number: 7405 }, + AcceptedNodePullLink { node: "2-scm-compatibility-shape", pull_number: 7431 }, + AcceptedNodePullLink { node: "2-scm-p0-landing-spine", pull_number: 7542 }, + AcceptedNodePullLink { node: "namespace-occurrence-transport", pull_number: 7393 }, + AcceptedNodePullLink { node: "pderive-typesafe-nullary-reflection", pull_number: 7393 }, + AcceptedNodePullLink { node: "pderive-static-supplemental-contract", pull_number: 7429 }, + AcceptedNodePullLink { node: "v1-test-hygiene-producer-retirement", pull_number: 7441 }, + ] +} + +fn cited_pull_timings() -> List { + [ + gunbc_pr_7393_timing, + gunbc_pr_7405_timing, + gunbc_pr_7429_timing, + gunbc_pr_7431_timing, + gunbc_pr_7441_timing, + gunbc_pr_7542_timing, + ] +} + +type CycleTimeProvenance + = AcceptedRoadmapNodePullHistory + | DashboardWorkItemDispatchPullHistory + +data cycle_time_provenance_note: String = "AcceptedRoadmapNodePullHistory measures PR open -> merge for nodes whose roadmap acceptance receipt is valid. DashboardWorkItemDispatchPullHistory is reserved as a distinct future provenance arm because dispatch -> merge measures a different interval; this slice constructs no value in that arm and calibration admits only accepted-roadmap observations. The two populations therefore cannot blend silently." + +type CycleTimeObservation { + node: RoadmapNodeId + sizing_cell: SizingCell + pull_number: Int + cycle_time: Millisecond + accepted_on: NonEmptyStr + source_url: NonEmptyStr + provenance: CycleTimeProvenance +} + +type CycleTimeHistoryRefusal + = HistoryNodeMissing { node: RoadmapNodeId } + | HistoryNodeDuplicate { node: RoadmapNodeId } + | HistoryAcceptanceMissing { node: RoadmapNodeId } + | HistoryAcceptanceDuplicate { node: RoadmapNodeId } + | HistoryNodeUnsized { node: RoadmapNodeId } + | HistoryPullMissing { node: RoadmapNodeId, pull_number: Int } + | HistoryPullDuplicate { node: RoadmapNodeId, pull_number: Int } + | HistoryPullNotMergedAfterOpen { node: RoadmapNodeId, pull_number: Int } + +type CycleTimePopulationExclusion + = BundledPullDelivery { pull_number: Int, nodes: List } + +type CycleTimeObservationBuild + = CycleTimeObserved { observation: CycleTimeObservation } + | CycleTimePopulationExcluded { exclusion: CycleTimePopulationExclusion } + | CycleTimeHistoryRefused { refusal: CycleTimeHistoryRefusal } + +fn observation_for_link( + link: AcceptedNodePullLink, + nodes: List, + receipts: List, + pulls: List, + links: List, +) -> CycleTimeObservationBuild { + let bundled_links = links |> filter(candidate => candidate.pull_number == link.pull_number) + let matching_nodes = nodes |> filter(n => n.node == link.node) + if count(matching_nodes) == 0 { + return CycleTimeHistoryRefused { refusal: HistoryNodeMissing { node: link.node } } + } + if count(matching_nodes) != 1 { + return CycleTimeHistoryRefused { refusal: HistoryNodeDuplicate { node: link.node } } + } + let node = first(matching_nodes) + let matching_receipts = receipts |> filter(r => r.node == link.node) + if count(matching_receipts) == 0 { + return CycleTimeHistoryRefused { refusal: HistoryAcceptanceMissing { node: link.node } } + } + if count(matching_receipts) != 1 { + return CycleTimeHistoryRefused { refusal: HistoryAcceptanceDuplicate { node: link.node } } + } + if !node_accepted(receipts: receipts, rn: node) { + return CycleTimeHistoryRefused { refusal: HistoryAcceptanceMissing { node: link.node } } + } + let cell = match sizing_cell_of(sizing: node.sizing) { + Absent => return CycleTimeHistoryRefused { refusal: HistoryNodeUnsized { node: link.node } } + Present { value } => value + } + let matching_pulls = pulls |> filter(p => p.pull_number == link.pull_number) + if count(matching_pulls) == 0 { + return CycleTimeHistoryRefused { + refusal: HistoryPullMissing { node: link.node, pull_number: link.pull_number } + } + } + if count(matching_pulls) != 1 { + return CycleTimeHistoryRefused { + refusal: HistoryPullDuplicate { node: link.node, pull_number: link.pull_number } + } + } + let pull = first(matching_pulls) + if pull.merged_at <= pull.opened_at { + return CycleTimeHistoryRefused { + refusal: HistoryPullNotMergedAfterOpen { node: link.node, pull_number: link.pull_number } + } + } + if count(bundled_links) > 1 { + return CycleTimePopulationExcluded { exclusion: BundledPullDelivery { + pull_number: link.pull_number, + nodes: bundled_links |> map(candidate => candidate.node), + } } + } + let receipt = first(matching_receipts) + CycleTimeObserved { + observation: CycleTimeObservation { + node: link.node, + sizing_cell: cell, + pull_number: pull.pull_number, + cycle_time: millisecond(count: pull.merged_at - pull.opened_at), + accepted_on: receipt.accepted_on, + source_url: pull.source_url, + provenance: AcceptedRoadmapNodePullHistory, + } + } +} + +fn roadmap_cycle_time_observation_builds() -> List { + let nodes = declared_roadmap_nodes() + let receipts = roadmap_acceptance_receipts() + let pulls = cited_pull_timings() + let links = accepted_node_pull_links() + links |> map(link => observation_for_link( + link: link, + nodes: nodes, + receipts: receipts, + pulls: pulls, + links: links, + )) +} + +fn accepted_cycle_time_observations(builds: List) -> List { + builds |> flat_map(b => match b { + CycleTimeObserved { observation } => [observation] + CycleTimePopulationExcluded { exclusion: _ } => [] + CycleTimeHistoryRefused { refusal: _ } => [] + }) +} + +fn cycle_time_history_refusals(builds: List) -> List { + builds |> flat_map(b => match b { + CycleTimeObserved { observation: _ } => [] + CycleTimePopulationExcluded { exclusion: _ } => [] + CycleTimeHistoryRefused { refusal } => [refusal] + }) +} + +fn cycle_time_population_exclusions(builds: List) -> List { + builds + |> flat_map(b => match b { + CycleTimeObserved { observation: _ } => [] + CycleTimePopulationExcluded { exclusion } => [exclusion] + CycleTimeHistoryRefused { refusal: _ } => [] + }) + |> fold(init: [], f: (acc, exclusion) => match exclusion { + BundledPullDelivery { pull_number, nodes: _ } => + if any(acc, existing => match existing { + BundledPullDelivery { pull_number: existing_pull, nodes: _ } => existing_pull == pull_number + }) { acc } else { concat(acc, [exclusion]) } + }) +} + +type CycleTimeInterval { + minimum: Millisecond + median: Millisecond + maximum: Millisecond + sample_count: Int +} + +type CalibrationResult + = Calibrated { interval: CycleTimeInterval } + | CalibrationRefusedNoSample { cell: SizingCell } + +fn int_at(sorted: List, index: Int) -> Int { + first(sorted |> skip(n: index)) +} + +fn median_int(sorted: List) -> Int { + let n = count(sorted) + if (n % 2) == 1 { + int_at(sorted: sorted, index: n / 2) + } else { + (int_at(sorted: sorted, index: (n / 2) - 1) + int_at(sorted: sorted, index: n / 2)) / 2 + } +} + +fn calibrate_cell(cell: SizingCell, observations: List) -> CalibrationResult { + let samples = observations + |> filter(o => o.provenance == AcceptedRoadmapNodePullHistory) + |> filter(o => sizing_cell_eq(a: o.sizing_cell, b: cell)) + |> map(o => millisecond_count(m: o.cycle_time)) + |> sort_by(n => n) + if count(samples) == 0 { + CalibrationRefusedNoSample { cell: cell } + } else { + Calibrated { + interval: CycleTimeInterval { + minimum: millisecond(count: first(samples)), + median: millisecond(count: median_int(sorted: samples)), + maximum: millisecond(count: first(reverse(samples))), + sample_count: count(samples), + } + } + } +} + +type ForecastAnchorObservation { observed_at: EpochMs } + +type ReviewCycleCount { count: PositiveMeasureCount } + +type OperatorMergeLatencyAssumption { duration: Millisecond } + +type EndToEndBeltSlotOccupancyAssumption {} + +data end_to_end_belt_slot_occupancy_assumption: EndToEndBeltSlotOccupancyAssumption = EndToEndBeltSlotOccupancyAssumption {} + +type ScheduleForecastAssumptions { + worker_concurrency_cap: BeltCapacity + review_cycle_count: ReviewCycleCount + operator_merge_latency: OperatorMergeLatencyAssumption + belt_slot_occupancy: EndToEndBeltSlotOccupancyAssumption +} + +type ScenarioAdjustmentUnavailableCause + = ReviewCycleDurationUnobserved { declared_count: PositiveMeasureCount } + | OperatorMergeLatencyUnobserved { declared_latency: Millisecond } + +type ScenarioAdjustment + = AdjustmentUnavailable { causes: List } + +fn scenario_adjustment(assumptions: ScheduleForecastAssumptions) -> ScenarioAdjustment { + AdjustmentUnavailable { causes: [ + ReviewCycleDurationUnobserved { declared_count: assumptions.review_cycle_count.count }, + OperatorMergeLatencyUnobserved { declared_latency: assumptions.operator_merge_latency.duration }, + ] } +} + +type ForecastInstant { epoch_ms: EpochMs } + +type ForecastLowerBoundInterval { + earliest: ForecastInstant + median: ForecastInstant + latest: ForecastInstant +} + +type NodeScheduleForecast { + node: RoadmapNodeId + calibrated_cycle_time: CycleTimeInterval + landing_lower_bound: ForecastLowerBoundInterval +} + +type DurationEstimateInterval { + minimum: Millisecond + median: Millisecond + maximum: Millisecond +} + +type TotalWork { duration: DurationEstimateInterval } + +data total_work_semantics_note: String = "TotalWork is the capacity-independent sum of per-node calibrated open-to-merge baseline durations. It applies no review-cycle multiplier and adds no merge latency because both are already inseparable inside the observed interval. It is an absolute cycle-time cost, not an observation of labor hours. Only CapacityConstrainedMinimum interprets that sum as belt slot-time, and it structurally carries the occupancy assumption required for that interpretation." + +type CriticalPathMinimum { duration: DurationEstimateInterval } + +type CapacityConstrainedMinimum { + duration: DurationEstimateInterval + under_belt_slot_occupancy: EndToEndBeltSlotOccupancyAssumption +} + +type ParallelismHeadroomInterval { + minimum: FieldOfFractions + median: FieldOfFractions + maximum: FieldOfFractions +} + +type ParallelismHeadroom { ratio: ParallelismHeadroomInterval } + +type ScheduleLowerBounds { + total_work: TotalWork + critical_path_minimum: CriticalPathMinimum + capacity_constrained_minimum: CapacityConstrainedMinimum + parallelism_headroom: ParallelismHeadroom +} + +type ScheduleForecast { + anchor: ForecastAnchorObservation + assumptions: ScheduleForecastAssumptions + nodes: List + critical_path: List + calibration_population_exclusions: List + scenario_adjustment: ScenarioAdjustment + lower_bounds: ScheduleLowerBounds + completion_lower_bound: ForecastLowerBoundInterval +} + +type CalibrationGap { + cell: SizingCell + nodes: List +} + +type ForecastRefusal + = ForecastGraphEmpty + | DependencyCycle { nodes: List } + | DependencyEndpointUnknown { nodes: List } + | NodeUnsized { nodes: List } + | CycleTimeObservationNonPositive { nodes: List } + | NoCalibrationSample { + gaps: List, + population_exclusions: List, + assumptions: ScheduleForecastAssumptions, + scenario_adjustment: ScenarioAdjustment, + } + | WorkerConcurrencyCapInvalid { declared_cap: Int } + | CalibrationHistoryRefused { refusals: List } + | ForecastBuildRefused { node: RoadmapNodeId } + +type ScheduleForecastResult + = ScheduleForecastKnown { forecast: ScheduleForecast } + | ScheduleForecastRefused { refusals: List } + +fn forecast_refusal_count(result: ScheduleForecastResult) -> Int { + match result { + ScheduleForecastKnown { forecast: _ } => 0 + ScheduleForecastRefused { refusals } => count(refusals) + } +} + +fn no_calibration_gap_count(result: ScheduleForecastResult) -> Int { + match result { + ScheduleForecastKnown { forecast: _ } => 0 + ScheduleForecastRefused { refusals } => refusals |> fold(init: 0, f: (acc, refusal) => + acc + match refusal { + NoCalibrationSample { gaps, population_exclusions: _, assumptions: _, scenario_adjustment: _ } => count(gaps) + WorkerConcurrencyCapInvalid { declared_cap: _ } => 0 + ForecastGraphEmpty => 0 + DependencyCycle { nodes: _ } => 0 + DependencyEndpointUnknown { nodes: _ } => 0 + NodeUnsized { nodes: _ } => 0 + CycleTimeObservationNonPositive { nodes: _ } => 0 + CalibrationHistoryRefused { refusals: _ } => 0 + ForecastBuildRefused { node: _ } => 0 + } + ) + } +} + +fn unique_cells(cells: List) -> List { + cells |> fold(init: [], f: (acc, cell) => + if any(acc, existing => sizing_cell_eq(a: existing, b: cell)) { acc } + else { concat(acc, [cell]) } + ) +} + +fn calibration_gaps(nodes: List, observations: List) -> List { + let cells = unique_cells(cells: nodes |> flat_map(n => match sizing_cell_of(sizing: n.sizing) { + Absent => [] + Present { value } => [value] + })) + cells |> flat_map(cell => match calibrate_cell(cell: cell, observations: observations) { + Calibrated { interval: _ } => [] + CalibrationRefusedNoSample { cell: missing } => [CalibrationGap { + cell: missing, + nodes: nodes + |> filter(n => match sizing_cell_of(sizing: n.sizing) { + Absent => false + Present { value } => sizing_cell_eq(a: value, b: missing) + }) + |> map(n => n.node), + }] + }) +} + +fn dependency_graph(nodes: List, edges: List) -> CallGraph { + CallGraph { + edges: edges |> map(e => GraphEdge { caller: e.child as String, callee: e.parent as String }) + } +} + +fn dependency_cycle_nodes(nodes: List, edges: List) -> List { + graph_cycle_members( + names: nodes |> map(n => n.node as String), + graph: dependency_graph(nodes: nodes, edges: edges), + ) |> map(n => n as RoadmapNodeId) +} + +fn unknown_dependency_endpoints(nodes: List, edges: List) -> List { + let ids = nodes |> map(n => n.node) + concat( + edges |> filter(e => !any(ids, id => id == e.child)) |> map(e => e.child), + edges |> filter(e => !any(ids, id => id == e.parent)) |> map(e => e.parent), + ) |> fold(init: [], f: (acc, id) => if any(acc, existing => existing == id) { acc } else { concat(acc, [id]) }) +} + +type ForecastBound = EarliestBound | MedianBound | LatestBound + +fn interval_bound(interval: CycleTimeInterval, bound: ForecastBound) -> Int { + match bound { + EarliestBound => millisecond_count(m: interval.minimum) + MedianBound => millisecond_count(m: interval.median) + LatestBound => millisecond_count(m: interval.maximum) + } +} + +fn baseline_node_duration( + interval: CycleTimeInterval, + bound: ForecastBound, +) -> Millisecond { + millisecond(count: interval_bound(interval: interval, bound: bound)) +} + +type PathEstimate { + duration: Millisecond + path: List +} + +type PathEstimateResult + = PathEstimated { estimate: PathEstimate } + | PathEstimateUnknown { node: RoadmapNodeId } + +fn longer_path(a: PathEstimate, b: PathEstimate) -> PathEstimate { + if millisecond_count(m: b.duration) > millisecond_count(m: a.duration) { b } else { a } +} + +fn path_to_node( + node: RoadmapNode, + nodes: List, + edges: List, + observations: List, + bound: ForecastBound, +) -> PathEstimateResult { + let cell = match sizing_cell_of(sizing: node.sizing) { + Absent => return PathEstimateUnknown { node: node.node } + Present { value } => value + } + let interval = match calibrate_cell(cell: cell, observations: observations) { + CalibrationRefusedNoSample { cell: _ } => return PathEstimateUnknown { node: node.node } + Calibrated { interval } => interval + } + let parent_ids = edges |> filter(e => e.child == node.node) |> map(e => e.parent) + let parent_estimates = parent_ids |> map(parent_id => + match nodes |> filter(n => n.node == parent_id) |> first { + Absent => PathEstimateUnknown { node: parent_id } + Present { value: parent } => path_to_node( + node: parent, + nodes: nodes, + edges: edges, + observations: observations, + bound: bound, + ) + } + ) + let refused = parent_estimates |> filter(r => match r { + PathEstimated { estimate: _ } => false + PathEstimateUnknown { node: _ } => true + }) + match first(refused) { + Present { value } => value + Absent => { + let parent_best = parent_estimates |> fold( + init: PathEstimate { duration: millisecond(count: 0), path: [] }, + f: (best, result) => match result { + PathEstimated { estimate } => longer_path(a: best, b: estimate) + PathEstimateUnknown { node: _ } => best + }, + ) + PathEstimated { estimate: PathEstimate { + duration: millisecond( + count: millisecond_count(m: parent_best.duration) + millisecond_count(m: baseline_node_duration( + interval: interval, + bound: bound, + )), + ), + path: concat(parent_best.path, [node.node]), + } } + } + } +} + +type NodeForecastBuild + = NodeForecastBuilt { forecast: NodeScheduleForecast, median_path: List } + | NodeForecastBuildRefused { node: RoadmapNodeId } + +type CriticalPathChoice { + median_epoch_ms: EpochMs + path: List +} + +fn forecast_instant(anchor: ForecastAnchorObservation, duration: Millisecond) -> ForecastInstant { + ForecastInstant { epoch_ms: anchor.observed_at + millisecond_count(m: duration) } +} + +fn build_node_forecast( + node: RoadmapNode, + nodes: List, + edges: List, + observations: List, + anchor: ForecastAnchorObservation, +) -> NodeForecastBuild { + let cell = match sizing_cell_of(sizing: node.sizing) { + Absent => return NodeForecastBuildRefused { node: node.node } + Present { value } => value + } + let calibrated = match calibrate_cell(cell: cell, observations: observations) { + CalibrationRefusedNoSample { cell: _ } => return NodeForecastBuildRefused { node: node.node } + Calibrated { interval } => interval + } + let earliest = path_to_node(node: node, nodes: nodes, edges: edges, observations: observations, bound: EarliestBound) + let median = path_to_node(node: node, nodes: nodes, edges: edges, observations: observations, bound: MedianBound) + let latest = path_to_node(node: node, nodes: nodes, edges: edges, observations: observations, bound: LatestBound) + match earliest { + PathEstimateUnknown { node: _ } => NodeForecastBuildRefused { node: node.node } + PathEstimated { estimate: early } => match median { + PathEstimateUnknown { node: _ } => NodeForecastBuildRefused { node: node.node } + PathEstimated { estimate: middle } => match latest { + PathEstimateUnknown { node: _ } => NodeForecastBuildRefused { node: node.node } + PathEstimated { estimate: late } => NodeForecastBuilt { + forecast: NodeScheduleForecast { + node: node.node, + calibrated_cycle_time: calibrated, + landing_lower_bound: ForecastLowerBoundInterval { + earliest: forecast_instant(anchor: anchor, duration: early.duration), + median: forecast_instant(anchor: anchor, duration: middle.duration), + latest: forecast_instant(anchor: anchor, duration: late.duration), + }, + }, + median_path: middle.path, + } + } + } + } +} + +fn dependency_completion_lower_bound(nodes: List, anchor: ForecastAnchorObservation) -> ForecastLowerBoundInterval { + nodes |> fold( + init: ForecastLowerBoundInterval { + earliest: ForecastInstant { epoch_ms: anchor.observed_at }, + median: ForecastInstant { epoch_ms: anchor.observed_at }, + latest: ForecastInstant { epoch_ms: anchor.observed_at }, + }, + f: (acc, node) => ForecastLowerBoundInterval { + earliest: ForecastInstant { epoch_ms: if node.landing_lower_bound.earliest.epoch_ms > acc.earliest.epoch_ms { node.landing_lower_bound.earliest.epoch_ms } else { acc.earliest.epoch_ms } }, + median: ForecastInstant { epoch_ms: if node.landing_lower_bound.median.epoch_ms > acc.median.epoch_ms { node.landing_lower_bound.median.epoch_ms } else { acc.median.epoch_ms } }, + latest: ForecastInstant { epoch_ms: if node.landing_lower_bound.latest.epoch_ms > acc.latest.epoch_ms { node.landing_lower_bound.latest.epoch_ms } else { acc.latest.epoch_ms } }, + }, + ) +} + +fn total_work_duration( + nodes: List, + bound: ForecastBound, +) -> Millisecond { + let total = nodes |> fold(init: 0, f: (acc, node) => + acc + millisecond_count(m: baseline_node_duration( + interval: node.calibrated_cycle_time, + bound: bound, + )) + ) + millisecond(count: total) +} + +fn total_work( + nodes: List, +) -> TotalWork { + TotalWork { duration: DurationEstimateInterval { + minimum: total_work_duration(nodes: nodes, bound: EarliestBound), + median: total_work_duration(nodes: nodes, bound: MedianBound), + maximum: total_work_duration(nodes: nodes, bound: LatestBound), + } } +} + +fn critical_path_minimum( + bound: ForecastLowerBoundInterval, + anchor: ForecastAnchorObservation, +) -> CriticalPathMinimum { + CriticalPathMinimum { duration: DurationEstimateInterval { + minimum: millisecond(count: bound.earliest.epoch_ms - anchor.observed_at), + median: millisecond(count: bound.median.epoch_ms - anchor.observed_at), + maximum: millisecond(count: bound.latest.epoch_ms - anchor.observed_at), + } } +} + +fn capacity_constrained_minimum( + total: TotalWork, + critical: CriticalPathMinimum, + worker_cap: Int, + belt_slot_occupancy: EndToEndBeltSlotOccupancyAssumption, +) -> CapacityConstrainedMinimum { + CapacityConstrainedMinimum { + duration: DurationEstimateInterval { + minimum: millisecond(count: if millisecond_count(m: critical.duration.minimum) > (millisecond_count(m: total.duration.minimum) + worker_cap - 1) / worker_cap { millisecond_count(m: critical.duration.minimum) } else { (millisecond_count(m: total.duration.minimum) + worker_cap - 1) / worker_cap }), + median: millisecond(count: if millisecond_count(m: critical.duration.median) > (millisecond_count(m: total.duration.median) + worker_cap - 1) / worker_cap { millisecond_count(m: critical.duration.median) } else { (millisecond_count(m: total.duration.median) + worker_cap - 1) / worker_cap }), + maximum: millisecond(count: if millisecond_count(m: critical.duration.maximum) > (millisecond_count(m: total.duration.maximum) + worker_cap - 1) / worker_cap { millisecond_count(m: critical.duration.maximum) } else { (millisecond_count(m: total.duration.maximum) + worker_cap - 1) / worker_cap }), + }, + under_belt_slot_occupancy: belt_slot_occupancy, + } +} + +fn completion_lower_bound_from_duration( + duration: DurationEstimateInterval, + anchor: ForecastAnchorObservation, +) -> ForecastLowerBoundInterval { + ForecastLowerBoundInterval { + earliest: forecast_instant(anchor: anchor, duration: duration.minimum), + median: forecast_instant(anchor: anchor, duration: duration.median), + latest: forecast_instant(anchor: anchor, duration: duration.maximum), + } +} + +fn parallelism_headroom(total: TotalWork, critical: CriticalPathMinimum) -> ParallelismHeadroom { + ParallelismHeadroom { ratio: ParallelismHeadroomInterval { + minimum: FieldOfFractions { num: total.duration.minimum, denom: critical.duration.minimum }, + median: FieldOfFractions { num: total.duration.median, denom: critical.duration.median }, + maximum: FieldOfFractions { num: total.duration.maximum, denom: critical.duration.maximum }, + } } +} + +fn schedule_forecast( + nodes: List, + edges: List, + observations: List, + history_refusals: List, + population_exclusions: List, + assumptions: ScheduleForecastAssumptions, + anchor: ForecastAnchorObservation, +) -> ScheduleForecastResult { + let cycles = dependency_cycle_nodes(nodes: nodes, edges: edges) + let unknown_endpoints = unknown_dependency_endpoints(nodes: nodes, edges: edges) + let unsized = nodes |> filter(n => match n.sizing { Unsized => true Sized { intricacy: _, volume: _, repo: _, acceptance: _ } => false }) |> map(n => n.node) + let non_positive_observation_nodes = observations + |> filter(observation => millisecond_count(m: observation.cycle_time) <= 0) + |> fold(init: [], f: (located, observation) => + if any(located, node => node == observation.node) { located } + else { append(located, items: [observation.node]) } + ) + let gaps = calibration_gaps(nodes: nodes, observations: observations) + let invalid_worker_cap = assumptions.worker_concurrency_cap.max_concurrent <= 0 + let refusals = concat( + if count(nodes) == 0 { [ForecastGraphEmpty] } else { [] }, + concat( + if count(cycles) > 0 { [DependencyCycle { nodes: cycles }] } else { [] }, + concat( + if count(unknown_endpoints) > 0 { [DependencyEndpointUnknown { nodes: unknown_endpoints }] } else { [] }, + concat( + if count(unsized) > 0 { [NodeUnsized { nodes: unsized }] } else { [] }, + concat( + if count(non_positive_observation_nodes) > 0 { [CycleTimeObservationNonPositive { nodes: non_positive_observation_nodes }] } else { [] }, + concat( + if count(gaps) > 0 { [NoCalibrationSample { + gaps: gaps, + population_exclusions: population_exclusions, + assumptions: assumptions, + scenario_adjustment: scenario_adjustment(assumptions: assumptions), + }] } else { [] }, + concat( + if invalid_worker_cap { [WorkerConcurrencyCapInvalid { declared_cap: assumptions.worker_concurrency_cap.max_concurrent }] } else { [] }, + if count(history_refusals) > 0 { [CalibrationHistoryRefused { refusals: history_refusals }] } else { [] }, + ), + ), + ), + ), + ), + ), + ) + if count(refusals) > 0 { + return ScheduleForecastRefused { refusals: refusals } + } + let builds = nodes |> map(node => build_node_forecast( + node: node, + nodes: nodes, + edges: edges, + observations: observations, + anchor: anchor, + )) + let build_refusals = builds |> flat_map(b => match b { + NodeForecastBuilt { forecast: _, median_path: _ } => [] + NodeForecastBuildRefused { node } => [ForecastBuildRefused { node: node }] + }) + if count(build_refusals) > 0 { + return ScheduleForecastRefused { refusals: build_refusals } + } + let forecasts = builds |> flat_map(b => match b { + NodeForecastBuilt { forecast, median_path: _ } => [forecast] + NodeForecastBuildRefused { node: _ } => [] + }) + let critical = builds |> fold( + init: CriticalPathChoice { median_epoch_ms: 0, path: [] }, + f: (best, candidate) => match candidate { + NodeForecastBuildRefused { node: _ } => best + NodeForecastBuilt { forecast, median_path } => + if forecast.landing_lower_bound.median.epoch_ms >= best.median_epoch_ms { + CriticalPathChoice { median_epoch_ms: forecast.landing_lower_bound.median.epoch_ms, path: median_path } + } else { best } + }, + ) + let dependency_bound = dependency_completion_lower_bound(nodes: forecasts, anchor: anchor) + let total = total_work(nodes: forecasts) + let critical_minimum = critical_path_minimum(bound: dependency_bound, anchor: anchor) + let capacity_minimum = capacity_constrained_minimum( + total: total, + critical: critical_minimum, + worker_cap: assumptions.worker_concurrency_cap.max_concurrent, + belt_slot_occupancy: assumptions.belt_slot_occupancy, + ) + ScheduleForecastKnown { forecast: ScheduleForecast { + anchor: anchor, + assumptions: assumptions, + nodes: forecasts, + critical_path: critical.path, + calibration_population_exclusions: population_exclusions, + scenario_adjustment: scenario_adjustment(assumptions: assumptions), + lower_bounds: ScheduleLowerBounds { + total_work: total, + critical_path_minimum: critical_minimum, + capacity_constrained_minimum: capacity_minimum, + parallelism_headroom: parallelism_headroom(total: total, critical: critical_minimum), + }, + completion_lower_bound: completion_lower_bound_from_duration(duration: capacity_minimum.duration, anchor: anchor), + } } +} + +fn live_roadmap_schedule_forecast( + assumptions: ScheduleForecastAssumptions, + anchor: ForecastAnchorObservation, +) -> ScheduleForecastResult { + let builds = roadmap_cycle_time_observation_builds() + schedule_forecast( + nodes: active_roadmap_nodes(), + edges: active_roadmap_edges(), + observations: accepted_cycle_time_observations(builds: builds), + history_refusals: cycle_time_history_refusals(builds: builds), + population_exclusions: cycle_time_population_exclusions(builds: builds), + assumptions: assumptions, + anchor: anchor, + ) +} diff --git a/dag/gunbc/roadmap_identity.dag b/dag/gunbc/roadmap_identity.dag index 51a84710a1b..958588d0d88 100644 --- a/dag/gunbc/roadmap_identity.dag +++ b/dag/gunbc/roadmap_identity.dag @@ -19,7 +19,7 @@ type RoadmapIdentityRecord superseded_by: RoadmapNodeIdentity?, } -data roadmap_tombstone_roster_note: String = "The first retirement landed 2026-07-31, in the same change that removed its node from declared_roadmap_nodes() — exactly the discipline this roster exists for (a retirement is a state, never a deletion; the failure mode is the 2026-07-27 authority refresh, when a node carrying the only execution contract was removed and nothing noticed). v2-phase-carriers was split by the post-merge verdict on gunbc#7489 into five staged nodes (self-grounding frontier, Translate refusal, inferred-tree completeness, per-kind derivation coverage, target realization gate); its identity is tombstoned with superseded_by naming the split's entry node, so historical links resolve and the identity can never be reused." +data roadmap_tombstone_roster_note: String = "The first retirement landed 2026-07-31, in the same change that removed its node from declared_roadmap_nodes() — exactly the discipline this roster exists for (a retirement is a state, never a deletion; the failure mode is the 2026-07-27 authority refresh, when a node carrying the only execution contract was removed and nothing noticed). v2-phase-carriers was split by the post-merge verdict on gunbc#7489 into five staged nodes (self-grounding frontier, Translate refusal, inferred-tree completeness, per-kind derivation coverage, target realization gate); its identity is tombstoned with superseded_by naming the split's entry node, so historical links resolve and the identity can never be reused. The 2026-08-01 spine reconciliation (operator verdict on the guarantee lane) retired four more the same way, each superseded_by the recut's entry node: the three monolithic walls whose landed fragments merged at a different grain than the nodes described (floor-method-existence-wall -> method-established-surface-wall via gunbc#7484; floor-call-shape-wall -> call-label-and-surplus-wall via gunbc#7519; floor-inhabitance-wall -> declared-conformance-ground-fragment via gunbc#7484's counted advisory), and the monolithic acceptance door, split into mechanism-then-activation so the door's audit form lands with the carrier instead of after every wall (compiler-accepted-obligation-closure -> accepted-obligation-door-mechanism)." fn roadmap_tombstoned_identities() -> List { [ @@ -28,6 +28,26 @@ fn roadmap_tombstoned_identities() -> List { former_slug: "v2-phase-carriers" as RoadmapNodeId, superseded_by: Present { value: "rn_PBIAB7X2M6IXIKMOXT3D7DKIXQ" as RoadmapNodeIdentity }, }, + TombstonedIdentity { + identity: "rn_A7QX5QZMWKC6STOGGDGD4KMIHQ" as RoadmapNodeIdentity, + former_slug: "floor-method-existence-wall" as RoadmapNodeId, + superseded_by: Present { value: "rn_IGN7UM5UQQPZ6MNMIX5TVUMNSK" as RoadmapNodeIdentity }, + }, + TombstonedIdentity { + identity: "rn_DYQZBEQKOWIYI776266OWMZREQ" as RoadmapNodeIdentity, + former_slug: "floor-call-shape-wall" as RoadmapNodeId, + superseded_by: Present { value: "rn_EDXPW53NQMZVLEPYNYH6X4VNKM" as RoadmapNodeIdentity }, + }, + TombstonedIdentity { + identity: "rn_6W5ACS5ZE3ZMTA7BFCQLY76VW4" as RoadmapNodeIdentity, + former_slug: "floor-inhabitance-wall" as RoadmapNodeId, + superseded_by: Present { value: "rn_4ELCVV2EIHCBGHMOG6T4IZEMT3" as RoadmapNodeIdentity }, + }, + TombstonedIdentity { + identity: "rn_J5XDQKBF6J4YPGFKWQXE45KOQM" as RoadmapNodeIdentity, + former_slug: "compiler-accepted-obligation-closure" as RoadmapNodeId, + superseded_by: Present { value: "rn_6QXWDNSCBUAK6JWU7VSUWBXWMH" as RoadmapNodeIdentity }, + }, ] } diff --git a/dag/gunbc/roadmap_page.dag b/dag/gunbc/roadmap_page.dag index 795f63cd7e8..042ab1055cf 100644 --- a/dag/gunbc/roadmap_page.dag +++ b/dag/gunbc/roadmap_page.dag @@ -6,49 +6,63 @@ import gunbc.site.markup { txt, el, el_class, el_attrs } import std.markdown { markdown_inlines_to_fragments } import extdeps.languages.markdown { parse_inline_markdown_minimal } import gunbc.roadmap_style { roadmap_css } -import gunbc.site.mark { render_mark } import gunbc.roadmap_model { - RoadmapNode, RoadmapEdge, Pointer, RoadmapNodeId, - RoadmapParent, TopLevel, UnderParent, roadmap_primary_parent, roadmap_parent_ids, is_top_level, + RoadmapNode, RoadmapEdge, RoadmapNodeId, + RoadmapParent, TopLevel, UnderParent, roadmap_primary_parent, is_top_level, SectionElement, Prose, Group, SectionGroup, - NodeSizing, Unsized, Sized, - Intricacy, IntricacyLow, IntricacyMedium, IntricacyHigh, - Volume, VolumeSmall, VolumeMedium, VolumeLarge, -} -import gunbc.roadmap_status { - RoadmapItem, DerivableLine, AuthoredLine, PlainLine, SupersededLine, TicketLine, - TicketFields, TicketUpdate, line_ticket, - item_superseded_by, item_is_done, signoff_accepts, signoff_by, } +import gunbc.roadmap_status { line_ticket } import gunbc.roadmap_document { RoadmapDocument, RoadmapSection } import gunbc.roadmap_altitude { section_counts, section_counts_wire, statuses_all_routine, SectionCounts } import gunbc.roadmap_authority { roadmap_authority, authored_merged_prs, - declared_roadmap_nodes, active_roadmap_nodes, accepted_roadmap_node_ids, + accepted_roadmap_node_ids, + ClosingContractTaskPair, closing_contract_task_pairs, closing_contract_family_claim, } import gunbc.roadmap_spawner { spawn_frontier, SpawnFrontier, doc_all_nodes, doc_all_edges } import gunbc.roadmap_component { roadmap_dispatch_asset_path, sound_toggle_class, theme_toggle_class, + theme_toggle_label_prefix, theme_toggle_auto_value, disclosure_mark_class, disclosure_mark_glyph, node_mid_class, - WorkflowLamp, workflow_lamps_v0, workflow_lamp_key, workflow_lamp_label, - workflow_lamp_initial_state, - workflow_controls_class, workflow_strip_class, workflow_lamp_class, - workflow_lamp_indicator_class, workflow_lamp_label_class, - workflow_lamp_data_attribute, workflow_observation_data_attribute, - workflow_lamp_label_data_attribute, - workflow_progress_stages, - workflow_progress_stage_key, workflow_progress_stage_label, - workflow_progress_class, workflow_stage_class, workflow_activity_class, - workflow_stage_data_attribute, workflow_segment_state_data_attribute, + workflow_controls_class, dashboard_instance_class, dashboard_instance_label_class, -} -import gunbc.roadmap_workflow_stage { WorkflowSegmentKind } -import gunbc.client_observation_state { client_observation_state_key } -import gunbc.v1_deletion_plan { V1FinishLineSpec, v1_exit_finish_lines, v1_finish_line_accepted } + node_activity_class, activity_summary_class, activity_detail_class, + activity_ledger_class, activity_obligation_class, + activity_evidence_class, activity_evidence_row_class, activity_located_reason_class, +} +import gunbc.roadmap_presentation { + WorkRowView, work_row_view, row_headline, + WorkLifecycle, OpenLifecycle, ReviewLifecycle, DoneLifecycle, SupersededLifecycle, + work_lifecycle, work_lifecycle_key, + WorkScheduling, SchedulingReady, SchedulingUpcoming, SchedulingDone, SchedulingSuperseded, SchedulingUnplanned, + scheduling_dependency_line, + ActivityView, NoActiveWork, ActiveWork, ActiveWorkRefused, CompletedWork, ActivityUnobservable, + ActivityAttention, activity_attention_key, activity_arm_key, activity_summary_line, + ActivityEvidenceRow, + ActionView, WorkAction, WorkActionKind, DispatchAction, ClearSessionAction, + SupportingFact, + DisclosureView, NoDisclosure, TicketDisclosure, LegacyBodyDisclosure, + DisclosureField, DisclosureNote, + work_scheduling_key, row_is_active_work, + activity_view_from_progress, + activity_state_sep, activity_detail_sep, activity_evidence_kv_sep, activity_disclosure_label, + ProgramProgressView, + DailyWorkspaceView, daily_workspace_view, + ObservationImpactView, ObservationsNominal, WorkspaceObservationImpact, + observation_impact_view, workspace_covers_row_activity, + lifecycle_chip_renders, + WorkFamilyDecl, WorkFamilyMemberFact, WorkFamilyView, + WorkBandEntry, BandRow, BandFamily, +} +import gunbc.roadmap_workflow_progress { WorkflowAttemptProgress } +import gunbc.roadmap_workflow_stage { + WorkflowSegment, workflow_segment_kind_label, workflow_segment_state_key, +} +import gunbc.v1_deletion_plan { V1FinishLineSpec, v1_exit_finish_lines, v1_finish_line_accepted, v1_finish_lines_accepted_count } fn roadmap_page_max_depth() -> Int { 12 } -data finish_line_stylesheet_debt_note: String = "DECLARED SCAFFOLD, counted not silent (2026-07-28): this section's markup is the finish-line projection, but gunbc.roadmap_style still carries the retired three-gate/brick rules (.three-gate-progress, .gate-row, .gate-bar, .bar-fill, ul.bricks, li.brick, .brick-state) and carries no rule for the .finish-line-* classes emitted here — so the section ships structurally correct and visually unstyled, and those rules are unreferenced. The styling pass was deliberately NOT bundled: test.claim.design_register_lift_parity_witness pins roadmap_css_lift_parity_digest, and every re-pin on that row is derived BY EXECUTION from the emitted stylesheet (never chosen), which this change's authoring environment cannot produce. Landing a hand-guessed digest would be the fabricated-output failure DESIGN §5 forbids, and landing a moved stylesheet against a stale pin would knowingly red the corpus. DISSOLVE-ON: one styling pass in an environment that can build the seed — delete the seven gate/brick rules, add the .finish-line-* rules on the same tokened/typed grain (the width_brick_state and radius_5/height_bar dimension tokens lose their last consumers with them), then re-pin roadmap_css_lift_parity_digest from the executed emission with the derivation receipt, exactly as every prior re-pin note on that row records." +data finish_line_placement_note: String = "The finish-line block is REFERENCE prose, not operational attention (A1 of the presentation recon, operator-signed 2026-08-01): the daily workspace's declared archetype is Operational (gunbc.design.archetype dashboard_archetype), so / leads with the frontier and carries only the one-line program strip (program_progress_markup_of, the ProgramProgressView consumer); the four claims render on /roadmap, where the strategic argument lives. The 2026-07-28 stylesheet-debt scaffold that shipped this section unstyled dissolved in the same pass: the gate/brick rules are deleted, .finish-line-* rules exist on the tokened grain, and roadmap_css_lift_parity_digest was re-pinned by execution (see the A1 re-pin note in test.claim.design_register_lift_parity_witness). The count line under the heading is the LOCAL denominator — accepted finish lines over the four — never the global roadmap population, which belonged to a different question (review: the section visually implied a denominator of four while reporting an unrelated global one)." fn finish_line_state_class(accepted: Bool) -> String { if accepted { "accepted" } else { "active" } @@ -64,11 +78,12 @@ fn finish_line_row(spec: V1FinishLineSpec, accepted_ids: List) -> ]) } -fn roadmap_counts_label() -> String { +fn finish_line_local_count_label(accepted_ids: List) -> String { join([ - to_string(count(declared_roadmap_nodes())), " declared · ", - to_string(count(accepted_roadmap_node_ids())), " accepted · ", - to_string(count(active_roadmap_nodes())), " active", + to_string(v1_finish_lines_accepted_count(accepted: accepted_ids)), + " of ", + to_string(count(v1_exit_finish_lines())), + " complete", ], "") } @@ -76,7 +91,7 @@ fn finish_line_progress_markup() -> Fragment { let accepted_ids = accepted_roadmap_node_ids() el_class("section", "finish-line-progress", [ el("h2", [ txt("v1 exit: four finish lines") ]), - el_class("p", "finish-line-counts", [ txt(roadmap_counts_label()) ]), + el_class("p", "finish-line-counts", [ txt(finish_line_local_count_label(accepted_ids: accepted_ids)) ]), el_class("ul", "finish-lines", map(v1_exit_finish_lines(), s => finish_line_row(spec: s, accepted_ids: accepted_ids))), ]) @@ -91,96 +106,40 @@ fn dashboard_instance_banner() -> Fragment { } -data sound_toggle_placement_note: String = "The mute lives in the HEADER, pushed to the far edge by margin-left auto, because it is a property of the whole surface rather than of any row — a per-row mute would be a second control on every line, which is exactly what the exemplar's scope forbids. It renders with an empty label: the text is painted by the client program from the persisted preference on load, so the markup never ships a claim about the sound state that a different browser's storage would contradict. With no JS the control is a blank button that does nothing, which is honest — there is no sound to mute either." +data sound_toggle_placement_note: String = "The mute lives in the HEADER, pushed to the far edge by margin-left auto, because it is a property of the whole surface rather than of any row — a per-row mute would be a second control on every line, which is exactly what the exemplar's scope forbids. The former empty-label rendering was corrected at A1 (presentation recon, 2026-08-01): honest-before-JS does not require blankness — a blank button has no visible content and no accessible name, which is a defect, not honesty. The TWO TOGGLES SHIP DIFFERENT AMOUNTS OF STATE, and the asymmetry is the honesty boundary, not an inconsistency (A1 reconciliation, operator feedback 2026-08-01): the theme control ships 'theme · auto' because at parse time the document carries no data-theme and prefers-color-scheme governs — which is the DEFINITION of auto, true for every visitor until the client program applies a stored choice; the sound control ships only its NAME because its state has no parse-time truth — the preference lives in this browser's storage (unreadable at serve), and for a no-JS visitor no sound ever plays, so any shipped state word would be a fabricated claim for someone. The client program repaints both from storage on load, exactly as before." fn sound_toggle_control() -> Fragment { el_attrs("button", [ attr(name: "class", value: sound_toggle_class ), attr(name: "type", value: "button" ), - ], []) + ], [ txt("sound") ]) } fn theme_toggle_control() -> Fragment { el_attrs("button", [ attr(name: "class", value: theme_toggle_class ), attr(name: "type", value: "button" ), - ], []) -} - -fn roadmap_header(title: String) -> Fragment { - el("header", [ render_mark(depth: 1), el("h1", [ txt(title) ]), sound_toggle_control(), theme_toggle_control() ]) -} - -fn node_status_symbol(rn: RoadmapNode, merged: List) -> String { - if item_superseded_by(item: rn.line) == "" { - if signoff_accepts(s: rn.signoff) { - "done" - } else { - if item_is_done(item: rn.line, merged: merged) { - "review" - } else { - "open" - } - } - } else { - "superseded" - } -} - -fn intricacy_label(i: Intricacy) -> String { - match i { - IntricacyLow => "i:low" - IntricacyMedium => "i:med" - IntricacyHigh => "i:high" - } + ], [ txt(join([theme_toggle_label_prefix, theme_toggle_auto_value], "")) ]) } -fn volume_label(v: Volume) -> String { - match v { - VolumeSmall => "v:small" - VolumeMedium => "v:med" - VolumeLarge => "v:large" - } -} - -fn sizing_label(s: NodeSizing) -> String { - match s { - Unsized => "" - Sized { intricacy, volume, repo, acceptance } => - join([intricacy_label(i: intricacy), " ", volume_label(v: volume), " ", repo], "") - } -} +data roadmap_wordmark_note: String = "A1 header decision (presentation recon, operator-signed 2026-08-01): the header carries a TEXT wordmark, not the site mark. The mark that stood here was gunbc.site.mark candidate A ('Confluence') — an explicitly unselected brand experiment (DESIGN's site thread still owes candidates B–D) whose aria-label said 'gunb.ai mark' on a gunbc operational tool: a public-site brand artifact leaking into a tool surface. The minimal correct move is the wordmark; a drawn mark returns if and when an actual brand-selection task runs (the sandbox auditioner is the venue, the swatch-graduation precedent). The h1 carries the PAGE name alone — the product name lives in the wordmark, so the two never restate each other." -fn line_title(line: RoadmapItem) -> String { - match line { - DerivableLine { prs, title, description } => title - AuthoredLine { done, content } => content - PlainLine { content } => content - SupersededLine { by: _, content } => content - TicketLine { done: _, ticket } => join(["**", ticket.headline, "** — ", ticket.brief], "") - } -} - -fn title_nodes(line: RoadmapItem) -> List { - markdown_inlines_to_fragments(inlines: parse_inline_markdown_minimal(text: line_title(line: line))) +fn roadmap_header(page: String) -> Fragment { + el("header", [ + el_class("span", "wordmark", [ txt("gunbc") ]), + el("h1", [ txt(page) ]), + sound_toggle_control(), + theme_toggle_control(), + ]) } -fn line_lead(line: RoadmapItem) -> String { - let full = line_title(line: line) - let dash_parts = split(s: full, delimiter: " — ") - if count(dash_parts) > 1 { - first(dash_parts).value - } else { - let dot_parts = split(s: full, delimiter: ". ") - if count(dot_parts) > 1 { first(dot_parts).value } else { full } - } -} +data page_status_vocab_note: String = "The status vocabulary the page renders (chip text, node- classes, altitude-band counts) is work_lifecycle_key over the seam's WorkLifecycle — the node_status_symbol decision moved to gunbc.roadmap_presentation as its single authority (operator blocker 1, 2026-08-01: the old renderer independently re-decided open/review/done/superseded beside the seam). The key set is unchanged, so the altitude machinery and the styling grain consume the same four words." -fn lead_nodes(line: RoadmapItem) -> List { - markdown_inlines_to_fragments(inlines: parse_inline_markdown_minimal(text: line_lead(line: line))) +fn node_lifecycle_key(rn: RoadmapNode, merged: List) -> String { + work_lifecycle_key(l: work_lifecycle(rn: rn, merged: merged)) } -data roadmap_lead_budget_note: String = "The lead is the row's at-a-glance face: everything before the first ' — ' (falling back to the first '. ', then the whole title). A row whose LEAD is itself paragraph-length defeats the disclosure — the brief stays collapsed but the summary line does not, so the reader pays the full text anyway. The bound is authoring residue, not a structural class: no construction forbids a long sentence, so this is the §5-sanctioned lens over the genuinely unstructurable part, and it is typed/located/counted (node id + measured length) rather than a bare Bool, so a red says WHICH row and by how much. 300 chars is the operator's declared bound (roadmap-workspace-ux-plan.md P1b); an offender is re-cut by moving its summary delimiter earlier or splitting its content, while the body remains available behind the disclosure." +data roadmap_lead_budget_note: String = "The lead is the row's at-a-glance face: the seam's row_headline (the ticket's typed headline; for legacy lines the named lead residue inside gunbc.roadmap_presentation). A row whose LEAD is itself paragraph-length defeats the disclosure — the brief stays collapsed but the summary line does not, so the reader pays the full text anyway. The bound is authoring residue, not a structural class: no construction forbids a long sentence, so this is the §5-sanctioned lens over the genuinely unstructurable part, and it is typed/located/counted (node id + measured length) rather than a bare Bool, so a red says WHICH row and by how much. 300 chars is the operator's declared bound (roadmap-workspace-ux-plan.md P1b); an offender is re-cut by moving its summary delimiter earlier or splitting its content, while the body remains available behind the disclosure." fn roadmap_lead_budget_chars() -> Int { 300 } @@ -191,7 +150,7 @@ type LeadBudgetViolation { fn lead_budget_violations(nodes: List) -> List { fold(nodes, init: [], f: (acc, rn) => { - let n = line_lead(line: rn.line).length() + let n = row_headline(line: rn.line).length() if n > roadmap_lead_budget_chars() { concat(acc, [LeadBudgetViolation { node: rn.node as String, lead_chars: n }]) } else { @@ -239,74 +198,80 @@ fn inline_md_nodes(text: String) -> List { markdown_inlines_to_fragments(inlines: parse_inline_markdown_minimal(text: text)) } -fn ticket_field_row(label: String, value: String) -> List { - if value == "" { - [] - } else { - [ el_class("div", "ticket-field", concat( - [ el_class("span", "ticket-field-label", [ txt(label) ]) ], - [ el_class("span", "ticket-field-value", inline_md_nodes(text: value)) ])) ] - } +fn disclosure_field_row(f: DisclosureField) -> Fragment { + el_class("div", "ticket-field", concat( + [ el_class("span", "ticket-field-label", [ txt(f.label) ]) ], + [ el_class("span", "ticket-field-value", inline_md_nodes(text: f.value)) ])) } -fn ticket_update_row(u: TicketUpdate) -> Fragment { +fn disclosure_update_row(u: DisclosureNote) -> Fragment { el_class("div", "ticket-update", concat( [ el_class("span", "ticket-update-date", [ txt(u.on) ]) ], [ el_class("span", "ticket-update-note", inline_md_nodes(text: u.note)) ])) } -fn ticket_updates_block(updates: List) -> List { +fn disclosure_updates_block(updates: List) -> List { if count(updates) == 0 { [] } else { [ el_class("div", "ticket-updates", concat( [ el_class("div", "ticket-updates-head", [ txt("updates") ]) ], - map(updates, u => ticket_update_row(u: u)))) ] + map(updates, u => disclosure_update_row(u: u)))) ] } } -fn ticket_body_nodes(t: TicketFields, carriers: List) -> List { - concat( - [ el_class("p", "ticket-brief", inline_md_nodes(text: t.brief)) ], - concat( - concat(ticket_field_row(label: "displaced cost", value: t.displaced_cost), - concat(ticket_field_row(label: "current state", value: t.current_state), - concat(ticket_field_row(label: "first slice", value: t.first_slice), - concat(ticket_field_row(label: "red control", value: t.red_control), - concat(ticket_field_row(label: "out of scope", value: t.out_of_scope), - ticket_field_row(label: "hand-back", value: t.handback)))))), +fn disclosure_body_nodes(d: DisclosureView) -> List { + match d { + NoDisclosure => [] + TicketDisclosure { brief, fields, updates } => concat( - ticket_field_row(label: "verified", value: join([t.authored_on, " · last verified ", t.last_verified_on], "")), - ticket_updates_block(updates: t.updates)))) + [ el_class("p", "ticket-brief", inline_md_nodes(text: brief)) ], + concat( + map(fields, f => disclosure_field_row(f: f)), + disclosure_updates_block(updates: updates))) + LegacyBodyDisclosure { content } => + markdown_inlines_to_fragments(inlines: parse_inline_markdown_minimal(text: content)) + } } -fn item_body_nodes(rn: RoadmapNode) -> List { - match line_ticket(item: rn.line) { - Present { value: t } => ticket_body_nodes(t: t, carriers: rn.carriers) - Absent => title_nodes(line: rn.line) +data item_facts_disclosure_note: String = "Supporting facts live in the disclosure, not the head row (A1 of the presentation recon, operator-signed 2026-08-01): the head line had grown to ~7 simultaneous inline facts — owner, sizing codes, carrier links, dependencies — wrapping arbitrarily under the title, which is the half-console-half-product voice fork the recon named. The head carries the row's PRIMARY facts only plus the actuator; owner/sizing/carriers are supporting facts behind the same disclosure that already held the body. A row with supporting facts but no body still discloses (summary label 'facts'), so no fact is dropped — only demoted. AT A2 this renderer reads the DECIDED view alone: supporting facts arrive as WorkRowView.supporting_facts rows (label/value/href), the body as WorkRowView.disclosure — the RoadmapNode/TicketFields reads that used to live here moved behind the seam." + +fn supporting_fact_node(f: SupportingFact) -> Fragment { + if f.href == "" { + el_class("span", f.label, [ txt(f.value) ]) + } else { + el_attrs("a", [ attr(name: "href", value: join([github_blob_base, f.href], "") ) ], [ txt(f.value) ]) } } -fn item_is_ticket(rn: RoadmapNode) -> Bool { - match line_ticket(item: rn.line) { - Present { value: _ } => true - Absent => false +fn view_supporting_facts(view: WorkRowView) -> List { + if count(view.supporting_facts) == 0 { + [] + } else { + [ el_class("div", "item-facts", map(view.supporting_facts, f => supporting_fact_node(f: f))) ] } } -fn item_detail_block(rn: RoadmapNode) -> List { - if line_lead(line: rn.line) == line_title(line: rn.line) { +fn view_detail_block(view: WorkRowView) -> List { + let facts = view_supporting_facts(view: view) + let body = disclosure_body_nodes(d: view.disclosure) + if count(facts) == 0 && count(body) == 0 { [] } else { - let is_ticket = item_is_ticket(rn: rn) + let is_ticket = match view.disclosure { + TicketDisclosure { brief: _, fields: _, updates: _ } => true + NoDisclosure => false + LegacyBodyDisclosure { content: _ } => false + } + let label = if is_ticket { "ticket" } else { if count(body) == 0 { "facts" } else { "brief" } } [ el_attrs("details", [ attr(name: "class", value: if is_ticket { "item-detail ticket-row" } else { "item-detail" } ), ], [ el("summary", [ el_class("span", disclosure_mark_class, [ txt(disclosure_mark_glyph) ]), - txt(if is_ticket { "ticket" } else { "brief" }), + txt(label), ]), - el_class("div", "item-body", item_body_nodes(rn: rn)), + el_class("div", "item-body", concat(facts, body)), ]) ] } } @@ -315,17 +280,6 @@ data github_blob_base_note: String = "The per-surface link projection (remodel W data github_blob_base: String = "https://github.com/gunb-ai/gunbc/blob/main/" -fn pointer_link(p: Pointer) -> Fragment { - Element { - tag: "a", - attributes: [ attr(name: "href", value: join([github_blob_base, p.path], "") ) ], - children: [ txt(p.label) ], - } -} - -fn carrier_links(carriers: List) -> List { - map(carriers, p => pointer_link(p: p)) -} fn children_of(nodes: List, edges: List, parent: RoadmapNodeId) -> List { nodes |> filter(n => match roadmap_primary_parent(edges: edges, node: n.node) { @@ -334,108 +288,42 @@ fn children_of(nodes: List, edges: List, parent: Roadm }) } -fn owner_badge(rn: RoadmapNode) -> List { - if rn.owner == "" { - [] - } else { - [ el_class("span", "owner", [ txt(concat("@", rn.owner)) ]) ] - } -} - -fn sizing_badge(rn: RoadmapNode) -> List { - let label = sizing_label(s: rn.sizing) - if label == "" { - [] - } else { - [ el_class("span", "sizing", [ txt(label) ]) ] - } -} data dispatch_button_note: String = "The launch button (operator 2026-07-20: manual click-to-dispatch). Rendered on every open or review node; only signoff-Done and authored Superseded rows hide it. This deliberately preserves manual out-of-order dispatch for blocked and unsized rows: spawn_frontier.ready governs automatic belt reconciliation and page grouping, not the manual POST admission contract. belt_dispatch_node currently refuses signoff-Done but does not consult Ready, so the page must not claim otherwise. The button carries data-node = the node id. Its client behavior is a MODELED TsProgram (gunbc.roadmap_component.dispatch_client_program), emitted as an EXTERNAL served asset (roadmap_dispatch_asset_path, served by belt B) and referenced by