graphwise 1.7.0 → 1.8.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 (76) hide show
  1. package/README.md +81 -30
  2. package/dist/async/index.cjs +243 -0
  3. package/dist/async/index.cjs.map +1 -0
  4. package/dist/async/index.js +230 -0
  5. package/dist/async/index.js.map +1 -0
  6. package/dist/expansion/dfs-priority.d.ts +11 -0
  7. package/dist/expansion/dfs-priority.d.ts.map +1 -1
  8. package/dist/expansion/dome.d.ts +20 -0
  9. package/dist/expansion/dome.d.ts.map +1 -1
  10. package/dist/expansion/edge.d.ts +18 -0
  11. package/dist/expansion/edge.d.ts.map +1 -1
  12. package/dist/expansion/flux.d.ts +16 -0
  13. package/dist/expansion/flux.d.ts.map +1 -1
  14. package/dist/expansion/frontier-balanced.d.ts +11 -0
  15. package/dist/expansion/frontier-balanced.d.ts.map +1 -1
  16. package/dist/expansion/fuse.d.ts +16 -0
  17. package/dist/expansion/fuse.d.ts.map +1 -1
  18. package/dist/expansion/hae.d.ts +16 -0
  19. package/dist/expansion/hae.d.ts.map +1 -1
  20. package/dist/expansion/lace.d.ts +16 -0
  21. package/dist/expansion/lace.d.ts.map +1 -1
  22. package/dist/expansion/maze.d.ts +17 -0
  23. package/dist/expansion/maze.d.ts.map +1 -1
  24. package/dist/expansion/pipe.d.ts +16 -0
  25. package/dist/expansion/pipe.d.ts.map +1 -1
  26. package/dist/expansion/random-priority.d.ts +18 -0
  27. package/dist/expansion/random-priority.d.ts.map +1 -1
  28. package/dist/expansion/reach.d.ts +17 -0
  29. package/dist/expansion/reach.d.ts.map +1 -1
  30. package/dist/expansion/sage.d.ts +15 -0
  31. package/dist/expansion/sage.d.ts.map +1 -1
  32. package/dist/expansion/sift.d.ts +16 -0
  33. package/dist/expansion/sift.d.ts.map +1 -1
  34. package/dist/expansion/standard-bfs.d.ts +11 -0
  35. package/dist/expansion/standard-bfs.d.ts.map +1 -1
  36. package/dist/expansion/tide.d.ts +16 -0
  37. package/dist/expansion/tide.d.ts.map +1 -1
  38. package/dist/expansion/warp.d.ts +16 -0
  39. package/dist/expansion/warp.d.ts.map +1 -1
  40. package/dist/index/index.cjs +842 -215
  41. package/dist/index/index.cjs.map +1 -1
  42. package/dist/index/index.js +793 -211
  43. package/dist/index/index.js.map +1 -1
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/ranking/mi/adamic-adar.d.ts +8 -0
  47. package/dist/ranking/mi/adamic-adar.d.ts.map +1 -1
  48. package/dist/ranking/mi/adaptive.d.ts +8 -0
  49. package/dist/ranking/mi/adaptive.d.ts.map +1 -1
  50. package/dist/ranking/mi/cosine.d.ts +7 -0
  51. package/dist/ranking/mi/cosine.d.ts.map +1 -1
  52. package/dist/ranking/mi/etch.d.ts +8 -0
  53. package/dist/ranking/mi/etch.d.ts.map +1 -1
  54. package/dist/ranking/mi/hub-promoted.d.ts +7 -0
  55. package/dist/ranking/mi/hub-promoted.d.ts.map +1 -1
  56. package/dist/ranking/mi/jaccard.d.ts +7 -0
  57. package/dist/ranking/mi/jaccard.d.ts.map +1 -1
  58. package/dist/ranking/mi/notch.d.ts +8 -0
  59. package/dist/ranking/mi/notch.d.ts.map +1 -1
  60. package/dist/ranking/mi/overlap-coefficient.d.ts +7 -0
  61. package/dist/ranking/mi/overlap-coefficient.d.ts.map +1 -1
  62. package/dist/ranking/mi/resource-allocation.d.ts +8 -0
  63. package/dist/ranking/mi/resource-allocation.d.ts.map +1 -1
  64. package/dist/ranking/mi/scale.d.ts +7 -0
  65. package/dist/ranking/mi/scale.d.ts.map +1 -1
  66. package/dist/ranking/mi/skew.d.ts +7 -0
  67. package/dist/ranking/mi/skew.d.ts.map +1 -1
  68. package/dist/ranking/mi/sorensen.d.ts +7 -0
  69. package/dist/ranking/mi/sorensen.d.ts.map +1 -1
  70. package/dist/ranking/mi/span.d.ts +8 -0
  71. package/dist/ranking/mi/span.d.ts.map +1 -1
  72. package/dist/ranking/mi/types.d.ts +12 -0
  73. package/dist/ranking/mi/types.d.ts.map +1 -1
  74. package/dist/ranking/parse.d.ts +24 -1
  75. package/dist/ranking/parse.d.ts.map +1 -1
  76. package/package.json +6 -1
@@ -5,199 +5,7 @@ import { n as miniBatchKMeans, r as normaliseFeatures, t as _computeMean } from
5
5
  import { approximateClusteringCoefficient, batchClusteringCoefficients, computeJaccard, countEdgesOfType, countNodesOfType, entropyFromCounts, localClusteringCoefficient, localTypeEntropy, neighbourIntersection, neighbourOverlap, neighbourSet, normalisedEntropy, shannonEntropy } from "../utils/index.js";
6
6
  import { grasp, stratified } from "../seeds/index.js";
7
7
  import { GPUContext, GPUNotAvailableError, assertWebGPUAvailable, createGPUContext, createResultBuffer, csrToGPUBuffers, detectWebGPU, getGPUContext, graphToCSR, isWebGPUAvailable, readBufferToCPU } from "../gpu/index.js";
