diff --git a/dag/gunbc/dispatch_preflight.dag b/dag/gunbc/dispatch_preflight.dag index 80e5d2ec235..6267dbf7de9 100644 --- a/dag/gunbc/dispatch_preflight.dag +++ b/dag/gunbc/dispatch_preflight.dag @@ -21,11 +21,10 @@ import gunbc.roadmap_dispatch_environment { capability_ensure_receipt_json, } import gunbc.fleet_desired_observe { - FleetRevisionStanding, - RevisionConverged, - RevisionDrifted, - RevisionUnobserved, - fleet_revision_standing, + LocalTreeRevision, + LocalTreeRevisionObserved, + LocalTreeRevisionUnobserved, + local_tree_revision, } import extdeps.git.object_store { git_object_id_wire_hex } import extdeps.languages.json.emit { @@ -54,7 +53,7 @@ import extdeps.shell // This module is the read-only route that answers it independently of whether a spawn is // admissible. It observes and it does not actuate: capability_ensure_observe runs // shell.Test.IsExecutable and the realization's own declared identity_args (the --version probes), -// and fleet_revision_standing runs git ls-remote and rev-parse in the deployed tree. No worktree is +// and local_tree_revision runs git rev-parse in the deployed tree. No worktree is // created, no tmux session is started, no file is written, and there is no flag that opens a write // path — a preflight that can also change what it measures is a preflight whose green proves // nothing. @@ -148,30 +147,22 @@ fn preflight_capability_receipts( capability_ensure_observe(realization: realization)) } -// The revision axis is the one that decides whether ANY spawn can happen, so it is -// reported beside the capabilities rather than ahead of them: a host may be perfectly -// provisioned and still spawn nothing, and a reader needs to see both facts at once. -fn revision_axis_verdict(standing: FleetRevisionStanding) -> PreflightAxisVerdict { +// The revision axis names the code the host would dispatch from, so it is reported beside the +// capabilities rather than ahead of them: a reader needs both facts at once. It refuses only when +// HEAD cannot be read. Drift from the admitted fleet ref is no longer an axis (operator ruling, +// 2026-09-05; `gunbc.roadmap_launch_admission` `launch_revision_outcome` carries the argument): +// convergence is the deploy path's fact, and the deployed-tree report renders it. +fn revision_axis_verdict(standing: LocalTreeRevision) -> PreflightAxisVerdict { match standing { - RevisionConverged { revision } => + LocalTreeRevisionObserved { revision } => AxisGrounded { axis_label: dispatch_preflight_revision_axis_label, evidence: join([ - "deployed tree is the admitted fleet revision ", + "deployed tree is at revision ", git_object_id_wire_hex(oid: revision) as String, ], ""), } - RevisionDrifted { desired, local } => - AxisRefused { - axis_label: dispatch_preflight_revision_axis_label, - reason: join([ - "deployed tree is not the admitted fleet revision; the belt will withhold spawn and reap on every tick until it converges. desired=", - git_object_id_wire_hex(oid: desired) as String, - " local=", - git_object_id_wire_hex(oid: local) as String, - ], ""), - } - RevisionUnobserved { cause } => + LocalTreeRevisionUnobserved { cause } => AxisRefused { axis_label: dispatch_preflight_revision_axis_label, reason: join([ @@ -191,7 +182,7 @@ fn revision_axis_verdict(standing: FleetRevisionStanding) -> PreflightAxisVerdic // establish the carrier's honesty. // // The repair is construction rather than a consistency check (DESIGN 5): the report carries the -// capability receipts and the fleet revision standing — two independent observations, neither +// capability receipts and the deployed tree revision — two independent observations, neither // derived from the other — and preflight_axes projects them on demand for the verdict and for the // wire. A report whose axes disagree with its receipts now has no spelling, so there is nothing // left to validate. @@ -200,7 +191,7 @@ type DispatchPreflightReport { instance_id: String repo_root: String capability_receipts: List - revision: FleetRevisionStanding + revision: LocalTreeRevision } fn preflight_axes(report: DispatchPreflightReport) -> List { @@ -225,7 +216,7 @@ fn preflight_report_for_instance( instance_id: instance.instance_id as String, repo_root: instance.repo_root as String, capability_receipts: receipts, - revision: fleet_revision_standing(repo: instance.repo_root), + revision: local_tree_revision(repo: instance.repo_root), } } diff --git a/dag/gunbc/fleet/fleet_desired_admission.dag b/dag/gunbc/fleet/fleet_desired_admission.dag index 28b86b13ab0..45555ceabff 100644 --- a/dag/gunbc/fleet/fleet_desired_admission.dag +++ b/dag/gunbc/fleet/fleet_desired_admission.dag @@ -46,7 +46,7 @@ import gunbc.fleet_revision_acceptance { } import extdeps.git.plumbing { GitWorktreeAt } import gunbc.required_ci_epoch_observation { RequiredCiEpochObservation, observe_required_ci_epoch_at_commit } -import gunbc.fleet_desired_observe { observe_fleet_revision_relation, observe_fleet_desired_revision, observe_local_tree_revision } +import gunbc.fleet_desired_observe { observe_fleet_revision_relation, observe_fleet_desired_revision, local_tree_revision, LocalTreeRevisionObserved, LocalTreeRevisionUnobserved } import gunbc.fleet_main_revision { FleetDesiredAdvanceInput, FleetDesiredExpectationRefusedInput, @@ -404,10 +404,10 @@ func admit_fleet_desired_from_floor_event_wet() -> ProcessExit { ) { AdmissionRefused { cause: why } => exit_failure(reason: why) AdmissionActionAuthorized { action: action } => - match observe_local_tree_revision(repo: fleet_desired_admission_repo_root) { - Absent => + match local_tree_revision(repo: fleet_desired_admission_repo_root) { + LocalTreeRevisionUnobserved { cause: _ } => exit_failure(reason: fleet_desired_unobservable_judge_source_refusal_reason()) - Present { value: judge_source_observed } => { + LocalTreeRevisionObserved { revision: judge_source_observed } => { let wrote = Filesystem.Write( path: fleet_desired_admission_authorized_action_path, content: fleet_desired_authorized_action_script( diff --git a/dag/gunbc/fleet/fleet_desired_observe.dag b/dag/gunbc/fleet/fleet_desired_observe.dag index e398dc9b587..00502146b1f 100644 --- a/dag/gunbc/fleet/fleet_desired_observe.dag +++ b/dag/gunbc/fleet/fleet_desired_observe.dag @@ -169,15 +169,33 @@ fn observe_fleet_revision_relation( ) } -// The local half of the comparison: what revision THIS host's deployed tree is at. -// Absent (typed, located in the cause the caller renders) when the repo path is not a -// git repository or HEAD does not decode — never a fabricated revision. -fn observe_local_tree_revision(repo: FilePath) -> GitObjectId? { +// THE DEPLOYED TREE'S OWN REVISION, read locally and compared to nothing: which software is acting. +// This is the fact the roadmap consumes -- a launch identity names the code that dispatched it, a +// tick receipt names the code that ticked -- and it is NOT convergence. Whether this revision equals +// the admitted fleet ref is the deploy path's question, answered by FleetRevisionStanding below for +// the converge CLI and the deployed-tree report. Dispatch and the belt do not consult it (operator +// ruling, 2026-09-05): desired advances on every green merge and a deploy takes minutes, so a gate +// demanding equality was satisfiable only in the gap between two merges, and the tick refusing +// under drift turned every merge into a pipeline outage on the host. Unreadable still refuses: a +// dispatch that cannot name the code it runs has no identity to bind. +type LocalTreeRevision + = LocalTreeRevisionObserved { revision: GitObjectId } + | LocalTreeRevisionUnobserved { cause: String } + +fn local_tree_revision(repo: FilePath) -> LocalTreeRevision { let read = git.Core.RevParseInRepo(repo: repo, target: "HEAD" as GitRef) if read.exit_code == 0 { - git_object_id_from_untagged_hex(hex: trim(s: read.revision)) + match git_object_id_from_untagged_hex(hex: trim(s: read.revision)) { + Present { value: oid } => LocalTreeRevisionObserved { revision: oid } + Absent => + LocalTreeRevisionUnobserved { + cause: concat("fleet_desired_observe: HEAD is not a decodable object id: ", trim(s: read.revision)), + } + } } else { - none + LocalTreeRevisionUnobserved { + cause: "fleet_desired_observe: local tree revision unreadable (not a git repository, or HEAD undecodable)", + } } } @@ -200,17 +218,14 @@ fn admitted_revision_standing(repo: FilePath, remote: String, ref_name: GitRef) let observed = observe_admitted_revision(repo: repo, remote: remote, ref_name: ref_name) match converge_target_revision(observed: observed) { Present { value: desired } => - match observe_local_tree_revision(repo: repo) { - Present { value: local } => + match local_tree_revision(repo: repo) { + LocalTreeRevisionObserved { revision: local } => if git_object_id_eq(left: desired, right: local) { RevisionConverged { revision: desired } } else { RevisionDrifted { desired: desired, local: local } } - Absent => - RevisionUnobserved { - cause: "fleet_desired_observe: local tree revision unreadable (not a git repository, or HEAD undecodable)", - } + LocalTreeRevisionUnobserved { cause } => RevisionUnobserved { cause: cause } } Absent => match observed { diff --git a/dag/gunbc/roadmap/roadmap_belt_actuate.dag b/dag/gunbc/roadmap/roadmap_belt_actuate.dag index 0da1e083c61..5d629ab7b17 100644 --- a/dag/gunbc/roadmap/roadmap_belt_actuate.dag +++ b/dag/gunbc/roadmap/roadmap_belt_actuate.dag @@ -41,14 +41,14 @@ import gunbc.roadmap_belt { import gunbc.roadmap_launch_admission { LaunchCause, Operator, Timer, LaunchRefusal, - LaunchNoActuatingInstance, LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved, LaunchRevisionDrift, + LaunchNoActuatingInstance, LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved, LaunchAuthorityUnobserved, LaunchNodeUnknown, LaunchNodeSuperseded, LaunchAlreadyAccepted, LaunchReviewRequired, LaunchSizeMissing, LaunchExecutionContractMissing, LaunchDependenciesBlocked, LaunchSessionsUnobservable, LaunchAlreadyLive, LaunchStaleSession, LaunchLivenessUnobserved, LaunchSessionMultiplicityConflict, LaunchNoCapacity, LaunchCauseNotPermitted, LaunchGateNotEvaluated, LaunchAdmission, LaunchAdmitted, LaunchRefused, LaunchHostStanding, LaunchHostObserved, LaunchHostObserveOnly, LaunchHostHalted, LaunchHostUnobserved, - launch_host_halt_transition, launch_host_stage_revision, LaunchHostRevisionStage, HostRevisionConverged, HostRevisionHalted, + launch_host_halt_transition, launch_host_stage_revision, LaunchHostRevisionStage, HostRevisionObserved, HostRevisionHalted, launch_host_refusal, LaunchSessionsStanding, LaunchSessionsObserved, LaunchSessionsUnobserved, LaunchSessionsPending, launch_host_with_sessions, LaunchStagedAdmission, LaunchStageDecided, LaunchStageNeedsSessions, launch_admission_staged, @@ -363,9 +363,9 @@ import gunbc.git_diff_change_window { } import extdeps.git.object_store { git_object_id_from_untagged_hex, git_object_id_wire_hex } import gunbc.fleet_desired_observe { - admitted_revision_standing, + local_tree_revision, FleetRevisionStanding, - RevisionConverged, RevisionDrifted, RevisionUnobserved, + LocalTreeRevisionObserved, LocalTreeRevisionUnobserved, } import extdeps.git { shape_git_rev_parse_head_args, shape_git_status_porcelain_nul_args, shape_git_add_all_args, shape_git_commit_message_args } import gunbc.roadmap.roadmap_belt_commit { BeltCommitDecision, CommitNeeded, CommitNotNeeded, belt_commit_decision } @@ -5868,30 +5868,27 @@ fn belt_session_liveness_from_process( // matched BeltSessionLiveness directly. Consumers match the coproduct. data belt_dispatch_wire_contract_exemplar_provider: String = "codex" -// DISPATCH READS BACK THE DEPLOYED REVISION BEFORE IT ACTS, because every fact this path consumes — -// node roster, signoff, contract, the command it runs — comes from the deployed tree. -// `gunbc.fleet_desired_observe` `fleet_revision_standing` already decided that cell three ways, and -// its ONLY consumer was `gunbc.fleet_converge_cli`: convergence knew the host was drifted while the -// served page dispatched against the drifted tree, saying nothing. Not a stale display — a worker -// spawned from code the fleet never admitted, reported as an ordinary spawn. -// -// So the standing is consulted FIRST — before node lookup, session observation, worktree — because -// a roster from an unadmitted tree cannot be trusted to name the right node, and refusing later -// means refusing after acting on it. -// -// BOTH non-converged arms refuse, neither an absorbing widen: `RevisionDrifted` carries BOTH -// revisions (the membership-diff `from` ruling — a catch-up needs the prior, a receipt must name -// what it observed); `RevisionUnobserved` carries its cause and is unobserved-GRADE, never -// satisfied. Dispatching on `RevisionUnobserved` would be the empty-observation narrow: -// could-not-compare rendered as nothing-is-wrong. +// DISPATCH READS BACK THE DEPLOYED REVISION BEFORE IT ACTS, and binds it into the launch identity: +// every fact this path consumes -- node roster, signoff, contract, the command it runs -- comes from +// the deployed tree, so an attempt must name the code that produced it. What it does NOT do, as of +// the 2026-09-05 operator ruling, is refuse when that revision differs from the admitted fleet ref. +// It did: `fleet_revision_standing` was consulted first and both non-converged arms refused, on the +// argument that a roster from an unadmitted tree cannot be trusted to name the right node. The +// argument priced the wrong side. The worker checks out origin/main itself, the belt verifies +// against main, publication opens a PR against main -- convergence decides only which version of +// the CONTROL software runs, and desired advances on every green merge while a deploy takes +// minutes, so equality held only in the gap between two merges. Because the tick shared the gate, +// a host one merge behind stopped verifying, reviewing and publishing as well: convergence lag +// became a pipeline outage. Drift is a host fact for the deployed-tree report and the deploy path; +// here only an UNREADABLE revision refuses, because an attempt with no code to name has no identity. // // The repo path is `instance.repo_root`, not the `srv1_gunbc_repo_root` constant -// `fleet_converge_cli` uses: the gate is a fact about the instance dispatched, and hardcoding srv1 +// `fleet_converge_cli` uses: the read is a fact about the instance dispatched, and hardcoding srv1 // would mint a second path authority answering for the wrong host on any other instance. // THE TRANSITION GATE DOMINATES THE REVISION GATE; the order is the safety property. A repository // convergence advances the base ref BEFORE it transitions the worktree, so a converge that died in // between leaves the ref at the admitted revision over a tree that never moved — and -// `fleet_revision_standing` reads that ref. The revision gate would answer CONVERGED and admit a +// a revision read alone would see an ordinary HEAD and admit a // dispatch into exactly the half-transitioned tree the belt must stay out of. Asking the inhibition // first stops a gate being satisfied by the artifact of the failure it should notice. // @@ -5912,7 +5909,7 @@ fn belt_transition_admission_for_instance(instance: HostDashboardInstance) -> Ac // THE ONE STAGED OBSERVER behind the page, POST /dispatch and the timer. It reads in gate order and // stops at the first refusal: instance posture is a model fact (no effect); the transition file is // read only for an actuating instance; the fleet revision only under an admitted transition; tmux -// only under a converged revision. Each halt is a LaunchHostHalted whose later fields do not exist, +// only under an observed revision. Each halt is a LaunchHostHalted whose later fields do not exist, // so `tmux listed under an inhibited transition` is not a state it can produce (DESIGN 4b: no // constructor). The refusal in a halt is derived by the admission's own gate functions // (launch_host_halt_transition, launch_host_stage_revision), never spelled here. @@ -5930,14 +5927,10 @@ fn belt_launch_host_standing_for_instance_with_capacity( ActuationAdmitted => match launch_host_stage_revision( instance_id: instance_id, - revision: admitted_revision_standing( - repo: instance.repo_root, - remote: instance.admitted_revision.remote, - ref_name: instance.admitted_revision.ref_name, - ), + revision: local_tree_revision(repo: instance.repo_root), ) { HostRevisionHalted { standing } => standing - HostRevisionConverged { revision } => + HostRevisionObserved { revision } => LaunchHostObserved { instance_id: instance.instance_id, actuation: DashboardActuating, @@ -6178,7 +6171,6 @@ fn launch_refusal_band(r: LaunchRefusal) -> DispatchBand { LaunchCauseNotPermitted { cause: _, mode: _ } => BandFail LaunchTransitionInhibited { reason: _ } => BandLoud LaunchRevisionUnobserved { cause: _ } => BandLoud - LaunchRevisionDrift { desired: _, local: _ } => BandLoud LaunchAuthorityUnobserved { detail: _ } => BandLoud LaunchSessionsUnobservable { reason: _ } => BandLoud LaunchStaleSession { session_name: _, detail: _ } => BandLoud @@ -7090,35 +7082,24 @@ fn belt_verification_presentation_detail( } // THE EXECUTION STANDING IS OBSERVED AT THE MINT SITE from the instance the tick ran for and the -// revision standing of that instance's tree -- the same admitted_revision_standing read the -// dispatch preflight makes, against the same per-instance admitting ref, because a tick receipt -// that answered from a different authority than the gate would report a revision the gate never -// admitted -- and the mode is the production row the tick itself consumed. A drifted or -// unobservable revision withholds the binding rather than recording a revision the tick did not -// serve. +// deployed tree's own revision -- the same local_tree_revision read the launch gate and the +// dispatch preflight make, because a tick receipt that answered from a different authority than +// the gate would report a revision the gate never bound -- and the mode is the production row the +// tick itself consumed. An unreadable revision withholds the binding rather than recording a +// revision the tick did not serve. Drift from the fleet ref is not consulted (operator ruling, +// 2026-09-05; see launch_revision_outcome). fn belt_tick_execution_standing_for_instance( instance: HostDashboardInstance, mode: SpawnMode, ) -> BeltTickExecutionStanding { - match admitted_revision_standing( - repo: instance.repo_root, - remote: instance.admitted_revision.remote, - ref_name: instance.admitted_revision.ref_name, - ) { - RevisionConverged { revision: r } => + match local_tree_revision(repo: instance.repo_root) { + LocalTreeRevisionObserved { revision: r } => TickExecuted { instance_id: instance.instance_id as String, deployed_revision: git_object_id_wire_hex(oid: r) as String, spawn_mode: mode, } - RevisionDrifted { desired: d, local: l } => - TickExecutionWithheld { - refusal: join([ - "revision drifted: desired ", git_object_id_wire_hex(oid: d) as String, - " local ", git_object_id_wire_hex(oid: l) as String, - ], ""), - } - RevisionUnobserved { cause: c } => + LocalTreeRevisionUnobserved { cause: c } => TickExecutionWithheld { refusal: join(["revision unobserved: ", c], "") } } } diff --git a/dag/gunbc/roadmap/roadmap_launch_admission.dag b/dag/gunbc/roadmap/roadmap_launch_admission.dag index bda319238ca..3123b94421c 100644 --- a/dag/gunbc/roadmap/roadmap_launch_admission.dag +++ b/dag/gunbc/roadmap/roadmap_launch_admission.dag @@ -15,7 +15,7 @@ import gunbc.roadmap_authority { } import gunbc.roadmap_dashboard_instance { DashboardActuationPosture, DashboardObserveOnly, DashboardActuating } import gunbc.deploy_transition { ActuationAdmission, ActuationAdmitted, ActuationRefused } -import gunbc.fleet_desired_observe { FleetRevisionStanding, RevisionConverged, RevisionDrifted, RevisionUnobserved } +import gunbc.fleet_desired_observe { LocalTreeRevision, LocalTreeRevisionObserved, LocalTreeRevisionUnobserved } import extdeps.git.object_store { GitObjectId, GitSha1ObjectId, git_object_id_wire_hex, git_object_id_from_untagged_hex } import gunbc.roadmap_dispatch_actuator { DispatchLiveSession } import gunbc.worker_lifecycle { @@ -86,7 +86,7 @@ type LaunchIdentity { // The five facts keep the types their authorities gave them (DESIGN 2: consume, never unwind at a // boundary): node_id is the RoadmapNodeId of the node the node gate FOUND in the projected // authority, never the request's spelling; instance_id is the dashboard instance's NonEmptyStr; -// revision is the GitObjectId the revision gate observed converged. The three text projections +// revision is the GitObjectId the revision gate observed as the deployed tree's HEAD. The three text projections // below are the ONLY place an identity becomes a string, so the wire, the page attributes and the // reason line spell each fact one way. @@ -208,7 +208,6 @@ type LaunchRefusal | LaunchInstanceActuationRefused { instance_id: String, reason: String } | LaunchTransitionInhibited { reason: String } | LaunchRevisionUnobserved { cause: String } - | LaunchRevisionDrift { desired: String, local: String } | LaunchAuthorityUnobserved { detail: String } | LaunchNodeUnknown | LaunchNodeSuperseded { by: String } @@ -241,7 +240,6 @@ fn launch_refusal_label(r: LaunchRefusal) -> String { LaunchInstanceActuationRefused { instance_id: _, reason: _ } => "disabled" LaunchTransitionInhibited { reason: _ } => "deployment_transition_inhibited" LaunchRevisionUnobserved { cause: _ } => "deployed_revision_unobserved" - LaunchRevisionDrift { desired: _, local: _ } => "deployed_revision_drifted" LaunchAuthorityUnobserved { detail: _ } => "authority_refused" LaunchNodeUnknown => "node_not_found" LaunchNodeSuperseded { by: _ } => "node_superseded" @@ -267,7 +265,6 @@ fn launch_refusal_reason(r: LaunchRefusal) -> String { LaunchInstanceActuationRefused { instance_id, reason } => join(["instance ", instance_id, ": ", reason], "") LaunchTransitionInhibited { reason } => reason LaunchRevisionUnobserved { cause } => join(["deployed revision could not be observed: ", cause], "") - LaunchRevisionDrift { desired, local } => join(["deployed tree is not the admitted fleet revision: desired=", desired, " local=", local], "") LaunchAuthorityUnobserved { detail } => join(["roadmap authority projection refused: ", detail], "") LaunchNodeUnknown => "node is not in the roadmap authority" LaunchNodeSuperseded { by } => join(["node is superseded by ", by], "") @@ -293,7 +290,6 @@ fn launch_refusal_exemplars() -> List { LaunchInstanceActuationRefused { instance_id: "i", reason: "r" }, LaunchTransitionInhibited { reason: "r" }, LaunchRevisionUnobserved { cause: "c" }, - LaunchRevisionDrift { desired: "a", local: "b" }, LaunchAuthorityUnobserved { detail: "d" }, LaunchNodeUnknown, LaunchNodeSuperseded { by: "s" }, @@ -345,7 +341,7 @@ type LaunchHostStanding instance_id: NonEmptyStr, actuation: DashboardActuationPosture, transition: ActuationAdmission, - revision: FleetRevisionStanding, + revision: LocalTreeRevision, sessions: LaunchSessionsStanding, capacity: BeltCapacity, mode: SpawnMode, @@ -358,7 +354,7 @@ type LaunchHostStanding // builds a standing for a real driver (gunbc.roadmap_belt_actuate // belt_launch_host_standing_for_instance -- the ONE observer behind the page, POST /dispatch and // the timer) reads the transition file only for an actuating instance, reads the fleet revision -// only under an admitted transition, and lists tmux only under a converged revision. When an +// only under an admitted transition, and lists tmux only under an observed revision. When an // earlier gate refuses, the standing is LaunchHostHalted at that gate and the later host facts are // NOT fields of it: there is no constructor for `transition inhibited AND sessions observed`, so // the effect order the route promises (`refused before observation`) is structural, not a @@ -376,12 +372,12 @@ fn launch_host_halt_transition(instance_id: String, reason: String) -> LaunchHos } type LaunchHostRevisionStage - = HostRevisionConverged { revision: FleetRevisionStanding } + = HostRevisionObserved { revision: LocalTreeRevision } | HostRevisionHalted { standing: LaunchHostStanding } -fn launch_host_stage_revision(instance_id: String, revision: FleetRevisionStanding) -> LaunchHostRevisionStage { +fn launch_host_stage_revision(instance_id: String, revision: LocalTreeRevision) -> LaunchHostRevisionStage { match launch_gate_revision(revision: revision) { - GatePassed => HostRevisionConverged { revision: revision } + GatePassed => HostRevisionObserved { revision: revision } GateRefused { refusal } => HostRevisionHalted { standing: LaunchHostHalted { instance_id: instance_id, gate: HostRevisionGate, refusal: refusal } } GateNotReached => @@ -496,7 +492,7 @@ type LaunchIdentityStanding | LaunchIdentityWithheld { refusal: LaunchRefusal } // The identity is a CONSEQUENCE of the first four gates, not a tenth gate: it is bound exactly when -// the host is actuating and transition-clear, the revision observed converged (that GitObjectId) +// the host is actuating and transition-clear, the revision observed (that GitObjectId) // and the node found (that RoadmapNodeId). When it is withheld, the standing carries the refusal of // the gate that withheld it -- the same verdict that gate's ledger row holds -- so the admission // never manufactures an `identity` gate and no ledger state says `every gate passed and no identity @@ -662,19 +658,21 @@ type LaunchRevisionOutcome = LaunchRevisionBound { oid: GitObjectId } | LaunchRevisionRefused { refusal: LaunchRefusal } -fn launch_revision_outcome(revision: FleetRevisionStanding) -> LaunchRevisionOutcome { +// THE REVISION GATE BINDS, IT DOES NOT COMPARE (operator ruling, 2026-09-05). It reads the deployed +// tree's own HEAD so the launch identity can name the code that dispatched it, and refuses only when +// that cannot be read. It used to refuse on drift from the admitted fleet ref as well; that made +// admissibility a race the host could not win -- desired advances on every green merge, a deploy +// takes minutes -- and, because the belt tick shared the gate, a lagging host stopped verifying, +// reviewing and publishing too. Convergence is the deploy path's fact (`gunbc.fleet_desired_observe` +// `FleetRevisionStanding`, the deployed-tree report), never an admission input here. +fn launch_revision_outcome(revision: LocalTreeRevision) -> LaunchRevisionOutcome { match revision { - RevisionConverged { revision: oid } => LaunchRevisionBound { oid: oid } - RevisionDrifted { desired, local } => - LaunchRevisionRefused { refusal: LaunchRevisionDrift { - desired: git_object_id_wire_hex(oid: desired) as String, - local: git_object_id_wire_hex(oid: local) as String, - } } - RevisionUnobserved { cause } => LaunchRevisionRefused { refusal: LaunchRevisionUnobserved { cause: cause } } + LocalTreeRevisionObserved { revision: oid } => LaunchRevisionBound { oid: oid } + LocalTreeRevisionUnobserved { cause } => LaunchRevisionRefused { refusal: LaunchRevisionUnobserved { cause: cause } } } } -fn launch_gate_revision(revision: FleetRevisionStanding) -> LaunchGateVerdict { +fn launch_gate_revision(revision: LocalTreeRevision) -> LaunchGateVerdict { match launch_revision_outcome(revision: revision) { LaunchRevisionBound { oid: _ } => GatePassed LaunchRevisionRefused { refusal } => GateRefused { refusal: refusal } @@ -881,7 +879,7 @@ fn launch_identity_of_observed( instance_id: NonEmptyStr, actuation: DashboardActuationPosture, transition: ActuationAdmission, - revision: FleetRevisionStanding, + revision: LocalTreeRevision, lookup: LaunchNodeLookup, ) -> LaunchIdentityStanding { match launch_instance_outcome(instance_id: instance_id, actuation: actuation) { diff --git a/dag/gunbc/roadmap/roadmap_launch_deployment_cli.dag b/dag/gunbc/roadmap/roadmap_launch_deployment_cli.dag index 9b4071573c4..e90bf210ea2 100644 --- a/dag/gunbc/roadmap/roadmap_launch_deployment_cli.dag +++ b/dag/gunbc/roadmap/roadmap_launch_deployment_cli.dag @@ -42,7 +42,7 @@ import gunbc.live_deploy.apply_receipt { } import gunbc.fleet_desired_observe { observe_fleet_desired_revision, FleetDesiredAdmitted, FleetDesiredAbsent, FleetDesiredUndecodable, FleetDesiredUnobserved, - RevisionConverged, RevisionDrifted, RevisionUnobserved, + LocalTreeRevisionObserved, LocalTreeRevisionUnobserved, } import gunbc.fleet_ssh_locus { prepare_fleet_ssh_agent_context, FleetSshContextReady, FleetSshContextRefused, fleet_locus_ssh_target } import gunbc.fleet_known_hosts_anchor { FleetSshExecutionContext } @@ -140,7 +140,7 @@ fn rlm_workflow_file_path() -> String { // is the belt's own selection path; a selection refusal is recorded as the provider fact's refusal // rather than skipped. data rlm1_merge_commit: String = "0beef8a2c1a035f4f1f0507e77075bdeaba74cba" -data rlm1_contract_digest_frozen: String = "8b884d701d7a356e" +data rlm1_contract_digest_frozen: String = "20ca8a8cde2f7e84" data rlm_dashboard_run_id_env_name: String = "RLM_DASHBOARD_RUN_ID" data rlm_receipt_host: String = "srv1" data rlm_receipt_vantage: String = "." @@ -340,9 +340,8 @@ fn dispatch_preflight_fact(instance: HostDashboardInstance) -> DispatchPreflight match dispatch_preflight_for_instance(instance: instance) { PreflightAdmitted { report: r } => match r.revision { - RevisionConverged { revision: rev } => DispatchPreflightAdmittedFact { instance_id: r.instance_id, revision_hex: git_object_id_wire_hex(oid: rev) as String } - RevisionDrifted { desired: d, local: l } => DispatchPreflightRefusedFact { refused_axis_count: 1, detail: join(["revision drifted desired=", git_object_id_wire_hex(oid: d) as String, " local=", git_object_id_wire_hex(oid: l) as String], "") } - RevisionUnobserved { cause: c } => DispatchPreflightRefusedFact { refused_axis_count: 1, detail: join(["revision unobserved: ", c], "") } + LocalTreeRevisionObserved { revision: rev } => DispatchPreflightAdmittedFact { instance_id: r.instance_id, revision_hex: git_object_id_wire_hex(oid: rev) as String } + LocalTreeRevisionUnobserved { cause: c } => DispatchPreflightRefusedFact { refused_axis_count: 1, detail: join(["revision unobserved: ", c], "") } } PreflightRefused { report: _, refused_axis_count: n } => DispatchPreflightRefusedFact { refused_axis_count: n, detail: "dispatch preflight refused" } } diff --git a/dag/gunbc/roadmap/roadmap_serve.dag b/dag/gunbc/roadmap/roadmap_serve.dag index 33952117125..6f43d613633 100644 --- a/dag/gunbc/roadmap/roadmap_serve.dag +++ b/dag/gunbc/roadmap/roadmap_serve.dag @@ -34,7 +34,7 @@ import extdeps.languages.json.emit { } import gunbc.roadmap_launch_admission { LaunchRefusal, - LaunchNoActuatingInstance, LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved, LaunchRevisionDrift, + LaunchNoActuatingInstance, LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved, LaunchAuthorityUnobserved, LaunchNodeUnknown, LaunchNodeSuperseded, LaunchAlreadyAccepted, LaunchReviewRequired, LaunchSizeMissing, LaunchExecutionContractMissing, LaunchDependenciesBlocked, LaunchSessionsUnobservable, LaunchAlreadyLive, LaunchStaleSession, LaunchLivenessUnobserved, LaunchSessionMultiplicityConflict, @@ -361,7 +361,6 @@ fn launch_refusal_http_status(r: LaunchRefusal) -> HttpStatus { LaunchNoCapacity { occupied: _, max_concurrent: _ } => 409 LaunchCauseNotPermitted { cause: _, mode: _ } => 409 LaunchStaleSession { session_name: _, detail: _ } => 409 - LaunchRevisionDrift { desired: _, local: _ } => 409 LaunchTransitionInhibited { reason: _ } => 409 LaunchSessionMultiplicityConflict { session_names: _, session_count: _ } => 409 LaunchSessionsUnobservable { reason: _ } => 503 diff --git a/dag/test/claim/dispatch_preflight_witness_test.dag b/dag/test/claim/dispatch_preflight_witness_test.dag index 2c0b7d2c8cb..c1ccba2154b 100644 --- a/dag/test/claim/dispatch_preflight_witness_test.dag +++ b/dag/test/claim/dispatch_preflight_witness_test.dag @@ -8,10 +8,9 @@ import gunbc.roadmap_execution_contract { } import gunbc.roadmap_dispatch_environment { CapabilityEnsureReceipt } import gunbc.fleet_desired_observe { - FleetRevisionStanding, - RevisionConverged, - RevisionDrifted, - RevisionUnobserved, + LocalTreeRevision, + LocalTreeRevisionObserved, + LocalTreeRevisionUnobserved, } import gunbc.dispatch_preflight { PreflightAxisVerdict, @@ -80,7 +79,7 @@ fn ungrounded_receipt() -> CapabilityEnsureReceipt { // its receipts even deliberately. fn report_of( receipts: List, - revision: FleetRevisionStanding, + revision: LocalTreeRevision, ) -> DispatchPreflightReport { DispatchPreflightReport { instance_id: "srv1-live", @@ -90,22 +89,15 @@ fn report_of( } } -fn converged_revision() -> FleetRevisionStanding { +fn observed_revision() -> LocalTreeRevision { match git_object_id_from_untagged_hex(hex: desired_hex) { - Present { value: oid } => RevisionConverged { revision: oid } - Absent => RevisionUnobserved { cause: "witness fixture hex did not decode" } + Present { value: oid } => LocalTreeRevisionObserved { revision: oid } + Absent => LocalTreeRevisionUnobserved { cause: "witness fixture hex did not decode" } } } -fn drifted_revision() -> FleetRevisionStanding { - match git_object_id_from_untagged_hex(hex: desired_hex) { - Absent => RevisionUnobserved { cause: "witness fixture hex did not decode" } - Present { value: desired } => - match git_object_id_from_untagged_hex(hex: local_hex) { - Absent => RevisionUnobserved { cause: "witness fixture hex did not decode" } - Present { value: local } => RevisionDrifted { desired: desired, local: local } - } - } +fn unobserved_revision() -> LocalTreeRevision { + LocalTreeRevisionUnobserved { cause: "HEAD does not decode" } } test fn preflight_capability_axis_follows_the_shared_receipt() -> Bool { @@ -127,49 +119,31 @@ test fn preflight_capability_axis_follows_the_shared_receipt() -> Bool { } } -test fn preflight_revision_axis_names_both_sides_of_the_drift() -> Bool { - return match git_object_id_from_untagged_hex(hex: desired_hex) { - Absent => false - Present { value: desired } => - match git_object_id_from_untagged_hex(hex: local_hex) { - Absent => false - Present { value: local } => { - let drifted = revision_axis_verdict( - standing: RevisionDrifted { desired: desired, local: local }, - ) - let converged = revision_axis_verdict( - standing: RevisionConverged { revision: desired }, - ) - let unobserved = revision_axis_verdict( - standing: RevisionUnobserved { cause: "HEAD does not decode" }, - ) - axis_verdict_is_refused(verdict: drifted) - && !axis_verdict_is_refused(verdict: converged) - && axis_verdict_is_refused(verdict: unobserved) - && match drifted { - AxisGrounded { axis_label: _, evidence: _ } => false - AxisRefused { axis_label: _, reason } => - string_contains(s: reason, pattern: desired_hex) - && string_contains(s: reason, pattern: local_hex) - } - && match unobserved { - AxisGrounded { axis_label: _, evidence: _ } => false - AxisRefused { axis_label: _, reason } => - string_contains(s: reason, pattern: "HEAD does not decode") - } - } - } - } +// The revision axis names the code the host would dispatch from and refuses only when it cannot be +// read; a drifted-but-readable tree is NOT an axis refusal (operator ruling 2026-09-05). +test fn preflight_revision_axis_names_the_deployed_revision_and_refuses_only_unreadable() -> Bool { + let observed = revision_axis_verdict(standing: observed_revision()) + let unobserved = revision_axis_verdict(standing: unobserved_revision()) + return !axis_verdict_is_refused(verdict: observed) + && axis_verdict_is_refused(verdict: unobserved) + && match observed { + AxisGrounded { axis_label: _, evidence } => string_contains(s: evidence, pattern: desired_hex) + AxisRefused { axis_label: _, reason: _ } => false + } + && match unobserved { + AxisGrounded { axis_label: _, evidence: _ } => false + AxisRefused { axis_label: _, reason } => string_contains(s: reason, pattern: "HEAD does not decode") + } } test fn preflight_counts_every_refusal_not_the_first() -> Bool { let two_bad = preflight_verdict_for_report(report: report_of( receipts: [ungrounded_receipt(), grounded_receipt()], - revision: drifted_revision(), + revision: unobserved_revision(), )) let all_good = preflight_verdict_for_report(report: report_of( receipts: [grounded_receipt()], - revision: converged_revision(), + revision: observed_revision(), )) return match two_bad { PreflightAdmitted { report: _ } => false @@ -185,13 +159,13 @@ test fn preflight_wire_reports_the_refusal_it_reached() -> Bool { let refused = serialize_json(v: dispatch_preflight_json_value( preflight: preflight_verdict_for_report(report: report_of( receipts: [], - revision: drifted_revision(), + revision: unobserved_revision(), )), )) let admitted = serialize_json(v: dispatch_preflight_json_value( preflight: preflight_verdict_for_report(report: report_of( receipts: [], - revision: converged_revision(), + revision: observed_revision(), )), )) return string_contains(s: refused, pattern: "\"status\": \"refused\"") @@ -220,11 +194,11 @@ test fn preflight_refuses_a_workdir_that_is_not_a_known_instance() -> Bool { test fn preflight_axes_are_derived_from_the_observations() -> Bool { let mixed = preflight_axes(report: report_of( receipts: [grounded_receipt(), ungrounded_receipt()], - revision: converged_revision(), + revision: observed_revision(), )) let bare = preflight_axes(report: report_of( receipts: [], - revision: drifted_revision(), + revision: unobserved_revision(), )) return count(mixed) == 3 && count(filter(mixed, a => axis_verdict_is_refused(verdict: a))) == 1 diff --git a/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag b/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag index 662e2ec2ba7..c11c95cd952 100644 --- a/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag @@ -139,7 +139,7 @@ import gunbc.roadmap_belt_actuate { import gunbc.roadmap_belt { ManualReady } import gunbc.roadmap_launch_admission { LaunchRefusal, - LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved, LaunchRevisionDrift, + LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved, LaunchAuthorityUnobserved, LaunchNodeUnknown, LaunchNodeSuperseded, LaunchAlreadyAccepted, LaunchReviewRequired, LaunchSizeMissing, LaunchExecutionContractMissing, LaunchDependenciesBlocked, LaunchSessionsUnobservable, LaunchAlreadyLive, LaunchStaleSession, LaunchLivenessUnobserved, LaunchSessionMultiplicityConflict, @@ -148,7 +148,7 @@ import gunbc.roadmap_launch_admission { LaunchGateVerdict, GatePassed, GateRefused, GateNotReached, launch_gate_live, launch_gate_revision, launch_identity_exemplar, - LaunchRevisionDrift, LaunchNodeUnknown, LaunchExecutionContractMissing, LaunchDependenciesBlocked, + LaunchNodeUnknown, LaunchExecutionContractMissing, LaunchDependenciesBlocked, LaunchHostStanding, LaunchGraphStanding, launch_graph_of_document, } import test.claim.roadmap.roadmap_launch_admission_fixture { @@ -166,7 +166,7 @@ import gunbc.roadmap_dashboard_instance { } import extdeps.git.object_store { GitObjectId, git_object_id_from_untagged_hex } import gunbc.fleet_desired_observe { - RevisionConverged, RevisionDrifted, RevisionUnobserved, + LocalTreeRevisionObserved, LocalTreeRevisionUnobserved, } import extdeps.languages.json.emit { serialize_json } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -701,7 +701,7 @@ fn band_key_for_label(label: String) -> String { } test fn witness_band_fold_pins_operator_vocabulary() -> Bool { - belt_dispatch_label_band_rows().length() == 23 + belt_dispatch_label_band_rows().length() == 22 && band_key_for_label(label: "stale_session") == "loud" && band_key_for_label(label: "dependencies_blocked") == "fail" && band_key_for_label(label: "execution_contract_missing") == "fail" @@ -712,7 +712,6 @@ test fn witness_band_fold_pins_operator_vocabulary() -> Bool { && band_key_for_label(label: "cause_not_permitted_by_mode") == "fail" && band_key_for_label(label: "gate_not_evaluated") == "loud" && band_key_for_label(label: "session_liveness_unobserved") == "loud" - && band_key_for_label(label: "deployed_revision_drifted") == "loud" && band_key_for_label(label: "deployed_revision_unobserved") == "loud" && band_key_for_label(label: "session_multiplicity_conflict") == "loud" && band_key_for_label(label: "deployment_transition_inhibited") == "loud" @@ -760,7 +759,6 @@ fn d4_wire_contract_labels() -> List { "disabled", "deployment_transition_inhibited", "deployed_revision_unobserved", - "deployed_revision_drifted", "authority_refused", "node_not_found", "node_superseded", @@ -1943,40 +1941,22 @@ test fn an_unsettled_receipt_is_re_observed_rather_than_halting_the_row() -> Boo } // --------------------------------------------------------------------------- -// PRESS-0 step 1 — the deployed-revision gate, at the grain it is actually proven. -// -// SCOPE FIRST, because the honest rung is the point: these witness the PROJECTION of the two new -// arms, not the live gate. `fleet_revision_standing` reads a real git tree, so whether the gate -// FIRES on a drifted srv1 is a wet fact owed a wet receipt — not established here. What IS -// established by execution: neither arm can reach the ok band, both carry a loud band, the drifted -// receipt names BOTH revisions, and the unobserved receipt names its cause. -// -// The `desired`/`local` assertion is the discriminating one. A refusal that dropped one side would -// still refuse, label correctly and be loud — and be useless to the operator deciding whether to -// catch the host up or roll it back. The membership-diff `from` ruling is exactly this: a receipt -// must name what it observed, not merely that it disagreed. Projection-grain only; the live gate -// is owed a wet srv1 receipt. +// The deployed-revision gate, at the grain it is actually proven. These witness the PROJECTION of +// the unobserved arm: it cannot reach the ok band, carries a loud band, and its receipt names its +// cause. The drift arm this block once covered is DELETED (operator ruling 2026-09-05, +// launch_revision_outcome): a readable-but-drifted tree is admitted, so there is no drift receipt +// to assert. Whether the gate fires on a host with an unreadable HEAD is a wet fact owed a wet +// receipt -- not established here. -test fn witness_revision_refusals_are_never_ok() -> Bool { - !belt_dispatch_result_ok(r: not_admitted(r: LaunchRevisionDrift { desired: "aaa", local: "bbb" })) - && !belt_dispatch_result_ok(r: not_admitted(r: LaunchRevisionUnobserved { cause: "no git" })) +test fn witness_revision_refusal_is_never_ok() -> Bool { + !belt_dispatch_result_ok(r: not_admitted(r: LaunchRevisionUnobserved { cause: "no git" })) } -test fn witness_revision_refusal_labels_pin_wire_contract() -> Bool { - belt_dispatch_status_label(r: not_admitted(r: LaunchRevisionDrift { desired: "aaa", local: "bbb" })) == "deployed_revision_drifted" - && belt_dispatch_status_label(r: not_admitted(r: LaunchRevisionUnobserved { cause: "no git" })) == "deployed_revision_unobserved" - && status_label_glued_to_ok(r: not_admitted(r: LaunchRevisionDrift { desired: "aaa", local: "bbb" })) +test fn witness_revision_refusal_label_pins_wire_contract() -> Bool { + belt_dispatch_status_label(r: not_admitted(r: LaunchRevisionUnobserved { cause: "no git" })) == "deployed_revision_unobserved" && status_label_glued_to_ok(r: not_admitted(r: LaunchRevisionUnobserved { cause: "no git" })) } -test fn witness_drift_receipt_names_both_revisions() -> Bool { - let wire = serialize_json(v: belt_spawn_outcome_json( - o: not_admitted(r: LaunchRevisionDrift { desired: "deadbeef", local: "cafebabe" }))) - string_contains(s: wire, pattern: "desired=deadbeef") - && string_contains(s: wire, pattern: "local=cafebabe") - && string_contains(s: wire, pattern: "\"status\": \"deployed_revision_drifted\"") -} - test fn witness_unobserved_receipt_names_its_cause() -> Bool { string_contains( s: serialize_json(v: belt_spawn_outcome_json( @@ -2028,33 +2008,22 @@ fn witness_oid_or_refuse(hex: String) -> GitObjectId? { } data witness_desired_hex: String = "1111111111111111111111111111111111111111" -data witness_local_hex: String = "2222222222222222222222222222222222222222" - -// The gate itself, not its output arms: deleting the fleet_revision_standing match -// would leave every constructed-arm witness green, so drive the fold directly. -test fn witness_revision_admission_fold_drives_all_three_arms() -> Bool { +// The gate itself, not its output arms: deleting the local_tree_revision match would leave every +// constructed-arm witness green, so drive the fold directly. DISCRIMINATING against the deleted +// drift arm: an observed revision passes whatever the fleet ref says, because the fold never sees +// the fleet ref at all. +test fn witness_revision_admission_fold_drives_both_arms() -> Bool { let unobserved = launch_gate_revision( - revision: RevisionUnobserved { cause: "HEAD undecodable" }) + revision: LocalTreeRevisionUnobserved { cause: "HEAD undecodable" }) match witness_oid_or_refuse(hex: witness_desired_hex) { - Present { value: d_oid } => - match witness_oid_or_refuse(hex: witness_local_hex) { - Present { value: l_oid } => { - let converged = launch_gate_revision( - revision: RevisionConverged { revision: d_oid }) - let drifted = launch_gate_revision( - revision: RevisionDrifted { desired: d_oid, local: l_oid }) - (match converged { GatePassed => true _ => false }) - && (match drifted { - GateRefused { refusal } => match refusal { LaunchRevisionDrift { desired: d, local: l } => d != l _ => false } - _ => false - }) - && (match unobserved { - GateRefused { refusal } => match refusal { LaunchRevisionUnobserved { cause: c } => c == "HEAD undecodable" _ => false } - _ => false - }) - } - Absent => false - } + Present { value: d_oid } => { + let observed = launch_gate_revision(revision: LocalTreeRevisionObserved { revision: d_oid }) + (match observed { GatePassed => true _ => false }) + && (match unobserved { + GateRefused { refusal } => match refusal { LaunchRevisionUnobserved { cause: c } => c == "HEAD undecodable" _ => false } + _ => false + }) + } Absent => false } } @@ -2257,7 +2226,7 @@ test fn revision_refusal_keeps_verify_independently_recorded() -> Bool { let receipt = belt_tick_receipt_from_result( result: BeltTickResult { wholesale: TickPassesRun, - host_refusal: Present { value: LaunchRevisionDrift { desired: "1111", local: "2222" } }, + host_refusal: Present { value: LaunchRevisionUnobserved { cause: "HEAD undecodable" } }, reconcile: belt_tick_empty_reconcile(state: ManualReady), spawn_outcomes: [], teardown_outcomes: [], @@ -2271,7 +2240,7 @@ test fn revision_refusal_keeps_verify_independently_recorded() -> Bool { observed_at: "2026-08-30T02:00:00Z", execution: TickExecuted { instance_id: "srv1-live", deployed_revision: "0123456789abcdef0123456789abcdef01234567", spawn_mode: ManualReady }, ) - (match receipt.spawn_pass { BeltPassRefused { reason } => string_contains(s: reason, pattern: "not the admitted fleet revision") _ => false }) + (match receipt.spawn_pass { BeltPassRefused { reason } => string_contains(s: reason, pattern: "deployed revision could not be observed") _ => false }) && (match receipt.teardown_pass { BeltPassRefused { reason: _ } => true _ => false }) && (match receipt.verify_pass { BeltPassRecorded => true _ => false }) && (match receipt.publish_pass { BeltPassRecorded => true _ => false }) diff --git a/dag/test/claim/roadmap/roadmap_launch_admission_fixture.dag b/dag/test/claim/roadmap/roadmap_launch_admission_fixture.dag index fa7d4eac4f1..e6e78449368 100644 --- a/dag/test/claim/roadmap/roadmap_launch_admission_fixture.dag +++ b/dag/test/claim/roadmap/roadmap_launch_admission_fixture.dag @@ -6,7 +6,7 @@ import gunbc.roadmap_document { RoadmapDocument, RoadmapSection } import gunbc.roadmap_execution_contract { WorkItemExecutionContract, gunbc_claim_execution_contract } import gunbc.roadmap_dashboard_instance { DashboardActuationPosture, DashboardActuating, DashboardObserveOnly } import gunbc.deploy_transition { ActuationAdmitted, ActuationRefused } -import gunbc.fleet_desired_observe { FleetRevisionStanding, RevisionConverged, RevisionDrifted, RevisionUnobserved } +import gunbc.fleet_desired_observe { LocalTreeRevision, LocalTreeRevisionObserved, LocalTreeRevisionUnobserved } import extdeps.git.object_store { GitObjectId, GitSha1ObjectId, git_object_id_from_untagged_hex } import std.content_hash { Sha1Digest, Sha1DigestHex } import gunbc.roadmap_dispatch_actuator { DispatchLiveSession, CodexDispatchProvider, dispatch_provider_instance_for_kind } @@ -17,46 +17,38 @@ import gunbc.roadmap_launch_admission { LaunchHostStanding, LaunchHostObserved, LaunchHostUnobserved, LaunchSessionsStanding, LaunchSessionsObserved, LaunchSessionsUnobserved, LaunchSessionsPending, LaunchCause, LaunchIdentity, - launch_host_halt_transition, launch_host_stage_revision, HostRevisionHalted, HostRevisionConverged, + launch_host_halt_transition, launch_host_stage_revision, HostRevisionHalted, HostRevisionObserved, } // Synthetic HOST facts for the launch-admission witnesses (RLM-1). The admission is pure over // LaunchHostStanding, so a witness fabricates exactly the host it wants -- an actuating instance, a -// clear transition, a converged (or drifted, or unobserved) revision, a chosen live-session set, a +// clear transition, an observed (or unobserved) revision, a chosen live-session set, a // capacity and a spawn mode -- and the graph it wants, and asks the same decision production asks. -// Nothing here touches a host; the two object ids are literal hex pinned by the fixture, never read +// Nothing here touches a host; the object id is literal hex pinned by the fixture, never read // from git. fn launch_fixture_instance_id() -> NonEmptyStr { "fixture-actuating-instance" as NonEmptyStr } data launch_fixture_desired_hex: String = "1111111111111111111111111111111111111111" -data launch_fixture_local_hex: String = "2222222222222222222222222222222222222222" fn launch_fixture_oid(hex: String) -> GitObjectId? { git_object_id_from_untagged_hex(hex: hex) } -fn launch_fixture_revision_converged() -> FleetRevisionStanding { +fn launch_fixture_revision_observed() -> LocalTreeRevision { match launch_fixture_oid(hex: launch_fixture_desired_hex) { - Present { value: oid } => RevisionConverged { revision: oid } - Absent => RevisionUnobserved { cause: "fixture object id did not decode" } + Present { value: oid } => LocalTreeRevisionObserved { revision: oid } + Absent => LocalTreeRevisionUnobserved { cause: "fixture object id did not decode" } } } -fn launch_fixture_revision_drifted() -> FleetRevisionStanding { - match launch_fixture_oid(hex: launch_fixture_desired_hex) { - Present { value: d } => - match launch_fixture_oid(hex: launch_fixture_local_hex) { - Present { value: l } => RevisionDrifted { desired: d, local: l } - Absent => RevisionUnobserved { cause: "fixture local object id did not decode" } - } - Absent => RevisionUnobserved { cause: "fixture desired object id did not decode" } - } +fn launch_fixture_revision_unobserved() -> LocalTreeRevision { + LocalTreeRevisionUnobserved { cause: "fixture: HEAD undecodable" } } fn launch_fixture_standing( mode: SpawnMode, - revision: FleetRevisionStanding, + revision: LocalTreeRevision, live: List, max_concurrent: Int, ) -> LaunchHostStanding { @@ -72,7 +64,7 @@ fn launch_fixture_standing( } fn launch_fixture_standing_clear(mode: SpawnMode) -> LaunchHostStanding { - launch_fixture_standing(mode: mode, revision: launch_fixture_revision_converged(), live: [], max_concurrent: 3) + launch_fixture_standing(mode: mode, revision: launch_fixture_revision_observed(), live: [], max_concurrent: 3) } fn launch_fixture_standing_observe_only() -> LaunchHostStanding { @@ -80,7 +72,7 @@ fn launch_fixture_standing_observe_only() -> LaunchHostStanding { instance_id: launch_fixture_instance_id(), actuation: DashboardObserveOnly, transition: ActuationAdmitted, - revision: launch_fixture_revision_converged(), + revision: launch_fixture_revision_observed(), sessions: LaunchSessionsObserved { live: [] }, capacity: BeltCapacity { max_concurrent: 3 }, mode: ManualReady, @@ -92,7 +84,7 @@ fn launch_fixture_standing_transition_inhibited() -> LaunchHostStanding { instance_id: launch_fixture_instance_id(), actuation: DashboardActuating, transition: ActuationRefused { reason: "fixture: deployment transition in force" }, - revision: launch_fixture_revision_converged(), + revision: launch_fixture_revision_observed(), sessions: LaunchSessionsObserved { live: [] }, capacity: BeltCapacity { max_concurrent: 3 }, mode: ManualReady, @@ -158,10 +150,10 @@ fn launch_fixture_standing_halt_transition() -> LaunchHostStanding { launch_host_halt_transition(instance_id: launch_fixture_instance_id(), reason: "fixture: deployment transition in force") } -fn launch_fixture_standing_halt_drift() -> LaunchHostStanding { - match launch_host_stage_revision(instance_id: launch_fixture_instance_id(), revision: launch_fixture_revision_drifted()) { +fn launch_fixture_standing_halt_unobserved() -> LaunchHostStanding { + match launch_host_stage_revision(instance_id: launch_fixture_instance_id(), revision: launch_fixture_revision_unobserved()) { HostRevisionHalted { standing } => standing - HostRevisionConverged { revision: _ } => LaunchHostUnobserved { reason: "fixture: a drifted revision was staged as converged" } + HostRevisionObserved { revision: _ } => LaunchHostUnobserved { reason: "fixture: an unreadable revision was staged as observed" } } } @@ -170,7 +162,7 @@ fn launch_fixture_standing_sessions_unobserved(mode: SpawnMode) -> LaunchHostSta instance_id: launch_fixture_instance_id(), actuation: DashboardActuating, transition: ActuationAdmitted, - revision: launch_fixture_revision_converged(), + revision: launch_fixture_revision_observed(), sessions: LaunchSessionsUnobserved { reason: "fixture: tmux ls refused" }, capacity: BeltCapacity { max_concurrent: 3 }, mode: mode, @@ -196,7 +188,7 @@ fn launch_fixture_standing_pending(mode: SpawnMode) -> LaunchHostStanding { instance_id: launch_fixture_instance_id(), actuation: DashboardActuating, transition: ActuationAdmitted, - revision: launch_fixture_revision_converged(), + revision: launch_fixture_revision_observed(), sessions: LaunchSessionsPending, capacity: BeltCapacity { max_concurrent: 3 }, mode: mode, diff --git a/dag/test/claim/roadmap/roadmap_launch_admission_witness_test.dag b/dag/test/claim/roadmap/roadmap_launch_admission_witness_test.dag index e89ae01ad5c..ddf96e5048f 100644 --- a/dag/test/claim/roadmap/roadmap_launch_admission_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_launch_admission_witness_test.dag @@ -27,7 +27,7 @@ import gunbc.roadmap_presentation { import gunbc.roadmap_launch_admission { LaunchCause, Operator, Timer, LaunchRefusal, - LaunchNoActuatingInstance, LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved, LaunchRevisionDrift, + LaunchNoActuatingInstance, LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved, LaunchAuthorityUnobserved, LaunchNodeUnknown, LaunchNodeSuperseded, LaunchAlreadyAccepted, LaunchReviewRequired, LaunchSizeMissing, LaunchExecutionContractMissing, LaunchDependenciesBlocked, LaunchSessionsUnobservable, LaunchAlreadyLive, LaunchStaleSession, LaunchLivenessUnobserved, LaunchSessionMultiplicityConflict, @@ -61,9 +61,9 @@ import test.claim.roadmap.roadmap_launch_admission_fixture { launch_fixture_instance_id, launch_fixture_desired_hex, launch_fixture_standing, launch_fixture_standing_clear, launch_fixture_standing_observe_only, launch_fixture_standing_transition_inhibited, - launch_fixture_revision_converged, launch_fixture_revision_drifted, + launch_fixture_revision_observed, launch_fixture_revision_unobserved, launch_fixture_session, - launch_fixture_standing_halt_transition, launch_fixture_standing_halt_drift, launch_fixture_standing_sessions_unobserved, + launch_fixture_standing_halt_transition, launch_fixture_standing_halt_unobserved, launch_fixture_standing_sessions_unobserved, launch_fixture_standing_pending, launch_fixture_identity, } @@ -273,13 +273,14 @@ fn action_of(a: LaunchAdmission) -> WorkAction? { dispatch_action(lifecycle: OpenLifecycle, admission: a).first() } -// (d) Drift refuses the UI availability and the backend actuation with ONE label and ONE reason. -test fn witness_drift_refuses_ui_and_backend_with_one_reason() -> Bool { - let drifted = launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_drifted(), live: [], max_concurrent: 3) +// (d) An unreadable revision refuses the UI availability and the backend actuation with ONE label +// and ONE reason. (Drift no longer refuses at all -- operator ruling 2026-09-05, launch_revision_outcome.) +test fn witness_unobserved_revision_refuses_ui_and_backend_with_one_reason() -> Bool { + let drifted = launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_unobserved(), live: [], max_concurrent: 3) let ui = decide(cause: Operator, standing: drifted, g: graph(), node_id: parent_id()) let backend = belt_tick_admit_and_spawn( candidates: [ DesiredMember { node_id: parent_id(), node: canary_parent(signoff: Unsigned) } ], - standing: launch_fixture_standing(mode: AutomaticReady, revision: launch_fixture_revision_drifted(), live: [], max_concurrent: 3), + standing: launch_fixture_standing(mode: AutomaticReady, revision: launch_fixture_revision_unobserved(), live: [], max_concurrent: 3), graph: graph(), merged: [], workdir: "/nonexistent/launch-admission-witness", @@ -288,7 +289,7 @@ test fn witness_drift_refuses_ui_and_backend_with_one_reason() -> Bool { Absent => false Present { value: action } => (match action.availability { ActionUnavailable => true ActionAvailable => false }) - && action.label == "deployed_revision_drifted" + && action.label == "deployed_revision_unobserved" && (match backend.first() { Absent => false Present { value: o } => @@ -297,7 +298,7 @@ test fn witness_drift_refuses_ui_and_backend_with_one_reason() -> Bool { SpawnNotAdmitted { node_id: _, refusal } => launch_refusal_reason(r: refusal) == action.detail && (match refusal { - LaunchRevisionDrift { desired, local } => desired != local + LaunchRevisionUnobserved { cause } => cause != "" _ => false }) _ => false @@ -327,7 +328,7 @@ test fn witness_second_operator_admission_is_already_live() -> Bool { let running = launch_fixture_session(node_id: parent_id(), process: WorkerProcessRunning { command: "codex" as NonEmptyStr }) let second = decide( cause: Operator, - standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_converged(), live: [running], max_concurrent: 3), + standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_observed(), live: [running], max_concurrent: 3), g: graph(), node_id: parent_id(), ) @@ -351,7 +352,7 @@ test fn witness_retained_dead_pane_refuses_stale_not_live() -> Bool { refused_as( a: decide( cause: Operator, - standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_converged(), live: [dead], max_concurrent: 3), + standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_observed(), live: [dead], max_concurrent: 3), g: graph(), node_id: parent_id(), ), @@ -362,8 +363,8 @@ test fn witness_retained_dead_pane_refuses_stale_not_live() -> Bool { test fn witness_two_sessions_for_one_node_refuse_multiplicity() -> Bool { let a = launch_fixture_session(node_id: parent_id(), process: WorkerProcessRunning { command: "codex" as NonEmptyStr }) let b = launch_fixture_session(node_id: parent_id(), process: WorkerProcessExited { exit_code: 0, last_command: "codex" }) - let forward = decide(cause: Operator, standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_converged(), live: [a, b], max_concurrent: 3), g: graph(), node_id: parent_id()) - let reverse = decide(cause: Operator, standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_converged(), live: [b, a], max_concurrent: 3), g: graph(), node_id: parent_id()) + let forward = decide(cause: Operator, standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_observed(), live: [a, b], max_concurrent: 3), g: graph(), node_id: parent_id()) + let reverse = decide(cause: Operator, standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_observed(), live: [b, a], max_concurrent: 3), g: graph(), node_id: parent_id()) refused_as(a: forward, label: "session_multiplicity_conflict") && refused_as(a: reverse, label: "session_multiplicity_conflict") } @@ -377,8 +378,8 @@ test fn witness_capacity_refuses_operator_too() -> Bool { launch_fixture_session(node_id: "other-1", process: WorkerProcessRunning { command: "codex" as NonEmptyStr }), launch_fixture_session(node_id: "other-2", process: WorkerProcessRunning { command: "codex" as NonEmptyStr }), ] - let full = decide(cause: Operator, standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_converged(), live: occupied, max_concurrent: 2), g: graph(), node_id: parent_id()) - let room = decide(cause: Operator, standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_converged(), live: occupied, max_concurrent: 3), g: graph(), node_id: parent_id()) + let full = decide(cause: Operator, standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_observed(), live: occupied, max_concurrent: 2), g: graph(), node_id: parent_id()) + let room = decide(cause: Operator, standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_observed(), live: occupied, max_concurrent: 3), g: graph(), node_id: parent_id()) (match full { LaunchRefused { refusal } => match refusal { LaunchNoCapacity { occupied: n, max_concurrent: m } => n == 2 && m == 2 _ => false } LaunchAdmitted { identity: _ } => false @@ -419,8 +420,8 @@ test fn witness_transition_gate_precedes_dependencies() -> Bool { test fn witness_revision_gate_precedes_node_gates() -> Bool { refused_as( - a: decide(cause: Operator, standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_drifted(), live: [], max_concurrent: 3), g: graph(), node_id: "no-such-node"), - label: "deployed_revision_drifted", + a: decide(cause: Operator, standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_unobserved(), live: [], max_concurrent: 3), g: graph(), node_id: "no-such-node"), + label: "deployed_revision_unobserved", ) } @@ -470,7 +471,7 @@ fn labels_distinct(labels: List) -> Bool { test fn witness_refusal_labels_are_distinct_and_total() -> Bool { let labels = map(launch_refusal_exemplars(), r => launch_refusal_label(r: r)) - count(labels) == 21 && labels_distinct(labels: labels) + count(labels) == 20 && labels_distinct(labels: labels) } test fn witness_contract_text_carries_gate_order_and_digest_is_hex16() -> Bool { @@ -540,7 +541,7 @@ fn nth(ds: List, n: Int) -> BeltLaunchDecision? { test fn witness_three_candidates_one_slot_all_reach_admission() -> Bool { let ds = belt_tick_admit( candidates: rlm_three_candidates(), - standing: launch_fixture_standing(mode: AutomaticReady, revision: launch_fixture_revision_converged(), live: [], max_concurrent: 1), + standing: launch_fixture_standing(mode: AutomaticReady, revision: launch_fixture_revision_observed(), live: [], max_concurrent: 1), graph: rlm_three_graph(), merged: [], ) @@ -562,7 +563,7 @@ test fn witness_three_candidates_one_slot_all_reach_admission() -> Bool { test fn witness_three_candidates_manual_ready_all_reach_cause_gate() -> Bool { let ds = belt_tick_admit( candidates: rlm_three_candidates(), - standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_converged(), live: [], max_concurrent: 1), + standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_observed(), live: [], max_concurrent: 1), graph: rlm_three_graph(), merged: [], ) @@ -615,12 +616,12 @@ test fn witness_timer_pass_transition_halt_refuses_every_desired_node() -> Bool && page_label_and_reason_match(standing: standing, label: "deployment_transition_inhibited", pass: pass) } -// (2) Revision drift staged by the observer's own producer, before any session observation. -test fn witness_timer_pass_revision_drift_refuses_before_sessions() -> Bool { - let standing = launch_fixture_standing_halt_drift() +// (2) An unreadable revision staged by the observer's own producer, before any session observation. +test fn witness_timer_pass_revision_unobserved_refuses_before_sessions() -> Bool { + let standing = launch_fixture_standing_halt_unobserved() let pass = timer_pass_over_canary(standing: standing) - pass_withholds_every_desired_as(pass: pass, label: "deployed_revision_drifted") - && page_label_and_reason_match(standing: standing, label: "deployed_revision_drifted", pass: pass) + pass_withholds_every_desired_as(pass: pass, label: "deployed_revision_unobserved") + && page_label_and_reason_match(standing: standing, label: "deployed_revision_unobserved", pass: pass) && (match standing { LaunchHostHalted { instance_id: _, gate: HostRevisionGate, refusal: _ } => true _ => false }) } @@ -662,7 +663,7 @@ test fn witness_unobserved_process_occupies_a_slot() -> Bool { let unknown = launch_fixture_session(node_id: "some-other-node", process: WorkerProcessUnobserved { reason: "pane unreadable" as NonEmptyStr }) match decide( cause: Operator, - standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_converged(), live: [unknown], max_concurrent: 1), + standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_observed(), live: [unknown], max_concurrent: 1), g: graph(), node_id: parent_id(), ) { @@ -681,7 +682,7 @@ test fn witness_exited_or_absent_process_frees_its_slot() -> Bool { fn one_slot_admits_parent_beside(live: List) -> Bool { match decide( cause: Operator, - standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_converged(), live: live, max_concurrent: 1), + standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_observed(), live: live, max_concurrent: 1), g: graph(), node_id: parent_id(), ) { @@ -761,6 +762,7 @@ test fn witness_tick_receipt_round_trips_launch_identities() -> Bool { launches: [id], spawn_pass: BeltPassRecorded, teardown_pass: BeltPassRecorded, + commit_pass: BeltPassRecorded, verify_pass: BeltPassRecorded, publish_pass: BeltPassRecorded, } @@ -874,7 +876,7 @@ test fn witness_identity_duplicated_member_names_the_duplication() -> Bool { test fn witness_withheld_identity_carries_the_withholding_gates_refusal() -> Bool { let drifted = launch_decision_facts( cause: Operator, - standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_drifted(), live: [], max_concurrent: 3), + standing: launch_fixture_standing(mode: ManualReady, revision: launch_fixture_revision_unobserved(), live: [], max_concurrent: 3), graph: graph(), merged: [], node_id: parent_id(), reserved: 0, ) let unknown = launch_decision_facts( @@ -887,8 +889,8 @@ test fn witness_withheld_identity_carries_the_withholding_gates_refusal() -> Boo ) (match drifted.identity { LaunchIdentityWithheld { refusal } => - launch_refusal_label(r: refusal) == "deployed_revision_drifted" - && (match drifted.ledger.revision { GateRefused { refusal: r } => launch_refusal_label(r: r) == "deployed_revision_drifted" _ => false }) + launch_refusal_label(r: refusal) == "deployed_revision_unobserved" + && (match drifted.ledger.revision { GateRefused { refusal: r } => launch_refusal_label(r: r) == "deployed_revision_unobserved" _ => false }) LaunchIdentityBound { identity: _ } => false }) && (match unknown.identity { diff --git a/dag/test/claim/roadmap/roadmap_serve_witness_test.dag b/dag/test/claim/roadmap/roadmap_serve_witness_test.dag index 5b25ee9eb8b..51c8663fec5 100644 --- a/dag/test/claim/roadmap/roadmap_serve_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_serve_witness_test.dag @@ -27,7 +27,7 @@ import gunbc.roadmap_belt_actuate { } import gunbc.roadmap_launch_admission { LaunchRefusal, launch_identity_exemplar, - LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved, LaunchRevisionDrift, + LaunchInstanceActuationRefused, LaunchTransitionInhibited, LaunchRevisionUnobserved, LaunchAuthorityUnobserved, LaunchNodeUnknown, LaunchAlreadyAccepted, LaunchSessionsUnobservable, LaunchAlreadyLive, LaunchStaleSession, LaunchLivenessUnobserved, LaunchSessionMultiplicityConflict, LaunchDependenciesBlocked, LaunchExecutionContractMissing, @@ -198,7 +198,6 @@ test fn witness_dispatch_status_total() -> Bool { && belt_dispatch_status(r: na(r: LaunchAuthorityUnobserved { detail: "d" })) == 503 && belt_dispatch_status(r: SpawnFailed { node_id: "n", step: "git", detail: "d", provider: "codex", launch: launch_identity_exemplar() }) == 502 && belt_dispatch_status(r: na(r: LaunchStaleSession { session_name: "s", detail: "d" })) == 409 - && belt_dispatch_status(r: na(r: LaunchRevisionDrift { desired: "a", local: "b" })) == 409 && belt_dispatch_status(r: na(r: LaunchSessionMultiplicityConflict { session_names: ["s1", "s2"], session_count: 2 })) == 409 && belt_dispatch_status(r: na(r: LaunchLivenessUnobserved { session_name: "s", reason: "r" })) == 503 && belt_dispatch_status(r: na(r: LaunchRevisionUnobserved { cause: "c" })) == 503