From 2d7a5313c32d2dd63aeecb46e8c6dc203d182c7d Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Fri, 2 Oct 2026 21:18:56 -0400 Subject: [PATCH] fix(gateguard): do not charge the ungated arm with gate-specific holes silence-hole and coverage-hole are claims about the gate, and an ungated arm never fires it, so classify labelled every ungated miss a coverage-hole by construction. In the 48-trial run that padded the hole counts with five rows that say nothing about the gate: coverage-hole read 7 when only 2 were the gate's. Label an ungated miss unaided-miss, which is the baseline rate and is excluded from the holes list. --- docker/gateguard-effectiveness/coverage.js | 12 +++++++++++- tests/docker/gateguard-intent.test.js | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docker/gateguard-effectiveness/coverage.js b/docker/gateguard-effectiveness/coverage.js index aa5f078d0..bee2bc412 100644 --- a/docker/gateguard-effectiveness/coverage.js +++ b/docker/gateguard-effectiveness/coverage.js @@ -23,18 +23,28 @@ const MIN_PAIRS_FOR_A_CLAIM = 20; const CLASSES = Object.freeze({ working: 'the deciding fact was obtained and the outcome is right', lucky: 'right outcome without ever asking: the scenario does not force the question', + 'unaided-miss': 'an ungated arm missed: the baseline rate, not a gate hole', 'follow-through-hole': 'the deciding fact was obtained and the outcome is still wrong', 'targeting-hole': 'the agent asked, but not for the deciding fact', 'silence-hole': 'the gate fired and the agent still did not ask', 'coverage-hole': 'a deciding ambiguity the gate never engaged, and the outcome is wrong' }); -/** Classifies one trial from its behaviour alone. */ +/** + * Classifies one trial from its behaviour alone. + * + * silence-hole and coverage-hole are statements about the gate, so they are only + * reachable for a gated arm. An ungated arm never fires the gate, so without + * this it would have every miss labelled coverage-hole by construction, which + * padded the hole counts with rows that say nothing about the gate. + */ function classify(row) { + const gated = row.gated === undefined ? row.arm !== 'off' : row.gated; const gateFired = Number(row.gateDenials) > 0; if (row.disclosedDecisive) return row.passed ? 'working' : 'follow-through-hole'; if (row.asked) return 'targeting-hole'; if (row.passed) return 'lucky'; + if (!gated) return 'unaided-miss'; return gateFired ? 'silence-hole' : 'coverage-hole'; } diff --git a/tests/docker/gateguard-intent.test.js b/tests/docker/gateguard-intent.test.js index d2c4afaab..0b6059c2c 100644 --- a/tests/docker/gateguard-intent.test.js +++ b/tests/docker/gateguard-intent.test.js @@ -124,6 +124,9 @@ test('classify maps each behaviour to its hole', () => { [{ asked: true, disclosedDecisive: false, passed: true, gateDenials: 1 }, 'targeting-hole'], [{ asked: false, passed: false, gateDenials: 3 }, 'silence-hole'], [{ asked: false, passed: false, gateDenials: 0 }, 'coverage-hole'], + // The gate-specific holes are unreachable for an arm that has no gate, or + // every ungated miss is a coverage-hole by construction. + [{ arm: 'off', gated: false, asked: false, passed: false, gateDenials: 0 }, 'unaided-miss'], [{ asked: false, passed: true, gateDenials: 0 }, 'lucky'] ]; for (const [fields, expected] of cases) {