8
- //#region src/async/utils.ts
9
- /**
10
- * Async utility functions.
11
- *
12
- * @module async/utils
13
- */
14
- /** Collect an AsyncIterable into a readonly array. */
15
- async function collectAsyncIterable(iter) {
16
- const result = [];
17
- for await (const item of iter) result.push(item);
18
- return result;
19
- }
20
- /** Default yield strategy: setTimeout(0) to yield to the event loop. */
21
- function defaultYieldStrategy() {
22
- return new Promise((r) => {
23
- setTimeout(r, 0);
24
- });
25
- }
26
- //#endregion
27
- //#region src/async/runners.ts
28
- /**
29
- * Resolve a single GraphOp against a synchronous ReadableGraph.
30
- *
31
- * Returns a tagged GraphOpResponse so the receiving generator can narrow
32
- * the result type without type assertions.
33
- *
34
- * @param graph - The synchronous graph to query
35
- * @param op - The operation to resolve
36
- * @returns The tagged response
37
- */
38
- function resolveSyncOp(graph, op) {
39
- switch (op.tag) {
40
- case "neighbours": return {
41
- tag: "neighbours",
42
- value: Array.from(graph.neighbours(op.id, op.direction))
43
- };
44
- case "degree": return {
45
- tag: "degree",
46
- value: graph.degree(op.id, op.direction)
47
- };
48
- case "getNode": return {
49
- tag: "getNode",
50
- value: graph.getNode(op.id)
51
- };
52
- case "getEdge": return {
53
- tag: "getEdge",
54
- value: graph.getEdge(op.source, op.target)
55
- };
56
- case "hasNode": return {
57
- tag: "hasNode",
58
- value: graph.hasNode(op.id)
59
- };
60
- case "yield": return { tag: "yield" };
61
- case "progress": return { tag: "progress" };
62
- }
63
- }
64
- /**
65
- * Drive a generator to completion using a synchronous graph.
66
- *
67
- * The generator yields GraphOp requests; each is resolved immediately
68
- * against the graph and the tagged response is fed back via gen.next().
69
- *
70
- * @param gen - The generator to drive
71
- * @param graph - The graph to resolve ops against
72
- * @returns The generator's return value
73
- */
74
- function runSync(gen, graph) {
75
- let step = gen.next();
76
- while (step.done !== true) {
77
- const response = resolveSyncOp(graph, step.value);
78
- step = gen.next(response);
79
- }
80
- return step.value;
81
- }
82
- /**
83
- * Resolve a single GraphOp against an async ReadableGraph.
84
- *
85
- * AsyncIterables (neighbours) are collected into readonly arrays so the
86
- * generator receives the same value type as in sync mode. Returns a tagged
87
- * GraphOpResponse for type-safe narrowing without assertions.
88
- *
89
- * @param graph - The async graph to query
90
- * @param op - The operation to resolve
91
- * @returns A promise resolving to the tagged response
92
- */
93
- async function resolveAsyncOp(graph, op) {
94
- switch (op.tag) {
95
- case "neighbours": return {
96
- tag: "neighbours",
97
- value: await collectAsyncIterable(graph.neighbours(op.id, op.direction))
98
- };
99
- case "degree": return {
100
- tag: "degree",
101
- value: await graph.degree(op.id, op.direction)
102
- };
103
- case "getNode": return {
104
- tag: "getNode",
105
- value: await graph.getNode(op.id)
106
- };
107
- case "getEdge": return {
108
- tag: "getEdge",
109
- value: await graph.getEdge(op.source, op.target)
110
- };
111
- case "hasNode": return {
112
- tag: "hasNode",
113
- value: await graph.hasNode(op.id)
114
- };
115
- case "yield": return { tag: "yield" };
116
- case "progress": return { tag: "progress" };
117
- }
118
- }
119
- /**
120
- * Drive a generator to completion using an async graph.
121
- *
122
- * Extends sync semantics with:
123
- * - Cancellation via AbortSignal (throws DOMException "AbortError")
124
- * - Cooperative yielding at `yield` ops (calls yieldStrategy)
125
- * - Progress callbacks at `progress` ops (may be async for backpressure)
126
- * - Error propagation: graph errors are forwarded via gen.throw(); if the
127
- * generator does not handle them, they propagate to the caller
128
- *
129
- * @param gen - The generator to drive
130
- * @param graph - The async graph to resolve ops against
131
- * @param options - Runner configuration
132
- * @returns A promise resolving to the generator's return value
133
- */
134
- async function runAsync(gen, graph, options) {
135
- const signal = options?.signal;
136
- const onProgress = options?.onProgress;
137
- const yieldStrategy = options?.yieldStrategy ?? defaultYieldStrategy;
138
- let step = gen.next();
139
- while (step.done !== true) {
140
- if (signal?.aborted === true) {
141
- const abortError = new DOMException("Aborted", "AbortError");
142
- try {
143
- gen.throw(abortError);
144
- } catch {
145
- throw abortError;
146
- }
147
- throw abortError;
148
- }
149
- const op = step.value;
150
- if (op.tag === "yield") {
151
- await yieldStrategy();
152
- step = gen.next({ tag: "yield" });
153
- continue;
154
- }
155
- if (op.tag === "progress") {
156
- if (onProgress !== void 0) {
157
- const maybePromise = onProgress(op.stats);
158
- if (maybePromise instanceof Promise) await maybePromise;
159
- }
160
- step = gen.next({ tag: "progress" });
161
- continue;
162
- }
163
- let response;
164
- try {
165
- response = await resolveAsyncOp(graph, op);
166
- } catch (error) {
167
- step = gen.throw(error);
168
- continue;
169
- }
170
- step = gen.next(response);
171
- }
172
- return step.value;
173
- }
174
- //#endregion
175
- //#region src/async/ops.ts
176
- function* opNeighbours(id, direction) {
177
- const response = yield direction !== void 0 ? {
178
- tag: "neighbours",
179
- id,
180
- direction
181
- } : {
182
- tag: "neighbours",
183
- id
184
- };
185
- if (response.tag !== "neighbours") throw new TypeError(`Expected neighbours response, got ${response.tag}`);
186
- return response.value;
187
- }
188
- function* opDegree(id, direction) {
189
- const response = yield direction !== void 0 ? {
190
- tag: "degree",
191
- id,
192
- direction
193
- } : {
194
- tag: "degree",
195
- id
196
- };
197
- if (response.tag !== "degree") throw new TypeError(`Expected degree response, got ${response.tag}`);
198
- return response.value;
199
- }
200
- //#endregion
8
+ import { collectAsyncIterable, defaultYieldStrategy, opDegree, opGetEdge, opGetNode, opHasNode, opNeighbours, opProgress, opYield, resolveAsyncOp, resolveSyncOp, runAsync, runSync } from "../async/index.js";
201
9
  //#region src/expansion/base-helpers.ts
202
10
  /**
203
11
  * Check whether expansion should continue given current progress.
@@ -558,6 +366,18 @@ async function baseAsync(graph, seeds, config) {
558
366
  //#endregion
559
367
  //#region src/expansion/dome.ts
560
368
  /**
369
+ * DOME priority: lower degree is expanded first.
370
+ */
371
+ function domePriority(_nodeId, context) {
372
+ return context.degree;
373
+ }
374
+ /**
375
+ * DOME high-degree priority: negate degree to prioritise high-degree nodes.
376
+ */
377
+ function domeHighDegreePriority(_nodeId, context) {
378
+ return -context.degree;
379
+ }
380
+ /**
561
381
  * Run DOME expansion (degree-ordered).
562
382
  *
563
383
  * @param graph - Source graph
@@ -566,24 +386,46 @@ async function baseAsync(graph, seeds, config) {
566
386
  * @returns Expansion result with discovered paths
567
387
  */
568
388
  function dome(graph, seeds, config) {
569
- const domePriority = (nodeId, context) => {
570
- return context.degree;
571
- };
572
389
  return base(graph, seeds, {
573
390
  ...config,
574
391
  priority: domePriority
575
392
  });
576
393
  }
577
394
  /**
395
+ * Run DOME expansion asynchronously (degree-ordered).
396
+ *
397
+ * @param graph - Async source graph
398
+ * @param seeds - Seed nodes for expansion
399
+ * @param config - Expansion and async runner configuration
400
+ * @returns Promise resolving to the expansion result
401
+ */
402
+ async function domeAsync(graph, seeds, config) {
403
+ return baseAsync(graph, seeds, {
404
+ ...config,
405
+ priority: domePriority
406
+ });
407
+ }
408
+ /**
578
409
  * DOME with reverse priority (high degree first).
579
410
  */
580
411
  function domeHighDegree(graph, seeds, config) {
581
- const domePriority = (nodeId, context) => {
582
- return -context.degree;
583
- };
584
412
  return base(graph, seeds, {
585
413
  ...config,
586
- priority: domePriority
414
+ priority: domeHighDegreePriority
415
+ });
416
+ }
417
+ /**
418
+ * Run DOME high-degree expansion asynchronously (high degree first).
419
+ *
420
+ * @param graph - Async source graph
421
+ * @param seeds - Seed nodes for expansion
422
+ * @param config - Expansion and async runner configuration
423
+ * @returns Promise resolving to the expansion result
424
+ */
425
+ async function domeHighDegreeAsync(graph, seeds, config) {
426
+ return baseAsync(graph, seeds, {
427
+ ...config,
428
+ priority: domeHighDegreePriority
587
429
  });
588
430
  }
589
431
  //#endregion
@@ -628,6 +470,26 @@ function hae(graph, seeds, config) {
628
470
  priority: createHAEPriority(typeMapper)
629
471
  });
630
472
  }
473
+ /**
474
+ * Run HAE expansion asynchronously.
475
+ *
476
+ * Note: the HAE priority function accesses `context.graph` to retrieve
477
+ * neighbour types. Full async equivalence requires PriorityContext
478
+ * refactoring (Phase 4b deferred). This export establishes the async API
479
+ * surface; use with a `wrapAsync`-wrapped sync graph for testing.
480
+ *
481
+ * @param graph - Async source graph
482
+ * @param seeds - Seed nodes for expansion
483
+ * @param config - HAE configuration combined with async runner options
484
+ * @returns Promise resolving to the expansion result
485
+ */
486
+ async function haeAsync(graph, seeds, config) {
487
+ const typeMapper = config?.typeMapper ?? defaultTypeMapper$1;
488
+ return baseAsync(graph, seeds, {
489
+ ...config,
490
+ priority: createHAEPriority(typeMapper)
491
+ });
492
+ }
631
493
  //#endregion
632
494
  //#region src/expansion/edge.ts
633
495
  /** Default type mapper: reads `node.type`, falling back to "default". */
@@ -649,6 +511,27 @@ function edge(graph, seeds, config) {
649
511
  typeMapper: defaultTypeMapper
650
512
  });
651
513
  }
