graphwise 1.3.3 → 1.4.1

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 (124) hide show
  1. package/README.md +71 -1
  2. package/dist/__test__/fixtures/graphs/city-suburban-village.d.ts +3 -0
  3. package/dist/__test__/fixtures/graphs/city-suburban-village.d.ts.map +1 -0
  4. package/dist/__test__/fixtures/graphs/city-village.d.ts +3 -0
  5. package/dist/__test__/fixtures/graphs/city-village.d.ts.map +1 -0
  6. package/dist/__test__/fixtures/graphs/index.d.ts +13 -0
  7. package/dist/__test__/fixtures/graphs/index.d.ts.map +1 -0
  8. package/dist/__test__/fixtures/graphs/quality-vs-popularity.d.ts +3 -0
  9. package/dist/__test__/fixtures/graphs/quality-vs-popularity.d.ts.map +1 -0
  10. package/dist/__test__/fixtures/graphs/social-hub.d.ts +3 -0
  11. package/dist/__test__/fixtures/graphs/social-hub.d.ts.map +1 -0
  12. package/dist/__test__/fixtures/graphs/three-community.d.ts +3 -0
  13. package/dist/__test__/fixtures/graphs/three-community.d.ts.map +1 -0
  14. package/dist/__test__/fixtures/graphs/two-department.d.ts +3 -0
  15. package/dist/__test__/fixtures/graphs/two-department.d.ts.map +1 -0
  16. package/dist/__test__/fixtures/graphs/typed-entity.d.ts +3 -0
  17. package/dist/__test__/fixtures/graphs/typed-entity.d.ts.map +1 -0
  18. package/dist/__test__/fixtures/helpers.d.ts +66 -0
  19. package/dist/__test__/fixtures/helpers.d.ts.map +1 -0
  20. package/dist/__test__/fixtures/helpers.unit.test.d.ts +7 -0
  21. package/dist/__test__/fixtures/helpers.unit.test.d.ts.map +1 -0
  22. package/dist/__test__/fixtures/index.d.ts +10 -0
  23. package/dist/__test__/fixtures/index.d.ts.map +1 -0
  24. package/dist/__test__/fixtures/types.d.ts +35 -0
  25. package/dist/__test__/fixtures/types.d.ts.map +1 -0
  26. package/dist/expansion/base.d.ts.map +1 -1
  27. package/dist/expansion/dome.d.ts.map +1 -1
  28. package/dist/expansion/dome.integration.test.d.ts +18 -0
  29. package/dist/expansion/dome.integration.test.d.ts.map +1 -0
  30. package/dist/expansion/edge.d.ts +3 -3
  31. package/dist/expansion/edge.d.ts.map +1 -1
  32. package/dist/expansion/edge.integration.test.d.ts +11 -0
  33. package/dist/expansion/edge.integration.test.d.ts.map +1 -0
  34. package/dist/expansion/flux.d.ts +25 -0
  35. package/dist/expansion/flux.d.ts.map +1 -0
  36. package/dist/expansion/flux.integration.test.d.ts +14 -0
  37. package/dist/expansion/flux.integration.test.d.ts.map +1 -0
  38. package/dist/expansion/flux.unit.test.d.ts +2 -0
  39. package/dist/expansion/flux.unit.test.d.ts.map +1 -0
  40. package/dist/expansion/fuse.d.ts +28 -0
  41. package/dist/expansion/fuse.d.ts.map +1 -0
  42. package/dist/expansion/fuse.integration.test.d.ts +15 -0
  43. package/dist/expansion/fuse.integration.test.d.ts.map +1 -0
  44. package/dist/expansion/fuse.unit.test.d.ts +2 -0
  45. package/dist/expansion/fuse.unit.test.d.ts.map +1 -0
  46. package/dist/expansion/hae.d.ts +7 -7
  47. package/dist/expansion/hae.d.ts.map +1 -1
  48. package/dist/expansion/hae.integration.test.d.ts +11 -0
  49. package/dist/expansion/hae.integration.test.d.ts.map +1 -0
  50. package/dist/expansion/index.d.ts +6 -0
  51. package/dist/expansion/index.d.ts.map +1 -1
  52. package/dist/expansion/lace.d.ts +22 -0
  53. package/dist/expansion/lace.d.ts.map +1 -0
  54. package/dist/expansion/lace.integration.test.d.ts +14 -0
  55. package/dist/expansion/lace.integration.test.d.ts.map +1 -0
  56. package/dist/expansion/lace.unit.test.d.ts +2 -0
  57. package/dist/expansion/lace.unit.test.d.ts.map +1 -0
  58. package/dist/expansion/maze.d.ts +3 -13
  59. package/dist/expansion/maze.d.ts.map +1 -1
  60. package/dist/expansion/maze.integration.test.d.ts +11 -0
  61. package/dist/expansion/maze.integration.test.d.ts.map +1 -0
  62. package/dist/expansion/pipe.d.ts +3 -3
  63. package/dist/expansion/pipe.d.ts.map +1 -1
  64. package/dist/expansion/pipe.integration.test.d.ts +12 -0
  65. package/dist/expansion/pipe.integration.test.d.ts.map +1 -0
  66. package/dist/expansion/reach.d.ts +4 -14
  67. package/dist/expansion/reach.d.ts.map +1 -1
  68. package/dist/expansion/reach.integration.test.d.ts +9 -0
  69. package/dist/expansion/reach.integration.test.d.ts.map +1 -0
  70. package/dist/expansion/sage.d.ts +5 -13
  71. package/dist/expansion/sage.d.ts.map +1 -1
  72. package/dist/expansion/sage.integration.test.d.ts +9 -0
  73. package/dist/expansion/sage.integration.test.d.ts.map +1 -0
  74. package/dist/expansion/sift.d.ts +26 -0
  75. package/dist/expansion/sift.d.ts.map +1 -0
  76. package/dist/expansion/sift.integration.test.d.ts +13 -0
  77. package/dist/expansion/sift.integration.test.d.ts.map +1 -0
  78. package/dist/expansion/sift.unit.test.d.ts +2 -0
  79. package/dist/expansion/sift.unit.test.d.ts.map +1 -0
  80. package/dist/expansion/tide.d.ts +15 -0
  81. package/dist/expansion/tide.d.ts.map +1 -0
  82. package/dist/expansion/tide.integration.test.d.ts +14 -0
  83. package/dist/expansion/tide.integration.test.d.ts.map +1 -0
  84. package/dist/expansion/tide.unit.test.d.ts +2 -0
  85. package/dist/expansion/tide.unit.test.d.ts.map +1 -0
  86. package/dist/expansion/warp.d.ts +15 -0
  87. package/dist/expansion/warp.d.ts.map +1 -0
  88. package/dist/expansion/warp.integration.test.d.ts +13 -0
  89. package/dist/expansion/warp.integration.test.d.ts.map +1 -0
  90. package/dist/expansion/warp.unit.test.d.ts +2 -0
  91. package/dist/expansion/warp.unit.test.d.ts.map +1 -0
  92. package/dist/graph/adjacency-map.d.ts.map +1 -1
  93. package/dist/graph/index.cjs +7 -0
  94. package/dist/graph/index.cjs.map +1 -1
  95. package/dist/graph/index.js +7 -0
  96. package/dist/graph/index.js.map +1 -1
  97. package/dist/index/index.cjs +323 -60
  98. package/dist/index/index.cjs.map +1 -1
  99. package/dist/index/index.js +318 -61
  100. package/dist/index/index.js.map +1 -1
  101. package/dist/ranking/mi/etch.integration.test.d.ts +2 -0
  102. package/dist/ranking/mi/etch.integration.test.d.ts.map +1 -0
  103. package/dist/ranking/mi/notch.integration.test.d.ts +2 -0
  104. package/dist/ranking/mi/notch.integration.test.d.ts.map +1 -0
  105. package/dist/ranking/mi/scale.d.ts.map +1 -1
  106. package/dist/ranking/mi/scale.integration.test.d.ts +2 -0
  107. package/dist/ranking/mi/scale.integration.test.d.ts.map +1 -0
  108. package/dist/ranking/mi/skew.integration.test.d.ts +2 -0
  109. package/dist/ranking/mi/skew.integration.test.d.ts.map +1 -0
  110. package/dist/ranking/mi/span.integration.test.d.ts +2 -0
  111. package/dist/ranking/mi/span.integration.test.d.ts.map +1 -0
  112. package/dist/ranking/parse.integration.test.d.ts +10 -0
  113. package/dist/ranking/parse.integration.test.d.ts.map +1 -0
  114. package/dist/structures/index.cjs +12 -2
  115. package/dist/structures/index.cjs.map +1 -1
  116. package/dist/structures/index.js +12 -2
  117. package/dist/structures/index.js.map +1 -1
  118. package/dist/structures/priority-queue.d.ts +4 -2
  119. package/dist/structures/priority-queue.d.ts.map +1 -1
  120. package/dist/utils/index.cjs +76 -77
  121. package/dist/utils/index.cjs.map +1 -1
  122. package/dist/utils/index.js +76 -77
  123. package/dist/utils/index.js.map +1 -1
  124. package/package.json +1 -1
