@stephen-netu/design-system 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +66 -0
  2. package/package.json +130 -0
  3. package/src/b0-app-shell/clarification-overlay/ClarificationOverlay.svelte +221 -0
  4. package/src/b0-app-shell/clarification-overlay/index.ts +1 -0
  5. package/src/b0-app-shell/command-bar/CommandBar.svelte +565 -0
  6. package/src/b0-app-shell/index.ts +14 -0
  7. package/src/base.css +152 -0
  8. package/src/d0-data-viz/BarChart.svelte +236 -0
  9. package/src/d0-data-viz/CheckpointBar.svelte +574 -0
  10. package/src/d0-data-viz/CheckpointPill.svelte +297 -0
  11. package/src/d0-data-viz/MetricCard.svelte +159 -0
  12. package/src/d0-data-viz/SparkLine.svelte +66 -0
  13. package/src/d0-data-viz/StorageMap.svelte +182 -0
  14. package/src/d0-data-viz/Timeline.svelte +188 -0
  15. package/src/d0-data-viz/checkpoint-types.ts +162 -0
  16. package/src/d0-data-viz/d0-tokens.css +32 -0
  17. package/src/d0-data-viz/index.ts +58 -0
  18. package/src/d0-data-viz/storage-map-types.ts +142 -0
  19. package/src/d0-data-viz/theme-integration.ts +134 -0
  20. package/src/d0-data-viz/types.ts +53 -0
  21. package/src/demos/BspSizingInfographic.svelte +386 -0
  22. package/src/demos/BspZoneDemo.svelte +255 -0
  23. package/src/demos/ControlRoomDemo.svelte +570 -0
  24. package/src/demos/NodepadBlocksShowcase.svelte +189 -0
  25. package/src/demos/index.ts +7 -0
  26. package/src/editor/CodeEditor.svelte +188 -0
  27. package/src/editor/FindReplaceDialog.svelte +516 -0
  28. package/src/editor/MarkdownEditor.svelte +660 -0
  29. package/src/editor/index.ts +2 -0
  30. package/src/entity-graph-view/WorldView.svelte +171 -0
  31. package/src/entity-graph-view/index.ts +6 -0
  32. package/src/entity-graph-view/types.ts +364 -0
  33. package/src/epistemic/index.ts +2 -0
  34. package/src/epistemic/types.ts +134 -0
  35. package/src/epistemic/utils.ts +32 -0
  36. package/src/f0-forms/checkbox/Checkbox.svelte +158 -0
  37. package/src/f0-forms/field/FormField.svelte +91 -0
  38. package/src/f0-forms/index.ts +11 -0
  39. package/src/f0-forms/radio/RadioGroup.svelte +192 -0
  40. package/src/f0-forms/search/SearchBar.svelte +99 -0
  41. package/src/f0-forms/select/Select.svelte +198 -0
  42. package/src/f0-forms/textarea/TextArea.svelte +170 -0
  43. package/src/f0-forms/toggle/FormToggle.svelte +147 -0
  44. package/src/f0-forms/validation/validate.ts +47 -0
  45. package/src/fabric/ai/ChatPanel.svelte +648 -0
  46. package/src/fabric/ai/index.ts +11 -0
  47. package/src/fabric/data/index.ts +21 -0
  48. package/src/fabric/feedback/StatusBar.svelte +274 -0
  49. package/src/fabric/feedback/ToastContainer.svelte +122 -0
  50. package/src/fabric/feedback/index.ts +14 -0
  51. package/src/fabric/index.ts +37 -0
  52. package/src/fabric/input/CommandBar.svelte +79 -0
  53. package/src/fabric/input/KeyboardShortcuts.svelte +388 -0
  54. package/src/fabric/input/index.ts +12 -0
  55. package/src/fabric/layout/GlassBox.svelte +193 -0
  56. package/src/fabric/layout/KanbanBoard.svelte +158 -0
  57. package/src/fabric/layout/KanbanCard.svelte +226 -0
  58. package/src/fabric/layout/KanbanColumn.svelte +278 -0
  59. package/src/fabric/layout/index.ts +17 -0
  60. package/src/fabric/navigation/ActivityRail.svelte +271 -0
  61. package/src/fabric/navigation/index.ts +11 -0
  62. package/src/g0-geo/SovereignMap.svelte +149 -0
  63. package/src/g0-geo/index.ts +2 -0
  64. package/src/index.ts +68 -0
  65. package/src/l0-layout/block-writer/BlockConnectionLines.svelte +98 -0
  66. package/src/l0-layout/block-writer/BlockContent.svelte +213 -0
  67. package/src/l0-layout/block-writer/BlockHeader.svelte +230 -0
  68. package/src/l0-layout/block-writer/BlockItem.svelte +177 -0
  69. package/src/l0-layout/block-writer/BlockWriter.svelte +219 -0
  70. package/src/l0-layout/block-writer/block-writer-types.ts +63 -0
  71. package/src/l0-layout/block-writer/index.ts +24 -0
  72. package/src/l0-layout/block-writer/markdown.ts +110 -0
  73. package/src/l0-layout/chat-view/ChatViewLayout.svelte +61 -0
  74. package/src/l0-layout/epistemic/AllayDetailPanel.svelte +113 -0
  75. package/src/l0-layout/epistemic/AssayDetailPanel.svelte +164 -0
  76. package/src/l0-layout/epistemic/AvenueTrace.svelte +253 -0
  77. package/src/l0-layout/epistemic/KnowledgeDetailPanel.svelte +204 -0
  78. package/src/l0-layout/epistemic/ParadigmLens.svelte +170 -0
  79. package/src/l0-layout/epistemic/ProvenanceChain.svelte +164 -0
  80. package/src/l0-layout/epistemic/TrustView.svelte +195 -0
  81. package/src/l0-layout/epistemic/index.ts +7 -0
  82. package/src/l0-layout/grove/AccordCard.svelte +156 -0
  83. package/src/l0-layout/grove/GroveCard.svelte +197 -0
  84. package/src/l0-layout/grove/MemberBadge.svelte +96 -0
  85. package/src/l0-layout/grove/MemberList.svelte +59 -0
  86. package/src/l0-layout/grove/grove-types.ts +29 -0
  87. package/src/l0-layout/grove/index.ts +14 -0
  88. package/src/l0-layout/index.ts +45 -0
  89. package/src/l0-layout/kanban/KanbanBoard.svelte +71 -0
  90. package/src/l0-layout/kanban/KanbanCard.svelte +276 -0
  91. package/src/l0-layout/kanban/KanbanColumn.svelte +198 -0
  92. package/src/l0-layout/kanban/KanbanViewLayout.svelte +61 -0
  93. package/src/l0-layout/workspace/WorkspaceLayout.svelte +93 -0
  94. package/src/n0-node-graph/GraphEditor.svelte +342 -0
  95. package/src/n0-node-graph/GraphHistory.svelte +122 -0
  96. package/src/n0-node-graph/Node.svelte +269 -0
  97. package/src/n0-node-graph/NodeCanvas.svelte +187 -0
  98. package/src/n0-node-graph/NodeEdge.svelte +152 -0
  99. package/src/n0-node-graph/NodePalette.svelte +318 -0
  100. package/src/n0-node-graph/NodePort.svelte +128 -0
  101. package/src/n0-node-graph/index.ts +33 -0
  102. package/src/n0-node-graph/n0-tokens.css +29 -0
  103. package/src/n0-node-graph/node-port-types.ts +19 -0
  104. package/src/n0-node-graph/types.ts +96 -0
  105. package/src/n0-node-graph/validate.test.ts +722 -0
  106. package/src/n0-node-graph/validate.ts +371 -0
  107. package/src/n1-flow-canvas/FlowCanvas.svelte +202 -0
  108. package/src/n1-flow-canvas/graph-state.svelte.ts +95 -0
  109. package/src/n1-flow-canvas/index.ts +21 -0
  110. package/src/n1-flow-canvas/layout.ts +118 -0
  111. package/src/n1-flow-canvas/layout.worker.ts +128 -0
  112. package/src/n1-flow-canvas/types.ts +102 -0
  113. package/src/n1-flow-canvas/utils.ts +146 -0
  114. package/src/n2-manifold-disk/ManifoldDisk.svelte +314 -0
  115. package/src/n2-manifold-disk/index.ts +6 -0
  116. package/src/n2-manifold-disk/poincare.ts +184 -0
  117. package/src/n3-force-graph/ForceGraph.svelte +180 -0
  118. package/src/n3-force-graph/GraphTooltip.svelte +99 -0
  119. package/src/n3-force-graph/animated-edge.ts +107 -0
  120. package/src/n3-force-graph/health-node.ts +128 -0
  121. package/src/n3-force-graph/index.ts +31 -0
  122. package/src/n3-force-graph/types.ts +94 -0
  123. package/src/n4-adaptive-manifold/AdaptiveManifold.svelte +1165 -0
  124. package/src/n4-adaptive-manifold/DetailCard.svelte +212 -0
  125. package/src/n4-adaptive-manifold/SuggestionIndicator.svelte +91 -0
  126. package/src/n4-adaptive-manifold/classifier.ts +139 -0
  127. package/src/n4-adaptive-manifold/index.ts +45 -0
  128. package/src/n4-adaptive-manifold/n4-tokens.css +155 -0
  129. package/src/n4-adaptive-manifold/orchestrator.ts +301 -0
  130. package/src/n4-adaptive-manifold/phase-theme.ts +92 -0
  131. package/src/n4-adaptive-manifold/pinnacle-events.ts +135 -0
  132. package/src/n4-adaptive-manifold/providers/gravitational-provider.ts +191 -0
  133. package/src/n4-adaptive-manifold/providers/manifold-provider.ts +276 -0
  134. package/src/n4-adaptive-manifold/transition.ts +90 -0
  135. package/src/n4-adaptive-manifold/types.ts +313 -0
  136. package/src/p0-primitives/index.ts +6 -0
  137. package/src/p0-primitives/tokens/control-room.css +309 -0
  138. package/src/p0-primitives/tokens/index.ts +6 -0
  139. package/src/p0-primitives/tokens/tokens.css +140 -0
  140. package/src/p0-primitives/tokens/tokens.json +132 -0
  141. package/src/p0-primitives/types/index.ts +123 -0
  142. package/src/p0-primitives/types/plugin-types.ts +74 -0
  143. package/src/p0-primitives/utils/index.ts +39 -0
  144. package/src/platform/hud-protocol.ts +20 -0
  145. package/src/platform/index.ts +3 -0
  146. package/src/s0-state/create-store.ts +84 -0
  147. package/src/s0-state/derived.ts +41 -0
  148. package/src/s0-state/index.ts +6 -0
  149. package/src/showcase/Showcase.svelte +401 -0
  150. package/src/spatial/BspTilingCanvas.svelte +232 -0
  151. package/src/spatial/ForceCanvas.svelte +289 -0
  152. package/src/spatial/LodRenderer.svelte +334 -0
  153. package/src/spatial/SnapZoomCamera.svelte +185 -0
  154. package/src/spatial/ZoneTiler.svelte +311 -0
  155. package/src/spatial/camera-types.ts +45 -0
  156. package/src/spatial/content-proportional-bsp.ts +253 -0
  157. package/src/spatial/force-canvas-types.ts +102 -0
  158. package/src/spatial/index.ts +15 -0
  159. package/src/spatial/lod-types.ts +70 -0
  160. package/src/spatial/zone-tiler-types.ts +177 -0
  161. package/src/stores/vaultStore.svelte +14 -0
  162. package/src/t0-transport/connection.ts +34 -0
  163. package/src/t0-transport/error.ts +37 -0
  164. package/src/t0-transport/events.ts +50 -0
  165. package/src/t0-transport/index.ts +6 -0
  166. package/src/t0-transport/invoke.ts +30 -0
  167. package/src/tauri/index.ts +13 -0
  168. package/src/u0-primitives/allay-gauge/AllayGauge.svelte +90 -0
  169. package/src/u0-primitives/allay-gauge/index.ts +1 -0
  170. package/src/u0-primitives/animated-icon/AnimatedIcon.svelte +273 -0
  171. package/src/u0-primitives/animated-icon/README.md +53 -0
  172. package/src/u0-primitives/animated-icon/index.ts +31 -0
  173. package/src/u0-primitives/assay-badge/AssayBadge.svelte +121 -0
  174. package/src/u0-primitives/assay-badge/AssaySignalBar.svelte +83 -0
  175. package/src/u0-primitives/assay-badge/index.ts +2 -0
  176. package/src/u0-primitives/avatar/Avatar.svelte +177 -0
  177. package/src/u0-primitives/badge/Badge.svelte +149 -0
  178. package/src/u0-primitives/button/Button.svelte +304 -0
  179. package/src/u0-primitives/card/Card.svelte +182 -0
  180. package/src/u0-primitives/dropdown/Dropdown.svelte +305 -0
  181. package/src/u0-primitives/expert-badge/ExpertBadge.svelte +89 -0
  182. package/src/u0-primitives/expert-badge/index.ts +1 -0
  183. package/src/u0-primitives/index.ts +35 -0
  184. package/src/u0-primitives/input/Input.svelte +237 -0
  185. package/src/u0-primitives/modal/Modal.svelte +331 -0
  186. package/src/u0-primitives/modal/index.ts +1 -0
  187. package/src/u0-primitives/spinner/Spinner.svelte +129 -0
  188. package/src/u0-primitives/tabs/Tabs.svelte +225 -0
  189. package/src/u0-primitives/tabs/tabs-types.ts +11 -0
  190. package/src/u0-primitives/toggle/Toggle.svelte +161 -0
  191. package/src/u0-primitives/tooltip/Tooltip.svelte +234 -0
  192. package/src/v0-diagrams/DiagramPanel.svelte +68 -0
  193. package/src/v0-diagrams/MarkmapDiagram.svelte +56 -0
  194. package/src/v0-diagrams/MermaidDiagram.svelte +53 -0
  195. package/src/v0-diagrams/index.ts +4 -0
  196. package/src/v0-diagrams/types.ts +28 -0
  197. package/src/v0-diagrams/v0-tokens.css +6 -0
  198. package/src/vite-env.d.ts +7 -0
  199. package/src/x0-enchanted-blocks/ENCHANTED_BLOCKS_SPEC.md +457 -0
  200. package/src/x0-enchanted-blocks/components/EnchantedBlock.svelte +485 -0
  201. package/src/x0-enchanted-blocks/components/NodepadBlock.svelte +262 -0
  202. package/src/x0-enchanted-blocks/core/block-layout.ts +211 -0
  203. package/src/x0-enchanted-blocks/core/block-types.ts +227 -0
  204. package/src/x0-enchanted-blocks/index.ts +46 -0
