experiment: narrow acknowledge behaviour eligibility

This commit is contained in:
2026-08-06 17:44:37 +01:00
parent eee8c6b1e4
commit 05d3d96014
2 changed files with 66 additions and 34 deletions
@@ -30,10 +30,29 @@ const PRIORITIES = {
continue: 5, // default
};
/* ── Acknowledge exclusion gate ─────────────────────────────── */
/**
* Deterministic exclusions for Acknowledge.
* Returns true when Acknowledge should not fire, even if its positive trigger matches.
* This gate qualifies the trigger; it does not replace it.
*/
function isAcknowledgeExcluded(assessment) {
// Phase-based exclusions: synthesising and concluding states call for Summarise, not Acknowledge
if (["synthesising", "concluding"].includes(assessment.phase.value)) return true;
// Progress-based exclusion: stalled progress calls for Pause, not Acknowledge
if (assessment.progress.value === "stalled") return true;
// Health-based exclusion: user_overloaded calls for Pause, not Acknowledge
if (assessment.conversationHealth.value === "user_overloaded") return true;
return false;
}
/* ── Selection rules (one rule per behaviour) ─────────────── */
function selectAcknowledge(assessment) {
if (assessment.conversationHealth.value === "healthy" && assessment.phase.confidence !== "low") {
// Apply exclusion gate before returning acknowledge
if (isAcknowledgeExcluded(assessment)) return null;
return {
behaviour: "acknowledge",
confidence: "medium",
+47 -34
View File
@@ -168,12 +168,28 @@ function buildAssessmentInput(turn) {
};
}
/* ═══════════════════════════════════════════════════
Acknowledge exclusion gate — mirrors production rules
═══════════════════════════════════════════════════ */
/**
* Deterministic exclusions for Acknowledge.
* Returns true when Acknowledge should not fire, even if its positive trigger matches.
* Mirrors the production selector's isAcknowledgeExcluded gate.
*/
function isAcknowledgeExcluded(assessment) {
if (["synthesising", "concluding"].includes(assessment.phase.value)) return true;
if (assessment.progress.value === "stalled") return true;
if (assessment.conversationHealth.value === "user_overloaded") return true;
return false;
}
/* ═══════════════════════════════════════════════════
Diagnostic audit helper — checks every rule per turn
═══════════════════════════════════════════════════ */
const BEHAVIOUR_CHECKERS = [
{ key: "acknowledge", fn: (a) => a.conversationHealth.value === "healthy" && a.phase.confidence !== "low" },
{ key: "acknowledge", fn: (a) => a.conversationHealth.value === "healthy" && a.phase.confidence !== "low" && !isAcknowledgeExcluded(a) },
{ 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" },
@@ -341,19 +357,20 @@ describe("Experiment 40 — Behaviour Reachability Diagnostic", () => {
return reachability;
});
it("Acknowledge is eligible in 5 of 7 turns and selected in all 5", () => {
it("Acknowledge is eligible in 3 of 7 turns and selected in 3 — excluded by phase/progress gates in 2 others", () => {
const reachability = global._exp40_reachability || {};
expect(reachability.acknowledge?.eligible).toBe(5);
expect(reachability.acknowledge?.selected).toBe(5);
// With exclusion gate in the diagnostic checker: ack is only eligible in non-excluded turns
expect(reachability.acknowledge?.eligible).toBe(3);
expect(reachability.acknowledge?.selected).toBe(3);
});
it("Summarise is eligible in 2 of 7 turns and always blocked", () => {
it("Summarise is eligible in 2 of 7 turns, selected in 1 via the exclusion gate", () => {
const reachability = global._exp40_reachability || {};
// Terminal state has phase=concluding, so summarise IS eligible
// Terminal state has phase=concluding, so summarise IS eligible and now unblocked (ack excluded)
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");
expect(reachability.summarise?.selected).toBe(1);
expect(reachability.summarise?.blocked).toBe(1);
// The one blocked instance is in long-investigation t1 (focusing with resolvedNodeCount>=3): ack still wins there because focusing isn't excluded
});
it("Clarify is eligible in 0 of 7 turns — never triggered", () => {
@@ -361,13 +378,12 @@ describe("Experiment 40 — Behaviour Reachability Diagnostic", () => {
expect(reachability.clarify?.eligible).toBe(0);
});
it("Pause is eligible in 1 of 7 turns but always blocked", () => {
it("Pause is eligible in 1 of 7 turns and now selected via the exclusion gate", () => {
const reachability = global._exp40_reachability || {};
// Contradictory turn 1: phase=focusing, progress=stalled → pause rule fires
// Acknowledge is excluded here because progress=stalled
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");
expect(reachability.pause?.selected).toBe(1);
});
it("Continue is eligible in 2 of 7 turns and selected in all 2", () => {
@@ -403,13 +419,14 @@ describe("Experiment 40 — Behaviour Reachability Diagnostic", () => {
console.log(` ${marker} ${b.behaviour}: eligible=${b.eligible}${b.blockedBy ? ` blocked by ${b.blockedBy}` : ""}`);
}
// Summarise should be eligible (concluding phase) but blocked
// Summarise should be eligible (concluding phase) and now selected — ack excluded by gate
const summariseAudit = terminalTurn.behaviours.find(b => b.behaviour === "summarise");
expect(summariseAudit.eligible).toBe(true);
expect(summariseAudit.blockedBy).toBe("acknowledge");
expect(summariseAudit.blockedBy).toBe(null);
// The selected behaviour is Acknowledge
expect(terminalTurn.selectedBehaviour).toBe("acknowledge");
// The selected behaviour is Summarise — the terminal turn's concluding phase
// now excludes Acknowledge, allowing Summarise to fire at priority 3.
expect(terminalTurn.selectedBehaviour).toBe("summarise");
});
it("prints turn-level detail for pause-eligible turn (contradictory turn 1)", () => {
@@ -428,7 +445,8 @@ describe("Experiment 40 — Behaviour Reachability Diagnostic", () => {
const pauseAudit = pauseEligibleTurn.behaviours.find(b => b.behaviour === "pause");
expect(pauseAudit.eligible).toBe(true);
expect(pauseAudit.blockedBy).toBe("acknowledge");
// Acknowledge is excluded (stalled progress) so pause is selected, not blocked
expect(pauseAudit.selected).toBe(true);
});
it("Clarify never eligible — no scenario produces too_broad health", () => {
@@ -485,17 +503,12 @@ describe("Experiment 40 — Behaviour Reachability Diagnostic", () => {
expect(terminalTurn.centralStatement).toContain("European market");
});
it("Acknowledge dominance caused by combination of broad eligibility, priority order, and scenario distribution", () => {
it("Acknowledge selected whenever eligible (priority-1 rule confirmed)", () => {
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);
// With exclusion gate: eligible 3/7 (2 excluded by phase/progress gates)
expect(reachability.acknowledge.eligible).toBe(3);
// All 3 eligible instances are selected — priority-1 position confirmed
expect(reachability.acknowledge.selected).toBe(3);
});
});
@@ -702,27 +715,27 @@ describe("Experiment 40 — Behaviour Reachability Diagnostic", () => {
it("classifies each missing behaviour for the final report", () => {
const reachability = global._exp40_reachability || {};
// Summarise: eligible in real turn but blocked → eligible_but_blocked
// Summarise: eligible in 2 real turns, 1 blocked (focusing+steady ack wins), 1 selected (concluding→ack excluded)
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
// Pause: eligible in 1 real turn, now selected via the exclusion gate (stalled→ack excluded)
expect(reachability.pause.eligible).toBeGreaterThan(0);
expect(reachability.pause.blocked).toBeGreaterThan(0);
expect(reachability.pause.selected).toBe(1);
console.log("\n=== Experiment 40 — Final Classifications ===");
console.log(`Summarise: eligible_but_blocked (blocked by acknowledge)`);
console.log(`Summarise: eligible_but_blocked (blocked by acknowledge in focusing phase)`);
console.log(`Clarify: never_eligible_in_tested_scenarios (reachable only in synthetic case)`);
console.log(`Pause: eligible_but_blocked (blocked by acknowledge)`);
console.log(`Pause: selected via exclusion gate (stalled progress excludes ack)`);
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"
pause: "selected_via_exclusion_gate"
};
});
});