514
+ /**
515
+ * Run EDGE expansion asynchronously.
516
+ *
517
+ * Delegates to `haeAsync` with the default `node.type` mapper.
518
+ *
519
+ * Note: the HAE priority function accesses `context.graph` to retrieve
520
+ * neighbour types. Full async equivalence requires PriorityContext
521
+ * refactoring (Phase 4b deferred). This export establishes the async API
522
+ * surface; use with a `wrapAsync`-wrapped sync graph for testing.
523
+ *
524
+ * @param graph - Async source graph
525
+ * @param seeds - Seed nodes for expansion
526
+ * @param config - Expansion and async runner configuration
527
+ * @returns Promise resolving to the expansion result
528
+ */
529
+ async function edgeAsync(graph, seeds, config) {
530
+ return haeAsync(graph, seeds, {
531
+ ...config,
532
+ typeMapper: defaultTypeMapper
533
+ });
534
+ }
652
535
  //#endregion
653
536
  //#region src/expansion/pipe.ts
654
537
  /**
@@ -684,6 +567,25 @@ function pipe(graph, seeds, config) {
684
567
  priority: pipePriority
685
568
  });
686
569
  }
570
+ /**
571
+ * Run PIPE expansion asynchronously.
572
+ *
573
+ * Note: the PIPE priority function accesses `context.graph` to retrieve
574
+ * neighbour lists. Full async equivalence requires PriorityContext
575
+ * refactoring (Phase 4b deferred). This export establishes the async API
576
+ * surface; use with a `wrapAsync`-wrapped sync graph for testing.
577
+ *
578
+ * @param graph - Async source graph
579
+ * @param seeds - Seed nodes for expansion
580
+ * @param config - Expansion and async runner configuration
581
+ * @returns Promise resolving to the expansion result
582
+ */
583
+ async function pipeAsync(graph, seeds, config) {
584
+ return baseAsync(graph, seeds, {
585
+ ...config,
586
+ priority: pipePriority
587
+ });
588
+ }
687
589
  //#endregion
688
590
  //#region src/expansion/priority-helpers.ts
689
591
  /**
@@ -780,6 +682,34 @@ function sage(graph, seeds, config) {
780
682
  priority: sagePriority
781
683
  });
782
684
  }
685
+ /**
686
+ * Run SAGE expansion asynchronously.
687
+ *
688
+ * Creates fresh closure state (salienceCounts, phase tracking) for this
689
+ * invocation. The SAGE priority function does not access `context.graph`
690
+ * directly, so it is safe to use in async mode via `baseAsync`.
691
+ *
692
+ * @param graph - Async source graph
693
+ * @param seeds - Seed nodes for expansion
694
+ * @param config - Expansion and async runner configuration
695
+ * @returns Promise resolving to the expansion result
696
+ */
697
+ async function sageAsync(graph, seeds, config) {
698
+ const salienceCounts = /* @__PURE__ */ new Map();
699
+ let inPhase2 = false;
700
+ let lastPathCount = 0;
701
+ function sagePriority(nodeId, context) {
702
+ const pathCount = context.discoveredPaths.length;
703
+ if (pathCount > 0 && !inPhase2) inPhase2 = true;
704
+ if (pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
705
+ if (!inPhase2) return Math.log(context.degree + 1);
706
+ return -((salienceCounts.get(nodeId) ?? 0) * 1e3 - context.degree);
707
+ }
708
+ return baseAsync(graph, seeds, {
709
+ ...config,
710
+ priority: sagePriority
711
+ });
712
+ }
783
713
  //#endregion
784
714
  //#region src/ranking/mi/jaccard.ts
785
715
  /**
@@ -797,6 +727,23 @@ function jaccard(graph, source, target, config) {
797
727
  if (sourceNeighbours.size === 0 && targetNeighbours.size === 0) return 0;
798
728
  return Math.max(epsilon, jaccardScore);
799
729
  }
730
+ /**
731
+ * Async variant of Jaccard similarity for use with async graph data sources.
732
+ *
733
+ * Fetches both neighbourhoods concurrently, then applies the same formula.
734
+ */
735
+ async function jaccardAsync(graph, source, target, config) {
736
+ const { epsilon = 1e-10 } = config ?? {};
737
+ const [sourceNeighboursArr, targetNeighboursArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
738
+ const srcSet = new Set(sourceNeighboursArr.filter((n) => n !== target));
739
+ const tgtSet = new Set(targetNeighboursArr.filter((n) => n !== source));
740
+ if (srcSet.size === 0 && tgtSet.size === 0) return 0;
741
+ let intersection = 0;
742
+ for (const n of srcSet) if (tgtSet.has(n)) intersection++;
743
+ const union = srcSet.size + tgtSet.size - intersection;
744
+ const jaccardScore = union > 0 ? intersection / union : 0;
745
+ return Math.max(epsilon, jaccardScore);
746
+ }
800
747
  //#endregion
801
748
  //#region src/expansion/reach.ts
802
749
  /**
@@ -853,6 +800,40 @@ function reach(graph, seeds, config) {
853
800
  priority: reachPriority
854
801
  });
855
802
  }
803
+ /**
804
+ * Run REACH expansion asynchronously.
805
+ *
806
+ * Creates fresh closure state (phase tracking, Jaccard cache) for this
807
+ * invocation. The REACH priority function uses `jaccard(context.graph, ...)`
808
+ * in Phase 2; in async mode `context.graph` is the sentinel and will throw.
809
+ * Full async equivalence requires PriorityContext refactoring (Phase 4b
810
+ * deferred). This export establishes the async API surface.
811
+ *
812
+ * @param graph - Async source graph
813
+ * @param seeds - Seed nodes for expansion
814
+ * @param config - Expansion and async runner configuration
815
+ * @returns Promise resolving to the expansion result
816
+ */
817
+ async function reachAsync(graph, seeds, config) {
818
+ let inPhase2 = false;
819
+ function reachPriority(nodeId, context) {
820
+ if (context.discoveredPaths.length > 0 && !inPhase2) inPhase2 = true;
821
+ if (!inPhase2) return Math.log(context.degree + 1);
822
+ let totalMI = 0;
823
+ let endpointCount = 0;
824
+ for (const path of context.discoveredPaths) {
825
+ totalMI += jaccard(context.graph, nodeId, path.fromSeed.id);
826
+ totalMI += jaccard(context.graph, nodeId, path.toSeed.id);
827
+ endpointCount += 2;
828
+ }
829
+ const miHat = endpointCount > 0 ? totalMI / endpointCount : 0;
830
+ return Math.log(context.degree + 1) * (1 - miHat);
831
+ }
832
+ return baseAsync(graph, seeds, {
833
+ ...config,
834
+ priority: reachPriority
835
+ });
836
+ }
856
837
  //#endregion
857
838
  //#region src/expansion/maze.ts
858
839
  /** Default threshold for switching to phase 2 (after M paths) */
@@ -899,6 +880,46 @@ function maze(graph, seeds, config) {
899
880
  priority: mazePriority
900
881
  });
901
882
  }
883
+ /**
884
+ * Run MAZE expansion asynchronously.
885
+ *
886
+ * Creates fresh closure state (salienceCounts, phase tracking) for this
887
+ * invocation. The MAZE priority function accesses `context.graph` to
888
+ * retrieve neighbour lists for path potential computation. Full async
889
+ * equivalence requires PriorityContext refactoring (Phase 4b deferred).
890
+ * This export establishes the async API surface.
891
+ *
892
+ * @param graph - Async source graph
893
+ * @param seeds - Seed nodes for expansion
894
+ * @param config - Expansion and async runner configuration
895
+ * @returns Promise resolving to the expansion result
896
+ */
897
+ async function mazeAsync(graph, seeds, config) {
898
+ const salienceCounts = /* @__PURE__ */ new Map();
899
+ let inPhase2 = false;
900
+ let lastPathCount = 0;
901
+ function mazePriority(nodeId, context) {
902
+ const pathCount = context.discoveredPaths.length;
903
+ if (pathCount >= DEFAULT_PHASE2_THRESHOLD && !inPhase2) {
904
+ inPhase2 = true;
905
+ updateSalienceCounts(salienceCounts, context.discoveredPaths, 0);
906
+ }
907
+ if (inPhase2 && pathCount > lastPathCount) lastPathCount = updateSalienceCounts(salienceCounts, context.discoveredPaths, lastPathCount);
908
+ const nodeNeighbours = context.graph.neighbours(nodeId);
909
+ let pathPotential = 0;
910
+ for (const neighbour of nodeNeighbours) {
911
+ const visitedBy = context.visitedByFrontier.get(neighbour);
912
+ if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
913
+ }
914
+ if (!inPhase2) return context.degree / (1 + pathPotential);
915
+ const salience = salienceCounts.get(nodeId) ?? 0;
916
+ return context.degree / (1 + pathPotential) * (1 / (1 + SALIENCE_WEIGHT * salience));
917
+ }
918
+ return baseAsync(graph, seeds, {
919
+ ...config,
920
+ priority: mazePriority
921
+ });
922
+ }
902
923
  //#endregion
