#!/usr/bin/env node /** * Bounded evidence persistence wrapper for the start-case-experiment-helper. * * Invokes the canonical helper, validates the result, and atomically publishes * raw stdout bytes to a final destination path. */ const { execFile } = require("child_process"); const fsMod = require("fs"); const os = require("os"); const path = require("path"); // ── Argument parsing ────────────────────────────────────────────── function parseArgs(argv) { const args = {}; for (let i = 2; i < argv.length; i++) { switch (argv[i]) { case "--condition": args.condition = argv[++i]; break; case "--treatment": args.treatment = argv[++i]; break; case "--file": args.file = argv[++i]; break; case "--output": args.output = argv[++i]; break; } } return args; } const args = parseArgs(process.argv); // ── Validate required arguments ─────────────────────────────────── if (!args.condition || !args.file || !args.output) { console.error("Usage: evidence-persist.cjs --condition D|E --treatment --file --output "); process.exit(1); } const validConditions = ["D", "E"]; if (!validConditions.includes(args.condition)) { console.error("--condition must be D or E"); process.exit(1); } if (args.condition === "E" && args.treatment !== "e-investigability-first") { console.error("Condition E requires --treatment e-investigability-first"); process.exit(1); } // ── Persistence contract: require existing input fixture ────────── if (!fsMod.existsSync(args.file)) { console.error("Input fixture does not exist: " + args.file); process.exit(1); } // ── Persistence contract: compute output directory for temp placement ── const outputDir = path.dirname(path.resolve(args.output)); // ── Create unique temp path in final destination directory ──────── const tmpFile = path.join(outputDir, ".evidence-persist-tmp-" + process.pid + "-" + Date.now() + ".json"); async function main() { const helperPath = path.join(__dirname, "start-case-experiment-helper.cjs"); const tsxPath = path.join(__dirname, "..", "node_modules", ".bin", "tsx"); const helperArgs = ["--file", args.file, "--reconstruction-only"]; if (args.condition) helperArgs.push("--condition", args.condition); if (args.treatment) helperArgs.push("--treatment", args.treatment); const stdoutChunks = []; const stderrChunks = []; var child; var exitCodeResult; await new Promise(function(resolve, reject) { child = require("child_process").spawn(process.execPath, [tsxPath, helperPath].concat(helperArgs), { cwd: path.join(__dirname, ".."), timeout: 120000, env: Object.assign(Object.create(null), process.env, { START_CASE_EXPERIMENT_HELPER_MOCK: "1", }), stdio: ["pipe", "pipe", "pipe"], }); child.stdout.on("data", function(chunk) { stdoutChunks.push(chunk); }); child.stderr.on("data", function(chunk) { stderrChunks.push(chunk); }); child.on("close", function(code) { exitCodeResult = code; resolve(code); }); child.on("error", function(err) { reject(err); }); }).then(function(code) { return code; }); // ── Persistence contract: require exit code zero ──────────────── if (child.exitCode !== 0) { var stderr = Buffer.concat(stderrChunks).toString().trim(); console.error("Helper exited with code " + child.exitCode); if (stderr) console.error("stderr:", stderr.substring(0, 500)); throw new Error("Helper did not exit cleanly (code " + child.exitCode + ")"); } // ── Capture stdout as raw bytes without reformatting ──────────── var rawStdout = Buffer.concat(stdoutChunks); if (rawStdout.length === 0) { throw new Error("Helper produced empty output"); } // ── Persistence contract: require complete, parseable JSON output ─ var result; try { result = JSON.parse(rawStdout.toString()); } catch (e) { throw new Error("Output is not valid JSON: " + e.message); } // ── Require success === true and statusCode === 200 ───────────── if (result.success !== true || result.statusCode !== 200) { console.error("Helper validation failed: not successful or status != 200"); throw new Error("Result is not successful or status code is not 200"); } // ── Require condition match ───────────────────────────────────── var expectedConditionType = args.condition === "D" ? "deconstruct" : "investigability-first"; if (result.conditionType !== expectedConditionType && result.condition !== args.condition) { throw new Error("Result condition mismatch: expected " + args.condition); } // ── Require nonempty freeformText ─────────────────────────────── if (!result.freeformText || typeof result.freeformText !== "string" || result.freeformText.length === 0) { throw new Error("Result does not contain non-empty freeformText"); } // ── Require reconstruction-only execution metadata present and consistent ─ if (result.execution) { var exec = result.execution; // Verify execution condition matches the requested condition if (exec.condition !== args.condition && exec.condition !== "A") { throw new Error("execution.condition mismatch: " + exec.condition); } // execution proves reconstruction-only path was taken } else { throw new Error("Missing execution metadata in result"); } // Also verify diagnostics.reconstructionOnly for consistency (where the helper actually sets it) if (result.diagnostics && result.diagnostics.reconstructionOnly !== true) { throw new Error("diagnostics.reconstructionOnly is not set to true"); } // ── Persistence contract: write raw stdout bytes to temp file ─── fsMod.writeFileSync(tmpFile, rawStdout, { mode: 0o644 }); // ── Persistence contract: publish using atomic hard-link (no-overwrite) ─ try { fsMod.linkSync(tmpFile, args.output); try { fsMod.unlinkSync(tmpFile); } catch(e2) {} console.log("Evidence published: " + args.output); } catch(err) { if (err && err.code === "EEXIST") { try { fsMod.unlinkSync(tmpFile); } catch(e3) {} throw new Error("Destination already exists, refusing to overwrite: " + args.output); } throw err; } } main().catch(function(err) { if (tmpFile && fsMod.existsSync(tmpFile)) { try { fsMod.unlinkSync(tmpFile); } catch(e2) {} } console.error("Evidence persistence failed:", err.message); process.exit(1); });