@@ -0,0 +1,722 @@
1
+ // N0 Node Graph — Validation Tests
2
+ // Uses Vitest — deterministic test data only
3
+
4
+ import { describe, it, expect } from 'vitest';
5
+ import { validateGraph } from './validate';
6
+ import type { GraphSchema, NodeDefinition, NodeInstance, EdgeDefinition } from './types';
7
+
8
+ // Deterministic test data factories — no Math.random() or Date.now()
9
+
10
+ function createMinimalNodeDefinition(): NodeDefinition {
11
+ return {
12
+ kind: 'test:task',
13
+ label: 'Task Node',
14
+ description: 'A test task node',
15
+ ports: [
16
+ {
17
+ id: 'input',
18
+ label: 'Input',
19
+ type: 'app:test:task' as const,
20
+ direction: 'input' as const,
21
+ required: false,
22
+ maxConnections: 1,
23
+ },
24
+ {
25
+ id: 'output',
26
+ label: 'Output',
27
+ type: 'app:test:task' as const,
28
+ direction: 'output' as const,
29
+ required: false,
30
+ maxConnections: Infinity,
31
+ },
32
+ ],
33
+ metadata: {},
34
+ };
35
+ }
36
+
37
+ function createMinimalNodeInstance(id: string, kind: string = 'test:task'): NodeInstance {
38
+ return {
39
+ id,
40
+ kind,
41
+ position: { x: 0, y: 0 },
42
+ size: { width: 100, height: 50 },
43
+ data: {},
44
+ collapsed: false,
45
+ };
46
+ }
47
+
48
+ function createMinimalEdge(
49
+ id: string,
50
+ sourceNodeId: string,
51
+ sourcePortId: string,
52
+ targetNodeId: string,
53
+ targetPortId: string
54
+ ): EdgeDefinition {
55
+ return {
56
+ id,
57
+ sourceNodeId,
58
+ sourcePortId,
59
+ targetNodeId,
60
+ targetPortId,
61
+ metadata: {},
62
+ };
63
+ }
64
+
65
+ function createValidGraph(): GraphSchema {
66
+ const nodeDef = createMinimalNodeDefinition();
67
+ return {
68
+ id: 'graph-001',
69
+ name: 'Valid Test Graph',
70
+ version: '1.0.0',
71
+ nodeDefinitions: [nodeDef],
72
+ nodes: [
73
+ createMinimalNodeInstance('node-1'),
74
+ createMinimalNodeInstance('node-2'),
75
+ ],
76
+ edges: [
77
+ createMinimalEdge('edge-1', 'node-1', 'output', 'node-2', 'input'),
78
+ ],
79
+ };
80
+ }
81
+
82
+ // === Valid Graph Tests ===
83
+
84
+ describe('validateGraph — valid graphs', () => {
85
+ it('passes for a valid minimal graph', () => {
86
+ const graph = createValidGraph();
87
+ const result = validateGraph(graph);
88
+ expect(result.valid).toBe(true);
89
+ expect(result.errors).toHaveLength(0);
90
+ });
91
+
92
+ it('passes for an empty graph', () => {
93
+ const graph: GraphSchema = {
94
+ id: 'empty-graph',
95
+ name: 'Empty Graph',
96
+ version: '1.0.0',
97
+ nodeDefinitions: [createMinimalNodeDefinition()],
98
+ nodes: [],
99
+ edges: [],
100
+ };
101
+ const result = validateGraph(graph);
102
+ expect(result.valid).toBe(true);
103
+ expect(result.errors).toHaveLength(0);
104
+ });
105
+
106
+ it('passes for a single node with no edges', () => {
107
+ const graph: GraphSchema = {
108
+ id: 'single-node-graph',
109
+ name: 'Single Node Graph',
110
+ version: '1.0.0',
111
+ nodeDefinitions: [createMinimalNodeDefinition()],
112
+ nodes: [createMinimalNodeInstance('node-1')],
113
+ edges: [],
114
+ };
115
+ const result = validateGraph(graph);
116
+ expect(result.valid).toBe(true);
117
+ expect(result.errors).toHaveLength(0);
118
+ });
119
+
120
+ it('passes for disconnected subgraphs (multiple node groups without edges)', () => {
121
+ const graph: GraphSchema = {
122
+ id: 'disconnected-graph',
123
+ name: 'Disconnected Graph',
124
+ version: '1.0.0',
125
+ nodeDefinitions: [createMinimalNodeDefinition()],
126
+ nodes: [
127
+ createMinimalNodeInstance('node-1'),
128
+ createMinimalNodeInstance('node-2'),
129
+ createMinimalNodeInstance('node-3'),
130
+ createMinimalNodeInstance('node-4'),
131
+ ],
132
+ // Only connect node-1 to node-2, leaving 3 and 4 disconnected
133
+ edges: [
134
+ createMinimalEdge('edge-1', 'node-1', 'output', 'node-2', 'input'),
135
+ ],
136
+ };
137
+ const result = validateGraph(graph);
138
+ expect(result.valid).toBe(true);
139
+ expect(result.errors).toHaveLength(0);
140
+ });
141
+ });
142
+
143
+ // === Rule 1: Node kind references ===
144
+
145
+ describe('validateGraph — Rule 1: Node kind must reference registered definition', () => {
146
+ it('fails when node kind is not registered', () => {
147
+ const graph: GraphSchema = {
148
+ id: 'invalid-kind-graph',
149
+ name: 'Invalid Kind Graph',
150
+ version: '1.0.0',
151
+ nodeDefinitions: [createMinimalNodeDefinition()],
152
+ nodes: [
153
+ {
154
+ id: 'node-1',
155
+ kind: 'nonexistent:kind',
156
+ position: { x: 0, y: 0 },
157
+ size: { width: 100, height: 50 },
158
+ data: {},
159
+ collapsed: false,
160
+ },
161
+ ],
162
+ edges: [],
163
+ };
164
+ const result = validateGraph(graph);
165
+ expect(result.valid).toBe(false);
166
+ expect(result.errors).toContainEqual(
167
+ expect.objectContaining({
168
+ code: 'INVALID_NODE_KIND',
169
+ message: expect.stringContaining('nonexistent:kind'),
170
+ nodeId: 'node-1',
171
+ })
172
+ );
173
+ });
174
+ });
175
+
176
+ // === Rule 2: Edge connections ===
177
+
178
+ describe('validateGraph — Rule 2: Edges must connect existing nodes and port', () => {
179
+ it('fails when edge references non-existent source node', () => {
180
+ const graph: GraphSchema = {
181
+ id: 'invalid-source-node',
182
+ name: 'Invalid Source Node',
183
+ version: '1.0.0',
184
+ nodeDefinitions: [createMinimalNodeDefinition()],
185
+ nodes: [createMinimalNodeInstance('node-1')],
186
+ edges: [
187
+ createMinimalEdge('edge-1', 'nonexistent-node', 'output', 'node-1', 'input'),
188
+ ],
189
+ };
190
+ const result = validateGraph(graph);
191
+ expect(result.valid).toBe(false);
192
+ expect(result.errors).toContainEqual(
193
+ expect.objectContaining({
194
+ code: 'INVALID_SOURCE_NODE',
195
+ })
196
+ );
197
+ });
198
+
199
+ it('fails when edge references non-existent target node', () => {
200
+ const graph: GraphSchema = {
201
+ id: 'invalid-target-node',
202
+ name: 'Invalid Target Node',
203
+ version: '1.0.0',
204
+ nodeDefinitions: [createMinimalNodeDefinition()],
205
+ nodes: [createMinimalNodeInstance('node-1')],
206
+ edges: [
207
+ createMinimalEdge('edge-1', 'node-1', 'output', 'nonexistent-node', 'input'),
208
+ ],
209
+ };
210
+ const result = validateGraph(graph);
211
+ expect(result.valid).toBe(false);
212
+ expect(result.errors).toContainEqual(
213
+ expect.objectContaining({
214
+ code: 'INVALID_TARGET_NODE',
215
+ })
216
+ );
217
+ });
218
+
219
+ it('fails when edge references non-existent source port', () => {
220
+ const graph: GraphSchema = {
221
+ id: 'invalid-source-port',
222
+ name: 'Invalid Source Port',
223
+ version: '1.0.0',
224
+ nodeDefinitions: [createMinimalNodeDefinition()],
225
+ nodes: [
226
+ createMinimalNodeInstance('node-1'),
227
+ createMinimalNodeInstance('node-2'),
228
+ ],
229
+ edges: [
230
+ createMinimalEdge('edge-1', 'node-1', 'nonexistent-port', 'node-2', 'input'),
231
+ ],
232
+ };
233
+ const result = validateGraph(graph);
234
+ expect(result.valid).toBe(false);
235
+ expect(result.errors).toContainEqual(
236
+ expect.objectContaining({
237
+ code: 'INVALID_SOURCE_PORT',
238
+ })
239
+ );
240
+ });
241
+
242
+ it('fails when edge references non-existent target port', () => {
243
+ const graph: GraphSchema = {
244
+ id: 'invalid-target-port',
245
+ name: 'Invalid Target Port',
246
+ version: '1.0.0',
247
+ nodeDefinitions: [createMinimalNodeDefinition()],
248
+ nodes: [
249
+ createMinimalNodeInstance('node-1'),
250
+ createMinimalNodeInstance('node-2'),
251
+ ],
252
+ edges: [
253
+ createMinimalEdge('edge-1', 'node-1', 'output', 'node-2', 'nonexistent-port'),
254
+ ],
255
+ };
256
+ const result = validateGraph(graph);
257
+ expect(result.valid).toBe(false);
258
+ expect(result.errors).toContainEqual(
259
+ expect.objectContaining({
260
+ code: 'INVALID_TARGET_PORT',
261
+ })
262
+ );
263
+ });
264
+ });
265
+
266
+ // === Rule 3: Port direction ===
267
+
268
+ describe('validateGraph — Rule 3: Port direction (output → input only)', () => {
269
+ it('fails when edge goes from input to output', () => {
270
+ const nodeDef: NodeDefinition = {
271
+ kind: 'test:task',
272
+ label: 'Task Node',
273
+ description: 'A test task node',
274
+ ports: [
275
+ {
276
+ id: 'input',
277
+ label: 'Input',
278
+ type: 'app:test:task' as const,
279
+ direction: 'input' as const,
280
+ required: false,
281
+ maxConnections: 1,
282
+ },
283
+ {
284
+ id: 'output',
285
+ label: 'Output',
286
+ type: 'app:test:task' as const,
287
+ direction: 'output' as const,
288
+ required: false,
289
+ maxConnections: Infinity,
290
+ },
291
+ ],
292
+ metadata: {},
293
+ };
294
+
295
+ const graph: GraphSchema = {
296
+ id: 'wrong-direction',
297
+ name: 'Wrong Direction',
298
+ version: '1.0.0',
299
+ nodeDefinitions: [nodeDef],
300
+ nodes: [
301
+ createMinimalNodeInstance('node-1'),
302
+ createMinimalNodeInstance('node-2'),
303
+ ],
304
+ edges: [
305
+ // Trying to connect input to output — wrong direction
306
+ createMinimalEdge('edge-1', 'node-1', 'input', 'node-2', 'output'),
307
+ ],
308
+ };
309
+ const result = validateGraph(graph);
310
+ expect(result.valid).toBe(false);
311
+ expect(result.errors).toContainEqual(
312
+ expect.objectContaining({
313
+ code: 'INVALID_PORT_DIRECTION',
314
+ })
315
+ );
316
+ });
317
+
318
+ it('fails when edge goes from output to output', () => {
319
+ const nodeDef: NodeDefinition = {
320
+ kind: 'test:task',
321
+ label: 'Task Node',
322
+ description: 'A test task node',
323
+ ports: [
324
+ {
325
+ id: 'input',
326
+ label: 'Input',
327
+ type: 'app:test:task' as const,
328
+ direction: 'input' as const,
329
+ required: false,
330
+ maxConnections: 1,
331
+ },
332
+ {
333
+ id: 'output',
334
+ label: 'Output',
335
+ type: 'app:test:task' as const,
336
+ direction: 'output' as const,
337
+ required: false,
338
+ maxConnections: Infinity,
339
+ },
340
+ ],
341
+ metadata: {},
342
+ };
343
+
344
+ const graph: GraphSchema = {
345
+ id: 'wrong-direction-2',
346
+ name: 'Wrong Direction 2',
347
+ version: '1.0.0',
348
+ nodeDefinitions: [nodeDef],
349
+ nodes: [
350
+ createMinimalNodeInstance('node-1'),
351
+ createMinimalNodeInstance('node-2'),
352
+ ],
353
+ edges: [
354
+ // Both are output ports — wrong direction
355
+ createMinimalEdge('edge-1', 'node-1', 'output', 'node-2', 'output'),
356
+ ],
357
+ };
358
+ const result = validateGraph(graph);
359
+ expect(result.valid).toBe(false);
360
+ expect(result.errors).toContainEqual(
361
+ expect.objectContaining({
362
+ code: 'INVALID_PORT_DIRECTION',
363
+ })
364
+ );
365
+ });
366
+ });
367
+
368
+ // === Rule 4: Type compatibility ===
369
+
370
+ describe('validateGraph — Rule 4: Port type compatibility (exact match)', () => {
371
+ it('fails when source and target port types do not match', () => {
372
+ const nodeDef1: NodeDefinition = {
373
+ kind: 'test:task',
374
+ label: 'Task Node',
375
+ description: 'A test task node',
376
+ ports: [
377
+ {
378
+ id: 'output',
379
+ label: 'Output',
380
+ type: 'app:test:task' as const,
381
+ direction: 'output' as const,
382
+ required: false,
383
+ maxConnections: Infinity,
384
+ },
385
+ ],
386
+ metadata: {},
387
+ };
388
+
389
+ const nodeDef2: NodeDefinition = {
390
+ kind: 'test:signal',
391
+ label: 'Signal Node',
392
+ description: 'A test signal node',
393
+ ports: [
394
+ {
395
+ id: 'input',
396
+ label: 'Input',
397
+ type: 'app:test:signal' as const,
398
+ direction: 'input' as const,
399
+ required: false,
400
+ maxConnections: 1,
401
+ },
402
+ ],
403
+ metadata: {},
404
+ };
405
+
406
+ const graph: GraphSchema = {
407
+ id: 'type-mismatch',
408
+ name: 'Type Mismatch',
409
+ version: '1.0.0',
410
+ nodeDefinitions: [nodeDef1, nodeDef2],
411
+ nodes: [
412
+ { ...createMinimalNodeInstance('node-1', 'test:task'), ports: undefined } as NodeInstance,
413
+ { ...createMinimalNodeInstance('node-2', 'test:signal'), ports: undefined } as NodeInstance,
414
+ ],
415
+ edges: [
416
+ createMinimalEdge('edge-1', 'node-1', 'output', 'node-2', 'input'),
417
+ ],
418
+ };
419
+ const result = validateGraph(graph);
420
+ expect(result.valid).toBe(false);
421
+ expect(result.errors).toContainEqual(
422
+ expect.objectContaining({
423
+ code: 'TYPE_MISMATCH',
424
+ message: expect.stringMatching(/app:test:(task|signal)/),
425
+ })
426
+ );
427
+ });
428
+ });
429
+
430
+ // === Rule 5: Connection limits ===
431
+
432
+ describe('validateGraph — Rule 5: Connection limits (maxConnections)', () => {
433
+ it('fails when input port maxConnections is exceeded', () => {
434
+ const nodeDef: NodeDefinition = {
435
+ kind: 'test:task',
436
+ label: 'Task Node',
437
+ description: 'A test task node',
438
+ ports: [
439
+ {
440
+ id: 'input',
441
+ label: 'Input',
442
+ type: 'app:test:task' as const,
443
+ direction: 'input' as const,
444
+ required: false,
445
+ maxConnections: 1, // Only allows 1 connection
446
+ },
447
+ {
448
+ id: 'output',
449
+ label: 'Output',
450
+ type: 'app:test:task' as const,
451
+ direction: 'output' as const,
452
+ required: false,
453
+ maxConnections: Infinity,
454
+ },
455
+ ],
456
+ metadata: {},
457
+ };
458
+
459
+ const graph: GraphSchema = {
460
+ id: 'max-connections',
461
+ name: 'Max Connections',
462
+ version: '1.0.0',
463
+ nodeDefinitions: [nodeDef],
464
+ nodes: [
465
+ createMinimalNodeInstance('node-1'), // Source
466
+ createMinimalNodeInstance('node-2'), // Target 1
467
+ createMinimalNodeInstance('node-3'), // Target 2 - exceeds limit
468
+ ],
469
+ edges: [
470
+ createMinimalEdge('edge-1', 'node-1', 'output', 'node-2', 'input'),
471
+ createMinimalEdge('edge-2', 'node-1', 'output', 'node-2', 'input'), // 2 connections to same port - exceeds maxConnections=1
472
+ ],
473
+ };
474
+ const result = validateGraph(graph);
475
+ expect(result.valid).toBe(false);
476
+ expect(result.errors).toContainEqual(
477
+ expect.objectContaining({
478
+ code: 'MAX_CONNECTIONS_EXCEEDED',
479
+ message: expect.stringMatching(/(1 connections|maximum allowed is 1)/),
480
+ })
481
+ );
482
+ });
483
+ });
484
+
485
+ // === Rule 6: Required ports ===
486
+
487
+ describe('validateGraph — Rule 6: Required ports must have connections', () => {
488
+ it('fails when required input port has no connections', () => {
489
+ const nodeDef: NodeDefinition = {
490
+ kind: 'test:task',
491
+ label: 'Task Node',
492
+ description: 'A test task node',
493
+ ports: [
494
+ {
495
+ id: 'input',
496
+ label: 'Input',
497
+ type: 'app:test:task' as const,
498
+ direction: 'input' as const,
499
+ required: true, // Required!
500
+ maxConnections: 1,
501
+ },
502
+ {
503
+ id: 'output',
504
+ label: 'Output',
505
+ type: 'app:test:task' as const,
506
+ direction: 'output' as const,
507
+ required: false,
508
+ maxConnections: Infinity,
509
+ },
510
+ ],
511
+ metadata: {},
512
+ };
513
+
514
+ const graph: GraphSchema = {
515
+ id: 'required-port',
516
+ name: 'Required Port',
517
+ version: '1.0.0',
518
+ nodeDefinitions: [nodeDef],
519
+ nodes: [createMinimalNodeInstance('node-1')],
520
+ edges: [], // No connections to the required input port
521
+ };
522
+ const result = validateGraph(graph);
523
+ expect(result.valid).toBe(false);
524
+ expect(result.errors.some(e =>
525
+ e.code === 'REQUIRED_PORT_UNCONNECTED' &&
526
+ e.nodeId === 'node-1' &&
527
+ e.portId === 'input'
528
+ )).toBe(true);
529
+ });
530
+
531
+ it('passes when required input port has at least one connection', () => {
532
+ const nodeDefRequired: NodeDefinition = {
533
+ kind: 'test:task',
534
+ label: 'Task Node',
535
+ description: 'A test task node',
536
+ ports: [
537
+ {
538
+ id: 'input',
539
+ label: 'Input',
540
+ type: 'app:test:task' as const,
541
+ direction: 'input' as const,
542
+ required: true,
543
+ maxConnections: 1,
544
+ },
545
+ {
546
+ id: 'output',
547
+ label: 'Output',
548
+ type: 'app:test:task' as const,
549
+ direction: 'output' as const,
550
+ required: false,
551
+ maxConnections: Infinity,
552
+ },
553
+ ],
554
+ metadata: {},
555
+ };
556
+
557
+ const nodeDefOptional: NodeDefinition = {
558
+ kind: 'test:task-optional',
559
+ label: 'Task Node Optional',
560
+ description: 'A test task node with optional input',
561
+ ports: [
562
+ {
563
+ id: 'input',
564
+ label: 'Input',
565
+ type: 'app:test:task' as const,
566
+ direction: 'input' as const,
567
+ required: false,
568
+ maxConnections: 1,
569
+ },
570
+ {
571
+ id: 'output',
572
+ label: 'Output',
573
+ type: 'app:test:task' as const,
574
+ direction: 'output' as const,
575
+ required: false,
576
+ maxConnections: Infinity,
577
+ },
578
+ ],
579
+ metadata: {},
580
+ };
581
+
582
+ const graph: GraphSchema = {
583
+ id: 'required-port-ok',
584
+ name: 'Required Port OK',
585
+ version: '1.0.0',
586
+ nodeDefinitions: [nodeDefRequired, nodeDefOptional],
587
+ nodes: [
588
+ createMinimalNodeInstance('node-1', 'test:task-optional'),
589
+ createMinimalNodeInstance('node-2', 'test:task'),
590
+ ],
591
+ edges: [
592
+ createMinimalEdge('edge-1', 'node-1', 'output', 'node-2', 'input'),
593
+ ],
594
+ };
595
+ const result = validateGraph(graph);
596
+ expect(result.valid).toBe(true);
597
+ expect(result.errors).toHaveLength(0);
598
+ });
599
+ });
600
+
601
+ // === Rule 7: No self-loops ===
602
+
603
+ describe('validateGraph — Rule 7: No self-loops', () => {
604
+ it('fails when edge connects node to itself', () => {
605
+ const graph: GraphSchema = {
606
+ id: 'self-loop',
607
+ name: 'Self Loop',
608
+ version: '1.0.0',
609
+ nodeDefinitions: [createMinimalNodeDefinition()],
610
+ nodes: [createMinimalNodeInstance('node-1')],
611
+ edges: [
612
+ // Self-loop: source and target are the same node
613
+ createMinimalEdge('edge-1', 'node-1', 'output', 'node-1', 'input'),
614
+ ],
615
+ };
616
+ const result = validateGraph(graph);
617
+ expect(result.valid).toBe(false);
618
+ expect(result.errors).toContainEqual(
619
+ expect.objectContaining({
620
+ code: 'SELF_LOOP',
621
+ message: expect.stringContaining('self-loop'),
622
+ edgeId: 'edge-1',
623
+ })
624
+ );
625
+ });
626
+ });
627
+
628
+ // === Rule 8: No duplicate edges ===
629
+
630
+ describe('validateGraph — Rule 8: No duplicate edges', () => {
631
+ it('fails when duplicate edge exists (same source and target ports)', () => {
632
+ const graph: GraphSchema = {
633
+ id: 'duplicate-edge',
634
+ name: 'Duplicate Edge',
635
+ version: '1.0.0',
636
+ nodeDefinitions: [createMinimalNodeDefinition()],
637
+ nodes: [
638
+ createMinimalNodeInstance('node-1'),
639
+ createMinimalNodeInstance('node-2'),
640
+ ],
641
+ edges: [
642
+ createMinimalEdge('edge-1', 'node-1', 'output', 'node-2', 'input'),
643
+ // Duplicate: same source and target ports
644
+ createMinimalEdge('edge-2', 'node-1', 'output', 'node-2', 'input'),
645
+ ],
646
+ };
647
+ const result = validateGraph(graph);
648
+ expect(result.valid).toBe(false);
649
+ expect(result.errors).toContainEqual(
650
+ expect.objectContaining({
651
+ code: 'DUPLICATE_EDGE',
652
+ message: expect.stringContaining('duplicate'),
653
+ })
654
+ );
655
+ });
656
+
657
+ it('passes when edges connect different ports on same nodes', () => {
658
+ const graph: GraphSchema = {
659
+ id: 'different-ports',
660
+ name: 'Different Ports',
661
+ version: '1.0.0',
662
+ nodeDefinitions: [
663
+ {
664
+ kind: 'test:multi',
665
+ label: 'Multi Port Node',
666
+ description: 'Node with multiple ports',
667
+ ports: [
668
+ { id: 'input1', label: 'Input 1', type: 'app:test:task' as const, direction: 'input' as const, required: false, maxConnections: 1 },
669
+ { id: 'input2', label: 'Input 2', type: 'app:test:task' as const, direction: 'input' as const, required: false, maxConnections: 1 },
670
+ { id: 'output1', label: 'Output 1', type: 'app:test:task' as const, direction: 'output' as const, required: false, maxConnections: Infinity },
671
+ { id: 'output2', label: 'Output 2', type: 'app:test:task' as const, direction: 'output' as const, required: false, maxConnections: Infinity },
672
+ ],
673
+ metadata: {},
674
+ },
675
+ ],
676
+ nodes: [
677
+ createMinimalNodeInstance('node-1', 'test:multi'),
678
+ createMinimalNodeInstance('node-2', 'test:multi'),
679
+ ],
680
+ edges: [
681
+ createMinimalEdge('edge-1', 'node-1', 'output1', 'node-2', 'input1'),
682
+ createMinimalEdge('edge-2', 'node-1', 'output2', 'node-2', 'input2'), // Different ports, not duplicate
683
+ ],
684
+ };
685
+ const result = validateGraph(graph);
686
+ expect(result.valid).toBe(true);
687
+ });
688
+ });
689
+
690
+ // === Multiple validation errors ===
691
+
692
+ describe('validateGraph — multiple errors', () => {
693
+ it('collects all errors from all rules', () => {
694
+ const graph: GraphSchema = {
695
+ id: 'multiple-errors',
696
+ name: 'Multiple Errors',
697
+ version: '1.0.0',
698
+ nodeDefinitions: [createMinimalNodeDefinition()],
699
+ nodes: [
700
+ {
701
+ id: 'node-1',
702
+ kind: 'nonexistent:kind', // Rule 1 error
703
+ position: { x: 0, y: 0 },
704
+ size: { width: 100, height: 50 },
705
+ data: {},
706
+ collapsed: false,
707
+ },
708
+ createMinimalNodeInstance('node-2'),
709
+ ],
710
+ edges: [
711
+ createMinimalEdge('edge-1', 'node-1', 'output', 'node-2', 'input'),
712
+ createMinimalEdge('edge-2', 'node-1', 'output', 'node-1', 'input'), // Rule 7: self-loop
713
+ ],
714
+ };
715
+ const result = validateGraph(graph);
716
+ expect(result.valid).toBe(false);
717
+ expect(result.errors.length).toBeGreaterThanOrEqual(2);
718
+ expect(result.errors.map(e => e.code).sort()).toEqual(
719
+ expect.arrayContaining(['INVALID_NODE_KIND', 'SELF_LOOP'].sort())
720
+ );
721
+ });
722
+ });