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.
This commit is contained in:
montjeffrey
2026-10-04 22:40:42 -04:00
parent f2c0cc8734
commit 2d7a5313c3
2 changed files with 14 additions and 1 deletions
+11 -1
View File
@@ -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';
}
+3
View File
@@ -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) {