plugin-designer skill: fix stale gallery claims, split depth into references/
Some checks are pending
Deploy to GitHub Pages / build (push) Waiting to run
Deploy to GitHub Pages / deploy (push) Blocked by required conditions

SKILL.md claimed new plugins land in a Research Gallery Experimental section.
That section was replaced by capability tiers (plugin-studio.vue:630); stage is
now a label prefix, not a place. Both stale lines corrected.

Adds what the skill never said:
- capability is DERIVED from composes and an explicit type is CHECKED against it
  (check-capability-derived.mjs), with its two traps;
- rail labels come from meta, and check-studio-rail.mjs:68 fails a bare id;
- presets merge OVER the profile's meta config, key by key -- rule 3 of
  documentation/PRESET-PLUGIN-STANDARD.md, asserted by check:preset-config-merge
  (generate-tree.mjs:1094, the comment there records the pinHint bug);
- targets is read (profile-meta.mjs:28) and consequential: the Research Gallery
  skips a bundle that does not list web (doxa-maps-spa.vue:605).

Depth moves to references/: glossary, targets, backend, workflow. backend.md
carries the PocketBase user tiers marked DESIGN INTENT, NOT BUILT -- quoted
rather than linked, because their source lives outside this repo.

Verified against this tree, rebased on main: 61 path citations resolve, 10 line
citations land on the code they claim.

README: one line pointing an agent at the skill.
This commit is contained in:
Doxa Maps 2026-09-28 03:15:51 +00:00
parent bbcc52cbcc
commit 53363ba80d
6 changed files with 435 additions and 45 deletions

View file

@ -12,6 +12,9 @@ Built with love, with the prayer that these maps bless the nations.
iframe, script embed, mobile app — or open every map at once as one app. Five plugin iframe, script embed, mobile app — or open every map at once as one app. Five plugin
capabilities, three outputs, one shared library, one build. capabilities, three outputs, one shared library, one build.
> **Using an AI agent?** Give it [`skills/plugin-designer`](./skills/plugin-designer). It learns
> this framework's terms and patterns, which lowers the barrier to entry.
### What the library gives every map ### What the library gives every map
| | | | | |

View file

