doxa-maps/documentation/reference.md
doxa-maps ec93f8aa1a
Some checks are pending
build-gate / build (push) Waiting to run
Gate: every declared dependency must be a real package name
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).
2026-09-22 06:49:21 +00:00

42 KiB
Raw Blame History

Reference

Look-up tables and contracts for doxa-maps. For learning, see the tutorial; for tasks, how-to.md; for the why, why.md; for the short "what it is", 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 — 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.

<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, sointernal/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.

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.

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 -rfs 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.

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" |

# 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.


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 · two homes in depth: ../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:

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 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/:

Doc What it holds
file-map.md every file, one line each, with the gotcha each carries
data-sources.md the api/ seam: connection contract + checklist
color-strategies.md the strategy contract + legend wiring
components.md shared component catalog + conventions
i18n.md the locale contract + add-a-language checklist
embeddability.md generated per-map pages + manifest — embeddable by default
app-wrappers.md the SPA shell: nav.js + PROFILES contracts
poster-generation.md the poster/preview generator pipeline
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.


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