Some checks are pending
build-gate / build (push) Waiting to run
Cards were being written with explanatory statuses -- "done — reconciled: …", "blocked — Steps 1+3 DONE and re-verified…", "ready". The prose is valuable and several of those strings carry measurements, but it made status unqueryable: every count used status !== 'done', which is FALSE for "done — reconciled: …", so finished cards kept counting as open. Measured across the three phase backlogs: 130 of 395 cards carried a prose status and 71 of those were already done. Reported open was 176 against a real 105. The P0 figure quoted every tick was 21; it was 14. That is worse than an off-by-N because a backlog is a decision instrument. It answers "what is left" and "is the rest human-gated", and inflating the open count makes a drained board look stalled, which pushes toward manufacturing work instead of holding honestly. The error also hides: every card reads correctly to a human, because the prose says exactly what is true. Only the aggregate is wrong. Migration preserved every explanation in fields.status_detail -- 130 of 130, none lost -- and normalised status to the enum. Six cards whose head-word was not in the map were left alone and resolved by hand against reality rather than guessed, since a wrong normalisation hides a card instead of counting it. The gate does not require status_detail. Most cards need no explanation and demanding one turns a real check into paperwork satisfied with filler. Negative-tested: restoring one prose status fails by name and prints it.
102 lines
5 KiB
JavaScript
102 lines
5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-backlog-status-enum.mjs — a card's `status` must be a machine-readable enum, not prose.
|
|
*
|
|
* THE DEFECT THIS CATCHES, and it was MY OWN reporting error for an entire flight. Cards were being
|
|
* written with explanatory statuses:
|
|
*
|
|
* "done — reconciled: big-picture + dashboard-plus-grid are current, unified-research-dashboard…"
|
|
* "blocked — Steps 1+3 DONE and re-verified 2026-09-22 at 19/19 bundles…"
|
|
* "ready"
|
|
*
|
|
* The prose is genuinely valuable — several of those strings carry measurements — but it made
|
|
* `status` unqueryable. Every count I reported used `status !== 'done'`, which is FALSE for
|
|
* `"done — reconciled: …"`, so finished cards kept being counted as open.
|
|
*
|
|
* MEASURED 2026-09-22 across the three phase backlogs: 130 of 395 cards carried a prose status, and
|
|
* 71 of those were ALREADY DONE. Reported open was 176; the real number was 105. The P0 figure I had
|
|
* been quoting every tick was 21; it was actually 14.
|
|
*
|
|
* ★ WHY THIS IS WORSE THAN AN ORDINARY OFF-BY-N. A backlog is a decision instrument — it answers
|
|
* "what is left" and "is the remaining work human-gated". Inflating the open count makes a drained
|
|
* board look stalled, which pushes toward manufacturing work rather than holding honestly. And the
|
|
* error is self-concealing: every individual card reads correctly to a human, because the prose says
|
|
* exactly what is true. Only the AGGREGATE is wrong.
|
|
*
|
|
* THE FIX PRESERVES THE PROSE. Migration moved it to `fields.status_detail` and normalised `status`
|
|
* to the enum; 130/130 preserved, 0 lost. The explanation was never the problem — putting it in the
|
|
* field that tooling reads was.
|
|
*
|
|
* WHAT IT ASSERTS: every card's `status` is one of todo / doing / in-progress / blocked / done.
|
|
*
|
|
* It deliberately does NOT require `status_detail` to exist. Most cards need no explanation, and
|
|
* demanding one would turn a real check into paperwork that gets satisfied with filler.
|
|
*
|
|
* NEGATIVE-TESTED: setting one card's status back to "done — reconciled: …" fails by name.
|
|
*/
|
|
import { readFileSync, existsSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
const QA = join(ROOT, '../_qa-cycle')
|
|
const PHASES = ['01-ideation-research', '02-builder-cycle', '03-deployment-cycle']
|
|
const ALLOWED = new Set(['todo', 'doing', 'in-progress', 'blocked', 'done'])
|
|
|
|
if (!existsSync(QA)) {
|
|
// _qa-cycle lives outside every repo the CI clones, so absence is EXPECTED there. Say so loudly
|
|
// rather than passing silently — a gate that reports success when it never ran is the failure
|
|
// mode this repo has already been bitten by.
|
|
console.log('BACKLOG-STATUS: SKIP — _qa-cycle not present (expected in CI; it is in no repo).')
|
|
process.exit(0)
|
|
}
|
|
|
|
const offenders = []
|
|
let total = 0
|
|
const counts = {}
|
|
|
|
for (const ph of PHASES) {
|
|
const p = join(QA, ph, 'backlog.json')
|
|
if (!existsSync(p)) continue
|
|
let cards
|
|
try {
|
|
cards = JSON.parse(readFileSync(p, 'utf8')).cards || []
|
|
} catch (e) {
|
|
console.error(`BACKLOG-STATUS: FAIL — ${ph}/backlog.json does not parse: ${e.message}`)
|
|
process.exit(1)
|
|
}
|
|
for (const c of cards) {
|
|
total++
|
|
const s = String(c.status)
|
|
counts[s] = (counts[s] || 0) + 1
|
|
if (!ALLOWED.has(s)) offenders.push({ ph, id: c.id, s })
|
|
}
|
|
}
|
|
|
|
if (!total) {
|
|
console.error('BACKLOG-STATUS: FAIL — found 0 cards across three phase backlogs.')
|
|
console.error(' A gate with nothing to assert is not passing; it is not running.')
|
|
process.exit(1)
|
|
}
|
|
|
|
if (offenders.length) {
|
|
console.error(`BACKLOG-STATUS: FAIL — ${offenders.length} of ${total} card(s) carry a non-enum status.\n`)
|
|
for (const o of offenders.slice(0, 12)) {
|
|
console.error(` ${o.ph} ${o.id}`)
|
|
console.error(` status: "${o.s.slice(0, 84)}${o.s.length > 84 ? '…' : ''}"`)
|
|
}
|
|
if (offenders.length > 12) console.error(` …and ${offenders.length - 12} more`)
|
|
console.error('\n Allowed: ' + [...ALLOWED].join(' / '))
|
|
console.error(' Keep the explanation — move it to fields.status_detail. The prose is valuable and')
|
|
console.error(' often carries measurements; it just must not live in the field tooling filters on.')
|
|
console.error(' A prose status makes every open-count wrong while each card still READS correctly,')
|
|
console.error(' so the error hides in the aggregate: 130 of 395 cards once carried one, 71 of them')
|
|
console.error(' already done, and the reported open count was 176 against a real 105.')
|
|
process.exit(1)
|
|
}
|
|
|
|
console.log(`BACKLOG-STATUS: PASS — ${total} card(s), every status is a queryable enum.`)
|
|
const order = ['todo', 'doing', 'in-progress', 'blocked', 'done']
|
|
console.log(' ' + order.filter((k) => counts[k]).map((k) => `${k} ${counts[k]}`).join(' · '))
|
|
console.log(' Explanations live in fields.status_detail, which is not required — most cards need')
|
|
console.log(' none, and demanding one would turn this into paperwork satisfied with filler.')
|