flowseeker 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/CHANGELOG.md +131 -108
- package/README.md +288 -221
- package/dist/cli/flowCommand.js +175 -0
- package/dist/cli/main.js +1794 -0
- package/dist/cli/mcpServer.js +7 -1
- package/dist/cli/runEvaluation.js +178 -2
- package/dist/config/defaultConfig.js +11 -1
- package/dist/config/env.js +118 -0
- package/dist/config/loadConfig.js +18 -1
- package/dist/config/loadConfigFromPath.js +3 -1
- package/dist/extension.js +23 -0
- package/dist/gateway/embeddingProviders.js +852 -0
- package/dist/index/cacheStore.js +43 -0
- package/dist/index/configRouteDiscoveryProbe.js +288 -0
- package/dist/index/embeddingIndex.js +193 -0
- package/dist/index/graphIndex.js +460 -0
- package/dist/index/indexWatcher.js +86 -0
- package/dist/index/structuredExtractor.js +303 -12
- package/dist/index/treeSitterExtractor.js +264 -0
- package/dist/index/vectorStore.js +41 -0
- package/dist/index/workspaceIndex.js +591 -26
- package/dist/mcp/mcpTools.js +51 -0
- package/dist/pipeline/contextPack.js +3 -3
- package/dist/pipeline/deterministicReranker.js +358 -0
- package/dist/pipeline/evaluationMetrics.js +14 -2
- package/dist/pipeline/fileGroups.js +7 -1
- package/dist/pipeline/fileScanner.js +93 -11
- package/dist/pipeline/llmReranker.js +151 -0
- package/dist/pipeline/nodeScan.js +91 -12
- package/dist/pipeline/ranker.js +875 -16
- package/dist/pipeline/retrievalFusion.js +41 -0
- package/dist/pipeline/runHeadless.js +35 -0
- package/dist/pipeline/solvePacket.js +549 -42
- package/dist/pipeline/subsystem.js +21 -0
- package/docs/demo-screenshot-checklist.md +86 -0
- package/docs/marketplace-copy.md +92 -0
- package/docs/mcp-onboarding.md +191 -0
- package/package.json +633 -561
package/dist/mcp/mcpTools.js
CHANGED
|
@@ -59,6 +59,24 @@ exports.TOOL_DEFINITIONS = [
|
|
|
59
59
|
required: ["task"]
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
|
+
{
|
|
63
|
+
name: "flowseeker_auto",
|
|
64
|
+
description: "Create an implementation-plan prompt from relevant code context. The agent should produce a reviewable plan or patch only after validating the retrieved evidence.",
|
|
65
|
+
inputSchema: {
|
|
66
|
+
type: "object",
|
|
67
|
+
properties: {
|
|
68
|
+
task: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "Natural-language codebase task to prepare an implementation plan for."
|
|
71
|
+
},
|
|
72
|
+
workspace: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Workspace root path. Use an absolute path if the IDE does not resolve cwd correctly."
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
required: ["task"]
|
|
78
|
+
}
|
|
79
|
+
},
|
|
62
80
|
{
|
|
63
81
|
name: "flowseeker_files",
|
|
64
82
|
description: "List top ranked files for a task. Lightweight result for quickly finding where to inspect or edit.",
|
|
@@ -103,6 +121,16 @@ exports.MCP_PROMPT_DEFINITIONS = [
|
|
|
103
121
|
{ name: "workspace", description: "Optional workspace root path." }
|
|
104
122
|
]
|
|
105
123
|
},
|
|
124
|
+
{
|
|
125
|
+
name: "auto",
|
|
126
|
+
description: "Create a reviewable implementation-plan prompt from relevant code context.",
|
|
127
|
+
toolName: "flowseeker_auto",
|
|
128
|
+
toolPurpose: "return an implementation-plan prompt with relevant files, edit guardrails, and verification steps",
|
|
129
|
+
arguments: [
|
|
130
|
+
{ name: "task", description: "Task, bug, or feature to prepare a plan for.", required: true },
|
|
131
|
+
{ name: "workspace", description: "Optional workspace root path." }
|
|
132
|
+
]
|
|
133
|
+
},
|
|
106
134
|
{
|
|
107
135
|
name: "files",
|
|
108
136
|
description: "List top ranked files for a task.",
|
|
@@ -127,6 +155,8 @@ async function handleToolCall(toolName, input, defaultWorkspace) {
|
|
|
127
155
|
return await handleRetrieve(task, workspace, input.maxFiles);
|
|
128
156
|
case "flowseeker_guide":
|
|
129
157
|
return await handleGuide(task, workspace);
|
|
158
|
+
case "flowseeker_auto":
|
|
159
|
+
return await handleAuto(task, workspace);
|
|
130
160
|
case "flowseeker_files":
|
|
131
161
|
return await handleFiles(task, workspace, input.limit);
|
|
132
162
|
default:
|
|
@@ -153,6 +183,27 @@ async function handleGuide(task, workspace) {
|
|
|
153
183
|
const summary = formatResultSummary(result);
|
|
154
184
|
return textResult(`${summary}\n\n${prompt}`);
|
|
155
185
|
}
|
|
186
|
+
async function handleAuto(task, workspace) {
|
|
187
|
+
const result = await runPipeline(task, workspace);
|
|
188
|
+
const prompt = (0, agentPromptHeadless_1.createAgentSolvePromptHeadless)(result);
|
|
189
|
+
const summary = formatResultSummary(result);
|
|
190
|
+
const autoInstructions = [
|
|
191
|
+
"# FlowSeeker Auto Plan Prompt",
|
|
192
|
+
"",
|
|
193
|
+
"You are using FlowSeeker evidence to prepare a code change.",
|
|
194
|
+
"First validate whether the retrieved files actually match the task.",
|
|
195
|
+
"Then produce a reviewable implementation plan before editing.",
|
|
196
|
+
"",
|
|
197
|
+
"Rules:",
|
|
198
|
+
"- Do not edit files outside Allowed Edit Files unless you explain why the retrieved scope is insufficient.",
|
|
199
|
+
"- If retrieval contains unrelated repositories or stale paths, stop and ask for the correct workspace/cache reset.",
|
|
200
|
+
"- Prefer the smallest coherent patch that satisfies the task.",
|
|
201
|
+
"- Include focused verification commands or manual checks.",
|
|
202
|
+
"- If you produce a patch, summarize changed files and residual risk.",
|
|
203
|
+
""
|
|
204
|
+
].join("\n");
|
|
205
|
+
return textResult(`${summary}\n\n${autoInstructions}\n${prompt}`);
|
|
206
|
+
}
|
|
156
207
|
async function handleFiles(task, workspace, limit) {
|
|
157
208
|
const result = await runPipeline(task, workspace);
|
|
158
209
|
const groups = (0, fileGroups_1.aggregateEvidenceFiles)(result.units).slice(0, limit ?? 15);
|
|
@@ -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.
|
|
16
|
+
let usedTokens = (0, text_1.fastTokenEstimate)(header);
|
|
17
17
|
const fileScope = renderFileScope(units);
|
|
18
18
|
output.push(fileScope);
|
|
19
|
-
usedTokens += (0, text_1.
|
|
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.
|
|
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 === "
|
|
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
|
-
|
|
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))
|