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
JavaScript
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