302 lines
10 KiB
JavaScript
302 lines
10 KiB
JavaScript
/**
|
|
* 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",
|
|
option: "option",
|
|
});
|
|
|
|
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<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",
|
|
contained_in: "contained_in",
|
|
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 ───────────────────────────────────
|
|
|
|
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<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 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<typeof graphUpdateSchema>} GraphUpdate */
|
|
|
|
// ── API request / response schemas ───────────────────
|
|
|
|
export const startCaseRequestSchema = z.object({
|
|
scenario: z.string().min(1).max(10000),
|
|
promptVersion: z.string().optional(),
|
|
});
|
|
|
|
/** Legacy schema — unchanged contract for existing consumers. */
|
|
export const updateCaseRequestSchema = z.object({
|
|
situationGraph: situationGraphSchema,
|
|
previousQuestion: z.string().min(1),
|
|
answer: z.string().min(1).max(5000),
|
|
promptVersion: z.string().optional(),
|
|
findings: z.array(
|
|
z.object({
|
|
id: z.string().min(1),
|
|
proposition: z.string().min(1),
|
|
status: z.literal("provisional"),
|
|
userDisposition: z.enum(["agree", "not_quite", "not_relevant"]).nullable(),
|
|
originatingTargetNodeId: z.string().min(1),
|
|
contributionId: z.string().min(1),
|
|
sourceObservation: z.string().min(1),
|
|
}),
|
|
).optional(),
|
|
});
|
|
|
|
/** Extended schema with optional fields accepted by the route for all requests. */
|
|
export const updateCaseRequestSchemaExtended = updateCaseRequestSchema.extend({
|
|
targetNodeId: z.string().optional(),
|
|
contributions: z.any().array().optional(),
|
|
});
|
|
|
|
/** Minimal schema for episode-mode requests (legacy fields not required). */
|
|
export const updateCaseEpisodeRequestSchema = z.object({
|
|
situationGraph: situationGraphSchema,
|
|
targetNodeId: z.string().min(1),
|
|
contributions: z.any().array().optional(),
|
|
findings: z.any().array().optional(),
|
|
}).passthrough();
|
|
|
|
// ── 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,
|
|
});
|
|
}
|