@statelyai/graph 0.13.0 → 2.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 (57) hide show
  1. package/README.md +57 -26
  2. package/dist/{adjacency-list-Ca0VjKIf.mjs → adjacency-list-GeL1Cu-L.mjs} +5 -3
  3. package/dist/{algorithms-BlM-qoJb.d.mts → algorithms-CsGNehct.d.mts} +137 -2
  4. package/dist/{algorithms-BNDQcHU3.mjs → algorithms-DF1pSQGv.mjs} +1494 -357
  5. package/dist/algorithms.d.mts +2 -2
  6. package/dist/algorithms.mjs +2 -2
  7. package/dist/{converter-Dspillnn.mjs → converter-DyCJJfTe.mjs} +2 -2
  8. package/dist/{edge-list-gKe8-iRa.mjs → edge-list-BcZ0h6zz.mjs} +1 -1
  9. package/dist/format-support.mjs +67 -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 -60
  13. package/dist/formats/converter/index.mjs +1 -1
  14. package/dist/formats/cytoscape/index.d.mts +1 -1
  15. package/dist/formats/cytoscape/index.mjs +5 -3
  16. package/dist/formats/d2/index.d.mts +109 -0
  17. package/dist/formats/d2/index.mjs +1100 -0
  18. package/dist/formats/d3/index.d.mts +2 -2
  19. package/dist/formats/d3/index.mjs +5 -3
  20. package/dist/formats/dot/index.d.mts +1 -1
  21. package/dist/formats/dot/index.mjs +24 -8
  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 +23 -16
  26. package/dist/formats/gexf/index.d.mts +1 -1
  27. package/dist/formats/gexf/index.mjs +30 -17
  28. package/dist/formats/gml/index.d.mts +1 -1
  29. package/dist/formats/gml/index.mjs +22 -13
  30. package/dist/formats/graphml/index.d.mts +1 -1
  31. package/dist/formats/graphml/index.mjs +83 -25
  32. package/dist/formats/jgf/index.d.mts +1 -1
  33. package/dist/formats/jgf/index.mjs +6 -3
  34. package/dist/formats/mermaid/index.d.mts +1 -1
  35. package/dist/formats/mermaid/index.mjs +57 -20
  36. package/dist/formats/tgf/index.d.mts +1 -1
  37. package/dist/formats/tgf/index.mjs +2 -2
  38. package/dist/formats/xyflow/index.d.mts +1 -1
  39. package/dist/formats/xyflow/index.mjs +33 -6
  40. package/dist/index-D51lJnt2.d.mts +61 -0
  41. package/dist/index-DWmo1mIp.d.mts +697 -0
  42. package/dist/index.d.mts +6 -631
  43. package/dist/index.mjs +144 -295
  44. package/dist/mode-D8OnHFBk.mjs +15 -0
  45. package/dist/queries-BfXeTXRf.d.mts +547 -0
  46. package/dist/queries-KirMDR7e.mjs +980 -0
  47. package/dist/queries.d.mts +1 -514
  48. package/dist/queries.mjs +1 -766
  49. package/dist/schemas.d.mts +21 -10
  50. package/dist/schemas.mjs +35 -86
  51. package/dist/{types-CnZ01raw.d.mts → types-DNYdIU21.d.mts} +83 -11
  52. package/dist/validate-TtH-x3JV.mjs +190 -0
  53. package/package.json +14 -3
  54. package/schemas/edge.schema.json +11 -0
  55. package/schemas/graph.schema.json +24 -3
  56. package/schemas/node.schema.json +6 -0
  57. package/dist/indexing-DUl3kTqm.mjs +0 -137
@@ -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-DUl3kTqm.mjs";
1
+ import { A as indexAddNode, M as indexUpdateEdgeEndpoints, N as invalidateIndex, O as getIndex, P as touchIndex, j as indexReparentNode, k as indexAddEdge, l as getInDegree, p as getOutDegree, r as getDegree } from "./queries-KirMDR7e.mjs";
2
+ import { t as getEdgeMode } from "./mode-D8OnHFBk.mjs";
2
3
 
3
4
  //#region src/graph.ts
4
5
  /**
@@ -88,6 +89,7 @@ function createGraphEdge(config) {
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,7 +113,7 @@ 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),
@@ -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
@@ -404,15 +406,46 @@ function deleteEdge(graph, id) {
404
406
  graph.edges = graph.edges.filter((e) => e.id !== id);
405
407
  invalidateIndex(graph);
406
408
  }
409
+ /** Optional fields where `null` in an update unsets the field. */
410
+ const NODE_OPTIONAL_KEYS = [
411
+ "x",
412
+ "y",
413
+ "width",
414
+ "height",
415
+ "shape",
416
+ "color",
417
+ "style"
418
+ ];
419
+ const EDGE_OPTIONAL_KEYS = [
420
+ "weight",
421
+ "mode",
422
+ "x",
423
+ "y",
424
+ "width",
425
+ "height",
426
+ "color",
427
+ "style"
428
+ ];
429
+ /** Apply optional-field updates: `null` unsets, a value sets, `undefined` is ignored. */
430
+ function applyOptionalUpdates(target, update, keys) {
431
+ for (const key of keys) {
432
+ const value = update[key];
433
+ if (value === void 0) continue;
434
+ if (value === null) delete target[key];
435
+ else target[key] = value;
436
+ }
437
+ }
407
438
  /**
408
439
  * **Mutable.** Update a node in place.
440
+ * Optional fields (`x`, `y`, `width`, `height`, `shape`, `color`, `style`,
441
+ * `ports`) accept `null` to unset; `undefined` leaves them unchanged.
409
442
  * @returns The updated node.
410
443
  *
411
444
  * @example
412
445
  * ```ts
413
446
  * const graph = createGraph({ nodes: [{ id: 'a', label: 'old' }] });
414
- * const updated = updateNode(graph, 'a', { label: 'new' });
415
- * // updated.label === 'new'
447
+ * const updated = updateNode(graph, 'a', { label: 'new', x: 100 });
448
+ * // updated.label === 'new', updated.x === 100
416
449
  * ```
417
450
  */
