Some checks are pending
build-gate / build (push) Waiting to run
Makes the cheap half of last tick's P0 automatic. A folder rename had rewritten three npm package names, so bun install failed in a fresh clone while everything here passed -- the real packages were already in node_modules from before the rename, making the breakage invisible to every build and every gate. Checks two things, offline, in milliseconds: the declared name resolves under node_modules/, AND the installed package's own name field equals the declared key. The first alone catches the capacitor case; the second catches a name that resolves to a DIFFERENT package, which a path-only check would miss. Together they detect the corrupted-name class without a network round-trip, which is what lets it run on every build rather than in a nightly job nobody watches. It does not replace the clone test. A gate can only see what is IN the repo; only a clone sees what the repo fails to do on a clean machine. Negative-tested four ways: recreating the EXACT P0 (the three @deployment/capacitor/* names) FAILS naming all three; a name that resolves to a package whose own name differs FAILS; an absent node_modules SKIPs honestly rather than passing or failing; an empty dependency list FAILS via the vacuous guard. Wired into build:unlocked and confirmed running inside a full build (17 declared, 17 verified, exit 0).
503 lines
42 KiB
Markdown
503 lines
42 KiB
Markdown
# Reference
|
||
|
||
Look-up tables and contracts for doxa-maps. For learning, see the
|
||
**[tutorial](./tutorial.md)**; for tasks, **[how-to.md](./how-to.md)**; for the *why*,
|
||
**[why.md](./why.md)**; for the short "what it is", **[explanation.md](./explanation.md)**.
|
||
|
||
`doxa-maps` is a Vite bundler: **one shared library, N independent embeddable bundles.** Each
|
||
bundle is a single self-contained `.js` file that registers a custom HTML element
|
||
(`<doxa-map>`, `<doxa-research-map>`, …) you can drop on any web page.
|
||
|
||
---
|
||
|
||
## The three zones
|
||
|
||
| Zone | Path | Alias | License | You edit it… |
|
||
|---|---|---|---|---|
|
||
| **Shared library** | `library/` | `@map` | Apache-2.0 | …once; the fix reaches every bundle on next build |
|
||
| **Your app-profile** | `plugin-designs/<name>/` | — | yours | …freely; nothing here leaks to other maps |
|
||
| **The build machine** | `internal/` | — | Apache-2.0 | …never — it's the bundler's own scripts |
|
||
|
||
**Terminology.** "Profile" is used at two levels: a **bundle / app-profile** is a folder under
|
||
`plugin-designs/`; a **profile screen** is one `.vue` file sitting **flat** in that folder (no
|
||
`profiles/` subfolder — the entry globs `./*.vue`). A bundle can hold several screens; the host
|
||
picks which to render with the `profile` field of `profile-config`.
|
||
|
||
**Two words first.** An **element** is a custom HTML tag this repo invents, such as `<doxa-map>`: the browser
|
||
learns it once, then you can paste it into any page as often as you like; each pasted tag is one live copy, an
|
||
*instance* (people also say "web component"). An **app profile** (this repo also says *mounting file*) is one
|
||
flat `.vue` in `plugin-designs/<bundle>/` that defines a whole map app — its main component; the settings on the
|
||
tag (`profile-config`) name which app profile to load and the shared `ProfileLoader` picks it. In class-and-object
|
||
terms: the element class is the class, each tag is an instance, the app profile is the screen that instance loads.
|
||
Most repos need one such file per app; doxa-maps builds hundreds of maps from one shared `library/`, so every map
|
||
gets its own app profile — and those files come in five shapes:
|
||
|
||
**Profile types — the ladder**, smallest first (head words are established terms; the `-element` compounds are
|
||
this repo's own; the coined names stay as a.k.a.):
|
||
`single-element` → `preset-element` → `multi-view-element` → `composite-element` → `app-shell`.
|
||
|
||
| Type (a.k.a.) | Machine key | What it is | How the build recognises it |
|
||
|---|---|---|---|
|
||
| **single-element** (single) | `single` | one app profile, one map — paste its tag anywhere, set it up in `profile-config`; the default | a flat `.vue` with no `<profile>.meta.json` and no `*.instances.json` in the folder |
|
||
| **preset-element** (parameterized) | `parameterized` | one app profile + a small table of named presets; each row is a ready-made map with its settings baked in, with its **own embed page** and sidebar entry — same code, same tag | the bundle ships `<name>.instances.json` — `{ "instances": [{ "id", "title", "props" }] }` or a bare array; the build writes one embed page per row at `<bundle>/<profile>/<id>/index.html` |
|
||
| **multi-view-element** (monolithic) | `monolithic` | one app profile holding several screens you switch between — tabs or steps — in one `.vue`, shown one screen at a time | declared: `<profile>.meta.json` → `{ "type": "monolithic" }` (or `"multi-view-element"`) |
|
||
| **composite-element** (nested-elements-mfe) | `nested-elements-mfe` | an app profile whose template places other app profiles' maps together (stacked or side by side, e.g. a map + a poster wall) and ships the set as one new element — one tag embeds it all | declared: `<profile>.meta.json` → `{ "type": "nested-elements-mfe", "composes": [ … ] }` (or `"composite-element"`) |
|
||
| **app-shell** (nested-mfe-spa) | `nested-mfe-spa` | the outer frame — sidebar, header, navigation — that loads any of the other kinds and switches between them without a reload; the top rung, the full doxa-maps app | a compiled Vue custom element **today** — `plugin-designs/doxa-maps-spa/doxa-maps-spa.vue`, built via `defineCustomElement` like every other rung (its shell layout is `library/layouts/app-wrapper-material/`). `app-wrapper-material/index.html` is now only the dev-time plugin-gallery scaffold, not the shipped shell |
|
||
|
||
> **Which bundle is the capability-N example *right now*?** See
|
||
> **[reference-capability-exemplars.md](./reference-capability-exemplars.md)** — generated from
|
||
> `manifest.json` by `bun run capability-exemplars` and gated by `bun run check:capability-exemplars`.
|
||
> **Cite that file; never cite memory.** This one question has been answered wrongly in three separate
|
||
> places — a gate, this README's capability captions, and the `/plugin-designer` skill — because a
|
||
> folder's apparent simplicity does not decide its tier. `doxa-vision-card` looks like the lightest
|
||
> thing in the repo and is **capability 2**, since it ships an `*.instances.json`.
|
||
|
||
Composite kinds are always **declared** in `<profile>.meta.json` (`{ "type": …, "label": … }`, either spelling —
|
||
see `TYPE_ALIASES` in `internal/plugin-gallery/discover.mjs`) — the build never guesses them from source (tag-scanning
|
||
produced false positives). Discovery is one pure module,
|
||
`internal/plugin-gallery/discover.mjs`; `bun run dev` renders its result as the **plugin gallery** (the same
|
||
generated page the build writes to `public/index.html` — see "Plugin gallery" under Build scripts: a list grouped by
|
||
bundle, every map deep-linkable at `#/<bundle>/<profile>[/<instance>]`), and `bun run smoke:gallery` proves every
|
||
element actually boots inside it.
|
||
|
||
---
|
||
|
||
## Directory tree
|
||
|
||
```
|
||
doxa-maps/
|
||
├── library/ ← SHARED LIBRARY (alias @map, Apache-2.0)
|
||
│ ├── index.js ← thin barrel; prefer subpath imports for tree-shaking
|
||
│ ├── ProfileLoader.vue ← the ONE component that reads profile-config & loads a screen
|
||
│ ├── components/ ← map UI: legends/, map-controls/, poster/, dashboards/, drawers
|
||
│ ├── composables/ ← useMapInstance, useMapData, useMapLayers, … (+ legends/, poster/)
|
||
│ ├── api/ ← DataSourceManager (csv | api | rest-api) + sources.json
|
||
│ ├── colors/ ← one color strategy per file, auto-registered by _registry.js
|
||
│ ├── constants/ ← never-change values: mapDefaults.js, zoom.js, poster*.js
|
||
│ ├── i18n/locales/ ← 11 base locales: ar de en es fr hi it pt ro ru zh
|
||
│ ├── stores/ ← Pinia: dataStore, mapStore, uiStore (per-instance)
|
||
│ └── utils/ ← pure helpers (geoUtils, clustering)
|
||
│
|
||
├── plugin-designs/ ← ALL bundles live here (each subfolder = 1 bundle)
|
||
│ ├── <bundle>/
|
||
│ │ ├── index.js ← bundle ENTRY — globs ./*.vue, registers the web component
|
||
│ │ ├── index.html ← Vite html entry = the dev STAGING page
|
||
│ │ ├── <profile>.vue ← 1..N FLAT profile screens (no profiles/ subfolder)
|
||
│ │ └── src/ ← everything else the map needs (its own colors/, api/, …)
|
||
│ └── template-bundle/ ← minimal copy-me reference bundle
|
||
│ (folders starting with _ or . are SKIPPED by the build)
|
||
│
|
||
├── internal/ ← the bundler's own build scripts — never edited by map builders
|
||
├── deployment/capacitor/ ← native projects (android/, ios/)
|
||
├── public/ ← THE SERVED WEB-ROOT, committed:
|
||
│ ├── index.html the app shell
|
||
│ ├── doxa-maps/<bundle>/ built bundles + manifest.json + staging HTML
|
||
│ └── assets/ posters & data the maps fetch at runtime
|
||
├── vite.config.js ← multi-entry: discovers plugin-designs/<name>/ folders
|
||
└── vite.doxa-maps-build-config.json ← output tree name + copy destinations + token settings
|
||
```
|
||
|
||
---
|
||
|
||
## The `profile-config` runtime contract
|
||
|
||
A bundle exposes ONE attribute, `profile-config` — a JSON string parsed by
|
||
`ProfileLoader.vue` at runtime. It is the entire wiring between host page and map, so the same
|
||
built bundle re-points to different data/colors/language with **no rebuild**.
|
||
|
||
```html
|
||
<doxa-map profile-config='{
|
||
"profile": "doxa-simple-map", // which flat .vue screen to render (required)
|
||
"tk": "pk.eyJ...", // Mapbox token (required; host-supplied, never baked)
|
||
"instanceId": "my-map", // optional; isolates window events between embeds
|
||
"dataSource": "pray-tools", // or { "type":"rest-api","endpoint":"https://…" }
|
||
"colorSet": "default",
|
||
"locale": "en",
|
||
"provider": "mapbox", // map engine: "mapbox" (default) | "maplibre" (preview);
|
||
// also flippable per page via ?provider= or window.MAP_PROVIDER
|
||
"assetsBase": "/assets", // where tables/, boundaries/, previews/ are served (see below)
|
||
"shareUrl": "https://…", // optional; the WHOLE url the Share button links/embeds (beats everything)
|
||
"embedBaseUrl": "https://…/doxa-maps", // optional; served tree root the Share button joins embed pages to
|
||
"tabs": [ /* parameterized screens read this */ ]
|
||
}'></doxa-map>
|
||
```
|
||
|
||
- `assetsBase` — data-driven profiles (connectivity-map, resources-map, cities-map, the poster walls
|
||
and data tables) fetch `${assetsBase}/tables/*.json` and `${assetsBase}/boundaries/*.geojson`
|
||
**before** they create their map. Unset, it defaults to `./assets` **relative to the host page**;
|
||
if the host doesn't serve that path the fetch gets an HTML fallback, `JSON.parse` throws, and the
|
||
map is never created (no console error, only the profile's status line). The generated pages and
|
||
the shell set it for you (`../../../assets` or the absolute `assetsBaseUrl` from the build config);
|
||
any other host must point it at a copy of `public/assets/`.
|
||
|
||
- **Share ladder** (`library/utils/embedBase.js`) — what the Share button links and iframes:
|
||
`shareUrl` (a whole url, e.g. the plugin gallery's own deep link) > the profile's **tree-relative**
|
||
embed page (`<bundle>/<profile>/index.html`, the `embedPath` prop or `shareEmbedPath`) joined to an
|
||
embed base — `embedBaseUrl` > `window.MAP_EMBED_BASE_URL` > the root the built bundle derives from
|
||
its **own script url** (two segments above `<bundle>/<bundle>.js`, so a dropped tree works on any
|
||
host/CDN with no config) > the page's own location. Never hardcode `/js/doxa-maps/…` in a profile.
|
||
|
||
- `profile` **must** match a flat `.vue` filename in the bundle. Mismatch → an inline
|
||
"Profile not found" error box listing the available names.
|
||
- `tk` is supplied at runtime so the token is never compiled in (dev uses `.env`'s
|
||
`VITE_MAPBOX_TOKEN`).
|
||
- `instanceId` scopes cross-instance window events so multiple embeds never interfere.
|
||
- `dataSource` is handed to `DataSourceManager` (`csv` | `api` | `rest-api`).
|
||
- `ProfileLoader` `provide()`s to descendants: `mapboxToken`, `dataSource` (both **computed
|
||
refs** — `unref` before use), `colorSet`, `profileConfig`, `instanceId`, `lang`, and the
|
||
instance-scoped Pinia stores `uiStore` / `mapStore` / `dataStore`. Descendants **inject**;
|
||
never call `useXxxStore()` directly (it would cross-bleed between embeds).
|
||
|
||
Each bundle registers a **shadow-DOM** web component via `defineCustomElement`, with its own
|
||
ShadowRoot, styles, Pinia, and i18n. `customElements.define()` is one-shot per tag name, so
|
||
each bundle registers a **distinct** tag.
|
||
|
||
---
|
||
|
||
## Build scripts & output
|
||
|
||
| Script | What it does |
|
||
|---|---|
|
||
| `bun run dev` | Vite dev server + HMR (default `http://localhost:5173`, or the next free port — read the terminal). `/` is the **plugin gallery** of every element — `internal/generate-gallery.mjs` in mode `dev` over `internal/plugin-gallery/{discover,dev-plugin}.mjs`; every bundle is also served at its own html path. Dev-only routes: `/__plugin-gallery/manifest.json` (the manifest-shaped model the page fetches), `/__plugin-gallery/registry.json` (the discovery registry the smoke tools read), `/assets/*` (served from `public/assets/`, else `plugin-designs/*/data/`). **Writes nothing to `public/`** — no HMR across that boundary. |
|
||
| `bun run smoke:gallery` | Opens every element of the plugin gallery by deep link in Firefox and proves it boots: element defined, shadow DOM populated, no error card, a WebGL canvas for map profiles (checked through nested shadow roots), no HTML-for-JSON data responses. `--json <file>` writes the evidence; exit 1 on any failure. The pre-handoff gate. |
|
||
| `bun run smoke:share` | The share round-trip gate (Firefox): opens a host page (`--plugin-gallery` = every canvas element of the dev plugin gallery; `--url <page> --tag <element>` = the website or any CDN page), clicks Share, reads the URL, fetches it, reopens it in a fresh page and asserts the SAME custom element mounts with a WebGL canvas. `--json <file>`; exit 1 on any failure — proves REQ-1 ("links work wherever the tree is hosted"). |
|
||
| `bun run check:public-hygiene [<dir>] [--strict]` | Proves a served folder is fit to publish: scans for local paths, internal folder names, dev-server URLs, JWT-shaped tokens, `.env`/token files, source maps, documentation and staging/preview pages, and prints an inventory (bundles, leaf pages, assets by folder). Under `--strict` any root entry other than `index.html`, `assets/` and `doxa-maps/` is a finding ("outside the drop-in contract"). Exit 1 on a finding. Run it on the website's drop-in before any public push. |
|
||
| `bun run sync:dropin <dest> [--from-config \| --only a,b] [--prune]` / `bun run deploy:dev` | Syncs the built `public/` into a host checkout (e.g. the website's `public/embed/doxa-maps-build/`). **The drop-in contract** lives in ONE place — `vite.doxa-maps-build-config.json` → `dropin.include`, exactly three root entries: `index.html` (the built plugin gallery — the root page generated by `internal/generate-gallery.mjs`, listing every bundle/profile/instance and mounting the chosen one), `assets/` (data) and `doxa-maps/` (the bundle folders + `manifest.json`). `--from-config` copies only those, `--prune` removes everything else in the destination, so a host carries exactly the served web-root and nothing else at its root. `dropin.exclude` (same file) lists built files inside the allowlist that no page fetches — the full people-groups CSV, the tables only internal scripts read, the table index; they stay in `public/` for the scripts, are skipped by `--from-config` (counted as "excluded (not shipped)") and removed from the destination if a copy is there. Without an allowlist it is a plain full mirror. Never copies credentials, docs or staging pages; `--protect a,b` keeps host-owned files; `--dry-run` first; prints a copied/unchanged/deleted/pruned/excluded summary and the resulting top-level tree. `deploy:dev` = `build` → sync `--from-config --prune` into the website (`DOXA_DROPIN_DEST` overrides the path) — the only sanctioned way a map changes on the site; then commit in both repos. |
|
||
| `bun run build` | `clean` → one `vite build` per `plugin-designs/*/` folder → `tree` → `check:drop` → `check:parse` → `verify` → `gallery` (writes `public/index.html`, the built plugin gallery, from the manifest). |
|
||
| `bun run build:bundle` | Builds ONE bundle (needs `BUNDLE=<name>`). |
|
||
| `bun run build:watch` | Rebuilds into `public/` on every change (integration loop). |
|
||
| `bun run clean` | Wipes + recreates the `public/<name>/` output tree. |
|
||
| `bun run tree` | Writes `manifest.json` + self-contained staging HTML, then copies the tree to every build-config `paths` destination. |
|
||
| `bun run verify` | Data-integrity + shipped-bundle + network-policy guards (fast, no browser). |
|
||
| `bun run verify:full` | + rendered posters and live apps in Firefox. |
|
||
|
||
**Plugin gallery.** One generator, two modes. `internal/generate-gallery.mjs` exports
|
||
`renderGallery(model, { mode })`, a pure function from a manifest-shaped model (bundles → profiles →
|
||
instances, with `scriptUrl` / `iframeUrl` / `embed` / `integrity`) to the whole page: the list, the stage,
|
||
`#/<bundle>/<profile>[/<instance>]` deep links, Open page, Copy embed, the token ladder, lazy engine load,
|
||
the phone layout and the baked fallback. Mode `build` (`bun run gallery`) renders
|
||
`public/doxa-maps/manifest.json` into `public/index.html` — the root page of the served web-root and,
|
||
through the drop-in, the website's `/doxa-maps` page. Mode `dev` is what `bun run dev` serves at `/`: the
|
||
same page over `galleryModel(discoverSpaRegistry())` (`internal/plugin-gallery/dev-plugin.mjs`), differing
|
||
only in the loader (it imports `/plugin-designs/<bundle>/index.js` instead of adding the built IIFE), the live
|
||
model route (`/__plugin-gallery/manifest.json`), the `/assets/*` dev route and the composability ladder's
|
||
"?" popovers in the header — kept out of the built page to keep it lean. Nothing about the gallery is
|
||
written twice.
|
||
|
||
**How a build works.** `vite.config.js` is multi-entry by discovery: at build time it
|
||
registers every `plugin-designs/` subfolder that (a) doesn't start with `_`/`.` and (b) has
|
||
**both** `index.html` and `index.js`. One `vite build` builds **exactly one** bundle, selected
|
||
by `BUNDLE=<name>`, so the output is a self-contained **IIFE** (`inlineDynamicImports: true`).
|
||
`bun run build` loops every folder. Each bundle only bundles what its screens import, so sizes
|
||
differ (simple ≈ 420 KB, research ≈ 1.3 MB).
|
||
|
||
**Where output goes.** One bundle folder → one file: `public/doxa-maps/<bundle>/<bundle>.js`.
|
||
CSS is injected by JS (`cssCodeSplit: false`), so there is no separate `.css`. The tree root
|
||
name comes from `vite.doxa-maps-build-config.json` `name` (default `doxa-maps`).
|
||
`emptyOutDir: false` keeps sibling bundles intact when you rebuild one.
|
||
|
||
**Shadow-style comments are stripped from production bundles.** Component CSS is injected
|
||
from `useShadowStyles(\`…\`)` template literals that ship verbatim, so `internal/vite-plugin-strip-shadow-css-comments.mjs`
|
||
(build only, `enforce: 'pre'`) removes the `/* … */` blocks inside those literals and collapses the blank lines
|
||
they leave; a literal with `${…}` is left untouched with a warning, and dev mode keeps every comment.
|
||
|
||
**What a built bundle folder contains.** Beyond the IIFE, `bun run tree` generates an
|
||
`index.html` **per bundle** (staging/showcase), **per profile**, and **per instance** —
|
||
each profile/instance page is a standalone, directly linkable/iframe-able map — plus
|
||
`public/doxa-maps/manifest.json` with ready-made `embed.iframe`/`embed.script` snippets for
|
||
every profile. Full design: [architecture/embeddability.md](./architecture/embeddability.md).
|
||
|
||
**Preset instances.** A `<profile>.instances.json` beside a parameterized profile
|
||
(`{ profile, apiBaseUrl?, instances: [{ id, title?, props }] }`) makes the build emit one
|
||
standalone page per instance at `<bundle>/<id>/index.html`. Recipe:
|
||
[how-to.md](./how-to.md#ship-n-preset-variants-of-one-screen-instances).
|
||
|
||
**Bun is required.** `bun.lock` is the committed lockfile; `bun x vite` runs project-local
|
||
Vite. `npm run …` resolves the same scripts but Bun must be on PATH.
|
||
|
||
### Standalone gates — the SILENT failure classes
|
||
|
||
These are **not** in `build:unlocked`, deliberately: each checks something a fresh build would mask,
|
||
so running it inside the build would make it unfalsifiable. Run them before publishing.
|
||
|
||
| Script | Catches | Why it cannot live in the build |
|
||
|---|---|---|
|
||
| `bun run check:composite` | a composite (cap-4/cap-5) still shipping an OLD copy of a child it nests — a one-bundle rebuild does NOT recompile its parents | a full build makes every composite fresh *by construction*, so it could never fail there |
|
||
| `bun run check:shots` | a README screenshot that predates the bundle it shows — **a stale screenshot looks exactly like a current one** | needs a browser to repair (`bun run shots`), which CI does not have |
|
||
| `bun run check:library-index` | `documentation/reference-library-index.md` fallen behind `library/` — its header promises it "can't drift", which is only true if regenerated (`bun run library-index`) | a fresh checkout gives every file the same mtime, so mtime comparison always passes in CI (CI regenerates + diffs instead) |
|
||
| `bun run check:done-evidence` | a backlog card marked `done` with no bluf **and** no fields — a status word the next team cannot reconcile against the code | reads `_qa-cycle/`, which is not in any repo CI clones |
|
||
| `bun run verify:gate1` | the Gate-1 chain end-to-end against LIVE data: header filter → composite props → real grid re-filter → count back up | needs network; it is an acceptance test, not a build step |
|
||
| `bun run check:catalog-identity` | the live server serving something **other than** the catalog — a stale CDN copy, a half-finished publish, or the login wall. Hashes each live bundle and compares sha384 against the manifest's own `integrity` | needs network egress, which CI cannot guarantee; it SKIPs when unreachable, and only "reached and wrong" fails |
|
||
| `bun run check:catalog-currency` | the live catalog being **an older build** than yours (identity passes on a catalog three publishes behind), **and** a locally-built bundle that is **absent** from the catalog — the "nothing was lost in the merge" guarantee | needs network egress; SKIPs when the catalog cannot be reached at all, because an offline box must not fail a build |
|
||
| `bun run smoke:embeds` | an element that **builds, lists in the manifest, and renders nothing** — a thrown `setup()` removes the subtree silently, so every build-time gate still passes. Mounts every profile page and asserts real shadow content | needs a browser; CI has none |
|
||
| `bun run verify:map-paint` | a map that **lays out but never paints** — Mapbox creates a full-size `<canvas>` and only *then* tries for a GL context, so "canvas is 1440×900" is evidence of layout, never of paint. Asserts a live GL context + attribution | needs a browser **launched with the SwiftShader flags** (the MCP/default browser here has no GL at all) |
|
||
| `bun run verify:5-caps` | a capability tier that no longer has a working exemplar — the 5×5 product claim going stale | needs a browser + a Mapbox token |
|
||
| `bun run check:quoted-scripts` | a **doc that quotes a command which has since changed** — an omitted step in a quoted chain is invisible: there is no wrong character to notice, just a check the reader believes is in the chain | compares docs against `package.json`; cheap, but it guards documentation rather than build output |
|
||
| `bun run check:capability-exemplars` | `documentation/reference-capability-exemplars.md` drifting from the manifest's real tiers (regenerate with `bun run capability-exemplars`) | the table is generated *from* the manifest, so running it mid-build would compare a file against the thing that just wrote it |
|
||
| `bun run check:dep-names` | a DECLARED DEPENDENCY THAT IS NOT A REAL PACKAGE NAME. In `build:unlocked`. Checks two things offline: the name resolves under `node_modules/`, AND the installed package's own `name` field equals the declared key — the second catches a name that resolves to a DIFFERENT package, which a path-only check misses. SKIPs honestly when `node_modules/` is absent. Found the hard way 2026-09-22: a folder rename rewrote `@capacitor/{android,cli,core}` to `@deployment/capacitor/*`, so **`bun install` failed in a fresh clone** while everything here passed, because the real packages were already installed from before the rename. |
|
||
| `bun run check:no-abs-paths` | a MACHINE-SPECIFIC PATH in tracked source — a `/home/<user>/` prefix, its macOS equivalent, or the Windows user-profile form. It is in `build:unlocked`, listed here because the failure it prevents is a HAND-OFF failure: an absolute path works perfectly for whoever wrote it and breaks only on the inheritor's machine, so no test run by its author can catch it. Scans `git ls-files` (the set a clone receives), which excludes node_modules and build output without an ignore list. Measured 2026-09-22: the repo was clean BY ACCIDENT — a planted `/home/…` line passed check:public-hygiene, check:parse and check:quoted-scripts, because public-hygiene scans the SERVED tree, not source. |
|
||
| `bun run check:settled` | measuring or publishing a tree **mid-build** — a build's `clean` step `rm -rf`s `public/doxa-maps`, so samples taken seconds apart give opposite answers | it exists to be run *around* builds, not inside one |
|
||
|
||
### Before you run the list — three gates need a PRECONDITION, or they look broken
|
||
|
||
A bare `bun run <gate>` is not always meaningful, and the failure text does not always say so.
|
||
Measured 2026-09-22, by running each one:
|
||
|
||
| Gate | Needs | What a bare run does |
|
||
|---|---|---|
|
||
| `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 |
|
||
|
||
**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
|
||
export MAPBOX_TOKEN=$(curl -s https://doxa.life/api/maps/token \
|
||
| python3 -c 'import json,sys;print(json.load(sys.stdin)["token"])')
|
||
|
||
B=http://127.0.0.1:4950/doxa-maps
|
||
ARGS=""
|
||
for p in demo-map/demo-map cities-internet/connectivity-map doxa-research-map/research-map \
|
||
country-posters/countries-map doxa-simple-map/doxa-simple-map \
|
||
gospel-resources/resources-map my-upg-100-list/upg-100-list my-upg-100-list/upg-100-mfe; do
|
||
ARGS="$ARGS --url $B/$p/index.html?tk=$MAPBOX_TOKEN"
|
||
done
|
||
node internal/share-roundtrip-smoke.mjs $ARGS --browser chromium
|
||
```
|
||
|
||
> **The pass count alone proves nothing — check the reopened TAGS.** The failure this gate exists to catch
|
||
> (an SPA fallback, or a host that strips the `#` deep-link) renders *a* map for every share URL, so every
|
||
> target passes while every link is wrong. The discriminating evidence is that the run returned **six
|
||
> distinct custom-element tags** — `demo-map`, `cities-internet-map`, `doxa-research-map`,
|
||
> `country-posters-map`, `doxa-map`, `gospel-resources-map` — each matching its own plugin, with
|
||
> `canvas:y` on all eight. (The two `my-upg-100-list` profiles legitimately share one tag.) Derive the
|
||
> plugin list the same way it was derived here rather than pasting it stale:
|
||
> `grep -rln ShareButton plugin-designs/*/*.vue`.
|
||
| `smoke:embeds` · `verify:map-paint` · `verify:5-caps` | `MAPBOX_TOKEN` in the environment | **does not fail — it DEGRADES.** Without a token map profiles cannot mount, and `smoke:embeds` reports them as TIMEOUTS. It once read 27/40 for this reason and 40/40 with a token. A timeout here means "no token" far more often than "broken plugin" |
|
||
|
||
```sh
|
||
# the token every map gate wants. Verified 2026-09-22: returns 200 to a plain curl (no browser
|
||
# User-Agent needed — that requirement belongs to pray.doxa.life, a different host) and yields a
|
||
# 90-char pk. token.
|
||
export MAPBOX_TOKEN=$(curl -s https://doxa.life/api/maps/token | python3 -c 'import json,sys;print(json.load(sys.stdin)["token"])')
|
||
```
|
||
|
||
> **The token is URL-restricted, and that limits what these gates can prove here.** Measured: tiles
|
||
> return **200** with a `doxa.life` Referer and **403** from `127.0.0.1`, `localhost`, `nuxtbase.app`
|
||
> or no Referer. So on this box a map can *mount*, load its style and paint its data layer while the
|
||
> basemap stays empty — `verify:map-paint` still passes, because a live GL context and attribution
|
||
> are exactly what it asserts. Judging "does it LOOK right" needs an allow-listed origin.
|
||
|
||
> **★ AN EXIT CODE IS NOT A VERDICT UNTIL YOU HAVE READ THE MESSAGE.** Two of these read as FAIL in a
|
||
> batch run and are neither passing nor failing — they never started. A checklist that turns a
|
||
> precondition into a red line trains its reader to ignore red lines, which is how a real failure
|
||
> gets skipped.
|
||
|
||
The rest, grouped by what they guard — same rule (outside the build, run before publishing):
|
||
|
||
| Script | Catches |
|
||
|---|---|
|
||
| `bun run smoke` · `bun run smoke:gallery` · `bun run smoke:share` | the gallery page, the playground, and share-link round-tripping — each needs a browser |
|
||
| `bun run check:a11y-button-names` | an icon-only control with no accessible name (needs a browser to read the computed name) |
|
||
| `bun run check:capability-citations` | the README claiming a capability the manifest does not back |
|
||
| `bun run check:dxlife` | **any edit to the read-only DX-life clone** — the standing law that tree is never written |
|
||
| `bun run check:mobile` · `bun run check:tauri` · `bun run check:wp-embed` | the Capacitor sync, the Tauri config contract, and the WordPress PHP embed — deploy methods 2, 5 and 4 |
|
||
| `bun run verify:full` | the research corpus end-to-end |
|
||
|
||
> **★ A STANDALONE GATE ONLY FIRES IF SOMEONE REMEMBERS IT — so this table IS the mechanism.**
|
||
> Measured 2026-09-22: **26** gates live outside `build:unlocked`; only **nine** were listed here. Two
|
||
> of the unlisted ones (`check:quoted-scripts`, `check:capability-exemplars`) were already FAILING,
|
||
> and had been for two sessions, because a build step and a bundle were added without their docs
|
||
> being regenerated. Nothing was broken in the product — the gates were doing their job — but nobody
|
||
> ran them, because an inheritor reads this table as the list of what to run before publishing.
|
||
> **An undocumented standalone gate is indistinguishable from one that does not exist.**
|
||
|
||
**Why SRI and not an HTTP status.** `https://nuxtbase.app/app/doxa-maps/manifest.json` returns **200**
|
||
and is a 2,872-byte HTML **sign-in page** — the app-shell login wall. A status check reads that as
|
||
healthy. The public door is `/plugin-assets/doxa-maps/`. Every profile already carries an `integrity`
|
||
hash the build computed, so comparing it against the live bytes is a cryptographic identity proof
|
||
that costs nothing extra to maintain. Re-measured 2026-09-22: **18 of 18 bundles identity-proven**, and
|
||
the live manifest byte-identical to `platform/mfe-index`. (It read "17 of 17" on 2026-09-21; the catalog
|
||
has since gained `golden-path-check`.)
|
||
|
||
> **A dated measurement is not a stale claim — but it still misleads once the underlying number moves.**
|
||
> "17 of 17" was true when written and the gate reads the live manifest, so nothing was broken. The
|
||
> problem is what a READER concludes: seeing 17 beside a catalog of 18, they cannot tell whether a bundle
|
||
> went missing or the sentence simply aged. ★ If you quote a count here, re-run the gate and restate it —
|
||
> `bun run check:catalog-identity` prints the current figure in one line.
|
||
|
||
### The screenshot gates — three different lies a picture can tell
|
||
|
||
A README image can fail in three ways that all look identical to a passing shot run. `bun run shots`
|
||
reports `OK` for every one of them, because it proves a page *loaded* — never that it looks right.
|
||
|
||
| Script | Catches | In CI? |
|
||
|---|---|---|
|
||
| `bun run check:shots` | a PNG older than the bundle it shows — **a stale screenshot looks exactly like a current one** | report-only (repair needs a browser) |
|
||
| `bun run check:shot-distinctness` | two slots capturing the same view — shipped as "List — page 1"/"page 2", the same picture under two captions | **blocking** (parses one file) |
|
||
| `bun run check:shot-fidelity` | this machine cannot *render* what the shots must *show* (no emoji font ⇒ every icon a tofu box) | not yet — reports locally |
|
||
|
||
The distinctness case is the instructive one: the shot chased a "Next" control and a scroll that
|
||
**do not exist** (the list has no pagination), every attempt sat inside `try/catch` so the miss was
|
||
silent, and the two PNGs differed by 0.16% render noise — so they were *not* byte-identical and no
|
||
checksum would have caught them. Meanwhile a real third profile (`poster-wall`) had no shot at all.
|
||
**A slot faking a view it cannot reach, while a genuine artifact goes unshot, is the recurring
|
||
shape** — the same one that captioned the product SPA as the plugin-studio.
|
||
|
||
**`bun run check:quoted-scripts` is the exception that proves the rule above** — it is the one
|
||
doc-integrity gate that DOES run in CI (blocking), because it compares two files already in the
|
||
checkout: it needs no browser, no network, and no mtime. It asserts that the `scripts` block
|
||
quoted verbatim in `documentation/README-SECTION-3-cli-commands.md` still matches `package.json`. That doc
|
||
carries its own hand-written plea to re-transcribe after every edit, and drifted anyway — twice.
|
||
The second drift omitted `check:public-hygiene` (the secret scan) from the build chain it
|
||
documents, which is the dangerous shape: **an omission in a quoted list is invisible**, since
|
||
there is no wrong character to notice. `package.json` is what runs, so the fix is always to update
|
||
the doc.
|
||
|
||
`bun run verify:5-caps` proves all five capability tiers mount and render. It needs a token —
|
||
`MAPBOX_TOKEN=$(curl -s <token-endpoint>) bun run verify:5-caps` — and it sets **both** `?tk=`
|
||
(the top-level element) **and** `window.MAP_TOKEN` (a composite's nested children read the global,
|
||
not the query param). Without a token it reports the token outage rather than the capabilities.
|
||
|
||
---
|
||
|
||
## `vite.doxa-maps-build-config.json` fields
|
||
|
||
Edit this one file, then `bun run build`.
|
||
|
||
| Field | What it does |
|
||
|---|---|
|
||
| `name` | Output folder name (default `doxa-maps`). |
|
||
| `paths` | **Extra** folders to copy the whole built tree into. Point at a host repo's `public/` → that host serves the maps and becomes the CDN. Empty (the default) ⇒ written to `public/<name>/` only, since `public/` is already the committed web-root. |
|
||
| `embedBaseUrl` | The public URL the tree is served from, used to generate **absolute, paste-anywhere** embed snippets. Empty ⇒ relative URLs (position-independent; keeps the drop-portability gate green; required for Capacitor, which loads from `https://localhost`). |
|
||
| `tokenUrl` | Endpoint each generated page fetches its Mapbox token from — a URL, **not** a secret. Set absolute (`https://…/api/maps/token`) so a CDN folder and the mobile app both work; endpoint must send CORS for the embedding origins. `window.MAP_TOKEN_URL` overrides at runtime. |
|
||
| `token` | Opt-in literal token baked into every page. **Leave empty** — prefer `tokenUrl`. |
|
||
| `assetsBaseUrl` | Where the shared asset tree (previews, posters, boundaries) is served from. Empty ⇒ a relative path per page depth (self-contained under one web-root). Set absolute for partner embeds and mobile, where there is no shared web-root to climb to. |
|
||
|
||
**Token resolution ladder:** per-map `tk` prop > `?tk=` > baked `token` > `window.MAP_TOKEN_URL` > `tokenUrl` > same-origin `/api/maps/token`.
|
||
|
||
---
|
||
|
||
## Data sources
|
||
|
||
### ★ Persecution-safety — a rule about WHICH FIELDS a profile may carry
|
||
|
||
Read this before adding a field or a link to any profile. **Some profiles are deliberately kept free of
|
||
gospel/church/believer data, and a profile failing a "should have resource links" check may be OBEYING
|
||
that rule rather than missing a feature.**
|
||
|
||
The binding instance, stated in the code it governs (`plugin-designs/cities-internet/connectivity-list.vue`):
|
||
|
||
> **PERSECUTION-SAFETY:** this profile carries connectivity + population + language only. It has **NO
|
||
> gospel-resource, church or believer field and must not gain one.**
|
||
|
||
Measured 2026-09-22: all three `cities-internet` profiles have **zero** `href` bindings, and the live leaf
|
||
renders 1,567 nodes with **0 anchors**. That is correct. Its `source` field —
|
||
`{name: "World Bank Open Data", url: "https://api.worldbank.org/v2"}` — is **API provenance**, not a
|
||
user-facing destination, and it is surfaced as a citation paragraph when a region is expanded, which is the
|
||
separate promise that header makes ("every figure carries its source, so the numbers can be audited").
|
||
|
||
★ **Why this is written down here and not left in one file header.** A standing work item reads
|
||
*"gospel-resources **+ cities-internet** maps must render with REAL resource links"* — one line batching two
|
||
profiles whose contracts are opposite. `gospel-resources` genuinely renders 155 real eBible links; taking
|
||
the same line literally for `cities-internet` means **adding a gospel-resource field to a profile whose
|
||
header forbids exactly that**. A requirement that names two subjects deserves a per-subject check before
|
||
anyone drains it.
|
||
|
||
> **"Persecution-safe" is used for three different things in this repo** — don't assume one rule. Here it
|
||
> means FIELD SEPARATION. In `key-cities/cities-list.vue` it means placename precision ("a wrong placename
|
||
> in a persecution-sensitive dataset is the dangerous kind of wrong"). In
|
||
> `research-dashboard/.../DashboardDataTable.vue` it means a dependency-free table (no third-party grid).
|
||
> Only the first governs what data a profile may hold.
|
||
|
||
---
|
||
|
||
Maps read data through `@map/api/` driven by `library/api/sources.json`. Three source types:
|
||
|
||
| Type | What | Points at |
|
||
|---|---|---|
|
||
| `rest-api` | versioned REST endpoint (default) | the live people-group API; base URL resolves at runtime (`window.MAP_APP_API_URL` → `VITE_API_BASE_URL` → same-origin) |
|
||
| `csv` | a local CSV parsed at load | the Joshua Project dataset (+ photo fallbacks) |
|
||
| `api` | a single JSON URL | any plain JSON endpoint |
|
||
|
||
Country/region polygons are **not** a `sources.json` connection — they render from Mapbox's built-in
|
||
`country-boundaries-v1` tileset plus a lazy-loaded WAGF-regions GeoJSON.
|
||
|
||
**Adding or changing a source?** Full contract + checklist:
|
||
[architecture/data-sources.md](./architecture/data-sources.md).
|
||
|
||
---
|
||
|
||
|
||
## Colors
|
||
|
||
**Reused by more than one map → shared `library/colors/`. Only your map uses it → your bundle's
|
||
`src/colors/`** (merges over the shared set, that bundle only). Filename derives the mode key
|
||
(`language-family.js` → `languageFamily`).
|
||
|
||
**Writing or overriding a strategy?** Full contract + checklist:
|
||
[architecture/color-strategies.md](./architecture/color-strategies.md) ·
|
||
two homes in depth: [../contributing/COLORS.md](../contributing/COLORS.md).
|
||
|
||
---
|
||
|
||
## Importing from `@map`
|
||
|
||
Everything under `library/` is importable via the `@map/…` alias (`vite.config.js` maps `@map`
|
||
→ `library/`). The import path mirrors the file path: a file at `library/<sub>/<file>` imports
|
||
as `@map/<sub>/<file>`. Examples:
|
||
|
||
```js
|
||
import SemanticTreeLegend from '@map/components/legends/SemanticTreeLegend.vue'
|
||
import { useMapInstance } from '@map/composables/useMapInstance.js'
|
||
import { useMapData } from '@map/composables/useMapData.js'
|
||
import langFamilyByIso from '@map/data/langFamilyByIso.json'
|
||
```
|
||
|
||
Prefer subpath imports over the thin `@map` barrel (which exports `MAP_CORE_VERSION` only) so
|
||
tree-shaking works. The per-file catalog in [architecture/file-map.md](./architecture/file-map.md) is the
|
||
authoritative menu — every `library/` file listed is importable this way. A generated one-line import index can also be produced with
|
||
`node internal/generate-library-index.mjs`.
|
||
|
||
---
|
||
|
||
## Going deeper — the architecture folder
|
||
|
||
Simple lookups live here; the full contracts, catalogs, and wiring detail live in
|
||
[`architecture/`](./architecture/):
|
||
|
||
| Doc | What it holds |
|
||
|---|---|
|
||
| [file-map.md](./architecture/file-map.md) | every file, one line each, with the gotcha each carries |
|
||
| [data-sources.md](./architecture/data-sources.md) | the `api/` seam: connection contract + checklist |
|
||
| [color-strategies.md](./architecture/color-strategies.md) | the strategy contract + legend wiring |
|
||
| [components.md](./architecture/components.md) | shared component catalog + conventions |
|
||
| [i18n.md](./architecture/i18n.md) | the locale contract + add-a-language checklist |
|
||
| [embeddability.md](./architecture/embeddability.md) | generated per-map pages + manifest — embeddable by default |
|
||
| [app-wrappers.md](./architecture/app-wrappers.md) | the SPA shell: `nav.js` + `PROFILES` contracts |
|
||
| [poster-generation.md](./architecture/poster-generation.md) | the poster/preview generator pipeline |
|
||
| [patterns-and-provenance.md](./architecture/patterns-and-provenance.md) | the two multi-map patterns + provenance |
|
||
|
||
The **Semantic Tree Legend** component (a reusable hierarchical legend) is documented in
|
||
[file-map.md](./architecture/file-map.md#the-semantic-tree-legend-mapcomponentslegendssemantictreelegendvue).
|
||
|
||
---
|
||
|
||
## Quick reference — "I want to…"
|
||
|
||
| I want to… | Do this |
|
||
|---|---|
|
||
| Add a new map | copy `plugin-designs/template-bundle/` → rename folder + tag in `index.js` |
|
||
| Add a screen to an existing bundle | drop a `.vue` flat in that bundle's folder, request it via `profile-config.profile` |
|
||
| Fix a legend / control for ALL maps | edit it once in `library/` (`@map`); rebuild bundles |
|
||
| Add a color strategy | new file in `@map/colors/` (shared) or the bundle's `src/colors/` (map-private) |
|
||
| Build everything | `bun run build` → `public/doxa-maps/<bundle>/<bundle>.js` |
|
||
| Build just one | `BUNDLE=<name> bun run build:bundle` |
|
||
| Preview in isolation | `bun run dev` → `http://localhost:5173/plugin-designs/<name>/index.html` |
|
||
| See a change on the served site | `bun run build`, then refresh (NOT `dev`) |
|
||
| Repoint the API without a rebuild | set `window.MAP_APP_API_URL` before the bundle loads |
|
||
| Repoint the Mapbox token | pass `tk` in `profile-config`, or set `window.MAP_TOKEN_URL` / build-config `tokenUrl` |
|
||
|