@statelyai/graph 1.0.0 → 2.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 (77) hide show
  1. package/README.md +121 -44
  2. package/dist/{adjacency-list-VsUaH9SJ.mjs → adjacency-list-DQ32Mmhx.mjs} +3 -1
  3. package/dist/algorithms-D1cgly0g.d.mts +452 -0
  4. package/dist/algorithms-DBpH74hR.mjs +3309 -0
  5. package/dist/algorithms.d.mts +2 -2
  6. package/dist/algorithms.mjs +2 -2
  7. package/dist/config-Dt5u1gSf.mjs +793 -0
  8. package/dist/{converter-udLITX36.mjs → converter-DB6Rg6Vd.mjs} +2 -2
  9. package/dist/format-support.mjs +38 -11
  10. package/dist/formats/adjacency-list/index.d.mts +1 -1
  11. package/dist/formats/adjacency-list/index.mjs +1 -1
  12. package/dist/formats/converter/index.d.mts +1 -1
  13. package/dist/formats/converter/index.mjs +1 -1
  14. package/dist/formats/cytoscape/index.d.mts +4 -4
  15. package/dist/formats/cytoscape/index.mjs +10 -4
  16. package/dist/formats/d2/index.d.mts +1 -1
  17. package/dist/formats/d2/index.mjs +26 -12
  18. package/dist/formats/d3/index.d.mts +4 -4
  19. package/dist/formats/d3/index.mjs +10 -4
  20. package/dist/formats/dot/index.d.mts +1 -1
  21. package/dist/formats/dot/index.mjs +22 -6
  22. package/dist/formats/edge-list/index.d.mts +1 -1
  23. package/dist/formats/edge-list/index.mjs +1 -1
  24. package/dist/formats/elk/index.d.mts +1 -1
  25. package/dist/formats/elk/index.mjs +63 -24
  26. package/dist/formats/gexf/index.d.mts +1 -1
  27. package/dist/formats/gexf/index.mjs +43 -16
  28. package/dist/formats/gml/index.d.mts +4 -4
  29. package/dist/formats/gml/index.mjs +28 -15
  30. package/dist/formats/graphml/index.d.mts +1 -1
  31. package/dist/formats/graphml/index.mjs +96 -23
  32. package/dist/formats/jgf/index.d.mts +4 -4
  33. package/dist/formats/jgf/index.mjs +12 -5
  34. package/dist/formats/mermaid/index.d.mts +1 -1
  35. package/dist/formats/mermaid/index.mjs +49 -12
  36. package/dist/formats/tgf/index.d.mts +4 -4
  37. package/dist/formats/tgf/index.mjs +4 -4
  38. package/dist/formats/xyflow/index.d.mts +12 -6
  39. package/dist/formats/xyflow/index.mjs +42 -10
  40. package/dist/{index-D9Kj6Fe3.d.mts → index-BlbSWUvH.d.mts} +1 -1
  41. package/dist/{index-CHoriXZD.d.mts → index-CNvqxPLJ.d.mts} +157 -30
  42. package/dist/index.d.mts +6 -6
  43. package/dist/index.mjs +290 -307
  44. package/dist/layout/cytoscape.d.mts +66 -0
  45. package/dist/layout/cytoscape.mjs +114 -0
  46. package/dist/layout/d3-force.d.mts +52 -0
  47. package/dist/layout/d3-force.mjs +127 -0
  48. package/dist/layout/d3-hierarchy.d.mts +39 -0
  49. package/dist/layout/d3-hierarchy.mjs +135 -0
  50. package/dist/layout/dagre.d.mts +32 -0
  51. package/dist/layout/dagre.mjs +99 -0
  52. package/dist/layout/elk.d.mts +47 -0
  53. package/dist/layout/elk.mjs +73 -0
  54. package/dist/layout/forceatlas2.d.mts +48 -0
  55. package/dist/layout/forceatlas2.mjs +100 -0
  56. package/dist/layout/graphviz.d.mts +50 -0
  57. package/dist/layout/graphviz.mjs +179 -0
  58. package/dist/layout/index.d.mts +185 -0
  59. package/dist/layout/index.mjs +181 -0
  60. package/dist/layout/webcola.d.mts +40 -0
  61. package/dist/layout/webcola.mjs +104 -0
  62. package/dist/{queries-BlkA1HAN.d.mts → queries-B6quF529.d.mts} +43 -12
  63. package/dist/queries-BMM0XAv_.mjs +986 -0
  64. package/dist/queries.d.mts +1 -1
  65. package/dist/queries.mjs +1 -768
  66. package/dist/schemas.d.mts +19 -1
  67. package/dist/schemas.mjs +32 -84
  68. package/dist/{types-3-FS9NV2.d.mts → types-BAEQTwK_.d.mts} +99 -7
  69. package/dist/validate-BsfSOv0S.mjs +190 -0
  70. package/package.json +59 -7
  71. package/schemas/edge.schema.json +27 -0
  72. package/schemas/graph.schema.json +27 -0
  73. package/dist/algorithms-Ba7o7niK.mjs +0 -2394
  74. package/dist/algorithms-fTqmvhzP.d.mts +0 -178
  75. package/dist/indexing-DR8M1vBy.mjs +0 -137
  76. /package/dist/{edge-list-DP4otyPU.mjs → edge-list-CA9UTvn2.mjs} +0 -0
  77. /package/dist/{mode-D8OnHFBk.mjs → mode-gu_mhKKs.mjs} +0 -0
