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
@@ -90,10 +90,47 @@ function scanTextFile(input) {
90
90
  const score = pathScore + kindBonus(input.relativePath, firstChunk?.text ?? "") + pathIntentModifier(input.relativePath, firstChunk?.text ?? "", input.profile);
91
91
  units.push(toEvidenceUnit(input.file, input.relativePath, kind, detectRole(kind, score, input.profile, input.relativePath, firstChunk?.text ?? ""), firstChunk?.range, score, matchedTerms, retrievalPasses, imports, reasonsFor(input.relativePath, kind, matchedTerms, retrievalPasses, imports, pathScore, firstChunk?.text ?? ""), firstChunk?.text, fileMetadata));
92
92
  }
93
+ if (units.length === 0 && input.channelHits?.some((hit) => hit.channel === "semantic_vector")) {
94
+ const semanticHit = input.channelHits.find((hit) => hit.channel === "semantic_vector");
95
+ const minSimilarity = input.config.index.semanticMinSimilarity ?? 0.5;
96
+ const maxRank = input.config.index.semanticEvidenceMaxRank ?? 20;
97
+ if (semanticHit.score >= minSimilarity && semanticHit.rank <= maxRank) {
98
+ const firstChunk = (0, chunker_1.chunkFile)(input.content, input.config.index.chunkSizeLines, input.config.index.chunkOverlapLines)[0];
99
+ const kind = detectKind(input.relativePath, firstChunk?.text ?? "");
100
+ const score = 10 + Math.max(0, semanticHit.score) * 20;
101
+ units.push(toEvidenceUnit(input.file, input.relativePath, kind, detectRole(kind, score, input.profile, input.relativePath, firstChunk?.text ?? ""), firstChunk?.range, score, [], ["semantic_vector"], imports, [`semantic-only file evidence: similarity=${semanticHit.score.toFixed(3)} rank=${semanticHit.rank}`], firstChunk?.text, {
102
+ ...fileMetadata,
103
+ semanticTrace: {
104
+ channel: "semantic_vector",
105
+ similarity: semanticHit.score,
106
+ rank: semanticHit.rank,
107
+ reason: semanticHit.reason
108
+ }
109
+ }));
110
+ }
111
+ }
93
112
  if (structuredNodes.length > 0 && language) {
94
113
  units.push(...buildStructuredEvidence(input, structuredNodes, weightedTerms, imports, pathScore, fileMetadata, language));
95
114
  }
96
- return dedupeUnitsPerFile(units, 6);
115
+ const result = dedupeUnitsPerFile(units, 6);
116
+ return result.map((unit) => {
117
+ const hasSemanticChannel = input.channelHits?.some((hit) => hit.channel === "semantic_vector");
118
+ const updatedPasses = hasSemanticChannel && !unit.retrievalPasses.includes("semantic_vector")
119
+ ? [...unit.retrievalPasses, "semantic_vector"]
120
+ : unit.retrievalPasses;
121
+ return {
122
+ ...unit,
123
+ channelHits: input.channelHits,
124
+ retrievalTrace: input.retrievalTrace,
125
+ graphReasons: input.graphReasons,
126
+ retrievalPasses: updatedPasses,
127
+ reasons: [
128
+ ...unit.reasons,
129
+ ...(input.retrievalTrace ?? []).map((trace) => `retrieval channel: ${trace}`),
130
+ ...(input.graphReasons ?? []).map((reason) => `graph expansion: ${reason}`)
131
+ ]
132
+ };
133
+ });
97
134
  }
