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
@@ -9,6 +9,7 @@ exports.isRequiredSlot = isRequiredSlot;
9
9
  exports.computeContextCoverage = computeContextCoverage;
10
10
  exports.sortSlots = sortSlots;
11
11
  const text_1 = require("../utils/text");
12
+ const laravel_1 = require("../framework/laravel");
12
13
  const slotOrder = ["entry", "handler", "domain", "data", "validation", "permission", "config", "side_effect", "ui", "tests", "docs", "unknown"];
13
14
  const slotTerms = {
14
15
  entry: ["route", "routes", "router", "endpoint", "api", "page", "screen", "loader", "action"],
@@ -58,7 +59,7 @@ const taskBlueprints = {
58
59
  optionalSlots: ["domain", "data", "validation", "permission", "config", "side_effect", "ui", "tests", "docs"]
59
60
  }
60
61
  };
61
- function buildTaskBlueprint(input) {
62
+ async function buildTaskBlueprint(input) {
62
63
  const baseShape = input.intent === "unknown" && isReviewLike(input.normalizedTask) ? "investigation" : input.intent;
63
64
  const template = taskBlueprints[baseShape];
64
65
  const required = new Set(template.requiredSlots);
@@ -67,6 +68,17 @@ function buildTaskBlueprint(input) {
67
68
  const normalized = input.normalizedTask;
68
69
  promoteSlots(required, optional, detectTargetedSlots(normalized, terms));
69
70
  adaptSlotsToTaskSurface(required, optional, normalized, terms);
71
+ // Apply Laravel template if workspace root is available
72
+ let frameworkHints;
73
+ if (input.workspaceRoot) {
74
+ const laravelResult = await (0, laravel_1.applyLaravelTemplate)(input.workspaceRoot, normalized, Array.from(required));
75
+ if (laravelResult.detected) {
76
+ for (const slot of laravelResult.suggestedRequiredSlots) {
77
+ required.add(slot);
78
+ }
79
+ frameworkHints = laravelResult.missingLinkHints;
80
+ }
81
+ }
70
82
  return {
71
83
  shape: baseShape,
72
84
  confidence: input.confidence,
@@ -74,7 +86,8 @@ function buildTaskBlueprint(input) {
74
86
  negativeTerms: input.negativeTerms ?? [],
75
87
  requiredSlots: sortSlots(Array.from(required)),
76
88
  optionalSlots: sortSlots(Array.from(optional).filter((slot) => !required.has(slot))),
77
- expansionHints: buildExpansionHints(required)
89
+ expansionHints: buildExpansionHints(required),
90
+ frameworkHints,
78
91
  };
79
92
  }
80
93
  function slotSearchTerms(slot) {
@@ -13,10 +13,10 @@ const sections = [
13
13
  function createContextPack(task, units, config) {
14
14
  const header = [`# FlowSeeker Context`, "", `## Task`, task, ""].join("\n");
15
15
  const output = [header];
16
- let usedTokens = (0, text_1.tokenEstimate)(header);
16
+ let usedTokens = (0, text_1.fastTokenEstimate)(header);
17
17
  const fileScope = renderFileScope(units);
18
18
  output.push(fileScope);
19
- usedTokens += (0, text_1.tokenEstimate)(fileScope);
19
+ usedTokens += (0, text_1.fastTokenEstimate)(fileScope);
20
20
  const contextUnits = (0, fileGroups_1.selectDiverseEvidenceUnits)(units, 2);
21
21
  for (const section of sections) {
22
22
  const sectionUnits = contextUnits.filter((unit) => section.roles.includes(unit.role));
@@ -26,7 +26,7 @@ function createContextPack(task, units, config) {
26
26
  const sectionLines = [`## ${section.title}`, ""];
27
27
  for (const unit of sectionUnits) {
28
28
  const block = renderUnit(unit);
29
- const nextTokens = (0, text_1.tokenEstimate)(block);
29
+ const nextTokens = (0, text_1.fastTokenEstimate)(block);
30
30
  if (usedTokens + nextTokens > config.pipeline.maxContextTokens) {
31
31
  sectionLines.push(`- Context budget reached. ${sectionUnits.length} candidates were ranked, but lower-ranked snippets were omitted.`, "");
32
32
  break;
@@ -0,0 +1,358 @@
1
+ "use strict";
2
+ // Phase 05 Sprint 5B-R — Disabled Deterministic Reranker Model
3
+ // Execution mode: report_only_disabled
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.DEFAULT_SELECTOR_CONFIG = exports.DEFAULT_RERANKER_CONFIG = void 0;
6
+ exports.validateConfig = validateConfig;
7
+ exports.computeScoreBreakdown = computeScoreBreakdown;
8
+ exports.rerankCandidates = rerankCandidates;
9
+ exports.validateSelectorConfig = validateSelectorConfig;
10
+ exports.validateSelectorMetadata = validateSelectorMetadata;
11
+ exports.validateSelectorMetadataMap = validateSelectorMetadataMap;
12
+ exports.deriveCountMaps = deriveCountMaps;
13
+ exports.selectCoverageTop10 = selectCoverageTop10;
14
+ exports.DEFAULT_RERANKER_CONFIG = Object.freeze({
15
+ baseScoreScale: 0.2,
16
+ baseScoreCap: 100,
17
+ weights: Object.freeze({
18
+ exactBoost: 4, slotBoost: 3, graphBoost: 2, symbolBoost: 2, subsystemBoost: 1, parserBoost: 1, noisePenalty: 5, duplicationPenalty: 3
19
+ }),
20
+ caps: Object.freeze({
21
+ exactBoost: 12, slotBoost: 6, graphBoost: 6, symbolBoost: 6, subsystemBoost: 4, parserBoost: 3, noisePenalty: 20, duplicationPenalty: 9
22
+ })
23
+ });
24
+ function validateConfig(c) {
25
+ const checkNumber = (label, v) => {
26
+ if (!Number.isFinite(v) || v < 0)
27
+ throw new Error(`Invalid config ${label}: ${v}`);
28
+ };
29
+ checkNumber("baseScoreScale", c.baseScoreScale);
30
+ checkNumber("baseScoreCap", c.baseScoreCap);
31
+ const w = c.weights;
32
+ const p = c.caps;
33
+ checkNumber("exactBoost weight", w.exactBoost);
34
+ checkNumber("exactBoost cap", p.exactBoost);
35
+ checkNumber("slotBoost weight", w.slotBoost);
36
+ checkNumber("slotBoost cap", p.slotBoost);
37
+ checkNumber("graphBoost weight", w.graphBoost);
38
+ checkNumber("graphBoost cap", p.graphBoost);
39
+ checkNumber("symbolBoost weight", w.symbolBoost);
40
+ checkNumber("symbolBoost cap", p.symbolBoost);
41
+ checkNumber("subsystemBoost weight", w.subsystemBoost);
42
+ checkNumber("subsystemBoost cap", p.subsystemBoost);
43
+ checkNumber("parserBoost weight", w.parserBoost);
44
+ checkNumber("parserBoost cap", p.parserBoost);
45
+ checkNumber("noisePenalty weight", w.noisePenalty);
46
+ checkNumber("noisePenalty cap", p.noisePenalty);
47
+ checkNumber("duplicationPenalty weight", w.duplicationPenalty);
48
+ checkNumber("duplicationPenalty cap", p.duplicationPenalty);
49
+ }
50
+ function norm(s) { return (s || "").replace(/\\/g, "/").toLowerCase(); }
51
+ function max(a, b) { return a > b ? a : b; }
52
+ // Locale-independent string comparison (no localeCompare)
53
+ function cmpStr(a, b) {
54
+ if (a < b)
55
+ return -1;
56
+ if (a > b)
57
+ return 1;
58
+ return 0;
59
+ }
60
+ function deepClone(c) {
61
+ return {
62
+ ...c,
63
+ symbols: c.symbols ? [...c.symbols] : undefined,
64
+ symbolMatches: c.symbolMatches ? [...c.symbolMatches] : undefined,
65
+ matchedTerms: [...c.matchedTerms],
66
+ imports: [...c.imports],
67
+ channelHits: c.channelHits ? c.channelHits.map(h => ({ ...h })) : undefined,
68
+ retrievalPasses: [...c.retrievalPasses],
69
+ reasons: [...c.reasons]
70
+ };
71
+ }
72
+ function computeScoreBreakdown(candidate, config = exports.DEFAULT_RERANKER_CONFIG, context) {
73
+ validateConfig(config);
74
+ const w = config.weights;
75
+ const c = config.caps;
76
+ const baseScore = max(0, Math.min(config.baseScoreCap, candidate.score * config.baseScoreScale));
77
+ // Exact boost: non-empty matched terms that appear literally in path
78
+ const effectiveTerms = candidate.matchedTerms.filter(t => t && t.trim().length > 0);
79
+ const cPath = norm(candidate.relativePath);
80
+ const exactCount = effectiveTerms.filter(t => cPath.includes(norm(t))).length;
81
+ const exactBoost = max(0, Math.min(c.exactBoost, exactCount * w.exactBoost));
82
+ const knownSlots = new Set(["entry", "handler", "domain", "data", "config", "side_effect", "tests", "validation", "permission"]);
83
+ const slotBoost = (candidate.slot && knownSlots.has(candidate.slot)) ? max(0, Math.min(c.slotBoost, w.slotBoost)) : 0;
84
+ const hasGraphEvidence = (candidate.channelHits || []).some(h => h.channel === "graph" || h.channel === "dependency");
85
+ const graphBoost = hasGraphEvidence ? max(0, Math.min(c.graphBoost, w.graphBoost)) : 0;
86
+ const symbolBoost = (candidate.symbolMatches && candidate.symbolMatches.length > 0)
87
+ ? max(0, Math.min(c.symbolBoost, candidate.symbolMatches.length * w.symbolBoost)) : 0;
88
+ const subsystemBoost = candidate.subsystem ? max(0, Math.min(c.subsystemBoost, w.subsystemBoost)) : 0;
89
+ const parserBoost = candidate.extractorKind && candidate.extractorKind !== "fallback-chunk"
90
+ ? max(0, Math.min(c.parserBoost, w.parserBoost)) : 0;
91
+ const noisePenalty = candidate.confidence < 0.4 && candidate.reasons.length < 2
92
+ ? max(0, Math.min(c.noisePenalty, w.noisePenalty)) : 0;
93
+ let duplicationPenalty = 0;
94
+ if (context) {
95
+ const np = norm(candidate.relativePath);
96
+ if (context.seenFiles?.has(np))
97
+ duplicationPenalty = max(0, Math.min(c.duplicationPenalty, w.duplicationPenalty));
98
+ if (candidate.subsystem && context.seenSubsystems?.has(norm(candidate.subsystem)))
99
+ duplicationPenalty = max(duplicationPenalty, Math.min(c.duplicationPenalty, w.duplicationPenalty * 0.5));
100
+ }
101
+ const finalScore = max(0, baseScore + exactBoost + slotBoost + graphBoost + symbolBoost + subsystemBoost + parserBoost - noisePenalty - duplicationPenalty);
102
+ return { baseScore, exactBoost, slotBoost, graphBoost, symbolBoost, subsystemBoost, parserBoost, noisePenalty, duplicationPenalty, finalScore };
103
+ }
104
+ function rerankCandidates(candidates, config = exports.DEFAULT_RERANKER_CONFIG) {
105
+ validateConfig(config);
106
+ // Deep clone — do not mutate input nested arrays/objects
107
+ const copy = candidates.map(c => deepClone(c));
108
+ const byOriginalRank = [...copy].sort((a, b) => {
109
+ if (a.originalRank !== b.originalRank)
110
+ return a.originalRank - b.originalRank;
111
+ return cmpStr(norm(a.relativePath), norm(b.relativePath));
112
+ });
113
+ const seenSubsystems = new Set();
114
+ const seenFiles = new Set();
115
+ const results = [];
116
+ for (const candidate of byOriginalRank) {
117
+ const breakdown = computeScoreBreakdown(candidate, config, { seenSubsystems, seenFiles });
118
+ const reasons = buildReasons(candidate, breakdown);
119
+ results.push({ relativePath: candidate.relativePath, originalRank: candidate.originalRank, scoreBreakdown: breakdown, finalScore: breakdown.finalScore, stableRank: 0, reasons });
120
+ seenFiles.add(norm(candidate.relativePath));
121
+ if (candidate.subsystem)
122
+ seenSubsystems.add(norm(candidate.subsystem));
123
+ }
124
+ results.sort((a, b) => {
125
+ if (b.finalScore !== a.finalScore)
126
+ return b.finalScore - a.finalScore;
127
+ if (a.originalRank !== b.originalRank)
128
+ return a.originalRank - b.originalRank;
129
+ return cmpStr(norm(a.relativePath), norm(b.relativePath));
130
+ });
131
+ for (let i = 0; i < results.length; i++)
132
+ results[i].stableRank = i + 1;
133
+ return results;
134
+ }
135
+ function buildReasons(candidate, b) {
136
+ const r = [];
137
+ if (b.exactBoost > 0)
138
+ r.push(`exact +${b.exactBoost.toFixed(1)}`);
139
+ if (b.slotBoost > 0)
140
+ r.push(`slot ${candidate.slot} +${b.slotBoost.toFixed(1)}`);
141
+ if (b.graphBoost > 0)
142
+ r.push(`graph +${b.graphBoost.toFixed(1)}`);
143
+ if (b.symbolBoost > 0)
144
+ r.push(`symbol +${b.symbolBoost.toFixed(1)}`);
145
+ if (b.subsystemBoost > 0)
146
+ r.push(`subsystem +${b.subsystemBoost.toFixed(1)}`);
147
+ if (b.parserBoost > 0)
148
+ r.push(`parser +${b.parserBoost.toFixed(1)}`);
149
+ if (b.noisePenalty > 0)
150
+ r.push(`noise -${b.noisePenalty.toFixed(1)}`);
151
+ if (b.duplicationPenalty > 0)
152
+ r.push(`duplicate -${b.duplicationPenalty.toFixed(1)}`);
153
+ if (b.baseScore > 0)
154
+ r.push(`base ${b.baseScore.toFixed(1)}`);
155
+ return r.length > 0 ? r : ["no signal"];
156
+ }
157
+ exports.DEFAULT_SELECTOR_CONFIG = {
158
+ topK: 10, maxProtected: 3, maxPerSlot: {}, maxPerSubsystem: 3, requiredSlots: [],
159
+ enableProtection: false, enableSlotCaps: false, enableSubsystemCaps: false
160
+ };
161
+ function validateSelectorConfig(c) {
162
+ if (!Number.isInteger(c.topK) || c.topK < 1)
163
+ throw new Error("topK must be integer >= 1");
164
+ if (!Number.isInteger(c.maxProtected) || c.maxProtected < 0 || c.maxProtected > c.topK)
165
+ throw new Error("maxProtected must be integer 0..topK");
166
+ if (typeof c.enableProtection !== "boolean")
167
+ throw new Error("enableProtection must be boolean");
168
+ if (typeof c.enableSlotCaps !== "boolean")
169
+ throw new Error("enableSlotCaps must be boolean");
170
+ if (typeof c.enableSubsystemCaps !== "boolean")
171
+ throw new Error("enableSubsystemCaps must be boolean");
172
+ if (!Array.isArray(c.requiredSlots))
173
+ throw new Error("requiredSlots must be array");
174
+ const seen = new Set();
175
+ for (const s of c.requiredSlots) {
176
+ if (typeof s !== "string" || s.trim().length === 0)
177
+ throw new Error("required slot must be non-empty string");
178
+ const ns = s.trim().toLowerCase();
179
+ if (seen.has(ns))
180
+ throw new Error("duplicate required slot");
181
+ seen.add(ns);
182
+ }
183
+ if (c.maxPerSlot === null || c.maxPerSlot === undefined || Array.isArray(c.maxPerSlot) || Object.getPrototypeOf(c.maxPerSlot) !== Object.prototype)
184
+ throw new Error("maxPerSlot must be plain object");
185
+ for (const [k, v] of Object.entries(c.maxPerSlot)) {
186
+ if (typeof k !== "string" || k.trim().length === 0)
187
+ throw new Error("slot cap key non-empty");
188
+ if (!Number.isInteger(v) || v < 1)
189
+ throw new Error(`slot cap must be integer >= 1`);
190
+ }
191
+ if (!Number.isInteger(c.maxPerSubsystem) || c.maxPerSubsystem < 1)
192
+ throw new Error("maxPerSubsystem must be integer >= 1");
193
+ }
194
+ function validateSelectorMetadata(m) {
195
+ if (!m || typeof m !== "object" || m === null)
196
+ throw new Error("metadata must be non-null object");
197
+ if (typeof m.protected !== "boolean")
198
+ throw new Error("metadata.protected must be boolean");
199
+ if (typeof m.slot !== "string" || m.slot.trim().length === 0)
200
+ throw new Error("metadata.slot must be non-empty string");
201
+ if (typeof m.subsystem !== "string" || m.subsystem.trim().length === 0)
202
+ throw new Error("metadata.subsystem must be non-empty string");
203
+ }
204
+ function validateSelectorMetadataMap(metaMap) {
205
+ if (metaMap === undefined)
206
+ return;
207
+ if (!(metaMap instanceof Map))
208
+ throw new Error("metaMap must be a Map or undefined");
209
+ for (const [k, v] of metaMap) {
210
+ if (typeof k !== "string" || k.trim().length === 0)
211
+ throw new Error("metadata key must be non-empty string");
212
+ if (k !== norm(k))
213
+ throw new Error("metadata key must be normalized (lowercase, slash)");
214
+ validateSelectorMetadata(v);
215
+ }
216
+ }
217
+ function deriveCountMaps(selected, metaMap) {
218
+ const sc = {}, sub = {};
219
+ for (const s of selected) {
220
+ const m = metaMap?.get(norm(s.relativePath)) || { protected: false, slot: "unknown", subsystem: "none" };
221
+ sc[m.slot] = (sc[m.slot] || 0) + 1;
222
+ sub[m.subsystem] = (sub[m.subsystem] || 0) + 1;
223
+ }
224
+ return { slotCounts: sc, subsystemCounts: sub };
225
+ }
226
+ function selectCoverageTop10(scored, config = exports.DEFAULT_SELECTOR_CONFIG, metaMap) {
227
+ validateSelectorConfig(config);
228
+ if (metaMap)
229
+ validateSelectorMetadataMap(metaMap);
230
+ const seen = new Set();
231
+ for (const c of scored) {
232
+ const rp = norm(c.relativePath);
233
+ if (seen.has(rp))
234
+ throw new Error(`Duplicate path: ${c.relativePath}`);
235
+ seen.add(rp);
236
+ }
237
+ const sorted = [...scored].sort((a, b) => { if (b.finalScore !== a.finalScore)
238
+ return b.finalScore - a.finalScore; if (a.originalRank !== b.originalRank)
239
+ return a.originalRank - b.originalRank; return cmpStr(norm(a.relativePath), norm(b.relativePath)); });
240
+ const sel = [], selP = new Set();
241
+ const relax = [];
242
+ let pass = 0;
243
+ const slotC = {}, subC = {};
244
+ const effSlot = { ...config.maxPerSlot };
245
+ const effSub = {};
246
+ for (const k of Object.keys(subC))
247
+ effSub[k] = config.maxPerSubsystem;
248
+ const sTrace = [], rTrace = [];
249
+ function m(rp) { return metaMap?.get(rp) || { protected: false, slot: "unknown", subsystem: "none" }; }
250
+ function capCheck(rp) {
251
+ const mt = m(rp);
252
+ if (config.enableSlotCaps && (slotC[mt.slot] || 0) >= (effSlot[mt.slot] ?? config.maxPerSlot[mt.slot] ?? 999))
253
+ return false;
254
+ if (config.enableSubsystemCaps && (subC[mt.subsystem] || 0) >= (effSub[mt.subsystem] ?? config.maxPerSubsystem))
255
+ return false;
256
+ return true;
257
+ }
258
+ function trackCounts(rp) { const mt = m(rp); slotC[mt.slot] = (slotC[mt.slot] || 0) + 1; subC[mt.subsystem] = (subC[mt.subsystem] || 0) + 1; }
259
+ function addSel(c, prot, reason) {
260
+ const rp = norm(c.relativePath);
261
+ if (sel.length >= config.topK || selP.has(rp))
262
+ return false;
263
+ if (!capCheck(rp)) {
264
+ rTrace.push({ decision: "rejected", phase: "cap_blocked", candidate: c.relativePath, reason: `blocked: ${reason}`, pass: 0 });
265
+ return false;
266
+ }
267
+ pass++;
268
+ const sc = { relativePath: c.relativePath, finalScore: c.finalScore, stableRank: 0, protected: prot, selectionPass: pass, reason };
269
+ sel.push(sc);
270
+ selP.add(rp);
271
+ sTrace.push({ decision: "selected", phase: prot ? "protected" : reason.includes("slot:") ? "slot_fill" : "score_fill", candidate: c.relativePath, reason, pass });
272
+ trackCounts(rp);
273
+ return true;
274
+ }
275
+ if (config.enableProtection) {
276
+ let n = 0;
277
+ for (const c of sorted) {
278
+ if (sel.length >= config.topK || n >= config.maxProtected)
279
+ break;
280
+ if (!m(norm(c.relativePath)).protected)
281
+ continue;
282
+ if (addSel(c, true, `protected`))
283
+ n++;
284
+ }
285
+ }
286
+ if (config.requiredSlots.length > 0) {
287
+ const filled = new Set();
288
+ for (const s of sel) {
289
+ filled.add(m(norm(s.relativePath)).slot);
290
+ }
291
+ for (const slot of config.requiredSlots) {
292
+ if (filled.has(slot))
293
+ continue;
294
+ for (const c of sorted) {
295
+ if (sel.length >= config.topK)
296
+ break;
297
+ if (m(norm(c.relativePath)).slot !== slot)
298
+ continue;
299
+ if (addSel(c, false, `required slot:${slot}`)) {
300
+ filled.add(slot);
301
+ break;
302
+ }
303
+ }
304
+ }
305
+ }
306
+ for (const c of sorted) {
307
+ if (sel.length >= config.topK)
308
+ break;
309
+ addSel(c, false, `score=${c.finalScore.toFixed(1)}`);
310
+ }
311
+ // Relaxation: per-candidate, separate slot/subsystem checks, relax all caps for same candidate
312
+ if (sel.length < config.topK) {
313
+ for (const c of sorted) {
314
+ if (sel.length >= config.topK)
315
+ break;
316
+ const rp = norm(c.relativePath);
317
+ if (selP.has(rp))
318
+ continue;
319
+ const mt = m(rp);
320
+ let slotBlocked = false, subBlocked = false;
321
+ if (config.enableSlotCaps && (slotC[mt.slot] || 0) >= (effSlot[mt.slot] ?? config.maxPerSlot[mt.slot] ?? 999))
322
+ slotBlocked = true;
323
+ if (config.enableSubsystemCaps && (subC[mt.subsystem] || 0) >= (effSub[mt.subsystem] ?? config.maxPerSubsystem))
324
+ subBlocked = true;
325
+ if (!slotBlocked && !subBlocked)
326
+ continue;
327
+ // Relax slot first, then subsystem, for the same candidate
328
+ if (slotBlocked) {
329
+ const old = effSlot[mt.slot] ?? config.maxPerSlot[mt.slot] ?? 999;
330
+ effSlot[mt.slot] = old + 1;
331
+ relax.push({ pass: sel.length + 1, capType: "slot", key: mt.slot, oldLimit: old, newLimit: old + 1, candidate: c.relativePath, reason: `topK_fill` });
332
+ }
333
+ // Re-check subsystem after slot relaxation
334
+ if (config.enableSubsystemCaps && (subC[mt.subsystem] || 0) >= (effSub[mt.subsystem] ?? config.maxPerSubsystem)) {
335
+ const old = effSub[mt.subsystem] ?? config.maxPerSubsystem;
336
+ effSub[mt.subsystem] = old + 1;
337
+ relax.push({ pass: sel.length + 1, capType: "subsystem", key: mt.subsystem, oldLimit: old, newLimit: old + 1, candidate: c.relativePath, reason: `topK_fill` });
338
+ }
339
+ pass++;
340
+ const sc = { relativePath: c.relativePath, finalScore: c.finalScore, stableRank: 0, protected: false, selectionPass: pass, reason: "topK_fill" };
341
+ sel.push(sc);
342
+ selP.add(rp);
343
+ sTrace.push({ decision: "selected", phase: "cap_relaxed", candidate: c.relativePath, reason: "topK_fill", pass });
344
+ trackCounts(rp);
345
+ }
346
+ }
347
+ // Rebuild rejectedTrace from final rejected
348
+ rTrace.length = 0;
349
+ const finRej = sorted.filter(c => !selP.has(norm(c.relativePath))).map(c => ({ relativePath: c.relativePath, finalScore: c.finalScore, originalRank: c.originalRank, reason: "not selected" }));
350
+ for (const r of finRej) {
351
+ rTrace.push({ decision: "rejected", phase: "score_fill", candidate: r.relativePath, reason: "not selected", pass: 0 });
352
+ }
353
+ for (let i = 0; i < sel.length; i++)
354
+ sel[i].stableRank = i + 1;
355
+ const finalCounts = deriveCountMaps(sel, metaMap);
356
+ return { selected: sel, rejected: finRej, selectedTrace: sTrace, rejectedTrace: rTrace, relaxations: relax, capRelaxations: relax.length, slotCounts: finalCounts.slotCounts, subsystemCounts: finalCounts.subsystemCounts };
357
+ }
358
+ //# sourceMappingURL=deterministicReranker.js.map
@@ -51,7 +51,11 @@ function computeExpectedPathMetrics(units, expectedPaths, passAtK, mode = "any")
51
51
  const firstUnitHitIndex = units.findIndex((unit) => expectedPaths.some((expectedPath) => pathMatches(unit.relativePath, expectedPath)));
52
52
  const firstFileHitIndex = fileGroups.findIndex((group) => expectedPaths.some((expectedPath) => pathMatches(group.relativePath, expectedPath)));
53
53
  const coverageAtPassK = coverageAt(fileGroups.map((group) => group.relativePath), expectedPaths, passAtK);
54
- const pass = mode === "all" ? coverageAtPassK === expectedPaths.length : firstFileHitIndex >= 0 && firstFileHitIndex + 1 <= passAtK;
54
+ const pass = mode === "any"
55
+ ? firstFileHitIndex >= 0 && firstFileHitIndex + 1 <= passAtK
56
+ : mode === "subset"
57
+ ? coverageAtPassK >= Math.max(2, Math.ceil(expectedPaths.length * 0.5))
58
+ : coverageAtPassK === expectedPaths.length;
55
59
  return {
56
60
  expectedCount: expectedPaths.length,
57
61
  firstUnitHitRank: firstUnitHitIndex >= 0 ? firstUnitHitIndex + 1 : undefined,
@@ -127,6 +131,10 @@ function computeRankingQualityMetrics(result, topK = 10) {
127
131
  const subsystemPurityTop10 = primarySubsystemTop10 && subsystemFiles.length > 0
128
132
  ? subsystemFiles.filter((group) => group.subsystem === primarySubsystemTop10).length / subsystemFiles.length
129
133
  : 0;
134
+ const treeSitterEvidenceTop10 = topUnits.filter((unit) => unit.extractorKind === "tree-sitter").length;
135
+ const structuredRegexEvidenceTop10 = topUnits.filter((unit) => unit.extractorKind === "structured-regex").length;
136
+ const fallbackEvidenceTop10 = topUnits.filter((unit) => !unit.extractorKind || unit.extractorKind === "fallback-chunk").length;
137
+ const parserFallbackCases = topUnits.filter((unit) => unit.extractorKind === "fallback-chunk").length > 0 ? 1 : 0;
130
138
  return {
131
139
  uniqueTop10Files: uniqueTopFiles,
132
140
  duplicateEvidenceTop10,
@@ -137,7 +145,11 @@ function computeRankingQualityMetrics(result, topK = 10) {
137
145
  contrastContextTop10: topUnits.filter((unit) => unit.contrastContext).length,
138
146
  primarySubsystemTop10,
139
147
  subsystemPurityTop10,
140
- symbolEvidenceTop10: topUnits.filter((unit) => (unit.symbolMatches?.length ?? 0) > 0 || unit.kind === "symbol").length
148
+ symbolEvidenceTop10: topUnits.filter((unit) => (unit.symbolMatches?.length ?? 0) > 0 || unit.kind === "symbol").length,
149
+ treeSitterEvidenceTop10,
150
+ structuredRegexEvidenceTop10,
151
+ fallbackEvidenceTop10,
152
+ parserFallbackCases
141
153
  };
142
154
  }
143
155
  function coverageAt(actualPaths, expectedPaths, k) {
@@ -13,7 +13,13 @@ const rolePriority = {
13
13
  function aggregateEvidenceFiles(units) {
14
14
  const byPath = new Map();
15
15
  for (const unit of units) {
16
- byPath.set(unit.relativePath, [...(byPath.get(unit.relativePath) ?? []), unit]);
16
+ const existing = byPath.get(unit.relativePath);
17
+ if (existing) {
18
+ existing.push(unit);
19
+ }
20
+ else {
21
+ byPath.set(unit.relativePath, [unit]);
22
+ }
17
23
  }
18
24
  return Array.from(byPath.entries())
19
25
  .map(([relativePath, fileUnits]) => toGroup(relativePath, fileUnits))