doxa-maps/documentation/develop-a-plugin-and-see-it-live.md
doxa-maps-88655c70 c46a9a04bd
Some checks are pending
build-gate / build (push) Waiting to run
README: lead with the objectives, showcase the Research Gallery, teach the agent swarm
- 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.
2026-09-24 18:34:02 +00:00

247 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Develop a plugin and see it live — the golden path
> **The one thing that matters:** author a map plugin, run one build, and see it live on the platform.
> This page THREADS the existing docs into that read-order — it does not duplicate them. Follow the six
> steps; each links to the doc that teaches that piece in depth. Brand-neutral: any team on any platform
> inherits this same path.
## The path at a glance
```
author build publish see it live
plugin-designs/ → bun run build → merge to catalog → https://<platform>/plugin-assets/…
(one folder) (one step) (the runbook) (a tab, an iframe, a <script>)
```
Everything you build is a **compiled Vue custom element** — one folder → one bundle → one `<tag>`. A static
HTML page is never a plugin (it can only be iframed). See [reference.md](reference.md) for the capability model.
## Step 1 — Understand the 5 capabilities (30 sec)
A plugin is one of five tiers, simplest → most composable:
**single-element → prop-table → monolithic → nested-element SPA → nested-SPA (app-shell)**.
You declare the tier in `<bundle>.meta.json` (`{"type": …}`) for the composite tiers. Read
[reference.md](reference.md) §capabilities; imitate the nearest reference plugin in `plugin-designs/`.
## Step 2 — Author the plugin (the 4-file pattern)
In **`plugin-designs/<your-plugin>/`** create these files (imitate the nearest reference plugin, never author
a plugin as a page). **Three are required; the rest are opt-in:**
| File | Required? | What it is |
|---|---|---|
| `<your-plugin>.vue` | **required** | the root Vue SFC — your plugin's UI (styles are shadow-scoped automatically) |
| `index.js` | **required** | `defineCustomElement(RootVue)` → `customElements.define('<your-tag>', El)` — register ONLY your own tag |
| `index.html` | **required** | the staging entry page that mounts `<your-tag>` — the build fails without it |
| `<your-plugin>.meta.json` | composite tiers only | `{"type": "<machine key>", "composes": […], "label": "…"}` |
| `<your-plugin>.instances.json` | optional | preset instances — supplying one makes the bundle **prop-table** (cap-2) |
| `src/` | optional | private components used only by this plugin |
> **Measured 2026-09-21, so you can calibrate.** Across the 18 shipped bundles the folder holds **3 to 8
> files** — not one has "exactly four." `all-people-groups` is the true minimum at **3** (root `.vue` +
> `index.js` + `index.html`) and is a fully working, manifest-listed plugin. **9 of 18 carry no `.meta.json`
> at all**, because single and prop-table tiers are inferred rather than declared. Start at three and add a
> file only when you need what it does.
>
> `index.html` really is load-bearing: moving it out of `demo-map/` made the build error and dropped the
> bundle from the buildable list (18 → 17). Restoring it returned a clean build.
**`plugin-designs/` is your FREE ZONE** — build freely here. Only contribute a component to the shared
`library/` (`@map`) if it is genuinely **reusable + parameterized** — a library edit reaches every plugin, so
it's a HIGH BAR (see [reference.md](reference.md) §governance). Full walkthrough: [tutorial.md](tutorial.md).
## Step 3 — Build (one step)
From `doxa-maps-plugin-studio/`:
```sh
bun run build # discovers plugin-designs/*, bundles each, writes public/doxa-maps/<plugin>/<plugin>.js
```
Exit 0 + all DX guards PASS + `inline-parse-check` PASS = a good build. Your plugin now exists as a real
custom-element bundle under `public/doxa-maps/<your-plugin>/`, with a standalone leaf page and a manifest
entry. Recipes: [how-to.md](how-to.md).
## Step 3a — The FAST loop: see it in the studio (~5s, no build)
Before the four-minute build, iterate in the **plugin studio** — the single staging environment, where
every plugin appears in one place as a real mounted element:
```sh
bun run dev # vite on :5173; `/` mounts <plugin-studio>, one tab per profile
bun run smoke:studio # ~5s: asserts the studio mounts and every tab is a real subtree
```
Measured 2026-09-22: **PASS in 4.7s — 29 tabs, 0 failed requests**, against a full `bun run build` of
**43s–4min** on the same box (it runs 19 bundles sequentially and queues behind any other build, so
the spread is real). An order of magnitude either way: this is the loop to live in while authoring.
The build is for publishing, not for looking.
> **★ The studio catches a class the built tree structurally cannot.** The dev manifest hands
> **rooted** refs (`/plugin-designs/…`) while the built manifest hands **relative** ones
> (`./bundle/bundle.js`). A base-join bug is therefore invisible in one surface and fatal in the
> other — which is why both this step and the next exist, rather than one replacing the other.
There is exactly **one** staging environment. If you find yourself looking at a second index of
plugins, it is the archived gallery at `/__gallery` — kept because the dev server still imports its
renderer, not because it is a place to review work.
## Step 3b — Prove it MOUNTS in a browser (before you publish)
A green build is a **static** result: the guards check that files exist, parse, carry a correct SRI hash and
resolve. None of them opens your element. A bundle can pass every guard above and still render nothing —
a thrown `setup()` removes the subtree silently, and the build stays green.
> **This was measured, not assumed (2026-09-21).** A `throw` was injected into `template-map-a.vue`'s
> `setup()` and the bundle rebuilt. Result: the build **succeeded** (206 kB emitted, exit 0), `check:parse`
> **PASS**, `check:drop` **PASS** — and `smoke:embeds` reported **0/2 pages mount cleanly**. One deliberate
> defect, invisible to every static guard, caught only in a browser. That gap is the entire reason this
> step exists.
```sh
# A token is required or map profiles cannot mount. Take it from the build config, which is
# the one source that works from a local gate — see the note below before changing this.
MAPBOX_TOKEN=$(python3 -c "import json;print(json.load(open('vite.doxa-maps-build-config.json'))['token'])")
MAPBOX_TOKEN=$MAPBOX_TOKEN bun run smoke:embeds # every profile in the manifest
MAPBOX_TOKEN=$MAPBOX_TOKEN node internal/embed-smoke.mjs --grep <your-plugin> # just yours
```
> **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 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 (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.
`smoke:embeds` serves `public/` and opens **every bundle × profile × preconfigured instance** from
`manifest.json`, asserting each custom element defines, attaches a shadow root and fills it. Expect
`N/N pages mount cleanly`. Measured 2026-09-21: **40/40**.
> **The token is not optional, and its absence is loud.** Without `MAPBOX_TOKEN` every *map* profile fails
> its mount assertion while every *DOM* profile passes — 27/40, which reads like 13 broken plugins and is
> not. The run prints a two-line warning when the token is missing; believe the warning, not the failures.
> `?tk=` alone is also not enough for a composite: it reaches only the top-level element's `profile-config`,
> while **nested children read `window.MAP_TOKEN`**. The smoke sets both.
**This step is not in CI.** CI is the single authoritative builder but installs no browser binary (no
`playwright install` step), so the mount check is a LOCAL gate an author runs deliberately. That is exactly
why it belongs here in the golden path rather than being left to be remembered.
## Step 4 — Publish to the live catalog
The platform serves from the **catalog repo** (`platform/mfe-index`), NOT from where you develop. Publishing
= merging your built bundles into the catalog. **The tested procedure, in one block:**
```sh
set -e # ← FIRST LINE, not a style choice. Explained below; without it this block is unsafe.
# 0. PRECONDITION: never publish a tree a build is rewriting. `clean` empties public/doxa-maps at
# the START of every build, so a concurrent build makes the source momentarily EMPTY.
bun run check:settled
# 1. clone the catalog fresh (fetch-first, never force)
git clone --depth 1 <catalog-repo-url> /tmp/catalog
# 2. merge your built bundles into its doxa-maps subtree (--delete mirrors removals)
rsync -a --delete public/doxa-maps/ /tmp/catalog/app/doxa-maps/
# 3. VERIFY — these are commands, not a reminder. Under `set -e` a failure aborts before the push.
bun run check:sri # every leaf's baked integrity matches the bundle it loads
bun run check:embed-targets # every embed.iframe src in the manifest resolves to a real file
git -C /tmp/catalog diff --stat -- . ':(exclude)app/doxa-maps' | grep -q . \
&& { echo 'ABORT: diff touches files outside app/doxa-maps'; exit 1; } || true
# 4. commit (product-only message), then push with a RAISED BUFFER (see below):
git -C /tmp/catalog -c http.postBuffer=524288000 push origin HEAD:main
```
> **★ Why `set -e` is the first line, and why step 3 is commands rather than a comment.** Both halves
> of this once took the live catalog down with a 404. The verify was chained into a compound command,
> so a *failing* check still fell through to the push; and `rsync --delete` ran from a source a
> concurrent build had just emptied, which faithfully mirrored that emptiness into the catalog.
> `--delete` is what makes a mid-churn source catastrophic — it deletes the live tree to match your
> empty one. So the settled check is a precondition, not a courtesy, and a verify that cannot abort
> is not a verify.
> **★ `http.postBuffer=524288000` is load-bearing, not optional.** The tree is ~39 MB and git's 1 MB
> default cannot stream it. Without the flag the push fails with `The remote end hung up unexpectedly`
> — which reads as a SERVER rejection (permissions? branch protection? repo full?) and sends you
> debugging the wrong machine entirely. It is a local buffer limit. This one line is the difference
> between step 4 working and step 4 looking like an access problem.
The platform serves the catalog directly, so a pushed change is live at once — verify at the URL in Step 5.
(The fuller runbook with every gotcha lives in the project's deploy-cycle notes; the block above is the
load-bearing sequence.)
(The end-state is the Forgejo CI build-gate — push source, CI builds + publishes automatically. Until a runner
is registered, the runbook is the manual path. Same `build:unlocked` seam.)
## Step 5 — See it live (the 5 deployment methods)
Your one element deploys five ways (only an element supports all five — a page supports iframe only):
| # | Method | How | Status today |
|---|---|---|---|
| 1 | **iframe** | point an `<iframe>` at the plugin's leaf page | ✅ proven |
| 3 | **JavaScript embed** | `<script src=".../<plugin>.js">` + `<your-tag>` (SRI-hashed) | ✅ proven |
| 2 | **mobile** | `bun run phone` (Capacitor) — build step, no new code | 🔶 built, not shot |
| 4 | **WordPress plugin** | the `deployment/wordpress/doxa-maps-embed/` PHP wrapper emits the embed snippet | 🔶 built, not shot |
| 5 | **desktop (Tauri)** | `bun run tauri:build` → a real binary + `.deb` wrapping the same `public/` tree | 🔶 built, not shot |
Say "proven" ONLY for 1 & 3. For 2, 4 and 5 say "built, not shot" — each is scaffolded and
contract-checked, and what is missing differs per method: 2 has no APK (no Android SDK here), 4 has
no capture of a live WordPress install, and 5 **does** emit artifacts — a 67 MB binary and a 60 MB
`.deb` are on disk — but no window has been opened, because this box is headless. "Not shot" means
nobody has watched it run, which is not the same as "nothing was produced". Verify at
`https://<platform>/plugin-assets/doxa-maps/<plugin>/<plugin>/index.html`.
**Done = the live site shows it, not a green build.**
> **Check the path itself, not just your plugin.** The catalog ships a one-element self-test of
> everything above — open
> `https://<platform>/plugin-assets/doxa-maps/golden-path-check/golden-path-check/index.html`.
> It reads the manifest it was published beside and reports four things: how many bundles and
> profiles the catalog holds, how many profiles describe themselves, and whether it can find
> *itself* listed. If it answers, authoring → build → publish → serve all worked on this platform,
> which is a different question from whether *your* plugin did. Verified on the live catalog
> 2026-09-23: 19 bundles, 28 profiles, 28 of 28 self-describing, listed = yes, zero console errors.
>
> Read what it does **not** claim. Those are questions about the catalog's SHAPE, so it stays green
> even if a bundle's CONTENT is stale — freshness belongs to `bun run check:catalog-currency`, which
> compares every live bundle's bytes against your build. Run both: the self-test says the pipe
> works, the currency gate says what came through it is current.
## Step 6 — Surface it (make it discoverable)
A plugin reachable only by direct URL isn't really "seen live." Good news: **you usually don't have to do
anything.**
**It is already surfaced — automatically.** The cap-5 SPA reads the built `manifest.json` at runtime and
auto-discovers every bundle that is neither already placed in a curated section nor on the small
`NOT_EXPERIMENTAL` exclusion list (the two shells, the template, and bundles that are nested *inside* another
tab). Your plugin appears in the collapsible **Experimental** folder at the bottom of the rail, marked 🧪, with
its label and description read from the manifest. No `TAB_DEFS` entry, no `NAV_MODEL` row, no code edit.
> Verified 2026-09-21 in a browser: the rail rendered **11 rows — 6 curated, then 5 auto-discovered**
> (`all-people-groups`, `country-posters`, `demo-map`, `doxa-vision-card`, `stack-demo`), each carrying the
> 🧪 marker. Discovery is fail-soft: if the manifest is unreachable the folder stays empty and the curated
> rail is unaffected — a discovery failure never takes down the app.
**Promote it to a curated tab (optional).** The curated sections encode a deliberate teaching order, so a
plugin graduates out of Experimental only when it earns a place in that order. That is the edit Step 6 used to
describe as mandatory: add a `TAB_DEFS` entry plus a `NAV_MODEL` row in `plugin-designs/doxa-maps-spa/`, then
rebuild + republish.
**One rail row per BUNDLE, not per profile.** A bundle's profiles (gospel-resources ships four) are alternate
views reached *inside* the pane. The rail is an onboarding ramp, not an inventory.
---
**That's the whole path: author in `plugin-designs/` → `bun run build` → prove it MOUNTS (`smoke:embeds`) →
publish via the runbook → see it live → and it surfaces itself in the SPA's Experimental folder.** Only two
of those six steps are edits you write: the plugin, and the optional promotion out of Experimental. Deep
dives: [tutorial.md](tutorial.md) · [how-to.md](how-to.md) · [reference.md](reference.md) · [why.md](why.md).