import { readFile } from "node:fs/promises"; import { spawnSync } from "node:child_process"; import { CACHEABLE_ASSET_CONTENT_TYPES } from "../src/contracts/service-worker-static-manifest.ts"; /** * GOV-01 / SW-RR-03. Structural gates for facts that a hand-maintained document * cannot keep true. * * The adapter review inventory claimed 118/118 while the tree held 119 files, * so a whole adapter was outside every review's coverage without anything * failing. And the Service Worker asset generator and the shared manifest * decoder each carried their own extension table, so a build could emit an * asset the runtime contract then refused. Both are now equalities this script * checks rather than numbers someone has to remember to update. */ const INVENTORY_PATH = "docs/reviews/adapters/INVENTORY.md"; const GENERATOR_PATH = "scripts/generate-service-worker-assets.ts"; function trackedAdapterFiles(): readonly string[] { const listed = spawnSync("git", ["ls-files", "src/adapters"], { encoding: "utf8", }); if (listed.status !== 0) { throw new Error(`git ls-files failed: ${listed.stderr}`); } return listed.stdout.split("\n").filter(Boolean).sort(); } function inventoryRows(markdown: string): readonly string[] { const rows: string[] = []; for (const line of markdown.split("\n")) { const match = /^\|\s*\d+\s*\|\s*`([^`]+)`\s*\|/u.exec(line); if (match?.[1]) rows.push(match[1]); } return rows; } function reportDifference( label: string, expected: readonly string[], actual: readonly string[], ): readonly string[] { const missing = expected.filter((value) => !actual.includes(value)); const extra = actual.filter((value) => !expected.includes(value)); const problems: string[] = []; for (const value of missing) problems.push(`${label}: missing ${value}`); for (const value of extra) problems.push(`${label}: unexpected ${value}`); return problems; } async function main(): Promise { const problems: string[] = []; const tracked = trackedAdapterFiles(); const markdown = await readFile(INVENTORY_PATH, "utf8"); const listed = inventoryRows(markdown); problems.push(...reportDifference("adapter inventory", tracked, listed)); if (listed.length !== new Set(listed).size) { problems.push("adapter inventory: duplicate row"); } const total = /합계: \*\*(\d+)\/(\d+)\*\*/u.exec(markdown); if ( !total || Number(total[1]) !== tracked.length || Number(total[2]) !== tracked.length ) { problems.push( `adapter inventory: total does not equal ${tracked.length} tracked files`, ); } // SW-RR-03. The generator must read the shared table rather than declare one. const generator = await readFile(GENERATOR_PATH, "utf8"); if (!generator.includes("CACHEABLE_ASSET_CONTENT_TYPES")) { problems.push( "service worker assets: generator does not use the shared extension table", ); } if (/const CACHEABLE_EXTENSIONS[^=]*=\s*Object\.freeze\(\{/u.test(generator)) { problems.push( "service worker assets: generator declares its own extension table", ); } for (const [extension, contentType] of Object.entries( CACHEABLE_ASSET_CONTENT_TYPES, )) { if (!extension.startsWith(".") || contentType.length === 0) { problems.push(`service worker assets: invalid table row ${extension}`); } } // A fixture that links the repository's node_modules with a single directory // symlink is destructive: pnpm running inside that fixture purges the modules // directory it does not recognise, follows the link, and deletes the real // dependencies mid-run. `linkFixtureNodeModules` is the only sanctioned form. const sources = spawnSync( "git", ["grep", "-n", "-e", 'symlink(', "--", "scripts", "tests"], { encoding: "utf8" }, ); if (sources.status === 0) { for (const line of sources.stdout.split("\n").filter(Boolean)) { if (!line.includes("node_modules")) continue; if (line.startsWith("scripts/lib/fixture-node-modules.ts:")) continue; problems.push( `fixture node_modules: use linkFixtureNodeModules instead — ${line}`, ); } } const linkedFixtures = spawnSync( "git", ["grep", "-l", "linkFixtureNodeModules", "--", "scripts", "tests"], { encoding: "utf8" }, ); if ( linkedFixtures.status !== 0 || linkedFixtures.stdout.split("\n").filter(Boolean).length < 2 ) { problems.push( "fixture node_modules: the shared linker has no callers, so it is not the sanctioned path", ); } // TR-RR-05. The shared abort/deadline primitive is only shared if production // code imports it. A helper with zero importers is a second implementation // waiting to happen, which is exactly how the four hand-written copies of // these mechanics diverged in the first place. const primitiveImporters = spawnSync( "git", ["grep", "-l", "platform/abortable-operation.ts", "--", "src"], { encoding: "utf8" }, ); const importers = ( primitiveImporters.status === 0 ? primitiveImporters.stdout : "" ) .split("\n") .filter(Boolean) .filter((file) => !file.endsWith("platform/abortable-operation.ts")); if (importers.length === 0) { problems.push( "abortable-operation: the shared primitive has no production importers", ); } if (problems.length > 0) { for (const problem of problems) console.error(problem); process.exitCode = 1; return; } console.log( `Adapter inventory: ${tracked.length} files PASS; ` + `service worker asset table: ${ Object.keys(CACHEABLE_ASSET_CONTENT_TYPES).length } shared extensions PASS; ` + `fixture node_modules linking PASS`, ); } await main();