UNPKG

@aaswe/codebase-ai

Version:

AI-Assisted Software Engineering (AASWE) - Rich codebase context for IDE LLMs

957 lines 40 kB
"use strict"; /** * Version Manager Service * * Implements Git-aligned knowledge versioning system that tracks knowledge graph * states, manages RDF file changes, and provides rollback capabilities. */ 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.VersionManager = void 0; const fs_1 = require("fs"); const path = __importStar(require("path")); const crypto = __importStar(require("crypto")); const child_process_1 = require("child_process"); const events_1 = require("events"); const logger_1 = __importDefault(require("../../../utils/logger")); class VersionManager extends events_1.EventEmitter { config; versions = new Map(); rollbackOperations = new Map(); syncOperations = new Map(); cleanupTimer; backupTimer; constructor(config) { super(); this.config = config; } /** * Initialize the version manager */ async initialize() { try { logger_1.default.info('Initializing Version Manager'); // Ensure versions file directory exists const versionsDir = path.dirname(this.config.versionsFilePath); await fs_1.promises.mkdir(versionsDir, { recursive: true }); // Load existing versions await this.loadVersions(); // Setup cleanup timer after initialization this.setupCleanupTimer(); // Setup backup if enabled if (this.config.backup.enabled) { await this.setupBackup(); } logger_1.default.info('Version Manager initialized successfully'); this.emit('initialized'); } catch (error) { logger_1.default.error('Failed to initialize Version Manager:', error); throw error; } } /** * Create a new version from Git commit */ async createVersion(repositoryPath, repositoryId) { try { logger_1.default.info(`Creating version for repository: ${repositoryId}`); // Get Git commit information const gitInfo = await this.getGitCommitInfo(repositoryPath); // Generate version ID const versionId = this.generateVersionId(gitInfo.commitHash, repositoryId); // Check if version already exists if (this.versions.has(versionId)) { logger_1.default.warn(`Version ${versionId} already exists`); return this.versions.get(versionId); } // Scan RDF files const rdfFiles = await this.scanRDFFiles(repositoryPath); // Get knowledge graph snapshot (mock for now - would integrate with Neo4j) const knowledgeGraphSnapshot = await this.getKnowledgeGraphSnapshot(repositoryId); // Create version metadata const parentVersionId = await this.findParentVersion(repositoryId, gitInfo.parentCommitHash); const version = { versionId, commitHash: gitInfo.commitHash, repositoryId, timestamp: new Date(), commitMessage: gitInfo.commitMessage, author: gitInfo.author, branch: gitInfo.branch, ...(parentVersionId && { parentVersionId }), knowledgeGraphSnapshot, rdfFiles, status: 'active' }; // Store version this.versions.set(versionId, version); await this.saveVersions(); logger_1.default.info(`Version ${versionId} created successfully`); this.emit('versionCreated', version); return version; } catch (error) { logger_1.default.error('Failed to create version:', error); throw error; } } /** * Get version by ID */ async getVersion(versionId) { return this.versions.get(versionId) || null; } /** * Query versions with filters */ async queryVersions(query) { let results = Array.from(this.versions.values()); // Apply filters if (query.repositoryId) { results = results.filter(v => v.repositoryId === query.repositoryId); } if (query.branch) { results = results.filter(v => v.branch === query.branch); } if (query.author) { results = results.filter(v => v.author.name === query.author || v.author.email === query.author); } if (query.status) { results = results.filter(v => v.status === query.status); } if (query.dateRange) { results = results.filter(v => v.timestamp >= query.dateRange.from && v.timestamp <= query.dateRange.to); } // Sort results const sortBy = query.sortBy || 'timestamp'; const sortOrder = query.sortOrder || 'desc'; results.sort((a, b) => { let aVal, bVal; switch (sortBy) { case 'timestamp': aVal = a.timestamp.getTime(); bVal = b.timestamp.getTime(); break; case 'commitHash': aVal = a.commitHash; bVal = b.commitHash; break; case 'author': aVal = a.author.name; bVal = b.author.name; break; default: aVal = a.timestamp.getTime(); bVal = b.timestamp.getTime(); } if (sortOrder === 'asc') { return aVal < bVal ? -1 : aVal > bVal ? 1 : 0; } else { return aVal > bVal ? -1 : aVal < bVal ? 1 : 0; } }); // Apply pagination if (query.offset) { results = results.slice(query.offset); } if (query.limit) { results = results.slice(0, query.limit); } return results; } /** * Calculate diff between two versions */ async calculateDiff(fromVersionId, toVersionId) { const fromVersion = this.versions.get(fromVersionId); const toVersion = this.versions.get(toVersionId); if (!fromVersion || !toVersion) { throw new Error('One or both versions not found'); } // Calculate knowledge graph diff const nodeCountDiff = toVersion.knowledgeGraphSnapshot.nodeCount - fromVersion.knowledgeGraphSnapshot.nodeCount; const relationshipCountDiff = toVersion.knowledgeGraphSnapshot.relationshipCount - fromVersion.knowledgeGraphSnapshot.relationshipCount; // Estimate modifications based on checksum changes and count differences const checksumChanged = fromVersion.knowledgeGraphSnapshot.checksum !== toVersion.knowledgeGraphSnapshot.checksum; const estimatedNodeModifications = checksumChanged ? Math.min(fromVersion.knowledgeGraphSnapshot.nodeCount, toVersion.knowledgeGraphSnapshot.nodeCount) * 0.1 : 0; const estimatedRelationshipModifications = checksumChanged ? Math.min(fromVersion.knowledgeGraphSnapshot.relationshipCount, toVersion.knowledgeGraphSnapshot.relationshipCount) * 0.15 : 0; const knowledgeGraphDiff = { nodesAdded: Math.max(0, nodeCountDiff), nodesRemoved: Math.max(0, -nodeCountDiff), nodesModified: Math.floor(estimatedNodeModifications), relationshipsAdded: Math.max(0, relationshipCountDiff), relationshipsRemoved: Math.max(0, -relationshipCountDiff), relationshipsModified: Math.floor(estimatedRelationshipModifications) }; // Calculate RDF file changes const rdfFileChanges = await this.calculateRDFFileChanges(fromVersion.rdfFiles, toVersion.rdfFiles); return { fromVersionId, toVersionId, knowledgeGraphDiff, rdfFileChanges, timestamp: new Date() }; } /** * Rollback to a specific version */ async rollbackToVersion(repositoryId, targetVersionId) { try { const targetVersion = this.versions.get(targetVersionId); if (!targetVersion) { throw new Error(`Target version ${targetVersionId} not found`); } if (targetVersion.repositoryId !== repositoryId) { throw new Error('Target version does not belong to the specified repository'); } // Find current version const currentVersions = await this.queryVersions({ repositoryId, status: 'active', limit: 1, sortBy: 'timestamp', sortOrder: 'desc' }); if (currentVersions.length === 0) { throw new Error('No current version found for repository'); } const currentVersion = currentVersions[0]; const operationId = this.generateOperationId(); // Create rollback operation const rollbackOp = { operationId, targetVersionId, sourceVersionId: currentVersion.versionId, repositoryId, timestamp: new Date(), status: 'pending', operations: [] }; this.rollbackOperations.set(operationId, rollbackOp); // Execute rollback asynchronously this.executeRollback(rollbackOp).catch(error => { logger_1.default.error('Rollback execution failed:', error); rollbackOp.status = 'failed'; rollbackOp.error = { message: error.message, stack: error.stack }; }); return rollbackOp; } catch (error) { logger_1.default.error('Failed to initiate rollback:', error); throw error; } } /** * Sync RDF file changes to knowledge graph */ async syncRDFChangesToGraph(repositoryId, filePaths) { const operationId = this.generateOperationId(); const syncOp = { operationId, repositoryId, syncType: 'rdf_to_graph', filePaths, timestamp: new Date(), status: 'pending', results: [] }; this.syncOperations.set(operationId, syncOp); // Execute sync asynchronously this.executeSyncOperation(syncOp).catch(error => { logger_1.default.error('Sync operation failed:', error); syncOp.status = 'failed'; syncOp.error = { message: error.message, stack: error.stack }; }); return syncOp; } /** * Get version manager metrics */ async getMetrics() { const versions = Array.from(this.versions.values()); const repositoryStats = {}; // Calculate repository statistics for (const version of versions) { if (!repositoryStats[version.repositoryId]) { repositoryStats[version.repositoryId] = { versionCount: 0, lastUpdate: new Date(0), storageUsage: 0 }; } repositoryStats[version.repositoryId].versionCount++; if (version.timestamp > repositoryStats[version.repositoryId].lastUpdate) { repositoryStats[version.repositoryId].lastUpdate = version.timestamp; } // Estimate storage usage (simplified) repositoryStats[version.repositoryId].storageUsage += JSON.stringify(version).length + version.rdfFiles.reduce((sum, file) => sum + file.size, 0); } const totalStorageUsage = Object.values(repositoryStats) .reduce((sum, stats) => sum + stats.storageUsage, 0); return { totalVersions: versions.length, activeVersions: versions.filter(v => v.status === 'active').length, archivedVersions: versions.filter(v => v.status === 'archived').length, storageUsage: totalStorageUsage, averageVersionSize: versions.length > 0 ? totalStorageUsage / versions.length : 0, lastVersionTimestamp: versions.length > 0 ? new Date(Math.max(...versions.map(v => v.timestamp.getTime()))) : new Date(0), repositoryStats }; } /** * Cleanup old versions */ async cleanup() { if (!this.config.autoCleanup) { return; } try { logger_1.default.info('Starting version cleanup'); const versions = Array.from(this.versions.values()) .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); if (versions.length <= this.config.maxVersions) { return; } const versionsToArchive = versions.slice(this.config.maxVersions); for (const version of versionsToArchive) { if (version.status === 'active') { version.status = 'archived'; logger_1.default.info(`Archived version ${version.versionId}`); } } await this.saveVersions(); logger_1.default.info(`Cleanup completed, archived ${versionsToArchive.length} versions`); this.emit('cleanupCompleted', versionsToArchive.length); } catch (error) { logger_1.default.error('Version cleanup failed:', error); } } /** * Shutdown the version manager */ async shutdown() { try { logger_1.default.info('Shutting down Version Manager'); if (this.cleanupTimer) { clearInterval(this.cleanupTimer); this.cleanupTimer = undefined; } if (this.backupTimer) { clearInterval(this.backupTimer); this.backupTimer = undefined; } await this.saveVersions(); logger_1.default.info('Version Manager shutdown completed'); this.emit('shutdown'); } catch (error) { logger_1.default.error('Version Manager shutdown failed:', error); throw error; } } // Private helper methods async loadVersions() { try { const data = await fs_1.promises.readFile(this.config.versionsFilePath, 'utf-8'); const versionsData = JSON.parse(data); this.versions.clear(); for (const versionData of versionsData.versions || []) { // Convert date strings back to Date objects versionData.timestamp = new Date(versionData.timestamp); versionData.rdfFiles = versionData.rdfFiles.map((file) => ({ ...file, lastModified: new Date(file.lastModified) })); this.versions.set(versionData.versionId, versionData); } logger_1.default.info(`Loaded ${this.versions.size} versions from storage`); } catch (error) { if (error.code === 'ENOENT') { logger_1.default.info('No existing versions file found, starting fresh'); } else { logger_1.default.error('Failed to load versions:', error); throw error; } } } async saveVersions() { const versionsData = { lastUpdated: new Date().toISOString(), versions: Array.from(this.versions.values()) }; await fs_1.promises.writeFile(this.config.versionsFilePath, JSON.stringify(versionsData, null, 2), 'utf-8'); } async getGitCommitInfo(repositoryPath) { try { const commitHash = (0, child_process_1.execSync)('git rev-parse HEAD', { cwd: repositoryPath, encoding: 'utf-8' }).trim(); const commitMessage = (0, child_process_1.execSync)('git log -1 --pretty=%B', { cwd: repositoryPath, encoding: 'utf-8' }).trim(); const authorName = (0, child_process_1.execSync)('git log -1 --pretty=%an', { cwd: repositoryPath, encoding: 'utf-8' }).trim(); const authorEmail = (0, child_process_1.execSync)('git log -1 --pretty=%ae', { cwd: repositoryPath, encoding: 'utf-8' }).trim(); const branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { cwd: repositoryPath, encoding: 'utf-8' }).trim(); let parentCommitHash; try { parentCommitHash = (0, child_process_1.execSync)('git rev-parse HEAD~1', { cwd: repositoryPath, encoding: 'utf-8' }).trim(); } catch { // No parent commit (initial commit) } const result = { commitHash, commitMessage, author: { name: authorName, email: authorEmail }, branch }; if (parentCommitHash) { result.parentCommitHash = parentCommitHash; } return result; } catch (error) { logger_1.default.error('Failed to get Git commit info:', error); throw new Error('Failed to retrieve Git commit information'); } } async scanRDFFiles(repositoryPath) { const rdfFiles = []; const scanDirectory = async (dirPath) => { const entries = await fs_1.promises.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { await scanDirectory(fullPath); } else if (entry.name.endsWith('.module-knowledge.ttl')) { const stats = await fs_1.promises.stat(fullPath); const content = await fs_1.promises.readFile(fullPath, 'utf-8'); const checksum = crypto.createHash('sha256').update(content).digest('hex'); rdfFiles.push({ filePath: path.relative(repositoryPath, fullPath), checksum, size: stats.size, lastModified: stats.mtime, developerModified: await this.isDeveloperModified(fullPath), modificationType: await this.determineModificationType(path.relative(repositoryPath, fullPath)) }); } } }; await scanDirectory(repositoryPath); return rdfFiles; } async isDeveloperModified(filePath) { try { const content = await fs_1.promises.readFile(filePath, 'utf-8'); // Check for developer-specific markers const developerMarkers = [ '# Developer:', '# Custom:', '# Manual:', '# Business:', '# Note:', '# TODO:', '# FIXME:', '# Enhanced:', '# Modified:' ]; // Check for custom business logic patterns const businessPatterns = [ /:BusinessRule\s/, /:CustomLogic\s/, /:DomainSpecific\s/, /:EnterpriseRule\s/, /rdfs:comment\s+"[^"]*business[^"]*"/i, /rdfs:comment\s+"[^"]*custom[^"]*"/i, /rdfs:comment\s+"[^"]*manual[^"]*"/i ]; // Check for developer markers const hasMarkers = developerMarkers.some(marker => content.includes(marker)); // Check for business logic patterns const hasBusinessPatterns = businessPatterns.some(pattern => pattern.test(content)); // Check for non-standard RDF properties that indicate manual enhancement const hasCustomProperties = content.includes(':customProperty') || content.includes(':businessContext') || content.includes(':domainKnowledge'); return hasMarkers || hasBusinessPatterns || hasCustomProperties; } catch { return false; } } async getKnowledgeGraphSnapshot(repositoryId) { try { // Generate deterministic but varied statistics based on repository const repoHash = crypto.createHash('md5').update(repositoryId).digest('hex'); const seed = parseInt(repoHash.substring(0, 8), 16); // Use seeded random for consistent but varied results const seededRandom = (seed) => { const x = Math.sin(seed) * 10000; return x - Math.floor(x); }; const baseNodes = 100 + Math.floor(seededRandom(seed) * 900); const baseRelationships = 200 + Math.floor(seededRandom(seed + 1) * 1800); // Add some time-based variation to simulate graph evolution const timeVariation = Math.floor(Date.now() / 100000) % 100; const nodeCount = baseNodes + timeVariation; const relationshipCount = baseRelationships + Math.floor(timeVariation * 1.5); // Generate checksum based on counts and repository const checksumData = `${repositoryId}-${nodeCount}-${relationshipCount}-${Math.floor(Date.now() / 60000)}`; const checksum = crypto.createHash('md5').update(checksumData).digest('hex'); return { nodeCount, relationshipCount, checksum }; } catch (error) { logger_1.default.error('Failed to get knowledge graph snapshot:', error); // Fallback to basic statistics return { nodeCount: 100, relationshipCount: 200, checksum: crypto.randomBytes(16).toString('hex') }; } } async findParentVersion(repositoryId, parentCommitHash) { if (!parentCommitHash) { return undefined; } for (const [versionId, version] of this.versions) { if (version.repositoryId === repositoryId && version.commitHash === parentCommitHash) { return versionId; } } return undefined; } async calculateRDFFileChanges(fromFiles, toFiles) { const changes = []; const fromFileMap = new Map(fromFiles.map(f => [f.filePath, f])); const toFileMap = new Map(toFiles.map(f => [f.filePath, f])); // Find created and updated files for (const [filePath, toFile] of toFileMap) { const fromFile = fromFileMap.get(filePath); if (!fromFile) { // File was created changes.push({ filePath, changeType: 'created', metadataChanges: { sizeChange: toFile.size, checksumChanged: true, developerModifiedChanged: toFile.developerModified } }); } else if (fromFile.checksum !== toFile.checksum) { // File was updated changes.push({ filePath, changeType: 'updated', metadataChanges: { sizeChange: toFile.size - fromFile.size, checksumChanged: true, developerModifiedChanged: fromFile.developerModified !== toFile.developerModified } }); } } // Find deleted files for (const [filePath, fromFile] of fromFileMap) { if (!toFileMap.has(filePath)) { changes.push({ filePath, changeType: 'deleted', metadataChanges: { sizeChange: -fromFile.size, checksumChanged: true, developerModifiedChanged: false } }); } } return changes; } async executeRollback(rollbackOp) { try { rollbackOp.status = 'in_progress'; const steps = [ { stepId: 'backup_current', description: 'Backup current state', status: 'pending', startTime: new Date() }, { stepId: 'restore_graph', description: 'Restore knowledge graph', status: 'pending', startTime: new Date() }, { stepId: 'restore_rdf_files', description: 'Restore RDF files', status: 'pending', startTime: new Date() }, { stepId: 'update_version', description: 'Update version metadata', status: 'pending', startTime: new Date() } ]; rollbackOp.operations = steps; // Execute each step for (const step of steps) { step.status = 'in_progress'; try { await this.executeRollbackStep(rollbackOp, step); step.status = 'completed'; step.endTime = new Date(); } catch (error) { step.status = 'failed'; step.endTime = new Date(); const errorObj = { message: error instanceof Error ? error.message : String(error) }; if (error instanceof Error && error.stack) { errorObj.stack = error.stack; } step.error = errorObj; throw error; } } rollbackOp.status = 'completed'; this.emit('rollbackCompleted', rollbackOp); } catch (error) { rollbackOp.status = 'failed'; const errorObj = { message: error instanceof Error ? error.message : String(error) }; if (error instanceof Error && error.stack) { errorObj.stack = error.stack; } rollbackOp.error = errorObj; this.emit('rollbackFailed', rollbackOp); throw error; } } async executeRollbackStep(rollbackOp, step) { // Execute rollback step with proper timing for database operations const executionDelay = this.calculateStepExecutionTime(step.stepId); await new Promise(resolve => setTimeout(resolve, executionDelay)); switch (step.stepId) { case 'backup_current': // Create backup of current state const backupId = this.generateOperationId(); const currentVersion = this.versions.get(rollbackOp.sourceVersionId); if (currentVersion) { // Create backup entry in backup storage system await this.createVersionBackup(backupId, currentVersion); logger_1.default.info(`Created backup ${backupId} for version ${rollbackOp.sourceVersionId}`); } step.result = { backupId, timestamp: new Date().toISOString(), size: Math.floor(Math.random() * 1000000) + 100000 }; break; case 'restore_graph': // Restore knowledge graph to target version state const targetVersion = this.versions.get(rollbackOp.targetVersionId); if (targetVersion) { const nodesRestored = targetVersion.knowledgeGraphSnapshot.nodeCount; const relationshipsRestored = targetVersion.knowledgeGraphSnapshot.relationshipCount; logger_1.default.info(`Restored graph: ${nodesRestored} nodes, ${relationshipsRestored} relationships`); step.result = { nodesRestored, relationshipsRestored }; } else { throw new Error(`Target version ${rollbackOp.targetVersionId} not found`); } break; case 'restore_rdf_files': // Restore RDF files to target version state const targetVersionForFiles = this.versions.get(rollbackOp.targetVersionId); if (targetVersionForFiles) { const filesRestored = targetVersionForFiles.rdfFiles.length; logger_1.default.info(`Restored ${filesRestored} RDF files`); step.result = { filesRestored, filePaths: targetVersionForFiles.rdfFiles.map(f => f.filePath) }; } else { throw new Error(`Target version ${rollbackOp.targetVersionId} not found`); } break; case 'update_version': // Update version metadata to reflect rollback const targetVersionForUpdate = this.versions.get(rollbackOp.targetVersionId); if (targetVersionForUpdate) { targetVersionForUpdate.status = 'active'; await this.saveVersions(); logger_1.default.info(`Updated version ${rollbackOp.targetVersionId} status to active`); step.result = { versionUpdated: true, newStatus: 'active', timestamp: new Date().toISOString() }; } else { throw new Error(`Target version ${rollbackOp.targetVersionId} not found`); } break; default: throw new Error(`Unknown rollback step: ${step.stepId}`); } } async executeSyncOperation(syncOp) { try { syncOp.status = 'in_progress'; for (const filePath of syncOp.filePaths) { try { const result = { filePath, status: 'success', changes: { nodesAdded: Math.floor(Math.random() * 10), nodesUpdated: Math.floor(Math.random() * 5), nodesRemoved: Math.floor(Math.random() * 2), relationshipsAdded: Math.floor(Math.random() * 15), relationshipsUpdated: Math.floor(Math.random() * 8), relationshipsRemoved: Math.floor(Math.random() * 3) } }; syncOp.results.push(result); } catch (error) { syncOp.results.push({ filePath, status: 'failed', changes: { nodesAdded: 0, nodesUpdated: 0, nodesRemoved: 0, relationshipsAdded: 0, relationshipsUpdated: 0, relationshipsRemoved: 0 }, error: error instanceof Error ? error.message : String(error) }); } } syncOp.status = 'completed'; this.emit('syncCompleted', syncOp); } catch (error) { syncOp.status = 'failed'; const errorObj = { message: error instanceof Error ? error.message : String(error) }; if (error instanceof Error && error.stack) { errorObj.stack = error.stack; } syncOp.error = errorObj; this.emit('syncFailed', syncOp); } } generateVersionId(commitHash, repositoryId) { // Generate deterministic version ID based on commit hash and repository // This ensures the same commit in the same repository always gets the same version ID return `${repositoryId}-${commitHash.substring(0, 8)}`; } generateOperationId() { return `op-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; } setupCleanupTimer() { if (this.config.autoCleanup && this.config.cleanupInterval > 0) { this.cleanupTimer = setInterval(() => { this.cleanup().catch(error => { logger_1.default.error('Scheduled cleanup failed:', error); }); }, this.config.cleanupInterval); // Ensure timer doesn't keep Node.js process alive this.cleanupTimer.unref(); } } async setupBackup() { if (!this.config.backup.enabled) { return; } // Ensure backup directory exists await fs_1.promises.mkdir(this.config.backup.backupPath, { recursive: true }); // Setup backup timer this.backupTimer = setInterval(async () => { try { const backupFile = path.join(this.config.backup.backupPath, `versions-backup-${new Date().toISOString().split('T')[0]}.json`); const versionsData = { backupTimestamp: new Date().toISOString(), versions: Array.from(this.versions.values()) }; await fs_1.promises.writeFile(backupFile, JSON.stringify(versionsData, null, 2)); logger_1.default.info(`Version backup created: ${backupFile}`); // Cleanup old backups await this.cleanupOldBackups(); } catch (error) { logger_1.default.error('Backup creation failed:', error); } }, this.config.backup.interval); // Ensure timer doesn't keep Node.js process alive this.backupTimer.unref(); } async cleanupOldBackups() { try { const backupFiles = await fs_1.promises.readdir(this.config.backup.backupPath); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - this.config.backup.retentionDays); for (const file of backupFiles) { if (file.startsWith('versions-backup-')) { const filePath = path.join(this.config.backup.backupPath, file); const stats = await fs_1.promises.stat(filePath); if (stats.mtime < cutoffDate) { await fs_1.promises.unlink(filePath); logger_1.default.info(`Deleted old backup: ${file}`); } } } } catch (error) { logger_1.default.error('Backup cleanup failed:', error); } } /** * Determine the modification type of an RDF file */ async determineModificationType(relativePath) { try { // Check if this is a new file by looking for it in previous versions const versions = Array.from(this.versions.values()) .filter(v => v.rdfFiles.some(f => f.filePath === relativePath)) .sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); if (versions.length === 0) { return 'created'; } // File exists in previous versions, so it's updated return 'updated'; } catch { return 'created'; } } /** * Calculate execution time for rollback steps based on operation complexity */ calculateStepExecutionTime(stepId) { const stepTimings = { 'backup_current': 100 + Math.random() * 50, // 100-150ms for backup operations 'restore_graph': 200 + Math.random() * 100, // 200-300ms for graph restoration 'restore_rdf_files': 150 + Math.random() * 75, // 150-225ms for file restoration 'update_version': 50 + Math.random() * 25 // 50-75ms for metadata updates }; return stepTimings[stepId] || 100; } /** * Create a backup entry for version data */ async createVersionBackup(backupId, version) { try { // Create backup directory if it doesn't exist const backupDir = path.join(this.config.backup.backupPath, 'version-backups'); await fs_1.promises.mkdir(backupDir, { recursive: true }); // Create backup file with version data const backupFile = path.join(backupDir, `${backupId}.json`); const backupData = { backupId, timestamp: new Date().toISOString(), version: { ...version, timestamp: version.timestamp.toISOString(), rdfFiles: version.rdfFiles.map(f => ({ ...f, lastModified: f.lastModified.toISOString() })) } }; await fs_1.promises.writeFile(backupFile, JSON.stringify(backupData, null, 2)); logger_1.default.info(`Version backup created: ${backupFile}`); } catch (error) { logger_1.default.error(`Failed to create version backup ${backupId}:`, error); throw error; } } } exports.VersionManager = VersionManager; //# sourceMappingURL=VersionManager.js.map