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 ❤️.
267 lines (266 loc) • 12 kB
JavaScript
import { Project, SyntaxKind, } from 'ts-morph';
import path from 'path';
import { log } from '../../utils/log.js';
import { getDbForRepo } from '../client.js';
import { markFileAsSkippedTemplate, markFileAsExtractedTemplate, markFileAsFailedTemplate, insertFunctionTemplate, insertGraphClassTemplate, insertEdgeTemplate, insertGraphEntityTagTemplate, insertGraphTagTemplate, selectGraphTagIdTemplate, } from '../sqlTemplates.js';
import { kgModule } from '../../pipeline/modules/kgModule.js';
import { BUILTINS } from '../../fileRules/builtins.js';
import { getUniqueId } from '../../utils/sharedUtils.js';
export async function extractFromTS(filePath, content, fileId) {
const db = getDbForRepo();
const normalizedPath = path.normalize(filePath).replace(/\\/g, '/');
try {
const project = new Project({ useInMemoryFileSystem: true });
const sourceFile = project.createSourceFile(filePath, content);
const functions = [];
const classes = [];
// --- Gather AST nodes ---
const allFuncNodes = [
...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration),
...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionExpression),
...sourceFile.getDescendantsOfKind(SyntaxKind.ArrowFunction),
];
const allClassNodes = [
...sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration),
...sourceFile.getDescendantsOfKind(SyntaxKind.ClassExpression),
];
if (allFuncNodes.length === 0 && allClassNodes.length === 0) {
log(`⚠️ No functions/classes found in TS file: ${filePath}`);
try {
db.prepare(markFileAsSkippedTemplate).run({ id: fileId });
}
catch { }
return false;
}
log(`🔍 Found ${allFuncNodes.length} functions and ${allClassNodes.length} classes in ${filePath}`);
// --- Knowledge Graph tagging ---
try {
const kgInput = { fileId, filepath: filePath, summary: undefined };
const kgResult = await kgModule.run(kgInput, content);
if (!kgResult.entities?.length) {
log(`⚠️ KG returned no entities for ${filePath}`);
try {
db.prepare(markFileAsFailedTemplate).run({ id: fileId });
}
catch { }
return false;
}
const insertTagStmt = db.prepare(insertGraphTagTemplate);
const getTagIdStmt = db.prepare(selectGraphTagIdTemplate);
const insertEntityTagStmt = db.prepare(insertGraphEntityTagTemplate);
const persistTag = (tag) => {
try {
insertTagStmt.run({ name: tag });
const tagRow = getTagIdStmt.get({ name: tag });
return tagRow?.id;
}
catch {
return undefined;
}
};
const persistEntityTags = (entity) => {
if (!entity.type || !Array.isArray(entity.tags) || entity.tags.length === 0)
return;
for (const tag of entity.tags) {
if (!tag || typeof tag !== 'string')
continue;
const tagId = persistTag(tag);
if (!tagId)
continue;
const matchedUniqueId = functions.find(f => f.name === entity.name)?.unique_id ||
classes.find(c => c.name === entity.name)?.unique_id ||
`${entity.name}@${filePath}`;
try {
insertEntityTagStmt.run({
entity_type: entity.type,
entity_unique_id: matchedUniqueId,
tag_id: tagId,
});
}
catch { }
}
};
kgResult.entities.forEach(persistEntityTags);
log(`🏷 Persisted LLM-generated tags for ${filePath}`);
}
catch (kgErr) {
log(`⚠️ KG tagging failed for ${filePath}: ${kgErr.message}`);
try {
db.prepare(markFileAsFailedTemplate).run({ id: fileId });
}
catch { }
return false;
}
// --- Collect functions ---
const tsBuiltins = BUILTINS.ts;
for (const funcNode of allFuncNodes) {
const name = funcNode.getSymbol()?.getName() ?? '<anon>';
const start = funcNode.getStartLineNumber();
const end = funcNode.getEndLineNumber();
const code = funcNode.getText();
const unique_id = getUniqueId(name, filePath, start, funcNode.getStart(), code);
functions.push({ name, start_line: start, end_line: end, content: code, unique_id });
try {
const callExprs = funcNode.getDescendantsOfKind(SyntaxKind.CallExpression);
const edgeSet = new Set();
for (const callExpr of callExprs) {
let calleeName;
try {
const expr = callExpr.getExpression();
calleeName = expr.getSymbol()?.getName() ?? expr.getText();
}
catch { /* ignore symbol resolution errors */ }
if (!calleeName || tsBuiltins.has(calleeName))
continue;
const targetUniqueId = `${calleeName}@${normalizedPath}`;
const edgeKey = `${unique_id}->${targetUniqueId}`;
if (!edgeSet.has(edgeKey)) {
edgeSet.add(edgeKey);
try {
db.prepare(insertEdgeTemplate).run({
source_type: 'function',
source_unique_id: unique_id,
target_type: 'function',
target_unique_id: targetUniqueId,
relation: 'calls',
});
}
catch { /* ignore DB insert errors */ }
}
}
}
catch (funcErr) {
log(`⚠️ Failed to process call expressions in function ${name} of ${filePath}: ${funcErr.message}`);
}
}
// --- Collect classes ---
for (const clsNode of allClassNodes) {
const name = clsNode.getName() ?? `${path.basename(filePath)}:<anon-class>`;
const start = clsNode.getStartLineNumber();
const end = clsNode.getEndLineNumber();
const code = clsNode.getText();
let superClass = null;
try {
superClass = clsNode.getExtends()?.getText() ?? null;
}
catch { /* ignore */ }
const unique_id = getUniqueId(name, filePath, start, clsNode.getStart(), code);
classes.push({ name, start_line: start, end_line: end, content: code, superClass, unique_id });
if (superClass) {
try {
db.prepare(insertEdgeTemplate).run({
source_type: 'class',
source_unique_id: unique_id,
target_type: 'class',
target_unique_id: `unresolved:${superClass}`,
relation: 'inherits',
});
}
catch { /* ignore DB insert errors */ }
}
}
const seenEdges = new Set();
// --- Insert functions ---
for (const fn of functions) {
db.prepare(insertFunctionTemplate).run({
file_id: fileId,
name: fn.name ?? '<anon>',
start_line: fn.start_line ?? -1,
end_line: fn.end_line ?? -1,
content: fn.content ?? '',
embedding: null,
lang: 'ts',
unique_id: fn.unique_id,
});
const containsEdgeKey = `file->${fn.unique_id}`;
if (!seenEdges.has(containsEdgeKey)) {
db.prepare(insertEdgeTemplate).run({
source_type: 'file',
source_unique_id: normalizedPath,
target_type: 'function',
target_unique_id: fn.unique_id,
relation: 'contains',
});
seenEdges.add(containsEdgeKey);
}
}
// --- Insert classes ---
for (const cls of classes) {
db.prepare(insertGraphClassTemplate).run({
file_id: fileId,
name: cls.name,
start_line: cls.start_line ?? -1,
end_line: cls.end_line ?? -1,
content: cls.content ?? '',
embedding: null,
lang: 'ts',
unique_id: cls.unique_id,
});
const containsEdgeKey = `file->${cls.unique_id}`;
if (!seenEdges.has(containsEdgeKey)) {
db.prepare(insertEdgeTemplate).run({
source_type: 'file',
source_unique_id: normalizedPath,
target_type: 'class',
target_unique_id: cls.unique_id,
relation: 'contains',
});
seenEdges.add(containsEdgeKey);
}
}
// --- Imports ---
const importDecls = sourceFile.getDescendantsOfKind(SyntaxKind.ImportDeclaration);
for (const imp of importDecls) {
const moduleSpecifier = imp.getModuleSpecifierValue();
if (!moduleSpecifier)
continue;
const resolvedPath = moduleSpecifier.startsWith('.')
? path.resolve(path.dirname(filePath), moduleSpecifier)
: moduleSpecifier;
const normalizedTarget = path.normalize(resolvedPath).replace(/\\/g, '/');
try {
db.prepare(insertEdgeTemplate).run({
source_type: 'file',
source_unique_id: normalizedPath,
target_type: 'file',
target_unique_id: normalizedTarget,
relation: 'imports',
});
}
catch { /* ignore */ }
}
// --- Exports ---
const exportDecls = sourceFile.getDescendantsOfKind(SyntaxKind.ExportDeclaration);
for (const exp of exportDecls) {
const moduleSpecifier = exp.getModuleSpecifierValue();
if (!moduleSpecifier)
continue;
const resolvedPath = moduleSpecifier.startsWith('.')
? path.resolve(path.dirname(filePath), moduleSpecifier)
: moduleSpecifier;
const normalizedTarget = path.normalize(resolvedPath).replace(/\\/g, '/');
try {
db.prepare(insertEdgeTemplate).run({
source_type: 'file',
source_unique_id: normalizedPath,
target_type: 'file',
target_unique_id: normalizedTarget,
relation: 'exports',
});
}
catch { /* ignore */ }
}
db.prepare(markFileAsExtractedTemplate).run({ id: fileId });
log(`📊 Extraction summary for ${filePath}: ${functions.length} functions, ${classes.length} classes`);
return true;
}
catch (err) {
log(`❌ Failed to extract from TS file: ${filePath}`);
log(` ↳ ${err.message}`);
try {
db.prepare(markFileAsFailedTemplate).run({ id: fileId });
}
catch { }
return false;
}
}