feat: add situation graph foundation
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 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<typeof situationNodeSchema>} 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<typeof situationEdgeSchema>} 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<typeof situationGraphSchema>} 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<typeof graphUpdateSchema>} 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 || "",
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user