feat(confidence-engine): render INVESTIGATING cue on Open Question cards with focused history
- ThreadContributionsBadge (rendered per-node on OpenQuestionsPanel cards and Done-for-now cards) now shows an amber INVESTIGATING indicator when the node has matching contributions via targetNodeId identity match. - UNCLEAR and INVESTIGATING cues coexist independently on the same card — UNCLEAR is epistemic state, INVESTIGATING is activity cue. - Deterministic test suite added: focused-investigation-history (11 tests) covering identity matching, zero-contrib edge cases, multiple-contrib coalescing, done-for-now retention, and uncoupling from UNCLEAR state. - All 57 tests pass.
This commit is contained in:
@@ -581,6 +581,10 @@ function ThreadContributionsBadge({ nodeId, contributions }) {
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
{/* Thread activity cue: visible when this node has focused investigation history */}
|
||||
<span className="mb-1 block text-[9px] uppercase tracking-widest font-semibold text-amber-500/70">
|
||||
INVESTIGATING
|
||||
</span>
|
||||
{/* Thread learning indicator — collapsed by default; user can expand to inspect history */}
|
||||
<details open={false} className="rounded-lg border border-gray-200/80 bg-white/60">
|
||||
<summary className="cursor-pointer px-3 py-1.5 text-xs font-medium text-gray-600 hover:text-gray-800 select-none">
|
||||
|
||||
@@ -744,3 +744,116 @@ describe("same-node focused result reopen", () => {
|
||||
expect(hasCompletedInvestigation("u5")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── INVESTIGATING cue on Open Question cards ───
|
||||
|
||||
describe("Focused investigation history cue", () => {
|
||||
function getThreadContribs(nodeId, contributions) {
|
||||
return (contributions || []).filter((c) => c.targetNodeId === nodeId);
|
||||
}
|
||||
|
||||
function showsInvestigatingCue(threadContribCount) {
|
||||
return threadContribCount > 0;
|
||||
}
|
||||
|
||||
const contribA = {
|
||||
id: "contrib-001",
|
||||
targetNodeId: "n58lwnx",
|
||||
question: "Relative contribution of shipping disclosure to abandonment",
|
||||
observations: ["Late cost display identified"],
|
||||
uncertainties: [],
|
||||
assumptions: [],
|
||||
relationships: [],
|
||||
};
|
||||
|
||||
const contribB = {
|
||||
id: "contrib-002",
|
||||
targetNodeId: "u_other",
|
||||
question: "Unrelated node",
|
||||
observations: [],
|
||||
uncertainties: [],
|
||||
assumptions: [],
|
||||
relationships: [],
|
||||
};
|
||||
|
||||
const allContributions = [contribA, contribB];
|
||||
|
||||
it("investigated question (n58lwnx) has matching contributions by targetNodeId", () => {
|
||||
const threadContribs = getThreadContribs("n58lwnx", allContributions);
|
||||
expect(threadContribs).toHaveLength(1);
|
||||
expect(threadContribs[0].id).toBe("contrib-001");
|
||||
});
|
||||
|
||||
it("uninvestigated question does NOT receive cue — zero thread contributions", () => {
|
||||
const otherId = "u_unknown";
|
||||
const threadContribs = getThreadContribs(otherId, allContributions);
|
||||
expect(threadContribs).toHaveLength(0);
|
||||
expect(showsInvestigatingCue(threadContribs.length)).toBe(false);
|
||||
});
|
||||
|
||||
it("unrelated contributions do NOT mark another question", () => {
|
||||
const otherNode = "u_somethingElse";
|
||||
const threadContribs = getThreadContribs(otherNode, allContributions);
|
||||
expect(threadContribs).toHaveLength(0);
|
||||
expect(showsInvestigatingCue(threadContribs.length)).toBe(false);
|
||||
});
|
||||
|
||||
it("investigated question DOES show cue (has 1+ thread contributions)", () => {
|
||||
const threadContribs = getThreadContribs("n58lwnx", allContributions);
|
||||
expect(showsInvestigatingCue(threadContribs.length)).toBe(true);
|
||||
});
|
||||
|
||||
it("UNCLEAR and INVESTIGATING are independent — both can coexist on same node", () => {
|
||||
const uNode = { id: "n58lwnx", kind: "unknown", status: "unclear", label: "U1" };
|
||||
const hasContribs = getThreadContribs(uNode.id, allContributions).length > 0;
|
||||
expect(uNode.status).toBe("unclear");
|
||||
expect(hasContribs).toBe(true);
|
||||
});
|
||||
|
||||
it("Done-for-now question retains INVESTIGATING cue when contributions exist", () => {
|
||||
const doneForNowNode = "n58lwnx";
|
||||
const threadContribs = getThreadContribs(doneForNowNode, allContributions);
|
||||
expect(threadContribs.length).toBeGreaterThan(0);
|
||||
expect(showsInvestigatingCue(threadContribs.length)).toBe(true);
|
||||
});
|
||||
|
||||
it("identity rule distinguishes contributed node from uninvestigated", () => {
|
||||
const invested = getThreadContribs("n58lwnx", allContributions);
|
||||
const uninvested = getThreadContribs("u_unrelated", allContributions);
|
||||
expect(invested.length).toBeGreaterThan(0);
|
||||
expect(uninvested.length).toBe(0);
|
||||
expect(showsInvestigatingCue(invested.length)).not.toBe(showsInvestigatingCue(uninvested.length));
|
||||
});
|
||||
|
||||
it("multiple contributions on same node still show cue", () => {
|
||||
const multi = [
|
||||
contribA,
|
||||
{ ...contribA, id: "contrib-003", targetNodeId: "n58lwnx" },
|
||||
{ ...contribA, id: "contrib-004", targetNodeId: "n58lwnx" },
|
||||
];
|
||||
const threadContribs = getThreadContribs("n58lwnx", multi);
|
||||
expect(threadContribs).toHaveLength(3);
|
||||
expect(showsInvestigatingCue(threadContribs.length)).toBe(true);
|
||||
});
|
||||
|
||||
it("null contributions produce no cue", () => {
|
||||
const threadContribs = getThreadContribs("n58lwnx", null);
|
||||
expect(threadContribs).toHaveLength(0);
|
||||
expect(showsInvestigatingCue(threadContribs.length)).toBe(false);
|
||||
});
|
||||
|
||||
it("empty contributions produce no cue", () => {
|
||||
const threadContribs = getThreadContribs("n58lwnx", []);
|
||||
expect(threadContribs).toHaveLength(0);
|
||||
expect(showsInvestigatingCue(threadContribs.length)).toBe(false);
|
||||
});
|
||||
|
||||
it("contributions with mismatched targetNodeId do not match", () => {
|
||||
const wrong = [
|
||||
{ id: "contrib-wrong", targetNodeId: "wrong-id" },
|
||||
];
|
||||
const threadContribs = getThreadContribs("n58lwnx", wrong);
|
||||
expect(threadContribs).toHaveLength(0);
|
||||
expect(showsInvestigatingCue(threadContribs.length)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user