@miragon/event-storming-schema-model 0.1.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/README.md +38 -0
- package/dist/index.cjs +273 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +480 -0
- package/dist/index.d.ts +480 -0
- package/dist/index.js +256 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @miragon/event-storming-schema-model
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@miragon/event-storming-schema-model)
|
|
4
|
+
[](https://github.com/Miragon/event-storming-modeler/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
DOM-free Event Storming metamodel: types, Zod validation, schema migrations, and deterministic
|
|
7
|
+
JSON serialization.
|
|
8
|
+
|
|
9
|
+
A board is a set of stickies (domain events, commands, actors, aggregates, policies, read models,
|
|
10
|
+
external systems, hotspots, notes, freeform drawings) plus arrows between them. Positions are
|
|
11
|
+
element centers in board pixels on an unbounded free canvas.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @miragon/event-storming-schema-model
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import {
|
|
23
|
+
createEmptyBoard,
|
|
24
|
+
serializeBoard,
|
|
25
|
+
parseBoardJSON,
|
|
26
|
+
} from '@miragon/event-storming-schema-model';
|
|
27
|
+
|
|
28
|
+
const board = createEmptyBoard('Order Checkout');
|
|
29
|
+
|
|
30
|
+
const json = serializeBoard(board); // deterministic: stable key order, rounded coordinates
|
|
31
|
+
const restored = parseBoardJSON(json); // validated + migrated to the current schema
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Part of the [Event Storming Modeler](https://github.com/Miragon/event-storming-modeler) monorepo.
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
|
|
5
|
+
// src/layout.ts
|
|
6
|
+
var DEFAULT_BOARD_SIZE = { width: 1080, height: 680 };
|
|
7
|
+
function sortByTimeline(board) {
|
|
8
|
+
return [...board.elements].sort(
|
|
9
|
+
(a, b) => a.position.x - b.position.x || a.position.y - b.position.y || a.id.localeCompare(b.id)
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/levels.ts
|
|
14
|
+
var DEFAULT_BOARD_LEVEL = "design";
|
|
15
|
+
var LEVEL_STICKY_KINDS = {
|
|
16
|
+
"big-picture": ["event", "actor", "external", "hotspot"],
|
|
17
|
+
process: ["event", "command", "actor", "policy", "readmodel", "external", "hotspot"],
|
|
18
|
+
design: ["event", "command", "actor", "aggregate", "policy", "readmodel", "external", "hotspot"]
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/attachments.ts
|
|
22
|
+
var ATTACHABLE_STICKY_KINDS = [
|
|
23
|
+
"actor",
|
|
24
|
+
"hotspot",
|
|
25
|
+
"note"
|
|
26
|
+
];
|
|
27
|
+
var HOST_STICKY_KINDS = [
|
|
28
|
+
"event",
|
|
29
|
+
"command",
|
|
30
|
+
"aggregate",
|
|
31
|
+
"policy",
|
|
32
|
+
"readmodel",
|
|
33
|
+
"external"
|
|
34
|
+
];
|
|
35
|
+
var coordinateSchema = zod.z.object({
|
|
36
|
+
x: zod.z.number(),
|
|
37
|
+
y: zod.z.number()
|
|
38
|
+
});
|
|
39
|
+
var baseFields = {
|
|
40
|
+
id: zod.z.string().min(1),
|
|
41
|
+
label: zod.z.string(),
|
|
42
|
+
position: coordinateSchema,
|
|
43
|
+
color: zod.z.string().optional()
|
|
44
|
+
};
|
|
45
|
+
var domainEventSchema = zod.z.object({ ...baseFields, elementType: zod.z.literal("event") });
|
|
46
|
+
var commandSchema = zod.z.object({ ...baseFields, elementType: zod.z.literal("command") });
|
|
47
|
+
var attachedTo = zod.z.string().min(1).optional();
|
|
48
|
+
var actorSchema = zod.z.object({ ...baseFields, elementType: zod.z.literal("actor"), attachedTo });
|
|
49
|
+
var aggregateSchema = zod.z.object({ ...baseFields, elementType: zod.z.literal("aggregate") });
|
|
50
|
+
var policySchema = zod.z.object({ ...baseFields, elementType: zod.z.literal("policy") });
|
|
51
|
+
var readModelSchema = zod.z.object({ ...baseFields, elementType: zod.z.literal("readmodel") });
|
|
52
|
+
var externalSystemSchema = zod.z.object({ ...baseFields, elementType: zod.z.literal("external") });
|
|
53
|
+
var hotspotSchema = zod.z.object({ ...baseFields, elementType: zod.z.literal("hotspot"), attachedTo });
|
|
54
|
+
var noteSizeSchema = zod.z.object({
|
|
55
|
+
width: zod.z.number().positive(),
|
|
56
|
+
height: zod.z.number().positive()
|
|
57
|
+
});
|
|
58
|
+
var noteAlignSchema = zod.z.object({
|
|
59
|
+
horizontal: zod.z.enum(["left", "center", "right"]).optional(),
|
|
60
|
+
vertical: zod.z.enum(["top", "middle", "bottom"]).optional()
|
|
61
|
+
});
|
|
62
|
+
var noteSchema = zod.z.object({
|
|
63
|
+
...baseFields,
|
|
64
|
+
elementType: zod.z.literal("note"),
|
|
65
|
+
size: noteSizeSchema.optional(),
|
|
66
|
+
align: noteAlignSchema.optional(),
|
|
67
|
+
attachedTo
|
|
68
|
+
});
|
|
69
|
+
var drawingSchema = zod.z.object({
|
|
70
|
+
...baseFields,
|
|
71
|
+
elementType: zod.z.literal("drawing"),
|
|
72
|
+
points: zod.z.array(coordinateSchema).min(2),
|
|
73
|
+
closed: zod.z.boolean().optional(),
|
|
74
|
+
strokeStyle: zod.z.enum(["solid", "dashed", "dotted"]).optional()
|
|
75
|
+
});
|
|
76
|
+
var boardElementSchema = zod.z.discriminatedUnion("elementType", [
|
|
77
|
+
domainEventSchema,
|
|
78
|
+
commandSchema,
|
|
79
|
+
actorSchema,
|
|
80
|
+
aggregateSchema,
|
|
81
|
+
policySchema,
|
|
82
|
+
readModelSchema,
|
|
83
|
+
externalSystemSchema,
|
|
84
|
+
hotspotSchema,
|
|
85
|
+
noteSchema,
|
|
86
|
+
drawingSchema
|
|
87
|
+
]);
|
|
88
|
+
var arrowSchema = zod.z.object({
|
|
89
|
+
id: zod.z.string().min(1),
|
|
90
|
+
edgeType: zod.z.literal("arrow"),
|
|
91
|
+
from: zod.z.string(),
|
|
92
|
+
to: zod.z.string(),
|
|
93
|
+
label: zod.z.string().optional()
|
|
94
|
+
});
|
|
95
|
+
var boardEdgeSchema = zod.z.discriminatedUnion("edgeType", [arrowSchema]);
|
|
96
|
+
var boardConfigSchema = zod.z.object({
|
|
97
|
+
title: zod.z.string(),
|
|
98
|
+
style: zod.z.enum(["classic", "dark"]).optional(),
|
|
99
|
+
level: zod.z.enum(["big-picture", "process", "design"]).optional()
|
|
100
|
+
});
|
|
101
|
+
var eventStormingBoardSchema = zod.z.object({
|
|
102
|
+
schemaVersion: zod.z.number().int().positive(),
|
|
103
|
+
config: boardConfigSchema,
|
|
104
|
+
elements: zod.z.array(boardElementSchema),
|
|
105
|
+
edges: zod.z.array(boardEdgeSchema),
|
|
106
|
+
rawPassthrough: zod.z.array(zod.z.string()).optional()
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// src/migrations.ts
|
|
110
|
+
var CURRENT_SCHEMA_VERSION = 1;
|
|
111
|
+
var MIGRATIONS = [
|
|
112
|
+
// No migrations yet — v1 is the starting version.
|
|
113
|
+
];
|
|
114
|
+
function migrate(input) {
|
|
115
|
+
if (typeof input !== "object" || input === null) {
|
|
116
|
+
throw new Error("EventStormingBoard must be an object.");
|
|
117
|
+
}
|
|
118
|
+
const obj = { ...input };
|
|
119
|
+
const rawVersion = obj["schemaVersion"];
|
|
120
|
+
const version = typeof rawVersion === "number" ? rawVersion : 1;
|
|
121
|
+
if (version > CURRENT_SCHEMA_VERSION) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Unknown schemaVersion ${version} (supported up to ${CURRENT_SCHEMA_VERSION}). Please update the tool.`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (version < 1) {
|
|
127
|
+
throw new Error(`Invalid schemaVersion ${version}.`);
|
|
128
|
+
}
|
|
129
|
+
let current = { ...obj, schemaVersion: version };
|
|
130
|
+
for (let v = version; v < CURRENT_SCHEMA_VERSION; v++) {
|
|
131
|
+
const step = MIGRATIONS[v - 1];
|
|
132
|
+
if (!step) throw new Error(`Missing migration for version ${v}.`);
|
|
133
|
+
current = { ...step(current), schemaVersion: v + 1 };
|
|
134
|
+
}
|
|
135
|
+
return current;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/serialize.ts
|
|
139
|
+
var COORD_PRECISION = 3;
|
|
140
|
+
var CONNECTABLE_TYPES = /* @__PURE__ */ new Set([
|
|
141
|
+
"event",
|
|
142
|
+
"command",
|
|
143
|
+
"actor",
|
|
144
|
+
"aggregate",
|
|
145
|
+
"policy",
|
|
146
|
+
"readmodel",
|
|
147
|
+
"external",
|
|
148
|
+
"hotspot"
|
|
149
|
+
]);
|
|
150
|
+
var HOST_TYPES = new Set(HOST_STICKY_KINDS);
|
|
151
|
+
function round(n, digits = COORD_PRECISION) {
|
|
152
|
+
const f = 10 ** digits;
|
|
153
|
+
return Math.round(n * f) / f;
|
|
154
|
+
}
|
|
155
|
+
function validateBoard(data) {
|
|
156
|
+
const parsed = eventStormingBoardSchema.parse(data);
|
|
157
|
+
const ids = /* @__PURE__ */ new Set();
|
|
158
|
+
const typeById = /* @__PURE__ */ new Map();
|
|
159
|
+
for (const el of parsed.elements) {
|
|
160
|
+
if (ids.has(el.id)) throw new Error(`Duplicate element id: ${el.id}`);
|
|
161
|
+
ids.add(el.id);
|
|
162
|
+
typeById.set(el.id, el.elementType);
|
|
163
|
+
}
|
|
164
|
+
for (const el of parsed.elements) {
|
|
165
|
+
if (!("attachedTo" in el) || el.attachedTo === void 0) continue;
|
|
166
|
+
if (!ids.has(el.attachedTo)) {
|
|
167
|
+
throw new Error(`Element ${el.id}: attachedTo "${el.attachedTo}" references no element.`);
|
|
168
|
+
}
|
|
169
|
+
if (!HOST_TYPES.has(typeById.get(el.attachedTo))) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Element ${el.id}: attachedTo "${el.attachedTo}" is a ${typeById.get(el.attachedTo)} \u2014 actors/hotspots/notes may only attach to host stickies.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const edgeIds = /* @__PURE__ */ new Set();
|
|
176
|
+
for (const edge of parsed.edges) {
|
|
177
|
+
if (edgeIds.has(edge.id)) throw new Error(`Duplicate edge id: ${edge.id}`);
|
|
178
|
+
if (ids.has(edge.id)) throw new Error(`Edge id collides with element id: ${edge.id}`);
|
|
179
|
+
edgeIds.add(edge.id);
|
|
180
|
+
if (!ids.has(edge.from)) {
|
|
181
|
+
throw new Error(`Edge ${edge.id}: source "${edge.from}" references no element.`);
|
|
182
|
+
}
|
|
183
|
+
if (!ids.has(edge.to)) {
|
|
184
|
+
throw new Error(`Edge ${edge.id}: target "${edge.to}" references no element.`);
|
|
185
|
+
}
|
|
186
|
+
if (!CONNECTABLE_TYPES.has(typeById.get(edge.from))) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Edge ${edge.id}: source "${edge.from}" is a ${typeById.get(edge.from)} \u2014 arrows may only connect stickies.`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
if (!CONNECTABLE_TYPES.has(typeById.get(edge.to))) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Edge ${edge.id}: target "${edge.to}" is a ${typeById.get(edge.to)} \u2014 arrows may only connect stickies.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return parsed;
|
|
198
|
+
}
|
|
199
|
+
function loadBoard(data) {
|
|
200
|
+
return validateBoard(migrate(data));
|
|
201
|
+
}
|
|
202
|
+
function parseBoardJSON(json) {
|
|
203
|
+
return loadBoard(JSON.parse(json));
|
|
204
|
+
}
|
|
205
|
+
function serializeBoard(board) {
|
|
206
|
+
return stableStringify(canonicalize(board));
|
|
207
|
+
}
|
|
208
|
+
function canonicalize(board) {
|
|
209
|
+
const elements = [...board.elements].sort((a, b) => a.id.localeCompare(b.id)).map((el) => roundNumbers(el));
|
|
210
|
+
const edges = [...board.edges].sort((a, b) => a.id.localeCompare(b.id));
|
|
211
|
+
return {
|
|
212
|
+
...board,
|
|
213
|
+
config: roundNumbers(board.config),
|
|
214
|
+
elements,
|
|
215
|
+
edges
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function roundNumbers(value) {
|
|
219
|
+
if (typeof value === "number") return round(value);
|
|
220
|
+
if (Array.isArray(value)) return value.map((v) => roundNumbers(v));
|
|
221
|
+
if (value && typeof value === "object") {
|
|
222
|
+
const out = {};
|
|
223
|
+
for (const [k, v] of Object.entries(value)) {
|
|
224
|
+
out[k] = roundNumbers(v);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
function stableStringify(value) {
|
|
231
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
232
|
+
const sortDeep = (v) => {
|
|
233
|
+
if (Array.isArray(v)) return v.map(sortDeep);
|
|
234
|
+
if (v && typeof v === "object") {
|
|
235
|
+
if (seen.has(v)) throw new Error("Cyclic reference in EventStormingBoard.");
|
|
236
|
+
seen.add(v);
|
|
237
|
+
const out = {};
|
|
238
|
+
for (const key of Object.keys(v).sort()) {
|
|
239
|
+
out[key] = sortDeep(v[key]);
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
return v;
|
|
244
|
+
};
|
|
245
|
+
return JSON.stringify(sortDeep(value), null, 2) + "\n";
|
|
246
|
+
}
|
|
247
|
+
function createEmptyBoard(title = "Untitled Board") {
|
|
248
|
+
return {
|
|
249
|
+
schemaVersion: CURRENT_SCHEMA_VERSION,
|
|
250
|
+
config: { title },
|
|
251
|
+
elements: [],
|
|
252
|
+
edges: []
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
exports.ATTACHABLE_STICKY_KINDS = ATTACHABLE_STICKY_KINDS;
|
|
257
|
+
exports.CURRENT_SCHEMA_VERSION = CURRENT_SCHEMA_VERSION;
|
|
258
|
+
exports.DEFAULT_BOARD_LEVEL = DEFAULT_BOARD_LEVEL;
|
|
259
|
+
exports.DEFAULT_BOARD_SIZE = DEFAULT_BOARD_SIZE;
|
|
260
|
+
exports.HOST_STICKY_KINDS = HOST_STICKY_KINDS;
|
|
261
|
+
exports.LEVEL_STICKY_KINDS = LEVEL_STICKY_KINDS;
|
|
262
|
+
exports.boardEdgeSchema = boardEdgeSchema;
|
|
263
|
+
exports.boardElementSchema = boardElementSchema;
|
|
264
|
+
exports.createEmptyBoard = createEmptyBoard;
|
|
265
|
+
exports.eventStormingBoardSchema = eventStormingBoardSchema;
|
|
266
|
+
exports.loadBoard = loadBoard;
|
|
267
|
+
exports.migrate = migrate;
|
|
268
|
+
exports.parseBoardJSON = parseBoardJSON;
|
|
269
|
+
exports.serializeBoard = serializeBoard;
|
|
270
|
+
exports.sortByTimeline = sortByTimeline;
|
|
271
|
+
exports.validateBoard = validateBoard;
|
|
272
|
+
//# sourceMappingURL=index.cjs.map
|
|
273
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/layout.ts","../src/levels.ts","../src/attachments.ts","../src/schema.ts","../src/migrations.ts","../src/serialize.ts"],"names":["z"],"mappings":"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","file":"index.cjs","sourcesContent":["/** Pure layout helpers for the free Event Storming canvas (no persistence, no DOM). */\n\nimport type { BoardElement, EventStormingBoard } from './types.js';\n\n/**\n * Default framing for an empty board in board pixels. Single source of truth for the\n * renderer's board-bounds service (fitView and SVG export fall back to it when the\n * board has no elements).\n */\nexport const DEFAULT_BOARD_SIZE = { width: 1080, height: 680 } as const;\n\n/**\n * Elements in timeline order: sorted by `position.x`, then `position.y`, then `id`.\n * Pure and deterministic — mirrors how a facilitator reads a board left to right.\n */\nexport function sortByTimeline(board: EventStormingBoard): BoardElement[] {\n return [...board.elements].sort(\n (a, b) =>\n a.position.x - b.position.x || a.position.y - b.position.y || a.id.localeCompare(b.id),\n );\n}\n","/** Workshop-level constants (DOM-free single source of truth for all creation surfaces). */\n\nimport type { BoardLevel, ElementType } from './types.js';\n\n/** Effective level when `config.level` is absent — everything available. */\nexport const DEFAULT_BOARD_LEVEL: BoardLevel = 'design';\n\n/**\n * Sticky kinds offered per level (palette, typed append, change-type popup).\n * `note` and `drawing` are annotations — always available, hence not listed here.\n */\nexport const LEVEL_STICKY_KINDS: Record<BoardLevel, readonly ElementType[]> = {\n 'big-picture': ['event', 'actor', 'external', 'hotspot'],\n process: ['event', 'command', 'actor', 'policy', 'readmodel', 'external', 'hotspot'],\n design: ['event', 'command', 'actor', 'aggregate', 'policy', 'readmodel', 'external', 'hotspot'],\n};\n","/** Attachment (pinning) constants — DOM-free single source of truth for model, DSL and renderer. */\n\nimport type { ElementType } from './types.js';\n\n/** Element kinds that can be pinned onto a host sticky and then move with it. */\nexport const ATTACHABLE_STICKY_KINDS = [\n 'actor',\n 'hotspot',\n 'note',\n] as const satisfies readonly ElementType[];\n\n/**\n * Sticky kinds that can carry attachments. Attachable kinds and drawings are never\n * hosts — so attach chains cannot exist.\n */\nexport const HOST_STICKY_KINDS = [\n 'event',\n 'command',\n 'aggregate',\n 'policy',\n 'readmodel',\n 'external',\n] as const satisfies readonly ElementType[];\n","import { z } from 'zod';\n\n/**\n * Zod schemas mirror the metamodel (types.ts) and form the runtime validation gate.\n * Coordinates are unbounded board pixels; unique IDs and edge endpoints referencing\n * existing elements are cross-field validated in `validateBoard`.\n */\n\nconst coordinateSchema = z.object({\n x: z.number(),\n y: z.number(),\n});\n\nconst baseFields = {\n id: z.string().min(1),\n label: z.string(),\n position: coordinateSchema,\n color: z.string().optional(),\n};\n\nconst domainEventSchema = z.object({ ...baseFields, elementType: z.literal('event') });\n\nconst commandSchema = z.object({ ...baseFields, elementType: z.literal('command') });\n\n// Pinning: only actor/hotspot/note may carry `attachedTo` (host existence/kind checked in validateBoard).\nconst attachedTo = z.string().min(1).optional();\n\nconst actorSchema = z.object({ ...baseFields, elementType: z.literal('actor'), attachedTo });\n\nconst aggregateSchema = z.object({ ...baseFields, elementType: z.literal('aggregate') });\n\nconst policySchema = z.object({ ...baseFields, elementType: z.literal('policy') });\n\nconst readModelSchema = z.object({ ...baseFields, elementType: z.literal('readmodel') });\n\nconst externalSystemSchema = z.object({ ...baseFields, elementType: z.literal('external') });\n\nconst hotspotSchema = z.object({ ...baseFields, elementType: z.literal('hotspot'), attachedTo });\n\n// Manual resize override — absent = auto-size from text (decided in the renderer).\nconst noteSizeSchema = z.object({\n width: z.number().positive(),\n height: z.number().positive(),\n});\n\n// Per-note text alignment — an absent axis means the default left / top.\nconst noteAlignSchema = z.object({\n horizontal: z.enum(['left', 'center', 'right']).optional(),\n vertical: z.enum(['top', 'middle', 'bottom']).optional(),\n});\n\nconst noteSchema = z.object({\n ...baseFields,\n elementType: z.literal('note'),\n size: noteSizeSchema.optional(),\n align: noteAlignSchema.optional(),\n attachedTo,\n});\n\nconst drawingSchema = z.object({\n ...baseFields,\n elementType: z.literal('drawing'),\n points: z.array(coordinateSchema).min(2),\n closed: z.boolean().optional(),\n strokeStyle: z.enum(['solid', 'dashed', 'dotted']).optional(),\n});\n\nexport const boardElementSchema = z.discriminatedUnion('elementType', [\n domainEventSchema,\n commandSchema,\n actorSchema,\n aggregateSchema,\n policySchema,\n readModelSchema,\n externalSystemSchema,\n hotspotSchema,\n noteSchema,\n drawingSchema,\n]);\n\nconst arrowSchema = z.object({\n id: z.string().min(1),\n edgeType: z.literal('arrow'),\n from: z.string(),\n to: z.string(),\n label: z.string().optional(),\n});\n\nexport const boardEdgeSchema = z.discriminatedUnion('edgeType', [arrowSchema]);\n\nconst boardConfigSchema = z.object({\n title: z.string(),\n style: z.enum(['classic', 'dark']).optional(),\n level: z.enum(['big-picture', 'process', 'design']).optional(),\n});\n\nexport const eventStormingBoardSchema = z.object({\n schemaVersion: z.number().int().positive(),\n config: boardConfigSchema,\n elements: z.array(boardElementSchema),\n edges: z.array(boardEdgeSchema),\n rawPassthrough: z.array(z.string()).optional(),\n});\n\nexport type EventStormingBoardInput = z.input<typeof eventStormingBoardSchema>;\n","/**\n * Schema migrations as an ordered chain of pure functions.\n * `migrate(json)` reads `schemaVersion`, applies all necessary steps and returns the\n * object raised to the current version (not yet validated).\n */\n\nexport const CURRENT_SCHEMA_VERSION = 1;\n\ntype Json = Record<string, unknown>;\n\n/** Migration from version N to N+1. Index 0 = (v1 -> v2), etc. */\nconst MIGRATIONS: ReadonlyArray<(json: Json) => Json> = [\n // No migrations yet — v1 is the starting version.\n];\n\nexport function migrate(input: unknown): Json {\n if (typeof input !== 'object' || input === null) {\n throw new Error('EventStormingBoard must be an object.');\n }\n const obj = { ...(input as Json) };\n const rawVersion = obj['schemaVersion'];\n const version = typeof rawVersion === 'number' ? rawVersion : 1;\n\n if (version > CURRENT_SCHEMA_VERSION) {\n throw new Error(\n `Unknown schemaVersion ${version} (supported up to ${CURRENT_SCHEMA_VERSION}). ` +\n 'Please update the tool.',\n );\n }\n if (version < 1) {\n throw new Error(`Invalid schemaVersion ${version}.`);\n }\n\n let current: Json = { ...obj, schemaVersion: version };\n for (let v = version; v < CURRENT_SCHEMA_VERSION; v++) {\n const step = MIGRATIONS[v - 1];\n if (!step) throw new Error(`Missing migration for version ${v}.`);\n current = { ...step(current), schemaVersion: v + 1 };\n }\n return current;\n}\n","import { eventStormingBoardSchema } from './schema.js';\nimport { migrate, CURRENT_SCHEMA_VERSION } from './migrations.js';\nimport { HOST_STICKY_KINDS } from './attachments.js';\nimport type { BoardConfig, EventStormingBoard } from './types.js';\n\n/** Number of decimal places for coordinates in serialization. */\nconst COORD_PRECISION = 3;\n\n// The 8 sticky kinds — the only legal arrow endpoints. Notes/drawings are annotations; the\n// renderer's connection rules forbid them and the DSL cannot reference them, so accepting\n// such edges here would mean silent loss on the next DSL round-trip.\nconst CONNECTABLE_TYPES: ReadonlySet<string> = new Set([\n 'event',\n 'command',\n 'actor',\n 'aggregate',\n 'policy',\n 'readmodel',\n 'external',\n 'hotspot',\n]);\n\nconst HOST_TYPES: ReadonlySet<string> = new Set(HOST_STICKY_KINDS);\n\nfunction round(n: number, digits = COORD_PRECISION): number {\n const f = 10 ** digits;\n return Math.round(n * f) / f;\n}\n\n/**\n * Validates arbitrary data against the schema plus additional cross-field invariants\n * (unique IDs, edge endpoints exist and are sticky kinds). Throws on violation.\n */\nexport function validateBoard(data: unknown): EventStormingBoard {\n const parsed = eventStormingBoardSchema.parse(data);\n\n const ids = new Set<string>();\n const typeById = new Map<string, string>();\n for (const el of parsed.elements) {\n if (ids.has(el.id)) throw new Error(`Duplicate element id: ${el.id}`);\n ids.add(el.id);\n typeById.set(el.id, el.elementType);\n }\n\n // Pinning: the host must exist and be of a host kind — actor/hotspot/note/drawing are never\n // hosts (no attach chains), which also rules out self-attachment.\n for (const el of parsed.elements) {\n if (!('attachedTo' in el) || el.attachedTo === undefined) continue;\n if (!ids.has(el.attachedTo)) {\n throw new Error(`Element ${el.id}: attachedTo \"${el.attachedTo}\" references no element.`);\n }\n if (!HOST_TYPES.has(typeById.get(el.attachedTo)!)) {\n throw new Error(\n `Element ${el.id}: attachedTo \"${el.attachedTo}\" is a ${typeById.get(el.attachedTo)} — actors/hotspots/notes may only attach to host stickies.`,\n );\n }\n }\n\n // Shared ID namespace: diagram-js' ElementRegistry has only ONE namespace for\n // shapes and connections — an edge with an element ID would crash the import midway.\n const edgeIds = new Set<string>();\n for (const edge of parsed.edges) {\n if (edgeIds.has(edge.id)) throw new Error(`Duplicate edge id: ${edge.id}`);\n if (ids.has(edge.id)) throw new Error(`Edge id collides with element id: ${edge.id}`);\n edgeIds.add(edge.id);\n if (!ids.has(edge.from)) {\n throw new Error(`Edge ${edge.id}: source \"${edge.from}\" references no element.`);\n }\n if (!ids.has(edge.to)) {\n throw new Error(`Edge ${edge.id}: target \"${edge.to}\" references no element.`);\n }\n if (!CONNECTABLE_TYPES.has(typeById.get(edge.from)!)) {\n throw new Error(\n `Edge ${edge.id}: source \"${edge.from}\" is a ${typeById.get(edge.from)} — arrows may only connect stickies.`,\n );\n }\n if (!CONNECTABLE_TYPES.has(typeById.get(edge.to)!)) {\n throw new Error(\n `Edge ${edge.id}: target \"${edge.to}\" is a ${typeById.get(edge.to)} — arrows may only connect stickies.`,\n );\n }\n }\n\n return parsed as unknown as EventStormingBoard;\n}\n\nexport function loadBoard(data: unknown): EventStormingBoard {\n return validateBoard(migrate(data));\n}\n\nexport function parseBoardJSON(json: string): EventStormingBoard {\n return loadBoard(JSON.parse(json) as unknown);\n}\n\n/**\n * Deterministic serialization: stable (alphabetical) key order, elements/edges sorted by `id`,\n * coordinates rounded to 3 decimal places. Produces clean Git diffs and reliable change\n * detection.\n */\nexport function serializeBoard(board: EventStormingBoard): string {\n return stableStringify(canonicalize(board));\n}\n\nfunction canonicalize(board: EventStormingBoard): EventStormingBoard {\n const elements = [...board.elements]\n .sort((a, b) => a.id.localeCompare(b.id))\n .map((el) => roundNumbers(el) as EventStormingBoard['elements'][number]);\n const edges = [...board.edges].sort((a, b) => a.id.localeCompare(b.id));\n return {\n ...board,\n config: roundNumbers(board.config) as BoardConfig,\n elements,\n edges,\n };\n}\n\nfunction roundNumbers<T>(value: T): T {\n if (typeof value === 'number') return round(value) as unknown as T;\n if (Array.isArray(value)) return value.map((v) => roundNumbers(v)) as unknown as T;\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = roundNumbers(v);\n }\n return out as T;\n }\n return value;\n}\n\nfunction stableStringify(value: unknown): string {\n const seen = new WeakSet<object>();\n const sortDeep = (v: unknown): unknown => {\n if (Array.isArray(v)) return v.map(sortDeep);\n if (v && typeof v === 'object') {\n if (seen.has(v as object)) throw new Error('Cyclic reference in EventStormingBoard.');\n seen.add(v as object);\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(v as Record<string, unknown>).sort()) {\n out[key] = sortDeep((v as Record<string, unknown>)[key]);\n }\n return out;\n }\n return v;\n };\n return JSON.stringify(sortDeep(value), null, 2) + '\\n';\n}\n\nexport function createEmptyBoard(title = 'Untitled Board'): EventStormingBoard {\n return {\n schemaVersion: CURRENT_SCHEMA_VERSION,\n config: { title },\n elements: [],\n edges: [],\n };\n}\n"]}
|