@@ -1,4 +1,4 @@
1
- import { _ as GraphNode, b as GraphPort, p as GraphEdge, u as Graph } from "./types-3-FS9NV2.mjs";
1
+ import { S as GraphPort, f as Graph, h as GraphEdge, y as GraphNode } from "./types-BAEQTwK_.mjs";
2
2
  import * as z from "zod";
3
3
 
4
4
  //#region src/schemas.d.ts
@@ -61,6 +61,15 @@ declare const EdgeSchema: z.ZodObject<{
61
61
  undirected: "undirected";
62
62
  bidirectional: "bidirectional";
63
63
  }>>;
64
+ points: z.ZodOptional<z.ZodArray<z.ZodObject<{
65
+ x: z.ZodNumber;
66
+ y: z.ZodNumber;
67
+ }, z.core.$strip>>>;
68
+ routing: z.ZodOptional<z.ZodEnum<{
69
+ polyline: "polyline";
70
+ orthogonal: "orthogonal";
71
+ splines: "splines";
72
+ }>>;
64
73
  data: z.ZodAny;
65
74
  x: z.ZodOptional<z.ZodNumber>;
66
75
  y: z.ZodOptional<z.ZodNumber>;
@@ -121,6 +130,15 @@ declare const GraphSchema: z.ZodObject<{
121
130
  undirected: "undirected";
122
131
  bidirectional: "bidirectional";
123
132
  }>>;
133
+ points: z.ZodOptional<z.ZodArray<z.ZodObject<{
134
+ x: z.ZodNumber;
135
+ y: z.ZodNumber;
136
+ }, z.core.$strip>>>;
137
+ routing: z.ZodOptional<z.ZodEnum<{
138
+ polyline: "polyline";
139
+ orthogonal: "orthogonal";
140
+ splines: "splines";
141
+ }>>;
124
142
  data: z.ZodAny;
125
143
  x: z.ZodOptional<z.ZodNumber>;
126
144
  y: z.ZodOptional<z.ZodNumber>;
package/dist/schemas.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { t as getGraphIssues$1 } from "./validate-BsfSOv0S.mjs";
1
2
  import * as z from "zod";
2
3
 
3
4
  //#region src/schemas.ts