418
451
  function updateNode(graph, id, update) {
@@ -421,24 +454,48 @@ function updateNode(graph, id, update) {
421
454
  if (arrayIdx === void 0) throw new Error(`Node "${id}" does not exist`);
422
455
  if (update.parentId !== void 0 && update.parentId !== null) {
423
456
  if (!idx.nodeById.has(update.parentId)) throw new Error(`Parent node "${update.parentId}" does not exist`);
457
+ let ancestorId = update.parentId;
458
+ const seen = /* @__PURE__ */ new Set();
459
+ while (ancestorId !== null && !seen.has(ancestorId)) {
460
+ if (ancestorId === id) throw new Error(`Cannot set parentId of node "${id}" to "${update.parentId}": "${update.parentId}" is "${id}" or one of its descendants, which would create a hierarchy cycle. Reparent "${update.parentId}" elsewhere first.`);
461
+ seen.add(ancestorId);
462
+ const ai = idx.nodeById.get(ancestorId);
463
+ ancestorId = ai !== void 0 ? graph.nodes[ai].parentId ?? null : null;
464
+ }
424
465
  }
425
- if (update.ports !== void 0 && update.ports.length > 0) validatePortNames(update.ports);
466
+ if (update.ports != null && update.ports.length > 0) validatePortNames(update.ports);
426
467
  const node = graph.nodes[arrayIdx];
468
+ if (update.ports !== void 0) {
469
+ const newPortNames = new Set((update.ports ?? []).map((p) => p.name));
470
+ for (const eid of idx.outEdges.get(id) ?? []) {
471
+ const e = graph.edges[idx.edgeById.get(eid)];
472
+ if (e.sourcePort !== void 0 && !newPortNames.has(e.sourcePort)) throw new Error(`Cannot update ports of node "${id}": edge "${e.id}" references port "${e.sourcePort}" via sourcePort. Keep that port, or update/delete the edge first.`);
473
+ }
474
+ for (const eid of idx.inEdges.get(id) ?? []) {
475
+ const e = graph.edges[idx.edgeById.get(eid)];
476
+ if (e.targetPort !== void 0 && !newPortNames.has(e.targetPort)) throw new Error(`Cannot update ports of node "${id}": edge "${e.id}" references port "${e.targetPort}" via targetPort. Keep that port, or update/delete the edge first.`);
477
+ }
478
+ }
427
479
  const oldParentId = node.parentId;
428
480
  const updated = {
429
481
  ...node,
430
482
  ...update.parentId !== void 0 && { parentId: update.parentId ?? null },
431
483
  ...update.initialNodeId !== void 0 && { initialNodeId: update.initialNodeId ?? null },
432
484
  ...update.label !== void 0 && { label: update.label },
433
- ...update.data !== void 0 && { data: update.data },
434
- ...update.ports !== void 0 && { ports: update.ports.map(createGraphPort) }
485
+ ...update.data !== void 0 && { data: update.data }
435
486
  };
487
+ if (update.ports !== void 0) if (update.ports === null) delete updated.ports;
488
+ else updated.ports = update.ports.map(createGraphPort);
489
+ applyOptionalUpdates(updated, update, NODE_OPTIONAL_KEYS);
436
490
  graph.nodes[arrayIdx] = updated;
437
491
  if (update.parentId !== void 0 && updated.parentId !== oldParentId) indexReparentNode(idx, id, oldParentId, updated.parentId);
438
492
  return updated;
439
493
  }
440
494
  /**
441
495
  * **Mutable.** Update an edge in place.
496
+ * Optional fields (`weight`, `mode`, `sourcePort`, `targetPort`, `x`, `y`,
497
+ * `width`, `height`, `color`, `style`) accept `null` to unset; `undefined`
498
+ * leaves them unchanged.
442
499
  * @returns The updated edge.
443
500
  *
444
501
  * @example
@@ -447,8 +504,8 @@ function updateNode(graph, id, update) {
447
504
  * nodes: [{ id: 'a' }, { id: 'b' }],
448
505
  * edges: [{ id: 'e1', sourceId: 'a', targetId: 'b', label: 'old' }],
449
506
  * });
450
- * const updated = updateEdge(graph, 'e1', { label: 'new' });
451
- * // updated.label === 'new'
507
+ * const updated = updateEdge(graph, 'e1', { label: 'new', weight: 2 });
508
+ * // updated.label === 'new', updated.weight === 2
452
509
  * ```
453
510
  */
454
511
  function updateEdge(graph, id, update) {
@@ -462,22 +519,28 @@ function updateEdge(graph, id, update) {
462
519
  const oldTargetId = edge.targetId;
463
520
  const effectiveSourceId = update.sourceId ?? edge.sourceId;
464
521
  const effectiveTargetId = update.targetId ?? edge.targetId;
465
- if (update.sourcePort !== void 0) {
466
- if (!graph.nodes[idx.nodeById.get(effectiveSourceId)].ports?.some((p) => p.name === update.sourcePort)) throw new Error(`Port "${update.sourcePort}" does not exist on source node "${effectiveSourceId}"`);
522
+ const effectiveSourcePort = update.sourcePort !== void 0 ? update.sourcePort ?? void 0 : edge.sourcePort;
523
+ const effectiveTargetPort = update.targetPort !== void 0 ? update.targetPort ?? void 0 : edge.targetPort;
524
+ if (effectiveSourcePort !== void 0) {
525
+ if (!graph.nodes[idx.nodeById.get(effectiveSourceId)].ports?.some((p) => p.name === effectiveSourcePort)) throw new Error(update.sourcePort !== void 0 ? `Port "${effectiveSourcePort}" does not exist on source node "${effectiveSourceId}"` : `Cannot update edge "${id}": its sourcePort "${effectiveSourcePort}" does not exist on the new source node "${effectiveSourceId}". Include sourcePort in the update (a port on "${effectiveSourceId}", or null to clear it).`);
467
526
  }
468
- if (update.targetPort !== void 0) {
469
- if (!graph.nodes[idx.nodeById.get(effectiveTargetId)].ports?.some((p) => p.name === update.targetPort)) throw new Error(`Port "${update.targetPort}" does not exist on target node "${effectiveTargetId}"`);
527
+ if (effectiveTargetPort !== void 0) {
528
+ if (!graph.nodes[idx.nodeById.get(effectiveTargetId)].ports?.some((p) => p.name === effectiveTargetPort)) throw new Error(update.targetPort !== void 0 ? `Port "${effectiveTargetPort}" does not exist on target node "${effectiveTargetId}"` : `Cannot update edge "${id}": its targetPort "${effectiveTargetPort}" does not exist on the new target node "${effectiveTargetId}". Include targetPort in the update (a port on "${effectiveTargetId}", or null to clear it).`);
470
529
  }
471
530
  const updated = {
472
531
  ...edge,
473
532
  ...update.sourceId !== void 0 && { sourceId: update.sourceId },
474
533
  ...update.targetId !== void 0 && { targetId: update.targetId },
475
534
  ...update.label !== void 0 && { label: update.label },
476
- ...update.data !== void 0 && { data: update.data },
477
- ...update.sourcePort !== void 0 && { sourcePort: update.sourcePort },
478
- ...update.targetPort !== void 0 && { targetPort: update.targetPort }
535
+ ...update.data !== void 0 && { data: update.data }
479
536
  };
537
+ if (update.sourcePort !== void 0) if (update.sourcePort === null) delete updated.sourcePort;
538
+ else updated.sourcePort = update.sourcePort;
539
+ if (update.targetPort !== void 0) if (update.targetPort === null) delete updated.targetPort;
540
+ else updated.targetPort = update.targetPort;
541
+ applyOptionalUpdates(updated, update, EDGE_OPTIONAL_KEYS);
480
542
  graph.edges[arrayIdx] = updated;
543
+ if (update.mode !== void 0 || update.weight !== void 0) touchIndex(idx);
481
544
  if (updated.sourceId !== oldSourceId || updated.targetId !== oldTargetId) indexUpdateEdgeEndpoints(idx, id, oldSourceId, oldTargetId, updated.sourceId, updated.targetId);
482
545
  return updated;
483
546
  }
@@ -582,8 +645,9 @@ var GraphInstance = class GraphInstance {
582
645
  get id() {
583
646
  return this.graph.id;
584
647
  }
585
- get type() {
586
- return this.graph.type;
648
+ /** Default directedness for all edges. */
649
+ get mode() {
650
+ return this.graph.mode;
587
651
  }
588
652
  get nodes() {
589
653
  return this.graph.nodes;
@@ -663,6 +727,9 @@ var MinPriorityQueue = class {
663
727
  this.items.push(item);
664
728
  this.bubbleUp(this.items.length - 1);
665
729
  }
730
+ peek() {
731
+ return this.items[0];
732
+ }
666
733
  pop() {
667
734
  if (this.items.length === 0) return void 0;
668
735
  const first = this.items[0];
@@ -696,16 +763,35 @@ var MinPriorityQueue = class {
696
763
  }
697
764
  }
698
765
  };
699
- function getNeighborIds$1(graph, nodeId) {
766
+ /**
767
+ * Classify a graph by the *effective* mode of its edges (per-edge `mode`
768
+ * overrides included): all-directed, all-non-directed, or genuinely mixed.
769
+ * Edge-less graphs fall back to `graph.mode`.
770
+ */
771
+ function getEffectiveModeKind(graph) {
772
+ let sawDirected = false;
773
+ let sawNonDirected = false;
774
+ for (const edge of graph.edges) {
775
+ if (getEdgeMode(graph, edge) === "directed") sawDirected = true;
776
+ else sawNonDirected = true;
777
+ if (sawDirected && sawNonDirected) return "mixed";
778
+ }
779
+ if (sawDirected) return "directed";
780
+ if (sawNonDirected) return "non-directed";
781
+ return graph.mode === "directed" ? "directed" : "non-directed";
782
+ }
783
+ function getNeighborIds(graph, nodeId) {
700
784
  const idx = getIndex(graph);
701
785
  const ids = [];
702
786
  for (const eid of idx.outEdges.get(nodeId) ?? []) {
703
787
  const ai = idx.edgeById.get(eid);
704
788
  if (ai !== void 0) ids.push(graph.edges[ai].targetId);
705
789
  }
706
- if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
790
+ for (const eid of idx.inEdges.get(nodeId) ?? []) {
707
791
  const ai = idx.edgeById.get(eid);
708
- if (ai !== void 0) ids.push(graph.edges[ai].sourceId);
792
+ if (ai === void 0) continue;
793
+ const edge = graph.edges[ai];
794
+ if (getEdgeMode(graph, edge) !== "directed") ids.push(edge.sourceId);
709
795
  }
710
796
  return ids;
711
797
  }
@@ -736,11 +822,11 @@ function getNeighborEdges(graph, nodeId) {
736
822
  });
737
823
  }
738
824
  }
739
- if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
825
+ for (const eid of idx.inEdges.get(nodeId) ?? []) {
740
826
  const ai = idx.edgeById.get(eid);
741
827
  if (ai !== void 0) {
742
828
  const edge = graph.edges[ai];
743
- result.push({
829
+ if (getEdgeMode(graph, edge) !== "directed") result.push({
744
830
  neighborId: edge.sourceId,
745
831
  edge
746
832
  });
@@ -774,67 +860,181 @@ function getNeighborEdgesAll(graph, nodeId) {
774
860
  return result;
775
861
  }
776
862
 
863
+ //#endregion
864
+ //#region src/algorithms/csr.ts
865
+ const csrCache = /* @__PURE__ */ new WeakMap();
866
+ /** Get or lazily (re)build the CSR snapshot for a graph. */
867
+ function getCSR(graph) {
868
+ const idx = getIndex(graph);
869
+ const cached = csrCache.get(idx);
870
+ if (cached && cached.version === idx.version && cached.mode === graph.mode) return cached.csr;
871
+ const csr = buildCSR(graph);
872
+ csrCache.set(idx, {
873
+ version: idx.version,
874
+ mode: graph.mode,
875
+ csr
876
+ });
877
+ return csr;
878
+ }
879
+ function buildCSR(graph) {
880
+ const n = graph.nodes.length;
881
+ const m = graph.edges.length;
882
+ const ids = new Array(n);
883
+ const indexOf = /* @__PURE__ */ new Map();
884
+ for (let i = 0; i < n; i++) {
885
+ ids[i] = graph.nodes[i].id;
886
+ indexOf.set(ids[i], i);
887
+ }
888
+ const srcPos = new Int32Array(m);
889
+ const tgtPos = new Int32Array(m);
890
+ const nonDirected = new Uint8Array(m);
891
+ const outCounts = new Int32Array(n);
892
+ const inCounts = new Int32Array(n);
893
+ let firstNegativeEdge = -1;
894
+ for (let e = 0; e < m; e++) {
895
+ const edge = graph.edges[e];
896
+ if (firstNegativeEdge === -1 && (edge.weight ?? 1) < 0) firstNegativeEdge = e;
897
+ const s = indexOf.get(edge.sourceId);
898
+ const t = indexOf.get(edge.targetId);
899
+ if (s === void 0 || t === void 0) {
900
+ srcPos[e] = -1;
901
+ tgtPos[e] = -1;
902
+ continue;
903
+ }
904
+ srcPos[e] = s;
905
+ tgtPos[e] = t;
906
+ const nd = getEdgeMode(graph, edge) !== "directed" ? 1 : 0;
907
+ nonDirected[e] = nd;
908
+ outCounts[s]++;
909
+ inCounts[t]++;
910
+ if (nd) {
911
+ outCounts[t]++;
912
+ inCounts[s]++;
913
+ }
914
+ }
915
+ const outOffsets = new Int32Array(n + 1);
916
+ const inOffsets = new Int32Array(n + 1);
917
+ for (let i = 0; i < n; i++) {
918
+ outOffsets[i + 1] = outOffsets[i] + outCounts[i];
919
+ inOffsets[i + 1] = inOffsets[i] + inCounts[i];
920
+ }
921
+ const outTargets = new Int32Array(outOffsets[n]);
922
+ const outEdgeIndex = new Int32Array(outOffsets[n]);
923
+ const inOrigins = new Int32Array(inOffsets[n]);
924
+ const inEdgeIndex = new Int32Array(inOffsets[n]);
925
+ const outCursor = outOffsets.slice(0, n);
926
+ const inCursor = inOffsets.slice(0, n);
927
+ for (let e = 0; e < m; e++) {
928
+ const s = srcPos[e];
929
+ const t = tgtPos[e];
930
+ if (s < 0) continue;
931
+ outTargets[outCursor[s]] = t;
932
+ outEdgeIndex[outCursor[s]++] = e;
933
+ inOrigins[inCursor[t]] = s;
934
+ inEdgeIndex[inCursor[t]++] = e;
935
+ if (nonDirected[e]) {
936
+ outTargets[outCursor[t]] = s;
937
+ outEdgeIndex[outCursor[t]++] = e;
938
+ inOrigins[inCursor[s]] = t;
939
+ inEdgeIndex[inCursor[s]++] = e;
940
+ }
941
+ }
942
+ return {
943
+ ids,
944
+ indexOf,
945
+ outOffsets,
946
+ outTargets,
947
+ outEdgeIndex,
948
+ inOffsets,
949
+ inOrigins,
950
+ inEdgeIndex,
951
+ firstNegativeEdge
952
+ };
953
+ }
954
+
777
955
  //#endregion
778
956
  //#region src/algorithms/paths.ts
779
- function computeShortestDistances(graph, sourceId, getWeight, algorithm) {
957
+ function computeShortestDistances(graph, sourceId, getWeight, algorithm, stopAtId) {
780
958
  if (algorithm === "bellman-ford") return bellmanFord(graph, sourceId, getWeight);
781
959
  const dist = /* @__PURE__ */ new Map();
782
960
  const prev = /* @__PURE__ */ new Map();
783
961
  dist.set(sourceId, 0);
784
962
  prev.set(sourceId, []);
963
+ const csr = getCSR(graph);
964
+ const source = csr.indexOf.get(sourceId);
965
+ if (source === void 0) return {
966
+ dist,
967
+ prev
968
+ };
969
+ const n = csr.ids.length;
970
+ const distArr = new Float64Array(n).fill(Infinity);
971
+ const prevArr = new Array(n);
972
+ distArr[source] = 0;
973
+ prevArr[source] = [];
974
+ const stopAt = stopAtId !== void 0 ? csr.indexOf.get(stopAtId) : void 0;
975
+ let stopDistance = Infinity;
976
+ if (stopAt !== void 0) assertNoNegativeWeights(graph, csr, getWeight, "Dijkstra", "Use { algorithm: 'bellman-ford' } instead.");
785
977
  if (!getWeight && !graph.edges.some((edge) => edge.weight !== void 0)) {
786
- const queue = [sourceId];
787
- while (queue.length > 0) {
788
- const id = queue.shift();
789
- const distance = dist.get(id);
790
- for (const { neighborId, edge } of getNeighborEdges(graph, id)) {
791
- const nextDistance = distance + 1;
792
- const existing = dist.get(neighborId);
793
- if (existing === void 0) {
794
- dist.set(neighborId, nextDistance);
795
- prev.set(neighborId, [{
796
- from: id,
797
- edge
798
- }]);
799
- queue.push(neighborId);
800
- } else if (existing === nextDistance) prev.get(neighborId).push({
801
- from: id,
802
- edge
803
- });
978
+ const queue = new Int32Array(n);
979
+ queue[0] = source;
980
+ let head = 0;
981
+ let tail = 1;
982
+ while (head < tail) {
983
+ const u = queue[head++];
984
+ if (distArr[u] > stopDistance) break;
985
+ if (u === stopAt) stopDistance = distArr[u];
986
+ const nextDistance = distArr[u] + 1;
987
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
988
+ const v = csr.outTargets[a];
989
+ if (distArr[v] === Infinity) {
990
+ distArr[v] = nextDistance;
991
+ prevArr[v] = [u, csr.outEdgeIndex[a]];
992
+ queue[tail++] = v;
993
+ } else if (distArr[v] === nextDistance) prevArr[v].push(u, csr.outEdgeIndex[a]);
804
994
  }
805
995
  }
806
996
  } else {
807
997
  const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
808
- const visited = /* @__PURE__ */ new Set();
998
+ const visited = new Uint8Array(n);
809
999
  const pq = new MinPriorityQueue((a, b) => a.dist - b.dist);
810
1000
  pq.push({
811
- id: sourceId,
1001
+ pos: source,
812
1002
  dist: 0
813
1003
  });
814
1004
  while (pq.size > 0) {
815
- const { id, dist: distance } = pq.pop();
816
- if (visited.has(id) || distance !== dist.get(id)) continue;
817
- visited.add(id);
818
- for (const { neighborId, edge } of getNeighborEdges(graph, id)) {
819
- const nextDistance = distance + effectiveWeight(edge);
820
- const existing = dist.get(neighborId);
821
- if (existing === void 0 || nextDistance < existing) {
822
- dist.set(neighborId, nextDistance);
823
- prev.set(neighborId, [{
824
- from: id,
825
- edge
826
- }]);
1005
+ const { pos: u, dist: distance } = pq.pop();
1006
+ if (visited[u] || distance !== distArr[u]) continue;
1007
+ if (distance > stopDistance) break;
1008
+ if (u === stopAt) stopDistance = distance;
1009
+ visited[u] = 1;
1010
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
1011
+ const edge = graph.edges[csr.outEdgeIndex[a]];
1012
+ const weight = effectiveWeight(edge);
1013
+ if (weight < 0) throw new Error(`Negative edge weight ${weight} on edge "${edge.sourceId}->${edge.targetId}" (id "${edge.id}"): Dijkstra requires non-negative weights. Use { algorithm: 'bellman-ford' } instead.`);
1014
+ const v = csr.outTargets[a];
1015
+ const nextDistance = distance + weight;
1016
+ if (nextDistance < distArr[v]) {
1017
+ distArr[v] = nextDistance;
1018
+ prevArr[v] = [u, csr.outEdgeIndex[a]];
827
1019
  pq.push({
828
- id: neighborId,
1020
+ pos: v,
829
1021
  dist: nextDistance
830
1022
  });
831
- } else if (existing === nextDistance) prev.get(neighborId).push({
832
- from: id,
833
- edge
834
- });
1023
+ } else if (nextDistance === distArr[v] && distArr[v] !== Infinity) prevArr[v].push(u, csr.outEdgeIndex[a]);
835
1024
  }
836
1025
  }
837
1026
  }
1027
+ for (let i = 0; i < n; i++) {
1028
+ if (distArr[i] === Infinity || distArr[i] > stopDistance) continue;
1029
+ dist.set(csr.ids[i], distArr[i]);
1030
+ const pairs = prevArr[i];
1031
+ const predecessors = [];
1032
+ for (let k = 0; k < pairs.length; k += 2) predecessors.push({
1033
+ from: csr.ids[pairs[k]],
1034
+ edge: graph.edges[pairs[k + 1]]
1035
+ });
1036
+ prev.set(csr.ids[i], predecessors);
1037
+ }
838
1038
  return {
839
1039
  dist,
840
1040
  prev
@@ -844,7 +1044,6 @@ function bellmanFord(graph, sourceId, getWeight) {
844
1044
  const dist = /* @__PURE__ */ new Map();
845
1045
  const prev = /* @__PURE__ */ new Map();
846
1046
  const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
847
- const isUndirected = graph.type === "undirected";
848
1047
  for (const node of graph.nodes) {
849
1048
  dist.set(node.id, Infinity);
850
1049
  prev.set(node.id, []);
@@ -857,7 +1056,7 @@ function bellmanFord(graph, sourceId, getWeight) {
857
1056
  toId: edge.targetId,
858
1057
  edge
859
1058
  });
860
- if (isUndirected) directedEdges.push({
1059
+ if (getEdgeMode(graph, edge) !== "directed") directedEdges.push({
861
1060
  fromId: edge.targetId,
862
1061
  toId: edge.sourceId,
863
1062
  edge
@@ -901,7 +1100,7 @@ function bellmanFord(graph, sourceId, getWeight) {
901
1100
  prev
902
1101
  };
903
1102
  }
904
- function* reconstructPaths(graph, prev, sourceNode, targetId) {
1103
+ function* reconstructPaths(graph, prev, sourceNode, targetId, onPath = /* @__PURE__ */ new Set()) {
905
1104
  if (targetId === sourceNode.id) {
906
1105
  yield {
907
1106
  source: sourceNode,
@@ -913,18 +1112,23 @@ function* reconstructPaths(graph, prev, sourceNode, targetId) {
913
1112
  if (!predecessors || predecessors.length === 0) return;
914
1113
  const targetNi = getIndex(graph).nodeById.get(targetId);
915
1114
  const targetNode = targetNi !== void 0 ? graph.nodes[targetNi] : graph.nodes.find((node) => node.id === targetId);
916
- for (const { from, edge } of predecessors) for (const prefix of reconstructPaths(graph, prev, sourceNode, from)) yield {
917
- source: sourceNode,
918
- steps: [...prefix.steps, {
919
- edge,
920
- node: targetNode
921
- }]
922
- };
1115
+ onPath.add(targetId);
1116
+ for (const { from, edge } of predecessors) {
1117
+ if (onPath.has(from)) continue;
1118
+ for (const prefix of reconstructPaths(graph, prev, sourceNode, from, onPath)) yield {
1119
+ source: sourceNode,
1120
+ steps: [...prefix.steps, {
1121
+ edge,
1122
+ node: targetNode
1123
+ }]
1124
+ };
1125
+ }
1126
+ onPath.delete(targetId);
923
1127
  }
924
1128
  function* genShortestPaths(graph, opts) {
925
1129
  const idx = getIndex(graph);
926
1130
  const sourceId = resolveFrom(graph, opts);
927
- const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm);
1131
+ const { dist, prev } = computeShortestDistances(graph, sourceId, opts?.getWeight, opts?.algorithm, opts?.to);
928
1132
  const targets = opts?.to ? [opts.to].filter((id) => dist.has(id)) : [...dist.keys()].filter((id) => id !== sourceId);
929
1133
  const sourceNi = idx.nodeById.get(sourceId);
930
1134
  const sourceNode = sourceNi !== void 0 ? graph.nodes[sourceNi] : graph.nodes.find((node) => node.id === sourceId);
@@ -934,8 +1138,161 @@ function getShortestPaths(graph, opts) {
934
1138
  return [...genShortestPaths(graph, opts)];
935
1139
  }
936
1140
  function getShortestPath(graph, opts) {
1141
+ if (opts.algorithm !== "bellman-ford") return bidirectionalShortestPath(graph, resolveFrom(graph, opts), opts.to, opts.getWeight);
937
1142
  for (const path of genShortestPaths(graph, opts)) return path;
938
1143
  }
1144
+ /**
1145
+ * Sublinear searches (early-exit, bidirectional) may legitimately terminate
1146
+ * without ever scanning a negative edge, so the throw-on-negative contract
1147
+ * must be enforced up front: O(1) via the CSR's cached flag for the default
1148
+ * weight, or one O(edges) sweep for a custom `getWeight`.
1149
+ */
1150
+ function assertNoNegativeWeights(graph, csr, getWeight, algorithmName, remedy) {
1151
+ let offending;
1152
+ let weight = 0;
1153
+ if (getWeight === void 0) {
1154
+ if (csr.firstNegativeEdge !== -1) {
1155
+ offending = graph.edges[csr.firstNegativeEdge];
1156
+ weight = offending.weight ?? 1;
1157
+ }
1158
+ } else for (const edge of graph.edges) {
1159
+ const w = getWeight(edge);
1160
+ if (w < 0) {
1161
+ offending = edge;
1162
+ weight = w;
1163
+ break;
1164
+ }
1165
+ }
1166
+ if (offending) throw new Error(`Negative edge weight ${weight} on edge "${offending.sourceId}->${offending.targetId}" (id "${offending.id}"): ${algorithmName} requires non-negative weights. ${remedy}`);
1167
+ }
1168
+ /**
1169
+ * Bidirectional Dijkstra for a single source→target query. Forward search
1170
+ * runs on the traversable arcs, backward search on the reverse arcs; `mu`
1171
+ * tracks the best meeting cost and the search stops when the two frontiers
1172
+ * prove no better meeting exists (Pohl's `topF + topB >= mu` condition).
1173
+ * Returns one shortest path (ties broken arbitrarily, as before).
1174
+ */
1175
+ function bidirectionalShortestPath(graph, sourceId, targetId, getWeight) {
1176
+ const csr = getCSR(graph);
1177
+ const source = csr.indexOf.get(sourceId);
1178
+ const target = csr.indexOf.get(targetId);
1179
+ if (source === void 0 || target === void 0) return void 0;
1180
+ const sourceNode = graph.nodes[source];
1181
+ if (source === target) return {
1182
+ source: sourceNode,
1183
+ steps: []
1184
+ };
1185
+ assertNoNegativeWeights(graph, csr, getWeight, "Dijkstra", "Use { algorithm: 'bellman-ford' } instead.");
1186
+ const effectiveWeight = getWeight ?? ((edge) => edge.weight ?? 1);
1187
+ const n = csr.ids.length;
1188
+ const distF = new Float64Array(n).fill(Infinity);
1189
+ const distB = new Float64Array(n).fill(Infinity);
1190
+ const predF = new Int32Array(n).fill(-1);
1191
+ const predFEdge = new Int32Array(n).fill(-1);
1192
+ const predB = new Int32Array(n).fill(-1);
1193
+ const predBEdge = new Int32Array(n).fill(-1);
1194
+ const settledF = new Uint8Array(n);
1195
+ const settledB = new Uint8Array(n);
1196
+ const compare = (a, b) => a.dist - b.dist;
1197
+ const pqF = new MinPriorityQueue(compare);
1198
+ const pqB = new MinPriorityQueue(compare);
1199
+ distF[source] = 0;
1200
+ distB[target] = 0;
1201
+ pqF.push({
1202
+ pos: source,
1203
+ dist: 0
1204
+ });
1205
+ pqB.push({
1206
+ pos: target,
1207
+ dist: 0
1208
+ });
1209
+ let mu = Infinity;
1210
+ let meet = -1;
1211
+ /** Discard stale/settled heap entries; return the next valid key. */
1212
+ const validTop = (pq, dist, settled) => {
1213
+ for (;;) {
1214
+ const top = pq.peek();
1215
+ if (top === void 0) return void 0;
1216
+ if (settled[top.pos] || top.dist !== dist[top.pos]) {
1217
+ pq.pop();
1218
+ continue;
1219
+ }
1220
+ return top.dist;
1221
+ }
1222
+ };
1223
+ const scanForward = () => {
1224
+ const { pos: u, dist: d } = pqF.pop();
1225
+ settledF[u] = 1;
1226
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
1227
+ const edge = graph.edges[csr.outEdgeIndex[a]];
1228
+ const weight = effectiveWeight(edge);
1229
+ const v = csr.outTargets[a];
1230
+ const next = d + weight;
1231
+ if (next < distF[v]) {
1232
+ distF[v] = next;
1233
+ predF[v] = u;
1234
+ predFEdge[v] = csr.outEdgeIndex[a];
1235
+ pqF.push({
1236
+ pos: v,
1237
+ dist: next
1238
+ });
1239
+ }
1240
+ if (distB[v] !== Infinity && next + distB[v] < mu) {
1241
+ mu = next + distB[v];
1242
+ meet = v;
1243
+ }
1244
+ }
1245
+ };
1246
+ const scanBackward = () => {
1247
+ const { pos: u, dist: d } = pqB.pop();
1248
+ settledB[u] = 1;
1249
+ for (let a = csr.inOffsets[u]; a < csr.inOffsets[u + 1]; a++) {
1250
+ const edge = graph.edges[csr.inEdgeIndex[a]];
1251
+ const weight = effectiveWeight(edge);
1252
+ const v = csr.inOrigins[a];
1253
+ const next = d + weight;
1254
+ if (next < distB[v]) {
1255
+ distB[v] = next;
1256
+ predB[v] = u;
1257
+ predBEdge[v] = csr.inEdgeIndex[a];
1258
+ pqB.push({
1259
+ pos: v,
1260
+ dist: next
1261
+ });
1262
+ }
1263
+ if (distF[v] !== Infinity && next + distF[v] < mu) {
1264
+ mu = next + distF[v];
1265
+ meet = v;
1266
+ }
1267
+ }
1268
+ };
1269
+ for (;;) {
1270
+ const topF = validTop(pqF, distF, settledF);
1271
+ const topB = validTop(pqB, distB, settledB);
1272
+ if (topF === void 0 || topB === void 0) break;
1273
+ if (topF + topB >= mu) break;
1274
+ if (topF <= topB) scanForward();
1275
+ else scanBackward();
1276
+ }
1277
+ if (meet === -1) return void 0;
1278
+ const steps = [];
1279
+ for (let v = meet; v !== source; v = predF[v]) steps.unshift({
1280
+ edge: graph.edges[predFEdge[v]],
1281
+ node: graph.nodes[v]
1282
+ });
1283
+ for (let v = meet; v !== target;) {
1284
+ const nextNode = predB[v];
1285
+ steps.push({
1286
+ edge: graph.edges[predBEdge[v]],
1287
+ node: graph.nodes[nextNode]
1288
+ });
1289
+ v = nextNode;
1290
+ }
1291
+ return {
1292
+ source: sourceNode,
1293
+ steps
1294
+ };
1295
+ }
939
1296
  function getSimplePaths(graph, opts) {
940
1297
  return [...genSimplePaths(graph, opts)];
941
1298
  }
@@ -993,15 +1350,10 @@ function getStronglyConnectedComponents(graph) {
993
1350
  indexCounter++;
994
1351
  stack.push(id);
995
1352
  onStack.add(id);
996
- for (const eid of idx.outEdges.get(id) ?? []) {
997
- const ai = idx.edgeById.get(eid);
998
- if (ai === void 0) continue;
999
- const neighborId = graph.edges[ai].targetId;
1000
- if (!nodeIndex.has(neighborId)) {
1001
- strongconnect(neighborId);
1002
- lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(neighborId)));
1003
- } else if (onStack.has(neighborId)) lowlink.set(id, Math.min(lowlink.get(id), nodeIndex.get(neighborId)));
1004
- }
1353
+ for (const neighborId of getNeighborIds(graph, id)) if (!nodeIndex.has(neighborId)) {
1354
+ strongconnect(neighborId);
1355
+ lowlink.set(id, Math.min(lowlink.get(id), lowlink.get(neighborId)));
1356
+ } else if (onStack.has(neighborId)) lowlink.set(id, Math.min(lowlink.get(id), nodeIndex.get(neighborId)));
1005
1357
  if (lowlink.get(id) === nodeIndex.get(id)) {
1006
1358
  const component = [];
1007
1359
  let neighborId;
@@ -1021,7 +1373,9 @@ function getCycles(graph) {
1021
1373
  return [...genCycles(graph)];
1022
1374
  }
1023
1375
  function* genCycles(graph) {
1024
- if (graph.type === "undirected") yield* genCyclesUndirected(graph);
1376
+ const kind = getEffectiveModeKind(graph);
1377
+ if (kind === "mixed") yield* genCyclesMixed(graph);
1378
+ else if (kind === "non-directed") yield* genCyclesUndirected(graph);
1025
1379
  else yield* genCyclesDirected(graph);
1026
1380
  }
1027
1381
  function* genCyclesDirected(graph) {
@@ -1077,17 +1431,14 @@ function* genCyclesUndirected(graph) {
1077
1431
  const startNi = idx.nodeById.get(startId);
1078
1432
  const startNode = graph.nodes[startNi];
1079
1433
  const found = [];
1080
- function dfsFind(currentId, parentId) {
1434
+ function dfsFind(currentId, arrivalEdgeId) {
1081
1435
  visited.add(currentId);
1082
1436
  for (const { neighborId, edge } of getNeighborEdgesAll(graph, currentId)) {
1083
- if (neighborId === parentId) {
1084
- parentId = null;
1085
- continue;
1086
- }
1087
- if (neighborId === startId && steps.length >= 2) {
1088
- const innerIds = steps.map((step) => step.node.id).sort().join(",");
1089
- if (!seen.has(innerIds)) {
1090
- seen.add(innerIds);
1437
+ if (edge.id === arrivalEdgeId) continue;
1438
+ if (neighborId === startId && (steps.length >= 1 || edge.sourceId === edge.targetId)) {
1439
+ const cycleEdgeIds = [...steps.map((step) => step.edge.id), edge.id].sort().join(",");
1440
+ if (!seen.has(cycleEdgeIds)) {
1441
+ seen.add(cycleEdgeIds);
1091
1442
  found.push({
1092
1443
  source: startNode,
1093
1444
  steps: [...steps, {
@@ -1102,7 +1453,7 @@ function* genCyclesUndirected(graph) {
1102
1453
  edge,
1103
1454
  node: graph.nodes[ni]
1104
1455
  });
1105
- dfsFind(neighborId, currentId);
1456
+ dfsFind(neighborId, edge.id);
1106
1457
  steps.pop();
1107
1458
  }
1108
1459
  }
@@ -1112,6 +1463,59 @@ function* genCyclesUndirected(graph) {
1112
1463
  yield* found;
1113
1464
  }
1114
1465
  }
1466
+ /**
1467
+ * Exact simple-cycle enumeration for graphs mixing directed and non-directed
1468
+ * edges. Traverses directed edges source→target only and non-directed edges
1469
+ * both ways; a cycle may use each edge at most once, visits distinct nodes,
1470
+ * and is identified by its set of traversed edge ids.
1471
+ */
1472
+ function* genCyclesMixed(graph) {
1473
+ const idx = getIndex(graph);
1474
+ const sortedIds = graph.nodes.map((node) => node.id).sort();
1475
+ const seen = /* @__PURE__ */ new Set();
1476
+ for (let startIndex = 0; startIndex < sortedIds.length; startIndex++) {
1477
+ const startId = sortedIds[startIndex];
1478
+ const allowed = new Set(sortedIds.slice(startIndex));
1479
+ const visited = /* @__PURE__ */ new Set();
1480
+ const steps = [];
1481
+ const pathEdgeIds = /* @__PURE__ */ new Set();
1482
+ const startNi = idx.nodeById.get(startId);
1483
+ const startNode = graph.nodes[startNi];
1484
+ const found = [];
1485
+ function dfsFind(currentId) {
1486
+ visited.add(currentId);
1487
+ for (const { neighborId, edge } of getNeighborEdges(graph, currentId)) {
1488
+ if (pathEdgeIds.has(edge.id)) continue;
1489
+ if (neighborId === startId && (steps.length >= 1 || edge.sourceId === edge.targetId)) {
1490
+ const cycleEdgeIds = [...steps.map((step) => step.edge.id), edge.id].sort().join(",");
1491
+ if (!seen.has(cycleEdgeIds)) {
1492
+ seen.add(cycleEdgeIds);
1493
+ found.push({
1494
+ source: startNode,
1495
+ steps: [...steps, {
1496
+ edge,
1497
+ node: startNode
1498
+ }]
1499
+ });
1500
+ }
1501
+ } else if (allowed.has(neighborId) && !visited.has(neighborId)) {
1502
+ const ni = idx.nodeById.get(neighborId);
1503
+ steps.push({
1504
+ edge,
1505
+ node: graph.nodes[ni]
1506
+ });
1507
+ pathEdgeIds.add(edge.id);
1508
+ dfsFind(neighborId);
1509
+ pathEdgeIds.delete(edge.id);
1510
+ steps.pop();
1511
+ }
1512
+ }
1513
+ visited.delete(currentId);
1514
+ }
1515
+ dfsFind(startId);
1516
+ yield* found;
1517
+ }
1518
+ }
1115
1519
  function getAllPairsShortestPaths(graph, opts) {
1116
1520
  const algorithm = opts?.algorithm ?? "dijkstra";
1117
1521
  if (algorithm === "floyd-warshall") return floydWarshallAllPaths(graph, opts?.getWeight);
@@ -1160,7 +1564,7 @@ function floydWarshallAllPaths(graph, getWeight) {
1160
1564
  from: source,
1161
1565
  edge
1162
1566
  });
1163
- if (graph.type === "undirected") {
1567
+ if (getEdgeMode(graph, edge) !== "directed") {
1164
1568
  if (edgeWeight < dist[target][source]) {
1165
1569
  dist[target][source] = edgeWeight;
1166
1570
  prev[target][source] = [{
@@ -1183,6 +1587,7 @@ function floydWarshallAllPaths(graph, getWeight) {
1183
1587
  for (const entry of prev[k][j]) if (!prev[i][j].some((existing) => existing.edge.id === entry.edge.id)) prev[i][j].push({ ...entry });
1184
1588
  }
1185
1589
  }
1590
+ for (let i = 0; i < nodeCount; i++) if (dist[i][i] < 0) throw new Error(`Negative cycle detected through node "${nodeIds[i]}": all-pairs shortest paths are undefined. Remove the negative cycle, or use getShortestPaths with { algorithm: 'bellman-ford' } per source to locate it.`);
1186
1591
  const results = [];
1187
1592
  for (let i = 0; i < nodeCount; i++) {
1188
1593
  const sourceNi = idx.nodeById.get(nodeIds[i]);
@@ -1229,48 +1634,53 @@ function getAStarPath(graph, opts) {
1229
1634
  source: graph.nodes[sourceNi],
1230
1635
  steps: []
1231
1636
  };
1232
- const gScore = /* @__PURE__ */ new Map();
1233
- const cameFrom = /* @__PURE__ */ new Map();
1234
- const closedSet = /* @__PURE__ */ new Set();
1637
+ const csr = getCSR(graph);
1638
+ const n = csr.ids.length;
1639
+ const source = csr.indexOf.get(sourceId);
1640
+ const target = csr.indexOf.get(targetId);
1641
+ const gScore = new Float64Array(n).fill(Infinity);
1642
+ const cameFromPos = new Int32Array(n).fill(-1);
1643
+ const cameFromEdge = new Int32Array(n).fill(-1);
1644
+ const closed = new Uint8Array(n);
1235
1645
  const openSet = new MinPriorityQueue((a, b) => a.f - b.f);
1236
- gScore.set(sourceId, 0);
1646
+ assertNoNegativeWeights(graph, csr, opts.getWeight, "A*", "Use getShortestPath with { algorithm: 'bellman-ford' } instead.");
1647
+ gScore[source] = 0;
1237
1648
  openSet.push({
1238
- id: sourceId,
1649
+ pos: source,
1239
1650
  f: heuristic(sourceId)
1240
1651
  });
1241
1652
  while (openSet.size > 0) {
1242
- const { id: currentId } = openSet.pop();
1243
- if (closedSet.has(currentId)) continue;
1244
- if (currentId === targetId) {
1653
+ const { pos: current } = openSet.pop();
1654
+ if (closed[current]) continue;
1655
+ if (current === target) {
1245
1656
  const steps = [];
1246
- let current = targetId;
1247
- while (current !== sourceId) {
1248
- const previous = cameFrom.get(current);
1249
- const ni = idx.nodeById.get(current);
1657
+ let cursor = target;
1658
+ while (cursor !== source) {
1250
1659
  steps.unshift({
1251
- edge: previous.edge,
1252
- node: graph.nodes[ni]
1660
+ edge: graph.edges[cameFromEdge[cursor]],
1661
+ node: graph.nodes[cursor]
1253
1662
  });
1254
- current = previous.from;
1663
+ cursor = cameFromPos[cursor];
1255
1664
  }
1256
1665
  return {
1257
1666
  source: graph.nodes[sourceNi],
1258
1667
  steps
1259
1668
  };
1260
1669
  }
1261
- closedSet.add(currentId);
1262
- for (const { neighborId, edge } of getNeighborEdges(graph, currentId)) {
1263
- if (closedSet.has(neighborId)) continue;
1264
- const tentativeScore = (gScore.get(currentId) ?? Infinity) + getWeight(edge);
1265
- if (tentativeScore < (gScore.get(neighborId) ?? Infinity)) {
1266
- cameFrom.set(neighborId, {
1267
- from: currentId,
1268
- edge
1269
- });
1270
- gScore.set(neighborId, tentativeScore);
1670
+ closed[current] = 1;
1671
+ for (let a = csr.outOffsets[current]; a < csr.outOffsets[current + 1]; a++) {
1672
+ const edge = graph.edges[csr.outEdgeIndex[a]];
1673
+ const weight = getWeight(edge);
1674
+ const neighbor = csr.outTargets[a];
1675
+ if (closed[neighbor]) continue;
1676
+ const tentativeScore = gScore[current] + weight;
1677
+ if (tentativeScore < gScore[neighbor]) {
1678
+ cameFromPos[neighbor] = current;
1679
+ cameFromEdge[neighbor] = csr.outEdgeIndex[a];
1680
+ gScore[neighbor] = tentativeScore;
1271
1681
  openSet.push({
1272
- id: neighborId,
1273
- f: tentativeScore + heuristic(neighborId)
1682
+ pos: neighbor,
1683
+ f: tentativeScore + heuristic(csr.ids[neighbor])
1274
1684
  });
1275
1685
  }
1276
1686
  }
@@ -1285,40 +1695,289 @@ function joinPaths(headPath, tailPath) {
1285
1695
  };
1286
1696
  }
1287
1697
 
1698
+ //#endregion
1699
+ //#region src/config.ts
1700
+ /**
1701
+ * Convert a resolved {@link GraphNode} back into a {@link NodeConfig}.
1702
+ *
1703
+ * Faithful and complete: round-tripping through `createGraphNode` yields a
1704
+ * deep-equal node. Optional fields are only included when present; ports are
1705
+ * deep-copied so the config does not share port objects with the source node.
1706
+ */
1707
+ function toNodeConfig(node) {
1708
+ const config = { id: node.id };
1709
+ if (node.parentId != null) config.parentId = node.parentId;
1710
+ if (node.initialNodeId != null) config.initialNodeId = node.initialNodeId;
1711
+ if (node.label != null) config.label = node.label;
1712
+ if (node.data != null) config.data = node.data;
1713
+ if (node.ports !== void 0) config.ports = node.ports.map((p) => ({ ...p }));
1714
+ if (node.x !== void 0) config.x = node.x;
1715
+ if (node.y !== void 0) config.y = node.y;
1716
+ if (node.width !== void 0) config.width = node.width;
1717
+ if (node.height !== void 0) config.height = node.height;
1718
+ if (node.shape !== void 0) config.shape = node.shape;
1719
+ if (node.color !== void 0) config.color = node.color;
1720
+ if (node.style !== void 0) config.style = node.style;
1721
+ return config;
1722
+ }
1723
+ /**
1724
+ * Convert a resolved {@link GraphEdge} back into an {@link EdgeConfig}.
1725
+ *
1726
+ * Faithful and complete: round-tripping through `createGraphEdge` yields a
1727
+ * deep-equal edge. Optional fields are only included when present.
1728
+ */
1729
+ function toEdgeConfig(edge) {
1730
+ const config = {
1731
+ id: edge.id,
1732
+ sourceId: edge.sourceId,
1733
+ targetId: edge.targetId
1734
+ };
1735
+ if (edge.label != null) config.label = edge.label;
1736
+ if (edge.data != null) config.data = edge.data;
1737
+ if (edge.weight !== void 0) config.weight = edge.weight;
1738
+ if (edge.mode !== void 0) config.mode = edge.mode;
1739
+ if (edge.sourcePort !== void 0) config.sourcePort = edge.sourcePort;
1740
+ if (edge.targetPort !== void 0) config.targetPort = edge.targetPort;
1741
+ if (edge.x !== void 0) config.x = edge.x;
1742
+ if (edge.y !== void 0) config.y = edge.y;
1743
+ if (edge.width !== void 0) config.width = edge.width;
1744
+ if (edge.height !== void 0) config.height = edge.height;
1745
+ if (edge.color !== void 0) config.color = edge.color;
1746
+ if (edge.style !== void 0) config.style = edge.style;
1747
+ return config;
1748
+ }
1749
+
1750
+ //#endregion
1751
+ //#region src/transforms.ts
1752
+ /**
1753
+ * Flattens a hierarchical graph into a flat graph with only leaf nodes.
1754
+ *
1755
+ * - Edges targeting a compound node resolve to its initial child (recursively).
1756
+ * - Edges originating from a compound node expand to all leaf descendants.
1757
+ * - Only leaf nodes (nodes with no children) appear in the result.
1758
+ * - Duplicate edges (same source + target) are deduplicated.
1759
+ *
1760
+ * @example
1761
+ * ```ts
1762
+ * import { createGraph, flatten } from '@statelyai/graph';
1763
+ *
1764
+ * const graph = createGraph({
1765
+ * nodes: [
1766
+ * { id: 'parent', initialNodeId: 'child1' },
1767
+ * { id: 'child1', parentId: 'parent' },
1768
+ * { id: 'child2', parentId: 'parent' },
1769
+ * { id: 'other' },
1770
+ * ],
1771
+ * edges: [{ id: 'e1', sourceId: 'other', targetId: 'parent' }],
1772
+ * });
1773
+ *
1774
+ * const flat = flatten(graph);
1775
+ * // flat.nodes → [child1, child2, other] (leaf nodes only)
1776
+ * // flat.edges → edge from 'other' → 'child1' (resolved via initialNodeId)
1777
+ * ```
1778
+ */
1779
+ function flatten(graph) {
1780
+ const idx = getIndex(graph);
1781
+ const leaves = /* @__PURE__ */ new Set();
1782
+ for (const node of graph.nodes) if ((idx.childNodes.get(node.id) ?? []).length === 0) leaves.add(node.id);
1783
+ function resolveInitial(nodeId, seen = /* @__PURE__ */ new Set()) {
1784
+ if (leaves.has(nodeId)) return nodeId;
1785
+ if (seen.has(nodeId)) return null;
1786
+ seen.add(nodeId);
1787
+ const ni = idx.nodeById.get(nodeId);
1788
+ if (ni === void 0) return null;
1789
+ const node = graph.nodes[ni];
1790
+ if (node.initialNodeId) return resolveInitial(node.initialNodeId, seen);
1791
+ const childIds = idx.childNodes.get(nodeId) ?? [];
1792
+ if (childIds.length > 0) return resolveInitial(childIds[0], seen);
1793
+ return nodeId;
1794
+ }
1795
+ function getLeafDescendants(nodeId) {
1796
+ if (leaves.has(nodeId)) return [nodeId];
1797
+ const result = [];
1798
+ const collect = (id) => {
1799
+ const childIds = idx.childNodes.get(id) ?? [];
1800
+ for (const childId of childIds) if (leaves.has(childId)) result.push(childId);
1801
+ else collect(childId);
1802
+ };
1803
+ collect(nodeId);
1804
+ return result;
1805
+ }
1806
+ const edgeSeen = /* @__PURE__ */ new Set();
1807
+ const flatEdges = [];
1808
+ for (const edge of graph.edges) {
1809
+ const sources = leaves.has(edge.sourceId) ? [edge.sourceId] : getLeafDescendants(edge.sourceId);
1810
+ const target = leaves.has(edge.targetId) ? edge.targetId : resolveInitial(edge.targetId);
1811
+ if (target === null) continue;
1812
+ for (const source of sources) {
1813
+ const isAuthoredLeafSelfLoop = edge.sourceId === edge.targetId && leaves.has(edge.sourceId);
1814
+ if (source === target && !isAuthoredLeafSelfLoop) continue;
1815
+ const key = `${source}->${target}`;
1816
+ if (edgeSeen.has(key)) continue;
1817
+ edgeSeen.add(key);
1818
+ flatEdges.push({
1819
+ type: "edge",
1820
+ id: `${edge.id}:${source}->${target}`,
1821
+ sourceId: source,
1822
+ targetId: target,
1823
+ label: edge.label,
1824
+ data: edge.data,
1825
+ ...edge.weight !== void 0 && { weight: edge.weight },
1826
+ ...edge.mode !== void 0 && { mode: edge.mode },
1827
+ ...source === edge.sourceId && edge.sourcePort !== void 0 && { sourcePort: edge.sourcePort },
1828
+ ...target === edge.targetId && edge.targetPort !== void 0 && { targetPort: edge.targetPort }
1829
+ });
1830
+ }
1831
+ }
1832
+ const leafNodes = graph.nodes.filter((n) => leaves.has(n.id)).map((n) => {
1833
+ const { type, parentId, initialNodeId, ...rest } = n;
1834
+ return rest;
1835
+ });
1836
+ return createGraph({
1837
+ id: graph.id,
1838
+ mode: graph.mode,
1839
+ initialNodeId: graph.initialNodeId ? resolveInitial(graph.initialNodeId) ?? void 0 : void 0,
1840
+ nodes: leafNodes,
1841
+ edges: flatEdges,
1842
+ data: graph.data
1843
+ });
1844
+ }
1845
+ /**
1846
+ * Convert a node to a config, stripping parentId/initialNodeId references
1847
+ * to nodes outside the given set.
1848
+ */
1849
+ function toScopedNodeConfig(node, nodeIdSet) {
1850
+ const config = toNodeConfig(node);
1851
+ if (nodeIdSet) {
1852
+ if (config.parentId != null && !nodeIdSet.has(config.parentId)) delete config.parentId;
1853
+ if (config.initialNodeId != null && !nodeIdSet.has(config.initialNodeId)) delete config.initialNodeId;
1854
+ }
1855
+ return config;
1856
+ }
1857
+ /**
1858
+ * Returns the induced subgraph containing only the given node IDs
1859
+ * and edges whose endpoints are both in the set.
1860
+ *
1861
+ * Parent references to nodes outside the set are removed.
1862
+ *
1863
+ * @example
1864
+ * ```ts
1865
+ * import { createGraph, getSubgraph } from '@statelyai/graph';
1866
+ *
1867
+ * const graph = createGraph({
1868
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
1869
+ * edges: [
1870
+ * { id: 'ab', sourceId: 'a', targetId: 'b' },
1871
+ * { id: 'bc', sourceId: 'b', targetId: 'c' },
1872
+ * ],
1873
+ * });
1874
+ *
1875
+ * const sub = getSubgraph(graph, ['a', 'b']);
1876
+ * // sub.nodes: [a, b], sub.edges: [ab]
1877
+ * ```
1878
+ */
1879
+ function getSubgraph(graph, nodeIds) {
1880
+ const nodeIdSet = new Set(nodeIds);
1881
+ return createGraph({
1882
+ id: graph.id,
1883
+ mode: graph.mode,
1884
+ initialNodeId: graph.initialNodeId && nodeIdSet.has(graph.initialNodeId) ? graph.initialNodeId : void 0,
1885
+ nodes: graph.nodes.filter((n) => nodeIdSet.has(n.id)).map((n) => toScopedNodeConfig(n, nodeIdSet)),
1886
+ edges: graph.edges.filter((e) => nodeIdSet.has(e.sourceId) && nodeIdSet.has(e.targetId)).map(toEdgeConfig),
1887
+ data: graph.data
1888
+ });
1889
+ }
1890
+ /**
1891
+ * Returns a new graph with all edge directions flipped (source ↔ target).
1892
+ * Optionally filters which edges to include.
1893
+ *
1894
+ * @example
1895
+ * ```ts
1896
+ * import { createGraph, reverseGraph } from '@statelyai/graph';
1897
+ *
1898
+ * const graph = createGraph({
1899
+ * nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
1900
+ * edges: [
1901
+ * { id: 'ab', sourceId: 'a', targetId: 'b' },
1902
+ * { id: 'bc', sourceId: 'b', targetId: 'c' },
1903
+ * ],
1904
+ * });
1905
+ *
1906
+ * const rev = reverseGraph(graph);
1907
+ * // rev edges: b→a, c→b
1908
+ *
1909
+ * const filtered = reverseGraph(graph, (e) => e.id !== 'bc');
1910
+ * // filtered edges: b→a (only ab reversed, bc excluded)
1911
+ * ```
1912
+ */
1913
+ function reverseGraph(graph, filterEdge) {
1914
+ const edges = filterEdge ? graph.edges.filter(filterEdge) : graph.edges;
1915
+ return createGraph({
1916
+ id: graph.id,
1917
+ mode: graph.mode,
1918
+ initialNodeId: graph.initialNodeId ?? void 0,
1919
+ nodes: graph.nodes.map((n) => toNodeConfig(n)),
1920
+ edges: edges.map((e) => {
1921
+ const config = toEdgeConfig(e);
1922
+ config.sourceId = e.targetId;
1923
+ config.targetId = e.sourceId;
1924
+ delete config.sourcePort;
1925
+ delete config.targetPort;
1926
+ if (e.targetPort !== void 0) config.sourcePort = e.targetPort;
1927
+ if (e.sourcePort !== void 0) config.targetPort = e.sourcePort;
1928
+ return config;
1929
+ }),
1930
+ data: graph.data
1931
+ });
1932
+ }
1933
+
1288
1934
  //#endregion
1289
1935
  //#region src/algorithms/traversal.ts
1290
1936
  function* bfs(graph, startId) {
1291
- const idx = getIndex(graph);
1292
- const visited = /* @__PURE__ */ new Set();
1293
- const queue = [startId];
1294
- visited.add(startId);
1295
- while (queue.length > 0) {
1296
- const id = queue.shift();
1297
- const ni = idx.nodeById.get(id);
1298
- if (ni === void 0) continue;
1299
- yield graph.nodes[ni];
1300
- for (const neighborId of getNeighborIds$1(graph, id)) if (!visited.has(neighborId)) {
1301
- visited.add(neighborId);
1302
- queue.push(neighborId);
1937
+ const csr = getCSR(graph);
1938
+ const start = csr.indexOf.get(startId);
1939
+ if (start === void 0) return;
1940
+ const n = csr.ids.length;
1941
+ const visited = new Uint8Array(n);
1942
+ const queue = new Int32Array(n);
1943
+ visited[start] = 1;
1944
+ queue[0] = start;
1945
+ let head = 0;
1946
+ let tail = 1;
1947
+ while (head < tail) {
1948
+ const u = queue[head++];
1949
+ yield graph.nodes[u];
1950
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
1951
+ const v = csr.outTargets[a];
1952
+ if (!visited[v]) {
1953
+ visited[v] = 1;
1954
+ queue[tail++] = v;
1955
+ }
1303
1956
  }
1304
1957
  }
1305
1958
  }
1306
1959
  function* dfs(graph, startId) {
1307
- const idx = getIndex(graph);
1308
- const visited = /* @__PURE__ */ new Set();
1309
- const stack = [startId];
1960
+ const csr = getCSR(graph);
1961
+ const start = csr.indexOf.get(startId);
1962
+ if (start === void 0) return;
1963
+ const n = csr.ids.length;
1964
+ const visited = new Uint8Array(n);
1965
+ const stack = [start];
1310
1966
  while (stack.length > 0) {
1311
- const id = stack.pop();
1312
- if (visited.has(id)) continue;
1313
- visited.add(id);
1314
- const ni = idx.nodeById.get(id);
1315
- if (ni === void 0) continue;
1316
- yield graph.nodes[ni];
1317
- for (const neighborId of getNeighborIds$1(graph, id)) if (!visited.has(neighborId)) stack.push(neighborId);
1967
+ const u = stack.pop();
1968
+ if (visited[u]) continue;
1969
+ visited[u] = 1;
1970
+ yield graph.nodes[u];
1971
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
1972
+ const v = csr.outTargets[a];
1973
+ if (!visited[v]) stack.push(v);
1974
+ }
1318
1975
  }
1319
1976
  }
1320
1977
  function isAcyclic(graph) {
1321
- if (graph.type === "undirected") return isAcyclicUndirected(graph);
1978
+ const kind = getEffectiveModeKind(graph);
1979
+ if (kind === "mixed") return isAcyclicMixed(graph);
1980
+ if (kind === "non-directed") return isAcyclicUndirected(graph);
1322
1981
  const WHITE = 0;
1323
1982
  const GRAY = 1;
1324
1983
  const BLACK = 2;
@@ -1337,6 +1996,63 @@ function isAcyclic(graph) {
1337
1996
  for (const node of graph.nodes) if (color.get(node.id) === WHITE && hasCycle(node.id)) return false;
1338
1997
  return true;
1339
1998
  }
1999
+ /**
2000
+ * Acyclicity for graphs mixing directed and non-directed edges.
2001
+ *
2002
+ * Polynomial fast paths first: a cycle among directed edges alone, a cycle
2003
+ * among non-directed edges alone (union-find), or all-singleton reachability
2004
+ * SCCs (then no mixed cycle can exist either). Only ambiguous multi-node
2005
+ * SCCs fall back to exact simple-cycle enumeration, restricted to that SCC.
2006
+ */
2007
+ function isAcyclicMixed(graph) {
2008
+ const idx = getIndex(graph);
2009
+ const WHITE = 0;
2010
+ const GRAY = 1;
2011
+ const BLACK = 2;
2012
+ const color = /* @__PURE__ */ new Map();
2013
+ for (const node of graph.nodes) color.set(node.id, WHITE);
2014
+ const hasDirectedCycle = (id) => {
2015
+ color.set(id, GRAY);
2016
+ for (const eid of idx.outEdges.get(id) ?? []) {
2017
+ const edge = graph.edges[idx.edgeById.get(eid)];
2018
+ if (getEdgeMode(graph, edge) !== "directed") continue;
2019
+ const current = color.get(edge.targetId);
2020
+ if (current === GRAY) return true;
2021
+ if (current === WHITE && hasDirectedCycle(edge.targetId)) return true;
2022
+ }
2023
+ color.set(id, BLACK);
2024
+ return false;
2025
+ };
2026
+ for (const node of graph.nodes) if (color.get(node.id) === WHITE && hasDirectedCycle(node.id)) return false;
2027
+ const parent = /* @__PURE__ */ new Map();
2028
+ const find = (id) => {
2029
+ let root = id;
2030
+ while (parent.get(root) !== root) root = parent.get(root);
2031
+ let cursor = id;
2032
+ while (parent.get(cursor) !== root) {
2033
+ const next = parent.get(cursor);
2034
+ parent.set(cursor, root);
2035
+ cursor = next;
2036
+ }
2037
+ return root;
2038
+ };
2039
+ for (const node of graph.nodes) parent.set(node.id, node.id);
2040
+ for (const edge of graph.edges) {
2041
+ if (getEdgeMode(graph, edge) === "directed") continue;
2042
+ if (edge.sourceId === edge.targetId) return false;
2043
+ const rootA = find(edge.sourceId);
2044
+ const rootB = find(edge.targetId);
2045
+ if (rootA === rootB) return false;
2046
+ parent.set(rootA, rootB);
2047
+ }
2048
+ const multiNodeSccs = getStronglyConnectedComponents(graph).filter((component) => component.length > 1);
2049
+ if (multiNodeSccs.length === 0) return true;
2050
+ for (const component of multiNodeSccs) {
2051
+ const subgraph = getSubgraph(graph, component.map((node) => node.id));
2052
+ for (const _cycle of genCycles(subgraph)) return false;
2053
+ }
2054
+ return true;
2055
+ }
1340
2056
  function isAcyclicUndirected(graph) {
1341
2057
  const idx = getIndex(graph);
1342
2058
  const visited = /* @__PURE__ */ new Set();
@@ -1364,34 +2080,33 @@ function isAcyclicUndirected(graph) {
1364
2080
  return true;
1365
2081
  }
1366
2082
  function getConnectedComponents(graph) {
1367
- const idx = getIndex(graph);
1368
- const visited = /* @__PURE__ */ new Set();
2083
+ const csr = getCSR(graph);
2084
+ const n = csr.ids.length;
2085
+ const visited = new Uint8Array(n);
2086
+ const queue = new Int32Array(n);
1369
2087
  const components = [];
1370
- for (const node of graph.nodes) {
1371
- if (visited.has(node.id)) continue;
2088
+ for (let s = 0; s < n; s++) {
2089
+ if (visited[s]) continue;
1372
2090
  const component = [];
1373
- const queue = [node.id];
1374
- visited.add(node.id);
1375
- while (queue.length > 0) {
1376
- const id = queue.shift();
1377
- const ni = idx.nodeById.get(id);
1378
- if (ni !== void 0) component.push(graph.nodes[ni]);
1379
- for (const eid of idx.outEdges.get(id) ?? []) {
1380
- const ai = idx.edgeById.get(eid);
1381
- if (ai === void 0) continue;
1382
- const neighborId = graph.edges[ai].targetId;
1383
- if (!visited.has(neighborId)) {
1384
- visited.add(neighborId);
1385
- queue.push(neighborId);
2091
+ visited[s] = 1;
2092
+ queue[0] = s;
2093
+ let head = 0;
2094
+ let tail = 1;
2095
+ while (head < tail) {
2096
+ const u = queue[head++];
2097
+ component.push(graph.nodes[u]);
2098
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
2099
+ const v = csr.outTargets[a];
2100
+ if (!visited[v]) {
2101
+ visited[v] = 1;
2102
+ queue[tail++] = v;
1386
2103
  }
1387
2104
  }
1388
- for (const eid of idx.inEdges.get(id) ?? []) {
1389
- const ai = idx.edgeById.get(eid);
1390
- if (ai === void 0) continue;
1391
- const neighborId = graph.edges[ai].sourceId;
1392
- if (!visited.has(neighborId)) {
1393
- visited.add(neighborId);
1394
- queue.push(neighborId);
2105
+ for (let a = csr.inOffsets[u]; a < csr.inOffsets[u + 1]; a++) {
2106
+ const v = csr.inOrigins[a];
2107
+ if (!visited[v]) {
2108
+ visited[v] = 1;
2109
+ queue[tail++] = v;
1395
2110
  }
1396
2111
  }
1397
2112
  }
@@ -1399,7 +2114,16 @@ function getConnectedComponents(graph) {
1399
2114
  }
1400
2115
  return components;
1401
2116
  }
2117
+ /**
2118
+ * Returns a topological ordering of the graph's nodes, or `null` if no such
2119
+ * ordering exists.
2120
+ *
2121
+ * Any edge whose effective mode (per {@link getEdgeMode}) is not `'directed'`
2122
+ * makes ordering impossible — an undirected/bidirectional edge is mutual
2123
+ * precedence, i.e. a 2-cycle — so the function returns `null`.
2124
+ */
1402
2125
  function getTopologicalSort(graph) {
2126
+ for (const edge of graph.edges) if (getEdgeMode(graph, edge) !== "directed") return null;
1403
2127
  const idx = getIndex(graph);
1404
2128
  const inDegree = /* @__PURE__ */ new Map();
1405
2129
  for (const node of graph.nodes) inDegree.set(node.id, 0);
@@ -1424,17 +2148,33 @@ function getTopologicalSort(graph) {
1424
2148
  return result;
1425
2149
  }
1426
2150
  function hasPath(graph, sourceId, targetId) {
1427
- return getShortestPaths(graph, {
1428
- from: sourceId,
1429
- to: targetId
1430
- }).length > 0;
2151
+ if (sourceId === targetId) return true;
2152
+ const visited = new Set([sourceId]);
2153
+ const queue = [sourceId];
2154
+ while (queue.length > 0) {
2155
+ const id = queue.shift();
2156
+ for (const neighborId of getNeighborIds(graph, id)) {
2157
+ if (neighborId === targetId) return true;
2158
+ if (!visited.has(neighborId)) {
2159
+ visited.add(neighborId);
2160
+ queue.push(neighborId);
2161
+ }
2162
+ }
2163
+ }
2164
+ return false;
1431
2165
  }
1432
2166
  function isConnected(graph) {
1433
2167
  if (graph.nodes.length === 0) return true;
1434
2168
  return getConnectedComponents(graph).length <= 1;
1435
2169
  }
2170
+ /**
2171
+ * Returns whether the graph is a tree: connected, acyclic, and with exactly
2172
+ * `nodes.length - 1` edges (so directed diamonds and parallel edges are not
2173
+ * trees). Empty and single-node graphs are considered trees.
2174
+ */
1436
2175
  function isTree(graph) {
1437
- return isConnected(graph) && isAcyclic(graph);
2176
+ if (graph.nodes.length === 0) return true;
2177
+ return graph.edges.length === graph.nodes.length - 1 && isConnected(graph) && isAcyclic(graph);
1438
2178
  }
1439
2179
 
1440
2180
  //#endregion
@@ -1449,7 +2189,7 @@ function getPreorder(graph, opts) {
1449
2189
  const stack = [startId];
1450
2190
  while (stack.length > 0) {
1451
2191
  const top = stack[stack.length - 1];
1452
- const next = getNeighborIds$1(graph, top).find((id) => !visited.has(id));
2192
+ const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
1453
2193
  if (next === void 0) {
1454
2194
  stack.pop();
1455
2195
  continue;
@@ -1470,7 +2210,7 @@ function getPostorder(graph, opts) {
1470
2210
  const stack = [startId];
1471
2211
  while (stack.length > 0) {
1472
2212
  const top = stack[stack.length - 1];
1473
- const next = getNeighborIds$1(graph, top).find((id) => !visited.has(id));
2213
+ const next = getNeighborIds(graph, top).find((id) => !visited.has(id));
1474
2214
  if (next === void 0) {
1475
2215
  stack.pop();
1476
2216
  const ni = idx.nodeById.get(top);
@@ -1506,7 +2246,7 @@ function* genPreorders(graph, opts) {
1506
2246
  let branched = false;
1507
2247
  while (dfsStack.length > 0) {
1508
2248
  const top = dfsStack[dfsStack.length - 1];
1509
- const unvisited = getNeighborIds$1(graph, top).filter((id) => !visited.has(id));
2249
+ const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
1510
2250
  if (unvisited.length === 0) {
1511
2251
  dfsStack.pop();
1512
2252
  continue;
@@ -1544,7 +2284,7 @@ function* genPostorders(graph, opts) {
1544
2284
  let branched = false;
1545
2285
  while (dfsStack.length > 0) {
1546
2286
  const top = dfsStack[dfsStack.length - 1];
1547
- const unvisited = getNeighborIds$1(graph, top).filter((id) => !visited.has(id));
2287
+ const unvisited = getNeighborIds(graph, top).filter((id) => !visited.has(id));
1548
2288
  if (unvisited.length === 0) {
1549
2289
  dfsStack.pop();
1550
2290
  const ni = idx.nodeById.get(top);
@@ -1576,23 +2316,10 @@ function getMinimumSpanningTree(graph, opts) {
1576
2316
  const mstEdges = algorithm === "kruskal" ? kruskalMST(graph, getWeight) : primMST(graph, getWeight);
1577
2317
  return createGraph({
1578
2318
  id: graph.id,
1579
- type: graph.type,
2319
+ mode: graph.mode,
1580
2320
  initialNodeId: graph.initialNodeId ?? void 0,
1581
- nodes: graph.nodes.map((node) => ({
1582
- id: node.id,
1583
- parentId: node.parentId ?? void 0,
1584
- initialNodeId: node.initialNodeId ?? void 0,
1585
- label: node.label,
1586
- data: node.data
1587
- })),
1588
- edges: mstEdges.map((edge) => ({
1589
- id: edge.id,
1590
- sourceId: edge.sourceId,
1591
- targetId: edge.targetId,
1592
- label: edge.label,
1593
- data: edge.data,
1594
- ...edge.weight !== void 0 && { weight: edge.weight }
1595
- }))
2321
+ nodes: graph.nodes.map((node) => toNodeConfig(node)),
2322
+ edges: mstEdges.map((edge) => toEdgeConfig(edge))
1596
2323
  });
1597
2324
  }
1598
2325
  function primMST(graph, getWeight) {
@@ -1611,26 +2338,28 @@ function primMST(graph, getWeight) {
1611
2338
  edge
1612
2339
  });
1613
2340
  }
1614
- if (graph.type === "undirected") for (const eid of idx.inEdges.get(nodeId) ?? []) {
2341
+ for (const eid of idx.inEdges.get(nodeId) ?? []) {
1615
2342
  const ai = idx.edgeById.get(eid);
1616
2343
  if (ai === void 0) continue;
1617
2344
  const edge = graph.edges[ai];
1618
- if (!inMST.has(edge.sourceId)) candidates.push({
2345
+ if (getEdgeMode(graph, edge) !== "directed" && !inMST.has(edge.sourceId)) candidates.push({
1619
2346
  weight: getWeight(edge),
1620
2347
  edge
1621
2348
  });
1622
2349
  }
1623
2350
  }
1624
- const startId = graph.nodes[0].id;
1625
- inMST.add(startId);
1626
- addEdgesOf(startId);
1627
- while (candidates.size > 0 && inMST.size < graph.nodes.length) {
1628
- const { edge } = candidates.pop();
1629
- const targetId = graph.type === "undirected" && inMST.has(edge.targetId) ? edge.sourceId : edge.targetId;
1630
- if (inMST.has(targetId)) continue;
1631
- inMST.add(targetId);
1632
- mstEdges.push(edge);
1633
- addEdgesOf(targetId);
2351
+ for (const node of graph.nodes) {
2352
+ if (inMST.has(node.id)) continue;
2353
+ inMST.add(node.id);
2354
+ addEdgesOf(node.id);
2355
+ while (candidates.size > 0 && inMST.size < graph.nodes.length) {
2356
+ const { edge } = candidates.pop();
2357
+ const targetId = getEdgeMode(graph, edge) !== "directed" && inMST.has(edge.targetId) ? edge.sourceId : edge.targetId;
2358
+ if (inMST.has(targetId)) continue;
2359
+ inMST.add(targetId);
2360
+ mstEdges.push(edge);
2361
+ addEdgesOf(targetId);
2362
+ }
1634
2363
  }
1635
2364
  return mstEdges;
1636
2365
  }
@@ -1668,60 +2397,38 @@ function kruskalMST(graph, getWeight) {
1668
2397
  function getNodeIds(graph) {
1669
2398
  return graph.nodes.map((node) => node.id);
1670
2399
  }
1671
- function getNeighborIds(graph, nodeId) {
1672
- const idx = getIndex(graph);
1673
- const neighbors = [];
1674
- for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
1675
- const edgeIndex = idx.edgeById.get(edgeId);
1676
- if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].targetId);
1677
- }
1678
- if (graph.type === "undirected") for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
1679
- const edgeIndex = idx.edgeById.get(edgeId);
1680
- if (edgeIndex !== void 0) neighbors.push(graph.edges[edgeIndex].sourceId);
1681
- }
1682
- return neighbors;
1683
- }
1684
- function getIncomingIds(graph, nodeId) {
1685
- const idx = getIndex(graph);
1686
- const incoming = [];
1687
- for (const edgeId of idx.inEdges.get(nodeId) ?? []) {
1688
- const edgeIndex = idx.edgeById.get(edgeId);
1689
- if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].sourceId);
1690
- }
1691
- if (graph.type === "undirected") for (const edgeId of idx.outEdges.get(nodeId) ?? []) {
1692
- const edgeIndex = idx.edgeById.get(edgeId);
1693
- if (edgeIndex !== void 0) incoming.push(graph.edges[edgeIndex].targetId);
1694
- }
1695
- return incoming;
1696
- }
1697
2400
  function createEmptyScoreMap(graph) {
1698
2401
  return Object.fromEntries(graph.nodes.map((node) => [node.id, 0]));
1699
2402
  }
1700
- function normalizeVector(scores) {
1701
- const magnitude = Math.sqrt(Object.values(scores).reduce((sum, value) => sum + value * value, 0));
1702
- if (magnitude === 0) return scores;
1703
- for (const key of Object.keys(scores)) scores[key] /= magnitude;
1704
- return scores;
1705
- }
1706
- function maxDiff(previous, next) {
1707
- let diff = 0;
1708
- for (const key of Object.keys(next)) diff = Math.max(diff, Math.abs((previous[key] ?? 0) - next[key]));
1709
- return diff;
2403
+ function normalizeTypedVector(values) {
2404
+ let sumOfSquares = 0;
2405
+ for (let i = 0; i < values.length; i++) sumOfSquares += values[i] * values[i];
2406
+ const magnitude = Math.sqrt(sumOfSquares);
2407
+ if (magnitude === 0) return;
2408
+ for (let i = 0; i < values.length; i++) values[i] /= magnitude;
1710
2409
  }
1711
- function getReachableDistances(graph, startId) {
1712
- const distances = /* @__PURE__ */ new Map();
1713
- const queue = [startId];
1714
- distances.set(startId, 0);
1715
- while (queue.length > 0) {
1716
- const currentId = queue.shift();
1717
- const currentDistance = distances.get(currentId);
1718
- for (const neighborId of getNeighborIds(graph, currentId)) {
1719
- if (distances.has(neighborId)) continue;
1720
- distances.set(neighborId, currentDistance + 1);
1721
- queue.push(neighborId);
2410
+ /**
2411
+ * BFS hop distances from a start position over the CSR arc snapshot.
2412
+ * `dist[i] === -1` means unreachable. Returns the visit count.
2413
+ */
2414
+ function bfsDistances(csr, start, dist, queue) {
2415
+ dist.fill(-1);
2416
+ dist[start] = 0;
2417
+ queue[0] = start;
2418
+ let head = 0;
2419
+ let tail = 1;
2420
+ while (head < tail) {
2421
+ const u = queue[head++];
2422
+ const du = dist[u];
2423
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
2424
+ const v = csr.outTargets[a];
2425
+ if (dist[v] === -1) {
2426
+ dist[v] = du + 1;
2427
+ queue[tail++] = v;
2428
+ }
1722
2429
  }
1723
2430
  }
1724
- return distances;
2431
+ return tail;
1725
2432
  }
1726
2433
  /**
1727
2434
  * Returns degree centrality scores for all nodes.
@@ -1736,14 +2443,8 @@ function getReachableDistances(graph, startId) {
1736
2443
  */
1737
2444
  function getDegreeCentrality(graph) {
1738
2445
  const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
1739
- const idx = getIndex(graph);
1740
2446
  const scores = createEmptyScoreMap(graph);
1741
- for (const node of graph.nodes) {
1742
- const outDegree = idx.outEdges.get(node.id)?.length ?? 0;
1743
- 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;
1745
- scores[node.id] = degree * scale;
1746
- }
2447
+ for (const node of graph.nodes) scores[node.id] = getDegree(graph, node.id) * scale;
1747
2448
  return scores;
1748
2449
  }
1749
2450
  /**
@@ -1753,9 +2454,8 @@ function getDegreeCentrality(graph) {
1753
2454
  */
1754
2455
  function getInDegreeCentrality(graph) {
1755
2456
  const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
1756
- const idx = getIndex(graph);
1757
2457
  const scores = createEmptyScoreMap(graph);
1758
- for (const node of graph.nodes) scores[node.id] = (idx.inEdges.get(node.id)?.length ?? 0) * scale;
2458
+ for (const node of graph.nodes) scores[node.id] = getInDegree(graph, node.id) * scale;
1759
2459
  return scores;
1760
2460
  }
1761
2461
  /**
@@ -1765,9 +2465,8 @@ function getInDegreeCentrality(graph) {
1765
2465
  */
1766
2466
  function getOutDegreeCentrality(graph) {
1767
2467
  const scale = graph.nodes.length > 1 ? 1 / (graph.nodes.length - 1) : 0;
1768
- const idx = getIndex(graph);
1769
2468
  const scores = createEmptyScoreMap(graph);
1770
- for (const node of graph.nodes) scores[node.id] = (idx.outEdges.get(node.id)?.length ?? 0) * scale;
2469
+ for (const node of graph.nodes) scores[node.id] = getOutDegree(graph, node.id) * scale;
1771
2470
  return scores;
1772
2471
  }
1773
2472
  /**
@@ -1778,16 +2477,19 @@ function getOutDegreeCentrality(graph) {
1778
2477
  */
1779
2478
  function getClosenessCentrality(graph) {
1780
2479
  const scores = createEmptyScoreMap(graph);
1781
- const order = graph.nodes.length;
1782
- for (const node of graph.nodes) {
1783
- const distances = getReachableDistances(graph, node.id);
1784
- distances.delete(node.id);
1785
- if (distances.size === 0) continue;
1786
- const totalDistance = [...distances.values()].reduce((sum, distance) => sum + distance, 0);
2480
+ const csr = getCSR(graph);
2481
+ const order = csr.ids.length;
2482
+ const dist = new Int32Array(order);
2483
+ const queue = new Int32Array(order);
2484
+ for (let s = 0; s < order; s++) {
2485
+ const visited = bfsDistances(csr, s, dist, queue);
2486
+ const reachable = visited - 1;
2487
+ if (reachable === 0) continue;
2488
+ let totalDistance = 0;
2489
+ for (let k = 0; k < visited; k++) totalDistance += dist[queue[k]];
1787
2490
  if (totalDistance === 0) continue;
1788
- const reachable = distances.size;
1789
2491
  const closeness = reachable / totalDistance;
1790
- scores[node.id] = order > 1 ? closeness * (reachable / (order - 1)) : closeness;
2492
+ scores[csr.ids[s]] = order > 1 ? closeness * (reachable / (order - 1)) : closeness;
1791
2493
  }
1792
2494
  return scores;
1793
2495
  }
@@ -1798,52 +2500,53 @@ function getClosenessCentrality(graph) {
1798
2500
  * normalized scores.
1799
2501
  */
1800
2502
  function getBetweennessCentrality(graph) {
1801
- const scores = createEmptyScoreMap(graph);
1802
- for (const source of graph.nodes) {
1803
- const stack = [];
1804
- const predecessors = /* @__PURE__ */ new Map();
1805
- const sigma = /* @__PURE__ */ new Map();
1806
- const distance = /* @__PURE__ */ new Map();
1807
- const queue = [source.id];
1808
- for (const node of graph.nodes) {
1809
- predecessors.set(node.id, []);
1810
- sigma.set(node.id, 0);
1811
- distance.set(node.id, -1);
1812
- }
1813
- sigma.set(source.id, 1);
1814
- distance.set(source.id, 0);
1815
- while (queue.length > 0) {
1816
- const currentId = queue.shift();
1817
- stack.push(currentId);
1818
- for (const neighborId of getNeighborIds(graph, currentId)) {
1819
- if (distance.get(neighborId) === -1) {
1820
- queue.push(neighborId);
1821
- distance.set(neighborId, distance.get(currentId) + 1);
1822
- }
1823
- if (distance.get(neighborId) === distance.get(currentId) + 1) {
1824
- sigma.set(neighborId, sigma.get(neighborId) + sigma.get(currentId));
1825
- predecessors.get(neighborId).push(currentId);
2503
+ const csr = getCSR(graph);
2504
+ const n = csr.ids.length;
2505
+ const totals = new Float64Array(n);
2506
+ const sigma = new Float64Array(n);
2507
+ const dist = new Int32Array(n);
2508
+ const delta = new Float64Array(n);
2509
+ const order_ = new Int32Array(n);
2510
+ for (let s = 0; s < n; s++) {
2511
+ sigma.fill(0);
2512
+ dist.fill(-1);
2513
+ delta.fill(0);
2514
+ sigma[s] = 1;
2515
+ dist[s] = 0;
2516
+ order_[0] = s;
2517
+ let head = 0;
2518
+ let tail = 1;
2519
+ while (head < tail) {
2520
+ const u = order_[head++];
2521
+ const du = dist[u];
2522
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) {
2523
+ const v = csr.outTargets[a];
2524
+ if (dist[v] === -1) {
2525
+ dist[v] = du + 1;
2526
+ order_[tail++] = v;
1826
2527
  }
2528
+ if (dist[v] === du + 1) sigma[v] += sigma[u];
1827
2529
  }
1828
2530
  }
1829
- const delta = /* @__PURE__ */ new Map();
1830
- for (const node of graph.nodes) delta.set(node.id, 0);
1831
- while (stack.length > 0) {
1832
- const nodeId = stack.pop();
1833
- const sigmaNode = sigma.get(nodeId);
1834
- if (sigmaNode === 0) continue;
1835
- for (const predecessorId of predecessors.get(nodeId)) {
1836
- const contribution = sigma.get(predecessorId) / sigmaNode * (1 + delta.get(nodeId));
1837
- delta.set(predecessorId, delta.get(predecessorId) + contribution);
2531
+ for (let k = tail - 1; k >= 0; k--) {
2532
+ const w = order_[k];
2533
+ const sigmaW = sigma[w];
2534
+ if (sigmaW === 0) continue;
2535
+ const coefficient = (1 + delta[w]) / sigmaW;
2536
+ for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) {
2537
+ const v = csr.inOrigins[a];
2538
+ if (dist[v] === dist[w] - 1) delta[v] += sigma[v] * coefficient;
1838
2539
  }
1839
- if (nodeId !== source.id) scores[nodeId] += delta.get(nodeId);
2540
+ if (w !== s) totals[w] += delta[w];
1840
2541
  }
1841
2542
  }
2543
+ const scores = createEmptyScoreMap(graph);
2544
+ for (let i = 0; i < n; i++) scores[csr.ids[i]] = totals[i];
1842
2545
  const order = graph.nodes.length;
1843
2546
  if (order <= 2) return scores;
1844
- const scale = graph.type === "undirected" ? 1 / ((order - 1) * (order - 2) / 2) : 1 / ((order - 1) * (order - 2));
2547
+ const scale = graph.mode !== "directed" ? 1 / ((order - 1) * (order - 2) / 2) : 1 / ((order - 1) * (order - 2));
1845
2548
  for (const nodeId of Object.keys(scores)) {
1846
- if (graph.type === "undirected") scores[nodeId] /= 2;
2549
+ if (graph.mode !== "directed") scores[nodeId] /= 2;
1847
2550
  scores[nodeId] *= scale;
1848
2551
  }
1849
2552
  return scores;
@@ -1860,28 +2563,32 @@ function getPageRank(graph, options) {
1860
2563
  const maxIterations = options?.maxIterations ?? 100;
1861
2564
  const tolerance = options?.tolerance ?? 1e-6;
1862
2565
  let scores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1 / nodeIds.length]));
2566
+ const csr = getCSR(graph);
2567
+ const n = csr.ids.length;
2568
+ let current = new Float64Array(n).fill(1 / n);
2569
+ for (let i = 0; i < n; i++) current[i] = scores[csr.ids[i]];
1863
2570
  for (let iteration = 0; iteration < maxIterations; iteration++) {
1864
- const nextScores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, (1 - alpha) / nodeIds.length]));
2571
+ const next = new Float64Array(n).fill((1 - alpha) / n);
1865
2572
  let danglingMass = 0;
1866
- for (const nodeId of nodeIds) {
1867
- const neighbors = getNeighborIds(graph, nodeId);
1868
- if (neighbors.length === 0) {
1869
- danglingMass += scores[nodeId];
2573
+ for (let u = 0; u < n; u++) {
2574
+ const arcCount = csr.outOffsets[u + 1] - csr.outOffsets[u];
2575
+ if (arcCount === 0) {
2576
+ danglingMass += current[u];
1870
2577
  continue;
1871
2578
  }
1872
- const share = scores[nodeId] / neighbors.length;
1873
- for (const neighborId of neighbors) nextScores[neighborId] += alpha * share;
2579
+ const share = alpha * current[u] / arcCount;
2580
+ for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) next[csr.outTargets[a]] += share;
1874
2581
  }
1875
2582
  if (danglingMass > 0) {
1876
- const share = alpha * danglingMass / nodeIds.length;
1877
- for (const nodeId of nodeIds) nextScores[nodeId] += share;
1878
- }
1879
- if (maxDiff(scores, nextScores) <= tolerance) {
1880
- scores = nextScores;
1881
- break;
2583
+ const share = alpha * danglingMass / n;
2584
+ for (let i = 0; i < n; i++) next[i] += share;
1882
2585
  }
1883
- scores = nextScores;
2586
+ let diff = 0;
2587
+ for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(current[i] - next[i]));
2588
+ current = next;
2589
+ if (diff <= tolerance) break;
1884
2590
  }
2591
+ for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
1885
2592
  const total = Object.values(scores).reduce((sum, value) => sum + value, 0);
1886
2593
  if (total !== 0) for (const nodeId of nodeIds) scores[nodeId] /= total;
1887
2594
  return scores;
@@ -1892,31 +2599,38 @@ function getPageRank(graph, options) {
1892
2599
  * Uses power iteration and L2 normalization per iteration.
1893
2600
  */
1894
2601
  function getHITS(graph, options) {
1895
- const nodeIds = getNodeIds(graph);
1896
- if (nodeIds.length === 0) return {
2602
+ if (getNodeIds(graph).length === 0) return {
1897
2603
  hubs: {},
1898
2604
  authorities: {}
1899
2605
  };
1900
2606
  const maxIterations = options?.maxIterations ?? 100;
1901
2607
  const tolerance = options?.tolerance ?? 1e-6;
1902
- let hubs = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1]));
1903
- let authorities = createEmptyScoreMap(graph);
2608
+ const csr = getCSR(graph);
2609
+ const n = csr.ids.length;
2610
+ let hubs = new Float64Array(n).fill(1);
2611
+ let authorities = new Float64Array(n);
1904
2612
  for (let iteration = 0; iteration < maxIterations; iteration++) {
1905
- const nextAuthorities = createEmptyScoreMap(graph);
1906
- for (const nodeId of nodeIds) for (const predecessorId of getIncomingIds(graph, nodeId)) nextAuthorities[nodeId] += hubs[predecessorId];
1907
- normalizeVector(nextAuthorities);
1908
- const nextHubs = createEmptyScoreMap(graph);
1909
- for (const nodeId of nodeIds) for (const neighborId of getNeighborIds(graph, nodeId)) nextHubs[nodeId] += nextAuthorities[neighborId];
1910
- normalizeVector(nextHubs);
1911
- const hubDiff = maxDiff(hubs, nextHubs);
1912
- const authorityDiff = maxDiff(authorities, nextAuthorities);
2613
+ const nextAuthorities = new Float64Array(n);
2614
+ for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) nextAuthorities[w] += hubs[csr.inOrigins[a]];
2615
+ normalizeTypedVector(nextAuthorities);
2616
+ const nextHubs = new Float64Array(n);
2617
+ for (let u = 0; u < n; u++) for (let a = csr.outOffsets[u]; a < csr.outOffsets[u + 1]; a++) nextHubs[u] += nextAuthorities[csr.outTargets[a]];
2618
+ normalizeTypedVector(nextHubs);
2619
+ let diff = 0;
2620
+ for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(hubs[i] - nextHubs[i]), Math.abs(authorities[i] - nextAuthorities[i]));
1913
2621
  hubs = nextHubs;
1914
2622
  authorities = nextAuthorities;
1915
- if (Math.max(hubDiff, authorityDiff) <= tolerance) break;
2623
+ if (diff <= tolerance) break;
2624
+ }
2625
+ const hubScores = createEmptyScoreMap(graph);
2626
+ const authorityScores = createEmptyScoreMap(graph);
2627
+ for (let i = 0; i < n; i++) {
2628
+ hubScores[csr.ids[i]] = hubs[i];
2629
+ authorityScores[csr.ids[i]] = authorities[i];
1916
2630
  }
1917
2631
  return {
1918
- hubs,
1919
- authorities
2632
+ hubs: hubScores,
2633
+ authorities: authorityScores
1920
2634
  };
1921
2635
  }
1922
2636
  /**
@@ -1926,20 +2640,24 @@ function getHITS(graph, options) {
1926
2640
  * undirected adjacency for undirected graphs.
1927
2641
  */
1928
2642
  function getEigenvectorCentrality(graph, options) {
1929
- const nodeIds = getNodeIds(graph);
1930
- if (nodeIds.length === 0) return {};
2643
+ if (getNodeIds(graph).length === 0) return {};
1931
2644
  const maxIterations = options?.maxIterations ?? 100;
1932
2645
  const tolerance = options?.tolerance ?? 1e-6;
1933
- let scores = Object.fromEntries(nodeIds.map((nodeId) => [nodeId, 1]));
1934
- normalizeVector(scores);
2646
+ const csr = getCSR(graph);
2647
+ const n = csr.ids.length;
2648
+ let current = new Float64Array(n).fill(1);
2649
+ normalizeTypedVector(current);
1935
2650
  for (let iteration = 0; iteration < maxIterations; iteration++) {
1936
- const nextScores = createEmptyScoreMap(graph);
1937
- for (const nodeId of nodeIds) for (const predecessorId of getIncomingIds(graph, nodeId)) nextScores[nodeId] += scores[predecessorId];
1938
- normalizeVector(nextScores);
1939
- const diff = maxDiff(scores, nextScores);
1940
- scores = nextScores;
2651
+ const next = new Float64Array(n);
2652
+ for (let w = 0; w < n; w++) for (let a = csr.inOffsets[w]; a < csr.inOffsets[w + 1]; a++) next[w] += current[csr.inOrigins[a]];
2653
+ normalizeTypedVector(next);
2654
+ let diff = 0;
2655
+ for (let i = 0; i < n; i++) diff = Math.max(diff, Math.abs(current[i] - next[i]));
2656
+ current = next;
1941
2657
  if (diff <= tolerance) break;
1942
2658
  }
2659
+ const scores = createEmptyScoreMap(graph);
2660
+ for (let i = 0; i < n; i++) scores[csr.ids[i]] = current[i];
1943
2661
  return scores;
1944
2662
  }
1945
2663
 
@@ -2247,8 +2965,8 @@ function traverseConnectivity(graph, nodeId, parentEdgeId, state) {
2247
2965
  traverseConnectivity(graph, neighbor.nodeId, neighbor.edgeId, state);
2248
2966
  state.low.set(nodeId, Math.min(state.low.get(nodeId), state.low.get(neighbor.nodeId)));
2249
2967
  if (state.low.get(neighbor.nodeId) > state.disc.get(nodeId)) state.bridges.add(neighbor.edgeId);
2250
- if (parentEdgeId !== null && state.low.get(neighbor.nodeId) >= state.disc.get(nodeId)) {
2251
- state.articulationPoints.add(nodeId);
2968
+ if (state.low.get(neighbor.nodeId) >= state.disc.get(nodeId)) {
2969
+ if (parentEdgeId !== null) state.articulationPoints.add(nodeId);
2252
2970
  popComponentUntil(state, neighbor.edgeId);
2253
2971
  }
2254
2972
  } else if (state.disc.get(neighbor.nodeId) < state.disc.get(nodeId)) {
@@ -2312,14 +3030,33 @@ function getBiconnectedComponents(graph) {
2312
3030
  //#region src/algorithms/isomorphism.ts
2313
3031
  function getDegreeSignature(graph, nodeId) {
2314
3032
  const idx = getIndex(graph);
2315
- const outDegree = idx.outEdges.get(nodeId)?.length ?? 0;
2316
- 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}`;
2318
- return `d:${inDegree}:${outDegree}`;
3033
+ let inDegree = 0;
3034
+ let outDegree = 0;
3035
+ let undirected = 0;
3036
+ const countedNonDirected = /* @__PURE__ */ new Set();
3037
+ for (const eid of idx.outEdges.get(nodeId) ?? []) {
3038
+ const edge = graph.edges[idx.edgeById.get(eid)];
3039
+ if (getEdgeMode(graph, edge) === "directed") outDegree++;
3040
+ else if (!countedNonDirected.has(eid)) {
3041
+ countedNonDirected.add(eid);
3042
+ undirected++;
3043
+ }
3044
+ }
3045
+ for (const eid of idx.inEdges.get(nodeId) ?? []) {
3046
+ const edge = graph.edges[idx.edgeById.get(eid)];
3047
+ if (getEdgeMode(graph, edge) === "directed") inDegree++;
3048
+ else if (!countedNonDirected.has(eid)) {
3049
+ countedNonDirected.add(eid);
3050
+ undirected++;
3051
+ }
3052
+ }
3053
+ return `d:${inDegree}:${outDegree}:u:${undirected}`;
2319
3054
  }
2320
3055
  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);
2322
- return graph.edges.filter((edge) => edge.sourceId === sourceId && edge.targetId === targetId);
3056
+ return graph.edges.filter((edge) => {
3057
+ if (edge.sourceId === sourceId && edge.targetId === targetId) return true;
3058
+ return edge.sourceId === targetId && edge.targetId === sourceId && getEdgeMode(graph, edge) !== "directed";
3059
+ });
2323
3060
  }
2324
3061
  function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
2325
3062
  if (edgesA.length !== edgesB.length) return false;
@@ -2339,7 +3076,6 @@ function edgesAreCompatible(edgesA, edgesB, edgeMatch) {
2339
3076
  * node and edge payloads.
2340
3077
  */
2341
3078
  function isIsomorphic(graphA, graphB, options) {
2342
- if (graphA.type !== graphB.type) return false;
2343
3079
  if (graphA.nodes.length !== graphB.nodes.length) return false;
2344
3080
  if (graphA.edges.length !== graphB.edges.length) return false;
2345
3081
  const nodeMatch = options?.nodeMatch;
@@ -2363,6 +3099,7 @@ function isIsomorphic(graphA, graphB, options) {
2363
3099
  if (usedB.has(nodeB.id)) continue;
2364
3100
  if (getDegreeSignature(graphB, nodeB.id) !== signatureA) continue;
2365
3101
  if (nodeMatch && !nodeMatch(nodeA, nodeB)) continue;
3102
+ if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, nodeA.id), getEdgesBetween(graphB, nodeB.id, nodeB.id), edgeMatch)) continue;
2366
3103
  let compatible = true;
2367
3104
  for (const [mappedAId, mappedBId] of mapping.entries()) {
2368
3105
  if (!edgesAreCompatible(getEdgesBetween(graphA, nodeA.id, mappedAId), getEdgesBetween(graphB, nodeB.id, mappedBId), edgeMatch)) {
@@ -2387,4 +3124,404 @@ function isIsomorphic(graphA, graphB, options) {
2387
3124
  }
2388
3125
 
2389
3126
  //#endregion
2390
- export { createGraphPort as $, isAcyclic as A, getShortestPaths as B, getPreorder as C, getConnectedComponents as D, dfs as E, genSimplePaths as F, GraphInstance as G, getSimplePaths as H, getAStarPath as I, addNode as J, addEdge as K, getAllPairsShortestPaths as L, isTree as M, genCycles as N, getTopologicalSort as O, genShortestPaths as P, createGraphNode as Q, getCycles as R, getPostorders as S, bfs as T, getStronglyConnectedComponents as U, getSimplePath as V, joinPaths as W, createGraphEdge as X, createGraph as Y, createGraphFromTransition as Z, getPageRank as _, genGirvanNewmanCommunities as a, getNode as at, genPreorders as b, getLabelPropagationCommunities as c, updateEdge as ct, getClosenessCentrality as d, createVisualGraph as et, getDegreeCentrality as f, getOutDegreeCentrality as g, getInDegreeCentrality as h, getBridges as i, getEdge as it, isConnected as j, hasPath as k, getModularity as l, updateEntities as lt, getHITS as m, getArticulationPoints as n, deleteEntities as nt, getGirvanNewmanCommunities as o, hasEdge as ot, getEigenvectorCentrality as p, addEntities as q, getBiconnectedComponents as r, deleteNode as rt, getGreedyModularityCommunities as s, hasNode as st, isIsomorphic as t, deleteEdge as tt, getBetweennessCentrality as u, updateNode as ut, getMinimumSpanningTree as v, getPreorders as w, getPostorder as x, genPostorders as y, getShortestPath as z };
3127
+ //#region src/algorithms/louvain.ts
3128
+ /**
3129
+ * Returns communities found by the classic two-phase Louvain modularity
3130
+ * optimization (local moving + community aggregation).
3131
+ *
3132
+ * Like the other community algorithms in this library, the graph is treated
3133
+ * as undirected regardless of `graph.mode` or per-edge modes. Parallel edges
3134
+ * have their weights summed; self-loops contribute to a community's internal
3135
+ * weight.
3136
+ *
3137
+ * The implementation is deterministic: nodes are visited in `graph.nodes`
3138
+ * array order and there is no random shuffling, so tie-breaking is
3139
+ * order-dependent but stable across runs.
3140
+ *
3141
+ * Returns communities of node ids, each community sorted lexicographically
3142
+ * and communities sorted by their first id.
3143
+ *
3144
+ * @example
3145
+ * ```ts
3146
+ * const communities = getLouvainCommunities(graph);
3147
+ * // [['a', 'b', 'c'], ['d', 'e', 'f']]
3148
+ * ```
3149
+ */
3150
+ function getLouvainCommunities(graph, options) {
3151
+ if (graph.nodes.length === 0) return [];
3152
+ const getWeight = options?.getWeight ?? ((edge) => edge.weight ?? 1);
3153
+ const resolution = options?.resolution ?? 1;
3154
+ const maxPasses = options?.maxPasses ?? 10;
3155
+ const nodeIds = graph.nodes.map((node) => node.id);
3156
+ const indexOf = new Map(nodeIds.map((id, i) => [id, i]));
3157
+ let count = nodeIds.length;
3158
+ let links = Array.from({ length: count }, () => /* @__PURE__ */ new Map());
3159
+ let selfLoops = new Array(count).fill(0);
3160
+ for (const edge of graph.edges) {
3161
+ const u = indexOf.get(edge.sourceId);
3162
+ const v = indexOf.get(edge.targetId);
3163
+ if (u === void 0 || v === void 0) continue;
3164
+ const w = getWeight(edge);
3165
+ if (u === v) selfLoops[u] += w;
3166
+ else {
3167
+ links[u].set(v, (links[u].get(v) ?? 0) + w);
3168
+ links[v].set(u, (links[v].get(u) ?? 0) + w);
3169
+ }
3170
+ }
3171
+ let membership = nodeIds.map((_, i) => i);
3172
+ for (let pass = 0; pass < maxPasses; pass++) {
3173
+ const degree = links.map((neighbors, i) => 2 * selfLoops[i] + [...neighbors.values()].reduce((sum, w) => sum + w, 0));
3174
+ const m2 = degree.reduce((sum, k) => sum + k, 0);
3175
+ if (m2 === 0) break;
3176
+ const communityOf = Array.from({ length: count }, (_, i) => i);
3177
+ const communityTotal = [...degree];
3178
+ let movedAny = false;
3179
+ let movedThisSweep = true;
3180
+ while (movedThisSweep) {
3181
+ movedThisSweep = false;
3182
+ for (let i = 0; i < count; i++) {
3183
+ const current = communityOf[i];
3184
+ const weightTo = /* @__PURE__ */ new Map();
3185
+ for (const [j, w] of links[i]) {
3186
+ const c = communityOf[j];
3187
+ weightTo.set(c, (weightTo.get(c) ?? 0) + w);
3188
+ }
3189
+ communityTotal[current] -= degree[i];
3190
+ const gainOf = (c) => (weightTo.get(c) ?? 0) - resolution * communityTotal[c] * degree[i] / m2;
3191
+ let best = current;
3192
+ let bestGain = gainOf(current);
3193
+ for (const c of weightTo.keys()) {
3194
+ if (c === current) continue;
3195
+ const gain = gainOf(c);
3196
+ if (gain > bestGain + 1e-12) {
3197
+ best = c;
3198
+ bestGain = gain;
3199
+ }
3200
+ }
3201
+ communityTotal[best] += degree[i];
3202
+ if (best !== current) {
3203
+ communityOf[i] = best;
3204
+ movedThisSweep = true;
3205
+ movedAny = true;
3206
+ }
3207
+ }
3208
+ }
3209
+ if (!movedAny) break;
3210
+ const renumber = /* @__PURE__ */ new Map();
3211
+ for (let i = 0; i < count; i++) {
3212
+ const c = communityOf[i];
3213
+ if (!renumber.has(c)) renumber.set(c, renumber.size);
3214
+ }
3215
+ const nextCount = renumber.size;
3216
+ const nextLinks = Array.from({ length: nextCount }, () => /* @__PURE__ */ new Map());
3217
+ const nextSelfLoops = new Array(nextCount).fill(0);
3218
+ for (let i = 0; i < count; i++) {
3219
+ const ci = renumber.get(communityOf[i]);
3220
+ nextSelfLoops[ci] += selfLoops[i];
3221
+ for (const [j, w] of links[i]) {
3222
+ const cj = renumber.get(communityOf[j]);
3223
+ if (ci === cj) nextSelfLoops[ci] += w / 2;
3224
+ else nextLinks[ci].set(cj, (nextLinks[ci].get(cj) ?? 0) + w);
3225
+ }
3226
+ }
3227
+ membership = membership.map((c) => renumber.get(communityOf[c]));
3228
+ links = nextLinks;
3229
+ selfLoops = nextSelfLoops;
3230
+ count = nextCount;
3231
+ if (nextCount === 1) break;
3232
+ }
3233
+ const grouped = /* @__PURE__ */ new Map();
3234
+ for (let i = 0; i < nodeIds.length; i++) {
3235
+ const c = membership[i];
3236
+ if (!grouped.has(c)) grouped.set(c, []);
3237
+ grouped.get(c).push(nodeIds[i]);
3238
+ }
3239
+ return [...grouped.values()].map((ids) => ids.sort((a, b) => a.localeCompare(b))).sort((a, b) => a[0].localeCompare(b[0]));
3240
+ }
3241
+
3242
+ //#endregion
3243
+ //#region src/algorithms/flow.ts
3244
+ /**
3245
+ * Returns the maximum flow from `from` to `to` using the Edmonds-Karp
3246
+ * algorithm (BFS augmenting paths).
3247
+ *
3248
+ * Directed edges carry capacity from source to target only. Edges whose
3249
+ * effective mode is not `'directed'` (undirected/bidirectional) are modeled
3250
+ * as two independent opposite arcs, each with the edge's full capacity.
3251
+ *
3252
+ * The returned `flows` record maps every edge id to its net flow (positive
3253
+ * in the source→target direction). `cutEdges` is a minimum s-t cut: the
3254
+ * edges crossing from the source side to the sink side of the final
3255
+ * residual graph; the sum of their capacities equals `value`.
3256
+ *
3257
+ * @example
3258
+ * ```ts
3259
+ * const { value, cutEdges } = getMaxFlow(graph, { from: 's', to: 't' });
3260
+ * ```
3261
+ */
3262
+ function getMaxFlow(graph, options) {
3263
+ const { from, to } = options;
3264
+ const getCapacity = options.getCapacity ?? ((edge) => edge.weight ?? 1);
3265
+ const idx = getIndex(graph);
3266
+ if (!idx.nodeById.has(from)) throw new Error(`getMaxFlow: source node "${from}" not found in graph — pass an existing node id as options.from`);
3267
+ if (!idx.nodeById.has(to)) throw new Error(`getMaxFlow: sink node "${to}" not found in graph — pass an existing node id as options.to`);
3268
+ if (from === to) throw new Error(`getMaxFlow: source and sink are both "${from}" — they must be different nodes`);
3269
+ const arcs = [];
3270
+ const outArcs = /* @__PURE__ */ new Map();
3271
+ for (const node of graph.nodes) outArcs.set(node.id, []);
3272
+ function addArc(u, v, capacity, edgeId, sign) {
3273
+ outArcs.get(u).push(arcs.length);
3274
+ arcs.push({
3275
+ to: v,
3276
+ capacity,
3277
+ flow: 0,
3278
+ edgeId,
3279
+ sign
3280
+ });
3281
+ outArcs.get(v).push(arcs.length);
3282
+ arcs.push({
3283
+ to: u,
3284
+ capacity: 0,
3285
+ flow: 0
3286
+ });
3287
+ }
3288
+ for (const edge of graph.edges) {
3289
+ const capacity = getCapacity(edge);
3290
+ if (capacity < 0) throw new Error(`getMaxFlow: edge "${edge.id}" has negative capacity ${capacity} — capacities must be >= 0; fix edge.weight or provide a non-negative getCapacity`);
3291
+ if (edge.sourceId === edge.targetId) continue;
3292
+ addArc(edge.sourceId, edge.targetId, capacity, edge.id, 1);
3293
+ if (getEdgeMode(graph, edge) !== "directed") addArc(edge.targetId, edge.sourceId, capacity, edge.id, -1);
3294
+ }
3295
+ function residual(arcIndex) {
3296
+ return arcs[arcIndex].capacity - arcs[arcIndex].flow;
3297
+ }
3298
+ let value = 0;
3299
+ while (true) {
3300
+ const parentArc = /* @__PURE__ */ new Map();
3301
+ const queue$1 = [from];
3302
+ const visited = new Set([from]);
3303
+ while (queue$1.length > 0 && !visited.has(to)) {
3304
+ const u = queue$1.shift();
3305
+ for (const ai of outArcs.get(u) ?? []) {
3306
+ const arc = arcs[ai];
3307
+ if (residual(ai) > 0 && !visited.has(arc.to)) {
3308
+ visited.add(arc.to);
3309
+ parentArc.set(arc.to, ai);
3310
+ queue$1.push(arc.to);
3311
+ }
3312
+ }
3313
+ }
3314
+ if (!visited.has(to)) break;
3315
+ let bottleneck = Infinity;
3316
+ for (let v = to; v !== from;) {
3317
+ const ai = parentArc.get(v);
3318
+ bottleneck = Math.min(bottleneck, residual(ai));
3319
+ v = arcs[ai ^ 1].to;
3320
+ }
3321
+ if (bottleneck === Infinity || bottleneck <= 0) break;
3322
+ for (let v = to; v !== from;) {
3323
+ const ai = parentArc.get(v);
3324
+ arcs[ai].flow += bottleneck;
3325
+ arcs[ai ^ 1].flow -= bottleneck;
3326
+ v = arcs[ai ^ 1].to;
3327
+ }
3328
+ value += bottleneck;
3329
+ }
3330
+ const flows = Object.fromEntries(graph.edges.map((edge) => [edge.id, 0]));
3331
+ for (const arc of arcs) if (arc.edgeId !== void 0 && arc.flow > 0) flows[arc.edgeId] += arc.sign * arc.flow;
3332
+ const sourceSide = new Set([from]);
3333
+ const queue = [from];
3334
+ while (queue.length > 0) {
3335
+ const u = queue.shift();
3336
+ for (const ai of outArcs.get(u) ?? []) {
3337
+ const arc = arcs[ai];
3338
+ if (residual(ai) > 0 && !sourceSide.has(arc.to)) {
3339
+ sourceSide.add(arc.to);
3340
+ queue.push(arc.to);
3341
+ }
3342
+ }
3343
+ }
3344
+ const cutEdgeIds = /* @__PURE__ */ new Set();
3345
+ for (let ai = 0; ai < arcs.length; ai++) {
3346
+ const arc = arcs[ai];
3347
+ if (arc.edgeId === void 0) continue;
3348
+ const arcFrom = arcs[ai ^ 1].to;
3349
+ if (sourceSide.has(arcFrom) && !sourceSide.has(arc.to)) cutEdgeIds.add(arc.edgeId);
3350
+ }
3351
+ const cutEdges = graph.edges.filter((edge) => cutEdgeIds.has(edge.id));
3352
+ return {
3353
+ value,
3354
+ flows,
3355
+ cutEdges
3356
+ };
3357
+ }
3358
+
3359
+ //#endregion
3360
+ //#region src/algorithms/dominators.ts
3361
+ /**
3362
+ * Returns the dominator tree of the graph rooted at `from`, computed with
3363
+ * the Cooper–Harvey–Kennedy iterative algorithm.
3364
+ *
3365
+ * Each reachable node maps to its immediate dominator's id; the root maps
3366
+ * to `null`. Unreachable nodes are omitted. Traversal is mode-aware:
3367
+ * undirected/bidirectional edges are traversable both ways.
3368
+ *
3369
+ * For statecharts this answers "which states must every path from the
3370
+ * initial state pass through to reach this state?" — node `d` dominates
3371
+ * node `n` when every path from the initial state to `n` goes through `d`.
3372
+ *
3373
+ * @example
3374
+ * ```ts
3375
+ * // a→b, a→c, b→d, c→d (diamond)
3376
+ * getDominatorTree(graph, { from: 'a' });
3377
+ * // { a: null, b: 'a', c: 'a', d: 'a' }
3378
+ * ```
3379
+ */
3380
+ function getDominatorTree(graph, options) {
3381
+ const root = resolveFrom(graph, options);
3382
+ if (!getIndex(graph).nodeById.has(root)) throw new Error(`getDominatorTree: root node "${root}" not found in graph — pass an existing node id as options.from`);
3383
+ const postorder = [];
3384
+ const visited = new Set([root]);
3385
+ const stack = [{
3386
+ id: root,
3387
+ neighborIndex: 0
3388
+ }];
3389
+ const successors = /* @__PURE__ */ new Map();
3390
+ function getSuccessors(id) {
3391
+ let succ = successors.get(id);
3392
+ if (!succ) {
3393
+ succ = getNeighborEdges(graph, id).map((entry) => entry.neighborId);
3394
+ successors.set(id, succ);
3395
+ }
3396
+ return succ;
3397
+ }
3398
+ while (stack.length > 0) {
3399
+ const frame = stack[stack.length - 1];
3400
+ const succ = getSuccessors(frame.id);
3401
+ if (frame.neighborIndex < succ.length) {
3402
+ const next = succ[frame.neighborIndex++];
3403
+ if (!visited.has(next)) {
3404
+ visited.add(next);
3405
+ stack.push({
3406
+ id: next,
3407
+ neighborIndex: 0
3408
+ });
3409
+ }
3410
+ } else {
3411
+ postorder.push(frame.id);
3412
+ stack.pop();
3413
+ }
3414
+ }
3415
+ const rpo = [...postorder].reverse();
3416
+ const rpoNumber = new Map(rpo.map((id, i) => [id, i]));
3417
+ const predecessors = new Map(rpo.map((id) => [id, []]));
3418
+ for (const id of rpo) for (const succ of getSuccessors(id)) if (visited.has(succ)) predecessors.get(succ).push(id);
3419
+ const idom = /* @__PURE__ */ new Map();
3420
+ idom.set(root, root);
3421
+ function intersect(a, b) {
3422
+ let f1 = a;
3423
+ let f2 = b;
3424
+ while (f1 !== f2) {
3425
+ while (rpoNumber.get(f1) > rpoNumber.get(f2)) f1 = idom.get(f1);
3426
+ while (rpoNumber.get(f2) > rpoNumber.get(f1)) f2 = idom.get(f2);
3427
+ }
3428
+ return f1;
3429
+ }
3430
+ let changed = true;
3431
+ while (changed) {
3432
+ changed = false;
3433
+ for (const id of rpo) {
3434
+ if (id === root) continue;
3435
+ let newIdom;
3436
+ for (const pred of predecessors.get(id)) {
3437
+ if (!idom.has(pred)) continue;
3438
+ newIdom = newIdom === void 0 ? pred : intersect(pred, newIdom);
3439
+ }
3440
+ if (newIdom !== void 0 && idom.get(id) !== newIdom) {
3441
+ idom.set(id, newIdom);
3442
+ changed = true;
3443
+ }
3444
+ }
3445
+ }
3446
+ const result = {};
3447
+ for (const id of rpo) result[id] = id === root ? null : idom.get(id);
3448
+ return result;
3449
+ }
3450
+
3451
+ //#endregion
3452
+ //#region src/algorithms/reduction.ts
3453
+ /**
3454
+ * Returns a new graph with all transitively-redundant edges removed (the
3455
+ * transitive reduction). The input graph is not mutated; nodes and surviving
3456
+ * edges keep all of their fields.
3457
+ *
3458
+ * An edge u→v is removed when v is also reachable from u via a path of
3459
+ * length ≥ 2. Exact-duplicate parallel edges u→v collapse to the first one
3460
+ * in `graph.edges` order (a duplicate adds no reachability, so at most one
3461
+ * edge per (u, v) pair survives).
3462
+ *
3463
+ * DAG-only: throws when the graph contains a cycle or any edge whose
3464
+ * effective mode is not `'directed'`.
3465
+ *
3466
+ * @example
3467
+ * ```ts
3468
+ * // a→b, b→c, a→c
3469
+ * const reduced = getTransitiveReduction(graph);
3470
+ * // a→c removed; edges: a→b, b→c
3471
+ * ```
3472
+ */
3473
+ function getTransitiveReduction(graph) {
3474
+ for (const edge of graph.edges) {
3475
+ const mode = getEdgeMode(graph, edge);
3476
+ if (mode !== "directed") throw new Error(`getTransitiveReduction: edge "${edge.id}" has effective mode "${mode}" — transitive reduction is only defined for directed acyclic graphs. Set edge.mode (or graph.mode) to 'directed'.`);
3477
+ }
3478
+ if (getEffectiveModeKind(graph) === "directed" && !isAcyclic(graph)) throw new Error("getTransitiveReduction: the graph contains a cycle — transitive reduction is only defined for directed acyclic graphs. Remove the cycle (see getCycles) first.");
3479
+ const successorsOf = /* @__PURE__ */ new Map();
3480
+ for (const node of graph.nodes) successorsOf.set(node.id, /* @__PURE__ */ new Set());
3481
+ for (const edge of graph.edges) successorsOf.get(edge.sourceId)?.add(edge.targetId);
3482
+ const reach = /* @__PURE__ */ new Map();
3483
+ function getReach(id) {
3484
+ let set = reach.get(id);
3485
+ if (set) return set;
3486
+ set = new Set([id]);
3487
+ reach.set(id, set);
3488
+ for (const succ of successorsOf.get(id) ?? []) for (const reached of getReach(succ)) set.add(reached);
3489
+ return set;
3490
+ }
3491
+ function isRedundant(u, v) {
3492
+ for (const w of successorsOf.get(u)) if (w !== v && getReach(w).has(v)) return true;
3493
+ return false;
3494
+ }
3495
+ const keptPairs = /* @__PURE__ */ new Set();
3496
+ const keptEdges = [];
3497
+ for (const edge of graph.edges) {
3498
+ const pair = `${edge.sourceId}${edge.targetId}`;
3499
+ if (keptPairs.has(pair)) continue;
3500
+ if (isRedundant(edge.sourceId, edge.targetId)) continue;
3501
+ keptPairs.add(pair);
3502
+ keptEdges.push(edge);
3503
+ }
3504
+ return createGraph({
3505
+ id: graph.id,
3506
+ mode: graph.mode,
3507
+ initialNodeId: graph.initialNodeId ?? void 0,
3508
+ data: graph.data ?? void 0,
3509
+ direction: graph.direction,
3510
+ style: graph.style,
3511
+ nodes: graph.nodes.map((node) => {
3512
+ const { type, parentId, initialNodeId, ...rest } = node;
3513
+ return {
3514
+ ...rest,
3515
+ parentId: parentId ?? void 0,
3516
+ initialNodeId: initialNodeId ?? void 0
3517
+ };
3518
+ }),
3519
+ edges: keptEdges.map((edge) => {
3520
+ const { type, ...rest } = edge;
3521
+ return rest;
3522
+ })
3523
+ });
3524
+ }
3525
+
3526
+ //#endregion
3527
+ export { joinPaths as $, dfs as A, toEdgeConfig as B, genPostorders as C, getPreorder as D, getPostorders as E, isConnected as F, getAStarPath as G, genCycles as H, isTree as I, getShortestPath as J, getAllPairsShortestPaths as K, flatten as L, getTopologicalSort as M, hasPath as N, getPreorders as O, isAcyclic as P, getStronglyConnectedComponents as Q, getSubgraph as R, getMinimumSpanningTree as S, getPostorder as T, genShortestPaths as U, toNodeConfig as V, genSimplePaths as W, getSimplePath as X, getShortestPaths as Y, getSimplePaths as Z, getEigenvectorCentrality as _, updateEdge as _t, isIsomorphic as a, createGraphEdge as at, getOutDegreeCentrality as b, getBridges as c, createGraphPort as ct, getGreedyModularityCommunities as d, deleteEntities as dt, GraphInstance as et, getLabelPropagationCommunities as f, deleteNode as ft, getDegreeCentrality as g, hasNode as gt, getClosenessCentrality as h, hasEdge as ht, getLouvainCommunities as i, createGraph as it, getConnectedComponents as j, bfs as k, genGirvanNewmanCommunities as l, createVisualGraph as lt, getBetweennessCentrality as m, getNode as mt, getDominatorTree as n, addEntities as nt, getArticulationPoints as o, createGraphFromTransition as ot, getModularity as p, getEdge as pt, getCycles as q, getMaxFlow as r, addNode as rt, getBiconnectedComponents as s, createGraphNode as st, getTransitiveReduction as t, addEdge as tt, getGirvanNewmanCommunities as u, deleteEdge as ut, getHITS as v, updateEntities as vt, genPreorders as w, getPageRank as x, getInDegreeCentrality as y, updateNode as yt, reverseGraph as z };