import { readFile } from 'node:fs/promises'; import { access } from 'node:fs/promises'; import { join } from 'node:path'; const SOURCE_COUNT = 29; const DOCS_ROOT = 'src/content/docs/'; export async function loadManifest(path) { return JSON.parse(await readFile(path, 'utf8')); } export function validateManifest(manifest) { if (!Array.isArray(manifest?.entries)) { throw new TypeError('Migration manifest entries must be an array.'); } if (manifest.entries.length !== SOURCE_COUNT) { throw new Error(`Migration manifest must contain exactly ${SOURCE_COUNT} entries.`); } const sources = new Set(); const destinations = new Set(); for (const entry of manifest.entries) { if (!entry || typeof entry !== 'object') { throw new TypeError('Each migration manifest entry must be an object.'); } const { source, destination, mode } = entry; if (typeof source !== 'string' || typeof destination !== 'string') { throw new TypeError('Each migration manifest entry needs string source and destination paths.'); } if (!['page', 'merge'].includes(mode)) { throw new TypeError('Each migration manifest entry mode must be page or merge.'); } if (source.startsWith('pages/wiki/')) { throw new Error(`Generic DokuWiki source is excluded: ${source}`); } if (!(source === 'pages/start.txt' || source === 'pages/study.txt' || source.startsWith('pages/study/'))) { throw new Error(`Source is outside the selected migration scope: ${source}`); } if (!destination.startsWith(DOCS_ROOT)) { throw new Error(`Destination is outside ${DOCS_ROOT}: ${destination}`); } if (sources.has(source)) { throw new Error(`Duplicate migration source: ${source}`); } if (destinations.has(destination) && mode !== 'merge') { throw new Error(`Duplicate destination requires a later merge entry: ${destination}`); } sources.add(source); destinations.add(destination); } return { entries: manifest.entries }; } export async function checkDestinations(manifest, root) { const destinations = [...new Set(manifest.entries.map(({ destination }) => destination))]; const missing = []; for (const destination of destinations) { try { await access(join(root, destination)); } catch { missing.push(destination); } } return { total: manifest.entries.length, missing, }; }