Browser gates now prove the server is serving THIS checkout

The gospel-resources gate was found adopting whatever answered on the default port,
which on this machine was an unrelated process from a different worktree. It reported
on a build that was not the one on disk: first a PASS, then a mount timeout that read
exactly like a regression in code nobody had touched. "Answers 200" and "is serving
this checkout" are different questions, and only the second one is the gate's job.

That fix was local to one gate. The same default sits under the rest of them, so it is
now one shared helper, internal/_serves-this-tree.mjs, fingerprinting
public/doxa-maps/manifest.json — which generate-tree rewrites on every build and which
names every bundle and profile in the tree. Different bytes means a different build:
another worktree, or a server someone forgot to restart. Both produce the same lie.

Wired into verify-browser-all, which hands ten gates their base URL and would have
passed the wrong tree to all of them, and into check-a11y-button-names. Both now refuse
with a NOT MEASURED exit rather than producing a confident wrong answer. The gospel
gate keeps its behaviour of starting its own server instead.

Verified in both directions: against the foreign 4173 each prints "serves a DIFFERENT
build than public/" and exits 2; against this tree the aggregator proceeds to its ten
gates and the gospel gate passes.

ONE REPORTED NUMBER WAS WRONG BECAUSE OF THIS. The a11y result recorded earlier in this
flight, "810 buttons, 810 with an accessible name", was measured against the foreign
tree. Re-run against this checkout it is 825 buttons, 825 with an accessible name,
still passing across 21 leaf pages. The conclusion held; the evidence had not been
about this build.

Audited the rest of this flight's browser evidence rather than assuming: the 41 probe
scripts in _qa-cycle all target port 5399, which is served by
serve-strict --directory <this worktree>/public, and the manifest served there is
byte-identical to the local one. Those results stand.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 12:54:35 +00:00
parent 77ac7e3436
commit fb7ed4566d
4 changed files with 82 additions and 14 deletions

View file

