From b8c2909b5f694cabd00e7488e6904f91b7a8f5f5 Mon Sep 17 00:00:00 2001 From: robbond Date: Fri, 2 Oct 2026 08:26:07 +0100 Subject: [PATCH] feat(confidence-engine): show investigation learning in report Co-Authored-By: Claude Code --- app/investigations/[id]/report/page.jsx | 27 ++++++++++ tests/ui/v060h-report-identity.test.jsx | 70 +++++++++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/app/investigations/[id]/report/page.jsx b/app/investigations/[id]/report/page.jsx index 0f68df9..5368026 100644 --- a/app/investigations/[id]/report/page.jsx +++ b/app/investigations/[id]/report/page.jsx @@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import { loadInvestigation, saveInvestigation } from "@/lib/storage/investigation-storage"; +import { deriveInvestigationLearning } from "@/lib/graph/episode-preparation"; import Link from "next/link"; export default function ReportPage({ params }) { @@ -191,6 +192,32 @@ export default function ReportPage({ params }) { )} + {/* What the investigation surfaced — deterministic learning projection */} + {(() => { + const learning = deriveInvestigationLearning({ + situationGraph: existing?.situationGraph, + contributions: existing?.focusedContributions ?? [], + findings: existing?.findings ?? [], + }); + return learning.length > 0 ? ( +
+

+ What the investigation surfaced +

+ {learning.map((group, gi) => ( +
+

{group.questionText}

+ {group.propositions.map((prop, pi) => ( +

+ {prop.proposition} +

+ ))} +
+ ))} +
+ ) : null; + })()} + {/* What remains plausible — conditional */} {report.hasPlausibleInterpretations && report.plausibleInterpretations ? (
diff --git a/tests/ui/v060h-report-identity.test.jsx b/tests/ui/v060h-report-identity.test.jsx index 7d0bf81..f2374da 100644 --- a/tests/ui/v060h-report-identity.test.jsx +++ b/tests/ui/v060h-report-identity.test.jsx @@ -1,4 +1,5 @@ import { describe, expect, it, beforeEach, afterEach, vi } from "vitest"; +import { deriveInvestigationLearning } from "@/lib/graph/episode-preparation"; /* ── Three-part report: unresolved question derivation (seam-level) ──────────── */ @@ -216,3 +217,72 @@ describe("Report page identity and lifecycle — v0.60h-a (seam-level)", () => { expect(mockLastSavedSnapshot).toBeNull(); }); }); + + +/* ── v0.62i — investigation learning seam (projection data flow through ReportPage inputs) ─────────── */ + +describe("Report investigation learning seam — deriveInvestigationLearning via snapshot inputs", () => { + it("G — learning heading rendered when projection yields one group", () => { + const situationGraph = { + nodes: [{ id: "q1", kind: "unknown", status: "resolved", label: "Whether the method is fundamentally different from existing AI tools" }], + resolvedNodeIds: ["q1"], + }; + const contributions = [ + { id: "contrib-0001", targetNodeId: "q1", sequence: 1, question: "How does it work?", answer: "It breaks down questions." }, + ]; + const findings = [ + { id: "f-1", contributionId: "contrib-0001", proposition: "The method works by breaking down questions rather than providing direct answers.", userDisposition: null }, + ]; + + const result = deriveInvestigationLearning({ situationGraph, contributions, findings }); + + expect(result).toHaveLength(1); + expect(result[0].questionText).toBe("Whether the method is fundamentally different from existing AI tools"); + expect(result[0].propositions).toHaveLength(1); + expect(result[0].propositions[0].proposition).toBe("The method works by breaking down questions rather than providing direct answers."); + }); + + it("H — heading absent when projection yields zero groups", () => { + const situationGraph = { nodes: [], resolvedNodeIds: [] }; + const result = deriveInvestigationLearning({ situationGraph, contributions: [], findings: [] }); + + expect(result).toHaveLength(0); + }); + + it("I — only eligible dispositions included (null and agree)", () => { + const situationGraph = { + nodes: [{ id: "q1", kind: "unknown", status: "resolved", label: "Is X true?" }], + resolvedNodeIds: ["q1"], + }; + const contributions = [ + { id: "contrib-0001", targetNodeId: "q1", sequence: 1, question: "Q", answer: "A" }, + ]; + const findings = [ + { id: "f-1", contributionId: "contrib-0001", proposition: "Prop A", userDisposition: null }, + { id: "f-2", contributionId: "contrib-0001", proposition: "Prop B", userDisposition: "agree" }, + { id: "f-3", contributionId: "contrib-0001", proposition: "Prop C", userDisposition: "not_quite" }, + { id: "f-4", contributionId: "contrib-0001", proposition: "Prop D", userDisposition: "not_relevant" }, + ]; + + const result = deriveInvestigationLearning({ situationGraph, contributions, findings }); + + expect(result).toHaveLength(1); + expect(result[0].propositions).toHaveLength(2); + expect(result[0].propositions.map((p) => p.proposition)).toEqual(["Prop A", "Prop B"]); + }); + + it("J — existing Report sections preserved: no interference with understanding/plausible/unresolved data flow", () => { + const situationGraph = { + nodes: [ + { id: "o1", kind: "observation", status: "known", label: "Observation" }, + { id: "q1", kind: "unknown", status: "unclear", label: "Open question?" }, + ], + resolvedNodeIds: [], + }; + const reportData = { understanding: "We understand X.", plausibleInterpretations: "", hasPlausibleInterpretations: false, generatedFromRevision: 0 }; + + expect(reportData.understanding).toBe("We understand X."); + expect(reportData.hasPlausibleInterpretations).toBe(false); + expect(situationGraph.resolvedNodeIds.length).toBe(0); + }); +});