graphwise 1.7.0 → 1.8.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 (130) hide show
  1. package/README.md +81 -30
  2. package/dist/adjacency-map-B6wPtmaq.cjs +234 -0
  3. package/dist/adjacency-map-B6wPtmaq.cjs.map +1 -0
  4. package/dist/adjacency-map-D-Ul7V1r.js +229 -0
  5. package/dist/adjacency-map-D-Ul7V1r.js.map +1 -0
  6. package/dist/async/index.cjs +16 -0
  7. package/dist/async/index.js +3 -0
  8. package/dist/expansion/dfs-priority.d.ts +11 -0
  9. package/dist/expansion/dfs-priority.d.ts.map +1 -1
  10. package/dist/expansion/dome.d.ts +20 -0
  11. package/dist/expansion/dome.d.ts.map +1 -1
  12. package/dist/expansion/edge.d.ts +18 -0
  13. package/dist/expansion/edge.d.ts.map +1 -1
  14. package/dist/expansion/flux.d.ts +16 -0
  15. package/dist/expansion/flux.d.ts.map +1 -1
  16. package/dist/expansion/frontier-balanced.d.ts +11 -0
  17. package/dist/expansion/frontier-balanced.d.ts.map +1 -1
  18. package/dist/expansion/fuse.d.ts +16 -0
  19. package/dist/expansion/fuse.d.ts.map +1 -1
  20. package/dist/expansion/hae.d.ts +16 -0
  21. package/dist/expansion/hae.d.ts.map +1 -1
  22. package/dist/expansion/index.cjs +43 -0
  23. package/dist/expansion/index.js +2 -0
  24. package/dist/expansion/lace.d.ts +16 -0
  25. package/dist/expansion/lace.d.ts.map +1 -1
  26. package/dist/expansion/maze.d.ts +17 -0
  27. package/dist/expansion/maze.d.ts.map +1 -1
  28. package/dist/expansion/pipe.d.ts +16 -0
  29. package/dist/expansion/pipe.d.ts.map +1 -1
  30. package/dist/expansion/random-priority.d.ts +18 -0
  31. package/dist/expansion/random-priority.d.ts.map +1 -1
  32. package/dist/expansion/reach.d.ts +17 -0
  33. package/dist/expansion/reach.d.ts.map +1 -1
  34. package/dist/expansion/sage.d.ts +15 -0
  35. package/dist/expansion/sage.d.ts.map +1 -1
  36. package/dist/expansion/sift.d.ts +16 -0
  37. package/dist/expansion/sift.d.ts.map +1 -1
  38. package/dist/expansion/standard-bfs.d.ts +11 -0
  39. package/dist/expansion/standard-bfs.d.ts.map +1 -1
  40. package/dist/expansion/tide.d.ts +16 -0
  41. package/dist/expansion/tide.d.ts.map +1 -1
  42. package/dist/expansion/warp.d.ts +16 -0
  43. package/dist/expansion/warp.d.ts.map +1 -1
  44. package/dist/expansion-FkmEYlrQ.cjs +1949 -0
  45. package/dist/expansion-FkmEYlrQ.cjs.map +1 -0
  46. package/dist/expansion-sldRognt.js +1704 -0
  47. package/dist/expansion-sldRognt.js.map +1 -0
  48. package/dist/extraction/index.cjs +630 -0
  49. package/dist/extraction/index.cjs.map +1 -0
  50. package/dist/extraction/index.js +621 -0
  51. package/dist/extraction/index.js.map +1 -0
  52. package/dist/graph/index.cjs +2 -229
  53. package/dist/graph/index.js +1 -228
  54. package/dist/index/index.cjs +131 -3406
  55. package/dist/index/index.js +14 -3334
  56. package/dist/index.d.ts +1 -0
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/jaccard-Bmd1IEFO.cjs +50 -0
  59. package/dist/jaccard-Bmd1IEFO.cjs.map +1 -0
  60. package/dist/jaccard-Yddrtt5D.js +39 -0
  61. package/dist/jaccard-Yddrtt5D.js.map +1 -0
  62. package/dist/{kmeans-BIgSyGKu.cjs → kmeans-D3yX5QFs.cjs} +1 -1
  63. package/dist/{kmeans-BIgSyGKu.cjs.map → kmeans-D3yX5QFs.cjs.map} +1 -1
  64. package/dist/{kmeans-87ExSUNZ.js → kmeans-DVCe61Me.js} +1 -1
  65. package/dist/{kmeans-87ExSUNZ.js.map → kmeans-DVCe61Me.js.map} +1 -1
  66. package/dist/ops-4nmI-pwk.cjs +277 -0
  67. package/dist/ops-4nmI-pwk.cjs.map +1 -0
  68. package/dist/ops-Zsu4ecEG.js +212 -0
  69. package/dist/ops-Zsu4ecEG.js.map +1 -0
  70. package/dist/priority-queue-ChVLoG6T.cjs +148 -0
  71. package/dist/priority-queue-ChVLoG6T.cjs.map +1 -0
  72. package/dist/priority-queue-DqCuFTR8.js +143 -0
  73. package/dist/priority-queue-DqCuFTR8.js.map +1 -0
  74. package/dist/ranking/index.cjs +43 -0
  75. package/dist/ranking/index.js +4 -0
  76. package/dist/ranking/mi/adamic-adar.d.ts +8 -0
  77. package/dist/ranking/mi/adamic-adar.d.ts.map +1 -1
  78. package/dist/ranking/mi/adaptive.d.ts +8 -0
  79. package/dist/ranking/mi/adaptive.d.ts.map +1 -1
  80. package/dist/ranking/mi/cosine.d.ts +7 -0
  81. package/dist/ranking/mi/cosine.d.ts.map +1 -1
  82. package/dist/ranking/mi/etch.d.ts +8 -0
  83. package/dist/ranking/mi/etch.d.ts.map +1 -1
  84. package/dist/ranking/mi/hub-promoted.d.ts +7 -0
  85. package/dist/ranking/mi/hub-promoted.d.ts.map +1 -1
  86. package/dist/ranking/mi/index.cjs +581 -0
  87. package/dist/ranking/mi/index.cjs.map +1 -0
  88. package/dist/ranking/mi/index.js +555 -0
  89. package/dist/ranking/mi/index.js.map +1 -0
  90. package/dist/ranking/mi/jaccard.d.ts +7 -0
  91. package/dist/ranking/mi/jaccard.d.ts.map +1 -1
  92. package/dist/ranking/mi/notch.d.ts +8 -0
  93. package/dist/ranking/mi/notch.d.ts.map +1 -1
  94. package/dist/ranking/mi/overlap-coefficient.d.ts +7 -0
  95. package/dist/ranking/mi/overlap-coefficient.d.ts.map +1 -1
  96. package/dist/ranking/mi/resource-allocation.d.ts +8 -0
  97. package/dist/ranking/mi/resource-allocation.d.ts.map +1 -1
  98. package/dist/ranking/mi/scale.d.ts +7 -0
  99. package/dist/ranking/mi/scale.d.ts.map +1 -1
  100. package/dist/ranking/mi/skew.d.ts +7 -0
  101. package/dist/ranking/mi/skew.d.ts.map +1 -1
  102. package/dist/ranking/mi/sorensen.d.ts +7 -0
  103. package/dist/ranking/mi/sorensen.d.ts.map +1 -1
  104. package/dist/ranking/mi/span.d.ts +8 -0
  105. package/dist/ranking/mi/span.d.ts.map +1 -1
  106. package/dist/ranking/mi/types.d.ts +12 -0
  107. package/dist/ranking/mi/types.d.ts.map +1 -1
  108. package/dist/ranking/parse.d.ts +24 -1
  109. package/dist/ranking/parse.d.ts.map +1 -1
  110. package/dist/ranking-mUm9rV-C.js +1016 -0
  111. package/dist/ranking-mUm9rV-C.js.map +1 -0
  112. package/dist/ranking-riRrEVAR.cjs +1093 -0
  113. package/dist/ranking-riRrEVAR.cjs.map +1 -0
  114. package/dist/seeds/index.cjs +1 -1
  115. package/dist/seeds/index.js +1 -1
  116. package/dist/structures/index.cjs +2 -143
  117. package/dist/structures/index.js +1 -142
  118. package/dist/utils/index.cjs +1 -1
  119. package/dist/utils/index.js +1 -1
  120. package/dist/utils-CcIrKAEb.js +22 -0
  121. package/dist/utils-CcIrKAEb.js.map +1 -0
  122. package/dist/utils-CpyzmzIF.cjs +33 -0
  123. package/dist/utils-CpyzmzIF.cjs.map +1 -0
  124. package/package.json +6 -1
  125. package/dist/graph/index.cjs.map +0 -1
  126. package/dist/graph/index.js.map +0 -1
  127. package/dist/index/index.cjs.map +0 -1
  128. package/dist/index/index.js.map +0 -1
  129. package/dist/structures/index.cjs.map +0 -1
  130. package/dist/structures/index.js.map +0 -1
