@eventmodelers/cli 1.0.54 → 1.0.55

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 (48) hide show
  1. package/README.md +6 -0
  2. package/cli.js +24 -0
  3. package/package.json +1 -1
  4. package/shared/build-kit/lib/ralph.js +103 -5
  5. package/shared/skills/learn-eventmodelers-api/SKILL.md +3 -0
  6. package/stacks/react/templates/.claude/skills/build-automation/SKILL.md +42 -0
  7. package/stacks/react/templates/.claude/skills/build-state-change/SKILL.md +43 -0
  8. package/stacks/react/templates/.claude/skills/build-state-view/SKILL.md +42 -0
  9. package/stacks/react/templates/build-kit/CLAUDE.md +62 -0
  10. package/stacks/react/templates/build-kit/README.md +79 -0
  11. package/stacks/react/templates/build-kit/lib/AGENT.md +47 -0
  12. package/stacks/react/templates/build-kit/lib/backend-prompt.md +135 -0
  13. package/stacks/react/templates/build-kit/lib/prompt.md +139 -0
  14. package/stacks/react/templates/build-kit/lib/ralph.js +508 -0
  15. package/stacks/react/templates/build-kit/package.json +9 -0
  16. package/stacks/react/templates/build-kit/ralph-claude.js +107 -0
  17. package/stacks/react/templates/build-kit/ralph-ollama.js +40 -0
  18. package/stacks/supabase-react/templates/.claude/skills/build-state-change/SKILL.md +305 -0
  19. package/stacks/supabase-react/templates/.claude/skills/build-state-view/SKILL.md +238 -0
  20. package/stacks/supabase-react/templates/.claude/skills/init-style-guide/SKILL.md +60 -0
  21. package/stacks/supabase-react/templates/.claude/skills/learn-styleguide/SKILL.md +28 -0
  22. package/stacks/supabase-react/templates/.claude/skills/learn-styleguide/references/README.md +5 -0
  23. package/stacks/supabase-react/templates/build-kit/CLAUDE.md +149 -0
  24. package/stacks/supabase-react/templates/build-kit/lib/AGENT.md +47 -0
  25. package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +139 -0
  26. package/stacks/supabase-react/templates/build-kit/lib/prompt.md +145 -0
  27. package/stacks/supabase-react/templates/root/.env.example +12 -0
  28. package/stacks/supabase-react/templates/root/.oxlintrc.json +9 -0
  29. package/stacks/supabase-react/templates/root/README.md +49 -0
  30. package/stacks/supabase-react/templates/root/index.html +13 -0
  31. package/stacks/supabase-react/templates/root/package.json +26 -0
  32. package/stacks/supabase-react/templates/root/public/favicon.svg +1 -0
  33. package/stacks/supabase-react/templates/root/public/icons.svg +24 -0
  34. package/stacks/supabase-react/templates/root/src/App.css +184 -0
  35. package/stacks/supabase-react/templates/root/src/App.tsx +122 -0
  36. package/stacks/supabase-react/templates/root/src/assets/hero.png +0 -0
  37. package/stacks/supabase-react/templates/root/src/assets/react.svg +1 -0
  38. package/stacks/supabase-react/templates/root/src/assets/vite.svg +1 -0
  39. package/stacks/supabase-react/templates/root/src/index.css +111 -0
  40. package/stacks/supabase-react/templates/root/src/lib/api.ts +127 -0
  41. package/stacks/supabase-react/templates/root/src/lib/supabase.ts +10 -0
  42. package/stacks/supabase-react/templates/root/src/main.tsx +10 -0
  43. package/stacks/supabase-react/templates/root/src/slices/.gitkeep +0 -0
  44. package/stacks/supabase-react/templates/root/src/vite-env.d.ts +13 -0
  45. package/stacks/supabase-react/templates/root/tsconfig.app.json +26 -0
  46. package/stacks/supabase-react/templates/root/tsconfig.json +7 -0
  47. package/stacks/supabase-react/templates/root/tsconfig.node.json +23 -0
  48. package/stacks/supabase-react/templates/root/vite.config.ts +7 -0
