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.
Files changed (39) hide show
  1. package/.env.example +7 -0
  2. package/CHANGELOG.md +131 -108
  3. package/README.md +288 -221
  4. package/dist/cli/flowCommand.js +175 -0
  5. package/dist/cli/main.js +1794 -0
  6. package/dist/cli/mcpServer.js +7 -1
  7. package/dist/cli/runEvaluation.js +178 -2
  8. package/dist/config/defaultConfig.js +11 -1
  9. package/dist/config/env.js +118 -0
  10. package/dist/config/loadConfig.js +18 -1
  11. package/dist/config/loadConfigFromPath.js +3 -1
  12. package/dist/extension.js +23 -0
  13. package/dist/gateway/embeddingProviders.js +852 -0
  14. package/dist/index/cacheStore.js +43 -0
  15. package/dist/index/configRouteDiscoveryProbe.js +288 -0
  16. package/dist/index/embeddingIndex.js +193 -0
  17. package/dist/index/graphIndex.js +460 -0
  18. package/dist/index/indexWatcher.js +86 -0
  19. package/dist/index/structuredExtractor.js +303 -12
  20. package/dist/index/treeSitterExtractor.js +264 -0
  21. package/dist/index/vectorStore.js +41 -0
  22. package/dist/index/workspaceIndex.js +591 -26
  23. package/dist/mcp/mcpTools.js +51 -0
  24. package/dist/pipeline/contextPack.js +3 -3
  25. package/dist/pipeline/deterministicReranker.js +358 -0
  26. package/dist/pipeline/evaluationMetrics.js +14 -2
  27. package/dist/pipeline/fileGroups.js +7 -1
  28. package/dist/pipeline/fileScanner.js +93 -11
  29. package/dist/pipeline/llmReranker.js +151 -0
  30. package/dist/pipeline/nodeScan.js +91 -12
  31. package/dist/pipeline/ranker.js +875 -16
  32. package/dist/pipeline/retrievalFusion.js +41 -0
  33. package/dist/pipeline/runHeadless.js +35 -0
  34. package/dist/pipeline/solvePacket.js +549 -42
  35. package/dist/pipeline/subsystem.js +21 -0
  36. package/docs/demo-screenshot-checklist.md +86 -0
  37. package/docs/marketplace-copy.md +92 -0
  38. package/docs/mcp-onboarding.md +191 -0
  39. package/package.json +633 -561
@@ -36,15 +36,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadOrBuildWorkspaceIndex = loadOrBuildWorkspaceIndex;
37
37
  exports.selectIndexCandidates = selectIndexCandidates;
38
38
  exports.clearNodeWorkspaceIndex = clearNodeWorkspaceIndex;
39
+ const crypto = __importStar(require("crypto"));
39
40
  const fs = __importStar(require("fs/promises"));
40
41
  const path = __importStar(require("path"));
41
42
  const dependencyExtractor_1 = require("./dependencyExtractor");
42
43
  const fileDiscovery_1 = require("./fileDiscovery");
44
+ const embeddingIndex_1 = require("./embeddingIndex");
45
+ const graphIndex_1 = require("./graphIndex");
43
46
  const structuredExtractor_1 = require("./structuredExtractor");
47
+ const treeSitterExtractor_1 = require("./treeSitterExtractor");
48
+ const retrievalFusion_1 = require("../pipeline/retrievalFusion");
44
49
  const text_1 = require("../utils/text");
45
50
  const fileScanner_1 = require("../pipeline/fileScanner");
46
51
  const subsystem_1 = require("../pipeline/subsystem");
47
- const indexVersion = 6;
52
+ const indexVersion = 7;
53
+ const regexExtractorVersion = "structured-regex-mvp-1";
54
+ const treeSitterExtractorVersion = "tree-sitter-mvp-1";
55
+ const chunkerVersion = "symbol-range-mvp-1";
56
+ const graphVersion = "indexed-edges-mvp-1";
48
57
  const tokenLimitPerFile = 2500;
