Kraft102's picture
Initial deployment - WidgeTDC Cortex Backend v2.1.0
529090e
/**
* 🧠 KNOWLEDGE GRAPH SERVICE
* Gemmer harvested filer i Neo4j som et selvbevidst knowledge graph
*/
import neo4j, { Driver, Session } from 'neo4j-driver';
interface FileNode {
path: string;
name: string;
extension: string;
content: string;
hash: string;
lines: number;
size: number;
}
interface GraphStats {
totalNodes: number;
totalRelationships: number;
filesByType: Record<string, number>;
importGraph: { from: string; to: string }[];
}
export class KnowledgeGraph {
private driver: Driver;
constructor() {
const uri = process.env.NEO4J_URI || 'bolt://neo4j:7687';
const user = process.env.NEO4J_USER || 'neo4j';
const password = process.env.NEO4J_PASSWORD || 'password';
this.driver = neo4j.driver(uri, neo4j.auth.basic(user, password));
console.log('🧠 KnowledgeGraph connected to Neo4j');
}
/**
* Ingest alle filer som nodes i grafen
*/
async ingestFiles(files: FileNode[]): Promise<{ created: number; updated: number }> {
const session = this.driver.session();
let created = 0;
let updated = 0;
try {
// Batch ingest for performance
for (const file of files) {
const result = await session.run(`
MERGE (f:File {path: $path})
ON CREATE SET
f.name = $name,
f.extension = $extension,
f.hash = $hash,
f.lines = $lines,
f.size = $size,
f.content = $content,
f.createdAt = datetime(),
f.updatedAt = datetime()
ON MATCH SET
f.hash = $hash,
f.lines = $lines,
f.size = $size,
f.content = $content,
f.updatedAt = datetime()
RETURN f.path AS path,
CASE WHEN f.createdAt = f.updatedAt THEN 'created' ELSE 'updated' END AS action
`, {
path: file.path,
name: file.name,
extension: file.extension,
hash: file.hash,
lines: file.lines,
size: file.size,
content: file.content.substring(0, 50000) // Truncate for Neo4j
});
const action = result.records[0]?.get('action');
if (action === 'created') created++;
else updated++;
}
// Create directory hierarchy
await this.createDirectoryHierarchy(session);
// Extract and create import relationships
await this.extractImportRelationships(session);
console.log(`✅ Ingested ${created} new, ${updated} updated files`);
return { created, updated };
} finally {
await session.close();
}
}
/**
* Opret mappe-hierarki som nodes
*/
private async createDirectoryHierarchy(session: Session): Promise<void> {
await session.run(`
MATCH (f:File)
WITH f, split(f.path, '/') AS parts
UNWIND range(1, size(parts)-1) AS idx
WITH f, reduce(s = '', i IN range(0, idx-1) | s + '/' + parts[i]) AS dirPath
WHERE dirPath <> ''
MERGE (d:Directory {path: dirPath})
MERGE (f)-[:IN_DIRECTORY]->(d)
`);
// Parent directory relationships
await session.run(`
MATCH (d:Directory)
WITH d, split(d.path, '/') AS parts
WHERE size(parts) > 2
WITH d, reduce(s = '', i IN range(0, size(parts)-2) | s + '/' + parts[i]) AS parentPath
WHERE parentPath <> ''
MERGE (p:Directory {path: parentPath})
MERGE (d)-[:SUBDIRECTORY_OF]->(p)
`);
}
/**
* Ekstraher import statements og opret relationer
*/
private async extractImportRelationships(session: Session): Promise<void> {
// TypeScript/JavaScript imports
await session.run(`
MATCH (f:File)
WHERE f.extension IN ['.ts', '.tsx', '.js', '.jsx']
WITH f, f.content AS content
// Match import statements
WITH f,
[x IN split(content, '\n') WHERE x CONTAINS 'import ' AND x CONTAINS 'from'] AS imports
UNWIND imports AS importLine
// Extract the path from import
WITH f, importLine,
trim(split(split(importLine, 'from')[1], ';')[0]) AS rawPath
WHERE rawPath IS NOT NULL AND rawPath <> ''
// Clean quotes
WITH f, replace(replace(rawPath, '"', ''), "'", '') AS importPath
WHERE importPath STARTS WITH '.' OR importPath STARTS WITH '/'
// Try to find matching file
OPTIONAL MATCH (target:File)
WHERE target.path CONTAINS split(importPath, '/')[-1]
FOREACH (_ IN CASE WHEN target IS NOT NULL THEN [1] ELSE [] END |
MERGE (f)-[:IMPORTS]->(target)
)
`);
}
/**
* Hent graf statistikker
*/
async getStats(): Promise<GraphStats> {
const session = this.driver.session();
try {
// Total counts
const countResult = await session.run(`
MATCH (f:File)
RETURN count(f) AS files,
count { MATCH ()-[r:IMPORTS]->() RETURN r } AS imports,
count { MATCH ()-[r:IN_DIRECTORY]->() RETURN r } AS dirRels
`);
const counts = countResult.records[0];
// Files by extension
const extResult = await session.run(`
MATCH (f:File)
RETURN f.extension AS ext, count(f) AS count
ORDER BY count DESC
`);
const filesByType: Record<string, number> = {};
extResult.records.forEach(r => {
filesByType[r.get('ext')] = r.get('count').toNumber();
});
// Import graph (top connections)
const importResult = await session.run(`
MATCH (a:File)-[:IMPORTS]->(b:File)
RETURN a.name AS from, b.name AS to
LIMIT 50
`);
const importGraph = importResult.records.map(r => ({
from: r.get('from'),
to: r.get('to')
}));
return {
totalNodes: counts.get('files').toNumber(),
totalRelationships: counts.get('imports').toNumber() + counts.get('dirRels').toNumber(),
filesByType,
importGraph
};
} finally {
await session.close();
}
}
/**
* Søg i grafen med Cypher
*/
async query(cypher: string, params: Record<string, any> = {}): Promise<any[]> {
const session = this.driver.session();
try {
const result = await session.run(cypher, params);
return result.records.map(r => r.toObject());
} finally {
await session.close();
}
}
/**
* Find relaterede filer (imports, same directory, etc.)
*/
async findRelated(filePath: string): Promise<{ imports: string[]; importedBy: string[]; sameDir: string[] }> {
const session = this.driver.session();
try {
const result = await session.run(`
MATCH (f:File {path: $path})
OPTIONAL MATCH (f)-[:IMPORTS]->(imported:File)
OPTIONAL MATCH (importer:File)-[:IMPORTS]->(f)
OPTIONAL MATCH (f)-[:IN_DIRECTORY]->(d:Directory)<-[:IN_DIRECTORY]-(sibling:File)
WHERE sibling <> f
RETURN
collect(DISTINCT imported.path) AS imports,
collect(DISTINCT importer.path) AS importedBy,
collect(DISTINCT sibling.path)[0..10] AS sameDir
`, { path: filePath });
const record = result.records[0];
return {
imports: record.get('imports').filter(Boolean),
importedBy: record.get('importedBy').filter(Boolean),
sameDir: record.get('sameDir').filter(Boolean)
};
} finally {
await session.close();
}
}
async close(): Promise<void> {
await this.driver.close();
}
}