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:
parent
c23b009738
commit
6cafb4c04c
4 changed files with 90 additions and 27 deletions
|
|
@ -160,14 +160,27 @@ export function verifyIntegrity(registry, frozen) {
|
|||
|
||||
const notMeasured = (detail) => ({ open: null, total: null, items: [], detail });
|
||||
|
||||
// What each probe kind can and cannot prove. An entry declares its kind; the
|
||||
// gate states the limit out loud, because a closed check is only ever as strong
|
||||
// as the thing it reads. Every kind a registry entry may declare belongs here —
|
||||
// the entry-shape test rejects a kind the gate cannot explain.
|
||||
export const PROBE_NOTES = {
|
||||
phrase:
|
||||
'a phrase probe closes on rewording: a closed phrase probe is evidence, not proof of behaviour',
|
||||
behaviour:
|
||||
'a behaviour probe reads code a test exercises: it proves the capability exists, not that a prose phase calls it (the wiring is pinned by the suite, not by this probe)',
|
||||
byte:
|
||||
"a byte probe reads the file's bytes, so it closes only on a real change to them",
|
||||
};
|
||||
|
||||
function tallyEntries(root, entries) {
|
||||
const items = entries.map((e) => ({ id: e.id, summary: e.summary, probe: e.probe, ...evaluateCheck(root, e.check) }));
|
||||
const phrase = items.some((i) => i.probe === 'phrase');
|
||||
const kinds = Object.keys(PROBE_NOTES).filter((k) => items.some((i) => i.probe === k));
|
||||
return {
|
||||
open: items.filter((i) => i.status !== 'closed').length,
|
||||
total: items.length,
|
||||
items,
|
||||
detail: phrase ? 'a phrase probe closes on rewording: a closed phrase probe is evidence, not proof of behaviour' : '',
|
||||
detail: kinds.map((k) => PROBE_NOTES[k]).join('; '),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -377,12 +390,12 @@ export function render(result) {
|
|||
for (const i of r.items) {
|
||||
if (i.status === 'closed') continue;
|
||||
let tag = i.status === 'not-fellable' ? 'NOT FELLABLE, counted open' : 'open';
|
||||
if (i.probe === 'phrase') tag += ' · phrase probe';
|
||||
if (i.probe) tag += ` · ${i.probe} probe`;
|
||||
out.push(` [${tag}] ${i.id}: ${i.summary}${i.detail ? ` — ${i.detail}` : ''}`);
|
||||
}
|
||||
const closed = r.items.filter((i) => i.status === 'closed' && i.probe);
|
||||
if (closed.length > 0) {
|
||||
out.push(` closed: ${closed.map((i) => (i.probe === 'phrase' ? `${i.id} (phrase probe)` : i.id)).join(', ')}`);
|
||||
out.push(` closed: ${closed.map((i) => `${i.id} (${i.probe} probe)`).join(', ')}`);
|
||||
}
|
||||
}
|
||||
return out.join('\n');
|
||||
|
|
|
|||
|
|
@ -35,33 +35,25 @@
|
|||
{
|
||||
"id": "D-03",
|
||||
"summary": "commands/trekexecute.md never runs a trekplan's `## Verification` (where the brief's success criteria land) on the single-session path: Phase 7 says 'Skip for trekplans', and only the multi-session wave path runs master verification",
|
||||
"closesWhen": "Phase 7 no longer skips trekplans (proxy: the fix must also make that path run the plan's Verification; Phase 4's entry-condition skip is legitimate and out of scope)",
|
||||
"probe": "phrase",
|
||||
"closesWhen": "a runnable plan-verification runner exists in lib/ (behaviour probe: it proves the capability exists and is exercised by a test, not that Phase 7 calls it — the wiring is pinned by tests/lib/doc-consistency.test.mjs)",
|
||||
"probe": "behaviour",
|
||||
"check": [
|
||||
{
|
||||
"path": "commands/trekexecute.md",
|
||||
"section": "## Phase 7 —",
|
||||
"pattern": "^\\*\\*Skip for trekplans\\.\\*\\*",
|
||||
"flags": "m",
|
||||
"expect": "match"
|
||||
"path": "lib/**/*.mjs",
|
||||
"pattern": "export function runPlanVerification",
|
||||
"expect": "no-match"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "D-04",
|
||||
"summary": "agents/brief-conformance-reviewer.md must judge whether a success criterion's verification command 'exists and passes', but its tools are Read/Glob/Grep, so it cannot run anything",
|
||||
"closesWhen": "the rubric no longer asks it to judge 'passes', or the reviewer can execute the command",
|
||||
"probe": "phrase",
|
||||
"closesWhen": "the evidence block the conformance reviewer judges is BUILT IN CODE (behaviour probe: it proves /trekreview can hand over a real exit code instead of asking a Read/Glob/Grep agent whether a command passes; the rubric rewrite and the hand-over are pinned by tests/lib/doc-consistency.test.mjs)",
|
||||
"probe": "behaviour",
|
||||
"check": [
|
||||
{
|
||||
"path": "agents/brief-conformance-reviewer.md",
|
||||
"pattern": "exists and passes",
|
||||
"expect": "match"
|
||||
},
|
||||
{
|
||||
"path": "agents/brief-conformance-reviewer.md",
|
||||
"pattern": "^tools:.*\"Bash\"",
|
||||
"flags": "m",
|
||||
"path": "lib/**/*.mjs",
|
||||
"pattern": "export function formatCriteriaEvidence",
|
||||
"expect": "no-match"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
6
tests/fixtures/end-state-frozen.json
vendored
6
tests/fixtures/end-state-frozen.json
vendored
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"why": "Frozen 2026-09-17, when the end-state direction was chosen. These ids and check signatures are the end-state gate's denominator: an entry may CLOSE, it may never disappear or have its check changed. Changing this file is a decision, not a refactor - tests/scripts/end-state-gate.test.mjs pins it literally.",
|
||||
"why": "Frozen 2026-09-17, when the end-state direction was chosen. These ids and check signatures are the end-state gate's denominator: an entry may CLOSE, it may never disappear or have its check changed. Changing this file is a decision, not a refactor - tests/scripts/end-state-gate.test.mjs pins it literally. Amended 2026-09-18 by operator decision: D-03 and D-04 were rewritten from phrase probes to behaviour probes, so both signatures moved on purpose. That is the mechanism working, not a bypass - the list may change, but only as a decision that is said out loud.",
|
||||
"defects": {
|
||||
"D-01": "48f7f5cca070091e8f9b9ca0c7cdb7a1880733691935c25d07ea1d3e8d508fc9",
|
||||
"D-02": "dfc94dba04a9116ae9be2097a0c5a9d8313b02c7de1ca6b49a472c1823a6d1e1",
|
||||
"D-03": "41dea56c14764494ed6d555ca97311b6001b0542f4ab69fa1dfc405780a57c32",
|
||||
"D-04": "721bdda19c0e98962acbfc78419bfb02790ee739694d1609b72d10481f0d6b62",
|
||||
"D-03": "20d2837af1a1247220c65e2a04069b41a3e922c2452369294da4d62bb985636b",
|
||||
"D-04": "200a142e7792eb00a0f0a58bbfb3e548585d9b45ba36a5c5bf94f28f2c896ffb",
|
||||
"D-05": "8fe6df52b43496c3302400507cb98002dcf8d2a12d92085e14bda4c7819bc7ba",
|
||||
"D-06": "72a787c8154d1c18849e9856dcf1a67703d6b5541f88db81d6bab13cab14b418",
|
||||
"D-07": "1bf3e0cd36c621720697475b0a3fd7db78611d66b58a9ce035e7695e57c69f1b"
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue