fix(confidence-engine): scope episode closure authority
This commit is contained in:
+68
-13
@@ -2,6 +2,7 @@ import { describeGraph } from "./builder.js";
|
||||
import {
|
||||
countRemainingMaterialFactors,
|
||||
hasRemainingMaterialFactors,
|
||||
isDecisionClosureAuthorityQuestion,
|
||||
isUserConfirmationOfNoRemainingUncertainty,
|
||||
shouldCloseDecision,
|
||||
} from "./decision-sufficiency.js";
|
||||
@@ -4032,15 +4033,39 @@ export async function applyValidatedProposal({
|
||||
);
|
||||
|
||||
// ── 60B.80 — normalise terminal parent closure without explicit confirmation
|
||||
// legacy only: episode evidence does not supply a single-answer confirmation string;
|
||||
// Done and Finding agree are NOT closure authority (settled architecture rule).
|
||||
const ownershipChanges = isLegacySingleTurn
|
||||
? reconcileDecisionClosureOwnership(
|
||||
situationGraph,
|
||||
reconciledProposal.proposal,
|
||||
answer,
|
||||
)
|
||||
: { strippedUpdates: [], strippedResolvedIds: [] };
|
||||
// legacy: use raw answer for explicit confirmation detection.
|
||||
// episode: scan preserved paired turns for a QUALIFIED authority pair;
|
||||
// question must grant decision-level closure authority AND
|
||||
// answer must confirm no remaining uncertainty, SAME TURN.
|
||||
let ownershipAnswer = answer;
|
||||
if (!isLegacySingleTurn) {
|
||||
const epTurns = evidenceContext?.isCompletedEpisode
|
||||
? evidenceContext.episodeEvidence?.turns
|
||||
: [];
|
||||
if (Array.isArray(epTurns) && epTurns.length > 0) {
|
||||
// Scan all turns — paired check on SAME turn only.
|
||||
let explicitConfirmationAnswer = null;
|
||||
for (const turn of epTurns) {
|
||||
const tQuestion = turn?.question;
|
||||
const tAnswer = turn?.answer;
|
||||
if (
|
||||
typeof tQuestion === "string" &&
|
||||
typeof tAnswer === "string" &&
|
||||
isDecisionClosureAuthorityQuestion(tQuestion) &&
|
||||
isUserConfirmationOfNoRemainingUncertainty(tAnswer)
|
||||
) {
|
||||
explicitConfirmationAnswer = tAnswer;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ownershipAnswer = explicitConfirmationAnswer ?? null;
|
||||
}
|
||||
}
|
||||
const ownershipChanges = reconcileDecisionClosureOwnership(
|
||||
situationGraph,
|
||||
reconciledProposal.proposal,
|
||||
ownershipAnswer,
|
||||
);
|
||||
if (ownershipChanges.strippedResolvedIds.length > 0) {
|
||||
// Reconciler may have added selectedQuestion = null because the parent
|
||||
// appeared resolved during reconciliation. If we stripped it, restore
|
||||
@@ -4175,10 +4200,40 @@ export async function applyValidatedProposal({
|
||||
? findNodeById(graphSnapshot, previousActiveUnknownNodeId)
|
||||
: null;
|
||||
const affectedNodeIds = buildAffectedNodeIds(graphSnapshot, proposalSnapshot);
|
||||
// For episode evidence: extract turn text from the ordered Q/A sequence.
|
||||
const _epTurns = evidenceContext?.isCompletedEpisode ? evidenceContext.episodeEvidence.turns : null;
|
||||
const _derivedQuestion = isLegacySingleTurn ? previousQuestion : (_epTurns?.[0]?.question ?? "");
|
||||
const _derivedAnswer = isLegacySingleTurn ? answer : (_epTurns?.[0]?.answer ?? "");
|
||||
// For episode evidence: derive comparability override from the relevant Q/A pair,
|
||||
// not from turns[0]. Scan preserved paired turns for a matching comparability question
|
||||
// whose answer confirms comparability.
|
||||
let _derivedQuestion = isLegacySingleTurn ? previousQuestion : "";
|
||||
let _derivedAnswer = isLegacySingleTurn ? answer : "";
|
||||
if (!isLegacySingleTurn) {
|
||||
const epTurns = evidenceContext?.isCompletedEpisode
|
||||
? evidenceContext.episodeEvidence?.turns
|
||||
: [];
|
||||
if (Array.isArray(epTurns)) {
|
||||
// Scan all turns for a matching comparability Q/A pair.
|
||||
let found = false;
|
||||
for (const turn of epTurns) {
|
||||
const tq = turn?.question;
|
||||
const ta = turn?.answer;
|
||||
if (
|
||||
typeof tq === "string" &&
|
||||
typeof ta === "string" &&
|
||||
isComparabilityQuestion(tq) &&
|
||||
answerConfirmsComparability(ta)
|
||||
) {
|
||||
_derivedQuestion = tq;
|
||||
_derivedAnswer = ta;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// No qualifying comparability pair — do not fabricate confirmation.
|
||||
_derivedQuestion = "";
|
||||
_derivedAnswer = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
const reasoningResolution = deriveReasoningStateOverride({
|
||||
graph: graphSnapshot,
|
||||
previousQuestion: _derivedQuestion,
|
||||
|
||||
@@ -65,6 +65,45 @@ export function isUserConfirmationOfNoRemainingUncertainty(answer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Pure: decision-closure authority question predicate ───────────
|
||||
|
||||
/**
|
||||
* Determines whether a question explicitly asks the user whether any
|
||||
* material uncertainty remains at the parent-decision level — i.e. the
|
||||
* question grants closure authority when answered with confirmation.
|
||||
*
|
||||
* Bounded to the decision_threshold sufficiency-confirmation semantic family.
|
||||
* Focused questions (supplier, budget, etc.) do NOT match even if they
|
||||
* receive a confirming answer.
|
||||
*
|
||||
* Returns true only for:
|
||||
* - "Is there anything else material that could change which option is better?"
|
||||
* (the canonical product template)
|
||||
* - Questions using the closure-family pattern:
|
||||
* <remaining-uncertainty-phrase> + <closure-action-phrase>
|
||||
* (e.g. "remaining material uncertainty preventing this decision from being closed")
|
||||
*/
|
||||
export function isDecisionClosureAuthorityQuestion(question) {
|
||||
const text = String(question || "");
|
||||
const lower = text.toLowerCase();
|
||||
|
||||
// Direct match for the canonical decision_threshold sufficiency confirmation template
|
||||
if (/change which option (is |was )?better/i.test(lower)) return true;
|
||||
|
||||
// Closure-family pattern: "remaining-uncertainty" + "closure-action" conjunction
|
||||
const hasRemainingUncertainty = /any(?:thing)?\s+(?:else\s+)?(?:material\s+)?uncertain(?:ty|ces)/i.test(lower);
|
||||
if (!hasRemainingUncertainty) return false;
|
||||
|
||||
const hasClosureAction = /\b(closure|close[sd]?|resolve[d]?)\b/i.test(lower);
|
||||
if (hasClosureAction) return true;
|
||||
|
||||
// "remaining material uncertainty" + decision-referencing words
|
||||
const hasDecisionRef = /(?:decision|deciding|disposition)\b/i.test(lower);
|
||||
if (hasDecisionRef) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Pure: unresolved predicate ─────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* Targeted tests for Defect 1 (parent-decision closure in episode mode)
|
||||
* and Defect 2 (comparability reasoning-state override routing).
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
|
||||
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/** Build a parent-decision fixture with options and a material factor. */
|
||||
function makeParentClosureFixture() {
|
||||
const parent = makeNode({
|
||||
id: "n-parent",
|
||||
label: "Should we proceed?",
|
||||
description: "Parent decision context.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
});
|
||||
|
||||
const optA = makeNode({
|
||||
id: "opt-a",
|
||||
label: "Option A",
|
||||
description: "Path A.",
|
||||
kind: "option",
|
||||
status: "known",
|
||||
});
|
||||
|
||||
const optB = makeNode({
|
||||
id: "opt-b",
|
||||
label: "Option B",
|
||||
description: "Path B.",
|
||||
kind: "option",
|
||||
status: "known",
|
||||
});
|
||||
|
||||
const factor = makeNode({
|
||||
id: "n-factor",
|
||||
label: "Material uncertainty factor",
|
||||
description: "A material factor for option A.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
});
|
||||
|
||||
return {
|
||||
graph: makeGraph({
|
||||
centralStatement: "Choosing between paths A and B.",
|
||||
nodes: [parent, optA, optB, factor],
|
||||
edges: [
|
||||
makeEdge({ id: "e-oa-to-p", fromNodeId: optA.id, toNodeId: parent.id, relationship: "contained_in" }),
|
||||
makeEdge({ id: "e-ob-to-p", fromNodeId: optB.id, toNodeId: parent.id, relationship: "contained_in" }),
|
||||
makeEdge({ id: "e-fa", fromNodeId: factor.id, toNodeId: optA.id, relationship: "may_cause" }),
|
||||
],
|
||||
activeUnknownNodeId: "n-factor",
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Parent closure fixture.",
|
||||
}),
|
||||
ids: { parent: parent.id, optA: optA.id, optB: optB.id, factor: factor.id },
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a comparability fixture. */
|
||||
function makeComparabilityFixture() {
|
||||
const compUnknown = makeNode({
|
||||
id: "n-comparability",
|
||||
label: "Figures on same basis?",
|
||||
description: "Uncertainty about comparability of two figures.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
});
|
||||
|
||||
const optA = makeNode({
|
||||
id: "opt-a",
|
||||
label: "Option A",
|
||||
description: "Path A.",
|
||||
kind: "option",
|
||||
status: "known",
|
||||
});
|
||||
|
||||
const optB = makeNode({
|
||||
id: "opt-b",
|
||||
label: "Option B",
|
||||
description: "Path B.",
|
||||
kind: "option",
|
||||
status: "known",
|
||||
});
|
||||
|
||||
const parent = makeNode({
|
||||
id: "n-parent",
|
||||
label: "Decision context",
|
||||
description: "Parent decision.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
});
|
||||
|
||||
return {
|
||||
graph: makeGraph({
|
||||
centralStatement: "Comparability context.",
|
||||
nodes: [compUnknown, optA, optB, parent],
|
||||
edges: [
|
||||
makeEdge({ id: "e-oa-to-p", fromNodeId: optA.id, toNodeId: parent.id, relationship: "contained_in" }),
|
||||
makeEdge({ id: "e-ob-to-p", fromNodeId: optB.id, toNodeId: parent.id, relationship: "contained_in" }),
|
||||
],
|
||||
activeUnknownNodeId: "n-comparability",
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Comparability fixture.",
|
||||
}),
|
||||
ids: { compUnknown: compUnknown.id, parent: parent.id, optA: optA.id, optB: optB.id },
|
||||
};
|
||||
}
|
||||
|
||||
// ── Defect 1 — Parent-decision closure ─────────────────────────────
|
||||
|
||||
describe("Defect 1 — parent-decision closure in episode mode", () => {
|
||||
it("legacy explicit closure regression — decision closes with confirmed answer", async () => {
|
||||
const { graph, ids } = makeParentClosureFixture();
|
||||
|
||||
const result = await applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{ nodeId: "n-factor", previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Factor resolved.", reason: "Resolved." },
|
||||
{ nodeId: ids.parent, previousStatus: "unknown", newStatus: "known", previousValue: null, newValue: "Decision closed.", reason: "Closes parent." },
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: ["n-factor"],
|
||||
affectedNodeIds: [ids.parent],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
answer: "no other material uncertainty remains",
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const parent = result.updatedSituationGraph.nodes.find((n) => n.id === ids.parent);
|
||||
expect(parent?.status).toBe("known");
|
||||
});
|
||||
|
||||
it("episode closure — no authority prevents terminal parent closure", async () => {
|
||||
const { graph, ids } = makeParentClosureFixture();
|
||||
|
||||
const result = await applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{ nodeId: "n-factor", previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Factor resolved.", reason: "Resolved." },
|
||||
{ nodeId: ids.parent, previousStatus: "unknown", newStatus: "known", previousValue: null, newValue: "Decision closed.", reason: "Closes parent." },
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: ["n-factor"],
|
||||
affectedNodeIds: [ids.parent],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
evidenceContext: {
|
||||
isCompletedEpisode: true,
|
||||
episodeEvidence: {
|
||||
turns: [
|
||||
{ contributionId: "c1", sequence: 1, question: "What did you find?", answer: "All looks good." },
|
||||
{ contributionId: "c2", sequence: 2, question: "Anything else uncertain?", answer: "No.", },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Factor resolved — valid update preserved.
|
||||
const factor = result.updatedSituationGraph.nodes.find((n) => n.id === "n-factor");
|
||||
expect(factor?.status).toBe("resolved");
|
||||
// Parent terminal closure prevented — fail-closed.
|
||||
const parent = result.updatedSituationGraph.nodes.find((n) => n.id === ids.parent);
|
||||
expect(parent?.status).not.toBe("known");
|
||||
});
|
||||
|
||||
it("episode closure — explicit authority allows closure", async () => {
|
||||
const { graph, ids } = makeParentClosureFixture();
|
||||
|
||||
const result = await applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{ nodeId: "n-factor", previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Factor resolved.", reason: "Resolved." },
|
||||
{ nodeId: ids.parent, previousStatus: "unknown", newStatus: "known", previousValue: null, newValue: "Decision closed.", reason: "Closes parent." },
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: ["n-factor"],
|
||||
affectedNodeIds: [ids.parent],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
evidenceContext: {
|
||||
isCompletedEpisode: true,
|
||||
episodeEvidence: {
|
||||
turns: [
|
||||
{ contributionId: "c1", sequence: 1, question: "What did you find?", answer: "All looks good." },
|
||||
{ contributionId: "c2", sequence: 2, question: "Is there anything else material that could change which option is better?", answer: "No, there is no remaining material uncertainty.", },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const factor = result.updatedSituationGraph.nodes.find((n) => n.id === "n-factor");
|
||||
expect(factor?.status).toBe("resolved");
|
||||
// Explicit authority Q/A pair present — parent closes.
|
||||
const parent = result.updatedSituationGraph.nodes.find((n) => n.id === ids.parent);
|
||||
expect(parent?.status).toBe("known");
|
||||
});
|
||||
|
||||
it("focused question + confirming answer does NOT authorise closure", async () => {
|
||||
// Required 1 — focused false-positive guard
|
||||
const { graph, ids } = makeParentClosureFixture();
|
||||
|
||||
const result = await applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{ nodeId: "n-factor", previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Factor resolved.", reason: "Resolved." },
|
||||
{ nodeId: ids.parent, previousStatus: "unknown", newStatus: "known", previousValue: null, newValue: "Decision closed.", reason: "Closes parent." },
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: ["n-factor"],
|
||||
affectedNodeIds: [ids.parent],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
evidenceContext: {
|
||||
isCompletedEpisode: true,
|
||||
episodeEvidence: {
|
||||
turns: [
|
||||
{ contributionId: "c1", sequence: 1, question: "What did you find?", answer: "All looks good." },
|
||||
{ contributionId: "c2", sequence: 2, question: "Is there any remaining uncertainty about the supplier?", answer: "No, there is no remaining material uncertainty.", },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const factor = result.updatedSituationGraph.nodes.find((n) => n.id === "n-factor");
|
||||
expect(factor?.status).toBe("resolved");
|
||||
// Question is focused, NOT parent-decision authority — closure stripped.
|
||||
const parent = result.updatedSituationGraph.nodes.find((n) => n.id === ids.parent);
|
||||
expect(parent?.status).not.toBe("known");
|
||||
});
|
||||
|
||||
it("cross-turn question and answer mixing does NOT authorise closure", async () => {
|
||||
// Required 3 — cross-turn mixing forbidden
|
||||
const { graph, ids } = makeParentClosureFixture();
|
||||
|
||||
const result = await applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{ nodeId: "n-factor", previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Factor resolved.", reason: "Resolved." },
|
||||
{ nodeId: ids.parent, previousStatus: "unknown", newStatus: "known", previousValue: null, newValue: "Decision closed.", reason: "Closes parent." },
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: ["n-factor"],
|
||||
affectedNodeIds: [ids.parent],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
evidenceContext: {
|
||||
isCompletedEpisode: true,
|
||||
episodeEvidence: {
|
||||
turns: [
|
||||
{ contributionId: "c1", sequence: 1, question: "Is there anything else material that could change which option is better?", answer: "I need to check the budget.", },
|
||||
{ contributionId: "c2", sequence: 2, question: "Are you satisfied with the analysis so far?", answer: "No, there is no remaining material uncertainty.", },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const factor = result.updatedSituationGraph.nodes.find((n) => n.id === "n-factor");
|
||||
expect(factor?.status).toBe("resolved");
|
||||
// Qualifying question and confirming answer are on different turns — closure stripped.
|
||||
const parent = result.updatedSituationGraph.nodes.find((n) => n.id === ids.parent);
|
||||
expect(parent?.status).not.toBe("known");
|
||||
});
|
||||
|
||||
it("Finding agree grants closure: NO", async () => {
|
||||
const { graph, ids } = makeParentClosureFixture();
|
||||
|
||||
const result = await applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{ nodeId: "n-factor", previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Factor resolved.", reason: "Resolved." },
|
||||
{ nodeId: ids.parent, previousStatus: "unknown", newStatus: "known", previousValue: null, newValue: "Decision closed.", reason: "Closes parent." },
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: ["n-factor"],
|
||||
affectedNodeIds: [ids.parent],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
evidenceContext: {
|
||||
isCompletedEpisode: true,
|
||||
episodeEvidence: {
|
||||
turns: [
|
||||
{ contributionId: "c1", sequence: 1, question: "What did you find?", answer: "All looks good." },
|
||||
],
|
||||
eligibleCanonicalFindings: [
|
||||
{ findingId: "f-1", contributionId: "c1", proposition: "No material uncertainty.", endorsement: "agree" },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Factor resolved — valid update preserved.
|
||||
const factor = result.updatedSituationGraph.nodes.find((n) => n.id === "n-factor");
|
||||
expect(factor?.status).toBe("resolved");
|
||||
// No qualifying Q/A pair — parent closure prevented despite Finding agree.
|
||||
const parent = result.updatedSituationGraph.nodes.find((n) => n.id === ids.parent);
|
||||
expect(parent?.status).not.toBe("known");
|
||||
});
|
||||
});
|
||||
|
||||
// ── Defect 2 — Comparability routing ────────────────────────────────
|
||||
|
||||
describe("Defect 2 — comparability reasoning-state override routing", () => {
|
||||
it("comparability relevant turn is not first — episode matches legacy semantics for the right Q/A pair", async () => {
|
||||
const { graph, ids } = makeComparabilityFixture();
|
||||
|
||||
const result = await applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{ nodeId: ids.compUnknown, previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Confirmed comparable.", reason: "Resolved." },
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.compUnknown],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
evidenceContext: {
|
||||
isCompletedEpisode: true,
|
||||
episodeEvidence: {
|
||||
turns: [
|
||||
// turn 1: unrelated
|
||||
{ contributionId: "c1", sequence: 1, question: "What evidence do you have?", answer: "Revenue data." },
|
||||
// turn 2: comparability Q/A (not first)
|
||||
{ contributionId: "c2", sequence: 2, question: "Are these figures on the same basis and at the same scale?", answer: "Yes, both figures cover the same basis.", },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Reasoning state should reflect comparability confirmation from turn 2.
|
||||
const rs = result.updatedSituationGraph.reasoningState;
|
||||
expect(rs?.comparabilityStatus).toBe("confirmed");
|
||||
expect(rs?.comparabilityReason).toContain("confirmed");
|
||||
});
|
||||
|
||||
it("comparability absent — no fabricated confirmation", async () => {
|
||||
const { graph, ids } = makeComparabilityFixture();
|
||||
|
||||
const result = await applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: {
|
||||
updatedNodes: [
|
||||
{ nodeId: ids.compUnknown, previousStatus: "unknown", newStatus: "resolved", previousValue: null, newValue: "Resolved.", reason: "Resolved." },
|
||||
],
|
||||
addedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [ids.compUnknown],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
evidenceContext: {
|
||||
isCompletedEpisode: true,
|
||||
episodeEvidence: {
|
||||
turns: [
|
||||
{ contributionId: "c1", sequence: 1, question: "Q?", answer: "A." },
|
||||
{ contributionId: "c2", sequence: 2, question: "Any other concerns?", answer: "No.", },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// No comparability pair found — status remains unchanged (uncertain).
|
||||
const rs = result.updatedSituationGraph.reasoningState;
|
||||
expect(rs?.comparabilityStatus).not.toBe("confirmed");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user