/* eslint-disable no-console */ import Parser, {Tree, Query} from 'web-tree-sitter' import {Project, SyntaxKind} from 'ts-morph' import {SymbolGraphCache, IncrementalSymbolGraph} from './symbol-graph-cache' import {createHash} from 'crypto' import {discoverFiles, FileDiscoverer} from './file-discoverer' import { LRUCache } from 'lru-cache' import pino from 'pino' import {readFile, writeFile} from 'fs/promises' import {existsSync, readdirSync} from 'fs' const logger = pino({ level: process.env.LOG_LEVEL || 'info' }) // Initialize Tree-sitter once at module load time let parserInitialized = false const initParser = async (): Promise => { if (parserInitialized) return true try { await Parser.init() parserInitialized = true logger.info('Tree-sitter parser initialized') return true } catch (e) { logger.warn({ error: e }, 'Tree-sitter init failed, using fallback parsing') return false } } export interface Symbol { name: string type: | 'function' | 'class' | 'interface' | 'type' | 'variable' | 'method' | 'const' file: string line: number exports: string[] imports: string[] references: string[] } export interface DependencyEdge { from: string to: string type: 'imports' | 'calls' | 'extends' | 'implements' | 'uses' } export interface SymbolGraph { symbols: Map edges: DependencyEdge[] fileIndex: Map } export interface RepoContext { symbolGraph: SymbolGraph typeHierarchy: Map callGraph: Map architecturalLayers: Map } export interface ArchitecturalViolation { from: string to: string fromLayer: string toLayer: string severity: 'critical' | 'major' | 'minor' description: string } const PARSER_DIR = process.env.PARSER_DIR || '/app/parsers' const LANGUAGE_LOADERS: Record Promise> = { '.ts': async () => Parser.Language.load(`${PARSER_DIR}/tree-sitter-typescript.wasm`), '.tsx': async () => Parser.Language.load(`${PARSER_DIR}/tree-sitter-tsx.wasm`), '.js': async () => Parser.Language.load(`${PARSER_DIR}/tree-sitter-javascript.wasm`), '.jsx': async () => Parser.Language.load(`${PARSER_DIR}/tree-sitter-javascript.wasm`), '.go': async () => Parser.Language.load(`${PARSER_DIR}/tree-sitter-go.wasm`), '.py': async () => Parser.Language.load(`${PARSER_DIR}/tree-sitter-python.wasm`), '.rs': async () => { try { return Parser.Language.load(`${PARSER_DIR}/tree-sitter-rust.wasm`) } catch { return null } }, '.cpp': async () => { try { return Parser.Language.load(`${PARSER_DIR}/tree-sitter-cpp.wasm`) } catch { return null } }, '.c': async () => { try { return Parser.Language.load(`${PARSER_DIR}/tree-sitter-c.wasm`) } catch { return null } }, '.java': async () => { try { return Parser.Language.load(`${PARSER_DIR}/tree-sitter-java.wasm`) } catch { return null } } } const LANGUAGE_LOADER_CACHE_TTL_MS = 30 * 60 * 1000 // 30 minutes const LANGUAGE_LOADER_MAX_SIZE = 10 function computeContentHash(content: string): string { return createHash('sha256').update(content).digest('hex').substring(0, 16) } interface CachedLanguage { language: any } export class UnifiedContextEngine { private graph: SymbolGraph = { symbols: new Map(), edges: [], fileIndex: new Map() } private project: Project // SECURITY: LRU cache prevents memory leaks (Issue #5) private languageCache: LRUCache private graphCache!: SymbolGraphCache private incrementalGraph!: IncrementalSymbolGraph private repoPath: string = '' constructor() { this.project = new Project() // LRU cache with max size and TTL to prevent unbounded memory growth this.languageCache = new LRUCache({ max: LANGUAGE_LOADER_MAX_SIZE, ttl: LANGUAGE_LOADER_CACHE_TTL_MS, updateAgeOnGet: true, allowStale: false, dispose: (value, key) => { logger.debug({ key }, 'Language evicted from cache') } }) } async initialize(repoPath: string, stableId?: string): Promise { this.repoPath = repoPath const cacheId = stableId || repoPath this.graphCache = new SymbolGraphCache(cacheId) this.incrementalGraph = new IncrementalSymbolGraph(cacheId) await this.buildSymbolGraph(repoPath) return this.buildRepoContext() } async updateForChanges( changedFiles: string[], deletedFiles: string[] ): Promise<{ updatedFiles: string[] cacheStats: {hits: number; misses: number; hitRate: number} }> { if (!this.incrementalGraph) { return {updatedFiles: [], cacheStats: {hits: 0, misses: 0, hitRate: 0}} } const result = await this.incrementalGraph.update( changedFiles, deletedFiles ) return { updatedFiles: result.updatedFiles, cacheStats: result.cacheStats } } private async buildSymbolGraph(repoPath: string): Promise { const parserReady = await initParser() if (!parserReady) { logger.warn('Parser not available, using fallback parsing') return } // Robustness: Check if parser assets exist const parserDir = process.env.PARSER_DIR || '/app/parsers' if (!existsSync(parserDir) || readdirSync(parserDir).length === 0) { console.warn(`[PRIX] WARNING: Parser directory ${parserDir} is missing or empty. Deep auditing will be degraded.`) } const discovered = discoverFiles(repoPath, { include: [ '.ts', '.tsx', '.js', '.jsx', '.go', '.py', '.rs', '.cpp', '.c', '.h', '.java' ] }) const filesByExt = new FileDiscoverer().groupByExtension(discovered) const tsFiles = [ ...(filesByExt.get('.ts') || []), ...(filesByExt.get('.tsx') || []), ...(filesByExt.get('.js') || []), ...(filesByExt.get('.jsx') || []) ].map(f => f.path) const goFiles = (filesByExt.get('.go') || []).map(f => f.path) const pyFiles = (filesByExt.get('.py') || []).map(f => f.path) const rsFiles = (filesByExt.get('.rs') || []).map(f => f.path) const cppFiles = [ ...(filesByExt.get('.cpp') || []), ...(filesByExt.get('.c') || []), ...(filesByExt.get('.h') || []), ...(filesByExt.get('.hpp') || []), ...(filesByExt.get('.java') || []) ].map(f => f.path) console.log( `[Graph] Discovered: ${tsFiles.length} TS/JS, ${goFiles.length} Go, ${pyFiles.length} Python, ${rsFiles.length} Rust, ${cppFiles.length} C++/Java files` ) await Promise.all([ this.processTSFilesWithCache(tsFiles), this.processGoFilesWithCache(goFiles), this.processPythonFilesWithCache(pyFiles), this.processRustFilesWithCache(rsFiles), this.processCppJavaFilesWithCache(cppFiles) ]) this.buildDependencyEdges() this.graphCache?.persist() } private async processTSFilesWithCache(files: string[]): Promise { const uncachedFiles: string[] = [] for (const file of files) { const cached = this.graphCache?.get(file) if (cached) { for (const sym of cached.symbols) { this.graph.symbols.set(`${file}:${sym.name}`, sym) } if (!this.graph.fileIndex.has(file)) { this.graph.fileIndex.set(file, []) } this.graph.fileIndex.get(file)!.push(...cached.symbols) this.graph.edges.push(...cached.edges) } else { uncachedFiles.push(file) } } if (uncachedFiles.length === 0) { console.log( `[GraphCache] TS: All ${files.length} files served from cache` ) return } console.log( `[GraphCache] TS: ${uncachedFiles.length}/${files.length} files need parsing` ) for (const file of uncachedFiles) { try { const content = await readFile(file, 'utf8') await this.analyzeTypeScriptFile(file, content) } catch (e) { this.analyzeWithTsMorph(file) } } } private async processGoFilesWithCache(files: string[]): Promise { const uncachedFiles: string[] = [] for (const file of files) { const cached = this.graphCache?.get(file) if (cached) { for (const sym of cached.symbols) { this.graph.symbols.set(`${file}:${sym.name}`, sym) } if (!this.graph.fileIndex.has(file)) { this.graph.fileIndex.set(file, []) } this.graph.fileIndex.get(file)!.push(...cached.symbols) this.graph.edges.push(...cached.edges) } else { uncachedFiles.push(file) } } if (uncachedFiles.length === 0) { console.log( `[GraphCache] Go: All ${files.length} files served from cache` ) return } console.log( `[GraphCache] Go: ${uncachedFiles.length}/${files.length} files need parsing` ) for (const file of uncachedFiles) { try { const content = await readFile(file, 'utf8') await this.analyzeGoFile(file, content) } catch (e) { // Skip files that can't be read } } } private async processPythonFilesWithCache(files: string[]): Promise { const uncachedFiles: string[] = [] for (const file of files) { const cached = this.graphCache?.get(file) if (cached) { for (const sym of cached.symbols) { this.graph.symbols.set(`${file}:${sym.name}`, sym) } if (!this.graph.fileIndex.has(file)) { this.graph.fileIndex.set(file, []) } this.graph.fileIndex.get(file)!.push(...cached.symbols) this.graph.edges.push(...cached.edges) } else { uncachedFiles.push(file) } } if (uncachedFiles.length === 0) { console.log( `[GraphCache] Python: All ${files.length} files served from cache` ) return } console.log( `[GraphCache] Python: ${uncachedFiles.length}/${files.length} files need parsing` ) for (const file of uncachedFiles) { try { const content = await readFile(file, 'utf8') await this.analyzePythonFile(file, content) } catch (e) { // Skip files that can't be read } } } private async analyzeRustFile(file: string, content: string): Promise { try { const lang = await this.getLanguage(file) if (!lang) return const parser = new Parser() parser.setLanguage(lang) const tree = parser.parse(content) const symbols: Symbol[] = [] const fnQuery = lang.query(` (function_item name: (identifier) @name) @func (struct_item name: (type_identifier) @name) @struct (enum_item name: (type_identifier) @name) @enum (trait_item name: (type_identifier) @name) @trait `) for (const match of fnQuery.captures(tree.rootNode)) { const name = match.node.text let type: Symbol['type'] = 'function' if (match.name === 'struct') type = 'class' if (match.name === 'enum') type = 'type' if (match.name === 'trait') type = 'interface' symbols.push({ name, type, file, line: match.node.startPosition.row, exports: [], imports: [], references: [] }) } for (const sym of symbols) { this.graph.symbols.set(`${file}:${sym.name}`, sym) } if (!this.graph.fileIndex.has(file)) this.graph.fileIndex.set(file, []) this.graph.fileIndex.get(file)!.push(...symbols) if (this.graphCache) { const {statSync} = require('fs') const stats = statSync(file) this.graphCache.set(file, { mtime: stats.mtimeMs, size: stats.size, contentHash: computeContentHash(content), symbols, edges: [], parsedAt: Date.now() }) } } catch {} } private async analyzeCppJavaFile( file: string, content: string ): Promise { try { const lang = await this.getLanguage(file) if (!lang) return const parser = new Parser() parser.setLanguage(lang) const tree = parser.parse(content) const symbols: Symbol[] = [] const query = lang.query(` (class_declaration name: (identifier) @name) @class (method_declaration name: (identifier) @name) @method (function_definition declarator: (function_declarator declarator: (identifier) @name)) @func `) for (const match of query.captures(tree.rootNode)) { const name = match.node.text let type: Symbol['type'] = 'variable' if (match.name === 'class') type = 'class' if (match.name === 'method') type = 'method' if (match.name === 'func') type = 'function' symbols.push({ name, type, file, line: match.node.startPosition.row, exports: [], imports: [], references: [] }) } for (const sym of symbols) { this.graph.symbols.set(`${file}:${sym.name}`, sym) } if (!this.graph.fileIndex.has(file)) this.graph.fileIndex.set(file, []) this.graph.fileIndex.get(file)!.push(...symbols) if (this.graphCache) { const {statSync} = require('fs') const stats = statSync(file) this.graphCache.set(file, { mtime: stats.mtimeMs, size: stats.size, contentHash: computeContentHash(content), symbols, edges: [], parsedAt: Date.now() }) } } catch {} } private async processRustFilesWithCache(files: string[]): Promise { const uncached: string[] = [] for (const file of files) { const cached = this.graphCache?.get(file) if (cached) { for (const sym of cached.symbols) this.graph.symbols.set(`${file}:${sym.name}`, sym) if (!this.graph.fileIndex.has(file)) this.graph.fileIndex.set(file, []) this.graph.fileIndex.get(file)!.push(...cached.symbols) } else { uncached.push(file) } } for (const file of uncached) { try { const content = await readFile(file, 'utf8') await this.analyzeRustFile(file, content) } catch {} } } private async processCppJavaFilesWithCache(files: string[]): Promise { const uncached: string[] = [] for (const file of files) { const cached = this.graphCache?.get(file) if (cached) { for (const sym of cached.symbols) this.graph.symbols.set(`${file}:${sym.name}`, sym) if (!this.graph.fileIndex.has(file)) this.graph.fileIndex.set(file, []) this.graph.fileIndex.get(file)!.push(...cached.symbols) } else { uncached.push(file) } } for (const file of uncached) { try { const content = await readFile(file, 'utf8') await this.analyzeCppJavaFile(file, content) } catch {} } } private async analyzeTypeScriptFile( file: string, content: string ): Promise { const lang = await this.getLanguage(file) if (!lang) return const parser = new Parser() parser.setLanguage(lang) try { const tree = parser.parse(content) const symbols = this.extractTSSymbols(file, tree, lang) const edges = this.extractTSEdges(file, tree, lang, symbols) for (const sym of symbols) { this.graph.symbols.set(`${file}:${sym.name}`, sym) if (!this.graph.fileIndex.has(file)) { this.graph.fileIndex.set(file, []) } this.graph.fileIndex.get(file)!.push(sym) } this.graph.edges.push(...edges) if (this.graphCache) { try { const {statSync} = require('fs') const stats = statSync(file) this.graphCache.set(file, { mtime: stats.mtimeMs, size: stats.size, contentHash: computeContentHash(content), symbols, edges, parsedAt: Date.now() }) } catch {} } } catch (e) { this.analyzeWithTsMorph(file) } } private extractTSEdges( file: string, tree: Tree, lang: any, symbols: Symbol[] ): DependencyEdge[] { const edges: DependencyEdge[] = [] for (const sym of symbols) { for (const imp of sym.imports) { edges.push({ from: file, to: imp, type: 'imports' }) } } return edges } private extractTSSymbols(file: string, tree: Tree, lang: any): Symbol[] { const symbols: Symbol[] = [] const query = lang.query(` (function_declaration name: (identifier) @name parameters: (parameter_list) @params) @func (class_declaration name: (identifier) @name heritage: (class_heritage)?) @class (interface_declaration name: (type_identifier) @name) @iface (type_alias_declaration name: (type_identifier) @name) @type (variable_declaration name: (identifier) @name value: (assignment_expression) @val) @var (method_definition name: (property_identifier) @name) @method `) const queryMatches = query.captures(tree.rootNode) for (const match of queryMatches) { const name = match.node.text const symbol: Symbol = { name, type: this.inferSymbolType(match.name), file, line: match.node.startPosition.row, exports: [], imports: [], references: [] } symbols.push(symbol) } return symbols } private inferSymbolType(captureName: string): Symbol['type'] { switch (captureName) { case 'func': return 'function' case 'class': return 'class' case 'iface': return 'interface' case 'type': return 'type' case 'var': return 'variable' case 'method': return 'method' default: return 'variable' } } private analyzeWithTsMorph(file: string): void { try { const sourceFile = this.project.addSourceFileAtPath(file) sourceFile.getExportedDeclarations().forEach((declarations, name) => { const decl = declarations[0] if (!decl) return const symbol: Symbol = { name, type: this.getTsMorphSymbolType(decl.getKind()), file, line: decl.getStartLineNumber(), exports: [name], imports: this.extractImports(sourceFile), references: [] } this.graph.symbols.set(`${file}:${name}`, symbol) if (!this.graph.fileIndex.has(file)) { this.graph.fileIndex.set(file, []) } this.graph.fileIndex.get(file)!.push(symbol) }) } catch (e) { // Skip files that can't be parsed } } private getTsMorphSymbolType(kind: SyntaxKind): Symbol['type'] { const map: Record = { [SyntaxKind.FunctionDeclaration]: 'function', [SyntaxKind.ClassDeclaration]: 'class', [SyntaxKind.InterfaceDeclaration]: 'interface', [SyntaxKind.TypeAliasDeclaration]: 'type', [SyntaxKind.VariableDeclaration]: 'const', [SyntaxKind.MethodDeclaration]: 'method', [SyntaxKind.PropertyDeclaration]: 'variable' } return map[kind] || 'variable' } private extractImports(sourceFile: any): string[] { const imports: string[] = [] try { const importDefs = sourceFile.getImportDeclarations() for (const imp of importDefs) { const moduleName = imp.getModuleSpecifier().getText() imports.push(moduleName.replace(/['"]/g, '')) } } catch (e) {} return imports } private async extractGoSymbolsTreeSitter( file: string, content: string ): Promise { const symbols: Symbol[] = [] try { const lang = await this.getLanguage(file) if (!lang) return this.extractGoSymbolsRegex(content, file) const parser = new Parser() parser.setLanguage(lang) const tree = parser.parse(content) const funcQuery = lang.query(` (function_declaration name: (identifier) @funcName) @func (method_declaration receiver: (parameter_list) name: (field_identifier) @methodName) @method (type_declaration name: (type_identifier) @typeName) @type `) for (const match of funcQuery.captures(tree.rootNode)) { const name = match.node.text const line = match.node.startPosition.row + 1 if (match.name === 'funcName') { const isExported = name[0] === name[0].toUpperCase() symbols.push({ name, type: 'function', file, line, exports: isExported ? [name] : [], imports: this.extractGoImports(content), references: [] }) } else if (match.name === 'methodName') { const isExported = name[0] === name[0].toUpperCase() symbols.push({ name, type: 'method', file, line, exports: isExported ? [name] : [], imports: this.extractGoImports(content), references: [] }) } else if (match.name === 'typeName') { symbols.push({ name, type: 'class', file, line, exports: [name], imports: [], references: [] }) } } } catch (e) { return this.extractGoSymbolsRegex(content, file) } return symbols } private extractGoSymbolsRegex(content: string, file: string): Symbol[] { const symbols: Symbol[] = [] const lines = content.split('\n') for (let i = 0; i < lines.length; i++) { const line = lines[i] let match = line.match(/^func\s+(\([^*]+\)\s+)?(\*)?([A-Z][a-zA-Z0-9_]*)/) if (match) { symbols.push({ name: match[3], type: match[2] ? 'method' : 'function', file, line: i + 1, exports: match[3][0] === match[3][0].toUpperCase() ? [match[3]] : [], imports: this.extractGoImports(content), references: [] }) } match = line.match(/^type\s+([A-Z][a-zA-Z0-9_]*)\s+(struct|interface)/) if (match) { symbols.push({ name: match[1], type: match[2] === 'struct' ? 'class' : 'interface', file, line: i + 1, exports: [match[1]], imports: [], references: [] }) } } return symbols } private async extractGoSymbols( file: string, content: string ): Promise { return this.extractGoSymbolsTreeSitter(file, content) } private extractGoImports(content: string): string[] { const imports: string[] = [] const importMatch = content.match(/import\s+\(([\s\S]*?)\)/) if (importMatch) { const importBlock = importMatch[1] const quoteMatches = importBlock.match(/"([^"]+)"/g) if (quoteMatches) { for (const m of quoteMatches) { imports.push(m.replace(/"/g, '')) } } } return imports } private async analyzeGoFile(file: string, content: string): Promise { const symbols = await this.extractGoSymbols(file, content) for (const sym of symbols) { this.graph.symbols.set(`${file}:${sym.name}`, sym) if (!this.graph.fileIndex.has(file)) this.graph.fileIndex.set(file, []) this.graph.fileIndex.get(file)!.push(sym) } } private async analyzePythonFile( file: string, content: string ): Promise { const symbols = this.extractPythonSymbols(file, content) for (const sym of symbols) { this.graph.symbols.set(`${file}:${sym.name}`, sym) if (!this.graph.fileIndex.has(file)) this.graph.fileIndex.set(file, []) this.graph.fileIndex.get(file)!.push(sym) } } private extractPythonSymbols(file: string, content: string): Symbol[] { const symbols: Symbol[] = [] const lines = content.split('\n') for (let i = 0; i < lines.length; i++) { const line = lines[i] let match = line.match(/^class\s+([A-Z][a-zA-Z0-9_]*)/) if (match) { symbols.push({ name: match[1], type: 'class', file, line: i + 1, exports: [match[1]], imports: this.extractPythonImports(content), references: [] }) } match = line.match(/^def\s+([a-z_][a-zA-Z0-9_]*)/) if (match) { symbols.push({ name: match[1], type: 'function', file, line: i + 1, exports: [], imports: [], references: [] }) } match = line.match(/^([A-Z_][A-Z0-9_]*)\s*=/) if (match) { symbols.push({ name: match[1], type: 'const', file, line: i + 1, exports: [match[1]], imports: [], references: [] }) } } return symbols } private extractPythonImports(content: string): string[] { const imports: string[] = [] const importRegex = /^(?:from\s+([.\w]+)\s+)?import\s+([.\w, ]+)/gm let match while ((match = importRegex.exec(content)) !== null) { const module = match[1] || match[2] imports.push(module.trim()) } return imports } private async getLanguage(file: string): Promise { const ext = file.substring(file.lastIndexOf('.')) // LRU cache handles TTL automatically - no manual eviction needed if (this.languageCache.has(ext)) { const cached = this.languageCache.get(ext)! logger.debug({ ext }, 'Language cache hit') return cached.language } const loader = LANGUAGE_LOADERS[ext] if (loader) { try { const lang = await loader() if (lang) { // LRU cache handles size limits automatically this.languageCache.set(ext, {language: lang}) logger.info({ ext, cacheSize: this.languageCache.size }, 'Language loaded and cached') return lang } else { logger.warn({ ext }, 'Language loader returned null (wasm might be missing)') return null } } catch (e) { logger.error({ ext, error: e }, 'Error loading parser') return null } } return null } private buildDependencyEdges(): void { for (const [key, sym] of this.graph.symbols) { for (const imported of sym.imports) { for (const [otherKey, otherSym] of this.graph.symbols) { if ( otherSym.file.includes(imported) || imported.includes(otherSym.name) ) { this.graph.edges.push({ from: key, to: otherKey, type: 'imports' }) } } } } } private buildRepoContext(): RepoContext { const typeHierarchy = new Map() const callGraph = new Map() for (const [key, sym] of this.graph.symbols) { if (sym.type === 'class' || sym.type === 'interface') { const children = this.findTypeChildren(sym.name) typeHierarchy.set(key, children) } if (sym.type === 'function' || sym.type === 'method') { const callers = this.findFunctionCallers(sym.name) callGraph.set(key, callers) } } const architecturalLayers = this.inferArchitecturalLayers() return { symbolGraph: this.graph, typeHierarchy, callGraph, architecturalLayers } } private findTypeChildren(typeName: string): string[] { const children: string[] = [] for (const [, sym] of this.graph.symbols) { if (sym.type === 'class' && sym.exports.includes(typeName)) { children.push(`${sym.file}:${sym.name}`) } } return children } private findFunctionCallers(funcName: string): string[] { const callers: string[] = [] for (const [key, sym] of this.graph.symbols) { if (sym.references.includes(funcName)) { callers.push(key) } } return callers } private inferFileLayer(file: string): string | null { const lowerFile = file.toLowerCase() const patterns: Array<{layer: string; patterns: string[]}> = [ { layer: 'controller', patterns: ['controller', 'handler', 'route', 'endpoint', 'api'] }, {layer: 'service', patterns: ['service', 'business', 'manager']}, { layer: 'repository', patterns: ['repository', 'repo', 'dal', 'dataaccess'] }, {layer: 'model', patterns: ['model', 'entity', 'domain', 'schema']}, {layer: 'util', patterns: ['util', 'helper', 'common', 'shared']} ] for (const {layer, patterns: layerPatterns} of patterns) { if (layerPatterns.some(p => lowerFile.includes(p))) { return layer } } return null } private isAllowedCrossing( fromLayer: string, toLayer: string, allowedCrossings: Record ): boolean { const allowed = allowedCrossings[fromLayer] return allowed ? allowed.includes(toLayer) : false } private calculateViolationSeverity( fromLayer: string, toLayer: string ): 'critical' | 'major' | 'minor' { if (fromLayer === 'model' || toLayer === 'model') { return fromLayer === 'model' ? 'critical' : 'major' } if ( fromLayer === 'controller' && (toLayer === 'repository' || toLayer === 'model') ) { return 'major' } return 'minor' } inferArchitecturalLayers(): Map { const layers = new Map() const layerPatterns = [ { layer: 'controller', patterns: ['controller', 'handler', 'route', 'endpoint', 'api'] }, {layer: 'service', patterns: ['service', 'business', 'logic', 'manager']}, { layer: 'repository', patterns: ['repository', 'repo', 'dal', 'dataaccess'] }, {layer: 'model', patterns: ['model', 'entity', 'domain', 'schema']}, {layer: 'util', patterns: ['util', 'helper', 'common', 'shared']} ] for (const [file] of this.graph.fileIndex) { const lowerFile = file.toLowerCase() for (const {layer, patterns} of layerPatterns) { if (patterns.some(p => lowerFile.includes(p))) { for (const sym of this.graph.fileIndex.get(file) || []) { layers.set(`${file}:${sym.name}`, layer) } break } } } return layers } detectArchitecturalViolations( changedFiles: string[], architecturalLayers: Map ): ArchitecturalViolation[] { const violations: ArchitecturalViolation[] = [] const layerHierarchy: Record = { controller: 1, handler: 1, service: 2, business: 2, repository: 3, repo: 3, dal: 3, model: 4, entity: 4, domain: 4, util: 5, helper: 5 } const allowedCrossings: Record = { controller: ['service', 'handler'], handler: ['service'], service: ['repository', 'model', 'util'], repository: ['model', 'dal'] } for (const file of changedFiles) { const fileLayer = this.inferFileLayer(file) if (!fileLayer) continue const fileDeps = this.getFileDependencies(file) const fileLayerRank = layerHierarchy[fileLayer] || 0 for (const dep of fileDeps) { const depFile = dep.split(':')[0] const depLayer = this.inferFileLayer(depFile) if (!depLayer) continue const depRank = layerHierarchy[depLayer] || 0 if ( fileLayerRank > depRank && !this.isAllowedCrossing(fileLayer, depLayer, allowedCrossings) ) { violations.push({ from: file, to: depFile, fromLayer: fileLayer, toLayer: depLayer, severity: this.calculateViolationSeverity(fileLayer, depLayer), description: `Layer violation: \`${fileLayer}\` should not depend on \`${depLayer}\`` }) } } } return violations } querySymbol(symbolName: string): Symbol | null { for (const [, sym] of this.graph.symbols) { if (sym.name === symbolName) { return sym } } return null } getFileDependencies(file: string): string[] { const deps: string[] = [] for (const edge of this.graph.edges) { if (edge.from.startsWith(file)) { deps.push(edge.to) } } return deps } getDownstreamImpact(symbolKey: string): string[] { const impacted: string[] = [] const maxDepth = 2 for (const edge of this.graph.edges) { if (edge.to === symbolKey) { impacted.push(edge.from) } } const transitive = new Set(impacted) const queue: Array<{key: string; depth: number}> = impacted.map(k => ({ key: k, depth: 1 })) while (queue.length > 0) { const {key: current, depth} = queue.shift()! if (depth >= maxDepth) continue for (const edge of this.graph.edges) { if (edge.to === current && !transitive.has(edge.from)) { transitive.add(edge.from) queue.push({key: edge.from, depth: depth + 1}) } } } return Array.from(transitive) } getContextForSymbol(symbolKey: string): string { const sym = this.graph.symbols.get(symbolKey) if (!sym) return '' const layer = this.inferFileLayer(sym.file) || 'unknown' const deps = this.getFileDependencies(sym.file) const downstream = this.getDownstreamImpact(symbolKey) return ` Symbol: ${sym.name} Type: ${sym.type} File: ${sym.file}:${sym.line} Layer: ${layer} Dependencies: ${deps.length} Downstream Impact: ${downstream.length} files `.trim() } getRemedyContext( filename: string, lineContext: number, codeSnippet: string, maxSymbols: number = 15 ): string { const symbolsInFile = this.graph.fileIndex.get(filename) || [] const fileSymbols = symbolsInFile.slice(0, maxSymbols) let context = `=== Symbols in ${filename} ===\n` for (const sym of fileSymbols) { context += `[${sym.type}] ${sym.name} (line ${sym.line})\n` } const nearbySymbols = symbolsInFile.filter( s => Math.abs(s.line - lineContext) <= 30 ) if (nearbySymbols.length > 0) { context += `\n=== Symbols near line ${lineContext} ===\n` for (const sym of nearbySymbols.slice(0, 5)) { context += `[${sym.type}] ${sym.name} (line ${sym.line})\n` } } const deps = this.getFileDependencies(filename) if (deps.length > 0) { context += `\n=== Dependencies ===\n` for (const dep of deps.slice(0, 10)) { const depFile = dep.split(':')[0] const depSyms = this.graph.fileIndex.get(depFile) || [] context += `${depFile}: ${depSyms.map(s => s.name).join(', ')}\n` } } return context } generateVisualImpactMap(changedFiles: string[]): string { let mermaid = 'graph TD\n' const edges = new Set() const nodes = new Set() const getBasename = (path: string) => path.split('/').pop() || path for (const file of changedFiles) { const layer = this.inferFileLayer(file) || 'Unknown' const baseName = getBasename(file) nodes.add(`file_${baseName.replace(/[^a-zA-Z0-9]/g, '_')}["${baseName} (${layer})"]`) const deps = this.getFileDependencies(file) for (const dep of deps) { const depFile = dep.split(':')[0] const depLayer = this.inferFileLayer(depFile) || 'Unknown' const depBaseName = getBasename(depFile) const fromId = `file_${baseName.replace(/[^a-zA-Z0-9]/g, '_')}` const toId = `file_${depBaseName.replace(/[^a-zA-Z0-9]/g, '_')}` nodes.add(`${toId}["${depBaseName} (${depLayer})"]`) edges.add(`${fromId} --> ${toId}`) } // Also show who depends ON the changed files (downstream impact) for (const edge of this.graph.edges) { if (edge.to.startsWith(file)) { const fromFile = edge.from.split(':')[0] const fromLayer = this.inferFileLayer(fromFile) || 'Unknown' const fromBaseName = getBasename(fromFile) const fromId = `file_${fromBaseName.replace(/[^a-zA-Z0-9]/g, '_')}` const toId = `file_${baseName.replace(/[^a-zA-Z0-9]/g, '_')}` nodes.add(`${fromId}["${fromBaseName} (${fromLayer})"]`) edges.add(`${fromId} -- impacts --> ${toId}`) } } } if (nodes.size === 0) return '' mermaid += Array.from(nodes).map(n => ` ${n}`).join('\n') + '\n' mermaid += Array.from(edges).map(e => ` ${e}`).join('\n') return `\`\`\`mermaid\n${mermaid}\n\`\`\`` } } export const unifiedContextEngine = new UnifiedContextEngine()