import { createHash } from "node:crypto"; import { mkdir, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs"; import { buildM3IoControlSourceReport, runM3IoControlLiveReport } from "./m3-io-control-e2e.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const defaultReportPath = "reports/dev-gate/dev-runtime-postflight-report.json"; const issue = "pikasTech/HWLAB#311"; const defaultHealthUrl = `${DEV_ENDPOINT}/health/live`; const defaultV1Url = `${DEV_ENDPOINT}/v1`; export async function runDevRuntimePostflightCli(argv = process.argv.slice(2), options = {}) { const args = parseArgs(argv); if (args.help) { process.stdout.write(`${usage()}\n`); return 0; } const report = await buildDevRuntimePostflightReport(args, options); if (args.writeReport) { await writeReport(report, args.reportPath, options.repoRoot ?? repoRoot); } const printable = args.pretty || args.writeReport ? report : summarizeReport(report); process.stdout.write(`${JSON.stringify(printable, null, 2)}\n`); return report.conclusion.status === "pass" || !args.failOnBlocked ? 0 : 1; } export async function buildDevRuntimePostflightReport(args = {}, options = {}) { const parsed = normalizeArgs(args); const now = options.now ?? (() => new Date().toISOString()); const report = { issue, mode: parsed.live ? "dev-live" : "check", generatedAt: now(), target: { environment: ENVIRONMENT_DEV, namespace: "hwlab-dev", prodAllowed: false, apiHealthUrl: defaultHealthUrl, apiV1Url: defaultV1Url, m3Target: parsed.target }, actions: { healthLiveReadAttempted: false, v1ReadAttempted: false, m3MutationAttempted: false, m3MutationAllowed: false }, apiHealth: null, apiV1: null, m3: null, blockers: [], safety: buildSafety(parsed), safetyRefusal: false }; if (!parsed.live) { const sourceReport = await (options.m3SourceBuilder ?? buildM3IoControlSourceReport)({ repoRoot: options.repoRoot ?? repoRoot }); report.m3 = summarizeM3Report(sourceReport); report.conclusion = { status: sourceReport.summary?.status === "pass" ? "pass" : "blocked", summary: "DEV runtime postflight source contract checked only; no live HTTP or M3 write was attempted.", blockerCount: sourceReport.summary?.status === "pass" ? 0 : 1 }; if (sourceReport.summary?.status !== "pass") { addBlocker(report, "contract_blocker", "m3-source-contract", "M3 postflight source contract is blocked.", { classification: sourceReport.summary?.classification ?? "unknown" }); } return report; } if (!parsed.confirmDev || !parsed.confirmedNonProduction) { addSafetyRefusal(report, "DEV runtime postflight live mode requires --live --confirm-dev --confirmed-non-production."); return finalizeReport(report); } const httpGetJson = options.httpGetJson ?? defaultHttpGetJson; report.actions.healthLiveReadAttempted = true; report.actions.v1ReadAttempted = true; const [health, v1] = await Promise.all([ httpGetJson(defaultHealthUrl, parsed.timeoutMs), httpGetJson(defaultV1Url, parsed.timeoutMs) ]); report.apiHealth = summarizeApiPayload("GET /health/live", health); report.apiV1 = summarizeApiPayload("GET /v1", v1); addApiReadinessBlockers(report); if (report.blockers.length === 0) { report.actions.m3MutationAllowed = true; report.actions.m3MutationAttempted = true; const m3Runner = options.m3Runner ?? defaultM3Runner; report.m3 = summarizeM3Report(await m3Runner(parsed)); addM3Blockers(report); } else { report.m3 = { status: "not_run", trustedGreen: false, classification: "runtime_durable_postflight_precondition_blocked", operationCount: 0, reason: "M3 true/false write/read postflight is skipped until /health/live and /v1 prove durable runtime readiness." }; } return finalizeReport(report); } export function parseArgs(argv = []) { const args = { live: false, confirmDev: false, confirmedNonProduction: false, writeReport: false, reportPath: defaultReportPath, pretty: false, failOnBlocked: false, target: "api", timeoutMs: 8000, help: false }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--check") args.live = false; else if (arg === "--live") args.live = true; else if (arg === "--confirm-dev") args.confirmDev = true; else if (arg === "--confirmed-non-production") args.confirmedNonProduction = true; else if (arg === "--write-report") args.writeReport = true; else if (arg === "--pretty") args.pretty = true; else if (arg === "--fail-on-blocked") args.failOnBlocked = true; else if (arg === "--target") { index += 1; args.target = requireArgValue(argv[index], "--target"); } else if (arg === "--timeout-ms") { index += 1; args.timeoutMs = parseTimeout(requireArgValue(argv[index], "--timeout-ms")); } else if (arg === "--report") { index += 1; args.reportPath = requireArgValue(argv[index], "--report"); } else if (arg.startsWith("--report=")) { args.reportPath = requireArgValue(arg.slice("--report=".length), "--report"); } else if (arg === "--help" || arg === "-h") { args.help = true; } else { throw new Error(`unknown argument: ${arg}`); } } if (!["api", "frontend"].includes(args.target)) { throw new Error("--target must be api or frontend"); } return args; } function normalizeArgs(args) { return { ...parseArgs([]), ...args }; } async function defaultM3Runner(args) { const sourceReport = await buildM3IoControlSourceReport({ repoRoot }); return runM3IoControlLiveReport({ flags: new Set(["--live", "--confirm-dev", "--confirmed-non-production"]), live: true, confirmDev: true, confirmedNonProduction: true, target: args.target, frontendUrl: "http://74.48.78.17:16666/", apiUrl: `${DEV_ENDPOINT}/v1/m3/io`, timeoutMs: args.timeoutMs }, sourceReport); } async function defaultHttpGetJson(url, timeoutMs) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { method: "GET", headers: { accept: "application/json" }, signal: controller.signal }); const text = await response.text(); let json = null; try { json = text ? JSON.parse(text) : null; } catch { json = null; } return { ok: response.ok, status: response.status, json, error: response.ok ? null : json?.error?.message ?? response.statusText }; } catch (error) { return { ok: false, status: 0, json: null, error: error.name === "AbortError" ? `request timed out after ${timeoutMs}ms` : error.message }; } finally { clearTimeout(timer); } } function summarizeApiPayload(check, response) { const json = response.json ?? {}; const runtime = summarizeRuntime(json.runtime); return { check, httpStatus: response.status, reachable: response.ok === true && Boolean(response.json), serviceId: json.serviceId ?? json.service?.id ?? null, environment: json.environment ?? null, status: json.status ?? null, ready: json.ready === true, runtime, db: summarizeDb(json.db), readiness: { ready: json.readiness?.ready === true, status: json.readiness?.status ?? null, durabilityReady: json.readiness?.durability?.ready === true, durabilityBlockedLayer: json.readiness?.durability?.blockedLayer ?? json.runtime?.durabilityContract?.blockedLayer ?? null }, blockerCodes: Array.isArray(json.blockerCodes) ? json.blockerCodes : [], m3IoControl: json.m3IoControl ? { route: json.m3IoControl.route ?? null, enabled: json.m3IoControl.enabled === true, contractVersion: json.m3IoControl.contractVersion ?? null } : null, error: response.error ?? null }; } function summarizeRuntime(runtime = {}) { return { adapter: runtime?.adapter ?? "unknown", durable: runtime?.durable === true, durableRequested: runtime?.durableRequested === true, ready: runtime?.ready === true, status: runtime?.status ?? null, blocker: runtime?.blocker ?? null, liveRuntimeEvidence: runtime?.liveRuntimeEvidence === true, requiredEvidence: runtime?.durabilityContract?.requiredEvidence ?? null, blockedLayer: runtime?.durabilityContract?.blockedLayer ?? null, gates: summarizeRuntimeGates(runtime?.gates) }; } function summarizeRuntimeGates(gates = {}) { return Object.fromEntries(["ssl", "auth", "schema", "migration", "durability"].map((name) => [ name, { checked: gates?.[name]?.checked === true, ready: gates?.[name]?.ready === true, status: gates?.[name]?.status ?? "unknown", blocker: gates?.[name]?.blocker ?? null } ])); } function summarizeDb(db = {}) { return { ready: db?.ready === true, connected: db?.connected === true, liveDbEvidence: db?.liveDbEvidence === true, endpointSource: db?.endpointSource ?? null, connectionResult: db?.connectionResult ?? db?.connection?.result ?? null, endpointRedacted: true, valueRedacted: true, runtimeReadiness: { ready: db?.runtimeReadiness?.ready === true, status: db?.runtimeReadiness?.status ?? null, blocker: db?.runtimeReadiness?.blocker ?? null, queryResult: db?.runtimeReadiness?.queryResult ?? null, requiredEvidence: db?.runtimeReadiness?.requiredEvidence ?? null } }; } function summarizeM3Report(report = {}) { const operations = Array.isArray(report.liveOperations) ? report.liveOperations : []; return { mode: report.mode ?? "unknown", status: report.summary?.status ?? report.status ?? "unknown", classification: report.summary?.classification ?? null, trustedGreen: report.summary?.trustedGreen === true, result: report.summary?.result ?? null, operationCount: operations.length, operations: operations.map((operation) => ({ id: operation.id, action: operation.action, status: operation.status, operationId: operation.operationId ?? null, traceId: operation.traceId ?? null, auditId: operation.auditId ?? null, evidenceId: operation.evidenceId ?? null, resultValue: operation.resultValue, evidenceState: { status: operation.evidenceState?.status ?? null, durable: operation.evidenceState?.durable === true, sourceKind: operation.evidenceState?.sourceKind ?? null }, blocker: operation.blocker ?? null })) }; } function addApiReadinessBlockers(report) { for (const [scope, payload] of [["api-health-live", report.apiHealth], ["api-v1", report.apiV1]]) { if (!payload.reachable) { addBlocker(report, "runtime_blocker", scope, `${payload.check} was not reachable.`, { httpStatus: payload.httpStatus, error: payload.error }); continue; } if (!runtimeDurableReady(payload.runtime)) { addBlocker(report, "runtime_blocker", scope, `${payload.check} did not prove durable runtime readiness.`, { runtime: payload.runtime, blockerCodes: payload.blockerCodes }); } } } function addM3Blockers(report) { if (report.m3?.trustedGreen !== true || report.m3?.status !== "pass") { addBlocker(report, "runtime_blocker", "m3-durable-postflight", "M3 true/false durable postflight did not produce trusted green persisted evidence.", { classification: report.m3?.classification ?? "unknown", trustedGreen: report.m3?.trustedGreen === true, operationCount: report.m3?.operationCount ?? 0 }); return; } for (const operation of report.m3.operations ?? []) { if (!operation.operationId || !operation.auditId || !operation.evidenceId || operation.evidenceState?.status !== "green" || operation.evidenceState?.durable !== true) { addBlocker(report, "runtime_blocker", "m3-durable-evidence", "M3 operation/audit/evidence durable fields were incomplete.", { operationIdPresent: Boolean(operation.operationId), auditIdPresent: Boolean(operation.auditId), evidenceIdPresent: Boolean(operation.evidenceId), evidenceState: operation.evidenceState }); return; } } } function runtimeDurableReady(runtime) { return runtime.adapter === "postgres" && runtime.durable === true && runtime.ready === true && runtime.liveRuntimeEvidence === true; } function addSafetyRefusal(report, summary) { report.safetyRefusal = true; addBlocker(report, "safety_refusal", "runtime-postflight-live-boundary", summary, { devOnly: true, prodAllowed: false, secretValuesPrinted: false }); } function addBlocker(report, type, scope, summary, evidence = {}) { report.blockers.push({ type, scope, status: "open", summary, sourceIssue: issue, evidence }); } function finalizeReport(report) { const openBlockers = report.blockers.filter((blocker) => blocker.status === "open"); report.conclusion = { status: openBlockers.length === 0 ? "pass" : "blocked", summary: openBlockers.length === 0 ? "DEV runtime postflight passed: /health/live, /v1, and M3 true/false durable evidence are green." : "DEV runtime postflight is blocked; M3 is skipped unless durable runtime readiness is proven first.", blockerCount: openBlockers.length }; return report; } function buildSafety(args) { return { devOnly: true, environment: ENVIRONMENT_DEV, prodAllowed: false, liveHttpReads: args.live === true, liveM3Writes: args.live === true, liveM3WritesRequireDurableRuntimeReady: true, readsKubernetesSecrets: false, writesKubernetesSecrets: false, secretValuesPrinted: false, dbUrlValueRedacted: true, endpointRedacted: true }; } function summarizeReport(report) { return { issue: report.issue, mode: report.mode, conclusion: report.conclusion, actions: report.actions, apiHealth: report.apiHealth, apiV1: report.apiV1, m3: report.m3, blockers: report.blockers, safety: report.safety }; } async function writeReport(report, reportPath, root) { const absolute = path.resolve(root, reportPath); await mkdir(path.dirname(absolute), { recursive: true }); await writeFile(absolute, `${JSON.stringify(report, null, 2)}\n`); } function parseTimeout(value) { const parsed = Number.parseInt(value, 10); if (!Number.isInteger(parsed) || parsed <= 0) { throw new Error("--timeout-ms must be a positive integer"); } return Math.min(parsed, 30000); } function requireArgValue(value, flag) { if (typeof value !== "string" || value.trim() === "") { throw new Error(`${flag} requires a value`); } return value; } function usage() { return [ "Usage: node scripts/dev-runtime-postflight.mjs [--check|--live]", "", "Default --check validates source M3 postflight contracts only.", "--live requires --confirm-dev --confirmed-non-production and reads /health/live plus /v1 before any M3 write.", "M3 true/false write/read is skipped unless durable runtime readiness is already green.", "Reports never print DB URLs, passwords, tokens, Secret values, or kubeconfig material." ].join("\n"); } export function formatDevRuntimePostflightFailure(error) { const message = redactFailureText(error instanceof Error ? error.message : String(error)); return { issue, conclusion: { status: "blocked", summary: "DEV runtime postflight command failed before producing a report." }, error: message, safety: { devOnly: true, prodAllowed: false, secretValuesPrinted: false }, trace: createHash("sha256").update(message).digest("hex").slice(0, 12) }; } function redactFailureText(value) { return String(value ?? "") .replace(/postgres(?:ql)?:\/\/[^\s"'<>]+/giu, "[redacted-postgres-url]") .replace(/(password\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]") .replace(/(token\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]") .replace(/(kubeconfig\s*[=:]\s*)[^\s,;]+/giu, "$1[redacted]"); }