186 lines
6.8 KiB
JavaScript
186 lines
6.8 KiB
JavaScript
#!/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 <val for E> --file <fixture> --output <dest>");
|
|
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);
|
|
});
|