@@ -25,6 +25,8 @@ function base(graph, seeds, config) {
25
25
  const { maxNodes = 0, maxIterations = 0, maxPaths = 0, priority = degreePriority, debug = false } = config ?? {};
26
26
  if (seeds.length === 0) return emptyResult("base", startTime);
27
27
  const numFrontiers = seeds.length;
28
+ const allVisited = /* @__PURE__ */ new Set();
29
+ const combinedVisited = /* @__PURE__ */ new Map();
28
30
  const visitedByFrontier = [];
29
31
  const predecessors = [];
30
32
  const queues = [];
@@ -36,15 +38,16 @@ function base(graph, seeds, config) {
36
38
  if (seed === void 0) continue;
37
39
  const seedNode = seed.id;
38
40
  predecessors[i]?.set(seedNode, null);
39
- const seedPriority = priority(seedNode, createPriorityContext(graph, seedNode, i, visitedByFrontier, [], 0));
41
+ combinedVisited.set(seedNode, i);
42
+ allVisited.add(seedNode);
43
+ const seedPriority = priority(seedNode, createPriorityContext(graph, seedNode, i, combinedVisited, allVisited, [], 0));
40
44
  queues[i]?.push({
41
45
  nodeId: seedNode,
42
46
  frontierIndex: i,
43
47
  predecessor: null
44
48
  }, seedPriority);
45
49
  }
46
- const allVisited = /* @__PURE__ */ new Set();
47
- const sampledEdges = /* @__PURE__ */ new Set();
50
+ const sampledEdgeMap = /* @__PURE__ */ new Map();
48
51
  const discoveredPaths = [];
49
52
  let iterations = 0;
50
53
  let edgesTraversed = 0;
@@ -89,6 +92,7 @@ function base(graph, seeds, config) {
89
92
  const frontierVisited = visitedByFrontier[activeFrontier];
90
93
  if (frontierVisited === void 0 || frontierVisited.has(nodeId)) continue;
91
94
  frontierVisited.set(nodeId, activeFrontier);
95
+ combinedVisited.set(nodeId, activeFrontier);
92
96
  if (predecessor !== null) {
93
97
  const predMap = predecessors[activeFrontier];
94
98
  if (predMap !== void 0) predMap.set(nodeId, predecessor);
@@ -110,11 +114,16 @@ function base(graph, seeds, config) {
110
114
  const neighbours = graph.neighbours(nodeId);
111
115
  for (const neighbour of neighbours) {
112
116
  edgesTraversed++;
113
- const edgeKey = nodeId < neighbour ? `${nodeId}::${neighbour}` : `${neighbour}::${nodeId}`;
114
- sampledEdges.add(edgeKey);
117
+ const [s, t] = nodeId < neighbour ? [nodeId, neighbour] : [neighbour, nodeId];
118
+ let targets = sampledEdgeMap.get(s);
119
+ if (targets === void 0) {
120
+ targets = /* @__PURE__ */ new Set();
121
+ sampledEdgeMap.set(s, targets);
122
+ }
123
+ targets.add(t);
115
124
  const frontierVisited = visitedByFrontier[activeFrontier];
116
125
  if (frontierVisited === void 0 || frontierVisited.has(neighbour)) continue;
117
- const neighbourPriority = priority(neighbour, createPriorityContext(graph, neighbour, activeFrontier, visitedByFrontier, discoveredPaths, iterations + 1));
126
+ const neighbourPriority = priority(neighbour, createPriorityContext(graph, neighbour, activeFrontier, combinedVisited, allVisited, discoveredPaths, iterations + 1));
118
127
  queue.push({
119
128
  nodeId: neighbour,
120
129
  frontierIndex: activeFrontier,
@@ -126,14 +135,7 @@ function base(graph, seeds, config) {
126
135
  const endTime = performance.now();
127
136
  const visitedPerFrontier = visitedByFrontier.map((m) => new Set(m.keys()));
128
137
  const edgeTuples = /* @__PURE__ */ new Set();
129
- for (const edgeKey of sampledEdges) {
130
- const parts = edgeKey.split("::");
131
- if (parts.length === 2) {
132
- const source = parts[0];
133
- const target = parts[1];
134
- if (source !== void 0 && target !== void 0) edgeTuples.add([source, target]);
135
- }
136
- }
138
+ for (const [source, targets] of sampledEdgeMap) for (const target of targets) edgeTuples.add([source, target]);
137
139
  return {
138
140
  paths: discoveredPaths,
139
141
  sampledNodes: allVisited,
@@ -153,10 +155,7 @@ function base(graph, seeds, config) {
153
155
  /**
154
156
  * Create priority context for a node.
155
157
  */
156
- function createPriorityContext(graph, nodeId, frontierIndex, visitedByFrontier, discoveredPaths, iteration) {
157
- const combinedVisited = /* @__PURE__ */ new Map();
158
- for (const frontierMap of visitedByFrontier) for (const [id, idx] of frontierMap) combinedVisited.set(id, idx);
159
- const allVisited = new Set(combinedVisited.keys());
158
+ function createPriorityContext(graph, nodeId, frontierIndex, combinedVisited, allVisited, discoveredPaths, iteration) {
160
159
  return {
161
160
  graph,
162
161
  degree: graph.degree(nodeId),
@@ -235,7 +234,7 @@ function emptyResult(algorithm, startTime) {
235
234
  */
236
235
  function dome(graph, seeds, config) {
237
236
  const domePriority = (nodeId, context) => {
238
- return graph.degree(nodeId);
237
+ return context.degree;
239
238
  };
240
239
  return base(graph, seeds, {
241
240
  ...config,
@@ -247,7 +246,7 @@ function dome(graph, seeds, config) {
247
246
  */
248
247
  function domeHighDegree(graph, seeds, config) {
249
248
  const domePriority = (nodeId, context) => {
250
- return -graph.degree(nodeId);
249
+ return -context.degree;
251
250
  };
252
251
  return base(graph, seeds, {
253
252
  ...config,
@@ -256,23 +255,26 @@ function domeHighDegree(graph, seeds, config) {
256
255
  }
257
256
  //#endregion
258
257
  //#region src/expansion/edge.ts
258
+ var EPSILON$1 = 1e-10;
259
259
  /**
260
- * EDGE priority function.
261
- *
262
- * Priority = degree(source) + degree(target)
263
- * Lower values = higher priority (explored first)
260
+ * Priority function using local type entropy.
261
+ * Lower values = higher priority (expanded first).
264
262
  */
265
263
  function edgePriority(nodeId, context) {
266
264
  const graph = context.graph;
267
- let totalDegree = context.degree;
268
- for (const neighbour of graph.neighbours(nodeId)) totalDegree += graph.degree(neighbour);
269
- return totalDegree;
265
+ const neighbours = graph.neighbours(nodeId);
266
+ const neighbourTypes = [];
267
+ for (const neighbour of neighbours) {
268
+ const node = graph.getNode(neighbour);
269
+ neighbourTypes.push(node?.type ?? "default");
270
+ }
271
+ return 1 / (localTypeEntropy(neighbourTypes) + EPSILON$1) * Math.log(context.degree + 1);
270
272
  }
271
273
  /**
272
- * Run EDGE expansion algorithm.
274
+ * Run EDGE expansion (Entropy-Driven Graph Expansion).
273
275
  *
274
- * Expands from seeds prioritising low-degree edges first.
275
- * Useful for avoiding hubs and exploring sparse regions.
276
+ * Discovers paths by prioritising nodes with diverse neighbour types,
277
+ * deferring nodes with homogeneous neighbourhoods.
276
278
  *
277
279
  * @param graph - Source graph
278
280
  * @param seeds - Seed nodes for expansion
@@ -286,6 +288,122 @@ function edge(graph, seeds, config) {
286
288
  });
287
289
  }
288
290
  //#endregion
291
+ //#region src/expansion/hae.ts
292
+ var EPSILON = 1e-10;
293
+ /**
294
+ * Default type mapper - uses node.type property.
295
+ */
296
+ function defaultTypeMapper(node) {
297
+ return node.type ?? "default";
298
+ }
299
+ /**
300
+ * Create a priority function using the given type mapper.
301
+ */
302
+ function createHAEPriority(typeMapper) {
303
+ return function haePriority(nodeId, context) {
304
+ const graph = context.graph;
305
+ const neighbours = graph.neighbours(nodeId);
306
+ const neighbourTypes = [];
307
+ for (const neighbour of neighbours) {
308
+ const node = graph.getNode(neighbour);
309
+ if (node !== void 0) neighbourTypes.push(typeMapper(node));
310
+ }
311
+ return 1 / (localTypeEntropy(neighbourTypes) + EPSILON) * Math.log(context.degree + 1);
312
+ };
313
+ }
314
+ /**
315
+ * Run HAE expansion (Heterogeneity-Aware Expansion).
316
+ *
317
+ * Discovers paths by prioritising nodes with diverse neighbour types,
318
+ * using a custom type mapper for flexible type extraction.
319
+ *
320
+ * @param graph - Source graph
321
+ * @param seeds - Seed nodes for expansion
322
+ * @param config - HAE configuration with optional typeMapper
323
+ * @returns Expansion result with discovered paths
324
+ */
325
+ function hae(graph, seeds, config) {
326
+ const typeMapper = config?.typeMapper ?? defaultTypeMapper;
327
+ return base(graph, seeds, {
328
+ ...config,
329
+ priority: createHAEPriority(typeMapper)
330
+ });
331
+ }
332
+ //#endregion
333
+ //#region src/expansion/pipe.ts
334
+ /**
335
+ * Priority function using path potential.
336
+ * Lower values = higher priority (expanded first).
337
+ *
338
+ * Path potential measures how many of a node's neighbours have been
339
+ * visited by OTHER frontiers (not the current frontier).
340
+ */
341
+ function pipePriority(nodeId, context) {
342
+ const neighbours = context.graph.neighbours(nodeId);
343
+ let pathPotential = 0;
344
+ for (const neighbour of neighbours) {
345
+ const visitedBy = context.visitedByFrontier.get(neighbour);
346
+ if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
347
+ }
348
+ return context.degree / (1 + pathPotential);
349
+ }
350
+ /**
351
+ * Run PIPE expansion (Path-Potential Informed Priority Expansion).
352
+ *
353
+ * Discovers paths by prioritising nodes that bridge multiple frontiers,
354
+ * identifying connecting points between seed regions.
355
+ *
356
+ * @param graph - Source graph
357
+ * @param seeds - Seed nodes for expansion
358
+ * @param config - Expansion configuration
359
+ * @returns Expansion result with discovered paths
360
+ */
361
+ function pipe(graph, seeds, config) {
362
+ return base(graph, seeds, {
363
+ ...config,
364
+ priority: pipePriority
365
+ });
366
+ }
367
+ //#endregion
368
+ //#region src/expansion/sage.ts
369
+ /**
370
+ * Run SAGE expansion algorithm.
371
+ *
372
+ * Salience-aware multi-frontier expansion with two phases:
373
+ * - Phase 1: Degree-based priority (early exploration)
374
+ * - Phase 2: Salience feedback (path-aware frontier steering)
375
+ *
376
+ * @param graph - Source graph
377
+ * @param seeds - Seed nodes for expansion
378
+ * @param config - Expansion configuration
379
+ * @returns Expansion result with discovered paths
380
+ */
381
+ function sage(graph, seeds, config) {
382
+ const salienceCounts = /* @__PURE__ */ new Map();
383
+ let inPhase2 = false;
384
+ let lastPathCount = 0;
385
+ /**
386
+ * SAGE priority function with phase transition logic.
387
+ */
388
+ function sagePriority(nodeId, context) {
389
+ const pathCount = context.discoveredPaths.length;
390
+ if (pathCount > 0 && !inPhase2) inPhase2 = true;
391
+ if (pathCount > lastPathCount) {
392
+ for (let i = lastPathCount; i < pathCount; i++) {
393
+ const path = context.discoveredPaths[i];
394
+ if (path !== void 0) for (const node of path.nodes) salienceCounts.set(node, (salienceCounts.get(node) ?? 0) + 1);
395
+ }
396
+ lastPathCount = pathCount;
397
+ }
398
+ if (!inPhase2) return Math.log(context.degree + 1);
399
+ return -((salienceCounts.get(nodeId) ?? 0) * 1e3 - context.degree);
400
+ }
401
+ return base(graph, seeds, {
402
+ ...config,
403
+ priority: sagePriority
404
+ });
405
+ }
406
+ //#endregion
289
407
  //#region src/ranking/mi/jaccard.ts
290
408
  /**
291
409
  * Compute Jaccard similarity between neighbourhoods of two nodes.
@@ -304,14 +422,153 @@ function jaccard(graph, source, target, config) {
304
422
  return Math.max(epsilon, score);
305
423
  }
306
424
  //#endregion
307
- //#region src/expansion/hae.ts
425
+ //#region src/expansion/reach.ts
308
426
  /**
309
- * HAE priority function.
427
+ * Run REACH expansion algorithm.
428
+ *
429
+ * Mutual information-aware multi-frontier expansion with two phases:
430
+ * - Phase 1: Degree-based priority (early exploration)
431
+ * - Phase 2: Structural similarity feedback (MI-guided frontier steering)
432
+ *
433
+ * @param graph - Source graph
434
+ * @param seeds - Seed nodes for expansion
435
+ * @param config - Expansion configuration
436
+ * @returns Expansion result with discovered paths
437
+ */
438
+ function reach(graph, seeds, config) {
439
+ let inPhase2 = false;
440
+ const jaccardCache = /* @__PURE__ */ new Map();
441
+ /**
442
+ * Compute Jaccard similarity with caching.
443
+ *
444
+ * Exploits symmetry of Jaccard (J(A,B) = J(B,A)) to reduce
445
+ * duplicate computations when the same pair appears in multiple
446
+ * discovered paths. Key format ensures consistent ordering.
447
+ */
448
+ function cachedJaccard(source, target) {
449
+ const key = source < target ? `${source}::${target}` : `${target}::${source}`;
450
+ let score = jaccardCache.get(key);
451
+ if (score === void 0) {
452
+ score = jaccard(graph, source, target);
453
+ jaccardCache.set(key, score);
454
+ }
455
+ return score;
456
+ }
457
+ /**
458
+ * REACH priority function with MI estimation.
459
+ */
460
+ function reachPriority(nodeId, context) {
461
+ if (context.discoveredPaths.length > 0 && !inPhase2) inPhase2 = true;
462
+ if (!inPhase2) return Math.log(context.degree + 1);
463
+ let totalMI = 0;
464
+ let endpointCount = 0;
465
+ for (const path of context.discoveredPaths) {
466
+ const fromNodeId = path.fromSeed.id;
467
+ const toNodeId = path.toSeed.id;
468
+ totalMI += cachedJaccard(nodeId, fromNodeId);
469
+ totalMI += cachedJaccard(nodeId, toNodeId);
470
+ endpointCount += 2;
471
+ }
472
+ const miHat = endpointCount > 0 ? totalMI / endpointCount : 0;
473
+ return Math.log(context.degree + 1) * (1 - miHat);
474
+ }
475
+ return base(graph, seeds, {
476
+ ...config,
477
+ priority: reachPriority
478
+ });
479
+ }
480
+ //#endregion
481
+ //#region src/expansion/maze.ts
482
+ /** Default threshold for switching to phase 2 (after M paths) */
483
+ var DEFAULT_PHASE2_THRESHOLD = 1;
484
+ /** Salience weighting factor */
485
+ var SALIENCE_WEIGHT = 1e3;
486
+ /**
487
+ * Run MAZE expansion algorithm.
488
+ *
489
+ * Multi-phase expansion combining path potential and salience with
490
+ * adaptive frontier steering.
491
+ *
492
+ * @param graph - Source graph
493
+ * @param seeds - Seed nodes for expansion
494
+ * @param config - Expansion configuration
495
+ * @returns Expansion result with discovered paths
496
+ */
497
+ function maze(graph, seeds, config) {
498
+ const salienceCounts = /* @__PURE__ */ new Map();
499
+ let inPhase2 = false;
500
+ let lastPathCount = 0;
501
+ /**
502
+ * MAZE priority function with path potential and salience feedback.
503
+ */
504
+ function mazePriority(nodeId, context) {
505
+ const pathCount = context.discoveredPaths.length;
506
+ if (pathCount >= DEFAULT_PHASE2_THRESHOLD && !inPhase2) {
507
+ inPhase2 = true;
508
+ for (const path of context.discoveredPaths) for (const node of path.nodes) salienceCounts.set(node, (salienceCounts.get(node) ?? 0) + 1);
509
+ }
510
+ if (inPhase2 && pathCount > lastPathCount) {
511
+ for (let i = lastPathCount; i < pathCount; i++) {
512
+ const path = context.discoveredPaths[i];
513
+ if (path !== void 0) for (const node of path.nodes) salienceCounts.set(node, (salienceCounts.get(node) ?? 0) + 1);
514
+ }
515
+ lastPathCount = pathCount;
516
+ }
517
+ const nodeNeighbours = graph.neighbours(nodeId);
518
+ let pathPotential = 0;
519
+ for (const neighbour of nodeNeighbours) {
520
+ const visitedBy = context.visitedByFrontier.get(neighbour);
521
+ if (visitedBy !== void 0 && visitedBy !== context.frontierIndex) pathPotential++;
522
+ }
523
+ if (!inPhase2) return context.degree / (1 + pathPotential);
524
+ const salience = salienceCounts.get(nodeId) ?? 0;
525
+ return context.degree / (1 + pathPotential) * (1 / (1 + SALIENCE_WEIGHT * salience));
526
+ }
527
+ return base(graph, seeds, {
528
+ ...config,
529
+ priority: mazePriority
530
+ });
531
+ }
532
+ //#endregion
533
+ //#region src/expansion/tide.ts
534
+ /**
535
+ * TIDE priority function.
536
+ *
537
+ * Priority = degree(source) + degree(target)
538
+ * Lower values = higher priority (explored first)
539
+ */
540
+ function tidePriority(nodeId, context) {
541
+ const graph = context.graph;
542
+ let totalDegree = context.degree;
543
+ for (const neighbour of graph.neighbours(nodeId)) totalDegree += graph.degree(neighbour);
544
+ return totalDegree;
545
+ }
546
+ /**
547
+ * Run TIDE expansion algorithm.
548
+ *
549
+ * Expands from seeds prioritising low-degree edges first.
550
+ * Useful for avoiding hubs and exploring sparse regions.
551
+ *
552
+ * @param graph - Source graph
553
+ * @param seeds - Seed nodes for expansion
554
+ * @param config - Expansion configuration
555
+ * @returns Expansion result with discovered paths
556
+ */
557
+ function tide(graph, seeds, config) {
558
+ return base(graph, seeds, {
559
+ ...config,
560
+ priority: tidePriority
561
+ });
562
+ }
563
+ //#endregion
564
+ //#region src/expansion/lace.ts
565
+ /**
566
+ * LACE priority function.
310
567
  *
311
568
  * Priority = 1 - MI(source, neighbour)
312
569
  * Higher MI = lower priority value = explored first
313
570
  */
314
- function haePriority(nodeId, context, mi) {
571
+ function lacePriority(nodeId, context, mi) {
315
572
  const graph = context.graph;
316
573
  const frontierIndex = context.frontierIndex;
317
574
  let maxMi = 0;
@@ -326,7 +583,7 @@ function haePriority(nodeId, context, mi) {
326
583
  return 1 - (count > 0 ? totalMi / count : 0);
327
584
  }
328
585
  /**
329
- * Run HAE expansion algorithm.
586
+ * Run LACE expansion algorithm.
330
587
  *
331
588
  * Expands from seeds prioritising high-MI edges.
332
589
  * Useful for finding paths with strong semantic associations.
@@ -336,16 +593,16 @@ function haePriority(nodeId, context, mi) {
336
593
  * @param config - Expansion configuration with MI function
337
594
  * @returns Expansion result with discovered paths
338
595
  */
339
- function hae(graph, seeds, config) {
596
+ function lace(graph, seeds, config) {
340
597
  const { mi = jaccard, ...restConfig } = config ?? {};
341
- const priority = (nodeId, context) => haePriority(nodeId, context, mi);
598
+ const priority = (nodeId, context) => lacePriority(nodeId, context, mi);
342
599
  return base(graph, seeds, {
343
600
  ...restConfig,
344
601
  priority
345
602
  });
346
603
  }
347
604
  //#endregion
348
- //#region src/expansion/pipe.ts
605
+ //#region src/expansion/warp.ts
349
606
  /**
350
607
  * PIPE priority function.
351
608
  *
@@ -353,7 +610,7 @@ function hae(graph, seeds, config) {
353
610
  * Bridge score = neighbourhood overlap with other frontiers
354
611
  * Higher bridge score = more likely to be on paths = explored first
355
612
  */
356
- function pipePriority(nodeId, context) {
613
+ function warpPriority(nodeId, context) {
357
614
  const graph = context.graph;
358
615
  const currentFrontier = context.frontierIndex;
359
616
  const nodeNeighbours = new Set(graph.neighbours(nodeId));
@@ -363,7 +620,7 @@ function pipePriority(nodeId, context) {
363
620
  return 1 / (1 + bridgeScore);
364
621
  }
365
622
  /**
366
- * Run PIPE expansion algorithm.
623
+ * Run WARP expansion algorithm.
367
624
  *
368
625
  * Expands from seeds prioritising bridge nodes.
369
626
  * Useful for finding paths through structurally important nodes.
@@ -373,14 +630,14 @@ function pipePriority(nodeId, context) {
373
630
  * @param config - Expansion configuration
374
631
  * @returns Expansion result with discovered paths
375
632
  */
376
- function pipe(graph, seeds, config) {
633
+ function warp(graph, seeds, config) {
377
634
  return base(graph, seeds, {
378
635
  ...config,
379
- priority: pipePriority
636
+ priority: warpPriority
380
637
  });
381
638
  }
382
639
  //#endregion
383
- //#region src/expansion/sage.ts
640
+ //#region src/expansion/fuse.ts
384
641
  /**
385
642
  * SAGE priority function.
386
643
  *
@@ -388,7 +645,7 @@ function pipe(graph, seeds, config) {
388
645
  * Priority = (1 - w) * degree + w * (1 - avg_salience)
389
646
  * Lower values = higher priority
390
647
  */
391
- function sagePriority(nodeId, context, mi, salienceWeight) {
648
+ function fusePriority(nodeId, context, mi, salienceWeight) {
392
649
  const graph = context.graph;
393
650
  const degree = context.degree;
394
651
  const frontierIndex = context.frontierIndex;
@@ -402,7 +659,7 @@ function sagePriority(nodeId, context, mi, salienceWeight) {
402
659
  return (1 - salienceWeight) * degree + salienceWeight * (1 - avgSalience);
403
660
  }
404
661
  /**
405
- * Run SAGE expansion algorithm.
662
+ * Run FUSE expansion algorithm.
406
663
  *
407
664
  * Combines structural exploration with semantic salience.
408
665
  * Useful for finding paths that are both short and semantically meaningful.
@@ -412,22 +669,22 @@ function sagePriority(nodeId, context, mi, salienceWeight) {
412
669
  * @param config - Expansion configuration with MI function
413
670
  * @returns Expansion result with discovered paths
414
671
  */
415
- function sage(graph, seeds, config) {
672
+ function fuse(graph, seeds, config) {
416
673
  const { mi = jaccard, salienceWeight = .5, ...restConfig } = config ?? {};
417
- const priority = (nodeId, context) => sagePriority(nodeId, context, mi, salienceWeight);
674
+ const priority = (nodeId, context) => fusePriority(nodeId, context, mi, salienceWeight);
418
675
  return base(graph, seeds, {
419
676
  ...restConfig,
420
677
  priority
421
678
  });
422
679
  }
423
680
  //#endregion
424
- //#region src/expansion/reach.ts
681
+ //#region src/expansion/sift.ts
425
682
  /**
426
683
  * REACH priority function (phase 2).
427
684
  *
428
685
  * Uses learned MI threshold to prioritise high-MI edges.
429
686
  */
430
- function reachPriority(nodeId, context, mi, miThreshold) {
687
+ function siftPriority(nodeId, context, mi, miThreshold) {
431
688
  const graph = context.graph;
432
689
  const frontierIndex = context.frontierIndex;
433
690
  let totalMi = 0;
@@ -441,7 +698,7 @@ function reachPriority(nodeId, context, mi, miThreshold) {
441
698
  else return context.degree + 100;
442
699
  }
443
700
  /**
444
- * Run REACH expansion algorithm.
701
+ * Run SIFT expansion algorithm.
445
702
  *
446
703
  * Two-phase adaptive expansion that learns MI thresholds
447
704
  * from initial sampling, then uses them for guided expansion.
@@ -451,16 +708,16 @@ function reachPriority(nodeId, context, mi, miThreshold) {
451
708
  * @param config - Expansion configuration
452
709
  * @returns Expansion result with discovered paths
453
710
  */
454
- function reach(graph, seeds, config) {
711
+ function sift(graph, seeds, config) {
455
712
  const { mi = jaccard, miThreshold = .25, ...restConfig } = config ?? {};
456
- const priority = (nodeId, context) => reachPriority(nodeId, context, mi, miThreshold);
713
+ const priority = (nodeId, context) => siftPriority(nodeId, context, mi, miThreshold);
457
714
  return base(graph, seeds, {
458
715
  ...restConfig,
459
716
  priority
460
717
  });
461
718
  }
462
719
  //#endregion
463
- //#region src/expansion/maze.ts
720
+ //#region src/expansion/flux.ts
464
721
  /**
465
722
  * Compute local density around a node.
466
723
  */
@@ -495,7 +752,7 @@ function bridgeScore(nodeId, context) {
495
752
  * - Low density + low bridge: DOME mode
496
753
  * - High bridge score: PIPE mode
497
754
  */
498
- function mazePriority(nodeId, context, densityThreshold, bridgeThreshold) {
755
+ function fluxPriority(nodeId, context, densityThreshold, bridgeThreshold) {
499
756
  const graph = context.graph;
500
757
  const degree = context.degree;
501
758
  const density = localDensity(graph, nodeId);
@@ -506,7 +763,7 @@ function mazePriority(nodeId, context, densityThreshold, bridgeThreshold) {
506
763
  else return degree;
507
764
  }
508
765
  /**
509
- * Run MAZE expansion algorithm.
766
+ * Run FLUX expansion algorithm.
510
767
  *
511
768
  * Adaptively switches between expansion strategies based on
512
769
  * local graph structure. Useful for heterogeneous graphs
@@ -517,9 +774,9 @@ function mazePriority(nodeId, context, densityThreshold, bridgeThreshold) {
517
774
  * @param config - Expansion configuration
518
775
  * @returns Expansion result with discovered paths
519
776
  */
520
- function maze(graph, seeds, config) {
777
+ function flux(graph, seeds, config) {
521
778
  const { densityThreshold = .5, bridgeThreshold = .3, ...restConfig } = config ?? {};
522
- const priority = (nodeId, context) => mazePriority(nodeId, context, densityThreshold, bridgeThreshold);
779
+ const priority = (nodeId, context) => fluxPriority(nodeId, context, densityThreshold, bridgeThreshold);
523
780
  return base(graph, seeds, {
524
781
  ...restConfig,
525
782
  priority
@@ -703,8 +960,8 @@ function scale(graph, source, target, config) {
703
960
  const jaccard = union > 0 ? intersection / union : 0;
704
961
  const n = graph.nodeCount;
705
962
  const m = graph.edgeCount;
706
- const densityNormaliser = graph.directed ? n * (n - 1) : 2 * n * (n - 1);
707
- const density = densityNormaliser > 0 ? m / densityNormaliser : 0;
963
+ const possibleEdges = n * (n - 1);
964
+ const density = possibleEdges > 0 ? (graph.directed ? m : 2 * m) / possibleEdges : 0;
708
965
  if (density === 0) return epsilon;
709
966
  const score = jaccard / density;
710
967
  return Math.max(epsilon, score);
@@ -875,7 +1132,7 @@ function degreeSum(graph, paths, config) {
875
1132
  return {
876
1133
  paths: scored.map(({ path, score }) => ({
877
1134
  ...path,
878
- score: includeScores ? score / maxScore : score / maxScore
1135
+ score: includeScores ? score / maxScore : score
879
1136
  })).sort((a, b) => b.score - a.score),
880
1137
  method: "degree-sum"
881
1138
  };
@@ -977,7 +1234,7 @@ function jaccardArithmetic(graph, paths, config) {
977
1234
  return {
978
1235
  paths: scored.map(({ path, score }) => ({
979
1236
  ...path,
980
- score: includeScores ? score / maxScore : score / maxScore
1237
+ score: includeScores ? score / maxScore : score
981
1238
  })).sort((a, b) => b.score - a.score),
982
1239
  method: "jaccard-arithmetic"
983
1240
  };
@@ -2164,6 +2421,6 @@ function filterSubgraph(graph, options) {
2164
2421
  return result;
2165
2422
  }
2166
2423
  //#endregion
2167
- export { AdjacencyMapGraph, GPUContext, GPUNotAvailableError, PriorityQueue, _computeMean, adamicAdar, adaptive, approximateClusteringCoefficient, assertWebGPUAvailable, base, batchClusteringCoefficients, betweenness, bfs, bfsWithPath, communicability, computeTrussNumbers, countEdgesOfType, countNodesOfType, createGPUContext, createResultBuffer, csrToGPUBuffers, degreeSum, detectWebGPU, dfs, dfsWithPath, dome, domeHighDegree, edge, entropyFromCounts, enumerateMotifs, enumerateMotifsWithInstances, etch, extractEgoNetwork, extractInducedSubgraph, extractKCore, extractKTruss, filterSubgraph, frontierBalanced, getGPUContext, getMotifName, graphToCSR, grasp, hae, isWebGPUAvailable, jaccard, jaccardArithmetic, katz, localClusteringCoefficient, localTypeEntropy, maze, miniBatchKMeans, neighbourIntersection, neighbourOverlap, neighbourSet, normaliseFeatures, normaliseFeatures as zScoreNormalise, normalisedEntropy, notch, pagerank, parse, pipe, randomPriority, randomRanking, reach, readBufferToCPU, resistanceDistance, sage, scale, shannonEntropy, shortest, skew, span, standardBfs, stratified, widestPath };
2424
+ export { AdjacencyMapGraph, GPUContext, GPUNotAvailableError, PriorityQueue, _computeMean, adamicAdar, adaptive, approximateClusteringCoefficient, assertWebGPUAvailable, base, batchClusteringCoefficients, betweenness, bfs, bfsWithPath, communicability, computeTrussNumbers, countEdgesOfType, countNodesOfType, createGPUContext, createResultBuffer, csrToGPUBuffers, degreeSum, detectWebGPU, dfs, dfsWithPath, dome, domeHighDegree, edge, entropyFromCounts, enumerateMotifs, enumerateMotifsWithInstances, etch, extractEgoNetwork, extractInducedSubgraph, extractKCore, extractKTruss, filterSubgraph, flux, frontierBalanced, fuse, getGPUContext, getMotifName, graphToCSR, grasp, hae, isWebGPUAvailable, jaccard, jaccardArithmetic, katz, lace, localClusteringCoefficient, localTypeEntropy, maze, miniBatchKMeans, neighbourIntersection, neighbourOverlap, neighbourSet, normaliseFeatures, normaliseFeatures as zScoreNormalise, normalisedEntropy, notch, pagerank, parse, pipe, randomPriority, randomRanking, reach, readBufferToCPU, resistanceDistance, sage, scale, shannonEntropy, shortest, sift, skew, span, standardBfs, stratified, tide, warp, widestPath };
2168
2425
 
2169
2426
  //# sourceMappingURL=index.js.map