flowseeker 0.1.7 → 0.1.9

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 (50) hide show
  1. package/.env.example +7 -0
  2. package/CHANGELOG.md +143 -108
  3. package/README.md +288 -221
  4. package/dist/chat/nativeChatParticipant.js +1 -1
  5. package/dist/cli/flowCommand.js +175 -0
  6. package/dist/cli/main.js +1810 -0
  7. package/dist/cli/mcpServer.js +7 -1
  8. package/dist/cli/runEvaluation.js +281 -2
  9. package/dist/config/defaultConfig.js +11 -1
  10. package/dist/config/env.js +118 -0
  11. package/dist/config/loadConfig.js +18 -1
  12. package/dist/config/loadConfigFromPath.js +3 -1
  13. package/dist/eval/accuracyV2.js +483 -0
  14. package/dist/eval/goldenTask.js +192 -0
  15. package/dist/extension.js +23 -0
  16. package/dist/framework/laravel.js +177 -0
  17. package/dist/gateway/embeddingProviders.js +852 -0
  18. package/dist/index/cacheStore.js +43 -0
  19. package/dist/index/configRouteDiscoveryProbe.js +288 -0
  20. package/dist/index/embeddingIndex.js +193 -0
  21. package/dist/index/graphIndex.js +460 -0
  22. package/dist/index/indexWatcher.js +86 -0
  23. package/dist/index/semanticChunkIndex.js +388 -0
  24. package/dist/index/structuredExtractor.js +303 -12
  25. package/dist/index/treeSitterExtractor.js +264 -0
  26. package/dist/index/vectorStore.js +41 -0
  27. package/dist/index/workspaceIndex.js +901 -26
  28. package/dist/mcp/mcpTools.js +1678 -2
  29. package/dist/pipeline/contextBlueprint.js +15 -2
  30. package/dist/pipeline/contextPack.js +3 -3
  31. package/dist/pipeline/deterministicReranker.js +358 -0
  32. package/dist/pipeline/evaluationMetrics.js +14 -2
  33. package/dist/pipeline/fileGroups.js +7 -1
  34. package/dist/pipeline/fileScanner.js +209 -12
  35. package/dist/pipeline/fusionTrace.js +149 -0
  36. package/dist/pipeline/llmReranker.js +151 -0
  37. package/dist/pipeline/nodeScan.js +102 -12
  38. package/dist/pipeline/ranker.js +875 -16
  39. package/dist/pipeline/retrievalFusion.js +41 -0
  40. package/dist/pipeline/roleRefinement.js +62 -0
  41. package/dist/pipeline/runHeadless.js +60 -5
  42. package/dist/pipeline/runPipeline.js +2 -2
  43. package/dist/pipeline/solvePacket.js +656 -43
  44. package/dist/pipeline/subsystem.js +21 -0
  45. package/dist/pipeline/taskUnderstanding.js +2 -2
  46. package/dist/ui/chatViewProvider.js +1 -1
  47. package/docs/demo-screenshot-checklist.md +86 -0
  48. package/docs/marketplace-copy.md +92 -0
  49. package/docs/mcp-onboarding.md +191 -0
  50. package/package.json +633 -561
@@ -38,37 +38,258 @@ exports.renderSolvePacketMarkdown = renderSolvePacketMarkdown;
38
38
  const fs = __importStar(require("fs/promises"));
39
39
  const fileGroups_1 = require("./fileGroups");
40
40
  const contextBlueprint_1 = require("./contextBlueprint");
41
+ const workspaceIndex_1 = require("../index/workspaceIndex");
42
+ const fusionTrace_1 = require("./fusionTrace");
41
43
  const maxPrimaryEditFiles = 7;
42
44
  const maxReadOnlyFiles = 7;
43
45
  const maxVerificationFiles = 6;
44
46
  const maxNoiseRiskFiles = 6;
45
47
  const maxSnippetChars = 850;
46
48
  const minSnippetBudgetChars = 9000;