@ -40,9 +40,14 @@ plugin-designs/<your-plugin>/ one FOLDER = one BUNDLE (the folder name is th
- **Elements live inside.** One bundle registers one tag named after the folder, and can hold more - **Elements live inside.** One bundle registers one tag named after the folder, and can hold more
elements for its own screens to nest (see `plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json`). elements for its own screens to nest (see `plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json`).
- **Folders starting with `_` are skipped** by the build: a free place for work in progress. - **Folders starting with `_` are skipped** by the build: a free place for work in progress.
- **New plugins appear in the Plugin Studio at once, and in the Research Gallery's Experimental - **New plugins appear in the Plugin Studio at once**, sorted into the rail by **capability**, with
section automatically.** Moving one into a production section is a deliberate edit to the a translated "experimental" prefix until they are marked production. There is no Experimental
gallery's curated sections (`plugin-designs/doxa-maps-spa/doxa-maps-spa.vue`), not your call alone. *section* to be promoted out of — the Driver replaced it with capability tiers
(`plugin-designs/plugin-studio/plugin-studio.vue:630`, and `:642` for the label). Stage and
capability are different axes; see `references/glossary.md`.
- **Your rail entry comes from `<profile>.meta.json` `"label"`**, and
`internal/check-studio-rail.mjs:68` fails a plugin whose label is just its own id. Give it a
human name.
## 2. Start by copying the starter ## 2. Start by copying the starter
@ -58,12 +63,11 @@ Then rename exactly these, and nothing else:
3. the `.vue` filenames (`template-map-a.vue` → `<screen>.vue`); delete `template-map-b.vue` if 3. the `.vue` filenames (`template-map-a.vue` → `<screen>.vue`); delete `template-map-b.vue` if
you need one screen. you need one screen.
`template-map-a.vue` is a real minimal map: it draws `data/places.geojson` on a basemap with one `template-map-a.vue` is a real minimal map: it draws `data/places.geojson` with one translated
translated title, and already follows the page's engine, language (with right to left) and token, title, and already follows the page's engine, language (with right to left) and token, falling
falling back to keyless MapLibre. **Replace `data/places.geojson` with your own points** and you have back to keyless MapLibre. **Replace `data/places.geojson` with your own points** and you have a
a working map to build on. Its title key (`template.title`) lives in the library catalogues. Your own working map. Your own translated text needs a catalogue file in `library/i18n/locales/`, which is
translated text needs its own catalogue file in `library/i18n/locales/`, which is a library change a library change (section 3); until that lands, plain English in your `.vue` is fine.
(section 3). Until that lands, plain English in your `.vue` is fine.
Keep the entry's `configureApp` exactly as the starter has it: it installs Pinia, installs i18n Keep the entry's `configureApp` exactly as the starter has it: it installs Pinia, installs i18n
**and** provides it as `appI18n`, and provides `profileModules`. The element's root **and** provides it as `appI18n`, and provides `profileModules`. The element's root
@ -170,6 +174,17 @@ levels 1 and 2 are detected on their own.
| 4 | composite element (`nested-elements-mfe`) | nests other elements, each still embeddable alone, wired by props and events | `plugin-designs/dashboard-plus-grid/` (the Big Picture tab) | | 4 | composite element (`nested-elements-mfe`) | nests other elements, each still embeddable alone, wired by props and events | `plugin-designs/dashboard-plus-grid/` (the Big Picture tab) |
| 5 | app shell (`nested-mfe-spa`) | nests whole apps | `plugin-designs/doxa-maps-spa/` (the Research Gallery) | | 5 | app shell (`nested-mfe-spa`) | nests whole apps | `plugin-designs/doxa-maps-spa/` (the Research Gallery) |
**Your declaration is checked, not merely trusted.** `internal/check-capability-derived.mjs`
derives the level from what your plugin actually `composes` and fails when an explicit `"type"`
disagrees with it — a `meta.json` that contradicts the structure means one of the two is stale.
Run `bun run check:capability-derived`. Details and the two traps in `references/targets.md`.
**Presets merge over the profile's defaults, they do not replace them** — a preset page's config is
the profile's `<profile>.meta.json` `config`, then the preset's `props` over it, key by key. Set a
key in `props` only to change it. That is rule 3 of `documentation/PRESET-PLUGIN-STANDARD.md`,
asserted by `bun run check:preset-config-merge`; **read that standard before shipping presets.**
If two presets need different *code*, they are not presets — make another profile.
## 8. Learn from the reference plugins ## 8. Learn from the reference plugins
The seven production plugins are the reference: each shows what a good plugin does. Before you The seven production plugins are the reference: each shows what a good plugin does. Before you
@ -187,63 +202,65 @@ line number, because lines move.
| Interop: one element reacts to another's events | Simple Research: `plugin-designs/research-map-plus-grid/research-map-plus-grid.vue` | the map listening for the grid's `groups-filtered` event | | Interop: one element reacts to another's events | Simple Research: `plugin-designs/research-map-plus-grid/research-map-plus-grid.vue` | the map listening for the grid's `groups-filtered` event |
| Composite element (capability 4) | Big Picture: `plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json` | `"composes"`: two elements, each still embeddable alone | | Composite element (capability 4) | Big Picture: `plugin-designs/dashboard-plus-grid/dashboard-plus-grid.meta.json` | `"composes"`: two elements, each still embeddable alone |
| One bundle, several embeddable screens | UPG-100: `plugin-designs/my-upg-100-list/` | `upg-100-mfe`, `upg-100-list` and `poster-wall` from one bundle | | One bundle, several embeddable screens | UPG-100: `plugin-designs/my-upg-100-list/` | `upg-100-mfe`, `upg-100-list` and `poster-wall` from one bundle |
| App shell (capability 5) | Research Gallery: `plugin-designs/doxa-maps-spa/doxa-maps-spa.vue` | every production plugin as a tab, and the Experimental folder filled from the manifest | | App shell (capability 5) | Research Gallery: `plugin-designs/doxa-maps-spa/doxa-maps-spa.vue` | every plugin as a tab, filled from the manifest |
`emissivePaint` and `labelFont` (section 5) have no production-plugin exemplar, because the `emissivePaint` and `labelFont` (section 5) have no production-plugin exemplar, because the
production plugins get them through the library's own layers. The starter uses both directly: production plugins get them through the library's own layers. The starter uses both directly:
read the pin and label layers in `plugin-designs/template-bundle/template-map-a.vue`. read the pin and label layers in `plugin-designs/template-bundle/template-map-a.vue`.
## 9. Develop in the Plugin Studio ## 9. Develop, build, land
```bash ```bash
bun install # once bun run dev # the Plugin Studio at http://localhost:5173/
bun run dev # the Plugin Studio at http://localhost:5173/ BUNDLE=<your-plugin> bun x vite build # just your bundle → public/dx-maps/<your-plugin>/
git add -A && git commit -m "…" && git pull --rebase origin main && git push origin HEAD:main
``` ```
- Your plugin appears in the studio as soon as its folder exists, and edits reload live. - Your plugin appears in the studio as soon as its folder exists; edits reload live. One screen
- One screen on its own: `http://localhost:5173/__leaf/<your-plugin>/<screen>`. alone: `http://localhost:5173/__leaf/<your-plugin>/<screen>`.
- Test the universal props from the studio header: switch the **engine**, the **language** - **Test every combination from the studio header**: both engines × with and without a token ×
(include Arabic), and the **token** (none, a token, a token service). Your plugin must work in English and Arabic.
every combination. - **Build only your own bundle.** `BUILD_ALLOW=1 bun run build` rewrites *every* plugin's output;
- `bun run check:dev-assets` checks that every map's data files load under the dev server. on a team one agent owns it.
- **A rejected push is normal** — someone landed first. Rebase and push again; measured at 95%
rejected, zero conflicts. Stage only your own files.
## 10. Build: one step **Done means**: your bundle builds and appears in `manifest.json`; it works under both engines,
with and without a token, in English and Arabic; every new string is in all 11 catalogues
(`bun run check:i18n-parity`); your expressions parse under MapLibre (section 5, rule 7); and you
changed nothing in `library/` or another plugin's folder.
```bash Full loop, the gate list, and working when your agent runs on a server: `references/workflow.md`.
BUNDLE=<your-plugin> bun x vite build # just your bundle → public/dx-maps/<your-plugin>/
BUILD_ALLOW=1 bun run build # every bundle, the tree, the manifest, the studio
```
The full build rewrites every plugin's output. It refuses to run without `BUILD_ALLOW=1`, and ## 10. The three outputs
on a team one agent owns it. Build only your own bundle unless you are that agent.
`public/dx-maps/manifest.json` then lists your plugin with ready-to-paste embed snippets.
## 11. The three outputs One built folder, three ways to ship it: the **web folder** (`public/dx-maps/`, iframe or script
tag from any host), the **mobile app** (the same folder wrapped by Capacitor, `bun run phone`), and
the **desktop app** (the same folder in Tauri, `bun run tauri:build`, a proof of concept).
| Output | What it is | How | **Your plugin runs everywhere unless you say otherwise.** A `<profile>.meta.json` `"targets"` list
|---|---|---| narrows that, and it is read, not decorative: the Research Gallery skips a bundle that does not
| **1. The web folder** | `public/dx-maps/`: every plugin, embeddable by **iframe** or **script tag**, from any host | copy the folder; snippets are in `manifest.json` | list `web` (`plugin-designs/doxa-maps-spa/doxa-maps-spa.vue:605`). Declare it only for a plugin
| **2. The mobile app** | the same folder wrapped by Capacitor (`capacitor.config.json` `webDir: public/dx-maps`) | `bun run phone` (Android) | that genuinely cannot work in a plain web embed — Rudder needs a local speech engine. Details:
| **3. The desktop app** | the same folder in Tauri (`deployment/src-tauri/`), a proof of concept | `bun run tauri:build` | `references/targets.md`.
WordPress (`deployment/wordpress/`) embeds the web folder's snippets; it's experimental. ## Going deeper
## 12. Done means | Read | For |
|---|---|
| `references/glossary.md` | bundle, profile, preset, element; stage vs capability; rail labels |
| `references/targets.md` | the five capabilities in full, how they are derived and checked, `targets`, the three outputs |
| `references/backend.md` | when your plugin needs a service; databases and migrations; the user tiers (design intent, not built) |
| `references/workflow.md` | the full develop → build → check → land loop |
- `BUNDLE=<your-plugin> bun x vite build` succeeds, and your plugin is in `manifest.json`. In the repo itself:
- It works in the Plugin Studio under **both engines**, **with and without a token**, in
**English and Arabic**.
- Every new string is in all 11 catalogues (`bun run check:i18n-parity`).
- Your layer expressions parse under MapLibre (section 5, rule 7).
- You changed nothing in `library/` or in another plugin's folder, or you handed that change to
the build owner.
## References (all in this repo) - `plugin-designs/README.md`: authoring guide
- `plugin-designs/README.md`: authoring guide, production vs experimental
- `plugin-designs/template-bundle/`: the starter, and its README - `plugin-designs/template-bundle/`: the starter, and its README
- `documentation/reference.md`: the `@map/…` library menu and the profile types - `documentation/reference.md`: the `@map/…` library menu and the profile types
- `documentation/PRESET-PLUGIN-STANDARD.md`: presets in full — the six rules, and how they are discovered
- `documentation/how-to.md`: recipes, including swapping the map engine - `documentation/how-to.md`: recipes, including swapping the map engine
- `documentation/HOWTO-join-the-table.md`: working alongside other people
- `library/composables/useMapProvider.js`: engine resolution and engine helpers - `library/composables/useMapProvider.js`: engine resolution and engine helpers
- `library/maps/label-font.js`: fonts per engine - `library/maps/label-font.js`: fonts per engine
- `library/i18n/index.js`: languages, `tStatic`, right to left - `library/i18n/index.js`: languages, `tStatic`, right to left

