Skip to main content

traverse_runtime/
workflows.rs

1use crate::security::{RuntimeWarning, verify_artifact};
2use crate::{
3    ExecutionFailureReason, ExecutionFailureState, LocalExecutor, Runtime, RuntimeError,
4    RuntimeErrorCode, RuntimeExecutionOutcome, execution_failure_outcome, runtime_error,
5    successful_execution_outcome, validate_payload_against_contract,
6};
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value, json};
9use std::collections::BTreeSet;
10use traverse_contracts::EventReference;
11use traverse_registry::{
12    LookupScope, RegistryScope, ResolvedCapability, ResolvedWorkflow, WorkflowEdge,
13    WorkflowEdgePredicate, WorkflowEdgeTrigger, WorkflowNode,
14};
15
16const WORKFLOW_REQUEST_KIND: &str = "workflow_execution_request";
17const WORKFLOW_EVIDENCE_KIND: &str = "workflow_traversal_evidence";
18const WORKFLOW_SCHEMA_VERSION: &str = "1.0.0";
19const WORKFLOW_GOVERNING_SPEC: &str = "007-workflow-registry-traversal";
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct WorkflowExecutionRequest {
23    pub kind: String,
24    pub schema_version: String,
25    pub request_id: String,
26    pub workflow_id: String,
27    pub workflow_version: String,
28    pub scope: WorkflowLookupScope,
29    pub input: Value,
30    pub governing_spec: String,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum WorkflowLookupScope {
36    PublicOnly,
37    PreferPrivate,
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct WorkflowTraversalEvidence {
42    pub kind: String,
43    pub schema_version: String,
44    pub trace_id: String,
45    pub request_id: String,
46    pub workflow_id: String,
47    pub workflow_version: String,
48    pub governing_spec: String,
49    pub visited_nodes: Vec<WorkflowTraversalStepRecord>,
50    pub traversed_edges: Vec<WorkflowTraversalEdgeRecord>,
51    pub emitted_events: Vec<EventReference>,
52    #[serde(default)]
53    pub waiting_edges: Vec<WaitingWorkflowEdgeContext>,
54    #[serde(default)]
55    pub event_match_records: Vec<EventMatchRecord>,
56    #[serde(default)]
57    pub event_wake_decisions: Vec<EventWakeDecision>,
58    #[serde(default)]
59    pub event_consumptions: Vec<EventConsumptionRecord>,
60    pub result: WorkflowTraversalResult,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct WorkflowTraversalStepRecord {
65    pub step_index: usize,
66    pub node_id: String,
67    pub capability_id: String,
68    pub capability_version: String,
69    pub status: WorkflowTraversalStepStatus,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74pub enum WorkflowTraversalStepStatus {
75    Entered,
76    Completed,
77    Failed,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct WorkflowTraversalEdgeRecord {
82    pub edge_id: String,
83    pub from: String,
84    pub to: String,
85    pub trigger: WorkflowTraversalTrigger,
86    #[serde(default)]
87    pub event: Option<EventReference>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct WaitingWorkflowEdgeContext {
92    pub workflow_execution_id: String,
93    pub edge_id: String,
94    pub from_node_id: String,
95    pub to_node_id: String,
96    pub event_ref: EventReference,
97    #[serde(default)]
98    pub predicate: Option<WorkflowEdgePredicate>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
102pub struct EventMatchRecord {
103    pub event_id: String,
104    pub event_version: String,
105    pub edge_id: String,
106    pub match_result: EventMatchResult,
107    #[serde(default)]
108    pub predicate_result: Option<EventPredicateResult>,
109    #[serde(default)]
110    pub rejection_reason: Option<String>,
111    pub recorded_at: String,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116pub enum EventMatchResult {
117    Matched,
118    NotMatched,
119    AlreadyConsumed,
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "snake_case")]
124pub enum EventPredicateResult {
125    Passed,
126    Failed,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
130pub struct EventWakeDecision {
131    pub decision_type: String,
132    pub event_id: String,
133    pub event_version: String,
134    pub edge_id: String,
135    pub workflow_execution_id: String,
136    pub wake_order: usize,
137    pub result: EventWakeDecisionResult,
138    pub recorded_at: String,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(rename_all = "snake_case")]
143pub enum EventWakeDecisionResult {
144    Taken,
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct EventConsumptionRecord {
149    pub event_id: String,
150    pub event_version: String,
151    pub edge_id: String,
152    pub workflow_execution_id: String,
153    pub consumed_at: String,
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "snake_case")]
158pub enum WorkflowTraversalTrigger {
159    Direct,
160    Event,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct WorkflowTraversalResult {
165    pub status: WorkflowTraversalStatus,
166    #[serde(default)]
167    pub failure_reason: Option<WorkflowTraversalFailureReason>,
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum WorkflowTraversalStatus {
173    Completed,
174    Error,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case")]
179pub enum WorkflowTraversalFailureReason {
180    WorkflowNotFound,
181    WorkflowInvalid,
182    AmbiguousNextEdge,
183    MissingRequiredEvent,
184    TerminalNodeNotReached,
185    StepExecutionFailed,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189pub struct WorkflowExecutionResult {
190    pub kind: String,
191    pub schema_version: String,
192    pub request_id: String,
193    pub workflow_id: String,
194    pub workflow_version: String,
195    pub status: WorkflowTraversalStatus,
196    #[serde(default)]
197    pub output: Option<Value>,
198    #[serde(default)]
199    pub error: Option<RuntimeError>,
200    #[serde(default)]
201    pub warnings: Vec<RuntimeWarning>,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct WorkflowExecutionOutcome {
206    pub result: WorkflowExecutionResult,
207    pub evidence: WorkflowTraversalEvidence,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq)]
211struct EmittedEventRecord {
212    record_id: String,
213    event: EventReference,
214    payload: Option<Value>,
215}
216
217#[derive(Debug, Clone, Default, PartialEq, Eq)]
218struct WorkflowEventEvidenceBundle {
219    waiting_edges: Vec<WaitingWorkflowEdgeContext>,
220    match_records: Vec<EventMatchRecord>,
221    wake_decisions: Vec<EventWakeDecision>,
222    consumptions: Vec<EventConsumptionRecord>,
223}
224
225#[derive(Debug, Clone, Default, PartialEq, Eq)]
226struct EventDrivenEvaluationOutcome {
227    taken_edge_ids: Vec<String>,
228    evidence: WorkflowEventEvidenceBundle,
229}
230
231impl<E> Runtime<E>
232where
233    E: LocalExecutor,
234{
235    #[must_use]
236    #[allow(clippy::needless_pass_by_value)]
237    pub fn execute_workflow(&self, request: WorkflowExecutionRequest) -> WorkflowExecutionOutcome {
238        if let Some(error) = validate_workflow_request(&request) {
239            return workflow_failure(
240                &request,
241                WorkflowTraversalFailureReason::WorkflowInvalid,
242                error,
243                Vec::new(),
244                Vec::new(),
245                Vec::new(),
246                WorkflowEventEvidenceBundle::default(),
247                Vec::new(),
248            );
249        }
250
251        let lookup_scope = map_workflow_lookup_scope(request.scope);
252        let Some(workflow) = self.workflow_registry.find_exact(
253            lookup_scope,
254            &request.workflow_id,
255            &request.workflow_version,
256        ) else {
257            return workflow_failure(
258                &request,
259                WorkflowTraversalFailureReason::WorkflowNotFound,
260                runtime_error(
261                    RuntimeErrorCode::CapabilityNotFound,
262                    "workflow definition was not found in the workflow registry",
263                    json!({"workflow_id": request.workflow_id, "workflow_version": request.workflow_version}),
264                ),
265                Vec::new(),
266                Vec::new(),
267                Vec::new(),
268                WorkflowEventEvidenceBundle::default(),
269                Vec::new(),
270            );
271        };
272
273        if let Err(error) = validate_payload_against_contract(
274            &request.input,
275            &workflow.definition.inputs.schema,
276            RuntimeErrorCode::RequestInvalid,
277            "workflow request input does not satisfy the workflow input contract",
278        ) {
279            return workflow_failure(
280                &request,
281                WorkflowTraversalFailureReason::WorkflowInvalid,
282                error,
283                Vec::new(),
284                Vec::new(),
285                Vec::new(),
286                WorkflowEventEvidenceBundle::default(),
287                Vec::new(),
288            );
289        }
290
291        match self.traverse_workflow(&request, &workflow) {
292            Ok(success) => success,
293            Err(failure) => failure,
294        }
295    }
296
297    pub(crate) fn execute_workflow_capability(
298        &self,
299        mut context: crate::ExecutionContext,
300        selected: &ResolvedCapability,
301        started_execution: crate::StartedExecution,
302    ) -> RuntimeExecutionOutcome {
303        let Some(workflow_ref) = selected.artifact.workflow_ref.as_ref() else {
304            let error = runtime_error(
305                RuntimeErrorCode::ArtifactMissing,
306                "workflow-backed capability is missing its workflow reference",
307                json!({"artifact_ref": selected.record.artifact_ref}),
308            );
309            return execution_failure_outcome(
310                context,
311                ExecutionFailureState {
312                    artifact_ref: selected.record.artifact_ref.clone(),
313                    started_at: started_execution.started_at,
314                    placement: started_execution.placement.clone(),
315                    failure_reason: ExecutionFailureReason::ArtifactMissing,
316                },
317                error,
318                Vec::new(),
319                None,
320            );
321        };
322
323        let workflow_scope = match selected.record.scope {
324            RegistryScope::Public => WorkflowLookupScope::PublicOnly,
325            RegistryScope::Private => WorkflowLookupScope::PreferPrivate,
326        };
327        let workflow = self.execute_workflow(WorkflowExecutionRequest {
328            kind: WORKFLOW_REQUEST_KIND.to_string(),
329            schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
330            request_id: context.attempt.request.request_id.clone(),
331            workflow_id: workflow_ref.workflow_id.clone(),
332            workflow_version: workflow_ref.workflow_version.clone(),
333            scope: workflow_scope,
334            input: context.attempt.request.input.clone(),
335            governing_spec: WORKFLOW_GOVERNING_SPEC.to_string(),
336        });
337        context
338            .attempt
339            .warnings
340            .extend(workflow.result.warnings.iter().cloned());
341
342        match workflow.result.status {
343            WorkflowTraversalStatus::Completed => {
344                let output = workflow.result.output.unwrap_or(Value::Object(Map::new()));
345                let workflow_evidence = workflow.evidence;
346                let emitted_events = workflow_evidence.emitted_events.clone();
347                successful_execution_outcome(
348                    context,
349                    selected,
350                    started_execution,
351                    output,
352                    emitted_events,
353                    Some(workflow_evidence),
354                )
355            }
356            WorkflowTraversalStatus::Error => {
357                let workflow_evidence = workflow.evidence;
358                let emitted_events = workflow_evidence.emitted_events.clone();
359                execution_failure_outcome(
360                    context,
361                    ExecutionFailureState {
362                        artifact_ref: selected.record.artifact_ref.clone(),
363                        started_at: started_execution.started_at,
364                        placement: started_execution.placement,
365                        failure_reason: ExecutionFailureReason::ExecutionFailed,
366                    },
367                    workflow.result.error.unwrap_or(runtime_error(
368                        RuntimeErrorCode::ExecutionFailed,
369                        "workflow-backed capability execution failed",
370                        json!({}),
371                    )),
372                    emitted_events,
373                    Some(workflow_evidence),
374                )
375            }
376        }
377    }
378
379    #[allow(clippy::result_large_err, clippy::too_many_lines)]
380    fn traverse_workflow(
381        &self,
382        request: &WorkflowExecutionRequest,
383        workflow: &ResolvedWorkflow,
384    ) -> Result<WorkflowExecutionOutcome, WorkflowExecutionOutcome> {
385        let mut state = workflow_state(&request.input);
386        let mut current = workflow.definition.start_node.clone();
387        let mut step_index = 0;
388        let mut visited = Vec::new();
389        let mut traversed = Vec::new();
390        let mut emitted = Vec::new();
391        let mut event_evidence = WorkflowEventEvidenceBundle::default();
392        let mut consumed_event_edges = BTreeSet::new();
393        let mut warnings: Vec<RuntimeWarning> = Vec::new();
394        let workflow_execution_id = format!("workflow_exec_{}", request.request_id);
395
396        loop {
397            let Some(node) = workflow
398                .definition
399                .nodes
400                .iter()
401                .find(|node| node.node_id == current)
402            else {
403                return Err(workflow_failure(
404                    request,
405                    WorkflowTraversalFailureReason::WorkflowInvalid,
406                    runtime_error(
407                        RuntimeErrorCode::ExecutionFailed,
408                        "workflow node could not be resolved during traversal",
409                        json!({"node_id": current}),
410                    ),
411                    visited,
412                    traversed,
413                    emitted,
414                    event_evidence,
415                    warnings,
416                ));
417            };
418
419            visited.push(WorkflowTraversalStepRecord {
420                step_index,
421                node_id: node.node_id.clone(),
422                capability_id: node.capability_id.clone(),
423                capability_version: node.capability_version.clone(),
424                status: WorkflowTraversalStepStatus::Entered,
425            });
426
427            let lookup_scope = map_workflow_lookup_scope(request.scope);
428            let Some(capability) = self.registry.find_exact(
429                lookup_scope,
430                &node.capability_id,
431                &node.capability_version,
432            ) else {
433                return Err(workflow_failure(
434                    request,
435                    WorkflowTraversalFailureReason::WorkflowInvalid,
436                    runtime_error(
437                        RuntimeErrorCode::CapabilityNotFound,
438                        "workflow node capability was not found in the capability registry",
439                        json!({"capability_id": node.capability_id, "capability_version": node.capability_version}),
440                    ),
441                    visited,
442                    traversed,
443                    emitted,
444                    event_evidence,
445                    warnings,
446                ));
447            };
448
449            // Artifact security gate (spec 030-security-identity-model FR-013):
450            // workflow steps apply the same verification as direct capability
451            // execution, so an artifact rejected by `Runtime::execute` cannot
452            // run by being wrapped in a workflow pipeline step (spec 058).
453            let artifact_bytes = match crate::load_artifact_bytes_for_verification(&capability) {
454                Ok(bytes) => bytes,
455                Err(error) => {
456                    let mut failed = visited;
457                    if let Some(last) = failed.last_mut() {
458                        last.status = WorkflowTraversalStepStatus::Failed;
459                    }
460                    return Err(workflow_failure(
461                        request,
462                        WorkflowTraversalFailureReason::StepExecutionFailed,
463                        error,
464                        failed,
465                        traversed,
466                        emitted,
467                        event_evidence,
468                        warnings,
469                    ));
470                }
471            };
472            match verify_artifact(&capability, &artifact_bytes, &self.security) {
473                Ok(record) => {
474                    if let Some(code) = record.warning_code {
475                        warnings.push(RuntimeWarning {
476                            code,
477                            message:
478                                "unsigned local/dev artifact allowed by development security mode"
479                                    .to_string(),
480                        });
481                    }
482                }
483                Err(failure) => {
484                    let mut failed = visited;
485                    if let Some(last) = failed.last_mut() {
486                        last.status = WorkflowTraversalStepStatus::Failed;
487                    }
488                    return Err(workflow_failure(
489                        request,
490                        WorkflowTraversalFailureReason::StepExecutionFailed,
491                        runtime_error(
492                            RuntimeErrorCode::ContractViolation,
493                            "artifact signature verification failed before workflow step execution",
494                            json!({
495                                "code": failure.code(),
496                                "artifact_verification": failure.record(),
497                                "node_id": node.node_id,
498                            }),
499                        ),
500                        failed,
501                        traversed,
502                        emitted,
503                        event_evidence,
504                        warnings,
505                    ));
506                }
507            }
508
509            let node_input = node_input(&state, node);
510            if let Err(error) = validate_payload_against_contract(
511                &node_input,
512                &capability.contract.inputs.schema,
513                RuntimeErrorCode::RequestInvalid,
514                "workflow node input does not satisfy the capability input contract",
515            ) {
516                let mut failed = visited;
517                if let Some(last) = failed.last_mut() {
518                    last.status = WorkflowTraversalStepStatus::Failed;
519                }
520                return Err(workflow_failure(
521                    request,
522                    WorkflowTraversalFailureReason::StepExecutionFailed,
523                    error,
524                    failed,
525                    traversed,
526                    emitted,
527                    event_evidence,
528                    warnings,
529                ));
530            }
531
532            let output = match self.executor.execute(&capability, &node_input) {
533                Ok(output) => output,
534                Err(failure) => {
535                    let mut failed = visited;
536                    if let Some(last) = failed.last_mut() {
537                        last.status = WorkflowTraversalStepStatus::Failed;
538                    }
539                    return Err(workflow_failure(
540                        request,
541                        WorkflowTraversalFailureReason::StepExecutionFailed,
542                        runtime_error(
543                            RuntimeErrorCode::ExecutionFailed,
544                            &failure.message,
545                            json!({"code": format!("{:?}", failure.code)}),
546                        ),
547                        failed,
548                        traversed,
549                        emitted,
550                        event_evidence,
551                        warnings,
552                    ));
553                }
554            };
555
556            if let Err(error) = validate_payload_against_contract(
557                &output,
558                &capability.contract.outputs.schema,
559                RuntimeErrorCode::OutputValidationFailed,
560                "workflow node output does not satisfy the capability output contract",
561            ) {
562                let mut failed = visited;
563                if let Some(last) = failed.last_mut() {
564                    last.status = WorkflowTraversalStepStatus::Failed;
565                }
566                return Err(workflow_failure(
567                    request,
568                    WorkflowTraversalFailureReason::StepExecutionFailed,
569                    error,
570                    failed,
571                    traversed,
572                    emitted,
573                    event_evidence,
574                    warnings,
575                ));
576            }
577
578            update_state(&mut state, node, &output);
579            let node_emitted = emitted_events(&output);
580            emitted.extend(node_emitted.iter().map(|record| record.event.clone()));
581            if let Some(last) = visited.last_mut() {
582                last.status = WorkflowTraversalStepStatus::Completed;
583            }
584
585            let outgoing = workflow
586                .definition
587                .edges
588                .iter()
589                .filter(|edge| edge.from == node.node_id)
590                .cloned()
591                .collect::<Vec<_>>();
592            let direct = outgoing
593                .iter()
594                .filter(|edge| edge.trigger == WorkflowEdgeTrigger::Direct)
595                .cloned()
596                .collect::<Vec<_>>();
597            if direct.len() > 1 {
598                return Err(workflow_failure(
599                    request,
600                    WorkflowTraversalFailureReason::AmbiguousNextEdge,
601                    runtime_error(
602                        RuntimeErrorCode::ExecutionFailed,
603                        "workflow traversal found more than one direct next edge",
604                        json!({"node_id": node.node_id}),
605                    ),
606                    visited,
607                    traversed,
608                    emitted,
609                    event_evidence,
610                    warnings,
611                ));
612            }
613            if let Some(edge) = direct.into_iter().next() {
614                traversed.push(edge_record(&edge));
615                current = edge.to;
616                step_index += 1;
617                continue;
618            }
619
620            let waiting_edges = waiting_edge_contexts(
621                &workflow_execution_id,
622                outgoing
623                    .iter()
624                    .filter(|edge| edge.trigger == WorkflowEdgeTrigger::Event)
625                    .cloned()
626                    .collect::<Vec<_>>()
627                    .as_slice(),
628            );
629            if !waiting_edges.is_empty() {
630                event_evidence.waiting_edges.extend(waiting_edges.clone());
631            }
632            let evaluation = evaluate_event_driven_edges(
633                &waiting_edges,
634                &node_emitted,
635                &mut consumed_event_edges,
636                &format!("{}:step:{step_index}", request.request_id),
637            );
638            event_evidence
639                .match_records
640                .extend(evaluation.evidence.match_records.iter().cloned());
641            event_evidence
642                .wake_decisions
643                .extend(evaluation.evidence.wake_decisions.iter().cloned());
644            event_evidence
645                .consumptions
646                .extend(evaluation.evidence.consumptions.iter().cloned());
647            let matched_event_edges = outgoing
648                .iter()
649                .filter(|edge| {
650                    evaluation
651                        .taken_edge_ids
652                        .iter()
653                        .any(|edge_id| edge_id == &edge.edge_id)
654                })
655                .cloned()
656                .collect::<Vec<_>>();
657            if matched_event_edges.len() > 1 {
658                return Err(workflow_failure(
659                    request,
660                    WorkflowTraversalFailureReason::AmbiguousNextEdge,
661                    runtime_error(
662                        RuntimeErrorCode::ExecutionFailed,
663                        "workflow traversal found more than one event next edge",
664                        json!({"node_id": node.node_id}),
665                    ),
666                    visited,
667                    traversed,
668                    emitted,
669                    event_evidence,
670                    warnings,
671                ));
672            }
673            if let Some(edge) = matched_event_edges.into_iter().next() {
674                traversed.push(edge_record(&edge));
675                current = edge.to;
676                step_index += 1;
677                continue;
678            }
679
680            if workflow.definition.terminal_nodes.contains(&node.node_id) {
681                let final_output =
682                    final_workflow_output(&state, &workflow.definition.output_projection);
683                if let Err(error) = validate_payload_against_contract(
684                    &final_output,
685                    &workflow.definition.outputs.schema,
686                    RuntimeErrorCode::OutputValidationFailed,
687                    "workflow output does not satisfy the workflow output contract",
688                ) {
689                    return Err(workflow_failure(
690                        request,
691                        WorkflowTraversalFailureReason::WorkflowInvalid,
692                        error,
693                        visited,
694                        traversed,
695                        emitted,
696                        event_evidence,
697                        warnings,
698                    ));
699                }
700
701                let evidence = WorkflowTraversalEvidence {
702                    kind: WORKFLOW_EVIDENCE_KIND.to_string(),
703                    schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
704                    trace_id: format!("workflow_trace_{}", request.request_id),
705                    request_id: request.request_id.clone(),
706                    workflow_id: workflow.definition.id.clone(),
707                    workflow_version: workflow.definition.version.clone(),
708                    governing_spec: WORKFLOW_GOVERNING_SPEC.to_string(),
709                    visited_nodes: visited,
710                    traversed_edges: traversed,
711                    emitted_events: emitted,
712                    waiting_edges: event_evidence.waiting_edges,
713                    event_match_records: event_evidence.match_records,
714                    event_wake_decisions: event_evidence.wake_decisions,
715                    event_consumptions: event_evidence.consumptions,
716                    result: WorkflowTraversalResult {
717                        status: WorkflowTraversalStatus::Completed,
718                        failure_reason: None,
719                    },
720                };
721
722                return Ok(WorkflowExecutionOutcome {
723                    result: WorkflowExecutionResult {
724                        kind: WORKFLOW_REQUEST_KIND.to_string(),
725                        schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
726                        request_id: request.request_id.clone(),
727                        workflow_id: workflow.definition.id.clone(),
728                        workflow_version: workflow.definition.version.clone(),
729                        status: WorkflowTraversalStatus::Completed,
730                        output: Some(final_output),
731                        error: None,
732                        warnings,
733                    },
734                    evidence,
735                });
736            }
737
738            let failure_reason = if outgoing
739                .iter()
740                .any(|edge| edge.trigger == WorkflowEdgeTrigger::Event)
741            {
742                WorkflowTraversalFailureReason::MissingRequiredEvent
743            } else {
744                WorkflowTraversalFailureReason::TerminalNodeNotReached
745            };
746
747            return Err(workflow_failure(
748                request,
749                failure_reason,
750                runtime_error(
751                    RuntimeErrorCode::ExecutionFailed,
752                    "workflow traversal could not reach a valid next node",
753                    json!({"node_id": node.node_id}),
754                ),
755                visited,
756                traversed,
757                emitted,
758                event_evidence,
759                warnings,
760            ));
761        }
762    }
763}
764
765fn validate_workflow_request(request: &WorkflowExecutionRequest) -> Option<RuntimeError> {
766    if request.kind != WORKFLOW_REQUEST_KIND {
767        return Some(runtime_error(
768            RuntimeErrorCode::RequestInvalid,
769            "kind must equal workflow_execution_request",
770            json!({"path": "$.kind"}),
771        ));
772    }
773    if request.schema_version != WORKFLOW_SCHEMA_VERSION {
774        return Some(runtime_error(
775            RuntimeErrorCode::RequestInvalid,
776            "schema_version must equal 1.0.0",
777            json!({"path": "$.schema_version"}),
778        ));
779    }
780    if request.governing_spec != WORKFLOW_GOVERNING_SPEC {
781        return Some(runtime_error(
782            RuntimeErrorCode::RequestInvalid,
783            "governing_spec must equal 007-workflow-registry-traversal",
784            json!({"path": "$.governing_spec"}),
785        ));
786    }
787    if request.request_id.trim().is_empty()
788        || request.workflow_id.trim().is_empty()
789        || request.workflow_version.trim().is_empty()
790    {
791        return Some(runtime_error(
792            RuntimeErrorCode::RequestInvalid,
793            "request_id, workflow_id, and workflow_version must be non-empty",
794            json!({"path": "$"}),
795        ));
796    }
797    None
798}
799
800fn map_workflow_lookup_scope(scope: WorkflowLookupScope) -> LookupScope {
801    match scope {
802        WorkflowLookupScope::PublicOnly => LookupScope::PublicOnly,
803        WorkflowLookupScope::PreferPrivate => LookupScope::PreferPrivate,
804    }
805}
806
807fn workflow_state(input: &Value) -> Map<String, Value> {
808    match input {
809        Value::Object(map) => map.clone(),
810        other => {
811            let mut map = Map::new();
812            map.insert("input".to_string(), other.clone());
813            map
814        }
815    }
816}
817
818fn node_input(state: &Map<String, Value>, node: &WorkflowNode) -> Value {
819    let mut input = Map::new();
820    for key in &node.input.from_workflow_input {
821        if let Some(value) = state.get(key) {
822            input.insert(key.clone(), value.clone());
823        }
824    }
825    Value::Object(input)
826}
827
828fn update_state(state: &mut Map<String, Value>, node: &WorkflowNode, output: &Value) {
829    let Value::Object(object) = output else {
830        return;
831    };
832    for key in &node.output.to_workflow_state {
833        if let Some(value) = object.get(key) {
834            state.insert(key.clone(), value.clone());
835        }
836    }
837    if let Some(namespace) = &node.output.publish_to_state_as {
838        state.insert(namespace.clone(), output.clone());
839    }
840}
841
842fn final_workflow_output(state: &Map<String, Value>, output_projection: &[String]) -> Value {
843    if output_projection.is_empty() {
844        return Value::Object(state.clone());
845    }
846    let mut projected = Map::new();
847    for key in output_projection {
848        if let Some(value) = state.get(key) {
849            projected.insert(key.clone(), value.clone());
850        }
851    }
852    Value::Object(projected)
853}
854
855fn emitted_events(output: &Value) -> Vec<EmittedEventRecord> {
856    let Value::Object(object) = output else {
857        return Vec::new();
858    };
859    let Some(Value::Array(events)) = object.get("emitted_events") else {
860        return Vec::new();
861    };
862    events
863        .iter()
864        .enumerate()
865        .filter_map(|(index, event)| {
866            let Value::Object(event) = event else {
867                return None;
868            };
869            Some(EmittedEventRecord {
870                record_id: format!("event_record_{index}"),
871                event: EventReference {
872                    event_id: event.get("event_id")?.as_str()?.to_string(),
873                    version: event.get("version")?.as_str()?.to_string(),
874                },
875                payload: event.get("payload").cloned(),
876            })
877        })
878        .collect()
879}
880
881fn waiting_edge_contexts(
882    workflow_execution_id: &str,
883    edges: &[WorkflowEdge],
884) -> Vec<WaitingWorkflowEdgeContext> {
885    edges
886        .iter()
887        .filter_map(|edge| {
888            Some(WaitingWorkflowEdgeContext {
889                workflow_execution_id: workflow_execution_id.to_string(),
890                edge_id: edge.edge_id.clone(),
891                from_node_id: edge.from.clone(),
892                to_node_id: edge.to.clone(),
893                event_ref: edge.event.clone()?,
894                predicate: edge.predicate.clone(),
895            })
896        })
897        .collect()
898}
899
900#[allow(clippy::too_many_lines)]
901fn evaluate_event_driven_edges(
902    waiting_edges: &[WaitingWorkflowEdgeContext],
903    emitted_events: &[EmittedEventRecord],
904    consumed_event_edges: &mut BTreeSet<String>,
905    record_prefix: &str,
906) -> EventDrivenEvaluationOutcome {
907    let mut ordered_waiting_edges = waiting_edges.to_vec();
908    ordered_waiting_edges.sort_by(|left, right| {
909        left.workflow_execution_id
910            .cmp(&right.workflow_execution_id)
911            .then_with(|| left.edge_id.cmp(&right.edge_id))
912    });
913
914    let mut outcome = EventDrivenEvaluationOutcome::default();
915    if emitted_events.is_empty() {
916        outcome
917            .evidence
918            .match_records
919            .extend(ordered_waiting_edges.iter().map(|edge| EventMatchRecord {
920                event_id: edge.event_ref.event_id.clone(),
921                event_version: edge.event_ref.version.clone(),
922                edge_id: edge.edge_id.clone(),
923                match_result: EventMatchResult::NotMatched,
924                predicate_result: None,
925                rejection_reason: Some("required event was not emitted".to_string()),
926                recorded_at: format!("{record_prefix}:no_event:{}", edge.edge_id),
927            }));
928        return outcome;
929    }
930
931    let mut wake_order = 1;
932    for (event_index, emitted_event) in emitted_events.iter().enumerate() {
933        for waiting_edge in &ordered_waiting_edges {
934            let match_recorded_at = format!(
935                "{record_prefix}:event:{event_index}:match:{}",
936                waiting_edge.edge_id
937            );
938            if emitted_event.event != waiting_edge.event_ref {
939                outcome.evidence.match_records.push(EventMatchRecord {
940                    event_id: emitted_event.event.event_id.clone(),
941                    event_version: emitted_event.event.version.clone(),
942                    edge_id: waiting_edge.edge_id.clone(),
943                    match_result: EventMatchResult::NotMatched,
944                    predicate_result: None,
945                    rejection_reason: Some(
946                        "event id/version did not match the waiting edge".to_string(),
947                    ),
948                    recorded_at: match_recorded_at,
949                });
950                continue;
951            }
952
953            if let Some(predicate) = waiting_edge.predicate.as_ref() {
954                let predicate_passed =
955                    event_payload_field(emitted_event.payload.as_ref(), &predicate.field)
956                        .is_some_and(|value| value == &predicate.equals);
957                if !predicate_passed {
958                    outcome.evidence.match_records.push(EventMatchRecord {
959                        event_id: emitted_event.event.event_id.clone(),
960                        event_version: emitted_event.event.version.clone(),
961                        edge_id: waiting_edge.edge_id.clone(),
962                        match_result: EventMatchResult::NotMatched,
963                        predicate_result: Some(EventPredicateResult::Failed),
964                        rejection_reason: Some(
965                            "event predicate did not match the emitted payload".to_string(),
966                        ),
967                        recorded_at: match_recorded_at,
968                    });
969                    continue;
970                }
971            }
972
973            let consumption_key = format!(
974                "{}|{}|{}",
975                emitted_event.record_id, waiting_edge.workflow_execution_id, waiting_edge.edge_id
976            );
977            if consumed_event_edges.contains(&consumption_key) {
978                outcome.evidence.match_records.push(EventMatchRecord {
979                    event_id: emitted_event.event.event_id.clone(),
980                    event_version: emitted_event.event.version.clone(),
981                    edge_id: waiting_edge.edge_id.clone(),
982                    match_result: EventMatchResult::AlreadyConsumed,
983                    predicate_result: waiting_edge
984                        .predicate
985                        .as_ref()
986                        .map(|_| EventPredicateResult::Passed),
987                    rejection_reason: Some(
988                        "event record was already consumed for this waiting edge".to_string(),
989                    ),
990                    recorded_at: match_recorded_at,
991                });
992                continue;
993            }
994
995            consumed_event_edges.insert(consumption_key);
996            outcome.evidence.match_records.push(EventMatchRecord {
997                event_id: emitted_event.event.event_id.clone(),
998                event_version: emitted_event.event.version.clone(),
999                edge_id: waiting_edge.edge_id.clone(),
1000                match_result: EventMatchResult::Matched,
1001                predicate_result: waiting_edge
1002                    .predicate
1003                    .as_ref()
1004                    .map(|_| EventPredicateResult::Passed),
1005                rejection_reason: None,
1006                recorded_at: match_recorded_at.clone(),
1007            });
1008            outcome.taken_edge_ids.push(waiting_edge.edge_id.clone());
1009            let wake_recorded_at = format!(
1010                "{record_prefix}:event:{event_index}:wake:{}",
1011                waiting_edge.edge_id
1012            );
1013            outcome.evidence.wake_decisions.push(EventWakeDecision {
1014                decision_type: "event_wake".to_string(),
1015                event_id: emitted_event.event.event_id.clone(),
1016                event_version: emitted_event.event.version.clone(),
1017                edge_id: waiting_edge.edge_id.clone(),
1018                workflow_execution_id: waiting_edge.workflow_execution_id.clone(),
1019                wake_order,
1020                result: EventWakeDecisionResult::Taken,
1021                recorded_at: wake_recorded_at.clone(),
1022            });
1023            outcome.evidence.consumptions.push(EventConsumptionRecord {
1024                event_id: emitted_event.event.event_id.clone(),
1025                event_version: emitted_event.event.version.clone(),
1026                edge_id: waiting_edge.edge_id.clone(),
1027                workflow_execution_id: waiting_edge.workflow_execution_id.clone(),
1028                consumed_at: wake_recorded_at,
1029            });
1030            wake_order += 1;
1031        }
1032    }
1033    outcome
1034}
1035
1036fn event_payload_field<'a>(payload: Option<&'a Value>, field: &str) -> Option<&'a Value> {
1037    let payload = payload?;
1038    let path = field.strip_prefix("payload.").unwrap_or(field);
1039    if path == "payload" || path.is_empty() {
1040        return Some(payload);
1041    }
1042
1043    let mut current = payload;
1044    for segment in path.split('.') {
1045        let Value::Object(map) = current else {
1046            return None;
1047        };
1048        current = map.get(segment)?;
1049    }
1050    Some(current)
1051}
1052
1053fn edge_record(edge: &WorkflowEdge) -> WorkflowTraversalEdgeRecord {
1054    WorkflowTraversalEdgeRecord {
1055        edge_id: edge.edge_id.clone(),
1056        from: edge.from.clone(),
1057        to: edge.to.clone(),
1058        trigger: match edge.trigger {
1059            WorkflowEdgeTrigger::Direct => WorkflowTraversalTrigger::Direct,
1060            WorkflowEdgeTrigger::Event => WorkflowTraversalTrigger::Event,
1061        },
1062        event: edge.event.clone(),
1063    }
1064}
1065
1066#[allow(clippy::too_many_arguments)]
1067fn workflow_failure(
1068    request: &WorkflowExecutionRequest,
1069    failure_reason: WorkflowTraversalFailureReason,
1070    error: RuntimeError,
1071    visited_nodes: Vec<WorkflowTraversalStepRecord>,
1072    traversed_edges: Vec<WorkflowTraversalEdgeRecord>,
1073    emitted_events: Vec<EventReference>,
1074    event_evidence: WorkflowEventEvidenceBundle,
1075    warnings: Vec<RuntimeWarning>,
1076) -> WorkflowExecutionOutcome {
1077    let evidence = WorkflowTraversalEvidence {
1078        kind: WORKFLOW_EVIDENCE_KIND.to_string(),
1079        schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
1080        trace_id: format!("workflow_trace_{}", request.request_id),
1081        request_id: request.request_id.clone(),
1082        workflow_id: request.workflow_id.clone(),
1083        workflow_version: request.workflow_version.clone(),
1084        governing_spec: WORKFLOW_GOVERNING_SPEC.to_string(),
1085        visited_nodes,
1086        traversed_edges,
1087        emitted_events,
1088        waiting_edges: event_evidence.waiting_edges,
1089        event_match_records: event_evidence.match_records,
1090        event_wake_decisions: event_evidence.wake_decisions,
1091        event_consumptions: event_evidence.consumptions,
1092        result: WorkflowTraversalResult {
1093            status: WorkflowTraversalStatus::Error,
1094            failure_reason: Some(failure_reason),
1095        },
1096    };
1097
1098    WorkflowExecutionOutcome {
1099        result: WorkflowExecutionResult {
1100            kind: WORKFLOW_REQUEST_KIND.to_string(),
1101            schema_version: WORKFLOW_SCHEMA_VERSION.to_string(),
1102            request_id: request.request_id.clone(),
1103            workflow_id: request.workflow_id.clone(),
1104            workflow_version: request.workflow_version.clone(),
1105            status: WorkflowTraversalStatus::Error,
1106            output: None,
1107            error: Some(error),
1108            warnings,
1109        },
1110        evidence,
1111    }
1112}
1113
1114#[cfg(test)]
1115mod tests {
1116    use super::*;
1117    use crate::security::RuntimeSecurityConfig;
1118    use crate::{
1119        CandidateCollectionRecord, LocalExecutionFailure, LocalExecutionFailureCode,
1120        RuntimeContext, RuntimeIntent, RuntimeLookup, RuntimeLookupScope, RuntimeRequest,
1121        RuntimeResultStatus, SelectionRecord,
1122    };
1123    use serde_json::json;
1124    use traverse_contracts::{
1125        BinaryFormat as ContractBinaryFormat, CapabilityContract, Condition, Entrypoint,
1126        EntrypointKind, EventReference, EvidenceStatus, EvidenceType, Execution,
1127        ExecutionConstraints, ExecutionTarget, FilesystemAccess, HostApiAccess, IdReference,
1128        Lifecycle, NetworkAccess, Owner, Provenance, ProvenanceSource, SchemaContainer,
1129        ServiceType, SideEffect, SideEffectKind, ValidationEvidence,
1130    };
1131    use traverse_registry::{
1132        ArtifactDigests, ArtifactSignature, ArtifactSignatureScheme, BinaryFormat, BinaryReference,
1133        CapabilityArtifactRecord, CapabilityRegistration, CapabilityRegistry,
1134        ComposabilityMetadata, CompositionKind, CompositionPattern, ImplementationKind,
1135        RegistryProvenance, RegistryScope, SourceKind, SourceReference, WorkflowDefinition,
1136        WorkflowEdge, WorkflowEdgeTrigger, WorkflowNode, WorkflowNodeInput, WorkflowNodeOutput,
1137        WorkflowRegistration, WorkflowRegistry, WorkflowRegistryRecord, workflow_artifact_record,
1138    };
1139
1140    #[test]
1141    fn workflow_request_validation_rejects_invalid_guards() {
1142        let mut request = valid_workflow_request();
1143        request.kind = "bad".to_string();
1144        assert_eq!(
1145            validate_workflow_request(&request).map(|error| error.code),
1146            Some(RuntimeErrorCode::RequestInvalid)
1147        );
1148
1149        let mut request = valid_workflow_request();
1150        request.schema_version = "2.0.0".to_string();
1151        assert_eq!(
1152            validate_workflow_request(&request).map(|error| error.code),
1153            Some(RuntimeErrorCode::RequestInvalid)
1154        );
1155
1156        let mut request = valid_workflow_request();
1157        request.governing_spec = "bad".to_string();
1158        assert_eq!(
1159            validate_workflow_request(&request).map(|error| error.code),
1160            Some(RuntimeErrorCode::RequestInvalid)
1161        );
1162
1163        let mut request = valid_workflow_request();
1164        request.request_id.clear();
1165        assert_eq!(
1166            validate_workflow_request(&request).map(|error| error.code),
1167            Some(RuntimeErrorCode::RequestInvalid)
1168        );
1169    }
1170
1171    #[test]
1172    fn workflow_helpers_cover_state_and_event_extraction_paths() {
1173        let scalar = workflow_state(&json!("value"));
1174        assert_eq!(scalar.get("input"), Some(&json!("value")));
1175
1176        let mut state = workflow_state(&json!({"comment_text": "hello"}));
1177        let node = WorkflowNode {
1178            node_id: "node".to_string(),
1179            capability_id: "content.comments.create-comment-draft".to_string(),
1180            capability_version: "1.0.0".to_string(),
1181            input: WorkflowNodeInput {
1182                from_workflow_input: vec!["comment_text".to_string(), "missing".to_string()],
1183            },
1184            output: WorkflowNodeOutput {
1185                to_workflow_state: vec!["draft_id".to_string()],
1186                publish_to_state_as: None,
1187            },
1188        };
1189        assert_eq!(node_input(&state, &node), json!({"comment_text": "hello"}));
1190        update_state(&mut state, &node, &json!({"draft_id": "draft-1"}));
1191        assert_eq!(state.get("draft_id"), Some(&json!("draft-1")));
1192        update_state(&mut state, &node, &json!("not-an-object"));
1193
1194        assert!(emitted_events(&json!("nope")).is_empty());
1195        assert_eq!(
1196            emitted_events(&json!({
1197                "emitted_events": [
1198                    "bad",
1199                    {
1200                        "event_id": "content.comments.draft-created",
1201                        "version": "1.0.0",
1202                        "payload": {"severity": "normal"}
1203                    },
1204                    {"event_id": "bad"}
1205                ]
1206            })),
1207            vec![EmittedEventRecord {
1208                record_id: "event_record_1".to_string(),
1209                event: EventReference {
1210                    event_id: "content.comments.draft-created".to_string(),
1211                    version: "1.0.0".to_string(),
1212                },
1213                payload: Some(json!({"severity": "normal"})),
1214            }]
1215        );
1216
1217        let edge = WorkflowEdge {
1218            edge_id: "edge".to_string(),
1219            from: "a".to_string(),
1220            to: "b".to_string(),
1221            trigger: WorkflowEdgeTrigger::Event,
1222            event: Some(EventReference {
1223                event_id: "content.comments.draft-created".to_string(),
1224                version: "1.0.0".to_string(),
1225            }),
1226            predicate: None,
1227        };
1228        assert_eq!(edge_record(&edge).trigger, WorkflowTraversalTrigger::Event);
1229        assert_eq!(
1230            map_workflow_lookup_scope(WorkflowLookupScope::PreferPrivate),
1231            LookupScope::PreferPrivate
1232        );
1233        assert_eq!(
1234            event_payload_field(Some(&json!({"severity": "normal"})), "payload.severity"),
1235            Some(&json!("normal"))
1236        );
1237        assert_eq!(
1238            event_payload_field(Some(&json!({"severity": "normal"})), "payload"),
1239            Some(&json!({"severity": "normal"}))
1240        );
1241        assert_eq!(
1242            event_payload_field(Some(&json!("normal")), "payload.severity"),
1243            None
1244        );
1245    }
1246
1247    #[test]
1248    #[allow(clippy::too_many_lines)]
1249    fn executes_workflow_deterministically_and_supports_workflow_backed_capabilities() {
1250        let workflow_registry = workflow_registry_fixture();
1251        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
1252            .with_workflow_registry(workflow_registry)
1253            .with_security_config(RuntimeSecurityConfig::development());
1254
1255        let workflow = runtime.execute_workflow(valid_workflow_request());
1256        assert_eq!(workflow.result.status, WorkflowTraversalStatus::Completed);
1257        assert_eq!(
1258            workflow.result.output,
1259            Some(
1260                json!({"comment_text": "hello", "draft_id": "draft-1", "comment_id": "comment-1"})
1261            )
1262        );
1263        assert_eq!(workflow.evidence.visited_nodes.len(), 3);
1264        assert_eq!(workflow.evidence.traversed_edges.len(), 2);
1265        assert_eq!(workflow.evidence.waiting_edges.len(), 2);
1266        assert_eq!(workflow.evidence.event_wake_decisions.len(), 2);
1267        assert_eq!(workflow.evidence.event_consumptions.len(), 2);
1268        assert!(
1269            workflow
1270                .evidence
1271                .event_match_records
1272                .iter()
1273                .all(|record| record.match_result == EventMatchResult::Matched)
1274        );
1275
1276        let mut composed_registry = capability_registry_fixture();
1277        register_capability_ok(
1278            &mut composed_registry,
1279            CapabilityRegistration {
1280                scope: RegistryScope::Public,
1281                contract: capability_contract(
1282                    "content.comments.publish-comment",
1283                    vec![],
1284                    json!({
1285                        "type": "object",
1286                        "properties": { "comment_text": { "type": "string" } },
1287                        "required": ["comment_text"],
1288                        "additionalProperties": true
1289                    }),
1290                    json!({
1291                        "type": "object",
1292                        "properties": { "comment_id": { "type": "string" } },
1293                        "required": ["comment_id"],
1294                        "additionalProperties": true
1295                    }),
1296                ),
1297                contract_path: "contracts/publish-comment.json".to_string(),
1298                artifact: workflow_artifact_record(
1299                    "content.comments.publish-comment",
1300                    "1.0.0",
1301                    "artifact-workflow",
1302                ),
1303                registered_at: "2026-03-27T00:10:00Z".to_string(),
1304                tags: vec!["comments".to_string()],
1305                composability: ComposabilityMetadata {
1306                    kind: CompositionKind::Composite,
1307                    patterns: vec![
1308                        CompositionPattern::Sequential,
1309                        CompositionPattern::EventDriven,
1310                    ],
1311                    provides: vec!["published-comment".to_string()],
1312                    requires: vec!["draft".to_string()],
1313                },
1314                governing_spec: "005-capability-registry".to_string(),
1315                validator_version: "validator".to_string(),
1316            },
1317        );
1318
1319        let runtime = Runtime::new(composed_registry, WorkflowExecutor)
1320            .with_workflow_registry(workflow_registry_fixture())
1321            .with_security_config(RuntimeSecurityConfig::development());
1322        let result = runtime.execute(RuntimeRequest {
1323            kind: "runtime_request".to_string(),
1324            schema_version: "1.0.0".to_string(),
1325            request_id: "request-workflow".to_string(),
1326            intent: RuntimeIntent {
1327                capability_id: Some("content.comments.publish-comment".to_string()),
1328                capability_version: Some("1.0.0".to_string()),
1329                version_range: None,
1330                intent_key: None,
1331            },
1332            input: json!({"comment_text": "hello"}),
1333            lookup: RuntimeLookup {
1334                scope: RuntimeLookupScope::PublicOnly,
1335                allow_ambiguity: false,
1336            },
1337            context: RuntimeContext {
1338                requested_target: crate::PlacementTarget::Local,
1339                correlation_id: None,
1340                caller: None,
1341                traceparent: None,
1342                tracestate: None,
1343                metadata: None,
1344                identity: None,
1345            },
1346            governing_spec: "006-runtime-request-execution".to_string(),
1347        });
1348        assert_eq!(result.result.status, RuntimeResultStatus::Completed);
1349        assert_eq!(
1350            result.result.output,
1351            Some(
1352                json!({"comment_text": "hello", "draft_id": "draft-1", "comment_id": "comment-1"})
1353            )
1354        );
1355        assert!(
1356            result
1357                .result
1358                .warnings
1359                .iter()
1360                .any(|warning| warning.code == "unsigned_local_dev_artifact")
1361        );
1362    }
1363
1364    #[test]
1365    fn workflow_failures_cover_not_found_missing_events_and_step_failures() {
1366        let workflow_registry = workflow_registry_fixture();
1367        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
1368            .with_workflow_registry(workflow_registry);
1369
1370        let mut missing_request = valid_workflow_request();
1371        missing_request.workflow_id = "missing".to_string();
1372        let missing = runtime.execute_workflow(missing_request);
1373        assert_eq!(
1374            missing.evidence.result.failure_reason,
1375            Some(WorkflowTraversalFailureReason::WorkflowNotFound)
1376        );
1377
1378        let workflow_registry = workflow_registry_fixture();
1379        let runtime = Runtime::new(capability_registry_fixture(), MissingEventWorkflowExecutor)
1380            .with_workflow_registry(workflow_registry)
1381            .with_security_config(RuntimeSecurityConfig::development());
1382        let missing_event = runtime.execute_workflow(valid_workflow_request());
1383        assert_eq!(
1384            missing_event.evidence.result.failure_reason,
1385            Some(WorkflowTraversalFailureReason::MissingRequiredEvent)
1386        );
1387
1388        let runtime = Runtime::new(capability_registry_fixture(), FailingWorkflowExecutor)
1389            .with_workflow_registry(workflow_registry_fixture())
1390            .with_security_config(RuntimeSecurityConfig::development());
1391        let failed = runtime.execute_workflow(valid_workflow_request());
1392        assert_eq!(
1393            failed.evidence.result.failure_reason,
1394            Some(WorkflowTraversalFailureReason::StepExecutionFailed)
1395        );
1396    }
1397
1398    #[test]
1399    fn event_driven_helpers_are_deterministic_and_prevent_duplicate_consumption() {
1400        let event = EmittedEventRecord {
1401            record_id: "event_record_0".to_string(),
1402            event: EventReference {
1403                event_id: "content.comments.validated".to_string(),
1404                version: "1.0.0".to_string(),
1405            },
1406            payload: Some(json!({"severity": "normal"})),
1407        };
1408        let waiting_edges = vec![
1409            WaitingWorkflowEdgeContext {
1410                workflow_execution_id: "wf_exec_b".to_string(),
1411                edge_id: "edge_b".to_string(),
1412                from_node_id: "from".to_string(),
1413                to_node_id: "to".to_string(),
1414                event_ref: event.event.clone(),
1415                predicate: Some(WorkflowEdgePredicate {
1416                    field: "payload.severity".to_string(),
1417                    equals: json!("normal"),
1418                }),
1419            },
1420            WaitingWorkflowEdgeContext {
1421                workflow_execution_id: "wf_exec_a".to_string(),
1422                edge_id: "edge_a".to_string(),
1423                from_node_id: "from".to_string(),
1424                to_node_id: "to".to_string(),
1425                event_ref: event.event.clone(),
1426                predicate: None,
1427            },
1428        ];
1429        let mut consumed = BTreeSet::new();
1430        let first = evaluate_event_driven_edges(
1431            &waiting_edges,
1432            std::slice::from_ref(&event),
1433            &mut consumed,
1434            "trace",
1435        );
1436        assert_eq!(
1437            first.taken_edge_ids,
1438            vec!["edge_a".to_string(), "edge_b".to_string()]
1439        );
1440        assert_eq!(
1441            first
1442                .evidence
1443                .wake_decisions
1444                .iter()
1445                .map(|decision| (&decision.workflow_execution_id, decision.wake_order))
1446                .collect::<Vec<_>>(),
1447            vec![(&"wf_exec_a".to_string(), 1), (&"wf_exec_b".to_string(), 2)]
1448        );
1449
1450        let second = evaluate_event_driven_edges(&waiting_edges, &[event], &mut consumed, "trace");
1451        assert!(second.taken_edge_ids.is_empty());
1452        assert!(
1453            second
1454                .evidence
1455                .match_records
1456                .iter()
1457                .all(|record| record.match_result == EventMatchResult::AlreadyConsumed)
1458        );
1459    }
1460
1461    #[test]
1462    fn event_driven_helpers_reject_non_matching_predicates() {
1463        let waiting_edges = vec![WaitingWorkflowEdgeContext {
1464            workflow_execution_id: "wf_exec_1".to_string(),
1465            edge_id: "edge_predicate".to_string(),
1466            from_node_id: "assess".to_string(),
1467            to_node_id: "validate".to_string(),
1468            event_ref: EventReference {
1469                event_id: "expedition.conditions.summary-assessed".to_string(),
1470                version: "1.0.0".to_string(),
1471            },
1472            predicate: Some(WorkflowEdgePredicate {
1473                field: "payload.severity".to_string(),
1474                equals: json!("high"),
1475            }),
1476        }];
1477        let emitted = vec![EmittedEventRecord {
1478            record_id: "event_record_0".to_string(),
1479            event: EventReference {
1480                event_id: "expedition.conditions.summary-assessed".to_string(),
1481                version: "1.0.0".to_string(),
1482            },
1483            payload: Some(json!({"severity": "normal"})),
1484        }];
1485        let outcome =
1486            evaluate_event_driven_edges(&waiting_edges, &emitted, &mut BTreeSet::new(), "trace");
1487        assert!(outcome.taken_edge_ids.is_empty());
1488        assert_eq!(
1489            outcome.evidence.match_records,
1490            vec![EventMatchRecord {
1491                event_id: "expedition.conditions.summary-assessed".to_string(),
1492                event_version: "1.0.0".to_string(),
1493                edge_id: "edge_predicate".to_string(),
1494                match_result: EventMatchResult::NotMatched,
1495                predicate_result: Some(EventPredicateResult::Failed),
1496                rejection_reason: Some(
1497                    "event predicate did not match the emitted payload".to_string()
1498                ),
1499                recorded_at: "trace:event:0:match:edge_predicate".to_string(),
1500            }]
1501        );
1502    }
1503
1504    #[test]
1505    fn event_driven_helpers_record_non_matching_event_identity() {
1506        let waiting_edges = vec![WaitingWorkflowEdgeContext {
1507            workflow_execution_id: "wf_exec_1".to_string(),
1508            edge_id: "edge_identity".to_string(),
1509            from_node_id: "create".to_string(),
1510            to_node_id: "validate".to_string(),
1511            event_ref: EventReference {
1512                event_id: "content.comments.validated".to_string(),
1513                version: "1.0.0".to_string(),
1514            },
1515            predicate: None,
1516        }];
1517        let emitted = vec![EmittedEventRecord {
1518            record_id: "event_record_0".to_string(),
1519            event: EventReference {
1520                event_id: "content.comments.other".to_string(),
1521                version: "1.0.0".to_string(),
1522            },
1523            payload: None,
1524        }];
1525        let outcome =
1526            evaluate_event_driven_edges(&waiting_edges, &emitted, &mut BTreeSet::new(), "trace");
1527        assert!(outcome.taken_edge_ids.is_empty());
1528        assert_eq!(
1529            outcome.evidence.match_records,
1530            vec![EventMatchRecord {
1531                event_id: "content.comments.other".to_string(),
1532                event_version: "1.0.0".to_string(),
1533                edge_id: "edge_identity".to_string(),
1534                match_result: EventMatchResult::NotMatched,
1535                predicate_result: None,
1536                rejection_reason: Some(
1537                    "event id/version did not match the waiting edge".to_string()
1538                ),
1539                recorded_at: "trace:event:0:match:edge_identity".to_string(),
1540            }]
1541        );
1542    }
1543
1544    #[test]
1545    #[allow(clippy::too_many_lines)]
1546    fn workflow_runtime_covers_additional_failure_and_helper_branches() {
1547        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
1548            .with_workflow_registry(workflow_registry_fixture())
1549            .with_security_config(RuntimeSecurityConfig::development());
1550
1551        let invalid_input = runtime.execute_workflow(WorkflowExecutionRequest {
1552            input: json!({}),
1553            ..valid_workflow_request()
1554        });
1555        assert_eq!(
1556            invalid_input.evidence.result.failure_reason,
1557            Some(WorkflowTraversalFailureReason::WorkflowInvalid)
1558        );
1559
1560        let invalid_request = runtime.execute_workflow(WorkflowExecutionRequest {
1561            kind: "bad".to_string(),
1562            ..valid_workflow_request()
1563        });
1564        assert_eq!(
1565            invalid_request.evidence.result.failure_reason,
1566            Some(WorkflowTraversalFailureReason::WorkflowInvalid)
1567        );
1568
1569        let missing_node = runtime.traverse_workflow(
1570            &valid_workflow_request(),
1571            &resolved_workflow(WorkflowDefinition {
1572                start_node: "missing".to_string(),
1573                ..workflow_definition_fixture(
1574                    Some(EventReference {
1575                        event_id: "content.comments.validated".to_string(),
1576                        version: "1.0.0".to_string(),
1577                    }),
1578                    None,
1579                )
1580            }),
1581        );
1582        assert!(missing_node.is_err());
1583
1584        let missing_capability_runtime = Runtime::new(CapabilityRegistry::new(), WorkflowExecutor)
1585            .with_workflow_registry(workflow_registry_fixture());
1586        let missing_capability =
1587            missing_capability_runtime.execute_workflow(valid_workflow_request());
1588        assert_eq!(
1589            missing_capability.evidence.result.failure_reason,
1590            Some(WorkflowTraversalFailureReason::WorkflowInvalid)
1591        );
1592
1593        let strict_runtime =
1594            Runtime::new(strict_input_capability_registry_fixture(), WorkflowExecutor)
1595                .with_workflow_registry(workflow_registry_fixture())
1596                .with_security_config(RuntimeSecurityConfig::development());
1597        let input_mismatch = strict_runtime.traverse_workflow(
1598            &valid_workflow_request(),
1599            &resolved_workflow(WorkflowDefinition {
1600                nodes: vec![WorkflowNode {
1601                    input: WorkflowNodeInput {
1602                        from_workflow_input: vec!["missing".to_string()],
1603                    },
1604                    ..workflow_definition_fixture(
1605                        Some(EventReference {
1606                            event_id: "content.comments.validated".to_string(),
1607                            version: "1.0.0".to_string(),
1608                        }),
1609                        None,
1610                    )
1611                    .nodes[0]
1612                        .clone()
1613                }],
1614                edges: Vec::new(),
1615                start_node: "create_draft".to_string(),
1616                terminal_nodes: vec!["create_draft".to_string()],
1617                ..workflow_definition_fixture(
1618                    Some(EventReference {
1619                        event_id: "content.comments.validated".to_string(),
1620                        version: "1.0.0".to_string(),
1621                    }),
1622                    None,
1623                )
1624            }),
1625        );
1626        assert!(input_mismatch.is_err());
1627
1628        let bad_output_runtime =
1629            Runtime::new(capability_registry_fixture(), BadOutputWorkflowExecutor)
1630                .with_workflow_registry(workflow_registry_fixture())
1631                .with_security_config(RuntimeSecurityConfig::development());
1632        let bad_output = bad_output_runtime.execute_workflow(valid_workflow_request());
1633        assert_eq!(
1634            bad_output.evidence.result.failure_reason,
1635            Some(WorkflowTraversalFailureReason::StepExecutionFailed)
1636        );
1637
1638        let direct_success = runtime.traverse_workflow(
1639            &valid_workflow_request(),
1640            &resolved_workflow(workflow_definition_fixture(
1641                Some(EventReference {
1642                    event_id: "content.comments.validated".to_string(),
1643                    version: "1.0.0".to_string(),
1644                }),
1645                Some(WorkflowEdge {
1646                    edge_id: "direct".to_string(),
1647                    from: "create_draft".to_string(),
1648                    to: "validate_comment".to_string(),
1649                    trigger: WorkflowEdgeTrigger::Direct,
1650                    event: None,
1651                    predicate: None,
1652                }),
1653            )),
1654        );
1655        assert!(direct_success.is_ok());
1656
1657        let ambiguous_direct = runtime.traverse_workflow(
1658            &valid_workflow_request(),
1659            &resolved_workflow(WorkflowDefinition {
1660                edges: vec![
1661                    WorkflowEdge {
1662                        edge_id: "direct-1".to_string(),
1663                        from: "create_draft".to_string(),
1664                        to: "validate_comment".to_string(),
1665                        trigger: WorkflowEdgeTrigger::Direct,
1666                        event: None,
1667                        predicate: None,
1668                    },
1669                    WorkflowEdge {
1670                        edge_id: "direct-2".to_string(),
1671                        from: "create_draft".to_string(),
1672                        to: "persist_comment".to_string(),
1673                        trigger: WorkflowEdgeTrigger::Direct,
1674                        event: None,
1675                        predicate: None,
1676                    },
1677                ],
1678                ..workflow_definition_fixture(
1679                    Some(EventReference {
1680                        event_id: "content.comments.validated".to_string(),
1681                        version: "1.0.0".to_string(),
1682                    }),
1683                    None,
1684                )
1685            }),
1686        );
1687        assert!(ambiguous_direct.is_err());
1688
1689        let ambiguous_event = runtime.traverse_workflow(
1690            &valid_workflow_request(),
1691            &resolved_workflow(WorkflowDefinition {
1692                edges: vec![
1693                    WorkflowEdge {
1694                        edge_id: "draft_to_validate".to_string(),
1695                        from: "create_draft".to_string(),
1696                        to: "validate_comment".to_string(),
1697                        trigger: WorkflowEdgeTrigger::Event,
1698                        event: Some(EventReference {
1699                            event_id: "content.comments.draft-created".to_string(),
1700                            version: "1.0.0".to_string(),
1701                        }),
1702                        predicate: None,
1703                    },
1704                    WorkflowEdge {
1705                        edge_id: "draft_to_persist".to_string(),
1706                        from: "create_draft".to_string(),
1707                        to: "persist_comment".to_string(),
1708                        trigger: WorkflowEdgeTrigger::Event,
1709                        event: Some(EventReference {
1710                            event_id: "content.comments.draft-created".to_string(),
1711                            version: "1.0.0".to_string(),
1712                        }),
1713                        predicate: None,
1714                    },
1715                ],
1716                ..workflow_definition_fixture(
1717                    Some(EventReference {
1718                        event_id: "content.comments.validated".to_string(),
1719                        version: "1.0.0".to_string(),
1720                    }),
1721                    None,
1722                )
1723            }),
1724        );
1725        assert!(ambiguous_event.is_err());
1726
1727        let terminal_miss = runtime.traverse_workflow(
1728            &valid_workflow_request(),
1729            &resolved_workflow(WorkflowDefinition {
1730                edges: Vec::new(),
1731                terminal_nodes: vec!["persist_comment".to_string()],
1732                ..workflow_definition_fixture(
1733                    Some(EventReference {
1734                        event_id: "content.comments.validated".to_string(),
1735                        version: "1.0.0".to_string(),
1736                    }),
1737                    None,
1738                )
1739            }),
1740        );
1741        assert!(terminal_miss.is_err());
1742
1743        let invalid_final_output = runtime.traverse_workflow(
1744            &valid_workflow_request(),
1745            &resolved_workflow(WorkflowDefinition {
1746                outputs: SchemaContainer {
1747                    schema: json!({
1748                        "type": "object",
1749                        "properties": { "missing": { "type": "string" } },
1750                        "required": ["missing"],
1751                        "additionalProperties": true
1752                    }),
1753                },
1754                ..workflow_definition_fixture(
1755                    Some(EventReference {
1756                        event_id: "content.comments.validated".to_string(),
1757                        version: "1.0.0".to_string(),
1758                    }),
1759                    None,
1760                )
1761            }),
1762        );
1763        assert!(invalid_final_output.is_err());
1764
1765        let selection = SelectionRecord {
1766            status: crate::SelectionStatus::Selected,
1767            selected_capability_id: Some("content.comments.publish-comment".to_string()),
1768            selected_capability_version: Some("1.0.0".to_string()),
1769            failure_reason: None,
1770            remaining_candidates: Vec::new(),
1771        };
1772        let mut selected = runtime
1773            .registry
1774            .find_exact(
1775                LookupScope::PublicOnly,
1776                "content.comments.create-comment-draft",
1777                "1.0.0",
1778            )
1779            .unwrap_or_else(|| unreachable!("fixture capability missing"));
1780        selected.record.implementation_kind = ImplementationKind::Workflow;
1781        let (attempt, mut emitter) = super::super::begin_attempt(
1782            RuntimeRequest {
1783                kind: "runtime_request".to_string(),
1784                schema_version: "1.0.0".to_string(),
1785                request_id: "workflow-capability".to_string(),
1786                intent: RuntimeIntent {
1787                    capability_id: Some("content.comments.publish-comment".to_string()),
1788                    capability_version: Some("1.0.0".to_string()),
1789                    version_range: None,
1790                    intent_key: None,
1791                },
1792                input: json!({"comment_text": "hello"}),
1793                lookup: RuntimeLookup {
1794                    scope: RuntimeLookupScope::PublicOnly,
1795                    allow_ambiguity: false,
1796                },
1797                context: RuntimeContext {
1798                    requested_target: crate::PlacementTarget::Local,
1799                    correlation_id: None,
1800                    caller: None,
1801                    traceparent: None,
1802                    tracestate: None,
1803                    metadata: None,
1804                    identity: None,
1805                },
1806                governing_spec: "006-runtime-request-execution".to_string(),
1807            },
1808            crate::RuntimeObservabilityConfig::default(),
1809        );
1810        emitter.push(
1811            crate::RuntimeState::Discovering,
1812            crate::RuntimeTransitionReasonCode::RequestStarted,
1813            json!({"lookup_scope": RuntimeLookupScope::PublicOnly}),
1814        );
1815        emitter.push(
1816            crate::RuntimeState::EvaluatingConstraints,
1817            crate::RuntimeTransitionReasonCode::CandidatesCollected,
1818            json!({"candidate_count": 1}),
1819        );
1820        emitter.push(
1821            crate::RuntimeState::Selecting,
1822            crate::RuntimeTransitionReasonCode::ConstraintsEvaluated,
1823            json!({"eligible_candidates": 1, "rejected_candidates": 0}),
1824        );
1825        let started_execution = crate::start_selected_execution(
1826            &mut emitter,
1827            &selected,
1828            crate::resolve_placement(crate::PlacementTarget::Local)
1829                .unwrap_or_else(|_| unreachable!("local placement should resolve")),
1830            None,
1831        );
1832        let outcome = runtime.execute_workflow_capability(
1833            crate::ExecutionContext {
1834                attempt,
1835                emitter,
1836                candidate_collection: CandidateCollectionRecord {
1837                    lookup_scope: RuntimeLookupScope::PublicOnly,
1838                    candidates: Vec::new(),
1839                    rejected_candidates: Vec::new(),
1840                },
1841                selection,
1842            },
1843            &selected,
1844            started_execution,
1845        );
1846        assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
1847
1848        let mut selected = runtime
1849            .registry
1850            .find_exact(
1851                LookupScope::PublicOnly,
1852                "content.comments.create-comment-draft",
1853                "1.0.0",
1854            )
1855            .unwrap_or_else(|| unreachable!("fixture capability missing"));
1856        selected.record.scope = RegistryScope::Private;
1857        selected.record.implementation_kind = ImplementationKind::Workflow;
1858        selected.artifact.workflow_ref = Some(traverse_registry::WorkflowReference {
1859            workflow_id: "content.comments.publish-comment".to_string(),
1860            workflow_version: "1.0.0".to_string(),
1861        });
1862        let (attempt, mut emitter) = super::super::begin_attempt(
1863            RuntimeRequest {
1864                request_id: "workflow-private".to_string(),
1865                ..valid_runtime_request()
1866            },
1867            crate::RuntimeObservabilityConfig::default(),
1868        );
1869        emitter.push(
1870            crate::RuntimeState::Discovering,
1871            crate::RuntimeTransitionReasonCode::RequestStarted,
1872            json!({"lookup_scope": RuntimeLookupScope::PreferPrivate}),
1873        );
1874        emitter.push(
1875            crate::RuntimeState::EvaluatingConstraints,
1876            crate::RuntimeTransitionReasonCode::CandidatesCollected,
1877            json!({"candidate_count": 1}),
1878        );
1879        emitter.push(
1880            crate::RuntimeState::Selecting,
1881            crate::RuntimeTransitionReasonCode::ConstraintsEvaluated,
1882            json!({"eligible_candidates": 1, "rejected_candidates": 0}),
1883        );
1884        let started_execution = crate::start_selected_execution(
1885            &mut emitter,
1886            &selected,
1887            crate::resolve_placement(crate::PlacementTarget::Local)
1888                .unwrap_or_else(|_| unreachable!("local placement should resolve")),
1889            None,
1890        );
1891        let failing_runtime = Runtime::new(capability_registry_fixture(), FailingWorkflowExecutor)
1892            .with_workflow_registry(workflow_registry_fixture())
1893            .with_security_config(RuntimeSecurityConfig::development());
1894        let outcome = failing_runtime.execute_workflow_capability(
1895            crate::ExecutionContext {
1896                attempt,
1897                emitter,
1898                candidate_collection: CandidateCollectionRecord {
1899                    lookup_scope: RuntimeLookupScope::PreferPrivate,
1900                    candidates: Vec::new(),
1901                    rejected_candidates: Vec::new(),
1902                },
1903                selection: SelectionRecord {
1904                    status: crate::SelectionStatus::Selected,
1905                    selected_capability_id: Some("content.comments.publish-comment".to_string()),
1906                    selected_capability_version: Some("1.0.0".to_string()),
1907                    failure_reason: None,
1908                    remaining_candidates: Vec::new(),
1909                },
1910            },
1911            &selected,
1912            started_execution,
1913        );
1914        assert_eq!(outcome.result.status, RuntimeResultStatus::Error);
1915
1916        let mut unknown = runtime
1917            .registry
1918            .find_exact(
1919                LookupScope::PublicOnly,
1920                "content.comments.create-comment-draft",
1921                "1.0.0",
1922            )
1923            .unwrap_or_else(|| unreachable!("fixture capability missing"));
1924        unknown.record.id = "unknown".to_string();
1925        let _ = WorkflowExecutor.execute(&unknown, &json!({}));
1926        let _ = MissingEventWorkflowExecutor.execute(&unknown, &json!({}));
1927        let _ = BadOutputWorkflowExecutor.execute(&unknown, &json!({}));
1928        unknown.record.id = "content.comments.persist-comment".to_string();
1929        let _ = MissingEventWorkflowExecutor.execute(&unknown, &json!({}));
1930    }
1931
1932    #[test]
1933    fn pipeline_workflow_merges_namespaced_step_outputs_deterministically() {
1934        let registry = pipeline_capability_registry();
1935        let mut workflows = WorkflowRegistry::new();
1936        register_workflow_ok(&mut workflows, &registry, pipeline_workflow_registration());
1937        let runtime = Runtime::new(registry, PipelineExecutor)
1938            .with_workflow_registry(workflows)
1939            .with_security_config(RuntimeSecurityConfig::development());
1940
1941        let first = runtime.execute_workflow(pipeline_workflow_request());
1942        let second = runtime.execute_workflow(pipeline_workflow_request());
1943
1944        assert_eq!(first.result.status, WorkflowTraversalStatus::Completed);
1945        assert_eq!(
1946            first.result.output,
1947            Some(json!({
1948                "validate": {"valid": true, "issues": []},
1949                "process": {
1950                    "title": "Hello world",
1951                    "tags": ["hello", "world"],
1952                    "noteType": "fleeting",
1953                    "suggestedNextAction": "archive",
1954                    "status": "complete"
1955                },
1956                "summarize": {"summary": "Hello world (fleeting)", "wordCount": 3}
1957            }))
1958        );
1959        assert_eq!(first.result, second.result);
1960        assert_eq!(first.evidence.visited_nodes, second.evidence.visited_nodes);
1961
1962        let steps = &first.evidence.visited_nodes;
1963        assert_eq!(steps.len(), 3);
1964        assert_eq!(steps[0].step_index, 0);
1965        assert_eq!(steps[0].capability_id, "content.comments.pipeline-validate");
1966        assert_eq!(steps[1].step_index, 1);
1967        assert_eq!(steps[1].capability_id, "content.comments.pipeline-process");
1968        assert_eq!(steps[2].step_index, 2);
1969        assert_eq!(
1970            steps[2].capability_id,
1971            "content.comments.pipeline-summarize"
1972        );
1973        assert!(
1974            steps
1975                .iter()
1976                .all(|step| step.status == WorkflowTraversalStepStatus::Completed)
1977        );
1978    }
1979
1980    #[test]
1981    fn pipeline_workflow_stops_on_failed_step_with_failed_step_id_in_trace() {
1982        let registry = pipeline_capability_registry();
1983        let mut workflows = WorkflowRegistry::new();
1984        register_workflow_ok(&mut workflows, &registry, pipeline_workflow_registration());
1985        let runtime = Runtime::new(registry, FailingPipelineExecutor)
1986            .with_workflow_registry(workflows)
1987            .with_security_config(RuntimeSecurityConfig::development());
1988
1989        let outcome = runtime.execute_workflow(pipeline_workflow_request());
1990
1991        assert_eq!(outcome.result.status, WorkflowTraversalStatus::Error);
1992        assert_eq!(
1993            outcome.evidence.result.failure_reason,
1994            Some(WorkflowTraversalFailureReason::StepExecutionFailed)
1995        );
1996        let steps = &outcome.evidence.visited_nodes;
1997        assert_eq!(steps.len(), 2);
1998        assert_eq!(steps[0].status, WorkflowTraversalStepStatus::Completed);
1999        assert_eq!(steps[1].node_id, "process_note");
2000        assert_eq!(steps[1].status, WorkflowTraversalStepStatus::Failed);
2001    }
2002
2003    #[test]
2004    fn workflow_step_rejects_unsigned_artifact_under_default_security_config() {
2005        // No explicit security config: the default (Production) posture must
2006        // reject the unsigned artifact before the workflow step executes,
2007        // exactly like direct capability execution (spec 030 FR-013).
2008        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
2009            .with_workflow_registry(workflow_registry_fixture());
2010
2011        let outcome = runtime.execute_workflow(valid_workflow_request());
2012
2013        assert_eq!(outcome.result.status, WorkflowTraversalStatus::Error);
2014        assert_eq!(
2015            outcome.evidence.result.failure_reason,
2016            Some(WorkflowTraversalFailureReason::StepExecutionFailed)
2017        );
2018        let error = outcome.result.error;
2019        assert_eq!(
2020            error.as_ref().map(|error| error.code),
2021            Some(RuntimeErrorCode::ContractViolation)
2022        );
2023        assert_eq!(
2024            error
2025                .as_ref()
2026                .and_then(|error| error.details.get("code"))
2027                .and_then(Value::as_str),
2028            Some("missing_signature")
2029        );
2030        assert_eq!(
2031            error
2032                .as_ref()
2033                .and_then(|error| error.details.get("node_id"))
2034                .and_then(Value::as_str),
2035            Some("create_draft")
2036        );
2037        let steps = &outcome.evidence.visited_nodes;
2038        assert_eq!(steps.len(), 1);
2039        assert_eq!(steps[0].node_id, "create_draft");
2040        assert_eq!(steps[0].status, WorkflowTraversalStepStatus::Failed);
2041        assert!(outcome.result.warnings.is_empty());
2042    }
2043
2044    #[test]
2045    fn workflow_step_unsigned_artifact_warns_and_executes_in_development_mode() {
2046        let runtime = Runtime::new(capability_registry_fixture(), WorkflowExecutor)
2047            .with_workflow_registry(workflow_registry_fixture())
2048            .with_security_config(RuntimeSecurityConfig::development());
2049
2050        let outcome = runtime.execute_workflow(valid_workflow_request());
2051
2052        assert_eq!(outcome.result.status, WorkflowTraversalStatus::Completed);
2053        assert_eq!(outcome.result.warnings.len(), 3);
2054        assert!(
2055            outcome
2056                .result
2057                .warnings
2058                .iter()
2059                .all(|warning| warning.code == "unsigned_local_dev_artifact")
2060        );
2061    }
2062
2063    #[test]
2064    fn workflow_step_fails_when_signed_artifact_bytes_cannot_be_loaded() {
2065        let runtime = Runtime::new(
2066            signed_missing_binary_capability_registry_fixture(),
2067            WorkflowExecutor,
2068        )
2069        .with_workflow_registry(workflow_registry_fixture());
2070
2071        let outcome = runtime.execute_workflow(valid_workflow_request());
2072
2073        assert_eq!(outcome.result.status, WorkflowTraversalStatus::Error);
2074        assert_eq!(
2075            outcome.evidence.result.failure_reason,
2076            Some(WorkflowTraversalFailureReason::StepExecutionFailed)
2077        );
2078        let error = outcome.result.error;
2079        assert_eq!(
2080            error.as_ref().map(|error| error.code),
2081            Some(RuntimeErrorCode::ArtifactMissing)
2082        );
2083        assert_eq!(
2084            error
2085                .as_ref()
2086                .and_then(|error| error.details.get("code"))
2087                .and_then(Value::as_str),
2088            Some("artifact_load_failed")
2089        );
2090        let steps = &outcome.evidence.visited_nodes;
2091        assert_eq!(steps.len(), 1);
2092        assert_eq!(steps[0].status, WorkflowTraversalStepStatus::Failed);
2093    }
2094
2095    fn pipeline_capability_registry() -> CapabilityRegistry {
2096        let mut registry = CapabilityRegistry::new();
2097        for id in [
2098            "content.comments.pipeline-validate",
2099            "content.comments.pipeline-process",
2100            "content.comments.pipeline-summarize",
2101        ] {
2102            register_capability_ok(
2103                &mut registry,
2104                CapabilityRegistration {
2105                    scope: RegistryScope::Public,
2106                    contract: capability_contract(
2107                        id,
2108                        Vec::new(),
2109                        json!({"type": "object", "additionalProperties": true}),
2110                        json!({"type": "object", "additionalProperties": true}),
2111                    ),
2112                    contract_path: format!("registry/{id}.json"),
2113                    artifact: CapabilityArtifactRecord {
2114                        artifact_ref: format!("artifact-{id}"),
2115                        implementation_kind: ImplementationKind::Executable,
2116                        source: SourceReference {
2117                            kind: SourceKind::Git,
2118                            location: format!("https://example.com/{id}.git"),
2119                        },
2120                        binary: Some(BinaryReference {
2121                            format: BinaryFormat::Wasm,
2122                            location: format!("{id}.wasm"),
2123                            signature: None,
2124                        }),
2125                        workflow_ref: None,
2126                        digests: ArtifactDigests {
2127                            source_digest: "source".to_string(),
2128                            binary_digest: Some("binary".to_string()),
2129                        },
2130                        provenance: RegistryProvenance {
2131                            source: "fixtures".to_string(),
2132                            author: "Enrico".to_string(),
2133                            created_at: "2026-03-27T00:00:00Z".to_string(),
2134                        },
2135                    },
2136                    registered_at: "2026-03-27T00:00:00Z".to_string(),
2137                    tags: vec!["pipeline".to_string()],
2138                    composability: ComposabilityMetadata {
2139                        kind: CompositionKind::Atomic,
2140                        patterns: vec![CompositionPattern::Sequential],
2141                        provides: vec!["pipeline-step".to_string()],
2142                        requires: Vec::new(),
2143                    },
2144                    governing_spec: "005-capability-registry".to_string(),
2145                    validator_version: "validator".to_string(),
2146                },
2147            );
2148        }
2149        registry
2150    }
2151
2152    #[allow(clippy::too_many_lines)]
2153    fn pipeline_workflow_registration() -> WorkflowRegistration {
2154        WorkflowRegistration {
2155            scope: RegistryScope::Public,
2156            definition: WorkflowDefinition {
2157                kind: "workflow_definition".to_string(),
2158                schema_version: "1.0.0".to_string(),
2159                id: "content.comments.pipeline".to_string(),
2160                name: "pipeline".to_string(),
2161                version: "1.0.0".to_string(),
2162                lifecycle: Lifecycle::Active,
2163                owner: Owner {
2164                    team: "traverse-core".to_string(),
2165                    contact: "test@example.com".to_string(),
2166                },
2167                summary: "Deterministic three-step pipeline fixture.".to_string(),
2168                inputs: SchemaContainer {
2169                    schema: json!({
2170                        "type": "object",
2171                        "required": ["note"],
2172                        "properties": {"note": {"type": "string"}},
2173                        "additionalProperties": false
2174                    }),
2175                },
2176                outputs: SchemaContainer {
2177                    schema: json!({
2178                        "type": "object",
2179                        "required": ["validate", "process", "summarize"],
2180                        "properties": {
2181                            "validate": {"type": "object"},
2182                            "process": {"type": "object"},
2183                            "summarize": {"type": "object"}
2184                        },
2185                        "additionalProperties": false
2186                    }),
2187                },
2188                nodes: vec![
2189                    WorkflowNode {
2190                        node_id: "validate_note".to_string(),
2191                        capability_id: "content.comments.pipeline-validate".to_string(),
2192                        capability_version: "1.0.0".to_string(),
2193                        input: WorkflowNodeInput {
2194                            from_workflow_input: vec!["note".to_string()],
2195                        },
2196                        output: WorkflowNodeOutput {
2197                            to_workflow_state: Vec::new(),
2198                            publish_to_state_as: Some("validate".to_string()),
2199                        },
2200                    },
2201                    WorkflowNode {
2202                        node_id: "process_note".to_string(),
2203                        capability_id: "content.comments.pipeline-process".to_string(),
2204                        capability_version: "1.0.0".to_string(),
2205                        input: WorkflowNodeInput {
2206                            from_workflow_input: vec!["note".to_string()],
2207                        },
2208                        output: WorkflowNodeOutput {
2209                            to_workflow_state: vec![
2210                                "title".to_string(),
2211                                "tags".to_string(),
2212                                "noteType".to_string(),
2213                                "suggestedNextAction".to_string(),
2214                                "status".to_string(),
2215                            ],
2216                            publish_to_state_as: Some("process".to_string()),
2217                        },
2218                    },
2219                    WorkflowNode {
2220                        node_id: "summarize_note".to_string(),
2221                        capability_id: "content.comments.pipeline-summarize".to_string(),
2222                        capability_version: "1.0.0".to_string(),
2223                        input: WorkflowNodeInput {
2224                            from_workflow_input: vec![
2225                                "title".to_string(),
2226                                "tags".to_string(),
2227                                "noteType".to_string(),
2228                                "suggestedNextAction".to_string(),
2229                                "status".to_string(),
2230                            ],
2231                        },
2232                        output: WorkflowNodeOutput {
2233                            to_workflow_state: Vec::new(),
2234                            publish_to_state_as: Some("summarize".to_string()),
2235                        },
2236                    },
2237                ],
2238                edges: vec![
2239                    WorkflowEdge {
2240                        edge_id: "validate_to_process".to_string(),
2241                        from: "validate_note".to_string(),
2242                        to: "process_note".to_string(),
2243                        trigger: WorkflowEdgeTrigger::Direct,
2244                        event: None,
2245                        predicate: None,
2246                    },
2247                    WorkflowEdge {
2248                        edge_id: "process_to_summarize".to_string(),
2249                        from: "process_note".to_string(),
2250                        to: "summarize_note".to_string(),
2251                        trigger: WorkflowEdgeTrigger::Direct,
2252                        event: None,
2253                        predicate: None,
2254                    },
2255                ],
2256                start_node: "validate_note".to_string(),
2257                terminal_nodes: vec!["summarize_note".to_string()],
2258                output_projection: vec![
2259                    "validate".to_string(),
2260                    "process".to_string(),
2261                    "summarize".to_string(),
2262                ],
2263                tags: vec!["pipeline".to_string()],
2264                governing_spec: "007-workflow-registry-traversal".to_string(),
2265            },
2266            workflow_path: "workflows/content.comments.pipeline/workflow.json".to_string(),
2267            registered_at: "2026-07-08T00:00:00Z".to_string(),
2268            validator_version: "validator".to_string(),
2269        }
2270    }
2271
2272    fn pipeline_workflow_request() -> WorkflowExecutionRequest {
2273        WorkflowExecutionRequest {
2274            kind: "workflow_execution_request".to_string(),
2275            schema_version: "1.0.0".to_string(),
2276            request_id: "pipeline-request".to_string(),
2277            workflow_id: "content.comments.pipeline".to_string(),
2278            workflow_version: "1.0.0".to_string(),
2279            scope: WorkflowLookupScope::PublicOnly,
2280            input: json!({"note": "Hello world"}),
2281            governing_spec: "007-workflow-registry-traversal".to_string(),
2282        }
2283    }
2284
2285    struct PipelineExecutor;
2286
2287    impl LocalExecutor for PipelineExecutor {
2288        fn execute(
2289            &self,
2290            capability: &ResolvedCapability,
2291            _input: &Value,
2292        ) -> Result<Value, LocalExecutionFailure> {
2293            let output = match capability.record.id.as_str() {
2294                "content.comments.pipeline-validate" => json!({"valid": true, "issues": []}),
2295                "content.comments.pipeline-process" => json!({
2296                    "title": "Hello world",
2297                    "tags": ["hello", "world"],
2298                    "noteType": "fleeting",
2299                    "suggestedNextAction": "archive",
2300                    "status": "complete"
2301                }),
2302                _ => json!({"summary": "Hello world (fleeting)", "wordCount": 3}),
2303            };
2304            Ok(output)
2305        }
2306    }
2307
2308    struct FailingPipelineExecutor;
2309
2310    impl LocalExecutor for FailingPipelineExecutor {
2311        fn execute(
2312            &self,
2313            capability: &ResolvedCapability,
2314            _input: &Value,
2315        ) -> Result<Value, LocalExecutionFailure> {
2316            match capability.record.id.as_str() {
2317                "content.comments.pipeline-validate" => Ok(json!({"valid": true, "issues": []})),
2318                other => Err(LocalExecutionFailure {
2319                    code: LocalExecutionFailureCode::ExecutionFailed,
2320                    message: format!("step failed: {other}"),
2321                }),
2322            }
2323        }
2324    }
2325
2326    fn capability_registry_fixture() -> CapabilityRegistry {
2327        build_capability_registry(false, None)
2328    }
2329
2330    fn signed_missing_binary_capability_registry_fixture() -> CapabilityRegistry {
2331        build_capability_registry(
2332            false,
2333            Some(ArtifactSignature {
2334                scheme: ArtifactSignatureScheme::Ed25519,
2335                public_key_hex: Some("00".repeat(32)),
2336                signature_hex: Some("00".repeat(64)),
2337                sigstore_bundle_ref: None,
2338            }),
2339        )
2340    }
2341
2342    #[allow(clippy::too_many_lines, clippy::needless_pass_by_value)]
2343    fn build_capability_registry(
2344        strict_inputs: bool,
2345        signature: Option<ArtifactSignature>,
2346    ) -> CapabilityRegistry {
2347        let mut registry = CapabilityRegistry::new();
2348        for (id, emits, output, required_key) in [
2349            (
2350                "content.comments.create-comment-draft",
2351                vec![EventReference {
2352                    event_id: "content.comments.draft-created".to_string(),
2353                    version: "1.0.0".to_string(),
2354                }],
2355                json!({
2356                    "type": "object",
2357                    "properties": {
2358                        "draft_id": { "type": "string" },
2359                        "emitted_events": { "type": "array" }
2360                    },
2361                    "required": ["draft_id"],
2362                    "additionalProperties": true
2363                }),
2364                "comment_text",
2365            ),
2366            (
2367                "content.comments.validate-comment",
2368                vec![EventReference {
2369                    event_id: "content.comments.validated".to_string(),
2370                    version: "1.0.0".to_string(),
2371                }],
2372                json!({
2373                    "type": "object",
2374                    "properties": {
2375                        "draft_id": { "type": "string" },
2376                        "emitted_events": { "type": "array" }
2377                    },
2378                    "required": ["draft_id"],
2379                    "additionalProperties": true
2380                }),
2381                "draft_id",
2382            ),
2383            (
2384                "content.comments.persist-comment",
2385                vec![],
2386                json!({
2387                    "type": "object",
2388                    "properties": { "comment_id": { "type": "string" } },
2389                    "required": ["comment_id"],
2390                    "additionalProperties": true
2391                }),
2392                "draft_id",
2393            ),
2394        ] {
2395            register_capability_ok(
2396                &mut registry,
2397                CapabilityRegistration {
2398                    scope: RegistryScope::Public,
2399                    contract: capability_contract(
2400                        id,
2401                        emits,
2402                        json!({
2403                            "type": "object",
2404                            "properties": {
2405                                "comment_text": { "type": "string" },
2406                                "draft_id": { "type": "string" }
2407                            },
2408                            "required": if strict_inputs {
2409                                vec![required_key]
2410                            } else {
2411                                Vec::<&str>::new()
2412                            },
2413                            "additionalProperties": true
2414                        }),
2415                        output,
2416                    ),
2417                    contract_path: format!("registry/{id}.json"),
2418                    artifact: CapabilityArtifactRecord {
2419                        artifact_ref: format!("artifact-{id}"),
2420                        implementation_kind: ImplementationKind::Executable,
2421                        source: SourceReference {
2422                            kind: SourceKind::Git,
2423                            location: format!("https://example.com/{id}.git"),
2424                        },
2425                        binary: Some(BinaryReference {
2426                            format: BinaryFormat::Wasm,
2427                            location: format!("{id}.wasm"),
2428                            signature: signature.clone(),
2429                        }),
2430                        workflow_ref: None,
2431                        digests: ArtifactDigests {
2432                            source_digest: "source".to_string(),
2433                            binary_digest: Some("binary".to_string()),
2434                        },
2435                        provenance: RegistryProvenance {
2436                            source: "fixtures".to_string(),
2437                            author: "Enrico".to_string(),
2438                            created_at: "2026-03-27T00:00:00Z".to_string(),
2439                        },
2440                    },
2441                    registered_at: "2026-03-27T00:00:00Z".to_string(),
2442                    tags: vec!["comments".to_string()],
2443                    composability: ComposabilityMetadata {
2444                        kind: CompositionKind::Atomic,
2445                        patterns: vec![CompositionPattern::Sequential],
2446                        provides: vec!["comment".to_string()],
2447                        requires: Vec::new(),
2448                    },
2449                    governing_spec: "005-capability-registry".to_string(),
2450                    validator_version: "validator".to_string(),
2451                },
2452            );
2453        }
2454        registry
2455    }
2456
2457    fn strict_input_capability_registry_fixture() -> CapabilityRegistry {
2458        build_capability_registry(true, None)
2459    }
2460
2461    fn workflow_registry_fixture() -> WorkflowRegistry {
2462        let registry = capability_registry_fixture();
2463        let mut workflows = WorkflowRegistry::new();
2464        register_workflow_ok(
2465            &mut workflows,
2466            &registry,
2467            WorkflowRegistration {
2468                scope: RegistryScope::Public,
2469                definition: workflow_definition_fixture(
2470                    Some(EventReference {
2471                        event_id: "content.comments.validated".to_string(),
2472                        version: "1.0.0".to_string(),
2473                    }),
2474                    None,
2475                ),
2476                workflow_path: "workflows/publish-comment.json".to_string(),
2477                registered_at: "2026-03-27T00:00:00Z".to_string(),
2478                validator_version: "workflow-validator".to_string(),
2479            },
2480        );
2481        workflows
2482    }
2483
2484    fn workflow_definition_fixture(
2485        second_event: Option<EventReference>,
2486        direct_edge: Option<WorkflowEdge>,
2487    ) -> WorkflowDefinition {
2488        let mut edges = vec![
2489            WorkflowEdge {
2490                edge_id: "draft_to_validate".to_string(),
2491                from: "create_draft".to_string(),
2492                to: "validate_comment".to_string(),
2493                trigger: WorkflowEdgeTrigger::Event,
2494                event: Some(EventReference {
2495                    event_id: "content.comments.draft-created".to_string(),
2496                    version: "1.0.0".to_string(),
2497                }),
2498                predicate: None,
2499            },
2500            WorkflowEdge {
2501                edge_id: "validate_to_persist".to_string(),
2502                from: "validate_comment".to_string(),
2503                to: "persist_comment".to_string(),
2504                trigger: WorkflowEdgeTrigger::Event,
2505                event: second_event,
2506                predicate: None,
2507            },
2508        ];
2509        if let Some(edge) = direct_edge {
2510            edges.push(edge);
2511        }
2512        WorkflowDefinition {
2513            kind: "workflow_definition".to_string(),
2514            schema_version: "1.0.0".to_string(),
2515            id: "content.comments.publish-comment".to_string(),
2516            name: "publish-comment".to_string(),
2517            version: "1.0.0".to_string(),
2518            lifecycle: Lifecycle::Active,
2519            owner: Owner {
2520                team: "comments".to_string(),
2521                contact: "comments@example.com".to_string(),
2522            },
2523            summary: "Publish a comment deterministically.".to_string(),
2524            inputs: SchemaContainer {
2525                schema: json!({
2526                    "type": "object",
2527                    "properties": { "comment_text": { "type": "string" } },
2528                    "required": ["comment_text"],
2529                    "additionalProperties": true
2530                }),
2531            },
2532            outputs: SchemaContainer {
2533                schema: json!({
2534                    "type": "object",
2535                    "properties": { "comment_id": { "type": "string" } },
2536                    "required": ["comment_id"],
2537                    "additionalProperties": true
2538                }),
2539            },
2540            nodes: vec![
2541                WorkflowNode {
2542                    node_id: "create_draft".to_string(),
2543                    capability_id: "content.comments.create-comment-draft".to_string(),
2544                    capability_version: "1.0.0".to_string(),
2545                    input: WorkflowNodeInput {
2546                        from_workflow_input: vec!["comment_text".to_string()],
2547                    },
2548                    output: WorkflowNodeOutput {
2549                        to_workflow_state: vec!["draft_id".to_string()],
2550                        publish_to_state_as: None,
2551                    },
2552                },
2553                WorkflowNode {
2554                    node_id: "validate_comment".to_string(),
2555                    capability_id: "content.comments.validate-comment".to_string(),
2556                    capability_version: "1.0.0".to_string(),
2557                    input: WorkflowNodeInput {
2558                        from_workflow_input: vec!["draft_id".to_string()],
2559                    },
2560                    output: WorkflowNodeOutput {
2561                        to_workflow_state: vec!["draft_id".to_string()],
2562                        publish_to_state_as: None,
2563                    },
2564                },
2565                WorkflowNode {
2566                    node_id: "persist_comment".to_string(),
2567                    capability_id: "content.comments.persist-comment".to_string(),
2568                    capability_version: "1.0.0".to_string(),
2569                    input: WorkflowNodeInput {
2570                        from_workflow_input: vec!["draft_id".to_string()],
2571                    },
2572                    output: WorkflowNodeOutput {
2573                        to_workflow_state: vec!["comment_id".to_string()],
2574                        publish_to_state_as: None,
2575                    },
2576                },
2577            ],
2578            edges,
2579            start_node: "create_draft".to_string(),
2580            terminal_nodes: vec!["persist_comment".to_string()],
2581            output_projection: Vec::new(),
2582            tags: vec!["comments".to_string()],
2583            governing_spec: "007-workflow-registry-traversal".to_string(),
2584        }
2585    }
2586
2587    fn capability_contract(
2588        id: &str,
2589        emits: Vec<EventReference>,
2590        inputs: Value,
2591        outputs: Value,
2592    ) -> CapabilityContract {
2593        CapabilityContract {
2594            kind: "capability_contract".to_string(),
2595            schema_version: "1.0.0".to_string(),
2596            id: id.to_string(),
2597            namespace: "content.comments".to_string(),
2598            name: id.rsplit('.').next().unwrap_or("capability").to_string(),
2599            version: "1.0.0".to_string(),
2600            lifecycle: Lifecycle::Active,
2601            owner: Owner {
2602                team: "comments".to_string(),
2603                contact: "comments@example.com".to_string(),
2604            },
2605            summary: "workflow fixture capability".to_string(),
2606            description: "workflow fixture capability used in runtime tests".to_string(),
2607            inputs: SchemaContainer { schema: inputs },
2608            outputs: SchemaContainer { schema: outputs },
2609            preconditions: vec![Condition {
2610                id: "precondition".to_string(),
2611                description: "must be valid".to_string(),
2612            }],
2613            postconditions: vec![Condition {
2614                id: "postcondition".to_string(),
2615                description: "must produce output".to_string(),
2616            }],
2617            side_effects: vec![SideEffect {
2618                kind: SideEffectKind::MemoryOnly,
2619                description: "memory only".to_string(),
2620            }],
2621            emits,
2622            consumes: Vec::new(),
2623            permissions: vec![IdReference {
2624                id: "permission".to_string(),
2625            }],
2626            execution: Execution {
2627                binary_format: ContractBinaryFormat::Wasm,
2628                entrypoint: Entrypoint {
2629                    kind: EntrypointKind::WasiCommand,
2630                    command: "run".to_string(),
2631                },
2632                preferred_targets: vec![ExecutionTarget::Local],
2633                constraints: ExecutionConstraints {
2634                    host_api_access: HostApiAccess::None,
2635                    network_access: NetworkAccess::Forbidden,
2636                    filesystem_access: FilesystemAccess::None,
2637                },
2638            },
2639            policies: Vec::new(),
2640            dependencies: Vec::new(),
2641            provenance: Provenance {
2642                source: ProvenanceSource::Greenfield,
2643                author: "Enrico".to_string(),
2644                created_at: "2026-03-27T00:00:00Z".to_string(),
2645                spec_ref: Some("007-workflow-registry-traversal".to_string()),
2646                adr_refs: Vec::new(),
2647                exception_refs: Vec::new(),
2648            },
2649            evidence: vec![ValidationEvidence {
2650                evidence_id: "evidence".to_string(),
2651                evidence_type: EvidenceType::ContractValidation,
2652                status: EvidenceStatus::Passed,
2653            }],
2654            service_type: ServiceType::Stateless,
2655            permitted_targets: vec![
2656                ExecutionTarget::Local,
2657                ExecutionTarget::Cloud,
2658                ExecutionTarget::Edge,
2659                ExecutionTarget::Device,
2660            ],
2661            event_trigger: None,
2662            connector_requirements: Vec::new(),
2663            state_schema: None,
2664        }
2665    }
2666
2667    fn valid_workflow_request() -> WorkflowExecutionRequest {
2668        WorkflowExecutionRequest {
2669            kind: "workflow_execution_request".to_string(),
2670            schema_version: "1.0.0".to_string(),
2671            request_id: "workflow-request".to_string(),
2672            workflow_id: "content.comments.publish-comment".to_string(),
2673            workflow_version: "1.0.0".to_string(),
2674            scope: WorkflowLookupScope::PublicOnly,
2675            input: json!({"comment_text": "hello"}),
2676            governing_spec: "007-workflow-registry-traversal".to_string(),
2677        }
2678    }
2679
2680    fn valid_runtime_request() -> RuntimeRequest {
2681        RuntimeRequest {
2682            kind: "runtime_request".to_string(),
2683            schema_version: "1.0.0".to_string(),
2684            request_id: "runtime-request".to_string(),
2685            intent: RuntimeIntent {
2686                capability_id: Some("content.comments.publish-comment".to_string()),
2687                capability_version: Some("1.0.0".to_string()),
2688                version_range: None,
2689                intent_key: None,
2690            },
2691            input: json!({"comment_text": "hello"}),
2692            lookup: RuntimeLookup {
2693                scope: RuntimeLookupScope::PublicOnly,
2694                allow_ambiguity: false,
2695            },
2696            context: RuntimeContext {
2697                requested_target: crate::PlacementTarget::Local,
2698                correlation_id: None,
2699                caller: None,
2700                traceparent: None,
2701                tracestate: None,
2702                metadata: None,
2703                identity: None,
2704            },
2705            governing_spec: "006-runtime-request-execution".to_string(),
2706        }
2707    }
2708
2709    struct WorkflowExecutor;
2710
2711    impl LocalExecutor for WorkflowExecutor {
2712        fn execute(
2713            &self,
2714            capability: &ResolvedCapability,
2715            _input: &Value,
2716        ) -> Result<Value, LocalExecutionFailure> {
2717            let output = match capability.record.id.as_str() {
2718                "content.comments.create-comment-draft" => json!({
2719                    "draft_id": "draft-1",
2720                    "emitted_events": [
2721                        {"event_id": "content.comments.draft-created", "version": "1.0.0"}
2722                    ]
2723                }),
2724                "content.comments.validate-comment" => json!({
2725                    "draft_id": "draft-1",
2726                    "emitted_events": [
2727                        {"event_id": "content.comments.validated", "version": "1.0.0"}
2728                    ]
2729                }),
2730                "content.comments.persist-comment" => json!({
2731                    "comment_id": "comment-1"
2732                }),
2733                _ => json!({}),
2734            };
2735            Ok(output)
2736        }
2737    }
2738
2739    struct FailingWorkflowExecutor;
2740
2741    impl LocalExecutor for FailingWorkflowExecutor {
2742        fn execute(
2743            &self,
2744            _capability: &ResolvedCapability,
2745            _input: &Value,
2746        ) -> Result<Value, LocalExecutionFailure> {
2747            Err(LocalExecutionFailure {
2748                code: LocalExecutionFailureCode::ExecutionFailed,
2749                message: "boom".to_string(),
2750            })
2751        }
2752    }
2753
2754    struct MissingEventWorkflowExecutor;
2755
2756    struct BadOutputWorkflowExecutor;
2757
2758    impl LocalExecutor for MissingEventWorkflowExecutor {
2759        fn execute(
2760            &self,
2761            capability: &ResolvedCapability,
2762            _input: &Value,
2763        ) -> Result<Value, LocalExecutionFailure> {
2764            let output = match capability.record.id.as_str() {
2765                "content.comments.create-comment-draft" => json!({
2766                    "draft_id": "draft-1",
2767                    "emitted_events": [
2768                        {"event_id": "content.comments.draft-created", "version": "1.0.0"}
2769                    ]
2770                }),
2771                "content.comments.validate-comment" => json!({
2772                    "draft_id": "draft-1"
2773                }),
2774                "content.comments.persist-comment" => json!({
2775                    "comment_id": "comment-1"
2776                }),
2777                _ => json!({}),
2778            };
2779            Ok(output)
2780        }
2781    }
2782
2783    impl LocalExecutor for BadOutputWorkflowExecutor {
2784        fn execute(
2785            &self,
2786            capability: &ResolvedCapability,
2787            _input: &Value,
2788        ) -> Result<Value, LocalExecutionFailure> {
2789            let output = match capability.record.id.as_str() {
2790                "content.comments.create-comment-draft" => json!({
2791                    "emitted_events": [
2792                        {"event_id": "content.comments.draft-created", "version": "1.0.0"}
2793                    ]
2794                }),
2795                _ => json!({}),
2796            };
2797            Ok(output)
2798        }
2799    }
2800
2801    fn register_capability_ok(registry: &mut CapabilityRegistry, request: CapabilityRegistration) {
2802        match registry.register(request) {
2803            Ok(_) => {}
2804            Err(error) => unreachable!("{error:?}"),
2805        }
2806    }
2807
2808    fn register_workflow_ok(
2809        registry: &mut WorkflowRegistry,
2810        capabilities: &CapabilityRegistry,
2811        request: WorkflowRegistration,
2812    ) {
2813        match registry.register(capabilities, request) {
2814            Ok(_) => {}
2815            Err(error) => unreachable!("{error:?}"),
2816        }
2817    }
2818
2819    #[test]
2820    fn helper_guards_cover_unreachable_branches() {
2821        let capability_panic = std::panic::catch_unwind(|| {
2822            register_capability_ok(
2823                &mut CapabilityRegistry::new(),
2824                CapabilityRegistration {
2825                    scope: RegistryScope::Public,
2826                    contract: capability_contract("bad", Vec::new(), json!({}), json!({})),
2827                    contract_path: String::new(),
2828                    artifact: workflow_artifact_record("bad", "1.0.0", "artifact"),
2829                    registered_at: String::new(),
2830                    tags: Vec::new(),
2831                    composability: ComposabilityMetadata {
2832                        kind: CompositionKind::Atomic,
2833                        patterns: Vec::new(),
2834                        provides: Vec::new(),
2835                        requires: Vec::new(),
2836                    },
2837                    governing_spec: "005-capability-registry".to_string(),
2838                    validator_version: "validator".to_string(),
2839                },
2840            );
2841        });
2842        assert!(capability_panic.is_err());
2843
2844        let workflow_panic = std::panic::catch_unwind(|| {
2845            register_workflow_ok(
2846                &mut WorkflowRegistry::new(),
2847                &CapabilityRegistry::new(),
2848                WorkflowRegistration {
2849                    scope: RegistryScope::Public,
2850                    definition: workflow_definition_fixture(
2851                        None,
2852                        Some(WorkflowEdge {
2853                            edge_id: "direct".to_string(),
2854                            from: "create_draft".to_string(),
2855                            to: "validate_comment".to_string(),
2856                            trigger: WorkflowEdgeTrigger::Direct,
2857                            event: None,
2858                            predicate: None,
2859                        }),
2860                    ),
2861                    workflow_path: String::new(),
2862                    registered_at: String::new(),
2863                    validator_version: "validator".to_string(),
2864                },
2865            );
2866        });
2867        assert!(workflow_panic.is_err());
2868    }
2869
2870    fn resolved_workflow(definition: WorkflowDefinition) -> ResolvedWorkflow {
2871        ResolvedWorkflow {
2872            record: WorkflowRegistryRecord {
2873                scope: RegistryScope::Public,
2874                id: definition.id.clone(),
2875                version: definition.version.clone(),
2876                lifecycle: definition.lifecycle.clone(),
2877                owner: definition.owner.clone(),
2878                workflow_path: "workflows/manual.json".to_string(),
2879                workflow_digest: "digest".to_string(),
2880                registered_at: "2026-03-27T00:00:00Z".to_string(),
2881                governing_spec: "007-workflow-registry-traversal".to_string(),
2882                validator_version: "validator".to_string(),
2883                evidence: traverse_registry::WorkflowRegistrationEvidence {
2884                    evidence_id: "evidence".to_string(),
2885                    workflow_id: definition.id.clone(),
2886                    workflow_version: definition.version.clone(),
2887                    scope: RegistryScope::Public,
2888                    governing_spec: "007-workflow-registry-traversal".to_string(),
2889                    validator_version: "validator".to_string(),
2890                    produced_at: "2026-03-27T00:00:00Z".to_string(),
2891                    result: traverse_registry::WorkflowRegistrationResult::Passed,
2892                },
2893            },
2894            index_entry: traverse_registry::WorkflowDiscoveryIndexEntry {
2895                scope: RegistryScope::Public,
2896                id: definition.id.clone(),
2897                version: definition.version.clone(),
2898                lifecycle: definition.lifecycle.clone(),
2899                owner: definition.owner.clone(),
2900                summary: definition.summary.clone(),
2901                tags: definition.tags.clone(),
2902                participating_capabilities: definition
2903                    .nodes
2904                    .iter()
2905                    .map(|node| node.capability_id.clone())
2906                    .collect(),
2907                events_used: Vec::new(),
2908                start_node: definition.start_node.clone(),
2909                terminal_nodes: definition.terminal_nodes.clone(),
2910                registered_at: "2026-03-27T00:00:00Z".to_string(),
2911            },
2912            definition,
2913        }
2914    }
2915}