Document why a gate cannot import plugin source, and stop teaching public binds
Some checks are pending
build-gate / build (push) Waiting to run

Three docs still told a reader to start a server on every interface: the
reference recipe for the share round-trip, the quoted scripts block, and
app-wrappers.md. Agents followed those recipes faithfully, which is how twenty
publicly bound http.server processes accumulated on this box. All three now
bind 127.0.0.1. check:quoted-scripts caught the second one on its own.

Also adds the gate-authoring constraint that just broke a build. Plugin and
library source is written for Vite, not Node, and fails two ways: the @map
alias is unresolvable (hit first), and import.meta.glob is undefined in 18
files including 11 of the 20 plugin index.js. A new check:facet-cascade
imported the dashboard config, reached library/colors/_registry.js
transitively, and threw -- while already wired into build:unlocked, so the
build failed for a reason unrelated to the code being built.

The counts in that section were measured, not estimated. A first draft said
"every plugin index.js" and the real number is 11 of 20; it also blamed the
glob when the first error is actually the alias.

States the two rules that follow: build:unlocked is only for gates that run
clean in plain Node with no browser and no network, and a gate gets run before
it gets wired in -- a broken gate in the build chain is worse than no gate,
because it goes red for the wrong reason and trains people to skip the step.

Records the other box constraints that have each produced a wrong answer:
Node 20 (no fs.glob), bfs find rejecting relative -newermt, no hardware WebGL,
and bunx not existing.
This commit is contained in:
doxa-maps-88655c70 2026-09-22 17:40:11 +00:00
parent 0296b03aa7
commit 64731f2f3e
3 changed files with 52 additions and 4 deletions

View file

