@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.
- package/dist/actions.d.ts +29 -0
- package/dist/actions.js +78 -0
- package/dist/background.d.ts +2 -0
- package/dist/background.js +138 -0
- package/dist/commands.js +46 -5
- package/dist/descriptor.d.ts +7 -2
- package/dist/descriptor.js +5 -2
- package/dist/element-renderer.d.ts +17 -0
- package/dist/element-renderer.js +15 -0
- package/dist/element-view.d.ts +18 -0
- package/dist/element-view.js +19 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +7 -2
- package/dist/legend.d.ts +16 -0
- package/dist/legend.js +58 -0
- package/dist/nudges.d.ts +26 -0
- package/dist/nudges.js +73 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +121 -0
- package/dist/roles.d.ts +62 -0
- package/dist/roles.js +84 -0
- package/dist/rules.d.ts +20 -0
- package/dist/rules.js +241 -0
- package/dist/toolbar/board-config.d.ts +29 -0
- package/dist/toolbar/board-config.js +86 -0
- package/dist/toolbar/event-storming-menu.d.ts +1 -1
- package/dist/toolbar/event-storming-menu.js +1 -1
- package/dist/toolbar/senior-button.d.ts +1 -0
- package/dist/toolbar/senior-button.js +1 -0
- package/dist/toolbar/senior-tool.js +1 -0
- package/dist/translations.d.ts +7 -5
- package/dist/translations.js +14 -7
- package/dist/view.d.ts +17 -1
- package/dist/view.js +61 -3
- package/package.json +3 -3
package/dist/profiles.js
ADDED
|
@@ -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
|
+
];
|
package/dist/roles.d.ts
ADDED
|
@@ -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])));
|
package/dist/rules.d.ts
ADDED
|
@@ -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:
|
|
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:
|
|
11
|
+
* other frameworks (Event Storming: 11, Context Map: 13).
|
|
12
12
|
*/
|
|
13
13
|
export class EdgelessDddEventStormingMenu extends EdgelessCommandMenu {
|
|
14
14
|
constructor() {
|