@eventmodelers/cli 1.0.20 → 1.0.22
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/cli.js +24 -8
- package/package.json +1 -1
- package/shared/skills/learn-eventmodelers-api/SKILL.md +46 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +10 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +22 -2
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +92 -8
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md +3 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +28 -9
- package/stacks/modeling-kit/templates/.claude/skills/html-screen/SKILL.md +50 -1
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +28 -0
- package/stacks/modeling-kit/templates/.claude/skills/storyboard/SKILL.md +41 -0
- package/stacks/modeling-kit/templates/.claude/skills/storyboard-screen/SKILL.md +46 -1
package/cli.js
CHANGED
|
@@ -1413,6 +1413,22 @@ async function runModeling(kitDir, projectDir, verbose = false) {
|
|
|
1413
1413
|
|
|
1414
1414
|
const channelName = `org:${cfg.organizationId}`;
|
|
1415
1415
|
const realtime = await createRealtimeAdapter(cfg, realtimeToken);
|
|
1416
|
+
|
|
1417
|
+
let lastTokenRefreshAt = 0;
|
|
1418
|
+
async function refreshRealtimeToken(reason) {
|
|
1419
|
+
// Guard against hammering the token endpoint: a rejected channel retries every
|
|
1420
|
+
// ~14s on its own, so without this a bad token would trigger a refresh call per retry.
|
|
1421
|
+
if (Date.now() - lastTokenRefreshAt < 5000) return;
|
|
1422
|
+
lastTokenRefreshAt = Date.now();
|
|
1423
|
+
try {
|
|
1424
|
+
realtimeToken = await getRealtimeToken();
|
|
1425
|
+
await realtime.setAuth(realtimeToken);
|
|
1426
|
+
log(`token refreshed (${reason})`);
|
|
1427
|
+
} catch (err) {
|
|
1428
|
+
log(`token refresh failed (${reason}): ${err.message}`);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1416
1432
|
realtime.subscribe(
|
|
1417
1433
|
channelName,
|
|
1418
1434
|
{
|
|
@@ -1429,19 +1445,19 @@ async function runModeling(kitDir, projectDir, verbose = false) {
|
|
|
1429
1445
|
(status) => {
|
|
1430
1446
|
log(`channel "${channelName}": ${status}`);
|
|
1431
1447
|
if (status === 'SUBSCRIBED') drain().catch((err) => log(`initial drain error: ${err.message}`));
|
|
1448
|
+
// A bad/stale token otherwise sits in realtime-js's own rejoin-retry loop until the
|
|
1449
|
+
// next scheduled refresh below — up to 10 minutes of failed joins. Refresh immediately
|
|
1450
|
+
// instead of waiting on the clock.
|
|
1451
|
+
if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
|
|
1452
|
+
refreshRealtimeToken(status).catch(() => {});
|
|
1453
|
+
}
|
|
1432
1454
|
},
|
|
1433
1455
|
).catch((err) => {
|
|
1434
1456
|
log(`realtime subscribe failed, prompts won't be pushed live: ${err.message}`);
|
|
1435
1457
|
});
|
|
1436
1458
|
|
|
1437
|
-
setInterval(
|
|
1438
|
-
|
|
1439
|
-
realtimeToken = await getRealtimeToken();
|
|
1440
|
-
await realtime.setAuth(realtimeToken);
|
|
1441
|
-
log('token refreshed');
|
|
1442
|
-
} catch (err) {
|
|
1443
|
-
log(`token refresh failed: ${err.message}`);
|
|
1444
|
-
}
|
|
1459
|
+
setInterval(() => {
|
|
1460
|
+
refreshRealtimeToken('scheduled').catch(() => {});
|
|
1445
1461
|
}, 10 * 60 * 1000);
|
|
1446
1462
|
|
|
1447
1463
|
const ping = async () => {
|
package/package.json
CHANGED
|
@@ -110,6 +110,7 @@ READMODEL // Query result / materialized view
|
|
|
110
110
|
SCENARIO // GWT scenario
|
|
111
111
|
LANE // Timeline row
|
|
112
112
|
SLICE_BORDER // Slice boundary marker
|
|
113
|
+
MARKDOWN // Free-text markdown note — the content type a `feedback` lane accepts (see §2)
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
---
|
|
@@ -283,6 +284,33 @@ Drop a node into a timeline cell. Validates placement rules. If the node was alr
|
|
|
283
284
|
|
|
284
285
|
---
|
|
285
286
|
|
|
287
|
+
### Feedback lanes and MARKDOWN nodes (free-text notes)
|
|
288
|
+
|
|
289
|
+
A chapter has no `feedback` lane by default — add one first via the lanes endpoint above (`{"type": "feedback", "label": "Notes"}`), which returns a `laneId`. This is a normal row in `meta.timelineData.rows` (`type: "feedback"`) alongside `actor`/`interaction`/`swimlane`/`spec`.
|
|
290
|
+
|
|
291
|
+
Place a free-text markdown note in that lane the same way any other node is placed — a plain `node:created` event through `POST .../nodes/events` (§3), **not** the cell-drop endpoint above. `cellId` is `"<feedbackRowId>-<columnId>"`, same convention as every other lane:
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"id": "<event-uuid>",
|
|
296
|
+
"eventType": "node:created",
|
|
297
|
+
"nodeId": "<node-uuid>",
|
|
298
|
+
"boardId": "<boardId>",
|
|
299
|
+
"timestamp": 1234567890,
|
|
300
|
+
"chapterId": "<chapterId>",
|
|
301
|
+
"cellId": "<feedbackRowId>-<columnId>",
|
|
302
|
+
"meta": {
|
|
303
|
+
"type": "MARKDOWN",
|
|
304
|
+
"title": "Modeling Reasoning — <Chapter Name>",
|
|
305
|
+
"description": "# Heading\n\nFull markdown body here — headings, lists, bold, etc. all render."
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The node's content lives in `meta.description` (a plain string of markdown source) — **not** `meta.content`; that field is silently accepted and stored but never rendered, producing a visibly empty note. There is no `fields[]` array on this element type, and no separate render/sketch call is needed (unlike SCREEN/HTML_SCREEN). `node.type` comes back as `"markdown"` (lowercase) on read.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
286
314
|
## 3. Nodes
|
|
287
315
|
|
|
288
316
|
**File**: `src/slices/change/api-nodes/routes.ts`
|
|
@@ -314,6 +342,7 @@ interface NodeChangeEvent {
|
|
|
314
342
|
backgroundColor?: string
|
|
315
343
|
title?: string
|
|
316
344
|
url?: string
|
|
345
|
+
linkedTo?: string // origin node id — marks this node as a linked copy (see below)
|
|
317
346
|
// ...other node data fields
|
|
318
347
|
// Do NOT set a "type" here — the server derives the node's render type from
|
|
319
348
|
// meta.type automatically. Setting one yourself risks it being read as the
|
|
@@ -363,12 +392,29 @@ Auto-connect a node to its timeline neighbors — mirrors the frontend's auto-co
|
|
|
363
392
|
|
|
364
393
|
Incompatible or already-connected neighbors are reported in `skipped`, not an error. Returns an empty result for nodes not placed on any timeline, or not a connectable element type (e.g. SCENARIO/spec nodes are never auto-connected).
|
|
365
394
|
|
|
395
|
+
**Known gap**: the "own column already has a SCREEN" guard checks only for a SCREEN specifically — not an AUTOMATION. Placing an AUTOMATION in a COMMAND's own column, with a SCREEN sitting in the previous column, wires *both* into the COMMAND, leaving it with two issuers. A command is never issued by more than one thing — see `place-element` Step 7c for the check-and-fix.
|
|
396
|
+
|
|
397
|
+
**Connections (both auto-connect and `set_connection`) only ever pair nodes on the same timeline** — a node in Chapter A can never be wired directly to a node in Chapter B, even when the type pair is otherwise valid (e.g. EVENT→READMODEL). No direct cross-timeline connection is possible.
|
|
398
|
+
|
|
399
|
+
**The supported workaround is a linked copy**: place a copy of the source EVENT into its own swimlane on the *consuming* timeline, with `node.data.linkedTo` set to the origin node's id (see `linkedTo` in the `NodeChangeEvent` shape above; `eventmodeling-checking-completeness` documents how to recognize one when reading the board — it's an intentional copy, never a duplicate to clean up). Once the copy exists on the consuming timeline, it's a same-timeline node like any other and can be wired normally (e.g. linked-EVENT→READMODEL→SCREEN) to satisfy that context's local data need. Only fall back to documenting an integration gap when a linked copy genuinely isn't the right shape for the need (e.g. the consuming context needs live/aggregate data no single event copy can represent).
|
|
400
|
+
|
|
366
401
|
**Response**:
|
|
367
402
|
- `200` — `{ connected: [{edgeId, source, target, created}], skipped: [{nodeId, reason}] }`
|
|
368
403
|
- `404` — node not found
|
|
369
404
|
|
|
370
405
|
---
|
|
371
406
|
|
|
407
|
+
### POST `/api/org/:orgId/boards/:boardId/connections`
|
|
408
|
+
Create a single type-checked directed edge between two existing nodes — the REST fallback for `set_connection`.
|
|
409
|
+
|
|
410
|
+
**Request body**: `{ source: string, target: string }` (node ids)
|
|
411
|
+
|
|
412
|
+
**Response**: `200`/`201` — `{ edgeId, source, target }` on success · `400` — the pair is not one of the allowed type combinations · `404` — a node id doesn't exist
|
|
413
|
+
|
|
414
|
+
**`EVENT → READMODEL` is exempt from column ordering** — an event in a later column can connect to a read model in an earlier column, and vice versa. A read model is a continuously-listening projection, not a point-in-time action, so it can be fed by an event anywhere on its timeline. Every other pair (`SCREEN → COMMAND`, `COMMAND → EVENT`, `READMODEL → SCREEN`, `READMODEL → AUTOMATION`, `AUTOMATION → COMMAND`) is still forward-only. If a connection you expect to work gets rejected, retry once before concluding it's blocked — a transient rejection has been observed on an otherwise-valid pair.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
372
418
|
## 4. Images
|
|
373
419
|
|
|
374
420
|
**File**: `src/slices/change/api-images/routes.ts`
|
package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md
CHANGED
|
@@ -19,6 +19,8 @@ Prefer `mcp__eventmodelers__*` tools when available (registered by the `connect`
|
|
|
19
19
|
artifact — only build one when the user's request explicitly asks for a "storyline", "walkthrough",
|
|
20
20
|
or "narrative". Never generate a storyline as a side effect of ordinary scenario elaboration.
|
|
21
21
|
|
|
22
|
+
**Exception — the mandatory read-model scenario pass** (see the "Per read model" section of the Quality Checklist below): when the user's request already frames read-model coverage as open to either form — e.g. "add scenarios for the read models, GWT or storylines", or the orchestrating skill's Step 7 gate, which requires exactly that — the explicit-ask condition is satisfied at that request level, not per read model. Within that pass, decide GWT vs. storyline **per read model** based on domain fit (does this read model's row genuinely walk through multiple states worth narrating?), not by asking again for every individual read model and not by defaulting to GWT for all of them just because that's the baseline elsewhere in this skill. Outside that pass — an ad-hoc "write scenarios for X" with no mention of storylines — the strict on-demand rule above still applies unchanged.
|
|
23
|
+
|
|
22
24
|
A GWT scenario asserts one isolated transition: a single precondition, a single action, a single
|
|
23
25
|
resulting outcome. A storyline instead narrates one specific use case as an ordered sequence of
|
|
24
26
|
**beats**, where the *same* element (usually a read model) is walked through multiple states across
|
|
@@ -804,11 +806,18 @@ After posting, tell the user:
|
|
|
804
806
|
|
|
805
807
|
**No command has only 2 scenarios unless all other types were reviewed and found inapplicable.**
|
|
806
808
|
|
|
809
|
+
**Per read model — this is a separate, equally mandatory pass, not an afterthought of the command pass above:**
|
|
810
|
+
- [ ] **Every READMODEL on the board has at least one view scenario** — GWT (`given`: source EVENTs, `when`: empty, `then`: the READMODEL) or a storyline. A model with dozens of command scenarios and 0 read-model scenarios is not a complete Step 7 — it's easy to walk away thinking coverage is thorough because the command side looks exhaustive, so check the read-model side explicitly before reporting this step done.
|
|
811
|
+
- [ ] **Population scenario** — the view shows correct data after its source event(s)
|
|
812
|
+
- [ ] **Removal/update scenario, where applicable** — a row disappears or changes (`expectEmptyList: true` for list-type views) after an event that supersedes it (expiry, return, archival, withdrawal, status change, etc.). `EVENT → READMODEL` is exempt from column ordering (see `learn-eventmodelers-api` §3), so a later event connecting back to an earlier-placed read model is normal — add the connection if it's missing rather than assuming the scenario is impossible. Only skip this scenario, with a documented gap (TASK comment), when the superseding event genuinely lives in a different chapter.
|
|
813
|
+
- [ ] **GWT vs. storyline decided per read model, not applied uniformly** — reach for a storyline wherever the *same* read model row genuinely walks through multiple states worth narrating; the rest of the read models in the same model may be correctly GWT-only. Don't default to one format for every read model just because it worked for the first one, and don't judge "multiple states" by counting *distinct connected event types* — that undercounts real candidates. **A single event type recurring with different data is just as valid a storyline driver as several different event types**: `AccountFunded($40)` then `AccountFunded($70)` walking a balance read model from $40 to $110 is exactly as strong a storyline as a multi-event lifecycle. In practice this means almost every list/aggregate read model qualifies — a titles list growing from one row to two as the same `TitleAdded`-shaped event recurs, a dashboard's counters incrementing as the same `CopyAdded` event recurs, are both genuine storylines, not "just" GWT territory. Ask "does replaying this read model's *actually connected* event(s) more than once produce an interesting accumulated/changed state?" — not "how many different event types feed this."
|
|
814
|
+
- [ ] **No redundancy or contradiction between a read model's GWT scenarios and its storyline** — if both exist for the same read model, read the storyline's beats before finalizing the GWTs. A GWT that asserts the same state a beat already shows is redundant (delete it); a GWT written without tracing the same causal sequence the storyline encodes can end up asserting something the storyline's beats actually contradict (e.g. claiming two entities coexist in a view when the storyline correctly shows one superseding the other) — delete or fix it, never leave a contradiction on the board.
|
|
815
|
+
- [ ] **Cross-context read models handled honestly** — if a read model's true source events live in a different chapter, `given` can't reference them (same-timeline-only, like connections); write the scenario with an empty `given` and say so explicitly in the scenario title, rather than silently omitting the scenario or fabricating a same-timeline event that isn't the real source
|
|
816
|
+
|
|
807
817
|
**Format and posting:**
|
|
808
818
|
- [ ] State preconditions are explicit in Given (not just "Given an order")
|
|
809
819
|
- [ ] Actions are clear in When
|
|
810
820
|
- [ ] Outcomes are verifiable in Then (event produced or rejection with reason)
|
|
811
|
-
- [ ] Every view has at least one update scenario
|
|
812
821
|
- [ ] All scenarios posted to board spec cells via the `/scenarios` API
|
|
813
822
|
- [ ] **Workshop facilitation approach documented**
|
|
814
823
|
- [ ] **All stakeholder roles (PO, Dev, QA, Domain Expert) perspectives captured**
|
package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md
CHANGED
|
@@ -384,6 +384,16 @@ After the step is done, **every SCREEN and every AUTOMATION on the board must be
|
|
|
384
384
|
|
|
385
385
|
> **Placement rule**: A read model must be placed in a column that already contains a SCREEN or AUTOMATION it serves. Do not place read models in columns with no screen or automation — doing so creates orphaned read models that will never have a consumer.
|
|
386
386
|
|
|
387
|
+
### Pull field mappings from Step 3 — they are the spec, not a guess
|
|
388
|
+
|
|
389
|
+
**Do not re-derive read model needs from a screen's title or description alone, and do not rely on the orchestrator's phase-summary handoff for this** — if you arrived here via `eventmodeling-orchestrating-event-modeling`, the handoff after Step 3 is a short hand-written prose summary (`.trogonai/interviews/.../EVENTMODELING.md`), not the actual field data. It will not reliably carry the per-field mappings forward. Go back to the board itself:
|
|
390
|
+
|
|
391
|
+
For every SCREEN node, fetch it directly (`get_node`/`get_nodes`, never from memory) and read its `meta.fields`. Step 3 already required every field to carry a `mapping`, and for view fields that mapping is already in the exact form `"<ReadModelTitle>.<fieldName>"` — recorded specifically so this step doesn't have to re-guess it.
|
|
392
|
+
|
|
393
|
+
- **Group the screen's fields by the `<ReadModelTitle>` already named in their `mapping`.** That grouping — not a fresh read of the screen's visuals — is the read model's title and field list. Build the READMODEL node from it directly.
|
|
394
|
+
- If a field's `mapping` names a read model that isn't `"<CommandTitle>.<fieldName>"` or `"session:..."` or `"derived:..."`, it is a read-model reference — treat it as a requirement, not a suggestion.
|
|
395
|
+
- A screen with no fields, or with fields that carry no read-model-shaped mapping, is **not** evidence that it needs no read model. Re-check it against the three rules above (view screen / automation / command screen showing prior state) before concluding it's the rare blank-form exception — and say explicitly why it qualifies.
|
|
396
|
+
|
|
387
397
|
### Field data lineage — the `mapping` attribute on READMODEL fields
|
|
388
398
|
|
|
389
399
|
Every field on a READMODEL must carry a `mapping` that says exactly which event (or command) field it is projected from. Use one of these forms:
|
|
@@ -582,6 +592,16 @@ After `place-element` returns the READMODEL node ID, create the arrows that comp
|
|
|
582
592
|
|
|
583
593
|
Skip a connection silently if the target cell is empty. Log each created arrow: `→ connected EVENT→READMODEL "OrderPlaced"→"OrderStatusView"`, `→ connected READMODEL→SCREEN "OrderStatusView"→"Order Status Screen"`, or `→ connected READMODEL→AUTOMATION "OrderStatusView"→"Fulfillment Processor"`.
|
|
584
594
|
|
|
595
|
+
### Mandatory per-node verification (run before declaring this step done)
|
|
596
|
+
|
|
597
|
+
Do not declare Step 5 complete on the strength of the read models you happened to design. Instead, **re-fetch every SCREEN and AUTOMATION node on the board** (`get_nodes` per type — don't rely on the list built earlier in this step, the board may have moved on) and check each one individually:
|
|
598
|
+
|
|
599
|
+
1. Does it now have an incoming `READMODEL → SCREEN` or `READMODEL → AUTOMATION` connection?
|
|
600
|
+
2. If not — is it a provably blank creation form with no prior state? State the reason in one line (e.g. `"Register Account" screen: blank form, no prior state — exempt`).
|
|
601
|
+
3. If it's neither connected nor exempt, it is an **unresolved gap**. Fix it now: design the missing read model (pulling from its `meta.fields`/`mapping` as above) and wire the connection. Do not move to Step 6 with an unresolved gap silently carried forward — either fix it or explicitly flag it to the user as accepted debt.
|
|
602
|
+
|
|
603
|
+
List the result of this pass (connected / exempt / fixed) for every screen and automation checked — this list is the evidence the orchestrator's Step 5 gate ("every screen data need is satisfied by a read model") actually holds, not just an assumption.
|
|
604
|
+
|
|
585
605
|
After all read models, screens, automations, and connections are in place, present the Read Model Catalog summary as text to the user.
|
|
586
606
|
|
|
587
607
|
---
|
|
@@ -694,8 +714,8 @@ Identify UI needs without event sources:
|
|
|
694
714
|
|
|
695
715
|
### Read Model Design
|
|
696
716
|
- [ ] **Typical pattern applied**: most screens follow `READ MODEL → SCREEN → COMMAND → EVENT`
|
|
697
|
-
- [ ] **Every SCREEN from storyboarding is connected to at least one read model** (via `READMODEL → SCREEN`); only blank creation forms may be exempt
|
|
698
|
-
- [ ] **Every AUTOMATION from storyboarding is connected to at least one read model** (via `READMODEL → AUTOMATION`)
|
|
717
|
+
- [ ] **Every SCREEN from storyboarding is connected to at least one read model** (via `READMODEL → SCREEN`); only blank creation forms may be exempt — verified via the mandatory per-node pass above, not assumed
|
|
718
|
+
- [ ] **Every AUTOMATION from storyboarding is connected to at least one read model** (via `READMODEL → AUTOMATION`) — same per-node verification
|
|
699
719
|
- [ ] **No read model is placed without a connected SCREEN or AUTOMATION consumer**
|
|
700
720
|
- [ ] Every read model has clear purpose
|
|
701
721
|
- [ ] Every data field has event source
|
|
@@ -13,7 +13,7 @@ allowed-tools:
|
|
|
13
13
|
|
|
14
14
|
Prefer `mcp__eventmodelers__*` tools when available (registered by the `connect` skill) — the curl blocks below are the fallback for sessions without MCP connected.
|
|
15
15
|
|
|
16
|
-
Coordinates the
|
|
16
|
+
Coordinates the 11-step Event Modeling workflow. Each step delegates to a
|
|
17
17
|
specialized skill — this skill holds the sequence, transition conditions, and
|
|
18
18
|
what to carry forward between steps.
|
|
19
19
|
|
|
@@ -101,6 +101,10 @@ Screens placed during Step 3 (Storyboarding) are provisional positions. Steps 4
|
|
|
101
101
|
### Column insertion
|
|
102
102
|
Use `POST /timelines/:tl/columns` with `{"index": N}` to insert a column at a specific position (shifts existing columns right). Do not use `{}` (append) when placing read models or view screens — always target the correct position.
|
|
103
103
|
|
|
104
|
+
### Documenting decisions inline, at any step
|
|
105
|
+
|
|
106
|
+
Separate from the Step 11 chapter-level reasoning note: at **any** step (1–10), if that step makes a decision or assumption important enough that a later reader could otherwise misread the model, add a small MARKDOWN note in the **column where that decision applies** (same feedback-lane + MARKDOWN mechanics as Step 11 — see there for the exact calls). Use sparingly — this is for a genuine "why is it like this" moment (an assumption that fills a gap the brief left open, a rejected alternative, a non-obvious constraint), not routine narration of what a step did.
|
|
107
|
+
|
|
104
108
|
---
|
|
105
109
|
|
|
106
110
|
## Interview Phase
|
|
@@ -166,7 +170,7 @@ Also update the Interview Trail table row for this step (Status → Done, Key Ou
|
|
|
166
170
|
|
|
167
171
|
After writing the summary, run `/compact` to clear the accumulated context before loading the next skill. The summary written above is the handoff — the next skill reads it from the file, not from the conversation history.
|
|
168
172
|
|
|
169
|
-
This keeps each step's context lean and prevents token bloat from accumulating across all
|
|
173
|
+
This keeps each step's context lean and prevents token bloat from accumulating across all 11 steps.
|
|
170
174
|
|
|
171
175
|
---
|
|
172
176
|
|
|
@@ -266,9 +270,14 @@ if Conway's Law boundaries are not relevant to the project.
|
|
|
266
270
|
Invoke `eventmodeling-elaborating-scenarios`.
|
|
267
271
|
|
|
268
272
|
**Input**: Commands and read models.
|
|
269
|
-
**Output to carry forward**: Given-When-Then specifications
|
|
270
|
-
|
|
271
|
-
|
|
273
|
+
**Output to carry forward**: Given-When-Then specifications (or storylines, for
|
|
274
|
+
walkthrough-style coverage) for every command **and every read model**, posted
|
|
275
|
+
to the board spec cells.
|
|
276
|
+
**Gate**: Every command has scenarios covering **all applicable types** from
|
|
277
|
+
the elaborating-scenarios workflow — not just happy path + one error case —
|
|
278
|
+
**and every READMODEL on the board has at least one view scenario**. See the
|
|
279
|
+
gate checklist below. A command-only pass is an incomplete Step 7, even if
|
|
280
|
+
every command's coverage looks exhaustive.
|
|
272
281
|
|
|
273
282
|
> **Do not reduce scenarios to a simple good-case / bad-case pair.** The `eventmodeling-elaborating-scenarios` skill defines a structured scenario workshop covering seven scenario types per command. All applicable types must be written before this step is complete.
|
|
274
283
|
|
|
@@ -283,7 +292,9 @@ and view, posted to the board spec cells.
|
|
|
283
292
|
|
|
284
293
|
For each type, ask the relevant question against the business case and write a scenario if the situation can occur. Do not decide based on brevity — decide based on the domain.
|
|
285
294
|
|
|
286
|
-
>
|
|
295
|
+
> **Read models need scenarios too — easy to forget since the seven types above are command-shaped.** Every READMODEL needs at least one view scenario (GWT or storyline); a read model with zero scenarios is as incomplete as a command with zero. `eventmodeling-elaborating-scenarios`'s own checklist covers the details — connectivity rules, GWT-vs-storyline judgment per read model, and avoiding redundancy between a storyline and its GWTs — don't re-derive those here, just enforce the gate.
|
|
296
|
+
|
|
297
|
+
> The `eventmodeling-elaborating-scenarios` skill designs scenarios **and** posts them to the board. It uses `GET /timelines/$TL/spec-info` to resolve node IDs, then `POST /timelines/$TL/columns/$COL/scenarios` with all scenarios for that column in one call (array body) — this applies identically whether the column holds a COMMAND or a READMODEL. The SCENARIO spec node is created automatically. Ensure the timeline and column IDs are resolved and passed to the skill before invoking it.
|
|
287
298
|
|
|
288
299
|
---
|
|
289
300
|
|
|
@@ -331,6 +342,76 @@ already had one.
|
|
|
331
342
|
|
|
332
343
|
---
|
|
333
344
|
|
|
345
|
+
### Step 11: Document Reasoning
|
|
346
|
+
|
|
347
|
+
Not delegated to a separate skill — performed directly by this orchestrating skill, since the reasoning being documented is the *orchestrator's own* accumulated context across all prior steps, not something a single-step skill has visibility into.
|
|
348
|
+
|
|
349
|
+
**Input**: The complete, sliced, validated model (Steps 1–10) plus this session's own record of decisions made along the way — assumptions added beyond the literal brief, sequencing corrections, business rules deliberately encoded as scenarios rather than events, read-model sharing choices, and any cross-context/integration gaps found (e.g. during Step 6 or Step 9).
|
|
350
|
+
|
|
351
|
+
**Output to carry forward**: One MARKDOWN node per chapter, placed in that chapter's first column, containing the full modeling reasoning for that bounded context in as much detail as the session actually has to give — not a boilerplate template filled in thinly.
|
|
352
|
+
|
|
353
|
+
**Gate**: Every chapter on the board has exactly one reasoning MARKDOWN node in its first column, non-empty, written after the model for that chapter was already complete (so it can describe the *finished* shape, not a plan).
|
|
354
|
+
|
|
355
|
+
**Mechanics** — a chapter has no `feedback` lane by default; add one first, then place a MARKDOWN node in it:
|
|
356
|
+
|
|
357
|
+
1. **Add a feedback lane** (once per chapter, skip if one already exists — check `meta.timelineData.rows` for `type === "feedback"` first):
|
|
358
|
+
|
|
359
|
+
**Prefer MCP:**
|
|
360
|
+
```
|
|
361
|
+
mcp__eventmodelers__add_lane { "boardId": "$BOARD_ID", "timelineId": "$CHAPTER_ID", "type": "feedback", "label": "Notes" }
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Fallback (no MCP):**
|
|
365
|
+
```bash
|
|
366
|
+
curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/timelines/$CHAPTER_ID/lanes" \
|
|
367
|
+
-H "x-token: $TOKEN" -H "x-board-id: $BOARD_ID" -H "x-user-id: orchestrator" \
|
|
368
|
+
-H "Content-Type: application/json" \
|
|
369
|
+
-d '{"type":"feedback","label":"Notes"}'
|
|
370
|
+
# → { laneId, type, label, index, totalLanes }
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
2. **Resolve the first column's ID** — the leftmost entry in `meta.timelineData.columns` (same chapter fetch used throughout this workflow for row/column lookups).
|
|
374
|
+
|
|
375
|
+
3. **Create the MARKDOWN node**, `cellId = "<feedbackLaneId>-<firstColumnId>"`:
|
|
376
|
+
|
|
377
|
+
**Prefer MCP:**
|
|
378
|
+
```
|
|
379
|
+
mcp__eventmodelers__submit_node_events {
|
|
380
|
+
"boardId": "$BOARD_ID",
|
|
381
|
+
"events": [{
|
|
382
|
+
"id": "<event-uuid>", "eventType": "node:created", "nodeId": "<node-uuid>",
|
|
383
|
+
"boardId": "$BOARD_ID", "timestamp": 1234567890,
|
|
384
|
+
"chapterId": "$CHAPTER_ID", "cellId": "<feedbackLaneId>-<firstColumnId>",
|
|
385
|
+
"meta": { "type": "MARKDOWN", "title": "Modeling Reasoning — <Chapter Name>", "description": "<full markdown body>" }
|
|
386
|
+
}]
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Fallback (no MCP):**
|
|
391
|
+
```bash
|
|
392
|
+
curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
|
|
393
|
+
-H "x-token: $TOKEN" -H "x-board-id: $BOARD_ID" -H "x-user-id: orchestrator" \
|
|
394
|
+
-H "Content-Type: application/json" \
|
|
395
|
+
-d '[{"id":"<event-uuid>","eventType":"node:created","nodeId":"<node-uuid>","boardId":"<BOARD_ID>",
|
|
396
|
+
"timestamp":1234567890,"chapterId":"<CHAPTER_ID>","cellId":"<feedbackLaneId>-<firstColumnId>",
|
|
397
|
+
"meta":{"type":"MARKDOWN","title":"Modeling Reasoning — <Chapter Name>","description":"<full markdown body>"}}]'
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
The note's body lives in **`meta.description`** as plain markdown source — headings, lists, bold, code fences, tables all render. **Not `meta.content`** — that field is accepted and stored without error but never rendered by the board UI, producing a visibly empty note; this was caught by comparing against a note authored directly in the UI, so treat it as confirmed, not a guess. There is no separate render/sketch call (unlike SCREEN/HTML_SCREEN) and no `fields[]` array on this element type.
|
|
401
|
+
|
|
402
|
+
**What the note should actually contain** — write for the next person (or next session) who opens this board cold, not for whoever just built it:
|
|
403
|
+
- **Scope**: what business process this chapter covers, and its stream roots (identity keys).
|
|
404
|
+
- **Assumptions added beyond the literal brief** — anything invented to fill a gap the requirements left open, and why (e.g. adding a resolution event so a state isn't a one-way trap door).
|
|
405
|
+
- **Business rules deliberately encoded as scenarios, not new events** — so a reader doesn't mistake a missing event for an oversight.
|
|
406
|
+
- **Sequencing or design corrections made mid-workflow** — e.g. a column reorder because an event's original placement implied the wrong causality.
|
|
407
|
+
- **Read model design rationale** — especially where one read model deliberately serves several screens/automations, so it doesn't read as a missing 1:1 mapping.
|
|
408
|
+
- **Any cross-context or integration gaps found** (Step 6 Conway's Law, or discovered incidentally, e.g. a same-timeline connection constraint blocking a needed cross-chapter data dependency) — state the finding and the viable resolutions, matching whatever TASK/QUESTION comment was also posted on the affected node.
|
|
409
|
+
- **Closing summary**: element counts and the validation verdict for this chapter's slice of the model.
|
|
410
|
+
|
|
411
|
+
If a chapter's story is genuinely simple, say so briefly rather than padding — but for any chapter with real design decisions behind it, this note is the place those decisions survive past the session that made them.
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
334
415
|
## Final Output
|
|
335
416
|
|
|
336
417
|
A complete, sliced event model consisting of:
|
|
@@ -344,10 +425,11 @@ A complete, sliced event model consisting of:
|
|
|
344
425
|
- Completeness verification
|
|
345
426
|
- Validation report with readiness verdict
|
|
346
427
|
- Slice definitions marking every independently deployable feature boundary
|
|
428
|
+
- A Modeling Reasoning MARKDOWN node in each chapter's first column, documenting the design decisions, assumptions, and any integration gaps behind that chapter's model
|
|
347
429
|
|
|
348
430
|
### Optional Follow-on Skills
|
|
349
431
|
|
|
350
|
-
These skills are not part of the
|
|
432
|
+
These skills are not part of the 11-step main path but extend the model for
|
|
351
433
|
specific needs:
|
|
352
434
|
|
|
353
435
|
- **`eventmodeling-designing-event-models`** — Use when stream identity,
|
|
@@ -364,12 +446,14 @@ specific needs:
|
|
|
364
446
|
## Quality Checklist
|
|
365
447
|
|
|
366
448
|
- [ ] No elements stranded at 0,0 — every EVENT, COMMAND, READMODEL, SCREEN, and AUTOMATION has a valid `cellId` in its chapter
|
|
367
|
-
- [ ] All
|
|
449
|
+
- [ ] All 11 modeling steps completed — no step skipped without explicit reason
|
|
368
450
|
- [ ] Every COMMAND, READMODEL, and AUTOMATION has a matching slice definition on the board
|
|
451
|
+
- [ ] Every chapter has a Modeling Reasoning MARKDOWN node in its first column, written after that chapter's model was complete
|
|
369
452
|
- [ ] Role Catalog exists with named human roles and system processors
|
|
370
453
|
- [ ] Every command is attributed to a specific role from the Role Catalog
|
|
371
454
|
- [ ] Every read model satisfies at least one UI or processor query need
|
|
372
455
|
- [ ] At least one Given-When-Then scenario exists per command
|
|
456
|
+
- [ ] At least one view scenario (GWT or storyline) exists per READMODEL — not just per command
|
|
373
457
|
- [ ] Completeness check shows no unresolved field traceability gaps
|
|
374
458
|
- [ ] Validation returns PASS or PASS WITH WARNINGS with all critical issues resolved
|
|
375
459
|
- [ ] Interview trail in `.trogonai/` updated with status of each completed step
|
package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models/SKILL.md
CHANGED
|
@@ -80,6 +80,7 @@ Verify each swimlane has:
|
|
|
80
80
|
### 2. Consistency Checks
|
|
81
81
|
|
|
82
82
|
- [ ] **Event-Stream Mapping**: Every event belongs to exactly one lane
|
|
83
|
+
- [ ] **Single Command Issuer**: Every command is issued by exactly one SCREEN or AUTOMATION — never two. Check each COMMAND node's inbound edges; more than one SCREEN/AUTOMATION wired into the same command is a CRITICAL violation (commonly an auto-connect artifact — see `place-element` Step 7c)
|
|
83
84
|
- [ ] **Command Outcomes**: Every command produces events OR documents rejection
|
|
84
85
|
- [ ] **Deterministic Projections**: State can only be derived one way from events
|
|
85
86
|
- [ ] **No Side Effects in Projections**: Pure state reconstruction logic
|
|
@@ -192,6 +193,7 @@ Format findings as comments:
|
|
|
192
193
|
| Orphaned events | Events no one listens to | Link to projections or commands |
|
|
193
194
|
| No read models | Commands reading query/read models for validation | Add separate query read models; keep command state minimal |
|
|
194
195
|
| Circular dependencies | Projection A depends on B, B on A | Redesign stream boundaries |
|
|
196
|
+
| Command issued by multiple things | COMMAND node has 2+ inbound SCREEN/AUTOMATION edges | Keep the deliberate same-column issuer, remove the rest via `set_connection` (`action: "remove"`) — see `place-element` Step 7c |
|
|
195
197
|
|
|
196
198
|
## Key Principles for Event Sourcing
|
|
197
199
|
|
|
@@ -233,6 +235,7 @@ A model is **ready for code generation** if:
|
|
|
233
235
|
- [ ] State projection is deterministic from events
|
|
234
236
|
- [ ] Commands validate against current state only
|
|
235
237
|
- [ ] Each command either produces events or rejects (no silent failures)
|
|
238
|
+
- [ ] **No command has more than one inbound SCREEN/AUTOMATION edge (a command is never issued by more than one thing)**
|
|
236
239
|
- [ ] Event causality/command-event mapping is clear
|
|
237
240
|
- [ ] State transitions are documented
|
|
238
241
|
- [ ] No direct references between lanes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: eventmodeling-validating-event-models-checklist
|
|
3
|
-
description: "Validate event-sourced CQRS models against
|
|
3
|
+
description: "Validate event-sourced CQRS models against 17 architectural checks across 7 phases. Identifies anti-patterns and confirms compliance with event sourcing principles. Use when reviewing event models for production readiness or after completing event modeling steps. Do not use for: reviewing incomplete or in-progress models (use eventmodeling-validating-event-models), or for elaborating new scenarios (use eventmodeling-elaborating-scenarios)."
|
|
4
4
|
allowed-tools:
|
|
5
5
|
- Write
|
|
6
6
|
- Bash
|
|
@@ -12,7 +12,7 @@ allowed-tools:
|
|
|
12
12
|
|
|
13
13
|
Prefer `mcp__eventmodelers__*` tools when available (registered by the `connect` skill) — the curl blocks below are the fallback for sessions without MCP connected.
|
|
14
14
|
|
|
15
|
-
**Purpose**: Validate any event-sourced CQRS event model against
|
|
15
|
+
**Purpose**: Validate any event-sourced CQRS event model against 17 architectural checks across 7 phases. Identifies anti-patterns and confirms compliance with event sourcing principles.
|
|
16
16
|
|
|
17
17
|
**Applies To**: Any domain - e-commerce, banking, SaaS, marketplace, healthcare, etc.
|
|
18
18
|
|
|
@@ -24,7 +24,7 @@ Prefer `mcp__eventmodelers__*` tools when available (registered by the `connect`
|
|
|
24
24
|
|
|
25
25
|
**What It Does**:
|
|
26
26
|
1. Reads current board state (EVENT, COMMAND, READMODEL nodes) as input
|
|
27
|
-
2. Systematically applies
|
|
27
|
+
2. Systematically applies 17 validation checks across 7 phases
|
|
28
28
|
2. Identifies violations of event sourcing principles (domain-agnostic)
|
|
29
29
|
3. Flags anti-patterns (calculations as events, non-entity streams, etc.)
|
|
30
30
|
4. Verifies read model/event distinction
|
|
@@ -59,12 +59,13 @@ Use the board nodes as the model input. After the checklist, use `handle-comment
|
|
|
59
59
|
|
|
60
60
|
## Validation Phases (Domain-Agnostic)
|
|
61
61
|
|
|
62
|
-
### Phase 1: Event Stream & Command Handler State Validation (
|
|
62
|
+
### Phase 1: Event Stream & Command Handler State Validation (4 checks)
|
|
63
63
|
- Check 1.1: Each event belongs to exactly one stream
|
|
64
64
|
- Check 1.2: Each command handler owns its own [CommandHandler]State class
|
|
65
65
|
- Check 1.3: No hard dependencies between command handlers (orchestrated via events only)
|
|
66
|
+
- Check 1.4: Each command is issued by exactly one thing — no COMMAND node has more than one inbound SCREEN/AUTOMATION edge
|
|
66
67
|
|
|
67
|
-
**Anti-pattern to catch**: Sharing state across handlers or treating state as persistent aggregate
|
|
68
|
+
**Anti-pattern to catch**: Sharing state across handlers or treating state as persistent aggregate; a command wired from two issuers (commonly a `place-element` auto-connect artifact where the command's own column holds an AUTOMATION and the previous column's SCREEN also gets wired in — see `learn-eventmodelers-api` §3 auto-connect "Known gap")
|
|
68
69
|
|
|
69
70
|
### Phase 2: Event Quality Validation (3 checks)
|
|
70
71
|
- Check 2.1: Events represent domain facts, not calculations
|
|
@@ -204,6 +205,24 @@ Reconstruct [CommandHandler]State on-demand
|
|
|
204
205
|
|
|
205
206
|
**Why**: State is derived from events, never stored. Events are source of truth. This enables consistent replay, audit trails, and time-travel debugging.
|
|
206
207
|
|
|
208
|
+
### 5. Command With Multiple Issuers
|
|
209
|
+
```
|
|
210
|
+
ANTI-PATTERN:
|
|
211
|
+
FlagLoanOverdue (COMMAND) has two inbound edges:
|
|
212
|
+
- "Flag Overdue Loans" (AUTOMATION, same column)
|
|
213
|
+
- "Adjust Due Date" (SCREEN, previous column)
|
|
214
|
+
- Result: unclear who/what actually triggers the command; validation and UI-vs-automation
|
|
215
|
+
authority checks (e.g. Role & Actor Attribution) can no longer be answered
|
|
216
|
+
|
|
217
|
+
CORRECT:
|
|
218
|
+
FlagLoanOverdue (COMMAND) has exactly one inbound edge, from the AUTOMATION that owns it.
|
|
219
|
+
If the SCREEN's user genuinely needs to trigger the same outcome, that's a second,
|
|
220
|
+
distinctly-named command (or the SCREEN issuing it directly, with the automation removed) —
|
|
221
|
+
not two issuers sharing one command.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Why**: A command is never issued by more than one thing. Each command represents one specific trigger's decision to act — collapsing two triggers onto one command node hides which actor is actually responsible, and usually means either a naming/slice-boundary mistake or a stray auto-connect edge (`learn-eventmodelers-api` §3, "Known gap"). Fix by removing the extra edge via `set_connection` (`action: "remove"`), not by keeping both.
|
|
225
|
+
|
|
207
226
|
---
|
|
208
227
|
|
|
209
228
|
## Questions to Ask During Validation
|
|
@@ -233,7 +252,7 @@ Reconstruct [CommandHandler]State on-demand
|
|
|
233
252
|
## Success Criteria
|
|
234
253
|
|
|
235
254
|
**Model is validated when**:
|
|
236
|
-
- All
|
|
255
|
+
- All 17 checks pass (or have documented workarounds)
|
|
237
256
|
- No critical anti-patterns identified
|
|
238
257
|
- All 3 final questions answer YES
|
|
239
258
|
- Event sourcing principles clearly upheld
|
|
@@ -306,7 +325,7 @@ Running the checklist after Step 2 prevents wasting time on later steps if core
|
|
|
306
325
|
|
|
307
326
|
## Checklist Questions by Domain
|
|
308
327
|
|
|
309
|
-
The skill applies the same
|
|
328
|
+
The skill applies the same 17 checks regardless of domain. Here's how to think about it in different contexts:
|
|
310
329
|
|
|
311
330
|
**E-commerce domain**:
|
|
312
331
|
- Events: OrderCreated, OrderConfirmed, PaymentAuthorized, OrderShipped
|
|
@@ -338,7 +357,7 @@ The principle is the same across all domains: **immutable facts as events, calcu
|
|
|
338
357
|
|
|
339
358
|
## Quality Checklist
|
|
340
359
|
|
|
341
|
-
- [ ] All
|
|
360
|
+
- [ ] All 17 checks evaluated — no check skipped without documented justification
|
|
342
361
|
- [ ] Every FAIL result includes the specific event, handler, or stream that violated the check
|
|
343
362
|
- [ ] Anti-patterns identified by name with the exact model element that triggered the flag
|
|
344
363
|
- [ ] Final verdict is one of: PASS / PASS WITH WARNINGS / FAIL — no ambiguous outcomes
|
|
@@ -359,6 +378,6 @@ The principle is the same across all domains: **immutable facts as events, calcu
|
|
|
359
378
|
|
|
360
379
|
## Validation Checklist Reference
|
|
361
380
|
|
|
362
|
-
The
|
|
381
|
+
The 17-point checklist is defined in the **Validation Phases** section above.
|
|
363
382
|
Each check includes the anti-pattern to catch and questions to ask when evaluating your model.
|
|
364
383
|
|
|
@@ -79,6 +79,8 @@ This skill only has a `pages`/`backgroundColor` field to send (no separate marks
|
|
|
79
79
|
|
|
80
80
|
Apply these only to the specific element(s) the request describes — don't guess at additional areas to call out.
|
|
81
81
|
|
|
82
|
+
**Marked screens and field scoping**: when the same underlying screen is rendered multiple times as separate nodes — once per slice, each with a different mark/highlight calling out a different part of the UI — scope each node's `meta.fields` (Step 5 below) to only the data inside that node's highlighted area, not the full screen. Three slice-specific screen nodes sharing one visual base should end up with three different, narrower field lists, each matching what that node's mark calls out.
|
|
83
|
+
|
|
82
84
|
## Step 4 — Render the pages
|
|
83
85
|
|
|
84
86
|
**Updating an existing node** (`nodeId` was given) — always sends the **complete** pages array, not just the changed/new entry:
|
|
@@ -133,7 +135,54 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/html-screen-nodes/$N
|
|
|
133
135
|
|
|
134
136
|
Expect `204 No Content` on success from either curl call.
|
|
135
137
|
|
|
136
|
-
## Step 5 —
|
|
138
|
+
## Step 5 — Define field data lineage (mandatory)
|
|
139
|
+
|
|
140
|
+
Every screen — new or updated — needs `meta.fields`: one entry per piece of data the screen displays or captures, each with a `mapping` naming where that data comes from. A screen with only a title and no fields is an empty placeholder from a data-lineage standpoint, even if the mockup itself looks complete.
|
|
141
|
+
|
|
142
|
+
| Field type | `mapping` | Example |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| User types a value, sent as a command | `"<CommandTitle>.<fieldName>"` | `"ReserveBike.bikeId"` |
|
|
145
|
+
| Read from session | `"session:<fieldName>"` | `"session:customerId"` |
|
|
146
|
+
| Displayed data, sourced from a read model | `"<ReadModelTitle>.<fieldName>"` | `"ActiveReservationView.status"` |
|
|
147
|
+
| Calculated/formatted only for display | `"derived:<expression>"` | `"derived:formatDuration(durationMinutes)"` |
|
|
148
|
+
|
|
149
|
+
Name the read model even if it doesn't exist as a board node yet — this skill only renders the screen, it does not create READMODEL nodes or connections (that's `eventmodeling-identifying-outputs` or `place-element`, if the model is taken that far). But naming the source is **not optional**: a screen displaying data should almost never have a field with no mapping. If you can't say which read model a displayed field comes from, that's a sign the model is missing something — not a reason to skip the field.
|
|
150
|
+
|
|
151
|
+
If this node is one of several sharing the same visual base with different marks/highlights (see "Marked screens and field scoping" above), only list the fields that fall inside *this* node's highlighted area — not every field the shared screen shows.
|
|
152
|
+
|
|
153
|
+
Set `cardinality` too (`"Single"` unless the field is a repeated/list value), then push the fields onto the node:
|
|
154
|
+
|
|
155
|
+
**Prefer MCP:**
|
|
156
|
+
```
|
|
157
|
+
mcp__eventmodelers__submit_node_events {
|
|
158
|
+
"boardId": "<BOARD_ID>",
|
|
159
|
+
"events": [{
|
|
160
|
+
"id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<NODE_ID>",
|
|
161
|
+
"boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
|
|
162
|
+
"changedAttributes": ["meta.fields"],
|
|
163
|
+
"meta": { "type": "HTML_SCREEN", "fields": [
|
|
164
|
+
{"name": "status", "type": "String", "example": "confirmed", "mapping": "ActiveReservationView.status", "cardinality": "Single"}
|
|
165
|
+
] }
|
|
166
|
+
}]
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Fallback (no MCP):**
|
|
171
|
+
```bash
|
|
172
|
+
curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
|
|
173
|
+
-H "x-token: $TOKEN" -H "x-board-id: $BOARD_ID" -H "x-user-id: agent" \
|
|
174
|
+
-H "Content-Type: application/json" \
|
|
175
|
+
-d '[{
|
|
176
|
+
"id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<NODE_ID>",
|
|
177
|
+
"boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
|
|
178
|
+
"changedAttributes": ["meta.fields"],
|
|
179
|
+
"meta": { "type": "HTML_SCREEN", "fields": [
|
|
180
|
+
{"name": "status", "type": "String", "example": "confirmed", "mapping": "ActiveReservationView.status", "cardinality": "Single"}
|
|
181
|
+
] }
|
|
182
|
+
}]'
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Step 6 — Report back
|
|
137
186
|
|
|
138
187
|
Tell the user:
|
|
139
188
|
- The node ID that was created or updated
|
|
@@ -446,6 +446,34 @@ Response: `{ "hashes": { "<event-uuid>": "<hash>" } }`
|
|
|
446
446
|
|
|
447
447
|
---
|
|
448
448
|
|
|
449
|
+
## Step 7c — Verify the command has exactly one issuer
|
|
450
|
+
|
|
451
|
+
**A command is never issued by more than one thing.** Run this check whenever `elementType` is `SCREEN`, `AUTOMATION`, or `COMMAND` — placing any of these can trigger the server's fire-and-forget auto-connect (`learn-eventmodelers-api` §3), which wires `SCREEN→COMMAND` and `AUTOMATION→COMMAND` edges to type-compatible neighbors in the node's own column and the previous column.
|
|
452
|
+
|
|
453
|
+
**Why this can go wrong**: auto-connect only skips the previous column's SCREEN when the COMMAND's own column already has a SCREEN — it does not check for an AUTOMATION there. So a COMMAND whose own column holds an AUTOMATION, with a SCREEN sitting in the previous column, ends up wired from *both* — the automation (same column) and the screen (previous column) — and now looks issued by two things.
|
|
454
|
+
|
|
455
|
+
After placing, resolve the relevant COMMAND node (the one just placed, or the one in the same/adjacent column as the SCREEN/AUTOMATION just placed) and inspect its edges:
|
|
456
|
+
|
|
457
|
+
**Prefer MCP:**
|
|
458
|
+
```
|
|
459
|
+
mcp__eventmodelers__get_node { "boardId": "<BOARD_ID>", "nodeId": "<COMMAND_NODE_ID>" }
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
Count inbound edges where `target === COMMAND_NODE_ID` and the source node is type `SCREEN` or `AUTOMATION`.
|
|
463
|
+
|
|
464
|
+
- **0 or 1 such edge** → fine, nothing to do.
|
|
465
|
+
- **2 or more** → keep the edge whose source sits in the COMMAND's own column (the deliberate, same-slice issuer) and remove every other one:
|
|
466
|
+
|
|
467
|
+
```
|
|
468
|
+
mcp__eventmodelers__set_connection { "boardId": "<BOARD_ID>", "source": "<extra-issuer-node-id>", "target": "<COMMAND_NODE_ID>", "action": "remove" }
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
If it's not clear which edge is the deliberate one (e.g. neither source sits in the COMMAND's own column), do not guess — leave both edges and post a `QUESTION` comment on the COMMAND node via `handle-comment` instead, describing the ambiguity.
|
|
472
|
+
|
|
473
|
+
**Fallback (no MCP)**: there is no documented single-purpose REST endpoint for edge removal outside `/nodes/events`. Connect MCP via the `connect` skill first; if that's genuinely not possible, skip the auto-fix and post a `QUESTION` comment on the COMMAND node flagging the double issuer for manual resolution instead of fabricating a payload.
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
449
477
|
## Step 8 — Report back
|
|
450
478
|
|
|
451
479
|
Tell the user:
|
|
@@ -35,6 +35,9 @@ Before making any API calls, plan all N screens. For each screen, decide:
|
|
|
35
35
|
- `screenTitle` — human-readable name (e.g. "Enter Credentials")
|
|
36
36
|
- `pages` (default) — one or more complete HTML/CSS fragments for this screen (see "HTML page design" below), or `elements` — a minimal list of grid elements (see "Sketch path" below, aim for 5–8 elements) **only** when the sketch path applies for this storyboard
|
|
37
37
|
- `visualDescription` — a prose description of the screen's visual layout and content (2–4 sentences) that lets someone who cannot see the image understand what is shown: what UI sections appear, what text/labels are visible, where buttons and inputs are placed, and the overall purpose of the screen
|
|
38
|
+
- `fields` — one entry per piece of data this screen displays or captures, each with a `mapping` naming its source (see "Field data lineage" in Step 5b below). Plan this alongside the visuals, not as an afterthought — every displayed value needs a named source.
|
|
39
|
+
|
|
40
|
+
If several screens in this storyboard share the same visual base but each highlights a different part of it (e.g. one shared mockup, marked up differently per slice), scope each screen's `fields` to only the data inside *that* screen's highlighted area — not the full shared mockup. Different highlight, different (narrower) field list.
|
|
38
41
|
|
|
39
42
|
Then **create one task per screen** using TaskCreate, naming each task after the screen title. This gives you a visible queue of work. Create the screens directly after each task has been planned.
|
|
40
43
|
|
|
@@ -257,6 +260,44 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/image-nodes/$SCREEN_
|
|
|
257
260
|
|
|
258
261
|
Pass the already-computed `actorCellId` directly as `cellId` in either path. Expect success (MCP: `created: true`; curl: `204`). On failure, read the validation error, fix the payload, and retry once before reporting failure.
|
|
259
262
|
|
|
263
|
+
### Step 5b(ii) — Set field data lineage (mandatory)
|
|
264
|
+
|
|
265
|
+
Push the `fields` planned in Step 2 onto the node with a `node:changed` call. Every field needs a `mapping`:
|
|
266
|
+
|
|
267
|
+
| Field type | `mapping` | Example |
|
|
268
|
+
|---|---|---|
|
|
269
|
+
| User types a value, sent as a command | `"<CommandTitle>.<fieldName>"` | `"ReserveBike.bikeId"` |
|
|
270
|
+
| Displayed data, sourced from a read model | `"<ReadModelTitle>.<fieldName>"` | `"ActiveReservationView.status"` |
|
|
271
|
+
| Calculated/formatted only for display | `"derived:<expression>"` | `"derived:formatDuration(durationMinutes)"` |
|
|
272
|
+
|
|
273
|
+
Name the read model even if it doesn't exist as a board node yet — this skill only creates SCREEN/HTML_SCREEN nodes, never READMODEL nodes or connections. But naming the source is **not optional**: a screen displaying data should almost never have a field with no mapping. Set `cardinality` too (`"Single"` unless it's a repeated/list value).
|
|
274
|
+
|
|
275
|
+
**Prefer MCP:**
|
|
276
|
+
```
|
|
277
|
+
mcp__eventmodelers__submit_node_events {
|
|
278
|
+
"boardId": "<BOARD_ID>",
|
|
279
|
+
"events": [{
|
|
280
|
+
"id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<SCREEN_NODE_ID>",
|
|
281
|
+
"boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
|
|
282
|
+
"changedAttributes": ["meta.fields"],
|
|
283
|
+
"meta": { "type": "HTML_SCREEN", "fields": [ /* planned fields */ ] }
|
|
284
|
+
}]
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Fallback (no MCP):**
|
|
289
|
+
```bash
|
|
290
|
+
curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
|
|
291
|
+
-H "x-token: $TOKEN" -H "x-board-id: $BOARD_ID" -H "x-user-id: agent" \
|
|
292
|
+
-H "Content-Type: application/json" \
|
|
293
|
+
-d '[{
|
|
294
|
+
"id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<SCREEN_NODE_ID>",
|
|
295
|
+
"boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
|
|
296
|
+
"changedAttributes": ["meta.fields"],
|
|
297
|
+
"meta": { "type": "HTML_SCREEN", "fields": [ /* planned fields */ ] }
|
|
298
|
+
}]'
|
|
299
|
+
```
|
|
300
|
+
|
|
260
301
|
### Step 5c — Mark the task complete
|
|
261
302
|
|
|
262
303
|
After the node and sketch succeed, mark the task for this screen as completed using TaskUpdate.
|
|
@@ -129,7 +129,52 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/images/$NODE_ID/sket
|
|
|
129
129
|
|
|
130
130
|
Expect `204 No Content` on success.
|
|
131
131
|
|
|
132
|
-
## Step 5 —
|
|
132
|
+
## Step 5 — Define field data lineage (mandatory)
|
|
133
|
+
|
|
134
|
+
Every screen — new or updated — needs `meta.fields`: one entry per piece of data the screen displays or captures, each with a `mapping` naming where that data comes from. A screen with only a title and no fields is an empty placeholder from a data-lineage standpoint, even once the wireframe is rendered.
|
|
135
|
+
|
|
136
|
+
| Field type | `mapping` | Example |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| User types a value, sent as a command | `"<CommandTitle>.<fieldName>"` | `"ReserveBike.bikeId"` |
|
|
139
|
+
| Read from session | `"session:<fieldName>"` | `"session:customerId"` |
|
|
140
|
+
| Displayed data, sourced from a read model | `"<ReadModelTitle>.<fieldName>"` | `"ActiveReservationView.status"` |
|
|
141
|
+
| Calculated/formatted only for display | `"derived:<expression>"` | `"derived:formatDuration(durationMinutes)"` |
|
|
142
|
+
|
|
143
|
+
Name the read model even if it doesn't exist as a board node yet — this skill only renders the screen, it does not create READMODEL nodes or connections. But naming the source is **not optional**: a screen displaying data should almost never have a field with no mapping. If you can't say which read model a displayed field comes from, that's a sign the model is missing something — not a reason to skip the field.
|
|
144
|
+
|
|
145
|
+
Set `cardinality` too (`"Single"` unless the field is a repeated/list value), then push the fields onto the node:
|
|
146
|
+
|
|
147
|
+
**Prefer MCP:**
|
|
148
|
+
```
|
|
149
|
+
mcp__eventmodelers__submit_node_events {
|
|
150
|
+
"boardId": "<BOARD_ID>",
|
|
151
|
+
"events": [{
|
|
152
|
+
"id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<NODE_ID>",
|
|
153
|
+
"boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
|
|
154
|
+
"changedAttributes": ["meta.fields"],
|
|
155
|
+
"meta": { "type": "SCREEN", "fields": [
|
|
156
|
+
{"name": "status", "type": "String", "example": "confirmed", "mapping": "ActiveReservationView.status", "cardinality": "Single"}
|
|
157
|
+
] }
|
|
158
|
+
}]
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Fallback (no MCP):**
|
|
163
|
+
```bash
|
|
164
|
+
curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
|
|
165
|
+
-H "x-token: $TOKEN" -H "x-board-id: $BOARD_ID" -H "x-user-id: agent" \
|
|
166
|
+
-H "Content-Type: application/json" \
|
|
167
|
+
-d '[{
|
|
168
|
+
"id": "<event-uuid>", "eventType": "node:changed", "nodeId": "<NODE_ID>",
|
|
169
|
+
"boardId": "<BOARD_ID>", "timestamp": <NOW_MS>,
|
|
170
|
+
"changedAttributes": ["meta.fields"],
|
|
171
|
+
"meta": { "type": "SCREEN", "fields": [
|
|
172
|
+
{"name": "status", "type": "String", "example": "confirmed", "mapping": "ActiveReservationView.status", "cardinality": "Single"}
|
|
173
|
+
] }
|
|
174
|
+
}]'
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Step 6 — Report back
|
|
133
178
|
|
|
134
179
|
Tell the user:
|
|
135
180
|
- The node ID that was updated
|