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.
@@ -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 pathScore = scorePath(input.relativePath, weightedTerms);
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, finalUnits, config);
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: finalUnits,
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;