flowseeker 0.1.7 → 0.1.8
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/.env.example +7 -0
- package/CHANGELOG.md +131 -108
- package/README.md +288 -221
- package/dist/cli/flowCommand.js +175 -0
- package/dist/cli/main.js +1794 -0
- package/dist/cli/mcpServer.js +7 -1
- package/dist/cli/runEvaluation.js +178 -2
- package/dist/config/defaultConfig.js +11 -1
- package/dist/config/env.js +118 -0
- package/dist/config/loadConfig.js +18 -1
- package/dist/config/loadConfigFromPath.js +3 -1
- package/dist/extension.js +23 -0
- package/dist/gateway/embeddingProviders.js +852 -0
- package/dist/index/cacheStore.js +43 -0
- package/dist/index/configRouteDiscoveryProbe.js +288 -0
- package/dist/index/embeddingIndex.js +193 -0
- package/dist/index/graphIndex.js +460 -0
- package/dist/index/indexWatcher.js +86 -0
- package/dist/index/structuredExtractor.js +303 -12
- package/dist/index/treeSitterExtractor.js +264 -0
- package/dist/index/vectorStore.js +41 -0
- package/dist/index/workspaceIndex.js +591 -26
- package/dist/mcp/mcpTools.js +51 -0
- package/dist/pipeline/contextPack.js +3 -3
- package/dist/pipeline/deterministicReranker.js +358 -0
- package/dist/pipeline/evaluationMetrics.js +14 -2
- package/dist/pipeline/fileGroups.js +7 -1
- package/dist/pipeline/fileScanner.js +93 -11
- package/dist/pipeline/llmReranker.js +151 -0
- package/dist/pipeline/nodeScan.js +91 -12
- package/dist/pipeline/ranker.js +875 -16
- package/dist/pipeline/retrievalFusion.js +41 -0
- package/dist/pipeline/runHeadless.js +35 -0
- package/dist/pipeline/solvePacket.js +549 -42
- package/dist/pipeline/subsystem.js +21 -0
- package/docs/demo-screenshot-checklist.md +86 -0
- package/docs/marketplace-copy.md +92 -0
- package/docs/mcp-onboarding.md +191 -0
- package/package.json +633 -561
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.expandIndexedGraph = expandIndexedGraph;
|
|
4
|
+
exports.buildIndexedGraph = buildIndexedGraph;
|
|
5
|
+
exports.getEdgesFrom = getEdgesFrom;
|
|
6
|
+
exports.getEdgesTo = getEdgesTo;
|
|
7
|
+
exports.getEdgesByKind = getEdgesByKind;
|
|
8
|
+
exports.getNodesByPath = getNodesByPath;
|
|
9
|
+
exports.getNodeById = getNodeById;
|
|
10
|
+
exports.dedupeGraphEdges = dedupeGraphEdges;
|
|
11
|
+
exports.extractGraphFromIndex = extractGraphFromIndex;
|
|
12
|
+
function expandIndexedGraph(index, seedPaths, options = {}) {
|
|
13
|
+
const lookup = buildGraphLookup(index);
|
|
14
|
+
const maxDepth = options.maxDepth ?? 2;
|
|
15
|
+
const maxEdgesPerSeed = options.maxEdgesPerSeed ?? 12;
|
|
16
|
+
const maxAddedFiles = options.maxAddedFiles ?? 25;
|
|
17
|
+
const maxPerEdgeType = options.maxPerEdgeType ?? 20;
|
|
18
|
+
const seen = new Set(seedPaths);
|
|
19
|
+
const edgeTypeCounts = new Map();
|
|
20
|
+
const queue = seedPaths.map((relativePath) => ({ relativePath, depth: 0 }));
|
|
21
|
+
const expanded = [];
|
|
22
|
+
while (queue.length > 0 && expanded.length < maxAddedFiles) {
|
|
23
|
+
const current = queue.shift();
|
|
24
|
+
if (!current || current.depth >= maxDepth) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const edges = edgesForPath(current.relativePath, lookup).slice(0, maxEdgesPerSeed);
|
|
28
|
+
for (const edge of edges) {
|
|
29
|
+
if ((edgeTypeCounts.get(edge.type) ?? 0) >= maxPerEdgeType) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const targets = targetFiles(edge, lookup);
|
|
33
|
+
for (const file of targets) {
|
|
34
|
+
if (seen.has(file.relativePath) || expanded.length >= maxAddedFiles) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
seen.add(file.relativePath);
|
|
38
|
+
edgeTypeCounts.set(edge.type, (edgeTypeCounts.get(edge.type) ?? 0) + 1);
|
|
39
|
+
expanded.push({
|
|
40
|
+
file,
|
|
41
|
+
distance: current.depth + 1,
|
|
42
|
+
viaEdge: edge,
|
|
43
|
+
reason: `${edge.type} from ${edge.fromPath || current.relativePath}: ${edge.reason}`
|
|
44
|
+
});
|
|
45
|
+
queue.push({ relativePath: file.relativePath, depth: current.depth + 1 });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return expanded;
|
|
50
|
+
}
|
|
51
|
+
function buildGraphLookup(index) {
|
|
52
|
+
const byFromPath = new Map();
|
|
53
|
+
const byToPath = new Map();
|
|
54
|
+
const bySymbolOrCall = new Map();
|
|
55
|
+
const byPath = new Map(index.files.map((file) => [file.relativePath, file]));
|
|
56
|
+
for (const file of index.files) {
|
|
57
|
+
for (const edge of file.edges ?? []) {
|
|
58
|
+
addEdge(byFromPath, edge.fromPath, edge);
|
|
59
|
+
addEdge(byToPath, edge.toPath, edge);
|
|
60
|
+
}
|
|
61
|
+
for (const symbol of file.symbols ?? []) {
|
|
62
|
+
addFile(bySymbolOrCall, normalizeName(symbol.name), file);
|
|
63
|
+
addFile(bySymbolOrCall, normalizeName(symbol.fqn), file);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { byFromPath, byToPath, bySymbolOrCall, byPath };
|
|
67
|
+
}
|
|
68
|
+
function edgesForPath(relativePath, lookup) {
|
|
69
|
+
return [...(lookup.byFromPath.get(relativePath) ?? []), ...(lookup.byToPath.get(relativePath) ?? [])];
|
|
70
|
+
}
|
|
71
|
+
function targetFiles(edge, lookup) {
|
|
72
|
+
const byDirectPath = [lookup.byPath.get(edge.toPath), lookup.byPath.get(edge.fromPath)].filter((file) => Boolean(file));
|
|
73
|
+
const bySymbol = lookup.bySymbolOrCall.get(normalizeName(edge.to)) ?? [];
|
|
74
|
+
return uniqueByPath([...byDirectPath, ...bySymbol]);
|
|
75
|
+
}
|
|
76
|
+
function addEdge(map, key, edge) {
|
|
77
|
+
if (!key) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
map.set(key, [...(map.get(key) ?? []), edge]);
|
|
81
|
+
}
|
|
82
|
+
function addFile(map, key, file) {
|
|
83
|
+
if (!key) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
map.set(key, [...(map.get(key) ?? []), file]);
|
|
87
|
+
}
|
|
88
|
+
function normalizeName(value) {
|
|
89
|
+
return value.toLowerCase().split(/\.|::|->/).filter(Boolean).pop() ?? value.toLowerCase();
|
|
90
|
+
}
|
|
91
|
+
function uniqueByPath(files) {
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
return files.filter((file) => {
|
|
94
|
+
if (seen.has(file.relativePath)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
seen.add(file.relativePath);
|
|
98
|
+
return true;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function norm(p) {
|
|
102
|
+
return (p || "").replace(/\\/g, "/");
|
|
103
|
+
}
|
|
104
|
+
function buildIndexedGraph(input) {
|
|
105
|
+
return {
|
|
106
|
+
nodes: input.nodes.map((n) => ({ ...n, relativePath: norm(n.relativePath) })),
|
|
107
|
+
edges: input.edges.map((e) => ({
|
|
108
|
+
...e,
|
|
109
|
+
fromPath: norm(e.fromPath),
|
|
110
|
+
toPath: norm(e.toPath)
|
|
111
|
+
}))
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function getEdgesFrom(index, nodeOrPath, options) {
|
|
115
|
+
const id = typeof nodeOrPath === "string" ? nodeOrPath : nodeOrPath.id;
|
|
116
|
+
const np = norm(typeof nodeOrPath === "string" ? nodeOrPath : nodeOrPath.relativePath);
|
|
117
|
+
let results = index.edges.filter((e) => e.fromId === id || norm(e.fromPath) === np);
|
|
118
|
+
if (options?.kind) {
|
|
119
|
+
results = results.filter((e) => e.kind === options.kind);
|
|
120
|
+
}
|
|
121
|
+
if (options?.maxResults !== undefined && results.length > options.maxResults) {
|
|
122
|
+
results = results.slice(0, options.maxResults);
|
|
123
|
+
}
|
|
124
|
+
return results;
|
|
125
|
+
}
|
|
126
|
+
function getEdgesTo(index, nodeOrPath, options) {
|
|
127
|
+
const id = typeof nodeOrPath === "string" ? nodeOrPath : nodeOrPath.id;
|
|
128
|
+
const np = norm(typeof nodeOrPath === "string" ? nodeOrPath : nodeOrPath.relativePath);
|
|
129
|
+
let results = index.edges.filter((e) => e.toId === id || norm(e.toPath) === np);
|
|
130
|
+
if (options?.kind) {
|
|
131
|
+
results = results.filter((e) => e.kind === options.kind);
|
|
132
|
+
}
|
|
133
|
+
if (options?.maxResults !== undefined && results.length > options.maxResults) {
|
|
134
|
+
results = results.slice(0, options.maxResults);
|
|
135
|
+
}
|
|
136
|
+
return results;
|
|
137
|
+
}
|
|
138
|
+
function getEdgesByKind(index, kind, options) {
|
|
139
|
+
let results = index.edges.filter((e) => e.kind === kind);
|
|
140
|
+
if (options?.maxResults !== undefined && results.length > options.maxResults) {
|
|
141
|
+
results = results.slice(0, options.maxResults);
|
|
142
|
+
}
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
function getNodesByPath(index, relativePath) {
|
|
146
|
+
const np = norm(relativePath);
|
|
147
|
+
return index.nodes.filter((n) => n.relativePath === np);
|
|
148
|
+
}
|
|
149
|
+
function getNodeById(index, id) {
|
|
150
|
+
return index.nodes.find((n) => n.id === id);
|
|
151
|
+
}
|
|
152
|
+
function structuralKey(e) {
|
|
153
|
+
return `${e.kind}|${e.fromId}|${e.toId}|${norm(e.fromPath)}|${norm(e.toPath)}`;
|
|
154
|
+
}
|
|
155
|
+
function dedupeGraphEdges(edges) {
|
|
156
|
+
const byId = new Map();
|
|
157
|
+
const byStructural = new Map();
|
|
158
|
+
for (const edge of edges) {
|
|
159
|
+
const sk = structuralKey(edge);
|
|
160
|
+
// Dedupe by stable id when present
|
|
161
|
+
if (edge.id) {
|
|
162
|
+
const existingById = byId.get(edge.id);
|
|
163
|
+
const existingBySk = byStructural.get(sk);
|
|
164
|
+
if (existingById || existingBySk) {
|
|
165
|
+
const best = pickHigherConfidence([existingById, existingBySk, edge].filter(Boolean));
|
|
166
|
+
byId.set(edge.id, best);
|
|
167
|
+
byStructural.set(sk, best);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
byId.set(edge.id, edge);
|
|
171
|
+
byStructural.set(sk, edge);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
// Dedupe by structural key when no id
|
|
175
|
+
const existing = byStructural.get(sk);
|
|
176
|
+
if (!existing || edge.confidence > existing.confidence) {
|
|
177
|
+
byStructural.set(sk, edge);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Merge: collect unique edges from both maps, prefer higher confidence
|
|
181
|
+
const seen = new Map();
|
|
182
|
+
for (const e of [...byId.values(), ...byStructural.values()]) {
|
|
183
|
+
const sk = structuralKey(e);
|
|
184
|
+
const existing = seen.get(sk);
|
|
185
|
+
if (!existing || e.confidence > existing.confidence) {
|
|
186
|
+
seen.set(sk, e);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return Array.from(seen.values());
|
|
190
|
+
}
|
|
191
|
+
function pickHigherConfidence(edges) {
|
|
192
|
+
return edges.reduce((a, b) => (a.confidence >= b.confidence ? a : b));
|
|
193
|
+
}
|
|
194
|
+
const AMBIGUOUS_CALL_NAMES = new Set([
|
|
195
|
+
"handle", "index", "show", "create", "store", "update", "delete",
|
|
196
|
+
"destroy", "run", "execute", "dispatch", "complete", "process",
|
|
197
|
+
"render", "boot", "register"
|
|
198
|
+
]);
|
|
199
|
+
function isAmbiguousCallName(name) {
|
|
200
|
+
return AMBIGUOUS_CALL_NAMES.has(name.toLowerCase());
|
|
201
|
+
}
|
|
202
|
+
// Count how many files define a symbol with this name
|
|
203
|
+
function countSymbolMatches(name, symbolIndex) {
|
|
204
|
+
return symbolIndex.get(name.toLowerCase())?.length ?? 0;
|
|
205
|
+
}
|
|
206
|
+
// Check if a call name resolves deterministically:
|
|
207
|
+
// - only one matching symbol exists across the workspace
|
|
208
|
+
// - OR the caller imports a file that contains exactly one matching symbol
|
|
209
|
+
function isDeterministicCallTarget(callName, fromFile, symbolIndex) {
|
|
210
|
+
const matches = symbolIndex.get(callName.toLowerCase()) ?? [];
|
|
211
|
+
if (matches.length === 1)
|
|
212
|
+
return true;
|
|
213
|
+
// Check if any import resolves to a file with exactly one match
|
|
214
|
+
for (const imp of fromFile.imports) {
|
|
215
|
+
const impBase = imp.replace(/\\/g, "/").split("/").pop()?.replace(/\.[^.]+$/, "").toLowerCase() || "";
|
|
216
|
+
// Check each match — if a match's file basename matches the import basename
|
|
217
|
+
for (const m of matches) {
|
|
218
|
+
const matchBase = m.np.split("/").pop()?.replace(/\.[^.]+$/, "").toLowerCase() || "";
|
|
219
|
+
if (matchBase === impBase)
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
function extractGraphFromIndex(index) {
|
|
226
|
+
const stats = {
|
|
227
|
+
duplicatesRemoved: 0,
|
|
228
|
+
unresolvedImportsSkipped: 0,
|
|
229
|
+
unresolvedLocalImportsSkipped: 0,
|
|
230
|
+
packageImportsSkipped: 0,
|
|
231
|
+
emptyPathEdgesSkipped: 0,
|
|
232
|
+
selfEdgesSkipped: 0,
|
|
233
|
+
ambiguousCallsSkipped: 0,
|
|
234
|
+
unresolvedCallsSkipped: 0,
|
|
235
|
+
callsKept: 0,
|
|
236
|
+
callsSkippedByAmbiguousName: {}
|
|
237
|
+
};
|
|
238
|
+
const pathIndex = new Map();
|
|
239
|
+
for (const file of index.files) {
|
|
240
|
+
pathIndex.set(norm(file.relativePath), file);
|
|
241
|
+
}
|
|
242
|
+
// File nodes: one per indexed file
|
|
243
|
+
const nodes = [];
|
|
244
|
+
const nodeIds = new Set();
|
|
245
|
+
for (const file of index.files) {
|
|
246
|
+
const np = norm(file.relativePath);
|
|
247
|
+
const id = `file:${np}`;
|
|
248
|
+
if (nodeIds.has(id))
|
|
249
|
+
continue;
|
|
250
|
+
nodeIds.add(id);
|
|
251
|
+
nodes.push({
|
|
252
|
+
id,
|
|
253
|
+
kind: "file",
|
|
254
|
+
relativePath: np,
|
|
255
|
+
label: np.split("/").pop() || np
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
// Symbol nodes: for files with symbol data
|
|
259
|
+
const symbolIndex = new Map(); // normalized name → file lookup
|
|
260
|
+
for (const file of index.files) {
|
|
261
|
+
const np = norm(file.relativePath);
|
|
262
|
+
for (const sym of file.symbols) {
|
|
263
|
+
const sid = `symbol:${np}:${sym.name}`;
|
|
264
|
+
if (nodeIds.has(sid))
|
|
265
|
+
continue;
|
|
266
|
+
nodeIds.add(sid);
|
|
267
|
+
nodes.push({
|
|
268
|
+
id: sid,
|
|
269
|
+
kind: "symbol",
|
|
270
|
+
relativePath: np,
|
|
271
|
+
label: sym.name,
|
|
272
|
+
symbolFqn: sym.fqn || sym.name,
|
|
273
|
+
range: sym.line > 0 ? { startLine: sym.line, endLine: sym.endLine || sym.line } : undefined
|
|
274
|
+
});
|
|
275
|
+
const symName = sym.name ? sym.name.toLowerCase() : "";
|
|
276
|
+
if (symName) {
|
|
277
|
+
const list = symbolIndex.get(symName) ?? [];
|
|
278
|
+
list.push({ file, np });
|
|
279
|
+
symbolIndex.set(symName, list);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const edges = [];
|
|
284
|
+
let edgeCounter = 0;
|
|
285
|
+
for (const file of index.files) {
|
|
286
|
+
const fromNp = norm(file.relativePath);
|
|
287
|
+
const fromFileId = `file:${fromNp}`;
|
|
288
|
+
// Import edges
|
|
289
|
+
for (const imp of file.imports) {
|
|
290
|
+
if (!imp || imp.trim() === "") {
|
|
291
|
+
stats.emptyPathEdgesSkipped++;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const impNorm = imp.replace(/\\/g, "/");
|
|
295
|
+
// Distinguish package imports from unresolved local imports
|
|
296
|
+
const isPackage = !impNorm.startsWith(".") && !impNorm.startsWith("/");
|
|
297
|
+
const resolved = resolveImportPath(fromNp, imp, pathIndex);
|
|
298
|
+
if (!resolved) {
|
|
299
|
+
stats.unresolvedImportsSkipped++;
|
|
300
|
+
if (isPackage) {
|
|
301
|
+
stats.packageImportsSkipped++;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
stats.unresolvedLocalImportsSkipped++;
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const toNp = norm(resolved);
|
|
309
|
+
// Skip self-edges for imports of the same file
|
|
310
|
+
if (toNp === fromNp) {
|
|
311
|
+
stats.selfEdgesSkipped++;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const toFileId = `file:${toNp}`;
|
|
315
|
+
edgeCounter++;
|
|
316
|
+
edges.push({
|
|
317
|
+
id: `import:${edgeCounter}:${fromNp}->${toNp}`,
|
|
318
|
+
kind: "import",
|
|
319
|
+
fromId: fromFileId,
|
|
320
|
+
toId: toFileId,
|
|
321
|
+
fromPath: fromNp,
|
|
322
|
+
toPath: toNp,
|
|
323
|
+
confidence: 0.85,
|
|
324
|
+
source: "import",
|
|
325
|
+
reason: `imports ${imp}`
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
// Call edges
|
|
329
|
+
for (const call of file.calls || []) {
|
|
330
|
+
if (!call || call.trim() === "") {
|
|
331
|
+
stats.emptyPathEdgesSkipped++;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const callName = call.toLowerCase();
|
|
335
|
+
const targets = symbolIndex.get(callName);
|
|
336
|
+
if (!targets || targets.length === 0) {
|
|
337
|
+
stats.unresolvedCallsSkipped++;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
// Ambiguity guard: skip ambiguous call names unless deterministic
|
|
341
|
+
if (isAmbiguousCallName(callName)) {
|
|
342
|
+
if (!isDeterministicCallTarget(callName, file, symbolIndex)) {
|
|
343
|
+
stats.ambiguousCallsSkipped++;
|
|
344
|
+
stats.callsSkippedByAmbiguousName[callName] = (stats.callsSkippedByAmbiguousName[callName] ?? 0) + 1;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Use the first target (there should be exactly 1 for deterministic)
|
|
349
|
+
// For ambiguous names that passed the guard, prefer target from import
|
|
350
|
+
let target = targets[0];
|
|
351
|
+
if (isAmbiguousCallName(callName) && targets.length > 1) {
|
|
352
|
+
const impBase = new Set(file.imports.map(imp => imp.replace(/\\/g, "/").split("/").pop()?.replace(/\.[^.]+$/, "").toLowerCase() || ""));
|
|
353
|
+
const matching = targets.find(t => impBase.has(t.np.split("/").pop()?.replace(/\.[^.]+$/, "").toLowerCase() || ""));
|
|
354
|
+
if (matching)
|
|
355
|
+
target = matching;
|
|
356
|
+
}
|
|
357
|
+
const toNp = norm(target.file.relativePath);
|
|
358
|
+
// Skip self-calls
|
|
359
|
+
if (toNp === fromNp) {
|
|
360
|
+
stats.selfEdgesSkipped++;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
const fromSymbolId = file.symbols.length > 0
|
|
364
|
+
? `symbol:${fromNp}:${file.symbols[0].name}`
|
|
365
|
+
: fromFileId;
|
|
366
|
+
const toSymbolId = `symbol:${toNp}:${call}`;
|
|
367
|
+
edgeCounter++;
|
|
368
|
+
edges.push({
|
|
369
|
+
id: `call:${edgeCounter}:${fromNp}->${toNp}`,
|
|
370
|
+
kind: "calls",
|
|
371
|
+
fromId: fromSymbolId,
|
|
372
|
+
toId: toSymbolId,
|
|
373
|
+
fromPath: fromNp,
|
|
374
|
+
toPath: toNp,
|
|
375
|
+
confidence: 0.70,
|
|
376
|
+
source: "parser",
|
|
377
|
+
reason: `calls ${call}`
|
|
378
|
+
});
|
|
379
|
+
stats.callsKept++;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Deduplicate
|
|
383
|
+
const deduped = dedupeGraphEdges(edges);
|
|
384
|
+
stats.duplicatesRemoved = edges.length - deduped.length;
|
|
385
|
+
return {
|
|
386
|
+
graph: { nodes, edges: deduped },
|
|
387
|
+
stats
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function resolveImportPath(fromFile, importSource, pathIndex) {
|
|
391
|
+
// Normalize the import source
|
|
392
|
+
let imp = importSource.replace(/\\/g, "/");
|
|
393
|
+
// Helper: check if pathIndex has a file, trying original and normalized case
|
|
394
|
+
const lookupIndex = (candidate) => {
|
|
395
|
+
if (pathIndex.has(candidate))
|
|
396
|
+
return candidate;
|
|
397
|
+
const lower = candidate.toLowerCase();
|
|
398
|
+
if (pathIndex.has(lower))
|
|
399
|
+
return lower;
|
|
400
|
+
// Also try finding a case-insensitive match
|
|
401
|
+
for (const key of pathIndex.keys()) {
|
|
402
|
+
if (key.toLowerCase() === lower)
|
|
403
|
+
return key;
|
|
404
|
+
}
|
|
405
|
+
return null;
|
|
406
|
+
};
|
|
407
|
+
// Skip package imports (no leading . or /)
|
|
408
|
+
if (!imp.startsWith(".") && !imp.startsWith("/")) {
|
|
409
|
+
// Workspace-relative path (e.g., "app/Services/PaymentService")
|
|
410
|
+
if (imp.includes("/")) {
|
|
411
|
+
for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".php", ".py", ".dart", ".java", ".go"]) {
|
|
412
|
+
const result = lookupIndex(imp + ext);
|
|
413
|
+
if (result)
|
|
414
|
+
return result;
|
|
415
|
+
}
|
|
416
|
+
const direct = lookupIndex(imp);
|
|
417
|
+
if (direct)
|
|
418
|
+
return direct;
|
|
419
|
+
}
|
|
420
|
+
// Basename matching for simple names
|
|
421
|
+
const base = imp.split("/").pop()?.toLowerCase() || "";
|
|
422
|
+
if (base) {
|
|
423
|
+
for (const [filePath] of pathIndex) {
|
|
424
|
+
const fileBase = filePath.split("/").pop()?.replace(/\.[^.]+$/, "").toLowerCase() || "";
|
|
425
|
+
if (fileBase === base)
|
|
426
|
+
return filePath;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
// Relative import: resolve from fromFile directory
|
|
432
|
+
const fromDir = fromFile.includes("/") ? fromFile.slice(0, fromFile.lastIndexOf("/")) : "";
|
|
433
|
+
const parts = fromDir ? fromDir.split("/") : [];
|
|
434
|
+
for (const seg of imp.split("/")) {
|
|
435
|
+
if (seg === "..") {
|
|
436
|
+
parts.pop();
|
|
437
|
+
}
|
|
438
|
+
else if (seg !== ".") {
|
|
439
|
+
parts.push(seg);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const candidate = parts.join("/");
|
|
443
|
+
const directHit = lookupIndex(candidate);
|
|
444
|
+
if (directHit)
|
|
445
|
+
return directHit;
|
|
446
|
+
// Try common extensions
|
|
447
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx", ".php", ".py", ".dart", ".java", ".go", ".vue", ".svelte"]) {
|
|
448
|
+
const result = lookupIndex(candidate + ext);
|
|
449
|
+
if (result)
|
|
450
|
+
return result;
|
|
451
|
+
}
|
|
452
|
+
// Try index files
|
|
453
|
+
for (const idx of ["/index.ts", "/index.tsx", "/index.js", "/index.jsx", "/index.php"]) {
|
|
454
|
+
const result = lookupIndex(candidate + idx);
|
|
455
|
+
if (result)
|
|
456
|
+
return result;
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
//# sourceMappingURL=graphIndex.js.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Phase 06 Sprint 6E-R1 — VS Code Index Watcher: Debounce, Batch, Invalidation Callback
|
|
3
|
+
// Tracks workspace file changes for safe index invalidation.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.createIndexWatcher = createIndexWatcher;
|
|
6
|
+
exports.onFileChanged = onFileChanged;
|
|
7
|
+
exports.onFileDeleted = onFileDeleted;
|
|
8
|
+
exports.onFileCreated = onFileCreated;
|
|
9
|
+
exports.flush = flush;
|
|
10
|
+
exports.dispose = dispose;
|
|
11
|
+
function createIndexWatcher(options) {
|
|
12
|
+
return {
|
|
13
|
+
changedPaths: new Set(),
|
|
14
|
+
deletedPaths: new Set(),
|
|
15
|
+
timer: null,
|
|
16
|
+
options,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function onFileChanged(state, relativePath) {
|
|
20
|
+
if (isIgnored(relativePath, state.options))
|
|
21
|
+
return;
|
|
22
|
+
const np = normalizePath(relativePath);
|
|
23
|
+
state.changedPaths.add(np);
|
|
24
|
+
state.deletedPaths.delete(np);
|
|
25
|
+
scheduleFlush(state);
|
|
26
|
+
}
|
|
27
|
+
function onFileDeleted(state, relativePath) {
|
|
28
|
+
if (isIgnored(relativePath, state.options))
|
|
29
|
+
return;
|
|
30
|
+
const np = normalizePath(relativePath);
|
|
31
|
+
state.deletedPaths.add(np);
|
|
32
|
+
state.changedPaths.delete(np);
|
|
33
|
+
scheduleFlush(state);
|
|
34
|
+
}
|
|
35
|
+
function onFileCreated(state, relativePath) {
|
|
36
|
+
if (isIgnored(relativePath, state.options))
|
|
37
|
+
return;
|
|
38
|
+
const np = normalizePath(relativePath);
|
|
39
|
+
state.changedPaths.add(np);
|
|
40
|
+
scheduleFlush(state);
|
|
41
|
+
}
|
|
42
|
+
function flush(state) {
|
|
43
|
+
if (state.timer !== null) {
|
|
44
|
+
clearTimeout(state.timer);
|
|
45
|
+
state.timer = null;
|
|
46
|
+
}
|
|
47
|
+
const changed = Array.from(state.changedPaths);
|
|
48
|
+
const deleted = Array.from(state.deletedPaths);
|
|
49
|
+
state.changedPaths.clear();
|
|
50
|
+
state.deletedPaths.clear();
|
|
51
|
+
if (state.options.onFlush) {
|
|
52
|
+
state.options.onFlush(changed, deleted);
|
|
53
|
+
}
|
|
54
|
+
return { changed, deleted };
|
|
55
|
+
}
|
|
56
|
+
function dispose(state) {
|
|
57
|
+
if (state.timer !== null) {
|
|
58
|
+
clearTimeout(state.timer);
|
|
59
|
+
state.timer = null;
|
|
60
|
+
}
|
|
61
|
+
state.changedPaths.clear();
|
|
62
|
+
state.deletedPaths.clear();
|
|
63
|
+
}
|
|
64
|
+
function scheduleFlush(state) {
|
|
65
|
+
if (state.timer !== null)
|
|
66
|
+
return;
|
|
67
|
+
state.timer = setTimeout(() => {
|
|
68
|
+
flush(state);
|
|
69
|
+
}, state.options.debounceMs);
|
|
70
|
+
}
|
|
71
|
+
function normalizePath(p) {
|
|
72
|
+
return (p || "").replace(/\\/g, "/").toLowerCase();
|
|
73
|
+
}
|
|
74
|
+
function isIgnored(relativePath, options) {
|
|
75
|
+
const np = normalizePath(relativePath);
|
|
76
|
+
const noiseDirs = ["node_modules", ".git", ".flowseeker", "dist", "build", "coverage", ".next", "vendor", "__pycache__"];
|
|
77
|
+
const segments = np.split("/");
|
|
78
|
+
for (const seg of segments) {
|
|
79
|
+
if (noiseDirs.includes(seg))
|
|
80
|
+
return true;
|
|
81
|
+
if (seg.startsWith(".") && seg !== ".env")
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=indexWatcher.js.map
|