feat: separate confidence from reasoning completeness

This commit is contained in:
2026-08-03 07:05:20 +01:00
parent 49765e95a0
commit 1d64144e01
8 changed files with 489 additions and 19 deletions
+170 -14
View File
@@ -439,6 +439,30 @@ function appendUniqueValue(values = [], nextValue) {
: values;
}
function getNodeConfidenceAssessment(node) {
return (
node?.confidenceAssessment || {
evidenceConfidence: node?.confidence ?? "medium",
completenessStatus:
node?.status === "resolved"
? "complete"
: node?.status === "provisional"
? "partial"
: "empty",
conclusionConfidence:
node?.status === "resolved"
? (node?.confidence ?? "high")
: node?.status === "provisional"
? (node?.confidence ?? "medium")
: "low",
}
);
}
function confidenceFromAssessment(assessment) {
return assessment?.conclusionConfidence ?? "medium";
}
function upsertProposalNodeUpdate(proposalSnapshot, update) {
const existing = proposalSnapshot.updatedNodes.find(
(candidate) => candidate.nodeId === update.nodeId,
@@ -521,16 +545,6 @@ function buildAncestorChain(graph, node) {
queue.push(candidate.id);
}
}
for (const edge of graph.edges || []) {
if (
edge.fromNodeId !== parentNode.id &&
edge.toNodeId === parentNode.id &&
edge.relationship === "depends_on"
) {
queue.push(edge.fromNodeId);
}
}
}
return chain;
@@ -562,28 +576,83 @@ function computeParentProgressState(graph, parentNode) {
const progressedChildren = childUnknowns.filter((child) =>
["resolved", "provisional"].includes(child.status),
);
const contradictoryChildren = childUnknowns.filter(
(child) => child.status === "contradicted",
);
const totalChildren = childUnknowns.length;
const resolvedCount = resolvedChildren.length;
const unresolvedCount = childUnknowns.filter(
(child) => child.status !== "resolved",
).length;
const beforeAssessment = getNodeConfidenceAssessment(parentNode);
if (totalChildren === 0) {
const nextAssessment = {
evidenceConfidence: beforeAssessment.evidenceConfidence,
completenessStatus: beforeAssessment.completenessStatus,
conclusionConfidence: beforeAssessment.conclusionConfidence,
};
return {
totalChildren,
resolvedChildren,
progressedChildren,
contradictoryChildren,
nextStatus: parentNode.status,
nextConfidence: parentNode.confidence,
nextConfidence: confidenceFromAssessment(nextAssessment),
nextConfidenceAssessment: nextAssessment,
parentResolved: parentNode.status === "resolved",
confidenceCapReason: "no_child_structure",
reason: "Parent has no child unknowns to aggregate.",
};
}
let nextAssessment;
let confidenceCapReason;
if (contradictoryChildren.length > 0) {
nextAssessment = {
evidenceConfidence: resolvedCount > 0 ? "medium" : "low",
completenessStatus: resolvedCount === 0 ? "empty" : "partial",
conclusionConfidence: "low",
};
confidenceCapReason = "contradictory_direct_children";
} else if (resolvedCount === 0) {
nextAssessment = {
evidenceConfidence: "low",
completenessStatus: "empty",
conclusionConfidence: "low",
};
confidenceCapReason = "no_resolved_direct_children";
} else if (resolvedCount < totalChildren) {
nextAssessment = {
evidenceConfidence: "high",
completenessStatus: "partial",
conclusionConfidence: "medium",
};
confidenceCapReason = "unresolved_direct_children_cap_conclusion";
} else {
nextAssessment = {
evidenceConfidence: "high",
completenessStatus: "complete",
conclusionConfidence: "high",
};
confidenceCapReason = null;
}
if (resolvedChildren.length === totalChildren) {
return {
totalChildren,
resolvedChildren,
progressedChildren,
contradictoryChildren,
nextStatus: "resolved",
nextConfidence: "high",
nextConfidence: confidenceFromAssessment(nextAssessment),
nextConfidenceAssessment: nextAssessment,
parentResolved: true,
resolvedDirectChildren: resolvedCount,
unresolvedDirectChildren: unresolvedCount,
contradictoryDirectChildren: contradictoryChildren.length,
confidenceCapReason,
reason:
"All direct child unknowns are resolved, so the parent can now resolve deterministically.",
};
@@ -594,9 +663,15 @@ function computeParentProgressState(graph, parentNode) {
totalChildren,
resolvedChildren,
progressedChildren,
contradictoryChildren,
nextStatus: "provisional",
nextConfidence: "high",
nextConfidence: confidenceFromAssessment(nextAssessment),
nextConfidenceAssessment: nextAssessment,
parentResolved: false,
resolvedDirectChildren: resolvedCount,
unresolvedDirectChildren: unresolvedCount,
contradictoryDirectChildren: contradictoryChildren.length,
confidenceCapReason,
reason:
"At least one direct child has been progressed, so the parent becomes provisional but remains unresolved until all direct children are resolved.",
};
@@ -606,9 +681,15 @@ function computeParentProgressState(graph, parentNode) {
totalChildren,
resolvedChildren,
progressedChildren,
contradictoryChildren,
nextStatus: parentNode.status,
nextConfidence: parentNode.confidence,
nextConfidence: confidenceFromAssessment(nextAssessment),
nextConfidenceAssessment: nextAssessment,
parentResolved: parentNode.status === "resolved",
resolvedDirectChildren: resolvedCount,
unresolvedDirectChildren: unresolvedCount,
contradictoryDirectChildren: contradictoryChildren.length,
confidenceCapReason,
reason: "No direct child progress exists yet for the parent.",
};
}
@@ -635,6 +716,17 @@ export function propagateResolvedChildEvidence({
parentStatusAfter: null,
parentConfidenceBefore: null,
parentConfidenceAfter: null,
evidenceConfidenceBefore: null,
evidenceConfidenceAfter: null,
completenessBefore: null,
completenessAfter: null,
conclusionConfidenceBefore: null,
conclusionConfidenceAfter: null,
resolvedDirectChildren: 0,
unresolvedDirectChildren: 0,
contradictoryDirectChildren: 0,
confidenceCapReason: null,
ancestorPropagationStoppedReason: "no_resolved_child_propagation_needed",
affectedAncestorIds: [],
nextSelectedSibling: null,
parentResolved: false,
@@ -646,6 +738,7 @@ export function propagateResolvedChildEvidence({
syncParentChildReferences(graph);
const propagationEvents = [];
const affectedAncestorIds = new Set();
let ancestorPropagationStoppedReason = "no_ancestor_state_changed";
for (const resolvedChildNode of resolvedChildNodes) {
const liveChildNode = graph.nodes.find(
@@ -662,10 +755,24 @@ export function propagateResolvedChildEvidence({
for (const ancestorNode of ancestorChain) {
const beforeStatus = ancestorNode.status;
const beforeConfidence = ancestorNode.confidence;
const beforeAssessment = getNodeConfidenceAssessment(ancestorNode);
const progressState = computeParentProgressState(graph, ancestorNode);
ancestorNode.status = progressState.nextStatus;
ancestorNode.confidence = progressState.nextConfidence;
ancestorNode.confidenceAssessment =
progressState.nextConfidenceAssessment;
if (
beforeStatus === progressState.nextStatus &&
beforeConfidence === progressState.nextConfidence &&
JSON.stringify(beforeAssessment) ===
JSON.stringify(progressState.nextConfidenceAssessment)
) {
continue;
}
ancestorPropagationStoppedReason = "ancestor_state_changed";
if (progressState.parentResolved) {
ensureResolvedUnknownId(proposalSnapshot, ancestorNode.id);
@@ -688,6 +795,19 @@ export function propagateResolvedChildEvidence({
parentStatusAfter: progressState.nextStatus,
parentConfidenceBefore: beforeConfidence,
parentConfidenceAfter: progressState.nextConfidence,
evidenceConfidenceBefore: beforeAssessment.evidenceConfidence,
evidenceConfidenceAfter:
progressState.nextConfidenceAssessment.evidenceConfidence,
completenessBefore: beforeAssessment.completenessStatus,
completenessAfter:
progressState.nextConfidenceAssessment.completenessStatus,
conclusionConfidenceBefore: beforeAssessment.conclusionConfidence,
conclusionConfidenceAfter:
progressState.nextConfidenceAssessment.conclusionConfidence,
resolvedDirectChildren: progressState.resolvedDirectChildren,
unresolvedDirectChildren: progressState.unresolvedDirectChildren,
contradictoryDirectChildren: progressState.contradictoryDirectChildren,
confidenceCapReason: progressState.confidenceCapReason,
parentResolved: progressState.parentResolved,
reason: progressState.reason,
});
@@ -717,6 +837,17 @@ export function propagateResolvedChildEvidence({
parentStatusAfter: firstEvent?.parentStatusAfter ?? null,
parentConfidenceBefore: firstEvent?.parentConfidenceBefore ?? null,
parentConfidenceAfter: firstEvent?.parentConfidenceAfter ?? null,
evidenceConfidenceBefore: firstEvent?.evidenceConfidenceBefore ?? null,
evidenceConfidenceAfter: firstEvent?.evidenceConfidenceAfter ?? null,
completenessBefore: firstEvent?.completenessBefore ?? null,
completenessAfter: firstEvent?.completenessAfter ?? null,
conclusionConfidenceBefore: firstEvent?.conclusionConfidenceBefore ?? null,
conclusionConfidenceAfter: firstEvent?.conclusionConfidenceAfter ?? null,
resolvedDirectChildren: firstEvent?.resolvedDirectChildren ?? 0,
unresolvedDirectChildren: firstEvent?.unresolvedDirectChildren ?? 0,
contradictoryDirectChildren: firstEvent?.contradictoryDirectChildren ?? 0,
confidenceCapReason: firstEvent?.confidenceCapReason ?? null,
ancestorPropagationStoppedReason,
affectedAncestorIds: [...affectedAncestorIds],
nextSelectedSibling:
siblingSelection?.status === "selected" ? siblingSelection.nodeId : null,
@@ -1767,6 +1898,20 @@ export function applyValidatedProposal({
const parentStatusAfter = propagationResult.parentStatusAfter;
const parentConfidenceBefore = propagationResult.parentConfidenceBefore;
const parentConfidenceAfter = propagationResult.parentConfidenceAfter;
const evidenceConfidenceBefore = propagationResult.evidenceConfidenceBefore;
const evidenceConfidenceAfter = propagationResult.evidenceConfidenceAfter;
const completenessBefore = propagationResult.completenessBefore;
const completenessAfter = propagationResult.completenessAfter;
const conclusionConfidenceBefore =
propagationResult.conclusionConfidenceBefore;
const conclusionConfidenceAfter = propagationResult.conclusionConfidenceAfter;
const resolvedDirectChildren = propagationResult.resolvedDirectChildren;
const unresolvedDirectChildren = propagationResult.unresolvedDirectChildren;
const contradictoryDirectChildren =
propagationResult.contradictoryDirectChildren;
const confidenceCapReason = propagationResult.confidenceCapReason;
const ancestorPropagationStoppedReason =
propagationResult.ancestorPropagationStoppedReason;
const affectedAncestorIds = propagationResult.affectedAncestorIds;
const nextSelectedSibling = propagationResult.nextSelectedSibling;
const parentResolved = propagationResult.parentResolved;
@@ -1894,6 +2039,17 @@ export function applyValidatedProposal({
parentStatusAfter,
parentConfidenceBefore,
parentConfidenceAfter,
evidenceConfidenceBefore,
evidenceConfidenceAfter,
completenessBefore,
completenessAfter,
conclusionConfidenceBefore,
conclusionConfidenceAfter,
resolvedDirectChildren,
unresolvedDirectChildren,
contradictoryDirectChildren,
confidenceCapReason,
ancestorPropagationStoppedReason,
affectedAncestorIds,
nextSelectedSibling,
parentResolved,
+58
View File
@@ -110,6 +110,17 @@ function buildUpdateDiagnostics({
parentStatusAfter,
parentConfidenceBefore,
parentConfidenceAfter,
evidenceConfidenceBefore,
evidenceConfidenceAfter,
completenessBefore,
completenessAfter,
conclusionConfidenceBefore,
conclusionConfidenceAfter,
resolvedDirectChildren,
unresolvedDirectChildren,
contradictoryDirectChildren,
confidenceCapReason,
ancestorPropagationStoppedReason,
affectedAncestorIds,
nextSelectedSibling,
parentResolved,
@@ -163,6 +174,17 @@ function buildUpdateDiagnostics({
parentStatusAfter: parentStatusAfter ?? null,
parentConfidenceBefore: parentConfidenceBefore ?? null,
parentConfidenceAfter: parentConfidenceAfter ?? null,
evidenceConfidenceBefore: evidenceConfidenceBefore ?? null,
evidenceConfidenceAfter: evidenceConfidenceAfter ?? null,
completenessBefore: completenessBefore ?? null,
completenessAfter: completenessAfter ?? null,
conclusionConfidenceBefore: conclusionConfidenceBefore ?? null,
conclusionConfidenceAfter: conclusionConfidenceAfter ?? null,
resolvedDirectChildren: resolvedDirectChildren ?? 0,
unresolvedDirectChildren: unresolvedDirectChildren ?? 0,
contradictoryDirectChildren: contradictoryDirectChildren ?? 0,
confidenceCapReason: confidenceCapReason ?? null,
ancestorPropagationStoppedReason: ancestorPropagationStoppedReason ?? null,
affectedAncestorIds: affectedAncestorIds ?? [],
nextSelectedSibling: nextSelectedSibling ?? null,
parentResolved: parentResolved ?? false,
@@ -440,6 +462,17 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
parentStatusAfter: null,
parentConfidenceBefore: null,
parentConfidenceAfter: null,
evidenceConfidenceBefore: null,
evidenceConfidenceAfter: null,
completenessBefore: null,
completenessAfter: null,
conclusionConfidenceBefore: null,
conclusionConfidenceAfter: null,
resolvedDirectChildren: 0,
unresolvedDirectChildren: 0,
contradictoryDirectChildren: 0,
confidenceCapReason: null,
ancestorPropagationStoppedReason: null,
affectedAncestorIds: [],
nextSelectedSibling: null,
parentResolved: false,
@@ -508,6 +541,20 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
parentStatusAfter: applicationResult.parentStatusAfter,
parentConfidenceBefore: applicationResult.parentConfidenceBefore,
parentConfidenceAfter: applicationResult.parentConfidenceAfter,
evidenceConfidenceBefore: applicationResult.evidenceConfidenceBefore,
evidenceConfidenceAfter: applicationResult.evidenceConfidenceAfter,
completenessBefore: applicationResult.completenessBefore,
completenessAfter: applicationResult.completenessAfter,
conclusionConfidenceBefore:
applicationResult.conclusionConfidenceBefore,
conclusionConfidenceAfter: applicationResult.conclusionConfidenceAfter,
resolvedDirectChildren: applicationResult.resolvedDirectChildren,
unresolvedDirectChildren: applicationResult.unresolvedDirectChildren,
contradictoryDirectChildren:
applicationResult.contradictoryDirectChildren,
confidenceCapReason: applicationResult.confidenceCapReason,
ancestorPropagationStoppedReason:
applicationResult.ancestorPropagationStoppedReason,
affectedAncestorIds: applicationResult.affectedAncestorIds,
nextSelectedSibling: applicationResult.nextSelectedSibling,
parentResolved: applicationResult.parentResolved,
@@ -560,6 +607,17 @@ async function updateCaseWithDependencies(body, dependencies = {}) {
parentStatusAfter: null,
parentConfidenceBefore: null,
parentConfidenceAfter: null,
evidenceConfidenceBefore: null,
evidenceConfidenceAfter: null,
completenessBefore: null,
completenessAfter: null,
conclusionConfidenceBefore: null,
conclusionConfidenceAfter: null,
resolvedDirectChildren: 0,
unresolvedDirectChildren: 0,
contradictoryDirectChildren: 0,
confidenceCapReason: null,
ancestorPropagationStoppedReason: null,
affectedAncestorIds: [],
nextSelectedSibling: null,
parentResolved: false,
+16
View File
@@ -36,6 +36,20 @@ export const ConfidenceLevel = /** @type {const} */ ({
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({
@@ -45,6 +59,7 @@ export const situationNodeSchema = z.object({
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([]),
@@ -188,6 +203,7 @@ export function makeNode(opts) {
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 ?? [],