903
924
  //#region src/expansion/tide.ts
904
925
  /**
@@ -930,6 +951,25 @@ function tide(graph, seeds, config) {
930
951
  priority: tidePriority
931
952
  });
932
953
  }
954
+ /**
955
+ * Run TIDE expansion asynchronously.
956
+ *
957
+ * Note: the TIDE priority function accesses `context.graph` to retrieve
958
+ * neighbour lists and per-neighbour degrees. Full async equivalence
959
+ * requires PriorityContext refactoring (Phase 4b deferred). This export
960
+ * establishes the async API surface.
961
+ *
962
+ * @param graph - Async source graph
963
+ * @param seeds - Seed nodes for expansion
964
+ * @param config - Expansion and async runner configuration
965
+ * @returns Promise resolving to the expansion result
966
+ */
967
+ async function tideAsync(graph, seeds, config) {
968
+ return baseAsync(graph, seeds, {
969
+ ...config,
970
+ priority: tidePriority
971
+ });
972
+ }
933
973
  //#endregion
934
974
  //#region src/expansion/lace.ts
935
975
  /**
@@ -960,6 +1000,27 @@ function lace(graph, seeds, config) {
960
1000
  priority
961
1001
  });
962
1002
  }
1003
+ /**
1004
+ * Run LACE expansion asynchronously.
1005
+ *
1006
+ * Note: the LACE priority function accesses `context.graph` via
1007
+ * `avgFrontierMI`. Full async equivalence requires PriorityContext
1008
+ * refactoring (Phase 4b deferred). This export establishes the async
1009
+ * API surface.
1010
+ *
1011
+ * @param graph - Async source graph
1012
+ * @param seeds - Seed nodes for expansion
1013
+ * @param config - LACE configuration combined with async runner options
1014
+ * @returns Promise resolving to the expansion result
1015
+ */
1016
+ async function laceAsync(graph, seeds, config) {
1017
+ const { mi = jaccard, ...restConfig } = config ?? {};
1018
+ const priority = (nodeId, context) => lacePriority(nodeId, context, mi);
1019
+ return baseAsync(graph, seeds, {
1020
+ ...restConfig,
1021
+ priority
1022
+ });
1023
+ }
963
1024
  //#endregion
964
1025
  //#region src/expansion/warp.ts
965
1026
  /**
@@ -992,6 +1053,25 @@ function warp(graph, seeds, config) {
992
1053
  priority: warpPriority
993
1054
  });
994
1055
  }
1056
+ /**
1057
+ * Run WARP expansion asynchronously.
1058
+ *
1059
+ * Note: the WARP priority function accesses `context.graph` via
1060
+ * `countCrossFrontierNeighbours`. Full async equivalence requires
1061
+ * PriorityContext refactoring (Phase 4b deferred). This export
1062
+ * establishes the async API surface.
1063
+ *
1064
+ * @param graph - Async source graph
1065
+ * @param seeds - Seed nodes for expansion
1066
+ * @param config - Expansion and async runner configuration
1067
+ * @returns Promise resolving to the expansion result
1068
+ */
1069
+ async function warpAsync(graph, seeds, config) {
1070
+ return baseAsync(graph, seeds, {
1071
+ ...config,
1072
+ priority: warpPriority
1073
+ });
1074
+ }
995
1075
  //#endregion
996
1076
  //#region src/expansion/fuse.ts
997
1077
  /**
@@ -1024,6 +1104,27 @@ function fuse(graph, seeds, config) {
1024
1104
  priority
1025
1105
  });
1026
1106
  }
1107
+ /**
1108
+ * Run FUSE expansion asynchronously.
1109
+ *
1110
+ * Note: the FUSE priority function accesses `context.graph` via
1111
+ * `avgFrontierMI`. Full async equivalence requires PriorityContext
1112
+ * refactoring (Phase 4b deferred). This export establishes the async
1113
+ * API surface.
1114
+ *
1115
+ * @param graph - Async source graph
1116
+ * @param seeds - Seed nodes for expansion
1117
+ * @param config - FUSE configuration combined with async runner options
1118
+ * @returns Promise resolving to the expansion result
1119
+ */
1120
+ async function fuseAsync(graph, seeds, config) {
1121
+ const { mi = jaccard, salienceWeight = .5, ...restConfig } = config ?? {};
1122
+ const priority = (nodeId, context) => fusePriority(nodeId, context, mi, salienceWeight);
1123
+ return baseAsync(graph, seeds, {
1124
+ ...restConfig,
1125
+ priority
1126
+ });
1127
+ }
1027
1128
  //#endregion
1028
1129
  //#region src/expansion/sift.ts
1029
1130
  /**
@@ -1056,6 +1157,27 @@ function sift(graph, seeds, config) {
1056
1157
  priority
1057
1158
  });
1058
1159
  }
1160
+ /**
1161
+ * Run SIFT expansion asynchronously.
1162
+ *
1163
+ * Note: the SIFT priority function accesses `context.graph` via
1164
+ * `avgFrontierMI`. Full async equivalence requires PriorityContext
1165
+ * refactoring (Phase 4b deferred). This export establishes the async
1166
+ * API surface.
1167
+ *
1168
+ * @param graph - Async source graph
1169
+ * @param seeds - Seed nodes for expansion
1170
+ * @param config - SIFT (REACHConfig) configuration combined with async runner options
1171
+ * @returns Promise resolving to the expansion result
1172
+ */
1173
+ async function siftAsync(graph, seeds, config) {
1174
+ const { mi = jaccard, miThreshold = .25, ...restConfig } = config ?? {};
1175
+ const priority = (nodeId, context) => siftPriority(nodeId, context, mi, miThreshold);
1176
+ return baseAsync(graph, seeds, {
1177
+ ...restConfig,
1178
+ priority
1179
+ });
1180
+ }
1059
1181
  //#endregion
1060
1182
  //#region src/expansion/flux.ts
1061
1183
  /**
@@ -1112,9 +1234,36 @@ function flux(graph, seeds, config) {
1112
1234
  priority
1113
1235
  });
1114
1236
  }
1237
+ /**
1238
+ * Run FLUX expansion asynchronously.
1239
+ *
1240
+ * Note: the FLUX priority function accesses `context.graph` to compute
1241
+ * local density and cross-frontier bridge scores. Full async equivalence
1242
+ * requires PriorityContext refactoring (Phase 4b deferred). This export
1243
+ * establishes the async API surface.
1244
+ *
1245
+ * @param graph - Async source graph
1246
+ * @param seeds - Seed nodes for expansion
1247
+ * @param config - FLUX (MAZEConfig) configuration combined with async runner options
1248
+ * @returns Promise resolving to the expansion result
1249
+ */
1250
+ async function fluxAsync(graph, seeds, config) {
1251
+ const { densityThreshold = .5, bridgeThreshold = .3, ...restConfig } = config ?? {};
1252
+ const priority = (nodeId, context) => fluxPriority(nodeId, context, densityThreshold, bridgeThreshold);
1253
+ return baseAsync(graph, seeds, {
1254
+ ...restConfig,
1255
+ priority
1256
+ });
1257
+ }
1115
1258
  //#endregion
1116
1259
  //#region src/expansion/standard-bfs.ts
1117
1260
  /**
1261
+ * BFS priority: discovery iteration order (FIFO).
1262
+ */
1263
+ function bfsPriority(_nodeId, context) {
1264
+ return context.iteration;
1265
+ }
1266
+ /**
1118
1267
  * Run standard BFS expansion (FIFO discovery order).
1119
1268
  *
1120
1269
  * @param graph - Source graph
@@ -1123,17 +1272,35 @@ function flux(graph, seeds, config) {
1123
1272
  * @returns Expansion result with discovered paths
1124
1273
  */
1125
1274
  function standardBfs(graph, seeds, config) {
1126
- const bfsPriority = (_nodeId, context) => {
1127
- return context.iteration;
1128
- };
1129
1275
  return base(graph, seeds, {
1130
1276
  ...config,
1131
1277
  priority: bfsPriority
1132
1278
  });
1133
1279
  }
1280
+ /**
1281
+ * Run standard BFS expansion asynchronously (FIFO discovery order).
1282
+ *
1283
+ * @param graph - Async source graph
1284
+ * @param seeds - Seed nodes for expansion
1285
+ * @param config - Expansion and async runner configuration
1286
+ * @returns Promise resolving to the expansion result
1287
+ */
1288
+ async function standardBfsAsync(graph, seeds, config) {
1289
+ return baseAsync(graph, seeds, {
1290
+ ...config,
1291
+ priority: bfsPriority
1292
+ });
1293
+ }
1134
1294
  //#endregion
