@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,371 @@
1
+ // N0 Node Graph — Validation
2
+ // Pure TypeScript — no side effects, no mutations
3
+
4
+ import type {
5
+ GraphSchema,
6
+ NodeInstance,
7
+ EdgeDefinition,
8
+ PortDefinition,
9
+ NodeDefinition,
10
+ ValidationError,
11
+ ValidationResult,
12
+ } from './types';
13
+
14
+ // Helper: Get a node definition by kind
15
+ function getNodeDefinition(
16
+ nodeDefinitions: NodeDefinition[],
17
+ kind: string
18
+ ): NodeDefinition | undefined {
19
+ return nodeDefinitions.find((def) => def.kind === kind);
20
+ }
21
+
22
+ // Helper: Get a node instance by id
23
+ function getNodeInstance(
24
+ nodes: NodeInstance[],
25
+ id: string
26
+ ): NodeInstance | undefined {
27
+ return nodes.find((node) => node.id === id);
28
+ }
29
+
30
+ // Helper: Get a port definition from a node definition
31
+ function getPortDefinition(
32
+ nodeDefinition: NodeDefinition,
33
+ portId: string
34
+ ): PortDefinition | undefined {
35
+ return nodeDefinition.ports.find((port) => port.id === portId);
36
+ }
37
+
38
+ // Helper: Count connections to a specific port
39
+ function countConnectionsToPort(
40
+ edges: EdgeDefinition[],
41
+ nodeId: string,
42
+ portId: string,
43
+ direction: 'source' | 'target'
44
+ ): number {
45
+ return edges.filter((edge) => {
46
+ if (direction === 'source') {
47
+ return edge.sourceNodeId === nodeId && edge.sourcePortId === portId;
48
+ }
49
+ return edge.targetNodeId === nodeId && edge.targetPortId === portId;
50
+ }).length;
51
+ }
52
+
53
+ // Helper: Check if an edge is a duplicate
54
+ function isDuplicateEdge(
55
+ edges: EdgeDefinition[],
56
+ edge: EdgeDefinition
57
+ ): boolean {
58
+ return edges.some((existing) =>
59
+ existing.id !== edge.id &&
60
+ existing.sourceNodeId === edge.sourceNodeId &&
61
+ existing.sourcePortId === edge.sourcePortId &&
62
+ existing.targetNodeId === edge.targetNodeId &&
63
+ existing.targetPortId === edge.targetPortId
64
+ );
65
+ }
66
+
67
+ // Validation Rule 1: NodeInstance.kind must reference a registered NodeDefinition.kind
68
+ function validateNodeKindReferences(
69
+ schema: GraphSchema
70
+ ): ValidationError[] {
71
+ const errors: ValidationError[] = [];
72
+
73
+ for (const node of schema.nodes) {
74
+ const definition = getNodeDefinition(schema.nodeDefinitions, node.kind);
75
+ if (!definition) {
76
+ errors.push({
77
+ code: 'INVALID_NODE_KIND',
78
+ message: `Node instance "${node.id}" references undefined kind "${node.kind}"`,
79
+ nodeId: node.id,
80
+ });
81
+ }
82
+ }
83
+
84
+ return errors;
85
+ }
86
+
87
+ // Validation Rule 2: Every edge must connect existing nodes and existing ports
88
+ function validateEdgeConnections(
89
+ schema: GraphSchema
90
+ ): ValidationError[] {
91
+ const errors: ValidationError[] = [];
92
+
93
+ for (const edge of schema.edges) {
94
+ // Check source node exists
95
+ const sourceNode = getNodeInstance(schema.nodes, edge.sourceNodeId);
96
+ if (!sourceNode) {
97
+ errors.push({
98
+ code: 'INVALID_SOURCE_NODE',
99
+ message: `Edge "${edge.id}" references non-existent source node "${edge.sourceNodeId}"`,
100
+ edgeId: edge.id,
101
+ });
102
+ continue;
103
+ }
104
+
105
+ // Check target node exists
106
+ const targetNode = getNodeInstance(schema.nodes, edge.targetNodeId);
107
+ if (!targetNode) {
108
+ errors.push({
109
+ code: 'INVALID_TARGET_NODE',
110
+ message: `Edge "${edge.id}" references non-existent target node "${edge.targetNodeId}"`,
111
+ edgeId: edge.id,
112
+ });
113
+ continue;
114
+ }
115
+
116
+ // Get node definitions
117
+ const sourceDef = getNodeDefinition(schema.nodeDefinitions, sourceNode.kind);
118
+ const targetDef = getNodeDefinition(schema.nodeDefinitions, targetNode.kind);
119
+
120
+ if (!sourceDef) {
121
+ errors.push({
122
+ code: 'INVALID_SOURCE_NODE_KIND',
123
+ message: `Source node "${sourceNode.id}" has no registered definition`,
124
+ edgeId: edge.id,
125
+ nodeId: sourceNode.id,
126
+ });
127
+ continue;
128
+ }
129
+
130
+ if (!targetDef) {
131
+ errors.push({
132
+ code: 'INVALID_TARGET_NODE_KIND',
133
+ message: `Target node "${targetNode.id}" has no registered definition`,
134
+ edgeId: edge.id,
135
+ nodeId: targetNode.id,
136
+ });
137
+ continue;
138
+ }
139
+
140
+ // Check source port exists
141
+ const sourcePort = getPortDefinition(sourceDef, edge.sourcePortId);
142
+ if (!sourcePort) {
143
+ errors.push({
144
+ code: 'INVALID_SOURCE_PORT',
145
+ message: `Edge "${edge.id}" references non-existent source port "${edge.sourcePortId}" on node "${sourceNode.id}"`,
146
+ edgeId: edge.id,
147
+ portId: edge.sourcePortId,
148
+ });
149
+ }
150
+
151
+ // Check target port exists
152
+ const targetPort = getPortDefinition(targetDef, edge.targetPortId);
153
+ if (!targetPort) {
154
+ errors.push({
155
+ code: 'INVALID_TARGET_PORT',
156
+ message: `Edge "${edge.id}" references non-existent target port "${edge.targetPortId}" on node "${targetNode.id}"`,
157
+ edgeId: edge.id,
158
+ portId: edge.targetPortId,
159
+ });
160
+ }
161
+ }
162
+
163
+ return errors;
164
+ }
165
+
166
+ // Validation Rule 3: Port direction — edges go from output port to input port only
167
+ function validatePortDirection(
168
+ schema: GraphSchema
169
+ ): ValidationError[] {
170
+ const errors: ValidationError[] = [];
171
+
172
+ for (const edge of schema.edges) {
173
+ const sourceNode = getNodeInstance(schema.nodes, edge.sourceNodeId);
174
+ const targetNode = getNodeInstance(schema.nodes, edge.targetNodeId);
175
+
176
+ if (!sourceNode || !targetNode) continue;
177
+
178
+ const sourceDef = getNodeDefinition(schema.nodeDefinitions, sourceNode.kind);
179
+ const targetDef = getNodeDefinition(schema.nodeDefinitions, targetNode.kind);
180
+
181
+ if (!sourceDef || !targetDef) continue;
182
+
183
+ const sourcePort = getPortDefinition(sourceDef, edge.sourcePortId);
184
+ const targetPort = getPortDefinition(targetDef, edge.targetPortId);
185
+
186
+ if (sourcePort && targetPort) {
187
+ if (sourcePort.direction !== 'output') {
188
+ errors.push({
189
+ code: 'INVALID_PORT_DIRECTION',
190
+ message: `Edge "${edge.id}" must start from an output port, but port "${edge.sourcePortId}" on node "${sourceNode.id}" is "${sourcePort.direction}"`,
191
+ edgeId: edge.id,
192
+ portId: edge.sourcePortId,
193
+ });
194
+ }
195
+
196
+ if (targetPort.direction !== 'input') {
197
+ errors.push({
198
+ code: 'INVALID_PORT_DIRECTION',
199
+ message: `Edge "${edge.id}" must end at an input port, but port "${edge.targetPortId}" on node "${targetNode.id}" is "${targetPort.direction}"`,
200
+ edgeId: edge.id,
201
+ portId: edge.targetPortId,
202
+ });
203
+ }
204
+ }
205
+ }
206
+
207
+ return errors;
208
+ }
209
+
210
+ // Validation Rule 4: Port type compatibility — exact match required
211
+ function validateTypeCompatibility(
212
+ schema: GraphSchema
213
+ ): ValidationError[] {
214
+ const errors: ValidationError[] = [];
215
+
216
+ for (const edge of schema.edges) {
217
+ const sourceNode = getNodeInstance(schema.nodes, edge.sourceNodeId);
218
+ const targetNode = getNodeInstance(schema.nodes, edge.targetNodeId);
219
+
220
+ if (!sourceNode || !targetNode) continue;
221
+
222
+ const sourceDef = getNodeDefinition(schema.nodeDefinitions, sourceNode.kind);
223
+ const targetDef = getNodeDefinition(schema.nodeDefinitions, targetNode.kind);
224
+
225
+ if (!sourceDef || !targetDef) continue;
226
+
227
+ const sourcePort = getPortDefinition(sourceDef, edge.sourcePortId);
228
+ const targetPort = getPortDefinition(targetDef, edge.targetPortId);
229
+
230
+ if (sourcePort && targetPort && sourcePort.type !== targetPort.type) {
231
+ errors.push({
232
+ code: 'TYPE_MISMATCH',
233
+ message: `Edge "${edge.id}" connects incompatible types: source port "${edge.sourcePortId}" has type "${sourcePort.type}" but target port "${edge.targetPortId}" has type "${targetPort.type}"`,
234
+ edgeId: edge.id,
235
+ });
236
+ }
237
+ }
238
+
239
+ return errors;
240
+ }
241
+
242
+ // Validation Rule 5: Connection limits — port.maxConnections not exceeded
243
+ function validateConnectionLimits(
244
+ schema: GraphSchema
245
+ ): ValidationError[] {
246
+ const errors: ValidationError[] = [];
247
+
248
+ for (const node of schema.nodes) {
249
+ const definition = getNodeDefinition(schema.nodeDefinitions, node.kind);
250
+ if (!definition) continue;
251
+
252
+ for (const port of definition.ports) {
253
+ const connectionCount = countConnectionsToPort(
254
+ schema.edges,
255
+ node.id,
256
+ port.id,
257
+ port.direction === 'output' ? 'source' : 'target'
258
+ );
259
+
260
+ if (connectionCount > port.maxConnections) {
261
+ errors.push({
262
+ code: 'MAX_CONNECTIONS_EXCEEDED',
263
+ message: `Port "${port.id}" on node "${node.id}" has ${connectionCount} connections but maximum allowed is ${port.maxConnections}`,
264
+ nodeId: node.id,
265
+ portId: port.id,
266
+ });
267
+ }
268
+ }
269
+ }
270
+
271
+ return errors;
272
+ }
273
+
274
+ // Validation Rule 6: Required ports — all required:true ports must have at least one connection
275
+ function validateRequiredPorts(
276
+ schema: GraphSchema
277
+ ): ValidationError[] {
278
+ const errors: ValidationError[] = [];
279
+
280
+ for (const node of schema.nodes) {
281
+ const definition = getNodeDefinition(schema.nodeDefinitions, node.kind);
282
+ if (!definition) continue;
283
+
284
+ for (const port of definition.ports) {
285
+ if (!port.required) continue;
286
+
287
+ const connectionCount = countConnectionsToPort(
288
+ schema.edges,
289
+ node.id,
290
+ port.id,
291
+ port.direction === 'output' ? 'source' : 'target'
292
+ );
293
+
294
+ if (connectionCount === 0) {
295
+ errors.push({
296
+ code: 'REQUIRED_PORT_UNCONNECTED',
297
+ message: `Required port "${port.id}" on node "${node.id}" has no connections`,
298
+ nodeId: node.id,
299
+ portId: port.id,
300
+ });
301
+ }
302
+ }
303
+ }
304
+
305
+ return errors;
306
+ }
307
+
308
+ // Validation Rule 7: No self-loops (edge source === edge target on same node)
309
+ function validateNoSelfLoops(
310
+ schema: GraphSchema
311
+ ): ValidationError[] {
312
+ const errors: ValidationError[] = [];
313
+
314
+ for (const edge of schema.edges) {
315
+ if (edge.sourceNodeId === edge.targetNodeId) {
316
+ errors.push({
317
+ code: 'SELF_LOOP',
318
+ message: `Edge "${edge.id}" creates a self-loop on node "${edge.sourceNodeId}"`,
319
+ edgeId: edge.id,
320
+ });
321
+ }
322
+ }
323
+
324
+ return errors;
325
+ }
326
+
327
+ // Validation Rule 8: No duplicate edges (same source port → same target port)
328
+ function validateNoDuplicateEdges(
329
+ schema: GraphSchema
330
+ ): ValidationError[] {
331
+ const errors: ValidationError[] = [];
332
+
333
+ for (const edge of schema.edges) {
334
+ if (isDuplicateEdge(schema.edges, edge)) {
335
+ errors.push({
336
+ code: 'DUPLICATE_EDGE',
337
+ message: `Edge "${edge.id}" is a duplicate: same source "${edge.sourceNodeId}:${edge.sourcePortId}" to target "${edge.targetNodeId}:${edge.targetPortId}"`,
338
+ edgeId: edge.id,
339
+ });
340
+ }
341
+ }
342
+
343
+ return errors;
344
+ }
345
+
346
+ /**
347
+ * Validates a graph schema against all 8 validation rules.
348
+ *
349
+ * Pure function — no side effects, no mutation of input.
350
+ *
351
+ * @param schema - The graph schema to validate
352
+ * @returns ValidationResult with valid flag and array of errors
353
+ */
354
+ export function validateGraph(schema: GraphSchema): ValidationResult {
355
+ // Collect all errors from all validation rules
356
+ const allErrors: ValidationError[] = [
357
+ ...validateNodeKindReferences(schema),
358
+ ...validateEdgeConnections(schema),
359
+ ...validatePortDirection(schema),
360
+ ...validateTypeCompatibility(schema),
361
+ ...validateConnectionLimits(schema),
362
+ ...validateRequiredPorts(schema),
363
+ ...validateNoSelfLoops(schema),
364
+ ...validateNoDuplicateEdges(schema),
365
+ ];
366
+
367
+ return {
368
+ valid: allErrors.length === 0,
369
+ errors: allErrors,
370
+ };
371
+ }
@@ -0,0 +1,202 @@
1
+ <script lang="ts">
2
+ // N1 FlowCanvas — Canvas 2D graph with ELK layout + BFS edge reveal
3
+ // S-02: BFS order drives edge reveal stagger (no Math.random)
4
+ // S-05: worker terminated on component destroy
5
+
6
+ import { onMount, onDestroy } from 'svelte';
7
+ import { Canvas, Layer } from 'svelte-canvas';
8
+ import { getArrow } from 'perfect-arrows';
9
+ import type { GraphSnapshot, GraphStateApi } from './types.js';
10
+ import { GraphState } from './graph-state.svelte.js';
11
+ import { runLayout } from './layout.js';
12
+ import { EDGE_REVEAL_STAGGER_MS } from './types.js';
13
+
14
+ // ── Props ────────────────────────────────────────────────────────────────────
15
+
16
+ interface Props {
17
+ snapshot: GraphSnapshot;
18
+ state?: GraphStateApi;
19
+ width?: number;
20
+ height?: number;
21
+ }
22
+
23
+ // Destructure as `graphStateProp` to avoid conflict with the `$state` rune.
24
+ let { snapshot, state: graphStateProp, width = 800, height = 600 }: Props = $props();
25
+
26
+ // ── Internal state ──────────────────────────────────────────────────────────
27
+
28
+ // svelte-ignore state_referenced_locally
29
+ const _state = graphStateProp instanceof GraphState ? graphStateProp : new GraphState();
30
+ let mounted = false;
31
+ /** Edge reveal offsets: edge id → 0.0 (hidden) .. 1.0 (fully drawn) */
32
+ let edgeOffsets = $state<Map<string, number>>(new Map());
33
+
34
+ // ── Layout (runs on main thread — ELK is async, won't block UI) ─────────────
35
+ // The previous Worker approach failed in Tauri: ELK's bundled JS tries to
36
+ // spawn a sub-Worker inside the layout Worker, which Tauri's WKWebView
37
+ // doesn't support (nested Workers unavailable, Worker = undefined in scope).
38
+
39
+ function requestLayout() {
40
+ if (!mounted) return;
41
+ runLayout(snapshot, width, height).then(result => {
42
+ _state.applyLayout(result);
43
+ scheduleEdgeReveal(result.bfsOrder);
44
+ }).catch(() => {
45
+ // Layout failed — leave existing positions in place
46
+ });
47
+ }
48
+
49
+ onMount(() => {
50
+ mounted = true;
51
+ requestLayout();
52
+ });
53
+
54
+ onDestroy(() => {
55
+ mounted = false;
56
+ if (_state instanceof GraphState) _state.destroy();
57
+ });
58
+
59
+ // Re-layout when snapshot changes
60
+ $effect(() => {
61
+ void snapshot;
62
+ requestLayout();
63
+ });
64
+
65
+ // ── Edge reveal animation (BFS stagger) ────────────────────────────────────
66
+
67
+ function scheduleEdgeReveal(bfsOrder: ReadonlyArray<string>) {
68
+ // Reset all edges to hidden
69
+ const next = new Map<string, number>();
70
+ for (const e of snapshot.edges) next.set(e.id, 0.0);
71
+ edgeOffsets = next;
72
+
73
+ // BFS order: each node index N → reveal its outgoing edges after N * STAGGER_MS
74
+ const nodeIndex = new Map<string, number>();
75
+ bfsOrder.forEach((id, i) => nodeIndex.set(id, i));
76
+
77
+ for (const edge of snapshot.edges) {
78
+ const idx = nodeIndex.get(edge.sourceId) ?? 0;
79
+ setTimeout(() => {
80
+ edgeOffsets = new Map(edgeOffsets).set(edge.id, 1.0);
81
+ }, idx * EDGE_REVEAL_STAGGER_MS);
82
+ }
83
+ }
84
+
85
+ // ── Design tokens ──────────────────────────────────────────────────────────
86
+
87
+ const COLORS = {
88
+ bg: '#1a1a1a',
89
+ node: '#2a2a2a',
90
+ nodeBorder: '#b87333',
91
+ nodeHover: '#3a3a3a',
92
+ nodeText: '#e8e0d0',
93
+ edgeLine: '#b87333',
94
+ edgeFade: '#5a4a30',
95
+ focus: '#d4943a',
96
+ };
97
+
98
+ const NODE_RADIUS = 4;
99
+ const FONT = '12px var(--font-mono, monospace)';
100
+
101
+ // ── Render ──────────────────────────────────────────────────────────────────
102
+
103
+ function render({ context }: { context: CanvasRenderingContext2D }) {
104
+ const positions = _state.positions;
105
+ const focusId = (_state as GraphState).focusCenterId;
106
+
107
+ context.clearRect(0, 0, width, height);
108
+
109
+ // Background
110
+ context.fillStyle = COLORS.bg;
111
+ context.fillRect(0, 0, width, height);
112
+
113
+ // ── Edges ──
114
+ context.font = FONT;
115
+ for (const edge of snapshot.edges) {
116
+ const src = positions.get(edge.sourceId);
117
+ const tgt = positions.get(edge.targetId);
118
+ if (!src || !tgt) continue;
119
+
120
+ const progress = edgeOffsets.get(edge.id) ?? 0;
121
+ if (progress <= 0) continue;
122
+
123
+ const sx = src.x + src.width / 2;
124
+ const sy = src.y + src.height / 2;
125
+ const tx = tgt.x + tgt.width / 2;
126
+ const ty = tgt.y + tgt.height / 2;
127
+
128
+ const [ax, ay, cx, cy, ex, ey, ae] = getArrow(sx, sy, tx, ty, {
129
+ bow: 0.2,
130
+ stretch: 0.4,
131
+ padStart: src.width / 2 + 4,
132
+ padEnd: tgt.width / 2 + 4,
133
+ });
134
+
135
+ // BFS reveal via lineDashOffset
136
+ const path = new Path2D();
137
+ path.moveTo(ax, ay);
138
+ path.quadraticCurveTo(cx, cy, ex, ey);
139
+
140
+ // Estimate path length for dash sizing (approximation)
141
+ const dx = ex - ax, dy2 = ey - ay;
142
+ const approxLen = Math.sqrt(dx * dx + dy2 * dy2) * 1.2;
143
+ const dash = approxLen * progress;
144
+
145
+ context.save();
146
+ context.strokeStyle = progress >= 1.0 ? COLORS.edgeLine : COLORS.edgeFade;
147
+ context.lineWidth = 1.5;
148
+ context.setLineDash([dash, approxLen]);
149
+ context.lineDashOffset = 0;
150
+ context.stroke(path);
151
+ context.restore();
152
+
153
+ // Arrowhead
154
+ if (progress >= 1.0) {
155
+ context.save();
156
+ context.fillStyle = COLORS.edgeLine;
157
+ context.translate(ex, ey);
158
+ context.rotate(ae);
159
+ context.beginPath();
160
+ context.moveTo(0, 0);
161
+ context.lineTo(-8, -4);
162
+ context.lineTo(-8, 4);
163
+ context.closePath();
164
+ context.fill();
165
+ context.restore();
166
+ }
167
+ }
168
+
169
+ // ── Nodes ──
170
+ for (const node of snapshot.nodes) {
171
+ const pos = positions.get(node.id);
172
+ if (!pos) continue;
173
+
174
+ const isFocus = node.id === focusId;
175
+ const { x, y, width: w, height: h } = pos;
176
+
177
+ // Node body
178
+ context.fillStyle = isFocus ? COLORS.focus : COLORS.node;
179
+ context.strokeStyle = COLORS.nodeBorder;
180
+ context.lineWidth = isFocus ? 2 : 1;
181
+
182
+ context.beginPath();
183
+ context.roundRect(x, y, w, h, NODE_RADIUS);
184
+ context.fill();
185
+ context.stroke();
186
+
187
+ // Label
188
+ context.fillStyle = COLORS.nodeText;
189
+ context.font = FONT;
190
+ context.textAlign = 'center';
191
+ context.textBaseline = 'middle';
192
+ // Truncate if needed
193
+ const maxWidth = w - 16;
194
+ const label = node.label.length > 18 ? node.label.slice(0, 16) + '…' : node.label;
195
+ context.fillText(label, x + w / 2, y + h / 2, maxWidth);
196
+ }
197
+ }
198
+ </script>
199
+
200
+ <Canvas {width} {height}>
201
+ <Layer {render} />
202
+ </Canvas>
@@ -0,0 +1,95 @@
1
+ // N1 GraphState — Svelte 5 runes, Spring-animated node positions
2
+ // S-02: positions lerped deterministically via rAF loop (no Math.random)
3
+ // S-05: animation loop bounded by convergence threshold
4
+
5
+ import type { NodeLayout, LayoutResult, GraphStateApi } from './types.js';
6
+
7
+ const LERP_FACTOR = 0.12; // per frame (bounded, S-05)
8
+ const SETTLE_THRESH = 0.5; // pixels — stop animating below this delta
9
+ const MAX_FRAMES = 240; // ~4s at 60fps hard cap (S-05 killability)
10
+
11
+ export class GraphState implements GraphStateApi {
12
+ positions = $state<Map<string, NodeLayout>>(new Map());
13
+ bfsOrder = $state<ReadonlyArray<string>>([]);
14
+
15
+ // Focus semantics
16
+ focusCenterId = $state<string | null>(null);
17
+ layoutRootId = $state<string | null>(null);
18
+
19
+ private _targets = new Map<string, NodeLayout>();
20
+ private _rafId: number | null = null;
21
+ private _frameCount = 0;
22
+
23
+ // Callback invoked by FlowCanvas when layoutRootId changes
24
+ onLayoutRootChange: ((nodeId: string) => void) | null = null;
25
+
26
+ applyLayout(result: LayoutResult): void {
27
+ this._targets = new Map(result.positions);
28
+ this.bfsOrder = result.bfsOrder;
29
+ this._frameCount = 0;
30
+ this._startAnimation();
31
+ }
32
+
33
+ setFocusCenter(nodeId: string): void {
34
+ // Re-centers Poincaré disk — does NOT trigger re-layout
35
+ this.focusCenterId = nodeId;
36
+ }
37
+
38
+ setLayoutRoot(nodeId: string): void {
39
+ this.layoutRootId = nodeId;
40
+ this.onLayoutRootChange?.(nodeId);
41
+ }
42
+
43
+ // ── Animation ──────────────────────────────────────────────────────────────
44
+
45
+ private _startAnimation(): void {
46
+ if (this._rafId !== null) cancelAnimationFrame(this._rafId);
47
+ this._tick();
48
+ }
49
+
50
+ private _tick = (): void => {
51
+ if (this._frameCount >= MAX_FRAMES) {
52
+ // Force-snap to targets on hard cap (S-05)
53
+ this.positions = new Map(this._targets);
54
+ this._rafId = null;
55
+ return;
56
+ }
57
+
58
+ let maxDelta = 0;
59
+ const next = new Map<string, NodeLayout>();
60
+
61
+ for (const [id, target] of this._targets) {
62
+ const current = this.positions.get(id);
63
+ if (!current) {
64
+ // First appearance — snap directly, no lerp
65
+ next.set(id, { ...target });
66
+ continue;
67
+ }
68
+ const nx = current.x + (target.x - current.x) * LERP_FACTOR;
69
+ const ny = current.y + (target.y - current.y) * LERP_FACTOR;
70
+ const dx = Math.abs(target.x - nx);
71
+ const dy = Math.abs(target.y - ny);
72
+ if (dx > maxDelta) maxDelta = dx;
73
+ if (dy > maxDelta) maxDelta = dy;
74
+ next.set(id, { id, x: nx, y: ny, width: target.width, height: target.height });
75
+ }
76
+
77
+ this.positions = next;
78
+ this._frameCount++;
79
+
80
+ if (maxDelta > SETTLE_THRESH) {
81
+ this._rafId = requestAnimationFrame(this._tick);
82
+ } else {
83
+ // Settled — snap to exact targets
84
+ this.positions = new Map(this._targets);
85
+ this._rafId = null;
86
+ }
87
+ };
88
+
89
+ destroy(): void {
90
+ if (this._rafId !== null) {
91
+ cancelAnimationFrame(this._rafId);
92
+ this._rafId = null;
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,21 @@
1
+ // N1 Flow Canvas — Canvas 2D graph with ELK layout + BFS edge reveal
2
+ // Depends on: graphology, elkjs, perfect-arrows, svelte-canvas
3
+
4
+ export type {
5
+ GraphNode,
6
+ GraphEdge,
7
+ GraphSnapshot,
8
+ NodeLayout,
9
+ LayoutResult,
10
+ WorkerIn,
11
+ WorkerOut,
12
+ WorkerMessage,
13
+ GraphStateApi,
14
+ EdgeRevealState,
15
+ } from './types.js';
16
+
17
+ export { EDGE_REVEAL_STAGGER_MS } from './types.js';
18
+
19
+ export { default as FlowCanvas } from './FlowCanvas.svelte';
20
+ export { GraphState } from './graph-state.svelte';
21
+ export { nodeHitTest, domEventToCanvasCoords, parseMentionEdges, parseGroupedSequenceEdges } from './utils.js';