Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-production-docs.mjs
T

334 lines
12 KiB
JavaScript

#!/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);
}
}