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.
The output tree's name has always been configurable (vite.doxa-maps-build-config.json "name"),
but 61 tooling files ignored it and hardcoded 'doxa-maps' (181 literals), so a rename would have
moved the build while the gates kept reading the old folder.
- internal/_tree-name.mjs reads it once (validated as one URL-safe path segment; falls back to the
historical name) and exports TREE_NAME / TREE_NAME_RE; `node internal/_tree-name.mjs` prints it
for shell scripts.
- A codemod rewrote 171 literals in internal/ to TREE_NAME; the regex literals, messages with
backticks, atomic-public-swap.sh (fails loudly if the name cannot be read), build-lock.sh and
vite.config.js (its duplicate readMfeName is gone) were done by hand.
- The name is unchanged: old and new tooling produce all 1408 files under public/ byte for byte.
Code lines still naming the tree in internal/ + vite.config.js: 2, both intended (the helper's
fallback, the private-repo-name pattern in public-hygiene-check).
This renames nothing visible; the eventual rename becomes a one-line config change.
smoke:embeds reports a node count per profile, and the counts vary enormously:
dashboard-plus-grid mounts 1,499 nodes with a canvas, big-picture 262,
research-map-plus-grid twelve. Read side by side that last one looks like a
capability-4 composite whose two children never rendered.
It is not. Measured with a recursive shadow-root walk at mobile viewport: 518
nodes, 1 canvas, both children present as real custom elements
(doxa-research-map and people-groups-grid), 13,867px of content, zero console
errors and zero responses >= 400.
The gate counts ONE shadow level. research-map-plus-grid nests its children by
TAG and bundle URL rather than importing their .vue files — deliberately, and
its meta.json explains why: importing the research map's component directly
would bypass doxa-research-map/index.js and silently drop that bundle's private
colour strategies, its profileModules registry, its Pinia/i18n pair and the RTL
plugin. Children mounted that way live in their own shadow roots, which a
single-level querySelectorAll cannot see.
So the count is a MOUNT signal, not a quality signal, and the `< 5` bar it feeds
is still exactly right — that bar catches a mount which produced nothing, which
is what this gate exists to assert. Only the reading was wrong, so only a
comment changes here.
Worth recording because this repo already carries the lesson that shadow DOM
does not pierce, and it cost three wrong diagnoses of a non-bug once before. It
reappeared inside a gate rather than in an ad-hoc probe, where it is harder to
notice: the gate is green, the number is simply not measuring what a reader
assumes. A near-miss, caught by checking the outlier in a browser instead of
filing it.
Whole run for the record: 43/43 pages mount cleanly, enumerated generically from
manifest.json — every bundle x every profile, not a hardcoded list. That is the
"Playwright coverage over every plugin" item on the standing work list, and it
is real coverage.
Without a token every map profile failed the mount assertion and the gate
reported 13 'mount timeout' failures that read as broken plugins. They were
not broken: the same profiles mount immediately once a token is supplied
(doxa-simple-map went 0/4 -> 4/4 with canvas; the full suite 27/40 -> 40/40).
Both channels are set because they are not interchangeable: ?tk= reaches only
the top-level element's profile-config, while nested children read
window.MAP_TOKEN. A composite mounts with ?tk= alone but its children stay
blank.
No token is not a failure - the run prints an explicit warning and degrades to
DOM-only coverage rather than reporting map profiles as broken. Negative-tested
in both directions.
A profile/instance whose config says provider:'maplibre' now generates a page
that loads the MapLibre peer (jsdelivr, same versions as useMapInstance) and
mounts IMMEDIATELY — the token ladder is skipped entirely. Mapbox pages are
unchanged. demo-map (the example vibe coders copy) is now provider-aware via
resolveMapProvider, and ships a 'maplibre' instance as the living token-free
demo. embed-smoke covers instance pages; verified 2/2 with live canvases —
including the maplibre one with NO token anywhere.
New permanent harness (bun run smoke:embeds): loads every profile page from the
partner-facing manifest headless (SwiftShader software WebGL — no GPU needed),
asserts the custom element defines + populates its shadow DOM, no ProfileLoader
error card, and a WebGL canvas on map profiles. First full run: 17/17 pass —
8 map profiles paint a canvas, 9 DOM profiles (lists/tables/charts/walls)
populate. Exit 1 on any failure, so CI can gate on it.