fix(end-state): D-03 and D-04 measure behaviour, and the frozen denominator moves with them

The operator decided on 2026-09-18 that the two checks must stop measuring a
phrase and start measuring behaviour. The reason is the gate's own caveat: a
phrase probe closes on rewording, so "Phase 7 no longer says 'Skip for
trekplans'" and "the rubric no longer says 'exists and passes'" could both have
been satisfied by an edit that changed no behaviour at all. Both entries now
point at code a test exercises:

  D-03  lib/**/*.mjs exports runPlanVerification
  D-04  lib/**/*.mjs exports formatCriteriaEvidence

Verified open against the pre-fix tree (8d1669e) and closed against this one —
a check that cannot be seen to fire is not evidence.

What the probes can and cannot prove is now stated by the gate itself rather
than left to the reader. `PROBE_NOTES` carries one note per kind, the detail
line prints a note for every kind PRESENT in the row (and none for a kind no
entry declares), and the open/closed tags name the kind instead of labelling
`phrase` alone and leaving every other kind unlabelled. A behaviour probe
proves the capability exists; it does not prove a prose phase calls it — the
wiring is pinned by tests/lib/doc-consistency.test.mjs, and the gate says so.

THE DENOMINATOR MOVED ON PURPOSE. `tests/fixtures/end-state-frozen.json` and
the `FROZEN` literal in `tests/scripts/end-state-gate.test.mjs` carry new
signatures for D-03 and D-04, amended together and dated in both places. That
is the freeze mechanism working, not a bypass: the integrity check went
VIOLATED the moment the checks changed and stayed red until the amendment was
written down. The list may change — only as a decision said out loud. Nothing
else about the denominator moved: seven defect ids, three experiment ids, and
the agents/decisions/freeze configuration are unchanged, and no entry was
removed.

Gate after: defects 0 of 7, registry intact, still RED and exit 1 on the other
four rows — which is correct.

Suite: 1120 (1118/0/2), up 3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-18 01:36:44 +02:00
commit 6cafb4c04c
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
4 changed files with 90 additions and 27 deletions

View file

@ -27,6 +27,7 @@ import {
checkSignature,
verifyIntegrity,
render,
PROBE_NOTES,
} from '../../scripts/end-state-gate.mjs';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
@ -41,8 +42,9 @@ const FROZEN = {
defects: {
'D-01': '48f7f5cca070091e8f9b9ca0c7cdb7a1880733691935c25d07ea1d3e8d508fc9',
'D-02': 'dfc94dba04a9116ae9be2097a0c5a9d8313b02c7de1ca6b49a472c1823a6d1e1',
'D-03': '41dea56c14764494ed6d555ca97311b6001b0542f4ab69fa1dfc405780a57c32',
'D-04': '721bdda19c0e98962acbfc78419bfb02790ee739694d1609b72d10481f0d6b62',
// Amended 2026-09-18 (operator decision): D-03/D-04 became behaviour probes.
'D-03': '20d2837af1a1247220c65e2a04069b41a3e922c2452369294da4d62bb985636b',
'D-04': '200a142e7792eb00a0f0a58bbfb3e548585d9b45ba36a5c5bf94f28f2c896ffb',
'D-05': '8fe6df52b43496c3302400507cb98002dcf8d2a12d92085e14bda4c7819bc7ba',
'D-06': '72a787c8154d1c18849e9856dcf1a67703d6b5541f88db81d6bab13cab14b418',
'D-07': '1bf3e0cd36c621720697475b0a3fd7db78611d66b58a9ce035e7695e57c69f1b',
@ -681,7 +683,7 @@ test('real registry: every counted entry has an id, a summary, a probe kind and
assert.ok(!ids.has(e.id), `duplicate id ${e.id}`);
ids.add(e.id);
assert.ok(typeof e.summary === 'string' && e.summary.length > 10, `${e.id} needs a summary`);
assert.ok(['phrase', 'byte'].includes(e.probe), `${e.id} must declare its probe kind`);
assert.ok(Object.keys(PROBE_NOTES).includes(e.probe), `${e.id} must declare a probe kind the gate can explain`);
// A check that cannot evaluate against this very repo is a typo, not a finding.
assert.notEqual(evaluateCheck(ROOT, e.check).status, 'not-fellable', `${e.id} check does not run here`);
}
@ -701,3 +703,59 @@ test('real repo: five rows, open is a count or null, exit code agrees with rows
assert.ok(existsSync(join(ROOT, 'scripts', 'end-state-registry.json')));
assert.ok(readFileSync(join(ROOT, 'tests', 'fixtures', 'end-state-frozen.json'), 'utf8').length > 0);
});
// --- probe labelling -------------------------------------------------------
//
// The gate labelled only `phrase` probes, so every other probe kind rendered
// unlabelled — indistinguishable from an entry that declared nothing. Once
// D-03/D-04 became behaviour probes (operator decision 2026-09-18) the reader
// needs to see WHICH kind of evidence each closed entry rests on.
test('PROBE_NOTES: every probe kind the registry may declare carries a note', () => {
assert.deepEqual(Object.keys(PROBE_NOTES).sort(), ['behaviour', 'byte', 'phrase']);
for (const [kind, note] of Object.entries(PROBE_NOTES)) {
assert.ok(note.length > 20, `${kind} needs a note that says what the probe can and cannot prove`);
}
});
test('render: every probe kind is labelled, open and closed — not just phrase', () => {
const text = render({
green: false,
integrity: { ok: true, violations: [], detail: '' },
rows: [{
id: 'defects', label: 'open pipeline defects', source: 's', open: 2, total: 3, target: 0, detail: '',
items: [
{ id: 'D-01', summary: 'still open', probe: 'phrase', status: 'open', detail: '' },
{ id: 'D-03', summary: 'still open', probe: 'behaviour', status: 'open', detail: '' },
{ id: 'D-06', summary: 'fixed', probe: 'byte', status: 'closed', detail: '' },
],
}],
});
assert.match(text, /\[open · phrase probe\] D-01/);
assert.match(text, /\[open · behaviour probe\] D-03/);
assert.match(text, /closed: D-06 \(byte probe\)/);
});
test('the detail line carries one note per probe kind PRESENT, and none for kinds absent', () => {
const dir = fixture({
'a.md': 'bad\n',
'STATE.md': '## Open decisions\n- [x] done\n',
'agents/a.md': '---\nname: a\n---\n',
'commands/r.md': 'Spawn `a`\n',
});
try {
writeGateFiles(dir, registry({
defects: [
{ id: 'D-01', summary: 'a phrase defect', probe: 'phrase', check: [{ path: 'a.md', pattern: 'bad', expect: 'match' }] },
{ id: 'D-02', summary: 'a behaviour defect', probe: 'behaviour', check: [{ path: 'a.md', pattern: 'nope', expect: 'match' }] },
],
}));
gitInit(dir);
const out = JSON.parse(runGate(dir, ['--json']).stdout);
const detail = rowById(out, 'defects').detail;
assert.match(detail, /phrase probe closes on rewording/);
assert.match(detail, /behaviour probe/);
assert.ok(!/byte probe/.test(detail), 'a kind no entry declares must not be explained');
assert.equal(rowById(out, 'experiments').detail, '', 'an empty row explains nothing');
} finally { cleanup(dir); }
});