@formicoidea/labre-framework-uml 0.42.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 (114) hide show
  1. package/LICENSE +384 -0
  2. package/dist/actions.d.ts +430 -0
  3. package/dist/actions.js +724 -0
  4. package/dist/background.d.ts +153 -0
  5. package/dist/background.js +557 -0
  6. package/dist/board-hit.d.ts +137 -0
  7. package/dist/board-hit.js +191 -0
  8. package/dist/commands-manifest.d.ts +19 -0
  9. package/dist/commands-manifest.js +571 -0
  10. package/dist/commands.d.ts +4 -0
  11. package/dist/commands.js +983 -0
  12. package/dist/component.d.ts +521 -0
  13. package/dist/component.js +708 -0
  14. package/dist/consts.d.ts +365 -0
  15. package/dist/consts.js +463 -0
  16. package/dist/descriptor.d.ts +12 -0
  17. package/dist/descriptor.js +10 -0
  18. package/dist/drawio-decode.d.ts +32 -0
  19. package/dist/drawio-decode.js +96 -0
  20. package/dist/drawio-import.d.ts +133 -0
  21. package/dist/drawio-import.js +885 -0
  22. package/dist/edge-morph.d.ts +154 -0
  23. package/dist/edge-morph.js +153 -0
  24. package/dist/edge-styles.d.ts +78 -0
  25. package/dist/edge-styles.js +210 -0
  26. package/dist/effects.d.ts +9 -0
  27. package/dist/effects.js +6 -0
  28. package/dist/element-renderer.d.ts +64 -0
  29. package/dist/element-renderer.js +114 -0
  30. package/dist/element-view.d.ts +234 -0
  31. package/dist/element-view.js +430 -0
  32. package/dist/export.d.ts +61 -0
  33. package/dist/export.js +41 -0
  34. package/dist/filename.d.ts +17 -0
  35. package/dist/filename.js +26 -0
  36. package/dist/frame-tag.d.ts +70 -0
  37. package/dist/frame-tag.js +78 -0
  38. package/dist/grammar.d.ts +695 -0
  39. package/dist/grammar.js +1436 -0
  40. package/dist/import.d.ts +306 -0
  41. package/dist/import.js +1270 -0
  42. package/dist/index.d.ts +47 -0
  43. package/dist/index.js +67 -0
  44. package/dist/interchange.d.ts +125 -0
  45. package/dist/interchange.js +372 -0
  46. package/dist/keywords.d.ts +204 -0
  47. package/dist/keywords.js +361 -0
  48. package/dist/kinds.d.ts +73 -0
  49. package/dist/kinds.js +91 -0
  50. package/dist/legend.d.ts +30 -0
  51. package/dist/legend.js +255 -0
  52. package/dist/model.d.ts +862 -0
  53. package/dist/model.js +1316 -0
  54. package/dist/morph.d.ts +178 -0
  55. package/dist/morph.js +496 -0
  56. package/dist/node/act-glyphs.d.ts +30 -0
  57. package/dist/node/act-glyphs.js +142 -0
  58. package/dist/node/compartment-watcher.d.ts +107 -0
  59. package/dist/node/compartment-watcher.js +163 -0
  60. package/dist/node/fit.d.ts +85 -0
  61. package/dist/node/fit.js +129 -0
  62. package/dist/node/node-renderer.d.ts +49 -0
  63. package/dist/node/node-renderer.js +611 -0
  64. package/dist/node/node-view.d.ts +51 -0
  65. package/dist/node/node-view.js +89 -0
  66. package/dist/node/paint.d.ts +84 -0
  67. package/dist/node/paint.js +143 -0
  68. package/dist/node/sd-glyphs.d.ts +36 -0
  69. package/dist/node/sd-glyphs.js +79 -0
  70. package/dist/node/stm-glyphs.d.ts +22 -0
  71. package/dist/node/stm-glyphs.js +111 -0
  72. package/dist/node/tier-metrics.d.ts +38 -0
  73. package/dist/node/tier-metrics.js +45 -0
  74. package/dist/operators.d.ts +45 -0
  75. package/dist/operators.js +75 -0
  76. package/dist/plantuml-import.d.ts +113 -0
  77. package/dist/plantuml-import.js +1723 -0
  78. package/dist/plantuml.d.ts +94 -0
  79. package/dist/plantuml.js +985 -0
  80. package/dist/presets.d.ts +149 -0
  81. package/dist/presets.js +302 -0
  82. package/dist/profiles.d.ts +2 -0
  83. package/dist/profiles.js +358 -0
  84. package/dist/reading.d.ts +160 -0
  85. package/dist/reading.js +648 -0
  86. package/dist/roles.d.ts +257 -0
  87. package/dist/roles.js +997 -0
  88. package/dist/rules.d.ts +267 -0
  89. package/dist/rules.js +3090 -0
  90. package/dist/templates.d.ts +2 -0
  91. package/dist/templates.js +197 -0
  92. package/dist/toolbar/config.d.ts +228 -0
  93. package/dist/toolbar/config.js +833 -0
  94. package/dist/toolbar/icons.d.ts +440 -0
  95. package/dist/toolbar/icons.js +711 -0
  96. package/dist/toolbar/senior-tool.d.ts +11 -0
  97. package/dist/toolbar/senior-tool.js +21 -0
  98. package/dist/toolbar/uml-menu.d.ts +17 -0
  99. package/dist/toolbar/uml-menu.js +20 -0
  100. package/dist/toolbar/uml-senior-button.d.ts +18 -0
  101. package/dist/toolbar/uml-senior-button.js +22 -0
  102. package/dist/translations.d.ts +27 -0
  103. package/dist/translations.js +117 -0
  104. package/dist/view.d.ts +33 -0
  105. package/dist/view.js +236 -0
  106. package/dist/xmi-import.d.ts +174 -0
  107. package/dist/xmi-import.js +2268 -0
  108. package/dist/xmi.d.ts +61 -0
  109. package/dist/xmi.js +1860 -0
  110. package/dist/xml-reader.d.ts +157 -0
  111. package/dist/xml-reader.js +430 -0
  112. package/dist/xml.d.ts +72 -0
  113. package/dist/xml.js +96 -0
  114. package/package.json +38 -0