View file

@ -0,0 +1,91 @@
# Backends, data, and users
Most plugins need no backend. A map that draws its own data files, fetches a public API, or reads
`data/` inside its bundle is complete without one. Reach for this page only when your plugin needs
a service or stored data.
## What exists today
`backend/` holds exactly two things:
| Folder | What it is |
|---|---|
| `backend/pocketbase/` | the database: migrations, and the local instance `bun run dev` starts |
| `backend/rudder-engine/` | the local speech engine for the Rudder voice plugin |
A plugin that needs one names it in its `<profile>.meta.json` `"backend"` key, as
`plugin-designs/rudder-voice/rudder-voice.meta.json` does with `"backend": "backend/rudder-engine"`.
A plugin that needs a local backend almost certainly also needs `"targets"` without `web`, which
keeps it out of the Research Gallery — see `targets.md`. Rudder declares both.
## The database rule: schema is a commit, never a click
**You never publish a database.** What travels between people is the *shape* — tables and fields —
as migration files committed to `backend/pocketbase/pb_migrations/`. Adding a field is a normal
change: edit, commit the migration, push it like any other file.
This is deliberate. A rule that opens data to the public arrives as **a commit someone can read**
rather than as a setting somebody clicked. `documentation/HOWTO-join-the-table.md` is the full
explanation.
**New collections start locked.** Your plugin's data is not readable until a migration opens it, so
nothing is public by accident.
### Which database your laptop talks to
By default, your own. `bun run dev` starts a local instance, keeps its data in `.pb/`, and gives
you an admin screen at `/api/pb/_/`. A fresh clone plus `bun run dev` is the whole setup.
Pointing at the team's shared test data is opt-in, one line in `.env.local`:
```
DX_PB_URL=https://<your team's dev server>/pb
```
`DX_PB_PORT` is the only other one, and only if the log says port 8090 is in use. The full set of
environment names is in `.env.example`: `VITE_MAPBOX_TOKEN`, `VITE_API_BASE_URL`, `DX_SYNC_REMOTE`,
`DX_SYNC_BRANCH`, `DX_SYNC_INTERVAL`, `DX_PB_URL`, `DX_PB_PORT`. Every line is optional; empty means
the all-local default.
---
## User tiers — DESIGN INTENT, NOT BUILT
> **Read this heading literally.** What follows is a *planned* model recorded in the project's
> architecture notes under the heading "Open load-bearing assumptions (DX base; unproven)". **None
> of it is implemented in this repo.** Do not design your plugin around it, do not write code that
> assumes a tier exists, and do not tell anyone a plugin "supports" these. It is here so you
> recognise the vocabulary when it comes up, and so you do not invent a competing scheme.
The intended tiers, which parallel the five plugin capabilities:
| Tier | Intended meaning |
|---|---|
| anonymous | reaches the plugin by URL, no sign-in |
| access-code | temporary access without an account |
| plugin user | authenticated against the database |
| base OIDC member | signed in through the platform's identity provider |
| plugin admin | administers one plugin |
| PB plugin owner | owns a plugin's data |
| PB plugin writer | writes a plugin's data, with **no access to other plugins' data** |
Quantum Forms is named as the reference plugin that would exercise every tier.
**Source.** This is recorded in `ARCHITECTURE-EQUATION.md` §6, which lives in the project's QA
cycle **outside this code repository** (`_qa-cycle/03-deployment-cycle/`). It is quoted here rather
than linked because a reader who clones this repo will not have that file. If you need the
authoritative text, ask whoever maintains the QA cycle — do not reconstruct it from this summary.
### What to do in the meantime
If your plugin needs any notion of "who is this" today, use what is actually built: PocketBase auth
plus a migration that opens exactly the collection you need, and nothing wider. Do not hand-roll a
permission model to bridge the gap — the last tier above ("no access to other plugins' data") is the
property a hand-rolled scheme tends to break, and breaking it crosses plugins.
## Related
- `targets.md` — the `targets` and `backend` meta keys
- `workflow.md` — landing a migration on main like any other file
- `documentation/HOWTO-join-the-table.md` — the three databases, in full

