- Add structuralActionRequired field to graphUpdateSchema (optional boolean nullable) - Validate declaration consistency in validateGraphUpdate(): - true requires meaningful mutation (addedNodes/updatedNodes/addedEdges) - false permits intentional no-op when userSupportedMeaning populated - null/absent with meaning → reject - true/false mismatch on output shape → reject - preserve legacy no-op guard for non-contract paths - Update prompt-builder: add field to required list, insert contract section between rules and Additional Guidance with two mandatory sentences - 50 new tests: schema validation (4), prompt builder content checks (10), utils contract matrix (10), plus 26 existing suite migrations All 197 graph tests pass.
274 lines
8.9 KiB
JavaScript
274 lines
8.9 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",
|
|
});
|
|
|
|
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",
|
|
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(),
|
|
});
|
|
|
|
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,
|
|
});
|
|
}
|