import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath, pathToFileURL } from 'node:url'; const defaultEvidencePath = path.resolve(process.cwd(), 'docs/refactor/production-launch-evidence.json'); const defaultMaxAgeDays = 14; const gateChecks = [ { id: 'readiness.production.env', label: 'Production environment readiness', commandIncludes: 'readiness:production', summary: { blocker: 0 }, }, { id: 'readiness.production.db', label: 'Production database readiness', commandIncludes: 'readiness:production:db', summary: { blocker: 0 }, }, { id: 'postgres.tuning-evidence', label: 'PostgreSQL 4c16g tuning evidence', commandIncludes: 'perf:postgres:evidence', summary: { status: 'pass', profile: { oneOf: ['shared-host', 'dedicated-db'] }, failures: 0, pgStatStatementsAvailable: true, pendingRestart: 0, }, }, { id: 'auth.remote-smoke', label: 'Remote Supabase Auth/JWKS smoke', commandIncludes: 'smoke:auth:remote', summary: { failed: 0, requireAdminTokens: true }, }, { id: 'rls.tenant-isolation', label: 'Runtime tenant RLS isolation', commandIncludes: 'test:rls', summary: { failed: 0 }, }, { id: 'migration.pb-production-dry-run', label: 'PocketBase production dry-run', commandIncludes: 'pb:import:dry-run', summary: { blocker: 0, warning: 0, migrationProfile: 'production', requiredCollectionsMissing: 0, criticalFieldCoverageWarnings: 0, }, }, { id: 'migration.pb-import-validate', label: 'PocketBase import validation', commandIncludes: 'pb:import:validate', summary: { fail: 0 }, }, { id: 'migration.pb-import-sample', label: 'PocketBase imported business sampling', commandIncludes: 'pb:import:sample', summary: { fail: 0 }, }, { id: 'performance.api-real-data-read', label: 'Real-data API read benchmark', commandIncludes: 'perf:api:local', summary: { errors: 0, errorRate: { lte: 0.001 }, p95Ms: { lte: 300 }, p99Ms: { lte: 800 }, concurrency: { gte: 30 }, durationSeconds: { gte: 120 }, includeWrites: false, }, }, { id: 'performance.api-real-data-mixed', label: 'Real-data API mixed read/write benchmark', commandIncludes: 'perf:api:local', summary: { errors: 0, errorRate: { lte: 0.001 }, p95Ms: { lte: 500 }, p99Ms: { lte: 1200 }, concurrency: { gte: 50 }, durationSeconds: { gte: 60 }, includeWrites: true, }, }, { id: 'api.integration', label: 'API integration regression', commandIncludes: 'test:api', summary: { failed: 0 }, }, { id: 'worker.assets', label: 'Asset worker security regression', commandIncludes: 'test:worker:assets', summary: { failed: 0 }, }, { id: 'worker.commerce', label: 'Commerce/payment worker regression', commandIncludes: 'test:worker:commerce', summary: { failed: 0 }, }, { id: 'worker.imports', label: 'Async import worker regression', commandIncludes: 'test:worker:imports', summary: { failed: 0 }, }, { id: 'worker.public-banks', label: 'Public question bank worker regression', commandIncludes: 'test:worker:public-banks', summary: { failed: 0 }, }, { id: 'taro.check', label: 'Taro type/security guardrails', commandIncludes: 'check:taro', summary: { failed: 0 }, }, { id: 'taro.build.student', label: 'Student H5 build', commandIncludes: 'build:taro:h5:student', summary: { failed: 0 }, }, { id: 'taro.build.tenant', label: 'Tenant admin H5 build', commandIncludes: 'build:taro:h5:tenant', summary: { failed: 0 }, }, { id: 'taro.build.platform', label: 'Platform admin H5 build', commandIncludes: 'build:taro:h5:platform', summary: { failed: 0 }, }, { id: 'taro.h5-static-smoke', label: 'Taro H5 static startup smoke', commandIncludes: 'smoke:taro:h5', summary: { fail: 0, portals: 3, tenantResolveRequests: 3, }, }, { id: 'taro.h5-interaction-smoke', label: 'Taro H5 real browser interaction smoke', commandIncludes: 'smoke:taro:h5:interaction', summary: { fail: 0, pass: { gte: 32 }, 'mockApi.keyRequests.answers': { gte: 1 }, 'mockApi.keyRequests.favorites': { gte: 1 }, 'mockApi.keyRequests.ordersCreated': { gte: 1 }, 'mockApi.keyRequests.paymentsCreated': { gte: 1 }, 'mockApi.keyRequests.orderStatus': { gte: 1 }, 'mockApi.keyRequests.tenantResolve': { gte: 3 }, 'mockApi.keyRequests.tenantContentWrites': { gte: 1 }, 'mockApi.keyRequests.tenantAdminWrites': { gte: 1 }, 'mockApi.keyRequests.crmWrites': { gte: 1 }, 'mockApi.keyRequests.commissionWrites': { gte: 1 }, 'mockApi.keyRequests.platformAdminWrites': { gte: 1 }, }, }, { id: 'taro.h5-release-guardrails', label: 'Taro H5 release artifact guardrails', commandIncludes: 'taro-h5-release-guardrails-test.js', summary: { fail: 0, warn: 0, }, }, { id: 'taro.h5-release-manifest', label: 'Taro H5 release deployment manifest', commandIncludes: 'manifest:taro:h5', summary: { fail: 0, warn: 0, portals: 3, distReady: 3, runtimeConfigs: 3, }, }, { id: 'audit.runtime', label: 'Runtime dependency audit', commandIncludes: 'audit:runtime', summary: { critical: 0, high: 0 }, }, { id: 'security.repo-scan', label: 'Repository static security scan', commandIncludes: 'security:repo', summary: { critical: 0, high: 0 }, }, { id: 'security.codex-scan', label: 'Codex Security scan', commandIncludes: 'codex-security', summary: { critical: 0, high: 0 }, }, ]; const requiredAttestations = [ { id: 'backup.snapshot', label: 'Production database backup/snapshot is prepared', }, { id: 'rollback.plan', label: 'Rollback plan and old PocketBase read-only snapshot are prepared', }, { id: 'migration.sampling', label: 'Real migrated users/questions/orders/assets were sampled', }, { id: 'provider.production-accounts', label: 'SMS/OAuth/payment production accounts and callback domains were verified', }, { id: 'object-storage.production-controls', label: 'Object storage AV/content scan, CDN boundary, watermark and lifecycle controls were verified', }, { id: 'payment.reconciliation-sampling', label: 'Real payment/refund bill samples were reconciled', }, { id: 'frontend.runtime-config-review', label: 'Three H5 runtime-config.json files were reviewed for public-only values', }, ]; function parseArgs(argv) { const options = { evidencePath: defaultEvidencePath, json: false, maxAgeDays: defaultMaxAgeDays, allowStale: false, }; for (let index = 2; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--json') options.json = true; else if (arg === '--allow-stale') options.allowStale = true; else if (arg === '--evidence') { options.evidencePath = path.resolve(process.cwd(), argv[index + 1] || ''); index += 1; } else if (arg.startsWith('--evidence=')) { options.evidencePath = path.resolve(process.cwd(), arg.slice('--evidence='.length)); } else if (arg === '--max-age-days') { options.maxAgeDays = Number(argv[index + 1]); index += 1; } else if (arg.startsWith('--max-age-days=')) { options.maxAgeDays = Number(arg.slice('--max-age-days='.length)); } } if (!Number.isFinite(options.maxAgeDays) || options.maxAgeDays <= 0) options.maxAgeDays = defaultMaxAgeDays; return options; } function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function normalizeStatus(value) { return String(value || '').trim().toLowerCase(); } function isApproved(value) { return ['approved', 'pass', 'passed', 'ok', 'complete', 'completed'].includes(normalizeStatus(value)); } function findById(items, id) { return (Array.isArray(items) ? items : []).find(item => item && item.id === id); } function resolveArtifact(evidencePath, artifact) { if (!artifact) return ''; if (path.isAbsolute(artifact)) return artifact; return path.resolve(path.dirname(evidencePath), artifact); } function daysSince(value, now = Date.now()) { const timestamp = Date.parse(value); if (!Number.isFinite(timestamp)) return Number.POSITIVE_INFINITY; return (now - timestamp) / (24 * 60 * 60 * 1000); } function hasPath(object, keyPath) { const parts = keyPath.split('.'); let cursor = object; for (const part of parts) { if (!cursor || typeof cursor !== 'object' || !(part in cursor)) return false; cursor = cursor[part]; } return true; } function valueAt(object, keyPath) { const parts = keyPath.split('.'); let cursor = object; for (const part of parts) cursor = cursor?.[part]; return cursor; } function compareSummary(actualSummary, expectedSummary) { const failures = []; for (const [key, expectedValue] of Object.entries(expectedSummary || {})) { if (!hasPath(actualSummary || {}, key)) { failures.push(`${key} is missing`); continue; } const actualValue = valueAt(actualSummary, key); const comparisonFailure = compareExpectedValue(actualValue, expectedValue); if (comparisonFailure) { failures.push(`${key} ${comparisonFailure}`); } } return failures; } function isComparatorSpec(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) return false; return ['eq', 'lt', 'lte', 'gt', 'gte', 'oneOf'].some(key => Object.prototype.hasOwnProperty.call(value, key)); } function compareExpectedValue(actualValue, expectedValue) { if (!isComparatorSpec(expectedValue)) { if (actualValue !== expectedValue) { return `expected ${JSON.stringify(expectedValue)} but got ${JSON.stringify(actualValue)}`; } return ''; } if (Object.prototype.hasOwnProperty.call(expectedValue, 'eq') && actualValue !== expectedValue.eq) { return `expected ${JSON.stringify(expectedValue.eq)} but got ${JSON.stringify(actualValue)}`; } if (Object.prototype.hasOwnProperty.call(expectedValue, 'oneOf')) { const choices = Array.isArray(expectedValue.oneOf) ? expectedValue.oneOf : []; if (!choices.includes(actualValue)) { return `expected one of ${JSON.stringify(choices)} but got ${JSON.stringify(actualValue)}`; } } const numericChecks = [ ['lt', (actual, expected) => actual < expected, '<'], ['lte', (actual, expected) => actual <= expected, '<='], ['gt', (actual, expected) => actual > expected, '>'], ['gte', (actual, expected) => actual >= expected, '>='], ]; for (const [key, predicate, label] of numericChecks) { if (!Object.prototype.hasOwnProperty.call(expectedValue, key)) continue; const actualNumber = Number(actualValue); const expectedNumber = Number(expectedValue[key]); if (!Number.isFinite(actualNumber) || !Number.isFinite(expectedNumber) || !predicate(actualNumber, expectedNumber)) { return `expected ${label} ${expectedNumber} but got ${JSON.stringify(actualValue)}`; } } return ''; } function resultCollector() { const checks = []; function pass(id, message, details = {}) { checks.push({ status: 'pass', id, message, details }); } function warn(id, message, details = {}) { checks.push({ status: 'warn', id, message, details }); } function block(id, message, details = {}) { checks.push({ status: 'blocker', id, message, details }); } return { checks, pass, warn, block }; } function validateTopLevel(evidence, collector) { if (evidence.schemaVersion !== 1) { collector.block('evidence.schema_version', 'Evidence schemaVersion must be 1', { actual: evidence.schemaVersion }); } else { collector.pass('evidence.schema_version', 'Evidence schema version is supported'); } if (evidence.environment !== 'production') { collector.block('evidence.environment', 'Evidence environment must be production', { actual: evidence.environment }); } else { collector.pass('evidence.environment', 'Evidence environment is production'); } if (!evidence.commit || !/^[0-9a-f]{7,40}$/i.test(String(evidence.commit))) { collector.block('evidence.commit', 'Evidence must include the reviewed deployment commit hash'); } else { collector.pass('evidence.commit', 'Deployment commit is recorded', { commit: evidence.commit }); } const target = evidence.target || {}; const requiredTargets = ['apiBaseUrl', 'studentH5Url', 'tenantAdminH5Url', 'platformAdminH5Url']; for (const key of requiredTargets) { const value = String(target[key] || ''); if (!value.startsWith('https://')) { collector.block(`target.${key}`, `${key} must be an HTTPS production URL`, { value }); } else { collector.pass(`target.${key}`, `${key} is HTTPS`); } } } function validateGateCheck(spec, evidence, options, collector) { const item = findById(evidence.checks, spec.id); if (!item) { collector.block(`check.${spec.id}`, `${spec.label} evidence is missing`); return; } if (normalizeStatus(item.status) !== 'pass') { collector.block(`check.${spec.id}.status`, `${spec.label} must have status=pass`, { status: item.status }); } else { collector.pass(`check.${spec.id}.status`, `${spec.label} passed`); } if (spec.commandIncludes && !String(item.command || '').includes(spec.commandIncludes)) { collector.block(`check.${spec.id}.command`, `${spec.label} command must include ${spec.commandIncludes}`, { command: item.command || '', }); } else { collector.pass(`check.${spec.id}.command`, `${spec.label} command is recorded`); } const age = daysSince(item.completedAt); if (!options.allowStale && age > options.maxAgeDays) { collector.block(`check.${spec.id}.freshness`, `${spec.label} evidence is stale or missing completedAt`, { completedAt: item.completedAt || '', maxAgeDays: options.maxAgeDays, }); } else { collector.pass(`check.${spec.id}.freshness`, `${spec.label} evidence is fresh enough`, { completedAt: item.completedAt || '', }); } if (!item.artifact) { collector.block(`check.${spec.id}.artifact`, `${spec.label} must include a saved artifact/log path`); } else { const artifactPath = resolveArtifact(options.evidencePath, item.artifact); if (!fs.existsSync(artifactPath)) { collector.block(`check.${spec.id}.artifact`, `${spec.label} artifact file is missing`, { artifact: item.artifact }); } else { collector.pass(`check.${spec.id}.artifact`, `${spec.label} artifact exists`, { artifact: item.artifact }); } } const summaryFailures = compareSummary(item.summary || {}, spec.summary); if (summaryFailures.length > 0) { collector.block(`check.${spec.id}.summary`, `${spec.label} summary does not satisfy gate`, { failures: summaryFailures }); } else { collector.pass(`check.${spec.id}.summary`, `${spec.label} summary satisfies gate`); } } function validateAttestation(spec, evidence, options, collector) { const item = findById(evidence.attestations, spec.id); if (!item) { collector.block(`attestation.${spec.id}`, `${spec.label} attestation is missing`); return; } if (!isApproved(item.status)) { collector.block(`attestation.${spec.id}.status`, `${spec.label} must be approved`, { status: item.status }); } else { collector.pass(`attestation.${spec.id}.status`, `${spec.label} is approved`); } if (!String(item.approver || '').trim()) { collector.block(`attestation.${spec.id}.approver`, `${spec.label} must include an approver`); } else { collector.pass(`attestation.${spec.id}.approver`, `${spec.label} has an approver`); } const age = daysSince(item.approvedAt); if (!options.allowStale && age > options.maxAgeDays) { collector.block(`attestation.${spec.id}.freshness`, `${spec.label} approval is stale or missing approvedAt`, { approvedAt: item.approvedAt || '', maxAgeDays: options.maxAgeDays, }); } else { collector.pass(`attestation.${spec.id}.freshness`, `${spec.label} approval is fresh enough`, { approvedAt: item.approvedAt || '', }); } } function validateEvidence(evidence, options) { const collector = resultCollector(); validateTopLevel(evidence, collector); for (const spec of gateChecks) validateGateCheck(spec, evidence, options, collector); for (const spec of requiredAttestations) validateAttestation(spec, evidence, options, collector); return collector.checks; } function summarize(checks) { return checks.reduce( (summary, item) => { summary[item.status] += 1; return summary; }, { blocker: 0, warn: 0, pass: 0 }, ); } function printHuman(options, evidence, checks) { const summary = summarize(checks); console.log('Production launch gate'); console.log(`Evidence: ${options.evidencePath}`); console.log(`Host: ${os.hostname()}`); console.log(`Target API: ${evidence.target?.apiBaseUrl || '(missing)'}`); console.log(`Summary: ${summary.blocker} blocker(s), ${summary.warn} warning(s), ${summary.pass} pass(es)`); for (const item of checks) { const marker = item.status === 'blocker' ? 'BLOCK' : item.status === 'warn' ? 'WARN ' : 'PASS '; console.log(`[${marker}] ${item.id}: ${item.message}`); } } function main() { const options = parseArgs(process.argv); if (!fs.existsSync(options.evidencePath)) { const message = `Evidence file not found: ${options.evidencePath}`; if (options.json) console.log(JSON.stringify({ summary: { blocker: 1, warn: 0, pass: 0 }, checks: [{ status: 'blocker', id: 'evidence.file', message }] }, null, 2)); else console.error(`${message}\nCreate it from docs/refactor/production-launch-evidence.template.json after running real production checks.`); process.exitCode = 1; return; } const evidence = readJson(options.evidencePath); const checks = validateEvidence(evidence, options); const summary = summarize(checks); if (options.json) console.log(JSON.stringify({ summary, checks }, null, 2)); else printHuman(options, evidence, checks); if (summary.blocker > 0) process.exitCode = 1; } const currentFile = fileURLToPath(import.meta.url); if (process.argv[1] && fileURLToPath(pathToFileURL(process.argv[1])) === currentFile) { main(); } export { gateChecks, requiredAttestations, validateEvidence };