@eventmodelers/cli 1.0.159 → 1.0.161

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.159",
3
+ "version": "1.0.161",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -631,6 +631,8 @@ Create a SCREEN node from a sketch description.
631
631
 
632
632
  **File**: `src/slices/change/api-.slices/routes.ts`
633
633
 
634
+ **One slice border per column.** Every node write (`/nodes/events`, MCP tools, the web UI) rejects a `SLICE_BORDER` whose `colId` — read from `meta.colId` or `node.data.colId` — already belongs to another `SLICE_BORDER` on the board, with `SLICE_BORDER_EXISTS` (`context.existingSliceBorderId` names the border to update or delete instead). Re-sending the same border, or creating a new one after the old one was deleted, is fine. Slice reads (`slicedata`) only ever return one slice per column, so a second border would otherwise exist on the board yet read back as an empty slice.
635
+
634
636
  ### POST `/api/org/:orgId/boards/:boardId/timelines/:timelineId/slices`
635
637
  Create a complete slice (1 column + its nodes automatically placed).
636
638
 
@@ -1175,11 +1177,12 @@ Rule of thumb: if you are about to write "Shall I…?", "Want me to…?", "A or
1175
1177
  | to answer where one piece of information comes from and where it ends up ("where is the customer email used?") | `dataJourney` | nothing — element names pan the canvas |
1176
1178
  | to show a diagram you drew yourself — a context map, a domain story, a sketch, any picture that isn't a chart | `svg` | nothing |
1177
1179
  | to show a UML or flow diagram you write as Mermaid — a sequence, flowchart, state, ER or class diagram | `mermaid` | nothing |
1180
+ | to ask a complex question with several fields at once — a short form with text, choices, a slider, a date, checkboxes ("which environment, when, and who to notify?") | `a2ui` | `Answer to "<title>" (<action>):` + one `- label: value` line per input |
1178
1181
  | to lay out rows and columns — a comparison, a matrix (stressors × elements), a short list with a few attributes | `table` | nothing — linked cells pan the canvas |
1179
1182
  | to show a prototype you just built and uploaded (the `prototype` skill), running in the chat | `prototype` | nothing — **Full screen** opens the board's prototype player |
1180
1183
  | something the customer defined their own snippet type for (a risk card, a sign-off — see `list_snippet_types`) | `custom` | the clicked action's `answer`; `context.snippetReply.then` says what to do |
1181
1184
 
1182
- `confirm`, `poll`, `tasks` and `changes` are questions: a person answers once, then they show as answered. `link`, `image`, `code`, `report`, `walkthrough`, `screen`, `dataJourney`, `svg`, `mermaid`, `table` and `prototype` are display only and never wait for anything.
1185
+ `confirm`, `poll`, `tasks`, `changes` and `a2ui` are questions: a person answers once, then they show as answered. `link`, `image`, `code`, `report`, `walkthrough`, `screen`, `dataJourney`, `svg`, `mermaid`, `table` and `prototype` are display only and never wait for anything.
1183
1186
 
1184
1187
  ### Shapes
1185
1188
  ```typescript
@@ -1230,6 +1233,12 @@ Rule of thumb: if you are about to write "Shall I…?", "Want me to…?", "A or
1230
1233
  width: number, height: number, title?: string }> } // a click zooms the canvas to nodeId; title = tooltip (else the element's title)
1231
1234
  { kind: 'mermaid', source: string, // Mermaid source, no ``` fence (≤ 60000 chars); the app draws it
1232
1235
  title?: string, caption?: string }
