From 949a7024b328610741caafbf29f5a28454aa04d7 Mon Sep 17 00:00:00 2001 From: robbond Date: Mon, 7 Sep 2026 15:50:50 +0100 Subject: [PATCH] fix(confidence-engine): allow no-op episode reconsideration --- lib/graph/apply-proposal.js | 11 ++++- tests/graph/apply-proposal.test.js | 66 +++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/lib/graph/apply-proposal.js b/lib/graph/apply-proposal.js index c6772af..44e26be 100644 --- a/lib/graph/apply-proposal.js +++ b/lib/graph/apply-proposal.js @@ -4089,7 +4089,16 @@ export async function applyValidatedProposal({ ); if (!proposalGraphValidation.valid) { - proposalCompatibilityErrors.push(...proposalGraphValidation.errors); + const acceptsCompletedEpisodeNoOp = evidenceContext?.isCompletedEpisode === true; + proposalCompatibilityErrors.push( + ...proposalGraphValidation.errors.filter( + (error) => + !( + acceptsCompletedEpisodeNoOp && + error === "Update contains no meaningful change" + ), + ), + ); } const existingEdgeIds = new Set(situationGraph.edges.map((edge) => edge.id)); diff --git a/tests/graph/apply-proposal.test.js b/tests/graph/apply-proposal.test.js index 6cd681c..05f3e6d 100644 --- a/tests/graph/apply-proposal.test.js +++ b/tests/graph/apply-proposal.test.js @@ -706,10 +706,10 @@ describe("applyValidatedProposal", () => { expect(graph).toEqual(originalGraph); }); - it("rejects a proposal with no meaningful change", () => { + it("rejects a proposal with no meaningful change", async () => { const { graph } = makeApplicationFixture(); - const result = applyValidatedProposal({ + const result = await applyValidatedProposal({ situationGraph: graph, proposal: { addedNodes: [], @@ -740,6 +740,68 @@ describe("applyValidatedProposal", () => { ); }); + it("accepts a completed-episode proposal with no additional graph mutation", async () => { + const { graph } = makeApplicationFixture(); + const originalGraph = JSON.parse(JSON.stringify(graph)); + const noOpProposal = { + addedNodes: [], + updatedNodes: [ + { + nodeId: "n-quality-deterioration", + previousStatus: null, + newStatus: null, + previousValue: null, + newValue: null, + reason: "No further graph change.", + }, + ], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [], + affectedNodeIds: [], + selectedQuestion: null, + }; + + const result = await applyValidatedProposal({ + situationGraph: graph, + proposal: noOpProposal, + evidenceContext: { + isCompletedEpisode: true, + episodeEvidence: { + turns: [{ question: "What did you find?", answer: "No further change." }], + eligibleCanonicalFindings: [{ proposition: "The episode is preserved." }], + }, + }, + }); + + expect(result.success).toBe(true); + expect(result.updatedSituationGraph.nodes).toEqual(originalGraph.nodes); + expect(result.updatedSituationGraph.edges).toEqual(originalGraph.edges); + }); + + it("still rejects an invalid completed-episode proposal", async () => { + const { graph } = makeApplicationFixture(); + + const result = await applyValidatedProposal({ + situationGraph: graph, + proposal: { + addedNodes: [], + updatedNodes: [{ nodeId: "missing-node", reason: "Invalid target." }], + addedEdges: [], + removedEdgeIds: [], + resolvedUnknownNodeIds: [], + affectedNodeIds: [], + selectedQuestion: null, + }, + evidenceContext: { isCompletedEpisode: true, episodeEvidence: { turns: [] } }, + }); + + expect(result).toMatchObject({ success: false, stage: "proposal_compatibility" }); + expect(result.errors).toEqual(expect.arrayContaining([ + expect.stringContaining("Cannot update non-existent node"), + ])); + }); + it("resolves one unknown and adds consequential unknowns with one selected question", () => { const { graph, ids } = makeApplicationFixture();