use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct SchemaHeader {
#[serde(rename = "type")]
pub doc_type: String,
pub version: String,
pub axon_version: String,
}
impl SchemaHeader {
pub fn new(doc_type: &str) -> Self {
SchemaHeader {
doc_type: doc_type.to_string(),
version: "1.0.0".to_string(),
axon_version: crate::runner::AXON_VERSION.to_string(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct PlanExport {
pub _schema: SchemaHeader,
pub source_file: String,
pub backend: String,
pub units: Vec<PlanUnit>,
pub tools: PlanTools,
pub dependencies: PlanDependencies,
pub summary: PlanSummary,
}
#[derive(Debug, Clone, Serialize)]
pub struct PlanUnit {
pub flow_name: String,
pub persona_name: String,
pub context_name: String,
pub effort: String,
pub anchor_count: usize,
pub anchors: Vec<String>,
pub steps: Vec<PlanStep>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PlanStep {
pub name: String,
pub step_type: String,
pub prompt_preview: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_argument: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory_expression: Option<String>,
pub depends_on: Vec<String>,
pub is_root: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct PlanTools {
pub total: usize,
pub builtin: Vec<String>,
pub program: Vec<String>,
pub registered: Vec<PlanToolEntry>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PlanToolEntry {
pub name: String,
pub provider: String,
pub source: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub output_schema: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub effect_row: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PlanDependencies {
pub max_depth: usize,
pub parallel_groups: Vec<Vec<String>>,
pub unresolved_refs: Vec<UnresolvedRef>,
}
#[derive(Debug, Clone, Serialize)]
pub struct UnresolvedRef {
pub step: String,
pub variable: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct PlanSummary {
pub total_units: usize,
pub total_steps: usize,
pub total_anchors: usize,
pub total_tools: usize,
pub has_parallel_steps: bool,
pub has_unresolved_refs: bool,
}
pub struct PlanBuilder;
impl PlanBuilder {
pub fn build(
source_file: &str,
backend: &str,
units: &[PlanUnit],
tools: PlanTools,
deps: PlanDependencies,
) -> PlanExport {
let total_steps: usize = units.iter().map(|u| u.steps.len()).sum();
let total_anchors: usize = units.iter().map(|u| u.anchor_count).sum();
PlanExport {
_schema: SchemaHeader::new("axon.plan"),
source_file: source_file.to_string(),
backend: backend.to_string(),
units: units.to_vec(),
tools: tools.clone(),
dependencies: deps.clone(),
summary: PlanSummary {
total_units: units.len(),
total_steps,
total_anchors,
total_tools: tools.total,
has_parallel_steps: !deps.parallel_groups.is_empty(),
has_unresolved_refs: !deps.unresolved_refs.is_empty(),
},
}
}
pub fn to_json(plan: &PlanExport) -> String {
serde_json::to_string_pretty(plan).unwrap_or_else(|e| {
format!("{{\"error\": \"serialization failed: {e}\"}}")
})
}
}
pub fn jsonb_query(value: &serde_json::Value, path: &str) -> Vec<serde_json::Value> {
let path = path.strip_prefix("$.").unwrap_or(path.strip_prefix('$').unwrap_or(path));
if path.is_empty() {
return vec![value.clone()];
}
let segments = parse_path(path);
let mut current = vec![value.clone()];
for seg in &segments {
let mut next = Vec::new();
for val in ¤t {
match seg {
PathSegment::Field(name) => {
if let Some(v) = val.get(name.as_str()) {
next.push(v.clone());
}
}
PathSegment::Index(idx) => {
if let Some(v) = val.get(*idx) {
next.push(v.clone());
}
}
PathSegment::Wildcard => {
if let Some(arr) = val.as_array() {
next.extend(arr.iter().cloned());
}
}
}
}
current = next;
}
current
}
#[derive(Debug)]
enum PathSegment {
Field(String),
Index(usize),
Wildcard,
}
fn parse_path(path: &str) -> Vec<PathSegment> {
let mut segments = Vec::new();
let mut remaining = path;
while !remaining.is_empty() {
remaining = remaining.strip_prefix('.').unwrap_or(remaining);
if remaining.is_empty() {
break;
}
if let Some(bracket_start) = remaining.find('[') {
let field = &remaining[..bracket_start];
if !field.is_empty() {
segments.push(PathSegment::Field(field.to_string()));
}
if let Some(bracket_end) = remaining[bracket_start..].find(']') {
let inner = &remaining[bracket_start + 1..bracket_start + bracket_end];
if inner == "*" {
segments.push(PathSegment::Wildcard);
} else if let Ok(idx) = inner.parse::<usize>() {
segments.push(PathSegment::Index(idx));
}
remaining = &remaining[bracket_start + bracket_end + 1..];
} else {
break;
}
} else {
let end = remaining.find('.').unwrap_or(remaining.len());
let field = &remaining[..end];
if !field.is_empty() {
segments.push(PathSegment::Field(field.to_string()));
}
remaining = &remaining[end..];
}
}
segments
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn schema_header_defaults() {
let h = SchemaHeader::new("axon.plan");
assert_eq!(h.doc_type, "axon.plan");
assert_eq!(h.version, "1.0.0");
assert!(!h.axon_version.is_empty());
}
#[test]
fn plan_builder_empty() {
let plan = PlanBuilder::build(
"test.axon",
"anthropic",
&[],
PlanTools {
total: 2,
builtin: vec!["Calculator".into(), "DateTimeTool".into()],
program: vec![],
registered: vec![],
},
PlanDependencies {
max_depth: 0,
parallel_groups: vec![],
unresolved_refs: vec![],
},
);
assert_eq!(plan._schema.doc_type, "axon.plan");
assert_eq!(plan.source_file, "test.axon");
assert_eq!(plan.summary.total_units, 0);
assert_eq!(plan.summary.total_steps, 0);
assert!(!plan.summary.has_parallel_steps);
}
#[test]
fn plan_builder_with_units() {
let units = vec![PlanUnit {
flow_name: "Analyze".into(),
persona_name: "Expert".into(),
context_name: "Review".into(),
effort: "high".into(),
anchor_count: 1,
anchors: vec!["NoHallucination".into()],
steps: vec![
PlanStep {
name: "Extract".into(),
step_type: "step".into(),
prompt_preview: "Extract entities".into(),
tool_argument: None,
memory_expression: None,
depends_on: vec![],
is_root: true,
},
PlanStep {
name: "Assess".into(),
step_type: "step".into(),
prompt_preview: "Assess ${Extract}".into(),
tool_argument: None,
memory_expression: None,
depends_on: vec!["Extract".into()],
is_root: false,
},
],
}];
let plan = PlanBuilder::build(
"contract.axon",
"anthropic",
&units,
PlanTools {
total: 2,
builtin: vec!["Calculator".into()],
program: vec![],
registered: vec![],
},
PlanDependencies {
max_depth: 1,
parallel_groups: vec![],
unresolved_refs: vec![],
},
);
assert_eq!(plan.summary.total_units, 1);
assert_eq!(plan.summary.total_steps, 2);
assert_eq!(plan.summary.total_anchors, 1);
}
#[test]
fn plan_serializes_to_json() {
let plan = PlanBuilder::build(
"test.axon",
"anthropic",
&[],
PlanTools { total: 0, builtin: vec![], program: vec![], registered: vec![] },
PlanDependencies { max_depth: 0, parallel_groups: vec![], unresolved_refs: vec![] },
);
let json = PlanBuilder::to_json(&plan);
assert!(json.contains("\"_schema\""));
assert!(json.contains("\"axon.plan\""));
assert!(json.contains("\"version\""));
}
#[test]
fn plan_json_has_schema_header() {
let plan = PlanBuilder::build(
"test.axon",
"anthropic",
&[],
PlanTools { total: 0, builtin: vec![], program: vec![], registered: vec![] },
PlanDependencies { max_depth: 0, parallel_groups: vec![], unresolved_refs: vec![] },
);
let json = PlanBuilder::to_json(&plan);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["_schema"]["type"], "axon.plan");
assert_eq!(parsed["_schema"]["version"], "1.0.0");
assert!(parsed["_schema"]["axon_version"].is_string());
}
#[test]
fn jsonb_query_simple_field() {
let val: serde_json::Value = serde_json::json!({"name": "test", "version": 1});
let results = jsonb_query(&val, "$.name");
assert_eq!(results.len(), 1);
assert_eq!(results[0], "test");
}
#[test]
fn jsonb_query_nested_field() {
let val: serde_json::Value = serde_json::json!({"a": {"b": {"c": 42}}});
let results = jsonb_query(&val, "$.a.b.c");
assert_eq!(results.len(), 1);
assert_eq!(results[0], 42);
}
#[test]
fn jsonb_query_array_index() {
let val: serde_json::Value = serde_json::json!({"items": [10, 20, 30]});
let results = jsonb_query(&val, "$.items[1]");
assert_eq!(results.len(), 1);
assert_eq!(results[0], 20);
}
#[test]
fn jsonb_query_wildcard() {
let val: serde_json::Value = serde_json::json!({"units": [
{"flow_name": "A"},
{"flow_name": "B"},
]});
let results = jsonb_query(&val, "$.units[*].flow_name");
assert_eq!(results.len(), 2);
assert_eq!(results[0], "A");
assert_eq!(results[1], "B");
}
#[test]
fn jsonb_query_missing_field() {
let val: serde_json::Value = serde_json::json!({"name": "test"});
let results = jsonb_query(&val, "$.nonexistent");
assert!(results.is_empty());
}
#[test]
fn jsonb_query_root() {
let val: serde_json::Value = serde_json::json!(42);
let results = jsonb_query(&val, "$");
assert_eq!(results.len(), 1);
assert_eq!(results[0], 42);
}
#[test]
fn jsonb_query_nested_wildcard() {
let val: serde_json::Value = serde_json::json!({
"units": [
{"steps": [{"name": "A"}, {"name": "B"}]},
{"steps": [{"name": "C"}]},
]
});
let results = jsonb_query(&val, "$.units[*].steps[*].name");
assert_eq!(results.len(), 3);
assert_eq!(results[0], "A");
assert_eq!(results[1], "B");
assert_eq!(results[2], "C");
}
}