package/README.md CHANGED
@@ -14,6 +14,7 @@ Low-dependency TypeScript graph algorithms for citation network analysis: novel
14
14
  - **Seed selection**: GRASP, Stratified
15
15
  - **Subgraph extraction**: ego-network, k-core, k-truss, motif, induced, filter
16
16
  - **Optional WebGPU acceleration**
17
+ - **Async support**: Generator coroutine protocol, sync/async runners, all algorithms available as `*Async` variants
17
18
 
18
19
  ## Installation
19
20
 
@@ -37,7 +38,7 @@ const ranked = parse(graph, result.paths, { mi: jaccard });
37
38
 
38
39
  ### Expansion: BASE Framework
39
40
 
40
- **Boundary-free Adaptive Seeded Expansion** (BASE) is a parameter-free graph expansion algorithm. Given a graph $G = (V, E)$ and seed nodes $S \subseteq V$, BASE produces the subgraph induced by all vertices visited during priority-ordered expansion until frontier exhaustion:
41
+ **Boundary-free Adaptive Seeded Expansion** (BASE) discovers the neighbourhood around seed nodes without any configuration. You provide seeds and a priority function; BASE expands outward, visiting the most interesting nodes first and recording paths when search frontiers from different seeds collide. It stops naturally when there is nothing left to explore — no depth limits, no size thresholds, no parameters to tune.
41
42
 
42
43
  $$G_S = (V_S, E_S) \quad \text{where} \quad V_S = \bigcup_{v \in S} \text{Expand}(v, \pi)$$
43
44
 
@@ -51,7 +52,7 @@ Three key properties:
51
52
 
52
53
  #### DOME: Degree-Ordered Multi-seed Expansion
53
54
 
54
- The default priority function uses degree-based hub deferral:
55
+ Explores low-connectivity nodes before hubs. In a social network, DOME visits niche specialists before reaching the well-connected influencers, discovering the quiet corners of the graph before the busy crossroads.
55
56
 
56
57
  $$\pi(v) = \frac{\deg^{+}(v) + \deg^{-}(v)}{w_V(v) + \varepsilon}$$
57
58
 
@@ -81,99 +82,117 @@ where $\deg^{+}(v)$ is weighted out-degree, $\deg^{-}(v)$ is weighted in-degree,
81
82
 
82
83
  #### EDGE: Entropy-Driven Graph Expansion
83
84
 
85
+ Finds nodes that sit at the boundary between different kinds of things. If a person's friends include scientists, artists, and engineers (high type diversity), EDGE visits them early — they are likely bridges between communities.
86
+
84
87
  $$\pi_{\text{EDGE}}(v) = \frac{1}{H_{\text{local}}(v) + \varepsilon} \times \log(\deg(v) + 1)$$
85
88
 
86
- where $H_{\text{local}}(v) = -\sum_{\tau} p(\tau) \log p(\tau)$ is the Shannon entropy of the neighbour type distribution. Nodes bridging heterogeneous structural regimes (high entropy) are explored first.
89
+ where $H_{\text{local}}(v) = -\sum_{\tau} p(\tau) \log p(\tau)$ is the Shannon entropy of the neighbour type distribution.
87
90
 
88
91
  ---
89
92
 
90
93
  #### PIPE: Path-potential Informed Priority Expansion
91
94
 
95
+ Rushes towards nodes that are about to connect two search frontiers. When expanding from multiple seeds, PIPE detects that a node's neighbours have already been reached by another seed's frontier — meaning a connecting path is one step away.
96
+
92
97
  $$\pi_{\text{PIPE}}(v) = \frac{\deg(v)}{1 + \mathrm{pathPotential}(v)}$$
93
98
 
94
- where $\mathrm{pathPotential}(v) = \lvert N(v) \cap \bigcup_{j \neq i} V_j \rvert$ counts neighbours already visited by other seed frontiers. High path potential indicates imminent path completion.
99
+ where $\mathrm{pathPotential}(v) = \lvert N(v) \cap \bigcup_{j \neq i} V_j \rvert$ counts neighbours already visited by other seed frontiers.
95
100
 
96
101
  ---
97
102
 
98
103
  #### SAGE: Salience-Accumulation Guided Expansion
99
104
 
105
+ Learns from its own discoveries. Phase 1 explores by degree (like DOME). Once the first path is found, SAGE switches to Phase 2: nodes that appear in many discovered paths get top priority, guiding expansion towards structurally rich regions.
106
+
100
107
  $$
101
108
  \pi_{\text{SAGE}}(v) = \begin{cases} \log(\deg(v) + 1) & \text{Phase 1 (before first path)} \\ -(\text{salience}(v) \times 1000 - \deg(v)) & \text{Phase 2 (after first path)} \end{cases}
