diff --git a/internal/settled-tree-gate.mjs b/internal/settled-tree-gate.mjs index b8248ed61..73a44e2c5 100644 --- a/internal/settled-tree-gate.mjs +++ b/internal/settled-tree-gate.mjs @@ -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)