@plitzi/sdk-mcp 0.37.2 → 0.37.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/CHANGELOG.md +1156 -0
  2. package/README.md +6 -2
  3. package/dist/createServer.js +1 -1
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +2 -1
  6. package/dist/modules/mcp/accessibilityOutline.d.ts +38 -0
  7. package/dist/modules/mcp/accessibilityOutline.js +75 -0
  8. package/dist/modules/mcp/accessibilityOutline.test.d.ts +1 -0
  9. package/dist/modules/mcp/catalogs/index.d.ts +2 -2
  10. package/dist/modules/mcp/catalogs/index.js +2 -2
  11. package/dist/modules/mcp/catalogs/observed.js +2 -2
  12. package/dist/modules/mcp/catalogs/pluginCatalog.d.ts +6 -3
  13. package/dist/modules/mcp/catalogs/pluginCatalog.js +12 -7
  14. package/dist/modules/mcp/helpers/agentPrompt.js +22 -5
  15. package/dist/modules/mcp/helpers/guide.d.ts +2 -2
  16. package/dist/modules/mcp/helpers/guide.js +212 -48
  17. package/dist/modules/mcp/helpers/interactions.d.ts +2 -1
  18. package/dist/modules/mcp/helpers/interactions.js +2 -0
  19. package/dist/modules/mcp/helpers/space.d.ts +6 -4
  20. package/dist/modules/mcp/helpers/space.js +7 -20
  21. package/dist/modules/mcp/helpers/uris.d.ts +3 -1
  22. package/dist/modules/mcp/helpers/uris.js +4 -1
  23. package/dist/modules/mcp/index.d.ts +2 -0
  24. package/dist/modules/mcp/localScreenshotClient.d.ts +11 -0
  25. package/dist/modules/mcp/localScreenshotClient.js +24 -9
  26. package/dist/modules/mcp/proxy/guard.d.ts +6 -0
  27. package/dist/modules/mcp/proxy/guard.js +8 -12
  28. package/dist/modules/mcp/resources/functions.d.ts +8 -0
  29. package/dist/modules/mcp/resources/functions.js +35 -0
  30. package/dist/modules/mcp/resources/register.d.ts +2 -1
  31. package/dist/modules/mcp/resources/register.js +49 -2
  32. package/dist/modules/mcp/resources/renderGuide.js +3 -2
  33. package/dist/modules/mcp/resources/router.js +3 -1
  34. package/dist/modules/mcp/screenshotClient.js +12 -4
  35. package/dist/modules/mcp/server.js +20 -10
  36. package/dist/modules/mcp/tests/guide.test.d.ts +1 -0
  37. package/dist/modules/mcp/tools/apply/dispatch.js +9 -1
  38. package/dist/modules/mcp/tools/apply/index.d.ts +34 -0
  39. package/dist/modules/mcp/tools/apply/index.js +35 -70
  40. package/dist/modules/mcp/tools/index.js +4 -2
  41. package/dist/modules/mcp/tools/operations/connectors/manifest.d.ts +0 -1
  42. package/dist/modules/mcp/tools/operations/functions/functions.test.d.ts +1 -0
  43. package/dist/modules/mcp/tools/operations/functions/index.d.ts +31 -0
  44. package/dist/modules/mcp/tools/operations/functions/index.js +48 -0
  45. package/dist/modules/mcp/tools/operations/index.d.ts +122 -2
  46. package/dist/modules/mcp/tools/operations/index.js +4 -3
  47. package/dist/modules/mcp/tools/operations/schema/elements/deleteElement.js +5 -6
  48. package/dist/modules/mcp/tools/operations/schema/elements/moveElement.js +3 -11
  49. package/dist/modules/mcp/tools/operations/schema/elements/upsertElement.js +6 -9
  50. package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.d.ts +5 -0
  51. package/dist/modules/mcp/tools/operations/schema/interactions/patchInteractionNode.js +3 -1
  52. package/dist/modules/mcp/tools/operations/schema/interactions/upsertInteractionFlow.d.ts +5 -0
  53. package/dist/modules/mcp/tools/operations/schema/operations.d.ts +27 -0
  54. package/dist/modules/mcp/tools/operations/schema/pages/deleteLayout.js +6 -5
  55. package/dist/modules/mcp/tools/operations/schema/pages/deletePage.js +6 -5
  56. package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.d.ts +17 -0
  57. package/dist/modules/mcp/tools/operations/schema/settings/patchSettings.js +34 -5
  58. package/dist/modules/mcp/tools/operations/schema/shared.d.ts +6 -0
  59. package/dist/modules/mcp/tools/operations/schema/shared.js +5 -0
  60. package/dist/modules/mcp/tools/operations/schema/translator.js +3 -2
  61. package/dist/modules/mcp/tools/operations/schema/write.d.ts +10 -5
  62. package/dist/modules/mcp/tools/operations/schema/write.js +16 -16
  63. package/dist/modules/mcp/tools/preview.d.ts +27 -0
  64. package/dist/modules/mcp/tools/render.d.ts +27 -0
  65. package/dist/modules/mcp/tools/render.js +7 -43
  66. package/dist/modules/mcp/tools/screenshot.d.ts +39 -0
  67. package/dist/modules/mcp/tools/screenshot.js +40 -5
  68. package/dist/modules/mcp/tools/shared/draftBatch.d.ts +25 -0
  69. package/dist/modules/mcp/tools/shared/draftBatch.js +58 -0
  70. package/dist/modules/mcp/tools/shared/draftBatch.test.d.ts +1 -0
  71. package/dist/modules/mcp/tools/shared/lintDraft.d.ts +30 -0
  72. package/dist/modules/mcp/tools/shared/lintDraft.js +142 -0
  73. package/dist/modules/mcp/tools/shared/lintDraft.test.d.ts +1 -0
  74. package/dist/modules/mcp/tools/shared/tool.d.ts +4 -0
  75. package/dist/modules/mcp/tools/shared/validator/bindings.d.ts +0 -4
  76. package/dist/modules/mcp/tools/shared/validator/bindings.js +1 -60
  77. package/dist/modules/mcp/tools/shared/validator/elements.js +3 -15
  78. package/dist/modules/mcp/tools/shared/validator/index.d.ts +1 -4
  79. package/dist/modules/mcp/tools/shared/validator/index.js +25 -20
  80. package/dist/modules/mcp/tools/shared/validator/interactions.js +3 -58
  81. package/dist/modules/mcp/tools/tryFunction.d.ts +18 -0
  82. package/dist/modules/mcp/tools/tryFunction.js +43 -0
  83. package/dist/modules/mcp/tools/validate.d.ts +34 -0
  84. package/dist/modules/mcp/tools/validate.js +9 -26
  85. package/dist/modules/mcp/types/aiSchema.d.ts +8 -1
  86. package/dist/modules/mcp/types/previewTypes.d.ts +6 -0
  87. package/dist/modules/mcp/types/screenshotTypes.d.ts +16 -0
  88. package/dist/modules/mcp/types/toolTypes.d.ts +5 -1
  89. package/dist/preview/createPreview.js +5 -4
  90. package/dist/stages/preview.js +15 -1
  91. package/package.json +11 -10
  92. package/skills/plitzi-render/SKILL.md +1 -2
  93. package/dist/modules/mcp/tools/shared/validator/audit.d.ts +0 -4
  94. package/dist/modules/mcp/tools/shared/validator/audit.js +0 -127
@@ -1,5 +1,8 @@
1
+ import { BUILTIN_TRANSFORMERS, GLOBAL_SOURCES } from "@plitzi/sdk-authoring";
1
2
  //#region src/modules/mcp/helpers/guide.ts