1236
+ { kind: 'a2ui', title?: string, // an A2UI v0.9 form (https://a2ui.org), basic catalog subset
1237
+ components: Array<{ id: string, component: string, ... }>, // flat list, ≤ 150; one has id 'root'; children refer to ids, no cycles
1238
+ data?: object } // initial data model; inputs bind to it with value: { path: '/form/x' }
1239
+ // Text{text, variant?} Row/Column/List{children:[id], align?} Card{child} Divider TextField{label, value?, variant?: 'shortText'|'longText'|'number'}
1240
+ // CheckBox{label, value?} ChoicePicker{label, options:[{label,value}], value?:[…], variant?:'mutuallyExclusive'} Slider{label, min, max, value?}
1241
+ // DateTimeInput{label, enableDate?, enableTime?} Button{child:<Text id>, variant?:'primary', action:{event:{name, context?}}}
1233
1242
  { kind: 'table', title?: string, caption?: string,
1234
1243
  columns: Array<string | { label: string, nodeId?: string, // 1–12; nodeId makes the header a link (a matrix with one column per element)
1235
1244
  align?: 'left' | 'center' | 'right' }>,
@@ -1252,6 +1261,7 @@ Rule of thumb: if you are about to write "Shall I…?", "Want me to…?", "A or
1252
1261
  - **`dataJourney`** answers "where does X come from / where is X used?" for **one piece of information**, as a table: one stop per element it passes through, in flow order, each with its `role`, the field name there and the slice. A headline counts the roles ("Originates in 1 place, stored in 2 elements and used in 3 places"). Build it from the board — the `data-journey` skill (modeling kit) has the procedure; every `nodeId` must be a real id from this board, and a field the board doesn't carry is not a stop. Keep `text` to one line naming the data and where it originates.
1253
1262
  - **`svg`** is for a picture you draw yourself: give the root a `viewBox` (and `width`/`height` for its natural size), inline colours and fonts — it is shown as an image, so scripts, `<a>` links and external resources do nothing. To make parts of it clickable, list them in `links`: a rectangle in the viewBox coordinates you drew in plus the element it stands for (only real ids from this board); the app lays a clickable area over each that zooms the canvas there. Prefer `report` for numbers. A domain story (who does what with which work object) is an `svg` you draw — the `domain-story` skill (modeling kit) has the procedure and the drawing style. Every SVG snippet has a download-as-PNG button.
1254
1263
  - **"Place it on the board" after you drew an SVG always means: put that picture on the board as an image** — never rebuild it from elements (stickies, lanes, a timeline), never a SCREEN, never a new snippet. `place_image` with `imageBase64` = the SVG document itself, base64-encoded, `mimeType: "image/svg+xml"`, `title` = the snippet's title: it becomes an OUTLINE (the drawing tools' image element), free-floating at `x`/`y`, in no chapter and connected to nothing. Place it where the person says, else in free space next to the chapter it is about (`get_chapter_bounds`: right of or below it). Add a white background `<rect>` first if the SVG has none, or it is unreadable on a dark board. One SVG = one image. The image on the board has no clickable areas — `links` stay in the chat.
1264
+ - **`a2ui`** is for a question that needs several answers at once — instead of five chat turns, send one form. Build it as a flat component list: a `Column` with id `root`, then the fields, then a primary `Button` whose `action.event.name` says what the click means (`submit`, `approve` …). Several buttons are fine (`approve` / `reject`); the clicked one's `name` is in the answer. The person's answer is one chat message — `Answer to "<title>" (<action>):` and a `- label: value` line per filled input, chosen options by their labels, then the action's `context` — so give every input a clear `label`. Keep it small (≤ ~8 fields) and send a meaningful `text`. Not supported (renders nothing): Image, Icon, Video, AudioPlayer, Tabs, Modal, template children (`{path, componentId}`) and function calls; a missing `root`, an unknown child id or a cycle makes the whole snippet invisible. Like every question it answers once.
1255
1265
  - **`mermaid`** is for a diagram you can write as text: a sequence diagram (`sequenceDiagram`), a flow (`flowchart LR`), states (`stateDiagram-v2`), an ER or class diagram. Send just the source; the app draws it, with a *Show source* toggle and a copy button, and shows the source when it has a syntax error — so keep the syntax plain (quote labels with special characters). Prefer it over a `code` block for any diagram: a `code` block with `language: "mermaid"` is drawn too, but `mermaid` says what you mean. It has no element links; for clickable parts draw an `svg`.
1256
1266
  - **`table`** is for anything that is rows and columns and not a chart: a comparison of options, an incidence matrix (rows = stressors, columns = elements, `true` where it hits), a short list with a few attributes per item. It computes nothing — totals are a row you add. Put element ids in `nodeId` (header or cell) so names are links; a row longer than the columns is cut, a shorter one padded, an empty one dropped. Prefer `report` for numbers you want *seen* as a trend or share, `dataJourney` for one piece of data's path.
