scripts/plan.js
#!/usr/bin/env node
// plan.js — generates the interview plan (cells = domain x lens pairs) for one
// model into its dataset repo, and seeds the manifest.
import fs from "node:fs";
import path from "node:path";
import { loadDomains, loadLenses, parseArgs, nowIso, PROTOCOL_VERSION, modelSlug } from "./lib.js";
import { findModel } from "./models.js";
const { args } = parseArgs(process.argv.slice(2), {
model: { takes: "value", default: null },
dataset: { takes: "value", default: null },
depth: { takes: "value", default: "standard" },
});
if (!args.model) { console.error("--model is required"); process.exit(2); }
if (!args.dataset) { console.error("--dataset (dataset repo path) is required"); process.exit(2); }
if (!["minimal", "standard", "deep"].includes(args.depth)) {
console.error("--depth must be minimal|standard|deep"); process.exit(2);
}
const domains = loadDomains();
const lenses = loadLenses();
const lensById = Object.fromEntries(lenses.map((l) => [l.id, l]));
const model = findModel(args.model);
const cells = [];
const push = (domain, lens) => cells.push({ cell_id: `${domain}-${lens}`, domain, lens, status: "pending" });
if (args.depth === "deep") {
for (const d of domains) for (const l of lenses) if (l.id !== "self-model") push(d.id, l.id);
} else if (args.depth === "standard") {
domains.forEach((d, i) => {
push(d.id, "principles");
push(d.id, "prospective");
if (d.retrospective_weight === "high") push(d.id, "retrospective");
if (i % 3 === 0) push(d.id, "controversy");
if (i % 3 === 1) push(d.id, "blindspots");
});
} else {
domains.forEach((d, i) => {
if (i % 3 !== 0) return;
push(d.id, "principles");
push(d.id, "prospective");
});
}
push("meta", "self-model");
fs.mkdirSync(args.dataset, { recursive: true });
// Don't clobber existing progress if re-run with the same depth.
const planPath = path.join(args.dataset, "plan.yaml");
if (fs.existsSync(planPath)) {
const existing = fs.readFileSync(planPath, "utf8");
const done = (existing.match(/status: (done|in_progress)/g) || []).length;
const prevCells = (existing.match(/^ - cell_id:/gm) || []).length;
if (prevCells === cells.length) {
console.log(`plan.yaml already exists with ${prevCells} cells (${done} done/in-progress) — kept.`);
} else {
const backup = planPath + ".bak";
fs.renameSync(planPath, backup);
console.log(`plan.yaml depth changed (was ${prevCells} cells) — old plan moved to ${backup}.`);
}
}
const yaml = [
`# xray interview plan — generated by scripts/plan.js`,
`model: ${args.model}`,
`model_slug: ${modelSlug(args.model)}`,
`depth: ${args.depth}`,
`protocol_version: ${PROTOCOL_VERSION}`,
`created: ${nowIso()}`,
``,
`cells:`,
...cells.map((c) => [
` - cell_id: ${c.cell_id}`,
` domain: ${c.domain}`,
` lens: ${c.lens}`,
` status: ${c.status}`,
` session: null`,
].join("\n")),
].join("\n") + "\n";
fs.writeFileSync(planPath, yaml, "utf8");
// Seed manifest if absent
const manifestPath = path.join(args.dataset, "manifest.json");
if (!fs.existsSync(manifestPath)) {
const manifest = {
dataset: modelSlug(args.model),
model: {
id: args.model,
family: model?.family ?? null,
version: null,
host: "cloudflare-workers-ai",
quantization: model?.quant ?? null,
context_window_tokens: model?.context ?? null,
},
depth: args.depth,
protocol_version: PROTOCOL_VERSION,
record_schema: "https://fact.ngo/xray/schemas/record.schema.json",
created: nowIso(),
elicited_by: "xray.fact.ngo",
license_note: "Model outputs archived under the terms of the model's license. Verify license terms of the model before redistribution.",
};
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
}
fs.mkdirSync(path.join(args.dataset, "sessions"), { recursive: true });
fs.mkdirSync(path.join(args.dataset, "records"), { recursive: true });
console.log(`plan written: ${planPath}`);
console.log(`cells: ${cells.length} (depth=${args.depth}, domains=${domains.length})`);
const byLens = {};
for (const c of cells) byLens[c.lens] = (byLens[c.lens] || 0) + 1;
console.log("per lens:", Object.entries(byLens).map(([k, v]) => `${k}=${v}`).join(" "));