scai
Version:
> **A local-first AI CLI for understanding, querying, and iterating on large codebases.** > **100% local • No token costs • No cloud • No prompt injection • Private by design**
215 lines (207 loc) • 8.49 kB
JavaScript
import { logInputOutput } from "../../utils/promptLogHelper.js";
import { cleanupModule } from "./cleanupModule.js";
import { generate } from "../../lib/generate.js";
const MAX_CODE_CHARS = 6000; // conservative prompt-safe limit
export const semanticAnalysisModule = {
name: "semanticAnalysis",
description: "Performs semantic analysis for a single target file defined by the current execution step.",
groups: ["analysis"],
run: async (input) => {
var _a;
const context = input.context;
if (!context)
throw new Error("[semanticAnalysisStep] No context provided");
const step = context.currentStep;
if (!step || step.action !== "semanticAnalysis") {
const notes = "[semanticAnalysisStep] Invoked without semanticAnalysis step";
logInputOutput("semanticAnalysisStep", "output", { notes });
return { query: input.query, data: { notes }, context };
}
if (!step.targetFile) {
const notes = "[semanticAnalysisStep] semanticAnalysis step missing targetFile";
logInputOutput("semanticAnalysisStep", "output", { notes });
return { query: input.query, data: { notes }, context };
}
const workingFiles = context.workingFiles ?? [];
const file = workingFiles.find(f => f.path === step.targetFile);
if (!file) {
const notes = `[semanticAnalysisStep] targetFile not found: ${step.targetFile}`;
logInputOutput("semanticAnalysisStep", "output", { notes });
return { query: input.query, data: { notes }, context };
}
// -----------------------------
// Ensure analysis containers
// -----------------------------
context.analysis || (context.analysis = {});
(_a = context.analysis).fileAnalysis || (_a.fileAnalysis = {});
const filePath = file.path;
const prevAnalysis = context.analysis.fileAnalysis[filePath];
// -----------------------------
// Semantic analysis (LLM)
// -----------------------------
const semanticResult = await analyzeFile(file, input.query, context);
// -----------------------------
// Write-once merge to context
// -----------------------------
context.analysis.fileAnalysis[filePath] = {
...prevAnalysis,
...semanticResult,
semanticAnalyzed: true,
};
logInputOutput("semanticAnalysisStep - per-file", "output", {
file: filePath,
analysis: context.analysis.fileAnalysis[filePath],
});
return {
query: input.query,
data: { notes: "Semantic analysis completed" },
context,
};
},
};
/* -------------------------
Analyze single file
---------------------------- */
async function analyzeFile(file, query, context) {
const slicedCode = sliceCodeForAnalysis(file.code);
const focus = context?.analysis?.focus;
const understanding = context?.analysis?.understanding;
const contextSnippet = [
focus?.rationale ? `Rationale: ${focus.rationale}` : "",
understanding?.assumptions
? `Assumptions: ${understanding.assumptions.join("; ")}`
: "",
understanding?.constraints
? `Constraints: ${understanding.constraints.join("; ")}`
: "",
understanding?.risks ? `Known risks: ${understanding.risks.join("; ")}` : "",
]
.filter(Boolean)
.join("\n");
const prompt = `
You are performing semantic analysis on a file that has already been determined
to be relevant to the user query.
User query:
"${query}"
Context from previous steps:
${contextSnippet}
File path: ${file.path}
Task:
- Explain *why* this file is relevant to the query in concrete, technical terms.
- Assign a semantic role if applicable.
- Identify risks, constraints, or noteworthy design considerations.
Additionally:
- Extract the most relevant code excerpts that justify your analysis.
- Prefer complete functions, configuration blocks, or object literals.
- Do NOT summarize the code — include the actual code text.
- Only include excerpts that directly support your analysis.
Code excerpt:
${slicedCode ?? "[no code]"}
Return STRICT JSON:
{
"role"?: "primary" | "supporting" | "contextual",
"relevanceExplanation": string,
"proposedChanges"?: {
"summary": string,
"scope": "none" | "minor" | "moderate" | "major",
"targets"?: string[],
"rationale"?: string
},
"excerpts"?: Array<{
"description": string,
"startLine"?: number,
"endLine"?: number,
"symbols"?: string[],
"code": string
}>,
"risks"?: string[]
}
`.trim();
try {
const ai = await generate({ query: file.path, content: prompt });
const cleaned = await cleanupModule.run({ query, content: ai.data });
let data;
if (typeof cleaned.data === "object" && cleaned.data) {
data = cleaned.data;
}
else {
try {
data = JSON.parse(String(cleaned.content ?? "{}"));
}
catch {
data = {};
}
}
const result = {
relevanceExplanation: typeof data.relevanceExplanation === "string" && data.relevanceExplanation.trim()
? data.relevanceExplanation
: "This file contributes to the query but did not provide a detailed explanation.",
role: ["primary", "supporting", "contextual"].includes(data.role)
? data.role
: undefined,
proposedChanges: data.proposedChanges
? {
summary: String(data.proposedChanges.summary ?? ""),
scope: data.proposedChanges.scope ?? "none",
targets: Array.isArray(data.proposedChanges.targets)
? data.proposedChanges.targets
: undefined,
rationale: typeof data.proposedChanges.rationale === "string"
? data.proposedChanges.rationale
: undefined,
}
: {
summary: "No changes required for this file.",
scope: "none",
},
excerpts: Array.isArray(data.excerpts)
? data.excerpts
.filter((e) => typeof e?.code === "string" && e.code.trim())
.map((e) => ({
description: String(e.description ?? "Relevant code excerpt"),
startLine: typeof e.startLine === "number" ? e.startLine : undefined,
endLine: typeof e.endLine === "number" ? e.endLine : undefined,
symbols: Array.isArray(e.symbols) ? e.symbols : undefined,
code: String(e.code),
}))
: undefined,
risks: Array.isArray(data.risks) ? data.risks : [],
};
// 🔹 Log proposed changes (decision-level signal)
logInputOutput("semanticAnalysisStep - proposedChanges", "output", {
file: file.path,
proposedChanges: result.proposedChanges ?? null,
});
// 🔹 Log excerpts (grounding signal, compact)
logInputOutput("semanticAnalysisStep - excerpts", "output", {
file: file.path,
excerpts: result.excerpts?.map(e => ({
description: e.description,
startLine: e.startLine,
endLine: e.endLine,
symbols: e.symbols,
codeLength: e.code.length,
})) ?? [],
});
return result;
}
catch (err) {
console.warn(`[semanticAnalysisStep] Failed to analyze file ${file.path}:`, err);
return {
relevanceExplanation: "This file could not be analyzed due to an error.",
risks: [],
};
}
}
/* -------------------------
Slice large code for prompts
---------------------------- */
function sliceCodeForAnalysis(code) {
if (!code)
return undefined;
if (code.length <= MAX_CODE_CHARS)
return code;
const head = code.slice(0, Math.floor(MAX_CODE_CHARS * 0.6));
const tail = code.slice(-Math.floor(MAX_CODE_CHARS * 0.4));
return [head, "\n/* … file truncated for semantic analysis … */\n", tail].join("");
}