Skip to main content

traverse_runtime/
workflows.rs

1use crate::events::{EventBroker, EventError};
2use crate::security::{RuntimeWarning, verify_artifact};
3use crate::{
4    ExecutionFailureReason, ExecutionFailureState, LocalExecutor, Runtime, RuntimeError,
5    RuntimeErrorCode, RuntimeExecutionOutcome, execution_failure_outcome, runtime_error,
6    successful_execution_outcome, validate_payload_against_contract,
7};
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value, json};
10use std::collections::BTreeSet;
11use traverse_contracts::{EventReference, ServiceType};
12use traverse_registry::{
13    LookupScope, RegistryScope, ResolvedCapability, ResolvedWorkflow, WorkflowEdge,
14    WorkflowEdgePredicate, WorkflowEdgeTrigger, WorkflowNode,
15};
16
17const WORKFLOW_REQUEST_KIND: &str = "workflow_execution_request";
18const WORKFLOW_EVIDENCE_KIND: &str = "workflow_traversal_evidence";
19const WORKFLOW_SCHEMA_VERSION: &str = "1.0.0";
20const WORKFLOW_GOVERNING_SPEC: &str = "007-workflow-registry-traversal";
21/// Maximum events drained per `EventBroker::poll` call while gathering
22/// candidate events for waiting event-driven edges (spec 099).
23const EVENT_BROKER_POLL_BATCH: usize = 256;
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct WorkflowExecutionRequest {
27    pub kind: String,
28    pub schema_version: String,
29    pub request_id: String,
30    pub workflow_id: String,
31    pub workflow_version: String,
32    pub scope: WorkflowLookupScope,
33    pub input: Value,
34    pub governing_spec: String,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "snake_case")]
39pub enum WorkflowLookupScope {
40    PublicOnly,
41    PreferPrivate,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct WorkflowTraversalEvidence {
46    pub kind: String,
47    pub schema_version: String,
48    pub trace_id: String,
49    pub request_id: String,
50    pub workflow_id: String,
51    pub workflow_version: String,
52    pub governing_spec: String,
53    pub visited_nodes: Vec<WorkflowTraversalStepRecord>,
54    pub traversed_edges: Vec<WorkflowTraversalEdgeRecord>,
55    pub emitted_events: Vec<EventReference>,
56    #[serde(default)]
57    pub waiting_edges: Vec<WaitingWorkflowEdgeContext>,
58    #[serde(default)]
59    pub event_match_records: Vec<EventMatchRecord>,
60    #[serde(default)]
61    pub event_wake_decisions: Vec<EventWakeDecision>,
62    #[serde(default)]
63    pub event_consumptions: Vec<EventConsumptionRecord>,
64    pub result: WorkflowTraversalResult,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68pub struct WorkflowTraversalStepRecord {
69    pub step_index: usize,
70    pub node_id: String,
71    pub capability_id: String,
72    pub capability_version: String,
73    pub status: WorkflowTraversalStepStatus,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum WorkflowTraversalStepStatus {
79    Entered,
80    Completed,
81    Failed,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct WorkflowTraversalEdgeRecord {
86    pub edge_id: String,
87    pub from: String,
88    pub to: String,
89    pub trigger: WorkflowTraversalTrigger,
90    #[serde(default)]
91    pub event: Option<EventReference>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
95pub struct WaitingWorkflowEdgeContext {
96    pub workflow_execution_id: String,
97    pub edge_id: String,
98    pub from_node_id: String,
99    pub to_node_id: String,
100    pub event_ref: EventReference,
101    #[serde(default)]
102    pub predicate: Option<WorkflowEdgePredicate>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct EventMatchRecord {
107    pub event_id: String,
108    pub event_version: String,
109    pub edge_id: String,
110    pub match_result: EventMatchResult,
111    #[serde(default)]
112    pub predicate_result: Option<EventPredicateResult>,
113    #[serde(default)]
114    pub rejection_reason: Option<String>,
115    pub recorded_at: String,
116    /// `EventBroker` subscription that sourced this event, `None` when the
117    /// event came from the current node's own emitted output rather than
118    /// `EventBroker` (spec 099 FR-005).
119    #[serde(default)]
120    pub subscription_id: Option<String>,
121    /// `EventBroker` cursor the event was delivered at, `None` for a
122    /// same-node emitted event (spec 099 FR-005).
123    #[serde(default)]
124    pub cursor: Option<String>,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128#[serde(rename_all = "snake_case")]
129pub enum EventMatchResult {
130    Matched,
131    NotMatched,
132    AlreadyConsumed,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum EventPredicateResult {
138    Passed,
139    Failed,
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct EventWakeDecision {
144    pub decision_type: String,
145    pub event_id: String,
146    pub event_version: String,
147    pub edge_id: String,
148    pub workflow_execution_id: String,
149    pub wake_order: usize,
150    pub result: EventWakeDecisionResult,
151    pub recorded_at: String,
152    /// `EventBroker` subscription responsible for this wake-up, `None` when
153    /// woken by the current node's own emitted output (spec 099 FR-005).
154    #[serde(default)]
155    pub subscription_id: Option<String>,
156    /// `EventBroker` cursor responsible for this wake-up, `None` when woken
157    /// by the current node's own emitted output (spec 099 FR-005).
158    #[serde(default)]
159    pub cursor: Option<String>,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "snake_case")]
164pub enum EventWakeDecisionResult {
165    Taken,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
169pub struct EventConsumptionRecord {
170    pub event_id: String,
171    pub event_version: String,
172    pub edge_id: String,
173    pub workflow_execution_id: String,
174    pub consumed_at: String,
175    /// `EventBroker` subscription that delivered the consumed event, `None`
176    /// when consumed from the current node's own emitted output (spec 099
177    /// FR-005).
178    #[serde(default)]
179    pub subscription_id: Option<String>,
180    /// `EventBroker` cursor the consumed event was delivered at, `None` for
181    /// a same-node emitted event (spec 099 FR-005).
182    #[serde(default)]
183    pub cursor: Option<String>,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
187#[serde(rename_all = "snake_case")]
188pub enum WorkflowTraversalTrigger {
189    Direct,
190    Event,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
194pub struct WorkflowTraversalResult {
195    pub status: WorkflowTraversalStatus,
196    #[serde(default)]
197    pub failure_reason: Option<WorkflowTraversalFailureReason>,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
201#[serde(rename_all = "snake_case")]
202pub enum WorkflowTraversalStatus {
203    Completed,
204    Error,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "snake_case")]
209pub enum WorkflowTraversalFailureReason {
210    WorkflowNotFound,
211    WorkflowInvalid,
212    AmbiguousNextEdge,
213    MissingRequiredEvent,
214    TerminalNodeNotReached,
215    StepExecutionFailed,
216    /// A waiting edge's declared event type is not registered in
217    /// `EventBroker`'s catalog, so a subscription could not be established
218    /// (spec 099 FR-007).
219    EventSubscriptionFailed,
220    /// `EventBroker` was unreachable or internally failing when a waiting
221    /// edge attempted to establish or poll its subscription, distinct from
222    /// an unregistered event type (spec 099 FR-008).
223    EventBrokerUnavailable,
224}
225
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227pub struct WorkflowExecutionResult {
228    pub kind: String,
229    pub schema_version: String,
230    pub request_id: String,
231    pub workflow_id: String,
232    pub workflow_version: String,
233    pub status: WorkflowTraversalStatus,
234    #[serde(default)]
235    pub output: Option<Value>,
236    #[serde(default)]
237    pub error: Option<RuntimeError>,
238    #[serde(default)]
239    pub warnings: Vec<RuntimeWarning>,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
243pub struct WorkflowExecutionOutcome {
244    pub result: WorkflowExecutionResult,
245    pub evidence: WorkflowTraversalEvidence,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq)]
249struct EmittedEventRecord {
250    record_id: String,
251    event: EventReference,
252    payload: Option<Value>,
253    /// `EventBroker` subscription/cursor this event was polled from, `None`
254    /// for an event extracted from the current node's own output.
255    source: Option<BrokerEventSource>,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq)]
259struct BrokerEventSource {
260    subscription_id: String,
261    cursor: String,
262}
263
264#[derive(Debug, Clone, Default, PartialEq, Eq)]
265struct WorkflowEventEvidenceBundle {
266    waiting_edges: Vec<WaitingWorkflowEdgeContext>,
267    match_records: Vec<EventMatchRecord>,
268    wake_decisions: Vec<EventWakeDecision>,
269    consumptions: Vec<EventConsumptionRecord>,
270}
271
272#[derive(Debug, Clone, Default, PartialEq, Eq)]
273struct EventDrivenEvaluationOutcome {
274    taken_edge_ids: Vec<String>,
275    evidence: WorkflowEventEvidenceBundle,
276}
277
278impl<E> Runtime<E>
279where
280    E: LocalExecutor,
281{
282    #[must_use]
283    #[allow(clippy::needless_pass_by_value)]
284    pub fn execute_workflow(&self, request: WorkflowExecutionRequest) -> WorkflowExecutionOutcome {
285        if let Some(error) = validate_workflow_request(&request) {
286            return workflow_failure(
287                &request,
288                WorkflowTraversalFailureReason::WorkflowInvalid,
289                error,
290                Vec::new(),
291                Vec::new(),
292                Vec::new(),
293                WorkflowEventEvidenceBundle::default(),
294                Vec::new(),
295            );
296        }
297
298        let lookup_scope = map_workflow_lookup_scope(request.scope);
299        let Some(workflow) = self.workflow_registry.find_exact(
300            lookup_scope,
301            &request.workflow_id,
302            &request.workflow_version,
303        ) else {
304            return workflow_failure(
305                &request,
306                WorkflowTraversalFailureReason::WorkflowNotFound,
307                runtime_error(
308                    RuntimeErrorCode::CapabilityNotFound,
309                    "workflow definition was not found in the workflow registry",
310                    json!({"workflow_id": request.workflow_id, "workflow_version": request.workflow_version}),
311                ),
312                Vec::new(),
313                Vec::new(),
314                Vec::new(),
315                WorkflowEventEvidenceBundle::default(),
316                Vec::new(),
317            );
318        };
319
320        if let Err(error) = validate_payload_against_contract(
321            &request.input,
322            &workflow.definition.inputs.schema,
323            RuntimeErrorCode::RequestInvalid,
324            "workflow request input does not satisfy the workflow input contract",
325        ) {
326            return workflow_failure(
327                &request,
328                WorkflowTraversalFailureReason::WorkflowInvalid,
329                error,
330                Vec::new(),
331                Vec::new(),
332                Vec::new(),
333                WorkflowEventEvidenceBundle::default(),
334                Vec::new(),
335            );
336        }
337
338        match self.traverse_workflow(&request, &workflow) {
339            Ok(success) => success,
340            Err(failure) => failure,
341        }
342    }
343
344    pub(crate) fn execute_workflow_capability(
345        &self,
346        mut context: crate::ExecutionContext,
347        selected: &ResolvedCapability,
348        started_execution: crate::StartedExecution,
349    ) -> RuntimeExecutionOutcome {
350        let Some(workflow_ref) = selected.artifact.workflow_ref.as_ref() else {
351            let error = runtime_error(
352                RuntimeErrorCode::ArtifactMissing,
353                "workflow-backed capability is missing its workflow reference",
354                json!({"artifact_ref": selected.record.artifact_ref}),
355            );
356            return execution_failure_outcome(
357                context,
358                ExecutionFailureState {
359                    artifact_ref: selected.record.artifact_ref.clone(),
360                    started_at: started_execution.started_at,
361                    placement: started_execution.placement.clone(),
362                    failure_reason: ExecutionFailureReason::ArtifactMissing,
363                },
364                error,
365                Vec::new(),
366                None,
367            );
368        };
369
370        let workflow_scope = match selected.record.scope {
371            RegistryScope::Public => WorkflowLookupScope::PublicOnly,
372            RegistryScope::Private => WorkflowLookupScope::PreferPrivate,
373        };
374        let workflow = self.execute_workflow(WorkflowExecutionRequest {
375            kind: WORKFLOW_REQUEST_KIND.to_string(),
376            schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
377            request_id: context.attempt.request.request_id.clone(),
378            workflow_id: workflow_ref.workflow_id.clone(),
379            workflow_version: workflow_ref.workflow_version.clone(),
380            scope: workflow_scope,
381            input: context.attempt.request.input.clone(),
382            governing_spec: WORKFLOW_GOVERNING_SPEC.to_string(),
383        });
384        context
385            .attempt
386            .warnings
387            .extend(workflow.result.warnings.iter().cloned());
388
389        match workflow.result.status {
390            WorkflowTraversalStatus::Completed => {
391                let output = workflow.result.output.unwrap_or(Value::Object(Map::new()));
392                let workflow_evidence = workflow.evidence;
393                let emitted_events = workflow_evidence.emitted_events.clone();
394                successful_execution_outcome(
395                    context,
396                    selected,
397                    started_execution,
398                    output,
399                    emitted_events,
400                    Some(workflow_evidence),
401                )
402            }
403            WorkflowTraversalStatus::Error => {
404                let workflow_evidence = workflow.evidence;
405                let emitted_events = workflow_evidence.emitted_events.clone();
406                execution_failure_outcome(
407                    context,
408                    ExecutionFailureState {
409                        artifact_ref: selected.record.artifact_ref.clone(),
410                        started_at: started_execution.started_at,
411                        placement: started_execution.placement,
412                        failure_reason: ExecutionFailureReason::ExecutionFailed,
413                    },
414                    workflow.result.error.unwrap_or(runtime_error(
415                        RuntimeErrorCode::ExecutionFailed,
416                        "workflow-backed capability execution failed",
417                        json!({}),
418                    )),
419                    emitted_events,
420                    Some(workflow_evidence),
421                )
422            }
423        }
424    }
425
426    #[allow(clippy::result_large_err, clippy::too_many_lines)]
427    fn traverse_workflow(
428        &self,
429        request: &WorkflowExecutionRequest,
430        workflow: &ResolvedWorkflow,
431    ) -> Result<WorkflowExecutionOutcome, WorkflowExecutionOutcome> {
432        let mut state = workflow_state(&request.input);
433        let mut current = workflow.definition.start_node.clone();
434        let mut step_index = 0;
435        let mut visited = Vec::new();
436        let mut traversed = Vec::new();
437        let mut emitted = Vec::new();
438        let mut event_evidence = WorkflowEventEvidenceBundle::default();
439        let mut consumed_event_edges = BTreeSet::new();
440        let mut warnings: Vec<RuntimeWarning> = Vec::new();
441        let workflow_execution_id = format!("workflow_exec_{}", request.request_id);
442
443        loop {
444            let Some(node) = workflow
445                .definition
446                .nodes
447                .iter()
448                .find(|node| node.node_id == current)
449            else {
450                return Err(workflow_failure(
451                    request,
452                    WorkflowTraversalFailureReason::WorkflowInvalid,
453                    runtime_error(
454                        RuntimeErrorCode::ExecutionFailed,
455                        "workflow node could not be resolved during traversal",
456                        json!({"node_id": current}),
457                    ),
458                    visited,
459                    traversed,
460                    emitted,
461                    event_evidence,
462                    warnings,
463                ));
464            };
465
466            visited.push(WorkflowTraversalStepRecord {
467                step_index,
468                node_id: node.node_id.clone(),
469                capability_id: node.capability_id.clone(),
470                capability_version: node.capability_version.clone(),
471                status: WorkflowTraversalStepStatus::Entered,
472            });
473
474            let lookup_scope = map_workflow_lookup_scope(request.scope);
475            let Some(capability) = self.registry.find_exact(
476                lookup_scope,
477                &node.capability_id,
478                &node.capability_version,
479            ) else {
480                return Err(workflow_failure(
481                    request,
482                    WorkflowTraversalFailureReason::WorkflowInvalid,
483                    runtime_error(
484                        RuntimeErrorCode::CapabilityNotFound,
485                        "workflow node capability was not found in the capability registry",
486                        json!({"capability_id": node.capability_id, "capability_version": node.capability_version}),
487                    ),
488                    visited,
489                    traversed,
490                    emitted,
491                    event_evidence,
492                    warnings,
493                ));
494            };
495
496            // Artifact security gate (spec 030-security-identity-model FR-013):
497            // workflow steps apply the same verification as direct capability
498            // execution, so an artifact rejected by `Runtime::execute` cannot
499            // run by being wrapped in a workflow pipeline step (spec 058).
500            let artifact_bytes = match crate::load_artifact_bytes_for_verification(&capability) {
501                Ok(bytes) => bytes,
502                Err(error) => {
503                    let mut failed = visited;
504                    if let Some(last) = failed.last_mut() {
505                        last.status = WorkflowTraversalStepStatus::Failed;
506                    }
507                    return Err(workflow_failure(
508                        request,
509                        WorkflowTraversalFailureReason::StepExecutionFailed,
510                        error,
511                        failed,
512                        traversed,
513                        emitted,
514                        event_evidence,
515                        warnings,
516                    ));
517                }
518            };
519            match verify_artifact(&capability, &artifact_bytes, &self.security) {
520                Ok(record) => {
521                    if let Some(code) = record.warning_code {
522                        warnings.push(RuntimeWarning {
523                            code,
524                            message:
525                                "unsigned local/dev artifact allowed by development security mode"
526                                    .to_string(),
527                        });
528                    }
529                }
530                Err(failure) => {
531                    let mut failed = visited;
532                    if let Some(last) = failed.last_mut() {
533                        last.status = WorkflowTraversalStepStatus::Failed;
534                    }
535                    return Err(workflow_failure(
536                        request,
537                        WorkflowTraversalFailureReason::StepExecutionFailed,
538                        runtime_error(
539                            RuntimeErrorCode::ContractViolation,
540                            "artifact signature verification failed before workflow step execution",
541                            json!({
542                                "code": failure.code(),
543                                "artifact_verification": failure.record(),
544                                "node_id": node.node_id,
545                            }),
546                        ),
547                        failed,
548                        traversed,
549                        emitted,
550                        event_evidence,
551                        warnings,
552                    ));
553                }
554            }
555
556            let node_input = node_input(&state, node);
557            if let Err(error) = validate_payload_against_contract(
558                &node_input,
559                &capability.contract.inputs.schema,
560                RuntimeErrorCode::RequestInvalid,
561                "workflow node input does not satisfy the capability input contract",
562            ) {
563                let mut failed = visited;
564                if let Some(last) = failed.last_mut() {
565                    last.status = WorkflowTraversalStepStatus::Failed;
566                }
567                return Err(workflow_failure(
568                    request,
569                    WorkflowTraversalFailureReason::StepExecutionFailed,
570                    error,
571                    failed,
572                    traversed,
573                    emitted,
574                    event_evidence,
575                    warnings,
576                ));
577            }
578
579            let output = match self.executor.execute(&capability, &node_input) {
580                Ok(output) => output,
581                Err(failure) => {
582                    let mut failed = visited;
583                    if let Some(last) = failed.last_mut() {
584                        last.status = WorkflowTraversalStepStatus::Failed;
585                    }
586                    return Err(workflow_failure(
587                        request,
588                        WorkflowTraversalFailureReason::StepExecutionFailed,
589                        runtime_error(
590                            RuntimeErrorCode::ExecutionFailed,
591                            &failure.message,
592                            json!({"code": format!("{:?}", failure.code)}),
593                        ),
594                        failed,
595                        traversed,
596                        emitted,
597                        event_evidence,
598                        warnings,
599                    ));
600                }
601            };
602
603            if let Err(error) = validate_payload_against_contract(
604                &output.value,
605                &capability.contract.outputs.schema,
606                RuntimeErrorCode::OutputValidationFailed,
607                "workflow node output does not satisfy the capability output contract",
608            ) {
609                let mut failed = visited;
610                if let Some(last) = failed.last_mut() {
611                    last.status = WorkflowTraversalStepStatus::Failed;
612                }
613                return Err(workflow_failure(
614                    request,
615                    WorkflowTraversalFailureReason::StepExecutionFailed,
616                    error,
617                    failed,
618                    traversed,
619                    emitted,
620                    event_evidence,
621                    warnings,
622                ));
623            }
624
625            // Spec 101-local-executor-event-emission FR-007/FR-008: validate
626            // natively-populated events before using them for anything,
627            // mirroring spec 098's WASM-boundary checks.
628            if let Err(validation_message) = crate::validate_natively_emitted_events(
629                &capability.contract,
630                &output.emitted_events,
631            ) {
632                let mut failed = visited;
633                if let Some(last) = failed.last_mut() {
634                    last.status = WorkflowTraversalStepStatus::Failed;
635                }
636                return Err(workflow_failure(
637                    request,
638                    WorkflowTraversalFailureReason::StepExecutionFailed,
639                    runtime_error(
640                        RuntimeErrorCode::ContractViolation,
641                        &validation_message,
642                        json!({"node_id": node.node_id}),
643                    ),
644                    failed,
645                    traversed,
646                    emitted,
647                    event_evidence,
648                    warnings,
649                ));
650            }
651
652            update_state(&mut state, node, &output.value);
653
654            // Spec 101-local-executor-event-emission FR-005: publish to
655            // EventBroker for Subscribable capabilities, structurally
656            // analogous to PlacementRouter Step 5 (same gate, same
657            // best-effort semantics — a publish error is recorded but does
658            // not fail the workflow step).
659            if capability.contract.service_type == ServiceType::Subscribable {
660                for event in &output.emitted_events {
661                    let _ = self.event_broker.publish(event.clone());
662                }
663            }
664
665            // Spec 101-local-executor-event-emission FR-006: Pass-1
666            // event-driven edge matching reads from the structured
667            // `LocalExecutionOutput.emitted_events` field, not a
668            // JSON-parsed "emitted_events" convention key.
669            let node_emitted: Vec<EmittedEventRecord> = output
670                .emitted_events
671                .iter()
672                .enumerate()
673                .map(|(index, event)| EmittedEventRecord {
674                    record_id: format!("event_record_{index}"),
675                    event: EventReference {
676                        event_id: event.event_type.clone(),
677                        version: event.version.clone(),
678                    },
679                    payload: Some(event.data.clone()),
680                    source: None,
681                })
682                .collect();
683            emitted.extend(node_emitted.iter().map(|record| record.event.clone()));
684            if let Some(last) = visited.last_mut() {
685                last.status = WorkflowTraversalStepStatus::Completed;
686            }
687
688            let outgoing = workflow
689                .definition
690                .edges
691                .iter()
692                .filter(|edge| edge.from == node.node_id)
693                .cloned()
694                .collect::<Vec<_>>();
695            let direct = outgoing
696                .iter()
697                .filter(|edge| edge.trigger == WorkflowEdgeTrigger::Direct)
698                .cloned()
699                .collect::<Vec<_>>();
700            if direct.len() > 1 {
701                return Err(workflow_failure(
702                    request,
703                    WorkflowTraversalFailureReason::AmbiguousNextEdge,
704                    runtime_error(
705                        RuntimeErrorCode::ExecutionFailed,
706                        "workflow traversal found more than one direct next edge",
707                        json!({"node_id": node.node_id}),
708                    ),
709                    visited,
710                    traversed,
711                    emitted,
712                    event_evidence,
713                    warnings,
714                ));
715            }
716            if let Some(edge) = direct.into_iter().next() {
717                traversed.push(edge_record(&edge));
718                current = edge.to;
719                step_index += 1;
720                continue;
721            }
722
723            let waiting_edges = waiting_edge_contexts(
724                &workflow_execution_id,
725                outgoing
726                    .iter()
727                    .filter(|edge| edge.trigger == WorkflowEdgeTrigger::Event)
728                    .cloned()
729                    .collect::<Vec<_>>()
730                    .as_slice(),
731            );
732            if !waiting_edges.is_empty() {
733                event_evidence.waiting_edges.extend(waiting_edges.clone());
734            }
735            // Pass 1: match against the current node's own emitted output,
736            // exactly as before spec 099 (cheapest path, and the only one
737            // that doesn't need a broker at all).
738            let local_evaluation = evaluate_event_driven_edges(
739                &waiting_edges,
740                &node_emitted,
741                &mut consumed_event_edges,
742                &format!("{}:step:{step_index}:local", request.request_id),
743            );
744            event_evidence
745                .match_records
746                .extend(local_evaluation.evidence.match_records.iter().cloned());
747            event_evidence
748                .wake_decisions
749                .extend(local_evaluation.evidence.wake_decisions.iter().cloned());
750            event_evidence
751                .consumptions
752                .extend(local_evaluation.evidence.consumptions.iter().cloned());
753            let mut taken_edge_ids = local_evaluation.taken_edge_ids;
754
755            // Pass 2 (spec 099 FR-001/FR-002): for edges the node's own
756            // output didn't satisfy, poll EventBroker for events published
757            // by any other execution, capability, or external publisher.
758            // Edges already resolved locally never touch the broker, so an
759            // unregistered/unreachable broker cannot fail a workflow whose
760            // event-driven edges are still fully satisfiable from the
761            // node's own output.
762            let unresolved_edges: Vec<WaitingWorkflowEdgeContext> = waiting_edges
763                .iter()
764                .filter(|edge| !taken_edge_ids.contains(&edge.edge_id))
765                .cloned()
766                .collect();
767            if !unresolved_edges.is_empty() {
768                let broker = self.event_broker.as_ref();
769                match poll_broker_events_for_waiting_edges(broker, &unresolved_edges) {
770                    Ok((broker_events, broker_warnings)) => {
771                        warnings.extend(broker_warnings);
772                        let broker_evaluation = evaluate_event_driven_edges(
773                            &unresolved_edges,
774                            &broker_events,
775                            &mut consumed_event_edges,
776                            &format!("{}:step:{step_index}:broker", request.request_id),
777                        );
778                        event_evidence
779                            .match_records
780                            .extend(broker_evaluation.evidence.match_records.iter().cloned());
781                        event_evidence
782                            .wake_decisions
783                            .extend(broker_evaluation.evidence.wake_decisions.iter().cloned());
784                        event_evidence
785                            .consumptions
786                            .extend(broker_evaluation.evidence.consumptions.iter().cloned());
787                        taken_edge_ids.extend(broker_evaluation.taken_edge_ids);
788                    }
789                    Err(BrokerQueryFailure::UnregisteredEventType(event_type)) => {
790                        let mut failed = visited;
791                        if let Some(last) = failed.last_mut() {
792                            last.status = WorkflowTraversalStepStatus::Failed;
793                        }
794                        return Err(workflow_failure(
795                            request,
796                            WorkflowTraversalFailureReason::EventSubscriptionFailed,
797                            runtime_error(
798                                RuntimeErrorCode::RequestInvalid,
799                                "waiting edge references an event type not registered in EventBroker's catalog",
800                                json!({"node_id": node.node_id, "event_type": event_type}),
801                            ),
802                            failed,
803                            traversed,
804                            emitted,
805                            event_evidence,
806                            warnings,
807                        ));
808                    }
809                    Err(BrokerQueryFailure::BrokerUnavailable(detail)) => {
810                        let mut failed = visited;
811                        if let Some(last) = failed.last_mut() {
812                            last.status = WorkflowTraversalStepStatus::Failed;
813                        }
814                        return Err(workflow_failure(
815                            request,
816                            WorkflowTraversalFailureReason::EventBrokerUnavailable,
817                            runtime_error(
818                                RuntimeErrorCode::ExecutionFailed,
819                                "EventBroker was unreachable or internally failing while evaluating a waiting event-driven edge",
820                                json!({"node_id": node.node_id, "detail": detail}),
821                            ),
822                            failed,
823                            traversed,
824                            emitted,
825                            event_evidence,
826                            warnings,
827                        ));
828                    }
829                }
830            }
831            let matched_event_edges = outgoing
832                .iter()
833                .filter(|edge| {
834                    taken_edge_ids
835                        .iter()
836                        .any(|edge_id| edge_id == &edge.edge_id)
837                })
838                .cloned()
839                .collect::<Vec<_>>();
840            if matched_event_edges.len() > 1 {
841                return Err(workflow_failure(
842                    request,
843                    WorkflowTraversalFailureReason::AmbiguousNextEdge,
844                    runtime_error(
845                        RuntimeErrorCode::ExecutionFailed,
846                        "workflow traversal found more than one event next edge",
847                        json!({"node_id": node.node_id}),
848                    ),
849                    visited,
850                    traversed,
851                    emitted,
852                    event_evidence,
853                    warnings,
854                ));
855            }
856            if let Some(edge) = matched_event_edges.into_iter().next() {
857                traversed.push(edge_record(&edge));
858                current = edge.to;
859                step_index += 1;
860                continue;
861            }
862
863            if workflow.definition.terminal_nodes.contains(&node.node_id) {
864                let final_output =
865                    final_workflow_output(&state, &workflow.definition.output_projection);
866                if let Err(error) = validate_payload_against_contract(
867                    &final_output,
868                    &workflow.definition.outputs.schema,
869                    RuntimeErrorCode::OutputValidationFailed,
870                    "workflow output does not satisfy the workflow output contract",
871                ) {
872                    return Err(workflow_failure(
873                        request,
874                        WorkflowTraversalFailureReason::WorkflowInvalid,
875                        error,
876                        visited,
877                        traversed,
878                        emitted,
879                        event_evidence,
880                        warnings,
881                    ));
882                }
883
884                let evidence = WorkflowTraversalEvidence {
885                    kind: WORKFLOW_EVIDENCE_KIND.to_string(),
886                    schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
887                    trace_id: format!("workflow_trace_{}", request.request_id),
888                    request_id: request.request_id.clone(),
889                    workflow_id: workflow.definition.id.clone(),
890                    workflow_version: workflow.definition.version.clone(),
891                    governing_spec: WORKFLOW_GOVERNING_SPEC.to_string(),
892                    visited_nodes: visited,
893                    traversed_edges: traversed,
894                    emitted_events: emitted,
895                    waiting_edges: event_evidence.waiting_edges,
896                    event_match_records: event_evidence.match_records,
897                    event_wake_decisions: event_evidence.wake_decisions,
898                    event_consumptions: event_evidence.consumptions,
899                    result: WorkflowTraversalResult {
900                        status: WorkflowTraversalStatus::Completed,
901                        failure_reason: None,
902                    },
903                };
904
905                return Ok(WorkflowExecutionOutcome {
906                    result: WorkflowExecutionResult {
907                        kind: WORKFLOW_REQUEST_KIND.to_string(),
908                        schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
909                        request_id: request.request_id.clone(),
910                        workflow_id: workflow.definition.id.clone(),
911                        workflow_version: workflow.definition.version.clone(),
912                        status: WorkflowTraversalStatus::Completed,
913                        output: Some(final_output),
914                        error: None,
915                        warnings,
916                    },
917                    evidence,
918                });
919            }
920
921            let failure_reason = if outgoing
922                .iter()
923                .any(|edge| edge.trigger == WorkflowEdgeTrigger::Event)
924            {
925                WorkflowTraversalFailureReason::MissingRequiredEvent
926            } else {
927                WorkflowTraversalFailureReason::TerminalNodeNotReached
928            };
929
930            return Err(workflow_failure(
931                request,
932                failure_reason,
933                runtime_error(
934                    RuntimeErrorCode::ExecutionFailed,
935                    "workflow traversal could not reach a valid next node",
936                    json!({"node_id": node.node_id}),
937                ),
938                visited,
939                traversed,
940                emitted,
941                event_evidence,
942                warnings,
943            ));
944        }
945    }
946}
947
948fn validate_workflow_request(request: &WorkflowExecutionRequest) -> Option<RuntimeError> {
949    if request.kind != WORKFLOW_REQUEST_KIND {
950        return Some(runtime_error(
951            RuntimeErrorCode::RequestInvalid,
952            "kind must equal workflow_execution_request",
953            json!({"path": "$.kind"}),
954        ));
955    }
956    if request.schema_version != WORKFLOW_SCHEMA_VERSION {
957        return Some(runtime_error(
958            RuntimeErrorCode::RequestInvalid,
959            "schema_version must equal 1.0.0",
960            json!({"path": "$.schema_version"}),
961        ));
962    }
963    if request.governing_spec != WORKFLOW_GOVERNING_SPEC {
964        return Some(runtime_error(
965            RuntimeErrorCode::RequestInvalid,
966            "governing_spec must equal 007-workflow-registry-traversal",
967            json!({"path": "$.governing_spec"}),
968        ));
969    }
970    if request.request_id.trim().is_empty()
971        || request.workflow_id.trim().is_empty()
972        || request.workflow_version.trim().is_empty()
973    {
974        return Some(runtime_error(
975            RuntimeErrorCode::RequestInvalid,
976            "request_id, workflow_id, and workflow_version must be non-empty",
977            json!({"path": "$"}),
978        ));
979    }
980    None
981}
982
983fn map_workflow_lookup_scope(scope: WorkflowLookupScope) -> LookupScope {
984    match scope {
985        WorkflowLookupScope::PublicOnly => LookupScope::PublicOnly,
986        WorkflowLookupScope::PreferPrivate => LookupScope::PreferPrivate,
987    }
988}
989
990fn workflow_state(input: &Value) -> Map<String, Value> {
991    match input {
992        Value::Object(map) => map.clone(),
993        other => {
994            let mut map = Map::new();
995            map.insert("input".to_string(), other.clone());
996            map
997        }
998    }
999}
1000
1001fn node_input(state: &Map<String, Value>, node: &WorkflowNode) -> Value {
1002    let mut input = Map::new();
1003    for key in &node.input.from_workflow_input {
1004        if let Some(value) = state.get(key) {
1005            input.insert(key.clone(), value.clone());
1006        }
1007    }
1008    Value::Object(input)
1009}
1010
1011fn update_state(state: &mut Map<String, Value>, node: &WorkflowNode, output: &Value) {
1012    let Value::Object(object) = output else {
1013        return;
1014    };
1015    for key in &node.output.to_workflow_state {
1016        if let Some(value) = object.get(key) {
1017            state.insert(key.clone(), value.clone());
1018        }
1019    }
1020    if let Some(namespace) = &node.output.publish_to_state_as {
1021        state.insert(namespace.clone(), output.clone());
1022    }
1023}
1024
1025fn final_workflow_output(state: &Map<String, Value>, output_projection: &[String]) -> Value {
1026    if output_projection.is_empty() {
1027        return Value::Object(state.clone());
1028    }
1029    let mut projected = Map::new();
1030    for key in output_projection {
1031        if let Some(value) = state.get(key) {
1032            projected.insert(key.clone(), value.clone());
1033        }
1034    }
1035    Value::Object(projected)
1036}
1037
1038fn waiting_edge_contexts(
1039    workflow_execution_id: &str,
1040    edges: &[WorkflowEdge],
1041) -> Vec<WaitingWorkflowEdgeContext> {
1042    edges
1043        .iter()
1044        .filter_map(|edge| {
1045            Some(WaitingWorkflowEdgeContext {
1046                workflow_execution_id: workflow_execution_id.to_string(),
1047                edge_id: edge.edge_id.clone(),
1048                from_node_id: edge.from.clone(),
1049                to_node_id: edge.to.clone(),
1050                event_ref: edge.event.clone()?,
1051                predicate: edge.predicate.clone(),
1052            })
1053        })
1054        .collect()
1055}
1056
1057/// A waiting edge's subscription to `EventBroker` could not be established
1058/// or polled (spec 099 FR-007/FR-008).
1059#[derive(Debug, Clone, PartialEq, Eq)]
1060enum BrokerQueryFailure {
1061    /// The declared event type is not registered in `EventBroker`'s catalog
1062    /// (FR-007).
1063    UnregisteredEventType(String),
1064    /// `EventBroker` was unreachable or internally failing (FR-008).
1065    BrokerUnavailable(String),
1066}
1067
1068/// Subscribes to `EventBroker` for every distinct event type referenced by
1069/// `waiting_edges`, drains all currently available events for each, and
1070/// cancels the subscription immediately after (spec 099 FR-001/FR-002).
1071///
1072/// Event types are visited in a fixed, sorted order, and each type's events
1073/// are appended in delivery order, so the resulting candidate pool is
1074/// deterministic and explainable (FR-003).
1075fn poll_broker_events_for_waiting_edges(
1076    broker: &dyn EventBroker,
1077    waiting_edges: &[WaitingWorkflowEdgeContext],
1078) -> Result<(Vec<EmittedEventRecord>, Vec<RuntimeWarning>), BrokerQueryFailure> {
1079    let mut event_types: Vec<&str> = waiting_edges
1080        .iter()
1081        .map(|edge| edge.event_ref.event_id.as_str())
1082        .collect();
1083    event_types.sort_unstable();
1084    event_types.dedup();
1085
1086    let mut records = Vec::new();
1087    let mut warnings = Vec::new();
1088
1089    for event_type in event_types {
1090        let subscription = broker
1091            .subscribe(event_type, "0")
1092            .map_err(|error| match error {
1093                EventError::UnregisteredEventType(event_type) => {
1094                    BrokerQueryFailure::UnregisteredEventType(event_type)
1095                }
1096                other => BrokerQueryFailure::BrokerUnavailable(other.to_string()),
1097            })?;
1098
1099        let mut delivered = Vec::new();
1100        let poll_failure = loop {
1101            match broker.poll(&subscription.subscription_id, EVENT_BROKER_POLL_BATCH) {
1102                Ok(batch) => {
1103                    let batch_len = batch.events.len();
1104                    delivered.extend(batch.events);
1105                    if batch_len < EVENT_BROKER_POLL_BATCH {
1106                        break None;
1107                    }
1108                }
1109                Err(error) => break Some(BrokerQueryFailure::BrokerUnavailable(error.to_string())),
1110            }
1111        };
1112
1113        if let Err(cancel_error) = broker.cancel(&subscription.subscription_id) {
1114            warnings.push(RuntimeWarning {
1115                code: "event_broker_subscription_cleanup_failed".to_string(),
1116                message: format!(
1117                    "failed to cancel workflow event-driven edge subscription {}: {cancel_error}",
1118                    subscription.subscription_id
1119                ),
1120            });
1121        }
1122
1123        if let Some(failure) = poll_failure {
1124            return Err(failure);
1125        }
1126
1127        for broker_event in delivered {
1128            records.push(EmittedEventRecord {
1129                record_id: format!("broker:{event_type}:{}", broker_event.cursor),
1130                event: EventReference {
1131                    event_id: event_type.to_string(),
1132                    version: broker_event.event.version.clone(),
1133                },
1134                payload: Some(broker_event.event.data.clone()),
1135                source: Some(BrokerEventSource {
1136                    subscription_id: subscription.subscription_id.clone(),
1137                    cursor: broker_event.cursor.clone(),
1138                }),
1139            });
1140        }
1141    }
1142
1143    Ok((records, warnings))
1144}
1145
1146#[allow(clippy::too_many_lines)]
1147fn evaluate_event_driven_edges(
1148    waiting_edges: &[WaitingWorkflowEdgeContext],
1149    emitted_events: &[EmittedEventRecord],
1150    consumed_event_edges: &mut BTreeSet<String>,
1151    record_prefix: &str,
1152) -> EventDrivenEvaluationOutcome {
1153    let mut ordered_waiting_edges = waiting_edges.to_vec();
1154    ordered_waiting_edges.sort_by(|left, right| {
1155        left.workflow_execution_id
1156            .cmp(&right.workflow_execution_id)
1157            .then_with(|| left.edge_id.cmp(&right.edge_id))
1158    });
1159
1160    let mut outcome = EventDrivenEvaluationOutcome::default();
1161    if emitted_events.is_empty() {
1162        outcome
1163            .evidence
1164            .match_records
1165            .extend(ordered_waiting_edges.iter().map(|edge| EventMatchRecord {
1166                event_id: edge.event_ref.event_id.clone(),
1167                event_version: edge.event_ref.version.clone(),
1168                edge_id: edge.edge_id.clone(),
1169                match_result: EventMatchResult::NotMatched,
1170                predicate_result: None,
1171                rejection_reason: Some("required event was not emitted".to_string()),
1172                recorded_at: format!("{record_prefix}:no_event:{}", edge.edge_id),
1173                subscription_id: None,
1174                cursor: None,
1175            }));
1176        return outcome;
1177    }
1178
1179    let mut wake_order = 1;
1180    for (event_index, emitted_event) in emitted_events.iter().enumerate() {
1181        let subscription_id = emitted_event
1182            .source
1183            .as_ref()
1184            .map(|source| source.subscription_id.clone());
1185        let cursor = emitted_event
1186            .source
1187            .as_ref()
1188            .map(|source| source.cursor.clone());
1189        for waiting_edge in &ordered_waiting_edges {
1190            let match_recorded_at = format!(
1191                "{record_prefix}:event:{event_index}:match:{}",
1192                waiting_edge.edge_id
1193            );
1194            if emitted_event.event != waiting_edge.event_ref {
1195                outcome.evidence.match_records.push(EventMatchRecord {
1196                    event_id: emitted_event.event.event_id.clone(),
1197                    event_version: emitted_event.event.version.clone(),
1198                    edge_id: waiting_edge.edge_id.clone(),
1199                    match_result: EventMatchResult::NotMatched,
1200                    predicate_result: None,
1201                    rejection_reason: Some(
1202                        "event id/version did not match the waiting edge".to_string(),
1203                    ),
1204                    recorded_at: match_recorded_at,
1205                    subscription_id: subscription_id.clone(),
1206                    cursor: cursor.clone(),
1207                });
1208                continue;
1209            }
1210
1211            if let Some(predicate) = waiting_edge.predicate.as_ref() {
1212                let predicate_passed =
1213                    event_payload_field(emitted_event.payload.as_ref(), &predicate.field)
1214                        .is_some_and(|value| value == &predicate.equals);
1215                if !predicate_passed {
1216                    outcome.evidence.match_records.push(EventMatchRecord {
1217                        event_id: emitted_event.event.event_id.clone(),
1218                        event_version: emitted_event.event.version.clone(),
1219                        edge_id: waiting_edge.edge_id.clone(),
1220                        match_result: EventMatchResult::NotMatched,
1221                        predicate_result: Some(EventPredicateResult::Failed),
1222                        rejection_reason: Some(
1223                            "event predicate did not match the emitted payload".to_string(),
1224                        ),
1225                        recorded_at: match_recorded_at,
1226                        subscription_id: subscription_id.clone(),
1227                        cursor: cursor.clone(),
1228                    });
1229                    continue;
1230                }
1231            }
1232
1233            let consumption_key = format!(
1234                "{}|{}|{}",
1235                emitted_event.record_id, waiting_edge.workflow_execution_id, waiting_edge.edge_id
1236            );
1237            if consumed_event_edges.contains(&consumption_key) {
1238                outcome.evidence.match_records.push(EventMatchRecord {
1239                    event_id: emitted_event.event.event_id.clone(),
1240                    event_version: emitted_event.event.version.clone(),
1241                    edge_id: waiting_edge.edge_id.clone(),
1242                    match_result: EventMatchResult::AlreadyConsumed,
1243                    predicate_result: waiting_edge
1244                        .predicate
1245                        .as_ref()
1246                        .map(|_| EventPredicateResult::Passed),
1247                    rejection_reason: Some(
1248                        "event record was already consumed for this waiting edge".to_string(),
1249                    ),
1250                    recorded_at: match_recorded_at,
1251                    subscription_id: subscription_id.clone(),
1252                    cursor: cursor.clone(),
1253                });
1254                continue;
1255            }
1256
1257            consumed_event_edges.insert(consumption_key);
1258            outcome.evidence.match_records.push(EventMatchRecord {
1259                event_id: emitted_event.event.event_id.clone(),
1260                event_version: emitted_event.event.version.clone(),
1261                edge_id: waiting_edge.edge_id.clone(),
1262                match_result: EventMatchResult::Matched,
1263                predicate_result: waiting_edge
1264                    .predicate
1265                    .as_ref()
1266                    .map(|_| EventPredicateResult::Passed),
1267                rejection_reason: None,
1268                recorded_at: match_recorded_at.clone(),
1269                subscription_id: subscription_id.clone(),
1270                cursor: cursor.clone(),
1271            });
1272            outcome.taken_edge_ids.push(waiting_edge.edge_id.clone());
1273            let wake_recorded_at = format!(
1274                "{record_prefix}:event:{event_index}:wake:{}",
1275                waiting_edge.edge_id
1276            );
1277            outcome.evidence.wake_decisions.push(EventWakeDecision {
1278                decision_type: "event_wake".to_string(),
1279                event_id: emitted_event.event.event_id.clone(),
1280                event_version: emitted_event.event.version.clone(),
1281                edge_id: waiting_edge.edge_id.clone(),
1282                workflow_execution_id: waiting_edge.workflow_execution_id.clone(),
1283                wake_order,
1284                result: EventWakeDecisionResult::Taken,
1285                recorded_at: wake_recorded_at.clone(),
1286                subscription_id: subscription_id.clone(),
1287                cursor: cursor.clone(),
1288            });
1289            outcome.evidence.consumptions.push(EventConsumptionRecord {
1290                event_id: emitted_event.event.event_id.clone(),
1291                event_version: emitted_event.event.version.clone(),
1292                edge_id: waiting_edge.edge_id.clone(),
1293                workflow_execution_id: waiting_edge.workflow_execution_id.clone(),
1294                consumed_at: wake_recorded_at,
1295                subscription_id: subscription_id.clone(),
1296                cursor: cursor.clone(),
1297            });
1298            wake_order += 1;
1299        }
1300    }
1301    outcome
1302}
1303
1304fn event_payload_field<'a>(payload: Option<&'a Value>, field: &str) -> Option<&'a Value> {
1305    let payload = payload?;
1306    let path = field.strip_prefix("payload.").unwrap_or(field);
1307    if path == "payload" || path.is_empty() {
1308        return Some(payload);
1309    }
1310
1311    let mut current = payload;
1312    for segment in path.split('.') {
1313        let Value::Object(map) = current else {
1314            return None;
1315        };
1316        current = map.get(segment)?;
1317    }
1318    Some(current)
1319}
1320
1321fn edge_record(edge: &WorkflowEdge) -> WorkflowTraversalEdgeRecord {
1322    WorkflowTraversalEdgeRecord {
1323        edge_id: edge.edge_id.clone(),
1324        from: edge.from.clone(),
1325        to: edge.to.clone(),
1326        trigger: match edge.trigger {
1327            WorkflowEdgeTrigger::Direct => WorkflowTraversalTrigger::Direct,
1328            WorkflowEdgeTrigger::Event => WorkflowTraversalTrigger::Event,
1329        },
1330        event: edge.event.clone(),
1331    }
1332}
1333
1334#[allow(clippy::too_many_arguments)]
1335fn workflow_failure(
1336    request: &WorkflowExecutionRequest,
1337    failure_reason: WorkflowTraversalFailureReason,
1338    error: RuntimeError,
1339    visited_nodes: Vec<WorkflowTraversalStepRecord>,
1340    traversed_edges: Vec<WorkflowTraversalEdgeRecord>,
1341    emitted_events: Vec<EventReference>,
1342    event_evidence: WorkflowEventEvidenceBundle,
1343    warnings: Vec<RuntimeWarning>,
1344) -> WorkflowExecutionOutcome {
1345    let evidence = WorkflowTraversalEvidence {
1346        kind: WORKFLOW_EVIDENCE_KIND.to_string(),
1347        schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
1348        trace_id: format!("workflow_trace_{}", request.request_id),
1349        request_id: request.request_id.clone(),
1350        workflow_id: request.workflow_id.clone(),
1351        workflow_version: request.workflow_version.clone(),
1352        governing_spec: WORKFLOW_GOVERNING_SPEC.to_string(),
1353        visited_nodes,
1354        traversed_edges,
1355        emitted_events,
1356        waiting_edges: event_evidence.waiting_edges,
1357        event_match_records: event_evidence.match_records,
1358        event_wake_decisions: event_evidence.wake_decisions,
1359        event_consumptions: event_evidence.consumptions,
1360        result: WorkflowTraversalResult {
1361            status: WorkflowTraversalStatus::Error,
1362            failure_reason: Some(failure_reason),
1363        },
1364    };
1365
1366    WorkflowExecutionOutcome {
1367        result: WorkflowExecutionResult {
1368            kind: WORKFLOW_REQUEST_KIND.to_string(),
1369            schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
1370            request_id: request.request_id.clone(),
1371            workflow_id: request.workflow_id.clone(),
1372            workflow_version: request.workflow_version.clone(),
1373            status: WorkflowTraversalStatus::Error,
1374            output: None,
1375            error: Some(error),
1376            warnings,
1377        },
1378        evidence,
1379    }
1380}
1381
1382#[cfg(test)]
1383mod tests {
1384    use super::*;
1385    use crate::events;
1386    use crate::events::InProcessBroker;
1387    use crate::security::RuntimeSecurityConfig;
1388    use crate::{
1389        CandidateCollectionRecord, LocalExecutionFailure, LocalExecutionFailureCode,
1390        LocalExecutionOutput, RuntimeContext, RuntimeIntent, RuntimeLookup, RuntimeLookupScope,
1391        RuntimeRequest, RuntimeResultStatus, SelectionRecord,
1392    };
1393    use serde_json::json;
1394    use std::sync::Arc;
1395    use traverse_contracts::{
1396        BinaryFormat as ContractBinaryFormat, CapabilityContract, Condition, Entrypoint,
1397        EntrypointKind, EventReference, EvidenceStatus, EvidenceType, Execution,
1398        ExecutionConstraints, ExecutionTarget, FilesystemAccess, HostApiAccess, IdReference,
1399        Lifecycle, NetworkAccess, Owner, Provenance, ProvenanceSource, SchemaContainer,
1400        ServiceType, SideEffect, SideEffectKind, ValidationEvidence,
1401    };
1402    use traverse_registry::{
1403        ArtifactDigests, ArtifactSignature, ArtifactSignatureScheme, BinaryFormat, BinaryReference,
1404        CapabilityArtifactRecord, CapabilityRegistration, CapabilityRegistry,
1405        ComposabilityMetadata, CompositionKind, CompositionPattern, ImplementationKind,
1406        RegistryProvenance, RegistryScope, SourceKind, SourceReference, WorkflowDefinition,
1407        WorkflowEdge, WorkflowEdgeTrigger, WorkflowNode, WorkflowNodeInput, WorkflowNodeOutput,
1408        WorkflowRegistration, WorkflowRegistry, WorkflowRegistryRecord, workflow_artifact_record,
1409    };
1410
1411    #[test]
1412    fn workflow_request_validation_rejects_invalid_guards() {
1413        let mut request = valid_workflow_request();
1414        request.kind = "bad".to_string();
1415        assert_eq!(
1416            validate_workflow_request(&request).map(|error| error.code),
1417            Some(RuntimeErrorCode::RequestInvalid)
1418        );
1419
1420        let mut request = valid_workflow_request();
1421        request.schema_version = "2.0.0".to_string();
1422        assert_eq!(
1423            validate_workflow_request(&request).map(|error| error.code),
1424            Some(RuntimeErrorCode::RequestInvalid)
1425        );
1426
1427        let mut request = valid_workflow_request();
1428        request.governing_spec = "bad".to_string();
1429        assert_eq!(
1430            validate_workflow_request(&request).map(|error| error.code),
1431            Some(RuntimeErrorCode::RequestInvalid)
1432        );
1433
1434        let mut request = valid_workflow_request();
1435        request.request_id.clear();
1436        assert_eq!(
1437            validate_workflow_request(&request).map(|error| error.code),
1438            Some(RuntimeErrorCode::RequestInvalid)
1439        );
1440    }
1441
1442    #[test]
1443    fn workflow_helpers_cover_state_and_edge_paths() {
1444        let scalar = workflow_state(&json!("value"));
1445        assert_eq!(scalar.get("input"), Some(&json!("value")));
1446
1447        let mut state = workflow_state(&json!({"comment_text": "hello"}));
1448        let node = WorkflowNode {
1449            node_id: "node".to_string(),
1450            capability_id: "content.comments.create-comment-draft".to_string(),
1451            capability_version: "1.0.0".to_string(),
1452            input: WorkflowNodeInput {
1453                from_workflow_input: vec!["comment_text".to_string(), "missing".to_string()],
1454            },
1455            output: WorkflowNodeOutput {
1456                to_workflow_state: vec!["draft_id".to_string()],
1457                publish_to_state_as: None,
1458            },
1459        };
1460        assert_eq!(node_input(&state, &node), json!({"comment_text": "hello"}));
1461        update_state(&mut state, &node, &json!({"draft_id": "draft-1"}));
1462        assert_eq!(state.get("draft_id"), Some(&json!("draft-1")));
1463        update_state(&mut state, &node, &json!("not-an-object"));
1464
1465        let edge = WorkflowEdge {
1466            edge_id: "edge".to_string(),
1467            from: "a".to_string(),
1468            to: "b".to_string(),
1469            trigger: WorkflowEdgeTrigger::Event,
1470            event: Some(EventReference {
1471                event_id: "content.comments.draft-created".to_string(),
1472                version: "1.0.0".to_string(),
1473            }),
1474            predicate: None,
1475        };
1476        assert_eq!(edge_record(&edge).trigger, WorkflowTraversalTrigger::Event);
1477        assert_eq!(
1478            map_workflow_lookup_scope(WorkflowLookupScope::PreferPrivate),
1479            LookupScope::PreferPrivate
1480        );
1481        assert_eq!(
1482            event_payload_field(Some(&json!({"severity": "normal"})), "payload.severity"),
1483            Some(&json!("normal"))
1484        );
1485        assert_eq!(
1486            event_payload_field(Some(&json!({"severity": "normal"})), "payload"),
1487            Some(&json!({"severity": "normal"}))
1488        );
1489        assert_eq!(
1490            event_payload_field(Some(&json!("normal")), "payload.severity"),
1491            None
1492        );
1493    }
1494
1495    #[test]
1496    #[allow(clippy::too_many_lines)]
1497    fn executes_workflow_deterministically_and_supports_workflow_backed_capabilities() {
1498        let workflow_registry = workflow_registry_fixture();
1499        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
1500            .with_workflow_registry(workflow_registry)
1501            .with_security_config(RuntimeSecurityConfig::development());
1502
1503        let workflow = runtime.execute_workflow(valid_workflow_request());
1504        assert_eq!(workflow.result.status, WorkflowTraversalStatus::Completed);
1505        assert_eq!(
1506            workflow.result.output,
1507            Some(
1508                json!({"comment_text": "hello", "draft_id": "draft-1", "comment_id": "comment-1"})
1509            )
1510        );
1511        assert_eq!(workflow.evidence.visited_nodes.len(), 3);
1512        assert_eq!(workflow.evidence.traversed_edges.len(), 2);
1513        assert_eq!(workflow.evidence.waiting_edges.len(), 2);
1514        assert_eq!(workflow.evidence.event_wake_decisions.len(), 2);
1515        assert_eq!(workflow.evidence.event_consumptions.len(), 2);
1516        assert!(
1517            workflow
1518                .evidence
1519                .event_match_records
1520                .iter()
1521                .all(|record| record.match_result == EventMatchResult::Matched)
1522        );
1523
1524        let mut composed_registry = capability_registry_fixture();
1525        register_capability_ok(
1526            &mut composed_registry,
1527            CapabilityRegistration {
1528                scope: RegistryScope::Public,
1529                contract: capability_contract(
1530                    "content.comments.publish-comment",
1531                    vec![],
1532                    json!({
1533                        "type": "object",
1534                        "properties": { "comment_text": { "type": "string" } },
1535                        "required": ["comment_text"],
1536                        "additionalProperties": true
1537                    }),
1538                    json!({
1539                        "type": "object",
1540                        "properties": { "comment_id": { "type": "string" } },
1541                        "required": ["comment_id"],
1542                        "additionalProperties": true
1543                    }),
1544                ),
1545                contract_path: "contracts/publish-comment.json".to_string(),
1546                artifact: workflow_artifact_record(
1547                    "content.comments.publish-comment",
1548                    "1.0.0",
1549                    "artifact-workflow",
1550                ),
1551                registered_at: "2026-03-27T00:10:00Z".to_string(),
1552                tags: vec!["comments".to_string()],
1553                composability: ComposabilityMetadata {
1554                    kind: CompositionKind::Composite,
1555                    patterns: vec![
1556                        CompositionPattern::Sequential,
1557                        CompositionPattern::EventDriven,
1558                    ],
1559                    provides: vec!["published-comment".to_string()],
1560                    requires: vec!["draft".to_string()],
1561                },
1562                governing_spec: "005-capability-registry".to_string(),
1563                validator_version: "validator".to_string(),
1564            },
1565        );
1566
1567        let runtime = Runtime::new(composed_registry, WorkflowExecutor)
1568            .with_workflow_registry(workflow_registry_fixture())
1569            .with_security_config(RuntimeSecurityConfig::development());
1570        let result = runtime.execute(RuntimeRequest {
1571            kind: "runtime_request".to_string(),
1572            schema_version: "1.0.0".to_string(),
1573            request_id: "request-workflow".to_string(),
1574            intent: RuntimeIntent {
1575                capability_id: Some("content.comments.publish-comment".to_string()),
1576                capability_version: Some("1.0.0".to_string()),
1577                version_range: None,
1578                intent_key: None,
1579            },
1580            input: json!({"comment_text": "hello"}),
1581            lookup: RuntimeLookup {
1582                scope: RuntimeLookupScope::PublicOnly,
1583                allow_ambiguity: false,
1584            },
1585            context: RuntimeContext {
1586                requested_target: crate::PlacementTarget::Local,
1587                correlation_id: None,
1588                caller: None,
1589                traceparent: None,
1590                tracestate: None,
1591                metadata: None,
1592                identity: None,
1593            },
1594            governing_spec: "006-runtime-request-execution".to_string(),
1595        });
1596        assert_eq!(result.result.status, RuntimeResultStatus::Completed);
1597        assert_eq!(
1598            result.result.output,
1599            Some(
1600                json!({"comment_text": "hello", "draft_id": "draft-1", "comment_id": "comment-1"})
1601            )
1602        );
1603        assert!(
1604            result
1605                .result
1606                .warnings
1607                .iter()
1608                .any(|warning| warning.code == "unsigned_local_dev_artifact")
1609        );
1610    }
1611
1612    /// Spec 101-local-executor-event-emission FR-005 / acceptance scenario 2:
1613    /// a workflow node's declared, `Subscribable`-gated event both satisfies
1614    /// same-execution waiting edges (already covered by
1615    /// `executes_workflow_deterministically_and_supports_workflow_backed_capabilities`)
1616    /// AND is published to `EventBroker` for external consumers, mirroring
1617    /// `PlacementRouter` Step 5's publish semantics.
1618    #[test]
1619    fn workflow_node_emitted_events_are_published_to_event_broker() {
1620        let broker = event_catalog_broker_fixture("content.comments.draft-created", "1.0.0");
1621        let subscription = broker
1622            .subscribe("content.comments.draft-created", "0")
1623            .unwrap_or_else(|error| unreachable!("{error:?}"));
1624
1625        let workflow_registry = workflow_registry_fixture();
1626        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
1627            .with_workflow_registry(workflow_registry)
1628            .with_security_config(RuntimeSecurityConfig::development())
1629            .with_event_broker(broker.clone());
1630
1631        let workflow = runtime.execute_workflow(valid_workflow_request());
1632        assert_eq!(workflow.result.status, WorkflowTraversalStatus::Completed);
1633
1634        let poll = broker
1635            .poll(&subscription.subscription_id, 10)
1636            .unwrap_or_else(|error| unreachable!("{error:?}"));
1637        assert_eq!(poll.events.len(), 1);
1638        assert_eq!(
1639            poll.events[0].event.event_type,
1640            "content.comments.draft-created"
1641        );
1642    }
1643
1644    /// Spec 101-local-executor-event-emission FR-005 / acceptance scenario 7:
1645    /// an `EventBroker` publish failure at workflow-node publish time is
1646    /// recorded (best-effort, matching `PlacementRouter` Step 5) but does not
1647    /// fail the workflow step — a broker outage does not fail traversal.
1648    #[test]
1649    fn workflow_node_publish_failure_does_not_fail_workflow_step() {
1650        let workflow_registry = workflow_registry_fixture();
1651        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
1652            .with_workflow_registry(workflow_registry)
1653            .with_security_config(RuntimeSecurityConfig::development())
1654            .with_event_broker(Arc::new(AlwaysFailingBroker));
1655
1656        let workflow = runtime.execute_workflow(valid_workflow_request());
1657        assert_eq!(workflow.result.status, WorkflowTraversalStatus::Completed);
1658    }
1659
1660    #[test]
1661    fn workflow_failures_cover_not_found_missing_events_and_step_failures() {
1662        let workflow_registry = workflow_registry_fixture();
1663        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
1664            .with_workflow_registry(workflow_registry);
1665
1666        let mut missing_request = valid_workflow_request();
1667        missing_request.workflow_id = "missing".to_string();
1668        let missing = runtime.execute_workflow(missing_request);
1669        assert_eq!(
1670            missing.evidence.result.failure_reason,
1671            Some(WorkflowTraversalFailureReason::WorkflowNotFound)
1672        );
1673
1674        let workflow_registry = workflow_registry_fixture();
1675        // The event type is registered (spec 099: EventBroker's catalog is
1676        // consulted before a waiting edge is declared unsatisfiable) but
1677        // never published, so this still exercises the genuine
1678        // "not-yet-happened" MissingRequiredEvent path rather than
1679        // EventSubscriptionFailed's unregistered-type path.
1680        let runtime = Runtime::new(capability_registry_fixture(), MissingEventWorkflowExecutor)
1681            .with_workflow_registry(workflow_registry)
1682            .with_security_config(RuntimeSecurityConfig::development())
1683            .with_event_broker(event_catalog_broker_fixture(
1684                "content.comments.validated",
1685                "1.0.0",
1686            ));
1687        let missing_event = runtime.execute_workflow(valid_workflow_request());
1688        assert_eq!(
1689            missing_event.evidence.result.failure_reason,
1690            Some(WorkflowTraversalFailureReason::MissingRequiredEvent)
1691        );
1692
1693        let runtime = Runtime::new(capability_registry_fixture(), FailingWorkflowExecutor)
1694            .with_workflow_registry(workflow_registry_fixture())
1695            .with_security_config(RuntimeSecurityConfig::development());
1696        let failed = runtime.execute_workflow(valid_workflow_request());
1697        assert_eq!(
1698            failed.evidence.result.failure_reason,
1699            Some(WorkflowTraversalFailureReason::StepExecutionFailed)
1700        );
1701    }
1702
1703    #[test]
1704    fn event_driven_helpers_are_deterministic_and_prevent_duplicate_consumption() {
1705        let event = EmittedEventRecord {
1706            record_id: "event_record_0".to_string(),
1707            event: EventReference {
1708                event_id: "content.comments.validated".to_string(),
1709                version: "1.0.0".to_string(),
1710            },
1711            payload: Some(json!({"severity": "normal"})),
1712            source: None,
1713        };
1714        let waiting_edges = vec![
1715            WaitingWorkflowEdgeContext {
1716                workflow_execution_id: "wf_exec_b".to_string(),
1717                edge_id: "edge_b".to_string(),
1718                from_node_id: "from".to_string(),
1719                to_node_id: "to".to_string(),
1720                event_ref: event.event.clone(),
1721                predicate: Some(WorkflowEdgePredicate {
1722                    field: "payload.severity".to_string(),
1723                    equals: json!("normal"),
1724                }),
1725            },
1726            WaitingWorkflowEdgeContext {
1727                workflow_execution_id: "wf_exec_a".to_string(),
1728                edge_id: "edge_a".to_string(),
1729                from_node_id: "from".to_string(),
1730                to_node_id: "to".to_string(),
1731                event_ref: event.event.clone(),
1732                predicate: None,
1733            },
1734        ];
1735        let mut consumed = BTreeSet::new();
1736        let first = evaluate_event_driven_edges(
1737            &waiting_edges,
1738            std::slice::from_ref(&event),
1739            &mut consumed,
1740            "trace",
1741        );
1742        assert_eq!(
1743            first.taken_edge_ids,
1744            vec!["edge_a".to_string(), "edge_b".to_string()]
1745        );
1746        assert_eq!(
1747            first
1748                .evidence
1749                .wake_decisions
1750                .iter()
1751                .map(|decision| (&decision.workflow_execution_id, decision.wake_order))
1752                .collect::<Vec<_>>(),
1753            vec![(&"wf_exec_a".to_string(), 1), (&"wf_exec_b".to_string(), 2)]
1754        );
1755
1756        let second = evaluate_event_driven_edges(&waiting_edges, &[event], &mut consumed, "trace");
1757        assert!(second.taken_edge_ids.is_empty());
1758        assert!(
1759            second
1760                .evidence
1761                .match_records
1762                .iter()
1763                .all(|record| record.match_result == EventMatchResult::AlreadyConsumed)
1764        );
1765    }
1766
1767    #[test]
1768    fn event_driven_helpers_reject_non_matching_predicates() {
1769        let waiting_edges = vec![WaitingWorkflowEdgeContext {
1770            workflow_execution_id: "wf_exec_1".to_string(),
1771            edge_id: "edge_predicate".to_string(),
1772            from_node_id: "assess".to_string(),
1773            to_node_id: "validate".to_string(),
1774            event_ref: EventReference {
1775                event_id: "expedition.conditions.summary-assessed".to_string(),
1776                version: "1.0.0".to_string(),
1777            },
1778            predicate: Some(WorkflowEdgePredicate {
1779                field: "payload.severity".to_string(),
1780                equals: json!("high"),
1781            }),
1782        }];
1783        let emitted = vec![EmittedEventRecord {
1784            record_id: "event_record_0".to_string(),
1785            event: EventReference {
1786                event_id: "expedition.conditions.summary-assessed".to_string(),
1787                version: "1.0.0".to_string(),
1788            },
1789            payload: Some(json!({"severity": "normal"})),
1790            source: None,
1791        }];
1792        let outcome =
1793            evaluate_event_driven_edges(&waiting_edges, &emitted, &mut BTreeSet::new(), "trace");
1794        assert!(outcome.taken_edge_ids.is_empty());
1795        assert_eq!(
1796            outcome.evidence.match_records,
1797            vec![EventMatchRecord {
1798                event_id: "expedition.conditions.summary-assessed".to_string(),
1799                event_version: "1.0.0".to_string(),
1800                edge_id: "edge_predicate".to_string(),
1801                match_result: EventMatchResult::NotMatched,
1802                predicate_result: Some(EventPredicateResult::Failed),
1803                rejection_reason: Some(
1804                    "event predicate did not match the emitted payload".to_string()
1805                ),
1806                recorded_at: "trace:event:0:match:edge_predicate".to_string(),
1807                subscription_id: None,
1808                cursor: None,
1809            }]
1810        );
1811    }
1812
1813    #[test]
1814    fn event_driven_helpers_record_non_matching_event_identity() {
1815        let waiting_edges = vec![WaitingWorkflowEdgeContext {
1816            workflow_execution_id: "wf_exec_1".to_string(),
1817            edge_id: "edge_identity".to_string(),
1818            from_node_id: "create".to_string(),
1819            to_node_id: "validate".to_string(),
1820            event_ref: EventReference {
1821                event_id: "content.comments.validated".to_string(),
1822                version: "1.0.0".to_string(),
1823            },
1824            predicate: None,
1825        }];
1826        let emitted = vec![EmittedEventRecord {
1827            record_id: "event_record_0".to_string(),
1828            event: EventReference {
1829                event_id: "content.comments.other".to_string(),
1830                version: "1.0.0".to_string(),
1831            },
1832            payload: None,
1833            source: None,
1834        }];
1835        let outcome =
1836            evaluate_event_driven_edges(&waiting_edges, &emitted, &mut BTreeSet::new(), "trace");
1837        assert!(outcome.taken_edge_ids.is_empty());
1838        assert_eq!(
1839            outcome.evidence.match_records,
1840            vec![EventMatchRecord {
1841                event_id: "content.comments.other".to_string(),
1842                event_version: "1.0.0".to_string(),
1843                edge_id: "edge_identity".to_string(),
1844                match_result: EventMatchResult::NotMatched,
1845                predicate_result: None,
1846                rejection_reason: Some(
1847                    "event id/version did not match the waiting edge".to_string()
1848                ),
1849                recorded_at: "trace:event:0:match:edge_identity".to_string(),
1850                subscription_id: None,
1851                cursor: None,
1852            }]
1853        );
1854    }
1855
1856    fn event_catalog_broker_fixture(event_type: &str, version: &str) -> Arc<InProcessBroker> {
1857        let catalog = Arc::new(events::EventCatalog::new());
1858        catalog
1859            .register(events::EventCatalogEntry {
1860                event_type: event_type.to_string(),
1861                owner: "content.comments".to_string(),
1862                version: version.to_string(),
1863                lifecycle_status: events::LifecycleStatus::Active,
1864                consumer_count: 0,
1865            })
1866            .unwrap_or_else(|error| unreachable!("{error:?}"));
1867        Arc::new(InProcessBroker::new(catalog).unwrap_or_else(|error| unreachable!("{error:?}")))
1868    }
1869
1870    fn sample_traverse_event(event_type: &str, version: &str) -> events::TraverseEvent {
1871        events::TraverseEvent {
1872            id: "evt-fixture-1".to_string(),
1873            source: "traverse-runtime/content.comments.validate-comment".to_string(),
1874            event_type: event_type.to_string(),
1875            datacontenttype: "application/json".to_string(),
1876            time: "2026-08-06T00:00:00Z".to_string(),
1877            data: json!({"comment_id": "comment-1"}),
1878            owner: "content.comments".to_string(),
1879            version: version.to_string(),
1880            lifecycle_status: events::LifecycleStatus::Active,
1881            deduplication_id: None,
1882            ordering_scope: None,
1883            correlation_id: None,
1884            causation_id: None,
1885            subject_id: None,
1886            actor_id: None,
1887        }
1888    }
1889
1890    /// Spec 099 FR-001/FR-002: a waiting event-driven edge advances from an
1891    /// event published to `EventBroker` by another publisher, not just an
1892    /// event declared in the same execution's own node output.
1893    #[test]
1894    fn event_driven_edge_advances_from_broker_published_event() {
1895        let broker = event_catalog_broker_fixture("content.comments.validated", "1.0.0");
1896        broker
1897            .publish(sample_traverse_event("content.comments.validated", "1.0.0"))
1898            .unwrap_or_else(|error| unreachable!("{error:?}"));
1899
1900        let runtime = Runtime::new(capability_registry_fixture(), MissingEventWorkflowExecutor)
1901            .with_workflow_registry(workflow_registry_fixture())
1902            .with_security_config(RuntimeSecurityConfig::development())
1903            .with_event_broker(broker);
1904
1905        let outcome = runtime.execute_workflow(valid_workflow_request());
1906        assert_eq!(
1907            outcome.evidence.result.status,
1908            WorkflowTraversalStatus::Completed
1909        );
1910        assert_eq!(outcome.evidence.result.failure_reason, None);
1911
1912        let wake = outcome
1913            .evidence
1914            .event_wake_decisions
1915            .iter()
1916            .find(|decision| decision.edge_id == "validate_to_persist")
1917            .unwrap_or_else(|| unreachable!("expected a wake decision for validate_to_persist"));
1918        assert!(wake.subscription_id.is_some());
1919        assert!(wake.cursor.is_some());
1920
1921        let consumption = outcome
1922            .evidence
1923            .event_consumptions
1924            .iter()
1925            .find(|record| record.edge_id == "validate_to_persist")
1926            .unwrap_or_else(|| unreachable!("expected a consumption record"));
1927        assert!(consumption.subscription_id.is_some());
1928        assert!(consumption.cursor.is_some());
1929    }
1930
1931    /// Spec 099 FR-005: a match sourced from the current node's own emitted
1932    /// output (not `EventBroker`) still carries no subscription/cursor,
1933    /// distinguishing same-execution matches from broker-sourced ones.
1934    #[test]
1935    fn event_driven_edge_same_node_match_has_no_broker_provenance() {
1936        let broker = event_catalog_broker_fixture("content.comments.validated", "1.0.0");
1937        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
1938            .with_workflow_registry(workflow_registry_fixture())
1939            .with_security_config(RuntimeSecurityConfig::development())
1940            .with_event_broker(broker);
1941
1942        let outcome = runtime.execute_workflow(valid_workflow_request());
1943        assert_eq!(
1944            outcome.evidence.result.status,
1945            WorkflowTraversalStatus::Completed
1946        );
1947        let wake = outcome
1948            .evidence
1949            .event_wake_decisions
1950            .iter()
1951            .find(|decision| decision.edge_id == "validate_to_persist")
1952            .unwrap_or_else(|| unreachable!("expected a wake decision for validate_to_persist"));
1953        assert_eq!(wake.subscription_id, None);
1954        assert_eq!(wake.cursor, None);
1955    }
1956
1957    /// Spec 099 FR-007: a waiting edge whose declared event type is not
1958    /// registered in `EventBroker`'s catalog surfaces a stable,
1959    /// machine-readable error rather than silently never waking.
1960    #[test]
1961    fn event_driven_edge_fails_with_event_subscription_failed_for_unregistered_type() {
1962        let empty_catalog = Arc::new(events::EventCatalog::new());
1963        let broker = Arc::new(
1964            InProcessBroker::new(empty_catalog).unwrap_or_else(|error| unreachable!("{error:?}")),
1965        );
1966
1967        let runtime = Runtime::new(capability_registry_fixture(), MissingEventWorkflowExecutor)
1968            .with_workflow_registry(workflow_registry_fixture())
1969            .with_security_config(RuntimeSecurityConfig::development())
1970            .with_event_broker(broker);
1971
1972        let outcome = runtime.execute_workflow(valid_workflow_request());
1973        assert_eq!(
1974            outcome.evidence.result.failure_reason,
1975            Some(WorkflowTraversalFailureReason::EventSubscriptionFailed)
1976        );
1977    }
1978
1979    struct AlwaysFailingBroker;
1980
1981    impl EventBroker for AlwaysFailingBroker {
1982        fn publish(&self, _event: events::TraverseEvent) -> Result<(), EventError> {
1983            Err(EventError::JournalWrite(
1984                "simulated broker failure".to_string(),
1985            ))
1986        }
1987
1988        fn subscribe(
1989            &self,
1990            _event_type: &str,
1991            _from_cursor: &str,
1992        ) -> Result<events::Subscription, EventError> {
1993            Err(EventError::JournalRead(
1994                "simulated broker failure".to_string(),
1995            ))
1996        }
1997
1998        fn subscribe_for_subject(
1999            &self,
2000            _event_type: &str,
2001            _from_cursor: &str,
2002            _subject_id: Option<&str>,
2003        ) -> Result<events::Subscription, EventError> {
2004            Err(EventError::JournalRead(
2005                "simulated broker failure".to_string(),
2006            ))
2007        }
2008
2009        fn poll(
2010            &self,
2011            _subscription_id: &str,
2012            _max_events: usize,
2013        ) -> Result<events::SubscriptionPoll, EventError> {
2014            Err(EventError::JournalRead(
2015                "simulated broker failure".to_string(),
2016            ))
2017        }
2018
2019        fn cancel(&self, _subscription_id: &str) -> Result<(), EventError> {
2020            Err(EventError::SubscriptionNotFound("unknown".to_string()))
2021        }
2022    }
2023
2024    /// Spec 099 FR-008: `EventBroker` being unreachable or internally
2025    /// failing when a waiting edge tries to establish its subscription
2026    /// surfaces a stable, retryable error distinct from FR-007's
2027    /// unregistered-type case, without crashing the workflow execution.
2028    #[test]
2029    fn event_driven_edge_fails_with_event_broker_unavailable_when_broker_errors() {
2030        let runtime = Runtime::new(capability_registry_fixture(), MissingEventWorkflowExecutor)
2031            .with_workflow_registry(workflow_registry_fixture())
2032            .with_security_config(RuntimeSecurityConfig::development())
2033            .with_event_broker(Arc::new(AlwaysFailingBroker));
2034
2035        let outcome = runtime.execute_workflow(valid_workflow_request());
2036        assert_eq!(
2037            outcome.evidence.result.failure_reason,
2038            Some(WorkflowTraversalFailureReason::EventBrokerUnavailable)
2039        );
2040    }
2041
2042    /// Exercises every `AlwaysFailingBroker` operation directly, confirming
2043    /// the double fails deterministically the way its name promises (only
2044    /// `subscribe` is reachable through the full workflow path above, since
2045    /// a failed subscribe short-circuits before poll/cancel/publish).
2046    #[test]
2047    fn always_failing_broker_fails_every_operation() {
2048        let broker = AlwaysFailingBroker;
2049        assert!(matches!(
2050            broker.publish(sample_traverse_event("content.comments.validated", "1.0.0")),
2051            Err(EventError::JournalWrite(_))
2052        ));
2053        assert!(matches!(
2054            broker.subscribe("content.comments.validated", "0"),
2055            Err(EventError::JournalRead(_))
2056        ));
2057        assert!(matches!(
2058            broker.subscribe_for_subject("content.comments.validated", "0", None),
2059            Err(EventError::JournalRead(_))
2060        ));
2061        assert!(matches!(
2062            broker.poll("sub-1", 1),
2063            Err(EventError::JournalRead(_))
2064        ));
2065        assert!(matches!(
2066            broker.cancel("sub-1"),
2067            Err(EventError::SubscriptionNotFound(_))
2068        ));
2069    }
2070
2071    struct PollFailingBroker(InProcessBroker);
2072
2073    impl EventBroker for PollFailingBroker {
2074        fn publish(&self, event: events::TraverseEvent) -> Result<(), EventError> {
2075            self.0.publish(event)
2076        }
2077
2078        fn subscribe(
2079            &self,
2080            event_type: &str,
2081            from_cursor: &str,
2082        ) -> Result<events::Subscription, EventError> {
2083            self.0.subscribe(event_type, from_cursor)
2084        }
2085
2086        fn subscribe_for_subject(
2087            &self,
2088            event_type: &str,
2089            from_cursor: &str,
2090            subject_id: Option<&str>,
2091        ) -> Result<events::Subscription, EventError> {
2092            self.0
2093                .subscribe_for_subject(event_type, from_cursor, subject_id)
2094        }
2095
2096        fn poll(
2097            &self,
2098            _subscription_id: &str,
2099            _max_events: usize,
2100        ) -> Result<events::SubscriptionPoll, EventError> {
2101            Err(EventError::JournalRead(
2102                "simulated poll failure".to_string(),
2103            ))
2104        }
2105
2106        fn cancel(&self, subscription_id: &str) -> Result<(), EventError> {
2107            self.0.cancel(subscription_id)
2108        }
2109    }
2110
2111    /// Spec 099 FR-008: a broker whose subscription establishes fine but
2112    /// whose `poll` fails mid-drain is a distinct failure point from
2113    /// subscribe failing outright, and must surface the same stable,
2114    /// retryable `EventBrokerUnavailable` reason.
2115    #[test]
2116    fn event_driven_edge_fails_with_event_broker_unavailable_when_poll_errors() {
2117        let catalog = Arc::new(events::EventCatalog::new());
2118        catalog
2119            .register(events::EventCatalogEntry {
2120                event_type: "content.comments.validated".to_string(),
2121                owner: "content.comments".to_string(),
2122                version: "1.0.0".to_string(),
2123                lifecycle_status: events::LifecycleStatus::Active,
2124                consumer_count: 0,
2125            })
2126            .unwrap_or_else(|error| unreachable!("{error:?}"));
2127        let inner = InProcessBroker::new(catalog).unwrap_or_else(|error| unreachable!("{error:?}"));
2128        let broker = Arc::new(PollFailingBroker(inner));
2129
2130        // `publish`/`subscribe_for_subject` delegate straight through to the
2131        // wrapped broker; only `poll` is overridden to fail.
2132        broker
2133            .publish(sample_traverse_event("content.comments.validated", "1.0.0"))
2134            .unwrap_or_else(|error| unreachable!("{error:?}"));
2135        broker
2136            .subscribe_for_subject("content.comments.validated", "0", None)
2137            .unwrap_or_else(|error| unreachable!("{error:?}"));
2138
2139        let runtime = Runtime::new(capability_registry_fixture(), MissingEventWorkflowExecutor)
2140            .with_workflow_registry(workflow_registry_fixture())
2141            .with_security_config(RuntimeSecurityConfig::development())
2142            .with_event_broker(broker);
2143
2144        let outcome = runtime.execute_workflow(valid_workflow_request());
2145        assert_eq!(
2146            outcome.evidence.result.failure_reason,
2147            Some(WorkflowTraversalFailureReason::EventBrokerUnavailable)
2148        );
2149    }
2150
2151    struct CancelFailingBroker(InProcessBroker);
2152
2153    impl EventBroker for CancelFailingBroker {
2154        fn publish(&self, event: events::TraverseEvent) -> Result<(), EventError> {
2155            self.0.publish(event)
2156        }
2157
2158        fn subscribe(
2159            &self,
2160            event_type: &str,
2161            from_cursor: &str,
2162        ) -> Result<events::Subscription, EventError> {
2163            self.0.subscribe(event_type, from_cursor)
2164        }
2165
2166        fn subscribe_for_subject(
2167            &self,
2168            event_type: &str,
2169            from_cursor: &str,
2170            subject_id: Option<&str>,
2171        ) -> Result<events::Subscription, EventError> {
2172            self.0
2173                .subscribe_for_subject(event_type, from_cursor, subject_id)
2174        }
2175
2176        fn poll(
2177            &self,
2178            subscription_id: &str,
2179            max_events: usize,
2180        ) -> Result<events::SubscriptionPoll, EventError> {
2181            self.0.poll(subscription_id, max_events)
2182        }
2183
2184        fn cancel(&self, _subscription_id: &str) -> Result<(), EventError> {
2185            Err(EventError::SubscriptionNotFound(
2186                "simulated cancel failure".to_string(),
2187            ))
2188        }
2189    }
2190
2191    /// A broker subscription that fails to cancel after a successful drain
2192    /// does not fail the workflow (cleanup is best-effort); it surfaces a
2193    /// non-fatal `RuntimeWarning` instead.
2194    #[test]
2195    fn event_driven_edge_surfaces_warning_when_subscription_cancel_fails() {
2196        let catalog = Arc::new(events::EventCatalog::new());
2197        catalog
2198            .register(events::EventCatalogEntry {
2199                event_type: "content.comments.validated".to_string(),
2200                owner: "content.comments".to_string(),
2201                version: "1.0.0".to_string(),
2202                lifecycle_status: events::LifecycleStatus::Active,
2203                consumer_count: 0,
2204            })
2205            .unwrap_or_else(|error| unreachable!("{error:?}"));
2206        let inner = InProcessBroker::new(catalog).unwrap_or_else(|error| unreachable!("{error:?}"));
2207        inner
2208            .publish(sample_traverse_event("content.comments.validated", "1.0.0"))
2209            .unwrap_or_else(|error| unreachable!("{error:?}"));
2210        let broker = Arc::new(CancelFailingBroker(inner));
2211
2212        // `publish`/`subscribe_for_subject` delegate straight through to the
2213        // wrapped broker; only `cancel` is overridden to fail.
2214        broker
2215            .publish(sample_traverse_event("content.comments.validated", "1.0.0"))
2216            .unwrap_or_else(|error| unreachable!("{error:?}"));
2217        broker
2218            .subscribe_for_subject("content.comments.validated", "0", None)
2219            .unwrap_or_else(|error| unreachable!("{error:?}"));
2220
2221        let runtime = Runtime::new(capability_registry_fixture(), MissingEventWorkflowExecutor)
2222            .with_workflow_registry(workflow_registry_fixture())
2223            .with_security_config(RuntimeSecurityConfig::development())
2224            .with_event_broker(broker);
2225
2226        let outcome = runtime.execute_workflow(valid_workflow_request());
2227        assert_eq!(
2228            outcome.evidence.result.status,
2229            WorkflowTraversalStatus::Completed
2230        );
2231        assert!(
2232            outcome
2233                .result
2234                .warnings
2235                .iter()
2236                .any(|warning| warning.code == "event_broker_subscription_cleanup_failed")
2237        );
2238    }
2239
2240    /// Spec 099 FR-004: within one evaluation, a broker-sourced event that
2241    /// has already been consumed by a waiting edge does not advance that
2242    /// edge a second time even though the broker still has the event
2243    /// buffered (simulating post-restart durable replay).
2244    #[test]
2245    fn poll_broker_events_for_waiting_edges_supports_exact_once_across_repeated_polls() {
2246        let broker = event_catalog_broker_fixture("content.comments.validated", "1.0.0");
2247        broker
2248            .publish(sample_traverse_event("content.comments.validated", "1.0.0"))
2249            .unwrap_or_else(|error| unreachable!("{error:?}"));
2250
2251        let waiting_edges = vec![WaitingWorkflowEdgeContext {
2252            workflow_execution_id: "wf_exec_replay".to_string(),
2253            edge_id: "validate_to_persist".to_string(),
2254            from_node_id: "validate_comment".to_string(),
2255            to_node_id: "persist_comment".to_string(),
2256            event_ref: EventReference {
2257                event_id: "content.comments.validated".to_string(),
2258                version: "1.0.0".to_string(),
2259            },
2260            predicate: None,
2261        }];
2262
2263        let mut consumed = BTreeSet::new();
2264
2265        let (first_batch, first_warnings) =
2266            poll_broker_events_for_waiting_edges(broker.as_ref(), &waiting_edges)
2267                .unwrap_or_else(|error| unreachable!("{error:?}"));
2268        assert!(first_warnings.is_empty());
2269        let first = evaluate_event_driven_edges(&waiting_edges, &first_batch, &mut consumed, "t1");
2270        assert_eq!(
2271            first.taken_edge_ids,
2272            vec!["validate_to_persist".to_string()]
2273        );
2274
2275        // The broker still has the event buffered (it was never cancelled
2276        // mid-retention), simulating a fresh subscription replaying the
2277        // same durable history after a restart.
2278        let (second_batch, second_warnings) =
2279            poll_broker_events_for_waiting_edges(broker.as_ref(), &waiting_edges)
2280                .unwrap_or_else(|error| unreachable!("{error:?}"));
2281        assert!(second_warnings.is_empty());
2282        let second =
2283            evaluate_event_driven_edges(&waiting_edges, &second_batch, &mut consumed, "t2");
2284        assert!(second.taken_edge_ids.is_empty());
2285        assert!(
2286            second
2287                .evidence
2288                .match_records
2289                .iter()
2290                .all(|record| record.match_result == EventMatchResult::AlreadyConsumed)
2291        );
2292    }
2293
2294    /// Spec 099: `poll_broker_events_for_waiting_edges` visits distinct
2295    /// event types in a fixed sorted order and cancels each subscription
2296    /// after draining it, so it does not leak broker subscriptions.
2297    #[test]
2298    fn poll_broker_events_for_waiting_edges_cancels_subscriptions_after_draining() {
2299        let catalog = Arc::new(events::EventCatalog::new());
2300        for event_type in [
2301            "content.comments.draft-created",
2302            "content.comments.validated",
2303        ] {
2304            catalog
2305                .register(events::EventCatalogEntry {
2306                    event_type: event_type.to_string(),
2307                    owner: "content.comments".to_string(),
2308                    version: "1.0.0".to_string(),
2309                    lifecycle_status: events::LifecycleStatus::Active,
2310                    consumer_count: 0,
2311                })
2312                .unwrap_or_else(|error| unreachable!("{error:?}"));
2313        }
2314        let broker =
2315            InProcessBroker::new(catalog).unwrap_or_else(|error| unreachable!("{error:?}"));
2316        broker
2317            .publish(sample_traverse_event("content.comments.validated", "1.0.0"))
2318            .unwrap_or_else(|error| unreachable!("{error:?}"));
2319
2320        let waiting_edges = vec![
2321            WaitingWorkflowEdgeContext {
2322                workflow_execution_id: "wf_exec_multi".to_string(),
2323                edge_id: "edge_a".to_string(),
2324                from_node_id: "a".to_string(),
2325                to_node_id: "b".to_string(),
2326                event_ref: EventReference {
2327                    event_id: "content.comments.validated".to_string(),
2328                    version: "1.0.0".to_string(),
2329                },
2330                predicate: None,
2331            },
2332            WaitingWorkflowEdgeContext {
2333                workflow_execution_id: "wf_exec_multi".to_string(),
2334                edge_id: "edge_b".to_string(),
2335                from_node_id: "a".to_string(),
2336                to_node_id: "c".to_string(),
2337                event_ref: EventReference {
2338                    event_id: "content.comments.draft-created".to_string(),
2339                    version: "1.0.0".to_string(),
2340                },
2341                predicate: None,
2342            },
2343        ];
2344
2345        let (records, warnings) = poll_broker_events_for_waiting_edges(&broker, &waiting_edges)
2346            .unwrap_or_else(|error| unreachable!("{error:?}"));
2347        assert!(warnings.is_empty());
2348        assert_eq!(records.len(), 1);
2349        let source = records[0]
2350            .source
2351            .as_ref()
2352            .unwrap_or_else(|| unreachable!("expected broker provenance on the record"));
2353
2354        // The subscription was cancelled after draining, so polling it
2355        // again must fail with `SubscriptionNotFound`.
2356        assert!(matches!(
2357            broker.poll(&source.subscription_id, 1),
2358            Err(EventError::SubscriptionNotFound(id)) if id == source.subscription_id
2359        ));
2360    }
2361
2362    /// `poll_broker_events_for_waiting_edges` drains a subscription across
2363    /// multiple internal `EventBroker::poll` calls when more events are
2364    /// buffered than one poll batch can return.
2365    #[test]
2366    fn poll_broker_events_for_waiting_edges_paginates_beyond_one_batch() {
2367        let broker = event_catalog_broker_fixture("content.comments.validated", "1.0.0");
2368        let published_count = EVENT_BROKER_POLL_BATCH + 5;
2369        for index in 0..published_count {
2370            let mut event = sample_traverse_event("content.comments.validated", "1.0.0");
2371            event.id = format!("evt-fixture-{index}");
2372            event.deduplication_id = Some(event.id.clone());
2373            broker
2374                .publish(event)
2375                .unwrap_or_else(|error| unreachable!("{error:?}"));
2376        }
2377
2378        let waiting_edges = vec![WaitingWorkflowEdgeContext {
2379            workflow_execution_id: "wf_exec_paginate".to_string(),
2380            edge_id: "validate_to_persist".to_string(),
2381            from_node_id: "validate_comment".to_string(),
2382            to_node_id: "persist_comment".to_string(),
2383            event_ref: EventReference {
2384                event_id: "content.comments.validated".to_string(),
2385                version: "1.0.0".to_string(),
2386            },
2387            predicate: None,
2388        }];
2389
2390        let (records, warnings) =
2391            poll_broker_events_for_waiting_edges(broker.as_ref(), &waiting_edges)
2392                .unwrap_or_else(|error| unreachable!("{error:?}"));
2393        assert!(warnings.is_empty());
2394        assert_eq!(records.len(), published_count);
2395    }
2396
2397    #[test]
2398    #[allow(clippy::too_many_lines)]
2399    fn workflow_runtime_covers_additional_failure_and_helper_branches() {
2400        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
2401            .with_workflow_registry(workflow_registry_fixture())
2402            .with_security_config(RuntimeSecurityConfig::development());
2403
2404        let invalid_input = runtime.execute_workflow(WorkflowExecutionRequest {
2405            input: json!({}),
2406            ..valid_workflow_request()
2407        });
2408        assert_eq!(
2409            invalid_input.evidence.result.failure_reason,
2410            Some(WorkflowTraversalFailureReason::WorkflowInvalid)
2411        );
2412
2413        let invalid_request = runtime.execute_workflow(WorkflowExecutionRequest {
2414            kind: "bad".to_string(),
2415            ..valid_workflow_request()
2416        });
2417        assert_eq!(
2418            invalid_request.evidence.result.failure_reason,
2419            Some(WorkflowTraversalFailureReason::WorkflowInvalid)
2420        );
2421
2422        let missing_node = runtime.traverse_workflow(
2423            &valid_workflow_request(),
2424            &resolved_workflow(WorkflowDefinition {
2425                start_node: "missing".to_string(),
2426                ..workflow_definition_fixture(
2427                    Some(EventReference {
2428                        event_id: "content.comments.validated".to_string(),
2429                        version: "1.0.0".to_string(),
2430                    }),
2431                    None,
2432                )
2433            }),
2434        );
2435        assert!(missing_node.is_err());
2436
2437        let missing_capability_runtime = Runtime::new(CapabilityRegistry::new(), WorkflowExecutor)
2438            .with_workflow_registry(workflow_registry_fixture());
2439        let missing_capability =
2440            missing_capability_runtime.execute_workflow(valid_workflow_request());
2441        assert_eq!(
2442            missing_capability.evidence.result.failure_reason,
2443            Some(WorkflowTraversalFailureReason::WorkflowInvalid)
2444        );
2445
2446        let strict_runtime =
2447            Runtime::new(strict_input_capability_registry_fixture(), WorkflowExecutor)
2448                .with_workflow_registry(workflow_registry_fixture())
2449                .with_security_config(RuntimeSecurityConfig::development());
2450        let input_mismatch = strict_runtime.traverse_workflow(
2451            &valid_workflow_request(),
2452            &resolved_workflow(WorkflowDefinition {
2453                nodes: vec![WorkflowNode {
2454                    input: WorkflowNodeInput {
2455                        from_workflow_input: vec!["missing".to_string()],
2456                    },
2457                    ..workflow_definition_fixture(
2458                        Some(EventReference {
2459                            event_id: "content.comments.validated".to_string(),
2460                            version: "1.0.0".to_string(),
2461                        }),
2462                        None,
2463                    )
2464                    .nodes[0]
2465                        .clone()
2466                }],
2467                edges: Vec::new(),
2468                start_node: "create_draft".to_string(),
2469                terminal_nodes: vec!["create_draft".to_string()],
2470                ..workflow_definition_fixture(
2471                    Some(EventReference {
2472                        event_id: "content.comments.validated".to_string(),
2473                        version: "1.0.0".to_string(),
2474                    }),
2475                    None,
2476                )
2477            }),
2478        );
2479        assert!(input_mismatch.is_err());
2480
2481        let bad_output_runtime =
2482            Runtime::new(capability_registry_fixture(), BadOutputWorkflowExecutor)
2483                .with_workflow_registry(workflow_registry_fixture())
2484                .with_security_config(RuntimeSecurityConfig::development());
2485        let bad_output = bad_output_runtime.execute_workflow(valid_workflow_request());
2486        assert_eq!(
2487            bad_output.evidence.result.failure_reason,
2488            Some(WorkflowTraversalFailureReason::StepExecutionFailed)
2489        );
2490
2491        // Spec 101-local-executor-event-emission FR-007/FR-008: a workflow
2492        // node that emits an event its contract does not declare must fail
2493        // the step with a contract-violation error, mirroring
2494        // `PlacementRouter`'s native-boundary validation.
2495        let undeclared_event_runtime = Runtime::new(
2496            capability_registry_fixture(),
2497            UndeclaredEventWorkflowExecutor,
2498        )
2499        .with_workflow_registry(workflow_registry_fixture())
2500        .with_security_config(RuntimeSecurityConfig::development());
2501        let undeclared_event = undeclared_event_runtime.execute_workflow(valid_workflow_request());
2502        assert_eq!(
2503            undeclared_event.evidence.result.failure_reason,
2504            Some(WorkflowTraversalFailureReason::StepExecutionFailed)
2505        );
2506
2507        let direct_success = runtime.traverse_workflow(
2508            &valid_workflow_request(),
2509            &resolved_workflow(workflow_definition_fixture(
2510                Some(EventReference {
2511                    event_id: "content.comments.validated".to_string(),
2512                    version: "1.0.0".to_string(),
2513                }),
2514                Some(WorkflowEdge {
2515                    edge_id: "direct".to_string(),
2516                    from: "create_draft".to_string(),
2517                    to: "validate_comment".to_string(),
2518                    trigger: WorkflowEdgeTrigger::Direct,
2519                    event: None,
2520                    predicate: None,
2521                }),
2522            )),
2523        );
2524        assert!(direct_success.is_ok());
2525
2526        let ambiguous_direct = runtime.traverse_workflow(
2527            &valid_workflow_request(),
2528            &resolved_workflow(WorkflowDefinition {
2529                edges: vec![
2530                    WorkflowEdge {
2531                        edge_id: "direct-1".to_string(),
2532                        from: "create_draft".to_string(),
2533                        to: "validate_comment".to_string(),
2534                        trigger: WorkflowEdgeTrigger::Direct,
2535                        event: None,
2536                        predicate: None,
2537                    },
2538                    WorkflowEdge {
2539                        edge_id: "direct-2".to_string(),
2540                        from: "create_draft".to_string(),
2541                        to: "persist_comment".to_string(),
2542                        trigger: WorkflowEdgeTrigger::Direct,
2543                        event: None,
2544                        predicate: None,
2545                    },
2546                ],
2547                ..workflow_definition_fixture(
2548                    Some(EventReference {
2549                        event_id: "content.comments.validated".to_string(),
2550                        version: "1.0.0".to_string(),
2551                    }),
2552                    None,
2553                )
2554            }),
2555        );
2556        assert!(ambiguous_direct.is_err());
2557
2558        let ambiguous_event = runtime.traverse_workflow(
2559            &valid_workflow_request(),
2560            &resolved_workflow(WorkflowDefinition {
2561                edges: vec![
2562                    WorkflowEdge {
2563                        edge_id: "draft_to_validate".to_string(),
2564                        from: "create_draft".to_string(),
2565                        to: "validate_comment".to_string(),
2566                        trigger: WorkflowEdgeTrigger::Event,
2567                        event: Some(EventReference {
2568                            event_id: "content.comments.draft-created".to_string(),
2569                            version: "1.0.0".to_string(),
2570                        }),
2571                        predicate: None,
2572                    },
2573                    WorkflowEdge {
2574                        edge_id: "draft_to_persist".to_string(),
2575                        from: "create_draft".to_string(),
2576                        to: "persist_comment".to_string(),
2577                        trigger: WorkflowEdgeTrigger::Event,
2578                        event: Some(EventReference {
2579                            event_id: "content.comments.draft-created".to_string(),
2580                            version: "1.0.0".to_string(),
2581                        }),
2582                        predicate: None,
2583                    },
2584                ],
2585                ..workflow_definition_fixture(
2586                    Some(EventReference {
2587                        event_id: "content.comments.validated".to_string(),
2588                        version: "1.0.0".to_string(),
2589                    }),
2590                    None,
2591                )
2592            }),
2593        );
2594        assert!(ambiguous_event.is_err());
2595
2596        let terminal_miss = runtime.traverse_workflow(
2597            &valid_workflow_request(),
2598            &resolved_workflow(WorkflowDefinition {
2599                edges: Vec::new(),
2600                terminal_nodes: vec!["persist_comment".to_string()],
2601                ..workflow_definition_fixture(
2602                    Some(EventReference {
2603                        event_id: "content.comments.validated".to_string(),
2604                        version: "1.0.0".to_string(),
2605                    }),
2606                    None,
2607                )
2608            }),
2609        );
2610        assert!(terminal_miss.is_err());
2611
2612        let invalid_final_output = runtime.traverse_workflow(
2613            &valid_workflow_request(),
2614            &resolved_workflow(WorkflowDefinition {
2615                outputs: SchemaContainer {
2616                    schema: json!({
2617                        "type": "object",
2618                        "properties": { "missing": { "type": "string" } },
2619                        "required": ["missing"],
2620                        "additionalProperties": true
2621                    }),
2622                },
2623                ..workflow_definition_fixture(
2624                    Some(EventReference {
2625                        event_id: "content.comments.validated".to_string(),
2626                        version: "1.0.0".to_string(),
2627                    }),
2628                    None,
2629                )
2630            }),
2631        );
2632        assert!(invalid_final_output.is_err());
2633
2634        let selection = SelectionRecord {
2635            status: crate::SelectionStatus::Selected,
2636            selected_capability_id: Some("content.comments.publish-comment".to_string()),
2637            selected_capability_version: Some("1.0.0".to_string()),
2638            failure_reason: None,
2639            remaining_candidates: Vec::new(),
2640        };
2641        let mut selected = runtime
2642            .registry
2643            .find_exact(
2644                LookupScope::PublicOnly,
2645                "content.comments.create-comment-draft",
2646                "1.0.0",
2647            )
2648            .unwrap_or_else(|| unreachable!("fixture capability missing"));
2649        selected.record.implementation_kind = ImplementationKind::Workflow;
2650        let (attempt, mut emitter) = super::super::begin_attempt(
2651            RuntimeRequest {
2652                kind: "runtime_request".to_string(),
2653                schema_version: "1.0.0".to_string(),
2654                request_id: "workflow-capability".to_string(),
2655                intent: RuntimeIntent {
2656                    capability_id: Some("content.comments.publish-comment".to_string()),
2657                    capability_version: Some("1.0.0".to_string()),
2658                    version_range: None,
2659                    intent_key: None,
2660                },
2661                input: json!({"comment_text": "hello"}),
2662                lookup: RuntimeLookup {
2663                    scope: RuntimeLookupScope::PublicOnly,
2664                    allow_ambiguity: false,
2665                },
2666                context: RuntimeContext {
2667                    requested_target: crate::PlacementTarget::Local,
2668                    correlation_id: None,
2669                    caller: None,
2670                    traceparent: None,
2671                    tracestate: None,
2672                    metadata: None,
2673                    identity: None,
2674                },
2675                governing_spec: "006-runtime-request-execution".to_string(),
2676            },
2677            crate::RuntimeObservabilityConfig::default(),
2678        );
2679        emitter.push(
2680            crate::RuntimeState::Discovering,
2681            crate::RuntimeTransitionReasonCode::RequestStarted,
2682            json!({"lookup_scope": RuntimeLookupScope::PublicOnly}),
2683        );
2684        emitter.push(
2685            crate::RuntimeState::EvaluatingConstraints,
2686            crate::RuntimeTransitionReasonCode::CandidatesCollected,
2687            json!({"candidate_count": 1}),
2688        );
2689        emitter.push(
2690            crate::RuntimeState::Selecting,
2691            crate::RuntimeTransitionReasonCode::ConstraintsEvaluated,
2692            json!({"eligible_candidates": 1, "rejected_candidates": 0}),
2693        );
2694        let started_execution = crate::start_selected_execution(
2695            &mut emitter,
2696            &selected,
2697            crate::resolve_placement(crate::PlacementTarget::Local)
2698                .unwrap_or_else(|_| unreachable!("local placement should resolve")),
2699            None,
2700        );
2701        let outcome = runtime.execute_workflow_capability(
2702            crate::ExecutionContext {
2703                attempt,
2704                emitter,
2705                candidate_collection: CandidateCollectionRecord {
2706                    lookup_scope: RuntimeLookupScope::PublicOnly,
2707                    candidates: Vec::new(),
2708                    rejected_candidates: Vec::new(),
2709                },
2710                selection,
2711            },
2712            &selected,
2713            started_execution,
2714        );
2715        assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
2716
2717        let mut selected = runtime
2718            .registry
2719            .find_exact(
2720                LookupScope::PublicOnly,
2721                "content.comments.create-comment-draft",
2722                "1.0.0",
2723            )
2724            .unwrap_or_else(|| unreachable!("fixture capability missing"));
2725        selected.record.scope = RegistryScope::Private;
2726        selected.record.implementation_kind = ImplementationKind::Workflow;
2727        selected.artifact.workflow_ref = Some(traverse_registry::WorkflowReference {
2728            workflow_id: "content.comments.publish-comment".to_string(),
2729            workflow_version: "1.0.0".to_string(),
2730        });
2731        let (attempt, mut emitter) = super::super::begin_attempt(
2732            RuntimeRequest {
2733                request_id: "workflow-private".to_string(),
2734                ..valid_runtime_request()
2735            },
2736            crate::RuntimeObservabilityConfig::default(),
2737        );
2738        emitter.push(
2739            crate::RuntimeState::Discovering,
2740            crate::RuntimeTransitionReasonCode::RequestStarted,
2741            json!({"lookup_scope": RuntimeLookupScope::PreferPrivate}),
2742        );
2743        emitter.push(
2744            crate::RuntimeState::EvaluatingConstraints,
2745            crate::RuntimeTransitionReasonCode::CandidatesCollected,
2746            json!({"candidate_count": 1}),
2747        );
2748        emitter.push(
2749            crate::RuntimeState::Selecting,
2750            crate::RuntimeTransitionReasonCode::ConstraintsEvaluated,
2751            json!({"eligible_candidates": 1, "rejected_candidates": 0}),
2752        );
2753        let started_execution = crate::start_selected_execution(
2754            &mut emitter,
2755            &selected,
2756            crate::resolve_placement(crate::PlacementTarget::Local)
2757                .unwrap_or_else(|_| unreachable!("local placement should resolve")),
2758            None,
2759        );
2760        let failing_runtime = Runtime::new(capability_registry_fixture(), FailingWorkflowExecutor)
2761            .with_workflow_registry(workflow_registry_fixture())
2762            .with_security_config(RuntimeSecurityConfig::development());
2763        let outcome = failing_runtime.execute_workflow_capability(
2764            crate::ExecutionContext {
2765                attempt,
2766                emitter,
2767                candidate_collection: CandidateCollectionRecord {
2768                    lookup_scope: RuntimeLookupScope::PreferPrivate,
2769                    candidates: Vec::new(),
2770                    rejected_candidates: Vec::new(),
2771                },
2772                selection: SelectionRecord {
2773                    status: crate::SelectionStatus::Selected,
2774                    selected_capability_id: Some("content.comments.publish-comment".to_string()),
2775                    selected_capability_version: Some("1.0.0".to_string()),
2776                    failure_reason: None,
2777                    remaining_candidates: Vec::new(),
2778                },
2779            },
2780            &selected,
2781            started_execution,
2782        );
2783        assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
2784
2785        let mut unknown = runtime
2786            .registry
2787            .find_exact(
2788                LookupScope::PublicOnly,
2789                "content.comments.create-comment-draft",
2790                "1.0.0",
2791            )
2792            .unwrap_or_else(|| unreachable!("fixture capability missing"));
2793        unknown.record.id = "unknown".to_string();
2794        let _ = WorkflowExecutor.execute(&unknown, &json!({}));
2795        let _ = MissingEventWorkflowExecutor.execute(&unknown, &json!({}));
2796        let _ = BadOutputWorkflowExecutor.execute(&unknown, &json!({}));
2797        unknown.record.id = "content.comments.persist-comment".to_string();
2798        let _ = MissingEventWorkflowExecutor.execute(&unknown, &json!({}));
2799    }
2800
2801    #[test]
2802    fn pipeline_workflow_merges_namespaced_step_outputs_deterministically() {
2803        let registry = pipeline_capability_registry();
2804        let mut workflows = WorkflowRegistry::new();
2805        register_workflow_ok(&mut workflows, &registry, pipeline_workflow_registration());
2806        let runtime = Runtime::new(registry, PipelineExecutor)
2807            .with_workflow_registry(workflows)
2808            .with_security_config(RuntimeSecurityConfig::development());
2809
2810        let first = runtime.execute_workflow(pipeline_workflow_request());
2811        let second = runtime.execute_workflow(pipeline_workflow_request());
2812
2813        assert_eq!(first.result.status, WorkflowTraversalStatus::Completed);
2814        assert_eq!(
2815            first.result.output,
2816            Some(json!({
2817                "validate": {"valid": true, "issues": []},
2818                "process": {
2819                    "title": "Hello world",
2820                    "tags": ["hello", "world"],
2821                    "noteType": "fleeting",
2822                    "suggestedNextAction": "archive",
2823                    "status": "complete"
2824                },
2825                "summarize": {"summary": "Hello world (fleeting)", "wordCount": 3}
2826            }))
2827        );
2828        assert_eq!(first.result, second.result);
2829        assert_eq!(first.evidence.visited_nodes, second.evidence.visited_nodes);
2830
2831        let steps = &first.evidence.visited_nodes;
2832        assert_eq!(steps.len(), 3);
2833        assert_eq!(steps[0].step_index, 0);
2834        assert_eq!(steps[0].capability_id, "content.comments.pipeline-validate");
2835        assert_eq!(steps[1].step_index, 1);
2836        assert_eq!(steps[1].capability_id, "content.comments.pipeline-process");
2837        assert_eq!(steps[2].step_index, 2);
2838        assert_eq!(
2839            steps[2].capability_id,
2840            "content.comments.pipeline-summarize"
2841        );
2842        assert!(
2843            steps
2844                .iter()
2845                .all(|step| step.status == WorkflowTraversalStepStatus::Completed)
2846        );
2847    }
2848
2849    #[test]
2850    fn pipeline_workflow_stops_on_failed_step_with_failed_step_id_in_trace() {
2851        let registry = pipeline_capability_registry();
2852        let mut workflows = WorkflowRegistry::new();
2853        register_workflow_ok(&mut workflows, &registry, pipeline_workflow_registration());
2854        let runtime = Runtime::new(registry, FailingPipelineExecutor)
2855            .with_workflow_registry(workflows)
2856            .with_security_config(RuntimeSecurityConfig::development());
2857
2858        let outcome = runtime.execute_workflow(pipeline_workflow_request());
2859
2860        assert_eq!(outcome.result.status, WorkflowTraversalStatus::Error);
2861        assert_eq!(
2862            outcome.evidence.result.failure_reason,
2863            Some(WorkflowTraversalFailureReason::StepExecutionFailed)
2864        );
2865        let steps = &outcome.evidence.visited_nodes;
2866        assert_eq!(steps.len(), 2);
2867        assert_eq!(steps[0].status, WorkflowTraversalStepStatus::Completed);
2868        assert_eq!(steps[1].node_id, "process_note");
2869        assert_eq!(steps[1].status, WorkflowTraversalStepStatus::Failed);
2870    }
2871
2872    #[test]
2873    fn workflow_step_rejects_unsigned_artifact_under_default_security_config() {
2874        // No explicit security config: the default (Production) posture must
2875        // reject the unsigned artifact before the workflow step executes,
2876        // exactly like direct capability execution (spec 030 FR-013).
2877        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
2878            .with_workflow_registry(workflow_registry_fixture());
2879
2880        let outcome = runtime.execute_workflow(valid_workflow_request());
2881
2882        assert_eq!(outcome.result.status, WorkflowTraversalStatus::Error);
2883        assert_eq!(
2884            outcome.evidence.result.failure_reason,
2885            Some(WorkflowTraversalFailureReason::StepExecutionFailed)
2886        );
2887        let error = outcome.result.error;
2888        assert_eq!(
2889            error.as_ref().map(|error| error.code),
2890            Some(RuntimeErrorCode::ContractViolation)
2891        );
2892        assert_eq!(
2893            error
2894                .as_ref()
2895                .and_then(|error| error.details.get("code"))
2896                .and_then(Value::as_str),
2897            Some("missing_signature")
2898        );
2899        assert_eq!(
2900            error
2901                .as_ref()
2902                .and_then(|error| error.details.get("node_id"))
2903                .and_then(Value::as_str),
2904            Some("create_draft")
2905        );
2906        let steps = &outcome.evidence.visited_nodes;
2907        assert_eq!(steps.len(), 1);
2908        assert_eq!(steps[0].node_id, "create_draft");
2909        assert_eq!(steps[0].status, WorkflowTraversalStepStatus::Failed);
2910        assert!(outcome.result.warnings.is_empty());
2911    }
2912
2913    #[test]
2914    fn workflow_step_unsigned_artifact_warns_and_executes_in_development_mode() {
2915        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
2916            .with_workflow_registry(workflow_registry_fixture())
2917            .with_security_config(RuntimeSecurityConfig::development());
2918
2919        let outcome = runtime.execute_workflow(valid_workflow_request());
2920
2921        assert_eq!(outcome.result.status, WorkflowTraversalStatus::Completed);
2922        assert_eq!(outcome.result.warnings.len(), 3);
2923        assert!(
2924            outcome
2925                .result
2926                .warnings
2927                .iter()
2928                .all(|warning| warning.code == "unsigned_local_dev_artifact")
2929        );
2930    }
2931
2932    #[test]
2933    fn workflow_step_fails_when_signed_artifact_bytes_cannot_be_loaded() {
2934        let runtime = Runtime::new(
2935            signed_missing_binary_capability_registry_fixture(),
2936            WorkflowExecutor,
2937        )
2938        .with_workflow_registry(workflow_registry_fixture());
2939
2940        let outcome = runtime.execute_workflow(valid_workflow_request());
2941
2942        assert_eq!(outcome.result.status, WorkflowTraversalStatus::Error);
2943        assert_eq!(
2944            outcome.evidence.result.failure_reason,
2945            Some(WorkflowTraversalFailureReason::StepExecutionFailed)
2946        );
2947        let error = outcome.result.error;
2948        assert_eq!(
2949            error.as_ref().map(|error| error.code),
2950            Some(RuntimeErrorCode::ArtifactMissing)
2951        );
2952        assert_eq!(
2953            error
2954                .as_ref()
2955                .and_then(|error| error.details.get("code"))
2956                .and_then(Value::as_str),
2957            Some("artifact_load_failed")
2958        );
2959        let steps = &outcome.evidence.visited_nodes;
2960        assert_eq!(steps.len(), 1);
2961        assert_eq!(steps[0].status, WorkflowTraversalStepStatus::Failed);
2962    }
2963
2964    fn pipeline_capability_registry() -> CapabilityRegistry {
2965        let mut registry = CapabilityRegistry::new();
2966        for id in [
2967            "content.comments.pipeline-validate",
2968            "content.comments.pipeline-process",
2969            "content.comments.pipeline-summarize",
2970        ] {
2971            register_capability_ok(
2972                &mut registry,
2973                CapabilityRegistration {
2974                    scope: RegistryScope::Public,
2975                    contract: capability_contract(
2976                        id,
2977                        Vec::new(),
2978                        json!({"type": "object", "additionalProperties": true}),
2979                        json!({"type": "object", "additionalProperties": true}),
2980                    ),
2981                    contract_path: format!("registry/{id}.json"),
2982                    artifact: CapabilityArtifactRecord {
2983                        artifact_ref: format!("artifact-{id}"),
2984                        implementation_kind: ImplementationKind::Executable,
2985                        source: SourceReference {
2986                            kind: SourceKind::Git,
2987                            location: format!("https://example.com/{id}.git"),
2988                        },
2989                        binary: Some(BinaryReference {
2990                            format: BinaryFormat::Wasm,
2991                            location: format!("{id}.wasm"),
2992                            signature: None,
2993                        }),
2994                        workflow_ref: None,
2995                        digests: ArtifactDigests {
2996                            source_digest: "source".to_string(),
2997                            binary_digest: Some("binary".to_string()),
2998                        },
2999                        provenance: RegistryProvenance {
3000                            source: "fixtures".to_string(),
3001                            author: "Enrico".to_string(),
3002                            created_at: "2026-03-27T00:00:00Z".to_string(),
3003                        },
3004                    },
3005                    registered_at: "2026-03-27T00:00:00Z".to_string(),
3006                    tags: vec!["pipeline".to_string()],
3007                    composability: ComposabilityMetadata {
3008                        kind: CompositionKind::Atomic,
3009                        patterns: vec![CompositionPattern::Sequential],
3010                        provides: vec!["pipeline-step".to_string()],
3011                        requires: Vec::new(),
3012                    },
3013                    governing_spec: "005-capability-registry".to_string(),
3014                    validator_version: "validator".to_string(),
3015                },
3016            );
3017        }
3018        registry
3019    }
3020
3021    #[allow(clippy::too_many_lines)]
3022    fn pipeline_workflow_registration() -> WorkflowRegistration {
3023        WorkflowRegistration {
3024            scope: RegistryScope::Public,
3025            definition: WorkflowDefinition {
3026                kind: "workflow_definition".to_string(),
3027                schema_version: "1.0.0".to_string(),
3028                id: "content.comments.pipeline".to_string(),
3029                name: "pipeline".to_string(),
3030                version: "1.0.0".to_string(),
3031                lifecycle: Lifecycle::Active,
3032                owner: Owner {
3033                    team: "traverse-core".to_string(),
3034                    contact: "test@example.com".to_string(),
3035                },
3036                summary: "Deterministic three-step pipeline fixture.".to_string(),
3037                inputs: SchemaContainer {
3038                    schema: json!({
3039                        "type": "object",
3040                        "required": ["note"],
3041                        "properties": {"note": {"type": "string"}},
3042                        "additionalProperties": false
3043                    }),
3044                },
3045                outputs: SchemaContainer {
3046                    schema: json!({
3047                        "type": "object",
3048                        "required": ["validate", "process", "summarize"],
3049                        "properties": {
3050                            "validate": {"type": "object"},
3051                            "process": {"type": "object"},
3052                            "summarize": {"type": "object"}
3053                        },
3054                        "additionalProperties": false
3055                    }),
3056                },
3057                nodes: vec![
3058                    WorkflowNode {
3059                        node_id: "validate_note".to_string(),
3060                        capability_id: "content.comments.pipeline-validate".to_string(),
3061                        capability_version: "1.0.0".to_string(),
3062                        input: WorkflowNodeInput {
3063                            from_workflow_input: vec!["note".to_string()],
3064                        },
3065                        output: WorkflowNodeOutput {
3066                            to_workflow_state: Vec::new(),
3067                            publish_to_state_as: Some("validate".to_string()),
3068                        },
3069                    },
3070                    WorkflowNode {
3071                        node_id: "process_note".to_string(),
3072                        capability_id: "content.comments.pipeline-process".to_string(),
3073                        capability_version: "1.0.0".to_string(),
3074                        input: WorkflowNodeInput {
3075                            from_workflow_input: vec!["note".to_string()],
3076                        },
3077                        output: WorkflowNodeOutput {
3078                            to_workflow_state: vec![
3079                                "title".to_string(),
3080                                "tags".to_string(),
3081                                "noteType".to_string(),
3082                                "suggestedNextAction".to_string(),
3083                                "status".to_string(),
3084                            ],
3085                            publish_to_state_as: Some("process".to_string()),
3086                        },
3087                    },
3088                    WorkflowNode {
3089                        node_id: "summarize_note".to_string(),
3090                        capability_id: "content.comments.pipeline-summarize".to_string(),
3091                        capability_version: "1.0.0".to_string(),
3092                        input: WorkflowNodeInput {
3093                            from_workflow_input: vec![
3094                                "title".to_string(),
3095                                "tags".to_string(),
3096                                "noteType".to_string(),
3097                                "suggestedNextAction".to_string(),
3098                                "status".to_string(),
3099                            ],
3100                        },
3101                        output: WorkflowNodeOutput {
3102                            to_workflow_state: Vec::new(),
3103                            publish_to_state_as: Some("summarize".to_string()),
3104                        },
3105                    },
3106                ],
3107                edges: vec![
3108                    WorkflowEdge {
3109                        edge_id: "validate_to_process".to_string(),
3110                        from: "validate_note".to_string(),
3111                        to: "process_note".to_string(),
3112                        trigger: WorkflowEdgeTrigger::Direct,
3113                        event: None,
3114                        predicate: None,
3115                    },
3116                    WorkflowEdge {
3117                        edge_id: "process_to_summarize".to_string(),
3118                        from: "process_note".to_string(),
3119                        to: "summarize_note".to_string(),
3120                        trigger: WorkflowEdgeTrigger::Direct,
3121                        event: None,
3122                        predicate: None,
3123                    },
3124                ],
3125                start_node: "validate_note".to_string(),
3126                terminal_nodes: vec!["summarize_note".to_string()],
3127                output_projection: vec![
3128                    "validate".to_string(),
3129                    "process".to_string(),
3130                    "summarize".to_string(),
3131                ],
3132                tags: vec!["pipeline".to_string()],
3133                governing_spec: "007-workflow-registry-traversal".to_string(),
3134            },
3135            workflow_path: "workflows/content.comments.pipeline/workflow.json".to_string(),
3136            registered_at: "2026-07-08T00:00:00Z".to_string(),
3137            validator_version: "validator".to_string(),
3138        }
3139    }
3140
3141    fn pipeline_workflow_request() -> WorkflowExecutionRequest {
3142        WorkflowExecutionRequest {
3143            kind: "workflow_execution_request".to_string(),
3144            schema_version: "1.0.0".to_string(),
3145            request_id: "pipeline-request".to_string(),
3146            workflow_id: "content.comments.pipeline".to_string(),
3147            workflow_version: "1.0.0".to_string(),
3148            scope: WorkflowLookupScope::PublicOnly,
3149            input: json!({"note": "Hello world"}),
3150            governing_spec: "007-workflow-registry-traversal".to_string(),
3151        }
3152    }
3153
3154    struct PipelineExecutor;
3155
3156    impl LocalExecutor for PipelineExecutor {
3157        fn execute(
3158            &self,
3159            capability: &ResolvedCapability,
3160            _input: &Value,
3161        ) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
3162            let value = match capability.record.id.as_str() {
3163                "content.comments.pipeline-validate" => json!({"valid": true, "issues": []}),
3164                "content.comments.pipeline-process" => json!({
3165                    "title": "Hello world",
3166                    "tags": ["hello", "world"],
3167                    "noteType": "fleeting",
3168                    "suggestedNextAction": "archive",
3169                    "status": "complete"
3170                }),
3171                _ => json!({"summary": "Hello world (fleeting)", "wordCount": 3}),
3172            };
3173            Ok(LocalExecutionOutput {
3174                value,
3175                emitted_events: Vec::new(),
3176            })
3177        }
3178    }
3179
3180    struct FailingPipelineExecutor;
3181
3182    impl LocalExecutor for FailingPipelineExecutor {
3183        fn execute(
3184            &self,
3185            capability: &ResolvedCapability,
3186            _input: &Value,
3187        ) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
3188            match capability.record.id.as_str() {
3189                "content.comments.pipeline-validate" => Ok(LocalExecutionOutput {
3190                    value: json!({"valid": true, "issues": []}),
3191                    emitted_events: Vec::new(),
3192                }),
3193                other => Err(LocalExecutionFailure {
3194                    code: LocalExecutionFailureCode::ExecutionFailed,
3195                    message: format!("step failed: {other}"),
3196                }),
3197            }
3198        }
3199    }
3200
3201    fn capability_registry_fixture() -> CapabilityRegistry {
3202        build_capability_registry(false, None)
3203    }
3204
3205    fn signed_missing_binary_capability_registry_fixture() -> CapabilityRegistry {
3206        build_capability_registry(
3207            false,
3208            Some(ArtifactSignature {
3209                scheme: ArtifactSignatureScheme::Ed25519,
3210                public_key_hex: Some("00".repeat(32)),
3211                signature_hex: Some("00".repeat(64)),
3212                sigstore_bundle_ref: None,
3213            }),
3214        )
3215    }
3216
3217    #[allow(clippy::too_many_lines, clippy::needless_pass_by_value)]
3218    fn build_capability_registry(
3219        strict_inputs: bool,
3220        signature: Option<ArtifactSignature>,
3221    ) -> CapabilityRegistry {
3222        let mut registry = CapabilityRegistry::new();
3223        for (id, emits, output, required_key) in [
3224            (
3225                "content.comments.create-comment-draft",
3226                vec![EventReference {
3227                    event_id: "content.comments.draft-created".to_string(),
3228                    version: "1.0.0".to_string(),
3229                }],
3230                json!({
3231                    "type": "object",
3232                    "properties": {
3233                        "draft_id": { "type": "string" },
3234                        "emitted_events": { "type": "array" }
3235                    },
3236                    "required": ["draft_id"],
3237                    "additionalProperties": true
3238                }),
3239                "comment_text",
3240            ),
3241            (
3242                "content.comments.validate-comment",
3243                vec![EventReference {
3244                    event_id: "content.comments.validated".to_string(),
3245                    version: "1.0.0".to_string(),
3246                }],
3247                json!({
3248                    "type": "object",
3249                    "properties": {
3250                        "draft_id": { "type": "string" },
3251                        "emitted_events": { "type": "array" }
3252                    },
3253                    "required": ["draft_id"],
3254                    "additionalProperties": true
3255                }),
3256                "draft_id",
3257            ),
3258            (
3259                "content.comments.persist-comment",
3260                vec![],
3261                json!({
3262                    "type": "object",
3263                    "properties": { "comment_id": { "type": "string" } },
3264                    "required": ["comment_id"],
3265                    "additionalProperties": true
3266                }),
3267                "draft_id",
3268            ),
3269        ] {
3270            register_capability_ok(
3271                &mut registry,
3272                CapabilityRegistration {
3273                    scope: RegistryScope::Public,
3274                    contract: capability_contract(
3275                        id,
3276                        emits,
3277                        json!({
3278                            "type": "object",
3279                            "properties": {
3280                                "comment_text": { "type": "string" },
3281                                "draft_id": { "type": "string" }
3282                            },
3283                            "required": if strict_inputs {
3284                                vec![required_key]
3285                            } else {
3286                                Vec::<&str>::new()
3287                            },
3288                            "additionalProperties": true
3289                        }),
3290                        output,
3291                    ),
3292                    contract_path: format!("registry/{id}.json"),
3293                    artifact: CapabilityArtifactRecord {
3294                        artifact_ref: format!("artifact-{id}"),
3295                        implementation_kind: ImplementationKind::Executable,
3296                        source: SourceReference {
3297                            kind: SourceKind::Git,
3298                            location: format!("https://example.com/{id}.git"),
3299                        },
3300                        binary: Some(BinaryReference {
3301                            format: BinaryFormat::Wasm,
3302                            location: format!("{id}.wasm"),
3303                            signature: signature.clone(),
3304                        }),
3305                        workflow_ref: None,
3306                        digests: ArtifactDigests {
3307                            source_digest: "source".to_string(),
3308                            binary_digest: Some("binary".to_string()),
3309                        },
3310                        provenance: RegistryProvenance {
3311                            source: "fixtures".to_string(),
3312                            author: "Enrico".to_string(),
3313                            created_at: "2026-03-27T00:00:00Z".to_string(),
3314                        },
3315                    },
3316                    registered_at: "2026-03-27T00:00:00Z".to_string(),
3317                    tags: vec!["comments".to_string()],
3318                    composability: ComposabilityMetadata {
3319                        kind: CompositionKind::Atomic,
3320                        patterns: vec![CompositionPattern::Sequential],
3321                        provides: vec!["comment".to_string()],
3322                        requires: Vec::new(),
3323                    },
3324                    governing_spec: "005-capability-registry".to_string(),
3325                    validator_version: "validator".to_string(),
3326                },
3327            );
3328        }
3329        registry
3330    }
3331
3332    fn strict_input_capability_registry_fixture() -> CapabilityRegistry {
3333        build_capability_registry(true, None)
3334    }
3335
3336    fn workflow_registry_fixture() -> WorkflowRegistry {
3337        let registry = capability_registry_fixture();
3338        let mut workflows = WorkflowRegistry::new();
3339        register_workflow_ok(
3340            &mut workflows,
3341            &registry,
3342            WorkflowRegistration {
3343                scope: RegistryScope::Public,
3344                definition: workflow_definition_fixture(
3345                    Some(EventReference {
3346                        event_id: "content.comments.validated".to_string(),
3347                        version: "1.0.0".to_string(),
3348                    }),
3349                    None,
3350                ),
3351                workflow_path: "workflows/publish-comment.json".to_string(),
3352                registered_at: "2026-03-27T00:00:00Z".to_string(),
3353                validator_version: "workflow-validator".to_string(),
3354            },
3355        );
3356        workflows
3357    }
3358
3359    fn workflow_definition_fixture(
3360        second_event: Option<EventReference>,
3361        direct_edge: Option<WorkflowEdge>,
3362    ) -> WorkflowDefinition {
3363        let mut edges = vec![
3364            WorkflowEdge {
3365                edge_id: "draft_to_validate".to_string(),
3366                from: "create_draft".to_string(),
3367                to: "validate_comment".to_string(),
3368                trigger: WorkflowEdgeTrigger::Event,
3369                event: Some(EventReference {
3370                    event_id: "content.comments.draft-created".to_string(),
3371                    version: "1.0.0".to_string(),
3372                }),
3373                predicate: None,
3374            },
3375            WorkflowEdge {
3376                edge_id: "validate_to_persist".to_string(),
3377                from: "validate_comment".to_string(),
3378                to: "persist_comment".to_string(),
3379                trigger: WorkflowEdgeTrigger::Event,
3380                event: second_event,
3381                predicate: None,
3382            },
3383        ];
3384        if let Some(edge) = direct_edge {
3385            edges.push(edge);
3386        }
3387        WorkflowDefinition {
3388            kind: "workflow_definition".to_string(),
3389            schema_version: "1.0.0".to_string(),
3390            id: "content.comments.publish-comment".to_string(),
3391            name: "publish-comment".to_string(),
3392            version: "1.0.0".to_string(),
3393            lifecycle: Lifecycle::Active,
3394            owner: Owner {
3395                team: "comments".to_string(),
3396                contact: "comments@example.com".to_string(),
3397            },
3398            summary: "Publish a comment deterministically.".to_string(),
3399            inputs: SchemaContainer {
3400                schema: json!({
3401                    "type": "object",
3402                    "properties": { "comment_text": { "type": "string" } },
3403                    "required": ["comment_text"],
3404                    "additionalProperties": true
3405                }),
3406            },
3407            outputs: SchemaContainer {
3408                schema: json!({
3409                    "type": "object",
3410                    "properties": { "comment_id": { "type": "string" } },
3411                    "required": ["comment_id"],
3412                    "additionalProperties": true
3413                }),
3414            },
3415            nodes: vec![
3416                WorkflowNode {
3417                    node_id: "create_draft".to_string(),
3418                    capability_id: "content.comments.create-comment-draft".to_string(),
3419                    capability_version: "1.0.0".to_string(),
3420                    input: WorkflowNodeInput {
3421                        from_workflow_input: vec!["comment_text".to_string()],
3422                    },
3423                    output: WorkflowNodeOutput {
3424                        to_workflow_state: vec!["draft_id".to_string()],
3425                        publish_to_state_as: None,
3426                    },
3427                },
3428                WorkflowNode {
3429                    node_id: "validate_comment".to_string(),
3430                    capability_id: "content.comments.validate-comment".to_string(),
3431                    capability_version: "1.0.0".to_string(),
3432                    input: WorkflowNodeInput {
3433                        from_workflow_input: vec!["draft_id".to_string()],
3434                    },
3435                    output: WorkflowNodeOutput {
3436                        to_workflow_state: vec!["draft_id".to_string()],
3437                        publish_to_state_as: None,
3438                    },
3439                },
3440                WorkflowNode {
3441                    node_id: "persist_comment".to_string(),
3442                    capability_id: "content.comments.persist-comment".to_string(),
3443                    capability_version: "1.0.0".to_string(),
3444                    input: WorkflowNodeInput {
3445                        from_workflow_input: vec!["draft_id".to_string()],
3446                    },
3447                    output: WorkflowNodeOutput {
3448                        to_workflow_state: vec!["comment_id".to_string()],
3449                        publish_to_state_as: None,
3450                    },
3451                },
3452            ],
3453            edges,
3454            start_node: "create_draft".to_string(),
3455            terminal_nodes: vec!["persist_comment".to_string()],
3456            output_projection: Vec::new(),
3457            tags: vec!["comments".to_string()],
3458            governing_spec: "007-workflow-registry-traversal".to_string(),
3459        }
3460    }
3461
3462    fn capability_contract(
3463        id: &str,
3464        emits: Vec<EventReference>,
3465        inputs: Value,
3466        outputs: Value,
3467    ) -> CapabilityContract {
3468        // Subscribable requires a non-empty event_trigger (registry
3469        // ContractValidationFailed otherwise); only capabilities that
3470        // actually declare `emits` need to be Subscribable at all (spec
3471        // 101-local-executor-event-emission FR-007/FR-008 rejects a
3472        // non-Subscribable capability's emitted events).
3473        let has_emits = !emits.is_empty();
3474        CapabilityContract {
3475            kind: "capability_contract".to_string(),
3476            schema_version: "1.0.0".to_string(),
3477            id: id.to_string(),
3478            namespace: "content.comments".to_string(),
3479            name: id.rsplit('.').next().unwrap_or("capability").to_string(),
3480            version: "1.0.0".to_string(),
3481            lifecycle: Lifecycle::Active,
3482            owner: Owner {
3483                team: "comments".to_string(),
3484                contact: "comments@example.com".to_string(),
3485            },
3486            summary: "workflow fixture capability".to_string(),
3487            description: "workflow fixture capability used in runtime tests".to_string(),
3488            inputs: SchemaContainer { schema: inputs },
3489            outputs: SchemaContainer { schema: outputs },
3490            preconditions: vec![Condition {
3491                id: "precondition".to_string(),
3492                description: "must be valid".to_string(),
3493            }],
3494            postconditions: vec![Condition {
3495                id: "postcondition".to_string(),
3496                description: "must produce output".to_string(),
3497            }],
3498            side_effects: vec![SideEffect {
3499                kind: SideEffectKind::MemoryOnly,
3500                description: "memory only".to_string(),
3501            }],
3502            emits,
3503            consumes: Vec::new(),
3504            permissions: vec![IdReference {
3505                id: "permission".to_string(),
3506            }],
3507            execution: Execution {
3508                binary_format: ContractBinaryFormat::Wasm,
3509                entrypoint: Entrypoint {
3510                    kind: EntrypointKind::WasiCommand,
3511                    command: "run".to_string(),
3512                },
3513                preferred_targets: vec![ExecutionTarget::Local],
3514                constraints: ExecutionConstraints {
3515                    host_api_access: HostApiAccess::None,
3516                    network_access: NetworkAccess::Forbidden,
3517                    filesystem_access: FilesystemAccess::None,
3518                },
3519            },
3520            policies: Vec::new(),
3521            dependencies: Vec::new(),
3522            provenance: Provenance {
3523                source: ProvenanceSource::Greenfield,
3524                author: "Enrico".to_string(),
3525                created_at: "2026-03-27T00:00:00Z".to_string(),
3526                spec_ref: Some("007-workflow-registry-traversal".to_string()),
3527                adr_refs: Vec::new(),
3528                exception_refs: Vec::new(),
3529            },
3530            evidence: vec![ValidationEvidence {
3531                evidence_id: "evidence".to_string(),
3532                evidence_type: EvidenceType::ContractValidation,
3533                status: EvidenceStatus::Passed,
3534            }],
3535            service_type: if has_emits {
3536                ServiceType::Subscribable
3537            } else {
3538                ServiceType::Stateless
3539            },
3540            permitted_targets: vec![
3541                ExecutionTarget::Local,
3542                ExecutionTarget::Cloud,
3543                ExecutionTarget::Edge,
3544                ExecutionTarget::Device,
3545            ],
3546            event_trigger: if has_emits {
3547                Some(format!("{id}.triggered"))
3548            } else {
3549                None
3550            },
3551            connector_requirements: Vec::new(),
3552            state_schema: None,
3553            use_cases: Vec::new(),
3554            risk: traverse_contracts::default_risk_metadata(),
3555        }
3556    }
3557
3558    fn valid_workflow_request() -> WorkflowExecutionRequest {
3559        WorkflowExecutionRequest {
3560            kind: "workflow_execution_request".to_string(),
3561            schema_version: "1.0.0".to_string(),
3562            request_id: "workflow-request".to_string(),
3563            workflow_id: "content.comments.publish-comment".to_string(),
3564            workflow_version: "1.0.0".to_string(),
3565            scope: WorkflowLookupScope::PublicOnly,
3566            input: json!({"comment_text": "hello"}),
3567            governing_spec: "007-workflow-registry-traversal".to_string(),
3568        }
3569    }
3570
3571    fn valid_runtime_request() -> RuntimeRequest {
3572        RuntimeRequest {
3573            kind: "runtime_request".to_string(),
3574            schema_version: "1.0.0".to_string(),
3575            request_id: "runtime-request".to_string(),
3576            intent: RuntimeIntent {
3577                capability_id: Some("content.comments.publish-comment".to_string()),
3578                capability_version: Some("1.0.0".to_string()),
3579                version_range: None,
3580                intent_key: None,
3581            },
3582            input: json!({"comment_text": "hello"}),
3583            lookup: RuntimeLookup {
3584                scope: RuntimeLookupScope::PublicOnly,
3585                allow_ambiguity: false,
3586            },
3587            context: RuntimeContext {
3588                requested_target: crate::PlacementTarget::Local,
3589                correlation_id: None,
3590                caller: None,
3591                traceparent: None,
3592                tracestate: None,
3593                metadata: None,
3594                identity: None,
3595            },
3596            governing_spec: "006-runtime-request-execution".to_string(),
3597        }
3598    }
3599
3600    struct WorkflowExecutor;
3601
3602    impl LocalExecutor for WorkflowExecutor {
3603        fn execute(
3604            &self,
3605            capability: &ResolvedCapability,
3606            _input: &Value,
3607        ) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
3608            let (value, emitted_events) = match capability.record.id.as_str() {
3609                "content.comments.create-comment-draft" => (
3610                    json!({"draft_id": "draft-1"}),
3611                    vec![sample_traverse_event(
3612                        "content.comments.draft-created",
3613                        "1.0.0",
3614                    )],
3615                ),
3616                "content.comments.validate-comment" => (
3617                    json!({"draft_id": "draft-1"}),
3618                    vec![sample_traverse_event("content.comments.validated", "1.0.0")],
3619                ),
3620                "content.comments.persist-comment" => {
3621                    (json!({"comment_id": "comment-1"}), Vec::new())
3622                }
3623                _ => (json!({}), Vec::new()),
3624            };
3625            Ok(LocalExecutionOutput {
3626                value,
3627                emitted_events,
3628            })
3629        }
3630    }
3631
3632    struct FailingWorkflowExecutor;
3633
3634    impl LocalExecutor for FailingWorkflowExecutor {
3635        fn execute(
3636            &self,
3637            _capability: &ResolvedCapability,
3638            _input: &Value,
3639        ) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
3640            Err(LocalExecutionFailure {
3641                code: LocalExecutionFailureCode::ExecutionFailed,
3642                message: "boom".to_string(),
3643            })
3644        }
3645    }
3646
3647    struct MissingEventWorkflowExecutor;
3648
3649    struct BadOutputWorkflowExecutor;
3650
3651    impl LocalExecutor for MissingEventWorkflowExecutor {
3652        fn execute(
3653            &self,
3654            capability: &ResolvedCapability,
3655            _input: &Value,
3656        ) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
3657            let (value, emitted_events) = match capability.record.id.as_str() {
3658                "content.comments.create-comment-draft" => (
3659                    json!({"draft_id": "draft-1"}),
3660                    vec![sample_traverse_event(
3661                        "content.comments.draft-created",
3662                        "1.0.0",
3663                    )],
3664                ),
3665                "content.comments.validate-comment" => (json!({"draft_id": "draft-1"}), Vec::new()),
3666                "content.comments.persist-comment" => {
3667                    (json!({"comment_id": "comment-1"}), Vec::new())
3668                }
3669                _ => (json!({}), Vec::new()),
3670            };
3671            Ok(LocalExecutionOutput {
3672                value,
3673                emitted_events,
3674            })
3675        }
3676    }
3677
3678    impl LocalExecutor for BadOutputWorkflowExecutor {
3679        fn execute(
3680            &self,
3681            capability: &ResolvedCapability,
3682            _input: &Value,
3683        ) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
3684            let (value, emitted_events) = match capability.record.id.as_str() {
3685                "content.comments.create-comment-draft" => (
3686                    json!({}),
3687                    vec![sample_traverse_event(
3688                        "content.comments.draft-created",
3689                        "1.0.0",
3690                    )],
3691                ),
3692                _ => (json!({}), Vec::new()),
3693            };
3694            Ok(LocalExecutionOutput {
3695                value,
3696                emitted_events,
3697            })
3698        }
3699    }
3700
3701    struct UndeclaredEventWorkflowExecutor;
3702
3703    impl LocalExecutor for UndeclaredEventWorkflowExecutor {
3704        fn execute(
3705            &self,
3706            _capability: &ResolvedCapability,
3707            _input: &Value,
3708        ) -> Result<LocalExecutionOutput, LocalExecutionFailure> {
3709            // The workflow under test in `workflow_failures_cover_not_found_missing_events_and_step_failures`
3710            // fails FR-007/FR-008 validation at its start node, so this
3711            // executor is only ever invoked for
3712            // "content.comments.create-comment-draft" and never reaches a
3713            // second node.
3714            Ok(LocalExecutionOutput {
3715                value: json!({"draft_id": "draft-1"}),
3716                emitted_events: vec![sample_traverse_event(
3717                    "content.comments.undeclared-event",
3718                    "1.0.0",
3719                )],
3720            })
3721        }
3722    }
3723
3724    fn register_capability_ok(registry: &mut CapabilityRegistry, request: CapabilityRegistration) {
3725        match registry.register(request) {
3726            Ok(_) => {}
3727            Err(error) => unreachable!("{error:?}"),
3728        }
3729    }
3730
3731    fn register_workflow_ok(
3732        registry: &mut WorkflowRegistry,
3733        capabilities: &CapabilityRegistry,
3734        request: WorkflowRegistration,
3735    ) {
3736        match registry.register(capabilities, request) {
3737            Ok(_) => {}
3738            Err(error) => unreachable!("{error:?}"),
3739        }
3740    }
3741
3742    #[test]
3743    fn helper_guards_cover_unreachable_branches() {
3744        let capability_panic = std::panic::catch_unwind(|| {
3745            register_capability_ok(
3746                &mut CapabilityRegistry::new(),
3747                CapabilityRegistration {
3748                    scope: RegistryScope::Public,
3749                    contract: capability_contract("bad", Vec::new(), json!({}), json!({})),
3750                    contract_path: String::new(),
3751                    artifact: workflow_artifact_record("bad", "1.0.0", "artifact"),
3752                    registered_at: String::new(),
3753                    tags: Vec::new(),
3754                    composability: ComposabilityMetadata {
3755                        kind: CompositionKind::Atomic,
3756                        patterns: Vec::new(),
3757                        provides: Vec::new(),
3758                        requires: Vec::new(),
3759                    },
3760                    governing_spec: "005-capability-registry".to_string(),
3761                    validator_version: "validator".to_string(),
3762                },
3763            );
3764        });
3765        assert!(capability_panic.is_err());
3766
3767        let workflow_panic = std::panic::catch_unwind(|| {
3768            register_workflow_ok(
3769                &mut WorkflowRegistry::new(),
3770                &CapabilityRegistry::new(),
3771                WorkflowRegistration {
3772                    scope: RegistryScope::Public,
3773                    definition: workflow_definition_fixture(
3774                        None,
3775                        Some(WorkflowEdge {
3776                            edge_id: "direct".to_string(),
3777                            from: "create_draft".to_string(),
3778                            to: "validate_comment".to_string(),
3779                            trigger: WorkflowEdgeTrigger::Direct,
3780                            event: None,
3781                            predicate: None,
3782                        }),
3783                    ),
3784                    workflow_path: String::new(),
3785                    registered_at: String::new(),
3786                    validator_version: "validator".to_string(),
3787                },
3788            );
3789        });
3790        assert!(workflow_panic.is_err());
3791    }
3792
3793    fn resolved_workflow(definition: WorkflowDefinition) -> ResolvedWorkflow {
3794        ResolvedWorkflow {
3795            record: WorkflowRegistryRecord {
3796                scope: RegistryScope::Public,
3797                id: definition.id.clone(),
3798                version: definition.version.clone(),
3799                lifecycle: definition.lifecycle.clone(),
3800                owner: definition.owner.clone(),
3801                workflow_path: "workflows/manual.json".to_string(),
3802                workflow_digest: "digest".to_string(),
3803                registered_at: "2026-03-27T00:00:00Z".to_string(),
3804                governing_spec: "007-workflow-registry-traversal".to_string(),
3805                validator_version: "validator".to_string(),
3806                evidence: traverse_registry::WorkflowRegistrationEvidence {
3807                    evidence_id: "evidence".to_string(),
3808                    workflow_id: definition.id.clone(),
3809                    workflow_version: definition.version.clone(),
3810                    scope: RegistryScope::Public,
3811                    governing_spec: "007-workflow-registry-traversal".to_string(),
3812                    validator_version: "validator".to_string(),
3813                    produced_at: "2026-03-27T00:00:00Z".to_string(),
3814                    result: traverse_registry::WorkflowRegistrationResult::Passed,
3815                },
3816            },
3817            index_entry: traverse_registry::WorkflowDiscoveryIndexEntry {
3818                scope: RegistryScope::Public,
3819                id: definition.id.clone(),
3820                version: definition.version.clone(),
3821                lifecycle: definition.lifecycle.clone(),
3822                owner: definition.owner.clone(),
3823                summary: definition.summary.clone(),
3824                tags: definition.tags.clone(),
3825                participating_capabilities: definition
3826                    .nodes
3827                    .iter()
3828                    .map(|node| node.capability_id.clone())
3829                    .collect(),
3830                events_used: Vec::new(),
3831                start_node: definition.start_node.clone(),
3832                terminal_nodes: definition.terminal_nodes.clone(),
3833                registered_at: "2026-03-27T00:00:00Z".to_string(),
3834            },
3835            definition,
3836        }
3837    }
3838}