/** * _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, existsSync, realpathSync } from 'node:fs' import { createHash } from 'node:crypto' import { resolve, dirname, join } 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' } } /** * Start a short-lived static server over THIS checkout, so a gate whose default port belongs * to another worktree can still MEASURE instead of skipping. * * Skipping was the honest thing to do once the mismatch was visible, but a gate that skips on * every wave is a gate nobody is running: check:provider-switch and check:packet-browser both * went quiet the moment the guard landed, because their default ports (4173, 4898) are held by * other worktrees on this box. Serving our own tree costs a few hundred milliseconds and gives * the gate the one thing it needs — a server it can prove is this build. * * @param {string} dir absolute directory to serve (public/, or the repo root) * @returns {Promise<{origin:string, stop:() => void}|null>} null when no server could be started */ export async function serveThisTree(dir) { const { spawn } = await import('node:child_process') for (let attempt = 0; attempt < 5; attempt++) { // An ephemeral high port, re-rolled on collision — two agents doing this at once must not // fight over one number. const port = 20000 + Math.floor(Math.random() * 20000) const child = spawn('python3', ['-m', 'http.server', String(port), '--bind', '127.0.0.1', '--directory', dir], { stdio: 'ignore', detached: false }) const origin = `http://127.0.0.1:${port}` let up = false for (let i = 0; i < 40 && !up; i++) { await new Promise((r) => setTimeout(r, 100)) up = await fetch(origin + '/', { signal: AbortSignal.timeout(500) }).then((r) => r.ok).catch(() => false) } if (up) { // unref so a caller that forgets to stop() still exits — a gate that hangs on its own // helper is a worse failure than the one it was written to catch. child.unref() return { origin, stop: () => { try { child.kill() } catch { /* already gone */ } } } } try { child.kill() } catch { /* nothing to kill */ } } return null } /** * The same check, as a one-liner a gate can put right after it resolves its base URL. * * ★ EXIT 75, NOT 1. A gate pointed at another worktree's server has NOT MEASURED this build — it * has not found a defect in it. flock uses 75 for the same idea (EX_TEMPFAIL) and the wave runner * already reports 75 as ⊘ rather than ✗, so one code means "not measured" everywhere in this repo. * Exiting 1 here would turn a mis-pointed probe into a red product gate, which is the failure this * whole family of checks exists to prevent. * * @param {string} base origin (or any URL on it) that should serve public/ * @param {string} repo absolute path to the repo root * @param {string} label the gate's own name, for the message */ export async function requireThisTree(base, repo, label) { let origin = String(base || '') // A gate that loads file:// URLs is reading THIS checkout by construction — there is no server to // be wrong about, so the guard stays silent rather than inventing a precondition. if (!/^https?:/i.test(origin)) return { ok: true, why: 'not an http target' } // ★ ONLY LOCAL SERVERS. The defect this guards is several worktrees sharing one machine's ports. // A gate aimed at a DEPLOYED site (verify-map-paint defaults to the live host) is asking a // different question on purpose — "does production match what we built" is a deploy check, not a // mis-pointed probe — and enforcing local identity there would make it permanently unmeasured. // MEASURED while rolling this out: that gate reported NOT MEASURED against the live site, which // is the guard being wrong rather than the gate. try { const host = new URL(origin).hostname if (!/^(localhost|127\.|0\.0\.0\.0$|\[?::1\]?$)/.test(host)) return { ok: true, why: 'remote target, not a worktree port' } } catch { /* unparseable — fall through to the content check */ } try { origin = new URL(origin).origin } catch { /* already an origin, or unusable — pass through */ } const verdict = await servesThisTree(origin, repo) if (/DIFFERENT build/.test(verdict.why)) { console.log(`${label}: NOT MEASURED — ${verdict.why}`) console.log(' Point this gate at a server for THIS checkout (bun run serve) and re-run.') process.exit(75) } return verdict } /** * The same guard for gates that READ A CHECKOUT instead of talking to a server. * * check-tabs-localized and check-poster-prints read library/ source from DOXA_REPO, which exists * so a gate can be pointed at ANOTHER repo on purpose. That is also exactly how a leaked env var * from someone else's shell makes a gate grade their tree while reporting as ours. So pointing * elsewhere is an explicit opt-in (DOXA_REPO_MEASURE_OTHER=1); without it the gate exits 75. * * @param {string} repoPath the checkout the gate is about to read * @param {string} repo this gate's own repo root * @param {string} label the gate's name, for the message */ export function requireThisRepo(repoPath, repo, label) { const real = (p) => { try { return realpathSync(p) } catch { return String(p) } } if (real(repoPath) === real(repo)) return { ok: true, why: 'this checkout' } if (process.env.DOXA_REPO_MEASURE_OTHER === '1') { console.log(`${label}: measuring ANOTHER checkout on purpose (DOXA_REPO_MEASURE_OTHER=1): ${repoPath}`) return { ok: true, why: 'explicit opt-in' } } console.log(`${label}: NOT MEASURED — DOXA_REPO points at ${repoPath}, not this checkout (${repo}).`) console.log(' Unset DOXA_REPO, or set DOXA_REPO_MEASURE_OTHER=1 if measuring that checkout is the intent.') process.exit(75) } /** * For a gate handed a FILE to render: refuse a file that lives inside a DIFFERENT git checkout. * A file outside any checkout (a /tmp or _qa-cycle output) cannot be attributed and is allowed, * with a note; one inside another worktree is that worktree's build, not ours. */ export function requirePathInThisCheckout(file, repo, label) { const real = (p) => { try { return realpathSync(p) } catch { return String(p) } } let dir = dirname(real(file)) while (dir && dir !== dirname(dir)) { if (existsSync(join(dir, '.git'))) { if (real(dir) === real(repo)) return { ok: true, why: 'inside this checkout' } console.log(`${label}: NOT MEASURED — ${file} belongs to another checkout (${dir}), not this one (${repo}).`) process.exit(75) } dir = dirname(dir) } console.log(`${label}: note — ${file} is outside any checkout, so which build produced it cannot be checked.`) return { ok: true, why: 'outside any checkout' } }