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
package/dist/pipeline/ranker.js
CHANGED
|
@@ -1,9 +1,72 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.rankEvidence = rankEvidence;
|
|
4
|
+
exports.classifyPathRole = classifyPathRole;
|
|
4
5
|
const contextBlueprint_1 = require("./contextBlueprint");
|
|
5
6
|
const text_1 = require("../utils/text");
|
|
6
7
|
const subsystem_1 = require("./subsystem");
|
|
8
|
+
// ── P15: taskAnchorCoverage (report-only, no ranking impact) ──
|
|
9
|
+
function computeTaskAnchorCoverage(taskText, matchedTerms) {
|
|
10
|
+
if (!taskText || taskText.trim().length === 0) {
|
|
11
|
+
return {
|
|
12
|
+
anchorHits: [], anchorMisses: [], anchorHitCount: 0, anchorMissCount: 0,
|
|
13
|
+
anchorRatio: 0, criticalAnchorHits: [], criticalAnchorMisses: [],
|
|
14
|
+
hasTaskAnchorCoverage: false, coverageReason: "no_task_text"
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const taskTokens = taskText.toLowerCase().replace(/[^a-z0-9À-ɏḀ-ỿ\s]/g, " ").split(/\s+/).filter(t => t.length > 1);
|
|
18
|
+
const mtLower = matchedTerms.map(t => t.toLowerCase());
|
|
19
|
+
const criticalTokens = taskTokens.filter(t => t.length > 4);
|
|
20
|
+
const anchorHits = [];
|
|
21
|
+
const anchorMisses = [];
|
|
22
|
+
for (const token of taskTokens) {
|
|
23
|
+
const found = mtLower.some(mt => mt.includes(token) || token.includes(mt));
|
|
24
|
+
if (found)
|
|
25
|
+
anchorHits.push(token);
|
|
26
|
+
else
|
|
27
|
+
anchorMisses.push(token);
|
|
28
|
+
}
|
|
29
|
+
const criticalAnchorHits = [];
|
|
30
|
+
const criticalAnchorMisses = [];
|
|
31
|
+
for (const ct of criticalTokens) {
|
|
32
|
+
const found = mtLower.some(mt => mt.includes(ct) || ct.includes(mt));
|
|
33
|
+
if (found)
|
|
34
|
+
criticalAnchorHits.push(ct);
|
|
35
|
+
else
|
|
36
|
+
criticalAnchorMisses.push(ct);
|
|
37
|
+
}
|
|
38
|
+
const ratio = taskTokens.length > 0 ? anchorHits.length / taskTokens.length : 0;
|
|
39
|
+
const reason = mtLower.length === 0 ? "no_matched_terms"
|
|
40
|
+
: anchorHits.length === 0 ? "no_anchor_match"
|
|
41
|
+
: anchorHits.length + "/" + taskTokens.length + " task tokens matched";
|
|
42
|
+
return {
|
|
43
|
+
anchorHits, anchorMisses, anchorHitCount: anchorHits.length, anchorMissCount: anchorMisses.length,
|
|
44
|
+
anchorRatio: ratio, criticalAnchorHits, criticalAnchorMisses,
|
|
45
|
+
hasTaskAnchorCoverage: anchorHits.length > 0, coverageReason: reason
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function attachTaskAnchorCoverage(units, profile) {
|
|
49
|
+
if (!profile)
|
|
50
|
+
return units;
|
|
51
|
+
const taskText = profile.normalizedTask || profile.rawTask || "";
|
|
52
|
+
return units.map(u => ({ ...u, taskAnchorCoverage: computeTaskAnchorCoverage(taskText, u.matchedTerms) }));
|
|
53
|
+
}
|
|
54
|
+
function applySupportingContextRoles(units, profile) {
|
|
55
|
+
if (!profile)
|
|
56
|
+
return units;
|
|
57
|
+
const routeTargeted = /\b(route|routing|router|url|endpoint)\b/.test(profile.normalizedTask);
|
|
58
|
+
const uiTargeted = hasTaskUiTarget(profile);
|
|
59
|
+
return units.map((unit) => {
|
|
60
|
+
if (unit.role !== "edit_candidate")
|
|
61
|
+
return unit;
|
|
62
|
+
const path = normalizePath(unit.relativePath);
|
|
63
|
+
const isRouteContext = !routeTargeted && unit.slot === "entry" && /(^|\/)routes?\//.test(path);
|
|
64
|
+
const isTemplateContext = !uiTargeted && unit.slot === "ui" && isTemplateOnlyPath(path);
|
|
65
|
+
if (!isRouteContext && !isTemplateContext)
|
|
66
|
+
return unit;
|
|
67
|
+
return { ...unit, role: "read_context", reasons: [...unit.reasons, "supporting context file"] };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
7
70
|
const rolePriority = {
|
|
8
71
|
edit_candidate: 5,
|
|
9
72
|
read_context: 4,
|
|
@@ -18,12 +81,20 @@ function rankEvidence(units, config, profile, rankOptions) {
|
|
|
18
81
|
for (const unit of deduped) {
|
|
19
82
|
fileCounts.set(unit.relativePath, (fileCounts.get(unit.relativePath) ?? 0) + 1);
|
|
20
83
|
}
|
|
21
|
-
const
|
|
84
|
+
const pool = deduped.slice(0, Math.min(80, deduped.length));
|
|
85
|
+
const genericTerms = computeGenericTerms(pool);
|
|
86
|
+
const boosted = deduped.map((unit) => boostUnit(unit, fileCounts, config, profile, rankOptions?.editorContext, genericTerms));
|
|
22
87
|
const sorted = boosted.sort(compareUnits);
|
|
23
88
|
if (!profile) {
|
|
24
|
-
|
|
89
|
+
const selected = selectDiverseUnits(sorted, config.pipeline.maxCandidates);
|
|
90
|
+
return attachTaskAnchorCoverage(applySupportingContextRoles(applyAvoidFilter(selected, profile), profile), profile);
|
|
91
|
+
}
|
|
92
|
+
const selected = selectWithBlueprintCoverage(sorted, profile, config.pipeline.maxCandidates);
|
|
93
|
+
const guardResult = applyAvoidAwarePromotion(selected, sorted, profile);
|
|
94
|
+
if (guardResult.units.length > 0) {
|
|
95
|
+
guardResult.units[0].__taskAvoidGuardStats = guardResult.stats;
|
|
25
96
|
}
|
|
26
|
-
return
|
|
97
|
+
return attachTaskAnchorCoverage(applySupportingContextRoles(applyAvoidFilter(guardResult.units, profile), profile), profile);
|
|
27
98
|
}
|
|
28
99
|
function dedupeUnits(units) {
|
|
29
100
|
const byId = new Map();
|
|
@@ -92,7 +163,7 @@ function selectWithBlueprintCoverage(sorted, profile, maxCandidates) {
|
|
|
92
163
|
}
|
|
93
164
|
pushFrameworkCompanions(sorted, state);
|
|
94
165
|
for (const slot of profile.blueprint.requiredSlots) {
|
|
95
|
-
pushUnit(state, bestUnitForSlot(sorted, slot, state), {
|
|
166
|
+
pushUnit(state, bestUnitForSlot(sorted, slot, state), { allowSecondFileUnitEarly: true });
|
|
96
167
|
}
|
|
97
168
|
pushEntityCompanions(sorted, state, profile);
|
|
98
169
|
for (const slot of (0, contextBlueprint_1.allContextSlots)(profile.blueprint)) {
|
|
@@ -110,7 +181,458 @@ function selectWithBlueprintCoverage(sorted, profile, maxCandidates) {
|
|
|
110
181
|
for (const unit of sorted) {
|
|
111
182
|
pushUnit(state, unit);
|
|
112
183
|
}
|
|
113
|
-
|
|
184
|
+
const result = state.selected.slice(0, maxCandidates);
|
|
185
|
+
return rebalanceSlots(result, sorted, profile, 10);
|
|
186
|
+
}
|
|
187
|
+
function rebalanceSlots(selected, sorted, profile, topK) {
|
|
188
|
+
if (selected.length < topK + 4)
|
|
189
|
+
return selected;
|
|
190
|
+
const topSlots = new Set(selected.slice(0, topK).map((u) => u.slot));
|
|
191
|
+
const missingSlots = profile.blueprint.requiredSlots.filter((s) => !topSlots.has(s));
|
|
192
|
+
const result = [...selected];
|
|
193
|
+
const usedIds = new Set(result.map((u) => u.id));
|
|
194
|
+
const usedFiles = new Set(result.slice(0, topK).map((u) => u.relativePath));
|
|
195
|
+
for (const slot of missingSlots) {
|
|
196
|
+
const candidate = sorted.find((u) => u.slot === slot && !usedIds.has(u.id) && !usedFiles.has(u.relativePath) && !u.contrastContext);
|
|
197
|
+
if (!candidate)
|
|
198
|
+
continue;
|
|
199
|
+
const slotCounts = new Map();
|
|
200
|
+
for (let i = 0; i < topK; i++)
|
|
201
|
+
slotCounts.set(result[i].slot ?? "unknown", (slotCounts.get(result[i].slot ?? "unknown") ?? 0) + 1);
|
|
202
|
+
let swapIdx = -1;
|
|
203
|
+
let worstScore = Infinity;
|
|
204
|
+
for (let i = topK - 1; i >= 0; i--) {
|
|
205
|
+
const s = result[i].slot ?? "unknown";
|
|
206
|
+
if ((slotCounts.get(s) ?? 0) > 1 && !profile.blueprint.requiredSlots.includes(s) && result[i].score < worstScore) {
|
|
207
|
+
worstScore = result[i].score;
|
|
208
|
+
swapIdx = i;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (swapIdx < 0) {
|
|
212
|
+
for (let i = topK - 1; i >= 0; i--) {
|
|
213
|
+
if (!profile.blueprint.requiredSlots.includes(result[i].slot ?? "unknown") && result[i].score < worstScore) {
|
|
214
|
+
worstScore = result[i].score;
|
|
215
|
+
swapIdx = i;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (swapIdx >= 0) {
|
|
220
|
+
usedIds.add(candidate.id);
|
|
221
|
+
usedFiles.add(candidate.relativePath);
|
|
222
|
+
result[swapIdx] = withSelectionReason(candidate, `slot rebalance: filling missing ${slot} slot`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Phase 2: targeted semantic promotion — at most 1 per case, positions 8-10 only.
|
|
226
|
+
// Promotes a semantic-supported candidate from 11-40 into the lower top10
|
|
227
|
+
// if it fills a missing required slot or replaces an avoid-like / non-required file.
|
|
228
|
+
const hasSemanticActive = sorted.some((u) => u.retrievalPasses.includes("semantic_vector"));
|
|
229
|
+
if (hasSemanticActive) {
|
|
230
|
+
for (const candidate of sorted.slice(10, Math.min(sorted.length, 40))) {
|
|
231
|
+
if (usedFiles.has(candidate.relativePath) || candidate.contrastContext)
|
|
232
|
+
continue;
|
|
233
|
+
if (!candidate.retrievalPasses.includes("semantic_vector"))
|
|
234
|
+
continue;
|
|
235
|
+
// Must have at least one other channel (lexical, path, symbol, graph)
|
|
236
|
+
const hasOther = candidate.matchedTerms.length > 0 || (candidate.symbolMatches?.length ?? 0) > 0 || (candidate.graphReasons?.length ?? 0) > 0;
|
|
237
|
+
if (!hasOther)
|
|
238
|
+
continue;
|
|
239
|
+
const candSlot = candidate.slot ?? "unknown";
|
|
240
|
+
const fillsMissing = missingSlots.includes(candSlot);
|
|
241
|
+
let swapIdx = -1;
|
|
242
|
+
let swapReason = "";
|
|
243
|
+
// Priority: replace avoid-like file in bottom 3 positions (8-10)
|
|
244
|
+
for (let i = 7; i < Math.min(topK, 10); i++) {
|
|
245
|
+
if (isAvoidLikeFile(result[i], profile) && !fillsMissing) {
|
|
246
|
+
const s = result[i].slot ?? "unknown";
|
|
247
|
+
if (profile.blueprint.requiredSlots.includes(s) && (result.slice(0, topK).filter((u) => (u.slot ?? "unknown") === s).length <= 1))
|
|
248
|
+
continue;
|
|
249
|
+
swapIdx = i;
|
|
250
|
+
swapReason = `semantic promotion: replaces avoid-like at pos ${i + 1}`;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// If fills missing slot, replace overrepresented non-required slot file in 8-10
|
|
255
|
+
if (swapIdx < 0 && fillsMissing) {
|
|
256
|
+
const slotCounts = new Map();
|
|
257
|
+
for (let i = 0; i < topK; i++)
|
|
258
|
+
slotCounts.set(result[i].slot ?? "unknown", (slotCounts.get(result[i].slot ?? "unknown") ?? 0) + 1);
|
|
259
|
+
for (let i = 9; i >= 7; i--) {
|
|
260
|
+
const s = result[i].slot ?? "unknown";
|
|
261
|
+
if (!profile.blueprint.requiredSlots.includes(s) && (slotCounts.get(s) ?? 0) > 1) {
|
|
262
|
+
swapIdx = i;
|
|
263
|
+
swapReason = `semantic promotion: fills missing slot ${candSlot} at pos ${i + 1}`;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (swapIdx >= 0) {
|
|
269
|
+
usedFiles.add(candidate.relativePath);
|
|
270
|
+
result[swapIdx] = withSelectionReason(candidate, swapReason);
|
|
271
|
+
if (fillsMissing)
|
|
272
|
+
missingSlots.splice(missingSlots.indexOf(candSlot), 1);
|
|
273
|
+
break; // Max 1 promotion per case
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Phase 3: deterministic lexical rescue — at most 1 per case, ranks 11→20 only.
|
|
278
|
+
// Promotes a candidate with 2+ independent evidence signals to position 8-10
|
|
279
|
+
// if it is not avoid-like and not contrast. Keeps top 1-7 unchanged.
|
|
280
|
+
const rescueWindow = sorted.slice(10, Math.min(sorted.length, 20));
|
|
281
|
+
let rescueFired = false;
|
|
282
|
+
for (const candidate of rescueWindow) {
|
|
283
|
+
if (rescueFired)
|
|
284
|
+
break;
|
|
285
|
+
if (usedFiles.has(candidate.relativePath) || usedIds.has(candidate.id))
|
|
286
|
+
continue;
|
|
287
|
+
if (candidate.contrastContext)
|
|
288
|
+
continue;
|
|
289
|
+
if (isAvoidLikeFile(candidate, profile))
|
|
290
|
+
continue;
|
|
291
|
+
const signals = countDeterministicSignals(candidate, profile);
|
|
292
|
+
if (signals < 2)
|
|
293
|
+
continue;
|
|
294
|
+
// Candidate qualified — mark as candidate
|
|
295
|
+
candidate.rescueStatus = "candidate";
|
|
296
|
+
// Find swap target in positions 8-10 (indices 7-9)
|
|
297
|
+
let swapIdx = -1;
|
|
298
|
+
let swapReason = "";
|
|
299
|
+
// Priority 1: replace avoid-like file in 8-10
|
|
300
|
+
for (let i = 7; i < Math.min(topK, 10); i++) {
|
|
301
|
+
if (isAvoidLikeFile(result[i], profile)) {
|
|
302
|
+
const s = result[i].slot ?? "unknown";
|
|
303
|
+
if (profile.blueprint.requiredSlots.includes(s) && (result.slice(0, topK).filter((u) => (u.slot ?? "unknown") === s).length <= 1))
|
|
304
|
+
continue;
|
|
305
|
+
swapIdx = i;
|
|
306
|
+
swapReason = `lexical rescue: replaces avoid-like ${result[i].relativePath.split("/").pop()} at pos ${i + 1}`;
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Priority 2: replace overrepresented non-required slot file in 8-10
|
|
311
|
+
if (swapIdx < 0) {
|
|
312
|
+
const slotCounts = new Map();
|
|
313
|
+
for (let i = 0; i < topK; i++)
|
|
314
|
+
slotCounts.set(result[i].slot ?? "unknown", (slotCounts.get(result[i].slot ?? "unknown") ?? 0) + 1);
|
|
315
|
+
for (let i = 9; i >= 7; i--) {
|
|
316
|
+
const s = result[i].slot ?? "unknown";
|
|
317
|
+
if (!profile.blueprint.requiredSlots.includes(s) && (slotCounts.get(s) ?? 0) > 1) {
|
|
318
|
+
swapIdx = i;
|
|
319
|
+
swapReason = `lexical rescue: replaces overrepresented ${s} at pos ${i + 1}`;
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Priority 3: replace lowest-scoring non-required file in 8-10
|
|
325
|
+
if (swapIdx < 0) {
|
|
326
|
+
let worstScore = Infinity;
|
|
327
|
+
for (let i = 9; i >= 7; i--) {
|
|
328
|
+
const s = result[i].slot ?? "unknown";
|
|
329
|
+
if (!profile.blueprint.requiredSlots.includes(s) && result[i].score < worstScore) {
|
|
330
|
+
worstScore = result[i].score;
|
|
331
|
+
swapIdx = i;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (swapIdx >= 0) {
|
|
335
|
+
swapReason = `lexical rescue: replaces lowest-scoring non-required ${result[swapIdx].relativePath.split("/").pop()} at pos ${swapIdx + 1}`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (swapIdx >= 0) {
|
|
339
|
+
candidate.rescueStatus = "applied";
|
|
340
|
+
usedFiles.add(candidate.relativePath);
|
|
341
|
+
usedIds.add(candidate.id);
|
|
342
|
+
result[swapIdx] = withSelectionReason(candidate, swapReason);
|
|
343
|
+
rescueFired = true;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
candidate.rescueStatus = "blocked";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
function countDeterministicSignals(unit, profile) {
|
|
352
|
+
let count = 0;
|
|
353
|
+
if (unit.matchedTerms.length > 0)
|
|
354
|
+
count++;
|
|
355
|
+
if ((unit.symbolMatches?.length ?? 0) > 0)
|
|
356
|
+
count++;
|
|
357
|
+
if ((unit.graphReasons?.length ?? 0) > 0)
|
|
358
|
+
count++;
|
|
359
|
+
if (hasStrongTaskMatch(unit, profile))
|
|
360
|
+
count++;
|
|
361
|
+
if (filenamePhraseMatchBonus(unit, profile) > 0)
|
|
362
|
+
count++;
|
|
363
|
+
// Slot fills a missing required slot
|
|
364
|
+
if (unit.slot && profile.blueprint.requiredSlots.includes(unit.slot))
|
|
365
|
+
count++;
|
|
366
|
+
return count;
|
|
367
|
+
}
|
|
368
|
+
function groupEvidenceByFile(sorted) {
|
|
369
|
+
const byFile = new Map();
|
|
370
|
+
for (const unit of sorted) {
|
|
371
|
+
const list = byFile.get(unit.relativePath) ?? [];
|
|
372
|
+
list.push(unit);
|
|
373
|
+
byFile.set(unit.relativePath, list);
|
|
374
|
+
}
|
|
375
|
+
const files = [];
|
|
376
|
+
for (const [path, units] of byFile) {
|
|
377
|
+
const best = units[0]; // Already sorted by score
|
|
378
|
+
files.push({
|
|
379
|
+
relativePath: path,
|
|
380
|
+
bestUnit: best,
|
|
381
|
+
bestScore: best.score,
|
|
382
|
+
slots: new Set(units.map((u) => u.slot ?? "unknown")),
|
|
383
|
+
roles: [...new Set(units.map((u) => u.role))],
|
|
384
|
+
subsystem: best.subsystem,
|
|
385
|
+
hasSemantic: units.some((u) => u.retrievalPasses.includes("semantic_vector")),
|
|
386
|
+
hasGraph: (best.graphReasons?.length ?? 0) > 0,
|
|
387
|
+
hasSymbol: (best.symbolMatches?.length ?? 0) > 0,
|
|
388
|
+
avoidRisk: false, // Will be set below
|
|
389
|
+
isContrast: best.contrastContext === true
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return files;
|
|
393
|
+
}
|
|
394
|
+
function selectTopFiles(fileGroups, profile, topK) {
|
|
395
|
+
const selected = [];
|
|
396
|
+
const seen = new Set();
|
|
397
|
+
const primarySubsystem = inferFileSubsystem(fileGroups, profile);
|
|
398
|
+
const add = (f) => {
|
|
399
|
+
if (selected.length >= topK || seen.has(f.relativePath))
|
|
400
|
+
return false;
|
|
401
|
+
seen.add(f.relativePath);
|
|
402
|
+
selected.push(f);
|
|
403
|
+
return true;
|
|
404
|
+
};
|
|
405
|
+
// Step 1: Keep top core candidate (non-contrast, non-test, high confidence)
|
|
406
|
+
const core = fileGroups.find((f) => !f.isContrast && f.roles.includes("edit_candidate") && !f.relativePath.match(/(^|\/)(tests?|specs?|__tests__)(\/|$)/));
|
|
407
|
+
if (core) {
|
|
408
|
+
add(core);
|
|
409
|
+
core.bestUnit = withSelectionReason(core.bestUnit, "selected: core file");
|
|
410
|
+
}
|
|
411
|
+
// Step 2: Fill required slots with unique files
|
|
412
|
+
for (const slot of orderedRequiredSlots(profile)) {
|
|
413
|
+
if (selected.length >= topK)
|
|
414
|
+
break;
|
|
415
|
+
const best = fileGroups
|
|
416
|
+
.filter((f) => !seen.has(f.relativePath) && f.slots.has(slot) && !f.isContrast)
|
|
417
|
+
.sort((a, b) => fileSlotScore(b, slot, profile, primarySubsystem) - fileSlotScore(a, slot, profile, primarySubsystem))[0];
|
|
418
|
+
if (best) {
|
|
419
|
+
add(best);
|
|
420
|
+
best.bestUnit = withSelectionReason(best.bestUnit, `selected: fills missing required slot ${slot}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// Step 3: Graph-aware expansion — same directory/subsystem as core
|
|
424
|
+
if (selected.length > 0 && selected.length < topK) {
|
|
425
|
+
const corePath = selected[0].relativePath;
|
|
426
|
+
const coreDir = corePath.replace(/\\/g, "/").toLowerCase().split("/").slice(0, -1).join("/");
|
|
427
|
+
for (const f of fileGroups) {
|
|
428
|
+
if (selected.length >= topK)
|
|
429
|
+
break;
|
|
430
|
+
if (seen.has(f.relativePath) || f.isContrast)
|
|
431
|
+
continue;
|
|
432
|
+
const fDir = f.relativePath.replace(/\\/g, "/").toLowerCase().split("/").slice(0, -1).join("/");
|
|
433
|
+
if (fDir === coreDir || (f.subsystem && f.subsystem === selected[0].subsystem)) {
|
|
434
|
+
add(f);
|
|
435
|
+
f.bestUnit = withSelectionReason(f.bestUnit, `selected: graph-connected to ${corePath.split("/").pop()}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Step 4: Fill remaining by file score
|
|
440
|
+
const remaining = fileGroups
|
|
441
|
+
.filter((f) => !seen.has(f.relativePath) && !f.isContrast)
|
|
442
|
+
.sort((a, b) => b.bestScore - a.bestScore);
|
|
443
|
+
for (const f of remaining) {
|
|
444
|
+
if (selected.length >= topK)
|
|
445
|
+
break;
|
|
446
|
+
add(f);
|
|
447
|
+
}
|
|
448
|
+
// Step 5: If still not enough, allow contrast files
|
|
449
|
+
if (selected.length < topK) {
|
|
450
|
+
for (const f of fileGroups) {
|
|
451
|
+
if (selected.length >= topK)
|
|
452
|
+
break;
|
|
453
|
+
if (!seen.has(f.relativePath))
|
|
454
|
+
add(f);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return selected;
|
|
458
|
+
}
|
|
459
|
+
function fileSlotScore(f, slot, profile, primarySubsystem) {
|
|
460
|
+
let s = f.bestScore;
|
|
461
|
+
if (hasFileStrongTaskMatch(f, profile))
|
|
462
|
+
s += 50;
|
|
463
|
+
if (primarySubsystem && f.subsystem === primarySubsystem)
|
|
464
|
+
s += 20;
|
|
465
|
+
if (f.roles.includes("edit_candidate"))
|
|
466
|
+
s += 15;
|
|
467
|
+
if (f.hasSymbol)
|
|
468
|
+
s += 8;
|
|
469
|
+
if (f.hasGraph)
|
|
470
|
+
s += 6;
|
|
471
|
+
if (f.hasSemantic)
|
|
472
|
+
s += 4;
|
|
473
|
+
return s;
|
|
474
|
+
}
|
|
475
|
+
function hasFileStrongTaskMatch(f, profile) {
|
|
476
|
+
const path = normalizePath(f.relativePath);
|
|
477
|
+
return [...profile.literalTerms, ...profile.keywords]
|
|
478
|
+
.map((term) => (0, text_1.normalizeText)(term).replace(/[^a-z0-9]+/g, ""))
|
|
479
|
+
.some((term) => term.length >= 5 && path.replace(/[^a-z0-9]+/g, "").includes(term));
|
|
480
|
+
}
|
|
481
|
+
function inferFileSubsystem(fileGroups, profile) {
|
|
482
|
+
const counts = new Map();
|
|
483
|
+
for (const f of fileGroups.slice(0, 30)) {
|
|
484
|
+
if (!f.subsystem || f.isContrast)
|
|
485
|
+
continue;
|
|
486
|
+
const w = 1 + (hasFileStrongTaskMatch(f, profile) ? 3 : 0);
|
|
487
|
+
counts.set(f.subsystem, (counts.get(f.subsystem) ?? 0) + w);
|
|
488
|
+
}
|
|
489
|
+
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0];
|
|
490
|
+
}
|
|
491
|
+
// Slot-constrained top10: ensure every required slot has at least one representative
|
|
492
|
+
function enforceSlotCoverage(selected, sorted, profile, topK) {
|
|
493
|
+
if (selected.length < topK + 4)
|
|
494
|
+
return selected;
|
|
495
|
+
const topSlots = new Set(selected.slice(0, topK).map((u) => u.slot));
|
|
496
|
+
const missingSlots = profile.blueprint.requiredSlots.filter((s) => !topSlots.has(s));
|
|
497
|
+
if (missingSlots.length === 0)
|
|
498
|
+
return selected;
|
|
499
|
+
const result = [...selected];
|
|
500
|
+
const usedIds = new Set(result.map((u) => u.id));
|
|
501
|
+
const usedFiles = new Set(result.slice(0, topK).map((u) => u.relativePath));
|
|
502
|
+
for (const slot of missingSlots) {
|
|
503
|
+
// Find best candidate for this slot
|
|
504
|
+
const candidate = sorted.find((u) => u.slot === slot &&
|
|
505
|
+
!usedIds.has(u.id) &&
|
|
506
|
+
!usedFiles.has(u.relativePath) &&
|
|
507
|
+
!u.contrastContext);
|
|
508
|
+
if (!candidate)
|
|
509
|
+
continue;
|
|
510
|
+
// Find weakest non-required-slot unit to swap
|
|
511
|
+
const slotCounts = new Map();
|
|
512
|
+
for (let i = 0; i < topK; i++) {
|
|
513
|
+
slotCounts.set(result[i].slot ?? "unknown", (slotCounts.get(result[i].slot ?? "unknown") ?? 0) + 1);
|
|
514
|
+
}
|
|
515
|
+
let swapIdx = -1;
|
|
516
|
+
let worstScore = Infinity;
|
|
517
|
+
// Prefer swapping overrepresented non-required slots
|
|
518
|
+
for (let i = topK - 1; i >= 0; i--) {
|
|
519
|
+
const s = result[i].slot ?? "unknown";
|
|
520
|
+
const overCount = slotCounts.get(s) ?? 0;
|
|
521
|
+
if (overCount > 1 && !profile.blueprint.requiredSlots.includes(s) && result[i].score < worstScore) {
|
|
522
|
+
worstScore = result[i].score;
|
|
523
|
+
swapIdx = i;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
// Fallback: lowest-scoring non-required
|
|
527
|
+
if (swapIdx < 0) {
|
|
528
|
+
for (let i = topK - 1; i >= 0; i--) {
|
|
529
|
+
const s = result[i].slot ?? "unknown";
|
|
530
|
+
if (!profile.blueprint.requiredSlots.includes(s) && result[i].score < worstScore) {
|
|
531
|
+
worstScore = result[i].score;
|
|
532
|
+
swapIdx = i;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
if (swapIdx >= 0) {
|
|
537
|
+
usedIds.add(candidate.id);
|
|
538
|
+
usedFiles.add(candidate.relativePath);
|
|
539
|
+
result[swapIdx] = withSelectionReason(candidate, `slot rebalance: filling missing ${slot} slot`);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
function bestSlotCandidate(sorted, slot, profile, state) {
|
|
545
|
+
const primarySubsystem = inferSelectionSubsystem(sorted.slice(0, 30), profile);
|
|
546
|
+
return sorted
|
|
547
|
+
.filter((u) => u.slot === slot && !state.seenIds.has(u.id))
|
|
548
|
+
.filter((u) => (state.fileCounts.get(u.relativePath) ?? 0) === 0)
|
|
549
|
+
.filter((u) => !u.contrastContext)
|
|
550
|
+
.sort((a, b) => slotCandidateScore(b, slot, profile, primarySubsystem) - slotCandidateScore(a, slot, profile, primarySubsystem))[0];
|
|
551
|
+
}
|
|
552
|
+
function slotCandidateScore(unit, slot, profile, primarySubsystem) {
|
|
553
|
+
let s = unit.score;
|
|
554
|
+
if (hasStrongTaskMatch(unit, profile))
|
|
555
|
+
s += 50;
|
|
556
|
+
if (primarySubsystem && unit.subsystem === primarySubsystem)
|
|
557
|
+
s += 20;
|
|
558
|
+
if (unit.slot === slot)
|
|
559
|
+
s += 30;
|
|
560
|
+
if (unit.role === "edit_candidate")
|
|
561
|
+
s += 15;
|
|
562
|
+
if (unit.contrastContext)
|
|
563
|
+
s -= 100;
|
|
564
|
+
return s;
|
|
565
|
+
}
|
|
566
|
+
function findGraphConnectedCandidates(sorted, corePath, profile, maxResults) {
|
|
567
|
+
const coreBase = corePath.replace(/\\/g, "/").split("/").pop()?.replace(/\.[^.]+$/, "").toLowerCase() ?? "";
|
|
568
|
+
const coreDir = corePath.replace(/\\/g, "/").toLowerCase().split("/").slice(0, -1).join("/");
|
|
569
|
+
const results = [];
|
|
570
|
+
for (const unit of sorted) {
|
|
571
|
+
if (results.length >= maxResults)
|
|
572
|
+
break;
|
|
573
|
+
const up = normalizePath(unit.relativePath);
|
|
574
|
+
const uDir = up.split("/").slice(0, -1).join("/");
|
|
575
|
+
// Same directory sibling
|
|
576
|
+
if (uDir === coreDir && up !== normalizePath(corePath)) {
|
|
577
|
+
results.push(unit);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
// Same subsystem
|
|
581
|
+
if (unit.subsystem && unit.subsystem === sorted.find((u) => normalizePath(u.relativePath) === normalizePath(corePath))?.subsystem) {
|
|
582
|
+
results.push(unit);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return results;
|
|
587
|
+
}
|
|
588
|
+
function withSelectionReason(unit, reason) {
|
|
589
|
+
return {
|
|
590
|
+
...unit,
|
|
591
|
+
reasons: [...unit.reasons.filter((r) => !r.startsWith("selected:") && !r.startsWith("demoted:")), reason]
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
// Path-role classifier: maps file path to semantic role
|
|
595
|
+
function classifyPathRole(relativePath) {
|
|
596
|
+
const p = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
597
|
+
if (/(^|\/)(routes?|router)(\/|$)|urlpatterns|urls\.py$|web\.php$|api\.php$|\+page\.|\+server\.|page\.(tsx|jsx|svelte)$/.test(p))
|
|
598
|
+
return "entry";
|
|
599
|
+
if (/(^|\/)(controllers?|handlers?)(\/|$)/.test(p))
|
|
600
|
+
return "controller";
|
|
601
|
+
if (/(^|\/)(services?|usecases?|domain)(\/|$)/.test(p))
|
|
602
|
+
return "service";
|
|
603
|
+
if (/(^|\/)(repositories?|dao)(\/|$)/.test(p))
|
|
604
|
+
return "repository";
|
|
605
|
+
if (/(^|\/)(models?|entities?)(\/|$)|schema\.(ts|js|prisma)$/.test(p))
|
|
606
|
+
return "model";
|
|
607
|
+
if (/(^|\/)(migrations?|schema)(\/|$)|\.sql$|\.prisma$/.test(p))
|
|
608
|
+
return "schema";
|
|
609
|
+
if (/(^|\/)(config|configs?|settings?|environments?)(\/|$)|\.(env|toml|ya?ml|ini|json|properties)$/.test(p))
|
|
610
|
+
return "config";
|
|
611
|
+
if (/(^|\/)(jobs?|workers?)(\/|$)/.test(p))
|
|
612
|
+
return "job";
|
|
613
|
+
if (/(^|\/)(listeners?|events?)(\/|$)/.test(p))
|
|
614
|
+
return "event";
|
|
615
|
+
if (/(^|\/)(notifications?|mail|mailers?)(\/|$)/.test(p))
|
|
616
|
+
return "notification";
|
|
617
|
+
if (/(^|\/)(mail|email|smtp)(\/|$)/.test(p))
|
|
618
|
+
return "mail";
|
|
619
|
+
if (/(^|\/)(queues?)(\/|$)/.test(p))
|
|
620
|
+
return "queue";
|
|
621
|
+
if (/(^|\/)(components?|views?|templates?|pages?)(\/|$)|\.(svelte|vue|tsx|jsx|blade\.php|twig|erb)$/.test(p))
|
|
622
|
+
return "ui";
|
|
623
|
+
if (/(^|\/)(templates?|views?|resources\/views)(\/|$)/.test(p))
|
|
624
|
+
return "template";
|
|
625
|
+
if (/(^|\/)(policies?|permissions?|guards?|middleware|auth)(\/|$)/.test(p))
|
|
626
|
+
return "auth";
|
|
627
|
+
if (/(^|\/)(admin|dashboard)(\/|$)/.test(p))
|
|
628
|
+
return "admin";
|
|
629
|
+
if (/(^|\/)(payment|billing|invoice|checkout)(\/|$)/.test(p))
|
|
630
|
+
return "payment";
|
|
631
|
+
if (/(^|\/)(tests?|specs?|__tests__)(\/|$)|\.(test|spec)\./.test(p))
|
|
632
|
+
return "test";
|
|
633
|
+
if (/(^|\/)(helpers?|utils?|lib|common|shared)(\/|$)/.test(p))
|
|
634
|
+
return "utility";
|
|
635
|
+
return "other";
|
|
114
636
|
}
|
|
115
637
|
function buildConstrainedTopK(sorted, profile, limit) {
|
|
116
638
|
const pool = sorted.slice(0, Math.min(80, sorted.length));
|
|
@@ -177,6 +699,12 @@ function constrainedCandidateScore(unit, profile, primarySubsystem) {
|
|
|
177
699
|
if (primarySubsystem && unit.subsystem === primarySubsystem) {
|
|
178
700
|
score += 24;
|
|
179
701
|
}
|
|
702
|
+
else if (primarySubsystem && unit.subsystem && unit.subsystem !== primarySubsystem) {
|
|
703
|
+
const coversRequiredSlot = unit.slot && profile.blueprint.requiredSlots.includes(unit.slot);
|
|
704
|
+
if (!coversRequiredSlot && !hasStrongTaskMatch(unit, profile)) {
|
|
705
|
+
score -= 80;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
180
708
|
if (unit.contrastContext) {
|
|
181
709
|
score -= 80;
|
|
182
710
|
}
|
|
@@ -184,11 +712,22 @@ function constrainedCandidateScore(unit, profile, primarySubsystem) {
|
|
|
184
712
|
score -= 50;
|
|
185
713
|
}
|
|
186
714
|
if (unit.slot === "entry" || unit.slot === "handler") {
|
|
187
|
-
score +=
|
|
715
|
+
score += 28;
|
|
188
716
|
}
|
|
189
717
|
if (unit.slot === "validation" || unit.slot === "ui") {
|
|
190
718
|
score += 12;
|
|
191
719
|
}
|
|
720
|
+
// Intent-aware keyword boosts based on raw task text
|
|
721
|
+
var pathLower = normalizePath(unit.relativePath).toLowerCase();
|
|
722
|
+
var taskLower = (profile.rawTask || "").toLowerCase();
|
|
723
|
+
if (profile.intent === "bugfix" && (/controller|handler|service|repository/i.test(pathLower)))
|
|
724
|
+
score += 8;
|
|
725
|
+
if (/export|import|csv|excel|pdf/i.test(taskLower) && (/export|import|excel|pdf|csv/i.test(pathLower)))
|
|
726
|
+
score += 10;
|
|
727
|
+
if (/auth|login|permission|middleware|policy/i.test(taskLower) && (/auth|middleware|policy|guard|permission/i.test(pathLower)))
|
|
728
|
+
score += 8;
|
|
729
|
+
if (/view|blade|template|component|frontend/i.test(taskLower) && (/view|blade|template|component/i.test(pathLower)))
|
|
730
|
+
score += 8;
|
|
192
731
|
return score;
|
|
193
732
|
}
|
|
194
733
|
function isAllowedInConstrainedTop(unit, profile, primarySubsystem) {
|
|
@@ -246,7 +785,7 @@ function pushUnit(state, unit, options = {}) {
|
|
|
246
785
|
}
|
|
247
786
|
const currentFileCount = state.fileCounts.get(unit.relativePath) ?? 0;
|
|
248
787
|
const earlyLimit = options.allowSecondFileUnitEarly ? 2 : 1;
|
|
249
|
-
const fileLimit = state.selected.length < 12 ? earlyLimit :
|
|
788
|
+
const fileLimit = state.selected.length < 12 ? earlyLimit : 2;
|
|
250
789
|
if (currentFileCount >= fileLimit) {
|
|
251
790
|
return false;
|
|
252
791
|
}
|
|
@@ -290,7 +829,7 @@ function pushEntityCompanions(sorted, state, profile) {
|
|
|
290
829
|
for (const entity of activeEntities) {
|
|
291
830
|
for (const slot of slotsToCover) {
|
|
292
831
|
const companion = bestEntityCompanion(sorted, entity, slot, state);
|
|
293
|
-
if (pushUnit(state, companion ? withEntityCompanionReason(companion, entity, slot) : undefined, {
|
|
832
|
+
if (pushUnit(state, companion ? withEntityCompanionReason(companion, entity, slot) : undefined, { allowSecondFileUnitEarly: true })) {
|
|
294
833
|
added += 1;
|
|
295
834
|
}
|
|
296
835
|
if (added >= 3 || state.selected.length >= Math.floor(state.maxCandidates * 0.5)) {
|
|
@@ -392,25 +931,83 @@ function withEntityCompanionReason(unit, entity, slot) {
|
|
|
392
931
|
reasons: [...unit.reasons, `entity companion for ${entity} ${slot} context`]
|
|
393
932
|
};
|
|
394
933
|
}
|
|
395
|
-
function
|
|
934
|
+
function computeGenericTerms(pool) {
|
|
935
|
+
if (process.env.FLOWSEEKER_DISABLE_GENERIC_TERMS === "1")
|
|
936
|
+
return new Set();
|
|
937
|
+
const termFileCounts = new Map();
|
|
938
|
+
for (const unit of pool) {
|
|
939
|
+
const base = normalizePath(unit.relativePath).split("/").pop()?.replace(/\.[^.]+$/, "") ?? "";
|
|
940
|
+
const compactBase = base.replace(/[^a-z0-9]+/g, "");
|
|
941
|
+
for (const term of unit.matchedTerms) {
|
|
942
|
+
const compactTerm = (0, text_1.normalizeText)(term).replace(/[^a-z0-9]+/g, "");
|
|
943
|
+
if (compactTerm.length < 5)
|
|
944
|
+
continue;
|
|
945
|
+
if (compactBase.includes(compactTerm)) {
|
|
946
|
+
const files = termFileCounts.get(compactTerm) ?? new Set();
|
|
947
|
+
files.add(unit.relativePath);
|
|
948
|
+
termFileCounts.set(compactTerm, files);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return new Set([...termFileCounts.entries()].filter(([, files]) => files.size > 8).map(([term]) => term));
|
|
953
|
+
}
|
|
954
|
+
function boostUnit(unit, fileCounts, config, profile, editorContext, genericTerms) {
|
|
396
955
|
const sameFileCount = fileCounts.get(unit.relativePath) ?? 1;
|
|
397
956
|
const crossEvidenceBonus = Math.min(4, sameFileCount - 1);
|
|
398
957
|
const multiTermBonus = Math.min(6, unit.matchedTerms.length);
|
|
399
958
|
const slotBonus = profile ? profileSlotBonus(unit, profile) : 0;
|
|
400
959
|
const subsystemBonus = profile ? (0, subsystem_1.subsystemAlignmentScore)(unit, profile) : 0;
|
|
401
|
-
const symbolBonus = profile && unit.symbolMatches?.length
|
|
960
|
+
const symbolBonus = profile && unit.symbolMatches?.length
|
|
961
|
+
? (() => {
|
|
962
|
+
const isTS = unit.extractorKind === "tree-sitter";
|
|
963
|
+
const maxBonus = isTS ? 16 : 3;
|
|
964
|
+
const perMatch = isTS ? 6 : 1;
|
|
965
|
+
const bonus = Math.min(maxBonus, unit.symbolMatches.length * perMatch);
|
|
966
|
+
return bonus;
|
|
967
|
+
})()
|
|
968
|
+
: 0;
|
|
402
969
|
const literalTermBonus = profile ? matchedTaskLiteralBonus(unit, profile) : 0;
|
|
403
|
-
const filenamePhraseBonus = profile ? filenamePhraseMatchBonus(unit, profile) : 0;
|
|
970
|
+
const filenamePhraseBonus = profile ? filenamePhraseMatchBonus(unit, profile, genericTerms) : 0;
|
|
404
971
|
const testPenalty = profile && isUnrequestedTestEvidence(unit, profile) ? 55 : 0;
|
|
405
972
|
const duplicatePenalty = unit.isDuplicate ? 8 : 0;
|
|
406
|
-
const contrastPenalty = unit.contrastContext ?
|
|
973
|
+
const contrastPenalty = unit.contrastContext ? 50 : 0;
|
|
974
|
+
const hasAnyBoost = (literalTermBonus + filenamePhraseBonus + symbolBonus) > 0 || unit.score > 15;
|
|
975
|
+
const lowSignalPenalty = profile && !hasAnyBoost && !unit.contrastContext ? 20 : 0;
|
|
407
976
|
const editorBoost = editorContext ? editorContextBoost(unit, editorContext) : 0;
|
|
408
|
-
|
|
977
|
+
// Graph expansion boost: files added via dependency/graph get a small floor lift
|
|
978
|
+
const graphExpansionBoost = (unit.graphReasons?.length ?? 0) > 0 && unit.score < 15 ? 8 : 0;
|
|
979
|
+
// Controlled semantic boost: only for hosted providers (not deterministicTest)
|
|
980
|
+
const hasSemantic = unit.retrievalPasses.includes("semantic_vector");
|
|
981
|
+
const hasOtherChannel = unit.matchedTerms.length > 0 || (unit.symbolMatches?.length ?? 0) > 0 || (unit.graphReasons?.length ?? 0) > 0;
|
|
982
|
+
const isHostedProvider = config.index.semanticEnabled && config.index.semanticProvider !== "deterministicTest";
|
|
983
|
+
// Gentle boost: +2 for hosted semantic when other channels also support the file
|
|
984
|
+
const semanticBoost = (hasSemantic && hasOtherChannel && isHostedProvider) ? 2 : 0;
|
|
985
|
+
const fusedScore = unit.score;
|
|
986
|
+
const exactBoost = literalTermBonus + filenamePhraseBonus;
|
|
987
|
+
const slotBoost = slotBonus;
|
|
988
|
+
const graphBoost = unit.graphReasons?.length ? Math.min(12, unit.graphReasons.length * 4) : 0;
|
|
989
|
+
const symbolSpecificityBoost = symbolBonus;
|
|
990
|
+
const subsystemSpecificityBoost = subsystemBonus;
|
|
991
|
+
const noisePenalty = testPenalty + contrastPenalty + lowSignalPenalty;
|
|
992
|
+
const duplicationPenalty = duplicatePenalty;
|
|
993
|
+
const score = fusedScore + crossEvidenceBonus + multiTermBonus + slotBoost + subsystemSpecificityBoost + symbolSpecificityBoost + exactBoost + graphBoost + semanticBoost + graphExpansionBoost - noisePenalty - duplicationPenalty + editorBoost;
|
|
994
|
+
const scoreBreakdown = {
|
|
995
|
+
fusedScore,
|
|
996
|
+
exactBoost,
|
|
997
|
+
slotBoost,
|
|
998
|
+
graphBoost,
|
|
999
|
+
symbolBoost: symbolSpecificityBoost,
|
|
1000
|
+
subsystemBoost: subsystemSpecificityBoost,
|
|
1001
|
+
noisePenalty,
|
|
1002
|
+
duplicationPenalty,
|
|
1003
|
+
finalScore: score
|
|
1004
|
+
};
|
|
409
1005
|
const confidence = (0, text_1.clamp)(score / 30, 0.1, 0.98);
|
|
410
1006
|
const role = confidence < config.pipeline.minConfidence && unit.role === "edit_candidate" ? "unknown" : unit.role;
|
|
411
1007
|
return {
|
|
412
1008
|
...unit,
|
|
413
1009
|
score,
|
|
1010
|
+
scoreBreakdown,
|
|
414
1011
|
role,
|
|
415
1012
|
confidence,
|
|
416
1013
|
tier: confidence >= 0.75 ? "high" : confidence >= config.pipeline.minConfidence ? "medium" : "low",
|
|
@@ -435,7 +1032,7 @@ function isUnrequestedTestEvidence(unit, profile) {
|
|
|
435
1032
|
}
|
|
436
1033
|
return !/\b(test|tests|spec|unit test|integration test|e2e|assert|expect|coverage)\b/.test(profile.normalizedTask);
|
|
437
1034
|
}
|
|
438
|
-
function filenamePhraseMatchBonus(unit, profile) {
|
|
1035
|
+
function filenamePhraseMatchBonus(unit, profile, genericTerms) {
|
|
439
1036
|
const normalizedPath = normalizePath(unit.relativePath);
|
|
440
1037
|
const base = normalizedPath.split("/").pop()?.replace(/\.[^.]+$/, "") ?? normalizedPath;
|
|
441
1038
|
const compactBase = base.replace(/[^a-z0-9]+/g, "");
|
|
@@ -446,11 +1043,12 @@ function filenamePhraseMatchBonus(unit, profile) {
|
|
|
446
1043
|
if (compactTerm.length < 5) {
|
|
447
1044
|
continue;
|
|
448
1045
|
}
|
|
1046
|
+
const isGeneric = genericTerms?.has(compactTerm) ?? false;
|
|
449
1047
|
if (compactBase.includes(compactTerm)) {
|
|
450
|
-
best = Math.max(best, 64);
|
|
1048
|
+
best = Math.max(best, isGeneric ? 48 : 64);
|
|
451
1049
|
}
|
|
452
1050
|
else if (compactPath.includes(compactTerm)) {
|
|
453
|
-
best = Math.max(best, 28);
|
|
1051
|
+
best = Math.max(best, isGeneric ? 22 : 28);
|
|
454
1052
|
}
|
|
455
1053
|
}
|
|
456
1054
|
if (/\b(source\s+code|source|src|implementation)\b/.test(profile.normalizedTask) && normalizedPath.startsWith("src/")) {
|
|
@@ -488,6 +1086,12 @@ function profileSlotBonus(unit, profile) {
|
|
|
488
1086
|
let bonus = 0;
|
|
489
1087
|
if (profile.blueprint.requiredSlots.includes(slot)) {
|
|
490
1088
|
bonus += 14;
|
|
1089
|
+
if (slot === "entry" || slot === "handler") {
|
|
1090
|
+
bonus += 10;
|
|
1091
|
+
}
|
|
1092
|
+
else if (slot === "ui" || slot === "validation") {
|
|
1093
|
+
bonus += 6;
|
|
1094
|
+
}
|
|
491
1095
|
}
|
|
492
1096
|
else if (profile.blueprint.optionalSlots.includes(slot)) {
|
|
493
1097
|
bonus += 5;
|
|
@@ -560,4 +1164,259 @@ function editorContextBoost(unit, ctx) {
|
|
|
560
1164
|
}
|
|
561
1165
|
return boost;
|
|
562
1166
|
}
|
|
1167
|
+
function applyAvoidFilter(units, profile) {
|
|
1168
|
+
if (units.length < 12)
|
|
1169
|
+
return units;
|
|
1170
|
+
const top10 = units.slice(0, 10);
|
|
1171
|
+
const rest = units.slice(10);
|
|
1172
|
+
const requiredSlots = profile ? new Set(profile.blueprint.requiredSlots) : new Set();
|
|
1173
|
+
const slotOccupants = new Map();
|
|
1174
|
+
for (const u of top10) {
|
|
1175
|
+
if (u.slot)
|
|
1176
|
+
slotOccupants.set(u.slot, (slotOccupants.get(u.slot) ?? 0) + 1);
|
|
1177
|
+
}
|
|
1178
|
+
// Identify avoid-like files in top 10, but protect sole slot occupants
|
|
1179
|
+
const avoidIndices = [];
|
|
1180
|
+
for (let i = 0; i < top10.length; i++) {
|
|
1181
|
+
if (!isAvoidLikeFile(top10[i], profile))
|
|
1182
|
+
continue;
|
|
1183
|
+
// Don't demote if this is the ONLY file covering a required slot
|
|
1184
|
+
const slot = top10[i].slot;
|
|
1185
|
+
if (slot && requiredSlots.has(slot) && (slotOccupants.get(slot) ?? 0) <= 1)
|
|
1186
|
+
continue;
|
|
1187
|
+
avoidIndices.push(i);
|
|
1188
|
+
}
|
|
1189
|
+
if (avoidIndices.length === 0)
|
|
1190
|
+
return units;
|
|
1191
|
+
// Swap avoid-like files with next-best non-avoid files from rest
|
|
1192
|
+
const result = [...units];
|
|
1193
|
+
let swapFrom = 10;
|
|
1194
|
+
for (const avoidIdx of avoidIndices) {
|
|
1195
|
+
while (swapFrom < result.length) {
|
|
1196
|
+
if (!isAvoidLikeFile(result[swapFrom], profile)) {
|
|
1197
|
+
// Swap
|
|
1198
|
+
const temp = result[avoidIdx];
|
|
1199
|
+
result[avoidIdx] = { ...result[swapFrom], score: temp.score, reasons: [...result[swapFrom].reasons, `promoted over avoid-like file: ${temp.relativePath}`] };
|
|
1200
|
+
result[swapFrom] = { ...temp, contrastContext: true, reasons: [...temp.reasons, `avoid-like pattern demoted`] };
|
|
1201
|
+
swapFrom++;
|
|
1202
|
+
break;
|
|
1203
|
+
}
|
|
1204
|
+
swapFrom++;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return result;
|
|
1208
|
+
}
|
|
1209
|
+
// Task-aware avoid guard: detects domain files with weak task/path token overlap
|
|
1210
|
+
// and wrong subsystem. These files likely entered top10 due to parser symbol evidence
|
|
1211
|
+
// rather than actual task relevance.
|
|
1212
|
+
function isTaskMismatchedDomainFile(unit, profile) {
|
|
1213
|
+
// Must have weak path/token overlap with task
|
|
1214
|
+
if (unit.matchedTerms.length > 1)
|
|
1215
|
+
return false;
|
|
1216
|
+
// Must not have strong task match
|
|
1217
|
+
if (hasStrongTaskMatch(unit, profile))
|
|
1218
|
+
return false;
|
|
1219
|
+
// Must not have explicit literal term match
|
|
1220
|
+
if (profile.literalTerms.length > 0 && unit.matchedTerms.some(t => profile.literalTerms.includes(t)))
|
|
1221
|
+
return false;
|
|
1222
|
+
// Check path tokens against task tokens
|
|
1223
|
+
const normalizedPath = (0, text_1.normalizeText)(unit.relativePath);
|
|
1224
|
+
const compactPath = normalizedPath.replace(/[^a-z0-9]+/g, "");
|
|
1225
|
+
const taskTokenSet = new Set([...profile.keywords.map(text_1.normalizeText), ...profile.concepts.map(text_1.normalizeText), ...profile.actions.map(text_1.normalizeText)]);
|
|
1226
|
+
const pathOverlapCount = [...taskTokenSet].filter(t => t.length >= 4 && compactPath.includes(t)).length;
|
|
1227
|
+
// If path has 2+ task token overlaps, it's not mismatched
|
|
1228
|
+
if (pathOverlapCount >= 2)
|
|
1229
|
+
return false;
|
|
1230
|
+
// If file has no task tokens at all in path AND has 0 matched terms AND is
|
|
1231
|
+
// not expected/required-slot-critical, it's a likely parser-boosted mismatch
|
|
1232
|
+
return pathOverlapCount === 0;
|
|
1233
|
+
}
|
|
1234
|
+
// Avoid-aware promotion: replace avoid/wrong-subsystem files in top 10
|
|
1235
|
+
// with better candidates from positions 11-40 that improve slot coverage or subsystem alignment.
|
|
1236
|
+
function applyAvoidAwarePromotion(units, sorted, profile) {
|
|
1237
|
+
const stats = { scanned: 0, candidates: 0, demoted: 0, promoted: 0, blockedSlot: 0, blockedExpected: 0, noSafeReplacement: 0 };
|
|
1238
|
+
if (!profile || units.length < 14)
|
|
1239
|
+
return { units, stats };
|
|
1240
|
+
const requiredSlots = new Set(profile.blueprint.requiredSlots);
|
|
1241
|
+
if (requiredSlots.size === 0)
|
|
1242
|
+
return { units, stats };
|
|
1243
|
+
const result = [...units];
|
|
1244
|
+
const topFiles = new Set(result.slice(0, 10).map((u) => u.relativePath));
|
|
1245
|
+
const topSlots = new Set();
|
|
1246
|
+
const topSlotOwners = new Map(); // slot → paths
|
|
1247
|
+
for (const u of result.slice(0, 10)) {
|
|
1248
|
+
const s = u.slot ?? "unknown";
|
|
1249
|
+
topSlots.add(s);
|
|
1250
|
+
const owners = topSlotOwners.get(s) ?? [];
|
|
1251
|
+
owners.push(u.relativePath);
|
|
1252
|
+
topSlotOwners.set(s, owners);
|
|
1253
|
+
}
|
|
1254
|
+
const missingSlots = profile.blueprint.requiredSlots.filter((s) => !topSlots.has(s));
|
|
1255
|
+
const primarySubsystem = inferSelectionSubsystem(result.slice(0, 20), profile);
|
|
1256
|
+
// Identify avoid-like and task-mismatched files in top 10 that are NOT sole slot occupants
|
|
1257
|
+
const swapCandidates = [];
|
|
1258
|
+
let taScanned = 0;
|
|
1259
|
+
let taCandidates = 0;
|
|
1260
|
+
for (let i = 0; i < 10; i++) {
|
|
1261
|
+
const u = result[i];
|
|
1262
|
+
stats.scanned++;
|
|
1263
|
+
const isAvoidLike = isAvoidLikeFile(u, profile);
|
|
1264
|
+
const isTaskMismatch = !isAvoidLike && isTaskMismatchedDomainFile(u, profile);
|
|
1265
|
+
if (!isAvoidLike && !isTaskMismatch)
|
|
1266
|
+
continue;
|
|
1267
|
+
const s = u.slot ?? "unknown";
|
|
1268
|
+
if (requiredSlots.has(s) && (topSlotOwners.get(s)?.length ?? 0) <= 1) {
|
|
1269
|
+
u.taskAvoidGuardFlag = "blocked_slot";
|
|
1270
|
+
stats.blockedSlot++;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
if ((u.slot === "entry" || u.slot === "handler") && (topSlotOwners.get(u.slot)?.length ?? 0) <= 1) {
|
|
1274
|
+
u.taskAvoidGuardFlag = "blocked_slot";
|
|
1275
|
+
stats.blockedSlot++;
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
if (u.role === "edit_candidate" && result.slice(0, 10).filter((r) => r.role === "edit_candidate").length <= 1) {
|
|
1279
|
+
u.taskAvoidGuardFlag = "blocked_expected";
|
|
1280
|
+
stats.blockedExpected++;
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
stats.candidates++;
|
|
1284
|
+
u.taskAvoidGuardFlag = "candidate";
|
|
1285
|
+
const reason = isTaskMismatch ? "task-aware guard (path/subsystem mismatch)" : "avoid-like guard";
|
|
1286
|
+
swapCandidates.push({ idx: i, unit: u, reason });
|
|
1287
|
+
}
|
|
1288
|
+
if (swapCandidates.length === 0)
|
|
1289
|
+
return { units: result, stats };
|
|
1290
|
+
// Scan positions 11-40 for replacement candidates
|
|
1291
|
+
for (const avoid of swapCandidates) {
|
|
1292
|
+
let bestReplacement;
|
|
1293
|
+
let bestScore = -Infinity;
|
|
1294
|
+
let bestIdx = -1;
|
|
1295
|
+
for (let j = 10; j < Math.min(result.length, 40); j++) {
|
|
1296
|
+
const candidate = result[j];
|
|
1297
|
+
if (topFiles.has(candidate.relativePath))
|
|
1298
|
+
continue;
|
|
1299
|
+
if (candidate.contrastContext)
|
|
1300
|
+
continue;
|
|
1301
|
+
const fillsMissing = candidate.slot && missingSlots.includes(candidate.slot);
|
|
1302
|
+
const improvesSubsystem = primarySubsystem && candidate.subsystem === primarySubsystem && avoid.unit.subsystem !== primarySubsystem;
|
|
1303
|
+
if (!fillsMissing && !improvesSubsystem)
|
|
1304
|
+
continue;
|
|
1305
|
+
if (isAvoidLikeFile(candidate, profile))
|
|
1306
|
+
continue;
|
|
1307
|
+
let s = candidate.score;
|
|
1308
|
+
if (fillsMissing)
|
|
1309
|
+
s += 30;
|
|
1310
|
+
if (improvesSubsystem)
|
|
1311
|
+
s += 20;
|
|
1312
|
+
if (candidate.role === "edit_candidate")
|
|
1313
|
+
s += 10;
|
|
1314
|
+
if (s > bestScore) {
|
|
1315
|
+
bestScore = s;
|
|
1316
|
+
bestReplacement = candidate;
|
|
1317
|
+
bestIdx = j;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
if (bestReplacement) {
|
|
1321
|
+
const oldUnit = result[avoid.idx];
|
|
1322
|
+
const fillsSlot = bestReplacement.slot && missingSlots.includes(bestReplacement.slot) ? `fills missing slot ${bestReplacement.slot}` : "";
|
|
1323
|
+
const betterSub = primarySubsystem && bestReplacement.subsystem === primarySubsystem && oldUnit.subsystem !== primarySubsystem ? ` better subsystem ${bestReplacement.subsystem}` : "";
|
|
1324
|
+
const reason = [fillsSlot, betterSub].filter(Boolean).join(";") || "safe avoid replacement";
|
|
1325
|
+
result[avoid.idx] = withSelectionReason(bestReplacement, `promoted: ${avoid.reason} replacement for ${oldUnit.relativePath.split("/").pop()} (${reason})`);
|
|
1326
|
+
result[bestIdx] = { ...oldUnit, contrastContext: true, taskAvoidGuardFlag: "demoted", reasons: [...oldUnit.reasons, `demoted: ${avoid.reason}`] };
|
|
1327
|
+
bestReplacement.taskAvoidGuardFlag = "promoted";
|
|
1328
|
+
stats.demoted++;
|
|
1329
|
+
stats.promoted++;
|
|
1330
|
+
topFiles.add(bestReplacement.relativePath);
|
|
1331
|
+
if (bestReplacement.slot && missingSlots.includes(bestReplacement.slot)) {
|
|
1332
|
+
missingSlots.splice(missingSlots.indexOf(bestReplacement.slot), 1);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
else {
|
|
1336
|
+
avoid.unit.taskAvoidGuardFlag = "blocked_no_replacement";
|
|
1337
|
+
stats.noSafeReplacement++;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
return { units: result, stats };
|
|
1341
|
+
}
|
|
1342
|
+
function isAvoidLikeFile(unit, profile) {
|
|
1343
|
+
const path = normalizePath(unit.relativePath);
|
|
1344
|
+
const isBackendTask = profile?.blueprint?.primarySurface !== "frontend";
|
|
1345
|
+
const targetsUi = profile ? hasTaskUiTarget(profile) : false;
|
|
1346
|
+
const targetsAdmin = profile ? hasTaskAdminTarget(profile) : false;
|
|
1347
|
+
const targetsData = profile ? hasTaskDataTarget(profile) : false;
|
|
1348
|
+
const targetsNotification = profile ? hasTaskNotificationTarget(profile) : false;
|
|
1349
|
+
// Strong avoid signals: helper/util/console/command files
|
|
1350
|
+
if (isGenericAvoidPath(path))
|
|
1351
|
+
return true;
|
|
1352
|
+
// Lock files — never useful as edit candidates
|
|
1353
|
+
if (/(package-lock|yarn\.lock|pnpm-lock|composer\.lock|Gemfile\.lock|poetry\.lock|cargo\.lock)$/.test(path))
|
|
1354
|
+
return true;
|
|
1355
|
+
// Admin-surface files when task doesn't target admin
|
|
1356
|
+
if (!targetsAdmin && isAdminOnlyPath(path))
|
|
1357
|
+
return true;
|
|
1358
|
+
// Template/view files when task is backend (not UI)
|
|
1359
|
+
if (isBackendTask && !targetsUi && isTemplateOnlyPath(path))
|
|
1360
|
+
return true;
|
|
1361
|
+
// Migration files when task isn't about data/schema
|
|
1362
|
+
if (!targetsData && isMigrationOnlyPath(path))
|
|
1363
|
+
return true;
|
|
1364
|
+
// Notification service files when task isn't about notification
|
|
1365
|
+
if (!targetsNotification && isNotificationOnlyPath(path))
|
|
1366
|
+
return true;
|
|
1367
|
+
// Command/scheduler files when task isn't about background jobs
|
|
1368
|
+
if (!profile || !hasTaskBackgroundTarget(profile)) {
|
|
1369
|
+
if (isCommandSchedulerOnlyPath(path))
|
|
1370
|
+
return true;
|
|
1371
|
+
}
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
function isGenericAvoidPath(normalizedPath) {
|
|
1375
|
+
return /(^|\/)(helpers?|utils?)(\/|$)/.test(normalizedPath)
|
|
1376
|
+
&& !/(controller|handler|service|repository|model|route|router)\b/.test(normalizedPath);
|
|
1377
|
+
}
|
|
1378
|
+
function isAdminOnlyPath(normalizedPath) {
|
|
1379
|
+
return /\b(admin|dashboard)(\/|$)/.test(normalizedPath)
|
|
1380
|
+
&& !/(controller|handler|service|repository|model)\b/.test(normalizedPath);
|
|
1381
|
+
}
|
|
1382
|
+
function isTemplateOnlyPath(normalizedPath) {
|
|
1383
|
+
return /(^|\/)(views?|templates?|resources\/views)(\/|$)/.test(normalizedPath)
|
|
1384
|
+
|| /\.(blade\.php|twig|erb|hbs|ejs)$/.test(normalizedPath);
|
|
1385
|
+
}
|
|
1386
|
+
function isMigrationOnlyPath(normalizedPath) {
|
|
1387
|
+
return /(^|\/)(migrations?)(\/|$)/.test(normalizedPath)
|
|
1388
|
+
&& !/(controller|handler|service|model)\b/.test(normalizedPath);
|
|
1389
|
+
}
|
|
1390
|
+
function isNotificationOnlyPath(normalizedPath) {
|
|
1391
|
+
return /\b(notification|notify)(\/|$|s?\.|\b)/.test(normalizedPath)
|
|
1392
|
+
&& !/(controller|handler|service|repository|model|route|router)\b/.test(normalizedPath);
|
|
1393
|
+
}
|
|
1394
|
+
function isCommandSchedulerOnlyPath(normalizedPath) {
|
|
1395
|
+
return /(^|\/)(console|commands?|scheduler|schedulers?|cli|scripts?)(\/|$)/.test(normalizedPath)
|
|
1396
|
+
&& !/(controller|handler|service|repository|model|route)\b/.test(normalizedPath);
|
|
1397
|
+
}
|
|
1398
|
+
function hasTaskUiTarget(profile) {
|
|
1399
|
+
const uiTerms = ["ui", "frontend", "client", "browser", "page", "screen", "component", "view", "form", "button", "modal", "template", "layout", "display", "render", "show", "visible", "giao", "dien", "hien", "thi"];
|
|
1400
|
+
return profile.keywords.some((k) => uiTerms.includes(k)) || profile.blueprint.primarySurface === "frontend";
|
|
1401
|
+
}
|
|
1402
|
+
function hasTaskAdminTarget(profile) {
|
|
1403
|
+
const adminTerms = ["admin", "dashboard", "backoffice", "console"];
|
|
1404
|
+
return profile.keywords.some((k) => adminTerms.includes(k));
|
|
1405
|
+
}
|
|
1406
|
+
function hasTaskDataTarget(profile) {
|
|
1407
|
+
const dataTerms = ["schema", "migration", "database", "table", "column", "db", "migrate", "prisma"];
|
|
1408
|
+
return profile.keywords.some((k) => dataTerms.includes(k)) || profile.intent === "migration";
|
|
1409
|
+
}
|
|
1410
|
+
function hasTaskNotificationTarget(profile) {
|
|
1411
|
+
const notifTerms = ["notification", "notify", "email", "mail", "message", "alert", "thong", "bao"];
|
|
1412
|
+
return profile.keywords.some((k) => notifTerms.includes(k));
|
|
1413
|
+
}
|
|
1414
|
+
function hasTaskBackgroundTarget(profile) {
|
|
1415
|
+
const bgTerms = ["job", "worker", "queue", "scheduler", "cron", "command", "console", "cli", "background", "batch", "schedule"];
|
|
1416
|
+
return profile.keywords.some((k) => bgTerms.includes(k));
|
|
1417
|
+
}
|
|
1418
|
+
function hasTaskTestTarget(profile) {
|
|
1419
|
+
const testTerms = ["test", "tests", "spec", "testing", "unit test", "integration", "e2e", "assert", "expect", "fixture", "mock", "seed", "coverage"];
|
|
1420
|
+
return profile.keywords.some((k) => testTerms.includes(k));
|
|
1421
|
+
}
|
|
563
1422
|
//# sourceMappingURL=ranker.js.map
|