Files
confidence-engine/lib/graph/schema.js
T
robbond 0d7ad5775c reasoning: add proposal-level answer meaning guard
- Add answerMeaning schema with supportCategory and resolutionGuidance enums
- Add pre-mutation guard that validates proposal alignment with answerMeaning
- Update prompt builder to instruct the model on answerMeaning contract
- Add tests for schema, guard logic, parsing defaults, and regression cases A-D
2026-08-09 07:07:21 +01:00

267 lines
8.8 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)),
resolutionGuidance: z.enum(Object.values(answerResolutionGuidance)),
})
.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),
});
/** @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,
});
}