49
58
  async function loadOrBuildWorkspaceIndex(rootPath, config, options = {}) {
50
59
  const startedAt = options.startedAt ?? Date.now();
@@ -54,6 +63,8 @@ async function loadOrBuildWorkspaceIndex(rootPath, config, options = {}) {
54
63
  const currentFingerprint = fingerprintConfig(config);
55
64
  const reusablePriorIndex = priorIndex?.configFingerprint === currentFingerprint ? priorIndex : undefined;
56
65
  const priorByPath = new Map((reusablePriorIndex?.files ?? []).map((file) => [file.relativePath, file]));
66
+ const discoveredPathSet = new Set(discovery.files.map((file) => file.relativePath));
67
+ const deletedFiles = (reusablePriorIndex?.files ?? []).filter((file) => !discoveredPathSet.has(file.relativePath)).length;
57
68
  const files = [];
58
69
  let reusedFiles = 0;
59
70
  let updatedFiles = 0;
@@ -104,29 +115,58 @@ async function loadOrBuildWorkspaceIndex(rootPath, config, options = {}) {
104
115
  configFingerprint: currentFingerprint,
105
116
  files
106
117
  };
107
- await writeIndex(resolvedRoot, index);
118
+ if (!options.readOnly) {
119
+ await writeIndex(resolvedRoot, index);
120
+ }
108
121
  return {
109
122
  index,
110
123
  discoveredFiles: discovery.files.length,
111
124
  reusedFiles,
112
125
  updatedFiles,
126
+ deletedFiles,
113
127
  indexedBytes: files.reduce((sum, file) => sum + file.size, 0),
114
128
  indexedTokens: files.reduce((sum, file) => sum + (file.tokenCount ?? Math.ceil(file.size / 4)), 0),
115
129
  stopReason
116
130
  };
117
131
  }
118
- function selectIndexCandidates(index, profile, config) {
132
+ async function selectIndexCandidates(index, profile, config) {
119
133
  const terms = (0, fileScanner_1.getWeightedTerms)(profile);
120
134
  if (terms.length === 0) {
121
- return [];
135
+ return { files: [], semanticStatus: "disabled", deepCandidateDiscoveryCandidates: 0, deepCandidateDiscoveryAdded: 0, deepCandidateDiscoveryAvoidBlocked: 0, semanticEmbeddedFiles: 0, semanticCacheHits: 0, semanticCacheMisses: 0, semanticHitCount: 0, semanticAddedFileCount: 0, semanticSupportedFileCount: 0 };
122
136
  }
123
- const scored = index.files
124
- .map((file) => ({
125
- file,
126
- score: scoreIndexedFile(file, terms, profile)
137
+ const baseHits = buildIndexChannelHits(index.files, terms, profile);
138
+ const semanticResult = await (0, embeddingIndex_1.searchSemanticFiles)(index.rootPath, index, profile.normalizedTask || profile.rawTask, config);
139
+ // Only add semantic hits for files NOT already discovered by other channels.
140
+ // This prevents semantic from modifying the ranking of lexically-discovered files.
141
+ const discoveredFiles = new Set(baseHits.map((h) => h.relativePath));
142
+ // Track semantic contributions + evidence for supported lexical files
143
+ let semanticAddedFileCount = 0;
144
+ let semanticSupportedFileCount = 0;
145
+ const semanticSupportByPath = new Map();
146
+ const semanticDiscoveryByPath = new Map();
147
+ semanticResult.hits.forEach((hit, rank) => {
148
+ if (discoveredFiles.has(hit.relativePath)) {
149
+ semanticSupportedFileCount++;
150
+ semanticSupportByPath.set(hit.relativePath, { score: hit.score, rank: rank + 1 });
151
+ return; // supported existing file but don't modify ranking
152
+ }
153
+ semanticAddedFileCount++;
154
+ semanticDiscoveryByPath.set(hit.relativePath, { score: hit.score, rank: rank + 1 });
155
+ baseHits.push({
156
+ channel: "semantic_vector",
157
+ relativePath: hit.relativePath,
158
+ score: hit.score,
159
+ rank: rank + 1,
160
+ reason: `semantic vector similarity ${hit.score.toFixed(3)} rank=${rank + 1} discovered by semantic`
161
+ });
162
+ });
163
+ const fused = (0, retrievalFusion_1.fuseRetrievalChannelHits)(baseHits);
164
+ const scored = fused
165
+ .map((candidate) => ({
166
+ candidate,
167
+ file: index.files.find((file) => file.relativePath === candidate.relativePath)
127
168
  }))
128
- .filter((item) => item.score > 0)
129
- .sort((left, right) => right.score - left.score);
169
+ .filter((item) => Boolean(item.file));
130
170
  const selected = new Map();
131
171
  const maxCandidates = config.index.maxIndexedCandidateFiles;
132
172
  const maxSeeds = Math.min(config.index.maxSeedCandidateFiles, maxCandidates);
@@ -134,6 +174,8 @@ function selectIndexCandidates(index, profile, config) {
134
174
  for (const seed of seeds) {
135
175
  selected.set(seed.relativePath, seed);
136
176
  }
177
+ const candidateTrace = new Map(scored.map((item) => [item.file.relativePath, item.candidate]));
178
+ const graphReasons = new Map();
137
179
  if (config.index.enableDependencyExpansion && selected.size < maxCandidates) {
138
180
  const maxExpansion = Math.min(config.index.maxDependencyExpansionFiles, maxCandidates - selected.size);
139
181
  let expanded = 0;
@@ -147,16 +189,164 @@ function selectIndexCandidates(index, profile, config) {
147
189
  }
148
190
  }
149
191
  }
192
+ if (config.index.enableDependencyExpansion && selected.size < maxCandidates) {
193
+ const graphBudget = Math.min(25, maxCandidates - selected.size);
194
+ for (const related of (0, graphIndex_1.expandIndexedGraph)(index, seeds.map((seed) => seed.relativePath), { maxAddedFiles: graphBudget })) {
195
+ if (selected.size >= maxCandidates) {
196
+ break;
197
+ }
198
+ selected.set(related.file.relativePath, related.file);
199
+ graphReasons.set(related.file.relativePath, [...(graphReasons.get(related.file.relativePath) ?? []), related.reason]);
200
+ }
201
+ }
202
+ // Attach semantic support evidence to lexically-discovered files (evidence-only, no ranking change)
203
+ for (const [relPath, semInfo] of semanticSupportByPath) {
204
+ if (selected.has(relPath)) {
205
+ const reasons = graphReasons.get(relPath) ?? [];
206
+ reasons.push("semantic_vector: similarity " + semInfo.score.toFixed(3) + " rank=" + semInfo.rank + " supported existing candidate");
207
+ graphReasons.set(relPath, reasons);
208
+ }
209
+ }
210
+ // Attach semantic discovered evidence to selected files
211
+ for (const [relPath, semInfo] of semanticDiscoveryByPath) {
212
+ if (selected.has(relPath)) {
213
+ const reasons = graphReasons.get(relPath) ?? [];
214
+ reasons.push("semantic_vector: similarity " + semInfo.score.toFixed(3) + " rank=" + semInfo.rank + " discovered by semantic");
215
+ graphReasons.set(relPath, reasons);
216
+ }
217
+ }
218
+ // Semantic candidate expansion: adds new files from semantic search that
219
+ // lexical channels missed. Used with fusion isolation (semantic hits only
220
+ // for undiscovered files) to avoid modifying lexical ranking of known files.
221
+ if (semanticResult.hits.length > 0 && selected.size < maxCandidates &&
222
+ semanticResult.providerName !== "deterministicTest") {
223
+ const semanticBudget = Math.min(40, maxCandidates - selected.size);
224
+ let added = 0;
225
+ for (const hit of semanticResult.hits) {
226
+ if (added >= semanticBudget || selected.size >= maxCandidates)
227
+ break;
228
+ const file = index.files.find((f) => f.relativePath === hit.relativePath);
229
+ if (!file || selected.has(file.relativePath))
230
+ continue;
231
+ if (isExpansionAvoidPath(file.relativePath))
232
+ continue;
233
+ selected.set(file.relativePath, file);
234
+ added++;
235
+ graphReasons.set(file.relativePath, [...(graphReasons.get(file.relativePath) ?? []), `semantic vector similarity ${hit.score.toFixed(3)} rank=${added} discovered by semantic`]);
236
+ }
237
+ }
238
+ // Deep candidate discovery: scan indexed files NOT already selected
239
+ // for files with 3+ independent deterministic evidence signals.
240
+ // Always evaluates for telemetry. Adds at most 3 (or replaces if pool full).
241
+ let deepCandidateDiscoveryCandidates = 0;
242
+ let deepCandidateDiscoveryAdded = 0;
243
+ let deepCandidateDiscoveryAvoidBlocked = 0;
244
+ // Always evaluate for telemetry, regardless of pool capacity
245
+ const allCandidates = discoverDeepCandidates(index.files, selected, profile, terms, 999);
246
+ deepCandidateDiscoveryCandidates = allCandidates.length;
247
+ if (allCandidates.length > 0) {
248
+ if (selected.size < maxCandidates) {
249
+ // Pool has space: add directly
250
+ const deepBudget = Math.min(3, maxCandidates - selected.size);
251
+ const addedCandidates = allCandidates.slice(0, deepBudget);
252
+ deepCandidateDiscoveryAdded = addedCandidates.length;
253
+ for (const dc of addedCandidates) {
254
+ selected.set(dc.file.relativePath, dc.file);
255
+ graphReasons.set(dc.file.relativePath, [...(graphReasons.get(dc.file.relativePath) ?? []), dc.reason]);
256
+ }
257
+ }
258
+ else {
259
+ // Pool is full: replace up to 3 lowest-priority non-required below top7
260
+ const requiredSlots = new Set(profile.blueprint.requiredSlots);
261
+ const selectedArr = Array.from(selected.entries()); // [path, file][]
262
+ const maxReplace = Math.min(3, allCandidates.length);
263
+ // Build replacement targets: indices >= 7 where the file is not a sole required-slot occupant
264
+ const slotOccupants = new Map();
265
+ for (const [, file] of selectedArr) {
266
+ const role = classifyDeepPathRole(file.relativePath);
267
+ if (role)
268
+ slotOccupants.set(role, (slotOccupants.get(role) ?? 0) + 1);
269
+ }
270
+ // Find replaceable indices (7+, non-sole-required, lowest priority = highest index)
271
+ const replaceTargets = [];
272
+ for (let i = selectedArr.length - 1; i >= 7 && replaceTargets.length < maxReplace; i--) {
273
+ const [, file] = selectedArr[i];
274
+ const role = classifyDeepPathRole(file.relativePath);
275
+ const isSoleRequired = role && requiredSlots.has(role) && (slotOccupants.get(role) ?? 0) <= 1;
276
+ if (!isSoleRequired) {
277
+ replaceTargets.push(i);
278
+ }
279
+ }
280
+ let added = 0;
281
+ for (const dc of allCandidates) {
282
+ if (added >= maxReplace || added >= replaceTargets.length)
283
+ break;
284
+ const targetIdx = replaceTargets[added];
285
+ const [oldPath] = selectedArr[targetIdx];
286
+ selected.delete(oldPath);
287
+ selected.set(dc.file.relativePath, dc.file);
288
+ graphReasons.set(dc.file.relativePath, [...(graphReasons.get(dc.file.relativePath) ?? []), dc.reason + " (replaced pool-full slot)"]);
289
+ added++;
290
+ }
291
+ deepCandidateDiscoveryAdded = added;
292
+ }
293
+ }
150
294
  for (const item of scored) {
151
295
  if (selected.size >= maxCandidates) {
152
296
  break;
153
297
  }
154
298
  selected.set(item.file.relativePath, item.file);
155
299
  }
156
- return Array.from(selected.values()).map((item) => ({
157
- absolutePath: path.join(index.rootPath, item.relativePath),
158
- relativePath: item.relativePath
159
- }));
300
+ return {
301
+ files: Array.from(selected.values()).map((item) => {
302
+ const trace = candidateTrace.get(item.relativePath);
303
+ const graphTrace = graphReasons.get(item.relativePath) ?? [];
304
+ return {
305
+ absolutePath: path.join(index.rootPath, item.relativePath),
306
+ relativePath: item.relativePath,
307
+ channelHits: trace?.channelHits,
308
+ retrievalTrace: trace?.reasons,
309
+ graphReasons: graphTrace
310
+ };
311
+ }),
312
+ semanticStatus: semanticResult.status,
313
+ semanticErrorType: semanticResult.errorType,
314
+ semanticErrorMessage: semanticResult.errorMessage,
315
+ semanticEmbeddedFiles: semanticResult.embeddedFiles,
316
+ semanticCacheHits: semanticResult.cacheHits,
317
+ semanticCacheMisses: semanticResult.cacheMisses,
318
+ semanticHitCount: semanticResult.hits.length,
319
+ semanticAddedFileCount,
320
+ semanticSupportedFileCount,
321
+ deepCandidateDiscoveryCandidates,
322
+ deepCandidateDiscoveryAdded,
323
+ deepCandidateDiscoveryAvoidBlocked
324
+ };
325
+ }
326
+ function isExpansionAvoidPath(normalizedPath) {
327
+ const p = normalizedPath.replace(/\\/g, "/").toLowerCase();
328
+ // Helper/util/console/command files
329
+ if (/(^|\/)(helpers?|utils?|console|commands?|scripts?|tasks?)(\/|$)/.test(p))
330
+ return true;
331
+ // Admin/dashboard surfaces
332
+ if (/(^|\/)(admin|dashboard)(\/|$)/.test(p))
333
+ return true;
334
+ // Lock files
335
+ if (/(package-lock|yarn\.lock|pnpm-lock|composer\.lock|Gemfile\.lock|poetry\.lock|cargo\.lock)$/.test(p))
336
+ return true;
337
+ // Templates/views
338
+ if (/(^|\/)(views?|templates?|resources\/views)(\/|$)|\.(blade\.php|twig|erb|hbs|ejs)$/.test(p))
339
+ return true;
340
+ // Generated/snapshot
341
+ if (/(^|\/)(generated|__generated__|gen|codegen|snapshots?)(\/|$)|\.snap$|\.generated\./.test(p))
342
+ return true;
343
+ // Static assets
344
+ if (/(^|\/)(public|assets?|static|images?|fonts?)(\/|$)|\.(css|scss|less|svg|png|jpe?g|gif|ico|woff2?)$/.test(p))
345
+ return true;
346
+ // Notification/mail files unless specific pattern match
347
+ if (/(^|\/)(notifications?|mail|email)(\/|$)/.test(p))
348
+ return true;
349
+ return false;
160
350
  }
161
351
  function findDependencyRelatedFiles(files, seeds, maxResults) {
162
352
  const lookup = buildDependencyLookup(files);
@@ -291,8 +481,11 @@ async function indexFile(discovered, prior, config) {
291
481
  if (stat.size > config.project.maxFileSizeKb * 1024) {
292
482
  return undefined;
293
483
  }
294
- if (prior && prior.size === stat.size && prior.mtimeMs === stat.mtimeMs) {
295
- return { file: prior, reused: true };
484
+ const language = (0, structuredExtractor_1.detectLanguage)(discovered.relativePath);
485
+ const useTreeSitter = config.index.enableTreeSitter && language != null && (0, treeSitterExtractor_1.supportsTreeSitterLanguage)(language);
486
+ const effectiveExtractorVersion = useTreeSitter ? treeSitterExtractorVersion : regexExtractorVersion;
487
+ if (prior && prior.size === stat.size && prior.mtimeMs === stat.mtimeMs && prior.extractorVersion === effectiveExtractorVersion && prior.chunkerVersion === chunkerVersion && prior.graphVersion === graphVersion) {
488
+ return { file: { ...prior, parserKind: prior.parserKind || (useTreeSitter ? "tree-sitter" : "structured-regex"), parserVersion: prior.parserVersion || effectiveExtractorVersion, symbolCount: prior.symbolCount ?? prior.symbols.length, graphEdgeCount: prior.graphEdgeCount ?? prior.edges.length, indexedAt: prior.indexedAt || new Date().toISOString() }, reused: true };
296
489
  }
297
490
  let content;
298
491
  try {
@@ -304,10 +497,24 @@ async function indexFile(discovered, prior, config) {
304
497
  if (content.includes("\u0000")) {
305
498
  return undefined;
306
499
  }
307
- const structuredNodes = extractStructuredNodesForIndex(discovered.relativePath, content);
308
- const symbols = extractSymbols(structuredNodes);
500
+ const contentHash = hashText(content);
501
+ if (prior && prior.contentHash === contentHash && prior.extractorVersion === effectiveExtractorVersion && prior.chunkerVersion === chunkerVersion && prior.graphVersion === graphVersion) {
502
+ return {
503
+ file: {
504
+ ...prior,
505
+ size: stat.size,
506
+ mtimeMs: stat.mtimeMs
507
+ },
508
+ reused: true
509
+ };
510
+ }
511
+ const structuredNodes = config.index.enableTreeSitter ? extractStructuredNodesForIndex(discovered.relativePath, content) : [];
512
+ const symbols = extractSymbols(structuredNodes, discovered.relativePath);
309
513
  const calls = extractCalls(structuredNodes);
310
514
  const subsystemTerms = (0, subsystem_1.inferSubsystemTerms)(discovered.relativePath, symbols.map((symbol) => symbol.name));
515
+ const imports = (0, dependencyExtractor_1.extractImports)(content).map((item) => item.source).slice(0, 50);
516
+ const chunks = buildIndexedChunks(discovered.relativePath, contentHash, structuredNodes);
517
+ const edges = buildIndexedEdges(discovered.relativePath, imports, symbols, calls);
311
518
  return {
312
519
  reused: false,
313
520
  file: {
@@ -317,14 +524,116 @@ async function indexFile(discovered, prior, config) {
317
524
  language: (0, text_1.languageFromPath)(discovered.relativePath),
318
525
  tokenCount: (0, text_1.fastTokenEstimate)(`${discovered.relativePath}\n${content}`),
319
526
  tokens: tokenizeForIndex(`${discovered.relativePath}\n${content}\n${symbolText(symbols)}`),
320
- imports: (0, dependencyExtractor_1.extractImports)(content).map((item) => item.source).slice(0, 50),
527
+ imports,
321
528
  symbols,
322
529
  calls,
530
+ contentHash,
531
+ extractorVersion: effectiveExtractorVersion,
532
+ chunkerVersion,
533
+ graphVersion,
534
+ chunks,
535
+ edges,
323
536
  subsystem: subsystemTerms[0],
324
- subsystemTerms
537
+ subsystemTerms,
538
+ parserKind: useTreeSitter ? "tree-sitter" : "structured-regex",
539
+ parserVersion: effectiveExtractorVersion,
540
+ symbolCount: symbols.length,
541
+ graphEdgeCount: edges.length,
542
+ indexedAt: new Date().toISOString()
325
543
  }
326
544
  };
327
545
  }
546
+ function buildIndexChannelHits(files, terms, profile) {
547
+ const hits = [];
548
+ const channelBuckets = {
549
+ exact_literal: [],
550
+ path: [],
551
+ lexical_content: [],
552
+ symbol: [],
553
+ semantic_vector: [],
554
+ dependency: [],
555
+ graph: [],
556
+ editor_context: []
557
+ };
558
+ for (const file of files) {
559
+ const scores = scoreIndexedFileChannels(file, terms, profile);
560
+ for (const [channel, score] of Object.entries(scores)) {
561
+ if (score <= 0) {
562
+ continue;
563
+ }
564
+ channelBuckets[channel].push({
565
+ channel,
566
+ relativePath: file.relativePath,
567
+ score,
568
+ rank: 0,
569
+ reason: `${channel} score ${score.toFixed(1)}`
570
+ });
571
+ }
572
+ }
573
+ for (const bucket of Object.values(channelBuckets)) {
574
+ bucket.sort((left, right) => right.score - left.score);
575
+ bucket.forEach((hit, index) => {
576
+ hits.push({ ...hit, rank: index + 1 });
577
+ });
578
+ }
579
+ return hits;
580
+ }
581
+ function scoreIndexedFileChannels(file, terms, profile) {
582
+ const normalizedPath = (0, text_1.normalizeText)(file.relativePath);
583
+ const compactPath = normalizedPath.replace(/[^a-z0-9]+/g, "");
584
+ const normalizedBaseName = (0, text_1.normalizeText)(fileBaseName(file.relativePath));
585
+ const compactBaseName = normalizedBaseName.replace(/[^a-z0-9]+/g, "");
586
+ const tokenSet = new Set(file.tokens);
587
+ const imports = file.imports.map(text_1.normalizeText).join("\n");
588
+ const symbolNames = file.symbols.map((symbol) => (0, text_1.normalizeText)(symbol.name));
589
+ const callNames = (file.calls ?? []).map(text_1.normalizeText);
590
+ const subsystemTerms = file.subsystemTerms ?? [];
591
+ const taskSubsystems = (0, subsystem_1.inferTaskSubsystemTerms)(profile);
592
+ const scores = {};
593
+ const add = (channel, value) => {
594
+ scores[channel] = (scores[channel] ?? 0) + value;
595
+ };
596
+ for (const term of terms) {
597
+ const normalizedTerm = (0, text_1.normalizeText)(term.term);
598
+ const compactTerm = normalizedTerm.replace(/[^a-z0-9]+/g, "");
599
+ if (normalizedPath.includes(normalizedTerm)) {
600
+ add("path", 6 * term.weight);
601
+ }
602
+ if (compactTerm.length >= 5 && compactBaseName.includes(compactTerm)) {
603
+ add("exact_literal", 18 * term.weight);
604
+ }
605
+ if (compactTerm.length >= 5 && compactPath.includes(compactTerm)) {
606
+ add("path", 8 * term.weight);
607
+ }
608
+ if (tokenSet.has(normalizedTerm)) {
609
+ add("lexical_content", 5 * term.weight);
610
+ }
611
+ if (imports.includes(normalizedTerm)) {
612
+ add("dependency", 2 * term.weight);
613
+ }
614
+ if (symbolNames.some((symbol) => symbol === normalizedTerm || symbol.includes(normalizedTerm))) {
615
+ add("symbol", 5 * term.weight);
616
+ }
617
+ if (callNames.some((call) => call === normalizedTerm || call.includes(normalizedTerm))) {
618
+ add("dependency", 3 * term.weight);
619
+ }
620
+ if (subsystemTerms.some((subsystem) => subsystem === normalizedTerm || subsystem.includes(normalizedTerm))) {
621
+ add("path", 4 * term.weight);
622
+ }
623
+ }
624
+ const subsystemMatches = subsystemTerms.filter((subsystem) => taskSubsystems.some((taskSubsystem) => subsystem === taskSubsystem || subsystem.includes(taskSubsystem) || taskSubsystem.includes(subsystem)));
625
+ if (subsystemMatches.length > 0) {
626
+ add("path", 4 + Math.min(10, subsystemMatches.length * 4));
627
+ }
628
+ if (wantsSourceCode(profile) && /(^|\/)src\//.test(file.relativePath.replace(/\\/g, "/"))) {
629
+ add("path", 14);
630
+ }
631
+ const pathModifier = pathQualityModifier(file.relativePath);
632
+ if (pathModifier !== 0) {
633
+ add("path", pathModifier);
634
+ }
635
+ return scores;
636
+ }
328
637
  function scoreIndexedFile(file, terms, profile) {
329
638
  const normalizedPath = (0, text_1.normalizeText)(file.relativePath);
330
639
  const compactPath = normalizedPath.replace(/[^a-z0-9]+/g, "");
@@ -410,6 +719,9 @@ function pathQualityModifier(relativePath) {
410
719
  if (/(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock|poetry\.lock|cargo\.lock)$/.test(normalizedPath)) {
411
720
  modifier -= 40;
412
721
  }
722
+ if (/(^|\/)(helpers?|utils?|console|commands?|scripts?|tasks?)(\/|$)/.test(normalizedPath) && !/(controller|handler|service|repository|model|route|router|worker|job|listener|command|resolver|middleware|policy|request|resource)/.test(normalizedPath)) {
723
+ modifier -= 28;
724
+ }
413
725
  return modifier;
414
726
  }
415
727
  function tokenizeForIndex(text) {
@@ -439,15 +751,83 @@ function extractStructuredNodesForIndex(relativePath, content) {
439
751
  }
440
752
  return (0, structuredExtractor_1.extractStructuredNodes)(content.slice(0, 200000), language);
441
753
  }
442
- function extractSymbols(nodes) {
754
+ function buildIndexedChunks(relativePath, contentHash, nodes) {
755
+ const symbolChunks = nodes
756
+ .filter((node) => node.kind === "declaration" && node.name)
757
+ .slice(0, 120)
758
+ .map((node) => ({
759
+ id: `${relativePath}:${node.range.startLine}-${node.range.endLine}:${node.name}`,
760
+ relativePath,
761
+ range: node.range,
762
+ kind: "symbol",
763
+ symbolFqn: node.fqn ?? node.name,
764
+ textHash: contentHash
765
+ }));
766
+ if (symbolChunks.length > 0) {
767
+ return symbolChunks;
768
+ }
769
+ return [{
770
+ id: `${relativePath}:file`,
771
+ relativePath,
772
+ range: { startLine: 1, endLine: 1 },
773
+ kind: inferChunkKind(relativePath),
774
+ textHash: contentHash
775
+ }];
776
+ }
777
+ function inferChunkKind(relativePath) {
778
+ const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
779
+ if (/\.(md|mdx|txt|rst)$/.test(normalized)) {
780
+ return "doc";
781
+ }
782
+ if (/(^|\/)(config|configs?|settings?)(\/|$)|\.(json|ya?ml|toml|ini|env)$/.test(normalized)) {
783
+ return "config";
784
+ }
785
+ return "file_region";
786
+ }
787
+ function buildIndexedEdges(relativePath, imports, symbols, calls) {
788
+ const edges = [];
789
+ for (const importSource of imports.slice(0, 80)) {
790
+ edges.push({
791
+ id: `${relativePath}:import:${importSource}`,
792
+ type: "import",
793
+ from: relativePath,
794
+ to: importSource,
795
+ fromPath: relativePath,
796
+ toPath: importSource,
797
+ weight: 0.75,
798
+ reason: `imports ${importSource}`
799
+ });
800
+ }
801
+ for (const call of calls.slice(0, 120)) {
802
+ edges.push({
803
+ id: `${relativePath}:call:${call}`,
804
+ type: "callee",
805
+ from: symbols[0]?.fqn ?? relativePath,
806
+ to: call,
807
+ fromPath: relativePath,
808
+ toPath: "",
809
+ weight: 0.65,
810
+ reason: `calls ${call}`
811
+ });
812
+ }
813
+ return edges;
814
+ }
815
+ function hashText(text) {
816
+ return crypto.createHash("sha256").update(text).digest("hex");
817
+ }
818
+ function extractSymbols(nodes, relativePath = "") {
443
819
  return nodes
444
820
  .filter((node) => node.kind === "declaration" && node.name)
445
821
  .slice(0, 80)
446
822
  .map((node) => ({
823
+ id: `${relativePath}:${node.fqn ?? node.name}:${node.range.startLine}`,
447
824
  name: node.name ?? "",
448
- kind: "declaration",
825
+ fqn: node.fqn ?? node.name ?? "",
826
+ kind: node.symbolKind ?? "declaration",
449
827
  line: node.range.startLine,
450
- exported: node.parent === "exported"
828
+ endLine: node.range.endLine,
829
+ exported: node.parent === "exported",
830
+ parentFqn: node.parentFqn
451
831
  }));
452
832
  }
453
833
  function extractCalls(nodes) {
@@ -486,22 +866,38 @@ function yieldToHost() {
486
866
  return new Promise((resolve) => setTimeout(resolve, 0));
487
867
  }
488
868
  async function readIndex(rootPath) {
869
+ const filePath = cachePath(rootPath);
489
870
  try {
490
- const text = await fs.readFile(cachePath(rootPath), "utf8");
871
+ const text = await fs.readFile(filePath, "utf8");
491
872
  const parsed = JSON.parse(text);
492
873
  if (parsed.version !== indexVersion || parsed.rootPath !== path.resolve(rootPath)) {
493
874
  return undefined;
494
875
  }
495
876
  return parsed;
496
877
  }
497
- catch {
878
+ catch (error) {
879
+ await moveCorruptIndexAside(filePath, error);
498
880
  return undefined;
499
881
  }
500
882
  }
501
883
  async function writeIndex(rootPath, index) {
502
884
  const filePath = cachePath(rootPath);
503
885
  await fs.mkdir(path.dirname(filePath), { recursive: true });
504
- await fs.writeFile(filePath, JSON.stringify(index), "utf8");
886
+ const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
887
+ await fs.writeFile(tempPath, JSON.stringify(index), "utf8");
888
+ await fs.rename(tempPath, filePath);
889
+ }
890
+ async function moveCorruptIndexAside(filePath, error) {
891
+ const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
892
+ if (code === "ENOENT") {
893
+ return;
894
+ }
895
+ try {
896
+ await fs.rename(filePath, `${filePath}.corrupt-${Date.now()}`);
897
+ }
898
+ catch {
899
+ // Cache can be rebuilt if corrupt/missing.
900
+ }
505
901
  }
506
902
  function cachePath(rootPath) {
507
903
  return path.join(path.resolve(rootPath), ".flowseeker", "cache", "workspace-index.json");
@@ -515,4 +911,173 @@ function fingerprintConfig(config) {
515
911
  indexVersion
516
912
  });
517
913
  }
914
+ function discoverDeepCandidates(files, selected, profile, terms, budget) {
915
+ if (budget <= 0)
916
+ return [];
917
+ const results = [];
918
+ const taskTokens = extractDeepTaskTokens(profile);
919
+ const taskSubsystems = (0, subsystem_1.inferTaskSubsystemTerms)(profile);
920
+ const seeds = [...selected.values()].slice(0, 3);
921
+ const seedDirs = new Set(seeds.map(s => deepDirname(s.relativePath)));
922
+ for (const file of files) {
923
+ if (results.length >= budget)
924
+ break;
925
+ if (selected.has(file.relativePath))
926
+ continue;
927
+ if (isDeepAvoidPath(file.relativePath))
928
+ continue;
929
+ const signals = computeDeepSignals(file, taskTokens, taskSubsystems, seedDirs, profile);
930
+ if (signals.length < 3)
931
+ continue;
932
+ const basename = deepBasename(file.relativePath);
933
+ const reason = `deep_discovery: ${basename} (${signals.length} signals: ${signals.join(", ")})`;
934
+ results.push({ file, reason, signals });
935
+ }
936
+ return results;
937
+ }
938
+ function extractDeepTaskTokens(profile) {
939
+ const tokens = new Set();
940
+ const allTerms = [...profile.keywords, ...profile.literalTerms, ...profile.concepts, ...profile.actions, ...profile.entities];
941
+ for (const term of allTerms) {
942
+ const n = (0, text_1.normalizeText)(term);
943
+ if (n.length >= 3)
944
+ tokens.add(n);
945
+ // Also split camelCase and snake_case
946
+ for (const part of splitCamelSnake(n)) {
947
+ if (part.length >= 3)
948
+ tokens.add(part);
949
+ }
950
+ }
951
+ return tokens;
952
+ }
953
+ function computeDeepSignals(file, taskTokens, taskSubsystems, seedDirs, profile) {
954
+ const signals = [];
955
+ const normalizedPath = (0, text_1.normalizeText)(file.relativePath);
956
+ const compactPath = normalizedPath.replace(/[^a-z0-9]+/g, "");
957
+ const normalizedBase = (0, text_1.normalizeText)(deepBasename(file.relativePath));
958
+ const compactBase = normalizedBase.replace(/[^a-z0-9]+/g, "");
959
+ const requiredSlots = new Set(profile.blueprint.requiredSlots);
960
+ // Signal 1: Path token overlap (task term of length >= 4 appears in file path)
961
+ let pathOverlapCount = 0;
962
+ for (const token of taskTokens) {
963
+ if (token.length >= 4 && compactPath.includes(token)) {
964
+ pathOverlapCount++;
965
+ }
966
+ }
967
+ if (pathOverlapCount >= 1)
968
+ signals.push("path_token");
969
+ // Signal 2: Basename overlap (task term in basename, camelCase/snake_split)
970
+ let baseOverlapCount = 0;
971
+ for (const token of taskTokens) {
972
+ if (token.length >= 4 && compactBase.includes(token)) {
973
+ baseOverlapCount++;
974
+ }
975
+ }
976
+ if (baseOverlapCount >= 1)
977
+ signals.push("basename_token");
978
+ // Signal 3: Symbol overlap (file symbols match task terms)
979
+ const symbolNames = file.symbols.map(s => (0, text_1.normalizeText)(s.name));
980
+ const symbolOverlap = symbolNames.filter(sym => [...taskTokens].some(t => t.length >= 3 && (sym === t || sym.includes(t))));
981
+ if (symbolOverlap.length >= 1)
982
+ signals.push("symbol_match");
983
+ // Signal 4: Import overlap (file imports match task terms/entities)
984
+ const importNorms = file.imports.map(text_1.normalizeText);
985
+ const importOverlap = importNorms.filter(imp => [...taskTokens].some(t => t.length >= 3 && imp.includes(t)));
986
+ if (importOverlap.length >= 1)
987
+ signals.push("import_match");
988
+ // Signal 5: Subsystem match (file subsystem matches task-inferred subsystem)
989
+ const fileSubsysTerms = file.subsystemTerms ?? [];
990
+ const subsystemOverlap = fileSubsysTerms.filter(st => taskSubsystems.some(ts => st === ts || st.includes(ts) || ts.includes(st)));
991
+ if (subsystemOverlap.length >= 1 || (file.subsystem && taskSubsystems.includes((0, text_1.normalizeText)(file.subsystem)))) {
992
+ signals.push("subsystem_match");
993
+ }
994
+ // Signal 6: Slot/role match (path role classification matches required slot)
995
+ const pathRole = classifyDeepPathRole(file.relativePath);
996
+ if (pathRole && requiredSlots.has(pathRole)) {
997
+ signals.push("slot_match");
998
+ }
999
+ // Signal 7: Same directory as a seed file
1000
+ const fileDir = deepDirname(file.relativePath);
1001
+ if (seedDirs.has(fileDir)) {
1002
+ signals.push("same_directory");
1003
+ }
1004
+ // Signal 8: Token content overlap (file tokens match task terms)
1005
+ const tokenSet = new Set(file.tokens.map(text_1.normalizeText));
1006
+ const tokenOverlap = [...taskTokens].filter(t => t.length >= 3 && tokenSet.has(t));
1007
+ if (tokenOverlap.length >= 2)
1008
+ signals.push("token_content");
1009
+ return signals;
1010
+ }
1011
+ function classifyDeepPathRole(relativePath) {
1012
+ const p = relativePath.replace(/\\/g, "/").toLowerCase();
1013
+ if (/(^|\/)(routes?|router)(\/|$)|urlpatterns|urls\.py$|web\.php$|api\.php$|\+page\.|\+server\.|page\.(tsx|jsx|svelte)$/.test(p))
1014
+ return "entry";
1015
+ if (/(^|\/)(controllers?|handlers?)(\/|$)/.test(p))
1016
+ return "handler";
1017
+ if (/(^|\/)(services?|usecases?|domain)(\/|$)/.test(p))
1018
+ return "core_logic";
1019
+ if (/(^|\/)(models?|entities?)(\/|$)|schema\.(ts|js|prisma)$/.test(p))
1020
+ return "data";
1021
+ if (/(^|\/)(migrations?|schema)(\/|$)|\.sql$|\.prisma$/.test(p))
1022
+ return "data";
1023
+ if (/(^|\/)(config|configs?|settings?)(\/|$)|\.(env|toml|ya?ml|ini|json|properties)$/.test(p))
1024
+ return "infrastructure";
1025
+ if (/(^|\/)(jobs?|workers?)(\/|$)/.test(p))
1026
+ return "side_effect";
1027
+ if (/(^|\/)(listeners?|events?)(\/|$)/.test(p))
1028
+ return "side_effect";
1029
+ if (/(^|\/)(notifications?|mail|email)(\/|$)/.test(p))
1030
+ return "side_effect";
1031
+ if (/(^|\/)(components?|views?|templates?|pages?)(\/|$)|\.(svelte|vue|tsx|jsx|blade\.php|twig)$/.test(p))
1032
+ return "ui";
1033
+ if (/(^|\/)(policies?|permissions?|guards?|middleware|auth)(\/|$)/.test(p))
1034
+ return "permission";
1035
+ if (/(^|\/)(tests?|specs?|__tests__)(\/|$)|\.(test|spec)\./.test(p))
1036
+ return "tests";
1037
+ if (/(^|\/)(validators?|validation)(\/|$)/.test(p))
1038
+ return "validation";
1039
+ return null;
1040
+ }
1041
+ function isDeepAvoidPath(normalizedPath) {
1042
+ const p = normalizedPath.replace(/\\/g, "/").toLowerCase();
1043
+ // Helpers/utils (unless they also match a strong signal)
1044
+ if (/(^|\/)(helpers?|utils?)(\/|$)/.test(p) && !/(controller|handler|service|repository|model|route|router)\b/.test(p))
1045
+ return true;
1046
+ // Lock files
1047
+ if (/(package-lock|yarn\.lock|pnpm-lock|composer\.lock|Gemfile\.lock|poetry\.lock|cargo\.lock)$/.test(p))
1048
+ return true;
1049
+ // Generated/snapshot
1050
+ if (/(^|\/)(generated|__generated__|gen|codegen|snapshots?)(\/|$)|\.snap$|\.generated\./.test(p))
1051
+ return true;
1052
+ // Static assets
1053
+ if (/(^|\/)(public|assets?|static|images?|fonts?)(\/|$)|\.(css|scss|less|svg|png|jpe?g|gif|ico|woff2?)$/.test(p))
1054
+ return true;
1055
+ // Admin/dashboard (unless task targets it)
1056
+ if (/(^|\/)(admin|dashboard)(\/|$)/.test(p))
1057
+ return true;
1058
+ // Templates/views
1059
+ if (/(^|\/)(views?|templates?|resources\/views)(\/|$)|\.(blade\.php|twig|erb|hbs|ejs)$/.test(p))
1060
+ return true;
1061
+ // Console/commands
1062
+ if (/(^|\/)(console|commands?|scheduler|schedulers?|cli)(\/|$)/.test(p))
1063
+ return true;
1064
+ return false;
1065
+ }
1066
+ function deepDirname(filePath) {
1067
+ return filePath.replace(/\\/g, "/").toLowerCase().split("/").slice(0, -1).join("/");
1068
+ }
1069
+ function deepBasename(filePath) {
1070
+ return filePath.replace(/\\/g, "/").split("/").pop()?.replace(/\.[^.]+$/, "") || filePath;
1071
+ }
1072
+ function splitCamelSnake(s) {
1073
+ const parts = [];
1074
+ for (const seg of s.toLowerCase().split(/[_\-]/)) {
1075
+ const camelParts = seg.replace(/([a-z])([A-Z])/g, "$1 $2").split(/[\s.]+/);
1076
+ for (const p of camelParts) {
1077
+ if (p.length >= 3)
1078
+ parts.push(p);
1079
+ }
1080
+ }
1081
+ return [...new Set(parts)];
1082
+ }
518
1083
  //# sourceMappingURL=workspaceIndex.js.map