plugin-designer skill: fix stale gallery claims, split depth into references/
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:
parent
bbcc52cbcc
commit
53363ba80d
6 changed files with 435 additions and 45 deletions
|
|
@ -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
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
91
skills/plugin-designer/references/backend.md
Normal file
91
skills/plugin-designer/references/backend.md
Normal 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
|
||||||
79
skills/plugin-designer/references/glossary.md
Normal file
79
skills/plugin-designer/references/glossary.md
Normal 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
|
||||||
103
skills/plugin-designer/references/targets.md
Normal file
103
skills/plugin-designer/references/targets.md
Normal 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
|
||||||
97
skills/plugin-designer/references/workflow.md
Normal file
97
skills/plugin-designer/references/workflow.md
Normal 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
|
||||||
Loading…
Reference in a new issue