scai 0.1.150 → 0.1.152
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.
|
@@ -3,31 +3,31 @@ import fs from "fs";
|
|
|
3
3
|
import { generate } from "../lib/generate.js";
|
|
4
4
|
import { cleanupModule } from "../pipeline/modules/cleanupModule.js";
|
|
5
5
|
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
6
|
-
import { MODULE_STEP_INDENTATION } from "../constants.js";
|
|
7
6
|
export const collectAnalysisEvidenceStep = {
|
|
8
7
|
name: "collectAnalysisEvidence",
|
|
9
8
|
description: "Evidence-first, deterministic analysis over candidate source files. Builds fileAnalysis only.",
|
|
10
9
|
groups: ["analysis"],
|
|
11
10
|
run: async (input) => {
|
|
12
|
-
var _a
|
|
11
|
+
var _a;
|
|
13
12
|
const query = input.query ?? "";
|
|
14
13
|
const context = input.context;
|
|
15
14
|
if (!context?.analysis) {
|
|
16
15
|
throw new Error("[collectAnalysisEvidence] context.analysis is required.");
|
|
17
16
|
}
|
|
18
|
-
|
|
19
|
-
(_b = context.analysis).requiresClarification ?? (_b.requiresClarification = false);
|
|
17
|
+
context.analysis.fileAnalysis = context.analysis.fileAnalysis ?? {};
|
|
20
18
|
const intentCategory = context.analysis.intent?.intentCategory ?? "other";
|
|
21
19
|
// ───────────── Guard ─────────────
|
|
22
20
|
if (!["refactorTask", "codingTask", "writing"].includes(intentCategory)) {
|
|
23
21
|
console.log(`[collectAnalysisEvidence] Skipping file scan because intentCategory=${intentCategory}`);
|
|
24
22
|
return {
|
|
25
23
|
query,
|
|
26
|
-
data: {
|
|
24
|
+
data: {
|
|
25
|
+
fileAnalysis: context.analysis.fileAnalysis
|
|
26
|
+
}
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
// --------------------------------------------------
|
|
30
|
-
// Collect candidate paths
|
|
30
|
+
// Collect candidate paths (old selectRelevantSources behavior)
|
|
31
31
|
// --------------------------------------------------
|
|
32
32
|
const candidatePaths = [
|
|
33
33
|
...(context.analysis.focus?.relevantFiles ?? []),
|
|
@@ -41,20 +41,6 @@ export const collectAnalysisEvidenceStep = {
|
|
|
41
41
|
}
|
|
42
42
|
const filesWithEvidence = [];
|
|
43
43
|
// --------------------------------------------------
|
|
44
|
-
// Pre-extract query signals
|
|
45
|
-
// --------------------------------------------------
|
|
46
|
-
const quotedRegex = /['"`](.+?)['"`]/g;
|
|
47
|
-
const quotedTargets = [];
|
|
48
|
-
let m;
|
|
49
|
-
while ((m = quotedRegex.exec(query)) !== null) {
|
|
50
|
-
quotedTargets.push(m[1]);
|
|
51
|
-
}
|
|
52
|
-
const identifierRegex = /\b([A-Za-z_][A-Za-z0-9_]*)\b/g;
|
|
53
|
-
const queryIdentifiers = new Set();
|
|
54
|
-
while ((m = identifierRegex.exec(query)) !== null) {
|
|
55
|
-
queryIdentifiers.add(m[1]);
|
|
56
|
-
}
|
|
57
|
-
// --------------------------------------------------
|
|
58
44
|
// Deterministic evidence collection
|
|
59
45
|
// --------------------------------------------------
|
|
60
46
|
for (const path of uniquePaths) {
|
|
@@ -67,107 +53,75 @@ export const collectAnalysisEvidenceStep = {
|
|
|
67
53
|
continue;
|
|
68
54
|
}
|
|
69
55
|
const lines = code.split("\n");
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
56
|
+
const evidenceItems = [];
|
|
57
|
+
const sentenceMatches = [];
|
|
58
|
+
// Heuristic: quoted strings in query
|
|
59
|
+
const sentenceRegex = /['"`](.+?)['"`]/g;
|
|
60
|
+
const targetSentences = [];
|
|
61
|
+
let match;
|
|
62
|
+
while ((match = sentenceRegex.exec(query)) !== null) {
|
|
63
|
+
targetSentences.push(match[1]);
|
|
64
|
+
}
|
|
65
|
+
// Line-by-line scan
|
|
74
66
|
lines.forEach((line, index) => {
|
|
75
|
-
const
|
|
76
|
-
// ───── Literal evidence ─────
|
|
77
|
-
for (const target of quotedTargets) {
|
|
67
|
+
for (const target of targetSentences) {
|
|
78
68
|
if (line.includes(target)) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
type: "literal",
|
|
82
|
-
claim: `Exact quoted text found: "${target}"`,
|
|
69
|
+
evidenceItems.push({
|
|
70
|
+
claim: `Line contains the target sentence: "${target}"`,
|
|
83
71
|
excerpt: line,
|
|
84
|
-
span: { startLine:
|
|
72
|
+
span: { startLine: index + 1, endLine: index + 1 },
|
|
85
73
|
confidence: 1,
|
|
86
74
|
});
|
|
75
|
+
sentenceMatches.push(line);
|
|
87
76
|
}
|
|
88
77
|
}
|
|
89
|
-
// ───── Identifier evidence ─────
|
|
90
|
-
for (const ident of queryIdentifiers) {
|
|
91
|
-
if (ident.length > 2 &&
|
|
92
|
-
line.includes(ident) &&
|
|
93
|
-
!quotedTargets.includes(ident)) {
|
|
94
|
-
identifierHits++;
|
|
95
|
-
evidence.push({
|
|
96
|
-
type: "identifier",
|
|
97
|
-
claim: `Identifier "${ident}" appears in file`,
|
|
98
|
-
excerpt: line,
|
|
99
|
-
span: { startLine: lineNo, endLine: lineNo },
|
|
100
|
-
confidence: 0.6,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
// ───── Anchored structural evidence ─────
|
|
105
|
-
if (line.match(/^#{1,6}\s+/) || // markdown headers
|
|
106
|
-
line.match(/^(export\s+)?(function|class)\s+/) ||
|
|
107
|
-
line.match(/^module\.exports\b/)) {
|
|
108
|
-
structuralHits++;
|
|
109
|
-
evidence.push({
|
|
110
|
-
type: "anchoredStructural",
|
|
111
|
-
claim: "Top-level structural anchor detected",
|
|
112
|
-
excerpt: line,
|
|
113
|
-
span: { startLine: lineNo, endLine: lineNo },
|
|
114
|
-
confidence: 0.4,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
78
|
});
|
|
118
|
-
const isRelevant =
|
|
79
|
+
const isRelevant = evidenceItems.length > 0;
|
|
119
80
|
const shouldModify = determineShouldModify(intentCategory, isRelevant);
|
|
120
|
-
const scope = determineScope(
|
|
121
|
-
if (isRelevant)
|
|
81
|
+
const scope = determineScope(evidenceItems.length, intentCategory, isRelevant);
|
|
82
|
+
if (isRelevant) {
|
|
122
83
|
filesWithEvidence.push(path);
|
|
123
|
-
// ───────────── Clarification heuristic ─────────────
|
|
124
|
-
const requiresClarification = literalHits === 0 &&
|
|
125
|
-
identifierHits === 0 &&
|
|
126
|
-
structuralHits > 0 &&
|
|
127
|
-
["refactorTask", "codingTask"].includes(intentCategory);
|
|
128
|
-
if (requiresClarification) {
|
|
129
|
-
context.analysis.requiresClarification = true;
|
|
130
84
|
}
|
|
131
85
|
const fileAnalysis = {
|
|
132
86
|
intent: isRelevant ? "relevant" : "irrelevant",
|
|
133
87
|
relevance: isRelevant
|
|
134
|
-
?
|
|
135
|
-
: "No
|
|
88
|
+
? `${evidenceItems.length} candidate item(s) relevant to the query.`
|
|
89
|
+
: "No matching lines found in the file.",
|
|
136
90
|
role: "primary",
|
|
137
91
|
action: {
|
|
138
92
|
isRelevant,
|
|
139
|
-
shouldModify
|
|
93
|
+
shouldModify
|
|
140
94
|
},
|
|
141
95
|
proposedChanges: {
|
|
142
96
|
summary: isRelevant
|
|
143
|
-
? "File contains
|
|
97
|
+
? "File contains elements that should be modified according to the query."
|
|
144
98
|
: "No changes required.",
|
|
145
99
|
scope,
|
|
146
|
-
|
|
147
|
-
|
|
100
|
+
targets: sentenceMatches,
|
|
101
|
+
rationale: isRelevant
|
|
102
|
+
? "Detected sentence(s) in file matching the query."
|
|
148
103
|
: undefined,
|
|
149
104
|
},
|
|
150
105
|
risks: [],
|
|
151
|
-
evidence,
|
|
152
|
-
requiresClarification,
|
|
106
|
+
evidence: evidenceItems,
|
|
153
107
|
};
|
|
154
|
-
//
|
|
155
|
-
if (isRelevant
|
|
108
|
+
// Optional LLM rationale (non-authoritative)
|
|
109
|
+
if (isRelevant) {
|
|
156
110
|
try {
|
|
157
111
|
const prompt = `
|
|
158
|
-
Provide a concise rationale (1–2 sentences) explaining why this file
|
|
112
|
+
Provide a concise rationale (1–2 sentences) explaining why the identified lines in this file are relevant to the user's query.
|
|
159
113
|
|
|
160
114
|
Query:
|
|
161
115
|
"${query}"
|
|
162
116
|
|
|
163
117
|
File:
|
|
164
118
|
${path}
|
|
119
|
+
|
|
120
|
+
Lines:
|
|
121
|
+
${sentenceMatches.join("\n")}
|
|
165
122
|
`.trim();
|
|
166
123
|
const response = await generate({ content: prompt, query });
|
|
167
|
-
const cleaned = await cleanupModule.run({
|
|
168
|
-
query,
|
|
169
|
-
content: response.data,
|
|
170
|
-
});
|
|
124
|
+
const cleaned = await cleanupModule.run({ query, content: response.data });
|
|
171
125
|
if (typeof cleaned.data === "string") {
|
|
172
126
|
fileAnalysis.proposedChanges.rationale = cleaned.data;
|
|
173
127
|
}
|
|
@@ -176,25 +130,34 @@ ${path}
|
|
|
176
130
|
console.warn(`[collectAnalysisEvidence] Rationale enrichment failed for ${path}`);
|
|
177
131
|
}
|
|
178
132
|
}
|
|
133
|
+
// Persist analysis only
|
|
179
134
|
context.analysis.fileAnalysis[path] = {
|
|
180
135
|
...context.analysis.fileAnalysis[path],
|
|
181
136
|
...fileAnalysis,
|
|
182
137
|
};
|
|
183
138
|
}
|
|
184
|
-
// ─────────────
|
|
139
|
+
// ───────────── Conditional relevance update ─────────────
|
|
140
|
+
// Only update focus.relevantFiles if at least one file has evidence
|
|
185
141
|
if (filesWithEvidence.length > 0) {
|
|
186
|
-
(
|
|
142
|
+
(_a = context.analysis).focus ?? (_a.focus = { relevantFiles: [] });
|
|
187
143
|
context.analysis.focus.relevantFiles = filesWithEvidence;
|
|
144
|
+
// Mark others as irrelevant
|
|
145
|
+
for (const path of uniquePaths) {
|
|
146
|
+
if (!filesWithEvidence.includes(path)) {
|
|
147
|
+
context.analysis.fileAnalysis[path] = {
|
|
148
|
+
...context.analysis.fileAnalysis[path],
|
|
149
|
+
intent: "irrelevant",
|
|
150
|
+
action: { ...context.analysis.fileAnalysis[path]?.action, isRelevant: false }
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
188
154
|
}
|
|
189
155
|
const output = {
|
|
190
156
|
query,
|
|
191
|
-
data: {
|
|
157
|
+
data: {
|
|
158
|
+
fileAnalysis: context.analysis.fileAnalysis,
|
|
159
|
+
},
|
|
192
160
|
};
|
|
193
|
-
// ───────────── Clarification note (agent-visible) ─────────────
|
|
194
|
-
if (context.analysis.requiresClarification) {
|
|
195
|
-
console.warn(`${MODULE_STEP_INDENTATION}[collectAnalysisEvidence] Clarification required: query is underspecified. ` +
|
|
196
|
-
`Only structural anchors were found; no literal or identifier evidence.`);
|
|
197
|
-
}
|
|
198
161
|
logInputOutput("collectAnalysisEvidence", "output", output.data);
|
|
199
162
|
return output;
|
|
200
163
|
},
|
|
@@ -208,9 +171,19 @@ function determineShouldModify(intentCategory, isRelevant) {
|
|
|
208
171
|
function determineScope(evidenceCount, intentCategory, isRelevant) {
|
|
209
172
|
if (!isRelevant)
|
|
210
173
|
return "none";
|
|
174
|
+
let scope = "minor";
|
|
211
175
|
if (evidenceCount <= 2)
|
|
212
|
-
|
|
213
|
-
if (evidenceCount <= 5)
|
|
214
|
-
|
|
215
|
-
|
|
176
|
+
scope = "minor";
|
|
177
|
+
else if (evidenceCount <= 5)
|
|
178
|
+
scope = "moderate";
|
|
179
|
+
else
|
|
180
|
+
scope = "major";
|
|
181
|
+
// Intent bump
|
|
182
|
+
if (["refactorTask", "codingTask"].includes(intentCategory)) {
|
|
183
|
+
if (scope === "minor" && evidenceCount > 1)
|
|
184
|
+
scope = "moderate";
|
|
185
|
+
if (scope === "moderate" && evidenceCount > 4)
|
|
186
|
+
scope = "major";
|
|
187
|
+
}
|
|
188
|
+
return scope;
|
|
216
189
|
}
|