Refuse to serve a half-built tree, so a gate cannot mistake one for a regression

A build removes public/doxa-maps before rebuilding it, so a browser gate running
during one reads an empty tree and reports a confident wrong answer.
verify-shell-scroll said it could not find the dashboard-plus-grid tab twice in
ten minutes while a swarm rebuilt underneath it. That reads exactly like a real
regression and cost three ticks of this flight.

The server now answers 503 for anything under /doxa-maps/ while the manifest is
absent, with a body naming the cause. A 503 rather than a 404 is deliberate: 404
means "this is not here", which a gate may reasonably report as a product defect,
while 503 with a reason cannot be mistaken for a finding. Assets outside the
bundle tree are unaffected.

The check belongs in the server because all nine gates fetch through it. Guarding
one chokepoint means none of them can misread a torn tree and none of them needs
editing, where having every gate take the build lock is nine changes plus a rule
every future gate has to remember.

The aggregator already refused to start against a missing tree, but only at
startup, and a full sweep runs for seventeen minutes. It also blamed a missing
server for a 503, sending the reader to start one that was already running; it
now names the real cause. The gap the server closes is the one that actually bit:
a single gate run directly, at any moment, including mid-sweep.

Verified by hiding the manifest in place: a leaf returns 503 with the explanation
while /assets/ still returns 404, and both return to 200 when it is restored.
This commit is contained in:
doxa-maps-88655c70 2026-09-23 03:52:30 +00:00
parent 236d289cc7
commit aa196db1d8
2 changed files with 45 additions and 1 deletions

View file

@ -63,10 +63,39 @@ const send = (res, code, body, headers = {}) => {
res.end(body)
}
// ── REFUSE TO SERVE A TORN TREE (card BUILD-BROWSER-GATES-RACE-THE-BUILD-LOCK) ────────────────
// `bun run clean` rm -rf's public/doxa-maps at the START of every build, so a browser gate running
// during one reads an EMPTY tree and reports a confident wrong diagnosis. Measured: verify-shell-
// scroll said "could not find the dashboard-plus-grid tab" twice in ten minutes while a swarm
// rebuilt underneath it — a phantom failure that reads exactly like a real regression, and cost
// three ticks of this flight.
//
// ★ THE CHECK BELONGS IN THE SERVER, NOT IN EACH GATE. All nine browser gates fetch through here.
// Guarding one chokepoint means none of them can misread a torn tree and none of them needs
// editing — where "have every gate acquire the build lock" is nine changes plus a rule every FUTURE
// gate has to remember. Same reasoning as serving directories strictly: fix the thing they all pass
// through, rather than asking nine callers to be careful.
//
// A 503 is deliberate, not a 404: 404 means "this is not here", which a gate may reasonably report
// as a product defect. 503 with a named cause means "ask again later" and cannot be mistaken for a
// finding. Static assets outside the bundle tree (assets/, __gallery/) are unaffected.
const TREE = join(DIR, 'doxa-maps')
const MANIFEST = join(TREE, 'manifest.json')
function treeIsTorn() {
try { return !statSync(MANIFEST).isFile() } catch { return true }
}
createServer((req, res) => {
let pathname
try { pathname = decodeURIComponent(new URL(req.url, 'http://x').pathname) } catch { return send(res, 400, 'bad request\n') }
if (pathname.startsWith('/doxa-maps/') && treeIsTorn()) {
return send(res, 503, 'tree is mid-build — public/doxa-maps/manifest.json is absent.\n' +
'A build rm -rf\'s this tree at its start, so anything measured now is a phantom.\n' +
'Wait for the build to finish and retry; do NOT record this as a product finding.\n',
{ 'retry-after': '30' })
}
// Never let a request escape the served root. normalize() collapses .. before we join.
const rel = normalize(pathname).replace(/^(\.\.[/\\])+/, '').replace(/^[/\\]+/, '')
const file = join(DIR, rel)

View file

@ -100,10 +100,25 @@ if (bundleCount < 2) {
// timeouts and looks like six broken gates.
try {
const r = await fetch(`${BASE}/doxa-maps/manifest.json`, { signal: AbortSignal.timeout(5000) })
// ★ 503 IS A DIFFERENT DIAGNOSIS FROM "no server", and saying so is the whole point of the code
// below. serve-strict.mjs returns 503 while public/doxa-maps/manifest.json is absent, because a
// build rm -rf's that tree at its START — so a gate running then reads an EMPTY tree and reports
// a confident wrong answer. Measured: verify-shell-scroll said "could not find the
// dashboard-plus-grid tab" twice in ten minutes during a swarm rebuild, which reads exactly like
// a real regression and cost three ticks. Without this branch the aggregator blamed a missing
// server, sending the reader to start one that was already running.
if (r.status === 503) {
console.error('BROWSER-ALL: NOT MEASURED — the built tree is mid-build.')
console.error(` ${BASE} answered 503: public/doxa-maps/manifest.json is absent, which is what a`)
console.error(' build looks like from the outside (clean rm -rf\'s the tree before rebuilding it).')
console.error(' Anything measured now is a PHANTOM. Wait for the build and re-run; do NOT record')
console.error(' this as a product finding. Check with: bun run check:settled')
process.exit(2)
}
if (!r.ok) throw new Error(`HTTP ${r.status}`)
} catch (e) {
console.error(`BROWSER-ALL: SETUP — nothing serving ${BASE} (${String(e.message).slice(0, 40)}).`)
console.error(' Start one first: bun run serve # python http.server on 4173, --directory public')
console.error(' Start one first: bun run serve # serve-strict.mjs on 4173, --directory public')
console.error(' Or point elsewhere: BASE_URL=http://host:port bun run verify:browser-all')
process.exit(2)
}