@git-stunts/git-warp 10.1.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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +16 -0
  3. package/README.md +480 -0
  4. package/SECURITY.md +30 -0
  5. package/bin/git-warp +24 -0
  6. package/bin/warp-graph.js +1574 -0
  7. package/index.d.ts +2366 -0
  8. package/index.js +180 -0
  9. package/package.json +129 -0
  10. package/scripts/install-git-warp.sh +258 -0
  11. package/scripts/uninstall-git-warp.sh +139 -0
  12. package/src/domain/WarpGraph.js +3157 -0
  13. package/src/domain/crdt/Dot.js +160 -0
  14. package/src/domain/crdt/LWW.js +154 -0
  15. package/src/domain/crdt/ORSet.js +371 -0
  16. package/src/domain/crdt/VersionVector.js +222 -0
  17. package/src/domain/entities/GraphNode.js +60 -0
  18. package/src/domain/errors/EmptyMessageError.js +47 -0
  19. package/src/domain/errors/ForkError.js +30 -0
  20. package/src/domain/errors/IndexError.js +23 -0
  21. package/src/domain/errors/OperationAbortedError.js +22 -0
  22. package/src/domain/errors/QueryError.js +39 -0
  23. package/src/domain/errors/SchemaUnsupportedError.js +17 -0
  24. package/src/domain/errors/ShardCorruptionError.js +56 -0
  25. package/src/domain/errors/ShardLoadError.js +57 -0
  26. package/src/domain/errors/ShardValidationError.js +61 -0
  27. package/src/domain/errors/StorageError.js +57 -0
  28. package/src/domain/errors/SyncError.js +30 -0
  29. package/src/domain/errors/TraversalError.js +23 -0
  30. package/src/domain/errors/WarpError.js +31 -0
  31. package/src/domain/errors/WormholeError.js +28 -0
  32. package/src/domain/errors/WriterError.js +39 -0
  33. package/src/domain/errors/index.js +21 -0
  34. package/src/domain/services/AnchorMessageCodec.js +99 -0
  35. package/src/domain/services/BitmapIndexBuilder.js +225 -0
  36. package/src/domain/services/BitmapIndexReader.js +435 -0
  37. package/src/domain/services/BoundaryTransitionRecord.js +463 -0
  38. package/src/domain/services/CheckpointMessageCodec.js +147 -0
  39. package/src/domain/services/CheckpointSerializerV5.js +281 -0
  40. package/src/domain/services/CheckpointService.js +384 -0
  41. package/src/domain/services/CommitDagTraversalService.js +156 -0
  42. package/src/domain/services/DagPathFinding.js +712 -0
  43. package/src/domain/services/DagTopology.js +239 -0
  44. package/src/domain/services/DagTraversal.js +245 -0
  45. package/src/domain/services/Frontier.js +108 -0
  46. package/src/domain/services/GCMetrics.js +101 -0
  47. package/src/domain/services/GCPolicy.js +122 -0
  48. package/src/domain/services/GitLogParser.js +205 -0
  49. package/src/domain/services/HealthCheckService.js +246 -0
  50. package/src/domain/services/HookInstaller.js +326 -0
  51. package/src/domain/services/HttpSyncServer.js +262 -0
  52. package/src/domain/services/IndexRebuildService.js +426 -0
  53. package/src/domain/services/IndexStalenessChecker.js +103 -0
  54. package/src/domain/services/JoinReducer.js +582 -0
  55. package/src/domain/services/KeyCodec.js +113 -0
  56. package/src/domain/services/LegacyAnchorDetector.js +67 -0
  57. package/src/domain/services/LogicalTraversal.js +351 -0
  58. package/src/domain/services/MessageCodecInternal.js +132 -0
  59. package/src/domain/services/MessageSchemaDetector.js +145 -0
  60. package/src/domain/services/MigrationService.js +55 -0
  61. package/src/domain/services/ObserverView.js +265 -0
  62. package/src/domain/services/PatchBuilderV2.js +669 -0
  63. package/src/domain/services/PatchMessageCodec.js +140 -0
  64. package/src/domain/services/ProvenanceIndex.js +337 -0
  65. package/src/domain/services/ProvenancePayload.js +242 -0
  66. package/src/domain/services/QueryBuilder.js +835 -0
  67. package/src/domain/services/StateDiff.js +300 -0
  68. package/src/domain/services/StateSerializerV5.js +156 -0
  69. package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
  70. package/src/domain/services/SyncProtocol.js +593 -0
  71. package/src/domain/services/TemporalQuery.js +201 -0
  72. package/src/domain/services/TranslationCost.js +221 -0
  73. package/src/domain/services/TraversalService.js +8 -0
  74. package/src/domain/services/WarpMessageCodec.js +29 -0
  75. package/src/domain/services/WarpStateIndexBuilder.js +127 -0
  76. package/src/domain/services/WormholeService.js +353 -0
  77. package/src/domain/types/TickReceipt.js +285 -0
  78. package/src/domain/types/WarpTypes.js +209 -0
  79. package/src/domain/types/WarpTypesV2.js +200 -0
  80. package/src/domain/utils/CachedValue.js +140 -0
  81. package/src/domain/utils/EventId.js +89 -0
  82. package/src/domain/utils/LRUCache.js +112 -0
  83. package/src/domain/utils/MinHeap.js +114 -0
  84. package/src/domain/utils/RefLayout.js +280 -0
  85. package/src/domain/utils/WriterId.js +205 -0
  86. package/src/domain/utils/cancellation.js +33 -0
  87. package/src/domain/utils/canonicalStringify.js +42 -0
  88. package/src/domain/utils/defaultClock.js +20 -0
  89. package/src/domain/utils/defaultCodec.js +51 -0
  90. package/src/domain/utils/nullLogger.js +21 -0
  91. package/src/domain/utils/roaring.js +181 -0
  92. package/src/domain/utils/shardVersion.js +9 -0
  93. package/src/domain/warp/PatchSession.js +217 -0
  94. package/src/domain/warp/Writer.js +181 -0
  95. package/src/hooks/post-merge.sh +60 -0
  96. package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
  97. package/src/infrastructure/adapters/ClockAdapter.js +57 -0
  98. package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
  99. package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
  100. package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
  101. package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
  102. package/src/infrastructure/adapters/NoOpLogger.js +62 -0
  103. package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
  104. package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
  105. package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
  106. package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
  107. package/src/infrastructure/codecs/CborCodec.js +384 -0
  108. package/src/ports/BlobPort.js +30 -0
  109. package/src/ports/ClockPort.js +25 -0
  110. package/src/ports/CodecPort.js +25 -0
  111. package/src/ports/CommitPort.js +114 -0
  112. package/src/ports/ConfigPort.js +31 -0
  113. package/src/ports/CryptoPort.js +38 -0
  114. package/src/ports/GraphPersistencePort.js +57 -0
  115. package/src/ports/HttpServerPort.js +25 -0
  116. package/src/ports/IndexStoragePort.js +39 -0
  117. package/src/ports/LoggerPort.js +68 -0
  118. package/src/ports/RefPort.js +51 -0
  119. package/src/ports/TreePort.js +51 -0
  120. package/src/visualization/index.js +26 -0
  121. package/src/visualization/layouts/converters.js +75 -0
  122. package/src/visualization/layouts/elkAdapter.js +86 -0
  123. package/src/visualization/layouts/elkLayout.js +95 -0
  124. package/src/visualization/layouts/index.js +29 -0
  125. package/src/visualization/renderers/ascii/box.js +16 -0
  126. package/src/visualization/renderers/ascii/check.js +271 -0
  127. package/src/visualization/renderers/ascii/colors.js +13 -0
  128. package/src/visualization/renderers/ascii/formatters.js +73 -0
  129. package/src/visualization/renderers/ascii/graph.js +344 -0
  130. package/src/visualization/renderers/ascii/history.js +335 -0
  131. package/src/visualization/renderers/ascii/index.js +14 -0
  132. package/src/visualization/renderers/ascii/info.js +245 -0
  133. package/src/visualization/renderers/ascii/materialize.js +255 -0
  134. package/src/visualization/renderers/ascii/path.js +240 -0
  135. package/src/visualization/renderers/ascii/progress.js +32 -0
  136. package/src/visualization/renderers/ascii/symbols.js +33 -0
  137. package/src/visualization/renderers/ascii/table.js +19 -0
  138. package/src/visualization/renderers/browser/index.js +1 -0
  139. package/src/visualization/renderers/svg/index.js +159 -0
  140. package/src/visualization/utils/ansi.js +14 -0
  141. package/src/visualization/utils/time.js +40 -0
  142. package/src/visualization/utils/truncate.js +40 -0
  143. package/src/visualization/utils/unicode.js +52 -0