@@ -0,0 +1,305 @@
1
+ ---
2
+ name: build-state-change
3
+ description: Build a write-side React slice for the Supabase stack — one dedicated component per COMMAND (splitting into a hook and/or a colocated Context/Provider only when the slice actually needs one) that POSTs to its slice-defined apiEndpoint via the shared api.ts, driven by the marked-up region of its actor HTML_SCREEN
4
+ ---
5
+
6
+ # Build State Change Slice
7
+
8
+ > Before doing anything else, read the slice definition from
9
+ > `.build-kit/.slices/<contextSlug>/<sliceFolder>/slice.json`. This file (plus, when a screen is
10
+ > involved, the live screen node fetched in Step 2) is the **source of truth** for every field,
11
+ > event, and endpoint — never invent a field, prop, or path that isn't there.
12
+
13
+ ---
14
+
15
+ ## What a State Change Slice is
16
+
17
+ One `COMMAND` element (`slice.json.commands[]`) that, when submitted, causes one or more `EVENT`
18
+ elements (`slice.json.events[]`) to be recorded by the backend. This stack builds only the
19
+ **frontend half**: a dedicated React component that collects the command's data (via props — this
20
+ component never fetches or owns its own data) and POSTs it to an already-existing backend
21
+ endpoint, via `postCommand` in `src/lib/api.ts` — never `fetch` directly. It does not generate any
22
+ server-side code.
23
+
24
+ Every component must be renderable and testable **in isolation**, with no real backend running —
25
+ see Step 4 (numbered `samples/`) and `src/lib/api.ts`'s `VITE_DATA_MODE=mock`, which makes
26
+ `postCommand` resolve locally instead of hitting the network.
27
+
28
+ ## Step 1 — Read slice.json
29
+
30
+ Extract:
31
+ - **title**, **context** — slice identity, used for file paths (see Step 3).
32
+ - **commands[0]** — the command element. Each `Element` carries:
33
+ - `title` — the command name, e.g. `"ReserveBike"`.
34
+ - `fields[]` — one entry per data field (`name`, `type`, `optional?`, `cardinality?`, `example?`,
35
+ `subfields?`). This is the **exact and complete** set of props the component takes — no more,
36
+ no fewer.
37
+ - `apiEndpoint` — the path to POST to. **Read this literally from slice.json when it's present**
38
+ — do not guess a REST convention, invent a path, or infer one from the command name while a real
39
+ one exists. **When `apiEndpoint` is missing** (no backend endpoint decided yet), flag it but keep
40
+ building — this is a partial variant of `request-feedback`, not a full stop:
41
+ 1. Post a comment on this slice node naming the command and stating the endpoint is missing
42
+ (`mcp__eventmodelers__add_comment`, same call `request-feedback` Step 3 uses), and mark the
43
+ slice `Blocked` (`mcp__eventmodelers__update_slice_status`, same as `request-feedback` Step 4).
44
+ 2. Unlike a normal `request-feedback` escalation, **do not stop here** — continue on to build the
45
+ component exactly as normal, but against a mocked backend: give `postCommand` a clearly
46
+ provisional path, e.g. `/api/mock/<command-title-kebab-case>`, with a one-line comment marking
47
+ it as a placeholder pending the real endpoint. `postCommand` already resolves locally under
48
+ `VITE_DATA_MODE=mock` regardless of what the path string is (see `src/lib/api.ts`), so the
49
+ component is fully functional offline either way — only make Step 4's samples do extra work in
50
+ this case: cover every `command.fields[]` entry with realistic, meaningful values (never
51
+ placeholders like `"foo"`/`"test"`), plus whatever edge cases `specifications[]` call out,
52
+ since those samples are the only data this slice will see until a real endpoint is wired in.
53
+ 3. Leave the slice's status as `Blocked` when you finish (see this stack's `CLAUDE.md` "Building
54
+ a Slice" step 6) — the built, mocked component still needs a real endpoint before it's done,
55
+ so don't flip it to `Done`.
56
+ - `description` — implementation hints (validation rules, business constraints).
57
+ - **events[]** — informational only (what the backend is expected to emit); this stack does not
58
+ act on them directly.
59
+ - **screens[]** — the actor `SCREEN` element(s) that trigger this command, if any. Empty when the
60
+ command has no UI trigger (rare — build the component from fields alone in that case, skip Step
61
+ 2).
62
+ - **specifications[]** — GWT scenarios; treat as acceptance criteria for the component's business
63
+ logic (e.g. "given empty `email`, submit is disabled").
64
+
65
+ > **Comments**: each element carries `comments: string[]` (board comments) — read them as
66
+ > implementation hints, and resolve consumed ones via
67
+ > `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve`.
68
+
69
+ ## Step 2 — Read the screen, and especially its marks
70
+
71
+ `slice.json`'s `screens[]` entries only carry `id`/`title`/`fields[]`/`description` (fields already
72
+ scoped to this slice) — not the actual markup. Fetch the live node to get that:
73
+
74
+ ```
75
+ mcp__eventmodelers__get_node { "boardId": "<BOARD_ID>", "nodeId": "<screen.id>" }
76
+ ```
77
+
78
+ This returns the full `meta`, including `pages: string[]` (one HTML fragment per page) and,
79
+ if the screen is shared across several slices, `marks: [{ id, color, pageIndex, blurOutside?,
80
+ whiteOutside? }]`.
81
+
82
+ - **If `meta.marks` is non-empty**: the same visual screen is shared by multiple slices, each
83
+ highlighting a different part. Find the mark(s) belonging to this node, then in
84
+ `meta.pages[mark.pageIndex]` locate the element carrying `data-em-mark-id="<mark.id>"` — **that
85
+ HTML subtree, not the rest of the page, is the blueprint for what this component builds.**
86
+ Ignore markup outside it; it belongs to other slices.
87
+ - **If `meta.marks` is empty**: the whole page (every entry in `meta.pages`, in order, for a
88
+ multi-step flow) is the blueprint.
89
+ - **If `screens[]` was empty in Step 1**: there is no visual reference — build the component from
90
+ `fields[]` alone, with plain, unstyled form markup.
91
+
92
+ If this project has a `learn-styleguide` skill installed (`.claude/skills/learn-styleguide/`),
93
+ read its references before turning markup into JSX and prefer its tokens/component classes over
94
+ whatever ad hoc styling the mockup used — the mockup gives you *structure and content*, the style
95
+ guide gives you the real visual language when the two disagree.
96
+
97
+ **Also resolve this screen's lane** (skip if `screens[]` was empty in Step 1 — see Step 3's file
98
+ path): the actor row this screen sits in becomes a folder segment. Fetch it cheaply with:
99
+
100
+ ```
101
+ mcp__eventmodelers__get_board_outline { "boardId": "<BOARD_ID>", "chapterId": "<screen node's chapterId>" }
102
+ ```
103
+
104
+ This returns `{id, type, title, lane}` for every node in the chapter — find the entry whose `id`
105
+ matches `screen.id` and take its `lane`. Slugify it the same way `contextSlug` is derived
106
+ (lowercase, spaces to hyphens, non-alphanumeric stripped) to get `laneSlug`.
107
+
108
+ ## Step 3 — Build the dedicated component
109
+
110
+ **File**: `src/slices/<contextSlug>/<laneSlug>/<sliceFolder>/<CommandTitle>.tsx` — `contextSlug` and
111
+ `sliceFolder` are derived exactly as in `load-slice` (lowercase-slugified context; slice title
112
+ lowercased with spaces removed and any `slice:` prefix stripped); `laneSlug` comes from Step 2
113
+ above. Omit the `<laneSlug>` segment (fall back to `<contextSlug>/<sliceFolder>/`) only for the
114
+ rare headless command with no `screens[]` at all — there's no lane to resolve without a screen.
115
+ Note this nests one level deeper than the staging data in
116
+ `.build-kit/.slices/<contextSlug>/<sliceFolder>/slice.json` — `load-slice`'s own local index has no
117
+ lane concept, so the frontend path isn't a strict 1:1 mirror of it. `CommandTitle` is the command's
118
+ `title` verbatim (e.g. `ReserveBike.tsx`).
119
+
120
+ **Markup**: translate the blueprint HTML from Step 2 into JSX **1:1** — same tags, same classes
121
+ (Bulma or whatever the mockup used), same layout and copy. This is not a redesign; the screen *is*
122
+ the design. Only change what must become dynamic:
123
+ - A field's example value becomes that input's controlled `value`.
124
+ - An element repeated for a `cardinality: "List"` field becomes a `.map()` over that field.
125
+ - `class`/`style`/structure otherwise stay exactly as drawn.
126
+
127
+ **Props**: exactly `command.fields[]`, typed per each field's `type` (`String`→`string`,
128
+ `Boolean`→`boolean`, `Int`/`Long`→`number`, etc. — `subfields[]` become a nested object type),
129
+ respecting `optional`. Add two callback props: `onSuccess?: (result: TResponse) => void` and
130
+ `onError?: (error: ApiError) => void`. **No other inputs** — no context/store reads, no fetching
131
+ inside the component. All business data the component needs arrives via props from its parent.
132
+
133
+ **Default — keep the logic in the component.** For an ordinary command (one form, one submit
134
+ action, which is most of them) local state and the `postCommand` call live directly inside the
135
+ component. This is the common case and needs nothing more:
136
+
137
+ ```tsx
138
+ // ReserveBike.tsx
139
+ import { useState } from 'react';
140
+ import { postCommand, ApiError } from '../../../../lib/api'; // one less '../' if this command has no lane segment
141
+
142
+ export interface ReserveBikeProps {
143
+ bikeId: string;
144
+ customerId: string;
145
+ onSuccess?: (result: unknown) => void;
146
+ onError?: (error: ApiError) => void;
147
+ }
148
+
149
+ export function ReserveBike({ bikeId, customerId, onSuccess, onError }: ReserveBikeProps) {
150
+ const [submitting, setSubmitting] = useState(false);
151
+ const [error, setError] = useState<string | null>(null);
152
+
153
+ const handleSubmit = async () => {
154
+ setSubmitting(true);
155
+ setError(null);
156
+ try {
157
+ const result = await postCommand('/api/reservebike', { bikeId, customerId });
158
+ onSuccess?.(result);
159
+ } catch (err) {
160
+ const message = err instanceof ApiError ? err.message : 'Something went wrong';
161
+ setError(message);
162
+ if (err instanceof ApiError) onError?.(err);
163
+ } finally {
164
+ setSubmitting(false);
165
+ }
166
+ };
167
+
168
+ // ...JSX translated from the marked screen fragment, wired to handleSubmit...
169
+ }
170
+ ```
171
+
172
+ The component owns only: local form/loading/error state, whatever client-side validation the
173
+ `specifications[]`/field `optional` flags imply, building the request body from props, and calling
174
+ `postCommand(command.apiEndpoint, body)`. It never imports `supabase`, builds headers, or calls
175
+ `fetch` directly — that is exclusively `src/lib/api.ts`'s job. In `VITE_DATA_MODE=mock`,
176
+ `postCommand` never hits the network at all — it resolves immediately, so this component is fully
177
+ exercisable (submitting/success/error UI states) offline.
178
+
179
+ **Extract a `use<CommandTitle>.ts` hook only when it earns its keep** — e.g. the exact same logic
180
+ is genuinely reused by more than one component in this slice, or the non-rendering logic (several
181
+ async steps, non-trivial multi-field validation) has grown large enough that pulling it out of the
182
+ component measurably improves readability. Don't extract a hook by default, for uniformity with
183
+ other slices, or "for consistency" — a small command's logic reads perfectly fine inline, and a
184
+ hook file that's just a thin wrapper around `postCommand` is pure ceremony working against
185
+ deletability, not for it (one more file to keep in sync instead of one). When it *is* warranted,
186
+ move the same logic verbatim into a colocated `use<CommandTitle>.ts` and have the component call
187
+ it — nothing about the logic itself changes, only where it lives.
188
+
189
+ ## Step 4 — Add numbered samples
190
+
191
+ Every command component ships a `samples/` folder colocated next to it, mirroring
192
+ `build-state-view`'s convention:
193
+ `src/slices/<contextSlug>/<laneSlug>/<sliceFolder>/samples/sample-<N>.json`. Since this component
194
+ is driven entirely by props, each sample is a **full set of prop values** — there's no query for
195
+ `postCommand` to serve (it's already mock-aware on its own, see Step 3). A sample is for whatever
196
+ renders this component in isolation (a `src/pages/` composition, or ad hoc while developing) to
197
+ spread as props, e.g. `<ReserveBike {...sample1} onSuccess={...} />`.
198
+
199
+ Add `sample-1.json` for the ordinary case (`command.fields[].example` values); add more numbered
200
+ ones for other scenarios `specifications[]` calls out (an edge case, an `optional` field omitted,
201
+ ...). As in `build-state-view`, the same number across different components' samples is meant to
202
+ represent one coherent scenario across the app, not an arbitrary per-component index.
203
+
204
+ ## Step 5 — Context/Provider, only if the marked fragment needs shared local state
205
+
206
+ Skip this step for the overwhelming majority of commands — an ordinary single-form command is
207
+ fully covered by Step 3 alone, and adding a Context there is needless indirection with nothing to
208
+ show for it.
209
+
210
+ Reach for a Context **only** when the blueprint from Step 2 decomposes into several sibling
211
+ components that must coordinate (e.g. a multi-step wizard, a shared "selected row" between a list
212
+ and a submit bar) — i.e. state that a single component's own `useState` genuinely cannot hold
213
+ because more than one sibling needs to read or change it. Follow the same `Context`/`Provider`/hook
214
+ shape this project's components use elsewhere (see `useReadOnly`/`ReadOnlyProvider` and
215
+ `useUser`/`UserProvider` for the two variants):
216
+
217
+ ```tsx
218
+ // ReserveBikeContext.tsx — colocated in the SAME slice folder, never in src/lib
219
+ import { createContext, useContext } from 'react';
220
+
221
+ interface ReserveBikeContextType {
222
+ selectedBikeId: string | null;
223
+ selectBike: (bikeId: string) => void;
224
+ }
225
+
226
+ const ReserveBikeContext = createContext<ReserveBikeContextType | null>(null);
227
+
228
+ export function ReserveBikeProvider({ children }: { children: React.ReactNode }) {
229
+ // ...state shared by this slice's sibling components...
230
+ return <ReserveBikeContext.Provider value={{ selectedBikeId, selectBike }}>{children}</ReserveBikeContext.Provider>;
231
+ }
232
+
233
+ export function useReserveBikeContext() {
234
+ const ctx = useContext(ReserveBikeContext);
235
+ if (!ctx) throw new Error('useReserveBikeContext must be used within ReserveBikeProvider');
236
+ return ctx;
237
+ }
238
+ ```
239
+
240
+ Throw when the context is missing (as above) — there is no sensible default for slice-scoped state,
241
+ and failing fast surfaces a sub-component rendered outside its own slice's provider immediately.
242
+ A plain default value (no throw) is only appropriate for a genuinely app-wide, always-available
243
+ value — that belongs in `src/lib`, not here (see **Deletability** in `.build-kit/CLAUDE.md`).
244
+
245
+ **File**: `src/slices/<contextSlug>/<laneSlug>/<sliceFolder>/<CommandTitle>Context.tsx` — colocated
246
+ with the hook and component, never imported by another slice. The command's own `.tsx` file
247
+ renders the `Provider` at its root, wrapping the sibling sub-components that call the context hook.
248
+
249
+ ## Step 6 — Compose onto the shared page, if this screen's name is shared
250
+
251
+ A screen's **title is the page name**. The `html-screen` skill gives each slice its own copy of a
252
+ shared visual screen — one node per slice, each with a different mark — so two slices whose
253
+ screens carry the **same title** are meant to sit together on **one real page**, arranged exactly
254
+ as their marks show. The number of *distinct* screen titles across the board is a direct hint at
255
+ how many pages this app needs — not one page per slice.
256
+
257
+ 1. Check whether any other slice references a screen with the same `title` as this slice's screen.
258
+ Search every context's local index, not just this slice's own:
259
+ ```bash
260
+ grep -l '"title": *"<ScreenTitle>"' .build-kit/.slices/*/index.json
261
+ ```
262
+ (Run `/load-slice` with no filter first if the board's other slices haven't been pulled locally
263
+ yet — this check is only as complete as what's on disk.)
264
+ 2. **No match** (this is the only slice on this screen so far): nothing further to do here — this
265
+ slice's component stands alone for now. A shared page gets created the first time a *second*
266
+ slice on the same screen is built.
267
+ 3. **Match found**: this screen is a shared page. Its composition lives at
268
+ `src/pages/<ScreenTitle>.tsx` (PascalCase, spaces stripped) — the **one place** in this codebase
269
+ allowed to import across slice folders; a slice component itself never does (see **Deletability**
270
+ in `.build-kit/CLAUDE.md`).
271
+ - Fetch the **whole, unmarked** page markup for this screen (every entry in `meta.pages`, from
272
+ Step 2) — this is the page-level layout blueprint, not just this slice's marked fragment.
273
+ - Translate it 1:1 into JSX, same rule as Step 3 — but at each position carrying a
274
+ `data-em-mark-id`, render that mark's owning slice's component instead of the mockup's static
275
+ content there, passing whatever props it declares. Only substitute positions whose slice
276
+ component already exists; leave any not-yet-built slice's marked area as the mockup's static
277
+ placeholder until that slice is built and this step runs again for it.
278
+ - The page owns threading shared inputs (route/query params, the current session, etc.) down as
279
+ props to each slice component — a slice component still only knows its own declared props, not
280
+ the rest of the page or its siblings.
281
+ - If `src/pages/<ScreenTitle>.tsx` already exists, update only this slice's marked position;
282
+ leave every other position (already-wired slices, and untouched mockup content) exactly as is.
283
+
284
+ ## Step 7 — Verify against slice.json
285
+
286
+ After writing the hook/component (and context/page, if any), confirm:
287
+ - Every prop matches a `command.fields[]` entry exactly (name + type) — no invented or missing
288
+ fields.
289
+ - The `postCommand` call uses `command.apiEndpoint` verbatim — or, when it was missing at build
290
+ time, the provisional `/api/mock/...` path noted in a comment (Step 1).
291
+ - At least one numbered sample exists under this slice's `samples/` folder, shaped exactly like
292
+ this component's props.
293
+ - If a screen was read in Step 2, the rendered JSX matches the marked (or whole, if unmarked)
294
+ HTML fragment structurally — no invented buttons/fields the mockup didn't show, nothing from
295
+ the mockup silently dropped.
296
+ - Nothing in this slice's folder is imported from another slice's folder, and nothing outside this
297
+ slice's folder imports from it except `src/pages/` (Step 6) — deleting the folder should delete
298
+ the feature cleanly.
299
+
300
+ ## Quality gate
301
+
302
+ Run `npm run build` (`tsc -b && vite build`) and `npm run lint` (`oxlint`) — both must pass clean.
303
+ There is no component test runner configured in this stack yet; if a slice's `specifications[]`
304
+ demand behavioral tests, flag that gap via `request-feedback` rather than inventing a test setup
305
+ ad hoc.
@@ -0,0 +1,238 @@
1
+ ---
2
+ name: build-state-view
3
+ description: Build a read-side React slice for the Supabase stack — a component tailored to its READMODEL(s), reading its table/view through the shared api.ts (never Supabase directly), which is what lets it also be tested/previewed in isolation from numbered samples/sample-N.json files
4
+ ---
5
+
6
+ # Build State View Slice
7
+
8
+ > Before doing anything else, read the slice definition from
9
+ > `.build-kit/.slices/<contextSlug>/<sliceFolder>/slice.json`. This file (plus, when a screen is
10
+ > involved, the live screen node fetched in Step 2) is the **source of truth** for every field and
11
+ > data source — never invent a field, prop, or table name that isn't there.
12
+
13
+ ---
14
+
15
+ ## What a State View Slice is
16
+
17
+ One or more `READMODEL` elements (`slice.json.readmodels[]`) that display data back to the user.
18
+ Read models here are **tailor-made for one component** — the default is a strict 1:1 correlation
19
+ between a read model and the component that renders it. A single component may combine more than
20
+ one read model (e.g. a dashboard-style screen), but the reverse — one read model reused by several
21
+ different components — is rare; if the same read model turns up wired into more than one slice's
22
+ screen, that's unusual enough to double-check rather than assume is intentional.
23
+
24
+ This stack builds only the **frontend half**, and its read architecture is **direct table access,
25
+ not a custom read API**: a read model is typically a real, persisted Postgres table (a projection
26
+ the backend keeps up to date), protected by Row Level Security policies that already enforce which
27
+ rows a given user may see. That RLS enforcement is exactly what makes it correct — not merely
28
+ convenient — for a component to read it straight from Supabase, with no bespoke backend endpoint in
29
+ between, the same way `miro-eventmodeling` and this stack's other Supabase-backed components do.
30
+
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
33
+ ever call `postCommand`. `queryReadModel` is the real Supabase call in production, but in mock mode
34
+ it serves a numbered sample instead (see Step 4) — routing every read through this one seam is what
35
+ makes a component testable/previewable in isolation, with zero Supabase project needed.
36
+
37
+ Assume a read model names a real persisted table unless its `comments[]`, `description`, or a
38
+ custom prompt on the slice explicitly says otherwise (e.g. it's actually a view joining several
39
+ tables, or computed on the fly) — check those before assuming a plain table lookup.
40
+
41
+ ## Step 1 — Read slice.json
42
+
43
+ Extract:
44
+ - **title**, **context** — slice identity, used for file paths (see Step 3).
45
+ - **readmodels[]** — one entry per read model this slice's screen displays. Each `Element` carries:
46
+ - `title` — the read model's name, e.g. `"ActiveReservationView"`.
47
+ - `fields[]` — the exact and complete set of columns/values the component displays — no more,
48
+ no fewer.
49
+ - `apiEndpoint` — in this stack, **the Postgres table or view name to query**, not a REST path.
50
+ **Read it literally from slice.json when it's present** — never invent or guess a table name
51
+ while a real one exists. **When it's missing** (no table/view decided yet), flag it but keep
52
+ building — this is a partial variant of `request-feedback`, not a full stop:
53
+ 1. Post a comment on this slice node naming the read model and stating the table/view is missing
54
+ (`mcp__eventmodelers__add_comment`, same call `request-feedback` Step 3 uses), and mark the
55
+ slice `Blocked` (`mcp__eventmodelers__update_slice_status`, same as `request-feedback` Step 4).
56
+ 2. Unlike a normal `request-feedback` escalation, **do not stop here** — continue on to build the
57
+ component exactly as normal, but against a mocked read: give `queryReadModel` a clearly
58
+ provisional table name, e.g. `mock_<read_model_title_snake_case>`, with a one-line comment
59
+ marking it as a placeholder pending the real table/view. In mock mode `queryReadModel` never
60
+ touches Supabase regardless of the table name (see `src/lib/api.ts`), so the component is
61
+ fully functional offline either way — only make Step 4's samples do extra work in this case:
62
+ cover every `readmodel.fields[]` entry with realistic, meaningful values (never placeholders
63
+ like `"foo"`/`"test"`), plus whatever edge cases `specifications[]` call out, since those
64
+ samples are the only data this slice will see until a real table/view is wired in.
65
+ 3. Leave the slice's status as `Blocked` when you finish (see this stack's `CLAUDE.md` "Building
66
+ a Slice" step 6) — the built, mocked component still needs a real table/view before it's done,
67
+ so don't flip it to `Done`.
68
+ - `description` — implementation hints (filtering rules, sort order, business meaning).
69
+ - **screens[]** — the actor `SCREEN` element(s) that display this data, if any (see Step 2).
70
+ - **specifications[]** — GWT scenarios/storyline; treat as acceptance criteria (e.g. "given no
71
+ active reservation, show an empty state").
72
+
73
+ > **Comments**: as in `build-state-change` — resolve consumed ones via the same node-comment
74
+ > endpoint.
75
+
76
+ ## Step 2 — Read the screen, and especially its marks
77
+
78
+ Identical to `build-state-change` Step 2 — fetch the live screen node
79
+ (`mcp__eventmodelers__get_node`), find this slice's marked fragment in `meta.pages`/`meta.marks`
80
+ (or the whole page if unmarked), and treat it as the blueprint. The same `learn-styleguide` note
81
+ applies: prefer its tokens/classes over the mockup's ad hoc styling where they disagree.
82
+
83
+ Also resolve this screen's lane the same way — `mcp__eventmodelers__get_board_outline` on the
84
+ screen's chapter, match this node's `id` to get its `lane`, slugify it to `laneSlug` — skip only if
85
+ `screens[]` was empty in Step 1.
86
+
87
+ ## Step 3 — Build the dedicated component
88
+
89
+ **File**: `src/slices/<contextSlug>/<laneSlug>/<sliceFolder>/<ReadModelTitle>.tsx` (omit
90
+ `<laneSlug>` only for the rare read model with no screen at all), `contextSlug`/`sliceFolder`/
91
+ `laneSlug` derived exactly as in `build-state-change`. If a component genuinely combines more than
92
+ one read model, name it after the screen/purpose rather than forcing one read model's title onto a
93
+ multi-read-model component.
94
+
95
+ **Markup**: same rule as `build-state-change` — translate the blueprint from Step 2 1:1 into JSX;
96
+ the mockup is the design, not a starting point for a redesign.
97
+
98
+ **Props**: whatever the component needs to *scope* its query (e.g. `customerId`) — passed in from
99
+ its parent, never read from a global store. The queried data itself is **not** a prop — the
100
+ component fetches it.
101
+
102
+ **Default — call `queryReadModel` from `src/lib/api.ts` inside the component**. Never `supabase.from(...)`
103
+ directly, never a raw `fetch`:
104
+
105
+ ```tsx
106
+ // ActiveReservationView.tsx
107
+ import { useEffect, useState } from 'react';
108
+ import { queryReadModel } from '../../../../lib/api'; // one less '../' if this read model has no lane segment
109
+
110
+ export interface ActiveReservationViewProps {
111
+ customerId: string;
112
+ }
113
+
114
+ interface ActiveReservation {
115
+ status: string;
116
+ // ...remaining readmodel.fields[], typed per each Field.type
117
+ }
118
+
119
+ const samples = import.meta.glob<{ default: ActiveReservation }>('./samples/sample-*.json', { eager: true });
120
+ const samplesByNumber = Object.fromEntries(
121
+ Object.entries(samples).map(([path, mod]) => [path.match(/sample-(\d+)\.json$/)![1], mod.default]),
122
+ );
123
+
124
+ export function ActiveReservationView({ customerId }: ActiveReservationViewProps) {
125
+ const [data, setData] = useState<ActiveReservation | null>(null);
126
+ const [loading, setLoading] = useState(true);
127
+ const [error, setError] = useState<string | null>(null);
128
+
129
+ useEffect(() => {
130
+ let cancelled = false;
131
+ setLoading(true);
132
+ queryReadModel<ActiveReservation>(
133
+ {
134
+ table: 'active_reservations', // readmodel.apiEndpoint, verbatim
135
+ select: 'status, ...', // every readmodel.fields[] name, by its actual column name — not '*'
136
+ filters: { customer_id: customerId },
137
+ single: true,
138
+ },
139
+ samplesByNumber,
140
+ )
141
+ .then((result) => {
142
+ if (cancelled) return;
143
+ setData(result);
144
+ })
145
+ .catch((err) => {
146
+ if (cancelled) return;
147
+ setError(err instanceof Error ? err.message : 'Something went wrong');
148
+ })
149
+ .finally(() => {
150
+ if (!cancelled) setLoading(false);
151
+ });
152
+ return () => {
153
+ cancelled = true;
154
+ };
155
+ }, [customerId]);
156
+
157
+ // ...JSX translated from the marked screen fragment. Render a sensible empty/background
158
+ // state for `data === null` (or `[]`), distinct from the loading and error states — a
159
+ // component must never assume data is always present...
160
+ }
161
+ ```
162
+
163
+ Select fields explicitly by name rather than `select('*')`. Map each field's `name` to its actual
164
+ column name — check the paired backend stack's migrations if the naming convention (e.g. camelCase
165
+ vs. snake_case) isn't obvious rather than guessing; if genuinely unclear, use `request-feedback`.
166
+
167
+ Extract a `use<ReadModelTitle>.ts` hook, or introduce a colocated `<Name>Context.tsx`, under
168
+ exactly the same conditions as `build-state-change` Steps 3-4 — only when genuinely warranted
169
+ (logic reused by more than one sibling component, or state that must be shared across them). Don't
170
+ reach for either by default; most read-model components are a query plus a render and need
171
+ nothing more.
172
+
173
+ ## Step 4 — Add numbered samples
174
+
175
+ Every read-model component ships a `samples/` folder colocated next to it:
176
+ `src/slices/<contextSlug>/<laneSlug>/<sliceFolder>/samples/sample-<N>.json` (`sample-1.json`,
177
+ `sample-2.json`, ...). Each file mimics **exactly the row shape `queryReadModel` returns** — the
178
+ same field names/types as `readmodel.fields[]`, as a single object for `single: true` or an array
179
+ otherwise; use `null`/`[]` for a sample meant to exercise the empty/background state.
180
+
181
+ Add at least one sample (`sample-1.json`) covering the ordinary case from
182
+ `readmodel.fields[].example` values; add more numbered ones for whatever other scenarios this
183
+ read model's `specifications[]` call out (e.g. an empty result, a second customer's data). The
184
+ same number across *different* components' `samples/` folders is meant to represent one coherent
185
+ scenario across the whole app (e.g. every component's `sample-2.json` together tell the story of
186
+ "a long-time customer") — keep that in mind when choosing what a given number represents, rather
187
+ than numbering arbitrarily per component.
188
+
189
+ No extra wiring is needed beyond the `import.meta.glob` shown in Step 3 — switching which sample is
190
+ active for the *entire app* is just the `VITE_MOCK_SAMPLE` env var (see `.env.example`); this
191
+ component doesn't do anything with that number itself, `queryReadModel` reads it internally.
192
+
193
+ ## Step 5 — Compose onto the shared page, if this screen's name is shared
194
+
195
+ Identical to `build-state-change` Step 5 — a screen's **title is the page name**, and the number
196
+ of *distinct* screen titles across the board is a direct hint at how many pages this app needs, not
197
+ one page per slice.
198
+
199
+ 1. Check whether any other slice references a screen with the same `title` as this slice's screen,
200
+ across every context's local index:
201
+ ```bash
202
+ grep -l '"title": *"<ScreenTitle>"' .build-kit/.slices/*/index.json
203
+ ```
204
+ (Run `/load-slice` with no filter first if the board's other slices haven't been pulled locally
205
+ yet.)
206
+ 2. **No match**: nothing further to do — this slice's component stands alone for now.
207
+ 3. **Match found**: this screen is a shared page, composed at `src/pages/<ScreenTitle>.tsx` — the
208
+ one place allowed to import across slice folders. Translate the screen's whole, unmarked
209
+ `meta.pages` markup into JSX (the page-level layout), and at each `data-em-mark-id` position
210
+ render that mark's owning slice's component (only for slices already built; leave the rest as
211
+ the mockup's static content until they are). The page threads shared inputs down as props; a
212
+ slice component still only knows its own declared props. Update only this slice's marked
213
+ position if the page file already exists.
214
+
215
+ ## Step 6 — Verify against slice.json
216
+
217
+ - Every rendered field traces back to a `readmodel.fields[]` entry — no invented or missing
218
+ fields.
219
+ - The `queryReadModel` call targets `readmodel.apiEndpoint` verbatim as `table` — or, when it was
220
+ missing at build time, the provisional `mock_...` table name noted in a comment (Step 1) — never a
221
+ raw `fetch`, a direct `supabase.from(...)` call, or a hardcoded table name that doesn't match
222
+ slice.json.
223
+ - At least one numbered sample exists under this slice's `samples/` folder, shaped exactly like
224
+ `queryReadModel`'s return value.
225
+ - The component renders a sensible empty/background state, distinct from loading/error, when the
226
+ data is `null`/`[]`.
227
+ - If a screen was read in Step 2, the rendered JSX matches the marked (or whole, if unmarked) HTML
228
+ fragment structurally.
229
+ - Nothing in this slice's folder is imported from another slice's folder, and nothing outside it
230
+ imports from it except `src/pages/` (Step 5) — deleting the folder should delete the feature
231
+ cleanly.
232
+
233
+ ## Quality gate
234
+
235
+ Run `npm run build` (`tsc -b && vite build`) and `npm run lint` (`oxlint`) — both must pass clean.
236
+ There is no component test runner configured in this stack yet; if a slice's `specifications[]`
237
+ demand behavioral tests, flag that gap via `request-feedback` rather than inventing a test setup
238
+ ad hoc.
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: init-style-guide
3
+ description: Interview the user for CSS, design tokens, brand documents, or any existing style guide material, and persist it as references inside the learn-styleguide skill so UI-generating skills (build-state-change, build-state-view) stay on-brand
4
+ ---
5
+
6
+ # Init Style Guide
7
+
8
+ Run this whenever the user wants to give this project a design system to follow — "here's our
9
+ style guide", "add our brand CSS", "use this doc for the UI" — or wants to add more material to
10
+ one already started. This skill only collects and files material; it does not itself apply any
11
+ styling to code.
12
+
13
+ ## Step 1 — Ask what they have
14
+
15
+ Ask the user what they'd like to provide. Accept any of:
16
+ - An existing CSS/SCSS file (a path) — design tokens, a component library stylesheet, a Tailwind
17
+ config, etc.
18
+ - A brand/style guide document (Markdown, PDF, plain text, a Figma export, ...) — a path.
19
+ - Pasted text/tokens directly in the conversation (e.g. a color palette, font stack, spacing
20
+ scale).
21
+ - A URL to a live, publicly reachable style guide page — only if the user gives the URL
22
+ themselves; never search for or guess one.
23
+
24
+ If they have nothing to add right now, stop — do not create empty placeholder files.
25
+
26
+ ## Step 2 — Persist each item as a reference, verbatim
27
+
28
+ For every item provided, write it into `.claude/skills/learn-styleguide/references/`. **Never
29
+ summarize, rewrite, or supplement** what was given — this is a store of source material, not an
30
+ interpretation of it.
31
+
32
+ - **A file path** → copy the file as-is into `references/<original-filename>`, preserving its
33
+ extension (`.css`, `.scss`, `.md`, `.pdf`, ...).
34
+ - **A URL** → fetch it and save the raw content into `references/<slugified-title-or-host>.md`,
35
+ with a one-line header noting the source URL and the fetch date.
36
+ - **Pasted text/tokens** → write into `references/<short-slug>.md`, wrapped in a fenced code block
37
+ when it's CSS/code, with a one-line header noting what it is and the date provided.
38
+
39
+ Pick filenames that describe the content (`brand-colors.css`, `component-library.md`,
40
+ `voice-and-tone.pdf`), never generic names like `input1`.
41
+
42
+ If `references/` already has files from a previous run, list them back to the user first so they
43
+ know what's already covered, then only add what's new. Never delete or overwrite an existing
44
+ reference unless the user confirms that specific file should be replaced.
45
+
46
+ ## Step 3 — Refresh learn-styleguide's index
47
+
48
+ In `.claude/skills/learn-styleguide/SKILL.md`, replace the `## References` section's contents with
49
+ one bullet per file now in `references/` — filename plus a short (one-line) description of what it
50
+ contains, based on a quick read of the file, never invented. Leave everything else in that file
51
+ (frontmatter, the "How to use this skill" section) untouched.
52
+
53
+ ## Step 4 — Confirm
54
+
55
+ Tell the user what was saved and where, e.g.:
56
+ ```
57
+ Saved to .claude/skills/learn-styleguide/references/:
58
+ - brand-colors.css (CSS custom properties: primary/secondary palette, spacing scale)
59
+ - voice-and-tone.md (brand document — tone, terminology to avoid)
60
+ ```
@@ -0,0 +1,28 @@
1
+ ---
2
+ name: learn-styleguide
3
+ description: This project's visual style guide / design system reference material — read before generating or redesigning any UI (screens, components). Populated and kept up to date by the init-style-guide skill.
4
+ ---
5
+
6
+ # Style Guide References
7
+
8
+ ## References
9
+
10
+ _None yet — run `init-style-guide` to add CSS, design tokens, or brand documents._
11
+
12
+ ## How to use this skill
13
+
14
+ Before turning a marked screen fragment into a component (`build-state-change`/`build-state-view`
15
+ Step 2), or designing any new screen, read whichever file(s) under `references/` are relevant to
16
+ the task at hand — colors/typography/spacing before styling, a component-library doc before
17
+ choosing markup patterns, a voice-and-tone doc before writing copy.
18
+
19
+ Prefer these references over values invented on the spot, and prefer them over an existing screen
20
+ mockup's own ad hoc inline styles when the two disagree — the mockup gives structure and content,
21
+ this skill gives the real visual language.
22
+
23
+ Do not invent references that aren't listed above. If the current task needs a design decision
24
+ these references don't cover, fall back to what the mockup/task already gives you rather than
25
+ guessing a brand rule — or ask the user, if the choice materially affects business logic.
26
+
27
+ This skill's content is entirely sourced from what the user provided via `init-style-guide` —
28
+ never add, rewrite, or supplement it with invented material.
@@ -0,0 +1,5 @@
1
+ # References
2
+
3
+ Source material dropped here by `init-style-guide`, verbatim — CSS/SCSS files, brand or style
4
+ guide documents, saved pages, or pasted design tokens. Each file is exactly what the user provided;
5
+ nothing here is generated or summarized. See `../SKILL.md` for how these get used.