@ -0,0 +1,48 @@
/**
* _serves-this-tree.mjs — a browser gate must prove the server it talks to is THIS build.
*
* THE FAILURE THIS EXISTS FOR, MEASURED. Browser gates here default to
* http://127.0.0.1:4173 and adopt whatever answers 200 on it. On this machine that port was
* held by an unrelated process belonging to a different worktree, and the gospel-resources
* gate spent three runs reporting on a build that was not the one on disk — first a PASS,
* then a mount timeout that looked exactly like a regression in code that had nothing to do
* with it. A gate that can report on the wrong tree is worse than no gate: it produces
* confident findings about code nobody changed, and hides real ones.
*
* "Answers 200" and "is serving this checkout" are different questions. This asks the second.
*
* THE FINGERPRINT is public/doxa-maps/manifest.json, which generate-tree rewrites on every
* build and which names every bundle and profile in the tree. If the bytes served differ
* from the bytes on disk, it is a different build — a different worktree, or a stale server
* someone forgot to restart, both of which produce the same lie.
*
* Deliberately NOT fatal on its own: the caller decides whether to start its own server
* instead, or to skip. What it must never do is quietly continue.
*/
import { readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { resolve } from 'node:path'
const MANIFEST = 'doxa-maps/manifest.json'
/**
* @param {string} base e.g. http://127.0.0.1:4173 (a trailing /doxa-maps/ is tolerated)
* @param {string} repo absolute path to the repo root
* @returns {Promise<{ok:boolean, why:string}>}
*/
export async function servesThisTree(base, repo) {
let local
try {
local = createHash('sha256').update(readFileSync(resolve(repo, 'public', MANIFEST))).digest('hex')
} catch {
return { ok: false, why: `no local public/${MANIFEST} to compare against — build the tree first` }
}
const root = String(base).replace(/\/+$/, '').replace(/\/doxa-maps$/, '')
const r = await fetch(`${root}/${MANIFEST}`).catch(() => null)
if (!r || !r.ok) return { ok: false, why: `nothing serving ${MANIFEST} at ${root}` }
const served = createHash('sha256').update(Buffer.from(await r.arrayBuffer())).digest('hex')
if (served !== local) {
return { ok: false, why: `${root} serves a DIFFERENT build than public/ — another worktree, or a server that needs restarting` }
}
return { ok: true, why: 'same build' }
}

View file

@ -34,6 +34,7 @@ import { readFileSync, existsSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { chromium } from 'playwright'
import { servesThisTree } from './_serves-this-tree.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json')
@ -45,6 +46,20 @@ if (!existsSync(MANIFEST)) {
process.exit(0)
}
// ★ The server has to be serving THIS checkout, not merely answering. 4173 on this machine
// was held by an unrelated process from another worktree, and a gate that adopts it reports
// a confident result about a build nobody here changed. A pass earned against the wrong tree
// is worse than no pass.
{
const tree = await servesThisTree(BASE, ROOT)
if (!tree.ok) {
console.error('A11Y-BUTTON-NAMES: NOT MEASURED — the server is not serving this checkout.')
console.error(` ${tree.why}`)
console.error(' Point BASE at your own server (BASE=http://127.0.0.1:PORT/doxa-maps/).')
process.exit(2)
}
}
let tk = ''
try {
const r = await fetch('https://doxa.life/api/maps/token', { signal: AbortSignal.timeout(8000) })

View file

@ -53,6 +53,7 @@
* Exit 2 = preconditions unmet (no tree / no server) — distinguishable from a real failure.
*/
import { spawnSync } from 'node:child_process'
import { servesThisTree } from './_serves-this-tree.mjs'
import { existsSync, readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
@ -120,6 +121,20 @@ try {
process.exit(2)
}
if (!r.ok) throw new Error(`HTTP ${r.status}`)
// ★ ANSWERING IS NOT THE SAME QUESTION AS SERVING THIS TREE. This used to stop at "something
// replied", and on this machine 4173 was held by an unrelated process from a DIFFERENT
// worktree. The gospel-resources gate adopted it and spent three runs reporting on a build
// that was not the one on disk — a PASS, then a mount timeout that read exactly like a
// regression in untouched code. Every gate below would inherit the same lie, so the
// aggregator refuses rather than hand six gates the wrong tree.
const tree = await servesThisTree(BASE, ROOT)
if (!tree.ok) {
console.error('BROWSER-ALL: NOT MEASURED — the server is not serving this checkout.')
console.error(` ${tree.why}`)
console.error(' Anything measured against it is about someone else\'s build. Point BASE_URL at')
console.error(' your own server, or stop the foreign one and run: bun run serve')
process.exit(2)
}
} catch (e) {
console.error(`BROWSER-ALL: SETUP — nothing serving ${BASE} (${String(e.message).slice(0, 40)}).`)
console.error(' Start one first: bun run serve # serve-strict.mjs on 4173, --directory public')

View file

@ -42,9 +42,9 @@
* Skips loudly (exit 0) only if the tree is not built, the way CI sees it.
*/
import { chromium } from 'playwright'
import { servesThisTree } from './_serves-this-tree.mjs'
import { spawn } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import { createHash } from 'node:crypto'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
@ -67,22 +67,12 @@ let server = null
// regression in code that had nothing to do with it.
// The fingerprint is the built bundle itself: if the bytes served differ from the bytes on
// disk, it is a different tree and must not be used.
const BUNDLE_PATH = 'doxa-maps/gospel-resources/gospel-resources.js'
const localBundle = (() => {
try { return createHash('sha256').update(readFileSync(resolve(REPO, 'public', BUNDLE_PATH))).digest('hex') } catch { return null }
})()
const servesThisTree = async (b) => {
if (!localBundle) return false
const r = await fetch(`${b}/${BUNDLE_PATH}`).catch(() => null)
if (!r || !r.ok) return false
const served = createHash('sha256').update(Buffer.from(await r.arrayBuffer())).digest('hex')
return served === localBundle
}
const alive = async (b) => {
const r = await fetch(`${b}/doxa-maps/gospel-resources/resources-map/index.html`).catch(() => null)
if (!(r && r.ok)) return false
if (await servesThisTree(b)) return true
console.log(`GOSPEL-MAP: something answers at ${b} but serves a DIFFERENT build than public/ — not adopting it.`)
const tree = await servesThisTree(b, REPO)
if (tree.ok) return true
console.log(`GOSPEL-MAP: not adopting ${b} — ${tree.why}`)
return false
}