Some checks are pending
build-gate / build (push) Waiting to run
The repo-hygiene agent found this in a fresh 1440px screenshot, which is the whole lesson: .pfs already had width:82%, so reading the CSS said it was fine. Flex items default to min-width:auto and refuse to shrink below their content width, so four controls — Prayer List, Card Deck, the stock select, Poster Map — pushed the row open regardless of what the container declared. A constrained box does not constrain flex children, and only geometry shows that. Measured at 1440px on cap4-upg100-mfe: 27 of 27 cards overflowing, worst 110px past a 212px card, 36 intrusions into a neighbouring card's box. After flex-wrap:wrap plus min-width:0 on the children: 0 and 0, with all 108 print controls still a usable size. That second number is asserted deliberately. "Nothing overflows" is trivially satisfied by a row whose buttons collapsed to zero width, which would be a worse product than the overflow it replaced. check:poster-card-fits holds both, and is mutation-tested: reverting the fix reproduces the reporter's exact figures. Also adds check:backlog-refs — a card may not depend on a card that does not exist, because a dangling depends_on can never be done, so the card can never become ready and nothing says so. Its first run disproved the finding that motivated it. I had censused three dangling references, including the PRINCIPLE card the loop prompt lists as open work. The gate came back PASS, and chasing the discrepancy rather than assuming the gate was broken showed my census was wrong: it read only the builder backlog, and all three cards exist in 01-ideation-research. Cross-phase references are legitimate, and a single-backlog census structurally cannot see them. Reporting three dangling refs would have sent someone hunting for cards that were never lost. It fails only for depends_on on an OPEN card; a dangling parent is reported and passes, because failing on filing debt teaches people to delete the reference rather than restore the card. Both severities mutation-tested.
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 _qa-cycle 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, '..', '..', '_qa-cycle')
|
|
if (!existsSync(QA)) {
|
|
console.log('BACKLOG-REFS: SKIP — _qa-cycle 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.')
|