1use serde::Serialize;
22
23#[derive(Debug, Clone, Serialize)]
27pub struct SchemaHeader {
28 #[serde(rename = "type")]
30 pub doc_type: String,
31 pub version: String,
33 pub axon_version: String,
35}
36
37impl SchemaHeader {
38 pub fn new(doc_type: &str) -> Self {
39 SchemaHeader {
40 doc_type: doc_type.to_string(),
41 version: "1.0.0".to_string(),
42 axon_version: crate::runner::AXON_VERSION.to_string(),
43 }
44 }
45}
46
47#[derive(Debug, Clone, Serialize)]
51pub struct PlanExport {
52 pub _schema: SchemaHeader,
53 pub source_file: String,
54 pub backend: String,
55 pub units: Vec<PlanUnit>,
56 pub tools: PlanTools,
57 pub dependencies: PlanDependencies,
58 pub summary: PlanSummary,
59}
60
61#[derive(Debug, Clone, Serialize)]
63pub struct PlanUnit {
64 pub flow_name: String,
65 pub persona_name: String,
66 pub context_name: String,
67 pub effort: String,
68 pub anchor_count: usize,
69 pub anchors: Vec<String>,
70 pub steps: Vec<PlanStep>,
71}
72
73#[derive(Debug, Clone, Serialize)]
75pub struct PlanStep {
76 pub name: String,
77 pub step_type: String,
78 pub prompt_preview: String,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub tool_argument: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub memory_expression: Option<String>,
83 pub depends_on: Vec<String>,
84 pub is_root: bool,
85}
86
87#[derive(Debug, Clone, Serialize)]
89pub struct PlanTools {
90 pub total: usize,
91 pub builtin: Vec<String>,
92 pub program: Vec<String>,
93 pub registered: Vec<PlanToolEntry>,
94}
95
96#[derive(Debug, Clone, Serialize)]
98pub struct PlanToolEntry {
99 pub name: String,
100 pub provider: String,
101 pub source: String,
102 #[serde(skip_serializing_if = "String::is_empty")]
103 pub output_schema: String,
104 #[serde(skip_serializing_if = "Vec::is_empty")]
105 pub effect_row: Vec<String>,
106}
107
108#[derive(Debug, Clone, Serialize)]
110pub struct PlanDependencies {
111 pub max_depth: usize,
112 pub parallel_groups: Vec<Vec<String>>,
113 pub unresolved_refs: Vec<UnresolvedRef>,
114}
115
116#[derive(Debug, Clone, Serialize)]
118pub struct UnresolvedRef {
119 pub step: String,
120 pub variable: String,
121}
122
123#[derive(Debug, Clone, Serialize)]
125pub struct PlanSummary {
126 pub total_units: usize,
127 pub total_steps: usize,
128 pub total_anchors: usize,
129 pub total_tools: usize,
130 pub has_parallel_steps: bool,
131 pub has_unresolved_refs: bool,
132}
133
134pub struct PlanBuilder;
138
139impl PlanBuilder {
140 pub fn build(
142 source_file: &str,
143 backend: &str,
144 units: &[PlanUnit],
145 tools: PlanTools,
146 deps: PlanDependencies,
147 ) -> PlanExport {
148 let total_steps: usize = units.iter().map(|u| u.steps.len()).sum();
149 let total_anchors: usize = units.iter().map(|u| u.anchor_count).sum();
150
151 PlanExport {
152 _schema: SchemaHeader::new("axon.plan"),
153 source_file: source_file.to_string(),
154 backend: backend.to_string(),
155 units: units.to_vec(),
156 tools: tools.clone(),
157 dependencies: deps.clone(),
158 summary: PlanSummary {
159 total_units: units.len(),
160 total_steps,
161 total_anchors,
162 total_tools: tools.total,
163 has_parallel_steps: !deps.parallel_groups.is_empty(),
164 has_unresolved_refs: !deps.unresolved_refs.is_empty(),
165 },
166 }
167 }
168
169 pub fn to_json(plan: &PlanExport) -> String {
171 serde_json::to_string_pretty(plan).unwrap_or_else(|e| {
172 format!("{{\"error\": \"serialization failed: {e}\"}}")
173 })
174 }
175}
176
177pub fn jsonb_query(value: &serde_json::Value, path: &str) -> Vec<serde_json::Value> {
189 let path = path.strip_prefix("$.").unwrap_or(path.strip_prefix('$').unwrap_or(path));
190 if path.is_empty() {
191 return vec![value.clone()];
192 }
193
194 let segments = parse_path(path);
195 let mut current = vec![value.clone()];
196
197 for seg in &segments {
198 let mut next = Vec::new();
199 for val in ¤t {
200 match seg {
201 PathSegment::Field(name) => {
202 if let Some(v) = val.get(name.as_str()) {
203 next.push(v.clone());
204 }
205 }
206 PathSegment::Index(idx) => {
207 if let Some(v) = val.get(*idx) {
208 next.push(v.clone());
209 }
210 }
211 PathSegment::Wildcard => {
212 if let Some(arr) = val.as_array() {
213 next.extend(arr.iter().cloned());
214 }
215 }
216 }
217 }
218 current = next;
219 }
220
221 current
222}
223
224#[derive(Debug)]
225enum PathSegment {
226 Field(String),
227 Index(usize),
228 Wildcard,
229}
230
231fn parse_path(path: &str) -> Vec<PathSegment> {
232 let mut segments = Vec::new();
233 let mut remaining = path;
234
235 while !remaining.is_empty() {
236 remaining = remaining.strip_prefix('.').unwrap_or(remaining);
238 if remaining.is_empty() {
239 break;
240 }
241
242 if let Some(bracket_start) = remaining.find('[') {
244 let field = &remaining[..bracket_start];
246 if !field.is_empty() {
247 segments.push(PathSegment::Field(field.to_string()));
248 }
249
250 if let Some(bracket_end) = remaining[bracket_start..].find(']') {
252 let inner = &remaining[bracket_start + 1..bracket_start + bracket_end];
253 if inner == "*" {
254 segments.push(PathSegment::Wildcard);
255 } else if let Ok(idx) = inner.parse::<usize>() {
256 segments.push(PathSegment::Index(idx));
257 }
258 remaining = &remaining[bracket_start + bracket_end + 1..];
259 } else {
260 break;
261 }
262 } else {
263 let end = remaining.find('.').unwrap_or(remaining.len());
265 let field = &remaining[..end];
266 if !field.is_empty() {
267 segments.push(PathSegment::Field(field.to_string()));
268 }
269 remaining = &remaining[end..];
270 }
271 }
272
273 segments
274}
275
276#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn schema_header_defaults() {
284 let h = SchemaHeader::new("axon.plan");
285 assert_eq!(h.doc_type, "axon.plan");
286 assert_eq!(h.version, "1.0.0");
287 assert!(!h.axon_version.is_empty());
288 }
289
290 #[test]
291 fn plan_builder_empty() {
292 let plan = PlanBuilder::build(
293 "test.axon",
294 "anthropic",
295 &[],
296 PlanTools {
297 total: 2,
298 builtin: vec!["Calculator".into(), "DateTimeTool".into()],
299 program: vec![],
300 registered: vec![],
301 },
302 PlanDependencies {
303 max_depth: 0,
304 parallel_groups: vec![],
305 unresolved_refs: vec![],
306 },
307 );
308
309 assert_eq!(plan._schema.doc_type, "axon.plan");
310 assert_eq!(plan.source_file, "test.axon");
311 assert_eq!(plan.summary.total_units, 0);
312 assert_eq!(plan.summary.total_steps, 0);
313 assert!(!plan.summary.has_parallel_steps);
314 }
315
316 #[test]
317 fn plan_builder_with_units() {
318 let units = vec![PlanUnit {
319 flow_name: "Analyze".into(),
320 persona_name: "Expert".into(),
321 context_name: "Review".into(),
322 effort: "high".into(),
323 anchor_count: 1,
324 anchors: vec!["NoHallucination".into()],
325 steps: vec![
326 PlanStep {
327 name: "Extract".into(),
328 step_type: "step".into(),
329 prompt_preview: "Extract entities".into(),
330 tool_argument: None,
331 memory_expression: None,
332 depends_on: vec![],
333 is_root: true,
334 },
335 PlanStep {
336 name: "Assess".into(),
337 step_type: "step".into(),
338 prompt_preview: "Assess ${Extract}".into(),
339 tool_argument: None,
340 memory_expression: None,
341 depends_on: vec!["Extract".into()],
342 is_root: false,
343 },
344 ],
345 }];
346
347 let plan = PlanBuilder::build(
348 "contract.axon",
349 "anthropic",
350 &units,
351 PlanTools {
352 total: 2,
353 builtin: vec!["Calculator".into()],
354 program: vec![],
355 registered: vec![],
356 },
357 PlanDependencies {
358 max_depth: 1,
359 parallel_groups: vec![],
360 unresolved_refs: vec![],
361 },
362 );
363
364 assert_eq!(plan.summary.total_units, 1);
365 assert_eq!(plan.summary.total_steps, 2);
366 assert_eq!(plan.summary.total_anchors, 1);
367 }
368
369 #[test]
370 fn plan_serializes_to_json() {
371 let plan = PlanBuilder::build(
372 "test.axon",
373 "anthropic",
374 &[],
375 PlanTools { total: 0, builtin: vec![], program: vec![], registered: vec![] },
376 PlanDependencies { max_depth: 0, parallel_groups: vec![], unresolved_refs: vec![] },
377 );
378 let json = PlanBuilder::to_json(&plan);
379 assert!(json.contains("\"_schema\""));
380 assert!(json.contains("\"axon.plan\""));
381 assert!(json.contains("\"version\""));
382 }
383
384 #[test]
385 fn plan_json_has_schema_header() {
386 let plan = PlanBuilder::build(
387 "test.axon",
388 "anthropic",
389 &[],
390 PlanTools { total: 0, builtin: vec![], program: vec![], registered: vec![] },
391 PlanDependencies { max_depth: 0, parallel_groups: vec![], unresolved_refs: vec![] },
392 );
393 let json = PlanBuilder::to_json(&plan);
394 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
395
396 assert_eq!(parsed["_schema"]["type"], "axon.plan");
397 assert_eq!(parsed["_schema"]["version"], "1.0.0");
398 assert!(parsed["_schema"]["axon_version"].is_string());
399 }
400
401 #[test]
404 fn jsonb_query_simple_field() {
405 let val: serde_json::Value = serde_json::json!({"name": "test", "version": 1});
406 let results = jsonb_query(&val, "$.name");
407 assert_eq!(results.len(), 1);
408 assert_eq!(results[0], "test");
409 }
410
411 #[test]
412 fn jsonb_query_nested_field() {
413 let val: serde_json::Value = serde_json::json!({"a": {"b": {"c": 42}}});
414 let results = jsonb_query(&val, "$.a.b.c");
415 assert_eq!(results.len(), 1);
416 assert_eq!(results[0], 42);
417 }
418
419 #[test]
420 fn jsonb_query_array_index() {
421 let val: serde_json::Value = serde_json::json!({"items": [10, 20, 30]});
422 let results = jsonb_query(&val, "$.items[1]");
423 assert_eq!(results.len(), 1);
424 assert_eq!(results[0], 20);
425 }
426
427 #[test]
428 fn jsonb_query_wildcard() {
429 let val: serde_json::Value = serde_json::json!({"units": [
430 {"flow_name": "A"},
431 {"flow_name": "B"},
432 ]});
433 let results = jsonb_query(&val, "$.units[*].flow_name");
434 assert_eq!(results.len(), 2);
435 assert_eq!(results[0], "A");
436 assert_eq!(results[1], "B");
437 }
438
439 #[test]
440 fn jsonb_query_missing_field() {
441 let val: serde_json::Value = serde_json::json!({"name": "test"});
442 let results = jsonb_query(&val, "$.nonexistent");
443 assert!(results.is_empty());
444 }
445
446 #[test]
447 fn jsonb_query_root() {
448 let val: serde_json::Value = serde_json::json!(42);
449 let results = jsonb_query(&val, "$");
450 assert_eq!(results.len(), 1);
451 assert_eq!(results[0], 42);
452 }
453
454 #[test]
455 fn jsonb_query_nested_wildcard() {
456 let val: serde_json::Value = serde_json::json!({
457 "units": [
458 {"steps": [{"name": "A"}, {"name": "B"}]},
459 {"steps": [{"name": "C"}]},
460 ]
461 });
462 let results = jsonb_query(&val, "$.units[*].steps[*].name");
463 assert_eq!(results.len(), 3);
464 assert_eq!(results[0], "A");
465 assert_eq!(results[1], "B");
466 assert_eq!(results[2], "C");
467 }
468}