Files
projectbrain-Public/scripts/release-operator-proof-report.mjs
T

184 lines
5.9 KiB
JavaScript

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
const baseUrl = process.env.PROJECTBRAIN_SMOKE_URL ?? "http://127.0.0.1:5173";
const projectId = process.env.PROJECTBRAIN_SMOKE_PROJECT ?? "widefrog";
const shouldRunSmokes = process.argv.includes("--run-smokes") || process.env.PROJECTBRAIN_PROOF_RUN_SMOKES === "1";
const hasExplicitBaseUrl = Boolean(process.env.PROJECTBRAIN_SMOKE_URL);
const shouldWriteReport = process.argv.includes("--write") || process.env.PROJECTBRAIN_PROOF_WRITE === "1";
const explicitOutputArg = process.argv.find((arg) => arg.startsWith("--out="));
const explicitOutputPath = explicitOutputArg?.slice("--out=".length) || process.env.PROJECTBRAIN_PROOF_OUT;
const evidenceRows = [
{
id: "RC63",
label: "Release evidence lane click-through",
script: "smoke:rc63-release-evidence-lane",
proves: "Release evidence routes are reachable from the operator lane.",
},
{
id: "RC60",
label: "Review decision receipt recovery",
script: "smoke:rc60-review-decision-loop",
proves: "Review decisions can be recovered and inspected from history.",
},
{
id: "RC73",
label: "Release evidence capture assist",
script: "smoke:rc73-release-evidence-capture-assist",
proves: "Runtime evidence capture assists the operator instead of leaving proof manual-only.",
},
{
id: "RC88",
label: "Release Center cockpit",
script: "smoke:rc88-release-center",
proves: "Release Center renders the cockpit, archives, quality and operator actions.",
},
{
id: "RC99",
label: "Release checklist click-through",
script: "smoke:rc99-release-checklist-clickthrough",
proves: "Checklist links land on exact target sections and expose return paths.",
},
];
const run = (command, args, options = {}) => spawnSync(command, args, {
encoding: "utf8",
shell: process.platform === "win32",
...options,
});
const readPackage = () => {
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
return packageJson.scripts ?? {};
};
const getGitCommit = () => {
const result = run("git", ["rev-parse", "--short", "HEAD"]);
return result.status === 0 ? result.stdout.trim() : "unknown";
};
const createDefaultReportPath = (commit) => {
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
return `checkpoints/release-proof/projectbrain-release-proof-${commit}-${stamp}.md`;
};
const checkLocalConfiguration = () => {
const scripts = readPackage();
const missing = [];
if (!scripts["smoke:release-operator"]) missing.push("smoke:release-operator");
if (!scripts["release:proof-report"]) missing.push("release:proof-report");
for (const row of evidenceRows) {
if (!scripts[row.script]) missing.push(row.script);
}
return missing;
};
const checkLiveHealth = async () => {
try {
const response = await fetch(`${baseUrl}/api/health`);
const body = await response.text();
return {
ok: response.ok,
status: response.status,
body: body.slice(0, 220),
};
} catch (error) {
return {
ok: false,
status: "unreachable",
body: error instanceof Error ? error.message : String(error),
};
}
};
const runOperatorSmoke = () => {
const result = run("pnpm", ["run", "smoke:release-operator"], {
stdio: "inherit",
env: process.env,
});
return result.status === 0;
};
const formatTable = (rows) => {
const header = "| Gate | Smoke | Proof |\n| --- | --- | --- |";
const body = rows.map((row) => `| ${row.id} | \`${row.script}\` | ${row.proves} |`).join("\n");
return `${header}\n${body}`;
};
const localMissing = checkLocalConfiguration();
let smokePassed = null;
if (shouldRunSmokes) {
console.log("Running release operator smoke suite before generating proof report...\n");
smokePassed = runOperatorSmoke();
}
const liveHealth = await checkLiveHealth();
const gitCommit = getGitCommit();
const localConfigOk = localMissing.length === 0;
const healthGateOk = liveHealth.ok || !hasExplicitBaseUrl;
const verdict = localConfigOk && healthGateOk && (smokePassed !== false)
? (smokePassed === true ? "PASS" : "CONFIGURED - SMOKE NOT RUN")
: "ATTENTION REQUIRED";
const reportPath = explicitOutputPath || createDefaultReportPath(gitCommit);
const report = [
"# ProjectBrain Release Operator Proof Report",
"",
`Generated: ${new Date().toISOString()}`,
`Commit: ${gitCommit}`,
`Base URL: ${baseUrl}`,
`Project: ${projectId}`,
`Verdict: ${verdict}`,
"",
"## Live Health",
"",
`- Status: ${liveHealth.status}`,
`- Result: ${liveHealth.ok ? "reachable" : hasExplicitBaseUrl ? "not reachable" : "not reachable in local default mode"}`,
`- Body: ${liveHealth.body}`,
"",
"## Proof Coverage",
"",
formatTable(evidenceRows),
"",
"## Local Configuration",
"",
localConfigOk
? "- Required release smoke scripts are registered."
: `- Missing scripts: ${localMissing.map((name) => `\`${name}\``).join(", ")}`,
`- Smoke execution: ${smokePassed === true ? "passed" : smokePassed === false ? "failed" : "not run"}`,
"",
"## Operator Commands",
"",
"```bash",
"pnpm run release:proof-report",
"pnpm run release:proof-report -- --write",
"PROJECTBRAIN_PROOF_RUN_SMOKES=1 pnpm run release:proof-report",
"PROJECTBRAIN_PROOF_RUN_SMOKES=1 PROJECTBRAIN_PROOF_WRITE=1 pnpm run release:proof-report",
"PROJECTBRAIN_SMOKE_URL=http://127.0.0.1:1209 pnpm run smoke:release-operator",
"```",
"",
"## Remaining Manual Gates",
"",
"- Confirm the intended release commit/tag.",
"- Confirm any environment-specific deployment notes.",
"- Confirm operator signoff after the smoke suite is green.",
].join("\n");
console.log(report);
if (shouldWriteReport) {
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
fs.writeFileSync(reportPath, `${report}\n`, "utf8");
console.log(`\nRelease proof report written to ${reportPath}`);
}
if (!localConfigOk || !healthGateOk || smokePassed === false) {
process.exitCode = 1;
}