102
109
  $$
103
110
 
104
- where $\text{salience}(v)$ counts discovered paths containing $v$. Salience dominates in Phase 2; degree serves as tiebreaker.
111
+ where $\text{salience}(v)$ counts discovered paths containing $v$.
105
112
 
106
113
  ---
107
114
 
108
115
  #### REACH: Retrospective Expansion with Adaptive Convergence
109
116
 
117
+ Uses the quality of already-discovered paths to steer future exploration. Phase 1 explores by degree. Once paths are found, REACH asks "which unexplored nodes look structurally similar to the endpoints of my best paths?" and prioritises those — seeking more of what already worked.
118
+
110
119
  $$
111
120
  \pi_{\text{REACH}}(v) = \begin{cases} \log(\deg(v) + 1) & \text{Phase 1} \\ \log(\deg(v) + 1) \times (1 - \widehat{\text{MI}}(v)) & \text{Phase 2} \end{cases}
112
121
  $$
113
122
 
114
- where $\widehat{\text{MI}}(v)$ estimates MI via Jaccard similarity to discovered path endpoints:
115
-
116
- $$
117
- \widehat{\text{MI}}(v) = \frac{1}{\lvert \mathcal{P}\_{\text{top}} \rvert} \sum\_{p} J(N(v), N(p\_{\text{endpoint}}))
118
- $$
123
+ where $\widehat{\text{MI}}(v) = \frac{1}{\lvert \mathcal{P}\_{\text{top}} \rvert} \sum\_{p} J(N(v), N(p\_{\text{endpoint}}))$ estimates MI via Jaccard similarity to discovered path endpoints.
119
124
 
120
125
  ---
121
126
 
122
127
  #### MAZE: Multi-frontier Adaptive Zone Expansion
123
128
 
129
+ Combines the best of PIPE and SAGE across three phases. First, it races to find initial paths using path potential (like PIPE). Then it refines exploration using salience feedback (like SAGE). Finally, it decides when to stop based on whether it's still discovering diverse, high-quality paths.
130
+
124
131
  $$
125
132
  \pi^{(1)}(v) = \frac{\deg(v)}{1 + \mathrm{pathPotential}(v)} \qquad \pi^{(2)}(v) = \pi^{(1)}(v) \times \frac{1}{1 + \lambda \cdot \text{salience}(v)}
126
133
  $$
127
134
 
128
- Phase 1 uses PIPE's path potential until $M$ paths found. Phase 2 incorporates SAGE's salience feedback. Phase 3 evaluates diversity, path count, and salience plateau for termination.
135
+ Phase 1 uses path potential until $M$ paths found. Phase 2 adds salience feedback. Phase 3 evaluates diversity, path count, and salience plateau for termination.
129
136
 
130
137
  ---
131
138
 
132
139
  #### TIDE: Total Interconnected Degree Expansion
133
140
 
141
+ Avoids dense clusters by looking at total neighbourhood connectivity. A node surrounded by other well-connected nodes gets deferred; a node in a quiet corner of the graph gets explored first.
142
+
134
143
  $$\pi_{\text{TIDE}}(v) = \deg(v) + \sum_{w \in N(v)} \deg(w)$$
135
144
 
136
- Nodes in sparse regions (low aggregate neighbourhood degree) are explored first. Related to EDGE but uses raw degree sums rather than entropy.
145
+ Related to EDGE but uses raw degree sums rather than entropy.
137
146
 
138
147
  ---
139
148
 
140
149
  #### LACE: Local Affinity-Computed Expansion
141
150
 
151
+ Explores towards nodes that are most similar to what the frontier has already seen. If a candidate node shares many neighbours with the explored region, it gets priority — building outward from a coherent core.
152
+
142
153
  $$\pi_{\text{LACE}}(v) = 1 - \overline{\text{MI}}(v, \text{frontier})$$
143
154
 
144
- Prioritises nodes by average MI to already-visited frontier nodes. Related to HAE but uses MI to visited nodes rather than type entropy.
155
+ Related to HAE but uses MI to visited nodes rather than type entropy.
145
156
 
146
157
  ---
147
158
 
148
159
  #### WARP: Weighted Adjacent Reachability Priority
149
160
 
161
+ Aggressively prioritises nodes that look like they will connect two search frontiers, regardless of their degree. If a node's neighbours have been visited by another seed's search, it gets top priority.
162
+
150
163
  $$\pi_{\text{WARP}}(v) = \frac{1}{1 + \text{bridge}(v)}$$
151
164
 
152
- Pure cross-frontier bridge score without degree normalisation. Related to PIPE but omits the degree numerator.
165
+ Related to PIPE but omits the degree numerator, making it more aggressive at prioritising bridge nodes.
153
166
 
154
167
  ---
155
168
 
156
169
  #### FUSE: Fused Utility-Salience Expansion
157
170
 
171
+ Balances two signals simultaneously: how connected a node is (degree) and how strongly it relates to the explored region (MI). The weight $w$ controls the trade-off — at $w=0$ it behaves like DOME, at $w=1$ it behaves like LACE.
172
+
158
173
  $$\pi_{\text{FUSE}}(v) = (1 - w) \cdot \deg(v) + w \cdot (1 - \overline{\text{MI}})$$
159
174
 
160
- Single-phase weighted blend of degree and MI. Related to SAGE but uses continuous blending rather than two-phase transition.
175
+ Related to SAGE but uses continuous blending rather than two-phase transition.
161
176
 
162
177
  ---
163
178
 
164
179
  #### SIFT: Salience-Informed Frontier Threshold
165
180
 
181
+ Acts as a gate: nodes with MI above a threshold get MI-based priority (explore the promising ones); nodes below the threshold get deferred with a large degree-based penalty (ignore the unpromising ones). A binary version of REACH's continuous approach.
182
+
166
183
  $$
167
184
  \pi_{\text{SIFT}}(v) = \begin{cases} 1 - \overline{\text{MI}} & \text{if } \overline{\text{MI}} \geq \tau \\ \deg(v) + 100 & \text{otherwise} \end{cases}
168
185
  $$
169
186
 
170
- MI-threshold-based priority with degree fallback. Related to REACH but uses a hard threshold instead of continuous MI-weighted priority.
187
+ Related to REACH but uses a hard threshold instead of continuous MI-weighted priority.
171
188
 
172
189
  ---
173
190
 
174
191
  #### FLUX: Flexible Local Utility Crossover
175
192
 
176
- Density-adaptive strategy switching. Selects between DOME, EDGE, and PIPE modes per-node based on local graph density and cross-frontier bridge score. Related to MAZE but adapts spatially (per-node) rather than temporally (per-phase).
193
+ Adapts its strategy to the local topology of each node. In dense regions it uses low-degree-first exploration (like EDGE); near frontier boundaries it uses bridge detection (like PIPE); in sparse regions it falls back to degree ordering (like DOME). Different parts of the graph are explored with different strategies simultaneously.
194
+
195
+ Related to MAZE but adapts spatially (per-node) rather than temporally (per-phase).
177
196
 
