doxa-maps/documentation/README-SECTION-3-cli-commands.md
doxa-maps-88655c70 4b39abf126 Regenerate the library index during the build, so it cannot fall behind
check:library-index existed, was correct, and had been failing for about 19.6
hours. Nothing noticed because it runs only when a person types it:
build:unlocked chains 24 gates and this was not one of them.

check:no-orphan-gates passes on it, which is worth understanding rather than
treating as a gap in that gate. It asks whether every gate is REACHABLE — "named
in package.json or imported by something that is" — and check:library-index is
named there. Reachable and wired are different properties. 64 gate-shaped files
exist; 24 run on every build; the rest only run on request.

The fix is to regenerate rather than to check harder. check:composite is also
mtime-based and is in the chain, but it guards build output whose freshness
depends on build ORDER — a real invariant only a human can resolve.
reference-library-index.md is DERIVED from library/, so a staleness failure has
exactly one remedy: run the generator. Wiring the check alone would fail builds
and hand the author a chore; wiring the generator makes the staleness
impossible, which is the only way the file's own header — "DERIVED — cannot
drift from the code" — can be true.

Both are now in the pre-build group, generator then check, beside the other
source-only gates and before `clean`. Safe to run unconditionally: measured
deterministic (byte-identical on re-run) and 237ms. The check is kept after it
so a generator that silently writes nothing still fails the build rather than
passing quietly.

Updating package.json made check:quoted-scripts fail, which is the gate doing
its job, and it surfaced that the quoted copy in README-SECTION-3 was ALREADY
stale before this change: it omitted check:tab-radius, check:synced-css,
check:wp-embed and check:doc-scripts. An omitted step in a quoted list is
invisible — there is no wrong character to notice, only a check the reader
believes is not in the chain. The doc now matches what runs.

Verified by a full build: the generator ran, the check passed, vite built every
bundle, exit 0. The index is not dirty afterwards, which is the determinism
claim holding inside the real chain rather than in an isolated test. Five lines
in the build log match /error|FAIL/ and all five are gate DESCRIPTIONS
containing those words, not results.
2026-09-23 07:25:01 +00:00

15 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.
// 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 check:template-literal && bun run check:mjs-parse && bun run check:sfc-parse && bun run check:tab-radius && bun run check:synced-css && bun run check:wp-embed && bun run check:doc-scripts && bun run library-index && bun run check:library-index && 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:facet-one-field && bun run check:heartbeat && bun run check:kpi-unit && bun run check:composite-reg && bun run check:descriptions && bun run check:no-abs-paths && bun run check:no-codename && 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":           "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-cities is withheld by gatedProfiles), and 19 subdirectories under public/doxa-maps/ — one of which is assets/, not a bundle at all. cap sync copies webDir wholesale, 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 + contract-proven; no binary emitted)

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

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, or http://tauri.localhost on Windows), not from your token host. The tokenUrl endpoint must send CORS headers permitting that origin — the same surprise capacitor.config.json documents for https://localhost. The fix is a response header on the token host, not a change in tauri.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.