1257
1267
  - **`prototype`** runs a prototype in the chat: the page the platform stored for `timelineId` (the `contextId` the `prototype` skill uploaded under — any timeline of that context works too), in a sandboxed iframe with **Restart** (empty event log) and **Full screen** (the same player as the timeline's ▶). Send it only after `upload` succeeded — before that it shows "No prototype has been built for this yet". The `text` is the short report of what was built; the prototype itself is the snippet.
@@ -51,7 +51,7 @@ Ask only what you cannot infer, one question per turn, with a snippet whenever t
51
51
  - `instructions`: at most 4000 characters.
52
52
 
53
53
  ### Based on a built-in kind — when the built-in snippet already looks right
54
- A type doesn't have to be custom-drawn. With `base` set to a built-in kind (`table`, `report`, `poll`, `tasks`, `confirm`, `dataJourney`, `link`, `code`, …) the type has **no template and no actions**, and the `schema` is optional. Its data is exactly that kind's JSON without `kind` (for `table`: `{ title?, columns, rows }`), and the chat draws and answers it like that kind — element links (`nodeId`), paging and its own answer buttons included.
54
+ A type doesn't have to be custom-drawn. With `base` set to a built-in kind (`table`, `report`, `poll`, `tasks`, `confirm`, `dataJourney`, `link`, `code`, `a2ui`, …) the type has **no template and no actions**, and the `schema` is optional. Its data is exactly that kind's JSON without `kind` (for `table`: `{ title?, columns, rows }`) — an `a2ui` base is the way to keep a reusable form (`{ title?, components, data? }`, see `learn-eventmodelers-api` §16), and the chat draws and answers it like that kind — element links (`nodeId`), paging and its own answer buttons included.
55
55
 
56
56
  - Prefer this whenever the person wants to keep a built-in snippet you showed, and whenever the card must **link to elements** on the board — a template's sandboxed frame can't.
57
57
  - The `instructions` carry what makes it reusable: what to collect each time and how to fill the kind's fields (*"One row per chapter (CHAPTER elements), sorted by title; column 'Chapter'; each cell `{text: <title>, nodeId: <id>}`."*).
@@ -315,6 +315,7 @@ for it. Instead:
315
315
  - **"draw" / "sketch" / "diagram" something** (a UML diagram, a flow, a class model) → a snippet in the chat (`mermaid` for a sequence/flow/state/ER/class diagram written as
316
316
  Mermaid, `svg` for a picture you draw yourself, or a `code` block with PlantUML/ASCII), **never** drawn on the board with stickies, arrows or `create_drawing`.
317
317
  Only an explicit request to put it on the board changes that, and then as an image (`/domain-story` § board), not rebuilt from shapes;
318
+ - **a question with several fields** (environment + date + who to notify, a small form) → `a2ui`, one form instead of several chat turns;
318
319
  - **rows and columns** — a comparison, a matrix, a short list with a few attributes per item → `table`.
319
320
  - **a prototype you just built** (the `/prototype` skill) → `prototype`, the uploaded page running in the chat with a Full screen button;
320
321
  - anything the customer defined **their own snippet type** for → `custom` with its `ref`: `list_snippet_types`
@@ -63,6 +63,9 @@ Extract:
63
63
  isn't built or re-fetched yet: fall back to `apiEndpoint` / `fields[]` as above and note the assumption
64
64
  in `progress.txt`. If contract and `command.fields[]` disagree, the contract wins for the request and
65
65
  you record the mismatch in `progress.txt`.
66
+ **Build the call from it**: `postCommand(buildPath('<contract path>', { <path params> }), { <requestBody
67
+ properties> })` — `buildPath` (in `src/lib/api.ts`) fills and URL-encodes `{param}` segments; the body
68
+ carries exactly the contract's `requestBody` properties (props the contract doesn't list stay out).
66
69
  - **events[]** — informational only (what the backend is expected to emit); this stack does not
67
70
  act on them directly.
68
71
  - **screens[]** — the actor `SCREEN` element(s) that trigger this command, if any. Empty when the
@@ -127,11 +130,14 @@ lane concept, so the frontend path isn't a strict 1:1 mirror of it. `CommandTitl
127
130
  `title` verbatim (e.g. `ReserveBike.tsx`).
128
131
 
129
132
  **Markup**: translate the blueprint HTML from Step 2 into JSX **1:1** — same tags, same classes
