arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
177 lines • 6.1 kB
JavaScript
import path from "node:path";
import fs from "fs-extra";
import { GraphDB } from "../ingest/storage.js";
export class GraphMemory {
cwd;
constructor(cwd = process.cwd()) {
this.cwd = cwd;
}
get dbPath() {
return path.join(this.cwd, ".arela", "memory", "graph.db");
}
async isReady() {
return fs.pathExists(this.dbPath);
}
async getStats() {
if (!(await this.isReady())) {
return {
ready: false,
files: 0,
imports: 0,
functions: 0,
functionCalls: 0,
apiEndpoints: 0,
apiCalls: 0,
dbPath: this.dbPath,
};
}
const db = new GraphDB(this.dbPath);
try {
const summary = db.getSummary();
return {
ready: true,
files: summary.filesCount,
imports: summary.importsCount,
functions: summary.functionsCount,
functionCalls: summary.functionCallsCount,
apiEndpoints: summary.apiEndpointsCount,
apiCalls: summary.apiCallsCount,
dbPath: this.dbPath,
};
}
finally {
db.close();
}
}
async impact(filePath) {
if (!(await this.isReady())) {
throw new Error("Graph memory not initialized. Run `arela memory init --refresh-graph` or `arela ingest codebase` first.");
}
const normalized = this.normalizePath(filePath);
const db = new GraphDB(this.dbPath);
try {
const fileId = db.getFileId(normalized);
if (!fileId) {
return {
file: normalized,
exists: false,
upstream: [],
downstream: [],
fanIn: 0,
fanOut: 0,
};
}
const upstream = db.query(`
SELECT f.path as file,
COUNT(*) as weight,
GROUP_CONCAT(DISTINCT COALESCE(i.import_type, 'unknown')) as import_types
FROM imports i
JOIN files f ON f.id = i.from_file_id
WHERE i.to_file_id = ?
GROUP BY f.path
ORDER BY weight DESC, f.path ASC
`, [fileId]);
const downstream = db.query(`
SELECT f.path as file,
COUNT(*) as weight,
GROUP_CONCAT(DISTINCT COALESCE(i.import_type, 'unknown')) as import_types
FROM imports i
JOIN files f ON f.id = i.to_file_id
WHERE i.from_file_id = ?
GROUP BY f.path
ORDER BY weight DESC, f.path ASC
`, [fileId]);
const upstreamEdges = mapDependencyEdges(upstream);
const downstreamEdges = mapDependencyEdges(downstream);
return {
file: normalized,
exists: true,
upstream: upstreamEdges,
downstream: downstreamEdges,
fanIn: upstreamEdges.reduce((sum, edge) => sum + edge.weight, 0),
fanOut: downstreamEdges.reduce((sum, edge) => sum + edge.weight, 0),
};
}
finally {
db.close();
}
}
async findSlice(identifier) {
if (!(await this.isReady())) {
return [];
}
const normalized = this.normalizePath(identifier);
const db = new GraphDB(this.dbPath);
try {
const targetIds = this.findCandidateFileIds(db, identifier);
if (targetIds.length === 0) {
return [];
}
const related = new Set();
related.add(normalized);
for (const id of targetIds) {
const neighbors = db.query(`
SELECT DISTINCT f.path as file
FROM imports i
JOIN files f ON f.id = i.to_file_id
WHERE i.from_file_id = ?
UNION
SELECT DISTINCT f2.path as file
FROM imports i2
JOIN files f2 ON f2.id = i2.from_file_id
WHERE i2.to_file_id = ?
`, [id, id]);
for (const neighbor of neighbors) {
if (neighbor.file && neighbor.file !== normalized) {
related.add(neighbor.file);
}
}
}
related.delete(normalized);
return Array.from(related).sort();
}
finally {
db.close();
}
}
findCandidateFileIds(db, identifier) {
const normalized = this.normalizePath(identifier);
const candidates = [];
const primaryId = db.getFileId(normalized);
if (primaryId) {
candidates.push(primaryId);
}
if (candidates.length === 0) {
const basename = path.basename(normalized);
const rows = db.query(`SELECT id FROM files WHERE path LIKE ? ORDER BY path LIMIT 25`, [`%${basename}%`]);
for (const row of rows) {
if (typeof row.id === "number") {
candidates.push(row.id);
}
}
}
return candidates;
}
normalizePath(filePath) {
const absolute = path.isAbsolute(filePath) ? filePath : path.join(this.cwd, filePath);
const relative = path.relative(this.cwd, absolute);
return relative.split(path.sep).join(path.posix.sep);
}
}
function mapDependencyEdges(rows) {
return rows
.filter((row) => Boolean(row.file))
.map((row) => ({
file: row.file,
reason: formatReason(row.import_types),
weight: typeof row.weight === "number" ? row.weight : Number(row.weight ?? 0),
}));
}
function formatReason(value) {
if (!value) {
return "imports";
}
const parts = value.split(",").map((part) => part.trim()).filter(Boolean);
return parts.length > 0 ? parts.join(", ") : "imports";
}
//# sourceMappingURL=graph.js.map