/** * 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", }); // ── 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)), 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 ─────────────────────────────────── 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), }); /** @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 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([]), }); /** @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", 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 || "", }); }