diff --git a/dag/gunbc/roadmap/roadmap_acceptance_history_carrier.dag b/dag/gunbc/roadmap/roadmap_acceptance_history_carrier.dag index d03766bc8ea..ee8c7671ccf 100644 --- a/dag/gunbc/roadmap/roadmap_acceptance_history_carrier.dag +++ b/dag/gunbc/roadmap/roadmap_acceptance_history_carrier.dag @@ -1,7 +1,13 @@ module gunbc.roadmap_acceptance_history_carrier import std.types { List } -import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.filesystem.filesystem_io { + Filesystem, + FilesystemReadOutcome, + FilesystemReadRefused, + FilesystemReadSucceeded, + filesystem_read_outcome, +} import gunbc.roadmap_model { RoadmapAcceptanceEvent } import std.decl_ref { DeclarationRef, WholeDeclaration } import std.disposition { Disposition, RealizationDispatch, Scaffold } @@ -33,12 +39,13 @@ fn load_roadmap_acceptance_event_history_from_carrier_text(text: String) -> Road fn load_roadmap_acceptance_event_history(path: String) -> RoadmapAcceptanceEventHistoryLoad { let read = Filesystem.Read(path: path) - if !read.success { - RoadmapAcceptanceEventHistoryLoadRefused { - detail: concat("carrier read refused: ", read.error), - } - } else { - load_roadmap_acceptance_event_history_from_carrier_text(text: read.content) + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error } => + RoadmapAcceptanceEventHistoryLoadRefused { + detail: concat("carrier read refused: ", error), + } + FilesystemReadSucceeded { content } => + load_roadmap_acceptance_event_history_from_carrier_text(text: content) } } diff --git a/dag/gunbc/roadmap/roadmap_attempt_request_record.dag b/dag/gunbc/roadmap/roadmap_attempt_request_record.dag index f5a48600a70..821c6cebb8d 100644 --- a/dag/gunbc/roadmap/roadmap_attempt_request_record.dag +++ b/dag/gunbc/roadmap/roadmap_attempt_request_record.dag @@ -4,7 +4,10 @@ import std.types { String, Bool, List, NonEmptyStr } import extdeps.filesystem.filesystem_io { Filesystem, FilesystemCreateNew, FilesystemCreated, FilesystemCreateTargetOccupied, FilesystemCreateRefused, FilesystemCreateKindUnrecognized, filesystem_create_new, + FilesystemFileObservation, FilesystemFileRead, FilesystemFileAbsent, FilesystemFileIndeterminate, + FilesystemFileObservationsDisagree, FilesystemFileSubjectRefused, } +import gunbc.filesystem_file_observe { filesystem_file_observation_of_path } 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, @@ -155,20 +158,24 @@ type AttemptRequestBindingRead | 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_from_observation(observation: FilesystemFileObservation) -> AttemptRequestBindingRead { + match observation { + FilesystemFileAbsent(_) => RequestBindingAbsent + FilesystemFileRead { path: _, content } => + match attempt_request_binding_decode(text: content) { + RequestBindingDecoded { binding } => RequestBindingRead { binding: binding } + RequestBindingUndecodable { reason } => RequestBindingUnreadable { reason: reason } + } + FilesystemFileIndeterminate { cause } => RequestBindingUnreadable { reason: cause } + FilesystemFileObservationsDisagree { path: _, cause } => RequestBindingUnreadable { reason: cause } + FilesystemFileSubjectRefused { directory: _, name: _, cause } => RequestBindingUnreadable { reason: cause } } } 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) + attempt_request_binding_read_from_observation( + observation: filesystem_file_observation_of_path( + path: attempt_request_binding_path_for_instance(instance: instance, node_id: node_id, attempt_key: attempt_key), + ), + ) } diff --git a/dag/gunbc/roadmap/roadmap_belt_actuate.dag b/dag/gunbc/roadmap/roadmap_belt_actuate.dag index 8c5ac14b5d8..0014edda5b1 100644 --- a/dag/gunbc/roadmap/roadmap_belt_actuate.dag +++ b/dag/gunbc/roadmap/roadmap_belt_actuate.dag @@ -533,6 +533,9 @@ import gunbc.roadmap_session_placement { session_placement_observe_for_attempt, SessionPlacementUnreadable, session_placement_settle_pass, SessionSettlePass, SessionSettlePassRan, SessionSettlePassRefused, session_settle_pass_wire, } +import gunbc.roadmap_workflow_command { + WorkflowModeledStateStanding, WorkflowModeledStateObserved, WorkflowModeledStateAbsent, WorkflowModeledStateUnreadable, +} import gunbc.roadmap_workflow_progress { AttemptWorktreeObservation, AttemptWorktreePresent, AttemptWorktreeAbsent, AttemptWorktreeUnobserved, attempt_worktree_observation_of, WorkflowAttemptEvidence, @@ -577,9 +580,13 @@ import extdeps.filesystem.filesystem_io { filesystem_listing_observation, filesystem_listing_names_entry, filesystem_entry_presence, + FilesystemReadOutcome, + FilesystemReadRefused, + FilesystemReadSucceeded, filesystem_read_outcome, filesystem_file_observation, } +import gunbc.filesystem_file_observe { filesystem_file_observation_of_path } import extdeps.llm.cli { shape_codex_login_status_env_args_for_program, shape_codex_version_env_args_for_program, @@ -1007,9 +1014,7 @@ fn belt_oracle_observe_in_worktree( ) validation_oracle_observe_from_source( oracle: oracle, - source_readable: read.success, - source: read.content, - read_error: read.error, + source_read: filesystem_read_outcome(content: read.content, success: read.success, error: read.error), ) } @@ -1612,8 +1617,7 @@ fn belt_claude_preflight_decision( claude_on_path: Bool, home: String, trust_store_path: String, - trust_store_readable: Bool, - trust_store_text: String, + trust_store_read: FilesystemReadOutcome, trust_root: String, ) -> BeltClaudePreflight { if !claude_on_path { @@ -1626,24 +1630,29 @@ fn belt_claude_preflight_decision( step: "claude-trust-home", detail: "HOME unset or empty — cannot locate the claude trust store (~/.claude.json), so the workspace-trust seed is unknowable; fail-closed", } - } else if !trust_store_readable { - ClaudePreflightRefused { - step: "claude-trust-store", - detail: concat( - concat("claude trust store unreadable at ", trust_store_path), - " — claude blocks on its workspace-trust dialog for an unseeded root; seed it by launching claude once under the dispatch worktree root, or converge via the live_deploy membership", - ), - } - } else if !claude_workspace_trust_seeded(config_text: trust_store_text, project_path: trust_root) { - ClaudePreflightRefused { - step: "claude-trust-seed", - detail: concat( - concat(concat("workspace trust not seeded for ", trust_root), " in "), - concat(trust_store_path, " (projects entry with hasTrustDialogAccepted: true) — claude would block interactively on its trust dialog; ancestor-dir trust inheritance means seeding this ONE root covers every dispatch worktree"), - ), - } } else { - ClaudePreflightOk + match trust_store_read { + FilesystemReadRefused { error: _ } => + ClaudePreflightRefused { + step: "claude-trust-store", + detail: concat( + concat("claude trust store unreadable at ", trust_store_path), + " — claude blocks on its workspace-trust dialog for an unseeded root; seed it by launching claude once under the dispatch worktree root, or converge via the live_deploy membership", + ), + } + FilesystemReadSucceeded { content: trust_store_text } => + if !claude_workspace_trust_seeded(config_text: trust_store_text, project_path: trust_root) { + ClaudePreflightRefused { + step: "claude-trust-seed", + detail: concat( + concat(concat("workspace trust not seeded for ", trust_root), " in "), + concat(trust_store_path, " (projects entry with hasTrustDialogAccepted: true) — claude would block interactively on its trust dialog; ancestor-dir trust inheritance means seeding this ONE root covers every dispatch worktree"), + ), + } + } else { + ClaudePreflightOk + } + } } } @@ -1658,8 +1667,7 @@ fn belt_claude_preflight_for_instance(instance: HostDashboardInstance) -> BeltCl claude_on_path: claude_ok, home: home, trust_store_path: "", - trust_store_readable: false, - trust_store_text: "", + trust_store_read: filesystem_read_outcome(content: "", success: false, error: "unused on refusal"), trust_root: dashboard_instance_dispatch_worktree_root(instance: instance) as String, ) } else { @@ -1669,8 +1677,7 @@ fn belt_claude_preflight_for_instance(instance: HostDashboardInstance) -> BeltCl claude_on_path: claude_ok, home: home, trust_store_path: path, - trust_store_readable: r.success, - trust_store_text: r.content, + trust_store_read: filesystem_read_outcome(content: r.content, success: r.success, error: r.error), trust_root: dashboard_instance_dispatch_worktree_root(instance: instance) as String, ) } @@ -3645,37 +3652,41 @@ fn belt_pass_outcome_from_member(doc: JsonValue, key: String) -> BeltPassOutcome } } -fn belt_tick_receipt_read_from_filesystem( - success: Bool, - content: String, - error: String, -) -> BeltTickReceiptRead { - if success { - if trim(content) == "" { - BeltTickReceiptAbsent - } else { - belt_tick_receipt_decode(raw: content) - } - } else if string_contains(s: error, pattern: "No such file") - || string_contains(s: error, pattern: "not found") { - BeltTickReceiptAbsent - } else { - BeltTickReceiptUnreadable { - reason: join(["belt tick receipt could not be read: ", error], ""), - } +// The tick receipt read classifies an OBSERVATION, not a host error string: the old classifier +// text-matched the host's error ("No such file", "not found") to decide absent versus unreadable, +// which breaks on every host phrasing and every error-transport change. The observation authority +// establishes absence from the listing instead, and the host error rides inside the cause text. +fn belt_tick_receipt_read_from_observation(observation: FilesystemFileObservation) -> BeltTickReceiptRead { + match observation { + FilesystemFileAbsent(_) => BeltTickReceiptAbsent + FilesystemFileRead { path: _, content } => + if trim(content) == "" { + BeltTickReceiptAbsent + } else { + belt_tick_receipt_decode(raw: content) + } + FilesystemFileIndeterminate { cause } => + BeltTickReceiptUnreadable { + reason: join(["belt tick receipt could not be read: ", cause], ""), + } + FilesystemFileObservationsDisagree { path: _, cause } => + BeltTickReceiptUnreadable { + reason: join(["belt tick receipt could not be read: ", cause], ""), + } + FilesystemFileSubjectRefused { directory: _, name: _, cause } => + BeltTickReceiptUnreadable { + reason: join(["belt tick receipt could not be read: ", cause], ""), + } } } fn belt_tick_receipt_read_for_instance( instance: HostDashboardInstance, ) -> BeltTickReceiptRead { - let read = Filesystem.Read( - path: belt_tick_receipt_path_for_instance(instance: instance), - ) - belt_tick_receipt_read_from_filesystem( - success: read.success, - content: read.content, - error: read.error, + belt_tick_receipt_read_from_observation( + observation: filesystem_file_observation_of_path( + path: belt_tick_receipt_path_for_instance(instance: instance), + ), ) } @@ -5008,47 +5019,89 @@ fn belt_occurrence_unit_row(unit: String) -> ObservedContainerRow { // attempt whose identity cannot be read produces no record, so a live container over it joins // nothing and reads Unclassified -- visible, occupying, never reaped -- rather than borrowing // today's selection as its meaning. -fn belt_occurrence_record_for_attempt(instance: HostDashboardInstance, attempt: DispatchAttemptRef) -> List { +// The attempt's durable occurrence is a typed read outcome, never a silently emptied list: before +// the fold conversion, an unreadable launch record returned [] and the occurrence census lost the +// attempt without a word. A refusal here reaches the census as a refusal naming the attempt, the +// same way an unobservable attempt population already does. +type AttemptOccurrenceRecordRead + = AttemptOccurrenceRecordDecoded { record: DurableAttemptRecord } + | AttemptOccurrenceRecordRefused { reason: NonEmptyStr } + +fn belt_occurrence_record_from_launch_read(attempt: DispatchAttemptRef, read: FilesystemReadOutcome) -> AttemptOccurrenceRecordRead { + match read { + FilesystemReadRefused { error } => + AttemptOccurrenceRecordRefused { reason: join(["attempt ", attempt.attempt_key, ": the launch record could not be read, so its durable occurrence is unknown: ", error], "") } + FilesystemReadSucceeded { content } => + match parse_json_document(s: content) { + JsonDocumentUnreadable { gap } => + AttemptOccurrenceRecordRefused { reason: join(["attempt ", attempt.attempt_key, ": the launch record does not decode as JSON: ", gap], "") } + JsonDocumentParsed { value: doc } => + match launch_identity_of_json(doc: doc) { + LaunchIdentityUnreadable { reason } => + AttemptOccurrenceRecordRefused { reason: join(["attempt ", attempt.attempt_key, ": the launch record carries no readable launch identity: ", reason], "") } + LaunchIdentityDecoded { identity } => + AttemptOccurrenceRecordDecoded { + record: DurableAttemptRecord { + node: attempt.node_id as String, + attempt: attempt.attempt_key, + spawn_identity: join([ + "cause=", launch_cause_label(c: identity.cause), + " mode=", spawn_mode_label(m: identity.mode), + " instance=", identity.instance_id as String, + " revision=", git_object_id_wire_hex(oid: identity.revision) as String, + ], ""), + }, + } + } + } + } +} + +fn belt_occurrence_record_for_attempt(instance: HostDashboardInstance, attempt: DispatchAttemptRef) -> AttemptOccurrenceRecordRead { let path = dispatch_attempt_launch_path_for_instance( instance: instance, node_id: attempt.node_id as gunbc.roadmap_model.RoadmapNodeId, attempt_key: attempt.attempt_key, ) let read = Filesystem.Read(path: path) - if !read.success { - [] - } else { - match parse_json_document(s: read.content) { - JsonDocumentUnreadable { gap: _ } => [] - JsonDocumentParsed { value: doc } => - match launch_identity_of_json(doc: doc) { - LaunchIdentityUnreadable { reason: _ } => [] - LaunchIdentityDecoded { identity } => - [DurableAttemptRecord { - node: attempt.node_id as String, - attempt: attempt.attempt_key, - spawn_identity: join([ - "cause=", launch_cause_label(c: identity.cause), - " mode=", spawn_mode_label(m: identity.mode), - " instance=", identity.instance_id as String, - " revision=", git_object_id_wire_hex(oid: identity.revision) as String, - ], ""), - }] - } - } - } + belt_occurrence_record_from_launch_read( + attempt: attempt, + read: filesystem_read_outcome(content: read.content, success: read.success, error: read.error), + ) } -fn belt_occurrence_records_for_instance(instance: HostDashboardInstance) -> List? { +// One unreadable attempt refuses the whole census rather than shrinking it silently: a census with +// a hole is not a census. The refusal names every attempt whose record could not be read. +type BeltAttemptOccurrenceRecords + = BeltAttemptOccurrenceRecordsObserved { records: List } + | BeltAttemptOccurrenceRecordsRefused { reason: NonEmptyStr } + +fn belt_occurrence_records_for_instance(instance: HostDashboardInstance) -> BeltAttemptOccurrenceRecords { match belt_attempts_observe_for_instance(instance: instance) { - AttemptsRefused { reason } => none - AttemptsObserved { attempts } => - Present { - value: attempts |> flat_map(a => belt_occurrence_record_for_attempt(instance: instance, attempt: a)), + AttemptsRefused { reason } => + BeltAttemptOccurrenceRecordsRefused { reason: join(["the attempt population is unobservable: ", reason], "") } + AttemptsObserved { attempts } => { + let outcomes = map(attempts, a => belt_occurrence_record_for_attempt(instance: instance, attempt: a)) + let refusal_reasons = fold(outcomes, init: [], f: (acc, o) => match o { + AttemptOccurrenceRecordRefused { reason } => concat(acc, [reason as String]) + AttemptOccurrenceRecordDecoded { record: _ } => acc + }) + if any(outcomes, o => match o { + AttemptOccurrenceRecordRefused { reason: _ } => true + AttemptOccurrenceRecordDecoded { record: _ } => false + }) { + BeltAttemptOccurrenceRecordsRefused { reason: join(refusal_reasons, "; ") as NonEmptyStr } + } else { + BeltAttemptOccurrenceRecordsObserved { + records: flat_map(outcomes, o => match o { + AttemptOccurrenceRecordDecoded { record: r } => [r] + AttemptOccurrenceRecordRefused { reason: _ } => [] + }), + } } + } } } - fn belt_attempt_occurrences_observe_for_instance(instance: HostDashboardInstance) -> BeltOccurrencePopulation { match belt_sessions_observe_for_instance(instance: instance) { ObserveRefused { reason } => @@ -5059,9 +5112,9 @@ fn belt_attempt_occurrences_observe_for_instance(instance: HostDashboardInstance OccurrencePopulationRefused { reason: "the user manager's gunbc unit population is unobservable; the census over half the population is unknowable" } Present { value: unit_names } => match belt_occurrence_records_for_instance(instance: instance) { - Absent => - OccurrencePopulationRefused { reason: "the durable attempt records are unobservable; occurrence identity cannot be joined" } - Present { value: records } => + BeltAttemptOccurrenceRecordsRefused { reason } => + OccurrencePopulationRefused { reason: join(["the durable attempt records are unobservable; occurrence identity cannot be joined: ", reason], "") } + BeltAttemptOccurrenceRecordsObserved { records } => OccurrencePopulationObserved { occurrences: occurrence_population( containers: concat( @@ -5502,6 +5555,132 @@ fn belt_verification_receipt_source( } } +// The modeled-state pointer read is a typed outcome, and the two failure facts are kept apart: a +// pointer that is established absent means this attempt has no modeled state; a pointer that could +// not be read means WHETHER the attempt has modeled state is unknown. Collapsing them -- the old +// `!read.success || trim(content) == ""` -- made an unreadable pointer claim "has no modeled state +// pointer", an absence the read never established. +type WorkflowModeledStatePointerState + = WorkflowModeledStatePointerAbsent + | WorkflowModeledStatePointerUnreadable { cause: NonEmptyStr } + +// The pointer standing decides the subordinate facts' standing: established ABSENCE (no pointer) +// carries absence-shaped facts -- the legacy arm's own meaning -- while an UNREADABLE pointer +// carries non-absence variants: the wrong state is unwritable, and nothing downstream may read an +// unreadable pointer as "not yet". +fn selection_standing_for_pointer(pointer_state: WorkflowModeledStatePointerState) -> CurrentVerificationSelectionObservation { + match pointer_state { + WorkflowModeledStatePointerAbsent => SelectionAbsent + WorkflowModeledStatePointerUnreadable { cause } => + SelectionUnaddressable { + cause: join(["this attempt's current-attempt-key pointer could not be read, so whether a verification window was selected is unknown: ", cause], ""), + } + } +} + +fn verification_source_standing_for_pointer(pointer_state: WorkflowModeledStatePointerState) -> VerificationReceiptFileSource { + match pointer_state { + WorkflowModeledStatePointerAbsent => VerificationReceiptAbsent + WorkflowModeledStatePointerUnreadable { cause } => + VerificationReceiptUnreadable { + detail: join(["this attempt's current-attempt-key pointer could not be read, so its verification receipt could not be located: ", cause], ""), + } + } +} + +fn publication_source_standing_for_pointer(pointer_state: WorkflowModeledStatePointerState) -> PublicationReceiptSource { + match pointer_state { + WorkflowModeledStatePointerAbsent => ReceiptSourceAbsent + WorkflowModeledStatePointerUnreadable { cause } => + ReceiptSourceUnreadable { + detail: join(["this attempt's current-attempt-key pointer could not be read, so its publication receipt could not be located: ", cause], ""), + } + } +} + +fn review_report_standing_for_pointer(pointer_state: WorkflowModeledStatePointerState) -> ReviewReport { + match pointer_state { + WorkflowModeledStatePointerAbsent => review_report_absent(reason: "this attempt has no modeled state pointer, so no review report exists for it") + WorkflowModeledStatePointerUnreadable { cause } => + review_report_refused(reason: join(["this attempt's current-attempt-key pointer could not be read, so its review report could not be located: ", cause], "")) + } +} + +fn belt_workflow_attempt_evidence_without_modeled_state( + instance: HostDashboardInstance, + panes: BeltAttemptPanes, + attempt: DispatchAttemptRef, + pointer_state: WorkflowModeledStatePointerState, +) -> WorkflowAttemptEvidence { + let pointer_reason = match pointer_state { + WorkflowModeledStatePointerAbsent => + "this attempt has no modeled state pointer, so neither its attempt key nor its head is established" + WorkflowModeledStatePointerUnreadable { cause } => + join(["this attempt's current-attempt-key pointer could not be read, so neither its attempt key nor its head is established: ", cause], "") + } + let worktree_cause = match pointer_state { + WorkflowModeledStatePointerAbsent => + "this attempt has no modeled state pointer, so no attempt key names its worktree" + WorkflowModeledStatePointerUnreadable { cause } => + join(["this attempt's current-attempt-key pointer could not be read, so whether an attempt key names its worktree is unknown: ", cause], "") + } + let standing = match pointer_state { + WorkflowModeledStatePointerAbsent => WorkflowModeledStateAbsent + WorkflowModeledStatePointerUnreadable { cause } => WorkflowModeledStateUnreadable { cause: cause } + } + let placement_detail = match pointer_state { + WorkflowModeledStatePointerAbsent => + "this attempt has no modeled state pointer, so no attempt key names its session placement" + WorkflowModeledStatePointerUnreadable { cause } => + join(["this attempt's current-attempt-key pointer could not be read, so whether an attempt key names its session placement is unknown: ", cause], "") + } + let provider_events_error = match pointer_state { + WorkflowModeledStatePointerAbsent => "" + WorkflowModeledStatePointerUnreadable { cause } => cause as String + } + WorkflowAttemptEvidence { + node_id: attempt.node_id, + attempt_key: attempt.attempt_key, + branch: attempt.branch, + modeled_state: standing, + worktree: AttemptWorktreeUnobserved { path: "", cause: worktree_cause }, + admission_receipt_readable: false, + admission_receipt: "", + spawn_failure_present: false, + spawn_failure: "", + provider_events_readable: false, + provider_events: "", + provider_events_error: provider_events_error, + process: attempt_process_for_node( + panes: panes, + node_id: attempt.node_id, + attempt_key: attempt.attempt_key, + ), + validation_summary: belt_validation_summary_for_node( + instance: instance, + node_id: attempt.node_id, + ), + verification_subject: VerificationSubjectUnobserved { + reason: pointer_reason, + }, + verification_selection: selection_standing_for_pointer(pointer_state: pointer_state), + verification_source: verification_source_standing_for_pointer(pointer_state: pointer_state), + publication_subject: PublicationSubjectUnobserved { + reason: pointer_reason, + }, + publication_source: publication_source_standing_for_pointer(pointer_state: pointer_state), + submission_capture: CaptureUnreadable { + reason: pointer_reason, + }, + integration: IntegrationReceiptUnreadable { + reason: pointer_reason, + }, + review: review_report_standing_for_pointer(pointer_state: pointer_state), + goal_audit: [], + session_placement: SessionPlacementUnreadable { unit: "", detail: placement_detail }, + } +} + fn belt_workflow_attempt_evidence_for_ref( instance: HostDashboardInstance, panes: BeltAttemptPanes, @@ -5513,57 +5692,28 @@ fn belt_workflow_attempt_evidence_for_ref( node_id: attempt.node_id as gunbc.roadmap_model.RoadmapNodeId, ), ) - if !key_read.success || trim(key_read.content) == "" { - WorkflowAttemptEvidence { - node_id: attempt.node_id, - attempt_key: attempt.attempt_key, - branch: attempt.branch, - modeled_state_present: false, - worktree: AttemptWorktreeUnobserved { path: "", cause: "this attempt has no modeled state pointer, so no attempt key names its worktree" }, - admission_receipt_readable: false, - admission_receipt: "", - spawn_failure_present: false, - spawn_failure: "", - provider_events_readable: false, - provider_events: "", - provider_events_error: key_read.error, - process: attempt_process_for_node( - panes: panes, - node_id: attempt.node_id, - attempt_key: attempt.attempt_key, - ), - validation_summary: belt_validation_summary_for_node( + match filesystem_read_outcome(content: key_read.content, success: key_read.success, error: key_read.error) { + FilesystemReadRefused { error } => + belt_workflow_attempt_evidence_without_modeled_state( instance: instance, - node_id: attempt.node_id, - ), - verification_subject: VerificationSubjectUnobserved { - reason: "this attempt has no modeled state pointer, so neither its attempt key nor its head is established", - }, - verification_selection: SelectionAbsent, - verification_source: VerificationReceiptAbsent, - publication_subject: PublicationSubjectUnobserved { - reason: "this attempt has no modeled state pointer, so neither its attempt key nor its head is established", - }, - publication_source: ReceiptSourceAbsent, - submission_capture: CaptureUnreadable { - reason: "this attempt has no modeled state pointer, so neither its attempt key nor its head is established", - }, - integration: IntegrationReceiptUnreadable { - reason: "this attempt has no modeled state pointer, so neither its attempt key nor its head is established", - }, - review: review_report_absent(reason: "this attempt has no modeled state pointer, so neither its attempt key nor its head is established"), - goal_audit: [], - session_placement: SessionPlacementUnreadable { unit: "", detail: "this attempt has no modeled state pointer, so no attempt key names its session placement" }, - } - } else { - belt_workflow_attempt_evidence_for_key(instance: instance, panes: panes, attempt: attempt, current_key: trim(key_read.content)) + panes: panes, + attempt: attempt, + pointer_state: WorkflowModeledStatePointerUnreadable { cause: error as NonEmptyStr }, + ) + FilesystemReadSucceeded { content } => + if trim(content) == "" { + belt_workflow_attempt_evidence_without_modeled_state( + instance: instance, + panes: panes, + attempt: attempt, + pointer_state: WorkflowModeledStatePointerAbsent, + ) + } else { + belt_workflow_attempt_evidence_for_key(instance: instance, panes: panes, attempt: attempt, current_key: trim(content)) + } } } -// THE ATTEMPT-VARYING RECORDS OF ONE NAMED ATTEMPT, addressed by its key -- the one place the -// evidence builder's admission, provider-event and spawn-failure reads are named, so a witness can -// establish that a historical attempt's reads name that attempt and never the node's current -// projection. type BeltAttemptRecordPaths { admission: String events: String @@ -5725,7 +5875,7 @@ fn belt_workflow_attempt_evidence_for_key( node_id: node_id, attempt_key: current_key, ), - modeled_state_present: true, + modeled_state: WorkflowModeledStateObserved, worktree: belt_attempt_worktree_observe(instance: instance, node_id: node_id, attempt_key: current_key), admission_receipt_readable: admission.success, admission_receipt: admission.content, @@ -5905,15 +6055,16 @@ fn belt_submission_observe(worktree: String) -> BeltSubmissionRead { BeltSubmissionObserved { observation: SubmissionAbsent } } else { let read = Filesystem.Read(path: join([worktree, "/", submission_artifact_name as String], "")) - if !read.success { - BeltSubmissionUnobservable { - reason: join([ - "the submission artifact was listed and then could not be read, so whether it declares a candidate is unknown: ", - read.error, - ], ""), - } - } else { - match submission_decode(text: read.content) { + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error } => + BeltSubmissionUnobservable { + reason: join([ + "the submission artifact was listed and then could not be read, so whether it declares a candidate is unknown: ", + error, + ], ""), + } + FilesystemReadSucceeded { content } => + match submission_decode(text: content) { SubmissionNotDecodable { reason } => BeltSubmissionObserved { observation: SubmissionUndecodable { reason: reason } } SubmissionDecoded { submission } => @@ -5995,15 +6146,16 @@ fn belt_submission_captures_observe(instance: HostDashboardInstance, worktree: S CaptureRosterUnobservable { reason: _ } => acc CaptureRosterObserved { captures } => { let read = Filesystem.Read(path: join([dir, "/", trim(entry)], "")) - if !read.success { - CaptureRosterUnobservable { - reason: join([ - "a submission capture record was listed and then could not be read: ", - trim(entry), " (", read.error, ")", - ], ""), - } - } else { - match submission_capture_decode(text: read.content) { + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error } => + CaptureRosterUnobservable { + reason: join([ + "a submission capture record was listed and then could not be read: ", + trim(entry), " (", error, ")", + ], ""), + } + FilesystemReadSucceeded { content } => + match submission_capture_decode(text: content) { CaptureNotDecodable { reason } => CaptureRosterUnobservable { reason: join(["the submission capture record ", trim(entry), " does not decode: ", reason], ""), @@ -6049,12 +6201,13 @@ fn belt_submission_capture_for_head(instance: HostDashboardInstance, worktree: S CaptureAbsent } else { let read = Filesystem.Read(path: join([dir, "/", submission_capture_basename(head_sha: head_sha)], "")) - if !read.success { - CaptureUnreadable { - reason: join(["the submission capture for this head was listed and then could not be read: ", read.error], ""), - } - } else { - match submission_capture_decode(text: read.content) { + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error } => + CaptureUnreadable { + reason: join(["the submission capture for this head was listed and then could not be read: ", error], ""), + } + FilesystemReadSucceeded { content } => + match submission_capture_decode(text: content) { CaptureNotDecodable { reason } => CaptureUnreadable { reason: join(["the submission capture at this head's own address does not decode: ", reason], ""), @@ -6274,15 +6427,16 @@ fn belt_scm_base_sha_observe(instance: HostDashboardInstance, node_id: RoadmapNo attempt_key: attempt_key, ) let read = Filesystem.Read(path: path) - if !read.success { - BaseShaUnobserved { - reason: join([ - "the attempt's spawn origin could not be read, so the intake base is unknown: ", - path, " (", read.error, ") — the project base must freeze the recorded dispatch base", - ], ""), - } - } else { - match parse_json_document(s: read.content) { + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error } => + BaseShaUnobserved { + reason: join([ + "the attempt's spawn origin could not be read, so the intake base is unknown: ", + path, " (", error, ") — the project base must freeze the recorded dispatch base", + ], ""), + } + FilesystemReadSucceeded { content } => + match parse_json_document(s: content) { JsonDocumentUnreadable { gap: _ } => BaseShaUnobserved { reason: join(["the spawn origin at ", path, " is not JSON"], "") } JsonDocumentParsed { value: doc } => @@ -6656,30 +6810,31 @@ fn belt_scm_delta_fold_step( } WorktreeBind { path } => { let read = Filesystem.Read(path: join([worktree, "/", path as String], "")) - if !read.success { - BeltDeltaFold { - store: acc.store, - unique: acc.unique, - refused: Present { - value: join([ - "the changed path ", path as String, - " could not be read from the worktree (", read.error, - "), so the tree cannot be captured whole", - ], ""), - }, - } - } else { - match belt_scm_delta_resident_for(path: path_of_envelope, store: acc.store, text: read.content) { - DeltaResidentRefused { detail } => - BeltDeltaFold { - store: acc.store, - unique: acc.unique, - refused: Present { - value: join(["the changed path ", path as String, " re-binds bytes the repository already holds, and they could not be read back: ", detail], ""), - }, - } - DeltaResident { store: resident } => - match store_authored_source(store: resident, text: read.content) { + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error } => + BeltDeltaFold { + store: acc.store, + unique: acc.unique, + refused: Present { + value: join([ + "the changed path ", path as String, + " could not be read from the worktree (", error, + "), so the tree cannot be captured whole", + ], ""), + }, + } + FilesystemReadSucceeded { content } => + match belt_scm_delta_resident_for(path: path_of_envelope, store: acc.store, text: content) { + DeltaResidentRefused { detail } => + BeltDeltaFold { + store: acc.store, + unique: acc.unique, + refused: Present { + value: join(["the changed path ", path as String, " re-binds bytes the repository already holds, and they could not be read back: ", detail], ""), + }, + } + DeltaResident { store: resident } => + match store_authored_source(store: resident, text: content) { SourceLocatorCollision { identity: _, existing: _, incoming: _ } => BeltDeltaFold { store: acc.store, @@ -10467,12 +10622,13 @@ fn belt_integration_receipt_observe( IntegrationReceiptAbsent } else { let read = Filesystem.Read(path: join([dir, "/", head_sha, ".json"], "")) - if !read.success { - IntegrationReceiptUnreadable { - reason: join(["the integration receipt for this head was listed and then could not be read: ", read.error], ""), - } - } else { - match integration_receipt_decode(text: read.content) { + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error } => + IntegrationReceiptUnreadable { + reason: join(["the integration receipt for this head was listed and then could not be read: ", error], ""), + } + FilesystemReadSucceeded { content } => + match integration_receipt_decode(text: content) { IntegrationReceiptNotDecodable { reason } => IntegrationReceiptUnreadable { reason: join(["the integration receipt at this head's own address does not decode: ", reason], ""), @@ -12188,17 +12344,25 @@ fn belt_publish_node_for_instance( node_id: node_id as gunbc.roadmap_model.RoadmapNodeId, ), ) - if !key_read.success || trim(key_read.content) == "" { - PublishDeferred { - node_id: node_id, - reason: "this node has no current attempt, so there is no branch or head for publication to be about", - } - } else { - belt_publish_attempt_for_instance( - instance: instance, - node_id: node_id, - attempt_key: trim(key_read.content), - ) + match filesystem_read_outcome(content: key_read.content, success: key_read.success, error: key_read.error) { + FilesystemReadRefused { error } => + PublishDeferred { + node_id: node_id, + reason: join(["the node's current-attempt-key pointer could not be read, so whether the node has a current attempt is unknown: ", error], ""), + } + FilesystemReadSucceeded { content } => + if trim(content) == "" { + PublishDeferred { + node_id: node_id, + reason: "this node has no current attempt, so there is no branch or head for publication to be about", + } + } else { + belt_publish_attempt_for_instance( + instance: instance, + node_id: node_id, + attempt_key: trim(content), + ) + } } } } diff --git a/dag/gunbc/roadmap/roadmap_closing_contract_authoring.dag b/dag/gunbc/roadmap/roadmap_closing_contract_authoring.dag index 11384eb4af9..1738444b8eb 100644 --- a/dag/gunbc/roadmap/roadmap_closing_contract_authoring.dag +++ b/dag/gunbc/roadmap/roadmap_closing_contract_authoring.dag @@ -7,7 +7,10 @@ import extdeps.shell import gunbc.output_policy { OutcomeIsData } import gunbc.roadmap_model { RoadmapNode, RoadmapNodeId } import gunbc.roadmap_authority { declared_roadmap_nodes } -import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.filesystem.filesystem_io { + Filesystem, + filesystem_read_outcome, +} import gunbc.roadmap_execution_contract { WorkItemExecutionContract, ValidationOperation, @@ -133,9 +136,7 @@ fn closing_contract_oracle_observe(oracle: ValidationOracleRef) -> OracleObserva let read = Filesystem.Read(path: oracle.entry as String) validation_oracle_observe_from_source( oracle: oracle, - source_readable: read.success, - source: read.content, - read_error: read.error, + source_read: filesystem_read_outcome(content: read.content, success: read.success, error: read.error), ) } diff --git a/dag/gunbc/roadmap/roadmap_event_carrier.dag b/dag/gunbc/roadmap/roadmap_event_carrier.dag index b3aec37e7f1..243837cf70e 100644 --- a/dag/gunbc/roadmap/roadmap_event_carrier.dag +++ b/dag/gunbc/roadmap/roadmap_event_carrier.dag @@ -1,7 +1,20 @@ module gunbc.roadmap.roadmap_event_carrier import std.types { String, Int, Bool, List, NonEmptyStr, FilePath, GitRef } -import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.filesystem.filesystem_io { + Filesystem, + FilesystemFileObservation, + FilesystemFileRead, + FilesystemFileAbsent, + FilesystemFileIndeterminate, + FilesystemFileObservationsDisagree, + FilesystemFileSubjectRefused, + FilesystemReadOutcome, + FilesystemReadRefused, + FilesystemReadSucceeded, + filesystem_read_outcome, +} +import gunbc.filesystem_file_observe { filesystem_file_observation_of_path } import std.algebra { trim } import std.content_hash { content_hash_of_value, content_hash_atom, content_hash_tagged_structural } import std.durable_compare_and_set { CasGeneration, CasReadableSlot, CasReadableAbsent, CasReadablePresent, CasSlotVersion, ExpectSlotAbsent, ExpectSlotGeneration, cas_expectation_admits } @@ -324,6 +337,30 @@ fn roadmap_event_same_command(a: RoadmapEvent, b: RoadmapEvent) -> Bool { a.node == b.node && a.author == b.author && a.parent == b.parent && a.kind == b.kind } +// The operation receipt is observed, not read-and-kind-matched: the old flow branched on the +// host error's error_kind string ("not_found"), which breaks on every host phrasing. The +// observation authority establishes absence from the listing, so a missing receipt is an +// established fact (proceed with the append), a listed-but-unreadable receipt is a refusal that +// says whether the operation already ran is UNKNOWN, and a readable receipt decodes. +fn roadmap_event_append_after_no_operation_receipt( + layout: RoadmapEventCarrierLayout, + event: RoadmapEvent, + env: RoadmapEventEnvelope, + operation: String, + receipt_path: String, + expected_commit: String, +) -> RoadmapEventAppend { + 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_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: expected_commit) } + } +} + fn roadmap_event_append_private(layout: RoadmapEventCarrierLayout, event: RoadmapEvent, operation: String) -> RoadmapEventAppend { let env = roadmap_event_envelope(e: event) match roadmap_event_carrier_sync(layout: layout) { @@ -332,27 +369,47 @@ fn roadmap_event_append_private(layout: RoadmapEventCarrierLayout, event: Roadma CarrierExecFailed { exit_code: _, stderr } => EventAppendRefused { id: env.id, step: "snapshot-revision", reason: stderr } CarrierExecOk { stdout: revision } => { let receipt_path = roadmap_event_operation_path(event: event, operation: operation) - let receipt = Filesystem.Read(path: concat(layout.worktree as String, concat("/", receipt_path))) - if operation != "" && receipt.success { - match roadmap_event_decode(text: receipt.content) { - RoadmapEventUndecodable { reason } => EventAppendRefused { id: env.id, step: "operation-read", reason: reason } - RoadmapEventDecoded { envelope: previous } => if roadmap_event_same_command(a: previous.event, b: event) { - let committed = Filesystem.Read(path: concat(layout.worktree as String, concat("/", roadmap_event_file_relative(node: previous.event.node, id: previous.id)))) - if committed.success && committed.content == receipt.content { EventAlreadyPresent { id: previous.id } } - else { EventAppendRefused { id: env.id, step: "operation-read", reason: "operation receipt has no matching committed event" } } - } else { EventAppendRefused { id: env.id, step: "operation-conflict", reason: "operation identity already belongs to another issue/principal/payload/precondition" } } + if operation == "" { + roadmap_event_append_after_no_operation_receipt( + layout: layout, + event: event, + env: env, + operation: operation, + receipt_path: receipt_path, + expected_commit: trim(revision), + ) + } else { + match filesystem_file_observation_of_path(path: concat(layout.worktree as String, concat("/", receipt_path))) { + FilesystemFileRead { path: _, content } => + match roadmap_event_decode(text: content) { + RoadmapEventUndecodable { reason } => EventAppendRefused { id: env.id, step: "operation-read", reason: reason } + RoadmapEventDecoded { envelope: previous } => if roadmap_event_same_command(a: previous.event, b: event) { + let committed = Filesystem.Read(path: concat(layout.worktree as String, concat("/", roadmap_event_file_relative(node: previous.event.node, id: previous.id)))) + match filesystem_read_outcome(content: committed.content, success: committed.success, error: committed.error) { + FilesystemReadRefused { error } => + EventAppendRefused { id: env.id, step: "operation-read", reason: join(["the committed event at this operation's address could not be read, so whether the operation already ran is unknown: ", error], "") } + FilesystemReadSucceeded { content: committed_text } => if committed_text == content { EventAlreadyPresent { id: previous.id } } + else { EventAppendRefused { id: env.id, step: "operation-read", reason: "operation receipt has no matching committed event" } } + } + } else { EventAppendRefused { id: env.id, step: "operation-conflict", reason: "operation identity already belongs to another issue/principal/payload/precondition" } } + } + FilesystemFileAbsent(_) => + roadmap_event_append_after_no_operation_receipt( + layout: layout, + event: event, + env: env, + operation: operation, + receipt_path: receipt_path, + expected_commit: trim(revision), + ) + FilesystemFileIndeterminate { cause } => + EventAppendRefused { id: env.id, step: "operation-read", reason: join(["the operation receipt could not be observed, so whether the operation already ran is unknown: ", cause], "") } + FilesystemFileObservationsDisagree { path: _, cause } => + EventAppendRefused { id: env.id, step: "operation-read", reason: join(["the operation receipt observation is self-contradictory, so whether the operation already ran is unknown: ", cause], "") } + FilesystemFileSubjectRefused { directory: _, name: _, cause } => + EventAppendRefused { id: env.id, step: "operation-read", reason: join(["the operation receipt path was refused before any filesystem call ran: ", cause], "") } } - } else if operation != "" && receipt.error_kind != "not_found" { - EventAppendRefused { id: env.id, step: "operation-read", reason: receipt.error } - } 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_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)) } - } } + } } } } @@ -422,7 +479,10 @@ type EventFileRead fn roadmap_events_decode_all(node: RoadmapNodeId, dir: String, names: List) -> RoadmapEventsRead { let reads = map(filter(names, n => ends_with(s: n, suffix: ".json")), n => { let r = Filesystem.Read(path: join([dir, "/", n], "")) - if r.success { EventFileContent { content: r.content } } else { EventFileUnreadable { detail: r.error } } + match filesystem_read_outcome(content: r.content, success: r.success, error: r.error) { + FilesystemReadSucceeded { content } => EventFileContent { content: content } + FilesystemReadRefused { error } => EventFileUnreadable { detail: error } + } }) roadmap_events_decode_reads(node: node, reads: reads) } diff --git a/dag/gunbc/roadmap/roadmap_publication_helper.dag b/dag/gunbc/roadmap/roadmap_publication_helper.dag index 8c5b0a64e39..7d41892f12e 100644 --- a/dag/gunbc/roadmap/roadmap_publication_helper.dag +++ b/dag/gunbc/roadmap/roadmap_publication_helper.dag @@ -7,7 +7,14 @@ import std.content_hash { content_hash_atom } import std.process { ProcessExit, ExitSuccess, exit_failure } import gunbc.output_policy { OutcomeIsData } import extdeps.shell -import extdeps.filesystem.filesystem_io { Filesystem, filesystem_listing_names_entry } +import extdeps.filesystem.filesystem_io { + Filesystem, + FilesystemReadOutcome, + FilesystemReadRefused, + FilesystemReadSucceeded, + filesystem_listing_names_entry, + filesystem_read_outcome, +} import extdeps.languages.json.emit { JsonValue, json_string, @@ -259,15 +266,16 @@ type PublicationAnswerRead fn publication_answer_read_at(path: String, expected: RoadmapPublicationSubject) -> PublicationAnswerRead { let read = Filesystem.Read(path: path) - if !read.success { - AnswerUnreadable { - reason: join([ - "a publication answer for this head was listed and then could not be read, so it exists and its content is unavailable: ", - path, " (", read.error, ")", - ], ""), - } - } else { - match publication_receipt_decode(receipt: read.content) { + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error } => + AnswerUnreadable { + reason: join([ + "a publication answer for this head was listed and then could not be read, so it exists and its content is unavailable: ", + path, " (", error, ")", + ], ""), + } + FilesystemReadSucceeded { content } => + match publication_receipt_decode(receipt: content) { ReceiptUndecodable { reason: r } => AnswerUnreadable { reason: join([ @@ -904,21 +912,22 @@ fn publication_helper_entry_outcome( publication_request_dir(instance_root: instance_root), "/", entry, ], "") let read = Filesystem.Read(path: path) - if !read.success { - HelperRequestUnreadable { - entry: entry, - detail: join([ - "a publication request was listed and then could not be read: ", - path, " (", read.error, ")", - ], ""), - } - } else { - publication_helper_answer_request( - instance_root: instance_root, - credential: credential, - entry: entry, - request_text: read.content, - ) + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error } => + HelperRequestUnreadable { + entry: entry, + detail: join([ + "a publication request was listed and then could not be read: ", + path, " (", error, ")", + ], ""), + } + FilesystemReadSucceeded { content } => + publication_helper_answer_request( + instance_root: instance_root, + credential: credential, + entry: entry, + request_text: content, + ) } } } diff --git a/dag/gunbc/roadmap/roadmap_served_observation.dag b/dag/gunbc/roadmap/roadmap_served_observation.dag index a260c0401af..2730c528f18 100644 --- a/dag/gunbc/roadmap/roadmap_served_observation.dag +++ b/dag/gunbc/roadmap/roadmap_served_observation.dag @@ -6,7 +6,17 @@ import gunbc.site.markup { page_render_refusal_body } import gunbc.roadmap.roadmap_event_log { RoadmapStandings } import gunbc.roadmap.roadmap_event_carrier { roadmap_event_carrier_layout_for_instance, roadmap_standings_read, CarrierClosed, SnapshotNotOpened, SnapshotClosed, SnapshotCloseFailed } import std.types { String, Int, Bool, List, NonEmptyStr } -import extdeps.filesystem.filesystem_io { Filesystem } +import extdeps.filesystem.filesystem_io { + Filesystem, + FilesystemFileObservation, + FilesystemFileRead, + FilesystemFileAbsent, + FilesystemFileIndeterminate, + FilesystemFileObservationsDisagree, + FilesystemFileSubjectRefused, + filesystem_established_absence_detail, +} +import gunbc.filesystem_file_observe { filesystem_file_observation_of_path } import std.algebra { trim } import std.content_hash { content_hash_tagged_structural, content_hash_atom } import extdeps.http.server { MediaType, text_html_utf8, text_plain_utf8, application_json_utf8, ServeHttpResponse } @@ -501,19 +511,29 @@ fn served_observation_decode(raw: String) -> ServedObservationRead { } } -fn served_observation_read_from_filesystem(success: Bool, content: String, error: String) -> ServedObservationRead { - if success { - if trim(content) == "" { ServedObservationAbsent } else { served_observation_decode(raw: content) } - } else if string_contains(s: error, pattern: "No such file") || string_contains(s: error, pattern: "not found") { - ServedObservationAbsent - } else { - ServedObservationUnreadable { reason: join(["served observation index could not be read: ", error], "") } +// The served-observation index is classified from an OBSERVATION, not by text-matching the host's +// error string: absence is established from the listing by the observation authority, and the host +// error rides inside the cause text instead of being pattern-matched. +fn served_observation_read_from_observation(observation: FilesystemFileObservation) -> ServedObservationRead { + match observation { + FilesystemFileAbsent(_) => ServedObservationAbsent + FilesystemFileRead { path: _, content } => + if trim(content) == "" { ServedObservationAbsent } else { served_observation_decode(raw: content) } + FilesystemFileIndeterminate { cause } => + ServedObservationUnreadable { reason: join(["served observation index could not be read: ", cause], "") } + FilesystemFileObservationsDisagree { path: _, cause } => + ServedObservationUnreadable { reason: join(["served observation index could not be read: ", cause], "") } + FilesystemFileSubjectRefused { directory: _, name: _, cause } => + ServedObservationUnreadable { reason: join(["served observation index could not be read: ", cause], "") } } } fn served_observation_read_for_instance(instance: HostDashboardInstance) -> ServedObservationRead { - let read = Filesystem.Read(path: served_observation_path_for_instance(instance: instance)) - served_observation_read_from_filesystem(success: read.success, content: read.content, error: read.error) + served_observation_read_from_observation( + observation: filesystem_file_observation_of_path( + path: served_observation_path_for_instance(instance: instance), + ), + ) } // Bodies first, index last: a reader that races the writer sees either the previous complete @@ -585,24 +605,33 @@ fn served_observation_entry_lookup(index: ServedObservationIndex, kind: ServedOb type ServedObservationBodyRead = ServedObservationBodyPresent { body: String } | ServedObservationBodyMissing { reason: String } + | ServedObservationBodyUnreadable { cause: String } | ServedObservationBodyMismatch { expected: String, actual: String } -fn served_observation_body_read_from_filesystem(entry: ServedObservationEntry, success: Bool, content: String, error: String) -> ServedObservationBodyRead { - if !success { - ServedObservationBodyMissing { reason: error } - } else { - let actual = served_observation_body_digest(body: content) - if actual == entry.digest { - ServedObservationBodyPresent { body: content } - } else { - ServedObservationBodyMismatch { expected: entry.digest, actual: actual } +fn served_observation_body_read_from_observation(entry: ServedObservationEntry, observation: FilesystemFileObservation) -> ServedObservationBodyRead { + match observation { + FilesystemFileAbsent(absence) => ServedObservationBodyMissing { reason: filesystem_established_absence_detail(absence: absence) } + FilesystemFileRead { path: _, content } => { + let actual = served_observation_body_digest(body: content) + if actual == entry.digest { + ServedObservationBodyPresent { body: content } + } else { + ServedObservationBodyMismatch { expected: entry.digest, actual: actual } + } } + FilesystemFileIndeterminate { cause } => ServedObservationBodyUnreadable { cause: cause } + FilesystemFileObservationsDisagree { path: _, cause } => ServedObservationBodyUnreadable { cause: cause } + FilesystemFileSubjectRefused { directory: _, name: _, cause } => ServedObservationBodyUnreadable { cause: cause } } } fn served_observation_body_read_for_instance(instance: HostDashboardInstance, entry: ServedObservationEntry) -> ServedObservationBodyRead { - let read = Filesystem.Read(path: served_observation_body_path_for_instance(instance: instance, kind: entry.kind)) - served_observation_body_read_from_filesystem(entry: entry, success: read.success, content: read.content, error: read.error) + served_observation_body_read_from_observation( + entry: entry, + observation: filesystem_file_observation_of_path( + path: served_observation_body_path_for_instance(instance: instance, kind: entry.kind), + ), + ) } fn served_observation_refusal_body(refusal: String, detail: String) -> String { @@ -629,6 +658,11 @@ fn served_observation_body_response(entry: ServedObservationEntry, body: ServedO refusal: "ServedObservationBodyMissing", detail: join(["the ", served_observation_kind_key(kind: entry.kind), " body named by the index could not be read: ", reason], ""), ) + ServedObservationBodyUnreadable { cause } => + served_observation_refusal_response( + refusal: "ServedObservationBodyUnreadable", + detail: join(["the ", served_observation_kind_key(kind: entry.kind), " body named by the index could not be observed: ", cause], ""), + ) ServedObservationBodyMismatch { expected, actual } => served_observation_refusal_response( refusal: "ServedObservationBodyMismatch", diff --git a/dag/gunbc/roadmap/roadmap_validation_oracle.dag b/dag/gunbc/roadmap/roadmap_validation_oracle.dag index fbbd6c42d19..f61ef26658a 100644 --- a/dag/gunbc/roadmap/roadmap_validation_oracle.dag +++ b/dag/gunbc/roadmap/roadmap_validation_oracle.dag @@ -11,8 +11,12 @@ import std.content_hash { fnv1a64_structural_hex_digest, serialize_content_hash, } -import extdeps.pin { - Pin, +import extdeps.filesystem.filesystem_io { + FilesystemReadOutcome, + FilesystemReadRefused, + FilesystemReadSucceeded, +} +import extdeps.pin { Pin, ExactPin, PinAdmission, PinIntegrityAdmitted, @@ -250,51 +254,51 @@ fn validation_oracle_declaration_count( fn validation_oracle_observe_from_source( oracle: ValidationOracleRef, - source_readable: Bool, - source: String, - read_error: String, + source_read: FilesystemReadOutcome, ) -> OracleObservation { - if !source_readable { - OracleMissing { - oracle: oracle, - detail: join([ - "validation oracle source could not be obtained at ", - oracle.entry as String, - ": ", - read_error, - ], ""), - } - } else { - let declarations = validation_oracle_declaration_count( - source: source, - function: oracle.function, - ) - if declarations == 0 { + match source_read { + FilesystemReadRefused { error } => OracleMissing { oracle: oracle, detail: join([ - "validation oracle source at ", + "validation oracle source could not be obtained at ", oracle.entry as String, - " declares no ", - oracle.function as String, + ": ", + error, ], ""), } - } else if declarations > 1 { - OracleAmbiguous { - oracle: oracle, - declaration_count: declarations, - } - } else { - let unpinned = validation_oracle_unpinned_dependencies(source: source) - if count(unpinned) > 0 { - OracleDependsOnUnpinnedOracle { + FilesystemReadSucceeded { content: source } => { + let declarations = validation_oracle_declaration_count( + source: source, + function: oracle.function, + ) + if declarations == 0 { + OracleMissing { oracle: oracle, - imports: unpinned, + detail: join([ + "validation oracle source at ", + oracle.entry as String, + " declares no ", + oracle.function as String, + ], ""), } - } else { - OracleFound { + } else if declarations > 1 { + OracleAmbiguous { oracle: oracle, - identity: validation_oracle_source_identity(source: source), + declaration_count: declarations, + } + } else { + let unpinned = validation_oracle_unpinned_dependencies(source: source) + if count(unpinned) > 0 { + OracleDependsOnUnpinnedOracle { + oracle: oracle, + imports: unpinned, + } + } else { + OracleFound { + oracle: oracle, + identity: validation_oracle_source_identity(source: source), + } } } } diff --git a/dag/gunbc/roadmap/roadmap_workflow_command.dag b/dag/gunbc/roadmap/roadmap_workflow_command.dag index 6cb8350362f..b57174ff775 100644 --- a/dag/gunbc/roadmap/roadmap_workflow_command.dag +++ b/dag/gunbc/roadmap/roadmap_workflow_command.dag @@ -1,11 +1,19 @@ module gunbc.roadmap_workflow_command -import std.types { String, Bool, Int, List } +import std.types { String, Bool, Int, List, NonEmptyStr } import std.content_hash { ContentHash } import gunbc.roadmap_verification_receipt { VerificationSubject } type AttemptKey = String +// The attempt's modeled-state pointer standing, as an established fact of the read that observed it: +// Observed carries the evidence; Absent is established absence; Unreadable is a pointer that could +// not be read, with the owner's located cause -- never treated as absence. +type WorkflowModeledStateStanding + = WorkflowModeledStateObserved + | WorkflowModeledStateAbsent + | WorkflowModeledStateUnreadable { cause: NonEmptyStr } + // THE THREE RETRY SHAPES ARE FIRST-CLASS COMMANDS, NOT INCIDENTAL TMUX CLEANUP. RetryMechanism // retries a failed mechanism at the same input identity without a new attempt or a consumed review // round. ResumeAttempt continues the same branch and worktree with a new worker turn and new head: @@ -54,8 +62,14 @@ type WorkflowCommandAdmission = WorkflowCommandAdmitted | WorkflowCommandRefused { reason: String } -fn is_legacy_attempt(modeled_state_present: Bool) -> Bool { - !modeled_state_present +// Legacy standing comes only from an ESTABLISHED absence of the modeled-state pointer. An +// unreadable pointer has unknown standing and is never legacy here. +fn is_legacy_attempt(modeled_state: WorkflowModeledStateStanding) -> Bool { + match modeled_state { + WorkflowModeledStateAbsent => true + WorkflowModeledStateObserved => false + WorkflowModeledStateUnreadable { cause: _ } => false + } } fn admit_start_new_attempt( @@ -105,8 +119,8 @@ fn admit_retry_mechanism(mints_attempt: Bool) -> WorkflowCommandAdmission { } } -fn legacy_attempt_cannot_satisfy_obligations(modeled_state_present: Bool) -> Bool { - is_legacy_attempt(modeled_state_present: modeled_state_present) +fn legacy_attempt_cannot_satisfy_obligations(modeled_state: WorkflowModeledStateStanding) -> Bool { + is_legacy_attempt(modeled_state: modeled_state) } fn attempt_key_short_form(key: AttemptKey) -> String { diff --git a/dag/gunbc/roadmap/roadmap_workflow_progress.dag b/dag/gunbc/roadmap/roadmap_workflow_progress.dag index 5bdb8f03609..666df0f50ce 100644 --- a/dag/gunbc/roadmap/roadmap_workflow_progress.dag +++ b/dag/gunbc/roadmap/roadmap_workflow_progress.dag @@ -185,6 +185,7 @@ import gunbc.roadmap.roadmap_fanin { import gunbc.roadmap_workflow_command { legacy_attempt_supersedable_label, legacy_attempt_supersedable_detail, + WorkflowModeledStateStanding, WorkflowModeledStateObserved, WorkflowModeledStateAbsent, WorkflowModeledStateUnreadable, } // THE ATTEMPT'S WORKTREE, OBSERVED RATHER THAN INFERRED. The Workspace segment read complete from the @@ -207,11 +208,14 @@ fn attempt_worktree_observation_of(path: String, marker: FilesystemFileObservati } } +// The modeled-state pointer's standing, carried as a typed fact: an attempt whose pointer is +// established absent is a legacy attempt; an attempt whose pointer could not be read has +// UNKNOWN standing, and no downstream route may mint legacy standing from that unknown. type WorkflowAttemptEvidence { node_id: String attempt_key: String branch: String - modeled_state_present: Bool + modeled_state: WorkflowModeledStateStanding worktree: AttemptWorktreeObservation admission_receipt_readable: Bool admission_receipt: String @@ -392,12 +396,25 @@ fn provider_execution_reconcile_process( fn workflow_provider_from_evidence( evidence: WorkflowAttemptEvidence, ) -> ProviderExecutionState { - if !evidence.modeled_state_present { - ProviderEventsRefused { - activity: legacy_attempt_supersedable_label, - detail: legacy_attempt_supersedable_detail, - } - } else if !evidence.admission_receipt_readable { + match evidence.modeled_state { + WorkflowModeledStateUnreadable { cause } => + ProviderEventsRefused { + activity: "attempt standing unknown", + detail: join(["this attempt's current-attempt-key pointer could not be read, so whether it predates modeled admission is unknown and supersession is refused: ", cause], ""), + } + WorkflowModeledStateAbsent => + ProviderEventsRefused { + activity: legacy_attempt_supersedable_label, + detail: legacy_attempt_supersedable_detail, + } + WorkflowModeledStateObserved => workflow_provider_from_observed(evidence: evidence) + } +} + +fn workflow_provider_from_observed( + evidence: WorkflowAttemptEvidence, +) -> ProviderExecutionState { + if !evidence.admission_receipt_readable { ProviderEventsRefused { activity: "attempt state refused", detail: "current attempt pointer exists but its admission receipt is unreadable", @@ -1068,6 +1085,26 @@ fn workflow_segment_from_evidence( evidence: WorkflowAttemptEvidence, provider: ProviderExecutionState, admitted: Bool, +) -> WorkflowSegment { + match evidence.modeled_state { + WorkflowModeledStateUnreadable { cause } => + WorkflowSegment { + kind: kind, + state: WorkflowSegmentRefused, + detail: join(["this attempt's standing is unknown because its current-attempt-key pointer could not be read: ", cause], ""), + } + WorkflowModeledStateObserved => + workflow_segment_from_standing_evidence(kind: kind, evidence: evidence, provider: provider, admitted: admitted) + WorkflowModeledStateAbsent => + workflow_segment_from_standing_evidence(kind: kind, evidence: evidence, provider: provider, admitted: admitted) + } +} + +fn workflow_segment_from_standing_evidence( + kind: WorkflowSegmentKind, + evidence: WorkflowAttemptEvidence, + provider: ProviderExecutionState, + admitted: Bool, ) -> WorkflowSegment { match kind { EnvironmentWorkflowSegment => @@ -1087,9 +1124,12 @@ fn workflow_segment_from_evidence( }, } WorkspaceWorkflowSegment => - if !evidence.modeled_state_present { - WorkflowSegment { kind: kind, state: WorkflowSegmentRefused, detail: "legacy branch has no modeled workspace publication receipt" } - } else { + match evidence.modeled_state { + WorkflowModeledStateUnreadable { cause } => + WorkflowSegment { kind: kind, state: WorkflowSegmentRefused, detail: join(["the attempt's standing is unknown because its current-attempt-key pointer could not be read: ", cause], "") } + WorkflowModeledStateAbsent => + WorkflowSegment { kind: kind, state: WorkflowSegmentRefused, detail: "legacy branch has no modeled workspace publication receipt" } + WorkflowModeledStateObserved => match evidence.worktree { AttemptWorktreePresent { path: _ } => WorkflowSegment { kind: kind, state: WorkflowSegmentComplete, detail: "attempt branch and worktree preceded state publication, and the worktree is present" } @@ -1326,7 +1366,7 @@ fn workflow_attempt_progress( evidence: WorkflowAttemptEvidence, ) -> WorkflowAttemptProgress { let provider = workflow_provider_from_evidence(evidence: evidence) - let admitted = evidence.modeled_state_present + let admitted = evidence.modeled_state == WorkflowModeledStateObserved && evidence.admission_receipt_readable && receipt_records_admission(receipt: evidence.admission_receipt) let segments = map( 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 06547e45e68..8bf608373b5 100644 --- a/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag @@ -2,6 +2,7 @@ module test.claim.roadmap_belt_actuate_witness import gunbc.roadmap_session_placement { SessionPlacementAbsent } import gunbc.roadmap_nesting { roadmap_memberships } +import extdeps.filesystem.filesystem_io { filesystem_read_outcome, filesystem_file_observation, filesystem_listing_observation, FilesystemReadRefused, FilesystemFileObservation } import gunbc.roadmap_belt_actuate { belt_spawn_outcome_with_release } import gunbc.roadmap.roadmap_submission { CaptureAbsent } @@ -50,6 +51,9 @@ import gunbc.roadmap_belt_actuate { belt_candidate_subject_reading, CandidateSubjectSettled, CandidateSubjectBound, VerificationPassedConfirmed, VerificationNotPassed, VerificationUnreadable, belt_publish_pass_outcome_from, + WorkflowModeledStatePointerState, WorkflowModeledStatePointerAbsent, WorkflowModeledStatePointerUnreadable, + selection_standing_for_pointer, verification_source_standing_for_pointer, + publication_source_standing_for_pointer, review_report_standing_for_pointer, } import gunbc.roadmap.roadmap_submission { SubmissionReadiness, SubmissionReady, SubmissionNeedsContext, @@ -127,7 +131,7 @@ import gunbc.roadmap_belt_actuate { belt_workflow_attempt_presentation_json_with_tick_read, belt_verification_presentation_detail, belt_tick_receipt_decode, - belt_tick_receipt_read_from_filesystem, + belt_tick_receipt_read_from_observation, belt_tick_receipt_wire_json_from_read, BeltTickReceiptRead, BeltTickReceiptAbsent, @@ -198,6 +202,7 @@ import gunbc.roadmap.roadmap_submission { } import gunbc.roadmap_workflow_stage { WorkflowSegment, VerificationWorkflowSegment, WorkflowSegmentFailed, + WorkflowSegmentRefused, } import gunbc.roadmap_authority { declared_roadmap_nodes, declared_roadmap_edges } import gunbc.worker_lifecycle { WorkerProcessUnobserved } @@ -1001,8 +1006,7 @@ fn preflight_with_config(config: String) -> BeltClaudePreflight { claude_on_path: true, home: "/home/u", trust_store_path: "/home/u/.claude.json", - trust_store_readable: true, - trust_store_text: config, + trust_store_read: filesystem_read_outcome(content: config, success: true, error: "unused on success"), trust_root: "/opt/x/worktrees", ) } @@ -1012,8 +1016,7 @@ test fn witness_preflight_refuses_missing_claude() -> Bool { claude_on_path: false, home: "/home/u", trust_store_path: "", - trust_store_readable: false, - trust_store_text: "", + trust_store_read: filesystem_read_outcome(content: "", success: false, error: "permission denied"), trust_root: "/opt/x/worktrees", )) == "claude-cli" } @@ -1023,8 +1026,7 @@ test fn witness_preflight_refuses_unknown_home() -> Bool { claude_on_path: true, home: "", trust_store_path: "", - trust_store_readable: false, - trust_store_text: "", + trust_store_read: filesystem_read_outcome(content: "", success: false, error: "permission denied"), trust_root: "/opt/x/worktrees", )) == "claude-trust-home" } @@ -1034,8 +1036,7 @@ test fn witness_preflight_refuses_unreadable_store() -> Bool { claude_on_path: true, home: "/home/u", trust_store_path: "/home/u/.claude.json", - trust_store_readable: false, - trust_store_text: "", + trust_store_read: filesystem_read_outcome(content: "", success: false, error: "permission denied"), trust_root: "/opt/x/worktrees", )) == "claude-trust-store" } @@ -1223,7 +1224,7 @@ fn completed_attempt_progress(node_id: String) -> WorkflowAttemptProgress { node_id: node_id, attempt_key: "feedfacefeedface", branch: join(["dispatch/", node_id, "-afeedfacefeedface"], ""), - modeled_state_present: true, + modeled_state: WorkflowModeledStateObserved, worktree: AttemptWorktreePresent { path: "/wt/fixture" }, admission_receipt_readable: true, admission_receipt: "{\"schema\": \"roadmap-dispatch-environment/v1\", \"status\": \"admitted\"}", @@ -1315,7 +1316,7 @@ test fn agent_incomplete_attempt_defers_verification() -> Bool { node_id: "belt-verify-running-node", attempt_key: "feedfacefeedface", branch: "dispatch/belt-verify-running-node-afeedfacefeedface", - modeled_state_present: true, + modeled_state: WorkflowModeledStateObserved, worktree: AttemptWorktreePresent { path: "/wt/fixture" }, admission_receipt_readable: true, admission_receipt: "{\"schema\": \"roadmap-dispatch-environment/v1\", \"status\": \"admitted\"}", @@ -1581,24 +1582,26 @@ test fn malformed_pass_reason_makes_receipt_unreadable() -> Bool { } } +// The absent fact is established through the owner's producers: a succeeded listing that omits +// the receipt plus a refused read yield FilesystemFileAbsent inside filesystem_io (the absence +// carrier is a sole_constructor and is never forged here). test fn belt_tick_receipt_missing_file_read_is_absent() -> Bool { - match belt_tick_receipt_read_from_filesystem( - success: false, - content: "", - error: "No such file or directory (os error 2)", - ) { + let listing = filesystem_listing_observation(directory: "/var/lib/gunbc/belt", success: true, entries: "other-state.json", error: "unused on success") + let observed = filesystem_file_observation(listing: listing, name: "belt-tick.json", path: "/var/lib/gunbc/belt/belt-tick.json", read: FilesystemReadRefused { error: "No such file or directory (os error 2)" }) + match belt_tick_receipt_read_from_observation(observation: observed) { BeltTickReceiptAbsent => true BeltTickReceiptPresent { receipt: _ } => false BeltTickReceiptUnreadable { reason: _ } => false } } +// Listed but refused: the listing NAMES the receipt, so absence is not established, and the read +// refusal makes the fact indeterminate -- present, content unavailable. The host error rides in +// the cause text, so the old "Permission denied" witness assertion still discriminates. test fn belt_tick_receipt_io_failure_read_is_unreadable() -> Bool { - match belt_tick_receipt_read_from_filesystem( - success: false, - content: "", - error: "Permission denied (os error 13)", - ) { + let listing = filesystem_listing_observation(directory: "/var/lib/gunbc/belt", success: true, entries: "belt-tick.json\nother-state.json", error: "unused on success") + let observed = filesystem_file_observation(listing: listing, name: "belt-tick.json", path: "/var/lib/gunbc/belt/belt-tick.json", read: FilesystemReadRefused { error: "Permission denied (os error 13)" }) + match belt_tick_receipt_read_from_observation(observation: observed) { BeltTickReceiptUnreadable { reason: r } => string_contains(s: r, pattern: "Permission denied") BeltTickReceiptPresent { receipt: _ } => false @@ -3330,6 +3333,39 @@ test fn a_failed_release_after_placement_is_carried_on_every_non_spawned_arm() - } } +// The pointer standing decides the subordinate facts' standing (side chat review 5409834403, fix +// (a)): an UNREADABLE pointer carries the non-absence variants -- the wrong state is unwritable -- +// while established ABSENCE keeps the absence-shaped facts the legacy arm means. +test fn an_unreadable_pointer_carries_non_absence_subordinate_facts() -> Bool { + let denied = WorkflowModeledStatePointerUnreadable { cause: "Permission denied (os error 13)" } + let absent = WorkflowModeledStatePointerAbsent + let sel_bad = selection_standing_for_pointer(pointer_state: denied) + let src_bad = verification_source_standing_for_pointer(pointer_state: denied) + let pub_bad = publication_source_standing_for_pointer(pointer_state: denied) + let rev_bad = review_report_standing_for_pointer(pointer_state: denied) + match sel_bad { + SelectionUnaddressable { cause } => + string_contains(s: cause, pattern: "pointer could not be read") + && match verification_source_standing_for_pointer(pointer_state: denied) { + VerificationReceiptUnreadable { detail } => + string_contains(s: detail, pattern: "pointer could not be read") + && match publication_source_standing_for_pointer(pointer_state: denied) { + ReceiptSourceUnreadable { detail: pd } => + string_contains(s: pd, pattern: "pointer could not be read") + && rev_bad.reconciliation.state == WorkflowSegmentRefused + && string_contains(s: rev_bad.reconciliation.detail, pattern: "pointer could not be read") + && match selection_standing_for_pointer(pointer_state: absent) { + SelectionAbsent => true + _ => false + } + _ => false + } + _ => false + } + _ => false + } +} + // A REFUSED METERING WRITE FAILS THE TICK'S METERING PASS, named by its cause; an attempt not yet due // and a persisted record do not. test fn a_refused_metering_write_fails_the_metering_pass() -> Bool { diff --git a/dag/test/claim/roadmap/roadmap_event_carrier_witness_test.dag b/dag/test/claim/roadmap/roadmap_event_carrier_witness_test.dag index 0da4cceb789..12108bb1b33 100644 --- a/dag/test/claim/roadmap/roadmap_event_carrier_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_event_carrier_witness_test.dag @@ -1,6 +1,7 @@ module test.claim.roadmap.roadmap_event_carrier_witness_test import std.types { Bool, String, FilePath, NonEmptyStr } +import extdeps.filesystem.filesystem_io { filesystem_file_observation, filesystem_listing_observation, FilesystemFileIndeterminate, FilesystemReadRefused, FilesystemFileObservation } import gunbc.roadmap_model { RoadmapNodeId } import gunbc.principal_projection { PrincipalRef } import gunbc.roadmap.roadmap_event_log { RoadmapEvent, RoadmapEventEnvelope, Claimed, roadmap_event_envelope, RoadmapEventId, EventPrincipalAuthenticated } @@ -75,6 +76,24 @@ test fn the_all_events_read_concatenates_and_the_first_refusal_refuses_the_whole && (match empty { AllEventsRead { envelopes } => count(envelopes) == 0 AllEventsReadRefused { step: _, reason: _ } => false }) } +// RED control at the receipt-flow site: the flow's unreadable arm consumes this observation, and +// the listing-refused world must arrive with the owner's "could not be listed" cause -- existence +// unknown -- never a claim that the receipt is listed. (The append flow itself is wet -- it runs +// the carrier program -- so the dry control pins the observation boundary it consumes.) +test fn a_refused_listing_at_the_receipt_site_arrives_with_existence_unknown_never_listed() -> Bool { + match filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/ops", success: false, entries: "", error: "Permission denied (os error 13)"), + name: "receipt.json", + path: "/srv/gunbc/ops/receipt.json", + read: FilesystemReadRefused { error: "not reached" }, + ) { + FilesystemFileIndeterminate { cause } => + string_contains(s: cause, pattern: "could not be listed") + && !string_contains(s: cause, pattern: "is listed") + _ => false + } +} + fn progress_on(node: RoadmapNodeId, parent: RoadmapEventId?, note: String, at: String) -> RoadmapEvent { let principal = EventPrincipalAuthenticated { ref: PrincipalRef { authority: "google-oidc" as NonEmptyStr, namespace: "accounts" as NonEmptyStr, subject: "s1" as NonEmptyStr } } RoadmapEvent { node: node, author: principal, parent: parent, kind: Progress { note: note as NonEmptyStr }, recorded_at: at } diff --git a/dag/test/claim/roadmap/roadmap_result_returned_witness_test.dag b/dag/test/claim/roadmap/roadmap_result_returned_witness_test.dag index bf98f5ba28a..8887c9fda98 100644 --- a/dag/test/claim/roadmap/roadmap_result_returned_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_result_returned_witness_test.dag @@ -27,12 +27,13 @@ import gunbc.roadmap.roadmap_event_carrier { } import extdeps.filesystem.filesystem_io { FilesystemCreateNew, FilesystemCreated, FilesystemCreateTargetOccupied, FilesystemCreateRefused, FilesystemPermissionDenied, + FilesystemReadSucceeded, FilesystemReadRefused, filesystem_file_observation, filesystem_listing_observation, } 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, + attempt_request_binding_read_from_observation, AttemptRequestBindingRead, RequestBindingRead, RequestBindingAbsent, RequestBindingUnreadable, } import gunbc.roadmap.roadmap_attempt_request_binding { @@ -318,9 +319,35 @@ test fn the_binding_record_round_trips_and_a_malformed_one_is_unreadable() -> Bo 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 }) + let absent_observed = filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/requests", success: true, entries: "other-attempt.json", error: "unused on success"), + name: "request.json", + path: "/srv/gunbc/requests/request.json", + read: FilesystemReadRefused { error: "No such file or directory (os error 2)" }, + ) + let refused_observed = filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/requests", success: true, entries: "request.json", error: "unused on success"), + name: "request.json", + path: "/srv/gunbc/requests/request.json", + read: FilesystemReadRefused { error: "Permission denied (os error 13)" }, + ) + let undecodable_observed = filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/requests", success: true, entries: "request.json", error: "unused on success"), + name: "request.json", + path: "/srv/gunbc/requests/request.json", + read: FilesystemReadSucceeded { content: "not json" }, + ) + let file_arms = attempt_request_binding_read_from_observation(observation: absent_observed) == RequestBindingAbsent + && (match attempt_request_binding_read_from_observation(observation: refused_observed) { + RequestBindingUnreadable { reason } => string_contains(s: reason, pattern: "Permission denied") + RequestBindingRead { binding: _ } => false + RequestBindingAbsent => false + }) + && (match attempt_request_binding_read_from_observation(observation: undecodable_observed) { + RequestBindingUnreadable { reason: _ } => true + RequestBindingRead { binding: _ } => false + RequestBindingAbsent => false + }) codec && strict && file_arms } @@ -591,7 +618,14 @@ test fn a_bound_attempts_result_reaches_its_requester_through_the_real_codecs() 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)) + let recorded = attempt_request_binding_read_from_observation( + observation: filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/requests", success: true, entries: "request.json", error: "unused on success"), + name: "request.json", + path: "/srv/gunbc/requests/request.json", + read: FilesystemReadSucceeded { 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 } => diff --git a/dag/test/claim/roadmap/roadmap_served_observation_witness_test.dag b/dag/test/claim/roadmap/roadmap_served_observation_witness_test.dag index d51c085ede6..c969773388b 100644 --- a/dag/test/claim/roadmap/roadmap_served_observation_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_served_observation_witness_test.dag @@ -1,5 +1,7 @@ module test.claim.roadmap_served_observation_witness +import extdeps.filesystem.filesystem_io { filesystem_file_observation, filesystem_listing_observation, FilesystemReadRefused, FilesystemReadSucceeded, FilesystemFileObservation } + import std.types { String, Int, Bool, List, FilePath } import extdeps.http.server { text_html_utf8, text_plain_utf8, application_json_utf8 } import gunbc.host_layout { instance_receipts_dir } @@ -16,7 +18,7 @@ import gunbc.roadmap_served_observation { ServedObservationEntry, ServedObservationIndex, ServedObservationRead, ServedObservationPresent, ServedObservationAbsent, ServedObservationUnreadable, - ServedObservationBodyRead, ServedObservationBodyPresent, ServedObservationBodyMissing, ServedObservationBodyMismatch, + ServedObservationBodyRead, ServedObservationBodyPresent, ServedObservationBodyMissing, ServedObservationBodyUnreadable, ServedObservationBodyMismatch, ServedObservationEntryLookup, ServedObservationEntryFound, ServedObservationEntryMissing, served_observation_schema, served_observation_path_for_instance, @@ -24,10 +26,10 @@ import gunbc.roadmap_served_observation { served_observation_index_of_snapshot, served_observation_index_wire, served_observation_decode, - served_observation_read_from_filesystem, + served_observation_read_from_observation, served_observation_identity_hex, served_observation_body_digest, - served_observation_body_read_from_filesystem, + served_observation_body_read_from_observation, served_observation_body_response, served_observation_index_refusal, served_observation_entry_for, @@ -129,9 +131,24 @@ test fn red_control_unknown_schema_is_unreadable() -> Bool { } } +// Both facts drive through the owner's producers: absence is ESTABLISHED by a succeeded listing +// that omits the index (the sole_constructor carrier is produced inside filesystem_io, never +// forged here), while a listed index whose read is refused stays present-but-unavailable. test fn a_missing_index_reads_as_absent_not_unreadable() -> Bool { - let absent = served_observation_read_from_filesystem(success: false, content: "", error: "No such file or directory") - let broken = served_observation_read_from_filesystem(success: false, content: "", error: "Permission denied") + let absent_obs = filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/var/lib/gunbc/served", success: true, entries: "bodies.json", error: "unused on success"), + name: "index.json", + path: "/var/lib/gunbc/served/index.json", + read: FilesystemReadRefused { error: "No such file or directory" }, + ) + let broken_obs = filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/var/lib/gunbc/served", success: true, entries: "index.json\nbodies.json", error: "unused on success"), + name: "index.json", + path: "/var/lib/gunbc/served/index.json", + read: FilesystemReadRefused { error: "Permission denied" }, + ) + let absent = served_observation_read_from_observation(observation: absent_obs) + let broken = served_observation_read_from_observation(observation: broken_obs) (match absent { ServedObservationAbsent => true _ => false }) && (match broken { ServedObservationUnreadable { reason } => string_contains(s: reason, pattern: "Permission denied") _ => false }) } @@ -153,11 +170,27 @@ test fn a_body_matching_its_entry_is_served_with_stored_status_and_media_type() let workflow = fixture_entry(kind: WorkflowJson) let daily_r = served_observation_body_response( entry: daily, - body: served_observation_body_read_from_filesystem(entry: daily, success: true, content: fixture_daily_body, error: ""), + body: served_observation_body_read_from_observation( + entry: daily, + observation: filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/served", success: true, entries: "daily.html", error: "unused on success"), + name: "daily.html", + path: "/srv/gunbc/served/daily.html", + read: FilesystemReadSucceeded { content: fixture_daily_body }, + ), + ), ) let workflow_r = served_observation_body_response( entry: workflow, - body: served_observation_body_read_from_filesystem(entry: workflow, success: true, content: "{\"refused\":\"tmux unavailable\"}", error: ""), + body: served_observation_body_read_from_observation( + entry: workflow, + observation: filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/served", success: true, entries: "workflow.json", error: "unused on success"), + name: "workflow.json", + path: "/srv/gunbc/served/workflow.json", + read: FilesystemReadSucceeded { content: "{\"refused\":\"tmux unavailable\"}" }, + ), + ), ) daily_r.status == 200 && daily_r.content_type == text_html_utf8 && daily_r.body == fixture_daily_body && workflow_r.status == 503 && workflow_r.content_type == application_json_utf8 && string_contains(s: workflow_r.body, pattern: "tmux unavailable") @@ -168,7 +201,15 @@ test fn red_control_a_body_that_does_not_hash_to_its_entry_is_refused() -> Bool let daily = fixture_entry(kind: DailyPage) let r = served_observation_body_response( entry: daily, - body: served_observation_body_read_from_filesystem(entry: daily, success: true, content: "stale", error: ""), + body: served_observation_body_read_from_observation( + entry: daily, + observation: filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/served", success: true, entries: "daily.html", error: "unused on success"), + name: "daily.html", + path: "/srv/gunbc/served/daily.html", + read: FilesystemReadSucceeded { content: "stale" }, + ), + ), ) r.status == 503 && string_contains(s: r.body, pattern: "ServedObservationBodyMismatch") @@ -179,16 +220,76 @@ test fn a_body_the_index_names_but_disk_lacks_is_a_typed_503() -> Bool { let daily = fixture_entry(kind: DailyPage) let r = served_observation_body_response( entry: daily, - body: served_observation_body_read_from_filesystem(entry: daily, success: false, content: "", error: "No such file or directory"), + body: served_observation_body_read_from_observation( + entry: daily, + observation: filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/served", success: true, entries: "workflow.json", error: "unused on success"), + name: "daily.html", + path: "/srv/gunbc/served/daily.html", + read: FilesystemReadRefused { error: "No such file or directory (os error 2)" }, + ), + ), ) r.status == 503 && string_contains(s: r.body, pattern: "ServedObservationBodyMissing") } +// RED control: the body the index names is present but unreadable -- the listing establishes it +// and the read refusal says so -- so the refusal says "unreadable", never "missing": a failed +// read is not evidence of absence. +test fn red_control_a_listed_body_that_cannot_be_read_is_unreadable_not_missing() -> Bool { + let daily = fixture_entry(kind: DailyPage) + let r = served_observation_body_response( + entry: daily, + body: served_observation_body_read_from_observation( + entry: daily, + observation: filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/served", success: true, entries: "daily.html", error: "unused on success"), + name: "daily.html", + path: "/srv/gunbc/served/daily.html", + read: FilesystemReadRefused { error: "Permission denied (os error 13)" }, + ), + ), + ) + r.status == 503 && string_contains(s: r.body, pattern: "ServedObservationBodyUnreadable") + && !string_contains(s: r.body, pattern: "ServedObservationBodyMissing") +} + +// RED control: the listing itself refused, so existence was never established either way. The +// refusal carries the owner's listing cause ("could not be listed") and never claims the body +// exists. +test fn a_refused_listing_leaves_existence_unknown_and_never_claims_the_body_exists() -> Bool { + let daily = fixture_entry(kind: DailyPage) + let r = served_observation_body_response( + entry: daily, + body: served_observation_body_read_from_observation( + entry: daily, + observation: filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/served", success: false, entries: "", error: "Permission denied (os error 13)"), + name: "daily.html", + path: "/srv/gunbc/served/daily.html", + read: FilesystemReadRefused { error: "not reached" }, + ), + ), + ) + r.status == 503 + && string_contains(s: r.body, pattern: "could not be observed") + && string_contains(s: r.body, pattern: "could not be listed") + && !string_contains(s: r.body, pattern: "exists") +} + test fn a_refused_daily_render_is_served_as_plain_text_not_html() -> Bool { let entry = ServedObservationEntry { kind: DailyPage, status: 500, digest: served_observation_body_digest(body: "page render refused") } let r = served_observation_body_response( entry: entry, - body: served_observation_body_read_from_filesystem(entry: entry, success: true, content: "page render refused", error: ""), + body: served_observation_body_read_from_observation( + entry: entry, + observation: filesystem_file_observation( + listing: filesystem_listing_observation(directory: "/srv/gunbc/served", success: true, entries: "daily.html", error: "unused on success"), + name: "daily.html", + path: "/srv/gunbc/served/daily.html", + read: FilesystemReadSucceeded { content: "page render refused" }, + ), + ), ) r.status == 500 && r.content_type == text_plain_utf8 } diff --git a/dag/test/claim/roadmap/roadmap_validation_oracle_witness_test.dag b/dag/test/claim/roadmap/roadmap_validation_oracle_witness_test.dag index 71e89ec5c2f..a9fde58ee90 100644 --- a/dag/test/claim/roadmap/roadmap_validation_oracle_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_validation_oracle_witness_test.dag @@ -3,6 +3,7 @@ module test.claim.roadmap_validation_oracle_witness_test import gunbc.roadmap_session_placement { SessionPlacementAbsent } import gunbc.roadmap.roadmap_submission { CaptureAbsent } import gunbc.roadmap.roadmap_fanin { IntegrationReceiptAbsent } +import extdeps.filesystem.filesystem_io { FilesystemReadSucceeded, FilesystemReadRefused } import gunbc.roadmap.roadmap_review_function { review_report_absent } import gunbc.worker_lifecycle { WorkerProcessAbsent } @@ -18,6 +19,7 @@ import std.content_hash { } import extdeps.pin { CrossFamilyIdentityIncomparable } import gunbc.git_diff_change_window { git_diff_window_absent_identity } +import gunbc.roadmap_workflow_command { WorkflowModeledStateObserved } import gunbc.roadmap_workflow_progress { AttemptWorktreePresent, WorkflowAttemptEvidence, @@ -172,9 +174,11 @@ fn observe_witness(source_readable: Bool, source: String) -> List WorkflowAttemptEvidence { node_id: "v1-emitter-fixed-point", attempt_key: "feedfacefeedface", branch: "dispatch/v1-emitter-fixed-point-afeedfacefeedface", - modeled_state_present: true, + modeled_state: WorkflowModeledStateObserved, worktree: AttemptWorktreePresent { path: "/wt/fixture" }, admission_receipt_readable: true, admission_receipt: "{\"schema\": \"roadmap-dispatch-environment/v1\", \"status\": \"admitted\"}", diff --git a/dag/test/claim/roadmap/roadmap_workflow_command_witness_test.dag b/dag/test/claim/roadmap/roadmap_workflow_command_witness_test.dag index 3e9a39a9f97..e8f4c391831 100644 --- a/dag/test/claim/roadmap/roadmap_workflow_command_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_workflow_command_witness_test.dag @@ -85,8 +85,9 @@ test fn retry_mechanism_cannot_mint_attempt() -> Bool { } test fn witness_legacy_attempt_cannot_satisfy_obligations() -> Bool { - legacy_attempt_cannot_satisfy_obligations(modeled_state_present: false) - && !legacy_attempt_cannot_satisfy_obligations(modeled_state_present: true) + legacy_attempt_cannot_satisfy_obligations(modeled_state: WorkflowModeledStateAbsent) + && !legacy_attempt_cannot_satisfy_obligations(modeled_state: WorkflowModeledStateObserved) + && !legacy_attempt_cannot_satisfy_obligations(modeled_state: WorkflowModeledStateUnreadable { cause: "Permission denied (os error 13)" }) } test fn legacy_attempt_projects_as_supersedable_not_fault() -> Bool { @@ -94,7 +95,7 @@ test fn legacy_attempt_projects_as_supersedable_not_fault() -> Bool { node_id: "legacy-node", attempt_key: "", branch: "dispatch/legacy-node", - modeled_state_present: false, + modeled_state: WorkflowModeledStateAbsent, worktree: AttemptWorktreeUnobserved { path: "", cause: "no modeled state pointer" }, admission_receipt_readable: false, admission_receipt: "", diff --git a/dag/test/claim/roadmap/roadmap_workflow_progress_witness_test.dag b/dag/test/claim/roadmap/roadmap_workflow_progress_witness_test.dag index 244c6d77c75..39a797cea7b 100644 --- a/dag/test/claim/roadmap/roadmap_workflow_progress_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_workflow_progress_witness_test.dag @@ -15,6 +15,9 @@ import gunbc.roadmap_provider_events { ProviderFailed, ProviderEventsRefused, } +import gunbc.roadmap_workflow_command { + WorkflowModeledStateStanding, WorkflowModeledStateObserved, WorkflowModeledStateAbsent, WorkflowModeledStateUnreadable, +} import gunbc.roadmap_workflow_progress { AttemptWorktreeObservation, AttemptWorktreePresent, AttemptWorktreeAbsent, AttemptWorktreeUnobserved, review_segment, @@ -119,7 +122,7 @@ fn fixture_evidence( node_id: "node-1", attempt_key: "feedfacefeedface", branch: "dispatch/node-1-afeedfacefeedface", - modeled_state_present: true, + modeled_state: WorkflowModeledStateObserved, worktree: AttemptWorktreePresent { path: "/wt/node-1-afeedfacefeedface" }, admission_receipt_readable: true, admission_receipt: "{\"schema\": \"roadmap-dispatch-environment/v1\", \"status\": \"admitted\"}", @@ -160,6 +163,53 @@ fn fixture_evidence( } } +fn fixture_evidence_with_standing( + standing: WorkflowModeledStateStanding, + events: String, + process: WorkerProcessEvidence, +) -> WorkflowAttemptEvidence { + let base = fixture_evidence(events: events, process: process) + WorkflowAttemptEvidence { + node_id: base.node_id, + attempt_key: base.attempt_key, + branch: base.branch, + modeled_state: standing, + worktree: base.worktree, + admission_receipt_readable: base.admission_receipt_readable, + admission_receipt: base.admission_receipt, + spawn_failure_present: base.spawn_failure_present, + spawn_failure: base.spawn_failure, + provider_events_readable: base.provider_events_readable, + provider_events: base.provider_events, + provider_events_error: base.provider_events_error, + process: base.process, + validation_summary: base.validation_summary, + verification_subject: base.verification_subject, + verification_selection: base.verification_selection, + verification_source: base.verification_source, + publication_subject: base.publication_subject, + publication_source: base.publication_source, + review: base.review, + submission_capture: base.submission_capture, + integration: base.integration, + goal_audit: base.goal_audit, + session_placement: base.session_placement, + } +} + +fn segment_detail( + segments: List, + key: String, +) -> String { + fold(segments, init: "", f: fn(acc, segment) { + if acc == "" && workflow_segment_kind_key(kind: segment.kind) == key { + segment.detail + } else { + acc + } + }) +} + fn segment_state( segments: List, key: String, @@ -252,6 +302,59 @@ test fn completed_event_and_nonzero_exit_refuse_contradiction() -> Bool { } } +// COMPOSED control (side chat on this PR): the evidence construction carries the typed pointer +// standing, so this route goes evidence -> provider, not helper text. An attempt whose +// current-attempt-key pointer could not be read has UNKNOWN standing: the provider must refuse +// with the standing-unknown arm and must NOT mint the legacy, supersedable standing that an +// established absence carries. +test fn an_unreadable_pointer_refuses_with_unknown_standing_and_never_legacy_standing() -> Bool { + let unreadable = workflow_provider_from_evidence(evidence: fixture_evidence_with_standing( + standing: WorkflowModeledStateUnreadable { cause: "Permission denied (os error 13)" }, + events: "{\"type\":\"turn.completed\",\"usage\":{}}\n", + process: WorkerProcessExited { exit_code: 0, last_command: "node" }, + )) + let absent = workflow_provider_from_evidence(evidence: fixture_evidence_with_standing( + standing: WorkflowModeledStateAbsent, + events: "{\"type\":\"turn.completed\",\"usage\":{}}\n", + process: WorkerProcessExited { exit_code: 0, last_command: "node" }, + )) + match unreadable { + ProviderEventsRefused { activity, detail } => + activity == "attempt standing unknown" + && string_contains(s: detail, pattern: "could not be read") + && !string_contains(s: detail, pattern: "legacy") + && match absent { + ProviderEventsRefused { activity: absent_activity, detail: absent_detail } => + absent_activity != "attempt standing unknown" + && string_contains(s: absent_detail, pattern: "predates modeled admission") + _ => false + } + _ => false + } +} + +// Composed control THROUGH the progress fold (side chat review 5409834403): an unreadable pointer +// refuses verification, review AND goal audit -- never pending, never absence-shaped -- while +// established absence still renders the legacy arm (pending). +test fn an_unreadable_pointer_refuses_verification_review_and_audit_through_workflow_attempt_progress() -> Bool { + let unreadable = workflow_attempt_progress(evidence: fixture_evidence_with_standing( + standing: WorkflowModeledStateUnreadable { cause: "Permission denied (os error 13)" }, + events: "{\"type\":\"turn.completed\",\"usage\":{}}\n", + process: WorkerProcessExited { exit_code: 0, last_command: "node" }, + )) + let absent = workflow_attempt_progress(evidence: fixture_evidence_with_standing( + standing: WorkflowModeledStateAbsent, + events: "{\"type\":\"turn.completed\",\"usage\":{}}\n", + process: WorkerProcessExited { exit_code: 0, last_command: "node" }, + )) + segment_state(unreadable.segments, "verify") == "refused" + && segment_state(unreadable.segments, "review") == "refused" + && segment_state(unreadable.segments, "audit") == "refused" + && string_contains(s: segment_detail(unreadable.segments, "verify"), pattern: "standing is unknown") + && segment_state(absent.segments, "verify") == "pending" + && segment_state(absent.segments, "review") == "pending" +} + // review 44032 REDs. The Agent obligation's design contract requires a compatible process exit, so // turn.completed alone must never paint agent complete. These three witnesses pin the arms that // previously preserved ProviderCompleted from incomplete evidence: still-running stays active, @@ -393,7 +496,7 @@ test fn forged_admission_receipt_cannot_complete_environment() -> Bool { node_id: base.node_id, attempt_key: base.attempt_key, branch: base.branch, - modeled_state_present: base.modeled_state_present, + modeled_state: base.modeled_state, worktree: base.worktree, admission_receipt_readable: base.admission_receipt_readable, admission_receipt: nested_admitted_forgery(), @@ -429,7 +532,7 @@ test fn persisted_staging_failure_precedes_empty_event_stream() -> Bool { node_id: base.node_id, attempt_key: base.attempt_key, branch: base.branch, - modeled_state_present: base.modeled_state_present, + modeled_state: base.modeled_state, worktree: base.worktree, admission_receipt_readable: base.admission_receipt_readable, admission_receipt: base.admission_receipt, @@ -463,7 +566,7 @@ test fn legacy_attempt_refuses_unobserved_early_stages() -> Bool { node_id: "legacy", attempt_key: "", branch: "dispatch/legacy", - modeled_state_present: false, + modeled_state: WorkflowModeledStateAbsent, worktree: AttemptWorktreeUnobserved { path: "", cause: "no modeled state pointer" }, admission_receipt_readable: false, admission_receipt: "", @@ -522,7 +625,7 @@ test fn readable_but_invalid_admission_receipt_refuses_progress() -> Bool { node_id: base.node_id, attempt_key: base.attempt_key, branch: base.branch, - modeled_state_present: base.modeled_state_present, + modeled_state: base.modeled_state, worktree: base.worktree, admission_receipt_readable: true, admission_receipt: "{\"schema\": \"roadmap-dispatch-environment/v1\", \"status\": \"refused\"}", @@ -666,7 +769,7 @@ fn publication_segment_of( node_id: base.node_id, attempt_key: base.attempt_key, branch: base.branch, - modeled_state_present: base.modeled_state_present, + modeled_state: base.modeled_state, worktree: base.worktree, admission_receipt_readable: base.admission_receipt_readable, admission_receipt: base.admission_receipt, @@ -829,7 +932,7 @@ fn evidence_with_worktree(base: WorkflowAttemptEvidence, worktree: AttemptWorktr node_id: base.node_id, attempt_key: base.attempt_key, branch: base.branch, - modeled_state_present: base.modeled_state_present, + modeled_state: base.modeled_state, worktree: worktree, admission_receipt_readable: base.admission_receipt_readable, admission_receipt: base.admission_receipt, diff --git a/dag/test/claim/verification_evidence_addressing_witness_test.dag b/dag/test/claim/verification_evidence_addressing_witness_test.dag index e3a8ec19a92..2ee1480fcdc 100644 --- a/dag/test/claim/verification_evidence_addressing_witness_test.dag +++ b/dag/test/claim/verification_evidence_addressing_witness_test.dag @@ -38,6 +38,7 @@ import gunbc.roadmap_model { import gunbc.roadmap_status { DerivableLine, Unsigned } import gunbc.roadmap_execution_contract { ExecutionContractUnspecified } import gunbc.roadmap_provider_events { ProviderCompleted } +import gunbc.roadmap_workflow_command { WorkflowModeledStateObserved } import gunbc.roadmap_workflow_progress { WorkflowAttemptEvidence, AttemptWorktreePresent, @@ -344,7 +345,7 @@ fn fixture_evidence( node_id: "recut-a", attempt_key: "feedfacefeedface", branch: "dispatch/recut-a-afeedfacefeedface", - modeled_state_present: true, + modeled_state: WorkflowModeledStateObserved, worktree: AttemptWorktreePresent { path: "/wt" }, admission_receipt_readable: true, admission_receipt: "{\"schema\": \"roadmap-dispatch-environment/v1\", \"status\": \"admitted\"}",