import { readFile, readdir, stat } from 'node:fs/promises'; import path from 'node:path'; import { fail, pass, repositoryRoot, sha256 } from './lib.mjs'; const manifestName = 'PHASE_11_PACKAGE_MANIFEST_v1.0.json'; const manifestPath = path.join(repositoryRoot, manifestName); const excludedDirectories = new Set([ '.git', '.next', 'node_modules', 'coverage', 'playwright-report', 'test-results', ]); const excludedFiles = new Set([manifestName, 'tsconfig.tsbuildinfo']); async function walk(directory) { const files = []; const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && excludedDirectories.has(entry.name)) continue; const fullPath = path.join(directory, entry.name); if (entry.isDirectory()) { files.push(...(await walk(fullPath))); continue; } 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(manifestPath, 'utf8')); const failures = []; if (manifest.project !== 'RentalDriveGo' || manifest.phase !== 11) { failures.push('Phase 11 manifest identity is invalid.'); } if (!Array.isArray(manifest.files)) { failures.push('Manifest files property must be an array.'); } else if (manifest.file_count !== manifest.files.length) { failures.push( `Manifest file_count ${manifest.file_count} does not match ${manifest.files.length} entries.`, ); } const listedPaths = new Set(); for (const entry of manifest.files ?? []) { if ( typeof entry.path !== 'string' || entry.path.length === 0 || path.isAbsolute(entry.path) || entry.path.split('/').includes('..') ) { failures.push(`Unsafe manifest path: ${String(entry.path)}`); continue; } if (listedPaths.has(entry.path)) { failures.push(`${entry.path}: duplicate manifest entry.`); continue; } listedPaths.add(entry.path); const filePath = path.join(repositoryRoot, entry.path); try { const details = await stat(filePath); if (!details.isFile()) failures.push(`${entry.path}: is not a regular file.`); if (details.size !== entry.bytes) failures.push(`${entry.path}: byte size changed.`); const digest = await sha256(filePath); if (digest !== entry.sha256) failures.push(`${entry.path}: SHA-256 changed.`); } catch { failures.push(`${entry.path}: file is missing.`); } } const actualPaths = new Set(await walk(repositoryRoot)); for (const actual of actualPaths) { if (!listedPaths.has(actual)) failures.push(`${actual}: unlisted repository file.`); } for (const listed of listedPaths) { if (!actualPaths.has(listed)) failures.push(`${listed}: listed but excluded or absent.`); } if (failures.length) fail(failures); else { pass(`Phase 11 package manifest verifies ${listedPaths.size} repository files with no extras.`); }