diff --git a/README.md b/README.md index 215b28010..1f68f5b31 100644 --- a/README.md +++ b/README.md @@ -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 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 | | | diff --git a/skills/plugin-designer/SKILL.md b/skills/plugin-designer/SKILL.md index 4945fca54..acae4cefa 100644 --- a/skills/plugin-designer/SKILL.md +++ b/skills/plugin-designer/SKILL.md @@ -40,9 +40,14 @@ plugin-designs// 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 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. -- **New plugins appear in the Plugin Studio at once, and in the Research Gallery's Experimental - section automatically.** Moving one into a production section is a deliberate edit to the - gallery's curated sections (`plugin-designs/doxa-maps-spa/doxa-maps-spa.vue`), not your call alone. +- **New plugins appear in the Plugin Studio at once**, sorted into the rail by **capability**, with + a translated "experimental" prefix until they are marked production. There is no Experimental + *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 `.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 @@ -58,12 +63,11 @@ Then rename exactly these, and nothing else: 3. the `.vue` filenames (`template-map-a.vue` → `.vue`); delete `template-map-b.vue` if you need one screen. -`template-map-a.vue` is a real minimal map: it draws `data/places.geojson` on a basemap with one -translated title, and already follows the page's engine, language (with right to left) and token, -falling back to keyless MapLibre. **Replace `data/places.geojson` with your own points** and you have -a working map to build on. Its title key (`template.title`) lives in the library catalogues. Your own -translated text needs its own catalogue file in `library/i18n/locales/`, which is a library change -(section 3). Until that lands, plain English in your `.vue` is fine. +`template-map-a.vue` is a real minimal map: it draws `data/places.geojson` with one translated +title, and already follows the page's engine, language (with right to left) and token, falling +back to keyless MapLibre. **Replace `data/places.geojson` with your own points** and you have a +working map. Your own translated text needs a catalogue file in `library/i18n/locales/`, which is +a library change (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 **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) | | 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 `.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 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 | | 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 | -| 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 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`. -## 9. Develop in the Plugin Studio +## 9. Develop, build, land ```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= bun x vite build # just your bundle → public/dx-maps// +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. -- One screen on its own: `http://localhost:5173/__leaf//`. -- 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. -- `bun run check:dev-assets` checks that every map's data files load under the dev server. +- Your plugin appears in the studio as soon as its folder exists; edits reload live. One screen + alone: `http://localhost:5173/__leaf//`. +- **Test every combination from the studio header**: both engines × with and without a token × + English and Arabic. +- **Build only your own bundle.** `BUILD_ALLOW=1 bun run build` rewrites *every* plugin's output; + 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 -BUNDLE= bun x vite build # just your bundle → public/dx-maps// -BUILD_ALLOW=1 bun run build # every bundle, the tree, the manifest, the studio -``` +Full loop, the gate list, and working when your agent runs on a server: `references/workflow.md`. -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. +## 10. The three outputs -## 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 | -|---|---|---| -| **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` | -| **2. The mobile app** | the same folder wrapped by Capacitor (`capacitor.config.json` `webDir: public/dx-maps`) | `bun run phone` (Android) | -| **3. The desktop app** | the same folder in Tauri (`deployment/src-tauri/`), a proof of concept | `bun run tauri:build` | +**Your plugin runs everywhere unless you say otherwise.** A `.meta.json` `"targets"` list +narrows that, and it is read, not decorative: the Research Gallery skips a bundle that does not +list `web` (`plugin-designs/doxa-maps-spa/doxa-maps-spa.vue:605`). Declare it only for a plugin +that genuinely cannot work in a plain web embed — Rudder needs a local speech engine. Details: +`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= 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 (section 5, rule 7). -- You changed nothing in `library/` or in another plugin's folder, or you handed that change to - the build owner. +In the repo itself: -## References (all in this repo) - -- `plugin-designs/README.md`: authoring guide, production vs experimental +- `plugin-designs/README.md`: authoring guide - `plugin-designs/template-bundle/`: the starter, and its README - `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/HOWTO-join-the-table.md`: working alongside other people - `library/composables/useMapProvider.js`: engine resolution and engine helpers - `library/maps/label-font.js`: fonts per engine - `library/i18n/index.js`: languages, `tStatic`, right to left diff --git a/skills/plugin-designer/references/backend.md b/skills/plugin-designer/references/backend.md new file mode 100644 index 000000000..e42405ea1 --- /dev/null +++ b/skills/plugin-designer/references/backend.md @@ -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 `.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:///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 diff --git a/skills/plugin-designer/references/glossary.md b/skills/plugin-designer/references/glossary.md new file mode 100644 index 000000000..0771906ca --- /dev/null +++ b/skills/plugin-designer/references/glossary.md @@ -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 `.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 `.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 `.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 `.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 .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 diff --git a/skills/plugin-designer/references/targets.md b/skills/plugin-designer/references/targets.md new file mode 100644 index 000000000..a822e1220 --- /dev/null +++ b/skills/plugin-designer/references/targets.md @@ -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 `.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 +`.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 `.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 diff --git a/skills/plugin-designer/references/workflow.md b/skills/plugin-designer/references/workflow.md new file mode 100644 index 000000000..47534f941 --- /dev/null +++ b/skills/plugin-designer/references/workflow.md @@ -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//`. +- 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 `.meta.json` `"label"`, and +`bun run check:studio-rail` fails a bare id. Give it a human name. + +## 2. Build + +```bash +BUNDLE= bun x vite build # just your bundle → public/dx-maps// +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= 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