98
135
  function getWeightedTerms(profile) {
99
136
  const byTerm = new Map();
@@ -401,6 +438,12 @@ function pathIntentModifier(relativePath, snippet, profile) {
401
438
  if ((isBackendOnlyPath(normalizedPath) || isRuntimePath(normalizedPath)) && !isUiSurfacePath(normalizedPath) && focusedKeywordPathHits > 0) {
402
439
  modifier += 40 + Math.min(24, focusedKeywordPathHits * 8);
403
440
  }
441
+ if ((isFrameworkEntryPath(normalizedPath) || /(^|\/)(controllers?|handlers?)(\/|$)/.test(normalizedPath)) && focusedPathHits > 0) {
442
+ modifier += 38 + Math.min(22, focusedPathHits * 8);
443
+ }
444
+ if (/(^|\/)(jobs?|events?|listeners?|mail|notifications?|queues?|workers?)(\/|$)/.test(normalizedPath) && focusedPathHits > 0) {
445
+ modifier += 28 + Math.min(18, focusedPathHits * 6);
446
+ }
404
447
  if (domainEntityTerms.length > 0 && entityPathHits === 0 && (isRuntimePath(normalizedPath) || isBackendOnlyPath(normalizedPath)) && !isFrameworkEntryPath(normalizedPath)) {
405
448
  modifier -= 18;
406
449
  }
@@ -414,41 +457,78 @@ function pathIntentModifier(relativePath, snippet, profile) {
414
457
  modifier -= taskTargetsCopyOrI18n(profile) ? 0 : 24;
415
458
  }
416
459
  if (isUiSurfacePath(normalizedPath) && !taskTargetsUi(profile) && !isPrimaryFlowPath(normalizedPath, normalizedSnippet, profile)) {
417
- modifier -= 10;
460
+ modifier -= 22;
418
461
  }
419
462
  if (isUiSurfacePath(normalizedPath) && taskTargetsSecurity(profile) && !taskTargetsUi(profile)) {
420
- modifier -= 34;
463
+ modifier -= 46;
421
464
  }
422
465
  if (taskTargetsConfig(profile) && !taskTargetsSecurity(profile) && isPermissionPath(normalizedPath) && focusedPathHits === 0) {
423
466
  modifier -= 20;
424
467
  }
425
468
  if (taskTargetsUi(profile) && !taskTargetsBackend(profile) && isBackendOnlyPath(normalizedPath) && focusedPathHits === 0) {
426
- modifier -= 30;
469
+ modifier -= 46;
427
470
  }
428
471
  if (!taskTargetsData(profile) && isGenericDataPath(normalizedPath) && focusedPathHits === 0) {
429
472
  modifier -= taskTargetsUi(profile) ? 26 : 10;
430
473
  }
431
474
  if (isGenericUtilityPath(normalizedPath) && !taskTargetsUtility(profile)) {
432
- modifier -= taskTargetsUi(profile) ? 24 : focusedPathHits === 0 ? 24 : 14;
475
+ modifier -= taskTargetsUi(profile) ? 36 : focusedPathHits === 0 ? 42 : 28;
433
476
  }
434
477
  if (isMaintenanceCommandPath(normalizedPath) && !taskTargetsMaintenanceCommand(profile)) {
435
- modifier -= 42;
478
+ modifier -= 64;
436
479
  }
437
480
  if (isGuidanceOrReadinessPath(normalizedPath)) {
438
- modifier -= 16;
481
+ modifier -= 28;
439
482
  }
440
483
  if (isStaticContentPath(normalizedPath)) {
441
- modifier -= 24;
484
+ modifier -= 38;
442
485
  }
443
486
  if (isAdminSurfacePath(normalizedPath) && !taskTargetsAdmin(profile) && !isRuntimeFlowPath(normalizedPath, normalizedSnippet)) {
444
- modifier -= 8;
487
+ modifier -= 22;
445
488
  }
446
489
  if (/\b(smoke|fixture|mock|seed)\b/.test(normalizedPath)) {
447
- modifier -= 8;
490
+ modifier -= 18;
448
491
  }
449
492
  if (/(^|\/)\.flowseeker(\/|$)|(^|\/)_gen[^/]*\.(py|sh)$|(^|\/)_bulk_write\.js$/.test(normalizedPath)) {
450
493
  modifier -= 80;
451
494
  }
495
+ // Framework-specific path hints — moderate boosts for common patterns in weak workspaces
496
+ if (focusedPathHits > 0 || keywordPathHits > 0) {
497
+ // Go: handler/service in internal/, cmd/server entry, router, domain services
498
+ if (/(^|\/)internal\/handler\b/.test(normalizedPath))
499
+ modifier += 12;
500
+ if (/(^|\/)internal\/service\b/.test(normalizedPath))
501
+ modifier += 10;
502
+ if (/(^|\/)cmd\/server\b/.test(normalizedPath))
503
+ modifier += 10;
504
+ if (/(^|\/)internal\/router\b/.test(normalizedPath))
505
+ modifier += 8;
506
+ if (/(^|\/)internal\/domain\b/.test(normalizedPath))
507
+ modifier += 8;
508
+ // Spring Boot: controller/service/repository with java/kotlin, annotated entry points
509
+ if (/\.java$|\.kt$/.test(relativePath.toLowerCase()) && /(^|\/)(controller|service|repository)(\/|$)/.test(normalizedPath))
510
+ modifier += 12;
511
+ if (/\.java$/.test(relativePath.toLowerCase()) && /\b@(RestController|Controller|Service|Repository)\b/i.test(snippet))
512
+ modifier += 6;
513
+ // Rails: controller, model, service, mailer, job, policy
514
+ if (/\.rb$/.test(relativePath.toLowerCase()) && /(^|\/)app\/(controllers|models|services|mailers|jobs|policies)(\/|$)/.test(normalizedPath))
515
+ modifier += 12;
516
+ if (/\.rb$/.test(relativePath.toLowerCase()) && /(^|\/)config\/routes\.rb$/.test(normalizedPath))
517
+ modifier += 10;
518
+ // ASP.NET Core: Controllers/, Services/, Program.cs, Startup
519
+ if (/\.cs$/.test(relativePath.toLowerCase()) && /(^|\/)(Controllers|Services|Repositories)(\/|$)/.test(normalizedPath))
520
+ modifier += 12;
521
+ if (/\.cs$/.test(relativePath.toLowerCase()) && /\b(Program|Startup)\.cs$/.test(normalizedPath))
522
+ modifier += 8;
523
+ // Flutter/Dart: screen, provider, bloc, service directories
524
+ if (/\.dart$/.test(relativePath.toLowerCase()) && /(^|\/)(screens?|providers?|blocs?|services?)(\/|$)/.test(normalizedPath))
525
+ modifier += 12;
526
+ // SvelteKit: route files, lib/server, lib/components
527
+ if (/\.svelte$|\.ts$/.test(relativePath.toLowerCase()) && /(^|\/)\+(page|layout|server)\./.test(normalizedPath))
528
+ modifier += 12;
529
+ if (/\.ts$/.test(relativePath.toLowerCase()) && /(^|\/)lib\/server\//.test(normalizedPath))
530
+ modifier += 10;
531
+ }
452
532
  return modifier;
453
533
  }
454
534
  function reasonsFor(relativePath, kind, matchedTerms, retrievalPasses, imports, pathScore, snippet) {
@@ -751,6 +831,7 @@ function buildStructuredEvidence(input, nodes, weightedTerms, imports, pathScore
751
831
  const role = detectRole(nodeKind, nodeScore, input.profile, input.relativePath, snippet);
752
832
  units.push(toEvidenceUnit(input.file, input.relativePath, nodeKind, role, node.range, nodeScore, matchedTerms, retrievalPasses, imports, reasonsFor(input.relativePath, nodeKind, matchedTerms, retrievalPasses, imports, pathScore, snippet), snippet, {
753
833
  ...fileMetadata,
834
+ extractorKind: node.extractorKind ?? "structured-regex",
754
835
  symbolMatches: collectSymbolMatches([node.name ?? "", ...(fileMetadata.symbols ?? [])], weightedTerms)
755
836
  }));
756
837
  }
@@ -788,7 +869,8 @@ function buildEvidenceMetadata(relativePath, nodes, weightedTerms) {
788
869
  symbols,
789
870
  symbolMatches: collectSymbolMatches(symbols, weightedTerms),
790
871
  subsystem: subsystemTerms[0],
791
- subsystemTerms
872
+ subsystemTerms,
873
+ extractorKind: "fallback-chunk"
792
874
  };
793
875
  }
794
876
  function collectSymbolMatches(symbols, weightedTerms) {
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ // Phase 05 Sprint 5G-B — Disabled Optional LLM Reranker Module
3
+ // Execution mode: disabled_prototype_report_only
4
+ // No provider/client/API calls. No network. No production imports.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildLlmRerankRequest = buildLlmRerankRequest;
7
+ exports.validateLlmRerankResponse = validateLlmRerankResponse;
8
+ exports.applyLlmRerankOrFallback = applyLlmRerankOrFallback;
9
+ exports.redactLlmRerankCandidate = redactLlmRerankCandidate;
10
+ exports.createDeterministicFallback = createDeterministicFallback;
11
+ const DEFAULT_SYSTEM_PROMPT = "You are a code search result reranker. Given a task description and a list of candidate files with their retrieval metadata, reorder the candidates so the most relevant files appear first. Output a JSON array of {relativePath, newRank, reason}. Do not add files not in the input list. Do not change relativePath values. Do not include file contents or secrets.";
12
+ const FORBIDDEN_FIELDS = new Set([
13
+ "expectedPath", "expectedPaths", "avoidPath", "avoidPaths",
14
+ "caseId", "caseIds", "suiteName", "suiteLabels",
15
+ "benchmark", "benchmarkLabel", "benchmarkLabels",
16
+ "oracle", "postHoc", "fixture", "isAvoid", "isExpected",
17
+ "contrastContext", "fullFileContents",
18
+ ]);
19
+ // --- 1. buildLlmRerankRequest ---
20
+ function buildLlmRerankRequest(candidates, model) {
21
+ const redacted = candidates.map((c) => redactLlmRerankCandidate(c));
22
+ const userContent = JSON.stringify({
23
+ taskDescription: candidates[0]?.taskDescription || "",
24
+ candidates: redacted,
25
+ });
26
+ return {
27
+ model: model || "default",
28
+ messages: [
29
+ { role: "system", content: DEFAULT_SYSTEM_PROMPT },
30
+ { role: "user", content: userContent },
31
+ ],
32
+ temperature: 0,
33
+ max_tokens: 1024,
34
+ response_format: { type: "json_object" },
35
+ };
36
+ }
37
+ // --- 2. validateLlmRerankResponse ---
38
+ function validateLlmRerankResponse(input, response) {
39
+ const errors = [];
40
+ if (!response || !Array.isArray(response.reranked)) {
41
+ errors.push("R1: reranked must be a non-empty array");
42
+ return { valid: false, errors };
43
+ }
44
+ if (response.reranked.length === 0) {
45
+ errors.push("R7: empty reranked array");
46
+ return { valid: false, errors };
47
+ }
48
+ // Build normalized input path set
49
+ const inputPaths = new Set();
50
+ for (const c of input) {
51
+ inputPaths.add((c.relativePath || "").replace(/\\/g, "/").toLowerCase());
52
+ }
53
+ const seenRanks = new Set();
54
+ const responsePaths = new Set();
55
+ for (const r of response.reranked) {
56
+ const np = (r.relativePath || "").replace(/\\/g, "/").toLowerCase();
57
+ // R3: no new paths
58
+ if (!inputPaths.has(np)) {
59
+ errors.push("R3: unknown file in response: " + r.relativePath);
60
+ }
61
+ // R4: unique ranks in [1, N]
62
+ if (!Number.isInteger(r.newRank) || r.newRank < 1 || r.newRank > input.length) {
63
+ errors.push("R4: invalid newRank " + r.newRank + " for " + r.relativePath);
64
+ }
65
+ if (seenRanks.has(r.newRank)) {
66
+ errors.push("R4: duplicate newRank " + r.newRank);
67
+ }
68
+ seenRanks.add(r.newRank);
69
+ responsePaths.add(np);
70
+ // R5: reason present, non-empty, <= 200 chars
71
+ if (typeof r.reason !== "string" || r.reason.length === 0) {
72
+ errors.push("R5: reason missing for " + r.relativePath);
73
+ }
74
+ if (r.reason && r.reason.length > 200) {
75
+ errors.push("R5: reason too long for " + r.relativePath);
76
+ }
77
+ }
78
+ // R2: all input paths must be in response
79
+ if (responsePaths.size !== inputPaths.size) {
80
+ errors.push("R2/R8: candidate count mismatch (input=" + inputPaths.size + " response=" + responsePaths.size + ")");
81
+ }
82
+ return { valid: errors.length === 0, errors };
83
+ }
84
+ // --- 3. applyLlmRerankOrFallback ---
85
+ function applyLlmRerankOrFallback(input, response, fallbackReason) {
86
+ if (!response) {
87
+ return createDeterministicFallback(input, fallbackReason || "no_response");
88
+ }
89
+ // Try to parse if response is string
90
+ let parsed = response;
91
+ // Validate
92
+ const validation = validateLlmRerankResponse(input, parsed);
93
+ if (!validation.valid) {
94
+ return createDeterministicFallback(input, (fallbackReason || "validation_failed") + ": " + validation.errors.join("; "));
95
+ }
96
+ // Reorder candidates by newRank
97
+ const rankMap = new Map();
98
+ for (const r of parsed.reranked) {
99
+ rankMap.set(r.newRank, r.relativePath);
100
+ }
101
+ const pathToCandidate = new Map();
102
+ for (const c of input) {
103
+ const np = (c.relativePath || "").replace(/\\/g, "/").toLowerCase();
104
+ pathToCandidate.set(np, c);
105
+ }
106
+ const reordered = [];
107
+ for (let rank = 1; rank <= input.length; rank++) {
108
+ const rp = rankMap.get(rank);
109
+ if (rp) {
110
+ const np = rp.replace(/\\/g, "/").toLowerCase();
111
+ const candidate = pathToCandidate.get(np);
112
+ if (candidate)
113
+ reordered.push(candidate);
114
+ }
115
+ }
116
+ // Append any remaining not assigned a rank
117
+ for (const c of input) {
118
+ const np = (c.relativePath || "").replace(/\\/g, "/").toLowerCase();
119
+ if (!reordered.some((r) => (r.relativePath || "").replace(/\\/g, "/").toLowerCase() === np)) {
120
+ reordered.push(c);
121
+ }
122
+ }
123
+ return {
124
+ candidates: reordered,
125
+ fallbackTriggered: false,
126
+ fallbackReason: null,
127
+ validationPassed: true,
128
+ };
129
+ }
130
+ // --- 4. redactLlmRerankCandidate ---
131
+ function redactLlmRerankCandidate(candidate) {
132
+ const redacted = {};
133
+ for (const key of Object.keys(candidate)) {
134
+ if (!FORBIDDEN_FIELDS.has(key)) {
135
+ redacted[key] = candidate[key];
136
+ }
137
+ }
138
+ return redacted;
139
+ }
140
+ // --- 5. createDeterministicFallback ---
141
+ function createDeterministicFallback(input, reason) {
142
+ // Sort by originalRank ascending (preserves deterministic ordering)
143
+ const sorted = [...input].sort((a, b) => a.originalRank - b.originalRank);
144
+ return {
145
+ candidates: sorted,
146
+ fallbackTriggered: true,
147
+ fallbackReason: reason,
148
+ validationPassed: false,
149
+ };
150
+ }
151
+ //# sourceMappingURL=llmReranker.js.map
@@ -119,7 +119,26 @@ async function nodeScanWorkspace(workspacePath, profile, config, options = {}) {
119
119
  indexedTokens: discovery.indexedTokens,
120
120
  candidateFiles: discovery.candidateFiles,
121
121
  indexReusedFiles: discovery.indexReusedFiles,
122
- indexUpdatedFiles: discovery.indexUpdatedFiles
122
+ indexUpdatedFiles: discovery.indexUpdatedFiles,
123
+ indexDeletedFiles: discovery.indexDeletedFiles,
124
+ indexDurationMs: discovery.indexDurationMs,
125
+ indexStatus: discovery.indexStatus,
126
+ partialReason: discovery.partialReason,
127
+ semanticRetrievalStatus: discovery.semanticRetrievalStatus,
128
+ semanticProvider: config.index.semanticProvider || "none",
129
+ semanticModel: config.index.semanticModel || "(empty)",
130
+ semanticErrorType: discovery.semanticErrorType,
131
+ semanticErrorMessage: discovery.semanticErrorMessage,
132
+ semanticEmbeddedFiles: discovery.semanticEmbeddedFiles ?? 0,
133
+ semanticCacheHits: discovery.semanticCacheHits ?? 0,
134
+ semanticCacheMisses: discovery.semanticCacheMisses ?? 0,
135
+ semanticHitCount: discovery.semanticHitCount ?? 0,
136
+ semanticAddedFileCount: discovery.semanticAddedFileCount ?? 0,
137
+ semanticSupportedFileCount: discovery.semanticSupportedFileCount ?? 0,
138
+ semanticFallbackUsed: discovery.semanticRetrievalStatus === "error_fallback",
139
+ deepCandidateDiscoveryCandidates: discovery.deepCandidateDiscoveryCandidates ?? 0,
140
+ deepCandidateDiscoveryAdded: discovery.deepCandidateDiscoveryAdded ?? 0,
141
+ deepCandidateDiscoveryAvoidBlocked: discovery.deepCandidateDiscoveryAvoidBlocked ?? 0
123
142
  }
124
143
  };
125
144
  }
@@ -136,12 +155,22 @@ async function discoverScanFiles(rootPath, profile, config, startedAt, options)
136
155
  if (!config.index.enableWorkspaceIndex) {
137
156
  return (0, fileDiscovery_1.discoverWorkspaceFiles)(rootPath, config, { startedAt, shouldCancel: options.shouldCancel });
138
157
  }
158
+ const idxStart = Date.now();
139
159
  const indexed = await (0, workspaceIndex_1.loadOrBuildWorkspaceIndex)(rootPath, config, {
140
160
  startedAt,
141
161
  shouldCancel: options.shouldCancel,
142
- onProgress: options.onProgress
162
+ onProgress: options.onProgress,
163
+ readOnly: options.readOnly
143
164
  });
144
- const candidates = (0, workspaceIndex_1.selectIndexCandidates)(indexed.index, profile, config);
165
+ const indexDurationMs = Date.now() - idxStart;
166
+ const selection = await (0, workspaceIndex_1.selectIndexCandidates)(indexed.index, profile, config);
167
+ const candidates = selection.files;
168
+ const semanticRetrievalStatus = selection.semanticStatus;
169
+ const semanticErrorType = selection.semanticErrorType;
170
+ const semanticErrorMessage = selection.semanticErrorMessage;
171
+ const deepCands = selection.deepCandidateDiscoveryCandidates ?? 0;
172
+ const deepAdded = selection.deepCandidateDiscoveryAdded ?? 0;
173
+ const deepBlocked = selection.deepCandidateDiscoveryAvoidBlocked ?? 0;
145
174
  const files = candidates.length > 0
146
175
  ? candidates
147
176
  : fallbackIndexCandidates(rootPath, indexed.index.files, config.index.maxIndexedCandidateFiles);
@@ -154,7 +183,23 @@ async function discoverScanFiles(rootPath, profile, config, startedAt, options)
154
183
  indexedTokens: indexed.indexedTokens,
155
184
  candidateFiles: files.length,
156
185
  indexReusedFiles: indexed.reusedFiles,
157
- indexUpdatedFiles: indexed.updatedFiles
186
+ indexUpdatedFiles: indexed.updatedFiles,
187
+ indexDeletedFiles: indexed.deletedFiles,
188
+ indexStatus: indexed.stopReason ? "partial" : "fresh",
189
+ indexDurationMs,
190
+ partialReason: indexed.stopReason,
191
+ semanticRetrievalStatus,
192
+ semanticErrorType,
193
+ semanticErrorMessage,
194
+ semanticEmbeddedFiles: selection.semanticEmbeddedFiles ?? 0,
195
+ semanticCacheHits: selection.semanticCacheHits ?? 0,
196
+ semanticCacheMisses: selection.semanticCacheMisses ?? 0,
197
+ semanticHitCount: selection.semanticHitCount ?? 0,
198
+ semanticAddedFileCount: selection.semanticAddedFileCount ?? 0,
199
+ semanticSupportedFileCount: selection.semanticSupportedFileCount ?? 0,
200
+ deepCandidateDiscoveryCandidates: deepCands,
201
+ deepCandidateDiscoveryAdded: deepAdded,
202
+ deepCandidateDiscoveryAvoidBlocked: deepBlocked
158
203
  };
