#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; export const REQUIRED_DOCS = [ "README.md", "docs/quickstart.md", "docs/new-project.md", "docs/local-dev.md", "docs/env-vars.md", "docs/database.md", "docs/mail.md", "docs/worker.md", "docs/api.md", "docs/deployment.md", "docs/deployments/boiler-saas-ctrl-com.md", "docs/security.md", "docs/observability.md", "docs/plugins.md", "docs/upgrades.md", "docs/troubleshooting.md", ]; export const REQUIRED_TERM_GROUPS = [ { id: "pinned-pnpm", description: "Corepack and the pinned pnpm version", terms: [/\bcorepack\b/i, /\bpnpm@10\.33\.0\b/], }, { id: "local-env", description: "local .env setup without production secrets", terms: [/\.env\b/i, /\bplaceholder/i, /production secrets?/i], }, { id: "postgres-prisma", description: "PostgreSQL and Prisma database setup", terms: [/\bDATABASE_URL\b/, /\bPostgreSQL\b|\bPostgres\b/i, /\bPrisma\b/i], }, { id: "seed-web-runtime", description: "seed command and local web runtime", terms: [/\bpnpm seed\b/, /\bpnpm dev\b/, /localhost:3000/i], }, { id: "optional-worker-redis", description: "optional worker Redis boundary", terms: [/\bWORKER_ENABLED\b/, /\bBULLMQ_REDIS_URL\b/, /\bRedis\b/i], }, { id: "manual-e2e-boundary", description: "manual browser/E2E verification boundary", terms: [/\bmanual(?:-only)?\b/i, /\bE2E\b|\bPlaywright\b|\bCypress\b|\bbrowser\b/i, /explicitly approved|explicit approval/i], }, { id: "docs-entrypoints", description: "README and onboarding links to docs entrypoints", terms: [/docs\/quickstart\.md/, /docs\/new-project\.md/, /docs\/local-dev\.md/], }, { id: "verification-troubleshooting", description: "focused non-E2E verification and troubleshooting guidance", terms: [/\bnon-E2E\b/i, /\bverification\b/i, /\btroubleshooting\b/i], }, { id: "s05-plugin-candidates", description: "plugin candidates documented as extraction contracts, not packaged plugins", terms: [/\bplugin-candidate\b|\bplugin candidates?\b/i, /\bextraction contracts?\b/i, /not (?:an )?(?:already-)?packaged plugin|not already-packaged plugins?/i], }, { id: "s05-upgrade-guidance", description: "upgrade runbook covers pinned pnpm, database/runtime/provider upgrades, and non-E2E checks", terms: [/\bupgrade(?:s|d)?\b/i, /\bpnpm@10\.33\.0\b/, /\bPrisma\b|\bdatabase\b/i, /\bnon-E2E\b/i], }, { id: "s05-troubleshooting-runbook", description: "production troubleshooting runbook covers safe evidence collection and redaction", terms: [/\btroubleshooting runbook\b/i, /\btriage\b/i, /\bsafe evidence\b/i, /\bredact(?:ed|ion)?\b|\braw secrets?\b/i], }, { id: "s05-docs-link-check-ci", description: "documentation names the local docs link checker and CI-safe non-E2E gate", terms: [/\bverify:docs-links\b|\bdocs link check\b/i, /\bCI\b/i, /\bnon-E2E\b/i], }, { id: "s03-env-secrets-redaction", description: "environment secret key handling and redacted diagnostics", terms: [/\benv(?:ironment)?\b|\.env\b/i, /\bsecret(?:s)?\b|\bAPI_KEY\b|\bTOKEN\b/i, /\bredact(?:ed|ion)?\b|\bmask(?:ed|ing)?\b/i], }, { id: "s03-database-operations", description: "database migration, status, and backup operations", terms: [/\bdatabase\b|\bPostgreSQL\b|\bPostgres\b/i, /\bmigrat(?:e|ion|ions)\b|\bdeploy\b/i, /\bstatus\b|\bhealth\b/i, /\bbackup(?:s)?\b|\brestore\b/i], }, { id: "s03-mailersend-boundaries", description: "MailerSend outbound mail and webhook signing boundaries", terms: [/\bMailerSend\b/i, /\boutbound\b|\bemail\b|\bmail\b/i, /\bwebhook(?:s)?\b/i, /\bsign(?:ing|ature|ed)\b/i], }, { id: "s03-worker-runtime", description: "worker enablement, Redis, queue health, and runtime boundary", terms: [/\bWORKER_ENABLED\b|\benable(?:d|ment)?\b/i, /\bRedis\b|\bBULLMQ_REDIS_URL\b/i, /\bqueue\b|\bjob(?:s)?\b/i, /\bhealth\b|\bdiagnostic(?:s)?\b/i], }, { id: "s03-production-troubleshooting-redacted", description: "production troubleshooting with redacted output and no raw secrets", terms: [/\bproduction\b/i, /\btroubleshoot(?:ing)?\b|\bfailure(?:s)?\b|\bdiagnostic(?:s)?\b/i, /\bredact(?:ed|ion)?\b|\bmask(?:ed|ing)?\b/i, /\braw secret(?:s)?\b|\bsecret value(?:s)?\b/i], }, { id: "s04-api-auth-openapi-request-id", description: "API authentication, OpenAPI surface, and request ID correlation", terms: [/\bAPI\b/i, /\bauth(?:entication|orization)?\b|\bBearer\b|\bAPI key\b/i, /\bOpenAPI\b|\bswagger\b/i, /\brequest[- ]id\b|\bX-Request-ID\b|\bcorrelation id\b/i], }, { id: "s04-deployment-image-coolify-runtime", description: "deployment image, Coolify, and manual runtime boundaries", terms: [/\bdeploy(?:ment)?\b|\brelease\b/i, /\bimage\b|\bcontainer\b|\bDocker\b/i, /\bCoolify\b/i, /\bmanual(?:-only)?\b/i, /\bruntime\b|\bstart command\b|\bservice boundary\b/i], }, { id: "s04-separate-worker-groups", description: "separate Coolify worker applications and the permanent mail worker group", terms: [ /\bworker-general\b/i, /\bworker-mail\b/i, /\bworker-plugin\b/i, /\bWORKER_QUEUE_GROUP=mail\b/, /docs\/ops\/worker-queues\.md/i, ], }, { id: "s04-security-headers-cors-secrets", description: "security headers, CORS, and secret handling", terms: [/\bsecurity\b/i, /\bheaders?\b|\bCSP\b|\bHSTS\b/i, /\bCORS\b|\ballowed origins?\b/i, /\bsecret(?:s)?\b|\bTOKEN\b|\bAPI_KEY\b/i, /\bredact(?:ed|ion)?\b|\bmask(?:ed|ing)?\b/i], }, { id: "s04-observability-diagnostics-health-queue-triage", description: "redacted diagnostics, health/readiness, queue inspection, and failure triage", terms: [/\bobservability\b|\bdiagnostic(?:s)?\b/i, /\bredact(?:ed|ion)?\b|\bmask(?:ed|ing)?\b/i, /\bhealth\b|\breadiness\b|\bready\b/i, /\bqueue\b|\bjob(?:s)?\b/i, /\bfailure triage\b|\btriage\b|\bincident\b/i], }, ]; export const FORBIDDEN_PLACEHOLDER_PATTERN = /\b(?:TODO|TBD|FIXME)\b/; export const FORBIDDEN_AUTOMATIC_E2E_PATTERNS = [ { id: "playwright-test-command", pattern: /(?:^|[`\n>\s])(?:pnpm\s+(?:exec\s+)?|npx\s+)?playwright\s+test\b/i, }, { id: "cypress-command", pattern: /(?:^|[`\n>\s])(?:pnpm\s+(?:exec\s+)?|npx\s+)?cypress\s+(?:run|open)\b/i, }, { id: "browser-e2e-script-command", pattern: /(?:^|[`\n>\s])(?:pnpm|npm|yarn)\s+(?:run\s+)?(?:test:e2e|e2e|browser)\b/i, }, ]; export const FORBIDDEN_LIVE_STATUS_DRIFT_PATTERNS = [ { id: "superseded-persistent-mail-worker-gap", pattern: /Remaining gap:\s*persistent Coolify mail-worker service/i, message: "superseded B-014 mail-worker blocker wording must not appear in live production docs", }, { id: "plugin-production-enabled-before-signoff", pattern: /third-party plugin execution (?:is|as) production-enabled/i, message: "third-party plugin execution must stay gated by external WASM signoff", }, ]; function normalizeRelativePath(relativePath) { return relativePath.split(path.sep).join("/"); } function lineAndColumnForIndex(content, index) { const prefix = content.slice(0, index); const lines = prefix.split(/\r?\n/); return { line: lines.length, column: lines.at(-1).length + 1 }; } function readDocs(rootDir, requiredDocs = REQUIRED_DOCS) { const docs = new Map(); const failures = []; for (const relativePath of requiredDocs) { const absolutePath = path.join(rootDir, relativePath); if (!fs.existsSync(absolutePath)) { failures.push(`${relativePath}: missing required documentation file`); continue; } const content = fs.readFileSync(absolutePath, "utf8"); if (content.trim().length === 0) { failures.push(`${relativePath}: documentation file is empty`); continue; } docs.set(normalizeRelativePath(relativePath), content); } return { docs, failures }; } function checkForbiddenPlaceholders(docs) { const failures = []; for (const [relativePath, content] of docs.entries()) { const match = FORBIDDEN_PLACEHOLDER_PATTERN.exec(content); if (!match) continue; const { line, column } = lineAndColumnForIndex(content, match.index); failures.push(`${relativePath}:${line}:${column}: forbidden placeholder marker "${match[0]}"`); } return failures; } function checkForbiddenAutomaticE2e(docs) { const failures = []; for (const [relativePath, content] of docs.entries()) { for (const { id, pattern } of FORBIDDEN_AUTOMATIC_E2E_PATTERNS) { const match = pattern.exec(content); if (!match) continue; const { line, column } = lineAndColumnForIndex(content, match.index + match[0].search(/\S/)); failures.push(`${relativePath}:${line}:${column}: forbidden automatic browser/E2E command (${id})`); } } return failures; } function checkForbiddenLiveStatusDrift(docs) { const failures = []; for (const [relativePath, content] of docs.entries()) { for (const { id, pattern, message } of FORBIDDEN_LIVE_STATUS_DRIFT_PATTERNS) { const match = pattern.exec(content); if (!match) continue; const { line, column } = lineAndColumnForIndex(content, match.index); failures.push(`${relativePath}:${line}:${column}: ${message} (${id})`); } } return failures; } function checkRequiredTermGroups(docs, termGroups = REQUIRED_TERM_GROUPS) { const failures = []; const combined = [...docs.entries()].map(([relativePath, content]) => `\n--- ${relativePath} ---\n${content}`).join("\n"); for (const group of termGroups) { const missingTerms = group.terms.filter((term) => !term.test(combined)); if (missingTerms.length > 0) { failures.push(`required term group "${group.id}" missing ${missingTerms.length} term(s): ${group.description}`); } } return failures; } function checkDeploymentCommandSlotDocs(docs) { const failures = []; const requiredSlotDocs = ["docs/deployment.md", "docs/deployments/boiler-saas-ctrl-com.md"]; for (const relativePath of requiredSlotDocs) { const content = docs.get(relativePath); if (!content) continue; if (!/\bpre_deployment_command\b/.test(content) || !/\bprisma\s+migrate\s+deploy\b/.test(content)) { failures.push(`${relativePath}: must document Coolify pre_deployment_command as the Prisma migration slot`); } if (!/\bpost_deployment_command\b/.test(content) || !/production-seed\.server\.js/.test(content)) { failures.push(`${relativePath}: must document Coolify post_deployment_command as the guarded seed slot`); } if (/Pre-deploy command:[^\n]*(?:&&|then)[^\n]*production-seed\.server\.js/i.test(content)) { failures.push(`${relativePath}: must not describe the guarded seed as combined into the pre-deploy command`); } } return failures; } export function verifyProductionDocsDocs(rootDir = process.cwd(), options = {}) { const requiredDocs = options.requiredDocs ?? REQUIRED_DOCS; const termGroups = options.termGroups ?? REQUIRED_TERM_GROUPS; const { docs, failures: readFailures } = readDocs(rootDir, requiredDocs); const failures = [ ...readFailures, ...checkForbiddenPlaceholders(docs), ...checkForbiddenAutomaticE2e(docs), ...checkForbiddenLiveStatusDrift(docs), ...checkRequiredTermGroups(docs, termGroups), ...checkDeploymentCommandSlotDocs(docs), ]; return { passes: failures.length === 0 ? [`${requiredDocs.length} required documentation files satisfy the Production Docs production documentation contract`] : [], failures, observations: [`checked files: ${requiredDocs.join(", ")}`], }; } export function formatProductionDocsDocsResult(result) { const status = result.failures.length > 0 ? "FAIL" : "PASS"; return [ `${status} Production Docs production documentation verification`, ...result.passes.map((pass) => `PASS ${pass}`), ...result.observations.map((observation) => `OBSERVE ${observation}`), ...result.failures.map((failure) => `FAIL ${failure}`), `Summary: ${result.passes.length} passed, ${result.observations.length} observed, ${result.failures.length} failed`, "", ].join("\n"); } const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]); if (isCli) { const result = verifyProductionDocsDocs(process.cwd()); const output = formatProductionDocsDocsResult(result); if (result.failures.length > 0) { process.stderr.write(output); process.exitCode = 1; } else { process.stdout.write(output); } }