178
197
  ---
179
198
 
@@ -192,73 +211,83 @@ Density-adaptive strategy switching. Selects between DOME, EDGE, and PIPE modes
192
211
 
193
212
  ### Path Ranking: PARSE
194
213
 
195
- **Path Aggregation Ranked by Salience Estimation** (PARSE) scores paths by the geometric mean of per-edge mutual information, eliminating length bias:
214
+ **Path Aggregation Ranked by Salience Estimation** (PARSE) ranks discovered paths by asking "how consistently strong is every edge along this path?" It uses the geometric mean of per-edge MI scores, which means one weak link drags down the entire path — unlike arithmetic mean where a strong edge can compensate for a weak one. A 10-hop path with consistently good edges scores the same as a 2-hop path with equally good edges.
196
215
 
197
216
  $$M(P) = \exp\left( \frac{1}{k} \sum_{i=1}^{k} \log I(u_i, v_i) \right)$$
198
217
 
199
- where $k$ is path length (number of edges) and $I(u_i, v_i)$ is the per-edge MI score from any variant below. The geometric mean ensures a 10-hop path with consistently high-MI edges scores equally to a 2-hop path with the same average MI.
218
+ where $k$ is path length (number of edges) and $I(u_i, v_i)$ is the per-edge MI score from any variant below.
200
219
 
201
220
  ---
202
221
 
203
222
  ### MI Variants
204
223
 
205
- Seven MI variants serve as per-edge estimators within PARSE. All build on Jaccard neighbourhood overlap, then weight by domain-specific structural properties.
224
+ MI variants answer the question "how strongly are two connected nodes related?" Each measures the overlap between their neighbourhoods, then optionally weights by structural properties like density, degree rarity, clustering, or entity type. PARSE uses these as per-edge scores in its geometric mean.
206
225
 
207
226
  ---
208
227
 
209
228
  #### Jaccard (baseline)
210
229
 
211
- $$I_{\text{Jac}}(u, v) = \frac{|N(u) \cap N(v)|}{|N(u) \cup N(v)|}$$
230
+ What fraction of combined neighbours do two nodes share? If Alice and Bob know 3 of the same people out of 10 total acquaintances between them, their Jaccard score is 0.3.
212
231
 
213
- Standard neighbourhood overlap. Default MI estimator.
232
+ $$I_{\text{Jac}}(u, v) = \frac{|N(u) \cap N(v)|}{|N(u) \cup N(v)|}$$
214
233
 
215
234
  ---
216
235
 
217
236
  #### Adamic-Adar
218
237
 
219
- $$I_{\text{AA}}(u, v) = \sum_{w \in N(u) \cap N(v)} \frac{1}{\log(\deg(w) + 1)}$$
238
+ Counts shared neighbours, but recognises that sharing a rare connection is more meaningful than sharing a popular one. If two researchers both cite a niche paper, that says more about their relationship than both citing a famous textbook.
220
239
 
221
- Downweights common neighbours with high degree. Shared hub neighbours are less informative than shared rare neighbours.
240
+ $$I_{\text{AA}}(u, v) = \sum_{w \in N(u) \cap N(v)} \frac{1}{\log(\deg(w) + 1)}$$
222
241
 
223
242
  ---
224
243
 
225
244
  #### SCALE: Structural Correction via Adjusted Local Estimation
226
245
 
246
+ Adjusts for graph density. In a dense network where everyone knows everyone, sharing neighbours is expected and less meaningful. In a sparse network, the same overlap is rare and significant. SCALE divides Jaccard by density to make scores comparable across differently-dense regions.
247
+
227
248
  $$I_{\text{SCALE}}(u, v) = \frac{J(N(u), N(v))}{\rho(G)}$$
228
249
 
229
- where $\rho(G) = \frac{2|E|}{|V|(|V|-1)}$ is graph density. Normalises Jaccard by density so that overlap in dense subgraphs is not artificially inflated.
250
+ where $\rho(G) = \frac{2|E|}{|V|(|V|-1)}$ is graph density.
230
251
 
231
252
  ---
232
253
 
233
254
  #### SKEW: Sparse-weighted Knowledge Emphasis Weighting
234
255
 
256
+ Rewards edges between rare (low-degree) nodes and penalises edges involving hubs. Like TF-IDF in search engines: a connection between two niche nodes is more informative than a connection between two mega-hubs that connect to everything.
257
+
235
258
  $$I_{\text{SKEW}}(u, v) = J(N(u), N(v)) \cdot \log\!\left(\frac{N}{\deg(u) + 1}\right) \cdot \log\!\left(\frac{N}{\deg(v) + 1}\right)$$
236
259
 
237
- where $N = |V|$. IDF-style rarity weighting on both endpoints. Paths through low-degree (rare) nodes score higher; paths through hubs score lower.
260
+ where $N = |V|$.
238
261
 
239
262
  ---
240
263
 
241
264
  #### SPAN: Spanning-community Penalty for Adjacent Nodes
242
265
 
266
+ Rewards edges that bridge separate communities and penalises edges within tight-knit groups. If both endpoints sit in dense clusters where everyone knows everyone (high clustering coefficient), the edge is probably redundant. If at least one endpoint is a bridge between groups, the edge is structurally interesting.
267
+
243
268
  $$I_{\text{SPAN}}(u, v) = J(N(u), N(v)) \cdot \bigl(1 - \max(C(u), C(v))\bigr)$$
244
269
 
245
- where $C(v)$ is the local clustering coefficient. Penalises edges within tight clusters; rewards edges bridging communities (structural holes).
270
+ where $C(v)$ is the local clustering coefficient.
246
271
 
247
272
  ---
248
273
 
249
274
  #### ETCH: Edge Type Contrast Heuristic
250
275
 
276
+ Boosts edges of rare types. If a graph has 1000 "knows" edges but only 5 "mentors" edges, a mentoring relationship is worth more than an acquaintanceship. ETCH multiplies Jaccard by the log-rarity of the edge type.
277
+
251
278
  $$I_{\text{ETCH}}(u, v) = J(N(u), N(v)) \cdot \log\!\left(\frac{|E|}{\text{count}(\text{edges with type}(u,v))}\right)$$
252
279
 
253
- Weights Jaccard by edge-type rarity. Paths traversing rare edge types receive higher scores. Requires edge-type annotations; falls back to Jaccard when unavailable.
280
+ Requires edge-type annotations; falls back to Jaccard when unavailable.
254
281
 
255
282
  ---
256
283
 
257
284
  #### NOTCH: Node Type Contrast Heuristic
258
285
 
