/** * Situation Graph schema — v0.4 experiment. * Defines types for an evolving multi-turn situation reconstruction graph. * Plain TypeScript interfaces implemented as Zod schemas for runtime validation. */ import { z } from "zod"; // ── Enums ──────────────────────────────────────────── export const SituationKind = /** @type {const} */ ({ observation: "observation", reported_claim: "reported_claim", metric: "metric", state: "state", transition: "transition", relationship: "relationship", assumption: "assumption", unknown: "unknown", conclusion: "conclusion", }); export const SituationStatus = /** @type {const} */ ({ known: "known", unknown: "unknown", provisional: "provisional", supported: "supported", weakened: "weakened", contradicted: "contradicted", resolved: "resolved", }); export const ConfidenceLevel = /** @type {const} */ ({ low: "low", medium: "medium", high: "high", }); export const CompletenessStatus = /** @type {const} */ ({ empty: "empty", partial: "partial", complete: "complete", }); export const confidenceAssessmentSchema = z .object({ evidenceConfidence: z.enum(Object.values(ConfidenceLevel)), completenessStatus: z.enum(Object.values(CompletenessStatus)), conclusionConfidence: z.enum(Object.values(ConfidenceLevel)), }) .strict(); // ── SituationNode ──────────────────────────────────── export const situationNodeSchema = z.object({ id: z.string().min(1), label: z.string().min(1), description: z.string().min(1), kind: z.enum(Object.values(SituationKind)), status: z.enum(Object.values(SituationStatus)), confidence: z.enum(Object.values(ConfidenceLevel)), confidenceAssessment: confidenceAssessmentSchema.optional(), value: z.union([z.string(), z.number(), z.null()]).nullable().optional(), unit: z.string().nullable().optional(), evidenceIds: z.array(z.string()).default([]), dependsOn: z.array(z.string()).default([]), affects: z.array(z.string()).default([]), parentId: z.string().nullable().optional(), childIds: z.array(z.string()).default([]), }); /** @typedef {z.infer} SituationNode */ // ── SituationEdge ──────────────────────────────────── export const SituationRelationship = /** @type {const} */ ({ supports: "supports", weakens: "weakens", contradicts: "contradicts", depends_on: "depends_on", causes: "causes", may_cause: "may_cause", measures: "measures", compares_with: "compares_with", updates: "updates", other: "other", }); export const situationEdgeSchema = z.object({ id: z.string().min(1), fromNodeId: z.string().min(1), toNodeId: z.string().min(1), relationship: z.enum(Object.values(SituationRelationship)), confidence: z.enum(Object.values(ConfidenceLevel)), description: z.string().min(1), }); /** @typedef {z.infer} SituationEdge */ // ── SituationGraph ─────────────────────────────────── const reasoningStageSchema = z.object({ stage: z.string().min(1), status: z.string().min(1), outcome: z.string().min(1), }); export const reasoningStateSchema = z .object({ comparabilityStatus: z.string().min(1).nullable().optional(), comparabilityReason: z.string().min(1).nullable().optional(), comparabilityEvidence: z.array(z.string()).default([]), relationshipStatus: z.string().min(1).nullable().optional(), relationshipReason: z.string().min(1).nullable().optional(), relationshipAssessed: z.boolean().optional(), contradictionReasoningAllowed: z.boolean().optional(), reasoningStages: z.array(reasoningStageSchema).default([]), }) .strict(); export const situationGraphSchema = z.object({ centralStatement: z.string().min(1), nodes: z.array(situationNodeSchema).min(1), edges: z.array(situationEdgeSchema).default([]), activeUnknownNodeId: z.string().nullable(), resolvedNodeIds: z.array(z.string()).default([]), currentSummary: z.string().min(1), reasoningState: reasoningStateSchema.optional(), }); /** @typedef {z.infer} SituationGraph */ // ── GraphUpdate (change set) ──────────────────────── const graphUpdateNodeChangeSchema = z.object({ nodeId: z.string().min(1), previousStatus: z.enum(Object.values(SituationStatus)).nullable().optional(), newStatus: z.enum(Object.values(SituationStatus)).nullable().optional(), previousValue: z .union([z.string(), z.number(), z.null()]) .nullable() .optional(), newValue: z.union([z.string(), z.number(), z.null()]).nullable().optional(), reason: z.string().min(1), }); export const answerSupportCategory = /** @type {const} */ ({ relative_priority_only: "relative_priority_only", conditional_tradeoff: "conditional_tradeoff", uncertain: "uncertain", explicit_hard_constraint: "explicit_hard_constraint", other: "other", }); export const answerResolutionGuidance = /** @type {const} */ ({ must_remain_unresolved: "must_remain_unresolved", may_resolve: "may_resolve", must_resolve: "must_resolve", }); export const answerMeaningSchema = z .object({ userSupportedMeaning: z.string().min(1), possibleInference: z.string().nullable().optional(), supportCategory: z .enum(Object.values(answerSupportCategory)) .nullable() .optional(), resolutionGuidance: z .enum(Object.values(answerResolutionGuidance)) .nullable() .optional(), }) .strict(); export const selectedQuestionSchema = z .object({ nodeId: z.string().min(1), question: z.string().min(1), reason: z.string().min(1), }) .strict(); export const graphUpdateSchema = z.object({ addedNodes: z.array(situationNodeSchema).default([]), updatedNodes: z.array(graphUpdateNodeChangeSchema).default([]), addedEdges: z.array(situationEdgeSchema).default([]), removedEdgeIds: z.array(z.string()).default([]), resolvedUnknownNodeIds: z.array(z.string()).default([]), affectedNodeIds: z.array(z.string()).default([]), selectedQuestion: selectedQuestionSchema.nullable().default(null), answerMeaning: answerMeaningSchema.nullable().default(null), structuralActionRequired: z.boolean().nullable().optional(), }); /** @typedef {z.infer} GraphUpdate */ // ── API request / response schemas ─────────────────── export const startCaseRequestSchema = z.object({ scenario: z.string().min(1).max(10000), promptVersion: z.string().optional(), }); export const updateCaseRequestSchema = z.object({ situationGraph: situationGraphSchema, previousQuestion: z.string().min(1), answer: z.string().min(1).max(5000), promptVersion: z.string().optional(), }); // ── Helpers ────────────────────────────────────────── /** Generate a short deterministic ID from a label */ export function makeNodeId(label) { return "n" + Math.abs(hashString(label)).toString(36).slice(0, 7); } function hashString(str) { let h = 0; for (let i = 0; i < str.length; i++) { h = (Math.imul(31, h) + str.charCodeAt(i)) | 0; } return h; } /** Create a minimal valid node — used in tests and fixtures */ export function makeNode(opts) { const id = opts.id || makeNodeId(opts.label); return situationNodeSchema.parse({ id, label: opts.label, description: opts.description ?? opts.label, kind: opts.kind ?? "observation", status: opts.status ?? "unknown", confidence: opts.confidence ?? "medium", confidenceAssessment: opts.confidenceAssessment, value: opts.value ?? null, unit: opts.unit ?? null, evidenceIds: opts.evidenceIds ?? [], dependsOn: opts.dependsOn ?? [], affects: opts.affects ?? [], parentId: opts.parentId ?? null, childIds: opts.childIds ?? [], }); } /** Create a minimal valid edge — used in tests and fixtures */ export function makeEdge(opts) { return situationEdgeSchema.parse({ id: opts.id || "e" + opts.fromNodeId.slice(0, 3) + "-" + opts.toNodeId.slice(0, 3), fromNodeId: opts.fromNodeId, toNodeId: opts.toNodeId, relationship: opts.relationship ?? "supports", confidence: opts.confidence ?? "medium", description: opts.description ?? opts.fromNodeId + " -> " + opts.toNodeId, }); } /** Build a minimal valid graph structure */ export function makeGraph(opts) { return situationGraphSchema.parse({ centralStatement: opts.centralStatement || "", nodes: opts.nodes ?? [], edges: opts.edges ?? [], activeUnknownNodeId: opts.activeUnknownNodeId ?? null, resolvedNodeIds: opts.resolvedNodeIds ?? [], currentSummary: opts.currentSummary || "", reasoningState: opts.reasoningState, }); }