import fs from "fs/promises"; import path from "path"; import dotenv from "dotenv"; import { z } from "zod"; import fixture from "../../tests/fixtures/live-product-launch-update-response.json" with { type: "json" }; import { getProvider } from "../../lib/llm/provider.js"; import { assertConfig } from "../../lib/config.js"; dotenv.config({ path: ".env.local" }); const TARGET_NODE_ID = "nxmeiab"; const FIXED_QUESTION = "What evidence would clarify whether competitors are actively developing similar products and how soon they might release them?"; const FIXED_ANSWER = "Two competitors have publicly announced products aimed at the same customer problem. One says it expects a beta within six months, while the other has not announced a release date. We do not yet know how closely either product matches ours."; const outputSchema = z .object({ targetNodeId: z.literal(TARGET_NODE_ID), observations: z.array(z.string()).default([]), uncertainties: z.array(z.string()).default([]), assumptions: z.array(z.string()).default([]), relationships: z .array( z.object({ from: z.string().min(1), to: z.string().min(1), type: z.string().min(1), rationale: z.string().min(1), }), ) .default([]), possibleFollowUpQuestions: z.array(z.string()).default([]), }) .strict() .superRefine((value, ctx) => { const forbiddenFields = [ "addedNodes", "updatedNodes", "removedNodes", "addedEdges", "removedEdges", "resolvedNodeIds", "activeUnknownNodeId", "selectedQuestion", ]; for (const key of forbiddenFields) { if (key in value) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: `Forbidden graph-mutation field present: ${key}`, path: [key], }); } } }); function assertRequiredEnv(name) { const value = process.env[name]; if (!value) { throw new Error( `Focused apparatus requires ${name}. Set it in .env.local. Found OLLAMA_BASE_URL=${process.env.OLLAMA_BASE_URL ?? "(missing)"}, OLLAMA_MODEL=${process.env.OLLAMA_MODEL ?? "(missing)"}`, ); } return value; } function buildFocusedPrompt({ targetNode, centralStatement, question, answer }) { return `You are performing focused answer deconstruction for one explicitly user-chosen investigation. Return exactly one JSON object. Return JSON only. This is NOT a graph update task. Do NOT output graph mutations. Do NOT output selection, ranking, ownership, recommendation, confidence, or next-best-question semantics. Do NOT include any of these fields: addedNodes, updatedNodes, removedNodes, addedEdges, removedEdges, resolvedNodeIds, activeUnknownNodeId, selectedQuestion. Required top-level fields: - targetNodeId - observations - uncertainties - assumptions - relationships - possibleFollowUpQuestions Field rules: - targetNodeId must be exactly "${TARGET_NODE_ID}" - observations: only statements directly supported by the answer - uncertainties: only things the answer explicitly leaves unknown or unclear - assumptions: include only if the answer itself relies on an assumption - relationships: only direct supported relationships among extracted items, each with { from, to, type, rationale } - possibleFollowUpQuestions: unresolved questions genuinely exposed by this answer, unranked Focused case context: - target label: ${targetNode.label} - target description: ${targetNode.description} - central case statement: ${centralStatement} Question: ${question} Answer: ${answer}`; } async function main() { const baseUrl = assertRequiredEnv("OLLAMA_BASE_URL"); const config = assertConfig(); const modelName = config.OLLAMA_MODEL; if (baseUrl === "http://localhost:11434" || baseUrl === "http://127.0.0.1:11434") { throw new Error( `Focused apparatus refuses localhost fallback. OLLAMA_BASE_URL=${baseUrl}`, ); } const graph = fixture.updatedSituationGraph; const targetNode = graph.nodes.find((node) => node.id === TARGET_NODE_ID); if (!targetNode || targetNode.kind !== "unknown" || targetNode.status === "resolved") { throw new Error("Fixed target node nxmeiab is not present as unresolved unknown in fixture."); } const prompt = buildFocusedPrompt({ targetNode, centralStatement: graph.centralStatement, question: FIXED_QUESTION, answer: FIXED_ANSWER, }); const provider = getProvider(); const startedAt = Date.now(); const raw = await provider.generateReconstruction(prompt, modelName); const elapsedMs = Date.now() - startedAt; const structuredResult = outputSchema.parse(raw); const artifact = { targetNodeId: TARGET_NODE_ID, targetLabel: targetNode.label, question: FIXED_QUESTION, answer: FIXED_ANSWER, modelName, elapsedMs, focusedContextSummary: [ "target node id and label", "target node description", "central case statement", "fixed question", "fixed answer", ], structuredResult, }; const artifactPath = path.resolve( "tests/experimental/artifacts/rto-focused-answer-deconstruction-smoke.json", ); await fs.mkdir(path.dirname(artifactPath), { recursive: true }); await fs.writeFile(artifactPath, JSON.stringify(artifact, null, 2)); console.log( JSON.stringify( { targetNodeId: artifact.targetNodeId, modelName: artifact.modelName, elapsedMs: artifact.elapsedMs, observations: artifact.structuredResult.observations, uncertainties: artifact.structuredResult.uncertainties, assumptions: artifact.structuredResult.assumptions, relationships: artifact.structuredResult.relationships, possibleFollowUpQuestions: artifact.structuredResult.possibleFollowUpQuestions, }, null, 2, ), ); } main().catch((error) => { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); });