@formicoidea/labre-framework-uml 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/import.js CHANGED
@@ -1,4 +1,4 @@
1
- import { connectorEndLabelBox, ConnectorMode, TextAlign, UML_FRAME_BAND_HEIGHT, UML_LIFELINE_HEAD, } from '@formicoidea/labre-core/model';
1
+ import { connectorEndLabelBox, ConnectorMode, TextAlign, UML_FRAME_BAND_HEIGHT, UML_LIFELINE_HEAD, } from '@formicoidea/labre-core/model/headless';
2
2
  import { Bound } from '@formicoidea/labre-core/global/gfx';
3
3
  import { umlCompartmentBoxes, umlLabelRoleOf, } from './component.js';
4
4
  import { UML_BODY_FONT_SIZE, UML_DIAGRAM_BOX, UML_EDGE_WIDTH, UML_FRAGMENT_BOX, UML_INK, UML_NAME_FONT_SIZE, UML_NODE_BOX, UML_PARTITION_BOX, UML_REGION_BOX, UML_SUBJECT_BOX, } from './consts.js';
@@ -184,8 +184,10 @@ export function umlSequenceFragment(columns, y0, y1) {
184
184
  return { x: 0, y: y0, w: UML_FRAGMENT_BOX.w, h: Math.max(1, y1 - y0) };
185
185
  }
186
186
  const centres = columns.map(column => column.x + column.w / 2);
187
- const left = Math.min(...centres) - UML_SD_FRAGMENT_PADDING;
188
- const right = Math.max(...centres) + UML_SD_FRAGMENT_PADDING;
187
+ const left = centres.reduce((a, b) => Math.min(a, b), Infinity) -
188
+ UML_SD_FRAGMENT_PADDING;
189
+ const right = centres.reduce((a, b) => Math.max(a, b), -Infinity) +
190
+ UML_SD_FRAGMENT_PADDING;
189
191
  return { x: left, y: y0, w: right - left, h: Math.max(1, y1 - y0) };
190
192
  }