2
- var serverInstructions = "Plitzi AI server: read-then-write editing of a Plitzi space. Reads follow a filesystem model — list cheap, read one item in detail on demand; never fetch a whole tree you do not need. Workflow: (1) read plitzi://primer/{env} once — it bundles the guide, types, css-properties and page/definition/variable summaries in a single call; (2) plitzi_search with include:\"detail\" to jump to elements — each hit then carries its uri, stateVersion AND full style/resolvedStyle, so an edit needs no per-element read; open a page skeleton or element only when you need its tree/detail (the skeleton already lists the style classes of each node, and plitzi_read fetches many uris at once); (3) plitzi_apply with dryRun to preview a batch; (4) plitzi_apply to persist, passing expectedResourceVersions to guard against concurrent edits — apply and search both hand back the versions you need for the next edit. Use patchElement / patchDefinition to change only some props / CSS (the upsert variants replace them all). An element read (and search include:\"detail\") inlines the CSS of the definitions it attaches under resolvedStyle, so you rarely need a separate definition read. Every element has ONE name and it IS its id ([A-Za-z0-9_-] starting with a letter, unique, chosen by you) — the same name is the runtime wiring key, so a provider source is `<type>_<name>.<field>`, visible to the provider’s DESCENDANTS only (bind inside its subtree). CSS is plain kebab-case — write shorthands freely (`border: 1px solid red`, `padding: 8px 16px`, `font: bold 16px/1.5 Arial`), they are expanded to longhands for you and STORED that way, so read-back shows the longhands; style vars are var(--name), schema vars are {{name}}. READERS — do not confuse them: MCP *resources* are the browsable catalog (list them, or open one by URI); plitzi_search FINDS elements by label/type/attribute and hands you the name to write ops against; plitzi_read BATCH-fetches URIs you already hold. Reach for search/read to work; browse resources to discover. CMS / API integrations (Strapi, WordPress, Contentful, any REST service) go through a CONNECTOR: a manifest you author declaring the base URL, endpoints and auth template, which the SERVER executes — read plitzi://connector-presets for working ones and plitzi://connectors/{env} for this space’s. You never see or create the credential; the space owner attaches it, and the connector saves without one. An apiContainer reads through it only with runtime:\"server\". Elements also carry applied style variants + visibility (initialState), data bindings and interaction flows: edit them with patchElement (initialState), upsertBinding/patchBinding/deleteBinding, and upsertInteractionFlow/patchInteractionNode/deleteInteraction. An element read shows all three plus availableVariants (which variant each of its classes offers). Separately, to SHOW the user a small self-contained widget (offline, no space or backend) instead of editing the space — a card, hero, pricing table, a visual answer — use plitzi_render; read plitzi://render/guide for it.";
3
+ var GLOBALS = GLOBAL_SOURCES.map((source) => `\`${source}\``).join(", ");
4
+ var OTHER_TRANSFORMERS = Object.keys(BUILTIN_TRANSFORMERS).filter((action) => action !== "twigTemplate").map((action) => `\`${action}\``).join(", ");
5
+ var serverInstructions = "Plitzi AI server: read-then-write editing of a Plitzi space. Reads follow a filesystem model — list cheap, read one item in detail on demand; never fetch a whole tree you do not need. Workflow: (1) read plitzi://primer/{env} once — it bundles a quickstart of the guide, types, css-properties and page/definition/variable summaries in a single call; (2) plitzi_search with include:\"detail\" to jump to elements — each hit then carries its uri, stateVersion AND full style/resolvedStyle, so an edit needs no per-element read; open a page skeleton or element only when you need its tree/detail (the skeleton already lists the style classes of each node, and plitzi_read fetches many uris at once); (3) plitzi_apply with dryRun to preview a batch; (4) plitzi_apply to persist, passing expectedResourceVersions to guard against concurrent edits — apply and search both hand back the versions you need for the next edit. Use patchElement / patchDefinition to change only some props / CSS (the upsert variants replace them all). An element read (and search include:\"detail\") inlines the CSS of the definitions it attaches under resolvedStyle, so you rarely need a separate definition read. Every element has ONE name and it IS its id ([A-Za-z0-9_-] starting with a letter, unique, chosen by you) — the same name is the runtime wiring key, so a provider source is `<type>_<name>.<field>`, visible to the provider’s DESCENDANTS only (bind inside its subtree). CSS is plain kebab-case — write shorthands freely (`border: 1px solid red`, `padding: 8px 16px`, `font: bold 16px/1.5 Arial`), they are expanded to longhands for you and STORED that way, so read-back shows the longhands; style vars are var(--name), schema vars are {{name}}. READERS — do not confuse them: MCP *resources* are the browsable catalog (list them, or open one by URI); plitzi_search FINDS elements by label/type/attribute and hands you the name to write ops against; plitzi_read BATCH-fetches URIs you already hold. Reach for search/read to work; browse resources to discover. CMS / API integrations (Strapi, WordPress, Contentful, any REST service) go through a CONNECTOR: a manifest you author declaring the base URL, endpoints and auth template, which the SERVER executes — read plitzi://connector-presets for working ones and plitzi://connectors/{env} for this space’s. You never see or create the credential; the space owner attaches it, and the connector saves without one. An apiContainer reads through it only with runtime:\"server\". Elements also carry applied style variants + visibility (initialState), data bindings and interaction flows: edit them with patchElement (initialState), upsertBinding/patchBinding/deleteBinding, and upsertInteractionFlow/patchInteractionNode/deleteInteraction. An element read shows all three plus availableVariants (which variant each of its classes offers). Build pages people can use without sight: screen readers and browser agents (Claude in Chrome) work a page through its accessibility tree, so every control needs words and clicks go on buttons and links — see Accessibility in plitzi://guide, and plitzi_screenshot view:\"accessibility\" to check. Separately, to SHOW the user a small self-contained widget (offline, no space or backend) instead of editing the space — a card, hero, pricing table, a visual answer — use plitzi_render; read plitzi://render/guide for it.";
3
6
  var widgetsOnlyInstructions = "Plitzi widget server (widgets-only connection). This connection carries NO Plitzi space: nothing can be read or edited in one, and the space tools are not offered here — do not look for them. What it does do is build self-contained UI widgets fully offline, with no backend, account or setup: call plitzi_render to SHOW the user a real rendered layout (card, hero, pricing table, checklist, form, gallery) instead of describing one. Read plitzi://render/guide first — the element/prop table, the style model and a worked example; plitzi://render/types lists every element type you can author (plitzi_read fetches both). To change a widget you already rendered, call plitzi_render again with patch:true and its renderId. If the user wants to edit their real Plitzi space from here, that is a reconnection they make: the integration must be re-authorized and granted a space (the consent screen lists theirs) — you cannot do it from this connection, and no retry will change it.";
4
7
  var guideQuickstart = `# Plitzi AI MCP — quickstart
5
8
  This is the condensed guide; read \`plitzi://guide\` for the full reference (every resource, op and example).
@@ -35,7 +38,7 @@ still \`display: flex\` + \`flex-direction\`/\`align-items\`/…, not a \`flex\`
35
38
 
36
39
  **Data bindings** (\`upsertBinding\`, category attributes|style|initialState): connect a \`source\` to a \`to\` field.
37
40
  A source \`<type>_<name>\` is scoped to the provider's **DESCENDANTS only** — bind inside the provider's subtree
38
- (module sources state/space/navigation/auth/collection are global). \`apiContainer.mockData\` is builder-only; set a
41
+ (the global sources ${GLOBALS} are bindable anywhere). \`apiContainer.mockData\` is builder-only; set a
39
42
  real \`query\` for production. \`transformers: [{action, params}]\` post-process the value — use exact action names
40
43
  from \`plitzi://data-sources\`; \`twigTemplate\` formats it (the value is \`{{source}}\`, not \`{{value}}\`). \`when\` is
41
44
  a QueryBuilder RuleGroup gating the binding.
@@ -46,6 +49,8 @@ computed for you). Node types: \`callback\` (an element's own callback — \`ele
46
49
  (category/key/value/revertOnFinish) ≠ global \`setState\` (source \`state\`, key/type/value). To turn a step off use
47
50
  \`patchInteractionNode {enabled:false}\` — \`deleteInteraction\` removes it (destructive; confirm first). Any param
48
51
  **value** can be a binding token \`{{ source }}\` (e.g. notification \`content: "{{ list_<name>.item.name }}"\`).
52
+ Steps read the page as it is when they run; a refiring trigger is ignored unless \`whileRunning: "queue"\`; keyboard
53
+ shortcuts are the \`onKey\` trigger (\`keys: "shift+f, escape"\`).
49
54
 
50
55
  **CMS / API integrations** (\`upsertConnector\`): a **connector** is a manifest declaring a provider's base URL,
51
56
  endpoints, auth template and filter operators, executed by the **server** — so integrating Strapi, WordPress,
@@ -62,11 +67,16 @@ slugs prepend the path). A \`:name\` segment (\`"posts/:postId"\`) is a route pa
62
67
  source \`navigation.routeParams.name\` → build dynamic pages this way. To move between pages **prefer the \`Link\`
63
68
  element** (a container: \`mode\` "page"/"internal"/"external") over a \`navigate\` interaction.
64
69
 
65
- **Touched resources must be malformation-free.** Editing an element/definition also checks its CURRENT stored content
66
- and BLOCKS the save on any \`Pre-existing malformation in <resource>\` error (a broken transformer, malformed node,
67
- invalid CSS) — even parts you did not touch. These are NOT from your change (the message says so); fix them in the
70
+ **Accessible by default:** screen readers and browser agents (Claude in Chrome) read the accessibility tree, so every
71
+ button/link needs words (an icon-only button a \`title\`), every field a \`label\` (\`hideLabel: true\` hides it), every
72
+ image an \`alt\` or \`decorative: true\`; clicks go on a \`button\`/\`link\`, never a container; headings step down one
73
+ level at a time. \`plitzi_screenshot view:"accessibility"\` shows the tree and what has no name.
74
+
75
+ **Touched elements must be malformation-free.** Editing an element also checks its CURRENT stored content and BLOCKS
76
+ the save on any \`Pre-existing malformation in element …\` error (a broken transformer, a malformed step, an attribute
77
+ it never reads) — even parts you did not touch. These are NOT from your change (the message says so); fix them in the
68
78
  SAME batch and re-apply (the check runs on the result, so the fix unblocks it). \`Pre-existing issue\` warnings advise
69
- but do not block.
79
+ but do not block. A broken tree your batch leaves ANYWHERE (an element orphaned by a delete) blocks it too.
70
80
 
71
81
  Read \`plitzi://guide\` before anything above is unclear.
72
82
  `;