View file

@ -0,0 +1,79 @@
# Glossary
The words this repo uses, and the file that decides each one. When a definition here and the
code disagree, the code is right — these are pointers, not a second source of truth.
## The shape
| Word | What it means | Decided by |
|---|---|---|
| **bundle** | One folder under `plugin-designs/`. Builds to exactly one JavaScript file. The folder name is the bundle name. | `internal/plugin-gallery/discover.mjs` |
| **profile** | One `.vue` file, flat in the bundle folder. One profile = one screen = one embeddable page. | `internal/plugin-gallery/discover.mjs` |
| **element** | The custom HTML tag a bundle registers. One bundle usually registers one tag; it may register more for its own screens to nest. | your `index.js` |
| **instance** (preset) | One row of `<name>.instances.json`. Same code, different settings, its own page. | `internal/plugin-gallery/discover.mjs:126-138` |
| **capability** | A number 1–5 saying how composed a profile is. See `targets.md`. | `internal/check-capability-derived.mjs` |
| **the tree** | The build output under `public/dx-maps/` — every bundle, its staging pages, and `manifest.json`. | `internal/generate-tree.mjs` |
| **the library** | `library/`, imported as `@map/…`. Shared by every plugin. | `documentation/reference.md` |
| **profile-config** | The JSON attribute an embedder puts on your tag to set the universal props. | `library/ProfileLoader.vue` |
## Presets, precisely
A preset is **not** a variant of your code. It is a row in `<name>.instances.json` giving a
preset `id`, `title` and `props`. The build writes one page per row.
**Presets merge over the profile's defaults, they do not replace them.** This is rule 3 of
`documentation/PRESET-PLUGIN-STANDARD.md`, and it is asserted on the built tree by
`bun run check:preset-config-merge`:
> A preset page's config is the profile's `<profile>.meta.json` `config`, then the preset's
> `props` over it, key by key (a shallow merge). A default the preset does not repeat is kept:
> set it in `props` only to change it.
You can see the merge order in the spread at `internal/generate-tree.mjs:1094` (the embed page) —
```js
config: { assetsBase: assetsBaseFor(4), ...(profile.extraConfig || {}), ...inst.props },
```
— and at `:1147` (the manifest row). The profile's own config is written first, the preset's props
second, so a key the preset sets wins and a key it omits survives. The comment directly above
records the bug that forced the rule: "a preset lost its profile's `pinHint:false`".
Read that standard before shipping presets — it also covers the two accepted file shapes, and the
silent failure mode (a filename not ending `.instances.json`, or an `instances` key that is not an
array, types the bundle `single` with no error).
If two presets need **different code**, they are not presets — make another profile.
## Stage vs capability — two different axes
These get confused, so keep them apart:
- **Capability (1–5)** is *how composed* a plugin is. Derived and checked by
`internal/check-capability-derived.mjs`.
- **Stage (`production` / experimental)** is *how finished* it is. It is an attribute on the
plugin's row, not a place it lives.
There is no "Experimental section" to be promoted out of. The Driver replaced that section with
capability tiers on 2026-09-27 — see the comment at `plugin-designs/plugin-studio/plugin-studio.vue:630`
("★ EVERY PLUGIN IS CAPABILITY 1-5 … replacing the Experimental section"). Line 642 shows what
replaced it: a non-production row simply gets a translated `stageExperimental` prefix on its
label, in a rail organised by capability.
## Rail labels
The Plugin Studio's left rail shows the `label` from `<profile>.meta.json`. It is not optional in
practice: `internal/check-studio-rail.mjs:68` fails a plugin whose label is just its own id —
```
if (r.label === r.profile || r.label === r.bundle)
fail(`${r.key} shows a bare id ("${r.label}") — give its <profile>.meta.json a "label"`)
```
So "give it a label" is a gate, not a convention. Run `bun run check:studio-rail`.
## Related
- `targets.md` — capabilities, `targets`, and where a plugin is allowed to run
- `backend.md` — data and the user tiers (design intent, not built)
- `workflow.md` — the loop from empty folder to landed on main

