doxa-maps/internal/check-done-evidence.mjs
doxa-maps-88655c70 28ce04f6ac check:done-evidence knew one card schema and failed the best-evidenced cards on the board
A done card with no record is the reporting failure this loop exists to avoid — so a
gate that mis-identifies WHICH cards lack a record is worse than none: it teaches the
next agent to pad the field the checker reads.

★ THE THREE FLAGGED CARDS WERE AMONG THE BEST-EVIDENCED THERE ARE. Each carried
one_line, why, and a `verify` block naming the probe, its assertion count and its
negative tests — "74 files match byte for byte, all three drift classes exit 1",
"9 assertions, all pass", "two new assertions, both negative-tested".

The gate looked only for `bluf` or `fields`. MEASURED across 443 done cards: 40 carry
`verify`, 41 carry `one_line`, and ALL THREE flagged cards had one or both. `verify`
is if anything the STRONGER signal — it names how the claim was checked, which is
what evidence means here.

Fixing the CARDS by adding a redundant bluf would have been gaming the check. Fixing
the GATE to recognise the evidence already there is the honest repair.

★ THE BASELINE IS NOW EMPTY. All ten exemptions reported STALE — every one had since
gained evidence — so the list exempted nothing while implying ten cards still owed a
record. An EMPTY set is not an unset one: every done card must now carry evidence
with NO exemptions, and the gate fails outright for a single card that does not.
Keeping ten satisfied ids would mean the same today and quietly permit a regression
the moment someone reused an entry. Same reasoning as emptying the i18n DEBT map.

MUTATION-TESTED both directions: a done card with NOTHING in any of the four fields
FAILS by name; the same card with ONLY a `verify` entry PASSES.

Wired into gates:post (30 -> 31) after confirming it reads only backlog JSON, which
the build does not mutate. The doc quotes gates:post, so it is re-transcribed in the
same commit — the self-referential update noted last tick, handled in one pass this
time.

Build: 7/7 pre, 23/23 bundles, 31/31 post, exit 0.
2026-09-24 11:38:17 +00:00

129 lines
7 KiB
JavaScript

