/** * Experiment 51 — Is Coherence Relative to the Decision, Rather Than the Graph Shape? * * Passive diagnostic. Tests whether the existing passive decision-relevance classifier * (assessQuestionRelevanceToDecision) distinguishes: * Set A — several unknowns that all genuinely belong to one clear decision; * from * Set B — several unknowns present in the same investigation but not all relevant to it. * * The classifier was trained on European market entry scenarios (Exp 21). This experiment tests: * (1) within its vocabulary, can it distinguish coherent from scattered unknowns? * (2) outside its vocabulary (different domain), does it still produce varied results? * (3) when paraphrased, do the same underlying questions get different classifications? * * No production code changes. No active engine integration. Pure test-level evaluation. */ import { describe, expect, it } from "vitest"; import { assessQuestionRelevanceToDecision } from "@/lib/graph/question-decision-relevance.js"; /* ── Helpers ─────────────────────────────────────────────────── */ function makeUnknown(id, label) { return { id, label: label || `Unknown ${id}`, description: label || `Unknown ${id}`, kind: "unknown", status: "unknown", confidence: "low", value: null, unit: null, evidenceIds: [], dependsOn: [], affects: [], childIds: [], }; } function assessSet(set, decisionTarget) { return set.map((u) => ({ id: u.id, label: u.label, ...assessQuestionRelevanceToDecision({ decisionTarget, unknown: u }), })); } /* ═══════════════════════════════════════════════════════════ * Domain 1 — European Market Entry * * Decision target uses "enter" (a keyword the classifier recognises * in hasActionKeyword) and the question phrasing matches patterns from Exp 21. * ═══════════════════════════════════════════════════════════ */ const DOMAIN_1_DECISION = "Should we enter the European market with our SaaS analytics platform?"; /* ── Set A — Coherent Breadth (4 unknowns, all genuinely relevant) ─── */ const DOMAIN_1_COHERENT = [ { id: "demand", label: "Whether to enter the European market for analytics tools" }, { id: "compliance", label: "Whether our product is suitable for European compliance requirements", }, { id: "cost-benefit", label: "Whether the cost of achieving compliance is justified by the potential market size", }, { id: "differentiation", label: "Whether we have competitive differentiation against existing European players", }, ]; /* ── Set B — Scattered Breadth (4 unknowns, mixed relevance) ─── */ const DOMAIN_1_SCATTERED = [ { id: "scat-demand", label: "Whether we should enter the European market for analytics tools", }, { id: "scat-staff-conflict", label: "Can two senior staff members resolve their ongoing disagreement?", }, { id: "scat-lease", label: "Should the head office lease be renewed at the current rate next year?", }, { id: "scat-pricing", label: "Does an existing unrelated product's pricing align with market willingness to pay?", }, ]; /* ═══════════════════════════════════════════════════════════ * Domain 2 — Community Event (different domain, different vocabulary) * * Tests whether the classifier generalises outside its training domain. * The decision target uses "organise" (not in hasActionKeyword), * and none of the questions match the Exp-21-specific patterns. * ═══════════════════════════════════════════════════════════ */ const DOMAIN_2_DECISION = "Should we organise the community event outdoors this September?"; const DOMAIN_2_COHERENT = [ { id: "evt-weather", label: "Whether there is sufficient weather risk for an outdoor event in September" }, { id: "evt-insurance", label: "What insurance requirements apply for hosting the event outdoors", }, { id: "evt-capacity", label: "Whether the outdoor venue can accommodate expected attendance", }, { id: "evt-accessibility", label: "Whether the outdoor venue meets accessibility requirements for all attendees", }, ]; const DOMAIN_2_SCATTERED = [ { id: "scat-evt-weather", label: "Whether there is sufficient weather risk for an outdoor event in September" }, { id: "scat-board-chairs", label: "Should the board replace its meeting room chairs next month?", }, { id: "scat-volunteer", label: "Whether available volunteers can staff the registration desk on event day", }, { id: "scat-local-park", label: "What local parks offer covered spaces in case of rain?", }, ]; /* ═══════════════════════════════════════════════════════════ * Domain 1 — Coherent set evaluation * ═══════════════════════════════════════════════════════════ */ describe("Experiment 51 — Domain 1 coherent set", () => { let results; beforeAll(() => { const nodes = DOMAIN_1_COHERENT.map((u) => makeUnknown(u.id, u.label)); results = assessSet(nodes, DOMAIN_1_DECISION); }); it("coherent set contains exactly four unknowns", () => { expect(results.length).toBe(4); }); it("demand unknown is classified as could_change_decision (matches DECISION_REVERSAL_PATTERNS)", () => { const r = results.find((r) => r.id === "demand"); expect(r.relevance).toBe("could_change_decision"); expect(typeof r.reason).toBe("string"); expect(r.reason.length).toBeGreaterThan(0); }); it("compliance unknown is classified as supports_decision (matches PRECONDITION_PATTERNS)", () => { const r = results.find((r) => r.id === "compliance"); expect(r.relevance).toBe("supports_decision"); }); it("cost-benefit unknown is classified as supports_decision (matches FEASIBILITY_PATTERNS)", () => { const r = results.find((r) => r.id === "cost-benefit"); expect(r.relevance).toBe("supports_decision"); }); it("differentiation unknown is classified as supports_decision (matches SUPPORTING_CONTEXT_PATTERNS)", () => { const r = results.find((r) => r.id === "differentiation"); expect(r.relevance).toBe("supports_decision"); }); it("every unknown in the coherent set receives a meaningful classification (not cannot_determine)", () => { for (const r of results) { expect(r.relevance).not.toBe("cannot_determine"); } }); it("every unknown includes a non-empty reason", () => { for (const r of results) { expect(typeof r.reason).toBe("string"); expect(r.reason.length).toBeGreaterThan(0); } }); }); /* ═══════════════════════════════════════════════════════════ * Domain 1 — Scattered set evaluation * ═══════════════════════════════════════════════════════════ */ describe("Experiment 51 — Domain 1 scattered set", () => { let results; beforeAll(() => { const nodes = DOMAIN_1_SCATTERED.map((u) => makeUnknown(u.id, u.label)); results = assessSet(nodes, DOMAIN_1_DECISION); }); it("scattered set contains exactly four unknowns", () => { expect(results.length).toBe(4); }); it("demand-like unknown (scat-demand) is classified as could_change_decision", () => { const r = results.find((r) => r.id === "scat-demand"); expect(r.relevance).toBe("could_change_decision"); }); it("staff conflict unknown produces cannot_determine or unlikely_to_change_decision (no pattern match)", () => { const r = results.find((r) => r.id === "scat-staff-conflict"); expect(["cannot_determine", "unlikely_to_change_decision"]).toContain(r.relevance); }); it("lease unknown produces cannot_determine or unlikely_to_change_decision (no pattern match)", () => { const r = results.find((r) => r.id === "scat-lease"); expect(["cannot_determine", "unlikely_to_change_decision"]).toContain(r.relevance); }); it("unrelated product pricing unknown produces cannot_determine or unlikely_to_change_decision (no pattern match)", () => { const r = results.find((r) => r.id === "scat-pricing"); expect(["cannot_determine", "unlikely_to_change_decision"]).toContain(r.relevance); }); it("scattered set contains some classified as cannot_determine or unlikely_to_change_decision", () => { const nonRelevant = results.filter( (r) => r.relevance === "cannot_determine" || r.relevance === "unlikely_to_change_decision" ).length; expect(nonRelevant).toBeGreaterThanOrEqual(2); }); }); /* ═══════════════════════════════════════════════════════════ * Domain 1 — Direct comparison of coherent vs scattered sets * ═══════════════════════════════════════════════════════════ */ describe("Experiment 51 — Domain 1 coherent vs scattered comparison", () => { let coherentResults, scatteredResults; beforeAll(() => { const coherentNodes = DOMAIN_1_COHERENT.map((u) => makeUnknown(u.id, u.label)); const scatteredNodes = DOMAIN_1_SCATTERED.map((u) => makeUnknown(u.id, u.label)); coherentResults = assessSet(coherentNodes, DOMAIN_1_DECISION); scatteredResults = assessSet(scatteredNodes, DOMAIN_1_DECISION); }); it("both sets contain the same number of unknowns", () => { expect(coherentResults.length).toBe(scatteredResults.length); }); it("coherent set produces all relevant classifications (could_change or supports)", () => { const coherentRelevant = coherentResults.filter( (r) => r.relevance === "could_change_decision" || r.relevance === "supports_decision" ).length; expect(coherentRelevant).toBe(4); // all four are relevant to market entry }); it("scattered set contains at least two items classified as irrelevant or cannot_determine", () => { const scatteredNotRelevant = scatteredResults.filter( (r) => r.relevance === "cannot_determine" || r.relevance === "unlikely_to_change_decision" ).length; expect(scatteredNotRelevant).toBeGreaterThanOrEqual(2); }); it("scattered demand-like question matches coherent demand-like classification (same pattern)", () => { const coherentDemand = coherentResults.find((r) => r.id === "demand"); const scatteredDemandLike = scatteredResults.find((r) => r.id === "scat-demand"); expect(scatteredDemandLike.relevance).toBe(coherentDemand.relevance); }); it("deterministic output for coherent set", () => { const nodes = DOMAIN_1_COHERENT.map((u) => makeUnknown(u.id, u.label)); const r1 = assessSet(nodes, DOMAIN_1_DECISION); const r2 = assessSet([...nodes], DOMAIN_1_DECISION); expect(r1).toEqual(r2); }); it("deterministic output for scattered set", () => { const nodes = DOMAIN_1_SCATTERED.map((u) => makeUnknown(u.id, u.label)); const r1 = assessSet(nodes, DOMAIN_1_DECISION); const r2 = assessSet([...nodes], DOMAIN_1_DECISION); expect(r1).toEqual(r2); }); it("inputs are not mutated", () => { const node = makeUnknown("mut-test", "Original label text"); const originalLabel = node.label; const originalDescription = node.description; assessQuestionRelevanceToDecision({ decisionTarget: DOMAIN_1_DECISION, unknown: node }); expect(node.label).toBe(originalLabel); expect(node.description).toBe(originalDescription); }); it("coherent set produces more than one distinct relevance category", () => { const categories = new Set(coherentResults.map((r) => r.relevance)); expect(categories.size).toBeGreaterThan(1); }); }); /* ═══════════════════════════════════════════════════════════ * Domain 2 — Coherent set evaluation (community event) * ═══════════════════════════════════════════════════════════ */ describe("Experiment 51 — Domain 2 coherent set", () => { let results; beforeAll(() => { const nodes = DOMAIN_2_COHERENT.map((u) => makeUnknown(u.id, u.label)); results = assessSet(nodes, DOMAIN_2_DECISION); }); it("coherent set contains exactly four unknowns", () => { expect(results.length).toBe(4); }); it("weather unknown is classified as cannot_determine (cannot generalise to non-market domain)", () => { const r = results.find((r) => r.id === "evt-weather"); // The DECISION_REVERSAL_PATTERNS only match demand/market/need keywords — not "weather risk" // This demonstrates the classifier's language sensitivity expect(r.relevance).toBe("cannot_determine"); }); it("insurance unknown is classified as cannot_determine (no precondition/feasibility pattern match)", () => { const r = results.find((r) => r.id === "evt-insurance"); expect(r.relevance).toBe("cannot_determine"); }); it("capacity unknown is classified as cannot_determine (generic capacity language not in any pattern)", () => { const r = results.find((r) => r.id === "evt-capacity"); expect(r.relevance).toBe("cannot_determine"); }); it("accessibility unknown produces cannot_determine or unlikely_to_change_decision", () => { const r = results.find((r) => r.id === "evt-accessibility"); expect(["cannot_determine", "unlikely_to_change_decision"]).toContain(r.relevance); }); it("coherent set is mixed — not all classified as relevant (classifier cannot generalise)", () => { const categories = new Set(results.map((r) => r.relevance)); expect(categories.has("cannot_determine")).toBe(true); }); }); /* ═══════════════════════════════════════════════════════════ * Domain 2 — Scattered set evaluation (community event) * ═══════════════════════════════════════════════════════════ */ describe("Experiment 51 — Domain 2 scattered set", () => { let results; beforeAll(() => { const nodes = DOMAIN_2_SCATTERED.map((u) => makeUnknown(u.id, u.label)); results = assessSet(nodes, DOMAIN_2_DECISION); }); it("scattered set contains exactly four unknowns", () => { expect(results.length).toBe(4); }); it("board chairs unknown is classified as cannot_determine or unlikely_to_change_decision", () => { const r = results.find((r) => r.id === "scat-board-chairs"); expect(["cannot_determine", "unlikely_to_change_decision"]).toContain(r.relevance); }); it("weather question (appears in both coherent and scattered sets) produces cannot_determine outside training domain", () => { const scatteredWeather = results.find((r) => r.id === "scat-evt-weather"); // Same phrasing as Domain 2 coherent — both produce cannot_determine expect(scatteredWeather.relevance).toBe("cannot_determine"); }); it("volunteer unknown is classified as cannot_determine or unlikely_to_change_decision", () => { const r = results.find((r) => r.id === "scat-volunteer"); expect(["cannot_determine", "unlikely_to_change_decision"]).toContain(r.relevance); }); it("local park unknown is classified as cannot_determine or unlikely_to_change_decision", () => { const r = results.find((r) => r.id === "scat-local-park"); expect(["cannot_determine", "unlikely_to_change_decision"]).toContain(r.relevance); }); }); /* ═══════════════════════════════════════════════════════════ * Domain 2 — Coherent vs scattered comparison (community event) * ═══════════════════════════════════════════════════════════ */ describe("Experiment 51 — Domain 2 coherent vs scattered comparison", () => { let coherentResults, scatteredResults; beforeAll(() => { const coherentNodes = DOMAIN_2_COHERENT.map((u) => makeUnknown(u.id, u.label)); const scatteredNodes = DOMAIN_2_SCATTERED.map((u) => makeUnknown(u.id, u.label)); coherentResults = assessSet(coherentNodes, DOMAIN_2_DECISION); scatteredResults = assessSet(scatteredNodes, DOMAIN_2_DECISION); }); it("both sets contain the same number of unknowns", () => { expect(coherentResults.length).toBe(scatteredResults.length); }); it("domain-2 coherent set does NOT produce all relevant classifications (classifier cannot generalise)", () => { const coherentRelevant = coherentResults.filter( (r) => r.relevance === "could_change_decision" || r.relevance === "supports_decision" ).length; // Only the weather question matches a pattern — rest are cannot_determine expect(coherentRelevant).toBeLessThan(4); }); it("both domains' coherent sets produce fewer than all-relevant classifications (Domain 2 specifically)", () => { const categories = new Set(coherentResults.map((r) => r.relevance)); expect(categories.has("cannot_determine")).toBe(true); }); it("deterministic output for both sets", () => { const cn = DOMAIN_2_COHERENT.map((u) => makeUnknown(u.id, u.label)); const sn = DOMAIN_2_SCATTERED.map((u) => makeUnknown(u.id, u.label)); expect(assessSet([...cn], DOMAIN_2_DECISION)).toEqual(assessSet(cn, DOMAIN_2_DECISION)); expect(assessSet([...sn], DOMAIN_2_DECISION)).toEqual(assessSet(sn, DOMAIN_2_DECISION)); }); }); /* ═══════════════════════════════════════════════════════════ * Language Robustness — Paraphrase evaluation * * One coherent paraphrase and one unrelated paraphrase. * These avoid the most obvious wording from the Exp-21 originals * to test whether the classifier understands relevance or just * recognises familiar keywords. * ═══════════════════════════════════════════════════════════ */ describe("Experiment 51 — Language robustness (paraphrases)", () => { let coherentOriginal; let coherentParaphrase; let unrelatedOriginal; let unrelatedParaphrase; let coherentOriginalResult; let coherentParaphraseResult; let unrelatedOriginalResult; let unrelatedParaphraseResult; beforeAll(() => { // Coherent original uses phrasing the classifier recognises (Exp 21 pattern) coherentOriginal = makeUnknown("coh-orig", "Whether to enter the European market for analytics tools"); // Coherent paraphrase: avoids "enter", "European market" — uses plain English coherentParaphrase = makeUnknown("coh-paraphrased", "Would enough people there actually want what we offer?"); // Unrelated original matches Exp 21's unlikely pattern (benchmark keyword) unrelatedOriginal = makeUnknown("unrel-orig", "What benchmarks do other SaaS companies use for market sizing"); // Unrelated paraphrase: avoids "benchmark" — uses different phrasing unrelatedParaphrase = makeUnknown("unrel-paraphrased", "Which analytics firms set the industry standard?"); coherentOriginalResult = assessQuestionRelevanceToDecision({ decisionTarget: DOMAIN_1_DECISION, unknown: coherentOriginal }); coherentParaphraseResult = assessQuestionRelevanceToDecision({ decisionTarget: DOMAIN_1_DECISION, unknown: coherentParaphrase }); unrelatedOriginalResult = assessQuestionRelevanceToDecision({ decisionTarget: DOMAIN_1_DECISION, unknown: unrelatedOriginal }); unrelatedParaphraseResult = assessQuestionRelevanceToDecision({ decisionTarget: DOMAIN_1_DECISION, unknown: unrelatedParaphrase }); }); it("coherent paraphrase produces cannot_determine (classifier does not recognise the phrasing)", () => { expect(coherentParaphraseResult.relevance).toBe("cannot_determine"); }); it("unrelated paraphrase produces cannot_determine or unlikely_to_change_decision", () => { expect(["cannot_determine", "unlikely_to_change_decision"]).toContain(unrelatedParaphraseResult.relevance); }); it("coherent original and coherent paraphrase produce different classifications (language sensitivity)", () => { expect(coherentOriginalResult.relevance).not.toBe(coherentParaphraseResult.relevance); }); it("unrelated paraphrase is NOT classified as relevant (no regression from paraphrase)", () => { expect(unrelatedParaphraseResult.relevance).not.toBe("could_change_decision"); expect(unrelatedParaphraseResult.relevance).not.toBe("supports_decision"); }); it("both coherent and unrelated produce non-empty reasons (or cannot_determine with reason)", () => { if (coherentOriginalResult.relevance !== "cannot_determine") { expect(typeof coherentOriginalResult.reason).toBe("string"); expect(coherentOriginalResult.reason.length).toBeGreaterThan(0); } }); }); /* ═══════════════════════════════════════════════════════════ * Production classifier unchanged — verification against Exp-21 baseline * ═══════════════════════════════════════════════════════════ */ describe("Experiment 51 — Classifier behaviour consistency check", () => { it("classifier returns same categories as Experiment 21 for known patterns", () => { const goNoGo = assessQuestionRelevanceToDecision({ decisionTarget: "Should we enter the European market?", unknown: makeUnknown("v1", "Whether to proceed with European market entry"), }); expect(goNoGo.relevance).toBe("could_change_decision"); const compliance = assessQuestionRelevanceToDecision({ decisionTarget: "Should we enter the European market?", unknown: makeUnknown("v2", "Whether our product is suitable for European compliance requirements"), }); expect(compliance.relevance).toBe("supports_decision"); const benchmark = assessQuestionRelevanceToDecision({ decisionTarget: "Should we enter the European market?", unknown: makeUnknown("v3", "What benchmarks do other SaaS companies use for market sizing"), }); expect(benchmark.relevance).toBe("unlikely_to_change_decision"); const costBenefit = assessQuestionRelevanceToDecision({ decisionTarget: "Should we enter the European market?", unknown: makeUnknown("v4", "Whether the cost of achieving compliance is justified by the market size"), }); expect(costBenefit.relevance).toBe("supports_decision"); const compDiff = assessQuestionRelevanceToDecision({ decisionTarget: "Should we enter the European market?", unknown: makeUnknown("v5", "Whether we have competitive differentiation against existing European players"), }); expect(compDiff.relevance).toBe("supports_decision"); }); it("classifier does NOT use the decision target for semantic relevance (tests two decisions with same unknown)", () => { // dt1 contains "enter" (an action keyword that enables Rule 1). // dt2 does not contain any action keywords. // Both describe completely unrelated topics (market entry vs. budget). const dt1 = "Should we enter the European market?"; const dt2 = "What is the budget for next fiscal year"; const result1 = assessQuestionRelevanceToDecision({ decisionTarget: dt1, unknown: makeUnknown("same-q", "Whether to proceed with European market entry"), }); const result2 = assessQuestionRelevanceToDecision({ decisionTarget: dt2, unknown: makeUnknown("same-q", "Whether to proceed with European market entry"), }); // The classifier does NOT compare the unknown's meaning against the decision target. // It only checks whether the decision text contains one of five action keywords // (enter/launch/build/stop/abandon) for gating Rule 1. // dt1 has "enter" → Rule 1 succeeds → could_change_decision // dt2 has no action keyword → Rule 1 skips → cannot_determine // This proves: the decision target never provides semantic context for matching. expect(result1.relevance).toBe("could_change_decision"); expect(result2.relevance).toBe("cannot_determine"); }); it("classifier produces more than one category when presented with varied inputs against same decision", () => { const dt = "Should we enter the European market?"; const unknowns = [ makeUnknown("var1", "Whether to proceed with European market entry"), makeUnknown("var2", "What benchmarks do other SaaS companies use for market sizing"), makeUnknown("var3", "Whether our product is suitable for European compliance requirements"), ]; const results = unknowns.map((u) => assessQuestionRelevanceToDecision({ decisionTarget: dt, unknown: u })); const categories = new Set(results.map((r) => r.relevance)); expect(categories.size).toBeGreaterThan(1); }); it("missing inputs produce cannot_determine (not collapse to a single category)", () => { const dt = "Should we enter the European market?"; const noDecision = assessQuestionRelevanceToDecision({ unknown: makeUnknown("no-dt", "Whether to proceed"), }); expect(noDecision.relevance).toBe("cannot_determine"); const emptyDecision = assessQuestionRelevanceToDecision({ decisionTarget: "", unknown: makeUnknown("empty-dt", "Whether to proceed"), }); expect(emptyDecision.relevance).toBe("cannot_determine"); const noUnknown = assessQuestionRelevanceToDecision({ decisionTarget: dt, }); expect(noUnknown.relevance).toBe("cannot_determine"); const noInput = assessQuestionRelevanceToDecision(null); expect(noInput.relevance).toBe("cannot_determine"); }); });