scai 0.1.168 → 0.1.170

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.
@@ -19,6 +19,7 @@ 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 { structuralPreloadStep } from "./structuralPreloadStep.js";
22
23
  import chalk from "chalk";
23
24
  /* ───────────────────────── registry ───────────────────────── */
24
25
  const MODULE_REGISTRY = Object.fromEntries(Object.entries(builtInModules).map(([name, mod]) => [name, mod]));
@@ -95,6 +96,10 @@ export class MainAgent {
95
96
  let ready = false;
96
97
  while (this.runCount < this.maxRuns) {
97
98
  // ---------------- EVIDENCE PIPELINE ----------------
99
+ // -------- STRUCTURAL PRELOAD --------
100
+ const t0 = this.startTimer();
101
+ await structuralPreloadStep.run({ query: this.query, context: this.context });
102
+ this.logLine("ANALYSIS", "structuralPreload", t0());
98
103
  const t1 = this.startTimer();
99
104
  await evidenceVerifierStep.run({ query: this.query, context: this.context });
100
105
  this.logLine("ANALYSIS", "collectAnalysisEvidence", t1());
@@ -7,6 +7,7 @@ import { logInputOutput } from "../utils/promptLogHelper.js";
7
7
  * - Filters stopwords and short tokens.
8
8
  * - Deduplicates symbol evidence per file.
9
9
  * - Removes low-signal keyword clustering.
10
+ * - Strictly leverages structural data (functions, classes, imports/exports) for additional evidence.
10
11
  */
11
12
  export const evidenceVerifierStep = {
12
13
  name: "evidenceVerifier",
@@ -111,7 +112,7 @@ export const evidenceVerifierStep = {
111
112
  const evidenceItems = [];
112
113
  const matchedLines = [];
113
114
  const addedSymbols = new Set();
114
- // Sentence matches
115
+ // -------- Sentence matches --------
115
116
  lines.forEach((line, idx) => {
116
117
  sentenceTargets.forEach(target => {
117
118
  if (line.includes(target)) {
@@ -129,7 +130,7 @@ export const evidenceVerifierStep = {
129
130
  }
130
131
  });
131
132
  });
132
- // Symbol matches (deduplicated per symbol)
133
+ // -------- Symbol matches --------
133
134
  uniqueSymbolTargets.forEach(sym => {
134
135
  for (let idx = 0; idx < lines.length; idx++) {
135
136
  const line = lines[idx];
@@ -150,11 +151,11 @@ export const evidenceVerifierStep = {
150
151
  });
151
152
  matchedLines.push(line);
152
153
  }
153
- break; // stop after first meaningful match
154
+ break;
154
155
  }
155
156
  }
156
157
  });
157
- // Filename-level evidence
158
+ // -------- Filename-level evidence --------
158
159
  const fullFileName = path.split("/").pop() ?? "";
159
160
  const baseFileName = fullFileName.replace(/\.(ts|js|tsx|md)$/, "");
160
161
  if (filenameTargets.includes(fullFileName) ||
@@ -167,23 +168,82 @@ export const evidenceVerifierStep = {
167
168
  confidence: 1,
168
169
  });
169
170
  }
170
- // ----------------- Compute file-level confidence -----------------
171
+ // -------- Structural evidence (strict) --------
172
+ const struct = context.analysis.fileAnalysis[path]?.structural;
173
+ const structuralEvidence = [];
174
+ if (struct) {
175
+ const queryTokens = query
176
+ .toLowerCase()
177
+ .match(/\b\w{3,}\b/g) ?? [];
178
+ const querySet = new Set(queryTokens);
179
+ (struct.functions ?? []).forEach(fn => {
180
+ if (fn.name && querySet.has(fn.name.toLowerCase())) {
181
+ const ev = {
182
+ claim: `Function name matches query: "${fn.name}"`,
183
+ type: "structural",
184
+ excerpt: fn.name,
185
+ span: { startLine: fn.start ?? 0, endLine: fn.end ?? 0 },
186
+ confidence: 0.85,
187
+ };
188
+ evidenceItems.push(ev);
189
+ structuralEvidence.push(ev);
190
+ }
191
+ });
192
+ (struct.classes ?? []).forEach(cls => {
193
+ if (cls.name && querySet.has(cls.name.toLowerCase())) {
194
+ const ev = {
195
+ claim: `Class name matches query: "${cls.name}"`,
196
+ type: "structural",
197
+ excerpt: cls.name,
198
+ span: { startLine: cls.start ?? 0, endLine: cls.end ?? 0 },
199
+ confidence: 0.85,
200
+ };
201
+ evidenceItems.push(ev);
202
+ structuralEvidence.push(ev);
203
+ }
204
+ });
205
+ [...(struct.imports ?? []), ...(struct.exports ?? [])].forEach(sym => {
206
+ if (sym && querySet.has(sym.toLowerCase())) {
207
+ const ev = {
208
+ claim: `Import/Export matches query: "${sym}"`,
209
+ type: "structural",
210
+ excerpt: sym,
211
+ span: { startLine: 0, endLine: 0 },
212
+ confidence: 0.85,
213
+ };
214
+ evidenceItems.push(ev);
215
+ structuralEvidence.push(ev);
216
+ }
217
+ });
218
+ // -------- Log structural evidence per file --------
219
+ if (structuralEvidence.length > 0) {
220
+ logInputOutput("evidenceVerifier", "output", {
221
+ file: path,
222
+ count: structuralEvidence.length,
223
+ examples: structuralEvidence.slice(0, 5).map(ev => ({
224
+ claim: ev.claim,
225
+ excerpt: ev.excerpt,
226
+ confidence: ev.confidence,
227
+ })),
228
+ });
229
+ }
230
+ }
231
+ // -------- Compute file-level confidence --------
171
232
  let fileScore = 0;
172
233
  for (const ev of evidenceItems) {
173
234
  if (ev.type === "sentence")
174
235
  fileScore += 1.0;
175
236
  else if (ev.type === "filename")
176
237
  fileScore += 1.0;
177
- else if (ev.type === "symbol")
238
+ else if (ev.type === "symbol" || ev.type === "structural")
178
239
  fileScore += ev.confidence ?? 0.8;
179
240
  }
180
- // Normalize to 0–1 range (soft cap)
181
241
  const fileConfidence = fileScore === 0
182
242
  ? 0
183
- : Math.min(1, fileScore / 3); // 3 strong signals = max confidence
243
+ : Math.min(1, fileScore / 3);
184
244
  const isFocusFile = context.analysis.focus?.selectedFiles?.includes(path) ?? false;
185
245
  const hasEvidence = evidenceItems.length > 0;
186
- // ----------------- Merge into fileAnalysis -----------------
246
+ // -------- Merge into fileAnalysis --------
187
247
  if (isFocusFile || hasEvidence) {
188
248
  const confidenceLabel = fileConfidence.toFixed(2);
189
249
  context.analysis.fileAnalysis[path] = {
@@ -229,7 +289,6 @@ export const evidenceVerifierStep = {
229
289
  query,
230
290
  data: { fileAnalysis: context.analysis.fileAnalysis },
231
291
  };
232
- // Build compact log summary
233
292
  const logSummary = Object.entries(context.analysis.fileAnalysis).map(([path, analysis]) => {
234
293
  const evidenceCount = analysis.evidence?.length ?? 0;
235
294
  const confidenceMatch = analysis.relevanceExplanation?.match(/\[confidence:(\d+\.\d+)\]/);
@@ -0,0 +1,61 @@
1
+ // File: src/modules/structuralPreloadStep.ts
2
+ import { buildInDepthContext } from "../utils/buildContextualPrompt.js";
3
+ import { logInputOutput } from "../utils/promptLogHelper.js";
4
+ /**
5
+ * Structural preload:
6
+ * - Calls buildInDepthContext for candidate files.
7
+ * - Extracts structural facts only.
8
+ * - Populates analysis.fileAnalysis[path].structural.
9
+ * - Does NOT reason or assign relevance.
10
+ */
11
+ export const structuralPreloadStep = {
12
+ name: "structuralPreload",
13
+ description: "Preloads structural KG and code metadata into fileAnalysis without performing reasoning.",
14
+ groups: ["analysis"],
15
+ run: async (input) => {
16
+ const query = input.query ?? "";
17
+ const context = input.context;
18
+ if (!context?.analysis) {
19
+ throw new Error("[structuralPreload] context.analysis is required.");
20
+ }
21
+ // ---- Ensure fileAnalysis exists (type-safe) ----
22
+ if (!context.analysis.fileAnalysis) {
23
+ context.analysis.fileAnalysis = {};
24
+ }
25
+ const fileAnalysis = context.analysis.fileAnalysis;
26
+ // ---- Candidate files ----
27
+ const candidatePaths = [
28
+ ...(context.initContext?.relatedFiles ?? []),
29
+ ];
30
+ const uniquePaths = Array.from(new Set(candidatePaths));
31
+ if (!uniquePaths.length) {
32
+ console.warn("[structuralPreload] No candidate files to preload.");
33
+ return { query, data: {} };
34
+ }
35
+ // ---- Only preload missing structural data ----
36
+ const pathsNeedingStructure = uniquePaths.filter((p) => !fileAnalysis[p]?.structural);
37
+ if (!pathsNeedingStructure.length)
38
+ return { query, data: {} };
39
+ // ---- Get structural data (path → structural object) ----
40
+ const structuralMap = await buildInDepthContext({
41
+ filenames: pathsNeedingStructure,
42
+ relatedFiles: context.initContext?.relatedFiles ?? [],
43
+ query,
44
+ });
45
+ // ---- Merge into fileAnalysis ----
46
+ for (const [path, structural] of Object.entries(structuralMap)) {
47
+ fileAnalysis[path] ?? (fileAnalysis[path] = { semanticAnalyzed: false });
48
+ fileAnalysis[path].structural = structural;
49
+ }
50
+ // ---- Minimal structured log ----
51
+ const logSummary = Object.entries(fileAnalysis).map(([path, analysis]) => ({
52
+ file: path,
53
+ hasStructural: !!analysis.structural,
54
+ functions: analysis.structural?.functions?.length ?? 0,
55
+ classes: analysis.structural?.classes?.length ?? 0,
56
+ imports: analysis.structural?.imports?.length ?? 0,
57
+ }));
58
+ logInputOutput("structuralPreload", "output", logSummary);
59
+ return { query, data: {} };
60
+ },
61
+ };
@@ -218,44 +218,23 @@ function extractFileReferences(query) {
218
218
  return [...new Set(matches)];
219
219
  }
220
220
  /* ======================================================
221
- IN-DEPTH CONTEXT
221
+ IN-DEPTH CONTEXT (Structural-Only)
222
222
  ====================================================== */
223
223
  export async function buildInDepthContext({ filenames, kgDepth = DEFAULT_KG_DEPTH, relatedFiles, query, }) {
224
224
  const db = getDbForRepo();
225
225
  const safeFilenames = Array.isArray(filenames) ? filenames : [];
226
- const safeRelated = Array.isArray(relatedFiles) ? relatedFiles : [];
227
- const initCtx = {
228
- userQuery: query?.trim() || "",
229
- repoTree: safeGenerateRepoTree(3),
230
- relatedFiles: safeRelated,
231
- folderCapsules: loadRelevantFolderCapsules(normalizeToFolders(safeRelated)),
232
- };
233
- const workingFiles = [];
234
- const out = {
235
- initContext: initCtx,
236
- workingFiles,
237
- task: {
238
- id: 0,
239
- projectId: 0,
240
- status: "active",
241
- initialQuery: query?.trim() ?? "",
242
- createdAt: new Date().toISOString(),
243
- updatedAt: new Date().toISOString(),
244
- taskSteps: [],
245
- },
246
- };
247
- /* -------- Working files (deep phase only) -------- */
226
+ const result = {};
248
227
  for (const p of safeFilenames) {
249
228
  const fileId = fileRowIdForPath(db, p);
250
- const fileObj = { path: p };
229
+ const structural = {};
251
230
  if (typeof fileId === "number") {
252
- fileObj.functions = loadFunctions(db, fileId, MAX_FUNCTIONS);
253
- fileObj.classes = loadClasses(db, fileId, 200);
231
+ structural.functions = loadFunctions(db, fileId, MAX_FUNCTIONS);
232
+ structural.classes = loadClasses(db, fileId, 200);
254
233
  }
255
234
  const neighbors = loadKgNeighbors(db, p, MAX_KG_NEIGHBORS);
256
- fileObj.kgTags = loadKgTags(db, p, MAX_KG_NEIGHBORS);
235
+ structural.kgTags = loadKgTags(db, p, MAX_KG_NEIGHBORS);
257
236
  if (neighbors.length) {
258
- fileObj.kgNeighborhood = neighbors.map((e) => `${e.relation}:${e.target}`);
237
+ structural.kgNeighborhood = neighbors.map((e) => `${e.relation}:${e.target}`);
259
238
  const imports = neighbors
260
239
  .filter((e) => e.relation === "imports")
261
240
  .map((e) => e.target);
@@ -263,12 +242,12 @@ export async function buildInDepthContext({ filenames, kgDepth = DEFAULT_KG_DEPT
263
242
  .filter((e) => e.relation === "exports")
264
243
  .map((e) => e.target);
265
244
  if (imports.length)
266
- fileObj.imports = imports.slice(0, 200);
245
+ structural.imports = imports.slice(0, 200);
267
246
  if (exports.length)
268
- fileObj.exports = exports.slice(0, 200);
247
+ structural.exports = exports.slice(0, 200);
269
248
  }
270
- fileObj.focusedTree = safeGenerateFocusedTree(p, 3);
271
- workingFiles.push(fileObj);
249
+ structural.focusedTree = safeGenerateFocusedTree(p, 3);
250
+ result[p] = structural;
272
251
  }
273
- return out;
252
+ return result;
274
253
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scai",
3
- "version": "0.1.168",
3
+ "version": "0.1.170",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "scai": "./dist/index.js"