experiment: audit behaviour reachability and blocking
This commit is contained in:
@@ -0,0 +1,729 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import assessInvestigationState from "@/lib/assessment/investigation-state-assessor.js";
|
||||
import selectBehaviour, { BEHAVIOUR_OPTIONS } from "@/lib/behaviour-selection/behaviour-selector.js";
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
Experiment 40 — Behaviour Reachability Diagnostic
|
||||
═══════════════════════════════════════════════════ */
|
||||
|
||||
/* ── Helpers from the real-assessment test (shared fixtures) ─ */
|
||||
|
||||
function mkN(id, label, opts = {}) {
|
||||
const kind = opts.kind || "unknown";
|
||||
const status = opts.status || (kind === "unknown" ? "unknown" : "known");
|
||||
const confidence = opts.confidence || (kind === "unknown" ? "low" : "high");
|
||||
return {
|
||||
id, label, description: label, kind, status, confidence,
|
||||
evidenceIds: [], dependsOn: [], affects: [], childIds: []
|
||||
};
|
||||
}
|
||||
|
||||
function getEvaluationScenarios() {
|
||||
return {
|
||||
"long-investigation": [
|
||||
{
|
||||
centralStatement: "Should we enter the European market with our SaaS analytics platform?",
|
||||
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")
|
||||
],
|
||||
resolvedNodeIds: [], activeUnknownNodeId: "u-1",
|
||||
selectedQuestion: { nodeId: "u-1", question: "How large and mature is the analytics SaaS market in Europe?", reason: "market_validity" },
|
||||
currentSummary: "We are US-based. The first question before any expansion is whether demand exists.",
|
||||
diagnosticReasoningPattern: "market_validity"
|
||||
},
|
||||
{
|
||||
centralStatement: "Should we enter the European market with our SaaS analytics platform?",
|
||||
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("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", { status: "resolved", confidence: "medium" }),
|
||||
mkN("u-2", "Whether our product is suitable for European compliance requirements", { status: "resolved", confidence: "high" }),
|
||||
mkN("u-3", "Whether the cost of achieving compliance is justified by the market size", { status: "resolved", confidence: "medium" }),
|
||||
mkN("u-4", "Whether we have competitive differentiation against existing European players")
|
||||
],
|
||||
resolvedNodeIds: ["u-1", "u-2", "u-3"], activeUnknownNodeId: "u-4",
|
||||
selectedQuestion: { nodeId: "u-4", question: "What differentiates our platform against established European competitors?", reason: "competitive_analysis" },
|
||||
currentSummary: "Compliance is feasible. The remaining question is competitive edge.",
|
||||
diagnosticReasoningPattern: "competitive_analysis"
|
||||
},
|
||||
{
|
||||
centralStatement: "Should we enter the European market with our SaaS analytics platform?",
|
||||
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", { kind: "observation", status: "provisional", confidence: "medium" }),
|
||||
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", { status: "resolved", confidence: "medium" }),
|
||||
mkN("u-2", "Whether our product is suitable for European compliance requirements", { status: "resolved", confidence: "high" }),
|
||||
mkN("u-3", "Whether the cost of achieving compliance is justified by the market size", { status: "resolved", confidence: "medium" }),
|
||||
mkN("u-4", "Whether we have competitive differentiation against existing European players", { status: "resolved", confidence: "medium" })
|
||||
],
|
||||
resolvedNodeIds: ["u-1", "u-2", "u-3", "u-4"], activeUnknownNodeId: null,
|
||||
selectedQuestion: null, noQuestionReason: "All investigation areas resolved.",
|
||||
currentSummary: "European market entry is justified if compliance is achieved and the real-time collaboration feature is positioned as differentiator.",
|
||||
diagnosticReasoningPattern: null
|
||||
}
|
||||
],
|
||||
|
||||
"contradictory-evidence": [
|
||||
{
|
||||
centralStatement: "Two consultants give opposite recommendations on which supplier to choose for a $2M procurement.",
|
||||
nodes: [
|
||||
mkN("obs-1", "Consultant A recommends Supplier X: lower cost, proven track record", { kind: "observation", status: "known", confidence: "high" }),
|
||||
mkN("obs-2", "Consultant B recommends Supplier Y: better integration capability, higher risk but long-term upside", { kind: "observation", status: "known", confidence: "high" }),
|
||||
mkN("obs-3", "Supplier X has 15+ years in the sector; Supplier Y has 2 years and mixed client reviews", { kind: "observation", status: "known", confidence: "medium" }),
|
||||
mkN("obs-4", "Our current infrastructure is compatible with neither supplier out of the box", { kind: "observation", status: "known", confidence: "high" }),
|
||||
mkN("state-1", "Evaluating $2M procurement against conflicting expert advice", { kind: "state", status: "provisional", confidence: "medium" }),
|
||||
mkN("u-1", "Whether the conflict is genuine or reflects different evaluation criteria")
|
||||
],
|
||||
resolvedNodeIds: [], activeUnknownNodeId: "u-1",
|
||||
selectedQuestion: { nodeId: "u-1", question: "Are the consultants evaluating the same criteria, or are they measuring different things?", reason: "comparability_check" },
|
||||
currentSummary: "Conflicting recommendations from two experts. The first uncertainty is whether we are comparing the same dimensions.",
|
||||
diagnosticReasoningPattern: "comparability_check"
|
||||
},
|
||||
{
|
||||
centralStatement: "Two consultants give opposite recommendations on which supplier to choose for a $2M procurement.",
|
||||
nodes: [
|
||||
mkN("obs-1", "Consultant A recommends Supplier X: lower cost, proven track record", { kind: "observation", status: "known", confidence: "high" }),
|
||||
mkN("obs-2", "Consultant B recommends Supplier Y: better integration capability, higher risk but long-term upside", { kind: "observation", status: "known", confidence: "high" }),
|
||||
mkN("obs-3", "Supplier X has 15+ years in the sector; Supplier Y has 2 years and mixed client reviews", { kind: "observation", status: "known", confidence: "medium" }),
|
||||
mkN("obs-4", "Our current infrastructure is compatible with neither supplier out of the box", { kind: "observation", status: "known", confidence: "high" }),
|
||||
mkN("obs-5", "The consultants used different evaluation weights: cost 60% vs integration 60%", { kind: "observation", status: "known", confidence: "medium" }),
|
||||
mkN("state-1", "Evaluating $2M procurement against conflicting expert advice", { kind: "state", status: "provisional", confidence: "medium" }),
|
||||
mkN("u-1", "Whether the conflict is genuine or reflects different evaluation criteria", { status: "resolved", confidence: "high" }),
|
||||
mkN("u-2", "Which supplier's strengths align with our strategic priorities")
|
||||
],
|
||||
resolvedNodeIds: ["u-1"], activeUnknownNodeId: "u-2",
|
||||
selectedQuestion: { nodeId: "u-2", question: "Does cost or integration capability matter more to the organisation over a 3-year horizon?", reason: "evidence_quality" },
|
||||
currentSummary: "The conflict reflects different evaluation weights. The next uncertainty is strategic alignment.",
|
||||
diagnosticReasoningPattern: "evidence_quality"
|
||||
},
|
||||
{
|
||||
centralStatement: "Two consultants give opposite recommendations on which supplier to choose for a $2M procurement.",
|
||||
nodes: [
|
||||
mkN("obs-1", "Consultant A recommends Supplier X: lower cost, proven track record", { kind: "observation", status: "known", confidence: "high" }),
|
||||
mkN("obs-2", "Consultant B recommends Supplier Y: better integration capability, higher risk but long-term upside", { kind: "observation", status: "known", confidence: "high" }),
|
||||
mkN("obs-3", "Supplier X has 15+ years in the sector; Supplier Y has 2 years and mixed client reviews", { kind: "observation", status: "known", confidence: "medium" }),
|
||||
mkN("obs-4", "Our current infrastructure is compatible with neither supplier out of the box", { kind: "observation", status: "known", confidence: "high" }),
|
||||
mkN("obs-5", "The consultants used different evaluation weights: cost 60% vs integration 60%", { kind: "observation", status: "known", confidence: "medium" }),
|
||||
mkN("obs-6", "Our strategic plan prioritises long-term capability over short-term cost savings", { kind: "observation", status: "known", confidence: "high" }),
|
||||
mkN("state-1", "Evaluating $2M procurement against conflicting expert advice", { kind: "state", status: "provisional", confidence: "medium" }),
|
||||
mkN("u-1", "Whether the conflict is genuine or reflects different evaluation criteria", { status: "resolved", confidence: "high" }),
|
||||
mkN("u-2", "Which supplier's strengths align with our strategic priorities", { status: "resolved", confidence: "medium" }),
|
||||
mkN("u-3", "Whether the integration risk of Supplier Y is manageable with internal resources")
|
||||
],
|
||||
resolvedNodeIds: ["u-1", "u-2"], activeUnknownNodeId: "u-3",
|
||||
selectedQuestion: { nodeId: "u-3", question: "Do we have the internal capacity to manage Supplier Y's integration risk?", reason: "alternative_explanation" },
|
||||
currentSummary: "Strategic priorities favour integration capability. The remaining uncertainty is operational feasibility.",
|
||||
diagnosticReasoningPattern: "alternative_explanation"
|
||||
}
|
||||
],
|
||||
|
||||
"short-early": [
|
||||
{
|
||||
centralStatement: "A manufacturing company reports complaints increased by 35% while production increased by 40%.",
|
||||
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")
|
||||
],
|
||||
resolvedNodeIds: [], activeUnknownNodeId: "u-1",
|
||||
selectedQuestion: { nodeId: "u-1", question: "Were the complaint and production figures measured over the same period?", reason: "comparability_check" },
|
||||
currentSummary: "Two changes have been reported, but we do not yet know whether the figures are directly comparable.",
|
||||
diagnosticReasoningPattern: "comparability_check"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function buildAssessmentInput(turn) {
|
||||
return {
|
||||
situationGraph: {
|
||||
centralStatement: turn.centralStatement,
|
||||
currentSummary: turn.currentSummary,
|
||||
nodes: turn.nodes,
|
||||
edges: turn.edges || [],
|
||||
activeUnknownNodeId: turn.activeUnknownNodeId,
|
||||
resolvedNodeIds: turn.resolvedNodeIds
|
||||
},
|
||||
selectedQuestion: turn.selectedQuestion,
|
||||
noQuestionReason: turn.noQuestionReason,
|
||||
diagnostics: {
|
||||
promptVersion: "v0.4",
|
||||
modelName: "mock-ollama",
|
||||
responseDurationMs: 0,
|
||||
validationStatus: "valid",
|
||||
nodeCount: turn.nodes.length,
|
||||
edgeCount: (turn.edges || []).length,
|
||||
reasoningPattern: turn.diagnosticReasoningPattern || null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
Diagnostic audit helper — checks every rule per turn
|
||||
═══════════════════════════════════════════════════ */
|
||||
|
||||
const BEHAVIOUR_CHECKERS = [
|
||||
{ key: "acknowledge", fn: (a) => a.conversationHealth.value === "healthy" && a.phase.confidence !== "low" },
|
||||
{ key: "clarify", fn: (a) => a.conversationHealth.value === "too_broad" || (a.phase.value === "orienting" && (a.phase.evidence?.observationDensity ?? Infinity) < 3) },
|
||||
{ key: "summarise", fn: (a) => a.phase.value === "synthesising" || a.phase.value === "concluding" || (a.phase.evidence?.resolvedNodeCount >= 3 && a.progress.value === "steady") },
|
||||
{ key: "pause", fn: (a) => (a.phase.value === "focusing" && a.progress.value === "stalled") || a.conversationHealth.value === "user_overloaded" },
|
||||
{ key: "continue", fn: () => true }, // default — always eligible as fallback
|
||||
];
|
||||
|
||||
/**
|
||||
* Audit one assessment against every behaviour rule.
|
||||
* Returns { behaviour, eligible, selected, blockedBy, reason } for each behaviour.
|
||||
*/
|
||||
function auditBehaviour(assessment) {
|
||||
const results = [];
|
||||
const actualResult = selectBehaviour(assessment);
|
||||
|
||||
for (const { key, fn } of BEHAVIOUR_CHECKERS) {
|
||||
if (key === "continue") continue; // handled separately below
|
||||
const eligible = fn(assessment);
|
||||
let blockedBy = null;
|
||||
let selected = false;
|
||||
let reason = "";
|
||||
|
||||
if (eligible) {
|
||||
// Check whether any earlier-priority behaviour fires first
|
||||
const priorityOrder = ["acknowledge", "clarify", "summarise", "pause"];
|
||||
const myIndex = priorityOrder.indexOf(key);
|
||||
for (let i = 0; i < myIndex; i++) {
|
||||
if (BEHAVIOUR_CHECKERS[i].fn(assessment)) {
|
||||
blockedBy = BEHAVIOUR_CHECKERS[i].key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
selected = (blockedBy === null);
|
||||
reason = eligible ? (blockedBy ? `eligible but blocked by ${blockedBy}` : "eligible and selected") : "";
|
||||
} else {
|
||||
reason = `${key} rule conditions not met`;
|
||||
}
|
||||
|
||||
results.push({ behaviour: key, eligible, selected, blockedBy, reason });
|
||||
}
|
||||
|
||||
// Continue is always the fallback when nothing else fires
|
||||
const continueEligible = !results.some(r => r.eligible && r.selected);
|
||||
if (continueEligible) {
|
||||
results.push({ behaviour: "continue", eligible: true, selected: true, blockedBy: null, reason: `No specific rule matched — defaulting to continue` });
|
||||
} else {
|
||||
const blockingBehaviour = results.find(r => r.eligible && r.selected);
|
||||
results.push({
|
||||
behaviour: "continue",
|
||||
eligible: false,
|
||||
selected: false,
|
||||
blockedBy: blockingBehaviour ? blockingBehaviour.behaviour : null,
|
||||
reason: blockingBehaviour ? `blocked by ${blockingBehaviour.behaviour}` : "specific rule matched"
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
Synthetic minimal assessment builders
|
||||
═══════════════════════════════════════════════════ */
|
||||
|
||||
function mkMinimal(opts = {}) {
|
||||
return {
|
||||
version: "v0.1",
|
||||
assessedAt: new Date().toISOString(),
|
||||
confidence: opts.overallConfidence || "medium",
|
||||
phase: opts.phase ?? { value: "cannot_determine", confidence: "low", signals: [], evidence: {} },
|
||||
progress: opts.progress ?? { value: "cannot_determine", confidence: "low", signals: [], evidence: {} },
|
||||
conversationHealth: opts.conversationHealth ?? { value: "cannot_determine", confidence: "low", signals: [], evidence: {} }
|
||||
};
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════
|
||||
Test suite — Experiment 40
|
||||
═══════════════════════════════════════════════════ */
|
||||
|
||||
describe("Experiment 40 — Behaviour Reachability Diagnostic", () => {
|
||||
|
||||
/* ── Scenario A: Real-scenario reachability audit ─────── */
|
||||
|
||||
describe("Real-scenario reachability (all Experiment 39 turns)", () => {
|
||||
let allAudits = {};
|
||||
|
||||
function runAllAudits() {
|
||||
if (Object.keys(allAudits).length > 0) return allAudits;
|
||||
const scenarios = getEvaluationScenarios();
|
||||
for (const [scenarioName, turns] of Object.entries(scenarios)) {
|
||||
allAudits[scenarioName] = [];
|
||||
for (let i = 0; i < turns.length; i++) {
|
||||
const input = buildAssessmentInput(turns[i]);
|
||||
const assessment = assessInvestigationState(input);
|
||||
const behaviourResult = selectBehaviour(assessment);
|
||||
allAudits[scenarioName].push({
|
||||
turn: i,
|
||||
centralStatement: (turns[i].centralStatement || "").substring(0, 50),
|
||||
phase: assessment.phase.value,
|
||||
progress: assessment.progress.value,
|
||||
health: assessment.conversationHealth.value,
|
||||
phaseConfidence: assessment.phase.confidence,
|
||||
selectedBehaviour: behaviourResult.behaviour,
|
||||
behaviours: auditBehaviour(assessment)
|
||||
});
|
||||
}
|
||||
}
|
||||
return allAudits;
|
||||
}
|
||||
|
||||
it("audits every turn across all scenarios", () => {
|
||||
const audits = runAllAudits();
|
||||
expect(audits["long-investigation"]).toHaveLength(3);
|
||||
expect(audits["contradictory-evidence"]).toHaveLength(3);
|
||||
expect(audits["short-early"]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("summarises reachability for each behaviour across real turns", () => {
|
||||
const audits = runAllAudits();
|
||||
const reachability = {};
|
||||
|
||||
for (const beh of ["acknowledge", "clarify", "summarise", "pause", "continue"]) {
|
||||
let eligibleCount = 0;
|
||||
let selectedCount = 0;
|
||||
let blockedCount = 0;
|
||||
const blockingSources = new Set();
|
||||
const neverEligibleScenarios = new Set();
|
||||
|
||||
for (const [scenarioName, turns] of Object.entries(audits)) {
|
||||
let scenarioEverEligible = false;
|
||||
for (const turnAudit of turns) {
|
||||
const behAudit = turnAudit.behaviours.find(b => b.behaviour === beh);
|
||||
if (behAudit.eligible) {
|
||||
eligibleCount++;
|
||||
scenarioEverEligible = true;
|
||||
if (behAudit.selected) selectedCount++;
|
||||
else { blockedCount++; blockingSources.add(behAudit.blockedBy); }
|
||||
}
|
||||
}
|
||||
if (!scenarioEverEligible) neverEligibleScenarios.add(scenarioName);
|
||||
}
|
||||
|
||||
reachability[beh] = {
|
||||
eligible: eligibleCount,
|
||||
selected: selectedCount,
|
||||
blocked: blockedCount,
|
||||
blockingBy: Array.from(blockingSources),
|
||||
neverEligibleInScenarios: Array.from(neverEligibleScenarios)
|
||||
};
|
||||
}
|
||||
|
||||
console.log("\n=== Experiment 40 — Reachability Summary ===");
|
||||
for (const [beh, data] of Object.entries(reachability)) {
|
||||
console.log(`\n${beh.toUpperCase()}:`);
|
||||
console.log(` eligible: ${data.eligible}`);
|
||||
console.log(` selected: ${data.selected}`);
|
||||
console.log(` blocked: ${data.blocked} (by: ${data.blockingBy.join(", ") || "none"})`);
|
||||
if (data.neverEligibleInScenarios.length) {
|
||||
console.log(` never eligible in: ${data.neverEligibleInScenarios.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Store for downstream assertions
|
||||
global._exp40_reachability = reachability;
|
||||
global._exp40_audits = audits;
|
||||
|
||||
return reachability;
|
||||
});
|
||||
|
||||
it("Acknowledge is eligible in 5 of 7 turns and selected in all 5", () => {
|
||||
const reachability = global._exp40_reachability || {};
|
||||
expect(reachability.acknowledge?.eligible).toBe(5);
|
||||
expect(reachability.acknowledge?.selected).toBe(5);
|
||||
});
|
||||
|
||||
it("Summarise is eligible in 2 of 7 turns and always blocked", () => {
|
||||
const reachability = global._exp40_reachability || {};
|
||||
// Terminal state has phase=concluding, so summarise IS eligible
|
||||
expect(reachability.summarise?.eligible).toBe(2);
|
||||
// But acknowledge (priority 1) fires first because health=healthy
|
||||
expect(reachability.summarise?.blocked).toBe(2);
|
||||
expect(reachability.summarise?.blockingBy).toContain("acknowledge");
|
||||
});
|
||||
|
||||
it("Clarify is eligible in 0 of 7 turns — never triggered", () => {
|
||||
const reachability = global._exp40_reachability || {};
|
||||
expect(reachability.clarify?.eligible).toBe(0);
|
||||
});
|
||||
|
||||
it("Pause is eligible in 1 of 7 turns but always blocked", () => {
|
||||
const reachability = global._exp40_reachability || {};
|
||||
// Contradictory turn 1: phase=focusing, progress=stalled → pause rule fires
|
||||
expect(reachability.pause?.eligible).toBe(1);
|
||||
// But acknowledge (priority 1) also fires (health=healthy), blocking pause
|
||||
expect(reachability.pause?.blocked).toBe(1);
|
||||
expect(reachability.pause?.blockingBy).toContain("acknowledge");
|
||||
});
|
||||
|
||||
it("Continue is eligible in 2 of 7 turns and selected in all 2", () => {
|
||||
const reachability = global._exp40_reachability || {};
|
||||
expect(reachability.continue?.eligible).toBe(2);
|
||||
expect(reachability.continue?.selected).toBe(2);
|
||||
});
|
||||
|
||||
it("all scenarios and turns are audited with full detail", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
for (const [scenarioName, turns] of Object.entries(audits)) {
|
||||
for (const t of turns) {
|
||||
expect(t.phase).toBeDefined();
|
||||
expect(t.progress).toBeDefined();
|
||||
expect(t.health).toBeDefined();
|
||||
expect(["acknowledge", "clarify", "summarise", "pause", "continue"].includes(t.selectedBehaviour)).toBe(true);
|
||||
expect(t.behaviours).toHaveLength(5); // all five behaviours checked
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("prints turn-level detail for the terminal long-investigation state", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
const terminalTurn = audits["long-investigation"][2];
|
||||
expect(terminalTurn.phase).toBe("concluding");
|
||||
expect(terminalTurn.health).toBe("healthy");
|
||||
|
||||
console.log("\n=== Terminal Turn Detail ===");
|
||||
console.log(`Scenario: long-investigation, turn ${terminalTurn.turn}`);
|
||||
console.log(` phase=${terminalTurn.phase}, progress=${terminalTurn.progress}, health=${terminalTurn.health}`);
|
||||
for (const b of terminalTurn.behaviours) {
|
||||
const marker = b.selected ? "▶" : b.eligible ? "⚠ blocked" : "✗";
|
||||
console.log(` ${marker} ${b.behaviour}: eligible=${b.eligible}${b.blockedBy ? ` blocked by ${b.blockedBy}` : ""}`);
|
||||
}
|
||||
|
||||
// Summarise should be eligible (concluding phase) but blocked
|
||||
const summariseAudit = terminalTurn.behaviours.find(b => b.behaviour === "summarise");
|
||||
expect(summariseAudit.eligible).toBe(true);
|
||||
expect(summariseAudit.blockedBy).toBe("acknowledge");
|
||||
|
||||
// The selected behaviour is Acknowledge
|
||||
expect(terminalTurn.selectedBehaviour).toBe("acknowledge");
|
||||
});
|
||||
|
||||
it("prints turn-level detail for pause-eligible turn (contradictory turn 1)", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
const pauseEligibleTurn = audits["contradictory-evidence"][1];
|
||||
expect(pauseEligibleTurn.phase).toBe("focusing");
|
||||
expect(pauseEligibleTurn.progress).toBe("stalled");
|
||||
|
||||
console.log("\n=== Pause-Eligible Turn Detail ===");
|
||||
console.log(`Scenario: contradictory-evidence, turn ${pauseEligibleTurn.turn}`);
|
||||
console.log(` phase=${pauseEligibleTurn.phase}, progress=${pauseEligibleTurn.progress}, health=${pauseEligibleTurn.health}`);
|
||||
for (const b of pauseEligibleTurn.behaviours) {
|
||||
const marker = b.selected ? "▶" : b.eligible ? "⚠ blocked" : "✗";
|
||||
console.log(` ${marker} ${b.behaviour}: eligible=${b.eligible}${b.blockedBy ? ` blocked by ${b.blockedBy}` : ""}`);
|
||||
}
|
||||
|
||||
const pauseAudit = pauseEligibleTurn.behaviours.find(b => b.behaviour === "pause");
|
||||
expect(pauseAudit.eligible).toBe(true);
|
||||
expect(pauseAudit.blockedBy).toBe("acknowledge");
|
||||
});
|
||||
|
||||
it("Clarify never eligible — no scenario produces too_broad health", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
for (const [scenarioName, turns] of Object.entries(audits)) {
|
||||
for (const t of turns) {
|
||||
expect(t.health).not.toBe("too_broad");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("Clarify never eligible — no scenario produces orienting phase with < 3 obs", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
for (const [scenarioName, turns] of Object.entries(audits)) {
|
||||
for (const t of turns) {
|
||||
expect(t.phase).not.toBe("orienting");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("Summarise eligible ONLY in the terminal long-investigation turn", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
let summariseTurns = [];
|
||||
for (const [scenarioName, turns] of Object.entries(audits)) {
|
||||
for (let i = 0; i < turns.length; i++) {
|
||||
const sBeh = turns[i].behaviours.find(b => b.behaviour === "summarise");
|
||||
if (sBeh.eligible) summariseTurns.push(`${scenarioName}-t${i}`);
|
||||
}
|
||||
}
|
||||
expect(summariseTurns).toEqual(["long-investigation-t1", "long-investigation-t2"]);
|
||||
});
|
||||
|
||||
it("Pause eligible ONLY in contradictory-evidence turn 1", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
let pauseTurns = [];
|
||||
for (const [scenarioName, turns] of Object.entries(audits)) {
|
||||
for (let i = 0; i < turns.length; i++) {
|
||||
const pBeh = turns[i].behaviours.find(b => b.behaviour === "pause");
|
||||
if (pBeh.eligible) pauseTurns.push(`${scenarioName}-t${i}`);
|
||||
}
|
||||
}
|
||||
expect(pauseTurns).toEqual(["contradictory-evidence-t1"]);
|
||||
});
|
||||
|
||||
it("terminal long-investigation turn IS included in the audit (confirms Experiment 39 coverage)", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
// Exp 39 tested turns: 0, 3, and 4 (complete). Turn 4 IS the terminal.
|
||||
// Our getEvaluationScenarios maps to indices 0, 1, 2 — index 2 is the terminal state.
|
||||
const terminalTurn = audits["long-investigation"][2];
|
||||
expect(terminalTurn.selectedQuestion).toBeFalsy();
|
||||
expect(terminalTurn.activeUnknownNodeId).toBeFalsy();
|
||||
// Phase must be concluding for a terminal assessment
|
||||
expect(terminalTurn.phase).toBe("concluding");
|
||||
expect(terminalTurn.centralStatement).toContain("European market");
|
||||
});
|
||||
|
||||
it("Acknowledge dominance caused by combination of broad eligibility, priority order, and scenario distribution", () => {
|
||||
const reachability = global._exp40_reachability || {};
|
||||
// Acknowledge eligible 5/7 — that's the broad eligibility factor
|
||||
expect(reachability.acknowledge.eligible).toBe(5);
|
||||
// It is selected every time it is eligible — that's the priority order factor
|
||||
expect(reachability.acknowledge.blocked).toBe(0);
|
||||
// 5/7 = 71% — scenario distribution (3 scenarios, most turns have healthy health)
|
||||
const totalTurns = global._exp40_audits
|
||||
? Object.values(global._exp40_audits).reduce((sum, arr) => sum + arr.length, 0)
|
||||
: 7;
|
||||
expect(reachability.acknowledge.eligible / totalTurns).toBeGreaterThan(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Scenario B: Synthetic isolated reachability ─────── */
|
||||
|
||||
describe("Synthetic reachability — each behaviour in isolation", () => {
|
||||
it("Acknowledge is reachable when health=healthy and phase confidence ≠ low", () => {
|
||||
const result = selectBehaviour(mkMinimal({
|
||||
phase: { value: "exploring", confidence: "medium" },
|
||||
conversationHealth: { value: "healthy", confidence: "high" }
|
||||
}));
|
||||
expect(result.behaviour).toBe("acknowledge");
|
||||
});
|
||||
|
||||
it("Clarify is reachable when health=too_broad (no other rule fires first)", () => {
|
||||
const result = selectBehaviour(mkMinimal({
|
||||
phase: { value: "exploring", confidence: "low" }, // low → no acknowledge
|
||||
conversationHealth: { value: "too_broad", confidence: "high" }
|
||||
}));
|
||||
expect(result.behaviour).toBe("clarify");
|
||||
});
|
||||
|
||||
it("Clarify is reachable when phase=orienting and observationDensity < 3", () => {
|
||||
const result = selectBehaviour(mkMinimal({
|
||||
phase: { value: "orienting", confidence: "low", evidence: { observationDensity: 1 } },
|
||||
conversationHealth: { value: "cannot_determine", confidence: "low" }
|
||||
}));
|
||||
expect(result.behaviour).toBe("clarify");
|
||||
});
|
||||
|
||||
it("Summarise is reachable when phase=synthesising (no health trigger to block)", () => {
|
||||
const result = selectBehaviour(mkMinimal({
|
||||
phase: { value: "synthesising", confidence: "high" },
|
||||
conversationHealth: { value: "cannot_determine", confidence: "low" } // no healthy → no acknowledge
|
||||
}));
|
||||
expect(result.behaviour).toBe("summarise");
|
||||
});
|
||||
|
||||
it("Summarise is reachable when phase=concluding (no health trigger to block)", () => {
|
||||
const result = selectBehaviour(mkMinimal({
|
||||
phase: { value: "concluding", confidence: "high" },
|
||||
conversationHealth: { value: "cannot_determine", confidence: "low" } // no healthy → no acknowledge
|
||||
}));
|
||||
expect(result.behaviour).toBe("summarise");
|
||||
});
|
||||
|
||||
it("Summarise is reachable when resolvedNodeCount >= 3 and progress=steady (no health trigger)", () => {
|
||||
const result = selectBehaviour(mkMinimal({
|
||||
phase: { value: "focusing", confidence: "high", evidence: { resolvedNodeCount: 4 } },
|
||||
progress: { value: "steady", confidence: "medium" },
|
||||
conversationHealth: { value: "too_narrow", confidence: "low" } // no healthy → no acknowledge
|
||||
}));
|
||||
expect(result.behaviour).toBe("summarise");
|
||||
});
|
||||
|
||||
it("Pause is reachable when phase=focusing + progress=stalled (no health trigger)", () => {
|
||||
const result = selectBehaviour(mkMinimal({
|
||||
phase: { value: "focusing", confidence: "high" },
|
||||
progress: { value: "stalled", confidence: "high" },
|
||||
conversationHealth: { value: "cannot_determine", confidence: "low" } // no healthy → no acknowledge
|
||||
}));
|
||||
expect(result.behaviour).toBe("pause");
|
||||
});
|
||||
|
||||
it("Pause is reachable when health=user_overloaded", () => {
|
||||
const result = selectBehaviour(mkMinimal({
|
||||
phase: { value: "exploring", confidence: "medium" },
|
||||
conversationHealth: { value: "user_overloaded", confidence: "medium" }
|
||||
}));
|
||||
expect(result.behaviour).toBe("pause");
|
||||
});
|
||||
|
||||
it("Continue is reachable when no rule conditions match", () => {
|
||||
const result = selectBehaviour(mkMinimal({
|
||||
phase: { value: "exploring", confidence: "low" },
|
||||
progress: { value: "cannot_determine", confidence: "low" },
|
||||
conversationHealth: { value: "cannot_determine", confidence: "low" }
|
||||
}));
|
||||
expect(result.behaviour).toBe("continue");
|
||||
});
|
||||
|
||||
it("each synthetic case confirms the rule is independently reachable", () => {
|
||||
const tests = [
|
||||
{ beh: "acknowledge", a: mkMinimal({ phase: { value: "focusing", confidence: "high" }, conversationHealth: { value: "healthy", confidence: "high" } }) },
|
||||
{ beh: "clarify", a: mkMinimal({ phase: { value: "exploring", confidence: "low" }, conversationHealth: { value: "too_broad", confidence: "high" } }) },
|
||||
{ beh: "summarise", a: mkMinimal({ phase: { value: "concluding", confidence: "high" }, conversationHealth: { value: "cannot_determine", confidence: "low" } }) },
|
||||
{ beh: "pause", a: mkMinimal({ phase: { value: "focusing", confidence: "high" }, progress: { value: "stalled", confidence: "high" }, conversationHealth: { value: "cannot_determine", confidence: "low" } }) },
|
||||
{ beh: "continue", a: mkMinimal({ phase: { value: "deepening", confidence: "medium" }, progress: { value: "steady", confidence: "medium" }, conversationHealth: { value: "cannot_determine", confidence: "low" } }) },
|
||||
];
|
||||
|
||||
const results = {};
|
||||
for (const t of tests) {
|
||||
results[t.beh] = selectBehaviour(t.a).behaviour;
|
||||
}
|
||||
|
||||
expect(results.acknowledge).toBe("acknowledge");
|
||||
expect(results.clarify).toBe("clarify");
|
||||
expect(results.summarise).toBe("summarise");
|
||||
expect(results.pause).toBe("pause");
|
||||
expect(results.continue).toBe("continue");
|
||||
|
||||
console.log("\n=== Synthetic Reachability ===");
|
||||
for (const [beh, result] of Object.entries(results)) {
|
||||
console.log(` ${beh}: ${result === beh ? "✓ reachable" : "✗ NOT reachable"}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Diagnostic correctness tests ────────────────────── */
|
||||
|
||||
describe("Diagnostic accuracy", () => {
|
||||
it("selected behaviour matches the real selector output", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
let allMatch = true;
|
||||
for (const [scenarioName, turns] of Object.entries(audits)) {
|
||||
for (const t of turns) {
|
||||
const input = buildAssessmentInput(getEvaluationScenarios()[scenarioName][t.turn]);
|
||||
const assessment = assessInvestigationState(input);
|
||||
const selectorResult = selectBehaviour(assessment);
|
||||
if (selectorResult.behaviour !== t.selectedBehaviour) allMatch = false;
|
||||
}
|
||||
}
|
||||
expect(allMatch).toBe(true);
|
||||
});
|
||||
|
||||
it("identical inputs produce identical diagnostics every time", () => {
|
||||
const scenarios = getEvaluationScenarios();
|
||||
for (const [scenarioName, turns] of Object.entries(scenarios)) {
|
||||
for (let i = 0; i < turns.length; i++) {
|
||||
const input = buildAssessmentInput(turns[i]);
|
||||
const assessment = assessInvestigationState(input);
|
||||
const diagnostic1 = auditBehaviour(assessment);
|
||||
const diagnostic2 = auditBehaviour(assessment);
|
||||
expect(JSON.stringify(diagnostic1)).toBe(JSON.stringify(diagnostic2));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("inputs are not mutated by audit function", () => {
|
||||
const scenarios = getEvaluationScenarios();
|
||||
for (const [scenarioName, turns] of Object.entries(scenarios)) {
|
||||
for (let i = 0; i < turns.length; i++) {
|
||||
const input = buildAssessmentInput(turns[i]);
|
||||
const snapshot = JSON.stringify(input);
|
||||
const assessment = assessInvestigationState(JSON.parse(snapshot));
|
||||
auditBehaviour(assessment);
|
||||
expect(JSON.stringify(input)).toBe(snapshot);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("eligible behaviours are identified correctly for each turn", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
let correctCount = 0;
|
||||
let totalCount = 0;
|
||||
|
||||
for (const [scenarioName, turns] of Object.entries(audits)) {
|
||||
for (const t of turns) {
|
||||
const input = buildAssessmentInput(getEvaluationScenarios()[scenarioName][t.turn]);
|
||||
const assessment = assessInvestigationState(input);
|
||||
|
||||
for (const behaviour of ["acknowledge", "clarify", "summarise", "pause"]) {
|
||||
totalCount++;
|
||||
const expectedEligible = auditBehaviour(assessment).find(b => b.behaviour === behaviour).eligible;
|
||||
if (expectedEligible === t.behaviours.find(b => b.behaviour === behaviour).eligible) {
|
||||
correctCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(correctCount).toBe(totalCount);
|
||||
});
|
||||
|
||||
it("blocked behaviours name the correct earlier winning rule", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
for (const [scenarioName, turns] of Object.entries(audits)) {
|
||||
for (const t of turns) {
|
||||
const blockedBehaviours = t.behaviours.filter(b => b.eligible && !b.selected);
|
||||
for (const b of blockedBehaviours) {
|
||||
// The blocking behaviour must have fired BEFORE this one in priority order
|
||||
const priorityOrder = ["acknowledge", "clarify", "summarise", "pause"];
|
||||
const blockerIdx = priorityOrder.indexOf(b.blockedBy);
|
||||
const behIdx = priorityOrder.indexOf(b.behaviour);
|
||||
expect(blockerIdx).toBeLessThan(behIdx);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("all Experiment 39 turns are audited (7 total)", () => {
|
||||
const audits = global._exp40_audits || {};
|
||||
let totalTurns = 0;
|
||||
for (const turns of Object.values(audits)) {
|
||||
totalTurns += turns.length;
|
||||
}
|
||||
expect(totalTurns).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Classification summary ──────────────────────────── */
|
||||
|
||||
describe("Reachability classification", () => {
|
||||
it("classifies each missing behaviour for the final report", () => {
|
||||
const reachability = global._exp40_reachability || {};
|
||||
|
||||
// Summarise: eligible in real turn but blocked → eligible_but_blocked
|
||||
expect(reachability.summarise.eligible).toBeGreaterThan(0);
|
||||
expect(reachability.summarise.blocked).toBeGreaterThan(0);
|
||||
|
||||
// Clarify: never eligible in tested scenarios, reachable only synthetically
|
||||
expect(reachability.clarify.eligible).toBe(0);
|
||||
|
||||
// Pause: eligible in real turn but blocked
|
||||
expect(reachability.pause.eligible).toBeGreaterThan(0);
|
||||
expect(reachability.pause.blocked).toBeGreaterThan(0);
|
||||
|
||||
console.log("\n=== Experiment 40 — Final Classifications ===");
|
||||
console.log(`Summarise: eligible_but_blocked (blocked by acknowledge)`);
|
||||
console.log(`Clarify: never_eligible_in_tested_scenarios (reachable only in synthetic case)`);
|
||||
console.log(`Pause: eligible_but_blocked (blocked by acknowledge)`);
|
||||
console.log(`Acknowledge: dominant because health=healthy is the most common state`);
|
||||
|
||||
global._exp40_classifications = {
|
||||
summarise: "eligible_but_blocked",
|
||||
clarify: "never_eligible_in_tested_scenarios",
|
||||
pause: "eligible_but_blocked"
|
||||
};
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user