Skip to main content

jellyflow_runtime/schema/
types.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use super::kit::{NodeKitContentDensity, NodeKitLayoutHints};
5use jellyflow_core::core::{
6    CanvasPoint, CanvasSize, Node, NodeId, NodeKindKey, Port, PortCapacity, PortDirection, PortId,
7    PortKey, PortKind,
8};
9use jellyflow_core::ops::{GraphOp, GraphTransaction};
10use jellyflow_core::types::TypeDesc;
11
12fn port_view_descriptor_is_default(value: &PortViewDescriptor) -> bool {
13    value.is_default()
14}
15
16fn is_false(value: &bool) -> bool {
17    !*value
18}
19
20fn node_surface_layout_budget_is_empty(value: &NodeSurfaceLayoutBudget) -> bool {
21    value.is_empty()
22}
23
24/// Declares a port for a node kind.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct PortDecl {
27    /// Stable schema key for this port.
28    pub key: PortKey,
29    /// Direction.
30    pub dir: PortDirection,
31    /// Kind.
32    pub kind: PortKind,
33    /// Capacity.
34    pub capacity: PortCapacity,
35    /// Optional type descriptor.
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub ty: Option<TypeDesc>,
38    /// UI-facing label.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub label: Option<String>,
41    /// Adapter-facing handle and label presentation metadata.
42    #[serde(default, skip_serializing_if = "port_view_descriptor_is_default")]
43    pub view: PortViewDescriptor,
44}
45
46/// Adapter-facing side hint for a node port handle.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum PortViewSide {
50    Top,
51    Right,
52    Bottom,
53    Left,
54}
55
56impl PortViewSide {
57    pub fn fallback_for_direction(dir: PortDirection) -> Self {
58        match dir {
59            PortDirection::In => Self::Left,
60            PortDirection::Out => Self::Right,
61        }
62    }
63}
64
65/// Adapter-facing visibility hint for a node port handle.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum PortHandleVisibility {
69    Visible,
70    Hidden,
71    Collapsed,
72}
73
74/// Renderer-neutral metadata that helps adapters place and present handles.
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
76pub struct PortViewDescriptor {
77    /// Preferred side for the handle.
78    #[serde(default, skip_serializing_if = "Option::is_none")]
79    pub side: Option<PortViewSide>,
80    /// Deterministic order within side/group.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub order: Option<i32>,
83    /// Optional grouping key for adapters that cluster ports.
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub group: Option<String>,
86    /// Optional adapter anchor id, such as a table field or form row id.
87    #[serde(default, skip_serializing_if = "Option::is_none")]
88    pub anchor: Option<String>,
89    /// Optional lane key inside a node renderer.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub lane: Option<String>,
92    /// Optional slot key inside a lane or anchor.
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub slot: Option<String>,
95    /// Optional label override for adapter handle labels.
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub label: Option<String>,
98    /// Optional adapter icon key.
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub icon_key: Option<String>,
101    /// Optional handle visibility hint.
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub visibility: Option<PortHandleVisibility>,
104}
105
106impl PortViewDescriptor {
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    pub fn side(side: PortViewSide) -> Self {
112        Self {
113            side: Some(side),
114            ..Self::default()
115        }
116    }
117
118    pub fn top() -> Self {
119        Self::side(PortViewSide::Top)
120    }
121
122    pub fn right() -> Self {
123        Self::side(PortViewSide::Right)
124    }
125
126    pub fn bottom() -> Self {
127        Self::side(PortViewSide::Bottom)
128    }
129
130    pub fn left() -> Self {
131        Self::side(PortViewSide::Left)
132    }
133
134    pub fn with_order(mut self, order: i32) -> Self {
135        self.order = Some(order);
136        self
137    }
138
139    pub fn with_group(mut self, group: impl Into<String>) -> Self {
140        self.group = Some(group.into());
141        self
142    }
143
144    pub fn with_anchor(mut self, anchor: impl Into<String>) -> Self {
145        self.anchor = Some(anchor.into());
146        self
147    }
148
149    pub fn with_lane(mut self, lane: impl Into<String>) -> Self {
150        self.lane = Some(lane.into());
151        self
152    }
153
154    pub fn with_slot(mut self, slot: impl Into<String>) -> Self {
155        self.slot = Some(slot.into());
156        self
157    }
158
159    pub fn with_label(mut self, label: impl Into<String>) -> Self {
160        self.label = Some(label.into());
161        self
162    }
163
164    pub fn with_icon_key(mut self, icon_key: impl Into<String>) -> Self {
165        self.icon_key = Some(icon_key.into());
166        self
167    }
168
169    pub fn with_visibility(mut self, visibility: PortHandleVisibility) -> Self {
170        self.visibility = Some(visibility);
171        self
172    }
173
174    pub fn hidden(self) -> Self {
175        self.with_visibility(PortHandleVisibility::Hidden)
176    }
177
178    pub fn collapsed(self) -> Self {
179        self.with_visibility(PortHandleVisibility::Collapsed)
180    }
181
182    pub fn is_visible(&self) -> bool {
183        matches!(self.visibility, None | Some(PortHandleVisibility::Visible))
184    }
185
186    pub fn is_hidden(&self) -> bool {
187        matches!(self.visibility, Some(PortHandleVisibility::Hidden))
188    }
189
190    pub fn is_collapsed(&self) -> bool {
191        matches!(self.visibility, Some(PortHandleVisibility::Collapsed))
192    }
193
194    pub fn is_hidden_or_collapsed(&self) -> bool {
195        !self.is_visible()
196    }
197
198    pub fn resolved_side(&self, dir: PortDirection) -> PortViewSide {
199        self.side
200            .unwrap_or_else(|| PortViewSide::fallback_for_direction(dir))
201    }
202
203    pub fn is_default(&self) -> bool {
204        self == &Self::default()
205    }
206}
207
208/// Renderer-neutral semantic role for a node-local surface slot.
209#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
210#[serde(rename_all = "snake_case")]
211pub enum NodeSurfaceSlotKind {
212    Header,
213    Body,
214    Footer,
215    Badge,
216    Icon,
217    FieldRow,
218    ActionRow,
219    Preview,
220    NestedRegion,
221    StatusBanner,
222    PortRail,
223    ConfigGroup,
224    MetricBadge,
225}
226
227/// Renderer-neutral semantic role for an editable node-local control.
228#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum NodeControlKind {
231    TextInput,
232    NumberInput,
233    Select,
234    MultiSelect,
235    Toggle,
236    Code,
237    Color,
238    Asset,
239    VariablePicker,
240    Expression,
241    TextArea,
242    Slider,
243    PortBinding,
244}
245
246/// Data-oriented binding target for a node-local control.
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248pub struct NodeControlBinding {
249    pub source: NodeControlBindingSource,
250    pub path: String,
251}
252
253/// Semantic source namespace for a control binding.
254#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
255#[serde(rename_all = "snake_case")]
256pub enum NodeControlBindingSource {
257    DataPath,
258    Slot,
259    JsonPointer,
260    GraphSymbol,
261    PortAnchor,
262}
263
264impl NodeControlBinding {
265    pub fn new(source: NodeControlBindingSource, path: impl Into<String>) -> Self {
266        Self {
267            source,
268            path: path.into(),
269        }
270    }
271
272    pub fn data_path(path: impl Into<String>) -> Self {
273        Self::new(NodeControlBindingSource::DataPath, path)
274    }
275
276    pub fn slot(path: impl Into<String>) -> Self {
277        Self::new(NodeControlBindingSource::Slot, path)
278    }
279
280    pub fn json_pointer(path: impl Into<String>) -> Self {
281        Self::new(NodeControlBindingSource::JsonPointer, path)
282    }
283
284    pub fn graph_symbol(path: impl Into<String>) -> Self {
285        Self::new(NodeControlBindingSource::GraphSymbol, path)
286    }
287
288    pub fn port_anchor(path: impl Into<String>) -> Self {
289        Self::new(NodeControlBindingSource::PortAnchor, path)
290    }
291}
292
293/// Inline selectable value for select-like controls.
294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
295pub struct NodeControlOption {
296    pub value: Value,
297    pub label: String,
298    #[serde(default, skip_serializing_if = "Option::is_none")]
299    pub icon_key: Option<String>,
300    #[serde(default, skip_serializing_if = "is_false")]
301    pub disabled: bool,
302}
303
304impl NodeControlOption {
305    pub fn new(value: impl Into<Value>, label: impl Into<String>) -> Self {
306        Self {
307            value: value.into(),
308            label: label.into(),
309            icon_key: None,
310            disabled: false,
311        }
312    }
313
314    pub fn with_icon_key(mut self, icon_key: impl Into<String>) -> Self {
315        self.icon_key = Some(icon_key.into());
316        self
317    }
318
319    pub fn disabled(mut self) -> Self {
320        self.disabled = true;
321        self
322    }
323}
324
325/// Renderer-neutral option source for controls whose choices are adapter or graph supplied.
326#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(rename_all = "snake_case", tag = "kind", content = "key")]
328pub enum NodeControlOptionSource {
329    #[default]
330    Inline,
331    Variables,
332    Assets,
333    Ports,
334    Custom(String),
335}
336
337fn node_control_option_source_is_default(value: &NodeControlOptionSource) -> bool {
338    value == &NodeControlOptionSource::Inline
339}
340
341/// Validation rules adapters can present without owning validation execution.
342#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
343#[serde(rename_all = "snake_case", tag = "kind")]
344pub enum NodeControlValidationRule {
345    Required,
346    EnumValues { values: Vec<Value> },
347    Regex { pattern: String },
348    Range { min: Option<f64>, max: Option<f64> },
349    ExpressionShape { language: Option<String> },
350}
351
352/// Validation metadata for a node-local control.
353#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
354pub struct NodeControlValidation {
355    #[serde(default, skip_serializing_if = "Vec::is_empty")]
356    pub rules: Vec<NodeControlValidationRule>,
357    #[serde(default, skip_serializing_if = "Vec::is_empty")]
358    pub messages: Vec<String>,
359}
360
361impl NodeControlValidation {
362    pub fn with_rule(mut self, rule: NodeControlValidationRule) -> Self {
363        self.rules.push(rule);
364        self
365    }
366
367    pub fn with_message(mut self, message: impl Into<String>) -> Self {
368        self.messages.push(message.into());
369        self
370    }
371
372    pub fn required(mut self) -> Self {
373        self.rules.push(NodeControlValidationRule::Required);
374        self
375    }
376
377    pub fn is_empty(&self) -> bool {
378        self.rules.is_empty() && self.messages.is_empty()
379    }
380}
381
382fn node_control_validation_is_empty(value: &NodeControlValidation) -> bool {
383    value.is_empty()
384}
385
386/// Presentation hints for adapter-local widgets.
387#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
388pub struct NodeControlPresentation {
389    #[serde(default, skip_serializing_if = "Option::is_none")]
390    pub placeholder: Option<String>,
391    #[serde(default, skip_serializing_if = "Option::is_none")]
392    pub help_text: Option<String>,
393    #[serde(default, skip_serializing_if = "Option::is_none")]
394    pub icon_key: Option<String>,
395    #[serde(default, skip_serializing_if = "Option::is_none")]
396    pub unit: Option<String>,
397    #[serde(default, skip_serializing_if = "Option::is_none")]
398    pub language: Option<String>,
399    #[serde(default, skip_serializing_if = "is_false")]
400    pub compact_label: bool,
401    #[serde(default, skip_serializing_if = "is_false")]
402    pub multiline: bool,
403}
404
405impl NodeControlPresentation {
406    pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
407        self.placeholder = Some(placeholder.into());
408        self
409    }
410
411    pub fn with_help_text(mut self, help_text: impl Into<String>) -> Self {
412        self.help_text = Some(help_text.into());
413        self
414    }
415
416    pub fn with_icon_key(mut self, icon_key: impl Into<String>) -> Self {
417        self.icon_key = Some(icon_key.into());
418        self
419    }
420
421    pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
422        self.unit = Some(unit.into());
423        self
424    }
425
426    pub fn with_language(mut self, language: impl Into<String>) -> Self {
427        self.language = Some(language.into());
428        self
429    }
430
431    pub fn compact_label(mut self) -> Self {
432        self.compact_label = true;
433        self
434    }
435
436    pub fn multiline(mut self) -> Self {
437        self.multiline = true;
438        self
439    }
440
441    pub fn is_empty(&self) -> bool {
442        self == &Self::default()
443    }
444}
445
446fn node_control_presentation_is_empty(value: &NodeControlPresentation) -> bool {
447    value.is_empty()
448}
449
450/// Editability metadata for adapter-local controls.
451#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
452pub struct NodeControlEditability {
453    #[serde(default, skip_serializing_if = "is_false")]
454    pub read_only: bool,
455    #[serde(default, skip_serializing_if = "Option::is_none")]
456    pub disabled_reason: Option<String>,
457    #[serde(default, skip_serializing_if = "is_false")]
458    pub secret: bool,
459}
460
461impl NodeControlEditability {
462    pub fn read_only(mut self) -> Self {
463        self.read_only = true;
464        self
465    }
466
467    pub fn disabled(mut self, reason: impl Into<String>) -> Self {
468        self.disabled_reason = Some(reason.into());
469        self
470    }
471
472    pub fn secret(mut self) -> Self {
473        self.secret = true;
474        self
475    }
476
477    pub fn is_disabled(&self) -> bool {
478        self.disabled_reason.is_some()
479    }
480
481    pub fn is_empty(&self) -> bool {
482        self == &Self::default()
483    }
484}
485
486fn node_control_editability_is_empty(value: &NodeControlEditability) -> bool {
487    value.is_empty()
488}
489
490/// Renderer-neutral metadata for one adapter-local editable control.
491#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
492pub struct NodeControlDescriptor {
493    pub key: String,
494    pub kind: NodeControlKind,
495    #[serde(default, skip_serializing_if = "Option::is_none")]
496    pub label: Option<String>,
497    #[serde(default, skip_serializing_if = "Option::is_none")]
498    pub binding: Option<NodeControlBinding>,
499    #[serde(default, skip_serializing_if = "Vec::is_empty")]
500    pub options: Vec<NodeControlOption>,
501    #[serde(default, skip_serializing_if = "node_control_option_source_is_default")]
502    pub option_source: NodeControlOptionSource,
503    #[serde(default, skip_serializing_if = "node_control_validation_is_empty")]
504    pub validation: NodeControlValidation,
505    #[serde(default, skip_serializing_if = "node_control_presentation_is_empty")]
506    pub presentation: NodeControlPresentation,
507    #[serde(default, skip_serializing_if = "node_control_editability_is_empty")]
508    pub editability: NodeControlEditability,
509    #[serde(default, skip_serializing_if = "Option::is_none")]
510    pub order: Option<i32>,
511    #[serde(default, skip_serializing_if = "Option::is_none")]
512    pub anchor: Option<String>,
513    #[serde(default, skip_serializing_if = "Option::is_none")]
514    pub slot: Option<String>,
515}
516
517impl NodeControlDescriptor {
518    pub fn new(key: impl Into<String>, kind: NodeControlKind) -> Self {
519        Self {
520            key: key.into(),
521            kind,
522            label: None,
523            binding: None,
524            options: Vec::new(),
525            option_source: NodeControlOptionSource::Inline,
526            validation: NodeControlValidation::default(),
527            presentation: NodeControlPresentation::default(),
528            editability: NodeControlEditability::default(),
529            order: None,
530            anchor: None,
531            slot: None,
532        }
533    }
534
535    pub fn text_input(key: impl Into<String>) -> Self {
536        Self::new(key, NodeControlKind::TextInput)
537    }
538
539    pub fn number_input(key: impl Into<String>) -> Self {
540        Self::new(key, NodeControlKind::NumberInput)
541    }
542
543    pub fn select(key: impl Into<String>) -> Self {
544        Self::new(key, NodeControlKind::Select)
545    }
546
547    pub fn multi_select(key: impl Into<String>) -> Self {
548        Self::new(key, NodeControlKind::MultiSelect)
549    }
550
551    pub fn toggle(key: impl Into<String>) -> Self {
552        Self::new(key, NodeControlKind::Toggle)
553    }
554
555    pub fn code(key: impl Into<String>) -> Self {
556        Self::new(key, NodeControlKind::Code)
557    }
558
559    pub fn color(key: impl Into<String>) -> Self {
560        Self::new(key, NodeControlKind::Color)
561    }
562
563    pub fn asset(key: impl Into<String>) -> Self {
564        Self::new(key, NodeControlKind::Asset)
565    }
566
567    pub fn variable_picker(key: impl Into<String>) -> Self {
568        Self::new(key, NodeControlKind::VariablePicker)
569    }
570
571    pub fn expression(key: impl Into<String>) -> Self {
572        Self::new(key, NodeControlKind::Expression)
573    }
574
575    pub fn text_area(key: impl Into<String>) -> Self {
576        Self::new(key, NodeControlKind::TextArea)
577            .with_presentation(NodeControlPresentation::default().multiline())
578    }
579
580    pub fn slider(key: impl Into<String>) -> Self {
581        Self::new(key, NodeControlKind::Slider)
582    }
583
584    pub fn port_binding(key: impl Into<String>) -> Self {
585        Self::new(key, NodeControlKind::PortBinding)
586    }
587
588    pub fn with_label(mut self, label: impl Into<String>) -> Self {
589        self.label = Some(label.into());
590        self
591    }
592
593    pub fn with_binding(mut self, binding: NodeControlBinding) -> Self {
594        self.binding = Some(binding);
595        self
596    }
597
598    pub fn with_option(mut self, option: NodeControlOption) -> Self {
599        self.options.push(option);
600        self
601    }
602
603    pub fn with_options(mut self, options: impl IntoIterator<Item = NodeControlOption>) -> Self {
604        self.options.extend(options);
605        self
606    }
607
608    pub fn with_option_source(mut self, option_source: NodeControlOptionSource) -> Self {
609        self.option_source = option_source;
610        self
611    }
612
613    pub fn with_validation(mut self, validation: NodeControlValidation) -> Self {
614        self.validation = validation;
615        self
616    }
617
618    pub fn with_validation_rule(mut self, rule: NodeControlValidationRule) -> Self {
619        self.validation.rules.push(rule);
620        self
621    }
622
623    pub fn required(self) -> Self {
624        self.with_validation_rule(NodeControlValidationRule::Required)
625    }
626
627    pub fn with_presentation(mut self, presentation: NodeControlPresentation) -> Self {
628        self.presentation = presentation;
629        self
630    }
631
632    pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
633        self.presentation.placeholder = Some(placeholder.into());
634        self
635    }
636
637    pub fn with_help_text(mut self, help_text: impl Into<String>) -> Self {
638        self.presentation.help_text = Some(help_text.into());
639        self
640    }
641
642    pub fn with_language(mut self, language: impl Into<String>) -> Self {
643        self.presentation.language = Some(language.into());
644        self
645    }
646
647    pub fn with_editability(mut self, editability: NodeControlEditability) -> Self {
648        self.editability = editability;
649        self
650    }
651
652    pub fn read_only(mut self) -> Self {
653        self.editability.read_only = true;
654        self
655    }
656
657    pub fn disabled(mut self, reason: impl Into<String>) -> Self {
658        self.editability.disabled_reason = Some(reason.into());
659        self
660    }
661
662    pub fn secret(mut self) -> Self {
663        self.editability.secret = true;
664        self
665    }
666
667    pub fn with_order(mut self, order: i32) -> Self {
668        self.order = Some(order);
669        self
670    }
671
672    pub fn with_anchor(mut self, anchor: impl Into<String>) -> Self {
673        self.anchor = Some(anchor.into());
674        self
675    }
676
677    pub fn with_slot(mut self, slot: impl Into<String>) -> Self {
678        self.slot = Some(slot.into());
679        self
680    }
681
682    pub fn data_key(&self) -> Option<&str> {
683        self.binding
684            .as_ref()
685            .and_then(|binding| match binding.source {
686                NodeControlBindingSource::DataPath | NodeControlBindingSource::Slot => {
687                    Some(binding.path.as_str())
688                }
689                NodeControlBindingSource::JsonPointer
690                | NodeControlBindingSource::GraphSymbol
691                | NodeControlBindingSource::PortAnchor => None,
692            })
693            .or(self.slot.as_deref())
694    }
695
696    pub fn display_label(&self) -> Option<&str> {
697        self.label.as_deref().or_else(|| {
698            self.key
699                .split_once('.')
700                .map(|(_, tail)| tail)
701                .or(Some(self.key.as_str()))
702        })
703    }
704
705    pub fn order_key(&self) -> i32 {
706        self.order.unwrap_or(i32::MAX)
707    }
708}
709
710/// Renderer-neutral overflow affordance adapters should expose when content is capped.
711#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
712#[serde(rename_all = "snake_case")]
713pub enum NodeSurfaceOverflowIndicator {
714    Summary,
715    Count,
716    Scroll,
717    Fade,
718}
719
720/// Semantic layout budget for readable node-internal product surfaces.
721#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
722pub struct NodeSurfaceLayoutBudget {
723    /// Minimum logical node size needed before rendering full-density internals.
724    #[serde(default, skip_serializing_if = "Option::is_none")]
725    pub min_readable_size: Option<CanvasSize>,
726    /// Preferred logical node size for first render or resize suggestions.
727    #[serde(default, skip_serializing_if = "Option::is_none")]
728    pub preferred_size: Option<CanvasSize>,
729    /// Maximum logical text lines adapters should budget for ordinary slots.
730    #[serde(default, skip_serializing_if = "Option::is_none")]
731    pub slot_line_budget: Option<usize>,
732    /// Maximum logical text lines adapters should budget for controls.
733    #[serde(default, skip_serializing_if = "Option::is_none")]
734    pub control_line_budget: Option<usize>,
735    /// Suggested visible repeatable item count before showing overflow.
736    #[serde(default, skip_serializing_if = "Option::is_none")]
737    pub repeatable_visible_items: Option<usize>,
738    /// Semantic overflow affordance expected when content is capped.
739    #[serde(default, skip_serializing_if = "Option::is_none")]
740    pub overflow_indicator: Option<NodeSurfaceOverflowIndicator>,
741    /// Density tiers adapters should prefer when the full surface does not fit.
742    #[serde(default, skip_serializing_if = "Vec::is_empty")]
743    pub density_priority: Vec<NodeKitContentDensity>,
744}
745
746impl NodeSurfaceLayoutBudget {
747    pub fn with_min_readable_size(mut self, size: CanvasSize) -> Self {
748        self.min_readable_size = Some(size);
749        self
750    }
751
752    pub fn with_preferred_size(mut self, size: CanvasSize) -> Self {
753        self.preferred_size = Some(size);
754        self
755    }
756
757    pub fn with_slot_line_budget(mut self, lines: usize) -> Self {
758        self.slot_line_budget = Some(lines);
759        self
760    }
761
762    pub fn with_control_line_budget(mut self, lines: usize) -> Self {
763        self.control_line_budget = Some(lines);
764        self
765    }
766
767    pub fn with_repeatable_visible_items(mut self, items: usize) -> Self {
768        self.repeatable_visible_items = Some(items);
769        self
770    }
771
772    pub fn with_overflow_indicator(mut self, indicator: NodeSurfaceOverflowIndicator) -> Self {
773        self.overflow_indicator = Some(indicator);
774        self
775    }
776
777    pub fn with_density_priority(
778        mut self,
779        density_priority: impl IntoIterator<Item = NodeKitContentDensity>,
780    ) -> Self {
781        self.density_priority = density_priority.into_iter().collect();
782        self
783    }
784
785    pub fn is_empty(&self) -> bool {
786        self == &Self::default()
787    }
788}
789
790/// Descriptor for node data arrays or maps rendered as stable dynamic rows.
791#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
792pub struct NodeRepeatableCollectionDescriptor {
793    pub key: String,
794    pub item_source: String,
795    pub item_id_path: String,
796    #[serde(default, skip_serializing_if = "Vec::is_empty")]
797    pub item_template_slots: Vec<NodeSurfaceSlotDescriptor>,
798    #[serde(default, skip_serializing_if = "Option::is_none")]
799    pub label: Option<String>,
800    #[serde(default, skip_serializing_if = "Option::is_none")]
801    pub empty_label: Option<String>,
802    #[serde(default, skip_serializing_if = "Option::is_none")]
803    pub min_items: Option<usize>,
804    #[serde(default, skip_serializing_if = "Option::is_none")]
805    pub max_items: Option<usize>,
806    #[serde(default, skip_serializing_if = "is_false")]
807    pub reorderable: bool,
808    #[serde(default, skip_serializing_if = "Option::is_none")]
809    pub add_action: Option<String>,
810    #[serde(default, skip_serializing_if = "Option::is_none")]
811    pub remove_action: Option<String>,
812    #[serde(default, skip_serializing_if = "Option::is_none")]
813    pub reorder_action: Option<String>,
814    #[serde(default)]
815    pub anchor_rule: NodeRepeatableAnchorRule,
816}
817
818/// Stable key derivation for slots and anchors generated from repeatable items.
819#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
820pub struct NodeRepeatableAnchorRule {
821    pub slot_prefix: String,
822    pub anchor_prefix: String,
823    #[serde(default, skip_serializing_if = "Option::is_none")]
824    pub port_key_path: Option<String>,
825}
826
827impl Default for NodeRepeatableAnchorRule {
828    fn default() -> Self {
829        Self {
830            slot_prefix: "item".to_owned(),
831            anchor_prefix: "item".to_owned(),
832            port_key_path: None,
833        }
834    }
835}
836
837/// Projected instance of one repeatable item for adapter rendering and measurement.
838#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
839pub struct NodeRepeatableItemProjection {
840    pub collection_key: String,
841    pub item_id: String,
842    pub item_index: usize,
843    pub slot_key: String,
844    pub anchor: String,
845    #[serde(default, skip_serializing_if = "Option::is_none")]
846    pub port_key: Option<String>,
847    #[serde(default)]
848    pub item_data: Value,
849    #[serde(default, skip_serializing_if = "Vec::is_empty")]
850    pub slots: Vec<NodeSurfaceSlotDescriptor>,
851}
852
853impl NodeRepeatableCollectionDescriptor {
854    pub fn new(
855        key: impl Into<String>,
856        item_source: impl Into<String>,
857        item_id_path: impl Into<String>,
858    ) -> Self {
859        let key = key.into();
860        Self {
861            anchor_rule: NodeRepeatableAnchorRule {
862                slot_prefix: key.clone(),
863                anchor_prefix: key.clone(),
864                port_key_path: None,
865            },
866            key,
867            item_source: item_source.into(),
868            item_id_path: item_id_path.into(),
869            item_template_slots: Vec::new(),
870            label: None,
871            empty_label: None,
872            min_items: None,
873            max_items: None,
874            reorderable: false,
875            add_action: None,
876            remove_action: None,
877            reorder_action: None,
878        }
879    }
880
881    pub fn with_label(mut self, label: impl Into<String>) -> Self {
882        self.label = Some(label.into());
883        self
884    }
885
886    pub fn with_empty_label(mut self, empty_label: impl Into<String>) -> Self {
887        self.empty_label = Some(empty_label.into());
888        self
889    }
890
891    pub fn with_item_template_slot(mut self, slot: NodeSurfaceSlotDescriptor) -> Self {
892        self.item_template_slots.push(slot);
893        self
894    }
895
896    pub fn with_item_template_slots(
897        mut self,
898        slots: impl IntoIterator<Item = NodeSurfaceSlotDescriptor>,
899    ) -> Self {
900        self.item_template_slots.extend(slots);
901        self
902    }
903
904    pub fn with_min_items(mut self, min_items: usize) -> Self {
905        self.min_items = Some(min_items);
906        self
907    }
908
909    pub fn with_max_items(mut self, max_items: usize) -> Self {
910        self.max_items = Some(max_items);
911        self
912    }
913
914    pub fn reorderable(mut self) -> Self {
915        self.reorderable = true;
916        self
917    }
918
919    pub fn with_add_action(mut self, action: impl Into<String>) -> Self {
920        self.add_action = Some(action.into());
921        self
922    }
923
924    pub fn with_remove_action(mut self, action: impl Into<String>) -> Self {
925        self.remove_action = Some(action.into());
926        self
927    }
928
929    pub fn with_reorder_action(mut self, action: impl Into<String>) -> Self {
930        self.reorder_action = Some(action.into());
931        self
932    }
933
934    pub fn with_anchor_rule(mut self, anchor_rule: NodeRepeatableAnchorRule) -> Self {
935        self.anchor_rule = anchor_rule;
936        self
937    }
938
939    pub fn item_projections(&self, node_data: &Value) -> Vec<NodeRepeatableItemProjection> {
940        let Some(items) = semantic_json_lookup(node_data, &self.item_source) else {
941            return Vec::new();
942        };
943
944        match items {
945            Value::Array(items) => items
946                .iter()
947                .enumerate()
948                .filter_map(|(index, item)| self.item_projection(index, item))
949                .collect(),
950            Value::Object(items) => items
951                .iter()
952                .enumerate()
953                .filter_map(|(index, (key, item))| {
954                    self.item_projection_with_fallback_id(index, item, key.as_str())
955                })
956                .collect(),
957            _ => Vec::new(),
958        }
959    }
960
961    pub fn is_empty_for(&self, node_data: &Value) -> bool {
962        self.item_projections(node_data).is_empty()
963    }
964
965    pub fn add_disabled_reason(&self, node_data: &Value) -> Option<String> {
966        let max_items = self.max_items?;
967        let len = self.item_projections(node_data).len();
968        (len >= max_items).then(|| format!("Maximum of {max_items} items reached"))
969    }
970
971    pub fn remove_disabled_reason(&self, node_data: &Value) -> Option<String> {
972        let min_items = self.min_items?;
973        let len = self.item_projections(node_data).len();
974        (len <= min_items).then(|| format!("Minimum of {min_items} items required"))
975    }
976
977    fn item_projection(
978        &self,
979        item_index: usize,
980        item_data: &Value,
981    ) -> Option<NodeRepeatableItemProjection> {
982        let item_id = semantic_json_lookup(item_data, &self.item_id_path)
983            .and_then(json_scalar_to_stable_string)?;
984        self.item_projection_with_id(item_index, item_data, &item_id)
985    }
986
987    fn item_projection_with_fallback_id(
988        &self,
989        item_index: usize,
990        item_data: &Value,
991        fallback_id: &str,
992    ) -> Option<NodeRepeatableItemProjection> {
993        let item_id = semantic_json_lookup(item_data, &self.item_id_path)
994            .and_then(json_scalar_to_stable_string)
995            .unwrap_or_else(|| fallback_id.to_owned());
996        self.item_projection_with_id(item_index, item_data, &item_id)
997    }
998
999    fn item_projection_with_id(
1000        &self,
1001        item_index: usize,
1002        item_data: &Value,
1003        item_id: &str,
1004    ) -> Option<NodeRepeatableItemProjection> {
1005        let item_id = sanitize_repeatable_key(item_id)?;
1006        let slot_key = format!("{}.{}", self.anchor_rule.slot_prefix, item_id);
1007        let anchor = format!("{}.{}", self.anchor_rule.anchor_prefix, item_id);
1008        let port_key = self
1009            .anchor_rule
1010            .port_key_path
1011            .as_deref()
1012            .and_then(|path| semantic_json_lookup(item_data, path))
1013            .and_then(json_scalar_to_stable_string);
1014        let slots = self
1015            .item_template_slots
1016            .iter()
1017            .map(|slot| repeatable_item_slot(slot, &slot_key, &anchor, item_id))
1018            .collect();
1019
1020        Some(NodeRepeatableItemProjection {
1021            collection_key: self.key.clone(),
1022            item_id: item_id.to_owned(),
1023            item_index,
1024            slot_key,
1025            anchor,
1026            port_key,
1027            item_data: item_data.clone(),
1028            slots,
1029        })
1030    }
1031}
1032
1033impl NodeRepeatableAnchorRule {
1034    pub fn new(slot_prefix: impl Into<String>, anchor_prefix: impl Into<String>) -> Self {
1035        Self {
1036            slot_prefix: slot_prefix.into(),
1037            anchor_prefix: anchor_prefix.into(),
1038            port_key_path: None,
1039        }
1040    }
1041
1042    pub fn with_port_key_path(mut self, port_key_path: impl Into<String>) -> Self {
1043        self.port_key_path = Some(port_key_path.into());
1044        self
1045    }
1046}
1047
1048fn repeatable_item_slot(
1049    template: &NodeSurfaceSlotDescriptor,
1050    slot_key: &str,
1051    anchor: &str,
1052    item_id: &str,
1053) -> NodeSurfaceSlotDescriptor {
1054    let suffix = template.key_tail().unwrap_or(template.key.as_str());
1055    let mut slot = template.clone();
1056    slot.key = format!("{slot_key}.{suffix}");
1057    slot.anchor = Some(format!("{anchor}.{suffix}"));
1058    if let Some(template_slot) = template.slot.as_deref() {
1059        slot.slot = Some(format!("{template_slot}.{item_id}"));
1060    }
1061    slot
1062}
1063
1064fn json_scalar_to_stable_string(value: &Value) -> Option<String> {
1065    match value {
1066        Value::String(value) => Some(value.clone()),
1067        Value::Number(value) => Some(value.to_string()),
1068        Value::Bool(value) => Some(value.to_string()),
1069        _ => None,
1070    }
1071}
1072
1073fn sanitize_repeatable_key(value: &str) -> Option<&str> {
1074    (!value.is_empty()
1075        && value
1076            .bytes()
1077            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')))
1078    .then_some(value)
1079}
1080
1081/// Renderer-neutral action target.
1082#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1083#[serde(rename_all = "snake_case", tag = "kind")]
1084pub enum ActionTarget {
1085    Graph,
1086    Node {
1087        node_kind: String,
1088    },
1089    Edge,
1090    Port {
1091        port_key: String,
1092    },
1093    Slot {
1094        slot_key: String,
1095    },
1096    Control {
1097        control_key: String,
1098    },
1099    RepeatableItem {
1100        collection_key: String,
1101        item_id: String,
1102    },
1103    DroppedWire {
1104        #[serde(default, skip_serializing_if = "Option::is_none")]
1105        source_port_key: Option<String>,
1106    },
1107    Inspector {
1108        inspector_key: String,
1109    },
1110    Blackboard {
1111        blackboard_key: String,
1112    },
1113}
1114
1115/// Renderer-neutral action intent.
1116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1117#[serde(rename_all = "snake_case", tag = "kind")]
1118pub enum ActionIntent {
1119    InsertNode {
1120        node_kind: String,
1121    },
1122    OpenMenu {
1123        menu_key: String,
1124    },
1125    OpenInspector {
1126        inspector_key: String,
1127    },
1128    AddRepeatableItem {
1129        collection_key: String,
1130    },
1131    RemoveRepeatableItem {
1132        collection_key: String,
1133        item_id: String,
1134    },
1135    ReorderRepeatableItem {
1136        collection_key: String,
1137        item_id: String,
1138    },
1139    SetControlValue {
1140        control_key: String,
1141    },
1142    RunNode,
1143    OpenBlackboard {
1144        blackboard_key: String,
1145    },
1146    Custom {
1147        key: String,
1148    },
1149}
1150
1151/// Applicability and disabled-state metadata for actions.
1152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1153pub struct ActionAvailability {
1154    #[serde(default = "default_true")]
1155    pub enabled: bool,
1156    #[serde(default, skip_serializing_if = "Option::is_none")]
1157    pub disabled_reason: Option<String>,
1158}
1159
1160fn default_true() -> bool {
1161    true
1162}
1163
1164impl Default for ActionAvailability {
1165    fn default() -> Self {
1166        Self {
1167            enabled: true,
1168            disabled_reason: None,
1169        }
1170    }
1171}
1172
1173impl ActionAvailability {
1174    pub fn enabled() -> Self {
1175        Self::default()
1176    }
1177
1178    pub fn disabled(reason: impl Into<String>) -> Self {
1179        Self {
1180            enabled: false,
1181            disabled_reason: Some(reason.into()),
1182        }
1183    }
1184
1185    pub fn is_enabled(&self) -> bool {
1186        self.enabled
1187    }
1188}
1189
1190fn action_availability_is_default(value: &ActionAvailability) -> bool {
1191    value == &ActionAvailability::default()
1192}
1193
1194/// Shortcut metadata adapters can map to toolkit-specific accelerators.
1195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1196pub struct ActionShortcut {
1197    pub key: String,
1198    #[serde(default, skip_serializing_if = "is_false")]
1199    pub ctrl: bool,
1200    #[serde(default, skip_serializing_if = "is_false")]
1201    pub shift: bool,
1202    #[serde(default, skip_serializing_if = "is_false")]
1203    pub alt: bool,
1204    #[serde(default, skip_serializing_if = "is_false")]
1205    pub meta: bool,
1206}
1207
1208impl ActionShortcut {
1209    pub fn new(key: impl Into<String>) -> Self {
1210        Self {
1211            key: key.into(),
1212            ctrl: false,
1213            shift: false,
1214            alt: false,
1215            meta: false,
1216        }
1217    }
1218
1219    pub fn ctrl(mut self) -> Self {
1220        self.ctrl = true;
1221        self
1222    }
1223
1224    pub fn shift(mut self) -> Self {
1225        self.shift = true;
1226        self
1227    }
1228
1229    pub fn alt(mut self) -> Self {
1230        self.alt = true;
1231        self
1232    }
1233
1234    pub fn meta(mut self) -> Self {
1235        self.meta = true;
1236        self
1237    }
1238}
1239
1240/// Renderer-neutral command/action descriptor.
1241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1242pub struct NodeActionDescriptor {
1243    pub key: String,
1244    pub label: String,
1245    pub target: ActionTarget,
1246    pub intent: ActionIntent,
1247    #[serde(default, skip_serializing_if = "action_availability_is_default")]
1248    pub availability: ActionAvailability,
1249    #[serde(default, skip_serializing_if = "Option::is_none")]
1250    pub group: Option<String>,
1251    #[serde(default, skip_serializing_if = "Option::is_none")]
1252    pub order: Option<i32>,
1253    #[serde(default, skip_serializing_if = "is_false")]
1254    pub danger: bool,
1255    #[serde(default, skip_serializing_if = "Option::is_none")]
1256    pub icon_key: Option<String>,
1257    #[serde(default, skip_serializing_if = "Option::is_none")]
1258    pub shortcut: Option<ActionShortcut>,
1259}
1260
1261impl NodeActionDescriptor {
1262    pub fn new(
1263        key: impl Into<String>,
1264        label: impl Into<String>,
1265        target: ActionTarget,
1266        intent: ActionIntent,
1267    ) -> Self {
1268        Self {
1269            key: key.into(),
1270            label: label.into(),
1271            target,
1272            intent,
1273            availability: ActionAvailability::default(),
1274            group: None,
1275            order: None,
1276            danger: false,
1277            icon_key: None,
1278            shortcut: None,
1279        }
1280    }
1281
1282    pub fn disabled(mut self, reason: impl Into<String>) -> Self {
1283        self.availability = ActionAvailability::disabled(reason);
1284        self
1285    }
1286
1287    pub fn with_group(mut self, group: impl Into<String>) -> Self {
1288        self.group = Some(group.into());
1289        self
1290    }
1291
1292    pub fn with_order(mut self, order: i32) -> Self {
1293        self.order = Some(order);
1294        self
1295    }
1296
1297    pub fn danger(mut self) -> Self {
1298        self.danger = true;
1299        self
1300    }
1301
1302    pub fn with_icon_key(mut self, icon_key: impl Into<String>) -> Self {
1303        self.icon_key = Some(icon_key.into());
1304        self
1305    }
1306
1307    pub fn with_shortcut(mut self, shortcut: ActionShortcut) -> Self {
1308        self.shortcut = Some(shortcut);
1309        self
1310    }
1311
1312    pub fn is_enabled(&self) -> bool {
1313        self.availability.is_enabled()
1314    }
1315}
1316
1317/// Surface on which a renderer-local menu appears.
1318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1319#[serde(rename_all = "snake_case", tag = "kind")]
1320pub enum MenuSurface {
1321    Graph,
1322    Node,
1323    Edge,
1324    Port,
1325    Slot,
1326    Control,
1327    DroppedWire,
1328    Toolbar,
1329    Blackboard,
1330    Inspector,
1331}
1332
1333/// Renderer-neutral menu descriptor. Adapters own popup state and widgets.
1334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1335pub struct MenuDescriptor {
1336    pub key: String,
1337    pub surface: MenuSurface,
1338    #[serde(default, skip_serializing_if = "Option::is_none")]
1339    pub label: Option<String>,
1340    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1341    pub action_keys: Vec<String>,
1342}
1343
1344impl MenuDescriptor {
1345    pub fn new(key: impl Into<String>, surface: MenuSurface) -> Self {
1346        Self {
1347            key: key.into(),
1348            surface,
1349            label: None,
1350            action_keys: Vec::new(),
1351        }
1352    }
1353
1354    pub fn with_label(mut self, label: impl Into<String>) -> Self {
1355        self.label = Some(label.into());
1356        self
1357    }
1358
1359    pub fn with_action_key(mut self, action_key: impl Into<String>) -> Self {
1360        self.action_keys.push(action_key.into());
1361        self
1362    }
1363
1364    pub fn with_action_keys(
1365        mut self,
1366        action_keys: impl IntoIterator<Item = impl Into<String>>,
1367    ) -> Self {
1368        self.action_keys
1369            .extend(action_keys.into_iter().map(Into::into));
1370        self
1371    }
1372}
1373
1374/// Target for an inspector surface.
1375#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1376#[serde(rename_all = "snake_case", tag = "kind")]
1377pub enum InspectorTarget {
1378    Graph,
1379    Node {
1380        node_kind: String,
1381    },
1382    Edge,
1383    Port {
1384        port_key: String,
1385    },
1386    Slot {
1387        slot_key: String,
1388    },
1389    Control {
1390        control_key: String,
1391    },
1392    RepeatableItem {
1393        collection_key: String,
1394        item_id: String,
1395    },
1396    Diagnostic {
1397        diagnostic_key: String,
1398    },
1399}
1400
1401/// Renderer-neutral inspector descriptor.
1402#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1403pub struct InspectorDescriptor {
1404    pub key: String,
1405    pub target: InspectorTarget,
1406    #[serde(default, skip_serializing_if = "Option::is_none")]
1407    pub label: Option<String>,
1408    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1409    pub controls: Vec<NodeControlDescriptor>,
1410    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1411    pub action_keys: Vec<String>,
1412}
1413
1414impl InspectorDescriptor {
1415    pub fn new(key: impl Into<String>, target: InspectorTarget) -> Self {
1416        Self {
1417            key: key.into(),
1418            target,
1419            label: None,
1420            controls: Vec::new(),
1421            action_keys: Vec::new(),
1422        }
1423    }
1424
1425    pub fn with_label(mut self, label: impl Into<String>) -> Self {
1426        self.label = Some(label.into());
1427        self
1428    }
1429
1430    pub fn with_control(mut self, control: NodeControlDescriptor) -> Self {
1431        self.controls.push(control);
1432        self
1433    }
1434
1435    pub fn with_action_key(mut self, action_key: impl Into<String>) -> Self {
1436        self.action_keys.push(action_key.into());
1437        self
1438    }
1439}
1440
1441/// Graph-level property collection exposed to adapters as a local blackboard UI.
1442#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1443pub struct BlackboardDescriptor {
1444    pub key: String,
1445    pub label: String,
1446    pub collection: NodeRepeatableCollectionDescriptor,
1447    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1448    pub action_keys: Vec<String>,
1449}
1450
1451impl BlackboardDescriptor {
1452    pub fn new(
1453        key: impl Into<String>,
1454        label: impl Into<String>,
1455        collection: NodeRepeatableCollectionDescriptor,
1456    ) -> Self {
1457        Self {
1458            key: key.into(),
1459            label: label.into(),
1460            collection,
1461            action_keys: Vec::new(),
1462        }
1463    }
1464
1465    pub fn with_action_key(mut self, action_key: impl Into<String>) -> Self {
1466        self.action_keys.push(action_key.into());
1467        self
1468    }
1469}
1470
1471/// Adapter-facing visibility hint for a node-local surface slot.
1472#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1473#[serde(rename_all = "snake_case")]
1474pub enum NodeSurfaceSlotVisibility {
1475    Visible,
1476    Hidden,
1477    Collapsed,
1478}
1479
1480/// Shared semantic role for adapter-owned chrome around a node.
1481#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1482#[serde(rename_all = "snake_case")]
1483pub enum NodeChromeKind {
1484    Resizer,
1485    Toolbar,
1486    StatusStrip,
1487    ValidationBanner,
1488    RunActionStrip,
1489    InspectorAnchor,
1490}
1491
1492/// Preferred placement for adapter-owned chrome around a node.
1493#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1494#[serde(rename_all = "snake_case")]
1495pub enum NodeChromePlacement {
1496    Top,
1497    TopRight,
1498    Right,
1499    BottomRight,
1500    Bottom,
1501    BottomLeft,
1502    Left,
1503    TopLeft,
1504    InsideHeader,
1505    InsideFooter,
1506}
1507
1508/// Adapter-facing visibility hint for node chrome.
1509#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1510#[serde(rename_all = "snake_case")]
1511pub enum NodeChromeVisibility {
1512    Always,
1513    Selected,
1514    Hovered,
1515    Focused,
1516    Hidden,
1517}
1518
1519/// Renderer-neutral metadata for adapter-owned node chrome.
1520#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1521pub struct NodeChromeDescriptor {
1522    pub key: String,
1523    pub kind: NodeChromeKind,
1524    pub placement: NodeChromePlacement,
1525    #[serde(default, skip_serializing_if = "Option::is_none")]
1526    pub label: Option<String>,
1527    #[serde(default, skip_serializing_if = "Option::is_none")]
1528    pub renderer_key: Option<String>,
1529    #[serde(default, skip_serializing_if = "Option::is_none")]
1530    pub icon_key: Option<String>,
1531    #[serde(default, skip_serializing_if = "Option::is_none")]
1532    pub visibility: Option<NodeChromeVisibility>,
1533    #[serde(default, skip_serializing_if = "Option::is_none")]
1534    pub order: Option<i32>,
1535    #[serde(default, skip_serializing_if = "is_false")]
1536    pub interactive: bool,
1537}
1538
1539impl NodeChromeDescriptor {
1540    pub fn new(
1541        key: impl Into<String>,
1542        kind: NodeChromeKind,
1543        placement: NodeChromePlacement,
1544    ) -> Self {
1545        Self {
1546            key: key.into(),
1547            kind,
1548            placement,
1549            label: None,
1550            renderer_key: None,
1551            icon_key: None,
1552            visibility: None,
1553            order: None,
1554            interactive: false,
1555        }
1556    }
1557
1558    pub fn resizer(key: impl Into<String>) -> Self {
1559        Self::new(
1560            key,
1561            NodeChromeKind::Resizer,
1562            NodeChromePlacement::BottomRight,
1563        )
1564        .interactive()
1565        .with_visibility(NodeChromeVisibility::Selected)
1566    }
1567
1568    pub fn toolbar(key: impl Into<String>, placement: NodeChromePlacement) -> Self {
1569        Self::new(key, NodeChromeKind::Toolbar, placement)
1570            .interactive()
1571            .with_visibility(NodeChromeVisibility::Selected)
1572    }
1573
1574    pub fn status_strip(key: impl Into<String>, placement: NodeChromePlacement) -> Self {
1575        Self::new(key, NodeChromeKind::StatusStrip, placement)
1576            .with_visibility(NodeChromeVisibility::Always)
1577    }
1578
1579    pub fn validation_banner(key: impl Into<String>, placement: NodeChromePlacement) -> Self {
1580        Self::new(key, NodeChromeKind::ValidationBanner, placement)
1581            .with_visibility(NodeChromeVisibility::Always)
1582    }
1583
1584    pub fn run_action_strip(key: impl Into<String>, placement: NodeChromePlacement) -> Self {
1585        Self::new(key, NodeChromeKind::RunActionStrip, placement)
1586            .interactive()
1587            .with_visibility(NodeChromeVisibility::Selected)
1588    }
1589
1590    pub fn inspector_anchor(key: impl Into<String>, placement: NodeChromePlacement) -> Self {
1591        Self::new(key, NodeChromeKind::InspectorAnchor, placement)
1592            .with_visibility(NodeChromeVisibility::Selected)
1593    }
1594
1595    pub fn with_label(mut self, label: impl Into<String>) -> Self {
1596        self.label = Some(label.into());
1597        self
1598    }
1599
1600    pub fn with_renderer_key(mut self, renderer_key: impl Into<String>) -> Self {
1601        self.renderer_key = Some(renderer_key.into());
1602        self
1603    }
1604
1605    pub fn with_icon_key(mut self, icon_key: impl Into<String>) -> Self {
1606        self.icon_key = Some(icon_key.into());
1607        self
1608    }
1609
1610    pub fn with_visibility(mut self, visibility: NodeChromeVisibility) -> Self {
1611        self.visibility = Some(visibility);
1612        self
1613    }
1614
1615    pub fn with_order(mut self, order: i32) -> Self {
1616        self.order = Some(order);
1617        self
1618    }
1619
1620    pub fn interactive(mut self) -> Self {
1621        self.interactive = true;
1622        self
1623    }
1624
1625    pub fn hidden(self) -> Self {
1626        self.with_visibility(NodeChromeVisibility::Hidden)
1627    }
1628
1629    pub fn effective_visibility(&self) -> NodeChromeVisibility {
1630        self.visibility.unwrap_or(NodeChromeVisibility::Always)
1631    }
1632
1633    pub fn is_visible_for_state(&self, selected: bool, hovered: bool, focused: bool) -> bool {
1634        match self.effective_visibility() {
1635            NodeChromeVisibility::Always => true,
1636            NodeChromeVisibility::Selected => selected,
1637            NodeChromeVisibility::Hovered => hovered,
1638            NodeChromeVisibility::Focused => focused,
1639            NodeChromeVisibility::Hidden => false,
1640        }
1641    }
1642}
1643
1644/// Adapter-facing, toolkit-neutral semantic surface projection for one node.
1645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1646pub struct NodeSurfaceProjection {
1647    /// Current density tier used by the adapter.
1648    pub density: NodeKitContentDensity,
1649    /// Maximum slots the adapter should show at this density.
1650    pub slot_limit: usize,
1651    /// Whether the adapter should prioritize compact value previews.
1652    pub compact_values: bool,
1653    /// Whether all visible slots should be shown without truncation.
1654    pub expand_all_slots: bool,
1655}
1656
1657impl NodeSurfaceProjection {
1658    pub fn from_layout_hints(layout_hints: &NodeKitLayoutHints, zoom: f32) -> Self {
1659        let density = layout_hints.content_density_for_zoom(zoom);
1660        let (slot_limit, compact_values, expand_all_slots) = match density {
1661            NodeKitContentDensity::Compact => (2, true, false),
1662            NodeKitContentDensity::Regular => (3, true, false),
1663            NodeKitContentDensity::Full => (usize::MAX, false, true),
1664        };
1665
1666        Self {
1667            density,
1668            slot_limit,
1669            compact_values,
1670            expand_all_slots,
1671        }
1672    }
1673}
1674
1675/// Renderer-neutral node-local slot metadata for rich adapter surfaces.
1676#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1677pub struct NodeSurfaceSlotDescriptor {
1678    /// Stable slot key within the node kind, such as `header.main` or `field.primary_key`.
1679    pub key: String,
1680    /// Semantic role that adapters map to toolkit-specific widgets.
1681    pub kind: NodeSurfaceSlotKind,
1682    /// Optional UI-facing label.
1683    #[serde(default, skip_serializing_if = "Option::is_none")]
1684    pub label: Option<String>,
1685    /// Deterministic order within the slot kind/lane.
1686    #[serde(default, skip_serializing_if = "Option::is_none")]
1687    pub order: Option<i32>,
1688    /// Optional adapter anchor id used by ports or nested regions.
1689    #[serde(default, skip_serializing_if = "Option::is_none")]
1690    pub anchor: Option<String>,
1691    /// Optional lane key for adapters that group slots.
1692    #[serde(default, skip_serializing_if = "Option::is_none")]
1693    pub lane: Option<String>,
1694    /// Optional sub-slot key inside a lane or anchor.
1695    #[serde(default, skip_serializing_if = "Option::is_none")]
1696    pub slot: Option<String>,
1697    /// Optional adapter renderer key for this slot.
1698    #[serde(default, skip_serializing_if = "Option::is_none")]
1699    pub renderer_key: Option<String>,
1700    /// Optional adapter icon key.
1701    #[serde(default, skip_serializing_if = "Option::is_none")]
1702    pub icon_key: Option<String>,
1703    /// Optional visibility hint.
1704    #[serde(default, skip_serializing_if = "Option::is_none")]
1705    pub visibility: Option<NodeSurfaceSlotVisibility>,
1706    /// Renderer-neutral controls adapters can map to local toolkit widgets.
1707    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1708    pub controls: Vec<NodeControlDescriptor>,
1709}
1710
1711impl NodeSurfaceSlotDescriptor {
1712    pub fn new(key: impl Into<String>, kind: NodeSurfaceSlotKind) -> Self {
1713        Self {
1714            key: key.into(),
1715            kind,
1716            label: None,
1717            order: None,
1718            anchor: None,
1719            lane: None,
1720            slot: None,
1721            renderer_key: None,
1722            icon_key: None,
1723            visibility: None,
1724            controls: Vec::new(),
1725        }
1726    }
1727
1728    pub fn header(key: impl Into<String>) -> Self {
1729        Self::new(key, NodeSurfaceSlotKind::Header)
1730    }
1731
1732    pub fn body(key: impl Into<String>) -> Self {
1733        Self::new(key, NodeSurfaceSlotKind::Body)
1734    }
1735
1736    pub fn footer(key: impl Into<String>) -> Self {
1737        Self::new(key, NodeSurfaceSlotKind::Footer)
1738    }
1739
1740    pub fn badge(key: impl Into<String>) -> Self {
1741        Self::new(key, NodeSurfaceSlotKind::Badge)
1742    }
1743
1744    pub fn icon(key: impl Into<String>) -> Self {
1745        Self::new(key, NodeSurfaceSlotKind::Icon)
1746    }
1747
1748    pub fn field_row(key: impl Into<String>) -> Self {
1749        Self::new(key, NodeSurfaceSlotKind::FieldRow)
1750    }
1751
1752    pub fn action_row(key: impl Into<String>) -> Self {
1753        Self::new(key, NodeSurfaceSlotKind::ActionRow)
1754    }
1755
1756    pub fn preview(key: impl Into<String>) -> Self {
1757        Self::new(key, NodeSurfaceSlotKind::Preview)
1758    }
1759
1760    pub fn nested_region(key: impl Into<String>) -> Self {
1761        Self::new(key, NodeSurfaceSlotKind::NestedRegion)
1762    }
1763
1764    pub fn status_banner(key: impl Into<String>) -> Self {
1765        Self::new(key, NodeSurfaceSlotKind::StatusBanner)
1766    }
1767
1768    pub fn port_rail(key: impl Into<String>) -> Self {
1769        Self::new(key, NodeSurfaceSlotKind::PortRail)
1770    }
1771
1772    pub fn config_group(key: impl Into<String>) -> Self {
1773        Self::new(key, NodeSurfaceSlotKind::ConfigGroup)
1774    }
1775
1776    pub fn metric_badge(key: impl Into<String>) -> Self {
1777        Self::new(key, NodeSurfaceSlotKind::MetricBadge)
1778    }
1779
1780    pub fn with_label(mut self, label: impl Into<String>) -> Self {
1781        self.label = Some(label.into());
1782        self
1783    }
1784
1785    pub fn with_order(mut self, order: i32) -> Self {
1786        self.order = Some(order);
1787        self
1788    }
1789
1790    pub fn with_anchor(mut self, anchor: impl Into<String>) -> Self {
1791        self.anchor = Some(anchor.into());
1792        self
1793    }
1794
1795    pub fn with_lane(mut self, lane: impl Into<String>) -> Self {
1796        self.lane = Some(lane.into());
1797        self
1798    }
1799
1800    pub fn with_slot(mut self, slot: impl Into<String>) -> Self {
1801        self.slot = Some(slot.into());
1802        self
1803    }
1804
1805    pub fn with_renderer_key(mut self, renderer_key: impl Into<String>) -> Self {
1806        self.renderer_key = Some(renderer_key.into());
1807        self
1808    }
1809
1810    pub fn with_icon_key(mut self, icon_key: impl Into<String>) -> Self {
1811        self.icon_key = Some(icon_key.into());
1812        self
1813    }
1814
1815    pub fn with_visibility(mut self, visibility: NodeSurfaceSlotVisibility) -> Self {
1816        self.visibility = Some(visibility);
1817        self
1818    }
1819
1820    pub fn with_control(mut self, control: NodeControlDescriptor) -> Self {
1821        self.controls.push(control);
1822        self
1823    }
1824
1825    pub fn with_controls(
1826        mut self,
1827        controls: impl IntoIterator<Item = NodeControlDescriptor>,
1828    ) -> Self {
1829        self.controls.extend(controls);
1830        self
1831    }
1832
1833    pub fn hidden(self) -> Self {
1834        self.with_visibility(NodeSurfaceSlotVisibility::Hidden)
1835    }
1836
1837    pub fn collapsed(self) -> Self {
1838        self.with_visibility(NodeSurfaceSlotVisibility::Collapsed)
1839    }
1840
1841    pub fn key_tail(&self) -> Option<&str> {
1842        self.key.split_once('.').map(|(_, tail)| tail)
1843    }
1844
1845    pub fn data_key(&self) -> Option<&str> {
1846        self.slot.as_deref().or_else(|| {
1847            (self.kind == NodeSurfaceSlotKind::FieldRow)
1848                .then(|| self.key_tail())
1849                .flatten()
1850        })
1851    }
1852
1853    pub fn display_label(&self) -> Option<&str> {
1854        self.label.as_deref().or_else(|| self.key_tail())
1855    }
1856
1857    pub fn order_key(&self) -> i32 {
1858        self.order.unwrap_or(i32::MAX)
1859    }
1860
1861    pub fn is_visible(&self) -> bool {
1862        matches!(
1863            self.visibility,
1864            None | Some(NodeSurfaceSlotVisibility::Visible)
1865        )
1866    }
1867
1868    pub fn is_hidden(&self) -> bool {
1869        matches!(self.visibility, Some(NodeSurfaceSlotVisibility::Hidden))
1870    }
1871
1872    pub fn is_collapsed(&self) -> bool {
1873        matches!(self.visibility, Some(NodeSurfaceSlotVisibility::Collapsed))
1874    }
1875
1876    pub fn is_hidden_or_collapsed(&self) -> bool {
1877        !self.is_visible()
1878    }
1879}
1880
1881/// Minimal adapter-facing slot projection derived from a semantic slot descriptor.
1882#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1883pub struct NodeSurfaceSlotProjection {
1884    pub key: String,
1885    pub kind: NodeSurfaceSlotKind,
1886    pub label: String,
1887    pub value: String,
1888    pub visible: bool,
1889    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1890    pub controls: Vec<NodeControlDescriptor>,
1891}
1892
1893impl NodeSurfaceSlotProjection {
1894    pub fn from_descriptor(
1895        node_data: &Value,
1896        slot: &NodeSurfaceSlotDescriptor,
1897        compact_values: bool,
1898    ) -> Self {
1899        let label = slot.display_label().unwrap_or(slot.key.as_str()).to_owned();
1900        let value = slot_value_preview(node_data, slot, compact_values).unwrap_or_default();
1901        Self {
1902            key: slot.key.clone(),
1903            kind: slot.kind,
1904            label,
1905            value,
1906            visible: slot.is_visible(),
1907            controls: slot.controls.clone(),
1908        }
1909    }
1910}
1911
1912/// Schema for a node kind.
1913#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1914pub struct NodeSchema {
1915    /// Canonical kind key.
1916    pub kind: NodeKindKey,
1917    /// Latest schema version for this kind.
1918    pub latest_kind_version: u32,
1919    /// Kind aliases (renames).
1920    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1921    pub kind_aliases: Vec<NodeKindKey>,
1922
1923    /// UI-facing title.
1924    pub title: String,
1925    /// Category path (for create-node search/palette).
1926    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1927    pub category: Vec<String>,
1928    /// Search keywords.
1929    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1930    pub keywords: Vec<String>,
1931    /// Adapter-facing renderer key.
1932    ///
1933    /// Runtime keeps this as data instead of a component reference so React, Svelte, native, and
1934    /// future adapters can map the key to their own renderer registry.
1935    #[serde(default, skip_serializing_if = "Option::is_none")]
1936    pub renderer_key: Option<String>,
1937    /// Default logical node size for adapters that need an initial rect before measurement.
1938    #[serde(default, skip_serializing_if = "Option::is_none")]
1939    pub default_size: Option<CanvasSize>,
1940    /// Semantic readable-surface budget for adapter-local node internals.
1941    #[serde(default, skip_serializing_if = "node_surface_layout_budget_is_empty")]
1942    pub layout_budget: NodeSurfaceLayoutBudget,
1943
1944    /// Declared ports.
1945    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1946    pub ports: Vec<PortDecl>,
1947
1948    /// Renderer-neutral semantic slots for rich adapter node surfaces.
1949    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1950    pub surface_slots: Vec<NodeSurfaceSlotDescriptor>,
1951    /// Renderer-neutral dynamic row/list descriptors for node-local authoring surfaces.
1952    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1953    pub repeatable_collections: Vec<NodeRepeatableCollectionDescriptor>,
1954    /// Renderer-neutral action descriptors for node-local authoring surfaces.
1955    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1956    pub actions: Vec<NodeActionDescriptor>,
1957    /// Renderer-neutral menu descriptors. Adapters own popup widgets and state.
1958    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1959    pub menus: Vec<MenuDescriptor>,
1960    /// Renderer-neutral inspector descriptors. Adapters own panel widgets and focus.
1961    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1962    pub inspectors: Vec<InspectorDescriptor>,
1963    /// Graph-level property lists exposed as adapter-local blackboard panels.
1964    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1965    pub blackboards: Vec<BlackboardDescriptor>,
1966    /// Renderer-neutral semantic chrome around rich adapter node surfaces.
1967    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1968    pub chrome: Vec<NodeChromeDescriptor>,
1969
1970    /// Default node payload.
1971    #[serde(default)]
1972    pub default_data: Value,
1973}
1974
1975/// Builder for adapter-facing node schemas.
1976#[derive(Debug, Clone)]
1977pub struct NodeSchemaBuilder {
1978    schema: NodeSchema,
1979}
1980
1981/// Error returned when a node cannot be instantiated from schema.
1982#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1983pub enum NodeInstantiationError {
1984    /// The requested node kind is not registered.
1985    #[error("node kind schema not found: {0:?}")]
1986    MissingSchema(NodeKindKey),
1987    /// The caller supplied a different number of port ids than the schema declares.
1988    #[error("port id count mismatch: expected {expected}, got {actual}")]
1989    PortIdCountMismatch { expected: usize, actual: usize },
1990}
1991
1992/// Concrete graph records produced from a node schema.
1993#[derive(Debug, Clone)]
1994pub struct NodeInstantiation {
1995    /// Allocated node id.
1996    pub node_id: NodeId,
1997    /// Node record to add to the graph.
1998    pub node: Node,
1999    /// Port records to add to the graph, in schema/UI order.
2000    pub ports: Vec<(PortId, Port)>,
2001}
2002
2003impl NodeInstantiation {
2004    /// Consumes this instantiation into graph records.
2005    pub fn into_parts(self) -> (NodeId, Node, Vec<(PortId, Port)>) {
2006        (self.node_id, self.node, self.ports)
2007    }
2008
2009    /// Consumes this instantiation into add-node/add-port operations.
2010    pub fn into_ops(self) -> Vec<GraphOp> {
2011        let port_order = self.node.ports.clone();
2012        let mut node = self.node;
2013        node.ports = Vec::new();
2014
2015        let mut ops =
2016            Vec::with_capacity(self.ports.len() + usize::from(!port_order.is_empty()) + 1);
2017        ops.push(GraphOp::AddNode {
2018            id: self.node_id,
2019            node,
2020        });
2021        ops.extend(
2022            self.ports
2023                .into_iter()
2024                .map(|(id, port)| GraphOp::AddPort { id, port }),
2025        );
2026        if !port_order.is_empty() {
2027            ops.push(GraphOp::SetNodePorts {
2028                id: self.node_id,
2029                from: Vec::new(),
2030                to: port_order,
2031            });
2032        }
2033        ops
2034    }
2035
2036    /// Consumes this instantiation into an unlabeled graph transaction.
2037    pub fn into_transaction(self) -> GraphTransaction {
2038        GraphTransaction::from_ops(self.into_ops())
2039    }
2040
2041    /// Consumes this instantiation into a labeled graph transaction.
2042    pub fn into_labeled_transaction(self, label: impl Into<String>) -> GraphTransaction {
2043        self.into_transaction().with_label(label)
2044    }
2045}
2046
2047impl NodeSchema {
2048    /// Starts a node schema builder with renderer-neutral defaults.
2049    pub fn builder(kind: impl Into<NodeKindKey>, title: impl Into<String>) -> NodeSchemaBuilder {
2050        NodeSchemaBuilder {
2051            schema: NodeSchema {
2052                kind: kind.into(),
2053                latest_kind_version: 1,
2054                kind_aliases: Vec::new(),
2055                title: title.into(),
2056                category: Vec::new(),
2057                keywords: Vec::new(),
2058                renderer_key: None,
2059                default_size: None,
2060                layout_budget: NodeSurfaceLayoutBudget::default(),
2061                ports: Vec::new(),
2062                surface_slots: Vec::new(),
2063                repeatable_collections: Vec::new(),
2064                actions: Vec::new(),
2065                menus: Vec::new(),
2066                inspectors: Vec::new(),
2067                blackboards: Vec::new(),
2068                chrome: Vec::new(),
2069                default_data: Value::Null,
2070            },
2071        }
2072    }
2073
2074    /// Instantiates a node and its declared ports with freshly allocated ids.
2075    pub fn instantiate(&self, pos: CanvasPoint) -> NodeInstantiation {
2076        let node_id = NodeId::new();
2077        let port_ids = std::iter::repeat_with(PortId::new)
2078            .take(self.ports.len())
2079            .collect();
2080        self.instantiate_from_port_ids(node_id, pos, port_ids)
2081    }
2082
2083    /// Instantiates a node and its declared ports with caller-provided ids.
2084    pub fn instantiate_with_ids(
2085        &self,
2086        node_id: NodeId,
2087        pos: CanvasPoint,
2088        port_ids: impl IntoIterator<Item = PortId>,
2089    ) -> Result<NodeInstantiation, NodeInstantiationError> {
2090        let port_ids: Vec<PortId> = port_ids.into_iter().collect();
2091        if port_ids.len() != self.ports.len() {
2092            return Err(NodeInstantiationError::PortIdCountMismatch {
2093                expected: self.ports.len(),
2094                actual: port_ids.len(),
2095            });
2096        }
2097
2098        Ok(self.instantiate_from_port_ids(node_id, pos, port_ids))
2099    }
2100
2101    fn instantiate_from_port_ids(
2102        &self,
2103        node_id: NodeId,
2104        pos: CanvasPoint,
2105        port_ids: Vec<PortId>,
2106    ) -> NodeInstantiation {
2107        let ports = self
2108            .ports
2109            .iter()
2110            .zip(port_ids.iter().copied())
2111            .map(|(decl, port_id)| (port_id, decl.instantiate(node_id)))
2112            .collect();
2113
2114        NodeInstantiation {
2115            node_id,
2116            node: Node {
2117                kind: self.kind.clone(),
2118                kind_version: self.latest_kind_version,
2119                pos,
2120                origin: None,
2121                selectable: None,
2122                focusable: None,
2123                draggable: None,
2124                connectable: None,
2125                deletable: None,
2126                parent: None,
2127                extent: None,
2128                expand_parent: None,
2129                size: self.default_size,
2130                hidden: false,
2131                collapsed: false,
2132                ports: port_ids,
2133                data: self.default_data.clone(),
2134            },
2135            ports,
2136        }
2137    }
2138}
2139
2140impl NodeSchemaBuilder {
2141    /// Sets the latest schema version for this node kind.
2142    pub fn latest_kind_version(mut self, version: u32) -> Self {
2143        self.schema.latest_kind_version = version;
2144        self
2145    }
2146
2147    /// Adds one alias for this node kind.
2148    pub fn alias(mut self, alias: impl Into<NodeKindKey>) -> Self {
2149        self.schema.kind_aliases.push(alias.into());
2150        self
2151    }
2152
2153    /// Adds aliases for this node kind.
2154    pub fn aliases(mut self, aliases: impl IntoIterator<Item = impl Into<NodeKindKey>>) -> Self {
2155        self.schema
2156            .kind_aliases
2157            .extend(aliases.into_iter().map(Into::into));
2158        self
2159    }
2160
2161    /// Sets the create-node category path.
2162    pub fn category(mut self, category: impl IntoIterator<Item = impl Into<String>>) -> Self {
2163        self.schema.category = category.into_iter().map(Into::into).collect();
2164        self
2165    }
2166
2167    /// Adds one search keyword.
2168    pub fn keyword(mut self, keyword: impl Into<String>) -> Self {
2169        self.schema.keywords.push(keyword.into());
2170        self
2171    }
2172
2173    /// Adds search keywords.
2174    pub fn keywords(mut self, keywords: impl IntoIterator<Item = impl Into<String>>) -> Self {
2175        self.schema
2176            .keywords
2177            .extend(keywords.into_iter().map(Into::into));
2178        self
2179    }
2180
2181    /// Sets the adapter-owned renderer lookup key.
2182    pub fn renderer_key(mut self, renderer_key: impl Into<String>) -> Self {
2183        self.schema.renderer_key = Some(renderer_key.into());
2184        self
2185    }
2186
2187    /// Sets the fallback logical node size.
2188    pub fn default_size(mut self, size: CanvasSize) -> Self {
2189        self.schema.default_size = Some(size);
2190        self
2191    }
2192
2193    /// Sets the semantic readable-surface budget for adapter-local node internals.
2194    pub fn layout_budget(mut self, budget: NodeSurfaceLayoutBudget) -> Self {
2195        self.schema.layout_budget = budget;
2196        self
2197    }
2198
2199    /// Adds one declared port.
2200    pub fn port(mut self, port: PortDecl) -> Self {
2201        self.schema.ports.push(port);
2202        self
2203    }
2204
2205    /// Adds declared ports.
2206    pub fn ports(mut self, ports: impl IntoIterator<Item = PortDecl>) -> Self {
2207        self.schema.ports.extend(ports);
2208        self
2209    }
2210
2211    /// Adds one renderer-neutral node surface slot.
2212    pub fn surface_slot(mut self, slot: NodeSurfaceSlotDescriptor) -> Self {
2213        self.schema.surface_slots.push(slot);
2214        self
2215    }
2216
2217    /// Adds renderer-neutral node surface slots.
2218    pub fn surface_slots(
2219        mut self,
2220        slots: impl IntoIterator<Item = NodeSurfaceSlotDescriptor>,
2221    ) -> Self {
2222        self.schema.surface_slots.extend(slots);
2223        self
2224    }
2225
2226    /// Adds one renderer-neutral repeatable collection descriptor.
2227    pub fn repeatable_collection(mut self, collection: NodeRepeatableCollectionDescriptor) -> Self {
2228        self.schema.repeatable_collections.push(collection);
2229        self
2230    }
2231
2232    /// Adds renderer-neutral repeatable collection descriptors.
2233    pub fn repeatable_collections(
2234        mut self,
2235        collections: impl IntoIterator<Item = NodeRepeatableCollectionDescriptor>,
2236    ) -> Self {
2237        self.schema.repeatable_collections.extend(collections);
2238        self
2239    }
2240
2241    /// Adds one renderer-neutral action descriptor.
2242    pub fn action(mut self, action: NodeActionDescriptor) -> Self {
2243        self.schema.actions.push(action);
2244        self
2245    }
2246
2247    /// Adds renderer-neutral action descriptors.
2248    pub fn actions(mut self, actions: impl IntoIterator<Item = NodeActionDescriptor>) -> Self {
2249        self.schema.actions.extend(actions);
2250        self
2251    }
2252
2253    /// Adds one renderer-neutral menu descriptor.
2254    pub fn menu(mut self, menu: MenuDescriptor) -> Self {
2255        self.schema.menus.push(menu);
2256        self
2257    }
2258
2259    /// Adds renderer-neutral menu descriptors.
2260    pub fn menus(mut self, menus: impl IntoIterator<Item = MenuDescriptor>) -> Self {
2261        self.schema.menus.extend(menus);
2262        self
2263    }
2264
2265    /// Adds one renderer-neutral inspector descriptor.
2266    pub fn inspector(mut self, inspector: InspectorDescriptor) -> Self {
2267        self.schema.inspectors.push(inspector);
2268        self
2269    }
2270
2271    /// Adds renderer-neutral inspector descriptors.
2272    pub fn inspectors(mut self, inspectors: impl IntoIterator<Item = InspectorDescriptor>) -> Self {
2273        self.schema.inspectors.extend(inspectors);
2274        self
2275    }
2276
2277    /// Adds one renderer-neutral blackboard descriptor.
2278    pub fn blackboard(mut self, blackboard: BlackboardDescriptor) -> Self {
2279        self.schema.blackboards.push(blackboard);
2280        self
2281    }
2282
2283    /// Adds renderer-neutral blackboard descriptors.
2284    pub fn blackboards(
2285        mut self,
2286        blackboards: impl IntoIterator<Item = BlackboardDescriptor>,
2287    ) -> Self {
2288        self.schema.blackboards.extend(blackboards);
2289        self
2290    }
2291
2292    /// Adds one renderer-neutral node chrome descriptor.
2293    pub fn chrome(mut self, chrome: NodeChromeDescriptor) -> Self {
2294        self.schema.chrome.push(chrome);
2295        self
2296    }
2297
2298    /// Adds renderer-neutral node chrome descriptors.
2299    pub fn chromes(mut self, chromes: impl IntoIterator<Item = NodeChromeDescriptor>) -> Self {
2300        self.schema.chrome.extend(chromes);
2301        self
2302    }
2303
2304    /// Sets the default node payload.
2305    pub fn default_data(mut self, data: Value) -> Self {
2306        self.schema.default_data = data;
2307        self
2308    }
2309
2310    /// Builds the schema.
2311    pub fn build(self) -> NodeSchema {
2312        self.schema
2313    }
2314}
2315
2316impl From<NodeSchemaBuilder> for NodeSchema {
2317    fn from(value: NodeSchemaBuilder) -> Self {
2318        value.build()
2319    }
2320}
2321
2322impl PortDecl {
2323    /// Creates a port declaration.
2324    pub fn new(
2325        key: impl Into<PortKey>,
2326        dir: PortDirection,
2327        kind: PortKind,
2328        capacity: PortCapacity,
2329    ) -> Self {
2330        Self {
2331            key: key.into(),
2332            dir,
2333            kind,
2334            capacity,
2335            ty: None,
2336            label: None,
2337            view: PortViewDescriptor::default(),
2338        }
2339    }
2340
2341    /// Creates a single-capacity data input port.
2342    pub fn data_input(key: impl Into<PortKey>) -> Self {
2343        Self::new(key, PortDirection::In, PortKind::Data, PortCapacity::Single)
2344    }
2345
2346    /// Creates a multi-capacity data output port.
2347    pub fn data_output(key: impl Into<PortKey>) -> Self {
2348        Self::new(key, PortDirection::Out, PortKind::Data, PortCapacity::Multi)
2349    }
2350
2351    /// Sets the port type descriptor.
2352    pub fn with_type(mut self, ty: TypeDesc) -> Self {
2353        self.ty = Some(ty);
2354        self
2355    }
2356
2357    /// Sets the adapter-facing label.
2358    pub fn with_label(mut self, label: impl Into<String>) -> Self {
2359        self.label = Some(label.into());
2360        self
2361    }
2362
2363    /// Sets the adapter-facing port view descriptor.
2364    pub fn with_view(mut self, view: PortViewDescriptor) -> Self {
2365        self.view = view;
2366        self
2367    }
2368
2369    /// Places this port on the top side.
2370    pub fn on_top(self) -> Self {
2371        self.with_view(PortViewDescriptor::top())
2372    }
2373
2374    /// Places this port on the right side.
2375    pub fn on_right(self) -> Self {
2376        self.with_view(PortViewDescriptor::right())
2377    }
2378
2379    /// Places this port on the bottom side.
2380    pub fn on_bottom(self) -> Self {
2381        self.with_view(PortViewDescriptor::bottom())
2382    }
2383
2384    /// Places this port on the left side.
2385    pub fn on_left(self) -> Self {
2386        self.with_view(PortViewDescriptor::left())
2387    }
2388
2389    /// Sets deterministic ordering within the selected side/group.
2390    pub fn with_view_order(mut self, order: i32) -> Self {
2391        self.view.order = Some(order);
2392        self
2393    }
2394
2395    /// Groups this port with related handles for adapter presentation.
2396    pub fn with_view_group(mut self, group: impl Into<String>) -> Self {
2397        self.view.group = Some(group.into());
2398        self
2399    }
2400
2401    /// Anchors this port to an adapter-owned region such as a field row.
2402    pub fn with_view_anchor(mut self, anchor: impl Into<String>) -> Self {
2403        self.view.anchor = Some(anchor.into());
2404        self
2405    }
2406
2407    /// Hides this handle from adapter hit testing without removing the semantic port.
2408    pub fn hidden_handle(mut self) -> Self {
2409        self.view.visibility = Some(PortHandleVisibility::Hidden);
2410        self
2411    }
2412
2413    /// Sets the capacity.
2414    pub fn with_capacity(mut self, capacity: PortCapacity) -> Self {
2415        self.capacity = capacity;
2416        self
2417    }
2418
2419    fn instantiate(&self, node: NodeId) -> Port {
2420        Port {
2421            node,
2422            key: self.key.clone(),
2423            dir: self.dir,
2424            kind: self.kind,
2425            capacity: self.capacity,
2426            connectable: None,
2427            connectable_start: None,
2428            connectable_end: None,
2429            ty: self.ty.clone(),
2430            data: Value::Null,
2431        }
2432    }
2433}
2434
2435/// Renderer-neutral node-kind descriptor for adapter palettes and renderer lookup.
2436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2437pub struct NodeKindViewDescriptor {
2438    /// Canonical kind key.
2439    pub kind: NodeKindKey,
2440    /// Adapter-owned renderer lookup key.
2441    pub renderer_key: String,
2442    /// UI-facing title.
2443    pub title: String,
2444    /// Category path for create-node search/palette grouping.
2445    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2446    pub category: Vec<String>,
2447    /// Search keywords.
2448    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2449    pub keywords: Vec<String>,
2450    /// Default logical node size for initial adapter layout before measurement.
2451    #[serde(default, skip_serializing_if = "Option::is_none")]
2452    pub default_size: Option<CanvasSize>,
2453    /// Semantic readable-surface budget for adapter-local node internals.
2454    #[serde(default, skip_serializing_if = "node_surface_layout_budget_is_empty")]
2455    pub layout_budget: NodeSurfaceLayoutBudget,
2456    /// Declared ports.
2457    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2458    pub ports: Vec<PortDecl>,
2459    /// Renderer-neutral semantic slots for rich adapter node surfaces.
2460    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2461    pub surface_slots: Vec<NodeSurfaceSlotDescriptor>,
2462    /// Renderer-neutral dynamic row/list descriptors for node-local authoring surfaces.
2463    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2464    pub repeatable_collections: Vec<NodeRepeatableCollectionDescriptor>,
2465    /// Renderer-neutral action descriptors for node-local authoring surfaces.
2466    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2467    pub actions: Vec<NodeActionDescriptor>,
2468    /// Renderer-neutral menu descriptors. Adapters own popup widgets and state.
2469    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2470    pub menus: Vec<MenuDescriptor>,
2471    /// Renderer-neutral inspector descriptors. Adapters own panel widgets and focus.
2472    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2473    pub inspectors: Vec<InspectorDescriptor>,
2474    /// Graph-level property lists exposed as adapter-local blackboard panels.
2475    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2476    pub blackboards: Vec<BlackboardDescriptor>,
2477    /// Renderer-neutral semantic chrome around rich adapter node surfaces.
2478    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2479    pub chrome: Vec<NodeChromeDescriptor>,
2480    /// Default node payload.
2481    #[serde(default)]
2482    pub default_data: Value,
2483}
2484
2485impl NodeKindViewDescriptor {
2486    pub(crate) fn from_schema(schema: &NodeSchema) -> Self {
2487        Self {
2488            kind: schema.kind.clone(),
2489            renderer_key: schema
2490                .renderer_key
2491                .clone()
2492                .unwrap_or_else(|| schema.kind.0.clone()),
2493            title: schema.title.clone(),
2494            category: schema.category.clone(),
2495            keywords: schema.keywords.clone(),
2496            default_size: schema.default_size,
2497            layout_budget: schema.layout_budget.clone(),
2498            ports: schema.ports.clone(),
2499            surface_slots: schema.surface_slots.clone(),
2500            repeatable_collections: schema.repeatable_collections.clone(),
2501            actions: schema.actions.clone(),
2502            menus: schema.menus.clone(),
2503            inspectors: schema.inspectors.clone(),
2504            blackboards: schema.blackboards.clone(),
2505            chrome: schema.chrome.clone(),
2506            default_data: schema.default_data.clone(),
2507        }
2508    }
2509
2510    pub fn port_decl(&self, key: impl AsRef<str>) -> Option<&PortDecl> {
2511        let key = key.as_ref();
2512        self.ports.iter().find(|decl| decl.key.0 == key)
2513    }
2514
2515    pub fn ports_by_anchor(&self, anchor: impl AsRef<str>) -> Vec<&PortDecl> {
2516        let anchor = anchor.as_ref();
2517        let mut ports: Vec<_> = self
2518            .ports
2519            .iter()
2520            .filter(|decl| decl.view.anchor.as_deref() == Some(anchor))
2521            .collect();
2522        ports.sort_by(|a, b| {
2523            a.view
2524                .order
2525                .unwrap_or(i32::MAX)
2526                .cmp(&b.view.order.unwrap_or(i32::MAX))
2527                .then_with(|| a.key.cmp(&b.key))
2528        });
2529        ports
2530    }
2531
2532    pub fn port_decl_by_anchor(&self, anchor: impl AsRef<str>) -> Option<&PortDecl> {
2533        self.ports_by_anchor(anchor).into_iter().next()
2534    }
2535
2536    pub fn surface_slot(&self, key: impl AsRef<str>) -> Option<&NodeSurfaceSlotDescriptor> {
2537        let key = key.as_ref();
2538        self.surface_slots.iter().find(|slot| slot.key == key)
2539    }
2540
2541    pub fn surface_slots_of_kind(
2542        &self,
2543        kind: NodeSurfaceSlotKind,
2544    ) -> Vec<&NodeSurfaceSlotDescriptor> {
2545        let mut slots: Vec<_> = self
2546            .surface_slots
2547            .iter()
2548            .filter(|slot| slot.kind == kind)
2549            .collect();
2550        slots.sort_by(|a, b| {
2551            a.order_key()
2552                .cmp(&b.order_key())
2553                .then_with(|| a.key.cmp(&b.key))
2554        });
2555        slots
2556    }
2557
2558    pub fn surface_slots_by_anchor(
2559        &self,
2560        anchor: impl AsRef<str>,
2561    ) -> Vec<&NodeSurfaceSlotDescriptor> {
2562        let anchor = anchor.as_ref();
2563        let mut slots: Vec<_> = self
2564            .surface_slots
2565            .iter()
2566            .filter(|slot| slot.anchor.as_deref() == Some(anchor))
2567            .collect();
2568        slots.sort_by(|a, b| {
2569            a.order_key()
2570                .cmp(&b.order_key())
2571                .then_with(|| a.key.cmp(&b.key))
2572        });
2573        slots
2574    }
2575
2576    pub fn surface_slot_by_anchor(
2577        &self,
2578        anchor: impl AsRef<str>,
2579    ) -> Option<&NodeSurfaceSlotDescriptor> {
2580        self.surface_slots_by_anchor(anchor).into_iter().next()
2581    }
2582
2583    pub fn repeatable_collection(
2584        &self,
2585        key: impl AsRef<str>,
2586    ) -> Option<&NodeRepeatableCollectionDescriptor> {
2587        let key = key.as_ref();
2588        self.repeatable_collections
2589            .iter()
2590            .find(|collection| collection.key == key)
2591    }
2592
2593    pub fn repeatable_items_projection(
2594        &self,
2595        node_data: &Value,
2596        collection_key: impl AsRef<str>,
2597    ) -> Vec<NodeRepeatableItemProjection> {
2598        self.repeatable_collection(collection_key)
2599            .map(|collection| collection.item_projections(node_data))
2600            .unwrap_or_default()
2601    }
2602
2603    pub fn action(&self, key: impl AsRef<str>) -> Option<&NodeActionDescriptor> {
2604        let key = key.as_ref();
2605        self.actions.iter().find(|action| action.key == key)
2606    }
2607
2608    pub fn menu(&self, key: impl AsRef<str>) -> Option<&MenuDescriptor> {
2609        let key = key.as_ref();
2610        self.menus.iter().find(|menu| menu.key == key)
2611    }
2612
2613    pub fn inspector(&self, key: impl AsRef<str>) -> Option<&InspectorDescriptor> {
2614        let key = key.as_ref();
2615        self.inspectors
2616            .iter()
2617            .find(|inspector| inspector.key == key)
2618    }
2619
2620    pub fn blackboard(&self, key: impl AsRef<str>) -> Option<&BlackboardDescriptor> {
2621        let key = key.as_ref();
2622        self.blackboards
2623            .iter()
2624            .find(|blackboard| blackboard.key == key)
2625    }
2626
2627    pub fn surface_slots_projection(
2628        &self,
2629        node_data: &Value,
2630        layout_hints: Option<&NodeKitLayoutHints>,
2631        zoom: f32,
2632    ) -> Vec<NodeSurfaceSlotProjection> {
2633        let projection = layout_hints
2634            .map(|layout_hints| NodeSurfaceProjection::from_layout_hints(layout_hints, zoom))
2635            .unwrap_or_else(|| {
2636                NodeSurfaceProjection::from_layout_hints(&NodeKitLayoutHints::default(), zoom)
2637            });
2638
2639        let mut slots = self
2640            .surface_slots
2641            .iter()
2642            .filter(|slot| slot.is_visible())
2643            .map(|slot| {
2644                NodeSurfaceSlotProjection::from_descriptor(
2645                    node_data,
2646                    slot,
2647                    projection.compact_values,
2648                )
2649            })
2650            .collect::<Vec<_>>();
2651
2652        if !projection.expand_all_slots && slots.len() > projection.slot_limit {
2653            slots.truncate(projection.slot_limit);
2654        }
2655
2656        slots
2657    }
2658}
2659
2660fn slot_value_preview(
2661    node_data: &Value,
2662    slot: &NodeSurfaceSlotDescriptor,
2663    compact_values: bool,
2664) -> Option<String> {
2665    let value = semantic_slot_value(node_data, slot)?;
2666    let preview = json_value_preview(value, compact_values);
2667    (!preview.is_empty()).then_some(preview)
2668}
2669
2670fn semantic_slot_value<'a>(
2671    node_data: &'a Value,
2672    slot: &NodeSurfaceSlotDescriptor,
2673) -> Option<&'a Value> {
2674    let key = slot.data_key()?;
2675    if slot.kind == NodeSurfaceSlotKind::FieldRow
2676        && let Some(fields) = node_data.get("fields").and_then(Value::as_object)
2677        && let Some(value) = fields.get(key)
2678    {
2679        return Some(value);
2680    }
2681    semantic_json_lookup(node_data, key)
2682}
2683
2684fn semantic_json_lookup<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
2685    let mut current = value;
2686    for segment in path.split('.') {
2687        current = current.get(segment)?;
2688    }
2689    Some(current)
2690}
2691
2692fn json_value_preview(value: &Value, compact_values: bool) -> String {
2693    match value {
2694        Value::String(text) => text.clone(),
2695        Value::Bool(value) => value.to_string(),
2696        Value::Number(value) => value.to_string(),
2697        Value::Array(items) => {
2698            let preview = items
2699                .iter()
2700                .take(if compact_values { 1 } else { 2 })
2701                .map(|value| json_value_preview(value, compact_values))
2702                .filter(|text| !text.is_empty())
2703                .collect::<Vec<_>>()
2704                .join(" · ");
2705            if preview.is_empty() {
2706                format!("{} items", items.len())
2707            } else if !compact_values && items.len() > 2 {
2708                format!("{preview} …")
2709            } else {
2710                preview
2711            }
2712        }
2713        Value::Object(map) => {
2714            if let Some(text) = map.get("label").and_then(Value::as_str) {
2715                return text.to_owned();
2716            }
2717            if let Some(text) = map.get("title").and_then(Value::as_str) {
2718                return text.to_owned();
2719            }
2720            let preview = map
2721                .iter()
2722                .take(if compact_values { 1 } else { 2 })
2723                .map(|(key, value)| format!("{key}: {}", json_value_preview(value, compact_values)))
2724                .collect::<Vec<_>>()
2725                .join(" · ");
2726            if preview.is_empty() {
2727                "{}".to_owned()
2728            } else {
2729                preview
2730            }
2731        }
2732        Value::Null => String::new(),
2733    }
2734}