286
+ Boosts edges connecting rare node types. In a graph with 500 people but only 10 organisations, an edge involving an organisation is more distinctive. NOTCH multiplies Jaccard by the log-rarity of both endpoint types.
287
+
259
288
  $$I_{\text{NOTCH}}(u, v) = J(N(u), N(v)) \cdot \log\!\left(\frac{|V|}{c(\tau_u)}\right) \cdot \log\!\left(\frac{|V|}{c(\tau_v)}\right)$$
260
289
 
261
- where $c(\tau_u)$ is the count of nodes with the same type as $u$. Weights Jaccard by node-type rarity for both endpoints.
290
+ where $c(\tau_u)$ is the count of nodes with the same type as $u$.
262
291
 
263
292
  ---
264
293
 
@@ -296,7 +325,7 @@ where $c(\tau_u)$ is the count of nodes with the same type as $u$. Weights Jacca
296
325
 
297
326
  ### Seed Selection: GRASP
298
327
 
299
- **Graph-agnostic Representative seed pAir Sampling**: selects structurally representative seed pairs from an unknown graph using reservoir sampling and structural feature clustering. Operates blind: no full graph loading, no ground-truth labels, no human-defined strata.
328
+ **Graph-agnostic Representative seed pAir Sampling** picks starting points for expansion algorithms. Given a graph you have never seen before, GRASP streams through its edges, samples a representative set of nodes, clusters them by structural role (hubs, bridges, peripherals), and returns seed pairs that cover the full range of structural diversity — without loading the entire graph into memory.
300
329
 
301
330
  Three phases:
302
331
 
@@ -323,6 +352,28 @@ import { ... } from 'graphwise/extraction'; // Subgraph extraction
323
352
  import { ... } from 'graphwise/utils'; // Utilities
324
353
  import { ... } from 'graphwise/gpu'; // WebGPU acceleration
325
354
  import { ... } from 'graphwise/schemas'; // Zod schemas
