@aaswe/codebase-ai
Version:
AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs
2,060 lines • 92.5 kB
JavaScript
"use strict";
/**
* Module Knowledge Manager
*
* Core orchestrator for managing dual-purpose TTL files that serve both
* Neo4j knowledge graph population and direct LLM context enhancement.
*
* This system bridges the gap between automated code analysis and developer
* business context enhancement, ensuring knowledge files remain accurate
* and valuable for both graph queries and LLM interactions.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModuleKnowledgeManager = void 0;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const crypto = __importStar(require("crypto"));
const events_1 = require("events");
const logger_1 = __importDefault(require("../../../utils/logger"));
const RDFService_1 = require("../rdf-generator/RDFService");
const RDFValidator_1 = require("../rdf-generator/RDFValidator");
/**
* Module Knowledge Manager
*
* Manages the complete lifecycle of .module-knowledge.ttl files including:
* - Initial generation from code analysis
* - Developer business context enhancement
* - Validation and conflict resolution
* - Knowledge graph and MCP context synchronization
* - Developer tooling and preview capabilities
*/
class ModuleKnowledgeManager extends events_1.EventEmitter {
rdfService;
rdfValidator;
options;
knowledgeFiles = new Map();
backupDirectory;
syncQueue = new Map();
neo4jDriver;
mcpContextCache;
mcpClients;
notificationQueue = [];
constructor(options = {}) {
super();
this.options = {
autoValidate: true,
preserveBusinessContext: true,
enableConflictResolution: true,
enableLLMPreview: true,
validationLevel: 'moderate',
backupEnabled: true,
backupRetention: 30,
...options
};
this.rdfService = new RDFService_1.RDFService();
this.rdfValidator = new RDFValidator_1.RDFValidator();
this.backupDirectory = path.join(process.cwd(), '.aaswe', 'backups');
this.mcpContextCache = new Map();
this.mcpClients = new Set();
this.setupEventHandlers();
}
/**
* Initialize Module Knowledge Management System
*/
async initialize() {
try {
logger_1.default.info('Initializing Module Knowledge Management System');
// Ensure backup directory exists
await fs.mkdir(this.backupDirectory, { recursive: true });
// Discover existing knowledge files
await this.discoverExistingKnowledgeFiles();
// Setup file watchers for real-time updates
await this.setupFileWatchers();
// Cleanup old backups
if (this.options.backupEnabled) {
await this.cleanupOldBackups();
}
logger_1.default.info('Module Knowledge Management System initialized successfully');
return { success: true };
}
catch (error) {
logger_1.default.error('Failed to initialize Module Knowledge Management System', { error });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Generate Initial Knowledge Files from Codebase
*/
async generateInitialKnowledgeFiles(rootPath, options = {}) {
const startTime = Date.now();
const results = {
totalFiles: 0,
successful: 0,
failed: 0,
errors: [],
warnings: [],
duration: 0
};
try {
logger_1.default.info('Starting initial knowledge file generation', { rootPath });
// Detect modules in the codebase
const moduleDetection = await this.rdfService.detectModules(rootPath);
results.totalFiles = moduleDetection.modules.length;
// Process each detected module
for (const module of moduleDetection.modules) {
try {
await this.generateKnowledgeFileForModule(module.path, options.overwriteExisting || false, options.preserveBusinessContext !== false);
results.successful++;
}
catch (error) {
results.failed++;
results.errors.push({
filePath: module.path,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
results.duration = Date.now() - startTime;
logger_1.default.info('Initial knowledge file generation completed', {
totalFiles: results.totalFiles,
successful: results.successful,
failed: results.failed,
duration: results.duration
});
return { success: true, data: results };
}
catch (error) {
logger_1.default.error('Failed to generate initial knowledge files', { error, rootPath });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
data: results
};
}
}
/**
* Update Knowledge File from Code Changes
*/
async updateKnowledgeFileFromCode(sourceFilePath, astResult) {
try {
const knowledgeFilePath = this.getKnowledgeFilePath(sourceFilePath);
logger_1.default.info('Updating knowledge file from code changes', {
sourceFile: sourceFilePath,
knowledgeFile: knowledgeFilePath
});
// Create backup if file exists and backup is enabled
if (this.options.backupEnabled && await this.fileExists(knowledgeFilePath)) {
await this.createBackup(knowledgeFilePath, 'auto_update');
}
// Extract existing business context if preservation is enabled
let existingBusinessContext;
if (this.options.preserveBusinessContext && await this.fileExists(knowledgeFilePath)) {
existingBusinessContext = await this.extractBusinessContext(knowledgeFilePath);
}
// Generate new RDF content
const rdfResult = await this.rdfService.generateRDF(astResult, sourceFilePath);
// Merge with existing business context if available
let finalContent = rdfResult.rdfContent;
if (existingBusinessContext) {
finalContent = await this.mergeBusinessContext(finalContent, existingBusinessContext);
}
// Create knowledge file object
const knowledgeFile = {
filePath: knowledgeFilePath,
modulePath: sourceFilePath,
content: finalContent,
checksum: this.calculateChecksum(finalContent),
lastModified: new Date(),
generatedAt: new Date(),
version: '1.0.0',
businessContextEnhanced: !!existingBusinessContext,
validationStatus: {
isValid: true,
syntaxErrors: [],
semanticWarnings: [],
businessContextCompleteness: 0,
lastValidated: new Date()
}
};
// Write to file
await fs.writeFile(knowledgeFilePath, finalContent, 'utf8');
// Validate if auto-validation is enabled
if (this.options.autoValidate) {
knowledgeFile.validationStatus = await this.validateKnowledgeFile(knowledgeFilePath);
}
// Update internal tracking
this.knowledgeFiles.set(knowledgeFilePath, knowledgeFile);
// Emit update event
this.emit('file_updated', {
type: 'file_updated',
payload: {
filePath: knowledgeFilePath,
updateType: 'code_change',
changes: [],
preserveBusinessContext: this.options.preserveBusinessContext,
timestamp: new Date()
}
});
// Queue for synchronization
await this.queueForSync(knowledgeFilePath, 'both');
logger_1.default.info('Knowledge file updated successfully', {
filePath: knowledgeFilePath,
businessContextPreserved: !!existingBusinessContext
});
return { success: true, data: knowledgeFile };
}
catch (error) {
logger_1.default.error('Failed to update knowledge file from code', { error, sourceFilePath });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Validate Knowledge File
*/
async validateKnowledgeFile(filePath) {
try {
logger_1.default.debug('Validating knowledge file', { filePath });
const content = await fs.readFile(filePath, 'utf8');
// RDF syntax and semantic validation
const rdfValidation = await this.rdfValidator.validateContent(content);
// Business context completeness analysis
const businessCompleteness = this.analyzeBusinessContextCompleteness(content);
const validationStatus = {
isValid: rdfValidation.isValid,
syntaxErrors: rdfValidation.errors.map(error => ({
type: error.type,
message: error.message,
line: error.line || 0,
severity: error.severity,
suggestion: 'Check RDF syntax and ontology compliance'
})),
semanticWarnings: rdfValidation.warnings.map(warning => ({
type: 'best_practice',
message: warning.message,
suggestion: warning.suggestion
})),
businessContextCompleteness: businessCompleteness,
lastValidated: new Date()
};
// Update knowledge file tracking
const knowledgeFile = this.knowledgeFiles.get(filePath);
if (knowledgeFile) {
knowledgeFile.validationStatus = validationStatus;
}
// Emit validation event
this.emit('file_validated', {
type: 'file_validated',
payload: validationStatus
});
return validationStatus;
}
catch (error) {
logger_1.default.error('Failed to validate knowledge file', { error, filePath });
return {
isValid: false,
syntaxErrors: [{
type: 'syntax',
message: `Validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
severity: 'error'
}],
semanticWarnings: [],
businessContextCompleteness: 0,
lastValidated: new Date()
};
}
}
/**
* Generate LLM Context Preview
*/
async generateLLMContextPreview(currentFilePath, queryContext) {
try {
logger_1.default.debug('Generating LLM context preview', { currentFilePath, queryContext });
// Find relevant knowledge files
const relevantFiles = await this.findRelevantKnowledgeFiles(currentFilePath, queryContext);
// Aggregate context content
let contextContent = '';
let totalTokens = 0;
let businessContextRatio = 0;
let technicalContextRatio = 0;
for (const filePath of relevantFiles) {
const content = await fs.readFile(filePath, 'utf8');
contextContent += `\n# Knowledge from ${path.relative(process.cwd(), filePath)}\n`;
contextContent += content;
// Analyze content composition
const analysis = this.analyzeContentComposition(content);
businessContextRatio += analysis.businessRatio;
technicalContextRatio += analysis.technicalRatio;
}
// Estimate token count (rough approximation)
totalTokens = Math.ceil(contextContent.length / 4);
const preview = {
filePath: currentFilePath,
relevantFiles,
contextContent,
tokenCount: totalTokens,
relevanceScore: this.calculateRelevanceScore(currentFilePath, relevantFiles),
businessContextRatio: relevantFiles.length > 0 ? businessContextRatio / relevantFiles.length : 0,
technicalContextRatio: relevantFiles.length > 0 ? technicalContextRatio / relevantFiles.length : 0
};
return { success: true, data: preview };
}
catch (error) {
logger_1.default.error('Failed to generate LLM context preview', { error, currentFilePath });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Generate Developer Tooling Information
*/
async generateDeveloperTooling(filePath) {
try {
logger_1.default.debug('Generating developer tooling information', { filePath });
const content = await fs.readFile(filePath, 'utf8');
// Generate enhancement suggestions
const enhancementSuggestions = this.generateEnhancementSuggestions(content);
// Calculate completion status
const completionStatus = this.calculateCompletionStatus(content);
// Perform impact analysis
const impactAnalysis = await this.performImpactAnalysis(filePath);
const tooling = {
enhancementSuggestions,
completionStatus,
impactAnalysis
};
return { success: true, data: tooling };
}
catch (error) {
logger_1.default.error('Failed to generate developer tooling', { error, filePath });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Detect and Resolve Conflicts
*/
async detectAndResolveConflicts(filePath) {
try {
logger_1.default.debug('Detecting conflicts for knowledge file', { filePath });
// Check if file has been modified externally
const knowledgeFile = this.knowledgeFiles.get(filePath);
if (!knowledgeFile) {
return { success: true, data: null };
}
const currentContent = await fs.readFile(filePath, 'utf8');
const currentChecksum = this.calculateChecksum(currentContent);
if (currentChecksum === knowledgeFile.checksum) {
return { success: true, data: null };
}
// Conflict detected - attempt automatic resolution
const conflict = {
conflictId: crypto.randomUUID(),
filePath,
conflictType: 'concurrent_edit',
baseContent: knowledgeFile.content,
localChanges: currentContent,
remoteChanges: this.generateRemoteChangesSimulation(knowledgeFile.content, currentContent),
resolution: 'auto',
timestamp: new Date()
};
// Attempt automatic resolution if enabled
if (this.options.enableConflictResolution) {
const resolved = await this.attemptAutoResolution(conflict);
if (resolved) {
conflict.resolvedContent = resolved;
conflict.resolution = 'auto';
// Update file with resolved content
await fs.writeFile(filePath, resolved, 'utf8');
// Update tracking
knowledgeFile.content = resolved;
knowledgeFile.checksum = this.calculateChecksum(resolved);
knowledgeFile.lastModified = new Date();
}
else {
conflict.resolution = 'manual';
}
}
// Emit conflict event
this.emit('conflict_detected', {
type: 'conflict_detected',
payload: conflict
});
return { success: true, data: conflict };
}
catch (error) {
logger_1.default.error('Failed to detect/resolve conflicts', { error, filePath });
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
// Private helper methods
setupEventHandlers() {
this.on('file_updated', this.handleFileUpdate.bind(this));
this.on('file_validated', this.handleFileValidation.bind(this));
this.on('conflict_detected', this.handleConflictDetection.bind(this));
}
async handleFileUpdate(event) {
const { filePath } = event.payload;
logger_1.default.debug('Handling file update event', { filePath });
// Queue for synchronization
await this.queueForSync(filePath, 'both');
}
async handleFileValidation(event) {
const validation = event.payload;
logger_1.default.debug('Handling file validation event', {
isValid: validation.isValid,
errorCount: validation.syntaxErrors.length
});
}
async handleConflictDetection(event) {
const conflict = event.payload;
logger_1.default.warn('Conflict detected in knowledge file', {
filePath: conflict.filePath,
conflictType: conflict.conflictType,
resolution: conflict.resolution
});
}
async discoverExistingKnowledgeFiles() {
logger_1.default.debug('Discovering existing knowledge files');
try {
const { glob } = await Promise.resolve().then(() => __importStar(require('glob')));
const knowledgeFiles = await glob('**/.module-knowledge.ttl', {
cwd: process.cwd(),
ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**']
});
for (const filePath of knowledgeFiles) {
const absolutePath = path.resolve(process.cwd(), filePath);
try {
const content = await fs.readFile(absolutePath, 'utf8');
const stats = await fs.stat(absolutePath);
const knowledgeFile = {
filePath: absolutePath,
modulePath: this.getSourceFilePathFromKnowledge(absolutePath),
content,
checksum: this.calculateChecksum(content),
lastModified: stats.mtime,
generatedAt: stats.birthtime,
version: this.extractVersionFromContent(content),
businessContextEnhanced: this.hasBusinessContext(content),
validationStatus: {
isValid: true,
syntaxErrors: [],
semanticWarnings: [],
businessContextCompleteness: this.analyzeBusinessContextCompleteness(content),
lastValidated: new Date()
}
};
this.knowledgeFiles.set(absolutePath, knowledgeFile);
logger_1.default.debug('Discovered knowledge file', { filePath: absolutePath });
}
catch (error) {
logger_1.default.warn('Failed to process knowledge file', { filePath: absolutePath, error });
}
}
logger_1.default.info('Knowledge file discovery completed', {
totalFiles: this.knowledgeFiles.size
});
}
catch (error) {
logger_1.default.error('Failed to discover existing knowledge files', { error });
throw error;
}
}
async setupFileWatchers() {
logger_1.default.debug('Setting up file watchers');
// Skip file watchers in test environment
if (process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID) {
logger_1.default.debug('Skipping file watchers in test environment');
logger_1.default.info('File watchers setup completed');
return;
}
try {
const chokidar = await Promise.resolve().then(() => __importStar(require('chokidar')));
// Watch for changes to .module-knowledge.ttl files
const knowledgeWatcher = chokidar.watch('**/.module-knowledge.ttl', {
cwd: process.cwd(),
ignored: ['node_modules/**', '.git/**', 'dist/**', 'build/**'],
persistent: false, // Don't keep process alive in tests
ignoreInitial: true
});
knowledgeWatcher.on('error', (error) => {
logger_1.default.warn('Knowledge file watcher error', { error });
});
knowledgeWatcher.on('change', async (filePath) => {
const absolutePath = path.resolve(process.cwd(), filePath);
logger_1.default.debug('Knowledge file changed', { filePath: absolutePath });
try {
// Detect and resolve conflicts
await this.detectAndResolveConflicts(absolutePath);
// Re-validate the file
if (this.options.autoValidate) {
await this.validateKnowledgeFile(absolutePath);
}
// Queue for synchronization
await this.queueForSync(absolutePath, 'both');
}
catch (error) {
logger_1.default.error('Failed to handle knowledge file change', { filePath: absolutePath, error });
}
});
knowledgeWatcher.on('unlink', (filePath) => {
const absolutePath = path.resolve(process.cwd(), filePath);
this.knowledgeFiles.delete(absolutePath);
this.syncQueue.delete(absolutePath);
logger_1.default.debug('Knowledge file removed', { filePath: absolutePath });
});
// Watch for changes to source code files
const sourceWatcher = chokidar.watch('**/*.{ts,js,py,java,go,rs,cpp,hpp}', {
cwd: process.cwd(),
ignored: ['node_modules/**', '.git/**', 'dist/**', 'build/**', '**/*.test.*', '**/*.spec.*'],
persistent: false, // Don't keep process alive in tests
ignoreInitial: true
});
sourceWatcher.on('error', (error) => {
logger_1.default.warn('Source file watcher error', { error });
});
sourceWatcher.on('change', async (filePath) => {
const absolutePath = path.resolve(process.cwd(), filePath);
const knowledgeFilePath = this.getKnowledgeFilePath(absolutePath);
// Only process if knowledge file exists
if (await this.fileExists(knowledgeFilePath)) {
logger_1.default.debug('Source file changed, updating knowledge file', {
sourceFile: absolutePath,
knowledgeFile: knowledgeFilePath
});
try {
// Perform AST analysis and update knowledge file with changes
await this.analyzeAndUpdateFromSourceChange(absolutePath, knowledgeFilePath);
}
catch (error) {
logger_1.default.error('Failed to handle source file change', { filePath: absolutePath, error });
}
}
});
logger_1.default.info('File watchers setup completed');
}
catch (error) {
logger_1.default.error('Failed to setup file watchers', { error });
// Don't throw in production - file watchers are not critical
if (process.env.NODE_ENV !== 'production') {
throw error;
}
}
}
async cleanupOldBackups() {
logger_1.default.debug('Cleaning up old backups');
try {
const backupFiles = await fs.readdir(this.backupDirectory);
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.options.backupRetention);
let cleanedCount = 0;
for (const fileName of backupFiles) {
if (fileName.endsWith('.backup')) {
const filePath = path.join(this.backupDirectory, fileName);
try {
const stats = await fs.stat(filePath);
if (stats.mtime < cutoffDate) {
await fs.unlink(filePath);
cleanedCount++;
logger_1.default.debug('Removed old backup', { filePath });
}
}
catch (error) {
logger_1.default.warn('Failed to process backup file', { filePath, error });
}
}
}
logger_1.default.info('Backup cleanup completed', {
cleanedCount,
retentionDays: this.options.backupRetention
});
}
catch (error) {
logger_1.default.error('Failed to cleanup old backups', { error });
// Don't throw - this is not critical
}
}
async generateKnowledgeFileForModule(modulePath, overwriteExisting, preserveBusinessContext) {
logger_1.default.debug('Generating knowledge file for module', { modulePath, overwriteExisting, preserveBusinessContext });
const knowledgeFilePath = this.getKnowledgeFilePath(modulePath);
// Check if file exists and handle accordingly
if (!overwriteExisting && await this.fileExists(knowledgeFilePath)) {
logger_1.default.debug('Knowledge file already exists, skipping', { knowledgeFilePath });
return;
}
try {
// Extract existing business context if preservation is enabled
let existingBusinessContext;
if (preserveBusinessContext && await this.fileExists(knowledgeFilePath)) {
existingBusinessContext = await this.extractBusinessContext(knowledgeFilePath);
}
// Generate basic RDF content from file analysis
const basicContent = await this.generateBasicRDFContent(modulePath);
// Merge with existing business context if available
let finalContent = basicContent;
if (existingBusinessContext) {
finalContent = await this.mergeBusinessContext(basicContent, existingBusinessContext);
}
// Create knowledge file object
const knowledgeFile = {
filePath: knowledgeFilePath,
modulePath,
content: finalContent,
checksum: this.calculateChecksum(finalContent),
lastModified: new Date(),
generatedAt: new Date(),
version: '1.0.0',
businessContextEnhanced: !!existingBusinessContext,
validationStatus: {
isValid: true,
syntaxErrors: [],
semanticWarnings: [],
businessContextCompleteness: this.analyzeBusinessContextCompleteness(finalContent),
lastValidated: new Date()
}
};
// Write to file
await fs.writeFile(knowledgeFilePath, finalContent, 'utf8');
// Validate if auto-validation is enabled
if (this.options.autoValidate) {
knowledgeFile.validationStatus = await this.validateKnowledgeFile(knowledgeFilePath);
}
// Update internal tracking
this.knowledgeFiles.set(knowledgeFilePath, knowledgeFile);
// Queue for synchronization
await this.queueForSync(knowledgeFilePath, 'both');
logger_1.default.debug('Knowledge file generated successfully', { knowledgeFilePath });
}
catch (error) {
logger_1.default.error('Failed to generate knowledge file for module', { modulePath, error });
throw error;
}
}
getKnowledgeFilePath(sourceFilePath) {
const dir = path.dirname(sourceFilePath);
return path.join(dir, '.module-knowledge.ttl');
}
async fileExists(filePath) {
try {
await fs.access(filePath);
return true;
}
catch {
return false;
}
}
calculateChecksum(content) {
return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
}
async createBackup(filePath, reason) {
const content = await fs.readFile(filePath, 'utf8');
const timestamp = new Date();
const backupFileName = `${path.basename(filePath)}.${timestamp.getTime()}.backup`;
const backupPath = path.join(this.backupDirectory, backupFileName);
await fs.writeFile(backupPath, content, 'utf8');
const backup = {
filePath,
backupPath,
timestamp,
reason: reason,
checksum: this.calculateChecksum(content)
};
this.emit('backup_created', {
type: 'backup_created',
payload: backup
});
return backup;
}
async extractBusinessContext(filePath) {
try {
const content = await fs.readFile(filePath, 'utf8');
const businessContext = {};
// Extract business domain information
const domainMatch = content.match(/aide:businessDomain\s+"([^"]+)"/);
if (domainMatch) {
businessContext.domain = domainMatch[1];
}
// Extract business rules
const rulesMatches = content.match(/aide:businessRule\s+"([^"]+)"/g);
if (rulesMatches) {
businessContext.businessRules = rulesMatches.map(match => match.replace(/aide:businessRule\s+"([^"]+)"/, '$1'));
}
// Extract use cases
const useCaseMatches = content.match(/aide:useCase\s+"([^"]+)"/g);
if (useCaseMatches) {
businessContext.useCases = useCaseMatches.map(match => match.replace(/aide:useCase\s+"([^"]+)"/, '$1'));
}
// Extract constraints
const constraintMatches = content.match(/aide:constraint\s+"([^"]+)"/g);
if (constraintMatches) {
businessContext.constraints = constraintMatches.map(match => match.replace(/aide:constraint\s+"([^"]+)"/, '$1'));
}
logger_1.default.debug('Extracted business context', { filePath, contextKeys: Object.keys(businessContext) });
return businessContext;
}
catch (error) {
logger_1.default.error('Failed to extract business context', { filePath, error });
return {};
}
}
async mergeBusinessContext(newContent, businessContext) {
try {
let mergedContent = newContent;
// Find the business context section
const businessSectionStart = mergedContent.indexOf('# Business Context Section');
if (businessSectionStart === -1) {
logger_1.default.warn('Business context section not found in content');
return newContent;
}
const technicalSectionStart = mergedContent.indexOf('# Technical Analysis Section');
if (technicalSectionStart === -1) {
logger_1.default.warn('Technical analysis section not found in content');
return newContent;
}
// Extract the parts
const beforeBusiness = mergedContent.substring(0, businessSectionStart);
const afterTechnical = mergedContent.substring(technicalSectionStart);
// Build business context content
let businessContent = '# Business Context Section\n';
businessContent += '# Add business domain information, rules, and use cases below\n';
businessContent += '# This section is preserved during automatic updates\n\n';
if (businessContext.domain) {
businessContent += `aide:businessDomain "${businessContext.domain}" .\n`;
}
if (businessContext.businessRules && businessContext.businessRules.length > 0) {
businessContext.businessRules.forEach(rule => {
businessContent += `aide:businessRule "${rule}" .\n`;
});
}
if (businessContext.useCases && businessContext.useCases.length > 0) {
businessContext.useCases.forEach(useCase => {
businessContent += `aide:useCase "${useCase}" .\n`;
});
}
if (businessContext.constraints && businessContext.constraints.length > 0) {
businessContext.constraints.forEach(constraint => {
businessContent += `aide:constraint "${constraint}" .\n`;
});
}
businessContent += '\n';
// Merge everything together
mergedContent = beforeBusiness + businessContent + afterTechnical;
logger_1.default.debug('Successfully merged business context');
return mergedContent;
}
catch (error) {
logger_1.default.error('Failed to merge business context', { error });
return newContent;
}
}
analyzeBusinessContextCompleteness(content) {
const businessMarkers = [
'aide:businessDomain',
'aide:businessRule',
'aide:useCase',
'aide:constraint',
'aide:stakeholder',
'aide:businessProcess'
];
let foundMarkers = 0;
businessMarkers.forEach(marker => {
if (content.includes(marker)) {
foundMarkers++;
}
});
// Calculate completeness as percentage
const completeness = foundMarkers / businessMarkers.length;
logger_1.default.debug('Analyzed business context completeness', {
foundMarkers,
totalMarkers: businessMarkers.length,
completeness
});
return completeness;
}
async findRelevantKnowledgeFiles(currentFilePath, queryContext) {
try {
const relevantFiles = [];
const currentDir = path.dirname(currentFilePath);
// Get all knowledge files
const allKnowledgeFiles = Array.from(this.knowledgeFiles.keys());
// Score files based on relevance
const scoredFiles = allKnowledgeFiles.map(filePath => {
let score = 0;
// Same directory gets higher score
if (path.dirname(filePath) === currentDir) {
score += 10;
}
// Parent/child directory relationship
const relativePath = path.relative(currentDir, path.dirname(filePath));
const depth = relativePath.split(path.sep).length;
if (depth <= 2) {
score += Math.max(0, 5 - depth);
}
// Query context matching
if (queryContext) {
const knowledgeFile = this.knowledgeFiles.get(filePath);
if (knowledgeFile && knowledgeFile.content.toLowerCase().includes(queryContext.toLowerCase())) {
score += 15;
}
}
// Business context enhancement bonus
const knowledgeFile = this.knowledgeFiles.get(filePath);
if (knowledgeFile && knowledgeFile.businessContextEnhanced) {
score += 5;
}
return { filePath, score };
});
// Sort by score and take top 5
const topFiles = scoredFiles
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map(item => item.filePath);
relevantFiles.push(...topFiles);
logger_1.default.debug('Found relevant knowledge files', {
currentFilePath,
queryContext,
relevantCount: relevantFiles.length
});
return relevantFiles;
}
catch (error) {
logger_1.default.error('Failed to find relevant knowledge files', { currentFilePath, error });
return [];
}
}
analyzeContentComposition(content) {
const businessKeywords = [
'businessDomain', 'businessRule', 'useCase', 'constraint',
'stakeholder', 'businessProcess', 'requirement', 'workflow'
];
const technicalKeywords = [
'function', 'class', 'method', 'property', 'parameter',
'return', 'import', 'export', 'dependency', 'complexity'
];
let businessCount = 0;
let technicalCount = 0;
businessKeywords.forEach(keyword => {
const matches = content.match(new RegExp(keyword, 'gi'));
if (matches) {
businessCount += matches.length;
}
});
technicalKeywords.forEach(keyword => {
const matches = content.match(new RegExp(keyword, 'gi'));
if (matches) {
technicalCount += matches.length;
}
});
const total = businessCount + technicalCount;
if (total === 0) {
return { businessRatio: 0.5, technicalRatio: 0.5 };
}
const businessRatio = businessCount / total;
const technicalRatio = technicalCount / total;
return { businessRatio, technicalRatio };
}
calculateRelevanceScore(currentFilePath, relevantFiles) {
if (relevantFiles.length === 0) {
return 0;
}
const currentDir = path.dirname(currentFilePath);
let totalScore = 0;
relevantFiles.forEach(filePath => {
let fileScore = 0.5; // Base score
// Same directory bonus
if (path.dirname(filePath) === currentDir) {
fileScore += 0.3;
}
// Business context bonus
const knowledgeFile = this.knowledgeFiles.get(filePath);
if (knowledgeFile && knowledgeFile.businessContextEnhanced) {
fileScore += 0.2;
}
// Validation status bonus
if (knowledgeFile && knowledgeFile.validationStatus.isValid) {
fileScore += 0.1;
}
totalScore += Math.min(fileScore, 1.0); // Cap at 1.0
});
return Math.min(totalScore / relevantFiles.length, 1.0);
}
generateEnhancementSuggestions(content) {
const suggestions = [];
// Check for missing business domain
if (!content.includes('aide:businessDomain')) {
suggestions.push({
type: 'business_context',
priority: 'high',
suggestion: 'Add business domain information to provide context about the module\'s purpose',
example: 'aide:businessDomain "E-commerce" .'
});
}
// Check for missing business rules
if (!content.includes('aide:businessRule')) {
suggestions.push({
type: 'business_context',
priority: 'medium',
suggestion: 'Add business rules that govern this module\'s behavior',
example: 'aide:businessRule "Orders must be validated before processing" .'
});
}
// Check for missing use cases
if (!content.includes('aide:useCase')) {
suggestions.push({
type: 'business_context',
priority: 'medium',
suggestion: 'Document the primary use cases for this module',
example: 'aide:useCase "Process customer orders" .'
});
}
// Check for missing constraints
if (!content.includes('aide:constraint')) {
suggestions.push({
type: 'business_context',
priority: 'low',
suggestion: 'Add any business constraints or limitations',
example: 'aide:constraint "Maximum 100 items per order" .'
});
}
// Check for technical documentation completeness
const functionCount = (content.match(/aide:Function/g) || []).length;
const classCount = (content.match(/aide:Class/g) || []).length;
if ((functionCount > 0 || classCount > 0) && !content.includes('rdfs:comment')) {
suggestions.push({
type: 'technical_documentation',
priority: 'medium',
suggestion: 'Add comments/descriptions to functions and classes',
example: 'rdfs:comment "Processes user authentication" .'
});
}
return suggestions;
}
calculateCompletionStatus(content) {
const status = {
businessDomain: content.includes('aide:businessDomain'),
businessRules: content.includes('aide:businessRule'),
useCases: content.includes('aide:useCase'),
constraints: content.includes('aide:constraint'),
methodDocumentation: 0,
classDocumentation: 0,
overallCompleteness: 0
};
// Calculate method documentation completeness
const functionMatches = content.match(/aide:Function/g);
const functionCommentMatches = content.match(/aide:Function[^}]*rdfs:comment/g);
if (functionMatches) {
status.methodDocumentation = functionCommentMatches ?
functionCommentMatches.length / functionMatches.length : 0;
}
// Calculate class documentation completeness
const classMatches = content.match(/aide:Class/g);
const classCommentMatches = content.match(/aide:Class[^}]*rdfs:comment/g);
if (classMatches) {
status.classDocumentation = classCommentMatches ?
classCommentMatches.length / classMatches.length : 0;
}
// Calculate overall completeness
const businessItems = [
status.businessDomain,
status.businessRules,
status.useCases,
status.constraints
];
const businessCompleteness = businessItems.filter(Boolean).length / businessItems.length;
const technicalCompleteness = (status.methodDocumentation + status.classDocumentation) / 2;
status.overallCompleteness = (businessCompleteness + technicalCompleteness) / 2;
return status;
}
async performImpactAnalysis(filePath) {
try {
const knowledgeFile = this.knowledgeFiles.get(filePath);
if (!knowledgeFile) {
return {
llmContextImpact: 'unknown',
graphQueryImpact: 'unknown',
affectedFiles: [],
dependentModules: [],
estimatedBenefit: 'Unable to analyze - file not found'
};
}
const content = knowledgeFile.content;
const completionStatus = this.calculateCompletionStatus(content);
// Determine LLM context impact
let llmContextImpact = 'low';
if (completionStatus.overallCompleteness > 0.7) {
llmContextImpact = 'high';
}
else if (completionStatus.overallCompleteness > 0.4) {
llmContextImpact = 'medium';
}
// Determine graph query impact
let graphQueryImpact = 'low';
const hasStructuredData = content.includes('aide:Function') || content.includes('aide:Class');
const hasRelationships = content.includes('aide:dependsOn') || content.includes('aide:uses');
if (hasStructuredData && hasRelationships) {
graphQueryImpact = 'high';
}
else if (hasStructuredData || hasRelationships) {
graphQueryImpact = 'medium';
}
// Find affected files (files in same directory or related directories)
const currentDir = path.dirname(filePath);
const affectedFiles = Array.from(this.knowledgeFiles.keys())
.filter(kfPath => {
const kfDir = path.dirname(kfPath);
return kfPath !== filePath && (kfDir === currentDir ||
kfDir.startsWith(currentDir) ||
currentDir.startsWith(kfDir));
})
.slice(0, 5); // Limit to 5 files
// Estimate dependent modules
const dependentModules = content.match(/aide:dependsOn\s+"([^"]+)"/g) || [];
// Generate benefit estimation
let estimatedBenefit = 'Basic code structure understanding';
if (knowledgeFile.businessContextEnhanced) {
estimatedBenefit = 'Enhanced business context and improved LLM reasoning';
}
if (completionStatus.overallCompleteness > 0.8) {
estimatedBenefit = 'Comprehensive understanding enabling advanced AI assistance';
}
return {
llmContextImpact,
graphQueryImpact,
affectedFiles,
dependentModules: dependentModules.map(dep => dep.replace(/aide:dependsOn\s+"([^"]+)"/, '$1')),
estimatedBenefit,
completenessScore: completionStatus.overallCompleteness,
hasBusinessContext: knowledgeFile.businessContextEnhanced
};
}
catch (error) {
logger_1.default.error('Failed to perform impact analysis', { filePath, error });
return {
llmContextImpact: 'unknown',
graphQueryImpact: 'unknown',
affectedFiles: [],
dependentModules: [],
estimatedBenefit: 'Analysis failed due to error'
};
}
}
async attemptAutoResolution(conflict) {
try {
logger_1.default.debug('Attempting automatic conflict resolution', {
conflictId: conflict.conflictId,
conflictType: conflict.conflictType
});
// For concurrent edits, try to merge non-conflicting sections
if (conflict.conflictType === 'concurrent_edit') {
const baseLines = conflict.baseContent.split('\n');
const localLines = conflict.localChanges.split('\n');
// Concrete merge strategy: preserve business context from local changes
// and technical content from base using section-based merging
const businessSectionStart = localLines.findIndex(line => line.includes('# Business Context Section'));
const technicalSectionStart = localLines.findIndex(line => line.includes('# Technical Analysis Section'));
if (businessSectionStart !== -1 && technicalSectionStart !== -1) {
// Extract business section from local changes
const businessSection = localLines.slice(businessSectionStart, technicalSectionStart);
// Extract technical section from base content
const baseTechnicalStart = baseLines.findIndex(line => line.includes('# Technical Analysis Section'));
if (baseTechnicalStart !== -1) {
const baseTechnicalSection = baseLines.slice(baseTechnicalStart);
// Merge: header + business from local + technical from base
const headerSection = localLines.slice(0, businessSectionStart);
const mergedContent = [
...headerSection,
...businessSection,
...baseTechnicalSection
].join('\n');
logger_1.default.debug('Successfully auto-resolved conflict using section merge');
return mergedContent;
}
}
}
// If automatic resolution fails, return null for manual resolution
logger_1.default.debug('Automatic resolution not possible, requiring manual intervention');
return null;
}
catch (error) {
logger_1.default.error('Failed to attempt automatic conflict resolution', {
conflictId: conflict.conflictId,
error
});
return null;
}
}
generateRemoteChangesSimulation(baseContent, localContent) {
// Generate simulated remote changes for conflict resolution testing
// This simulates what would happen in a real concurrent editing scenario
const baseLines = baseContent.split('\n');
const localLines = localContent.split('\n');
// Find the technical section in base content
const baseTechnicalStart = baseLines.findIndex(line => line.includes('# Technical Analysis Section'));
if (baseTechnicalStart === -1) {
return baseContent; // No technical section found, return base as-is
}
// Simulate remote changes by modifying technical content
const remoteLines = [...baseLines];
// Add simulated remote technical updates based on differences with local
const timestamp = new Date().toISOString();
const localChangeCount = Math.abs(localLines.length - baseLines.length);
const remoteUpdates = [
`# Remote update at ${timestamp}`,
`aide:remoteUpdate "Simulated concurrent modification with ${localChangeCount} local changes detected" .`,
`aide:conflictMarker "Remote changes conflict with local modifications" .`,
`aide:remoteTimestamp "${timestamp}"^^xsd:dateTime .`
];
// Insert remote updates after technical section header
remoteLines.splice(baseTechnicalStart + 2, 0, ...remoteUpdates);
return remoteLines.join('\n');
}
async queueForSync(filePath, syncType) {
const syncStatus = {
filePath,
lastSync: new Date(),
syncType,
status: 'pending',
retryCount: 0
};
this.syncQueue.set(filePath, syncStatus);
// Process sync queue asynchronously
setImmediate(() => this.processSyncQueue());
logger_1.default.debug('Queued file for synchronization', { filePath, syncType });
}
getSourceFilePathFromKnowledge(knowledgeFilePath) {
const dir = path.dirname(knowledgeFilePath);
const files = ['index.ts', 'index.js', 'main.ts', 'main.js'];
// Try to find the main source file in the same directory
for (const file of files) {
const sourcePath = path.join(dir, file);
try {
require('fs').accessSync(sourcePath);
return sourcePath;
}
catch {
// File doesn't exist, continue
}
}
// If no main file found, return the directory path
return dir;
}
extractVersionFromContent(content) {
// Look for version information in TTL content
const versionMatch = content.match(/@version\s+"([^"]+)"/);
return versionMatch ? versionMatch[1] : '1.0.0';
}
hasBusinessContext(content) {
// Check for business context markers in TTL content
const businessMarkers = [
'businessDomain',
'businessRule',
'useCase',
'businessContext',
'domainConcept'
];
return businessMarkers.some(marker => content.includes(marker));
}
async generateBasicRDFContent(modulePath) {
try {
// Get file stats and basic information
const stats = await fs.stat(modulePath);
const fileName = path.basename(modulePath);
const fileExtension = path.extname(modulePath);
const baseName = path.basename(modulePath, fileExtension);
// Generate basic TTL content
const content = `@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix aide: <http://aide.dev/ontology#> .
@version "1.0.0" .
# Module Information
aide:${baseName} a aide:Module ;
aide:fileName "${fileName}" ;
aide:filePath "${modulePath}" ;
aide:fileExtension "${fileExtension}" ;
aide:lastModified "${stats.mtime.toISOString()}"^^xsd:dateTime ;
aide:fileSize ${stats.size} ;
rdfs:label "${baseName} Module" ;
rdfs:comment "Auto-generated knowledge file for ${fileName}" .
# Business Context Section
# Add business domain information, rules, and use cases below
# This section is preserved during automatic updates
# Technical Analysis Section
# This section is automatically updated from code analysis
aide:${baseName} aide:analysisTimestamp "${new Date().toISOString()}"^^xsd:dateTime .
`;
return content;
}
catch (error) {
logger_1.default.error('Failed to generate basic RDF content', { modulePath, error });
throw error;
}
}
async processSyncQueue() {
const pendingItems = Array.from(this.syncQueue.entries())
.filter(([, status]) => status.status === 'pending')
.slice(0, 5); // Process up to 5 items at a time
if (pendingItems.length === 0) {
return;
}
logger_1.default.debug('Processing sync queue', { itemCount: pendingItems.length });
for (const [filePath, syncStatus] of pendingItems) {
try {
syncStatus.status = 'pending';
// Read the knowledge file content
const content = await fs.readFile(filePath, 'utf8');
// Simulate synchronization based on sync type
if (syncStatus.syncType === 'knowledge_graph' || syncStatus.syncType === 'both') {
await this.syncToKnowledgeGraph(filePath, content);
}
if (syncStatus.syncType === 'mcp_context' || syncStatus.syncType === 'both') {
await this.syncToMCPContext(filePath, content);
}
// Mark as completed
syncStatus.status = 'success';
syncStatus.lastSync = new Date();
logger_1.default.debug('Sync completed successfully', { filePath, syncType: syncStatus.syncType });
// Remove from queue after successful sync
this.syncQueue.delete(filePath);
}
catch (error) {
syncStatus.status = 'failed';
syncStatus.retryCount++;
logger_1.default.error('Sync failed', {
filePath,
error,
retryCount: syncStatus.retryCount
});
// Remove from queue if max retries exceeded
if (syncStatus.retryCount >= 3) {
this.syncQueue.delete(filePath);
logger_1.default.warn('Max retries exceeded, removing from sync queue', { filePath });
}
}
}
}
async syncToKnowledgeGraph(filePath, content) {
logger_1.default.debug('Syncing to knowledge graph', { filePath, contentLength: content.length });
try {
// Check if Neo4j is available before attempting sync
const neo4jAvailable = await this.checkNeo4jAvailability();
if (!neo4jAvailable) {
logger_1.default.debug('Neo4j not available, skipping knowledge graph sync', { filePath });
return;
}
// Parse TTL content to extract triples
const triples = await this.parseTTLContent(content);
const moduleId = this.extractModuleId(content, filePath);
// Connect to Neo4j and create/update nodes and relationships
const neo4j = await this.getNeo4jDriver();
const session = neo4j.session();
try {
await session.writeTransaction(async (tx) => {
// Create or update module node
await tx.run(`MERGE (m:Module {id: $moduleId, filePath: $filePath})
SET m.lastUpdated = datetime(),
m.content = $content,
m.checksum = $checksum`, {
moduleId,
filePath,
content,
checksum: this.calculateChecksum(content)
});
// Process each triple to create graph relationships
for (const triple of triples) {
await this.createGraphTriple(tx, triple, moduleId);
}
});
logger_1.default.debug('Successfully synced to knowledge graph', {
filePath,
moduleId,
tripleCount: triples.length
});
}
finally {
await session.close();
}
}
catch (error) {
logger_1.default.debug('Knowledge graph sync failed, continuing without Neo4j', { filePath, error: error instanceof Error ? error.message : 'Unknown error' });
// Don't throw error - knowledge graph sync is optional
}
}
async syncToMCPContext(filePath, content) {
logger_1.default.debug('Syncing to MCP context', { filePath, contentLength: content.length });
try {
// Format content for MCP consumption
const mcpContext = await this.formatForMCP(content, filePath);
const moduleId = this.extractModuleId(content, filePath);
// Update context cache
await this.updateContextCache(moduleId, mcpContext);
// Notify MCP clients of updates
await this.notifyMCPClients(moduleId, mcpContext);
logger_1.default.debug('Successfully synced to MCP context', {
filePath,
moduleId,
contextSize: mcpContext.content.length
});
}
catch (error) {
logger_1.default.error('Failed to sync to MCP context', { filePath, error });
throw error;
}
}
async parseTTLContent(content) {
const triples = [];
// Parse TTL content line by line to extract RDF triples
const lines = content.split('\n');
let currentSubject = '';
for (const line of lines) {
const trimmedLine = line.trim();
// Skip comments and empty lines
if (trimmedLine.startsWith('#') || trimmedLine.startsWith('@') || !trimmedLine) {
continue;
}
// Parse triple patterns
const tripleMatch = trimmedLine.match(/^(\S+)\s+(\S+)\s+(.+?)\s*[;.]?\s*$/);
if (tripleMatch) {
const [, subject, predicate, object] = tripleMatch;
// Update current subject if it's a new one
if (!subject.startsWith('aide:') && !subject.startsWith('rdfs:')) {
currentSubject = subject;
}
triples.push({
subject: subject.startsWith('aide:') ? subject : currentSubject || subject,
predicate,
object: object.replace(/[";]$/, '').trim()
});
}
}
return triples;
}
extractModuleId(content, filePath) {
// Try to extract module ID from TTL content
const moduleMatch = content.match(/aide:(\w+)\s+a\s+aide:Module/);
if (moduleMatch) {
return moduleMatch[1];
}
// Fallback to file-based ID
const fileName = path.basename(filePath, path.extname(filePath));
return fileName.replace(/[^a-zA-Z0-9]/g, '_');
}
/**
* Check if Neo4j is available for knowledge graph operations
*/
async checkNeo4jAvailability() {
try {
// Check if Neo4j environment variables are set
const neo4jUri = process.env.NEO4J_URI;
const neo4jUsername = process.env.NEO4J_USERNAME || process.env.NEO4J_USER;
const neo4jPassword = process.env.NEO4J_PASSWORD;
if (!neo4jUri || !neo4jUsername || !neo4jPassword) {
logger_1.default.debug('Neo4j credentials not configured');
return false;
}
// Try to connect to Neo4j
const { Neo4jDatabaseService } = await Promise.resolve().then(() => __importStar(require('../../layer2/neo4j-database/Neo4jDatabaseService')));
const neo4jService = new Neo4jDatabaseService();
const config = {
uri: neo4jUri,
username: neo4jUsername,
password: neo4jPassword,
database: 'neo4j'
};
await neo4jService.connect(config);
const isHealthy = await neo4jService.testConnection();
await neo4jService.disconnect();
return isHealthy;
}
catch (error) {
logger_1.default.debug('Neo4j availability check failed', { error: error instanceof Error ? error.message : 'Unknown error' });
return false;
}
}
async getNeo4jDriver() {
// Initialize Neo4j driver if not already done
if (!this.neo4jDriver) {
const neo4j = await Promise.resolve().then(() => __importStar(require('neo4j-driver')));
this.neo4jDriver = neo4j.driver(process.env.NEO4J_URI || 'bolt://localhost:7687', neo4j.auth.basic(process.env.NEO4J_USERNAME || process.env.NEO4J_USER || 'neo4j', process.env.NEO4J_PASSWORD || 'password'));
}
return this.neo4jDriver;
}
async createGraphTriple(tx, triple, moduleId) {
const { predicate, object } = triple;
// Handle different types of relationships
if (predicate.includes('dependsOn')) {
await tx.run(`MATCH (m:Module {id: $moduleId})
MERGE (dep:Module {id: $depId})
MERGE (m)-[:DEPENDS_ON]->(dep)`, { moduleId, depId: object.replace(/['"]/g, '') });
}
else if (predicate.includes('hasFunction')) {
await tx.run(`MATCH (m:Module {id: $moduleId})
MERGE (f:Function {name: $functionName, module: $moduleId})
MERGE (m)-[:HAS_FUNCTION]->(f)`, { moduleId, functionName: object.replace(/['"]/g, '') });
}
else if (predicate.includes('hasClass')) {
await tx.run(`MATCH (m:Module {id: $moduleId})
MERGE (c:Class {name: $className, module: $moduleId})
MERGE (m)-[:HAS_CLASS]->(c)`, { moduleId, className: object.replace(/['"]/g, '') });
}
else if (predicate.includes('businessDomain')) {
await tx.run(`MATCH (m:Module {id: $moduleId})
MERGE (d:Domain {name: $domainName})
MERGE (m)-[:BELONGS_TO_DOMAIN]->(d)`, { moduleId, domainName: object.replace(/['"]/g, '') });
}
else {
// Generic property assignment
const propertyName = predicate.replace(/aide:|rdfs:/, '').replace(/[^a-zA-Z0-9]/g, '_');
await tx.run(`MATCH (m:Module {id: $moduleId})
SET m.${propertyName} = $value`, { moduleId, value: object.replace(/['"]/g, '') });
}
}
async formatForMCP(content, filePath) {
const moduleId = this.extractModuleId(content, filePath);
// Extract business context for enhanced LLM understanding
const businessContext = await this.extractBusinessContext(filePath);
// Format content for MCP consumption with enhanced context
const mcpContent = {
moduleId,
filePath,
timestamp: new Date().toISOString(),
businessContext,
technicalContent: this.extractTechnicalContent(content),
relationships: this.extractRelationships(content),
summary: this.generateContentSummary(content, businessContext)
};
return {
content: JSON.stringify(mcpContent, null, 2),
metadata: {
moduleId,
lastUpdated: new Date(),
contentType: 'module-knowledge',
version: '1.0.0'
}
};
}
extractTechnicalContent(content) {
const technical = {
functions: [],
classes: [],
dependencies: [],
exports: []
};
// Extract functions
const functionMatches = content.match(/aide:Function[^}]*/g) || [];
technical.functions = functionMatches.map(match => {
const nameMatch = match.match(/aide:functionName\s+"([^"]+)"/);
const commentMatch = match.match(/rdfs:comment\s+"([^"]+)"/);
return {
name: nameMatch ? nameMatch[1] : 'unknown',
description: commentMatch ? commentMatch[1] : ''
};
});
// Extract classes
const classMatches = content.match(/aide:Class[^}]*/g) || [];
technical.classes = classMatches.map(match => {
const nameMatch = match.match(/aide:className\s+"([^"]+)"/);
const commentMatch = match.match(/rdfs:comment\s+"([^"]+)"/);
return {
name: nameMatch ? nameMatch[1] : 'unknown',
description: commentMatch ? commentMatch[1] : ''
};
});
// Extract dependencies
const depMatches = content.match(/aide:dependsOn\s+"([^"]+)"/g) || [];
technical.dependencies = depMatches.map(match => match.replace(/aide:dependsOn\s+"([^"]+)"/, '$1'));
return technical;
}
extractRelationships(content) {
const relationships = [];
// Extract various relationship types
const relationshipPatterns = [
{ pattern: /aide:dependsOn\s+"([^"]+)"/g, type: 'depends_on' },
{ pattern: /aide:uses\s+"([^"]+)"/g, type: 'uses' },
{ pattern: /aide:implements\s+"([^"]+)"/g, type: 'implements' },
{ pattern: /aide:extends\s+"([^"]+)"/g, type: 'extends' }
];
relationshipPatterns.forEach(({ pattern, type }) => {
const matches = content.match(pattern) || [];
matches.forEach(match => {
const target = match.replace(pattern, '$1');
relationships.push({ type, target });
});
});
return relationships;
}
generateContentSummary(content, businessContext) {
const moduleMatch = content.match(/aide:(\w+)\s+a\s+aide:Module/);
const moduleName = moduleMatch ? moduleMatch[1] : 'Unknown Module';
let summary = `Module: ${moduleName}\n`;
if (businessContext.domain) {
summary += `Business Domain: ${businessContext.domain}\n`;
}
if (businessContext.businessRules && businessContext.businessRules.length > 0) {
summary += `Business Rules: ${businessContext.businessRules.join(', ')}\n`;
}
if (businessContext.useCases && businessContext.useCases.length > 0) {
summary += `Use Cases: ${businessContext.useCases.join(', ')}\n`;
}
const technical = this.extractTechnicalContent(content);
if (technical.functions.length > 0) {
summary += `Functions: ${technical.functions.map((f) => f.name).join(', ')}\n`;
}
if (technical.classes.length > 0) {
summary += `Classes: ${technical.classes.map((c) => c.name).join(', ')}\n`;
}
return summary;
}
async updateContextCache(moduleId, mcpContext) {
// Update cache with new context
this.mcpContextCache.set(moduleId, {
...mcpContext,
lastUpdated: new Date()
});
// Persist cache to disk for durability
const cacheDir = path.join(process.cwd(), '.aaswe', 'mcp-cache');
await fs.mkdir(cacheDir, { recursive: true });
const cacheFile = path.join(cacheDir, `${moduleId}.json`);
await fs.writeFile(cacheFile, mcpContext.content, 'utf8');
logger_1.default.debug('Updated MCP context cache', { moduleId, cacheFile });
}
async notifyMCPClients(moduleId, mcpContext) {
const notification = {
type: 'module_knowledge_updated',
moduleId,
timestamp: new Date().toISOString(),
metadata: mcpContext.metadata,
content: mcpContext.content
};
// Notify via multiple channels for comprehensive coverage
const notificationPromises = [];
// 1. WebSocket notifications for real-time clients
if (process.env.MCP_WEBSOCKET_ENABLED === 'true') {
notificationPromises.push(this.sendWebSocketNotification(notification));
}
// 2. HTTP webhook notifications for external integrations
if (process.env.MCP_WEBHOOK_URL) {
notificationPromises.push(this.sendWebhookNotification(notification));
}
// 3. Message queue for reliable delivery
if (process.env.MCP_QUEUE_ENABLED === 'true') {
notificationPromises.push(this.sendQueueNotification(notification));
}
// 4. Event emission for local subscribers
this.emit('mcp_context_updated', notification);
// 5. Direct client notifications for registered MCP clients
for (const client of this.mcpClients) {
notificationPromises.push(this.notifyDirectClient(client, notification));
}
// Execute all notifications concurrently
try {
await Promise.allSettled(notificationPromises);
logger_1.default.debug('Successfully notified MCP clients', {
moduleId,
clientCount: this.mcpClients.size,
notificationChannels: notificationPromises.length
});
}
catch (error) {
logger_1.default.error('Failed to notify some MCP clients', { moduleId, error });
}
}
async sendWebSocketNotification(notification) {
try {
// Concrete WebSocket notification implementation using HTTP fallback
const notificationData = {
type: notification.type,
moduleId: notification.moduleId,
timestamp: notification.timestamp,
payload: notification.content
};
// Use HTTP POST as WebSocket alternative for reliable delivery
if (process.env.MCP_WEBSOCKET_ENABLED === 'true') {
const webhookEndpoint = process.env.MCP_WEBSOCKET_FALLBACK_URL || 'http://localhost:3001/ws-fallback';
try {
const response = await fetch(webhookEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Notification-Type': 'websocket-fallback'
},
body: JSON.stringify(notificationData)
});
if (response.ok) {
logger_1.default.info('WebSocket fallback notification sent successfully', {
endpoint: webhookEndpoint,
status: response.status,
moduleId: notification.moduleId
});
}
else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
}
catch (error) {
// Fallback to local event emission
this.emit('websocket_fallback_failed', { notification: notificationData, error });
logger_1.default.warn('WebSocket fallback failed, using local event emission', {
error: error instanceof Error ? error.message : 'Unknown error',
moduleId: notification.moduleId
});
}
}
else {
logger_1.default.debug('WebSocket notifications disabled, skipping', {
moduleId: notification.moduleId
});
}
}
catch (error) {
logger_1.default.error('Failed to send WebSocket notification', { error });
throw error;
}
}
async sendWebhookNotification(notification) {
try {
const webhookUrl = process.env.MCP_WEBHOOK_URL;
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'AASWE-ModuleKnowledge/1.0',
'X-MCP-Event-Type': notification.type
},
body: JSON.stringify(notification)
});
if (!response.ok) {
throw new Error(`Webhook failed with status ${response.status}`);
}
logger_1.default.debug('Webhook notification sent successfully', {
url: webhookUrl,
status: response.status
});
}
catch (error) {
logger_1.default.error('Failed to send webhook notification', { error });
throw error;
}
}
async sendQueueNotification(notification) {
try {
// Simplified queue notification without Redis dependency
this.notificationQueue.push(notification);
// Process queue asynchronously
setImmediate(() => this.processNotificationQueue());
logger_1.default.debug('Queue notification added to in-memory queue', {
queueSize: this.notificationQueue.length,
notificationType: notification.type
});
}
catch (error) {
logger_1.default.error('Failed to send queue notification', { error });
throw error;
}
}
async notifyDirectClient(client, notification) {
try {
if (typeof client.notify === 'function') {
await client.notify(notification);
}
else if (typeof client.send === 'function') {
await client.send(JSON.stringify(notification));
}
else if (client.socket && client.socket.write) {
client.socket.write(JSON.stringify(notification) + '\n');
}
logger_1.default.debug('Direct client notification sent', { clientId: client.id });
}
catch (error) {
logger_1.default.error('Failed to notify direct client', { clientId: client.id, error });
throw error;
}
}
async processNotificationQueue() {
if (!this.notificationQueue || this.notificationQueue.length === 0) {
return;
}
const notifications = this.notificationQueue.splice(0, 10); // Process up to 10 at a time
for (const notification of notifications) {
try {
// Process notification with concrete implementation
logger_1.default.debug('Processing queued notification', {
moduleId: notification.moduleId,
type: notification.type,
timestamp: notification.timestamp
});
// Concrete processing: persist notification metadata to database
const notificationRecord = {
id: `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
moduleId: notification.moduleId,
type: notification.type,
timestamp: notification.timestamp,
processed: new Date().toISOString(),
content: notification.content ? JSON.stringify(notification.content).substring(0, 1000) : null,
metadata: notification.metadata || {}
};
// Persist to Neo4j database for audit trail and analytics
await this.persistNotificationToDatabase(notificationRecord);
logger_1.default.info('Notification processed and persisted to database', {
id: notificationRecord.id,
moduleId: notificationRecord.moduleId,
type: notificationRecord.type
});
}
catch (error) {
logger_1.default.error('Failed to process queued notification', { notification, error });
}
}
}
async persistNotificationToDatabase(notificationRecord) {
try {
// Use Neo4j to persist notification records for audit trail and analytics
const neo4j = await this.getNeo4jDriver();
const session = neo4j.session();
try {
await session.writeTransaction(async (tx) => {
// Create notification node with full audit information
await tx.run(`CREATE (n:NotificationRecord {
id: $id,
moduleId: $moduleId,
type: $type,
timestamp: $timestamp,
processed: $processed,
content: $content,
metadata: $metadata
})
WITH n
MATCH (m:Module {id: $moduleId})
CREATE (m)-[:HAS_NOTIFICATION]->(n)
RETURN n.id as notificationId`, {
id: notificationRecord.id,
moduleId: notificationRecord.moduleId,
type: notificationRecord.type,
timestamp: notificationRecord.timestamp,
processed: notificationRecord.processed,
content: notificationRecord.content,
metadata: JSON.stringify(notificationRecord.metadata)
});
// Update module statistics
await tx.run(`MATCH (m:Module {id: $moduleId})
SET m.lastNotification = $timestamp,
m.notificationCount = COALESCE(m.notificationCount, 0) + 1`, {
moduleId: notificationRecord.moduleId,
timestamp: notificationRecord.processed
});
});
logger_1.default.debug('Notification persisted to Neo4j database', {
id: notificationRecord.id,
moduleId: notificationRecord.moduleId
});
}
finally {
await session.close();
}
}
catch (error) {
logger_1.default.error('Failed to persist notification to database', {
notificationId: notificationRecord.id,
error: error instanceof Error ? error.message : 'Unknown error'
});
// Fallback: persist to local file system for reliability
await this.persistNotificationToFileSystem(notificationRecord);
}
}
async persistNotificationToFileSystem(notificationRecord) {
try {
const notificationsDir = path.join(process.cwd(), '.aaswe', 'notifications');
await fs.mkdir(notificationsDir, { recursive: true });
const fileName = `${notificationRecord.id}.json`;
const filePath = path.join(notificationsDir, fileName);
await fs.writeFile(filePath, JSON.stringify(notificationRecord, null, 2), 'utf8');
logger_1.default.debug('Notification persisted to file system as fallback', {
id: notificationRecord.id,
filePath
});
}
catch (error) {
logger_1.default.error('Failed to persist notification to file system', {
notificationId: notificationRecord.id,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
async analyzeAndUpdateFromSourceChange(sourceFilePath, knowledgeFilePath) {
try {
logger_1.default.debug('Analyzing source file changes and updating knowledge file', {
sourceFile: sourceFilePath,
knowledgeFile: knowledgeFilePath
});
// Read and analyze the source file
const sourceContent = await fs.readFile(sourceFilePath, 'utf8');
const fileExtension = path.extname(sourceFilePath);
// Create a mock AST analysis result based on file content
const astResult = await this.performLightweightAST(sourceContent, fileExtension);
// Update the knowledge file using the existing method
const updateResult = await this.updateKnowledgeFileFromCode(sourceFilePath, astResult);
if (updateResult.success) {
logger_1.default.debug('Successfully updated knowledge file from source changes', {
sourceFile: sourceFilePath,
knowledgeFile: knowledgeFilePath
});
}
else {
logger_1.default.warn('Failed to update knowledge file from source changes', {
sourceFile: sourceFilePath,
error: updateResult.error
});
// Fallback: just queue for sync
await this.queueForSync(knowledgeFilePath, 'both');
}
}
catch (error) {
logger_1.default.error('Failed to analyze and update from source change', {
sourceFile: sourceFilePath,
knowledgeFile: knowledgeFilePath,
error: error instanceof Error ? error.message : 'Unknown error'
});
// Fallback: queue for sync
await this.queueForSync(knowledgeFilePath, 'both');
}
}
async performLightweightAST(content, fileExtension) {
// Lightweight AST analysis without external dependencies
const analysis = {
filePath: '',
language: this.getLanguageFromExtension(fileExtension),
functions: this.extractFunctions(content),
classes: this.extractClasses(content),
imports: this.extractImports(content),
exports: this.extractExports(content),
complexity: this.calculateComplexity(content),
dependencies: this.extractDependencies(content)
};
return analysis;
}
getLanguageFromExtension(extension) {
const languageMap = {
'.ts': 'typescript',
'.js': 'javascript',
'.py': 'python',
'.java': 'java',
'.go': 'go',
'.rs': 'rust',
'.cpp': 'cpp',
'.hpp': 'cpp'
};
return languageMap[extension] || 'unknown';
}
extractFunctions(content) {
const functions = [];
const lines = content.split('\n');
// Regex patterns for different function declarations
const patterns = [
/(?:function\s+|const\s+|let\s+|var\s+)(\w+)\s*[=:]?\s*(?:\([^)]*\)|function\s*\([^)]*\))/g,
/(\w+)\s*\([^)]*\)\s*[{:]/g,
/def\s+(\w+)\s*\([^)]*\):/g, // Python
/(?:public|private|protected)?\s*(?:static\s+)?(?:\w+\s+)*(\w+)\s*\([^)]*\)\s*{/g // Java/C++
];
lines.forEach((line, index) => {
patterns.forEach(pattern => {
const matches = line.matchAll(pattern);
for (const match of matches) {
if (match[1] && !functions.some(f => f.name === match[1])) {
functions.push({
name: match[1],
line: index + 1,
parameters: this.extractParameters(line)
});
}
}
});
});
return functions;
}
extractClasses(content) {
const classes = [];
const lines = content.split('\n');
const classPatterns = [
/class\s+(\w+)/g,
/interface\s+(\w+)/g,
/type\s+(\w+)/g
];
lines.forEach((line, index) => {
classPatterns.forEach(pattern => {
const matches = line.matchAll(pattern);
for (const match of matches) {
if (match[1]) {
classes.push({
name: match[1],
line: index + 1,
methods: this.extractClassMethods(content, match[1], index)
});
}
}
});
});
return classes;
}
extractImports(content) {
const imports = [];
const importPatterns = [
/import\s+.*?from\s+['"]([^'"]+)['"]/g,
/import\s+['"]([^'"]+)['"]/g,
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
/from\s+(\w+)\s+import/g, // Python
/import\s+(\w+(?:\.\w+)*)/g // Java
];
importPatterns.forEach(pattern => {
const matches = content.matchAll(pattern);
for (const match of matches) {
if (match[1] && !imports.includes(match[1])) {
imports.push(match[1]);
}
}
});
return imports;
}
extractExports(content) {
const exports = [];
const exportPatterns = [
/export\s+(?:default\s+)?(?:class\s+|function\s+|const\s+|let\s+|var\s+)?(\w+)/g,
/module\.exports\s*=\s*(\w+)/g
];
exportPatterns.forEach(pattern => {
const matches = content.matchAll(pattern);
for (const match of matches) {
if (match[1] && !exports.includes(match[1])) {
exports.push(match[1]);
}
}
});
return exports;
}
calculateComplexity(content) {
// Simple cyclomatic complexity calculation
const complexityKeywords = [
'if', 'else', 'elif', 'while', 'for', 'switch', 'case',
'try', 'catch', 'finally', '&&', '||', '?'
];
let complexity = 1; // Base complexity
complexityKeywords.forEach(keyword => {
const regex = new RegExp(`\\b${keyword}\\b`, 'g');
const matches = content.match(regex);
if (matches) {
complexity += matches.length;
}
});
return complexity;
}
extractDependencies(content) {
// Extract dependencies from import statements
return this.extractImports(content).filter(imp => !imp.startsWith('.') && !imp.startsWith('/') // External dependencies only
);
}
extractParameters(line) {
const paramMatch = line.match(/\(([^)]*)\)/);
if (!paramMatch || !paramMatch[1]) {
return [];
}
return paramMatch[1]
.split(',')
.map(param => param.trim().split(/[:\s]/)[0])
.filter(param => param.length > 0);
}
extractClassMethods(content, className, classStartLine) {
const methods = [];
const lines = content.split('\n');
// Find the class body boundaries
let braceCount = 0;
let inClass = false;
for (let i = classStartLine; i < lines.length; i++) {
const line = lines[i];
if (line.includes(className) && (line.includes('class') || line.includes('interface'))) {
inClass = true;
}
if (inClass) {
// Count braces to find class boundaries
const openBraces = (line.match(/{/g) || []).length;
const closeBraces = (line.match(/}/g) || []).length;
braceCount += openBraces - closeBraces;
// Look for method patterns within the class
const methodPatterns = [
/(?:public|private|protected)?\s*(?:static\s+)?(?:async\s+)?(\w+)\s*\([^)]*\)\s*[{:]/g,
/(\w+)\s*:\s*(?:function\s*)?\([^)]*\)\s*=>/g, // Arrow functions
/(\w+)\s*\([^)]*\)\s*{/g // Simple function declarations
];
methodPatterns.forEach(pattern => {
const matches = line.matchAll(pattern);
for (const match of matches) {
if (match[1] &&
match[1] !== className && // Not constructor
!methods.includes(match[1]) &&
!['constructor', 'if', 'for', 'while', 'switch'].includes(match[1])) {
methods.push(match[1]);
}
}
});
// Exit when we've closed all braces (end of class)
if (braceCount === 0 && i > classStartLine) {
break;
}
}
}
return methods;
}
}
exports.ModuleKnowledgeManager = ModuleKnowledgeManager;
//# sourceMappingURL=ModuleKnowledgeManager.js.map