daedalus_runtime/
graph_builder.rs

1use daedalus_data::model::Value;
2use daedalus_planner::{ComputeAffinity, Edge, Graph, NodeInstance, NodeRef, PortRef};
3use daedalus_registry::{ids::NodeId, store::Registry};
4use crate::handles::{NodeHandleLike, PortHandle};
5use crate::host_bridge::HOST_BRIDGE_META_KEY;
6use std::collections::{BTreeMap, HashMap};
7
8use crate::host_bridge::HOST_BRIDGE_ID;
9
10/// Convenience wrapper so callers can pre-prefix ids (e.g. via a plugin helper)
11/// and pass them into `GraphBuilder::node_spec`.
12#[derive(Clone, Debug)]
13pub struct NodeSpec {
14    pub id: String,
15}
16
17impl NodeSpec {
18    pub fn new(id: impl Into<String>) -> Self {
19        Self { id: id.into() }
20    }
21
22    pub fn prefixed(prefix: &str, id: &str) -> Self {
23        Self {
24            id: format!("{prefix}:{id}"),
25        }
26    }
27}
28
29impl From<(String, String)> for NodeSpec {
30    fn from(value: (String, String)) -> Self {
31        NodeSpec { id: value.0 }
32    }
33}
34
35impl<'a> From<(&'a str, &'a str)> for NodeSpec {
36    fn from(value: (&'a str, &'a str)) -> Self {
37        NodeSpec {
38            id: value.0.to_string(),
39        }
40    }
41}
42
43/// Internal representation of a port reference, from either strings or handles.
44#[derive(Clone, Debug)]
45pub struct PortSpec {
46    pub node: String,
47    pub port: String,
48}
49
50pub trait IntoPortSpec {
51    fn into_spec(self) -> PortSpec;
52}
53
54impl IntoPortSpec for &str {
55    fn into_spec(self) -> PortSpec {
56        let mut parts = self.split(':');
57        PortSpec {
58            node: parts.next().unwrap_or("").to_string(),
59            port: parts.next().unwrap_or("").to_string(),
60        }
61    }
62}
63
64impl IntoPortSpec for (&str, &str) {
65    fn into_spec(self) -> PortSpec {
66        PortSpec {
67            node: self.0.to_string(),
68            port: self.1.to_string(),
69        }
70    }
71}
72
73impl IntoPortSpec for (String, String) {
74    fn into_spec(self) -> PortSpec {
75        PortSpec {
76            node: self.0,
77            port: self.1,
78        }
79    }
80}
81
82impl IntoPortSpec for &PortHandle {
83    fn into_spec(self) -> PortSpec {
84        PortSpec {
85            node: self.node_alias.clone(),
86            port: self.port.clone(),
87        }
88    }
89}
90
91fn is_host_bridge(node: &NodeInstance) -> bool {
92    matches!(
93        node.metadata.get(HOST_BRIDGE_META_KEY),
94        Some(Value::Bool(true))
95    )
96}
97
98/// A graph paired with the host-bridge alias used to expose its inputs/outputs.
99#[derive(Clone, Debug)]
100pub struct NestedGraph {
101    graph: Graph,
102    host_alias: String,
103    host_index: usize,
104}
105
106impl NestedGraph {
107    /// Build a nested graph using the provided host-bridge alias.
108    pub fn new(graph: Graph, host_alias: impl Into<String>) -> Result<Self, &'static str> {
109        let host_alias = host_alias.into();
110        let host_index = graph
111            .nodes
112            .iter()
113            .position(|n| is_host_bridge(n) && n.label.as_deref() == Some(host_alias.as_str()))
114            .ok_or("host bridge alias not found in nested graph")?;
115
116        Ok(Self {
117            graph,
118            host_alias,
119            host_index,
120        })
121    }
122
123    /// Build a nested graph using the first host bridge found (by metadata flag).
124    pub fn first_host(graph: Graph) -> Result<Self, &'static str> {
125        let (host_index, host_alias) = graph
126            .nodes
127            .iter()
128            .enumerate()
129            .find_map(|(idx, n)| {
130                is_host_bridge(n).then(|| (idx, n.label.clone().unwrap_or_else(|| n.id.0.clone())))
131            })
132            .ok_or("nested graph missing host bridge")?;
133
134        Ok(Self {
135            graph,
136            host_alias,
137            host_index,
138        })
139    }
140
141    pub fn host_alias(&self) -> &str {
142        &self.host_alias
143    }
144
145    pub fn graph(&self) -> &Graph {
146        &self.graph
147    }
148}
149
150/// Interface for a nested graph once it has been inlined into another graph.
151#[derive(Clone, Debug)]
152pub struct NestedGraphHandle {
153    pub alias: String,
154    pub inputs: BTreeMap<String, Vec<PortRef>>, // host -> inner targets
155    pub outputs: BTreeMap<String, Vec<PortRef>>, // inner sources -> host
156}
157
158impl NestedGraphHandle {
159    /// Port handle for a nested graph input (outer -> nested).
160    pub fn input(&self, port: impl Into<String>) -> PortHandle {
161        PortHandle::new(self.alias.clone(), port)
162    }
163
164    /// Port handle for a nested graph output (nested -> outer).
165    pub fn output(&self, port: impl Into<String>) -> PortHandle {
166        PortHandle::new(self.alias.clone(), port)
167    }
168
169    pub fn input_ports(&self) -> impl Iterator<Item = &str> {
170        self.inputs.keys().map(|k| k.as_str())
171    }
172
173    pub fn output_ports(&self) -> impl Iterator<Item = &str> {
174        self.outputs.keys().map(|k| k.as_str())
175    }
176}
177
178/// Graph builder with alias and basic port validation using the registry.
179pub struct GraphBuilder<'r> {
180    reg: &'r Registry,
181    nodes: Vec<NodeInstance>,
182    edges: Vec<Edge>,
183    const_overrides: HashMap<String, HashMap<String, Option<Value>>>,
184    node_metadata_overrides: HashMap<String, BTreeMap<String, Value>>,
185    graph_metadata: BTreeMap<String, String>,
186    graph_metadata_values: BTreeMap<String, Value>,
187    injected_node_metadata: BTreeMap<String, Value>,
188    injected_node_metadata_overwrite: BTreeMap<String, Value>,
189    host_bridge_alias: Option<String>,
190    host_bridge_added: bool,
191    nested: HashMap<String, NestedGraphHandle>,
192}
193
194impl<'r> GraphBuilder<'r> {
195    pub fn new(registry: &'r Registry) -> Self {
196        Self {
197            reg: registry,
198            nodes: Vec::new(),
199            edges: Vec::new(),
200            const_overrides: HashMap::new(),
201            node_metadata_overrides: HashMap::new(),
202            graph_metadata: BTreeMap::new(),
203            graph_metadata_values: BTreeMap::new(),
204            injected_node_metadata: BTreeMap::new(),
205            injected_node_metadata_overwrite: BTreeMap::new(),
206            host_bridge_alias: Some("host".to_string()),
207            host_bridge_added: false,
208            nested: HashMap::new(),
209        }
210    }
211
212    /// Attach string graph-level metadata to the built graph (`Graph.metadata`).
213    ///
214    /// For typed graph metadata visible to nodes at runtime, prefer `graph_metadata_value`.
215    pub fn graph_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
216        let key = key.into();
217        let value = value.into();
218        self.graph_metadata.insert(key.clone(), value.clone());
219        self.graph_metadata_values
220            .insert(key, Value::String(value.into()));
221        self
222    }
223
224    /// Attach graph-level metadata (typed `Value`) to the built graph (`Graph.metadata_values`).
225    ///
226    /// Nodes can read this via `ExecutionContext.graph_metadata` at runtime.
227    pub fn graph_metadata_value(mut self, key: impl Into<String>, value: Value) -> Self {
228        self.graph_metadata_values.insert(key.into(), value);
229        self
230    }
231
232    /// Inject a key/value into every node's metadata, without overwriting existing keys.
233    ///
234    /// This is the ergonomic way to "broadcast" host-provided metadata to all nodes so it shows up
235    /// in each node's `ExecutionContext.metadata` at runtime.
236    pub fn inject_node_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
237        self.injected_node_metadata.insert(key.into(), value);
238        self
239    }
240
241    /// Inject a key/value into every node's metadata, overwriting existing keys.
242    pub fn inject_node_metadata_overwrite(mut self, key: impl Into<String>, value: Value) -> Self {
243        self.injected_node_metadata_overwrite
244            .insert(key.into(), value);
245        self
246    }
247
248    /// Add a node via a pre-built spec (useful with plugin helpers).
249    pub fn node_spec(self, spec: NodeSpec, alias: &str) -> Self {
250        self.node_id(&spec.id, alias)
251    }
252
253    /// Add a node from an id/version pair (e.g., produced by a plugin helper).
254    pub fn node_pair<T>(self, pair: T, alias: &str) -> Self
255    where
256        T: Into<NodeSpec>,
257    {
258        let spec: NodeSpec = pair.into();
259        self.node_spec(spec, alias)
260    }
261
262    /// Add a node using any handle that exposes id/alias.
263    pub fn node_handle_like(self, handle: &dyn NodeHandleLike) -> Self {
264        self.node_id(handle.id(), handle.alias())
265    }
266
267    /// Add a node using a typed handle (preferred).
268    pub fn node<H>(self, handle: H) -> Self
269    where
270        H: NodeHandleLike,
271    {
272        self.node_id(handle.id(), handle.alias())
273    }
274
275    /// Add a node by id.
276    pub fn node_from_id(self, id: &str, alias: &str) -> Self {
277        self.node_id(id, alias)
278    }
279
280    /// Add a node using its descriptor default compute affinity (or override via `node_with_compute`).
281    pub fn node_id(mut self, id: &str, alias: &str) -> Self {
282        let view = self.reg.view();
283        let desc = view.nodes.get(&NodeId::new(id));
284        let ports = desc
285            .map(|desc| {
286                (
287                    desc.inputs.iter().map(|p| p.name.clone()).collect(),
288                    desc.outputs.iter().map(|p| p.name.clone()).collect(),
289                )
290            })
291            .unwrap_or_default();
292
293        // Base consts from registry descriptor, if any.
294        let mut const_inputs = Vec::new();
295        let mut compute = ComputeAffinity::CpuOnly;
296        let mut metadata: BTreeMap<String, Value> = BTreeMap::new();
297        let mut sync_groups = Vec::new();
298        if let Some(desc) = desc {
299            compute = desc.default_compute;
300            metadata = desc.metadata.clone();
301            sync_groups = desc.sync_groups.clone();
302            for port in &desc.inputs {
303                if let Some(v) = &port.const_value {
304                    const_inputs.push((port.name.clone(), v.clone()));
305                }
306            }
307        }
308        if let Some(over) = self.const_overrides.get(alias) {
309            for (p, v) in over {
310                match v {
311                    Some(val) => {
312                        const_inputs.retain(|(name, _)| name != p);
313                        const_inputs.push((p.clone(), val.clone()));
314                    }
315                    None => const_inputs.retain(|(name, _)| name != p),
316                }
317            }
318        }
319        if let Some(overrides) = self.node_metadata_overrides.get(alias) {
320            for (k, v) in overrides {
321                metadata.insert(k.clone(), v.clone());
322            }
323        }
324
325        self.nodes.push(NodeInstance {
326            id: daedalus_registry::ids::NodeId::new(id),
327            bundle: Some(id.to_string()),
328            label: Some(alias.to_string()),
329            inputs: ports.0,
330            outputs: ports.1,
331            compute,
332            const_inputs,
333            sync_groups,
334            metadata,
335        });
336        self
337    }
338
339    /// Add a node with an explicit compute affinity override.
340    pub fn node_with_compute(mut self, id: &str, alias: &str, compute: ComputeAffinity) -> Self {
341        self = self.node_id(id, alias);
342        if let Some(last) = self.nodes.last_mut() {
343            last.compute = compute;
344        }
345        self
346    }
347
348    /// Attach sync groups metadata to the most recently added node.
349    pub fn sync_groups(mut self, groups: Vec<daedalus_core::sync::SyncGroup>) -> Self {
350        if let Some(last) = self.nodes.last_mut() {
351            last.sync_groups = groups;
352        }
353        self
354    }
355
356    /// Attach or override metadata for a node handle. Values can store arbitrary UI hints
357    /// (e.g. positions, styles) without changing core types.
358    pub fn node_metadata(
359        self,
360        handle: &impl NodeHandleLike,
361        key: impl Into<String>,
362        value: Value,
363    ) -> Self {
364        self.node_metadata_by_id(handle.alias(), key, value)
365    }
366
367    /// Attach or override metadata for a node alias. Values can store arbitrary UI hints
368    /// (e.g. positions, styles) without changing core types.
369    pub fn node_metadata_by_id(
370        mut self,
371        node_alias: impl Into<String>,
372        key: impl Into<String>,
373        value: Value,
374    ) -> Self {
375        let alias = node_alias.into();
376        let key = key.into();
377        let entry = self
378            .node_metadata_overrides
379            .entry(alias.clone())
380            .or_default();
381        entry.insert(key.clone(), value.clone());
382        if let Some(node) = self
383            .nodes
384            .iter_mut()
385            .find(|n| n.label.as_deref() == Some(alias.as_str()))
386        {
387            node.metadata.insert(key, value);
388        }
389        self
390    }
391
392    /// Bulk metadata helper for a node handle.
393    pub fn node_metadata_map<H, K, I>(self, handle: &H, metadata: I) -> Self
394    where
395        H: NodeHandleLike,
396        K: Into<String>,
397        I: IntoIterator<Item = (K, Value)>,
398    {
399        self.node_metadata_map_by_id(handle.alias(), metadata)
400    }
401
402    /// Bulk metadata helper for a node alias.
403    pub fn node_metadata_map_by_id<K, I>(
404        mut self,
405        node_alias: impl Into<String>,
406        metadata: I,
407    ) -> Self
408    where
409        K: Into<String>,
410        I: IntoIterator<Item = (K, Value)>,
411    {
412        let alias = node_alias.into();
413        for (k, v) in metadata {
414            self = self.node_metadata_by_id(alias.clone(), k.into(), v);
415        }
416        self
417    }
418
419    /// Set or unset a constant value for a node alias/port. None unsets/ignores default.
420    pub fn const_input(mut self, port: &PortHandle, value: Option<Value>) -> Self {
421        let alias = port.node_alias.clone();
422        let port_name = port.port.clone();
423        let entry = self.const_overrides.entry(alias.clone()).or_default();
424        entry.insert(port_name.clone(), value.clone());
425        if let Some(node) = self
426            .nodes
427            .iter_mut()
428            .find(|n| n.label.as_deref() == Some(alias.as_str()))
429        {
430            match value {
431                Some(v) => {
432                    node.const_inputs.retain(|(name, _)| name != &port_name);
433                    node.const_inputs.push((port_name, v));
434                }
435                None => node.const_inputs.retain(|(name, _)| name != &port_name),
436            }
437        }
438        self
439    }
440
441    /// Ensure a host-bridge node exists with the provided alias (one per graph).
442    pub fn host_bridge(mut self, alias: impl Into<String>) -> Self {
443        let alias = alias.into();
444        self.host_bridge_alias = Some(alias.clone());
445        self.ensure_host_bridge(Some(alias))
446    }
447
448    fn ensure_host_bridge(mut self, alias: Option<String>) -> Self {
449        let alias = alias
450            .or_else(|| self.host_bridge_alias.clone())
451            .unwrap_or_else(|| "host".to_string());
452        if self.host_bridge_added {
453            return self;
454        }
455        let exists = self
456            .nodes
457            .iter()
458            .any(|n| n.label.as_deref() == Some(alias.as_str()) || n.id.0 == HOST_BRIDGE_ID);
459        if exists {
460            self.host_bridge_added = true;
461            return self;
462        }
463        self.host_bridge_added = true;
464        self.nodes.push(NodeInstance {
465            id: daedalus_registry::ids::NodeId::new(HOST_BRIDGE_ID),
466            bundle: None,
467            label: Some(alias),
468            inputs: Vec::new(),
469            outputs: Vec::new(),
470            compute: ComputeAffinity::CpuOnly,
471            const_inputs: Vec::new(),
472            sync_groups: Vec::new(),
473            metadata: BTreeMap::from([
474                (HOST_BRIDGE_META_KEY.to_string(), Value::Bool(true)),
475                // Allow arbitrary host ports without registry-declared schemas.
476                // The planner treats `Opaque("generic")` as a type variable and infers
477                // concrete types from graph edges.
478                (
479                    "dynamic_inputs".to_string(),
480                    Value::String(std::borrow::Cow::from("generic")),
481                ),
482                (
483                    "dynamic_outputs".to_string(),
484                    Value::String(std::borrow::Cow::from("generic")),
485                ),
486            ]),
487        });
488        self
489    }
490
491    pub(crate) fn ensure_host_bridge_port(mut self, is_output: bool, port: &str) -> Self {
492        let host_alias = self
493            .host_bridge_alias
494            .clone()
495            .unwrap_or_else(|| "host".to_string());
496        if let Some(host) = self.nodes.iter_mut().find(|n| {
497            is_host_bridge(n)
498                && (n.label.as_deref() == Some(host_alias.as_str()) || n.id.0 == HOST_BRIDGE_ID)
499        }) {
500            let ports = if is_output {
501                &mut host.outputs
502            } else {
503                &mut host.inputs
504            };
505            if !ports.iter().any(|p| p == port) {
506                ports.push(port.to_string());
507            }
508        }
509        self
510    }
511
512    /// Set or unset a constant value by explicit id/port tuple.
513    pub fn const_input_by_id(
514        mut self,
515        node_alias: impl Into<String>,
516        port: impl Into<String>,
517        value: Option<Value>,
518    ) -> Self {
519        let node_alias = node_alias.into();
520        let port = port.into();
521        let entry = self.const_overrides.entry(node_alias.clone()).or_default();
522        entry.insert(port.clone(), value.clone());
523        if let Some(node) = self
524            .nodes
525            .iter_mut()
526            .find(|n| n.label.as_deref() == Some(node_alias.as_str()))
527        {
528            match value {
529                Some(v) => {
530                    node.const_inputs.retain(|(name, _)| name != &port);
531                    node.const_inputs.push((port, v));
532                }
533                None => node.const_inputs.retain(|(name, _)| name != &port),
534            }
535        }
536        self
537    }
538
539    /// Inline another graph by prefixing node labels with `alias` and wiring its host bridge
540    /// to return a handle representing the nested inputs/outputs.
541    pub fn nest(
542        mut self,
543        nested: &NestedGraph,
544        alias: impl Into<String>,
545    ) -> (Self, NestedGraphHandle) {
546        let alias = alias.into();
547        if self.nested.contains_key(&alias)
548            || self
549                .nodes
550                .iter()
551                .any(|n| n.label.as_deref() == Some(alias.as_str()))
552        {
553            panic!("nested alias '{}' already in use", alias);
554        }
555        let prefix = format!("{alias}::");
556        let mut index_map: Vec<Option<usize>> = vec![None; nested.graph.nodes.len()];
557
558        for (idx, node) in nested.graph.nodes.iter().enumerate() {
559            if idx == nested.host_index {
560                continue;
561            }
562            let mut cloned = node.clone();
563            let base_label = cloned.label.clone().unwrap_or_else(|| cloned.id.0.clone());
564            cloned.label = Some(format!("{prefix}{base_label}"));
565            let new_idx = self.nodes.len();
566            self.nodes.push(cloned);
567            index_map[idx] = Some(new_idx);
568        }
569
570        let mut inputs: BTreeMap<String, Vec<PortRef>> = BTreeMap::new();
571        let mut outputs: BTreeMap<String, Vec<PortRef>> = BTreeMap::new();
572
573        for edge in &nested.graph.edges {
574            let from_is_host = edge.from.node.0 == nested.host_index;
575            let to_is_host = edge.to.node.0 == nested.host_index;
576
577            match (from_is_host, to_is_host) {
578                (true, false) => {
579                    if let Some(target_idx) = index_map[edge.to.node.0] {
580                        inputs
581                            .entry(edge.from.port.clone())
582                            .or_default()
583                            .push(PortRef {
584                                node: NodeRef(target_idx),
585                                port: edge.to.port.clone(),
586                            });
587                    }
588                }
589                (false, true) => {
590                    if let Some(source_idx) = index_map[edge.from.node.0] {
591                        outputs
592                            .entry(edge.to.port.clone())
593                            .or_default()
594                            .push(PortRef {
595                                node: NodeRef(source_idx),
596                                port: edge.from.port.clone(),
597                            });
598                    }
599                }
600                (false, false) => {
601                    let Some(from_idx) = index_map[edge.from.node.0] else {
602                        continue;
603                    };
604                    let Some(to_idx) = index_map[edge.to.node.0] else {
605                        continue;
606                    };
607
608                    self.edges.push(Edge {
609                        from: PortRef {
610                            node: NodeRef(from_idx),
611                            port: edge.from.port.clone(),
612                        },
613                        to: PortRef {
614                            node: NodeRef(to_idx),
615                            port: edge.to.port.clone(),
616                        },
617                        metadata: edge.metadata.clone(),
618                    });
619                }
620                (true, true) => {}
621            }
622        }
623
624        let handle = NestedGraphHandle {
625            alias: alias.clone(),
626            inputs,
627            outputs,
628        };
629
630        self.nested.insert(alias.clone(), handle.clone());
631        (self, handle)
632    }
633
634    pub fn connect<F, T>(self, from: F, to: T) -> Self
635    where
636        F: IntoPortSpec,
637        T: IntoPortSpec,
638    {
639        self.connect_ports(from, to)
640    }
641
642    /// Connect two ports and attach metadata to the edge.
643    pub fn connect_with_metadata<F, T, K>(
644        mut self,
645        from: F,
646        to: T,
647        metadata: impl IntoIterator<Item = (K, Value)>,
648    ) -> Self
649    where
650        F: IntoPortSpec,
651        T: IntoPortSpec,
652        K: Into<String>,
653    {
654        let edge_idx = self.edges.len();
655        self = self.connect_ports(from, to);
656        let meta: Vec<(String, Value)> = metadata.into_iter().map(|(k, v)| (k.into(), v)).collect();
657        for e in self.edges.iter_mut().skip(edge_idx) {
658            for (k, v) in &meta {
659                e.metadata.insert(k.clone(), v.clone());
660            }
661        }
662        self
663    }
664
665    /// Attach/override metadata for an existing connection edge.
666    pub fn edge_metadata<F, T>(
667        mut self,
668        from: F,
669        to: T,
670        key: impl Into<String>,
671        value: Value,
672    ) -> Self
673    where
674        F: IntoPortSpec,
675        T: IntoPortSpec,
676    {
677        let from_spec = from.into_spec();
678        let to_spec = to.into_spec();
679        let f_idx = self.find_index(&from_spec.node);
680        let t_idx = self.find_index(&to_spec.node);
681        let key = key.into();
682        for edge in &mut self.edges {
683            if edge.from.node.0 == f_idx
684                && edge.to.node.0 == t_idx
685                && edge.from.port == from_spec.port
686                && edge.to.port == to_spec.port
687            {
688                edge.metadata.insert(key.clone(), value.clone());
689            }
690        }
691        self
692    }
693
694    /// Connect using PortHandle pairs for string-free wiring.
695    pub fn connect_handles(mut self, from: &PortHandle, to: &PortHandle) -> Self {
696        self = self.connect_ports(from, to);
697        self
698    }
699
700    /// Connect by explicit id/port tuples (old-style, but structured).
701    pub fn connect_by_id(
702        mut self,
703        from: (impl Into<String>, impl Into<String>),
704        to: (impl Into<String>, impl Into<String>),
705    ) -> Self {
706        self = self.connect_ports((from.0.into(), from.1.into()), (to.0.into(), to.1.into()));
707        self
708    }
709
710    /// No-op helpers to mirror the desired API shape for host I/O handles.
711    pub fn inputs(self, _ports: &[PortHandle]) -> Self {
712        self
713    }
714
715    pub fn outputs(self, _ports: &[PortHandle]) -> Self {
716        self
717    }
718
719    /// Connect using explicit tuple arguments instead of a colon string, e.g.
720    /// `.connect_ports(("src", "out"), ("dst", "inp"))`.
721    pub fn connect_ports<F, T>(mut self, from: F, to: T) -> Self
722    where
723        F: IntoPortSpec,
724        T: IntoPortSpec,
725    {
726        let from_spec = from.into_spec();
727        let to_spec = to.into_spec();
728        let host_alias = self
729            .host_bridge_alias
730            .clone()
731            .unwrap_or_else(|| "host".to_string());
732        if from_spec.node == host_alias || to_spec.node == host_alias {
733            self = self.ensure_host_bridge(Some(host_alias.clone()));
734        }
735        if from_spec.node == host_alias {
736            self = self.ensure_host_bridge_port(true, &from_spec.port);
737        }
738        if to_spec.node == host_alias {
739            self = self.ensure_host_bridge_port(false, &to_spec.port);
740        }
741        let from_nested = self.nested.get(&from_spec.node).cloned();
742        let to_nested = self.nested.get(&to_spec.node).cloned();
743
744        if from_nested.is_some() && to_nested.is_some() {
745            panic!(
746                "cannot connect nested graph '{}' directly to nested graph '{}'",
747                from_spec.node, to_spec.node
748            );
749        }
750        if let Some(nested) = from_nested {
751            return self.connect_from_nested_spec(&nested, &from_spec.port, to_spec);
752        }
753        if let Some(nested) = to_nested {
754            return self.connect_to_nested_spec(from_spec, &nested, &to_spec.port);
755        }
756
757        let f_idx = self.find_index(&from_spec.node);
758        let t_idx = self.find_index(&to_spec.node);
759        self.edges.push(Edge {
760            from: PortRef {
761                node: NodeRef(f_idx),
762                port: from_spec.port,
763            },
764            to: PortRef {
765                node: NodeRef(t_idx),
766                port: to_spec.port,
767            },
768            metadata: BTreeMap::new(),
769        });
770        self
771    }
772
773    /// Connect an outer node/port to a nested graph input port.
774    pub fn connect_to_nested<F>(
775        mut self,
776        from: F,
777        nested: &NestedGraphHandle,
778        port: impl AsRef<str>,
779    ) -> Self
780    where
781        F: IntoPortSpec,
782    {
783        let from_spec = from.into_spec();
784        let host_alias = self
785            .host_bridge_alias
786            .clone()
787            .unwrap_or_else(|| "host".to_string());
788        if from_spec.node == host_alias {
789            self = self.ensure_host_bridge(Some(host_alias));
790            self = self.ensure_host_bridge_port(true, &from_spec.port);
791        }
792        let lookup = |name: &str, nodes: &[NodeInstance]| {
793            nodes
794                .iter()
795                .position(|n| n.id.0 == name || n.label.as_deref() == Some(name))
796                .unwrap_or_else(|| panic!("node alias '{}' not found", name))
797        };
798        let f_idx = lookup(&from_spec.node, &self.nodes);
799        let port = port.as_ref();
800        let targets = nested
801            .inputs
802            .get(port)
803            .unwrap_or_else(|| panic!("nested input '{}' not found", port));
804
805        for target in targets {
806            self.edges.push(Edge {
807                from: PortRef {
808                    node: NodeRef(f_idx),
809                    port: from_spec.port.clone(),
810                },
811                to: target.clone(),
812                metadata: BTreeMap::new(),
813            });
814        }
815        self
816    }
817
818    /// Connect a nested graph output port to a node/port in the outer graph.
819    pub fn connect_from_nested<T>(
820        mut self,
821        nested: &NestedGraphHandle,
822        port: impl AsRef<str>,
823        to: T,
824    ) -> Self
825    where
826        T: IntoPortSpec,
827    {
828        let to_spec = to.into_spec();
829        let host_alias = self
830            .host_bridge_alias
831            .clone()
832            .unwrap_or_else(|| "host".to_string());
833        if to_spec.node == host_alias {
834            self = self.ensure_host_bridge(Some(host_alias));
835            self = self.ensure_host_bridge_port(false, &to_spec.port);
836        }
837        let lookup = |name: &str, nodes: &[NodeInstance]| {
838            nodes
839                .iter()
840                .position(|n| n.id.0 == name || n.label.as_deref() == Some(name))
841                .unwrap_or_else(|| panic!("node alias '{}' not found", name))
842        };
843        let t_idx = lookup(&to_spec.node, &self.nodes);
844        let port = port.as_ref();
845        let sources = nested
846            .outputs
847            .get(port)
848            .unwrap_or_else(|| panic!("nested output '{}' not found", port));
849
850        for source in sources {
851            self.edges.push(Edge {
852                from: source.clone(),
853                to: PortRef {
854                    node: NodeRef(t_idx),
855                    port: to_spec.port.clone(),
856                },
857                metadata: BTreeMap::new(),
858            });
859        }
860        self
861    }
862
863    fn connect_from_nested_spec(
864        mut self,
865        nested: &NestedGraphHandle,
866        port: &str,
867        to: PortSpec,
868    ) -> Self {
869        let t_idx = self.find_index(&to.node);
870        let sources = nested
871            .outputs
872            .get(port)
873            .unwrap_or_else(|| panic!("nested output '{}' not found", port));
874
875        for source in sources {
876            self.edges.push(Edge {
877                from: source.clone(),
878                to: PortRef {
879                    node: NodeRef(t_idx),
880                    port: to.port.clone(),
881                },
882                metadata: BTreeMap::new(),
883            });
884        }
885        self
886    }
887
888    fn connect_to_nested_spec(
889        mut self,
890        from: PortSpec,
891        nested: &NestedGraphHandle,
892        port: &str,
893    ) -> Self {
894        let f_idx = self.find_index(&from.node);
895        let targets = nested
896            .inputs
897            .get(port)
898            .unwrap_or_else(|| panic!("nested input '{}' not found", port));
899
900        for target in targets {
901            self.edges.push(Edge {
902                from: PortRef {
903                    node: NodeRef(f_idx),
904                    port: from.port.clone(),
905                },
906                to: target.clone(),
907                metadata: BTreeMap::new(),
908            });
909        }
910        self
911    }
912
913    fn find_index(&self, name: &str) -> usize {
914        self.nodes
915            .iter()
916            .position(|n| n.id.0 == name || n.label.as_deref() == Some(name))
917            .unwrap_or_else(|| panic!("node alias '{}' not found", name))
918    }
919
920    pub fn build(self) -> Graph {
921        let mut nodes = self.nodes;
922        if !self.injected_node_metadata.is_empty()
923            || !self.injected_node_metadata_overwrite.is_empty()
924        {
925            for node in &mut nodes {
926                for (k, v) in &self.injected_node_metadata {
927                    node.metadata.entry(k.clone()).or_insert_with(|| v.clone());
928                }
929                for (k, v) in &self.injected_node_metadata_overwrite {
930                    node.metadata.insert(k.clone(), v.clone());
931                }
932            }
933        }
934        Graph {
935            nodes,
936            edges: self.edges,
937            metadata: self.graph_metadata,
938            metadata_values: self.graph_metadata_values,
939        }
940    }
941}
942
943/// Graph definition context for graph-backed nodes.
944pub struct GraphCtx<'r> {
945    builder: GraphBuilder<'r>,
946    host_alias: String,
947    expected_inputs: Vec<String>,
948    expected_outputs: Vec<String>,
949}
950
951impl<'r> GraphCtx<'r> {
952    fn take_builder(&mut self) -> GraphBuilder<'r> {
953        let reg = self.builder.reg;
954        std::mem::replace(&mut self.builder, GraphBuilder::new(reg))
955    }
956
957    pub fn new(registry: &'r Registry, inputs: &[&str], outputs: &[&str]) -> Self {
958        Self {
959            builder: GraphBuilder::new(registry),
960            host_alias: "host".to_string(),
961            expected_inputs: inputs.iter().map(|v| v.to_string()).collect(),
962            expected_outputs: outputs.iter().map(|v| v.to_string()).collect(),
963        }
964    }
965
966    pub fn node(&mut self, id: &str) -> crate::handles::NodeHandle {
967        self.node_as(id, id)
968    }
969
970    pub fn node_as(&mut self, id: &str, alias: &str) -> crate::handles::NodeHandle {
971        let builder = self.take_builder();
972        self.builder = builder.node_id(id, alias);
973        crate::handles::NodeHandle {
974            id: id.to_string(),
975            alias: alias.to_string(),
976        }
977    }
978
979    pub fn connect(&mut self, from: &PortHandle, to: &PortHandle) {
980        let builder = self.take_builder();
981        self.builder = builder.connect_handles(from, to);
982    }
983
984    pub fn const_input(&mut self, port: &PortHandle, value: Value) {
985        let builder = self.take_builder();
986        self.builder = builder.const_input(port, Some(value));
987    }
988
989    pub fn input(&self, name: &str) -> PortHandle {
990        PortHandle::new(self.host_alias.clone(), name)
991    }
992
993    pub fn output(&self, name: &str) -> PortHandle {
994        PortHandle::new(self.host_alias.clone(), name)
995    }
996
997    pub fn bind_output(&mut self, name: &str, from: &PortHandle) {
998        let host = self.output(name);
999        let builder = self.take_builder();
1000        self.builder = builder.connect_handles(from, &host);
1001    }
1002
1003    pub fn build(mut self) -> Graph {
1004        self.builder = self.builder.host_bridge(self.host_alias.clone());
1005        for name in &self.expected_inputs {
1006            self.builder = self.builder.ensure_host_bridge_port(false, name);
1007        }
1008        for name in &self.expected_outputs {
1009            self.builder = self.builder.ensure_host_bridge_port(true, name);
1010        }
1011        self.builder.build()
1012    }
1013}
1014
1015pub fn graph_to_json(graph: &Graph) -> Result<String, serde_json::Error> {
1016    serde_json::to_string(graph)
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021    use super::*;
1022    use daedalus_data::model::{TypeExpr, Value, ValueType};
1023    use daedalus_registry::store::{NodeDescriptorBuilder, Registry};
1024
1025    #[test]
1026    fn applies_metadata_overrides() {
1027        let mut reg = Registry::new();
1028        let desc = NodeDescriptorBuilder::new("demo.node")
1029            .metadata("from_desc", Value::Bool(true))
1030            .build()
1031            .unwrap();
1032        reg.register_node(desc).unwrap();
1033
1034        let graph = GraphBuilder::new(&reg)
1035            .node_from_id("demo.node", "alias")
1036            .node_metadata_by_id("alias", "pos_x", Value::Int(10))
1037            .build();
1038
1039        let meta = &graph.nodes[0].metadata;
1040        assert_eq!(meta.get("from_desc"), Some(&Value::Bool(true)));
1041        assert_eq!(meta.get("pos_x"), Some(&Value::Int(10)));
1042    }
1043
1044    #[test]
1045    fn can_inject_graph_metadata_and_broadcast_to_nodes() {
1046        let mut reg = Registry::new();
1047        let desc = NodeDescriptorBuilder::new("demo.node")
1048            .metadata("existing", Value::String("keep".into()))
1049            .build()
1050            .unwrap();
1051        reg.register_node(desc).unwrap();
1052
1053        let graph = GraphBuilder::new(&reg)
1054            .graph_metadata("graph_run_id", "run-123")
1055            .graph_metadata_value("multiplier", Value::Int(3))
1056            .inject_node_metadata("trace_id", Value::String("trace-abc".into()))
1057            .inject_node_metadata_overwrite("existing", Value::String("overwrite".into()))
1058            .node_from_id("demo.node", "alias")
1059            .build();
1060
1061        assert_eq!(graph.metadata.get("graph_run_id"), Some(&"run-123".into()));
1062        assert_eq!(
1063            graph.metadata_values.get("graph_run_id"),
1064            Some(&Value::String("run-123".into()))
1065        );
1066        assert_eq!(
1067            graph.metadata_values.get("multiplier"),
1068            Some(&Value::Int(3))
1069        );
1070        let meta = &graph.nodes[0].metadata;
1071        assert_eq!(
1072            meta.get("trace_id"),
1073            Some(&Value::String("trace-abc".into()))
1074        );
1075        assert_eq!(
1076            meta.get("existing"),
1077            Some(&Value::String("overwrite".into()))
1078        );
1079    }
1080
1081    #[test]
1082    fn nests_graph_and_exposes_ports() {
1083        let reg = Registry::new();
1084
1085        let inner = GraphBuilder::new(&reg)
1086            .host_bridge("inner")
1087            .node_from_id("demo.add", "add")
1088            .connect_by_id(("inner", "lhs"), ("add", "lhs"))
1089            .connect_by_id(("inner", "rhs"), ("add", "rhs"))
1090            .connect_by_id(("add", "sum"), ("inner", "sum"))
1091            .build();
1092        let nested = NestedGraph::new(inner, "inner").expect("inner host bridge missing");
1093
1094        let (builder, nested_handle) = GraphBuilder::new(&reg)
1095            .node_from_id("demo.src", "src")
1096            .nest(&nested, "adder");
1097
1098        let graph = builder
1099            .node_from_id("demo.sink", "sink")
1100            .connect(("src", "out_lhs"), &nested_handle.input("lhs"))
1101            .connect(("src", "out_rhs"), &nested_handle.input("rhs"))
1102            .connect(&nested_handle.output("sum"), ("sink", "in"))
1103            .build();
1104
1105        assert!(nested_handle.inputs.contains_key("lhs"));
1106        assert!(nested_handle.inputs.contains_key("rhs"));
1107        assert!(nested_handle.outputs.contains_key("sum"));
1108
1109        let find = |name: &str| {
1110            graph
1111                .nodes
1112                .iter()
1113                .position(|n| n.label.as_deref() == Some(name))
1114                .unwrap()
1115        };
1116        let src_idx = find("src");
1117        let sink_idx = find("sink");
1118        let add_idx = find("adder::add");
1119
1120        let has_inbound = graph
1121            .edges
1122            .iter()
1123            .any(|e| e.from.node.0 == src_idx && e.to.node.0 == add_idx && e.to.port == "lhs");
1124        let has_outbound = graph
1125            .edges
1126            .iter()
1127            .any(|e| e.from.node.0 == add_idx && e.to.node.0 == sink_idx && e.from.port == "sum");
1128
1129        assert!(has_inbound, "nested inputs should target inner nodes");
1130        assert!(has_outbound, "nested outputs should feed outer nodes");
1131    }
1132
1133    #[test]
1134    fn applies_edge_metadata() {
1135        let mut reg = Registry::new();
1136        reg.register_node(
1137            NodeDescriptorBuilder::new("demo.src")
1138                .output("out", TypeExpr::Scalar(ValueType::Bool))
1139                .build()
1140                .unwrap(),
1141        )
1142        .unwrap();
1143        reg.register_node(
1144            NodeDescriptorBuilder::new("demo.sink")
1145                .input("in", TypeExpr::Scalar(ValueType::Bool))
1146                .build()
1147                .unwrap(),
1148        )
1149        .unwrap();
1150
1151        let graph = GraphBuilder::new(&reg)
1152            .node_from_id("demo.src", "a")
1153            .node_from_id("demo.sink", "b")
1154            .connect_with_metadata(
1155                ("a", "out"),
1156                ("b", "in"),
1157                [("ui.color", Value::String("red".into()))],
1158            )
1159            .build();
1160        assert_eq!(graph.edges.len(), 1);
1161        assert!(matches!(
1162            graph.edges[0].metadata.get("ui.color"),
1163            Some(Value::String(s)) if s.as_ref() == "red"
1164        ));
1165    }
1166}