import { readFile } from "node:fs/promises"; import { readFileSync } from "node:fs"; import path from "node:path"; 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 / GOV-04. Every consumer the re-review named must use the shared // primitive, not merely one file somewhere. Checking `importers.length > 0` // let an unrelated production import satisfy the gate while Image and // Resumable kept their own diverging copies of the same mechanics — which is // exactly how the four hand-written versions drifted apart in the first place. const REQUIRED_ABORT_CONSUMERS: readonly string[] = [ "src/adapters/browser-transfer/presigned/presigned-capability-http-provider.ts", "src/adapters/browser-transfer/presigned/presigned-transfer-executor.ts", "src/adapters/browser-transfer/image-cdn/browser-image-probe.ts", "src/adapters/browser-transfer/resumable-upload/fetch-json-transport.ts", ]; 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")) .sort(); const importerSet = new Set(importers); const missingConsumers = REQUIRED_ABORT_CONSUMERS.filter( (consumer) => !importerSet.has(consumer), ); if (missingConsumers.length > 0) { problems.push( `abortable-operation: required consumers do not import the shared primitive: ${missingConsumers.join( ", ", )}`, ); } // The importer must reach the primitive by a specifier that resolves to the // primitive itself, so a same-named local helper cannot satisfy the gate. const PRIMITIVE_PATH = path.resolve( "src/adapters/platform/abortable-operation.ts", ); for (const consumer of REQUIRED_ABORT_CONSUMERS) { if (!importerSet.has(consumer)) continue; const source = readFileSync(consumer, "utf8"); const specifiers = [ ...source.matchAll(/from\s+"([^"]*platform\/abortable-operation\.ts)"/gu), ].map((match) => match[1] ?? ""); const resolved = specifiers.some( (specifier) => path.resolve(path.dirname(consumer), specifier) === PRIMITIVE_PATH, ); if (!resolved) { problems.push( `abortable-operation: ${consumer} does not resolve its import to the shared primitive`, ); } } if (problems.length > 0) { for (const problem of problems) console.error(problem); process.exitCode = 1; return; } // GOV-04. The exact importer set is part of the receipt, so a reviewer can // see which consumers the gate actually verified rather than a bare count. 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; ` + `shared abort primitive: ${importers.length} importers ` + `(${importers.join(", ")}) PASS`, ); } await main();