@formicoidea/labre-framework-bpmn 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.
Files changed (47) hide show
  1. package/dist/actions.d.ts +202 -6
  2. package/dist/actions.js +421 -43
  3. package/dist/background.d.ts +2 -0
  4. package/dist/background.js +158 -0
  5. package/dist/commands.js +496 -5
  6. package/dist/consts.d.ts +157 -3
  7. package/dist/consts.js +192 -3
  8. package/dist/element-renderer.d.ts +10 -4
  9. package/dist/element-renderer.js +14 -55
  10. package/dist/element-view.d.ts +100 -8
  11. package/dist/element-view.js +249 -30
  12. package/dist/export.d.ts +277 -0
  13. package/dist/export.js +1802 -0
  14. package/dist/facts.d.ts +48 -0
  15. package/dist/facts.js +127 -0
  16. package/dist/import.d.ts +44 -0
  17. package/dist/import.js +1440 -0
  18. package/dist/index.d.ts +12 -0
  19. package/dist/index.js +44 -0
  20. package/dist/interchange.d.ts +109 -0
  21. package/dist/interchange.js +191 -0
  22. package/dist/morph.d.ts +61 -0
  23. package/dist/morph.js +118 -0
  24. package/dist/node/node-renderer.d.ts +0 -9
  25. package/dist/node/node-renderer.js +294 -17
  26. package/dist/pool-hit.d.ts +98 -0
  27. package/dist/pool-hit.js +130 -0
  28. package/dist/presets.d.ts +114 -0
  29. package/dist/presets.js +232 -0
  30. package/dist/profiles.d.ts +2 -0
  31. package/dist/profiles.js +189 -0
  32. package/dist/roles.d.ts +96 -0
  33. package/dist/roles.js +410 -0
  34. package/dist/rules.d.ts +199 -0
  35. package/dist/rules.js +1539 -0
  36. package/dist/templates/index.js +116 -9
  37. package/dist/toolbar/bpmn-senior-button.js +8 -2
  38. package/dist/toolbar/config.d.ts +27 -2
  39. package/dist/toolbar/config.js +86 -2
  40. package/dist/toolbar/icons.d.ts +67 -0
  41. package/dist/toolbar/icons.js +141 -0
  42. package/dist/toolbar/senior-tool.js +1 -0
  43. package/dist/translations.d.ts +3 -1
  44. package/dist/translations.js +8 -3
  45. package/dist/view.d.ts +6 -2
  46. package/dist/view.js +68 -5
  47. package/package.json +2 -2
