diff --git a/docs/codeless-bnp.md b/docs/codeless-bnp.md new file mode 100644 index 0000000..ca94810 --- /dev/null +++ b/docs/codeless-bnp.md @@ -0,0 +1,96 @@ +# Codeless `.bnp` — Declarative Plugins (WO-WZ-020) + +A **codeless `.bnp`** is a plugin artifact with **no `plugin.wasm`**: pure +declaration the host runs. No wazero compile, no instance pool, no capability +binding — zero guest code ever executes. This is the end-state of "injection, +not reliance" for themes, content sites, and template-only block packs: the +plugin is *data the host runs*, not a program that links a library. No Go, no +`block/core` dependency, no toolchain beyond the `ninja` CLI. + +## The classifier — exactly one rule + +`ninja plugin build` classifies by repo shape: + +- **No Go source at the repo root → codeless.** The manifest is synthesized + from `plugin.mod` + the declarative files below; the artifact packs without + a wasm. `--codeless` asserts this and fails if Go is present. +- **Go source → wasm**, exactly as before. A converted repo DELETES its Go — + partial conversions keep a smaller wasm and still ship `blocks/` + + `seed/` alongside it (both artifact kinds carry the declarative dirs). + +## The declarative / logic split + +**Codeless-expressible** (declaration or host-rendered template): +- Blocks: a `.ninjatpl` template + declared **data providers** (the CMS's own + provider set — `posts`, `site`, `menus`, `authors`, … ADR 0018). The host + fetches, the engine renders. Defined in `blocks/blocks.yaml` — the SAME + manifest-FS layout core builtin block definitions use. +- Master pages, theme presets, fonts, CSS manifest, icon packs, settings + schema (`manifest.yaml`), assets (host-served), migrations (host-run Goose), + seed data (`seed/seed.json`, applied via the WO-WZ-019 provisioner). + +**Requires code (a wasm plugin):** +- A block whose data no declared provider can produce; a *computing* tag or + filter; HTTP handlers; background jobs; Load/Unload logic; RAG fetchers; + media hooks; bridge services; AI tools; `data_dir`. A codeless manifest + declaring any of these is rejected at build (`ninja plugin verify` / + `CodelessHookViolation`) AND at load (cms reader `codelessHookViolation`). + +## Repo layout + +``` +my-theme/ + plugin.mod # name/version/kind (data_dir forbidden) + manifest.yaml # optional: theme_presets/bundled_fonts/settings_schema + # (JSON file refs), master_pages (JSON file), + # required_icon_packs, css {...}, dependencies [...] + blocks/ + blocks.yaml # key/title/category/schema/template/providers per block + hero.schema.json + hero.ninjatpl + templates/ # reserved for host-rendered page templates (future) + seed/ + seed.json # settings {merge/override/ensure}, media, pages, menu_items + hero.jpg # media bytes referenced by seed.json entries + assets/ # host-served statics + migrations/ # Goose SQL, host-run under the plugin schema +``` + +`ninja plugin build` → `-.bnp` (tar.zst, no `plugin.wasm`, +`manifest.pb` with `codeless: true`). `ninja plugin verify` re-runs the +loader's checks standalone. + +## How the host runs it (cms `plugin/codeless_loader.go`) + +- The `.bnp` reader accepts a missing `plugin.wasm` iff `manifest.codeless`, + and rejects codeless manifests with computing-hook declarations. +- Load: `blocks/` → `blocks.LoadManifest` → `Registry.RegisterDefinition` — + rendering flows through the existing definition engine (providers + + ninjatpl + layer fallback, WO-089..096). `seed/seed.json` → provisioner + (`EnsureMedia`/`MergeSiteSettings`/`EnsureSetting`/`EnsurePage`/ + `EnsureMenuItem`), idempotent by construction. Presets/fonts/CSS/master + pages/settings schema come from the manifest exactly as for wasm plugins. +- Unload/disable: definitions unregister; seeded content stays (it is site + data, not runtime state). Uninstall: normal teardown (schema/role drop, + artifact removal). +- Hot-swap: codeless→codeless swaps run migrations first, then re-register + definitions and re-apply seed. Wasm↔codeless transitions require + uninstall + reinstall. + +## Seed schema (`seed/seed.json`) + +```json +{ + "settings": {"merge": {"k": "v"}, "override": {"k": "v"}, "ensure": {"k": "v"}}, + "media": [{"id": "", "file": "hero.jpg", "alt": "", "folder": ""}], + "pages": [{"slug": "/", "title": "Home", "template_key": "landing", + "parent_slug": "", "reconcile_blocks": false, + "blocks": [{"block_key": "hero", "title": "Hero", + "content": {}, "slot": "main", "sort_order": 1}]}], + "menu_items": [{"menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1}] +} +``` + +Media `id` is the deterministic, template-referable key (`media:`, +`{% img %}`); bytes live in `seed/` next to the JSON. Apply order: +media → settings → pages → menu items.