scai 0.1.152 → 0.1.154
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 +105 -77
- package/dist/agents/evidenceVerifierStep.js +141 -0
- package/dist/agents/infoPlanGenStep.js +5 -2
- package/dist/agents/readinessGateStep.js +67 -50
- package/dist/agents/resolveExecutionModeStep.js +46 -15
- package/dist/agents/routingDecisionStep.js +69 -0
- package/dist/agents/scopeClassificationStep.js +73 -0
- package/dist/agents/selectRelevantSourcesStep.js +27 -74
- package/dist/pipeline/modules/codeTransformModule.js +8 -5
- package/dist/pipeline/modules/finalAnswerModule.js +62 -56
- package/package.json +1 -1
- package/dist/agents/collectAnalysisEvidenceStep.js +0 -189
package/dist/agents/MainAgent.js
CHANGED
|
@@ -13,7 +13,9 @@ import { resolveExecutionModeStep } from "./resolveExecutionModeStep.js";
|
|
|
13
13
|
import { fileCheckStep } from "./fileCheckStep.js";
|
|
14
14
|
import { analysisPlanGenStep } from "./analysisPlanGenStep.js";
|
|
15
15
|
import { readinessGateStep } from "./readinessGateStep.js";
|
|
16
|
-
import {
|
|
16
|
+
import { scopeClassificationStep } from "./scopeClassificationStep.js";
|
|
17
|
+
import { routingDecisionStep } from "./routingDecisionStep.js";
|
|
18
|
+
import { evidenceVerifierStep } from "./evidenceVerifierStep.js";
|
|
17
19
|
/* ───────────────────────── registry ───────────────────────── */
|
|
18
20
|
const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod]));
|
|
19
21
|
function resolveModuleForAction(action) {
|
|
@@ -82,19 +84,29 @@ export class MainAgent {
|
|
|
82
84
|
/* ───────────── execution gates ───────────── */
|
|
83
85
|
canExecutePhase(phase) {
|
|
84
86
|
const mode = this.context.executionControl?.mode ?? "transform";
|
|
87
|
+
const constraints = this.context.executionControl?.constraints;
|
|
88
|
+
let allowed;
|
|
89
|
+
// If docsOnly is true, prevent any non-doc actions
|
|
90
|
+
const docsOnly = constraints?.docsOnly ?? false;
|
|
85
91
|
switch (phase) {
|
|
86
92
|
case "analysis":
|
|
87
|
-
|
|
93
|
+
allowed = !docsOnly && (constraints?.allowAnalysis ?? (mode !== "explain"));
|
|
94
|
+
break;
|
|
88
95
|
case "planning":
|
|
89
|
-
|
|
96
|
+
allowed = !docsOnly && (constraints?.allowPlanning ?? (mode === "analyze" || mode === "transform"));
|
|
97
|
+
break;
|
|
90
98
|
case "transform":
|
|
91
99
|
case "write":
|
|
92
|
-
|
|
100
|
+
allowed = constraints?.allowFileWrites ?? (mode === "transform");
|
|
101
|
+
break;
|
|
93
102
|
case "explain":
|
|
94
|
-
|
|
103
|
+
allowed = mode === "explain"; // explain is purely mode-based
|
|
104
|
+
break;
|
|
95
105
|
default:
|
|
96
|
-
|
|
106
|
+
allowed = false;
|
|
97
107
|
}
|
|
108
|
+
this.logLine("EXEC", "canExecutePhase", undefined, `phase=${phase}, mode=${mode}, docsOnly=${docsOnly}, allowed=${allowed}`);
|
|
109
|
+
return allowed;
|
|
98
110
|
}
|
|
99
111
|
/* ───────────── main run ───────────── */
|
|
100
112
|
async run() {
|
|
@@ -103,54 +115,115 @@ export class MainAgent {
|
|
|
103
115
|
this.logLine("RUN", `start #${this.runCount}`);
|
|
104
116
|
logInputOutput("GlobalContext (structured)", "input", this.context);
|
|
105
117
|
/* ================= BOOT ================= */
|
|
118
|
+
// AXIS 1: Capability — determine WHAT actions are allowed
|
|
119
|
+
// executionMode: "explain" | "analyze" | "transform"
|
|
120
|
+
// Controls: files may be written, code analyzed, or run text-only
|
|
121
|
+
// This step does NOT determine WHERE to act or whether enough evidence exists
|
|
106
122
|
{
|
|
107
123
|
const t1 = this.startTimer();
|
|
108
|
-
await understandIntentStep.run({ context: this.context });
|
|
124
|
+
await understandIntentStep.run({ context: this.context }); // Classify user intent
|
|
109
125
|
this.logLine("BOOT", "understandIntent", t1());
|
|
110
126
|
const t2 = this.startTimer();
|
|
111
|
-
await resolveExecutionModeStep.run(this.context);
|
|
127
|
+
await resolveExecutionModeStep.run(this.context); // Set executionMode
|
|
112
128
|
this.logLine("BOOT", "resolveExecutionMode", t2(), `mode = ${this.context.executionControl?.mode}`);
|
|
113
|
-
ensureExecutionControl(this.context);
|
|
114
129
|
const db = getDbForRepo();
|
|
115
|
-
this.taskId = bootTaskForRepo(this.context, db, this.logLine.bind(this));
|
|
130
|
+
this.taskId = bootTaskForRepo(this.context, db, this.logLine.bind(this)); // Persist task
|
|
116
131
|
}
|
|
117
132
|
/* ================= PRECHECK (INITIAL) ================= */
|
|
133
|
+
// Quick file existence check (pre-grounding)
|
|
134
|
+
// Does NOT infer relevance or scope beyond directly referenced files
|
|
118
135
|
{
|
|
119
136
|
const t = this.startTimer();
|
|
120
137
|
await fileCheckStep(this.context);
|
|
121
138
|
this.logLine("PRECHECK", "preFileSearch", t());
|
|
122
139
|
}
|
|
123
|
-
/*
|
|
124
|
-
|
|
125
|
-
|
|
140
|
+
/* ───────────────────────── SCOPE & ROUTING GATE ───────────────────────── */
|
|
141
|
+
// AXIS 2: Scope — determine WHERE the agent may act
|
|
142
|
+
// scopeType: "none" | "single-file" | "multi-file" | "repo-wide"
|
|
143
|
+
// Responsible for deciding which files, repos, or modules the agent should consider
|
|
144
|
+
// Does NOT validate anchors (Axis 3) or decide allowed actions (Axis 1)
|
|
145
|
+
// ----------------- SCOPE CLASSIFICATION -----------------
|
|
146
|
+
// Responsibility:
|
|
147
|
+
// - Determine problem coverage: none / single-file / multi-file / repo-wide
|
|
148
|
+
// - Purely repo/file coverage decision
|
|
149
|
+
// - Output: context.analysis.scopeType
|
|
150
|
+
// Does NOT decide allowed actions, validate evidence, or affect executionMode
|
|
151
|
+
{
|
|
152
|
+
const t1 = this.startTimer();
|
|
153
|
+
await scopeClassificationStep.run(this.context);
|
|
154
|
+
this.logLine("SCOPE", "scopeClassification", t1());
|
|
155
|
+
}
|
|
156
|
+
// ----------------- ROUTING DECISION -----------------
|
|
157
|
+
// Responsibility:
|
|
158
|
+
// - Determine allowed actions within scope
|
|
159
|
+
// * Can search be performed?
|
|
160
|
+
// * Are early exits (explain) allowed?
|
|
161
|
+
// - Output: context.analysis.routingDecision
|
|
162
|
+
// Does NOT check anchors, determine readiness, or modify scope
|
|
163
|
+
{
|
|
164
|
+
const t2 = this.startTimer();
|
|
165
|
+
await routingDecisionStep.run(this.context);
|
|
166
|
+
this.logLine("SCOPE", "routingDecision", t2());
|
|
167
|
+
}
|
|
168
|
+
/* ================= GROUNDING & READINESS LOOP ================= */
|
|
169
|
+
// AXIS 3: Certainty — do we have enough evidence to safely proceed?
|
|
170
|
+
// confidence: "high" | "medium" | "low"
|
|
171
|
+
// Controls whether we loop back to acquire more info
|
|
172
|
+
// Does NOT change executionMode (Axis 1) or scopeType (Axis 2)
|
|
173
|
+
let ready = false;
|
|
174
|
+
while (!ready && this.runCount < this.maxRuns) {
|
|
175
|
+
const routing = this.context.analysis?.routingDecision;
|
|
176
|
+
// ---------------- INFORMATION ACQUISITION ----------------
|
|
177
|
+
// Plan & execute info steps; may discover new files
|
|
178
|
+
// Does NOT guarantee readiness — anchors still need verification
|
|
179
|
+
if (this.canExecutePhase("planning") && routing?.allowInfoSteps !== false) {
|
|
126
180
|
const t = this.startTimer();
|
|
127
|
-
await infoPlanGen.run(this.context);
|
|
181
|
+
await infoPlanGen.run(this.context); // Generate info-gathering plan
|
|
128
182
|
this.logLine("PLAN", "infoPlanGen", t());
|
|
183
|
+
let stepIO = { query: this.query };
|
|
184
|
+
const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
185
|
+
for (const step of infoPlan.steps.filter(s => s.groups?.includes("info"))) {
|
|
186
|
+
// Guard: skip fileSearch if routingDecision forbids repo search
|
|
187
|
+
if (step.action === "fileSearch" && !routing?.allowSearch) {
|
|
188
|
+
this.logLine("PLAN", "infoStepSkipped", undefined, `Step "${step.description}" skipped: file search not allowed by routingDecision`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
stepIO = await this.executeStep(step, stepIO);
|
|
192
|
+
}
|
|
193
|
+
// Re-run precheck for newly discovered files
|
|
194
|
+
if (infoPlan.steps.length > 0) {
|
|
195
|
+
const t2 = this.startTimer();
|
|
196
|
+
await fileCheckStep(this.context);
|
|
197
|
+
this.logLine("PRECHECK", "postFileSearch", t2());
|
|
198
|
+
}
|
|
129
199
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
stepIO = await this.executeStep(step, stepIO);
|
|
134
|
-
}
|
|
135
|
-
if (infoPlan.steps.length > 0) {
|
|
136
|
-
const t = this.startTimer();
|
|
137
|
-
await fileCheckStep(this.context);
|
|
138
|
-
this.logLine("PRECHECK", "postFileSearch", t());
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
/* ================= GROUNDING & READINESS GATE ================= */
|
|
142
|
-
{
|
|
200
|
+
// ---------------- DETERMINISTIC EVIDENCE VERIFICATION ----------------
|
|
201
|
+
// Verify anchors exist (filenames, paths, literals)
|
|
202
|
+
// Does NOT execute full analysis or transformations
|
|
143
203
|
const t1 = this.startTimer();
|
|
144
|
-
await
|
|
145
|
-
this.logLine("ANALYSIS", "collectAnalysisEvidence", t1());
|
|
204
|
+
await evidenceVerifierStep.run({ query: this.query, context: this.context });
|
|
205
|
+
this.logLine("ANALYSIS", "collectAnalysisEvidence", t1(), undefined);
|
|
206
|
+
// Select grounded candidate files
|
|
207
|
+
// Does NOT update executionMode or scope classification
|
|
146
208
|
const t2 = this.startTimer();
|
|
147
209
|
await selectRelevantSourcesStep.run({ query: this.query, context: this.context });
|
|
148
|
-
this.logLine("ANALYSIS", "selectRelevantSources", t2());
|
|
210
|
+
this.logLine("ANALYSIS", "selectRelevantSources", t2(), undefined);
|
|
211
|
+
// ---------------- READINESS GATE ----------------
|
|
212
|
+
// Decide if we have enough info to proceed
|
|
213
|
+
// Does NOT modify info plan or transform steps
|
|
149
214
|
const t3 = this.startTimer();
|
|
150
215
|
await readinessGateStep.run(this.context);
|
|
151
|
-
this.logLine("HASINFO", "readinessGate", t3());
|
|
216
|
+
this.logLine("HASINFO", "readinessGate", t3(), undefined);
|
|
217
|
+
ready = this.context.analysis?.readiness?.decision === "ready";
|
|
218
|
+
if (!ready) {
|
|
219
|
+
this.runCount++;
|
|
220
|
+
this.resetInitContextForLoop();
|
|
221
|
+
this.logLine("HASINFO", "readinessGate", undefined, "Not ready, looping back to information acquisition");
|
|
222
|
+
}
|
|
152
223
|
}
|
|
153
224
|
/* ================= EXPLAIN (EVIDENCE EXIT) ================= */
|
|
225
|
+
// Early exit if mode=explain and routing allows it
|
|
226
|
+
// Does NOT perform analysis, transform, or write steps
|
|
154
227
|
if (this.canExecutePhase("explain") &&
|
|
155
228
|
this.context.analysis?.routingDecision?.decision === "has-info") {
|
|
156
229
|
const explainMod = resolveModuleForAction("explain");
|
|
@@ -161,6 +234,7 @@ export class MainAgent {
|
|
|
161
234
|
context: this.context
|
|
162
235
|
});
|
|
163
236
|
}
|
|
237
|
+
// Remaining phases (ANALYSIS, TRANSFORM, FINALIZE, PERSIST) execute after loop exits
|
|
164
238
|
/* ================= ANALYSIS (DEEP) ================= */
|
|
165
239
|
if (this.canExecutePhase("analysis")) {
|
|
166
240
|
{
|
|
@@ -300,52 +374,6 @@ export function persistTaskData(context, taskId, db, logLine) {
|
|
|
300
374
|
db.prepare(`UPDATE tasks SET ${setClause}, updated_at = ? WHERE id = ?`).run(...Object.values(fieldsToUpdate), now, taskId);
|
|
301
375
|
logLine("TASK", "persistTaskData", undefined, `task ${taskId} updated with run data`);
|
|
302
376
|
}
|
|
303
|
-
/**
|
|
304
|
-
* Ensure executionControl exists and constraints are correctly set
|
|
305
|
-
*/
|
|
306
|
-
export function ensureExecutionControl(context) {
|
|
307
|
-
var _a;
|
|
308
|
-
context.executionControl ?? (context.executionControl = {
|
|
309
|
-
mode: "transform",
|
|
310
|
-
rationale: "Default execution mode",
|
|
311
|
-
confidence: 1,
|
|
312
|
-
constraints: {
|
|
313
|
-
allowAnalysis: true,
|
|
314
|
-
allowPlanning: true,
|
|
315
|
-
allowFileWrites: false
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
const mode = context.executionControl.mode;
|
|
319
|
-
switch (mode) {
|
|
320
|
-
case "explain":
|
|
321
|
-
context.executionControl.constraints = {
|
|
322
|
-
allowAnalysis: false,
|
|
323
|
-
allowPlanning: false,
|
|
324
|
-
allowFileWrites: false
|
|
325
|
-
};
|
|
326
|
-
break;
|
|
327
|
-
case "analyze":
|
|
328
|
-
context.executionControl.constraints = {
|
|
329
|
-
allowAnalysis: true,
|
|
330
|
-
allowPlanning: true,
|
|
331
|
-
allowFileWrites: false
|
|
332
|
-
};
|
|
333
|
-
break;
|
|
334
|
-
case "transform":
|
|
335
|
-
context.executionControl.constraints = {
|
|
336
|
-
allowAnalysis: true,
|
|
337
|
-
allowPlanning: true,
|
|
338
|
-
allowFileWrites: true
|
|
339
|
-
};
|
|
340
|
-
break;
|
|
341
|
-
default:
|
|
342
|
-
(_a = context.executionControl).constraints ?? (_a.constraints = {
|
|
343
|
-
allowAnalysis: true,
|
|
344
|
-
allowPlanning: true,
|
|
345
|
-
allowFileWrites: false
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
377
|
/**
|
|
350
378
|
* Ensure global_state, project, and task exist for this repo.
|
|
351
379
|
* Returns the created taskId
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// File: src/modules/evidenceVerifierStep.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
4
|
+
/**
|
|
5
|
+
* Purpose:
|
|
6
|
+
* Deterministic evidence verification.
|
|
7
|
+
* - Scans candidate files line-by-line for exact or heuristic matches to the query.
|
|
8
|
+
* - Checks for filename matches to the query (dominant signal).
|
|
9
|
+
* - Optionally checks for top-level function/class names in query.
|
|
10
|
+
* - Populates context.analysis.fileAnalysis.
|
|
11
|
+
* - Does NOT make execution decisions, update scope, or plan transformations.
|
|
12
|
+
* - Strictly Axis 3: Certainty.
|
|
13
|
+
*/
|
|
14
|
+
export const evidenceVerifierStep = {
|
|
15
|
+
name: "evidenceVerifier",
|
|
16
|
+
description: "Deterministic evidence-first scan over candidate files to populate fileAnalysis, with filename dominance.",
|
|
17
|
+
groups: ["analysis"],
|
|
18
|
+
run: async (input) => {
|
|
19
|
+
var _a, _b;
|
|
20
|
+
const query = input.query ?? "";
|
|
21
|
+
const context = input.context;
|
|
22
|
+
if (!context?.analysis) {
|
|
23
|
+
throw new Error("[evidenceVerifier] context.analysis is required.");
|
|
24
|
+
}
|
|
25
|
+
(_a = context.analysis).fileAnalysis ?? (_a.fileAnalysis = {});
|
|
26
|
+
const candidatePaths = [
|
|
27
|
+
...(context.analysis.focus?.relevantFiles ?? []),
|
|
28
|
+
...(context.workingFiles?.map(f => f.path) ?? []),
|
|
29
|
+
];
|
|
30
|
+
const uniquePaths = Array.from(new Set(candidatePaths));
|
|
31
|
+
if (!uniquePaths.length) {
|
|
32
|
+
console.warn("[evidenceVerifier] No candidate files to scan.");
|
|
33
|
+
return { query, data: {} };
|
|
34
|
+
}
|
|
35
|
+
const filesWithEvidence = [];
|
|
36
|
+
// ----------------- Parse query for targets -----------------
|
|
37
|
+
const sentenceRegex = /['"`](.+?)['"`]/g;
|
|
38
|
+
const sentenceTargets = [];
|
|
39
|
+
let match;
|
|
40
|
+
while ((match = sentenceRegex.exec(query)) !== null) {
|
|
41
|
+
sentenceTargets.push(match[1]);
|
|
42
|
+
}
|
|
43
|
+
const filenameTargets = query.split(/\s+/).filter(w => w.match(/\.(ts|js|tsx)$/));
|
|
44
|
+
const symbolTargets = [];
|
|
45
|
+
const symbolRegex = /\b([A-Z]\w+|[a-z]\w+)\(\)/g;
|
|
46
|
+
let symMatch;
|
|
47
|
+
while ((symMatch = symbolRegex.exec(query)) !== null) {
|
|
48
|
+
symbolTargets.push(symMatch[1]);
|
|
49
|
+
}
|
|
50
|
+
const mode = context.executionControl?.mode ?? "transform";
|
|
51
|
+
const allowAnalysis = context.executionControl?.constraints?.allowAnalysis ?? (mode !== "explain");
|
|
52
|
+
const allowWrites = context.executionControl?.constraints?.allowFileWrites ?? (mode === "transform");
|
|
53
|
+
// ----------------- Process each file -----------------
|
|
54
|
+
for (const path of uniquePaths) {
|
|
55
|
+
let code = null;
|
|
56
|
+
try {
|
|
57
|
+
code = fs.readFileSync(path, "utf-8");
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.warn(`[evidenceVerifier] Failed to read ${path}: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
const lines = code?.split("\n") ?? [];
|
|
63
|
+
const evidenceItems = [];
|
|
64
|
+
const matchedLines = [];
|
|
65
|
+
// Sentence & Symbol evidence
|
|
66
|
+
lines.forEach((line, idx) => {
|
|
67
|
+
sentenceTargets.forEach(target => {
|
|
68
|
+
if (line.includes(target)) {
|
|
69
|
+
evidenceItems.push({
|
|
70
|
+
claim: `Line contains the target sentence: "${target}"`,
|
|
71
|
+
excerpt: line,
|
|
72
|
+
span: { startLine: idx + 1, endLine: idx + 1 },
|
|
73
|
+
confidence: 1,
|
|
74
|
+
});
|
|
75
|
+
matchedLines.push(line);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
symbolTargets.forEach(sym => {
|
|
79
|
+
if (line.includes(`function ${sym}`) || line.includes(`class ${sym}`)) {
|
|
80
|
+
evidenceItems.push({
|
|
81
|
+
claim: `Line contains target symbol: "${sym}"`,
|
|
82
|
+
excerpt: line,
|
|
83
|
+
span: { startLine: idx + 1, endLine: idx + 1 },
|
|
84
|
+
confidence: 0.9,
|
|
85
|
+
});
|
|
86
|
+
matchedLines.push(line);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
// Filename-level evidence
|
|
91
|
+
const fileName = path.split("/").pop() ?? "";
|
|
92
|
+
if (filenameTargets.includes(fileName)) {
|
|
93
|
+
evidenceItems.push({
|
|
94
|
+
claim: `Filename matches query target: "${fileName}"`,
|
|
95
|
+
excerpt: "",
|
|
96
|
+
span: { startLine: 0, endLine: 0 },
|
|
97
|
+
confidence: 1,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// ----------------- Force focus files as relevant -----------------
|
|
101
|
+
const isFocusFile = context.analysis.focus?.relevantFiles?.includes(path) ?? false;
|
|
102
|
+
const isRelevant = isFocusFile || evidenceItems.length > 0;
|
|
103
|
+
context.analysis.fileAnalysis[path] = {
|
|
104
|
+
...context.analysis.fileAnalysis[path],
|
|
105
|
+
intent: isRelevant ? "relevant" : "irrelevant",
|
|
106
|
+
relevance: isRelevant
|
|
107
|
+
? `${evidenceItems.length} evidence item(s) match the query${isFocusFile ? " (focus file forced relevant)" : ""}`
|
|
108
|
+
: "No matching evidence found",
|
|
109
|
+
role: "primary",
|
|
110
|
+
action: { isRelevant, shouldModify: isRelevant && allowAnalysis && allowWrites },
|
|
111
|
+
proposedChanges: {
|
|
112
|
+
summary: isRelevant ? "Evidence found in file" : "No evidence",
|
|
113
|
+
scope: isRelevant ? "minor" : "none",
|
|
114
|
+
targets: matchedLines,
|
|
115
|
+
rationale: isFocusFile ? "Focus file forced relevant even without explicit evidence" : undefined,
|
|
116
|
+
},
|
|
117
|
+
risks: [],
|
|
118
|
+
evidence: evidenceItems,
|
|
119
|
+
};
|
|
120
|
+
if (isRelevant)
|
|
121
|
+
filesWithEvidence.push(path);
|
|
122
|
+
}
|
|
123
|
+
// ----------------- Update focus after processing -----------------
|
|
124
|
+
if (filesWithEvidence.length > 0) {
|
|
125
|
+
(_b = context.analysis).focus ?? (_b.focus = { relevantFiles: [] });
|
|
126
|
+
context.analysis.focus.relevantFiles = filesWithEvidence;
|
|
127
|
+
for (const path of uniquePaths) {
|
|
128
|
+
if (!filesWithEvidence.includes(path)) {
|
|
129
|
+
context.analysis.fileAnalysis[path] = {
|
|
130
|
+
...context.analysis.fileAnalysis[path],
|
|
131
|
+
intent: "irrelevant",
|
|
132
|
+
action: { ...context.analysis.fileAnalysis[path]?.action, isRelevant: false },
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const output = { query, data: { fileAnalysis: context.analysis.fileAnalysis } };
|
|
138
|
+
logInputOutput("evidenceVerifier", "output", output.data);
|
|
139
|
+
return output;
|
|
140
|
+
},
|
|
141
|
+
};
|
|
@@ -27,9 +27,12 @@ export const infoPlanGen = {
|
|
|
27
27
|
const missingFiles = Array.isArray(context.analysis.focus?.missingFiles)
|
|
28
28
|
? context.analysis.focus.missingFiles
|
|
29
29
|
: [];
|
|
30
|
-
|
|
30
|
+
const analysis = context.analysis;
|
|
31
|
+
const needsDiscovery = analysis?.routingDecision?.decision === "needs-info" ||
|
|
32
|
+
analysis?.scopeType === "repo-wide";
|
|
33
|
+
if (!needsDiscovery && missingFiles.length === 0) {
|
|
31
34
|
context.analysis.planSuggestion = { plan: { steps: [] } };
|
|
32
|
-
logInputOutput(
|
|
35
|
+
logInputOutput("infoPlanGen", "output", { steps: [] });
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
38
|
// --------------------------------------------------
|
|
@@ -1,52 +1,45 @@
|
|
|
1
|
+
// File: src/modules/readinessGateStep.ts
|
|
1
2
|
import { generate } from '../lib/generate.js';
|
|
2
3
|
import { logInputOutput } from '../utils/promptLogHelper.js';
|
|
3
|
-
function logRoutingDecision(context) {
|
|
4
|
-
logInputOutput('readinessGateStep', 'output', {
|
|
5
|
-
routingDecision: context.analysis?.routingDecision,
|
|
6
|
-
focus: context.analysis?.focus,
|
|
7
|
-
fileAnalysis: context.analysis?.fileAnalysis,
|
|
8
|
-
});
|
|
9
|
-
}
|
|
10
4
|
export const readinessGateStep = {
|
|
11
5
|
name: 'readinessGate',
|
|
6
|
+
description: 'Determines if there is sufficient evidence to safely proceed with analysis or transformation.',
|
|
7
|
+
groups: ['analysis'],
|
|
12
8
|
requires: ['userQuery', 'intent'],
|
|
13
|
-
produces: ['analysis.
|
|
9
|
+
produces: ['analysis.readiness'],
|
|
14
10
|
async run(context) {
|
|
15
|
-
var _a, _b;
|
|
11
|
+
var _a, _b, _c, _d, _e;
|
|
16
12
|
context.analysis || (context.analysis = {});
|
|
17
13
|
(_a = context.analysis).focus || (_a.focus = { relevantFiles: [], missingFiles: [], rationale: '' });
|
|
18
14
|
(_b = context.analysis).fileAnalysis || (_b.fileAnalysis = {});
|
|
15
|
+
(_c = context.analysis).readiness || (_c.readiness = { decision: 'not-ready', confidence: 0, rationale: '' });
|
|
19
16
|
const focus = context.analysis.focus;
|
|
20
17
|
const fileAnalysis = context.analysis.fileAnalysis;
|
|
21
|
-
//
|
|
18
|
+
// ---------------- EVIDENCE COLLECTION ----------------
|
|
22
19
|
const relevantFiles = Object.entries(fileAnalysis)
|
|
23
20
|
.filter(([_, fa]) => fa.intent === 'relevant')
|
|
24
|
-
.map(([path
|
|
25
|
-
const irrelevantFiles = Object.entries(fileAnalysis)
|
|
26
|
-
.filter(([_, fa]) => fa.intent === 'irrelevant')
|
|
27
|
-
.map(([path, _]) => path);
|
|
21
|
+
.map(([path]) => path);
|
|
28
22
|
const missingFiles = focus.missingFiles ?? [];
|
|
29
|
-
//
|
|
23
|
+
// Deterministic short-circuit: nothing relevant or missing files
|
|
30
24
|
if (relevantFiles.length === 0 || missingFiles.length > 0) {
|
|
31
|
-
context.analysis.
|
|
32
|
-
decision: '
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
context.analysis.readiness = {
|
|
26
|
+
decision: 'not-ready',
|
|
27
|
+
confidence: 0.95,
|
|
28
|
+
rationale: `No relevant files or missing files detected: ${missingFiles.join(', ')}`,
|
|
35
29
|
};
|
|
36
|
-
|
|
30
|
+
logOutput(context);
|
|
37
31
|
return;
|
|
38
32
|
}
|
|
39
|
-
//
|
|
40
|
-
// Build simple summary for model
|
|
33
|
+
// ---------------- SUMMARIZE EVIDENCE FOR LLM ----------------
|
|
41
34
|
const evidenceSummary = relevantFiles
|
|
42
35
|
.map(path => {
|
|
43
36
|
const fa = fileAnalysis[path];
|
|
44
37
|
const claims = fa.evidence?.map(e => `- ${e.claim}`).join('\n') ?? '';
|
|
45
|
-
return `File: ${path}\nRole: ${fa.role}\
|
|
38
|
+
return `File: ${path}\nRole: ${fa.role}\nClaims:\n${claims}`;
|
|
46
39
|
})
|
|
47
40
|
.join('\n\n');
|
|
48
41
|
const prompt = `
|
|
49
|
-
You are a
|
|
42
|
+
You are a focused evidence assessor. Do NOT invent new facts. Only reason about the evidence given.
|
|
50
43
|
|
|
51
44
|
User query:
|
|
52
45
|
${context.initContext?.userQuery}
|
|
@@ -54,48 +47,72 @@ ${context.initContext?.userQuery}
|
|
|
54
47
|
Evidence from analyzed files:
|
|
55
48
|
${evidenceSummary}
|
|
56
49
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- Sufficient evidence exists in the analyzed files.
|
|
61
|
-
- All required information is present to answer the user's query.
|
|
62
|
-
|
|
63
|
-
NEEDS-INFO:
|
|
64
|
-
- Insufficient evidence or missing critical files.
|
|
65
|
-
- Additional information acquisition must run.
|
|
50
|
+
Instruction:
|
|
51
|
+
- Decide whether there is sufficient evidence to proceed safely with analysis or transformation.
|
|
52
|
+
- Return a JSON object EXACTLY in this format:
|
|
66
53
|
|
|
67
|
-
|
|
54
|
+
{
|
|
55
|
+
"decision": "ready" | "not-ready",
|
|
56
|
+
"confidence": 0.0-1.0,
|
|
57
|
+
"rationale": "Concise explanation based only on the evidence provided."
|
|
58
|
+
}
|
|
68
59
|
`.trim();
|
|
69
60
|
try {
|
|
70
|
-
const genInput = {
|
|
71
|
-
query: context.initContext?.userQuery ?? '',
|
|
72
|
-
content: prompt,
|
|
73
|
-
};
|
|
61
|
+
const genInput = { query: context.initContext?.userQuery ?? '', content: prompt };
|
|
74
62
|
const genOutput = await generate(genInput);
|
|
75
63
|
const raw = String(genOutput.data ?? '').trim();
|
|
76
64
|
logInputOutput('readinessGateStep raw', 'output', raw);
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
// Attempt to parse JSON from LLM
|
|
66
|
+
let modelDecision;
|
|
67
|
+
try {
|
|
68
|
+
modelDecision = JSON.parse(raw);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
modelDecision = {
|
|
72
|
+
decision: 'not-ready',
|
|
73
|
+
confidence: 0.5,
|
|
74
|
+
rationale: 'Failed to parse model output; assuming not-ready.',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Merge deterministic info with LLM summary
|
|
79
78
|
const combinedRationale = [
|
|
80
|
-
`Evidence-based
|
|
81
|
-
|
|
79
|
+
`Evidence-based: ${relevantFiles.length} relevant file(s).`,
|
|
80
|
+
modelDecision.rationale,
|
|
82
81
|
]
|
|
83
82
|
.filter(Boolean)
|
|
84
|
-
.join('
|
|
85
|
-
context.analysis.
|
|
86
|
-
decision: modelDecision,
|
|
83
|
+
.join(' ');
|
|
84
|
+
context.analysis.readiness = {
|
|
85
|
+
decision: modelDecision.decision,
|
|
86
|
+
confidence: modelDecision.confidence,
|
|
87
87
|
rationale: combinedRationale,
|
|
88
|
-
confidence: modelDecision === 'has-info' ? 0.85 : 0.9,
|
|
89
88
|
};
|
|
90
|
-
|
|
89
|
+
// ---------------- DOCS-ONLY FILES ----------------
|
|
90
|
+
// If executionControl.docsOnly is true, mark relevant files as modifiable
|
|
91
|
+
const docsOnly = context.executionControl?.constraints?.docsOnly ?? false;
|
|
92
|
+
if (docsOnly && context.analysis.readiness.decision === 'ready') {
|
|
93
|
+
for (const path of relevantFiles) {
|
|
94
|
+
(_d = context.analysis.fileAnalysis)[path] || (_d[path] = { action: {}, intent: 'relevant' });
|
|
95
|
+
(_e = context.analysis.fileAnalysis[path]).action || (_e.action = {});
|
|
96
|
+
context.analysis.fileAnalysis[path].action.shouldModify = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
logOutput(context);
|
|
91
100
|
}
|
|
92
101
|
catch (err) {
|
|
93
102
|
console.warn('⚠️ readinessGateStep failed:', err);
|
|
94
|
-
context.analysis.
|
|
95
|
-
decision: '
|
|
96
|
-
rationale: 'readinessGateStep encountered an internal error.',
|
|
103
|
+
context.analysis.readiness = {
|
|
104
|
+
decision: 'not-ready',
|
|
97
105
|
confidence: 0.2,
|
|
106
|
+
rationale: 'Internal error while determining readiness.',
|
|
98
107
|
};
|
|
99
108
|
}
|
|
100
109
|
},
|
|
101
110
|
};
|
|
111
|
+
// ---------------- HELPER ----------------
|
|
112
|
+
function logOutput(context) {
|
|
113
|
+
logInputOutput('readinessGateStep', 'output', {
|
|
114
|
+
readiness: context.analysis?.readiness,
|
|
115
|
+
focus: context.analysis?.focus,
|
|
116
|
+
fileAnalysis: context.analysis?.fileAnalysis,
|
|
117
|
+
});
|
|
118
|
+
}
|