@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
@@ -0,0 +1,2268 @@
1
+ import { UML_SD_EVENT_STEP, umlSequenceColumn, umlSequenceDestruction, umlSequenceExecution, umlSequenceFragment, umlSequenceSlot, } from './import.js';
2
+ import { formatEndLabel, } from './grammar.js';
3
+ import { readXml, xmlAttr, xmlChild, xmlChildren, xmlDescendants, xmlFragmentOf, } from './xml-reader.js';
4
+ /* ── The vocabulary ───────────────────────────────────────────────────── */
5
+ /**
6
+ * The prefixes a metaclass may be written under.
7
+ *
8
+ * `uml` is Annex E's own (`tag "org.omg.xmi.nsPrefix" set to "uml"`), and it is
9
+ * what Papyrus, StarUML, MagicDraw and our own writer all use. The other two
10
+ * are casings Enterprise Architect has shipped. A bare `type="Class"` with no
11
+ * prefix at all is read too, further down, but only when the value NAMES a
12
+ * metaclass this reader knows — because `type="_7"` on an `ownedAttribute` is
13
+ * an id reference and means something else entirely.
14
+ */
15
+ const META_PREFIXES = new Set(['uml', 'UML', 'Uml']);
16
+ /** The classifier metaclasses, and the {@link UmlClassifier.kind} each is. */
17
+ const CLASSIFIER_KIND = {
18
+ Class: 'class',
19
+ Interface: 'interface',
20
+ Enumeration: 'enumeration',
21
+ InstanceSpecification: 'object',
22
+ // §10.2 makes a DataType a Classifier with attributes and operations, and the
23
+ // canvas draws it as the box §9.2.4 draws — which is a class with a keyword
24
+ // on it. The keyword is added rather than assumed, so the box says what the
25
+ // file said.
26
+ DataType: 'class',
27
+ PrimitiveType: 'class',
28
+ };
29
+ /** The metaclasses {@link CLASSIFIER_KIND} maps that need their keyword said. */
30
+ const CLASSIFIER_KEYWORD = {
31
+ DataType: 'datatype',
32
+ PrimitiveType: 'primitive',
33
+ };
34
+ /** The three cubes of §19.4.2, and the one `kind` they differ by. */
35
+ const DEPLOYMENT_KIND = {
36
+ Node: 'node',
37
+ Device: 'device',
38
+ ExecutionEnvironment: 'execution-environment',
39
+ };
40
+ /** The inverse of `xmi.ts`'s `ACTIVITY_NODE_TYPE`. */
41
+ const ACTIVITY_NODE_KIND = {
42
+ OpaqueAction: 'action',
43
+ Action: 'action',
44
+ CallBehaviorAction: 'action',
45
+ CallOperationAction: 'action',
46
+ InitialNode: 'initial',
47
+ ActivityFinalNode: 'activity-final',
48
+ FlowFinalNode: 'flow-final',
49
+ DecisionNode: 'decision',
50
+ MergeNode: 'decision',
51
+ ForkNode: 'fork',
52
+ JoinNode: 'fork',
53
+ ObjectNode: 'object-node',
54
+ CentralBufferNode: 'object-node',
55
+ DataStoreNode: 'object-node',
56
+ SendSignalAction: 'send-signal',
57
+ // Ambiguous by design — §16.10.4 draws the hourglass as an AcceptEventAction
58
+ // whose trigger names a TimeEvent, so the two are told apart by the EVENT and
59
+ // not by the metaclass. See {@link readActivity}.
60
+ AcceptEventAction: 'accept-event',
61
+ };
62
+ /** The inverse of `xmi.ts`'s `PSEUDOSTATE_KIND` (§14.5.7's enumeration). */
63
+ const PSEUDOSTATE_OF_KIND = {
64
+ initial: 'initial',
65
+ choice: 'choice',
66
+ junction: 'junction',
67
+ shallowHistory: 'shallow-history',
68
+ deepHistory: 'deep-history',
69
+ entryPoint: 'entry-point',
70
+ exitPoint: 'exit-point',
71
+ terminate: 'terminate',
72
+ fork: 'fork',
73
+ join: 'fork',
74
+ };
75
+ /** The dependency metaclasses, and the relation each states. */
76
+ const DEPENDENCY_KIND = {
77
+ Dependency: 'dependency',
78
+ Usage: 'dependency',
79
+ Abstraction: 'dependency',
80
+ Realization: 'realization',
81
+ Deployment: 'deploy',
82
+ Manifestation: 'manifest',
83
+ };
84
+ /** What Labre stamps on the XMI extensions it writes — `xmi.ts`'s `EXTENDER`. */
85
+ const LABRE_EXTENDER = 'labre';
86
+ /** The four VisibilityKinds, as §7.4 spells them in a file. */
87
+ const VISIBILITIES = new Set(['public', 'private', 'protected', 'package']);
88
+ /** The reasons a fragment is kept and not written back (ADR 0012 D5). */
89
+ export const UML_XMI_QUARANTINE_REASON = {
90
+ unmapped: 'This element has no Labre artefact, and the XMI writer builds its ' +
91
+ 'document out of the diagram alone — it has no slot to splice a fragment ' +
92
+ 'back into. It is kept in the document, verbatim, and not written back.',
93
+ danglingEnd: 'One of this relationship’s ends names an element the file does not ' +
94
+ 'declare, so there is nothing on the canvas to draw it between. It is kept ' +
95
+ 'in the document, verbatim, and not written back.',
96
+ };
97
+ /**
98
+ * The remarks this reader raises whose wording is FIXED, as `[key, English]`
99
+ * pairs — the same table, the same shape and the same reasons as
100
+ * `UML_PLANTUML_REMARKS` (`plantuml-import.ts`, where the contract is written
101
+ * out) and BPMN's own `BPMN_IMPORT_REMARKS`: one entry per SHAPE of sentence,
102
+ * `{{name}}` holes filled at the call site by `InterchangeNote.messageParams`,
103
+ * the English kept here as the fallback so a playground with no catalogue
104
+ * reads exactly what it read before.
105
+ *
106
+ * {@link UML_XMI_QUARANTINE_REASON} above is NOT part of it, and cannot be:
107
+ * its plain string is also written verbatim into
108
+ * `ForeignInterchange.quarantined[].reason` — data, never translated — so it
109
+ * cannot become a `[key, english]` pair without breaking that contract. BPMN
110
+ * carries the identical constraint on its own quarantine reasons.
111
+ */
112
+ export const UML_XMI_REMARKS = {
113
+ mintedId: [
114
+ 'com.labre.uml.import.xmi.minted-id',
115
+ '<{{tag}}> carries no xmi:id, so one was minted for it. Nothing in the file can refer to it, and nothing does.',
116
+ ],
117
+ unlistedOperator: [
118
+ 'com.labre.uml.import.xmi.unlisted-operator',
119
+ 'This combined fragment\'s operator is "{{operator}}", which UML 2.5.1 §17.6.4 does not list. It is drawn as an "alt" and the file\'s own word is kept beside it.',
120
+ ],
121
+ undrawnOccurrence: [
122
+ 'com.labre.uml.import.xmi.undrawn-occurrence',
123
+ 'A message in "{{name}}" names an occurrence this sheet has no lifeline for, so it is not drawn. The file still says it, and nothing was removed from the document.',
124
+ ],
125
+ wrongRoot: [
126
+ 'com.labre.uml.import.xmi.wrong-root',
127
+ 'A UML interchange file opens on <uml:Model> or <uml:Package>; this one opens on <{{tag}}>. Nothing was imported.',
128
+ ],
129
+ noXml: [
130
+ 'com.labre.uml.import.xmi.no-xml',
131
+ 'There is no XML element in this file, so there is no model in it.',
132
+ ],
133
+ offSheetRelation: [
134
+ 'com.labre.uml.import.xmi.off-sheet-relation',
135
+ 'A {{kind}} in "{{name}}" runs to an element that is not on this sheet, so it is not drawn. The file still says it, and nothing was removed from the document.',
136
+ ],
137
+ modelWithoutDiagram: [
138
+ 'com.labre.uml.import.xmi.model-without-diagram',
139
+ 'This file carries a model and no diagram, which is what the UML interchange format is for. Every position on the canvas was invented; none of them came from the file.',
140
+ ],
141
+ nestedWithoutDrawing: [
142
+ 'com.labre.uml.import.xmi.nested-without-drawing',
143
+ '{{count}} element(s) are declared inside another in this file — a class in a package, a use case in its subject. Labre states that by DRAWING one box inside the other, and this file carries no drawing, so they were laid out side by side. Move them into the box to say it again.',
144
+ ],
145
+ };
146
+ /* ── Reading one node ─────────────────────────────────────────────────── */
147
+ /** The local half of a qualified name — `uml:Class` is `Class`. */
148
+ function localOf(qualified) {
149
+ const colon = qualified.indexOf(':');
150
+ return colon < 0 ? qualified : qualified.slice(colon + 1);
151
+ }
152
+ /** The prefix half — `''` when there is none. */
153
+ function prefixOf(qualified) {
154
+ const colon = qualified.indexOf(':');
155
+ return colon < 0 ? '' : qualified.slice(0, colon);
156
+ }
157
+ /**
158
+ * The METACLASS an element states, in three spellings and a fallback.
159
+ *
160
+ * 1. `xmi:type="uml:Class"` — what our writer, Papyrus and MagicDraw write.
161
+ * 2. `type="uml:Class"` — StarUML's, and the reason this function exists at
162
+ * all: a bare `type` is an id reference on half the elements in a file, so
163
+ * it counts as a metaclass ONLY when its value is prefixed with one of
164
+ * {@link META_PREFIXES}.
165
+ * 3. `<uml:Class …/>` — the element itself named for its metaclass, which is
166
+ * what a UML2/Ecore file does at the root and inside a `xmi:XMI` wrapper.
167
+ */
168
+ function metaOf(node) {
169
+ for (const [key, value] of Object.entries(node.attrs)) {
170
+ if (localOf(key) !== 'type')
171
+ continue;
172
+ const prefix = prefixOf(key);
173
+ if (prefix === '') {
174
+ if (META_PREFIXES.has(prefixOf(value)))
175
+ return localOf(value);
176
+ continue;
177
+ }
178
+ // `xmi:type`, `XMI:type`, `xsi:type` — a prefixed `type` is always the
179
+ // metaclass, whatever the file calls the XMI namespace.
180
+ if (value)
181
+ return localOf(value);
182
+ }
183
+ return META_PREFIXES.has(node.prefix) ? node.local : undefined;
184
+ }
185
+ /** `true` only for the string `true` — everything else is "not stated". */
186
+ function isTrue(value) {
187
+ return value === 'true';
188
+ }
189
+ /** A `visibility="…"` that names one of §7.4's four, or nothing. */
190
+ function visibilityAttr(node) {
191
+ const raw = xmlAttr(node, 'visibility');
192
+ return raw && VISIBILITIES.has(raw) ? raw : undefined;
193
+ }
194
+ /**
195
+ * The single id an element points at, whichever of the four spellings it uses.
196
+ *
197
+ * `xmi:idref` is XMI's own, a bare attribute value is what every reference our
198
+ * writer emits looks like (`general="_4"`), a nested `<type xmi:idref="…"/>` is
199
+ * the element form, and `href="…#Name"` points into another document — the last
200
+ * resolves to the FRAGMENT, which for `PrimitiveTypes.xmi#String` is the name of
201
+ * the type and is exactly what a type name is.
202
+ */
203
+ function refOf(node) {
204
+ const idref = xmlAttr(node, 'idref');
205
+ if (idref)
206
+ return idref;
207
+ const href = xmlAttr(node, 'href');
208
+ if (href) {
209
+ const hash = href.lastIndexOf('#');
210
+ return hash < 0 ? href : href.slice(hash + 1);
211
+ }
212
+ return undefined;
213
+ }
214
+ function note(ctx, entry) {
215
+ ctx.notes.push(entry);
216
+ }
217
+ /**
218
+ * The payload riding on one source id, created on first use.
219
+ *
220
+ * `ctx.foreign` has NO prototype (see {@link importXmi}), which is what makes
221
+ * this lookup safe: on a plain object, a file whose `xmi:id` is `__proto__`
222
+ * would have this return `Object.prototype` and the caller write its payload
223
+ * onto it — every object in the process, poisoned by opening a document.
224
+ */
225
+ function foreignOf(ctx, scope) {
226
+ const known = ctx.foreign[scope];
227
+ if (known)
228
+ return known;
229
+ const payload = { id: scope };
230
+ ctx.foreign[scope] = payload;
231
+ return payload;
232
+ }
233
+ /** One attribute, kept under the scope of the element that carried it. */
234
+ function carryAttr(ctx, owner, scope, name, value) {
235
+ const payload = foreignOf(ctx, owner);
236
+ payload.attrs = {
237
+ ...payload.attrs,
238
+ [scope]: { ...payload.attrs?.[scope], [name]: value },
239
+ };
240
+ ctx.counts.carried += 1;
241
+ }
242
+ /** One fragment, kept and declared un-re-emittable (D5). */
243
+ function quarantine(ctx, owner, fragment, reason, entry) {
244
+ const payload = foreignOf(ctx, owner);
245
+ payload.quarantined = [...(payload.quarantined ?? []), { fragment, reason }];
246
+ ctx.counts.quarantined += 1;
247
+ note(ctx, { kind: 'quarantined', ...entry, message: reason });
248
+ }
249
+ /** Read, and ride under the enclosing artefact's scope. */
250
+ function consume(ctx, ...nodes) {
251
+ for (const node of nodes)
252
+ if (node)
253
+ ctx.consumed.add(node);
254
+ }
255
+ /** Read whole — the element and everything under it. */
256
+ function consumeSubtree(ctx, node) {
257
+ ctx.consumed.add(node);
258
+ for (const child of node.children)
259
+ consumeSubtree(ctx, child);
260
+ }
261
+ /**
262
+ * Read, and become an artefact of its own — the scope everything under it is
263
+ * filed against.
264
+ *
265
+ * The id is the file's, verbatim (D3). An element the file left unidentified
266
+ * gets a minted one and a note: the surface element's identity is Labre's
267
+ * either way, but a relationship pointing at an id nobody declared is a
268
+ * dangling reference, and this is where that becomes visible.
269
+ */
270
+ function map(ctx, node) {
271
+ ctx.consumed.add(node);
272
+ let id = xmlAttr(node, 'id');
273
+ if (!id) {
274
+ ctx.minted += 1;
275
+ id = `_g${ctx.minted}`;
276
+ note(ctx, {
277
+ kind: 'substituted-id',
278
+ element: node.name,
279
+ messageKey: UML_XMI_REMARKS.mintedId[0],
280
+ message: `<${node.name}> carries no xmi:id, so one was minted for it. Nothing in the file can refer to it, and nothing does.`,
281
+ messageParams: { tag: node.name },
282
+ });
283
+ }
284
+ ctx.scopeOf.set(node, id);
285
+ ctx.counts.mapped += 1;
286
+ return id;
287
+ }
288
+ /**
289
+ * The attributes each element states in the DRAWING, and therefore the ones
290
+ * that are not carried.
291
+ *
292
+ * The default row is the honest one, and it is BPMN's: an id and a name are the
293
+ * model everywhere in this format, and anything else on an element Labre draws
294
+ * is something Labre does not model and must not lose.
295
+ */
296
+ const READ_ATTRS = {
297
+ packagedElement: [
298
+ 'xmi:type',
299
+ 'type',
300
+ 'xmi:id',
301
+ 'id',
302
+ 'name',
303
+ 'isAbstract',
304
+ 'classifier',
305
+ 'client',
306
+ 'supplier',
307
+ 'memberEnd',
308
+ 'general',
309
+ 'contract',
310
+ 'source',
311
+ 'target',
312
+ 'kind',
313
+ ],
314
+ ownedAttribute: [
315
+ 'xmi:type',
316
+ 'type',
317
+ 'xmi:id',
318
+ 'id',
319
+ 'name',
320
+ 'visibility',
321
+ 'isDerived',
322
+ 'isStatic',
323
+ // `aggregation` is deliberately NOT here: a Property's aggregation is a
324
+ // fact this reader does not model on an attribute (it models it on an
325
+ // association END, where the diamond is drawn), so a file that states one —
326
+ // `aggregation="none"` included, which is a default spelled out — has it
327
+ // carried rather than swallowed.
328
+ 'association',
329
+ ],
330
+ ownedOperation: [
331
+ 'xmi:type',
332
+ 'type',
333
+ 'xmi:id',
334
+ 'id',
335
+ 'name',
336
+ 'visibility',
337
+ 'isAbstract',
338
+ 'isStatic',
339
+ 'isQuery',
340
+ ],
341
+ '': ['xmi:type', 'type', 'xmi:id', 'id', 'name'],
342
+ };
343
+ /** Everything on this element the reader did not read, kept verbatim. */
344
+ function carryUnknownAttrs(ctx, node, owner, scope = owner, understood = READ_ATTRS[node.local] ?? READ_ATTRS['']) {
345
+ for (const [name, value] of Object.entries(node.attrs)) {
346
+ if (name === 'xmlns' || name.startsWith('xmlns:'))
347
+ continue;
348
+ if (understood.includes(name))
349
+ continue;
350
+ // A prefixed spelling of an attribute the row names unprefixed, and the
351
+ // other way round: `xmi:id` and `id` are one attribute in three dialects.
352
+ if (understood.some(known => localOf(known) === localOf(name)))
353
+ continue;
354
+ carryAttr(ctx, owner, scope, name, value);
355
+ }
356
+ }
357
+ /* ── Labre's own extension ────────────────────────────────────────────── */
358
+ /** Is this the `xmi:Extension` our writer stamps? */
359
+ function isLabreExtension(node) {
360
+ return (node.local === 'Extension' && xmlAttr(node, 'extender') === LABRE_EXTENDER);
361
+ }
362
+ /**
363
+ * The keywords an element states, out of the extension `xmi.ts` files them in.
364
+ *
365
+ * The metaclass keywords are NOT put back — `«interface»` on a `uml:Interface`
366
+ * is the fact the type already states, and the writer drops it for that reason
367
+ * (`METACLASS_KEYWORDS`). Only what the file actually wrote comes back, which
368
+ * is what makes the round-trip an equality rather than an approximation.
369
+ */
370
+ function readKeywords(ctx, node) {
371
+ const keywords = [];
372
+ for (const child of node.children) {
373
+ if (!isLabreExtension(child))
374
+ continue;
375
+ let read = false;
376
+ for (const entry of xmlChildren(child, 'keyword')) {
377
+ const name = xmlAttr(entry, 'name');
378
+ if (name === undefined)
379
+ continue;
380
+ keywords.push(name);
381
+ consume(ctx, entry);
382
+ read = true;
383
+ }
384
+ // Consumed only when something in it was ours: a `xmi:Extension` stamped
385
+ // `labre` that holds something this build has never heard of is a fragment
386
+ // to keep, not one to swallow.
387
+ if (read && child.children.every(entry => ctx.consumed.has(entry))) {
388
+ consume(ctx, child);
389
+ }
390
+ }
391
+ return keywords;
392
+ }
393
+ /** Everything `UmlNodeBase` says, read off one element. */
394
+ function readBase(ctx, node, id) {
395
+ const bounds = ctx.layout[id];
396
+ return {
397
+ id,
398
+ name: xmlAttr(node, 'name') ?? '',
399
+ keywords: readKeywords(ctx, node),
400
+ isAbstract: isTrue(xmlAttr(node, 'isAbstract')),
401
+ ...(bounds ? { bounds } : {}),
402
+ };
403
+ }
404
+ /* ── Types, features, compartments ────────────────────────────────────── */
405
+ /** The NAME of the type a typed element points at, however it points. */
406
+ function typeNameOf(ctx, node) {
407
+ const child = xmlChild(node, 'type');
408
+ if (child) {
409
+ consume(ctx, child);
410
+ const ref = refOf(child);
411
+ if (ref)
412
+ return nameOfId(ctx, ref) ?? ref;
413
+ }
414
+ // `type="_7"` — an idref, which is the reading exactly when the value is NOT
415
+ // a metaclass name (see {@link metaOf}).
416
+ const attr = node.attrs['type'];
417
+ if (attr && !META_PREFIXES.has(prefixOf(attr))) {
418
+ return nameOfId(ctx, attr) ?? attr;
419
+ }
420
+ return undefined;
421
+ }
422
+ /** What the file calls the element with this id. */
423
+ function nameOfId(ctx, id) {
424
+ const node = ctx.byId.get(id);
425
+ if (!node)
426
+ return undefined;
427
+ const name = xmlAttr(node, 'name');
428
+ if (name)
429
+ return name;
430
+ const href = xmlAttr(node, 'href');
431
+ return href ? localOf(href.slice(href.lastIndexOf('#') + 1)) : undefined;
432
+ }
433
+ /** `lowerValue` / `upperValue`, when the author stated a range (§9.5.4). */
434
+ function readMultiplicity(ctx, node) {
435
+ const lower = xmlChild(node, 'lowerValue');
436
+ const upper = xmlChild(node, 'upperValue');
437
+ consume(ctx, lower, upper);
438
+ if (!lower && !upper)
439
+ return undefined;
440
+ const lowerRaw = lower ? (xmlAttr(lower, 'value') ?? '0') : '1';
441
+ const upperRaw = upper ? (xmlAttr(upper, 'value') ?? '1') : '1';
442
+ const lowerNumber = Number.parseInt(lowerRaw, 10);
443
+ const upperNumber = Number.parseInt(upperRaw, 10);
444
+ return {
445
+ lower: Number.isFinite(lowerNumber) ? lowerNumber : 0,
446
+ // `*` is the unlimited value of UnlimitedNatural (Annex E.3 requires it to
447
+ // be serialized as exactly that); `-1` is the sentinel Eclipse UML2 writes
448
+ // for it, and reading it as the number would be an upper bound below the
449
+ // lower one.
450
+ upper: upperRaw === '*' || upperRaw === '-1'
451
+ ? '*'
452
+ : Number.isFinite(upperNumber)
453
+ ? upperNumber
454
+ : 1,
455
+ };
456
+ }
457
+ /** A `<defaultValue value="…"/>`, whichever LiteralSpecification it is. */
458
+ function readDefault(ctx, node) {
459
+ const value = xmlChild(node, 'defaultValue');
460
+ if (!value)
461
+ return undefined;
462
+ consume(ctx, value);
463
+ return xmlAttr(value, 'value') ?? value.text?.trim();
464
+ }
465
+ /** One `ownedAttribute` as the compartment line it was read off. */
466
+ function readProperty(ctx, node) {
467
+ const multiplicity = readMultiplicity(ctx, node);
468
+ const defaultValue = readDefault(ctx, node);
469
+ const type = typeNameOf(ctx, node);
470
+ const visibility = visibilityAttr(node);
471
+ return {
472
+ ...(visibility ? { visibility } : {}),
473
+ isDerived: isTrue(xmlAttr(node, 'isDerived')),
474
+ name: xmlAttr(node, 'name') ?? '',
475
+ ...(type ? { type } : {}),
476
+ ...(multiplicity ? { multiplicity } : {}),
477
+ ...(defaultValue ? { defaultValue } : {}),
478
+ modifiers: [],
479
+ ...(isTrue(xmlAttr(node, 'isStatic')) ? { isStatic: true } : {}),
480
+ };
481
+ }
482
+ /**
483
+ * One `ownedParameter`, minus the one that carries the return type.
484
+ *
485
+ * The direction is kept whenever the file STATES one, `in` included — and the
486
+ * file states one on every parameter, because §9.4.4's default is applied by
487
+ * the writer that needs a value (`xmi.ts`, `direction: parameter.direction ??
488
+ * 'in'`). So "the author wrote no direction" is a fact the format does not
489
+ * carry, and this reader reports what the file says rather than guessing which
490
+ * `in` was typed and which was supplied.
491
+ */
492
+ function readParameter(ctx, node) {
493
+ const multiplicity = readMultiplicity(ctx, node);
494
+ const defaultValue = readDefault(ctx, node);
495
+ const type = typeNameOf(ctx, node);
496
+ const direction = xmlAttr(node, 'direction');
497
+ return {
498
+ ...(direction ? { direction: direction } : {}),
499
+ name: xmlAttr(node, 'name') ?? '',
500
+ ...(type ? { type } : {}),
501
+ ...(multiplicity ? { multiplicity } : {}),
502
+ ...(defaultValue ? { defaultValue } : {}),
503
+ };
504
+ }
505
+ /**
506
+ * One `ownedOperation`, with §9.4.4's return parameter folded back into it.
507
+ *
508
+ * `direction="return"` is where UML puts a result — there is no separate slot —
509
+ * so reading it back into {@link UmlOperation.returnType} is not a convenience,
510
+ * it is the inverse of what the writer had to do to say it at all.
511
+ */
512
+ function readOperation(ctx, node) {
513
+ const parameters = [];
514
+ let returnType;
515
+ let returnMultiplicity;
516
+ for (const child of xmlChildren(node, 'ownedParameter')) {
517
+ consume(ctx, child);
518
+ if (xmlAttr(child, 'direction') === 'return') {
519
+ returnMultiplicity = readMultiplicity(ctx, child);
520
+ returnType = typeNameOf(ctx, child);
521
+ readDefault(ctx, child);
522
+ continue;
523
+ }
524
+ parameters.push(readParameter(ctx, child));
525
+ }
526
+ const visibility = visibilityAttr(node);
527
+ return {
528
+ ...(visibility ? { visibility } : {}),
529
+ name: xmlAttr(node, 'name') ?? '',
530
+ parameters,
531
+ ...(returnType ? { returnType } : {}),
532
+ ...(returnMultiplicity ? { returnMultiplicity } : {}),
533
+ modifiers: isTrue(xmlAttr(node, 'isQuery')) ? ['query'] : [],
534
+ ...(isTrue(xmlAttr(node, 'isStatic')) ? { isStatic: true } : {}),
535
+ ...(isTrue(xmlAttr(node, 'isAbstract')) ? { isAbstract: true } : {}),
536
+ };
537
+ }
538
+ /* ── Compartments, re-spelled ─────────────────────────────────────────── */
539
+ /** The four visibility markers of §7.4, by the VisibilityKind each is. */
540
+ const VISIBILITY_MARKER = {
541
+ public: '+',
542
+ private: '-',
543
+ protected: '#',
544
+ package: '~',
545
+ };
546
+ /** `[1..*]`, `[0..1]`, `[3]` — §7.5.4's own notation. */
547
+ function multiplicityText(multiplicity) {
548
+ const { lower, upper } = multiplicity;
549
+ return lower === upper ? `[${lower}]` : `[${lower}..${upper}]`;
550
+ }
551
+ /**
552
+ * A parsed property back as the compartment line it would have been typed as.
553
+ *
554
+ * `UmlClassifier.lines` is the compartment AS THE AUTHOR TYPED IT, and an
555
+ * import has no author to ask: the file carries the structure, not the text.
556
+ * So the line is re-spelled in §9.5.4's own notation, which is the one form
557
+ * both writers already agree on — PlantUML prints these lines verbatim, and
558
+ * feeding it a line it could not have produced is the one way this reader could
559
+ * poison the other export.
560
+ *
561
+ * It is re-spelled and not invented: every part of it came out of the file.
562
+ */
563
+ function propertyLine(property) {
564
+ const parts = [];
565
+ if (property.visibility)
566
+ parts.push(VISIBILITY_MARKER[property.visibility]);
567
+ if (property.isDerived)
568
+ parts.push('/');
569
+ parts.push(property.name);
570
+ if (property.type)
571
+ parts.push(`: ${property.type}`);
572
+ if (property.multiplicity)
573
+ parts.push(multiplicityText(property.multiplicity));
574
+ if (property.defaultValue)
575
+ parts.push(`= ${property.defaultValue}`);
576
+ if (property.isStatic)
577
+ parts.push('{static}');
578
+ return parts.join(' ').trim();
579
+ }
580
+ /** A parsed operation back as its compartment line — §9.6.4's notation. */
581
+ function operationLine(operation) {
582
+ const parameters = operation.parameters
583
+ .map(parameter => {
584
+ const head = parameter.direction
585
+ ? `${parameter.direction} ${parameter.name}`
586
+ : parameter.name;
587
+ const typed = parameter.type ? `${head} : ${parameter.type}` : head;
588
+ const bounded = parameter.multiplicity
589
+ ? `${typed} ${multiplicityText(parameter.multiplicity)}`
590
+ : typed;
591
+ return parameter.defaultValue
592
+ ? `${bounded} = ${parameter.defaultValue}`
593
+ : bounded;
594
+ })
595
+ .join(', ');
596
+ const parts = [];
597
+ if (operation.visibility)
598
+ parts.push(VISIBILITY_MARKER[operation.visibility]);
599
+ parts.push(`${operation.name}(${parameters})`);
600
+ if (operation.returnType)
601
+ parts.push(`: ${operation.returnType}`);
602
+ if (operation.returnMultiplicity) {
603
+ parts.push(multiplicityText(operation.returnMultiplicity));
604
+ }
605
+ if (operation.isAbstract)
606
+ parts.push('{abstract}');
607
+ if (operation.isStatic)
608
+ parts.push('{static}');
609
+ for (const modifier of operation.modifiers)
610
+ parts.push(`{${modifier}}`);
611
+ return parts.join(' ').trim();
612
+ }
613
+ /* ── Diagram interchange (Annex E.5's `umldi`, and Papyrus's `notation`) ─ */
614
+ /** A number an attribute states, or nothing. */
615
+ function numberAttr(node, name) {
616
+ const raw = xmlAttr(node, name);
617
+ if (raw === undefined || raw.trim() === '')
618
+ return undefined;
619
+ const value = Number(raw);
620
+ return Number.isFinite(value) ? value : undefined;
621
+ }
622
+ /**
623
+ * Every shape the file draws, by the id of the element it draws.
624
+ *
625
+ * Two dialects, and the same four numbers in both:
626
+ *
627
+ * - **UMLDI** (Annex E.5, `umldi` prefix): a `UMLShape` with a `modelElement`
628
+ * and a `<bounds x= y= width= height=/>`;
629
+ * - **Papyrus `.notation`** (GMF): a `<children xmi:type="notation:Shape"
630
+ * element="…">` with a `<layoutConstraint xmi:type="notation:Bounds">`.
631
+ *
632
+ * Read by SHAPE rather than by walking the diagram tree, because the diagram
633
+ * tree is where the two dialects differ and the shape is where they agree.
634
+ * Edges are not read at all: a connector on this canvas routes itself between
635
+ * its two ends and re-routes whenever either moves, so a frozen waypoint list
636
+ * would be a path that stops following the drawing (D4's own reasoning about
637
+ * two-point edges, applied to all of them until phase 3 says otherwise).
638
+ */
639
+ function readLayout(root) {
640
+ // No prototype, for the reason `importXmi` gives: the key is the file's.
641
+ const layout = Object.create(null);
642
+ for (const node of [root, ...xmlDescendants(root)]) {
643
+ const meta = xmlAttr(node, 'type') ?? '';
644
+ const isShape = node.local === 'UMLShape' ||
645
+ localOf(meta) === 'UMLShape' ||
646
+ localOf(meta) === 'Shape';
647
+ if (!isShape)
648
+ continue;
649
+ const target = xmlAttr(node, 'modelElement') ??
650
+ xmlAttr(node, 'element') ??
651
+ refOf(xmlChild(node, 'modelElement') ?? node);
652
+ if (!target)
653
+ continue;
654
+ const box = xmlChild(node, 'bounds') ?? xmlChild(node, 'layoutConstraint');
655
+ const holder = box ?? node;
656
+ const x = numberAttr(holder, 'x');
657
+ const y = numberAttr(holder, 'y');
658
+ const w = numberAttr(holder, 'width');
659
+ const h = numberAttr(holder, 'height');
660
+ if (x === undefined || y === undefined)
661
+ continue;
662
+ layout[target] = { x, y, w: w ?? 0, h: h ?? 0 };
663
+ }
664
+ return layout;
665
+ }
666
+ function emptyModel(id, name) {
667
+ return {
668
+ diagram: { id, kind: 'class', name, heading: '' },
669
+ classifiers: [],
670
+ packages: [],
671
+ actors: [],
672
+ useCases: [],
673
+ subjects: [],
674
+ notes: [],
675
+ components: [],
676
+ ports: [],
677
+ artifacts: [],
678
+ nodes: [],
679
+ activities: [],
680
+ stateMachines: [],
681
+ interactions: [],
682
+ relations: [],
683
+ warnings: [],
684
+ };
685
+ }
686
+ /** Record an artefact of this sheet, so a relation can resolve against it. */
687
+ function own(draft, id) {
688
+ draft.ids.add(id);
689
+ return id;
690
+ }
691
+ /** The label a relationship wears — its `name`, when the file wrote one. */
692
+ function labelOf(node) {
693
+ const name = xmlAttr(node, 'name');
694
+ return name ? { label: name } : {};
695
+ }
696
+ /** One relation, once both of its ends are ids this sheet owns. */
697
+ function relate(draft, kind, sourceId, targetId,
698
+ // The centre label AND, for an association, §11.5.4's two end labels — every
699
+ // part of a relation that is not its kind or its two ends.
700
+ label) {
701
+ if (!sourceId || !targetId)
702
+ return false;
703
+ const relation = { kind, sourceId, targetId, ...label };
704
+ draft.model.relations.push(relation);
705
+ return true;
706
+ }
707
+ /* ── The element readers ──────────────────────────────────────────────── */
708
+ /**
709
+ * One classifier, with the features and the two relationships it owns.
710
+ *
711
+ * An `ownedAttribute` that names an `association` is NOT a feature: it is an
712
+ * association END the classifier happens to own (§11.5.4 lets either side own
713
+ * one), and reading it as an attribute would put `order : Customer` in the
714
+ * compartment of a class whose author drew a line.
715
+ */
716
+ function readClassifier(ctx, draft, node, meta, id) {
717
+ const kind = CLASSIFIER_KIND[meta];
718
+ const base = readBase(ctx, node, id);
719
+ const keyword = CLASSIFIER_KEYWORD[meta];
720
+ const attributes = [];
721
+ const operations = [];
722
+ const slots = [];
723
+ for (const child of node.children) {
724
+ if (child.local === 'ownedAttribute') {
725
+ const association = xmlAttr(child, 'association');
726
+ const endId = xmlAttr(child, 'id');
727
+ if (association && endId) {
728
+ consume(ctx, child);
729
+ draft.ends.set(endId, {
730
+ typeId: endTypeId(ctx, child),
731
+ ...(xmlAttr(child, 'aggregation') &&
732
+ xmlAttr(child, 'aggregation') !== 'none'
733
+ ? { aggregation: xmlAttr(child, 'aggregation') }
734
+ : {}),
735
+ });
736
+ continue;
737
+ }
738
+ consume(ctx, child);
739
+ carryUnknownAttrs(ctx, child, id, endId ?? id);
740
+ attributes.push(readProperty(ctx, child));
741
+ }
742
+ else if (child.local === 'ownedLiteral') {
743
+ // An enumeration's compartment holds LITERALS: a name and nothing else,
744
+ // which is what the writer reads out of the line and all it writes back.
745
+ consume(ctx, child);
746
+ attributes.push({
747
+ isDerived: false,
748
+ name: xmlAttr(child, 'name') ?? '',
749
+ modifiers: [],
750
+ });
751
+ }
752
+ else if (child.local === 'ownedOperation') {
753
+ consume(ctx, child);
754
+ carryUnknownAttrs(ctx, child, id, xmlAttr(child, 'id') ?? id);
755
+ operations.push(readOperation(ctx, child));
756
+ }
757
+ else if (child.local === 'slot') {
758
+ consume(ctx, child);
759
+ const value = xmlChild(child, 'value');
760
+ const extension = child.children.find(isLabreExtension);
761
+ const feature = extension ? xmlChild(extension, 'feature') : undefined;
762
+ consume(ctx, value, extension, feature);
763
+ const slotValue = value ? xmlAttr(value, 'value') : undefined;
764
+ slots.push({
765
+ name: feature ? (xmlAttr(feature, 'name') ?? '') : '',
766
+ ...(slotValue ? { value: slotValue } : {}),
767
+ });
768
+ }
769
+ else if (child.local === 'generalization') {
770
+ consume(ctx, child);
771
+ relate(draft, 'generalization', id, xmlAttr(child, 'general'));
772
+ }
773
+ else if (child.local === 'interfaceRealization') {
774
+ consume(ctx, child);
775
+ relate(draft, 'realization', id, xmlAttr(child, 'contract') ?? xmlAttr(child, 'supplier'), labelOf(child));
776
+ }
777
+ }
778
+ const classifier = {
779
+ ...base,
780
+ keywords: keyword ? [keyword, ...base.keywords] : base.keywords,
781
+ kind,
782
+ attributes: kind === 'object' ? [] : attributes,
783
+ operations: kind === 'object' ? [] : operations,
784
+ slots: kind === 'object' ? slots : [],
785
+ lines: {
786
+ attributes: kind === 'object' ? [] : attributes.map(propertyLine),
787
+ operations: kind === 'object' ? [] : operations.map(operationLine),
788
+ },
789
+ };
790
+ const instanceOf = xmlAttr(node, 'classifier');
791
+ if (kind === 'object' && instanceOf) {
792
+ const named = nameOfId(ctx, instanceOf);
793
+ if (named)
794
+ classifier.instanceOf = named;
795
+ }
796
+ draft.model.classifiers.push(classifier);
797
+ own(draft, id);
798
+ }
799
+ /** What an association end is typed by — an id, not a name. */
800
+ function endTypeId(ctx, node) {
801
+ const child = xmlChild(node, 'type');
802
+ if (child) {
803
+ consume(ctx, child);
804
+ const ref = refOf(child);
805
+ if (ref)
806
+ return ref;
807
+ }
808
+ const attr = node.attrs['type'];
809
+ return attr && !META_PREFIXES.has(prefixOf(attr)) ? attr : undefined;
810
+ }
811
+ /**
812
+ * One association, with the diamond on the end the metamodel puts it on.
813
+ *
814
+ * `Property::isComposite` is a statement about the end whose TYPE is the part —
815
+ * "the object containing the attribute is a container for the object contained
816
+ * in the attribute" — so the flagged end is the PART and the whole is the other
817
+ * one. That is the reading `xmi.ts` writes with, and reading it back the same
818
+ * way is what keeps a diamond on the same classifier after a round trip.
819
+ *
820
+ * ## Where an end's adornments land
821
+ *
822
+ * On the end DRAWN at the classifier that types it — the identity §11.5.4 states
823
+ * and `xmi.ts` writes with, read back the same way round. So the `lowerValue` /
824
+ * `upperValue`, the `name` and the `visibility` of the end typed by the WHOLE
825
+ * become {@link UmlRelation.sourceEnd}, and the part's end becomes `targetEnd`.
826
+ * They used to ride in `interchange.xmi.attrs` because the canvas had nowhere to
827
+ * draw them (ADR 0018); ADR 0020 gave the connector two end labels, so they are
828
+ * now on the board and are no longer carried — carrying a fact the drawing shows
829
+ * would put it in the document twice and re-emit it beside the one the writer
830
+ * produces. Everything else an end says is still carried verbatim.
831
+ */
832
+ function readAssociation(ctx, draft, node, id, kind) {
833
+ // Both spellings of `memberEnd`: an idref list, and one element per end.
834
+ const listed = (xmlAttr(node, 'memberEnd') ?? '')
835
+ .split(/\s+/)
836
+ .filter(Boolean);
837
+ for (const child of xmlChildren(node, 'memberEnd')) {
838
+ consume(ctx, child);
839
+ const ref = refOf(child);
840
+ if (ref)
841
+ listed.push(ref);
842
+ }
843
+ for (const child of xmlChildren(node, 'ownedEnd')) {
844
+ consume(ctx, child);
845
+ const endId = xmlAttr(child, 'id');
846
+ if (!endId)
847
+ continue;
848
+ const aggregation = xmlAttr(child, 'aggregation');
849
+ draft.ends.set(endId, {
850
+ typeId: endTypeId(ctx, child),
851
+ ...(aggregation && aggregation !== 'none' ? { aggregation } : {}),
852
+ ...adornmentsOf(ctx, child),
853
+ });
854
+ if (!listed.includes(endId))
855
+ listed.push(endId);
856
+ if (aggregation && aggregation !== 'none') {
857
+ carryAttr(ctx, id, endId, 'aggregation', aggregation);
858
+ }
859
+ }
860
+ // Ends the association only NAMES — owned by a classifier, which is where
861
+ // Papyrus and StarUML put a navigable one.
862
+ for (const endId of listed) {
863
+ if (draft.ends.has(endId))
864
+ continue;
865
+ const owned = ctx.byId.get(endId);
866
+ if (!owned)
867
+ continue;
868
+ const aggregation = xmlAttr(owned, 'aggregation');
869
+ draft.ends.set(endId, {
870
+ typeId: endTypeId(ctx, owned),
871
+ ...(aggregation && aggregation !== 'none' ? { aggregation } : {}),
872
+ ...adornmentsOf(ctx, owned),
873
+ });
874
+ }
875
+ const [first, second] = listed.map(endId => ({
876
+ id: endId,
877
+ ...(draft.ends.get(endId) ?? {}),
878
+ }));
879
+ if (!first || !second || !first.typeId || !second.typeId) {
880
+ quarantine(ctx, draft.model.diagram.id, xmlFragmentOf(ctx.source, node), UML_XMI_QUARANTINE_REASON.danglingEnd, { sourceId: id, element: node.name });
881
+ return;
882
+ }
883
+ // The flagged end is the PART; the whole is the other one. With no flag the
884
+ // file's own order is the direction, which is what the writer wrote.
885
+ const flagged = second.aggregation
886
+ ? second
887
+ : first.aggregation
888
+ ? first
889
+ : undefined;
890
+ const part = flagged ?? second;
891
+ const whole = flagged === first ? second : first;
892
+ const aggregation = flagged?.aggregation;
893
+ const relationKind = kind === 'communication-path'
894
+ ? 'communication-path'
895
+ : aggregation === 'composite'
896
+ ? 'composition'
897
+ : aggregation === 'shared'
898
+ ? 'aggregation'
899
+ : 'association';
900
+ carryAttr(ctx, id, '@ends', 'source', whole.id);
901
+ carryAttr(ctx, id, '@ends', 'target', part.id);
902
+ relate(draft, relationKind, whole.typeId, part.typeId, {
903
+ ...labelOf(node),
904
+ ...(whole.label ? { sourceEnd: whole.label } : {}),
905
+ ...(part.label ? { targetEnd: part.label } : {}),
906
+ });
907
+ }
908
+ /**
909
+ * The §11.5.4 adornments one association end states — or nothing.
910
+ *
911
+ * `raw` is composed rather than read off the file, because the file has no such
912
+ * string: XMI states the range, the name and the visibility as three separate
913
+ * pieces of markup, and what goes BESIDE the end on the board is their one-line
914
+ * spelling. {@link formatEndLabel} is the same printer the PlantUML writer uses,
915
+ * so an end imported from XMI and one exported to a `.puml` read identically.
916
+ */
917
+ function adornmentsOf(ctx, node) {
918
+ const multiplicity = readMultiplicity(ctx, node);
919
+ const role = xmlAttr(node, 'name');
920
+ const visibility = visibilityAttr(node);
921
+ if (!multiplicity && !role && !visibility)
922
+ return {};
923
+ const parts = {
924
+ ...(multiplicity ? { multiplicity } : {}),
925
+ ...(role ? { role } : {}),
926
+ ...(visibility ? { visibility } : {}),
927
+ };
928
+ return { label: { ...parts, raw: formatEndLabel({ ...parts, raw: '' }) } };
929
+ }
930
+ /** One component, with its ports and the two kinds of interface it wires. */
931
+ function readComponent(ctx, draft, node, id) {
932
+ const base = readBase(ctx, node, id);
933
+ const ports = [];
934
+ const provided = [];
935
+ const required = [];
936
+ /** The interfaces this component declares, so a wire can name one. */
937
+ const declared = new Map();
938
+ for (const child of node.children) {
939
+ const meta = metaOf(child) ?? '';
940
+ if (child.local === 'ownedAttribute' && meta === 'Port') {
941
+ consume(ctx, child);
942
+ const portId = map(ctx, child);
943
+ ports.push({ ...readBase(ctx, child, portId), ownerId: id });
944
+ continue;
945
+ }
946
+ if (child.local === 'packagedElement' && meta === 'Interface') {
947
+ consume(ctx, child);
948
+ const interfaceId = xmlAttr(child, 'id');
949
+ if (interfaceId)
950
+ declared.set(interfaceId, xmlAttr(child, 'name') ?? '');
951
+ continue;
952
+ }
953
+ if (child.local === 'interfaceRealization') {
954
+ consume(ctx, child);
955
+ const contract = xmlAttr(child, 'contract') ?? xmlAttr(child, 'supplier');
956
+ const name = contract
957
+ ? (declared.get(contract) ?? nameOfId(ctx, contract))
958
+ : undefined;
959
+ if (name !== undefined)
960
+ provided.push(name);
961
+ else if (contract)
962
+ relate(draft, 'realization', id, contract);
963
+ continue;
964
+ }
965
+ if (child.local === 'packagedElement' && meta === 'Usage') {
966
+ consume(ctx, child);
967
+ const supplier = xmlAttr(child, 'supplier');
968
+ const name = supplier
969
+ ? (declared.get(supplier) ?? nameOfId(ctx, supplier))
970
+ : undefined;
971
+ if (name !== undefined && supplier && declared.has(supplier)) {
972
+ required.push(name);
973
+ }
974
+ else if (supplier) {
975
+ relate(draft, 'dependency', id, supplier, labelOf(child));
976
+ }
977
+ continue;
978
+ }
979
+ }
980
+ // An interface the component declared and nothing wired to is still one the
981
+ // file said it provides — the lollipop with no line is §10.4.4's own drawing.
982
+ const wired = new Set([...provided, ...required]);
983
+ for (const name of declared.values()) {
984
+ if (!wired.has(name))
985
+ provided.push(name);
986
+ }
987
+ for (const port of ports)
988
+ own(draft, port.id);
989
+ draft.model.ports.push(...ports);
990
+ return { ...base, ports, provided, required };
991
+ }
992
+ /** One use case, with the two relationships §18.1.4 makes it own. */
993
+ function readUseCase(ctx, draft, node, id) {
994
+ for (const child of xmlChildren(node, 'include')) {
995
+ consume(ctx, child);
996
+ relate(draft, 'include', id, xmlAttr(child, 'addition'));
997
+ }
998
+ for (const child of xmlChildren(node, 'extend')) {
999
+ consume(ctx, child);
1000
+ relate(draft, 'extend', id, xmlAttr(child, 'extendedCase'));
1001
+ }
1002
+ return readBase(ctx, node, id);
1003
+ }
1004
+ /** One activity — its glyphs, its arrows and its lanes (§15.2.4, §15.6.4). */
1005
+ function readActivity(ctx, draft, node, id) {
1006
+ const nodes = [];
1007
+ const edges = [];
1008
+ const partitions = [];
1009
+ const byId = new Map();
1010
+ for (const child of xmlChildren(node, 'node')) {
1011
+ const meta = metaOf(child) ?? '';
1012
+ const kind = ACTIVITY_NODE_KIND[meta];
1013
+ if (!kind)
1014
+ continue;
1015
+ consume(ctx, child);
1016
+ const nodeId = map(ctx, child);
1017
+ carryUnknownAttrs(ctx, child, nodeId, nodeId, [
1018
+ 'xmi:type',
1019
+ 'type',
1020
+ 'xmi:id',
1021
+ 'id',
1022
+ 'name',
1023
+ 'signal',
1024
+ ]);
1025
+ // §16.10.4's hourglass IS an AcceptEventAction — the one whose trigger
1026
+ // names a TimeEvent. The metaclass cannot tell the two apart, so the EVENT
1027
+ // is asked, exactly as the writer had to say it through one.
1028
+ let resolved = kind;
1029
+ for (const trigger of xmlChildren(child, 'trigger')) {
1030
+ consume(ctx, trigger);
1031
+ const event = xmlAttr(trigger, 'event');
1032
+ const declaration = event ? ctx.byId.get(event) : undefined;
1033
+ if (declaration && (metaOf(declaration) ?? '') === 'TimeEvent') {
1034
+ resolved = 'time-event';
1035
+ }
1036
+ }
1037
+ const activityNode = {
1038
+ ...readBase(ctx, child, nodeId),
1039
+ kind: resolved,
1040
+ };
1041
+ nodes.push(activityNode);
1042
+ byId.set(nodeId, activityNode);
1043
+ own(draft, nodeId);
1044
+ const signal = xmlAttr(child, 'signal');
1045
+ if (signal)
1046
+ draft.usedSignals.add(signal);
1047
+ }
1048
+ for (const child of xmlChildren(node, 'edge')) {
1049
+ consume(ctx, child);
1050
+ const source = xmlAttr(child, 'source');
1051
+ const target = xmlAttr(child, 'target');
1052
+ if (!source || !target)
1053
+ continue;
1054
+ const guard = xmlChild(child, 'guard');
1055
+ const weight = xmlChild(child, 'weight');
1056
+ consume(ctx, guard, weight);
1057
+ const name = xmlAttr(child, 'name');
1058
+ const guardText = guard
1059
+ ? (xmlAttr(guard, 'body') ?? guard.text?.trim())
1060
+ : undefined;
1061
+ const weightText = weight
1062
+ ? (xmlAttr(weight, 'value') ?? xmlAttr(weight, 'body'))
1063
+ : undefined;
1064
+ edges.push({
1065
+ kind: (metaOf(child) ?? '') === 'ObjectFlow' ? 'object-flow' : 'control-flow',
1066
+ sourceId: source,
1067
+ targetId: target,
1068
+ ...(name ? { name } : {}),
1069
+ ...(guardText ? { guard: guardText } : {}),
1070
+ ...(weightText ? { weight: weightText } : {}),
1071
+ });
1072
+ }
1073
+ for (const child of xmlChildren(node, 'group')) {
1074
+ if ((metaOf(child) ?? '') !== 'ActivityPartition')
1075
+ continue;
1076
+ consume(ctx, child);
1077
+ const partitionId = map(ctx, child);
1078
+ const nodeIds = (xmlAttr(child, 'node') ?? '')
1079
+ .split(/\s+/)
1080
+ .filter(nodeId => byId.has(nodeId));
1081
+ for (const nodeId of nodeIds)
1082
+ byId.get(nodeId).partitionId = partitionId;
1083
+ partitions.push({
1084
+ ...readBase(ctx, child, partitionId),
1085
+ // §15.6.4's band runs down the sheet unless the author turned it, and the
1086
+ // metamodel has no slot for which way it was drawn: `ActivityPartition`
1087
+ // says what it holds, never how it is oriented. A column is what the
1088
+ // creation site draws and what an invented layout can honour.
1089
+ orientation: 'vertical',
1090
+ nodeIds,
1091
+ });
1092
+ own(draft, partitionId);
1093
+ }
1094
+ return { id, name: xmlAttr(node, 'name') ?? '', nodes, edges, partitions };
1095
+ }
1096
+ /** One state machine — its regions, its vertices and its transitions. */
1097
+ function readStateMachine(ctx, draft, node, id) {
1098
+ const machine = {
1099
+ id,
1100
+ name: xmlAttr(node, 'name') ?? '',
1101
+ regions: [],
1102
+ states: [],
1103
+ finalStates: [],
1104
+ pseudostates: [],
1105
+ transitions: [],
1106
+ };
1107
+ /**
1108
+ * One `<region>`, and everything drawn in it.
1109
+ *
1110
+ * `regionId` is the COMPOSITE STATE the region belongs to, not the region
1111
+ * element: the canvas draws the composite state as a box and never draws the
1112
+ * region inside it, so that box's id is the one every vertex is filed under
1113
+ * — which is exactly what `xmi.ts`'s `subvertexElements` selects on.
1114
+ */
1115
+ const readRegion = (region, regionId) => {
1116
+ consume(ctx, region);
1117
+ for (const child of xmlChildren(region, 'subvertex')) {
1118
+ const meta = metaOf(child) ?? '';
1119
+ consume(ctx, child);
1120
+ const vertexId = map(ctx, child);
1121
+ const base = readBase(ctx, child, vertexId);
1122
+ own(draft, vertexId);
1123
+ if (meta === 'FinalState') {
1124
+ machine.finalStates.push({
1125
+ ...base,
1126
+ ...(regionId ? { regionId } : {}),
1127
+ });
1128
+ continue;
1129
+ }
1130
+ if (meta === 'Pseudostate') {
1131
+ const kind = PSEUDOSTATE_OF_KIND[xmlAttr(child, 'kind') ?? 'initial'];
1132
+ const pseudostate = {
1133
+ ...base,
1134
+ kind: kind ?? 'junction',
1135
+ ...(regionId ? { regionId } : {}),
1136
+ };
1137
+ machine.pseudostates.push(pseudostate);
1138
+ continue;
1139
+ }
1140
+ const nested = xmlChild(child, 'region');
1141
+ if (nested) {
1142
+ // A `uml:State` with a Region of its own is §14.2.4's COMPOSITE state,
1143
+ // and on this canvas that is a `umlRegion` box rather than a state.
1144
+ machine.regions.push({
1145
+ ...base,
1146
+ ...(regionId ? { parentId: regionId } : {}),
1147
+ });
1148
+ readRegion(nested, vertexId);
1149
+ continue;
1150
+ }
1151
+ const entry = [];
1152
+ const doActivity = [];
1153
+ const exit = [];
1154
+ for (const behaviour of child.children) {
1155
+ const body = xmlAttr(behaviour, 'body') ??
1156
+ xmlAttr(behaviour, 'name') ??
1157
+ behaviour.text?.trim();
1158
+ if (body === undefined)
1159
+ continue;
1160
+ if (behaviour.local === 'entry')
1161
+ entry.push(body);
1162
+ else if (behaviour.local === 'doActivity')
1163
+ doActivity.push(body);
1164
+ else if (behaviour.local === 'exit')
1165
+ exit.push(body);
1166
+ else
1167
+ continue;
1168
+ consume(ctx, behaviour);
1169
+ }
1170
+ machine.states.push({
1171
+ ...base,
1172
+ entry,
1173
+ doActivity,
1174
+ exit,
1175
+ lines: [],
1176
+ ...(regionId ? { regionId } : {}),
1177
+ });
1178
+ }
1179
+ for (const child of xmlChildren(region, 'transition')) {
1180
+ consume(ctx, child);
1181
+ const source = xmlAttr(child, 'source');
1182
+ const target = xmlAttr(child, 'target');
1183
+ if (!source || !target)
1184
+ continue;
1185
+ const triggers = [];
1186
+ for (const trigger of xmlChildren(child, 'trigger')) {
1187
+ consume(ctx, trigger);
1188
+ const name = xmlAttr(trigger, 'name');
1189
+ if (name)
1190
+ triggers.push(name);
1191
+ }
1192
+ const guard = xmlChild(child, 'guard');
1193
+ const effect = xmlChild(child, 'effect');
1194
+ consume(ctx, guard, effect);
1195
+ const specification = guard
1196
+ ? xmlChild(guard, 'specification')
1197
+ : undefined;
1198
+ consume(ctx, specification);
1199
+ const guardText = specification
1200
+ ? (xmlAttr(specification, 'body') ?? specification.text?.trim())
1201
+ : guard
1202
+ ? xmlAttr(guard, 'body')
1203
+ : undefined;
1204
+ const effectText = effect
1205
+ ? (xmlAttr(effect, 'body') ?? xmlAttr(effect, 'name'))
1206
+ : undefined;
1207
+ machine.transitions.push({
1208
+ sourceId: source,
1209
+ targetId: target,
1210
+ triggers,
1211
+ ...(guardText ? { guard: guardText } : {}),
1212
+ ...(effectText ? { effect: effectText } : {}),
1213
+ });
1214
+ }
1215
+ };
1216
+ for (const region of xmlChildren(node, 'region'))
1217
+ readRegion(region, undefined);
1218
+ return machine;
1219
+ }
1220
+ /* ── Interactions (§17) ───────────────────────────────────────────────── */
1221
+ /** §17.4.3's `messageSort`, read back to the arrow §17.4.4 draws for it. */
1222
+ const MESSAGE_KIND_OF_SORT = {
1223
+ synchCall: 'message-sync',
1224
+ asynchCall: 'message-async',
1225
+ asynchSignal: 'message-async',
1226
+ reply: 'message-reply',
1227
+ createMessage: 'message-create',
1228
+ deleteMessage: 'message-delete',
1229
+ };
1230
+ /** The metaclasses §17.2.4 draws as the bar on a spine, whatever the tool. */
1231
+ const EXECUTION_TYPES = new Set([
1232
+ 'BehaviorExecutionSpecification',
1233
+ 'ActionExecutionSpecification',
1234
+ 'ExecutionSpecification',
1235
+ ]);
1236
+ /** §17.6.4's operators, as a set — what an `interactionOperator` is checked against. */
1237
+ const FRAGMENT_OPERATORS = new Set([
1238
+ 'alt',
1239
+ 'opt',
1240
+ 'loop',
1241
+ 'par',
1242
+ 'break',
1243
+ 'critical',
1244
+ 'seq',
1245
+ 'strict',
1246
+ 'neg',
1247
+ 'assert',
1248
+ 'ignore',
1249
+ 'consider',
1250
+ ]);
1251
+ /**
1252
+ * One `uml:Interaction`, read — §17's whole vocabulary in one pass.
1253
+ *
1254
+ * ## Why the ORDER of the fragment list is the whole of it
1255
+ *
1256
+ * §17.2.4 makes an Interaction's `fragment` an ORDERED collection and says the
1257
+ * order is time. A file therefore states the sequence and no heights, and a
1258
+ * canvas states heights and no sequence, so this reader turns one into the
1259
+ * other: every event in the list takes the next slot,
1260
+ * {@link UML_SD_EVENT_STEP} below the last, and `model.ts` reads the order back
1261
+ * off those heights. A slot per EVENT — not per message — is what keeps an
1262
+ * `activate` written between two arrows between them on the way back.
1263
+ *
1264
+ * ## What each metaclass becomes
1265
+ *
1266
+ * A pair of `MessageOccurrenceSpecification`s is ONE arrow and takes ONE slot:
1267
+ * they are the two ends of the same horizontal line (§17.4.4). An
1268
+ * `ExecutionOccurrenceSpecification` is the top or the bottom of a bar, and the
1269
+ * `BehaviorExecutionSpecification` between them carries no slot of its own — it
1270
+ * is the bar, and the bar is the space between its two occurrences. A
1271
+ * `DestructionOccurrenceSpecification` is the cross. A `CombinedFragment` is the
1272
+ * rectangle: it takes a slot for its top, one for each `else` after the first
1273
+ * operand, and one for its bottom.
1274
+ */
1275
+ function readInteraction(ctx, draft, node, id) {
1276
+ const interaction = {
1277
+ id,
1278
+ name: xmlAttr(node, 'name') ?? '',
1279
+ lifelines: [],
1280
+ messages: [],
1281
+ fragments: [],
1282
+ executions: [],
1283
+ destructions: [],
1284
+ };
1285
+ const pass = { occurrence: new Map(), slot: 0 };
1286
+ const take = () => umlSequenceSlot(pass.slot++);
1287
+ const columnOf = new Map();
1288
+ for (const child of xmlChildren(node, 'lifeline')) {
1289
+ consume(ctx, child);
1290
+ const lifelineId = map(ctx, child);
1291
+ carryUnknownAttrs(ctx, child, lifelineId, lifelineId, [
1292
+ 'xmi:type',
1293
+ 'type',
1294
+ 'xmi:id',
1295
+ 'id',
1296
+ 'name',
1297
+ 'represents',
1298
+ 'coveredBy',
1299
+ ]);
1300
+ const record = {
1301
+ ...readBase(ctx, child, lifelineId),
1302
+ ...lifelineTypeOf(ctx, child),
1303
+ };
1304
+ columnOf.set(lifelineId, interaction.lifelines.length);
1305
+ interaction.lifelines.push(record);
1306
+ own(draft, lifelineId);
1307
+ }
1308
+ /** The bars, with their two occurrences still unresolved. */
1309
+ const bars = [];
1310
+ /** Message id → the slot its FIRST occurrence took — see the docblock. */
1311
+ const seen = new Map();
1312
+ const readFragments = (parent) => {
1313
+ const touched = new Set();
1314
+ for (const child of xmlChildren(parent, 'fragment')) {
1315
+ const meta = metaOf(child) ?? '';
1316
+ consume(ctx, child);
1317
+ if (meta === 'MessageOccurrenceSpecification') {
1318
+ const fragmentId = map(ctx, child);
1319
+ carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
1320
+ 'xmi:type',
1321
+ 'type',
1322
+ 'xmi:id',
1323
+ 'id',
1324
+ 'name',
1325
+ 'covered',
1326
+ 'message',
1327
+ ]);
1328
+ const covered = xmlAttr(child, 'covered')?.split(/\s+/)[0];
1329
+ const message = xmlAttr(child, 'message');
1330
+ // The SECOND occurrence of a message is the other end of the same
1331
+ // horizontal line (§17.4.4), so it shares the slot rather than taking
1332
+ // one — otherwise every arrow would come back twice as far apart as the
1333
+ // file drew it.
1334
+ const y = message && seen.has(message) ? seen.get(message) : take();
1335
+ if (message)
1336
+ seen.set(message, y);
1337
+ pass.occurrence.set(fragmentId, { ...(covered ? { covered } : {}), y });
1338
+ if (covered)
1339
+ touched.add(covered);
1340
+ continue;
1341
+ }
1342
+ if (meta === 'ExecutionOccurrenceSpecification') {
1343
+ const fragmentId = map(ctx, child);
1344
+ carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
1345
+ 'xmi:type',
1346
+ 'type',
1347
+ 'xmi:id',
1348
+ 'id',
1349
+ 'name',
1350
+ 'covered',
1351
+ 'execution',
1352
+ ]);
1353
+ const covered = xmlAttr(child, 'covered')?.split(/\s+/)[0];
1354
+ pass.occurrence.set(fragmentId, {
1355
+ ...(covered ? { covered } : {}),
1356
+ y: take(),
1357
+ });
1358
+ if (covered)
1359
+ touched.add(covered);
1360
+ continue;
1361
+ }
1362
+ if (EXECUTION_TYPES.has(meta)) {
1363
+ const fragmentId = map(ctx, child);
1364
+ carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
1365
+ 'xmi:type',
1366
+ 'type',
1367
+ 'xmi:id',
1368
+ 'id',
1369
+ 'name',
1370
+ 'covered',
1371
+ 'start',
1372
+ 'finish',
1373
+ ]);
1374
+ const covered = xmlAttr(child, 'covered')?.split(/\s+/)[0];
1375
+ const record = {
1376
+ ...readBase(ctx, child, fragmentId),
1377
+ ...(covered ? { lifelineId: covered } : {}),
1378
+ y0: 0,
1379
+ y1: 0,
1380
+ };
1381
+ interaction.executions.push(record);
1382
+ own(draft, fragmentId);
1383
+ bars.push({
1384
+ record,
1385
+ ...(xmlAttr(child, 'start')
1386
+ ? { start: xmlAttr(child, 'start') }
1387
+ : {}),
1388
+ ...(xmlAttr(child, 'finish')
1389
+ ? { finish: xmlAttr(child, 'finish') }
1390
+ : {}),
1391
+ });
1392
+ if (covered)
1393
+ touched.add(covered);
1394
+ continue;
1395
+ }
1396
+ if (meta === 'DestructionOccurrenceSpecification') {
1397
+ const fragmentId = map(ctx, child);
1398
+ carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
1399
+ 'xmi:type',
1400
+ 'type',
1401
+ 'xmi:id',
1402
+ 'id',
1403
+ 'name',
1404
+ 'covered',
1405
+ ]);
1406
+ const covered = xmlAttr(child, 'covered')?.split(/\s+/)[0];
1407
+ const record = {
1408
+ ...readBase(ctx, child, fragmentId),
1409
+ ...(covered ? { lifelineId: covered } : {}),
1410
+ y: take(),
1411
+ };
1412
+ interaction.destructions.push(record);
1413
+ pass.occurrence.set(fragmentId, {
1414
+ ...(covered ? { covered } : {}),
1415
+ y: record.y,
1416
+ });
1417
+ own(draft, fragmentId);
1418
+ if (covered)
1419
+ touched.add(covered);
1420
+ continue;
1421
+ }
1422
+ if (meta === 'InteractionUse') {
1423
+ const fragmentId = map(ctx, child);
1424
+ carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
1425
+ 'xmi:type',
1426
+ 'type',
1427
+ 'xmi:id',
1428
+ 'id',
1429
+ 'name',
1430
+ 'covered',
1431
+ 'refersTo',
1432
+ ]);
1433
+ const covered = (xmlAttr(child, 'covered') ?? '')
1434
+ .split(/\s+/)
1435
+ .filter(Boolean);
1436
+ const y0 = take();
1437
+ const y1 = take();
1438
+ // §17.7.4's `refersTo` points at an Interaction that may be on another
1439
+ // sheet of this document; the NAME is what a board can draw, so the
1440
+ // reference is resolved to one when the file declares it and the
1441
+ // element's own name is kept otherwise.
1442
+ const refersTo = xmlAttr(child, 'refersTo');
1443
+ const named = xmlAttr(child, 'name') ??
1444
+ (refersTo ? nameOfId(ctx, refersTo) : undefined) ??
1445
+ '';
1446
+ interaction.fragments.push({
1447
+ ...readBase(ctx, child, fragmentId),
1448
+ name: named,
1449
+ operator: 'ref',
1450
+ operands: [{ y0, y1 }],
1451
+ coveredLifelineIds: covered,
1452
+ bounds: { x: 0, y: y0, w: 0, h: Math.max(1, y1 - y0) },
1453
+ });
1454
+ own(draft, fragmentId);
1455
+ for (const each of covered)
1456
+ touched.add(each);
1457
+ continue;
1458
+ }
1459
+ if (meta === 'CombinedFragment') {
1460
+ const fragmentId = map(ctx, child);
1461
+ carryUnknownAttrs(ctx, child, fragmentId, fragmentId, [
1462
+ 'xmi:type',
1463
+ 'type',
1464
+ 'xmi:id',
1465
+ 'id',
1466
+ 'name',
1467
+ 'covered',
1468
+ 'interactionOperator',
1469
+ ]);
1470
+ const stated = xmlAttr(child, 'interactionOperator') ?? 'seq';
1471
+ const operator = (FRAGMENT_OPERATORS.has(stated) ? stated : 'alt');
1472
+ if (!FRAGMENT_OPERATORS.has(stated)) {
1473
+ // CARRIED, and not merely mentioned (ADR 0012 D1): the word is the
1474
+ // one thing this fragment said that the drawing cannot, so it rides
1475
+ // along on the element's foreign payload. `interactionOperator` is in
1476
+ // the understood list above — a word the pentagon DOES draw is not
1477
+ // foreign matter — so the carry is written here, where it is.
1478
+ carryAttr(ctx, fragmentId, fragmentId, 'interactionOperator', stated);
1479
+ note(ctx, {
1480
+ kind: 'warning',
1481
+ element: child.name,
1482
+ sourceId: fragmentId,
1483
+ messageKey: UML_XMI_REMARKS.unlistedOperator[0],
1484
+ message: `This combined fragment's operator is "${stated}", which UML 2.5.1 §17.6.4 does not list. It is drawn as an "alt" and the file's own word is kept beside it.`,
1485
+ messageParams: { operator: stated },
1486
+ });
1487
+ }
1488
+ const declared = (xmlAttr(child, 'covered') ?? '')
1489
+ .split(/\s+/)
1490
+ .filter(Boolean);
1491
+ const y0 = take();
1492
+ const bands = [];
1493
+ const inside = new Set(declared);
1494
+ const operands = xmlChildren(child, 'operand');
1495
+ for (const [index, operand] of operands.entries()) {
1496
+ consume(ctx, operand);
1497
+ map(ctx, operand);
1498
+ const opened = index === 0 ? y0 : take();
1499
+ const guardNode = xmlChild(operand, 'guard');
1500
+ consume(ctx, guardNode);
1501
+ const specification = guardNode
1502
+ ? xmlChild(guardNode, 'specification')
1503
+ : undefined;
1504
+ consume(ctx, specification);
1505
+ const guard = ((specification
1506
+ ? (xmlAttr(specification, 'value') ??
1507
+ xmlAttr(specification, 'body') ??
1508
+ specification.text)
1509
+ : undefined) ??
1510
+ (guardNode
1511
+ ? (xmlAttr(guardNode, 'name') ?? guardNode.text)
1512
+ : undefined) ??
1513
+ '').trim();
1514
+ const held = readFragments(operand);
1515
+ for (const each of held.covered)
1516
+ inside.add(each);
1517
+ bands.push({ ...(guard ? { guard } : {}), y0: opened, y1: 0 });
1518
+ }
1519
+ if (bands.length === 0)
1520
+ bands.push({ y0, y1: 0 });
1521
+ const y1 = take();
1522
+ for (const [index, band] of bands.entries()) {
1523
+ band.y1 = index + 1 < bands.length ? bands[index + 1].y0 : y1;
1524
+ }
1525
+ interaction.fragments.push({
1526
+ ...readBase(ctx, child, fragmentId),
1527
+ name: bands[0]?.guard ?? '',
1528
+ operator,
1529
+ operands: bands,
1530
+ coveredLifelineIds: [...inside],
1531
+ bounds: { x: 0, y: y0, w: 0, h: Math.max(1, y1 - y0) },
1532
+ });
1533
+ own(draft, fragmentId);
1534
+ for (const each of inside)
1535
+ touched.add(each);
1536
+ continue;
1537
+ }
1538
+ // Anything else in the ordered list — a StateInvariant, a
1539
+ // ContinuationSpecification, a coregion — is out of scope (ADR 0022) and
1540
+ // is left for the sweep to quarantine verbatim.
1541
+ ctx.consumed.delete(child);
1542
+ }
1543
+ return { covered: touched };
1544
+ };
1545
+ readFragments(node);
1546
+ for (const bar of bars) {
1547
+ const start = bar.start ? pass.occurrence.get(bar.start) : undefined;
1548
+ const finish = bar.finish ? pass.occurrence.get(bar.finish) : undefined;
1549
+ bar.record.y0 = start?.y ?? 0;
1550
+ bar.record.y1 = finish?.y ?? bar.record.y0 + UML_SD_EVENT_STEP;
1551
+ if (!bar.record.lifelineId) {
1552
+ const covered = start?.covered ?? finish?.covered;
1553
+ if (covered)
1554
+ bar.record.lifelineId = covered;
1555
+ }
1556
+ }
1557
+ for (const child of xmlChildren(node, 'message')) {
1558
+ consume(ctx, child);
1559
+ const messageId = map(ctx, child);
1560
+ carryUnknownAttrs(ctx, child, messageId, messageId, [
1561
+ 'xmi:type',
1562
+ 'type',
1563
+ 'xmi:id',
1564
+ 'id',
1565
+ 'name',
1566
+ 'messageSort',
1567
+ 'sendEvent',
1568
+ 'receiveEvent',
1569
+ 'connector',
1570
+ ]);
1571
+ const send = xmlAttr(child, 'sendEvent');
1572
+ const receive = xmlAttr(child, 'receiveEvent');
1573
+ const from = send ? pass.occurrence.get(send) : undefined;
1574
+ const to = receive ? pass.occurrence.get(receive) : undefined;
1575
+ if (!from?.covered || !to?.covered) {
1576
+ note(ctx, {
1577
+ kind: 'warning',
1578
+ element: child.name,
1579
+ sourceId: messageId,
1580
+ messageKey: UML_XMI_REMARKS.undrawnOccurrence[0],
1581
+ message: `A message in "${interaction.name}" names an occurrence this sheet has no lifeline for, so it is not drawn. The file still says it, and nothing was removed from the document.`,
1582
+ messageParams: { name: interaction.name },
1583
+ });
1584
+ continue;
1585
+ }
1586
+ const sort = xmlAttr(child, 'messageSort') ?? 'synchCall';
1587
+ const label = xmlAttr(child, 'name');
1588
+ // §17.4.4 draws a deleteMessage ENDING on the cross, and that is what the
1589
+ // board draws too: the arrow's target is the destruction, not the spine.
1590
+ const cross = interaction.destructions.find(record => record.lifelineId === to.covered && record.y >= from.y);
1591
+ const kind = MESSAGE_KIND_OF_SORT[sort] ?? 'message-sync';
1592
+ interaction.messages.push({
1593
+ id: messageId,
1594
+ kind,
1595
+ sourceId: from.covered,
1596
+ targetId: kind === 'message-delete' && cross ? cross.id : to.covered,
1597
+ ...(label ? { label } : {}),
1598
+ y: from.y,
1599
+ });
1600
+ }
1601
+ // §17.4.4: top to bottom is time. The `<message>` list is not ordered by the
1602
+ // metamodel — the `fragment` list is — so the order comes off the occurrences.
1603
+ interaction.messages.sort((a, b) => a.y - b.y);
1604
+ /* ── The geometry the file does not carry ──────────────────────────── */
1605
+ const height = umlSequenceSlot(pass.slot) + UML_SD_EVENT_STEP;
1606
+ const columns = new Map();
1607
+ for (const lifeline of interaction.lifelines) {
1608
+ const box = umlSequenceColumn(columnOf.get(lifeline.id) ?? 0, height);
1609
+ lifeline.bounds = box;
1610
+ columns.set(lifeline.id, box);
1611
+ }
1612
+ for (const record of interaction.executions) {
1613
+ const column = record.lifelineId
1614
+ ? columns.get(record.lifelineId)
1615
+ : undefined;
1616
+ if (column) {
1617
+ record.bounds = umlSequenceExecution(column, record.y0, record.y1);
1618
+ }
1619
+ }
1620
+ for (const record of interaction.destructions) {
1621
+ const column = record.lifelineId
1622
+ ? columns.get(record.lifelineId)
1623
+ : undefined;
1624
+ if (column)
1625
+ record.bounds = umlSequenceDestruction(column, record.y);
1626
+ }
1627
+ for (const record of interaction.fragments) {
1628
+ const covered = record.coveredLifelineIds
1629
+ .map(each => columns.get(each))
1630
+ .filter((box) => box !== undefined);
1631
+ const y0 = record.bounds?.y ?? 0;
1632
+ const y1 = y0 + (record.bounds?.h ?? UML_SD_EVENT_STEP);
1633
+ record.bounds = umlSequenceFragment(covered.length > 0 ? covered : [...columns.values()], y0, y1);
1634
+ if (record.coveredLifelineIds.length === 0) {
1635
+ record.coveredLifelineIds = interaction.lifelines.map(each => each.id);
1636
+ }
1637
+ }
1638
+ return interaction;
1639
+ }
1640
+ /**
1641
+ * §17.3.4's `: <Type>` half of a lifeline head, wherever the file put it.
1642
+ *
1643
+ * `Lifeline::represents` is a ConnectableElement, so a tool that has one writes
1644
+ * an idref and this reader resolves it to a NAME — which is what a head can
1645
+ * draw. Our own writer has no Property to point at and files the type under
1646
+ * `xmi:Extension` instead (XMI 2.5.1 §7.9), so that is read too, and the head
1647
+ * comes back exactly as it went out.
1648
+ */
1649
+ function lifelineTypeOf(ctx, node) {
1650
+ for (const child of node.children) {
1651
+ if (!isLabreExtension(child))
1652
+ continue;
1653
+ const represents = xmlChild(child, 'represents');
1654
+ const named = represents ? xmlAttr(represents, 'name') : undefined;
1655
+ if (named) {
1656
+ consume(ctx, represents);
1657
+ if (child.children.every(entry => ctx.consumed.has(entry))) {
1658
+ consume(ctx, child);
1659
+ }
1660
+ return { type: named };
1661
+ }
1662
+ }
1663
+ // A tool that HAS a ConnectableElement to point at: §17.3.4's `: <Type>` is
1664
+ // then the type of that Property, not its name — `anOrder : Order` is a
1665
+ // property called `anOrder` typed by `Order`, and the head writes the second.
1666
+ // A Property with no type says nothing a head can draw, so nothing is
1667
+ // invented.
1668
+ const represents = xmlAttr(node, 'represents');
1669
+ const property = represents ? ctx.byId.get(represents) : undefined;
1670
+ const typed = property &&
1671
+ (xmlAttr(property, 'type') ??
1672
+ (xmlChild(property, 'type')
1673
+ ? refOf(xmlChild(property, 'type'))
1674
+ : undefined));
1675
+ const named = typed ? nameOfId(ctx, typed) : undefined;
1676
+ return named ? { type: named } : {};
1677
+ }
1678
+ /* ── One package ──────────────────────────────────────────────────────── */
1679
+ /**
1680
+ * Everything one package declares, read into one model.
1681
+ *
1682
+ * Recursive over nested packages, and FLAT in the model: `UmlModel.packages` is
1683
+ * a list of boxes whose nesting the drawing states (`xmi.ts`'s `containerOf`
1684
+ * reads it off geometry), and a file that carries no geometry has not said
1685
+ * where anything is drawn. The nesting is therefore a fact this import cannot
1686
+ * place on the canvas, and it says so once rather than silently drawing a flat
1687
+ * sheet that looks like the author's.
1688
+ */
1689
+ function readPackage(ctx, draft, node, containerId) {
1690
+ const contained = (childId) => {
1691
+ if (!containerId)
1692
+ return;
1693
+ draft.nested += 1;
1694
+ // Recorded as well as counted: the count is what the report says, and the
1695
+ // pair is what the invented layout needs to DRAW the nesting rather than
1696
+ // apologise for it (`import.ts`, `UmlLayoutHints.containment`).
1697
+ ctx.containment[childId] = containerId;
1698
+ };
1699
+ for (const child of node.children) {
1700
+ if (isLabreExtension(child))
1701
+ continue;
1702
+ const meta = metaOf(child) ?? '';
1703
+ if (child.local === 'ownedComment' || meta === 'Comment') {
1704
+ consume(ctx, child);
1705
+ const commentId = map(ctx, child);
1706
+ const bodyChild = xmlChild(child, 'body');
1707
+ consume(ctx, bodyChild);
1708
+ const body = xmlAttr(child, 'body') ?? bodyChild?.text ?? child.text ?? '';
1709
+ draft.model.notes.push({
1710
+ id: commentId,
1711
+ name: '',
1712
+ keywords: [],
1713
+ isAbstract: false,
1714
+ ...(ctx.layout[commentId] ? { bounds: ctx.layout[commentId] } : {}),
1715
+ body,
1716
+ });
1717
+ own(draft, commentId);
1718
+ for (const annotated of (xmlAttr(child, 'annotatedElement') ?? '')
1719
+ .split(/\s+/)
1720
+ .filter(Boolean)) {
1721
+ relate(draft, 'anchor', commentId, annotated);
1722
+ }
1723
+ continue;
1724
+ }
1725
+ if (child.local !== 'packagedElement' && child.local !== 'ownedUseCase') {
1726
+ continue;
1727
+ }
1728
+ // A use case owned by its subject (§18.1.4) — containment, not a reference,
1729
+ // so the case is read here and the subject is what it is drawn inside.
1730
+ if (child.local === 'ownedUseCase' || meta === 'UseCase') {
1731
+ consume(ctx, child);
1732
+ const useCaseId = map(ctx, child);
1733
+ carryUnknownAttrs(ctx, child, useCaseId);
1734
+ draft.model.useCases.push(readUseCase(ctx, draft, child, useCaseId));
1735
+ own(draft, useCaseId);
1736
+ contained(useCaseId);
1737
+ continue;
1738
+ }
1739
+ if (meta === 'Package') {
1740
+ consume(ctx, child);
1741
+ const packageId = map(ctx, child);
1742
+ carryUnknownAttrs(ctx, child, packageId);
1743
+ draft.model.packages.push(readBase(ctx, child, packageId));
1744
+ own(draft, packageId);
1745
+ contained(packageId);
1746
+ readPackage(ctx, draft, child, packageId);
1747
+ continue;
1748
+ }
1749
+ if (CLASSIFIER_KIND[meta]) {
1750
+ consume(ctx, child);
1751
+ const classifierId = map(ctx, child);
1752
+ carryUnknownAttrs(ctx, child, classifierId);
1753
+ readClassifier(ctx, draft, child, meta, classifierId);
1754
+ contained(classifierId);
1755
+ continue;
1756
+ }
1757
+ if (meta === 'Actor') {
1758
+ consume(ctx, child);
1759
+ const actorId = map(ctx, child);
1760
+ carryUnknownAttrs(ctx, child, actorId);
1761
+ draft.model.actors.push(readBase(ctx, child, actorId));
1762
+ own(draft, actorId);
1763
+ continue;
1764
+ }
1765
+ if (meta === 'Component') {
1766
+ consume(ctx, child);
1767
+ const componentId = map(ctx, child);
1768
+ carryUnknownAttrs(ctx, child, componentId);
1769
+ // The SUBJECT of a use case diagram is a Component with cases in it —
1770
+ // which is what §18.1.4 makes it and what `xmi.ts` writes. A component
1771
+ // with no `ownedUseCase` is a component (§11.6.4), and the drawing is the
1772
+ // only thing that differs.
1773
+ if (xmlChild(child, 'ownedUseCase') || draft.subjectLikely) {
1774
+ draft.model.subjects.push(readBase(ctx, child, componentId));
1775
+ own(draft, componentId);
1776
+ readPackage(ctx, draft, child, componentId);
1777
+ }
1778
+ else {
1779
+ draft.model.components.push(readComponent(ctx, draft, child, componentId));
1780
+ own(draft, componentId);
1781
+ }
1782
+ continue;
1783
+ }
1784
+ if (meta === 'Artifact') {
1785
+ consume(ctx, child);
1786
+ const artifactId = map(ctx, child);
1787
+ carryUnknownAttrs(ctx, child, artifactId);
1788
+ draft.model.artifacts.push(readBase(ctx, child, artifactId));
1789
+ own(draft, artifactId);
1790
+ for (const manifestation of xmlChildren(child, 'manifestation')) {
1791
+ consume(ctx, manifestation);
1792
+ relate(draft, 'manifest', artifactId, xmlAttr(manifestation, 'utilizedElement') ??
1793
+ xmlAttr(manifestation, 'supplier'), labelOf(manifestation));
1794
+ }
1795
+ continue;
1796
+ }
1797
+ if (DEPLOYMENT_KIND[meta]) {
1798
+ consume(ctx, child);
1799
+ const cubeId = map(ctx, child);
1800
+ carryUnknownAttrs(ctx, child, cubeId);
1801
+ draft.model.nodes.push({
1802
+ ...readBase(ctx, child, cubeId),
1803
+ kind: DEPLOYMENT_KIND[meta],
1804
+ });
1805
+ own(draft, cubeId);
1806
+ for (const deployment of xmlChildren(child, 'deployment')) {
1807
+ consume(ctx, deployment);
1808
+ relate(draft, 'deploy', xmlAttr(deployment, 'deployedArtifact') ??
1809
+ xmlAttr(deployment, 'supplier'), cubeId, labelOf(deployment));
1810
+ }
1811
+ continue;
1812
+ }
1813
+ if (meta === 'Association' || meta === 'CommunicationPath') {
1814
+ consume(ctx, child);
1815
+ const associationId = map(ctx, child);
1816
+ carryUnknownAttrs(ctx, child, associationId);
1817
+ readAssociation(ctx, draft, child, associationId, meta === 'CommunicationPath' ? 'communication-path' : 'association');
1818
+ continue;
1819
+ }
1820
+ if (DEPENDENCY_KIND[meta]) {
1821
+ consume(ctx, child);
1822
+ const dependencyId = map(ctx, child);
1823
+ carryUnknownAttrs(ctx, child, dependencyId);
1824
+ const clientChild = xmlChild(child, 'client');
1825
+ const supplierChild = xmlChild(child, 'supplier');
1826
+ consume(ctx, clientChild, supplierChild);
1827
+ const client = xmlAttr(child, 'client') ??
1828
+ (clientChild ? refOf(clientChild) : undefined);
1829
+ const supplier = xmlAttr(child, 'supplier') ??
1830
+ (supplierChild ? refOf(supplierChild) : undefined);
1831
+ relate(draft, DEPENDENCY_KIND[meta], client, supplier, labelOf(child));
1832
+ continue;
1833
+ }
1834
+ if (meta === 'Activity') {
1835
+ consume(ctx, child);
1836
+ const activityId = map(ctx, child);
1837
+ carryUnknownAttrs(ctx, child, activityId);
1838
+ draft.model.activities.push(readActivity(ctx, draft, child, activityId));
1839
+ continue;
1840
+ }
1841
+ if (meta === 'StateMachine') {
1842
+ consume(ctx, child);
1843
+ const machineId = map(ctx, child);
1844
+ carryUnknownAttrs(ctx, child, machineId);
1845
+ draft.model.stateMachines.push(readStateMachine(ctx, draft, child, machineId));
1846
+ continue;
1847
+ }
1848
+ if (meta === 'Interaction') {
1849
+ consume(ctx, child);
1850
+ const interactionId = map(ctx, child);
1851
+ carryUnknownAttrs(ctx, child, interactionId);
1852
+ draft.model.interactions.push(readInteraction(ctx, draft, child, interactionId));
1853
+ continue;
1854
+ }
1855
+ }
1856
+ }
1857
+ /**
1858
+ * The Signals and Events an activity glyph REFERENCED, folded into the sheet.
1859
+ *
1860
+ * `xmi.ts` mints a `uml:Signal` beside an Activity because a SendSignalAction's
1861
+ * `signal` is a reference and a reference needs something to point at. Reading
1862
+ * it back means recognising exactly those: a Signal a glyph names is the glyph's
1863
+ * own name written twice, and re-drawing it would put a box on the canvas the
1864
+ * author never drew. A Signal NOTHING names is a declaration the file makes on
1865
+ * its own account, and it is quarantined like anything else this reader cannot
1866
+ * draw.
1867
+ */
1868
+ function consumeReferencedEvents(ctx, draft, node) {
1869
+ for (const child of xmlChildren(node, 'packagedElement')) {
1870
+ const meta = metaOf(child) ?? '';
1871
+ if (meta !== 'Signal' && meta !== 'SignalEvent' && meta !== 'TimeEvent') {
1872
+ continue;
1873
+ }
1874
+ const id = xmlAttr(child, 'id');
1875
+ if (!id)
1876
+ continue;
1877
+ if (meta === 'Signal' && !draft.usedSignals.has(id))
1878
+ continue;
1879
+ if (meta !== 'Signal' && !referencedByATrigger(node, id))
1880
+ continue;
1881
+ // Whole: a TimeEvent owns its `<when>` TimeExpression, and the expression
1882
+ // is the glyph's own words read back through `parseTrigger` at export.
1883
+ consumeSubtree(ctx, child);
1884
+ // A signal referenced by an event that is itself referenced.
1885
+ const signal = xmlAttr(child, 'signal');
1886
+ if (signal)
1887
+ draft.usedSignals.add(signal);
1888
+ }
1889
+ // A second pass, because a SignalEvent names its Signal and the two are
1890
+ // written in either order.
1891
+ for (const child of xmlChildren(node, 'packagedElement')) {
1892
+ const id = xmlAttr(child, 'id');
1893
+ if ((metaOf(child) ?? '') !== 'Signal' || !id)
1894
+ continue;
1895
+ if (draft.usedSignals.has(id))
1896
+ consume(ctx, child);
1897
+ }
1898
+ }
1899
+ /** Does any `<trigger event="…">` under this package name that id? */
1900
+ function referencedByATrigger(node, id) {
1901
+ return xmlDescendants(node).some(descendant => descendant.local === 'trigger' && xmlAttr(descendant, 'event') === id);
1902
+ }
1903
+ /* ── The diagram kind ─────────────────────────────────────────────────── */
1904
+ /**
1905
+ * Which Annex A frame this package was drawn as.
1906
+ *
1907
+ * Our own extension when the file carries one — it is the one fact the UML
1908
+ * metamodel has no slot for, which is why `xmi.ts` files it under
1909
+ * `xmi:Extension` in the first place. Otherwise INFERRED from what the package
1910
+ * declares, in the order a diagram is recognised by eye: actors and use cases
1911
+ * make a use case sheet whatever else is on it, a flow makes an activity, a
1912
+ * machine makes a state machine, cubes and artefacts make a deployment,
1913
+ * components make a component diagram, and everything else is a class diagram
1914
+ * — which is also what `UmlDiagramElementModel` defaults to.
1915
+ */
1916
+ function declaredDiagramKind(ctx, node) {
1917
+ for (const child of node.children) {
1918
+ if (!isLabreExtension(child))
1919
+ continue;
1920
+ const diagram = xmlChild(child, 'diagram');
1921
+ const kind = diagram ? xmlAttr(diagram, 'kind') : undefined;
1922
+ if (!kind)
1923
+ continue;
1924
+ consume(ctx, diagram);
1925
+ if (child.children.every(entry => ctx.consumed.has(entry))) {
1926
+ consume(ctx, child);
1927
+ }
1928
+ return kind;
1929
+ }
1930
+ return undefined;
1931
+ }
1932
+ /** What the package DECLARES, when nothing said which frame it was drawn as. */
1933
+ function inferDiagramKind(model) {
1934
+ // An Interaction first: §17.2.4's sheet is the one that draws its
1935
+ // participants as lifelines, and a `uml:Actor` referenced by one would
1936
+ // otherwise make it look like a use case diagram.
1937
+ if (model.interactions.length > 0)
1938
+ return 'sd';
1939
+ if (model.useCases.length > 0 || model.actors.length > 0)
1940
+ return 'uc';
1941
+ if (model.activities.length > 0)
1942
+ return 'act';
1943
+ if (model.stateMachines.length > 0)
1944
+ return 'stm';
1945
+ if (model.nodes.length > 0 || model.artifacts.length > 0)
1946
+ return 'dep';
1947
+ if (model.components.length > 0)
1948
+ return 'cmp';
1949
+ return 'class';
1950
+ }
1951
+ /* ── The sweep ────────────────────────────────────────────────────────── */
1952
+ /**
1953
+ * Everything the reader did not read, kept verbatim under the nearest artefact.
1954
+ *
1955
+ * One pass, after the whole document has been read, because "nearest mapped
1956
+ * ancestor" is a question only the finished tree can answer. It descends into
1957
+ * what was consumed and stops at what was not: an `<eAnnotations>` is
1958
+ * quarantined whole, with its children inside it, rather than once per node —
1959
+ * the fragment is the file's own bytes and the file nested them.
1960
+ */
1961
+ function sweep(ctx, node, scope) {
1962
+ for (const child of node.children) {
1963
+ const nearer = ctx.scopeOf.get(child);
1964
+ if (nearer !== undefined) {
1965
+ sweep(ctx, child, nearer);
1966
+ continue;
1967
+ }
1968
+ if (ctx.consumed.has(child)) {
1969
+ sweep(ctx, child, scope);
1970
+ continue;
1971
+ }
1972
+ quarantine(ctx, scope, xmlFragmentOf(ctx.source, child), UML_XMI_QUARANTINE_REASON.unmapped, {
1973
+ element: child.name,
1974
+ ...(xmlAttr(child, 'id') ? { sourceId: xmlAttr(child, 'id') } : {}),
1975
+ });
1976
+ }
1977
+ }
1978
+ /* ── The document ─────────────────────────────────────────────────────── */
1979
+ /** The `uml:Model` / `uml:Package` a file opens on, through any wrapper. */
1980
+ function rootOf(document) {
1981
+ for (const root of document.roots) {
1982
+ if (root.local === 'Model' || root.local === 'Package')
1983
+ return root;
1984
+ // `<xmi:XMI>` is XMI's own multi-root wrapper, and Enterprise Architect
1985
+ // writes one round everything.
1986
+ if (root.local === 'XMI') {
1987
+ const inner = root.children.find(child => child.local === 'Model' || child.local === 'Package');
1988
+ if (inner)
1989
+ return inner;
1990
+ }
1991
+ }
1992
+ return undefined;
1993
+ }
1994
+ /**
1995
+ * Read an XMI 2.5.1 document as models plus a report.
1996
+ *
1997
+ * See the module comment for the contract, and `docs/adr/0012` D1–D6 for why it
1998
+ * is this contract and not a shorter one.
1999
+ */
2000
+ export function importXmi(source, context = {}) {
2001
+ const document = readXml(source);
2002
+ // The three bags keyed by an id the FILE chose have no prototype: `__proto__`
2003
+ // and `constructor` are ids a document is free to use, and on a plain object
2004
+ // the first writes through to `Object.prototype` and the second reads a
2005
+ // function back as a box. Nothing downstream asks these bags for an inherited
2006
+ // method — spread, `Object.keys` and `Object.entries` all still work.
2007
+ const ctx = {
2008
+ source,
2009
+ notes: [],
2010
+ foreign: Object.create(null),
2011
+ layout: Object.create(null),
2012
+ byId: new Map(),
2013
+ consumed: new Set(),
2014
+ scopeOf: new Map(),
2015
+ counts: { mapped: 0, carried: 0, quarantined: 0 },
2016
+ containment: Object.create(null),
2017
+ minted: 0,
2018
+ };
2019
+ for (const complaint of document.notes) {
2020
+ note(ctx, { kind: 'warning', message: complaint });
2021
+ }
2022
+ const root = rootOf(document);
2023
+ if (!root) {
2024
+ const opened = document.roots[0]?.name;
2025
+ note(ctx, {
2026
+ kind: 'warning',
2027
+ ...(opened ? { element: opened } : {}),
2028
+ // Two SHAPES of sentence, so two keys — never one key whose wording
2029
+ // depends on a branch the host cannot see.
2030
+ ...(opened
2031
+ ? {
2032
+ messageKey: UML_XMI_REMARKS.wrongRoot[0],
2033
+ message: `A UML interchange file opens on <uml:Model> or <uml:Package>; this one opens on <${opened}>. Nothing was imported.`,
2034
+ messageParams: { tag: opened },
2035
+ }
2036
+ : {
2037
+ messageKey: UML_XMI_REMARKS.noXml[0],
2038
+ message: UML_XMI_REMARKS.noXml[1],
2039
+ }),
2040
+ });
2041
+ return {
2042
+ models: [],
2043
+ layout: {},
2044
+ containment: {},
2045
+ foreign: {},
2046
+ report: { mapped: 0, carried: 0, quarantined: 0, notes: ctx.notes },
2047
+ };
2048
+ }
2049
+ for (const node of [root, ...xmlDescendants(root)]) {
2050
+ const id = xmlAttr(node, 'id');
2051
+ if (id && !ctx.byId.has(id))
2052
+ ctx.byId.set(id, node);
2053
+ }
2054
+ // The DI lives beside the model, not under it — a `.notation` island, a
2055
+ // `umldi` root — so it is read off the whole document rather than off `root`.
2056
+ for (const island of document.roots) {
2057
+ Object.assign(ctx.layout, readLayout(island));
2058
+ }
2059
+ /* ── The packages that are the sheets ──────────────────────────────── */
2060
+ const packages = root.local === 'Package'
2061
+ ? [root]
2062
+ : root.children.filter(child => child.local === 'packagedElement' &&
2063
+ (metaOf(child) ?? '') === 'Package');
2064
+ const models = [];
2065
+ const drafts = [];
2066
+ /** A `uml:Model` whose classifiers hang loose — Papyrus's own shape. */
2067
+ const sheets = packages.length > 0 ? packages : [root];
2068
+ for (const sheet of sheets) {
2069
+ const sheetId = map(ctx, sheet);
2070
+ carryUnknownAttrs(ctx, sheet, sheetId);
2071
+ const name = xmlAttr(sheet, 'name') ?? context.name ?? '';
2072
+ const declaredKind = declaredDiagramKind(ctx, sheet);
2073
+ const draft = {
2074
+ model: emptyModel(sheetId, name),
2075
+ ids: new Set(),
2076
+ ends: new Map(),
2077
+ usedSignals: new Set(),
2078
+ nested: 0,
2079
+ ...(declaredKind ? { declaredKind } : {}),
2080
+ subjectLikely: declaredKind === 'uc' ||
2081
+ xmlDescendants(sheet).some(each => each.local === 'ownedUseCase' || (metaOf(each) ?? '') === 'UseCase'),
2082
+ };
2083
+ readPackage(ctx, draft, sheet);
2084
+ consumeReferencedEvents(ctx, draft, sheet);
2085
+ drafts.push(draft);
2086
+ models.push(draft.model);
2087
+ }
2088
+ /* ── The root's own declarations ───────────────────────────────────── */
2089
+ // A `uml:DataType` at the root is what `xmi.ts` mints for a type name nothing
2090
+ // on the canvas answered for. It is READ — every `total : Money` in the file
2091
+ // resolves through it — and it is not drawn, because the author never drew
2092
+ // it. Consumed rather than quarantined for exactly that reason.
2093
+ if (root.local !== 'Package') {
2094
+ for (const child of root.children) {
2095
+ const meta = metaOf(child) ?? '';
2096
+ if (child.local !== 'packagedElement')
2097
+ continue;
2098
+ if (meta === 'DataType' || meta === 'PrimitiveType')
2099
+ consume(ctx, child);
2100
+ }
2101
+ }
2102
+ /* ── What each sheet can actually draw ─────────────────────────────── */
2103
+ for (const draft of drafts) {
2104
+ const { model } = draft;
2105
+ const kept = [];
2106
+ for (const relation of model.relations) {
2107
+ if (draft.ids.has(relation.sourceId) &&
2108
+ draft.ids.has(relation.targetId)) {
2109
+ kept.push(relation);
2110
+ continue;
2111
+ }
2112
+ note(ctx, {
2113
+ kind: 'warning',
2114
+ messageKey: UML_XMI_REMARKS.offSheetRelation[0],
2115
+ message: `A ${relation.kind} in "${model.diagram.name}" runs to an element that is not on this sheet, so it is not drawn. The file still says it, and nothing was removed from the document.`,
2116
+ messageParams: { kind: relation.kind, name: model.diagram.name },
2117
+ });
2118
+ }
2119
+ model.relations = kept;
2120
+ // A partition or a lane whose nodes are elsewhere is still a lane.
2121
+ for (const activity of model.activities) {
2122
+ activity.edges = activity.edges.filter(edge => draft.ids.has(edge.sourceId) && draft.ids.has(edge.targetId));
2123
+ }
2124
+ for (const machine of model.stateMachines) {
2125
+ machine.transitions = machine.transitions.filter(transition => draft.ids.has(transition.sourceId) &&
2126
+ draft.ids.has(transition.targetId));
2127
+ }
2128
+ }
2129
+ /* ── The frame each sheet is ───────────────────────────────────────── */
2130
+ for (const draft of drafts) {
2131
+ const kind = draft.declaredKind ?? inferDiagramKind(draft.model);
2132
+ draft.model.diagram.kind = kind;
2133
+ draft.model.diagram.heading = `${kind} ${draft.model.diagram.name}`.trim();
2134
+ const bounds = ctx.layout[draft.model.diagram.id];
2135
+ if (bounds)
2136
+ draft.model.diagram.bounds = bounds;
2137
+ }
2138
+ /* ── Nothing is lost (D1, D5) ──────────────────────────────────────── */
2139
+ // D6's residue, and its one accepted asymmetry: what has no artefact of its
2140
+ // own rides on the FIRST sheet's frame. Delete that frame and what it carried
2141
+ // goes with it — which is the whole argument of D2, made once more.
2142
+ const residue = drafts[0]?.model.diagram.id;
2143
+ if (residue) {
2144
+ for (const island of document.roots)
2145
+ sweep(ctx, island, residue);
2146
+ }
2147
+ if (Object.keys(ctx.layout).length === 0 && ctx.counts.mapped > 0) {
2148
+ note(ctx, {
2149
+ kind: 'invented-layout',
2150
+ messageKey: UML_XMI_REMARKS.modelWithoutDiagram[0],
2151
+ message: UML_XMI_REMARKS.modelWithoutDiagram[1],
2152
+ });
2153
+ // The one statement a file can make that this reader can read and cannot
2154
+ // draw. Said once per document, in the user's words, and said because a
2155
+ // silent flattening is the kind of loss they would only find months later:
2156
+ // on this canvas a package holds what its box holds (§12.2.4 is drawn, not
2157
+ // stored), so containment needs geometry, and a model file carries none.
2158
+ const nested = drafts.reduce((total, draft) => total + draft.nested, 0);
2159
+ if (nested > 0) {
2160
+ note(ctx, {
2161
+ kind: 'warning',
2162
+ // The one wording this lot changed: the count's own singular/plural
2163
+ // is the HOST's (ADR 0023), so the English fallback stays neutral —
2164
+ // `{{count}} element(s) are` — exactly as BPMN's `{{count}} lane(s)`
2165
+ // does, rather than picking English's agreement in the library.
2166
+ messageKey: UML_XMI_REMARKS.nestedWithoutDrawing[0],
2167
+ message: `${nested} element(s) are declared inside another in this file — a class in a package, a use case in its subject. Labre states that by DRAWING one box inside the other, and this file carries no drawing, so they were laid out side by side. Move them into the box to say it again.`,
2168
+ messageParams: { count: nested },
2169
+ });
2170
+ }
2171
+ }
2172
+ // The behaviour edges, counted once they are known to be DRAWN.
2173
+ //
2174
+ // An `<edge>` and a `<transition>` are children of the Activity and the
2175
+ // StateMachine rather than elements with an id of their own, so `map()` — the
2176
+ // one place `mapped` is incremented — never sees them. Counted here, after the
2177
+ // pass that drops the ones running off the sheet, so the report says how many
2178
+ // arrows the board has rather than how many the file wrote.
2179
+ for (const model of models) {
2180
+ for (const activity of model.activities) {
2181
+ ctx.counts.mapped += activity.edges.length;
2182
+ }
2183
+ for (const machine of model.stateMachines) {
2184
+ ctx.counts.mapped += machine.transitions.length;
2185
+ }
2186
+ }
2187
+ const version = xmlAttr(root, 'version');
2188
+ return {
2189
+ models,
2190
+ layout: ctx.layout,
2191
+ containment: ctx.containment,
2192
+ foreign: ctx.foreign,
2193
+ report: {
2194
+ mapped: ctx.counts.mapped,
2195
+ carried: ctx.counts.carried,
2196
+ quarantined: ctx.counts.quarantined,
2197
+ notes: ctx.notes,
2198
+ ...(version ? { sourceVersion: version } : {}),
2199
+ },
2200
+ };
2201
+ }
2202
+ /* ── Putting the foreign matter back on the elements ──────────────────── */
2203
+ /**
2204
+ * The materializer's elements, each carrying what the file said about it.
2205
+ *
2206
+ * ## Why this is a second pass and not an argument
2207
+ *
2208
+ * `umlElementsFromModel` already writes `interchange[formatId] = { id }` on
2209
+ * every element it mints — the source id, verbatim, which is D3's fixed point —
2210
+ * so the id map this needs is not a thing the parser has to be told: it is
2211
+ * written on the elements. Merging here keeps the materializer one function of
2212
+ * one IR, shared by three importers, none of which has to know what the others
2213
+ * carry.
2214
+ *
2215
+ * The payload is written as ONE whole blob per format, never patched field by
2216
+ * field: the `Y.Map` entry is the entire record, so a partial update is a
2217
+ * last-write-wins overwrite of everything (D2).
2218
+ *
2219
+ * ## The residue (D6)
2220
+ *
2221
+ * A payload whose source id matched no element — the document-scope matter, a
2222
+ * quarantined fragment off an element that was never drawn — rides on the FIRST
2223
+ * element, which for this materializer is the first sheet's frame. Its accepted
2224
+ * asymmetry is D6's own: delete that frame and what it carried goes with it.
2225
+ */
2226
+ export function umlElementsWithForeign(elements, foreign, formatId) {
2227
+ const placed = new Set();
2228
+ const merged = elements.map(props => {
2229
+ const carried = props.interchange;
2230
+ const sourceId = carried?.[formatId]?.id;
2231
+ const payload = sourceId ? foreign[sourceId] : undefined;
2232
+ if (!sourceId || !payload)
2233
+ return props;
2234
+ placed.add(sourceId);
2235
+ return {
2236
+ ...props,
2237
+ interchange: { ...carried, [formatId]: payload },
2238
+ };
2239
+ });
2240
+ const residue = Object.entries(foreign).filter(([sourceId]) => !placed.has(sourceId));
2241
+ const first = merged[0];
2242
+ if (residue.length === 0 || !first)
2243
+ return merged;
2244
+ const carried = first.interchange;
2245
+ const own = carried?.[formatId] ?? {};
2246
+ // Flattened onto the frame's own payload rather than nested under a second
2247
+ // key, because `ForeignInterchange` is one record per format and the SCOPES
2248
+ // inside it are what keep two source elements apart.
2249
+ const attrs = residue.reduce((all, [, payload]) => ({ ...all, ...payload.attrs }), { ...own.attrs });
2250
+ const quarantined = [
2251
+ ...(own.quarantined ?? []),
2252
+ ...residue.flatMap(([, payload]) => payload.quarantined ?? []),
2253
+ ];
2254
+ merged[0] = {
2255
+ ...first,
2256
+ interchange: {
2257
+ ...carried,
2258
+ [formatId]: {
2259
+ ...own,
2260
+ // Absent rather than empty: `{}` costs a `Y.Map` entry and makes "this
2261
+ // element carried nothing" untestable (D2).
2262
+ ...(Object.keys(attrs).length > 0 ? { attrs } : {}),
2263
+ ...(quarantined.length > 0 ? { quarantined } : {}),
2264
+ },
2265
+ },
2266
+ };
2267
+ return merged;
2268
+ }