@ziprack/sdk 0.1.2 → 0.2.0

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.
@@ -0,0 +1,425 @@
1
+ # zipRACK Studio
2
+
3
+ - [What It Is](#what-it-is)
4
+ - [Getting Started](#getting-started)
5
+ - [The Document](#the-document)
6
+ - [Prop Values](#prop-values)
7
+ - [The Block Catalog](#the-block-catalog)
8
+ - [Theme Tokens](#theme-tokens)
9
+ - [The Derived Manifest](#the-derived-manifest)
10
+ - [Validation](#validation)
11
+ - [The Editor](#the-editor)
12
+ - [Custom Code](#custom-code)
13
+ - [Building and Packaging](#building-and-packaging)
14
+ - [The MCP Server](#the-mcp-server)
15
+ - [When Not To Use It](#when-not-to-use-it)
16
+
17
+ <a name="what-it-is"></a>
18
+ ## What It Is
19
+
20
+ Studio is an optional visual builder for `ziprack/1` applets. A typed JSON document is the applet;
21
+ `index.html`, `manifest.json` and the bundle are compiled from it. The same renderer draws the editor
22
+ canvas and the built package, so what an author arranges is what a participant gets.
23
+
24
+ The load-bearing idea is that **the manifest is derived, not written**. Every block declares what it
25
+ contributes — an activity, a collection, an upload, a prompt, a capability — and the compiler walks the
26
+ document and merges those contributions. `undeclared_capability` at runtime, the most common way an
27
+ applet breaks, stops being possible to cause by omission: placing a question block is what declares its
28
+ activity, and there is no second step to forget.
29
+
30
+ ```
31
+ applet.json ──► block registry ──► contributes() walk ──► manifest.json
32
+ │ │
33
+ ├──► @ziprack/studio-runtime ──► editor canvas │
34
+ │ └─► dist/index.html ◄────────┘
35
+ └──► MCP server ──► an AI assistant
36
+ ```
37
+
38
+ Three packages, all installed as one dev dependency:
39
+
40
+ | Package | Contents |
41
+ |---|---|
42
+ | `@ziprack/studio-core` | Document model, prop types, block registry, validator, manifest compiler, theme tokens. No DOM. |
43
+ | `@ziprack/studio-runtime` | The only renderer. Mounts a document into an element and calls `@ziprack/sdk`. |
44
+ | `@ziprack/studio` | The `ziprack-studio` CLI: editor, compiler, packager, MCP server. |
45
+
46
+ <a name="getting-started"></a>
47
+ ## Getting Started
48
+
49
+ ```sh
50
+ npm install --save-dev @ziprack/studio
51
+ npx ziprack-studio init --slug team-pulse
52
+ npx ziprack-studio edit
53
+ ```
54
+
55
+ `init` writes `applet.json` — one screen with a heading, a rating question and a finish button — and a
56
+ `.gitignore` for the generated build inputs. `edit` opens the editor and the live canvas. `package`
57
+ produces the zip you upload:
58
+
59
+ ```sh
60
+ npx ziprack-studio package # dist-zip/team-pulse-0.1.0.zip
61
+ ```
62
+
63
+ | Command | What it does |
64
+ |---|---|
65
+ | `init [dir]` | Start an `applet.json`. `--slug`, `--name`, `--force`. |
66
+ | `edit` | Editor and canvas on `http://localhost:5273`. `--port`, `--host`, `--open`. |
67
+ | `validate` | Check the document and the manifest it derives. Exits non-zero on errors, and on warnings under `--strict`. |
68
+ | `manifest` | Print the derived `manifest.json`. |
69
+ | `build` | Validate, derive, bundle to `dist/`. `--out`, `--keep`. |
70
+ | `package` | Build, then write `dist-zip/{slug}-{version}.zip` with an SBOM. |
71
+ | `mcp` | Run the stdio MCP server. |
72
+
73
+ Every command takes `--project <dir>`; without it, the nearest `applet.json` in a parent directory wins.
74
+
75
+ <a name="the-document"></a>
76
+ ## The Document
77
+
78
+ `applet.json` is a tree of nodes. Each node names a block, holds that block's props, and holds children
79
+ in **named slots**:
80
+
81
+ ```json
82
+ {
83
+ "schema": "ziprack-studio/1",
84
+ "applet": { "slug": "team-pulse", "name": "Team Pulse", "version": "0.1.0" },
85
+ "theme": { "preset": "clean", "tokens": { "color": { "accent": "#2563eb" } } },
86
+ "settings": { "segments": [], "metrics": [], "profile": [] },
87
+ "screens": [{
88
+ "id": "screen_a1b2c3",
89
+ "block": "layout.screen",
90
+ "version": "1",
91
+ "props": { "title": { "$$type": "string", "value": "Welcome" } },
92
+ "slots": {
93
+ "default": [{
94
+ "id": "rating_d4e5f6",
95
+ "block": "question.rating",
96
+ "version": "1",
97
+ "props": {
98
+ "activityId": { "$$type": "activity-id", "value": "confidence.pre" },
99
+ "label": { "$$type": "string", "value": "How confident do you feel?" },
100
+ "scale": { "$$type": "scale", "value": { "min": 1, "max": 6 } }
101
+ },
102
+ "slots": {},
103
+ "style": { "variant": "card", "spacing": "lg" },
104
+ "editor": { "label": "Confidence (pre)" }
105
+ }]
106
+ }
107
+ }]
108
+ }
109
+ ```
110
+
111
+ Slots, rather than one child array, are what make structure checkable without a layout engine:
112
+ `layout.split` has `left` and `right`, and each slot declares which blocks it accepts. Ids are
113
+ prefixed by block kind and checked against every id already in the document, so they stay readable in
114
+ diffs and in error messages.
115
+
116
+ `editor.label` renames a node in the block tree without touching anything a participant sees — the way
117
+ you tell two rating questions apart at a glance.
118
+
119
+ <a name="prop-values"></a>
120
+ ## Prop Values
121
+
122
+ Every prop value is an envelope, primitives included:
123
+
124
+ ```json
125
+ { "$$type": "string", "value": "How confident do you feel?" }
126
+ ```
127
+
128
+ The uniformity buys two things: one generic parser for every prop, and an unambiguous discriminator
129
+ where a slot accepts more than one kind of value. A colour prop takes either a literal or a theme
130
+ token, and the envelope says which without guesswork:
131
+
132
+ ```json
133
+ { "$$type": "color", "value": "#2563eb" }
134
+ { "$$type": "token", "value": "color.accent" }
135
+ ```
136
+
137
+ Four states are distinct, because "leave it alone" and "clear it" are different instructions:
138
+
139
+ | State | Written as | Means |
140
+ |---|---|---|
141
+ | Absent | the key is missing | Use the block's default. |
142
+ | Null | `"scale": null` | Explicit reset back to the default. |
143
+ | Partial null | `{ "$$type": "scale", "value": { "min": 1, "max": null } }` | Reset one leaf, keep the rest. |
144
+ | Disabled | `{ "$$type": "string", "value": "…", "disabled": true }` | Stored, but suppressed at render. |
145
+
146
+ You do not type envelopes by hand. The editor forms write them, and the MCP tools take plain values and
147
+ wrap them for you.
148
+
149
+ <a name="the-block-catalog"></a>
150
+ ## The Block Catalog
151
+
152
+ Each block declares the capabilities it needs, and those are what end up in `requires.capabilities`.
153
+
154
+ | Block | Capability | Contributes |
155
+ |---|---|---|
156
+ | `layout.screen` | — | One screen. Slot: `default`. |
157
+ | `layout.stack`, `layout.card` | — | Grouping. Slot: `default`. |
158
+ | `layout.split` | — | Slots: `left`, `right`. |
159
+ | `content.heading`, `content.prose`, `content.image` | — | — |
160
+ | `content.media` | — | `media[]` |
161
+ | `question.rating`, `question.scale`, `question.text`, `question.choice` | `activity@1` | One `activities[]` entry each |
162
+ | `flow.next`, `flow.complete` | `attempt@1` | Attempt state and completion |
163
+ | `flow.progress` | — | — |
164
+ | `data.form`, `data.list` | `db@1` | `collections[]`, with the JSON Schema and `indexed` paths |
165
+ | `upload.dropzone` | `storage@1` | `uploads[]` |
166
+ | `ai.analysis`, `ai.chat` | `ai@1` | `prompts[]`, with `vars` and `output_schema` |
167
+ | `room.presence`, `room.facilitator` | `broadcast@1` | `broadcast.channels`, `facilitator_events` |
168
+ | `room.board` | `broadcast@1`, `db@1` | Both of the above |
169
+ | `custom.code` | whatever you declare | Whatever you declare |
170
+
171
+ `ziprack-studio mcp` exposes the same list, and the editor's insert menu is generated from it, so there
172
+ is one catalog rather than three.
173
+
174
+ <a name="theme-tokens"></a>
175
+ ## Theme Tokens
176
+
177
+ Styling is a token set plus a handful of per-block variants. It is deliberately not free-form CSS: the
178
+ constraint is what keeps an author's screen legible on a phone, which is where most participants are.
179
+
180
+ `theme.tokens.color.accent` compiles to `--zr-color-accent` on `.zr-root`, and a prop holding
181
+ `{ "$$type": "token", "value": "color.accent" }` renders `var(--zr-color-accent)`. Storage references
182
+ the path, so changing the accent is one edit rather than a document-wide find-and-replace.
183
+
184
+ | Group | Tokens |
185
+ |---|---|
186
+ | `color` | `bg`, `surface`, `text`, `muted`, `border`, `accent`, `accent-text`, `success`, `danger` |
187
+ | `space` | `none`, `sm`, `md`, `lg`, `xl` |
188
+ | `font` | `body`, `heading` |
189
+ | `size` | `text-sm` … `text-2xl`, `measure` |
190
+ | `radius` | `sm`, `md`, `lg`, `pill` |
191
+
192
+ Presets `clean` and `warm` supply the defaults; `theme.tokens` overrides only what you name. Per-node
193
+ `style` carries `variant`, `spacing`, `align` and `width` — a small vocabulary shared by every block,
194
+ rather than keys each block invents.
195
+
196
+ <a name="the-derived-manifest"></a>
197
+ ## The Derived Manifest
198
+
199
+ `manifest.json` is output. You never edit it, and there is nothing to keep in sync:
200
+
201
+ ```sh
202
+ npx ziprack-studio manifest
203
+ ```
204
+
205
+ Each block's `contributes()` returns manifest fragments; the compiler merges them and refuses
206
+ contradictions. Two blocks may share a collection only if their declarations match exactly; an activity
207
+ id used twice is an error, because ids are the column keys in the export. The compiler also records
208
+ which node produced each entry, which is how the editor jumps from a manifest problem to the block that
209
+ caused it.
210
+
211
+ `requires.capabilities` is the union of the blocks in the document. Delete the last question and
212
+ `activity@1` disappears on the next build — worth knowing, since a capability change is what
213
+ [publishing](publishing.md) parks in the review queue.
214
+
215
+ <a name="validation"></a>
216
+ ## Validation
217
+
218
+ ```sh
219
+ npx ziprack-studio validate
220
+ ```
221
+
222
+ Validation is per-key. One bad prop is reported as one bad prop; the rest of the document still
223
+ compiles, and the editor still renders. Every error names the node, the block, the prop path, and
224
+ **the JSON Schema that would have been valid** — the same schema the editor's form is generated from
225
+ and the same one the MCP tools return.
226
+
227
+ Two layers run. Document rules cover prop types, slot membership and structure. Manifest rules are the
228
+ `ziprack/1` rules from [The Manifest](manifest.md), applied to the derived output: segment references,
229
+ metric pre/post pairing, scope implies capability, the eight-indexed-path ceiling, the reserved `$room`
230
+ channel, prompt placeholders against declared vars. Those rules also live in PHP on the Rack, so the
231
+ two implementations are held to a shared fixture corpus that both test suites run — if they ever
232
+ disagree, a test fails rather than an upload.
233
+
234
+ <a name="the-editor"></a>
235
+ ## The Editor
236
+
237
+ ```sh
238
+ npx ziprack-studio edit
239
+ ```
240
+
241
+ A panel and a canvas iframe. The panel holds the screen list, the block tree, a form generated from the
242
+ selected block's prop schemas, and the theme editor. The canvas runs the real runtime with the real
243
+ SDK in mock mode against the derived manifest, so answers are real statements, resume really resumes,
244
+ and two browser tabs really share a `BroadcastChannel` room. Clicking a block in the canvas selects it
245
+ in the tree; the two halves talk only over `postMessage`.
246
+
247
+ Every change — typing in a field, reordering a block, an MCP call — goes through one command bus with
248
+ pre-commit hooks that can refuse it. Slot rules, unique activity ids, scope-implies-capability and the
249
+ reserved channel are enforced in one place, so no entry point can get around them. Undo is a log of
250
+ named reversible operations rather than document snapshots, and a compound edit collapses to one step.
251
+
252
+ Saves are written back to `applet.json` as you work. If you edit the file directly, the editor reloads
253
+ it.
254
+
255
+ <a name="custom-code"></a>
256
+ ## Custom Code
257
+
258
+ When no block fits, `custom.code` points at a module in your own project:
259
+
260
+ ```ts
261
+ // src/blocks/thermometer.ts
262
+ import { h, type RenderContext } from '@ziprack/studio-core';
263
+
264
+ export default function thermometer(ctx: RenderContext) {
265
+ return h('div', { class: 'thermometer' }, h('button', {
266
+ onclick: () => void ctx.rack.answer('mood.check', 5),
267
+ }, 'Warm'));
268
+ }
269
+ ```
270
+
271
+ The editor shows a named, movable box; the compiler bundles the module. This is the one place
272
+ declaration is not automatic, because the compiler cannot see inside your code. Two props carry what it
273
+ cannot infer: `capabilities` lists what the module calls, and `declares` carries the manifest entries it
274
+ needs, in manifest shape.
275
+
276
+ ```json
277
+ {
278
+ "code": { "module": "./src/blocks/thermometer.ts", "export": "default" },
279
+ "label": "Thermometer",
280
+ "capabilities": ["activity@1"],
281
+ "declares": { "activities": [{ "id": "mood.check", "type": "rating", "label": "Mood" }] }
282
+ }
283
+ ```
284
+
285
+ Both halves are enforced. An undeclared capability call is refused at runtime, and a capability declared
286
+ with nothing to go with it fails the build — `activity@1` with no activity is as invalid here as it
287
+ would be in a hand-written manifest. `declares` accepts `activities`, `media`, `collections`,
288
+ `uploads`, `prompts`, `broadcast` and `profile`, and each is checked by the same manifest rules as a
289
+ catalog block's contribution.
290
+
291
+ Because the box stays a node in the tree, an applet with hand-written blocks is still openable in the
292
+ editor. That is the difference between an escape hatch and a one-way export.
293
+
294
+ <a name="building-and-packaging"></a>
295
+ ## Building and Packaging
296
+
297
+ ```sh
298
+ npx ziprack-studio build # dist/
299
+ npx ziprack-studio package # dist-zip/{slug}-{version}.zip
300
+ ```
301
+
302
+ `build` validates, derives and checks the manifest, then emits `dist/` through Vite with `base: './'`,
303
+ because the Rack serves packages from a token-prefixed path and absolute URLs break there:
304
+
305
+ ```
306
+ dist/
307
+ ├── index.html the generated shell
308
+ ├── manifest.json derived
309
+ ├── applet.json the document, so the package can be reopened in the editor
310
+ └── assets/… the runtime, your custom modules, and the compiled theme
311
+ ```
312
+
313
+ That is an ordinary `ziprack/1` package, subject to the same
314
+ [installer rules](applet-packages.md) as a hand-written one.
315
+
316
+ `package` adds a CycloneDX `sbom.json`, falling back to `package-lock.json` if `npm sbom` fails. Bump
317
+ `applet.version` before every upload; a version string installs once per applet.
318
+
319
+ <a name="the-mcp-server"></a>
320
+ ## The MCP Server
321
+
322
+ ```sh
323
+ npx ziprack-studio mcp
324
+ ```
325
+
326
+ Line-delimited JSON-RPC 2.0 over stdio. Point your assistant's MCP configuration at it with the project
327
+ directory as the working directory:
328
+
329
+ ```json
330
+ {
331
+ "mcpServers": {
332
+ "ziprack-studio": {
333
+ "command": "npx",
334
+ "args": ["ziprack-studio", "mcp", "--project", "."]
335
+ }
336
+ }
337
+ }
338
+ ```
339
+
340
+ The point is not that an assistant can write the file — it could do that anyway. The point is that it
341
+ writes through the same registry, the same command bus and the same validator as the editor, so what it
342
+ produces is a document a non-technical colleague can then open and edit.
343
+
344
+ | Tool | Purpose |
345
+ |---|---|
346
+ | `ziprack_list_blocks` | The catalog: id, label, category, `requires`, slots, what it contributes. Start here. |
347
+ | `ziprack_get_block_schema` | One block's prop schemas and defaults, `allowedParents`, each slot's `allowedBlocks`, and guidance on when to use it. |
348
+ | `ziprack_read_document` | The tree, whole or one subtree, summarised below a given `depth`. |
349
+ | `ziprack_compose` | Build a tree from a structure-only skeleton plus content maps. |
350
+ | `ziprack_patch` | `create`, `delete`, `move`, `duplicate`, `props`, `style`, `label`, `settings`, `applet`, `theme`. |
351
+ | `ziprack_validate` | Per-key errors, each with the schema that would have been valid. |
352
+ | `ziprack_derive_manifest` | The manifest this document produces, plus a capability diff against the last build. |
353
+
354
+ The SDK's docs tools (`ziprack_guidelines`, `ziprack_list_docs`, `ziprack_search_docs`,
355
+ `ziprack_read_doc`) are served alongside these. See [the SDK's MCP server](sdk.md#the-mcp-server).
356
+
357
+ `compose` separates structure from content, which is what stops a long generation from corrupting a
358
+ tree. The skeleton is nesting only; props arrive in a map keyed by `configuration-id`:
359
+
360
+ ```xml
361
+ <layout.screen configuration-id="intro">
362
+ <content.heading configuration-id="title" />
363
+ <question.rating configuration-id="confidence" />
364
+ <flow.next configuration-id="go" />
365
+ </layout.screen>
366
+ ```
367
+
368
+ ```json
369
+ {
370
+ "intro": { "title": "Welcome" },
371
+ "title": { "text": "How is the team feeling?" },
372
+ "confidence": { "activityId": "confidence.pre", "label": "How confident do you feel?", "scale": { "min": 1, "max": 6 } },
373
+ "go": { "label": "Start" }
374
+ }
375
+ ```
376
+
377
+ Use `slot="left"` on a child to place it in a named slot, `parent` to append inside an existing node,
378
+ and `replace: true` to rebuild the document from scratch.
379
+
380
+ Three properties hold for every mutating call:
381
+
382
+ - **`dry_run` everywhere.** Same answer, same validation, nothing written.
383
+ - **All or nothing.** One refused operation rolls the whole batch back. Half a composition is worse
384
+ than none.
385
+ - **Refusals carry schemas.** A wrong prop name comes back with the names the block does have; a wrong
386
+ value comes back with the schema that would have been accepted. Correction takes one round trip
387
+ rather than a read, a guess and another write.
388
+
389
+ ```json
390
+ {
391
+ "applied": false,
392
+ "dry_run": false,
393
+ "errors": [{
394
+ "message": "Expected a string.",
395
+ "block": "content.heading",
396
+ "node": "heading_ozbhbm",
397
+ "prop": "level",
398
+ "schema": {
399
+ "type": "object",
400
+ "properties": {
401
+ "$$type": { "type": "string", "const": "level" },
402
+ "value": { "type": "string", "enum": ["1", "2", "3"] },
403
+ "disabled": { "type": "boolean" }
404
+ },
405
+ "required": ["$$type", "value"],
406
+ "additionalProperties": false,
407
+ "default": { "$$type": "level", "value": "1" }
408
+ }
409
+ }]
410
+ }
411
+ ```
412
+
413
+ Successful mutations answer with the validation state of the whole document, not just the part that
414
+ changed, because an edit that leaves the applet unpublishable is worth knowing about immediately.
415
+
416
+ The server and the editor can be open at once. Each picks up the other's writes to `applet.json`.
417
+
418
+ <a name="when-not-to-use-it"></a>
419
+ ## When Not To Use It
420
+
421
+ Studio is one way to build an applet, not the way. It constrains layout and styling on purpose, and
422
+ that is the wrong trade when the interface *is* the work — a custom visualisation, an unusual
423
+ interaction, a design that has to match a brand exactly. For those, write the applet directly against
424
+ [the SDK](sdk.md) and start from `starter/`. A Rack cannot tell the difference: both produce a
425
+ `ziprack/1` package, and both are subject to the same manifest and the same installer.
@@ -0,0 +1,101 @@
1
+ # Versioning
2
+
3
+ - [Three Axes](#three-axes)
4
+ - [The Format String](#the-format-string)
5
+ - [Capability Versions](#capability-versions)
6
+ - [Package Versions](#package-versions)
7
+ - [Version Pinning](#version-pinning)
8
+ - [Applet Versions](#applet-versions)
9
+
10
+ <a name="three-axes"></a>
11
+ ## Three Axes
12
+
13
+ There are three independent version numbers in zipRACK, and coupling any two of them is a mistake.
14
+
15
+ | Axis | Example | Governs | Audience |
16
+ |---|---|---|---|
17
+ | **Format string** | `ziprack/1` | Manifest shape, envelope, installer rules | Applet authors and the validator |
18
+ | **Capability version** | `db@1` | The SDK surface an applet binds to | Applet authors |
19
+ | **Package semver** | `ziprack/db` 2.4.1 | The PHP API: classes, contracts, config keys, migrations | Whoever maintains a Rack |
20
+
21
+ `ziprack/db` can reach 4.0.0 — several breaking PHP refactors — while still implementing `db@1` and
22
+ requiring `ziprack/1`. An applet built two years ago keeps working, because none of what it binds to
23
+ has changed.
24
+
25
+ > A capability version changes **only** when the applet-facing surface changes. Package major
26
+ > versions are free to move independently and frequently.
27
+
28
+ <a name="the-format-string"></a>
29
+ ## The Format String
30
+
31
+ `schema: "ziprack/1"` versions the envelope (`ns`, `v`, ack shape), the core manifest fields, and the
32
+ installer's file rules. A Rack refuses a manifest with any other schema, naming what it speaks.
33
+
34
+ `cn-sim/1` was a single monolithic schema version, which made every new surface a breaking change.
35
+ Splitting the format from the capabilities is what allows surfaces to ship incrementally and allows
36
+ different deployments to run different capability sets — which matters given standalone Racks per
37
+ engagement.
38
+
39
+ <a name="capability-versions"></a>
40
+ ## Capability Versions
41
+
42
+ A manifest declares `id@version` references. A Rack implements a set of them, built by iterating
43
+ installed packages:
44
+
45
+ ```sh
46
+ php artisan ziprack:status
47
+ # format: ziprack/1
48
+ # capabilities: activity@1, ai@1, attempt@1, auth@1, broadcast@1, db@1, storage@1
49
+ ```
50
+
51
+ An applet requesting something outside that set is refused at **install**, not at runtime, with a
52
+ message naming what is implemented.
53
+
54
+ A package may implement more than one version of its capability at once — `db@1` and `db@2` during a
55
+ transition — which is what makes migrating applets across a capability break survivable. The
56
+ versions a package implements are metadata the core reads at boot, never inferred from the package's
57
+ own semver.
58
+
59
+ <a name="package-versions"></a>
60
+ ## Package Versions
61
+
62
+ Ordinary semver on the PHP API, and the thing a Rack maintainer upgrades:
63
+
64
+ ```sh
65
+ composer update ziprack/core
66
+ ```
67
+
68
+ The packages are authored in one monorepo and published as read-only subtree splits on tag — one
69
+ repository per package, which nobody commits to. A change spanning `ziprack/core`'s registry and the
70
+ `ziprack/db` package that registers into it is one PR, one test run, one tag.
71
+
72
+ What a maintainer needs to know about a fix landing everywhere is covered by
73
+ [Conformance](conformance.md) and `/ziprack/health`.
74
+
75
+ <a name="version-pinning"></a>
76
+ ## Version Pinning
77
+
78
+ Pinning is the one behaviour that is not a capability, because it is a policy the Rack applies *to*
79
+ an applet rather than a surface the applet calls. Rather than declare it, zipRACK derives it:
80
+
81
+ > **Requesting `attempt@1` implies pinned. Its absence implies rolling.**
82
+
83
+ Attempts are the only reason pinning exists — activity ids cannot shift mid-attempt without
84
+ corrupting the export — and apps must roll, or people are stranded on stale builds. One rule, one
85
+ capability, nothing for an author to get wrong and no new manifest field. See
86
+ [Attempts](attempts.md#version-pinning) for the authoring consequences.
87
+
88
+ <a name="applet-versions"></a>
89
+ ## Applet Versions
90
+
91
+ Your own `version` field is semver, unique per applet, and uploadable exactly once. Bump it for
92
+ every upload, including test builds — the installer refuses a duplicate rather than overwriting,
93
+ which is deliberate: a version is immutable, so what a participant ran is always recoverable.
94
+
95
+ Between versions:
96
+
97
+ - Changing an activity `id` splits an export column.
98
+ - Changing a `label` changes only the header.
99
+ - Adding a capability or a profile attribute blocks promotion until approved.
100
+ - Adding or removing `attempt@1` flips the applet between pinned and rolling, and is flagged
101
+ prominently in the diff.