#!/usr/bin/env node /** * verify-browser-all.mjs — run every BROWSER gate over the built tree, in one command. * * WHY THIS EXISTS. library/scripts/research/verify-all.mjs already solved this for the DATA guards, * and its header states the principle better than I can paraphrase it: "Scattered, they are worth * much less than they look: nobody knows the list, so nobody runs them, and a guard nobody runs is * documentation." That file has ZERO references to playwright or public/doxa-maps — it is the data * half. The BROWSER half had no equivalent. * * ★ MEASURED 2026-09-22: all seven browser gates were ORPHANS — each had a package.json script, and * NOT ONE was invoked by any other script. They were reachable only by someone who already knew the * name to type, which is precisely the person who does not need the list. This repo has already * been bitten by exactly this: the Gate-1 acceptance test existed, worked, and was invoked by * nothing, so it "could never report anything" (IDEA-SIX-INTERNAL-SCRIPTS-ARE-UNREACHABLE). An * orphan gate is worse than a missing one, because it LOOKS like coverage on the board. * * ★ EVERY GATE LISTED HERE WAS RUN BEFORE IT WAS LISTED. That rule is inherited deliberately from * the same card: "do not mass-wire ... Adding scripts for programs I have not executed would put * unvalidated entries in the hand-off surface." Results on 2026-09-22, against the built tree: * check:nav-reachable PASS 2 cap-5 shells reach their rail at 414px * check:a11y-button-names PASS 332/332 buttons have an accessible name across 19 leaves * verify:gate1 PASS 13/13 acceptance assertions against 2107 LIVE records * verify:5-caps PASS all 5 capability levels mount, render, error-free * smoke:embeds PASS 43/43 leaf pages mount cleanly * verify:map-paint PASS 11/11 map leaves paint a real canvas under SwiftShader * * ★ I EXCLUDED smoke:share HERE AND THAT WAS WRONG — corrected 2026-09-22, two ticks later. * The original note read: "it takes required arguments (--url / --plugin-gallery) and its gallery * mode reads /__plugin-gallery/registry.json, which only the VITE DEV SERVER serves". Every clause * is true and the CONCLUSION did not follow. I had tested exactly two invocations — bare (exit 2, * usage) and --plugin-gallery (needs vite) — and never tried --url, which the script's own header * documents FIRST. It runs perfectly against the static tree: measured 8/8 pass, each Share button * reopening the correct custom element with a live canvas. * * That is the same shape as AP-a-plausible-disposition-recorded-without-checking-it, found in my * own work: a not-applicable verdict is more durable than a wrong bug report, because it closes the * question. It cost this suite its highest-consequence surface for two ticks — a share URL is the * one artefact a partner pastes into an email, and it fails SILENTLY. * * It now runs as `smoke:share-all`, a script carrying its own 8 --url flags plus --browser chromium * (Firefox headless has no WebGL on this box, so the default engine stops — see * BUG-SHARE-SMOKE-FIREFOX-DEFAULT-FALSE-FAILURES). The 8 leaves are the set derived by grepping * plugin-designs for ShareButton, not a guess. * * Still not covered here: `--plugin-gallery` mode, which genuinely does need the vite dev server. * * PRECONDITIONS (asserted, not assumed): a built tree, and a static server on the BASE port. Both * are checked up front so a missing server reports as a SETUP problem rather than six mystery * failures. * * Exit 0 = every gate passed. Exit 1 = at least one failed (each named, with its output tail). * Exit 2 = preconditions unmet (no tree / no server) — distinguishable from a real failure. */ import { spawnSync } from 'node:child_process' import { existsSync, readFileSync } from 'node:fs' import { resolve, dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const BASE = process.env.BASE_URL || 'http://127.0.0.1:4173' // Each entry was executed on 2026-09-22 with the result recorded above. `why` is what the gate // actually protects, so a failure here tells a reader what broke without opening the file. const GATES = [ { script: 'verify:5-caps', why: 'all 5 capability levels mount and render' }, { script: 'smoke:embeds', why: 'every shipped leaf page mounts its element' }, { script: 'verify:map-paint', why: 'every map leaf paints a real canvas' }, { script: 'verify:gate1', why: 'the Gate-1 chain filters live data end to end' }, { script: 'check:a11y-button-names', why: 'every visible button has an accessible name' }, { script: 'check:nav-reachable', why: 'a phone can open the cap-5 tab rail' }, { script: 'smoke:share-all', why: 'every Share button emits a URL that reopens the right element' }, // ★ The only gate here that measures ONE component in TWO hosts. It exists because a popover can // be perfectly anchored standalone and 256px adrift inside the cap-5 shell, and no single-host // gate above can tell those apart — the defect lives in the host's ancestor chain, not the // component. See internal/check-fixed-anchoring.mjs. { script: 'check:fixed-anchoring', why: 'the filter dropdown stays anchored in BOTH hosts, not just standalone' }, // Same family as the gate above, and found the same week: the cap-5 shell's pane geometry breaks a // plugin that is perfect standalone. Here .vtl-host was inset:0 with overflow:visible, so 2586px of // mounted grid was unreachable and a real wheel gesture moved 0px. Asserts with a WHEEL, not with // scrollHeight — scrollHeight > innerHeight was the FALSE PASS that closed this bug twice. { script: 'check:shell-scroll', why: 'a wheel gesture in the cap-5 shell actually reaches the grid below the dashboard' }, ] // ── PRECONDITION 1: a built tree ──────────────────────────────────────────── const MANIFEST = join(ROOT, 'public', 'doxa-maps', 'manifest.json') if (!existsSync(MANIFEST)) { console.error('BROWSER-ALL: SETUP — no public/doxa-maps/manifest.json. Run `bun run build` first.') process.exit(2) } let bundleCount = 0 try { bundleCount = (JSON.parse(readFileSync(MANIFEST, 'utf8')).bundles || []).length } catch { /* reported below */ } if (bundleCount < 2) { console.error(`BROWSER-ALL: SETUP — manifest lists ${bundleCount} bundle(s); the tree looks empty or truncated.`) process.exit(2) } // ── PRECONDITION 2: a server on BASE ──────────────────────────────────────── // Every gate here fetches over HTTP. Without this check a missing server yields six identical // timeouts and looks like six broken gates. try { const r = await fetch(`${BASE}/doxa-maps/manifest.json`, { signal: AbortSignal.timeout(5000) }) 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(' Or point elsewhere: BASE_URL=http://host:port bun run verify:browser-all') process.exit(2) } // A token is optional: the map gates need one to paint, the chrome gates do not. Fetch once and // share it, rather than letting six gates each pay for the round trip. let token = process.env.MAPBOX_TOKEN || '' if (!token) { try { const r = await fetch('https://doxa.life/api/maps/token', { signal: AbortSignal.timeout(8000) }) if (r.ok) token = (await r.json()).token || '' } catch { /* the chrome gates still run; the map gates will say so themselves */ } } console.log(`BROWSER-ALL: ${GATES.length} gate(s) over ${bundleCount} bundle(s) at ${BASE}` + `${token ? ' (token present)' : ' (NO TOKEN — map gates may report blank)'}\n`) const results = [] for (const g of GATES) { process.stdout.write(` … ${g.script.padEnd(24)} ${g.why}\n`) const t0 = Date.now() const r = spawnSync('bun', ['run', g.script], { cwd: ROOT, encoding: 'utf8', timeout: 900000, env: { ...process.env, MAPBOX_TOKEN: token, BASE_URL: BASE }, }) const secs = Math.round((Date.now() - t0) / 1000) const out = ((r.stdout || '') + (r.stderr || '')).trim() const ok = r.status === 0 results.push({ ...g, ok, secs, tail: out.split('\n').filter(Boolean).slice(-3).join('\n') }) console.log(` ${ok ? '✓' : '✗'} ${g.script.padEnd(24)} ${ok ? 'PASS' : 'FAIL'} (${secs}s)`) } const failed = results.filter(r => !r.ok) console.log(`\nBROWSER-ALL: ${results.length - failed.length}/${results.length} gate(s) passed.`) if (failed.length) { console.error('\nFAILED:') for (const f of failed) { console.error(` ✗ ${f.script} — ${f.why}`) for (const line of f.tail.split('\n')) console.error(` ${line}`) } console.error('\n Re-run one on its own to see its full output: bun run