From 644108db71fb98f0f9e610316d80ffff2480e646 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 19 Aug 2026 15:27:14 +0100 Subject: [PATCH] test(experiment): checkpoint derived focused current-view apparatus --- .../rto-derived-focused-current-view.mjs | 413 ++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 scripts/experimental/rto-derived-focused-current-view.mjs diff --git a/scripts/experimental/rto-derived-focused-current-view.mjs b/scripts/experimental/rto-derived-focused-current-view.mjs new file mode 100644 index 0000000..22dab29 --- /dev/null +++ b/scripts/experimental/rto-derived-focused-current-view.mjs @@ -0,0 +1,413 @@ +/** + * 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); +});