@@ -53,6 +54,15 @@ const EdgeSchema = z.object({
53
54
  sourcePort: z.string().optional(),
54
55
  targetPort: z.string().optional(),
55
56
  mode: ModeSchema.optional(),
57
+ points: z.array(z.object({
58
+ x: z.number(),
59
+ y: z.number()
60
+ })).optional(),
61
+ routing: z.enum([
62
+ "polyline",
63
+ "orthogonal",
64
+ "splines"
65
+ ]).optional(),
56
66
  data: z.any(),
57
67
  x: z.number().optional(),
58
68
  y: z.number().optional(),
@@ -116,37 +126,30 @@ function createIssue(code, message, path) {
116
126
  path
117
127
  };
118
128
  }
119
- function getDuplicateIndexes(items, getKey) {
120
- const indexesByKey = /* @__PURE__ */ new Map();
121
- items.forEach((item, index) => {
122
- const key = getKey(item);
123
- if (key == null) return;
124
- const indexes = indexesByKey.get(key) ?? [];
125
- indexes.push(index);
126
- indexesByKey.set(key, indexes);
127
- });
128
- for (const [key, indexes] of indexesByKey) if (indexes.length < 2) indexesByKey.delete(key);
129
- return indexesByKey;
129
+ /** Maps structural issue codes from `validate.ts` to this module's codes. */
130
+ const STRUCTURAL_CODE_MAP = {
131
+ "duplicate-node-id": "duplicate_node_id",
132
+ "duplicate-edge-id": "duplicate_edge_id",
133
+ "missing-initial-node": "missing_initial_node",
134
+ "missing-parent": "missing_parent",
135
+ "missing-node-initial": "missing_node_initial",
136
+ "duplicate-port-name": "duplicate_port_name",
137
+ "parent-cycle": "parent_cycle",
138
+ "missing-source-port": "missing_source_port",
139
+ "missing-target-port": "missing_target_port"
140
+ };
141
+ function toValidationIssue(issue) {
142
+ const path = issue.path ?? [];
143
+ let code = STRUCTURAL_CODE_MAP[issue.code] ?? issue.code;
144
+ if (issue.code === "dangling-edge-endpoint") code = path[path.length - 1] === "sourceId" ? "missing_source_node" : "missing_target_node";
145
+ return {
146
+ code,
147
+ message: issue.message,
148
+ path
149
+ };
130
150
  }
131
151
  function getGraphInvariantIssues(graph) {
132
- const issues = [];
133
- const nodeIndexes = /* @__PURE__ */ new Map();
134
- const nodesById = /* @__PURE__ */ new Map();
135
- for (const [id, indexes] of getDuplicateIndexes(graph.nodes, (node) => node.id)) for (const index of indexes) issues.push(createIssue("duplicate_node_id", `Duplicate node id "${id}"`, [
136
- "nodes",
137
- index,
138
- "id"
139
- ]));
140
- for (const [id, indexes] of getDuplicateIndexes(graph.edges, (edge) => edge.id)) for (const index of indexes) issues.push(createIssue("duplicate_edge_id", `Duplicate edge id "${id}"`, [
141
- "edges",
142
- index,
143
- "id"
144
- ]));
145
- graph.nodes.forEach((node, index) => {
146
- nodeIndexes.set(node.id, index);
147
- nodesById.set(node.id, node);
148
- });
149
- if (graph.initialNodeId && !nodeIndexes.has(graph.initialNodeId)) issues.push(createIssue("missing_initial_node", `Initial node "${graph.initialNodeId}" does not exist`, ["initialNodeId"]));
152
+ const issues = getGraphIssues$1(graph).map(toValidationIssue);
150
153
  graph.nodes.forEach((node, index) => {
151
154
  if (node.id === "") issues.push(createIssue("empty_node_id", "Node id must be a non-empty string", [
152
155
  "nodes",
@@ -158,68 +161,13 @@ function getGraphInvariantIssues(graph) {
158
161
  index,
159
162
  "parentId"
160
163
  ]));
161
- else if (node.parentId != null && !nodeIndexes.has(node.parentId)) issues.push(createIssue("missing_parent", `Parent node "${node.parentId}" does not exist`, [
162
- "nodes",
163
- index,
164
- "parentId"
165
- ]));
166
- if (node.initialNodeId && !nodeIndexes.has(node.initialNodeId)) issues.push(createIssue("missing_node_initial", `Initial node "${node.initialNodeId}" does not exist`, [
167
- "nodes",
168
- index,
169
- "initialNodeId"
170
- ]));
171
- for (const [name, indexes] of getDuplicateIndexes(node.ports ?? [], (port) => port.name)) for (const portIndex of indexes) issues.push(createIssue("duplicate_port_name", `Duplicate port name "${name}" on node "${node.id}"`, [
172
- "nodes",
173
- index,
174
- "ports",
175
- portIndex,
176
- "name"
177
- ]));
178
164
  });
179
- for (const node of graph.nodes) {
180
- const seen = /* @__PURE__ */ new Set();
181
- let current = node.parentId;
182
- while (current != null) {
183
- if (current === node.id || seen.has(current)) {
184
- issues.push(createIssue("parent_cycle", `Node "${node.id}" is part of a parent cycle`, [
185
- "nodes",
186
- nodeIndexes.get(node.id) ?? 0,
187
- "parentId"
188
- ]));
189
- break;
190
- }
191
- seen.add(current);
192
- current = nodesById.get(current)?.parentId;
193
- }
194
- }
195
165
  graph.edges.forEach((edge, index) => {
196
166
  if (edge.id === "") issues.push(createIssue("empty_edge_id", "Edge id must be a non-empty string", [
197
167
  "edges",
198
168
  index,
199
169
  "id"
200
170
  ]));
201
- const source = nodesById.get(edge.sourceId);
202
- const target = nodesById.get(edge.targetId);
203
- if (!source) issues.push(createIssue("missing_source_node", `Source node "${edge.sourceId}" does not exist`, [
204
- "edges",
205
- index,
206
- "sourceId"
207
- ]));
208
- if (!target) issues.push(createIssue("missing_target_node", `Target node "${edge.targetId}" does not exist`, [
209
- "edges",
210
- index,
211
- "targetId"
212
- ]));
213
- if (source && edge.sourcePort !== void 0 && !source.ports?.some((port) => port.name === edge.sourcePort)) issues.push(createIssue("missing_source_port", `Port "${edge.sourcePort}" does not exist on source node "${edge.sourceId}"`, [
214
- "edges",
215
- index,
216
- "sourcePort"
217
- ]));
218
- if (target && edge.targetPort !== void 0 && !target.ports?.some((port) => port.name === edge.targetPort)) issues.push(createIssue("missing_target_port", `Port "${edge.targetPort}" does not exist on target node "${edge.targetId}"`, [
219
- "edges",
220
- index,
221
- "targetPort"
222
- ]));
223
171
  });
224
172
  return issues;
225
173
  }
@@ -16,6 +16,20 @@ interface EntityRect {
16
16
  width: number;
17
17
  height: number;
18
18
  }
19
+ /** A 2D point, used for edge routing waypoints. */
20
+ interface Point {
21
+ x: number;
22
+ y: number;
23
+ }
24
+ /**
25
+ * How an edge's {@link GraphEdge.points} should be interpreted by renderers:
26
+ *
27
+ * - `'polyline'` — straight segments through the points.
28
+ * - `'orthogonal'` — axis-aligned segments (ELK layered routing).
29
+ * - `'splines'` — bezier control points (Graphviz convention: 3n+1 chained
30
+ * cubic curves, tail → head).
31
+ */
32
+ type EdgeRouting = 'polyline' | 'orthogonal' | 'splines';
19
33
  /** Shared optional visual/style props for nodes, edges, ports. */
20
34
  interface GraphEntity {
21
35
  x?: number;
@@ -55,7 +69,7 @@ interface GraphConfig<TNodeData = any, TEdgeData = any, TGraphData = any, TPortD
55
69
  id?: string;
56
70
  /** Default directedness for all edges. Defaults to `'directed'`. */
57
71
  mode?: GraphMode;
58
- initialNodeId?: string;
72
+ initialNodeId?: string | null;
59
73
  nodes?: NodeConfig<TNodeData, TPortData>[];
60
74
  edges?: EdgeConfig<TEdgeData>[];
61
75
  data?: TGraphData;
@@ -65,13 +79,20 @@ interface GraphConfig<TNodeData = any, TEdgeData = any, TGraphData = any, TPortD
65
79
  interface NodeConfig<TNodeData = any, TPortData = any> extends GraphEntity {
66
80
  id: string;
67
81
  parentId?: string | null;
68
- initialNodeId?: string;
82
+ initialNodeId?: string | null;
69
83
  label?: string | null;
70
84
  data?: TNodeData;
71
85
  ports?: PortConfig<TPortData>[];
72
86
  shape?: string;
73
87
  color?: string;
74
88
  }
89
+ /**
90
+ * Note on edge geometry: an edge's `x`/`y`/`width`/`height` are canonically
91
+ * the **label rect** (top-left + size). Layout adapters write computed edge
92
+ * label positions here, and engines that need label dimensions as input
93
+ * (dagre, ELK) read `width`/`height`. The edge's *route* lives in
94
+ * {@link EdgeConfig.points}.
95
+ */
75
96
  interface EdgeConfig<TEdgeData = any> extends GraphEntity {
76
97
  /**
77
98
  * The id of the edge.
@@ -104,6 +125,13 @@ interface EdgeConfig<TEdgeData = any> extends GraphEntity {
104
125
  * {@link GraphConfig.mode}.
105
126
  */
106
127
  mode?: GraphMode;
128
+ /**
129
+ * Edge route waypoints (including endpoints, tail → head), as computed by a
130
+ * layout engine. Interpretation is governed by {@link EdgeConfig.routing}.
131
+ */
132
+ points?: Point[];
133
+ /** How {@link EdgeConfig.points} should be interpreted. Default: polyline. */
134
+ routing?: EdgeRouting;
107
135
  data?: TEdgeData;
108
136
  color?: string;
109
137
  }
@@ -129,6 +157,11 @@ interface GraphNode<TNodeData = any, TPortData = any> extends GraphEntity {
129
157
  shape?: string;
130
158
  color?: string;
131
159
  }
160
+ /**
161
+ * Note on edge geometry: an edge's `x`/`y`/`width`/`height` are canonically
162
+ * the **label rect** (top-left + size); the edge's *route* lives in
163
+ * {@link GraphEdge.points}. See {@link EdgeConfig} for details.
164
+ */
132
165
  interface GraphEdge<TEdgeData = any> extends GraphEntity {
133
166
  type: 'edge';
134
167
  id: string;
@@ -150,6 +183,13 @@ interface GraphEdge<TEdgeData = any> extends GraphEntity {
150
183
  * {@link Graph.mode}.
151
184
  */
152
185
  mode?: GraphMode;
186
+ /**
187
+ * Edge route waypoints (including endpoints, tail → head), as computed by a
188
+ * layout engine. Interpretation is governed by {@link GraphEdge.routing}.
189
+ */
190
+ points?: Point[];
191
+ /** How {@link GraphEdge.points} should be interpreted. Default: polyline. */
192
+ routing?: EdgeRouting;
153
193
  data: TEdgeData;
154
194
  color?: string;
155
195
  }
@@ -177,15 +217,67 @@ interface VisualGraphConfig<TNodeData = any, TEdgeData = any, TGraphData = any,
177
217
  interface DeleteNodeOptions {
178
218
  reparent?: boolean;
179
219
  }
220
+ /**
221
+ * Update payload for {@link updateNode}/`updateEntities`.
222
+ *
223
+ * Optional fields (`x`, `y`, `width`, `height`, `shape`, `color`, `style`,
224
+ * `ports`) accept `null` to **unset** the field. `undefined` (or omitting the
225
+ * key) leaves the field unchanged. `null` is used for unsetting so update
226
+ * payloads stay JSON-serializable.
227
+ */
228
+ interface NodeUpdate<TNodeData = any, TPortData = any> {
229
+ parentId?: string | null;
230
+ initialNodeId?: string | null;
231
+ label?: string | null;
232
+ data?: TNodeData;
233
+ /** New ports for the node, or `null` to remove all ports. */
234
+ ports?: PortConfig<TPortData>[] | null;
235
+ x?: number | null;
236
+ y?: number | null;
237
+ width?: number | null;
238
+ height?: number | null;
239
+ shape?: string | null;
240
+ color?: string | null;
241
+ style?: Record<string, string | number | boolean> | null;
242
+ }
243
+ /**
244
+ * Update payload for {@link updateEdge}/`updateEntities`.
245
+ *
246
+ * Optional fields (`weight`, `mode`, `sourcePort`, `targetPort`, `x`, `y`,
247
+ * `width`, `height`, `color`, `style`) accept `null` to **unset** the field.
248
+ * `undefined` (or omitting the key) leaves the field unchanged. `null` is
249
+ * used for unsetting so update payloads stay JSON-serializable.
250
+ */
251
+ interface EdgeUpdate<TEdgeData = any> {
252
+ sourceId?: string;
253
+ targetId?: string;
254
+ label?: string | null;
255
+ data?: TEdgeData;
256
+ weight?: number | null;
257
+ mode?: GraphMode | null;
258
+ /** Port name on the source node, or `null` to clear the port reference. */
259
+ sourcePort?: string | null;
260
+ /** Port name on the target node, or `null` to clear the port reference. */
261
+ targetPort?: string | null;
262
+ /** Edge route waypoints, or `null` to clear the route. */
263
+ points?: Point[] | null;
264
+ routing?: EdgeRouting | null;
265
+ x?: number | null;
266
+ y?: number | null;
267
+ width?: number | null;
268
+ height?: number | null;
269
+ color?: string | null;
270
+ style?: Record<string, string | number | boolean> | null;
271
+ }
180
272
  interface EntitiesConfig<TNodeData = any, TEdgeData = any, TPortData = any> {
181
273
  nodes?: NodeConfig<TNodeData, TPortData>[];
182
274
  edges?: EdgeConfig<TEdgeData>[];
183
275
  }
184
276
  interface EntitiesUpdate<TNodeData = any, TEdgeData = any, TPortData = any> {
185
- nodes?: (Partial<Omit<NodeConfig<TNodeData, TPortData>, 'id'>> & {
277
+ nodes?: (NodeUpdate<TNodeData, TPortData> & {
186
278
  id: string;
187
279
  })[];
188
- edges?: (Partial<Omit<EdgeConfig<TEdgeData>, 'id'>> & {
280
+ edges?: (EdgeUpdate<TEdgeData> & {
189
281
  id: string;
190
282
  })[];
191
283
  }
@@ -281,7 +373,7 @@ type GraphPatch<TNodeData = any, TEdgeData = any> = {
281
373
  } | {
282
374
  op: 'updateNode';
283
375
  id: string;
284
- data: Partial<Omit<NodeConfig<TNodeData>, 'id'>>;
376
+ data: NodeUpdate<TNodeData>;
285
377
  description?: string;
286
378
  } | {
287
379
  op: 'deleteNode';
@@ -294,7 +386,7 @@ type GraphPatch<TNodeData = any, TEdgeData = any> = {
294
386
  } | {
295
387
  op: 'updateEdge';
296
388
  id: string;
297
- data: Partial<Omit<EdgeConfig<TEdgeData>, 'id'>>;
389
+ data: EdgeUpdate<TEdgeData>;
298
390
  description?: string;
299
391
  } | {
300
392
  op: 'deleteEdge';
@@ -375,4 +467,4 @@ interface TransitionOptions<TState, TEvent> {
375
467
  id?: string;
376
468
  }
377
469
  //#endregion
378
- export { TraversalOptions as A, WeightedWalkOptions as B, NodeChange as C, PortDirection as D, PortConfig as E, VisualGraphFormatConverter as F, VisualNode as I, VisualPort as L, VisualGraph as M, VisualGraphConfig as N, SinglePathOptions as O, VisualGraphEntity as P, WalkContext as R, MSTOptions as S, PathOptions as T, GraphNode as _, EdgeChange as a, GraphPort as b, EntitiesUpdate as c, GraphConfig as d, GraphDiff as f, GraphMode as g, GraphFormatConverter as h, DeleteNodeOptions as i, VisualEdge as j, TransitionOptions as k, EntityRect as l, GraphEntity as m, AllPairsShortestPathsOptions as n, EdgeConfig as o, GraphEdge as p, CoverageStats as r, EntitiesConfig as s, AStarOptions as t, Graph as u, GraphPatch as v, NodeConfig as w, GraphStep as x, GraphPath as y, WalkOptions as z };
470
+ export { PortConfig as A, VisualNode as B, GraphStep as C, NodeUpdate as D, NodeConfig as E, VisualEdge as F, WalkContext as H, VisualGraph as I, VisualGraphConfig as L, SinglePathOptions as M, TransitionOptions as N, PathOptions as O, TraversalOptions as P, VisualGraphEntity as R, GraphPort as S, NodeChange as T, WalkOptions as U, VisualPort as V, WeightedWalkOptions as W, GraphFormatConverter as _, EdgeChange as a, GraphPatch as b, EdgeUpdate as c, EntityRect as d, Graph as f, GraphEntity as g, GraphEdge as h, DeleteNodeOptions as i, PortDirection as j, Point as k, EntitiesConfig as l, GraphDiff as m, AllPairsShortestPathsOptions as n, EdgeConfig as o, GraphConfig as p, CoverageStats as r, EdgeRouting as s, AStarOptions as t, EntitiesUpdate as u, GraphMode as v, MSTOptions as w, GraphPath as x, GraphNode as y, VisualGraphFormatConverter as z };
@@ -0,0 +1,190 @@
1
+ //#region src/validate.ts
2
+ function getDuplicateIndexes(items, getKey) {
3
+ const indexesByKey = /* @__PURE__ */ new Map();
4
+ items.forEach((item, index) => {
5
+ const key = getKey(item);
6
+ if (key == null) return;
7
+ const indexes = indexesByKey.get(key) ?? [];
8
+ indexes.push(index);
9
+ indexesByKey.set(key, indexes);
10
+ });
11
+ for (const [key, indexes] of indexesByKey) if (indexes.length < 2) indexesByKey.delete(key);
12
+ return indexesByKey;
13
+ }
14
+ /**
15
+ * Returns the parent cycles in a graph's hierarchy, each cycle reported once
16
+ * as the list of node ids forming the cycle (cycle members only — nodes whose
17
+ * ancestry merely *leads into* a cycle are not included).
18
+ */
19
+ function getParentCycles(nodesById) {
20
+ const cycles = [];
21
+ const visited = /* @__PURE__ */ new Set();
22
+ for (const startId of nodesById.keys()) {
23
+ if (visited.has(startId)) continue;
24
+ const path = [];
25
+ const positions = /* @__PURE__ */ new Map();
26
+ let currentId = startId;
27
+ while (currentId != null && !visited.has(currentId) && nodesById.has(currentId)) {
28
+ const position = positions.get(currentId);
29
+ if (position !== void 0) {
30
+ cycles.push(path.slice(position));
31
+ break;
32
+ }
33
+ positions.set(currentId, path.length);
34
+ path.push(currentId);
35
+ currentId = nodesById.get(currentId).parentId;
36
+ }
37
+ for (const id of path) visited.add(id);
38
+ }
39
+ return cycles;
40
+ }
41
+ /**
42
+ * Validates the structural invariants of a graph and returns the issues
43
+ * found, or `[]` when the graph is valid. Pure — never throws, never mutates.
44
+ *
45
+ * This is the recommended gate for untrusted or imported graphs (e.g. parsed
46
+ * from a file or received over the wire) before handing them to queries and
47
+ * algorithms: the mutation APIs (`addNode`, `addEdge`, `updateNode`, …)
48
+ * validate incrementally, but `createGraph` does **not** — it accepts
49
+ * dangling `parentId`/edge references and even `parentId` cycles as-is.
50
+ *
51
+ * For Zod-based shape validation of arbitrary unknown values, see
52
+ * `validateGraph` in `@statelyai/graph/schemas` (which reuses these checks).
53
+ *
54
+ * Issue codes: `duplicate-node-id`, `duplicate-edge-id`,
55
+ * `missing-initial-node`, `missing-parent`, `missing-node-initial`,
56
+ * `duplicate-port-name`, `parent-cycle`, `dangling-edge-endpoint`,
57
+ * `missing-source-port`, `missing-target-port`.
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * const graph = createGraph({
62
+ * nodes: [{ id: 'a', parentId: 'ghost' }],
63
+ * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b' }],
64
+ * });
65
+ * getGraphIssues(graph);
66
+ * // => [
67
+ * // { code: 'missing-parent', message: '...', path: ['nodes', 0, 'parentId'] },
68
+ * // { code: 'dangling-edge-endpoint', message: '...', path: ['edges', 0, 'targetId'] },
69
+ * // ]
70
+ * ```
71
+ */
72
+ function getGraphIssues(graph) {
73
+ const issues = [];
74
+ const nodeIndexes = /* @__PURE__ */ new Map();
75
+ const nodesById = /* @__PURE__ */ new Map();
76
+ graph.nodes.forEach((node, index) => {
77
+ nodeIndexes.set(node.id, index);
78
+ nodesById.set(node.id, node);
79
+ });
80
+ for (const [id, indexes] of getDuplicateIndexes(graph.nodes, (node) => node.id)) for (const index of indexes) issues.push({
81
+ code: "duplicate-node-id",
82
+ message: `Duplicate node id "${id}". Node ids must be unique; rename or remove the duplicates.`,
83
+ path: [
84
+ "nodes",
85
+ index,
86
+ "id"
87
+ ]
88
+ });
89
+ for (const [id, indexes] of getDuplicateIndexes(graph.edges, (edge) => edge.id)) for (const index of indexes) issues.push({
90
+ code: "duplicate-edge-id",
91
+ message: `Duplicate edge id "${id}". Edge ids must be unique; rename or remove the duplicates.`,
92
+ path: [
93
+ "edges",
94
+ index,
95
+ "id"
96
+ ]
97
+ });
98
+ if (graph.initialNodeId && !nodesById.has(graph.initialNodeId)) issues.push({
99
+ code: "missing-initial-node",
100
+ message: `Graph initialNodeId references missing node "${graph.initialNodeId}". Add that node or update the graph's initialNodeId.`,
101
+ path: ["initialNodeId"]
102
+ });
103
+ graph.nodes.forEach((node, index) => {
104
+ if (node.parentId != null && !nodesById.has(node.parentId)) issues.push({
105
+ code: "missing-parent",
106
+ message: `Node "${node.id}" has parentId "${node.parentId}", which does not exist. Add that node or remove the parentId.`,
107
+ path: [
108
+ "nodes",
109
+ index,
110
+ "parentId"
111
+ ]
112
+ });
113
+ if (node.initialNodeId && !nodesById.has(node.initialNodeId)) issues.push({
114
+ code: "missing-node-initial",
115
+ message: `Node "${node.id}" has initialNodeId "${node.initialNodeId}", which does not exist. Add that node or remove the initialNodeId.`,
116
+ path: [
117
+ "nodes",
118
+ index,
119
+ "initialNodeId"
120
+ ]
121
+ });
122
+ for (const [name, portIndexes] of getDuplicateIndexes(node.ports ?? [], (port) => port.name)) for (const portIndex of portIndexes) issues.push({
123
+ code: "duplicate-port-name",
124
+ message: `Duplicate port name "${name}" on node "${node.id}". Port names must be unique per node; rename or remove the duplicates.`,
125
+ path: [
126
+ "nodes",
127
+ index,
128
+ "ports",
129
+ portIndex,
130
+ "name"
131
+ ]
132
+ });
133
+ });
134
+ for (const cycle of getParentCycles(nodesById)) {
135
+ const chain = [...cycle, cycle[0]].join(" → ");
136
+ issues.push({
137
+ code: "parent-cycle",
138
+ message: `Parent cycle detected: ${chain}. Break the cycle by changing the parentId of one of these nodes.`,
139
+ path: [
140
+ "nodes",
141
+ nodeIndexes.get(cycle[0]) ?? 0,
142
+ "parentId"
143
+ ]
144
+ });
145
+ }
146
+ graph.edges.forEach((edge, index) => {
147
+ const source = nodesById.get(edge.sourceId);
148
+ const target = nodesById.get(edge.targetId);
149
+ if (!source) issues.push({
150
+ code: "dangling-edge-endpoint",
151
+ message: `Edge "${edge.id}" has sourceId "${edge.sourceId}", which references a missing node. Add that node or fix the edge's sourceId.`,
152
+ path: [
153
+ "edges",
154
+ index,
155
+ "sourceId"
156
+ ]
157
+ });
158
+ if (!target) issues.push({
159
+ code: "dangling-edge-endpoint",
160
+ message: `Edge "${edge.id}" has targetId "${edge.targetId}", which references a missing node. Add that node or fix the edge's targetId.`,
161
+ path: [
162
+ "edges",
163
+ index,
164
+ "targetId"
165
+ ]
166
+ });
167
+ if (source && edge.sourcePort !== void 0 && !source.ports?.some((port) => port.name === edge.sourcePort)) issues.push({
168
+ code: "missing-source-port",
169
+ message: `Edge "${edge.id}" has sourcePort "${edge.sourcePort}", but source node "${edge.sourceId}" has no port with that name. Add the port or fix the edge's sourcePort.`,
170
+ path: [
171
+ "edges",
172
+ index,
173
+ "sourcePort"
174
+ ]
175
+ });
176
+ if (target && edge.targetPort !== void 0 && !target.ports?.some((port) => port.name === edge.targetPort)) issues.push({
177
+ code: "missing-target-port",
178
+ message: `Edge "${edge.id}" has targetPort "${edge.targetPort}", but target node "${edge.targetId}" has no port with that name. Add the port or fix the edge's targetPort.`,
179
+ path: [
180
+ "edges",
181
+ index,
182
+ "targetPort"
183
+ ]
184
+ });
185
+ });
186
+ return issues;
187
+ }
188
+
189
+ //#endregion
190
+ export { getGraphIssues as t };