UNPKG

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
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(""); }