191
193
  /**
@@ -237,8 +239,12 @@ export function umlInventLayout(nodes, relations = []) {
237
239
  const inner = children.map(measure);
238
240
  const columns = Math.ceil(Math.sqrt(children.length));
239
241
  const rows = Math.ceil(children.length / columns);
240
- const cellW = Math.max(...inner.map(box => box.w));
241
- const cellH = Math.max(...inner.map(box => box.h));
242
+ const cellW = inner
243
+ .map(box => box.w)
244
+ .reduce((a, b) => Math.max(a, b), -Infinity);
245
+ const cellH = inner
246
+ .map(box => box.h)
247
+ .reduce((a, b) => Math.max(a, b), -Infinity);
242
248
  const size = {
243
249
  w: Math.max(node.size.w, UML_IMPORT_CONTAINER_INSET * 2 +
244
250
  columns * cellW +
@@ -283,8 +289,12 @@ export function umlInventLayout(nodes, relations = []) {
283
289
  if (children.length === 0)
284
290
  return;
285
291
  const columns = Math.ceil(Math.sqrt(children.length));
286
- const cellW = Math.max(...children.map(child => (measured.get(child.id) ?? child.size).w));
287
- const cellH = Math.max(...children.map(child => (measured.get(child.id) ?? child.size).h));
292
+ const cellW = children
293
+ .map(child => (measured.get(child.id) ?? child.size).w)
294
+ .reduce((a, b) => Math.max(a, b), -Infinity);
295
+ const cellH = children
296
+ .map(child => (measured.get(child.id) ?? child.size).h)
297
+ .reduce((a, b) => Math.max(a, b), -Infinity);
288
298
  children.forEach((child, index) => {
289
299
  const column = index % columns;
290
300
  const row = Math.floor(index / columns);
@@ -899,10 +909,22 @@ export function umlElementsFromModel(models, options) {
899
909
  const drawn = drafts
900
910
  .map(draft => draft.bounds)
901
911
  .filter((box) => box !== undefined);
902
- const left = drawn.length > 0 ? Math.min(...drawn.map(box => box.x)) : 0;
903
- const top = drawn.length > 0 ? Math.min(...drawn.map(box => box.y)) : 0;
904
- const right = drawn.length > 0 ? Math.max(...drawn.map(box => box.x + box.w)) : 0;
905
- const bottom = drawn.length > 0 ? Math.max(...drawn.map(box => box.y + box.h)) : 0;
912
+ const left = drawn.length > 0
913
+ ? drawn.map(box => box.x).reduce((a, b) => Math.min(a, b), Infinity)
914
+ : 0;
915
+ const top = drawn.length > 0
916
+ ? drawn.map(box => box.y).reduce((a, b) => Math.min(a, b), Infinity)
917
+ : 0;
918
+ const right = drawn.length > 0
919
+ ? drawn
920
+ .map(box => box.x + box.w)
921
+ .reduce((a, b) => Math.max(a, b), -Infinity)
922
+ : 0;
923
+ const bottom = drawn.length > 0
924
+ ? drawn
925
+ .map(box => box.y + box.h)
926
+ .reduce((a, b) => Math.max(a, b), -Infinity)
927
+ : 0;
906
928
  const frame = {
907
929
  x: origin.x,
908
930
  y: frameTop,
@@ -1182,7 +1204,9 @@ function placeDrafts(drafts, model, layout) {
1182
1204
  .map(draft => draft.bounds)
1183
1205
  .filter((box) => box !== undefined);
1184
1206
  const below = placed.length > 0
1185
- ? Math.max(...placed.map(box => box.y + box.h)) + UML_IMPORT_GUTTER
1207
+ ? placed
1208
+ .map(box => box.y + box.h)
1209
+ .reduce((a, b) => Math.max(a, b), -Infinity) + UML_IMPORT_GUTTER
1186
1210
  : 0;
1187
1211
  // Containment, so the invented layout draws the nesting the file stated:
1188
1212
  // a class in its package, a case in its subject, an action in its lane, a
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@labre/affine-gfx-uml/interchange` — UML's whole interchange, with no editor
3
+ * behind it (ADR 0012, P3 as amended 2026-10-14).
4
+ *
5
+ * Every UML capability is pure on plain data — the exporters read structural
6
+ * records (`umlModelsFrom` picks by `type`, never by `instanceof`) — so this
7
+ * entry is the registry list itself, plus `decodeDrawio`, the container step a
8
+ * server runs before `uml:drawio:import` (capped, `DRAWIO_MAX_INFLATED_BYTES`).
9
+ *
10
+ * Held by `interchange-headless.unit.spec.ts` in `packages/affine/all`.
11
+ */
12
+ export * from './drawio-decode.js';
13
+ export * from './interchange.js';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `@labre/affine-gfx-uml/interchange` — UML's whole interchange, with no editor
3
+ * behind it (ADR 0012, P3 as amended 2026-10-14).
4
+ *
5
+ * Every UML capability is pure on plain data — the exporters read structural
6
+ * records (`umlModelsFrom` picks by `type`, never by `instanceof`) — so this
7
+ * entry is the registry list itself, plus `decodeDrawio`, the container step a
8
+ * server runs before `uml:drawio:import` (capped, `DRAWIO_MAX_INFLATED_BYTES`).
9
+ *
10
+ * Held by `interchange-headless.unit.spec.ts` in `packages/affine/all`.
11
+ */
12
+ export * from './drawio-decode.js';
13
+ export * from './interchange.js';
@@ -1,4 +1,4 @@
1
- import { interchangeCapabilityId } from '@formicoidea/labre-core/blocks/surface';
1
+ import { interchangeCapabilityId } from '@formicoidea/labre-core/interchange';
2
2
  import { importDrawio, UML_DRAWIO_FORMAT_ID } from './drawio-import.js';
3
3
  import { exportUmlPlantuml, exportUmlXmi } from './export.js';
4
4
  import { umlSafeFilename } from './filename.js';
package/dist/keywords.js CHANGED
@@ -297,7 +297,9 @@ export const UML_SLOTS_SEED_KEY = 'com.labre.uml.seed.slots';
297
297
  * tool that predates a unicode-safe pipeline writes, and a class pasted in from
298
298
  * one would otherwise export as a class named `<<interface>> Foo`.
299
299
  */
300
- const GUILLEMET_GROUP = /«([^»]*)»|<<([^>]*)>>/g;
300
+ // Bounded, as `STEREOTYPE` in `plantuml-import.ts` is and for its reason:
301
+ // `[^>]*` re-scanned the line from every `<<` that never closed.
302
+ const GUILLEMET_GROUP = /«([^»]{0,256})»|<<([^>]{0,256})>>/g;
301
303
  /** `{abstract}`, with whatever spacing and case the author used (§9.2.4). */
302
304
  const ABSTRACT_MARKER = /\{\s*abstract\s*\}/i;
303
305
  /**
package/dist/model.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type UmlDiagramKind, type UmlFragmentOperand, type UmlFragmentOperator, type UmlNodeKind } from '@formicoidea/labre-core/model';
1
+ import type { UmlDiagramKind, UmlFragmentOperand, UmlFragmentOperator, UmlNodeKind } from '@formicoidea/labre-core/model';
2
2
  import { type UmlBox, type UmlComponent, type UmlComponentElement, type UmlComponentGroup } from './component.js';
3
3
  import { type UmlAssociationEnd, type UmlOperation, type UmlProperty } from './grammar.js';
4
4
  /**
@@ -595,6 +595,13 @@ export interface UmlModel {
595
595
  */
