414 lines
15 KiB
JavaScript
414 lines
15 KiB
JavaScript
/**
|
|
* RTO.20A — Derived Focused Current-View Apparatus
|
|
*
|
|
* Purpose: Test whether an LLM can produce a concise, faithful "where are we
|
|
* now?" view for one selected investigation using only authoritative granular
|
|
* fragments and their known relationship, without whole-case reconstruction,
|
|
* accumulated focused state, or introducing new evidential claims.
|
|
*
|
|
* Design boundary:
|
|
* - Standalone experimental runner.
|
|
* - Zero production code changes.
|
|
* - Inspect-only by default (zero live model calls).
|
|
* - --live flag enables exactly one live call producing a derived current view.
|
|
*
|
|
* Fixed record:
|
|
* Fragment 2 — competitor hiring + conference signals
|
|
* Fragment 3 — technical architecture + prototype evidence
|
|
* One known relationship between them
|
|
*
|
|
* Output contract (presentation only, not new reasoning):
|
|
* { currentView, whatChanged, remainingUncertainties }
|
|
*/
|
|
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const resultsDir = path.resolve(__dirname, "../../tests/experimental/results");
|
|
|
|
// ─── Fragment paths ──────────────────────────────────────────────────────────
|
|
|
|
const FRAGMENT_2_PATH = path.resolve(
|
|
__dirname,
|
|
"../../tests/experimental/results/rto-granular-fragment-turn2.json"
|
|
);
|
|
|
|
const FRAGMENT_3_PATH = path.resolve(
|
|
__dirname,
|
|
"../../tests/experimental/results/rto-granular-fragment-turn3.json"
|
|
);
|
|
|
|
// ─── Known relationship (supplied, not discovered) ───────────────────────────
|
|
|
|
const KNOWN_RELATIONSHIP =
|
|
"Fragment 3's technical evidence bears on Fragment 2's earlier uncertainty " +
|
|
"by making active competing-product development more plausible, while " +
|
|
"commercialisation and other uncertainties remain unresolved.";
|
|
|
|
// ─── Load fragments ──────────────────────────────────────────────────────────
|
|
|
|
async function loadFragments() {
|
|
const frag2Raw = await fs.readFile(FRAGMENT_2_PATH, "utf-8");
|
|
const frag3Raw = await fs.readFile(FRAGMENT_3_PATH, "utf-8");
|
|
|
|
const frag2 = JSON.parse(frag2Raw);
|
|
const frag3 = JSON.parse(frag3Raw);
|
|
|
|
if (!frag2.structuredResult) {
|
|
throw new Error("Fragment 2 missing structuredResult");
|
|
}
|
|
if (!frag3.structuredResult) {
|
|
throw new Error("Fragment 3 missing structuredResult");
|
|
}
|
|
|
|
return {
|
|
fragment2: frag2,
|
|
structuredResult2: frag2.structuredResult,
|
|
fragment3: frag3,
|
|
structuredResult3: frag3.structuredResult,
|
|
};
|
|
}
|
|
|
|
// ─── Build the derived-current-view prompt ─────────────────────────────────────
|
|
|
|
function buildCurrentViewPrompt(srs2, srs3) {
|
|
const parts = [
|
|
"You are producing a FACILITATOR CURRENT VIEW — a concise summary of where one",
|
|
"selected investigation stands right now. This is PRESENTATION ONLY.",
|
|
"",
|
|
"Return exactly one JSON object with these fields:",
|
|
"- currentView (string): Where does this investigation stand? What do we understand?",
|
|
"- whatChanged (string): How has the interpretation shifted from the earlier evidence?",
|
|
"- remainingUncertainties (array of strings): What is still unresolved?",
|
|
"",
|
|
"CRITICAL BOUNDARIES:",
|
|
"- Every claim must be traceable to Fragment 2, Fragment 3, or the known relationship.",
|
|
"- Do NOT introduce new evidential claims.",
|
|
"- Do NOT take action or advise on decisions.",
|
|
"- Do NOT predict decisions or outcomes.",
|
|
"- Do NOT select next questions or take ownership from the user.",
|
|
"- This output is derived presentation, not new authoritative reasoning.",
|
|
"",
|
|
"Selected investigation identity:",
|
|
];
|
|
|
|
// Node label from Fragment 2 (they share the same target)
|
|
parts.push(`- target node: "${srs2.targetNodeId}"`);
|
|
parts.push("");
|
|
|
|
// ── Fragment 2 ────────────────────────────────────────────────
|
|
parts.push("=== Fragment 2 (earlier evidence) ===");
|
|
if (srs2.observations && srs2.observations.length) {
|
|
parts.push("Observations:");
|
|
for (const o of srs2.observations) {
|
|
parts.push(` - ${o}`);
|
|
}
|
|
}
|
|
if (srs2.uncertainties && srs2.uncertainties.length) {
|
|
parts.push("Uncertainties:");
|
|
for (const u of srs2.uncertainties) {
|
|
parts.push(` - ${u}`);
|
|
}
|
|
}
|
|
if (srs2.assumptions && srs2.assumptions.length) {
|
|
parts.push("Assumptions:");
|
|
for (const a of srs2.assumptions) {
|
|
parts.push(` - ${a}`);
|
|
}
|
|
}
|
|
if (srs2.relationships && srs2.relationships.length) {
|
|
parts.push("Relationships:");
|
|
for (const r of srs2.relationships) {
|
|
parts.push(` - ${r.from} -> ${r.to} (${r.type})`);
|
|
}
|
|
}
|
|
parts.push("");
|
|
|
|
// ── Fragment 3 ────────────────────────────────────────────────
|
|
parts.push("=== Fragment 3 (newer evidence) ===");
|
|
if (srs3.observations && srs3.observations.length) {
|
|
parts.push("Observations:");
|
|
for (const o of srs3.observations) {
|
|
parts.push(` - ${o}`);
|
|
}
|
|
}
|
|
if (srs3.uncertainties && srs3.uncertainties.length) {
|
|
parts.push("Uncertainties:");
|
|
for (const u of srs3.uncertainties) {
|
|
parts.push(` - ${u}`);
|
|
}
|
|
}
|
|
if (srs3.assumptions && srs3.assumptions.length) {
|
|
parts.push("Assumptions:");
|
|
for (const a of srs3.assumptions) {
|
|
parts.push(` - ${a}`);
|
|
}
|
|
}
|
|
if (srs3.relationships && srs3.relationships.length) {
|
|
parts.push("Relationships:");
|
|
for (const r of srs3.relationships) {
|
|
parts.push(` - ${r.from} -> ${r.to} (${r.type})`);
|
|
}
|
|
}
|
|
parts.push("");
|
|
|
|
// ── Known relationship ────────────────────────────────────────
|
|
parts.push("=== Known relationship (supplied, not discovered) ===");
|
|
parts.push(KNOWN_RELATIONSHIP);
|
|
parts.push("");
|
|
|
|
// ── Instructions ──────────────────────────────────────────────
|
|
parts.push("Produce a concise facilitator view of where this selected investigation stands now.");
|
|
parts.push("Answer: What do we understand now? How has the interpretation shifted? What remains unresolved?");
|
|
parts.push("Do NOT take action or advise on decisions.");
|
|
parts.push("Return JSON only. No additional commentary.");
|
|
|
|
return parts.join("\n");
|
|
}
|
|
|
|
// ─── Output schema for live mode ──────────────────────────────────────────────
|
|
|
|
const CURRENT_VIEW_SCHEMA = {
|
|
required: ["currentView", "whatChanged", "remainingUncertainties"],
|
|
forbidden: [
|
|
"recommendation",
|
|
"nextQuestion",
|
|
"confidence",
|
|
"materiality",
|
|
"decision",
|
|
"action",
|
|
"status",
|
|
"score",
|
|
"addedNodes",
|
|
"updatedNodes",
|
|
"resolvedNodeIds",
|
|
"selectedQuestion",
|
|
],
|
|
};
|
|
|
|
function validateOutputSchema(result) {
|
|
const errors = [];
|
|
|
|
for (const field of CURRENT_VIEW_SCHEMA.required) {
|
|
if (!(field in result)) {
|
|
errors.push(`Missing required field: ${field}`);
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(result.remainingUncertainties)) {
|
|
errors.push("remainingUncertainties must be an array");
|
|
} else if (result.remainingUncertainties.some((s) => typeof s !== "string")) {
|
|
errors.push("remainingUncertainties array items must all be strings");
|
|
}
|
|
|
|
for (const field of CURRENT_VIEW_SCHEMA.forbidden) {
|
|
if (field in result && typeof result[field] !== "undefined") {
|
|
errors.push(`Forbidden field present: ${field}`);
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
// ─── Inspect mode (default: zero model calls) ─────────────────────────────────
|
|
|
|
async function inspectApparatus() {
|
|
console.log("=== RTO.20A Derived Focused Current-View Apparatus (inspect-only) ===\n");
|
|
|
|
// Load and verify fragments
|
|
let fragment2, structuredResult2, fragment3, structuredResult3;
|
|
try {
|
|
const frags = await loadFragments();
|
|
fragment2 = frags.fragment2;
|
|
structuredResult2 = frags.structuredResult2;
|
|
fragment3 = frags.fragment3;
|
|
structuredResult3 = frags.structuredResult3;
|
|
} catch (e) {
|
|
console.log("ERROR: Cannot load fragments:", e.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Fragment verification
|
|
const f2Loaded = typeof structuredResult2 === "object" && structuredResult2 !== null;
|
|
const f3Loaded = typeof structuredResult3 === "object" && structuredResult3 !== null;
|
|
console.log("--- Fragment verification ---");
|
|
console.log(`Fragment 2 loaded: ${f2Loaded ? "YES" : "NO"}`);
|
|
console.log(`Fragment 3 loaded: ${f3Loaded ? "YES" : "NO"}`);
|
|
|
|
// Build prompt for inspection
|
|
const prompt = buildCurrentViewPrompt(structuredResult2, structuredResult3);
|
|
|
|
// Known relationship verification
|
|
const relationshipIncluded = prompt.includes(KNOWN_RELATIONSHIP);
|
|
console.log("\n--- Relationship ---");
|
|
console.log(`Known relationship included: ${relationshipIncluded ? "YES" : "NO"}`);
|
|
|
|
// Context boundary checks
|
|
console.log("\n--- Context boundaries ---");
|
|
const boundaryChecks = {
|
|
"Whole SituationGraph": !prompt.includes("SituationGraph"),
|
|
"Central case statement": !prompt.includes("central case") && !prompt.includes("centralStatement"),
|
|
"Turn history": !prompt.includes("turn history"),
|
|
"Other fragments": !prompt.includes("Fragment 1"),
|
|
"Prior focusedUnderstanding": !prompt.includes("focusedUnderstanding"),
|
|
"Prior decisionSignificance": !prompt.includes("decisionSignificance"),
|
|
"Graph mutation instructions": !prompt.includes("addedNodes") && !prompt.includes("updatedNodes"),
|
|
"Recommendation instructions": !prompt.includes("recommend what"),
|
|
};
|
|
|
|
for (const [name, passed] of Object.entries(boundaryChecks)) {
|
|
const status = passed ? "NO" : "VIOLATION";
|
|
console.log(` ${name}: ${status}`);
|
|
}
|
|
|
|
// Schema validation capability
|
|
const mockOutput = {
|
|
currentView: "test",
|
|
whatChanged: "test",
|
|
remainingUncertainties: ["test"],
|
|
};
|
|
const schemaErrors = validateOutputSchema(mockOutput);
|
|
console.log("\n--- Output schema ---");
|
|
console.log(`Required fields: ${CURRENT_VIEW_SCHEMA.required.join(", ")}`);
|
|
console.log(`Validation capability: ${schemaErrors.length === 0 ? "valid" : "errors"}`);
|
|
|
|
// Prompt size
|
|
console.log("\n--- Prompt size ---");
|
|
console.log(`inputCharacterCount: ${prompt.length}`);
|
|
|
|
// Live route
|
|
console.log("\n--- Live route ---");
|
|
console.log("node scripts/experimental/rto-derived-focused-current-view.mjs --live");
|
|
|
|
// Inspect summary
|
|
const allFragmentsOK = f2Loaded && f3Loaded;
|
|
const relationshipOK = relationshipIncluded;
|
|
const noViolations = Object.values(boundaryChecks).every((v) => v === true);
|
|
|
|
console.log("\n=== SUMMARY ===");
|
|
console.log(`Inspect live calls: 0`);
|
|
console.log(`Fragments valid: ${allFragmentsOK ? "YES" : "NO"}`);
|
|
console.log(`Boundary clean: ${noViolations ? "YES" : "VIOLATION DETECTED"}`);
|
|
|
|
const passed = allFragmentsOK && relationshipOK && noViolations;
|
|
console.log(`\nInspect result: ${passed ? "PASS" : "FAIL"}`);
|
|
|
|
if (!passed) {
|
|
console.log("\nApparatus cannot satisfy boundary without production changes.");
|
|
console.log("F — APPARATUS CANNOT ISOLATE DERIVED VIEW");
|
|
return null;
|
|
}
|
|
|
|
return { prompt, inputCharacterCount: prompt.length };
|
|
}
|
|
|
|
// ─── Live execution (exactly one model call) ──────────────────────────────────
|
|
|
|
async function executeLive() {
|
|
const baseUrl = process.env.OLLAMA_BASE_URL;
|
|
if (!baseUrl) {
|
|
console.error("OLLAMA_BASE_URL not set. Cannot execute live mode.");
|
|
process.exit(1);
|
|
}
|
|
if (baseUrl === "http://localhost:11434" || baseUrl === "http://127.0.0.1:11434") {
|
|
console.error("Refuses localhost fallback. OLLAMA_BASE_URL=" + baseUrl);
|
|
process.exit(1);
|
|
}
|
|
|
|
var z = (await import("zod")).z;
|
|
|
|
const { structuredResult2, structuredResult3 } = await loadFragments();
|
|
|
|
const prompt = buildCurrentViewPrompt(structuredResult2, structuredResult3);
|
|
|
|
// Schema for live output validation
|
|
const currentViewSchema = z.object({
|
|
currentView: z.string().min(1),
|
|
whatChanged: z.string().min(1),
|
|
remainingUncertainties: z.array(z.string()),
|
|
}).strict();
|
|
|
|
const startedAt = Date.now();
|
|
console.log("Live mode: sending to model...");
|
|
|
|
const provider = (await import(path.resolve(__dirname, "../../lib/llm/provider.js"))).getProvider();
|
|
const configEnv = await import(path.resolve(__dirname, "../../lib/config.js"));
|
|
const modelName = configEnv.assertConfig().OLLAMA_MODEL;
|
|
|
|
var raw = await provider.generateReconstruction(prompt, modelName);
|
|
var elapsedMs = Date.now() - startedAt;
|
|
|
|
console.log(`Model response received in ${elapsedMs}ms`);
|
|
|
|
const parsedResult = currentViewSchema.parse(raw);
|
|
|
|
// Validate no forbidden fields leaked in
|
|
const schemaErrors = validateOutputSchema(parsedResult);
|
|
if (schemaErrors.length > 0) {
|
|
throw new Error(`Live output schema validation errors: ${schemaErrors.join("; ")}`);
|
|
}
|
|
|
|
await fs.mkdir(resultsDir, { recursive: true });
|
|
|
|
const artifactPath = path.resolve(
|
|
resultsDir,
|
|
"rto-derived-focused-current-view-live.json"
|
|
);
|
|
|
|
const payload = {
|
|
apparatus: "rto-derived-focused-current-view.mjs",
|
|
experiment: "RTO.20A",
|
|
artifactType: "LIVE RESULT — Derived focused current view",
|
|
modelName: modelName,
|
|
elapsedMs: elapsedMs,
|
|
inputCharacterCount: prompt.length,
|
|
fragment2Path: FRAGMENT_2_PATH,
|
|
fragment3Path: FRAGMENT_3_PATH,
|
|
knownRelationshipIncluded: true,
|
|
wholeSituationGraphSupplied: false,
|
|
centralCaseStatementSupplied: false,
|
|
turnHistorySupplied: false,
|
|
outputSchema: {
|
|
currentView: typeof parsedResult.currentView === "string",
|
|
whatChanged: typeof parsedResult.whatChanged === "string",
|
|
remainingUncertainties: Array.isArray(parsedResult.remainingUncertainties),
|
|
},
|
|
derivedCurrentView: parsedResult,
|
|
};
|
|
|
|
await fs.writeFile(artifactPath, JSON.stringify(payload, null, 2));
|
|
console.log(`\nLive result written to: ${artifactPath}`);
|
|
console.log("\n=== Derived Current View ===");
|
|
console.log(JSON.stringify(parsedResult, null, 2));
|
|
|
|
return payload;
|
|
}
|
|
|
|
// ─── CLI entry point ──────────────────────────────────────────────────────────
|
|
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
// --live flag for exactly one live model call
|
|
if (args.includes("--live")) {
|
|
const result = await executeLive();
|
|
return;
|
|
}
|
|
|
|
// Default: inspect-only, zero model calls
|
|
console.log("RTO.20A — Derived Focused Current View");
|
|
console.log("Run with --live for one live model call.\n");
|
|
|
|
const result = await inspectApparatus();
|
|
if (result === null) {
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : String(error));
|
|
process.exit(1);
|
|
});
|