UNPKG

context-crystallizer

Version:

AI Crystallization Engineering for Large Repositories - Transform massive repositories into crystallized, AI-consumable knowledge through systematic analysis and optimization. Crystallization extracts meaningful context from any readable files.

205 lines 7.23 kB
import { promises as fs } from 'fs'; import path from 'path'; import crypto from 'crypto'; export class ChangeDetector { repoPath; manifestPath; contextBasePath; constructor(repoPath) { this.repoPath = path.resolve(repoPath); this.contextBasePath = path.join(this.repoPath, '.context-crystallizer'); this.manifestPath = path.join(this.contextBasePath, 'file-hash-manifest.json'); } async detectChanges(currentFiles) { const previousManifest = await this.loadManifest(); const newManifest = { version: '1.0', generatedAt: new Date(), files: {}, }; const changes = []; const processedPaths = new Set(); // Check for added or modified files for (const file of currentFiles) { processedPaths.add(file.path); const hash = await this.calculateFileHash(file.path); const hasContext = await this.hasExistingContext(file.relativePath); newManifest.files[file.path] = { hash, size: file.size, lastModified: file.lastModified.toISOString(), hasContext, }; const previousEntry = previousManifest?.files[file.path]; if (!previousEntry) { // New file changes.push({ path: file.path, relativePath: file.relativePath, type: 'added', newHash: hash, lastModified: file.lastModified, size: file.size, }); } else if (previousEntry.hash !== hash) { // Modified file changes.push({ path: file.path, relativePath: file.relativePath, type: 'modified', oldHash: previousEntry.hash, newHash: hash, lastModified: file.lastModified, size: file.size, }); } } // Check for deleted files if (previousManifest) { for (const [filePath, entry] of Object.entries(previousManifest.files)) { if (!processedPaths.has(filePath) && entry.hasContext) { changes.push({ path: filePath, relativePath: path.relative(this.repoPath, filePath), type: 'deleted', oldHash: entry.hash, lastModified: new Date(entry.lastModified), size: entry.size, }); } } } // Calculate stats const stats = { added: changes.filter(c => c.type === 'added').length, modified: changes.filter(c => c.type === 'modified').length, deleted: changes.filter(c => c.type === 'deleted').length, totalChanges: changes.length, }; // Save the new manifest await this.saveManifest(newManifest); return { changes, stats, hashManifest: newManifest, }; } async getFilesNeedingContext(currentFiles) { const needsContext = []; for (const file of currentFiles) { const hasContext = await this.hasExistingContext(file.relativePath); if (!hasContext) { needsContext.push(file); } } return needsContext; } async getOutdatedContexts(changes) { const outdatedContexts = []; for (const change of changes) { if (change.type === 'modified' || change.type === 'deleted') { const contextPath = path.join(this.contextBasePath, 'context', `${change.relativePath}.context.md`); try { await fs.access(contextPath); outdatedContexts.push(change.relativePath); } catch { // Context doesn't exist, nothing to update } } } return outdatedContexts; } async cleanupObsoleteContexts(deletedFiles) { let cleanedCount = 0; for (const relativePath of deletedFiles) { // Remove context file const contextPath = path.join(this.contextBasePath, 'context', `${relativePath}.context.md`); try { await fs.unlink(contextPath); cleanedCount++; } catch { // File might not exist } // Remove metadata file const metadataPath = path.join(this.contextBasePath, 'ai-metadata', `${relativePath.replace(/\//g, '_')}.json`); try { await fs.unlink(metadataPath); } catch { // File might not exist } } return cleanedCount; } async getChangesSummary() { const manifest = await this.loadManifest(); if (!manifest) { return { lastScan: null, totalTrackedFiles: 0, filesWithContext: 0, contextCoverage: 0, }; } const totalTrackedFiles = Object.keys(manifest.files).length; const filesWithContext = Object.values(manifest.files).filter(f => f.hasContext).length; const contextCoverage = totalTrackedFiles > 0 ? Math.round((filesWithContext / totalTrackedFiles) * 100) : 0; return { lastScan: manifest.generatedAt, totalTrackedFiles, filesWithContext, contextCoverage, }; } async calculateFileHash(filePath) { try { const content = await fs.readFile(filePath); const hash = crypto.createHash('sha256'); hash.update(content); return hash.digest('hex'); } catch (_error) { // If file can't be read, return empty hash return ''; } } async hasExistingContext(relativePath) { const contextPath = path.join(this.contextBasePath, 'context', `${relativePath}.context.md`); try { await fs.access(contextPath); return true; } catch { return false; } } async loadManifest() { try { const content = await fs.readFile(this.manifestPath, 'utf-8'); const manifest = JSON.parse(content); manifest.generatedAt = new Date(manifest.generatedAt); return manifest; } catch { return null; } } async saveManifest(manifest) { await fs.mkdir(this.contextBasePath, { recursive: true }); await fs.writeFile(this.manifestPath, JSON.stringify(manifest, null, 2), 'utf-8'); } async resetManifest() { try { await fs.unlink(this.manifestPath); } catch { // File might not exist } } } //# sourceMappingURL=change-detector.js.map