package/dist/commands.js CHANGED
@@ -1,11 +1,44 @@
1
- import { activateBpmnSequenceFlow, createBpmnNode, createBpmnPool, } from './actions.js';
2
- import { bpmnEndIcon, bpmnGatewayIcon, bpmnPoolIcon, bpmnSequenceIcon, bpmnStartIcon, bpmnTaskIcon, } from './toolbar/icons.js';
1
+ import { activateBpmnAssociation, activateBpmnMessageFlow, activateBpmnSequenceFlow, addBpmnLane, bpmnLanesOf, bpmnPoolsForLaneEdit, bpmnPoolsSelected, createBpmnNode, createBpmnPool, exportBpmnXmlFile, importBpmnSvgFile, importBpmnXmlFile, removeBpmnLane, } from './actions.js';
2
+ import { bpmnAssociationIcon, bpmnCallActivityIcon, bpmnDataObjectIcon, bpmnDataStoreIcon, bpmnEndIcon, bpmnEndMessageIcon, bpmnEndTerminateIcon, bpmnExportXmlIcon, bpmnGatewayIcon, bpmnGatewayParallelIcon, bpmnGroupIcon, bpmnImportSvgIcon, bpmnImportXmlIcon, bpmnLaneAddIcon, bpmnLaneRemoveIcon, bpmnMessageIcon, bpmnPoolIcon, bpmnSequenceIcon, bpmnStartIcon, bpmnStartMessageIcon, bpmnStartTimerIcon, bpmnSubProcessIcon, bpmnTaskIcon, bpmnTaskServiceIcon, bpmnTaskUserIcon, bpmnTextAnnotationIcon, } from './toolbar/icons.js';
3
+ /**
4
+ * Declaration order is DISPLAY order, and past the cap it is also the COLD
5
+ * START — which is why it leads with the canonical core rather than with the
6
+ * events.
7
+ *
8
+ * `selectSeniorMenuCommands` falls back to the authored head of the NOMINATED
9
+ * list for a user who has invoked nothing yet. The row held seven when the
10
+ * recette caught this, and an order grouped strictly by family spent all seven
11
+ * on events: start, three of its variants, two ends, task. A first contact with
12
+ * BPMN with no gateway, no sequence flow and no pool — every button drawing a
13
+ * circle, and nothing to connect them with. Caught in a live recette, and it is
14
+ * pure data.
15
+ *
16
+ * The first seven are therefore the seven artefacts a process cannot be drawn
17
+ * without: start, end, task, exclusive gateway, sequence flow, pool, message
18
+ * flow. Everything else follows in family blocks. The row seats thirteen since
19
+ * 2026-08-28, which makes the head wider but not less load-bearing: a framework
20
+ * whose declarations opened on six variants of one family would still meet a
21
+ * user with a lopsided first contact. Nothing about position STABILITY changes
22
+ * — author order is still the position law, and usage only ever changes
23
+ * membership, never where a button sits (`docs/adr/0008`, amendments of
24
+ * 2026-08-26 and 2026-08-28).
25
+ *
26
+ * The catalogue reads off the same order, so its headers now appear in
27
+ * first-encounter order — events, activities, gateways, flows, swimlanes, data,
28
+ * annotations — and the entries inside each keep author order. Both are better
29
+ * reading than the strict grouping was: swimlanes climbs to where a pool
30
+ * belongs, and the events section opens on the plain start and end rather than
31
+ * burying them under their own variants.
32
+ */
3
33
  const SPECS = [
34
+ /* ── The core: a drawable process, from the first click ─────────────── */
4
35
  {
5
36
  id: 'addStartEvent',
6
37
  label: 'Start event',
7
38
  iconKey: 'bpmn.start',
8
39
  kind: 'artefact',
40
+ category: 'events',
41
+ senior: true,
9
42
  element: 'node:startEvent',
10
43
  run: std => createBpmnNode(std, 'startEvent'),
11
44
  },
@@ -14,6 +47,8 @@ const SPECS = [
14
47
  label: 'End event',
15
48
  iconKey: 'bpmn.end',
16
49
  kind: 'artefact',
50
+ category: 'events',
51
+ senior: true,
17
52
  element: 'node:endEvent',
18
53
  run: std => createBpmnNode(std, 'endEvent'),
19
54
  },
@@ -22,6 +57,8 @@ const SPECS = [
22
57
  label: 'Task',
23
58
  iconKey: 'bpmn.task',
24
59
  kind: 'artefact',
60
+ category: 'activities',
61
+ senior: true,
25
62
  element: 'node:task',
26
63
  run: std => createBpmnNode(std, 'task'),
27
64
  },
@@ -30,6 +67,8 @@ const SPECS = [
30
67
  label: 'Exclusive gateway',
31
68
  iconKey: 'bpmn.gateway',
32
69
  kind: 'artefact',
70
+ category: 'gateways',
71
+ senior: true,
33
72
  element: 'node:gatewayExclusive',
34
73
  run: std => createBpmnNode(std, 'gatewayExclusive'),
35
74
  },
@@ -38,6 +77,8 @@ const SPECS = [
38
77
  label: 'Sequence flow',
39
78
  iconKey: 'bpmn.sequence',
40
79
  kind: 'tool',
80
+ category: 'flows',
81
+ senior: true,
41
82
  element: 'connector:sequence',
42
83
  run: activateBpmnSequenceFlow,
43
84
  },
@@ -46,19 +87,195 @@ const SPECS = [
46
87
  label: 'Pool',
47
88
  iconKey: 'bpmn.pool',
48
89
  kind: 'artefact',
90
+ category: 'swimlanes',
91
+ senior: true,
49
92
  element: 'pool',
50
93
  run: createBpmnPool,
51
94
  },
95
+ {
96
+ // Seventh, and the last of the cold start: the moment there are two
97
+ // participants there is a message between them, and it is the one arrow a
98
+ // sequence flow may never stand in for.
99
+ id: 'messageFlowTool',
100
+ label: 'Message flow',
101
+ iconKey: 'bpmn.message',
102
+ kind: 'tool',
103
+ category: 'flows',
104
+ senior: true,
105
+ element: 'connector:message',
106
+ run: activateBpmnMessageFlow,
107
+ },
108
+ /* ── Activities: the typed tasks and the two that stand for a process ─ */
109
+ {
110
+ id: 'addUserTask',
111
+ label: 'User task',
112
+ iconKey: 'bpmn.task.user',
113
+ kind: 'artefact',
114
+ category: 'activities',
115
+ senior: true,
116
+ element: 'node:taskUser',
117
+ run: std => createBpmnNode(std, 'taskUser'),
118
+ },
119
+ {
120
+ id: 'addServiceTask',
121
+ label: 'Service task',
122
+ iconKey: 'bpmn.task.service',
123
+ kind: 'artefact',
124
+ category: 'activities',
125
+ senior: true,
126
+ element: 'node:taskService',
127
+ run: std => createBpmnNode(std, 'taskService'),
128
+ },
129
+ {
130
+ id: 'addSubProcess',
131
+ label: 'Sub-process',
132
+ iconKey: 'bpmn.sub-process',
133
+ kind: 'artefact',
134
+ category: 'activities',
135
+ senior: true,
136
+ element: 'node:subProcess',
137
+ run: std => createBpmnNode(std, 'subProcess'),
138
+ },
139
+ {
140
+ id: 'addCallActivity',
141
+ label: 'Call activity',
142
+ iconKey: 'bpmn.call-activity',
143
+ kind: 'artefact',
144
+ category: 'activities',
145
+ senior: true,
146
+ element: 'node:callActivity',
147
+ run: std => createBpmnNode(std, 'callActivity'),
148
+ },
149
+ /* ── The other gateway ──────────────────────────────────────────────── */
150
+ {
151
+ // `addParallelGateway`, matching the word order of the sibling that already
152
+ // shipped (`addExclusiveGateway`) — a command id is a value a host override
153
+ // table points at, and the pair reads as a pair in every list that sorts.
154
+ id: 'addParallelGateway',
155
+ label: 'Parallel gateway',
156
+ iconKey: 'bpmn.gateway.parallel',
157
+ kind: 'artefact',
158
+ category: 'gateways',
159
+ senior: true,
160
+ element: 'node:gatewayParallel',
161
+ run: std => createBpmnNode(std, 'gatewayParallel'),
162
+ },
163
+ /* ── Event variants: what TRIGGERS a start, what an end does on the way
164
+ out. Refinements of the two plain events above, so they follow them. ─ */
165
+ {
166
+ id: 'addMessageStartEvent',
167
+ label: 'Message start event',
168
+ iconKey: 'bpmn.start.message',
169
+ kind: 'artefact',
170
+ category: 'events',
171
+ senior: false,
172
+ element: 'node:startEventMessage',
173
+ run: std => createBpmnNode(std, 'startEventMessage'),
174
+ },
175
+ {
176
+ id: 'addTimerStartEvent',
177
+ label: 'Timer start event',
178
+ iconKey: 'bpmn.start.timer',
179
+ kind: 'artefact',
180
+ category: 'events',
181
+ senior: false,
182
+ element: 'node:startEventTimer',
183
+ run: std => createBpmnNode(std, 'startEventTimer'),
184
+ },
185
+ {
186
+ id: 'addMessageEndEvent',
187
+ label: 'Message end event',
188
+ iconKey: 'bpmn.end.message',
189
+ kind: 'artefact',
190
+ category: 'events',
191
+ senior: false,
192
+ element: 'node:endEventMessage',
193
+ run: std => createBpmnNode(std, 'endEventMessage'),
194
+ },
195
+ {
196
+ id: 'addTerminateEndEvent',
197
+ label: 'Terminate end event',
198
+ iconKey: 'bpmn.end.terminate',
199
+ kind: 'artefact',
200
+ category: 'events',
201
+ senior: false,
202
+ element: 'node:endEventTerminate',
203
+ run: std => createBpmnNode(std, 'endEventTerminate'),
204
+ },
205
+ /* ── The last connecting object ─────────────────────────────────────── */
206
+ {
207
+ id: 'associationTool',
208
+ label: 'Association',
209
+ iconKey: 'bpmn.association',
210
+ kind: 'tool',
211
+ category: 'flows',
212
+ senior: false,
213
+ element: 'connector:association',
214
+ run: activateBpmnAssociation,
215
+ },
216
+ /* ── Data ───────────────────────────────────────────────────────────── */
217
+ {
218
+ id: 'addDataObject',
219
+ label: 'Data object',
220
+ iconKey: 'bpmn.data-object',
221
+ kind: 'artefact',
222
+ category: 'data',
223
+ senior: true,
224
+ element: 'node:dataObject',
225
+ run: std => createBpmnNode(std, 'dataObject'),
226
+ },
227
+ {
228
+ id: 'addDataStore',
229
+ label: 'Data store',
230
+ iconKey: 'bpmn.data-store',
231
+ kind: 'artefact',
232
+ category: 'data',
233
+ senior: false,
234
+ element: 'node:dataStore',
235
+ run: std => createBpmnNode(std, 'dataStore'),
236
+ },
237
+ /* ── Artifacts: what an author writes ON the picture ────────────────── */
238
+ {
239
+ id: 'addTextAnnotation',
240
+ label: 'Text annotation',
241
+ iconKey: 'bpmn.text-annotation',
242
+ kind: 'artefact',
243
+ category: 'annotations',
244
+ senior: true,
245
+ element: 'node:textAnnotation',
246
+ run: std => createBpmnNode(std, 'textAnnotation'),
247
+ },
248
+ {
249
+ // Filed with the annotation and not in a section of its own: both are
250
+ // things an author writes ON the picture rather than parts of the process,
251
+ // and BPMN 2.0.2 §10.4 exempts both from every rule for the same reason.
252
+ id: 'addGroup',
253
+ label: 'Group',
254
+ iconKey: 'bpmn.group',
255
+ kind: 'artefact',
256
+ category: 'annotations',
257
+ // Out of the fourteen, and not because it is unimportant: a lasso is drawn
258
+ // round a part of the process that is already there, so it is never the
259
+ // first thing reached for on a blank board — which is precisely what the
260
+ // senior row is a shortcut to.
261
+ senior: false,
262
+ element: 'node:group',
263
+ run: std => createBpmnNode(std, 'group'),
264
+ },
52
265
  ];