#!/usr/bin/env node
/**
* check-done-evidence.mjs — a card may not reach status:done with NO evidence.
*
* WHY (2026-09-21)
* Two quality cards were status:done with an EMPTY bluf. Both turned out genuinely done — but
* only because I opened a browser and read the source. A board-wide count then found 18 done
* cards carrying NEITHER a bluf NOR any fields: a status word and nothing else.
*
* ★ THE HAND-OFF IS THE POINT. The Doxa team will read this board to learn what exists. A card
* reading "done" with no evidence cannot be reconciled against the code at all — they cannot
* tell a shipped deliverable from an abandoned one someone closed. This flight found SIX cards
* whose stated status disagreed with reality; an evidence-less card is the unfalsifiable case.
*
* WHAT COUNTS AS EVIDENCE (deliberately generous — this is a floor, not a style guide)
* EITHER a non-empty `bluf` OR a non-empty `fields` object. Both are free to write at the moment
* the work is fresh, which is the only moment the evidence is cheap and accurate.
*
* BASELINE, NOT A CLIFF
* The 10 pre-existing offenders are allow-listed by id. A gate that fails on day one for history
* nobody can fix gets disabled, and then it protects nothing. This fails ONLY on a NEW one — the
* same shape as check-label-joins, which allow-lists two known occurrences.
* ★ TO CLEAR AN ENTRY: add real evidence to the card, then delete its id here. The list should
* only ever shrink; a PR that grows it is doing the thing this gate exists to stop.
*
* NEGATIVE-TESTED: adding a fresh done card with no bluf/fields makes this FAIL naming it,
* EXIT=1; removing it returns PASS.
*/
import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
// ★ DEPTH 2, NOT 1 — and that is deliberate but load-bearing. Every sibling gate in internal/
// resolves ROOT one level up (the repo). This one goes TWO levels up, because the backlogs it
// checks live in `_qa-cycle/`, which is AGENTIC SCRATCH OUTSIDE THE REPO — tracked by no git
// repository, and never cloned with the studio.
//
// THE HAND-OFF DEFECT THIS GUARD FIXES (measured 2026-09-22). On this machine _qa-cycle happens
// to sit beside the repo, so the gate passes. An inheritor who clones ONLY the studio has no
// sibling _qa-cycle — and the gate did NOT fail there. It printed
// "0 card(s) across 3 backlog(s), 0 done" ... "PASS"
// and exited 0. A VACUOUSLY GREEN gate is worse than no gate: it certifies nothing while looking
// healthy, which is exactly the failure shape this repo keeps re-learning.
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
const QA = join(ROOT, '_qa-cycle')
if (!existsSync(QA)) {
console.log('DONE-EVIDENCE: SKIP — no _qa-cycle/ beside this repo, so there are no backlogs to check.')
console.log(' This gate reads the AGENTIC BACKLOGS, which live outside the repo and are not cloned')
console.log(' with it. Absence is expected for anyone who checked out the studio on its own; it is')
console.log(' NOT a pass. Run it from a workspace that has _qa-cycle/ as a sibling of this repo.')
process.exit(0)
}
const BACKLOGS = [
'01-ideation-research/backlog.json',
'02-builder-cycle/backlog.json',
'03-deployment-cycle/backlog.json',
]
// Pre-existing, from before the rule. Verified 2026-09-21. SHRINK ONLY.
// ★ THE BASELINE IS NOW EMPTY, and that is the strongest state this gate can be in.
// All ten exemptions had since gained evidence — the gate reported every one of them
// as STALE on 2026-09-24 — so the list was pure dead weight: ten ids that exempted
// nothing while implying ten cards still owed a record.
// An EMPTY set is not an unset one: every done card must now carry evidence, with no
// exemptions, and the gate fails outright for a single card that does not. Keeping
// `{ ... }` full of satisfied ids would mean the same today and quietly permit a
// regression the moment someone "reused" an entry.
// If a genuinely un-reconstructable card ever needs an exemption, add its id here WITH
// the reason — and the gate will tell you the day it becomes stale.
const BASELINE = new Set([
])
let checked = 0, done = 0
const offenders = []
const staleBaseline = []
for (const rel of BACKLOGS) {
const p = join(QA, rel)
if (!existsSync(p)) continue
let cards
try { cards = JSON.parse(readFileSync(p, 'utf8')).cards || [] } catch { continue }
for (const c of cards) {
checked++
if (c.status !== 'done') continue
done++
const hasBluf = String(c.bluf || '').trim().length > 0
const hasFields = c.fields && Object.keys(c.fields).length > 0
// ★ THE BOARD USES TWO CARD SCHEMAS, and this gate only knew one. Older cards carry
// `bluf` + `fields`; a second family carries `one_line` (what was done), `why`, and
// `verify` (the artifact and how it was checked). MEASURED 2026-09-24 across 443 done
// cards: 40 carry `verify` and 41 carry `one_line`, and ALL THREE cards this gate was
// failing had one or both — with named probes, assertion counts and negative tests.
// They were among the best-evidenced cards on the board.
// `verify` is if anything the STRONGER signal: it names how the claim was checked,
// which is exactly what "evidence" means here. Demanding a redundant `bluf` alongside
// it would have taught the next agent to pad the field the checker reads rather than
// write the record — the gate training the behaviour it exists to prevent.
const hasVerify = String(c.verify || '').trim().length > 0
const hasOneLine = String(c.one_line || '').trim().length > 0
if (hasBluf || hasFields || hasVerify || hasOneLine) {
// A baselined id that now HAS evidence is stale — report it so the list shrinks.
if (BASELINE.has(c.id)) staleBaseline.push(`${c.id} (${rel})`)
continue
}
if (BASELINE.has(c.id)) continue
offenders.push(`${c.id} (${rel})`)
}
}
console.log(`DONE-EVIDENCE: ${checked} card(s) across ${BACKLOGS.length} backlog(s), ${done} done, ` +
`${BASELINE.size} baselined.`)
for (const s of staleBaseline) {
console.log(` i BASELINE NOW STALE — ${s} has evidence; remove its id from check-done-evidence.mjs`)
}
if (offenders.length) {
console.error(`\nDONE-EVIDENCE: FAIL — ${offenders.length} card(s) are done with NO evidence in any field:`)
for (const o of offenders) console.error(` ✗ ${o}`)
console.error(
'\n Add ONE of: `bluf` or `one_line` saying what was done, `verify` naming how it was checked,\n' +
' or a `fields` entry naming the artifact. Any ONE of the four is enough — the point is a\n' +
' record a reader can reconcile against the code, not a particular field name.\n' +
' it was verified. Write it now, while the work is fresh — reconstructing it later produces a\n' +
' plausible story indistinguishable from a real record.'
)
process.exit(1)
}
console.log('DONE-EVIDENCE: PASS — every done card carries evidence (bluf, one_line, verify or fields).')