View file

@ -0,0 +1,103 @@
# Capabilities and targets
Two separate questions about a plugin: **how composed is it** (capability 1–5) and **where can it
run** (`targets`). Only the first is enforced.
## Capability 1–5
Pick the lowest level that does the job.
| Level | `type` | What it is | Example |
|---|---|---|---|
| 1 | `single` | one screen, one element | `plugin-designs/prayer-walk-planner/` |
| 2 | `parameterized` | one screen, several named presets from `<screen>.instances.json` | `plugin-designs/doxa-simple-map/` |
| 3 | `monolithic` | many views fused inside one element | `plugin-designs/doxa-research-map/` |
| 4 | `nested-elements-mfe` | nests other elements, each still embeddable alone | `plugin-designs/dashboard-plus-grid/` |
| 5 | `nested-mfe-spa` | nests whole apps | `plugin-designs/doxa-maps-spa/` |
### It is DERIVED, and your declaration is CHECKED against it
Levels 1 and 2 are inferred: a bundle with an `*.instances.json` is `parameterized`, otherwise
`single` (`internal/plugin-gallery/discover.mjs:44-52`). Levels 3–5 you declare in
`<screen>.meta.json` `"type"`.
Declaring is not the end of it. `internal/check-capability-derived.mjs` derives the capability
from what your plugin actually `composes` and **fails when an explicit `type` disagrees**. Its
own header states the contract:
> A profile that composes anything derives cap-4, or cap-5 if it composes a cap-4 — and an
> EXPLICIT type must AGREE with that. The declaration stays authoritative; it is not overridden,
> it is checked.
The defect it exists for: research-dashboard nested ten views with no `meta.json` and shipped as
capability 1, because a missing file is indistinguishable from a deliberate choice. Every surface
that shows a tier — the manifest, the gallery, the studio rail — reads that one field, so the
understatement propagated everywhere.
Run `bun run check:capability-derived`. Two related gates: `check:capability-citations` and
`check:capability-exemplars`.
### Two traps the gate documents
Worth knowing because they will bite you the same way:
- **`composes` is per-profile at source but hoisted to the bundle in the manifest.** Deriving from
the bundle row applies a composite's edges to its plain sibling leaves — gospel-resources has one
composite and four single leaves, and all four wrongly derived as cap-4.
- **Refs appear in three forms in the same field**: `'bundle/profile'`, a bare `'bundle'`, and an
element tag. Resolving only the first demoted plugin-studio from 5 to 4.
## `targets` — where the plugin may run, and it has teeth
A `<profile>.meta.json` may carry `"targets"`, naming where the plugin is meant to run:
```json
{ "type": "single", "label": "Rudder", "targets": ["desktop"], "backend": "backend/rudder-engine" }
```
**Omitting it means "everywhere", and that is the right default for almost every plugin.** The
whole point of the build is one folder that runs as a web embed, a mobile app and a desktop app.
**Declaring it has a real consequence**, so declare it only when you mean it:
- `internal/profile-meta.mjs:28` reads the field (string entries only) and `:36` carries it into
the manifest, so every consumer of the manifest sees it.
- **The Research Gallery skips a bundle whose every profile is a widget or lacks the `web`
target** — `plugin-designs/doxa-maps-spa/doxa-maps-spa.vue:605`:
```js
if (profiles.every(p => p.widget || (Array.isArray(p.targets) && !p.targets.includes('web')))) continue
```
So `"targets": ["desktop"]` removes your plugin from the gallery. That is the feature, not a
bug: it keeps a plugin that cannot work on a static host out of a surface that is served from
one. `bun run check:gallery-embed` covers the gallery's embeds.
The worked example is Rudder: its speech model runs in a local backend started by `bun run dev`
and run by Tauri as a sidecar, so "a website has no local engine" (its `_comment`). It is
correctly absent from the gallery and makes no network call on a static host.
- **Say why in `_comment`** when you declare it — the next reader needs the reason, not just the list.
- **A `backend` key alongside it** names the folder that must be running. See `backend.md`.
- **Do not add `targets` "for documentation".** Adding it without `web` silently removes you from
the gallery.
## Where a plugin actually runs
The three outputs, all from the same built folder:
| Output | What it is | How |
|---|---|---|
| Web folder | `public/dx-maps/`, embeddable by iframe or script tag from any host | copy the folder; snippets in `manifest.json` |
| Mobile app | the same folder wrapped by Capacitor (`capacitor.config.json` `webDir`) | `bun run phone` |
| Desktop app | the same folder in Tauri (`deployment/src-tauri/`), a proof of concept | `bun run tauri:build` |
WordPress (`deployment/wordpress/`) embeds the web folder's snippets; it is experimental.
Embeddability is checked, unlike `targets`: `bun run check:embed-targets` runs
`internal/check-embed-targets-exist.mjs`.
## Related
- `glossary.md` — bundle, profile, preset, stage vs capability
- `backend.md` — plugins that need a service, and the user tiers

