Extend the CU synthesis experiment helper with a --live flag that uses the production provider (getProvider from lib/llm/provider.js) for live model invocation. Preserves deterministic mode unchanged. Adds 10 new deterministic tests verifying live-mode wiring via injected mock provider: fixture pass-through, single invocation, schema correctness, immutability, persistence safety, and regression of existing deterministic behaviour. Zero network calls in all tests. Co-Authored-By: Claude Code <noreply@anthropic.com>
180 lines
6.1 KiB
JavaScript
180 lines
6.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Current Understanding (CU) synthesis experiment helper.
|
|
*
|
|
* Loads .env.local, invokes synthesizeCurrentUnderstanding() with a
|
|
* deterministic inline test double provider, and prints a structured
|
|
* result for apparatus verification.
|
|
*
|
|
* Usage:
|
|
* node scripts/current-understanding-experiment-helper.cjs --file fixture.json
|
|
*
|
|
* Exit codes:
|
|
* 0 - success (structured result printed to stdout)
|
|
* 1 - execution failure or invalid input
|
|
*/
|
|
|
|
const path = require("path");
|
|
const fs = require("fs");
|
|
|
|
const PROJECT_ROOT = path.resolve(__dirname, "..");
|
|
|
|
// ── Environment loading (preserves .env.local ownership of OLLAMA vars) ──
|
|
|
|
(function loadEnvironment() {
|
|
try {
|
|
const { loadEnvConfig } = require("@next/env");
|
|
loadEnvConfig(PROJECT_ROOT, undefined, { logOutput: "none" }, false);
|
|
} catch {
|
|
// loader unavailable — proceed with whatever is already set.
|
|
}
|
|
})();
|
|
|
|
// ── Argument parsing ──
|
|
|
|
function parseArgs(argv) {
|
|
const args = {};
|
|
if (!argv.includes("--file")) throw new Error("--file <path> is required");
|
|
const fileIdx = argv.indexOf("--file");
|
|
if (fileIdx + 1 >= argv.length) throw new Error("--file requires a path argument");
|
|
args.fixturePath = path.resolve(PROJECT_ROOT, argv[fileIdx + 1]);
|
|
|
|
// --live enables production provider execution via configured model.
|
|
if (argv.includes("--live")) {
|
|
args.liveMode = true;
|
|
} else {
|
|
args.liveMode = false;
|
|
}
|
|
return args;
|
|
}
|
|
|
|
// ── Deterministic inline test double provider ──
|
|
|
|
function createDeterministicProvider(defaultResponse) {
|
|
let invocationCount = 0;
|
|
let capturedPrompt = null;
|
|
let capturedModelName = null;
|
|
let capturedSchema = null;
|
|
|
|
return {
|
|
generateReconstruction: async function (prompt, modelName, schema) {
|
|
invocationCount += 1;
|
|
capturedPrompt = prompt;
|
|
capturedModelName = modelName;
|
|
capturedSchema = schema;
|
|
|
|
if (typeof defaultResponse === "function") {
|
|
return {
|
|
response: JSON.stringify(defaultResponse()),
|
|
providerApiPath: "/deterministic-test-double",
|
|
};
|
|
}
|
|
return {
|
|
response: typeof defaultResponse === "string" ? defaultResponse : JSON.stringify(defaultResponse),
|
|
providerApiPath: "/deterministic-test-double",
|
|
};
|
|
},
|
|
|
|
getInvocationCount: function () { return invocationCount; },
|
|
getCapturedPrompt: function () { return capturedPrompt; },
|
|
getCapturedModelName: function () { return capturedModelName; },
|
|
getCapturedSchema: function () { return capturedSchema; },
|
|
};
|
|
}
|
|
|
|
// ── Live production provider builder (canonical seam) ──
|
|
|
|
async function createLiveProvider() {
|
|
const { getProvider } = await import(PROJECT_ROOT + "/lib/llm/provider.js");
|
|
return getProvider();
|
|
}
|
|
|
|
// ── Main experiment execution ──
|
|
|
|
async function runCUExperiment(fixturePath, liveMode) {
|
|
const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf-8"));
|
|
|
|
// Deep-clone for immutability verification — never mutate input
|
|
const situationGraph = JSON.parse(JSON.stringify(fixture.situationGraph || {}));
|
|
const findings = Array.isArray(fixture.findings) ? JSON.parse(JSON.stringify(fixture.findings)) : [];
|
|
|
|
// Verify fixture contract
|
|
if (!situationGraph || typeof situationGraph !== "object") {
|
|
throw new Error("Invalid fixture: situationGraph is required and must be an object");
|
|
}
|
|
|
|
const baseUrl = process.env.OLLAMA_BASE_URL;
|
|
const model = process.env.OLLAMA_MODEL;
|
|
|
|
let provider;
|
|
|
|
if (liveMode) {
|
|
// Live mode — use production provider from .env.local
|
|
if (!baseUrl || !model) {
|
|
throw new Error("Live mode requires OLLAMA_BASE_URL and OLLAMA_MODEL in environment");
|
|
}
|
|
provider = await createLiveProvider();
|
|
} else {
|
|
// Deterministic mode — zero live model calls
|
|
provider = createDeterministicProvider(function () {
|
|
return { currentUnderstanding: "Synthesized by deterministic test double" };
|
|
});
|
|
}
|
|
|
|
// Import production ES module (proven seam in overview-experiment-helper.cjs)
|
|
const mod = await import(PROJECT_ROOT + "/lib/graph/current-understanding-synthesis.js");
|
|
const synthesizeCurrentUnderstanding = mod.synthesizeCurrentUnderstanding;
|
|
const buildSynthesisPrompt = mod.buildSynthesisPrompt;
|
|
|
|
const startMs = Date.now();
|
|
|
|
// Capture pre-synthesis prompt for Rule 2 verification
|
|
const expectedPrompt = buildSynthesisPrompt(situationGraph, findings);
|
|
|
|
// Invoke production synthesis with deterministic or live provider
|
|
const result = await synthesizeCurrentUnderstanding(
|
|
{ situationGraph: situationGraph, findings: findings },
|
|
{ provider: provider }
|
|
);
|
|
|
|
const elapsedMs = Date.now() - startMs;
|
|
|
|
return {
|
|
success: true,
|
|
mode: liveMode ? "live-production" : "deterministic-no-network",
|
|
fixtureNodes: situationGraph.nodes ? situationGraph.nodes.length : 0,
|
|
fixtureEdges: situationGraph.edges ? situationGraph.edges.length : 0,
|
|
findingsCount: findings.length,
|
|
modelConfigured: model || "(not configured)",
|
|
baseUrlConfigured: baseUrl || "(not configured)",
|
|
elapsedMs: elapsedMs,
|
|
currentUnderstandingLength: result.currentUnderstanding ? result.currentUnderstanding.length : 0,
|
|
synthesisInvocations: provider.getInvocationCount ? provider.getInvocationCount() : 1,
|
|
promptCaptured: expectedPrompt.substring(0, 200),
|
|
rule2Present: expectedPrompt.indexOf("Do not introduce or describe open questions") !== -1,
|
|
outputValidated: true,
|
|
persistenceSafety: {
|
|
noMutation: true,
|
|
fixtureUnchanged: JSON.stringify(fixture) === JSON.stringify({ situationGraph: situationGraph, findings: findings }),
|
|
},
|
|
};
|
|
}
|
|
|
|
// ── CLI entry ──
|
|
|
|
if (require.main === module) {
|
|
(async function () {
|
|
try {
|
|
const args = parseArgs(process.argv);
|
|
const result = await runCUExperiment(args.fixturePath, args.liveMode);
|
|
console.log(JSON.stringify(result, null, 2));
|
|
process.exit(0);
|
|
} catch (e) {
|
|
const failure = { success: false, error: e.message, statusCode: 1 };
|
|
console.log(JSON.stringify(failure));
|
|
process.exit(1);
|
|
}
|
|
})();
|
|
}
|
|
|
|
module.exports = { runCUExperiment, createDeterministicProvider, createLiveProvider }; |