53
- export const bpmnCommands = SPECS.map((spec, order) => ({
266
+ const toolboxCommands = SPECS.map((spec, order) => ({
54
267
  id: `bpmn.${spec.id}`,
55
268
  owner: 'bpmn',
56
269
  kind: spec.kind,
57
270
  labelKey: `com.labre.commands.bpmn.${spec.id}`,
58
271
  labelFallback: spec.label,
59
- category: 'flow',
272
+ category: spec.category,
60
273
  iconKey: spec.iconKey,
61
- surfaces: ['senior-menu', 'catalogue', 'palette', 'agent'],
274
+ // The catalogue holds all of them; `senior` decides which fourteen the
275
+ // sub-menu opens on before this user has reached for anything.
276
+ surfaces: spec.senior
277
+ ? ['senior-menu', 'catalogue', 'palette', 'agent']
278
+ : ['catalogue', 'palette', 'agent'],
62
279
  order,
63
280
  scope: 'edgeless',
64
281
  defaultKeys: { mac: [], other: [] },
@@ -66,11 +283,285 @@ export const bpmnCommands = SPECS.map((spec, order) => ({
66
283
  run: spec.run,
67
284
  telemetry: { framework: 'bpmn', element: spec.element },
68
285
  }));
286
+ /**
287
+ * The LANE commands (B4) — the first BPMN entries that are not a toolbox slot.
288
+ *
289
+ * A lane is a subdivision of a pool that already exists, so there is nothing to
290
+ * pick up off a palette and drop: these act on a SELECTION. That is why they
291
+ * decline `'senior-menu'` — a permanently greyed entry in the sub-menu of a
292
+ * framework you have not drawn anything with yet is furniture, not an
293
+ * affordance. They keep `'catalogue'`, which every framework command carries,
294
+ * so a host catalogue and Settings › Shortcuts still list them, and they join
295
+ * `'contextual-toolbar'`, whose entry is declared by the pool's own
296
+ * `ToolbarModuleConfig` and INVOKES these — one behaviour, one availability
297
+ * rule, one telemetry emission (`docs/adr/0008`, `docs/adr/0010` M3).
298
+ */
299
+ const LANE_COMMON = {
300
+ owner: 'bpmn',
301
+ kind: 'action',
302
+ // The same section as the pool they divide: the catalogue reads "here is the
303
+ // frame, and here is what you do to it" in one place, which is where a reader
304
+ // looking for lanes actually looks.
305
+ category: 'swimlanes',
306
+ surfaces: ['catalogue', 'contextual-toolbar', 'palette', 'agent'],
307
+ scope: 'edgeless',
308
+ // Keyless by intent — still bindable from Settings › Shortcuts, which is what
309
+ // `toShortcutDescriptor` being total buys.
310
+ defaultKeys: { mac: [], other: [] },
311
+ availability: 'selection',
312
+ };
313
+ const laneCommands = [
314
+ {
315
+ ...LANE_COMMON,
316
+ id: 'bpmn.addLane',
317
+ labelKey: 'com.labre.commands.bpmn.addLane',
318
+ labelFallback: 'Add lane',
319
+ descriptionKey: 'com.labre.commands.bpmn.addLane.description',
320
+ descriptionFallback: 'Divide the selected pool into lanes; the new one takes an equal share.',
321
+ iconKey: 'bpmn.lane-add',
322
+ // Ranked after every toolbox entry, so the catalogue reads "here is what
323
+ // BPMN draws" before "here is what you do to it". They still land in the
324
+ // `swimlanes` section beside the pool — a section is where a command is
325
+ // FILED, and `order` only decides where it sits inside it.
326
+ order: SPECS.length,
327
+ run: addBpmnLane,
328
+ telemetry: { framework: 'bpmn', element: 'pool:lane-add' },
329
+ // Narrows `'selection'`, never contradicts it: a selection holding no
330
+ // unlocked pool has nothing to divide. Read-only rides in
331
+ // `bpmnPoolsForLaneEdit` for the reason the union documents — it holds ONE
332
+ // value, and `'selection'` is the precondition a catalogue has to show.
333
+ when: std => bpmnPoolsForLaneEdit(std).length > 0,
334
+ },
335
+ {
336
+ ...LANE_COMMON,
337
+ id: 'bpmn.removeLane',
338
+ labelKey: 'com.labre.commands.bpmn.removeLane',
339
+ labelFallback: 'Remove lane',
340
+ descriptionKey: 'com.labre.commands.bpmn.removeLane.description',
341
+ descriptionFallback: 'Remove the last lane of the selected pool. Nothing drawn in it moves.',
342
+ iconKey: 'bpmn.lane-remove',
343
+ order: SPECS.length + 1,
344
+ run: removeBpmnLane,
345
+ telemetry: { framework: 'bpmn', element: 'pool:lane-remove' },
346
+ // …and additionally: a pool with no lane has none to remove.
347
+ when: std => bpmnPoolsForLaneEdit(std).some(model => bpmnLanesOf(model).length > 0),
348
+ },
349
+ ];
350
+ /**
351
+ * The EXPORT — the first BPMN command whose subject is the whole board.
352
+ *
353
+ * ## Why it hangs off the pool's toolbar and still exports everything
354
+ *
355
+ * A pool is the only thing on the canvas that is unambiguously "this drawing is
356
+ * a BPMN process", so it is where a reader looks for what to do with one. What
357
+ * it is NOT is the scope: a BPMN document is a process, half a process is not a
358
+ * smaller process, and a file holding one participant of a two-participant
359
+ * collaboration would be a picture of a conversation with one side deleted. The
360
+ * selected pool decides the FILENAME and nothing else — see
361
+ * {@link exportBpmnXmlFile}.
362
+ *
363
+ * ## Surfaces
364
+ *
365
+ * It declines `'senior-menu'` for the same reason the lane gestures do: the
366
+ * sub-menu is what you reach for to DRAW something, and this draws nothing. It
367
+ * keeps `'catalogue'`, which is not a category claim but the registry's own
368
+ * invariant — the catalogue is the TOTAL surface, and a command missing from it
369
+ * is unreachable the moment its framework overflows the fourteen slots (pinned
370
+ * by `registry.unit.spec.ts`). On the row itself it sits in the "⋮" menu rather
371
+ * than as a button: it is the rarest thing anybody does to a pool, and the row
372
+ * is already three entries wide.
373
+ */
374
+ const exportCommand = {
375
+ id: 'bpmn.exportXml',
376
+ owner: 'bpmn',
377
+ kind: 'action',
378
+ labelKey: 'com.labre.commands.bpmn.exportXml',
379
+ labelFallback: 'Export BPMN XML',
380
+ descriptionKey: 'com.labre.commands.bpmn.exportXml.description',
381
+ descriptionFallback: 'Download the whole board as a BPMN 2.0 XML file, ready to open in any BPMN tool.',
382
+ // Filed with the import it is the other half of — see {@link INTERCHANGE}.
383
+ // It shipped filed under `swimlanes`, because the pool's "⋮" is where it is
384
+ // REACHED from and there was no better section for a command with no sibling;
385
+ // a category is where a command is FILED, and it has one now.
386
+ category: 'interchange',
387
+ iconKey: 'bpmn.export-xml',
388
+ surfaces: ['catalogue', 'contextual-toolbar', 'palette', 'agent'],
389
+ order: SPECS.length + 2,
390
+ scope: 'edgeless',
391
+ defaultKeys: { mac: [], other: [] },
392
+ availability: 'selection',
393
+ run: exportBpmnXmlFile,
394
+ telemetry: { framework: 'bpmn', element: 'pool:export-xml' },
395
+ // A pool in the selection, and no more than that: an export READS, so unlike
396
+ // the lane gestures it is offered on a locked pool and on a read-only
397
+ // document — which is precisely the board somebody wants to take away.
398
+ when: std => bpmnPoolsSelected(std).length > 0,
399
+ };
400
+ /**
401
+ * The IMPORT — the other direction of the same format, and the first BPMN
402
+ * command that needs nothing on the board at all.
403
+ *
404
+ * ## `interchange`, and why the export moved into it
405
+ *
406
+ * These two are one subject: this board as a `.bpmn` file, out and in. The
407
+ * export was filed under `swimlanes` because that is the toolbar it is reached
408
+ * from and because a section of one is not a section; a category is where a
409
+ * command is FILED and not where it is reached from, and filing the pair apart
410
+ * would make the catalogue say that taking a process away and bringing one back
411
+ * are different kinds of thing. `order` is untouched, so nothing MOVES inside a
412
+ * section — the position law of `docs/adr/0008` is about rank, and this is
413
+ * membership.
414
+ *
415
+ * ## Surfaces: four, and the one it declines
416
+ *
417
+ * `'senior-menu'` since the PO decision of 2026-08-28, which REVERSES the
418
+ * ruling this comment used to carry ("the sub-menu is a row of things you DRAW,
419
+ * and this draws nothing you chose"). The distinction survives for the EXPORT,
420
+ * which is still catalogue-and-toolbar only: an export is what you do to a
421
+ * board you already have, and it is reached from the pool it is about. An
422
+ * import is where a board COMES FROM. On an empty canvas the sub-menu is the
423
+ * first thing a user opens, and "start from a file somebody sent me" belongs in
424
+ * that row beside "start from a start event" — asking them to find the
425
+ * catalogue sidepanel first was the friction the decision names.
426
+ *
427
+ * The row itself is not at risk: BPMN's catalogue has outgrown the cap since
428
+ * #157, so `selectSeniorMenuCommands` already ranks the nomination list down to
429
+ * thirteen buttons plus "More artefacts…", and this entry takes a slot only
430
+ * when the user actually reaches for it. It is the fifteenth nomination, and
431
+ * nothing about the arbitration changes.
432
+ *
433
+ * No `'contextual-toolbar'`, and that is the difference from the export — a
434
+ * contextual toolbar is a statement about a SELECTION, and the moment this
435
+ * command is most wanted is on an empty board with nothing selected at all. It
436
+ * keeps `'catalogue'` (the registry's total surface — a command missing from it
437
+ * is unreachable), `'palette'` and `'agent'`.
438
+ *
439
+ * ## `'editable'`, which is the first use of it in the repo
440
+ *
441
+ * An import needs no selection — but it WRITES, so a read-only document is a
442
+ * document it cannot run on, and that is a precondition a catalogue has to be
443
+ * able to show. `'editable'` is exactly that value and it has been in the union
444
+ * since `docs/adr/0008` (`Availability`, `command-registry.ts`); nothing had
445
+ * reached for it before. `'always'` would light the entry on a read-only
446
+ * document, do nothing when clicked, and put the same untruth into the
447
+ * serializable manifest a host reads — which is the one thing `availability`
448
+ * exists to prevent.
449
+ *
450
+ * The guard inside {@link importBpmnXmlFile} stays: a declaration is what a
451
+ * surface renders from, and the action is what actually touches the store.
452
+ *
453
+ * The mirror image of the export, which READS and is therefore `'selection'` on
454
+ * a pool and offered on a read-only document precisely because that is the
455
+ * board somebody wants to take away.
456
+ */
457
+ const importCommand = {
458
+ id: 'bpmn.importXml',
459
+ owner: 'bpmn',
460
+ kind: 'action',
461
+ labelKey: 'com.labre.commands.bpmn.importXml',
462
+ labelFallback: 'Import BPMN XML',
463
+ descriptionKey: 'com.labre.commands.bpmn.importXml.description',
464
+ descriptionFallback: 'Open a BPMN 2.0 XML file as a board. What Labre cannot draw is kept in the document, and the import says what it was.',
465
+ category: 'interchange',
466
+ iconKey: 'bpmn.import-xml',
467
+ surfaces: ['senior-menu', 'catalogue', 'palette', 'agent'],
468
+ order: SPECS.length + 3,
469
+ scope: 'edgeless',
470
+ // Keyless by intent, like every other BPMN entry: past fourteen a framework
471
+ // binds by host override rather than by shipping a default chord. Still
472
+ // bindable from Settings › Shortcuts, which is what `toShortcutDescriptor`
473
+ // being total buys.
474
+ defaultKeys: { mac: [], other: [] },
475
+ availability: 'editable',
476
+ run: importBpmnXmlFile,
477
+ // `board:` and not `pool:`: the export names the pool whose toolbar launched
478
+ // it, and this one is launched with no pool anywhere.
479
+ telemetry: { framework: 'bpmn', element: 'board:import-xml' },
480
+ };
481
+ /**
482
+ * The SVG FALLBACK import — the visual tier, named as such before the picker
483
+ * opens.
484
+ *
485
+ * ## Why it is not in the senior sub-menu, and that is an arbitration
486
+ *
487
+ * `bpmn.importXml` took the fifteenth nomination on the PO's ruling of
488
+ * 2026-08-28, and that ruling was about where a BPMN board comes FROM: a file
489
+ * somebody sent you, in the format the framework speaks. This is the other
490
+ * kind of file — a picture of a process, from a tool that does not export
491
+ * `.bpmn` at all — and it lands one click away, in the artefact catalogue
492
+ * behind "More artefacts…", rather than taking a sixteenth slot in a row that
493
+ * seats fourteen. The registry spec's budget assertion allows exactly ONE
494
+ * over-nomination per owner and BPMN has spent it; this entry deliberately does
495
+ * not contest it.
496
+ *
497
+ * **Flagged for the PO**: if the fallback turns out to be what people actually
498
+ * reach for — a `.svg` is what most drawing tools export — this is a one-line
499
+ * change (`surfaces` gains `'senior-menu'`), and it is a curation decision
500
+ * rather than a merge.
501
+ *
502
+ * ## The label says the tier, because P2 requires it BEFORE the file is read
503
+ *
504
+ * "Import SVG sketch", and a description that spends its whole sentence on what
505
+ * this is not: best effort, shapes and text, no round-trip. ADR 0012 is blunt
506
+ * about the cost of getting this wrong — "a single 'Import…' entry that hides
507
+ * the difference would earn a support ticket per user" — and the surface is the
508
+ * only place the difference can be stated, because the report comes AFTER the
509
+ * decision to open the file.
510
+ */
511
+ const importSvgCommand = {
512
+ id: 'bpmn.importSvg',
513
+ owner: 'bpmn',
514
+ kind: 'action',
515
+ labelKey: 'com.labre.commands.bpmn.importSvg',
516
+ labelFallback: 'Import SVG sketch',
517
+ descriptionKey: 'com.labre.commands.bpmn.importSvg.description',
518
+ descriptionFallback: 'Best effort: recognises shapes and text, no round-trip. What arrives is a sketch you then promote into BPMN artefacts.',
519
+ // Filed with the two `.bpmn` directions: the subject is the same one — this
520
+ // board, and a file it came from or goes to.
521
+ category: 'interchange',
522
+ iconKey: 'bpmn.import-svg',
523
+ surfaces: ['catalogue', 'palette', 'agent'],
524
+ order: SPECS.length + 4,
525
+ scope: 'edgeless',
526
+ defaultKeys: { mac: [], other: [] },
527
+ // It WRITES, so a read-only document is one it cannot run on — the same
528
+ // reasoning as `bpmn.importXml`, and the value the catalogue renders from.
529
+ availability: 'editable',
530
+ run: importBpmnSvgFile,
531
+ telemetry: { framework: 'bpmn', element: 'board:import-svg' },
532
+ };
533
+ export const bpmnCommands = [
534
+ ...toolboxCommands,
535
+ ...laneCommands,
536
+ exportCommand,
537
+ importCommand,
538
+ importSvgCommand,
539
+ ];
69
540
  export const bpmnCommandIcons = {
70
541
  'bpmn.start': bpmnStartIcon,
542
+ 'bpmn.start.message': bpmnStartMessageIcon,
543
+ 'bpmn.start.timer': bpmnStartTimerIcon,
71
544
  'bpmn.end': bpmnEndIcon,
545
+ 'bpmn.end.message': bpmnEndMessageIcon,
546
+ 'bpmn.end.terminate': bpmnEndTerminateIcon,
72
547
  'bpmn.task': bpmnTaskIcon,
548
+ 'bpmn.task.user': bpmnTaskUserIcon,
549
+ 'bpmn.task.service': bpmnTaskServiceIcon,
550
+ 'bpmn.sub-process': bpmnSubProcessIcon,
551
+ 'bpmn.call-activity': bpmnCallActivityIcon,
73
552
  'bpmn.gateway': bpmnGatewayIcon,
553
+ 'bpmn.gateway.parallel': bpmnGatewayParallelIcon,
74
554
  'bpmn.sequence': bpmnSequenceIcon,
555
+ 'bpmn.message': bpmnMessageIcon,
556
+ 'bpmn.association': bpmnAssociationIcon,
557
+ 'bpmn.data-object': bpmnDataObjectIcon,
558
+ 'bpmn.data-store': bpmnDataStoreIcon,
559
+ 'bpmn.text-annotation': bpmnTextAnnotationIcon,
560
+ 'bpmn.group': bpmnGroupIcon,
75
561
  'bpmn.pool': bpmnPoolIcon,
562
+ 'bpmn.lane-add': bpmnLaneAddIcon,
563
+ 'bpmn.lane-remove': bpmnLaneRemoveIcon,
564
+ 'bpmn.export-xml': bpmnExportXmlIcon,
565
+ 'bpmn.import-xml': bpmnImportXmlIcon,
566
+ 'bpmn.import-svg': bpmnImportSvgIcon,
76
567
  };