doxa-maps/internal/check-backlog-refs.mjs
Doxa Maps e19697673d
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions
Doxa Maps — the map bundler, 7 production plugins and the Experimental shelf
2026-09-25 20:07:59 +00:00

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.')