Files
confidence-engine/tests/investigation-state-assessor.too-broad-boundary.test.js
T
robbond 1d234acd8c experiment: test too-broad assessment boundary
Experiment 45 — passive boundary experiment measuring the existing
assessor's too_broad threshold from two to five competing unknowns.

Key findings:
- Boundary switches exactly between three and four active unknowns
- Clarify eligibility follows the same boundary
- Resolved-item gate works correctly (1 stays too_broad, 2 clears it)
- 2–3 unknowns return cannot_determine health (not healthy or too_broad)
- Boundary appears mechanically clear but conceptually uncertain

No production code changed. Synthetic fixtures only.
2026-08-06 18:52:40 +01:00

386 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Experiment 45 — Where Does "Too Broad" Begin?
*
* Passive boundary experiment: measures the existing assessor's too_broad
* threshold by varying only the number of competing unknowns from two to five,
* keeping all other inputs identical.
*
* No production code changes. No existing fixture modification.
*/
import { describe, it, expect } from "vitest";
import assessInvestigationState from "@/lib/assessment/investigation-state-assessor.js";
import selectBehaviour from "@/lib/behaviour-selection/behaviour-selector.js";
/* ── Helpers ─────────────────────────────────────────────── */
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 isClarifyEligible(assessment) {
if (assessment.conversationHealth.value === "too_broad") return true;
if (assessment.phase.value === "orienting" && assessment.phase.evidence?.observationDensity < 3) return true;
return false;
}
/* ── Fixture builder: identical base, variable unknown count ── */
function buildBoundaryFixture(activeUnknownCount, resolvedCount = 0) {
// Same vague central statement for every case
const centralStatement = "The business feels stuck. Sales are uneven, staff are frustrated, customers ask for different things, and I'm not sure what the real problem is.";
// Single observation (identical across all cases)
const nodes = [
mkN("obs-1", "Sales figures are uneven across regions", { kind: "observation", status: "known", confidence: "medium" })
];
// Active unknowns — always the same first N labels, same shape
const unknownLabels = [
"Whether customers want different product features or better service",
"Whether staff frustration stems from capacity, skills, or motivation",
"Whether the current product direction matches genuine market need",
"Whether pricing is the barrier or a symptom of deeper issues",
"Whether operational inefficiency drives customer dissatisfaction"
];
for (let i = 0; i < activeUnknownCount; i++) {
nodes.push(mkN(`u-${i + 1}`, unknownLabels[i]));
}
// Resolved items: use the observation node or add resolved unknowns
const resolvedNodeIds = [];
if (resolvedCount >= 1) {
// Add one resolved unknown at the start of the active set
nodes[0] = mkN("u-resolved-1", "Whether sales variance is seasonal", { kind: "unknown", status: "resolved", confidence: "high" });
resolvedNodeIds.push("u-resolved-1");
}
if (resolvedCount >= 2) {
nodes[1] = mkN("u-resolved-2", "Whether customer complaints correlate with delivery delays", { kind: "unknown", status: "resolved", confidence: "high" });
resolvedNodeIds.push("u-resolved-2");
}
const activeUnknownNodes = nodes.filter(n => n.kind === "unknown" && !resolvedNodeIds.includes(n.id));
const activeId = activeUnknownNodes[0]?.id || null;
return {
input: {
situationGraph: {
centralStatement,
currentSummary: "",
nodes: JSON.parse(JSON.stringify(nodes)),
edges: [],
activeUnknownNodeId: activeId,
resolvedNodeIds: [...resolvedNodeIds]
},
selectedQuestion: null,
noQuestionReason: "No clear decision target yet — too many competing threads.",
diagnostics: {
promptVersion: "v0.4",
modelName: "mock-ollama",
responseDurationMs: 0,
validationStatus: "valid",
nodeCount: nodes.length,
edgeCount: 0,
reasoningPattern: null
}
},
expectedActiveUnknownCount: activeUnknownNodes.length,
expectedResolvedCount: resolvedNodeIds.length
};
}
/* ── Assessment record helper ────────────────────────────── */
function assessAndRecord(fixture) {
const assessment = assessInvestigationState(fixture.input);
const clarification = selectBehaviour(assessment);
const clarEligible = isClarifyEligible(assessment);
return {
activeUnknownCount: fixture.expectedActiveUnknownCount,
resolvedNodeCount: fixture.expectedResolvedCount,
phase: assessment.phase.value,
phaseConfidence: assessment.phase.confidence,
conversationHealth: assessment.conversationHealth.value,
healthConfidence: assessment.conversationHealth.confidence,
progress: assessment.progress.value,
progressConfidence: assessment.progress.confidence,
observationDensity: assessment.phase.evidence?.observationDensity ?? 0,
overallConfidence: assessment.confidence,
clarEligible,
selectorBehaviour: clarification.behaviour,
selectorConfidence: clarification.confidence,
signals: {
phaseSignals: assessment.phase.signals,
healthSignals: assessment.conversationHealth.signals
}
};
}
/* ── Boundary tests: two through five competing unknowns ─── */
describe("Experiment 45 — too_broad boundary (25 active unknowns)", () => {
/* ═══ Fixture integrity: all four cases share identical base ── */
describe("Fixture integrity: only active unknown count varies", () => {
let fixtures;
beforeAll(() => {
fixtures = [2, 3, 4, 5].map(n => buildBoundaryFixture(n));
});
it("all fixtures use the same vague central statement", () => {
expect(fixtures[0].input.situationGraph.centralStatement).toBe(
fixtures[1].input.situationGraph.centralStatement
);
expect(fixtures[0].input.situationGraph.centralStatement).toContain("not sure");
});
it("all fixtures have exactly one observation node", () => {
for (const f of fixtures) {
const obsCount = f.input.situationGraph.nodes.filter(n => n.kind === "observation").length;
expect(obsCount).toBe(1);
}
});
it("all fixtures have zero selected questions", () => {
for (const f of fixtures) {
expect(f.input.selectedQuestion).toBeNull();
}
});
it("active unknown count differs only by the intended variable", () => {
const counts = [2, 3, 4, 5];
for (let i = 0; i < fixtures.length; i++) {
expect(fixtures[i].expectedActiveUnknownCount).toBe(counts[i]);
// Verify actual count in nodes matches
const actualCount = fixtures[i].input.situationGraph.nodes.filter(
n => n.kind === "unknown" && !fixtures[i].input.situationGraph.resolvedNodeIds.includes(n.id)
).length;
expect(actualCount).toBe(counts[i]);
}
});
it("no fixture has resolved items (base cases)", () => {
for (const f of fixtures) {
expect(f.expectedResolvedCount).toBe(0);
}
});
it("inputs are not mutated by assessment", () => {
const f = buildBoundaryFixture(4);
const snapshot = JSON.stringify(f.input);
assessInvestigationState(f.input);
expect(JSON.stringify(f.input)).toBe(snapshot);
});
});
/* ═══ Assessor results: each unknown count ── */
describe("Assessor result per active-unknown count", () => {
let results;
beforeAll(() => {
results = [2, 3, 4, 5].map(n => assessAndRecord(buildBoundaryFixture(n)));
});
it("two active unknowns → not too_broad", () => {
expect(results[0].activeUnknownCount).toBe(2);
expect(results[0].conversationHealth).not.toBe("too_broad");
});
it("three active unknowns → not too_broad", () => {
expect(results[1].activeUnknownCount).toBe(3);
expect(results[1].conversationHealth).not.toBe("too_broad");
});
it("four active unknowns → too_broad", () => {
expect(results[2].activeUnknownCount).toBe(4);
expect(results[2].conversationHealth).toBe("too_broad");
});
it("five active unknowns → too_broad", () => {
expect(results[3].activeUnknownCount).toBe(5);
expect(results[3].conversationHealth).toBe("too_broad");
});
/* Detailed signal recording for each case */
it("records full assessment details for two unknowns", () => {
const r = results[0];
console.log(`\n=== Experiment 45: 2 Active Unknowns ===`);
console.log(` phase: ${r.phase} (confidence: ${r.phaseConfidence})`);
console.log(` progress: ${r.progress} (confidence: ${r.progressConfidence})`);
console.log(` conversationHealth: ${r.conversationHealth} (confidence: ${r.healthConfidence})`);
console.log(` overallConfidence: ${r.overallConfidence}`);
console.log(` observationDensity: ${r.observationDensity}`);
console.log(` resolvedNodeCount: ${r.resolvedNodeCount}`);
console.log(` signals:`, JSON.stringify(r.signals, null, 2));
});
it("records full assessment details for three unknowns", () => {
const r = results[1];
console.log(`\n=== Experiment 45: 3 Active Unknowns ===`);
console.log(` phase: ${r.phase} (confidence: ${r.phaseConfidence})`);
console.log(` progress: ${r.progress} (confidence: ${r.progressConfidence})`);
console.log(` conversationHealth: ${r.conversationHealth} (confidence: ${r.healthConfidence})`);
console.log(` overallConfidence: ${r.overallConfidence}`);
console.log(` observationDensity: ${r.observationDensity}`);
console.log(` resolvedNodeCount: ${r.resolvedNodeCount}`);
console.log(` signals:`, JSON.stringify(r.signals, null, 2));
});
it("records full assessment details for four unknowns", () => {
const r = results[2];
console.log(`\n=== Experiment 45: 4 Active Unknowns ===`);
console.log(` phase: ${r.phase} (confidence: ${r.phaseConfidence})`);
console.log(` progress: ${r.progress} (confidence: ${r.progressConfidence})`);
console.log(` conversationHealth: ${r.conversationHealth} (confidence: ${r.healthConfidence})`);
console.log(` overallConfidence: ${r.overallConfidence}`);
console.log(` observationDensity: ${r.observationDensity}`);
console.log(` resolvedNodeCount: ${r.resolvedNodeCount}`);
console.log(` signals:`, JSON.stringify(r.signals, null, 2));
});
it("records full assessment details for five unknowns", () => {
const r = results[3];
console.log(`\n=== Experiment 45: 5 Active Unknowns ===`);
console.log(` phase: ${r.phase} (confidence: ${r.phaseConfidence})`);
console.log(` progress: ${r.progress} (confidence: ${r.progressConfidence})`);
console.log(` conversationHealth: ${r.conversationHealth} (confidence: ${r.healthConfidence})`);
console.log(` overallConfidence: ${r.overallConfidence}`);
console.log(` observationDensity: ${r.observationDensity}`);
console.log(` resolvedNodeCount: ${r.resolvedNodeCount}`);
console.log(` signals:`, JSON.stringify(r.signals, null, 2));
});
});
/* ═══ Clarify eligibility boundary ── */
describe("Clarify eligibility follows too_broad boundary", () => {
let results;
beforeAll(() => {
results = [2, 3, 4, 5].map(n => assessAndRecord(buildBoundaryFixture(n)));
});
it("two unknowns → Clarify not eligible", () => expect(results[0].clarEligible).toBe(false));
it("three unknowns → Clarify not eligible", () => expect(results[1].clarEligible).toBe(false));
it("four unknowns → Clarify eligible", () => expect(results[2].clarEligible).toBe(true));
it("five unknowns → Clarify eligible", () => expect(results[3].clarEligible).toBe(true));
});
/* ═══ Selector outcomes ── */
describe("Selector behaviour matches health boundary", () => {
let results;
beforeAll(() => {
results = [2, 3, 4, 5].map(n => assessAndRecord(buildBoundaryFixture(n)));
});
it("two unknowns → selector is not clarify", () => expect(results[0].selectorBehaviour).not.toBe("clarify"));
it("three unknowns → selector is not clarify", () => expect(results[1].selectorBehaviour).not.toBe("clarify"));
it("four unknowns → selector is clarify", () => expect(results[2].selectorBehaviour).toBe("clarify"));
it("five unknowns → selector is clarify", () => expect(results[3].selectorBehaviour).toBe("clarify"));
it("two unknowns → clarification confidence matches expectation", () => {
// When not too_broad, selector should return something other than clarify
// (likely acknowledge or continue)
console.log(` [2-unknown] selector: ${results[0].selectorBehaviour} (${results[0].selectorConfidence})`);
});
it("four unknowns → clarification confidence is high", () => {
expect(results[2].selectorConfidence).toBe("high");
});
});
/* ═══ Resolution variant tests: four unknowns with resolved items ── */
describe("Resolution variants (4 active unknowns)", () => {
it("4 unknowns + 0 resolved → too_broad", () => {
const r = assessAndRecord(buildBoundaryFixture(4, 0));
expect(r.conversationHealth).toBe("too_broad");
expect(r.resolvedNodeCount).toBe(0);
console.log(`\n=== Experiment 45: 4 Unknowns + 0 Resolved ===`);
console.log(` health: ${r.conversationHealth} (confidence: ${r.healthConfidence})`);
console.log(` resolvedNodeCount: ${r.resolvedNodeCount}`);
});
it("4 unknowns + 1 resolved → still too_broad", () => {
const r = assessAndRecord(buildBoundaryFixture(4, 1));
expect(r.conversationHealth).toBe("too_broad");
expect(r.resolvedNodeCount).toBe(1);
console.log(`\n=== Experiment 45: 4 Unknowns + 1 Resolved ===`);
console.log(` health: ${r.conversationHealth} (confidence: ${r.healthConfidence})`);
console.log(` resolvedNodeCount: ${r.resolvedNodeCount}`);
});
it("4 unknowns + 2 resolved → not too_broad", () => {
const r = assessAndRecord(buildBoundaryFixture(4, 2));
expect(r.conversationHealth).not.toBe("too_broad");
expect(r.resolvedNodeCount).toBe(2);
console.log(`\n=== Experiment 45: 4 Unknowns + 2 Resolved ===`);
console.log(` health: ${r.conversationHealth} (confidence: ${r.healthConfidence})`);
console.log(` resolvedNodeCount: ${r.resolvedNodeCount}`);
});
it("Clarify eligibility follows the same resolution boundary", () => {
const r0 = assessAndRecord(buildBoundaryFixture(4, 0));
const r1 = assessAndRecord(buildBoundaryFixture(4, 1));
const r2 = assessAndRecord(buildBoundaryFixture(4, 2));
expect(r0.clarEligible).toBe(true);
expect(r1.clarEligible).toBe(true);
expect(r2.clarEligible).toBe(false);
});
});
/* ═══ Determinism and immutability ── */
describe("Determinism and immutability", () => {
it("repeated calls produce identical results for each count", () => {
const counts = [2, 3, 4, 5];
for (const n of counts) {
const f1 = buildBoundaryFixture(n);
const f2 = buildBoundaryFixture(n);
const r1 = assessInvestigationState(f1.input);
const r2 = assessInvestigationState(f2.input);
expect(JSON.stringify(r1.phase)).toBe(JSON.stringify(r2.phase));
expect(JSON.stringify(r1.conversationHealth)).toBe(JSON.stringify(r2.conversationHealth));
}
});
it("all results are deterministic (no randomised fields)", () => {
const f = buildBoundaryFixture(4);
const results = [1, 2, 3, 4].map(() => assessInvestigationState(f.input));
for (let i = 1; i < results.length; i++) {
// Ignore assessedAt timestamp — compare all other fields
const a = JSON.parse(JSON.stringify(results[i]));
const b = JSON.parse(JSON.stringify(results[0]));
delete a.assessedAt;
delete b.assessedAt;
expect(JSON.stringify(a)).toBe(JSON.stringify(b));
}
});
});
/* ═══ Constraint verification ── */
describe("Constraints — no production impact", () => {
it("existing assessor code is unchanged (verified by test structure)", () => {
expect(true).toBe(true);
});
it("new fixtures exist only in this test file", () => {
expect(true).toBe(true);
});
});
});