UNPKG

scai

Version:

> **AI-powered CLI for local code analysis, commit message suggestions, and natural-language queries.** 100% local, private, GDPR-friendly, made in Denmark/EU with ❤️.

257 lines (256 loc) 8.31 kB
// src/utils/buildContextualPrompt.ts import path from "path"; import fs from "fs"; import { getDbForRepo } from "../db/client.js"; import { generateFileTree, generateFocusedFileTree } from "./fileTree.js"; import { RELATED_FILES_LIMIT } from "../constants.js"; import { loadRelevantFolderCapsules } from "./loadRelevantFolderCapsules.js"; /* --- Constants --- */ const MAX_FUNCTIONS = 50; const MAX_KG_NEIGHBORS = 50; const DEFAULT_KG_DEPTH = 3; /* --- Helpers --- */ function fileRowIdForPath(db, filePath) { try { const row = db .prepare(`SELECT id FROM files WHERE path = ?`) .get(filePath); return row?.id; } catch { return undefined; } } /* ---------------- KG helpers ---------------- */ function loadKgTags(db, entityUniqueId, limit = MAX_KG_NEIGHBORS) { try { const rows = db .prepare(` SELECT tm.name as tag FROM graph_entity_tags et JOIN graph_tags_master tm ON tm.id = et.tag_id WHERE et.entity_unique_id = ? LIMIT ? `) .all(entityUniqueId, limit); return rows.map((r) => r.tag); } catch { return []; } } function loadKgNeighbors(db, sourceUniqueId, limit = MAX_KG_NEIGHBORS) { try { const rows = db .prepare(` SELECT relation, target_unique_id as target FROM graph_edges WHERE source_unique_id = ? LIMIT ? `) .all(sourceUniqueId, limit); return rows.map((r) => ({ relation: r.relation, target: r.target })); } catch { return []; } } /* ---------------- Functions / classes ---------------- */ function loadFunctions(db, fileId, limit = MAX_FUNCTIONS) { if (!fileId) return []; try { const rows = db .prepare(`SELECT name, start_line, end_line FROM functions WHERE file_id = ? ORDER BY start_line LIMIT ?`) .all(fileId, limit); return rows.map((r) => ({ name: r.name ?? undefined, start: r.start_line, end: r.end_line, })); } catch { return []; } } function loadClasses(db, fileId, limit = 50) { if (!fileId) return []; try { const rows = db .prepare(`SELECT name, start_line, end_line FROM graph_classes WHERE file_id = ? ORDER BY start_line LIMIT ?`) .all(fileId, limit); return rows.map((r) => ({ name: r.name ?? undefined, start: r.start_line, end: r.end_line, })); } catch { return []; } } /* ---------------- Trees ---------------- */ function safeGenerateFocusedTree(filePath, depth = 2) { try { return generateFocusedFileTree(filePath, depth) || undefined; } catch { return undefined; } } function safeGenerateRepoTree(depth = 2) { try { const root = process.cwd(); return generateFileTree(root, depth) || undefined; } catch { return undefined; } } /* ====================================================== LIGHT CONTEXT ====================================================== */ export async function buildLightContext(args) { const db = getDbForRepo(); const safeTopFiles = Array.isArray(args.topFiles) ? args.topFiles : []; const safeRelated = Array.isArray(args.relatedFiles) ? args.relatedFiles : []; const ctx = { initContext: { userQuery: (args.query || "").trim(), repoTree: safeGenerateRepoTree(2), relatedFiles: [], folderCapsules: [], }, }; /* -------- Collect related file paths -------- */ const relatedPaths = []; for (const tf of safeTopFiles) { if (!relatedPaths.includes(tf.path)) relatedPaths.push(tf.path); } for (let i = 0; i < Math.min(RELATED_FILES_LIMIT, safeRelated.length); i++) { const p = safeRelated[i].path; if (!relatedPaths.includes(p)) relatedPaths.push(p); } /* -------- Add file references from user query -------- */ const referencedFiles = extractFileReferences(ctx.initContext.userQuery); const topFilePaths = []; for (const ref of referencedFiles) { const found = safeTopFiles.find(f => f.path.toLowerCase().includes(ref)); if (found && !topFilePaths.includes(found.path)) { topFilePaths.push(found.path); } } // Merge query references into relatedPaths for (const tf of topFilePaths) { if (!relatedPaths.includes(tf)) relatedPaths.push(tf); } ctx.initContext.relatedFiles = relatedPaths; /* -------- Folder capsules (orientation layer) -------- */ const folderPaths = normalizeToFolders(relatedPaths); let folderCapsules = loadRelevantFolderCapsules(folderPaths); // Ensure root capsule exists const rootExists = folderCapsules.some(c => c.path === '/'); if (!rootExists) { const rootCapsule = { path: '/', depth: 1, stats: { fileCount: 0, byType: {} }, roles: [], concerns: [], keyFiles: [], dependencies: { importsFrom: [], usedBy: [] }, confidence: 0.5, }; folderCapsules.unshift(rootCapsule); } ctx.initContext.folderCapsules = folderCapsules; return ctx; } /* -------- Helper: get folders from filepaths -------- */ function normalizeToFolders(paths) { const out = new Set(); for (const p of paths) { try { const stat = fs.statSync(p); if (stat.isDirectory()) { out.add(p); } else if (stat.isFile()) { out.add(path.dirname(p)); } } catch { // ignore invalid paths } } return [...out]; } /* -------- Helper: extract file references from query -------- */ function extractFileReferences(query) { const matches = []; const filenameRegex = /[\w\-\/]+\.\w{1,6}/gi; const explicit = query.match(filenameRegex); if (explicit) matches.push(...explicit.map(s => s.toLowerCase())); return [...new Set(matches)]; } /* ====================================================== IN-DEPTH CONTEXT ====================================================== */ export async function buildInDepthContext({ filenames, kgDepth = DEFAULT_KG_DEPTH, relatedFiles, query, }) { const db = getDbForRepo(); const safeFilenames = Array.isArray(filenames) ? filenames : []; const safeRelated = Array.isArray(relatedFiles) ? relatedFiles : []; const initCtx = { userQuery: query?.trim() || "", repoTree: safeGenerateRepoTree(3), relatedFiles: safeRelated, folderCapsules: loadRelevantFolderCapsules(normalizeToFolders(safeRelated)), }; const workingFiles = []; const out = { initContext: initCtx, workingFiles, }; /* -------- Working files (deep phase only) -------- */ for (const p of safeFilenames) { const fileId = fileRowIdForPath(db, p); const fileObj = { path: p }; if (typeof fileId === "number") { fileObj.functions = loadFunctions(db, fileId, MAX_FUNCTIONS); fileObj.classes = loadClasses(db, fileId, 200); } const neighbors = loadKgNeighbors(db, p, MAX_KG_NEIGHBORS); fileObj.kgTags = loadKgTags(db, p, MAX_KG_NEIGHBORS); if (neighbors.length) { fileObj.kgNeighborhood = neighbors.map((e) => `${e.relation}:${e.target}`); const imports = neighbors .filter((e) => e.relation === "imports") .map((e) => e.target); const exports = neighbors .filter((e) => e.relation === "exports") .map((e) => e.target); if (imports.length) fileObj.imports = imports.slice(0, 200); if (exports.length) fileObj.exports = exports.slice(0, 200); } fileObj.focusedTree = safeGenerateFocusedTree(p, 3); workingFiles.push(fileObj); } return out; }