Censused the 67 gate scripts that are in no wave. The framing I had been using — "coverage we are not getting" — was wrong in the more serious direction. Two of them were FAILING on main right now. The gates exist, work, and had found something; because nothing ran them the findings sat silently. check:legend-source, a real product finding: the live corpus moved 2108 -> 2107 and one group crossed Unengaged -> Engaged, so the legend baseline was describing a dataset that no longer exists. Confirmed 2107 independently against the live endpoint rather than trusting the gate's own reading. Updated the baseline following the procedure its header requires, repeated rather than assumed: grepping the six figures across plugin-designs/ and library/ finds them only in explanatory comments, so no shipping code hardcodes a corpus total and only the baseline lagged. Mutation-tested — an off-by-8 baseline fails. Recorded that this is the third reading, that it drifts in both directions, and that it must NOT be widened into a range: the noise is the signal that the numbers on the page are live rather than asserted. check:verification-tags asks whether a human ever looked at a done card. 98 could not answer. 24 of those were mine, and they are now tagged agent/state — honestly agent rather than human, and state rather than visual, because every one was verified by measuring rendered DOM, PDF bytes and request tallies, never by a human looking at pixels. The other 74 belong to other agents and the Driver; guessing their provenance would defeat the gate, so it stays unwired. Six further gates were green and self-contained and are now wired: locale reachability, url resolution, manifest coverage, the palette registry, and the legend/source comparison. gates:post 37 -> 42; wave membership 43 -> 50 of 106. One I nearly wired wrong. check:dxlife passes standalone but exits 2 by documented contract — 0 unchanged, 1 drift, 2 clone not found — and the wave runner grades any non-zero as failure, so a checkout without the sibling clone would go red on a precondition. Caught in the fresh-checkout run, unwired, with the reason recorded on the script so the next census does not re-add it. "Can it run" and "is it wave-safe" are different questions. 42/42 with servers up; 31/42 passed and 7 SKIPPED on a fresh checkout, every skip named.
18 KiB
3. CLI commands + required installs (per deployment method)
Everything runs through bun and the scripts in package.json. The build loop
iterates every folder under plugin-designs/ (each folder = one bundle), so once a
map exists there, bun run build picks it up automatically — no per-map wiring.
Install once (all methods): Bun (
curl -fsSL https://bun.sh/install | bash) — Node ≥ 18 also works (npm run <script>; the scripts are identical). Then:bun install
The scripts these commands are grounded in (from this branch's package.json):
// TRANSCRIBED FROM package.json 2026-09-20. The previous copy had DRIFTED: it listed
// `check:drop` twice, omitted `check:template-literal` and `check:sri`, and still showed
// the old inline `build` pipeline — which has since moved behind a LOCK WRAPPER.
// ★ `build` is now `bash internal/build-lock.sh`; the real pipeline is `build:unlocked`.
// The lock SERIALIZES concurrent builds (a `clean` rm -rf's the shared output tree at the
// START of every build, so two builds racing give opposite answers seconds apart). Use
// BUILD_LOCK_WAIT=600 to QUEUE rather than collide. Note the lock serializes but does NOT
// dedupe: N queued builds = N full rebuilds.
// ★ THE PIPELINE RUNS IN PHASES, NOT AS ONE `&&` CHAIN (2026-09-23). Measured on the build
// box AS IT WAS THAT DAY — the numbers below are a DATED BENCHMARK, not a running total.
// The gate set has grown since (36 steps as of 2026-09-24: 7 pre + 29 post); the timings
// and the "31 in, 31 out" count are kept verbatim because they are the EVIDENCE that the
// phase change preserved the step set. Restating them as current would destroy that.
// Then: the 29 gates cost 84.5s chained, but 21 of them finish under 700ms — they were
// queueing behind check:mjs-parse (26.9s) and check:parse (18.7s), not working. The 22 vite
// builds cost 129.0s and were never ordered at all: vite.config.js gives each bundle its own
// outDir with emptyOutDir:false, so they only ever needed to not overwrite each other.
// Now: gates:pre ‖ → library-index → clean → token → build:bundles ‖ → tree → gates:post ‖.
// MEASURED end-to-end (both green, clean+token excluded as they are common to both):
// 165.7s → 65.3s mean of two rounds each. Concurrency: GATES_CONCURRENCY (default 6),
// BUILD_CONCURRENCY (default 4 — 6 buys only 3-9%, and ambient load on this shared box moves
// the same setting 40s→55s, so being a good neighbour costs nothing measurable).
// ★ NO GATE WAS REMOVED, WEAKENED, SAMPLED, OR MADE CONDITIONAL ON WHAT CHANGED. The step set
// is byte-identical (31 in, 31 out, asserted at edit time); only the start times moved. The
// `&&` BETWEEN phases is load-bearing and stays: a source gate still blocks the build, and
// the build still blocks every gate that reads its output. Within a phase the runner reports
// ALL failures instead of stopping at the first, so three broken gates cost one cycle, not three.
// ⚠ check:quoted-scripts IS ITSELF IN gates:post, so adding or removing ANY gate changes the
// very line this doc quotes — the update is self-referential and takes two passes: wire the
// gate, then re-transcribe. That is the mechanism working, not a bug.
// If you edit package.json, re-transcribe this block — a doc that QUOTES a config is
// 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 gates:pre && bun run library-index && bun run check:library-index && bun run clean && bun run token && bun run build:bundles && bun run tree && bun run gates:post && bun run gallery",
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:wp-embed check:doc-scripts",
"gates:post": "node internal/run-parallel.mjs --label=post -- check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:legend-source",
"build:bundles": "node internal/run-parallel.mjs --bundles", // the 22 vite builds, 4 at a time
"preview": "bun x vite preview",
"serve": "node internal/serve-strict.mjs", // 404s a directory, like production
"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",
"apk:install": "adb install -r deployment/capacitor/android/app/build/outputs/apk/debug/app-debug.apk",
"phone": "bun run apk:debug && bun run apk:install"
bun run build writes the whole deployable tree to public/doxa-maps/, including
manifest.json — the machine-readable index that carries the ready-to-paste
iframe and <script src> + SRI embed snippets for every map.
Summary table
| # | Deploy method | Install first (prereqs) | Command(s) |
|---|---|---|---|
| 1 | iframe embed | Bun | bun install → bun run build → bun run serve (or drop public/doxa-maps/ on any host) |
| 2 | Mobile app (Android) | Bun ALONE for check:mobile; + Android SDK · adb on PATH · JDK 17 (Gradle) for the APK |
bun run check:mobile (sync + webDir assertion, NO toolchain needed) · bun run phone (= apk:debug + apk:install to a device/emulator) |
| 3 | Direct-JS / dashboard | Bun | bun run build, then paste the <script src> + integrity (SRI) snippet from manifest.json. Proven same-root AND foreign-host (verified 2026-09-20: gl + attribution, 0 errors, 0 404s). Fill the YOUR_* placeholders the snippet ships with |
| 4 | WordPress | Bun (+ php CLI to run the contract gate) |
bun run build, then either paste the iframe/script snippet into a Custom HTML block, or install deployment/wordpress/doxa-maps-embed/ (shortcode + Gutenberg block). bun run check:wp-embed verifies the emitted markup. (Contract-proven; not yet shot on a live WP install.) |
| 5 | Desktop (Tauri) | Bun ALONE for check:tauri; + Rust toolchain · Tauri CLI · system webview libs for the binary |
bun run check:tauri (config contract, NO toolchain needed) · cargo tauri build (needs the toolchain). (Built, not shot — a 65 MB binary + 58 MB .deb WERE emitted; only the GUI launch is unverified.) |
1. iframe embed (required)
Install: Bun.
bun install
bun run build # builds every plugin-designs/* bundle -> public/doxa-maps/
bun run serve # optional: serves public/ at http://localhost:4173
The build writes public/doxa-maps/manifest.json. Each profile carries an
embed.iframe snippet, e.g.:
<iframe src="./all-people-groups/all-people-groups/index.html"
width="100%" height="600"
style="border:none;border-radius:12px;" allowfullscreen></iframe>
URLs are root-relative, so the whole doxa-maps/ folder is position-independent —
drop it on Cloudflare/Vercel/any static host and every iframe resolves. (For an
external absolute base, set embedBaseUrl in the build config before building.)
2. Mobile app — Android / Capacitor (required)
Install: Bun · Android SDK (sdkmanager, adb on your PATH) · JDK 17
(the Gradle wrapper needs it) · Capacitor (@capacitor/* are already devDependencies,
so bun install covers them). A connected device or running emulator for install.
bun run check:mobile # CONTRACT check — needs NO JDK/SDK/device
bun run phone # full path — needs the toolchain above
check:mobile is runnable anywhere. cap sync is a file copy plus plugin wiring, so it
needs no Android toolchain at all; cap:verify then asserts
deployment/capacitor/android/app/src/main/assets/public/index.html exists. That guards the soft
failure capacitor.config.json warns about — a wrong webDir yields an EMPTY
assets/public and a BLANK WHITE SCREEN at launch, not an error. Verified 2026-09-20 with
java, gradle, adb and sdkmanager all absent: sync EXIT=0, the whole public/ tree
staged into the native assets, cap:verify OK. Negative-tested by removing index.html — EXIT=1.
Why this says "the whole tree" and not a bundle count. It used to read "21 bundles staged". That number matched nothing measurable: the tree today has 18 built bundles, 17 of them listed in
manifest.json(key-citiesis withheld bygatedProfiles), and 19 subdirectories underpublic/doxa-maps/— one of which isassets/, not a bundle at all.cap synccopieswebDirwholesale, so bundles was never the unit it operates on. A count that names the wrong noun goes stale the moment the tree changes and cannot be re-checked, which is exactly how the original figure survived unnoticed.
Status: built + contract-proven; not shot. The APK itself (gradlew assembleDebug) and
its launch on a device still need JDK 17 + the Android SDK + adb.
phone chains the whole path: apk:debug (which runs bun run build, then
cap:sync to copy the web output into the Android project, then cap:verify, then
./gradlew assembleDebug) and finally apk:install (adb install -r ...) onto the
device. Run the halves separately if you want to inspect between steps:
bun run apk:debug # build the APK only
bun run apk:install # push the built APK to a connected device
Only Android is scaffolded on this branch (deployment/capacitor/android, capacitor.config.json);
iOS is not set up.
3. Direct-JS / dashboard (required)
Install: Bun.
bun run build
Then paste the profile's embed.script snippet from manifest.json — a <script src>
with a Subresource-Integrity (SRI) hash plus the custom-element tag:
<script src="./all-people-groups/all-people-groups.js"
integrity="sha384-j5Ypx+EI9DGlTClzgLs1R8wAnmTnkQ7HgAhHrxyGoVoHDMTlFVY7slU6JNicn77T"
crossorigin="anonymous"></script>
<all-people-groups
profile-config='{"profile":"all-people-groups","tk":"YOUR_MAPBOX_TOKEN"}'>
</all-people-groups>
The element accepts props / emits events, so it can wire into a surrounding page as a unified research dashboard (deploy-3 proof). The SRI hash regenerates on every build.
Embedding on a FOREIGN host (a partner site) — the two things that differ
Both embed methods were run to their first failure on a foreign host page (2026-09-20). deploy-1 needed nothing; deploy-3 needed two values, and the snippet now ships both.
Why they differ: an iframe carries its own document base, so the leaf page's relative asset and API paths resolve correctly. A direct-JS embed executes in the host's document, so those same relative paths resolve against the host — and 404. Same bundle, same data, different containing document.
| iframe (deploy-1) | direct-JS (deploy-3) | |
|---|---|---|
assetsBase |
not needed | required — in profile-config |
| API base | not needed | required — window.MAP_APP_API_URL (a host global, not a profile-config key) |
| Mapbox token | inherited by the leaf | replace YOUR_MAPBOX_TOKEN |
Both are emitted into embed.script already: the API base uses the configured value, and
assetsBase falls back to a YOUR_TREE_URL/assets placeholder when assetsBaseUrl is unset.
One rewrite either way. Snippet URLs are tree-relative (./bundle/...) because
embedBaseUrl is empty by default — that is what keeps a dropped folder position-independent
and the check:drop gate green. A partner whose page is not at the tree root must either
prefix the path (./doxa-maps/...) or set embedBaseUrl in the build config to emit
absolute URLs. That is a deployment choice, not a defect.
4. WordPress (built + contract-proven; not yet shot on a live WP install)
Corrected 2026-09-20. This section previously read "deferred POC — no dedicated WordPress build step exists on this branch". That is no longer true: a first-class plugin exists at
deployment/wordpress/doxa-maps-embed/(a[doxa_map]shortcode + a Gutenberg block), and its emitted markup is now verified by a gate.
The simple path — no plugin needed, same artifacts as methods 1 and 3:
bun run build
Then paste either the iframe snippet or the <script src> + SRI snippet from
manifest.json into a WordPress Custom HTML block.
The plugin path — deployment/wordpress/doxa-maps-embed/ registers the bundle through WordPress's
own dependency graph, so the SRI hash and crossorigin survive and the script loads once
per page rather than once per embed.
bun run check:wp-embed # renders the REAL shortcode with no WordPress installed
That gate stubs the ~20 WordPress functions the plugin actually calls (deployment/wordpress/tests/),
renders the shortcode against the REAL built manifest.json, and asserts the contract
HOW-TO-TEST.md defines: the element tag is emitted, profile-config decodes to valid
JSON, the script src equals the catalog path, the SRI integrity is injected by the
script_loader_tag filter, crossorigin="anonymous" is present — plus a negative test
that a malformed integrity is rejected.
Status honesty — say "contract-proven", not "proven". What is proven: the plugin emits
the correct markup, repeatably and in CI. What is not: that WordPress renders the page,
that the block editor registers it, or that a browser executes the result. Those still need
a real WP install (wp-env start — see deployment/wordpress/doxa-maps-embed/HOW-TO-TEST.md).
5. Desktop — Tauri (built, not shot — a binary and a .deb ARE emitted; only the GUI launch is unverified)
Install: Bun ALONE for the contract check. For the binary: the Rust toolchain (rustup),
the Tauri CLI (cargo install tauri-cli), and the platform's system webview libs.
bun run check:tauri # CONTRACT check — needs NO Rust, NO Tauri CLI
cargo tauri build # the binary — needs the toolchain above
What "built, not shot" means here, measured rather than asserted. cargo tauri build has been run
and its outputs are on disk: deployment/src-tauri/target/release/doxa-maps-desktop is a 67 MB ELF
64-bit executable, and target/release/bundle/deb/ holds a 60 MB .deb. Eight source files are
git-tracked under deployment/src-tauri/. What has not happened is a window opening: this build box
is headless with no Xvfb, so nothing has confirmed the app paints rather than launching to a blank
frame. Say "built, not shot" — not "proven", and not "aspirational".
deployment/src-tauri/tauri.conf.json + the deployment/src-tauri/ Rust crate wrap the same public/ tree
Capacitor and the CDN serve — build.frontendDist is ../public, the desktop counterpart of
Capacitor's webDir. One bun run build now feeds four consumers: CDN, Capacitor, local
staging, and Tauri. The crate carries no application logic; the product is the web tree.
check:tauri is runnable anywhere, exactly like check:mobile. It asserts the config points
at files that exist: frontendDist resolves (Tauri resolves it relative to deployment/src-tauri/ — the
easiest thing to get wrong), every window url exists in the built tree, the identifier matches
Capacitor's appId, and the three crate files are present. That guards the soft failure Tauri
shares with Capacitor — a wrong path yields a BLANK WHITE WINDOW at launch, not an error.
Negative-tested five ways (wrong frontendDist, missing window url, identifier drift, empty
windows array, missing crate file); each exits 1.
The token/CORS caveat, stated before you hit it. A Tauri webview serves from a custom origin (
tauri://localhost, orhttp://tauri.localhoston Windows), not from your token host. ThetokenUrlendpoint must send CORS headers permitting that origin — the same surprisecapacitor.config.jsondocuments forhttps://localhost. The fix is a response header on the token host, not a change intauri.conf.json.
Status: BUILT — artifacts emitted; launch unverified. Run 2026-09-21 with the real toolchain
(tauri info all ✔: webkit2gtk 2.50.6, rustc/cargo 1.96.1, frontendDist ../public, Vue.js/Vite):
bun run tauri:build produced a 65 MB ELF executable and a 58 MB .deb
(Package: doxa-maps, Depends: libwebkit2gtk-4.1-0, libgtk-3-0). The web tree is compiled INTO
the binary — 19 distinct doxa-maps/... asset paths are embedded in the ELF, which is why the
.deb holds only 6 entries.
bundle.targets is ["deb"], not "all": rpmbuild and linuxdeploy (AppImage) are absent on
this box, so "all" would fail two of three formats at the very end of a ~6-minute release build.
Still not "proven" — say built, not shot. The GUI launch is unverified because this VPS is
headless: the binary panics in tao with "Failed to initialize GTK" (no display, not a defect) and
no Xvfb is installed. See deployment/src-tauri/README.md for placement rules, the
token/CORS caveat and the full artifact table.