flowseeker 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.env.example +7 -0
  2. package/CHANGELOG.md +143 -108
  3. package/README.md +288 -221
  4. package/dist/chat/nativeChatParticipant.js +1 -1
  5. package/dist/cli/flowCommand.js +175 -0
  6. package/dist/cli/main.js +1810 -0
  7. package/dist/cli/mcpServer.js +7 -1
  8. package/dist/cli/runEvaluation.js +281 -2
  9. package/dist/config/defaultConfig.js +11 -1
  10. package/dist/config/env.js +118 -0
  11. package/dist/config/loadConfig.js +18 -1
  12. package/dist/config/loadConfigFromPath.js +3 -1
  13. package/dist/eval/accuracyV2.js +483 -0
  14. package/dist/eval/goldenTask.js +192 -0
  15. package/dist/extension.js +23 -0
  16. package/dist/framework/laravel.js +177 -0
  17. package/dist/gateway/embeddingProviders.js +852 -0
  18. package/dist/index/cacheStore.js +43 -0
  19. package/dist/index/configRouteDiscoveryProbe.js +288 -0
  20. package/dist/index/embeddingIndex.js +193 -0
  21. package/dist/index/graphIndex.js +460 -0
  22. package/dist/index/indexWatcher.js +86 -0
  23. package/dist/index/semanticChunkIndex.js +388 -0
  24. package/dist/index/structuredExtractor.js +303 -12
  25. package/dist/index/treeSitterExtractor.js +264 -0
  26. package/dist/index/vectorStore.js +41 -0
  27. package/dist/index/workspaceIndex.js +901 -26
  28. package/dist/mcp/mcpTools.js +1678 -2
  29. package/dist/pipeline/contextBlueprint.js +15 -2
  30. package/dist/pipeline/contextPack.js +3 -3
  31. package/dist/pipeline/deterministicReranker.js +358 -0
  32. package/dist/pipeline/evaluationMetrics.js +14 -2
  33. package/dist/pipeline/fileGroups.js +7 -1
  34. package/dist/pipeline/fileScanner.js +209 -12
  35. package/dist/pipeline/fusionTrace.js +149 -0
  36. package/dist/pipeline/llmReranker.js +151 -0
  37. package/dist/pipeline/nodeScan.js +102 -12
  38. package/dist/pipeline/ranker.js +875 -16
  39. package/dist/pipeline/retrievalFusion.js +41 -0
  40. package/dist/pipeline/roleRefinement.js +62 -0
  41. package/dist/pipeline/runHeadless.js +60 -5
  42. package/dist/pipeline/runPipeline.js +2 -2
  43. package/dist/pipeline/solvePacket.js +656 -43
  44. package/dist/pipeline/subsystem.js +21 -0
  45. package/dist/pipeline/taskUnderstanding.js +2 -2
  46. package/dist/ui/chatViewProvider.js +1 -1
  47. package/docs/demo-screenshot-checklist.md +86 -0
  48. package/docs/marketplace-copy.md +92 -0
  49. package/docs/mcp-onboarding.md +191 -0
  50. package/package.json +633 -561
@@ -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 boosted = deduped.map((unit) => boostUnit(unit, fileCounts, config, profile, rankOptions?.editorContext));
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
- return selectDiverseUnits(sorted, config.pipeline.maxCandidates);
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 selectWithBlueprintCoverage(sorted, profile, config.pipeline.maxCandidates);
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), { allowThirdFileUnit: true });
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
- return state.selected.slice(0, maxCandidates);
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 += 18;
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 : options.allowThirdFileUnit ? 3 : 2;
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, { allowThirdFileUnit: true })) {
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 boostUnit(unit, fileCounts, config, profile, editorContext) {
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 ? Math.min(8, unit.symbolMatches.length * 4) : 0;
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 ? 8 : 0;
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
- const score = unit.score + crossEvidenceBonus + multiTermBonus + slotBonus + subsystemBonus + symbolBonus + literalTermBonus + filenamePhraseBonus - testPenalty - duplicatePenalty - contrastPenalty + editorBoost;
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