arela
Version:
AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.
479 lines • 15.6 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");
}
/**
* Hexi-005: Initialization wrapper.
* Construction with `cwd` is sufficient today; this simply ensures the directory exists.
*/
async init(projectPath) {
if (projectPath && path.resolve(projectPath) !== path.resolve(this.cwd)) {
// Keep backwards compatible – we don't enforce equality here.
}
await fs.ensureDir(path.dirname(this.dbPath));
}
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();
let lastUpdatedAt;
try {
const stats = await fs.stat(this.dbPath);
lastUpdatedAt = stats.mtimeMs;
}
catch {
lastUpdatedAt = undefined;
}
return {
ready: true,
files: summary.filesCount,
imports: summary.importsCount,
functions: summary.functionsCount,
functionCalls: summary.functionCallsCount,
apiEndpoints: summary.apiEndpointsCount,
apiCalls: summary.apiCallsCount,
dbPath: this.dbPath,
lastUpdatedAt,
};
}
finally {
db.close();
}
}
/**
* Existing impact analysis used by Tri-Memory.
*/
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();
}
}
/**
* Hexi-005: Get a single file node by path.
*/
async getFile(pathOrIdentifier) {
if (!(await this.isReady())) {
return undefined;
}
const normalized = this.normalizePath(pathOrIdentifier);
const db = new GraphDB(this.dbPath);
try {
const rows = db.query("SELECT path, repo, type, lines FROM files WHERE path = ?", [normalized]);
if (rows.length === 0) {
return undefined;
}
const row = rows[0];
return {
path: row.path,
repoPath: row.repo,
language: inferLanguageFromPath(row.path),
size: row.lines ?? 0,
};
}
finally {
db.close();
}
}
/**
* Hexi-005: Get all files, optionally filtered by repo path.
*/
async getFiles(repoPaths) {
if (!(await this.isReady())) {
return [];
}
const db = new GraphDB(this.dbPath);
try {
let sql = "SELECT path, repo, type, lines FROM files";
const params = [];
if (repoPaths && repoPaths.length > 0) {
const placeholders = repoPaths.map(() => "?").join(", ");
sql += ` WHERE repo IN (${placeholders})`;
params.push(...repoPaths);
}
const rows = db.query(sql, params);
return rows.map((row) => ({
path: row.path,
repoPath: row.repo,
language: inferLanguageFromPath(row.path),
size: row.lines ?? 0,
}));
}
finally {
db.close();
}
}
/**
* Hexi-005: Simple file name search using LIKE on path.
*/
async searchFiles(pattern) {
if (!(await this.isReady())) {
return [];
}
const db = new GraphDB(this.dbPath);
try {
const rows = db.query("SELECT path, repo, type, lines FROM files WHERE path LIKE ? ORDER BY path", [`%${pattern}%`]);
return rows.map((row) => ({
path: row.path,
repoPath: row.repo,
language: inferLanguageFromPath(row.path),
size: row.lines ?? 0,
}));
}
finally {
db.close();
}
}
/**
* Hexi-005: Get direct import edges from a file.
*/
async getImports(filePath) {
if (!(await this.isReady())) {
return [];
}
const normalized = this.normalizePath(filePath);
const db = new GraphDB(this.dbPath);
try {
const fileId = db.getFileId(normalized);
if (!fileId) {
return [];
}
const rows = db.query(`
SELECT
ff.path as source,
COALESCE(tf.path, i.to_module) as target,
i.to_module as module_name
FROM imports i
JOIN files ff ON ff.id = i.from_file_id
LEFT JOIN files tf ON tf.id = i.to_file_id
WHERE i.from_file_id = ?
`, [fileId]);
return rows
.filter((row) => Boolean(row.target))
.map((row) => ({
source: row.source,
target: row.target,
type: isInternalPath(row.target) ? "internal" : "external",
}));
}
finally {
db.close();
}
}
/**
* Hexi-005: Get all files that import the given file.
*/
async getImportedBy(filePath) {
if (!(await this.isReady())) {
return [];
}
const normalized = this.normalizePath(filePath);
const db = new GraphDB(this.dbPath);
try {
const fileId = db.getFileId(normalized);
if (!fileId) {
return [];
}
const rows = db.query(`
SELECT DISTINCT ff.path as source
FROM imports i
JOIN files ff ON ff.id = i.from_file_id
WHERE i.to_file_id = ?
`, [fileId]);
return rows.map((row) => row.source);
}
finally {
db.close();
}
}
/**
* Hexi-005: Get transitive dependencies (files this file depends on).
*/
async getDependencies(filePath, depth = 1) {
return this.walkDependencies(filePath, depth, "downstream");
}
/**
* Hexi-005: Get transitive dependents (files that depend on this file).
*/
async getDependents(filePath, depth = 1) {
return this.walkDependencies(filePath, depth, "upstream");
}
/**
* Hexi-005: Get functions in a file.
*/
async getFunctions(filePath) {
if (!(await this.isReady())) {
return [];
}
const normalized = this.normalizePath(filePath);
const db = new GraphDB(this.dbPath);
try {
const fileId = db.getFileId(normalized);
if (!fileId) {
return [];
}
const rows = db.query(`
SELECT name, line_start, line_end
FROM functions
WHERE file_id = ?
ORDER BY line_start
`, [fileId]);
return rows.map((row) => ({
name: row.name,
file: normalized,
lineStart: row.line_start,
lineEnd: row.line_end,
}));
}
finally {
db.close();
}
}
/**
* Hexi-005: Search functions by name (substring match).
*/
async searchFunctions(name) {
if (!(await this.isReady())) {
return [];
}
const db = new GraphDB(this.dbPath);
try {
const rows = db.query(`
SELECT f.name, f.line_start, f.line_end, files.path as file
FROM functions f
JOIN files ON files.id = f.file_id
WHERE f.name LIKE ?
ORDER BY files.path, f.line_start
`, [`%${name}%`]);
return rows.map((row) => ({
name: row.name,
file: row.file,
lineStart: row.line_start,
lineEnd: row.line_end,
}));
}
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);
}
async walkDependencies(filePath, depth, direction) {
if (depth <= 0 || !(await this.isReady())) {
return [];
}
const visited = new Set();
const queue = [];
const normalized = this.normalizePath(filePath);
queue.push({ path: normalized, level: 0 });
visited.add(normalized);
const db = new GraphDB(this.dbPath);
try {
const results = new Set();
while (queue.length > 0) {
const current = queue.shift();
if (current.level >= depth) {
continue;
}
const fileId = db.getFileId(current.path);
if (!fileId) {
continue;
}
const neighbors = direction === "downstream"
? 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 = ?
`, [fileId])
: db.query(`
SELECT DISTINCT f.path as file
FROM imports i
JOIN files f ON f.id = i.from_file_id
WHERE i.to_file_id = ?
`, [fileId]);
for (const neighbor of neighbors) {
if (!neighbor.file || visited.has(neighbor.file)) {
continue;
}
visited.add(neighbor.file);
results.add(neighbor.file);
queue.push({ path: neighbor.file, level: current.level + 1 });
}
}
return Array.from(results).sort();
}
finally {
db.close();
}
}
}
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";
}
function inferLanguageFromPath(filePath) {
const ext = path.extname(filePath).toLowerCase();
switch (ext) {
case ".ts":
case ".tsx":
return "TypeScript";
case ".js":
case ".jsx":
return "JavaScript";
case ".py":
return "Python";
case ".go":
return "Go";
case ".rs":
return "Rust";
case ".java":
return "Java";
default:
return "unknown";
}
}
function isInternalPath(target) {
return target.startsWith(".") || target.startsWith("/") || target.includes("/");
}
//# sourceMappingURL=graph.js.map