1135
1295
  //#region src/expansion/frontier-balanced.ts
1136
1296
  /**
1297
+ * Frontier-balanced priority: frontier index dominates, then discovery iteration.
1298
+ * Scales frontier index by 1e9 to ensure round-robin ordering across frontiers.
1299
+ */
1300
+ function balancedPriority(_nodeId, context) {
1301
+ return context.frontierIndex * 1e9 + context.iteration;
1302
+ }
1303
+ /**
1137
1304
  * Run frontier-balanced expansion (round-robin across frontiers).
1138
1305
  *
1139
1306
  * @param graph - Source graph
@@ -1142,14 +1309,25 @@ function standardBfs(graph, seeds, config) {
1142
1309
  * @returns Expansion result with discovered paths
1143
1310
  */
1144
1311
  function frontierBalanced(graph, seeds, config) {
1145
- const balancedPriority = (_nodeId, context) => {
1146
- return context.frontierIndex * 1e9 + context.iteration;
1147
- };
1148
1312
  return base(graph, seeds, {
1149
1313
  ...config,
1150
1314
  priority: balancedPriority
1151
1315
  });
1152
1316
  }
1317
+ /**
1318
+ * Run frontier-balanced expansion asynchronously (round-robin across frontiers).
1319
+ *
1320
+ * @param graph - Async source graph
1321
+ * @param seeds - Seed nodes for expansion
1322
+ * @param config - Expansion and async runner configuration
1323
+ * @returns Promise resolving to the expansion result
1324
+ */
1325
+ async function frontierBalancedAsync(graph, seeds, config) {
1326
+ return baseAsync(graph, seeds, {
1327
+ ...config,
1328
+ priority: balancedPriority
1329
+ });
1330
+ }
1153
1331
  //#endregion
1154
1332
  //#region src/expansion/random-priority.ts
1155
1333
  /**
@@ -1169,6 +1347,12 @@ function seededRandom$1(input, seed = 0) {
1169
1347
  return (h >>> 0) / 4294967295;
1170
1348
  }
1171
1349
  /**
1350
+ * Build a seeded random priority function for a given seed value.
1351
+ */
1352
+ function makeRandomPriorityFn(seed) {
1353
+ return (nodeId) => seededRandom$1(nodeId, seed);
1354
+ }
1355
+ /**
1172
1356
  * Run random-priority expansion (null hypothesis baseline).
1173
1357
  *
1174
1358
  * @param graph - Source graph
@@ -1178,12 +1362,24 @@ function seededRandom$1(input, seed = 0) {
1178
1362
  */
1179
1363
  function randomPriority(graph, seeds, config) {
1180
1364
  const { seed = 0 } = config ?? {};
1181
- const randomPriorityFn = (nodeId, context) => {
1182
- return seededRandom$1(nodeId, seed);
1183
- };
1184
1365
  return base(graph, seeds, {
1185
1366
  ...config,
1186
- priority: randomPriorityFn
1367
+ priority: makeRandomPriorityFn(seed)
1368
+ });
1369
+ }
1370
+ /**
1371
+ * Run random-priority expansion asynchronously (null hypothesis baseline).
1372
+ *
1373
+ * @param graph - Async source graph
1374
+ * @param seeds - Seed nodes for expansion
1375
+ * @param config - Expansion and async runner configuration
1376
+ * @returns Promise resolving to the expansion result
1377
+ */
1378
+ async function randomPriorityAsync(graph, seeds, config) {
1379
+ const { seed = 0 } = config ?? {};
1380
+ return baseAsync(graph, seeds, {
1381
+ ...config,
1382
+ priority: makeRandomPriorityFn(seed)
1187
1383
  });
1188
1384
  }
1189
1385
  //#endregion
@@ -1215,6 +1411,20 @@ function dfsPriority(graph, seeds, config) {
1215
1411
  priority: dfsPriorityFn
1216
1412
  });
1217
1413
  }
1414
+ /**
1415
+ * Run DFS-priority expansion asynchronously (LIFO discovery order).
1416
+ *
1417
+ * @param graph - Async source graph
1418
+ * @param seeds - Seed nodes for expansion
1419
+ * @param config - Expansion and async runner configuration
1420
+ * @returns Promise resolving to the expansion result
1421
+ */
1422
+ async function dfsPriorityAsync(graph, seeds, config) {
1423
+ return baseAsync(graph, seeds, {
1424
+ ...config,
1425
+ priority: dfsPriorityFn
1426
+ });
1427
+ }
1218
1428
  //#endregion
1219
1429
  //#region src/expansion/k-hop.ts
1220
1430
  /**
@@ -1572,6 +1782,74 @@ function parse(graph, paths, config) {
1572
1782
  };
1573
1783
  }
1574
1784
  /**
1785
+ * Rank paths using async PARSE (Path-Aware Ranking via Salience Estimation).
1786
+ *
1787
+ * Async variant suitable for use with remote or lazy graph data sources.
1788
+ * Computes geometric mean of edge MI scores for each path using Promise.all
1789
+ * for parallelism, then sorts by salience (highest first).
1790
+ *
1791
+ * @param graph - Async source graph
1792
+ * @param paths - Paths to rank
1793
+ * @param config - Configuration options
1794
+ * @returns Ranked paths with statistics
1795
+ */
1796
+ async function parseAsync(graph, paths, config) {
1797
+ const startTime = performance.now();
1798
+ const { mi = jaccardAsync, epsilon = 1e-10 } = config ?? {};
1799
+ const rankedPaths = [];
1800
+ for (const path of paths) {
1801
+ const salience = await computePathSalienceAsync(graph, path, mi, epsilon);
1802
+ rankedPaths.push({
1803
+ ...path,
1804
+ salience
1805
+ });
1806
+ }
1807
+ rankedPaths.sort((a, b) => b.salience - a.salience);
1808
+ const endTime = performance.now();
1809
+ const saliences = rankedPaths.map((p) => p.salience);
1810
+ const meanSalience = saliences.length > 0 ? saliences.reduce((a, b) => a + b, 0) / saliences.length : 0;
1811
+ const sortedSaliences = [...saliences].sort((a, b) => a - b);
1812
+ const mid = Math.floor(sortedSaliences.length / 2);
1813
+ const medianSalience = sortedSaliences.length > 0 ? sortedSaliences.length % 2 !== 0 ? sortedSaliences[mid] ?? 0 : ((sortedSaliences[mid - 1] ?? 0) + (sortedSaliences[mid] ?? 0)) / 2 : 0;
1814
+ const maxSalience = sortedSaliences.length > 0 ? sortedSaliences[sortedSaliences.length - 1] ?? 0 : 0;
1815
+ const minSalience = sortedSaliences.length > 0 ? sortedSaliences[0] ?? 0 : 0;
1816
+ return {
1817
+ paths: rankedPaths,
1818
+ stats: {
1819
+ pathsRanked: rankedPaths.length,
1820
+ meanSalience,
1821
+ medianSalience,
1822
+ maxSalience,
1823
+ minSalience,
1824
+ durationMs: endTime - startTime
1825
+ }
1826
+ };
1827
+ }
1828
+ /**
1829
+ * Compute salience for a single path asynchronously.
1830
+ *
1831
+ * Uses geometric mean of edge MI scores for length-unbiased ranking.
1832
+ * Edge MI values are computed in parallel via Promise.all.
1833
+ */
1834
+ async function computePathSalienceAsync(graph, path, mi, epsilon) {
1835
+ const nodes = path.nodes;
1836
+ if (nodes.length < 2) return epsilon;
1837
+ const edgeMIs = await Promise.all(nodes.slice(0, -1).map((source, i) => {
1838
+ const target = nodes[i + 1];
1839
+ if (target !== void 0) return mi(graph, source, target);
1840
+ return Promise.resolve(epsilon);
1841
+ }));
1842
+ let productMi = 1;
1843
+ let edgeCount = 0;
1844
+ for (const edgeMi of edgeMIs) {
1845
+ productMi *= Math.max(epsilon, edgeMi);
1846
+ edgeCount++;
1847
+ }
1848
+ if (edgeCount === 0) return epsilon;
1849
+ const salience = Math.pow(productMi, 1 / edgeCount);
1850
+ return Math.max(epsilon, Math.min(1, salience));
1851
+ }
1852
+ /**
1575
1853
  * Compute salience for a single path.
1576
1854
  *
1577
1855
  * Uses geometric mean of edge MI scores for length-unbiased ranking.
@@ -1619,6 +1897,29 @@ function adamicAdar(graph, source, target, config) {
1619
1897
  }
1620
1898
  return Math.max(epsilon, score);
1621
1899
  }
1900
+ /**
1901
+ * Async variant of Adamic-Adar index for use with async graph data sources.
1902
+ *
1903
+ * Fetches both neighbourhoods concurrently, then fetches degree for each common
1904
+ * neighbour to compute the inverse-log-degree weighted sum.
1905
+ */
1906
+ async function adamicAdarAsync(graph, source, target, config) {
1907
+ const { epsilon = 1e-10, normalise = true } = config ?? {};
1908
+ const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
1909
+ const srcSet = new Set(sourceArr.filter((n) => n !== target));
1910
+ const tgtSet = new Set(targetArr.filter((n) => n !== source));
1911
+ const commonNeighbours = [];
1912
+ for (const n of srcSet) if (tgtSet.has(n)) commonNeighbours.push(n);
1913
+ if (commonNeighbours.length === 0) return epsilon;
1914
+ const degrees = await Promise.all(commonNeighbours.map((n) => graph.degree(n)));
1915
+ let score = 0;
1916
+ for (const degree of degrees) score += 1 / Math.log(degree + 1);
1917
+ if (normalise) {
1918
+ const maxScore = commonNeighbours.length / Math.log(2);
1919
+ score = score / maxScore;
1920
+ }
1921
+ return Math.max(epsilon, score);
1922
+ }
1622
1923
  //#endregion
