Files
pikasTech-HWLAB/scripts/src/dev-runtime-postflight.mjs
T
2026-05-23 10:22:57 +00:00

493 lines
16 KiB
JavaScript

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]");
}