130
- (Bulma or whatever the mockup used), same layout and copy. This is not a redesign; the screen *is*
133
+ (the screen's `cssFrameworks` framework), same layout and copy. This is not a redesign; the screen *is*
131
134
  the design. Only change what must become dynamic:
132
135
  - A field's example value becomes that input's controlled `value`.
133
136
  - An element repeated for a `cardinality: "List"` field becomes a `.map()` over that field.
134
- - `class`/`style`/structure otherwise stay exactly as drawn.
137
+ - `class`/`style`/structure otherwise stay exactly as drawn — except the board's own mark plumbing:
138
+ drop every `data-em-mark-id` attribute and `em-mark`/`em-mark-<hex>` class (they only draw the
139
+ highlight outline on the canvas; the app never ships it).
140
+ - The classes only render correctly if the screen's CSS framework (`cssFrameworks`) is loaded in the app — see Step 7 (styles); don't hand-write CSS to compensate.
135
141
 
136
142
  **Props**: exactly `command.fields[]`, typed per each field's `type` (`String`→`string`,
137
143
  `Boolean`→`boolean`, `Int`/`Long`→`number`, etc. — `subfields[]` become a nested object type),
@@ -290,9 +296,52 @@ how many pages this app needs — not one page per slice.
290
296
  - If `src/pages/<ScreenTitle>.tsx` already exists, update only this slice's marked position;
291
297
  leave every other position (already-wired slices, and untouched mockup content) exactly as is.
292
298
 
293
- ## Step 7 — Verify against slice.json
294
-
295
- After writing the hook/component (and context/page, if any), confirm:
299
+ ## Step 7 — Load the screen's styles, every slice
300
+
301
+ The mockup's classes only mean something with the CSS framework the board renders it with, so the
302
+ app must load that same one. This runs on **every** slice (not just the first) and is idempotent —
303
+ an existing project that is missing it gets fixed on its next build.
304
+
305
+ 1. **Detect** the framework the screen loads, from the node fetched in Step 2: `cssFrameworks` (one id).
306
+ If it isn't returned, infer it from the classes in `meta.pages` (`title`/`field`/`is-*` → bulma,
307
+ `btn`/`form-control`/`col-*` → bootstrap, utility classes like `flex p-6` → tailwind). If the
308
+ screen loads **no** framework (`[]`, or nothing detectable), use Vite's default: change nothing —
309
+ skip this whole step, leave `index.html` and `src/index.css` as the template ships them.
310
+ 2. **Ensure it's loaded** in `index.html`'s `<head>`, same pinned version the board uses — check with
311
+ `grep -n -i "bulma\|bootstrap\|cdn.tailwindcss" index.html` first, add only what's missing:
312
+ - `bulma` → `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">`
313
+ - `bootstrap` → `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">`
314
+ - `tailwind` → `<script src="https://cdn.tailwindcss.com/3.4.16"></script>`
315
+
316
+ A *different* framework already there means two screens disagree: don't mix them — keep what's
317
+ there and flag it via `request-feedback`.
318
+ 3. **Neutralise the Vite demo CSS** (only because a framework was detected). If `src/index.css` still holds the demo (it contains `#social` or
319
+ `--social-bg`), replace the whole file with `body { margin: 0; }` — the centred 1126px `#root`,
320
+ dark-mode palette, `color-scheme: light dark`, grey text and 56px `h1` all fight the mockup. A file
321
+ without those markers was already customised (or filled from `learn-styleguide`): leave it alone.
322
+
323
+ ## Step 8 — Base page, only if this is the first slice
324
+
325
+ `src/App.tsx` ships as the Vite demo scaffold. The **first slice built in this project** replaces it
326
+ with the app's **base page**, so there is something real to open in the browser; every later slice
327
+ leaves it alone.
328
+
329
+ 1. **First slice?** Yes only if `src/slices/` holds no other slice folder than this one (only
330
+ `.gitkeep` besides it) *and* `src/App.tsx` still contains the scaffold (`Get started` heading).
331
+ Otherwise skip this step entirely.
332
+ 2. Rewrite `src/App.tsx` as the base page: a minimal `<main>` that renders the top-level thing this
333
+ slice produced — `src/pages/<ScreenTitle>.tsx` when Step 6 composed a shared page, else this
334
+ slice's own component. Nothing from the Vite demo (counter, logos, doc links) stays. No router and
335
+ no new dependency — a single page needs neither.
336
+ 3. Delete what the scaffold alone used and nothing else now imports: `src/App.css` and
337
+ `src/assets/{hero.png,react.svg,vite.svg}` (check with `grep -rn` first); `public/icons.svg` too if
338
+ nothing references it. Leave `src/main.tsx` and `src/index.css` (Step 7 handles it) as they are.
339
+ 4. Keep the page working with no backend, per the isolation rule in `.build-kit/CLAUDE.md`: the
340
+ slice component must render its empty state, and `VITE_DATA_MODE=mock` must show its sample.
341
+
342
+ ## Step 9 — Verify against slice.json
343
+
344
+ After writing the hook/component (and context/page/base page, if any), confirm:
296
345
  - Every prop matches a `command.fields[]` entry exactly (name + type) — no invented or missing
