Skip to main content

jellyflow_runtime/schema/kit/
builtins.rs

1use serde_json::json;
2
3use super::{
4    NodeKitContentDensity, NodeKitFixture, NodeKitFixtureEdge, NodeKitFixtureNode,
5    NodeKitLayoutHints, NodeKitManifest, NodeKitRegistry,
6};
7use crate::schema::{
8    ActionIntent, ActionTarget, BlackboardDescriptor, InspectorDescriptor, InspectorTarget,
9    MenuDescriptor, MenuSurface, NodeActionDescriptor, NodeChromeDescriptor, NodeChromePlacement,
10    NodeControlBinding, NodeControlDescriptor, NodeControlEditability, NodeControlOption,
11    NodeControlOptionSource, NodeControlPresentation, NodeControlValidation,
12    NodeControlValidationRule, NodeRepeatableAnchorRule, NodeRepeatableCollectionDescriptor,
13    NodeSchema, NodeSurfaceLayoutBudget, NodeSurfaceOverflowIndicator, NodeSurfaceSlotDescriptor,
14    PortDecl, PortViewDescriptor,
15};
16use jellyflow_core::core::{
17    CanvasPoint, CanvasSize, EdgeKind, EdgeLabelAnchor, EdgeViewDescriptor, PortCapacity,
18    PortDirection, PortKind,
19};
20use jellyflow_core::types::TypeDesc;
21
22pub fn builtin_node_kits() -> NodeKitRegistry {
23    let mut registry = NodeKitRegistry::new();
24    registry.register(workflow_automation_manifest());
25    registry.register(shader_blueprint_manifest());
26    registry.register(erd_table_manifest());
27    registry.register(mind_map_knowledge_canvas_manifest());
28    registry
29}
30
31pub fn workflow_automation_manifest() -> NodeKitManifest {
32    NodeKitManifest::new("workflow.automation", "Workflow automation")
33        .with_supported_adapter("egui")
34        .with_supported_adapter("proof")
35        .with_supported_adapter("gpui")
36        .with_capability("workflow")
37        .with_capability("automation")
38        .with_layout_hints(
39            NodeKitLayoutHints::default()
40                .with_zoom_range(0.72, 0.9)
41                .with_field_spacing(10.0)
42                .with_action_spacing(8.0)
43                .with_measurement_note(
44                    "Compact action rows and badge blocks should collapse at low zoom.",
45                ),
46        )
47        .recipe(workflow_start_schema())
48        .recipe(workflow_task_schema())
49        .recipe(workflow_trigger_schema())
50        .recipe(workflow_tool_schema())
51        .recipe(workflow_llm_schema())
52        .recipe(workflow_decision_schema())
53        .recipe(workflow_switch_schema())
54        .recipe(workflow_output_schema())
55        .recipe(workflow_return_schema())
56        .recipe(workflow_error_schema())
57        .fixture(workflow_fixture())
58}
59
60pub fn erd_table_manifest() -> NodeKitManifest {
61    NodeKitManifest::new("erd.table", "ERD table")
62        .with_supported_adapter("egui")
63        .with_supported_adapter("proof")
64        .with_supported_adapter("gpui")
65        .with_capability("erd")
66        .with_capability("table")
67        .with_layout_hints(
68            NodeKitLayoutHints::default()
69                .with_zoom_range(0.72, 0.88)
70                .with_field_spacing(6.0)
71                .with_measurement_note("Field rows should stay aligned to anchored ports."),
72        )
73        .recipe(erd_table_schema())
74        .fixture(erd_fixture())
75}
76
77pub fn shader_blueprint_manifest() -> NodeKitManifest {
78    NodeKitManifest::new("shader.blueprint", "Shader and blueprint")
79        .with_supported_adapter("egui")
80        .with_supported_adapter("proof")
81        .with_supported_adapter("gpui")
82        .with_capability("shader")
83        .with_capability("blueprint")
84        .with_layout_hints(
85            NodeKitLayoutHints::default()
86                .with_zoom_range(0.74, 0.92)
87                .with_field_spacing(6.0)
88                .with_measurement_note(
89                    "Typed port rails and preview regions should preserve handle alignment.",
90                ),
91        )
92        .recipe(shader_texture_sample_schema())
93        .recipe(shader_mix_schema())
94        .fixture(shader_fixture())
95}
96
97pub fn mind_map_knowledge_canvas_manifest() -> NodeKitManifest {
98    NodeKitManifest::new("mind-map.knowledge-canvas", "Mind map and knowledge canvas")
99        .with_supported_adapter("egui")
100        .with_supported_adapter("proof")
101        .with_supported_adapter("gpui")
102        .with_capability("mind-map")
103        .with_capability("knowledge-canvas")
104        .with_layout_hints(
105            NodeKitLayoutHints::default()
106                .with_zoom_range(0.68, 0.88)
107                .with_measurement_note(
108                    "Topic, summary, and source nodes should degrade to shells with stable anchors.",
109                ),
110        )
111        .recipe(mind_topic_schema())
112        .recipe(mind_idea_schema())
113        .recipe(source_card_schema())
114        .fixture(mind_map_fixture())
115}
116
117fn product_layout_budget(
118    min_readable_size: CanvasSize,
119    preferred_size: CanvasSize,
120    slot_line_budget: usize,
121    control_line_budget: usize,
122    repeatable_visible_items: Option<usize>,
123) -> NodeSurfaceLayoutBudget {
124    let budget = NodeSurfaceLayoutBudget::default()
125        .with_min_readable_size(min_readable_size)
126        .with_preferred_size(preferred_size)
127        .with_slot_line_budget(slot_line_budget)
128        .with_control_line_budget(control_line_budget)
129        .with_overflow_indicator(NodeSurfaceOverflowIndicator::Count)
130        .with_density_priority([
131            NodeKitContentDensity::Full,
132            NodeKitContentDensity::Regular,
133            NodeKitContentDensity::Compact,
134        ]);
135
136    if let Some(items) = repeatable_visible_items {
137        budget.with_repeatable_visible_items(items)
138    } else {
139        budget
140    }
141}
142
143fn workflow_trigger_schema() -> NodeSchema {
144    NodeSchema::builder("demo.trigger", "Trigger")
145        .category(["Automation", "Workflow"])
146        .keywords(["webhook", "schedule", "event"])
147        .renderer_key("data-card")
148        .default_size(CanvasSize {
149            width: 208.0,
150            height: 96.0,
151        })
152        .port(exec_output("event").on_right().with_view_group("exec"))
153        .port(
154            data_output("payload")
155                .on_bottom()
156                .with_view_group("data")
157                .with_view_order(1),
158        )
159        .default_data(json!({ "title": "Trigger", "summary": "Starts an automation" }))
160        .build()
161}
162
163fn workflow_start_schema() -> NodeSchema {
164    NodeSchema::builder("demo.start", "Start")
165        .category(["Workflow"])
166        .renderer_key("data-card")
167        .default_size(CanvasSize {
168            width: 176.0,
169            height: 80.0,
170        })
171        .port(output_port("out").on_right())
172        .default_data(json!({ "title": "Start", "summary": "Entry point" }))
173        .build()
174}
175
176fn workflow_task_schema() -> NodeSchema {
177    NodeSchema::builder("demo.task", "Task")
178        .category(["Workflow"])
179        .renderer_key("task-card")
180        .default_size(CanvasSize {
181            width: 208.0,
182            height: 96.0,
183        })
184        .port(PortDecl::data_input("in").with_label("in").on_left())
185        .port(PortDecl::data_output("out").with_label("out").on_right())
186        .default_data(json!({ "title": "Task", "summary": "Run a unit of work" }))
187        .build()
188}
189
190fn workflow_tool_schema() -> NodeSchema {
191    NodeSchema::builder("demo.tool", "Tool")
192        .category(["Automation", "Workflow"])
193        .keywords(["api", "function", "action"])
194        .renderer_key("task-card")
195        .default_size(CanvasSize {
196            width: 208.0,
197            height: 104.0,
198        })
199        .port(exec_input("in").on_left().with_view_group("exec"))
200        .port(exec_output("out").on_right().with_view_group("exec"))
201        .port(
202            data_input("args")
203                .on_top()
204                .with_view_group("data")
205                .with_view_order(0),
206        )
207        .port(
208            data_output("result")
209                .on_bottom()
210                .with_view_group("data")
211                .with_view_order(0),
212        )
213        .default_data(json!({ "title": "Tool", "summary": "Runs an external action" }))
214        .build()
215}
216
217fn workflow_llm_schema() -> NodeSchema {
218    NodeSchema::builder("demo.llm", "LLM")
219        .category(["Automation", "AI"])
220        .keywords(["prompt", "model", "dify"])
221        .renderer_key("decision-card")
222        .default_size(CanvasSize {
223            width: 228.0,
224            height: 196.0,
225        })
226        .layout_budget(product_layout_budget(
227            CanvasSize {
228                width: 340.0,
229                height: 288.0,
230            },
231            CanvasSize {
232                width: 364.0,
233                height: 312.0,
234            },
235            4,
236            3,
237            Some(3),
238        ))
239        .port(exec_input("in").on_left().with_view_group("exec"))
240        .port(exec_output("out").on_right().with_view_group("exec"))
241        .port(
242            data_input("prompt")
243                .on_top()
244                .with_view_anchor("field.prompt")
245                .with_view_group("parameters")
246                .with_view_order(0),
247        )
248        .port(
249            data_output("completion")
250                .on_bottom()
251                .with_view_anchor("field.completion")
252                .with_view_group("outputs")
253                .with_view_order(0),
254        )
255        .surface_slot(
256            NodeSurfaceSlotDescriptor::field_row("field.prompt")
257                .with_label("Prompt")
258                .with_slot("prompt")
259                .with_anchor("field.prompt")
260                .with_lane("parameters")
261                .with_order(0)
262                .with_control(
263                    NodeControlDescriptor::text_area("control.prompt")
264                        .with_label("Prompt")
265                        .with_binding(NodeControlBinding::slot("prompt"))
266                        .required()
267                        .with_placeholder("Use {{ variable }} references"),
268                )
269                .with_control(
270                    NodeControlDescriptor::variable_picker("control.prompt_variable")
271                        .with_label("Variable")
272                        .with_binding(NodeControlBinding::graph_symbol("workflow.inputs"))
273                        .with_option_source(NodeControlOptionSource::Variables),
274                ),
275        )
276        .surface_slot(
277            NodeSurfaceSlotDescriptor::field_row("field.completion")
278                .with_label("Completion")
279                .with_slot("completion")
280                .with_anchor("field.completion")
281                .with_lane("outputs")
282                .with_order(1)
283                .with_control(
284                    NodeControlDescriptor::text_area("control.completion")
285                        .with_label("Completion")
286                        .with_binding(NodeControlBinding::slot("completion"))
287                        .read_only(),
288                ),
289        )
290        .surface_slot(
291            NodeSurfaceSlotDescriptor::badge("badge.model")
292                .with_label("Model")
293                .with_slot("meta.model")
294                .with_anchor("meta.model")
295                .with_order(0)
296                .with_control(llm_model_select_control()),
297        )
298        .surface_slot(
299            NodeSurfaceSlotDescriptor::metric_badge("metric.latency")
300                .with_label("Latency")
301                .with_slot("metrics.latency")
302                .with_anchor("metric.latency")
303                .with_order(1),
304        )
305        .surface_slot(
306            NodeSurfaceSlotDescriptor::config_group("config.model")
307                .with_label("Config")
308                .with_slot("config.model")
309                .with_anchor("config.model")
310                .with_order(1)
311                .with_controls([
312                    llm_model_select_control(),
313                    NodeControlDescriptor::slider("control.temperature")
314                        .with_label("Temperature")
315                        .with_binding(NodeControlBinding::data_path("config.model.temperature"))
316                        .with_validation_rule(NodeControlValidationRule::Range {
317                            min: Some(0.0),
318                            max: Some(2.0),
319                        })
320                        .with_presentation(
321                            NodeControlPresentation::default()
322                                .with_unit("temp")
323                                .compact_label(),
324                        ),
325                    NodeControlDescriptor::toggle("control.stream")
326                        .with_label("Stream")
327                        .with_binding(NodeControlBinding::data_path("config.model.stream")),
328                    NodeControlDescriptor::asset("control.knowledge_base")
329                        .with_label("Knowledge base")
330                        .with_binding(NodeControlBinding::data_path("config.model.knowledge_base"))
331                        .with_option_source(NodeControlOptionSource::Assets),
332                ]),
333        )
334        .surface_slot(
335            NodeSurfaceSlotDescriptor::nested_region("nested.policy")
336                .with_label("Policy")
337                .with_slot("nested.policy")
338                .with_anchor("nested.policy")
339                .with_order(2),
340        )
341        .surface_slot(
342            NodeSurfaceSlotDescriptor::status_banner("status.validation")
343                .with_label("Status")
344                .with_slot("status.validation")
345                .with_anchor("status.validation")
346                .with_order(3),
347        )
348        .surface_slot(
349            NodeSurfaceSlotDescriptor::action_row("actions.primary")
350                .with_label("Actions")
351                .with_slot("actions.primary")
352                .with_anchor("actions.primary")
353                .with_order(4)
354                .with_control(
355                    NodeControlDescriptor::toggle("control.enable_run")
356                        .with_label("Run enabled")
357                        .with_binding(NodeControlBinding::data_path("actions.primary.enabled"))
358                        .with_editability(
359                            NodeControlEditability::default()
360                                .disabled("Requires a valid model and prompt"),
361                        ),
362                ),
363        )
364        .action(
365            NodeActionDescriptor::new(
366                "action.llm.run",
367                "Run",
368                ActionTarget::Node {
369                    node_kind: "demo.llm".to_owned(),
370                },
371                ActionIntent::RunNode,
372            )
373            .with_group("primary")
374            .with_order(0)
375            .with_icon_key("play"),
376        )
377        .action(
378            NodeActionDescriptor::new(
379                "action.insert.llm",
380                "Insert LLM",
381                ActionTarget::DroppedWire {
382                    source_port_key: Some("completion".to_owned()),
383                },
384                ActionIntent::InsertNode {
385                    node_kind: "demo.llm".to_owned(),
386                },
387            )
388            .with_group("create")
389            .with_order(0),
390        )
391        .menu(
392            MenuDescriptor::new("menu.dropped_wire.llm", MenuSurface::DroppedWire)
393                .with_label("Insert compatible node")
394                .with_action_key("action.insert.llm"),
395        )
396        .menu(
397            MenuDescriptor::new("menu.node.llm", MenuSurface::Node)
398                .with_label("LLM")
399                .with_action_key("action.llm.run"),
400        )
401        .inspector(
402            InspectorDescriptor::new(
403                "inspector.llm",
404                InspectorTarget::Node {
405                    node_kind: "demo.llm".to_owned(),
406                },
407            )
408            .with_label("LLM")
409            .with_control(
410                NodeControlDescriptor::select("inspector.model")
411                    .with_label("Model")
412                    .with_binding(NodeControlBinding::data_path("meta.model"))
413                    .with_options([
414                        NodeControlOption::new("gpt-4.1-mini", "GPT 4.1 Mini"),
415                        NodeControlOption::new("gpt-4.1", "GPT 4.1"),
416                    ]),
417            )
418            .with_action_key("action.llm.run"),
419        )
420        .repeatable_collection(
421            NodeRepeatableCollectionDescriptor::new("llm.params", "params", "id")
422                .with_label("Parameters")
423                .with_empty_label("No parameters")
424                .with_item_template_slot(
425                    NodeSurfaceSlotDescriptor::field_row("param")
426                        .with_label("Parameter")
427                        .with_slot("params")
428                        .with_control(
429                            NodeControlDescriptor::text_input("control.param.name")
430                                .with_label("Name")
431                                .with_binding(NodeControlBinding::data_path("name")),
432                        )
433                        .with_control(
434                            NodeControlDescriptor::expression("control.param.value")
435                                .with_label("Value")
436                                .with_binding(NodeControlBinding::data_path("value"))
437                                .with_validation_rule(NodeControlValidationRule::ExpressionShape {
438                                    language: Some("jinja".to_owned()),
439                                }),
440                        ),
441                )
442                .with_anchor_rule(NodeRepeatableAnchorRule::new("param", "param"))
443                .with_min_items(0)
444                .with_max_items(12)
445                .reorderable()
446                .with_add_action("action.param.add")
447                .with_remove_action("action.param.remove")
448                .with_reorder_action("action.param.reorder"),
449        )
450        .action(
451            NodeActionDescriptor::new(
452                "action.param.add",
453                "Add parameter",
454                ActionTarget::Node {
455                    node_kind: "demo.llm".to_owned(),
456                },
457                ActionIntent::AddRepeatableItem {
458                    collection_key: "llm.params".to_owned(),
459                },
460            )
461            .with_group("parameters"),
462        )
463        .action(
464            NodeActionDescriptor::new(
465                "action.param.remove",
466                "Remove parameter",
467                ActionTarget::Node {
468                    node_kind: "demo.llm".to_owned(),
469                },
470                ActionIntent::RemoveRepeatableItem {
471                    collection_key: "llm.params".to_owned(),
472                    item_id: String::new(),
473                },
474            )
475            .with_group("parameters")
476            .danger(),
477        )
478        .action(
479            NodeActionDescriptor::new(
480                "action.param.reorder",
481                "Reorder parameter",
482                ActionTarget::Node {
483                    node_kind: "demo.llm".to_owned(),
484                },
485                ActionIntent::ReorderRepeatableItem {
486                    collection_key: "llm.params".to_owned(),
487                    item_id: String::new(),
488                },
489            )
490            .with_group("parameters"),
491        )
492        .inspector(
493            InspectorDescriptor::new(
494                "inspector.param.topic",
495                InspectorTarget::RepeatableItem {
496                    collection_key: "llm.params".to_owned(),
497                    item_id: "topic".to_owned(),
498                },
499            )
500            .with_label("Parameter")
501            .with_control(
502                NodeControlDescriptor::expression("inspector.param.value")
503                    .with_label("Value")
504                    .with_binding(NodeControlBinding::data_path("value")),
505            ),
506        )
507        .chrome(NodeChromeDescriptor::resizer("resize.corner").with_order(0))
508        .chrome(
509            NodeChromeDescriptor::toolbar("toolbar.primary", NodeChromePlacement::TopRight)
510                .with_label("Node tools")
511                .with_renderer_key("node-toolbar")
512                .with_icon_key("settings")
513                .with_order(10),
514        )
515        .chrome(
516            NodeChromeDescriptor::status_strip("status.run", NodeChromePlacement::InsideFooter)
517                .with_label("Ready")
518                .with_renderer_key("run-status")
519                .with_icon_key("activity")
520                .with_order(20),
521        )
522        .chrome(
523            NodeChromeDescriptor::run_action_strip("actions.run", NodeChromePlacement::Bottom)
524                .with_label("Run")
525                .with_renderer_key("run-actions")
526                .with_icon_key("play")
527                .with_order(30),
528        )
529        .default_data(json!({
530            "title": "LLM",
531            "summary": "Prompt, model, tools, and variables",
532            "meta": {
533                "model": "gpt-4.1-mini"
534            },
535            "metrics": {
536                "latency": "420ms"
537            },
538            "config": {
539                "model": {
540                    "temperature": 0.2,
541                    "tools": "retrieval",
542                    "stream": true,
543                    "knowledge_base": "product-docs"
544                }
545            },
546            "status": {
547                "validation": "Ready"
548            },
549            "nested": {
550                "policy": {
551                    "guardrails": "Block PII",
552                    "response": "Return structured route"
553                }
554            },
555            "actions": {
556                "primary": {
557                    "enabled": false,
558                    "items": ["Test prompt", "Open trace", "Copy config"]
559                }
560            },
561            "fields": {
562                "prompt": "Customer intake + policy",
563                "completion": "Priority and route"
564            },
565            "params": [
566                { "id": "topic", "name": "topic", "value": "{{ customer.topic }}" },
567                { "id": "priority", "name": "priority", "value": "{{ intake.priority }}" }
568            ]
569        }))
570        .build()
571}
572
573fn llm_model_select_control() -> NodeControlDescriptor {
574    let model_values = vec![
575        json!("gpt-4.1-mini"),
576        json!("gpt-4.1"),
577        json!("local-llama"),
578    ];
579
580    NodeControlDescriptor::select("control.model")
581        .with_label("Model")
582        .with_binding(NodeControlBinding::data_path("meta.model"))
583        .with_options([
584            NodeControlOption::new("gpt-4.1-mini", "GPT 4.1 Mini"),
585            NodeControlOption::new("gpt-4.1", "GPT 4.1"),
586            NodeControlOption::new("local-llama", "Local Llama"),
587        ])
588        .with_validation(NodeControlValidation::default().required().with_rule(
589            NodeControlValidationRule::EnumValues {
590                values: model_values,
591            },
592        ))
593}
594
595fn workflow_decision_schema() -> NodeSchema {
596    NodeSchema::builder("demo.decision", "Decision")
597        .category(["Workflow"])
598        .renderer_key("decision-card")
599        .default_size(CanvasSize {
600            width: 208.0,
601            height: 104.0,
602        })
603        .port(input_port("in"))
604        .port(output_port("yes").on_top().with_view_order(0))
605        .port(output_port("no").on_bottom().with_view_order(1))
606        .surface_slot(
607            NodeSurfaceSlotDescriptor::badge("badge.branch")
608                .with_label("Branch")
609                .with_slot("meta.branch")
610                .with_anchor("meta.branch")
611                .with_order(0),
612        )
613        .default_data(json!({ "title": "Decision", "summary": "Branch the flow" }))
614        .build()
615}
616
617fn workflow_switch_schema() -> NodeSchema {
618    NodeSchema::builder("demo.switch", "Switch")
619        .category(["Automation", "Workflow"])
620        .keywords(["branch", "condition", "router"])
621        .renderer_key("decision-card")
622        .default_size(CanvasSize {
623            width: 208.0,
624            height: 104.0,
625        })
626        .port(exec_input("in").on_left().with_view_group("exec"))
627        .port(exec_output("yes").on_top().with_view_order(0))
628        .port(exec_output("no").on_bottom().with_view_order(1))
629        .default_data(json!({ "title": "Switch", "summary": "Branch execution" }))
630        .build()
631}
632
633fn workflow_output_schema() -> NodeSchema {
634    NodeSchema::builder("demo.output", "Output")
635        .alias("demo.workflow_output")
636        .category(["Workflow"])
637        .renderer_key("output-card")
638        .default_size(CanvasSize {
639            width: 208.0,
640            height: 96.0,
641        })
642        .port(PortDecl::data_input("in").with_label("in").on_left())
643        .default_data(json!({ "title": "Output", "summary": "Publish the result" }))
644        .build()
645}
646
647fn workflow_return_schema() -> NodeSchema {
648    NodeSchema::builder("demo.workflow_output", "Workflow output")
649        .category(["Automation", "Workflow"])
650        .keywords(["return", "response", "result"])
651        .renderer_key("output-card")
652        .default_size(CanvasSize {
653            width: 208.0,
654            height: 96.0,
655        })
656        .port(exec_input("in").on_left().with_view_group("exec"))
657        .port(
658            data_input("result")
659                .on_top()
660                .with_view_group("data")
661                .with_view_order(0),
662        )
663        .default_data(json!({ "title": "Workflow output", "summary": "Returns data to caller" }))
664        .build()
665}
666
667fn workflow_error_schema() -> NodeSchema {
668    NodeSchema::builder("demo.error", "Error")
669        .category(["Automation", "Workflow"])
670        .keywords(["failure", "retry", "fallback"])
671        .renderer_key("output-card")
672        .default_size(CanvasSize {
673            width: 208.0,
674            height: 96.0,
675        })
676        .port(exec_input("error").on_left().with_view_group("exec"))
677        .port(exec_output("out").on_right().with_view_group("exec"))
678        .default_data(json!({ "title": "Error path", "summary": "Retry or recover" }))
679        .build()
680}
681
682fn erd_table_schema() -> NodeSchema {
683    NodeSchema::builder("demo.table", "Table")
684        .category(["ERD"])
685        .keywords(["database", "schema", "relation"])
686        .renderer_key("table-card")
687        .default_size(CanvasSize {
688            width: 226.0,
689            height: 186.0,
690        })
691        .layout_budget(product_layout_budget(
692            CanvasSize {
693                width: 420.0,
694                height: 330.0,
695            },
696            CanvasSize {
697                width: 448.0,
698                height: 356.0,
699            },
700            4,
701            2,
702            Some(4),
703        ))
704        .port(
705            data_input("fk")
706                .with_label("foreign key")
707                .on_left()
708                .with_view_anchor("field.foreign_key")
709                .with_view_group("fields")
710                .with_view_order(0),
711        )
712        .port(
713            data_output("pk")
714                .with_label("primary key")
715                .on_right()
716                .with_view_anchor("field.primary_key")
717                .with_view_group("fields")
718                .with_view_order(0),
719        )
720        .surface_slot(
721            NodeSurfaceSlotDescriptor::field_row("field.primary_key")
722                .with_label("Primary key")
723                .with_slot("primary_key")
724                .with_anchor("field.primary_key")
725                .with_lane("fields")
726                .with_order(0)
727                .with_control(
728                    NodeControlDescriptor::text_input("control.primary_key.name")
729                        .with_label("Name")
730                        .with_binding(NodeControlBinding::slot("primary_key"))
731                        .required(),
732                ),
733        )
734        .surface_slot(
735            NodeSurfaceSlotDescriptor::field_row("field.field")
736                .with_label("Field")
737                .with_slot("field")
738                .with_anchor("field.field")
739                .with_lane("fields")
740                .with_order(1)
741                .with_controls([
742                    NodeControlDescriptor::text_input("control.field.name")
743                        .with_label("Name")
744                        .with_binding(NodeControlBinding::slot("field")),
745                    NodeControlDescriptor::select("control.field.type")
746                        .with_label("Type")
747                        .with_binding(NodeControlBinding::data_path("schema.field.type"))
748                        .with_options([
749                            NodeControlOption::new("text", "Text"),
750                            NodeControlOption::new("integer", "Integer"),
751                            NodeControlOption::new("uuid", "UUID"),
752                        ]),
753                ]),
754        )
755        .surface_slot(
756            NodeSurfaceSlotDescriptor::field_row("field.foreign_key")
757                .with_label("Foreign key")
758                .with_slot("foreign_key")
759                .with_anchor("field.foreign_key")
760                .with_lane("fields")
761                .with_order(2)
762                .with_control(
763                    NodeControlDescriptor::port_binding("control.foreign_key.binding")
764                        .with_label("Relation")
765                        .with_binding(NodeControlBinding::port_anchor("field.foreign_key")),
766                ),
767        )
768        .surface_slot(
769            NodeSurfaceSlotDescriptor::badge("badge.cardinality")
770                .with_label("1:N")
771                .with_slot("meta.cardinality")
772                .with_anchor("meta.cardinality")
773                .with_order(0),
774        )
775        .surface_slot(
776            NodeSurfaceSlotDescriptor::metric_badge("metric.rows")
777                .with_label("Rows")
778                .with_slot("metrics.rows")
779                .with_anchor("metric.rows")
780                .with_order(1),
781        )
782        .surface_slot(
783            NodeSurfaceSlotDescriptor::action_row("actions.table")
784                .with_label("Actions")
785                .with_slot("actions.table")
786                .with_anchor("actions.table")
787                .with_order(2),
788        )
789        .repeatable_collection(
790            NodeRepeatableCollectionDescriptor::new("table.columns", "columns", "id")
791                .with_label("Columns")
792                .with_empty_label("No columns")
793                .with_item_template_slot(
794                    NodeSurfaceSlotDescriptor::field_row("column")
795                        .with_label("Column")
796                        .with_slot("columns")
797                        .with_control(
798                            NodeControlDescriptor::text_input("control.column.name")
799                                .with_label("Name")
800                                .with_binding(NodeControlBinding::data_path("name")),
801                        )
802                        .with_control(
803                            NodeControlDescriptor::select("control.column.type")
804                                .with_label("Type")
805                                .with_binding(NodeControlBinding::data_path("ty"))
806                                .with_options([
807                                    NodeControlOption::new("uuid", "UUID"),
808                                    NodeControlOption::new("text", "Text"),
809                                    NodeControlOption::new("integer", "Integer"),
810                                ]),
811                        ),
812                )
813                .with_anchor_rule(
814                    NodeRepeatableAnchorRule::new("field.column", "field.column")
815                        .with_port_key_path("port"),
816                )
817                .with_min_items(1)
818                .with_max_items(16)
819                .reorderable()
820                .with_add_action("action.column.add")
821                .with_remove_action("action.column.remove")
822                .with_reorder_action("action.column.reorder"),
823        )
824        .action(
825            NodeActionDescriptor::new(
826                "action.column.add",
827                "Add column",
828                ActionTarget::Node {
829                    node_kind: "demo.table".to_owned(),
830                },
831                ActionIntent::AddRepeatableItem {
832                    collection_key: "table.columns".to_owned(),
833                },
834            )
835            .with_group("columns")
836            .with_order(0),
837        )
838        .action(
839            NodeActionDescriptor::new(
840                "action.column.remove",
841                "Remove column",
842                ActionTarget::Node {
843                    node_kind: "demo.table".to_owned(),
844                },
845                ActionIntent::RemoveRepeatableItem {
846                    collection_key: "table.columns".to_owned(),
847                    item_id: String::new(),
848                },
849            )
850            .with_group("columns")
851            .danger(),
852        )
853        .action(
854            NodeActionDescriptor::new(
855                "action.column.reorder",
856                "Reorder column",
857                ActionTarget::Node {
858                    node_kind: "demo.table".to_owned(),
859                },
860                ActionIntent::ReorderRepeatableItem {
861                    collection_key: "table.columns".to_owned(),
862                    item_id: String::new(),
863                },
864            )
865            .with_group("columns"),
866        )
867        .menu(
868            MenuDescriptor::new("menu.table", MenuSurface::Node).with_action_keys([
869                "action.column.add",
870                "action.column.remove",
871                "action.column.reorder",
872            ]),
873        )
874        .inspector(
875            InspectorDescriptor::new(
876                "inspector.column.email",
877                InspectorTarget::RepeatableItem {
878                    collection_key: "table.columns".to_owned(),
879                    item_id: "email".to_owned(),
880                },
881            )
882            .with_label("Column")
883            .with_control(
884                NodeControlDescriptor::text_input("inspector.column.name")
885                    .with_label("Name")
886                    .with_binding(NodeControlBinding::data_path("name")),
887            ),
888        )
889        .default_data(json!({
890            "title": "Table",
891            "summary": "id · field · field",
892            "meta": { "cardinality": "1:N" },
893            "metrics": { "rows": "12k" },
894            "actions": { "table": ["Add column", "Inspect relation"] },
895            "field_order": ["primary_key", "field", "foreign_key"],
896            "fields": {
897                "primary_key": "id",
898                "field": "field",
899                "foreign_key": "field_id"
900            },
901            "schema": {
902                "field": {
903                    "type": "text"
904                }
905            },
906            "columns": [
907                { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
908                { "id": "email", "name": "email", "ty": "text", "port": "field_email" },
909                { "id": "user_id", "name": "user_id", "ty": "uuid", "port": "fk" }
910            ]
911        }))
912        .build()
913}
914
915fn shader_texture_sample_schema() -> NodeSchema {
916    NodeSchema::builder("demo.shader.texture_sample", "Texture Sample")
917        .category(["Shader", "Blueprint"])
918        .keywords(["shader", "texture", "unreal", "graph"])
919        .renderer_key("shader-card")
920        .default_size(CanvasSize {
921            width: 224.0,
922            height: 156.0,
923        })
924        .layout_budget(product_layout_budget(
925            CanvasSize {
926                width: 360.0,
927                height: 272.0,
928            },
929            CanvasSize {
930                width: 384.0,
931                height: 296.0,
932            },
933            3,
934            2,
935            Some(3),
936        ))
937        .port(
938            data_input("uv")
939                .with_label("UV")
940                .with_type(shader_vec(2))
941                .on_left()
942                .with_view_anchor("rail.inputs")
943                .with_view_group("typed_ports")
944                .with_view_order(0),
945        )
946        .port(
947            data_output("color")
948                .with_label("Color")
949                .with_type(shader_vec(4))
950                .on_right()
951                .with_view_anchor("rail.outputs")
952                .with_view_group("typed_ports")
953                .with_view_order(0),
954        )
955        .surface_slot(
956            NodeSurfaceSlotDescriptor::port_rail("rail.inputs")
957                .with_label("Inputs")
958                .with_slot("ports.inputs")
959                .with_anchor("rail.inputs")
960                .with_lane("ports")
961                .with_order(0),
962        )
963        .surface_slot(
964            NodeSurfaceSlotDescriptor::preview("preview.texture")
965                .with_label("Preview")
966                .with_slot("preview.texture")
967                .with_anchor("preview.texture")
968                .with_order(1)
969                .with_control(
970                    NodeControlDescriptor::asset("control.texture")
971                        .with_label("Texture")
972                        .with_binding(NodeControlBinding::data_path("preview.texture"))
973                        .with_option_source(NodeControlOptionSource::Assets),
974                ),
975        )
976        .surface_slot(
977            NodeSurfaceSlotDescriptor::port_rail("rail.outputs")
978                .with_label("Outputs")
979                .with_slot("ports.outputs")
980                .with_anchor("rail.outputs")
981                .with_lane("ports")
982                .with_order(2),
983        )
984        .default_data(json!({
985            "title": "Texture Sample",
986            "summary": "Samples albedo from UV",
987            "ports": {
988                "inputs": ["vec2 uv"],
989                "outputs": ["vec4 color"]
990            },
991            "preview": {
992                "texture": "checker"
993            }
994        }))
995        .build()
996}
997
998fn shader_mix_schema() -> NodeSchema {
999    let shader_properties =
1000        NodeRepeatableCollectionDescriptor::new("shader.properties", "properties", "id")
1001            .with_item_template_slot(
1002                NodeSurfaceSlotDescriptor::field_row("property")
1003                    .with_label("Property")
1004                    .with_slot("properties")
1005                    .with_control(
1006                        NodeControlDescriptor::text_input("control.property.name")
1007                            .with_label("Name")
1008                            .with_binding(NodeControlBinding::data_path("name")),
1009                    ),
1010            )
1011            .with_anchor_rule(NodeRepeatableAnchorRule::new("property", "property"))
1012            .with_add_action("action.shader_property.add");
1013
1014    NodeSchema::builder("demo.shader.mix", "Mix")
1015        .category(["Shader", "Blueprint"])
1016        .keywords(["shader", "mix", "lerp", "blueprint"])
1017        .renderer_key("shader-card")
1018        .default_size(CanvasSize {
1019            width: 224.0,
1020            height: 168.0,
1021        })
1022        .layout_budget(product_layout_budget(
1023            CanvasSize {
1024                width: 360.0,
1025                height: 272.0,
1026            },
1027            CanvasSize {
1028                width: 392.0,
1029                height: 340.0,
1030            },
1031            3,
1032            2,
1033            Some(3),
1034        ))
1035        .port(
1036            data_input("a")
1037                .with_label("A")
1038                .with_type(shader_vec(4))
1039                .on_left()
1040                .with_view_anchor("rail.inputs")
1041                .with_view_group("typed_ports")
1042                .with_view_order(0),
1043        )
1044        .port(
1045            data_input("b")
1046                .with_label("B")
1047                .with_type(shader_vec(4))
1048                .on_left()
1049                .with_view_anchor("rail.inputs")
1050                .with_view_group("typed_ports")
1051                .with_view_order(1),
1052        )
1053        .port(
1054            data_input("factor")
1055                .with_label("Factor")
1056                .with_type(TypeDesc::Float)
1057                .on_bottom()
1058                .with_view_anchor("config.factor")
1059                .with_view_group("config")
1060                .with_view_order(2),
1061        )
1062        .port(
1063            data_output("result")
1064                .with_label("Result")
1065                .with_type(shader_vec(4))
1066                .on_right()
1067                .with_view_anchor("rail.outputs")
1068                .with_view_group("typed_ports")
1069                .with_view_order(0),
1070        )
1071        .surface_slot(
1072            NodeSurfaceSlotDescriptor::port_rail("rail.inputs")
1073                .with_label("Inputs")
1074                .with_slot("ports.inputs")
1075                .with_anchor("rail.inputs")
1076                .with_lane("ports")
1077                .with_order(0),
1078        )
1079        .surface_slot(
1080            NodeSurfaceSlotDescriptor::config_group("config.factor")
1081                .with_label("Factor")
1082                .with_slot("config.factor")
1083                .with_anchor("config.factor")
1084                .with_order(1)
1085                .with_control(
1086                    NodeControlDescriptor::slider("control.factor")
1087                        .with_label("Factor")
1088                        .with_binding(NodeControlBinding::data_path("config.factor.default"))
1089                        .with_validation_rule(NodeControlValidationRule::Range {
1090                            min: Some(0.0),
1091                            max: Some(1.0),
1092                        }),
1093                ),
1094        )
1095        .surface_slot(
1096            NodeSurfaceSlotDescriptor::preview("preview.result")
1097                .with_label("Preview")
1098                .with_slot("preview.result")
1099                .with_anchor("preview.result")
1100                .with_order(2),
1101        )
1102        .surface_slot(
1103            NodeSurfaceSlotDescriptor::port_rail("rail.outputs")
1104                .with_label("Outputs")
1105                .with_slot("ports.outputs")
1106                .with_anchor("rail.outputs")
1107                .with_lane("ports")
1108                .with_order(3),
1109        )
1110        .repeatable_collection(
1111            NodeRepeatableCollectionDescriptor::new("shader.inputs", "dynamic_inputs", "id")
1112                .with_label("Dynamic inputs")
1113                .with_empty_label("No dynamic inputs")
1114                .with_item_template_slot(
1115                    NodeSurfaceSlotDescriptor::port_rail("input")
1116                        .with_label("Input")
1117                        .with_slot("dynamic_inputs")
1118                        .with_control(
1119                            NodeControlDescriptor::select("control.shader_input.type")
1120                                .with_label("Type")
1121                                .with_binding(NodeControlBinding::data_path("ty"))
1122                                .with_options([
1123                                    NodeControlOption::new("float", "Float"),
1124                                    NodeControlOption::new("vec2", "Vec2"),
1125                                    NodeControlOption::new("vec4", "Vec4"),
1126                                ]),
1127                        ),
1128                )
1129                .with_anchor_rule(
1130                    NodeRepeatableAnchorRule::new("rail.inputs", "rail.inputs")
1131                        .with_port_key_path("port"),
1132                )
1133                .with_min_items(2)
1134                .with_max_items(8)
1135                .reorderable()
1136                .with_add_action("action.shader_input.add")
1137                .with_remove_action("action.shader_input.remove")
1138                .with_reorder_action("action.shader_input.reorder"),
1139        )
1140        .repeatable_collection(shader_properties.clone())
1141        .action(
1142            NodeActionDescriptor::new(
1143                "action.shader_input.add",
1144                "Add input",
1145                ActionTarget::Node {
1146                    node_kind: "demo.shader.mix".to_owned(),
1147                },
1148                ActionIntent::AddRepeatableItem {
1149                    collection_key: "shader.inputs".to_owned(),
1150                },
1151            )
1152            .with_group("inputs"),
1153        )
1154        .action(
1155            NodeActionDescriptor::new(
1156                "action.shader_input.remove",
1157                "Remove input",
1158                ActionTarget::Node {
1159                    node_kind: "demo.shader.mix".to_owned(),
1160                },
1161                ActionIntent::RemoveRepeatableItem {
1162                    collection_key: "shader.inputs".to_owned(),
1163                    item_id: String::new(),
1164                },
1165            )
1166            .with_group("inputs")
1167            .danger(),
1168        )
1169        .action(
1170            NodeActionDescriptor::new(
1171                "action.shader_input.reorder",
1172                "Reorder input",
1173                ActionTarget::Node {
1174                    node_kind: "demo.shader.mix".to_owned(),
1175                },
1176                ActionIntent::ReorderRepeatableItem {
1177                    collection_key: "shader.inputs".to_owned(),
1178                    item_id: String::new(),
1179                },
1180            )
1181            .with_group("inputs"),
1182        )
1183        .action(
1184            NodeActionDescriptor::new(
1185                "action.shader_property.add",
1186                "Add property",
1187                ActionTarget::Blackboard {
1188                    blackboard_key: "blackboard.shader.properties".to_owned(),
1189                },
1190                ActionIntent::AddRepeatableItem {
1191                    collection_key: "shader.properties".to_owned(),
1192                },
1193            )
1194            .with_group("blackboard"),
1195        )
1196        .blackboard(
1197            BlackboardDescriptor::new(
1198                "blackboard.shader.properties",
1199                "Shader properties",
1200                shader_properties,
1201            )
1202            .with_action_key("action.shader_property.add"),
1203        )
1204        .default_data(json!({
1205            "title": "Mix",
1206            "summary": "Blend two color streams",
1207            "ports": {
1208                "inputs": ["vec4 a", "vec4 b"],
1209                "outputs": ["vec4 result"]
1210            },
1211            "config": {
1212                "factor": {
1213                    "type": "float",
1214                    "default": 0.5
1215                }
1216            },
1217            "preview": {
1218                "result": "gradient"
1219            },
1220            "dynamic_inputs": [
1221                { "id": "a", "name": "A", "ty": "vec4", "port": "a" },
1222                { "id": "b", "name": "B", "ty": "vec4", "port": "b" },
1223                { "id": "factor", "name": "Factor", "ty": "float", "port": "factor" }
1224            ],
1225            "properties": [
1226                { "id": "base_color", "name": "Base Color", "ty": "vec4" },
1227                { "id": "roughness", "name": "Roughness", "ty": "float" }
1228            ]
1229        }))
1230        .build()
1231}
1232
1233fn mind_topic_schema() -> NodeSchema {
1234    NodeSchema::builder("demo.topic", "Topic")
1235        .category(["Mind map"])
1236        .keywords(["mindnode", "margin-note", "knowledge"])
1237        .renderer_key("topic-card")
1238        .default_size(CanvasSize {
1239            width: 210.0,
1240            height: 96.0,
1241        })
1242        .layout_budget(product_layout_budget(
1243            CanvasSize {
1244                width: 320.0,
1245                height: 220.0,
1246            },
1247            CanvasSize {
1248                width: 344.0,
1249                height: 240.0,
1250            },
1251            3,
1252            2,
1253            None,
1254        ))
1255        .port(input_port("in"))
1256        .port(output_port("out"))
1257        .surface_slot(
1258            NodeSurfaceSlotDescriptor::header("header.main")
1259                .with_label("Topic")
1260                .with_slot("title")
1261                .with_order(0)
1262                .with_control(
1263                    NodeControlDescriptor::text_input("control.topic.title")
1264                        .with_label("Title")
1265                        .with_binding(NodeControlBinding::slot("title")),
1266                ),
1267        )
1268        .surface_slot(
1269            NodeSurfaceSlotDescriptor::body("body.summary")
1270                .with_label("Summary")
1271                .with_slot("summary")
1272                .with_anchor("body.summary")
1273                .with_order(1)
1274                .with_control(
1275                    NodeControlDescriptor::text_area("control.topic.summary")
1276                        .with_label("Summary")
1277                        .with_binding(NodeControlBinding::slot("summary")),
1278                ),
1279        )
1280        .default_data(json!({ "title": "Topic", "summary": "Central idea" }))
1281        .build()
1282}
1283
1284fn mind_idea_schema() -> NodeSchema {
1285    NodeSchema::builder("demo.idea", "Idea")
1286        .category(["Mind map"])
1287        .keywords(["branch", "note", "thought"])
1288        .renderer_key("idea-card")
1289        .default_size(CanvasSize {
1290            width: 176.0,
1291            height: 76.0,
1292        })
1293        .layout_budget(product_layout_budget(
1294            CanvasSize {
1295                width: 248.0,
1296                height: 132.0,
1297            },
1298            CanvasSize {
1299                width: 280.0,
1300                height: 156.0,
1301            },
1302            2,
1303            1,
1304            None,
1305        ))
1306        .port(input_port("in"))
1307        .port(output_port("out"))
1308        .surface_slot(
1309            NodeSurfaceSlotDescriptor::header("header.main")
1310                .with_label("Idea")
1311                .with_slot("title")
1312                .with_order(0)
1313                .with_control(
1314                    NodeControlDescriptor::text_input("control.idea.title")
1315                        .with_label("Title")
1316                        .with_binding(NodeControlBinding::slot("title")),
1317                ),
1318        )
1319        .default_data(json!({ "title": "Idea", "summary": "Branch note" }))
1320        .build()
1321}
1322
1323fn source_card_schema() -> NodeSchema {
1324    NodeSchema::builder("demo.source", "Source")
1325        .category(["Knowledge"])
1326        .keywords(["paper", "quote", "annotation", "marginnote"])
1327        .renderer_key("source-card")
1328        .default_size(CanvasSize {
1329            width: 210.0,
1330            height: 92.0,
1331        })
1332        .layout_budget(product_layout_budget(
1333            CanvasSize {
1334                width: 328.0,
1335                height: 220.0,
1336            },
1337            CanvasSize {
1338                width: 352.0,
1339                height: 240.0,
1340            },
1341            3,
1342            2,
1343            None,
1344        ))
1345        .port(output_port("out"))
1346        .surface_slot(
1347            NodeSurfaceSlotDescriptor::header("header.main")
1348                .with_label("Source")
1349                .with_slot("title")
1350                .with_order(0)
1351                .with_control(
1352                    NodeControlDescriptor::text_input("control.source.title")
1353                        .with_label("Title")
1354                        .with_binding(NodeControlBinding::slot("title")),
1355                ),
1356        )
1357        .surface_slot(
1358            NodeSurfaceSlotDescriptor::preview("preview.main")
1359                .with_label("Excerpt")
1360                .with_slot("preview")
1361                .with_anchor("preview.main")
1362                .with_order(1)
1363                .with_control(
1364                    NodeControlDescriptor::asset("control.source.asset")
1365                        .with_label("Source")
1366                        .with_binding(NodeControlBinding::slot("preview"))
1367                        .with_option_source(NodeControlOptionSource::Assets),
1368                ),
1369        )
1370        .default_data(json!({
1371            "title": "Source",
1372            "summary": "Evidence card",
1373            "preview": "Annotated excerpt"
1374        }))
1375        .build()
1376}
1377
1378fn workflow_fixture() -> NodeKitFixture {
1379    NodeKitFixture::new("workflow.review", "Workflow review")
1380        .with_description("A compact review loop with branch and output paths.")
1381        .node(
1382            NodeKitFixtureNode::new(
1383                "trigger",
1384                "demo.trigger",
1385                CanvasPoint {
1386                    x: -320.0,
1387                    y: -40.0,
1388                },
1389            )
1390            .with_data(json!({
1391                "title": "Trigger",
1392                "summary": "Starts an automation"
1393            })),
1394        )
1395        .node(
1396            NodeKitFixtureNode::new("tool", "demo.tool", CanvasPoint { x: -20.0, y: -80.0 })
1397                .with_data(json!({
1398                    "title": "Tool",
1399                    "summary": "Runs an external action"
1400                })),
1401        )
1402        .node(
1403            NodeKitFixtureNode::new(
1404                "decision",
1405                "demo.decision",
1406                CanvasPoint { x: 300.0, y: -80.0 },
1407            )
1408            .with_data(json!({
1409                "title": "Decision",
1410                "summary": "Branch the flow"
1411            })),
1412        )
1413        .node(
1414            NodeKitFixtureNode::new(
1415                "output",
1416                "demo.workflow_output",
1417                CanvasPoint { x: 620.0, y: -40.0 },
1418            )
1419            .with_data(json!({
1420                "title": "Workflow output",
1421                "summary": "Returns data to caller"
1422            })),
1423        )
1424        .edge(
1425            NodeKitFixtureEdge::new("trigger", "tool", EdgeKind::Exec)
1426                .with_from_port("event")
1427                .with_to_port("in")
1428                .with_data(json!({ "label": "event" }))
1429                .with_view(
1430                    EdgeViewDescriptor::new()
1431                        .with_label("event")
1432                        .with_label_anchor(EdgeLabelAnchor::Center)
1433                        .with_target_marker_key("arrow")
1434                        .with_style_token("workflow"),
1435                ),
1436        )
1437        .edge(
1438            NodeKitFixtureEdge::new("tool", "decision", EdgeKind::Exec)
1439                .with_from_port("out")
1440                .with_to_port("in")
1441                .with_data(json!({ "label": "flow" })),
1442        )
1443        .edge(
1444            NodeKitFixtureEdge::new("decision", "output", EdgeKind::Exec)
1445                .with_from_port("yes")
1446                .with_to_port("in")
1447                .with_data(json!({ "label": "yes" })),
1448        )
1449        .expect_counts(4, 3)
1450}
1451
1452fn erd_fixture() -> NodeKitFixture {
1453    NodeKitFixture::new("erd.customer_orders", "Customer orders")
1454        .with_description("Three linked tables showing a classic 1:N relationship.")
1455        .node(
1456            NodeKitFixtureNode::new(
1457                "customers",
1458                "demo.table",
1459                CanvasPoint {
1460                    x: -520.0,
1461                    y: -80.0,
1462                },
1463            )
1464            .with_data(json!({
1465                "title": "customers",
1466                "summary": "id · email · plan_id",
1467                "field_order": ["primary_key", "field", "foreign_key"],
1468                "fields": {
1469                    "primary_key": "id",
1470                    "field": "email",
1471                    "foreign_key": "plan_id"
1472                },
1473                "meta": { "cardinality": "1:N" },
1474                "metrics": { "rows": "42k" },
1475                "actions": { "table": ["Add column", "Inspect relation"] },
1476                "columns": [
1477                    { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
1478                    { "id": "email", "name": "email", "ty": "text", "port": "field_email" },
1479                    { "id": "plan_id", "name": "plan_id", "ty": "uuid", "port": "fk" }
1480                ]
1481            })),
1482        )
1483        .node(
1484            NodeKitFixtureNode::new("orders", "demo.table", CanvasPoint { x: 0.0, y: -100.0 })
1485                .with_data(json!({
1486                    "title": "orders",
1487                    "summary": "id · customer_id · total",
1488                    "field_order": ["primary_key", "field", "foreign_key"],
1489                    "fields": {
1490                        "primary_key": "id",
1491                        "field": "total",
1492                        "foreign_key": "customer_id"
1493                    },
1494                    "meta": { "cardinality": "1:N" },
1495                    "metrics": { "rows": "94k" },
1496                    "actions": { "table": ["Add column", "Inspect relation"] },
1497                    "columns": [
1498                        { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
1499                        { "id": "customer_id", "name": "customer_id", "ty": "uuid", "port": "fk" },
1500                        { "id": "total", "name": "total", "ty": "integer", "port": "field_total" }
1501                    ]
1502                })),
1503        )
1504        .node(
1505            NodeKitFixtureNode::new(
1506                "order_items",
1507                "demo.table",
1508                CanvasPoint { x: 520.0, y: -80.0 },
1509            )
1510            .with_data(json!({
1511                "title": "order_items",
1512                "summary": "id · order_id · sku_id · qty",
1513                "field_order": ["primary_key", "field", "foreign_key", "field"],
1514                "fields": {
1515                    "primary_key": "id",
1516                    "field": "qty",
1517                    "foreign_key": "order_id"
1518                },
1519                "meta": { "cardinality": "1:N" },
1520                "metrics": { "rows": "320k" },
1521                "actions": { "table": ["Add column", "Inspect relation"] },
1522                "columns": [
1523                    { "id": "id", "name": "id", "ty": "uuid", "port": "pk" },
1524                    { "id": "order_id", "name": "order_id", "ty": "uuid", "port": "fk" },
1525                    { "id": "qty", "name": "qty", "ty": "integer", "port": "field_qty" }
1526                ]
1527            })),
1528        )
1529        .edge(
1530            NodeKitFixtureEdge::new("customers", "orders", EdgeKind::Data)
1531                .with_from_port("pk")
1532                .with_to_port("fk")
1533                .with_data(json!({ "label": "1:N" })),
1534        )
1535        .edge(
1536            NodeKitFixtureEdge::new("orders", "order_items", EdgeKind::Data)
1537                .with_from_port("pk")
1538                .with_to_port("fk")
1539                .with_data(json!({ "label": "1:N" })),
1540        )
1541        .expect_counts(3, 2)
1542}
1543
1544fn shader_fixture() -> NodeKitFixture {
1545    NodeKitFixture::new("shader.material_mix", "Material mix")
1546        .with_description("A compact shader graph with typed rails and preview slots.")
1547        .node(
1548            NodeKitFixtureNode::new(
1549                "texture",
1550                "demo.shader.texture_sample",
1551                CanvasPoint {
1552                    x: -320.0,
1553                    y: -60.0,
1554                },
1555            )
1556            .with_data(json!({
1557                "title": "Albedo",
1558                "summary": "Texture sample",
1559                "ports": {
1560                    "inputs": ["vec2 uv"],
1561                    "outputs": ["vec4 color"]
1562                },
1563                "preview": {
1564                    "texture": "checker"
1565                }
1566            })),
1567        )
1568        .node(
1569            NodeKitFixtureNode::new("mix", "demo.shader.mix", CanvasPoint { x: 80.0, y: -60.0 })
1570                .with_data(json!({
1571                    "title": "Mix",
1572                    "summary": "Blend albedo with tint",
1573                    "ports": {
1574                        "inputs": ["vec4 albedo", "vec4 tint"],
1575                        "outputs": ["vec4 result"]
1576                    },
1577                    "config": {
1578                        "factor": {
1579                            "type": "float",
1580                            "default": 0.5
1581                        }
1582                    },
1583                    "preview": {
1584                        "result": "gradient"
1585                    }
1586                })),
1587        )
1588        .edge(
1589            NodeKitFixtureEdge::new("texture", "mix", EdgeKind::Data)
1590                .with_from_port("color")
1591                .with_to_port("a")
1592                .with_data(json!({ "label": "vec4" })),
1593        )
1594        .expect_counts(2, 1)
1595}
1596
1597fn mind_map_fixture() -> NodeKitFixture {
1598    NodeKitFixture::new("mind-map.strategy", "Strategy map")
1599        .with_description("A radial map showing topic, ideas, and source cards.")
1600        .node(
1601            NodeKitFixtureNode::new("center", "demo.topic", CanvasPoint { x: 0.0, y: 0.0 })
1602                .with_data(json!({
1603                    "title": "Product strategy",
1604                    "summary": "MindNode-style radial map"
1605                })),
1606        )
1607        .node(
1608            NodeKitFixtureNode::new(
1609                "users",
1610                "demo.idea",
1611                CanvasPoint {
1612                    x: 432.0,
1613                    y: -128.0,
1614                },
1615            )
1616            .with_data(json!({
1617                "title": "Users",
1618                "summary": "Researchers, builders, editors"
1619            })),
1620        )
1621        .node(
1622            NodeKitFixtureNode::new("risks", "demo.idea", CanvasPoint { x: 432.0, y: 136.0 })
1623                .with_data(json!({
1624                    "title": "Risks",
1625                    "summary": "Trust, scale, migration"
1626                })),
1627        )
1628        .node(
1629            NodeKitFixtureNode::new("sources", "demo.source", CanvasPoint { x: -420.0, y: 24.0 })
1630                .with_data(json!({
1631                    "title": "Source",
1632                    "summary": "Evidence card"
1633                })),
1634        )
1635        .edge(
1636            NodeKitFixtureEdge::new("center", "users", EdgeKind::Data)
1637                .with_data(json!({ "label": "branch" })),
1638        )
1639        .edge(
1640            NodeKitFixtureEdge::new("center", "risks", EdgeKind::Data)
1641                .with_data(json!({ "label": "branch" })),
1642        )
1643        .edge(
1644            NodeKitFixtureEdge::new("sources", "center", EdgeKind::Data)
1645                .with_from_port("out")
1646                .with_to_port("in")
1647                .with_data(json!({ "label": "evidence" })),
1648        )
1649        .expect_counts(4, 3)
1650}
1651
1652fn input_port(key: &str) -> PortDecl {
1653    PortDecl::new(key, PortDirection::In, PortKind::Data, PortCapacity::Multi)
1654        .with_label(key)
1655        .on_left()
1656}
1657
1658fn output_port(key: &str) -> PortDecl {
1659    PortDecl::new(key, PortDirection::Out, PortKind::Data, PortCapacity::Multi)
1660        .with_label(key)
1661        .with_view(PortViewDescriptor::right())
1662}
1663
1664fn data_input(key: &str) -> PortDecl {
1665    PortDecl::new(key, PortDirection::In, PortKind::Data, PortCapacity::Multi).with_label(key)
1666}
1667
1668fn data_output(key: &str) -> PortDecl {
1669    PortDecl::new(key, PortDirection::Out, PortKind::Data, PortCapacity::Multi).with_label(key)
1670}
1671
1672fn shader_vec(width: u8) -> TypeDesc {
1673    TypeDesc::Opaque {
1674        key: format!("shader.vec{width}"),
1675        params: Vec::new(),
1676    }
1677}
1678
1679fn exec_input(key: &str) -> PortDecl {
1680    PortDecl::new(key, PortDirection::In, PortKind::Exec, PortCapacity::Single).with_label(key)
1681}
1682
1683fn exec_output(key: &str) -> PortDecl {
1684    PortDecl::new(key, PortDirection::Out, PortKind::Exec, PortCapacity::Multi).with_label(key)
1685}