Reference implementation for the Phoenix Architecture. Work in progress. aicoding.leaflet.pub/
ai coding crazy
at main 86 lines 2.9 kB view raw
1/** 2 * Drift Detection — compares working tree vs generated manifest. 3 * 4 * Detects when generated files have been manually edited without 5 * a waiver, which breaks the provenance chain. 6 */ 7 8import { readFileSync, existsSync } from 'node:fs'; 9import type { GeneratedManifest, DriftEntry, DriftReport, DriftWaiver } from './models/manifest.js'; 10import { DriftStatus } from './models/manifest.js'; 11import { sha256 } from './semhash.js'; 12 13/** 14 * Check all files in the manifest against the working tree. 15 */ 16export function detectDrift( 17 manifest: GeneratedManifest, 18 projectRoot: string, 19 waivers?: Map<string, DriftWaiver>, 20): DriftReport { 21 const entries: DriftEntry[] = []; 22 23 for (const iuManifest of Object.values(manifest.iu_manifests)) { 24 for (const [filePath, entry] of Object.entries(iuManifest.files)) { 25 const fullPath = projectRoot + '/' + filePath; 26 const waiver = waivers?.get(filePath); 27 28 if (!existsSync(fullPath)) { 29 entries.push({ 30 status: DriftStatus.MISSING, 31 file_path: filePath, 32 iu_id: iuManifest.iu_id, 33 expected_hash: entry.content_hash, 34 }); 35 continue; 36 } 37 38 const actualContent = readFileSync(fullPath, 'utf8'); 39 const actualHash = sha256(actualContent); 40 41 if (actualHash === entry.content_hash) { 42 entries.push({ 43 status: DriftStatus.CLEAN, 44 file_path: filePath, 45 iu_id: iuManifest.iu_id, 46 expected_hash: entry.content_hash, 47 actual_hash: actualHash, 48 }); 49 } else if (waiver) { 50 entries.push({ 51 status: DriftStatus.WAIVED, 52 file_path: filePath, 53 iu_id: iuManifest.iu_id, 54 expected_hash: entry.content_hash, 55 actual_hash: actualHash, 56 waiver, 57 }); 58 } else { 59 entries.push({ 60 status: DriftStatus.DRIFTED, 61 file_path: filePath, 62 iu_id: iuManifest.iu_id, 63 expected_hash: entry.content_hash, 64 actual_hash: actualHash, 65 }); 66 } 67 } 68 } 69 70 const clean = entries.filter(e => e.status === DriftStatus.CLEAN).length; 71 const drifted = entries.filter(e => e.status === DriftStatus.DRIFTED).length; 72 const missing = entries.filter(e => e.status === DriftStatus.MISSING).length; 73 const waived = entries.filter(e => e.status === DriftStatus.WAIVED).length; 74 75 let summary: string; 76 if (drifted === 0 && missing === 0) { 77 summary = `All ${clean} generated files are clean.${waived > 0 ? ` ${waived} waived.` : ''}`; 78 } else { 79 const parts: string[] = []; 80 if (drifted > 0) parts.push(`${drifted} drifted`); 81 if (missing > 0) parts.push(`${missing} missing`); 82 summary = `DRIFT DETECTED: ${parts.join(', ')}. ${clean} clean.`; 83 } 84 85 return { entries, clean_count: clean, drifted_count: drifted, missing_count: missing, summary }; 86}