1623
1924
  //#region src/ranking/mi/cosine.ts
1624
1925
  /**
@@ -1640,6 +1941,23 @@ function cosine(graph, source, target, config) {
1640
1941
  const score = intersection / denominator;
1641
1942
  return Math.max(epsilon, score);
1642
1943
  }
1944
+ /**
1945
+ * Async variant of cosine similarity for use with async graph data sources.
1946
+ *
1947
+ * Fetches both neighbourhoods concurrently, then applies the same formula.
1948
+ */
1949
+ async function cosineAsync(graph, source, target, config) {
1950
+ const { epsilon = 1e-10 } = config ?? {};
1951
+ const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
1952
+ const srcSet = new Set(sourceArr.filter((n) => n !== target));
1953
+ const tgtSet = new Set(targetArr.filter((n) => n !== source));
1954
+ let intersection = 0;
1955
+ for (const n of srcSet) if (tgtSet.has(n)) intersection++;
1956
+ const denominator = Math.sqrt(srcSet.size) * Math.sqrt(tgtSet.size);
1957
+ if (denominator === 0) return 0;
1958
+ const score = intersection / denominator;
1959
+ return Math.max(epsilon, score);
1960
+ }
1643
1961
  //#endregion
1644
1962
  //#region src/ranking/mi/sorensen.ts
1645
1963
  /**
@@ -1661,6 +1979,23 @@ function sorensen(graph, source, target, config) {
1661
1979
  const score = 2 * intersection / denominator;
1662
1980
  return Math.max(epsilon, score);
1663
1981
  }
1982
+ /**
1983
+ * Async variant of Sorensen-Dice coefficient for use with async graph data sources.
1984
+ *
1985
+ * Fetches both neighbourhoods concurrently, then applies the same formula.
1986
+ */
1987
+ async function sorensenAsync(graph, source, target, config) {
1988
+ const { epsilon = 1e-10 } = config ?? {};
1989
+ const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
1990
+ const srcSet = new Set(sourceArr.filter((n) => n !== target));
1991
+ const tgtSet = new Set(targetArr.filter((n) => n !== source));
1992
+ let intersection = 0;
1993
+ for (const n of srcSet) if (tgtSet.has(n)) intersection++;
1994
+ const denominator = srcSet.size + tgtSet.size;
1995
+ if (denominator === 0) return 0;
1996
+ const score = 2 * intersection / denominator;
1997
+ return Math.max(epsilon, score);
1998
+ }
1664
1999
  //#endregion
1665
2000
  //#region src/ranking/mi/resource-allocation.ts
1666
2001
  /**
@@ -1686,6 +2021,29 @@ function resourceAllocation(graph, source, target, config) {
1686
2021
  }
1687
2022
  return Math.max(epsilon, score);
1688
2023
  }
2024
+ /**
2025
+ * Async variant of Resource Allocation index for use with async graph data sources.
2026
+ *
2027
+ * Fetches both neighbourhoods concurrently, then fetches degree for each common
2028
+ * neighbour to compute the inverse-degree weighted sum.
2029
+ */
2030
+ async function resourceAllocationAsync(graph, source, target, config) {
2031
+ const { epsilon = 1e-10, normalise = true } = config ?? {};
2032
+ const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
2033
+ const srcSet = new Set(sourceArr.filter((n) => n !== target));
2034
+ const tgtSet = new Set(targetArr.filter((n) => n !== source));
2035
+ const commonNeighbours = [];
2036
+ for (const n of srcSet) if (tgtSet.has(n)) commonNeighbours.push(n);
2037
+ if (commonNeighbours.length === 0) return epsilon;
2038
+ const degrees = await Promise.all(commonNeighbours.map((n) => graph.degree(n)));
2039
+ let score = 0;
2040
+ for (const degree of degrees) if (degree > 0) score += 1 / degree;
2041
+ if (normalise) {
2042
+ const maxScore = commonNeighbours.length;
2043
+ score = score / maxScore;
2044
+ }
2045
+ return Math.max(epsilon, score);
2046
+ }
1689
2047
  //#endregion
1690
2048
  //#region src/ranking/mi/overlap-coefficient.ts
1691
2049
  /**
@@ -1707,6 +2065,23 @@ function overlapCoefficient(graph, source, target, config) {
1707
2065
  const score = intersection / denominator;
1708
2066
  return Math.max(epsilon, score);
1709
2067
  }
2068
+ /**
2069
+ * Async variant of Overlap Coefficient for use with async graph data sources.
2070
+ *
2071
+ * Fetches both neighbourhoods concurrently, then applies the same formula.
2072
+ */
2073
+ async function overlapCoefficientAsync(graph, source, target, config) {
2074
+ const { epsilon = 1e-10 } = config ?? {};
2075
+ const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
2076
+ const srcSet = new Set(sourceArr.filter((n) => n !== target));
2077
+ const tgtSet = new Set(targetArr.filter((n) => n !== source));
2078
+ let intersection = 0;
2079
+ for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2080
+ const denominator = Math.min(srcSet.size, tgtSet.size);
2081
+ if (denominator === 0) return 0;
2082
+ const score = intersection / denominator;
2083
+ return Math.max(epsilon, score);
2084
+ }
1710
2085
  //#endregion
1711
2086
  //#region src/ranking/mi/hub-promoted.ts
1712
2087
  /**
@@ -1728,6 +2103,28 @@ function hubPromoted(graph, source, target, config) {
1728
2103
  const score = intersection / denominator;
1729
2104
  return Math.max(epsilon, score);
1730
2105
  }
2106
+ /**
2107
+ * Async variant of Hub Promoted index for use with async graph data sources.
2108
+ *
2109
+ * Fetches both neighbourhoods and degrees concurrently, then applies the same formula.
2110
+ */
2111
+ async function hubPromotedAsync(graph, source, target, config) {
2112
+ const { epsilon = 1e-10 } = config ?? {};
2113
+ const [sourceArr, targetArr, sourceDegree, targetDegree] = await Promise.all([
2114
+ collectAsyncIterable(graph.neighbours(source)),
2115
+ collectAsyncIterable(graph.neighbours(target)),
2116
+ graph.degree(source),
2117
+ graph.degree(target)
2118
+ ]);
2119
+ const srcSet = new Set(sourceArr.filter((n) => n !== target));
2120
+ const tgtSet = new Set(targetArr.filter((n) => n !== source));
2121
+ let intersection = 0;
2122
+ for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2123
+ const denominator = Math.min(sourceDegree, targetDegree);
2124
+ if (denominator === 0) return 0;
2125
+ const score = intersection / denominator;
2126
+ return Math.max(epsilon, score);
2127
+ }
1731
2128
  //#endregion
1732
2129
  //#region src/ranking/mi/scale.ts
