#!/usr/bin/env node /** * Drift guard: the site's generated catalog MUST match the API source. * * Run in CI / pre-deploy. Regenerates the catalog into memory from * api/src/service-catalog.ts and compares it to the committed * site/src/lib/service-catalog.generated.ts. Fails (exit 1) on any difference, * so a price edited in the API but not regenerated, or the generated file * hand-edited, is caught before it reaches customers. * * Usage: node scripts/check-service-catalog-drift.mjs */ import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = resolve(__dirname, ".."); const GEN = resolve(ROOT, "site/src/lib/service-catalog.generated.ts"); // Regenerate to a temp string by invoking the generator's parse logic inline. const SRC = resolve(ROOT, "api/src/service-catalog.ts"); const tsSource = readFileSync(SRC, "utf8"); const m = tsSource.match(/export const COMPLIANCE_SERVICES[^=]*=\s*(\{[\s\S]*?\n\});/); if (!m) { console.error("drift-check: cannot parse API catalog"); process.exit(1); } const apiCatalog = Function(`"use strict"; return (${m[1]});`)(); // Parse the committed generated file's SERVICE_META. const genSource = readFileSync(GEN, "utf8"); const gm = genSource.match(/export const SERVICE_META[^=]*=\s*(\{[\s\S]*?\n\});/); if (!gm) { console.error("drift-check: cannot parse generated SERVICE_META"); process.exit(1); } const gen = Function(`"use strict"; return (${gm[1]});`)(); let problems = []; for (const slug of Object.keys(apiCatalog)) { const a = apiCatalog[slug]; const g = gen[slug]; if (!g) { problems.push(`${slug}: missing from generated file`); continue; } if (a.price_cents !== g.price_cents) problems.push(`${slug}: price API=${a.price_cents} generated=${g.price_cents}`); if (a.name !== g.name) problems.push(`${slug}: name mismatch`); if ((a.gov_fee_label || undefined) !== (g.gov_fee_label || undefined)) problems.push(`${slug}: gov_fee_label mismatch`); } for (const slug of Object.keys(gen)) { if (!apiCatalog[slug]) problems.push(`${slug}: in generated file but not in API`); } if (problems.length) { console.error("SERVICE CATALOG DRIFT DETECTED (run: node scripts/gen-service-catalog.mjs):"); for (const p of problems) console.error(" - " + p); process.exit(1); } console.log(`drift-check: OK -- ${Object.keys(apiCatalog).length} services, API and generated catalog match.`);