@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.
- package/LICENSE +201 -0
- package/NOTICE +16 -0
- package/README.md +480 -0
- package/SECURITY.md +30 -0
- package/bin/git-warp +24 -0
- package/bin/warp-graph.js +1574 -0
- package/index.d.ts +2366 -0
- package/index.js +180 -0
- package/package.json +129 -0
- package/scripts/install-git-warp.sh +258 -0
- package/scripts/uninstall-git-warp.sh +139 -0
- package/src/domain/WarpGraph.js +3157 -0
- package/src/domain/crdt/Dot.js +160 -0
- package/src/domain/crdt/LWW.js +154 -0
- package/src/domain/crdt/ORSet.js +371 -0
- package/src/domain/crdt/VersionVector.js +222 -0
- package/src/domain/entities/GraphNode.js +60 -0
- package/src/domain/errors/EmptyMessageError.js +47 -0
- package/src/domain/errors/ForkError.js +30 -0
- package/src/domain/errors/IndexError.js +23 -0
- package/src/domain/errors/OperationAbortedError.js +22 -0
- package/src/domain/errors/QueryError.js +39 -0
- package/src/domain/errors/SchemaUnsupportedError.js +17 -0
- package/src/domain/errors/ShardCorruptionError.js +56 -0
- package/src/domain/errors/ShardLoadError.js +57 -0
- package/src/domain/errors/ShardValidationError.js +61 -0
- package/src/domain/errors/StorageError.js +57 -0
- package/src/domain/errors/SyncError.js +30 -0
- package/src/domain/errors/TraversalError.js +23 -0
- package/src/domain/errors/WarpError.js +31 -0
- package/src/domain/errors/WormholeError.js +28 -0
- package/src/domain/errors/WriterError.js +39 -0
- package/src/domain/errors/index.js +21 -0
- package/src/domain/services/AnchorMessageCodec.js +99 -0
- package/src/domain/services/BitmapIndexBuilder.js +225 -0
- package/src/domain/services/BitmapIndexReader.js +435 -0
- package/src/domain/services/BoundaryTransitionRecord.js +463 -0
- package/src/domain/services/CheckpointMessageCodec.js +147 -0
- package/src/domain/services/CheckpointSerializerV5.js +281 -0
- package/src/domain/services/CheckpointService.js +384 -0
- package/src/domain/services/CommitDagTraversalService.js +156 -0
- package/src/domain/services/DagPathFinding.js +712 -0
- package/src/domain/services/DagTopology.js +239 -0
- package/src/domain/services/DagTraversal.js +245 -0
- package/src/domain/services/Frontier.js +108 -0
- package/src/domain/services/GCMetrics.js +101 -0
- package/src/domain/services/GCPolicy.js +122 -0
- package/src/domain/services/GitLogParser.js +205 -0
- package/src/domain/services/HealthCheckService.js +246 -0
- package/src/domain/services/HookInstaller.js +326 -0
- package/src/domain/services/HttpSyncServer.js +262 -0
- package/src/domain/services/IndexRebuildService.js +426 -0
- package/src/domain/services/IndexStalenessChecker.js +103 -0
- package/src/domain/services/JoinReducer.js +582 -0
- package/src/domain/services/KeyCodec.js +113 -0
- package/src/domain/services/LegacyAnchorDetector.js +67 -0
- package/src/domain/services/LogicalTraversal.js +351 -0
- package/src/domain/services/MessageCodecInternal.js +132 -0
- package/src/domain/services/MessageSchemaDetector.js +145 -0
- package/src/domain/services/MigrationService.js +55 -0
- package/src/domain/services/ObserverView.js +265 -0
- package/src/domain/services/PatchBuilderV2.js +669 -0
- package/src/domain/services/PatchMessageCodec.js +140 -0
- package/src/domain/services/ProvenanceIndex.js +337 -0
- package/src/domain/services/ProvenancePayload.js +242 -0
- package/src/domain/services/QueryBuilder.js +835 -0
- package/src/domain/services/StateDiff.js +300 -0
- package/src/domain/services/StateSerializerV5.js +156 -0
- package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
- package/src/domain/services/SyncProtocol.js +593 -0
- package/src/domain/services/TemporalQuery.js +201 -0
- package/src/domain/services/TranslationCost.js +221 -0
- package/src/domain/services/TraversalService.js +8 -0
- package/src/domain/services/WarpMessageCodec.js +29 -0
- package/src/domain/services/WarpStateIndexBuilder.js +127 -0
- package/src/domain/services/WormholeService.js +353 -0
- package/src/domain/types/TickReceipt.js +285 -0
- package/src/domain/types/WarpTypes.js +209 -0
- package/src/domain/types/WarpTypesV2.js +200 -0
- package/src/domain/utils/CachedValue.js +140 -0
- package/src/domain/utils/EventId.js +89 -0
- package/src/domain/utils/LRUCache.js +112 -0
- package/src/domain/utils/MinHeap.js +114 -0
- package/src/domain/utils/RefLayout.js +280 -0
- package/src/domain/utils/WriterId.js +205 -0
- package/src/domain/utils/cancellation.js +33 -0
- package/src/domain/utils/canonicalStringify.js +42 -0
- package/src/domain/utils/defaultClock.js +20 -0
- package/src/domain/utils/defaultCodec.js +51 -0
- package/src/domain/utils/nullLogger.js +21 -0
- package/src/domain/utils/roaring.js +181 -0
- package/src/domain/utils/shardVersion.js +9 -0
- package/src/domain/warp/PatchSession.js +217 -0
- package/src/domain/warp/Writer.js +181 -0
- package/src/hooks/post-merge.sh +60 -0
- package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
- package/src/infrastructure/adapters/ClockAdapter.js +57 -0
- package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
- package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
- package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
- package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
- package/src/infrastructure/adapters/NoOpLogger.js +62 -0
- package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
- package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
- package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
- package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
- package/src/infrastructure/codecs/CborCodec.js +384 -0
- package/src/ports/BlobPort.js +30 -0
- package/src/ports/ClockPort.js +25 -0
- package/src/ports/CodecPort.js +25 -0
- package/src/ports/CommitPort.js +114 -0
- package/src/ports/ConfigPort.js +31 -0
- package/src/ports/CryptoPort.js +38 -0
- package/src/ports/GraphPersistencePort.js +57 -0
- package/src/ports/HttpServerPort.js +25 -0
- package/src/ports/IndexStoragePort.js +39 -0
- package/src/ports/LoggerPort.js +68 -0
- package/src/ports/RefPort.js +51 -0
- package/src/ports/TreePort.js +51 -0
- package/src/visualization/index.js +26 -0
- package/src/visualization/layouts/converters.js +75 -0
- package/src/visualization/layouts/elkAdapter.js +86 -0
- package/src/visualization/layouts/elkLayout.js +95 -0
- package/src/visualization/layouts/index.js +29 -0
- package/src/visualization/renderers/ascii/box.js +16 -0
- package/src/visualization/renderers/ascii/check.js +271 -0
- package/src/visualization/renderers/ascii/colors.js +13 -0
- package/src/visualization/renderers/ascii/formatters.js +73 -0
- package/src/visualization/renderers/ascii/graph.js +344 -0
- package/src/visualization/renderers/ascii/history.js +335 -0
- package/src/visualization/renderers/ascii/index.js +14 -0
- package/src/visualization/renderers/ascii/info.js +245 -0
- package/src/visualization/renderers/ascii/materialize.js +255 -0
- package/src/visualization/renderers/ascii/path.js +240 -0
- package/src/visualization/renderers/ascii/progress.js +32 -0
- package/src/visualization/renderers/ascii/symbols.js +33 -0
- package/src/visualization/renderers/ascii/table.js +19 -0
- package/src/visualization/renderers/browser/index.js +1 -0
- package/src/visualization/renderers/svg/index.js +159 -0
- package/src/visualization/utils/ansi.js +14 -0
- package/src/visualization/utils/time.js +40 -0
- package/src/visualization/utils/truncate.js +40 -0
- 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
|
+
}
|