README: lead with the objectives, showcase the Research Gallery, teach the agent swarm
Some checks are pending
build-gate / build (push) Waiting to run

- Opens with what dx-maps is for: seven objectives, each paired with the gate
  or section that proves it is met.
- New Research Gallery section: the cap-5 app shell and the tree of cap-4..1
  plugins it nests, each still independently embeddable.
- Plugin Studio: clone main (the old branch was 198 commits behind), a command
  table, and a fresh studio screenshot (the old one showed a retired nav and
  tofu icons).
- Agent swarm: renamed to industry terms (trunk-based development, merge queue,
  maintainer), a laptop recipe (worktrees, strict ports, one builder, flock as
  the queue), and a redrawn diagram whose inner ring is an actual queue with
  one gate. SVG source committed next to the PNG.
- MapLibre is now described as it is: a one-prop, token-free engine.
- Removed internal credential/push instructions (AGENT-BRIEF.md covers agents).
- documentation/: fixed 19 dead links left by the docs/ move, removed internal
  references, and listed the working docs in the index.
- readme-shots: ONLY=<name> retakes a single image.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 18:34:02 +00:00
parent 3fd1fbcaea
commit c46a9a04bd
12 changed files with 336 additions and 208 deletions

421
README.md
View file

@ -1,4 +1,4 @@
# doxa-maps — one build, maps that embed everywhere
# dx-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.
@ -18,24 +18,99 @@
> (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.
script-embed it, wrap it as a mobile app, or open every map at once as one app.
---
## 1. The 5 plugin capabilities — most advanced first
## What dx-maps is for — and how each objective is met
Maps multiply a prayer movement only when they can live on *every* partner's site, not just one.
dx-maps exists to make that cheap. It set out to meet seven objectives; each one is met by something
you can open or run, not by a claim.
| # | Objective | How it is met | See it / check it |
|---|-----------|---------------|-------------------|
| 1 | **One build → one folder.** Seven maps that were seven codebases become one repo and one build. | `bun run build` writes a single drop-anywhere tree, `public/doxa-maps/`, with a `manifest.json` describing every bundle. | `bun run check:drop` · `bun run check:settled` |
| 2 | **Every map is embeddable.** Any map, on any site, with a copy-paste snippet. | Each bundle is one custom element; `manifest.json` ships an iframe snippet and an SRI-pinned `<script>` snippet for it. | [Deployment methods](#3-the-5-deployment-methods) · `bun run check:embed-targets` |
| 3 | **Every map is interoperable.** Maps talk to each other through props and events. | Composites wire their children with props, events and shared layouts — no copy-pasted code. | `bun run verify:interop` |
| 4 | **One app nests every map — and each stays independently embeddable.** | The **Research Gallery** is a single capability-5 plugin that nests all seven production maps. | [The Research Gallery](#1-the-research-gallery--five-capabilities-in-one-plugin) · `bun run check:composite-reg` |
| 5 | **5 capabilities × 5 deployment methods.** Anything you build, at any tier, ships everywhere. | A plugin's tier is read from its files; every tier builds to the same tree, and the tree feeds all five methods. | [Capabilities](#2-the-5-plugin-capabilities--most-advanced-first) · `bun run verify:5-caps` |
| 6 | **A clean shared/private boundary.** Reuse without breaking each other. | `library/` holds shared, parameterized pieces (generated index); anything map-specific stays in its own `plugin-designs/` folder. | [reference-library-index.md](./documentation/reference-library-index.md) · `bun run check:library-index` |
| 7 | **Many builders, one green `main`.** The repo is built to be worked by a swarm of agents at once. | One agent per plugin, one worktree each, and every change landed through a merge queue. | [Build it with an agent swarm](#5-build-it-with-an-agent-swarm) |
Plus one practical objective: **a token-free option.** Mapbox is the default engine; setting a single
prop, `provider: "maplibre"`, swaps in MapLibre so a partner running many embeds needs no Mapbox key.
`bun run check:provider-switch` proves the switch really changes the engine.
## Contents
1. **[The Research Gallery](#1-the-research-gallery--five-capabilities-in-one-plugin)** — the showcase: five capabilities in one plugin.
2. **[The 5 plugin capabilities](#2-the-5-plugin-capabilities--most-advanced-first)** — what you can build, most advanced first.
3. **[The 5 deployment methods](#3-the-5-deployment-methods)** — where a plugin can run.
4. **[The Plugin Studio](#4-the-plugin-studio)** — ★ where you *work*, and every command to get it running. Start here if you are new.
5. **[Build it with an agent swarm](#5-build-it-with-an-agent-swarm)** — how to run many agents on one laptop without collisions.
---
## 1. The Research Gallery — five capabilities in one plugin
The Research Gallery (`plugin-designs/doxa-maps-spa/`) is the product a user opens. It is **one
capability-5 plugin** — one custom element, `<doxa-maps-spa>` — and every tab inside it is a
**real, independently embeddable plugin** from a lower tier. Nothing is copied into it and nothing is
framed: each tab loads its own bundle and mounts that bundle's own custom element.
![Capability 5 — the Research Gallery app shell (doxa-maps-spa)](documentation/readme-images/cap5-doxa-maps-spa.png)
This is how five different capabilities compose into one:
```text
Research Gallery <doxa-maps-spa> capability 5 — app shell
│
├─ Dashboards
│ ├─ Big Picture dashboard-plus-grid capability 4 — composite
│ │ ├─ research dashboard research-dashboard capability 3 — monolithic
│ │ └─ people-groups grid people-groups-grid capability 2 — prop-table
│ ├─ Simple Research research-map-plus-grid capability 4 — composite
│ │ ├─ research map doxa-research-map capability 3 — monolithic
│ │ └─ people-groups grid people-groups-grid capability 2 — prop-table
│ └─ Regions regions-mfe capability 4 — composite
│ ├─ regions list regions-list capability 1 — single
│ └─ region dashboards region-dashboards capability 1 — single
├─ Prayer Mobilization
│ ├─ UPG-100 List upg-100-mfe capability 4 — composite
│ │ ├─ list upg-100-list capability 1 — single
│ │ └─ poster wall poster-wall capability 1 — single
│ └─ Prayer Progress doxa-simple-map (prayer) capability 2 — prop-table preset
├─ Gospel Research
│ ├─ Cities with Internet connectivity-map capability 1 — single
│ └─ Gospel Resources resources-map capability 1 — single
└─ Experimental (folder) every other bundle, discovered automatically from manifest.json
```
What that buys you:
- **Seven production maps, one URL.** The three sections above are the seven curated tabs.
- **Every tab is still a standalone embed.** Want only the UPG-100 list on a partner site? Embed
`upg-100-mfe` — or just its `upg-100-list` leaf. Taking the whole app apart costs nothing.
- **Proving the whole proves the parts.** When the Research Gallery was embedded cross-origin on a
foreign host page (see [deployment](#3-the-5-deployment-methods)), every nested plugin rode along.
- **New plugins appear on their own.** Any bundle not placed in a named section lands in
*Experimental*, read from the built `manifest.json`. A new folder in `plugin-designs/` shows up with
zero edits to the shell.
The tab order and grouping live in one place, `NAV_GROUPS` in
[`doxa-maps-spa.vue`](./plugin-designs/doxa-maps-spa/doxa-maps-spa.vue); `bun run check:spa-tab-order`
guards it.
---
## 2. 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
the most advanced thing this repo produces (an 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 |
@ -52,20 +127,15 @@ embeddable. Every screenshot is a real bundle from this repo's build output.
> **[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)
one: **`doxa-maps-spa` (the Research Gallery, [section 1](#1-the-research-gallery--five-capabilities-in-one-plugin))
is what an end user gets; `plugin-studio` is what a developer works in.**
*`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).
studio without any wiring. This is [section 4](#4-the-plugin-studio).
![Capability 5 — the developer studio (plugin-studio)](documentation/readme-images/cap5-plugin-studio.png)
@ -104,7 +174,7 @@ group on one map.
---
## 2. The 5 deployment methods
## 3. 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.
@ -119,12 +189,12 @@ nothing below is upgraded past what the files support.
**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.
Android SDK for method 2, a live WordPress for method 4, and a GUI 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):
Both were **certified cross-origin on 2026-09-24** — the entire Research Gallery, 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 |
|----------------------------------|-------------------------------------|
@ -139,8 +209,8 @@ The full per-method command matrix, with every prerequisite, is in
### 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.
The maps use Mapbox by default, 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 |
|---|---|
@ -153,8 +223,7 @@ 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:
To use your own token in dev, create a `.env` — it is **gitignored**, so it never reaches the repo:
```bash
printf 'VITE_MAPBOX_TOKEN=%s\n' 'pk.your_token_here' > .env
@ -163,39 +232,16 @@ 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).
**Or skip the token entirely.** A public `pk.` token can be restricted to specific URLs in the Mapbox
dashboard, but a JAMstack embed still has nowhere to hide one. So the engine is one prop: set
`provider: "maplibre"` on an instance (or add `?provider=maplibre` to a leaf page) and the map renders
on MapLibre with no Mapbox key. `plugin-designs/doxa-research-map/research-map.instances.json` ships a
ready-made MapLibre instance. The trade-off is deliberate: under MapLibre the place-search bar is
hidden, because the geocoder would still bill the Mapbox key the instance exists to avoid.
---
## 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
## 4. The Plugin Studio
★ **This is where you work — and the keyword is INTEROPERABILITY.**
@ -206,27 +252,22 @@ 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.
SPA; group those and you get a cap-5 app shell. That is exactly how the Research Gallery was built.
### Get it running
Four steps from nothing to every map in one place.
Four steps from nothing to every map in one place. You need **git**, **[Bun](https://bun.sh)** and
**Node ≥ 18**.
```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
# 1. Clone (the repo root IS the studio — there is no subfolder to cd into)
git clone https://git.nuxtbase.app/doxa-life/doxa-maps.git
cd doxa-maps
# 2. Install dependencies (installs 206 packages; needs Node >= 18)
# 2. Install dependencies
bun install --frozen-lockfile
# 3. Optional — your own Mapbox token for dev (see section 2)
# 3. Optional — your own Mapbox token for dev (see section 3)
printf 'VITE_MAPBOX_TOKEN=%s\n' 'pk.your_token_here' > .env
# 4. Open the Plugin Studio
@ -239,16 +280,29 @@ spot. (If port 5173 is busy Vite picks another one and prints it; read the line
> **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.
### Every command you need
| Command | What it does |
|---|---|
| `bun run dev` | The Plugin Studio with hot reload, at http://localhost:5173 |
| `bun x vite --port 5299 --strictPort` | A dev server on your own port (see the swarm rules below) |
| `BUNDLE=<name> bun x vite build` | Build **one** bundle, without cleaning anyone else's output |
| `bun run build` | The full production tree in `public/doxa-maps/` (~4 minutes, takes a lock) |
| `bun run serve` | Serve the built tree at http://localhost:4173, the way production does |
| `bun run check:settled` | Exit 0 = the built tree is safe to measure or publish right now |
| `bun run gates:pre` / `bun run gates:post` | The static and post-build verification gates |
| `bun run verify:5-caps` · `bun run verify:interop` | Prove the five capabilities, and cap-4 interoperability, in a real browser |
| `bun run shots` | Regenerate the README screenshots |
The full list — every deployment command with its prerequisites — is in
[documentation/README-SECTION-3-cli-commands.md](./documentation/README-SECTION-3-cli-commands.md).
### Work with your agent
The studio's companion is your coding agent. The loop:
@ -270,62 +324,10 @@ Two shared layouts do the composing, both in `library/layouts/`:
- **`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:
The dev server is for working; **you do not need a build to see a change.** When you want the
deployable tree:
```bash
bun run build # ~4 minutes; writes public/doxa-maps/
@ -335,85 +337,131 @@ 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 lock protects a second WRITER. It does not protect a READER.** Every build begins by deleting
`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"). Run `bun run check:settled` before
any browser check, any publish, and any claim about what the tree contains, and **grade it by exit
code**: like every gate here, it prints explanatory prose after its verdict, so `| tail -1` will
mislead you. Do not substitute a `pgrep` check — a process list samples an instant, while the lock
knows the queue.
---
## The delegation model — one build, many agents, one main
## 5. Build it with an agent swarm
This repo is designed for **many builders working in parallel without stepping on each
other**. The model is a set of concentric rings:
This repo is designed for **many builders working in parallel without stepping on each other** — and
the builders can be coding agents on your own laptop. It is **trunk-based development with a merge
queue**, the same model as GitHub's merge queue, GitLab's merge trains and Rust's bors, arranged as
concentric rings:
![The delegation model: concentric rings from main outward to the shepherd](documentation/readme-images/delegation-model.png)
![The delegation model: agents in their own worktrees send changes through one merge queue into main](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.
- **`main` — the trunk.** The one shared branch. Everything converges here, and it is always green.
- **Merge queue.** Changes land **one at a time, in order.** Each change is rebased onto the latest
`main` — its author resolves any conflicts — and lands only if the gates pass. A change that fails
leaves the queue; the next one goes. Nothing writes to `main` except the queue.
- **Plugin Studio.** One repo and one shared `library/`, with every plugin visible in one place, so
each builder can see what already exists and reuse it instead of rebuilding it.
- **Agents.** **One agent per plugin, each in its own git worktree.** Each owns its plugin end to end;
its private components stay inside its own `plugin-designs/` folder.
- **Maintainer.** One reviewer — human or agent — keeps everyone honoring the model: plugins built in
the studio, shared components genuinely shared, and every change going through the queue.
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.**
The effect is cooperative: every agent races to land working features, but the only way to land is
through the queue, one change at a time, **leaving `main` green.**
The diagram's source is [`documentation/readme-images/src/delegation-model.svg`](./documentation/readme-images/src/delegation-model.svg).
### Spin one up on your laptop
**1. One worktree per agent**, as sibling folders, so no two agents share a working directory:
```bash
git worktree add -b regions/work ../dx-maps-regions main
git worktree add -b upg100/work ../dx-maps-upg100 main
git worktree add -b maintainer/work ../dx-maps-maintainer main
git worktree list
```
**2. One agent per worktree, each owning one plugin.** Start your coding agent in each folder and give
it a single `plugin-designs/<name>/` folder to own. The maintainer gets no plugin; it reviews what
lands and pushes back when shared code is duplicated or a plugin reaches into another's folder.
**3. One dev server per agent, each on its own port, always with `--strictPort`:**
```bash
bun install --frozen-lockfile # once per worktree
bun x vite --port 5301 --strictPort # regions agent
bun x vite --port 5302 --strictPort # upg100 agent
```
Without `--strictPort`, Vite silently moves a busy port to the next one — and several gates have
`5173` written into them, so a drifted server leaves those gates reporting a confident wrong answer
about somebody else's page. With `--strictPort` the duplicate exits non-zero instead.
**4. One owner for the full build.** Agents edit source and check their own bundle with
`BUNDLE=<name> bun x vite build`. Only one agent runs `bun run build`, because every full build wipes
the shared output tree first.
**5. Land through the merge queue.** On one machine, a file lock is a complete merge queue — agents
wait their turn on it (`flock` ships with Linux; on macOS, `brew install flock`):
```bash
flock /tmp/dx-maps-main.lock sh -c '
git fetch origin &&
git rebase origin/main &&
bun run gates:pre &&
git push origin HEAD:main
'
```
The lock guarantees one landing at a time; the rebase makes the lander resolve every conflict; the
gates keep `main` green. When several machines share one remote, use the hosting platform's merge
queue instead (GitHub merge queue, GitLab merge trains), or protect `main` so only one builder at a
time may push.
Agents joining this repo should also read [AGENT-BRIEF.md](./AGENT-BRIEF.md).
---
## Repository layout
Two directories matter to a plugin author:
```text
doxa-maps/
├── plugin-designs/ YOUR maps live here — one folder per plugin (the FREE zone).
│ ├── doxa-maps-spa/ the Research Gallery, capability 5 (nests the production maps)
│ ├── plugin-studio/ the developer studio, capability 5 (nests every plugin)
│ ├── dashboard-plus-grid/ capability-4 composite: research dashboard + people-groups grid
│ ├── my-upg-100-list/ capability-4: UPG-100 list + poster wall
│ ├── 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.
│ ├── layouts/ VerticalTabsLayout, StackLayout — how composites compose
│ ├── components/ map controls, legends, search, poster, detail panels
│ ├── composables/ map instance, shadow styles, print-in-place, retry
│ └── styles/ colors/ data/ style registry, palettes, lookup tables
├── data/ the public datasets the maps ship with
├── deployment/ Capacitor (mobile), WordPress plugin, Tauri (desktop)
├── 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 and embedded.
```
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.
parameterized, so other maps can use it. Keep map-specific, non-parameterized code in your own
plugin-design. See [documentation/contributing/](./documentation/contributing/).
---
## More docs
Start at **[documentation/README.md](./documentation/README.md)** — the index of everything below.
| 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 |
@ -429,4 +477,5 @@ non-parameterized code in your own plugin-design.
> **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.
> build. The Plugin Studio is where you compose them into something bigger; the Research Gallery is
> what that looks like when you do.

View file

@ -1,8 +1,8 @@
# Contributing to doxa-maps
Everything a contributor needs, short. Recipes live in [documentation/how-to.md](./documentation/how-to.md),
lookups in [documentation/reference.md](./documentation/reference.md), depth in
[documentation/architecture/](./documentation/architecture/).
Everything a contributor needs, short. Recipes live in [documentation/how-to.md](./how-to.md),
lookups in [documentation/reference.md](./reference.md), depth in
[documentation/architecture/](./architecture/).
## The mental model — three zones
@ -26,7 +26,7 @@ genuinely needs it. One fix in `library/` reaches every map on the next build.
Pick A when the maps differ by *configuration*, B when they differ by *layout/behavior*.
Full walkthrough + the migration story:
[documentation/architecture/patterns-and-provenance.md](./documentation/architecture/patterns-and-provenance.md).
[documentation/architecture/patterns-and-provenance.md](./architecture/patterns-and-provenance.md).
## The two build steps (never confuse them)
@ -45,7 +45,7 @@ into `public/` on save) · terminal 2 serves the host site / `public/`.
`public/` is the committed web-root — serve it from anywhere and every map is embeddable from
it (script tag or the generated per-map page). Same-origin vs external-CDN modes, `paths`,
`embedBaseUrl`: [documentation/how-to.md → "Where do the maps get served from?"](./documentation/how-to.md).
`embedBaseUrl`: [documentation/how-to.md → "Where do the maps get served from?"](./how-to.md).
## Ground rules

View file

@ -274,5 +274,5 @@ this box, so `"all"` would fail two of three formats at the very end of a ~6-min
Still **not "proven"** — say *built, not shot*. The GUI launch is unverified because this VPS is
headless: the binary panics in `tao` with "Failed to initialize GTK" (no display, not a defect) and
no Xvfb is installed. See [`deployment/src-tauri/README.md`](../src-tauri/README.md) for placement rules, the
no Xvfb is installed. See [`deployment/src-tauri/README.md`](../deployment/src-tauri/README.md) for placement rules, the
token/CORS caveat and the full artifact table.

View file

@ -26,7 +26,7 @@ the goal is never one trained person, but trainers who train trainers. This READ
pass it on with the same button — **[the Playbook](#the-playbook--start-a-prayer-movement-with-this-map)**.
Everything below is for the people who build the maps. If you only want to *run the campaign*, jump to the
Playbook; the full walkthrough is [documentation/tutorial.md](./documentation/tutorial.md).
Playbook; the full walkthrough is [documentation/tutorial.md](./tutorial.md).
---
@ -275,11 +275,11 @@ Your map data has to come from somewhere. Common sources:
| Read this | When |
|---|---|
| **[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 (great to hand to your agent) |
| **[documentation/how-to.md](./documentation/how-to.md)** | task recipes (embed, data, mobile) |
| **[documentation/why.md](./why.md)** | 2 min — the problem, the idea, one diagram |
| **[documentation/tutorial.md](./tutorial.md)** | build your first map, step by step (great to hand to your agent) |
| **[documentation/how-to.md](./how-to.md)** | task recipes (embed, data, mobile) |
| **[library/layouts/README.md](../library/layouts/README.md)** | the SPA composition machinery (many maps → one app) |
| **[documentation/architecture/](./documentation/architecture/)** | the deep end — embeddability, app-wrappers, file map |
| **[documentation/architecture/](./architecture/)** | the deep end — embeddability, app-wrappers, file map |
---

View file

@ -15,5 +15,13 @@
- **[architecture/](./architecture/)** — the deep end: full contracts, the per-file catalog.
- **[README-embed-example.md](./README-embed-example.md)** — the drop-in README used when this bundler is embedded inside a host repo.
Working on the repo:
- **[develop-a-plugin-and-see-it-live.md](./develop-a-plugin-and-see-it-live.md)** — ★ the golden path: zero to a plugin live, in 6 steps.
- **[README-SECTION-3-cli-commands.md](./README-SECTION-3-cli-commands.md)** — every command, per deployment method, with prerequisites.
- **[reference-capability-exemplars.md](./reference-capability-exemplars.md)** — which bundle is at which capability tier (generated).
- **[reference-library-index.md](./reference-library-index.md)** — the reusable `library/` pieces you can import (generated).
- **[CONTRIBUTING.md](./CONTRIBUTING.md)** and **[contributing/](./contributing/)** — how to contribute, and the bar for `library/`.
Nothing on the simple path is longer than a few minutes; everything complex lives in
`architecture/`, linked from where you'd need it.

View file

@ -14,17 +14,17 @@ being duplicated.
"**Local by default, promote deliberately.**" When in doubt, keep your change local to your
own map; something graduates into the shared library only once a *second* map genuinely
needs it. Full reasoning — the promotion / orphan-audit rule — is in
[`../documentation/explanation.md`](../documentation/explanation.md).
[`../documentation/explanation.md`](../explanation.md).
## Start here
| You want to… | Read |
|---|---|
| Understand the whole picture (one library, many bundles, no rebuild) | [`../README.md`](../README.md) · [`../documentation/explanation.md`](../documentation/explanation.md) |
| Learn step-by-step with no jargon (non-coders welcome) | [`../documentation/tutorial.md`](../documentation/tutorial.md) |
| Do a task (add a map, embed, change colors, pick a CDN) | [`../documentation/how-to.md`](../documentation/how-to.md) |
| Look something up (file map, config fields, library index) | [`../documentation/reference.md`](../documentation/reference.md) |
| Understand build-vs-dev + partner-site embedding | [`../documentation/CONTRIBUTING.md`](../documentation/CONTRIBUTING.md) |
| Understand the whole picture (one library, many bundles, no rebuild) | [`../README.md`](../README.md) · [`../documentation/explanation.md`](../explanation.md) |
| Learn step-by-step with no jargon (non-coders welcome) | [`../documentation/tutorial.md`](../tutorial.md) |
| Do a task (add a map, embed, change colors, pick a CDN) | [`../documentation/how-to.md`](../how-to.md) |
| Look something up (file map, config fields, library index) | [`../documentation/reference.md`](../reference.md) |
| Understand build-vs-dev + partner-site embedding | [`../documentation/CONTRIBUTING.md`](../CONTRIBUTING.md) |
| Edit or add colors (the two places a color can live) | [`COLORS.md`](./COLORS.md) |
## Colors live here
@ -33,4 +33,4 @@ needs it. Full reasoning — the promotion / orphan-audit rule — is in
can live** — the shared `library/colors/` set vs your bundle's private
`plugin-designs/<bundle>/src/colors/` — and how the filename decides override-vs-add. The
strategy **contract** (the required `export default` shape) is in
[`../library/colors/README.md`](../library/colors/README.md).
[`../library/colors/README.md`](../../library/colors/README.md).

View file

@ -110,13 +110,13 @@ MAPBOX_TOKEN=$MAPBOX_TOKEN node internal/embed-smoke.mjs --grep <your-plugin>
> **Do not fetch the token from `https://doxa.life/api/maps/token` here — measured 2026-09-23, it does
> not work for this.** That endpoint serves a token URL-restricted to `doxa.life`, so a tile request
> without that Referer returns **403** and every basemap in your smoke test renders blank while the pins
> draw fine. That is the exact symptom the Driver reported as *"it's loading the pins, but it's not
> draw fine. That is the exact symptom first reported as *"it's loading the pins, but it's not
> loading the actual underlying map"*, and it is why `tokenUrl` was emptied in the build config on
> 2026-09-22. This doc used to hand you that command, so an inheritor's first run reproduced the bug.
>
> The `token` field in `vite.doxa-maps-build-config.json` is the one that works: measured, the config
> token returns **200** for a tile request with no Referer while the endpoint token returns **403**. It
> is a `pk.` public key, baked deliberately (Driver ruling 2026-09-21) so a map works on any host —
> is a `pk.` public key, baked deliberately (a project decision, 2026-09-21) so a map works on any host —
> protected by URL restrictions rather than secrecy. `check:public-hygiene` allows `pk.` and fails the
> build on `sk.`/`tk.`, so this is safe to read and never a secret to guard.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 KiB

After

Width:  |  Height:  |  Size: 1,015 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 228 KiB

After

Width:  |  Height:  |  Size: 175 KiB

View file

@ -0,0 +1,68 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1010" viewBox="0 0 1200 1010" font-family="Inter, 'Helvetica Neue', Arial, sans-serif">
<defs>
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#64748b"/></marker>
<marker id="arM" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#b91c1c"/></marker>
<radialGradient id="gM" cx="45%" cy="40%"><stop offset="0" stop-color="#16a34a"/><stop offset="1" stop-color="#166534"/></radialGradient>
</defs>
<rect width="1200" height="1010" fill="#ffffff"/>
<circle cx="600" cy="475" r="450" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2.5"/>
<circle cx="600" cy="475" r="365" fill="#eff6ff" stroke="#2563eb" stroke-width="2.5"/>
<circle cx="600" cy="475" r="262" fill="#fffbeb" stroke="#d97706" stroke-width="2.5"/>
<circle cx="600" cy="475" r="168" fill="#fef2f2" stroke="#dc2626" stroke-width="2.5" stroke-dasharray="8 6"/>
<rect x="578.0" y="297.0" width="44" height="20" fill="#fef2f2"/>
<line x1="578.0" y1="291.0" x2="578.0" y2="323.0" stroke="#dc2626" stroke-width="5" stroke-linecap="round"/>
<line x1="622.0" y1="291.0" x2="622.0" y2="323.0" stroke="#dc2626" stroke-width="5" stroke-linecap="round"/>
<text x="600.0" y="283.0" text-anchor="middle" font-size="13" font-weight="700" fill="#b91c1c">ONE GATE</text>
<line x1="600.0" y1="319.0" x2="600" y2="389" stroke="#b91c1c" stroke-width="3" marker-end="url(#arM)"/>
<text x="610" y="355.0" font-size="12" font-weight="700" fill="#b91c1c">merge</text>
<rect x="515.7941844763895" y="305.1584883484701" width="48" height="26" rx="13" fill="#ffffff" stroke="#dc2626" stroke-width="2"/>
<text x="539.7941844763895" y="323.1584883484701" text-anchor="middle" font-size="13" font-weight="700" fill="#b91c1c">#1</text>
<rect x="465.7820831295948" y="335.2087905225743" width="48" height="26" rx="13" fill="#ffffff" stroke="#dc2626" stroke-width="2"/>
<text x="489.7820831295948" y="353.2087905225743" text-anchor="middle" font-size="13" font-weight="700" fill="#b91c1c">#2</text>
<rect x="429.0638892005815" y="380.55198379861537" width="48" height="26" rx="13" fill="#ffffff" stroke="#dc2626" stroke-width="2"/>
<text x="453.0638892005815" y="398.55198379861537" text-anchor="middle" font-size="13" font-weight="700" fill="#b91c1c">#3</text>
<text x="431.7820831295948" y="328.2087905225743" text-anchor="middle" font-size="12" fill="#b91c1c" font-style="italic">waiting in line</text>
<text x="600" y="581" text-anchor="middle" font-size="16" font-weight="800" fill="#b91c1c">MERGE QUEUE</text>
<text x="600" y="599" text-anchor="middle" font-size="12" fill="#7f1d1d">one change at a time · rebased on main</text>
<text x="600" y="614" text-anchor="middle" font-size="12" fill="#7f1d1d">lands only if the gates pass</text>
<circle cx="600" cy="475" r="82" fill="url(#gM)" stroke="#14532d" stroke-width="2"/>
<text x="600" y="473" text-anchor="middle" font-size="30" font-weight="800" fill="#ffffff">main</text>
<text x="600" y="497" text-anchor="middle" font-size="13" fill="#dcfce7">trunk · always green</text>
<rect x="405.70000000000005" y="683.0" width="388.59999999999997" height="30" rx="15" fill="#ffffff" stroke="#d97706" stroke-width="1.5"/>
<text x="600" y="703.0" text-anchor="middle" font-size="14" font-weight="700" fill="#92400e">PLUGIN STUDIO · one repo, shared library/</text>
<rect x="371.3" y="777.5" width="457.4" height="30" rx="15" fill="#ffffff" stroke="#2563eb" stroke-width="1.5"/>
<text x="600" y="797.5" text-anchor="middle" font-size="14" font-weight="700" fill="#1e40af">AGENTS · one per plugin, each in its own worktree</text>
<rect x="336.9" y="867.5" width="526.2" height="30" rx="15" fill="#ffffff" stroke="#7c3aed" stroke-width="1.5"/>
<text x="600" y="887.5" text-anchor="middle" font-size="14" font-weight="700" fill="#5b21b6">MAINTAINER · reviews what lands, keeps shared code shared</text>
<circle cx="834.6174775355869" cy="195.393778261573" r="58" fill="#ffffff" stroke="#2563eb" stroke-width="2"/>
<text x="834.6174775355869" y="189.393778261573" text-anchor="middle" font-size="14" font-weight="700" fill="#1e3a8a">Research</text>
<text x="834.6174775355869" y="206.393778261573" text-anchor="middle" font-size="14" font-weight="700" fill="#1e3a8a">Gallery</text>
<text x="834.6174775355869" y="224.393778261573" text-anchor="middle" font-size="10.5" fill="#64748b">agent + worktree</text>
<line x1="796.0502209543945" y1="241.3564448487117" x2="711.8450440854579" y2="341.7082668972978" stroke="#64748b" stroke-width="2" marker-end="url(#ar)"/>
<circle cx="965.0" cy="475.0" r="58" fill="#ffffff" stroke="#2563eb" stroke-width="2"/>
<text x="965.0" y="469.0" text-anchor="middle" font-size="14" font-weight="700" fill="#1e3a8a">Big</text>
<text x="965.0" y="486.0" text-anchor="middle" font-size="14" font-weight="700" fill="#1e3a8a">Picture</text>
<text x="965.0" y="504.0" text-anchor="middle" font-size="10.5" fill="#64748b">agent + worktree</text>
<line x1="905.0" y1="475.0" x2="774.0" y2="475.0" stroke="#64748b" stroke-width="2" marker-end="url(#ar)"/>
<circle cx="858.0939751330899" cy="733.0939751330898" r="58" fill="#ffffff" stroke="#2563eb" stroke-width="2"/>
<text x="858.0939751330899" y="736.0939751330898" text-anchor="middle" font-size="14" font-weight="700" fill="#1e3a8a">Regions</text>
<text x="858.0939751330899" y="762.0939751330898" text-anchor="middle" font-size="10.5" fill="#64748b">agent + worktree</text>
<line x1="815.667568261897" y1="690.667568261897" x2="723.0365799264592" y2="598.0365799264592" stroke="#64748b" stroke-width="2" marker-end="url(#ar)"/>
<circle cx="341.9060248669102" cy="733.0939751330899" r="58" fill="#ffffff" stroke="#2563eb" stroke-width="2"/>
<text x="341.9060248669102" y="727.0939751330899" text-anchor="middle" font-size="14" font-weight="700" fill="#1e3a8a">UPG-100</text>
<text x="341.9060248669102" y="744.0939751330899" text-anchor="middle" font-size="14" font-weight="700" fill="#1e3a8a">List</text>
<text x="341.9060248669102" y="762.0939751330899" text-anchor="middle" font-size="10.5" fill="#64748b">agent + worktree</text>
<line x1="384.332431738103" y1="690.667568261897" x2="476.96342007354076" y2="598.0365799264592" stroke="#64748b" stroke-width="2" marker-end="url(#ar)"/>
<circle cx="235.0" cy="475.00000000000006" r="58" fill="#ffffff" stroke="#2563eb" stroke-width="2"/>
<text x="235.0" y="469.00000000000006" text-anchor="middle" font-size="14" font-weight="700" fill="#1e3a8a">Gospel</text>
<text x="235.0" y="486.00000000000006" text-anchor="middle" font-size="14" font-weight="700" fill="#1e3a8a">Resources</text>
<text x="235.0" y="504.00000000000006" text-anchor="middle" font-size="10.5" fill="#64748b">agent + worktree</text>
<line x1="295.0" y1="475.00000000000006" x2="426.0" y2="475.0" stroke="#64748b" stroke-width="2" marker-end="url(#ar)"/>
<circle cx="320.39377826157306" cy="240.38252246441309" r="58" fill="#ffffff" stroke="#2563eb" stroke-width="2" stroke-dasharray="5 4"/>
<text x="320.39377826157306" y="234.38252246441309" text-anchor="middle" font-size="14" font-weight="700" fill="#1e3a8a">your next</text>
<text x="320.39377826157306" y="251.38252246441309" text-anchor="middle" font-size="14" font-weight="700" fill="#1e3a8a">plugin</text>
<text x="320.39377826157306" y="269.3825224644131" text-anchor="middle" font-size="10.5" fill="#64748b">agent + worktree</text>
<text x="600" y="25" dy="-0" />
<text x="600" y="968" text-anchor="middle" font-size="18" font-weight="800" fill="#0f172a">Many agents build in parallel. Changes land one at a time. main stays green.</text>
<text x="600" y="992" text-anchor="middle" font-size="13" fill="#475569">Each arrow is a change entering the merge queue; only the queue writes to main.</text>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

@ -344,7 +344,7 @@ export MAPBOX_TOKEN=$(python3 -c "import json;print(json.load(open('vite.doxa-ma
> diagnosed the blank basemap and then kept prescribing its cause.
>
> The `token` field in `vite.doxa-maps-build-config.json` returns **200** on the identical request
> from any origin. It is a `pk.` public key baked deliberately (Driver ruling 2026-09-21), protected
> from any origin. It is a `pk.` public key baked deliberately (a project decision, 2026-09-21), protected
> by URL restrictions rather than secrecy; `check:public-hygiene` allows `pk.` and fails the build on
> `sk.`/`tk.`. So the SHELL COMMANDS above — the ones that curl a token and hand it to a gate as
> `MAPBOX_TOKEN` — now work where they previously fetched a token that 403s.
@ -514,7 +514,7 @@ Country/region polygons are **not** a `sources.json` connection — they render
**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).
two homes in depth: [../contributing/COLORS.md](./contributing/COLORS.md).
---

View file

@ -86,6 +86,7 @@ const browser = await chromium.launch({ args: [
// ★ A SHOT THAT FAILS MUST NOT SILENTLY PRODUCE A README IMAGE. Collected and asserted at the end.
const failures = []
const ONLY = process.env.ONLY ? process.env.ONLY.split(',') : null
// mapbox canvas selector to wait on (leaf pages mount a custom element with a canvas)
const CANVAS = 'canvas'
@ -114,6 +115,8 @@ async function shot(name, path, {
// generalised: the predicate is "does this surface use iframes", not "did this shot fail".
mapboxReferer = true,
} = {}) {
// ONLY=<name>[,<name>] retakes just those shots, leaving every other image untouched.
if (ONLY && !ONLY.includes(name)) return
const ctx = await browser.newContext({ viewport: vp, deviceScaleFactor: dsf })
// ★ SEND THE REFERER THE TOKEN IS SCOPED TO, or every map ships a BLANK BASEMAP.
//