diff --git a/dag/gunbc/roadmap/roadmap_attempt_request_binding.dag b/dag/gunbc/roadmap/roadmap_attempt_request_binding.dag new file mode 100644 index 00000000000..76ce439309e --- /dev/null +++ b/dag/gunbc/roadmap/roadmap_attempt_request_binding.dag @@ -0,0 +1,135 @@ +module gunbc.roadmap.roadmap_attempt_request_binding + +import std.types { String, Bool, List, NonEmptyStr } +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.roadmap_dashboard_instance { HostDashboardInstance } +import gunbc.roadmap.roadmap_event_log { + RoadmapEventEnvelope, RoadmapEventId, roadmap_claim_return_to, roadmap_history_unordered_reason, +} +import gunbc.roadmap.roadmap_event_carrier { + roadmap_event_carrier_layout_for_instance, roadmap_events_read, RoadmapEventsRead, EventsRead, EventsReadRefused, +} +import gunbc.roadmap.roadmap_attempt_request_record { + AttemptRequestBinding, RequestClaimBound, RequestAutonomous, + AttemptRequestBindingRead, RequestBindingRead, RequestBindingAbsent, RequestBindingUnreadable, + AttemptRequestBindingCommit, RequestBindingCommitted, RequestBindingCommitRefused, + attempt_request_binding_read_for_instance, attempt_request_binding_create_for_instance, +} + +// THE REQUEST A LAUNCH WAS DECIDED FOR, CARRIED FROM THE DECISION. An attempt exists because a +// dispatch decision admitted it. When that decision was caused by someone assigning the issue, the +// assignment is one Claimed event on the issue's log, and its id is the request's identity: the +// assign route holds that id the moment the claim is appended and hands it to the dispatch it +// triggers. This type is that hand-off. It is NOT read back from the issue later -- by the time an +// attempt initializes, another claim may be the current one, and binding whichever claim is current +// would address this attempt's result to a request that did not cause it. A dispatch nobody's claim +// caused (the timer's, or an operator pressing dispatch) is Autonomous when it starts a lineage: its +// own launch class, with no requester, never a request whose identity went unobserved. +// +// A CONTINUATION IS NEITHER. When such a dispatch turns out to continue an earlier attempt's +// lineage, that lineage may have a requester, and calling the new turn autonomous would erase the +// obligation -- its result would never return. Continuing names the attempt it continues, and the +// new attempt's binding is a COPY of that attempt's recorded, create-only binding +// (attempt_launch_request_for_origin decides the class; attempt_request_binding_admission does the +// copy). That carries a recorded fact forward along the lineage; it reads no issue state, so the +// claim current at the time of the continuation has no way in. +type AttemptLaunchRequest + = LaunchForClaim { node: RoadmapNodeId, claim: RoadmapEventId } + | LaunchContinuing { predecessor_attempt_key: String } + | LaunchAutonomous + +// THE LAUNCH CLASS ONCE THE LINEAGE IS KNOWN. The dispatch decision supplies the request; the +// continuation decision, taken later in the spawn, says whether this attempt continues a +// predecessor. A launch for a claim stays a launch for that claim even when it continues a lineage: +// a new assignment caused it, and it answers that assignment. A launch nobody's claim caused becomes +// Continuing when there is a predecessor and stays Autonomous when there is none. A Continuing +// request is already what it is. +fn attempt_launch_request_for_origin(request: AttemptLaunchRequest, predecessor_attempt_key: String?) -> AttemptLaunchRequest { + match request { + LaunchForClaim { node: _, claim: _ } => request + LaunchContinuing { predecessor_attempt_key: _ } => request + LaunchAutonomous => match predecessor_attempt_key { + Present { value: key } => LaunchContinuing { predecessor_attempt_key: key } + Absent => LaunchAutonomous + } + } +} + +// WHETHER THE CARRIED REQUEST CAN BE BOUND TO THIS ATTEMPT, pure over the issue's log and the +// predecessor's binding as read. An autonomous launch binds as autonomous and consults nothing. A +// continuing launch takes exactly the binding its predecessor recorded -- bound to the same claim, +// or autonomous -- and refuses when the predecessor has no readable binding, because a continuation +// whose lineage's request is unknown would run unable to answer anyone (the remedy is a new +// assignment, which launches for its own claim). A launch for a claim binds only when +// the request names THIS attempt's node and the claim is a Claimed event on that node's readable, +// ordered history. Everything else refuses and says which: a request for another node, a log that +// could not be read, a history that forks or is incomplete, a claim id the history does not carry +// as a claim. The log is read to ESTABLISH the claim the decision carried, never to choose one, so +// a newer claim on the same issue changes nothing here. +type AttemptRequestBindingAdmission + = RequestBindingAdmitted { binding: AttemptRequestBinding } + | RequestBindingNotAdmitted { step: String, detail: String } + +fn attempt_request_binding_admission(request: AttemptLaunchRequest, node_id: RoadmapNodeId, read: RoadmapEventsRead, predecessor: AttemptRequestBindingRead) -> AttemptRequestBindingAdmission { + match request { + LaunchAutonomous => RequestBindingAdmitted { binding: RequestAutonomous } + LaunchContinuing { predecessor_attempt_key } => + match predecessor { + RequestBindingRead { binding } => RequestBindingAdmitted { binding: binding } + RequestBindingAbsent => + RequestBindingNotAdmitted { step: "request-predecessor", detail: join(["this attempt continues attempt ", predecessor_attempt_key, " of issue ", node_id as String, ", which recorded no request binding, so the request this lineage answers is unknown; assign the issue again to continue it under a recorded request"], "") } + RequestBindingUnreadable { reason } => + RequestBindingNotAdmitted { step: "request-predecessor", detail: join(["this attempt continues attempt ", predecessor_attempt_key, " of issue ", node_id as String, ", whose request binding cannot be read: ", reason], "") } + } + LaunchForClaim { node, claim } => + if (node as String) != (node_id as String) { + RequestBindingNotAdmitted { step: "request-node", detail: join(["the launch request is for issue ", node as String, " and cannot bind an attempt of issue ", node_id as String], "") } + } else { + match read { + EventsReadRefused { node: _, step, reason } => + RequestBindingNotAdmitted { step: "request-log", detail: join(["the issue's event log could not be read (", step, "), so the claim this launch answers cannot be established: ", reason], "") } + EventsRead { node: _, envelopes } => + match roadmap_history_unordered_reason(envs: envelopes) { + Present { value: why } => + RequestBindingNotAdmitted { step: "request-history", detail: join(["the issue's history cannot be ordered, so the claim this launch answers cannot be established: ", why], "") } + Absent => + match roadmap_claim_return_to(envs: envelopes, claim: claim) { + Present { value: _ } => RequestBindingAdmitted { binding: RequestClaimBound { claim: claim } } + Absent => RequestBindingNotAdmitted { step: "request-claim", detail: join(["the launch request names claim ", claim as String, ", which is not a claim on issue ", node_id as String, "'s history"], "") } + } + } + } + } + } +} + +// THE LAUNCH-TIME COMMIT (gunbc.roadmap_belt_actuate belt_attempt_state_initialize_for_instance, +// before the worker starts). The request arrives from the dispatch decision; the issue's log is +// read only when the request names a claim, to establish it, and the predecessor's binding only +// when the launch continues one (same issue, by the path it is read from); the binding is written +// create-only. +// A refusal here fails the launch. +fn attempt_request_binding_commit_for_instance(instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String, request: AttemptLaunchRequest) -> AttemptRequestBindingCommit { + let admission = match request { + LaunchAutonomous => attempt_request_binding_admission(request: request, node_id: node_id, read: EventsRead { node: node_id, envelopes: [] }, predecessor: RequestBindingAbsent) + LaunchContinuing { predecessor_attempt_key } => + attempt_request_binding_admission( + request: request, + node_id: node_id, + read: EventsRead { node: node_id, envelopes: [] }, + predecessor: attempt_request_binding_read_for_instance(instance: instance, node_id: node_id, attempt_key: predecessor_attempt_key), + ) + LaunchForClaim { node: _, claim: _ } => + attempt_request_binding_admission( + request: request, + node_id: node_id, + read: roadmap_events_read(layout: roadmap_event_carrier_layout_for_instance(instance: instance), node: node_id), + predecessor: RequestBindingAbsent, + ) + } + match admission { + RequestBindingNotAdmitted { step, detail } => RequestBindingCommitRefused { step: step, detail: detail } + RequestBindingAdmitted { binding } => + attempt_request_binding_create_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key, binding: binding) + } +} diff --git a/dag/gunbc/roadmap/roadmap_attempt_request_record.dag b/dag/gunbc/roadmap/roadmap_attempt_request_record.dag new file mode 100644 index 00000000000..f5a48600a70 --- /dev/null +++ b/dag/gunbc/roadmap/roadmap_attempt_request_record.dag @@ -0,0 +1,174 @@ +module gunbc.roadmap.roadmap_attempt_request_record + +import std.types { String, Bool, List, NonEmptyStr } +import extdeps.filesystem.filesystem_io { + Filesystem, FilesystemCreateNew, FilesystemCreated, FilesystemCreateTargetOccupied, FilesystemCreateRefused, + FilesystemCreateKindUnrecognized, filesystem_create_new, +} +import extdeps.languages.json.emit { JsonValue, JsonNull, JsonBool, JsonNumber, JsonString, JsonArray, JsonObject, json_object, json_kv, json_string, serialize_json } +import extdeps.languages.json.parse { + parse_json_document, JsonDocumentParsed, JsonDocumentUnreadable, json_document_gap_text, + json_object_unique_member, JsonMemberFound, JsonMemberAbsent, JsonMemberDuplicated, JsonMemberNotAnObject, +} +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.roadmap_dashboard_instance { HostDashboardInstance } +import gunbc.roadmap_dispatch_actuator { dispatch_attempt_state_path_for_instance } +import gunbc.roadmap.roadmap_event_log { RoadmapEventId } + +// THE ATTEMPT'S REQUEST RECORD: the durable, create-only fact of which request an attempt answers. +// It is its own module, below the event carrier, because two readers on opposite sides of that +// carrier need it: the launch (gunbc.roadmap.roadmap_attempt_request_binding), which decides the +// binding with the carrier's help and commits it here, and the carrier's bound result append +// (gunbc.roadmap.roadmap_event_carrier roadmap_bound_result_append), which reads it back to learn +// the claim a result must name -- so that the claim on a result is never something a caller hands +// in. This module knows the record, its codec, its path and its two operations, and nothing about +// events. +// THE BINDING AN ATTEMPT RECORDS, ONCE. Bound names the claim event; Autonomous is an attempt with +// no requester. There is no third state: a request-bound launch whose claim cannot be established +// does not launch (gunbc.roadmap.roadmap_attempt_request_binding attempt_request_binding_commit_for_instance refuses), because an attempt that +// ran without knowing who asked could finish and never be able to answer. +type AttemptRequestBinding + = RequestClaimBound { claim: RoadmapEventId } + | RequestAutonomous + +data attempt_request_binding_schema: String = "roadmap-attempt-request-binding/v1" +data attempt_request_binding_basename: String = "request-binding.json" + +fn attempt_request_binding_path_for_instance(instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String) -> String { + join([dispatch_attempt_state_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), "/", attempt_request_binding_basename], "") +} + +fn attempt_request_binding_json(binding: AttemptRequestBinding) -> String { + serialize_json(v: json_object(members: match binding { + RequestClaimBound { claim } => [ + json_kv(key: "schema", value: json_string(s: attempt_request_binding_schema)), + json_kv(key: "status", value: json_string(s: "bound")), + json_kv(key: "claim", value: json_string(s: claim as String)), + ] + RequestAutonomous => [ + json_kv(key: "schema", value: json_string(s: attempt_request_binding_schema)), + json_kv(key: "status", value: json_string(s: "autonomous")), + ] + })) +} + +fn binding_member_string(doc: JsonValue, key: String) -> String? { + match json_object_unique_member(v: doc, key: key) { + JsonMemberFound { value: JsonString { value: s } } => Present { value: s } + JsonMemberFound { value: JsonNull } => none + JsonMemberFound { value: JsonBool { value: _ } } => none + JsonMemberFound { value: JsonNumber { lexeme: _ } } => none + JsonMemberFound { value: JsonArray { elements: _ } } => none + JsonMemberFound { value: JsonObject { members: _ } } => none + JsonMemberAbsent => none + JsonMemberDuplicated { count: _ } => none + JsonMemberNotAnObject => none + } +} + +type AttemptRequestBindingDecode + = RequestBindingDecoded { binding: AttemptRequestBinding } + | RequestBindingUndecodable { reason: String } + +// THE DECODE IS STRICT: the schema must be this one, the status one of the two, and a bound record +// must name a non-empty claim. A document that is anything else is undecodable, never autonomous. +fn attempt_request_binding_decode(text: String) -> AttemptRequestBindingDecode { + match parse_json_document(s: text) { + JsonDocumentUnreadable { gap } => RequestBindingUndecodable { reason: join(["not JSON: ", json_document_gap_text(gap: gap)], "") } + JsonDocumentParsed { value: doc } => + match binding_member_string(doc: doc, key: "schema") { + Absent => RequestBindingUndecodable { reason: "the binding has no schema member" } + Present { value: schema } => + if schema != attempt_request_binding_schema { + RequestBindingUndecodable { reason: join(["schema is ", schema, ", expected ", attempt_request_binding_schema], "") } + } else { + match binding_member_string(doc: doc, key: "status") { + Absent => RequestBindingUndecodable { reason: "the binding has no status member" } + Present { value: status } => + if status == "bound" { + match binding_member_string(doc: doc, key: "claim") { + Absent => RequestBindingUndecodable { reason: "a bound binding names no claim" } + Present { value: claim } => + if claim == "" { RequestBindingUndecodable { reason: "a bound binding names an empty claim" } } + else { RequestBindingDecoded { binding: RequestClaimBound { claim: claim as NonEmptyStr as RoadmapEventId } } } + } + } else if status == "autonomous" { + RequestBindingDecoded { binding: RequestAutonomous } + } else { + RequestBindingUndecodable { reason: join(["status ", status, " is not bound or autonomous"], "") } + } + } + } + } + } +} + +// THE CREATE-ONLY COMMIT, pure over what the create answered and what is on disk. Created is the +// first commit. An occupied target is read back: the SAME binding is an idempotent success, so a +// repeated initialization of the same decision changes nothing; a DIFFERENT binding, or a file that +// does not decode, refuses -- the first commit stands and a later initialization under another +// claim cannot replace it. Any other refusal of the create is the host's and fails the launch. +type AttemptRequestBindingCommit + = RequestBindingCommitted { binding: AttemptRequestBinding } + | RequestBindingCommitRefused { step: String, detail: String } + +fn attempt_request_binding_commit_of(binding: AttemptRequestBinding, created: FilesystemCreateNew, existing: AttemptRequestBindingRead) -> AttemptRequestBindingCommit { + match created { + FilesystemCreated { path: _ } => RequestBindingCommitted { binding: binding } + FilesystemCreateRefused { path: _, kind: _, error } => RequestBindingCommitRefused { step: "attempt-request-binding-persist", detail: error } + FilesystemCreateKindUnrecognized { path: _, observed, error } => RequestBindingCommitRefused { step: "attempt-request-binding-persist", detail: join([observed, ": ", error], "") } + FilesystemCreateTargetOccupied { path: _ } => + match existing { + RequestBindingRead { binding: recorded } => + if recorded == binding { RequestBindingCommitted { binding: recorded } } + else { RequestBindingCommitRefused { step: "attempt-request-binding-conflict", detail: join(["the attempt is already bound (", attempt_request_binding_json(binding: recorded), ") and cannot be re-bound (", attempt_request_binding_json(binding: binding), ")"], "") } } + RequestBindingAbsent => RequestBindingCommitRefused { step: "attempt-request-binding-persist", detail: "the binding could not be created because the target was occupied, and then could not be found" } + RequestBindingUnreadable { reason } => RequestBindingCommitRefused { step: "attempt-request-binding-conflict", detail: join(["the attempt already has a binding that cannot be read, which is not replaced: ", reason], "") } + } + } +} + +// THE CREATE-ONLY WRITE: publish the binding with a create that fails when the file exists, then +// decide with attempt_request_binding_commit_of over what the create answered and, only when the +// target was occupied, what is recorded there. +fn attempt_request_binding_create_for_instance(instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String, binding: AttemptRequestBinding) -> AttemptRequestBindingCommit { + let path = attempt_request_binding_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key) + let write = Filesystem.WriteCreateNew(path: path, content: attempt_request_binding_json(binding: binding)) + let created = filesystem_create_new(path: write.path, success: write.success, error: write.error, error_kind: write.error_kind) + attempt_request_binding_commit_of( + binding: binding, + created: created, + existing: match created { + FilesystemCreateTargetOccupied { path: _ } => attempt_request_binding_read_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key) + FilesystemCreated { path: _ } => RequestBindingAbsent + FilesystemCreateRefused { path: _, kind: _, error: _ } => RequestBindingAbsent + FilesystemCreateKindUnrecognized { path: _, observed: _, error: _ } => RequestBindingAbsent + }, + ) +} + +// THE BINDING AS A LATER READER FINDS IT. Absent is an attempt launched before bindings were +// recorded (or one whose state was never initialized): it has no recorded request, which is its +// own arm and not autonomous. Unreadable is a file that is there and cannot be read or decoded. +type AttemptRequestBindingRead + = RequestBindingRead { binding: AttemptRequestBinding } + | RequestBindingAbsent + | RequestBindingUnreadable { reason: String } + +fn attempt_request_binding_read_of(success: Bool, error_kind: String, error: String, content: String) -> AttemptRequestBindingRead { + if success { + match attempt_request_binding_decode(text: content) { + RequestBindingDecoded { binding } => RequestBindingRead { binding: binding } + RequestBindingUndecodable { reason } => RequestBindingUnreadable { reason: reason } + } + } else if error_kind == "not_found" { + RequestBindingAbsent + } else { + RequestBindingUnreadable { reason: error } + } +} + +fn attempt_request_binding_read_for_instance(instance: HostDashboardInstance, node_id: RoadmapNodeId, attempt_key: String) -> AttemptRequestBindingRead { + let read = Filesystem.Read(path: attempt_request_binding_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key)) + attempt_request_binding_read_of(success: read.success, error_kind: read.error_kind, error: read.error, content: read.content) +} diff --git a/dag/gunbc/roadmap/roadmap_belt_actuate.dag b/dag/gunbc/roadmap/roadmap_belt_actuate.dag index 5a18ede4724..f316d46bfdc 100644 --- a/dag/gunbc/roadmap/roadmap_belt_actuate.dag +++ b/dag/gunbc/roadmap/roadmap_belt_actuate.dag @@ -1,5 +1,9 @@ module gunbc.roadmap_belt_actuate +import gunbc.roadmap.roadmap_attempt_request_binding { + AttemptLaunchRequest, LaunchForClaim, LaunchAutonomous, attempt_launch_request_for_origin, + attempt_request_binding_commit_for_instance, RequestBindingCommitted, RequestBindingCommitRefused, +} import gunbc.roadmap_nesting { roadmap_memberships } import std.algebra { trim } @@ -734,6 +738,7 @@ fn belt_attempt_state_initialize_for_instance( launch: LaunchIdentity, turn: Int, binding: AlignmentBinding, + request: AttemptLaunchRequest, ) -> AttemptStateInitialization { let key_write = Filesystem.Write( path: dispatch_attempt_key_path_for_instance( @@ -852,7 +857,12 @@ fn belt_attempt_state_initialize_for_instance( node: node, attempt_key: attempt_key, ) { - SpawnOriginRecorded => AttemptStateInitialized + SpawnOriginRecorded => + match attempt_request_binding_commit_for_instance(instance: instance, node_id: node.node, attempt_key: attempt_key, request: request) { + RequestBindingCommitted { binding: _ } => AttemptStateInitialized + RequestBindingCommitRefused { step, detail } => + AttemptStateInitializationFailed { step: step, detail: detail } + } SpawnOriginRecordFailed { step, detail } => AttemptStateInitializationFailed { step: step, detail: detail } } @@ -2006,6 +2016,7 @@ fn belt_actuate_spawn_for_instance( node: RoadmapNode, probed_at: String, launch: LaunchIdentity, + request: AttemptLaunchRequest, ) -> BeltSpawnOutcome { match belt_footprint_admission_for_instance(instance: instance) { FootprintRefused { step: fst, detail: fd } => @@ -2075,6 +2086,7 @@ fn belt_actuate_spawn_for_instance( provider_wire_label: belt_dispatch_spawn_provider_wire_label(provider: provider), launch: launch, selection: selection, + request: request, ) } } @@ -2091,6 +2103,7 @@ fn belt_actuate_spawn_exec_for_instance( provider_wire_label: String, launch: LaunchIdentity, selection: DispatchActuatorSelection, + request: AttemptLaunchRequest, ) -> BeltSpawnOutcome { belt_actuate_spawn_exec_modeled( instance: instance, @@ -2101,9 +2114,19 @@ fn belt_actuate_spawn_exec_for_instance( provider_wire_label: provider_wire_label, launch: launch, selection: selection, + request: request, ) } +// The attempt a worker origin continues, if any: the predecessor whose recorded request binding a +// continuing launch copies (gunbc.roadmap.roadmap_attempt_request_binding attempt_launch_request_for_origin). +fn belt_origin_predecessor_key(origin: AttemptOrigin) -> String? { + match origin { + FreshFromBase => none + ContinueFromAttempt { parent_attempt_key, branch: _, turn: _, verdict_detail: _ } => Present { value: parent_attempt_key } + } +} + // The tmux creation boundary is explicit: worktree/state publication first; tmux new-session alone; // only after its success do session configuration, pipe attachment and provider respawn run. A // later staging failure reaps exactly the session this request created, no name-only ownership @@ -2150,6 +2173,7 @@ fn belt_actuate_spawn_exec_modeled( provider_wire_label: String, launch: LaunchIdentity, selection: DispatchActuatorSelection, + request: AttemptLaunchRequest, ) -> BeltSpawnOutcome { let attempt_key = dispatch_attempt_key( node_id: node.node, @@ -2179,6 +2203,7 @@ fn belt_actuate_spawn_exec_modeled( provider_wire_label: provider_wire_label, launch: launch, probed_at: probed_at, attempt_key: attempt_key, session_name: session_name, origin: origin, selection: selection, + request: attempt_launch_request_for_origin(request: request, predecessor_attempt_key: belt_origin_predecessor_key(origin: origin)), ) } } @@ -2738,6 +2763,7 @@ fn belt_actuate_spawn_with_origin( session_name: String, origin: AttemptOrigin, selection: DispatchActuatorSelection, + request: AttemptLaunchRequest, ) -> BeltSpawnOutcome { let resolution = alignment_resolve( node_id: node.node as String, @@ -2795,13 +2821,13 @@ fn belt_actuate_spawn_with_origin( BeltPlacementRefused { detail } => SpawnNotAdmitted { node_id: node.node as String, refusal: LaunchNoPlacementGrant { detail: detail } } BeltPlacementNotHeld => - belt_spawn_admitted(instance: instance, node: node, workdir: workdir, admission_receipt: admission_receipt, provider_wire_label: provider_wire_label, launch: launch, attempt_key: attempt_key, session_name: session_name, origin: origin, cmds: cmds, binding: binding, placement: none) + belt_spawn_admitted(instance: instance, node: node, workdir: workdir, admission_receipt: admission_receipt, provider_wire_label: provider_wire_label, launch: launch, attempt_key: attempt_key, session_name: session_name, origin: origin, cmds: cmds, binding: binding, placement: none, request: request) BeltPlacementHeld { held } => belt_spawn_release_unless_spawned( held: held, provider_wire_label: provider_wire_label, launch: launch, - outcome: belt_spawn_admitted(instance: instance, node: node, workdir: workdir, admission_receipt: admission_receipt, provider_wire_label: provider_wire_label, launch: launch, attempt_key: attempt_key, session_name: session_name, origin: origin, cmds: cmds, binding: binding, placement: Present { value: held }), + outcome: belt_spawn_admitted(instance: instance, node: node, workdir: workdir, admission_receipt: admission_receipt, provider_wire_label: provider_wire_label, launch: launch, attempt_key: attempt_key, session_name: session_name, origin: origin, cmds: cmds, binding: binding, placement: Present { value: held }, request: request), ) } } @@ -2825,6 +2851,7 @@ fn belt_spawn_admitted( cmds: DispatchSpawnCommands, binding: AlignmentBinding, placement: BeltHeldPlacement?, + request: AttemptLaunchRequest, ) -> BeltSpawnOutcome { match belt_exec_steps( steps: [ @@ -2871,6 +2898,7 @@ fn belt_spawn_admitted( launch: launch, turn: attempt_origin_turn(origin: origin), binding: binding, + request: request, ) { AttemptStateInitializationFailed { step, detail } => SpawnFailed { @@ -3053,6 +3081,7 @@ fn belt_actuate_spawn_exec( provider_wire_label: provider_wire_label, launch: launch, selection: selection, + request: LaunchAutonomous, ) } @@ -9177,6 +9206,7 @@ fn belt_dispatch_node_for_instance_over( instance: HostDashboardInstance, plan: RoadmapAuthorityProjection, node_id: String, + request: AttemptLaunchRequest, ) -> BeltSpawnOutcome { belt_dispatch_node_staged_over( instance: instance, @@ -9184,6 +9214,7 @@ fn belt_dispatch_node_for_instance_over( graph: launch_graph_of_projection(proj: plan), merged: authored_merged_prs(), node_id: node_id, + request: request, ) } @@ -9203,6 +9234,7 @@ fn belt_dispatch_node_staged( graph: launch_graph_of_projection(proj: roadmap_authority_projection(history: roadmap_acceptance_event_history_load())), merged: authored_merged_prs(), node_id: node_id, + request: LaunchAutonomous, ) } @@ -9212,6 +9244,7 @@ fn belt_dispatch_node_staged_over( graph: LaunchGraphStanding, merged: List, node_id: String, + request: AttemptLaunchRequest, ) -> BeltSpawnOutcome { match (match launch_admission_staged(cause: Operator, standing: standing, graph: graph, merged: merged, node_id: node_id, reserved: 0) { LaunchStageDecided { admission } => admission @@ -9256,6 +9289,7 @@ fn belt_dispatch_node_staged_over( node: node, probed_at: probed_at as String, launch: identity, + request: request, ) Absent => SpawnFailed { diff --git a/dag/gunbc/roadmap/roadmap_belt_tick_cli.dag b/dag/gunbc/roadmap/roadmap_belt_tick_cli.dag index 205fc33873f..864c132e776 100644 --- a/dag/gunbc/roadmap/roadmap_belt_tick_cli.dag +++ b/dag/gunbc/roadmap/roadmap_belt_tick_cli.dag @@ -34,6 +34,10 @@ import gunbc.roadmap_dogfood_route { dogfood_start_record_for_instance, DogfoodStartRecord, DogfoodStartRecorded, DogfoodStartAlreadyRecorded, DogfoodStartNoCompleteAttempt, DogfoodStartRefused, } +import gunbc.roadmap.roadmap_result_returned { + ResultReturnPass, ResultReturnPassRan, ResultReturnPassWithheld, ResultReturnPassRefused, + result_return_attempts_for_instance, result_return_pass_exit, result_return_pass_failure, +} import extdeps.filesystem.filesystem_io { Filesystem } import extdeps.languages.json.emit { json_array } import gunbc.roadmap.roadmap_review_function { review_report_json } @@ -43,9 +47,23 @@ import gunbc.roadmap.roadmap_review_function { review_report_json } // looked like when it did it). It lives outside gunbc.roadmap_belt_actuate because the observation // snapshot module imports the belt's observers, so the tick that also writes the snapshot must sit // above both — the import graph's only law is acyclicity (DESIGN 3). - -data belt_run_once_cli_note: String = "The ProcessExit entrypoint — this is what `gunbc run --function belt_run_once_cli` invokes (belt_run_once_json returns String, which `gunbc run` rejects; belt_run_once_cli is its runnable wrapper). EXIT SEMANTICS (operator P0, 2026-08-02): exit 0 when the tick COMPLETED and PERSISTED its BeltTickReceipt and its served observation — spawn failures, recorded validation refusals, and observe_refused on the spawn pass do NOT flip the exit, because those are counted outcomes in the receipt, not a failure to run or record the tick. Nonzero ONLY when the tick could not persist its receipt or its served observation (BeltPassFailed on write), when GUNBC_BELT_SPAWN_WORKDIR is missing/empty, or when the dogfood start record (gunbc.roadmap_dogfood_route dogfood_start_record_for_instance), which runs only after the served observation is persisted, refuses. systemd therefore distinguishes 'tick crashed' from 'tick ran, work refused'. The stdout JSON (roadmap-belt-tick/v1) remains the per-tick operational receipt for journald; the persisted BeltTickReceipt is what /workflow.json's producer reads, and the persisted served observation is what the dashboard serves." - +// +// The ProcessExit entrypoint — this is what `gunbc run --function belt_run_once_cli` invokes +// (belt_run_once_json returns String, which `gunbc run` rejects; belt_run_once_cli is its runnable +// wrapper). EXIT SEMANTICS (operator P0, 2026-08-02): exit 0 when the tick COMPLETED and PERSISTED +// its BeltTickReceipt and its served observation — spawn failures, recorded validation refusals, +// and observe_refused on the spawn pass do NOT flip the exit, because those are counted outcomes +// in the receipt, not a failure to run or record the tick. Nonzero ONLY when the tick could not +// persist its receipt or its served observation (BeltPassFailed on write), could not append a +// terminal attempt's result to its issue's event history (gunbc.roadmap.roadmap_result_returned +// result_return_pass_exit -- an answer owed to a requester and not delivered), when +// GUNBC_BELT_SPAWN_WORKDIR is missing/empty, or when the dogfood start record +// (gunbc.roadmap_dogfood_route dogfood_start_record_for_instance), which runs only after the +// served observation is persisted, refuses. systemd therefore distinguishes 'tick crashed' from +// 'tick ran, work refused'. The stdout JSON (roadmap-belt-tick/v1) remains the per-tick +// operational receipt for journald; the persisted BeltTickReceipt is what /workflow.json's +// producer reads, and the persisted served observation is what the dashboard serves. +// // THE SPAWN ROOT IS SUPPLIED BY WHOEVER INVOKES THE TICK; MAKING IT A LITERAL WAS THE DEFECT. // belt_actuate_workdir projects srv1_gunbc_repo_root, so a tick ran `git worktree add` into // PRODUCTION's tree whichever deployment invoked it — invisible while nothing ticked on a cadence @@ -104,10 +122,11 @@ fn belt_run_once_cli_in(spawn_workdir: String) -> ProcessExit { r, ], "")) BeltPassRecorded => - match belt_served_observation_exit(instance: instance, observed_at: observed_at) { - ExitFailure { code, reason } => ExitFailure { code: code, reason: reason } - ExitSuccess => belt_start_record_exit(start: dogfood_start_record_for_instance(instance: instance)) - } + belt_tick_tail_after_result_return( + instance: instance, + observed_at: observed_at, + returned: result_return_attempts_for_instance(instance: instance, recorded_at: observed_at), + ) BeltPassDeferred { reason: r } => exit_failure(reason: join(["belt tick receipt write returned deferred: ", r], "")) BeltPassRefused { reason: r } => @@ -130,6 +149,47 @@ fn belt_start_record_exit(start: DogfoodStartRecord) -> ProcessExit { } } +// THE TAIL IN ITS ORDER, EACH STEP A DEPENDENCY OF THE NEXT: result return, then served +// observation, then start record. The result return has already run -- its pass is this function's +// argument -- and the observation is taken INSIDE a match on that pass, so it cannot be evaluated +// before the pass exists; every arm of the pass proceeds to the observation, because an undelivered +// result must not freeze the page. The start record runs only after a persisted observation +// (belt_observation_then_start_exit). So the standing the dashboard serves already reads an event +// this tick appended, and a slow or failing recorder cannot stop the page being written. +fn belt_tick_tail_after_result_return(instance: HostDashboardInstance, observed_at: String, returned: ResultReturnPass) -> ProcessExit { + belt_tick_result_tail_exit( + returned: returned, + tail: match returned { + ResultReturnPassRan { outcomes: _ } => belt_observation_then_start_exit(instance: instance, observed_at: observed_at) + ResultReturnPassWithheld { reason: _ } => belt_observation_then_start_exit(instance: instance, observed_at: observed_at) + ResultReturnPassRefused { step: _, reason: _ } => belt_observation_then_start_exit(instance: instance, observed_at: observed_at) + }, + ) +} + +fn belt_observation_then_start_exit(instance: HostDashboardInstance, observed_at: String) -> ProcessExit { + match belt_served_observation_exit(instance: instance, observed_at: observed_at) { + ExitFailure { code, reason } => ExitFailure { code: code, reason: reason } + ExitSuccess => belt_start_record_exit(start: dogfood_start_record_for_instance(instance: instance)) + } +} + +// THE ONE JOIN OF THE RESULT RETURN WITH THE REST OF THE TAIL, pure: `tail` is the exit of the +// observation-then-start chain. The result return runs on EVERY tick, including one whose passes +// were withheld: an answer owed to a requester is a standing obligation that an unrelated launch +// refusal does not discharge. When both the result return and the tail failed, the exit names +// both; neither cause replaces the other. +fn belt_tick_result_tail_exit(returned: ResultReturnPass, tail: ProcessExit) -> ProcessExit { + match tail { + ExitSuccess => result_return_pass_exit(pass: returned) + ExitFailure { code, reason } => + match result_return_pass_failure(pass: returned) { + Absent => ExitFailure { code: code, reason: reason } + Present { value: undelivered } => ExitFailure { code: code, reason: join([reason, "\n", undelivered], "") } + } + } +} + fn belt_served_observation_exit(instance: HostDashboardInstance, observed_at: String) -> ProcessExit { match served_observation_observe_and_write_for_instance(instance: instance, observed_at: observed_at) { BeltPassRecorded => ExitSuccess diff --git a/dag/gunbc/roadmap/roadmap_event_carrier.dag b/dag/gunbc/roadmap/roadmap_event_carrier.dag index c016de9526e..78bc86a5975 100644 --- a/dag/gunbc/roadmap/roadmap_event_carrier.dag +++ b/dag/gunbc/roadmap/roadmap_event_carrier.dag @@ -5,7 +5,16 @@ import extdeps.filesystem.filesystem_io { Filesystem } import std.algebra { trim } import std.content_hash { content_hash_of_value } import std.durable_compare_and_set { CasGeneration, CasReadableSlot, CasReadableAbsent, CasReadablePresent, CasSlotVersion, ExpectSlotAbsent, ExpectSlotGeneration, cas_expectation_admits } -import gunbc.roadmap.roadmap_event_log { roadmap_event_chain } +import gunbc.roadmap.roadmap_event_log { + roadmap_event_chain, roadmap_event_admitted_after, + EventPrincipal, AttemptResultOutcome, RoadmapResultBinding, roadmap_result_returned_event, + RoadmapResultAdmission, ResultAdmitted, ResultAlreadyOnLog, ResultNotAdmitted, roadmap_bound_result_admission, +} +import gunbc.roadmap.roadmap_attempt_request_record { + AttemptRequestBinding, RequestClaimBound, RequestAutonomous, + AttemptRequestBindingRead, RequestBindingRead, RequestBindingAbsent, RequestBindingUnreadable, + attempt_request_binding_read_for_instance, +} import extdeps.shell import gunbc.roadmap.roadmap_event_log { roadmap_event_heads, roadmap_node_standing_key, roadmap_issue_state } import gunbc.output_policy { OutcomeIsData } @@ -244,7 +253,9 @@ fn roadmap_event_append_private(layout: RoadmapEventCarrierLayout, event: Roadma } else { match roadmap_events_read_synced(layout: layout, node: event.node) { EventsReadRefused { node: _, step, reason } => EventAppendRefused { id: env.id, step: step, reason: reason } EventsRead { node: _, envelopes } => if any(envelopes, existing => existing.id == env.id) { EventAlreadyPresent { id: env.id } } - else if !roadmap_event_parent_admitted(event: event, envelopes: envelopes) { + else if !roadmap_event_admitted_after(envs: envelopes, event: event) { + EventAppendRefused { id: env.id, step: "result-requires-binding", reason: "a result is written only through the bound result append, which builds it from the attempt's own binding; the generic append does not admit one" } + } else if !roadmap_event_parent_admitted(event: event, envelopes: envelopes) { EventAppendRefused { id: env.id, step: "revision-conflict", reason: "expected issue revision does not match the committed issue head" } } else { roadmap_event_publish_private(layout: layout, env: env, operation: operation, receipt_path: receipt_path, expected_commit: trim(revision)) } } } @@ -425,6 +436,19 @@ fn roadmap_all_events_of_reads(reads: List) -> RoadmapAllEven } } +// Every node's events from a worktree the caller already synced: the one walk both the all-events +// read and the bound result append decide over. +fn roadmap_events_read_all_synced(layout: RoadmapEventCarrierLayout) -> RoadmapAllEventsRead { + let root = join([layout.worktree as String, "/", roadmap_events_dir_segment], "") + let listing = Filesystem.List(path: root) + if !listing.success && listing.error_kind != "not_found" { AllEventsReadRefused { step: "list", reason: listing.error } } else if !listing.success { + AllEventsRead { envelopes: [] } + } else { + let nodes = filter(split(s: listing.entries, delimiter: "\n"), n => trim(n) != "") + roadmap_all_events_of_reads(map(nodes, n => roadmap_events_read_synced(layout: layout, node: trim(n) as NonEmptyStr as RoadmapNodeId))) + } +} + fn roadmap_events_read_all_private(layout: RoadmapEventCarrierLayout) -> RoadmapAllEventsRead { match roadmap_event_carrier_ensure(layout: layout) { CarrierEnsureRefused { step, reason } => AllEventsReadRefused { step: join(["ensure/", step], ""), reason: reason } @@ -432,14 +456,7 @@ fn roadmap_events_read_all_private(layout: RoadmapEventCarrierLayout) -> Roadmap match roadmap_event_carrier_sync(layout: layout) { CarrierEnsureRefused { step, reason } => AllEventsReadRefused { step: join(["sync/", step], ""), reason: reason } CarrierReady => { - let root = join([layout.worktree as String, "/", roadmap_events_dir_segment], "") - let listing = Filesystem.List(path: root) - if !listing.success && listing.error_kind != "not_found" { AllEventsReadRefused { step: "list", reason: listing.error } } else if !listing.success { - AllEventsRead { envelopes: [] } - } else { - let nodes = filter(split(s: listing.entries, delimiter: "\n"), n => trim(n) != "") - roadmap_all_events_of_reads(map(nodes, n => roadmap_events_read_synced(layout: layout, node: trim(n) as NonEmptyStr as RoadmapNodeId))) - } + roadmap_events_read_all_synced(layout: layout) } } } @@ -477,3 +494,100 @@ fn roadmap_events_read_all(layout: RoadmapEventCarrierLayout) -> RoadmapAllEvent } } } + +// WHOSE RESULT THIS IS, DERIVED FROM THE ATTEMPT'S OWN RECORD AND NOTHING THE CALLER SAYS. Given the +// issue and the attempt and what the attempt's create-only request record reads as, the identity a +// result is written under is established or refused. Established carries the claim THE RECORD +// names; there is no parameter through which a claim could be offered, so a result for attempt K +// cannot be addressed to any claim but the one K recorded at launch. An attempt with no record, an +// autonomous one, and a record that cannot be read have no claim to answer and refuse, each by +// name. +type RoadmapResultAuthority + = ResultAuthorityEstablished { binding: RoadmapResultBinding } + | ResultAuthorityRefused { step: String, reason: String } + +fn roadmap_result_authority_of_record(node: RoadmapNodeId, attempt: String, record: AttemptRequestBindingRead) -> RoadmapResultAuthority { + if attempt == "" { + ResultAuthorityRefused { step: "result-attempt", reason: "the attempt has no key, so no request record can be read for it" } + } else { + match record { + RequestBindingRead { binding: RequestClaimBound { claim } } => + ResultAuthorityEstablished { binding: RoadmapResultBinding { node: node, attempt: attempt as NonEmptyStr, claim: claim } } + RequestBindingRead { binding: RequestAutonomous } => + ResultAuthorityRefused { step: "result-binding", reason: join(["attempt ", attempt, " of issue ", node as String, " was launched autonomously; it has no request to answer"], "") } + RequestBindingAbsent => + ResultAuthorityRefused { step: "result-binding", reason: join(["attempt ", attempt, " of issue ", node as String, " has no request record, so the claim a result must name is not established"], "") } + RequestBindingUnreadable { reason } => + ResultAuthorityRefused { step: "result-binding", reason: join(["the request record of attempt ", attempt, " of issue ", node as String, " cannot be read: ", reason], "") } + } + } +} + +// WHAT THE BOUND APPEND ANSWERS. Its own type, because a refusal reached before the claim is derived +// has no event to name: appended and already-present name the result event; refused names the step +// and the reason. +type RoadmapResultAppend + = ResultAppended { id: RoadmapEventId } + | ResultAppendAlreadyPresent { id: RoadmapEventId } + | ResultAppendRefused { step: String, reason: String } + +fn roadmap_result_append_of(append: RoadmapEventAppend) -> RoadmapResultAppend { + match append { + EventAppended { id } => ResultAppended { id: id } + EventAlreadyPresent { id } => ResultAppendAlreadyPresent { id: id } + EventAppendRefused { id: _, step, reason } => ResultAppendRefused { step: step, reason: reason } + } +} + +// THE BOUND RESULT APPEND: the one way a ResultReturned reaches the log. The caller names the issue +// and the attempt and supplies the outcome. It supplies neither an event nor a claim. Inside ONE +// private snapshot the effect reads the attempt's own create-only request record and derives the +// claim from it (roadmap_result_authority_of_record), reads the whole log, +// roadmap_bound_result_admission decides over it (no result for the attempt on any other issue, +// none that differs on this one, the claim a Claimed on this issue's history, a single head), the +// event is built from that derived identity and chained on the head the snapshot read, and it is +// published against that exact commit. A log that moved between the read and the publish is +// refused by the same conditional push as any append. The snapshot is closed through a match the +// result passes through, so the close is a step of the append and not a binding nothing consumes. +fn roadmap_bound_result_append(instance: HostDashboardInstance, node: RoadmapNodeId, attempt: String, outcome: AttemptResultOutcome, author: EventPrincipal, recorded_at: String) -> RoadmapResultAppend { + match roadmap_event_private_snapshot(layout: roadmap_event_carrier_layout_for_instance(instance: instance)) { + CarrierSnapshotRefused { reason } => ResultAppendRefused { step: "snapshot", reason: reason } + CarrierSnapshotReady { layout: private } => { + let result = roadmap_bound_result_append_private(instance: instance, layout: private, node: node, attempt: attempt, outcome: outcome, author: author, recorded_at: recorded_at) + match roadmap_event_snapshot_close(layout: private) { + CarrierExecOk { stdout: _ } => result + CarrierExecFailed { exit_code: _, stderr: _ } => result + } + } + } +} + +fn roadmap_bound_result_append_private(instance: HostDashboardInstance, layout: RoadmapEventCarrierLayout, node: RoadmapNodeId, attempt: String, outcome: AttemptResultOutcome, author: EventPrincipal, recorded_at: String) -> RoadmapResultAppend { + match roadmap_result_authority_of_record(node: node, attempt: attempt, record: attempt_request_binding_read_for_instance(instance: instance, node_id: node, attempt_key: attempt)) { + ResultAuthorityRefused { step, reason } => ResultAppendRefused { step: step, reason: reason } + ResultAuthorityEstablished { binding } => + match roadmap_event_carrier_sync(layout: layout) { + CarrierEnsureRefused { step, reason } => ResultAppendRefused { step: step, reason: reason } + CarrierReady => match carrier_exec(layout: layout, workdir: layout.worktree as String, args: ["rev-parse", "HEAD"]) { + CarrierExecFailed { exit_code: _, stderr } => ResultAppendRefused { step: "snapshot-revision", reason: stderr } + CarrierExecOk { stdout: revision } => + match roadmap_events_read_all_synced(layout: layout) { + AllEventsReadRefused { step, reason } => ResultAppendRefused { step: step, reason: reason } + AllEventsRead { envelopes } => + match roadmap_bound_result_admission(envs: envelopes, binding: binding, outcome: outcome) { + ResultNotAdmitted { step, reason } => ResultAppendRefused { step: step, reason: reason } + ResultAlreadyOnLog { id } => ResultAppendAlreadyPresent { id: id } + ResultAdmitted { parent } => + roadmap_result_append_of(append: roadmap_event_publish_private( + layout: layout, + env: roadmap_event_envelope(e: roadmap_result_returned_event(binding: binding, outcome: outcome, author: author, parent: parent, recorded_at: recorded_at)), + operation: "", + receipt_path: "", + expected_commit: trim(revision), + )) + } + } + } + } + } +} diff --git a/dag/gunbc/roadmap/roadmap_event_log.dag b/dag/gunbc/roadmap/roadmap_event_log.dag index 7bfcac8849d..9a1c92f17dc 100644 --- a/dag/gunbc/roadmap/roadmap_event_log.dag +++ b/dag/gunbc/roadmap/roadmap_event_log.dag @@ -93,7 +93,7 @@ type RoadmapEventId = NonEmptyStr where brand("RoadmapEventId") // authenticated principals in v3, LegacyEmailPrincipal in history). Released relinquishes the // hold; Progress carries a note; Blocked and HandedBack carry a reason and end the holder's work // without finishing it, which is what the roadmap's own hand-back conditions describe; Verified, -// Reviewed, Audited and Published are the belt's judgments, stated here by REFERENCE to their +// Reviewed and Audited are the belt's judgments, stated here by REFERENCE to their // receipts (an address the reader can resolve) so the log never becomes a second copy of a // receipt. COMMENTS ARE DURABLE EVENTS TOO (owner mandate 2026-09-25, // docs/plans/issue-page-architecture.md): CommentCreated / CommentEdited / CommentDeleted ride @@ -112,7 +112,60 @@ type RoadmapEventKind | CommentCreated { comment: NonEmptyStr, body: NonEmptyStr, visibility: NonEmptyStr, parent_comment: NonEmptyStr? } | CommentEdited { comment: NonEmptyStr, body: NonEmptyStr } | CommentDeleted { comment: NonEmptyStr } - | PublicationObserved { pr: NonEmptyStr, head: NonEmptyStr } + | ResultReturned { attempt: NonEmptyStr, claim: RoadmapEventId, outcome: AttemptResultOutcome } + +// THE RESULT A REQUESTER IS OWED, ONE ARM PER WAY AN ATTEMPT FINALLY ENDS (operator ruling +// 2026-10-03, the dogfood vertical's result-returned stage). The requester who assigned the issue +// used to learn the answer only by reading the dashboard; the belt now states it on the issue's own +// history as ONE event per attempt: the attempt, the Claimed event the attempt was launched under +// (the request's identity), and what the attempt came to. WHO THE RESULT RETURNS TO IS NOT ON THE +// EVENT: it is the return_to of the claim the event names, read from that claim wherever it is +// needed (roadmap_claim_return_to), so the recipient has one home and a result cannot name a +// principal its claim does not. The arms are FINAL arms of gunbc.roadmap.roadmap_submission +// CandidateHandoffState and no second classification: published carries the pull request and the +// exact head it offers; a failed verification carries the head it judged and the verdict; handed +// back carries the worker's own declaration that it ended without a candidate. A state the belt +// may still move past -- evidence it could not read, an integration or publication that is blocked +// -- is not an arm, because an event is an answer and that is not one yet. +// +// THIS KIND REPLACES PublicationObserved RATHER THAN SITTING BESIDE IT. That kind had a fold step +// and a page row and no writer anywhere, so no publication was ever stated on a log; a published +// result is the same fact with the attempt and the request it lacked, and two kinds for one fact +// would be the fork. +type AttemptResultOutcome + = ResultPublished { pr: NonEmptyStr, head: NonEmptyStr } + | ResultVerificationFailed { head: NonEmptyStr, cause: NonEmptyStr } + | ResultHandedBack { reason: NonEmptyStr } + +fn attempt_result_outcome_word(o: AttemptResultOutcome) -> String { + match o { + ResultPublished { pr: _, head: _ } => "published" + ResultVerificationFailed { head: _, cause: _ } => "verification_failed" + ResultHandedBack { reason: _ } => "handed_back" + } +} + +fn attempt_result_outcome_members(o: AttemptResultOutcome) -> List { + match o { + ResultPublished { pr, head } => [ + json_kv(key: "pr", value: json_string(s: pr as String)), + json_kv(key: "head", value: json_string(s: head as String)), + ] + ResultVerificationFailed { head, cause } => [ + json_kv(key: "head", value: json_string(s: head as String)), + json_kv(key: "cause", value: json_string(s: cause as String)), + ] + ResultHandedBack { reason } => [json_kv(key: "reason", value: json_string(s: reason as String))] + } +} + +fn attempt_result_outcome_text(o: AttemptResultOutcome) -> String { + match o { + ResultPublished { pr, head } => join(["published ", pr as String, " at ", head as String], "") + ResultVerificationFailed { head, cause } => join(["verification failed at ", head as String, ": ", cause as String], "") + ResultHandedBack { reason } => join(["handed back: ", reason as String], "") + } +} // THE EVENT'S AUTHOR IS A PRINCIPAL, never an email and never a transport identity (owner ruling // 2026-09-25): v3 writes carry an authenticated PrincipalRef (a Google OIDC sub for humans, @@ -142,7 +195,7 @@ fn roadmap_event_kind_word(k: RoadmapEventKind) -> String { CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => "comment_created" CommentEdited { comment: _, body: _ } => "comment_edited" CommentDeleted { comment: _ } => "comment_deleted" - PublicationObserved { pr: _, head: _ } => "publication_observed" + ResultReturned { attempt: _, claim: _, outcome: _ } => "result_returned" } } @@ -192,10 +245,11 @@ fn roadmap_event_kind_members_for(k: RoadmapEventKind, schema: String) -> List [ - json_kv(key: "pr", value: json_string(s: pr as String)), - json_kv(key: "head", value: json_string(s: head as String)), - ] + ResultReturned { attempt, claim, outcome } => concat([ + json_kv(key: "attempt", value: json_string(s: attempt as String)), + json_kv(key: "claim", value: json_string(s: claim as String)), + json_kv(key: "outcome", value: json_string(s: attempt_result_outcome_word(o: outcome))), + ], attempt_result_outcome_members(o: outcome)) } } @@ -358,18 +412,53 @@ fn roadmap_event_kind_decode(doc: JsonValue, word: String, schema: String, autho } else if word == "comment_deleted" { let id_key = if schema == roadmap_event_schema_v2 { "key" } else { "comment" } match event_member_nonempty(doc: doc, key: id_key) { Present { value: c } => Present { value: CommentDeleted { comment: c } } Absent => none } - } else if word == "publication_observed" { - match event_member_nonempty(doc: doc, key: "pr") { + } else if word == "result_returned" { + match event_member_nonempty(doc: doc, key: "attempt") { Absent => none - Present { value: pr } => - match event_member_nonempty(doc: doc, key: "head") { + Present { value: attempt } => + match event_member_nonempty(doc: doc, key: "claim") { Absent => none - Present { value: head } => Present { value: PublicationObserved { pr: pr, head: head } } + Present { value: claim } => + match attempt_result_outcome_decode(doc: doc) { + Absent => none + Present { value: outcome } => Present { value: ResultReturned { attempt: attempt, claim: claim as RoadmapEventId, outcome: outcome } } + } } } } else { none } } +// THE OUTCOME DECODE: the word selects the arm and every member the arm names must be present and +// non-empty, so an outcome word this reader does not know, or a published result with no head, is +// an undecodable event and never a result with a blank in it. +fn attempt_result_outcome_decode(doc: JsonValue) -> AttemptResultOutcome? { + match event_member_string(doc: doc, key: "outcome") { + Absent => none + Present { value: word } => + if word == "published" { + match event_member_nonempty(doc: doc, key: "pr") { + Absent => none + Present { value: pr } => + match event_member_nonempty(doc: doc, key: "head") { + Absent => none + Present { value: head } => Present { value: ResultPublished { pr: pr, head: head } } + } + } + } else if word == "verification_failed" { + match event_member_nonempty(doc: doc, key: "head") { + Absent => none + Present { value: head } => + match event_member_nonempty(doc: doc, key: "cause") { + Absent => none + Present { value: cause } => Present { value: ResultVerificationFailed { head: head, cause: cause } } + } + } + } else if word == "handed_back" { + match event_member_nonempty(doc: doc, key: "reason") { Present { value: r } => Present { value: ResultHandedBack { reason: r } } Absent => none } + } else { none } + } +} + fn roadmap_event_decode(text: String) -> RoadmapEventDecode { match parse_json_document(s: text) { JsonDocumentUnreadable { gap } => RoadmapEventUndecodable { reason: join(["not JSON: ", json_document_gap_text(gap: gap)], "") } @@ -492,7 +581,10 @@ fn roadmap_event_chain(envs: List, head: RoadmapEventEnvel // Claimed step replaces the holder, so the NEWEST Claimed wins by construction, never by clock. // THE FOLD'S ACC carries the return_to the handoff needs: the standing alone cannot answer // "who does the published work come back to" — that principal is recorded on the latest Claimed -// and rides the acc until a PublicationObserved spends it. +// and rides the acc; a published ResultReturned hands the issue to the return_to of the CLAIM it +// names (read from that claim in the same history, so the step takes the history), and only that +// outcome moves the holder -- a failed or handed-back attempt is an answer on the history, not a +// reassignment. type RoadmapStandingAcc { standing: RoadmapNodeStanding assignment: IssueAssignmentProjection @@ -504,7 +596,7 @@ fn roadmap_standing_acc_empty() -> RoadmapStandingAcc { // One transition authority produces progress and ownership together. Progress/blocked/handback // authors are actors, not implicit assignees. Only explicit assignment transitions change holder. -fn roadmap_standing_step_acc(acc: RoadmapStandingAcc, env: RoadmapEventEnvelope) -> RoadmapStandingAcc { +fn roadmap_standing_step_acc(acc: RoadmapStandingAcc, env: RoadmapEventEnvelope, envs: List) -> RoadmapStandingAcc { let e = env.event match e.kind { Claimed { assignee, return_to } => RoadmapStandingAcc { @@ -514,10 +606,14 @@ fn roadmap_standing_step_acc(acc: RoadmapStandingAcc, env: RoadmapEventEnvelope) Progress { note } => RoadmapStandingAcc { standing: NodeInProgress { by: e.author, last_note: note }, assignment: acc.assignment } Blocked { reason } => RoadmapStandingAcc { standing: NodeBlocked { by: e.author, reason: reason }, assignment: acc.assignment } HandedBack { reason } => RoadmapStandingAcc { standing: NodeHandedBack { by: e.author, reason: reason }, assignment: acc.assignment } - PublicationObserved { pr, head } => match acc.assignment.return_to { - Present { value: return_to } => RoadmapStandingAcc { standing: NodeReadyForReview { by: return_to, pr: pr, head: head }, - assignment: IssueAssignmentProjection { holder: Present { value: return_to }, return_to: acc.assignment.return_to } } - Absent => acc + ResultReturned { attempt: _, claim, outcome } => match outcome { + ResultPublished { pr, head } => match roadmap_claim_return_to(envs: envs, claim: claim) { + Present { value: return_to } => RoadmapStandingAcc { standing: NodeReadyForReview { by: return_to, pr: pr, head: head }, + assignment: IssueAssignmentProjection { holder: Present { value: return_to }, return_to: acc.assignment.return_to } } + Absent => acc + } + ResultVerificationFailed { head: _, cause: _ } => acc + ResultHandedBack { reason: _ } => acc } ReceiptReferenced { obligation: _, address: _, verdict: _ } => acc CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => acc @@ -536,7 +632,7 @@ fn roadmap_issue_state(envs: List) -> RoadmapStandingAcc { else if count(heads) != 1 { RoadmapStandingAcc { standing: NodeForked { heads: map(heads, h => h.id) }, assignment: roadmap_assignment_empty() } } else { match heads.first() { Absent => roadmap_standing_acc_empty() - Present { value: head } => fold(roadmap_event_chain(envs: envs, head: head, budget: count(envs)), init: roadmap_standing_acc_empty(), f: (acc, env) => roadmap_standing_step_acc(acc: acc, env: env)) + Present { value: head } => fold(roadmap_event_chain(envs: envs, head: head, budget: count(envs)), init: roadmap_standing_acc_empty(), f: (acc, env) => roadmap_standing_step_acc(acc: acc, env: env, envs: envs)) } } } } @@ -620,7 +716,7 @@ fn roadmap_recent_claimed_assignees(envs: List) -> List false CommentEdited { comment: _, body: _ } => false CommentDeleted { comment: _ } => false - PublicationObserved { pr: _, head: _ } => false + ResultReturned { attempt: _, claim: _, outcome: _ } => false }) roadmap_event_principal_dedup(map(roadmap_events_newest_first(envs: claimed), e => match e.event.kind { Claimed { assignee, return_to: _ } => assignee @@ -632,7 +728,7 @@ fn roadmap_recent_claimed_assignees(envs: List) -> List LegacyEmailPrincipal { email: "unused" as NonEmptyStr, authentication_unestablished: true } CommentEdited { comment: _, body: _ } => LegacyEmailPrincipal { email: "unused" as NonEmptyStr, authentication_unestablished: true } CommentDeleted { comment: _ } => LegacyEmailPrincipal { email: "unused" as NonEmptyStr, authentication_unestablished: true } - PublicationObserved { pr: _, head: _ } => LegacyEmailPrincipal { email: "unused" as NonEmptyStr, authentication_unestablished: true } + ResultReturned { attempt: _, claim: _, outcome: _ } => LegacyEmailPrincipal { email: "unused" as NonEmptyStr, authentication_unestablished: true } })) } @@ -668,7 +764,7 @@ fn roadmap_event_insert_newest(sorted: List, env: RoadmapE // Claimed wins by the same oldest-first chain fold as the standing; Released clears the hold. A // forked or incomplete history names no holder rather than fabricating one — the standing reports // the fork, and the rail reads the standing for that. THE PUBLICATION HANDOFF (its own slice) -// extends the step: an observed publication moves the holder to the stored return_to. +// extends the step: a published result moves the holder to the return_to the result names. type IssueAssignmentProjection { holder: EventPrincipal? return_to: EventPrincipal? @@ -746,7 +842,7 @@ fn roadmap_issue_comments_of_chain(chain: List) -> List acc HandedBack { reason: _ } => acc ReceiptReferenced { obligation: _, address: _, verdict: _ } => acc - PublicationObserved { pr: _, head: _ } => acc + ResultReturned { attempt: _, claim: _, outcome: _ } => acc }) } @@ -780,7 +876,7 @@ fn roadmap_comment_created_key_present(envs: List, comment CommentCreated { comment: c, body: _, visibility: _, parent_comment: _ } => (c as String) == (comment as String) CommentEdited { comment: _, body: _ } => false CommentDeleted { comment: _ } => false - PublicationObserved { pr: _, head: _ } => false + ResultReturned { attempt: _, claim: _, outcome: _ } => false Claimed { assignee: _, return_to: _ } => false Released => false Progress { note: _ } => false @@ -804,7 +900,7 @@ fn roadmap_comment_retry_present(envs: List, author: Event }) CommentEdited { comment: _, body: _ } => false CommentDeleted { comment: _ } => false - PublicationObserved { pr: _, head: _ } => false + ResultReturned { attempt: _, claim: _, outcome: _ } => false Claimed { assignee: _, return_to: _ } => false Released => false Progress { note: _ } => false @@ -820,13 +916,144 @@ fn issue_comment_anchor(ordinal: Int) -> String { join(["comment-", to_string(value: ordinal)], "") } -// THE HANDOFF EVENT, the one constructor the belt's publication-observation pass appends on an -// observed publication (owner mandate: durable publication event, then the fold does the -// reassignment). Factored so the pass names the event it writes and the witness drives the fold -// directly; wiring the append into the pass itself is the belt lane's cut (named, not silently -// omitted). -fn roadmap_publication_handoff_event(node: RoadmapNodeId, pr: NonEmptyStr, head: NonEmptyStr, author: EventPrincipal, parent: RoadmapEventId?, recorded_at: String) -> RoadmapEvent { - RoadmapEvent { node: node, author: author, parent: parent, kind: PublicationObserved { pr: pr, head: head }, recorded_at: recorded_at } +// THE RESULT EVENT, built from the binding and nothing else: the issue, attempt and claim are the +// binding's, so a caller cannot offer a result addressed by any other identity. The bound append +// (gunbc.roadmap.roadmap_event_carrier roadmap_bound_result_append) is its one production caller. +fn roadmap_result_returned_event(binding: RoadmapResultBinding, outcome: AttemptResultOutcome, author: EventPrincipal, parent: RoadmapEventId?, recorded_at: String) -> RoadmapEvent { + RoadmapEvent { node: binding.node, author: author, parent: parent, kind: roadmap_result_kind_for(binding: binding, outcome: outcome), recorded_at: recorded_at } +} + +// THE RESULTS A HISTORY ALREADY CARRIES FOR ONE ATTEMPT, in the order given. An attempt has one +// final result, so this is empty or a single event on any log the carrier admitted. +fn roadmap_results_for_attempt(envs: List, attempt: String) -> List { + filter(envs, e => match e.event.kind { + ResultReturned { attempt: a, claim: _, outcome: _ } => (a as String) == attempt + Claimed { assignee: _, return_to: _ } => false + Released => false + Progress { note: _ } => false + Blocked { reason: _ } => false + HandedBack { reason: _ } => false + ReceiptReferenced { obligation: _, address: _, verdict: _ } => false + CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => false + CommentEdited { comment: _, body: _ } => false + CommentDeleted { comment: _ } => false + }) +} + +// THE PRINCIPAL A CLAIM RETURNS TO, read from the claim itself: the event with this id on this +// history, if it is a Claimed. An id the history does not carry, or one that names another kind of +// event, returns none -- there is no principal to read. +fn roadmap_claim_return_to(envs: List, claim: RoadmapEventId) -> EventPrincipal? { + match filter(envs, e => (e.id as String) == (claim as String)).first() { + Absent => none + Present { value: env } => + match env.event.kind { + Claimed { assignee: _, return_to } => Present { value: return_to } + Released => none + Progress { note: _ } => none + Blocked { reason: _ } => none + HandedBack { reason: _ } => none + ReceiptReferenced { obligation: _, address: _, verdict: _ } => none + CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => none + CommentEdited { comment: _, body: _ } => none + CommentDeleted { comment: _ } => none + ResultReturned { attempt: _, claim: _, outcome: _ } => none + } + } +} + +// WHETHER A HISTORY CAN BE ORDERED, DECIDED ON THE STANDING'S OWN ARMS. A forked history and an +// incomplete one have no single causal chain; every other standing is the fold of one. The answer +// is the sentence that says why when it cannot, and none when it can. It is a match on +// RoadmapNodeStanding, so a standing added later must be placed on one side or the other here +// before anything that asks this question compiles. +fn roadmap_history_unordered_reason(envs: List) -> String? { + let standing = roadmap_node_standing(envs: envs) + match standing { + NodeForked { heads: _ } => Present { value: roadmap_node_standing_text(s: standing) } + NodeHistoryIncomplete { missing_parents: _ } => Present { value: roadmap_node_standing_text(s: standing) } + NodeOpen => none + NodeClaimed { by: _, since: _ } => none + NodeInProgress { by: _, last_note: _ } => none + NodeBlocked { by: _, reason: _ } => none + NodeHandedBack { by: _, reason: _ } => none + NodeReadyForReview { by: _, pr: _, head: _ } => none + } +} + +// A RESULT IS NEVER ADMITTED THROUGH THE GENERIC APPEND. Every other kind is judged by the +// carrier's revision check alone; a ResultReturned is an answer to one request by one attempt, and +// whether it may be written depends on the attempt's own binding, which the generic append does not +// have. So the generic append refuses the kind outright, and the only way a result reaches the log +// is the bound append (gunbc.roadmap.roadmap_event_carrier roadmap_bound_result_append), which +// builds the event from the binding it is given and decides with roadmap_bound_result_admission. +fn roadmap_event_admitted_after(envs: List, event: RoadmapEvent) -> Bool { + match event.kind { + ResultReturned { attempt: _, claim: _, outcome: _ } => false + Claimed { assignee: _, return_to: _ } => true + Released => true + Progress { note: _ } => true + Blocked { reason: _ } => true + HandedBack { reason: _ } => true + ReceiptReferenced { obligation: _, address: _, verdict: _ } => true + CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => true + CommentEdited { comment: _, body: _ } => true + CommentDeleted { comment: _ } => true + } +} + +// THE IDENTITY A RESULT ANSWERS UNDER: the issue, the attempt, and the claim the attempt was bound +// to at launch. All three, together; an attempt key alone identifies nothing a result may be +// addressed by. +type RoadmapResultBinding { + node: RoadmapNodeId + attempt: NonEmptyStr + claim: RoadmapEventId +} + +fn roadmap_result_kind_for(binding: RoadmapResultBinding, outcome: AttemptResultOutcome) -> RoadmapEventKind { + ResultReturned { attempt: binding.attempt, claim: binding.claim, outcome: outcome } +} + +// WHETHER THE BOUND RESULT MAY BE WRITTEN, pure over EVERY issue's events as read in the append's +// own snapshot. Admitted names the head to chain on. Already on the log is the identical result on +// the binding's issue -- the same attempt, claim and outcome -- and names its event. Everything else +// is not admitted and says which rule: the attempt already has a result on ANOTHER issue; it +// already has a different result on this issue; the claim is not a Claimed on this issue's history; +// the issue's history forks or is incomplete. One final result per (issue, attempt, claim), and no +// result for the attempt anywhere else. +type RoadmapResultAdmission + = ResultAdmitted { parent: RoadmapEventId? } + | ResultAlreadyOnLog { id: RoadmapEventId } + | ResultNotAdmitted { step: String, reason: String } + +fn roadmap_bound_result_admission(envs: List, binding: RoadmapResultBinding, outcome: AttemptResultOutcome) -> RoadmapResultAdmission { + let existing = roadmap_results_for_attempt(envs: envs, attempt: binding.attempt as String) + let elsewhere = filter(existing, e => (e.event.node as String) != (binding.node as String)) + let here = filter(existing, e => (e.event.node as String) == (binding.node as String)) + let node_envs = filter(envs, e => (e.event.node as String) == (binding.node as String)) + match elsewhere.first() { + Present { value: other } => + ResultNotAdmitted { step: "result-elsewhere", reason: join(["attempt ", binding.attempt as String, " already has a result on issue ", other.event.node as String, " (event ", other.id as String, "); a result for it is not written on issue ", binding.node as String], "") } + Absent => + match here.first() { + Present { value: prior } => + if count(here) == 1 && prior.event.kind == roadmap_result_kind_for(binding: binding, outcome: outcome) { + ResultAlreadyOnLog { id: prior.id } + } else { + ResultNotAdmitted { step: "result-duplicate", reason: join(["attempt ", binding.attempt as String, " already has a final result on this issue (event ", prior.id as String, ") that differs from the one offered (", attempt_result_outcome_text(o: outcome), " for claim ", binding.claim as String, "); a second result is not written"], "") } + } + Absent => + match roadmap_history_unordered_reason(envs: node_envs) { + Present { value: why } => ResultNotAdmitted { step: "result-history", reason: join(["issue ", binding.node as String, " has no single head for a result to follow: ", why], "") } + Absent => + match roadmap_claim_return_to(envs: node_envs, claim: binding.claim) { + Absent => ResultNotAdmitted { step: "result-claim", reason: join(["claim ", binding.claim as String, " is not a claim on issue ", binding.node as String, "'s history"], "") } + Present { value: _ } => ResultAdmitted { parent: match roadmap_event_heads(envs: node_envs).first() { Present { value: h } => Present { value: h.id } Absent => none } } + } + } + } + } } // A single issue's audit ordering is causal. Cross-issue recents retain their separate clock diff --git a/dag/gunbc/roadmap/roadmap_page.dag b/dag/gunbc/roadmap/roadmap_page.dag index 3d935b9a581..bfc2233f67c 100644 --- a/dag/gunbc/roadmap/roadmap_page.dag +++ b/dag/gunbc/roadmap/roadmap_page.dag @@ -14,6 +14,8 @@ import gunbc.roadmap.roadmap_event_log { RoadmapNodeStanding, RoadmapStandings, import gunbc.roadmap.roadmap_event_log { EventPrincipal, EventPrincipalAuthenticated, LegacyEmailPrincipal, event_principal_label, IssueComment, IssueAssignmentProjection, roadmap_issue_comments, roadmap_issue_assignment, issue_comment_anchor, + ResultReturned, AttemptResultOutcome, ResultPublished, ResultVerificationFailed, ResultHandedBack, + attempt_result_outcome_word, attempt_result_outcome_text, roadmap_claim_return_to, RoadmapEventId, } import gunbc.principal_projection { PrincipalRef, principal_ref_label, principal_ref_parse_label, principal_ref_eq, @@ -1945,8 +1947,8 @@ fn issue_event_payload(env: RoadmapEventEnvelope) -> String { CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => "comment created" CommentEdited { comment: _, body: _ } => "comment edited" CommentDeleted { comment: _ } => "comment deleted" - PublicationObserved { pr, head } => - join(["published ", pr as String, " at ", head as String], "") + ResultReturned { attempt, claim, outcome } => + join(["result of attempt ", attempt as String, " for claim ", claim as String, ": ", attempt_result_outcome_text(o: outcome)], "") } } @@ -2094,15 +2096,16 @@ fn comment_time_text(c: IssueComment) -> String { } // ONE STREAM ENTRY, the chronological merge the mandate's Comments tab renders: a durable -// comment, or the compact automated handoff entry an observed publication appends (PR published, -// reassigned for merge) — lifecycle events interleave with the narrative, never raw belt noise. +// comment, or the compact automated entry a returned attempt result appends (published and +// reassigned for merge, verification failed, or handed back) — the answer the requester +// is owed interleaves with the narrative, never raw belt noise. type IssueStreamEntry = StreamComment { comment: IssueComment } | StreamPublication { env: RoadmapEventEnvelope } fn issue_stream_entries(envs: List) -> List { let comments = map(roadmap_issue_comments(envs: envs), c => StreamComment { comment: c }) - let publications = map(filter(envs, e => match e.event.kind { PublicationObserved { pr: _, head: _ } => true Claimed { assignee: _, return_to: _ } => false Released => false Progress { note: _ } => false Blocked { reason: _ } => false HandedBack { reason: _ } => false ReceiptReferenced { obligation: _, address: _, verdict: _ } => false CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => false CommentEdited { comment: _, body: _ } => false CommentDeleted { comment: _ } => false }), e => StreamPublication { env: e }) + let publications = map(filter(envs, e => match e.event.kind { ResultReturned { attempt: _, claim: _, outcome: _ } => true Claimed { assignee: _, return_to: _ } => false Released => false Progress { note: _ } => false Blocked { reason: _ } => false HandedBack { reason: _ } => false ReceiptReferenced { obligation: _, address: _, verdict: _ } => false CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => false CommentEdited { comment: _, body: _ } => false CommentDeleted { comment: _ } => false }), e => StreamPublication { env: e }) issue_entries_sort_ascending(concat(comments, publications)) } @@ -2154,16 +2157,19 @@ fn roadmap_comments_of(entries: List) -> List { }) } -// THE COMPACT AUTOMATED HANDOFF ENTRY (owner mandate: one compact automated comment with PR/head, -// reassigned for merge): fabric's service avatar, the PR and head, and the merge-owner the fold -// reassigned to — the durable event is the comment; nothing is synthesized twice. +// THE COMPACT AUTOMATED RESULT ENTRY (owner mandate: one compact automated comment with PR/head, +// reassigned for merge; operator ruling 2026-10-03: every terminal outcome is returned, not only +// the published one): fabric's service avatar, the attempt, the outcome sentence, and the +// principal the result names — the durable event is the comment; nothing is synthesized twice. fn issue_publication_entry_row_node(node_id: String, envs: List, env: RoadmapEventEnvelope, profiles: List) -> Fragment { match env.event.kind { - PublicationObserved { pr, head } => + ResultReturned { attempt, claim, outcome } => let display = event_principal_display_with_profile(p: env.event.author, profiles: profiles) el_attrs("div", [ attr(name: "class", value: "issue-comment issue-publication" ), - attr(name: "data-event-kind", value: "publication_observed" ), + attr(name: "data-event-kind", value: "result_returned" ), + attr(name: "data-result-outcome", value: attempt_result_outcome_word(o: outcome) ), + attr(name: "data-attempt", value: attempt as String ), ], [ issue_avatar_chip_node(display: display, workflow: event_principal_is_fabric(p: env.event.author)), el_class("div", "issue-comment-main", [ @@ -2173,7 +2179,7 @@ fn issue_publication_entry_row_node(node_id: String, envs: List, profiles: List) -> String { - match roadmap_issue_assignment(envs: envs).holder { - Present { value: holder } => event_principal_display_with_profile(p: holder, profiles: profiles).name - Absent => "the stored merge owner" +// THE NAME OF THE PRINCIPAL A RESULT RETURNS TO, read from the claim the result names in the same +// history. The carrier admits no result whose claim the history lacks, so the absent arm is a +// history this page was handed incomplete, and it says so rather than naming anyone. +fn issue_result_return_to_name(envs: List, claim: RoadmapEventId, profiles: List) -> String { + match roadmap_claim_return_to(envs: envs, claim: claim) { + Present { value: return_to } => event_principal_display_with_profile(p: return_to, profiles: profiles).name + Absent => "the requester of a claim this history does not carry" + } +} + +// THE ENTRY'S SENTENCE, one per outcome, naming the principal its claim returns to: a published +// result says who it was reassigned to for merge; every other outcome says who the answer was +// returned to, because the holder did not move. +fn issue_result_entry_sentence(outcome: AttemptResultOutcome, return_to: String) -> String { + match outcome { + ResultPublished { pr: _, head: _ } => join([attempt_result_outcome_text(o: outcome), " — reassigned for merge to ", return_to], "") + ResultVerificationFailed { head: _, cause: _ } => join([attempt_result_outcome_text(o: outcome), " — returned to ", return_to], "") + ResultHandedBack { reason: _ } => join([attempt_result_outcome_text(o: outcome), " — returned to ", return_to], "") } } diff --git a/dag/gunbc/roadmap/roadmap_publication_helper.dag b/dag/gunbc/roadmap/roadmap_publication_helper.dag index 8867eadeaf0..8c5b0a64e39 100644 --- a/dag/gunbc/roadmap/roadmap_publication_helper.dag +++ b/dag/gunbc/roadmap/roadmap_publication_helper.dag @@ -974,7 +974,7 @@ fn publication_helper_run_json(outcomes: List) -> Stri ])) } -// EXIT SEMANTICS, on the belt's own precedent (belt_run_once_cli_note): a recorded refusal is a +// EXIT SEMANTICS, on the belt's own precedent (gunbc.roadmap_belt_tick_cli belt_run_once_cli): a recorded refusal is a // counted outcome and not a failure to run, so an answer recording an absent credential exits 0 -- // the fact reached disk and the requester will read it. Nonzero is reserved for the helper failing // to do its job at all: a spool it cannot reach, a request it cannot read, an answer it cannot diff --git a/dag/gunbc/roadmap/roadmap_result_returned.dag b/dag/gunbc/roadmap/roadmap_result_returned.dag new file mode 100644 index 00000000000..be1b96aed79 --- /dev/null +++ b/dag/gunbc/roadmap/roadmap_result_returned.dag @@ -0,0 +1,488 @@ +module gunbc.roadmap.roadmap_result_returned + +import std.types { String, Bool, List, NonEmptyStr } +import std.process { ProcessExit, ExitSuccess, exit_failure } +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.principal_projection { fabric_workflow_principal } +import gunbc.roadmap_dashboard_instance { + HostDashboardInstance, DashboardObserveOnly, DashboardActuating, + dashboard_instance_for_repo_root, DashboardInstanceResolved, DashboardInstanceRootUnknown, +} +import gunbc.roadmap_dispatch_actuator { DispatchAttemptRef } +import gunbc.roadmap.roadmap_submission { + CandidateHandoffState, HandoffAwaitingWorker, HandoffYielded, HandoffEvidenceRefused, HandoffDeclaredBlocked, + HandoffSubmitted, HandoffVerificationFailed, HandoffAwaitingIntegration, HandoffBlockedOnIntegration, + HandoffPublicationOwed, HandoffBlockedOnPublication, HandoffPublished, + candidate_handoff_state_key, submission_readiness_key, +} +import gunbc.roadmap_workflow_progress { WorkflowAttemptProgress, workflow_attempt_progress } +import gunbc.roadmap.roadmap_event_log { + RoadmapEvent, RoadmapEventEnvelope, RoadmapEventId, EventPrincipal, EventPrincipalAuthenticated, + Claimed, Released, Progress, Blocked, HandedBack, ReceiptReferenced, CommentCreated, CommentEdited, CommentDeleted, + ResultReturned, AttemptResultOutcome, ResultPublished, ResultVerificationFailed, ResultHandedBack, + attempt_result_outcome_word, attempt_result_outcome_text, event_principal_label, + roadmap_results_for_attempt, roadmap_history_unordered_reason, + RoadmapResultBinding, RoadmapResultAdmission, ResultAdmitted, ResultAlreadyOnLog, ResultNotAdmitted, + roadmap_bound_result_admission, +} +import gunbc.roadmap.roadmap_event_carrier { + RoadmapEventCarrierLayout, roadmap_event_carrier_layout_for_instance, roadmap_bound_result_append, + roadmap_events_read_all, roadmap_events_read, + RoadmapAllEventsRead, AllEventsRead, AllEventsReadRefused, RoadmapEventsRead, EventsRead, EventsReadRefused, + RoadmapResultAppend, ResultAppended, ResultAppendAlreadyPresent, ResultAppendRefused, +} +import gunbc.roadmap.roadmap_attempt_request_record { + AttemptRequestBinding, RequestClaimBound, RequestAutonomous, + AttemptRequestBindingRead, RequestBindingRead, RequestBindingAbsent, RequestBindingUnreadable, + attempt_request_binding_read_for_instance, +} +import gunbc.roadmap_belt_actuate { + belt_attempts_observe_for_instance, AttemptsObserved, AttemptsRefused, + BeltAttemptPanes, belt_attempt_panes_observe_for_instance, belt_workflow_attempt_evidence_for_key, +} + +// WHAT AN ATTEMPT FINALLY CAME TO, AS THE REQUESTER IS OWED IT. The classification is not made +// here: it is gunbc.roadmap.roadmap_submission CandidateHandoffState, the state the attempt row +// already shows, read off the progress carrier. This fold only says which of those states are FINAL +// -- no later observation of the same attempt can move past them -- because a result is appended +// once and never revised. Published is final (a pull request offers this exact head); a failed +// verification is final (a verdict was read for this head); a worker's own declaration that it is +// blocked or needs context is final (the declaration is captured for this head). +// +// EVERYTHING ELSE IS PENDING, INCLUDING STATES THAT LOOK LIKE ENDINGS. Evidence the belt could not +// read may become readable; a blocked integration or publication may be repaired or re-observed; +// and a worker that ended with no captured submission (yielded) has ended a TURN, not answered the +// request -- its lineage continues through a continuation turn or its supervisor. Returning any of +// those would put an answer on the log that a later tick contradicts. +// DECLARED FRONTIER: a lineage that ends without a candidate (a supervisor's plan-defect or stuck +// verdict, an exhausted escalation) IS a final answer and is owed a result; no handoff state +// carries it, so this fold cannot return it. The trigger is the change that gives the result pass +// the lineage's terminal verdict as an input. +// +// Unstatable is the refusal arm: a published state with no pull request or head to name cannot be +// spelled as a result, and inventing either would be the fabricated answer. +type AttemptResultStanding + = AttemptResultTerminal { outcome: AttemptResultOutcome } + | AttemptResultPending { handoff: String } + | AttemptResultUnstatable { reason: String } + +fn attempt_result_standing(handoff: CandidateHandoffState) -> AttemptResultStanding { + match handoff { + HandoffAwaitingWorker => AttemptResultPending { handoff: candidate_handoff_state_key(state: handoff) } + HandoffSubmitted { head_sha: _ } => AttemptResultPending { handoff: candidate_handoff_state_key(state: handoff) } + HandoffAwaitingIntegration { head_sha: _ } => AttemptResultPending { handoff: candidate_handoff_state_key(state: handoff) } + HandoffPublicationOwed { head_sha: _ } => AttemptResultPending { handoff: candidate_handoff_state_key(state: handoff) } + HandoffYielded { reason: _ } => AttemptResultPending { handoff: candidate_handoff_state_key(state: handoff) } + HandoffEvidenceRefused { reason: _ } => AttemptResultPending { handoff: candidate_handoff_state_key(state: handoff) } + HandoffBlockedOnIntegration { head_sha: _, cause: _ } => AttemptResultPending { handoff: candidate_handoff_state_key(state: handoff) } + HandoffBlockedOnPublication { head_sha: _, cause: _ } => AttemptResultPending { handoff: candidate_handoff_state_key(state: handoff) } + HandoffPublished { head_sha, pr_number: _, url } => + if url == "" || head_sha == "" { + AttemptResultUnstatable { reason: "the attempt reads as published but its pull request or head is empty, so there is no result to name" } + } else { + AttemptResultTerminal { outcome: ResultPublished { pr: url as NonEmptyStr, head: head_sha as NonEmptyStr } } + } + HandoffVerificationFailed { head_sha, verdict_key } => + if head_sha == "" { + AttemptResultUnstatable { reason: "verification failed for a head that is empty, so the result has no subject to name" } + } else { + AttemptResultTerminal { outcome: ResultVerificationFailed { + head: head_sha as NonEmptyStr, + cause: join(["verdict ", verdict_key], "") as NonEmptyStr, + } } + } + HandoffDeclaredBlocked { head_sha, readiness, explanation } => + AttemptResultTerminal { outcome: ResultHandedBack { + reason: join(["the worker declared ", submission_readiness_key(r: readiness), " at ", head_sha, ": ", explanation], "") as NonEmptyStr, + } } + } +} + +// THE REQUEST AN ATTEMPT ANSWERS, JOINED THROUGH ITS LAUNCH BINDING AND NOTHING ELSE. Joined is a +// claim event on THIS issue's history that is a Claimed, with the return_to that event states; the +// issue's current assignment is never consulted, so a later claim or a release changes nothing +// about who this attempt answers. No request is an attempt launched autonomously, or before +// bindings were recorded: it has no requester and never will. Unjoinable is a binding naming a +// claim the issue's history does not carry as a Claimed (absent, another issue's, another kind); +// the launch established that claim before it committed the binding, so this is a log that has +// LOST something and is a failure, not a quiet outcome. Unreadable is a binding file that is there +// and cannot be read, which may clear. +type ResultRequestJoin + = ResultRequestJoined { claim: RoadmapEventId, return_to: EventPrincipal } + | ResultRequestNone { reason: String } + | ResultRequestUnjoinable { reason: String } + | ResultRequestUnreadable { reason: String } + +fn result_claim_join(node_envelopes: List, claim: RoadmapEventId) -> ResultRequestJoin { + match filter(node_envelopes, e => (e.id as String) == (claim as String)).first() { + Absent => ResultRequestUnjoinable { reason: join(["the attempt is bound to claim ", claim as String, ", which is not an event on this issue's history"], "") } + Present { value: env } => + match env.event.kind { + Claimed { assignee: _, return_to } => ResultRequestJoined { claim: env.id, return_to: return_to } + Released => ResultRequestUnjoinable { reason: join(["the attempt is bound to event ", claim as String, ", which is not a claim"], "") } + Progress { note: _ } => ResultRequestUnjoinable { reason: join(["the attempt is bound to event ", claim as String, ", which is not a claim"], "") } + Blocked { reason: _ } => ResultRequestUnjoinable { reason: join(["the attempt is bound to event ", claim as String, ", which is not a claim"], "") } + HandedBack { reason: _ } => ResultRequestUnjoinable { reason: join(["the attempt is bound to event ", claim as String, ", which is not a claim"], "") } + ReceiptReferenced { obligation: _, address: _, verdict: _ } => ResultRequestUnjoinable { reason: join(["the attempt is bound to event ", claim as String, ", which is not a claim"], "") } + CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => ResultRequestUnjoinable { reason: join(["the attempt is bound to event ", claim as String, ", which is not a claim"], "") } + CommentEdited { comment: _, body: _ } => ResultRequestUnjoinable { reason: join(["the attempt is bound to event ", claim as String, ", which is not a claim"], "") } + CommentDeleted { comment: _ } => ResultRequestUnjoinable { reason: join(["the attempt is bound to event ", claim as String, ", which is not a claim"], "") } + ResultReturned { attempt: _, claim: _, outcome: _ } => ResultRequestUnjoinable { reason: join(["the attempt is bound to event ", claim as String, ", which is not a claim"], "") } + } + } +} + +fn result_request_join(binding: AttemptRequestBindingRead, node_envelopes: List) -> ResultRequestJoin { + match binding { + RequestBindingAbsent => ResultRequestNone { reason: "the attempt recorded no request binding at launch, so no request is known for it" } + RequestBindingUnreadable { reason } => ResultRequestUnreadable { reason: join(["the attempt's request binding could not be read: ", reason], "") } + RequestBindingRead { binding: RequestAutonomous } => ResultRequestNone { reason: "the attempt was launched autonomously, so there is no requester" } + RequestBindingRead { binding: RequestClaimBound { claim } } => result_claim_join(node_envelopes: node_envelopes, claim: claim) + } +} + +// THE RESULT ALREADY ON THE LOG FOR THIS EXACT IDENTITY: a ResultReturned on the binding's issue, +// for the binding's attempt, naming the binding's claim. Only that event answers the attempt. A +// result carrying the same attempt key on another issue, or naming another claim, is not this +// attempt's answer and does not take it out of the population still owed one. +fn result_attempt_answer(envelopes: List, binding: RoadmapResultBinding) -> RoadmapEventEnvelope? { + filter(roadmap_results_for_attempt(envs: envelopes, attempt: binding.attempt as String), e => + (e.event.node as String) == (binding.node as String) && (match e.event.kind { + ResultReturned { attempt: _, claim, outcome: _ } => (claim as String) == (binding.claim as String) + Claimed { assignee: _, return_to: _ } => false + Released => false + Progress { note: _ } => false + Blocked { reason: _ } => false + HandedBack { reason: _ } => false + ReceiptReferenced { obligation: _, address: _, verdict: _ } => false + CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => false + CommentEdited { comment: _, body: _ } => false + CommentDeleted { comment: _ } => false + })).first() +} + +// THE WRITE DECISION, PURE, OVER EVERY ISSUE'S EVENTS AS READ: the same admission the bound append +// applies inside its own snapshot (roadmap_bound_result_admission), asked here first so a pass that +// has nothing to write performs no append. This is a PRECHECK and decides nothing about what is +// written: the append is handed the issue, the attempt and the outcome, re-reads the attempt's own +// request record, and derives the claim itself. Already returned names the identical result on the log. +// Refused carries the rule that refused -- a result for the attempt on another issue, a different +// result on this one, a claim the history does not carry, a history with no single head. +type ResultReturnDecision + = ResultReturnAppend { binding: RoadmapResultBinding, outcome: AttemptResultOutcome } + | ResultAlreadyReturned { id: RoadmapEventId } + | ResultReturnRefused { step: String, reason: String } + +fn result_return_decision(binding: RoadmapResultBinding, outcome: AttemptResultOutcome, envelopes: List) -> ResultReturnDecision { + match roadmap_bound_result_admission(envs: envelopes, binding: binding, outcome: outcome) { + ResultAdmitted { parent: _ } => ResultReturnAppend { binding: binding, outcome: outcome } + ResultAlreadyOnLog { id } => ResultAlreadyReturned { id: id } + ResultNotAdmitted { step, reason } => ResultReturnRefused { step: step, reason: reason } + } +} + +// ONE ATTEMPT'S PASS OUTCOME, every arm a named state. Recorded and already-recorded are delivered. +// Not-terminal is an attempt still in motion and stays eligible. No-request is the one PERMANENT +// quiet outcome -- an autonomous attempt, or one launched before bindings were recorded -- and it is +// derived from the attempt's durable launch binding on every pass, so nothing is written beside it. +// Failed is a result that is owed and was not delivered, and stays eligible: it includes a bound +// claim the log no longer carries and a result the log holds for this attempt under another +// identity, neither of which may pass quietly. +type ResultReturnOutcome + = ResultReturnRecorded { node_id: String, attempt_key: String, event: String, outcome_word: String } + | ResultReturnAlreadyRecorded { node_id: String, attempt_key: String, event: String } + | ResultReturnNotTerminal { node_id: String, attempt_key: String, handoff: String } + | ResultReturnNoRequest { node_id: String, attempt_key: String, reason: String } + | ResultReturnFailed { node_id: String, attempt_key: String, step: String, reason: String } + +fn result_envelopes_of_node(envelopes: List, node_id: String) -> List { + filter(envelopes, e => (e.event.node as String) == node_id) +} + +// THE ATTEMPTS THE PASS CONSIDERS: every attempt of the durable history that has a key. The +// population is the HISTORY (one ref per attempt ever dispatched), not the node's current attempt, +// and no result event takes an attempt out of it by attempt key alone: whether an attempt is +// answered is decided per attempt against its own binding (result_attempt_answer), so an attempt +// whose result could not be appended is still here after a newer attempt of the same node has +// become current, and a result misaddressed under its key does not hide it. +fn result_return_keyed_attempts(attempts: List) -> List { + filter(attempts, a => a.attempt_key != "") +} + +fn result_return_for_progress( + instance: HostDashboardInstance, + progress: WorkflowAttemptProgress, + binding: RoadmapResultBinding, + envelopes: List, + recorded_at: String, +) -> ResultReturnOutcome { + match attempt_result_standing(handoff: progress.handoff) { + AttemptResultPending { handoff } => + ResultReturnNotTerminal { node_id: progress.node_id, attempt_key: progress.attempt_key, handoff: handoff } + AttemptResultUnstatable { reason } => + ResultReturnFailed { node_id: progress.node_id, attempt_key: progress.attempt_key, step: "outcome", reason: reason } + AttemptResultTerminal { outcome } => + match result_return_decision(binding: binding, outcome: outcome, envelopes: envelopes) { + ResultAlreadyReturned { id } => + ResultReturnAlreadyRecorded { node_id: progress.node_id, attempt_key: progress.attempt_key, event: id as String } + ResultReturnRefused { step, reason } => + ResultReturnFailed { node_id: progress.node_id, attempt_key: progress.attempt_key, step: step, reason: reason } + ResultReturnAppend { binding: bound, outcome: final } => + match roadmap_bound_result_append(instance: instance, node: bound.node, attempt: bound.attempt as String, outcome: final, author: EventPrincipalAuthenticated { ref: fabric_workflow_principal }, recorded_at: recorded_at) { + ResultAppended { id } => + ResultReturnRecorded { node_id: progress.node_id, attempt_key: progress.attempt_key, event: id as String, outcome_word: attempt_result_outcome_word(o: final) } + ResultAppendAlreadyPresent { id } => + ResultReturnAlreadyRecorded { node_id: progress.node_id, attempt_key: progress.attempt_key, event: id as String } + ResultAppendRefused { step, reason } => + ResultReturnFailed { node_id: progress.node_id, attempt_key: progress.attempt_key, step: step, reason: reason } + } + } + } +} + +// WHAT ONE ATTEMPT NEEDS BEFORE ITS EVIDENCE IS READ, pure: the request joined from the binding +// file alone, then the log asked whether this exact identity is already answered. Only an attempt +// that answers a request and is not yet answered has its evidence gathered. +type ResultReturnPrecheck + = ResultPrecheckSettled { outcome: ResultReturnOutcome } + | ResultPrecheckOwed { binding: RoadmapResultBinding } + +fn result_return_precheck(node_id: String, attempt_key: String, binding: AttemptRequestBindingRead, envelopes: List) -> ResultReturnPrecheck { + match result_request_join(binding: binding, node_envelopes: result_envelopes_of_node(envelopes: envelopes, node_id: node_id)) { + ResultRequestNone { reason } => ResultPrecheckSettled { outcome: ResultReturnNoRequest { node_id: node_id, attempt_key: attempt_key, reason: reason } } + ResultRequestUnjoinable { reason } => ResultPrecheckSettled { outcome: ResultReturnFailed { node_id: node_id, attempt_key: attempt_key, step: "claim", reason: reason } } + ResultRequestUnreadable { reason } => ResultPrecheckSettled { outcome: ResultReturnFailed { node_id: node_id, attempt_key: attempt_key, step: "request-binding", reason: reason } } + ResultRequestJoined { claim, return_to: _ } => { + let bound = RoadmapResultBinding { node: node_id as NonEmptyStr as RoadmapNodeId, attempt: attempt_key as NonEmptyStr, claim: claim } + match result_attempt_answer(envelopes: envelopes, binding: bound) { + Present { value: answer } => ResultPrecheckSettled { outcome: ResultReturnAlreadyRecorded { node_id: node_id, attempt_key: attempt_key, event: answer.id as String } } + Absent => ResultPrecheckOwed { binding: bound } + } + } + } +} + +fn result_return_attempt( + instance: HostDashboardInstance, + panes: BeltAttemptPanes, + attempt: DispatchAttemptRef, + envelopes: List, + recorded_at: String, +) -> ResultReturnOutcome { + match result_return_precheck( + node_id: attempt.node_id as String, + attempt_key: attempt.attempt_key, + binding: attempt_request_binding_read_for_instance(instance: instance, node_id: attempt.node_id as RoadmapNodeId, attempt_key: attempt.attempt_key), + envelopes: envelopes, + ) { + ResultPrecheckSettled { outcome } => outcome + ResultPrecheckOwed { binding } => + result_return_for_progress( + instance: instance, + progress: workflow_attempt_progress(evidence: belt_workflow_attempt_evidence_for_key(instance: instance, panes: panes, attempt: attempt, current_key: attempt.attempt_key)), + binding: binding, + envelopes: envelopes, + recorded_at: recorded_at, + ) + } +} + +// THE PASS. Ran carries one outcome per attempt it considered; Withheld is an instance that must +// not write (observe-only); Refused is a pass that could not read what it decides over, named by +// the step, never an empty roster. +type ResultReturnPass + = ResultReturnPassRan { outcomes: List } + | ResultReturnPassWithheld { reason: String } + | ResultReturnPassRefused { step: String, reason: String } + +// THE BELT'S RESULT-RETURN RECONCILIATION, run by every tick (gunbc.roadmap_belt_tick_cli +// belt_run_once_cli_in) whether or not the tick's other passes ran: an outstanding result is a +// durable obligation, and a launch or authority refusal elsewhere does not discharge it. It reads +// the event log once and the attempt refs once, and considers every keyed attempt of the history. +// Nothing is remembered between passes -- delivery is whatever the log says for the attempt's exact +// identity, so a pass that failed, or never ran, is repeated by the next one. +fn result_return_attempts_for_instance(instance: HostDashboardInstance, recorded_at: String) -> ResultReturnPass { + match instance.actuation { + DashboardObserveOnly => + ResultReturnPassWithheld { reason: join(["dashboard instance ", instance.instance_id as String, " is observe-only, so no result was appended"], "") } + DashboardActuating => { + match roadmap_events_read_all(layout: roadmap_event_carrier_layout_for_instance(instance: instance)) { + AllEventsReadRefused { step, reason } => ResultReturnPassRefused { step: join(["events/", step], ""), reason: reason } + AllEventsRead { envelopes } => + match belt_attempts_observe_for_instance(instance: instance) { + AttemptsRefused { reason } => ResultReturnPassRefused { step: "attempts", reason: reason } + AttemptsObserved { attempts } => { + let keyed = result_return_keyed_attempts(attempts: attempts) + if count(keyed) == 0 { + ResultReturnPassRan { outcomes: [] } + } else { + let panes = belt_attempt_panes_observe_for_instance(instance: instance) + ResultReturnPassRan { + outcomes: map(keyed, a => result_return_attempt(instance: instance, panes: panes, attempt: a, envelopes: envelopes, recorded_at: recorded_at)), + } + } + } + } + } + } + } +} + +fn result_return_outcome_failure_line(o: ResultReturnOutcome) -> String? { + match o { + ResultReturnFailed { node_id, attempt_key, step, reason } => + Present { value: join([node_id, " attempt ", attempt_key, " (", step, "): ", reason], "") } + ResultReturnRecorded { node_id: _, attempt_key: _, event: _, outcome_word: _ } => none + ResultReturnAlreadyRecorded { node_id: _, attempt_key: _, event: _ } => none + ResultReturnNotTerminal { node_id: _, attempt_key: _, handoff: _ } => none + ResultReturnNoRequest { node_id: _, attempt_key: _, reason: _ } => none + } +} + +// WHAT THE PASS OWES THE TICK'S EXIT: absent when everything owed was delivered or is honestly not +// owed yet, present with every undelivered attempt and its step when something was not. +fn result_return_pass_failure(pass: ResultReturnPass) -> String? { + match pass { + ResultReturnPassWithheld { reason: _ } => none + ResultReturnPassRefused { step, reason } => + Present { value: join(["result return could not run (", step, "): ", reason], "") } + ResultReturnPassRan { outcomes } => { + let failures = fold(outcomes, init: [], f: (acc, o) => match result_return_outcome_failure_line(o: o) { + Present { value: line } => concat(acc, [line]) + Absent => acc + }) + if count(failures) > 0 { + Present { value: join([to_string(value: count(failures)), " attempt result(s) could not be returned: ", join(failures, "; ")], "") } + } else { + none + } + } + } +} + +// A RESULT THAT IS OWED AND WAS NOT DELIVERED STOPS THE LINE: a nonzero exit naming each attempt and +// the step that refused. Not-terminal and no-request are counted outcomes, never failures. +fn result_return_pass_exit(pass: ResultReturnPass) -> ProcessExit { + match result_return_pass_failure(pass: pass) { + Present { value: reason } => exit_failure(reason: reason) + Absent => ExitSuccess + } +} + +// WHAT A READER OF ONE ATTEMPT GETS. The attempt is named by its issue AND its key, and its answer +// is the result on that issue naming the claim the attempt was bound to at launch. Observed is that +// result, with the event that states it, the claim, and the principal the claim returns to. Not +// returned is a readable issue history with no result for the attempt. Not owed is an attempt with +// no request (autonomous, or launched before bindings were recorded). Unobserved is everything that +// is none of those: a binding or log that could not be read, a history that cannot be ordered, more +// than one result for the attempt, or a result naming a claim other than the bound one -- each +// named by its step, and none of them reported as the attempt's answer. +type ResultReturnedObservation + = ResultReturnedObserved { node: RoadmapNodeId, event: RoadmapEventId, claim: RoadmapEventId, return_to: EventPrincipal, outcome: AttemptResultOutcome, recorded_at: String } + | ResultNotReturned { attempt_key: String } + | ResultNotOwed { reason: String } + | ResultReturnedUnobserved { step: String, reason: String } + +fn result_returned_observation_of_envelope(env: RoadmapEventEnvelope, node_envelopes: List, bound_claim: RoadmapEventId) -> ResultReturnedObservation { + match env.event.kind { + ResultReturned { attempt: _, claim, outcome } => + if (claim as String) != (bound_claim as String) { + ResultReturnedUnobserved { step: "binding", reason: join(["the issue carries a result for this attempt naming claim ", claim as String, ", but the attempt is bound to claim ", bound_claim as String], "") } + } else { + match result_claim_join(node_envelopes: node_envelopes, claim: claim) { + ResultRequestJoined { claim: joined, return_to } => + ResultReturnedObserved { node: env.event.node, event: env.id, claim: joined, return_to: return_to, outcome: outcome, recorded_at: env.event.recorded_at } + ResultRequestUnjoinable { reason } => ResultReturnedUnobserved { step: "claim", reason: reason } + ResultRequestNone { reason } => ResultReturnedUnobserved { step: "claim", reason: reason } + ResultRequestUnreadable { reason } => ResultReturnedUnobserved { step: "claim", reason: reason } + } + } + Claimed { assignee: _, return_to: _ } => ResultReturnedUnobserved { step: "kind", reason: "the selected event is not a result" } + Released => ResultReturnedUnobserved { step: "kind", reason: "the selected event is not a result" } + Progress { note: _ } => ResultReturnedUnobserved { step: "kind", reason: "the selected event is not a result" } + Blocked { reason: _ } => ResultReturnedUnobserved { step: "kind", reason: "the selected event is not a result" } + HandedBack { reason: _ } => ResultReturnedUnobserved { step: "kind", reason: "the selected event is not a result" } + ReceiptReferenced { obligation: _, address: _, verdict: _ } => ResultReturnedUnobserved { step: "kind", reason: "the selected event is not a result" } + CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => ResultReturnedUnobserved { step: "kind", reason: "the selected event is not a result" } + CommentEdited { comment: _, body: _ } => ResultReturnedUnobserved { step: "kind", reason: "the selected event is not a result" } + CommentDeleted { comment: _ } => ResultReturnedUnobserved { step: "kind", reason: "the selected event is not a result" } + } +} + +// THE READ, PURE OVER THE ATTEMPT'S BINDING AND ITS ISSUE'S HISTORY AS READ, so a witness drives it +// with constructed values while the two reads stay in result_returned_observe_for_attempt. Only the +// named issue's history is consulted: a result carrying this attempt key on any other issue is not +// read, so it cannot be returned as this attempt's answer. +fn result_returned_observation_of(read: RoadmapEventsRead, binding: AttemptRequestBindingRead, node_id: String, attempt_key: String) -> ResultReturnedObservation { + if attempt_key == "" || node_id == "" { + ResultReturnedUnobserved { step: "attempt-identity", reason: "the issue or the attempt key is empty, so there is no attempt to read a result for" } + } else { + match binding { + RequestBindingAbsent => ResultNotOwed { reason: "the attempt recorded no request binding at launch, so no request is known for it" } + RequestBindingRead { binding: RequestAutonomous } => ResultNotOwed { reason: "the attempt was launched autonomously, so there is no requester" } + RequestBindingUnreadable { reason } => ResultReturnedUnobserved { step: "request-binding", reason: reason } + RequestBindingRead { binding: RequestClaimBound { claim } } => + match read { + EventsReadRefused { node: _, step, reason } => ResultReturnedUnobserved { step: join(["events/", step], ""), reason: reason } + EventsRead { node: _, envelopes } => { + let node_envelopes = result_envelopes_of_node(envelopes: envelopes, node_id: node_id) + match roadmap_history_unordered_reason(envs: node_envelopes) { + Present { value: why } => ResultReturnedUnobserved { step: "history", reason: join(["issue ", node_id, ": ", why], "") } + Absent => { + let results = roadmap_results_for_attempt(envs: node_envelopes, attempt: attempt_key) + if count(results) > 1 { + ResultReturnedUnobserved { step: "result-duplicate", reason: join(["issue ", node_id, " carries ", to_string(value: count(results)), " results for attempt ", attempt_key, "; an attempt has one"], "") } + } else { + match results.first() { + Present { value: env } => result_returned_observation_of_envelope(env: env, node_envelopes: node_envelopes, bound_claim: claim) + Absent => ResultNotReturned { attempt_key: attempt_key } + } + } + } + } + } + } + } + } +} + +// ONE ATTEMPT'S RESULT, READ FOR ITS EXACT IDENTITY: the attempt's launch binding from its attempt +// state and its issue's history from the event log. Its consumer in this module is +// result_returned_report, the operator's entry. +// DECLARED FRONTIER: the dogfood route receipt (gunbc.roadmap_dogfood_route) is to fold this reader +// as its result-returned stage; the trigger is the change that makes that receipt call it -- until +// then no receipt consumes this function and none is claimed. +fn result_returned_observe_for_attempt(instance: HostDashboardInstance, node_id: String, attempt_key: String) -> ResultReturnedObservation { + if attempt_key == "" || node_id == "" { + ResultReturnedUnobserved { step: "attempt-identity", reason: "the issue or the attempt key is empty, so there is no attempt to read a result for" } + } else { + result_returned_observation_of( + read: roadmap_events_read(layout: roadmap_event_carrier_layout_for_instance(instance: instance), node: node_id as NonEmptyStr as RoadmapNodeId), + binding: attempt_request_binding_read_for_instance(instance: instance, node_id: node_id as NonEmptyStr as RoadmapNodeId, attempt_key: attempt_key), + node_id: node_id, + attempt_key: attempt_key, + ) + } +} + +fn result_returned_observation_text(o: ResultReturnedObservation) -> String { + match o { + ResultReturnedObserved { node, event, claim, return_to, outcome, recorded_at } => + join(["returned on issue ", node as String, " (event ", event as String, ", ", recorded_at, ") for claim ", claim as String, " to ", event_principal_label(p: return_to), ": ", attempt_result_outcome_text(o: outcome)], "") + ResultNotReturned { attempt_key } => join(["no result has been returned for attempt ", attempt_key], "") + ResultNotOwed { reason } => join(["no result is owed: ", reason], "") + ResultReturnedUnobserved { step, reason } => join(["the result could not be read (", step, "): ", reason], "") + } +} + +// THE OPERATOR'S READ OF ONE ATTEMPT'S RESULT, a String because `gunbc run` prints a String return +// inside its typed envelope (gunbc.roadmap.roadmap_event_cli roadmap_event_standing_report): +// gunbc run --entry dag/gunbc/roadmap/roadmap_result_returned.dag --function result_returned_report \ +// --arg repo_root= --arg node_id= --arg attempt_key= +fn result_returned_report(repo_root: String, node_id: String, attempt_key: String) -> String { + match dashboard_instance_for_repo_root(repo_root: repo_root) { + DashboardInstanceRootUnknown { repo_root: w } => join(["result_returned_report: no dashboard instance is rooted at ", w], "") + DashboardInstanceResolved { instance } => + result_returned_observation_text(o: result_returned_observe_for_attempt(instance: instance, node_id: node_id, attempt_key: attempt_key)) + } +} diff --git a/dag/gunbc/roadmap/roadmap_serve.dag b/dag/gunbc/roadmap/roadmap_serve.dag index 47e6851affd..e323070973e 100644 --- a/dag/gunbc/roadmap/roadmap_serve.dag +++ b/dag/gunbc/roadmap/roadmap_serve.dag @@ -1,5 +1,6 @@ module gunbc.roadmap_serve +import gunbc.roadmap.roadmap_attempt_request_binding { AttemptLaunchRequest, LaunchForClaim, LaunchAutonomous } import gunbc.roadmap.roadmap_event_log { StandingsObserved, NodeStandingRow } import gunbc.roadmap.roadmap_event_log { roadmap_issue_state } @@ -502,7 +503,7 @@ fn serve_dispatch_handler_response_over( ) } else { belt_dispatch_response( - r: belt_dispatch_node_for_instance_over(instance: instance, plan: plan, node_id: node_id), + r: belt_dispatch_node_for_instance_over(instance: instance, plan: plan, node_id: node_id, request: LaunchAutonomous), ) } } @@ -1003,7 +1004,12 @@ fn serve_assignment_launch_follow_through_over( node_id: node_id, assignee_display: assignee_display, event_id: event_id, - outcome: belt_dispatch_node_for_instance_over(instance: instance, plan: plan, node_id: node_id), + outcome: belt_dispatch_node_for_instance_over( + instance: instance, + plan: plan, + node_id: node_id, + request: LaunchForClaim { node: node_id as NonEmptyStr as RoadmapNodeId, claim: event_id as NonEmptyStr as RoadmapEventId }, + ), ) } else { serve_text_response(status: 200, content_type: text_plain_utf8, body: join(["assigned: ", node_id, " → ", assignee_display, " (event ", event_id, ")\n"], "")) diff --git a/dag/gunbc/roadmap/roadmap_workflow_progress.dag b/dag/gunbc/roadmap/roadmap_workflow_progress.dag index 27b60d237a1..5bdb8f03609 100644 --- a/dag/gunbc/roadmap/roadmap_workflow_progress.dag +++ b/dag/gunbc/roadmap/roadmap_workflow_progress.dag @@ -245,6 +245,7 @@ type WorkflowAttemptProgress { segments: List reconciliation: WorkflowReconcileReport rounds: HarnessRoundLedger + handoff: CandidateHandoffState } // The row progress bar is a pure projection over independent evidence, never a stored workflow @@ -768,13 +769,15 @@ fn submission_segment( } } -// The submission standing once the integration receipt is readable (absent or for this head): -// hoisted out of submission_segment so its integration match can name every arm. -fn submission_segment_from_handoff( +// THE ATTEMPT'S HANDOFF STATE, DERIVED ONCE from the evidence the progress fold already holds. The +// submission segment paints it and the progress row carries it, so the consumer that returns an +// attempt's result to its requester (gunbc.roadmap.roadmap_result_returned) reads the same state +// the row shows and cannot classify an attempt the row classifies otherwise. +fn workflow_attempt_handoff_state( provider: ProviderExecutionState, evidence: WorkflowAttemptEvidence, -) -> WorkflowSegment { - let state = candidate_handoff_state( +) -> CandidateHandoffState { + candidate_handoff_state( provider: provider, capture: evidence.submission_capture, verification: handoff_verification_standing( @@ -795,6 +798,15 @@ fn submission_segment_from_handoff( publication_source: evidence.publication_source, ), ) +} + +// The submission standing once the integration receipt is readable (absent or for this head): +// hoisted out of submission_segment so its integration match can name every arm. +fn submission_segment_from_handoff( + provider: ProviderExecutionState, + evidence: WorkflowAttemptEvidence, +) -> WorkflowSegment { + let state = workflow_attempt_handoff_state(provider: provider, evidence: evidence) match state { HandoffAwaitingWorker => WorkflowSegment { @@ -1340,6 +1352,7 @@ fn workflow_attempt_progress( segments: segments, ), rounds: harness_round_ledger_from_events(body: evidence.provider_events), + handoff: workflow_attempt_handoff_state(provider: provider, evidence: evidence), } } diff --git a/dag/test/claim/long/roadmap_page_witness_test.dag b/dag/test/claim/long/roadmap_page_witness_test.dag index b2a06bf0bd5..9a48911a9ff 100644 --- a/dag/test/claim/long/roadmap_page_witness_test.dag +++ b/dag/test/claim/long/roadmap_page_witness_test.dag @@ -2330,7 +2330,7 @@ test fn witness_issue_comment_composer_posts_urlencoded_with_stable_key_and_csrf } } -// THE PUBLICATION HANDOFF (owner mandate control 9): an observed publication renders ONE compact +// THE PUBLICATION HANDOFF (owner mandate control 9): a published attempt result renders ONE compact // automated entry in the Comments stream — fabric's service avatar, the PR and head, and the // merge owner the fold reassigned to (the stored return_to) — while the standing the page reads // becomes ready for review and the History tab carries the raw event. @@ -2342,7 +2342,7 @@ fn fx_handoff_events() -> List false EmitOk { html: h } => - occurrence_count(s: h, pattern: "data-event-kind=\"publication_observed\"") == 2 + occurrence_count(s: h, pattern: "data-event-kind=\"result_returned\"") == 2 + && string_contains(s: h, pattern: "data-result-outcome=\"published\" data-attempt=\"feedfacefeedface\"") && string_contains(s: h, pattern: "") && string_contains(s: h, pattern: "published gunb-ai/gunbc#12210 at 52c56a8 — reassigned for merge to principal:google-oidc/accounts/sub-viewer") && string_contains(s: h, pattern: "ready for review — published gunb-ai/gunbc#12210 at 52c56a8; reassigned for merge to principal:google-oidc/accounts/sub-viewer") @@ -2371,6 +2372,41 @@ test fn witness_issue_publication_handoff_renders_the_compact_automated_entry() } } +// A RESULT THAT IS NOT A PUBLICATION IS STILL RETURNED ON THE PAGE (operator ruling 2026-10-03): a +// failed verification renders its own entry naming the head, the cause and the principal it was +// returned to, and it does NOT read as a reassignment for merge -- the standing stays claimed. +test fn witness_issue_failed_verification_result_renders_without_a_merge_handoff() -> Bool { + let claim = fx_event_envelope(id: "rr-1", at: "2026-09-25T10:00:00Z", kind: gunbc.roadmap.roadmap_event_log.Claimed { assignee: fixture_fabric_principal(), return_to: fixture_human_principal() }) + let failed = gunbc.roadmap.roadmap_event_log.RoadmapEventEnvelope { + id: "rr-2" as gunbc.roadmap.roadmap_event_log.RoadmapEventId, + event: gunbc.roadmap.roadmap_event_log.RoadmapEvent { + node: "issue-detail-specimen" as gunbc.roadmap_model.RoadmapNodeId, + author: fixture_fabric_principal(), + parent: Present { value: "rr-1" as gunbc.roadmap.roadmap_event_log.RoadmapEventId }, + kind: gunbc.roadmap.roadmap_event_log.ResultReturned { attempt: "feedfacefeedface" as NonEmptyStr, claim: "rr-1" as gunbc.roadmap.roadmap_event_log.RoadmapEventId, outcome: gunbc.roadmap.roadmap_event_log.ResultVerificationFailed { head: "52c56a8" as NonEmptyStr, cause: "verdict validation-failed" as NonEmptyStr } }, + recorded_at: "2026-09-25T11:00:00Z", + }, + } + match try_serialize_html_source(node: fx_issue_detail_body( + view: issue_detail_fixture_view(), + panel: issue_detail_fixture_panel(), + observation: gunbc.roadmap_presentation.ObservationsNominal, + attempts: [], + events: [claim, failed], + viewer: "principal:google-oidc/accounts/sub-viewer", + recents: [ fixture_teammate_legacy() ], + expected_revision: "", + command_context: IssueFormContext { operation: "", csrf: "" }, + profiles: [])) { + EmitRejected { reason: _ } => false + EmitOk { html: h } => + string_contains(s: h, pattern: "data-result-outcome=\"verification_failed\" data-attempt=\"feedfacefeedface\"") + && string_contains(s: h, pattern: "verification failed at 52c56a8: verdict validation-failed — returned to principal:google-oidc/accounts/sub-viewer") + && !string_contains(s: h, pattern: "reassigned for merge") + && !string_contains(s: h, pattern: "ready for review") + } +} + // THE CLIPPING FIX, pinned at the layout law (owner defect 2026-09-26: the slug and field labels // rendered cut off at the left viewport edge): the detail page's container carries the SAME side // padding as every other surface — the defect's cause was .issue-detail being a body-level flex @@ -2626,7 +2662,7 @@ test fn witness_issue_detail_rail_and_comments_render_the_profile_identity() -> node: "issue-detail-specimen" as gunbc.roadmap_model.RoadmapNodeId, author: fixture_fabric_principal(), parent: Present { value: "prof-ho-1" as gunbc.roadmap.roadmap_event_log.RoadmapEventId }, - kind: gunbc.roadmap.roadmap_event_log.PublicationObserved { pr: "gunb-ai/gunbc#12210" as NonEmptyStr, head: "52c56a8" as NonEmptyStr }, + kind: gunbc.roadmap.roadmap_event_log.ResultReturned { attempt: "feedfacefeedface" as NonEmptyStr, claim: "prof-ho-1" as gunbc.roadmap.roadmap_event_log.RoadmapEventId, outcome: gunbc.roadmap.roadmap_event_log.ResultPublished { pr: "gunb-ai/gunbc#12210" as NonEmptyStr, head: "52c56a8" as NonEmptyStr } }, recorded_at: "2026-09-25T11:00:00Z", }, } 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 297b953555a..1924250c689 100644 --- a/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag @@ -2414,6 +2414,7 @@ fn staged_post_over_canary(graph: LaunchGraphStanding, standing: LaunchHostStand graph: graph, merged: [], node_id: node_id, + request: LaunchAutonomous, ) } diff --git a/dag/test/claim/roadmap/roadmap_event_log_witness_test.dag b/dag/test/claim/roadmap/roadmap_event_log_witness_test.dag index a359175b4ff..a65db59f7d5 100644 --- a/dag/test/claim/roadmap/roadmap_event_log_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_event_log_witness_test.dag @@ -15,7 +15,9 @@ import gunbc.roadmap.roadmap_event_log { roadmap_node_standing, roadmap_node_standing_key, roadmap_node_standing_text, IssueAssignmentProjection, roadmap_issue_assignment, IssueComment, roadmap_issue_comments, roadmap_comment_created_key_present, roadmap_comment_retry_present, issue_comment_anchor, - roadmap_recent_claimed_assignees, roadmap_events_newest_first, roadmap_publication_handoff_event, PublicationObserved, NodeReadyForReview, + roadmap_recent_claimed_assignees, roadmap_events_newest_first, roadmap_result_returned_event, ResultReturned, AttemptResultOutcome, ResultPublished, ResultVerificationFailed, ResultHandedBack, NodeReadyForReview, + roadmap_event_admitted_after, roadmap_results_for_attempt, + RoadmapResultBinding, RoadmapResultAdmission, ResultAdmitted, ResultAlreadyOnLog, ResultNotAdmitted, roadmap_bound_result_admission, } fn node() -> RoadmapNodeId { "demo-node" as RoadmapNodeId } @@ -114,7 +116,7 @@ test fn a_v2_claimed_event_decodes_as_a_legacy_email_principal_never_auto_matche CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => false CommentEdited { comment: _, body: _ } => false CommentDeleted { comment: _ } => false - PublicationObserved { pr: _, head: _ } => false + ResultReturned { attempt: _, claim: _, outcome: _ } => false }) && (roadmap_event_id(e: envelope.event) as String) != (id as String) RoadmapEventUndecodable { reason: _ } => false @@ -142,7 +144,7 @@ test fn a_v1_claimed_event_still_reads_as_a_legacy_self_claim() -> Bool { CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => false CommentEdited { comment: _, body: _ } => false CommentDeleted { comment: _ } => false - PublicationObserved { pr: _, head: _ } => false + ResultReturned { attempt: _, claim: _, outcome: _ } => false }) && (roadmap_event_id(e: envelope.event) as String) != (id as String) RoadmapEventUndecodable { reason: _ } => false @@ -334,15 +336,14 @@ test fn an_edit_or_delete_with_an_unknown_comment_id_is_an_honest_no_op() -> Boo && (match comments.first() { Present { value: c } => (c.body as String) == "one" && !c.deleted && (c.edited_at as String) == "" Absent => false }) } -// THE RETURN-TO-MERGE HANDOFF (owner mandate 2026-09-25): an observed publication is a durable -// event (PublicationObserved { pr, head }, codec round-trip pinned); the fold then moves the -// holder to the stored return_to and the standing reads READY FOR REVIEW — merge stays the human -// completion action (publication never closes the issue), and a publication with no recorded -// return_to changes no holder. -test fn the_publication_handoff_reassigns_to_the_stored_return_to() -> Bool { +// THE RETURN-TO-MERGE HANDOFF (owner mandate 2026-09-25): a published attempt result is a durable +// event (ResultReturned with a ResultPublished outcome, codec round-trip pinned); the fold moves the +// holder to the return_to OF THE CLAIM THE EVENT NAMES and the standing reads READY FOR REVIEW — merge stays the +// human completion action (publication never closes the issue). +test fn the_publication_handoff_reassigns_to_the_return_to_of_the_results_claim() -> Bool { let fabric = event_principal_authenticated(ref: fabric_workflow_principal()) let claim = ev(kind: Claimed { assignee: fabric, return_to: human_principal() }, parent: none, author: human_principal(), at: "t1") - let pub = ev(kind: PublicationObserved { pr: "gunb-ai/gunbc#12210" as NonEmptyStr, head: "52c56a8" as NonEmptyStr }, parent: Present { value: claim.id }, author: fabric, at: "t2") + let pub = ev(kind: ResultReturned { attempt: "feedfacefeedface" as NonEmptyStr, claim: claim.id, outcome: ResultPublished { pr: "gunb-ai/gunbc#12210" as NonEmptyStr, head: "52c56a8" as NonEmptyStr } }, parent: Present { value: claim.id }, author: fabric, at: "t2") let folded = roadmap_node_standing(envs: [claim, pub]) let key_ok = roadmap_node_standing_key(s: folded) == "ready_for_review" let holder_ok = match folded { NodeReadyForReview { by, pr, head } => @@ -351,40 +352,156 @@ test fn the_publication_handoff_reassigns_to_the_stored_return_to() -> Bool { && (head as String) == "52c56a8" _ => false } let text_ok = string_contains(s: roadmap_node_standing_text(s: folded), pattern: "reassigned for merge to principal:google-oidc/accounts/sub-1") + let orphan = ev(kind: ResultReturned { attempt: "feedfacefeedface" as NonEmptyStr, claim: "0000000000000000" as RoadmapEventId, outcome: ResultPublished { pr: "p" as NonEmptyStr, head: "h" as NonEmptyStr } }, parent: Present { value: claim.id }, author: fabric, at: "t2") + let unjoined_moves_nothing = roadmap_node_standing_key(s: roadmap_node_standing(envs: [claim, orphan])) == "claimed" let projection_ok = match roadmap_issue_assignment(envs: [claim, pub]) { IssueAssignmentProjection { holder, return_to: _ } => match holder { Present { value: h } => event_principal_label(p: h) == event_principal_label(p: human_principal()) Absent => false } _ => false } - let note = ev(kind: Progress { note: "work without an assignment" as NonEmptyStr }, parent: none, author: human_principal(), at: "t0") - let no_return_to = roadmap_node_standing(envs: [note, ev(kind: PublicationObserved { pr: "p" as NonEmptyStr, head: "h" as NonEmptyStr }, parent: Present { value: note.id }, author: human_principal(), at: "t1")]) - let merge_is_human = roadmap_node_standing_key(s: no_return_to) == "in_progress" let wire_round_trip = match roadmap_event_decode(text: roadmap_event_wire_text(env: pub)) { RoadmapEventDecoded { envelope } => - (envelope.id as String) == (pub.id as String) - && (match envelope.event.kind { PublicationObserved { pr, head } => (pr as String) == "gunb-ai/gunbc#12210" && (head as String) == "52c56a8" _ => false }) + (envelope.id as String) == (pub.id as String) && envelope.event.kind == pub.event.kind RoadmapEventUndecodable { reason: _ } => false } - key_ok && holder_ok && text_ok && projection_ok && merge_is_human && wire_round_trip -} - -// THE HANDOFF EVENT CONSTRUCTOR the belt's publication-observation pass will append: the -// PublicationObserved kind on the chained-idiom shape, content-addressed like every event. -test fn the_handoff_event_constructor_records_the_publication_fact() -> Bool { - let e = roadmap_publication_handoff_event( - node: node(), - pr: "gunb-ai/gunbc#12210" as NonEmptyStr, - head: "52c56a8" as NonEmptyStr, - author: event_principal_authenticated(ref: fabric_workflow_principal()), - parent: none, - recorded_at: "t1", - ) - let env = roadmap_event_envelope(e: e) - string_length(s: env.id as String) > 8 - && (match roadmap_event_decode(text: roadmap_event_wire_text(env: env)) { - RoadmapEventDecoded { envelope } => - (envelope.id as String) == (env.id as String) - && (match envelope.event.kind { PublicationObserved { pr, head } => (pr as String) == "gunb-ai/gunbc#12210" && (head as String) == "52c56a8" _ => false }) + key_ok && holder_ok && text_ok && projection_ok && wire_round_trip && unjoined_moves_nothing +} + +// ONLY A PUBLISHED RESULT MOVES THE HOLDER. A failed verification and a hand-back are +// answers on the history: the standing and the holder stay exactly what they were before the +// result, so a result that is not a candidate never reads as ready for review. +test fn an_unpublished_result_changes_no_holder_and_no_standing() -> Bool { + let fabric = event_principal_authenticated(ref: fabric_workflow_principal()) + let claim = ev(kind: Claimed { assignee: fabric, return_to: human_principal() }, parent: none, author: human_principal(), at: "t1") + let outcomes = [ + ResultVerificationFailed { head: "52c56a8" as NonEmptyStr, cause: "verdict validation-failed" as NonEmptyStr }, + ResultHandedBack { reason: "the worker declared blocked" as NonEmptyStr }, + ] + all(outcomes, o => { + let result = ev(kind: ResultReturned { attempt: "feedfacefeedface" as NonEmptyStr, claim: claim.id, outcome: o }, parent: Present { value: claim.id }, author: fabric, at: "t2") + roadmap_node_standing_key(s: roadmap_node_standing(envs: [claim, result])) == "claimed" + && (match roadmap_issue_assignment(envs: [claim, result]).holder { Present { value: h } => event_principal_label(p: h) == event_principal_label(p: fabric) Absent => false }) + }) +} + +// EVERY OUTCOME ROUND-TRIPS THE WIRE WITH ITS ID, and a result missing a member its outcome names is +// UNDECODABLE rather than a result with a blank: the red is the published wire with its head member +// removed (the id recomputed over the altered bytes would not matter -- the kind decode refuses +// first), and an outcome word this reader does not know. +test fn every_result_outcome_round_trips_and_a_result_missing_its_member_refuses() -> Bool { + let fabric = event_principal_authenticated(ref: fabric_workflow_principal()) + let outcomes = [ + ResultPublished { pr: "gunb-ai/gunbc#12210" as NonEmptyStr, head: "52c56a8" as NonEmptyStr }, + ResultVerificationFailed { head: "52c56a8" as NonEmptyStr, cause: "verdict validation-failed" as NonEmptyStr }, + ResultHandedBack { reason: "the worker declared blocked" as NonEmptyStr }, + ] + let round_trips = all(outcomes, o => { + let env = roadmap_event_envelope(e: roadmap_result_returned_event(binding: RoadmapResultBinding { node: node(), attempt: "feedfacefeedface" as NonEmptyStr, claim: "c1a1c1a1c1a1c1a1" as RoadmapEventId }, outcome: o, author: fabric, parent: none, recorded_at: "t1")) + match roadmap_event_decode(text: roadmap_event_wire_text(env: env)) { + RoadmapEventDecoded { envelope } => (envelope.id as String) == (env.id as String) && envelope.event.kind == env.event.kind RoadmapEventUndecodable { reason: _ } => false - }) + } + }) + let headless = "{\"id\":\"0000000000000000\",\"event\":{\"schema\":\"roadmap-event/v3\",\"node\":\"demo-node\",\"author\":\"principal:gunbc/workflows/fabric\",\"parent\":\"\",\"kind\":\"result_returned\",\"recorded_at\":\"t1\",\"attempt\":\"feedfacefeedface\",\"claim\":\"c1a1c1a1c1a1c1a1\",\"outcome\":\"published\",\"pr\":\"gunb-ai/gunbc#12210\"}}" + let unknown_outcome = "{\"id\":\"0000000000000000\",\"event\":{\"schema\":\"roadmap-event/v3\",\"node\":\"demo-node\",\"author\":\"principal:gunbc/workflows/fabric\",\"parent\":\"\",\"kind\":\"result_returned\",\"recorded_at\":\"t1\",\"attempt\":\"feedfacefeedface\",\"claim\":\"c1a1c1a1c1a1c1a1\",\"outcome\":\"merged\",\"pr\":\"p\",\"head\":\"h\"}}" + let refuses = all([headless, unknown_outcome], wire => match roadmap_event_decode(text: wire) { + RoadmapEventDecoded { envelope: _ } => false + RoadmapEventUndecodable { reason } => string_contains(s: reason, pattern: "kind result_returned is unknown or missing its members") + }) + round_trips && refuses +} + +// A RESULT WITHOUT ITS CLAIM IS NOT A RESULT: the wire that names the attempt, the principal and a +// complete outcome but no claim member is undecodable, so no event can be read that answers an +// attempt without saying which request it answers. +test fn a_result_wire_without_its_claim_refuses() -> Bool { + let claimless = "{\"id\":\"0000000000000000\",\"event\":{\"schema\":\"roadmap-event/v3\",\"node\":\"demo-node\",\"author\":\"principal:gunbc/workflows/fabric\",\"parent\":\"\",\"kind\":\"result_returned\",\"recorded_at\":\"t1\",\"attempt\":\"feedfacefeedface\",\"outcome\":\"published\",\"pr\":\"p\",\"head\":\"h\"}}" + match roadmap_event_decode(text: claimless) { + RoadmapEventDecoded { envelope: _ } => false + RoadmapEventUndecodable { reason } => string_contains(s: reason, pattern: "kind result_returned is unknown or missing its members") + } +} + +fn note_env(parent: RoadmapEventId) -> RoadmapEventEnvelope { + ev(kind: Progress { note: "not a claim" as NonEmptyStr }, parent: Present { value: parent }, author: human_principal(), at: "t2") +} + +fn evlog_binding(claim: RoadmapEventId) -> RoadmapResultBinding { + RoadmapResultBinding { node: node(), attempt: "feedfacefeedface" as NonEmptyStr, claim: claim } +} + +fn evlog_failed_outcome() -> AttemptResultOutcome { + ResultVerificationFailed { head: "52c56a8" as NonEmptyStr, cause: "verdict validation-failed" as NonEmptyStr } +} + +fn evlog_published_outcome() -> AttemptResultOutcome { + ResultPublished { pr: "p" as NonEmptyStr, head: "52c56a8" as NonEmptyStr } +} + +fn evlog_not_admitted_at(a: RoadmapResultAdmission, step: String) -> Bool { + match a { + ResultNotAdmitted { step: s, reason } => s == step && string_length(s: reason) > 0 + ResultAdmitted { parent: _ } => false + ResultAlreadyOnLog { id: _ } => false + } +} + +// THE GENERIC APPEND ADMITS NO RESULT AT ALL. A ResultReturned offered to the admission every append +// asks is refused whatever it says -- a first result, a well-formed one, one naming a real claim -- +// because the generic append has no binding to check it against; every other kind is admitted. +test fn the_generic_append_admits_no_result_event() -> Bool { + let fabric = event_principal_authenticated(ref: fabric_workflow_principal()) + let claim = ev(kind: Claimed { assignee: fabric, return_to: human_principal() }, parent: none, author: human_principal(), at: "t1") + let first_result = RoadmapEvent { node: node(), author: fabric, parent: Present { value: claim.id }, kind: ResultReturned { attempt: "feedfacefeedface" as NonEmptyStr, claim: claim.id, outcome: evlog_failed_outcome() }, recorded_at: "t2" } + let note = RoadmapEvent { node: node(), author: fabric, parent: Present { value: claim.id }, kind: Progress { note: "n" as NonEmptyStr }, recorded_at: "t2" } + !roadmap_event_admitted_after(envs: [claim], event: first_result) + && roadmap_event_admitted_after(envs: [claim], event: note) + && roadmap_event_admitted_after(envs: [], event: claim.event) +} + +// THE BOUND ADMISSION'S POSITIVE CONTROL AND ITS IDEMPOTENCE: a first result for the exact identity +// is admitted on the issue's head; the identical result already on the log is reported as on the +// log and names its event. +test fn a_bound_result_is_admitted_once_on_the_head_and_recognised_when_already_there() -> Bool { + let fabric = event_principal_authenticated(ref: fabric_workflow_principal()) + let claim = ev(kind: Claimed { assignee: fabric, return_to: human_principal() }, parent: none, author: human_principal(), at: "t1") + let note = note_env(parent: claim.id) + let admitted = match roadmap_bound_result_admission(envs: [claim, note], binding: evlog_binding(claim: claim.id), outcome: evlog_failed_outcome()) { + ResultAdmitted { parent } => match parent { Present { value: p } => (p as String) == (note.id as String) Absent => false } + ResultAlreadyOnLog { id: _ } => false + ResultNotAdmitted { step: _, reason: _ } => false + } + let failed = ev(kind: ResultReturned { attempt: "feedfacefeedface" as NonEmptyStr, claim: claim.id, outcome: evlog_failed_outcome() }, parent: Present { value: note.id }, author: fabric, at: "t3") + let already = match roadmap_bound_result_admission(envs: [claim, note, failed], binding: evlog_binding(claim: claim.id), outcome: evlog_failed_outcome()) { + ResultAlreadyOnLog { id } => (id as String) == (failed.id as String) + ResultAdmitted { parent: _ } => false + ResultNotAdmitted { step: _, reason: _ } => false + } + admitted && already && count(roadmap_results_for_attempt(envs: [claim, note, failed], attempt: "feedfacefeedface")) == 1 +} + +// THE BOUND ADMISSION'S REDS, EACH NAMED BY ITS RULE. A different outcome for an answered attempt is +// a second result. The same attempt answered under ANOTHER claim of the same issue is a second +// result. A result for the attempt already on ANOTHER issue refuses this one -- at the append, not +// as an ambiguity found later. A claim that is absent from the issue's history, or an event that is +// not a claim, or a claim that belongs to another issue, is no claim to answer. A forked history +// has no head to follow. +test fn a_bound_result_is_refused_for_a_second_answer_a_foreign_claim_or_another_issue() -> Bool { + let fabric = event_principal_authenticated(ref: fabric_workflow_principal()) + let c1 = ev(kind: Claimed { assignee: fabric, return_to: human_principal() }, parent: none, author: human_principal(), at: "t1") + let c2 = ev(kind: Claimed { assignee: fabric, return_to: legacy("lead@gunb.ai") }, parent: Present { value: c1.id }, author: human_principal(), at: "t2") + let failed = ev(kind: ResultReturned { attempt: "feedfacefeedface" as NonEmptyStr, claim: c1.id, outcome: evlog_failed_outcome() }, parent: Present { value: c2.id }, author: fabric, at: "t3") + let revised = evlog_not_admitted_at(a: roadmap_bound_result_admission(envs: [c1, c2, failed], binding: evlog_binding(claim: c1.id), outcome: evlog_published_outcome()), step: "result-duplicate") + let reclaimed = evlog_not_admitted_at(a: roadmap_bound_result_admission(envs: [c1, c2, failed], binding: evlog_binding(claim: c2.id), outcome: evlog_failed_outcome()), step: "result-duplicate") + let other_node = "other-node" as RoadmapNodeId + let b1 = roadmap_event_envelope(e: RoadmapEvent { node: other_node, author: human_principal(), parent: none, kind: Claimed { assignee: fabric, return_to: human_principal() }, recorded_at: "t1" }) + let on_b = roadmap_event_envelope(e: RoadmapEvent { node: other_node, author: fabric, parent: Present { value: b1.id }, kind: ResultReturned { attempt: "feedfacefeedface" as NonEmptyStr, claim: b1.id, outcome: evlog_published_outcome() }, recorded_at: "t2" }) + let elsewhere = evlog_not_admitted_at(a: roadmap_bound_result_admission(envs: [c1, c2, b1, on_b], binding: evlog_binding(claim: c1.id), outcome: evlog_failed_outcome()), step: "result-elsewhere") + let absent_claim = evlog_not_admitted_at(a: roadmap_bound_result_admission(envs: [c1, c2], binding: evlog_binding(claim: "0000000000000000" as RoadmapEventId), outcome: evlog_failed_outcome()), step: "result-claim") + let note = note_env(parent: c1.id) + let not_a_claim = evlog_not_admitted_at(a: roadmap_bound_result_admission(envs: [c1, note], binding: evlog_binding(claim: note.id), outcome: evlog_failed_outcome()), step: "result-claim") + let foreign_claim = evlog_not_admitted_at(a: roadmap_bound_result_admission(envs: [c1, c2, b1], binding: evlog_binding(claim: b1.id), outcome: evlog_failed_outcome()), step: "result-claim") + let fork = ev(kind: Progress { note: "a writer who never saw the second claim" as NonEmptyStr }, parent: Present { value: c1.id }, author: fabric, at: "t2") + let forked = evlog_not_admitted_at(a: roadmap_bound_result_admission(envs: [c1, c2, fork], binding: evlog_binding(claim: c1.id), outcome: evlog_failed_outcome()), step: "result-history") + revised && reclaimed && elsewhere && absent_claim && not_a_claim && foreign_claim && forked } test fn progress_actor_does_not_replace_the_committed_holder() -> Bool { diff --git a/dag/test/claim/roadmap/roadmap_result_returned_witness_test.dag b/dag/test/claim/roadmap/roadmap_result_returned_witness_test.dag new file mode 100644 index 00000000000..5ffe35cd0ef --- /dev/null +++ b/dag/test/claim/roadmap/roadmap_result_returned_witness_test.dag @@ -0,0 +1,678 @@ +module test.claim.roadmap.roadmap_result_returned_witness_test + +import std.types { Bool, String, List, NonEmptyStr } +import std.process { ProcessExit, ExitSuccess, ExitFailure } +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.principal_projection { PrincipalRef, fabric_workflow_principal } +import gunbc.roadmap_dashboard_instance { srv2_preview_dashboard_instance } +import gunbc.roadmap_dispatch_actuator { DispatchAttemptRef } +import gunbc.roadmap.roadmap_submission { + CandidateHandoffState, HandoffAwaitingWorker, HandoffYielded, HandoffEvidenceRefused, HandoffDeclaredBlocked, + HandoffSubmitted, HandoffVerificationFailed, HandoffAwaitingIntegration, HandoffBlockedOnIntegration, + HandoffPublicationOwed, HandoffBlockedOnPublication, HandoffPublished, + SubmissionBlocked, candidate_handoff_state_key, +} +import gunbc.roadmap.roadmap_event_log { + RoadmapEvent, RoadmapEventKind, RoadmapEventEnvelope, RoadmapEventId, EventPrincipal, EventPrincipalAuthenticated, + Claimed, Released, Progress, ResultReturned, AttemptResultOutcome, ResultPublished, ResultVerificationFailed, ResultHandedBack, + event_principal_label, roadmap_event_envelope, roadmap_event_wire_text, RoadmapResultBinding, roadmap_result_returned_event, + roadmap_event_admitted_after, RoadmapResultAdmission, ResultAdmitted, ResultAlreadyOnLog, ResultNotAdmitted, roadmap_bound_result_admission, +} +import gunbc.roadmap.roadmap_event_carrier { + EventFileContent, roadmap_events_decode_reads, roadmap_all_events_of_reads, + RoadmapEventsRead, EventsRead, EventsReadRefused, RoadmapAllEventsRead, AllEventsRead, AllEventsReadRefused, + RoadmapResultAuthority, ResultAuthorityEstablished, ResultAuthorityRefused, roadmap_result_authority_of_record, + RoadmapResultAppend, ResultAppended, ResultAppendAlreadyPresent, ResultAppendRefused, roadmap_result_append_of, + EventAppended, EventAlreadyPresent, EventAppendRefused, +} +import extdeps.filesystem.filesystem_io { + FilesystemCreateNew, FilesystemCreated, FilesystemCreateTargetOccupied, FilesystemCreateRefused, FilesystemPermissionDenied, +} +import gunbc.roadmap.roadmap_attempt_request_record { + AttemptRequestBinding, RequestClaimBound, RequestAutonomous, + attempt_request_binding_json, attempt_request_binding_decode, RequestBindingDecoded, RequestBindingUndecodable, + AttemptRequestBindingCommit, RequestBindingCommitted, RequestBindingCommitRefused, attempt_request_binding_commit_of, + attempt_request_binding_read_of, + AttemptRequestBindingRead, RequestBindingRead, RequestBindingAbsent, RequestBindingUnreadable, +} +import gunbc.roadmap.roadmap_attempt_request_binding { + AttemptLaunchRequest, LaunchForClaim, LaunchContinuing, LaunchAutonomous, attempt_launch_request_for_origin, + AttemptRequestBindingAdmission, RequestBindingAdmitted, RequestBindingNotAdmitted, attempt_request_binding_admission, +} +import gunbc.roadmap.roadmap_result_returned { + AttemptResultStanding, AttemptResultTerminal, AttemptResultPending, AttemptResultUnstatable, attempt_result_standing, + ResultRequestJoin, ResultRequestJoined, ResultRequestNone, ResultRequestUnjoinable, ResultRequestUnreadable, result_request_join, + ResultReturnDecision, ResultReturnAppend, ResultAlreadyReturned, ResultReturnRefused, result_return_decision, + ResultReturnPrecheck, ResultPrecheckSettled, ResultPrecheckOwed, result_return_precheck, result_return_keyed_attempts, + ResultReturnOutcome, ResultReturnRecorded, ResultReturnAlreadyRecorded, ResultReturnNotTerminal, ResultReturnNoRequest, ResultReturnFailed, + ResultReturnPass, ResultReturnPassRan, ResultReturnPassWithheld, ResultReturnPassRefused, + result_return_attempts_for_instance, result_return_pass_exit, result_return_pass_failure, + ResultReturnedObservation, ResultReturnedObserved, ResultNotReturned, ResultNotOwed, ResultReturnedUnobserved, + result_returned_observation_of, +} +import gunbc.roadmap_belt_tick_cli { belt_tick_result_tail_exit } +import test.claim.roadmap_belt_actuate_witness { completed_attempt_progress } + +data rr_attempt: String = "feedfacefeedface" + +fn rr_node() -> RoadmapNodeId { "result-returned-node" as RoadmapNodeId } + +fn rr_principal(subject: String) -> EventPrincipal { + EventPrincipalAuthenticated { ref: PrincipalRef { authority: "google-oidc" as NonEmptyStr, namespace: "accounts" as NonEmptyStr, subject: subject as NonEmptyStr } } +} + +fn rr_alice() -> EventPrincipal { rr_principal(subject: "sub-alice") } + +fn rr_bob() -> EventPrincipal { rr_principal(subject: "sub-bob") } + +fn rr_fabric() -> EventPrincipal { + EventPrincipalAuthenticated { ref: fabric_workflow_principal } +} + +fn rr_ev(node: RoadmapNodeId, kind: RoadmapEventKind, parent: RoadmapEventId?, author: EventPrincipal, at: String) -> RoadmapEventEnvelope { + roadmap_event_envelope(e: RoadmapEvent { node: node, author: author, parent: parent, kind: kind, recorded_at: at }) +} + +fn rr_claim_by(node: RoadmapNodeId, requester: EventPrincipal, parent: RoadmapEventId?, at: String) -> RoadmapEventEnvelope { + rr_ev(node: node, kind: Claimed { assignee: rr_fabric(), return_to: requester }, parent: parent, author: requester, at: at) +} + +fn rr_claim(node: RoadmapNodeId) -> RoadmapEventEnvelope { + rr_claim_by(node: node, requester: rr_alice(), parent: none, at: "t1") +} + +fn rr_result(node: RoadmapNodeId, attempt: String, claim: RoadmapEventId, outcome: AttemptResultOutcome, parent: RoadmapEventId, at: String) -> RoadmapEventEnvelope { + rr_ev(node: node, kind: ResultReturned { attempt: attempt as NonEmptyStr, claim: claim, outcome: outcome }, parent: Present { value: parent }, author: rr_fabric(), at: at) +} + +fn rr_published() -> AttemptResultOutcome { + ResultPublished { pr: "https://github.com/gunb-ai/gunbc/pull/12210" as NonEmptyStr, head: "52c56a8" as NonEmptyStr } +} + +fn rr_failed() -> AttemptResultOutcome { + ResultVerificationFailed { head: "52c56a8" as NonEmptyStr, cause: "verdict validation-failed" as NonEmptyStr } +} + +fn rr_binding(claim: RoadmapEventId) -> RoadmapResultBinding { + RoadmapResultBinding { node: rr_node(), attempt: rr_attempt as NonEmptyStr, claim: claim } +} + +fn rr_bound(claim: RoadmapEventId) -> AttemptRequestBindingRead { + RequestBindingRead { binding: RequestClaimBound { claim: claim } } +} + +fn rr_standing_is_pending(handoff: CandidateHandoffState) -> Bool { + match attempt_result_standing(handoff: handoff) { + AttemptResultPending { handoff: key } => key == candidate_handoff_state_key(state: handoff) + AttemptResultTerminal { outcome: _ } => false + AttemptResultUnstatable { reason: _ } => false + } +} + +fn rr_joined_to(join: ResultRequestJoin, claim: RoadmapEventId, principal: EventPrincipal) -> Bool { + match join { + ResultRequestJoined { claim: c, return_to } => (c as String) == (claim as String) && return_to == principal + ResultRequestNone { reason: _ } => false + ResultRequestUnjoinable { reason: _ } => false + ResultRequestUnreadable { reason: _ } => false + } +} + +// THE FINAL ANSWERS: published, a read verification verdict, and the worker's own captured +// declaration are each exactly one outcome arm carrying the facts the state carried. +test fn each_final_handoff_state_is_returned_as_its_own_outcome() -> Bool { + let published = match attempt_result_standing(handoff: HandoffPublished { head_sha: "52c56a8", pr_number: 12210, url: "https://github.com/gunb-ai/gunbc/pull/12210" }) { + AttemptResultTerminal { outcome } => outcome == rr_published() + AttemptResultPending { handoff: _ } => false + AttemptResultUnstatable { reason: _ } => false + } + let failed = match attempt_result_standing(handoff: HandoffVerificationFailed { head_sha: "52c56a8", verdict_key: "validation-failed" }) { + AttemptResultTerminal { outcome } => outcome == rr_failed() + AttemptResultPending { handoff: _ } => false + AttemptResultUnstatable { reason: _ } => false + } + let handed_back = match attempt_result_standing(handoff: HandoffDeclaredBlocked { head_sha: "52c56a8", readiness: SubmissionBlocked, explanation: "needs the fixture authority" }) { + AttemptResultTerminal { outcome: ResultHandedBack { reason } } => string_contains(s: reason as String, pattern: "needs the fixture authority") && string_contains(s: reason as String, pattern: "52c56a8") + AttemptResultTerminal { outcome: _ } => false + AttemptResultPending { handoff: _ } => false + AttemptResultUnstatable { reason: _ } => false + } + published && failed && handed_back +} + +// THE RED: NOTHING THAT CAN STILL CHANGE IS RETURNED. A running worker, a submitted candidate, a +// candidate awaiting integration and an owed publication are in motion; evidence the belt could not +// read, a blocked integration and a blocked publication are retryable; a yielded worker may not have +// been captured yet. Each is Pending naming the state -- never a result -- and in particular an +// unreadable verdict is not a failed verification. +test fn a_state_that_can_still_change_returns_no_result() -> Bool { + rr_standing_is_pending(handoff: HandoffAwaitingWorker) + && rr_standing_is_pending(handoff: HandoffSubmitted { head_sha: "52c56a8" }) + && rr_standing_is_pending(handoff: HandoffAwaitingIntegration { head_sha: "52c56a8" }) + && rr_standing_is_pending(handoff: HandoffPublicationOwed { head_sha: "52c56a8" }) + && rr_standing_is_pending(handoff: HandoffEvidenceRefused { reason: "the verification receipt for this head is unreadable" }) + && rr_standing_is_pending(handoff: HandoffBlockedOnIntegration { head_sha: "52c56a8", cause: "conflict in a.dag" }) + && rr_standing_is_pending(handoff: HandoffBlockedOnPublication { head_sha: "52c56a8", cause: "the publication observation was refused" }) + && rr_standing_is_pending(handoff: HandoffYielded { reason: "terminal without a captured submission" }) +} + +// A REFUSED OBSERVATION THAT LATER RESOLVES PRODUCES ONLY THE FINAL RESULT. While the publication +// observation is refused the attempt returns nothing, so the history carries no result for it; when +// the same attempt is later observed published, the decision over that unchanged history appends +// the published result as the attempt's first and only one. The same holds for a verdict that could +// not be read and is later read as failed. +test fn a_retryable_state_that_resolves_yields_exactly_the_final_result() -> Bool { + let claim = rr_claim(node: rr_node()) + let early_publication = rr_standing_is_pending(handoff: HandoffBlockedOnPublication { head_sha: "52c56a8", cause: "the publication observation was refused" }) + let early_verdict = rr_standing_is_pending(handoff: HandoffEvidenceRefused { reason: "the verification receipt for this head is unreadable" }) + let later_published = match attempt_result_standing(handoff: HandoffPublished { head_sha: "52c56a8", pr_number: 12210, url: "https://github.com/gunb-ai/gunbc/pull/12210" }) { + AttemptResultTerminal { outcome } => match result_return_decision(binding: rr_binding(claim: claim.id), outcome: outcome, envelopes: [claim]) { + ResultReturnAppend { binding, outcome: final } => binding == rr_binding(claim: claim.id) && final == rr_published() + ResultAlreadyReturned { id: _ } => false + ResultReturnRefused { step: _, reason: _ } => false + } + AttemptResultPending { handoff: _ } => false + AttemptResultUnstatable { reason: _ } => false + } + let later_failed = match attempt_result_standing(handoff: HandoffVerificationFailed { head_sha: "52c56a8", verdict_key: "validation-failed" }) { + AttemptResultTerminal { outcome } => outcome == rr_failed() + AttemptResultPending { handoff: _ } => false + AttemptResultUnstatable { reason: _ } => false + } + early_publication && early_verdict && later_published && later_failed +} + +// THE REAL PROGRESS FOLD INHABITS THE FIELD THE PASS READS. An attempt row built by +// gunbc.roadmap_workflow_progress workflow_attempt_progress from attempt evidence -- a worker that +// ended with no captured submission -- carries the yielded handoff state on the row, and the pass +// classifies THAT value as pending: a row that lost the field, or a classifier that returned a +// yielded worker as handed back before its capture concluded, turns this red. +test fn the_real_attempt_row_carries_its_handoff_state_and_a_yielded_row_is_pending() -> Bool { + let progress = completed_attempt_progress(node_id: "result-returned-node") + candidate_handoff_state_key(state: progress.handoff) == "yielded" + && (match attempt_result_standing(handoff: progress.handoff) { + AttemptResultPending { handoff } => handoff == "yielded" + AttemptResultTerminal { outcome: _ } => false + AttemptResultUnstatable { reason: _ } => false + }) +} + +// A published state with nothing to name is refused, not spelled with a blank. +test fn a_published_state_without_a_pull_request_or_head_is_unstatable() -> Bool { + all([ + HandoffPublished { head_sha: "52c56a8", pr_number: 1, url: "" }, + HandoffPublished { head_sha: "", pr_number: 1, url: "https://github.com/gunb-ai/gunbc/pull/1" }, + HandoffVerificationFailed { head_sha: "", verdict_key: "validation-failed" }, + ], h => match attempt_result_standing(handoff: h) { + AttemptResultUnstatable { reason } => string_length(s: reason) > 0 + AttemptResultTerminal { outcome: _ } => false + AttemptResultPending { handoff: _ } => false + }) +} + +// CONTROL 1 AND 3 OF THE LAUNCH BINDING: THE ATTEMPT IS BOUND TO THE CLAIM ITS DISPATCH DECISION +// CARRIED. The decision for claim C1 reaches initialization after a second claim C2 has become the +// issue's current one; the binding is C1, because the log is read to ESTABLISH the carried claim, +// never to choose the current one. A request made for another issue cannot bind this attempt. A +// claim id the issue's history does not carry as a claim -- absent, another issue's, or an event of +// another kind -- cannot bind. An autonomous launch binds as autonomous without consulting the log +// at all (it is admitted over a log that could not even be read). +test fn a_launch_binds_the_claim_its_dispatch_decision_carried() -> Bool { + let c1 = rr_claim_by(node: rr_node(), requester: rr_alice(), parent: none, at: "t1") + let c2 = rr_claim_by(node: rr_node(), requester: rr_bob(), parent: Present { value: c1.id }, at: "t2") + let log = EventsRead { node: rr_node(), envelopes: [c1, c2] } + let bound_to_c1 = attempt_request_binding_admission(request: LaunchForClaim { node: rr_node(), claim: c1.id }, node_id: rr_node(), read: log, predecessor: RequestBindingAbsent) == RequestBindingAdmitted { binding: RequestClaimBound { claim: c1.id } } + let other_issue = rr_binding_refused_at(a: attempt_request_binding_admission(request: LaunchForClaim { node: "result-returned-other-node" as RoadmapNodeId, claim: c1.id }, node_id: rr_node(), read: log, predecessor: RequestBindingAbsent), step: "request-node") + let foreign = rr_claim(node: "result-returned-other-node" as RoadmapNodeId) + let note = rr_ev(node: rr_node(), kind: Progress { note: "started" as NonEmptyStr }, parent: Present { value: c2.id }, author: rr_fabric(), at: "t3") + let no_such_claim = all(["0000000000000000" as RoadmapEventId, foreign.id, note.id], id => + rr_binding_refused_at(a: attempt_request_binding_admission(request: LaunchForClaim { node: rr_node(), claim: id }, node_id: rr_node(), read: EventsRead { node: rr_node(), envelopes: [c1, c2, note] }, predecessor: RequestBindingAbsent), step: "request-claim")) + let autonomous = attempt_request_binding_admission(request: LaunchAutonomous, node_id: rr_node(), read: EventsReadRefused { node: rr_node(), step: "sync/fetch", reason: "exit 128" }, predecessor: RequestBindingAbsent) == RequestBindingAdmitted { binding: RequestAutonomous } + bound_to_c1 && other_issue && no_such_claim && autonomous +} + +fn rr_binding_refused_at(a: AttemptRequestBindingAdmission, step: String) -> Bool { + match a { + RequestBindingNotAdmitted { step: s, detail } => s == step && string_length(s: detail) > 0 + RequestBindingAdmitted { binding: _ } => false + } +} + +// A REQUEST-BOUND LAUNCH WHOSE CLAIM CANNOT BE ESTABLISHED DOES NOT LAUNCH. An issue log that could +// not be read, and a history that forks, each refuse the binding -- there is no "unobserved" binding +// to record and run under -- so the attempt that would have finished unable to answer anyone is +// never started. The positive control is the same request over a readable log. +test fn a_request_bound_launch_refuses_when_its_claim_cannot_be_read() -> Bool { + let c1 = rr_claim(node: rr_node()) + let request = LaunchForClaim { node: rr_node(), claim: c1.id } + let unread = rr_binding_refused_at(a: attempt_request_binding_admission(request: request, node_id: rr_node(), read: EventsReadRefused { node: rr_node(), step: "sync/fetch", reason: "exit 128" }, predecessor: RequestBindingAbsent), step: "request-log") + let left = rr_ev(node: rr_node(), kind: Progress { note: "left" as NonEmptyStr }, parent: Present { value: c1.id }, author: rr_fabric(), at: "t2") + let right = rr_ev(node: rr_node(), kind: Progress { note: "right" as NonEmptyStr }, parent: Present { value: c1.id }, author: rr_fabric(), at: "t2") + let forked = rr_binding_refused_at(a: attempt_request_binding_admission(request: request, node_id: rr_node(), read: EventsRead { node: rr_node(), envelopes: [c1, left, right] }, predecessor: RequestBindingAbsent), step: "request-history") + let readable = attempt_request_binding_admission(request: request, node_id: rr_node(), read: EventsRead { node: rr_node(), envelopes: [c1, left] }, predecessor: RequestBindingAbsent) == RequestBindingAdmitted { binding: RequestClaimBound { claim: c1.id } } + unread && forked && readable +} + +// A CONTINUATION ANSWERS ITS LINEAGE'S REQUEST, BY COPYING A RECORDED FACT. A dispatch nobody's +// claim caused that continues attempt K is Continuing, not Autonomous; one that starts a lineage is +// Autonomous; a dispatch for a claim stays for that claim even when it continues a lineage. The +// continuing attempt takes exactly the binding K recorded -- C1 -- although a later claim C2 is on +// the issue's log, which the admission is not even handed (it is given a log that could not be +// read and still binds C1). A predecessor that was autonomous continues autonomous. A predecessor +// with no binding record, or an unreadable one, REFUSES the launch: the red is a continuation that +// would run as autonomous and never answer its requester. +test fn a_continuation_copies_its_predecessors_recorded_binding_or_refuses() -> Bool { + let c1 = rr_claim_by(node: rr_node(), requester: rr_alice(), parent: none, at: "t1") + let class_continuing = attempt_launch_request_for_origin(request: LaunchAutonomous, predecessor_attempt_key: Present { value: rr_attempt }) == LaunchContinuing { predecessor_attempt_key: rr_attempt } + let class_fresh = attempt_launch_request_for_origin(request: LaunchAutonomous, predecessor_attempt_key: none) == LaunchAutonomous + let class_reassigned = attempt_launch_request_for_origin(request: LaunchForClaim { node: rr_node(), claim: c1.id }, predecessor_attempt_key: Present { value: rr_attempt }) == LaunchForClaim { node: rr_node(), claim: c1.id } + let unread_log = EventsReadRefused { node: rr_node(), step: "sync/fetch", reason: "exit 128" } + let continuing = LaunchContinuing { predecessor_attempt_key: rr_attempt } + let copies_c1 = attempt_request_binding_admission(request: continuing, node_id: rr_node(), read: unread_log, predecessor: rr_bound(claim: c1.id)) == RequestBindingAdmitted { binding: RequestClaimBound { claim: c1.id } } + let copies_autonomous = attempt_request_binding_admission(request: continuing, node_id: rr_node(), read: unread_log, predecessor: RequestBindingRead { binding: RequestAutonomous }) == RequestBindingAdmitted { binding: RequestAutonomous } + let missing_refuses = rr_binding_refused_at(a: attempt_request_binding_admission(request: continuing, node_id: rr_node(), read: unread_log, predecessor: RequestBindingAbsent), step: "request-predecessor") + let unreadable_refuses = rr_binding_refused_at(a: attempt_request_binding_admission(request: continuing, node_id: rr_node(), read: unread_log, predecessor: RequestBindingUnreadable { reason: "denied" }), step: "request-predecessor") + class_continuing && class_fresh && class_reassigned && copies_c1 && copies_autonomous && missing_refuses && unreadable_refuses +} + +// CONTROL 2 OF THE LAUNCH BINDING: THE COMMIT IS CREATE-ONLY. The first commit is a create. A +// repeated initialization that finds the SAME binding is an idempotent success. One that finds a +// DIFFERENT binding -- the attempt re-initialized under a later claim, or as autonomous -- refuses +// and the first stands. A binding already there that cannot be read is not replaced, and a create +// the host refused fails the launch. +test fn a_committed_binding_is_created_once_and_cannot_be_replaced() -> Bool { + let c1 = rr_claim_by(node: rr_node(), requester: rr_alice(), parent: none, at: "t1") + let c2 = rr_claim_by(node: rr_node(), requester: rr_bob(), parent: Present { value: c1.id }, at: "t2") + let first = attempt_request_binding_commit_of(binding: RequestClaimBound { claim: c1.id }, created: FilesystemCreated { path: "p" }, existing: RequestBindingAbsent) == RequestBindingCommitted { binding: RequestClaimBound { claim: c1.id } } + let again = attempt_request_binding_commit_of(binding: RequestClaimBound { claim: c1.id }, created: FilesystemCreateTargetOccupied { path: "p" }, existing: RequestBindingRead { binding: RequestClaimBound { claim: c1.id } }) == RequestBindingCommitted { binding: RequestClaimBound { claim: c1.id } } + let rebind = all([RequestClaimBound { claim: c2.id }, RequestAutonomous], b => rr_commit_refused_at(c: attempt_request_binding_commit_of(binding: b, created: FilesystemCreateTargetOccupied { path: "p" }, existing: RequestBindingRead { binding: RequestClaimBound { claim: c1.id } }), step: "attempt-request-binding-conflict")) + let unreadable_kept = rr_commit_refused_at(c: attempt_request_binding_commit_of(binding: RequestClaimBound { claim: c1.id }, created: FilesystemCreateTargetOccupied { path: "p" }, existing: RequestBindingUnreadable { reason: "not json" }), step: "attempt-request-binding-conflict") + let host_refused = rr_commit_refused_at(c: attempt_request_binding_commit_of(binding: RequestClaimBound { claim: c1.id }, created: FilesystemCreateRefused { path: "p", kind: FilesystemPermissionDenied, error: "denied" }, existing: RequestBindingAbsent), step: "attempt-request-binding-persist") + first && again && rebind && unreadable_kept && host_refused +} + +fn rr_commit_refused_at(c: AttemptRequestBindingCommit, step: String) -> Bool { + match c { + RequestBindingCommitRefused { step: s, detail } => s == step && string_length(s: detail) > 0 + RequestBindingCommitted { binding: _ } => false + } +} + +// THE BINDING RECORD'S CODEC IS STRICT: both states round-trip; a bound record with no claim or an +// empty one, an unknown status, another schema and a non-document are undecodable, never +// autonomous; and the file read keeps absent, unreadable and decoded apart. +test fn the_binding_record_round_trips_and_a_malformed_one_is_unreadable() -> Bool { + let c1 = rr_claim(node: rr_node()) + let codec = all([RequestClaimBound { claim: c1.id }, RequestAutonomous], b => match attempt_request_binding_decode(text: attempt_request_binding_json(binding: b)) { + RequestBindingDecoded { binding } => binding == b + RequestBindingUndecodable { reason: _ } => false + }) + let strict = all([ + "{\"schema\": \"roadmap-attempt-request-binding/v1\", \"status\": \"bound\"}", + "{\"schema\": \"roadmap-attempt-request-binding/v1\", \"status\": \"bound\", \"claim\": \"\"}", + "{\"schema\": \"roadmap-attempt-request-binding/v1\", \"status\": \"unobserved\", \"reason\": \"x\"}", + "{\"schema\": \"roadmap-attempt-request-binding/v0\", \"status\": \"autonomous\"}", + "not json", + ], text => match attempt_request_binding_decode(text: text) { + RequestBindingUndecodable { reason } => string_length(s: reason) > 0 + RequestBindingDecoded { binding: _ } => false + }) + let file_arms = attempt_request_binding_read_of(success: false, error_kind: "not_found", error: "no such file", content: "") == RequestBindingAbsent + && (match attempt_request_binding_read_of(success: false, error_kind: "permission_denied", error: "denied", content: "") { RequestBindingUnreadable { reason } => reason == "denied" RequestBindingRead { binding: _ } => false RequestBindingAbsent => false }) + && (match attempt_request_binding_read_of(success: true, error_kind: "", error: "", content: "not json") { RequestBindingUnreadable { reason: _ } => true RequestBindingRead { binding: _ } => false RequestBindingAbsent => false }) + codec && strict && file_arms +} + +// THE RECIPIENT IS THE BOUND CLAIM'S, WHATEVER THE ISSUE SAYS NOW. Alice's claim C1 launched the +// attempt. A second claim by Bob made while the attempt runs does not take its result; a release +// does not erase its requester. The red is the join the first version made: reading the issue's +// current assignment would answer Bob, or nobody. +test fn a_later_claim_or_release_does_not_change_the_attempts_requester() -> Bool { + let c1 = rr_claim_by(node: rr_node(), requester: rr_alice(), parent: none, at: "t1") + let c2 = rr_claim_by(node: rr_node(), requester: rr_bob(), parent: Present { value: c1.id }, at: "t2") + let released = rr_ev(node: rr_node(), kind: Released, parent: Present { value: c1.id }, author: rr_alice(), at: "t2") + rr_joined_to(join: result_request_join(binding: rr_bound(claim: c1.id), node_envelopes: [c1, c2]), claim: c1.id, principal: rr_alice()) + && rr_joined_to(join: result_request_join(binding: rr_bound(claim: c1.id), node_envelopes: [c1, released]), claim: c1.id, principal: rr_alice()) +} + +// AN ATTEMPT WITH NO REQUEST HAS NO REQUESTER, AND A BOUND CLAIM THE LOG NO LONGER CARRIES IS NOT +// QUIET. An autonomous attempt and one with no binding record are no-request. A binding naming an +// event the history does not carry, another issue's claim, or an event that is not a claim is +// unjoinable -- the launch established that claim, so this is a log that lost it. An unreadable +// binding file is its own arm. +test fn an_attempt_without_a_request_or_with_a_lost_claim_is_told_apart() -> Bool { + let claim = rr_claim(node: rr_node()) + let note = rr_ev(node: rr_node(), kind: Progress { note: "started" as NonEmptyStr }, parent: Present { value: claim.id }, author: rr_fabric(), at: "t2") + let foreign = rr_claim(node: "result-returned-other-node" as RoadmapNodeId) + let unjoinable = all([rr_bound(claim: "0000000000000000" as RoadmapEventId), rr_bound(claim: foreign.id), rr_bound(claim: note.id)], b => match result_request_join(binding: b, node_envelopes: [claim, note]) { + ResultRequestUnjoinable { reason } => string_length(s: reason) > 0 + ResultRequestJoined { claim: _, return_to: _ } => false + ResultRequestNone { reason: _ } => false + ResultRequestUnreadable { reason: _ } => false + }) + let no_request = all([RequestBindingAbsent, RequestBindingRead { binding: RequestAutonomous }], b => match result_request_join(binding: b, node_envelopes: [claim, note]) { + ResultRequestNone { reason } => string_length(s: reason) > 0 + ResultRequestJoined { claim: _, return_to: _ } => false + ResultRequestUnjoinable { reason: _ } => false + ResultRequestUnreadable { reason: _ } => false + }) + let unreadable = match result_request_join(binding: RequestBindingUnreadable { reason: "denied" }, node_envelopes: [claim, note]) { + ResultRequestUnreadable { reason } => string_contains(s: reason, pattern: "denied") + ResultRequestJoined { claim: _, return_to: _ } => false + ResultRequestNone { reason: _ } => false + ResultRequestUnjoinable { reason: _ } => false + } + let lost_claim_fails_the_pass = match result_return_precheck(node_id: "result-returned-node", attempt_key: rr_attempt, binding: rr_bound(claim: "0000000000000000" as RoadmapEventId), envelopes: [claim, note]) { + ResultPrecheckSettled { outcome: ResultReturnFailed { node_id: _, attempt_key: _, step, reason: _ } } => step == "claim" + ResultPrecheckSettled { outcome: _ } => false + ResultPrecheckOwed { binding: _ } => false + } + let autonomous_is_quiet = match result_return_precheck(node_id: "result-returned-node", attempt_key: rr_attempt, binding: RequestBindingRead { binding: RequestAutonomous }, envelopes: [claim, note]) { + ResultPrecheckSettled { outcome: ResultReturnNoRequest { node_id: _, attempt_key: _, reason: _ } } => true + ResultPrecheckSettled { outcome: _ } => false + ResultPrecheckOwed { binding: _ } => false + } + unjoinable && no_request && unreadable && lost_claim_fails_the_pass && autonomous_is_quiet +} + +fn rr_decision_refused_at(d: ResultReturnDecision, step: String) -> Bool { + match d { + ResultReturnRefused { step: s, reason } => s == step && string_length(s: reason) > 0 + ResultReturnAppend { binding: _, outcome: _ } => false + ResultAlreadyReturned { id: _ } => false + } +} + +// THE WRITE DECISION COMPARES THE WHOLE IDENTITY. The identical result already on the log is already +// returned and names its event. A different outcome under the same binding, and the same outcome +// under another claim, are each refused as a second result. A first result for another attempt of +// the same issue is appended, carrying its own binding and no event of the caller's making. +test fn an_answered_attempt_is_already_returned_only_for_the_same_binding_and_outcome() -> Bool { + let c1 = rr_claim_by(node: rr_node(), requester: rr_alice(), parent: none, at: "t1") + let c2 = rr_claim_by(node: rr_node(), requester: rr_bob(), parent: Present { value: c1.id }, at: "t2") + let failed = rr_result(node: rr_node(), attempt: rr_attempt, claim: c1.id, outcome: rr_failed(), parent: c2.id, at: "t3") + let history = [c1, c2, failed] + let same = match result_return_decision(binding: rr_binding(claim: c1.id), outcome: rr_failed(), envelopes: history) { + ResultAlreadyReturned { id } => (id as String) == (failed.id as String) + ResultReturnAppend { binding: _, outcome: _ } => false + ResultReturnRefused { step: _, reason: _ } => false + } + let other_outcome = rr_decision_refused_at(d: result_return_decision(binding: rr_binding(claim: c1.id), outcome: rr_published(), envelopes: history), step: "result-duplicate") + let other_claim = rr_decision_refused_at(d: result_return_decision(binding: rr_binding(claim: c2.id), outcome: rr_failed(), envelopes: history), step: "result-duplicate") + let other = RoadmapResultBinding { node: rr_node(), attempt: "0ther0ther0ther00" as NonEmptyStr, claim: c2.id } + let another_attempt_appends = match result_return_decision(binding: other, outcome: rr_failed(), envelopes: history) { + ResultReturnAppend { binding, outcome } => binding == other && outcome == rr_failed() + ResultAlreadyReturned { id: _ } => false + ResultReturnRefused { step: _, reason: _ } => false + } + same && other_outcome && other_claim && another_attempt_appends +} + +fn rr_authority_refused_at(a: RoadmapResultAuthority, step: String) -> Bool { + match a { + ResultAuthorityRefused { step: s, reason } => s == step && string_length(s: reason) > 0 + ResultAuthorityEstablished { binding: _ } => false + } +} + +// THE WRITER'S CLAIM COMES FROM THE ATTEMPT'S OWN RECORD, SO C2 CANNOT POISON K. Attempt K is durably +// bound to claim C1; C2 is a later, perfectly valid claim on the same issue. The append authority +// is a function of the issue, the attempt and K's request record -- it has no claim parameter -- and +// it establishes C1. The only remaining way to offer K/C2 is a raw ResultReturned handed to the +// generic append, and that is refused, so K's result slot is NOT occupied: the log still holds only +// the two claims. The correct K/C1 result is then admitted on the head. An attempt with no record, +// an autonomous one, an unreadable record and a keyless attempt establish no authority and refuse. +test fn the_result_writer_derives_the_claim_from_the_attempts_record_so_a_later_claim_cannot_poison_it() -> Bool { + let c1 = rr_claim_by(node: rr_node(), requester: rr_alice(), parent: none, at: "t1") + let c2 = rr_claim_by(node: rr_node(), requester: rr_bob(), parent: Present { value: c1.id }, at: "t2") + let log = [c1, c2] + let established = roadmap_result_authority_of_record(node: rr_node(), attempt: rr_attempt, record: rr_bound(claim: c1.id)) + let derives_c1 = established == ResultAuthorityEstablished { binding: rr_binding(claim: c1.id) } + let poison = RoadmapEvent { node: rr_node(), author: rr_fabric(), parent: Present { value: c2.id }, kind: ResultReturned { attempt: rr_attempt as NonEmptyStr, claim: c2.id, outcome: rr_published() }, recorded_at: "t3" } + let c2_refused = !roadmap_event_admitted_after(envs: log, event: poison) + let c1_still_appendable = match established { + ResultAuthorityRefused { step: _, reason: _ } => false + ResultAuthorityEstablished { binding } => + match roadmap_bound_result_admission(envs: log, binding: binding, outcome: rr_published()) { + ResultAdmitted { parent } => match parent { Present { value: p } => (p as String) == (c2.id as String) Absent => false } + ResultAlreadyOnLog { id: _ } => false + ResultNotAdmitted { step: _, reason: _ } => false + } + } + let no_authority = rr_authority_refused_at(a: roadmap_result_authority_of_record(node: rr_node(), attempt: rr_attempt, record: RequestBindingAbsent), step: "result-binding") + && rr_authority_refused_at(a: roadmap_result_authority_of_record(node: rr_node(), attempt: rr_attempt, record: RequestBindingRead { binding: RequestAutonomous }), step: "result-binding") + && rr_authority_refused_at(a: roadmap_result_authority_of_record(node: rr_node(), attempt: rr_attempt, record: RequestBindingUnreadable { reason: "denied" }), step: "result-binding") + && rr_authority_refused_at(a: roadmap_result_authority_of_record(node: rr_node(), attempt: "", record: rr_bound(claim: c1.id)), step: "result-attempt") + let outcome_maps = roadmap_result_append_of(append: EventAppended { id: c1.id }) == ResultAppended { id: c1.id } + && roadmap_result_append_of(append: EventAlreadyPresent { id: c1.id }) == ResultAppendAlreadyPresent { id: c1.id } + && roadmap_result_append_of(append: EventAppendRefused { id: c1.id, step: "revision-conflict", reason: "moved" }) == ResultAppendRefused { step: "revision-conflict", reason: "moved" } + derives_c1 && c2_refused && c1_still_appendable && no_authority && outcome_maps +} + +fn rr_precheck_owed(p: ResultReturnPrecheck, claim: RoadmapEventId) -> Bool { + match p { + ResultPrecheckOwed { binding } => binding == rr_binding(claim: claim) + ResultPrecheckSettled { outcome: _ } => false + } +} + +// ANTI-ENTROPY IS OVER THE HISTORY AND IS NOT DISCHARGED BY A MISADDRESSED RESULT. K finished and +// its result was never appended; K2 of the same node is now current: K is still considered. K is +// bound to issue A and claim C1. A result planted on A for K naming the later claim C2 does NOT +// answer K: K is still owed, and the decision refuses to write a second result rather than treating +// the plant as K's. A result planted on another issue B under K's key does NOT answer K either: K +// is still owed, and the decision refuses because the attempt has a result elsewhere. Only the +// result on A naming C1 settles K. +test fn a_superseded_attempt_stays_owed_and_a_misaddressed_result_does_not_answer_it() -> Bool { + let k = DispatchAttemptRef { node_id: "result-returned-node" as NonEmptyStr, branch: "dispatch/result-returned-node-afeedfacefeedface" as NonEmptyStr, attempt_key: rr_attempt } + let k2 = DispatchAttemptRef { node_id: "result-returned-node" as NonEmptyStr, branch: "dispatch/result-returned-node-a0ther0ther0ther00" as NonEmptyStr, attempt_key: "0ther0ther0ther00" } + let keyless = DispatchAttemptRef { node_id: "result-returned-node" as NonEmptyStr, branch: "dispatch/result-returned-node" as NonEmptyStr, attempt_key: "" } + let considered = result_return_keyed_attempts(attempts: [k2, k, keyless]) + let history_population = count(considered) == 2 && any(considered, a => a.attempt_key == rr_attempt) && any(considered, a => a.attempt_key == "0ther0ther0ther00") + let c1 = rr_claim_by(node: rr_node(), requester: rr_alice(), parent: none, at: "t1") + let c2 = rr_claim_by(node: rr_node(), requester: rr_bob(), parent: Present { value: c1.id }, at: "t2") + let unanswered = rr_precheck_owed(p: result_return_precheck(node_id: "result-returned-node", attempt_key: rr_attempt, binding: rr_bound(claim: c1.id), envelopes: [c1, c2]), claim: c1.id) + let wrong_claim = rr_result(node: rr_node(), attempt: rr_attempt, claim: c2.id, outcome: rr_published(), parent: c2.id, at: "t3") + let still_owed_after_wrong_claim = rr_precheck_owed(p: result_return_precheck(node_id: "result-returned-node", attempt_key: rr_attempt, binding: rr_bound(claim: c1.id), envelopes: [c1, c2, wrong_claim]), claim: c1.id) + && rr_decision_refused_at(d: result_return_decision(binding: rr_binding(claim: c1.id), outcome: rr_published(), envelopes: [c1, c2, wrong_claim]), step: "result-duplicate") + let other_issue = "result-returned-other-node" as RoadmapNodeId + let b1 = rr_claim(node: other_issue) + let on_other_issue = rr_result(node: other_issue, attempt: rr_attempt, claim: b1.id, outcome: rr_published(), parent: b1.id, at: "t2") + let still_owed_after_other_issue = rr_precheck_owed(p: result_return_precheck(node_id: "result-returned-node", attempt_key: rr_attempt, binding: rr_bound(claim: c1.id), envelopes: [c1, c2, b1, on_other_issue]), claim: c1.id) + && rr_decision_refused_at(d: result_return_decision(binding: rr_binding(claim: c1.id), outcome: rr_published(), envelopes: [c1, c2, b1, on_other_issue]), step: "result-elsewhere") + let correct = rr_result(node: rr_node(), attempt: rr_attempt, claim: c1.id, outcome: rr_published(), parent: c2.id, at: "t3") + let settled = match result_return_precheck(node_id: "result-returned-node", attempt_key: rr_attempt, binding: rr_bound(claim: c1.id), envelopes: [c1, c2, correct]) { + ResultPrecheckSettled { outcome: ResultReturnAlreadyRecorded { node_id: _, attempt_key: _, event } } => event == (correct.id as String) + ResultPrecheckSettled { outcome: _ } => false + ResultPrecheckOwed { binding: _ } => false + } + history_population && unanswered && still_owed_after_wrong_claim && still_owed_after_other_issue && settled +} + +// THE READER'S POSITIVE CONTROL: the attempt named by its issue and key, its launch binding, and its +// issue's history give the one result naming the bound claim -- the event, the claim, the principal +// that claim returns to, and the outcome. A later claim by someone else is on the history and does +// not change the answer. +test fn the_reader_observes_the_result_for_the_attempts_exact_identity() -> Bool { + let c1 = rr_claim_by(node: rr_node(), requester: rr_alice(), parent: none, at: "t1") + let c2 = rr_claim_by(node: rr_node(), requester: rr_bob(), parent: Present { value: c1.id }, at: "t2") + let published = rr_result(node: rr_node(), attempt: rr_attempt, claim: c1.id, outcome: rr_published(), parent: c2.id, at: "2026-10-03T08:00:00Z") + match result_returned_observation_of(read: EventsRead { node: rr_node(), envelopes: [c2, published, c1] }, binding: rr_bound(claim: c1.id), node_id: "result-returned-node", attempt_key: rr_attempt) { + ResultReturnedObserved { node, event, claim, return_to, outcome, recorded_at } => + (node as String) == (rr_node() as String) + && (event as String) == (published.id as String) + && (claim as String) == (c1.id as String) + && event_principal_label(p: return_to) == "principal:google-oidc/accounts/sub-alice" + && outcome == rr_published() + && recorded_at == "2026-10-03T08:00:00Z" + ResultNotReturned { attempt_key: _ } => false + ResultNotOwed { reason: _ } => false + ResultReturnedUnobserved { step: _, reason: _ } => false + } +} + +// ABSENCE AND NOT-OWED ARE THEIR OWN ARMS. A readable issue history with only the claim, or with +// another attempt's result, answers not-returned for this attempt. An attempt launched autonomously +// or with no binding record is not owed a result at all -- which is not the same as not yet +// returned. +test fn an_unanswered_attempt_reads_as_not_returned_and_an_unrequested_one_as_not_owed() -> Bool { + let claim = rr_claim(node: rr_node()) + let other = rr_result(node: rr_node(), attempt: "0ther0ther0ther00", claim: claim.id, outcome: rr_published(), parent: claim.id, at: "t2") + let not_returned = all([[claim, other], [claim]], envelopes => match result_returned_observation_of(read: EventsRead { node: rr_node(), envelopes: envelopes }, binding: rr_bound(claim: claim.id), node_id: "result-returned-node", attempt_key: rr_attempt) { + ResultNotReturned { attempt_key } => attempt_key == rr_attempt + ResultReturnedObserved { node: _, event: _, claim: _, return_to: _, outcome: _, recorded_at: _ } => false + ResultNotOwed { reason: _ } => false + ResultReturnedUnobserved { step: _, reason: _ } => false + }) + let not_owed = all([RequestBindingAbsent, RequestBindingRead { binding: RequestAutonomous }], b => match result_returned_observation_of(read: EventsRead { node: rr_node(), envelopes: [claim] }, binding: b, node_id: "result-returned-node", attempt_key: rr_attempt) { + ResultNotOwed { reason } => string_length(s: reason) > 0 + ResultNotReturned { attempt_key: _ } => false + ResultReturnedObserved { node: _, event: _, claim: _, return_to: _, outcome: _, recorded_at: _ } => false + ResultReturnedUnobserved { step: _, reason: _ } => false + }) + not_returned && not_owed +} + +fn rr_unobserved_at(o: ResultReturnedObservation, step: String) -> Bool { + match o { + ResultReturnedUnobserved { step: s, reason } => s == step && string_length(s: reason) > 0 + ResultReturnedObserved { node: _, event: _, claim: _, return_to: _, outcome: _, recorded_at: _ } => false + ResultNotReturned { attempt_key: _ } => false + ResultNotOwed { reason: _ } => false + } +} + +// THE READER'S REDS. K is bound to issue A and claim C1. A result on A for K naming the later claim +// C2 is NOT read as K's answer: it refuses at the binding. A result on another issue under K's key +// is never consulted, so K reads as not returned. And each remaining refusal is its own step: an +// unreadable log, an unreadable binding, a forked history, two results for the attempt, a bound +// claim the history does not carry, and an empty identity. +test fn a_misaddressed_unreadable_forked_or_duplicated_result_is_not_the_attempts_answer() -> Bool { + let c1 = rr_claim_by(node: rr_node(), requester: rr_alice(), parent: none, at: "t1") + let c2 = rr_claim_by(node: rr_node(), requester: rr_bob(), parent: Present { value: c1.id }, at: "t2") + let wrong_claim = rr_result(node: rr_node(), attempt: rr_attempt, claim: c2.id, outcome: rr_published(), parent: c2.id, at: "t3") + let later_claim_is_not_the_answer = rr_unobserved_at(o: result_returned_observation_of(read: EventsRead { node: rr_node(), envelopes: [c1, c2, wrong_claim] }, binding: rr_bound(claim: c1.id), node_id: "result-returned-node", attempt_key: rr_attempt), step: "binding") + let other_issue = "result-returned-other-node" as RoadmapNodeId + let b1 = rr_claim(node: other_issue) + let on_other_issue = rr_result(node: other_issue, attempt: rr_attempt, claim: b1.id, outcome: rr_published(), parent: b1.id, at: "t2") + let other_issue_is_not_the_answer = match result_returned_observation_of(read: EventsRead { node: rr_node(), envelopes: [c1, c2, b1, on_other_issue] }, binding: rr_bound(claim: c1.id), node_id: "result-returned-node", attempt_key: rr_attempt) { + ResultNotReturned { attempt_key } => attempt_key == rr_attempt + ResultReturnedObserved { node: _, event: _, claim: _, return_to: _, outcome: _, recorded_at: _ } => false + ResultNotOwed { reason: _ } => false + ResultReturnedUnobserved { step: _, reason: _ } => false + } + let result = rr_result(node: rr_node(), attempt: rr_attempt, claim: c1.id, outcome: rr_published(), parent: c2.id, at: "t3") + let unreadable_log = rr_unobserved_at(o: result_returned_observation_of(read: EventsReadRefused { node: rr_node(), step: "sync/fetch", reason: "exit 128" }, binding: rr_bound(claim: c1.id), node_id: "result-returned-node", attempt_key: rr_attempt), step: "events/sync/fetch") + let unreadable_binding = rr_unobserved_at(o: result_returned_observation_of(read: EventsRead { node: rr_node(), envelopes: [c1, c2, result] }, binding: RequestBindingUnreadable { reason: "denied" }, node_id: "result-returned-node", attempt_key: rr_attempt), step: "request-binding") + let sibling = rr_ev(node: rr_node(), kind: Progress { note: "a writer who never saw the result" as NonEmptyStr }, parent: Present { value: c2.id }, author: rr_alice(), at: "t3") + let forked = rr_unobserved_at(o: result_returned_observation_of(read: EventsRead { node: rr_node(), envelopes: [c1, c2, result, sibling] }, binding: rr_bound(claim: c1.id), node_id: "result-returned-node", attempt_key: rr_attempt), step: "history") + let second = rr_result(node: rr_node(), attempt: rr_attempt, claim: c1.id, outcome: rr_failed(), parent: result.id, at: "t4") + let duplicated = rr_unobserved_at(o: result_returned_observation_of(read: EventsRead { node: rr_node(), envelopes: [c1, c2, result, second] }, binding: rr_bound(claim: c1.id), node_id: "result-returned-node", attempt_key: rr_attempt), step: "result-duplicate") + let orphan = rr_result(node: rr_node(), attempt: rr_attempt, claim: "0000000000000000" as RoadmapEventId, outcome: rr_published(), parent: c2.id, at: "t3") + let lost_claim = rr_unobserved_at(o: result_returned_observation_of(read: EventsRead { node: rr_node(), envelopes: [c1, c2, orphan] }, binding: rr_bound(claim: "0000000000000000" as RoadmapEventId), node_id: "result-returned-node", attempt_key: rr_attempt), step: "claim") + let keyless = rr_unobserved_at(o: result_returned_observation_of(read: EventsRead { node: rr_node(), envelopes: [c1, c2, result] }, binding: rr_bound(claim: c1.id), node_id: "result-returned-node", attempt_key: ""), step: "attempt-identity") + later_claim_is_not_the_answer && other_issue_is_not_the_answer && unreadable_log && unreadable_binding && forked && duplicated && lost_claim && keyless +} + +// THE ROUTE OVER ITS PURE LINKS, WITH THE REAL CODECS IN THE MIDDLE. The dispatch decision carries +// claim C1; a second claim lands before the attempt initializes; the binding is admitted for C1, +// written and read back through its own codec, and committed create-only. The request is joined +// from it; the outcome is classified from a handoff state; the decision asks for an append; the +// result event is built FROM THE BINDING, goes to the wire and comes back through the carrier's +// decode; and the reader, given the attempt's issue, key and binding, answers with claim C1, its +// requester and the outcome the route started from. +test fn a_bound_attempts_result_reaches_its_requester_through_the_real_codecs() -> Bool { + let c1 = rr_claim_by(node: rr_node(), requester: rr_alice(), parent: none, at: "t1") + let c2 = rr_claim_by(node: rr_node(), requester: rr_bob(), parent: Present { value: c1.id }, at: "t2") + match attempt_request_binding_admission(request: LaunchForClaim { node: rr_node(), claim: c1.id }, node_id: rr_node(), read: EventsRead { node: rr_node(), envelopes: [c1, c2] }, predecessor: RequestBindingAbsent) { + RequestBindingNotAdmitted { step: _, detail: _ } => false + RequestBindingAdmitted { binding: launch } => + match attempt_request_binding_commit_of(binding: launch, created: FilesystemCreated { path: "p" }, existing: RequestBindingAbsent) { + RequestBindingCommitRefused { step: _, detail: _ } => false + RequestBindingCommitted { binding: committed } => { + let recorded = attempt_request_binding_read_of(success: true, error_kind: "", error: "", content: attempt_request_binding_json(binding: committed)) + match result_return_precheck(node_id: "result-returned-node", attempt_key: rr_attempt, binding: recorded, envelopes: [c1, c2]) { + ResultPrecheckSettled { outcome: _ } => false + ResultPrecheckOwed { binding: bound } => + match attempt_result_standing(handoff: HandoffVerificationFailed { head_sha: "52c56a8", verdict_key: "validation-failed" }) { + AttemptResultPending { handoff: _ } => false + AttemptResultUnstatable { reason: _ } => false + AttemptResultTerminal { outcome } => + match result_return_decision(binding: bound, outcome: outcome, envelopes: [c1, c2]) { + ResultAlreadyReturned { id: _ } => false + ResultReturnRefused { step: _, reason: _ } => false + ResultReturnAppend { binding: to_write, outcome: final } => { + let appended = roadmap_event_envelope(e: roadmap_result_returned_event(binding: to_write, outcome: final, author: rr_fabric(), parent: Present { value: c2.id }, recorded_at: "2026-10-03T10:00:00Z")) + let read = roadmap_events_decode_reads(node: rr_node(), reads: [ + EventFileContent { content: roadmap_event_wire_text(env: c1) }, + EventFileContent { content: roadmap_event_wire_text(env: c2) }, + EventFileContent { content: roadmap_event_wire_text(env: appended) }, + ]) + match result_returned_observation_of(read: read, binding: recorded, node_id: "result-returned-node", attempt_key: rr_attempt) { + ResultReturnedObserved { node, event: id, claim: answered, return_to: to, outcome: returned, recorded_at: _ } => + (node as String) == "result-returned-node" + && (id as String) == (appended.id as String) + && (answered as String) == (c1.id as String) + && event_principal_label(p: to) == "principal:google-oidc/accounts/sub-alice" + && returned == rr_failed() + ResultNotReturned { attempt_key: _ } => false + ResultNotOwed { reason: _ } => false + ResultReturnedUnobserved { step: _, reason: _ } => false + } + } + } + } + } + } + } + } +} + +// THE PASS'S OWN ENTRY ON AN OBSERVE-ONLY INSTANCE WRITES NOTHING AND SAYS SO: the real +// result_return_attempts_for_instance is driven against srv2 preview and withholds on the posture +// before it reads the log or observes an attempt. +test fn the_result_return_pass_withholds_on_an_observe_only_instance() -> Bool { + match result_return_attempts_for_instance(instance: srv2_preview_dashboard_instance(), recorded_at: "2026-10-03T10:00:00Z") { + ResultReturnPassWithheld { reason } => string_contains(s: reason, pattern: "observe-only") + ResultReturnPassRan { outcomes: _ } => false + ResultReturnPassRefused { step: _, reason: _ } => false + } +} + +// THE TICK'S EXIT OVER THE PASS, AND BOTH TAIL CAUSES WHEN BOTH FAIL. An undelivered result is a +// failure naming the attempt and the step; a pass that could not read its inputs names the step; an +// attempt still in motion and one with no request are counted outcomes and exit clean. When the served observation fails too, the exit carries BOTH reasons -- the red is +// an exit that reports the observation's failure and drops the undelivered result. +test fn an_undelivered_result_fails_the_tick_and_is_named_beside_an_observation_failure() -> Bool { + let undelivered_pass = ResultReturnPassRan { outcomes: [ + ResultReturnRecorded { node_id: "a", attempt_key: "k1", event: "e1", outcome_word: "published" }, + ResultReturnFailed { node_id: "b", attempt_key: "k2", step: "publication-refused", reason: "conditional publication refused" }, + ] } + let undelivered = match result_return_pass_exit(pass: undelivered_pass) { + ExitFailure { code: _, reason } => string_contains(s: reason, pattern: "b attempt k2 (publication-refused): conditional publication refused") && string_contains(s: reason, pattern: "1 attempt result(s)") + ExitSuccess => false + } + let unreadable = match result_return_pass_exit(pass: ResultReturnPassRefused { step: "events/sync/fetch", reason: "exit 128" }) { + ExitFailure { code: _, reason } => string_contains(s: reason, pattern: "events/sync/fetch") + ExitSuccess => false + } + let clean_pass = ResultReturnPassRan { outcomes: [ + ResultReturnNotTerminal { node_id: "a", attempt_key: "k1", handoff: "submitted" }, + ResultReturnNoRequest { node_id: "b", attempt_key: "k2", reason: "launched unclaimed" }, + ] } + let clean = all([clean_pass, ResultReturnPassRan { outcomes: [] }, ResultReturnPassWithheld { reason: "observe-only" }], p => match result_return_pass_exit(pass: p) { ExitSuccess => true ExitFailure { code: _, reason: _ } => false }) + let both = match belt_tick_result_tail_exit(returned: undelivered_pass, tail: ExitFailure { code: 1, reason: "served observation write failed" }) { + ExitFailure { code, reason } => code == 1 && string_contains(s: reason, pattern: "served observation write failed") && string_contains(s: reason, pattern: "b attempt k2 (publication-refused)") + ExitSuccess => false + } + let observation_only = match belt_tick_result_tail_exit(returned: clean_pass, tail: ExitFailure { code: 1, reason: "served observation write failed" }) { + ExitFailure { code: _, reason } => reason == "served observation write failed" + ExitSuccess => false + } + let result_only = match belt_tick_result_tail_exit(returned: undelivered_pass, tail: ExitSuccess) { + ExitFailure { code: _, reason } => string_contains(s: reason, pattern: "b attempt k2") + ExitSuccess => false + } + let neither = match belt_tick_result_tail_exit(returned: clean_pass, tail: ExitSuccess) { ExitSuccess => true ExitFailure { code: _, reason: _ } => false } + undelivered && unreadable && clean && both && observation_only && result_only && neither +}