1use serde::Serialize;
2
3use crate::ast::{FieldValue, PgnPacket};
4use crate::registry::WorkflowRegistry;
5use crate::resolver::ResolvedRef;
6use crate::safety::SafetyResult;
7
8#[derive(Debug, Serialize)]
9pub struct ExpandedRef {
10 pub reference: String,
11 pub status: String,
12 pub confidence: f32,
13 pub path: Option<String>,
14}
15
16impl From<&ResolvedRef> for ExpandedRef {
17 fn from(r: &ResolvedRef) -> Self {
18 ExpandedRef {
19 reference: r.original.clone(),
20 status: format!("{:?}", r.status),
21 confidence: r.confidence,
22 path: r.resolved_path.as_ref().map(|p| p.display().to_string()),
23 }
24 }
25}
26
27#[derive(Debug, Serialize)]
28pub struct ExpandedRunPacket {
29 pub spec_version: String,
30 pub run_id: String,
31 pub workflow: String,
32 pub mode: String,
33 pub inputs: Vec<ExpandedRef>,
34 pub outputs: Vec<ExpandedRef>,
35 pub do_actions: Vec<String>,
36 pub deny_actions: Vec<String>,
37 pub effective_risk: String,
38 pub human_required: bool,
39 pub recommended_executor: String,
40 pub fallback_executor: String,
41 pub ttl: String,
42 pub note: Option<String>,
43}
44
45pub fn expand_to_run_packet(
46 packet: &PgnPacket,
47 resolved_refs: &[ResolvedRef],
48 safety: &SafetyResult,
49 workflows: &WorkflowRegistry,
50) -> ExpandedRunPacket {
51 let wf_name = packet
52 .fields
53 .get("wf")
54 .and_then(|v| match v {
55 FieldValue::Scalar(s) => Some(s.as_str()),
56 _ => None,
57 })
58 .unwrap_or("unknown");
59
60 let workflow_entry = workflows.workflows.get(wf_name);
61
62 let inputs: Vec<ExpandedRef> = resolved_refs
63 .iter()
64 .filter(|r| matches!(packet.fields.get("in"), Some(FieldValue::List(refs)) if refs.contains(&r.original)))
65 .map(ExpandedRef::from)
66 .collect();
67
68 let outputs: Vec<ExpandedRef> = resolved_refs
69 .iter()
70 .filter(|r| matches!(packet.fields.get("out"), Some(FieldValue::List(refs)) if refs.contains(&r.original)))
71 .map(ExpandedRef::from)
72 .collect();
73
74 let do_actions = packet
75 .fields
76 .get("do")
77 .and_then(|v| match v {
78 FieldValue::List(items) => Some(items.clone()),
79 _ => None,
80 })
81 .unwrap_or_default();
82
83 let deny_actions = packet
84 .fields
85 .get("deny")
86 .and_then(|v| match v {
87 FieldValue::List(items) => Some(items.clone()),
88 _ => None,
89 })
90 .unwrap_or_default();
91
92 let note = packet.fields.get("note").and_then(|v| match v {
93 FieldValue::Scalar(s) => Some(s.clone()),
94 _ => None,
95 });
96
97 let recommended_executor = workflow_entry
98 .map(|w| w.recommended_executor.clone())
99 .unwrap_or_else(|| "unknown".to_string());
100
101 let fallback_executor = workflow_entry
102 .map(|w| w.fallback_executor.clone())
103 .unwrap_or_else(|| "unknown".to_string());
104
105 let ttl = packet
106 .fields
107 .get("ttl")
108 .and_then(|v| match v {
109 FieldValue::Scalar(s) => Some(s.clone()),
110 _ => None,
111 })
112 .unwrap_or_else(|| "24h".to_string());
113
114 ExpandedRunPacket {
115 spec_version: "1.0".to_string(),
116 run_id: packet.run_id.clone(),
117 workflow: wf_name.to_string(),
118 mode: packet
119 .fields
120 .get("mode")
121 .and_then(|v| match v {
122 FieldValue::Scalar(s) => Some(s.clone()),
123 _ => None,
124 })
125 .unwrap_or_else(|| "unknown".to_string()),
126 inputs,
127 outputs,
128 do_actions,
129 deny_actions,
130 effective_risk: safety.effective_risk.clone(),
131 human_required: safety.human_required,
132 recommended_executor,
133 fallback_executor,
134 ttl,
135 note,
136 }
137}