Settled-tree gate: assert leaf-page count, which per-bundle builds silently delete

This commit is contained in:
doxa-maps-88655c70 2026-09-20 06:05:52 +00:00
parent 37b9a550f7
commit edb935f46a

View file

@ -44,6 +44,7 @@ import { execSync } from 'node:child_process'
const TREE = process.env.TREE || 'public/doxa-maps'
const QUIET_SECONDS = Number(process.env.QUIET_SECONDS || 45)
const MIN_DIRS = Number(process.env.MIN_DIRS || 17)
const MIN_LEAVES = Number(process.env.MIN_LEAVES || 50)
if (!existsSync(TREE)) {
console.error('SETTLED-TREE: FAIL — tree does not exist: ' + TREE + ' (a build may be mid-clean)')
@ -80,13 +81,33 @@ let newest = 0
// ── signal 3: shape ──────────────────────────────────────────────────────────
const dirs = readdirSync(TREE, { withFileTypes: true }).filter((e) => e.isDirectory()).length
// ── signal 4: LEAF COUNT ─────────────────────────────────────────────────────
// ★ ADDED because a per-bundle `BUNDLE=x bun x vite build` does something worse than leave a stale
// SRI hash: it EMPTIES public/doxa-maps/ of every OTHER bundle's leaf index.html. An agent ran it
// five times as a compile check and silently deleted 62 leaf pages across 18 bundles — including
// the root index.html and manifest.json — in bundles it had never touched.
// Nothing caught it. check:sri only compares a leaf's baked hash to its bundle, so a leaf that no
// longer EXISTS is simply not checked — the gate passes because there is nothing left to disagree.
// dirs alone does not catch it either: the directories survive, only their index.html vanishes.
// (Full builds regenerate the leaves via `bun run tree`, which is why the damage kept healing
// itself and stayed invisible.)
const countLeaves = (dir) => {
let n = 0
for (const e of readdirSync(dir, { withFileTypes: true })) {
if (e.isDirectory()) n += countLeaves(join(dir, e.name))
else if (e.name === 'index.html') n++
}
return n
}
const leaves = countLeaves(TREE)
const quietFor = newest ? Math.round((Date.now() - newest) / 1000) : Infinity
const ok = builders === 0 && waiters === 0 && churn === 0 && dirs >= MIN_DIRS
const ok = builders === 0 && waiters === 0 && churn === 0 && dirs >= MIN_DIRS && leaves >= MIN_LEAVES
console.log(
'SETTLED-TREE: builders=' + builders + ' waiters=' + waiters +
' churn(' + QUIET_SECONDS + 's)=' + churn +
' dirs=' + dirs + '/' + MIN_DIRS +
' dirs=' + dirs + '/' + MIN_DIRS + ' leaves=' + leaves + '/' + MIN_LEAVES +
' quietFor=' + (quietFor === Infinity ? 'n/a' : quietFor + 's')
)
@ -99,4 +120,5 @@ if (builders) console.error(' a build is HOLDING the lock — wait for it')
if (waiters) console.error(' ' + waiters + ' build(s) QUEUED — each will re-run clean and rebuild everything; wait for the QUEUE to drain, not just the current build')
if (churn) console.error(' ' + churn + ' file(s) written in the last ' + QUIET_SECONDS + 's — something is still writing')
if (dirs < MIN_DIRS) console.error(' only ' + dirs + ' bundle dirs (expected >=' + MIN_DIRS + ') — likely mid-clean or a half-built tree')
if (leaves < MIN_LEAVES) console.error(' only ' + leaves + ' leaf index.html (expected >=' + MIN_LEAVES + ') — a per-bundle `BUNDLE=x vite build` empties OTHER bundles\' leaves; run a FULL build to regenerate them')
process.exit(1)