#!/usr/bin/env node /* eslint-disable no-console */ import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(__dirname, ".."); export const reportPath = path.join(repoRoot, ".tc/baselines/readiness-docs/baseline-health.md"); const ignoredDirectoryNames = new Set([ ".git", ".cache", ".next", ".react-router", ".tc", ".turbo", ".vite", "build", "cache", "coverage", "dist", "generated", "node_modules", ]); const ignoredFileNames = new Set(["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]); export const readinessDocsReadinessInventoryScanRoots = [ "docs/porting-guide.md", "docs/porting-verification.md", "docs/production-checklist.md", "docs/feature-inventory.md", "docs/env-access-inventory.md", "docs/deployment.md", ]; export const readinessDocsReadinessInventoryPatterns = [ { id: "readiness-signal", pattern: /\b(?:readiness|ready|checklist|baseline|verification|verified|health|production-ready)\b/gi }, { id: "deployment-signal", pattern: /\b(?:deploy(?:ment)?|Docker|Coolify|Gitea|registry|workflow|CI|production|release)\b/gi }, { id: "environment-signal", pattern: /\b(?:env|environment|secret|DATABASE_URL|DIRECT_URL|REDIS_URL|BULLMQ_REDIS_URL|MAILERSEND_[A-Z0-9_]+|TOKEN|credential|access)\b/g, }, { id: "feature-plugin-signal", pattern: /\b(?:feature|manifest|plugin|plugin-candidate|core|remove-candidate|inventory|boundary|extraction)\b/gi }, { id: "risk-marker", pattern: /\b(?:TODO|TBD|FIXME|XXX|follow-up|manual|missing|required|risk|blocked)\b/gi }, ]; export const gates = [ { id: "typecheck", command: "pnpm", args: ["run", "typecheck"] }, { id: "lint", command: "pnpm", args: ["run", "lint"] }, { id: "vitest-readinessDocs-smoke", command: "pnpm", args: ["exec", "vitest", "run", "scripts/check-api-inventory.test.ts", "apps/web/app/application/featureManifests.test.ts"], }, { id: "readinessDocs-status-authority-guard", type: "readinessDocs-status-authority-guard" }, { id: "readinessDocs-doc-truth-guard", type: "readinessDocs-doc-truth-guard" }, { id: "readinessDocs-docs-readiness-inventory", type: "readinessDocs-docs-readiness-inventory" }, ]; function shellCommand(gate) { if (gate.type === "readinessDocs-docs-readiness-inventory") return "read-only Readiness Docs docs readiness inventory"; if (gate.type === "readinessDocs-status-authority-guard") return "static readiness authority marker guard"; if (gate.type === "readinessDocs-doc-truth-guard") return "static doc truth guard"; return [gate.command, ...gate.args].join(" "); } export const readinessDocsStatusAuthorityContracts = [ { path: "docs/improvement-plan/PROGRESS.md", requiredMarkers: [ "READINESS_AUTHORITY: non-authoritative historical tracker", "Do not use it to\n> decide deploy readiness, live environment state, or operator closeout.", "The\n> canonical readiness source is\n> `docs/improvement-plan/OPERATOR-CHECKLIST.md`", ], }, { path: "docs/improvement-plan/BLOCKERS.md", requiredMarkers: [ "READINESS_AUTHORITY: non-authoritative blocker register", "is not the deploy\n> readiness source of truth", "The canonical readiness source is\n> `docs/improvement-plan/OPERATOR-CHECKLIST.md`", ], }, { path: "docs/improvement-plan/OPERATOR-CHECKLIST.md", requiredMarkers: [ "READINESS_AUTHORITY: authoritative operator readiness checklist", "the canonical readiness source for operator-pending actions, live env flips,\n> and deploy/operator closeout", "PROGRESS.md` and unresolved-risk registers such as\n> `docs/improvement-plan/BLOCKERS.md` are supporting inputs only", ], }, { path: "docs/improvement-plan/CURRENT-STATUS.md", requiredMarkers: [ "READINESS_AUTHORITY: status-source map", "This file does not replace the\n> referenced runbooks", "Use this map before interpreting old `PASS`, `completed`, deploy, or nonstop\nagent notes under `docs/improvement-plan/`", "When a historical file and a canonical source disagree, the canonical source\nwins", ], }, ]; export const readinessDocsTruthContracts = [ { path: "docs/features/email-sequences.md", requiredMarkers: [ "foundation/admin-preview", "not an end-to-end production email-delivery claim", "MARKETING_SEQUENCES_ENABLED", "Keep it unset or `false` in production", "bundled template renderer", "`@toolctrl/mail` MailerSend adapter", "seed data", "delivery webhook production evidence", "production scheduled-job registration/deploy evidence", ], }, { path: "docs/feature-inventory.md", requiredMarkers: [ "signed MailerSend bounce/rejection ingress existieren", "provider delivery/open/click reconciliation evidence beyond the in-repo tracking routes", "MARKETING_SEQUENCES_ENABLED=false", ], forbiddenMarkers: ["Bounce/Provider-Webhooks", "bounce/provider webhooks"], }, { path: "docs/improvement-plan/05-business-features/README.md", requiredMarkers: [ "Signed MailerSend\nbounce/rejection ingress is shipped", "provider delivery/open/click\nreconciliation evidence beyond the in-repo tracking routes", "production scheduled-job registration/deploy evidence", ], forbiddenMarkers: ["Bounce/Provider-Webhooks", "bounce/provider webhooks"], }, { path: "docs/ops/worker-queues.md", requiredMarkers: [ "Email-sequence subscribers require `MARKETING_SEQUENCES_ENABLED=true`", "keep it unset or false in production until the remaining approval follow-ups", "docs/features/email-sequences.md", "Stale-lead/trial-expiring cron runner functions exist but are not production scheduler-registered yet", ], }, { path: "docs/ops/asset-cdn.md", requiredMarkers: [ "CDN provisioning and `ASSET_CDN_URL` flips remain parked", "A7/B2/#51", "Do not execute these steps while A7/B2/#51 remains parked", "Do not run these as active acceptance checks while CDN rollout remains parked", ], }, { path: "docs/improvement-plan/03-scaling-infrastructure/asset-cdn.md", requiredMarkers: [ "rollout remain backlogged/operator-deferred with A7/B2/#51", "This detail plan is historical implementation context", "Historical runbook only while #51 is backlogged", "These steps are currently parked with A7/B2", ], }, { path: "docs/improvement-plan/OPERATOR-CHECKLIST.md", requiredMarkers: [ "A7/#51 is backlogged, not an active CDN item", "B-008 is closed; B-012 is mitigated today", "B-012 is mitigated today, but the permanent fix is still operator-owned", "Parked CDN rows A7/B2/#51 are deliberate", ], }, { path: "docs/improvement-plan/BLOCKERS.md", requiredMarkers: [ "B-012 — Gitea-runner Docker `default-address-pool` recurrence risk", "mitigated-now-but-recurs-without-config-change", "Blocks:** no active deploy", ], forbiddenMarkers: ["B-008"], }, { path: "docs/deployment.md", requiredMarkers: ["B-012 is not a repository code blocker", "Docker network exhaustion is operator-owned host hardening"], }, ]; export const readinessDocsArchivedHandoffDirectory = "docs/improvement-plan/archive/handoffs"; export const readinessDocsArchivedHandoffHistoricalMarkers = [ "Historical snapshot only", "Historical/imported snapshot only", ]; export const readinessDocsArchivedHandoffRequiredMarkers = [ "Do not treat", "current work", "OPERATOR-CHECKLIST.md", ]; function outputFingerprint(output) { const normalized = output .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean) .slice(-40) .join("\n"); return { hash: createHash("sha256").update(output).digest("hex").slice(0, 12), excerpt: normalized.slice(0, 1200) || "(no output)" }; } function parseNumber(value) { const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) ? parsed : 0; } function conservativeFailureCount(count, output) { return count > 0 ? count : output.trim() ? 1 : 0; } function countVitestFailures(output) { const failedTests = output.match(/Tests\s+(\d+)\s+failed/i); if (failedTests) return parseNumber(failedTests[1]); const failedFiles = output.match(/Test Files\s+(\d+)\s+failed/i); return failedFiles ? parseNumber(failedFiles[1]) : 0; } export function failureCount(gateId, stdout, stderr, exitCode = 1) { if (exitCode === 0 || gateId === "readinessDocs-docs-readiness-inventory") return 0; const output = `${stdout}\n${stderr}`; if (gateId === "typecheck") return conservativeFailureCount((output.match(/error TS\d+/g) ?? []).length, output); if (gateId === "lint") { const problemsMatch = output.match(/✖\s+(\d+)\s+problems?\s+\((\d+)\s+errors?/i); const compactMatch = output.match(/(\d+)\s+errors?/i); return conservativeFailureCount(problemsMatch ? parseNumber(problemsMatch[2]) : compactMatch ? parseNumber(compactMatch[1]) : 0, output); } if (gateId === "vitest-readinessDocs-smoke") return conservativeFailureCount(countVitestFailures(output), output); return output.trim() ? 1 : 0; } function runShellGate(gate) { return new Promise((resolve) => { const startedAt = Date.now(); const child = spawn(gate.command, gate.args, { cwd: repoRoot, env: { ...process.env, CI: "1" }, shell: false }); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk) => { stdout += chunk; }); child.stderr.on("data", (chunk) => { stderr += chunk; }); child.on("error", (error) => { stderr += `\n${error.message}`; }); child.on("close", (code) => { const output = `${stdout}\n${stderr}`; resolve({ id: gate.id, command: shellCommand(gate), exitCode: code ?? 1, durationMs: Date.now() - startedAt, failureCount: failureCount(gate.id, stdout, stderr, code ?? 1), fingerprint: outputFingerprint(output), }); }); }); } function isIgnoredDirectory(name, relativePath) { return ignoredDirectoryNames.has(name) || ignoredDirectoryNames.has(relativePath) || name.endsWith(".cache"); } function isIgnoredFile(name) { return ignoredFileNames.has(name) || name.endsWith(".generated.ts") || name.endsWith(".generated.tsx") || name.endsWith(".min.js") || name.endsWith(".map"); } async function rootExists(root, rootRelativePath) { try { await stat(path.join(root, rootRelativePath)); return true; } catch (error) { if (error?.code === "ENOENT") return false; throw error; } } async function collectFiles(root, rootRelativePath) { const absoluteRoot = path.join(root, rootRelativePath); const rootStat = await stat(absoluteRoot); if (rootStat.isFile()) return [absoluteRoot]; const files = []; async function walk(directory) { const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { const absolutePath = path.join(directory, entry.name); const relativePath = path.relative(root, absolutePath).split(path.sep).join("/"); if (entry.isDirectory()) { if (isIgnoredDirectory(entry.name, relativePath)) continue; await walk(absolutePath); } else if (entry.isFile() && /\.(?:mdx?|json)$/.test(entry.name) && !isIgnoredFile(entry.name)) { files.push(absolutePath); } } } await walk(absoluteRoot); return files; } export async function runReadinessDocsReadinessInventory(root = repoRoot) { const existingRoots = []; const missingRoots = []; for (const rootRelativePath of readinessDocsReadinessInventoryScanRoots) { if (await rootExists(root, rootRelativePath)) existingRoots.push(rootRelativePath); else missingRoots.push(rootRelativePath); } const files = (await Promise.all(existingRoots.map((rootRelativePath) => collectFiles(root, rootRelativePath)))).flat(); const counts = new Map(readinessDocsReadinessInventoryPatterns.map(({ id }) => [id, 0])); const matches = []; for (const file of files) { const relativeFile = path.relative(root, file).split(path.sep).join("/"); const lines = (await readFile(file, "utf8")).split(/\r?\n/); lines.forEach((line, index) => { for (const { id, pattern } of readinessDocsReadinessInventoryPatterns) { pattern.lastIndex = 0; const found = line.match(pattern); if (!found) continue; counts.set(id, (counts.get(id) ?? 0) + found.length); matches.push(`${relativeFile}:${index + 1}: [${id}] ${line.trim()}`); } }); } return { roots: existingRoots, missingRoots, filesScanned: files.length, counts: Object.fromEntries(counts.entries()), totalMatches: matches.length, matches, }; } async function runReadinessDocsReadinessInventoryGate(gate) { const startedAt = Date.now(); const inventory = await runReadinessDocsReadinessInventory(repoRoot); const outputLines = [ `Readiness Docs docs readiness inventory scanned roots: ${inventory.roots.join(", ") || "(none)"}`, `Readiness Docs docs readiness inventory missing roots skipped: ${inventory.missingRoots.join(", ") || "(none)"}`, `files scanned: ${inventory.filesScanned}`, ...Object.entries(inventory.counts).map(([id, count]) => `${id}: ${count}`), `total matches: ${inventory.totalMatches}`, ...inventory.matches.slice(0, 240), ]; if (inventory.matches.length > 240) outputLines.push(`... ${inventory.matches.length - 240} additional matches omitted`); const output = outputLines.join("\n"); return { id: gate.id, command: shellCommand(gate), exitCode: 0, durationMs: Date.now() - startedAt, failureCount: 0, inventory, fingerprint: outputFingerprint(output), }; } export async function runReadinessDocsStatusAuthorityGuard(root = repoRoot) { const failures = []; const checked = []; for (const contract of readinessDocsStatusAuthorityContracts) { const absolutePath = path.join(root, contract.path); let contents = ""; try { contents = await readFile(absolutePath, "utf8"); } catch (error) { if (error?.code === "ENOENT") { failures.push(`${contract.path}: missing authority-controlled document`); continue; } throw error; } checked.push(contract.path); for (const marker of contract.requiredMarkers) { if (!contents.includes(marker)) failures.push(`${contract.path}: missing marker "${marker}"`); } } return { checked, failures }; } function hasAnyMarker(contents, markers) { return markers.some((marker) => contents.includes(marker)); } export async function runReadinessDocsDocTruthGuard(root = repoRoot) { const failures = []; const checked = []; for (const contract of readinessDocsTruthContracts) { const absolutePath = path.join(root, contract.path); let contents = ""; try { contents = await readFile(absolutePath, "utf8"); } catch (error) { if (error?.code === "ENOENT") { failures.push(`${contract.path}: missing doc-truth-controlled document`); continue; } throw error; } checked.push(contract.path); for (const marker of contract.requiredMarkers) { if (!contents.includes(marker)) failures.push(`${contract.path}: missing marker "${marker}"`); } for (const marker of contract.forbiddenMarkers ?? []) { if (contents.includes(marker)) failures.push(`${contract.path}: forbidden stale marker "${marker}"`); } } const handoffDirectory = path.join(root, readinessDocsArchivedHandoffDirectory); let handoffEntries = []; try { handoffEntries = await readdir(handoffDirectory, { withFileTypes: true }); } catch (error) { if (error?.code === "ENOENT") failures.push(`${readinessDocsArchivedHandoffDirectory}: missing archived handoff directory`); else throw error; } for (const entry of handoffEntries) { if (!entry.isFile() || !/^HANDOFF-.*\.md$/u.test(entry.name)) continue; const relativePath = path.posix.join(readinessDocsArchivedHandoffDirectory, entry.name); const contents = await readFile(path.join(handoffDirectory, entry.name), "utf8"); checked.push(relativePath); if (!hasAnyMarker(contents, readinessDocsArchivedHandoffHistoricalMarkers)) { failures.push(`${relativePath}: missing historical snapshot marker`); } for (const marker of readinessDocsArchivedHandoffRequiredMarkers) { if (!contents.includes(marker)) failures.push(`${relativePath}: missing archived-handoff marker "${marker}"`); } } return { checked, failures }; } async function runReadinessDocsStatusAuthorityGuardGate(gate) { const startedAt = Date.now(); const guard = await runReadinessDocsStatusAuthorityGuard(repoRoot); const output = [ `Readiness Docs status authority guard checked: ${guard.checked.join(", ") || "(none)"}`, `failures: ${guard.failures.length}`, ...guard.failures, ].join("\n"); return { id: gate.id, command: shellCommand(gate), exitCode: guard.failures.length > 0 ? 1 : 0, durationMs: Date.now() - startedAt, failureCount: guard.failures.length, authorityGuard: guard, fingerprint: outputFingerprint(output), }; } async function runReadinessDocsDocTruthGuardGate(gate) { const startedAt = Date.now(); const guard = await runReadinessDocsDocTruthGuard(repoRoot); const output = [ `Readiness Docs doc truth guard checked: ${guard.checked.join(", ") || "(none)"}`, `failures: ${guard.failures.length}`, ...guard.failures, ].join("\n"); return { id: gate.id, command: shellCommand(gate), exitCode: guard.failures.length > 0 ? 1 : 0, durationMs: Date.now() - startedAt, failureCount: guard.failures.length, docTruthGuard: guard, fingerprint: outputFingerprint(output), }; } async function runGate(gate) { if (gate.type === "readinessDocs-status-authority-guard") return runReadinessDocsStatusAuthorityGuardGate(gate); if (gate.type === "readinessDocs-doc-truth-guard") return runReadinessDocsDocTruthGuardGate(gate); if (gate.type === "readinessDocs-docs-readiness-inventory") return runReadinessDocsReadinessInventoryGate(gate); return runShellGate(gate); } function renderAuthorityGuard(result) { if (!result.authorityGuard) return []; return [ "- Authority Docs Checked: " + (result.authorityGuard.checked.join(", ") || "(none)"), `- Authority Guard Failures: ${result.authorityGuard.failures.length}`, ...result.authorityGuard.failures.map((failure) => ` - ${failure}`), ]; } function renderDocTruthGuard(result) { if (!result.docTruthGuard) return []; return [ "- Doc Truth Docs Checked: " + (result.docTruthGuard.checked.join(", ") || "(none)"), `- Doc Truth Guard Failures: ${result.docTruthGuard.failures.length}`, ...result.docTruthGuard.failures.map((failure) => ` - ${failure}`), ]; } function renderInventoryCounts(result) { if (!result.inventory) return []; return [ "- Inventory Roots: " + (result.inventory.roots.join(", ") || "(none)"), "- Missing Roots Skipped: " + (result.inventory.missingRoots.join(", ") || "(none)"), `- Files Scanned: ${result.inventory.filesScanned}`, "- Inventory Counts:", ...Object.entries(result.inventory.counts).map(([id, count]) => ` - ${id}: ${count}`), `- Total Inventory Matches: ${result.inventory.totalMatches}`, ]; } export function renderReport(results) { const lines = [ "# Readiness Docs Baseline Health", "", `Generated: ${new Date().toISOString()}`, "", "This report captures non-E2E baseline gates for Readiness Docs baseline. The runner exits non-zero when any shell gate fails; inventory gates remain read-only unless the runner itself fails.", "The shell gates run with CI=1 and fingerprint TypeScript, ESLint, and focused Vitest smoke coverage. The Readiness Docs docs readiness inventory gate is read-only and fingerprints readiness, deployment, environment, feature/plugin, and risk-marker terms across the configured docs while reporting missing optional documentation roots as skipped.", "", ]; for (const result of results) { lines.push( `## ${result.id}`, "", `- Command: \`${result.command}\``, `- Exitcode: ${result.exitCode}`, `- Failure Count: ${result.failureCount}`, `- Duration: ${result.durationMs}ms`, `- Output Fingerprint: ${result.fingerprint.hash}`, ...renderAuthorityGuard(result), ...renderDocTruthGuard(result), ...renderInventoryCounts(result), "", "```text", result.fingerprint.excerpt, "```", "" ); } return `${lines.join("\n")}\n`; } async function writeFailureReport(error) { await mkdir(path.dirname(reportPath), { recursive: true }); const output = error?.stack ?? String(error); await writeFile( reportPath, renderReport([ { id: "runner-error", command: "node scripts/verify-readiness-docs-baseline.mjs", exitCode: 1, durationMs: 0, failureCount: 1, fingerprint: outputFingerprint(output), }, ]), "utf8" ); } async function main() { const results = []; for (const gate of gates) { console.log(`Running ${gate.id}: ${shellCommand(gate)}`); results.push(await runGate(gate)); } await mkdir(path.dirname(reportPath), { recursive: true }); await writeFile(reportPath, renderReport(results), "utf8"); console.log(`Wrote ${path.relative(repoRoot, reportPath)}`); const failedResults = results.filter((result) => result.exitCode !== 0 || result.failureCount > 0); if (failedResults.length > 0) { console.error(`Baseline gates failed: ${failedResults.map((result) => result.id).join(", ")}`); process.exitCode = 1; } } if (process.argv[1] === fileURLToPath(import.meta.url)) { main().catch(async (error) => { console.error(error); try { await writeFailureReport(error); } finally { process.exit(1); } }); }