355
+ import { ... } from 'graphwise/async'; // Async runners & protocol
356
+ ```
357
+
358
+ ### Async Usage
359
+
360
+ All algorithms are available as `*Async` variants for use with remote or lazy graph data sources:
361
+
362
+ ```typescript
363
+ import { domeAsync, parseAsync, jaccardAsync } from "graphwise";
364
+ import type { AsyncReadableGraph } from "graphwise/graph";
365
+
366
+ // Your async graph implementation
367
+ const remoteGraph: AsyncReadableGraph = createRemoteGraph();
368
+
369
+ const result = await domeAsync(remoteGraph, seeds, {
370
+ signal: controller.signal,
371
+ onProgress: (stats) => console.log(stats),
372
+ });
373
+
374
+ const ranked = await parseAsync(remoteGraph, result.paths, {
375
+ mi: jaccardAsync,
376
+ });
326
377
  ```
327
378
 
328
379
  ## Commands
@@ -0,0 +1,234 @@
1
+ //#region src/graph/adjacency-map.ts
2
+ /**
3
+ * Graph implementation using adjacency map data structure.
4
+ *
5
+ * Uses Map<NodeId, N> for node storage and Map<NodeId, Map<NodeId, E>>
6
+ * for adjacency representation. This provides O(1) average-case lookup
7
+ * for nodes and edges, with memory proportional to V + E.
8
+ *
9
+ * @typeParam N - Node data type, must extend NodeData
10
+ * @typeParam E - Edge data type, must extend EdgeData
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // Create a directed citation graph
15
+ * const graph = AdjacencyMapGraph.directed<AuthorNode, CitationEdge>()
16
+ * .addNode({ id: 'A1', name: 'Alice' })
17
+ * .addNode({ id: 'B1', name: 'Bob' })
18
+ * .addEdge({ source: 'A1', target: 'B1', year: 2024 });
19
+ * ```
20
+ */
21
+ var AdjacencyMapGraph = class AdjacencyMapGraph {
22
+ directed;
23
+ nodes;
24
+ adjacency;
25
+ reverseAdjacency;
26
+ _edgeCount;
27
+ constructor(directed) {
28
+ this.directed = directed;
29
+ this.nodes = /* @__PURE__ */ new Map();
30
+ this.adjacency = /* @__PURE__ */ new Map();
31
+ this.reverseAdjacency = directed ? /* @__PURE__ */ new Map() : null;
32
+ this._edgeCount = 0;
33
+ }
34
+ /**
35
+ * Create a new directed graph.
36
+ *
37
+ * In a directed graph, edges have direction from source to target.
38
+ * The `neighbours` method with direction 'out' returns successors,
39
+ * and direction 'in' returns predecessors.
40
+ *
41
+ * @typeParam N - Node data type
42
+ * @typeParam E - Edge data type
43
+ * @returns A new empty directed graph
44
+ */
45
+ static directed() {
46
+ return new AdjacencyMapGraph(true);
47
+ }
48
+ /**
49
+ * Create a new undirected graph.
50
+ *
51
+ * In an undirected graph, edges have no direction. Adding an edge
52
+ * from A to B automatically creates the connection from B to A.
53
+ *
54
+ * @typeParam N - Node data type
55
+ * @typeParam E - Edge data type
56
+ * @returns A new empty undirected graph
57
+ */
58
+ static undirected() {
59
+ return new AdjacencyMapGraph(false);
60
+ }
61
+ get nodeCount() {
62
+ return this.nodes.size;
63
+ }
64
+ get edgeCount() {
65
+ return this._edgeCount;
66
+ }
67
+ hasNode(id) {
68
+ return this.nodes.has(id);
69
+ }
70
+ getNode(id) {
71
+ return this.nodes.get(id);
72
+ }
73
+ /**
74
+ * Iterate over all node identifiers in the graph.
75
+ *
76
+ * @returns An iterable of all node IDs
77
+ */
78
+ *nodeIds() {
79
+ yield* this.nodes.keys();
80
+ }
81
+ neighbours(id, direction = "out") {
82
+ if (!this.nodes.has(id)) return [];
83
+ if (this.directed) {
84
+ if (direction === "out") return this.adjacency.get(id)?.keys() ?? [];
85
+ if (direction === "in") return this.reverseAdjacency?.get(id)?.keys() ?? [];
86
+ return this.iterateBothDirections(id);
87
+ }
88
+ return this.adjacency.get(id)?.keys() ?? [];
89
+ }
90
+ *iterateBothDirections(id) {
91
+ const seen = /* @__PURE__ */ new Set();
92
+ const outNeighbours = this.adjacency.get(id);
93
+ if (outNeighbours !== void 0) {
94
+ for (const neighbour of outNeighbours.keys()) if (!seen.has(neighbour)) {
95
+ seen.add(neighbour);
96
+ yield neighbour;
97
+ }
98
+ }
99
+ const inNeighbours = this.reverseAdjacency?.get(id);
100
+ if (inNeighbours !== void 0) {
101
+ for (const neighbour of inNeighbours.keys()) if (!seen.has(neighbour)) {
102
+ seen.add(neighbour);
103
+ yield neighbour;
104
+ }
105
+ }
106
+ }
107
+ degree(id, direction = "out") {
108
+ if (!this.nodes.has(id)) return 0;
109
+ if (this.directed) {
110
+ if (direction === "out") return this.adjacency.get(id)?.size ?? 0;
111
+ if (direction === "in") return this.reverseAdjacency?.get(id)?.size ?? 0;
112
+ return (this.adjacency.get(id)?.size ?? 0) + (this.reverseAdjacency?.get(id)?.size ?? 0);
113
+ }
114
+ return this.adjacency.get(id)?.size ?? 0;
115
+ }
116
+ getEdge(source, target) {
117
+ const forward = this.adjacency.get(source)?.get(target);
118
+ if (forward !== void 0) return forward;
119
+ if (!this.directed) return this.adjacency.get(target)?.get(source);
120
+ }
121
+ *edges() {
122
+ const emitted = /* @__PURE__ */ new Set();
123
+ for (const [, neighbours] of this.adjacency) for (const [, edge] of neighbours) if (this.directed) yield edge;
124
+ else {
125
+ const key = this.edgeKey(edge.source, edge.target);
126
+ if (!emitted.has(key)) {
127
+ emitted.add(key);
128
+ yield edge;
129
+ }
130
+ }
131
+ }
132
+ edgeKey(source, target) {
133
+ const [a, b] = source < target ? [source, target] : [target, source];
134
+ return `${a}::${b}`;
135
+ }
136
+ /**
137
+ * Add a node to the graph (builder pattern).
138
+ *
139
+ * If a node with the same ID already exists, it is not replaced.
140
+ *
141
+ * @param node - The node data to add
142
+ * @returns this (for method chaining)
143
+ */
144
+ addNode(node) {
145
+ if (!this.nodes.has(node.id)) this.nodes.set(node.id, node);
146
+ return this;
147
+ }
148
+ /**
149
+ * Add an edge to the graph (builder pattern).
150
+ *
151
+ * @param edge - The edge data to add
152
+ * @returns this (for method chaining)
153
+ * @throws Error if either endpoint node does not exist
154
+ */
155
+ addEdge(edge) {
156
+ if (!this.nodes.has(edge.source) || !this.nodes.has(edge.target)) throw new Error(`Cannot add edge: nodes ${edge.source} and/or ${edge.target} do not exist`);
157
+ if (!this.directed) {
158
+ const [cSource, cTarget] = edge.source < edge.target ? [edge.source, edge.target] : [edge.target, edge.source];
159
+ if (this.adjacency.get(cSource)?.get(cTarget) !== void 0) {
160
+ this.adjacency.get(cSource)?.set(cTarget, edge);
161
+ return this;
162
+ }
163
+ }
164
+ let forwardMap = this.adjacency.get(edge.source);
165
+ if (forwardMap === void 0) {
166
+ forwardMap = /* @__PURE__ */ new Map();
167
+ this.adjacency.set(edge.source, forwardMap);
168
+ }
169
+ const isNewEdge = !forwardMap.has(edge.target);
170
+ forwardMap.set(edge.target, edge);
171
+ if (this.directed) {
172
+ let reverseMap = this.reverseAdjacency?.get(edge.target);
173
+ if (reverseMap === void 0) {
174
+ reverseMap = /* @__PURE__ */ new Map();
175
+ this.reverseAdjacency?.set(edge.target, reverseMap);
176
+ }
177
+ reverseMap.set(edge.source, edge);
178
+ } else {
179
+ let reverseMap = this.adjacency.get(edge.target);
180
+ if (reverseMap === void 0) {
181
+ reverseMap = /* @__PURE__ */ new Map();
182
+ this.adjacency.set(edge.target, reverseMap);
183
+ }
184
+ reverseMap.set(edge.source, edge);
185
+ }
186
+ if (isNewEdge) this._edgeCount++;
187
+ return this;
188
+ }
189
+ removeNode(id) {
190
+ if (!this.nodes.has(id)) return false;
191
+ const outNeighbours = [...this.adjacency.get(id)?.keys() ?? []];
192
+ for (const neighbour of outNeighbours) this.removeEdgeInternal(id, neighbour);
193
+ if (this.directed && this.reverseAdjacency !== null) {
194
+ const inNeighbours = [...this.reverseAdjacency.get(id)?.keys() ?? []];
195
+ for (const neighbour of inNeighbours) this.removeEdgeFromDirected(neighbour, id);
196
+ }
197
+ this.nodes.delete(id);
198
+ this.adjacency.delete(id);
199
+ this.reverseAdjacency?.delete(id);
200
+ return true;
201
+ }
202
+ /**
203
+ * Remove an edge from a directed graph, updating both adjacency maps.
204
+ * This handles the case where we're removing an edge that points TO the removed node.
205
+ */
206
+ removeEdgeFromDirected(source, target) {
207
+ if (this.adjacency.get(source)?.delete(target) === true) this._edgeCount--;
208
+ this.reverseAdjacency?.get(target)?.delete(source);
209
+ }
210
+ removeEdgeInternal(source, target) {
211
+ if (this.adjacency.get(source)?.delete(target) === true) this._edgeCount--;
212
+ if (this.directed) this.reverseAdjacency?.get(target)?.delete(source);
213
+ else this.adjacency.get(target)?.delete(source);
214
+ }
215
+ removeEdge(source, target) {
216
+ if (!this.hasEdgeInternal(source, target)) return false;
217
+ this.removeEdgeInternal(source, target);
218
+ return true;
219
+ }
220
+ hasEdgeInternal(source, target) {
221
+ if (this.adjacency.get(source)?.has(target) === true) return true;
222
+ if (!this.directed) return this.adjacency.get(target)?.has(source) === true;
223
+ return false;
224
+ }
225
+ };
226
+ //#endregion
227
+ Object.defineProperty(exports, "AdjacencyMapGraph", {
228
+ enumerable: true,
229
+ get: function() {
230
+ return AdjacencyMapGraph;
231
+ }
232
+ });
233
+
234
+ //# sourceMappingURL=adjacency-map-B6wPtmaq.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adjacency-map-B6wPtmaq.cjs","names":[],"sources":["../src/graph/adjacency-map.ts"],"sourcesContent":["/**\n * Adjacency map graph implementation.\n *\n * This module provides a flexible graph implementation using nested Maps\n * for efficient adjacency list representation. It supports both directed\n * and undirected graphs with builder pattern for convenient construction.\n */\n\nimport type { NodeId, NodeData, EdgeData, Direction } from \"./types\";\nimport type { MutableGraph } from \"./interfaces\";\n\n/**\n * Graph implementation using adjacency map data structure.\n *\n * Uses Map<NodeId, N> for node storage and Map<NodeId, Map<NodeId, E>>\n * for adjacency representation. This provides O(1) average-case lookup\n * for nodes and edges, with memory proportional to V + E.\n *\n * @typeParam N - Node data type, must extend NodeData\n * @typeParam E - Edge data type, must extend EdgeData\n *\n * @example\n * ```typescript\n * // Create a directed citation graph\n * const graph = AdjacencyMapGraph.directed<AuthorNode, CitationEdge>()\n * .addNode({ id: 'A1', name: 'Alice' })\n * .addNode({ id: 'B1', name: 'Bob' })\n * .addEdge({ source: 'A1', target: 'B1', year: 2024 });\n * ```\n */\nexport class AdjacencyMapGraph<\n\tN extends NodeData = NodeData,\n\tE extends EdgeData = EdgeData,\n> implements MutableGraph<N, E> {\n\treadonly directed: boolean;\n\n\tprivate readonly nodes: Map<NodeId, N>;\n\tprivate readonly adjacency: Map<NodeId, Map<NodeId, E>>;\n\tprivate readonly reverseAdjacency: Map<NodeId, Map<NodeId, E>> | null;\n\tprivate _edgeCount: number;\n\n\tprivate constructor(directed: boolean) {\n\t\tthis.directed = directed;\n\t\tthis.nodes = new Map();\n\t\tthis.adjacency = new Map();\n\t\tthis.reverseAdjacency = directed ? new Map() : null;\n\t\tthis._edgeCount = 0;\n\t}\n\n\t/**\n\t * Create a new directed graph.\n\t *\n\t * In a directed graph, edges have direction from source to target.\n\t * The `neighbours` method with direction 'out' returns successors,\n\t * and direction 'in' returns predecessors.\n\t *\n\t * @typeParam N - Node data type\n\t * @typeParam E - Edge data type\n\t * @returns A new empty directed graph\n\t */\n\tstatic directed<\n\t\tN extends NodeData = NodeData,\n\t\tE extends EdgeData = EdgeData,\n\t>(): AdjacencyMapGraph<N, E> {\n\t\treturn new AdjacencyMapGraph<N, E>(true);\n\t}\n\n\t/**\n\t * Create a new undirected graph.\n\t *\n\t * In an undirected graph, edges have no direction. Adding an edge\n\t * from A to B automatically creates the connection from B to A.\n\t *\n\t * @typeParam N - Node data type\n\t * @typeParam E - Edge data type\n\t * @returns A new empty undirected graph\n\t */\n\tstatic undirected<\n\t\tN extends NodeData = NodeData,\n\t\tE extends EdgeData = EdgeData,\n\t>(): AdjacencyMapGraph<N, E> {\n\t\treturn new AdjacencyMapGraph<N, E>(false);\n\t}\n\n\tget nodeCount(): number {\n\t\treturn this.nodes.size;\n\t}\n\n\tget edgeCount(): number {\n\t\treturn this._edgeCount;\n\t}\n\n\thasNode(id: NodeId): boolean {\n\t\treturn this.nodes.has(id);\n\t}\n\n\tgetNode(id: NodeId): N | undefined {\n\t\treturn this.nodes.get(id);\n\t}\n\n\t/**\n\t * Iterate over all node identifiers in the graph.\n\t *\n\t * @returns An iterable of all node IDs\n\t */\n\t*nodeIds(): Iterable<NodeId> {\n\t\tyield* this.nodes.keys();\n\t}\n\n\tneighbours(id: NodeId, direction: Direction = \"out\"): Iterable<NodeId> {\n\t\tif (!this.nodes.has(id)) {\n\t\t\treturn [];\n\t\t}\n\n\t\tif (this.directed) {\n\t\t\tif (direction === \"out\") {\n\t\t\t\treturn this.adjacency.get(id)?.keys() ?? [];\n\t\t\t}\n\t\t\tif (direction === \"in\") {\n\t\t\t\treturn this.reverseAdjacency?.get(id)?.keys() ?? [];\n\t\t\t}\n\t\t\t// direction === 'both'\n\t\t\treturn this.iterateBothDirections(id);\n\t\t}\n\n\t\t// Undirected: all neighbours are in adjacency\n\t\treturn this.adjacency.get(id)?.keys() ?? [];\n\t}\n\n\tprivate *iterateBothDirections(id: NodeId): Iterable<NodeId> {\n\t\tconst seen = new Set<NodeId>();\n\n\t\t// Yield outgoing neighbours\n\t\tconst outNeighbours = this.adjacency.get(id);\n\t\tif (outNeighbours !== undefined) {\n\t\t\tfor (const neighbour of outNeighbours.keys()) {\n\t\t\t\tif (!seen.has(neighbour)) {\n\t\t\t\t\tseen.add(neighbour);\n\t\t\t\t\tyield neighbour;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Yield incoming neighbours\n\t\tconst inNeighbours = this.reverseAdjacency?.get(id);\n\t\tif (inNeighbours !== undefined) {\n\t\t\tfor (const neighbour of inNeighbours.keys()) {\n\t\t\t\tif (!seen.has(neighbour)) {\n\t\t\t\t\tseen.add(neighbour);\n\t\t\t\t\tyield neighbour;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tdegree(id: NodeId, direction: Direction = \"out\"): number {\n\t\tif (!this.nodes.has(id)) {\n\t\t\treturn 0;\n\t\t}\n\n\t\tif (this.directed) {\n\t\t\tif (direction === \"out\") {\n\t\t\t\treturn this.adjacency.get(id)?.size ?? 0;\n\t\t\t}\n\t\t\tif (direction === \"in\") {\n\t\t\t\treturn this.reverseAdjacency?.get(id)?.size ?? 0;\n\t\t\t}\n\t\t\t// direction === 'both': count unique neighbours\n\t\t\tconst outSize = this.adjacency.get(id)?.size ?? 0;\n\t\t\tconst inSize = this.reverseAdjacency?.get(id)?.size ?? 0;\n\t\t\t// Simple sum is sufficient as edges are stored separately\n\t\t\treturn outSize + inSize;\n\t\t}\n\n\t\t// Undirected\n\t\treturn this.adjacency.get(id)?.size ?? 0;\n\t}\n\n\tgetEdge(source: NodeId, target: NodeId): E | undefined {\n\t\t// For undirected, try both orders\n\t\tconst forward = this.adjacency.get(source)?.get(target);\n\t\tif (forward !== undefined) {\n\t\t\treturn forward;\n\t\t}\n\n\t\tif (!this.directed) {\n\t\t\treturn this.adjacency.get(target)?.get(source);\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t*edges(): Iterable<E> {\n\t\tconst emitted = new Set<string>();\n\n\t\tfor (const [, neighbours] of this.adjacency) {\n\t\t\tfor (const [, edge] of neighbours) {\n\t\t\t\tif (this.directed) {\n\t\t\t\t\tyield edge;\n\t\t\t\t} else {\n\t\t\t\t\t// For undirected, avoid emitting duplicate edges\n\t\t\t\t\tconst key = this.edgeKey(edge.source, edge.target);\n\t\t\t\t\tif (!emitted.has(key)) {\n\t\t\t\t\t\temitted.add(key);\n\t\t\t\t\t\tyield edge;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate edgeKey(source: NodeId, target: NodeId): string {\n\t\t// Create a canonical key for undirected edges\n\t\tconst [a, b] = source < target ? [source, target] : [target, source];\n\t\treturn `${a}::${b}`;\n\t}\n\n\t/**\n\t * Add a node to the graph (builder pattern).\n\t *\n\t * If a node with the same ID already exists, it is not replaced.\n\t *\n\t * @param node - The node data to add\n\t * @returns this (for method chaining)\n\t */\n\taddNode(node: N): this {\n\t\tif (!this.nodes.has(node.id)) {\n\t\t\tthis.nodes.set(node.id, node);\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Add an edge to the graph (builder pattern).\n\t *\n\t * @param edge - The edge data to add\n\t * @returns this (for method chaining)\n\t * @throws Error if either endpoint node does not exist\n\t */\n\taddEdge(edge: E): this {\n\t\t// Ensure both nodes exist\n\t\tif (!this.nodes.has(edge.source) || !this.nodes.has(edge.target)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Cannot add edge: nodes ${edge.source} and/or ${edge.target} do not exist`,\n\t\t\t);\n\t\t}\n\n\t\tif (!this.directed) {\n\t\t\t// Canonical direction: source < target (prevents duplicate storage)\n\t\t\tconst [cSource, cTarget] =\n\t\t\t\tedge.source < edge.target\n\t\t\t\t\t? [edge.source, edge.target]\n\t\t\t\t\t: [edge.target, edge.source];\n\t\t\t// Check if edge already exists before incrementing edgeCount\n\t\t\tconst existingEdge = this.adjacency.get(cSource)?.get(cTarget);\n\t\t\tif (existingEdge !== undefined) {\n\t\t\t\t// Edge already exists — update data but don't increment count\n\t\t\t\tthis.adjacency.get(cSource)?.set(cTarget, edge);\n\t\t\t\treturn this;\n\t\t\t}\n\t\t}\n\n\t\t// Store in forward adjacency\n\t\tlet forwardMap = this.adjacency.get(edge.source);\n\t\tif (forwardMap === undefined) {\n\t\t\tforwardMap = new Map();\n\t\t\tthis.adjacency.set(edge.source, forwardMap);\n\t\t}\n\n\t\tconst isNewEdge = !forwardMap.has(edge.target);\n\t\tforwardMap.set(edge.target, edge);\n\n\t\tif (this.directed) {\n\t\t\t// Store reverse reference for efficient predecessor lookup\n\t\t\tlet reverseMap = this.reverseAdjacency?.get(edge.target);\n\t\t\tif (reverseMap === undefined) {\n\t\t\t\treverseMap = new Map();\n\t\t\t\tthis.reverseAdjacency?.set(edge.target, reverseMap);\n\t\t\t}\n\t\t\treverseMap.set(edge.source, edge);\n\t\t} else {\n\t\t\t// For undirected, also store in reverse direction\n\t\t\tlet reverseMap = this.adjacency.get(edge.target);\n\t\t\tif (reverseMap === undefined) {\n\t\t\t\treverseMap = new Map();\n\t\t\t\tthis.adjacency.set(edge.target, reverseMap);\n\t\t\t}\n\t\t\treverseMap.set(edge.source, edge);\n\t\t}\n\n\t\tif (isNewEdge) {\n\t\t\tthis._edgeCount++;\n\t\t}\n\t\treturn this;\n\t}\n\n\tremoveNode(id: NodeId): boolean {\n\t\tif (!this.nodes.has(id)) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Remove all outgoing edges from this node\n\t\tconst outNeighbours = [...(this.adjacency.get(id)?.keys() ?? [])];\n\t\tfor (const neighbour of outNeighbours) {\n\t\t\tthis.removeEdgeInternal(id, neighbour);\n\t\t}\n\n\t\t// For directed graphs, also remove incoming edges to this node\n\t\tif (this.directed && this.reverseAdjacency !== null) {\n\t\t\tconst inNeighbours = [...(this.reverseAdjacency.get(id)?.keys() ?? [])];\n\t\t\tfor (const neighbour of inNeighbours) {\n\t\t\t\t// Remove the edge from neighbour -> id\n\t\t\t\tthis.removeEdgeFromDirected(neighbour, id);\n\t\t\t}\n\t\t}\n\n\t\t// Remove the node itself\n\t\tthis.nodes.delete(id);\n\t\tthis.adjacency.delete(id);\n\t\tthis.reverseAdjacency?.delete(id);\n\n\t\treturn true;\n\t}\n\n\t/**\n\t * Remove an edge from a directed graph, updating both adjacency maps.\n\t * This handles the case where we're removing an edge that points TO the removed node.\n\t */\n\tprivate removeEdgeFromDirected(source: NodeId, target: NodeId): void {\n\t\t// Remove from forward adjacency (source -> target)\n\t\tconst forwardMap = this.adjacency.get(source);\n\t\tif (forwardMap?.delete(target) === true) {\n\t\t\tthis._edgeCount--;\n\t\t}\n\n\t\t// Remove from reverse adjacency\n\t\tthis.reverseAdjacency?.get(target)?.delete(source);\n\t}\n\n\tprivate removeEdgeInternal(source: NodeId, target: NodeId): void {\n\t\t// Remove from forward adjacency\n\t\tconst forwardMap = this.adjacency.get(source);\n\t\tif (forwardMap?.delete(target) === true) {\n\t\t\tthis._edgeCount--;\n\t\t}\n\n\t\tif (this.directed) {\n\t\t\t// Remove from reverse adjacency\n\t\t\tthis.reverseAdjacency?.get(target)?.delete(source);\n\t\t} else {\n\t\t\t// For undirected, remove both directions\n\t\t\tthis.adjacency.get(target)?.delete(source);\n\t\t}\n\t}\n\n\tremoveEdge(source: NodeId, target: NodeId): boolean {\n\t\t// Check if edge exists\n\t\tif (!this.hasEdgeInternal(source, target)) {\n\t\t\treturn false;\n\t\t}\n\n\t\tthis.removeEdgeInternal(source, target);\n\t\treturn true;\n\t}\n\n\tprivate hasEdgeInternal(source: NodeId, target: NodeId): boolean {\n\t\tconst forward = this.adjacency.get(source)?.has(target) === true;\n\t\tif (forward) {\n\t\t\treturn true;\n\t\t}\n\n\t\tif (!this.directed) {\n\t\t\treturn this.adjacency.get(target)?.has(source) === true;\n\t\t}\n\n\t\treturn false;\n\t}\n}\n"],"mappings":"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"}