@statelyai/graph 0.12.0 → 1.0.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 (56) hide show
  1. package/README.md +26 -16
  2. package/dist/{adjacency-list-Ca0VjKIf.mjs → adjacency-list-VsUaH9SJ.mjs} +2 -2
  3. package/dist/{algorithms-BHHg7lGq.mjs → algorithms-Ba7o7niK.mjs} +35 -31
  4. package/dist/{algorithms-BlM-qoJb.d.mts → algorithms-fTqmvhzP.d.mts} +1 -1
  5. package/dist/algorithms.d.mts +1 -1
  6. package/dist/algorithms.mjs +1 -1
  7. package/dist/{converter-Dspillnn.mjs → converter-udLITX36.mjs} +2 -2
  8. package/dist/{edge-list-gKe8-iRa.mjs → edge-list-DP4otyPU.mjs} +1 -1
  9. package/dist/format-support.d.mts +6 -0
  10. package/dist/format-support.mjs +68 -33
  11. package/dist/formats/adjacency-list/index.d.mts +1 -1
  12. package/dist/formats/adjacency-list/index.mjs +1 -1
  13. package/dist/formats/converter/index.d.mts +1 -60
  14. package/dist/formats/converter/index.mjs +1 -1
  15. package/dist/formats/cytoscape/index.d.mts +1 -1
  16. package/dist/formats/cytoscape/index.mjs +19 -3
  17. package/dist/formats/d2/index.d.mts +109 -0
  18. package/dist/formats/d2/index.mjs +1086 -0
  19. package/dist/formats/d3/index.d.mts +8 -1
  20. package/dist/formats/d3/index.mjs +35 -7
  21. package/dist/formats/dot/index.d.mts +1 -1
  22. package/dist/formats/dot/index.mjs +3 -3
  23. package/dist/formats/edge-list/index.d.mts +1 -1
  24. package/dist/formats/edge-list/index.mjs +1 -1
  25. package/dist/formats/elk/index.d.mts +1 -1
  26. package/dist/formats/elk/index.mjs +86 -23
  27. package/dist/formats/gexf/index.d.mts +1 -1
  28. package/dist/formats/gexf/index.mjs +102 -4
  29. package/dist/formats/gml/index.d.mts +1 -1
  30. package/dist/formats/gml/index.mjs +43 -16
  31. package/dist/formats/graphml/index.d.mts +1 -1
  32. package/dist/formats/graphml/index.mjs +11 -4
  33. package/dist/formats/jgf/index.d.mts +1 -1
  34. package/dist/formats/jgf/index.mjs +19 -3
  35. package/dist/formats/mermaid/index.d.mts +2 -1
  36. package/dist/formats/mermaid/index.mjs +48 -18
  37. package/dist/formats/tgf/index.d.mts +1 -1
  38. package/dist/formats/tgf/index.mjs +2 -2
  39. package/dist/formats/xyflow/index.d.mts +2 -1
  40. package/dist/formats/xyflow/index.mjs +97 -40
  41. package/dist/index-CHoriXZD.d.mts +638 -0
  42. package/dist/index-D9Kj6Fe3.d.mts +61 -0
  43. package/dist/index.d.mts +6 -631
  44. package/dist/index.mjs +8 -7
  45. package/dist/{indexing-CJc-ul8e.mjs → indexing-DR8M1vBy.mjs} +18 -4
  46. package/dist/mode-D8OnHFBk.mjs +15 -0
  47. package/dist/queries-BlkA1HAN.d.mts +516 -0
  48. package/dist/queries.d.mts +1 -514
  49. package/dist/queries.mjs +17 -15
  50. package/dist/schemas.d.mts +29 -17
  51. package/dist/schemas.mjs +139 -6
  52. package/dist/{types-CnZ01raw.d.mts → types-3-FS9NV2.d.mts} +30 -7
  53. package/package.json +2 -1
  54. package/schemas/edge.schema.json +11 -0
  55. package/schemas/graph.schema.json +25 -3
  56. package/schemas/node.schema.json +7 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @statelyai/graph
2
2
 
3
- A TypeScript graph library built on plain JSON objects. Supports directed/undirected graphs, hierarchical nodes, graph algorithms, visual properties, and serialization to DOT, GraphML, Mermaid, and more.
3
+ A TypeScript graph library for JSON-serializable graph IR. Use it to validate, analyze, transform, and round-trip directed, undirected, hierarchical, port-aware, and visual graphs across tools.
4
4
 
