@ziprack/sdk 0.1.2 → 0.2.1

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,127 @@
1
+ # Publishing
2
+
3
+ - [Introduction](#introduction)
4
+ - [Installing a Version](#installing-a-version)
5
+ - [Capability Diff Review](#capability-diff-review)
6
+ - [Promotion](#promotion)
7
+ - [The Review Queue](#the-review-queue)
8
+ - [Retiring a Version](#retiring-a-version)
9
+ - [The Audit Log](#the-audit-log)
10
+
11
+ <a name="introduction"></a>
12
+ ## Introduction
13
+
14
+ `cn-sim/1` assumed CN authored every package. Accepting client-authored applets changes the problem
15
+ from untrusted *content* to untrusted *code*, and that calls for process rather than more sandboxing.
16
+
17
+ The pipeline is deliberately boring: every upload lands inactive, a human promotes it, and anything
18
+ that widens what the applet can reach needs an explicit approval first.
19
+
20
+ <a name="installing-a-version"></a>
21
+ ## Installing a Version
22
+
23
+ ```sh
24
+ php artisan ziprack:install dist-zip/my-applet-1.0.0.zip
25
+ php artisan ziprack:install dist-zip/my-applet-1.0.0.zip --activate # install and promote
26
+ ```
27
+
28
+ or `POST /ziprack/admin/versions/upload` with the zip.
29
+
30
+ Installation validates the manifest against this Rack's capability set, records the version, runs
31
+ each declared capability's `onInstall()` hook inside a transaction, and only then extracts files. A
32
+ version arrives as `ready` but **not current**, unless it is the applet's first — there is nothing
33
+ to review a first version against, so it is promoted automatically.
34
+
35
+ Once files are extracted the Rack resolves declared media (the oEmbed lookup) and, when
36
+ `publishing.osv_lookup` is on, queues a batch of the SBOM's purls against OSV.dev. Findings attach
37
+ to the version and surface in the review queue; they do not block the install.
38
+
39
+ <a name="capability-diff-review"></a>
40
+ ## Capability Diff Review
41
+
42
+ **This is the control that actually holds.** When a new version of an existing applet is uploaded,
43
+ the Rack diffs its `requires` block against the current version:
44
+
45
+ ```json
46
+ {
47
+ "added": ["ai@1", "db@1"],
48
+ "removed": [],
49
+ "profile_added": ["business_unit"],
50
+ "profile_removed": [],
51
+ "pinning_changed": true,
52
+ "pinned": true
53
+ }
54
+ ```
55
+
56
+ Any addition — a capability or a profile attribute — blocks promotion until someone approves it.
57
+ This is the Android permissions model, and it catches the realistic attack: a benign v1 that earns
58
+ trust, followed by a v2 that quietly widens its reach.
59
+
60
+ `pinning_changed` is flagged just as prominently, because adding or removing `attempt@1` changes the
61
+ applet's own update semantics from rolling to pinned or back. See
62
+ [Version Pinning](attempts.md#version-pinning).
63
+
64
+ Removals need no approval. Narrowing what an applet can reach is always allowed.
65
+
66
+ <a name="promotion"></a>
67
+ ## Promotion
68
+
69
+ ```sh
70
+ php artisan ziprack:promote my-applet 1.1.0
71
+ php artisan ziprack:promote my-applet 1.1.0 --force # approve the pending review, then promote
72
+ ```
73
+
74
+ or `POST /ziprack/admin/applets/{applet}/versions/{version}/approve` followed by
75
+ `…/promote`.
76
+
77
+ Promotion is refused when the version is not `ready`, or when it has an unapproved capability
78
+ addition — the error names exactly what was added. On success, `current_version_id` moves and each
79
+ capability's `onPromote()` hook runs.
80
+
81
+ What promotion means depends on pinning:
82
+
83
+ - **Rolling applet** (no `attempt@1`): everyone is on the new version at their next page load.
84
+ - **Pinned applet** (`attempt@1`): new attempts start on the new version; in-progress attempts stay
85
+ on the one they started on until they complete. Two versions run side by side until the old one
86
+ drains.
87
+
88
+ <a name="the-review-queue"></a>
89
+ ## The Review Queue
90
+
91
+ `GET /ziprack/admin/review-queue` collects everything awaiting a human decision rather than hiding
92
+ it behind a modal:
93
+
94
+ - Versions requesting new capabilities or profile attributes
95
+ - OSV findings from the SBOM
96
+ - The first upload from a publisher
97
+ - Manifest validation warnings that were overridden
98
+
99
+ A version's own page (`GET /ziprack/admin/applets/{applet}/versions/{version}`) carries the diff,
100
+ the SBOM summary, the findings and the publisher.
101
+
102
+ Each version records an attributable publisher, so a bad version can be traced and that publisher's
103
+ other applets reviewed.
104
+
105
+ <a name="retiring-a-version"></a>
106
+ ## Retiring a Version
107
+
108
+ The kill switch:
109
+
110
+ ```sh
111
+ php artisan ziprack:retire my-applet 1.1.0
112
+ ```
113
+
114
+ A retired version can no longer be launched. In-progress attempts pinned to it land on a holding
115
+ screen — a defined state with an explanation, not a blank frame. Retiring the current version leaves
116
+ the applet with no current version until another is promoted, which is deliberate: the operator
117
+ decides what to fall back to.
118
+
119
+ <a name="the-audit-log"></a>
120
+ ## The Audit Log
121
+
122
+ With `ziprack.audit.enabled` on, capability calls are recorded per applet — type, participant,
123
+ outcome — excluding the high-volume types each capability already persists (`activity:statement`,
124
+ `attempt:state`, `broadcast:heartbeat`). Read it at `GET /ziprack/admin/audit-log`.
125
+
126
+ This is what an incident response actually needs: what the applet asked for, when, and what the Rack
127
+ answered. AI spend and quota usage are reported alongside it per applet.
@@ -0,0 +1,190 @@
1
+ # Reporting
2
+
3
+ - [Composition](#composition)
4
+ - [The Admin API](#the-admin-api)
5
+ - [Who May Do What](#who-may-do-what)
6
+ - [Preview Runs](#preview-runs)
7
+ - [The CSV Export](#the-csv-export)
8
+ - [Metrics](#metrics)
9
+ - [Response Analysis](#response-analysis)
10
+ - [Building a UI](#building-a-ui)
11
+
12
+ <a name="composition"></a>
13
+ ## Composition
14
+
15
+ `ziprack/admin` is **headless**: JSON endpoints, export and report services, no pages. The
16
+ deployment skeleton owns the UI, and replacing it entirely is expected — the API is the contract,
17
+ the pages are not.
18
+
19
+ The admin composes from declared capabilities rather than assuming every applet is a simulation.
20
+ Ask a Rack what to render:
21
+
22
+ ```http
23
+ GET /ziprack/admin/applets/{applet}/panels
24
+ ```
25
+
26
+ ```json
27
+ {
28
+ "declared": ["activity@1", "attempt@1", "auth@1"],
29
+ "panels": ["activity", "attempt", "auth"],
30
+ "implemented": ["activity@1", "ai@1", "attempt@1", "auth@1", "broadcast@1", "db@1", "storage@1"]
31
+ }
32
+ ```
33
+
34
+ `panels` is the intersection of what the current version declares and what this Rack implements —
35
+ render exactly those and nothing else.
36
+
37
+ | Declared | Renders |
38
+ |---|---|
39
+ | `attempt@1` | Attempts started and completed, completion rate, metric cards |
40
+ | `activity@1` | Statement log per attempt; CSV export |
41
+ | `db@1` | Record browser per collection, from the declared JSON Schema |
42
+ | `storage@1` | Uploaded artifact gallery, scoped and permissioned |
43
+ | `ai@1` | Per-prompt spend and call volume |
44
+ | `broadcast@1` | Room history, participation by role, facilitator timeline |
45
+
46
+ <a name="the-admin-api"></a>
47
+ ## The Admin API
48
+
49
+ All routes sit under `ziprack-admin.prefix` (default `/ziprack/admin`) behind `web`, `auth` and the
50
+ `ziprack.use-admin` gate. Every route with an `{applet}`, including capability routes, also requires
51
+ `ziprack.manage-applet` for that applet. See [Who May Do What](#who-may-do-what).
52
+
53
+ | Route | Purpose |
54
+ |---|---|
55
+ | `GET status` | Format, core version, capabilities, warnings |
56
+ | `GET applets` · `POST applets` | List and create |
57
+ | `GET applets/{applet}` · `PATCH applets/{applet}` | Show and update |
58
+ | `GET applets/{applet}/panels` | What to render |
59
+ | `POST versions/upload` · `POST applets/{applet}/versions` | Install a package |
60
+ | `GET applets/{applet}/versions/{version}` | Manifest, requires diff, SBOM, OSV findings, publisher |
61
+ | `POST …/{version}/approve` · `…/promote` · `…/retire` | The publishing actions |
62
+ | `GET review-queue` | Everything awaiting a decision |
63
+ | `GET participants` · `GET audit-log` | People and capability calls |
64
+
65
+ Capability packages add their own, and only when installed:
66
+
67
+ | Route | Capability |
68
+ |---|---|
69
+ | `GET applets/{applet}/overview` · `…/attempts` · `…/attempts/{attempt}` | `attempt` |
70
+ | `GET applets/{applet}/attempts/{attempt}/statements` · `…/export.csv` · `…/metrics` | `activity` |
71
+ | `GET applets/{applet}/collections` · `…/collections/{name}/records` | `db` |
72
+ | `GET applets/{applet}/files` | `storage` |
73
+ | `GET applets/{applet}/ai/spend` · `…/ai/calls` | `ai` |
74
+ | `GET applets/{applet}/rooms` · `POST rooms` · `GET rooms/{room}` · `POST rooms/{room}/status` · `GET rooms/{room}/events` | `broadcast` |
75
+ | `GET participants/{participant}/profile` · `PUT …` | `auth` |
76
+
77
+ <a name="who-may-do-what"></a>
78
+ ## Who May Do What
79
+
80
+ zipRACK asks, and the Rack answers. Every check goes through one contract,
81
+ `ZipRack\Core\Contracts\AppletAccess`, with the applet in hand when there is one:
82
+
83
+ | Ability | Gate | Asked for |
84
+ |---|---|---|
85
+ | `UseAdmin` | `ziprack.use-admin` | The admin API, the MCP server, personal MCP tokens |
86
+ | `CreateApplet` | `ziprack.create-applet` | `POST applets`, `POST versions/upload`, an agent's upload without a slug |
87
+ | `ManageApplet` | `ziprack.manage-applet` | Every `applets/{applet}` route; unpublished and `?preview` player launches |
88
+ | `ReviewVersions` | `ziprack.review` | `GET review-queue`, approving, promoting with `force` past review |
89
+ | `ViewRack` | `ziprack.view-rack` | `GET status`, `GET audit-log`, every participant, the `rack-status` tool |
90
+
91
+ `scopeVisible()` narrows the applet list, the MCP tools' slug lookups and `list-applets` to what
92
+ the user may see. An applet they may not see reads as not installed.
93
+
94
+ The default binding, `AdminOnlyAccess`, answers every ability with the `ziprack.admin` gate
95
+ (`$user->is_admin`), so a Rack that binds nothing, or only redefines that gate, behaves as before.
96
+ A Rack with other roles binds its own:
97
+
98
+ ```php
99
+ $this->app->singleton(AppletAccess::class, RackAppletAccess::class);
100
+ ```
101
+
102
+ Applets have an **owner**, a morph like a version's publisher: whoever published the first
103
+ version, or created the empty applet. `$applet->isOwnedBy($user)` is the usual basis for
104
+ `ManageApplet`. A package whose slug already belongs to an applet the publisher cannot manage is
105
+ refused, so an upload cannot land in someone else's applet.
106
+
107
+ **Participants** are Rack-wide. A user with `ViewRack` sees all of them; anyone else sees the
108
+ participants with audit-log entries on an applet they can see, through `GET participants` and the
109
+ `auth` profile routes. `ZipRack\Core\Access\ParticipantVisibility` applies the rule.
110
+
111
+ To react to publishing, listen for `ZipRack\Core\Events\VersionInstalled` (with the publisher),
112
+ `VersionApproved` and `VersionPromoted`.
113
+
114
+ <a name="preview-runs"></a>
115
+ ## Preview Runs
116
+
117
+ Every attempt query above — the table, the overview counts, the metric cards and the CSV export —
118
+ returns **live attempts only** unless told otherwise:
119
+
120
+ | `?preview=` | Returns |
121
+ |---|---|
122
+ | *(absent)* · `live` | Real runs. The default, and what a report should almost always show. |
123
+ | `preview` | Staff [preview runs](attempts.md#preview-runs) only |
124
+ | `all` | Both, with `is_preview` on each row to tell them apart |
125
+
126
+ An unrecognised value falls back to `live` rather than erroring, so a stale bookmark cannot quietly
127
+ turn preview data into a report.
128
+
129
+ The `db@1` record browser (`…/collections` and `…/collections/{name}/records`) takes the same
130
+ `?preview=` with the same default, and the collection `records` counts follow it. `ziprack_statements`
131
+ and `ziprack_records` both carry an `is_preview` column set from the launch session, so preview data
132
+ is saved, never mixed in, and easy to delete in one query when you want it gone.
133
+
134
+ <a name="the-csv-export"></a>
135
+ ## The CSV Export
136
+
137
+ `GET /ziprack/admin/applets/{applet}/export.csv` produces one row per attempt:
138
+
139
+ ```
140
+ ID, Name, Email, {first segment label}, Status, Started At, Completed At,
141
+ {declared activities in sequence order, headed by label},
142
+ {undeclared activity ids that appeared, appended}
143
+ ```
144
+
145
+ Each activity cell is the **last** `result.response` for that activity in that attempt; non-scalar
146
+ values are JSON-encoded, and duplicate labels are disambiguated so the header row stays usable.
147
+
148
+ This is the one artifact every applet declaring `activity@1` produces identically, which is what
149
+ makes it worth protecting. The authoring consequences are covered in
150
+ [Designing for the Export](activity.md#designing-for-the-export) and are unchanged from `cn-sim/1`:
151
+ `id` is the data contract, `label` is the header, renaming either has consequences.
152
+
153
+ <a name="metrics"></a>
154
+ ## Metrics
155
+
156
+ `GET /ziprack/admin/applets/{applet}/metrics` computes each declared
157
+ [metric](activity.md#metrics) over completed attempts: the pre mean, the post mean and the shift,
158
+ sliceable by the primary segment. Metrics need `attempt@1` as well as `activity@1`, which the
159
+ manifest validator enforces at upload rather than leaving to discover at report time.
160
+
161
+ <a name="response-analysis"></a>
162
+ ## Response Analysis
163
+
164
+ The admin can summarise free-text answers across attempts:
165
+
166
+ ```sh
167
+ php artisan ziprack:response-analysis --applet=my-applet
168
+ ```
169
+
170
+ ```http
171
+ GET /ziprack/admin/applets/{applet}/response-analysis
172
+ POST /ziprack/admin/applets/{applet}/response-analysis/refresh
173
+ ```
174
+
175
+ Summaries regenerate only for applets whose answers changed; schedule the command every few hours.
176
+
177
+ **This is a Rack feature, not an applet capability.** It operates on collected statements from any
178
+ applet declaring `activity@1`, whether or not that applet uses [`ai@1`](ai.md). The two are budgeted
179
+ and configured separately, and conflating them is the mistake this note exists to prevent.
180
+
181
+ <a name="building-a-ui"></a>
182
+ ## Building a UI
183
+
184
+ The skeleton ships a reference admin over these endpoints. When you replace it:
185
+
186
+ 1. Call `/panels` first and render from the answer. Hard-coding panels breaks the moment a Rack runs
187
+ a different capability set.
188
+ 2. Render `db@1` record tables from the collection's declared JSON Schema — the schema is there
189
+ precisely so the UI does not need per-applet knowledge.
190
+ 3. Treat the review queue as the landing page. It is where the decisions are.
@@ -0,0 +1,258 @@
1
+ # The JavaScript SDK
2
+
3
+ - [Installation](#installation)
4
+ - [Booting](#booting)
5
+ - [Launch Data](#launch-data)
6
+ - [Acks and Errors](#acks-and-errors)
7
+ - [Events](#events)
8
+ - [Mock Mode](#mock-mode)
9
+ - [Testing](#testing)
10
+ - [Leaving the Applet](#leaving-the-applet)
11
+ - [The MCP Server](#the-mcp-server)
12
+
13
+ <a name="installation"></a>
14
+ ## Installation
15
+
16
+ ```sh
17
+ npm install @ziprack/sdk
18
+ ```
19
+
20
+ ```ts
21
+ import { Rack } from '@ziprack/sdk';
22
+ ```
23
+
24
+ `Rack` is a singleton — one per frame, which is what an applet always is. `createRack()` returns a
25
+ fresh client for tests or unusual multi-frame setups.
26
+
27
+ A Rack also serves an IIFE build at `/ziprack/assets/sdk.js` that exposes `window.Rack` (and
28
+ `window.ZipRack`), for applets with no build step. It is the same code; prefer the npm package when you have a bundler, because mock
29
+ mode and the test host come with it.
30
+
31
+ <a name="booting"></a>
32
+ ## Booting
33
+
34
+ ```ts
35
+ const launch = await Rack.init({ slug: 'my-applet' });
36
+ ```
37
+
38
+ `init()` announces `rack:ready` to the parent, waits for `rack:launch`, and resolves with the launch
39
+ payload. It is safe to call repeatedly: subsequent calls return the same promise. Calls made before
40
+ the handshake completes are queued, not dropped, so you never have to gate your UI on initialisation
41
+ order.
42
+
43
+ | Option | Default | Purpose |
44
+ |---|---|---|
45
+ | `slug` | — | Labels the mock session and locates `./manifest.json` when standalone. |
46
+ | `launchTimeoutMs` | `3000` | How long to wait for `rack:launch` before falling back to mock mode. |
47
+ | `ackTimeoutMs` | `5000` | Per-call ack timeout. Streamed calls reset it per chunk. |
48
+ | `manifestUrl` | `./manifest.json` | Where mock mode reads your manifest. |
49
+ | `transport` | — | Inject a transport (tests). Skips the handshake and mock detection. |
50
+
51
+ `Rack.mode` is `'pending'`, `'host'` or `'mock'`. `Rack.launched` tells you whether launch data has
52
+ arrived.
53
+
54
+ <a name="launch-data"></a>
55
+ ## Launch Data
56
+
57
+ Everything the Rack is willing to tell you up front arrives in one payload — there is no second
58
+ round trip to fetch the participant, the attempt or the manifest.
59
+
60
+ ```ts
61
+ interface LaunchData {
62
+ schema: 'ziprack/1';
63
+ actor: { id: number; name: string };
64
+ applet: { slug: string; name: string; version: string };
65
+ capabilities: string[]; // as declared, e.g. ['auth@1', 'db@1']
66
+ media: Record<string, MediaEmbed>; // Rack-built player URLs by declared id
67
+
68
+ auth?: { user: LaunchUser };
69
+ attempt?: { id, status, state, segment };
70
+ activity?: { activities: LaunchActivity[] };
71
+ db?: { collections: Array<{ name, scope }> };
72
+ storage?: { uploads: Array<{ name, scope, mime, max_bytes }> };
73
+ ai?: { prompts: Array<{ id, mode }> };
74
+ broadcast?: { room, role, members, state, channels };
75
+ }
76
+ ```
77
+
78
+ Each capability contributes its own key, and only when declared. The typed accessors read from the
79
+ same payload, so `Rack.auth.user()`, `Rack.attempt.state()` and `Rack.activity.activities()` are
80
+ synchronous and free.
81
+
82
+ ```ts
83
+ Rack.on('launch', (data) => render(data)); // fires immediately if already launched
84
+ Rack.declares('broadcast'); // did the manifest declare it?
85
+ ```
86
+
87
+ <a name="acks-and-errors"></a>
88
+ ## Acks and Errors
89
+
90
+ **Every SDK call resolves to an `AckResult` and never throws.** There is no `try`/`catch` ceremony
91
+ around a capability call; there is a branch on `ok`.
92
+
93
+ ```ts
94
+ interface AckResult<T> {
95
+ ok: boolean;
96
+ data?: T;
97
+ error?: AckErrorCode;
98
+ message?: string; // human-readable, safe to log, not to show
99
+ details?: object; // validation errors, keyed by field
100
+ }
101
+ ```
102
+
103
+ ```ts
104
+ const result = await Rack.db.collection('ideas').create({ title, status: 'draft' });
105
+
106
+ if (! result.ok) {
107
+ if (result.error === 'quota_exceeded') return showFull();
108
+ return showRetry();
109
+ }
110
+
111
+ const idea = result.data.record;
112
+ ```
113
+
114
+ | Code | Means |
115
+ |---|---|
116
+ | `timeout` | No ack within the timeout. The shell retries the batch once; this is what you see after that. |
117
+ | `disposed` | The client was torn down while the call was in flight. |
118
+ | `persist_failed` | The Rack could not process the message. Already reported server-side. |
119
+ | `undeclared_capability` | The manifest does not declare it — a packaging bug, not a runtime condition. |
120
+ | `unknown_type` | Malformed or unimplemented message type. |
121
+ | `validation` | Payload failed the handler's rules or a declared schema. See `details`. |
122
+ | `forbidden` | Permitted by the manifest, refused for this participant (not the owner, not the facilitator). |
123
+ | `completed` | The attempt is finished; state, segment and attempt-scoped records are frozen. |
124
+ | `quota_exceeded` | Collection full, upload slot full, file too large. |
125
+ | `budget_exceeded` | An `ai@1` budget was reached. |
126
+ | `not_found` | Unknown record, file, channel or collection. |
127
+ | `conflict` | A concurrent write lost. |
128
+
129
+ **Do not block the participant on an ack.** The shell batches messages on a 300 ms window (up to 50
130
+ per request) and coalesces `attempt:state` so only the latest value is sent. Fire and forget for
131
+ progress and state; `await` only what you must confirm — typically the last answer before
132
+ `complete()`.
133
+
134
+ ```ts
135
+ void Rack.attempt.setState({ step: 4 }); // fire and forget
136
+ await Rack.activity.answer('outro.confidence', 5); // confirm, then:
137
+ await Rack.attempt.complete();
138
+ ```
139
+
140
+ <a name="events"></a>
141
+ ## Events
142
+
143
+ The Rack pushes events into the frame — room broadcasts, presence changes, role grants. Subscribe
144
+ through the capability that owns them (see [Broadcast](broadcast.md)), or read the raw stream:
145
+
146
+ ```ts
147
+ Rack.on('event', ({ event, payload, channel, from }) => {
148
+ console.log(event, payload, channel, from?.participant_id);
149
+ });
150
+ ```
151
+
152
+ <a name="mock-mode"></a>
153
+ ## Mock Mode
154
+
155
+ Run `npm run dev` and there is no Rack, so the SDK becomes one. Mock mode reads your
156
+ `./manifest.json` and services every capability locally — which means declared capabilities,
157
+ activities, collections, prompts and uploads behave exactly as they will in production, including
158
+ refusing calls the manifest did not declare.
159
+
160
+ ```
161
+ [ziprack] running in mock mode (not embedded in a Rack)
162
+ [ziprack mock] activity:statement { verb: 'answered', … }
163
+ ```
164
+
165
+ | Surface | Mock behaviour |
166
+ |---|---|
167
+ | `attempt` | State and status persist in `localStorage` under `ziprack-mock:{slug}:*`. Refresh resumes. |
168
+ | `activity` | Statements are logged and kept in memory. |
169
+ | `db` | Records persist in `localStorage`, validated against the declared schema. |
170
+ | `storage` | Files are held as blob URLs and marked clean. |
171
+ | `ai` | Returns a stub conforming to `output_schema` — or your own handler. |
172
+ | `broadcast` | Uses `BroadcastChannel`, so **two tabs of your dev server form a room**. |
173
+
174
+ Two hooks exist only in mock mode:
175
+
176
+ ```ts
177
+ Rack.mock.ai('score_pitch', (vars) => ({ clarity: 4, feedback: `Nice work on "${vars.pitch}"` }));
178
+ Rack.mock.resetAttempt(); // replay a completed attempt
179
+ ```
180
+
181
+ Mock mode also engages if a real Rack fails to answer `rack:launch` within `launchTimeoutMs`. That
182
+ is a deliberate development convenience, and the console says so — if you see it in production,
183
+ something is wrong with the shell, not with your applet.
184
+
185
+ <a name="testing"></a>
186
+ ## Testing
187
+
188
+ `@ziprack/sdk/testing` ships an in-memory Rack you can assert against.
189
+
190
+ ```ts
191
+ import { createRack } from '@ziprack/sdk';
192
+ import { FakeHost } from '@ziprack/sdk/testing';
193
+ import manifest from '../public/manifest.json';
194
+
195
+ it('records the answer and completes exactly once', async () => {
196
+ const host = new FakeHost(manifest);
197
+ const rack = createRack();
198
+ await rack.init({ transport: host.transport() });
199
+
200
+ await rack.activity.answer('intro.confidence', 5);
201
+ await rack.attempt.complete();
202
+ await rack.attempt.complete(); // idempotent
203
+
204
+ expect(host.answers()).toEqual({ 'intro.confidence': 5 });
205
+ expect(host.attempt().status).toBe('completed');
206
+ });
207
+ ```
208
+
209
+ | Member | Gives you |
210
+ |---|---|
211
+ | `host.transport()` | Pass to `rack.init({ transport })`. |
212
+ | `host.statements` | Every statement the applet emitted, in order. |
213
+ | `host.answers()` | The last `answered` value per activity — what reporting would export. |
214
+ | `host.attempt()`, `host.records(name)`, `host.files()` | Persisted state. |
215
+ | `host.push(event)` | Push a `rack:event` in, as the Rack would, to drive broadcast handlers. |
216
+
217
+ <a name="leaving-the-applet"></a>
218
+ ## Leaving the Applet
219
+
220
+ ```ts
221
+ Rack.eject();
222
+ ```
223
+
224
+ Asks the Rack to navigate away — back to the applet list, or wherever the deployment sends people.
225
+ It needs no capability and takes no ack. In mock mode it logs what would have happened.
226
+
227
+ <a name="the-mcp-server"></a>
228
+ ## The MCP Server
229
+
230
+ ```sh
231
+ npx ziprack-mcp
232
+ ```
233
+
234
+ The SDK ships a stdio MCP server that gives an AI assistant these docs and the applet authoring
235
+ guidelines. The docs are bundled into the package when it is published, so what the assistant reads
236
+ always matches the SDK version in your `node_modules`, not whatever it remembers of an older one. The
237
+ starter already includes the configuration in `.mcp.json`:
238
+
239
+ ```json
240
+ {
241
+ "mcpServers": {
242
+ "ziprack": {
243
+ "command": "npx",
244
+ "args": ["ziprack-mcp"]
245
+ }
246
+ }
247
+ }
248
+ ```
249
+
250
+ | Tool | Purpose |
251
+ |---|---|
252
+ | `ziprack_guidelines` | The rules every applet follows (`ziprack-1-authoring.md`). Read before writing applet code. |
253
+ | `ziprack_list_docs` | Every doc in reading order, with its section anchors. |
254
+ | `ziprack_search_docs` | The best-matching sections, in full, for one or more phrasings. |
255
+ | `ziprack_read_doc` | One doc, or one section of it by anchor. |
256
+
257
+ Every answer names the SDK version it came from. `ziprack-studio mcp` includes the same four tools,
258
+ so a Studio project needs only the one server.