import { readFile, readdir, stat } from 'node:fs/promises'; import path from 'node:path'; import { fail, pass, repositoryRoot, sha256 } from './lib.mjs'; const manifestName = 'PHASE_16_PACKAGE_MANIFEST_v1.0.json'; const excludedDirectories = new Set(['.git', '.next', 'node_modules', 'coverage', 'playwright-report', 'test-results', '__pycache__']); const excludedFiles = new Set([manifestName, 'package-lock.json', 'tsconfig.tsbuildinfo']); async function walk(directory) { const files = []; for (const entry of await readdir(directory, { withFileTypes: true })) { if (entry.isDirectory() && excludedDirectories.has(entry.name)) continue; const fullPath = path.join(directory, entry.name); if (entry.isDirectory()) files.push(...(await walk(fullPath))); else { const relative = path.relative(repositoryRoot, fullPath).split(path.sep).join('/'); if (!excludedFiles.has(relative)) files.push(relative); } } return files; } const manifest = JSON.parse(await readFile(path.join(repositoryRoot, manifestName), 'utf8')); const failures = []; if (manifest.project !== 'RentalDriveGo' || manifest.phase !== 16) failures.push('Phase 16 manifest identity is invalid.'); if (manifest.release_status !== 'Launch aborted before deployment') failures.push('Manifest release status is not the recorded Phase 16 decision.'); if (!Array.isArray(manifest.files) || manifest.file_count !== manifest.files.length) failures.push('Manifest file count is invalid.'); const listed = new Set(); for (const entry of manifest.files ?? []) { if (typeof entry.path !== 'string' || path.isAbsolute(entry.path) || entry.path.split('/').includes('..')) { failures.push(`Unsafe manifest path: ${String(entry.path)}`); continue; } if (listed.has(entry.path)) { failures.push(`${entry.path}: duplicate manifest entry.`); continue; } listed.add(entry.path); try { const filePath = path.join(repositoryRoot, entry.path); const details = await stat(filePath); if (!details.isFile() || details.size !== entry.bytes) failures.push(`${entry.path}: file metadata changed.`); if ((await sha256(filePath)) !== entry.sha256) failures.push(`${entry.path}: SHA-256 changed.`); } catch { failures.push(`${entry.path}: file is missing.`); } } const actual = new Set(await walk(repositoryRoot)); for (const file of actual) if (!listed.has(file)) failures.push(`${file}: unlisted repository file.`); for (const file of listed) if (!actual.has(file)) failures.push(`${file}: listed but excluded or absent.`); if (failures.length) fail(failures); else pass(`Phase 16 package manifest verifies ${listed.size} repository files with no extras.`);