scai 0.1.165 → 0.1.167
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/dist/agents/MainAgent.js +52 -114
- package/dist/agents/evidenceVerifierStep.js +105 -38
- package/dist/agents/fileCheckStep.js +42 -7
- package/dist/agents/reasonNextTaskStep.js +81 -1
- package/dist/agents/selectRelevantSourcesStep.js +36 -77
- package/dist/agents/writeFileStep.js +1 -1
- package/dist/db/fileIndex.js +91 -146
- package/dist/pipeline/modules/finalAnswerModule.js +16 -4
- package/package.json +1 -1
package/dist/agents/MainAgent.js
CHANGED
|
@@ -19,18 +19,9 @@ import { selectRelevantSourcesStep } from "./selectRelevantSourcesStep.js";
|
|
|
19
19
|
import { iterationFileSelector } from "./iterationFileSelector.js";
|
|
20
20
|
import { finalAnswerModule } from "../pipeline/modules/finalAnswerModule.js";
|
|
21
21
|
import { reasonNextStep } from "./reasonNextStep.js";
|
|
22
|
+
import chalk from "chalk";
|
|
22
23
|
/* ───────────────────────── registry ───────────────────────── */
|
|
23
|
-
/**
|
|
24
|
-
* Registry mapping action names to their corresponding Module implementations.
|
|
25
|
-
* This registry is populated with built-in modules from the module registry.
|
|
26
|
-
*/
|
|
27
24
|
const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod]));
|
|
28
|
-
/**
|
|
29
|
-
* Resolves a module by its action name.
|
|
30
|
-
*
|
|
31
|
-
* @param action - The action name to resolve.
|
|
32
|
-
* @returns The Module implementation if found, otherwise undefined.
|
|
33
|
-
*/
|
|
34
25
|
function resolveModuleForAction(action) {
|
|
35
26
|
return MODULE_REGISTRY[action];
|
|
36
27
|
}
|
|
@@ -66,7 +57,6 @@ export class MainAgent {
|
|
|
66
57
|
async run() {
|
|
67
58
|
this.runCount = 0;
|
|
68
59
|
await this.runBoot();
|
|
69
|
-
await this.runPrecheck();
|
|
70
60
|
await this.runScope();
|
|
71
61
|
await this.runGrounding();
|
|
72
62
|
await this.runWorkLoop();
|
|
@@ -78,27 +68,23 @@ export class MainAgent {
|
|
|
78
68
|
await understandIntentStep.run({ context: this.context });
|
|
79
69
|
await resolveExecutionModeStep.run(this.context);
|
|
80
70
|
// Boot the task and get the real DB taskId
|
|
81
|
-
this.taskId = bootTaskForRepo(this.context, getDbForRepo(), this.logLine
|
|
82
|
-
// Ensure context.task exists and has all required fields
|
|
71
|
+
this.taskId = bootTaskForRepo(this.context, getDbForRepo(), (phase, step, ms, desc) => this.logLine(phase, step, ms, desc, { highlight: true }));
|
|
83
72
|
(_a = this.context).task || (_a.task = {
|
|
84
73
|
id: this.taskId,
|
|
85
|
-
projectId: 0,
|
|
74
|
+
projectId: 0,
|
|
86
75
|
status: "active",
|
|
87
76
|
initialQuery: this.context.initContext?.userQuery ?? "",
|
|
88
77
|
createdAt: new Date().toISOString(),
|
|
89
78
|
updatedAt: new Date().toISOString(),
|
|
90
79
|
taskSteps: [],
|
|
91
80
|
});
|
|
92
|
-
// If task existed but id wasn’t set, set it now
|
|
93
81
|
this.context.task.id = this.taskId;
|
|
94
|
-
|
|
95
|
-
/* ───────────── precheck ───────────── */
|
|
96
|
-
async runPrecheck() {
|
|
97
|
-
await fileCheckStep(this.context);
|
|
82
|
+
this.logLine("TASK", "Boot complete", undefined, `taskId=${this.taskId}`, { highlight: true });
|
|
98
83
|
}
|
|
99
84
|
/* ───────────── scope ───────────── */
|
|
100
85
|
async runScope() {
|
|
101
86
|
await scopeClassificationStep.run(this.context);
|
|
87
|
+
this.logLine("TASK", "Scope classification complete");
|
|
102
88
|
}
|
|
103
89
|
/* ───────────── grounding / info acquisition loop ───────────── */
|
|
104
90
|
async runGrounding() {
|
|
@@ -108,48 +94,44 @@ export class MainAgent {
|
|
|
108
94
|
if (this.canExecutePhase("planning") &&
|
|
109
95
|
this.canExecuteScope("planning")) {
|
|
110
96
|
const t = this.startTimer();
|
|
111
|
-
// Generate info plan step(s) in context
|
|
112
97
|
await infoPlanGenStep.run(this.context);
|
|
113
98
|
const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
114
99
|
for (const step of infoPlan.steps) {
|
|
115
100
|
const stepIO = { query: this.query };
|
|
116
101
|
await this.executeStep(step, stepIO);
|
|
117
|
-
// Re-check any new files discovered
|
|
118
|
-
const t2 = this.startTimer();
|
|
119
|
-
await fileCheckStep(this.context);
|
|
120
|
-
this.logLine("PRECHECK", "postFileSearch", t2());
|
|
121
102
|
}
|
|
122
|
-
this.logLine("PLAN", "infoPlanGen", t());
|
|
103
|
+
this.logLine("PLAN", "infoPlanGen", t(), undefined, { highlight: false });
|
|
123
104
|
}
|
|
124
105
|
// ---------------- DETERMINISTIC EVIDENCE VERIFICATION ----------------
|
|
125
106
|
const t1 = this.startTimer();
|
|
126
107
|
await evidenceVerifierStep.run({ query: this.query, context: this.context });
|
|
127
|
-
this.logLine("ANALYSIS", "collectAnalysisEvidence", t1()
|
|
128
|
-
|
|
108
|
+
this.logLine("ANALYSIS", "collectAnalysisEvidence", t1());
|
|
109
|
+
/* ───────────── precheck ───────────── */
|
|
129
110
|
const t2 = this.startTimer();
|
|
111
|
+
await fileCheckStep(this.context);
|
|
112
|
+
this.logLine("ANALYSIS", "fileCheckStep", t2());
|
|
113
|
+
// Select grounded candidate files
|
|
114
|
+
const t3 = this.startTimer();
|
|
130
115
|
await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
|
|
131
|
-
this.logLine("ANALYSIS", "selectRelevantSources",
|
|
116
|
+
this.logLine("ANALYSIS", "selectRelevantSources", t3());
|
|
132
117
|
// ---------------- READINESS GATE ----------------
|
|
133
|
-
const
|
|
118
|
+
const t4 = this.startTimer();
|
|
134
119
|
await readinessGateStep.run(this.context);
|
|
135
|
-
this.logLine("HASINFO", "readinessGate",
|
|
120
|
+
this.logLine("HASINFO", "readinessGate", t4());
|
|
136
121
|
ready = this.context.analysis?.readiness?.decision === "ready";
|
|
137
122
|
if (!ready) {
|
|
138
123
|
this.runCount++;
|
|
139
|
-
|
|
140
|
-
if (this.context.analysis) {
|
|
124
|
+
if (this.context.analysis)
|
|
141
125
|
this.context.analysis.planSuggestion = undefined;
|
|
142
|
-
}
|
|
143
|
-
this.logLine("HASINFO", "readinessGate", undefined, "Not ready, looping back to information acquisition");
|
|
126
|
+
this.logLine("HASINFO", "Not ready — looping back to information acquisition", undefined, undefined, { highlight: false });
|
|
144
127
|
}
|
|
145
128
|
}
|
|
146
129
|
}
|
|
147
130
|
/* ───────────── finalize ───────────── */
|
|
148
131
|
async runFinalize() {
|
|
149
|
-
// generate final answer
|
|
150
132
|
await finalAnswerModule.run({ query: this.query, context: this.context });
|
|
151
|
-
// persist task and steps
|
|
152
133
|
persistTaskData(this.context, this.taskId, getDbForRepo(), this.logLine.bind(this));
|
|
134
|
+
this.logLine("TASK", "Finalize complete", undefined, undefined, { highlight: false });
|
|
153
135
|
}
|
|
154
136
|
/* ───────────── work loop ───────────── */
|
|
155
137
|
async runWorkLoop() {
|
|
@@ -159,29 +141,21 @@ export class MainAgent {
|
|
|
159
141
|
await reasonNextTaskStep.run(this.context);
|
|
160
142
|
const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
|
|
161
143
|
if (nextAction === "complete") {
|
|
162
|
-
this.logLine("TASK", "All selected files processed — task complete");
|
|
144
|
+
this.logLine("TASK", "All selected files processed — task complete", undefined, undefined, { highlight: false });
|
|
163
145
|
return;
|
|
164
146
|
}
|
|
165
147
|
const taskStep = await iterationFileSelector.run(this.context);
|
|
166
148
|
if (!taskStep) {
|
|
167
|
-
this.logLine("TASK", "No eligible taskStep found — task complete");
|
|
149
|
+
this.logLine("TASK", "No eligible taskStep found — task complete", undefined, undefined, { highlight: false });
|
|
168
150
|
return;
|
|
169
151
|
}
|
|
170
|
-
|
|
171
|
-
if (this.context.task) {
|
|
172
|
-
this.context.task.currentStep = taskStep;
|
|
173
|
-
}
|
|
152
|
+
this.context.task.currentStep = taskStep;
|
|
174
153
|
stepCount++;
|
|
175
154
|
taskStep.taskId = this.taskId;
|
|
176
155
|
taskStep.stepIndex = stepCount;
|
|
177
156
|
taskStep.status = "pending";
|
|
178
|
-
// ⬇️ Persist newly created step
|
|
179
157
|
persistTaskStepInsert(taskStep, getDbForRepo());
|
|
180
|
-
|
|
181
|
-
this.logLine("TASK", `Processing taskStep ${stepCount}/${MAX_TASK_STEPS}`, undefined, taskStep.filePath);
|
|
182
|
-
// ---------------------------
|
|
183
|
-
// Start the step
|
|
184
|
-
// ---------------------------
|
|
158
|
+
this.logLine("NEW STEP", `Processing taskStep ${stepCount}/${MAX_TASK_STEPS}`, undefined, taskStep.filePath, { highlight: true });
|
|
185
159
|
taskStep.startTime = Date.now();
|
|
186
160
|
persistTaskStepStart(taskStep, getDbForRepo());
|
|
187
161
|
// ---------------------------
|
|
@@ -192,14 +166,15 @@ export class MainAgent {
|
|
|
192
166
|
taskStep.status = "completed";
|
|
193
167
|
taskStep.endTime = Date.now();
|
|
194
168
|
persistTaskStepCompletion(taskStep, getDbForRepo());
|
|
169
|
+
this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined, taskStep.filePath, { highlight: false });
|
|
195
170
|
}
|
|
196
171
|
else {
|
|
197
|
-
// optionally mark as pending or redo-step
|
|
198
172
|
taskStep.status = "pending";
|
|
199
173
|
persistTaskStepCompletion(taskStep, getDbForRepo());
|
|
174
|
+
this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined, taskStep.filePath);
|
|
200
175
|
}
|
|
201
176
|
}
|
|
202
|
-
this.logLine("TASK", "Max task step limit reached — stopping work loop");
|
|
177
|
+
this.logLine("TASK", "Max task step limit reached — stopping work loop", undefined, undefined, { highlight: false });
|
|
203
178
|
}
|
|
204
179
|
/* ───────────── step iterations ───────────── */
|
|
205
180
|
async runStepIterations(taskStep) {
|
|
@@ -207,31 +182,22 @@ export class MainAgent {
|
|
|
207
182
|
let loopCount = 0;
|
|
208
183
|
const getNextIterationAction = () => {
|
|
209
184
|
const nextAction = this.context.analysis?.iterationReasoning?.nextAction;
|
|
210
|
-
if (nextAction
|
|
211
|
-
|
|
212
|
-
nextAction !== "expand-scope" &&
|
|
213
|
-
nextAction !== "request-feedback" &&
|
|
214
|
-
nextAction !== "complete") {
|
|
215
|
-
return "complete"; // fallback
|
|
216
|
-
}
|
|
185
|
+
if (!["continue", "redo-step", "expand-scope", "request-feedback", "complete"].includes(nextAction ?? ""))
|
|
186
|
+
return "complete";
|
|
217
187
|
return nextAction;
|
|
218
188
|
};
|
|
219
|
-
// Loop through iterations for a single task step (file)
|
|
220
189
|
while (loopCount < MAX_ITERATIONS) {
|
|
221
190
|
this.runCount++;
|
|
222
191
|
loopCount++;
|
|
223
|
-
|
|
224
|
-
if (this.context.analysis?.iterationReasoning) {
|
|
192
|
+
if (this.context.analysis?.iterationReasoning)
|
|
225
193
|
this.context.analysis.iterationReasoning.nextAction = undefined;
|
|
226
|
-
}
|
|
227
194
|
await this.runWorkIteration(taskStep);
|
|
228
195
|
const nextAction = getNextIterationAction();
|
|
229
|
-
this.logLine("STEP-LOOP",
|
|
196
|
+
this.logLine("STEP-LOOP", `nextAction = ${nextAction}`);
|
|
230
197
|
if (nextAction === "complete")
|
|
231
198
|
return "complete";
|
|
232
199
|
if (nextAction === "request-feedback")
|
|
233
200
|
return "request-feedback";
|
|
234
|
-
// else: continue
|
|
235
201
|
}
|
|
236
202
|
return "continue";
|
|
237
203
|
}
|
|
@@ -239,56 +205,47 @@ export class MainAgent {
|
|
|
239
205
|
async runWorkIteration(taskStep) {
|
|
240
206
|
if (!this.context.analysis)
|
|
241
207
|
this.context.analysis = {};
|
|
242
|
-
// ---------------- ANALYSIS ----------------
|
|
243
208
|
if (this.canExecutePhase("analysis") && this.canExecuteScope("analysis")) {
|
|
244
209
|
const tAnalysis = this.startTimer();
|
|
245
210
|
await analysisPlanGenStep.run(this.context);
|
|
246
|
-
this.logLine("PLAN", "analysisPlanGen", tAnalysis());
|
|
211
|
+
this.logLine("PLAN", "analysisPlanGen", tAnalysis(), undefined, { highlight: false });
|
|
247
212
|
const analysisPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
248
213
|
for (const step of analysisPlan.steps) {
|
|
249
214
|
const tStep = this.startTimer();
|
|
250
215
|
await this.executeStep(step, { query: this.query });
|
|
251
|
-
this.logLine("STEP", step.action || "unnamedStep", tStep());
|
|
216
|
+
this.logLine("PLANNING-STEP", step.action || "unnamedStep", tStep());
|
|
252
217
|
}
|
|
253
|
-
if (this.context.analysis)
|
|
218
|
+
if (this.context.analysis)
|
|
254
219
|
this.context.analysis.planSuggestion = undefined;
|
|
255
|
-
}
|
|
256
220
|
}
|
|
257
|
-
// ---------------- TRANSFORM ----------------
|
|
258
221
|
if (this.canExecutePhase("transform") && this.canExecuteScope("transform")) {
|
|
259
222
|
const tTransform = this.startTimer();
|
|
260
223
|
await transformPlanGenStep.run(this.context);
|
|
261
|
-
this.logLine("PLAN", "transformPlanGen", tTransform());
|
|
224
|
+
this.logLine("PLAN", "transformPlanGen", tTransform(), undefined, { highlight: false });
|
|
262
225
|
const transformPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
263
226
|
const firstStep = transformPlan.steps[0];
|
|
264
227
|
if (firstStep) {
|
|
265
228
|
const tStep = this.startTimer();
|
|
266
229
|
await this.executeStep(firstStep, { query: this.query });
|
|
267
|
-
this.logLine("STEP", `#1 (only) - ${firstStep.action || "unnamedStep"}`, tStep());
|
|
230
|
+
this.logLine("PLANNING-STEP", `#1 (only) - ${firstStep.action || "unnamedStep"}`, tStep());
|
|
268
231
|
}
|
|
269
|
-
if (this.context.analysis)
|
|
232
|
+
if (this.context.analysis)
|
|
270
233
|
this.context.analysis.planSuggestion = undefined;
|
|
271
|
-
}
|
|
272
|
-
// ---------------- WRITE ----------------
|
|
273
234
|
if (this.canExecutePhase("write") && this.canExecuteScope("write")) {
|
|
274
235
|
const tWrite = this.startTimer();
|
|
275
236
|
await writeFileStep.run({ query: this.query, context: this.context });
|
|
276
237
|
this.logLine("WRITE", "writeFileStep", tWrite());
|
|
277
238
|
}
|
|
278
|
-
// ---------------- VALIDATION ----------------
|
|
279
239
|
const tValidate = this.startTimer();
|
|
280
240
|
await validateChangesStep.run(this.context);
|
|
281
241
|
this.logLine("VALIDATION", "validateChangesStep", tValidate());
|
|
282
242
|
}
|
|
283
|
-
// ---------------- REASONING ----------------
|
|
284
243
|
const tReason = this.startTimer();
|
|
285
244
|
await reasonNextStep.run(this.context, taskStep);
|
|
286
245
|
this.logLine("REASONING", "reasonNextStep", tReason());
|
|
287
|
-
// ---------------- COLLABORATOR FEEDBACK ----------------
|
|
288
246
|
const tCollab = this.startTimer();
|
|
289
247
|
await collaboratorStep.run(this.context);
|
|
290
248
|
this.logLine("FEEDBACK", "collaboratorStep", tCollab());
|
|
291
|
-
// ---------------- INTEGRATE FEEDBACK ----------------
|
|
292
249
|
const tIntegrate = this.startTimer();
|
|
293
250
|
await integrateFeedbackStep.run(this.context);
|
|
294
251
|
this.logLine("FEEDBACK", "integrateFeedbackStep", tIntegrate());
|
|
@@ -304,9 +261,7 @@ export class MainAgent {
|
|
|
304
261
|
*/
|
|
305
262
|
async executeStep(step, input) {
|
|
306
263
|
const stop = this.startTimer();
|
|
307
|
-
// Set current step in context
|
|
308
264
|
this.context.currentStep = step;
|
|
309
|
-
// Resolve module for this action
|
|
310
265
|
const mod = resolveModuleForAction(step.action);
|
|
311
266
|
if (!mod) {
|
|
312
267
|
this.logLine("EXECUTE", step.action, stop(), "skipped (missing module)");
|
|
@@ -314,13 +269,7 @@ export class MainAgent {
|
|
|
314
269
|
}
|
|
315
270
|
try {
|
|
316
271
|
this.ui.update(`Running step: ${step.action}`);
|
|
317
|
-
|
|
318
|
-
await mod.run({
|
|
319
|
-
query: step.description ?? input.query,
|
|
320
|
-
content: input.data ?? input.content,
|
|
321
|
-
context: this.context
|
|
322
|
-
});
|
|
323
|
-
// Return ModuleIO (can remain minimal since output is untyped)
|
|
272
|
+
await mod.run({ query: step.description ?? input.query, content: input.data ?? input.content, context: this.context });
|
|
324
273
|
return { query: step.description ?? input.query, data: {} };
|
|
325
274
|
}
|
|
326
275
|
catch (err) {
|
|
@@ -342,12 +291,10 @@ export class MainAgent {
|
|
|
342
291
|
switch (phase) {
|
|
343
292
|
case "analysis":
|
|
344
293
|
case "planning":
|
|
345
|
-
// Read-only phases are always allowed
|
|
346
294
|
allowed = !docsOnly;
|
|
347
295
|
break;
|
|
348
296
|
case "transform":
|
|
349
297
|
case "write":
|
|
350
|
-
// Side-effect phases require explicit permission
|
|
351
298
|
allowed = constraints?.allowFileWrites ?? false;
|
|
352
299
|
break;
|
|
353
300
|
}
|
|
@@ -365,14 +312,10 @@ export class MainAgent {
|
|
|
365
312
|
let allowed = false;
|
|
366
313
|
switch (scope) {
|
|
367
314
|
case "none":
|
|
368
|
-
// Non-repo questions: analysis/planning only
|
|
369
315
|
allowed = phase === "analysis" || phase === "planning";
|
|
370
316
|
break;
|
|
371
|
-
|
|
372
|
-
case "multi-file":
|
|
373
|
-
case "repo-wide":
|
|
317
|
+
default:
|
|
374
318
|
allowed = true;
|
|
375
|
-
break;
|
|
376
319
|
}
|
|
377
320
|
return allowed;
|
|
378
321
|
}
|
|
@@ -396,38 +339,33 @@ export class MainAgent {
|
|
|
396
339
|
* @param fn - The function to execute while spinner is paused.
|
|
397
340
|
*/
|
|
398
341
|
withSpinnerPaused(fn) {
|
|
399
|
-
this.ui.pause(() => {
|
|
400
|
-
// Ensure spinner line is fully gone
|
|
401
|
-
process.stdout.write('\r\x1b[K');
|
|
402
|
-
fn();
|
|
403
|
-
});
|
|
342
|
+
this.ui.pause(() => { process.stdout.write('\r\x1b[K'); fn(); });
|
|
404
343
|
}
|
|
405
|
-
|
|
406
|
-
* Logs a line with timing information.
|
|
407
|
-
*
|
|
408
|
-
* @param phase - The phase of execution.
|
|
409
|
-
* @param step - The step being executed.
|
|
410
|
-
* @param ms - The execution time in milliseconds.
|
|
411
|
-
* @param desc - Optional description of the step.
|
|
412
|
-
*/
|
|
413
|
-
logLine(phase, step, ms, desc) {
|
|
344
|
+
logLine(phase, step, ms, desc, options) {
|
|
414
345
|
this.withSpinnerPaused(() => {
|
|
415
346
|
const suffix = desc ? ` — ${desc}` : "";
|
|
416
347
|
const timing = typeof ms === "number" ? ` (${ms}ms)` : "";
|
|
417
|
-
|
|
348
|
+
let line = `[AGENT] ${phase} :: ${step}${suffix}${timing}`;
|
|
349
|
+
// Coloring rules
|
|
350
|
+
if (phase === "NEW STEP" || phase === "STEP") {
|
|
351
|
+
line = chalk.green(line);
|
|
352
|
+
}
|
|
353
|
+
else if (options?.highlight) {
|
|
354
|
+
line = chalk.green(line);
|
|
355
|
+
}
|
|
356
|
+
// Print a blank line **before** NEW STEP only
|
|
357
|
+
if (phase === "NEW STEP")
|
|
358
|
+
console.log();
|
|
359
|
+
console.log(line);
|
|
418
360
|
});
|
|
419
361
|
}
|
|
420
|
-
/**
|
|
421
|
-
* Logs a message to the user output.
|
|
422
|
-
*
|
|
423
|
-
* @param message - The message to log.
|
|
424
|
-
*/
|
|
425
362
|
userOutput(message) {
|
|
426
363
|
this.withSpinnerPaused(() => {
|
|
427
364
|
console.log(`[USER OUTPUT] ${message}`);
|
|
428
365
|
});
|
|
429
366
|
}
|
|
430
367
|
}
|
|
368
|
+
// All helper functions (persistTaskData, bootTaskForRepo, persistTaskStep*) remain unchanged
|
|
431
369
|
/* ───────────── FOLDER CAPSULES SUMMARY HELPER ───────────── */
|
|
432
370
|
/**
|
|
433
371
|
* Generates a human-readable summary of folder capsules and logs it.
|
|
@@ -3,15 +3,17 @@ import fs from "fs";
|
|
|
3
3
|
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
4
4
|
/**
|
|
5
5
|
* Deterministic evidence verification:
|
|
6
|
-
* - Scans candidate files line-by-line for
|
|
7
|
-
* -
|
|
6
|
+
* - Scans candidate files line-by-line for meaningful matches to the query.
|
|
7
|
+
* - Filters stopwords and short tokens.
|
|
8
|
+
* - Deduplicates symbol evidence per file.
|
|
9
|
+
* - Removes low-signal keyword clustering.
|
|
8
10
|
*/
|
|
9
11
|
export const evidenceVerifierStep = {
|
|
10
12
|
name: "evidenceVerifier",
|
|
11
13
|
description: "Deterministic evidence-first scan over candidate files to populate fileAnalysis, with filename dominance.",
|
|
12
14
|
groups: ["analysis"],
|
|
13
15
|
run: async (input) => {
|
|
14
|
-
var _a, _b
|
|
16
|
+
var _a, _b;
|
|
15
17
|
const query = input.query ?? "";
|
|
16
18
|
const context = input.context;
|
|
17
19
|
if (!context?.analysis) {
|
|
@@ -19,24 +21,32 @@ export const evidenceVerifierStep = {
|
|
|
19
21
|
}
|
|
20
22
|
(_a = context.analysis).fileAnalysis ?? (_a.fileAnalysis = {});
|
|
21
23
|
const candidatePaths = [
|
|
22
|
-
...(context.
|
|
23
|
-
...(context.analysis.focus?.candidateFiles ?? []),
|
|
24
|
+
...(context.initContext?.relatedFiles ?? []),
|
|
24
25
|
];
|
|
25
26
|
const uniquePaths = Array.from(new Set(candidatePaths));
|
|
26
27
|
if (!uniquePaths.length) {
|
|
27
28
|
console.warn("[evidenceVerifier] No candidate files to scan.");
|
|
28
29
|
return { query, data: {} };
|
|
29
30
|
}
|
|
30
|
-
|
|
31
|
+
// ----------------- Stopwords -----------------
|
|
32
|
+
const STOPWORDS = new Set([
|
|
33
|
+
"the", "and", "for", "with", "from", "that",
|
|
34
|
+
"this", "are", "was", "were", "has", "have",
|
|
35
|
+
"had", "not", "but", "can", "could", "should",
|
|
36
|
+
"would", "into", "onto", "about", "above",
|
|
37
|
+
"below", "under", "over", "then", "else",
|
|
38
|
+
"when", "where", "what", "which", "while",
|
|
39
|
+
"return", "const", "let", "var", "true", "false",
|
|
40
|
+
"null", "undefined", "new", "set", "get",
|
|
41
|
+
"in", "to", "of", "on", "at", "by"
|
|
42
|
+
]);
|
|
31
43
|
// ----------------- Parse query for targets -----------------
|
|
32
44
|
const sentenceTargets = [];
|
|
33
|
-
// 1️⃣ Capture text inside quotes (existing behavior)
|
|
34
45
|
const quoteRegex = /['"`](.+?)['"`]/g;
|
|
35
46
|
let match;
|
|
36
47
|
while ((match = quoteRegex.exec(query)) !== null) {
|
|
37
48
|
sentenceTargets.push(match[1]);
|
|
38
49
|
}
|
|
39
|
-
// 2️⃣ Capture heuristic unquoted sentences if none found inside quotes
|
|
40
50
|
if (!sentenceTargets.length) {
|
|
41
51
|
const heuristicSentences = query
|
|
42
52
|
.split(/[\.\n]/)
|
|
@@ -51,15 +61,43 @@ export const evidenceVerifierStep = {
|
|
|
51
61
|
const filenameTargets = query
|
|
52
62
|
.split(/\s+/)
|
|
53
63
|
.map(word => word.replace(/['",]/g, ''))
|
|
54
|
-
.filter(w => w.match(/\.(ts|js|tsx|md)$/)
|
|
64
|
+
.filter(w => w.match(/\.(ts|js|tsx|md)$/));
|
|
55
65
|
const baseNameTargets = filenameTargets.map(t => t.replace(/\.(ts|js|tsx|md)$/, ''));
|
|
66
|
+
// ---- Symbol extraction (filtered + deduplicated) ----
|
|
56
67
|
const symbolTargets = [];
|
|
57
|
-
const symbolRegex = /\b([a-zA-Z_]\w
|
|
68
|
+
const symbolRegex = /\b([a-zA-Z_]\w{2,})(?:\(\))?\b/g;
|
|
58
69
|
let symMatch;
|
|
59
70
|
while ((symMatch = symbolRegex.exec(query)) !== null) {
|
|
60
|
-
|
|
71
|
+
const token = symMatch[1];
|
|
72
|
+
if (token.length >= 3 &&
|
|
73
|
+
!STOPWORDS.has(token.toLowerCase()) &&
|
|
74
|
+
!sentenceTargets.includes(token)) {
|
|
75
|
+
symbolTargets.push(token);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const uniqueSymbolTargets = Array.from(new Set(symbolTargets));
|
|
79
|
+
// ----------------- Token strength tiering -----------------
|
|
80
|
+
const WEAK_TOKENS = new Set([
|
|
81
|
+
"file",
|
|
82
|
+
"line",
|
|
83
|
+
"move",
|
|
84
|
+
"update",
|
|
85
|
+
"change",
|
|
86
|
+
"modify",
|
|
87
|
+
"readme"
|
|
88
|
+
]);
|
|
89
|
+
function computeSymbolConfidence(sym) {
|
|
90
|
+
const lower = sym.toLowerCase();
|
|
91
|
+
// If symbol appears inside quoted sentence → very strong
|
|
92
|
+
const fromQuoted = sentenceTargets.some(s => s.toLowerCase().includes(lower));
|
|
93
|
+
if (fromQuoted)
|
|
94
|
+
return 0.95;
|
|
95
|
+
// Weak structural tokens → low weight
|
|
96
|
+
if (WEAK_TOKENS.has(lower))
|
|
97
|
+
return 0.5;
|
|
98
|
+
// Default meaningful symbol
|
|
99
|
+
return 0.85;
|
|
61
100
|
}
|
|
62
|
-
const mode = context.executionControl?.mode ?? "transform";
|
|
63
101
|
// ----------------- Process each file -----------------
|
|
64
102
|
for (const path of uniquePaths) {
|
|
65
103
|
let code = null;
|
|
@@ -72,60 +110,98 @@ export const evidenceVerifierStep = {
|
|
|
72
110
|
const lines = code?.split("\n") ?? [];
|
|
73
111
|
const evidenceItems = [];
|
|
74
112
|
const matchedLines = [];
|
|
75
|
-
|
|
113
|
+
const addedSymbols = new Set();
|
|
114
|
+
// Sentence matches
|
|
76
115
|
lines.forEach((line, idx) => {
|
|
77
116
|
sentenceTargets.forEach(target => {
|
|
78
117
|
if (line.includes(target)) {
|
|
118
|
+
const snippet = lines
|
|
119
|
+
.slice(Math.max(0, idx - 1), Math.min(lines.length, idx + 2))
|
|
120
|
+
.join("\n");
|
|
79
121
|
evidenceItems.push({
|
|
80
|
-
claim: `
|
|
81
|
-
|
|
122
|
+
claim: `Sentence match: "${target}"`,
|
|
123
|
+
type: "sentence",
|
|
124
|
+
excerpt: snippet,
|
|
82
125
|
span: { startLine: idx + 1, endLine: idx + 1 },
|
|
83
126
|
confidence: 1,
|
|
84
127
|
});
|
|
85
128
|
matchedLines.push(line);
|
|
86
129
|
}
|
|
87
130
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
131
|
+
});
|
|
132
|
+
// Symbol matches (deduplicated per symbol)
|
|
133
|
+
uniqueSymbolTargets.forEach(sym => {
|
|
134
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
135
|
+
const line = lines[idx];
|
|
136
|
+
if (line.includes(`function ${sym}`) ||
|
|
137
|
+
line.includes(`class ${sym}`) ||
|
|
138
|
+
line.includes(sym)) {
|
|
139
|
+
if (!addedSymbols.has(sym)) {
|
|
140
|
+
addedSymbols.add(sym);
|
|
141
|
+
const snippet = lines
|
|
142
|
+
.slice(Math.max(0, idx - 1), Math.min(lines.length, idx + 2))
|
|
143
|
+
.join("\n");
|
|
144
|
+
evidenceItems.push({
|
|
145
|
+
claim: `Symbol reference found: "${sym}"`,
|
|
146
|
+
type: "symbol",
|
|
147
|
+
excerpt: snippet,
|
|
148
|
+
span: { startLine: idx + 1, endLine: idx + 1 },
|
|
149
|
+
confidence: computeSymbolConfidence(sym),
|
|
150
|
+
});
|
|
151
|
+
matchedLines.push(line);
|
|
152
|
+
}
|
|
153
|
+
break; // stop after first meaningful match
|
|
97
154
|
}
|
|
98
|
-
}
|
|
155
|
+
}
|
|
99
156
|
});
|
|
100
157
|
// Filename-level evidence
|
|
101
158
|
const fullFileName = path.split("/").pop() ?? "";
|
|
102
159
|
const baseFileName = fullFileName.replace(/\.(ts|js|tsx|md)$/, "");
|
|
103
|
-
if (filenameTargets.includes(fullFileName) ||
|
|
160
|
+
if (filenameTargets.includes(fullFileName) ||
|
|
161
|
+
baseNameTargets.includes(baseFileName)) {
|
|
104
162
|
evidenceItems.push({
|
|
105
163
|
claim: `Filename matches query target: "${fullFileName}"`,
|
|
164
|
+
type: "filename",
|
|
106
165
|
excerpt: `Path: ${path}`,
|
|
107
166
|
span: { startLine: 0, endLine: 0 },
|
|
108
167
|
confidence: 1,
|
|
109
168
|
});
|
|
110
169
|
}
|
|
170
|
+
// ----------------- Compute file-level confidence -----------------
|
|
171
|
+
let fileScore = 0;
|
|
172
|
+
for (const ev of evidenceItems) {
|
|
173
|
+
if (ev.type === "sentence")
|
|
174
|
+
fileScore += 1.0;
|
|
175
|
+
else if (ev.type === "filename")
|
|
176
|
+
fileScore += 1.0;
|
|
177
|
+
else if (ev.type === "symbol")
|
|
178
|
+
fileScore += ev.confidence ?? 0.8;
|
|
179
|
+
}
|
|
180
|
+
// Normalize to 0–1 range (soft cap)
|
|
181
|
+
const fileConfidence = fileScore === 0
|
|
182
|
+
? 0
|
|
183
|
+
: Math.min(1, fileScore / 3); // 3 strong signals = max confidence
|
|
111
184
|
const isFocusFile = context.analysis.focus?.selectedFiles?.includes(path) ?? false;
|
|
112
185
|
const hasEvidence = evidenceItems.length > 0;
|
|
113
186
|
// ----------------- Merge into fileAnalysis -----------------
|
|
114
187
|
if (isFocusFile || hasEvidence) {
|
|
188
|
+
const confidenceLabel = fileConfidence.toFixed(2);
|
|
115
189
|
context.analysis.fileAnalysis[path] = {
|
|
116
190
|
...context.analysis.fileAnalysis[path],
|
|
117
191
|
intent: "relevant",
|
|
118
|
-
relevanceExplanation:
|
|
192
|
+
relevanceExplanation: `[confidence:${confidenceLabel}] ${evidenceItems.length} evidence item(s) match the query${isFocusFile ? " (focus file already selected)" : ""}`,
|
|
119
193
|
role: "primary",
|
|
120
194
|
action: {
|
|
121
195
|
isRelevant: true,
|
|
122
|
-
shouldModify: hasEvidence
|
|
196
|
+
shouldModify: hasEvidence,
|
|
123
197
|
},
|
|
124
198
|
proposedChanges: hasEvidence
|
|
125
199
|
? {
|
|
126
200
|
summary: "Evidence found in file",
|
|
127
201
|
scope: "minor",
|
|
128
|
-
targets: matchedLines.length
|
|
202
|
+
targets: matchedLines.length
|
|
203
|
+
? Array.from(new Set(matchedLines))
|
|
204
|
+
: undefined,
|
|
129
205
|
}
|
|
130
206
|
: {
|
|
131
207
|
summary: "No evidence found",
|
|
@@ -137,9 +213,6 @@ export const evidenceVerifierStep = {
|
|
|
137
213
|
: ["No concrete evidence found; modification not permitted"],
|
|
138
214
|
evidence: evidenceItems,
|
|
139
215
|
};
|
|
140
|
-
if (hasEvidence) {
|
|
141
|
-
filesWithEvidence.push(path);
|
|
142
|
-
}
|
|
143
216
|
}
|
|
144
217
|
else {
|
|
145
218
|
(_b = context.analysis.fileAnalysis)[path] || (_b[path] = {
|
|
@@ -152,12 +225,6 @@ export const evidenceVerifierStep = {
|
|
|
152
225
|
});
|
|
153
226
|
}
|
|
154
227
|
}
|
|
155
|
-
// ----------------- Update focus after processing -----------------
|
|
156
|
-
(_c = context.analysis).focus ?? (_c.focus = { selectedFiles: [] });
|
|
157
|
-
context.analysis.focus.selectedFiles = Array.from(new Set([
|
|
158
|
-
...(context.analysis.focus.selectedFiles ?? []),
|
|
159
|
-
...filesWithEvidence,
|
|
160
|
-
]));
|
|
161
228
|
const output = {
|
|
162
229
|
query,
|
|
163
230
|
data: { fileAnalysis: context.analysis.fileAnalysis },
|