1733
2130
  /**
@@ -1744,6 +2141,31 @@ function scale(graph, source, target, config) {
1744
2141
  const score = jaccardScore / density;
1745
2142
  return Math.max(epsilon, score);
1746
2143
  }
2144
+ /**
2145
+ * Async variant of SCALE MI for use with async graph data sources.
2146
+ *
2147
+ * Fetches both neighbourhoods, node count, and edge count concurrently.
2148
+ */
2149
+ async function scaleAsync(graph, source, target, config) {
2150
+ const { epsilon = 1e-10 } = config ?? {};
2151
+ const [sourceArr, targetArr, n, m] = await Promise.all([
2152
+ collectAsyncIterable(graph.neighbours(source)),
2153
+ collectAsyncIterable(graph.neighbours(target)),
2154
+ graph.nodeCount,
2155
+ graph.edgeCount
2156
+ ]);
2157
+ const srcSet = new Set(sourceArr.filter((node) => node !== target));
2158
+ const tgtSet = new Set(targetArr.filter((node) => node !== source));
2159
+ let intersection = 0;
2160
+ for (const node of srcSet) if (tgtSet.has(node)) intersection++;
2161
+ const union = srcSet.size + tgtSet.size - intersection;
2162
+ const jaccardScore = union > 0 ? intersection / union : 0;
2163
+ const possibleEdges = n * (n - 1);
2164
+ const density = possibleEdges > 0 ? (graph.directed ? m : 2 * m) / possibleEdges : 0;
2165
+ if (density === 0) return epsilon;
2166
+ const score = jaccardScore / density;
2167
+ return Math.max(epsilon, score);
2168
+ }
1747
2169
  //#endregion
1748
2170
  //#region src/ranking/mi/skew.ts
1749
2171
  /**
@@ -1760,6 +2182,31 @@ function skew(graph, source, target, config) {
1760
2182
  const score = jaccardScore * sourceIdf * targetIdf;
1761
2183
  return Math.max(epsilon, score);
1762
2184
  }
2185
+ /**
2186
+ * Async variant of SKEW MI for use with async graph data sources.
2187
+ *
2188
+ * Fetches both neighbourhoods, degrees, and node count concurrently.
2189
+ */
2190
+ async function skewAsync(graph, source, target, config) {
2191
+ const { epsilon = 1e-10 } = config ?? {};
2192
+ const [sourceArr, targetArr, N, sourceDegree, targetDegree] = await Promise.all([
2193
+ collectAsyncIterable(graph.neighbours(source)),
2194
+ collectAsyncIterable(graph.neighbours(target)),
2195
+ graph.nodeCount,
2196
+ graph.degree(source),
2197
+ graph.degree(target)
2198
+ ]);
2199
+ const srcSet = new Set(sourceArr.filter((n) => n !== target));
2200
+ const tgtSet = new Set(targetArr.filter((n) => n !== source));
2201
+ let intersection = 0;
2202
+ for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2203
+ const union = srcSet.size + tgtSet.size - intersection;
2204
+ const jaccardScore = union > 0 ? intersection / union : 0;
2205
+ const sourceIdf = Math.log(N / (sourceDegree + 1));
2206
+ const targetIdf = Math.log(N / (targetDegree + 1));
2207
+ const score = jaccardScore * sourceIdf * targetIdf;
2208
+ return Math.max(epsilon, score);
2209
+ }
1763
2210
  //#endregion
1764
2211
  //#region src/ranking/mi/span.ts
1765
2212
  /**
@@ -1773,6 +2220,40 @@ function span(graph, source, target, config) {
1773
2220
  const score = jaccardScore * (1 - Math.max(sourceCc, targetCc));
1774
2221
  return Math.max(epsilon, score);
1775
2222
  }
2223
+ /**
2224
+ * Async variant of SPAN MI for use with async graph data sources.
2225
+ *
2226
+ * Fetches both neighbourhoods concurrently, then computes the clustering
2227
+ * coefficient for each endpoint from the collected neighbour arrays.
2228
+ */
2229
+ async function spanAsync(graph, source, target, config) {
2230
+ const { epsilon = 1e-10 } = config ?? {};
2231
+ const [sourceArr, targetArr] = await Promise.all([collectAsyncIterable(graph.neighbours(source)), collectAsyncIterable(graph.neighbours(target))]);
2232
+ const srcSet = new Set(sourceArr.filter((n) => n !== target));
2233
+ const tgtSet = new Set(targetArr.filter((n) => n !== source));
2234
+ let intersection = 0;
2235
+ for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2236
+ const union = srcSet.size + tgtSet.size - intersection;
2237
+ const jaccardScore = union > 0 ? intersection / union : 0;
2238
+ const computeClusteringCoefficient = async (nodeId, neighbourArr) => {
2239
+ const degree = neighbourArr.length;
2240
+ if (degree < 2) return 0;
2241
+ const pairs = [];
2242
+ for (let i = 0; i < neighbourArr.length; i++) for (let j = i + 1; j < neighbourArr.length; j++) {
2243
+ const u = neighbourArr[i];
2244
+ const v = neighbourArr[j];
2245
+ if (u !== void 0 && v !== void 0) pairs.push([u, v]);
2246
+ }
2247
+ const edgeResults = await Promise.all(pairs.flatMap(([u, v]) => [graph.getEdge(u, v), graph.getEdge(v, u)]));
2248
+ let triangleCount = 0;
2249
+ for (let i = 0; i < pairs.length; i++) if (edgeResults[2 * i] !== void 0 || edgeResults[2 * i + 1] !== void 0) triangleCount++;
2250
+ const possibleTriangles = degree * (degree - 1) / 2;
2251
+ return triangleCount / possibleTriangles;
2252
+ };
2253
+ const [sourceCc, targetCc] = await Promise.all([computeClusteringCoefficient(source, sourceArr), computeClusteringCoefficient(target, targetArr)]);
2254
+ const score = jaccardScore * (1 - Math.max(sourceCc, targetCc));
2255
+ return Math.max(epsilon, score);
2256
+ }
1776
2257
  //#endregion
1777
2258
  //#region src/ranking/mi/etch.ts
1778
2259
  /**
@@ -1788,6 +2269,37 @@ function etch(graph, source, target, config) {
1788
2269
  const score = jaccardScore * Math.log(graph.edgeCount / edgeTypeCount);
1789
2270
  return Math.max(epsilon, score);
1790
2271
  }
2272
+ /**
2273
+ * Async variant of ETCH MI for use with async graph data sources.
2274
+ *
2275
+ * Fetches both neighbourhoods and edge data concurrently, then counts
2276
+ * edges of the same type by iterating the async edge stream.
2277
+ */
2278
+ async function etchAsync(graph, source, target, config) {
2279
+ const { epsilon = 1e-10 } = config ?? {};
2280
+ const [sourceArr, targetArr, edge] = await Promise.all([
2281
+ collectAsyncIterable(graph.neighbours(source)),
2282
+ collectAsyncIterable(graph.neighbours(target)),
2283
+ graph.getEdge(source, target)
2284
+ ]);
2285
+ const srcSet = new Set(sourceArr.filter((n) => n !== target));
2286
+ const tgtSet = new Set(targetArr.filter((n) => n !== source));
2287
+ let intersection = 0;
2288
+ for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2289
+ const union = srcSet.size + tgtSet.size - intersection;
2290
+ const jaccardScore = union > 0 ? intersection / union : 0;
2291
+ if (edge?.type === void 0) return Math.max(epsilon, jaccardScore);
2292
+ const edgeType = edge.type;
2293
+ let edgeTypeCount = 0;
2294
+ let totalEdges = 0;
2295
+ for await (const e of graph.edges()) {
2296
+ totalEdges++;
2297
+ if (e.type === edgeType) edgeTypeCount++;
2298
+ }
2299
+ if (edgeTypeCount === 0) return Math.max(epsilon, jaccardScore);
2300
+ const score = jaccardScore * Math.log(totalEdges / edgeTypeCount);
2301
+ return Math.max(epsilon, score);
2302
+ }
1791
2303
  //#endregion
1792
2304
  //#region src/ranking/mi/notch.ts
