Skip to main content

warble_claude_code/
manifest.rs

1//! Capability manifest emitter.
2//!
3//! The manifest is the runtime-agnostic **interop surface** a meta-harness consumes to decide
4//! whether it can call a Warble profile and what it needs — without absorbing execution (see
5//! [`capability-model.md`][spec-cap]). It is a pure projection of the IR: verbs, context, required
6//! capabilities, render contract. Distinct from `resolve.rs`, which links required capabilities
7//! against a *specific* target.
8//!
9//! [spec-cap]: https://github.com/Canner/Warble/blob/main/docs/spec/capability-model.md
10
11use 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    /// Whether this mounted component may be selected as a root entry. A false value keeps the
34    /// component visible for structural inspection without advertising it as independent work.
35    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    /// Declared render block types, or null when the component renders nothing.
45    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    /// Root-first deterministic transitive closure. Internal-only mounts never get an entry row.
59    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
137/// Project a resolved IR into its runtime-agnostic capability manifest.
138pub 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}