flowseeker 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/chat/nativeChatParticipant.js +1 -1
- package/dist/cli/main.js +19 -3
- package/dist/cli/runEvaluation.js +103 -0
- package/dist/eval/accuracyV2.js +483 -0
- package/dist/eval/goldenTask.js +192 -0
- package/dist/framework/laravel.js +177 -0
- package/dist/index/semanticChunkIndex.js +388 -0
- package/dist/index/workspaceIndex.js +339 -29
- package/dist/mcp/mcpTools.js +1627 -2
- package/dist/pipeline/contextBlueprint.js +15 -2
- package/dist/pipeline/fileScanner.js +116 -1
- package/dist/pipeline/fusionTrace.js +149 -0
- package/dist/pipeline/nodeScan.js +13 -2
- package/dist/pipeline/roleRefinement.js +62 -0
- package/dist/pipeline/runHeadless.js +25 -5
- package/dist/pipeline/runPipeline.js +2 -2
- package/dist/pipeline/solvePacket.js +112 -6
- package/dist/pipeline/taskUnderstanding.js +2 -2
- package/dist/ui/chatViewProvider.js +1 -1
- package/package.json +2 -2
|
@@ -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) {
|
|
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getGuardReasons = getGuardReasons;
|
|
37
|
+
exports.clearGuardReasons = clearGuardReasons;
|
|
36
38
|
exports.scanTextFile = scanTextFile;
|
|
37
39
|
exports.getWeightedTerms = getWeightedTerms;
|
|
38
40
|
const path = __importStar(require("path"));
|
|
@@ -48,12 +50,20 @@ const MAX_CHUNK_LINES = 120;
|
|
|
48
50
|
const MAX_SEARCHABLE_LINE_CHARS = 12000;
|
|
49
51
|
const MAX_STRUCTURED_MATCHED_LINES = 80;
|
|
50
52
|
const MAX_STRUCTURED_SCAN_BYTES = 220000;
|
|
53
|
+
const guardReasons = new Map();
|
|
54
|
+
function getGuardReasons() {
|
|
55
|
+
return guardReasons;
|
|
56
|
+
}
|
|
57
|
+
function clearGuardReasons() {
|
|
58
|
+
guardReasons.clear();
|
|
59
|
+
}
|
|
51
60
|
function scanTextFile(input) {
|
|
52
61
|
if (input.content.includes("\u0000")) {
|
|
53
62
|
return [];
|
|
54
63
|
}
|
|
55
64
|
const weightedTerms = getWeightedTerms(input.profile);
|
|
56
|
-
const
|
|
65
|
+
const slotBoostEnabled = process.env.FLOWSEEKER_ENABLE_SLOT_BOOST !== "0";
|
|
66
|
+
const pathScore = scorePath(input.relativePath, weightedTerms) + (slotBoostEnabled ? scorePathSlotBoost(input.relativePath, input.profile) : 0);
|
|
57
67
|
const matchedLines = findMatchedLines(input.content, weightedTerms, MAX_MATCHED_LINES_PER_FILE);
|
|
58
68
|
const hasOversizedLine = containsLineLongerThan(input.content, MAX_SEARCHABLE_LINE_CHARS);
|
|
59
69
|
const headContent = input.content.slice(0, 200000);
|
|
@@ -155,6 +165,44 @@ function scorePath(relativePath, terms) {
|
|
|
155
165
|
}
|
|
156
166
|
return score;
|
|
157
167
|
}
|
|
168
|
+
function scorePathSlotBoost(relativePath, profile) {
|
|
169
|
+
const requiredSlots = profile.blueprint.requiredSlots;
|
|
170
|
+
if (!requiredSlots || requiredSlots.length === 0)
|
|
171
|
+
return 0;
|
|
172
|
+
const fp = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
173
|
+
const normalizedText = (0, text_1.normalizeText)(relativePath);
|
|
174
|
+
const keywords = profile.keywords.map(text_1.normalizeText);
|
|
175
|
+
const slotTerms = {
|
|
176
|
+
entry: ["route", "routes", "router", "endpoint", "api"],
|
|
177
|
+
handler: ["controller", "handler", "command"],
|
|
178
|
+
domain: ["service", "usecase", "repository", "manager", "processor", "store", "hook"],
|
|
179
|
+
data: ["model", "schema", "migration", "entity", "query", "prisma"],
|
|
180
|
+
validation: ["request", "validator", "validation", "dto", "formrequest"],
|
|
181
|
+
permission: ["policy", "permission", "role", "guard", "middleware", "auth"],
|
|
182
|
+
config: ["config", "env", "setting", "yaml", "json"],
|
|
183
|
+
side_effect: ["event", "listener", "job", "queue", "mail", "email", "notification", "webhook", "dispatch"],
|
|
184
|
+
tests: ["test", "spec", "expect", "assert"],
|
|
185
|
+
ui: ["component", "page", "view", "template", "form"],
|
|
186
|
+
};
|
|
187
|
+
let maxBoost = 0;
|
|
188
|
+
for (const slot of requiredSlots) {
|
|
189
|
+
const slotKeywords = slotTerms[slot] ?? [];
|
|
190
|
+
for (const sk of slotKeywords) {
|
|
191
|
+
if (!fp.includes(sk))
|
|
192
|
+
continue;
|
|
193
|
+
// File path matches a required slot keyword — check if it also has task keyword overlap
|
|
194
|
+
for (const kw of keywords) {
|
|
195
|
+
if (normalizedText.includes(kw)) {
|
|
196
|
+
// File matches both a required slot keyword AND a task keyword — higher boost
|
|
197
|
+
maxBoost = Math.max(maxBoost, 12);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Pure slot match (no task keyword overlap) gets a smaller boost
|
|
201
|
+
maxBoost = Math.max(maxBoost, 5);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return maxBoost;
|
|
205
|
+
}
|
|
158
206
|
function findMatchedLines(content, terms, maxMatches) {
|
|
159
207
|
const lines = content.split(/\r?\n/);
|
|
160
208
|
const matches = new Map();
|
|
@@ -275,6 +323,7 @@ function detectKind(relativePath, snippet) {
|
|
|
275
323
|
function detectRole(kind, score, profile, relativePath, snippet) {
|
|
276
324
|
const normalizedPath = (0, text_1.normalizeText)(relativePath);
|
|
277
325
|
const normalizedSnippet = (0, text_1.normalizeText)(snippet);
|
|
326
|
+
const fp = relativePath.toLowerCase().replace(/\\/g, "/");
|
|
278
327
|
if (kind === "test") {
|
|
279
328
|
return "test";
|
|
280
329
|
}
|
|
@@ -321,6 +370,23 @@ function detectRole(kind, score, profile, relativePath, snippet) {
|
|
|
321
370
|
if (isFrontendStatePath(normalizedPath) && isPrimaryFlowPath(normalizedPath, normalizedSnippet, profile) && score >= 10) {
|
|
322
371
|
return "edit_candidate";
|
|
323
372
|
}
|
|
373
|
+
// Must-not guard: demote helper/command/feed/cross-domain files from edit_candidate.
|
|
374
|
+
// Runs before the runtime-flow check so high-scoring guard-pattern files are intercepted.
|
|
375
|
+
const mustNotGuardEnabled = process.env.FLOWSEEKER_ENABLE_MUST_NOT_GUARD !== "0";
|
|
376
|
+
if (mustNotGuardEnabled && score >= 5) {
|
|
377
|
+
const taskMentionsCmd = /\b(command|console|cli|cron|artisan)\b/.test(profile.normalizedTask || "");
|
|
378
|
+
const isCommand = /\b(console\/commands?|commands?\/console|management\/commands?)\b/.test(fp) && !taskMentionsCmd;
|
|
379
|
+
const taskMentionsFeed = /\b(feed|digest|sitemap|seo|rss|atom)\b/.test(profile.normalizedTask || "");
|
|
380
|
+
const isFeedLike = /\b(feed|digest|sitemap|seo|rss|atom)\b/.test(fp) && !taskMentionsFeed;
|
|
381
|
+
const isHelper = /\b(helpers?|utils?|vendor)\b/.test(fp) && !/\b(helpers?|utils?|vendor)\b/.test(profile.normalizedTask || "");
|
|
382
|
+
const taskConceptsLower = profile.concepts.map(c => (0, text_1.normalizeText)(c));
|
|
383
|
+
const isCrossDomainService = /\bservices?\b/.test(fp) && !taskConceptsLower.some(c => fp.includes(c)) && !/\bservices?\b/.test(profile.normalizedTask || "");
|
|
384
|
+
if (isHelper || isCommand || isFeedLike || isCrossDomainService) {
|
|
385
|
+
const cat = isHelper ? "helper_like" : isCommand ? "command_like" : isFeedLike ? "feed_like" : "cross_domain_service_like";
|
|
386
|
+
guardReasons.set(relativePath, cat);
|
|
387
|
+
return "read_context";
|
|
388
|
+
}
|
|
389
|
+
}
|
|
324
390
|
if (isRuntimeFlowPath(normalizedPath, normalizedSnippet) && isPrimaryFlowPath(normalizedPath, normalizedSnippet, profile) && score >= 10) {
|
|
325
391
|
return "edit_candidate";
|
|
326
392
|
}
|
|
@@ -356,6 +422,7 @@ function kindBonus(relativePath, snippet) {
|
|
|
356
422
|
function pathIntentModifier(relativePath, snippet, profile) {
|
|
357
423
|
const normalizedPath = (0, text_1.normalizeText)(relativePath);
|
|
358
424
|
const normalizedSnippet = (0, text_1.normalizeText)(snippet);
|
|
425
|
+
const editRecallBoost = process.env.FLOWSEEKER_ENABLE_EDIT_RECALL_BOOST !== "0";
|
|
359
426
|
let modifier = 0;
|
|
360
427
|
const pathTermHits = countTaskTermHits(normalizedPath, profile.concepts);
|
|
361
428
|
const snippetTermHits = countTaskTermHits(normalizedSnippet, profile.concepts);
|
|
@@ -371,6 +438,29 @@ function pathIntentModifier(relativePath, snippet, profile) {
|
|
|
371
438
|
if (entityPathHits > 0) {
|
|
372
439
|
modifier += 28 + Math.min(28, entityPathHits * 10);
|
|
373
440
|
}
|
|
441
|
+
// Filename keyword hit: task keyword appears as a filename token
|
|
442
|
+
// Uses token matching so "order" matches "order_controller" but "data" doesn't match "database"
|
|
443
|
+
const fpSegments = normalizedPath.replace(/\\/g, "/").split("/");
|
|
444
|
+
const fileName = fpSegments[fpSegments.length - 1] || "";
|
|
445
|
+
if (editRecallBoost) {
|
|
446
|
+
const fileTokens = (0, text_1.splitIdentifier)(fileName);
|
|
447
|
+
const fileNameKeywordWeight = profile.keywords
|
|
448
|
+
.map(text_1.normalizeText)
|
|
449
|
+
.filter(k => k.length >= 3 && !genericPathTerms.has(k) && fileTokens.includes(k))
|
|
450
|
+
.reduce((sum, k) => sum + Math.min(k.length, 12), 0);
|
|
451
|
+
if (fileNameKeywordWeight > 0 && !isGenericUtilityPath(normalizedPath) && !isConfigPath(normalizedPath)) {
|
|
452
|
+
modifier += 22 + Math.min(38, fileNameKeywordWeight * 5);
|
|
453
|
+
}
|
|
454
|
+
// Directory segment keyword match: directory names matching task keywords
|
|
455
|
+
// Gives a small domain signal boost (e.g. "auth/" dir matching "auth" keyword)
|
|
456
|
+
const commonDirs = new Set(["src", "app", "lib", "test", "tests", "dist", "build", "node_modules", "public", "assets"]);
|
|
457
|
+
const dirSegments = fpSegments.slice(0, -1);
|
|
458
|
+
const dirKeywords = profile.keywords.map(text_1.normalizeText).filter(k => k.length >= 3 && !genericPathTerms.has(k));
|
|
459
|
+
const dirKeywordHits = dirSegments.filter(s => s.length >= 3 && !commonDirs.has(s) && dirKeywords.some(k => s.includes(k))).length;
|
|
460
|
+
if (dirKeywordHits > 0 && !isConfigPath(normalizedPath)) {
|
|
461
|
+
modifier += 5 + Math.min(15, dirKeywordHits * 5);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
374
464
|
if (taskTargetsUi(profile) && isUiSurfacePath(normalizedPath)) {
|
|
375
465
|
const focusedSnippetHits = countTaskTermHits(normalizedSnippet, focusedPathTerms(profile));
|
|
376
466
|
modifier += 8;
|
|
@@ -405,6 +495,31 @@ function pathIntentModifier(relativePath, snippet, profile) {
|
|
|
405
495
|
}
|
|
406
496
|
}
|
|
407
497
|
}
|
|
498
|
+
// Framework convention boost: penalize common/shared paths for auth/security tasks
|
|
499
|
+
// to prefer framework-local paths (src/auth/, app/Auth/) over generic (src/common/)
|
|
500
|
+
var frameworkConvBoostEnabled = process.env.FLOWSEEKER_ENABLE_FRAMEWORK_CONVENTION_BOOST !== "0";
|
|
501
|
+
if (frameworkConvBoostEnabled) {
|
|
502
|
+
var isAuthTask = /\b(auth|jwt|token|authentication|login|permission|guard|strategy|policy)\b/i.test(profile.normalizedTask || "");
|
|
503
|
+
var isCommonOrSharedPath = /(^|\/)(common|shared)(\/|$)/.test(relativePath.toLowerCase().replace(/\\/g, "/"));
|
|
504
|
+
if (isAuthTask && isCommonOrSharedPath) {
|
|
505
|
+
modifier -= 12;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Role intent boost: penalize files whose role conflicts with task intent
|
|
509
|
+
// e.g. event handlers demoted for endpoint tasks, consumers demoted for notify tasks
|
|
510
|
+
var roleIntentEnabled = process.env.FLOWSEEKER_ENABLE_ROLE_INTENT_BOOST !== "0";
|
|
511
|
+
if (roleIntentEnabled) {
|
|
512
|
+
var taskLower = profile.normalizedTask || "";
|
|
513
|
+
var isEndpointTask = /\b(endpoint|route|api|controller|request handler)\b/i.test(taskLower);
|
|
514
|
+
var isNotifyTask = /\b(notification|email|alert|send|notify|approved)\b/i.test(taskLower);
|
|
515
|
+
var fpForward = relativePath.toLowerCase().replace(/\\/g, "/");
|
|
516
|
+
var isEventHandler = /\bevents?\//.test(fpForward) && /\.(handler|listener)\./.test(fpForward);
|
|
517
|
+
var isConsumer = /\bconsumer\b/.test(fpForward);
|
|
518
|
+
if (isEndpointTask && isEventHandler)
|
|
519
|
+
modifier -= 10;
|
|
520
|
+
if (isNotifyTask && isConsumer)
|
|
521
|
+
modifier -= 10;
|
|
522
|
+
}
|
|
408
523
|
if (isFrontendStatePath(normalizedPath) && focusedPathHits > 0) {
|
|
409
524
|
modifier += 18 + Math.min(24, focusedPathHits * 8);
|
|
410
525
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RRF_CHANNELS = void 0;
|
|
4
|
+
exports.computeRrfTrace = computeRrfTrace;
|
|
5
|
+
// ── Channel definitions ──────────────────────────────────────────────────────
|
|
6
|
+
exports.RRF_CHANNELS = [
|
|
7
|
+
"lexical",
|
|
8
|
+
"path",
|
|
9
|
+
"filename",
|
|
10
|
+
"symbol",
|
|
11
|
+
"chunk",
|
|
12
|
+
"graph",
|
|
13
|
+
"framework_slot",
|
|
14
|
+
"semantic",
|
|
15
|
+
"test_adjacency",
|
|
16
|
+
"avoid_terms",
|
|
17
|
+
"noise_penalty",
|
|
18
|
+
];
|
|
19
|
+
const RRF_K = 60;
|
|
20
|
+
// Map evidence channel hits to RRF channels
|
|
21
|
+
function mapToRrfChannel(channel) {
|
|
22
|
+
const m = {
|
|
23
|
+
lexical_content: "lexical",
|
|
24
|
+
exact_literal: "lexical",
|
|
25
|
+
path: "path",
|
|
26
|
+
symbol: "symbol",
|
|
27
|
+
graph: "graph",
|
|
28
|
+
dependency: "graph",
|
|
29
|
+
semantic_vector: "semantic",
|
|
30
|
+
editor_context: "test_adjacency",
|
|
31
|
+
};
|
|
32
|
+
return m[channel] ?? null;
|
|
33
|
+
}
|
|
34
|
+
function computeRrfTrace(result) {
|
|
35
|
+
const units = result.units;
|
|
36
|
+
if (units.length === 0)
|
|
37
|
+
return null;
|
|
38
|
+
// Collect channel hits from all units
|
|
39
|
+
const channelHits = new Map();
|
|
40
|
+
for (const unit of units) {
|
|
41
|
+
const hits = unit.channelHits ?? [];
|
|
42
|
+
for (const hit of hits) {
|
|
43
|
+
const rrfCh = mapToRrfChannel(hit.channel);
|
|
44
|
+
if (!rrfCh)
|
|
45
|
+
continue;
|
|
46
|
+
const existing = channelHits.get(rrfCh) ?? [];
|
|
47
|
+
existing.push(hit);
|
|
48
|
+
channelHits.set(rrfCh, existing);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Build per-channel rankings (by score, then rank)
|
|
52
|
+
const channelRankings = new Map();
|
|
53
|
+
for (const ch of exports.RRF_CHANNELS) {
|
|
54
|
+
const hits = channelHits.get(ch) ?? [];
|
|
55
|
+
const sorted = [...hits].sort((a, b) => (b.score - a.score) || ((a.rank ?? 99) - (b.rank ?? 99)));
|
|
56
|
+
const rankMap = new Map();
|
|
57
|
+
let rank = 1;
|
|
58
|
+
for (const hit of sorted) {
|
|
59
|
+
if (!rankMap.has(hit.relativePath)) {
|
|
60
|
+
rankMap.set(hit.relativePath, rank++);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
channelRankings.set(ch, rankMap);
|
|
64
|
+
}
|
|
65
|
+
// Compute RRF scores per file
|
|
66
|
+
const fileScores = new Map();
|
|
67
|
+
for (const ch of exports.RRF_CHANNELS) {
|
|
68
|
+
const chWeight = channelWeight(ch);
|
|
69
|
+
const ranking = channelRankings.get(ch);
|
|
70
|
+
if (!ranking || ranking.size === 0)
|
|
71
|
+
continue;
|
|
72
|
+
for (const [path, rank] of ranking) {
|
|
73
|
+
const contribution = chWeight / (RRF_K + rank);
|
|
74
|
+
const existing = fileScores.get(path) ?? { score: 0, contributions: {} };
|
|
75
|
+
existing.score += contribution;
|
|
76
|
+
existing.contributions[ch] = contribution;
|
|
77
|
+
fileScores.set(path, existing);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Build RRF-sorted file list
|
|
81
|
+
const rrfEntries = [];
|
|
82
|
+
for (const [path, data] of fileScores) {
|
|
83
|
+
// Find original rank from unit ordering
|
|
84
|
+
const origIdx = units.findIndex((u) => u.relativePath === path);
|
|
85
|
+
rrfEntries.push({
|
|
86
|
+
relativePath: path,
|
|
87
|
+
rrfScore: data.score,
|
|
88
|
+
channelContributions: data.contributions,
|
|
89
|
+
originalRank: origIdx >= 0 ? origIdx + 1 : 99,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
rrfEntries.sort((a, b) => b.rrfScore - a.rrfScore);
|
|
93
|
+
// Current ranking (from unit ordering)
|
|
94
|
+
const seenPaths = new Set();
|
|
95
|
+
const currentTopFiles = [];
|
|
96
|
+
for (const unit of units) {
|
|
97
|
+
if (!seenPaths.has(unit.relativePath)) {
|
|
98
|
+
seenPaths.add(unit.relativePath);
|
|
99
|
+
currentTopFiles.push(unit.relativePath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const rrfTopFiles = rrfEntries.map((e) => e.relativePath);
|
|
103
|
+
// Overlap computation
|
|
104
|
+
const overlapAt = (k) => {
|
|
105
|
+
const curSet = new Set(currentTopFiles.slice(0, k));
|
|
106
|
+
return rrfTopFiles.slice(0, k).filter((f) => curSet.has(f)).length;
|
|
107
|
+
};
|
|
108
|
+
// Promoted/demoted
|
|
109
|
+
const curTop10 = new Set(currentTopFiles.slice(0, 10));
|
|
110
|
+
const rrfTop10 = new Set(rrfTopFiles.slice(0, 10));
|
|
111
|
+
const promotedByRrf = rrfTopFiles.slice(0, 10).filter((f) => !curTop10.has(f));
|
|
112
|
+
const demotedByRrf = currentTopFiles.slice(0, 10).filter((f) => !rrfTop10.has(f));
|
|
113
|
+
// Channel traces
|
|
114
|
+
const channelTraces = exports.RRF_CHANNELS.map((ch) => ({
|
|
115
|
+
channel: ch,
|
|
116
|
+
active: (channelHits.get(ch)?.length ?? 0) > 0,
|
|
117
|
+
candidates: channelHits.get(ch)?.length ?? 0,
|
|
118
|
+
}));
|
|
119
|
+
return {
|
|
120
|
+
enabled: true,
|
|
121
|
+
k: RRF_K,
|
|
122
|
+
currentTopFiles,
|
|
123
|
+
rrfTopFiles,
|
|
124
|
+
overlapAt3: overlapAt(3),
|
|
125
|
+
overlapAt5: overlapAt(5),
|
|
126
|
+
overlapAt10: overlapAt(10),
|
|
127
|
+
promotedByRrf,
|
|
128
|
+
demotedByRrf,
|
|
129
|
+
defaultRankingChanged: false, // RRF is report-only
|
|
130
|
+
channelTraces,
|
|
131
|
+
topRrfEntries: rrfEntries.slice(0, 10),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function channelWeight(ch) {
|
|
135
|
+
const weights = {
|
|
136
|
+
lexical: 1.0,
|
|
137
|
+
path: 0.8,
|
|
138
|
+
symbol: 1.0,
|
|
139
|
+
chunk: 0.7,
|
|
140
|
+
graph: 0.9,
|
|
141
|
+
framework_slot: 0.6,
|
|
142
|
+
semantic: 0.5,
|
|
143
|
+
test_adjacency: 0.4,
|
|
144
|
+
avoid_terms: -0.3,
|
|
145
|
+
noise_penalty: -0.5,
|
|
146
|
+
};
|
|
147
|
+
return weights[ch] ?? 0.5;
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=fusionTrace.js.map
|
|
@@ -138,7 +138,12 @@ async function nodeScanWorkspace(workspacePath, profile, config, options = {}) {
|
|
|
138
138
|
semanticFallbackUsed: discovery.semanticRetrievalStatus === "error_fallback",
|
|
139
139
|
deepCandidateDiscoveryCandidates: discovery.deepCandidateDiscoveryCandidates ?? 0,
|
|
140
140
|
deepCandidateDiscoveryAdded: discovery.deepCandidateDiscoveryAdded ?? 0,
|
|
141
|
-
deepCandidateDiscoveryAvoidBlocked: discovery.deepCandidateDiscoveryAvoidBlocked ?? 0
|
|
141
|
+
deepCandidateDiscoveryAvoidBlocked: discovery.deepCandidateDiscoveryAvoidBlocked ?? 0,
|
|
142
|
+
// Chunk stats (15D)
|
|
143
|
+
totalChunks: discovery.totalChunks ?? 0,
|
|
144
|
+
chunksByKind: discovery.chunksByKind ?? {},
|
|
145
|
+
filesWithChunks: discovery.filesWithChunks ?? 0,
|
|
146
|
+
parserKindCounts: discovery.parserKindCounts ?? {},
|
|
142
147
|
}
|
|
143
148
|
};
|
|
144
149
|
}
|
|
@@ -174,6 +179,7 @@ async function discoverScanFiles(rootPath, profile, config, startedAt, options)
|
|
|
174
179
|
const files = candidates.length > 0
|
|
175
180
|
? candidates
|
|
176
181
|
: fallbackIndexCandidates(rootPath, indexed.index.files, config.index.maxIndexedCandidateFiles);
|
|
182
|
+
const chunkStats = (0, workspaceIndex_1.computeChunkStats)(indexed.index);
|
|
177
183
|
return {
|
|
178
184
|
files,
|
|
179
185
|
stopReason: indexed.stopReason,
|
|
@@ -199,7 +205,12 @@ async function discoverScanFiles(rootPath, profile, config, startedAt, options)
|
|
|
199
205
|
semanticSupportedFileCount: selection.semanticSupportedFileCount ?? 0,
|
|
200
206
|
deepCandidateDiscoveryCandidates: deepCands,
|
|
201
207
|
deepCandidateDiscoveryAdded: deepAdded,
|
|
202
|
-
deepCandidateDiscoveryAvoidBlocked: deepBlocked
|
|
208
|
+
deepCandidateDiscoveryAvoidBlocked: deepBlocked,
|
|
209
|
+
// Chunk stats (15D)
|
|
210
|
+
totalChunks: chunkStats.totalChunks,
|
|
211
|
+
chunksByKind: chunkStats.chunksByKind,
|
|
212
|
+
filesWithChunks: chunkStats.filesWithChunks,
|
|
213
|
+
parserKindCounts: chunkStats.parserKindCounts,
|
|
203
214
|
};
|
|
204
215
|
}
|
|
205
216
|
function fallbackIndexCandidates(rootPath, files, maxFiles) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyRoleRefinement = applyRoleRefinement;
|
|
4
|
+
function applyRoleRefinement(units, profile, config) {
|
|
5
|
+
const mode = config.pipeline.roleRefinementMode ?? "off";
|
|
6
|
+
if (mode !== "conservative")
|
|
7
|
+
return { units, demotions: [] };
|
|
8
|
+
const taskLower = profile.normalizedTask || "";
|
|
9
|
+
const taskShape = profile.taskShape || "unknown";
|
|
10
|
+
const taskExplicitlyTargetsTests = /\b(test|spec|verify|assert)\b/i.test(taskLower);
|
|
11
|
+
const taskTargetsRouteConfig = /\b(route|router|endpoint|url|config|env|setting)\b/i.test(taskLower);
|
|
12
|
+
const taskTargetsViewUI = /\b(view|page|layout|render|blade|template|ui|frontend|modal|component)\b/i.test(taskLower);
|
|
13
|
+
const taskNeedsSideEffects = /\b(job|queue|event|listener|notify|notification|email|mail|dispatch|async|webhook|cron|command|console)\b/i.test(taskLower);
|
|
14
|
+
const taskNamedFiles = extractNamedFiles(taskLower);
|
|
15
|
+
const demotions = [];
|
|
16
|
+
const refined = units.map((unit) => {
|
|
17
|
+
if (unit.role !== "edit_candidate")
|
|
18
|
+
return unit;
|
|
19
|
+
const fp = (unit.relativePath || "").toLowerCase().replace(/\\/g, "/");
|
|
20
|
+
const taskMentionsFile = taskNamedFiles.some((n) => fp.includes(n));
|
|
21
|
+
// Rule 1: tests/specs → test
|
|
22
|
+
if (!taskExplicitlyTargetsTests && /(^|\/)(tests?|specs?|__tests__)(\/|$)/.test(fp)) {
|
|
23
|
+
demotions.push({ filePath: unit.relativePath, beforeRole: "edit_candidate", afterRole: "test", reason: "test path demoted — task does not target tests", ruleId: "demote_test", confidence: "high", taskShape });
|
|
24
|
+
return { ...unit, role: "test" };
|
|
25
|
+
}
|
|
26
|
+
// Rule 2: routes/config/views → read_context
|
|
27
|
+
if (!taskTargetsRouteConfig && /(^|\/)(routes?|router|config)(\/|$)/.test(fp)) {
|
|
28
|
+
demotions.push({ filePath: unit.relativePath, beforeRole: "edit_candidate", afterRole: "read_context", reason: "route/config demoted to read_context — task does not target routing/config", ruleId: "demote_route_config", confidence: "high", taskShape });
|
|
29
|
+
return { ...unit, role: "read_context" };
|
|
30
|
+
}
|
|
31
|
+
if (!taskTargetsViewUI && /(^|\/)resources\/views(\/|$)/.test(fp)) {
|
|
32
|
+
demotions.push({ filePath: unit.relativePath, beforeRole: "edit_candidate", afterRole: "read_context", reason: "view demoted to read_context — task does not target UI/view", ruleId: "demote_view", confidence: "high", taskShape });
|
|
33
|
+
return { ...unit, role: "read_context" };
|
|
34
|
+
}
|
|
35
|
+
// Rule 3: helpers/utilities → read_context
|
|
36
|
+
if (!taskMentionsFile && /(^|\/)(helpers?|utils?)(\/|$)/i.test(fp)) {
|
|
37
|
+
demotions.push({ filePath: unit.relativePath, beforeRole: "edit_candidate", afterRole: "read_context", reason: "helper/utility demoted to read_context — not directly named by task", ruleId: "demote_helper", confidence: "medium", taskShape });
|
|
38
|
+
return { ...unit, role: "read_context" };
|
|
39
|
+
}
|
|
40
|
+
// Rule 4: side effects → read_context unless task needs them (includes Console/Commands)
|
|
41
|
+
if (!taskNeedsSideEffects && /(^|\/)(Jobs|Events|Listeners|Mail|Notifications|Console\/Commands)(\/|$)/i.test(fp)) {
|
|
42
|
+
demotions.push({ filePath: unit.relativePath, beforeRole: "edit_candidate", afterRole: "read_context", reason: "side_effect file demoted to read_context — task does not need side effects", ruleId: "demote_side_effect", confidence: "high", taskShape });
|
|
43
|
+
return { ...unit, role: "read_context" };
|
|
44
|
+
}
|
|
45
|
+
// Rule 5: contrast/avoid → read_context
|
|
46
|
+
if (unit.contrastContext || unit.taskAvoidGuardFlag === "demoted" || unit.taskAvoidGuardFlag === "blocked_slot") {
|
|
47
|
+
demotions.push({ filePath: unit.relativePath, beforeRole: "edit_candidate", afterRole: "read_context", reason: "contrast/avoid file demoted — flagged by guard", ruleId: "demote_contrast_avoid", confidence: "high", taskShape });
|
|
48
|
+
return { ...unit, role: "read_context" };
|
|
49
|
+
}
|
|
50
|
+
return unit;
|
|
51
|
+
});
|
|
52
|
+
return { units: refined, demotions };
|
|
53
|
+
}
|
|
54
|
+
function extractNamedFiles(taskLower) {
|
|
55
|
+
const names = [];
|
|
56
|
+
const m = taskLower.match(/['"]?([\w./-]+\.(?:php|ts|js|py|java|rb|go|cs|kt|rs|dart))['"]?/g);
|
|
57
|
+
if (m)
|
|
58
|
+
for (const n of m)
|
|
59
|
+
names.push(n.replace(/['"]/g, "").toLowerCase());
|
|
60
|
+
return names;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=roleRefinement.js.map
|
|
@@ -7,14 +7,16 @@ const evidenceGraph_1 = require("./evidenceGraph");
|
|
|
7
7
|
const contextBlueprint_1 = require("./contextBlueprint");
|
|
8
8
|
const nodeScan_1 = require("./nodeScan");
|
|
9
9
|
const ranker_1 = require("./ranker");
|
|
10
|
+
const roleRefinement_1 = require("./roleRefinement");
|
|
10
11
|
const solvePacket_1 = require("./solvePacket");
|
|
11
12
|
const taskUnderstanding_1 = require("./taskUnderstanding");
|
|
12
13
|
const tokenSavings_1 = require("./tokenSavings");
|
|
14
|
+
const semanticChunkIndex_1 = require("../index/semanticChunkIndex");
|
|
13
15
|
async function runFlowSeekerHeadless(workspacePath, task, config, options = {}) {
|
|
14
16
|
const log = (stage, detail) => options.onStage?.(stage, detail);
|
|
15
17
|
log("task", task);
|
|
16
18
|
const tTaskStart = performance.now();
|
|
17
|
-
const profile = (0, taskUnderstanding_1.understandTask)(task, config);
|
|
19
|
+
const profile = await (0, taskUnderstanding_1.understandTask)(task, config, workspacePath);
|
|
18
20
|
const taskDurationMs = Math.round(performance.now() - tTaskStart);
|
|
19
21
|
log("scan", `${profile.intent} | ${profile.keywords.length} keywords`);
|
|
20
22
|
const tScanStart = performance.now();
|
|
@@ -47,27 +49,45 @@ async function runFlowSeekerHeadless(workspacePath, task, config, options = {})
|
|
|
47
49
|
const tRank2Start = performance.now();
|
|
48
50
|
const finalUnits = (0, ranker_1.rankEvidence)(graphBoosted, config, profile);
|
|
49
51
|
const rankFinalPassDurationMs = Math.round(performance.now() - tRank2Start);
|
|
52
|
+
// Conservative role demotion (16B, config-gated, default off)
|
|
53
|
+
const refinement = (0, roleRefinement_1.applyRoleRefinement)(finalUnits, profile, config);
|
|
54
|
+
const refinedUnits = refinement.units;
|
|
50
55
|
const tCtxStart = performance.now();
|
|
51
|
-
const contextPack = (0, contextPack_1.createContextPack)(task,
|
|
56
|
+
const contextPack = (0, contextPack_1.createContextPack)(task, refinedUnits, config);
|
|
52
57
|
const contextPackDurationMs = Math.round(performance.now() - tCtxStart);
|
|
53
58
|
const result = {
|
|
54
59
|
task,
|
|
55
60
|
profile,
|
|
56
|
-
units:
|
|
61
|
+
units: refinedUnits,
|
|
57
62
|
contextPack,
|
|
58
63
|
stats: {
|
|
59
64
|
...scan.stats,
|
|
60
65
|
rankedEvidenceCount: finalUnits.length,
|
|
61
66
|
maxCandidates: config.pipeline.maxCandidates,
|
|
62
|
-
capped: scan.stats.rawEvidenceCount > finalUnits.length
|
|
67
|
+
capped: scan.stats.rawEvidenceCount > finalUnits.length,
|
|
68
|
+
// Role refinement telemetry (16B/16D-R1) — needed before buildSolvePacket
|
|
69
|
+
roleRefinementMode: config.pipeline.roleRefinementMode ?? "off",
|
|
70
|
+
roleDemotions: refinement.demotions.length,
|
|
71
|
+
roleDemotionLog: refinement.demotions.slice(0, 100),
|
|
63
72
|
}
|
|
64
73
|
};
|
|
65
74
|
const tSolveStart = performance.now();
|
|
66
|
-
result.solvePacket = await (0, solvePacket_1.buildSolvePacket)(result, config);
|
|
75
|
+
result.solvePacket = await (0, solvePacket_1.buildSolvePacket)(result, config, workspacePath);
|
|
67
76
|
const solvePacketDurationMs = Math.round(performance.now() - tSolveStart);
|
|
68
77
|
const tPostStart = performance.now();
|
|
69
78
|
result.contextCoverage = (0, contextBlueprint_1.computeContextCoverage)(profile, finalUnits);
|
|
70
79
|
result.tokenSavings = (0, tokenSavings_1.computeTokenSavings)(result);
|
|
80
|
+
// Semantic chunk diagnostics (18A — report-only, disabled by default)
|
|
81
|
+
result.stats = {
|
|
82
|
+
...result.stats,
|
|
83
|
+
semanticChunkDiagnostic: (0, semanticChunkIndex_1.buildSemanticChunkDiagnostic)(config, {
|
|
84
|
+
chunksConsidered: 0, cacheHits: 0, cacheMisses: 0,
|
|
85
|
+
candidatesFound: 0, candidatesAccepted: 0, candidatesRejected: 0,
|
|
86
|
+
topK: 0, providerReady: false,
|
|
87
|
+
contributionReport: (0, semanticChunkIndex_1.buildContributionReport)(false, false, 0, 0),
|
|
88
|
+
hybridPromotionReport: (0, semanticChunkIndex_1.buildHybridPromotionReport)(false, false, [], [], []),
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
71
91
|
const postProcessDurationMs = Math.round(performance.now() - tPostStart);
|
|
72
92
|
const totalDurationMs = taskDurationMs + scanLoopDurationMs + rankFirstPassDurationMs + graphBuildDurationMs + frameworkExpansionDurationMs + graphBoostDurationMs + rankFinalPassDurationMs + contextPackDurationMs + solvePacketDurationMs + postProcessDurationMs;
|
|
73
93
|
result.stats = {
|
|
@@ -47,7 +47,7 @@ const tokenSavings_1 = require("./tokenSavings");
|
|
|
47
47
|
const universalScan_1 = require("./universalScan");
|
|
48
48
|
async function runFlowSeeker(task, options = {}) {
|
|
49
49
|
const config = await (0, loadConfig_1.loadConfig)();
|
|
50
|
-
const profile = (0, taskUnderstanding_1.understandTask)(task, config);
|
|
50
|
+
const profile = await (0, taskUnderstanding_1.understandTask)(task, config, process.cwd());
|
|
51
51
|
// Collect editor context for ranking bias (only in VS Code; null in headless).
|
|
52
52
|
const editorContext = options.editorContext ?? collectEditorContext();
|
|
53
53
|
options.progress?.report({ message: `Scanning workspace for "${task.slice(0, 60)}..."` });
|
|
@@ -85,7 +85,7 @@ async function runFlowSeeker(task, options = {}) {
|
|
|
85
85
|
capped: scan.stats.rawEvidenceCount > finalUnits.length
|
|
86
86
|
}
|
|
87
87
|
};
|
|
88
|
-
result.solvePacket = await (0, solvePacket_1.buildSolvePacket)(result, config);
|
|
88
|
+
result.solvePacket = await (0, solvePacket_1.buildSolvePacket)(result, config, process.cwd());
|
|
89
89
|
result.contextCoverage = (0, contextBlueprint_1.computeContextCoverage)(profile, finalUnits);
|
|
90
90
|
result.tokenSavings = (0, tokenSavings_1.computeTokenSavings)(result);
|
|
91
91
|
return result;
|