Skip to main content

warble_claude_code/emit/
resolution.rs

1//! Capability resolution against a target profile (`resolve_node_capabilities` /
2//! `resolve_node_with_shared_binding`) and the stderr resolution summary
3//! (`print_resolution_summary`).
4
5use crate::error::DispatchError;
6use crate::ir::ComponentNode;
7use crate::resolve::{resolve_capabilities, ResolutionReport};
8use crate::targets::{is_known_target, known_target_names, TargetId};
9
10// --- capability resolution + summary --------------------------------------------------------------
11
12fn format_summary_line(entry: &crate::resolve::ResolvedCapability) -> String {
13    let outcome = serde_json::to_value(entry.outcome)
14        .ok()
15        .and_then(|v| v.as_str().map(str::to_string))
16        .unwrap_or_default();
17    let provided = serde_json::to_value(entry.provided_by)
18        .ok()
19        .and_then(|v| v.as_str().map(str::to_string))
20        .unwrap_or_default();
21    let criticality = serde_json::to_value(entry.criticality)
22        .ok()
23        .and_then(|v| v.as_str().map(str::to_string))
24        .unwrap_or_default();
25    let suffix = entry
26        .note
27        .as_ref()
28        .map(|n| format!(" — {n}"))
29        .unwrap_or_default();
30    format!(
31        "  {:<28} {:<12} ({provided}, {criticality}){suffix}",
32        entry.capability, outcome
33    )
34}
35
36pub(super) fn print_resolution_summary(target_label: &str, report: &ResolutionReport) {
37    eprintln!("warble-dispatch: capability resolution for target '{target_label}':");
38    for entry in report {
39        eprintln!("{}", format_summary_line(entry));
40    }
41}
42
43/// Resolve one node's required capabilities against `target_id`, erroring on any `fail` outcome
44/// (no silent degradation). Callers must not emit files when this errors.
45pub fn resolve_node_capabilities(
46    node: &ComponentNode,
47    target_id: &str,
48) -> Result<ResolutionReport, DispatchError> {
49    if !is_known_target(target_id) {
50        return Err(DispatchError(format!(
51            "target '{target_id}' has no capability profile (known targets: {})",
52            known_target_names().join(", ")
53        )));
54    }
55    let profile = TargetId::parse(target_id)
56        .expect("known target parses")
57        .profile();
58    resolve_capabilities(node, target_id, &profile)
59}
60
61/// Resolve a node using the IR's shared top-level fine-grained binding. The compiler emits the
62/// `resolved` block once at `ir.context_binding` (a single coarse binding is shared by every mounted
63/// component — ir-schema §v0.3), so a per-node `context_binding` is coarse. Binding-shape capability
64/// resolution (`blast_radius`) reads the node's own `context_binding.resolved`, so mirror the shared
65/// top-level `resolved` onto the node before resolving. Dispatcher-side hydration only — this is why
66/// `core` can keep `resolved` at the top level (it is never edited here; hard invariant: `core/`
67/// stays untouched).
68pub(super) fn resolve_node_with_shared_binding(
69    node: &ComponentNode,
70    top: &crate::ir::ContextBinding,
71    target_id: &str,
72    profile: &crate::targets::CapabilityProfile,
73) -> Result<ResolutionReport, DispatchError> {
74    if node.context_binding.resolved.is_some() || top.resolved.is_none() {
75        return crate::resolve::resolve_capabilities(node, target_id, profile);
76    }
77    let mut hydrated = node.clone();
78    hydrated.context_binding.resolved = top.resolved.clone();
79    crate::resolve::resolve_capabilities(&hydrated, target_id, profile)
80}