596
596
  warnings: string[];
597
597
  }
598
+ /**
599
+ * The relationship a connector ROLE states — `uml:generalization` → `'generalization'`
600
+ * — or `undefined` for a role that states none. The one reading of
601
+ * {@link RELATION_ROLE} backwards, exported for the draw.io reader, which meets
602
+ * the same roles as `labre-role` attributes on a drawing the canvas wrote.
603
+ */
604
+ export declare function umlRelationKindOfRole(role: string | undefined): UmlRelationKind | undefined;
598
605
  /**
599
606
  * An element's box, from whichever of the three spellings it carries.
600
607
  *
package/dist/model.js CHANGED
@@ -1,4 +1,4 @@
1
- import { UML_DIAGRAM_KIND_TAG, UML_FRAGMENT_BAND, UML_FRAME_BAND_HEIGHT, UML_LIFELINE_HEAD, } from '@formicoidea/labre-core/model';
1
+ import { UML_DIAGRAM_KIND_TAG, UML_FRAGMENT_BAND, UML_FRAME_BAND_HEIGHT, UML_LIFELINE_HEAD, } from '@formicoidea/labre-core/model/headless';
2
2
  import { umlComponentSiblings, umlGroupOf, umlTierText, } from './component.js';
3
3
  import { parseActivityEdge, parseCompartment, parseEndLabel, parseLifelineIdent, parseOperation, parseProperty, parseStateBehavior, parseTransition, } from './grammar.js';
4
4
  import { stereotypesOf } from './keywords.js';
@@ -60,6 +60,15 @@ const RELATION_OF_ROLE = new Map(Object.entries(RELATION_ROLE).map(([kind, role]
60
60
  role,
61
61
  kind,
62
62
  ]));
63
+ /**
64
+ * The relationship a connector ROLE states — `uml:generalization` → `'generalization'`
65
+ * — or `undefined` for a role that states none. The one reading of
66
+ * {@link RELATION_ROLE} backwards, exported for the draw.io reader, which meets
67
+ * the same roles as `labre-role` attributes on a drawing the canvas wrote.
68
+ */
69
+ export function umlRelationKindOfRole(role) {
70
+ return role === undefined ? undefined : RELATION_OF_ROLE.get(role);
71
+ }
63
72
  /* ── Geometry ─────────────────────────────────────────────────────────── */
64
73
  /**
65
74
  * An element's box, from whichever of the three spellings it carries.
@@ -94,6 +94,16 @@ export interface UmlPlantumlImport {
94
94
  * They are contributed to the manifest by `./translations.ts`, with the
95
95
  * framework, because they ship in the UML bundle rather than in core.
96
96
  */