159
204
  }
160
205
  function fallbackIndexCandidates(rootPath, files, maxFiles) {
@@ -202,15 +247,49 @@ async function scanNodeFile(file, profile, config) {
202
247
  if (content.includes("\u0000")) {
203
248
  return { units: [], scanned: false, size: stat.size, tokenCount: 0 };
204
249
  }
250
+ var units = (0, fileScanner_1.scanTextFile)({
251
+ file: file.absolutePath,
252
+ relativePath: file.relativePath,
253
+ content,
254
+ size: stat.size,
255
+ profile,
256
+ config,
257
+ channelHits: file.channelHits,
258
+ retrievalTrace: file.retrievalTrace,
259
+ graphReasons: file.graphReasons
260
+ });
261
+ // Fallback: if a selected file has graphReasons but produced no units, create
262
+ // one minimal unit so the semantic evidence (discovered by semantic) is visible.
263
+ if (units.length === 0 && file.graphReasons && file.graphReasons.length > 0) {
264
+ units = [{
265
+ id: file.relativePath,
266
+ file: file.absolutePath,
267
+ relativePath: file.relativePath,
268
+ absolutePath: file.absolutePath,
269
+ size: stat.size,
270
+ kind: "file",
271
+ role: "read_context",
272
+ score: 0,
273
+ confidence: 0,
274
+ tier: "low",
275
+ reasons: [],
276
+ matchedTerms: [],
277
+ retrievalPasses: file.retrievalTrace ?? [],
278
+ imports: [],
279
+ channelHits: file.channelHits,
280
+ graphReasons: file.graphReasons,
281
+ lines: "0-0",
282
+ context: "",
283
+ contrastContext: false,
284
+ symbols: [],
285
+ symbolMatches: [],
286
+ subsystem: undefined,
287
+ subsystemTerms: [],
288
+ extractorKind: undefined
289
+ }];
290
+ }
205
291
  return {
206
- units: (0, fileScanner_1.scanTextFile)({
207
- file: file.absolutePath,
208
- relativePath: file.relativePath,
209
- content,
210
- size: stat.size,
211
- profile,
212
- config
213
- }),
292
+ units,
214
293
  scanned: true,
215
294
  size: stat.size,
216
295
  tokenCount: (0, text_1.fastTokenEstimate)(`${file.relativePath}\n${content}`)