297
346
  fields.
298
347
  - The `postCommand` call uses the UI contract's `path` and `body` when one was found, else `command.apiEndpoint` verbatim — or, when it was missing at build
@@ -29,7 +29,8 @@ convenient — for a component to read it straight from Supabase, with no bespok
29
29
  between, the same way `miro-eventmodeling` and this stack's other Supabase-backed components do.
30
30
 
31
31
  That said, a component never calls `supabase.from(...)` (or imports `src/lib/supabase.ts`) itself —
32
- it calls `queryReadModel(...)` from `src/lib/api.ts`, same as `build-state-change` components only
32
+ it calls `getReadModel(...)` (backend `GET` UI contract available) or `queryReadModel(...)` (table
33
+ read) from `src/lib/api.ts`, same as `build-state-change` components only
33
34
  ever call `postCommand`. `queryReadModel` is the real Supabase call in production, but in mock mode
34
35
  it serves a numbered sample instead (see Step 4) — routing every read through this one seam is what
35
36
  makes a component testable/previewable in isolation, with zero Supabase project needed.
@@ -66,10 +67,22 @@ Extract:
66
67
  a Slice" step 6) — the built, mocked component still needs a real table/view before it's done,
67
68
  so don't flip it to `Done`.
68
69
  - `description` — implementation hints (filtering rules, sort order, business meaning).
69
- - **No UI contract for reads** — the backend's `ui-contract` skill publishes a `GET` contract, but this
70
- stack reads the table/view directly (RLS-protected) and never calls a read endpoint, so that
71
- contract does not apply here. `readmodel.apiEndpoint` (table/view name) and `fields[]` stay the
72
- source. (Only the write side, `build-state-change`, consumes a contract.)
70
+ - **UI contract (read)** — the backend slice's published `GET` contract: look it up as described in
71
+ the `ui-contract` skill ("How the UI build kit looks the contract up" — the `## UI Contract` entry in
72
+ `slice.json` `notes[]`; parse only its `ui-contract` block — an OpenAPI operation). **When present it
73
+ is the specification and you build the call from it** — it overrides `apiEndpoint` and the table read:
74
+ - fetch with `getReadModel(buildPath('<contract path>', { <path params> }), { <query params> }, samples)`
75
+ from `src/lib/api.ts` — path and query `parameters` exactly as the contract names them, never a
76
+ guessed URL, never `supabase.from(...)`;
77
+ - type the result from the 2xx `responses` schema (it must match `readmodel.fields[]`; the contract
78
+ wins for the wire shape, record any mismatch in `progress.txt`);
79
+ - render **every** non-2xx response (status + description) as its own distinct state — a 404 "no
80
+ reservation" is an empty state, not an error banner; don't collapse them into one generic failure;
81
+ - the numbered samples (Step 4) are shaped like the contract's 2xx response body.
82
+
83
+ **When absent** (backend slice not built or not re-fetched yet): fall back to the table read —
84
+ `readmodel.apiEndpoint` (table/view name) + `fields[]` via `queryReadModel` — and note the assumption
85
+ in `progress.txt`.
73
86
  - **screens[]** — the actor `SCREEN` element(s) that display this data, if any (see Step 2).
