feat: back next questions with explicit graph unknowns

This commit is contained in:
2026-08-02 19:03:07 +01:00
parent 25a989450c
commit b1c633ba5c
7 changed files with 310 additions and 40 deletions
+163 -21
View File
@@ -1,11 +1,15 @@
import { describeGraph } from "./builder.js";
import {
buildReasoningState,
classifyObservationRelationship,
COMPARABILITY_REASONING_NODE_ID,
formulateQuestion,
formulateTieResolutionQuestion,
} from "./question-formulator.js";
import { graphUpdateSchema, situationGraphSchema } from "./schema.js";
import {
graphUpdateSchema,
makeNodeId,
situationGraphSchema,
} from "./schema.js";
import {
applyGraphUpdate,
detectDuplicateNodeIds,
@@ -428,6 +432,124 @@ function buildChangesApplied(proposal, affectedNodeIds) {
};
}
function buildEmergentReasoningUnknownLabel(graph) {
const central = String(graph?.centralStatement || "these observations")
.trim()
.replace(/[.?!:;]+$/g, "");
return `Explanation for why ${central}`;
}
function findEquivalentEmergentUnknown(graph, label, description) {
const targetId = makeNodeId(label);
const targetTexts = [normaliseText(label), normaliseText(description)].filter(
Boolean,
);
return (graph.nodes || []).find((node) => {
if (
node.kind !== "unknown" ||
(graph.resolvedNodeIds || []).includes(node.id)
) {
return false;
}
if (node.id === targetId) {
return true;
}
const nodeTexts = [
normaliseText(node.label),
normaliseText(node.description),
].filter(Boolean);
return targetTexts.some((text) => nodeTexts.includes(text));
});
}
function buildEmergentReasoningUnknown(graph, relationshipAssessment) {
if (!relationshipAssessment?.relationshipAssessed) {
return null;
}
if (!relationshipAssessment.questionRequired) {
return null;
}
if (
![
"potentially_related",
"insufficient_information",
"contradictory",
].includes(relationshipAssessment.relationshipStatus)
) {
return null;
}
const label = buildEmergentReasoningUnknownLabel(graph);
const description =
"Need to understand what change or event could explain why these observations differ, because that is needed to investigate their relationship.";
const existingNode = findEquivalentEmergentUnknown(graph, label, description);
if (existingNode) {
return {
created: false,
node: existingNode,
edges: [],
reason:
"Reused an existing unresolved reasoning unknown for the next investigation stage.",
};
}
const observationNodes = (graph.nodes || []).filter(
(node) => node.kind === "observation" && node.status === "supported",
);
const relationshipNode = (graph.nodes || []).find(
(node) => node.kind === "relationship" && node.status === "supported",
);
const nodeId = makeNodeId(label);
const relatedNodeIds = relationshipNode
? [relationshipNode.id]
: observationNodes.slice(0, 2).map((node) => node.id);
if (relatedNodeIds.length === 0) {
return null;
}
const node = {
id: nodeId,
label,
description,
kind: "unknown",
status: "unknown",
confidence: "medium",
value: null,
unit: null,
evidenceIds: [],
dependsOn: relatedNodeIds,
affects: [],
parentId: relationshipNode?.id ?? null,
childIds: [],
};
const edges = relatedNodeIds.map((relatedNodeId) => ({
id: `e-${relatedNodeId.slice(0, 6)}-${nodeId.slice(0, 6)}`,
fromNodeId: relatedNodeId,
toNodeId: nodeId,
relationship:
relationshipNode?.id === relatedNodeId ? "depends_on" : "other",
confidence: "medium",
description:
"This unresolved explanation arises from the now-assessed relationship between the observations.",
}));
return {
created: true,
node,
edges,
reason:
"Created a new unresolved reasoning unknown so the next justified question is backed by the graph.",
};
}
function isComparabilityQuestion(question) {
const text = String(question || "").toLowerCase();
return (
@@ -643,6 +765,36 @@ export function applyValidatedProposal({
resolvedUnknownNodeIds: validatedProposal.resolvedUnknownNodeIds,
});
const provisionalApplied = applyGraphUpdate(graphSnapshot, proposalSnapshot);
if (!provisionalApplied.success) {
return {
success: false,
stage: "application",
errors: provisionalApplied.errors,
};
}
const provisionalGraph = {
...graphSnapshot,
nodes: provisionalApplied.nodes,
edges: provisionalApplied.edges,
resolvedNodeIds: provisionalApplied.resolvedNodeIds,
};
provisionalGraph.reasoningState = buildReasoningState(
provisionalGraph,
reasoningResolution.reasoningStateOverride,
);
const relationshipAssessment =
classifyObservationRelationship(provisionalGraph);
const emergentReasoningUnknown = buildEmergentReasoningUnknown(
provisionalGraph,
relationshipAssessment,
);
if (emergentReasoningUnknown?.created) {
proposalSnapshot.addedNodes.push(emergentReasoningUnknown.node);
proposalSnapshot.addedEdges.push(...emergentReasoningUnknown.edges);
}
const applied = applyGraphUpdate(graphSnapshot, proposalSnapshot);
if (!applied.success) {
return {
@@ -738,7 +890,8 @@ export function applyValidatedProposal({
? {
nodeId: null,
tiedCandidateIds: deterministicSelection.tiedCandidateIds,
...formulateTieResolutionQuestion({ graph: updatedSituationGraph }),
question: null,
reason: deterministicSelection.reason,
}
: deterministicSelection?.status === "selected"
? {
@@ -749,21 +902,7 @@ export function applyValidatedProposal({
strategy: formulatedQuestion?.strategy,
investigationStrategy: formulatedQuestion?.investigationStrategy,
}
: (() => {
const relationshipFallback = formulateTieResolutionQuestion({
graph: updatedSituationGraph,
});
return relationshipFallback?.question
? {
nodeId: null,
question: relationshipFallback.question,
reason: relationshipFallback.reason,
strategy: relationshipFallback.strategy,
investigationStrategy:
relationshipFallback.investigationStrategy,
}
: null;
})();
: null;
const resultGraphValidation = situationGraphSchema.safeParse(
updatedSituationGraph,
@@ -809,14 +948,17 @@ export function applyValidatedProposal({
return {
success: true,
updatedSituationGraph,
graphUpdate: validatedProposal,
graphUpdate: proposalSnapshot,
affectedNodeIds,
resolvedUnknownNodeIds: validatedProposal.resolvedUnknownNodeIds,
resolvedUnknownNodeIds: proposalSnapshot.resolvedUnknownNodeIds,
resolvedReasoningNodeIds: reasoningResolution.resolvedReasoningNodeIds,
emergentReasoningNodeCreated: Boolean(emergentReasoningUnknown?.created),
emergentReasoningNodeId: emergentReasoningUnknown?.node?.id ?? null,
emergentReasoningNodeReason: emergentReasoningUnknown?.reason ?? null,
previousActiveUnknownNodeId,
newActiveUnknownNodeId,
selectedQuestion: finalSelectedQuestion,
changesApplied: buildChangesApplied(validatedProposal, affectedNodeIds),
changesApplied: buildChangesApplied(proposalSnapshot, affectedNodeIds),
graphReferenceValidation: resultReferenceValidation,
previousReasoningState: reasoningResolution.previousReasoningState,
reasoningState: nextReasoningState,