View file

@ -0,0 +1,97 @@
# The workflow: empty folder to landed on main
## 1. Develop in the Plugin Studio
```bash
bun install # once
bun run dev # the Plugin Studio at http://localhost:5173/
```
- Your plugin appears in the studio as soon as its folder exists; edits reload live.
- One screen on its own: `http://localhost:5173/__leaf/<your-plugin>/<screen>`.
- Test the universal props from the studio header: switch the **engine**, the **language**
(include Arabic), and the **token** (none, a token, a token service). Your plugin must work in
every combination — that is the point of the header.
- `bun run check:dev-assets` checks that every map's data files load under the dev server.
Your plugin's rail entry comes from `<profile>.meta.json` `"label"`, and
`bun run check:studio-rail` fails a bare id. Give it a human name.
## 2. Build
```bash
BUNDLE=<your-plugin> bun x vite build # just your bundle → public/dx-maps/<your-plugin>/
BUILD_ALLOW=1 bun run build # every bundle, the tree, the manifest, the studio
```
The full build rewrites **every** plugin's output. It refuses to run without `BUILD_ALLOW=1`, and
on a team one agent owns it. **Build only your own bundle unless you are that agent.**
`public/dx-maps/manifest.json` then lists your plugin with ready-to-paste embed snippets.
## 3. Check
Run the gates that cover what you touched. The ones most often relevant:
| Gate | What it protects |
|---|---|
| `bun run check:i18n-parity` | every new key exists in all 11 catalogues |
| `bun run check:i18n-never-translated` | a key that was copied rather than translated |
| `bun run check:capability-derived` | your declared `type` agrees with what you compose |
| `bun run check:studio-rail` | your rail entry is a label, not a bare id |
| `bun run check:embed-targets` | the embed targets in the manifest exist |
| `bun run check:dev-assets` | data files load under the dev server |
`package.json` holds the full list; they are all prefixed `check:`.
## 4. Land it on main
Three commands, in this order, every time:
```bash
git add -A && git commit -m "what you changed"
git pull --rebase origin main
git push origin HEAD:main
```
**A rejected push is normal, not an error.** Someone landed first: run the middle command again and
push again. In a ten-writer stress test at a far faster rate than a human works, 95% of pushes were
rejected and retried, **zero** became merge conflicts, averaging 1.1 retries per edit
(`documentation/HOWTO-join-the-table.md`).
That result is the one-folder-per-person rule paying off: everyone pushes to one branch, so you
collide on *who goes first*, never on the same lines. A real conflict means two people edited one
file — stop and talk to them; that is a coordination problem, not a git problem.
**Stage only your own files.** `git add -A` is safe when you own your folder and nothing else is
dirty; check `git status` first if you are unsure. Never commit another agent's uncommitted work.
## 5. Done means
- `BUNDLE=<your-plugin> bun x vite build` succeeds, and your plugin is in `manifest.json`.
- It works in the Plugin Studio under **both engines**, **with and without a token**, in
**English and Arabic**.
- Every new string is in all 11 catalogues (`bun run check:i18n-parity`).
- Your layer expressions parse under MapLibre (SKILL.md section 5, rule 7).
- You changed nothing in `library/` or in another plugin's folder, or you handed that change to the
build owner.
## Working when your agent runs on a server
If your coding agent runs somewhere other than your laptop, one line in `.env` or `.env.local`
makes your local dev server follow the branch the agent pushes to:
```
DX_SYNC_REMOTE=origin
```
A full repo URL works too. It only ever fast-forwards, and only when all three are true: you are on
that branch, you have no unsaved edits, and you have no local commits. Otherwise it leaves your
files alone and says `main is N commits ahead` in the log. So **push your own work often**, or you
will drift. `documentation/CLOUD-DEVELOPMENT-ENVIRONMENT.md` covers this and the shared-container
alternative.
## Related
- `glossary.md` — the vocabulary
- `targets.md` — capabilities and where a plugin runs
- `backend.md` — databases and migrations