doxa-maps/README.md
doxa-maps-88655c70 fdaeb7b44b README: explain how many builders share one main
The repo is built by several people at once against a single branch,
and nothing in the README said so. The new section carries the
concentric-rings diagram and states the rule that makes it work: a
builder captures main before pushing, resolves every conflict while
holding it, then releases.

Placed before "Repository layout" — it answers the question a reader
has after seeing the studio and before reading a folder list.
2026-09-24 17:46:42 +00:00

432 lines
23 KiB
Markdown

# doxa-maps — one build, maps that embed everywhere
> **The map cruncher.** Many hand-built maps go in; one shared library, one repo and one build
> come out — and every map fans out to five deployment targets from the same artifacts.
>
> ```text
> map · map · map · map · map · map · map (7 production maps, once 7 codebases)
> \ \ | | | / /
> +----- plugin-designs/ ------+ one folder per plugin, free zone
> |
> library/ (@map) shared, parameterized, high-bar zone
> |
> ONE bun run build
> |
> doxa-maps/ (drop-anywhere tree: bundles + leaf pages + manifest.json)
> / | \ \ \
> iframe JS-embed mobile WordPress desktop
> (proven) (proven) (built) (built) (built)
> ```
**A Vite bundler that turns each map into a self-contained web component.** Write a map in
`plugin-designs/<your-map>/`, run one build, and get a folder you can drop on any host — iframe it,
script-embed it, wrap it as a mobile app, or open every map at once as one SPA.
This README has three parts, in the order you need them:
1. **[The 5 plugin capabilities](#1-the-5-plugin-capabilities)** — what you can build, simplest to richest.
2. **[The 5 deployment methods](#2-the-5-deployment-methods)** — where a plugin can run.
3. **[The Plugin Studio](#3-the-plugin-studio)** — ★ where you *work*. Start here if you are new.
---
## 1. The 5 plugin capabilities — most advanced first
A "capability" is **how much a plugin nests** — not how hard it is to use. The tour below starts from
the most advanced thing this repo produces (a whole app of apps) and walks down to the simplest
(one element), because the top of the ladder is the point: every rung below it stays independently
embeddable. Every screenshot is a real bundle from this repo's build output.
| # | Capability | What it is | Example bundle |
|---|------------|------------|----------------|
| 5 | **Nested-SPA / app shell** | Many SPAs nested into one deep-linkable app. | `doxa-maps-spa` |
| 4 | **Nested-element SPA** | A composite that nests *other plugins* and wires them with props/events. | `big-picture` |
| 3 | **Monolithic element** | One `root.vue` nests many views internally → one element. | `doxa-research-map` |
| 2 | **Prop-table element** | One `root.vue` + a preset table → many maps from one plugin. | `doxa-simple-map` |
| 1 | **Single element** | One folder → one bundle → one custom element. | `all-people-groups` |
> The tier is decided by the files, not by how simple a folder looks: an `*.instances.json` makes a
> bundle **cap-2** however light it is, and a `meta.json` `type` declares tiers 3/4/5. The authoritative,
> generated answer lives in
> **[documentation/reference-capability-exemplars.md](./documentation/reference-capability-exemplars.md)**
> — cite that file rather than remembering.
### Capability 5 — nested-SPA / app shell
Every plugin in one deep-linkable page. Two cap-5 apps ship, and the difference matters when you embed
one: **`doxa-maps-spa` is what an end user gets; `plugin-studio` is what a developer works in.**
*`doxa-maps-spa` — the curated product SPA*, with tabs grouped by audience.
![Capability 5 — the curated product SPA (doxa-maps-spa)](documentation/readme-images/cap5-doxa-maps-spa.png)
*`plugin-studio` — the developer studio*, grouping every built plugin by capability (5 → 1). It is
auto-generated: the build discovers every folder in `plugin-designs/`, so a new plugin appears in the
studio without any wiring. This is [section 3](#3-the-plugin-studio).
![Capability 5 — the developer studio (plugin-studio)](documentation/readme-images/cap5-plugin-studio.png)
### Capability 4 — nested-element SPA
A composite that nests **lower-tier plugins** and wires them with props and events. Each inner plugin
stays independently embeddable, and the composite is embeddable too. `plugin-designs/my-upg-100-list/`
ships three leaves:
| Composite (`upg-100-mfe`) | List (`upg-100-list`) | Poster wall (`poster-wall`) |
|---------------------------|-----------------------|-----------------------------|
| ![Capability 4 — the UPG-100 composite (my-upg-100-list)](documentation/readme-images/cap4-upg100-mfe.png) | ![UPG-100 list leaf](documentation/readme-images/cap4-upg100-list.png) | ![UPG-100 poster-wall leaf](documentation/readme-images/cap4-upg100-poster-wall.png) |
### Capability 3 — monolithic element
A whole multi-view map from one `root.vue`. The trade-off: you cannot embed just one of its tabs.
![Capability 3 — the monolithic research map (doxa-research-map)](documentation/readme-images/cap3-monolithic-research.png)
### Capability 2 — prop-table element
One `root.vue`, many maps. `plugin-designs/doxa-simple-map/` ships a preset prop-table
(`simple-map.instances.json`) that renders it three ways — each a standalone embeddable page from the
**same** code.
| Prayer | Engagement | Adoption |
|--------|------------|----------|
| ![Capability 2 — prayer preset (doxa-simple-map)](documentation/readme-images/cap2-prayer.png) | ![Capability 2 — engagement preset (doxa-simple-map)](documentation/readme-images/cap2-engagement.png) | ![Capability 2 — adoption preset (doxa-simple-map)](documentation/readme-images/cap2-adoption.png) |
### Capability 1 — single element
One folder, one `.vue`, one embeddable element. `plugin-designs/all-people-groups/` shows every people
group on one map.
![Capability 1 — a single-element plugin (all-people-groups)](documentation/readme-images/cap1-single-element.png)
---
## 2. The 5 deployment methods
Ordered easiest to hardest. **Status is stated honestly** — "proven" means it has been run and seen, and
nothing below is upgraded past what the files support.
| # | Method | Status | Command / install |
|---|--------|--------|-------------------|
| 1 | **iframe** on any site | ✅ **proven** | `bun run build`, then paste the `embed.iframe` snippet from `manifest.json` |
| 2 | **Mobile app** (Capacitor, Android) | 🛠️ built, not shot | `bun run check:mobile` (no toolchain) · `bun run phone` needs Android SDK + JDK 17 + `adb` |
| 3 | **JavaScript embed** (`<script src>` + SRI) | ✅ **proven** | `bun run build`, then paste the `embed.script` + `integrity` snippet |
| 4 | **WordPress plugin** (shortcode + block) | 🛠️ built, contract-proven, not shot | `bun run check:wp-embed`, then install `deployment/wordpress/doxa-maps-embed/` |
| 5 | **Desktop app** (Tauri) | 🛠️ built, not shot | `bun run check:tauri` (no toolchain) · `cargo tauri build` needs Rust + webview libs |
**What "not shot" means:** the code exists and its contract test passes, but nobody has captured it
running on a real device or install. Each is blocked by a missing local toolchain, not by missing code —
Android SDK for method 2, a live WordPress for method 4, and a GUI (this box is headless) for method 5.
Methods **1 and 3 are the two proven-live paths.** If you need a map on a site today, use one of those.
Both were **certified cross-origin on 2026-09-24** — the entire capability-5 app, embedded on a
foreign host page, every inner plugin riding along (prove-the-whole = prove-the-parts):
| Method 1 — iframe on a host site | Method 3 — JS embed on a host site |
|----------------------------------|-------------------------------------|
| ![The cap-5 app inside a host page iframe](documentation/readme-images/deploy1-iframe-hostsite.png) | ![The cap-5 app JS-embedded on a host page](documentation/readme-images/deploy3-js-embed-hostsite.png) |
> For a JS embed on another site the host page sets `window.MAP_TOKEN` (nested elements read it)
> and the serving layer must send `Access-Control-Allow-Origin: *` — the tree is fetched
> cross-origin by `manifest.json`-composed surfaces.
The full per-method command matrix, with every prerequisite, is in
**[documentation/README-SECTION-3-cli-commands.md](./documentation/README-SECTION-3-cli-commands.md)**.
### The Mapbox token (read this before you file a bug)
The maps use Mapbox, which needs a `pk.` token. **The token works differently in dev than in the built
tree**, and diagnosing one from the other is the most common way to lose an hour.
| Where | How it gets a token |
|---|---|
| **Dev server** (`bun run dev`) | Baked into the page for you — from `.env` (`VITE_MAPBOX_TOKEN`) if present, otherwise from `token` in `vite.doxa-maps-build-config.json`. |
| **Built pages** (`bun run serve`, or hosted) | **Not baked.** Each page resolves a token at runtime: `?tk=<TOKEN>` on the URL, a token you paste into the page's own input box (kept in `localStorage`), or a token endpoint (`tokenUrl`). |
So on a built leaf page you typically need:
```
http://localhost:4173/doxa-maps/demo-map/demo-map/index.html?tk=pk.your_token_here
```
To use your own token in dev, create a `.env` — it is **gitignored** (`.gitignore` lines 21-24), so it
never reaches the repo:
```bash
printf 'VITE_MAPBOX_TOKEN=%s\n' 'pk.your_token_here' > .env
```
**If a basemap is an empty grey void while the pins still draw, that is the token failing** — not a
broken app.
**Why this is genuinely awkward:** the five deployment methods each handle the token differently, and a
JAMstack embed has nowhere safe to keep a secret. The answer is not to hide the token but to make hiding
it unnecessary: a public `pk.` token is restricted to specific URLs in the Mapbox dashboard. **MapLibre
is being pursued to remove the per-embed token requirement entirely — that work is IN PROGRESS, not
done** (`maplibre-gl` is a dependency and the page generator has a MapLibre path, but no plugin-design
runs on it yet).
---
## The repo at a glance
```text
doxa-maps/
├── plugin-designs/ YOUR maps live here — one folder per plugin (the FREE zone).
│ ├── doxa-maps-spa/ the capability-5 app (nests everything below)
│ ├── dashboard-plus-grid/ capability-4 composite: dashboard + people-groups grid
│ ├── my-upg-100-list/ capability-4: UPG-100 lists + poster wall + map
│ ├── doxa-simple-map/ capability-2: one root.vue, three preset maps
│ └── template-bundle/ copy this to start a new plugin
├── library/ the SHARED zone (@map) — reusable, parameterized only.
│ │ A change here reaches EVERY bundle: high bar, see
│ │ documentation/contributing/.
│ ├── components/ map controls, legends, search, poster, detail panels
│ ├── composables/ useMapInstance, shadow styles, print-in-place, retry
│ └── styles/ colors/ data/ style registry, palettes, lookup tables
├── data/ the public datasets the maps ship with
├── documentation/ tutorial · how-to · reference · explanation + architecture
├── internal/ the build generator + the verification gates
└── public/doxa-maps/ the BUILD OUTPUT — the drop-anywhere tree (bundles,
leaf pages, manifest.json). What gets served + embedded.
```
## 3. The Plugin Studio
★ **This is where you work — and the keyword is INTEROPERABILITY.**
The Plugin Studio is one place to **view all of your plugins at once** and **edit all of them in one
place using your agent**. Every plugin appears in a single view, sorted **highest capability down to
lowest**, so you can see the whole catalogue instead of hunting through folders.
That sorting is the point. Once you can see every plugin side by side, you can tell your agent **which
ones to group together** — any number of them — and compose them into **higher-capability plugins**
using **props, events and layouts**. Group cap-1 and cap-2 elements and you get a cap-4 nested-element
SPA; group those and you get a cap-5 app shell. That is what makes the studio the right place to build
map plugins, rather than a gallery you merely look at.
**Why this matters.** Without a working studio, a developer *sees the fruit but not the deeper
strategy* — they get the maps, but they cannot reproduce or extend them. The studio is what turns a set
of finished maps into something you can build on.
### Get it running
Four steps from nothing to every map in one place.
```bash
# 1. Clone (the repo root IS the studio — there is no subfolder to cd into).
# This host is PRIVATE: it will prompt for your Forgejo username + token.
git clone --branch doxa-maps-plugin-studio https://git.nuxtbase.app/doxa-life/doxa-maps.git
cd doxa-maps
# 2. Install dependencies (installs 206 packages; needs Node >= 18)
bun install --frozen-lockfile
# 3. Optional — your own Mapbox token for dev (see section 2)
printf 'VITE_MAPBOX_TOKEN=%s\n' 'pk.your_token_here' > .env
# 4. Open the Plugin Studio
bun run dev
```
Then open **http://localhost:5173/** — that root URL *is* the Plugin Studio, with every plugin in one
spot. (If port 5173 is busy Vite picks another one and prints it; read the line it logs.)
> **You do not need to build first.** The build output in `public/` is committed, so a fresh clone has
> plugins to show the moment the dev server starts.
> **If step 1 fails with `remote: Unauthorized`,** the clone is not broken — `git.nuxtbase.app` is a
> private self-hosted Forgejo and anonymous cloning is refused. Ask for an account, then use your
> username plus a Forgejo **access token** (not your password) when git prompts.
Two things worth knowing:
- **`/` is the studio; `/__gallery` is the old staging gallery**, kept but demoted to an explicit URL.
- **Edits hot-reload.** The studio mounts the real `plugin-studio` custom element through its own
`index.js` — the same path the built bundle takes — so what you see is the real element, not a preview.
### Work with your agent
The studio's companion is your coding agent. The loop:
1. Open the studio (`bun run dev`) and look at the catalogue, sorted cap-5 → cap-1.
2. Tell your agent which plugins to **group** — e.g. *"combine the research map and the people-groups
grid into one composite."*
3. The agent writes a new folder in `plugin-designs/<name>/`, wiring the children with props, events and
a layout from `library/layouts/`.
4. Refresh the studio. The new, higher-capability plugin is there.
**The whole contract is: write your app inside `plugin-designs/<your-map>/`.** There is no registration
step and no per-plugin wiring — the build discovers every folder under `plugin-designs/`. Copy
`plugin-designs/template-bundle/` to start a new one.
Two shared layouts do the composing, both in `library/layouts/`:
- **`VerticalTabsLayout`** — one element visible at a time, lazy-mounted on first open.
- **`StackLayout`** — the opposite: elements stacked top-to-bottom at natural height, so the page
scrolls through them.
### Working fast: use the dev server, not the build
A full `bun run build` rebuilds every bundle and takes minutes. **You do not need it to see a
change.** The dev server hot-reloads:
```bash
bun run dev # http://localhost:5173
bun x vite --port 5299 --strictPort # any other port, when 5173 is taken
```
★ **If more than one of you is working in this repo at once, EVERY dev server must name its own
port, and must pass `--strictPort`.** Vite's default behaviour on a busy port is to silently move to
the next one — 5173 becomes 5174 — and five gates (`check-facet-changes-map`,
`plugin-gallery-smoke`, `smoke-studio`, `generate-gallery`, `check-scrim-covers-viewport`) have
`5173` written into them. A second server without `--strictPort` therefore leaves those gates
talking to somebody else's server, or to none, and reporting a confident wrong answer with no error.
With `--strictPort` the duplicate exits non-zero instead of drifting — verified: port taken → exit 1.
Reach for the full build only when you need the deployable tree, an SRI-consistent leaf page, or a
manifest. To check one bundle's compiled output without touching anyone else's:
```bash
BUNDLE=<name> bun x vite build # this bundle only; does NOT clean the tree
```
### Everything must reach Forgejo
**A local commit is invisible.** The Driver runs a laptop watcher against
`git.nuxtbase.app/doxa-life/doxa-maps` and sees changes in the browser from THAT repo — so work that
stops at `git commit` has not been delivered, no matter how well it was verified locally.
Push every increment:
```bash
TOKEN="$(pass show vivarium/forgejo-certified-sandbox | head -1)"
git -c http.postBuffer=524288000 \
push "https://oauth2:${TOKEN}@git.nuxtbase.app/doxa-life/doxa-maps.git" HEAD:doxa-maps-plugin-studio \
2>&1 | sed "s/${TOKEN}/[REDACTED]/g"
unset TOKEN
```
Never write the credential into `.git/config` (see the clone note above), always filter output
through `sed`, and prefer many small pushed commits over one large local one.
★ After pushing to an INLINE url, `git log @{u}..` still reports the commits as unpushed, because git
only moves a remote-tracking ref when it pushes to a NAMED remote. Sync it so the next reader is not
misled:
```bash
git fetch "https://oauth2:${TOKEN}@git.nuxtbase.app/doxa-life/doxa-maps.git" \
'refs/heads/doxa-maps-plugin-studio:refs/remotes/origin/doxa-maps-plugin-studio'
```
### The production build
The dev server is for working. When you want the deployable tree:
```bash
bun run build # ~4 minutes; writes public/doxa-maps/
bun run serve # serve the BUILT tree at http://localhost:4173
```
`bun run build` takes a **lock**: if another build is already running it fails fast rather than
corrupting the shared output. Set `BUILD_LOCK_WAIT=600` to queue behind it instead.
### Before you MEASURE the built tree — `bun run check:settled`
```bash
bun run check:settled # exit 0 = safe to measure/publish; exit 1 = do not
```
**The lock protects a second WRITER. It does not protect a READER.** Every build begins with
`bun run clean`, which `rm -rf`s `public/doxa-maps/`, so while a build runs the tree is transiently
near-empty — and anything reading it then gets a confident, wrong answer: "that bundle is missing",
"the dashboard tab does not exist", "461 files were deleted". Those are not hypotheticals; each has
happened here, and one nearly produced a bug report against a correct change.
So run this gate before any browser check, any publish, and any claim about what the tree contains.
It reports `builders`, `waiters`, `churn(45s)`, `dirs`, `leaves` and `quietFor`, and **grade it by
exit code** — like every gate here, it prints teaching prose after its verdict, so `| tail -1` will
mislead you.
★ **Do not substitute a `pgrep` check for this.** A process list samples an INSTANT; the lock knows
the QUEUE. Between one queued build finishing and the next acquiring the lock there is a real window
where zero build processes exist and the tree is nonetheless about to be wiped — and a long quiet
period makes that window look *more* convincing, not less, because the queue is idle precisely
because it is between runs. Measured 2026-09-23: `pgrep` reported 0 builders while the gate reported
`builders=1 waiters=1`, and `ps` confirmed a build had started 17 seconds earlier.
If you are one of several agents working this repo at once, the stronger rule is simply: **do not run
`bun run build` at all.** Edit source, and let one owner run the single authoritative build. To
sanity-check one bundle without touching anyone else's output, use
`BUNDLE=<name> bun x vite build` — it does not clean the whole tree.
---
## The delegation model — one build, many agents, one main
This repo is designed for **many builders working in parallel without stepping on each
other**. The model is a set of concentric rings:
![The delegation model: concentric rings from main outward to the shepherd](documentation/readme-images/delegation-model.png)
- **`main`** — the one shared branch. Everything converges here.
- **Capture ring** — before pushing to `main`, a builder must *capture* it: **one builder at
a time, first-come-first-serve.** Whoever holds it **resolves all merge conflicts**, lands
their work, then releases. The next builder waits and retries. No two pushes race.
- **Plugin Studio** — one repo, every plugin visible in one place, so each builder can see
what already exists and reuse it.
- **Plugins** — each is built in the shared bundler, so its shared, parameterized components
become available to every other plugin (private components stay inside each plugin's own
folder).
- **Agents** — **one builder per plugin.** Each owns their plugin end to end. A cluster of
experimental plugins is owned by a single roaming builder.
- **Shepherd** — one builder keeps everyone honoring the model: plugins built in the studio,
shared components truly shared, and every push serialized through the capture ring.
The effect is cooperative: each builder races to land the most working features, but the only
way to score is to pass through the capture ring one at a time and **leave `main` green.**
## Repository layout
Two directories matter to a plugin author:
```
plugin-designs/ ← YOUR FREE ZONE. One folder per plugin. Copy template-bundle/ to start.
library/ ← the SHARED toolkit (layouts, components, composables, colors, stores, i18n).
```
Everything else is supporting cast: `internal/` (build + gates), `public/` (committed build output),
`deployment/` (Capacitor, WordPress, Tauri), `documentation/`.
**`plugin-designs/` is a free zone.** Your plugin is its own application in its own folder, building to
its own output. Nothing you do there can break another author's map.
**`library/` is a HIGH BAR.** It is multi-tenant — reused by many maps and authors — so every edit is a
breaking-change risk to everyone. Only contribute here when a component is genuinely reusable *and*
parameterized, so other maps can use it. Never widen or casually add. Keep map-specific,
non-parameterized code in your own plugin-design.
---
## More docs
| Read this | When |
|---|---|
| **[documentation/develop-a-plugin-and-see-it-live.md](./documentation/develop-a-plugin-and-see-it-live.md)** | ★ the golden path — zero to a plugin live, in 6 steps |
| **[documentation/README-SECTION-3-cli-commands.md](./documentation/README-SECTION-3-cli-commands.md)** | the full per-method CLI + install matrix |
| **[documentation/reference-capability-exemplars.md](./documentation/reference-capability-exemplars.md)** | which bundle is the capability-N example (generated) |
| **[documentation/reference-library-index.md](./documentation/reference-library-index.md)** | the menu of reusable `library/` pieces you can import |
| **[documentation/why.md](./documentation/why.md)** | 2 min — the problem, the idea, one diagram |
| **[documentation/tutorial.md](./documentation/tutorial.md)** | build your first map, step by step |
| **[documentation/how-to.md](./documentation/how-to.md)** | task recipes (embed, data, mobile) |
| **[documentation/architecture/](./documentation/architecture/)** | the deep end — embeddability, app-wrappers, file map |
---
> **Bottom line: maps should be open source, run everywhere, and multiply.** This bundler makes each map
> a self-contained file that embeds on your site, your partners' sites, WordPress, and mobile — from one
> build. The Plugin Studio is where you compose them into something bigger.