@eventmodelers/cli 2.0.1 → 2.0.2
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/README.md +26 -9
- package/cli.js +320 -29
- package/lib/build-kits.js +23 -0
- package/lib/scaffold-model.js +71 -0
- package/lib/scaffold.js +224 -0
- package/lib/supervise.js +60 -0
- package/lib/ui-baseline.js +123 -0
- package/package.json +1 -1
- package/shared/build-kit/lib/events-view.js +16 -3
- package/shared/build-kit/lib/ralph.js +19 -15
- package/shared/build-kit/lib/slice-run.js +2 -2
- package/shared/build-kit/lib/slice-state.js +5 -3
- package/shared/scaffold/LICENSE-em-scaffold +21 -0
- package/shared/scaffold/supabase/generate.mjs +290 -0
- package/shared/scaffold/supabase/template/scripts/create-user.mjs.tmpl +29 -0
- package/shared/scaffold/supabase/template/scripts/dev.mjs.tmpl +145 -0
- package/shared/scaffold/supabase/template/scripts/write-env.mjs.tmpl +44 -0
- package/shared/scaffold/supabase/template/server.ts.tmpl +168 -0
- package/shared/scaffold/supabase/template/src/common/extraEndpoints.ts.tmpl +5 -0
- package/shared/scaffold/supabase/template/src/common/notImplemented.test.ts.tmpl +58 -0
- package/shared/scaffold/supabase/template/src/common/notImplemented.ts.tmpl +29 -0
- package/shared/scaffold/supabase/template/supabase/migrations/_V0__supabase_stubs.sql.tmpl +34 -0
- package/shared/scaffold/supabase/template/web/src/App.tsx.tmpl +38 -0
- package/shared/scaffold/supabase/template/web/src/app/Layout.tsx.tmpl +48 -0
- package/shared/scaffold/supabase/template/web/src/app/NotFound.tsx.tmpl +12 -0
- package/shared/scaffold/supabase/template/web/src/app/SignIn.tsx.tmpl +42 -0
- package/shared/scaffold/supabase/template/web/src/app/TimelinePage.tsx.tmpl +73 -0
- package/shared/scaffold/supabase/template/web/src/app/pages.ts.tmpl +26 -0
- package/shared/scaffold/supabase/template/web/src/app/skeleton.css.tmpl +25 -0
- package/shared/scaffold/supabase/template/web/src/lib/NotBuilt.tsx.tmpl +12 -0
- package/shared/scaffold/supabase/template/web/src/lib/api.ts.tmpl +226 -0
- package/shared/scaffold/supabase/template/web/src/main.tsx.tmpl +13 -0
- package/shared/skills/learn-eventmodelers-api/SKILL.md +4 -4
- package/shared/skills/ui-contract/SKILL.md +6 -6
- package/shared/skills/update-slice-status/SKILL.md +1 -1
- package/stacks/axon/templates/build-kit/CLAUDE.md +4 -3
- package/stacks/axon/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/axon/templates/build-kit/lib/prompt.md +14 -0
- package/stacks/blank/templates/build-kit/CLAUDE.md +4 -3
- package/stacks/blank/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/blank/templates/build-kit/lib/prompt.md +14 -0
- package/stacks/classic/templates/build-kit/CLAUDE.md +6 -5
- package/stacks/classic/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/classic/templates/build-kit/lib/prompt.md +14 -0
- package/stacks/kurrent/templates/build-kit/CLAUDE.md +4 -3
- package/stacks/kurrent/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/kurrent/templates/build-kit/lib/prompt.md +14 -0
- package/stacks/modeling-kit/templates/.claude/skills/analyze-existing-model/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +9 -4
- package/stacks/node/templates/build-kit/CLAUDE.md +4 -3
- package/stacks/node/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/opencqrs/templates/build-kit/CLAUDE.md +4 -3
- package/stacks/opencqrs/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/opencqrs/templates/build-kit/lib/prompt.md +14 -0
- package/stacks/react/templates/build-kit/CLAUDE.md +4 -3
- package/stacks/react/templates/build-kit/lib/AGENT.md +1 -1
- package/stacks/react/templates/build-kit/lib/backend-prompt.md +7 -7
- package/stacks/react/templates/build-kit/lib/kit.json +1 -1
- package/stacks/react/templates/build-kit/lib/prompt.md +29 -2
- package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +15 -0
- package/stacks/supabase/templates/build-kit/CLAUDE.md +4 -3
- package/stacks/supabase/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/supabase-react/templates/.claude/skills/build-state-change/SKILL.md +78 -6
- package/stacks/supabase-react/templates/.claude/skills/build-state-view/SKILL.md +64 -11
- package/stacks/supabase-react/templates/build-kit/CLAUDE.md +5 -4
- package/stacks/supabase-react/templates/build-kit/lib/AGENT.md +1 -1
- package/stacks/supabase-react/templates/build-kit/lib/backend-prompt.md +7 -7
- package/stacks/supabase-react/templates/build-kit/lib/kit.json +1 -1
- package/stacks/supabase-react/templates/build-kit/lib/prompt.md +30 -3
- package/stacks/umadb/templates/build-kit/CLAUDE.md +4 -3
- package/stacks/umadb/templates/build-kit/lib/backend-prompt.md +1 -1
- package/stacks/umadb/templates/build-kit/lib/prompt.md +14 -0
|
@@ -73,13 +73,14 @@ When asked to build a slice, always follow this flow:
|
|
|
73
73
|
- **State-view** — `projections` or `queries` array is non-empty → invoke `/build-state-view`
|
|
74
74
|
- **State-change** — default (has `commands` / `events`) → invoke `/build-state-change`
|
|
75
75
|
3. Invoke the matching skill and follow its instructions completely. Do not deviate.
|
|
76
|
-
4. **
|
|
76
|
+
4. **Baseline gate**: run `npx @eventmodelers/cli build-baseline check --scope backend` first and act only on its verdict — `UNCHANGED` means nothing on the board changed since the last recorded build: do not rebuild, just run the slice's tests and finish; `NO_BASELINE`/`UNKNOWN` mean one full field-by-field diff of slice.json against the code; `CHANGED` lists exactly which elements changed, were added or removed — diff and fix only those. After the checks pass and before committing, run `build-baseline record --scope backend`. A screen or UI prompt change never triggers a backend build.
|
|
77
|
+
5. **Verify against slice.json**: After the skill completes, check that every command field, event field,
|
|
77
78
|
and specification in slice.json appears in the implementation. No invented fields — if it is not in
|
|
78
79
|
slice.json, it must not be in the code. This applies even when the slice was previously `Done` and
|
|
79
80
|
reappears as `Planned` — never dismiss a mismatch as "already implemented" or harmless drift; diff
|
|
80
81
|
slice.json against the code field by field and update the code to match every change.
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
6. Run quality checks (`./mvnw compile -q`, then the slice tests only).
|
|
83
|
+
7. If checks pass, commit with `feat: [Slice Name]` and set slice status to `Done`. For STATE_VIEW/STATE_CHANGE slices when `handover` is enabled the status to set is `Waiting`, not `Done` — see the `ui-contract` skill, Step 4.
|
|
83
84
|
|
|
84
85
|
After you are done, automatically run the tests for the slice that was edited.
|
|
85
86
|
|
|
@@ -42,7 +42,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
|
|
|
42
42
|
If the tests for the slice are not yet named predictably, run `./mvnw test -q` and check for failures.
|
|
43
43
|
15. If checks pass, commit ALL changes with message: `feat: [Slice Name]` and merge back to main as FF merge ( update
|
|
44
44
|
first )
|
|
45
|
-
16. Record the outcome with `npx @eventmodelers/cli slice-status Done` — not by editing index.json, and do not call `update-slice-status`: the run loop pushes the status to the board when your turn ends. For STATE_VIEW/STATE_CHANGE slices when `handover` is enabled the status to record is `
|
|
45
|
+
16. Record the outcome with `npx @eventmodelers/cli slice-status Done` — not by editing index.json, and do not call `update-slice-status`: the run loop pushes the status to the board when your turn ends. For STATE_VIEW/STATE_CHANGE slices when `handover` is enabled the status to record is `Waiting`, not `Done` — see the `ui-contract` skill, Step 4. A slice you could not finish needs no status: leave it as it is and the run loop discards the attempt.
|
|
46
46
|
17. Record any reusable learning you have not recorded yet (see **Recording Learnings**).
|
|
47
47
|
18. If an entry in `.build-kit/AGENTS.md` turned out wrong or obsolete, retract it (see **Recording Learnings**).
|
|
48
48
|
19. Finish the iteration.
|
|
@@ -58,6 +58,20 @@ This is the build trigger. Setting `InProgress` and building are one atomic step
|
|
|
58
58
|
|
|
59
59
|
4. Invoke the matching skill and follow its instructions **completely**. Do NOT implement the slice manually.
|
|
60
60
|
|
|
61
|
+
**Baseline gate** — before touching any code, run `npx @eventmodelers/cli build-baseline check --scope backend`.
|
|
62
|
+
It compares the board's node hashes of every element of the slice (commands, events, read models,
|
|
63
|
+
processors, specifications, ...) with what was recorded after the last successful build:
|
|
64
|
+
- `UNCHANGED` — nothing on the board changed. Do not rebuild or re-diff; run the slice's tests and
|
|
65
|
+
finish. Only now is "no code changes needed" a valid outcome.
|
|
66
|
+
- `NO_BASELINE` / `UNKNOWN` — no recorded build, or the board could not be read: do one full
|
|
67
|
+
field-by-field diff of slice.json against the code (never treat `UNKNOWN` as `UNCHANGED`).
|
|
68
|
+
- `CHANGED` — it lists the elements that changed, were added or were removed. Diff slice.json
|
|
69
|
+
against the code for exactly those and fix every drift; leave what matches alone.
|
|
70
|
+
|
|
71
|
+
When the checks pass, run `npx @eventmodelers/cli build-baseline record --scope backend` **before
|
|
72
|
+
committing**, so `ui-baseline.json` is committed with the slice. Never record without having done the
|
|
73
|
+
work the verdict asked for. Screens and UI prompts are not this kit's business and never trigger a build.
|
|
74
|
+
|
|
61
75
|
5. **Verify against slice.json**: Cross-check the implementation — every command field, event field, and specification in slice.json must appear in the code. No invented fields — if it is not in slice.json, it must not be in the code.
|
|
62
76
|
|
|
63
77
|
6. Run quality checks:
|
|
@@ -43,9 +43,10 @@ When asked to build a slice, always follow this flow:
|
|
|
43
43
|
- **State-view** — `projections` or `queries` array is non-empty → invoke `/build-state-view`
|
|
44
44
|
- **State-change** — default (has `commands` / `events`) → invoke `/build-state-change`
|
|
45
45
|
3. Invoke the matching skill and follow its instructions completely. Do not deviate.
|
|
46
|
-
4. **
|
|
47
|
-
5.
|
|
48
|
-
6.
|
|
46
|
+
4. **UI baseline gate**: run `npx @eventmodelers/cli ui-baseline check` first and act only on its verdict — `UNCHANGED` means skip all UI work; `NO_BASELINE`/`UNKNOWN` mean one full screen diff and every `codeGen.uiPrompts` entry; `CHANGED` names the elements that changed, were added or removed (work on exactly those) and flags `SCREEN_CHANGED` (re-fetch the screen node and adjust the components to it), `ELEMENTS_CHANGED` (diff slice.json against the code) and `PROMPTS_CHANGED` (apply the printed prompts). Details in `lib/prompt.md`, Step 4. After the checks pass and before committing, run `ui-baseline record`.
|
|
47
|
+
5. **Verify against slice.json**: After the skill completes, check that every command field, event field, and specification in slice.json appears in the implementation. No invented fields — if it is not in slice.json, it must not be in the code.
|
|
48
|
+
6. Run quality checks (TODO: this stack's build + test commands), then the slice tests only.
|
|
49
|
+
7. If checks pass, commit with `feat: [Slice Name]` and set slice status to `Done`.
|
|
49
50
|
|
|
50
51
|
After you are done, automatically run the tests for the slice that was edited.
|
|
51
52
|
|
|
@@ -43,4 +43,4 @@ These files are always up to date — read them directly before invoking any ski
|
|
|
43
43
|
- The `boardId` and `organizationId` from each payload provide full context — pass them to skills.
|
|
44
44
|
- Node events use `node:created`, `node:changed`, `node:deleted` — always POST to `/api/org/:orgId/boards/:boardId/nodes/events`.
|
|
45
45
|
- Slice metadata (title, status) lives on the SLICE_BORDER node under `meta.sliceStatus` and `meta.title`.
|
|
46
|
-
- `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard, not a bug. It means another agent already claimed the slice. Treat it as `ALREADY_IN_STATUS`, skip that slice, and move on to the next `
|
|
46
|
+
- `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard, not a bug. It means another agent already claimed the slice. Treat it as `ALREADY_IN_STATUS`, skip that slice, and move on to the next `Waiting` one instead of erroring out. This applies to queued tasks only: when the task request names the slice to build, the run loop owns its status — never call `/update-slice-status` there.
|
|
@@ -9,9 +9,9 @@ The structure defined in the Project-Skills is relevant.
|
|
|
9
9
|
You work within **exactly ONE context at a time** — the one named in `.build-kit/.slices/current_context.json` (the runner picks it).
|
|
10
10
|
|
|
11
11
|
- **ONLY** look for and build slices inside `.build-kit/.slices/<currentContext>/`.
|
|
12
|
-
- **NEVER** read, scan, or build slices from any other context directory, even if it has "
|
|
13
|
-
- A "
|
|
14
|
-
- If the current context has no "
|
|
12
|
+
- **NEVER** read, scan, or build slices from any other context directory, even if it has "Waiting" slices, and even if the current context has no work left.
|
|
13
|
+
- A "Waiting" slice in a *different* context is **NOT yours to build**. Ignore it completely.
|
|
14
|
+
- If the current context has no "Waiting" slice, you are **done for this iteration** — reply `<promise>NO_TASKS</promise>` and stop. Do not go looking elsewhere. The runner moves `current_context.json` on to the next context with Waiting work between iterations — never change it yourself.
|
|
15
15
|
|
|
16
16
|
## Your Task
|
|
17
17
|
|
|
@@ -19,8 +19,8 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
|
|
|
19
19
|
1. Read `.build-kit/.slices/current_context.json` to find the active context name, then read `.build-kit/.slices/<contextName>/index.json`. Every item in status "handover" is a task.
|
|
20
20
|
2. Read `.build-kit/AGENTS.md` **if it exists** — the project's accumulated learnings, generated from what earlier builds recorded. If the slice's `slice.json` has a `lastAttempt`, an earlier attempt at it failed: read its `reason`, `lastReply`, `branch` and `changed`, and do not repeat what failed.
|
|
21
21
|
3. Make sure you are on the right branch "feature/<slicename>", if unsure, start from main.
|
|
22
|
-
5. Your slice is the one the task request names (the run loop picked the highest priority slice with status "
|
|
23
|
-
**IMPORTANT: Only work on the slice named in the task request. Never switch to another slice, whatever its status. If the task request names no slice and no slice has status "
|
|
22
|
+
5. Your slice is the one the task request names (the run loop picked the highest priority slice with status "Waiting"); it becomes your PRD. The run loop has already claimed it — its status is "InProgress" in index.json and on the board. Build that slice and do not call `update-slice-status`: the run loop owns the slice's status on the board.
|
|
23
|
+
**IMPORTANT: Only work on the slice named in the task request. Never switch to another slice, whatever its status. If the task request names no slice and no slice has status "Waiting" in the current context, reply with:**
|
|
24
24
|
<promise>NO_TASKS</promise> and stop immediately. Do not work on other slices and do not switch to another context.
|
|
25
25
|
6. Pick the slice definition from `.build-kit/.slices/<contextName>/<folder>/slice.json` as defined in the prd. Never work on more than one slice per iteration.
|
|
26
26
|
7. A slice can define additional prompts as codegen/backendPrompt. Any additional prompts defined in backend are hints for the implementation of the slice and have to be taken into account.
|
|
@@ -90,12 +90,12 @@ A Slice is not complete if specifications are missing or can't be executed.
|
|
|
90
90
|
|
|
91
91
|
## Stop Condition
|
|
92
92
|
|
|
93
|
-
**After completing ONE slice, always stop — regardless of whether more slices are in
|
|
93
|
+
**After completing ONE slice, always stop — regardless of whether more slices are in Waiting.** The ralph loop will invoke you again for the next slice. Never chain multiple slices in one iteration.
|
|
94
94
|
|
|
95
95
|
If the slice was completed and committed successfully, reply with:
|
|
96
96
|
<promise>DONE</promise>
|
|
97
97
|
|
|
98
|
-
If no slice has status "
|
|
98
|
+
If no slice has status "Waiting" in the current context, reply with:
|
|
99
99
|
<promise>NO_TASKS</promise>
|
|
100
100
|
(Do NOT switch to another context to find work — stop here.)
|
|
101
101
|
|
|
@@ -38,13 +38,13 @@ Run `/load-slice sliceId=<payload.sliceId>` to fetch full slice details (title,
|
|
|
38
38
|
|
|
39
39
|
Inspect the `sliceStatus` in the payload:
|
|
40
40
|
|
|
41
|
-
#### `
|
|
41
|
+
#### `Waiting` — build the slice
|
|
42
42
|
|
|
43
43
|
This is the build trigger. Setting `InProgress` and building are one atomic step:
|
|
44
44
|
|
|
45
45
|
1. Immediately call `/update-slice-status` to set the slice to `InProgress` on the board.
|
|
46
46
|
|
|
47
|
-
**Claim conflict**: if this call reports the slice is already in `InProgress` (or any status other than `
|
|
47
|
+
**Claim conflict**: if this call reports the slice is already in `InProgress` (or any status other than `Waiting`), another agent already claimed it first — this is expected, not an error. Say so in your reply, drop this task without building, and continue the loop (the next task will naturally cover the next slice). Do not retry.
|
|
48
48
|
|
|
49
49
|
2. Run `/load-slice sliceId=<payload.sliceId>` to fetch the full slice, then read the slice definition from `.build-kit/.slices/<contextSlug>/<sliceFolder>/slice.json`. The runner keeps a `slice.json` fresh from a summary endpoint, so a file holding only `id`, `title`, `status` and `sliceType` is that stub, not an empty slice — never conclude a slice has no elements from it. Only if the full fetch itself returns no commands/events/read models/fields is the slice actually empty.
|
|
50
50
|
|
|
@@ -56,6 +56,33 @@ This is the build trigger. Setting `InProgress` and building are one atomic step
|
|
|
56
56
|
|
|
57
57
|
4. Invoke the matching skill and follow its instructions **completely**. Do NOT implement the slice manually.
|
|
58
58
|
|
|
59
|
+
**UI baseline gate** — before touching any code, run `npx @eventmodelers/cli ui-baseline check`. It
|
|
60
|
+
compares the board's node hashes (the slice's screen and every element) and `codeGen.uiPrompts` with
|
|
61
|
+
what was recorded after the last UI build, and decides what this build has to do:
|
|
62
|
+
- `UNCHANGED` — nothing on the board changed since the last UI build. Skip all UI work; do not
|
|
63
|
+
re-fetch or diff the screen. Only now is "no code changes needed" a valid outcome.
|
|
64
|
+
- `NO_BASELINE` — no UI build recorded yet (first build, or the slice predates the baseline). Do one
|
|
65
|
+
full screen diff (below), apply every UI prompt.
|
|
66
|
+
- `UNKNOWN` — the board could not be read. Treat it as `NO_BASELINE`; never as `UNCHANGED`.
|
|
67
|
+
- `CHANGED` — it also lists, per element, what changed, was added or was removed since the last recorded
|
|
68
|
+
build (e.g. `changed: Contact Details (screen), Assign Tag (command)`). Work on exactly those nodes
|
|
69
|
+
and leave what matches alone. Act on the parts it flags:
|
|
70
|
+
- `SCREEN_CHANGED`: re-fetch the live screen node (`get_node`, its `meta.pages` / `meta.marks`) —
|
|
71
|
+
never trust the JSX already in the repo — and diff the existing component(s) against the marked
|
|
72
|
+
(or whole, if unmarked) HTML: tags, classes, layout, copy, fields/buttons added, removed or
|
|
73
|
+
reordered, and `cssFrameworks`. Edit the components (and `src/pages/<ScreenTitle>.tsx`, for this
|
|
74
|
+
slice's marked position only) until they match 1:1 and update `samples/`. Keep what already
|
|
75
|
+
matches; do not regenerate the slice from scratch.
|
|
76
|
+
- `ELEMENTS_CHANGED`: diff slice.json against the code field by field (fields,
|
|
77
|
+
`apiEndpoint`/UI contract) and fix every drift.
|
|
78
|
+
- `PROMPTS_CHANGED`: apply the UI prompts it prints — each is an instruction the user typed on the
|
|
79
|
+
board for this slice's UI (e.g. "adjust the UI to the new layout!") — to the component(s) and
|
|
80
|
+
screen markup. Name each prompt and the change you made for it in your final reply.
|
|
81
|
+
|
|
82
|
+
When the UI work is done and the checks in step 6 pass, run `npx @eventmodelers/cli ui-baseline record`
|
|
83
|
+
**before committing**, so `ui-baseline.json` is committed with the slice. Never record without having
|
|
84
|
+
done the work the verdict asked for.
|
|
85
|
+
|
|
59
86
|
5. **Verify against slice.json**: cross-check the implementation — every command field, event field, and specification in slice.json must appear in the code. No invented fields — if it is not in slice.json, it must not be in the code.
|
|
60
87
|
|
|
61
88
|
6. Run quality checks (TODO: your stack's build + test commands — slice-scoped tests only, not the full suite).
|
|
@@ -42,8 +42,22 @@ CREATE TABLE IF NOT EXISTS "public"."{tablename}"
|
|
|
42
42
|
-- use snake_case for all column names
|
|
43
43
|
created_at TIMESTAMP DEFAULT NOW()
|
|
44
44
|
);
|
|
45
|
+
|
|
46
|
+
-- The UI reads this table DIRECTLY through Supabase (no read endpoint in between), so it must be readable.
|
|
47
|
+
-- RLS on with no policy returns an EMPTY list, not an error - always ship the policy with the table.
|
|
48
|
+
ALTER TABLE "public"."{tablename}" ENABLE ROW LEVEL SECURITY;
|
|
49
|
+
GRANT SELECT ON "public"."{tablename}" TO authenticated;
|
|
50
|
+
CREATE POLICY "{tablename}_select" ON "public"."{tablename}"
|
|
51
|
+
FOR SELECT TO authenticated USING (true);
|
|
45
52
|
```
|
|
46
53
|
|
|
54
|
+
**The UI reads the read-model table directly.** The frontend never goes through the `GET` route for a
|
|
55
|
+
read model: it selects from this table with the user's session, and Row Level Security decides which
|
|
56
|
+
rows come back. `USING (true)` is the default (every signed-in user sees every row); when the read
|
|
57
|
+
model is per user or per tenant (an owner/user/tenant column, or `description`/`comments[]` say so),
|
|
58
|
+
narrow the policy to it, e.g. `USING (owner_id = auth.uid()::text)`. The `GET` route in Step 5 stays
|
|
59
|
+
for API consumers and tests; the UI does not depend on it.
|
|
60
|
+
|
|
47
61
|
**Column type guide** — field `type` values are the canonical set from the [event-modeling-spec schema](https://github.com/dilgerma/event-modeling-spec/blob/main/eventmodeling.schema.json):
|
|
48
62
|
|
|
49
63
|
| Field type | SQL type |
|
|
@@ -487,6 +501,7 @@ src/common/
|
|
|
487
501
|
## Checklist
|
|
488
502
|
|
|
489
503
|
- [ ] Migration file created with correct version number and all columns
|
|
504
|
+
- [ ] Migration enables RLS, grants `SELECT` to `authenticated` and creates a select policy: the UI reads the table directly, and no policy means an empty list
|
|
490
505
|
- [ ] `tableName` constant matches the migration table name exactly
|
|
491
506
|
- [ ] Projection registered in `loadPostgresEventstore.ts`
|
|
492
507
|
- [ ] `canHandle` lists every event type the projection reacts to
|
|
@@ -49,9 +49,10 @@ When asked to build a slice, always follow this flow:
|
|
|
49
49
|
- **State-view** — `projections` or `queries` array is non-empty → invoke `/build-state-view`
|
|
50
50
|
- **State-change** — default (has `commands` / `events`) → invoke `/build-state-change`
|
|
51
51
|
3. Invoke the matching skill and follow its instructions completely. Do not deviate.
|
|
52
|
-
4. **
|
|
53
|
-
5.
|
|
54
|
-
6.
|
|
52
|
+
4. **Baseline gate**: run `npx @eventmodelers/cli build-baseline check --scope backend` first and act only on its verdict — `UNCHANGED` means nothing on the board changed since the last recorded build: do not rebuild, just run the slice's tests and finish; `NO_BASELINE`/`UNKNOWN` mean one full field-by-field diff of slice.json against the code; `CHANGED` lists exactly which elements changed, were added or removed — diff and fix only those. After the checks pass and before committing, run `build-baseline record --scope backend`. A screen or UI prompt change never triggers a backend build.
|
|
53
|
+
5. **Verify against slice.json**: After the skill completes, check that every command field, event field, and specification in slice.json appears in the implementation. No invented fields — if it is not in slice.json, it must not be in the code. This applies even when the slice was previously `Done` and reappears as `Planned` — never dismiss a mismatch as "already implemented" or harmless drift; diff slice.json against the code field by field and update the code to match every change.
|
|
54
|
+
6. Run quality checks (`npm run build`, then the slice tests only).
|
|
55
|
+
7. If checks pass, commit with `feat: [Slice Name]` and set slice status to `Done`. For STATE_VIEW/STATE_CHANGE slices when `handover` is enabled the status to set is `Waiting`, not `Done` — see the `ui-contract` skill, Step 4.
|
|
55
56
|
|
|
56
57
|
After you are done, automatically run the tests for the slice that was edited.
|
|
57
58
|
|
|
@@ -36,7 +36,7 @@ You work within **exactly ONE context at a time** — the one named in `.build-k
|
|
|
36
36
|
14. even if the slice is fully implemented, run your test-analyzer skill and provide the code-slice.json file as defined in the skill
|
|
37
37
|
15. If checks pass, commit ALL changes with message: `feat: [Slice Name]` and merge back to main as FF merge ( update
|
|
38
38
|
first )
|
|
39
|
-
16. Record the outcome with `npx @eventmodelers/cli slice-status Done` — not by editing index.json, and do not call `update-slice-status`: the run loop pushes the status to the board when your turn ends. For STATE_VIEW/STATE_CHANGE slices when `handover` is enabled the status to record is `
|
|
39
|
+
16. Record the outcome with `npx @eventmodelers/cli slice-status Done` — not by editing index.json, and do not call `update-slice-status`: the run loop pushes the status to the board when your turn ends. For STATE_VIEW/STATE_CHANGE slices when `handover` is enabled the status to record is `Waiting`, not `Done` — see the `ui-contract` skill, Step 4. A slice you could not finish needs no status: leave it as it is and the run loop discards the attempt.
|
|
40
40
|
17. Record any reusable learning you have not recorded yet (see **Recording Learnings**).
|
|
41
41
|
18. If an entry in `.build-kit/AGENTS.md` turned out wrong or obsolete, retract it (see **Recording Learnings**).
|
|
42
42
|
19. Finish the iteration.
|
|
@@ -71,8 +71,9 @@ Extract:
|
|
|
71
71
|
- **screens[]** — the actor `SCREEN` element(s) that trigger this command, if any. Empty when the
|
|
72
72
|
command has no UI trigger (rare — build the component from fields alone in that case, skip Step
|
|
73
73
|
2).
|
|
74
|
-
- **specifications[]** — GWT scenarios
|
|
75
|
-
|
|
74
|
+
- **specifications[]** and **storylines[]** (optional) — GWT scenarios / board walkthroughs. These
|
|
75
|
+
describe how the slice should **behave** — read them in full (Step 3a) to understand the outcomes
|
|
76
|
+
and states the UI must present. The business logic itself lives in the backend; the UI does not implement it.
|
|
76
77
|
|
|
77
78
|
> **Comments**: each element carries `comments: string[]` (board comments) — read them as
|
|
78
79
|
> implementation hints, and resolve consumed ones via
|
|
@@ -80,6 +81,10 @@ Extract:
|
|
|
80
81
|
|
|
81
82
|
## Step 2 — Read the screen, and especially its marks
|
|
82
83
|
|
|
84
|
+
> **Gated by `ui-baseline check`** (build prompt, `lib/prompt.md`, Step 4): do this step when the verdict
|
|
85
|
+
> is `SCREEN_CHANGED`, `NO_BASELINE` or `UNKNOWN` — and then also diff the existing JSX against the fetched
|
|
86
|
+
> screen and adjust the UI to it. On `UNCHANGED` skip it.
|
|
87
|
+
|
|
83
88
|
`slice.json`'s `screens[]` entries only carry `id`/`title`/`fields[]`/`description` (fields already
|
|
84
89
|
scoped to this slice) — not the actual markup. Fetch the live node to get that:
|
|
85
90
|
|
|
@@ -132,19 +137,76 @@ lane concept, so the frontend path isn't a strict 1:1 mirror of it. `CommandTitl
|
|
|
132
137
|
**Markup**: translate the blueprint HTML from Step 2 into JSX **1:1** — same tags, same classes
|
|
133
138
|
(the screen's `cssFrameworks` framework), same layout and copy. This is not a redesign; the screen *is*
|
|
134
139
|
the design. Only change what must become dynamic:
|
|
135
|
-
- A field's example value becomes that input's
|
|
140
|
+
- A field's example value becomes that input's **initial/default** `value` only for what the user is
|
|
141
|
+
*entering* (a new-record form). Anything the screen *displays* about an existing entity (names,
|
|
142
|
+
titles, current selection, existing tags, list rows) is **not** static — see "Data binding" below.
|
|
136
143
|
- An element repeated for a `cardinality: "List"` field becomes a `.map()` over that field.
|
|
137
144
|
- `class`/`style`/structure otherwise stay exactly as drawn — except the board's own mark plumbing:
|
|
138
145
|
drop every `data-em-mark-id` attribute and `em-mark`/`em-mark-<hex>` class (they only draw the
|
|
139
146
|
highlight outline on the canvas; the app never ships it).
|
|
140
147
|
- 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.
|
|
141
148
|
|
|
149
|
+
**Data binding — mockup text is sample data, not copy.** The example values drawn in a screen are
|
|
150
|
+
placeholders from the model. Before translating, classify every piece of text/value in the fragment:
|
|
151
|
+
*label/copy* (headings like "Tags", button captions, placeholders — keep verbatim) vs. *data* (a person's
|
|
152
|
+
name, a role/organisation line, a pre-selected dropdown value, dropdown options, existing list rows,
|
|
153
|
+
badges, counts, dates). For each *data* item find its source in `slice.json` / the model — the
|
|
154
|
+
`readmodels[]` whose fields carry it, or the command's own `fields[]` — and render it from there.
|
|
155
|
+
**Never hardcode a data value from the mockup into JSX.**
|
|
156
|
+
|
|
157
|
+
**Find the screen's data sources on the board, not just in `slice.json`.** A screen is often shown by
|
|
158
|
+
one slice but fed by a read model that belongs to *another* slice (e.g. the Contact Details screen
|
|
159
|
+
sits in the Assign Tag slice, yet its contact data comes from the Contact List read model via an edge
|
|
160
|
+
READMODEL → SCREEN). `slice.json` only lists this slice's own elements, so also call
|
|
161
|
+
`mcp__eventmodelers__get_connected_nodes { boardId, nodeId: <screen.id> }` and take every READMODEL
|
|
162
|
+
wired into the screen. Each one is a data source for the displayed values; look up its fields in that
|
|
163
|
+
read model's slice (`.build-kit/.slices/*/*/slice.json`, or `get_node`). A list read model feeding a
|
|
164
|
+
detail screen (`Contact List[]` → one contact) means: query it filtered by the entity id the page
|
|
165
|
+
gets from the route (match the id field, e.g. `contactId` / the command's `recordId`) and render that
|
|
166
|
+
row. If the source read model's slice isn't built, still type and wire against its fields (TODO), do
|
|
167
|
+
not substitute the mockup's values.
|
|
168
|
+
|
|
169
|
+
- Data comes from a read model (query component of the same page/screen, via `queryReadModel` /
|
|
170
|
+
`getReadModel`) → bind it to that data (props from the parent that fetched it, or fetch it here
|
|
171
|
+
when this component owns that read). If that read-model slice is not built yet, wire the component
|
|
172
|
+
against the typed shape and leave a `// TODO(<read model title>)` — do not paste the example in.
|
|
173
|
+
- Select/dropdown *options* come from a read model (e.g. the customer list), never from the
|
|
174
|
+
options drawn in the mockup; the *selected* value comes from the entity's own read model.
|
|
175
|
+
- The only legitimate static content is label/copy and `VITE_DATA_MODE=mock` samples in `samples/*.json`.
|
|
176
|
+
- Self-check before finishing: grep the component for the mockup's example strings (e.g. the person's
|
|
177
|
+
name, organisation, tag text). Any hit outside `samples/` is a bug.
|
|
178
|
+
|
|
142
179
|
**Props**: exactly `command.fields[]`, typed per each field's `type` (`String`→`string`,
|
|
143
180
|
`Boolean`→`boolean`, `Int`/`Long`→`number`, etc. — `subfields[]` become a nested object type),
|
|
144
181
|
respecting `optional`. Add two callback props: `onSuccess?: (result: TResponse) => void` and
|
|
145
182
|
`onError?: (error: ApiError) => void`. **No other inputs** — no context/store reads, no fetching
|
|
146
183
|
inside the component. All business data the component needs arrives via props from its parent.
|
|
147
184
|
|
|
185
|
+
### Step 3a — Read the scenarios to understand behavior (not to implement rules)
|
|
186
|
+
|
|
187
|
+
**Business logic lives in the backend. The UI never re-implements it.** Do not copy a scenario's
|
|
188
|
+
rules (preconditions, limits, uniqueness, state checks) into the component as validation or guards —
|
|
189
|
+
the backend decides, and a duplicated rule drifts the moment the model changes.
|
|
190
|
+
|
|
191
|
+
Read every entry in `specifications[]` (and the beat runs in `storylines[]` containing this command)
|
|
192
|
+
to understand **how the component should behave around the command**: what the user is trying to
|
|
193
|
+
accomplish, what outcomes exist, and what each outcome must look like. Use them to decide:
|
|
194
|
+
|
|
195
|
+
| Scenario shows | What it tells the UI |
|
|
196
|
+
|---|---|
|
|
197
|
+
| `when` this command → `then` EVENT(s) | the success outcome: what to confirm or reset after `postCommand` resolves, and what `onSuccess` hands the parent |
|
|
198
|
+
| `when` this command → `then` an error / rejection (e.g. "already reserved") | the failure outcomes that exist: render each as its own readable message from the `ApiError` the backend returns — not one generic failure. The backend decides *whether* it fails; the UI only shows it |
|
|
199
|
+
| `optional` fields, fields that vary between scenarios | which inputs are required vs. optional in the form (mirror the flags, no invented rules) |
|
|
200
|
+
| `given` events before the command | context for what the user was doing — background to understand the flow, not logic to reproduce |
|
|
201
|
+
| a `STORYLINE_NOTE` beat | background only |
|
|
202
|
+
|
|
203
|
+
Rules:
|
|
204
|
+
- Understand, don't implement: no business validation, no precondition checks, no computed outcomes
|
|
205
|
+
in the component. Only presentation: states (idle/submitting/success/error), required-vs-optional
|
|
206
|
+
inputs, and showing what the backend returns.
|
|
207
|
+
- If a scenario's outcome needs a UI state the markup doesn't show, add the minimal state and note
|
|
208
|
+
the scenario in a one-line comment (e.g. `// spec: "already reserved" → backend 409 shown inline`).
|
|
209
|
+
|
|
148
210
|
**Default — keep the logic in the component.** For an ordinary command (one form, one submit
|
|
149
211
|
action, which is most of them) local state and the `postCommand` call live directly inside the
|
|
150
212
|
component. This is the common case and needs nothing more:
|
|
@@ -211,9 +273,10 @@ is driven entirely by props, each sample is a **full set of prop values** — th
|
|
|
211
273
|
renders this component in isolation (a `src/pages/` composition, or ad hoc while developing) to
|
|
212
274
|
spread as props, e.g. `<ReserveBike {...sample1} onSuccess={...} />`.
|
|
213
275
|
|
|
214
|
-
Add `sample-1.json` for the ordinary case (`command.fields[].example` values); add
|
|
215
|
-
|
|
216
|
-
|
|
276
|
+
Add `sample-1.json` for the ordinary case (`command.fields[].example` values); add a numbered
|
|
277
|
+
sample for **each** scenario in `specifications[]` that changes what the component does (an invalid
|
|
278
|
+
input that blocks submit, an `optional` field omitted, a precondition that disables the trigger).
|
|
279
|
+
Take the values from the scenario's own example data. As in `build-state-view`, the same number across different components' samples is meant to
|
|
217
280
|
represent one coherent scenario across the app, not an arbitrary per-component index.
|
|
218
281
|
|
|
219
282
|
## Step 5 — Context/Provider, only if the marked fragment needs shared local state
|
|
@@ -290,6 +353,13 @@ how many pages this app needs — not one page per slice.
|
|
|
290
353
|
content there, passing whatever props it declares. Only substitute positions whose slice
|
|
291
354
|
component already exists; leave any not-yet-built slice's marked area as the mockup's static
|
|
292
355
|
placeholder until that slice is built and this step runs again for it.
|
|
356
|
+
- **Unmarked/unbuilt regions are not an excuse to hardcode data.** Static placeholder is only for
|
|
357
|
+
label/copy. A displayed data value (e.g. the contact's name and "role · organisation" line, the
|
|
358
|
+
selected organisation and the organisation dropdown options) must be bound to the read model that
|
|
359
|
+
carries it — if that read model's component exists (e.g. the contact list/details read), fetch
|
|
360
|
+
through it and pass the values down; if not, type the prop and leave a `// TODO(<read model>)`.
|
|
361
|
+
Never paste the mockup's example (or a slice's `samples/sample-1.json`) into the page as the
|
|
362
|
+
data. The page reads the entity id from the route/query param.
|
|
293
363
|
- The page owns threading shared inputs (route/query params, the current session, etc.) down as
|
|
294
364
|
props to each slice component — a slice component still only knows its own declared props, not
|
|
295
365
|
the rest of the page or its siblings.
|
|
@@ -348,6 +418,8 @@ After writing the hook/component (and context/page/base page, if any), confirm:
|
|
|
348
418
|
time, the provisional `/api/mock/...` path noted in a comment (Step 1).
|
|
349
419
|
- At least one numbered sample exists under this slice's `samples/` folder, shaped exactly like
|
|
350
420
|
this component's props.
|
|
421
|
+
- Every scenario outcome (success, each named failure) has a rendered state, and **no business rule
|
|
422
|
+
from a scenario was re-implemented in the component** — the backend owns it.
|
|
351
423
|
- If a screen was read in Step 2, the rendered JSX matches the marked (or whole, if unmarked)
|
|
352
424
|
HTML fragment structurally — no invented buttons/fields the mockup didn't show, nothing from
|
|
353
425
|
the mockup silently dropped.
|
|
@@ -67,10 +67,13 @@ Extract:
|
|
|
67
67
|
a Slice" step 6) — the built, mocked component still needs a real table/view before it's done,
|
|
68
68
|
so don't flip it to `Done`.
|
|
69
69
|
- `description` — implementation hints (filtering rules, sort order, business meaning).
|
|
70
|
-
- **UI contract (read)** —
|
|
70
|
+
- **UI contract (read)** — **not used for a read model backed by a table, which is the normal case on this
|
|
71
|
+
stack: that is always read directly with `queryReadModel`, even when the backend published a `GET`
|
|
72
|
+
contract.** Only when `slice.json` says the read model is not a persisted table (a computed view, an
|
|
73
|
+
external source) does the backend's published `GET` contract apply: look it up as described in
|
|
71
74
|
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). **
|
|
73
|
-
is the specification and you build the call from it** — it overrides `apiEndpoint
|
|
75
|
+
`slice.json` `notes[]`; parse only its `ui-contract` block — an OpenAPI operation). **In that case it
|
|
76
|
+
is the specification and you build the call from it** — it overrides `apiEndpoint`:
|
|
74
77
|
- fetch with `getReadModel(buildPath('<contract path>', { <path params> }), { <query params> }, samples)`
|
|
75
78
|
from `src/lib/api.ts` — path and query `parameters` exactly as the contract names them, never a
|
|
76
79
|
guessed URL, never `supabase.from(...)`;
|
|
@@ -84,14 +87,19 @@ Extract:
|
|
|
84
87
|
`readmodel.apiEndpoint` (table/view name) + `fields[]` via `queryReadModel` — and note the assumption
|
|
85
88
|
as a code comment.
|
|
86
89
|
- **screens[]** — the actor `SCREEN` element(s) that display this data, if any (see Step 2).
|
|
87
|
-
- **specifications[]** — GWT scenarios/
|
|
88
|
-
|
|
90
|
+
- **specifications[]** and **storylines[]** (optional) — GWT scenarios / board walkthroughs. These
|
|
91
|
+
describe how the slice should **behave** — read them in full (Step 3a) to understand the outcomes
|
|
92
|
+
and states the UI must present. The business logic itself lives in the backend; the UI does not implement it.
|
|
89
93
|
|
|
90
94
|
> **Comments**: as in `build-state-change` — resolve consumed ones via the same node-comment
|
|
91
95
|
> endpoint.
|
|
92
96
|
|
|
93
97
|
## Step 2 — Read the screen, and especially its marks
|
|
94
98
|
|
|
99
|
+
> **Gated by `ui-baseline check`** (build prompt, `lib/prompt.md`, Step 4): do this step when the verdict
|
|
100
|
+
> is `SCREEN_CHANGED`, `NO_BASELINE` or `UNKNOWN` — and then also diff the existing JSX against the fetched
|
|
101
|
+
> screen and adjust the UI to it. On `UNCHANGED` skip it.
|
|
102
|
+
|
|
95
103
|
Identical to `build-state-change` Step 2 — fetch the live screen node
|
|
96
104
|
(`mcp__eventmodelers__get_node`), find this slice's marked fragment in `meta.pages`/`meta.marks`
|
|
97
105
|
(or the whole page if unmarked), and treat it as the blueprint. The same `learn-styleguide` note
|
|
@@ -112,16 +120,39 @@ multi-read-model component.
|
|
|
112
120
|
**Markup**: same rule as `build-state-change` — translate the blueprint from Step 2 1:1 into JSX;
|
|
113
121
|
the mockup is the design, not a starting point for a redesign.
|
|
114
122
|
|
|
123
|
+
**Data binding — the whole point of a read-model component.** Every *data* value in the mockup (names,
|
|
124
|
+
role/organisation lines, selected values, dropdown options, list rows, badges, counts, dates) is a
|
|
125
|
+
sample taken from the model's examples; it must be rendered from the queried data, mapped from
|
|
126
|
+
`readmodel.fields[]` — keep only label/copy (headings, button captions, placeholders) verbatim.
|
|
127
|
+
Check where each displayed value comes from before writing JSX: a field of this read model → bind it;
|
|
128
|
+
a field of another read model on the same screen → it arrives as a prop from the parent that fetched
|
|
129
|
+
it (or a sibling read-model component), never from the mockup. List rows are a `.map()` over the
|
|
130
|
+
fetched rows, not the rows drawn. Self-check: grep the component for the mockup's example strings;
|
|
131
|
+
any hit outside `samples/` means hardcoded data — fix it.
|
|
132
|
+
|
|
133
|
+
**Find the screen's data sources on the board, not just in `slice.json`.** A screen is often shown by
|
|
134
|
+
one slice but fed by a read model that belongs to *another* slice (e.g. the Contact Details screen
|
|
135
|
+
sits in the Assign Tag slice, yet its contact data comes from the Contact List read model via an edge
|
|
136
|
+
READMODEL → SCREEN). `slice.json` only lists this slice's own elements, so also call
|
|
137
|
+
`mcp__eventmodelers__get_connected_nodes { boardId, nodeId: <screen.id> }` and take every READMODEL
|
|
138
|
+
wired into the screen. Each one is a data source for the displayed values; look up its fields in that
|
|
139
|
+
read model's slice (`.build-kit/.slices/*/*/slice.json`, or `get_node`). A list read model feeding a
|
|
140
|
+
detail screen (`Contact List[]` → one contact) means: query it filtered by the entity id the page
|
|
141
|
+
gets from the route (match the id field, e.g. `contactId` / the command's `recordId`) and render that
|
|
142
|
+
row. If the source read model's slice isn't built, still type and wire against its fields (TODO), do
|
|
143
|
+
not substitute the mockup's values.
|
|
144
|
+
|
|
145
|
+
|
|
115
146
|
**Props**: whatever the component needs to *scope* its query (e.g. `customerId`) — passed in from
|
|
116
147
|
its parent, never read from a global store. The queried data itself is **not** a prop — the
|
|
117
148
|
component fetches it.
|
|
118
149
|
|
|
119
|
-
**With a UI contract (Step 1)**: call `getReadModel(buildPath(path, pathParams), queryParams, samples)`
|
|
150
|
+
**With a UI contract (Step 1, non-table read models only)**: call `getReadModel(buildPath(path, pathParams), queryParams, samples)`
|
|
120
151
|
from `src/lib/api.ts` inside the component instead of the `queryReadModel` snippet below — same
|
|
121
152
|
effect shape (`useEffect` → loading / empty / error / data), but each documented non-2xx response
|
|
122
153
|
(`ApiError.status`) maps to its own state. Never a raw `fetch`.
|
|
123
154
|
|
|
124
|
-
**Default (
|
|
155
|
+
**Default (a table-backed read model — always, unless the contract case above applies) — call `queryReadModel` from `src/lib/api.ts` inside the component**. Never
|
|
125
156
|
`supabase.from(...)` directly, never a raw `fetch`:
|
|
126
157
|
|
|
127
158
|
```tsx
|
|
@@ -182,6 +213,26 @@ export function ActiveReservationView({ customerId }: ActiveReservationViewProps
|
|
|
182
213
|
}
|
|
183
214
|
```
|
|
184
215
|
|
|
216
|
+
### Step 3a — Read the scenarios to understand behavior (not to implement rules)
|
|
217
|
+
|
|
218
|
+
**Business logic lives in the backend (the projection). The UI never re-derives it** — no
|
|
219
|
+
recomputing totals, statuses or membership from events; render what the read model holds.
|
|
220
|
+
|
|
221
|
+
Read every entry in `specifications[]` (and the READMODEL beats in `storylines[]`) to understand
|
|
222
|
+
**what the read model looks like in different situations and how the component should present each**:
|
|
223
|
+
|
|
224
|
+
| Scenario shows | What it tells the UI |
|
|
225
|
+
|---|---|
|
|
226
|
+
| `then` read model is empty (`expectEmptyList` / no rows) | an **empty state** must exist, distinct from loading and error |
|
|
227
|
+
| the same field takes different values across scenarios (e.g. status `open` → `closed`) | each value needs a sensible presentation (label, badge, shown/hidden actions) — presentation only |
|
|
228
|
+
| rows appear/disappear between scenarios | the component renders whatever rows the read model returns; it does not decide which belong |
|
|
229
|
+
| row order visible across scenarios | hint for `order`/sort on the query, if the read model doesn't already guarantee it |
|
|
230
|
+
| a `STORYLINE_NOTE` beat | background only |
|
|
231
|
+
|
|
232
|
+
Rules:
|
|
233
|
+
- Understand, don't implement: presentation of what the backend provides, nothing more.
|
|
234
|
+
- Cite the scenario behind a non-obvious state in a one-line comment.
|
|
235
|
+
|
|
185
236
|
Select fields explicitly by name rather than `select('*')`. Map each field's `name` to its actual
|
|
186
237
|
column name — check the paired backend stack's migrations if the naming convention (e.g. camelCase
|
|
187
238
|
vs. snake_case) isn't obvious rather than guessing; if genuinely unclear, use `request-feedback`.
|
|
@@ -202,7 +253,9 @@ otherwise; use `null`/`[]` for a sample meant to exercise the empty/background s
|
|
|
202
253
|
|
|
203
254
|
Add at least one sample (`sample-1.json`) covering the ordinary case from
|
|
204
255
|
`readmodel.fields[].example` values; add more numbered ones for whatever other scenarios this
|
|
205
|
-
read model's `specifications[]`
|
|
256
|
+
read model's `specifications[]` show — **one sample per distinct scenario outcome**, using the
|
|
257
|
+
scenario's own example values (an empty result, each status a row can take, a second customer's
|
|
258
|
+
data). The
|
|
206
259
|
same number across *different* components' `samples/` folders is meant to represent one coherent
|
|
207
260
|
scenario across the whole app (e.g. every component's `sample-2.json` together tell the story of
|
|
208
261
|
"a long-time customer") — keep that in mind when choosing what a given number represents, rather
|
|
@@ -230,7 +283,7 @@ one page per slice.
|
|
|
230
283
|
one place allowed to import across slice folders. Translate the screen's whole, unmarked
|
|
231
284
|
`meta.pages` markup into JSX (the page-level layout), and at each `data-em-mark-id` position
|
|
232
285
|
render that mark's owning slice's component (only for slices already built; leave the rest as
|
|
233
|
-
the mockup's static content until they are). The page threads shared inputs down as props; a
|
|
286
|
+
the mockup's static content until they are). Static placeholder content is label/copy only — displayed data (names, selections, options, rows) is bound to its read model, never the mockup's examples or a sample file. The page threads shared inputs down as props; a
|
|
234
287
|
slice component still only knows its own declared props. Update only this slice's marked
|
|
235
288
|
position if the page file already exists.
|
|
236
289
|
|
|
@@ -289,8 +342,8 @@ leaves it alone.
|
|
|
289
342
|
slice.json.
|
|
290
343
|
- At least one numbered sample exists under this slice's `samples/` folder, shaped exactly like
|
|
291
344
|
`queryReadModel`'s return value.
|
|
292
|
-
-
|
|
293
|
-
|
|
345
|
+
- Every scenario situation (empty, each distinct field value) has a rendered presentation, and the
|
|
346
|
+
component re-derives nothing the read model already holds.
|
|
294
347
|
- If a screen was read in Step 2, the rendered JSX matches the marked (or whole, if unmarked) HTML
|
|
295
348
|
fragment structurally.
|
|
296
349
|
- Nothing in this slice's folder is imported from another slice's folder, and nothing outside it
|
|
@@ -28,7 +28,7 @@ not attempt to build it, flag it via `request-feedback` instead.
|
|
|
28
28
|
2. Maintain clear separation of concerns within each slice
|
|
29
29
|
3. Headers, auth, `fetch`, and Supabase table access live exclusively in `src/lib/api.ts` — a
|
|
30
30
|
slice component never imports `src/lib/supabase.ts` itself, never calls `fetch`, and never
|
|
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
|
+
calls `supabase.from(...)` directly; a table-backed read model is always read directly with `queryReadModel` (the backend's `GET` contract is ignored for it); when the backend slice published a UI contract (`ui-contract` skill, `slice.json` `notes[]`) for a *write* or a non-table read, 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
|
|
32
32
|
business logic (validation, request-body shaping, response handling)
|
|
33
33
|
4. A command's/read-model's screen markup (`meta.pages` on its actor HTML_SCREEN node, scoped to
|
|
34
34
|
this slice's `meta.marks` when the screen is shared) is the visual blueprint — translate it into
|
|
@@ -122,9 +122,10 @@ When asked to build a slice, always follow this flow:
|
|
|
122
122
|
- **`AUTOMATION`** → not this stack's job (no UI counterpart) — invoke `request-feedback`
|
|
123
123
|
rather than building it; it belongs to whichever backend stack is installed alongside this one
|
|
124
124
|
3. Invoke the matching skill and follow its instructions completely. Do not deviate.
|
|
125
|
-
4. **
|
|
126
|
-
5.
|
|
127
|
-
6.
|
|
125
|
+
4. **UI baseline gate**: run `npx @eventmodelers/cli ui-baseline check` first and act only on its verdict — `UNCHANGED` means skip all UI work; `NO_BASELINE`/`UNKNOWN` mean one full screen diff and every `codeGen.uiPrompts` entry; `CHANGED` names the elements that changed, were added or removed (work on exactly those) and flags `SCREEN_CHANGED` (re-fetch the screen node and adjust the components to it), `ELEMENTS_CHANGED` (diff slice.json against the code) and `PROMPTS_CHANGED` (apply the printed prompts). Details in `lib/prompt.md`, Step 4. After the checks pass and before committing, run `ui-baseline record`.
|
|
126
|
+
5. **Verify against slice.json**: After the skill completes, check that every command field, event field, and specification in slice.json appears in the implementation — specifications are read (Step 3a of the build skill) to understand how the UI should behave and present outcomes; the business logic lives in the backend and is not re-implemented here. No invented fields — if it is not in slice.json, it must not be in the code.
|
|
127
|
+
6. Run `npm run build` and `npm run lint` (both must pass clean), then the slice's own tests only, if any exist.
|
|
128
|
+
7. If checks pass, commit with `feat: [Slice Name]` and set slice status to `Done` — **unless** the
|
|
128
129
|
matching build skill flagged a missing `apiEndpoint` and already marked the slice `Blocked`
|
|
129
130
|
(Step 1 exception above); in that case, commit as normal but leave the status `Blocked`.
|
|
130
131
|
|
|
@@ -43,4 +43,4 @@ These files are always up to date — read them directly before invoking any ski
|
|
|
43
43
|
- The `boardId` and `organizationId` from each payload provide full context — pass them to skills.
|
|
44
44
|
- Node events use `node:created`, `node:changed`, `node:deleted` — always POST to `/api/org/:orgId/boards/:boardId/nodes/events`.
|
|
45
45
|
- Slice metadata (title, status) lives on the SLICE_BORDER node under `meta.sliceStatus` and `meta.title`.
|
|
46
|
-
- `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard, not a bug. It means another agent already claimed the slice. Treat it as `ALREADY_IN_STATUS`, skip that slice, and move on to the next `
|
|
46
|
+
- `/update-slice-status` rejects moving a slice into a status it's already in — this is a concurrency guard, not a bug. It means another agent already claimed the slice. Treat it as `ALREADY_IN_STATUS`, skip that slice, and move on to the next `Waiting` one instead of erroring out. This applies to queued tasks only: when the task request names the slice to build, the run loop owns its status — never call `/update-slice-status` there.
|