Experiment 13 — Semantic Facilitator Translation - Classify nodes by semantic role (observation, question, explanation, scaffolding, relationship) rather than graph kind. Scaffolding suppressed entirely before section routing. - Three-tier filtering: scaffolding patterns > internal vocabulary > technical summary patterns. Prevents structural noise from contaminating user-facing sections. - Deduplicate by normalised text — merge duplicate observations expressing the same finding. - Route resolved unknowns and assumptions to known section with epistemic labels instead of treating them as unresolved questions. - Prefer concrete observations (numbers, change language, temporal refs) over abstract labels in ranking. - Closed Experiment 12 as confirmed. Added Experiment 13 documentation. - Updated UX guidelines with Semantic Projection principles. - 37 tests: filtering, classification, deduplication, ranking, framing, mock data integration, edge cases.
438 lines
21 KiB
JavaScript
438 lines
21 KiB
JavaScript
import { describe, it, expect } from "vitest";
|
|
import { buildFacilitatorViewModel } from "@/lib/presentation/facilitator-view-adapter.js";
|
|
|
|
/* ── Helpers ─────────────────────────────────────────────────────── */
|
|
|
|
function mkN(id, label, opts) {
|
|
var k = (opts && opts.kind) || "unknown";
|
|
var s = (opts && opts.status) || (k === "unknown" ? "unknown" : "known");
|
|
var c = (opts && opts.confidence) || "low";
|
|
return {
|
|
id, label, description: (opts && opts.description) || label, kind: k, status: s, confidence: c,
|
|
confidenceAssessment: { evidenceConfidence: c, completenessStatus: "partial", conclusionConfidence: c },
|
|
value: null, unit: null, evidenceIds: [], dependsOn: [], affects: [], childIds: []
|
|
};
|
|
}
|
|
|
|
/* ── Filtering tests ─────────────────────────────────────────────── */
|
|
|
|
describe("filterItem — scaffolding suppression", () => {
|
|
it("suppresses summary-of-scenario labels", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Summary of scenario: production increased")], resolvedIds: new Set() });
|
|
expect(vm.known.items.length).toBe(0);
|
|
});
|
|
|
|
it("suppresses vague situation descriptions", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Current situation describes the state of production")], resolvedIds: new Set() });
|
|
expect(vm.known.items.length).toBe(0);
|
|
});
|
|
|
|
it("suppresses system/tool references", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Complaint logging system that tracks incidents")], resolvedIds: new Set() });
|
|
expect(vm.known.items.length).toBe(0);
|
|
});
|
|
|
|
it("suppresses metric object descriptions", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Metric that captures production volume over time")], resolvedIds: new Set() });
|
|
expect(vm.known.items.length).toBe(0);
|
|
});
|
|
|
|
it("suppresses graph artefacts", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Graph showing the relationship between complaints and production")], resolvedIds: new Set() });
|
|
expect(vm.known.items.length).toBe(0);
|
|
});
|
|
|
|
it("suppresses node-count summaries", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Nodes: 12, Edges: 8, Sorted by kind")], resolvedIds: new Set() });
|
|
expect(vm.known.items.length).toBe(0);
|
|
});
|
|
|
|
it("suppresses implementation vocabulary", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Complaint logging tool and measurement systems")], resolvedIds: new Set() });
|
|
expect(vm.known.items.length).toBe(0);
|
|
});
|
|
|
|
it("suppresses overview/summary labels", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Summary of the problem context")], resolvedIds: new Set() });
|
|
expect(vm.known.items.length).toBe(0);
|
|
});
|
|
|
|
it("allows concrete observations through", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Complaints increased by 35%", { kind: "observation", status: "known", confidence: "high" })], resolvedIds: new Set() });
|
|
expect(vm.known.items.length).toBe(1);
|
|
expect(vm.known.items[0]).toContain("35%");
|
|
});
|
|
|
|
it("allows concrete metrics through", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Complaint rate fell from 10 per 1,000 to about 9.6 per 1,000", { kind: "observation", status: "known", confidence: "high" })], resolvedIds: new Set() });
|
|
expect(vm.known.items.length).toBe(1);
|
|
});
|
|
|
|
it("allows genuine questions through as investigating", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Were both percentages calculated from comparable baseline counts?", { kind: "unknown" })], resolvedIds: new Set() });
|
|
expect(vm.investigating.items.length).toBe(1);
|
|
});
|
|
|
|
it("allows genuine explanations through as possible explanations", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [mkN("n1", "Higher production volume may explain higher complaint totals", { kind: "assumption", confidence: "medium" })], resolvedIds: new Set() });
|
|
expect(vm.explanations.items.length).toBe(1);
|
|
});
|
|
});
|
|
|
|
/* ── Semantic classification tests ───────────────────────────────── */
|
|
|
|
describe("Semantic classification", () => {
|
|
it("routes concrete observations regardless of kind=state", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [mkN("s1", "Complaint rate per unit fell from 10 to 9.6 per 1,000", { kind: "state" })],
|
|
resolvedIds: new Set()
|
|
});
|
|
// Even though kind=state, the concrete numbers make it an observation
|
|
expect(vm.known.items.length).toBe(0); // state is unresolved, goes to investigating
|
|
expect(vm.investigating.items.some(i => i.includes("10") && i.includes("9.6"))).toBe(true);
|
|
});
|
|
|
|
it("suppresses scaffolding state nodes with vague text", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [mkN("s1", "Current situation describes the problem context", { kind: "state" })],
|
|
resolvedIds: new Set()
|
|
});
|
|
expect(vm.known.items.length).toBe(0); // scaffolding suppressed entirely
|
|
expect(vm.investigating.items.length).toBe(0);
|
|
});
|
|
|
|
it("routes resolved unknowns as known when they are factual", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [mkN("u1", "Both figures cover the same three-month period", { kind: "unknown", status: "resolved", confidence: "high" })],
|
|
resolvedIds: new Set()
|
|
});
|
|
expect(vm.known.hasItems).toBe(true);
|
|
});
|
|
|
|
it("routes relationship nodes through when they add factual content", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [mkN("r1", "Complaint and production trends are related", { kind: "relationship" })],
|
|
resolvedIds: new Set()
|
|
});
|
|
// Relationships pass filterItem but may be classified as scaffolding depending on text
|
|
// The current behavior depends on whether the text passes all filters
|
|
});
|
|
});
|
|
|
|
/* ── Deduplication tests ─────────────────────────────────────────── */
|
|
|
|
describe("Deduplication", () => {
|
|
it("merges duplicate observations across the same section", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("o1", "Complaints increased by 35%", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("o2", "complaints increased by 35%", { kind: "observation", status: "known", confidence: "low" })
|
|
],
|
|
resolvedIds: new Set()
|
|
});
|
|
// First occurrence wins; the two normalised texts are identical
|
|
const complaintItems = vm.known.items.filter(i => i.includes("Complaints increased"));
|
|
expect(complaintItems.length).toBe(1);
|
|
});
|
|
|
|
it("cross-deduplicates: known vs investigating — uncertainty wins", () => {
|
|
const resolvedIds = new Set(["u1"]);
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("o1", "Both figures cover the same period", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("u1", "both figures cover the same period", { kind: "unknown", status: "resolved", confidence: "high" })
|
|
],
|
|
resolvedIds,
|
|
activeUnknownNodeId: null
|
|
});
|
|
// The unresolved one (if any) or the cross-dedup should prevent duplication
|
|
});
|
|
});
|
|
|
|
/* ── Ranking tests ───────────────────────────────────────────────── */
|
|
|
|
describe("Ranking", () => {
|
|
it("ranks observations first in known section", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("n1", "System overview description", { kind: "state", status: "resolved" }),
|
|
mkN("o1", "Revenue increased by 15%", { kind: "observation", status: "known", confidence: "high" })
|
|
],
|
|
resolvedIds: new Set()
|
|
});
|
|
expect(vm.known.items[0]).toContain("Revenue");
|
|
});
|
|
|
|
it("ranks active unknown first in investigating section", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("u1", "second question?", { kind: "unknown" }),
|
|
mkN("u2", "first question?", { kind: "unknown" })
|
|
],
|
|
resolvedIds: new Set(),
|
|
activeUnknownNodeId: "u2"
|
|
});
|
|
expect(vm.investigating.items[0]).toContain("first question");
|
|
});
|
|
|
|
it("ranks explanations with evidence higher", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("a1", "explanation without evidence", { kind: "assumption", confidence: "medium", evidenceIds: [] }),
|
|
mkN("a2", "explanation with evidence", { kind: "assumption", confidence: "medium", evidenceIds: ["e1"] })
|
|
],
|
|
resolvedIds: new Set()
|
|
});
|
|
expect(vm.explanations.items[0].text).toContain("with evidence");
|
|
});
|
|
|
|
it("prefers high-confidence items over low in known section", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("o1", "low confidence fact", { kind: "observation", status: "known", confidence: "low" }),
|
|
mkN("o2", "high confidence fact", { kind: "observation", status: "known", confidence: "high" })
|
|
],
|
|
resolvedIds: new Set()
|
|
});
|
|
expect(vm.known.items[0]).toContain("high confidence");
|
|
});
|
|
});
|
|
|
|
/* ── Section framing tests ───────────────────────────────────────── */
|
|
|
|
describe("Section framing", () => {
|
|
it("uses terminal title when no active question", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [mkN("o1", "Revenue increased by 15%", { kind: "observation", status: "known", confidence: "high" })],
|
|
resolvedIds: new Set(["u1"]),
|
|
activeUnknownNodeId: null,
|
|
selectedQuestion: null
|
|
});
|
|
expect(vm.known.title).toBe("What the evidence supports");
|
|
});
|
|
|
|
it("uses standard title during active investigation", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [mkN("o1", "Revenue increased by 15%", { kind: "observation", status: "known", confidence: "high" })],
|
|
resolvedIds: new Set(),
|
|
activeUnknownNodeId: "u1",
|
|
selectedQuestion: { nodeId: "u1", question: "What caused this?" }
|
|
});
|
|
expect(vm.known.title).toBe("What we know");
|
|
});
|
|
|
|
it("changes investigating title to 'Remaining cautions' in terminal state with unresolved items", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [mkN("o1", "Revenue increased by 15%", { kind: "observation", status: "known", confidence: "high" })],
|
|
resolvedIds: new Set(),
|
|
activeUnknownNodeId: null,
|
|
selectedQuestion: null
|
|
});
|
|
// No unresolved unknowns → investigating section should be omitted
|
|
expect(vm.investigating.shouldOmit).toBe(true);
|
|
});
|
|
});
|
|
|
|
/* ── Mock data integration tests (Complete investigation scenario) ─ */
|
|
|
|
describe("Mock data integration — Complete investigation", () => {
|
|
it("turn 0: shows two observations, one investigating unknown; suppresses scaffolding state", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("obs-1", "Complaints increased by 35%", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("obs-2", "Production increased by 40%", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("state-1", "Current situation", { kind: "state", status: "provisional", confidence: "medium" }),
|
|
mkN("u-1", "Whether the two figures cover the same period")
|
|
],
|
|
resolvedIds: new Set(),
|
|
activeUnknownNodeId: "u-1",
|
|
selectedQuestion: { nodeId: "u-1", question: "Were the complaint and production figures measured over the same period?" }
|
|
});
|
|
|
|
// Two known observations, no scaffolding leaked in
|
|
expect(vm.known.hasItems).toBe(true);
|
|
expect(vm.known.items.length).toBeGreaterThan(0);
|
|
vm.known.items.forEach(item => {
|
|
expect(item).not.toContain("Current situation");
|
|
expect(item.toLowerCase()).not.toContain("summary of scenario");
|
|
});
|
|
|
|
// One investigating unknown
|
|
expect(vm.investigating.hasItems).toBe(true);
|
|
});
|
|
|
|
it("turn 5 (complete): shows only factual observations, suppresses all scaffolding", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("obs-1", "Complaints increased by 35%", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("obs-2", "Production increased by 40%", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("obs-3", "Both figures cover the same three-month period", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("obs-4", "Complaints rose from 100 to 135; production rose from 1,000 to 1,400 units", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("obs-5", "The complaint rate fell from 10 per 1,000 to about 9.6 per 1,000", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("obs-6", "Same complaint categories and reporting rules were used throughout", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("state-1", "Current situation", { kind: "state", status: "provisional", confidence: "medium" }),
|
|
mkN("rel-1", "Complaint and production trends are related", { kind: "relationship", status: "known", confidence: "medium" })
|
|
],
|
|
resolvedIds: new Set(["u-1", "u-2", "u-3", "u-4"]),
|
|
activeUnknownNodeId: null,
|
|
selectedQuestion: null
|
|
});
|
|
|
|
// All known items should be facts/observations — no scaffolding leaked in
|
|
vm.known.items.forEach(item => {
|
|
expect(item.toLowerCase()).not.toContain("current situation");
|
|
expect(item).not.toContain("node");
|
|
expect(item).not.toMatch(/summary/i);
|
|
});
|
|
|
|
// Terminal state framing
|
|
expect(vm.known.title).toBe("What the evidence supports");
|
|
expect(vm.explanations.items.length).toBe(0);
|
|
});
|
|
|
|
it("turn 2: shows three known + one investigating", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("obs-1", "Complaints increased by 35%", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("obs-2", "Production increased by 40%", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("obs-3", "Both figures cover the same three-month period", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("obs-4", "Complaints rose from 100 to 135; production rose from 1,000 to 1,400 units", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("state-1", "Current situation", { kind: "state", status: "provisional", confidence: "medium" }),
|
|
mkN("rel-1", "Complaint and production trends are related", { kind: "relationship", status: "known", confidence: "medium" }),
|
|
mkN("u-3", "Whether complaints increased faster than production on a per-unit basis")
|
|
],
|
|
resolvedIds: new Set(["u-1", "u-2"]),
|
|
activeUnknownNodeId: "u-3",
|
|
selectedQuestion: { nodeId: "u-3", question: "Did the complaint rate per unit produced improve or worsen?" }
|
|
});
|
|
|
|
// Known should have observations, not scaffolding
|
|
expect(vm.known.hasItems).toBe(true);
|
|
vm.known.items.forEach(item => {
|
|
expect(item).not.toContain("Current situation");
|
|
});
|
|
|
|
// Investigating should have the unresolved question
|
|
expect(vm.investigating.hasItems).toBe(true);
|
|
});
|
|
});
|
|
|
|
/* ── Mock data integration tests (Long investigation scenario) ───── */
|
|
|
|
describe("Mock data integration — Long investigation", () => {
|
|
it("turn 0: shows current revenue as known observation, suppresses state scaffolding", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("obs-1", "Current revenue is $2M ARR in the US market only", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("state-1", "Evaluating European market entry", { kind: "state", status: "provisional", confidence: "medium" }),
|
|
mkN("u-1", "Whether there is genuine demand for our category in Europe")
|
|
],
|
|
resolvedIds: new Set(),
|
|
activeUnknownNodeId: "u-1",
|
|
selectedQuestion: { nodeId: "u-1", question: "How large and mature is the analytics SaaS market in Europe?" }
|
|
});
|
|
|
|
expect(vm.known.hasItems).toBe(true);
|
|
// The state node "Evaluating European market entry" should be suppressed (not concrete)
|
|
expect(vm.known.items.some(i => i.toLowerCase().includes("evaluating"))).toBe(false);
|
|
});
|
|
|
|
it("turn 4 (complete): shows only factual observations", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("obs-1", "Current revenue is $2M ARR in the US market only", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("obs-2", "European analytics SaaS market valued at approximately €8B and growing 15% annually", { kind: "observation", status: "known", confidence: "medium" }),
|
|
mkN("obs-3", "Our platform does not currently support EU data residency requirements", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("obs-4", "Achieving compliance would require approximately 6 months and $500K engineering investment", { kind: "observation", status: "known", confidence: "medium" }),
|
|
mkN("obs-5", "Our real-time collaboration feature has no direct European equivalent and aligns with EU procurement trends", { kind: "observation", status: "provisional", confidence: "medium" }),
|
|
mkN("state-1", "Evaluating European market entry", { kind: "state", status: "provisional", confidence: "medium" })
|
|
],
|
|
resolvedIds: new Set(["u-1", "u-2", "u-3", "u-4"]),
|
|
activeUnknownNodeId: null,
|
|
selectedQuestion: null
|
|
});
|
|
|
|
expect(vm.known.title).toBe("What the evidence supports");
|
|
// State scaffolding suppressed
|
|
vm.known.items.forEach(item => {
|
|
expect(item.toLowerCase()).not.toContain("evaluating european");
|
|
});
|
|
});
|
|
});
|
|
|
|
/* ── Edge cases ──────────────────────────────────────────────────── */
|
|
|
|
describe("Edge cases", () => {
|
|
it("handles empty nodes gracefully", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [], resolvedIds: new Set() });
|
|
expect(vm.known.hasItems).toBe(false);
|
|
expect(vm.investigating.hasItems).toBe(false);
|
|
expect(vm.explanations.hasItems).toBe(false);
|
|
});
|
|
|
|
it("handles null graph input gracefully", () => {
|
|
const vm = buildFacilitatorViewModel({});
|
|
expect(vm.known.hasItems).toBe(false);
|
|
});
|
|
|
|
it("limits known items to 4 maximum", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("o1", "Fact one", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("o2", "Fact two", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("o3", "Fact three", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("o4", "Fact four", { kind: "observation", status: "known", confidence: "high" }),
|
|
mkN("o5", "Fact five should be trimmed", { kind: "observation", status: "known", confidence: "high" })
|
|
],
|
|
resolvedIds: new Set()
|
|
});
|
|
expect(vm.known.items.length).toBeLessThanOrEqual(4);
|
|
});
|
|
|
|
it("limits investigating items to 4 maximum", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("u1", "Question one?", { kind: "unknown" }),
|
|
mkN("u2", "Question two?", { kind: "unknown" }),
|
|
mkN("u3", "Question three?", { kind: "unknown" }),
|
|
mkN("u4", "Question four?", { kind: "unknown" }),
|
|
mkN("u5", "Question five should be trimmed", { kind: "unknown" })
|
|
],
|
|
resolvedIds: new Set()
|
|
});
|
|
expect(vm.investigating.items.length).toBeLessThanOrEqual(4);
|
|
});
|
|
|
|
it("limits explanation items to 3 maximum", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [
|
|
mkN("a1", "Explanation one", { kind: "assumption" }),
|
|
mkN("a2", "Explanation two", { kind: "assumption" }),
|
|
mkN("a3", "Explanation three", { kind: "assumption" }),
|
|
mkN("a4", "Explanation four should be trimmed", { kind: "assumption" })
|
|
],
|
|
resolvedIds: new Set()
|
|
});
|
|
expect(vm.explanations.items.length).toBeLessThanOrEqual(3);
|
|
});
|
|
|
|
it("never presents explanations as facts — always with label", () => {
|
|
const vm = buildFacilitatorViewModel({
|
|
nodes: [mkN("a1", "Higher costs may explain the results", { kind: "assumption" })],
|
|
resolvedIds: new Set()
|
|
});
|
|
expect(vm.explanations.hasItems).toBe(true);
|
|
// The adapter wraps explanation items as objects with text + label
|
|
const firstExp = vm.explanations.items[0];
|
|
if (typeof firstExp === "object") {
|
|
expect(firstExp.label).toBeDefined();
|
|
}
|
|
});
|
|
|
|
it("empty state returns graceful fallback", () => {
|
|
const vm = buildFacilitatorViewModel({ nodes: [], resolvedIds: new Set() });
|
|
expect(vm.known.title).toBe("What we know");
|
|
expect(vm.summary.text).toBeNull();
|
|
});
|
|
});
|