A reader who picks a religion family on the map now sees the grid narrow to the
same peoples, and a language selection travels the same way. Region already
linked; these two did not, because the two halves do not name a religion the
same way and nothing yet proved what the honest translation was.
The map filters religion through its legend's flat tab, keyed by the FIRST
LETTER of the religion code ('B'); the grid filters through a derived axis that
runs library/colors/religion.js forward per row ('BLA' -> 'Buddhism'). One
shared table, read in opposite directions, copied by neither half — so the wire
carries the family NAME and each side resolves it against its own data.
Two measurements changed the design on the way:
- mapStore.filters.religions, which the map's filter panel writes, is read by
NOTHING: the mapLayers.applyFilters() it was meant to reach is called and
never implemented anywhere in the tree. A link routed through it would have
set state, passed a smoke test and dimmed no pin.
- the map's legend selections are mutually exclusive by design, so it can
light exactly one of region/religion/language. The composite mirrors that
rather than letting the grid hold three facets the map cannot show.
Religion links at the FAMILY and not at the code, deliberately: a map cannot
distinguish 'BLA' from 'BMA' — both dim as Buddhism — so linking at the code
would promise a precision one half cannot keep.
check:linked-vocabulary proves it by measurement rather than by reading: it
compares the total reached over the composite's wire against the total the grid
gives for the same facet on its own (83 of 2,107 both ways), and runs the MUTANT
first — the legend key 'B' instead of 'Buddhism' — failing as NO-TEETH if the
wrong mapping scores the same as the right one. Measured: mutant 0, real 83.
The CLI doc's apk:debug quote was realigned in passing; package.json had gained
app:start-page and the doc still described the older chain.
207 lines
13 KiB
Markdown
207 lines
13 KiB
Markdown
## 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](https://bun.sh) (`curl -fsSL https://bun.sh/install | bash`) — Node ≥ 18 also works (`npm run <script>`; the scripts are identical). Then:
|
|
>
|
|
> ```bash
|
|
> bun install
|
|
> ```
|
|
|
|
The scripts these commands are grounded in (from this branch's `package.json`):
|
|
|
|
```jsonc
|
|
// 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 check:backlog-refs check:composite-embed-knobs check:linked-vocabulary",
|
|
"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 app:start-page && 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"
|
|
```
|
|
|
|
`BUILD_ALLOW=1 bun run build` writes the whole deployable tree to `public/doxa-maps/`, including
|
|
`manifest.json`: the machine-readable index that carries a ready-to-paste **iframe** snippet and a
|
|
**`<script src>` + SRI** snippet for every plugin.
|
|
|
|
### Summary table
|
|
|
|
| # | Deployment method | Install first | Command(s) | Status |
|
|
|---|---|---|---|---|
|
|
| 1 | **iframe** | Bun | `bun install` → `BUILD_ALLOW=1 bun run build` → drop `public/doxa-maps/` on any host | ✅ production |
|
|
| 2 | **Mobile app (Android)** | Bun for the check; + Android SDK · `adb` · JDK 17 for the APK | `bun run check:mobile` (no toolchain) · `bun run phone` | ✅ built |
|
|
| 3 | **JavaScript embed** | Bun | `BUILD_ALLOW=1 bun run build`, then paste the `embed.script` snippet from `manifest.json` | ✅ production |
|
|
| 4 | **WordPress / Disciple Tools** | Bun (+ `php` for the check) | paste a snippet into a Custom HTML block, or install `deployment/wordpress/doxa-maps-embed/` · `bun run check:wp-embed` | 🧪 proof of concept |
|
|
| 5 | **Desktop (Tauri)** | Bun for the check; + Rust · Tauri CLI · webview libs for the binary | `bun run check:tauri` (no toolchain) · `bun run tauri:build` | 🧪 proof of concept |
|
|
|
|
---
|
|
|
|
### 1. iframe
|
|
|
|
**Install:** Bun.
|
|
|
|
```bash
|
|
bun install
|
|
BUILD_ALLOW=1 bun run build # builds every plugin-designs/* bundle -> public/doxa-maps/
|
|
bun run serve # optional: serves public/ at http://localhost:4173
|
|
```
|
|
|
|
Each profile in `manifest.json` carries an `embed.iframe` snippet:
|
|
|
|
```html
|
|
<iframe src="./all-people-groups/all-people-groups/index.html"
|
|
width="100%" height="600"
|
|
style="border:none;border-radius:12px;" allowfullscreen></iframe>
|
|
```
|
|
|
|
**The folder is position-independent.** Snippet URLs are relative to the tree root, so `doxa-maps/`
|
|
works dropped on any static host. For absolute URLs, set `embedBaseUrl` in the build config.
|
|
|
|
### 2. Mobile app — Android / Capacitor
|
|
|
|
**Install:** Bun · **Android SDK** (`sdkmanager`, `adb` on your PATH) · **JDK 17** · a device or
|
|
emulator. The `@capacitor/*` packages are already devDependencies.
|
|
|
|
```bash
|
|
bun run check:mobile # contract check — needs NO JDK, SDK or device
|
|
bun run phone # the full path: build → cap:sync → cap:verify → gradlew assembleDebug → adb install
|
|
bun run apk:debug # or the halves separately: build the APK only
|
|
bun run apk:install # push the built APK to a connected device
|
|
```
|
|
|
|
**`check:mobile` runs anywhere.** It copies the built tree into the Android project and asserts
|
|
`deployment/capacitor/android/app/src/main/assets/public/index.html` exists, which catches the one
|
|
silent failure: a wrong `webDir` launches to a blank white screen, not an error. Only Android is
|
|
scaffolded; iOS is not set up.
|
|
|
|
### 3. JavaScript embed
|
|
|
|
**Install:** Bun. Build, then paste the plugin's `embed.script` snippet from `manifest.json`: a
|
|
`<script src>` with a Subresource-Integrity hash, plus the custom element.
|
|
|
|
```html
|
|
<script src="./all-people-groups/all-people-groups.js"
|
|
integrity="sha384-…" crossorigin="anonymous"></script>
|
|
<all-people-groups
|
|
profile-config='{"profile":"all-people-groups","tk":"YOUR_MAPBOX_TOKEN"}'>
|
|
</all-people-groups>
|
|
```
|
|
|
|
**The hash changes on every build**, so always copy it from the current `manifest.json`. The element
|
|
takes props and emits events, so it can wire into the surrounding page.
|
|
|
|
**On a partner's site, a JS embed needs two values an iframe does not.** An iframe carries its own
|
|
document base; a script runs in the host's document, so relative asset and API paths would resolve
|
|
against the host and 404. The snippet already ships both:
|
|
|
|
| | iframe | JavaScript embed |
|
|
|---|---|---|
|
|
| `assetsBase` | not needed | **required**, in `profile-config` (`YOUR_TREE_URL/assets` until you set `assetsBaseUrl`) |
|
|
| API base | not needed | **required**, `window.MAP_APP_API_URL` (a host global) |
|
|
| Mapbox token | inherited by the leaf page | replace `YOUR_MAPBOX_TOKEN` |
|
|
|
|
### 4. WordPress / Disciple Tools — proof of concept
|
|
|
|
**No plugin needed for the simple path:** build, then paste the iframe or script snippet into a
|
|
WordPress **Custom HTML** block.
|
|
|
|
**The plugin path:** `deployment/wordpress/doxa-maps-embed/` adds a `[doxa_map]` shortcode and a
|
|
Gutenberg block, registering the bundle through WordPress's own dependency graph so the SRI hash and
|
|
`crossorigin` survive and the script loads once per page.
|
|
|
|
```bash
|
|
bun run check:wp-embed # renders the REAL shortcode with no WordPress installed
|
|
```
|
|
|
|
**What is proven, and what is not.** The check renders the shortcode against the real `manifest.json`
|
|
and asserts the element tag, valid `profile-config` JSON, the script `src`, the injected SRI hash and
|
|
`crossorigin` — and rejects a malformed hash. It does not prove that WordPress renders the page or a
|
|
browser runs it; that needs a live install (`wp-env start`, see
|
|
`deployment/wordpress/doxa-maps-embed/HOW-TO-TEST.md`).
|
|
|
|
### 5. Desktop — Tauri — proof of concept
|
|
|
|
**Install:** Bun for the check. For the binary: Rust (`rustup`), the Tauri CLI
|
|
(`cargo install tauri-cli`) and the platform's webview libraries.
|
|
|
|
```bash
|
|
bun run check:tauri # contract check — needs NO Rust, NO Tauri CLI
|
|
bun run tauri:build # the binary and a .deb — needs the toolchain above
|
|
```
|
|
|
|
**The desktop app wraps the same tree.** `deployment/src-tauri/tauri.conf.json` points
|
|
`build.frontendDist` at `../public`, the desktop twin of Capacitor's `webDir`; the Rust crate carries
|
|
no application logic. `check:tauri` asserts that path resolves, every window `url` exists in the built
|
|
tree, the identifier matches Capacitor's `appId`, and the crate files are present.
|
|
|
|
**Token caveat:** a Tauri webview serves from `tauri://localhost` (or `http://tauri.localhost` on
|
|
Windows), so a `tokenUrl` endpoint must send CORS headers for that origin. The fix is a response header
|
|
on the token host, not a change in `tauri.conf.json`. Details:
|
|
[`deployment/src-tauri/README.md`](../deployment/src-tauri/README.md).
|
|
|
|
---
|
|
|
|
## Notes and history
|
|
|
|
The measurements behind the sections above.
|
|
|
|
- **Mobile — `check:mobile` needs no toolchain (verified 2026-09-20).** With `java`, `gradle`, `adb` and
|
|
`sdkmanager` all absent, sync exited 0, the whole `public/` tree was staged into the native assets and
|
|
`cap:verify` passed; removing `index.html` made it exit 1. The doc once said "21 bundles staged"; that
|
|
count named the wrong unit (`cap sync` copies `webDir` wholesale) and went stale, so it says "the whole
|
|
tree" now.
|
|
- **JavaScript embed — foreign-host run (2026-09-20).** Both embed methods were run on a foreign host page
|
|
until they failed: the iframe needed nothing; the script embed needed `assetsBase` and the API base,
|
|
which the snippet now emits. Same-root and foreign-host both then passed (map painted, attribution
|
|
shown, 0 errors, 0 404s).
|
|
- **WordPress — corrected 2026-09-20.** This section once said no WordPress build step existed; the
|
|
shortcode + block plugin and its contract check were added after. The check stubs the ~20 WordPress
|
|
functions the plugin calls (`deployment/wordpress/tests/`).
|
|
- **Tauri — the build ran (2026-09-21).** With the real toolchain (`tauri info` all ✔: webkit2gtk 2.50.6,
|
|
rustc/cargo 1.96.1) `bun run tauri:build` produced a 65 MB ELF executable and a 58 MB `.deb`
|
|
(`Depends: libwebkit2gtk-4.1-0, libgtk-3-0`); a later run measured 67 MB and 60 MB. The web tree is
|
|
compiled into the binary, which is why the `.deb` holds only 6 entries. `bundle.targets` is `["deb"]`
|
|
because `rpmbuild` and `linuxdeploy` were absent, so `"all"` would fail two formats at the end of a
|
|
~6-minute build. The window has not been opened: the build box is headless, and the binary stops with
|
|
"Failed to initialize GTK" (no display, not a defect). `check:tauri` is negative-tested five ways
|
|
(wrong `frontendDist`, missing window url, identifier drift, empty windows array, missing crate file).
|