warble_claude_code/
manifest.rs1use crate::ir::{ComponentNode, WarbleIr};
12use serde::Serialize;
13use std::collections::{HashMap, HashSet};
14
15pub const MANIFEST_VERSION: &str = "0.3";
16
17#[derive(Debug, Serialize)]
18pub struct RenderContract {
19 pub blocks: Vec<String>,
20}
21
22#[derive(Debug, Serialize)]
23pub struct ManifestContext {
24 pub project: String,
25 pub binding_mode: String,
26 pub precondition: String,
27}
28
29#[derive(Debug, Serialize)]
30pub struct ManifestComponent {
31 pub id: String,
32 pub verb: String,
33 pub entrypoint: bool,
36 #[serde(rename = "type")]
37 pub component_type: String,
38 pub realization_kind: String,
39 pub context: ManifestContext,
40 pub trigger: String,
41 pub outcome: String,
42 pub required_capabilities: Vec<String>,
43 pub dependencies: Vec<ManifestDependency>,
44 pub render_contract: Option<RenderContract>,
46}
47
48#[derive(Debug, Clone, Serialize)]
49pub struct ManifestDependency {
50 pub step: String,
51 pub alias: String,
52 pub component: String,
53}
54
55#[derive(Debug, Serialize)]
56pub struct ManifestEntry {
57 pub id: String,
58 pub closure: Vec<String>,
60}
61
62#[derive(Debug, Serialize)]
63pub struct CapabilityManifest {
64 pub warble_manifest_version: String,
65 pub profile: String,
66 pub entries: Vec<ManifestEntry>,
67 pub components: Vec<ManifestComponent>,
68}
69
70fn manifest_component(node: &ComponentNode) -> ManifestComponent {
71 let block_types: Vec<String> = node
72 .effect
73 .render_blocks
74 .iter()
75 .map(|b| b.block_type.clone())
76 .collect();
77 ManifestComponent {
78 id: node.id.clone(),
79 verb: node.verb.clone(),
80 entrypoint: node.entrypoint,
81 component_type: node.component_type.as_str().to_string(),
82 realization_kind: node.realization_kind.as_str().to_string(),
83 context: ManifestContext {
84 project: node.context_binding.project.clone(),
85 binding_mode: node.context_binding.binding_mode.clone(),
86 precondition: node.precondition_result.status.clone(),
87 },
88 trigger: node.trigger.kind.as_str().to_string(),
89 outcome: node.effect.outcome.kind.as_str().to_string(),
90 required_capabilities: node.required_capabilities.clone(),
91 dependencies: node
92 .llm_calls
93 .iter()
94 .flat_map(|step| {
95 step.component_calls.iter().map(|call| ManifestDependency {
96 step: step.name.clone(),
97 alias: call.alias.clone(),
98 component: call.component.clone(),
99 })
100 })
101 .collect(),
102 render_contract: if block_types.is_empty() {
103 None
104 } else {
105 Some(RenderContract {
106 blocks: block_types,
107 })
108 },
109 }
110}
111
112fn entry_closure(root: &ComponentNode, by_id: &HashMap<&str, &ComponentNode>) -> Vec<String> {
113 fn visit(
114 id: &str,
115 by_id: &HashMap<&str, &ComponentNode>,
116 visited: &mut HashSet<String>,
117 closure: &mut Vec<String>,
118 ) {
119 if !visited.insert(id.to_string()) {
120 return;
121 }
122 closure.push(id.to_string());
123 let Some(node) = by_id.get(id) else { return };
124 for step in &node.llm_calls {
125 for call in &step.component_calls {
126 visit(&call.component, by_id, visited, closure);
127 }
128 }
129 }
130
131 let mut visited = HashSet::new();
132 let mut closure = Vec::new();
133 visit(&root.id, by_id, &mut visited, &mut closure);
134 closure
135}
136
137pub fn build_manifest(ir: &WarbleIr) -> CapabilityManifest {
139 let by_id: HashMap<&str, &ComponentNode> = ir
140 .components
141 .iter()
142 .map(|node| (node.id.as_str(), node))
143 .collect();
144 CapabilityManifest {
145 warble_manifest_version: MANIFEST_VERSION.to_string(),
146 profile: ir.profile.clone(),
147 entries: ir
148 .components
149 .iter()
150 .filter(|node| node.entrypoint)
151 .map(|node| ManifestEntry {
152 id: node.id.clone(),
153 closure: entry_closure(node, &by_id),
154 })
155 .collect(),
156 components: ir.components.iter().map(manifest_component).collect(),
157 }
158}