5
5
  Made from our experience at [stately.ai](https://stately.ai), where we build visual tools for complex systems.
6
6
 
@@ -25,13 +25,13 @@ Optional peers are only needed for specific adapters:
25
25
 
26
26
  ## Highlights
27
27
 
28
- - Plain JSON graphs with no runtime wrappers required
28
+ - Plain JSON graphs with no runtime wrappers required; omitted `data` defaults to `null`
29
29
  - Standalone functions with a consistent `get*`/`gen*`/`is*`/`add*` naming model
30
30
  - Directed, undirected, hierarchical, and visual graph support
31
31
  - Ports for node-editor and dataflow-style graphs
32
32
  - Algorithms for traversal, paths, centrality, communities, connectivity, isomorphism, ordering, MST, and walks
33
33
  - Diff/patch utilities for graph state changes
34
- - Multi-format conversion via package subpaths
34
+ - Multi-format conversion via package subpaths, with fidelity claims tested against fixtures
35
35
  - Small, fast test suite with broad format coverage
36
36
 
37
37
  ## Quick Start
@@ -149,17 +149,17 @@ getEdgesByPort(graph, 'render', 'input'); // [e1]
149
149
 
150
150
  <!-- validation helpers exported from src/schemas.ts -->
151
151
 
152
- Use the `@statelyai/graph/schemas` subpath when you want runtime validation or JSON Schema generation.
152
+ Use the `@statelyai/graph/schemas` subpath when you want runtime validation or JSON Schema generation. `validateGraph()` combines shape checks with graph invariants such as duplicate ids, dangling edges, missing parents, missing initial nodes, duplicate ports, invalid port references, and parent cycles.
153
153
 
154
154
  ```ts
155
- import { GraphSchema, getGraphIssues, isGraph } from '@statelyai/graph/schemas';
155
+ import { GraphSchema, isGraph, validateGraph } from '@statelyai/graph/schemas';
156
156
 
157
157
  const unknownValue: unknown = JSON.parse(input);
158
158
 
159
159
  if (isGraph(unknownValue)) {
160
160
  // fully typed Graph
161
161
  } else {
162
- console.error(getGraphIssues(unknownValue));
162
+ console.error(validateGraph(unknownValue));
163
163
  }
164
164
 
165
165
  const parsed = GraphSchema.parse(unknownValue);
@@ -252,7 +252,7 @@ const imported = fromGEXF(gexfXmlString); // GEXF (Gephi)
252
252
 
253
253
  <!-- supported format adapters derived from src/formats/* subdirectories -->
254
254
 
255
- **Supported formats:** Cytoscape.js JSON, D3.js JSON, JSON Graph Format, GEXF, GraphML, GML, TGF, DOT, Mermaid (flowchart, state, sequence, class, ER, mindmap, block, Ishikawa), ELK, xyflow, adjacency list, and edge list.
255
+ **Supported formats:** Cytoscape.js JSON, D3.js JSON, D2, JSON Graph Format, GEXF, GraphML, GML, TGF, DOT, Mermaid (flowchart, state, sequence, class, ER, mindmap, block, Ishikawa), ELK, xyflow, adjacency list, and edge list.
256
256
 
257
257
  Each bidirectional format also has a converter object:
258
258
 
@@ -263,6 +263,11 @@ const cyto = cytoscapeConverter.to(graph);
263
263
  const back = cytoscapeConverter.from(cyto);
264
264
  ```
265
265
 
266
+ Round-trip fidelity may use adapter-specific graph, node, and edge `data`
267
+ metadata when the target format does not have a native field for a source
268
+ concept. A `partial` round-trip entry means the adapter still drops meaningful
269
+ source information instead of preserving it as metadata.
270
+
266
271
  ## Format Support
267
272
 
268
273
  <!-- format support matrix derived from src/formats/support.ts -->
@@ -270,17 +275,18 @@ const back = cytoscapeConverter.from(cyto);
270
275
  | Format | Hierarchy | Ports | Visual | Round-trip | Notes |
271
276
  | ------------------- | --------- | ------- | ------- | ---------- | -------------------------------------------------------------------------- |
272
277
  | `adjacency-list` | none | none | none | partial | Connectivity only; edge metadata is lost. |
273
- | `cytoscape` | full | full | partial | partial | Ports round-trip through element data. |
274
- | `d3` | none | full | partial | partial | Ports round-trip through node/link objects. |
278
+ | `cytoscape` | full | full | full | full | Graph, node, and edge metadata round-trip through element data. |
279
+ | `d3` | full | full | full | full | Graph, node, and edge metadata round-trip through the loose JSON shape. |
280
+ | `d2` | full | full | full | full | D2 syntax, hierarchy, ports, styles, and connector modes round-trip. |
275
281
  | `dot` | partial | partial | partial | partial | Edge port ids round-trip, but `:port:compass` mapping is still incomplete. |
276
282
  | `edge-list` | none | none | none | partial | Endpoints only. |
277
- | `elk` | full | full | full | partial | Best for layout exchange. |
278
- | `gexf` | full | full | partial | partial | Ports round-trip via custom attributes. |
279
- | `gml` | full | full | partial | partial | Ports round-trip through JSON-stringified metadata. |
283
+ | `elk` | full | full | full | full | Metadata round-trips through reserved layout options. |
284
+ | `gexf` | full | full | full | full | Custom attributes preserve metadata beyond the standard viz module. |
285
+ | `gml` | full | full | full | full | Graph, node, and edge metadata round-trip through direct and JSON fields. |
280
286
  | `graphml` | full | full | partial | partial | Ports round-trip through `<data>` fields. |
281
- | `jgf` | full | full | none | partial | Ports round-trip through metadata. |
287
+ | `jgf` | full | full | full | full | Graph, node, and edge metadata round-trip through `metadata` objects. |
282
288
  | `tgf` | none | none | none | partial | Minimal ids and labels only. |
283
- | `xyflow` | none | full | full | partial | Ports map directly to handles. |
289
+ | `xyflow` | full | full | full | full | Metadata round-trips through reserved data fields. |
284
290
  | `mermaid/block` | partial | none | partial | partial | Syntax-driven, not port-aware. |
285
291
  | `mermaid/class` | none | none | none | partial | Class syntax is stored conservatively. |
286
292
  | `mermaid/er` | none | none | none | partial | Focuses on entities and cardinality. |
@@ -288,7 +294,7 @@ const back = cytoscapeConverter.from(cyto);
288
294
  | `mermaid/ishikawa` | full | none | none | partial | Preserves hierarchy, not fishbone layout. |
289
295
  | `mermaid/mindmap` | full | none | partial | partial | Icon syntax is not fully re-emitted. |
290
296
  | `mermaid/sequence` | partial | none | none | partial | Actor links and menu syntax are incomplete. |
291
- | `mermaid/state` | full | none | partial | partial | State notes are still lossy. |
297
+ | `mermaid/state` | full | none | partial | full | State syntax round-trips through graph and node data. |
292
298
 
293
299
  Some formats have optional peer dependencies: `fast-xml-parser` (GEXF, GraphML) and `dotparser` (DOT). All other formats are dependency-free.
294
300
 
@@ -299,6 +305,7 @@ Format-specific docs live alongside the source:
299
305
  - [Adjacency list](./src/formats/adjacency-list/README.md)
300
306
  - [Cytoscape](./src/formats/cytoscape/README.md)
301
307
  - [D3](./src/formats/d3/README.md)
308
+ - [D2](./src/formats/d2/README.md)
302
309
  - [DOT](./src/formats/dot/README.md)
303
310
  - [Edge list](./src/formats/edge-list/README.md)
304
311
  - [ELK](./src/formats/elk/README.md)
@@ -327,13 +334,14 @@ The repo includes runnable examples under [`examples/`](./examples):
327
334
  ```bash
328
335
  pnpm install
329
336
  pnpm verify
337
+ pnpm bench
330
338
  ```
331
339
 
332
340
  See [CONTRIBUTING.md](./CONTRIBUTING.md) for contributor conventions, format-module checklist, and release notes guidance.
333
341
 
334
342
  ## Why this library?
335
343
 
336
- Graph file formats define how to _store_ graphs. Visualization libraries define how to _render_ them. This library is the computational layer in between: plain JSON objects in, algorithms and mutations, plain JSON objects out.
344
+ Graph file formats define how to _store_ graphs. Visualization libraries define how to _render_ them. This library is the trusted interchange and analysis layer in between: plain JSON objects in, validation, algorithms, transforms, diffing, and format-preserving conversion out.
337
345
 
338
346
  ```
339
347
  GEXF file → fromGEXF() → Graph → run algorithms, mutate → toCytoscapeJSON() → render
@@ -341,6 +349,8 @@ GEXF file → fromGEXF() → Graph → run algorithms, mutate → toCytoscapeJSO
341
349
 
342
350
  Your `Graph` is a plain object that survives `JSON.stringify`, `structuredClone`, `postMessage`, and `localStorage` without adapters.
343
351
 
352
+ A canonical graph is a deterministic projection of a graph for comparison, hashing, snapshots, or caches. A future pure helper would return a new graph with stable node/edge ordering and normalized optional fields. A hash would be a digest of that canonical JSON. A summary would be a small structural report, for example node count, edge count, roots, sinks, component count, compound depth, port count, and whether the graph is acyclic. A pure `sortGraph()` would return a sorted copy and never mutate the input.
353
+
344
354
  ## License
345
355
 
346
356
  MIT
@@ -20,7 +20,7 @@ function toAdjacencyList(graph) {
20
20
  for (const node of graph.nodes) adj[node.id] = [];
21
21
  for (const edge of graph.edges) {
22
22
  adj[edge.sourceId]?.push(edge.targetId);
23
- if (graph.type === "undirected") adj[edge.targetId]?.push(edge.sourceId);
23
+ if (graph.mode !== "directed") adj[edge.targetId]?.push(edge.sourceId);
24
24
  }
25
25
  return adj;
26
26
  }
@@ -70,7 +70,7 @@ function fromAdjacencyList(adj, options) {
70
70
  }
71
71
  return {
72
72
  id: options?.id ?? "",
73
- type: directed ? "directed" : "undirected",
73
+ mode: directed ? "directed" : "undirected",
74
74
  initialNodeId: null,
75
75
  nodes,
76
76
  edges,
@@ -1,4 +1,5 @@
1
- import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-CJc-ul8e.mjs";
1
+ import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdge, o as invalidateIndex, r as indexAddNode, t as getIndex } from "./indexing-DR8M1vBy.mjs";
2
+ import { t as getEdgeMode } from "./mode-D8OnHFBk.mjs";
2
3
 
3
4
  //#region src/graph.ts
4
5
  /**
@@ -7,7 +8,7 @@ import { a as indexUpdateEdgeEndpoints, i as indexReparentNode, n as indexAddEdg
7
8
  * @example
8
9
  * ```ts
9
10
  * const port = createGraphPort({ name: 'output', direction: 'out' });
10
- * // { name: 'output', direction: 'out', data: undefined }
11
+ * // { name: 'output', direction: 'out', data: null }
11
12
  * ```
12
13
  */
13
14
  function createGraphPort(config) {
@@ -15,7 +16,7 @@ function createGraphPort(config) {
15
16
  const port = {
16
17
  name: config.name,
17
18
  direction: config.direction ?? "inout",
18
- data: config.data
19
+ data: config.data ?? null
19
20
  };
20
21
  if (config.label !== void 0) port.label = config.label;
21
22
  if (config.x !== void 0) port.x = config.x;
@@ -50,7 +51,7 @@ function createGraphNode(config) {
50
51
  ...config.parentId !== void 0 && { parentId: config.parentId ?? null },
51
52
  ...config.initialNodeId !== void 0 && { initialNodeId: config.initialNodeId ?? null },
52
53
  label: config.label ?? null,
53
- data: config.data
54
+ data: config.data ?? null
54
55
  };
55
56
  if (config.ports !== void 0 && config.ports.length > 0) {
56
57
  validatePortNames(config.ports);
@@ -71,7 +72,7 @@ function createGraphNode(config) {
71
72
  * @example
72
73
  * ```ts
73
74
  * const edge = createGraphEdge({ id: 'e1', sourceId: 'a', targetId: 'b' });
74
- * // { type: 'edge', id: 'e1', sourceId: 'a', targetId: 'b', label: null, data: undefined }
75
+ * // { type: 'edge', id: 'e1', sourceId: 'a', targetId: 'b', label: null, data: null }
75
76
  * ```
76
77
  */
77
78
  function createGraphEdge(config) {
@@ -84,10 +85,11 @@ function createGraphEdge(config) {
84
85
  sourceId: config.sourceId,
85
86
  targetId: config.targetId,
86
87
  label: config.label ?? null,
87
- data: config.data
88
+ data: config.data ?? null
88
89
  };
89
90
  if (config.sourcePort !== void 0) edge.sourcePort = config.sourcePort;
90
91
  if (config.targetPort !== void 0) edge.targetPort = config.targetPort;
92
+ if (config.mode !== void 0) edge.mode = config.mode;
91
93
  if (config.weight !== void 0) edge.weight = config.weight;
92
94
  if (config.x !== void 0) edge.x = config.x;
93
95
  if (config.y !== void 0) edge.y = config.y;
@@ -111,11 +113,11 @@ function createGraphEdge(config) {
111
113
  function createGraph(config) {
112
114
  const graph = {
113
115
  id: config?.id ?? "",
114
- type: config?.type ?? "directed",
116
+ mode: config?.mode ?? "directed",
115
117
  initialNodeId: config?.initialNodeId ?? null,
116
118
  nodes: (config?.nodes ?? []).map(createGraphNode),
117
119
  edges: (config?.edges ?? []).map(createGraphEdge),
118
- data: config?.data ?? void 0
120
+ data: config?.data ?? null
119
121
  };
120
122
  if (config?.direction !== void 0) graph.direction = config.direction;
121
123
  if (config?.style !== void 0) graph.style = config.style;
@@ -244,7 +246,7 @@ function createGraphFromTransition(transition, options) {
244
246
  }
245
247
  return createGraph({
246
248
  id: options.id ?? "",
247
- type: "directed",
249
+ mode: "directed",
248
250
  initialNodeId: initialStateId,
249
251
  nodes,
250
252
  edges
@@ -582,8 +584,9 @@ var GraphInstance = class GraphInstance {
582
584
  get id() {
583
585
  return this.graph.id;
584
586
  }
585
- get type() {
586
- return this.graph.type;
587
+ /** Default directedness for all edges. */
588
+ get mode() {
589
+ return this.graph.mode;
587
590
  }
588
591
  get nodes() {
589
592
  return this.graph.nodes;
@@ -703,9 +706,11 @@ function getNeighborIds$1(graph, nodeId) {
703
706
  const ai = idx.edgeById.get(eid);
704
707
  if (ai !== void 0) ids.push(graph.edges[ai].targetId);
705
708
  }
706
- if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
709
+ for (const eid of idx.inEdges.get(nodeId) ?? []) {
707
710
  const ai = idx.edgeById.get(eid);
708
- if (ai !== void 0) ids.push(graph.edges[ai].sourceId);
711
+ if (ai === void 0) continue;
712
+ const edge = graph.edges[ai];
713
+ if (getEdgeMode(graph, edge) !== "directed") ids.push(edge.sourceId);
709
714
  }
710
715
  return ids;
711
716
  }
@@ -736,11 +741,11 @@ function getNeighborEdges(graph, nodeId) {
736
741
  });
737
742
  }
738
743
  }
739
- if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
744
+ for (const eid of idx.inEdges.get(nodeId) ?? []) {
740
745
  const ai = idx.edgeById.get(eid);
741
746
  if (ai !== void 0) {
742
747
  const edge = graph.edges[ai];
743
- result.push({
748
+ if (getEdgeMode(graph, edge) !== "directed") result.push({
744
749
  neighborId: edge.sourceId,
745
750
  edge
746
751
  });
@@ -844,7 +849,6 @@ function bellmanFord(graph, sourceId, getWeight) {
844
849
  const dist = /* @__PURE__ */ new Map();
845
850
  const prev = /* @__PURE__ */ new Map();
846
851
  const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
847
- const isUndirected = graph.type === "undirected";
848
852
  for (const node of graph.nodes) {
849
853
  dist.set(node.id, Infinity);
850
854
  prev.set(node.id, []);
@@ -857,7 +861,7 @@ function bellmanFord(graph, sourceId, getWeight) {
857
861
  toId: edge.targetId,
858
862
  edge
859
863
  });
860
- if (isUndirected) directedEdges.push({
864
+ if (getEdgeMode(graph, edge) !== "directed") directedEdges.push({
861
865
  fromId: edge.targetId,
862
866
  toId: edge.sourceId,
863
867
  edge
@@ -1021,7 +1025,7 @@ function getCycles(graph) {
1021
1025
  return [...genCycles(graph)];
1022
1026
  }
1023
1027
  function* genCycles(graph) {
1024
- if (graph.type === "undirected") yield* genCyclesUndirected(graph);
1028
+ if (graph.mode !== "directed") yield* genCyclesUndirected(graph);
1025
1029
  else yield* genCyclesDirected(graph);
1026
1030
  }
1027
1031
  function* genCyclesDirected(graph) {
@@ -1160,7 +1164,7 @@ function floydWarshallAllPaths(graph, getWeight) {
1160
1164
  from: source,
1161
1165
  edge
1162
1166
  });
1163
- if (graph.type === "undirected") {
1167
+ if (getEdgeMode(graph, edge) !== "directed") {
1164
1168
  if (edgeWeight < dist[target][source]) {
1165
1169
  dist[target][source] = edgeWeight;
1166
1170
  prev[target][source] = [{
@@ -1318,7 +1322,7 @@ function* dfs(graph, startId) {
1318
1322
  }
1319
1323
  }
1320
1324
  function isAcyclic(graph) {
1321
- if (graph.type === "undirected") return isAcyclicUndirected(graph);
1325
+ if (graph.mode !== "directed") return isAcyclicUndirected(graph);
1322
1326
  const WHITE = 0;
1323
1327
  const GRAY = 1;
1324
1328
  const BLACK = 2;
@@ -1576,7 +1580,7 @@ function getMinimumSpanningTree(graph, opts) {
1576
1580
  const mstEdges = algorithm === "kruskal" ? kruskalMST(graph, getWeight) : primMST(graph, getWeight);
1577
1581
  return createGraph({
1578
1582
  id: graph.id,
1579
- type: graph.type,
1583
+ mode: graph.mode,
1580
1584
  initialNodeId: graph.initialNodeId ?? void 0,
1581
1585
  nodes: graph.nodes.map((node) => ({
1582
1586
  id: node.id,
@@ -1611,7 +1615,7 @@ function primMST(graph, getWeight) {
1611
1615
  edge
1612
1616
  });
1613
1617
  }
1614
- if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
1618
+ if (graph.mode !== "directed") for (const eid of idx.inEdges.get(nodeId) ?? []) {
1615
1619
  const ai = idx.edgeById.get(eid);
1616
1620
  if (ai === void 0) continue;
1617
1621
  const edge = graph.edges[ai];
@@ -1626,7 +1630,7 @@ function primMST(graph, getWeight) {
1626
1630
  addEdgesOf(startId);
1627
1631
  while (candidates.size > 0 && inMST.size < graph.nodes.length) {
1628
1632
  const { edge } = candidates.pop();
1629
- const targetId = graph.type === "undirected" && inMST.has(edge.targetId) ? edge.sourceId : edge.targetId;
1633
+ const targetId = graph.mode !== "directed" && inMST.has(edge.targetId) ? edge.sourceId : edge.targetId;
1630
1634
  if (inMST.has(targetId)) continue;
1631
1635
  inMST.add(targetId);
1632
1636
  mstEdges.push(edge);
@@ -1675,7 +1679,7 @@ function getNeighborIds(graph, nodeId) {
1675
1679
  const edgeIndex = idx.edgeById.get(edgeId);
1676
1680
  if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].targetId);
1677
1681
  }
1678
- if (graph.type === "undirected") for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
1682
+ if (graph.mode !== "directed") for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
1679
1683
  const edgeIndex = idx.edgeById.get(edgeId);
1680
1684
  if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].sourceId);
1681
1685
  }
@@ -1688,7 +1692,7 @@ function getIncomingIds(graph, nodeId) {
1688
1692
  const edgeIndex = idx.edgeById.get(edgeId);
1689
1693
  if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].sourceId);
1690
1694
  }
1691
- if (graph.type === "undirected") for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
1695
+ if (graph.mode !== "directed") for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
1692
1696
  const edgeIndex = idx.edgeById.get(edgeId);
1693
1697
  if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].targetId);
1694
1698
  }
@@ -1741,7 +1745,7 @@ function getDegreeCentrality(graph) {
1741
1745
  for (const node of graph.nodes) {
1742
1746
  const outDegree = idx.outEdges.get(node.id)?.length ?? 0;
1743
1747
  const inDegree = idx.inEdges.get(node.id)?.length ?? 0;
1744
- const degree = graph.type === "undirected" ? new Set([...idx.outEdges.get(node.id) ?? [], ...idx.inEdges.get(node.id) ?? []]).size : outDegree + inDegree;
1748
+ const degree = graph.mode !== "directed" ? new Set([...idx.outEdges.get(node.id) ?? [], ...idx.inEdges.get(node.id) ?? []]).size : outDegree + inDegree;
1745
1749
  scores[node.id] = degree * scale;
1746
1750
  }
1747
1751
  return scores;
@@ -1841,9 +1845,9 @@ function getBetweennessCentrality(graph) {
1841
1845
  }
1842
1846
  const order = graph.nodes.length;
1843
1847
  if (order <= 2) return scores;
1844
- const scale = graph.type === "undirected" ? 1 / ((order - 1) * (order - 2) / 2) : 1 / ((order - 1) * (order - 2));
1848
+ const scale = graph.mode !== "directed" ? 1 / ((order - 1) * (order - 2) / 2) : 1 / ((order - 1) * (order - 2));
1845
1849
  for (const nodeId of Object.keys(scores)) {
1846
- if (graph.type === "undirected") scores[nodeId] /= 2;
1850
+ if (graph.mode !== "directed") scores[nodeId] /= 2;
1847
1851
  scores[nodeId] *= scale;
1848
1852
  }
1849
1853
  return scores;
@@ -2314,11 +2318,11 @@ function getDegreeSignature(graph, nodeId) {
2314
2318
  const idx = getIndex(graph);
2315
2319
  const outDegree = idx.outEdges.get(nodeId)?.length ?? 0;
2316
2320
  const inDegree = idx.inEdges.get(nodeId)?.length ?? 0;
2317
- if (graph.type === "undirected") return `u:${new Set([...idx.outEdges.get(nodeId) ?? [], ...idx.inEdges.get(nodeId) ?? []]).size}`;
2321
+ if (graph.mode !== "directed") return `u:${new Set([...idx.outEdges.get(nodeId) ?? [], ...idx.inEdges.get(nodeId) ?? []]).size}`;
2318
2322
  return `d:${inDegree}:${outDegree}`;
2319
2323
  }
2320
2324
  function getEdgesBetween(graph, sourceId, targetId) {
2321
- if (graph.type === "undirected") return graph.edges.filter((edge) => edge.sourceId === sourceId && edge.targetId === targetId || edge.sourceId === targetId && edge.targetId === sourceId);
2325
+ if (graph.mode !== "directed") return graph.edges.filter((edge) => edge.sourceId === sourceId && edge.targetId === targetId || edge.sourceId === targetId && edge.targetId === sourceId);
2322
2326
  return graph.edges.filter((edge) => edge.sourceId === sourceId && edge.targetId === targetId);
2323
2327
  }
2324
2328
  function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
@@ -2339,7 +2343,7 @@ function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
2339
2343
  * node and edge payloads.
2340
2344
  */
2341
2345
  function isIsomorphic(graphA, graphB, options) {
2342
- if (graphA.type !== graphB.type) return false;
2346
+ if (graphA.mode !== graphB.mode) return false;
2343
2347
  if (graphA.nodes.length !== graphB.nodes.length) return false;
2344
2348
  if (graphA.edges.length !== graphB.edges.length) return false;
2345
2349
  const nodeMatch = options?.nodeMatch;
@@ -1,4 +1,4 @@
1
- import { D as SinglePathOptions, g as GraphNode, k as TraversalOptions, n as AllPairsShortestPathsOptions, p as GraphEdge, t as AStarOptions, u as Graph, v as GraphPath, w as PathOptions, x as MSTOptions } from "./types-CnZ01raw.mjs";
1
+ import { A as TraversalOptions, O as SinglePathOptions, S as MSTOptions, T as PathOptions, _ as GraphNode, n as AllPairsShortestPathsOptions, p as GraphEdge, t as AStarOptions, u as Graph, y as GraphPath } from "./types-3-FS9NV2.mjs";
2
2
 
3
3
  //#region src/algorithms/traversal.d.ts
4
4
  declare function bfs<N>(graph: Graph<N>, startId: string): Generator<GraphNode<N>>;
@@ -1,2 +1,2 @@
1
- import { A as genCycles, B as getStronglyConnectedComponents, C as getMinimumSpanningTree, D as getPostorders, E as getPostorder, F as getCycles, G as getTopologicalSort, H as bfs, I as getShortestPath, J as isConnected, K as hasPath, L as getShortestPaths, M as genSimplePaths, N as getAStarPath, O as getPreorder, P as getAllPairsShortestPaths, R as getSimplePath, S as getPageRank, T as genPreorders, U as dfs, V as joinPaths, W as getConnectedComponents, Y as isTree, _ as getDegreeCentrality, a as getBridges, b as getInDegreeCentrality, c as genGirvanNewmanCommunities, d as getLabelPropagationCommunities, f as getModularity, g as getClosenessCentrality, h as getBetweennessCentrality, i as getBiconnectedComponents, j as genShortestPaths, k as getPreorders, l as getGirvanNewmanCommunities, m as IterativeCentralityOptions, n as isIsomorphic, o as GirvanNewmanOptions, p as HITSResult, q as isAcyclic, r as getArticulationPoints, s as LabelPropagationOptions, t as IsomorphismOptions, u as getGreedyModularityCommunities, v as getEigenvectorCentrality, w as genPostorders, x as getOutDegreeCentrality, y as getHITS, z as getSimplePaths } from "./algorithms-BlM-qoJb.mjs";
1
+ import { A as genCycles, B as getStronglyConnectedComponents, C as getMinimumSpanningTree, D as getPostorders, E as getPostorder, F as getCycles, G as getTopologicalSort, H as bfs, I as getShortestPath, J as isConnected, K as hasPath, L as getShortestPaths, M as genSimplePaths, N as getAStarPath, O as getPreorder, P as getAllPairsShortestPaths, R as getSimplePath, S as getPageRank, T as genPreorders, U as dfs, V as joinPaths, W as getConnectedComponents, Y as isTree, _ as getDegreeCentrality, a as getBridges, b as getInDegreeCentrality, c as genGirvanNewmanCommunities, d as getLabelPropagationCommunities, f as getModularity, g as getClosenessCentrality, h as getBetweennessCentrality, i as getBiconnectedComponents, j as genShortestPaths, k as getPreorders, l as getGirvanNewmanCommunities, m as IterativeCentralityOptions, n as isIsomorphic, o as GirvanNewmanOptions, p as HITSResult, q as isAcyclic, r as getArticulationPoints, s as LabelPropagationOptions, t as IsomorphismOptions, u as getGreedyModularityCommunities, v as getEigenvectorCentrality, w as genPostorders, x as getOutDegreeCentrality, y as getHITS, z as getSimplePaths } from "./algorithms-fTqmvhzP.mjs";
2
2
  export { GirvanNewmanOptions, HITSResult, IsomorphismOptions, IterativeCentralityOptions, LabelPropagationOptions, bfs, dfs, genCycles, genGirvanNewmanCommunities, genPostorders, genPreorders, genShortestPaths, genSimplePaths, getAStarPath, getAllPairsShortestPaths, getArticulationPoints, getBetweennessCentrality, getBiconnectedComponents, getBridges, getClosenessCentrality, getConnectedComponents, getCycles, getDegreeCentrality, getEigenvectorCentrality, getGirvanNewmanCommunities, getGreedyModularityCommunities, getHITS, getInDegreeCentrality, getLabelPropagationCommunities, getMinimumSpanningTree, getModularity, getOutDegreeCentrality, getPageRank, getPostorder, getPostorders, getPreorder, getPreorders, getShortestPath, getShortestPaths, getSimplePath, getSimplePaths, getStronglyConnectedComponents, getTopologicalSort, hasPath, isAcyclic, isConnected, isIsomorphic, isTree, joinPaths };
@@ -1,3 +1,3 @@
1
- import { A as isAcyclic, B as getShortestPaths, C as getPreorder, D as getConnectedComponents, E as dfs, F as genSimplePaths, H as getSimplePaths, I as getAStarPath, L as getAllPairsShortestPaths, M as isTree, N as genCycles, O as getTopologicalSort, P as genShortestPaths, R as getCycles, S as getPostorders, T as bfs, U as getStronglyConnectedComponents, V as getSimplePath, W as joinPaths, _ as getPageRank, a as genGirvanNewmanCommunities, b as genPreorders, c as getLabelPropagationCommunities, d as getClosenessCentrality, f as getDegreeCentrality, g as getOutDegreeCentrality, h as getInDegreeCentrality, i as getBridges, j as isConnected, k as hasPath, l as getModularity, m as getHITS, n as getArticulationPoints, o as getGirvanNewmanCommunities, p as getEigenvectorCentrality, r as getBiconnectedComponents, s as getGreedyModularityCommunities, t as isIsomorphic, u as getBetweennessCentrality, v as getMinimumSpanningTree, w as getPreorders, x as getPostorder, y as genPostorders, z as getShortestPath } from "./algorithms-BHHg7lGq.mjs";
1
+ import { A as isAcyclic, B as getShortestPaths, C as getPreorder, D as getConnectedComponents, E as dfs, F as genSimplePaths, H as getSimplePaths, I as getAStarPath, L as getAllPairsShortestPaths, M as isTree, N as genCycles, O as getTopologicalSort, P as genShortestPaths, R as getCycles, S as getPostorders, T as bfs, U as getStronglyConnectedComponents, V as getSimplePath, W as joinPaths, _ as getPageRank, a as genGirvanNewmanCommunities, b as genPreorders, c as getLabelPropagationCommunities, d as getClosenessCentrality, f as getDegreeCentrality, g as getOutDegreeCentrality, h as getInDegreeCentrality, i as getBridges, j as isConnected, k as hasPath, l as getModularity, m as getHITS, n as getArticulationPoints, o as getGirvanNewmanCommunities, p as getEigenvectorCentrality, r as getBiconnectedComponents, s as getGreedyModularityCommunities, t as isIsomorphic, u as getBetweennessCentrality, v as getMinimumSpanningTree, w as getPreorders, x as getPostorder, y as genPostorders, z as getShortestPath } from "./algorithms-Ba7o7niK.mjs";
2
2
 
3
3
  export { bfs, dfs, genCycles, genGirvanNewmanCommunities, genPostorders, genPreorders, genShortestPaths, genSimplePaths, getAStarPath, getAllPairsShortestPaths, getArticulationPoints, getBetweennessCentrality, getBiconnectedComponents, getBridges, getClosenessCentrality, getConnectedComponents, getCycles, getDegreeCentrality, getEigenvectorCentrality, getGirvanNewmanCommunities, getGreedyModularityCommunities, getHITS, getInDegreeCentrality, getLabelPropagationCommunities, getMinimumSpanningTree, getModularity, getOutDegreeCentrality, getPageRank, getPostorder, getPostorders, getPreorder, getPreorders, getShortestPath, getShortestPaths, getSimplePath, getSimplePaths, getStronglyConnectedComponents, getTopologicalSort, hasPath, isAcyclic, isConnected, isIsomorphic, isTree, joinPaths };
@@ -1,5 +1,5 @@
1
- import { n as toAdjacencyList, t as fromAdjacencyList } from "./adjacency-list-Ca0VjKIf.mjs";
2
- import { n as toEdgeList, t as fromEdgeList } from "./edge-list-gKe8-iRa.mjs";
1
+ import { n as toAdjacencyList, t as fromAdjacencyList } from "./adjacency-list-VsUaH9SJ.mjs";
2
+ import { n as toEdgeList, t as fromEdgeList } from "./edge-list-DP4otyPU.mjs";
3
3
 
4
4
  //#region src/formats/converter/index.ts
5
5
  /**
@@ -59,7 +59,7 @@ function fromEdgeList(edges, options) {
59
59
  }));
60
60
  return {
61
61
  id: options?.id ?? "",
62
- type: directed ? "directed" : "undirected",
62
+ mode: directed ? "directed" : "undirected",
63
63
  initialNodeId: null,
64
64
  nodes,
65
65
  edges: edgeObjects,
@@ -16,6 +16,12 @@ interface FormatSupportEntry {
16
16
  features: FormatSupportFeatures;
17
17
  notes: string[];
18
18
  }
19
+ /**
20
+ * Round-trip support is allowed to use adapter-specific graph, node, and edge
21
+ * `data` metadata when the target format has no native field for a source
22
+ * concept. A `partial` value means the adapter still drops meaningful source
23
+ * information instead of preserving it as metadata.
24
+ */
19
25
  declare const FORMAT_SUPPORT_MATRIX: FormatSupportEntry[];
20
26
  declare function getFormatSupportEntry(id: string): FormatSupportEntry | undefined;
21
27
  //#endregion
@@ -1,4 +1,10 @@
1
1
  //#region src/formats/support.ts
2
+ /**
3
+ * Round-trip support is allowed to use adapter-specific graph, node, and edge
4
+ * `data` metadata when the target format has no native field for a source
5
+ * concept. A `partial` value means the adapter still drops meaningful source
6
+ * information instead of preserving it as metadata.
7
+ */
2
8
  const FORMAT_SUPPORT_MATRIX = [
3
9
  {
4
10
  id: "adjacency-list",
@@ -23,12 +29,12 @@ const FORMAT_SUPPORT_MATRIX = [
23
29
  undirected: "full",
24
30
  hierarchy: "full",
25
31
  ports: "full",
26
- visual: "partial",
27
- style: "partial",
32
+ visual: "full",
33
+ style: "full",
28
34
  weight: "full",
29
- roundTrip: "partial"
35
+ roundTrip: "full"
30
36
  },
31
- notes: ["Uses Cytoscape JSON element data with partial layout/style fidelity.", "Ports round-trip through element data as `ports`, `sourcePort`, and `targetPort`."]
37
+ notes: ["Uses Cytoscape JSON element data with graph, node, and edge metadata stored in element data.", "Ports round-trip through element data as `ports`, `sourcePort`, and `targetPort`."]
32
38
  },
33
39
  {
34
40
  id: "d3",
@@ -36,14 +42,35 @@ const FORMAT_SUPPORT_MATRIX = [
36
42
  features: {
37
43
  directed: "full",
38
44
  undirected: "full",
39
- hierarchy: "none",
45
+ hierarchy: "full",
40
46
  ports: "full",
41
- visual: "partial",
42
- style: "none",
47
+ visual: "full",
48
+ style: "full",
43
49
  weight: "full",
44
- roundTrip: "partial"
50
+ roundTrip: "full"
51
+ },
52
+ notes: ["Targets force-graph structures, but graph, node, and edge metadata can be preserved on the loose JSON shape.", "Ports round-trip through node/link objects."]
53
+ },
54
+ {
55
+ id: "d2",
56
+ importPath: "@statelyai/graph/d2",
57
+ features: {
58
+ directed: "full",
59
+ undirected: "full",
60
+ hierarchy: "full",
61
+ ports: "full",
62
+ visual: "full",
63
+ style: "full",
64
+ weight: "none",
65
+ roundTrip: "full"
45
66
  },
46
- notes: ["Targets force-graph structures, not compound graph metadata.", "Ports round-trip through node/link objects."]
67
+ notes: [
68
+ "Containers map to hierarchy; dot vs block declaration form is preserved per node.",
69
+ "sql_table/class fields map to ports; node.field connections round-trip as sourcePort/targetPort.",
70
+ "Per-edge connectors (->, <-, --, <->) map to edge.mode; the authored glyph is preserved in _d2.arrow.",
71
+ "vars/classes/imports are preserved on graph data; comments attach to the following entity (best-effort).",
72
+ "d2 has no native edge weight."
73
+ ]
47
74
  },
48
75
  {
49
76
  id: "dot",
@@ -84,11 +111,11 @@ const FORMAT_SUPPORT_MATRIX = [
84
111
  hierarchy: "full",
85
112
  ports: "full",
86
113
  visual: "full",
87
- style: "partial",
88
- weight: "none",
89
- roundTrip: "partial"
114
+ style: "full",
115
+ weight: "full",
116
+ roundTrip: "full"
90
117
  },
91
- notes: ["Optimized for layout exchange with ELK rather than exact styling parity."]
118
+ notes: ["ELK-native layout fields are preserved directly; graph, node, port, and edge metadata round-trip through reserved layout options."]
92
119
  },
93
120
  {
94
121
  id: "gexf",
@@ -98,12 +125,16 @@ const FORMAT_SUPPORT_MATRIX = [
98
125
  undirected: "full",
99
126
  hierarchy: "full",
100
127
  ports: "full",
101
- visual: "partial",
102
- style: "partial",
128
+ visual: "full",
129
+ style: "full",
103
130
  weight: "full",
104
- roundTrip: "partial"
131
+ roundTrip: "full"
105
132
  },
106
- notes: ["Attribute and viz extensions round-trip partially.", "Ports round-trip via custom node/edge attributes."]
133
+ notes: [
134
+ "Custom attributes preserve graph, node, and edge metadata beyond the standard viz module.",
135
+ "Ports round-trip via custom node/edge attributes.",
136
+ "Per-edge directedness round-trips via the `type` edge attribute; bidirectional maps to directed."
137
+ ]
107
138
  },
108
139
  {
109
140
  id: "gml",
@@ -113,12 +144,12 @@ const FORMAT_SUPPORT_MATRIX = [
113
144
  undirected: "full",
114
145
  hierarchy: "full",
115
146
  ports: "full",
116
- visual: "partial",
117
- style: "partial",
147
+ visual: "full",
148
+ style: "full",
118
149
  weight: "full",
119
- roundTrip: "partial"
150
+ roundTrip: "full"
120
151
  },
121
- notes: ["GML support focuses on common graph/node/edge attributes.", "Ports round-trip through JSON-stringified node metadata and edge fields."]
152
+ notes: ["GML stores graph, node, and edge metadata directly or as JSON-stringified fields.", "Ports round-trip through JSON-stringified node metadata and edge fields."]
122
153
  },
123
154
  {
124
155
  id: "graphml",
@@ -133,7 +164,11 @@ const FORMAT_SUPPORT_MATRIX = [
133
164
  weight: "full",
134
165
  roundTrip: "partial"
135
166
  },
136
- notes: ["GraphML attribute fidelity is good, but not every extension is represented.", "Ports round-trip through node and edge `<data>` fields."]
167
+ notes: [
168
+ "GraphML attribute fidelity is good, but not every extension is represented.",
169
+ "Ports round-trip through node and edge `<data>` fields.",
170
+ "Per-edge directedness round-trips via the `directed` edge attribute; bidirectional maps to directed."
171
+ ]
137
172
  },
138
173
  {
139
174
  id: "jgf",
@@ -143,12 +178,12 @@ const FORMAT_SUPPORT_MATRIX = [
143
178
  undirected: "full",
144
179
  hierarchy: "full",
145
180
  ports: "full",
146
- visual: "none",
147
- style: "none",
181
+ visual: "full",
182
+ style: "full",
148
183
  weight: "full",
149
- roundTrip: "partial"
184
+ roundTrip: "full"
150
185
  },
151
- notes: ["JGF preserves core graph structure and data, but not layout primitives.", "Ports round-trip through node and edge metadata."]
186
+ notes: ["JGF preserves graph, node, and edge metadata via `metadata` objects.", "Ports round-trip through node and edge metadata."]
152
187
  },
153
188
  {
154
189
  id: "tgf",
@@ -171,14 +206,14 @@ const FORMAT_SUPPORT_MATRIX = [
171
206
  features: {
172
207
  directed: "full",
173
208
  undirected: "partial",
174
- hierarchy: "none",
209
+ hierarchy: "full",
175
210
  ports: "full",
176
211
  visual: "full",
177
- style: "partial",
178
- weight: "none",
179
- roundTrip: "partial"
212
+ style: "full",
213
+ weight: "full",
214
+ roundTrip: "full"
180
215
  },
181
- notes: ["Ports map cleanly to handles, but styling remains adapter-specific."]
216
+ notes: ["xyflow-native fields are preserved directly; graph, node, edge, style, weight, and port metadata round-trip through reserved data fields."]
182
217
  },
183
218
  {
184
219
  id: "mermaid/block",
@@ -296,9 +331,9 @@ const FORMAT_SUPPORT_MATRIX = [
296
331
  visual: "partial",
297
332
  style: "partial",
298
333
  weight: "none",
299
- roundTrip: "partial"
334
+ roundTrip: "full"
300
335
  },
301
- notes: ["State notes are stored, but not fully round-trippable as separate graph entities."]
336
+ notes: ["State-specific syntax such as notes, classes, descriptions, directions, hierarchy, and parallel regions round-trips through node and graph data."]
302
337
  }
303
338
  ];
304
339
  function getFormatSupportEntry(id) {
@@ -1,4 +1,4 @@
1
- import { u as Graph } from "../../types-CnZ01raw.mjs";
1
+ import { u as Graph } from "../../types-3-FS9NV2.mjs";
2
2
 
3
3
  //#region src/formats/adjacency-list/index.d.ts
4
4