@ -25,9 +25,9 @@ The scripts these commands are grounded in (from this branch's `package.json`):
// claiming to be ground truth, and this one had silently stopped being it.
"dev": "bun x vite",
"build": "bash internal/build-lock.sh",
"build:unlocked": "bun run check:template-literal && bun run check:sfc-parse && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:descriptions && bun run check:no-abs-paths && bun run check:dep-names && bun run check:composite-srcs && bun run check:embed-targets && bun run check:nested-token && bun run gallery",
"build:unlocked": "bun run check:template-literal && bun run check:sfc-parse && bun run clean && bun run token && for d in plugin-designs/*/; do BUNDLE=$(basename \"$d\") bun x vite build || exit 1; done && bun run tree && bun run check:drop && bun run check:parse && bun run verify && bun run check:sri && bun run check:public-hygiene && bun run check:label-joins && bun run check:facet-cascade && bun run check:descriptions && bun run check:no-abs-paths && bun run check:dep-names && bun run check:composite-srcs && bun run check:embed-targets && bun run check:nested-token && bun run gallery",
"preview": "bun x vite preview",
"serve": "python3 -m http.server 4173 --bind 0.0.0.0 --directory public",
"serve": "python3 -m http.server 4173 --bind 127.0.0.1 --directory public",
"cap:sync": "./node_modules/.bin/cap sync android",
"cap:verify": "test -f deployment/capacitor/android/app/src/main/assets/public/index.html && echo \"OK webDir copied\" || (echo \"FAIL: empty assets/public -> blank white screen. Check webDir.\" && exit 1)",
"apk:debug": "bun run build && bun run cap:sync && bun run cap:verify && cd deployment/capacitor/android && ./gradlew assembleDebug",

View file

@ -58,7 +58,7 @@ local assets. Paths follow the Lucide idiom (24×24, 2px stroke, round caps).
```bash
cd library/layouts/app-wrapper-material
python3 -m http.server 4173 --bind 0.0.0.0
python3 -m http.server 4173 --bind 127.0.0.1
```
Then open it and narrow the window below 1024px (or use the browser device toolbar) to check the

View file

@ -249,13 +249,61 @@ Measured 2026-09-22, by running each one:
| `smoke:gallery` (and `smoke:playground`, its alias) | a dev server: `bun x vite` on **:5174** | exits non-zero with *"cannot read http://localhost:5174/__plugin-gallery/registry.json — is `bun x vite` running?"* — a **precondition**, not a defect |
| `smoke:share` | `--url <page>` arguments, a served tree, a token, and **`--browser chromium` on this box** | prints `usage:` and exits non-zero; with default Firefox it stops with "Verified on a BLANK page, so it is the engine/box, not the product" and names the chromium flag itself |
#### ★ WRITING A NEW GATE: you cannot `import()` plugin or library source into plain Node
This is the trap that will bite the next gate author, so it is written here rather than left to be
rediscovered. Plugin and library source is written for **Vite**, not for Node, and it fails two
different ways — counted 2026-09-22, not estimated:
| Vite-only feature | Where | What plain `node` does |
|---|---|---|
| the **`@map` alias** (`@map/ProfileLoader.vue`) | resolved by `vite.config`, not by Node | `Cannot find package '@map/ProfileLoader.vue'` — this is what you hit **first**, before any glob |
| **`import.meta.glob`** | **18 files**: 11 of the 20 plugin `index.js`, plus `library/ProfileLoader.vue`, `library/i18n/index.js`, `library/composables/useShadowStyles.js`, `library/colors/_registry.js`, and 3 more | `TypeError: (intermediate value).glob is not a function` |
`import.meta.glob` is a **build-time** feature: Vite statically replaces it with real imports while
bundling. Plain `node` leaves it undefined, so the module throws the instant it is evaluated:
```
TypeError: (intermediate value).glob is not a function
at library/colors/_registry.js:26
```
The glob failure is the nastier of the two because it can arrive **transitively**, from a module you
never named — you import a config, the config imports a colour strategy, and the registry throws.
Measured 2026-09-22: a new `check:facet-cascade` gate did `await import()` on the dashboard config,
which transitively reached the colour registry, and crashed — **while already wired into
`build:unlocked`**, so `bun run build` failed for a reason that had nothing to do with the code being
built. The library file was correct and pre-existing; the defect was entirely in the gate reaching it.
So when a gate needs to exercise real plugin logic, pick one:
| Approach | When | Cost |
|---|---|---|
| Import only the **leaf module** under test | the logic is in a plain `.js` with no Vite features (most composables are) | cheapest and most reliable — keep the dependency cone small |
| Test against the **BUILT bundle** over `bun run serve` | the logic only exists after bundling, or needs the DOM | needs a browser + a served tree, so it belongs in `verify:browser-all`, **not** `build:unlocked` |
| Parse the **source text** instead of executing it | you are asserting a structural property (a rule exists, a field is declared) | cannot catch runtime behaviour — say so in the gate's header |
**Two hard rules that follow.** A gate goes in `build:unlocked` only if it runs clean in plain Node
with no browser and no network — everything else is a standalone or browser gate, the way
`check:a11y-button-names` and `check:composite` are handled. And **run the gate before wiring it in**:
a broken gate inside the build chain is worse than no gate, because it goes red for the wrong reason
and trains whoever hits it to skip the step.
**Other runtime constraints on the build box**, all of which have produced a wrong answer at least
once: Node is **v20** (no `fs.glob`, which is 22+); `find` is **bfs**, which *rejects* a relative
`-newermt '-45 seconds'` and errors to stderr while printing nothing — use `bun run check:settled`,
or an absolute `$(date -u -d '-45 seconds' +%Y-%m-%dT%H:%M:%SZ)`; there is **no hardware WebGL** (judge
maps with `verify:map-paint`'s SwiftShader path, never by expecting a painted basemap); and `bunx`
does not exist here — use the spaced `bun x`.
**Copy-paste: the full share round-trip over every plugin that has a Share button.** Assembled and run
2026-09-22 — **8/8 pass**. Without this recipe an inheritor has to work out the server, the token and the
eight `--url` flags from scratch, which is why the gate had never been run across all of them.
```sh
cd doxa-maps-plugin-studio
( cd public && python3 -m http.server 4950 & ) # serve the built tree
( cd public && python3 -m http.server 4950 --bind 127.0.0.1 & ) # serve the built tree, LOOPBACK ONLY
export MAPBOX_TOKEN=$(curl -s https://doxa.life/api/maps/token \
| python3 -c 'import json,sys;print(json.load(sys.stdin)["token"])')