97
+ /**
98
+ * The most PlantUML source the reader takes: 1 MiB, the size of a diagram
99
+ * nobody types, and the bound on what the line regexes below can be handed
100
+ * (the server's budget: 256 MiB, two seconds).
101
+ */
102
+ export declare const PLANTUML_MAX_CHARACTERS: number;
103
+ /** The reader's refusal, static so a server can hand the key on (ADR 0023). */
104
+ export declare const UML_PLANTUML_ERRORS: {
105
+ readonly tooLarge: readonly ["com.labre.uml.import.plantuml.too-large", "This PlantUML file is longer than an import accepts, so it was not read."];
106
+ };
97
107
  export declare const UML_PLANTUML_REMARKS: {
98
108
  readonly relationEnd: readonly ["com.labre.uml.import.plantuml.relation-end", "The {{side}} end of the {{kind}} between \"{{left}}\" and \"{{right}}\" is written \"{{raw}}\". It is drawn on the board, but UML gives that relationship's ends no multiplicity, so no export writes it."];
99
109
  readonly duplicateDeclaration: readonly ["com.labre.uml.import.plantuml.duplicate-declaration", "This document declares \"{{name}}\" twice. The second declaration was read as a separate artefact under an id of Labre's own."];
@@ -1,4 +1,5 @@
1
- import { UML_DIAGRAM_KIND_TAG, } from '@formicoidea/labre-core/model';
1
+ import { InterchangeImportError } from '@formicoidea/labre-core/interchange';
2
+ import { UML_DIAGRAM_KIND_TAG } from '@formicoidea/labre-core/model/headless';
2
3
  import { UML_FRAGMENT_BOX, UML_NODE_BOX, UML_REGION_BOX, UML_SUBJECT_BOX, } from './consts.js';
3
4
  import { parseEndLabel, parseLifelineIdent, parseOperation, parseProperty, parseTransition, } from './grammar.js';
4
5
  import { UML_SD_COLUMN_GAP, UML_SD_EVENT_STEP, UML_SD_FRAGMENT_PADDING, umlInventLayout, umlSequenceColumn, umlSequenceDestruction, umlSequenceExecution, umlSequenceFragment, umlSequenceSlot, } from './import.js';
@@ -33,6 +34,19 @@ import { ADORNED_RELATION_KINDS } from './model.js';
33
34
  * They are contributed to the manifest by `./translations.ts`, with the
34
35
  * framework, because they ship in the UML bundle rather than in core.
35
36
  */
37
+ /**
38
+ * The most PlantUML source the reader takes: 1 MiB, the size of a diagram
39
+ * nobody types, and the bound on what the line regexes below can be handed
40
+ * (the server's budget: 256 MiB, two seconds).
41
+ */
42
+ export const PLANTUML_MAX_CHARACTERS = 1024 * 1024;
43
+ /** The reader's refusal, static so a server can hand the key on (ADR 0023). */
44
+ export const UML_PLANTUML_ERRORS = {
45
+ tooLarge: [
46
+ 'com.labre.uml.import.plantuml.too-large',
47
+ 'This PlantUML file is longer than an import accepts, so it was not read.',
48
+ ],
49
+ };
36
50
  export const UML_PLANTUML_REMARKS = {
37
51
  relationEnd: [
38
52
  'com.labre.uml.import.plantuml.relation-end',
@@ -82,7 +96,7 @@ const KIND_OF_TAG = new Map(Object.entries(UML_DIAGRAM_KIND_TAG).map(([kind, tag
82
96
  * boundary box — because that is the only thing this pack draws round a group of
83
97
  * use cases, and it is what our own writer emits for one.
84
98
  */
85
- const DECLARATION_KIND = {
99
+ const DECLARATION_KIND = Object.assign(Object.create(null), {
86
100
  class: 'class',
87
101
  entity: 'class',
88
102
  interface: 'interface',
@@ -100,7 +114,7 @@ const DECLARATION_KIND = {
100
114
  artifact: 'artifact',
101
115
  node: 'node',
102
116
  state: 'state',
103
- };
117
+ });
104
118
  /** The declaration words, as the alternation the line matcher is built on. */
105
119
  const DECLARATION_WORDS = [
106
120
  'abstract class',
@@ -117,7 +131,9 @@ const DECLARATION_WORDS = [
117
131
  * (a terminate was borrowing the stop). Reading them the other way would turn a
118
132
  * diagram somebody drew into a different one on the way home.
119
133
  */
120
- const STATE_KIND_OF_STEREOTYPE = {
134
+ // No prototype, like every table here read by a word the FILE wrote:
135
+ // `<<constructor>>` must read as a stereotype, not as `Object`.
136
+ const STATE_KIND_OF_STEREOTYPE = Object.assign(Object.create(null), {
121
137
  start: 'initial',
122
138
  end: 'final-state',
123
139
  choice: 'choice',
@@ -128,7 +144,7 @@ const STATE_KIND_OF_STEREOTYPE = {
128
144
  entrypoint: 'entry-point',
129
145
  exitpoint: 'exit-point',
130
146
  terminate: 'terminate',
131
- };
147
+ });
132
148
  /** The vertices §14.2.4.6 files under `Pseudostate`, as a set. */
133
149
  const PSEUDOSTATE_KINDS = new Set([
134
150
  'initial',
@@ -184,18 +200,25 @@ const OPERATORS = [
184
200
  * role, and reading it as prose would import an include as an untyped dependency
185
201
  * carrying a stereotype.
186
202
  */
187
- const KEYWORD_ROLE = {
203
+ const KEYWORD_ROLE = Object.assign(Object.create(null), {
188
204
  include: 'include',
189
205
  extend: 'extend',
190
206
  deploy: 'deploy',
191
207
  manifest: 'manifest',
192
208
  use: 'dependency',
193
- };
209
+ });
194
210
  /** PlantUML's contextual beginning-and-end marker (`plantuml.ts`). */
195
211
  const TERMINAL = '[*]';
196
212
  /* ── Reading a line ───────────────────────────────────────────────────── */
197
- /** A `<<stereo>>` / `«stereo»` group, wherever it was written. */
198
- const STEREOTYPE = /<<([^>]*)>>|«([^»]*)»/g;
213
+ /**
214
+ * A `<<stereo>>` / `«stereo»` group, wherever it was written.
215
+ *
216
+ * Bounded like draw.io's `ASCII_GUILLEMETS`: an unbounded `[^>]*` re-scanned
217
+ * the rest of the line from every `<<` that never closed, 1.8 s for a 40 KiB
218
+ * `class A <<<<…` and four times that per doubling (counter-review of
219
+ * 49aa247ce). No stereotype is 256 characters long.
220
+ */
221
+ const STEREOTYPE = /<<([^>]{0,256})>>|«([^»]{0,256})»/g;
199
222
  /** Direction hints inside an arrow — `-down->`, `-up->`: layout, not model. */
200
223
  const DIRECTION_HINT = /-(?:left|right|up|down|le|ri|do)-/gi;
201
224
  /** A colour or a style tail — `#red`, `#line:blue;line.dashed`. */
@@ -1020,6 +1043,11 @@ function layOut(model, contains) {
1020
1043
  * the same source always produces the same models, down to the coordinates.
1021
1044
  */
1022
1045
  export function importPlantuml(source) {
1046
+ if (source.length > PLANTUML_MAX_CHARACTERS) {
1047
+ throw new InterchangeImportError(UML_PLANTUML_ERRORS.tooLarge[1], {
1048
+ messageKey: UML_PLANTUML_ERRORS.tooLarge[0],
1049
+ });
1050
+ }
1023
1051
  const notes = [];
1024
1052
  const note = (entry) => notes.push(entry);
1025
1053
  const lines = source.replaceAll('\r\n', '\n').split('\n');
@@ -1125,7 +1153,7 @@ function sequenceArrow(token) {
1125
1153
  };
1126
1154
  }
1127
1155
  /** §17.6.4's operators, read back off the block keyword that opened them. */
1128
- const FRAGMENT_OPERATOR_OF_WORD = {
1156
+ const FRAGMENT_OPERATOR_OF_WORD = Object.assign(Object.create(null), {
1129
1157
  alt: 'alt',
1130
1158
  opt: 'opt',
1131
1159
  loop: 'loop',
@@ -1138,7 +1166,7 @@ const FRAGMENT_OPERATOR_OF_WORD = {
1138
1166
  assert: 'assert',
1139
1167
  ignore: 'ignore',
1140
1168
  consider: 'consider',
1141
- };
1169
+ });
1142
1170
  /**
1143
1171
  * The declarations that can only be a CLASS diagram — §9's classifiers and
1144
1172
  * §12's package.
package/dist/presets.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { TextAlign, TextDecoration, type UmlNodeKind } from '@formicoidea/labre-core/model';
1
+ import { TextAlign, TextDecoration } from '@formicoidea/labre-core/model/headless';
2
+ import type { UmlNodeKind } from '@formicoidea/labre-core/model';
2
3
  import type { UmlBox } from './component.js';
3
4
  /**
4
5
  * What a UML shape IS, as props — the ONE description the creation site, the
package/dist/presets.js CHANGED
@@ -1,4 +1,4 @@
1
- import { FontFamily, FontStyle, FontWeight, ShapeStyle, StrokeStyle, TextAlign, TextDecoration, } from '@formicoidea/labre-core/model';
1
+ import { FontFamily, FontStyle, FontWeight, ShapeStyle, StrokeStyle, TextAlign, TextDecoration, } from '@formicoidea/labre-core/model/headless';
2
2
  import { Bound } from '@formicoidea/labre-core/global/gfx';
3
3
  import { UML_CARD, UML_INK, UML_NAME_FONT_SIZE, UML_NODE_BOX, UML_NODE_RADIUS, UML_NODE_STROKE_WIDTH, } from './consts.js';
4
4
  import { UML_ROLE_OF_KIND } from './roles.js';
@@ -1,18 +1,19 @@
1
1
  import { collectTranslationKeys, commandCategoryTranslationEntries, commandTranslationEntries, mergeTranslationEntries, } from '@formicoidea/labre-core/std';
2
2
  import { UML_DIAGRAM_FRAME, UML_SUBJECT_FRAME } from './background.js';
3
3
  import { umlCommands } from './commands.js';
4
+ import { UML_DRAWIO_DECODE_ERRORS } from './drawio-decode.js';
4
5
  import { UML_DRAWIO_REMARKS } from './drawio-import.js';
5
6
  import { UML_LAYOUT_REMARKS } from './import.js';
6
7
  import { UML_ATTRIBUTES_SEED, UML_ATTRIBUTES_SEED_KEY, UML_NAME_SEED, UML_OPERATIONS_SEED, UML_OPERATIONS_SEED_KEY, UML_SLOTS_SEED, UML_SLOTS_SEED_KEY, umlSeedKey, } from './keywords.js';
7
8
  import { UML_DIAGRAM_KIND_MENU } from './kinds.js';
8
9
  import { UML_LEGEND_SECTION_WORDINGS } from './legend.js';
9
10
  import { UML_FRAGMENT_OPERATOR_MENU } from './operators.js';
10
- import { UML_PLANTUML_REMARKS } from './plantuml-import.js';
11
+ import { UML_PLANTUML_ERRORS, UML_PLANTUML_REMARKS, } from './plantuml-import.js';
11
12
  import { UML_PROFILES } from './profiles.js';
12
13
  import { UML_READINGS } from './reading.js';
13
14
  import { UML_ROLES } from './roles.js';
14
15
  import { UML_RULES } from './rules.js';
15
- import { UML_XMI_REMARKS } from './xmi-import.js';
16
+ import { UML_XMI_ERRORS, UML_XMI_REMARKS } from './xmi-import.js';
16
17
  /**
17
18
  * The words a placed artefact is SEEDED with — its name compartment, and the
18
19
  * three compartment lines a fresh classifier carries.
@@ -57,6 +58,9 @@ const chromeEntries = () => [
57
58
  ...Object.values(UML_PLANTUML_REMARKS),
58
59
  ...Object.values(UML_XMI_REMARKS),
59
60
  ...Object.values(UML_DRAWIO_REMARKS),
61
+ ...Object.values(UML_DRAWIO_DECODE_ERRORS),
62
+ ...Object.values(UML_PLANTUML_ERRORS),
63
+ ...Object.values(UML_XMI_ERRORS),
60
64
  ...Object.values(UML_LAYOUT_REMARKS),
61
65
  ].map(([key, fallback]) => ({ key, fallback, source: 'chrome' }));
62
66
  /**
@@ -131,6 +131,18 @@ export declare const UML_XMI_QUARANTINE_REASON: {
131
131
  * cannot become a `[key, english]` pair without breaking that contract. BPMN
132
132
  * carries the identical constraint on its own quarantine reasons.
133
133
  */
134
+ /**
135
+ * The deepest an XMI file may nest: 1 000 levels. The reader walks packages,
136
+ * consumed subtrees and the quarantine sweep by recursion, and 5 000 nested
137
+ * packages overflowed the stack with a bare `RangeError` (counter-review of
138
+ * 49aa247ce). A real model nests a few dozen levels; one cap at the door bounds
139
+ * every recursion below it.
140
+ */
141
+ export declare const XMI_MAX_DEPTH = 1000;
142
+ /** The reader's refusal, static so a server can hand the key on (ADR 0023). */
143
+ export declare const UML_XMI_ERRORS: {
144
+ readonly tooDeep: readonly ["com.labre.uml.import.xmi.too-deep", "This XMI file nests its elements deeper than an import accepts, so it was not read."];
145
+ };
134
146
  export declare const UML_XMI_REMARKS: {
135
147
  readonly mintedId: readonly ["com.labre.uml.import.xmi.minted-id", "<{{tag}}> carries no xmi:id, so one was minted for it. Nothing in the file can refer to it, and nothing does."];
136
148
  readonly unlistedOperator: readonly ["com.labre.uml.import.xmi.unlisted-operator", "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."];
@@ -1,3 +1,4 @@
1
+ import { InterchangeImportError } from '@formicoidea/labre-core/interchange';
1
2
  import { UML_SD_EVENT_STEP, umlSequenceColumn, umlSequenceDestruction, umlSequenceExecution, umlSequenceFragment, umlSequenceSlot, } from './import.js';
2
3
  import { formatEndLabel, } from './grammar.js';
3
4
  import { readXml, xmlAttr, xmlChild, xmlChildren, xmlDescendants, xmlFragmentOf, } from './xml-reader.js';
@@ -14,7 +15,8 @@ import { readXml, xmlAttr, xmlChild, xmlChildren, xmlDescendants, xmlFragmentOf,
14
15
  */
15
16
  const META_PREFIXES = new Set(['uml', 'UML', 'Uml']);
16
17
  /** The classifier metaclasses, and the {@link UmlClassifier.kind} each is. */
17
- const CLASSIFIER_KIND = {
18
+ // Tables read by a name the FILE wrote have no prototype (`<constructor>`).
19
+ const CLASSIFIER_KIND = Object.assign(Object.create(null), {
18
20
  Class: 'class',
19
21
  Interface: 'interface',
20
22
  Enumeration: 'enumeration',
@@ -25,20 +27,20 @@ const CLASSIFIER_KIND = {
25
27
  // file said.
26
28
  DataType: 'class',
27
29
  PrimitiveType: 'class',
28
- };
30
+ });
29
31
  /** The metaclasses {@link CLASSIFIER_KIND} maps that need their keyword said. */
30
- const CLASSIFIER_KEYWORD = {
32
+ const CLASSIFIER_KEYWORD = Object.assign(Object.create(null), {
31
33
  DataType: 'datatype',
32
34
  PrimitiveType: 'primitive',
33
- };
35
+ });
34
36
  /** The three cubes of §19.4.2, and the one `kind` they differ by. */
35
- const DEPLOYMENT_KIND = {
37
+ const DEPLOYMENT_KIND = Object.assign(Object.create(null), {
36
38
  Node: 'node',
37
39
  Device: 'device',
38
40
  ExecutionEnvironment: 'execution-environment',
39
- };
41
+ });
40
42
  /** The inverse of `xmi.ts`'s `ACTIVITY_NODE_TYPE`. */
41
- const ACTIVITY_NODE_KIND = {
43
+ const ACTIVITY_NODE_KIND = Object.assign(Object.create(null), {
42
44
  OpaqueAction: 'action',
43
45
  Action: 'action',
44
46
  CallBehaviorAction: 'action',
@@ -58,9 +60,9 @@ const ACTIVITY_NODE_KIND = {
58
60
  // whose trigger names a TimeEvent, so the two are told apart by the EVENT and
59
61
  // not by the metaclass. See {@link readActivity}.
60
62
  AcceptEventAction: 'accept-event',
61
- };
63
+ });
62
64
  /** The inverse of `xmi.ts`'s `PSEUDOSTATE_KIND` (§14.5.7's enumeration). */
63
- const PSEUDOSTATE_OF_KIND = {
65
+ const PSEUDOSTATE_OF_KIND = Object.assign(Object.create(null), {
64
66
  initial: 'initial',
65
67
  choice: 'choice',
66
68
  junction: 'junction',
@@ -71,16 +73,16 @@ const PSEUDOSTATE_OF_KIND = {
71
73
  terminate: 'terminate',
72
74
  fork: 'fork',
73
75
  join: 'fork',
74
- };
76
+ });
75
77
  /** The dependency metaclasses, and the relation each states. */
76
- const DEPENDENCY_KIND = {
78
+ const DEPENDENCY_KIND = Object.assign(Object.create(null), {
77
79
  Dependency: 'dependency',
78
80
  Usage: 'dependency',
79
81
  Abstraction: 'dependency',
80
82
  Realization: 'realization',
81
83
  Deployment: 'deploy',
82
84
  Manifestation: 'manifest',
83
- };
85
+ });
84
86
  /** What Labre stamps on the XMI extensions it writes — `xmi.ts`'s `EXTENDER`. */
85
87
  const LABRE_EXTENDER = 'labre';
86
88
  /** The four VisibilityKinds, as §7.4 spells them in a file. */
@@ -109,6 +111,33 @@ export const UML_XMI_QUARANTINE_REASON = {
109
111
  * cannot become a `[key, english]` pair without breaking that contract. BPMN
110
112
  * carries the identical constraint on its own quarantine reasons.
111
113
  */
114
+ /**
115
+ * The deepest an XMI file may nest: 1 000 levels. The reader walks packages,
116
+ * consumed subtrees and the quarantine sweep by recursion, and 5 000 nested
117
+ * packages overflowed the stack with a bare `RangeError` (counter-review of
118
+ * 49aa247ce). A real model nests a few dozen levels; one cap at the door bounds
119
+ * every recursion below it.
120
+ */
121
+ export const XMI_MAX_DEPTH = 1_000;
122
+ /** The reader's refusal, static so a server can hand the key on (ADR 0023). */
123
+ export const UML_XMI_ERRORS = {
124
+ tooDeep: [
125
+ 'com.labre.uml.import.xmi.too-deep',
126
+ 'This XMI file nests its elements deeper than an import accepts, so it was not read.',
127
+ ],
128
+ };
129
+ /** The depth of the deepest element, walked with a stack. */
130
+ function depthOf(roots) {
131
+ let deepest = 0;
132
+ const stack = roots.map(node => ({ node, depth: 1 }));
133
+ while (stack.length > 0) {
134
+ const { node, depth } = stack.pop();
135
+ deepest = Math.max(deepest, depth);
136
+ for (const child of node.children)
137
+ stack.push({ node: child, depth: depth + 1 });
138
+ }
139
+ return deepest;
140
+ }
112
141
  export const UML_XMI_REMARKS = {
113
142
  mintedId: [
114
143
  'com.labre.uml.import.xmi.minted-id',
@@ -233,16 +262,17 @@ function foreignOf(ctx, scope) {
233
262
  /** One attribute, kept under the scope of the element that carried it. */
234
263
  function carryAttr(ctx, owner, scope, name, value) {
235
264
  const payload = foreignOf(ctx, owner);
236
- payload.attrs = {
237
- ...payload.attrs,
238
- [scope]: { ...payload.attrs?.[scope], [name]: value },
239
- };
265
+ // In place, in bags with no prototype: a copy per attribute was quadratic,
266
+ // and the keys are the file's (BPMN's keep-helpers, same review).
267
+ const attrs = (payload.attrs ??= Object.create(null));
268
+ (attrs[scope] ??= Object.create(null))[name] =
269
+ value;
240
270
  ctx.counts.carried += 1;
241
271
  }
242
272
  /** One fragment, kept and declared un-re-emittable (D5). */
243
273
  function quarantine(ctx, owner, fragment, reason, entry) {
244
274
  const payload = foreignOf(ctx, owner);
245
- payload.quarantined = [...(payload.quarantined ?? []), { fragment, reason }];
275
+ (payload.quarantined ??= []).push({ fragment, reason });
246
276
  ctx.counts.quarantined += 1;
247
277
  note(ctx, { kind: 'quarantined', ...entry, message: reason });
248
278
  }
@@ -293,7 +323,9 @@ function map(ctx, node) {
293
323
  * model everywhere in this format, and anything else on an element Labre draws
294
324
  * is something Labre does not model and must not lose.
295
325
  */
296
- const READ_ATTRS = {
326
+ // No prototype: read by a local name the file chose (`<constructor>`), the
327
+ // BPMN reader's TypeError (security review, 2026-10-14).
328
+ const READ_ATTRS = Object.assign(Object.create(null), {
297
329
  packagedElement: [
298
330
  'xmi:type',
299
331
  'type',
@@ -339,7 +371,7 @@ const READ_ATTRS = {
339
371
  'isQuery',
340
372
  ],
341
373
  '': ['xmi:type', 'type', 'xmi:id', 'id', 'name'],
342
- };
374
+ });
343
375
  /** Everything on this element the reader did not read, kept verbatim. */
344
376
  function carryUnknownAttrs(ctx, node, owner, scope = owner, understood = READ_ATTRS[node.local] ?? READ_ATTRS['']) {
345
377
  for (const [name, value] of Object.entries(node.attrs)) {
@@ -1219,14 +1251,14 @@ function readStateMachine(ctx, draft, node, id) {
1219
1251
  }
1220
1252
  /* ── Interactions (§17) ───────────────────────────────────────────────── */
1221
1253
  /** §17.4.3's `messageSort`, read back to the arrow §17.4.4 draws for it. */
1222
- const MESSAGE_KIND_OF_SORT = {
1254
+ const MESSAGE_KIND_OF_SORT = Object.assign(Object.create(null), {
1223
1255
  synchCall: 'message-sync',
1224
1256
  asynchCall: 'message-async',
1225
1257
  asynchSignal: 'message-async',
1226
1258
  reply: 'message-reply',
1227
1259
  createMessage: 'message-create',
1228
1260
  deleteMessage: 'message-delete',
1229
- };
1261
+ });
1230
1262
  /** The metaclasses §17.2.4 draws as the bar on a spine, whatever the tool. */
1231
1263
  const EXECUTION_TYPES = new Set([
1232
1264
  'BehaviorExecutionSpecification',
@@ -1999,6 +2031,11 @@ function rootOf(document) {
1999
2031
  */
2000
2032
  export function importXmi(source, context = {}) {
2001
2033
  const document = readXml(source);
2034
+ if (depthOf(document.roots) > XMI_MAX_DEPTH) {
2035
+ throw new InterchangeImportError(UML_XMI_ERRORS.tooDeep[1], {
2036
+ messageKey: UML_XMI_ERRORS.tooDeep[0],
2037
+ });
2038
+ }
2002
2039
  // The three bags keyed by an id the FILE chose have no prototype: `__proto__`
2003
2040
  // and `constructor` are ids a document is free to use, and on a plain object
2004
2041
  // the first writes through to `Object.prototype` and the second reads a
@@ -2246,7 +2283,9 @@ export function umlElementsWithForeign(elements, foreign, formatId) {
2246
2283
  // Flattened onto the frame's own payload rather than nested under a second
2247
2284
  // key, because `ForeignInterchange` is one record per format and the SCOPES
2248
2285
  // inside it are what keep two source elements apart.
2249
- const attrs = residue.reduce((all, [, payload]) => ({ ...all, ...payload.attrs }), { ...own.attrs });
2286
+ const attrs = residue.reduce((all, [, payload]) => Object.assign(all, payload.attrs),
2287
+ // No prototype: a scope is an id the file chose, `__proto__` included.
2288
+ Object.assign(Object.create(null), own.attrs));
2250
2289
  const quarantined = [
2251
2290
  ...(own.quarantined ?? []),
2252
2291
  ...residue.flatMap(([, payload]) => payload.quarantined ?? []),