/** * Situation Graph Case Orchestrator — manages the lifecycle of a case. * startCase builds initial graph from analysis; updateCase applies answers. */ import { analyseScenario } from "../analysis.js"; import { assertConfig } from "../config.js"; import { getProvider } from "../llm/provider.js"; import { makeGraph, startCaseRequestSchema, situationGraphSchema, updateCaseRequestSchema, } from "./schema.js"; import { buildInitialGraph, describeGraph } from "./builder.js"; import { applyValidatedProposal } from "./apply-proposal.js"; import { buildGraphUpdatePrompt } from "./prompt-builder.js"; import { parseGraphUpdateProposal } from "./update-proposal.js"; import { selectActiveUnknownCandidate, validateGraphReferences, } from "./utils.js"; function toValidationErrors(error) { return ( error?.errors?.map((issue) => ({ path: issue.path, message: issue.message, code: issue.code, })) ?? [{ message: "Validation failed" }] ); } function buildDiagnostics({ analysis, graph, graphReferenceValidation }) { return { promptVersion: analysis?.promptVersion ?? null, modelName: analysis?.modelName ?? null, responseDurationMs: analysis?.responseDurationMs ?? null, validationStatus: analysis?.validationStatus ?? "invalid", nodeCount: graph?.nodes?.length ?? 0, edgeCount: graph?.edges?.length ?? 0, graphReferenceValidation, compatibilityApplied: analysis?.compatibilityApplied ?? false, compatibilityChanges: analysis?.compatibilityChanges ?? [], compatibilityWarnings: analysis?.compatibilityWarnings ?? [], }; } export async function startCase(body) { const parsedRequest = startCaseRequestSchema.safeParse(body); if (!parsedRequest.success) { return { success: false, error: "Invalid start-case request", validationErrors: toValidationErrors(parsedRequest.error), statusCode: 400, }; } const { scenario, promptVersion } = parsedRequest.data; const analysis = await analyseScenario(scenario, { promptVersion }); if (!analysis.success) { return { success: false, error: analysis.error ?? "Scenario analysis failed", diagnostics: buildDiagnostics({ analysis, graph: null, graphReferenceValidation: null, }), analysisErrors: analysis.errors ?? undefined, rawResponse: analysis.rawResponse ?? undefined, statusCode: Number(analysis.statusCode) || 502, }; } const initialGraph = buildInitialGraph({ reconstruction: analysis.reconstruction, evidence: analysis.evidence, }); const currentSummary = describeGraph(initialGraph); const activeUnknownNodeId = selectActiveUnknownCandidate( { ...initialGraph, resolvedNodeIds: [], }, [], )?.nodeId ?? null; const situationGraph = makeGraph({ centralStatement: scenario, nodes: initialGraph.nodes, edges: initialGraph.edges, activeUnknownNodeId, resolvedNodeIds: [], currentSummary, }); situationGraphSchema.parse(situationGraph); const graphReferenceValidation = validateGraphReferences(situationGraph); if (!graphReferenceValidation.valid) { return { success: false, error: "Situation graph reference validation failed", diagnostics: buildDiagnostics({ analysis, graph: situationGraph, graphReferenceValidation, }), validationErrors: graphReferenceValidation.errors, statusCode: 500, }; } return { success: true, situationGraph, selectedQuestion: analysis.nextQuestion ?? null, diagnostics: buildDiagnostics({ analysis, graph: situationGraph, graphReferenceValidation, }), }; } export async function updateCase() { return updateCaseWithDependencies(...arguments); } function sanitiseErrorMessage(error, fallbackMessage) { if (typeof error?.message === "string" && error.message.trim().length > 0) { return error.message; } return fallbackMessage; } async function updateCaseWithDependencies(body, dependencies = {}) { const parsedRequest = updateCaseRequestSchema.safeParse(body); if (!parsedRequest.success) { return { success: false, stage: "request_validation", error: "Invalid update-case request", validationErrors: toValidationErrors(parsedRequest.error), statusCode: 400, }; } const { situationGraph, previousQuestion, answer, promptVersion } = parsedRequest.data; const graphSchemaValidation = situationGraphSchema.safeParse(situationGraph); const graphReferenceValidation = validateGraphReferences(situationGraph); if (!graphSchemaValidation.success || !graphReferenceValidation.valid) { return { success: false, stage: "graph_validation", error: "Invalid situation graph", graphValidationErrors: [ ...(!graphSchemaValidation.success ? toValidationErrors(graphSchemaValidation.error) : []), ...(!graphReferenceValidation.valid ? graphReferenceValidation.errors : []), ], statusCode: 400, }; } const buildPrompt = dependencies.buildGraphUpdatePrompt ?? buildGraphUpdatePrompt; const parseProposal = dependencies.parseGraphUpdateProposal ?? parseGraphUpdateProposal; const applyProposalUpdate = dependencies.applyValidatedProposal ?? applyValidatedProposal; const shouldApplyProposal = dependencies.applyProposal === true; let modelName = null; let rawResponse; const startedAt = Date.now(); try { const config = dependencies.config ?? assertConfig(); modelName = config.OLLAMA_MODEL; const prompt = buildPrompt({ situationGraph, previousQuestion, answer, promptVersion, }); const provider = dependencies.provider ?? getProvider(); rawResponse = await provider.generateReconstruction(prompt, modelName); } catch (error) { return { success: false, stage: "provider", error: "Graph update proposal generation failed", providerErrors: [ sanitiseErrorMessage( error, "Provider failed to generate graph update proposal", ), ], diagnostics: { promptVersion: promptVersion ?? null, modelName, responseDurationMs: Date.now() - startedAt, normalisationsApplied: [], }, statusCode: 502, }; } const parsedProposal = parseProposal(rawResponse); const responseDurationMs = Date.now() - startedAt; if (!parsedProposal.success) { return { success: false, stage: "proposal_validation", error: "Invalid graph update proposal", proposalErrors: parsedProposal.errors, diagnostics: { promptVersion: promptVersion ?? null, modelName, responseDurationMs, normalisationsApplied: parsedProposal.normalisationsApplied, }, statusCode: 502, }; } if (shouldApplyProposal) { const applicationResult = applyProposalUpdate({ situationGraph, proposal: parsedProposal.proposal, }); if (!applicationResult.success) { return { success: false, stage: applicationResult.stage, errors: applicationResult.errors, diagnostics: { promptVersion: promptVersion ?? null, modelName, responseDurationMs, normalisationsApplied: parsedProposal.normalisationsApplied, graphNodeCount: situationGraph.nodes.length, graphEdgeCount: situationGraph.edges.length, }, statusCode: applicationResult.stage === "application" || applicationResult.stage === "result_validation" ? 500 : 400, }; } return { success: true, stage: "update_applied", updatedSituationGraph: applicationResult.updatedSituationGraph, proposal: applicationResult.graphUpdate, affectedNodeIds: applicationResult.affectedNodeIds, resolvedUnknownNodeIds: applicationResult.resolvedUnknownNodeIds, previousActiveUnknownNodeId: applicationResult.previousActiveUnknownNodeId, newActiveUnknownNodeId: applicationResult.newActiveUnknownNodeId, changesApplied: applicationResult.changesApplied, diagnostics: { promptVersion: promptVersion ?? null, modelName, responseDurationMs, normalisationsApplied: parsedProposal.normalisationsApplied, graphNodeCount: applicationResult.updatedSituationGraph.nodes.length, graphEdgeCount: applicationResult.updatedSituationGraph.edges.length, }, }; } return { success: true, stage: "proposal_ready", proposal: parsedProposal.proposal, diagnostics: { promptVersion: promptVersion ?? null, modelName, responseDurationMs, normalisationsApplied: parsedProposal.normalisationsApplied, graphNodeCount: situationGraph.nodes.length, graphEdgeCount: situationGraph.edges.length, }, }; }