package/dist/xmi.js ADDED
@@ -0,0 +1,1860 @@
1
+ import { parseTrigger, } from './grammar.js';
2
+ import { umlCentreInside, umlInteractionTimeline, } from './model.js';
3
+ import { el, serializeDocument, } from './xml.js';
4
+ /**
5
+ * The diagrams as an **XMI 2.5.1 document** — the interchange file every UML
6
+ * tool reads.
7
+ *
8
+ * ## Which XMI, exactly
9
+ *
10
+ * The one a 2016 UML tool writes and reads, and the three strings that say so
11
+ * (Annex E of UML 2.5.1 gives the namespaces; the XMI version is XMI 2.5.1's
12
+ * own):
13
+ *
14
+ * - `xmi:version="20131001"` with `xmlns:xmi="http://www.omg.org/spec/XMI/20131001"`;
15
+ * - `xmlns:uml="http://www.omg.org/spec/UML/20161101"` — the UML 2.5.1
16
+ * metamodel namespace;
17
+ * - `PrimitiveTypes.xmi` under the same base, which is where `String`,
18
+ * `Integer`, `Boolean`, `Real` and `UnlimitedNatural` live.
19
+ *
20
+ * Getting these wrong is the difference between a file Papyrus opens and a file
21
+ * it refuses, which is why they are constants with a citation rather than
22
+ * literals in a template.
23
+ *
24
+ * ## No diagram interchange
25
+ *
26
+ * Not one coordinate is written. UML's DI is a second metamodel with its own
27
+ * namespace, and every tool lays a model out its own way on import anyway — so a
28
+ * half-DI would be a promise the file cannot keep, and a full one is a chantier
29
+ * of its own. What the file carries is the MODEL: the classifiers, their
30
+ * features, and the relationships between them. The picture stays in Labre.
31
+ *
32
+ * The one thing geometry still decides is CONTAINMENT — which package a class is
33
+ * drawn inside, which subject a use case sits in — because on a UML diagram that
34
+ * is not layout, it is the statement (§12.2.4, §18.1.4).
35
+ *
36
+ * ## Deterministic ids
37
+ *
38
+ * `_1`, `_2`, … minted in document order, never a hash and never a random id:
39
+ * the same model exports to the same bytes, so a golden test is a diff and a
40
+ * re-export is not a spurious change in someone's version control.
41
+ *
42
+ * ## Pure
43
+ *
44
+ * A model in, a string out. No `std`, no DOM, no clock — `docs/adr/0012` P3.
45
+ */
46
+ /* ── The namespaces (UML 2.5.1 Annex E) ───────────────────────────────── */
47
+ export const XMI_VERSION = '20131001';
48
+ export const XMI_NAMESPACE = 'http://www.omg.org/spec/XMI/20131001';
49
+ export const UML_NAMESPACE = 'http://www.omg.org/spec/UML/20161101';
50
+ export const UML_PRIMITIVE_TYPES = `${UML_NAMESPACE}/PrimitiveTypes.xmi`;
51
+ /** What Labre stamps on the XMI extensions it writes. */
52
+ const EXTENDER = 'labre';
53
+ /**
54
+ * The five PrimitiveTypes UML itself ships, keyed by the spellings an author
55
+ * types.
56
+ *
57
+ * Referenced by `href` into `PrimitiveTypes.xmi` rather than declared in the
58
+ * file, because they are not ours to declare: every tool resolves that URI to
59
+ * the same five DataTypes, and a locally minted `String` would be a sixth type
60
+ * that merely looks like it.
61
+ *
62
+ * The lowercase spellings are here because they are what people write —
63
+ * `boolean`, `int`, `string` — and folding them costs nothing while refusing
64
+ * them would leave a perfectly ordinary attribute untyped. `int` and `float` are
65
+ * the two aliases that are NOT mere case: they are named in the map rather than
66
+ * inferred, so the reading is visible.
67
+ */
68
+ const PRIMITIVE_TYPES = {
69
+ string: 'String',
70
+ integer: 'Integer',
71
+ int: 'Integer',
72
+ boolean: 'Boolean',
73
+ bool: 'Boolean',
74
+ real: 'Real',
75
+ float: 'Real',
76
+ double: 'Real',
77
+ unlimitednatural: 'UnlimitedNatural',
78
+ };
79
+ /** The `xmi:type` each classifier kind is serialized as. */
80
+ const CLASSIFIER_TYPE = {
81
+ class: 'uml:Class',
82
+ interface: 'uml:Interface',
83
+ enumeration: 'uml:Enumeration',
84
+ object: 'uml:InstanceSpecification',
85
+ };
86
+ /**
87
+ * The keywords a classifier's `xmi:type` already states.
88
+ *
89
+ * `«interface»` in a box header is not extra information once the element is
90
+ * serialized as a `uml:Interface` — it IS that fact, written in notation. Only
91
+ * the keywords that say something the metamodel has no slot for survive into the
92
+ * extension below.
93
+ */
94
+ const METACLASS_KEYWORDS = new Set([
95
+ 'interface',
96
+ 'enumeration',
97
+ 'datatype',
98
+ 'class',
99
+ 'object',
100
+ 'instance',
101
+ ]);
102
+ /** Annex C keywords that make a Dependency a Usage (§7.8.4, Table C.1). */
103
+ const USAGE_KEYWORDS = new Set(['use', 'call', 'create', 'instantiate']);
104
+ /**
105
+ * The `xmi:type` each deployment cube is serialized as (§19.4.2).
106
+ *
107
+ * Three metaclasses for one glyph: §19.4.4 draws a Device as "a Node graphic
108
+ * with the keyword «device»" and an ExecutionEnvironment as a Node annotated
109
+ * with its own, so the cube is the same drawing and the TYPE is the whole of the
110
+ * difference. Total over the discriminant, so a fourth cube cannot land without
111
+ * being given a metaclass.
112
+ */
113
+ const DEPLOYMENT_TYPE = {
114
+ node: 'uml:Node',
115
+ device: 'uml:Device',
116
+ 'execution-environment': 'uml:ExecutionEnvironment',
117
+ };
118
+ /**
119
+ * The keywords the structural metaclasses already state.
120
+ *
121
+ * `«component»` on a component box, `«artifact»` on an artefact, `«device»` on a
122
+ * device: each is the stencil's own seed (`keywords.ts`), each is what the
123
+ * `xmi:type` beside it says, and carrying it into the extension as well would
124
+ * file a tool-specific note whose only content is the element's own metaclass.
125
+ * The same reading {@link METACLASS_KEYWORDS} makes for the classifiers.
126
+ */
127
+ const STRUCTURAL_KEYWORDS = new Set([
128
+ 'component',
129
+ 'artifact',
130
+ 'node',
131
+ 'device',
132
+ 'executionenvironment',
133
+ 'execution environment',
134
+ ]);
135
+ /* ── Id minting ───────────────────────────────────────────────────────── */
136
+ /** `_1`, `_2`, … in the order they are asked for. */
137
+ class Ids {
138
+ #next = 0;
139
+ mint() {
140
+ this.#next += 1;
141
+ return `_${this.#next}`;
142
+ }
143
+ }
144
+ /* ── Geometry helpers ─────────────────────────────────────────────────── */
145
+ /** The area of a box — what "the SMALLEST containing box" is measured on. */
146
+ function areaOf(box) {
147
+ return Math.max(0, box.w) * Math.max(0, box.h);
148
+ }
149
+ /**
150
+ * The smallest container whose box holds this element's centre — `undefined`
151
+ * for one drawn outside them all.
152
+ *
153
+ * The most-nested container wins, which is the only reading that lets a package
154
+ * drawn inside a package mean what it draws.
155
+ *
156
+ * `strictlyLarger` is asked for when the element is ITSELF a container, and it
157
+ * is what makes nesting a strict order: a candidate must be strictly bigger than
158
+ * the thing it contains, so two packages drawn exactly on top of each other
159
+ * cannot each claim the other and the recursive walk below cannot loop.
160
+ */
161
+ function containerOf(element, containers, strictlyLarger = false) {
162
+ const bounds = element.bounds;
163
+ if (!bounds)
164
+ return undefined;
165
+ const own = areaOf(bounds);
166
+ let best;
167
+ let bestArea = Number.POSITIVE_INFINITY;
168
+ for (const candidate of containers) {
169
+ if (candidate.id === element.id || !candidate.bounds)
170
+ continue;
171
+ if (!umlCentreInside(bounds, candidate.bounds))
172
+ continue;
173
+ const area = areaOf(candidate.bounds);
174
+ if (strictlyLarger && area <= own)
175
+ continue;
176
+ if (area < bestArea) {
177
+ best = candidate;
178
+ bestArea = area;
179
+ }
180
+ }
181
+ return best;
182
+ }
183
+ /** Every type name a model mentions, in the order the file will mention it. */
184
+ function typeNamesOf(model) {
185
+ const names = [];
186
+ const add = (value) => {
187
+ if (value)
188
+ names.push(value);
189
+ };
190
+ for (const classifier of model.classifiers) {
191
+ for (const attribute of classifier.attributes)
192
+ add(attribute.type);
193
+ for (const operation of classifier.operations) {
194
+ for (const parameter of operation.parameters)
195
+ add(parameter.type);
196
+ add(operation.returnType);
197
+ }
198
+ add(classifier.instanceOf);
199
+ }
200
+ return names;
201
+ }
202
+ /**
203
+ * Ids for everything a reference can point at, minted before anything is
204
+ * written.
205
+ *
206
+ * Two passes rather than one because XMI is full of FORWARD references: an
207
+ * attribute is typed by a class declared further down, an association names both
208
+ * of its ends, a generalization points at a superclass in another package. A
209
+ * single pass would have to patch strings after the fact, which is how an id
210
+ * that does not resolve gets into a file.
211
+ */
212
+ function planOf(models, ids) {
213
+ const plan = {
214
+ ids,
215
+ idOf: new Map(),
216
+ classifierByName: new Map(),
217
+ kindOf: new Map(),
218
+ interactionByName: new Map(),
219
+ synthesized: new Map(),
220
+ };
221
+ const claim = (element) => {
222
+ plan.idOf.set(element.id, ids.mint());
223
+ };
224
+ for (const model of models) {
225
+ plan.idOf.set(model.diagram.id, ids.mint());
226
+ for (const node of model.packages)
227
+ claim(node);
228
+ for (const classifier of model.classifiers) {
229
+ claim(classifier);
230
+ plan.kindOf.set(classifier.id, classifier.kind);
231
+ // A named classifier is what a type name resolves to. The FIRST wins:
232
+ // two classes called `Order` on two sheets of one export are one name in
233
+ // one `uml:Model`, and document order is the tie-break everything else in
234
+ // this framework breaks on.
235
+ if (classifier.name && !plan.classifierByName.has(classifier.name)) {
236
+ plan.classifierByName.set(classifier.name, plan.idOf.get(classifier.id));
237
+ }
238
+ }
239
+ for (const node of model.actors)
240
+ claim(node);
241
+ for (const node of model.useCases)
242
+ claim(node);
243
+ for (const node of model.subjects)
244
+ claim(node);
245
+ for (const note of model.notes)
246
+ claim(note);
247
+ // Phase 2, appended AFTER the phase-1 claims and never interleaved with
248
+ // them: the ids are minted in the order they are asked for, so a model with
249
+ // no structural artefacts mints none here and exports to exactly the bytes
250
+ // it exported to before these four lines existed. That is what makes a
251
+ // golden document a regression test rather than a chore.
252
+ for (const component of model.components) {
253
+ claim(component);
254
+ // A port is an `ownedAttribute` and therefore referenceable: §11.6.4's own
255
+ // figure wires a Dependency between two of them.
256
+ for (const port of component.ports)
257
+ claim(port);
258
+ }
259
+ for (const artifact of model.artifacts)
260
+ claim(artifact);
261
+ for (const node of model.nodes)
262
+ claim(node);
263
+ // The behaviour sheets, appended after the structural claims and never
264
+ // interleaved with them, for the reason the four lines above give: a model
265
+ // with no flow on it mints nothing here and exports to exactly the bytes it
266
+ // exported to before these lines existed.
267
+ //
268
+ // Everything a REFERENCE can reach is claimed. An activity edge names its
269
+ // two ends, a partition lists its nodes, a transition names its source and
270
+ // its target, and a composite state is pointed at by nothing but is a
271
+ // `uml:State` all the same — so every drawn glyph gets an id and the
272
+ // minted-on-the-spot ones (the edges, the triggers, the regions) are exactly
273
+ // the elements nothing refers to.
274
+ for (const activity of model.activities) {
275
+ for (const node of activity.nodes)
276
+ claim(node);
277
+ for (const partition of activity.partitions)
278
+ claim(partition);
279
+ }
280
+ for (const machine of model.stateMachines) {
281
+ for (const region of machine.regions)
282
+ claim(region);
283
+ for (const state of machine.states)
284
+ claim(state);
285
+ for (const final of machine.finalStates)
286
+ claim(final);
287
+ for (const pseudo of machine.pseudostates)
288
+ claim(pseudo);
289
+ }
290
+ // Phase 3, appended after the phase-2 claims for the reason those were
291
+ // appended after phase 1's: a model with no conversation on it mints
292
+ // nothing here and exports to exactly the bytes it exported to before these
293
+ // lines existed.
294
+ //
295
+ // The Interaction takes the SHEET's own id, not one of its own: §17.2.4
296
+ // draws the interaction as the frame, so the package and the Interaction are
297
+ // two elements of one sheet and each needs an id — hence the mint here
298
+ // beside, rather than reusing the package's.
299
+ for (const interaction of model.interactions) {
300
+ plan.idOf.set(`${interaction.id}#interaction`, ids.mint());
301
+ if (interaction.name &&
302
+ !plan.interactionByName.has(interaction.name.trim())) {
303
+ plan.interactionByName.set(interaction.name.trim(), plan.idOf.get(`${interaction.id}#interaction`));
304
+ }
305
+ for (const lifeline of interaction.lifelines)
306
+ claim(lifeline);
307
+ for (const execution of interaction.executions)
308
+ claim(execution);
309
+ for (const destruction of interaction.destructions)
310
+ claim(destruction);
311
+ for (const fragment of interaction.fragments)
312
+ claim(fragment);
313
+ // A message is claimed like an artefact: its two occurrences point back
314
+ // at it, and an occurrence written before the message it names is the
315
+ // ordinary case (§17.4 owns the Messages on the Interaction, the
316
+ // occurrences in the ordered fragment list).
317
+ for (const message of interaction.messages) {
318
+ plan.idOf.set(message.id, ids.mint());
319
+ }
320
+ }
321
+ }
322
+ // A type the author named that is neither a UML PrimitiveType nor a box on any
323
+ // sheet — `Money`, `OrderId`. It gets a DataType of its own at the root rather
324
+ // than being dropped: the author typed it, so the file says it, and an
325
+ // importer shows `total : Money` instead of an untyped attribute.
326
+ for (const model of models) {
327
+ for (const name of typeNamesOf(model)) {
328
+ if (PRIMITIVE_TYPES[name.toLowerCase()])
329
+ continue;
330
+ if (plan.classifierByName.has(name))
331
+ continue;
332
+ if (plan.synthesized.has(name))
333
+ continue;
334
+ plan.synthesized.set(name, ids.mint());
335
+ }
336
+ }
337
+ return plan;
338
+ }
339
+ /* ── Values ───────────────────────────────────────────────────────────── */
340
+ /**
341
+ * A default value as the LiteralSpecification it looks like.
342
+ *
343
+ * `3` is a `LiteralInteger` and `true` a `LiteralBoolean` because that is what
344
+ * every tool writes and what an importer can compute with; everything else is a
345
+ * `LiteralString`, verbatim. Nothing is evaluated — §9.5.4's `<default>` is an
346
+ * expression, and this writer is not an interpreter.
347
+ */
348
+ function literalOf(value, ids) {
349
+ const attrs = (type) => ({
350
+ 'xmi:type': type,
351
+ 'xmi:id': ids.mint(),
352
+ value,
353
+ });
354
+ if (/^[+-]?\d+$/.test(value)) {
355
+ return el('defaultValue', attrs('uml:LiteralInteger'));
356
+ }
357
+ if (value === 'true' || value === 'false') {
358
+ return el('defaultValue', attrs('uml:LiteralBoolean'));
359
+ }
360
+ return el('defaultValue', attrs('uml:LiteralString'));
361
+ }
362
+ /**
363
+ * `lowerValue` and `upperValue` — written only when the author STATED a
364
+ * multiplicity.
365
+ *
366
+ * An omitted range means `1` (§9.5.4 says so in as many words), and that is also
367
+ * the metamodel's default, so writing it out would add two elements that say
368
+ * what the absence already says. The upper bound is an `UnlimitedNatural`, which
369
+ * is what lets it be `*`.
370
+ */
371
+ function multiplicityElements(multiplicity, ids) {
372
+ if (!multiplicity)
373
+ return [];
374
+ return [
375
+ el('lowerValue', {
376
+ 'xmi:type': 'uml:LiteralInteger',
377
+ 'xmi:id': ids.mint(),
378
+ value: multiplicity.lower,
379
+ }),
380
+ el('upperValue', {
381
+ 'xmi:type': 'uml:LiteralUnlimitedNatural',
382
+ 'xmi:id': ids.mint(),
383
+ value: String(multiplicity.upper),
384
+ }),
385
+ ];
386
+ }
387
+ /**
388
+ * The `<type>` of a typed element — an in-file reference, a UML PrimitiveType,
389
+ * or nothing.
390
+ *
391
+ * The nested `<type xmi:idref="…"/>` form rather than a `type="…"` attribute:
392
+ * both are legal XMI, and the element form is the one that can carry an `href`
393
+ * for a type that lives in ANOTHER document, which is exactly what a
394
+ * PrimitiveType is. One shape for both cases means one thing for a reader to
395
+ * understand and one thing for a spec to check.
396
+ */
397
+ function typeElement(name, plan) {
398
+ if (!name)
399
+ return undefined;
400
+ const primitive = PRIMITIVE_TYPES[name.toLowerCase()];
401
+ if (primitive) {
402
+ return el('type', {
403
+ 'xmi:type': 'uml:PrimitiveType',
404
+ href: `${UML_PRIMITIVE_TYPES}#${primitive}`,
405
+ });
406
+ }
407
+ const local = plan.classifierByName.get(name) ?? plan.synthesized.get(name);
408
+ return local ? el('type', { 'xmi:idref': local }) : undefined;
409
+ }
410
+ /* ── Features ─────────────────────────────────────────────────────────── */
411
+ /** `visibility="public"`, or nothing when the author wrote no marker. */
412
+ function visibilityAttrs(visibility) {
413
+ return visibility ? { visibility } : {};
414
+ }
415
+ /** One attribute compartment line as an `ownedAttribute` (§9.5.4). */
416
+ function attributeElement(property, plan) {
417
+ const children = [];
418
+ const type = typeElement(property.type, plan);
419
+ if (type)
420
+ children.push(type);
421
+ children.push(...multiplicityElements(property.multiplicity, plan.ids));
422
+ if (property.defaultValue) {
423
+ children.push(literalOf(property.defaultValue, plan.ids));
424
+ }
425
+ return el('ownedAttribute', {
426
+ 'xmi:type': 'uml:Property',
427
+ 'xmi:id': plan.ids.mint(),
428
+ name: property.name,
429
+ ...visibilityAttrs(property.visibility),
430
+ // Written only when TRUE: both default to false in the metamodel, and an
431
+ // explicit `false` everywhere is noise in a file a human reads.
432
+ ...(property.isDerived ? { isDerived: 'true' } : {}),
433
+ ...(property.isStatic ? { isStatic: 'true' } : {}),
434
+ }, children);
435
+ }
436
+ /** One operation compartment line as an `ownedOperation` (§9.6.4). */
437
+ function operationElement(operation, plan) {
438
+ const children = operation.parameters.map(parameter => {
439
+ const parts = [];
440
+ const type = typeElement(parameter.type, plan);
441
+ if (type)
442
+ parts.push(type);
443
+ parts.push(...multiplicityElements(parameter.multiplicity, plan.ids));
444
+ if (parameter.defaultValue) {
445
+ parts.push(literalOf(parameter.defaultValue, plan.ids));
446
+ }
447
+ return el('ownedParameter', {
448
+ 'xmi:type': 'uml:Parameter',
449
+ 'xmi:id': plan.ids.mint(),
450
+ name: parameter.name,
451
+ // §9.4.4: an omitted direction defaults to `in`. The DEFAULT is applied
452
+ // here, where a file needs a value, rather than in the parser, which
453
+ // reports what the line said.
454
+ direction: parameter.direction ?? 'in',
455
+ }, parts);
456
+ });
457
+ // The return type is a Parameter with `direction="return"` — UML has no
458
+ // separate slot for it, and a writer that invented one would produce an
459
+ // operation whose result no importer can find.
460
+ if (operation.returnType || operation.returnMultiplicity) {
461
+ const parts = [];
462
+ const type = typeElement(operation.returnType, plan);
463
+ if (type)
464
+ parts.push(type);
465
+ parts.push(...multiplicityElements(operation.returnMultiplicity, plan.ids));
466
+ children.push(el('ownedParameter', {
467
+ 'xmi:type': 'uml:Parameter',
468
+ 'xmi:id': plan.ids.mint(),
469
+ direction: 'return',
470
+ }, parts));
471
+ }
472
+ return el('ownedOperation', {
473
+ 'xmi:type': 'uml:Operation',
474
+ 'xmi:id': plan.ids.mint(),
475
+ name: operation.name,
476
+ ...visibilityAttrs(operation.visibility),
477
+ ...(operation.isAbstract ? { isAbstract: 'true' } : {}),
478
+ ...(operation.isStatic ? { isStatic: 'true' } : {}),
479
+ ...(operation.modifiers.some(m => m.toLowerCase() === 'query')
480
+ ? { isQuery: 'true' }
481
+ : {}),
482
+ }, children);
483
+ }
484
+ /**
485
+ * The keywords the metamodel has nowhere to put, as an XMI extension.
486
+ *
487
+ * `«entity»` on a class is a STEREOTYPE application, and applying a stereotype
488
+ * in UML requires a Profile the author never wrote — so there is no valid place
489
+ * in the model for it. `xmi:Extension` is XMI's own answer to exactly this
490
+ * question (XMI 2.5.1 §7.9): tool-specific content, in a container every reader
491
+ * is required to skip if it does not recognise the extender. So the word travels
492
+ * with the file and no importer trips over it.
493
+ *
494
+ * The keywords that merely restate the metaclass are not extended — see
495
+ * {@link METACLASS_KEYWORDS}.
496
+ */
497
+ function keywordExtension(keywords, stated = METACLASS_KEYWORDS) {
498
+ const extra = keywords.filter(keyword => !stated.has(keyword.toLowerCase()));
499
+ if (extra.length === 0)
500
+ return undefined;
501
+ return el('xmi:Extension', { extender: EXTENDER }, extra.map(keyword => el('keyword', { name: keyword })));
502
+ }
503
+ /* ── Classifiers ──────────────────────────────────────────────────────── */
504
+ /**
505
+ * One classifier as a `packagedElement`, with every relationship it OWNS.
506
+ *
507
+ * Three relationships are children of their source rather than siblings of it,
508
+ * because that is where the metamodel puts them: a `Generalization` is owned by
509
+ * the specific Classifier, an `InterfaceRealization` by the implementing one,
510
+ * and both are meaningless detached from it.
511
+ */
512
+ function classifierElement(classifier, model, plan) {
513
+ const id = plan.idOf.get(classifier.id);
514
+ const children = [];
515
+ if (classifier.kind === 'enumeration') {
516
+ // An enumeration's compartment holds LITERALS, not properties: the line is
517
+ // parsed for its name and nothing else, so `NEW` and `+ NEW` both name the
518
+ // same literal.
519
+ for (const literal of classifier.attributes) {
520
+ children.push(el('ownedLiteral', {
521
+ 'xmi:type': 'uml:EnumerationLiteral',
522
+ 'xmi:id': plan.ids.mint(),
523
+ name: literal.name,
524
+ }));
525
+ }
526
+ }
527
+ else if (classifier.kind === 'object') {
528
+ // A `slot` with a `value` and NO `definingFeature`.
529
+ //
530
+ // The metamodel requires the feature, and it is the one reference this
531
+ // writer cannot honestly mint: `Slot::definingFeature` points at a Property
532
+ // of the instance's Classifier, and an object diagram almost never has that
533
+ // class on it — §11.6.4's compartment names the feature in TEXT, which is
534
+ // all the author gave us. The choice is between a slot a strict validator
535
+ // flags and dropping what somebody typed into their diagram; the slot stays.
536
+ //
537
+ // A `Slot` is not a NamedElement, so the feature's NAME has nowhere to go in
538
+ // the model either, and it rides in the extension XMI provides for exactly
539
+ // this — where a reader that wants it can find it and a reader that does not
540
+ // is required to skip it.
541
+ for (const slot of classifier.slots) {
542
+ children.push(el('slot', { 'xmi:type': 'uml:Slot', 'xmi:id': plan.ids.mint() }, [
543
+ el('value', {
544
+ 'xmi:type': 'uml:LiteralString',
545
+ 'xmi:id': plan.ids.mint(),
546
+ value: slot.value ?? '',
547
+ }),
548
+ el('xmi:Extension', { extender: EXTENDER }, [
549
+ el('feature', { name: slot.name }),
550
+ ]),
551
+ ]));
552
+ }
553
+ }
554
+ else {
555
+ for (const attribute of classifier.attributes) {
556
+ children.push(attributeElement(attribute, plan));
557
+ }
558
+ for (const operation of classifier.operations) {
559
+ children.push(operationElement(operation, plan));
560
+ }
561
+ }
562
+ for (const relation of model.relations) {
563
+ if (relation.sourceId !== classifier.id)
564
+ continue;
565
+ const target = plan.idOf.get(relation.targetId);
566
+ if (!target)
567
+ continue;
568
+ if (relation.kind === 'generalization') {
569
+ children.push(el('generalization', {
570
+ 'xmi:type': 'uml:Generalization',
571
+ 'xmi:id': plan.ids.mint(),
572
+ general: target,
573
+ }));
574
+ }
575
+ else if (relation.kind === 'realization' &&
576
+ plan.kindOf.get(relation.targetId) === 'interface') {
577
+ // An InterfaceRealization only when the far end IS an Interface — its
578
+ // `contract` is typed `Interface` in the metamodel, so pointing it at a
579
+ // class would be a reference no conformant reader can resolve. A
580
+ // realization of anything else is written as a `uml:Realization` beside
581
+ // the classifiers instead.
582
+ children.push(el('interfaceRealization', {
583
+ 'xmi:type': 'uml:InterfaceRealization',
584
+ 'xmi:id': plan.ids.mint(),
585
+ client: id,
586
+ supplier: target,
587
+ contract: target,
588
+ }));
589
+ }
590
+ }
591
+ const extension = keywordExtension(classifier.keywords);
592
+ if (extension)
593
+ children.push(extension);
594
+ // `object : Class` — the Classifier the instance is of. Resolved against the
595
+ // boxes on the export first and against the DataTypes minted for the names
596
+ // nothing answered for second, so `order1 : Order` is typed even on an object
597
+ // diagram that does not draw `Order` (which is most of them).
598
+ const instanceOf = classifier.kind === 'object' && classifier.instanceOf
599
+ ? (plan.classifierByName.get(classifier.instanceOf) ??
600
+ plan.synthesized.get(classifier.instanceOf))
601
+ : undefined;
602
+ return el('packagedElement', {
603
+ 'xmi:type': CLASSIFIER_TYPE[classifier.kind],
604
+ 'xmi:id': id,
605
+ name: classifier.name,
606
+ ...(classifier.isAbstract ? { isAbstract: 'true' } : {}),
607
+ ...(instanceOf ? { classifier: instanceOf } : {}),
608
+ }, children);
609
+ }
610
+ /* ── Use case artefacts ───────────────────────────────────────────────── */
611
+ /** One use case, with the `Include`s and `Extend`s it owns (§18.1.4). */
612
+ function useCaseElement(useCase, model, plan) {
613
+ const children = [];
614
+ for (const relation of model.relations) {
615
+ if (relation.sourceId !== useCase.id)
616
+ continue;
617
+ const target = plan.idOf.get(relation.targetId);
618
+ if (!target)
619
+ continue;
620
+ if (relation.kind === 'include') {
621
+ // Owned by the INCLUDING use case, pointing at the addition — the
622
+ // direction the role table states (source = the base case).
623
+ children.push(el('include', {
624
+ 'xmi:type': 'uml:Include',
625
+ 'xmi:id': plan.ids.mint(),
626
+ addition: target,
627
+ }));
628
+ }
629
+ else if (relation.kind === 'extend') {
630
+ // Owned by the EXTENDING use case, pointing at the case it extends —
631
+ // `Extend::extension` is the owner in the metamodel, which is why the
632
+ // role's source is the extending case and not the base one.
633
+ children.push(el('extend', {
634
+ 'xmi:type': 'uml:Extend',
635
+ 'xmi:id': plan.ids.mint(),
636
+ extendedCase: target,
637
+ }));
638
+ }
639
+ }
640
+ const extension = keywordExtension(useCase.keywords);
641
+ if (extension)
642
+ children.push(extension);
643
+ return el('packagedElement', {
644
+ 'xmi:type': 'uml:UseCase',
645
+ 'xmi:id': plan.idOf.get(useCase.id),
646
+ name: useCase.name,
647
+ }, children);
648
+ }
649
+ /* ── Structural artefacts (§11.6, §19.3, §19.4) ───────────────────────── */
650
+ /**
651
+ * One component as a `packagedElement`, with its ports and its two kinds of
652
+ * interface.
653
+ *
654
+ * ## The interfaces are MINTED, and that is the whole difficulty
655
+ *
656
+ * A lollipop on this canvas is a circle with a name written beside it — §10.4.4
657
+ * draws exactly that, and nothing else — so the model holds a STRING where XMI
658
+ * needs an element. There is no honest alternative to minting one: an
659
+ * `InterfaceRealization`'s `contract` is typed `Interface` in the metamodel, so
660
+ * a realization pointing at nothing is a reference no importer can resolve, and
661
+ * dropping the lollipop would throw away the only statement the author made
662
+ * about what the component offers.
663
+ *
664
+ * The minted `uml:Interface` is a `packagedElement` of the COMPONENT rather than
665
+ * of the diagram's package, which §11.6.4's "packaged elements" compartment is
666
+ * the notation for: it is the component's own declaration of the contract it
667
+ * realizes, and putting it at the sheet level would make two components that
668
+ * both provide `IOrder` look like one shared interface the author never drew.
669
+ * Two components providing the same name therefore produce two Interfaces, and
670
+ * that is the honest reading of two lollipops — a drawing that means one
671
+ * interface draws it once and wires both components to it, which is the
672
+ * rectangle notation §11.6.4 offers for exactly this case.
673
+ *
674
+ * ## Provided is an `interfaceRealization`, required is a `uml:Usage`
675
+ *
676
+ * §10.4.4 in as many words: the ball notation IS an InterfaceRealization
677
+ * dependency from the classifier to the interface, and the socket IS a Usage
678
+ * dependency. So the two glyphs are not two decorations on one relationship,
679
+ * they are two different metaclasses, and a writer that collapsed them would
680
+ * lose the direction of every dependency on the sheet.
681
+ */
682
+ function componentElement(component, plan) {
683
+ const id = plan.idOf.get(component.id);
684
+ const children = [];
685
+ for (const port of component.ports) {
686
+ const portId = plan.idOf.get(port.id);
687
+ if (!portId)
688
+ continue;
689
+ children.push(el('ownedAttribute', {
690
+ 'xmi:type': 'uml:Port',
691
+ 'xmi:id': portId,
692
+ // §11.3.4: "The name of a Port may be suppressed. Every depiction of an
693
+ // unnamed Port denotes a different Port from any other Port." So an
694
+ // unnamed square is a port with no `name` attribute, never a port called
695
+ // the empty string.
696
+ ...(port.name ? { name: port.name } : {}),
697
+ }));
698
+ }
699
+ for (const name of component.provided) {
700
+ const interfaceId = plan.ids.mint();
701
+ children.push(el('packagedElement', {
702
+ 'xmi:type': 'uml:Interface',
703
+ 'xmi:id': interfaceId,
704
+ name,
705
+ }), el('interfaceRealization', {
706
+ 'xmi:type': 'uml:InterfaceRealization',
707
+ 'xmi:id': plan.ids.mint(),
708
+ client: id,
709
+ supplier: interfaceId,
710
+ contract: interfaceId,
711
+ }));
712
+ }
713
+ for (const name of component.required) {
714
+ const interfaceId = plan.ids.mint();
715
+ children.push(el('packagedElement', {
716
+ 'xmi:type': 'uml:Interface',
717
+ 'xmi:id': interfaceId,
718
+ name,
719
+ }), el('packagedElement', {
720
+ 'xmi:type': 'uml:Usage',
721
+ 'xmi:id': plan.ids.mint(),
722
+ client: id,
723
+ supplier: interfaceId,
724
+ }));
725
+ }
726
+ const extension = keywordExtension(component.keywords, STRUCTURAL_KEYWORDS);
727
+ if (extension)
728
+ children.push(extension);
729
+ return el('packagedElement', {
730
+ 'xmi:type': 'uml:Component',
731
+ 'xmi:id': id,
732
+ name: component.name,
733
+ ...(component.isAbstract ? { isAbstract: 'true' } : {}),
734
+ }, children);
735
+ }
736
+ /**
737
+ * One artefact, with the Manifestations it OWNS (§19.3.2).
738
+ *
739
+ * The manifestation is a child rather than a sibling because the metamodel says
740
+ * so — `Artifact::manifestation` subsets `ownedElement` — and because it is
741
+ * meaningless detached from the file that does the manifesting, exactly as a
742
+ * Generalization is meaningless detached from the specific classifier.
743
+ *
744
+ * Three attributes for two facts: `utilizedElement` subsets `supplier` and both
745
+ * have to be written, because `utilizedElement` is the name the Manifestation
746
+ * metaclass gives the end and `supplier` is the one `Dependency` gives it, and
747
+ * an importer reading only the general form would otherwise find the dependency
748
+ * empty.
749
+ */
750
+ function artifactElement(artifact, model, plan) {
751
+ const id = plan.idOf.get(artifact.id);
752
+ const children = [];
753
+ for (const relation of model.relations) {
754
+ if (relation.kind !== 'manifest' || relation.sourceId !== artifact.id) {
755
+ continue;
756
+ }
757
+ const utilized = plan.idOf.get(relation.targetId);
758
+ if (!utilized)
759
+ continue;
760
+ children.push(el('manifestation', {
761
+ 'xmi:type': 'uml:Manifestation',
762
+ 'xmi:id': plan.ids.mint(),
763
+ ...(relation.label ? { name: relation.label } : {}),
764
+ client: id,
765
+ supplier: utilized,
766
+ utilizedElement: utilized,
767
+ }));
768
+ }
769
+ const extension = keywordExtension(artifact.keywords, STRUCTURAL_KEYWORDS);
770
+ if (extension)
771
+ children.push(extension);
772
+ return el('packagedElement', {
773
+ 'xmi:type': 'uml:Artifact',
774
+ 'xmi:id': id,
775
+ name: artifact.name,
776
+ }, children);
777
+ }
778
+ /**
779
+ * One node, device or execution environment, with the Deployments it OWNS.
780
+ *
781
+ * `Deployment::location` subsets both `client` and `owner` — the target is where
782
+ * the deployment LIVES in the metamodel — so the element is a child of the cube
783
+ * and names it back, and `deployedArtifact` subsets `supplier` the way
784
+ * `utilizedElement` does one metaclass over. Writing all four is the same
785
+ * belt-and-braces {@link artifactElement} explains: the specific names are what
786
+ * a UML importer reads, the general ones are what a generic XMI reader reads,
787
+ * and they cannot disagree because they are minted from one id.
788
+ *
789
+ * §19.2.4 also allows the artefacts to be drawn INSIDE the cube instead of being
790
+ * joined to it by an arrow. This writer reads the arrow alone: nesting is
791
+ * geometry, and the two ways of saying it produce the same `deployment` element,
792
+ * so the day the canvas learns to read a nested artefact nothing in this
793
+ * function changes.
794
+ */
795
+ function deploymentNodeElement(node, model, plan) {
796
+ const id = plan.idOf.get(node.id);
797
+ const children = [];
798
+ for (const relation of model.relations) {
799
+ if (relation.kind !== 'deploy' || relation.targetId !== node.id)
800
+ continue;
801
+ const artifact = plan.idOf.get(relation.sourceId);
802
+ if (!artifact)
803
+ continue;
804
+ children.push(el('deployment', {
805
+ 'xmi:type': 'uml:Deployment',
806
+ 'xmi:id': plan.ids.mint(),
807
+ ...(relation.label ? { name: relation.label } : {}),
808
+ client: id,
809
+ supplier: artifact,
810
+ location: id,
811
+ deployedArtifact: artifact,
812
+ }));
813
+ }
814
+ const extension = keywordExtension(node.keywords, STRUCTURAL_KEYWORDS);
815
+ if (extension)
816
+ children.push(extension);
817
+ return el('packagedElement', {
818
+ 'xmi:type': DEPLOYMENT_TYPE[node.kind],
819
+ 'xmi:id': id,
820
+ name: node.name,
821
+ }, children);
822
+ }
823
+ /* ── Behaviour artefacts (§15.2, §14.2) ───────────────────────────────── */
824
+ /**
825
+ * The metaclass each activity glyph is written as — §15.7's own names.
826
+ *
827
+ * `uml:OpaqueAction` for the rounded rectangle, because that is the Action a
828
+ * drawing states: §16.2 makes OpaqueAction the one whose behaviour is given as
829
+ * a text nobody has to interpret, which is exactly what a box with a sentence in
830
+ * it says. Every stronger reading — a CallBehaviorAction, a CallOperationAction
831
+ * — would need a Behavior or an Operation to point at, and the drawing names
832
+ * none.
833
+ *
834
+ * `uml:ForkNode` for the bar, and the join is NOT told apart here: §15.3.4
835
+ * draws one bar for both and `roles.ts` gives it one role, so the file says what
836
+ * the picture says. An importer counting the edges gets the same answer a reader
837
+ * does.
838
+ *
839
+ * `Record<UmlActivityNodeKind, string>` and therefore compile-total: a glyph
840
+ * appended to the union with no metaclass to write it as fails the build here
841
+ * rather than vanishing from a file.
842
+ */
843
+ const ACTIVITY_NODE_TYPE = {
844
+ action: 'uml:OpaqueAction',
845
+ initial: 'uml:InitialNode',
846
+ 'activity-final': 'uml:ActivityFinalNode',
847
+ 'flow-final': 'uml:FlowFinalNode',
848
+ decision: 'uml:DecisionNode',
849
+ fork: 'uml:ForkNode',
850
+ 'object-node': 'uml:ObjectNode',
851
+ 'send-signal': 'uml:SendSignalAction',
852
+ 'accept-event': 'uml:AcceptEventAction',
853
+ // §16.10.4's hourglass IS an AcceptEventAction — the one whose trigger names a
854
+ // TimeEvent. The metamodel has no `AcceptTimeEventAction`, and inventing one
855
+ // would produce a file no importer can read.
856
+ 'time-event': 'uml:AcceptEventAction',
857
+ };
858
+ /** The `kind` attribute §14.5.7's PseudostateKind enumeration spells. */
859
+ const PSEUDOSTATE_KIND = {
860
+ initial: 'initial',
861
+ choice: 'choice',
862
+ junction: 'junction',
863
+ 'shallow-history': 'shallowHistory',
864
+ 'deep-history': 'deepHistory',
865
+ 'entry-point': 'entryPoint',
866
+ 'exit-point': 'exitPoint',
867
+ terminate: 'terminate',
868
+ // §14.2.4 draws the fork and the join as one bar, like §15.3.4 does. One role,
869
+ // one PseudostateKind written, and the edges say which it is.
870
+ fork: 'fork',
871
+ };
872
+ /** An `OpaqueExpression` carrying a body — §8.3's text-valued specification. */
873
+ function opaqueExpression(tag, body, plan) {
874
+ return el(tag, {
875
+ 'xmi:type': 'uml:OpaqueExpression',
876
+ 'xmi:id': plan.ids.mint(),
877
+ body,
878
+ });
879
+ }
880
+ /** An `OpaqueBehavior` carrying a body — a state's entry, do or exit. */
881
+ function opaqueBehavior(tag, body, plan) {
882
+ return el(tag, {
883
+ 'xmi:type': 'uml:OpaqueBehavior',
884
+ 'xmi:id': plan.ids.mint(),
885
+ name: body,
886
+ body,
887
+ });
888
+ }
889
+ function signalIdFor(name, side, plan) {
890
+ const known = side.signals.get(name);
891
+ if (known)
892
+ return known;
893
+ const id = plan.ids.mint();
894
+ side.signals.set(name, id);
895
+ side.extras.push(el('packagedElement', {
896
+ 'xmi:type': 'uml:Signal',
897
+ 'xmi:id': id,
898
+ name,
899
+ }));
900
+ return id;
901
+ }
902
+ /**
903
+ * One activity node as an `<node>` of its Activity.
904
+ *
905
+ * The three event-shaped glyphs are the only ones that cost more than a line,
906
+ * and each costs what the metamodel charges:
907
+ *
908
+ * - a **send signal** names the Signal it sends (§16.3.3), which is a
909
+ * reference, so the Signal is minted beside the Activity;
910
+ * - an **accept event** carries a Trigger whose Event is a SignalEvent
911
+ * (§16.10.3) — a reference again, so both the event and its signal are
912
+ * minted beside;
913
+ * - a **time event** carries a Trigger whose Event is a TimeEvent, and its
914
+ * `when` IS a containment (`{subsets ownedElement}`), so the TimeExpression
915
+ * is written inside it. `isRelative` comes from the glyph's own words through
916
+ * {@link parseTrigger}: `after` is relative, `at` is absolute, and §13.3.4
917
+ * says so in exactly those terms.
918
+ *
919
+ * A control node writes NO `name` attribute. §15.3.4 gives the disc, the
920
+ * bullseye, the crossed circle, the diamond and the bar no label at all, and the
921
+ * creation site draws none, so an empty `name=""` would be this writer inventing
922
+ * a nameless element where the notation has an unnamed one.
923
+ */
924
+ function activityNodeElement(node, side, plan) {
925
+ const id = plan.idOf.get(node.id);
926
+ const name = node.name.trim();
927
+ const children = [];
928
+ const attrs = {
929
+ 'xmi:type': ACTIVITY_NODE_TYPE[node.kind],
930
+ 'xmi:id': id,
931
+ };
932
+ if (node.kind === 'send-signal') {
933
+ attrs.signal = signalIdFor(name || 'Signal', side, plan);
934
+ }
935
+ else if (node.kind === 'accept-event') {
936
+ const eventId = plan.ids.mint();
937
+ side.extras.push(el('packagedElement', {
938
+ 'xmi:type': 'uml:SignalEvent',
939
+ 'xmi:id': eventId,
940
+ ...(name ? { name } : {}),
941
+ signal: signalIdFor(name || 'Signal', side, plan),
942
+ }));
943
+ children.push(el('trigger', {
944
+ 'xmi:type': 'uml:Trigger',
945
+ 'xmi:id': plan.ids.mint(),
946
+ ...(name ? { name } : {}),
947
+ event: eventId,
948
+ }));
949
+ }
950
+ else if (node.kind === 'time-event') {
951
+ const trigger = parseTrigger(name);
952
+ const eventId = plan.ids.mint();
953
+ side.extras.push(el('packagedElement', {
954
+ 'xmi:type': 'uml:TimeEvent',
955
+ 'xmi:id': eventId,
956
+ ...(name ? { name } : {}),
957
+ // §13.3.3.4: `after` is a relative TimeEvent, `at` an absolute one.
958
+ // Written only when TRUE, like every other boolean in this file.
959
+ ...(trigger.kind === 'relative-time' ? { isRelative: 'true' } : {}),
960
+ }, [
961
+ el('when', { 'xmi:type': 'uml:TimeExpression', 'xmi:id': plan.ids.mint() }, [
962
+ el('expr', {
963
+ 'xmi:type': 'uml:LiteralString',
964
+ 'xmi:id': plan.ids.mint(),
965
+ // The TimeExpression, without the keyword that classified it —
966
+ // `after 5 s` is a relative event whose expression is `5 s`, and
967
+ // writing the keyword into the value would make an importer
968
+ // wait "after after 5 s".
969
+ value: trigger.expression ?? name,
970
+ }),
971
+ ]),
972
+ ]));
973
+ children.push(el('trigger', {
974
+ 'xmi:type': 'uml:Trigger',
975
+ 'xmi:id': plan.ids.mint(),
976
+ ...(name ? { name } : {}),
977
+ event: eventId,
978
+ }));
979
+ }
980
+ // The control nodes stay unnamed — see the header.
981
+ const named = node.kind === 'initial' ||
982
+ node.kind === 'activity-final' ||
983
+ node.kind === 'flow-final' ||
984
+ node.kind === 'decision' ||
985
+ node.kind === 'fork'
986
+ ? {}
987
+ : name
988
+ ? { name }
989
+ : {};
990
+ return el('node', { ...attrs, ...named }, children);
991
+ }
992
+ /**
993
+ * One activity edge as an `<edge>` of its Activity.
994
+ *
995
+ * `guard` and `weight` are both `{subsets ownedElement}` on ActivityEdge, so
996
+ * both are written as CHILDREN — a guard as the OpaqueExpression §15.2.4's
997
+ * bracketed text is, a weight as a LiteralInteger when it is a plain number and
998
+ * a LiteralUnlimitedNatural when the author wrote `*`. §15.2.4 admits any
999
+ * ValueSpecification there; anything this writer cannot read as a number goes
1000
+ * through as an OpaqueExpression, which keeps the author's words and asks an
1001
+ * importer to interpret them.
1002
+ *
1003
+ * An end this sheet has no id for is an edge the file cannot carry, and it is
1004
+ * skipped rather than written with a dangling reference — the same call
1005
+ * {@link associationElement} makes. `umlModelFrom` has already warned the
1006
+ * author about it.
1007
+ */
1008
+ function activityEdgeElement(edge, plan) {
1009
+ const source = plan.idOf.get(edge.sourceId);
1010
+ const target = plan.idOf.get(edge.targetId);
1011
+ if (!source || !target)
1012
+ return undefined;
1013
+ const children = [];
1014
+ if (edge.guard)
1015
+ children.push(opaqueExpression('guard', edge.guard, plan));
1016
+ if (edge.weight) {
1017
+ const weight = edge.weight.trim();
1018
+ children.push(weight === '*'
1019
+ ? el('weight', {
1020
+ 'xmi:type': 'uml:LiteralUnlimitedNatural',
1021
+ 'xmi:id': plan.ids.mint(),
1022
+ value: '*',
1023
+ })
1024
+ : /^\d+$/.test(weight)
1025
+ ? el('weight', {
1026
+ 'xmi:type': 'uml:LiteralInteger',
1027
+ 'xmi:id': plan.ids.mint(),
1028
+ value: weight,
1029
+ })
1030
+ : opaqueExpression('weight', weight, plan));
1031
+ }
1032
+ return el('edge', {
1033
+ 'xmi:type': edge.kind === 'object-flow' ? 'uml:ObjectFlow' : 'uml:ControlFlow',
1034
+ 'xmi:id': plan.ids.mint(),
1035
+ ...(edge.name ? { name: edge.name } : {}),
1036
+ source,
1037
+ target,
1038
+ }, children);
1039
+ }
1040
+ /**
1041
+ * One Activity as a `packagedElement`, and whatever it had to mint beside
1042
+ * itself.
1043
+ *
1044
+ * The partitions are `group`s — §15.6.2 makes ActivityPartition an
1045
+ * ActivityGroup, and `Activity::group` is where a group lives — each listing the
1046
+ * nodes it holds as a space-separated `node` idref list, which is how XMI writes
1047
+ * a multi-valued reference. The membership itself came from geometry
1048
+ * (`model.ts`), so a lane the author widened takes the actions it now covers and
1049
+ * the file follows the drawing.
1050
+ *
1051
+ * `isReadOnly`, `isSingleExecution` and the pre/post-conditions of §15.2.4 are
1052
+ * not written: the notation states none of them on this canvas, and a default
1053
+ * spelled out is a fact nobody stated.
1054
+ */
1055
+ function activityElements(activity, plan) {
1056
+ const side = { extras: [], signals: new Map() };
1057
+ const children = [];
1058
+ for (const node of activity.nodes) {
1059
+ children.push(activityNodeElement(node, side, plan));
1060
+ }
1061
+ for (const edge of activity.edges) {
1062
+ const element = activityEdgeElement(edge, plan);
1063
+ if (element)
1064
+ children.push(element);
1065
+ }
1066
+ for (const partition of activity.partitions) {
1067
+ const nodes = partition.nodeIds
1068
+ .map(nodeId => plan.idOf.get(nodeId))
1069
+ .filter((nodeId) => Boolean(nodeId));
1070
+ children.push(el('group', {
1071
+ 'xmi:type': 'uml:ActivityPartition',
1072
+ 'xmi:id': plan.idOf.get(partition.id),
1073
+ name: partition.name,
1074
+ ...(nodes.length > 0 ? { node: nodes.join(' ') } : {}),
1075
+ }));
1076
+ }
1077
+ return [
1078
+ ...side.extras,
1079
+ el('packagedElement', {
1080
+ 'xmi:type': 'uml:Activity',
1081
+ 'xmi:id': plan.ids.mint(),
1082
+ name: activity.name,
1083
+ }, children),
1084
+ ];
1085
+ }
1086
+ /** One state's vertices, as the `<subvertex>`es of the region that holds them. */
1087
+ function subvertexElements(machine, regionId, plan) {
1088
+ const children = [];
1089
+ for (const state of machine.states) {
1090
+ if (state.regionId !== regionId)
1091
+ continue;
1092
+ const inner = [];
1093
+ for (const body of state.entry) {
1094
+ inner.push(opaqueBehavior('entry', body, plan));
1095
+ }
1096
+ for (const body of state.doActivity) {
1097
+ inner.push(opaqueBehavior('doActivity', body, plan));
1098
+ }
1099
+ for (const body of state.exit) {
1100
+ inner.push(opaqueBehavior('exit', body, plan));
1101
+ }
1102
+ children.push(el('subvertex', {
1103
+ 'xmi:type': 'uml:State',
1104
+ 'xmi:id': plan.idOf.get(state.id),
1105
+ name: state.name,
1106
+ }, inner));
1107
+ }
1108
+ for (const final of machine.finalStates) {
1109
+ if (final.regionId !== regionId)
1110
+ continue;
1111
+ children.push(el('subvertex', {
1112
+ 'xmi:type': 'uml:FinalState',
1113
+ 'xmi:id': plan.idOf.get(final.id),
1114
+ // §14.2.4.5 draws the bullseye with no name, and the creation site
1115
+ // writes none. A named one is an author who named it.
1116
+ ...(final.name.trim() ? { name: final.name } : {}),
1117
+ }));
1118
+ }
1119
+ for (const pseudo of machine.pseudostates) {
1120
+ if (pseudo.regionId !== regionId)
1121
+ continue;
1122
+ children.push(el('subvertex', {
1123
+ 'xmi:type': 'uml:Pseudostate',
1124
+ 'xmi:id': plan.idOf.get(pseudo.id),
1125
+ ...(pseudo.name.trim() ? { name: pseudo.name } : {}),
1126
+ kind: PSEUDOSTATE_KIND[pseudo.kind],
1127
+ }));
1128
+ }
1129
+ // The COMPOSITE states drawn inside this region. Each is a `uml:State` with a
1130
+ // Region of its own — §14.2.4's decomposition compartment, and the shape
1131
+ // `roles.ts` records the canvas draws as a frame.
1132
+ for (const region of machine.regions) {
1133
+ if (region.parentId !== regionId)
1134
+ continue;
1135
+ children.push(el('subvertex', {
1136
+ 'xmi:type': 'uml:State',
1137
+ 'xmi:id': plan.idOf.get(region.id),
1138
+ name: region.name,
1139
+ }, [regionElement(machine, region, plan)]));
1140
+ }
1141
+ return children;
1142
+ }
1143
+ /** One `<region>` — its vertices, and the transitions that run between them. */
1144
+ function regionElement(machine, region, plan) {
1145
+ const regionId = region?.id;
1146
+ const children = subvertexElements(machine, regionId, plan);
1147
+ // A transition belongs to the region its SOURCE is in — §14.5.12 owns a
1148
+ // Transition on a Region, and the source is the vertex the arc leaves. A
1149
+ // transition crossing out of a composite state is therefore written inside it,
1150
+ // which is where §14.2.4 draws it from.
1151
+ const regionOfVertex = new Map();
1152
+ for (const state of machine.states) {
1153
+ regionOfVertex.set(state.id, state.regionId);
1154
+ }
1155
+ for (const final of machine.finalStates) {
1156
+ regionOfVertex.set(final.id, final.regionId);
1157
+ }
1158
+ for (const pseudo of machine.pseudostates) {
1159
+ regionOfVertex.set(pseudo.id, pseudo.regionId);
1160
+ }
1161
+ for (const nested of machine.regions) {
1162
+ regionOfVertex.set(nested.id, nested.parentId);
1163
+ }
1164
+ for (const transition of machine.transitions) {
1165
+ if (regionOfVertex.get(transition.sourceId) !== regionId)
1166
+ continue;
1167
+ const source = plan.idOf.get(transition.sourceId);
1168
+ const target = plan.idOf.get(transition.targetId);
1169
+ if (!source || !target)
1170
+ continue;
1171
+ const inner = [];
1172
+ for (const trigger of transition.triggers) {
1173
+ // A Trigger's `event` is a reference and this drawing names no Event
1174
+ // declaration — §14.2.4.8 says as much, "SignalEvent triggers and
1175
+ // CallEvent triggers are not distinguishable by syntax and must be
1176
+ // discriminated by their declaration elsewhere". So the Trigger carries
1177
+ // the author's word as its NAME and points at nothing, which an importer
1178
+ // can bind and this writer cannot honestly guess.
1179
+ inner.push(el('trigger', {
1180
+ 'xmi:type': 'uml:Trigger',
1181
+ 'xmi:id': plan.ids.mint(),
1182
+ name: trigger,
1183
+ }));
1184
+ }
1185
+ if (transition.guard) {
1186
+ // `Transition::guard` is a CONSTRAINT (`{subsets ownedElement}`), not a
1187
+ // ValueSpecification — which is where it differs from an ActivityEdge's,
1188
+ // one clause over. The expression goes in its `specification`.
1189
+ inner.push(el('guard', { 'xmi:type': 'uml:Constraint', 'xmi:id': plan.ids.mint() }, [opaqueExpression('specification', transition.guard, plan)]));
1190
+ }
1191
+ if (transition.effect) {
1192
+ inner.push(opaqueBehavior('effect', transition.effect, plan));
1193
+ }
1194
+ children.push(el('transition', {
1195
+ 'xmi:type': 'uml:Transition',
1196
+ 'xmi:id': plan.ids.mint(),
1197
+ source,
1198
+ target,
1199
+ }, inner));
1200
+ }
1201
+ return el('region', {
1202
+ 'xmi:type': 'uml:Region',
1203
+ // A Region is minted rather than claimed: the canvas draws the COMPOSITE
1204
+ // STATE (which is claimed, above) and never the region inside it, and the
1205
+ // top region of a machine is one §14.2.4 makes implicit — the frame IS it.
1206
+ // Nothing refers to a Region by id, so nothing needs it planned.
1207
+ 'xmi:id': plan.ids.mint(),
1208
+ name: region ? region.name : machine.name,
1209
+ }, children);
1210
+ }
1211
+ /**
1212
+ * One StateMachine as a `packagedElement`.
1213
+ *
1214
+ * ONE top-level Region, always, and the sheet's vertices that sit inside no
1215
+ * drawn composite state are its subvertices. §14.2.4 makes the top region
1216
+ * implicit — the frame IS it — so there is nothing on the canvas to read it off,
1217
+ * and a machine with no region drawn would otherwise have nowhere to put a
1218
+ * single state.
1219
+ *
1220
+ * Orthogonal regions — two or more side by side inside one composite state,
1221
+ * separated by a dashed line — are a PHASE 3 refinement and this writer cannot
1222
+ * produce one: the canvas draws a composite state as a single `umlRegion` box
1223
+ * (`roles.ts` records the arbitration), so every composite state here has
1224
+ * exactly one region. What is written is always a legal StateMachine; it is
1225
+ * simply never an orthogonal one.
1226
+ */
1227
+ function stateMachineElement(machine, plan) {
1228
+ return el('packagedElement', {
1229
+ 'xmi:type': 'uml:StateMachine',
1230
+ 'xmi:id': plan.ids.mint(),
1231
+ name: machine.name,
1232
+ }, [regionElement(machine, undefined, plan)]);
1233
+ }
1234
+ /* ── Interactions (§17) ───────────────────────────────────────────────── */
1235
+ /**
1236
+ * §17.4.3's `messageSort`, which is the one attribute that says WHAT KIND of
1237
+ * message an arrow is.
1238
+ *
1239
+ * A total map over {@link UmlMessageKind}, so a sixth arrow added to the union
1240
+ * fails the build here rather than exporting as a synchronous call.
1241
+ */
1242
+ const MESSAGE_SORT = {
1243
+ 'message-sync': 'synchCall',
1244
+ 'message-async': 'asynchCall',
1245
+ 'message-reply': 'reply',
1246
+ 'message-create': 'createMessage',
1247
+ 'message-delete': 'deleteMessage',
1248
+ };
1249
+ /**
1250
+ * One Interaction as a `packagedElement` — its lifelines, the ordered account of
1251
+ * what happens on it, and its messages.
1252
+ *
1253
+ * ## The shape, and why the fragments are a tree
1254
+ *
1255
+ * §17.4.4's arrow is TWO things in the metamodel: a `Message`, owned by the
1256
+ * Interaction, and a pair of `MessageOccurrenceSpecification`s, which are
1257
+ * InteractionFragments and therefore live in the ORDERED `fragment` list that IS
1258
+ * the sequence. So the `<fragment>`s are written in time order, each pointing
1259
+ * back at its message, and the `<message>`s follow — which is where §17.4 puts
1260
+ * them (`Interaction::message` is a containment of its own) and what lets an
1261
+ * occurrence refer to one that has not been written yet.
1262
+ *
1263
+ * A CombinedFragment is a fragment like any other, and its operands OWN the
1264
+ * fragments drawn in their bands (§17.6.4). That nesting is
1265
+ * {@link umlInteractionTimeline}'s, computed once for both writers, which is why
1266
+ * an `alt`'s two branches hold the same messages here as they do in the `.puml`.
1267
+ *
1268
+ * ## What is written for an ExecutionSpecification
1269
+ *
1270
+ * Three elements, because the metamodel needs three: an
1271
+ * `ExecutionOccurrenceSpecification` where the bar starts, a
1272
+ * `BehaviorExecutionSpecification` pointing at both ends, and a second
1273
+ * occurrence where it finishes. §17.2.4's `start` and `finish` are
1274
+ * OccurrenceSpecifications, not heights, and a bar written with neither would be
1275
+ * an activation attached to no moment in the conversation.
1276
+ */
1277
+ function interactionElement(interaction, plan) {
1278
+ const children = [];
1279
+ for (const lifeline of interaction.lifelines) {
1280
+ // §17.3.4's `: <Type>` half, and the one fact about a lifeline the
1281
+ // metamodel has nowhere to put: `Lifeline::represents` wants a
1282
+ // ConnectableElement — a Property of the enclosing Classifier — and a
1283
+ // whiteboard has drawn no such Property. The author wrote a type NAME in a
1284
+ // head. So it travels where XMI 2.5.1 §7.9 puts what a tool knows and the
1285
+ // metamodel does not model, exactly as an Annex C keyword does, and comes
1286
+ // back as the same half of the same compartment.
1287
+ const extension = keywordExtension(lifeline.keywords) ??
1288
+ (lifeline.type ? el('xmi:Extension', { extender: EXTENDER }) : undefined);
1289
+ if (extension && lifeline.type) {
1290
+ extension.children.push(el('represents', { name: lifeline.type }));
1291
+ }
1292
+ children.push(el('lifeline', {
1293
+ 'xmi:type': 'uml:Lifeline',
1294
+ 'xmi:id': plan.idOf.get(lifeline.id),
1295
+ name: lifeline.name,
1296
+ }, extension ? [extension] : []));
1297
+ }
1298
+ /** The `xmi:id` of the LIFELINE an end of a message is drawn on. */
1299
+ const spineOf = new Map();
1300
+ for (const lifeline of interaction.lifelines) {
1301
+ const id = plan.idOf.get(lifeline.id);
1302
+ if (id)
1303
+ spineOf.set(lifeline.id, id);
1304
+ }
1305
+ for (const execution of interaction.executions) {
1306
+ const id = execution.lifelineId
1307
+ ? plan.idOf.get(execution.lifelineId)
1308
+ : undefined;
1309
+ if (id)
1310
+ spineOf.set(execution.id, id);
1311
+ }
1312
+ for (const destruction of interaction.destructions) {
1313
+ const id = destruction.lifelineId
1314
+ ? plan.idOf.get(destruction.lifelineId)
1315
+ : undefined;
1316
+ if (id)
1317
+ spineOf.set(destruction.id, id);
1318
+ }
1319
+ // Minted before the walk, because a BehaviorExecutionSpecification names the
1320
+ // occurrence it FINISHES at and that occurrence is written further down the
1321
+ // list. Two passes rather than a patched string — the reason `planOf` exists.
1322
+ const startOf = new Map();
1323
+ const finishOf = new Map();
1324
+ for (const execution of interaction.executions) {
1325
+ startOf.set(execution.id, plan.ids.mint());
1326
+ finishOf.set(execution.id, plan.ids.mint());
1327
+ }
1328
+ const sendOf = new Map();
1329
+ const receiveOf = new Map();
1330
+ for (const message of interaction.messages) {
1331
+ sendOf.set(message.id, plan.ids.mint());
1332
+ receiveOf.set(message.id, plan.ids.mint());
1333
+ }
1334
+ const fragmentsOf = (entries) => {
1335
+ const out = [];
1336
+ for (const entry of entries) {
1337
+ switch (entry.at) {
1338
+ case 'message': {
1339
+ const { message } = entry;
1340
+ const from = spineOf.get(message.sourceId);
1341
+ const to = spineOf.get(message.targetId);
1342
+ const owned = plan.idOf.get(message.id);
1343
+ // An occurrence covers exactly one lifeline (§17.2.4), so an end this
1344
+ // sheet has no lifeline for writes no occurrence — and the Message
1345
+ // below writes no `sendEvent` for it either.
1346
+ if (from) {
1347
+ out.push(el('fragment', {
1348
+ 'xmi:type': 'uml:MessageOccurrenceSpecification',
1349
+ 'xmi:id': sendOf.get(message.id),
1350
+ covered: from,
1351
+ message: owned,
1352
+ }));
1353
+ }
1354
+ if (to) {
1355
+ out.push(el('fragment', {
1356
+ 'xmi:type': 'uml:MessageOccurrenceSpecification',
1357
+ 'xmi:id': receiveOf.get(message.id),
1358
+ covered: to,
1359
+ message: owned,
1360
+ }));
1361
+ }
1362
+ break;
1363
+ }
1364
+ case 'execution-start': {
1365
+ const { execution } = entry;
1366
+ const covered = spineOf.get(execution.id);
1367
+ out.push(el('fragment', {
1368
+ 'xmi:type': 'uml:ExecutionOccurrenceSpecification',
1369
+ 'xmi:id': startOf.get(execution.id),
1370
+ ...(covered ? { covered } : {}),
1371
+ }), el('fragment', {
1372
+ 'xmi:type': 'uml:BehaviorExecutionSpecification',
1373
+ 'xmi:id': plan.idOf.get(execution.id),
1374
+ ...(covered ? { covered } : {}),
1375
+ start: startOf.get(execution.id),
1376
+ finish: finishOf.get(execution.id),
1377
+ }));
1378
+ break;
1379
+ }
1380
+ case 'execution-finish': {
1381
+ const covered = spineOf.get(entry.execution.id);
1382
+ out.push(el('fragment', {
1383
+ 'xmi:type': 'uml:ExecutionOccurrenceSpecification',
1384
+ 'xmi:id': finishOf.get(entry.execution.id),
1385
+ ...(covered ? { covered } : {}),
1386
+ }));
1387
+ break;
1388
+ }
1389
+ case 'destruction': {
1390
+ const covered = spineOf.get(entry.destruction.id);
1391
+ out.push(el('fragment', {
1392
+ 'xmi:type': 'uml:DestructionOccurrenceSpecification',
1393
+ 'xmi:id': plan.idOf.get(entry.destruction.id),
1394
+ ...(covered ? { covered } : {}),
1395
+ }));
1396
+ break;
1397
+ }
1398
+ case 'fragment': {
1399
+ const { fragment, operands } = entry;
1400
+ const covered = fragment.coveredLifelineIds
1401
+ .map(id => plan.idOf.get(id))
1402
+ .filter((id) => Boolean(id));
1403
+ if (fragment.operator === 'ref') {
1404
+ // §17.7.4's InteractionUse. `refersTo` when another sheet of this
1405
+ // document declares an Interaction of that name — which is the
1406
+ // whole point of a `ref` — and the NAME alone when nothing here
1407
+ // answers for it, because the author still said which interaction
1408
+ // they meant.
1409
+ const refersTo = plan.interactionByName.get(fragment.name.trim());
1410
+ out.push(el('fragment', {
1411
+ 'xmi:type': 'uml:InteractionUse',
1412
+ 'xmi:id': plan.idOf.get(fragment.id),
1413
+ ...(fragment.name ? { name: fragment.name } : {}),
1414
+ ...(covered.length > 0 ? { covered: covered.join(' ') } : {}),
1415
+ ...(refersTo ? { refersTo } : {}),
1416
+ }));
1417
+ // An InteractionUse owns no fragments, so anything drawn over it is
1418
+ // written beside it rather than lost.
1419
+ for (const band of operands)
1420
+ out.push(...fragmentsOf(band.entries));
1421
+ break;
1422
+ }
1423
+ out.push(el('fragment', {
1424
+ 'xmi:type': 'uml:CombinedFragment',
1425
+ 'xmi:id': plan.idOf.get(fragment.id),
1426
+ ...(covered.length > 0 ? { covered: covered.join(' ') } : {}),
1427
+ interactionOperator: fragment.operator,
1428
+ }, operands.map(band => {
1429
+ const inner = [];
1430
+ const guard = (band.operand.guard ?? '').trim();
1431
+ if (guard) {
1432
+ inner.push(el('guard', {
1433
+ 'xmi:type': 'uml:InteractionConstraint',
1434
+ 'xmi:id': plan.ids.mint(),
1435
+ }, [
1436
+ el('specification', {
1437
+ 'xmi:type': 'uml:LiteralString',
1438
+ 'xmi:id': plan.ids.mint(),
1439
+ value: guard,
1440
+ }),
1441
+ ]));
1442
+ }
1443
+ inner.push(...fragmentsOf(band.entries));
1444
+ return el('operand', {
1445
+ 'xmi:type': 'uml:InteractionOperand',
1446
+ 'xmi:id': plan.ids.mint(),
1447
+ }, inner);
1448
+ })));
1449
+ break;
1450
+ }
1451
+ }
1452
+ }
1453
+ return out;
1454
+ };
1455
+ children.push(...fragmentsOf(umlInteractionTimeline(interaction)));
1456
+ for (const message of interaction.messages) {
1457
+ const from = spineOf.get(message.sourceId);
1458
+ const to = spineOf.get(message.targetId);
1459
+ children.push(el('message', {
1460
+ 'xmi:type': 'uml:Message',
1461
+ 'xmi:id': plan.idOf.get(message.id),
1462
+ ...(message.label ? { name: message.label } : {}),
1463
+ messageSort: MESSAGE_SORT[message.kind],
1464
+ ...(from ? { sendEvent: sendOf.get(message.id) } : {}),
1465
+ ...(to ? { receiveEvent: receiveOf.get(message.id) } : {}),
1466
+ }));
1467
+ }
1468
+ return el('packagedElement', {
1469
+ 'xmi:type': 'uml:Interaction',
1470
+ 'xmi:id': plan.idOf.get(`${interaction.id}#interaction`),
1471
+ name: interaction.name,
1472
+ }, children);
1473
+ }
1474
+ /* ── Relationships that are packaged elements ─────────────────────────── */
1475
+ /**
1476
+ * An association, with both ends owned by the association itself.
1477
+ *
1478
+ * ## Why both ends are `ownedEnd`s
1479
+ *
1480
+ * UML lets an end be owned either by the Association or by the Classifier at the
1481
+ * other end (where it shows up as an attribute). A canvas connector states
1482
+ * neither: the author drew a line, not a navigable attribute. Owning both ends
1483
+ * on the association is the reading that adds nothing the author did not say,
1484
+ * and it is what every tool produces for a line drawn with no role names.
1485
+ *
1486
+ * ## Which end carries the diamond
1487
+ *
1488
+ * The aggregation flag goes on the end whose TYPE is the PART — the property the
1489
+ * whole conceptually owns — because that is what `Property::isComposite` means
1490
+ * in the metamodel ("the object containing the attribute is a container for the
1491
+ * object contained in the attribute"). The diamond is then drawn at the whole's
1492
+ * end of the line, which is the source: the role table makes the source the
1493
+ * whole, so the flag lands on the end typed by the target. Putting it on the
1494
+ * other end would draw the diamond on the wrong classifier in every tool that
1495
+ * opens the file.
1496
+ *
1497
+ * ## Which `ownedEnd` an end LABEL lands on
1498
+ *
1499
+ * The one drawn at the same end. §11.5.4 defines an Association end as "the
1500
+ * connection between the line depicting an Association and the icon … depicting
1501
+ * the connected Classifier", and everything written near it — the name string,
1502
+ * the multiplicity, the visibility glyph — adorns the memberEnd whose TYPE is
1503
+ * that classifier. So `relation.sourceEnd` becomes the `ownedEnd` carrying
1504
+ * `<type idref="{source}">`, and `targetEnd` the one typed by the target.
1505
+ *
1506
+ * The question is worth stating because the OTHER reading is the familiar one:
1507
+ * where a Classifier owns the end (§11.5.4's dot notation — "the dot shows that
1508
+ * the model includes a Property of the type represented by the Classifier
1509
+ * touched by the dot. This Property is owned by the Classifier at the other
1510
+ * end"), the end drawn at B is an attribute OF A, and a writer that put the
1511
+ * adornments on the owning classifier's side would swap every multiplicity on
1512
+ * the diagram. It does not arise here: this writer owns BOTH ends on the
1513
+ * association and types each one explicitly, so "the end at X" and "the end
1514
+ * typed by X" are the same end, and the mapping is direct. `xmi-import.ts` reads
1515
+ * it back through the same identity.
1516
+ *
1517
+ * The aggregation flag is not a counter-example: §11.5.4 has the diamond drawn
1518
+ * at the end OPPOSITE the flagged one ("a hollow diamond is added as a terminal
1519
+ * adornment at the end of the Association line opposite the end marked with
1520
+ * aggregation = AggregationKind::shared"), which is precisely why it is the one
1521
+ * adornment written across the line rather than beside its own end.
1522
+ */
1523
+ function associationElement(relation, plan,
1524
+ // §19.4.3: "A CommunicationPath is an Association between two
1525
+ // DeploymentTargets". A specialization of Association, drawn as one and
1526
+ // structured as one, so it takes this writer whole and differs by its
1527
+ // metaclass alone — which is the entire content of this parameter.
1528
+ type = 'uml:Association') {
1529
+ const source = plan.idOf.get(relation.sourceId);
1530
+ const target = plan.idOf.get(relation.targetId);
1531
+ if (!source || !target)
1532
+ return undefined;
1533
+ const id = plan.ids.mint();
1534
+ const sourceEnd = plan.ids.mint();
1535
+ const targetEnd = plan.ids.mint();
1536
+ const aggregation = relation.kind === 'composition'
1537
+ ? 'composite'
1538
+ : relation.kind === 'aggregation'
1539
+ ? 'shared'
1540
+ : undefined;
1541
+ return el('packagedElement', {
1542
+ 'xmi:type': type,
1543
+ 'xmi:id': id,
1544
+ ...(relation.label ? { name: relation.label } : {}),
1545
+ memberEnd: `${sourceEnd} ${targetEnd}`,
1546
+ }, [
1547
+ el('ownedEnd', {
1548
+ 'xmi:type': 'uml:Property',
1549
+ 'xmi:id': sourceEnd,
1550
+ association: id,
1551
+ ...endAttrs(relation.sourceEnd),
1552
+ }, [
1553
+ el('type', { 'xmi:idref': source }),
1554
+ ...multiplicityElements(relation.sourceEnd?.multiplicity, plan.ids),
1555
+ ]),
1556
+ el('ownedEnd', {
1557
+ 'xmi:type': 'uml:Property',
1558
+ 'xmi:id': targetEnd,
1559
+ association: id,
1560
+ ...endAttrs(relation.targetEnd),
1561
+ ...(aggregation ? { aggregation } : {}),
1562
+ }, [
1563
+ el('type', { 'xmi:idref': target }),
1564
+ ...multiplicityElements(relation.targetEnd?.multiplicity, plan.ids),
1565
+ ]),
1566
+ ]);
1567
+ }
1568
+ /**
1569
+ * The `name` and `visibility` of one association end, when the label said them.
1570
+ *
1571
+ * Nothing when it did not, which is what keeps a connector with no end labels
1572
+ * byte-identical to what phase 1 wrote: the fields are absent from the IR, so
1573
+ * the attributes are absent from the file, so every golden of a plain
1574
+ * association still matches character for character.
1575
+ */
1576
+ function endAttrs(end) {
1577
+ if (!end)
1578
+ return {};
1579
+ return {
1580
+ ...(end.role ? { name: end.role } : {}),
1581
+ ...visibilityAttrs(end.visibility),
1582
+ };
1583
+ }
1584
+ /**
1585
+ * A dependency — a `uml:Usage` when its label carries one of Annex C's usage
1586
+ * keywords, a plain `uml:Dependency` otherwise.
1587
+ *
1588
+ * `«use»`, `«call»` and `«create»` are not decoration on a dashed arrow: Table
1589
+ * C.1 gives all three the metamodel element `Usage`, so an arrow labelled `«use»`
1590
+ * IS a Usage and writing it as a bare Dependency would throw away the one thing
1591
+ * the author said about it.
1592
+ */
1593
+ function dependencyElement(relation, plan) {
1594
+ const client = plan.idOf.get(relation.sourceId);
1595
+ const supplier = plan.idOf.get(relation.targetId);
1596
+ if (!client || !supplier)
1597
+ return undefined;
1598
+ const label = relation.label ?? '';
1599
+ const isUsage = [...USAGE_KEYWORDS].some(keyword => label.toLowerCase().includes(keyword));
1600
+ return el('packagedElement', {
1601
+ 'xmi:type': isUsage ? 'uml:Usage' : 'uml:Dependency',
1602
+ 'xmi:id': plan.ids.mint(),
1603
+ ...(relation.label ? { name: relation.label } : {}),
1604
+ client,
1605
+ supplier,
1606
+ });
1607
+ }
1608
+ /** A realization whose far end is not an Interface — see {@link classifierElement}. */
1609
+ function realizationElement(relation, plan) {
1610
+ const client = plan.idOf.get(relation.sourceId);
1611
+ const supplier = plan.idOf.get(relation.targetId);
1612
+ if (!client || !supplier)
1613
+ return undefined;
1614
+ return el('packagedElement', {
1615
+ 'xmi:type': 'uml:Realization',
1616
+ 'xmi:id': plan.ids.mint(),
1617
+ ...(relation.label ? { name: relation.label } : {}),
1618
+ client,
1619
+ supplier,
1620
+ });
1621
+ }
1622
+ /* ── One diagram ──────────────────────────────────────────────────────── */
1623
+ /**
1624
+ * One diagram as a `uml:Package`.
1625
+ *
1626
+ * A package per diagram even when there is only one, because a `uml:Model` with
1627
+ * loose classifiers in it says the four sheets an architect drew are one
1628
+ * namespace, which is exactly the merge ADR 0017 refuses on the canvas.
1629
+ */
1630
+ function diagramPackage(model, plan) {
1631
+ const children = [];
1632
+ /** Where each classifier and sub-package is drawn — §12.2.4's containment. */
1633
+ const packageOf = new Map();
1634
+ for (const pkg of model.packages) {
1635
+ packageOf.set(pkg.id, containerOf(pkg, model.packages, true)?.id);
1636
+ }
1637
+ for (const classifier of model.classifiers) {
1638
+ packageOf.set(classifier.id, containerOf(classifier, model.packages)?.id);
1639
+ }
1640
+ /** Which subject a use case sits in — §18.1.4's. */
1641
+ const subjectOf = new Map();
1642
+ for (const useCase of model.useCases) {
1643
+ subjectOf.set(useCase.id, containerOf(useCase, model.subjects)?.id);
1644
+ }
1645
+ const packageElement = (pkg) => {
1646
+ const inner = [];
1647
+ for (const nested of model.packages) {
1648
+ if (packageOf.get(nested.id) === pkg.id)
1649
+ inner.push(packageElement(nested));
1650
+ }
1651
+ for (const classifier of model.classifiers) {
1652
+ if (packageOf.get(classifier.id) === pkg.id) {
1653
+ inner.push(classifierElement(classifier, model, plan));
1654
+ }
1655
+ }
1656
+ const extension = keywordExtension(pkg.keywords);
1657
+ if (extension)
1658
+ inner.push(extension);
1659
+ return el('packagedElement', {
1660
+ 'xmi:type': 'uml:Package',
1661
+ 'xmi:id': plan.idOf.get(pkg.id),
1662
+ name: pkg.name,
1663
+ }, inner);
1664
+ };
1665
+ for (const pkg of model.packages) {
1666
+ if (!packageOf.get(pkg.id))
1667
+ children.push(packageElement(pkg));
1668
+ }
1669
+ for (const classifier of model.classifiers) {
1670
+ if (!packageOf.get(classifier.id)) {
1671
+ children.push(classifierElement(classifier, model, plan));
1672
+ }
1673
+ }
1674
+ for (const actor of model.actors) {
1675
+ const extension = keywordExtension(actor.keywords);
1676
+ children.push(el('packagedElement', {
1677
+ 'xmi:type': 'uml:Actor',
1678
+ 'xmi:id': plan.idOf.get(actor.id),
1679
+ name: actor.name,
1680
+ }, extension ? [extension] : []));
1681
+ }
1682
+ for (const subject of model.subjects) {
1683
+ // The subject of a use case diagram is a Classifier, and the cases drawn
1684
+ // inside it are its `ownedUseCase`s (§18.1.4) — which is containment, not a
1685
+ // reference, so they are serialized here rather than beside it.
1686
+ const owned = model.useCases
1687
+ .filter(useCase => subjectOf.get(useCase.id) === subject.id)
1688
+ .map(useCase => {
1689
+ const element = useCaseElement(useCase, model, plan);
1690
+ return el('ownedUseCase', element.attrs, element.children);
1691
+ });
1692
+ children.push(el('packagedElement', {
1693
+ 'xmi:type': 'uml:Component',
1694
+ 'xmi:id': plan.idOf.get(subject.id),
1695
+ name: subject.name,
1696
+ }, owned));
1697
+ }
1698
+ for (const useCase of model.useCases) {
1699
+ if (!subjectOf.get(useCase.id)) {
1700
+ children.push(useCaseElement(useCase, model, plan));
1701
+ }
1702
+ }
1703
+ // The structural artefacts, flat at the sheet's own package. Unlike a class,
1704
+ // none of them is attributed to a drawn PACKAGE: §12.2.4's containment is a
1705
+ // statement about namespaces made by drawing a box round a classifier, and a
1706
+ // component diagram drawn inside a package frame is a drawing this pack does
1707
+ // not read that way yet. What the cubes DO contain is written as deployments,
1708
+ // which is a reference rather than a nesting.
1709
+ for (const component of model.components) {
1710
+ children.push(componentElement(component, plan));
1711
+ }
1712
+ for (const artifact of model.artifacts) {
1713
+ children.push(artifactElement(artifact, model, plan));
1714
+ }
1715
+ for (const node of model.nodes) {
1716
+ children.push(deploymentNodeElement(node, model, plan));
1717
+ }
1718
+ // The two behaviours. Each is a `packagedElement` of the sheet's own package —
1719
+ // an Activity and a StateMachine are both Behaviors and both PackageableElements
1720
+ // — and the Activity brings with it the Signals and Events its action glyphs
1721
+ // had to reference (see {@link BehaviourSideElements}).
1722
+ for (const activity of model.activities) {
1723
+ children.push(...activityElements(activity, plan));
1724
+ }
1725
+ for (const machine of model.stateMachines) {
1726
+ children.push(stateMachineElement(machine, plan));
1727
+ }
1728
+ // §17.2 — the Interaction, a Behavior and a PackageableElement like the other
1729
+ // two, and written after them for the reason they were written after the
1730
+ // structural artefacts: appended, never interleaved.
1731
+ for (const interaction of model.interactions) {
1732
+ children.push(interactionElement(interaction, plan));
1733
+ }
1734
+ // Relationships that are packaged elements in their own right, owned by the
1735
+ // diagram's package: the nearest common namespace of their two ends, which
1736
+ // for one sheet is the sheet.
1737
+ for (const relation of model.relations) {
1738
+ let element;
1739
+ switch (relation.kind) {
1740
+ case 'association':
1741
+ case 'aggregation':
1742
+ case 'composition':
1743
+ element = associationElement(relation, plan);
1744
+ break;
1745
+ case 'dependency':
1746
+ element = dependencyElement(relation, plan);
1747
+ break;
1748
+ case 'realization':
1749
+ element =
1750
+ plan.kindOf.get(relation.targetId) === 'interface'
1751
+ ? undefined
1752
+ : realizationElement(relation, plan);
1753
+ break;
1754
+ case 'communication-path':
1755
+ // §19.4.3 — an Association between two DeploymentTargets, and an
1756
+ // Association is a packaged element in its own right.
1757
+ element = associationElement(relation, plan, 'uml:CommunicationPath');
1758
+ break;
1759
+ // Written by the element that OWNS them, above.
1760
+ case 'generalization':
1761
+ case 'include':
1762
+ case 'extend':
1763
+ // …and the two structural dependencies, owned by the artefact that
1764
+ // manifests and by the cube that hosts (§19.3.2, §19.2.2).
1765
+ case 'manifest':
1766
+ case 'deploy':
1767
+ // …and the three behaviour edges, owned by the Activity and the
1768
+ // StateMachine that hold them: `Activity::edge` and `Region::transition`
1769
+ // are both containments, so a control flow written here as well would be
1770
+ // the same arrow in the file twice.
1771
+ case 'control-flow':
1772
+ case 'object-flow':
1773
+ case 'transition':
1774
+ // …and the five MESSAGES, owned by the Interaction that holds them
1775
+ // (`Interaction::message`) and written there in the ORDER §17.4.4 draws
1776
+ // them in, which is a fact a list of relations does not carry.
1777
+ case 'message-sync':
1778
+ case 'message-async':
1779
+ case 'message-reply':
1780
+ case 'message-create':
1781
+ case 'message-delete':
1782
+ // Not a model relationship at all: an anchor attaches a Comment, and it
1783
+ // is written as that Comment's `annotatedElement`.
1784
+ case 'anchor':
1785
+ element = undefined;
1786
+ break;
1787
+ default: {
1788
+ // Exhaustive: a relationship added to the union with nothing to write
1789
+ // it as fails the build here rather than vanishing from a file.
1790
+ const never = relation.kind;
1791
+ throw new Error(`unhandled UML relation: ${String(never)}`);
1792
+ }
1793
+ }
1794
+ if (element)
1795
+ children.push(element);
1796
+ }
1797
+ for (const note of model.notes) {
1798
+ const anchored = model.relations
1799
+ .filter(relation => relation.kind === 'anchor')
1800
+ .map(relation => relation.sourceId === note.id
1801
+ ? relation.targetId
1802
+ : relation.targetId === note.id
1803
+ ? relation.sourceId
1804
+ : undefined)
1805
+ .map(id => (id ? plan.idOf.get(id) : undefined))
1806
+ .filter((id) => Boolean(id));
1807
+ children.push(el('ownedComment', {
1808
+ 'xmi:type': 'uml:Comment',
1809
+ 'xmi:id': plan.idOf.get(note.id),
1810
+ ...(anchored.length > 0
1811
+ ? { annotatedElement: anchored.join(' ') }
1812
+ : {}),
1813
+ body: note.body,
1814
+ }));
1815
+ }
1816
+ // Which of Annex A's frame kinds this package was drawn as — the one fact the
1817
+ // UML metamodel has no slot for, kept where XMI puts tool data.
1818
+ children.push(el('xmi:Extension', { extender: EXTENDER }, [
1819
+ el('diagram', { kind: model.diagram.kind }),
1820
+ ]));
1821
+ return el('packagedElement', {
1822
+ 'xmi:type': 'uml:Package',
1823
+ 'xmi:id': plan.idOf.get(model.diagram.id),
1824
+ name: model.diagram.name || model.diagram.heading,
1825
+ }, children);
1826
+ }
1827
+ /* ── The document ─────────────────────────────────────────────────────── */
1828
+ /**
1829
+ * Every diagram as one XMI document — one `uml:Model`, one package per diagram.
1830
+ *
1831
+ * A LIST rather than a single model, because that is what the format wants: XMI
1832
+ * carries a whole model, and four sheets of one architecture are four packages
1833
+ * of it, not four files. Exporting one diagram is `exportXmi([model])` and
1834
+ * produces the same shape, which is what keeps a one-diagram file and a
1835
+ * four-diagram file readable by the same importer.
1836
+ */
1837
+ export function exportXmi(models, options = {}) {
1838
+ const ids = new Ids();
1839
+ // The root takes `_1` before anything else, so the model element of a file is
1840
+ // always the first id in it.
1841
+ const rootId = ids.mint();
1842
+ const plan = planOf(models, ids);
1843
+ const children = models.map(model => diagramPackage(model, plan));
1844
+ // The DataTypes nothing on the canvas answered for, declared once at the root
1845
+ // so every package that mentions one refers to the same element.
1846
+ for (const [name, id] of plan.synthesized) {
1847
+ children.push(el('packagedElement', {
1848
+ 'xmi:type': 'uml:DataType',
1849
+ 'xmi:id': id,
1850
+ name,
1851
+ }));
1852
+ }
1853
+ return serializeDocument(el('uml:Model', {
1854
+ 'xmi:version': XMI_VERSION,
1855
+ 'xmlns:xmi': XMI_NAMESPACE,
1856
+ 'xmlns:uml': UML_NAMESPACE,
1857
+ 'xmi:id': rootId,
1858
+ name: options.name || 'Model',
1859
+ }, children));
1860
+ }