101 lines
5.1 KiB
JavaScript
101 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* check-backlog-refs.mjs — a card may not point at a card that does not exist.
|
|
*
|
|
* THE DEFECT THIS CATCHES. `depends_on` is the board's readiness mechanism: a card is ready when
|
|
* its dependencies are done. A dependency naming a card that is NOT IN THE BACKLOG can never be
|
|
* done, so the card can never become ready — and nothing says so. It reads as ordinary blocked
|
|
* work forever.
|
|
*
|
|
* MEASURED 2026-09-24, three dangling references across the builder backlog:
|
|
* IDEA-SLIM-MAP-SEARCHBAR-HOW-DO-FILTERS-FIT <- BUILD-PEOPLE-GROUP-SEARCH-…depends_on
|
|
* IDEA-REGIONS-DASHBOARD-REVERSE-ENGINEER-DXLIFE <- two regions cards' depends_on
|
|
* PRINCIPLE-MAPS-ARE-SECONDARY-LISTS-AND-GRIDS-ARE-PRIMARY <- two cards' parent
|
|
* The first one matters most: that card is status:todo and waiting on something unfindable.
|
|
* The last is cited by the loop prompt itself as an open card ("PRINCIPLE-MA"), so a reader is
|
|
* being pointed at work that has no record.
|
|
*
|
|
* ★ SEVERITY IS NOT UNIFORM, so this gate grades it. A dangling `depends_on` on an OPEN card is a
|
|
* FAILURE — that card is unreachable. A dangling `parent`, or a dangling anything on a card that is
|
|
* already done, is REPORTED but does not fail: losing a parent is a filing problem, not a blocked
|
|
* one, and failing on it would only teach people to delete the reference rather than restore the
|
|
* card. Naming the difference is the point.
|
|
*
|
|
* ⚠ IT ONLY CHECKS REFERENCES THAT LOOK LIKE CARD IDS. Fields like `parent` also legitimately hold
|
|
* prose in some cards ("the regions epic"), and treating those as broken links would drown the
|
|
* real ones. The shape is SCREAMING-KEBAB, 9+ chars — the convention every id in this board uses.
|
|
*
|
|
* Usage: node internal/check-backlog-refs.mjs
|
|
* SKIPs when internal-tracker is absent (it lives outside every repo, by design).
|
|
*/
|
|
import { readFileSync, existsSync } from 'node:fs'
|
|
import { resolve, dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
const QA = process.env.QA_CYCLE || resolve(HERE, '..', '..', 'internal-tracker')
|
|
if (!existsSync(QA)) {
|
|
console.log('BACKLOG-REFS: SKIP — internal-tracker not present (expected in CI; it is in no repo).')
|
|
console.log(' This gate reads the AGENTIC BACKLOGS, which live outside the repo and are not')
|
|
console.log(' cloned with it. Absence is expected; it is NOT a pass.')
|
|
process.exit(0)
|
|
}
|
|
|
|
const PHASES = ['01-ideation-research', '02-builder-cycle', '03-deployment-cycle']
|
|
const REF_FIELDS = ['parent', 'depends_on', 'blocks', 'supersedes', 'supersedes_mechanism_of']
|
|
// The id convention every card in this board follows. Prose in a `parent` field must NOT match.
|
|
const ID_SHAPE = /^[A-Z][A-Z0-9-]{8,}$/
|
|
|
|
const cards = []
|
|
for (const phase of PHASES) {
|
|
const p = join(QA, phase, 'backlog.json')
|
|
if (!existsSync(p)) continue
|
|
const j = JSON.parse(readFileSync(p, 'utf8'))
|
|
for (const c of j.cards || []) cards.push({ ...c, _phase: phase })
|
|
}
|
|
if (!cards.length) {
|
|
console.log('BACKLOG-REFS: SKIP — no cards found in any phase backlog.')
|
|
process.exit(0)
|
|
}
|
|
|
|
const ids = new Set(cards.map((c) => c.id))
|
|
const OPEN = new Set(['todo', 'doing', 'in-progress', 'blocked'])
|
|
|
|
const blocking = [] // dangling depends_on on an OPEN card — the card can never become ready
|
|
const noted = [] // everything else dangling — filing debt, reported not failed
|
|
|
|
for (const c of cards) {
|
|
const f = c.fields || {}
|
|
for (const key of REF_FIELDS) {
|
|
const raw = f[key] !== undefined ? f[key] : c[key]
|
|
if (!raw) continue
|
|
for (const ref of (Array.isArray(raw) ? raw : [raw])) {
|
|
if (typeof ref !== 'string' || !ID_SHAPE.test(ref)) continue
|
|
if (ids.has(ref)) continue
|
|
const entry = { from: c.id, key, ref, status: c.status || '(none)' }
|
|
if (key === 'depends_on' && OPEN.has(String(c.status || '').toLowerCase())) blocking.push(entry)
|
|
else noted.push(entry)
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`BACKLOG-REFS: ${cards.length} card(s) across ${PHASES.length} backlog(s), `
|
|
+ `${REF_FIELDS.length} reference field(s) checked.`)
|
|
if (noted.length) {
|
|
console.log(` ${noted.length} dangling reference(s) that do NOT block anything:`)
|
|
for (const n of noted) console.log(` ${n.from} [${n.status}] .${n.key} -> ${n.ref}`)
|
|
console.log(' Filing debt: a lost parent or a stale link on a finished card. Restore the card')
|
|
console.log(' or drop the reference — but do not delete it just to quiet this line.')
|
|
}
|
|
|
|
if (blocking.length) {
|
|
console.error(`\nBACKLOG-REFS: FAIL — ${blocking.length} OPEN card(s) depend on a card that does not exist.`)
|
|
for (const b of blocking) {
|
|
console.error(` ✗ ${b.from} [${b.status}] depends_on ${b.ref}`)
|
|
}
|
|
console.error('\n A dependency that is not in the backlog can never be done, so these cards can')
|
|
console.error(' never become ready — and nothing says so. They read as ordinary blocked work.')
|
|
console.error(' Fix: restore the missing card, or remove the dependency if it is obsolete.')
|
|
process.exit(1)
|
|
}
|
|
console.log('BACKLOG-REFS: PASS — every open card\'s dependencies name a card that exists.')
|