@formicoidea/labre-framework-ddd-event-storming 0.32.0 → 0.33.0

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.
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Event Storming validation profiles (WS5).
3
+ *
4
+ * DATA owned by the framework, like its rules and its roles. A profile is
5
+ * chosen per BOARD, not per document, and the choice rides on the board element
6
+ * itself — a Big Picture and a Process-modelling board coexist on one canvas at
7
+ * two levels of requirement, which is precisely how the workshop goes.
8
+ *
9
+ * ## Three, and why three (PO arbitration, 26/08/2026)
10
+ *
11
+ * Every other framework in this library ships two. Event Storming gets three
12
+ * because it is not one activity, it is a SEQUENCE of them, and the same wall
13
+ * means different things at each stage:
14
+ *
15
+ * 1. **Big Picture (Sketch)** — everything on the wall at once, out of order,
16
+ * the grammar not yet decided. The linter must be silent;
17
+ * 2. **Process modelling** — the frieze gets read left to right and reordered.
18
+ * Time starts to mean something; the grammar still does not;
19
+ * 3. **Software design** — the sentences are supposed to be sentences.
20
+ *
21
+ * Two profiles would have forced stage 2 to pick a side: either the timeline
22
+ * goes unchecked through the very session that is about ordering it, or the
23
+ * grammar starts firing at a wall that has not been cleaned up yet. The middle
24
+ * profile exists because the middle stage exists.
25
+ *
26
+ * Registered from the flag-gated `DddEventStormingViewExtension`, beside the
27
+ * rules: switching the flag off takes the choice away with the rest of the
28
+ * tooling, and a board already set to `design` simply stops being checked until
29
+ * it comes back — the id stays written, untouched.
30
+ */
31
+ /**
32
+ * Sketch — the Big Picture. Every rule demoted to `audit`: findings still reach
33
+ * `violations$` for a host panel and a conformance report, and the canvas says
34
+ * nothing.
35
+ *
36
+ * Named **"Big Picture (Sketch)"** in the dropdown, both words on purpose (PO
37
+ * recette, 26/08/2026). "Sketch" is the vocabulary every framework in this
38
+ * library shares for its quietest level, and it is the word the recette script
39
+ * uses; "Big Picture" is the word the workshop uses for this stage and the only
40
+ * one a facilitator would look for. Carrying both is what lets the two readings
41
+ * meet on one entry instead of the user having to learn which name this
42
+ * framework chose.
43
+ *
44
+ * The DEFAULT, and more deliberately here than anywhere else in the library. A
45
+ * Big Picture is **supposed to be chaotic**: that is the method, not a failure
46
+ * of it. Events go up out of order because remembering is not sorting, arcs get
47
+ * drawn between things nobody has typed yet, and stickies get stacked three
48
+ * deep while a stack is what somebody means. A tool arguing with that hand is
49
+ * not strict, it is wrong — it is judging a stage of the workshop by the
50
+ * criteria of a later one.
51
+ *
52
+ * Being the default also means it WRITES NOTHING: a board on `sketch` carries no
53
+ * profile key, so every board ever stormed is on it, with no migration and no
54
+ * backfill.
55
+ */
56
+ const sketch = {
57
+ id: 'es.sketch',
58
+ framework: 'ddd-event-storming',
59
+ labelKey: 'com.labre.event-storming.profile.sketch',
60
+ fallback: 'Big Picture (Sketch)',
61
+ isDefault: true,
62
+ rules: {
63
+ 'es.against-timeline': 'audit',
64
+ 'es.forbidden-arc': 'audit',
65
+ 'es.overlapping-stickies': 'audit',
66
+ },
67
+ };
68
+ /**
69
+ * Process — the frieze is being read left to right and reordered.
70
+ *
71
+ * The timeline is the ONLY thing promoted, because ordering the frieze is
72
+ * exactly what this stage is for: an arc running backwards is now the finding
73
+ * the session is looking for, and the tool pointing at it is the tool helping.
74
+ *
75
+ * The grammar stays at `audit`, on purpose. The wall is still full of stickies
76
+ * whose kind nobody has settled — an event that is going to turn out to be a
77
+ * command, a read model somebody drew as an aggregate — and firing
78
+ * `forbidden-arc` at that is arguing about a sentence whose words are still
79
+ * being chosen. Overlaps stay quiet for the same reason: things are being
80
+ * MOVED, and everything overlaps while it is being moved.
81
+ */
82
+ const process = {
83
+ id: 'es.process',
84
+ framework: 'ddd-event-storming',
85
+ labelKey: 'com.labre.event-storming.profile.process',
86
+ fallback: 'Process modelling',
87
+ rules: {
88
+ 'es.against-timeline': 'warning',
89
+ 'es.forbidden-arc': 'audit',
90
+ 'es.overlapping-stickies': 'audit',
91
+ },
92
+ };
93
+ /**
94
+ * Design — the deliverable level: a board that leaves the room and is read by
95
+ * somebody who was not in it.
96
+ *
97
+ * All three at `warning`. By this stage every sticky has a kind, every arc is a
98
+ * claim about the model, and a frieze nobody can read is a frieze that will be
99
+ * misread. Every severity is spelled out rather than left absent, so the answer
100
+ * is readable in one place.
101
+ *
102
+ * Nothing here is `blocking-overridable`: nothing in this library implements
103
+ * refusal, and declaring it would be data claiming an effect that does not
104
+ * exist.
105
+ */
106
+ const design = {
107
+ id: 'es.design',
108
+ framework: 'ddd-event-storming',
109
+ labelKey: 'com.labre.event-storming.profile.design',
110
+ fallback: 'Software design',
111
+ rules: {
112
+ 'es.against-timeline': 'warning',
113
+ 'es.forbidden-arc': 'warning',
114
+ 'es.overlapping-stickies': 'warning',
115
+ },
116
+ };
117
+ export const EVENT_STORMING_PROFILES = [
118
+ sketch,
119
+ process,
120
+ design,
121
+ ];
@@ -0,0 +1,62 @@
1
+ import { ES_STICKIES } from '@formicoidea/labre-ddd-shared';
2
+ import type { RoleDefs } from '@formicoidea/labre-core/std/gfx';
3
+ /**
4
+ * Event Storming role vocabulary (WS5).
5
+ *
6
+ * A role is the semantic identity of an artefact — no rule will ever look at a
7
+ * shape type or at a fill colour. Three families:
8
+ *
9
+ * - the **board** (`es:board`), the paper roll the storm is run on;
10
+ * - the **sticky** (`es:sticky`), parent of the nine kinds the notation has;
11
+ * - the **flow** (`es:flow`), the edge that says one thing leads to another.
12
+ *
13
+ * ## Why a sticky PARENT
14
+ *
15
+ * Because two of the three rules are written on it and neither wants to know
16
+ * about the nine. `es.overlapping-stickies` is a readability rule — any two
17
+ * stickies on top of each other are unreadable, whichever kinds they are — and
18
+ * the day a tenth kind lands it is covered without a line changing.
19
+ * Specialisation is DATA (`parent`), read by `roleIsA`.
20
+ *
21
+ * ## The alphabet is smaller than the vocabulary
22
+ *
23
+ * All nine kinds get a role; only seven of them appear in the grammar
24
+ * (`es.forbidden-arc`). The **hotspot** and the **constraint** are declared here
25
+ * and cited by no triplet, deliberately: a hotspot is the workshop saying "we do
26
+ * not know", and a flow drawn onto one is somebody parking a question, not
27
+ * making a claim. Being outside the alphabet takes the whole edge out of the
28
+ * conversation (see `RelationEndpointsDef.allowed`), which is exactly the
29
+ * proportionality the family was built for.
30
+ *
31
+ * ## Compat
32
+ *
33
+ * Nothing is backfilled. Stickies placed before these roles existed carry none
34
+ * and are never evaluated (promesse #71): they stay drawings, in the documents
35
+ * they are in.
36
+ */
37
+ /** The sticky kinds as used at the creation sites, hotspot included. */
38
+ export type EventStormingStickyKind = (typeof ES_STICKIES)[number]['kind'] | 'hotspot';
39
+ export type EventStormingRoleId = `es:${string}`;
40
+ /**
41
+ * Role id per sticky kind, DERIVED from the shared preset table rather than
42
+ * restated — so the day a tenth sticky lands in `ES_STICKIES` it gets its role
43
+ * here with no edit. The hotspot is appended because it lives in its own preset
44
+ * (`ES_HOTSPOT`, a diamond rather than a square) and not in the table.
45
+ */
46
+ export declare const ES_STICKY_ROLE: Record<EventStormingStickyKind, EventStormingRoleId>;
47
+ /** The three roles that are not a sticky kind, plus the nine that are. */
48
+ export declare const ES_ROLE: {
49
+ readonly domainEvent: `es:${string}`;
50
+ readonly command: `es:${string}`;
51
+ readonly aggregate: `es:${string}`;
52
+ readonly actor: `es:${string}`;
53
+ readonly constraint: `es:${string}`;
54
+ readonly policy: `es:${string}`;
55
+ readonly readModel: `es:${string}`;
56
+ readonly system: `es:${string}`;
57
+ readonly hotspot: `es:${string}`;
58
+ readonly board: "es:board";
59
+ readonly sticky: "es:sticky";
60
+ readonly flow: "es:flow";
61
+ };
62
+ export declare const EVENT_STORMING_ROLES: RoleDefs;
package/dist/roles.js ADDED
@@ -0,0 +1,84 @@
1
+ import { ES_HOTSPOT, ES_STICKIES } from '@formicoidea/labre-ddd-shared';
2
+ /** camelCase → kebab-case, the WS2 derivation. */
3
+ const kebab = (kind) => kind.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
4
+ /**
5
+ * The one kind whose role id is not its kind kebab-cased.
6
+ *
7
+ * `system` is what the palette has called the sticky since the tool shipped and
8
+ * what its telemetry emits; `external-system` is what Event Storming calls the
9
+ * thing, and it is the word a rule and a hover reveal have to say. Renaming the
10
+ * kind would move a telemetry value (ADR 0008 forbids it) and renaming the role
11
+ * would make the grammar read `command → system`, which is not a sentence
12
+ * anybody says. One entry in a table is the cost of keeping both right.
13
+ */
14
+ const ROLE_SLUG = {
15
+ system: 'external-system',
16
+ };
17
+ const slug = (kind) => ROLE_SLUG[kind] ?? kebab(kind);
18
+ /**
19
+ * Role id per sticky kind, DERIVED from the shared preset table rather than
20
+ * restated — so the day a tenth sticky lands in `ES_STICKIES` it gets its role
21
+ * here with no edit. The hotspot is appended because it lives in its own preset
22
+ * (`ES_HOTSPOT`, a diamond rather than a square) and not in the table.
23
+ */
24
+ export const ES_STICKY_ROLE = Object.fromEntries([
25
+ ...ES_STICKIES.map(preset => [preset.kind, `es:${slug(preset.kind)}`]),
26
+ ['hotspot', 'es:hotspot'],
27
+ ]);
28
+ /** The three roles that are not a sticky kind, plus the nine that are. */
29
+ export const ES_ROLE = {
30
+ board: 'es:board',
31
+ sticky: 'es:sticky',
32
+ flow: 'es:flow',
33
+ ...ES_STICKY_ROLE,
34
+ };
35
+ /** Human label per kind, from the same presets the palette renders. */
36
+ const STICKY_LABEL = {
37
+ ...Object.fromEntries(ES_STICKIES.map(preset => [preset.kind, preset.label])),
38
+ hotspot: ES_HOTSPOT.label,
39
+ };
40
+ const DEFS = [
41
+ // The board: a frame, not a sticky. It specialises nothing, so a rule written
42
+ // on `es:sticky` can never fall on the roll its subjects are stuck to.
43
+ {
44
+ id: ES_ROLE.board,
45
+ kind: 'node',
46
+ labelKey: 'com.labre.event-storming.role.board',
47
+ labelFallback: 'Event Storming board',
48
+ },
49
+ {
50
+ id: ES_ROLE.sticky,
51
+ kind: 'node',
52
+ labelKey: 'com.labre.event-storming.role.sticky',
53
+ labelFallback: 'Sticky',
54
+ },
55
+ ...Object.keys(STICKY_LABEL).map((kind) => ({
56
+ id: ES_STICKY_ROLE[kind],
57
+ parent: ES_ROLE.sticky,
58
+ kind: 'node',
59
+ labelKey: `com.labre.event-storming.role.${slug(kind)}`,
60
+ labelFallback: STICKY_LABEL[kind],
61
+ })),
62
+ /**
63
+ * The flow. Tier 1 of `docs/adr/0010`: the source is the subject of the verb,
64
+ * so the source is what happens FIRST. Everything downstream of that sentence
65
+ * — the tool hint, the hover reveal, the inversion command and
66
+ * `es.against-timeline` — reads this one declaration and never a framework
67
+ * name.
68
+ */
69
+ {
70
+ id: ES_ROLE.flow,
71
+ kind: 'edge',
72
+ labelKey: 'com.labre.event-storming.role.flow',
73
+ labelFallback: 'Flow',
74
+ direction: {
75
+ verbKey: 'com.labre.event-storming.role.flow.verb',
76
+ verbFallback: 'leads to',
77
+ gestureHintKey: 'com.labre.event-storming.role.flow.gesture',
78
+ gestureHintFallback: 'Drag from what happens first to what follows.',
79
+ },
80
+ },
81
+ ];
82
+ // Null prototype: this is a lookup table keyed by ids that may one day come
83
+ // from host-supplied packs, so `defs['toString']` must not resolve.
84
+ export const EVENT_STORMING_ROLES = Object.assign(Object.create(null), Object.fromEntries(DEFS.map(def => [def.id, def])));
@@ -0,0 +1,20 @@
1
+ import type { EndpointTriplet, ValidationRule } from '@formicoidea/labre-core/blocks/surface';
2
+ /**
3
+ * The canonical Event Storming sentence, as a matrix.
4
+ *
5
+ * Read aloud, which is how it was checked: an actor issues a command; a command
6
+ * lands on an aggregate, or on an external system; an aggregate raises a domain
7
+ * event, and so does an external system; a domain event triggers a policy, and
8
+ * a domain event feeds a read model; a policy issues a command; a read model
9
+ * informs an actor.
10
+ *
11
+ * Nine sentences and no more. Every one of them is in Brandolini's own picture
12
+ * of the "Big Picture → Process modelling" grammar, and nothing has been added
13
+ * because it seemed plausible: a matrix that sanctions a sentence the notation
14
+ * does not say is worse than no matrix, because the sentence it fails to catch
15
+ * is the one somebody will ship.
16
+ *
17
+ * Exported so a test asserts THIS table rather than a copy of it.
18
+ */
19
+ export declare const ES_FLOW_MATRIX: readonly EndpointTriplet[];
20
+ export declare const EVENT_STORMING_RULES: readonly ValidationRule[];
package/dist/rules.js ADDED
@@ -0,0 +1,241 @@
1
+ import { EVENT_STORMING_BACKGROUND } from './background.js';
2
+ import { ES_ROLE, EVENT_STORMING_ROLES } from './roles.js';
3
+ /**
4
+ * Event Storming validation rules (WS5).
5
+ *
6
+ * DATA owned by the framework, versioned per rule: the engine
7
+ * (`@labre/affine-block-surface`) knows how to evaluate a FAMILY, never a
8
+ * concrete rule. Registered from the flag-gated
9
+ * `DddEventStormingViewExtension`, so switching the Event Storming flag off
10
+ * removes them with the rest of the tooling — boards already stormed keep
11
+ * rendering, they simply stop being checked (`docs/adr/0009`).
12
+ *
13
+ * ## Three rules, and the ones that are deliberately absent
14
+ *
15
+ * Event Storming's grammar is small and its etiquette is large, and the two
16
+ * must not be confused. What is decidable here is the DIRECTION of the frieze,
17
+ * the SHAPE of an arc and whether two stickies are on top of each other.
18
+ *
19
+ * What is not, and is a level-2 nudge instead (`./nudges.ts`, PO arbitration of
20
+ * 26/08/2026): **the past tense**. "Order placed" is a domain event and "Place
21
+ * order" is a command, and every workshop gets that wrong for the first hour —
22
+ * but deciding it means parsing a human sentence, in whatever language the room
23
+ * speaks, written in marker pen by somebody who abbreviates. There are no
24
+ * linguistic rules in this file, on purpose. A checklist item that says "the
25
+ * events are named in the past tense" costs a tick and is true; a regular
26
+ * expression over `Y.Text` costs the tool's credibility and is wrong every
27
+ * fifth sticky.
28
+ */
29
+ /**
30
+ * **ES1** — a flow runs forwards in time.
31
+ *
32
+ * The one rule the frame makes possible, and the reason the board declares an
33
+ * axis at all: the source of a flow is what happens FIRST (tier 1 of
34
+ * `docs/adr/0010`, the verb is "leads to"), so its target belongs to the RIGHT
35
+ * of it. A flow drawn the other way says the effect precedes the cause — either
36
+ * the arc was drawn from the wrong end, or the two stickies need swapping, and
37
+ * only the room knows which.
38
+ *
39
+ * ## The tolerance, and why it is tiny
40
+ *
41
+ * `0.01` of the board's width — 32 units on the 3200-wide reference, about a
42
+ * quarter of a sticky. Much tighter than the Wardley equivalent's 2 %, and for
43
+ * the opposite reason: on a Wardley map two components drawn level are a chain
44
+ * waiting to be spread out, whereas on a frieze two stickies drawn level are
45
+ * two things that happen at the same moment — which is a claim, not a
46
+ * placeholder — and an arc between them is the one that wants asking about.
47
+ * The tolerance is here to absorb a hand, not a habit: stickies stacked to
48
+ * within a few pixels stay silent, anything genuinely to the left does not.
49
+ *
50
+ * As with every other position in this engine it is a RATIO, so the verdict
51
+ * survives the roll being unspooled (the lesson of the 01/08/2026 recette).
52
+ *
53
+ * Silent, from the family: an arc with a free end, an arc between two different
54
+ * boards, an arc carrying no role, and an arc on a board that has no
55
+ * `es:board` element under it — a frieze stormed on the bare canvas is a
56
+ * frieze, and it has no timeline to run against.
57
+ */
58
+ const againstTimeline = {
59
+ id: 'es.against-timeline',
60
+ framework: 'ddd-event-storming',
61
+ family: 'relative-order-along-axis',
62
+ severity: 'warning',
63
+ // No `appliesTo`: the subject is a RELATION, and the role that names it is
64
+ // declared where the family reads it — naming one of the three indicted
65
+ // elements here would be data that lies.
66
+ roles: EVENT_STORMING_ROLES,
67
+ messageKey: 'com.labre.event-storming.validation.against-timeline',
68
+ messageFallback: 'This flow runs backwards along the timeline.',
69
+ suggestionKey: 'com.labre.event-storming.validation.against-timeline.suggestion',
70
+ suggestionFallback: 'Time runs left to right: move what follows to the right of what leads to it — or, if the arc was drawn from the wrong end, reverse it.',
71
+ version: 1,
72
+ provenance: {
73
+ source: 'recommendation',
74
+ reference: 'Event Storming (Brandolini) — the frieze runs left to right in time',
75
+ },
76
+ backgroundRole: ES_ROLE.board,
77
+ background: EVENT_STORMING_BACKGROUND,
78
+ relativeOrder: {
79
+ edgeRole: ES_ROLE.flow,
80
+ axis: 'time',
81
+ // The verb is "leads to": the source happens first, so the TARGET is the
82
+ // one further along the axis' forward sense.
83
+ expect: 'target-ahead',
84
+ toleranceRatio: 0.01,
85
+ },
86
+ };
87
+ /**
88
+ * The canonical Event Storming sentence, as a matrix.
89
+ *
90
+ * Read aloud, which is how it was checked: an actor issues a command; a command
91
+ * lands on an aggregate, or on an external system; an aggregate raises a domain
92
+ * event, and so does an external system; a domain event triggers a policy, and
93
+ * a domain event feeds a read model; a policy issues a command; a read model
94
+ * informs an actor.
95
+ *
96
+ * Nine sentences and no more. Every one of them is in Brandolini's own picture
97
+ * of the "Big Picture → Process modelling" grammar, and nothing has been added
98
+ * because it seemed plausible: a matrix that sanctions a sentence the notation
99
+ * does not say is worse than no matrix, because the sentence it fails to catch
100
+ * is the one somebody will ship.
101
+ *
102
+ * Exported so a test asserts THIS table rather than a copy of it.
103
+ */
104
+ export const ES_FLOW_MATRIX = [
105
+ { source: ES_ROLE.actor, edge: ES_ROLE.flow, target: ES_ROLE.command },
106
+ { source: ES_ROLE.command, edge: ES_ROLE.flow, target: ES_ROLE.aggregate },
107
+ { source: ES_ROLE.command, edge: ES_ROLE.flow, target: ES_ROLE.system },
108
+ {
109
+ source: ES_ROLE.aggregate,
110
+ edge: ES_ROLE.flow,
111
+ target: ES_ROLE.domainEvent,
112
+ },
113
+ { source: ES_ROLE.system, edge: ES_ROLE.flow, target: ES_ROLE.domainEvent },
114
+ { source: ES_ROLE.domainEvent, edge: ES_ROLE.flow, target: ES_ROLE.policy },
115
+ {
116
+ source: ES_ROLE.domainEvent,
117
+ edge: ES_ROLE.flow,
118
+ target: ES_ROLE.readModel,
119
+ },
120
+ { source: ES_ROLE.policy, edge: ES_ROLE.flow, target: ES_ROLE.command },
121
+ { source: ES_ROLE.readModel, edge: ES_ROLE.flow, target: ES_ROLE.actor },
122
+ ];
123
+ /**
124
+ * **ES2** — a flow says one of the nine things the notation can say.
125
+ *
126
+ * The grammar rule, and the one that earns the framework its roles: a flow from
127
+ * a command straight to a domain event skips the aggregate, which is the whole
128
+ * modelling question Event Storming is run to answer. Reading it off the roles
129
+ * rather than off the colours means the verdict survives someone restyling a
130
+ * sticky, and reading it off `source → target` means it survives someone
131
+ * re-laying-out the wall.
132
+ *
133
+ * ## What it stays silent about, and why that is the rule
134
+ *
135
+ * **Hotspots and constraints are outside the alphabet.** Neither is cited by a
136
+ * triplet, so an arc with either at one end takes the whole edge out of the
137
+ * conversation — not the matrix, not the self-loop, nothing. That is the point
138
+ * of the family (`RelationEndpointsDef.allowed`) and it is the hard requirement
139
+ * for Event Storming in particular: a hotspot is a workshop saying "we do not
140
+ * know", and an arrow drawn at one is somebody parking a question. A tool that
141
+ * answered "that arc is forbidden" would be indicting the act of storming.
142
+ *
143
+ * The same silence covers an arc onto a plain rectangle, onto a note, onto an
144
+ * artefact of another framework, and every flow drawn before WS5 — those carry
145
+ * no role at all and are not even looked at.
146
+ *
147
+ * ## Self-loops yes, duplicates no
148
+ *
149
+ * A sticky leading to itself is not a sentence in this notation at whichever
150
+ * end you read it, so `forbidSelfLoop` is on.
151
+ *
152
+ * `forbidDuplicate` is deliberately OFF. Two flows between the same two
153
+ * stickies is what a wall looks like when a process has two paths to the same
154
+ * outcome and the room drew both; the frieze is read by following arcs, not by
155
+ * counting them, and a workshop is exactly where a line gets drawn twice while
156
+ * three people talk over each other. Nothing is lost by staying quiet — unlike
157
+ * a context map, where the same pattern twice between two contexts is a claim
158
+ * made twice.
159
+ */
160
+ const forbiddenArc = {
161
+ id: 'es.forbidden-arc',
162
+ framework: 'ddd-event-storming',
163
+ family: 'relation-endpoints',
164
+ severity: 'warning',
165
+ roles: EVENT_STORMING_ROLES,
166
+ messageKey: 'com.labre.event-storming.validation.forbidden-arc',
167
+ messageFallback: 'This flow is not a sentence Event Storming says.',
168
+ suggestionKey: 'com.labre.event-storming.validation.forbidden-arc.suggestion',
169
+ suggestionFallback: 'The frieze reads: an actor issues a command, the command lands on an aggregate or an external system, and that raises a domain event, which triggers a policy or feeds a read model. Re-point the arc onto the sticky that is missing.',
170
+ version: 1,
171
+ provenance: {
172
+ source: 'recommendation',
173
+ reference: 'Event Storming (Brandolini) — the flow the big-picture notation draws',
174
+ },
175
+ backgroundRole: ES_ROLE.board,
176
+ background: EVENT_STORMING_BACKGROUND,
177
+ endpoints: {
178
+ edgeRole: ES_ROLE.flow,
179
+ allowed: ES_FLOW_MATRIX,
180
+ forbidSelfLoop: true,
181
+ // See the header: not on a wall.
182
+ selfLoop: {
183
+ messageKey: 'com.labre.event-storming.validation.flow-self-loop',
184
+ messageFallback: 'This flow loops back onto the sticky it starts from.',
185
+ suggestionKey: 'com.labre.event-storming.validation.flow-self-loop.suggestion',
186
+ suggestionFallback: 'Nothing leads to itself on a frieze — drop the loose end on what actually follows, or delete the arc.',
187
+ },
188
+ },
189
+ };
190
+ /**
191
+ * **ES3** — two stickies must not be on top of each other.
192
+ *
193
+ * A readability rule, not a semantic one, and the softest thing in the pack: a
194
+ * frieze you cannot read is useless, but a sticky nudged over its neighbour
195
+ * while somebody reorders the morning is not a mistake.
196
+ *
197
+ * Written on the PARENT role, which is what makes it one rule instead of
198
+ * thirty-six pairs: any two stickies hide each other, whichever kinds they are,
199
+ * and the tenth kind is covered on the day it lands.
200
+ *
201
+ * ## The threshold, and why it is wide
202
+ *
203
+ * **12 model units** — a tenth of the 120-unit sticky, against the 4 units
204
+ * Wardley uses for a node the size of a full stop. Stickies FRAME each other on
205
+ * a real wall: a workshop overlaps the corners of a run of events to say they
206
+ * belong together, tucks a policy under the event that triggers it, and shingles
207
+ * a stack of read models. At 4 units all of that would be reported, and the rule
208
+ * would be switched off by lunchtime. At 12 a deliberate tuck stays silent and a
209
+ * sticky genuinely covering another — half a label hidden — is still reported
210
+ * with an order of magnitude to spare.
211
+ */
212
+ const overlappingStickies = {
213
+ id: 'es.overlapping-stickies',
214
+ framework: 'ddd-event-storming',
215
+ family: 'no-overlap',
216
+ severity: 'warning',
217
+ // No `appliesTo`: the subject is a PAIR, and naming one half of it here would
218
+ // be data that lies.
219
+ roles: EVENT_STORMING_ROLES,
220
+ messageKey: 'com.labre.event-storming.validation.overlapping-stickies',
221
+ messageFallback: 'These two stickies cover each other.',
222
+ suggestionKey: 'com.labre.event-storming.validation.overlapping-stickies.suggestion',
223
+ suggestionFallback: 'Slide one aside so both can be read.',
224
+ version: 1,
225
+ provenance: {
226
+ source: 'labre-convention',
227
+ reference: 'Labre readability convention — no Event Storming rule speaks of overlapping ink',
228
+ },
229
+ // Not a frame the rule measures against — an overlap is an overlap wherever
230
+ // it happens — but the board a finding is ATTRIBUTED to, so the arbitration
231
+ // "ignore this rule on the whole board" has one board to be written on.
232
+ backgroundRole: ES_ROLE.board,
233
+ overlap: [[ES_ROLE.sticky, ES_ROLE.sticky]],
234
+ // How deep a collision has to be before it is one. See the header.
235
+ minPenetration: 12,
236
+ };
237
+ export const EVENT_STORMING_RULES = [
238
+ againstTimeline,
239
+ forbiddenArc,
240
+ overlappingStickies,
241
+ ];
@@ -0,0 +1,29 @@
1
+ import { type ToolbarContext } from '@formicoidea/labre-core/shared/services';
2
+ /**
3
+ * The selected board's contextual toolbar: the resize toggle, and the automatic
4
+ * legend of the sticky kinds actually stuck to the board. Registered ALWAYS-ON
5
+ * (`DddEventStormingRenderViewExtension`) — a stored board must stay usable with
6
+ * the Event Storming button switched off (`docs/adr/0009`), legend included: a
7
+ * legend is real editable elements, so generating one is authoring a document,
8
+ * not tooling that a flag may take away.
9
+ *
10
+ * This is the module's ONLY legend gesture: the Event Storming palette never had
11
+ * a static Legend entry, and a wall of colour-coded stickies is exactly the
12
+ * board a reader needs one for.
13
+ */
14
+ export declare const eventStormingBoardToolbarConfig: {
15
+ readonly actions: [{
16
+ readonly id: "a.toggle-resize";
17
+ readonly tooltip: "Enable / lock resizing";
18
+ readonly icon: import("lit-html").TemplateResult<1>;
19
+ readonly active: (ctx: ToolbarContext) => boolean;
20
+ readonly run: (ctx: ToolbarContext) => void;
21
+ }, {
22
+ readonly id: "b.legend";
23
+ readonly tooltip: "Generate the legend (notation present)";
24
+ readonly icon: import("lit-html").TemplateResult<1>;
25
+ readonly run: (ctx: ToolbarContext) => void;
26
+ }];
27
+ readonly when: (ctx: ToolbarContext) => boolean;
28
+ };
29
+ export declare const eventStormingBoardToolbarExtension: import("@formicoidea/labre-core/store").ExtensionType;
@@ -0,0 +1,86 @@
1
+ import { EdgelessCRUDIdentifier } from '@formicoidea/labre-core/blocks/surface';
2
+ import { createAutoLegend, dddLegendIcon } from '@formicoidea/labre-ddd-shared';
3
+ import { EventStormingBoardElementModel } from '@formicoidea/labre-core/model';
4
+ import { TelemetryProvider, ToolbarModuleExtension, } from '@formicoidea/labre-core/shared/services';
5
+ import { BlockFlavourIdentifier } from '@formicoidea/labre-core/std';
6
+ import { html } from 'lit';
7
+ import { EVENT_STORMING_AUTO_LEGEND } from '../legend.js';
8
+ const ResizeIcon = html `<svg
9
+ width="24"
10
+ height="24"
11
+ viewBox="0 0 24 24"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ stroke-width="1.6"
15
+ stroke-linecap="round"
16
+ stroke-linejoin="round"
17
+ >
18
+ <path d="M9 5H5v4M15 19h4v-4" />
19
+ <path d="M5 5l6 6M19 19l-6-6" />
20
+ </svg>`;
21
+ /**
22
+ * The selected board's contextual toolbar: the resize toggle, and the automatic
23
+ * legend of the sticky kinds actually stuck to the board. Registered ALWAYS-ON
24
+ * (`DddEventStormingRenderViewExtension`) — a stored board must stay usable with
25
+ * the Event Storming button switched off (`docs/adr/0009`), legend included: a
26
+ * legend is real editable elements, so generating one is authoring a document,
27
+ * not tooling that a flag may take away.
28
+ *
29
+ * This is the module's ONLY legend gesture: the Event Storming palette never had
30
+ * a static Legend entry, and a wall of colour-coded stickies is exactly the
31
+ * board a reader needs one for.
32
+ */
33
+ export const eventStormingBoardToolbarConfig = {
34
+ actions: [
35
+ {
36
+ id: 'a.toggle-resize',
37
+ tooltip: 'Enable / lock resizing',
38
+ icon: ResizeIcon,
39
+ active(ctx) {
40
+ const models = ctx.getSurfaceModelsByType(EventStormingBoardElementModel);
41
+ return models.length > 0 && models.every(model => model.resizeEnabled);
42
+ },
43
+ run(ctx) {
44
+ const models = ctx.getSurfaceModelsByType(EventStormingBoardElementModel);
45
+ if (!models.length)
46
+ return;
47
+ const enable = !models.every(model => model.resizeEnabled);
48
+ ctx.std.store.captureSync();
49
+ const crud = ctx.std.get(EdgelessCRUDIdentifier);
50
+ for (const model of models) {
51
+ crud.updateElement(model.id, { resizeEnabled: enable });
52
+ }
53
+ },
54
+ },
55
+ {
56
+ id: 'b.legend',
57
+ tooltip: 'Generate the legend (notation present)',
58
+ icon: dddLegendIcon,
59
+ run(ctx) {
60
+ const board = ctx.getSurfaceModelsByType(EventStormingBoardElementModel)[0];
61
+ if (!board)
62
+ return;
63
+ createAutoLegend(ctx.std, board, EVENT_STORMING_AUTO_LEGEND);
64
+ ctx.std
65
+ .getOptional(TelemetryProvider)
66
+ ?.track('FrameworkLegendCreated', {
67
+ // The WIRE value, which is not the module id: the framework is
68
+ // `ddd-event-storming` in code and `event-storming` in PostHog
69
+ // (`frameworks.ts` `telemetryKey`, and the only value
70
+ // `FrameworkElementEvent` accepts). Same convention as Wardley's own
71
+ // legend button, so the two are comparable.
72
+ framework: 'event-storming',
73
+ element: 'legend',
74
+ page: 'whiteboard editor',
75
+ segment: 'element toolbar',
76
+ module: 'event-storming toolbar',
77
+ });
78
+ },
79
+ },
80
+ ],
81
+ when: (ctx) => ctx.getSurfaceModelsByType(EventStormingBoardElementModel).length > 0,
82
+ };
83
+ export const eventStormingBoardToolbarExtension = ToolbarModuleExtension({
84
+ id: BlockFlavourIdentifier('affine:surface:eventStorming'),
85
+ config: eventStormingBoardToolbarConfig,
86
+ });
@@ -7,7 +7,7 @@ import { EdgelessCommandMenu } from '@formicoidea/labre-core/widgets/edgeless-to
7
7
  * helper — is gone with the duplication it carried (`docs/adr/0008`).
8
8
  *
9
9
  * The DDD palettes keep their tighter gap: they carry more buttons than the
10
- * other frameworks (Context Map: 12).
10
+ * other frameworks (Event Storming: 11, Context Map: 13).
11
11
  */
12
12
  export declare class EdgelessDddEventStormingMenu extends EdgelessCommandMenu {
13
13
  static styles: import("lit").CSSResultGroup[];
@@ -8,7 +8,7 @@ import { css } from 'lit';
8
8
  * helper — is gone with the duplication it carried (`docs/adr/0008`).
9
9
  *
10
10
  * The DDD palettes keep their tighter gap: they carry more buttons than the
11
- * other frameworks (Context Map: 12).
11
+ * other frameworks (Event Storming: 11, Context Map: 13).
12
12
  */
13
13
  export class EdgelessDddEventStormingMenu extends EdgelessCommandMenu {
14
14
  constructor() {