Only the browser seat runs browsers, and only inside internal/browser-slot.sh.
That rule was remembered, not enforced: an agent ran a gate it took for a
headless source check and it launched Chromium outside the slot.
internal/lib/playwright-guarded.mjs is now the one way a repo file reaches
playwright: its launch(), launchPersistentContext(), launchServer(), connect()
and connectOverCDP() exit 75 (NOT MEASURED) with a message naming
browser-slot.sh unless DOXA_BROWSER_SLOT is set, and browser-slot.sh exports it
for the command it runs, so a bash -c grandchild inherits it. All 72 files that
imported playwright switch to it (internal/lib/browser.mjs and 64 internal gates;
plugin-designs scripts: doxa-research-map 3, my-upg-100-list 2,
research-map-plus-grid 2, research-dashboard 1): a mechanical import swap.
check:browser-gates-guarded (gates:pre) fails a direct import;
bun run browser-gates lists the 97 scripts that launch a browser. isBrowserGate
moves to internal/lib/browser-gates.mjs so the runner and the check share it.
verify-map-paint.mjs:293 asserts shadow && glContext && attribution &&
!asksForToken && cssOk && no-real-errors. Every one of those is TRUE of a
flawless basemap carrying none of our zones, polygons or pins, so a data-layer
regression lands green. Measured by starve-probe (abort our
/assets/{boundaries,tables,people-groups}/ and people-groups API requests,
leave the basemap untouched, then read exactly what verify-map-paint reads):
NINE of its thirteen leaves still report a live canvas, live GL, attribution
and ZERO page errors with none of our data on them. The hole is the majority of
the suite. The other four lose their canvas entirely, so the existing gate
already fails them and they are deliberately not listed here -- a leaf list is
a claim about where a failure would otherwise go unseen, not a trophy cabinet.
TWO MEASURES, PASS ON EITHER, because each alone produces a false red on a real
leaf. dashboard-plus-grid's map is a 686x340 pane whose basemap already
contributes ~846 distinct colours, so removing a choropleth deletes AREA
(26.4% of pixels) while barely moving the palette (ratio 0.788 -- I nearly
reported that as a defect). demo-map is the mirror: palette collapses to 0.196
while only 5.0% of pixels change. A map with NO data moves NEITHER.
A global floor cannot work here at all: check:map-paints requires 40 distinct
colours and a starved upg-100-list scores 306.
THE INSTRUMENT MATTERS MORE THAN THE THRESHOLD, three findings:
- drawImage/readPixels straight off a GL canvas returns an EMPTY buffer
(preserveDrawingBuffer:false). Three leaves read colours:1, nonzero:0 while
visibly full of colour. page.screenshot({clip}) goes through the compositor.
- elementHandle.screenshot() waits for the element to be STABLE, which an
animating dashboard canvas never becomes; it timed out on three leaves.
- A FIXED WAIT MAKES THE GATE FLAKY AND THE FLAKINESS READS AS A DEFECT. With
waitForTimeout(10000): countries-map measured 1868 distinct colours then 54,
and research-dashboard PASSED at 19.1% then timed out, on identical code
minutes apart. The fix is a readiness predicate, not a longer clock --
isStyleLoaded() && areTilesLoaded(), reachable on ANY leaf without a
per-profile handle via host()._instance.provides.mapStore. countries-map
then reported 1862->194 / 46.6% twice, settling in 12.7s and 10.5s: the old
10s wait was 2.7s short, which a clock cannot tell you and the map can.
IT REFUSES TO JUDGE RATHER THAN GUESS, exit 75 (never 0): no canvas, a starve
that aborted 0 requests, images that cannot be compared, a readiness that could
not be measured, or the gate itself throwing. That last one is not theoretical
-- the first run died on a Playwright arity error and exited 1, which a wave
runner reads as "these maps have no data on them". Three leaves are EXCLUDED
with their measured reasons rather than left to emit a permanent no-measurement
that reads as coverage.
--poison starves BOTH runs so neither measure can move and every leaf must go
red; it is mandatory because "either measure moved" is a weaker claim than
either half, and a widened assertion that still passes everything reads as
coverage while proving nothing.