@git-stunts/git-warp 11.5.1 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -10
- package/bin/cli/commands/registry.js +4 -0
- package/bin/cli/commands/reindex.js +41 -0
- package/bin/cli/commands/verify-index.js +59 -0
- package/bin/cli/infrastructure.js +7 -2
- package/bin/cli/schemas.js +19 -0
- package/bin/cli/types.js +2 -0
- package/index.d.ts +49 -12
- package/package.json +2 -2
- package/src/domain/WarpGraph.js +40 -0
- package/src/domain/errors/ShardIdOverflowError.js +28 -0
- package/src/domain/errors/index.js +1 -0
- package/src/domain/services/AdjacencyNeighborProvider.js +140 -0
- package/src/domain/services/BitmapNeighborProvider.js +178 -0
- package/src/domain/services/CheckpointMessageCodec.js +3 -3
- package/src/domain/services/CheckpointService.js +77 -12
- package/src/domain/services/GraphTraversal.js +1239 -0
- package/src/domain/services/IncrementalIndexUpdater.js +765 -0
- package/src/domain/services/JoinReducer.js +233 -5
- package/src/domain/services/LogicalBitmapIndexBuilder.js +323 -0
- package/src/domain/services/LogicalIndexBuildService.js +108 -0
- package/src/domain/services/LogicalIndexReader.js +315 -0
- package/src/domain/services/LogicalTraversal.js +321 -202
- package/src/domain/services/MaterializedViewService.js +379 -0
- package/src/domain/services/ObserverView.js +138 -47
- package/src/domain/services/PatchBuilderV2.js +3 -3
- package/src/domain/services/PropertyIndexBuilder.js +64 -0
- package/src/domain/services/PropertyIndexReader.js +111 -0
- package/src/domain/services/TemporalQuery.js +128 -14
- package/src/domain/types/PatchDiff.js +90 -0
- package/src/domain/types/WarpTypesV2.js +4 -4
- package/src/domain/utils/MinHeap.js +45 -17
- package/src/domain/utils/canonicalCbor.js +36 -0
- package/src/domain/utils/fnv1a.js +20 -0
- package/src/domain/utils/roaring.js +14 -3
- package/src/domain/utils/shardKey.js +40 -0
- package/src/domain/utils/toBytes.js +17 -0
- package/src/domain/warp/_wiredMethods.d.ts +7 -1
- package/src/domain/warp/checkpoint.methods.js +21 -5
- package/src/domain/warp/materialize.methods.js +17 -5
- package/src/domain/warp/materializeAdvanced.methods.js +142 -3
- package/src/domain/warp/query.methods.js +78 -12
- package/src/infrastructure/adapters/CasSeekCacheAdapter.js +26 -5
- package/src/ports/BlobPort.js +1 -1
- package/src/ports/NeighborProviderPort.js +59 -0
- package/src/ports/SeekCachePort.js +4 -3
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LogicalTraversal - Traversal utilities for the logical WARP graph.
|
|
3
3
|
*
|
|
4
|
+
* **Deprecated**: delegates to GraphTraversal + AdjacencyNeighborProvider
|
|
5
|
+
* internally. The public API is unchanged for backward compatibility.
|
|
6
|
+
* New code should use GraphTraversal directly.
|
|
7
|
+
*
|
|
4
8
|
* Provides deterministic BFS/DFS/shortestPath/connectedComponent over
|
|
5
9
|
* the materialized logical graph (node/edge OR-Sets), not the Git DAG.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import TraversalError from '../errors/TraversalError.js';
|
|
13
|
+
import GraphTraversal from './GraphTraversal.js';
|
|
14
|
+
import AdjacencyNeighborProvider from './AdjacencyNeighborProvider.js';
|
|
15
|
+
import { orsetElements } from '../crdt/ORSet.js';
|
|
9
16
|
|
|
10
17
|
const DEFAULT_MAX_DEPTH = 1000;
|
|
11
18
|
|
|
@@ -33,15 +40,15 @@ function assertDirection(direction) {
|
|
|
33
40
|
* Normalizes a label filter into a Set for efficient lookup.
|
|
34
41
|
*
|
|
35
42
|
* Accepts a single label string, an array of labels, or undefined. Returns
|
|
36
|
-
* a Set containing the label(s) or
|
|
43
|
+
* a Set containing the label(s) or undefined if no filter is specified.
|
|
37
44
|
*
|
|
38
45
|
* @param {string|string[]|undefined} labelFilter - The label filter to normalize
|
|
39
|
-
* @returns {Set<string>|
|
|
46
|
+
* @returns {Set<string>|undefined} A Set of labels for filtering, or undefined if no filter
|
|
40
47
|
* @throws {TraversalError} If labelFilter is neither a string, array, nor undefined
|
|
41
48
|
*/
|
|
42
49
|
function normalizeLabelFilter(labelFilter) {
|
|
43
50
|
if (labelFilter === undefined) {
|
|
44
|
-
return
|
|
51
|
+
return undefined;
|
|
45
52
|
}
|
|
46
53
|
if (Array.isArray(labelFilter)) {
|
|
47
54
|
return new Set(labelFilter);
|
|
@@ -55,73 +62,10 @@ function normalizeLabelFilter(labelFilter) {
|
|
|
55
62
|
});
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
/**
|
|
59
|
-
* Filters a list of neighbor edges by label.
|
|
60
|
-
*
|
|
61
|
-
* If no label set is provided (null), returns all neighbors unchanged.
|
|
62
|
-
* If an empty label set is provided, returns an empty array.
|
|
63
|
-
* Otherwise, returns only edges whose label is in the set.
|
|
64
|
-
*
|
|
65
|
-
* @param {Array<{neighborId: string, label: string}>} neighbors - The list of neighbor edges to filter
|
|
66
|
-
* @param {Set<string>|null} labelSet - The set of allowed labels, or null to allow all
|
|
67
|
-
* @returns {Array<{neighborId: string, label: string}>} The filtered list of neighbor edges
|
|
68
|
-
*/
|
|
69
|
-
function filterByLabel(neighbors, labelSet) {
|
|
70
|
-
if (!labelSet) {
|
|
71
|
-
return neighbors;
|
|
72
|
-
}
|
|
73
|
-
if (labelSet.size === 0) {
|
|
74
|
-
return [];
|
|
75
|
-
}
|
|
76
|
-
return neighbors.filter((edge) => labelSet.has(edge.label));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Retrieves neighbors of a node based on direction and label filter.
|
|
81
|
-
*
|
|
82
|
-
* Returns outgoing neighbors for 'out', incoming neighbors for 'in', or
|
|
83
|
-
* a merged and sorted list of both for 'both'. Results are filtered by
|
|
84
|
-
* label if a label set is provided.
|
|
85
|
-
*
|
|
86
|
-
* For 'both' direction, neighbors are sorted first by neighborId, then by label,
|
|
87
|
-
* ensuring deterministic traversal order.
|
|
88
|
-
*
|
|
89
|
-
* @param {Object} params - The neighbor lookup parameters
|
|
90
|
-
* @param {string} params.nodeId - The node ID to get neighbors for
|
|
91
|
-
* @param {'out'|'in'|'both'} params.direction - The edge direction to follow
|
|
92
|
-
* @param {Object} params.adjacency - The adjacency structure from materialized graph
|
|
93
|
-
* @param {Map<string, Array<{neighborId: string, label: string}>>} params.adjacency.outgoing - Outgoing edge map
|
|
94
|
-
* @param {Map<string, Array<{neighborId: string, label: string}>>} params.adjacency.incoming - Incoming edge map
|
|
95
|
-
* @param {Set<string>|null} params.labelSet - The set of allowed labels, or null to allow all
|
|
96
|
-
* @returns {Array<{neighborId: string, label: string}>} The list of neighbor edges
|
|
97
|
-
*/
|
|
98
|
-
function getNeighbors({ nodeId, direction, adjacency, labelSet }) {
|
|
99
|
-
const outgoing = filterByLabel(adjacency.outgoing.get(nodeId) || [], labelSet);
|
|
100
|
-
const incoming = filterByLabel(adjacency.incoming.get(nodeId) || [], labelSet);
|
|
101
|
-
|
|
102
|
-
if (direction === 'out') {
|
|
103
|
-
return outgoing;
|
|
104
|
-
}
|
|
105
|
-
if (direction === 'in') {
|
|
106
|
-
return incoming;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const merged = outgoing.concat(incoming);
|
|
110
|
-
merged.sort((a, b) => {
|
|
111
|
-
if (a.neighborId !== b.neighborId) {
|
|
112
|
-
return a.neighborId < b.neighborId ? -1 : 1;
|
|
113
|
-
}
|
|
114
|
-
return a.label < b.label ? -1 : a.label > b.label ? 1 : 0;
|
|
115
|
-
});
|
|
116
|
-
return merged;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
65
|
/**
|
|
120
66
|
* Deterministic graph traversal engine for the materialized WARP graph.
|
|
121
67
|
*
|
|
122
|
-
*
|
|
123
|
-
* connected component algorithms over the logical node/edge OR-Sets.
|
|
124
|
-
* All traversals produce deterministic results via sorted neighbor ordering.
|
|
68
|
+
* @deprecated Use GraphTraversal + AdjacencyNeighborProvider directly.
|
|
125
69
|
*/
|
|
126
70
|
export default class LogicalTraversal {
|
|
127
71
|
/**
|
|
@@ -134,26 +78,60 @@ export default class LogicalTraversal {
|
|
|
134
78
|
}
|
|
135
79
|
|
|
136
80
|
/**
|
|
137
|
-
* Prepares
|
|
81
|
+
* Prepares a GraphTraversal engine backed by the current adjacency.
|
|
82
|
+
* Does NOT validate any start node — use this for methods that accept
|
|
83
|
+
* multiple starts or no start at all (topologicalSort, commonAncestors).
|
|
138
84
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
85
|
+
* @private
|
|
86
|
+
* @param {Object} opts - The traversal options
|
|
87
|
+
* @param {'out'|'in'|'both'} [opts.dir] - Edge direction to follow
|
|
88
|
+
* @param {string|string[]} [opts.labelFilter] - Edge label(s) to include
|
|
89
|
+
* @param {number} [opts.maxDepth] - Maximum depth to traverse
|
|
90
|
+
* @returns {Promise<{engine: GraphTraversal, direction: 'out'|'in'|'both', options: {labels?: Set<string>}|undefined, depthLimit: number}>}
|
|
91
|
+
* @throws {TraversalError} If the direction is invalid (INVALID_DIRECTION)
|
|
92
|
+
* @throws {TraversalError} If the labelFilter is invalid (INVALID_LABEL_FILTER)
|
|
93
|
+
*/
|
|
94
|
+
async _prepareEngine({ dir, labelFilter, maxDepth }) {
|
|
95
|
+
// Private access: _materializeGraph is a WarpGraph internal.
|
|
96
|
+
// This coupling will be removed when the LogicalTraversal facade is sunset
|
|
97
|
+
// and callers migrate to GraphTraversal + NeighborProvider directly.
|
|
98
|
+
const materialized = await /** @type {{ _materializeGraph: () => Promise<{state: {nodeAlive: import('../crdt/ORSet.js').ORSet}, adjacency: {outgoing: Map<string, Array<{neighborId: string, label: string}>>, incoming: Map<string, Array<{neighborId: string, label: string}>>}}> }} */ (this._graph)._materializeGraph();
|
|
99
|
+
|
|
100
|
+
const direction = assertDirection(dir);
|
|
101
|
+
const labelSet = normalizeLabelFilter(labelFilter);
|
|
102
|
+
const { adjacency, state } = materialized;
|
|
103
|
+
const depthLimit = maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
104
|
+
|
|
105
|
+
const provider = new AdjacencyNeighborProvider({
|
|
106
|
+
outgoing: adjacency.outgoing,
|
|
107
|
+
incoming: adjacency.incoming,
|
|
108
|
+
aliveNodes: new Set(orsetElements(state.nodeAlive)),
|
|
109
|
+
});
|
|
110
|
+
const engine = new GraphTraversal({ provider });
|
|
111
|
+
|
|
112
|
+
/** @type {{labels?: Set<string>}|undefined} */
|
|
113
|
+
const options = labelSet ? { labels: labelSet } : undefined;
|
|
114
|
+
|
|
115
|
+
return { engine, direction, options, depthLimit };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Prepares a GraphTraversal engine and validates a single start node.
|
|
141
120
|
*
|
|
142
121
|
* @private
|
|
143
122
|
* @param {string} start - The starting node ID for traversal
|
|
144
|
-
* @param {Object}
|
|
145
|
-
* @param {'out'|'in'|'both'} [
|
|
146
|
-
* @param {string|string[]} [
|
|
147
|
-
* @param {number} [
|
|
148
|
-
* @returns {Promise<{
|
|
149
|
-
* The normalized traversal parameters
|
|
123
|
+
* @param {Object} opts - The traversal options
|
|
124
|
+
* @param {'out'|'in'|'both'} [opts.dir] - Edge direction to follow
|
|
125
|
+
* @param {string|string[]} [opts.labelFilter] - Edge label(s) to include
|
|
126
|
+
* @param {number} [opts.maxDepth] - Maximum depth to traverse
|
|
127
|
+
* @returns {Promise<{engine: GraphTraversal, direction: 'out'|'in'|'both', options: {labels?: Set<string>}|undefined, depthLimit: number}>}
|
|
150
128
|
* @throws {TraversalError} If the start node is not found (NODE_NOT_FOUND)
|
|
151
129
|
* @throws {TraversalError} If the direction is invalid (INVALID_DIRECTION)
|
|
152
130
|
* @throws {TraversalError} If the labelFilter is invalid (INVALID_LABEL_FILTER)
|
|
153
131
|
*/
|
|
154
|
-
async _prepare(start,
|
|
155
|
-
const
|
|
156
|
-
|
|
132
|
+
async _prepare(start, opts) {
|
|
133
|
+
const prepared = await this._prepareEngine(opts);
|
|
134
|
+
// Note: engine also validates via provider.hasNode — redundant but harmless.
|
|
157
135
|
if (!(await this._graph.hasNode(start))) {
|
|
158
136
|
throw new TraversalError(`Start node not found: ${start}`, {
|
|
159
137
|
code: 'NODE_NOT_FOUND',
|
|
@@ -161,12 +139,7 @@ export default class LogicalTraversal {
|
|
|
161
139
|
});
|
|
162
140
|
}
|
|
163
141
|
|
|
164
|
-
|
|
165
|
-
const labelSet = normalizeLabelFilter(labelFilter);
|
|
166
|
-
const { adjacency } = materialized;
|
|
167
|
-
const depthLimit = maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
168
|
-
|
|
169
|
-
return { dir: resolvedDir, labelSet, adjacency, depthLimit };
|
|
142
|
+
return prepared;
|
|
170
143
|
}
|
|
171
144
|
|
|
172
145
|
/**
|
|
@@ -181,42 +154,15 @@ export default class LogicalTraversal {
|
|
|
181
154
|
* @throws {TraversalError} If the start node is not found or direction is invalid
|
|
182
155
|
*/
|
|
183
156
|
async bfs(start, options = {}) {
|
|
184
|
-
const {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
if (current.depth > depthLimit) {
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
visited.add(current.nodeId);
|
|
199
|
-
result.push(current.nodeId);
|
|
200
|
-
|
|
201
|
-
if (current.depth === depthLimit) {
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const neighbors = getNeighbors({
|
|
206
|
-
nodeId: current.nodeId,
|
|
207
|
-
direction: dir,
|
|
208
|
-
adjacency,
|
|
209
|
-
labelSet,
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
for (const edge of neighbors) {
|
|
213
|
-
if (!visited.has(edge.neighborId)) {
|
|
214
|
-
queue.push({ nodeId: edge.neighborId, depth: current.depth + 1 });
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return result;
|
|
157
|
+
const { engine, direction, options: opts, depthLimit } = await this._prepare(start, options);
|
|
158
|
+
const { nodes } = await engine.bfs({
|
|
159
|
+
start,
|
|
160
|
+
direction,
|
|
161
|
+
options: opts,
|
|
162
|
+
maxDepth: depthLimit,
|
|
163
|
+
maxNodes: Infinity,
|
|
164
|
+
});
|
|
165
|
+
return await Promise.resolve(nodes);
|
|
220
166
|
}
|
|
221
167
|
|
|
222
168
|
/**
|
|
@@ -231,43 +177,15 @@ export default class LogicalTraversal {
|
|
|
231
177
|
* @throws {TraversalError} If the start node is not found or direction is invalid
|
|
232
178
|
*/
|
|
233
179
|
async dfs(start, options = {}) {
|
|
234
|
-
const {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
if (current.depth > depthLimit) {
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
visited.add(current.nodeId);
|
|
249
|
-
result.push(current.nodeId);
|
|
250
|
-
|
|
251
|
-
if (current.depth === depthLimit) {
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const neighbors = getNeighbors({
|
|
256
|
-
nodeId: current.nodeId,
|
|
257
|
-
direction: dir,
|
|
258
|
-
adjacency,
|
|
259
|
-
labelSet,
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
for (let i = neighbors.length - 1; i >= 0; i -= 1) {
|
|
263
|
-
const edge = neighbors[i];
|
|
264
|
-
if (!visited.has(edge.neighborId)) {
|
|
265
|
-
stack.push({ nodeId: edge.neighborId, depth: current.depth + 1 });
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
return result;
|
|
180
|
+
const { engine, direction, options: opts, depthLimit } = await this._prepare(start, options);
|
|
181
|
+
const { nodes } = await engine.dfs({
|
|
182
|
+
start,
|
|
183
|
+
direction,
|
|
184
|
+
options: opts,
|
|
185
|
+
maxDepth: depthLimit,
|
|
186
|
+
maxNodes: Infinity,
|
|
187
|
+
});
|
|
188
|
+
return await Promise.resolve(nodes);
|
|
271
189
|
}
|
|
272
190
|
|
|
273
191
|
/**
|
|
@@ -285,68 +203,269 @@ export default class LogicalTraversal {
|
|
|
285
203
|
* @throws {TraversalError} If the start node is not found or direction is invalid
|
|
286
204
|
*/
|
|
287
205
|
async shortestPath(from, to, options = {}) {
|
|
288
|
-
const {
|
|
206
|
+
const { engine, direction, options: opts, depthLimit } = await this._prepare(from, options);
|
|
207
|
+
const { found, path, length } = await engine.shortestPath({
|
|
208
|
+
start: from,
|
|
209
|
+
goal: to,
|
|
210
|
+
direction,
|
|
211
|
+
options: opts,
|
|
212
|
+
maxDepth: depthLimit,
|
|
213
|
+
maxNodes: Infinity,
|
|
214
|
+
});
|
|
215
|
+
return await Promise.resolve({ found, path, length });
|
|
216
|
+
}
|
|
289
217
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Connected component (undirected by default).
|
|
220
|
+
*
|
|
221
|
+
* @param {string} start - Starting node ID
|
|
222
|
+
* @param {Object} [options] - Traversal options
|
|
223
|
+
* @param {number} [options.maxDepth] - Maximum depth to traverse (default: 1000)
|
|
224
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
225
|
+
* @returns {Promise<string[]>} Node IDs in visit order
|
|
226
|
+
* @throws {TraversalError} If the start node is not found
|
|
227
|
+
*/
|
|
228
|
+
async connectedComponent(start, options = {}) {
|
|
229
|
+
return await this.bfs(start, { ...options, dir: 'both' });
|
|
230
|
+
}
|
|
293
231
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Reachability check — BFS with early termination.
|
|
234
|
+
*
|
|
235
|
+
* Non-existent nodes are simply unreachable (no NODE_NOT_FOUND throw).
|
|
236
|
+
*
|
|
237
|
+
* @param {string} from - Source node ID
|
|
238
|
+
* @param {string} to - Target node ID
|
|
239
|
+
* @param {Object} [options] - Traversal options
|
|
240
|
+
* @param {number} [options.maxDepth] - Maximum search depth
|
|
241
|
+
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
242
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
243
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
244
|
+
* @returns {Promise<{reachable: boolean}>}
|
|
245
|
+
*/
|
|
246
|
+
async isReachable(from, to, options = {}) {
|
|
247
|
+
const { engine, direction, options: opts, depthLimit } = await this._prepareEngine(options);
|
|
248
|
+
const { reachable } = await engine.isReachable({
|
|
249
|
+
start: from,
|
|
250
|
+
goal: to,
|
|
251
|
+
direction,
|
|
252
|
+
options: opts,
|
|
253
|
+
maxDepth: depthLimit,
|
|
254
|
+
maxNodes: Infinity,
|
|
255
|
+
signal: options.signal,
|
|
256
|
+
});
|
|
257
|
+
return { reachable };
|
|
258
|
+
}
|
|
297
259
|
|
|
298
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Weighted shortest path (Dijkstra's algorithm).
|
|
262
|
+
*
|
|
263
|
+
* @param {string} from - Source node ID
|
|
264
|
+
* @param {string} to - Target node ID
|
|
265
|
+
* @param {Object} [options] - Traversal options
|
|
266
|
+
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
267
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
268
|
+
* @param {(from: string, to: string, label: string) => number | Promise<number>} [options.weightFn] - Edge weight function
|
|
269
|
+
* @param {(nodeId: string) => number | Promise<number>} [options.nodeWeightFn] - Node weight function (mutually exclusive with weightFn)
|
|
270
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
271
|
+
* @returns {Promise<{path: string[], totalCost: number}>}
|
|
272
|
+
* @throws {TraversalError} code 'NO_PATH' if unreachable
|
|
273
|
+
* @throws {TraversalError} code 'E_WEIGHT_FN_CONFLICT' if both weightFn and nodeWeightFn provided
|
|
274
|
+
*/
|
|
275
|
+
async weightedShortestPath(from, to, options = {}) {
|
|
276
|
+
const { engine, direction, options: opts } = await this._prepare(from, options);
|
|
277
|
+
const { path, totalCost } = await engine.weightedShortestPath({
|
|
278
|
+
start: from,
|
|
279
|
+
goal: to,
|
|
280
|
+
direction,
|
|
281
|
+
options: opts,
|
|
282
|
+
weightFn: options.weightFn,
|
|
283
|
+
nodeWeightFn: options.nodeWeightFn,
|
|
284
|
+
maxNodes: Infinity,
|
|
285
|
+
signal: options.signal,
|
|
286
|
+
});
|
|
287
|
+
return { path, totalCost };
|
|
288
|
+
}
|
|
299
289
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
290
|
+
/**
|
|
291
|
+
* A* search with heuristic guidance.
|
|
292
|
+
*
|
|
293
|
+
* @param {string} from - Source node ID
|
|
294
|
+
* @param {string} to - Target node ID
|
|
295
|
+
* @param {Object} [options] - Traversal options
|
|
296
|
+
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
297
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
298
|
+
* @param {(from: string, to: string, label: string) => number | Promise<number>} [options.weightFn] - Edge weight function
|
|
299
|
+
* @param {(nodeId: string) => number | Promise<number>} [options.nodeWeightFn] - Node weight function (mutually exclusive with weightFn)
|
|
300
|
+
* @param {(nodeId: string, goalId: string) => number} [options.heuristicFn] - Heuristic function
|
|
301
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
302
|
+
* @returns {Promise<{path: string[], totalCost: number, nodesExplored: number}>}
|
|
303
|
+
* @throws {TraversalError} code 'NO_PATH' if unreachable
|
|
304
|
+
* @throws {TraversalError} code 'E_WEIGHT_FN_CONFLICT' if both weightFn and nodeWeightFn provided
|
|
305
|
+
*/
|
|
306
|
+
async aStarSearch(from, to, options = {}) {
|
|
307
|
+
const { engine, direction, options: opts } = await this._prepare(from, options);
|
|
308
|
+
const { path, totalCost, nodesExplored } = await engine.aStarSearch({
|
|
309
|
+
start: from,
|
|
310
|
+
goal: to,
|
|
311
|
+
direction,
|
|
312
|
+
options: opts,
|
|
313
|
+
weightFn: options.weightFn,
|
|
314
|
+
nodeWeightFn: options.nodeWeightFn,
|
|
315
|
+
heuristicFn: options.heuristicFn,
|
|
316
|
+
maxNodes: Infinity,
|
|
317
|
+
signal: options.signal,
|
|
318
|
+
});
|
|
319
|
+
return { path, totalCost, nodesExplored };
|
|
320
|
+
}
|
|
305
321
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
322
|
+
/**
|
|
323
|
+
* Bidirectional A* search.
|
|
324
|
+
*
|
|
325
|
+
* Direction is fixed: forward uses 'out', backward uses 'in'.
|
|
326
|
+
*
|
|
327
|
+
* @param {string} from - Source node ID
|
|
328
|
+
* @param {string} to - Target node ID
|
|
329
|
+
* @param {Object} [options] - Traversal options
|
|
330
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
331
|
+
* @param {(from: string, to: string, label: string) => number | Promise<number>} [options.weightFn] - Edge weight function
|
|
332
|
+
* @param {(nodeId: string) => number | Promise<number>} [options.nodeWeightFn] - Node weight function (mutually exclusive with weightFn)
|
|
333
|
+
* @param {(nodeId: string, goalId: string) => number} [options.forwardHeuristic] - Forward heuristic
|
|
334
|
+
* @param {(nodeId: string, goalId: string) => number} [options.backwardHeuristic] - Backward heuristic
|
|
335
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
336
|
+
* @returns {Promise<{path: string[], totalCost: number, nodesExplored: number}>}
|
|
337
|
+
* @throws {TraversalError} code 'NO_PATH' if unreachable
|
|
338
|
+
* @throws {TraversalError} code 'E_WEIGHT_FN_CONFLICT' if both weightFn and nodeWeightFn provided
|
|
339
|
+
*/
|
|
340
|
+
async bidirectionalAStar(from, to, options = {}) {
|
|
341
|
+
const { engine, options: opts } = await this._prepareEngine(options);
|
|
342
|
+
|
|
343
|
+
if (!(await this._graph.hasNode(from))) {
|
|
344
|
+
throw new TraversalError(`Start node not found: ${from}`, {
|
|
345
|
+
code: 'NODE_NOT_FOUND',
|
|
346
|
+
context: { start: from },
|
|
311
347
|
});
|
|
348
|
+
}
|
|
312
349
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
350
|
+
const { path, totalCost, nodesExplored } = await engine.bidirectionalAStar({
|
|
351
|
+
start: from,
|
|
352
|
+
goal: to,
|
|
353
|
+
options: opts,
|
|
354
|
+
weightFn: options.weightFn,
|
|
355
|
+
nodeWeightFn: options.nodeWeightFn,
|
|
356
|
+
forwardHeuristic: options.forwardHeuristic,
|
|
357
|
+
backwardHeuristic: options.backwardHeuristic,
|
|
358
|
+
maxNodes: Infinity,
|
|
359
|
+
signal: options.signal,
|
|
360
|
+
});
|
|
361
|
+
return { path, totalCost, nodesExplored };
|
|
362
|
+
}
|
|
319
363
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
364
|
+
/**
|
|
365
|
+
* Topological sort (Kahn's algorithm).
|
|
366
|
+
*
|
|
367
|
+
* @param {string|string[]} start - One or more start nodes
|
|
368
|
+
* @param {Object} [options] - Traversal options
|
|
369
|
+
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
370
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
371
|
+
* @param {boolean} [options.throwOnCycle] - Whether to throw on cycle detection
|
|
372
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
373
|
+
* @returns {Promise<{sorted: string[], hasCycle: boolean}>}
|
|
374
|
+
* @throws {TraversalError} code 'ERR_GRAPH_HAS_CYCLES' if throwOnCycle and cycle found
|
|
375
|
+
* @throws {TraversalError} code 'NODE_NOT_FOUND' if a start node does not exist
|
|
376
|
+
*/
|
|
377
|
+
async topologicalSort(start, options = {}) {
|
|
378
|
+
const { engine, direction, options: opts } = await this._prepareEngine(options);
|
|
379
|
+
|
|
380
|
+
// Validate each start node
|
|
381
|
+
const starts = Array.isArray(start) ? start : [start];
|
|
382
|
+
for (const s of starts) {
|
|
383
|
+
if (!(await this._graph.hasNode(s))) {
|
|
384
|
+
throw new TraversalError(`Start node not found: ${s}`, {
|
|
385
|
+
code: 'NODE_NOT_FOUND',
|
|
386
|
+
context: { start: s },
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const { sorted, hasCycle } = await engine.topologicalSort({
|
|
392
|
+
start,
|
|
393
|
+
direction,
|
|
394
|
+
options: opts,
|
|
395
|
+
maxNodes: Infinity,
|
|
396
|
+
throwOnCycle: options.throwOnCycle,
|
|
397
|
+
signal: options.signal,
|
|
398
|
+
});
|
|
399
|
+
return { sorted, hasCycle };
|
|
400
|
+
}
|
|
331
401
|
|
|
332
|
-
|
|
402
|
+
/**
|
|
403
|
+
* Common ancestors — multi-source ancestor intersection.
|
|
404
|
+
*
|
|
405
|
+
* Direction is fixed to 'in' (backward BFS).
|
|
406
|
+
*
|
|
407
|
+
* @param {string[]} nodes - Nodes to find common ancestors of
|
|
408
|
+
* @param {Object} [options] - Traversal options
|
|
409
|
+
* @param {number} [options.maxDepth] - Maximum search depth
|
|
410
|
+
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
411
|
+
* @param {number} [options.maxResults] - Maximum number of results
|
|
412
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
413
|
+
* @returns {Promise<{ancestors: string[]}>}
|
|
414
|
+
* @throws {TraversalError} code 'NODE_NOT_FOUND' if a node does not exist
|
|
415
|
+
*/
|
|
416
|
+
async commonAncestors(nodes, options = {}) {
|
|
417
|
+
const { engine, options: opts, depthLimit } = await this._prepareEngine(options);
|
|
418
|
+
|
|
419
|
+
// Validate each node
|
|
420
|
+
for (const n of nodes) {
|
|
421
|
+
if (!(await this._graph.hasNode(n))) {
|
|
422
|
+
throw new TraversalError(`Node not found: ${n}`, {
|
|
423
|
+
code: 'NODE_NOT_FOUND',
|
|
424
|
+
context: { node: n },
|
|
425
|
+
});
|
|
333
426
|
}
|
|
334
427
|
}
|
|
335
428
|
|
|
336
|
-
|
|
429
|
+
const { ancestors } = await engine.commonAncestors({
|
|
430
|
+
nodes,
|
|
431
|
+
options: opts,
|
|
432
|
+
maxDepth: depthLimit,
|
|
433
|
+
maxResults: options.maxResults,
|
|
434
|
+
signal: options.signal,
|
|
435
|
+
});
|
|
436
|
+
return { ancestors };
|
|
337
437
|
}
|
|
338
438
|
|
|
339
439
|
/**
|
|
340
|
-
*
|
|
440
|
+
* Weighted longest path via topological sort + DP.
|
|
341
441
|
*
|
|
342
|
-
*
|
|
442
|
+
* Only valid on DAGs.
|
|
443
|
+
*
|
|
444
|
+
* @param {string} from - Source node ID
|
|
445
|
+
* @param {string} to - Target node ID
|
|
343
446
|
* @param {Object} [options] - Traversal options
|
|
344
|
-
* @param {
|
|
447
|
+
* @param {'out'|'in'|'both'} [options.dir] - Edge direction to follow
|
|
345
448
|
* @param {string|string[]} [options.labelFilter] - Edge label(s) to include
|
|
346
|
-
* @
|
|
347
|
-
* @
|
|
449
|
+
* @param {(from: string, to: string, label: string) => number | Promise<number>} [options.weightFn] - Edge weight function
|
|
450
|
+
* @param {(nodeId: string) => number | Promise<number>} [options.nodeWeightFn] - Node weight function (mutually exclusive with weightFn)
|
|
451
|
+
* @param {AbortSignal} [options.signal] - Abort signal
|
|
452
|
+
* @returns {Promise<{path: string[], totalCost: number}>}
|
|
453
|
+
* @throws {TraversalError} code 'ERR_GRAPH_HAS_CYCLES' if graph has cycles
|
|
454
|
+
* @throws {TraversalError} code 'NO_PATH' if unreachable
|
|
455
|
+
* @throws {TraversalError} code 'E_WEIGHT_FN_CONFLICT' if both weightFn and nodeWeightFn provided
|
|
348
456
|
*/
|
|
349
|
-
async
|
|
350
|
-
|
|
457
|
+
async weightedLongestPath(from, to, options = {}) {
|
|
458
|
+
const { engine, direction, options: opts } = await this._prepare(from, options);
|
|
459
|
+
const { path, totalCost } = await engine.weightedLongestPath({
|
|
460
|
+
start: from,
|
|
461
|
+
goal: to,
|
|
462
|
+
direction,
|
|
463
|
+
options: opts,
|
|
464
|
+
weightFn: options.weightFn,
|
|
465
|
+
nodeWeightFn: options.nodeWeightFn,
|
|
466
|
+
maxNodes: Infinity,
|
|
467
|
+
signal: options.signal,
|
|
468
|
+
});
|
|
469
|
+
return { path, totalCost };
|
|
351
470
|
}
|
|
352
471
|
}
|