1use serde::Serialize;
18
19#[derive(Debug, Clone, Serialize)]
23pub struct FlowInspection {
24 pub name: String,
26 pub source_file: String,
28 pub source_hash: String,
30 pub source_lines: usize,
32 pub signature: FlowSignature,
34 pub steps: Vec<StepInfo>,
36 pub edges: Vec<EdgeInfo>,
38 pub execution_levels: Vec<Vec<String>>,
40 pub anchors: Vec<AnchorInfo>,
42 pub tools: Vec<ToolInfo>,
44 pub personas_referenced: Vec<String>,
46 pub compilation: CompilationInfo,
48}
49
50#[derive(Debug, Clone, Serialize)]
52pub struct FlowSignature {
53 pub name: String,
54 pub parameters: Vec<ParameterInfo>,
55 pub return_type: String,
56 pub return_type_optional: bool,
57}
58
59#[derive(Debug, Clone, Serialize)]
61pub struct ParameterInfo {
62 pub name: String,
63 pub type_name: String,
64}
65
66#[derive(Debug, Clone, Serialize)]
68pub struct StepInfo {
69 pub name: String,
70 pub persona_ref: String,
71 pub has_tool_use: bool,
72 pub has_probe: bool,
73 pub has_reason: bool,
74 pub has_weave: bool,
75 pub output_type: String,
76 pub source_line: u32,
77}
78
79#[derive(Debug, Clone, Serialize)]
81pub struct EdgeInfo {
82 pub from: String,
83 pub to: String,
84 pub type_name: String,
85}
86
87#[derive(Debug, Clone, Serialize)]
89pub struct AnchorInfo {
90 pub name: String,
91 pub description: String,
92 pub enforce: String,
93 pub on_violation: String,
94 pub source_line: u32,
95}
96
97#[derive(Debug, Clone, Serialize)]
99pub struct ToolInfo {
100 pub name: String,
101 pub provider: String,
102 pub timeout: String,
103 pub sandbox: Option<bool>,
104 pub source_line: u32,
105}
106
107#[derive(Debug, Clone, Serialize)]
109pub struct CompilationInfo {
110 pub success: bool,
111 pub token_count: usize,
112 pub flow_count: usize,
113 pub anchor_count: usize,
114 pub tool_count: usize,
115 pub type_errors: Vec<String>,
116}
117
118pub fn inspect_flow(
125 flow_name: &str,
126 source: &str,
127 source_file: &str,
128 source_hash: &str,
129) -> Result<FlowInspection, String> {
130 let tokens = crate::lexer::Lexer::new(source, source_file)
132 .tokenize()
133 .map_err(|e| format!("lex error: {e:?}"))?;
134
135 let token_count = tokens.len();
136
137 let mut parser = crate::parser::Parser::new(tokens);
139 let program = parser
140 .parse()
141 .map_err(|e| format!("parse error: {e:?}"))?;
142
143 let type_errors = crate::type_checker::TypeChecker::new(&program).check();
145 let type_error_msgs: Vec<String> = type_errors.iter().map(|e| format!("{e:?}")).collect();
146
147 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
149
150 let ir_flow = ir
152 .flows
153 .iter()
154 .find(|f| f.name == flow_name)
155 .ok_or_else(|| format!("flow '{}' not found in IR (available: {})",
156 flow_name,
157 ir.flows.iter().map(|f| f.name.as_str()).collect::<Vec<_>>().join(", ")))?;
158
159 let signature = FlowSignature {
161 name: ir_flow.name.clone(),
162 parameters: ir_flow
163 .parameters
164 .iter()
165 .map(|p| ParameterInfo {
166 name: p.name.clone(),
167 type_name: p.type_name.clone(),
168 })
169 .collect(),
170 return_type: if ir_flow.return_type_generic.is_empty() {
171 ir_flow.return_type_name.clone()
172 } else {
173 format!("{}<{}>", ir_flow.return_type_name, ir_flow.return_type_generic)
174 },
175 return_type_optional: ir_flow.return_type_optional,
176 };
177
178 let mut steps = Vec::new();
180 let mut personas_set = std::collections::BTreeSet::new();
181
182 for node in &ir_flow.steps {
183 if let crate::ir_nodes::IRFlowNode::Step(step) = node {
184 if !step.persona_ref.is_empty() {
185 personas_set.insert(step.persona_ref.clone());
186 }
187 steps.push(StepInfo {
188 name: step.name.clone(),
189 persona_ref: step.persona_ref.clone(),
190 has_tool_use: step.use_tool.is_some(),
191 has_probe: step.probe.is_some(),
192 has_reason: step.reason.is_some(),
193 has_weave: step.weave.is_some(),
194 output_type: step.output_type.clone(),
195 source_line: step.source_line,
196 });
197 }
198 }
199
200 let edges: Vec<EdgeInfo> = ir_flow
202 .edges
203 .iter()
204 .map(|e| EdgeInfo {
205 from: e.source_step.clone(),
206 to: e.target_step.clone(),
207 type_name: e.type_name.clone(),
208 })
209 .collect();
210
211 let anchors: Vec<AnchorInfo> = ir
213 .anchors
214 .iter()
215 .map(|a| AnchorInfo {
216 name: a.name.clone(),
217 description: a.description.clone(),
218 enforce: a.enforce.clone(),
219 on_violation: a.on_violation.clone(),
220 source_line: a.source_line,
221 })
222 .collect();
223
224 let tools: Vec<ToolInfo> = ir
226 .tools
227 .iter()
228 .map(|t| ToolInfo {
229 name: t.name.clone(),
230 provider: t.provider.clone(),
231 timeout: t.timeout.clone(),
232 sandbox: t.sandbox,
233 source_line: t.source_line,
234 })
235 .collect();
236
237 let source_lines = source.lines().count();
238
239 Ok(FlowInspection {
240 name: flow_name.to_string(),
241 source_file: source_file.to_string(),
242 source_hash: source_hash.to_string(),
243 source_lines,
244 signature,
245 steps,
246 edges,
247 execution_levels: ir_flow.execution_levels.clone(),
248 anchors,
249 tools,
250 personas_referenced: personas_set.into_iter().collect(),
251 compilation: CompilationInfo {
252 success: type_error_msgs.is_empty(),
253 token_count,
254 flow_count: ir.flows.len(),
255 anchor_count: ir.anchors.len(),
256 tool_count: ir.tools.len(),
257 type_errors: type_error_msgs,
258 },
259 })
260}
261
262#[derive(Debug, Clone, Serialize)]
264pub struct FlowSummary {
265 pub name: String,
266 pub source_file: String,
267 pub source_hash: String,
268 pub step_count: usize,
269 pub has_anchors: bool,
270 pub has_tools: bool,
271}
272
273pub fn inspect_all_flows(
275 source: &str,
276 source_file: &str,
277 source_hash: &str,
278) -> Result<Vec<FlowSummary>, String> {
279 let tokens = crate::lexer::Lexer::new(source, source_file)
280 .tokenize()
281 .map_err(|e| format!("lex error: {e:?}"))?;
282
283 let mut parser = crate::parser::Parser::new(tokens);
284 let program = parser.parse().map_err(|e| format!("parse error: {e:?}"))?;
285 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
286
287 let has_anchors = !ir.anchors.is_empty();
288 let has_tools = !ir.tools.is_empty();
289
290 Ok(ir
291 .flows
292 .iter()
293 .map(|f| {
294 let step_count = f
295 .steps
296 .iter()
297 .filter(|n| matches!(n, crate::ir_nodes::IRFlowNode::Step(_)))
298 .count();
299
300 FlowSummary {
301 name: f.name.clone(),
302 source_file: source_file.to_string(),
303 source_hash: source_hash.to_string(),
304 step_count,
305 has_anchors,
306 has_tools,
307 }
308 })
309 .collect())
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
316pub enum GraphFormat {
317 Dot,
318 Mermaid,
319}
320
321impl GraphFormat {
322 pub fn from_str(s: &str) -> Self {
324 match s.to_lowercase().as_str() {
325 "mermaid" => GraphFormat::Mermaid,
326 _ => GraphFormat::Dot,
327 }
328 }
329
330 pub fn content_type(&self) -> &'static str {
331 match self {
332 GraphFormat::Dot => "text/vnd.graphviz",
333 GraphFormat::Mermaid => "text/plain",
334 }
335 }
336}
337
338#[derive(Debug, Clone, Serialize)]
340pub struct GraphExport {
341 pub flow_name: String,
343 pub format: String,
345 pub graph: String,
347 pub node_count: usize,
349 pub edge_count: usize,
351 pub parallel_groups: usize,
353 pub max_depth: usize,
355}
356
357pub fn export_flow_graph(
361 flow_name: &str,
362 source: &str,
363 source_file: &str,
364 format: GraphFormat,
365) -> Result<GraphExport, String> {
366 let tokens = crate::lexer::Lexer::new(source, source_file)
368 .tokenize()
369 .map_err(|e| format!("lex error: {e:?}"))?;
370
371 let mut parser = crate::parser::Parser::new(tokens);
372 let program = parser.parse().map_err(|e| format!("parse error: {e:?}"))?;
373 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
374
375 let graphs = crate::graph_export::graph_from_ir(&ir);
377
378 let (name, graph) = graphs
380 .into_iter()
381 .find(|(n, _)| n == flow_name)
382 .ok_or_else(|| format!("flow '{}' not found in graph analysis", flow_name))?;
383
384 let node_count = graph.steps.len();
385 let edge_count: usize = graph.steps.iter().map(|s| s.depends_on.len()).sum();
386 let parallel_group_count = graph.parallel_groups.len();
387 let max_depth = graph.max_depth;
388
389 let (graph_text, format_str) = match format {
390 GraphFormat::Dot => (crate::graph_export::to_dot(&name, &graph), "dot"),
391 GraphFormat::Mermaid => (crate::graph_export::to_mermaid(&name, &graph), "mermaid"),
392 };
393
394 Ok(GraphExport {
395 flow_name: name,
396 format: format_str.to_string(),
397 graph: graph_text,
398 node_count,
399 edge_count,
400 parallel_groups: parallel_group_count,
401 max_depth,
402 })
403}
404
405#[cfg(test)]
408mod tests {
409 use super::*;
410
411 const SAMPLE_SOURCE: &str = r#"
412persona Analyst {
413 tone: "analytical"
414 domain: ["data", "statistics"]
415}
416
417anchor NoHallucination {
418 description: "Prevent fabrication"
419 require: factual
420 enforce: strict
421 on_violation: retry
422}
423
424tool Calculator {
425 provider: builtin
426 timeout: 5s
427}
428
429flow Analyze(data: Text) -> Report {
430 step gather use Analyst {
431 given: data
432 ask: "Summarize the data"
433 }
434 step conclude use Analyst {
435 given: gather.output
436 ask: "Draw conclusions"
437 }
438}
439"#;
440
441 #[test]
442 fn inspect_flow_basic() {
443 let result = inspect_flow("Analyze", SAMPLE_SOURCE, "test.axon", "abc123");
444 assert!(result.is_ok());
445
446 let inspection = result.unwrap();
447 assert_eq!(inspection.name, "Analyze");
448 assert_eq!(inspection.source_file, "test.axon");
449 assert_eq!(inspection.source_hash, "abc123");
450 assert!(inspection.source_lines > 0);
451
452 assert_eq!(inspection.signature.name, "Analyze");
454 assert_eq!(inspection.signature.parameters.len(), 1);
455 assert_eq!(inspection.signature.parameters[0].name, "data");
456 assert_eq!(inspection.signature.parameters[0].type_name, "Text");
457
458 assert_eq!(inspection.steps.len(), 2);
460 assert_eq!(inspection.steps[0].name, "gather");
461 assert_eq!(inspection.steps[1].name, "conclude");
462
463 assert!(inspection.personas_referenced.contains(&"Analyst".to_string()));
465
466 assert_eq!(inspection.anchors.len(), 1);
468 assert_eq!(inspection.anchors[0].name, "NoHallucination");
469
470 assert_eq!(inspection.tools.len(), 1);
472 assert_eq!(inspection.tools[0].name, "Calculator");
473
474 assert_eq!(inspection.compilation.flow_count, 1);
476 }
477
478 #[test]
479 fn inspect_flow_not_found() {
480 let result = inspect_flow("NonExistent", SAMPLE_SOURCE, "test.axon", "abc");
481 assert!(result.is_err());
482 let err = result.unwrap_err();
483 assert!(err.contains("not found"));
484 }
485
486 #[test]
487 fn inspect_flow_invalid_source() {
488 let result = inspect_flow("X", "this is not valid axon {{{{", "bad.axon", "x");
489 assert!(result.is_err());
490 }
491
492 #[test]
493 fn inspect_all_flows_basic() {
494 let result = inspect_all_flows(SAMPLE_SOURCE, "test.axon", "hash123");
495 assert!(result.is_ok());
496
497 let summaries = result.unwrap();
498 assert_eq!(summaries.len(), 1);
499 assert_eq!(summaries[0].name, "Analyze");
500 assert_eq!(summaries[0].step_count, 2);
501 assert!(summaries[0].has_anchors);
502 assert!(summaries[0].has_tools);
503 }
504
505 #[test]
506 fn flow_inspection_serializable() {
507 let result = inspect_flow("Analyze", SAMPLE_SOURCE, "test.axon", "abc123").unwrap();
508 let json = serde_json::to_value(&result).unwrap();
509 assert_eq!(json["name"], "Analyze");
510 assert!(json["signature"].is_object());
511 assert!(json["steps"].is_array());
512 assert!(json["anchors"].is_array());
513 assert!(json["tools"].is_array());
514 assert!(json["compilation"].is_object());
515 assert!(json["compilation"].is_object());
516 }
517
518 #[test]
519 fn flow_summary_serializable() {
520 let summary = FlowSummary {
521 name: "TestFlow".to_string(),
522 source_file: "test.axon".to_string(),
523 source_hash: "abc".to_string(),
524 step_count: 3,
525 has_anchors: true,
526 has_tools: false,
527 };
528 let json = serde_json::to_value(&summary).unwrap();
529 assert_eq!(json["name"], "TestFlow");
530 assert_eq!(json["step_count"], 3);
531 assert_eq!(json["has_anchors"], true);
532 assert_eq!(json["has_tools"], false);
533 }
534
535 #[test]
536 fn step_info_details() {
537 let result = inspect_flow("Analyze", SAMPLE_SOURCE, "test.axon", "abc").unwrap();
538 let gather = &result.steps[0];
539 assert_eq!(gather.persona_ref, "Analyst");
540 assert!(!gather.has_tool_use);
541 assert!(!gather.has_probe);
542 assert!(!gather.has_reason);
543 assert!(!gather.has_weave);
544 }
545
546 #[test]
547 fn compilation_info_details() {
548 let result = inspect_flow("Analyze", SAMPLE_SOURCE, "test.axon", "abc").unwrap();
549 assert!(result.compilation.token_count > 0);
550 assert_eq!(result.compilation.anchor_count, 1);
551 assert_eq!(result.compilation.tool_count, 1);
552 }
553
554 #[test]
555 fn graph_format_parsing() {
556 assert_eq!(GraphFormat::from_str("dot"), GraphFormat::Dot);
557 assert_eq!(GraphFormat::from_str("mermaid"), GraphFormat::Mermaid);
558 assert_eq!(GraphFormat::from_str("MERMAID"), GraphFormat::Mermaid);
559 assert_eq!(GraphFormat::from_str("unknown"), GraphFormat::Dot); assert_eq!(GraphFormat::Dot.content_type(), "text/vnd.graphviz");
561 assert_eq!(GraphFormat::Mermaid.content_type(), "text/plain");
562 }
563
564 #[test]
565 fn export_flow_graph_dot() {
566 let result = export_flow_graph("Analyze", SAMPLE_SOURCE, "test.axon", GraphFormat::Dot);
567 assert!(result.is_ok());
568 let export = result.unwrap();
569 assert_eq!(export.flow_name, "Analyze");
570 assert_eq!(export.format, "dot");
571 assert!(export.graph.contains("digraph"));
572 assert!(export.graph.contains("gather"));
573 assert!(export.graph.contains("conclude"));
574 assert!(export.node_count >= 2);
575 }
576
577 #[test]
578 fn export_flow_graph_mermaid() {
579 let result = export_flow_graph("Analyze", SAMPLE_SOURCE, "test.axon", GraphFormat::Mermaid);
580 assert!(result.is_ok());
581 let export = result.unwrap();
582 assert_eq!(export.format, "mermaid");
583 assert!(export.graph.contains("graph TD"));
584 assert!(export.graph.contains("gather"));
585 assert!(export.graph.contains("conclude"));
586 }
587
588 #[test]
589 fn export_flow_graph_not_found() {
590 let result = export_flow_graph("NonExistent", SAMPLE_SOURCE, "test.axon", GraphFormat::Dot);
591 assert!(result.is_err());
592 assert!(result.unwrap_err().contains("not found"));
593 }
594
595 #[test]
596 fn graph_export_serializable() {
597 let result = export_flow_graph("Analyze", SAMPLE_SOURCE, "test.axon", GraphFormat::Dot).unwrap();
598 let json = serde_json::to_value(&result).unwrap();
599 assert_eq!(json["flow_name"], "Analyze");
600 assert_eq!(json["format"], "dot");
601 assert!(json["graph"].as_str().unwrap().contains("digraph"));
602 assert!(json["node_count"].as_u64().unwrap() >= 2);
603 }
604}