@milehimikey/em 1.17.0 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/em-implementer.md +7 -4
- package/.claude/agents/em-validator.md +2 -2
- package/.claude/skills/event-modeling/SKILL.md +1 -1
- package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
- package/.claude/skills/event-modeling-design/SKILL.md +46 -5
- package/.claude/skills/event-modeling-discover/SKILL.md +43 -8
- package/.claude/skills/event-modeling-discover/reference/extract.md +6 -1
- package/.claude/skills/event-modeling-discover/reference/prd-rubric.md +102 -0
- package/.claude/skills/event-modeling-discover/reference/prd-spec-path.md +171 -0
- package/.claude/skills/event-modeling-engagement/SKILL.md +21 -9
- package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/reference/implement.md +18 -0
- package/.claude/skills/event-modeling-review/SKILL.md +4 -2
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +94 -6
- package/.claude/skills/event-modeling-shared/reference/methodology.md +268 -0
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +96 -2
- package/.claude/skills/event-modeling-shared/templates/slice.md +11 -3
- package/.claude-plugin/marketplace.json +2 -2
- package/dist/catalog/docModelConsistencyValidate.js +160 -0
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
- package/dist/catalog/invariantsValidate.d.ts +2 -0
- package/dist/catalog/invariantsValidate.js +67 -1
- package/dist/catalog/invariantsValidate.js.map +1 -1
- package/dist/catalog/scenarioQualityValidate.d.ts +4 -0
- package/dist/catalog/scenarioQualityValidate.js +148 -0
- package/dist/catalog/scenarioQualityValidate.js.map +1 -0
- package/dist/catalog/scenarios.d.ts +5 -0
- package/dist/catalog/scenarios.js +80 -0
- package/dist/catalog/scenarios.js.map +1 -1
- package/dist/catalog/sliceDoc.d.ts +4 -0
- package/dist/catalog/sliceDoc.js +16 -7
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +11 -2
- package/dist/catalog/sliceReadyValidate.js +62 -3
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/catalog/sliceSections.d.ts +3 -2
- package/dist/catalog/sliceSections.js +99 -9
- package/dist/catalog/sliceSections.js.map +1 -1
- package/dist/cli/ciInit.d.ts +4 -11
- package/dist/cli/ciInit.js +24 -6
- package/dist/cli/ciInit.js.map +1 -1
- package/dist/cli/coverage.d.ts +9 -0
- package/dist/cli/coverage.js +18 -0
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/defer.d.ts +28 -0
- package/dist/cli/defer.js +87 -4
- package/dist/cli/defer.js.map +1 -1
- package/dist/cli/engagement.d.ts +13 -2
- package/dist/cli/engagement.js +60 -5
- package/dist/cli/engagement.js.map +1 -1
- package/dist/cli/engagementFile.d.ts +2 -0
- package/dist/cli/engagementFile.js +3 -1
- package/dist/cli/engagementFile.js.map +1 -1
- package/dist/cli/markImplemented.d.ts +14 -10
- package/dist/cli/markImplemented.js +49 -1
- package/dist/cli/markImplemented.js.map +1 -1
- package/dist/cli/prdTriage.d.ts +109 -0
- package/dist/cli/prdTriage.js +393 -0
- package/dist/cli/prdTriage.js.map +1 -0
- package/dist/cli/render-inputs.d.ts +2 -0
- package/dist/cli/render-inputs.js +4 -0
- package/dist/cli/render-inputs.js.map +1 -1
- package/dist/cli/review.d.ts +17 -2
- package/dist/cli/review.js +39 -10
- package/dist/cli/review.js.map +1 -1
- package/dist/cli/revise.d.ts +2 -0
- package/dist/cli/revise.js +7 -2
- package/dist/cli/revise.js.map +1 -1
- package/dist/cli/shape.d.ts +27 -0
- package/dist/cli/shape.js +77 -0
- package/dist/cli/shape.js.map +1 -0
- package/dist/cli/sliceIndex.d.ts +6 -1
- package/dist/cli/sliceIndex.js +28 -13
- package/dist/cli/sliceIndex.js.map +1 -1
- package/dist/cli/sliceNew.d.ts +4 -1
- package/dist/cli/sliceNew.js +26 -9
- package/dist/cli/sliceNew.js.map +1 -1
- package/dist/cli/status.d.ts +21 -1
- package/dist/cli/status.js +25 -3
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/storylineSuggest.d.ts +26 -0
- package/dist/cli/storylineSuggest.js +252 -0
- package/dist/cli/storylineSuggest.js.map +1 -0
- package/dist/cli/upgrade.d.ts +15 -3
- package/dist/cli/upgrade.js +516 -7
- package/dist/cli/upgrade.js.map +1 -1
- package/dist/cli/upgradeReshape.d.ts +60 -0
- package/dist/cli/upgradeReshape.js +425 -0
- package/dist/cli/upgradeReshape.js.map +1 -0
- package/dist/cli.js +311 -35
- package/dist/cli.js.map +1 -1
- package/dist/emit/dot.d.ts +2 -0
- package/dist/emit/dot.js +44 -8
- package/dist/emit/dot.js.map +1 -1
- package/dist/emit/engagementJson.d.ts +4 -3
- package/dist/emit/engagementJson.js +11 -4
- package/dist/emit/engagementJson.js.map +1 -1
- package/dist/emit/json.d.ts +21 -1
- package/dist/emit/json.js +26 -2
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/queryJson.d.ts +1 -1
- package/dist/emit/queryJson.js +3 -1
- package/dist/emit/queryJson.js.map +1 -1
- package/dist/emit/statusJson.d.ts +1 -1
- package/dist/emit/statusJson.js +6 -1
- package/dist/emit/statusJson.js.map +1 -1
- package/dist/emit/theme.d.ts +1 -0
- package/dist/emit/theme.js +13 -0
- package/dist/emit/theme.js.map +1 -1
- package/dist/emit/validateJson.d.ts +9 -5
- package/dist/emit/validateJson.js +40 -5
- package/dist/emit/validateJson.js.map +1 -1
- package/dist/layout/grid.d.ts +5 -1
- package/dist/layout/grid.js +93 -2
- package/dist/layout/grid.js.map +1 -1
- package/dist/mcp/server.js +46 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/edges.d.ts +7 -0
- package/dist/model/edges.js +33 -16
- package/dist/model/edges.js.map +1 -1
- package/dist/model/model.d.ts +33 -0
- package/dist/model/model.js +87 -3
- package/dist/model/model.js.map +1 -1
- package/dist/model/queryIndex.js +2 -2
- package/dist/model/queryIndex.js.map +1 -1
- package/dist/model/refs.js +8 -5
- package/dist/model/refs.js.map +1 -1
- package/dist/model/rules.d.ts +149 -0
- package/dist/model/rules.js +144 -0
- package/dist/model/rules.js.map +1 -1
- package/dist/model/shapeLint.d.ts +12 -0
- package/dist/model/shapeLint.js +170 -0
- package/dist/model/shapeLint.js.map +1 -0
- package/dist/model/storylines.d.ts +51 -0
- package/dist/model/storylines.js +188 -0
- package/dist/model/storylines.js.map +1 -0
- package/dist/model/validate.d.ts +6 -1
- package/dist/model/validate.js +317 -14
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/againSugar.d.ts +30 -0
- package/dist/parser/againSugar.js +105 -0
- package/dist/parser/againSugar.js.map +1 -0
- package/dist/parser/ast.d.ts +33 -0
- package/dist/parser/lexer.d.ts +2 -0
- package/dist/parser/lexer.js +4 -0
- package/dist/parser/lexer.js.map +1 -1
- package/dist/parser/parser.js +284 -4
- package/dist/parser/parser.js.map +1 -1
- package/dist/query/format.js +1 -0
- package/dist/query/format.js.map +1 -1
- package/dist/query/verbs.d.ts +4 -0
- package/dist/query/verbs.js +30 -3
- package/dist/query/verbs.js.map +1 -1
- package/dist/render/drawEdges.js +100 -4
- package/dist/render/drawEdges.js.map +1 -1
- package/dist/render/render.js +20 -2
- package/dist/render/render.js.map +1 -1
- package/dist/render/sliceDiagram.js +5 -5
- package/dist/render/sliceDiagram.js.map +1 -1
- package/dist/render/storylineBands.d.ts +11 -0
- package/dist/render/storylineBands.js +80 -0
- package/dist/render/storylineBands.js.map +1 -0
- package/dist/render/storylineDiagram.d.ts +14 -0
- package/dist/render/storylineDiagram.js +142 -0
- package/dist/render/storylineDiagram.js.map +1 -0
- package/dist/roster/check.d.ts +32 -0
- package/dist/roster/check.js +92 -0
- package/dist/roster/check.js.map +1 -0
- package/dist/roster/context.d.ts +3 -0
- package/dist/roster/context.js +23 -0
- package/dist/roster/context.js.map +1 -0
- package/dist/roster/init.d.ts +15 -0
- package/dist/roster/init.js +52 -0
- package/dist/roster/init.js.map +1 -0
- package/dist/roster/roster.d.ts +79 -0
- package/dist/roster/roster.js +298 -0
- package/dist/roster/roster.js.map +1 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/agents/em-implementer.md +7 -4
- package/plugin/agents/em-validator.md +2 -2
- package/plugin/shared/reference/em-dsl.md +94 -6
- package/plugin/shared/reference/methodology.md +268 -0
- package/plugin/shared/reference/slice-doc-schema.md +96 -2
- package/plugin/shared/templates/slice.md +11 -3
- package/plugin/skills/conform/SKILL.md +1 -1
- package/plugin/skills/design/SKILL.md +46 -5
- package/plugin/skills/discover/SKILL.md +43 -8
- package/plugin/skills/discover/reference/extract.md +6 -1
- package/plugin/skills/discover/reference/prd-rubric.md +103 -0
- package/plugin/skills/discover/reference/prd-spec-path.md +172 -0
- package/plugin/skills/engagement/SKILL.md +21 -9
- package/plugin/skills/event-modeling/SKILL.md +1 -1
- package/plugin/skills/implement/SKILL.md +1 -1
- package/plugin/skills/implement/reference/implement.md +18 -0
- package/plugin/skills/review/SKILL.md +4 -2
|
@@ -144,6 +144,17 @@ command** — it never records an event directly.
|
|
|
144
144
|
duplicated name.
|
|
145
145
|
- Socratic prompts: *"What should happen without a human? What condition triggers it? What work
|
|
146
146
|
list does the processor watch? What command does it fire? What if it fails or retries?"*
|
|
147
|
+
- **Outcomes are events of the SAME slice.** Success, failure, retry-scheduled and timeout are
|
|
148
|
+
outcome events of the one command (`Fulfilment Succeeded` / `Fulfilment Failed` / `Fulfilment
|
|
149
|
+
Retry Scheduled`), not sibling slices per control-flow branch (1.18.0 adds the
|
|
150
|
+
lint `automation-split-by-outcome`). Retry policy and failure handling are invariants and scenarios
|
|
151
|
+
in that slice's doc. A new slice only when a different actor or a different trigger is
|
|
152
|
+
involved. Canon: Dilger, "one business action… two different possible outcomes"
|
|
153
|
+
(`PaymentIssued` / `PaymentFailed`); a failure that continues the story is "Linearized" as a
|
|
154
|
+
later chapter linked to the originating event, never a branch; it reads the failure event
|
|
155
|
+
through a view or a reaction (Law 1 forbids event-to-event arrows). A retry is the processor's
|
|
156
|
+
to-do view reading the retry-scheduled event (`view X again`), so the same reaction re-fires. Same rule for State Change: one
|
|
157
|
+
command, several outcome events, one slice.
|
|
147
158
|
|
|
148
159
|
### 4. Translation
|
|
149
160
|
**Boundary crossing → command → event, the reaction and what it triggers together in one slice.**
|
|
@@ -193,6 +204,260 @@ The `em` shape only tracks the second question — trigger source doesn't change
|
|
|
193
204
|
|
|
194
205
|
---
|
|
195
206
|
|
|
207
|
+
## The story, not the schema
|
|
208
|
+
|
|
209
|
+
A model is a story of what success looks like, not a data model (Dymitruk, *What is Event Modeling*: a
|
|
210
|
+
story "on a time line and with no branching"; commands capture intent, "as opposed to simply saving form
|
|
211
|
+
data to a table"; "all information has to have an origin and a destination"). Models drafted from a spec
|
|
212
|
+
drift into **CRUD laid on a timeline**; these rules stop that. Quoted lines are the canon; rulings marked
|
|
213
|
+
*(em ruling)* are em's own — neither Dymitruk nor Dilger states a child-entity rule or bans "Update" or
|
|
214
|
+
"Set" by name. Full write-up with the canon quotes: `docs/storyline.md`.
|
|
215
|
+
|
|
216
|
+
- **One journey per process, start to finish.** The timeline holds one business journey per process,
|
|
217
|
+
ending in an outcome. The **main storyline** is the end-to-end happy path; retire, extend and
|
|
218
|
+
reorganize are **secondary storylines** (chapters; Dilger: "one linear flow from left to right. No
|
|
219
|
+
exceptions… Linearize it!", "Model one process at a time, each with its own clear ending"). Never one
|
|
220
|
+
lifecycle block per entity. Name the main and every secondary storyline; a `storyline "Name" { … }`
|
|
221
|
+
block groups the slices (`dsl.md`, Storylines).
|
|
222
|
+
- **Build from walkthroughs first.** The story already lives in the spec's walkthroughs, use cases and
|
|
223
|
+
user journeys. Build the timeline from those; the resource/API reference (OpenAPI, endpoint tables,
|
|
224
|
+
migrations) fills only the operations no walkthrough touches, and is read last. Headless: a persona
|
|
225
|
+
per API caller, and that caller's journey is the story.
|
|
226
|
+
- **The CRUD shape.** N entities × (create, update, archive), one trailing view per entity, no read
|
|
227
|
+
across contexts. Recognise it in your own output. Both models below validate; only the second is a
|
|
228
|
+
story:
|
|
229
|
+
```em
|
|
230
|
+
model "Catalog (CRUD shape)"
|
|
231
|
+
|
|
232
|
+
persona Merchandiser
|
|
233
|
+
|
|
234
|
+
context Product
|
|
235
|
+
|
|
236
|
+
slice "Create Product" {
|
|
237
|
+
ui Product Form @Merchandiser
|
|
238
|
+
command Create Product
|
|
239
|
+
event Product Created @Product
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
slice "Update Product" {
|
|
243
|
+
ui Product Form @Merchandiser
|
|
244
|
+
command Update Product
|
|
245
|
+
event Product Updated @Product
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
slice "Archive Product" {
|
|
249
|
+
ui Product Form @Merchandiser
|
|
250
|
+
command Archive Product
|
|
251
|
+
event Product Archived @Product
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
slice "Products" {
|
|
255
|
+
view Products from "Product Created", "Product Updated", "Product Archived"
|
|
256
|
+
ui Product List @Merchandiser
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
```em
|
|
260
|
+
model "Catalog (storyline shape)"
|
|
261
|
+
|
|
262
|
+
persona Merchandiser
|
|
263
|
+
|
|
264
|
+
context Product
|
|
265
|
+
|
|
266
|
+
storyline "Bring a product to market" {
|
|
267
|
+
slice "Create Product" {
|
|
268
|
+
ui Product Form @Merchandiser
|
|
269
|
+
command Create Product
|
|
270
|
+
event Product Created @Product
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
slice "Catalog" {
|
|
274
|
+
view Catalog from "Product Created"
|
|
275
|
+
ui Browse Catalog @Merchandiser
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
slice "Set Price" {
|
|
279
|
+
ui Pricing Panel @Merchandiser
|
|
280
|
+
command Set Price
|
|
281
|
+
event Price Set @Product
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
slice "Catalog with Price" {
|
|
285
|
+
view Catalog again from "Price Set"
|
|
286
|
+
ui Browse Catalog @Merchandiser
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
storyline "Retire a product" after "Bring a product to market" {
|
|
291
|
+
slice "Retire Product" {
|
|
292
|
+
ui Pricing Panel @Merchandiser
|
|
293
|
+
command Retire Product
|
|
294
|
+
event Product Retired @Product
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
slice "Catalog without Product" {
|
|
298
|
+
view Catalog again from "Product Retired"
|
|
299
|
+
ui Browse Catalog @Merchandiser
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
The `timeline-entity-clustered` lint is the mechanical half (it fires only once two or more
|
|
304
|
+
contexts are written; this one-entity pair stays silent); once any storyline exists,
|
|
305
|
+
`slice-outside-storyline` flags slices left outside one.
|
|
306
|
+
- **Commands name intent, not endpoints** (Dilger: "Name by intent", "Every command has a reason"). One
|
|
307
|
+
endpoint may carry several intents. Signal of two intents in one command: invariants or field rules
|
|
308
|
+
that apply only "if `<field>` is set" — split it, and both commands may record the same event
|
|
309
|
+
(`CreateProduct` / `CreateProductVariant`, same `POST /products`, same `ProductCreated`). **Intent
|
|
310
|
+
test:** if a storyline step reads awkwardly with the command's name, the command is named after the
|
|
311
|
+
endpoint. `command-second-intent` and `command-repeated-across-slices` are the mechanical half.
|
|
312
|
+
- **A read model is one information need** (Dilger: "Every Read Model answers a question. No question,
|
|
313
|
+
no Read Model."). **Question test:** write the question each view answers; add a view only when no
|
|
314
|
+
existing one answers it. A nested route hints the child belongs in the parent's view; the real test
|
|
315
|
+
is whether the query needs information the parent view lacks. Child with references only → the parent
|
|
316
|
+
view carries ids; child with its own data → embedded in the parent view; child queried without the
|
|
317
|
+
parent id → its own view. Aggregate rules decide write-side consistency, never read models. *(em
|
|
318
|
+
ruling)*
|
|
319
|
+
- **Write side: children belong in the parent's context** when the child's invariants are checks against
|
|
320
|
+
the parent. A sub-resource schema shared across parent types is one event per parent on the parent's
|
|
321
|
+
context, not a shared aggregate — check the spec's event reference, not its resource section. Where
|
|
322
|
+
two slices record the same fact, repeat the event bare (same name, same context). *(em ruling)*
|
|
323
|
+
- **Repeat the view after every command that changes it** (`again`, span-1 — see State View above), so
|
|
324
|
+
the reader sees the information arrive rather than a view at the end.
|
|
325
|
+
- **Outcomes are events of one slice** (Dilger: "One decision. One event. One state change."; one
|
|
326
|
+
action with two outcomes is the legitimate exception): alternative events on one context in one slice
|
|
327
|
+
(`event A | B @Ctx`, GH #278), failure as a scenario first, a chapter only when core (it changes what the user does next, or has
|
|
328
|
+
its own actor/SLA). Stated once for
|
|
329
|
+
automations in the Automation pattern (`docs/patterns.md#automation`); `automation-split-by-outcome`
|
|
330
|
+
and `retry-slice-of` are the mechanical half. Two forms, alternatives always on one context: inline `event A | B @Ctx`
|
|
331
|
+
(shared clauses and fields) and `oneof { … }` (each alternative keeps its own fields and clauses):
|
|
332
|
+
```em
|
|
333
|
+
model "Payments"
|
|
334
|
+
|
|
335
|
+
persona Customer
|
|
336
|
+
|
|
337
|
+
context Payment
|
|
338
|
+
|
|
339
|
+
slice "Settle Payment" {
|
|
340
|
+
ui Pay Screen @Customer
|
|
341
|
+
command Settle Payment
|
|
342
|
+
event Payment Issued | Payment Failed @Payment { paymentId: UUID }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
slice "Payment Status" {
|
|
346
|
+
view Payment Status from "Payment Issued", "Payment Failed"
|
|
347
|
+
ui Payment Result @Customer
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
slice "Refund Payment" {
|
|
351
|
+
ui Refund Screen @Customer
|
|
352
|
+
command Refund Payment
|
|
353
|
+
oneof {
|
|
354
|
+
event Refund Issued @Payment { paymentId: UUID, amount: Decimal }
|
|
355
|
+
event Refund Declined @Payment { paymentId: UUID, reason: String }
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
slice "Refund Status" {
|
|
360
|
+
view Refund Status from "Refund Issued", "Refund Declined"
|
|
361
|
+
ui Refund Result @Customer
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
- **One event, two commands.** When two commands record the same fact, repeat the event bare
|
|
365
|
+
(`event X @Ctx`, same name and context) in the second slice: it inherits the first declaration, no
|
|
366
|
+
marker needed. Declaring different fields is `shared-event-field-mismatch`. (The arrow set for a
|
|
367
|
+
shared event read by a repeated view is under review: GH #328.)
|
|
368
|
+
```em
|
|
369
|
+
model "Ordering"
|
|
370
|
+
|
|
371
|
+
persona Customer
|
|
372
|
+
|
|
373
|
+
context Sales
|
|
374
|
+
|
|
375
|
+
slice "Place Order" {
|
|
376
|
+
ui Checkout @Customer
|
|
377
|
+
command Place Order
|
|
378
|
+
event Order Placed @Sales { orderId: UUID }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
slice "Order Status" {
|
|
382
|
+
view Order Status from "Order Placed"
|
|
383
|
+
ui Track Order @Customer
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
slice "Reorder" {
|
|
387
|
+
ui Reorder Button @Customer
|
|
388
|
+
command Reorder
|
|
389
|
+
event Order Placed @Sales
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
slice "Order Status after Reorder" {
|
|
393
|
+
view Order Status again from "Order Placed"
|
|
394
|
+
ui Track Order @Customer
|
|
395
|
+
}
|
|
396
|
+
```
|
|
397
|
+
Combined with alternatives: when one alternative is a shared repeat, use `oneof` and write that
|
|
398
|
+
alternative bare so it inherits its fields (an inline line's fields would be copied onto every
|
|
399
|
+
alternative and mismatch):
|
|
400
|
+
```em
|
|
401
|
+
model "Payments"
|
|
402
|
+
|
|
403
|
+
persona Customer
|
|
404
|
+
|
|
405
|
+
context Payment
|
|
406
|
+
|
|
407
|
+
slice "Settle Payment" {
|
|
408
|
+
ui Pay Screen @Customer
|
|
409
|
+
command Settle Payment
|
|
410
|
+
oneof {
|
|
411
|
+
event Payment Issued @Payment { paymentId: UUID }
|
|
412
|
+
event Payment Failed @Payment { paymentId: UUID, reason: String }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
slice "Payment Status" {
|
|
417
|
+
view Payment Status from "Payment Issued", "Payment Failed"
|
|
418
|
+
ui Payment Result @Customer
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
slice "Retry Payment" {
|
|
422
|
+
ui Retry Screen @Customer
|
|
423
|
+
command Retry Payment
|
|
424
|
+
oneof {
|
|
425
|
+
event Payment Issued @Payment
|
|
426
|
+
event Payment Retry Declined @Payment { paymentId: UUID }
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
slice "Payment Status after Retry" {
|
|
431
|
+
view Payment Status again from "Payment Issued", "Payment Retry Declined"
|
|
432
|
+
ui Payment Result @Customer
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
- **Reference-only PRD groups sit outside storylines.** A group that only carries reference material
|
|
436
|
+
(#327) is not a journey; leave it out of every `storyline` block.
|
|
437
|
+
- **Business terms in the events.** "If your Event Model doesn't reflect terms used by business experts —
|
|
438
|
+
especially in the recorded Events — that's a red flag." (Dilger, *Seven insights*.)
|
|
439
|
+
|
|
440
|
+
### Reshaping an existing model
|
|
441
|
+
|
|
442
|
+
The upgrade path for a CRUD-shaped model. After each step run `em validate`.
|
|
443
|
+
|
|
444
|
+
`em storyline suggest <model>.em` proposes the `storyline` blocks as a diff (advisory, never
|
|
445
|
+
writes); `em upgrade` offers `collapse-alternative-events` and the opt-in `fold-continuations`.
|
|
446
|
+
|
|
447
|
+
1. Name the processes (list each spec's walkthroughs or journeys, not endpoints): the main storyline and each secondary one.
|
|
448
|
+
2. Reorder the slices into those stories and wrap each in a `storyline` block (`after "…"` where one
|
|
449
|
+
follows another); a slice that fits no story is a deletion candidate.
|
|
450
|
+
3. Intent test per command: rename to the business intent; split any command with "if X is set" rules.
|
|
451
|
+
4. Apply the information-need test to each read model and fold children into parents: merge views
|
|
452
|
+
answering the same question; add one only for an unanswered one.
|
|
453
|
+
5. Move child-entity events onto the parent's context when the child's invariants check the parent.
|
|
454
|
+
6. Repeat each view (`again`, span-1) after every command that changes it; drop the lone trailing view.
|
|
455
|
+
7. Fold success/failure slice pairs into one slice with alternative events.
|
|
456
|
+
8. Tooling, last: `em validate` (and `em validate --shape`, the 1.18.0 design exit gate), `em slice sync
|
|
457
|
+
--check` then `em slice sync`, `em slice index`, and regenerate TypeSpec/contracts last.
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
196
461
|
## The 7 steps
|
|
197
462
|
|
|
198
463
|
Run them in order. The skill groups them into phases: **discover = 1–4**, **model = 5–7**,
|
|
@@ -256,6 +521,9 @@ dedicated `slice` phase writes the full rich spec (see `../templates/slice.md`).
|
|
|
256
521
|
- Prompts: *"Given what starting state, when this command/trigger fires, then what event(s)
|
|
257
522
|
result? What must always be true? What are the failure paths? Which fields are essential and
|
|
258
523
|
what are their rules?"*
|
|
524
|
+
- **Failure, retry, timeout surface here?** Add the outcome events and the scenarios to the
|
|
525
|
+
SAME slice, never a new slice per branch (Dymitruk: "a few of these stories to show how a
|
|
526
|
+
command can and cannot succeed" — Given/When/Then scenarios first).
|
|
259
527
|
- **Existing codebase:** before finalizing field names/types, check adjacent real sources
|
|
260
528
|
(OpenAPI specs, DB migrations, existing DTOs/event classes in sibling contexts) rather than
|
|
261
529
|
guessing — mirrors `extract.md`'s whole-system grounding, applied per-slice.
|
|
@@ -88,6 +88,68 @@ over an omission — it reports). `implementedIn` and the lineage keys are the g
|
|
|
88
88
|
optional ones; `implementedIn`'s required-once-implemented rule in the table below is
|
|
89
89
|
convention, not parser-enforced.
|
|
90
90
|
|
|
91
|
+
## Who may ratify
|
|
92
|
+
|
|
93
|
+
The schema records *who* signed (`reviewedBy`, `ratifiedBy`) but, by default, accepts any
|
|
94
|
+
non-empty name: who is allowed to sign is the host repo's decision. A host that wants em to
|
|
95
|
+
enforce it supplies an optional **roster**, `ratifiers.yaml`, beside the model. See
|
|
96
|
+
[examples/order-fulfillment/ratifiers.yaml](https://github.com/milehimikey/em/blob/main/examples/order-fulfillment/ratifiers.yaml).
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
rosterSchemaVersion: "1.0"
|
|
100
|
+
ratifiers:
|
|
101
|
+
- name: Aaron McKay
|
|
102
|
+
contexts: [Product, Attribute] # omit = the whole model
|
|
103
|
+
- name: Mike Key
|
|
104
|
+
- name: Dana Lee
|
|
105
|
+
models: [pim] # omit = every model the roster governs
|
|
106
|
+
reviewers: # optional; same shape, checked by `em slice review --by`
|
|
107
|
+
- name: Mike Key
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- **Schema.** `rosterSchemaVersion` is required (`"1.0"`); at least one of `ratifiers:` /
|
|
111
|
+
`reviewers:` is required; each is a non-empty list of `{ name, contexts?, models? }`. Unknown
|
|
112
|
+
keys are errors (`roster-invalid`). A missing `ratifiers:` / `reviewers:` key leaves that role
|
|
113
|
+
unrestricted.
|
|
114
|
+
- **Placement.** The nearest `ratifiers.yaml` walking up from the model's directory, stopping at
|
|
115
|
+
the nearest directory holding `.git` (or the filesystem root). If no `.git` exists anywhere
|
|
116
|
+
above the model, only the model's own directory is searched. `--roster <path>` on `ratify`,
|
|
117
|
+
`review`, `validate --slice-ready`, `roster check` overrides discovery; a path that does not
|
|
118
|
+
exist is an error, never "no roster".
|
|
119
|
+
- **A slice's contexts** are the contexts of its **events** plus the contexts of the events its
|
|
120
|
+
**views read** - each view instance's **own** `from`, never a union across the read model's
|
|
121
|
+
instances (adding a later `again` instance never changes an earlier slice's contexts). A
|
|
122
|
+
continuation slice also takes its originating instance's own `from`. A slice with neither has no
|
|
123
|
+
context and is permitted only by entries without `contexts`.
|
|
124
|
+
A continuation fed from a different context than its originating view (e.g. `tool-shows-on-loan` =
|
|
125
|
+
Loan + Tool) therefore needs a ratifier permitted for every one of those contexts, so a team with
|
|
126
|
+
one lead per context needs a joint or whole-model roster entry for such slices.
|
|
127
|
+
- **An entry permits a slice** when it applies to the model and either has no `contexts` or lists
|
|
128
|
+
**every** context of the slice: a cross-context slice needs a ratifier who may sign for all of
|
|
129
|
+
them (a leading `@` is tolerated in `contexts`).
|
|
130
|
+
- **Per-model scoping.** `models: [<modelKey>]` (the key `em export` reports as `model.key`) limits
|
|
131
|
+
an entry to those models; an entry without it applies to every model the roster governs. Context
|
|
132
|
+
names are matched **per model** against that model's declared contexts, so a shared root roster
|
|
133
|
+
needs `models:` whenever the models' ratifiers differ. A `models:` key the enclosing
|
|
134
|
+
`system.yaml` does not list is `roster-invalid`.
|
|
135
|
+
- **Validation against the model.** A `contexts:` entry the model does not declare (case-sensitive)
|
|
136
|
+
is `roster-invalid`, naming the context; only entries that apply to the model are checked.
|
|
137
|
+
- **File absent: today's behaviour**, no warning. **File present but invalid: fail closed** -
|
|
138
|
+
`ratify` and `review` refuse, `--slice-ready` and `roster check` report `roster-invalid`.
|
|
139
|
+
- **`grandfathered (unsigned before em 1.14)`** is em's own migration marker (`em upgrade`'s
|
|
140
|
+
`ratified-signoff`), not a signer: exempt from the `--slice-ready` check and from `roster check`
|
|
141
|
+
(which counts it) until the slice is re-ratified - but only the **exact** string, and only on a
|
|
142
|
+
doc with no `ratifiedHash` (the state `em upgrade` leaves). `grandfathered by X`, or the marker on
|
|
143
|
+
a doc that carries a hash, is a signer and is checked.
|
|
144
|
+
- **Where it is enforced.** At write time by `em slice ratify` / `em slice review` (the refusal
|
|
145
|
+
lists the permitted names); at check time by `em validate --slice-ready <key>`
|
|
146
|
+
(`ratified-by-not-in-roster`, error - one `ready-to-implement` slice, ratifier only) and by
|
|
147
|
+
`em roster check <model>` (every doc, `ratifiedBy` on `ready-to-implement` and `implemented`,
|
|
148
|
+
`reviewedBy` on any), the command `em ci init` wires into CI when a roster exists - see
|
|
149
|
+
[ci.md](ci.md#ratifier-roster-gate). `em status` reports `roster: present|absent|invalid`.
|
|
150
|
+
- `em roster init <model>` seeds the file from the names already in the docs
|
|
151
|
+
([cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-roster-init-file)).
|
|
152
|
+
|
|
91
153
|
## Required vs optional, by `status`
|
|
92
154
|
|
|
93
155
|
| Key | draft | reviewed | ready-to-implement | implemented |
|
|
@@ -261,6 +323,21 @@ the union of events every later `again` instance of that view reads via `from`,
|
|
|
261
323
|
order — so implementing the read model from one export never means walking every `again`
|
|
262
324
|
slice's doc by hand to assemble the full event list.
|
|
263
325
|
|
|
326
|
+
The generated `em-slice-view` region follows the same rule (GH #280, 1.18.0): its `built from
|
|
327
|
+
events:` line lists the union of `from` across the originating instance and every `again`
|
|
328
|
+
instance (originating first, then timeline order, deduped), and each field's `from event` note
|
|
329
|
+
resolves against that union (each later event annotated `(via "<slice>")` with its instance's slice) — whichever instance's slice the doc is bound to. `em slice sync`
|
|
330
|
+
rewrites existing view regions accordingly.
|
|
331
|
+
|
|
332
|
+
**Synthesized continuations (GH #287).** A `view X again` line written inside a command's own slice
|
|
333
|
+
is lifted into a continuation column right after it. That column is a continuation like any other
|
|
334
|
+
(`continuationOf` names the view's originating slice, `alsoReads` counts its events) but exists
|
|
335
|
+
only as sugar over the host slice's text: its export key is `<host-slice-key>--again` (one column per host slice, its lifted lines stacked),
|
|
336
|
+
it never has a doc, and `em slice index` lists no row for it. A slice key never contains `/` (the slug generator emits single dashes only, so `--` cannot occur in a hand-written key); the `/` in an element ref `<slice-key>/<kind>.<slug>` is always the first and only one. A hand-written continuation slice
|
|
337
|
+
may also hold several `again` views, and may omit its name (auto-named `"<View> View: <Event>"`).
|
|
338
|
+
When one slice stacks views of different read models, `continuationOf` names the originating
|
|
339
|
+
slice of the FIRST view; the others are listed in their originating slices' `alsoReads` (with this slice's key as `atSlice`).
|
|
340
|
+
|
|
264
341
|
**Authoring convention:** a continuation's behavior belongs in the originating doc's own
|
|
265
342
|
`## Scenarios (Given / When / Then)` section — one scenario per event a later `again` instance
|
|
266
343
|
feeds — rather than in a doc of its own; no new frontmatter is needed to record it.
|
|
@@ -397,7 +474,7 @@ re-run `em slice sync`.
|
|
|
397
474
|
|---|---|---|
|
|
398
475
|
| `em-slice-command` | `## Command / Input` | `**Command:** \`Name\`` + `\| Field \| Type \| Required \| Rules / Validation \|` (Required is `no` for a `name?: Type` field) |
|
|
399
476
|
| `em-slice-event` | `## Event(s) Emitted` | `**Event:** \`Name\` → context \`Ctx\`` + `\| Field \| Type \| Immutable Fact? \| Source / Notes \|` (Source: the same-slice command field it is copied from, `assigned` for a system-assigned field, else `{{ }}`) |
|
|
400
|
-
| `em-slice-view` | `## Read Model / View` | `- **View:** \`Name\` built from events: …` + `\| Field \| Type \| Source / Notes \|` (Source: the source event carrying the field, `Derived` for a `derived` field, else `{{ }}`) |
|
|
477
|
+
| `em-slice-view` | `## Read Model / View` | `- **View:** \`Name\` built from events: …` (the union across the originating and every `again` instance; each later event is annotated `(via "<slice>")` with the slice of the instance that adds it) + `\| Field \| Type \| Source / Notes \|` (Source: the source event, from that union, carrying the field, `Derived` for a `derived` field, `Collection of \`T\`, fed by …` for a `T[] derived from …` field — followed by one neutral Given/Then scenario stub per named event under the table, to copy into `## Scenarios` and complete there (the region itself stays read-only) — else `{{ }}`) |
|
|
401
478
|
| `em-slice-invariants` | `## Invariants / Business Rules` | one `- **INV-X** — rule` line per model-declared invariant of the slice's commands and events |
|
|
402
479
|
|
|
403
480
|
A slice with several elements of one kind gets one region per element, the name suffixed with
|
|
@@ -436,12 +513,22 @@ case is a block:
|
|
|
436
513
|
- **Given:**
|
|
437
514
|
- a basket whose total is 0
|
|
438
515
|
- **When:**
|
|
439
|
-
- the customer
|
|
516
|
+
- the customer issues PlaceOrder
|
|
440
517
|
- **Then:**
|
|
441
518
|
- the command is rejected with a validation error
|
|
442
519
|
- no event is recorded
|
|
443
520
|
```
|
|
444
521
|
|
|
522
|
+
**Scenario quality (citation convention).** A scenario is tied to exactly one command: its When
|
|
523
|
+
names the slice's command (a reaction slice: the command it triggers), its Then names an event
|
|
524
|
+
the slice records or a rejection/error, and it **cites the `INV-…` ids it exercises** anywhere in
|
|
525
|
+
the block (the title above, or a Given/When/Then bullet). Every invariant declared for the slice
|
|
526
|
+
(an `invariant` line in the model, or the doc's Invariants section) should be cited by at least one
|
|
527
|
+
scenario. `em validate` advises on all three — `scenario-when-not-command`,
|
|
528
|
+
`scenario-then-not-outcome`, `invariant-without-scenario` — see
|
|
529
|
+
[validation.md](https://github.com/milehimikey/em/blob/main/docs/validation.md#scenario-quality). Older bullet-format scenarios are checked too; a doc with no scenarios is never
|
|
530
|
+
flagged.
|
|
531
|
+
|
|
445
532
|
A `### Scenario: <title>` heading, then the three column-0 bullets `- **Given:**`, `- **When:**`,
|
|
446
533
|
`- **Then:**`, each with nested ` - ` items (text after the label on the same line counts as a
|
|
447
534
|
first item). `em export --json` publishes the parsed blocks as `slice.doc.scenarios`
|
|
@@ -520,6 +607,13 @@ one it is asking about:
|
|
|
520
607
|
slice shipped, and which" is `shippedRecordOf()` (`src/catalog/sliceDoc.ts`); `em export`
|
|
521
608
|
publishes its answer as `slice.doc.shipped`.
|
|
522
609
|
|
|
610
|
+
**Reading a PR that shows `status: draft`.** When `mark-implemented` opened the next version
|
|
611
|
+
(`status: draft`, `version: <N+1>`), the diff of that slice doc does not say "never marked": the
|
|
612
|
+
`shippedVersion: <N>` / `implementedIn:` lines **are** the record that v<N> was implemented, and
|
|
613
|
+
`status:` is the state of v<N+1>. The command prints a notice naming both
|
|
614
|
+
([cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-slice-mark-implemented-file-slice-key-pr-url)), and `em status` lists the slice
|
|
615
|
+
as `implemented (v<N>) · draft v<N+1> open` rather than a bare `draft`.
|
|
616
|
+
|
|
523
617
|
So a shipped slice can be a `draft` again. `em slice revise <key>`
|
|
524
618
|
([cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-slice-revise-file-slice-key)) opens v<N+1> as `status: draft`, bumps
|
|
525
619
|
`version:`, clears the prior version's review and sign-off, and leaves `implementedIn`/`shipped*`
|
|
@@ -209,15 +209,23 @@ an event directly.}}
|
|
|
209
209
|
--json` → slice.doc.scenarios). Cover the happy path AND the key rule boundaries, one block
|
|
210
210
|
per case: a `### Scenario: <title>` heading, then exactly the three column-0 bullets
|
|
211
211
|
`- **Given:**`, `- **When:**`, `- **Then:**`, each with its beats as nested ` - ` bullets.
|
|
212
|
-
A block missing any of the three is flagged `slice-doc/structured-section-malformed`.
|
|
212
|
+
A block missing any of the three is flagged `slice-doc/structured-section-malformed`.
|
|
213
|
+
Quality (advisory warnings): each scenario is tied to exactly ONE command — its When names
|
|
214
|
+
this slice's command (a reaction slice: the reaction, its view, or the command it triggers); its Then names an event (or a stated no-op)
|
|
215
|
+
this slice records, or a rejection/error; and it CITES the INV ids it exercises (in the
|
|
216
|
+
title or any bullet). Every INV declared for the slice should be cited by some scenario
|
|
217
|
+
(`scenario-when-not-command`, `scenario-then-not-outcome`, `invariant-without-scenario`). -->
|
|
213
218
|
### Scenario: Happy path
|
|
214
219
|
- **Given:**
|
|
215
220
|
- {{starting state / prior events}}
|
|
216
221
|
- **When:**
|
|
217
|
-
- {{command
|
|
222
|
+
- {{the command, by name}}
|
|
218
223
|
- **Then:**
|
|
219
|
-
- {{event(s) recorded}}
|
|
224
|
+
- {{the event(s) recorded, by name}}
|
|
220
225
|
- {{resulting read-model change}}
|
|
226
|
+
<!-- Add one block per rule boundary, citing the INV id in the title, e.g.
|
|
227
|
+
`### Scenario: Non-positive total is rejected (INV-CHK-1)` — When names the command, Then
|
|
228
|
+
names the rejection. -->
|
|
221
229
|
|
|
222
230
|
## Alternate & Error Flows
|
|
223
231
|
<!-- Failure paths, retries, compensations, timeouts, idempotency. -->
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "em-1-
|
|
2
|
+
"name": "em-1-18-1",
|
|
3
3
|
"owner": {
|
|
4
4
|
"name": "Mike Key"
|
|
5
5
|
},
|
|
6
|
-
"description": "em v1.
|
|
6
|
+
"description": "em v1.18.1: the Event Modeling skill bundle and MCP server as a Claude Code plugin.",
|
|
7
7
|
"plugins": [
|
|
8
8
|
{
|
|
9
9
|
"name": "em",
|
|
@@ -62,7 +62,10 @@ import { pushDiag } from "../model/rules.js";
|
|
|
62
62
|
import { classifySlicePattern, slicePatternLabel } from "./classify.js";
|
|
63
63
|
import { resolveSliceDocJoin } from "./docJoin.js";
|
|
64
64
|
import { readSliceDoc } from "./readSliceDoc.js";
|
|
65
|
+
import { checkScenarioQuality } from "./scenarioQualityValidate.js";
|
|
65
66
|
import { findStructuredSectionProblems, splitCells } from "./sliceSections.js";
|
|
67
|
+
import { parseScenariosLenient } from "./scenarios.js";
|
|
68
|
+
import { viewSourceNames } from "../model/continuation.js";
|
|
66
69
|
const COMMAND_MARKER = /^\*\*Command:\*\*\s*`([^`]+)`\s*$/;
|
|
67
70
|
const EVENT_MARKER = /^\*\*Event:\*\*\s*`([^`]+)`(?:\s*→\s*context\s*`[^`]+`\s*)?$/;
|
|
68
71
|
const VIEW_MARKER = /^-\s*\*\*View:\*\*\s*`([^`]+)`/;
|
|
@@ -240,6 +243,8 @@ export function validateDocModelConsistency(model, refs, baseDir) {
|
|
|
240
243
|
message: `slice doc "${docPath}" (slice "${ownerKey}"): ${problem}`,
|
|
241
244
|
});
|
|
242
245
|
}
|
|
246
|
+
// GH #293: scenario-quality advisories (catalog/scenarioQualityValidate.ts).
|
|
247
|
+
checkScenarioQuality(diags, parsed.body, docPath, sliceIndices, ownerKey, model, refs, baseDir);
|
|
243
248
|
const markers = parseDocMarkers(parsed.body);
|
|
244
249
|
const coveredNames = sliceIndices.map((i) => model.slices[i].name);
|
|
245
250
|
for (const kind of ["command", "event", "view"]) {
|
|
@@ -263,6 +268,7 @@ export function validateDocModelConsistency(model, refs, baseDir) {
|
|
|
263
268
|
checkRosterDirections(diags, kind, docPath, docByName, modelByName, ownerSlice, ownerKey, coveredNames, model, refs);
|
|
264
269
|
checkFields(diags, kind, docPath, docByName, modelByName, model, refs);
|
|
265
270
|
}
|
|
271
|
+
checkViewFeeds(diags, parsed, docPath, markers, sliceIndices, ownerSlice, model, refs);
|
|
266
272
|
}
|
|
267
273
|
return diags;
|
|
268
274
|
}
|
|
@@ -368,4 +374,158 @@ function checkFields(diags, kind, docPath, docByName, modelByName, model, refs)
|
|
|
368
374
|
}
|
|
369
375
|
}
|
|
370
376
|
}
|
|
377
|
+
// GH #294 (MIL-245): a State View doc's claims about WHICH EVENTS feed the view, against the
|
|
378
|
+
// model's `from` union (`viewSourceNames`, across `again` instances). Two advisory warnings:
|
|
379
|
+
// doc-names-unfed-event — the doc's field-table Source column (or, when the doc has exactly one
|
|
380
|
+
// view, a scenario Given/Then item) names an event that exists in the model but is not among
|
|
381
|
+
// the view's sources (the toolshed case: prose anticipated an edge the model never drew).
|
|
382
|
+
// doc-omits-fed-event — the model feeds the view an event the doc body never mentions.
|
|
383
|
+
// Event names are found by scanning text for the model's own event names (longest first, word-
|
|
384
|
+
// bounded, case-insensitive), so a name embedded in a longer event name is not double-counted.
|
|
385
|
+
function escapeRe(s) {
|
|
386
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
387
|
+
}
|
|
388
|
+
/** Normalized names of the model events mentioned in `text`. */
|
|
389
|
+
function mentionedEvents(text, eventNames) {
|
|
390
|
+
const found = new Set();
|
|
391
|
+
let rest = text;
|
|
392
|
+
for (const name of [...eventNames].sort((a, b) => b.length - a.length)) {
|
|
393
|
+
const re = new RegExp(`(?<![\\p{L}\\p{N}])${escapeRe(name).replace(/\s+/g, "\\s+")}(?![\\p{L}\\p{N}])`, "giu");
|
|
394
|
+
if (re.test(rest)) {
|
|
395
|
+
found.add(normalizeName(name));
|
|
396
|
+
re.lastIndex = 0;
|
|
397
|
+
rest = rest.replace(re, " ");
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return found;
|
|
401
|
+
}
|
|
402
|
+
/** The text of every cell in the Source column of the first field table in lines[start, end). */
|
|
403
|
+
function sourceColumnCells(lines, start, end) {
|
|
404
|
+
for (let i = start; i < end - 1; i++) {
|
|
405
|
+
if (!TABLE_ROW.test(lines[i]) || !TABLE_ROW.test(lines[i + 1]))
|
|
406
|
+
continue;
|
|
407
|
+
const sep = splitCells(lines[i + 1]);
|
|
408
|
+
if (sep.length === 0 || !sep.every((c) => TABLE_SEPARATOR_CELL.test(c)))
|
|
409
|
+
continue;
|
|
410
|
+
const header = splitCells(lines[i]);
|
|
411
|
+
if (normalizeName(header[0] ?? "") !== "field")
|
|
412
|
+
continue;
|
|
413
|
+
const col = header.findIndex((c) => normalizeName(c).startsWith("source"));
|
|
414
|
+
if (col < 0)
|
|
415
|
+
return [];
|
|
416
|
+
const cells = [];
|
|
417
|
+
for (let j = i + 2; j < end && TABLE_ROW.test(lines[j]); j++) {
|
|
418
|
+
const c = splitCells(lines[j])[col];
|
|
419
|
+
if (c && !PLACEHOLDER.test(c))
|
|
420
|
+
cells.push({ text: c, idx: j });
|
|
421
|
+
}
|
|
422
|
+
return cells;
|
|
423
|
+
}
|
|
424
|
+
return [];
|
|
425
|
+
}
|
|
426
|
+
/** Given/Then items (with a body line index) of every scenario, via the shared lenient reader
|
|
427
|
+
* (`### Scenario:` blocks, the older bullet format, `## Delta` blocks, wrapped lines). The reader
|
|
428
|
+
* carries only a block's heading line, so an item's line is found by searching forward from it
|
|
429
|
+
* for the item's opening text. */
|
|
430
|
+
function scenarioGivenThenLines(body, lines) {
|
|
431
|
+
const out = [];
|
|
432
|
+
const scenarios = parseScenariosLenient(body);
|
|
433
|
+
scenarios.forEach((sc, k) => {
|
|
434
|
+
// search only inside this block: from its heading to the next scenario's heading
|
|
435
|
+
const from = sc.line - 1;
|
|
436
|
+
const to = k + 1 < scenarios.length ? scenarios[k + 1].line - 1 : lines.length;
|
|
437
|
+
for (const item of [...sc.given, ...sc.then]) {
|
|
438
|
+
let idx = from;
|
|
439
|
+
for (let j = from; j < to; j++) {
|
|
440
|
+
// a wrapped item is joined with spaces by the reader, so compare against the item's
|
|
441
|
+
// opening: the physical line's own text (label and bullet stripped) must begin it
|
|
442
|
+
const seg = lines[j].replace(/^\s*[-*]\s+(\*\*(?:Given|When|Then):\*\*\s*)?/, "").trim();
|
|
443
|
+
if (seg !== "" && item.startsWith(seg.slice(0, 24))) {
|
|
444
|
+
idx = j;
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
out.push({ text: item, idx });
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
function checkViewFeeds(diags, doc, docPath, markers, sliceIndices, ownerSlice, model, refs) {
|
|
454
|
+
const docViews = markers.filter((m) => m.kind === "view");
|
|
455
|
+
if (docViews.length === 0)
|
|
456
|
+
return;
|
|
457
|
+
const lines = doc.body.split(/\r?\n/);
|
|
458
|
+
const lineOffset = doc.raw.split(/\r?\n/).length - lines.length;
|
|
459
|
+
const eventNames = model.elements.filter((e) => e.kind === "event").map((e) => e.name);
|
|
460
|
+
if (eventNames.length === 0)
|
|
461
|
+
return;
|
|
462
|
+
const modelViews = new Map();
|
|
463
|
+
for (const i of sliceIndices) {
|
|
464
|
+
for (const el of model.slices[i].elements) {
|
|
465
|
+
if (el.kind !== "view")
|
|
466
|
+
continue;
|
|
467
|
+
const key = normalizeName(el.name);
|
|
468
|
+
if (!modelViews.has(key))
|
|
469
|
+
modelViews.set(key, el);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const matched = docViews.filter((m) => modelViews.has(normalizeName(m.name)));
|
|
473
|
+
const bodyMentions = mentionedEvents(doc.body, eventNames);
|
|
474
|
+
// Scenario text attaches to a view only when the doc has exactly one matched view AND the doc's
|
|
475
|
+
// own slice is the State View that holds it — a State Change doc that `covers:` a view slice, or
|
|
476
|
+
// a merged reaction doc with a to-do view, writes scenarios about its command, not the view.
|
|
477
|
+
const ownsView = (el) => classifySlicePattern(ownerSlice) === "state-view" && model.slices[el.sliceIndex] === ownerSlice;
|
|
478
|
+
const scenarioLines = matched.length === 1 && ownsView(modelViews.get(normalizeName(matched[0].name))) ? scenarioGivenThenLines(doc.body, lines) : [];
|
|
479
|
+
for (const marker of matched) {
|
|
480
|
+
const view = modelViews.get(normalizeName(marker.name));
|
|
481
|
+
const sources = viewSourceNames(model, view);
|
|
482
|
+
const fed = new Set(sources.map(normalizeName));
|
|
483
|
+
const refsOut = [refs.sliceKeys[view.sliceIndex], refs.refById.get(view.id)];
|
|
484
|
+
let end = lines.length;
|
|
485
|
+
for (let j = marker.bodyLine + 1; j < lines.length; j++) {
|
|
486
|
+
if (/^##\s/.test(lines[j]) || matchMarker(lines[j])) {
|
|
487
|
+
end = j;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// event -> body line index of its first mention
|
|
492
|
+
const named = new Map();
|
|
493
|
+
const note = (text, idx) => {
|
|
494
|
+
for (const e of mentionedEvents(text, eventNames))
|
|
495
|
+
if (!named.has(e))
|
|
496
|
+
named.set(e, idx);
|
|
497
|
+
};
|
|
498
|
+
// the view marker's own built-from prose (the marker line and its wrapped continuation) —
|
|
499
|
+
// only in the State View doc that owns the view; a reaction doc's to-do-view prose also
|
|
500
|
+
// describes what its rows drop on, which is not a claim about the view's feed.
|
|
501
|
+
if (ownsView(view)) {
|
|
502
|
+
for (let j = marker.bodyLine; j < end && (j === marker.bodyLine || lines[j].trim() !== ""); j++)
|
|
503
|
+
note(lines[j], j);
|
|
504
|
+
}
|
|
505
|
+
for (const c of sourceColumnCells(lines, marker.bodyLine + 1, end))
|
|
506
|
+
note(c.text, c.idx);
|
|
507
|
+
for (const sl of scenarioLines)
|
|
508
|
+
note(sl.text, sl.idx);
|
|
509
|
+
for (const [key, mentionIdx] of named) {
|
|
510
|
+
if (fed.has(key))
|
|
511
|
+
continue;
|
|
512
|
+
const ev = model.elements.find((e) => e.kind === "event" && normalizeName(e.name) === key);
|
|
513
|
+
pushDiag(diags, "doc-names-unfed-event", {
|
|
514
|
+
message: `slice doc "${docPath}" (view \`${marker.name}\`, line ${mentionIdx + 1 + lineOffset}) names event "${ev.name}", but view "${view.name}" does not read it (from: ${sources.map((n) => `"${n}"`).join(", ") || "—"}) — add \`from "${ev.name}"\` to the view if the edge is real, or fix the doc`,
|
|
515
|
+
line: view.line,
|
|
516
|
+
refs: refsOut,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
const eventKeys = new Set(eventNames.map(normalizeName));
|
|
520
|
+
for (const name of view.from ?? []) {
|
|
521
|
+
if (!eventKeys.has(normalizeName(name)) || bodyMentions.has(normalizeName(name)))
|
|
522
|
+
continue;
|
|
523
|
+
pushDiag(diags, "doc-omits-fed-event", {
|
|
524
|
+
message: `view "${view.name}" reads event "${name}", but slice doc "${docPath}" never mentions it — list it in the view's Source column (run \`em slice sync\`) or drop it from the view's from`,
|
|
525
|
+
line: view.line,
|
|
526
|
+
refs: refsOut,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
371
531
|
//# sourceMappingURL=docModelConsistencyValidate.js.map
|