@@ -101,6 +111,8 @@ Never download a whole tree you do not need.
101
111
  \`link\` navigates between pages, \`list\` repeats a template over a data array. \`plugin\` types are custom elements.
102
112
  - \`plitzi://css-properties\` — valid kebab-case CSS property keys.
103
113
  - \`plitzi://schema/{env}/pages\` — page **summaries** (ref, label, elementCount, folder). No element trees.
114
+ - \`plitzi://schema/{env}/layouts\` — the shared **layout shells** (header/sidebar/footer) and the pages rendered
115
+ inside each. Read one like a page. See *Shared layouts* below.
104
116
  - \`plitzi://folders/{env}\` — page **folders** (the sidebar tree): ref, name, slug, parentId. \`/{ref}\` for one.
105
117
  - \`plitzi://schema/{env}/pages/{ref}\` — one page as a **skeleton tree**: each node is \`ref/type/label\` **plus the
106
118
  style classes it attaches** (\`base\`, and \`slots\` for non-base slots) — names only, no CSS. So you can map every
@@ -119,6 +131,7 @@ Never download a whole tree you do not need.
119
131
  - \`plitzi://global-styles/{env}\` — element **types** that have a site-wide global style. \`/{componentType}\` for one.
120
132
  - \`plitzi://id-styles/{env}\` — DOM **ids** that have an id rule (\`#id\`) targeting a single element. \`/{targetId}\` for one.
121
133
  - \`plitzi://style-variables/{env}\` — design tokens by category. \`/{category}\` for one.
134
+ - \`plitzi://fonts/{env}\` — the font **families the space loads**, with their source and fallback. \`/{family}\` for one.
122
135
  - \`plitzi://schema-variables/{env}\` — space-level values referenced in props as \`{{name}}\`.
123
136
  - \`plitzi://settings/{env}\` — space-level settings: the global \`customCss\` and the state/auth (user-provider) config.
124
137
  - \`plitzi://interactions/{env}\` — interaction **actions** observed in this space (grouped by node type): the
@@ -132,6 +145,16 @@ Never download a whole tree you do not need.
132
145
  filter operators and its published fields. \`/{ref}\` opens one manifest in full. See *Connectors* below.
133
146
  - \`plitzi://actions/{env}\` — the **server actions this space has**: what starts each one, who may run it, and the
134
147
  input/output contract a caller is held to. \`/{ref}\` opens one flow in full. See *Server actions* below.
148
+ - \`plitzi://actions/{env}/tasks\` — the **server tasks this deployment can run**, each with its params: the steps an
149
+ action may be built from.
150
+ - \`plitzi://functions/{env}\` — the space's **functions**, its own server code: which files, and the tasks, routes and
151
+ hosts they declare. \`plitzi://functions/{env}/{+path}\` reads one file whole. See *Functions* below.
152
+ - \`plitzi://changes/{env}\` — the space's **change history**, read-only and newest first: who made each change (a
153
+ person in the builder, an agent, the autofix), when, and every element, class, token or font it touched, whole, before
154
+ and after. \`/{id}\` for the history of one of them — how you find what changed an element you are asked about, or
155
+ review what you or another agent just did.
156
+ - \`plitzi://render/guide\`, \`plitzi://render/types\` — the manual and the type table for \`plitzi_render\` (offline
157
+ widgets, not this space).
135
158
 
136
159
  The style resources also answer under the \`plitzi://schema/{env}/…\` root as aliases — \`plitzi://schema/{env}/definitions/{ref}\`, \`plitzi://schema/{env}/style-variables/{category}\`, \`plitzi://schema/{env}/schema-variables\` — but prefer the ready-made \`uri\` from search / a write response over hand-building either form.
137
160
 
@@ -161,7 +184,7 @@ element read is the exception. Search also matches **pages** by name/slug (retur
161
184
  stateVersion) and returns any **style definitions** whose name matches the query, with full CSS, under \`definitions\`.
162
185
  When you do hold several refs to open (e.g. from a skeleton), read them together with \`plitzi_read\` rather than one at a time.
163
186
 
164
- ## Tools (write)
187
+ ## Tools
165
188
  - \`plitzi_validate\` — check a batch, returns teachable errors/warnings. Writes nothing. Also reports **pre-existing
166
189
  malformations** in any resource the batch touches (see below).
167
190
  - \`plitzi_apply\` — validate → apply → persist atomically. Rejects the whole batch on any error or conflict. Pass
@@ -171,6 +194,13 @@ When you do hold several refs to open (e.g. from a skeleton), read them together
171
194
  - \`plitzi_read\` — read many resource **uris in one batch** (pages, elements, definitions, variables). Pass the
172
195
  ready-made uris from search / a write response; each result is \`{ uri, stateVersion, data }\` or a teachable error,
173
196
  so one bad uri never fails the batch. Use it instead of N single reads whenever you already hold several refs.
197
+ - \`plitzi_preview\` — render a page to HTML, optionally with unsaved \`operations\` applied, to check structure.
198
+ - \`plitzi_screenshot\` — render a page to a PNG (desktop, mobile or \`both\`), optionally with unsaved \`operations\`:
199
+ the way to SEE overflow, misalignment and broken layout before you commit. \`view: "accessibility"\` reads the same
200
+ page as a screen reader and a browser agent (Claude in Chrome) do — its accessibility tree as an outline of roles and
201
+ names, with every control and picture that has no name listed in \`unnamed\` — as text, far cheaper than an image
202
+ (\`"both"\` returns both). Both need the SSR render service; where it is missing they answer \`PREVIEW_UNAVAILABLE\`.
203
+ - \`plitzi_render\` — a different job: show the user a self-contained widget built offline. It never touches the space.
174
204
 
175
205
  ## Readers: resources vs plitzi_search vs plitzi_read (do not confuse them)
176
206
  Three ways to read, each for a different moment — pick by what you have in hand:
@@ -215,6 +245,15 @@ reports it only when it is set.
215
245
  every interaction target across the space that named the old one is repointed with it, so nothing comes unwired.
216
246
  You do not have to rewrite them.
217
247
 
248
+ ## Elements — create, move, delete
249
+ - \`upsertElement\` creates an element (or replaces one by name) under \`parentRef\` — the page root when omitted — and
250
+ builds a whole subtree in one op through nested \`children\`. \`patchElement\` changes an existing one in place.
251
+ - \`moveElement { pageRef, ref, toParentRef, position }\` moves or reorders one: \`position\` \`"inside"\` (the default)
252
+ nests it in \`toParentRef\`; \`"before"\`/\`"after"\` make it that element's sibling. Its subtree moves with it.
253
+ - \`deleteElement { pageRef, ref }\` removes an element **and everything under it**. A binding or an interaction
254
+ elsewhere that named one of them is left pointing at nothing, and the save reports it — delete them in the same
255
+ batch. Destructive: confirm first.
256
+
218
257
  ## Styling (crosses both schemas)
219
258
  - **Mind the type's intrinsic default style.** A type renders with a base CSS *before* any class is attached — read
220
259
  it from \`defaultStyle\` on the type in \`plitzi://types\` (the primer includes it). Do not assume \`display: block\`:
@@ -290,6 +329,8 @@ An element read reports \`availableVariants\` (which variant each attached class
290
329
  \`initialState\`, so you can see a button **has** a \`primary\` variant and whether it uses it. If the user asks for a
291
330
  variant that does not exist yet, **create it (upsertDefinition variants) and apply it in the same batch**.
292
331
  - \`initialState.visibility\` (boolean) sets whether the element starts shown or hidden.
332
+ - The \`hidden\` state of a class is how it looks while hidden — where it goes and where it comes from. With a
333
+ \`transition\` that includes \`display 200ms allow-discrete\`, it fades or slides in and out instead of blinking.
293
334
 
294
335
  **Ancestor conditions** — a class that changes inside an ANCESTOR (\`.toolbar .icon\`), or while that ancestor is
295
336
  hovered or in a variant (\`.card:hover .icon\`, a collapsed sidebar hiding its labels) is \`ancestors\` on that class,
@@ -297,23 +338,42 @@ keyed by a class the ancestor wears: \`ancestors: { "toolbar": { "desktop": {
297
338
  { "desktop": { … } } } }, "sidebar": { "variants": { "collapsed": … } } }\`.
298
339
  Any ancestor counts, not only the parent; the class's own states and variants win over these. Never customCss for it.
299
340
 
341
+ ## Variables, design tokens and fonts
342
+ - **Schema variables** are space-wide values a prop reads as \`{{name}}\`: \`upsertVariable { name, variableType,
343
+ value, category?, subValues? }\` (\`variableType\` is the runtime type — \`text\`, \`number\`, … — not the op's
344
+ \`type\`), \`deleteVariable { name }\`.
345
+ - **Design tokens** are CSS values a definition reads as \`var(--name)\`: \`upsertStyleVariable { category, name,
346
+ value }\` with \`category\` one of \`color\`, \`spacing\`, \`shadow\`, \`custom\` and \`name\` without the \`--\`. A
347
+ \`value\` may be one string or \`{ light, dark }\`, so a colour follows the theme. \`deleteStyleVariable\` removes one.
348
+ Reach for a token whenever a value repeats across classes — a hardcoded colour is a colour the theme cannot change.
349
+ - **Fonts**: a \`font-family\` only renders in its face when the space loads that family. \`upsertFont { family,
350
+ source, fallback, weights? }\` declares one — \`source\` \`google\` needs nothing else, \`remote\` a \`stylesheet\` or
351
+ \`files\` URL, \`system\` loads nothing. \`deleteFont { family }\` stops loading it; rules that still name it render
352
+ in their fallback.
353
+
300
354
  ## Data bindings
301
355
  Connect a data **source** to an element field. A binding is \`{ to, source, transformers?, when?, enabled? }\` grouped
302
356
  by **category**: \`attributes\` (a prop), \`style\` (a style value), \`initialState\` (an initial-state key).
303
357
  - \`upsertBinding\` adds one, or replaces the binding already feeding the same \`to\` (or \`id\`).
304
358
  - \`patchBinding\` edits an existing one (matched by \`to\`/\`id\`); \`deleteBinding\` removes it.
305
- Discover valid source paths **and the transformer catalog** in \`plitzi://data-sources/{env}\`. Example — feed an API
306
- list into a list element:
359
+ Discover valid source paths **and the transformer catalog** in \`plitzi://data-sources/{env}\`. Example — feed a
360
+ provider's records into a list element:
307
361
  \`{ "type": "upsertBinding", "pageRef": "home", "ref": "myList", "category": "attributes",
308
- "binding": { "to": "items", "source": "apiContainer_x.data" } }\`.
362
+ "binding": { "to": "items", "source": "apiContainer_products.records" } }\`.
363
+
364
+ **What an \`apiContainer\` publishes** depends on where it reads. Through a connector (\`runtime: "server"\`) it is the
365
+ fixed shape under *Connectors* — \`.records\` or \`.record\`, \`.pageInfo\`, … With a browser \`query\` it is the
366
+ response: \`.data\` (the parsed body — an API answering \`{ "items": [...] }\` is read as
367
+ \`apiContainer_<name>.data.items\`) and \`.status\` (the HTTP status). Both add \`.isLoading\`, \`.isEmpty\`,
368
+ \`.hasError\` and \`.errorMessage\`.
309
369
 
310
370
  **Source scope — a source is visible to the provider's DESCENDANTS only.** An element source named
311
371
  \`<type>_<name>\` (e.g. \`apiContainer_products\`, \`list_food-list\`) is published by that element into the scope of
312
372
  its **subtree**, so **only elements INSIDE the provider can bind to it**. Binding a sibling or an unrelated element
313
- to it resolves to nothing at runtime. So to consume \`apiContainer_products.data\`, the bound element must live under
373
+ to it resolves to nothing at runtime. So to consume \`apiContainer_products.records\`, the bound element must live under
314
374
  that apiContainer; inside a \`list\`, the repeated \`listItem\` and its children read the per-row source
315
- (\`list_<name>.item.<field>\`). Module sources (no \`<type>_<name>\` head — \`state\`, \`space\`, \`navigation\`,
316
- \`auth\`, \`collection\`) are global and bindable anywhere. Binding an element to an element source outside its
375
+ (\`list_<name>.item.<field>\`). The global sources — ${GLOBALS} — have no \`<type>_<name>\` head and are
376
+ bindable anywhere. Binding an element to an element source outside its
317
377
  provider's subtree is schema-valid but **broken at runtime** (the source is not in scope), so
318
378
  \`plitzi_validate\`/\`plitzi_apply\` treat it as an **error and reject the batch** — move the element under the
319
379
  provider, or bind a source that is in scope.
@@ -332,9 +392,8 @@ show a number with units:
332
392
  \`{ "type": "upsertBinding", "pageRef": "home", "ref": "food-item-time", "category": "attributes",
333
393
  "binding": { "to": "content", "source": "list_food-list.item.cookTimeMinutes",
334
394
  "transformers": [ { "action": "twigTemplate", "params": { "template": "{{source}} min de cocción" } } ] } }\`.
335
- Other transformers: \`dateConverter\` (format a date/timestamp), \`capitalize\`, \`stringToArray\` (split on a
336
- separator), \`arrayMap\` (remap the keys of each object in an array), \`staticValue\`. Transformer \`params\` values are
337
- strings. Each transformer also takes an optional \`enabled\` flag: set \`"enabled": false\` to keep it in the chain but
395
+ The others: ${OTHER_TRANSFORMERS} — each one's purpose and params are in \`plitzi://data-sources/{env}\`.
396
+ Transformer \`params\` values are strings. Each transformer also takes an optional \`enabled\` flag: set \`"enabled": false\` to keep it in the chain but
338
397
  skip it at runtime (defaults to true) — the value passes through untouched, and a disabled transformer is not
339
398
  validated.
340
399
 
@@ -349,6 +408,14 @@ An interaction **flow** is a **trigger** (an event like \`onClick\`, \`onPageLoa
349
408
  it runs, in order. You pass the steps **in order** and the stored beforeNode/afterNode/flowId links are computed for
350
409
  you — never wire them by hand. Each step also has an \`enabled\` flag (see disable vs delete below).
351
410
 
411
+ **Each step reads the page as it is when it runs**: a \`when\` or a \`{{ state.x }}\` after a \`setState\` sees the new
412
+ value, and one after a \`delay\` sees what changed meanwhile. To act on the value from BEFORE a write, put the reading
413
+ step first. **A trigger fired again while its flow runs is ignored** (no double submit); set \`"whileRunning": "queue"\`
414
+ on the trigger node for a stream of events that must each run in order (\`"parallel"\` runs them at once).
415
+ **Keyboard shortcuts** are a trigger every element has: \`onKey\` with param \`keys\` — \`"f"\`, \`"shift+f"\`,
416
+ \`"mod+k"\` (⌘ on a Mac, Ctrl elsewhere), \`"plus, ="\`, \`"escape"\`. Heard on the whole page while the element is
417
+ mounted, ignored while someone types in a field; the flow reads the key pressed as \`{{ <trigger id>.key }}\`.
418
+
352
419
  **Node types & \`elementId\`** — a step names which element (or module) provides the callback it runs. Picking the
353
420
  **wrong node type for an action** makes the runtime resolve it against nothing, so the step **silently does nothing**:
354
421
  - \`trigger\` — the event; belongs to the host element. \`elementId\` defaults to the host.
@@ -363,15 +430,16 @@ you — never wire them by hand. Each step also has an \`enabled\` flag (see dis
363
430
  \`setState\` has **no** \`type\` param (that belongs to the global one below). Beside it every element registers
364
431
  **\`toggleState\`** — the same write with **no \`value\`**: it stores the OPPOSITE of what is there
365
432
  (\`category\`, \`key\`, \`revertOnFinish\`). That is how expand/collapse is authored — **one step on one trigger**,
366
- never two \`setState\` branches under complementary \`when\` conditions, which read the state as it was when the
367
- flow STARTED and so are always one step behind. An element type may also register its own extra callbacks.
433
+ never two \`setState\` branches under complementary \`when\` conditions: every step reads the page as it is when it
434
+ runs, so the second sees what the first wrote and flips it back. An element type may also register its own extra
435
+ callbacks.
368
436
  - \`globalCallback\` — a callback provided by a **source module**, NOT by any element: \`addNotification\` (source
369
437
  \`space\`), \`setState\`/\`toggleState\`/\`clearState\` (\`state\`), \`navigate\` (\`navigation\`), \`login\`/\`logout\`/
370
438
  \`refreshDetails\` (\`auth\`), \`runServerAction\`/\`cancelServerAction\` (\`actions\`). Its \`elementId\` is the
371
439
  **source module id**, never the host element — a node that stored the host element's name here would resolve to
372
440
  nothing at runtime. **Omit \`elementId\`**: the MCP sets the correct source and fills the
373
441
  builder's **param defaults** (e.g. \`addNotification\` gets \`autoDismiss:true\`, \`autoDismissTimeout:5000\`,
374
- \`placement:"top-right"\`, \`appeareance:"success"\`) for any params you leave out. Use **only** the params each
442
+ \`placement:"top-right"\`, \`appearance:"success"\`) for any params you leave out. Use **only** the params each
375
443
  callback declares (exact spelling) — for \`addNotification\` the visible text goes in \`content\`; there is **no**
376
444
  \`title\`/\`message\`/\`type\` param, and any unknown key is dropped. See the full param schema for each callback under
377
445
  \`globalCallbacks\` in \`plitzi://interactions/{env}\`.
@@ -439,26 +507,6 @@ server-rendered ignores its connector entirely.
439
507
  - \`plitzi://connectors/{env}\` — the connectors **this space already has**, each with its read/write endpoint names,
440
508
  its filter operators and its published fields. \`plitzi://connectors/{env}/{ref}\` opens one manifest in full.
441
509
 
442
- ### Server actions
443
- Work a page cannot do in the browser — charge a card, send mail, read a system only the server reaches. An action
444
- is a stored flow the SERVER runs; a page names it and never learns what it does. Write one with
445
- \`upsertAction\` / \`patchAction\`, then call it from a page flow with the \`runServerAction\` step (mode
446
- \`await\` to use the result, \`detached\` to fire and carry on).
447
-
448
- Four rules worth knowing before you write:
449
-
450
- - **Steps are server tasks.** \`plitzi://actions/{env}/tasks\` lists what this deployment can run — a browser step
451
- (\`setState\`, \`navigate\`) has nothing to act on here, and the reverse is true too: a task cannot run in a page
452
- flow.
453
- - **The contracts.** \`input\` is coerced and anything undeclared is DROPPED. The OUTPUT is whatever the final
454
- \`flow.output\` step names — there is no separate list to keep in step with it — and that step must be LAST,
455
- since only the last one that runs is answered. Everything else a step produced stays on the server.
456
- - **Whether it runs lives on its TRIGGERS.** Each trigger step carries \`enabled\`, and the action is on when any
457
- way into it is — there is no switch beside the flow. So "pause this action" means disabling its trigger step(s),
458
- never deleting them.
459
- - **A step names the credential it uses**, and sees no other. \`credentials\` on the document says which ones the
460
- action may ask for at all; the secret itself is never yours to see, exactly as with a connector.
461
-
462
510
  ### Credentials are NOT yours to write
463
511
  You never create, see or transmit a secret. Author the manifest with the token in place
464
512
  (\`"value": "Bearer {{credential.token}}"\`) and **leave \`connection.credential\` unset** unless the user hands you an
@@ -514,7 +562,8 @@ The provider element is \`apiContainer\`, and it needs **both halves**:
514
562
  other than \`list\`), \`filters\`, \`limit\`, \`singleRecord\`, \`pagination\`, \`pageParam\`.
515
563
 
516
564
  It then publishes one source **to its descendants only** (like any provider — bind *inside* its subtree):
517
- \`apiContainer_<name>.records\` (an array), \`.pageInfo\` (\`page\`, \`pageCount\`, \`total\`, \`hasNextPage\`…),
565
+ \`apiContainer_<name>.records\` (an array of \`{ id, values }\` — a record's fields are under \`values\`, so a row
566
+ reads \`list_<name>.item.values.<field>\`), \`.pageInfo\` (\`page\`, \`pageCount\`, \`total\`, \`hasNextPage\`…),
518
567
  \`.isEmpty\`, \`.hasError\`, \`.errorMessage\`, \`.isLoading\`. With \`singleRecord: true\` it publishes \`.record\`
519
568
  instead of \`.records\` — that is what a **detail page** uses. Bind an empty-state block's visibility to \`.isEmpty\`
520
569
  and an error block's to \`.hasError\`; they are ordinary bindings, no special mechanism.
@@ -531,7 +580,7 @@ and an error block's to \`.hasError\`; they are ordinary bindings, no special me
531
580
  { "type": "upsertBinding", "pageRef": "blog", "ref": "posts", "category": "attributes",
532
581
  "binding": { "to": "items", "source": "apiContainer_posts-api.records" } },
533
582
  { "type": "upsertBinding", "pageRef": "blog", "ref": "post-title", "category": "attributes",
534
- "binding": { "to": "content", "source": "list_posts.item.title" } } ] }
583
+ "binding": { "to": "content", "source": "list_posts.item.values.title" } } ] }
535
584
  \`\`\`
536
585
 
537
586
  **A detail page** is the same provider with \`singleRecord\` and a filter resolved from the URL. Create the page with a
@@ -542,7 +591,7 @@ route param (\`"slug": "blog/:postSlug"\`), then filter on it — the filter \`v
542
591
  "props": { "connector": "strapi-blog", "resource": "articles", "singleRecord": true,
543
592
  "filters": [ { "field": "slug", "operator": "eq", "value": "{{routeParams.postSlug}}" } ] } } }
544
593
  \`\`\`
545
- Its children then bind to \`apiContainer_post-api.record.<field>\`. A filter whose template resolves to nothing
594
+ Its children then bind to \`apiContainer_post-api.record.values.<field>\`. A filter whose template resolves to nothing
546
595
  returns **no records** rather than the whole collection — a URL that addressed one post never renders a different one.
547
596
 
548
597
  **Paging** is the \`pagination\` prop: \`"none"\`, \`"url"\` (the page number rides the query string, so pages are
@@ -559,6 +608,73 @@ credential and refuses any action the manifest does not declare. Writes exist **
559
608
  builder and renders empty when published. If the user has no SSR deployment, say so rather than leaving them to
560
609
  discover it — the alternative is a browser-side \`query\`, which cannot keep a secret.
561
610
 
611
+ ## Server actions
612
+ Work a page cannot do in the browser — charge a card, send mail, read a system only the server reaches. An action
613
+ is a stored flow the SERVER runs; a page names it and never learns what it does. Write one with
614
+ \`upsertAction\` / \`patchAction\`, then call it from a page flow with the \`runServerAction\` step (mode
615
+ \`await\` to use the result, \`detached\` to fire and carry on).
616
+
617
+ Four rules worth knowing before you write:
618
+
619
+ - **Steps are server tasks.** \`plitzi://actions/{env}/tasks\` lists what this deployment can run — a browser step
620
+ (\`setState\`, \`navigate\`) has nothing to act on here, and the reverse is true too: a task cannot run in a page
621
+ flow.
622
+ - **The contracts.** \`input\` is coerced and anything undeclared is DROPPED. The OUTPUT is whatever the final
623
+ \`flow.output\` step names — there is no separate list to keep in step with it — and that step must be LAST,
624
+ since only the last one that runs is answered. Everything else a step produced stays on the server.
625
+ - **Whether it runs lives on its TRIGGERS.** Each trigger step carries \`enabled\`, and the action is on when any
626
+ way into it is — there is no switch beside the flow. So "pause this action" means disabling its trigger step(s),
627
+ never deleting them.
628
+ - **A step names the credential it uses**, and sees no other. \`credentials\` on the document says which ones the
629
+ action may ask for at all; the secret itself is never yours to see, exactly as with a connector.
630
+
631
+ \`deleteAction { ref }\` removes one; every \`runServerAction\` step that names it stops resolving, so confirm first.
632
+
633
+ ## Functions
634
+ When no server task does what a flow needs — parse a feed, call an API with its own shape, compute something — the
635
+ space can have its own: **functions**, TypeScript the platform builds and runs in a sandbox. Their tasks are steps like
636
+ any other (\`<namespace>.<action>\`), listed in \`plitzi://actions/{env}/tasks\` once saved.
637
+
638
+ - **Write** files with \`upsertFunctionFile { path, content }\` (whole files; \`deleteFunctionFile { path }\` removes
639
+ one). \`index.ts\` default-exports the definition:
640
+ \`export default defineFunctions({ allow: { hosts: ["api.example.com"] }, tasks: [{ namespace, action, title, params, run: async (params, ctx) => … }] })\`,
641
+ \`defineFunctions\` from \`@plitzi/sdk-server/functions\`. Files import each other by relative path — no package,
642
+ no Node built-in.
643
+ - **\`ctx\` is all a function reaches:** \`ctx.kv\` (the space's store), \`ctx.fetch(url, init)\` to the hosts
644
+ \`allow.hosts\` names (\`*.example.com\` for subdomains), \`ctx.publish/grant/revoke\` (channels), \`ctx.log\`,
645
+ \`ctx.user\` (who asked, never their session). A secret is NAMED, never read: \`ctx.fetch(url, { credential: "stripe",
646
+ headers: { authorization: "Bearer {{ credential.apiKey }}" } })\`.
647
+ - **Stateless:** nothing a function keeps in memory outlives the call — a module-level cache or counter starts empty
648
+ every time, on whichever instance runs it. What must last goes in \`ctx.kv\`. A call gets 100 ms of CPU and 10 s.
649
+ - **Saving builds and checks** the files: the batch is refused with each problem where it is (file and line). A
650
+ namespace the platform uses (\`kv\`, \`http\`, \`flow\`…) is refused.
651
+ - **Try it** with \`plitzi_try_function { task, params }\` before a page relies on it: the value, the logs, the error.
652
+ The live site runs what the space was last published with, never the draft. Running functions is part of the paid
653
+ plans: on another plan they save, and a run answers why it did not start — tell the user rather than work around it.
654
+
655
+ ## Realtime channels
656
+ Pages that see each other — cursors, presence, a shared board, a game. Three parts, all needed:
657
+
658
+ 1. **Declare** the topic pattern with \`patchSettings { channels: { "board:{id}": { access: { mode: "public" }, publish: "server" } } }\`.
659
+ \`publish: "server"\` is for what must be validated and saved first; \`"clients"\` (the default) for what pages send
660
+ directly — add \`presence: true\` for who-is-here.
661
+ 2. **Subscribe** with a \`channel\` element whose \`topic\` a pattern matches (\`board:{{ id }}\`, a route param).
662
+ Its descendants bind \`channel_<id>\`: \`connected\`, \`members\`, \`messages\`, \`last\`; flows use its \`onMessage\`,
663
+ \`onJoin\`, \`onLeave\` triggers (\`from\`, \`user\` and the \`state\` the member announced — \`onJoin\` only for who
664
+ came after this page) and its \`publish\` / \`setPresence\` callbacks.
665
+ 3. **Announce from the server**: an action whose last step is the \`realtime.publish\` task — validate, save, then say so.
666
+
667
+ A topic no pattern matches is refused by the server and reported by the linter (\`channel-topic\`).
668
+
669
+ **Private topics** (a room behind a password, a customer's order): declare the pattern \`grant: true\`. The action that
670
+ decides the visitor may be there ends with \`realtime.grant { topic }\` and answers its \`grant\`; the \`channel\`
671
+ element binds \`grant\` to it. Without one the topic is refused however well its name is known (\`channel-grant\`).
672
+ \`realtime.revoke { topic, grant }\` takes one back — or all, naming none — and lets go whoever is on it with it.
673
+
674
+ **Two writers at once**: read-change-write with \`kv.setIf\` (written only if the value is still the one read — empty
675
+ \`expected\` = only if nothing is there yet), ordered lists with \`list.put\` / \`list.range\`, and put
676
+ \`flow.rateLimit\` first in any public action that writes.
677
+
562
678
  ## Shared layouts — the chrome a page does NOT contain
563
679
  A page's tree is usually **not the whole page**. The header, the sidebar and the footer normally live in a **layout
564
680
  container**: a shell several pages are rendered inside, so the navigation is authored once instead of once per page.
@@ -640,15 +756,62 @@ Space-level configuration lives in \`plitzi://settings/{env}\` and is edited wit
640
756
  (merge — only the fields you pass change):
641
757
  - \`customCss\` — **raw global CSS** injected for the whole space. Use it only for genuinely site-wide rules
642
758
  (\`@keyframes\`, \`@font-face\`, resets). To style an element, write a **definition** and attach it — never customCss.
643
- - \`keepState\` / \`stateStorage\` — persist element state across reloads (\`localStorage\`/\`sessionStorage\`).
759
+ - \`keepState\` / \`stateStorage\` — keep the runtime state (\`state.<key>\`, what \`setState\` writes) across reloads,
760
+ in \`localStorage\`/\`sessionStorage\`, filed under whoever is signed in. It is the space's setting — pages do not
761
+ take one. \`transientState\` lists the top-level keys never kept (a filter, a demo, a panel left open); do not
762
+ reset kept state from \`onPageLoad\` instead — it is restored later than that and undoes half of the reset.
763
+ \`paintedState\` lists the kept keys the FIRST PAINT shows (the tool a toolbar shows, a name in an avatar): they go
764
+ in a cookie too, so the server draws with them instead of the page swapping them in after load. Small values only.
644
765
  - **User provider / auth**: \`userProvider\` — \`basic\` for any HTTP+JSON backend, the name of a provider registered
645
766
  in the page, or \`""\` to disable auth — plus \`tokenStorage\`, the \`loginUrl\`/\`userUrl\`/\`refreshUrl\`/\`logoutUrl\`
646
767
  endpoints and the \`detailsPath\`/\`tokenPath\`/\`refreshTokenPath\`/\`expirationTimePath\` mapping that says where the
647
768
  values sit in their responses. \`sessionHintCookie\` is worth setting whenever the backend can: it names a readable
648
769
  cookie carrying only expiries, which is what lets a page answer "nobody is signed in" without a request.
770
+ On the Plitzi platform, \`userProvider: "server"\` signs visitors in with their Plitzi account on the space's own
771
+ host: a \`link\` with \`mode: "external"\` to \`/auth/sign-in?return=/\` starts it, \`authLogout\` ends it.
772
+ - \`visitorRoles\` — what each visitor role gives, \`{ "author": ["postPublish"] }\`: the permissions an action's
773
+ \`access: { mode: "role" }\` and a page's \`can()\` ask for. A visitor holds ONLY the permissions of their roles here,
774
+ never their account's. Who holds a role is given by email in the builder's Visitors panel — never in settings.
775
+ - \`channels\` — the realtime channels, by topic pattern (see **Realtime channels**). Merged pattern by pattern;
776
+ \`null\` removes one.
649
777
  Example — inject a keyframe globally:
650
778
  \`{ "type": "patchSettings", "customCss": "@keyframes spin { to { transform: rotate(360deg); } }" }\`.
651
779
 
780
+ ## Accessibility — pages people, screen readers and browser agents can all use
781
+ A page is read three ways: seen, heard through a screen reader, and worked by a **browser agent** (Claude in Chrome)
782
+ — which finds the controls it can press in the same **accessibility tree** a screen reader walks. A control that
783
+ tree names nothing is invisible to both, however it looks. So build every page to be operable without sight:
784
+ - **Every control has words.** A \`button\` is named by its \`content\` and the words inside it; an icon-only one needs a
785
+ \`title\` (also its tooltip), and one whose words do not say what it does (a key hint, a count) a \`label\`. A \`link\` is named by what it holds, or \`label\` (set it on a link wrapping a whole card).
786
+ A \`formControl\` is named by its \`label\` — keep it and set \`hideLabel: true\` when the design shows what the field
787
+ is (a swatch, a search box); a \`placeholder\` names only a field typed into, and only until something is typed.
788
+ - **Every picture says what it shows** (\`alt\`), or is marked \`decorative: true\` when the words beside it already say
789
+ it. A \`fontAwesome\` icon is decoration (hidden) unless its \`label\` gives it a meaning of its own.
790
+ - **Only controls are clicked.** A click flow belongs on a \`button\` (it holds children — a whole card can be one) or
791
+ a \`link\` when it goes somewhere — never on a \`container\`, \`text\` or \`image\`: a keyboard cannot reach those and
792
+ no agent finds them. An empty backdrop that closes a panel is the exception (the panel's close button and Escape
793
+ are the ways everyone has).
794
+ - **State is said, not only styled.** A toggle sets \`ariaPressed\`, a button that opens a panel \`ariaExpanded\`
795
+ (bind them like any attribute) — an "active" class alone is invisible to both readers.
796
+ - **A dropdown opens from a \`button\`** among its children, outside the popup (an avatar, a "⋯": the button holds it and
797
+ has a \`title\`). The dropdown marks it and moves the focus in and out; a box or an icon alone opens nothing from a
798
+ keyboard.
799
+ - **An illustration** built from elements (a mock of a page, art) is a \`container\` with \`decorative: true\`: both
800
+ readers skip it. Keep controls out of it.
801
+ - **The page has an outline.** Headings go down one level at a time (h1 → h2 → h3; size them with their class, not
802
+ their level). Landmarks are containers with a tag — \`header\`, \`nav\`, \`main\`, \`aside\`, \`footer\` — and a
803
+ \`label\` when a page has two of one kind; a \`section\` with a \`label\` is a region an assistant can jump to.
804
+ - **What comes built in:** modals and dialogs are \`dialog\`s named by their title, take the focus, keep Tab inside
805
+ and close on Escape; tabs are a tab list worked with the arrow keys; a field that breaks a rule is marked invalid and
806
+ described by its message; the pager marks the current page. Nothing to add for those.
807
+ - **A canvas is a picture** to both readers. A plugin that draws (a chart, a board, a map) also renders what it shows
808
+ as real elements — a list, a table — with a button for every action; one hidden until focused cannot be clicked by an
809
+ agent that clicks by position, so offer a visible "list view".
810
+ The linter warns about these (\`control-without-name\`, \`image-without-alt\`, \`click-on-static-element\`,
811
+ \`dropdown-without-control\`, \`heading-level-skipped\`, \`label-ignored\`, \`control-in-decorative\`) on every element
812
+ you touch; fix them in the same batch. Then look:
813
+ \`plitzi_screenshot\` with \`view: "accessibility"\` shows the page as those readers get it, and lists what has no name.
814
+
652
815
  ## Semantics
653
816
  - **props are fully replaced** on \`upsertElement\`: send every prop you want to keep. To change only some props,
654
817
  use **\`patchElement\`** — it merges \`props\`/\`style\` onto the existing element (listed keys change, \`null\` unsets
@@ -660,15 +823,16 @@ Space-level configuration lives in \`plitzi://settings/{env}\` and is edited wit
660
823
  Example — recolor one definition without resending it: \`{ "type": "patchDefinition", "ref": "btn-x",
661
824
  "desktop": { "background-color": "#111" } }\`.
662
825
  - **Atomic batches**: if any operation fails, \`plitzi_apply\` persists nothing.
663
- - **Every resource you touch must be malformation-free — pre-existing errors block the save.** When your batch edits
664
- an element (or a definition/global/id style), the validator also checks the resource's **current stored content**
665
- for malformations — a broken transformer action, a malformed interaction node, invalid CSS — even in parts your
666
- edit does not touch. Such a finding is reported as a \`Pre-existing malformation in <resource>: …\` **error**, and
826
+ - **Every element you touch must be malformation-free — pre-existing errors block the save.** When your batch edits
827
+ an element, the result is read by the same linter every writer of a space is held to, and the element's **current
828
+ stored content** with it — a broken transformer action, a malformed step, an attribute it never reads — even in
829
+ parts your edit does not touch. Such a finding is reported as a \`Pre-existing malformation in element …\` **error**, and
667
830
  \`plitzi_apply\` rejects the batch until it is fixed. These are **not caused by your change** (the message says so)
668
831
  — do not be confused; fix them **in the same batch** and re-apply. Because the check runs on the resulting state,
669
832
  including the fix in your batch is exactly what unblocks the save. (Advisory issues — an unobserved source/action
670
833
  name that may still be a valid plugin, a binding target a plugin manifest does not list — come back as
671
- \`Pre-existing issue …\` **warnings** and do not block.)
834
+ \`Pre-existing issue …\` **warnings** and do not block.) Elements you do not touch are not held against you — except
835
+ that a broken tree your batch leaves anywhere (an element orphaned by a delete) is refused.
672
836
  - **Optimistic concurrency — read before you write, and prove your read is current.** Editing a resource means you
673
837
  read it first, so you hold its \`stateVersion\`. **Always pass \`expectedResourceVersions\`** (URI → the stateVersion
674
838
  you read) for every resource your batch changes. If another agent edited it in the meantime, the live version no
@@ -1,6 +1,6 @@
1
1
  import { AIBinding, AIBindings, AIInteractionFlow, AIInteractionNodeType } from '../types';
2
2
  import { RuleGroup } from '@plitzi/plitzi-ui/QueryBuilder';
3
- import { ElementBinding, ElementDefinition, ElementInteraction } from '@plitzi/sdk-shared';
3
+ import { ElementBinding, ElementDefinition, ElementInteraction, WhileRunning } from '@plitzi/sdk-shared';
4
4
  /** The node shape agents supply on write: same as AIInteractionNode but `id` is optional (generated when
5
5
  * omitted). Kept structural so the tool-layer zod type assigns to it without importing the tool layer here. */
6
6
  export interface FlowNodeInput {
@@ -11,6 +11,7 @@ export interface FlowNodeInput {
11
11
  params?: Record<string, unknown>;
12
12
  enabled?: boolean;
13
13
  when?: RuleGroup;
14
+ whileRunning?: WhileRunning;
14
15
  elementId?: string;
15
16
  preview?: Record<string, unknown>;
16
17
  }
@@ -21,6 +21,7 @@ var nodeToAI = (node) => {
21
21
  if (isNonEmptyObject(node.params)) ai.params = node.params;
22
22
  if (!node.enabled) ai.enabled = false;
23
23
  if (node.when !== void 0) ai.when = node.when;
24
+ if (node.whileRunning !== void 0) ai.whileRunning = node.whileRunning;
24
25
  if (node.elementId && node.elementId !== node.id) ai.elementId = node.elementId;
25
26
  if (isNonEmptyObject(node.preview)) ai.preview = node.preview;
26
27
  return ai;
@@ -101,6 +102,7 @@ var materializeFlow = (nodes, ownerId) => {
101
102
  enabled: node.enabled ?? true
102
103
  };
103
104
  if (node.when !== void 0) interaction.when = node.when;
105
+ if (node.whileRunning !== void 0) interaction.whileRunning = node.whileRunning;
104
106
  record[ids[i]] = interaction;
105
107
  });
106
108
  return {
@@ -1,5 +1,5 @@
1
1
  import { AIElementDetail } from '../types';
2
- import { ActionEntry, ActionTaskDescriptor, ComponentCatalog, ConnectorEntry, Element, PageFolder, Schema, Style } from '@plitzi/sdk-shared';
2
+ import { ActionEntry, ActionTaskDescriptor, ComponentCatalog, ConnectorEntry, Element, FunctionsDraft, PageFolder, Schema, Style } from '@plitzi/sdk-shared';
3
3
  /** The working view the tools read and mutate: the two Plitzi schemas (elements + style), which the platform
4
4
  * stores and persists as separate documents (Space model / Style model), plus the space's `connectors` — a third
5
5
  * store (one row per connector) that a provider element points into by identifier. `catalog` is read-only
@@ -13,6 +13,9 @@ export interface Space {
13
13
  /** The server tasks this deployment can run. Read-only reference data like `catalog`, not persisted: it is what
14
14
  * the server has, not what the space owns, and an action authored against a task it lacks cannot run. */
15
15
  actionTasks?: ActionTaskDescriptor[];
16
+ /** The space's own functions — their source files, which copy of them this is, and what they declare. Absent when
17
+ * the deployment offers no functions: then there is nothing to read and every function op is refused. */
18
+ functions?: FunctionsDraft;
16
19
  catalog?: ComponentCatalog;
17
20
  }
18
21
  export declare const cloneSpace: (space: Space) => Space;
@@ -69,8 +72,8 @@ export declare const indexRemoveElements: (schema: Schema, els: Element[]) => vo
69
72
  export declare const indexAddPage: (schema: Schema, page: Element) => void;
70
73
  /** A new layout shell was created. */
71
74
  export declare const indexAddLayout: (schema: Schema, layout: Element) => void;
72
- /** A layout shell and its `descendants` were deleted. */
73
- export declare const indexRemoveLayout: (schema: Schema, layout: Element, descendants: Element[]) => void;
75
+ /** A layout shell and the elements under it (`removed`) were deleted. */
76
+ export declare const indexRemoveLayout: (schema: Schema, layout: Element, removed: Element[]) => void;
74
77
  /** A page and its `descendants` (its non-page elements) were deleted. */
75
78
  export declare const indexRemovePage: (schema: Schema, page: Element, descendants: Element[]) => void;
76
79
  /** A move reparented an element within its page: the page map is unchanged, but the moved element's parent and
@@ -113,7 +116,6 @@ export declare const slugRouteParams: (slug: string) => string[];
113
116
  export declare const routeParamNames: (schema: Schema) => string[];
114
117
  /** Indexed lookup that reflects the runtime reality: a flat id may be dangling (rsc placeholders, stale items). */
115
118
  export declare const elementById: (schema: Schema, id: string) => Element | undefined;
116
- export declare const descendantIds: (schema: Schema, pageRootId: string) => string[];
117
119
  /** Resolve a name to a concrete element within a page subtree (or the page root itself). The name is the element's
118
120
  * key in `flat`; what this adds is the check that it really belongs to this page. */
119
121
  export declare const resolveRef: (schema: Schema, page: Element, ref: string) => Element | undefined;
@@ -1,9 +1,11 @@
1
+ import { descendants } from "@plitzi/sdk-schema/helpers/elementTree";
1
2
  //#region src/modules/mcp/helpers/space.ts
2
3
  var cloneSpace = (space) => ({
3
4
  schema: structuredClone(space.schema),
4
5
  style: structuredClone(space.style),
5
6
  connectors: structuredClone(space.connectors),
6
7
  actions: structuredClone(space.actions),
8
+ ...space.functions ? { functions: structuredClone(space.functions) } : {},
7
9
  ...space.actionTasks ? { actionTasks: space.actionTasks } : {},
8
10
  ...space.catalog ? { catalog: space.catalog } : {}
9
11
  });
@@ -134,15 +136,15 @@ var indexAddLayout = (schema, layout) => {
134
136
  index.pageOf.set(layout.id, layout.id);
135
137
  index.detailCache.clear();
136
138
  };
137
- /** A layout shell and its `descendants` were deleted. */
138
- var indexRemoveLayout = (schema, layout, descendants) => {
139
+ /** A layout shell and the elements under it (`removed`) were deleted. */
140
+ var indexRemoveLayout = (schema, layout, removed) => {
139
141
  const index = cachedIndex(schema);
140
142
  if (!index) return;
141
143
  index.layoutIds.delete(layout.id);
142
144
  const at = index.layoutElements.findIndex((entry) => entry.id === layout.id);
143
145
  if (at >= 0) index.layoutElements.splice(at, 1);
144
146
  index.pageOf.delete(layout.id);
145
- for (const el of descendants) index.pageOf.delete(el.id);
147
+ for (const el of removed) index.pageOf.delete(el.id);
146
148
  index.detailCache.clear();
147
149
  };
148
150
  /** A page and its `descendants` (its non-page elements) were deleted. */
@@ -255,21 +257,6 @@ var routeParamNames = (schema) => {
255
257
  };
256
258
  /** Indexed lookup that reflects the runtime reality: a flat id may be dangling (rsc placeholders, stale items). */
257
259
  var elementById = (schema, id) => schema.flat[id];
258
- /** All element ids belonging to a page subtree (excluding the page root). */
259
- var collectDescendants = (schema, rootId, acc) => {
260
- const el = elementById(schema, rootId);
261
- if (!el) return;
262
- const childIds = el.definition.items ?? [];
263
- for (const childId of childIds) if (elementById(schema, childId)) {
264
- acc.push(childId);
265
- collectDescendants(schema, childId, acc);
266
- }
267
- };
268
- var descendantIds = (schema, pageRootId) => {
269
- const acc = [];
270
- collectDescendants(schema, pageRootId, acc);
271
- return acc;
272
- };
273
260
  /** Resolve a name to a concrete element within a page subtree (or the page root itself). The name is the element's
274
261
  * key in `flat`; what this adds is the check that it really belongs to this page. */
275
262
  var resolveRef = (schema, page, ref) => {
@@ -285,7 +272,7 @@ var orderedChildren = (schema, el) => {
285
272
  /** The page ref an element belongs to. 'unknown' when it has no page ancestor. */
286
273
  var pageRefOfElement = (schema, el) => spaceIndex(schema).pageOf.get(el.id) ?? "unknown";
287
274
  /** Total number of descendant elements under a subtree (excluding the root). */
288
- var descendantCount = (schema, rootId) => descendantIds(schema, rootId).length;
275
+ var descendantCount = (schema, rootId) => descendants(schema.flat, rootId).length;
289
276
  var emptySpaceMessage = "Space data not available";
290
277
  /** The code a space-dependent answer carries when the connection reaches no space, so both the agent and a host UI
291
278
  * can tell "this connection cannot do that" from "the call failed". */
@@ -311,4 +298,4 @@ var ReadOnlyGrantError = class extends Error {
311
298
  }
312
299
  };
313
300
  //#endregion
314
- export { NoSpaceError, ReadOnlyGrantError, cloneSpace, descendantCount, descendantIds, elementById, emptySpace, emptySpaceMessage, findActionEntry, findConnectorEntry, findElementByRef, findFolderByRef, findLayoutByRef, findPageByRef, findRootByRef, folderAncestorIds, getLayoutElements, getPageElements, indexAddElement, indexAddLayout, indexAddPage, indexInvalidateDetails, indexRemoveElements, indexRemoveLayout, indexRemovePage, invalidateIndex, isPageElement, nameOf, noSpaceErrorCode, orderedChildren, pageFoldersOf, pageRefOfElement, pagesUsingLayout, readOnlyGrantErrorCode, readOnlyGrantMessage, resolveRef, routeParamNames, slugRouteParams, slugify, sortFolders, spaceIndex, strOr, unauthorizedSpaceMessage };
301
+ export { NoSpaceError, ReadOnlyGrantError, cloneSpace, descendantCount, elementById, emptySpace, emptySpaceMessage, findActionEntry, findConnectorEntry, findElementByRef, findFolderByRef, findLayoutByRef, findPageByRef, findRootByRef, folderAncestorIds, getLayoutElements, getPageElements, indexAddElement, indexAddLayout, indexAddPage, indexInvalidateDetails, indexRemoveElements, indexRemoveLayout, indexRemovePage, invalidateIndex, isPageElement, nameOf, noSpaceErrorCode, orderedChildren, pageFoldersOf, pageRefOfElement, pagesUsingLayout, readOnlyGrantErrorCode, readOnlyGrantMessage, resolveRef, routeParamNames, slugRouteParams, slugify, sortFolders, spaceIndex, strOr, unauthorizedSpaceMessage };