scai 0.1.170 → 0.1.172

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.
@@ -10,6 +10,7 @@ import { fileCheckStep } from "./fileCheckStep.js";
10
10
  import { analysisPlanGenStep } from "./analysisPlanGenStep.js";
11
11
  import { readinessGateStep } from "./readinessGateStep.js";
12
12
  import { scopeClassificationStep } from "./scopeClassificationStep.js";
13
+ import { routingDecisionStep } from "./routingDecisionStep.js";
13
14
  import { evidenceVerifierStep } from "./evidenceVerifierStep.js";
14
15
  import { validateChangesStep } from './validateChangesStep.js';
15
16
  import { reasonNextTaskStep } from './reasonNextTaskStep.js';
@@ -19,8 +20,14 @@ import { selectRelevantSourcesStep } from "./selectRelevantSourcesStep.js";
19
20
  import { iterationFileSelector } from "./iterationFileSelector.js";
20
21
  import { finalAnswerModule } from "../pipeline/modules/finalAnswerModule.js";
21
22
  import { reasonNextStep } from "./reasonNextStep.js";
23
+ import { buildLightContext } from "../utils/buildContextualPrompt.js";
24
+ import { semanticSearchFiles } from "../db/fileIndex.js";
25
+ import { NUM_TOPFILES, RELATED_FILES_LIMIT } from "../constants.js";
22
26
  import { structuralPreloadStep } from "./structuralPreloadStep.js";
27
+ import { extractFileReferences } from "../utils/extractFileReferences.js";
28
+ import { PREFILTER_STOP_WORDS } from "../fileRules/stopWords.js";
23
29
  import chalk from "chalk";
30
+ import path from "path";
24
31
  /* ───────────────────────── registry ───────────────────────── */
25
32
  const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod]));