47
- async function buildSolvePacket(result, config) {
49
+ async function buildSolvePacket(result, config, workspaceRoot) {
48
50
  const groups = (0, fileGroups_1.aggregateEvidenceFiles)(result.units);
49
51
  const coverageGroups = selectCoverageGroups(groups, result);
50
52
  const primaryEditCandidates = coverageGroups.filter((group) => group.role === "edit_candidate" && group.slot !== "tests" && !group.contrastContext);
51
- const verificationTargets = groups.filter((group) => group.slot === "tests" || group.role === "test");
53
+ const verificationTargets = groups.filter((group) => group.slot === "tests" || group.role === "test" || isTestAdjacent(group));
54
+ // Fallback: if no test files found, include config/build files that suggest verification capability
55
+ if (verificationTargets.length === 0) {
56
+ const fallbackVerify = groups.filter((group) => isVerificationHint(group));
57
+ verificationTargets.push(...fallbackVerify.slice(0, maxVerificationFiles));
58
+ }
52
59
  const noiseRisk = detectNoiseRisk(groups, result);
60
+ // Fallback: include low-confidence groups that aren't assigned elsewhere
61
+ if (noiseRisk.length === 0) {
62
+ const assigned = new Set([...primaryEditCandidates.map(g => g.relativePath), ...verificationTargets.map(g => g.relativePath)]);
63
+ const lowConfidence = groups.filter(g => !assigned.has(g.relativePath) && g.tier === "low" && g.score < 50);
64
+ noiseRisk.push(...lowConfidence.slice(0, maxNoiseRiskFiles));
65
+ }
53
66
  const noisePaths = new Set(noiseRisk.map((group) => group.relativePath));
54
67
  const primaryPaths = new Set(primaryEditCandidates.map((group) => group.relativePath));
55
68
  const verificationPaths = new Set(verificationTargets.map((group) => group.relativePath));
56
- const readOnlyContext = coverageGroups.filter((group) => !primaryPaths.has(group.relativePath) && !verificationPaths.has(group.relativePath) && !noisePaths.has(group.relativePath));
69
+ let readOnlyContext = coverageGroups.filter((group) => !primaryPaths.has(group.relativePath) && !verificationPaths.has(group.relativePath) && !noisePaths.has(group.relativePath));
70
+ // Fallback: include any remaining groups with context role or connected files
71
+ if (readOnlyContext.length === 0) {
72
+ const assigned = new Set([...primaryPaths, ...verificationPaths, ...noisePaths]);
73
+ readOnlyContext = groups.filter((group) => !assigned.has(group.relativePath)).slice(0, maxReadOnlyFiles);
74
+ }
57
75
  const snippetBudget = {
58
76
  remainingChars: Math.max(minSnippetBudgetChars, config.pipeline.maxContextTokens * 4)
59
77
  };
78
+ // Build packet files
79
+ const primaryFiles = await toPacketFiles(primaryEditCandidates.slice(0, Math.min(maxPrimaryEditFiles, config.pipeline.maxCandidates)), snippetBudget, "primary", result);
80
+ const readOnlyFiles = await toPacketFiles(readOnlyContext.slice(0, Math.min(maxReadOnlyFiles, config.pipeline.maxCandidates)), snippetBudget, "context", result);
81
+ const verificationFiles = await toPacketFiles(verificationTargets.slice(0, Math.min(maxVerificationFiles, config.pipeline.maxCandidates)), snippetBudget, "verification", result);
82
+ const noiseFiles = await toPacketFiles(noiseRisk.slice(0, Math.min(maxNoiseRiskFiles, config.pipeline.maxCandidates)), snippetBudget, "noise", result);
83
+ // Chunk scoring (15E, report-only)
84
+ const allFilePaths = [
85
+ ...primaryFiles.map((f) => f.relativePath),
86
+ ...readOnlyFiles.map((f) => f.relativePath),
87
+ ];
88
+ const topChunks = (0, workspaceIndex_1.scoreChunksForTask)(allFilePaths, result.profile, workspaceRoot, { maxChunksPerFile: 3, maxTotalChunks: 30 });
89
+ const chunksByFile = new Map();
90
+ for (const tc of topChunks) {
91
+ const existing = chunksByFile.get(tc.filePath) || [];
92
+ existing.push(tc);
93
+ chunksByFile.set(tc.filePath, existing);
94
+ }
95
+ // Attach top chunks to packet files
96
+ const attachChunks = (files) => {
97
+ for (const f of files) {
98
+ const matches = chunksByFile.get(f.relativePath);
99
+ if (matches && matches.length > 0)
100
+ f.topChunks = matches.slice(0, 3);
101
+ }
102
+ };
103
+ attachChunks(primaryFiles);
104
+ attachChunks(readOnlyFiles);
60
105
  return {
61
106
  userTask: result.task,
107
+ taskShape: result.profile.taskShape,
62
108
  problemHypothesis: buildHypothesis(result, groups),
63
109
  candidateFlow: buildCandidateFlow(coverageGroups, result),
64
- primaryEditCandidates: await toPacketFiles(primaryEditCandidates.slice(0, Math.min(maxPrimaryEditFiles, config.pipeline.maxCandidates)), snippetBudget, "primary"),
65
- readOnlyContext: await toPacketFiles(readOnlyContext.slice(0, Math.min(maxReadOnlyFiles, config.pipeline.maxCandidates)), snippetBudget, "context"),
66
- verificationTargets: await toPacketFiles(verificationTargets.slice(0, Math.min(maxVerificationFiles, config.pipeline.maxCandidates)), snippetBudget, "verification"),
67
- noiseRisk: await toPacketFiles(noiseRisk.slice(0, Math.min(maxNoiseRiskFiles, config.pipeline.maxCandidates)), snippetBudget, "noise"),
68
- missingLinks: detectMissingLinks(groups, result),
69
- agentInstructions: defaultAgentInstructions(result)
110
+ primaryEditCandidates: primaryFiles,
111
+ readOnlyContext: readOnlyFiles,
112
+ verificationTargets: verificationFiles,
113
+ noiseRisk: noiseFiles,
114
+ missingLinks: detectMissingLinks(coverageGroups, result),
115
+ verificationCommands: suggestVerificationCommands(result),
116
+ retrievalTrace: buildRetrievalTrace(groups),
117
+ tokenBudgetWarning: snippetBudget.remainingChars <= 0 ? "Solve Packet snippet budget exhausted; inspect listed files directly before editing." : undefined,
118
+ agentInstructions: defaultAgentInstructions(result),
119
+ semanticDiagnostic: buildSemanticDiagnostic(result.stats, config),
120
+ firstThreeFiles: buildFirstThree(primaryEditCandidates, readOnlyContext, verificationTargets, noiseRisk, groups, result),
121
+ whyTheseFiles: buildWhyTheseFiles(result, groups),
122
+ doNotEditYet: buildDoNotEditYet(readOnlyContext, noiseRisk, coverageGroups, result),
123
+ // Chunk-assisted evidence
124
+ chunkAssistedMode: "file_level_current",
125
+ topChunksSummary: topChunks.slice(0, 15),
126
+ // RRF fusion trace (report-only)
127
+ rrfComparison: (0, fusionTrace_1.computeRrfTrace)(result) ?? undefined,
128
+ // Role refinement trace (16D-R1)
129
+ roleRefinementMode: result.stats.roleRefinementMode ?? "off",
130
+ roleDemotions: result.stats.roleDemotions ?? 0,
131
+ roleDemotionLog: result.stats.roleDemotionLog ?? [],
70
132
  };
71
133
  }
134
+ function buildFirstThree(primary, readOnly, verification, noise, allGroups, result) {
135
+ const isReadOnly = result.profile.taskShape === "investigation" || result.profile.taskShape === "unknown";
136
+ const editLabel = isReadOnly ? "strongest evidence" : "primary edit candidate";
137
+ const first = [];
138
+ const used = new Set();
139
+ // 1. Top candidate
140
+ const topEdit = primary.find(g => g.role === "edit_candidate" && !g.contrastContext);
141
+ if (topEdit) {
142
+ first.push(`${topEdit.relativePath} — ${editLabel}, score ${topEdit.score.toFixed(0)}, tier ${topEdit.tier}`);
143
+ used.add(topEdit.relativePath);
144
+ }
145
+ // 2. Entry or handler context
146
+ const ctxCandidate = readOnly.find(g => !used.has(g.relativePath) && (g.slot === "entry" || g.slot === "handler") && !g.contrastContext)
147
+ ?? allGroups.find(g => !used.has(g.relativePath) && (g.slot === "entry" || g.slot === "handler") && !g.contrastContext);
148
+ if (ctxCandidate) {
149
+ const why = ctxCandidate.slot === "entry" ? "route/entry point — confirm change path" : "handler/controller — confirm change surface";
150
+ first.push(`${ctxCandidate.relativePath} — ${why}, score ${ctxCandidate.score.toFixed(0)}`);
151
+ used.add(ctxCandidate.relativePath);
152
+ }
153
+ // 3. Test, model, or service for verification
154
+ const verifyCandidate = verification.find(g => !used.has(g.relativePath) && !g.contrastContext)
155
+ ?? allGroups.find(g => !used.has(g.relativePath) && (g.slot === "tests" || g.slot === "data" || g.slot === "domain") && !g.contrastContext);
156
+ if (verifyCandidate) {
157
+ const why = verifyCandidate.slot === "tests" ? "tests/verification — check test coverage"
158
+ : verifyCandidate.slot === "data" ? "data/model — confirm schema impact"
159
+ : "service/domain — understand business logic";
160
+ first.push(`${verifyCandidate.relativePath} — ${why}, score ${verifyCandidate.score.toFixed(0)}`);
161
+ }
162
+ // Fallback: fill remaining from primary or read-only
163
+ const fallback = [...primary, ...readOnly, ...allGroups].filter(g => !used.has(g.relativePath) && !g.contrastContext && !noise.includes(g));
164
+ while (first.length < 3 && fallback.length > 0) {
165
+ const g = fallback.shift();
166
+ if (used.has(g.relativePath))
167
+ continue;
168
+ first.push(`${g.relativePath} — supports context, score ${g.score.toFixed(0)}`);
169
+ used.add(g.relativePath);
170
+ }
171
+ return first;
172
+ }
173
+ function buildWhyTheseFiles(result, groups) {
174
+ const parts = [];
175
+ const isEdit = !["investigation", "unknown"].includes(result.profile.taskShape);
176
+ // Task shape and intent
177
+ parts.push(`Task classified as "${result.profile.taskShape}"${isEdit ? " (edit intended)" : " (read-only investigation)"}, confidence ${(result.profile.intentConfidence * 100).toFixed(0)}%.`);
178
+ // Exact matches
179
+ const exactMatches = groups.filter(g => g.units.some(u => u.matchedTerms.length > 0));
180
+ if (exactMatches.length > 0) {
181
+ const names = exactMatches.slice(0, 3).map(g => g.relativePath.split("/").pop()).join(", ");
182
+ parts.push(`${exactMatches.length} file(s) matched task keywords in filename or content (e.g., ${names}).`);
183
+ }
184
+ else {
185
+ parts.push("No lexical keyword matches — files selected by path structure and concept similarity.");
186
+ }
187
+ // Flow coverage from retrieval passes
188
+ const passes = new Set();
189
+ for (const g of groups.slice(0, 15)) {
190
+ for (const unit of g.units) {
191
+ for (const pass of unit.retrievalPasses) {
192
+ if (pass !== "Seed concepts")
193
+ passes.add(pass);
194
+ }
195
+ }
196
+ }
197
+ if (passes.size > 0) {
198
+ parts.push(`Candidate flow covers ${passes.size} context slot(s): ${[...passes].slice(0, 6).join(", ")}.`);
199
+ }
200
+ // Semantic
201
+ const s = result.stats.semanticRetrievalStatus;
202
+ if (s === "active") {
203
+ const added = result.stats.semanticAddedFileCount ?? 0;
204
+ const supported = result.stats.semanticSupportedFileCount ?? 0;
205
+ parts.push(`Semantic retrieval active (${result.stats.semanticProvider || "local"}): ${supported} existing files supported, ${added} new candidate(s) discovered by vector similarity.`);
206
+ }
207
+ else if (s === "error_fallback") {
208
+ parts.push("Semantic retrieval unavailable — deterministic retrieval only.");
209
+ }
210
+ // Graph/symbol evidence
211
+ const graphCount = groups.filter(g => g.units.some(u => (u.graphReasons?.length ?? 0) > 0)).length;
212
+ const symbolCount = groups.filter(g => g.symbols.length > 0).length;
213
+ if (graphCount > 0 || symbolCount > 0) {
214
+ const ev = [];
215
+ if (graphCount > 0)
216
+ ev.push(`${graphCount} file(s) with graph/dependency evidence`);
217
+ if (symbolCount > 0)
218
+ ev.push(`${symbolCount} file(s) with symbol-name matches`);
219
+ parts.push(`${ev.join(", ")}.`);
220
+ }
221
+ // Noise separation
222
+ const noiseCount = groups.filter(g => g.contrastContext).length;
223
+ if (noiseCount > 0) {
224
+ parts.push(`${noiseCount} file(s) flagged as noise/contrast risk and separated into read-only or noise sections.`);
225
+ }
226
+ // Uncertainty
227
+ if (result.stats.stoppedEarly || result.stats.capped) {
228
+ const cap = result.stats.capped ? `candidate cap at ${result.stats.maxCandidates}` : "";
229
+ const early = result.stats.stoppedEarly ? `scan stopped early (${result.stats.stopReason ?? "unknown"})` : "";
230
+ parts.push(`Uncertainty: ${[cap, early].filter(Boolean).join("; ")}. Lower-ranked files may exist outside scanned window.`);
231
+ }
232
+ return parts.join(" ");
233
+ }
234
+ function buildDoNotEditYet(readOnly, noise, allGroups, result) {
235
+ const items = [];
236
+ const added = new Set();
237
+ for (const g of noise.slice(0, 3)) {
238
+ if (added.has(g.relativePath))
239
+ continue;
240
+ added.add(g.relativePath);
241
+ const reason = g.contrastContext ? "contrast context (matches avoided/previous-behavior terms)"
242
+ : g.tier === "low" && g.score < 50 ? "low confidence, likely unrelated"
243
+ : "noise risk — verify relevance before editing";
244
+ items.push(`${g.relativePath} — ${reason} (score ${g.score.toFixed(0)}, tier ${g.tier})`);
245
+ }
246
+ for (const g of readOnly.slice(0, 3)) {
247
+ if (added.has(g.relativePath))
248
+ continue;
249
+ added.add(g.relativePath);
250
+ const slot = g.slot ? (0, contextBlueprint_1.contextSlotLabel)(g.slot) : "context";
251
+ const reason = g.slot === "entry" ? "route/entry files — read for change path, edit elsewhere"
252
+ : g.slot === "ui" ? "UI/template files — read for display context, edit in handler/service"
253
+ : g.slot === "config" ? "config files — read for settings context, edit in domain files"
254
+ : g.role === "read_context" ? `read-only ${slot} context — useful for understanding, not the change surface`
255
+ : `read-only ${slot} — confirms context, risky as edit surface without more evidence`;
256
+ items.push(`${g.relativePath} — ${reason} (score ${g.score.toFixed(0)}, tier ${g.tier})`);
257
+ }
258
+ if (items.length === 0) {
259
+ items.push("All selected files appear relevant and safe for consideration. No files flagged as noise or read-only risk.");
260
+ }
261
+ return items;
262
+ }
263
+ function buildSemanticDiagnostic(stats, config) {
264
+ const s = stats.semanticRetrievalStatus;
265
+ if (!s || s === "disabled")
266
+ return "Semantic: disabled — deterministic/no-embedding retrieval only";
267
+ const provider = stats.semanticProvider || "unknown";
268
+ if (s === "active") {
269
+ const embedded = stats.semanticEmbeddedFiles ?? stats.semanticCacheHits ?? "?";
270
+ const hits = stats.semanticHitCount ?? "?";
271
+ const added = stats.semanticAddedFileCount ?? "?";
272
+ const supported = stats.semanticSupportedFileCount ?? "?";
273
+ const cacheHits = stats.semanticCacheHits ?? "?";
274
+ const cacheMisses = stats.semanticCacheMisses ?? "?";
275
+ if (provider === "local") {
276
+ const dims = config.index.semanticDimensions;
277
+ const dimStr = dims && dims > 0 ? `, ${dims}d` : "";
278
+ const cacheStr = (cacheHits !== "?" || cacheMisses !== "?") ? `, cache hits=${cacheHits}, cache misses=${cacheMisses}` : "";
279
+ return `Semantic: active (local bundled model${dimStr}, embedded=${embedded}${cacheStr}, vector hits=${hits}, semantic candidates=${added}, files with semantic support=${supported})`;
280
+ }
281
+ const model = stats.semanticModel || "unknown";
282
+ return `Semantic: active (${provider}, model=${model}, embedded=${embedded}, vector hits=${hits}, semantic candidates=${added}, files with semantic support=${supported})`;
283
+ }
284
+ if (s === "error_fallback") {
285
+ const err = stats.semanticErrorType || "unknown";
286
+ const msg = stats.semanticErrorMessage || "";
287
+ return `Semantic: error_fallback (${provider}, ${err}${msg ? ": " + msg.substring(0, 100) : ""}) — deterministic retrieval used as fallback`;
288
+ }
289
+ if (s === "skipped_no_key")
290
+ return `Semantic: skipped (no API key configured) — deterministic retrieval used`;
291
+ return `Semantic: ${s}`;
292
+ }
72
293
  function renderSolvePacketMarkdown(packet) {
73
294
  const lines = [
74
295
  "# FlowSeeker Solve Packet",
@@ -85,24 +306,114 @@ function renderSolvePacketMarkdown(packet) {
85
306
  "## User Task",
86
307
  packet.userTask,
87
308
  "",
309
+ "## Retrieval Mode",
310
+ packet.semanticDiagnostic || "Semantic: disabled",
311
+ "",
88
312
  "## Problem Hypothesis",
89
313
  packet.problemHypothesis
90
314
  ];
315
+ // First 3 files to open
316
+ if (packet.firstThreeFiles.length > 0) {
317
+ lines.push("", "## First 3 Files To Open");
318
+ for (const file of packet.firstThreeFiles) {
319
+ lines.push(`- ${file}`);
320
+ }
321
+ lines.push("");
322
+ }
323
+ // Why these files
324
+ if (packet.whyTheseFiles) {
325
+ lines.push("", "## Why These Files", packet.whyTheseFiles, "");
326
+ }
91
327
  if (packet.candidateFlow.length > 0) {
92
- lines.push("", "## Candidate Flow Coverage");
93
- for (const step of packet.candidateFlow) {
94
- lines.push(`### ${sectionLabel(step.section)}`, ...step.files.map((file) => `- ${file}`), "");
328
+ lines.push("", "## Candidate Flow", "Ordered path target: route -> controller -> service/job/export/import -> model/data -> view/template -> test.");
329
+ for (let i = 0; i < packet.candidateFlow.length; i++) {
330
+ const step = packet.candidateFlow[i];
331
+ lines.push(`### ${i + 1}. ${step.label}${step.purpose ? ` — ${step.purpose}` : ""}`);
332
+ for (const file of step.files) {
333
+ lines.push(`- ${file}`);
334
+ }
335
+ if (step.retrievalTrace && step.retrievalTrace.length > 0) {
336
+ lines.push(` Reasons: ${step.retrievalTrace.join("; ")}`);
337
+ }
338
+ if (step.hint) {
339
+ lines.push(` ${step.hint}`);
340
+ }
341
+ lines.push("");
95
342
  }
96
343
  }
97
- lines.push("## Primary Edit Candidates", ...renderPacketFiles(packet.primaryEditCandidates));
98
- lines.push("## Read-Only Context", ...renderPacketFiles(packet.readOnlyContext));
99
- lines.push("## Tests / Verification", ...renderPacketFiles(packet.verificationTargets));
100
- if (packet.noiseRisk.length > 0) {
101
- lines.push("## Noise Risk", ...renderPacketFiles(packet.noiseRisk));
344
+ const isReadOnly = packet.taskShape === "investigation" || packet.taskShape === "unknown";
345
+ const primaryTitle = isReadOnly ? "## Primary Context Candidates" : "## Primary Edit Candidates";
346
+ lines.push(primaryTitle, ...renderPacketFiles(packet.primaryEditCandidates, isReadOnly));
347
+ lines.push("## Read-Only Context", ...renderPacketFiles(packet.readOnlyContext, isReadOnly, "No additional read-only context was selected beyond primary edit candidates and verification targets."));
348
+ lines.push("## Tests / Verification", ...renderPacketFiles(packet.verificationTargets, isReadOnly, "No concrete test or verification config file was selected. Use Verification Command Hints below for suggested test commands."));
349
+ lines.push("## Noise Risk", ...renderPacketFiles(packet.noiseRisk, isReadOnly, "No obvious noise-risk files were selected. All ranked candidates passed relevance filters."));
350
+ if (packet.doNotEditYet.length > 0) {
351
+ lines.push("## Do Not Edit Yet", ...packet.doNotEditYet.map((item) => `- ${item}`), "");
102
352
  }
103
353
  if (packet.missingLinks.length > 0) {
104
354
  lines.push("## Missing Links", ...packet.missingLinks.map((link) => `- ${link}`), "");
105
355
  }
356
+ if (packet.retrievalTrace.length > 0) {
357
+ lines.push("## Retrieval Trace", ...packet.retrievalTrace.map((trace) => `- ${trace}`), "");
358
+ }
359
+ if (packet.verificationCommands.length > 0) {
360
+ lines.push("## Verification Command Hints");
361
+ for (const vc of packet.verificationCommands) {
362
+ lines.push(`- \`${vc.command}\``);
363
+ lines.push(` Evidence: ${vc.evidence} (${vc.confidence}) — ${vc.reason}`);
364
+ }
365
+ lines.push("");
366
+ }
367
+ // Role Decision Trace (16D-R1)
368
+ var rMode = packet.roleRefinementMode || "off";
369
+ var demotions = packet.roleDemotions || 0;
370
+ var demotionLog = packet.roleDemotionLog || [];
371
+ lines.push("## Role Decision Trace", "", `Role refinement: ${rMode}` + (rMode !== "conservative" ? " (opt-in conservative available via config pipeline.roleRefinementMode)" : ""), "");
372
+ // Edit candidates
373
+ for (var ed = 0; ed < Math.min(packet.primaryEditCandidates.length, 5); ed++) {
374
+ var f = packet.primaryEditCandidates[ed];
375
+ var topChunks = f.topChunks || [];
376
+ var chunkInfo = topChunks.length > 0 ? " chunks=" + topChunks.slice(0, 2).map(function (c) { return c.kind + "@L" + c.startLine; }).join(",") : "";
377
+ lines.push(` edit: ${f.relativePath} score=${f.score.toFixed(0)} role=${f.role} tier=${f.tier}${chunkInfo}`);
378
+ }
379
+ // Demoted files (conservative mode)
380
+ if (rMode === "conservative" && demotions > 0) {
381
+ lines.push("", " Demoted (not edit targets in conservative mode):");
382
+ for (var dm = 0; dm < Math.min(demotionLog.length, 5); dm++) {
383
+ var d = demotionLog[dm];
384
+ lines.push(` ${d.beforeRole}→${d.afterRole}: ${d.filePath} rule=${d.ruleId} confidence=${d.confidence} reason="${d.reason}"`);
385
+ }
386
+ if (demotionLog.length > 5)
387
+ lines.push(` ... and ${demotionLog.length - 5} more demotions`);
388
+ }
389
+ // Read-only context
390
+ if (packet.readOnlyContext.length > 0) {
391
+ lines.push("", " Read-only context:");
392
+ for (var rc = 0; rc < Math.min(packet.readOnlyContext.length, 3); rc++) {
393
+ var rcf = packet.readOnlyContext[rc];
394
+ lines.push(` read: ${rcf.relativePath} role=${rcf.role}`);
395
+ }
396
+ }
397
+ lines.push("");
398
+ if (rMode !== "conservative") {
399
+ lines.push(" Caution: role refinement is off/opt-in. AI agents should validate edit candidates before editing.", "");
400
+ }
401
+ lines.push(` Default role decisions are from the current ranker.${packet.primaryEditCandidates.length > 0 ? " Verify relevance of each edit candidate." : ""}`, "");
402
+ if (packet.tokenBudgetWarning) {
403
+ lines.push("## Token Budget Warning", packet.tokenBudgetWarning, "");
404
+ }
405
+ // RRF Fusion Trace (15G-R1, report-only)
406
+ if (packet.rrfComparison) {
407
+ const r = packet.rrfComparison;
408
+ lines.push("## RRF Fusion Trace", "", `Mode: report-only | Default ranking changed: ${r.defaultRankingChanged} | k: ${r.k}`, `Overlap: @3=${r.overlapAt3}/${Math.min(3, r.currentTopFiles.length, r.rrfTopFiles.length)} @5=${r.overlapAt5}/${Math.min(5, r.currentTopFiles.length, r.rrfTopFiles.length)} @10=${r.overlapAt10}/${Math.min(10, r.currentTopFiles.length, r.rrfTopFiles.length)}`, `Active channels: ${r.channelTraces.filter((c) => c.active).map((c) => c.channel).join(", ") || "none"}`, `Inactive channels: ${r.channelTraces.filter((c) => !c.active).map((c) => c.channel).join(", ") || "none"}`, "", `Current top files (${r.currentTopFiles.length}): ${r.currentTopFiles.slice(0, 8).join(", ")}`, `RRF top files (${r.rrfTopFiles.length}): ${r.rrfTopFiles.slice(0, 8).join(", ")}`, ...(r.promotedByRrf.length > 0 ? [`Promoted by RRF: ${r.promotedByRrf.join(", ")}`] : ["Promoted by RRF: none"]), ...(r.demotedByRrf.length > 0 ? [`Demoted by RRF: ${r.demotedByRrf.join(", ")}`] : ["Demoted by RRF: none"]), "", "Top RRF channel contributions:");
409
+ for (const entry of r.topRrfEntries.slice(0, 5)) {
410
+ const contribs = Object.entries(entry.channelContributions)
411
+ .map(([ch, val]) => `${ch}=${val.toFixed(4)}`)
412
+ .join(", ");
413
+ lines.push(` ${entry.relativePath} rrfScore=${entry.rrfScore.toFixed(4)} [${contribs}]`);
414
+ }
415
+ lines.push("");
416
+ }
106
417
  lines.push("## Agent Instructions", ...packet.agentInstructions.map((instruction, index) => `${index + 1}. ${instruction}`), "");
107
418
  return lines.join("\n");
108
419
  }
@@ -148,40 +459,126 @@ function bestGroupForSlot(groups, slot, selected) {
148
459
  }
149
460
  function buildCandidateFlow(groups, result) {
150
461
  const flow = [];
151
- const orderedSlots = (0, contextBlueprint_1.sortSlots)([...result.profile.blueprint.requiredSlots, ...result.profile.blueprint.optionalSlots, "unknown"]);
462
+ const orderedSlots = ["entry", "handler", "domain", "side_effect", "validation", "data", "ui", "tests"];
463
+ const isEdit = isEditIntendedTask(result);
464
+ const required = new Set(result.profile.blueprint.requiredSlots);
465
+ const optional = new Set(result.profile.blueprint.optionalSlots);
152
466
  for (const slot of orderedSlots) {
153
- const files = groups.filter((group) => group.slot === slot).map((group) => formatFlowFile(group, result));
154
- if (files.length > 0) {
467
+ if (!required.has(slot) && !optional.has(slot))
468
+ continue;
469
+ const slotGroups = groups.filter((group) => group.slot === slot);
470
+ if (slotGroups.length === 0) {
155
471
  flow.push({
156
472
  section: slot,
157
473
  label: sectionLabel(slot),
158
- files: files.slice(0, 4)
474
+ files: [`Missing ${(0, contextBlueprint_1.contextSlotLabel)(slot)} evidence`],
475
+ purpose: "missing link to verify before relying on packet",
476
+ hint: `Search hint: ${slotSearchHints(slot, result)}`
159
477
  });
478
+ continue;
160
479
  }
480
+ const files = slotGroups.map((group) => formatFlowFile(group, result));
481
+ const topGroup = slotGroups[0];
482
+ const purpose = flowPurpose(slot, isEdit, topGroup);
483
+ const hint = isEdit && (slot === "entry" || slot === "handler" || slot === "domain") ? "Inspect before editing — confirm this is the correct change surface." : undefined;
484
+ flow.push({
485
+ section: slot,
486
+ label: flowStepLabel(slot),
487
+ files: files.slice(0, 3),
488
+ retrievalTrace: slotGroups.flatMap((group) => group.reasons).slice(0, 2),
489
+ purpose,
490
+ hint
491
+ });
161
492
  }
162
493
  return flow;
163
494
  }
164
- function formatFlowFile(group, result) {
165
- const required = (0, contextBlueprint_1.isRequiredSlot)(result.profile, group.slot) ? "required-slot" : "optional";
166
- return `${group.relativePath} [${group.role}; ${required}; score ${group.score.toFixed(1)}]`;
495
+ function flowStepLabel(slot) {
496
+ const labels = {
497
+ entry: "Route / entry",
498
+ handler: "Controller / handler",
499
+ domain: "Service / domain",
500
+ side_effect: "Job / export / import",
501
+ validation: "Request / validation",
502
+ data: "Model / data",
503
+ ui: "View / template",
504
+ tests: "Test / verification"
505
+ };
506
+ return labels[slot] ?? sectionLabel(slot);
507
+ }
508
+ function flowPurpose(slot, isEdit, group) {
509
+ const base = slotPurposeLabel(slot);
510
+ if (isEdit) {
511
+ if (slot === "entry" || slot === "handler")
512
+ return `${base}: start here to trace the change path`;
513
+ if (slot === "domain" || slot === "data")
514
+ return `${base}: likely change surface`;
515
+ if (slot === "tests")
516
+ return `${base}: update after changes`;
517
+ return `${base}: review for impact`;
518
+ }
519
+ return `${base}: read for context`;
167
520
  }
168
- function renderPacketFiles(files) {
521
+ function slotPurposeLabel(slot) {
522
+ const labels = {
523
+ entry: "Request entry point",
524
+ handler: "Request handler / controller",
525
+ domain: "Business logic / domain",
526
+ data: "Data model / persistence",
527
+ validation: "Input validation",
528
+ permission: "Authorization / permissions",
529
+ config: "Configuration",
530
+ side_effect: "Side effects / events",
531
+ ui: "UI / presentation",
532
+ tests: "Tests / verification",
533
+ docs: "Documentation",
534
+ unknown: "Matched context"
535
+ };
536
+ return labels[slot] ?? "Context";
537
+ }
538
+ function formatFlowFile(group, _result) {
539
+ return `${group.relativePath} [${group.tier}; score ${group.score.toFixed(0)}]`;
540
+ }
541
+ function renderPacketFiles(files, isReadOnly, placeholderText) {
169
542
  if (files.length === 0) {
170
- return ["- None identified.", ""];
171
- }
172
- return files.flatMap((file) => [
173
- `- ${file.relativePath}`,
174
- ` Slot: ${file.slot ? (0, contextBlueprint_1.contextSlotLabel)(file.slot) : "Unknown"}; Role: ${file.role}; Kind: ${file.kind}; Score: ${file.score.toFixed(1)}; Tier: ${file.tier}`,
175
- ...(file.contextMode ? [` Context: ${contextModeLabel(file.contextMode)}${file.contextRange ? `, lines ${file.contextRange.startLine}-${file.contextRange.endLine}` : ""}`] : []),
176
- ...(file.reasons.length > 0 ? [` Reasons: ${file.reasons.join("; ")}`] : []),
177
- ...(file.snippet ? ["", "```", file.snippet, "```"] : []),
178
- ""
179
- ]);
543
+ return placeholderText ? [`- ${placeholderText}`, ""] : ["- None identified.", ""];
544
+ }
545
+ return files.flatMap((file) => {
546
+ const roleLabel = isReadOnly && file.role === "edit_candidate" ? "context_candidate" : file.role;
547
+ const connectedLabel = isReadOnly ? "Related context to inspect" : "Read before editing";
548
+ return [
549
+ `- ${file.relativePath}`,
550
+ ` Slot: ${file.slot ? (0, contextBlueprint_1.contextSlotLabel)(file.slot) : "Unknown"}; Role: ${roleLabel}; Kind: ${file.kind}; Score: ${file.score.toFixed(1)}; Tier: ${file.tier}`,
551
+ ...(file.contextMode ? [` Context: ${contextModeLabel(file.contextMode)}${file.contextRange ? `, lines ${file.contextRange.startLine}-${file.contextRange.endLine}` : ""}`] : []),
552
+ ...(file.expectedChangeSurface ? [` Expected change surface: ${file.expectedChangeSurface}`] : []),
553
+ ...(file.riskLevel ? [` Risk: ${file.riskLevel}`] : []),
554
+ ...(file.connectedContext && file.connectedContext.length > 0 ? [` ${connectedLabel}: ${file.connectedContext.join(", ")}`] : []),
555
+ ...(file.retrievalTrace && file.retrievalTrace.length > 0 ? [` Selected by: ${file.retrievalTrace.join("; ")}`] : []),
556
+ ...(file.reasons.length > 0 ? [` Reasons: ${file.reasons.join("; ")}`] : []),
557
+ ...(file.topChunks && file.topChunks.length > 0 ? renderTopChunks(file.topChunks) : []),
558
+ ...(file.snippet ? ["", "```", file.snippet, "```"] : []),
559
+ ""
560
+ ];
561
+ });
562
+ }
563
+ function renderTopChunks(chunks) {
564
+ if (!chunks || chunks.length === 0)
565
+ return [];
566
+ const lines = [" Top Chunks:"];
567
+ for (const tc of chunks) {
568
+ const preview = (tc.contentPreview || "").slice(0, 120).replace(/\n/g, "\\n");
569
+ const symbol = tc.symbolName ? ` symbol=${tc.symbolName}` : "";
570
+ lines.push(` L${tc.startLine}-${tc.endLine} ${tc.kind}/${tc.slot} score=${tc.score}${symbol}`);
571
+ lines.push(` → ${tc.reasons.join(", ")}`);
572
+ if (preview)
573
+ lines.push(` \`${preview}\``);
574
+ }
575
+ return lines;
180
576
  }
181
577
  function sectionLabel(section) {
182
578
  return (0, contextBlueprint_1.contextSlotLabel)(section);
183
579
  }
184
- async function toPacketFiles(groups, budget, purpose) {
580
+ async function toPacketFiles(groups, budget, purpose, result) {
581
+ const isReadOnly = result.profile.taskShape === "investigation" || result.profile.taskShape === "unknown";
185
582
  const files = [];
186
583
  for (const [index, group] of groups.entries()) {
187
584
  const snippetContext = await buildSnippetContext(group, budget, maxCharsForPurpose(purpose, index));
@@ -192,6 +589,10 @@ async function toPacketFiles(groups, budget, purpose) {
192
589
  slot: group.slot,
193
590
  contextMode: snippetContext.mode,
194
591
  contextRange: snippetContext.range,
592
+ expectedChangeSurface: isReadOnly ? undefined : expectedChangeSurface(group, purpose),
593
+ riskLevel: riskLevel(group, purpose),
594
+ connectedContext: connectedContext(group, groups),
595
+ retrievalTrace: retrievalTraceForGroup(group),
195
596
  score: group.score,
196
597
  confidence: group.confidence,
197
598
  tier: group.tier,
@@ -201,6 +602,115 @@ async function toPacketFiles(groups, budget, purpose) {
201
602
  }
202
603
  return files;
203
604
  }
605
+ function expectedChangeSurface(group, purpose) {
606
+ if (purpose !== "primary") {
607
+ return undefined;
608
+ }
609
+ const slot = group.slot ? (0, contextBlueprint_1.contextSlotLabel)(group.slot).toLowerCase() : "matched code";
610
+ return `Smallest change likely belongs in this ${slot} surface after validating connected context.`;
611
+ }
612
+ function riskLevel(group, purpose) {
613
+ if (purpose === "noise") {
614
+ return "high";
615
+ }
616
+ if (purpose === "context" || group.contrastContext) {
617
+ return "medium";
618
+ }
619
+ return group.tier === "high" ? "low" : "medium";
620
+ }
621
+ function connectedContext(group, groups) {
622
+ const groupTrace = new Set(retrievalTraceForGroup(group));
623
+ return groups
624
+ .filter((candidate) => candidate.relativePath !== group.relativePath)
625
+ .filter((candidate) => candidate.subsystem && group.subsystem ? candidate.subsystem === group.subsystem : candidate.slot && candidate.slot !== group.slot)
626
+ .filter((candidate) => {
627
+ const trace = retrievalTraceForGroup(candidate);
628
+ return trace.some((item) => groupTrace.has(item)) || candidate.subsystem === group.subsystem || isAdjacentSlot(group.slot, candidate.slot);
629
+ })
630
+ .slice(0, 3)
631
+ .map((candidate) => candidate.relativePath);
632
+ }
633
+ function isTestAdjacent(group) {
634
+ const normalized = group.relativePath.replace(/\\/g, "/").toLowerCase();
635
+ return /\b(test|spec|__tests__|e2e|cypress|playwright)\b/.test(normalized) ||
636
+ /\b(test|spec)\.(ts|tsx|js|jsx|php|py|rb|go|java)$/.test(normalized);
637
+ }
638
+ function isVerificationHint(group) {
639
+ const normalized = group.relativePath.replace(/\\/g, "/").toLowerCase();
640
+ return /\b(composer\.json|package\.json|phpunit\.xml|phpunit\.xml\.dist|jest\.config|vitest\.config|Makefile|docker-compose|\.env\.example)\b/.test(normalized) ||
641
+ /\.(yml|yaml|toml|xml\.dist)$/.test(normalized);
642
+ }
643
+ function isAdjacentSlot(left, right) {
644
+ const adjacent = new Set([
645
+ "entry:handler",
646
+ "handler:domain",
647
+ "domain:data",
648
+ "domain:config",
649
+ "domain:side_effect",
650
+ "handler:permission",
651
+ "ui:handler",
652
+ "tests:domain",
653
+ "tests:handler"
654
+ ]);
655
+ return adjacent.has(`${left}:${right}`) || adjacent.has(`${right}:${left}`);
656
+ }
657
+ function retrievalTraceForGroup(group) {
658
+ return group.units
659
+ .flatMap((unit) => [
660
+ ...unit.retrievalPasses.map((pass) => `pass:${pass}`),
661
+ ...(unit.retrievalTrace ?? []),
662
+ ...(unit.channelHits ?? []).map((hit) => `${hit.channel}:${hit.reason}`),
663
+ ...(unit.graphReasons ?? []).map((reason) => `graph:${reason}`),
664
+ ...(unit.frameworkEdgeReason ? [`graph:${unit.frameworkEdgeReason}`] : [])
665
+ ])
666
+ .filter((trace, index, traces) => traces.indexOf(trace) === index)
667
+ .slice(0, 4);
668
+ }
669
+ function buildRetrievalTrace(groups) {
670
+ return groups
671
+ .slice(0, 10)
672
+ .flatMap((group) => retrievalTraceForGroup(group).map((trace) => `${group.relativePath}: ${trace}`))
673
+ .slice(0, 20);
674
+ }
675
+ function suggestVerificationCommands(result) {
676
+ const paths = result.units.map((unit) => unit.relativePath.replace(/\\/g, "/"));
677
+ const commands = [];
678
+ const added = new Set();
679
+ function add(cmd, evidence, confidence, reason) {
680
+ if (added.has(cmd))
681
+ return;
682
+ added.add(cmd);
683
+ commands.push({ command: cmd, evidence, confidence, reason, runnable: true });
684
+ }
685
+ const hasComposer = paths.some((p) => p === "composer.json");
686
+ const hasLaravel = paths.some((p) => p === "artisan" || /(^|\/)(routes|app\/Http|app\/Models|app\/Console|resources\/views)\//.test(p));
687
+ const hasPHP = hasLaravel || hasComposer || paths.some((p) => p.endsWith(".php"));
688
+ const hasPackageJson = paths.some((p) => p === "package.json" || p.endsWith("/package.json"));
689
+ const hasJS = hasPackageJson || paths.some((p) => /(^|\/)src\//.test(p) || /\.(ts|tsx|js|jsx)$/.test(p));
690
+ const hasPython = paths.some((p) => /(^|\/)tests?\/.*\.py$/.test(p) || p.endsWith(".py"));
691
+ const hasGo = paths.some((p) => /\.go$/.test(p));
692
+ if (hasLaravel || hasPHP) {
693
+ const trigger = paths.find((p) => p.startsWith("routes/")) ?? paths.find((p) => p.endsWith(".php")) ?? "Laravel/PHP files";
694
+ add("php artisan route:list", trigger, "detected", "Laravel/PHP route context detected; verify route/controller mapping before edits");
695
+ add("php artisan test", trigger, "detected", "Laravel/PHP app file detected; run framework tests after edits");
696
+ add("composer test", hasComposer ? "composer.json" : trigger, hasComposer ? "detected" : "inferred", "Run Composer test script when composer.json defines one");
697
+ }
698
+ if (hasJS) {
699
+ const trigger = paths.find((p) => p.endsWith("package.json")) ?? "JS/TS source";
700
+ add("npm run compile --silent", trigger, hasPackageJson ? "detected" : "inferred", "JavaScript/TypeScript project detected; run type-check before verifying edits");
701
+ add("npm run test:smoke", trigger, hasPackageJson ? "detected" : "inferred", "JavaScript/TypeScript project detected; run available package test/smoke script");
702
+ }
703
+ if (hasPython) {
704
+ const trigger = paths.find((p) => p.endsWith(".py")) ?? "tests/";
705
+ add("pytest", trigger, "detected", "Python test file detected; run pytest to verify changes");
706
+ }
707
+ if (hasGo) {
708
+ const trigger = paths.find((p) => /\.go$/.test(p)) ?? "*.go";
709
+ add("go test ./...", trigger, "detected", "Go source file detected; run Go test suite");
710
+ }
711
+ add("Run the smallest focused check for the edited file(s)", "generic fallback", "inferred", "Always run the narrowest available verification if framework commands are absent or too broad");
712
+ return commands.slice(0, 6);
713
+ }
204
714
  function compactSnippet(snippet) {
205
715
  if (!snippet) {
206
716
  return undefined;
@@ -335,22 +845,125 @@ function isNoiseGroup(group, neededSlots) {
335
845
  }
336
846
  return false;
337
847
  }
848
+ function missingSlotMessage(slot, groups, result, coveredSlots) {
849
+ const slotLabel = (0, contextBlueprint_1.contextSlotLabel)(slot);
850
+ const hints = slotSearchHints(slot, result);
851
+ const currentlyCovered = Array.from(coveredSlots).filter(s => s !== "unknown").map(contextBlueprint_1.contextSlotLabel).join(", ") || "no clear slots";
852
+ const uncertainty = result.stats.stoppedEarly
853
+ ? ` Scan stopped early (${result.stats.stopReason ?? "unknown reason"}) — lower-ranked context may exist; do not convert scan-cap uncertainty into a missing-slot claim.`
854
+ : ` ${capUncertainty(result)}`;
855
+ return `Missing ${slotLabel} — search hint: ${hints}. Currently covered slots: ${currentlyCovered}.${uncertainty} Pause editing until this slot is found or confirmed not needed.`;
856
+ }
857
+ function slotSearchHints(slot, result) {
858
+ const concepts = result.profile.concepts.slice(0, 4).join(", ") || result.profile.keywords.slice(0, 4).join(", ") || result.task;
859
+ const keywords = result.profile.keywords.slice(0, 6).join(" ");
860
+ const hintMap = {
861
+ entry: `search paths matching routes, controllers, or entry points near ${concepts}; try "${keywords}" as grep terms`,
862
+ handler: `search for handler/controller/service files containing ${concepts}; look in src/, app/, or server/ directories`,
863
+ domain: `search for service/domain/logic files with ${concepts}; check for interfaces, use-cases, or business logic modules`,
864
+ data: `search for model/entity/schema files related to ${concepts}; check database migrations, ORM models, or API schemas`,
865
+ validation: `search for validation/request/schema files; try "validate", "sanitize", "schema", or "${concepts}"`,
866
+ permission: `search for auth/guard/policy files; try "can", "authorize", "policy", "role", or "${concepts}"`,
867
+ config: `search for config/env/settings files; check .env, config/, or app config for ${concepts}`,
868
+ side_effect: `search for event/job/mail/queue files; try "dispatch", "emit", "fire", "send", or "${concepts}"`,
869
+ ui: `search for view/template/component files; check resources/, views/, or components/ for ${concepts}`,
870
+ tests: `search for test/spec files; try "test", "spec", "it(", "describe(" near ${concepts}`,
871
+ docs: `search for README/CONTRIBUTING/docs files; check docs/ or project root for ${concepts}`,
872
+ unknown: `search broadly for ${concepts} across all project files`
873
+ };
874
+ return hintMap[slot] || hintMap.unknown;
875
+ }
876
+ function capUncertainty(result) {
877
+ const stats = result.stats;
878
+ const parts = [];
879
+ if (stats.capped)
880
+ parts.push(`candidate cap hit (ranked=${stats.rankedEvidenceCount}, max=${stats.maxCandidates})`);
881
+ if (stats.stoppedEarly)
882
+ parts.push(`scan stopped early (${stats.stopReason ?? "unknown reason"})`);
883
+ if (stats.candidateFiles && stats.maxCandidates && stats.candidateFiles >= stats.maxCandidates)
884
+ parts.push(`candidate file cap may hide lower-ranked matches (candidateFiles=${stats.candidateFiles})`);
885
+ return parts.length > 0 ? parts.join("; ") : "no scan/candidate cap signal reported";
886
+ }
338
887
  function detectMissingLinks(groups, result) {
339
888
  const missing = [];
340
- const foundSlots = new Set(groups.map((group) => group.slot));
889
+ const isEdit = isEditIntendedTask(result);
890
+ const coveredSlots = computeCoveredSlots(groups, result);
341
891
  for (const slot of result.profile.blueprint.requiredSlots) {
342
- if (!foundSlots.has(slot)) {
343
- missing.push(`No ${(0, contextBlueprint_1.contextSlotLabel)(slot)} file was identified for this ${result.profile.taskShape} task.`);
892
+ if (!coveredSlots.has(slot)) {
893
+ missing.push(missingSlotMessage(slot, groups, result, coveredSlots));
344
894
  }
345
895
  }
346
- if (isEditIntendedTask(result) && !groups.some((group) => group.role === "edit_candidate")) {
347
- missing.push("No edit_candidate files were ranked with sufficient confidence. Broaden the search or relax minConfidence.");
896
+ if (isEdit && !groups.some((group) => group.role === "edit_candidate")) {
897
+ const topPaths = groups.slice(0, 3).map(g => g.relativePath).join(", ");
898
+ missing.push(`No edit_candidate files were ranked with sufficient confidence. Try broadening search with additional keywords or lowering minConfidence. Nearest candidates: ${topPaths || "none"}.`);
899
+ }
900
+ const uncertainty = capUncertainty(result);
901
+ const stoppedEarly = result.stats.stoppedEarly;
902
+ if (missing.length === 0) {
903
+ const coveredLabels = Array.from(coveredSlots).filter(s => s !== "unknown").map(contextBlueprint_1.contextSlotLabel).join(", ");
904
+ const allRequired = result.profile.blueprint.requiredSlots.every(s => coveredSlots.has(s));
905
+ const topFile = groups[0]?.relativePath ?? "top-ranked file";
906
+ const uncertaintyNote = stoppedEarly
907
+ ? ` Scan stopped early — lower-ranked context may exist outside the scan window.`
908
+ : "";
909
+ const capNote = stoppedEarly
910
+ ? ` Uncertainty: ${uncertainty}. Before editing, verify: (1) read ${topFile} to confirm it is the correct change surface; (2) check Connected Context on primary candidates for additional files; (3) confirm no test coverage gap exists in Verification Targets.${uncertaintyNote}`
911
+ : ` Uncertainty: ${uncertainty}. Before editing, verify: (1) read ${topFile} to confirm it is the correct change surface; (2) check Connected Context on primary candidates for additional files; (3) confirm no test coverage gap exists in Verification Targets.`;
912
+ if (isEdit) {
913
+ missing.push(`All required context slots covered: ${coveredLabels || "none identified"}. Search hint if context feels thin: inspect adjacent files from Candidate Flow and Connected Context.${capNote}`);
914
+ }
915
+ else {
916
+ missing.push(`All required context slots covered: ${coveredLabels || "none identified"}. Search hint if context feels thin: inspect adjacent files from Candidate Flow and Connected Context. Uncertainty: ${uncertainty}. Verify: read ${topFile} to confirm relevance; cross-check Candidate Flow ordering against your understanding of the codebase.`);
917
+ }
348
918
  }
349
- if (result.stats.stoppedEarly) {
350
- missing.push(`Scan stopped early: ${result.stats.stopReason ?? "budget limit reached"}. Treat coverage as partial.`);
919
+ else {
920
+ if (stoppedEarly) {
921
+ missing.push(`Scan stopped early (${result.stats.stopReason ?? "unknown reason"}). Lower-ranked context may exist outside the scan window. Do not convert this scan-cap uncertainty into a missing-slot claim — only the slots listed above are verified missing from the scanned evidence.`);
922
+ }
923
+ }
924
+ // Append framework-specific hints when slots are missing and framework hints exist
925
+ if (missing.length > 0 && result.profile.blueprint.frameworkHints?.length) {
926
+ for (const hint of result.profile.blueprint.frameworkHints) {
927
+ missing.push(hint);
928
+ }
351
929
  }
352
930
  return missing;
353
931
  }
932
+ function computeCoveredSlots(groups, result) {
933
+ const covered = new Set();
934
+ // Source 1: group.slot values
935
+ for (const group of groups) {
936
+ if (group.slot && group.slot !== "unknown") {
937
+ covered.add(group.slot);
938
+ }
939
+ }
940
+ // Source 2: retrieval passes — parse "pass:Entry / Routes coverage" patterns
941
+ const slotLabelToKey = new Map();
942
+ for (const slot of (0, contextBlueprint_1.allContextSlots)(result.profile.blueprint)) {
943
+ slotLabelToKey.set((0, contextBlueprint_1.contextSlotLabel)(slot).toLowerCase(), slot);
944
+ }
945
+ // Also add abbreviated forms
946
+ slotLabelToKey.set("seed concepts", "unknown");
947
+ for (const group of groups) {
948
+ for (const unit of group.units) {
949
+ for (const pass of unit.retrievalPasses) {
950
+ const normalized = pass.toLowerCase().replace(/\s+coverage$/, "").trim();
951
+ if (slotLabelToKey.has(normalized)) {
952
+ covered.add(slotLabelToKey.get(normalized));
953
+ }
954
+ }
955
+ }
956
+ }
957
+ // Source 3: evidence units that have a specific slot assigned
958
+ for (const group of groups) {
959
+ for (const unit of group.units) {
960
+ if (unit.slot && unit.slot !== "unknown") {
961
+ covered.add(unit.slot);
962
+ }
963
+ }
964
+ }
965
+ return covered;
966
+ }
354
967
  function isEditIntendedTask(result) {
355
968
  return !["investigation", "unknown"].includes(result.profile.taskShape);
356
969
  }