flowseeker 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/CHANGELOG.md +131 -108
- package/README.md +288 -221
- package/dist/cli/flowCommand.js +175 -0
- package/dist/cli/main.js +1794 -0
- package/dist/cli/mcpServer.js +7 -1
- package/dist/cli/runEvaluation.js +178 -2
- package/dist/config/defaultConfig.js +11 -1
- package/dist/config/env.js +118 -0
- package/dist/config/loadConfig.js +18 -1
- package/dist/config/loadConfigFromPath.js +3 -1
- package/dist/extension.js +23 -0
- package/dist/gateway/embeddingProviders.js +852 -0
- package/dist/index/cacheStore.js +43 -0
- package/dist/index/configRouteDiscoveryProbe.js +288 -0
- package/dist/index/embeddingIndex.js +193 -0
- package/dist/index/graphIndex.js +460 -0
- package/dist/index/indexWatcher.js +86 -0
- package/dist/index/structuredExtractor.js +303 -12
- package/dist/index/treeSitterExtractor.js +264 -0
- package/dist/index/vectorStore.js +41 -0
- package/dist/index/workspaceIndex.js +591 -26
- package/dist/mcp/mcpTools.js +51 -0
- package/dist/pipeline/contextPack.js +3 -3
- package/dist/pipeline/deterministicReranker.js +358 -0
- package/dist/pipeline/evaluationMetrics.js +14 -2
- package/dist/pipeline/fileGroups.js +7 -1
- package/dist/pipeline/fileScanner.js +93 -11
- package/dist/pipeline/llmReranker.js +151 -0
- package/dist/pipeline/nodeScan.js +91 -12
- package/dist/pipeline/ranker.js +875 -16
- package/dist/pipeline/retrievalFusion.js +41 -0
- package/dist/pipeline/runHeadless.js +35 -0
- package/dist/pipeline/solvePacket.js +549 -42
- package/dist/pipeline/subsystem.js +21 -0
- package/docs/demo-screenshot-checklist.md +86 -0
- package/docs/marketplace-copy.md +92 -0
- package/docs/mcp-onboarding.md +191 -0
- package/package.json +633 -561
|
@@ -48,27 +48,210 @@ async function buildSolvePacket(result, config) {
|
|
|
48
48
|
const groups = (0, fileGroups_1.aggregateEvidenceFiles)(result.units);
|
|
49
49
|
const coverageGroups = selectCoverageGroups(groups, result);
|
|
50
50
|
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");
|
|
51
|
+
const verificationTargets = groups.filter((group) => group.slot === "tests" || group.role === "test" || isTestAdjacent(group));
|
|
52
|
+
// Fallback: if no test files found, include config/build files that suggest verification capability
|
|
53
|
+
if (verificationTargets.length === 0) {
|
|
54
|
+
const fallbackVerify = groups.filter((group) => isVerificationHint(group));
|
|
55
|
+
verificationTargets.push(...fallbackVerify.slice(0, maxVerificationFiles));
|
|
56
|
+
}
|
|
52
57
|
const noiseRisk = detectNoiseRisk(groups, result);
|
|
58
|
+
// Fallback: include low-confidence groups that aren't assigned elsewhere
|
|
59
|
+
if (noiseRisk.length === 0) {
|
|
60
|
+
const assigned = new Set([...primaryEditCandidates.map(g => g.relativePath), ...verificationTargets.map(g => g.relativePath)]);
|
|
61
|
+
const lowConfidence = groups.filter(g => !assigned.has(g.relativePath) && g.tier === "low" && g.score < 50);
|
|
62
|
+
noiseRisk.push(...lowConfidence.slice(0, maxNoiseRiskFiles));
|
|
63
|
+
}
|
|
53
64
|
const noisePaths = new Set(noiseRisk.map((group) => group.relativePath));
|
|
54
65
|
const primaryPaths = new Set(primaryEditCandidates.map((group) => group.relativePath));
|
|
55
66
|
const verificationPaths = new Set(verificationTargets.map((group) => group.relativePath));
|
|
56
|
-
|
|
67
|
+
let readOnlyContext = coverageGroups.filter((group) => !primaryPaths.has(group.relativePath) && !verificationPaths.has(group.relativePath) && !noisePaths.has(group.relativePath));
|
|
68
|
+
// Fallback: include any remaining groups with context role or connected files
|
|
69
|
+
if (readOnlyContext.length === 0) {
|
|
70
|
+
const assigned = new Set([...primaryPaths, ...verificationPaths, ...noisePaths]);
|
|
71
|
+
readOnlyContext = groups.filter((group) => !assigned.has(group.relativePath)).slice(0, maxReadOnlyFiles);
|
|
72
|
+
}
|
|
57
73
|
const snippetBudget = {
|
|
58
74
|
remainingChars: Math.max(minSnippetBudgetChars, config.pipeline.maxContextTokens * 4)
|
|
59
75
|
};
|
|
60
76
|
return {
|
|
61
77
|
userTask: result.task,
|
|
78
|
+
taskShape: result.profile.taskShape,
|
|
62
79
|
problemHypothesis: buildHypothesis(result, groups),
|
|
63
80
|
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(
|
|
69
|
-
|
|
81
|
+
primaryEditCandidates: await toPacketFiles(primaryEditCandidates.slice(0, Math.min(maxPrimaryEditFiles, config.pipeline.maxCandidates)), snippetBudget, "primary", result),
|
|
82
|
+
readOnlyContext: await toPacketFiles(readOnlyContext.slice(0, Math.min(maxReadOnlyFiles, config.pipeline.maxCandidates)), snippetBudget, "context", result),
|
|
83
|
+
verificationTargets: await toPacketFiles(verificationTargets.slice(0, Math.min(maxVerificationFiles, config.pipeline.maxCandidates)), snippetBudget, "verification", result),
|
|
84
|
+
noiseRisk: await toPacketFiles(noiseRisk.slice(0, Math.min(maxNoiseRiskFiles, config.pipeline.maxCandidates)), snippetBudget, "noise", result),
|
|
85
|
+
missingLinks: detectMissingLinks(coverageGroups, result),
|
|
86
|
+
verificationCommands: suggestVerificationCommands(result),
|
|
87
|
+
retrievalTrace: buildRetrievalTrace(groups),
|
|
88
|
+
tokenBudgetWarning: snippetBudget.remainingChars <= 0 ? "Solve Packet snippet budget exhausted; inspect listed files directly before editing." : undefined,
|
|
89
|
+
agentInstructions: defaultAgentInstructions(result),
|
|
90
|
+
semanticDiagnostic: buildSemanticDiagnostic(result.stats, config),
|
|
91
|
+
firstThreeFiles: buildFirstThree(primaryEditCandidates, readOnlyContext, verificationTargets, noiseRisk, groups, result),
|
|
92
|
+
whyTheseFiles: buildWhyTheseFiles(result, groups),
|
|
93
|
+
doNotEditYet: buildDoNotEditYet(readOnlyContext, noiseRisk, coverageGroups, result)
|
|
70
94
|
};
|
|
71
95
|
}
|
|
96
|
+
function buildFirstThree(primary, readOnly, verification, noise, allGroups, result) {
|
|
97
|
+
const isReadOnly = result.profile.taskShape === "investigation" || result.profile.taskShape === "unknown";
|
|
98
|
+
const editLabel = isReadOnly ? "strongest evidence" : "primary edit candidate";
|
|
99
|
+
const first = [];
|
|
100
|
+
const used = new Set();
|
|
101
|
+
// 1. Top candidate
|
|
102
|
+
const topEdit = primary.find(g => g.role === "edit_candidate" && !g.contrastContext);
|
|
103
|
+
if (topEdit) {
|
|
104
|
+
first.push(`${topEdit.relativePath} — ${editLabel}, score ${topEdit.score.toFixed(0)}, tier ${topEdit.tier}`);
|
|
105
|
+
used.add(topEdit.relativePath);
|
|
106
|
+
}
|
|
107
|
+
// 2. Entry or handler context
|
|
108
|
+
const ctxCandidate = readOnly.find(g => !used.has(g.relativePath) && (g.slot === "entry" || g.slot === "handler") && !g.contrastContext)
|
|
109
|
+
?? allGroups.find(g => !used.has(g.relativePath) && (g.slot === "entry" || g.slot === "handler") && !g.contrastContext);
|
|
110
|
+
if (ctxCandidate) {
|
|
111
|
+
const why = ctxCandidate.slot === "entry" ? "route/entry point — confirm change path" : "handler/controller — confirm change surface";
|
|
112
|
+
first.push(`${ctxCandidate.relativePath} — ${why}, score ${ctxCandidate.score.toFixed(0)}`);
|
|
113
|
+
used.add(ctxCandidate.relativePath);
|
|
114
|
+
}
|
|
115
|
+
// 3. Test, model, or service for verification
|
|
116
|
+
const verifyCandidate = verification.find(g => !used.has(g.relativePath) && !g.contrastContext)
|
|
117
|
+
?? allGroups.find(g => !used.has(g.relativePath) && (g.slot === "tests" || g.slot === "data" || g.slot === "domain") && !g.contrastContext);
|
|
118
|
+
if (verifyCandidate) {
|
|
119
|
+
const why = verifyCandidate.slot === "tests" ? "tests/verification — check test coverage"
|
|
120
|
+
: verifyCandidate.slot === "data" ? "data/model — confirm schema impact"
|
|
121
|
+
: "service/domain — understand business logic";
|
|
122
|
+
first.push(`${verifyCandidate.relativePath} — ${why}, score ${verifyCandidate.score.toFixed(0)}`);
|
|
123
|
+
}
|
|
124
|
+
// Fallback: fill remaining from primary or read-only
|
|
125
|
+
const fallback = [...primary, ...readOnly, ...allGroups].filter(g => !used.has(g.relativePath) && !g.contrastContext && !noise.includes(g));
|
|
126
|
+
while (first.length < 3 && fallback.length > 0) {
|
|
127
|
+
const g = fallback.shift();
|
|
128
|
+
if (used.has(g.relativePath))
|
|
129
|
+
continue;
|
|
130
|
+
first.push(`${g.relativePath} — supports context, score ${g.score.toFixed(0)}`);
|
|
131
|
+
used.add(g.relativePath);
|
|
132
|
+
}
|
|
133
|
+
return first;
|
|
134
|
+
}
|
|
135
|
+
function buildWhyTheseFiles(result, groups) {
|
|
136
|
+
const parts = [];
|
|
137
|
+
const isEdit = !["investigation", "unknown"].includes(result.profile.taskShape);
|
|
138
|
+
// Task shape and intent
|
|
139
|
+
parts.push(`Task classified as "${result.profile.taskShape}"${isEdit ? " (edit intended)" : " (read-only investigation)"}, confidence ${(result.profile.intentConfidence * 100).toFixed(0)}%.`);
|
|
140
|
+
// Exact matches
|
|
141
|
+
const exactMatches = groups.filter(g => g.units.some(u => u.matchedTerms.length > 0));
|
|
142
|
+
if (exactMatches.length > 0) {
|
|
143
|
+
const names = exactMatches.slice(0, 3).map(g => g.relativePath.split("/").pop()).join(", ");
|
|
144
|
+
parts.push(`${exactMatches.length} file(s) matched task keywords in filename or content (e.g., ${names}).`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
parts.push("No lexical keyword matches — files selected by path structure and concept similarity.");
|
|
148
|
+
}
|
|
149
|
+
// Flow coverage from retrieval passes
|
|
150
|
+
const passes = new Set();
|
|
151
|
+
for (const g of groups.slice(0, 15)) {
|
|
152
|
+
for (const unit of g.units) {
|
|
153
|
+
for (const pass of unit.retrievalPasses) {
|
|
154
|
+
if (pass !== "Seed concepts")
|
|
155
|
+
passes.add(pass);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (passes.size > 0) {
|
|
160
|
+
parts.push(`Candidate flow covers ${passes.size} context slot(s): ${[...passes].slice(0, 6).join(", ")}.`);
|
|
161
|
+
}
|
|
162
|
+
// Semantic
|
|
163
|
+
const s = result.stats.semanticRetrievalStatus;
|
|
164
|
+
if (s === "active") {
|
|
165
|
+
const added = result.stats.semanticAddedFileCount ?? 0;
|
|
166
|
+
const supported = result.stats.semanticSupportedFileCount ?? 0;
|
|
167
|
+
parts.push(`Semantic retrieval active (${result.stats.semanticProvider || "local"}): ${supported} existing files supported, ${added} new candidate(s) discovered by vector similarity.`);
|
|
168
|
+
}
|
|
169
|
+
else if (s === "error_fallback") {
|
|
170
|
+
parts.push("Semantic retrieval unavailable — deterministic retrieval only.");
|
|
171
|
+
}
|
|
172
|
+
// Graph/symbol evidence
|
|
173
|
+
const graphCount = groups.filter(g => g.units.some(u => (u.graphReasons?.length ?? 0) > 0)).length;
|
|
174
|
+
const symbolCount = groups.filter(g => g.symbols.length > 0).length;
|
|
175
|
+
if (graphCount > 0 || symbolCount > 0) {
|
|
176
|
+
const ev = [];
|
|
177
|
+
if (graphCount > 0)
|
|
178
|
+
ev.push(`${graphCount} file(s) with graph/dependency evidence`);
|
|
179
|
+
if (symbolCount > 0)
|
|
180
|
+
ev.push(`${symbolCount} file(s) with symbol-name matches`);
|
|
181
|
+
parts.push(`${ev.join(", ")}.`);
|
|
182
|
+
}
|
|
183
|
+
// Noise separation
|
|
184
|
+
const noiseCount = groups.filter(g => g.contrastContext).length;
|
|
185
|
+
if (noiseCount > 0) {
|
|
186
|
+
parts.push(`${noiseCount} file(s) flagged as noise/contrast risk and separated into read-only or noise sections.`);
|
|
187
|
+
}
|
|
188
|
+
// Uncertainty
|
|
189
|
+
if (result.stats.stoppedEarly || result.stats.capped) {
|
|
190
|
+
const cap = result.stats.capped ? `candidate cap at ${result.stats.maxCandidates}` : "";
|
|
191
|
+
const early = result.stats.stoppedEarly ? `scan stopped early (${result.stats.stopReason ?? "unknown"})` : "";
|
|
192
|
+
parts.push(`Uncertainty: ${[cap, early].filter(Boolean).join("; ")}. Lower-ranked files may exist outside scanned window.`);
|
|
193
|
+
}
|
|
194
|
+
return parts.join(" ");
|
|
195
|
+
}
|
|
196
|
+
function buildDoNotEditYet(readOnly, noise, allGroups, result) {
|
|
197
|
+
const items = [];
|
|
198
|
+
const added = new Set();
|
|
199
|
+
for (const g of noise.slice(0, 3)) {
|
|
200
|
+
if (added.has(g.relativePath))
|
|
201
|
+
continue;
|
|
202
|
+
added.add(g.relativePath);
|
|
203
|
+
const reason = g.contrastContext ? "contrast context (matches avoided/previous-behavior terms)"
|
|
204
|
+
: g.tier === "low" && g.score < 50 ? "low confidence, likely unrelated"
|
|
205
|
+
: "noise risk — verify relevance before editing";
|
|
206
|
+
items.push(`${g.relativePath} — ${reason} (score ${g.score.toFixed(0)}, tier ${g.tier})`);
|
|
207
|
+
}
|
|
208
|
+
for (const g of readOnly.slice(0, 3)) {
|
|
209
|
+
if (added.has(g.relativePath))
|
|
210
|
+
continue;
|
|
211
|
+
added.add(g.relativePath);
|
|
212
|
+
const slot = g.slot ? (0, contextBlueprint_1.contextSlotLabel)(g.slot) : "context";
|
|
213
|
+
const reason = g.slot === "entry" ? "route/entry files — read for change path, edit elsewhere"
|
|
214
|
+
: g.slot === "ui" ? "UI/template files — read for display context, edit in handler/service"
|
|
215
|
+
: g.slot === "config" ? "config files — read for settings context, edit in domain files"
|
|
216
|
+
: g.role === "read_context" ? `read-only ${slot} context — useful for understanding, not the change surface`
|
|
217
|
+
: `read-only ${slot} — confirms context, risky as edit surface without more evidence`;
|
|
218
|
+
items.push(`${g.relativePath} — ${reason} (score ${g.score.toFixed(0)}, tier ${g.tier})`);
|
|
219
|
+
}
|
|
220
|
+
if (items.length === 0) {
|
|
221
|
+
items.push("All selected files appear relevant and safe for consideration. No files flagged as noise or read-only risk.");
|
|
222
|
+
}
|
|
223
|
+
return items;
|
|
224
|
+
}
|
|
225
|
+
function buildSemanticDiagnostic(stats, config) {
|
|
226
|
+
const s = stats.semanticRetrievalStatus;
|
|
227
|
+
if (!s || s === "disabled")
|
|
228
|
+
return "Semantic: disabled — deterministic/no-embedding retrieval only";
|
|
229
|
+
const provider = stats.semanticProvider || "unknown";
|
|
230
|
+
if (s === "active") {
|
|
231
|
+
const embedded = stats.semanticEmbeddedFiles ?? stats.semanticCacheHits ?? "?";
|
|
232
|
+
const hits = stats.semanticHitCount ?? "?";
|
|
233
|
+
const added = stats.semanticAddedFileCount ?? "?";
|
|
234
|
+
const supported = stats.semanticSupportedFileCount ?? "?";
|
|
235
|
+
const cacheHits = stats.semanticCacheHits ?? "?";
|
|
236
|
+
const cacheMisses = stats.semanticCacheMisses ?? "?";
|
|
237
|
+
if (provider === "local") {
|
|
238
|
+
const dims = config.index.semanticDimensions;
|
|
239
|
+
const dimStr = dims && dims > 0 ? `, ${dims}d` : "";
|
|
240
|
+
const cacheStr = (cacheHits !== "?" || cacheMisses !== "?") ? `, cache hits=${cacheHits}, cache misses=${cacheMisses}` : "";
|
|
241
|
+
return `Semantic: active (local bundled model${dimStr}, embedded=${embedded}${cacheStr}, vector hits=${hits}, semantic candidates=${added}, files with semantic support=${supported})`;
|
|
242
|
+
}
|
|
243
|
+
const model = stats.semanticModel || "unknown";
|
|
244
|
+
return `Semantic: active (${provider}, model=${model}, embedded=${embedded}, vector hits=${hits}, semantic candidates=${added}, files with semantic support=${supported})`;
|
|
245
|
+
}
|
|
246
|
+
if (s === "error_fallback") {
|
|
247
|
+
const err = stats.semanticErrorType || "unknown";
|
|
248
|
+
const msg = stats.semanticErrorMessage || "";
|
|
249
|
+
return `Semantic: error_fallback (${provider}, ${err}${msg ? ": " + msg.substring(0, 100) : ""}) — deterministic retrieval used as fallback`;
|
|
250
|
+
}
|
|
251
|
+
if (s === "skipped_no_key")
|
|
252
|
+
return `Semantic: skipped (no API key configured) — deterministic retrieval used`;
|
|
253
|
+
return `Semantic: ${s}`;
|
|
254
|
+
}
|
|
72
255
|
function renderSolvePacketMarkdown(packet) {
|
|
73
256
|
const lines = [
|
|
74
257
|
"# FlowSeeker Solve Packet",
|
|
@@ -85,24 +268,67 @@ function renderSolvePacketMarkdown(packet) {
|
|
|
85
268
|
"## User Task",
|
|
86
269
|
packet.userTask,
|
|
87
270
|
"",
|
|
271
|
+
"## Retrieval Mode",
|
|
272
|
+
packet.semanticDiagnostic || "Semantic: disabled",
|
|
273
|
+
"",
|
|
88
274
|
"## Problem Hypothesis",
|
|
89
275
|
packet.problemHypothesis
|
|
90
276
|
];
|
|
277
|
+
// First 3 files to open
|
|
278
|
+
if (packet.firstThreeFiles.length > 0) {
|
|
279
|
+
lines.push("", "## First 3 Files To Open");
|
|
280
|
+
for (const file of packet.firstThreeFiles) {
|
|
281
|
+
lines.push(`- ${file}`);
|
|
282
|
+
}
|
|
283
|
+
lines.push("");
|
|
284
|
+
}
|
|
285
|
+
// Why these files
|
|
286
|
+
if (packet.whyTheseFiles) {
|
|
287
|
+
lines.push("", "## Why These Files", packet.whyTheseFiles, "");
|
|
288
|
+
}
|
|
91
289
|
if (packet.candidateFlow.length > 0) {
|
|
92
|
-
lines.push("", "## Candidate Flow
|
|
93
|
-
for (
|
|
94
|
-
|
|
290
|
+
lines.push("", "## Candidate Flow", "Ordered path target: route -> controller -> service/job/export/import -> model/data -> view/template -> test.");
|
|
291
|
+
for (let i = 0; i < packet.candidateFlow.length; i++) {
|
|
292
|
+
const step = packet.candidateFlow[i];
|
|
293
|
+
lines.push(`### ${i + 1}. ${step.label}${step.purpose ? ` — ${step.purpose}` : ""}`);
|
|
294
|
+
for (const file of step.files) {
|
|
295
|
+
lines.push(`- ${file}`);
|
|
296
|
+
}
|
|
297
|
+
if (step.retrievalTrace && step.retrievalTrace.length > 0) {
|
|
298
|
+
lines.push(` Reasons: ${step.retrievalTrace.join("; ")}`);
|
|
299
|
+
}
|
|
300
|
+
if (step.hint) {
|
|
301
|
+
lines.push(` ${step.hint}`);
|
|
302
|
+
}
|
|
303
|
+
lines.push("");
|
|
95
304
|
}
|
|
96
305
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
lines.push(
|
|
100
|
-
|
|
101
|
-
|
|
306
|
+
const isReadOnly = packet.taskShape === "investigation" || packet.taskShape === "unknown";
|
|
307
|
+
const primaryTitle = isReadOnly ? "## Primary Context Candidates" : "## Primary Edit Candidates";
|
|
308
|
+
lines.push(primaryTitle, ...renderPacketFiles(packet.primaryEditCandidates, isReadOnly));
|
|
309
|
+
lines.push("## Read-Only Context", ...renderPacketFiles(packet.readOnlyContext, isReadOnly, "No additional read-only context was selected beyond primary edit candidates and verification targets."));
|
|
310
|
+
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."));
|
|
311
|
+
lines.push("## Noise Risk", ...renderPacketFiles(packet.noiseRisk, isReadOnly, "No obvious noise-risk files were selected. All ranked candidates passed relevance filters."));
|
|
312
|
+
if (packet.doNotEditYet.length > 0) {
|
|
313
|
+
lines.push("## Do Not Edit Yet", ...packet.doNotEditYet.map((item) => `- ${item}`), "");
|
|
102
314
|
}
|
|
103
315
|
if (packet.missingLinks.length > 0) {
|
|
104
316
|
lines.push("## Missing Links", ...packet.missingLinks.map((link) => `- ${link}`), "");
|
|
105
317
|
}
|
|
318
|
+
if (packet.retrievalTrace.length > 0) {
|
|
319
|
+
lines.push("## Retrieval Trace", ...packet.retrievalTrace.map((trace) => `- ${trace}`), "");
|
|
320
|
+
}
|
|
321
|
+
if (packet.verificationCommands.length > 0) {
|
|
322
|
+
lines.push("## Verification Command Hints");
|
|
323
|
+
for (const vc of packet.verificationCommands) {
|
|
324
|
+
lines.push(`- \`${vc.command}\``);
|
|
325
|
+
lines.push(` Evidence: ${vc.evidence} (${vc.confidence}) — ${vc.reason}`);
|
|
326
|
+
}
|
|
327
|
+
lines.push("");
|
|
328
|
+
}
|
|
329
|
+
if (packet.tokenBudgetWarning) {
|
|
330
|
+
lines.push("## Token Budget Warning", packet.tokenBudgetWarning, "");
|
|
331
|
+
}
|
|
106
332
|
lines.push("## Agent Instructions", ...packet.agentInstructions.map((instruction, index) => `${index + 1}. ${instruction}`), "");
|
|
107
333
|
return lines.join("\n");
|
|
108
334
|
}
|
|
@@ -148,40 +374,111 @@ function bestGroupForSlot(groups, slot, selected) {
|
|
|
148
374
|
}
|
|
149
375
|
function buildCandidateFlow(groups, result) {
|
|
150
376
|
const flow = [];
|
|
151
|
-
const orderedSlots =
|
|
377
|
+
const orderedSlots = ["entry", "handler", "domain", "side_effect", "validation", "data", "ui", "tests"];
|
|
378
|
+
const isEdit = isEditIntendedTask(result);
|
|
379
|
+
const required = new Set(result.profile.blueprint.requiredSlots);
|
|
380
|
+
const optional = new Set(result.profile.blueprint.optionalSlots);
|
|
152
381
|
for (const slot of orderedSlots) {
|
|
153
|
-
|
|
154
|
-
|
|
382
|
+
if (!required.has(slot) && !optional.has(slot))
|
|
383
|
+
continue;
|
|
384
|
+
const slotGroups = groups.filter((group) => group.slot === slot);
|
|
385
|
+
if (slotGroups.length === 0) {
|
|
155
386
|
flow.push({
|
|
156
387
|
section: slot,
|
|
157
388
|
label: sectionLabel(slot),
|
|
158
|
-
files:
|
|
389
|
+
files: [`Missing ${(0, contextBlueprint_1.contextSlotLabel)(slot)} evidence`],
|
|
390
|
+
purpose: "missing link to verify before relying on packet",
|
|
391
|
+
hint: `Search hint: ${slotSearchHints(slot, result)}`
|
|
159
392
|
});
|
|
393
|
+
continue;
|
|
160
394
|
}
|
|
395
|
+
const files = slotGroups.map((group) => formatFlowFile(group, result));
|
|
396
|
+
const topGroup = slotGroups[0];
|
|
397
|
+
const purpose = flowPurpose(slot, isEdit, topGroup);
|
|
398
|
+
const hint = isEdit && (slot === "entry" || slot === "handler" || slot === "domain") ? "Inspect before editing — confirm this is the correct change surface." : undefined;
|
|
399
|
+
flow.push({
|
|
400
|
+
section: slot,
|
|
401
|
+
label: flowStepLabel(slot),
|
|
402
|
+
files: files.slice(0, 3),
|
|
403
|
+
retrievalTrace: slotGroups.flatMap((group) => group.reasons).slice(0, 2),
|
|
404
|
+
purpose,
|
|
405
|
+
hint
|
|
406
|
+
});
|
|
161
407
|
}
|
|
162
408
|
return flow;
|
|
163
409
|
}
|
|
164
|
-
function
|
|
165
|
-
const
|
|
166
|
-
|
|
410
|
+
function flowStepLabel(slot) {
|
|
411
|
+
const labels = {
|
|
412
|
+
entry: "Route / entry",
|
|
413
|
+
handler: "Controller / handler",
|
|
414
|
+
domain: "Service / domain",
|
|
415
|
+
side_effect: "Job / export / import",
|
|
416
|
+
validation: "Request / validation",
|
|
417
|
+
data: "Model / data",
|
|
418
|
+
ui: "View / template",
|
|
419
|
+
tests: "Test / verification"
|
|
420
|
+
};
|
|
421
|
+
return labels[slot] ?? sectionLabel(slot);
|
|
422
|
+
}
|
|
423
|
+
function flowPurpose(slot, isEdit, group) {
|
|
424
|
+
const base = slotPurposeLabel(slot);
|
|
425
|
+
if (isEdit) {
|
|
426
|
+
if (slot === "entry" || slot === "handler")
|
|
427
|
+
return `${base}: start here to trace the change path`;
|
|
428
|
+
if (slot === "domain" || slot === "data")
|
|
429
|
+
return `${base}: likely change surface`;
|
|
430
|
+
if (slot === "tests")
|
|
431
|
+
return `${base}: update after changes`;
|
|
432
|
+
return `${base}: review for impact`;
|
|
433
|
+
}
|
|
434
|
+
return `${base}: read for context`;
|
|
435
|
+
}
|
|
436
|
+
function slotPurposeLabel(slot) {
|
|
437
|
+
const labels = {
|
|
438
|
+
entry: "Request entry point",
|
|
439
|
+
handler: "Request handler / controller",
|
|
440
|
+
domain: "Business logic / domain",
|
|
441
|
+
data: "Data model / persistence",
|
|
442
|
+
validation: "Input validation",
|
|
443
|
+
permission: "Authorization / permissions",
|
|
444
|
+
config: "Configuration",
|
|
445
|
+
side_effect: "Side effects / events",
|
|
446
|
+
ui: "UI / presentation",
|
|
447
|
+
tests: "Tests / verification",
|
|
448
|
+
docs: "Documentation",
|
|
449
|
+
unknown: "Matched context"
|
|
450
|
+
};
|
|
451
|
+
return labels[slot] ?? "Context";
|
|
452
|
+
}
|
|
453
|
+
function formatFlowFile(group, _result) {
|
|
454
|
+
return `${group.relativePath} [${group.tier}; score ${group.score.toFixed(0)}]`;
|
|
167
455
|
}
|
|
168
|
-
function renderPacketFiles(files) {
|
|
456
|
+
function renderPacketFiles(files, isReadOnly, placeholderText) {
|
|
169
457
|
if (files.length === 0) {
|
|
170
|
-
return ["- None identified.", ""];
|
|
171
|
-
}
|
|
172
|
-
return files.flatMap((file) =>
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
458
|
+
return placeholderText ? [`- ${placeholderText}`, ""] : ["- None identified.", ""];
|
|
459
|
+
}
|
|
460
|
+
return files.flatMap((file) => {
|
|
461
|
+
const roleLabel = isReadOnly && file.role === "edit_candidate" ? "context_candidate" : file.role;
|
|
462
|
+
const connectedLabel = isReadOnly ? "Related context to inspect" : "Read before editing";
|
|
463
|
+
return [
|
|
464
|
+
`- ${file.relativePath}`,
|
|
465
|
+
` Slot: ${file.slot ? (0, contextBlueprint_1.contextSlotLabel)(file.slot) : "Unknown"}; Role: ${roleLabel}; Kind: ${file.kind}; Score: ${file.score.toFixed(1)}; Tier: ${file.tier}`,
|
|
466
|
+
...(file.contextMode ? [` Context: ${contextModeLabel(file.contextMode)}${file.contextRange ? `, lines ${file.contextRange.startLine}-${file.contextRange.endLine}` : ""}`] : []),
|
|
467
|
+
...(file.expectedChangeSurface ? [` Expected change surface: ${file.expectedChangeSurface}`] : []),
|
|
468
|
+
...(file.riskLevel ? [` Risk: ${file.riskLevel}`] : []),
|
|
469
|
+
...(file.connectedContext && file.connectedContext.length > 0 ? [` ${connectedLabel}: ${file.connectedContext.join(", ")}`] : []),
|
|
470
|
+
...(file.retrievalTrace && file.retrievalTrace.length > 0 ? [` Selected by: ${file.retrievalTrace.join("; ")}`] : []),
|
|
471
|
+
...(file.reasons.length > 0 ? [` Reasons: ${file.reasons.join("; ")}`] : []),
|
|
472
|
+
...(file.snippet ? ["", "```", file.snippet, "```"] : []),
|
|
473
|
+
""
|
|
474
|
+
];
|
|
475
|
+
});
|
|
180
476
|
}
|
|
181
477
|
function sectionLabel(section) {
|
|
182
478
|
return (0, contextBlueprint_1.contextSlotLabel)(section);
|
|
183
479
|
}
|
|
184
|
-
async function toPacketFiles(groups, budget, purpose) {
|
|
480
|
+
async function toPacketFiles(groups, budget, purpose, result) {
|
|
481
|
+
const isReadOnly = result.profile.taskShape === "investigation" || result.profile.taskShape === "unknown";
|
|
185
482
|
const files = [];
|
|
186
483
|
for (const [index, group] of groups.entries()) {
|
|
187
484
|
const snippetContext = await buildSnippetContext(group, budget, maxCharsForPurpose(purpose, index));
|
|
@@ -192,6 +489,10 @@ async function toPacketFiles(groups, budget, purpose) {
|
|
|
192
489
|
slot: group.slot,
|
|
193
490
|
contextMode: snippetContext.mode,
|
|
194
491
|
contextRange: snippetContext.range,
|
|
492
|
+
expectedChangeSurface: isReadOnly ? undefined : expectedChangeSurface(group, purpose),
|
|
493
|
+
riskLevel: riskLevel(group, purpose),
|
|
494
|
+
connectedContext: connectedContext(group, groups),
|
|
495
|
+
retrievalTrace: retrievalTraceForGroup(group),
|
|
195
496
|
score: group.score,
|
|
196
497
|
confidence: group.confidence,
|
|
197
498
|
tier: group.tier,
|
|
@@ -201,6 +502,115 @@ async function toPacketFiles(groups, budget, purpose) {
|
|
|
201
502
|
}
|
|
202
503
|
return files;
|
|
203
504
|
}
|
|
505
|
+
function expectedChangeSurface(group, purpose) {
|
|
506
|
+
if (purpose !== "primary") {
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
const slot = group.slot ? (0, contextBlueprint_1.contextSlotLabel)(group.slot).toLowerCase() : "matched code";
|
|
510
|
+
return `Smallest change likely belongs in this ${slot} surface after validating connected context.`;
|
|
511
|
+
}
|
|
512
|
+
function riskLevel(group, purpose) {
|
|
513
|
+
if (purpose === "noise") {
|
|
514
|
+
return "high";
|
|
515
|
+
}
|
|
516
|
+
if (purpose === "context" || group.contrastContext) {
|
|
517
|
+
return "medium";
|
|
518
|
+
}
|
|
519
|
+
return group.tier === "high" ? "low" : "medium";
|
|
520
|
+
}
|
|
521
|
+
function connectedContext(group, groups) {
|
|
522
|
+
const groupTrace = new Set(retrievalTraceForGroup(group));
|
|
523
|
+
return groups
|
|
524
|
+
.filter((candidate) => candidate.relativePath !== group.relativePath)
|
|
525
|
+
.filter((candidate) => candidate.subsystem && group.subsystem ? candidate.subsystem === group.subsystem : candidate.slot && candidate.slot !== group.slot)
|
|
526
|
+
.filter((candidate) => {
|
|
527
|
+
const trace = retrievalTraceForGroup(candidate);
|
|
528
|
+
return trace.some((item) => groupTrace.has(item)) || candidate.subsystem === group.subsystem || isAdjacentSlot(group.slot, candidate.slot);
|
|
529
|
+
})
|
|
530
|
+
.slice(0, 3)
|
|
531
|
+
.map((candidate) => candidate.relativePath);
|
|
532
|
+
}
|
|
533
|
+
function isTestAdjacent(group) {
|
|
534
|
+
const normalized = group.relativePath.replace(/\\/g, "/").toLowerCase();
|
|
535
|
+
return /\b(test|spec|__tests__|e2e|cypress|playwright)\b/.test(normalized) ||
|
|
536
|
+
/\b(test|spec)\.(ts|tsx|js|jsx|php|py|rb|go|java)$/.test(normalized);
|
|
537
|
+
}
|
|
538
|
+
function isVerificationHint(group) {
|
|
539
|
+
const normalized = group.relativePath.replace(/\\/g, "/").toLowerCase();
|
|
540
|
+
return /\b(composer\.json|package\.json|phpunit\.xml|phpunit\.xml\.dist|jest\.config|vitest\.config|Makefile|docker-compose|\.env\.example)\b/.test(normalized) ||
|
|
541
|
+
/\.(yml|yaml|toml|xml\.dist)$/.test(normalized);
|
|
542
|
+
}
|
|
543
|
+
function isAdjacentSlot(left, right) {
|
|
544
|
+
const adjacent = new Set([
|
|
545
|
+
"entry:handler",
|
|
546
|
+
"handler:domain",
|
|
547
|
+
"domain:data",
|
|
548
|
+
"domain:config",
|
|
549
|
+
"domain:side_effect",
|
|
550
|
+
"handler:permission",
|
|
551
|
+
"ui:handler",
|
|
552
|
+
"tests:domain",
|
|
553
|
+
"tests:handler"
|
|
554
|
+
]);
|
|
555
|
+
return adjacent.has(`${left}:${right}`) || adjacent.has(`${right}:${left}`);
|
|
556
|
+
}
|
|
557
|
+
function retrievalTraceForGroup(group) {
|
|
558
|
+
return group.units
|
|
559
|
+
.flatMap((unit) => [
|
|
560
|
+
...unit.retrievalPasses.map((pass) => `pass:${pass}`),
|
|
561
|
+
...(unit.retrievalTrace ?? []),
|
|
562
|
+
...(unit.channelHits ?? []).map((hit) => `${hit.channel}:${hit.reason}`),
|
|
563
|
+
...(unit.graphReasons ?? []).map((reason) => `graph:${reason}`),
|
|
564
|
+
...(unit.frameworkEdgeReason ? [`graph:${unit.frameworkEdgeReason}`] : [])
|
|
565
|
+
])
|
|
566
|
+
.filter((trace, index, traces) => traces.indexOf(trace) === index)
|
|
567
|
+
.slice(0, 4);
|
|
568
|
+
}
|
|
569
|
+
function buildRetrievalTrace(groups) {
|
|
570
|
+
return groups
|
|
571
|
+
.slice(0, 10)
|
|
572
|
+
.flatMap((group) => retrievalTraceForGroup(group).map((trace) => `${group.relativePath}: ${trace}`))
|
|
573
|
+
.slice(0, 20);
|
|
574
|
+
}
|
|
575
|
+
function suggestVerificationCommands(result) {
|
|
576
|
+
const paths = result.units.map((unit) => unit.relativePath.replace(/\\/g, "/"));
|
|
577
|
+
const commands = [];
|
|
578
|
+
const added = new Set();
|
|
579
|
+
function add(cmd, evidence, confidence, reason) {
|
|
580
|
+
if (added.has(cmd))
|
|
581
|
+
return;
|
|
582
|
+
added.add(cmd);
|
|
583
|
+
commands.push({ command: cmd, evidence, confidence, reason, runnable: true });
|
|
584
|
+
}
|
|
585
|
+
const hasComposer = paths.some((p) => p === "composer.json");
|
|
586
|
+
const hasLaravel = paths.some((p) => p === "artisan" || /(^|\/)(routes|app\/Http|app\/Models|app\/Console|resources\/views)\//.test(p));
|
|
587
|
+
const hasPHP = hasLaravel || hasComposer || paths.some((p) => p.endsWith(".php"));
|
|
588
|
+
const hasPackageJson = paths.some((p) => p === "package.json" || p.endsWith("/package.json"));
|
|
589
|
+
const hasJS = hasPackageJson || paths.some((p) => /(^|\/)src\//.test(p) || /\.(ts|tsx|js|jsx)$/.test(p));
|
|
590
|
+
const hasPython = paths.some((p) => /(^|\/)tests?\/.*\.py$/.test(p) || p.endsWith(".py"));
|
|
591
|
+
const hasGo = paths.some((p) => /\.go$/.test(p));
|
|
592
|
+
if (hasLaravel || hasPHP) {
|
|
593
|
+
const trigger = paths.find((p) => p.startsWith("routes/")) ?? paths.find((p) => p.endsWith(".php")) ?? "Laravel/PHP files";
|
|
594
|
+
add("php artisan route:list", trigger, "detected", "Laravel/PHP route context detected; verify route/controller mapping before edits");
|
|
595
|
+
add("php artisan test", trigger, "detected", "Laravel/PHP app file detected; run framework tests after edits");
|
|
596
|
+
add("composer test", hasComposer ? "composer.json" : trigger, hasComposer ? "detected" : "inferred", "Run Composer test script when composer.json defines one");
|
|
597
|
+
}
|
|
598
|
+
if (hasJS) {
|
|
599
|
+
const trigger = paths.find((p) => p.endsWith("package.json")) ?? "JS/TS source";
|
|
600
|
+
add("npm run compile --silent", trigger, hasPackageJson ? "detected" : "inferred", "JavaScript/TypeScript project detected; run type-check before verifying edits");
|
|
601
|
+
add("npm run test:smoke", trigger, hasPackageJson ? "detected" : "inferred", "JavaScript/TypeScript project detected; run available package test/smoke script");
|
|
602
|
+
}
|
|
603
|
+
if (hasPython) {
|
|
604
|
+
const trigger = paths.find((p) => p.endsWith(".py")) ?? "tests/";
|
|
605
|
+
add("pytest", trigger, "detected", "Python test file detected; run pytest to verify changes");
|
|
606
|
+
}
|
|
607
|
+
if (hasGo) {
|
|
608
|
+
const trigger = paths.find((p) => /\.go$/.test(p)) ?? "*.go";
|
|
609
|
+
add("go test ./...", trigger, "detected", "Go source file detected; run Go test suite");
|
|
610
|
+
}
|
|
611
|
+
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");
|
|
612
|
+
return commands.slice(0, 6);
|
|
613
|
+
}
|
|
204
614
|
function compactSnippet(snippet) {
|
|
205
615
|
if (!snippet) {
|
|
206
616
|
return undefined;
|
|
@@ -335,22 +745,119 @@ function isNoiseGroup(group, neededSlots) {
|
|
|
335
745
|
}
|
|
336
746
|
return false;
|
|
337
747
|
}
|
|
748
|
+
function missingSlotMessage(slot, groups, result, coveredSlots) {
|
|
749
|
+
const slotLabel = (0, contextBlueprint_1.contextSlotLabel)(slot);
|
|
750
|
+
const hints = slotSearchHints(slot, result);
|
|
751
|
+
const currentlyCovered = Array.from(coveredSlots).filter(s => s !== "unknown").map(contextBlueprint_1.contextSlotLabel).join(", ") || "no clear slots";
|
|
752
|
+
const uncertainty = result.stats.stoppedEarly
|
|
753
|
+
? ` Scan stopped early (${result.stats.stopReason ?? "unknown reason"}) — lower-ranked context may exist; do not convert scan-cap uncertainty into a missing-slot claim.`
|
|
754
|
+
: ` ${capUncertainty(result)}`;
|
|
755
|
+
return `Missing ${slotLabel} — search hint: ${hints}. Currently covered slots: ${currentlyCovered}.${uncertainty} Pause editing until this slot is found or confirmed not needed.`;
|
|
756
|
+
}
|
|
757
|
+
function slotSearchHints(slot, result) {
|
|
758
|
+
const concepts = result.profile.concepts.slice(0, 4).join(", ") || result.profile.keywords.slice(0, 4).join(", ") || result.task;
|
|
759
|
+
const keywords = result.profile.keywords.slice(0, 6).join(" ");
|
|
760
|
+
const hintMap = {
|
|
761
|
+
entry: `search paths matching routes, controllers, or entry points near ${concepts}; try "${keywords}" as grep terms`,
|
|
762
|
+
handler: `search for handler/controller/service files containing ${concepts}; look in src/, app/, or server/ directories`,
|
|
763
|
+
domain: `search for service/domain/logic files with ${concepts}; check for interfaces, use-cases, or business logic modules`,
|
|
764
|
+
data: `search for model/entity/schema files related to ${concepts}; check database migrations, ORM models, or API schemas`,
|
|
765
|
+
validation: `search for validation/request/schema files; try "validate", "sanitize", "schema", or "${concepts}"`,
|
|
766
|
+
permission: `search for auth/guard/policy files; try "can", "authorize", "policy", "role", or "${concepts}"`,
|
|
767
|
+
config: `search for config/env/settings files; check .env, config/, or app config for ${concepts}`,
|
|
768
|
+
side_effect: `search for event/job/mail/queue files; try "dispatch", "emit", "fire", "send", or "${concepts}"`,
|
|
769
|
+
ui: `search for view/template/component files; check resources/, views/, or components/ for ${concepts}`,
|
|
770
|
+
tests: `search for test/spec files; try "test", "spec", "it(", "describe(" near ${concepts}`,
|
|
771
|
+
docs: `search for README/CONTRIBUTING/docs files; check docs/ or project root for ${concepts}`,
|
|
772
|
+
unknown: `search broadly for ${concepts} across all project files`
|
|
773
|
+
};
|
|
774
|
+
return hintMap[slot] || hintMap.unknown;
|
|
775
|
+
}
|
|
776
|
+
function capUncertainty(result) {
|
|
777
|
+
const stats = result.stats;
|
|
778
|
+
const parts = [];
|
|
779
|
+
if (stats.capped)
|
|
780
|
+
parts.push(`candidate cap hit (ranked=${stats.rankedEvidenceCount}, max=${stats.maxCandidates})`);
|
|
781
|
+
if (stats.stoppedEarly)
|
|
782
|
+
parts.push(`scan stopped early (${stats.stopReason ?? "unknown reason"})`);
|
|
783
|
+
if (stats.candidateFiles && stats.maxCandidates && stats.candidateFiles >= stats.maxCandidates)
|
|
784
|
+
parts.push(`candidate file cap may hide lower-ranked matches (candidateFiles=${stats.candidateFiles})`);
|
|
785
|
+
return parts.length > 0 ? parts.join("; ") : "no scan/candidate cap signal reported";
|
|
786
|
+
}
|
|
338
787
|
function detectMissingLinks(groups, result) {
|
|
339
788
|
const missing = [];
|
|
340
|
-
const
|
|
789
|
+
const isEdit = isEditIntendedTask(result);
|
|
790
|
+
const coveredSlots = computeCoveredSlots(groups, result);
|
|
341
791
|
for (const slot of result.profile.blueprint.requiredSlots) {
|
|
342
|
-
if (!
|
|
343
|
-
missing.push(
|
|
792
|
+
if (!coveredSlots.has(slot)) {
|
|
793
|
+
missing.push(missingSlotMessage(slot, groups, result, coveredSlots));
|
|
344
794
|
}
|
|
345
795
|
}
|
|
346
|
-
if (
|
|
347
|
-
|
|
796
|
+
if (isEdit && !groups.some((group) => group.role === "edit_candidate")) {
|
|
797
|
+
const topPaths = groups.slice(0, 3).map(g => g.relativePath).join(", ");
|
|
798
|
+
missing.push(`No edit_candidate files were ranked with sufficient confidence. Try broadening search with additional keywords or lowering minConfidence. Nearest candidates: ${topPaths || "none"}.`);
|
|
799
|
+
}
|
|
800
|
+
const uncertainty = capUncertainty(result);
|
|
801
|
+
const stoppedEarly = result.stats.stoppedEarly;
|
|
802
|
+
if (missing.length === 0) {
|
|
803
|
+
const coveredLabels = Array.from(coveredSlots).filter(s => s !== "unknown").map(contextBlueprint_1.contextSlotLabel).join(", ");
|
|
804
|
+
const allRequired = result.profile.blueprint.requiredSlots.every(s => coveredSlots.has(s));
|
|
805
|
+
const topFile = groups[0]?.relativePath ?? "top-ranked file";
|
|
806
|
+
const uncertaintyNote = stoppedEarly
|
|
807
|
+
? ` Scan stopped early — lower-ranked context may exist outside the scan window.`
|
|
808
|
+
: "";
|
|
809
|
+
const capNote = stoppedEarly
|
|
810
|
+
? ` 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}`
|
|
811
|
+
: ` 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.`;
|
|
812
|
+
if (isEdit) {
|
|
813
|
+
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}`);
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
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.`);
|
|
817
|
+
}
|
|
348
818
|
}
|
|
349
|
-
|
|
350
|
-
|
|
819
|
+
else {
|
|
820
|
+
if (stoppedEarly) {
|
|
821
|
+
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.`);
|
|
822
|
+
}
|
|
351
823
|
}
|
|
352
824
|
return missing;
|
|
353
825
|
}
|
|
826
|
+
function computeCoveredSlots(groups, result) {
|
|
827
|
+
const covered = new Set();
|
|
828
|
+
// Source 1: group.slot values
|
|
829
|
+
for (const group of groups) {
|
|
830
|
+
if (group.slot && group.slot !== "unknown") {
|
|
831
|
+
covered.add(group.slot);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
// Source 2: retrieval passes — parse "pass:Entry / Routes coverage" patterns
|
|
835
|
+
const slotLabelToKey = new Map();
|
|
836
|
+
for (const slot of (0, contextBlueprint_1.allContextSlots)(result.profile.blueprint)) {
|
|
837
|
+
slotLabelToKey.set((0, contextBlueprint_1.contextSlotLabel)(slot).toLowerCase(), slot);
|
|
838
|
+
}
|
|
839
|
+
// Also add abbreviated forms
|
|
840
|
+
slotLabelToKey.set("seed concepts", "unknown");
|
|
841
|
+
for (const group of groups) {
|
|
842
|
+
for (const unit of group.units) {
|
|
843
|
+
for (const pass of unit.retrievalPasses) {
|
|
844
|
+
const normalized = pass.toLowerCase().replace(/\s+coverage$/, "").trim();
|
|
845
|
+
if (slotLabelToKey.has(normalized)) {
|
|
846
|
+
covered.add(slotLabelToKey.get(normalized));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
// Source 3: evidence units that have a specific slot assigned
|
|
852
|
+
for (const group of groups) {
|
|
853
|
+
for (const unit of group.units) {
|
|
854
|
+
if (unit.slot && unit.slot !== "unknown") {
|
|
855
|
+
covered.add(unit.slot);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
return covered;
|
|
860
|
+
}
|
|
354
861
|
function isEditIntendedTask(result) {
|
|
355
862
|
return !["investigation", "unknown"].includes(result.profile.taskShape);
|
|
356
863
|
}
|