74
87
  - **specifications[]** — GWT scenarios/storyline; treat as acceptance criteria (e.g. "given no
75
88
  active reservation, show an empty state").
@@ -103,8 +116,13 @@ the mockup is the design, not a starting point for a redesign.
103
116
  its parent, never read from a global store. The queried data itself is **not** a prop — the
104
117
  component fetches it.
105
118
 
106
- **Default — call `queryReadModel` from `src/lib/api.ts` inside the component**. Never `supabase.from(...)`
107
- directly, never a raw `fetch`:
119
+ **With a UI contract (Step 1)**: call `getReadModel(buildPath(path, pathParams), queryParams, samples)`
120
+ from `src/lib/api.ts` inside the component instead of the `queryReadModel` snippet below — same
121
+ effect shape (`useEffect` → loading / empty / error / data), but each documented non-2xx response
122
+ (`ApiError.status`) maps to its own state. Never a raw `fetch`.
123
+
124
+ **Default (no contract) — call `queryReadModel` from `src/lib/api.ts` inside the component**. Never
125
+ `supabase.from(...)` directly, never a raw `fetch`:
108
126
 
109
127
  ```tsx
110
128
  // ActiveReservationView.tsx
@@ -196,7 +214,7 @@ component doesn't do anything with that number itself, `queryReadModel` reads it
196
214
 
197
215
  ## Step 5 — Compose onto the shared page, if this screen's name is shared
198
216
 
199
- Identical to `build-state-change` Step 5 — a screen's **title is the page name**, and the number
217
+ Identical to `build-state-change` Step 6 — a screen's **title is the page name**, and the number
200
218
  of *distinct* screen titles across the board is a direct hint at how many pages this app needs, not
201
219
  one page per slice.
202
220
 
@@ -216,10 +234,55 @@ one page per slice.
216
234
  slice component still only knows its own declared props. Update only this slice's marked
217
235
  position if the page file already exists.
218
236
 
219
- ## Step 6 — Verify against slice.json
237
+ ## Step 6 — Load the screen's styles, every slice
238
+
239
+ The mockup's classes only mean something with the CSS framework the board renders it with, so the
240
+ app must load that same one. This runs on **every** slice (not just the first) and is idempotent —
241
+ an existing project that is missing it gets fixed on its next build.
242
+
243
+ 1. **Detect** the framework the screen loads, from the node fetched in Step 2: `cssFrameworks` (one id).
244
+ If it isn't returned, infer it from the classes in `meta.pages` (`title`/`field`/`is-*` → bulma,
245
+ `btn`/`form-control`/`col-*` → bootstrap, utility classes like `flex p-6` → tailwind). If the
246
+ screen loads **no** framework (`[]`, or nothing detectable), use Vite's default: change nothing —
247
+ skip this whole step, leave `index.html` and `src/index.css` as the template ships them.
248
+ 2. **Ensure it's loaded** in `index.html`'s `<head>`, same pinned version the board uses — check with
249
+ `grep -n -i "bulma\|bootstrap\|cdn.tailwindcss" index.html` first, add only what's missing:
250
+ - `bulma` → `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">`
251
+ - `bootstrap` → `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">`
252
+ - `tailwind` → `<script src="https://cdn.tailwindcss.com/3.4.16"></script>`
253
+
254
+ A *different* framework already there means two screens disagree: don't mix them — keep what's
255
+ there and flag it via `request-feedback`.
256
+ 3. **Neutralise the Vite demo CSS** (only because a framework was detected). If `src/index.css` still holds the demo (it contains `#social` or
257
+ `--social-bg`), replace the whole file with `body { margin: 0; }` — the centred 1126px `#root`,
258
+ dark-mode palette, `color-scheme: light dark`, grey text and 56px `h1` all fight the mockup. A file
259
+ without those markers was already customised (or filled from `learn-styleguide`): leave it alone.
260
+
261
+ ## Step 7 — Base page, only if this is the first slice
262
+
263
+ `src/App.tsx` ships as the Vite demo scaffold. The **first slice built in this project** replaces it
264
+ with the app's **base page**, so there is something real to open in the browser; every later slice
265
+ leaves it alone.
266
+
267
+ 1. **First slice?** Yes only if `src/slices/` holds no other slice folder than this one (only
268
+ `.gitkeep` besides it) *and* `src/App.tsx` still contains the scaffold (`Get started` heading).
269
+ Otherwise skip this step entirely.
270
+ 2. Rewrite `src/App.tsx` as the base page: a minimal `<main>` that renders the top-level thing this
271
+ slice produced — `src/pages/<ScreenTitle>.tsx` when Step 5 composed a shared page, else this
272
+ slice's own component. Nothing from the Vite demo (counter, logos, doc links) stays. No router and
273
+ no new dependency — a single page needs neither.
274
+ 3. Delete what the scaffold alone used and nothing else now imports: `src/App.css` and
275
+ `src/assets/{hero.png,react.svg,vite.svg}` (check with `grep -rn` first); `public/icons.svg` too if
276
+ nothing references it. Leave `src/main.tsx` and `src/index.css` (Step 6 handles it) as they are.
277
+ 4. Keep the page working with no backend, per the isolation rule in `.build-kit/CLAUDE.md`: the
278
+ slice component must render its empty state, and `VITE_DATA_MODE=mock` must show its sample.
279
+
280
+ ## Step 8 — Verify against slice.json
220
281
 
221
282
  - Every rendered field traces back to a `readmodel.fields[]` entry — no invented or missing
222
283
  fields.
284
+ - With a UI contract: the `getReadModel` call uses the contract's path and query `parameters`
285
+ verbatim, and every non-2xx response has its own rendered state. Otherwise:
223
286
  - The `queryReadModel` call targets `readmodel.apiEndpoint` verbatim as `table` — or, when it was
224
287
  missing at build time, the provisional `mock_...` table name noted in a comment (Step 1) — never a
225
288
  raw `fetch`, a direct `supabase.from(...)` call, or a hardcoded table name that doesn't match
@@ -11,8 +11,9 @@ not attempt to build it, flag it via `request-feedback` instead.
11
11
  ## File Structure Constraints
12
12
 
13
13
  - **Strict Path Limitation**: `src/slices/{contextSlug}/{laneSlug}/{slicename}/*` — the one
14
- exception is `src/pages/`, where multiple slices sharing one screen get composed together (see
15
- guideline 8 below)
14
+ exceptions are `src/pages/`, where multiple slices sharing one screen get composed together (see
15
+ guideline 7 below), and — for the **first slice only** — `src/App.tsx` plus the Vite demo files it
16
+ replaces (guideline 10 below); `index.html` and the demo `src/index.css` for every slice (guideline 11)
16
17
  - **Slice Organization**: Each feature/domain should be organized as a separate slice
17
18
 
18
19
  ## Code Standards
@@ -27,7 +28,7 @@ not attempt to build it, flag it via `request-feedback` instead.
27
28
  2. Maintain clear separation of concerns within each slice
28
29
  3. Headers, auth, `fetch`, and Supabase table access live exclusively in `src/lib/api.ts` — a
29
30
  slice component never imports `src/lib/supabase.ts` itself, never calls `fetch`, and never
30
- calls `supabase.from(...)` directly; it calls `postCommand`/`queryReadModel` and only contains
31
+ calls `supabase.from(...)` directly; when the backend slice published a UI contract (`ui-contract` skill, `slice.json` `notes[]`) the slice is built *from* it — path, parameters, body and every error response — and falls back to `apiEndpoint`/`fields[]` only when none exists; it calls `postCommand`/`getReadModel`/`queryReadModel` and only contains
31
32
  business logic (validation, request-body shaping, response handling)
32
33
  4. A command's/read-model's screen markup (`meta.pages` on its actor HTML_SCREEN node, scoped to
33
34
  this slice's `meta.marks` when the screen is shared) is the visual blueprint — translate it into
@@ -82,6 +83,18 @@ not attempt to build it, flag it via `request-feedback` instead.
82
83
  the app, not an arbitrary per-component index. See `build-state-change`/`build-state-view`'s
83
84
  samples step for the full convention.
84
85
 
86
+ 10. **The first slice also builds the base page.** `src/App.tsx` ships as the Vite demo. When the
87
+ slice being built is the first one in the project (no other folder under `src/slices/`), replace
88
+ it with a minimal base page rendering that slice's page or component and delete the demo-only
89
+ files; no router, no new dependency. Later slices never touch `src/App.tsx`. See
90
+ `build-state-change` Step 8 / `build-state-view` Step 7.
91
+ 11. **Every slice loads its screen's styles.** The mockup's classes are meaningless without the CSS
92
+ framework the board renders them with. Each slice detects it (`cssFrameworks`, else the classes
93
+ in `meta.pages`; none → Vite's default stays as is), makes sure `index.html` loads it, and neutralises the Vite demo
94
+ `src/index.css` if it's still there — automatically, idempotently, so an existing project heals
95
+ on its next build. Never hand-write CSS to imitate a framework. See `build-state-change` Step 7 /
96
+ `build-state-view` Step 6.
97
+
85
98
  At the start of every session, read `.build-kit/AGENTS.md` if it exists to load accumulated project learnings.
86
99
 
87
100
  When starting to work on a slice, invoke the `update-slice-status` skill with `InProgress` status before doing anything else.
@@ -124,6 +137,7 @@ src/lib/supabase.ts # Supabase client singleton
124
137
  src/lib/api.ts # postCommand/queryReadModel + auth headers + mock-mode switch (shared, not per-slice)
125
138
  src/lib/<CrossCutting>Provider.tsx # only for concerns every slice might need (session, theme, ...)
126
139
  src/pages/<ScreenTitle>.tsx # only when >1 slice shares a screen title — composes their components
140
+ src/App.tsx # base page — replaces the Vite demo when the first slice is built, untouched afterwards
127
141
  src/slices/<contextSlug>/<laneSlug>/<sliceFolder>/
128
142
  <CommandTitle>.tsx # STATE_CHANGE slice — component (logic inline by default)
129
143
  use<CommandTitle>.ts # only if the logic was genuinely worth splitting out
@@ -84,6 +84,58 @@ export async function postCommand<TResponse = unknown>(
84
84
  return payload as TResponse;
85
85
  }
86
86
 
87
+ /**
88
+ * Fill an OpenAPI path template (`/api/customers/{customerId}/reservation`) with values,
89
+ * URL-encoding each one. Throws if a `{param}` has no value — a contract's path parameters are
90
+ * required. Use the contract's path verbatim as `template`; never hand-assemble a URL.
91
+ */
92
+ export function buildPath(template: string, params: Record<string, string | number> = {}): string {
93
+ return template.replace(/\{(\w+)\}/g, (_, name: string) => {
94
+ const value = params[name];
95
+ if (value === undefined) throw new Error(`Missing path parameter "${name}" for ${template}`);
96
+ return encodeURIComponent(String(value));
97
+ });
98
+ }
99
+
100
+ function mockSample<T>(samples: Record<string, T> | undefined, what: string): T {
101
+ const n = activeMockSample();
102
+ const sample = samples?.[n];
103
+ if (sample === undefined) {
104
+ throw new Error(
105
+ `Mock mode is on (VITE_MOCK_SAMPLE=${n}) but no sample-${n}.json exists for ${what} — add one under this component's samples/ folder.`,
106
+ );
107
+ }
108
+ return sample;
109
+ }
110
+
111
+ /**
112
+ * GET a read model from the backend's published `GET` UI contract (`path` already filled via
113
+ * `buildPath`, `query` = the contract's query `parameters`). Resolves with the parsed JSON, or
114
+ * throws `ApiError` on a non-2xx response — the status and body let a component render each
115
+ * documented error response as its own state. Mock mode serves `samples` like `queryReadModel`.
116
+ */
117
+ export async function getReadModel<T>(
118
+ path: string,
119
+ query?: Record<string, string | number | boolean | undefined>,
120
+ samples?: Record<string, T>,
121
+ ): Promise<T> {
122
+ if (isMockMode()) return mockSample(samples, `GET ${path}`);
123
+
124
+ const qs = new URLSearchParams(
125
+ Object.entries(query ?? {}).filter(([, v]) => v !== undefined).map(([k, v]) => [k, String(v)]),
126
+ ).toString();
127
+ const headers = await getAuthHeaders();
128
+ const res = await fetch(`${API_BASE}${path}${qs ? `?${qs}` : ''}`, { method: 'GET', headers });
129
+
130
+ const payload = await res.json().catch(() => undefined);
131
+ if (!res.ok) {
132
+ const message =
133
+ (payload && (payload.error || payload.message)) || `Request failed with status ${res.status}`;
134
+ throw new ApiError(message, res.status, payload);
135
+ }
136
+ return payload as T;
137
+ }
138
+
87
139
  export interface ReadModelParams {
88
140
  /** `readmodel.apiEndpoint`, verbatim — the Postgres table/view name. */
89
141
  table: string;
@@ -105,16 +157,7 @@ export async function queryReadModel<T>(
105
157
  params: ReadModelParams,
106
158
  samples?: Record<string, T>,
107
159
  ): Promise<T> {
108
- if (isMockMode()) {
109
- const n = activeMockSample();
110
- const sample = samples?.[n];
111
- if (sample === undefined) {
112
- throw new Error(
113
- `Mock mode is on (VITE_MOCK_SAMPLE=${n}) but no sample-${n}.json exists for table "${params.table}" — add one under this component's samples/ folder.`,
114
- );
115
- }
116
- return sample;
117
- }
160
+ if (isMockMode()) return mockSample(samples, `table "${params.table}"`);
118
161
 
119
162
  let query = supabase.from(params.table).select(params.select);
120
163
  for (const [column, value] of Object.entries(params.filters ?? {})) {