@@ -0,0 +1,712 @@
1
+ /**
2
+ * Service for DAG path-finding operations: findPath, shortestPath,
3
+ * weightedShortestPath, A*, and bidirectional A*.
4
+ *
5
+ * Split from CommitDagTraversalService as part of the SRP refactor.
6
+ *
7
+ * @module domain/services/DagPathFinding
8
+ */
9
+
10
+ import nullLogger from '../utils/nullLogger.js';
11
+ import TraversalError from '../errors/TraversalError.js';
12
+ import MinHeap from '../utils/MinHeap.js';
13
+ import { checkAborted } from '../utils/cancellation.js';
14
+
15
+ /**
16
+ * Default limits for path-finding operations.
17
+ * @const
18
+ */
19
+ const DEFAULT_MAX_NODES = 100000;
20
+ const DEFAULT_MAX_DEPTH = 1000;
21
+
22
+ /**
23
+ * Epsilon for A* tie-breaking: small enough not to affect ordering by f,
24
+ * but large enough to break ties in favor of higher g (more progress made).
25
+ * @const
26
+ */
27
+ const EPSILON = 1e-10;
28
+
29
+ /**
30
+ * Service for DAG path-finding operations.
31
+ *
32
+ * Provides path finding, shortest path (bidirectional BFS),
33
+ * weighted shortest path (Dijkstra), A*, and bidirectional A*
34
+ * algorithms using async operations for processing large graphs.
35
+ */
36
+ export default class DagPathFinding {
37
+ /**
38
+ * Creates a new DagPathFinding service.
39
+ *
40
+ * @param {Object} options
41
+ * @param {import('./BitmapIndexReader.js').default} options.indexReader - Index reader for O(1) lookups
42
+ * @param {import('../../ports/LoggerPort.js').default} [options.logger] - Logger instance
43
+ */
44
+ constructor({ indexReader, logger = nullLogger } = {}) {
45
+ if (!indexReader) {
46
+ throw new Error('DagPathFinding requires an indexReader');
47
+ }
48
+ this._indexReader = indexReader;
49
+ this._logger = logger;
50
+ }
51
+
52
+ /**
53
+ * Finds ANY path between two nodes using BFS (forward direction only).
54
+ *
55
+ * Uses unidirectional BFS from source to target, following child edges.
56
+ * Returns the first path found, which is guaranteed to be a shortest path
57
+ * (in terms of number of edges) due to BFS's level-order exploration.
58
+ *
59
+ * @param {Object} options - Path finding options
60
+ * @param {string} options.from - Source node SHA
61
+ * @param {string} options.to - Target node SHA
62
+ * @param {number} [options.maxNodes=100000] - Maximum nodes to visit
63
+ * @param {number} [options.maxDepth=1000] - Maximum path length
64
+ * @param {AbortSignal} [options.signal] - Optional AbortSignal for cancellation
65
+ * @returns {Promise<{found: boolean, path: string[], length: number}>} Path result
66
+ */
67
+ async findPath({
68
+ from, to,
69
+ maxNodes = DEFAULT_MAX_NODES,
70
+ maxDepth = DEFAULT_MAX_DEPTH,
71
+ signal,
72
+ }) {
73
+ if (from === to) {
74
+ return { found: true, path: [from], length: 0 };
75
+ }
76
+
77
+ this._logger.debug('findPath started', { from, to, maxNodes, maxDepth });
78
+
79
+ const visited = new Set();
80
+ const parentMap = new Map();
81
+ const queue = [{ sha: from, depth: 0 }];
82
+
83
+ while (queue.length > 0 && visited.size < maxNodes) {
84
+ if (visited.size % 1000 === 0) {
85
+ checkAborted(signal, 'findPath');
86
+ }
87
+
88
+ const current = queue.shift();
89
+
90
+ if (current.depth > maxDepth) { continue; }
91
+ if (visited.has(current.sha)) { continue; }
92
+
93
+ visited.add(current.sha);
94
+
95
+ if (current.sha === to) {
96
+ const path = this._reconstructPath(parentMap, from, to);
97
+ this._logger.debug('findPath found', { pathLength: path.length });
98
+ return { found: true, path, length: path.length - 1 };
99
+ }
100
+
101
+ const children = await this._indexReader.getChildren(current.sha);
102
+ for (const child of children) {
103
+ if (!visited.has(child)) {
104
+ parentMap.set(child, current.sha);
105
+ queue.push({ sha: child, depth: current.depth + 1 });
106
+ }
107
+ }
108
+ }
109
+
110
+ this._logger.debug('findPath not found', { from, to });
111
+ return { found: false, path: [], length: -1 };
112
+ }
113
+
114
+ /**
115
+ * Finds the shortest path between two nodes using bidirectional BFS.
116
+ *
117
+ * @param {Object} options - Path finding options
118
+ * @param {string} options.from - Source node SHA
119
+ * @param {string} options.to - Target node SHA
120
+ * @param {number} [options.maxDepth=1000] - Maximum search depth per direction
121
+ * @param {AbortSignal} [options.signal] - Optional AbortSignal for cancellation
122
+ * @returns {Promise<{found: boolean, path: string[], length: number}>} Path result
123
+ */
124
+ async shortestPath({ from, to, maxDepth = DEFAULT_MAX_DEPTH, signal }) {
125
+ if (from === to) {
126
+ return { found: true, path: [from], length: 0 };
127
+ }
128
+
129
+ this._logger.debug('shortestPath started', { from, to, maxDepth });
130
+
131
+ // Forward search state (from -> to, using children)
132
+ const fwdVisited = new Set([from]);
133
+ const fwdParent = new Map();
134
+ let fwdFrontier = [from];
135
+
136
+ // Backward search state (to -> from, using parents)
137
+ const bwdVisited = new Set([to]);
138
+ const bwdParent = new Map();
139
+ let bwdFrontier = [to];
140
+
141
+ for (let depth = 0; depth < maxDepth; depth++) {
142
+ checkAborted(signal, 'shortestPath');
143
+
144
+ if (fwdFrontier.length === 0 && bwdFrontier.length === 0) {
145
+ break;
146
+ }
147
+
148
+ // Expand forward frontier
149
+ if (fwdFrontier.length > 0) {
150
+ const nextFwd = [];
151
+ for (const sha of fwdFrontier) {
152
+ const children = await this._indexReader.getChildren(sha);
153
+ for (const child of children) {
154
+ if (bwdVisited.has(child)) {
155
+ fwdParent.set(child, sha);
156
+ const path = this._reconstructBidirectionalPath(fwdParent, bwdParent, from, to, child);
157
+ this._logger.debug('shortestPath found', { pathLength: path.length });
158
+ return { found: true, path, length: path.length - 1 };
159
+ }
160
+ if (!fwdVisited.has(child)) {
161
+ fwdVisited.add(child);
162
+ fwdParent.set(child, sha);
163
+ nextFwd.push(child);
164
+ }
165
+ }
166
+ }
167
+ fwdFrontier = nextFwd;
168
+ }
169
+
170
+ // Expand backward frontier
171
+ if (bwdFrontier.length > 0) {
172
+ const nextBwd = [];
173
+ for (const sha of bwdFrontier) {
174
+ const parents = await this._indexReader.getParents(sha);
175
+ for (const parent of parents) {
176
+ if (fwdVisited.has(parent)) {
177
+ bwdParent.set(parent, sha);
178
+ const path = this._reconstructBidirectionalPath(fwdParent, bwdParent, from, to, parent);
179
+ this._logger.debug('shortestPath found', { pathLength: path.length });
180
+ return { found: true, path, length: path.length - 1 };
181
+ }
182
+ if (!bwdVisited.has(parent)) {
183
+ bwdVisited.add(parent);
184
+ bwdParent.set(parent, sha);
185
+ nextBwd.push(parent);
186
+ }
187
+ }
188
+ }
189
+ bwdFrontier = nextBwd;
190
+ }
191
+ }
192
+
193
+ this._logger.debug('shortestPath not found', { from, to });
194
+ return { found: false, path: [], length: -1 };
195
+ }
196
+
197
+ /**
198
+ * Finds shortest path using Dijkstra's algorithm with custom edge weights.
199
+ *
200
+ * @param {Object} options - Path finding options
201
+ * @param {string} options.from - Starting SHA
202
+ * @param {string} options.to - Target SHA
203
+ * @param {Function} [options.weightProvider] - Async callback `(fromSha, toSha) => number`
204
+ * @param {string} [options.direction='children'] - Edge direction: 'children' or 'parents'
205
+ * @param {AbortSignal} [options.signal] - Optional AbortSignal for cancellation
206
+ * @returns {Promise<{path: string[], totalCost: number}>} Path and cost
207
+ * @throws {TraversalError} With code 'NO_PATH' if no path exists
208
+ */
209
+ async weightedShortestPath({
210
+ from, to,
211
+ weightProvider = () => 1,
212
+ direction = 'children',
213
+ signal,
214
+ }) {
215
+ this._logger.debug('weightedShortestPath started', { from, to, direction });
216
+
217
+ const distances = new Map();
218
+ distances.set(from, 0);
219
+
220
+ const previous = new Map();
221
+ const pq = new MinHeap();
222
+ pq.insert(from, 0);
223
+
224
+ const visited = new Set();
225
+
226
+ while (!pq.isEmpty()) {
227
+ if (visited.size % 1000 === 0) {
228
+ checkAborted(signal, 'weightedShortestPath');
229
+ }
230
+
231
+ const current = pq.extractMin();
232
+
233
+ if (visited.has(current)) {
234
+ continue;
235
+ }
236
+ visited.add(current);
237
+
238
+ if (current === to) {
239
+ const path = this._reconstructWeightedPath(previous, from, to);
240
+ const totalCost = distances.get(to);
241
+ this._logger.debug('weightedShortestPath found', { pathLength: path.length, totalCost });
242
+ return { path, totalCost };
243
+ }
244
+
245
+ const neighbors =
246
+ direction === 'children'
247
+ ? await this._indexReader.getChildren(current)
248
+ : await this._indexReader.getParents(current);
249
+
250
+ for (const neighbor of neighbors) {
251
+ if (visited.has(neighbor)) {
252
+ continue;
253
+ }
254
+
255
+ const edgeWeight = await weightProvider(current, neighbor);
256
+ const newDist = distances.get(current) + edgeWeight;
257
+ const currentDist = distances.has(neighbor) ? distances.get(neighbor) : Infinity;
258
+
259
+ if (newDist < currentDist) {
260
+ distances.set(neighbor, newDist);
261
+ previous.set(neighbor, current);
262
+ pq.insert(neighbor, newDist);
263
+ }
264
+ }
265
+ }
266
+
267
+ this._logger.debug('weightedShortestPath not found', { from, to });
268
+ throw new TraversalError(`No path exists from ${from} to ${to}`, {
269
+ code: 'NO_PATH',
270
+ context: { from, to, direction },
271
+ });
272
+ }
273
+
274
+ /**
275
+ * Finds shortest path using A* algorithm with heuristic guidance.
276
+ *
277
+ * @param {Object} options - Path finding options
278
+ * @param {string} options.from - Starting SHA
279
+ * @param {string} options.to - Target SHA
280
+ * @param {Function} [options.weightProvider] - Async callback `(fromSha, toSha) => number`
281
+ * @param {Function} [options.heuristicProvider] - Callback `(sha, targetSha) => number`
282
+ * @param {string} [options.direction='children'] - Edge direction: 'children' or 'parents'
283
+ * @param {AbortSignal} [options.signal] - Optional AbortSignal for cancellation
284
+ * @returns {Promise<{path: string[], totalCost: number, nodesExplored: number}>} Path result
285
+ * @throws {TraversalError} With code 'NO_PATH' if no path exists
286
+ */
287
+ async aStarSearch({
288
+ from, to,
289
+ weightProvider = () => 1,
290
+ heuristicProvider = () => 0,
291
+ direction = 'children',
292
+ signal,
293
+ }) {
294
+ this._logger.debug('aStarSearch started', { from, to, direction });
295
+
296
+ const gScore = new Map();
297
+ gScore.set(from, 0);
298
+
299
+ const fScore = new Map();
300
+ const initialH = heuristicProvider(from, to);
301
+ const initialG = 0;
302
+ fScore.set(from, initialH);
303
+
304
+ const previous = new Map();
305
+
306
+ const pq = new MinHeap();
307
+ pq.insert(from, initialH - EPSILON * initialG);
308
+
309
+ const visited = new Set();
310
+ let nodesExplored = 0;
311
+
312
+ while (!pq.isEmpty()) {
313
+ if (nodesExplored % 1000 === 0) {
314
+ checkAborted(signal, 'aStarSearch');
315
+ }
316
+
317
+ const current = pq.extractMin();
318
+
319
+ if (visited.has(current)) {
320
+ continue;
321
+ }
322
+ visited.add(current);
323
+ nodesExplored++;
324
+
325
+ if (current === to) {
326
+ const path = this._reconstructWeightedPath(previous, from, to);
327
+ const totalCost = gScore.get(to);
328
+ this._logger.debug('aStarSearch found', { pathLength: path.length, totalCost, nodesExplored });
329
+ return { path, totalCost, nodesExplored };
330
+ }
331
+
332
+ const neighbors =
333
+ direction === 'children'
334
+ ? await this._indexReader.getChildren(current)
335
+ : await this._indexReader.getParents(current);
336
+
337
+ for (const neighbor of neighbors) {
338
+ if (visited.has(neighbor)) {
339
+ continue;
340
+ }
341
+
342
+ const edgeWeight = await weightProvider(current, neighbor);
343
+ const tentativeG = gScore.get(current) + edgeWeight;
344
+ const currentG = gScore.has(neighbor) ? gScore.get(neighbor) : Infinity;
345
+
346
+ if (tentativeG < currentG) {
347
+ previous.set(neighbor, current);
348
+ gScore.set(neighbor, tentativeG);
349
+ const h = heuristicProvider(neighbor, to);
350
+ const f = tentativeG + h;
351
+ fScore.set(neighbor, f);
352
+ pq.insert(neighbor, f - EPSILON * tentativeG);
353
+ }
354
+ }
355
+ }
356
+
357
+ this._logger.debug('aStarSearch not found', { from, to, nodesExplored });
358
+ throw new TraversalError(`No path exists from ${from} to ${to}`, {
359
+ code: 'NO_PATH',
360
+ context: { from, to, direction, nodesExplored },
361
+ });
362
+ }
363
+
364
+ /**
365
+ * Bi-directional A* search - meets in the middle from both ends.
366
+ *
367
+ * @param {Object} options - Path finding options
368
+ * @param {string} options.from - Starting SHA
369
+ * @param {string} options.to - Target SHA
370
+ * @param {Function} [options.weightProvider] - Async callback `(fromSha, toSha) => number`
371
+ * @param {Function} [options.forwardHeuristic] - Callback for forward search
372
+ * @param {Function} [options.backwardHeuristic] - Callback for backward search
373
+ * @param {AbortSignal} [options.signal] - Optional AbortSignal for cancellation
374
+ * @returns {Promise<{path: string[], totalCost: number, nodesExplored: number}>} Path result
375
+ * @throws {TraversalError} With code 'NO_PATH' if no path exists
376
+ */
377
+ async bidirectionalAStar({
378
+ from,
379
+ to,
380
+ weightProvider = () => 1,
381
+ forwardHeuristic = () => 0,
382
+ backwardHeuristic = () => 0,
383
+ signal,
384
+ }) {
385
+ this._logger.debug('bidirectionalAStar started', { from, to });
386
+
387
+ if (from === to) {
388
+ return { path: [from], totalCost: 0, nodesExplored: 1 };
389
+ }
390
+
391
+ // Forward search state
392
+ const fwdGScore = new Map();
393
+ fwdGScore.set(from, 0);
394
+ const fwdPrevious = new Map();
395
+ const fwdVisited = new Set();
396
+ const fwdHeap = new MinHeap();
397
+ const fwdInitialH = forwardHeuristic(from, to);
398
+ fwdHeap.insert(from, fwdInitialH);
399
+
400
+ // Backward search state
401
+ const bwdGScore = new Map();
402
+ bwdGScore.set(to, 0);
403
+ const bwdNext = new Map();
404
+ const bwdVisited = new Set();
405
+ const bwdHeap = new MinHeap();
406
+ const bwdInitialH = backwardHeuristic(to, from);
407
+ bwdHeap.insert(to, bwdInitialH);
408
+
409
+ let mu = Infinity;
410
+ let meetingPoint = null;
411
+ let nodesExplored = 0;
412
+
413
+ while (!fwdHeap.isEmpty() || !bwdHeap.isEmpty()) {
414
+ if (nodesExplored % 1000 === 0) {
415
+ checkAborted(signal, 'bidirectionalAStar');
416
+ }
417
+
418
+ const fwdMinF = fwdHeap.isEmpty() ? Infinity : fwdHeap.peekPriority();
419
+ const bwdMinF = bwdHeap.isEmpty() ? Infinity : bwdHeap.peekPriority();
420
+
421
+ if (Math.min(fwdMinF, bwdMinF) >= mu) {
422
+ break;
423
+ }
424
+
425
+ if (fwdMinF <= bwdMinF) {
426
+ const result = await this._expandForward({
427
+ fwdHeap, fwdVisited, fwdGScore, fwdPrevious,
428
+ bwdVisited, bwdGScore,
429
+ weightProvider, forwardHeuristic, to,
430
+ mu, meetingPoint,
431
+ });
432
+ nodesExplored += result.explored;
433
+ mu = result.mu;
434
+ meetingPoint = result.meetingPoint;
435
+ } else {
436
+ const result = await this._expandBackward({
437
+ bwdHeap, bwdVisited, bwdGScore, bwdNext,
438
+ fwdVisited, fwdGScore,
439
+ weightProvider, backwardHeuristic, from,
440
+ mu, meetingPoint,
441
+ });
442
+ nodesExplored += result.explored;
443
+ mu = result.mu;
444
+ meetingPoint = result.meetingPoint;
445
+ }
446
+ }
447
+
448
+ if (meetingPoint === null) {
449
+ this._logger.debug('bidirectionalAStar not found', { from, to, nodesExplored });
450
+ throw new TraversalError(`No path exists from ${from} to ${to}`, {
451
+ code: 'NO_PATH',
452
+ context: { from, to, nodesExplored },
453
+ });
454
+ }
455
+
456
+ const path = this._reconstructBidirectionalAStarPath(fwdPrevious, bwdNext, from, to, meetingPoint);
457
+
458
+ this._logger.debug('bidirectionalAStar found', { pathLength: path.length, totalCost: mu, nodesExplored });
459
+ return { path, totalCost: mu, nodesExplored };
460
+ }
461
+
462
+ /**
463
+ * Expands the forward frontier by one node in bidirectional A*.
464
+ *
465
+ * @param {Object} state - Forward expansion state
466
+ * @returns {Promise<{explored: number, mu: number, meetingPoint: string|null}>}
467
+ * @private
468
+ */
469
+ async _expandForward({
470
+ fwdHeap, fwdVisited, fwdGScore, fwdPrevious,
471
+ bwdVisited, bwdGScore,
472
+ weightProvider, forwardHeuristic, to,
473
+ mu: inputMu, meetingPoint: inputMeeting,
474
+ }) {
475
+ const current = fwdHeap.extractMin();
476
+ let explored = 0;
477
+ let bestMu = inputMu;
478
+ let bestMeeting = inputMeeting;
479
+
480
+ if (fwdVisited.has(current)) {
481
+ return { explored, mu: bestMu, meetingPoint: bestMeeting };
482
+ }
483
+ fwdVisited.add(current);
484
+ explored = 1;
485
+
486
+ if (bwdVisited.has(current)) {
487
+ const totalCost = fwdGScore.get(current) + bwdGScore.get(current);
488
+ if (totalCost < bestMu) {
489
+ bestMu = totalCost;
490
+ bestMeeting = current;
491
+ }
492
+ }
493
+
494
+ const children = await this._indexReader.getChildren(current);
495
+ for (const child of children) {
496
+ if (fwdVisited.has(child)) {
497
+ continue;
498
+ }
499
+
500
+ const edgeWeight = await weightProvider(current, child);
501
+ const tentativeG = fwdGScore.get(current) + edgeWeight;
502
+ const currentG = fwdGScore.has(child) ? fwdGScore.get(child) : Infinity;
503
+
504
+ if (tentativeG < currentG) {
505
+ fwdPrevious.set(child, current);
506
+ fwdGScore.set(child, tentativeG);
507
+ const h = forwardHeuristic(child, to);
508
+ const f = tentativeG + h;
509
+ fwdHeap.insert(child, f);
510
+
511
+ if (bwdGScore.has(child)) {
512
+ const totalCost = tentativeG + bwdGScore.get(child);
513
+ if (totalCost < bestMu) {
514
+ bestMu = totalCost;
515
+ bestMeeting = child;
516
+ }
517
+ }
518
+ }
519
+ }
520
+
521
+ return { explored, mu: bestMu, meetingPoint: bestMeeting };
522
+ }
523
+
524
+ /**
525
+ * Expands the backward frontier by one node in bidirectional A*.
526
+ *
527
+ * @param {Object} state - Backward expansion state
528
+ * @returns {Promise<{explored: number, mu: number, meetingPoint: string|null}>}
529
+ * @private
530
+ */
531
+ async _expandBackward({
532
+ bwdHeap, bwdVisited, bwdGScore, bwdNext,
533
+ fwdVisited, fwdGScore,
534
+ weightProvider, backwardHeuristic, from,
535
+ mu: inputMu, meetingPoint: inputMeeting,
536
+ }) {
537
+ const current = bwdHeap.extractMin();
538
+ let explored = 0;
539
+ let bestMu = inputMu;
540
+ let bestMeeting = inputMeeting;
541
+
542
+ if (bwdVisited.has(current)) {
543
+ return { explored, mu: bestMu, meetingPoint: bestMeeting };
544
+ }
545
+ bwdVisited.add(current);
546
+ explored = 1;
547
+
548
+ if (fwdVisited.has(current)) {
549
+ const totalCost = fwdGScore.get(current) + bwdGScore.get(current);
550
+ if (totalCost < bestMu) {
551
+ bestMu = totalCost;
552
+ bestMeeting = current;
553
+ }
554
+ }
555
+
556
+ const parents = await this._indexReader.getParents(current);
557
+ for (const parent of parents) {
558
+ if (bwdVisited.has(parent)) {
559
+ continue;
560
+ }
561
+
562
+ const edgeWeight = await weightProvider(parent, current);
563
+ const tentativeG = bwdGScore.get(current) + edgeWeight;
564
+ const currentG = bwdGScore.has(parent) ? bwdGScore.get(parent) : Infinity;
565
+
566
+ if (tentativeG < currentG) {
567
+ bwdNext.set(parent, current);
568
+ bwdGScore.set(parent, tentativeG);
569
+ const h = backwardHeuristic(parent, from);
570
+ const f = tentativeG + h;
571
+ bwdHeap.insert(parent, f);
572
+
573
+ if (fwdGScore.has(parent)) {
574
+ const totalCost = fwdGScore.get(parent) + tentativeG;
575
+ if (totalCost < bestMu) {
576
+ bestMu = totalCost;
577
+ bestMeeting = parent;
578
+ }
579
+ }
580
+ }
581
+ }
582
+
583
+ return { explored, mu: bestMu, meetingPoint: bestMeeting };
584
+ }
585
+
586
+ /**
587
+ * Reconstructs path by walking a predecessor map backwards.
588
+ *
589
+ * @param {Map<string, string>} predecessorMap - Maps each node to its predecessor
590
+ * @param {string} from - Start node
591
+ * @param {string} to - End node
592
+ * @param {string} [context='Path'] - Context label for error logging
593
+ * @returns {string[]} Path from start to end
594
+ * @private
595
+ */
596
+ _walkPredecessors(predecessorMap, from, to, context = 'Path') {
597
+ const path = [to];
598
+ let current = to;
599
+ while (current !== from) {
600
+ const prev = predecessorMap.get(current);
601
+ if (prev === undefined) {
602
+ this._logger.error(`${context} reconstruction failed: missing predecessor`, { from, to, path });
603
+ break;
604
+ }
605
+ current = prev;
606
+ path.unshift(current);
607
+ }
608
+ return path;
609
+ }
610
+
611
+ /**
612
+ * Reconstructs path by walking a successor map forwards.
613
+ *
614
+ * @param {Map<string, string>} successorMap - Maps each node to its successor
615
+ * @param {string} from - Start node
616
+ * @param {string} to - End node
617
+ * @param {string} [context='Path'] - Context label for error logging
618
+ * @returns {string[]} Path from start to end
619
+ * @private
620
+ */
621
+ _walkSuccessors(successorMap, from, to, context = 'Path') {
622
+ const path = [from];
623
+ let current = from;
624
+ while (current !== to) {
625
+ const next = successorMap.get(current);
626
+ if (next === undefined) {
627
+ this._logger.error(`${context} reconstruction failed: missing successor`, { from, to, path });
628
+ break;
629
+ }
630
+ current = next;
631
+ path.push(current);
632
+ }
633
+ return path;
634
+ }
635
+
636
+ /**
637
+ * Reconstructs path from bidirectional A* search.
638
+ *
639
+ * @param {Map<string, string>} fwdPrevious - Forward search predecessor map
640
+ * @param {Map<string, string>} bwdNext - Backward search successor map
641
+ * @param {string} from - Start node
642
+ * @param {string} to - End node
643
+ * @param {string} meeting - Meeting point
644
+ * @returns {string[]} Complete path
645
+ * @private
646
+ */
647
+ _reconstructBidirectionalAStarPath(fwdPrevious, bwdNext, from, to, meeting) {
648
+ const forwardPath = this._walkPredecessors(fwdPrevious, from, meeting, 'Forward path');
649
+ const backwardPath = this._walkSuccessors(bwdNext, meeting, to, 'Backward path');
650
+ return forwardPath.concat(backwardPath.slice(1));
651
+ }
652
+
653
+ /**
654
+ * Reconstructs path from weighted search previous pointers.
655
+ *
656
+ * @param {Map<string, string>} previous - Predecessor map
657
+ * @param {string} from - Start node
658
+ * @param {string} to - End node
659
+ * @returns {string[]} Path from start to end
660
+ * @private
661
+ */
662
+ _reconstructWeightedPath(previous, from, to) {
663
+ return this._walkPredecessors(previous, from, to, 'Weighted path');
664
+ }
665
+
666
+ /**
667
+ * Reconstructs path from BFS parent map.
668
+ *
669
+ * @param {Map<string, string>} parentMap - BFS predecessor map
670
+ * @param {string} from - Start node
671
+ * @param {string} to - End node
672
+ * @returns {string[]} Path from start to end
673
+ * @private
674
+ */
675
+ _reconstructPath(parentMap, from, to) {
676
+ return this._walkPredecessors(parentMap, from, to, 'Path');
677
+ }
678
+
679
+ /**
680
+ * Reconstructs path from bidirectional BFS search.
681
+ *
682
+ * @param {Map<string, string>} fwdParent - Forward predecessor map
683
+ * @param {Map<string, string>} bwdParent - Backward predecessor map
684
+ * @param {string} from - Start node
685
+ * @param {string} to - End node
686
+ * @param {string} meeting - Meeting point
687
+ * @returns {string[]} Complete path
688
+ * @private
689
+ */
690
+ _reconstructBidirectionalPath(fwdParent, bwdParent, from, to, meeting) {
691
+ const forwardPath = [meeting];
692
+ let current = meeting;
693
+ while (fwdParent.has(current) && fwdParent.get(current) !== undefined) {
694
+ current = fwdParent.get(current);
695
+ forwardPath.unshift(current);
696
+ }
697
+ if (forwardPath[0] !== from) {
698
+ forwardPath.unshift(from);
699
+ }
700
+
701
+ current = meeting;
702
+ while (bwdParent.has(current) && bwdParent.get(current) !== undefined) {
703
+ current = bwdParent.get(current);
704
+ forwardPath.push(current);
705
+ }
706
+ if (forwardPath[forwardPath.length - 1] !== to) {
707
+ forwardPath.push(to);
708
+ }
709
+
710
+ return forwardPath;
711
+ }
712
+ }