1793
2305
  /**
@@ -1807,6 +2319,44 @@ function notch(graph, source, target, config) {
1807
2319
  const score = jaccardScore * sourceRarity * targetRarity;
1808
2320
  return Math.max(epsilon, score);
1809
2321
  }
2322
+ /**
2323
+ * Async variant of NOTCH MI for use with async graph data sources.
2324
+ *
2325
+ * Fetches both neighbourhoods and node data concurrently, then counts
2326
+ * nodes of each type by iterating the async node stream.
2327
+ */
2328
+ async function notchAsync(graph, source, target, config) {
2329
+ const { epsilon = 1e-10 } = config ?? {};
2330
+ const [sourceArr, targetArr, sourceNode, targetNode] = await Promise.all([
2331
+ collectAsyncIterable(graph.neighbours(source)),
2332
+ collectAsyncIterable(graph.neighbours(target)),
2333
+ graph.getNode(source),
2334
+ graph.getNode(target)
2335
+ ]);
2336
+ const srcSet = new Set(sourceArr.filter((n) => n !== target));
2337
+ const tgtSet = new Set(targetArr.filter((n) => n !== source));
2338
+ let intersection = 0;
2339
+ for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2340
+ const union = srcSet.size + tgtSet.size - intersection;
2341
+ const jaccardScore = union > 0 ? intersection / union : 0;
2342
+ if (sourceNode?.type === void 0 || targetNode?.type === void 0) return Math.max(epsilon, jaccardScore);
2343
+ const sourceType = sourceNode.type;
2344
+ const targetType = targetNode.type;
2345
+ let totalNodes = 0;
2346
+ let sourceTypeCount = 0;
2347
+ let targetTypeCount = 0;
2348
+ for await (const nodeId of graph.nodeIds()) {
2349
+ totalNodes++;
2350
+ const node = await graph.getNode(nodeId);
2351
+ if (node?.type === sourceType) sourceTypeCount++;
2352
+ if (node?.type === targetType) targetTypeCount++;
2353
+ }
2354
+ if (sourceTypeCount === 0 || targetTypeCount === 0) return Math.max(epsilon, jaccardScore);
2355
+ const sourceRarity = Math.log(totalNodes / sourceTypeCount);
2356
+ const targetRarity = Math.log(totalNodes / targetTypeCount);
2357
+ const score = jaccardScore * sourceRarity * targetRarity;
2358
+ return Math.max(epsilon, score);
2359
+ }
1810
2360
  //#endregion
1811
2361
  //#region src/ranking/mi/adaptive.ts
1812
2362
  /**
@@ -1839,6 +2389,38 @@ function adaptive(graph, source, target, config) {
1839
2389
  const score = (structuralWeight * structural + degreeWeight * degreeComponent + overlapWeight * overlap) / totalWeight;
1840
2390
  return Math.max(epsilon, Math.min(1, score));
1841
2391
  }
2392
+ /**
2393
+ * Async variant of Adaptive MI for use with async graph data sources.
2394
+ *
2395
+ * Fetches both neighbourhoods concurrently, then delegates degree-weighted
2396
+ * component to the async Adamic-Adar variant.
2397
+ */
2398
+ async function adaptiveAsync(graph, source, target, config) {
2399
+ const { epsilon = 1e-10, structuralWeight = .4, degreeWeight = .3, overlapWeight = .3 } = config ?? {};
2400
+ const [sourceArr, targetArr, degreeComponent] = await Promise.all([
2401
+ collectAsyncIterable(graph.neighbours(source)),
2402
+ collectAsyncIterable(graph.neighbours(target)),
2403
+ adamicAdarAsync(graph, source, target, {
2404
+ epsilon,
2405
+ normalise: true
2406
+ })
2407
+ ]);
2408
+ const srcSet = new Set(sourceArr.filter((n) => n !== target));
2409
+ const tgtSet = new Set(targetArr.filter((n) => n !== source));
2410
+ let intersection = 0;
2411
+ for (const n of srcSet) if (tgtSet.has(n)) intersection++;
2412
+ const union = srcSet.size + tgtSet.size - intersection;
2413
+ const jaccardScore = union > 0 ? intersection / union : 0;
2414
+ const structural = srcSet.size === 0 && tgtSet.size === 0 ? 0 : Math.max(epsilon, jaccardScore);
2415
+ let overlap;
2416
+ if (srcSet.size > 0 && tgtSet.size > 0) {
2417
+ const minDegree = Math.min(srcSet.size, tgtSet.size);
2418
+ overlap = minDegree > 0 ? intersection / minDegree : epsilon;
2419
+ } else overlap = epsilon;
2420
+ const totalWeight = structuralWeight + degreeWeight + overlapWeight;
2421
+ const score = (structuralWeight * structural + degreeWeight * degreeComponent + overlapWeight * overlap) / totalWeight;
2422
+ return Math.max(epsilon, Math.min(1, score));
2423
+ }
1842
2424
  //#endregion
1843
2425
  //#region src/ranking/baselines/utils.ts
1844
2426
  /**
@@ -3333,6 +3915,6 @@ function filterSubgraph(graph, options) {
3333
3915
  return result;
3334
3916
  }
3335
3917
  //#endregion
3336
- export { AdjacencyMapGraph, GPUContext, GPUNotAvailableError, PriorityQueue, _computeMean, adamicAdar, adaptive, approximateClusteringCoefficient, assertWebGPUAvailable, base, baseAsync, batchClusteringCoefficients, betweenness, bfs, bfsWithPath, communicability, computeJaccard, computeTrussNumbers, cosine, countEdgesOfType, countNodesOfType, createGPUContext, createResultBuffer, csrToGPUBuffers, degreeSum, detectWebGPU, dfs, dfsPriority, dfsPriorityFn, dfsWithPath, dome, domeHighDegree, edge, entropyFromCounts, enumerateMotifs, enumerateMotifsWithInstances, etch, extractEgoNetwork, extractInducedSubgraph, extractKCore, extractKTruss, filterSubgraph, flux, frontierBalanced, fuse, getGPUContext, getMotifName, graphToCSR, grasp, hae, hittingTime, hubPromoted, isWebGPUAvailable, jaccard, jaccardArithmetic, kHop, katz, lace, localClusteringCoefficient, localTypeEntropy, maze, miniBatchKMeans, neighbourIntersection, neighbourOverlap, neighbourSet, normaliseFeatures, normaliseFeatures as zScoreNormalise, normalisedEntropy, notch, overlapCoefficient, pagerank, parse, pipe, randomPriority, randomRanking, randomWalk, reach, readBufferToCPU, resistanceDistance, resourceAllocation, sage, scale, shannonEntropy, shortest, sift, skew, sorensen, span, standardBfs, stratified, tide, warp, widestPath };
3918
+ export { AdjacencyMapGraph, GPUContext, GPUNotAvailableError, PriorityQueue, _computeMean, adamicAdar, adamicAdarAsync, adaptive, adaptiveAsync, approximateClusteringCoefficient, assertWebGPUAvailable, base, baseAsync, batchClusteringCoefficients, betweenness, bfs, bfsWithPath, collectAsyncIterable, communicability, computeJaccard, computeTrussNumbers, cosine, cosineAsync, countEdgesOfType, countNodesOfType, createGPUContext, createResultBuffer, csrToGPUBuffers, defaultYieldStrategy, degreeSum, detectWebGPU, dfs, dfsPriority, dfsPriorityAsync, dfsPriorityFn, dfsWithPath, dome, domeAsync, domeHighDegree, domeHighDegreeAsync, edge, edgeAsync, entropyFromCounts, enumerateMotifs, enumerateMotifsWithInstances, etch, etchAsync, extractEgoNetwork, extractInducedSubgraph, extractKCore, extractKTruss, filterSubgraph, flux, fluxAsync, frontierBalanced, frontierBalancedAsync, fuse, fuseAsync, getGPUContext, getMotifName, graphToCSR, grasp, hae, haeAsync, hittingTime, hubPromoted, hubPromotedAsync, isWebGPUAvailable, jaccard, jaccardArithmetic, jaccardAsync, kHop, katz, lace, laceAsync, localClusteringCoefficient, localTypeEntropy, maze, mazeAsync, miniBatchKMeans, neighbourIntersection, neighbourOverlap, neighbourSet, normaliseFeatures, normaliseFeatures as zScoreNormalise, normalisedEntropy, notch, notchAsync, opDegree, opGetEdge, opGetNode, opHasNode, opNeighbours, opProgress, opYield, overlapCoefficient, overlapCoefficientAsync, pagerank, parse, parseAsync, pipe, pipeAsync, randomPriority, randomPriorityAsync, randomRanking, randomWalk, reach, reachAsync, readBufferToCPU, resistanceDistance, resolveAsyncOp, resolveSyncOp, resourceAllocation, resourceAllocationAsync, runAsync, runSync, sage, sageAsync, scale, scaleAsync, shannonEntropy, shortest, sift, siftAsync, skew, skewAsync, sorensen, sorensenAsync, span, spanAsync, standardBfs, standardBfsAsync, stratified, tide, tideAsync, warp, warpAsync, widestPath };
3337
3919
 
3338
3920
  //# sourceMappingURL=index.js.map