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.
- package/.env.example +7 -0
- package/CHANGELOG.md +143 -108
- package/README.md +288 -221
- package/dist/chat/nativeChatParticipant.js +1 -1
- package/dist/cli/flowCommand.js +175 -0
- package/dist/cli/main.js +1810 -0
- package/dist/cli/mcpServer.js +7 -1
- package/dist/cli/runEvaluation.js +281 -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/eval/accuracyV2.js +483 -0
- package/dist/eval/goldenTask.js +192 -0
- package/dist/extension.js +23 -0
- package/dist/framework/laravel.js +177 -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/semanticChunkIndex.js +388 -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 +901 -26
- package/dist/mcp/mcpTools.js +1678 -2
- package/dist/pipeline/contextBlueprint.js +15 -2
- 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 +209 -12
- package/dist/pipeline/fusionTrace.js +149 -0
- package/dist/pipeline/llmReranker.js +151 -0
- package/dist/pipeline/nodeScan.js +102 -12
- package/dist/pipeline/ranker.js +875 -16
- package/dist/pipeline/retrievalFusion.js +41 -0
- package/dist/pipeline/roleRefinement.js +62 -0
- package/dist/pipeline/runHeadless.js +60 -5
- package/dist/pipeline/runPipeline.js +2 -2
- package/dist/pipeline/solvePacket.js +656 -43
- package/dist/pipeline/subsystem.js +21 -0
- package/dist/pipeline/taskUnderstanding.js +2 -2
- package/dist/ui/chatViewProvider.js +1 -1
- 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
|
@@ -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
|
-
|
|
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:
|
|
65
|
-
readOnlyContext:
|
|
66
|
-
verificationTargets:
|
|
67
|
-
noiseRisk:
|
|
68
|
-
missingLinks: detectMissingLinks(
|
|
69
|
-
|
|
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
|
|
93
|
-
for (
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
lines.push(
|
|
100
|
-
|
|
101
|
-
|
|
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 =
|
|
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
|
-
|
|
154
|
-
|
|
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:
|
|
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
|
|
165
|
-
const
|
|
166
|
-
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
|
889
|
+
const isEdit = isEditIntendedTask(result);
|
|
890
|
+
const coveredSlots = computeCoveredSlots(groups, result);
|
|
341
891
|
for (const slot of result.profile.blueprint.requiredSlots) {
|
|
342
|
-
if (!
|
|
343
|
-
missing.push(
|
|
892
|
+
if (!coveredSlots.has(slot)) {
|
|
893
|
+
missing.push(missingSlotMessage(slot, groups, result, coveredSlots));
|
|
344
894
|
}
|
|
345
895
|
}
|
|
346
|
-
if (
|
|
347
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
}
|