diff --git a/ROADMAP.md b/ROADMAP.md index e08148ee658..7f190346c40 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -10,10 +10,14 @@ Parked context (non-dispatchable): [compiler algorithm survey](docs/plans/compil The graph has nine lanes: SCM prerequisites · 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. The three SCM R0 models and P−1 evidence carrier are independent roots; compatibility extraction and later SCM/product lanes remain parked. 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 → v2 emitter exit lane.** 32 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. Hidden lanes remain readable through their carriers: [docs/plans/dag-scm-design.md](docs/plans/dag-scm-design.md) · [docs/plans/namespace-unique-on-chain-operational-plan.md](docs/plans/namespace-unique-on-chain-operational-plan.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/ci-two-tier-placement-redesign.md](docs/plans/ci-two-tier-placement-redesign.md) · [dag/gunbc/v1_deletion_plan.dag](dag/gunbc/v1_deletion_plan.dag) · [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). +**Focused view — the v1 deletion lane.** 27 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. 5 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, v1-seed-honesty-decision, namespace-legacy-choice-retirement, namespace-v2-import-completion, namespace-bound-only-consumers. 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/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). +- [ ] **Have exactly one rule decide what every name refers to** — Work out the candidates by looking outward from where the name was written, then call one shared decision function once. Not several mechanisms racing to answer. 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. [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) - [ ] **Write down which extra type requirements each generated shape needs** — For each shape we generate Rust for, record which extra requirements each of its type slots needs. 'No requirements' and 'we never said' stay different answers. Why: The generator currently guesses these, so generated Rust fails to compile in ways that look like a dozen unrelated bugs. [authority](docs/plans/v2-self-hosting.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) + - [ ] **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) — requires all: pderive-static-supplemental-contract, namespace-canonical-binding - [ ] **Generate those requirements from one place in the generator** — The Rust generator reads the record and the mapping through its existing path, rather than needing a special case per shape. Why: Every shape currently needing its own hand-written patch is a shape someone has to fix again next time. [authority](docs/plans/v2-self-hosting.md) - [ ] **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 new compiler to rebuild itself, twice, and still work** — The new compiler generates its own source, that source is built, and the result generates the same behaviour again. Behaviour has to match; the bytes do not. Why: This is the whole point of the project: until the new compiler can build itself, there is nothing to replace the old one with. [authority](dag/gunbc/v1_deletion_plan.dag) @@ -21,11 +25,12 @@ The graph has nine lanes: SCM prerequisites · namespace · P-derive · observat - [ ] **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) - [ ] **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, caret-parse-smoke-seed-growth-justification, v1-test-migration + - [ ] **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, caret-parse-smoke-seed-growth-justification, v1-test-migration, v1-seed-honesty-decision, namespace-legacy-choice-retirement - [ ] **Move one hand-written test check into the substrate and delete the original** — Re-express the check, prove it behaves identically, then delete the hand-written version rather than keeping both. Why: Keeping both means two things to fix whenever the rule changes, and they will drift. [authority](docs/plans/v2-self-hosting.md) - [ ] **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) - [ ] **Compute things once and reuse the result** — Tests and the compiler share one store keyed by what went into the result, with a size limit and a clear answer when something is missing. Why: Everything is currently recomputed from scratch on every run, and the one-off caches that exist do not help each other. [authority](docs/plans/witness-realization-plan.md) - [ ] **Prove old and new agree, by running them, not by waiting** — Run the whole test suite cold on both, compare compile results, check the self-rebuild, and record each family's outcome. Why: Confidence that comes from 'it has been fine for a while' is not evidence; this replaces it with runs someone chose to do. [authority](dag/gunbc/v1_deletion_plan.dag) — requires all: v1-materialization-kernel, v1-emitter-fixed-point - [ ] **Work through the remaining hand-written files the products need** — Move the hand-written files the shipping tools still depend on. Anything that genuinely has to stay gets an explicit reason. Why: The main command-line tool is itself one of these files, so 'the products still build' is not something we can honestly claim until this is done. [authority](dag/gunbc/v1_deletion_plan.dag) - [ ] **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) +- [ ] **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) - [ ] **Regenerate or delete one leftover hand-written parser test** — Exactly one hand-written file is kept for now: src/v1/stage0/tests/caret_parse_smoke.rs. This adds no new test behaviour and no second hand-written file. Why: Why it is still here: This old hand-written parser test predates a later change that added fields to a structure it builds. It was updated only enough to keep compiling — no new behaviour — but it is still a hand-written Rust file, so it needs to be counted rather than forgotten. [authority](dag/gunbc/v1_deletion_plan.dag) diff --git a/dag/gunbc/ci_layer_roots.dag b/dag/gunbc/ci_layer_roots.dag index 01e79a695bd..7600078537f 100644 --- a/dag/gunbc/ci_layer_roots.dag +++ b/dag/gunbc/ci_layer_roots.dag @@ -100,6 +100,12 @@ data witness_exclusion_frontier: List = [ reason: "manual-test home at dir grain: fns run by local recipe only, never per-PR discovery", dissolve_on: "manual suites migrate under test/claim with declared consumers; row deletes" }, + WitnessExclusionRow { + pattern: "observation_raw_print_retirement_acceptance_test.dag", + classification: QuarantineProbeExpectRed, + 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: "glob_discovery.dag", classification: OfflineLocalRecipe, @@ -729,7 +735,8 @@ data known_red_probe_entries: List = [ probe_red(entry: "src/v2/test/claim/emit/logic_ground_truth_test.dag", f: "logic_join_truth_table"), 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: "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") ] fn known_red_probe_roster() -> List { diff --git a/dag/gunbc/roadmap_authority.dag b/dag/gunbc/roadmap_authority.dag index 281ec14872d..20a03e1961b 100644 --- a/dag/gunbc/roadmap_authority.dag +++ b/dag/gunbc/roadmap_authority.dag @@ -28,7 +28,12 @@ import gunbc.roadmap_model { section_group, section_prose, } -import gunbc.roadmap_execution_contract { ExecutionContractUnspecified } +import std.types { FilePath, NonEmptyStr } +import gunbc.roadmap_execution_contract { + ExecutionContractUnspecified, + WorkItemExecutionContract, + gunbc_claim_execution_contract, +} import gunbc.roadmap_focus { RoadmapFocus, NoFocus, @@ -69,7 +74,7 @@ fn signed(by: String, works: RedControlEvidence, scope_equivalent: HandbackEvide SignedOff { by: by, works: works, scope_equivalent: scope_equivalent, as_expected: as_expected } } -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." +data roadmap_execution_contract_frontier_note: String = "EXACTLY TWO nodes carry an execution contract, and they are a deliberate PAIR rather than two independent bindings: shell-typed-invocation names an already-green validation and proves the Verify path executes; observation-raw-print-retirement names one that fails today and proves it discriminates. Either alone is a half-claim — a green-only frontier cannot tell a working verifier from an unconditional one, and a red-only frontier never demonstrates the success path — so the count reaching two is the point at which Verify is actually evidenced in both directions. Every other node 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, which left the frontier at ZERO bound nodes — dispatch refused on the entire roadmap and nothing below Env had ever executed end to end. This row records that the count is now one, and it is a COUNT, not a completion: binding remains per-node roadmap authoring, never a merge-time default, and the frontier closes one deliberate node at a time." fn authored(id: String, done: Bool, content: String) -> RoadmapNode { RoadmapNode { @@ -192,6 +197,38 @@ fn superseded(id: String, by: String, content: String) -> RoadmapNode { } } +data shell_typed_invocation_contract_note: String = "THE FIRST BOUND NODE, and it is bound to an ALREADY-GREEN validation on purpose. Until this row existed every node was ExecutionContractUnspecified, so dispatch_environment_admit refused on all of them and no part of the workflow below Env had ever executed end to end. A first binding therefore has two jobs that pull in opposite directions: prove the Verify path runs at all, and prove it discriminates. Those want different nodes, and conflating them is how a green pipeline gets mistaken for a working one. This row takes the FIRST job only — the validation it names passes today, so an Agent turn that changes nothing still produces a real admitted environment, a real derived command, and a real validation receipt. What that proves is the PLUMBING: capability admission, argv derivation, and receipt production, independent of whether the provider accomplished anything. It deliberately proves NOTHING about discrimination, and must not be read as evidence that Verify can fail — the discriminating red belongs on a second node whose validation is currently red and goes green when the work lands, and that node is the next binding, not this one.\n\nThe operation is the transport-clean corpus witness because it is what this ticket's own red control describes: 'a new unclassified production construction site reds the census'. It reads the live tree (SubstrateInputsOnly), so it is a statement about the repository rather than a fixture — a raw shell literal reappearing in that transport module reds it for real." + +fn shell_typed_invocation_execution_contract() -> WorkItemExecutionContract { + gunbc_claim_execution_contract( + entry: "src/v2/test/claim/host_language_transport_script/corpus/migrated_transports_clean_test.dag" as FilePath, + function: "corpus_dag_compile_clean_transport_clean_holds" as NonEmptyStr, + ) +} + +data observation_raw_print_retirement_contract_note: String = "THE DISCRIMINATING BINDING — the half shell_typed_invocation_contract_note deliberately does not supply. That one names an already-green validation and therefore proves only that the Verify path RUNS: admission, argv derivation, receipt production. It cannot distinguish a working verifier from one that returns green unconditionally, because nothing it runs is capable of returning anything else. A pipeline with only that binding is the coverage-by-illusion tier — machinery present, nothing gating on it.\n\nThis contract names a validation that FAILS today. observation_emit_frontier_count() is 8; the witness asserts 0, which is this node's handback stated exactly ('the classified zero-residue progress census'). So Verify has a target that can genuinely go red, the red is the ticket's own acceptance condition rather than a planted fixture, and the transition red -> green IS the work being finished. Together the two bindings pin both directions: one proves the path executes, the other proves it discriminates, and neither claim is available from the other.\n\nThe witness is enrolled QuarantineProbeExpectRed in gunbc.ci_layer_roots so a deliberately-red acceptance test does not red the corpus — the cadence built for precisely this, where greening is a counted un-quarantine event and not a silent pass." + +fn observation_raw_print_retirement_execution_contract() -> WorkItemExecutionContract { + gunbc_claim_execution_contract( + entry: "dag/test/claim/observation_raw_print_retirement_acceptance_test.dag" as FilePath, + function: "observation_emit_frontier_is_zero" 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." + +fn with_execution(rn: RoadmapNode, e: WorkItemExecutionContract) -> RoadmapNode { + RoadmapNode { + node: rn.node, + line: rn.line, + carriers: rn.carriers, + sizing: rn.sizing, + execution: e, + owner: rn.owner, + signoff: rn.signoff, + } +} + fn sign(rn: RoadmapNode, s: Signoff) -> RoadmapNode { RoadmapNode { node: rn.node, @@ -623,7 +660,9 @@ fn declared_roadmap_nodes() -> List { handback: "All three behaviours demonstrated." ) ), - active_small( + with_execution( + e: observation_raw_print_retirement_execution_contract(), + rn: active_small( id: "observation-raw-print-retirement", owner: "ci-observation", path: "docs/plans/progress-observation-design.md", @@ -636,7 +675,7 @@ fn declared_roadmap_nodes() -> List { out_of_scope: "A program's actual output — the thing you asked for — is not progress and stays as it is.", handback: "The inventory at zero unclassified." ) - ), + )), active_small( id: "placement-live-roster-preflight", @@ -884,7 +923,9 @@ fn declared_roadmap_nodes() -> List { ) ), - active_small( + with_execution( + e: shell_typed_invocation_execution_contract(), + rn: active_small( id: "shell-typed-invocation", owner: "shell", path: "docs/plans/shell-to-dag-residual-census-and-arc-completion.md", @@ -897,7 +938,7 @@ fn declared_roadmap_nodes() -> List { out_of_scope: "Genuinely shell-shaped leftovers stay in the shell layer.", handback: "The described version, proof it does the same thing, and the deleted string-building." ) - ), + )), active_large( id: "shell-effectplan-to-bash", owner: "shell", @@ -1219,7 +1260,7 @@ fn preamble() -> List { data roadmap_focus_selection_note: String = "THE ONE ROW THAT SETS THE VIEW. Focusing is a projection policy, not an edit — every node and edge stays declared in this file, the dispatch surface still sees the whole graph, and setting this back to NoFocus restores the full page with no recovery step. It is set to the v1 exit lane because that is the sustained effort under way (the v1 → v2 emitter exit plan); the other lanes remain real work, they are simply not on the page while this one is being ground out. The projection states how many it is hiding, derived rather than transcribed, so a focused page can never read as a shorter roadmap." data roadmap_focus_selection: RoadmapFocus = FocusOnOwners { - label: "the v1 → v2 emitter exit lane", + label: "the v1 deletion lane", owners: ["self-host"], } @@ -1249,6 +1290,7 @@ fn active_section_for(focus: RoadmapFocus) -> RoadmapSection { [section_prose(content: focus_notice( focus: focus, nodes: active_roadmap_nodes(), + edges: active_roadmap_edges(), ))] } else { [] @@ -1259,6 +1301,7 @@ fn active_section_for(focus: RoadmapFocus) -> RoadmapSection { nodes: focus_visible_nodes( focus: focus, nodes: active_roadmap_nodes(), + edges: active_roadmap_edges(), ), edges: focus_visible_edges( focus: focus, diff --git a/dag/gunbc/roadmap_focus.dag b/dag/gunbc/roadmap_focus.dag index 87bcdc4cbb8..51fcf221304 100644 --- a/dag/gunbc/roadmap_focus.dag +++ b/dag/gunbc/roadmap_focus.dag @@ -1,6 +1,7 @@ module gunbc.roadmap_focus import std.types { String, Bool, Int, List } +import std.graph { GraphEdge, CallGraph, graph_reverse_reachable_members } import gunbc.roadmap_model { RoadmapNode, RoadmapEdge } data roadmap_focus_note: String = "A VIEW policy over the roadmap, never an edit to it. When one lane is being worked, the other lanes are noise on the page — but deleting or parking them to get quiet would destroy work, and a roadmap that forgets a deliverable because someone wanted a shorter page is the worst possible failure for a queue. So focus filters the PROJECTION and leaves the authority whole: every node stays declared, every edge stays declared, and clearing the focus restores the full page with no recovery step. The mechanism is one row (roadmap_focus_selection) and reverting it is one edit.\n\nSelection is by OWNER rather than by a list of node ids. An id list would silently stop covering the lane the moment a new node joined it — the focus would look right and quietly understate, which is the same class of failure as a hand-maintained count. Owner is the lane's own identity and a new node in the lane inherits the focus by construction." @@ -25,13 +26,62 @@ fn focus_admits_node(focus: RoadmapFocus, n: RoadmapNode) -> Bool { focus_admits_owner(focus: focus, owner: n.owner) } -fn focus_visible_nodes( +data focus_prerequisite_closure_note: String = "A FOCUS MUST BE CLOSED UNDER PREREQUISITES, or it shows work that cannot start and does not say why. Owner membership alone is the wrong visibility rule: the v1 deletion lane's critical path runs pderive-source-slot-binding, whose prerequisite namespace-canonical-binding belongs to the namespace lane — so an owner-only filter rendered the fourth link of the spine as though it were ready, with its blocker invisible. An operator working that page would pick it up and discover the block by hitting it. Worse for an agent: a dispatched worker reads a ready-looking node, starts, and stalls on work the page never mentioned.\n\nVisibility is therefore lane membership UNIONED with the transitive prerequisite closure of the lane. The closure reuses std.graph.graph_reverse_reachable_members over the roadmap's own edge relation rather than minting a second walk — the roadmap edge (child, parent) is already a dependency edge, reverse reachability from a lane node is exactly its ancestors, and the graph is acyclic by witness so the walk terminates by construction. A blocker is pulled onto the page WITH its own prerequisites, recursively, so the visible subgraph is genuinely startable end to end.\n\nPulled-in nodes stay distinguishable from lane work in the notice. They are not deliverables of this lane; they are the reason part of it cannot move, and conflating the two would misreport the lane's size." + +fn focus_lane_nodes( focus: RoadmapFocus, nodes: List, ) -> List { nodes |> filter(n => focus_admits_node(focus: focus, n: n)) } +fn roadmap_dependency_graph(edges: List) -> CallGraph { + CallGraph { + edges: edges |> map(e => GraphEdge { + caller: e.parent as String, + callee: e.child as String, + }), + } +} + +fn focus_prerequisite_ids( + focus: RoadmapFocus, + nodes: List, + edges: List, +) -> List { + let names = nodes |> map(n => n.node as String) + let graph = roadmap_dependency_graph(edges: edges) + let lane_ids = focus_lane_nodes(focus: focus, nodes: nodes) |> map(n => n.node as String) + fold(lane_ids, init: [], f: fn(acc, id) { + fold( + graph_reverse_reachable_members(names: names, graph: graph, start: id), + init: acc, + f: fn(inner, anc) { + if any(lane_ids, l => l == anc) { + inner + } else { + append_distinct(acc: inner, p: anc) + } + }, + ) + }) +} + +fn focus_visible_nodes( + focus: RoadmapFocus, + nodes: List, + edges: List, +) -> List { + match focus { + NoFocus => nodes + FocusOnOwners { label: _, owners: _ } => + let pulled = focus_prerequisite_ids(focus: focus, nodes: nodes, edges: edges) + nodes |> filter(n => + focus_admits_node(focus: focus, n: n) + || any(pulled, p => p == (n.node as String))) + } +} + data focus_edge_containment_note: String = "An edge survives only when BOTH endpoints do. A dependency edge pointing at a hidden node would render as a prerequisite the reader cannot see and cannot act on — worse than omitting it, because it implies the page is complete while pointing off the edge of it. Dropping the edge keeps the visible subgraph internally consistent: everything named on the page is on the page. The cross-lane prerequisite is not lost, it is declared in the authority and returns the moment the focus clears, which is precisely the property that makes filtering safe here and unsafe as an edit." fn focus_visible_edges( @@ -39,7 +89,7 @@ fn focus_visible_edges( nodes: List, edges: List, ) -> List { - let visible = focus_visible_nodes(focus: focus, nodes: nodes) + let visible = focus_visible_nodes(focus: focus, nodes: nodes, edges: edges) edges |> filter(e => any(visible, n => (n.node as String) == (e.child as String)) && any(visible, n => (n.node as String) == (e.parent as String))) @@ -48,8 +98,9 @@ fn focus_visible_edges( fn focus_hidden_node_count( focus: RoadmapFocus, nodes: List, + edges: List, ) -> Int { - count(nodes) - count(focus_visible_nodes(focus: focus, nodes: nodes)) + count(nodes) - count(focus_visible_nodes(focus: focus, nodes: nodes, edges: edges)) } fn focus_label(focus: RoadmapFocus) -> String { @@ -71,8 +122,10 @@ data focus_carrier_retention_note: String = "HIDING A NODE MUST NOT ORPHAN ITS D fn focus_hidden_nodes( focus: RoadmapFocus, nodes: List, + edges: List, ) -> List { - nodes |> filter(n => !focus_admits_node(focus: focus, n: n)) + let visible = focus_visible_nodes(focus: focus, nodes: nodes, edges: edges) + nodes |> filter(n => !any(visible, v => (v.node as String) == (n.node as String))) } fn append_distinct(acc: List, p: String) -> List { @@ -82,9 +135,10 @@ fn append_distinct(acc: List, p: String) -> List { fn focus_hidden_carrier_paths( focus: RoadmapFocus, nodes: List, + edges: List, ) -> List { fold( - focus_hidden_nodes(focus: focus, nodes: nodes), + focus_hidden_nodes(focus: focus, nodes: nodes, edges: edges), init: [], f: fn(acc, n) { fold(n.carriers, init: acc, f: fn(inner, p) { @@ -97,17 +151,41 @@ fn focus_hidden_carrier_paths( fn focus_hidden_carrier_links( focus: RoadmapFocus, nodes: List, + edges: List, ) -> String { join( map( - focus_hidden_carrier_paths(focus: focus, nodes: nodes), + focus_hidden_carrier_paths(focus: focus, nodes: nodes, edges: edges), p => join(["[", p, "](", p, ")"], ""), ), " · ", ) } -fn focus_notice(focus: RoadmapFocus, nodes: List) -> String { +fn focus_pulled_in_summary( + focus: RoadmapFocus, + nodes: List, + edges: List, +) -> String { + let pulled = focus_prerequisite_ids(focus: focus, nodes: nodes, edges: edges) + if count(pulled) == 0 { + "" + } else { + join([ + " ", + to_string(count(pulled)), + " of the rows below are NOT lane deliverables — they are prerequisites pulled in from other lanes because lane work is blocked on them: ", + join(pulled, ", "), + ".", + ], "") + } +} + +fn focus_notice( + focus: RoadmapFocus, + nodes: List, + edges: List, +) -> String { match focus { NoFocus => "" FocusOnOwners { label, owners: _ } => @@ -115,9 +193,11 @@ fn focus_notice(focus: RoadmapFocus, nodes: List) -> String { "**Focused view — ", label, ".** ", - to_string(focus_hidden_node_count(focus: focus, nodes: nodes)), - " 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. Hidden lanes remain readable through their carriers: ", - focus_hidden_carrier_links(focus: focus, nodes: nodes), + to_string(focus_hidden_node_count(focus: focus, nodes: nodes, edges: edges)), + " 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.", + focus_pulled_in_summary(focus: focus, nodes: nodes, edges: edges), + " Hidden lanes remain readable through their carriers: ", + focus_hidden_carrier_links(focus: focus, nodes: nodes, edges: edges), ".", ], "") } diff --git a/dag/gunbc/roadmap_identity.dag b/dag/gunbc/roadmap_identity.dag new file mode 100644 index 00000000000..835c9e448e5 --- /dev/null +++ b/dag/gunbc/roadmap_identity.dag @@ -0,0 +1,265 @@ +module gunbc.roadmap_identity + +import std.types { String, NonEmptyStr, Bool, Int, List } + +data roadmap_identity_law: String = "A roadmap node's identity is MINTED, never derived. Slug, title, criteria digest, containment position, dependency neighbourhood, declaration order, and file position are FORBIDDEN identity inputs — the same law std.occurrence_identity states for occurrences, generalized to an entity that must outlive the graph it is read from.\n\nThe law is reused; the TYPE deliberately is not. std.occurrence_identity.OccurrenceId is an ordinal from a per-graph allocator that starts at zero (OccurrenceIdAllocator { next_id: 0 }, alloc returns next_id then increments), so it is unique only INSIDE one constructed graph. Reconstructing the roadmap with a node inserted at the front renumbers every node after it, and two concurrent branches both mint the same next integer. That is positional identity wearing an allocator's clothes: correct for occurrences within one parse, wrong for a work item that must survive edits, reordering, regeneration, and independent branches. What occurrence identity deliberately does not claim is PERSISTENCE SCOPE, and that is exactly what a roadmap node needs.\n\nThe minted value is therefore collision-resistant rather than sequential, and it is persisted in the authority at authoring time. Nothing mints during rendering, compilation, or regeneration — a value that could be re-derived by evaluation would reintroduce the property this module exists to remove." + +data roadmap_identity_five_axes_note: String = "Five things the roadmap must keep separate, because collapsing any pair is what makes replanning destructive: IDENTITY (which work item this is — never changes); SLUG (what it is currently called — may change); CRITERIA DIGEST (which acceptance bar applies — changes, and correctly invalidates a stale receipt); CONTAINMENT (what decomposes it); DEPENDENCY (what must precede it). Today identity and slug are the same value, so a rename is a delete plus a create: edges stop resolving, acceptance receipts name a node that no longer exists, and any dispatch attempt or session joined to it is orphaned. That is not hypothetical — the 2026-07-27 authority refresh deleted the node carrying the only execution contract and no claim noticed. Separating the two makes a rename a label edit and nothing more.\n\nCriteria digest stays a content hash and that is correct: it is not identifying the node, it is fingerprinting the bar so an old acceptance cannot silently survive a changed one. Content-addressing is the right tool for revision and the forbidden tool for identity; the roadmap needs both, keyed differently." + +type RoadmapNodeIdentity { + value: NonEmptyStr +} + +type RoadmapNodeSlug { + value: NonEmptyStr +} + +type RoadmapIdentityBinding { + identity: RoadmapNodeIdentity + slug: RoadmapNodeSlug +} + +data roadmap_identity_additive_stage_note: String = "STAGE I0, deliberately additive. This module introduces the identity space and binds one persisted value to every declared node; it changes no rendering, no dependency edge, no acceptance receipt and no dispatch path. The v1-deletion recut can therefore be authored against durable identities immediately, without the recut waiting on a full consumer migration — and without every new node and edge being rewritten a second time once the migration lands. Consumers move in later stages: edges and receipts, then explicit containment, then runtime/dispatch, then the slug-as-identity constructors delete." + +fn roadmap_identity_bindings() -> List { + [ + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_G9B7NZQFBN04NWCEC7VYDP8SDD" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "2-scm-git-upstream-model" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_WFHJ3P2RB33BDPBEV8EAZ8FRK0" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "2-scm-mercurial-upstream-model" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_F9285EGNWE01HBE0RRJNKGJQHN" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "2-scm-pijul-upstream-model" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_YFCNE6Y2CM3SMF3X34Y7QZ6C4T" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "2-claim-indexed-evidence" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_NCRFN1DR7WVGFKPHAW2ECHJY7F" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "namespace-occurrence-transport" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_K7PKNE9BSC7WNMZDE04BBZ50J9" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "namespace-canonical-binding" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_KMQ1T4H8MYS0S5ZXJDF64BPG4A" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "namespace-bound-only-consumers" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_CRFH5GPMK20QTABFBCPQG1HJXA" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "namespace-v2-import-completion" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_N69YATD37CE2PGGD9TDKJTFEDR" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "namespace-legacy-choice-retirement" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_04A8MQDYC58QF2K858FD14X6MT" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "pderive-typesafe-nullary-reflection" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_GP4QV1630JT574FMR6BGA5HN73" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "pderive-static-supplemental-contract" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_DP7AEAB5Y0XEDWR8W3JX0D871P" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "pderive-source-slot-binding" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_D05JH2NHVC67FJBE7M27P34BV2" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "pderive-single-emitter" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_KACQCKH4S1EW8E4HG488R4YNEG" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "pderive-error-movement-receipt" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_5XPYVCC6SXKE0597XC7KG57CSW" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "observation-scoped-run-seed-growth-justification" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_VP16AJH7CZNRKR82G3PK2TN2XP" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "observation-scoped-run-consumer" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_7W523G7WMCVWQYJ0X9XTDT9XJ5" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "observation-ci-event-grouping" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_PTX9F8JYFQVZHPKXX4651WZ694" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "observation-collapse-watchdog-restatement" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_8YTWD194C1E8C2S2P0TSGSMDZ3" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "observation-raw-print-retirement" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_E81RKJKRAED4VJGFYQDR0Y4EFC" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "placement-live-roster-preflight" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_BYKWQJ1ZBTSFF3PDXPKBB61SNG" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "placement-two-host-warm-qualification" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_RY7HZH0CVR7AMFWTHDGZZVTP7H" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "placement-shadow-equality" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_7255F6D2Z6EV70HX53F9PH35WY" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "placement-atomic-authority-flip" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_0WVWDY6ET9GW1R8KSKTC7G0CQF" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-test-hygiene-producer-retirement" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_GCTJXB2DWA9CYV4RYPDTM76X60" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-emitter-fixed-point" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_QVDHKA5W6HCH9EDFQAZTFHDX84" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-honest-frontier" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_JMGB6EBAG399MJ3FTGH5AJYVKB" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-materialization-kernel" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_CN3156CD7F5XDYA7W9HFK0J2AE" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-verification-ledger" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_WFVQ6W096KRCS1YQS6STXZGGX2" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-ci-floor-cutover" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_RKJRESX9R04B8Q9R278ED1361H" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-hand-queue-drain" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_CHKAM7B6RKAM3EP0M8WTADHSEZ" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-test-migration" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_B9C9KXNPKC55HJR7ZA4J1MFNP7" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-seed-honesty-decision" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_GMWNBP7JP5741MVM7YH32P5T47" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-interpreter-quarantine" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_T0AQV2QH2D3MZ4VFZKJ270A5CA" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-interpreter-delete" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_SHNCBR5QEP2WV7MPEZXB4WNX6H" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-products-v1-free" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_5J7SPTY2T4QHP1CXH12JSNPCHS" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "caret-parse-smoke-seed-growth-justification" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_K3XYM8GVDVQWBW25M3CB9STATB" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "v1-zero-hand-maintained-rust" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_9ES87AKY6FB21JF2HC2MCVYTDS" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "shell-typed-invocation" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_XHHXTJAKJ7XD46AH1MK7BZHW2D" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "shell-effectplan-to-bash" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_J1GWN2EWJ0YBHN4D5A4GD52R4M" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "shell-gate-migration" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_G08YK92MMFGSEH7GC8M51FV75A" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "shell-residue-zero" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_QGZX2Z2QB4P2H28DMRJBHDTC58" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "roadmap-runtime-identity-join" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_E5H7PYHXS4QCTDQGJAEQKR31RB" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "roadmap-runtime-ready-override" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_E3QBEXX1GWDRD1M00AP2VD2G7T" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "roadmap-runtime-dispatch-consumer" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_DXG5ZWBG8E2ZMY6WN6G35DJ26Q" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "roadmap-runtime-readback" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_C85XCYNFVZJM6T45NSEP0RWYVJ" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "fleet-state-frontier" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_NNTPHE3XT0HHEN5BJPVNH7EZVA" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "fleet-bmc-policy-convergence" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_62E7JDB5J1VAQ7H0EB4P0DB669" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "fleet-closed-loop-converge" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_DPAGSD6NGS51D2CY04GRJ9X1B9" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "fleet-dashboard-instance-membership" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_VBQCVP1ZZKBPCET9SZSAHYK1ZB" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "fleet-anti-entropy-hygiene" as NonEmptyStr }, + }, + RoadmapIdentityBinding { + identity: RoadmapNodeIdentity { value: "rn_X2AKFARNE3QFHRT8PPKZX38C4H" as NonEmptyStr }, + slug: RoadmapNodeSlug { value: "fleet-receipt-qualified-control" as NonEmptyStr }, + }, + ] +} + +fn identity_for_slug(slug: String) -> RoadmapNodeIdentity? { + roadmap_identity_bindings() + |> filter(b => (b.slug.value as String) == slug) + |> map(b => b.identity) + |> first +} + +fn slug_for_identity(identity: String) -> RoadmapNodeSlug? { + roadmap_identity_bindings() + |> filter(b => (b.identity.value as String) == identity) + |> map(b => b.slug) + |> first +} + +fn identity_count() -> Int { count(roadmap_identity_bindings()) } + +fn distinct_identity_count() -> Int { + count(fold(roadmap_identity_bindings(), init: [], f: fn(acc, b) { + let v = b.identity.value as String + if any(acc, e => e == v) { acc } else { append(acc, [v]) } + })) +} + +fn distinct_slug_count() -> Int { + count(fold(roadmap_identity_bindings(), init: [], f: fn(acc, b) { + let v = b.slug.value as String + if any(acc, e => e == v) { acc } else { append(acc, [v]) } + })) +} + +data roadmap_identity_permanence_gate_owed: String = "OWED, NOT DONE — the mechanical check this stage cannot supply. Every claim in roadmap_identity_witness_test is a statement about the CURRENT tree: identities unique, slugs unique, binding total, resolution invertible. None of them can see history, and permanence is a historical property — an identity silently replaced between the base revision and a PR head produces a tree that satisfies every one of those claims. The snapshot corpus is structurally incapable of catching the exact edit this module exists to forbid, and saying so here is the difference between a declared gap and coverage by illusion.\n\nThe missing mechanism is a base-versus-head comparison of the identity registry, which the CI floor already has the shape for (the merge-base diff observation the affected-set selection reads). ADMITTED transitions: an identity retained; a new identity added; an identity explicitly tombstoned; a slug changed while its identity is unchanged; containment or dependency changed while identity is unchanged. REFUSED: an existing identity replaced; a retired identity reused; two nodes sharing one identity; one active slug resolving to two identities; a parent identity dropped rather than retained when it is decomposed.\n\nUntil that gate exists, permanence rests on review rather than construction, and this row is what keeps that fact countable instead of forgotten." + +data roadmap_identity_decomposition_policy_owed: String = "OWED — decomposition needs an explicit completion policy, and containment alone does not supply one. Keeping a parent's identity when it is split is what stops receipts and dependency edges dangling, but it leaves a question containment cannot answer: is the parent complete when it is directly accepted, when all its contained children are accepted, or both. Inferring that from the containment relation would be a guess dressed as a derivation, and the wrong guess silently closes a parent whose children are unfinished — the exact goal-shrink failure the workflow model already refuses elsewhere.\n\nSo the safe decomposition sequence is: retain the parent identity; leave external dependencies and receipts pointed at it; mint identities for the children; add containment edges; state the parent's aggregate completion policy EXPLICITLY; and add child dependencies only where execution order genuinely requires them. The policy is a declared field, never an inference. This lands with explicit containment, which is a later stage than I0." diff --git a/dag/gunbc/roadmap_provider_events.dag b/dag/gunbc/roadmap_provider_events.dag index 8b10cf48acb..395ff01a6e0 100644 --- a/dag/gunbc/roadmap_provider_events.dag +++ b/dag/gunbc/roadmap_provider_events.dag @@ -74,6 +74,12 @@ data codex_provider_event_line_observation_budget: CharacterCount = character_co data codex_provider_projection_type_budget: CharacterCount = character_count(count: 128) data codex_provider_projection_detail_budget: CharacterCount = character_count(count: 4096) +data codex_provider_projection_message_budget_note: String = "The agent's REPORT gets its own budget rather than reusing the detail budget beside it, because the two bound different things for different readers. A detail is a diagnostic — a terminal error's message, read to find out what broke, and short by nature. A report is what the provider says it DID, and it is the text an operator reads to judge whether the work is any good; it is the closest thing the dashboard has to the agent's answer. Sharing one number would mean a future tuning of either bound silently retunes the other, which is the coincidence-not-concept trap DESIGN §2 warns about — they are the same TYPE (CharacterCount, the same Measure axis) and genuinely different FACTS.\n\n8192 is chosen against the incident this whole projection exists to prevent: a single item.completed carrying ~154KB of aggregated command output, which killed the old 64KB line reader. A report is bounded an order of magnitude under the 65536 line budget, so it cannot be the payload that reproduces that failure, while still being generous enough that a substantial multi-paragraph summary arrives whole and truncation stays the exception rather than the norm. It is deliberately larger than the 4096 detail budget: an error message that needs 4KB is pathological, a work summary that needs 6KB is ordinary." + +data codex_provider_projection_message_budget: CharacterCount = character_count(count: 8192) + +data codex_provider_message_truncation_note: String = "TRUNCATION IS REPORTED, NEVER SILENT — the one place this projection could otherwise fabricate a plausible output (DESIGN §5). Everywhere else the bounds guard a machine-read classification, where a cut string simply fails to match and refuses; here the bounded value is PROSE AN OPERATOR READS AND ACTS ON, so a report severed mid-sentence and presented as if complete is a wrong answer delivered confidently — the operator would judge the work on a fragment while the screen gave no sign anything was missing. The projected envelope therefore carries the boundedness as its own member — a `message` object pairing the bounded text with a `truncated` flag — where truncated is computed from the ORIGINAL length before slicing, so it is a statement about the provider's actual output rather than about the slice. A consumer that renders text without consulting truncated is re-opening the arm this row closes, which is why the flag is a sibling of the text and not a derived guess about whether the slice happens to sit on the budget." + data codex_provider_event_projection_filter: NonEmptyStr = join([ "fromjson | def event_type: if (.type | type) == \"string\" then .type[0:", to_string(character_count_value(c: codex_provider_projection_type_budget)), @@ -85,7 +91,11 @@ data codex_provider_event_projection_filter: NonEmptyStr = join([ to_string(character_count_value(c: codex_provider_projection_detail_budget)), "] else tostring[0:", to_string(character_count_value(c: codex_provider_projection_detail_budget)), - "] end; (event_type) as $type | if $type == \"item.started\" or $type == \"item.completed\" then {\"type\":$type,\"item\":{\"type\":item_type}} elif $type == \"turn.failed\" then {\"type\":$type,\"error\":{\"message\":(.error | bounded_text)}} elif $type == \"error\" then {\"type\":$type,\"message\":((.message // .error) | bounded_text)} else {\"type\":$type} end", + "] end; def agent_report: if (.item.text | type) == \"string\" then {\"text\":(.item.text[0:", + to_string(character_count_value(c: codex_provider_projection_message_budget)), + "]),\"truncated\":((.item.text | length) > ", + to_string(character_count_value(c: codex_provider_projection_message_budget)), + ")} else null end; (event_type) as $type | if $type == \"item.started\" or $type == \"item.completed\" then (if item_type == \"agent_message\" and (agent_report != null) then {\"type\":$type,\"item\":{\"type\":item_type,\"message\":agent_report}} else {\"type\":$type,\"item\":{\"type\":item_type}} end) elif $type == \"turn.failed\" then {\"type\":$type,\"error\":{\"message\":(.error | bounded_text)}} elif $type == \"error\" then {\"type\":$type,\"message\":((.message // .error) | bounded_text)} else {\"type\":$type} end", ], "") as NonEmptyStr fn codex_provider_event_projection_args(path: FilePath) -> List { @@ -103,7 +113,7 @@ data codex_projection_filter_scaffold_note: String = "DECLARED SCAFFOLD (review data codex_jsonl_classifier_note: String = "Codex --json is the provider protocol authority: one JSON object per line with thread.started, turn.started, item.*, turn.completed, turn.failed, and error event types. The raw stream is structurally reduced by codex_provider_event_projection_filter before this bounded total envelope classifier runs. The shared RFC 8259 grammar proves each projected line parseable, then the classifier requires the emitted top-level `type` member to be the first object member before selecting a closed event variant. That fail-closed envelope rule prevents a nested item or user-authored string containing `type: turn.completed` from fabricating terminal success. Projected lines are mechanically bounded; the remaining line budget is a red control against projector drift and refuses before grammar descent with the payload elided. Unknown, reordered, malformed, or truncated projected lines still refuse with their located evidence and parsed prefix. A refusal is a workflow fact, never dropped as console noise. Item activity is only a presentation projection from the bounded item type; terminal success/failure comes only from the anchored top-level turn.completed, turn.failed, or error envelope, never from tmux Spawned." -data codex_projected_envelope_walk_note: String = "review 44058. Both reads below are ANCHORED walks over the projector's own envelope shape, not scans. codex_line_contains_type_token, which this replaces, searched the whole line for a `\"type\":\"…\"` fragment in either spacing — so an agent_message whose text quoted `\"type\":\"file_change\"` reclassified the pane's activity, the same fragment-scan class the top-level classifier already refused to use for terminal events and the same one the dashboard health readback was repaired for. The projector emits exactly {\"type\":…} for plain events and {\"type\":…,\"item\":{\"type\":…}} for item events, so both members have a known position and the walk either lands on it or answers false. codex_envelope_member_value_start is the single step both readings share (DESIGN 2, one concept)." +data codex_projected_envelope_walk_note: String = "review 44058. Both reads below are ANCHORED walks over the projector's own envelope shape, not scans. codex_line_contains_type_token, which this replaces, searched the whole line for a `\"type\":\"…\"` fragment in either spacing — so an agent_message whose text quoted `\"type\":\"file_change\"` reclassified the pane's activity, the same fragment-scan class the top-level classifier already refused to use for terminal events and the same one the dashboard health readback was repaired for. The projector emits exactly {\"type\":…} for plain events and {\"type\":…,\"item\":{\"type\":…}} for item events — plus, on an agent_message item only, a \"message\" member SIBLING to and AFTER item.type carrying the bounded report and its truncation flag. The anchor is unaffected by that addition and that is why it was placed there: item.type remains item's FIRST member, so the walk's landing position is unchanged, and a report whose own text quotes an item-type fragment still cannot reclassify the pane because the walk never reaches it. Both anchored members have a known position and the walk either lands on it or answers false. codex_envelope_member_value_start is the single step both readings share (DESIGN 2, one concept)." fn codex_envelope_object_first_member(raw: String, at: Int) -> Int? { let start = json_text_skip_ws(s: raw, i: at) diff --git a/dag/test/claim/observation_raw_print_retirement_acceptance_test.dag b/dag/test/claim/observation_raw_print_retirement_acceptance_test.dag new file mode 100644 index 00000000000..5759f16a711 --- /dev/null +++ b/dag/test/claim/observation_raw_print_retirement_acceptance_test.dag @@ -0,0 +1,16 @@ +module test.claim.observation_raw_print_retirement_acceptance_test + +import std.types { Bool } +import std.nat { Nat } +import gunbc.observation_emit_census { observation_emit_frontier_count } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data acceptance_witness_note: String = "THE ACCEPTANCE TEST FOR observation-raw-print-retirement, and it is RED ON PURPOSE — enrolled QuarantineProbeExpectRed, which is the cadence for exactly this: a known-red row whose greening is a counted un-quarantine event. It is not a broken witness and it must not be 'fixed' by relaxing the assertion; the only edit that may turn it green is migrating the emit sites it counts.\n\nWhat it asserts is that node's own handback verbatim — 'the classified zero-residue progress census'. gunbc.observation_emit_census classifies every seed emit site as exactly one of MigratedToObservation or CountedFrontierSite (a closed sum, so a site cannot be half-classified), and observation_emit_frontier_count is the population of the second arm. Eight today: floor-drain, gate-warm-cost, receipt, file, resolve-split, assembly-split, witness-row-cost, witness-row-cost-drift. Zero is the ticket being finished, so the count IS the completion condition rather than a proxy for it, and every site migrated moves it monotonically toward green.\n\nWhy it exists as a separate row rather than an assertion inside observation_emit_census_witness_test: that suite states the count the census HAS (== 8, a fact about today that must red if the roster drifts), while this one states the count the WORK OWES (== 0). Both are true statements and they disagree by construction — putting them in one file would force the roster's own hygiene check into the same quarantine as an unfinished deliverable, which would stop the roster from reding when it should." + +data acceptance_dissolution_note: String = "DISSOLVES BY GREENING, not by deletion. When the frontier reaches zero this witness passes and its QuarantineProbeExpectRed row in gunbc.ci_layer_roots.witness_exclusion_frontier becomes wrong — a still-red expectation against a green witness — so the un-quarantine is forced rather than optional: the roster row must be removed in the same change that lands the last migration, and the witness then joins ordinary DiscoverySelection as a standing ratchet against regression. That is the counted un-quarantine event the admission authority describes." + +test fn observation_emit_frontier_is_zero() -> Bool { + observation_emit_frontier_count() == 0 +} diff --git a/dag/test/claim/roadmap_authority_test.dag b/dag/test/claim/roadmap_authority_test.dag index e8596cfbda5..e8a02cd37ad 100644 --- a/dag/test/claim/roadmap_authority_test.dag +++ b/dag/test/claim/roadmap_authority_test.dag @@ -25,7 +25,13 @@ import gunbc.v1_deletion_plan { v1_finish_line_key, v1_finish_lines_accepted_count, } -import gunbc.roadmap_execution_contract { ExecutionContractUnspecified } +import gunbc.roadmap_execution_contract { + ExecutionContractUnspecified, + ExecutionContract, + WorkItemExecutionContract, + dispatch_required_capabilities, + execution_capability_label, +} import gunbc.roadmap_document { project_roadmap_to_markdown } import gunbc.roadmap_model { RoadmapNode, @@ -532,3 +538,49 @@ test fn witness_projection_is_active_only() -> Bool { && !string_contains(s: out, pattern: "awaiting sign-off") && !string_contains(s: out, pattern: "Operator TODO") } + +data execution_frontier_witness_note: String = "The execution-contract frontier is a COUNT, and these claims pin both of its halves. Before the first binding the count was zero — dispatch_environment_admit refused on every node, so nothing below Env in the workflow had ever executed end to end and the whole seven-segment strip was unreachable in production. A binding is therefore the gate for every downstream receipt, and an unwitnessed one could regress to zero silently (the way it already did once, when the 2026-07-27 authority refresh deleted the node that carried the prior contract and no claim noticed). The first claim proves the bound node yields a real contract rather than the fail-closed variant; the second proves the platform/task capability JOIN actually happens at admission — 8 fixed dispatch mechanics plus the one capability this contract's validation derives — which is the fact that turns a contract into an admissible environment; the third is the frontier itself, and it is the RED-shaped one: it fails if a binding is lost AND it fails if a second node is bound without this roster being updated, so the count in roadmap_execution_contract_frontier_note cannot silently drift from the authority it describes." + +fn bound_execution_contract_for(id: String) -> WorkItemExecutionContract { + match nodes() + |> filter(n => (n.node as String) == id) + |> map(n => n.execution) + |> first { + Present { value: c } => c + Absent => ExecutionContractUnspecified + } +} + +test fn witness_shell_typed_invocation_carries_a_real_contract() -> Bool { + match bound_execution_contract_for(id: "shell-typed-invocation") { + ExecutionContractUnspecified => false + ExecutionContract { validations, extra_capabilities: _ } => count(validations) == 1 + } +} + +test fn witness_bound_contract_joins_platform_and_task_capabilities() -> Bool { + let caps = dispatch_required_capabilities( + contract: bound_execution_contract_for(id: "shell-typed-invocation"), + ) + count(caps) == 9 + && any(caps, c => execution_capability_label(capability: c) == "gunbc-claim-runner") +} + +test fn witness_execution_contract_frontier_is_exactly_two_nodes() -> Bool { + count(nodes() + |> filter(n => match n.execution { + ExecutionContractUnspecified => false + ExecutionContract { validations: _, extra_capabilities: _ } => true + })) == 2 +} + +test fn witness_bound_pair_covers_both_verify_directions() -> Bool { + match bound_execution_contract_for(id: "observation-raw-print-retirement") { + ExecutionContractUnspecified => false + ExecutionContract { validations, extra_capabilities: _ } => count(validations) == 1 + } + && match bound_execution_contract_for(id: "shell-typed-invocation") { + ExecutionContractUnspecified => false + ExecutionContract { validations, extra_capabilities: _ } => count(validations) == 1 + } +} diff --git a/dag/test/claim/roadmap_focus_witness_test.dag b/dag/test/claim/roadmap_focus_witness_test.dag index 01df1eea192..dc56576559a 100644 --- a/dag/test/claim/roadmap_focus_witness_test.dag +++ b/dag/test/claim/roadmap_focus_witness_test.dag @@ -14,6 +14,8 @@ import gunbc.roadmap_focus { focus_visible_nodes, focus_visible_edges, focus_hidden_node_count, + focus_lane_nodes, + focus_prerequisite_ids, focus_is_active, focus_notice, } @@ -26,19 +28,20 @@ data focus_witness_note: String = "Focus is a VIEW, so the property that matters fn all_nodes() -> List { active_roadmap_nodes() } fn visible() -> List { - focus_visible_nodes(focus: roadmap_focus_selection, nodes: all_nodes()) + focus_visible_nodes(focus: roadmap_focus_selection, nodes: all_nodes(), edges: active_roadmap_edges()) } test fn focus_partitions_rather_than_deletes() -> Bool { count(visible()) + focus_hidden_node_count( focus: roadmap_focus_selection, nodes: all_nodes(), + edges: active_roadmap_edges(), ) == count(all_nodes()) } test fn clearing_focus_restores_every_node() -> Bool { - count(focus_visible_nodes(focus: NoFocus, nodes: all_nodes())) == count(all_nodes()) - && focus_hidden_node_count(focus: NoFocus, nodes: all_nodes()) == 0 + count(focus_visible_nodes(focus: NoFocus, nodes: all_nodes(), edges: active_roadmap_edges())) == count(all_nodes()) + && focus_hidden_node_count(focus: NoFocus, nodes: all_nodes(), edges: active_roadmap_edges()) == 0 } test fn focus_actually_narrows_the_page() -> Bool { @@ -61,16 +64,40 @@ test fn every_visible_edge_has_both_endpoints_visible() -> Bool { } test fn focus_notice_states_the_derived_hidden_count() -> Bool { - let notice = focus_notice(focus: roadmap_focus_selection, nodes: all_nodes()) + let notice = focus_notice(focus: roadmap_focus_selection, nodes: all_nodes(), edges: active_roadmap_edges()) string_contains( s: notice, pattern: to_string(focus_hidden_node_count( focus: roadmap_focus_selection, nodes: all_nodes(), + edges: active_roadmap_edges(), )), ) && string_contains(s: notice, pattern: "Focused view") } test fn unfocused_projection_emits_no_notice() -> Bool { - focus_notice(focus: NoFocus, nodes: all_nodes()) == "" + focus_notice(focus: NoFocus, nodes: all_nodes(), edges: active_roadmap_edges()) == "" +} + +data focus_closure_witness_note: String = "THE CLAIM THAT WOULD HAVE CAUGHT THE ORIGINAL BUG. The first focus shipped owner-membership as the whole visibility rule, and the page it produced rendered pderive-source-slot-binding as ready while its prerequisite namespace-canonical-binding — a node in a lane the focus hid — was nowhere on it. Nothing was red; the page simply lied by omission, and the failure mode is worse for a dispatched agent than for a person: a worker reads a ready-looking node, starts, and stalls on work the roadmap never mentioned.\n\nThis is the structural version of that check, not a spot-fix for that one node: EVERY prerequisite of EVERY visible node must itself be visible. It holds no matter which lane is focused or which edges are added later, and it fails the moment a filter shows work whose blockers it hides." + +test fn no_visible_node_has_a_hidden_prerequisite() -> Bool { + let vis = visible() + all( + active_roadmap_edges() |> filter(e => + any(vis, n => (n.node as String) == (e.child as String))), + e => any(vis, n => (n.node as String) == (e.parent as String)), + ) +} + +test fn prerequisites_are_pulled_in_and_named_as_such() -> Bool { + let pulled = focus_prerequisite_ids( + focus: roadmap_focus_selection, + nodes: all_nodes(), + edges: active_roadmap_edges(), + ) + let lane = focus_lane_nodes(focus: roadmap_focus_selection, nodes: all_nodes()) + count(pulled) > 0 + && count(visible()) == count(lane) + count(pulled) + && all(pulled, p => !any(lane, l => (l.node as String) == p)) } diff --git a/dag/test/claim/roadmap_identity_witness_test.dag b/dag/test/claim/roadmap_identity_witness_test.dag new file mode 100644 index 00000000000..70aa8f8e8d7 --- /dev/null +++ b/dag/test/claim/roadmap_identity_witness_test.dag @@ -0,0 +1,70 @@ +module test.claim.roadmap_identity_witness_test + +import std.types { String, NonEmptyStr, Bool, Int, List } +import gunbc.roadmap_authority { declared_roadmap_nodes } +import gunbc.roadmap_identity { + RoadmapNodeIdentity, + RoadmapNodeSlug, + RoadmapIdentityBinding, + roadmap_identity_bindings, + identity_for_slug, + slug_for_identity, + identity_count, + distinct_identity_count, + distinct_slug_count, +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data identity_witness_note: String = "What a single-snapshot witness CAN and CANNOT prove, stated plainly so the gap is not mistaken for coverage. It can prove the registry is well formed here and now: identities unique, slugs unique, the binding total over every declared node, and resolution invertible in both directions. It CANNOT prove permanence — nothing in this tree distinguishes an identity that has always been that value from one silently replaced between the base revision and this head, because both look identical in a snapshot. Permanence is a statement about HISTORY, so it needs a base-versus-head comparison the current-tree corpus cannot supply, and claiming it here would be exactly the coverage-by-illusion this repository names. That gate is declared and owed, not quietly assumed.\\n\\nThe positional claim below is the discriminating one and the reason this stage exists at all. std.occurrence_identity's allocator is an ordinal from zero, so an identity derived by evaluation would shift the moment a node is inserted or reordered; asserting that a specific slug still resolves to a specific literal value is what separates a persisted identity from a positional one. It fails loudly if anyone reintroduces mint-during-evaluation." + +fn declared_slugs() -> List { + declared_roadmap_nodes() |> map(n => n.node as String) +} + +test fn every_declared_node_has_a_bound_identity() -> Bool { + all(declared_slugs(), s => match identity_for_slug(slug: s) { + Present { value: _ } => true + Absent => false + }) +} + +test fn identities_are_unique() -> Bool { + distinct_identity_count() == identity_count() +} + +test fn slugs_are_unique() -> Bool { + distinct_slug_count() == identity_count() +} + +test fn binding_is_total_over_declared_nodes() -> Bool { + identity_count() == count(declared_slugs()) +} + +test fn resolution_is_invertible() -> Bool { + all(roadmap_identity_bindings(), b => + match identity_for_slug(slug: b.slug.value as String) { + Present { value: i } => (i.value as String) == (b.identity.value as String) + Absent => false + } + && match slug_for_identity(identity: b.identity.value as String) { + Present { value: s } => (s.value as String) == (b.slug.value as String) + Absent => false + }) +} + +test fn an_unknown_slug_resolves_to_nothing() -> Bool { + match identity_for_slug(slug: "no-such-roadmap-node") { + Present { value: _ } => false + Absent => true + } +} + +test fn identity_is_persisted_not_positional() -> Bool { + match identity_for_slug(slug: "v1-emitter-fixed-point") { + Present { value: i } => + (i.value as String) == "rn_GCTJXB2DWA9CYV4RYPDTM76X60" + Absent => false + } +} diff --git a/dag/test/claim/roadmap_provider_events_witness_test.dag b/dag/test/claim/roadmap_provider_events_witness_test.dag index b6821b34f44..86cbacf3828 100644 --- a/dag/test/claim/roadmap_provider_events_witness_test.dag +++ b/dag/test/claim/roadmap_provider_events_witness_test.dag @@ -1,6 +1,6 @@ module test.claim.roadmap_provider_events_witness_test -import std.types { Bool, String } +import std.types { Bool, String, Int } import std.measure { character_count, character_count_value } import gunbc.roadmap_provider_events { CodexEventsParsed, @@ -14,6 +14,7 @@ import gunbc.roadmap_provider_events { codex_provider_event_line_observation_budget, codex_provider_projection_type_budget, codex_provider_projection_detail_budget, + codex_provider_projection_message_budget, codex_provider_event_projection_filter, provider_execution_state, provider_execution_state_label, @@ -217,3 +218,34 @@ test fn roadmap_provider_events_keystone_holds() -> Bool { && oversized_event_refuses_without_echoing_payload() && refusal_marks_prior_activity_as_historical() } + +data agent_report_projection_witness_note: String = "The agent REPORT members of the projection filter, pinned as a string contract because the filter is a jq program the substrate assembles but does not itself interpret (codex_projection_filter_scaffold_note). Nothing downstream of the shell can prove what the projector emitted, so the claims that matter here are: the report bound is the MESSAGE budget and not the detail budget beside it (a copy-paste of the wrong bound would still emit valid jq and still look fine on a short message, failing only on the day a long one arrived); the truncation flag is computed with a length comparison against the ORIGINAL text, so it describes the provider output rather than the slice; and the anchored item.type walk is untouched, since the report is emitted as a member AFTER item.type and the walk depends on that position. The behavioural half — real captured events through real jq — is dag/test/fixture/codex_provider_events, whose third line carries the item.text this filter reads." + +fn projection_filter_text() -> String { codex_provider_event_projection_filter as String } + +fn message_budget_chars() -> Int { character_count_value(c: codex_provider_projection_message_budget) } + +fn detail_budget_chars() -> Int { character_count_value(c: codex_provider_projection_detail_budget) } + +test fn agent_report_is_bounded_by_the_message_budget() -> Bool { + string_contains( + s: projection_filter_text(), + pattern: concat(".item.text[0:", concat(to_string(message_budget_chars()), "]")), + ) +} + +test fn agent_report_budget_is_not_the_detail_budget() -> Bool { + let m = message_budget_chars() + let d = detail_budget_chars() + !(m == d) +} + +test fn agent_report_declares_truncation_from_original_length() -> Bool { + string_contains(s: projection_filter_text(), pattern: "\"truncated\":((.item.text | length) > ") + && string_contains(s: projection_filter_text(), pattern: "agent_message") +} + +test fn agent_report_does_not_disturb_the_item_type_anchor() -> Bool { + string_contains(s: projection_filter_text(), pattern: "\"item\":{\"type\":item_type,\"message\":agent_report}") + && string_contains(s: projection_filter_text(), pattern: "\"item\":{\"type\":item_type}") +} diff --git a/dag/test/fixture/codex_provider_events/README.md b/dag/test/fixture/codex_provider_events/README.md new file mode 100644 index 00000000000..66ba8f87e69 --- /dev/null +++ b/dag/test/fixture/codex_provider_events/README.md @@ -0,0 +1,36 @@ +# Captured Codex provider-event receipt + +`agent_message_turn_2026-07-29.jsonl` is a **real** `codex exec --json` event stream, captured +2026-07-29 against `@openai/codex` **0.145.0** (`codex-linux-arm64`, aarch64-unknown-linux-musl) +authenticated via ChatGPT, with the prompt `Reply with exactly: ok` under `--sandbox read-only`. + +It exists because the `agent_message` item's text field could not be cited from anywhere else. +Nothing in the repository recorded it, the shipped CLI is a stripped native binary rather than +readable source, and `learn.chatgpt.com/docs/non-interactive-mode` — the locator +`extdeps.llm.cli` already cites — documents the mode, not the per-item event schema. Modeling +the field from memory would have been exactly the guess DESIGN §3 forbids for an `extdeps` +surface ("model what the API actually returns"), and the #7368 design note makes the same rule +explicit for provider grammars: a vocabulary is admitted only "after an actual provider receipt +demonstrates its grammar." So one was produced. + +The load-bearing line is the third: + +```json +{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"ok"}} +``` + +`item.text` is therefore evidence, not inference, and `codex_provider_event_projection_filter` +reads that member on that authority. + +Two incidental facts the capture also settles, recorded because they are cheap to keep and +expensive to re-derive: `turn.completed` carries a `usage` object with token counts (unread by +the projection today — a candidate carrier if attempt cost ever wants modeling), and +`thread.started` carries `thread_id`, which is the provider-session identity the resume/follow-up +lane will need. + +The `thread_id` is scrubbed to a zero UUID: it identifies a real provider session and is not a +fact this fixture is asserting. Every other byte is as captured. + +**Re-capture on:** a Codex major/minor version bump that changes the event envelope. This receipt +is version-stamped precisely so a schema change is a visible re-capture rather than a silent +drift in what the projection believes. diff --git a/dag/test/fixture/codex_provider_events/agent_message_turn_2026-07-29.jsonl b/dag/test/fixture/codex_provider_events/agent_message_turn_2026-07-29.jsonl new file mode 100644 index 00000000000..22a3715b318 --- /dev/null +++ b/dag/test/fixture/codex_provider_events/agent_message_turn_2026-07-29.jsonl @@ -0,0 +1,4 @@ +{"type":"thread.started","thread_id":"00000000-0000-0000-0000-000000000000"} +{"type":"turn.started"} +{"type":"item.completed","item":{"id":"item_0","type":"agent_message","text":"ok"}} +{"type":"turn.completed","usage":{"input_tokens":15923,"cached_input_tokens":13056,"cache_write_input_tokens":0,"output_tokens":5,"reasoning_output_tokens":0}}