26
33
  function resolveModuleForAction(action) {
@@ -49,7 +56,6 @@ export class MainAgent {
49
56
  */
50
57
  constructor(context, ui) {
51
58
  this.runCount = 0;
52
- this.maxRuns = 2;
53
59
  this.context = context;
54
60
  this.query = context.initContext?.userQuery ?? "";
55
61
  this.ui = ui;
@@ -60,6 +66,7 @@ export class MainAgent {
60
66
  this.runCount = 0;
61
67
  await this.runBoot();
62
68
  await this.runScope();
69
+ await this.runInitialRetrieval();
63
70
  await this.runGrounding();
64
71
  await this.runWorkLoop();
65
72
  await this.runFinalize();
@@ -72,7 +79,6 @@ export class MainAgent {
72
79
  async runBoot() {
73
80
  var _a;
74
81
  await understandIntentStep.run({ context: this.context });
75
- await resolveExecutionModeStep.run(this.context);
76
82
  // Boot the task and get the real DB taskId
77
83
  this.taskId = bootTaskForRepo(this.context, getDbForRepo(), (phase, step, ms, desc) => this.logLine(phase, step, ms, desc, { highlight: true }));
78
84
  (_a = this.context).task || (_a.task = {
@@ -90,11 +96,38 @@ export class MainAgent {
90
96
  /* ───────────── scope ───────────── */
91
97
  async runScope() {
92
98
  await scopeClassificationStep.run(this.context);
99
+ await resolveExecutionModeStep.run(this.context);
100
+ await routingDecisionStep.run(this.context);
101
+ const routing = this.context.analysis?.routingDecision;
102
+ if (routing) {
103
+ this.logLine("TASK", "Routing decision", undefined, `${routing.decision} | search=${routing.allowSearch} | transform=${routing.allowTransform} | scopeLocked=${routing.scopeLocked}`);
104
+ }
93
105
  this.logLine("TASK", "Scope classification complete");
94
106
  }
107
+ /* ───────────── initial retrieval ───────────── */
108
+ async runInitialRetrieval() {
109
+ const { rawUserQuery, retrievalQuery } = this.resolveInitialRetrievalQueries();
110
+ const t = this.startTimer();
111
+ try {
112
+ const results = await this.fetchInitialRetrievalResults(retrievalQuery);
113
+ const promptArgs = this.buildInitialRetrievalPromptArgs(results, retrievalQuery);
114
+ const seededContext = await buildLightContext(promptArgs);
115
+ const mergedRelatedCount = this.mergeSeededInitialContext(rawUserQuery, seededContext);
116
+ const prefilter = this.applyDeterministicPreGroundingPrefilter(retrievalQuery);
117
+ this.logLine("ANALYSIS", "initialRetrieval", t(), `${results.length} result(s), ${mergedRelatedCount} candidate file(s), prefilter ${prefilter.before} -> ${prefilter.after}`);
118
+ }
119
+ catch (err) {
120
+ this.logLine("ANALYSIS", "initialRetrieval", t(), `failed: ${String(err)}`);
121
+ }
122
+ }
123
+ /* ───────────── grounding ───────────── */
95
124
  async runGrounding() {
96
125
  let ready = false;
97
- while (this.runCount < this.maxRuns) {
126
+ const maxGroundingWaves = this.getGroundingWaveBudget();
127
+ let groundingWave = 0;
128
+ while (groundingWave < maxGroundingWaves) {
129
+ groundingWave++;
130
+ this.logLine("ANALYSIS", "groundingWave", undefined, `wave ${groundingWave}/${maxGroundingWaves}`);
98
131
  // ---------------- EVIDENCE PIPELINE ----------------
99
132
  // -------- STRUCTURAL PRELOAD --------
100
133
  const t0 = this.startTimer();
@@ -112,7 +145,7 @@ export class MainAgent {
112
145
  // ---------------- READINESS GATE ----------------
113
146
  const t4 = this.startTimer();
114
147
  await readinessGateStep.run(this.context);
115
- this.logLine("HASINFO", "readinessGate", t4());
148
+ this.logLine("ANALYSIS", "readinessGate", t4());
116
149
  ready = this.context.analysis?.readiness?.decision === "ready";
117
150
  if (ready) {
118
151
  break;
@@ -135,82 +168,67 @@ export class MainAgent {
135
168
  }
136
169
  this.logLine("PLAN", "infoPlanGen", t(), undefined, { highlight: false });
137
170
  }
138
- this.runCount++;
139
171
  this.logLine("HASINFO", "Not ready — looping back to evidence collection", undefined, undefined, { highlight: false });
140
172
  }
173
+ const selectedFiles = this.context.analysis?.focus?.selectedFiles ?? [];
174
+ if (selectedFiles.length > 0) {
175
+ this.context.analysis.readiness.decision = "ready";
176
+ this.logLine("ANALYSIS", "readinessOverrideFromSelectedFiles", undefined, `${selectedFiles.length} selected file(s) available after grounding`);
177
+ }
141
178
  }
142
- /* ───────────── finalize ───────────── */
143
- async runFinalize() {
144
- await finalAnswerModule.run({ query: this.query, context: this.context });
145
- persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
146
- this.logLine("TASK", "Finalize complete", undefined, undefined, { highlight: false });
179
+ /**
180
+ * Resolves grounding wave budget from current routing metadata.
181
+ * Example: scope=repo-wide + decision=needs-info => 4 waves.
182
+ */
183
+ getGroundingWaveBudget() {
184
+ const scope = this.context.analysis?.scopeType ?? "repo-wide";
185
+ const decision = this.context.analysis?.routingDecision?.decision ?? "has-info";
186
+ const allowSearch = this.context.analysis?.routingDecision?.allowSearch ?? true;
187
+ let budget = 2;
188
+ if (!allowSearch || scope === "none")
189
+ budget = 1;
190
+ else if (scope === "single-file" && decision === "has-info")
191
+ budget = 2;
192
+ else if (scope === "multi-file")
193
+ budget = 3;
194
+ else if (scope === "repo-wide" && decision === "needs-info")
195
+ budget = 4;
196
+ this.logLine("ANALYSIS", "groundingBudget", undefined, `scope=${scope}, decision=${decision}, search=${allowSearch}, maxWaves=${budget}`);
197
+ return budget;
147
198
  }
148
199
  /* ───────────── work loop ───────────── */
149
200
  async runWorkLoop() {
150
- const readinessDecision = this.context.analysis?.readiness?.decision;
151
- const readinessConfidence = this.context.analysis?.readiness?.confidence ?? 0;
152
- if (readinessDecision !== "ready") {
153
- // ❌ Graceful fallback instead of throwing
154
- this.context.task.status = "deferred";
155
- this.context.task.reason = `Readiness not achieved (decision=${readinessDecision}, confidence=${readinessConfidence})`;
156
- persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
157
- this.logLine("TASK", `Cannot start work loop — agent needs more evidence to safely proceed`, undefined, `Readiness: ${readinessDecision}, Confidence: ${readinessConfidence}`, { highlight: true });
201
+ if (!this.isWorkLoopReady())
158
202
  return;
159
- }
160
- if (!this.context.task) {
161
- throw new Error("runWorkLoop: missing task");
162
- }
203
+ this.ensureTaskForWorkLoop();
163
204
  const MAX_TASK_STEPS = 5;
164
205
  let stepCount = 0;
165
206
  while (stepCount < MAX_TASK_STEPS &&
166
207
  this.context.task.status === "active") {
167
- await reasonNextTaskStep.run(this.context);
168
- const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
169
- // 🟡 Pause for user clarification
208
+ const nextAction = await this.resolveNextTaskAction();
170
209
  if (nextAction === "request-feedback") {
171
- this.context.task.status = "paused";
172
- persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
210
+ this.persistTaskStatus("paused");
173
211
  this.logLine("TASK", "Execution paused — awaiting user clarification", undefined, undefined, { highlight: false });
174
212
  return;
175
213
  }
176
- // 🟢 Completed task
177
214
  if (nextAction === "complete") {
178
- this.context.task.status = "completed";
179
- persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
215
+ this.persistTaskStatus("completed");
180
216
  this.logLine("TASK", "All selected files processed — task complete", undefined, undefined, { highlight: false });
181
217
  return;
182
218
  }
183
219
  const taskStep = await iterationFileSelector.run(this.context);
184
220
  if (!taskStep) {
185
- this.context.task.status = "completed";
186
- persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
221
+ this.persistTaskStatus("completed");
187
222
  this.logLine("TASK", "No eligible taskStep found — task complete", undefined, undefined, { highlight: false });
188
223
  return;
189
224
  }
190
- this.context.task.currentStep = taskStep;
191
225
  stepCount++;
192
- taskStep.taskId = this.taskId;
193
- taskStep.stepIndex = stepCount;
194
- taskStep.status = "pending";
195
- persistTaskStepInsert(taskStep, getDbForRepo());
196
- this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: true });
197
- taskStep.startTime = Date.now();
198
- persistTaskStepStart(taskStep, getDbForRepo());
226
+ this.startTaskStep(taskStep, stepCount);
199
227
  // ---------------------------
200
228
  // Step-level iterations
201
229
  // ---------------------------
202
230
  const stepAction = await this.runStepIterations(taskStep);
203
- if (stepAction === "complete") {
204
- taskStep.status = "completed";
205
- taskStep.endTime = Date.now();
206
- persistTaskStepCompletion(taskStep, getDbForRepo());
207
- this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: false });
208
- }
209
- else {
210
- taskStep.status = "pending";
211
- persistTaskStepCompletion(taskStep, getDbForRepo());
212
- this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined, taskStep.filePath);
213
- }
231
+ this.finishTaskStep(taskStep, stepCount, stepAction);
214
232
  }
215
233
  this.logLine("TASK", "Max task step limit reached — stopping work loop", undefined, undefined, { highlight: false });
216
234
  }
@@ -219,16 +237,17 @@ export class MainAgent {
219
237
  const MAX_ITERATIONS = 5;
220
238
  let loopCount = 0;
221
239
  const getNextIterationAction = () => {
222
- const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
223
- if (!["continue", "redo-step", "expand-scope", "request-feedback", "complete"].includes(nextAction ?? ""))
224
- return "complete";
240
+ const nextAction = taskStep.result?.stepReasoning?.nextAction;
241
+ if (!["continue", "redo-step", "expand-scope", "request-feedback", "complete"].includes(nextAction ?? "")) {
242
+ return "continue";
243
+ }
225
244
  return nextAction;
226
245
  };
227
246
  while (loopCount < MAX_ITERATIONS) {
228
247
  this.runCount++;
229
248
  loopCount++;
230
- if (this.context.analysis?.iterationReasoning)
231
- this.context.analysis.iterationReasoning.nextAction = undefined;
249
+ if (taskStep.result?.stepReasoning)
250
+ taskStep.result.stepReasoning.nextAction = undefined;
232
251
  await this.runWorkIteration(taskStep);
233
252
  const nextAction = getNextIterationAction();
234
253
  this.logLine("STEP-LOOP", `nextAction = ${nextAction}`);
@@ -236,6 +255,8 @@ export class MainAgent {
236
255
  return "complete";
237
256
  if (nextAction === "request-feedback")
238
257
  return "request-feedback";
258
+ if (nextAction === "redo-step")
259
+ continue;
239
260
  }
240
261
  return "continue";
241
262
  }
@@ -307,14 +328,189 @@ export class MainAgent {
307
328
  }
308
329
  try {
309
330
  this.ui.update(`Running step: ${step.action}`);
310
- await mod.run({ query: step.description ?? input.query, content: input.data ?? input.content, context: this.context });
311
- return { query: step.description ?? input.query, data: {} };
331
+ const output = await mod.run({ query: step.description ?? input.query, content: input.data ?? input.content, context: this.context });
332
+ const errors = Array.isArray(output.data?.errors)
333
+ ? output.data.errors.filter((e) => typeof e === "string" && e.trim().length > 0)
334
+ : [];
335
+ if (errors.length > 0) {
336
+ const detail = errors.slice(0, 2).join(" | ");
337
+ this.logLine("EXECUTE", step.action, stop(), `completed with errors: ${detail}`);
338
+ console.error(`[${step.action}] ${errors.join(" | ")}`);
339
+ }
340
+ return output;
312
341
  }
313
342
  catch (err) {
314
343
  this.logLine("EXECUTE", step.action, stop(), "failed");
315
344
  throw err;
316
345
  }
317
346
  }
347
+ /* ───────────── finalize ───────────── */
348
+ async runFinalize() {
349
+ await finalAnswerModule.run({ query: this.query, context: this.context });
350
+ persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
351
+ this.logLine("TASK", "Finalize complete", undefined, undefined, { highlight: false });
352
+ }
353
+ /* ───────────── extracted from runInitialRetrieval ───────────── */
354
+ resolveInitialRetrievalQueries() {
355
+ const rawUserQuery = this.context.initContext?.userQuery ?? this.query;
356
+ const retrievalQuery = this.context.analysis?.intent?.normalizedQuery?.trim() || rawUserQuery;
357
+ return { rawUserQuery, retrievalQuery };
358
+ }
359
+ async fetchInitialRetrievalResults(retrievalQuery) {
360
+ return semanticSearchFiles(retrievalQuery, RELATED_FILES_LIMIT, this.context.analysis?.intent ?? {});
361
+ }
362
+ mapSearchResultToTopFile(result) {
363
+ return {
364
+ id: result.id,
365
+ path: result.path,
366
+ summary: result.summary ?? undefined,
367
+ bm25Score: result.bm25Score,
368
+ };
369
+ }
370
+ mapSearchResultToRelatedFile(result) {
371
+ return {
372
+ id: result.id,
373
+ path: result.path,
374
+ summary: result.summary ?? undefined,
375
+ bm25Score: result.bm25Score,
376
+ };
377
+ }
378
+ buildInitialRetrievalPromptArgs(results, retrievalQuery) {
379
+ const topFiles = results
380
+ .slice(0, NUM_TOPFILES)
381
+ .map(result => this.mapSearchResultToTopFile(result));
382
+ const relatedFiles = results
383
+ .slice(NUM_TOPFILES)
384
+ .map(result => this.mapSearchResultToRelatedFile(result));
385
+ const queryExpansionTerms = results.find(result => Array.isArray(result.queryExpansionTerms))?.queryExpansionTerms;
386
+ return {
387
+ topFiles,
388
+ relatedFiles,
389
+ query: retrievalQuery,
390
+ queryExpansionTerms,
391
+ };
392
+ }
393
+ mergeSeededInitialContext(rawUserQuery, seededContext) {
394
+ const existingInit = this.context.initContext ?? { userQuery: rawUserQuery };
395
+ const seededInit = seededContext.initContext;
396
+ const mergedRelatedFiles = Array.from(new Set([
397
+ ...(existingInit.relatedFiles ?? []),
398
+ ...(seededInit?.relatedFiles ?? []),
399
+ ]));
400
+ const mergedScores = {
401
+ ...(existingInit.relatedFileScores ?? {}),
402
+ ...(seededInit?.relatedFileScores ?? {}),
403
+ };
404
+ const mergedQueryExpansionTerms = Array.from(new Set([
405
+ ...(existingInit.queryExpansionTerms ?? []),
406
+ ...(seededInit?.queryExpansionTerms ?? []),
407
+ ]));
408
+ this.context.initContext = {
409
+ ...existingInit,
410
+ ...(seededInit ?? {}),
411
+ userQuery: rawUserQuery,
412
+ relatedFiles: mergedRelatedFiles,
413
+ relatedFileScores: mergedScores,
414
+ queryExpansionTerms: mergedQueryExpansionTerms,
415
+ folderCapsules: (seededInit?.folderCapsules?.length
416
+ ? seededInit.folderCapsules
417
+ : existingInit.folderCapsules) ?? [],
418
+ };
419
+ return mergedRelatedFiles.length;
420
+ }
421
+ applyDeterministicPreGroundingPrefilter(retrievalQuery) {
422
+ const init = this.context.initContext;
423
+ if (!init?.relatedFiles?.length)
424
+ return { before: 0, after: 0 };
425
+ const before = init.relatedFiles.length;
426
+ const scored = scoreCandidateFiles(init.relatedFiles, init.relatedFileScores ?? {}, retrievalQuery);
427
+ if (scored.length === 0)
428
+ return { before, after: before };
429
+ const scope = this.context.analysis?.scopeType ?? "repo-wide";
430
+ const baseKeepCount = scope === "single-file" ? 6 : 10;
431
+ const keepCount = Math.min(Math.max(3, baseKeepCount), scored.length);
432
+ const selected = new Set(scored
433
+ .slice(0, keepCount)
434
+ .map(item => item.filePath));
435
+ for (const item of scored) {
436
+ if (item.reasons.includes("exact-filename") || item.reasons.includes("path-anchor")) {
437
+ selected.add(item.filePath);
438
+ }
439
+ }
440
+ init.relatedFiles = scored
441
+ .filter(item => selected.has(item.filePath))
442
+ .map(item => item.filePath);
443
+ init.relatedFileScores = Object.fromEntries(Object.entries(init.relatedFileScores ?? {})
444
+ .filter(([filePath]) => selected.has(filePath)));
445
+ logInputOutput("deterministicPreGroundingPrefilter", "output", {
446
+ retrievalQuery,
447
+ scope,
448
+ before,
449
+ after: init.relatedFiles.length,
450
+ keepCount,
451
+ files: scored.map(item => ({
452
+ file: item.filePath,
453
+ score: Number(item.score.toFixed(2)),
454
+ bm25Raw: item.bm25Raw,
455
+ kept: selected.has(item.filePath),
456
+ reasons: item.reasons,
457
+ })),
458
+ });
459
+ return { before, after: init.relatedFiles.length };
460
+ }
461
+ /* ───────────── extracted from runWorkLoop ───────────── */
462
+ isWorkLoopReady() {
463
+ const readinessDecision = this.context.analysis?.readiness?.decision;
464
+ const readinessConfidence = this.context.analysis?.readiness?.confidence ?? 0;
465
+ if (readinessDecision === "ready")
466
+ return true;
467
+ this.context.task.status = "deferred";
468
+ this.context.task.reason = `Readiness not achieved (decision=${readinessDecision}, confidence=${readinessConfidence})`;
469
+ this.persistTaskDataForRun();
470
+ this.logLine("TASK", "Cannot start work loop — agent needs more evidence to safely proceed", undefined, `Readiness: ${readinessDecision}, Confidence: ${readinessConfidence}`, { highlight: true });
471
+ return false;
472
+ }
473
+ ensureTaskForWorkLoop() {
474
+ if (!this.context.task) {
475
+ throw new Error("runWorkLoop: missing task");
476
+ }
477
+ }
478
+ async resolveNextTaskAction() {
479
+ await reasonNextTaskStep.run(this.context);
480
+ const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
481
+ if (nextAction === "request-feedback" || nextAction === "complete")
482
+ return nextAction;
483
+ return "continue";
484
+ }
485
+ persistTaskDataForRun() {
486
+ persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
487
+ }
488
+ persistTaskStatus(status) {
489
+ this.context.task.status = status;
490
+ this.persistTaskDataForRun();
491
+ }
492
+ startTaskStep(taskStep, stepCount) {
493
+ this.context.task.currentStep = taskStep;
494
+ taskStep.taskId = this.taskId;
495
+ taskStep.stepIndex = stepCount;
496
+ taskStep.status = "pending";
497
+ persistTaskStepInsert(taskStep, getDbForRepo());
498
+ this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: true });
499
+ taskStep.startTime = Date.now();
500
+ persistTaskStepStart(taskStep, getDbForRepo());
501
+ }
502
+ finishTaskStep(taskStep, stepCount, stepAction) {
503
+ taskStep.endTime = Date.now();
504
+ if (stepAction === "complete") {
505
+ taskStep.status = "completed";
506
+ persistTaskStepCompletion(taskStep, getDbForRepo());
507
+ this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: false });
508
+ return;
509
+ }
510
+ taskStep.status = "pending";
511
+ persistTaskStepCompletion(taskStep, getDbForRepo());
512
+ this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined, taskStep.filePath);
513
+ }
318
514
  /* ───────────── execution gates ───────────── */
319
515
  /**
320
516
  * Determines whether a phase can be executed based on execution mode and constraints.
@@ -403,6 +599,66 @@ export class MainAgent {
403
599
  });
404
600
  }
405
601
  }
602
+ function scoreCandidateFiles(filePaths, relatedFileScores, retrievalQuery) {
603
+ const explicitRefs = extractFileReferences(retrievalQuery, { lowercase: true });
604
+ const explicitBasenames = new Set(explicitRefs.map(ref => path.basename(ref)));
605
+ const symbolAnchors = extractSymbolAnchors(retrievalQuery);
606
+ const scored = filePaths.map(filePath => {
607
+ const fileLower = filePath.toLowerCase();
608
+ const basename = path.basename(filePath).toLowerCase();
609
+ let score = 0;
610
+ const reasons = [];
611
+ if (explicitBasenames.has(basename)) {
612
+ score += 100;
613
+ reasons.push("exact-filename");
614
+ }
615
+ if (explicitRefs.some(ref => fileLower.includes(ref))) {
616
+ score += 60;
617
+ reasons.push("path-anchor");
618
+ }
619
+ for (const anchor of symbolAnchors) {
620
+ if (basename.includes(anchor)) {
621
+ score += 40;
622
+ reasons.push(`symbol:${anchor}:filename`);
623
+ }
624
+ else if (fileLower.includes(anchor)) {
625
+ score += 20;
626
+ reasons.push(`symbol:${anchor}:path`);
627
+ }
628
+ }
629
+ const bm25Raw = relatedFileScores[filePath];
630
+ if (typeof bm25Raw === "number" && Number.isFinite(bm25Raw)) {
631
+ const prior = Math.max(0, Math.min(20, -bm25Raw));
632
+ score += prior;
633
+ reasons.push(`bm25-prior:${prior.toFixed(2)}`);
634
+ }
635
+ return { filePath, score, bm25Raw, reasons };
636
+ });
637
+ return scored.sort((a, b) => {
638
+ if (b.score !== a.score)
639
+ return b.score - a.score;
640
+ const aBm25 = typeof a.bm25Raw === "number" ? a.bm25Raw : Number.POSITIVE_INFINITY;
641
+ const bBm25 = typeof b.bm25Raw === "number" ? b.bm25Raw : Number.POSITIVE_INFINITY;
642
+ return aBm25 - bBm25;
643
+ });
644
+ }
645
+ function extractSymbolAnchors(query) {
646
+ const matches = query.match(/[A-Za-z_][A-Za-z0-9_]{2,}/g) ?? [];
647
+ const out = new Set();
648
+ for (const token of matches) {
649
+ const lowered = token.toLowerCase();
650
+ const looksLikeSymbol = /[A-Z]/.test(token) ||
651
+ token.includes("_") ||
652
+ token.endsWith("Step") ||
653
+ token.endsWith("Module");
654
+ if (!looksLikeSymbol)
655
+ continue;
656
+ if (PREFILTER_STOP_WORDS.has(lowered))
657
+ continue;
658
+ out.add(lowered);
659
+ }
660
+ return Array.from(out);
661
+ }
406
662
  // All helper functions (persistTaskData, bootTaskForRepo, persistTaskStep*) remain unchanged
407
663
  /* ───────────── FOLDER CAPSULES SUMMARY HELPER ───────────── */
408
664
  /**