dsh-model-everything 0.0.0-stage → 0.2.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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.en-US.md +332 -0
  3. package/README.md +332 -3
  4. package/cordis.patch.yml +20 -0
  5. package/examples/README.md +48 -0
  6. package/examples/control-dispatcher.json +113 -0
  7. package/examples/control-dispatcher.overbudget.json +113 -0
  8. package/examples/ecommerce-data-model.json +185 -0
  9. package/examples/order-state-machine.after.json +116 -0
  10. package/examples/order-state-machine.before.json +101 -0
  11. package/examples/user-migration.after.json +29 -0
  12. package/examples/user-migration.before.json +29 -0
  13. package/gate/locale/en.json +6 -0
  14. package/gate/locale/zh.json +6 -0
  15. package/gate/package.json +5 -0
  16. package/lib/baseline.js +249 -0
  17. package/lib/compose-tool.js +80 -0
  18. package/lib/concurrency-tool.js +40 -0
  19. package/lib/concurrency.js +206 -0
  20. package/lib/data-engine.js +1141 -0
  21. package/lib/data-tool.js +87 -0
  22. package/lib/design-engine.js +1149 -0
  23. package/lib/design-tool.js +63 -0
  24. package/lib/engine.js +4343 -0
  25. package/lib/export-tool.js +78 -0
  26. package/lib/exporters.js +434 -0
  27. package/lib/gate.js +274 -0
  28. package/lib/granularity.js +237 -0
  29. package/lib/index.js +237 -0
  30. package/lib/json-value.js +18 -0
  31. package/lib/model-file.js +48 -0
  32. package/lib/option-shape.js +62 -0
  33. package/lib/report-diff-tool.js +63 -0
  34. package/lib/report-view.js +433 -0
  35. package/lib/structure-tool.js +91 -0
  36. package/lib/structure.js +711 -0
  37. package/lib/tool.js +94 -0
  38. package/lib/types/baseline.d.ts +65 -0
  39. package/lib/types/compose-tool.d.ts +7 -0
  40. package/lib/types/concurrency-tool.d.ts +8 -0
  41. package/lib/types/concurrency.d.ts +37 -0
  42. package/lib/types/data-engine.d.ts +211 -0
  43. package/lib/types/data-tool.d.ts +10 -0
  44. package/lib/types/design-engine.d.ts +215 -0
  45. package/lib/types/design-tool.d.ts +11 -0
  46. package/lib/types/engine.d.ts +494 -0
  47. package/lib/types/export-tool.d.ts +7 -0
  48. package/lib/types/exporters.d.ts +37 -0
  49. package/lib/types/gate.d.ts +91 -0
  50. package/lib/types/granularity.d.ts +106 -0
  51. package/lib/types/index.d.ts +45 -0
  52. package/lib/types/json-value.d.ts +21 -0
  53. package/lib/types/model-file.d.ts +7 -0
  54. package/lib/types/option-shape.d.ts +22 -0
  55. package/lib/types/report-diff-tool.d.ts +8 -0
  56. package/lib/types/report-view.d.ts +50 -0
  57. package/lib/types/structure-tool.d.ts +10 -0
  58. package/lib/types/structure.d.ts +152 -0
  59. package/lib/types/tool.d.ts +11 -0
  60. package/lib/types/uml-communication.d.ts +109 -0
  61. package/lib/types/uml-composite.d.ts +107 -0
  62. package/lib/types/uml-interaction-overview.d.ts +91 -0
  63. package/lib/types/uml-profile.d.ts +112 -0
  64. package/lib/types/uml-structure.d.ts +53 -0
  65. package/lib/types/uml-syntax.d.ts +296 -0
  66. package/lib/types/uml-timing.d.ts +152 -0
  67. package/lib/types/uml-tool.d.ts +12 -0
  68. package/lib/types/uml-usecase.d.ts +58 -0
  69. package/lib/types/uml.d.ts +303 -0
  70. package/lib/uml-communication.js +353 -0
  71. package/lib/uml-composite.js +442 -0
  72. package/lib/uml-interaction-overview.js +277 -0
  73. package/lib/uml-profile.js +373 -0
  74. package/lib/uml-structure.js +306 -0
  75. package/lib/uml-syntax.js +545 -0
  76. package/lib/uml-timing.js +974 -0
  77. package/lib/uml-tool.js +213 -0
  78. package/lib/uml-usecase.js +532 -0
  79. package/lib/uml.js +2110 -0
  80. package/locale/en.json +6 -0
  81. package/locale/zh.json +6 -0
  82. package/package.json +128 -6
  83. package/skills/model-everything/SKILL.md +450 -0
  84. package/skills/model-everything/references/dsh-model-schema.md +437 -0
  85. package/skills/model-everything/references/gap-routing-guide.md +42 -0
  86. package/skills/model-everything/references/hash-spec.md +152 -0
  87. package/skills/model-everything/references/logic-verification-guide.md +422 -0
  88. package/skills/model-everything-datamodel/SKILL.md +130 -0
  89. package/skills/model-everything-datamodel/references/data-model-guide.md +84 -0
  90. package/skills/model-everything-datamodel/references/data-model-schema.md +128 -0
  91. package/skills/model-everything-design/SKILL.md +115 -0
  92. package/skills/model-everything-interleaving/SKILL.md +63 -0
  93. package/skills/model-everything-interleaving/SKILL.zh-CN.md +60 -0
  94. package/skills/model-everything-interleaving/references/concurrency-risk-guide.md +63 -0
  95. package/skills/model-everything-report-reading/SKILL.md +137 -0
  96. package/skills/model-everything-report-reading/references/reading-a-report.md +166 -0
  97. package/skills/model-everything-report-reading/references/reading-a-report.mmd +59 -0
  98. package/skills/model-everything-structure/SKILL.md +96 -0
  99. package/skills/model-everything-structure/references/structure-review-guide.md +201 -0
  100. package/skills/model-everything-uml/SKILL.md +60 -0
  101. package/skills/model-everything-uml/references/uml-modeling-guide.md +551 -0
  102. package/src/baseline.ts +288 -0
  103. package/src/compose-tool.ts +82 -0
  104. package/src/concurrency-tool.ts +44 -0
  105. package/src/concurrency.ts +254 -0
  106. package/src/data-engine.ts +1208 -0
  107. package/src/data-tool.ts +91 -0
  108. package/src/design-engine.ts +1263 -0
  109. package/src/design-tool.ts +68 -0
  110. package/src/engine.ts +4564 -0
  111. package/src/export-tool.ts +81 -0
  112. package/src/exporters.ts +399 -0
  113. package/src/gate.ts +330 -0
  114. package/src/granularity.ts +312 -0
  115. package/src/index.ts +289 -0
  116. package/src/json-value.ts +20 -0
  117. package/src/model-file.ts +49 -0
  118. package/src/option-shape.ts +64 -0
  119. package/src/report-diff-tool.ts +67 -0
  120. package/src/report-view.ts +483 -0
  121. package/src/structure-tool.ts +93 -0
  122. package/src/structure.ts +786 -0
  123. package/src/tool.ts +103 -0
  124. package/src/uml-communication.ts +428 -0
  125. package/src/uml-composite.ts +520 -0
  126. package/src/uml-interaction-overview.ts +321 -0
  127. package/src/uml-profile.ts +439 -0
  128. package/src/uml-structure.ts +317 -0
  129. package/src/uml-syntax.ts +764 -0
  130. package/src/uml-timing.ts +1106 -0
  131. package/src/uml-tool.ts +230 -0
  132. package/src/uml-usecase.ts +566 -0
  133. package/src/uml.ts +2139 -0
@@ -0,0 +1,442 @@
1
+ /**
2
+ * The composite structure diagram module.
3
+ *
4
+ * A composite structure diagram is a structured classifier seen from the inside: the parts
5
+ * it is made of, the ports on its boundary, and the connectors between them. That reads
6
+ * back out of the text, which is what separates this family from the profile.
7
+ *
8
+ * PlantUML has no composite-structure page, so the view is an approximation built from the
9
+ * deployment diagram's vocabulary: a nested element for the classifier, the nestable
10
+ * elements for its parts, `port`/`portin`/`portout` for the boundary, and the arrows for
11
+ * connectors. Nesting is spelled, port direction is spelled, and both connector ends are
12
+ * spelled - so the model survives the round trip. What the notation cannot say (a part's
13
+ * type, a port's required interface, a connector's name) stays out of the model rather
14
+ * than being invented on the way back.
15
+ *
16
+ * @module model-everything-uml-composite
17
+ */
18
+ import { UmlError, UML_PLAIN_ID, directiveLine, } from './uml-syntax.js';
19
+ /** Elements PlantUML can nest; a part is declared with one of these. */
20
+ const NESTABLE = [
21
+ 'component', 'node', 'artifact', 'database', 'file', 'folder', 'frame', 'cloud',
22
+ 'package', 'queue', 'rectangle', 'stack', 'storage', 'card', 'hexagon', 'action',
23
+ 'process', 'collections',
24
+ ];
25
+ const PORT_KEYWORDS = { port: 'inout', portin: 'in', portout: 'out' };
26
+ /** The port keyword a direction is drawn with. */
27
+ function portKeyword(direction) {
28
+ if (direction === 'in')
29
+ return 'portin';
30
+ if (direction === 'out')
31
+ return 'portout';
32
+ return 'port';
33
+ }
34
+ function quote(text) {
35
+ return '"' + text.replace(/"/g, "'") + '"';
36
+ }
37
+ function isPlainId(id) {
38
+ return UML_PLAIN_ID.test(id) && !NESTABLE.includes(id.toLowerCase()) && id !== 'port' && id !== 'portin' && id !== 'portout';
39
+ }
40
+ /**
41
+ * Validate a candidate model and normalize it.
42
+ *
43
+ * A port id and a part id may not collide: a connector endpoint is a bare name, so two
44
+ * declarations sharing one would make the drawing say two things at once.
45
+ */
46
+ export function validateCompositeModel(input) {
47
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
48
+ throw new UmlError('a composite structure model must be an object with schemaVersion 1, a self, parts, ports and connectors', 'UML_INPUT');
49
+ }
50
+ const model = input;
51
+ if (model.schemaVersion !== 1)
52
+ throw new UmlError('a composite structure model must declare schemaVersion 1', 'UML_INPUT');
53
+ const self = model.self;
54
+ if (self === null || typeof self !== 'object' || typeof self.id !== 'string' || self.id.trim() === '') {
55
+ throw new UmlError('a composite structure model needs a `self` with a non-empty id', 'UML_INPUT');
56
+ }
57
+ const kind = self.kind ?? 'component';
58
+ if (!NESTABLE.includes(kind)) {
59
+ throw new UmlError('self.kind must be one of ' + NESTABLE.join(' | ') + ', got ' + quote(String(kind)), 'UML_INPUT');
60
+ }
61
+ for (const [name, value] of Object.entries({ parts: model.parts ?? [], ports: model.ports ?? [], connectors: model.connectors ?? [] })) {
62
+ if (!Array.isArray(value))
63
+ throw new UmlError(name + ' must be an array', 'UML_INPUT');
64
+ }
65
+ const declared = new Set([self.id]);
66
+ const parts = [];
67
+ for (const part of model.parts ?? []) {
68
+ if (part === null || typeof part !== 'object' || typeof part.id !== 'string' || part.id.trim() === '') {
69
+ throw new UmlError('every part needs a non-empty string id', 'UML_INPUT');
70
+ }
71
+ if (declared.has(part.id))
72
+ throw new UmlError(quote(part.id) + ' is declared more than once as a part or a port', 'UML_INPUT');
73
+ declared.add(part.id);
74
+ parts.push({ id: part.id, ...(part.label === undefined ? {} : { label: part.label }) });
75
+ }
76
+ const ports = [];
77
+ for (const port of model.ports ?? []) {
78
+ if (port === null || typeof port !== 'object' || typeof port.id !== 'string' || port.id.trim() === '') {
79
+ throw new UmlError('every port needs a non-empty string id', 'UML_INPUT');
80
+ }
81
+ if (declared.has(port.id))
82
+ throw new UmlError(quote(port.id) + ' is declared more than once as a part or a port', 'UML_INPUT');
83
+ declared.add(port.id);
84
+ const direction = port.direction ?? 'inout';
85
+ if (direction !== 'in' && direction !== 'out' && direction !== 'inout') {
86
+ throw new UmlError('port direction must be in | out | inout, got ' + quote(String(direction)), 'UML_INPUT');
87
+ }
88
+ ports.push({ id: port.id, direction });
89
+ }
90
+ const undeclared = new Set();
91
+ const connectors = [];
92
+ for (const connector of model.connectors ?? []) {
93
+ if (connector === null || typeof connector !== 'object')
94
+ throw new UmlError('every connector must be an object', 'UML_INPUT');
95
+ for (const side of ['from', 'to']) {
96
+ const endpoint = connector[side];
97
+ if (typeof endpoint !== 'string' || endpoint.trim() === '') {
98
+ throw new UmlError('every connector needs a non-empty `' + side + '` end', 'UML_INPUT');
99
+ }
100
+ if (!declared.has(endpoint))
101
+ undeclared.add(endpoint);
102
+ }
103
+ if (connector.from === connector.to) {
104
+ throw new UmlError('a connector cannot join ' + quote(connector.from) + ' to itself', 'UML_INPUT');
105
+ }
106
+ connectors.push({
107
+ from: connector.from,
108
+ to: connector.to,
109
+ ...(connector.label === undefined ? {} : { label: connector.label }),
110
+ });
111
+ }
112
+ return {
113
+ schemaVersion: 1,
114
+ self: { id: self.id, kind, ...(self.label === undefined ? {} : { label: self.label }) },
115
+ parts,
116
+ ports,
117
+ connectors,
118
+ ...(undeclared.size === 0 ? {} : { undeclared: [...undeclared].sort() }),
119
+ ...(model.narrative === undefined ? {} : { narrative: model.narrative }),
120
+ };
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // Rendering
124
+ // ---------------------------------------------------------------------------
125
+ /**
126
+ * Render a composite structure diagram as PlantUML.
127
+ *
128
+ * The classifier is drawn as a nested element, its parts inside it, its ports on its
129
+ * boundary, and the connectors after the block. The `self` directive carries the
130
+ * classifier's own id, because a nested element's closing brace is the only thing that
131
+ * says where its inside ends.
132
+ *
133
+ * @param input - candidate `CompositeModelV1`.
134
+ * @param notation - only `plantuml` draws this family; mermaid has no ports or connectors.
135
+ */
136
+ export function renderComposite(input, notation) {
137
+ if (notation !== 'plantuml') {
138
+ throw new UmlError('a composite structure diagram can only be drawn in plantuml; mermaid has no port or connector element and no nested element to put parts in. Render it with notation "plantuml".', 'UML_VIEW_UNREPRESENTABLE');
139
+ }
140
+ const model = validateCompositeModel(input);
141
+ const warnings = [
142
+ 'UML_RENDER_APPROXIMATE_VIEW: plantuml has no composite structure diagram; this is a deployment-diagram drawing of one. Nesting, part names, port direction and both connector ends are carried; a part\'s type, a port\'s interfaces and a connector\'s name are not expressible and are not drawn.',
143
+ ];
144
+ const lines = ['@startuml'];
145
+ lines.push(directiveLine('plantuml', 'uml v1 notation=plantuml family=composite-structure'));
146
+ lines.push(directiveLine('plantuml', 'compositeSelf ' + model.self.kind + ' ' + model.self.id));
147
+ const label = model.self.label === undefined ? quote(model.self.id) : quote(model.self.label);
148
+ lines.push(model.self.kind + ' ' + label + ' as ' + model.self.id + ' {');
149
+ for (const port of model.ports) {
150
+ // A port declaration takes no alias, so its id is what the reader sees; a port whose
151
+ // id PlantUML cannot use is reported rather than silently renamed.
152
+ lines.push(' ' + portKeyword(port.direction) + ' ' + port.id);
153
+ }
154
+ for (const part of model.parts) {
155
+ if (part.label === undefined)
156
+ lines.push(' component ' + part.id);
157
+ else
158
+ lines.push(' component ' + quote(part.label) + ' as ' + part.id);
159
+ }
160
+ lines.push('}');
161
+ for (const connector of model.connectors) {
162
+ lines.push(connector.from + ' --> ' + connector.to + (connector.label === undefined ? '' : ' : ' + connector.label));
163
+ }
164
+ lines.push('@enduml');
165
+ return { primary: lines.join('\n') + '\n', warnings };
166
+ }
167
+ const SELF_DIRECTIVE = /^compositeSelf\s+([A-Za-z_][A-Za-z0-9_]*)\s+(\S+)\s*$/;
168
+ const DECLARATION = new RegExp('^(' + NESTABLE.join('|') + ')\\s+(?:"([^"]*)"|(\\S+))(?:\\s+as\\s+(\\S+))?\\s*(\\{)?\\s*$', 'i');
169
+ const PORT_DECLARATION = /^(port|portin|portout)\s+(\S+)\s*$/i;
170
+ const CONNECTOR = /^(\S+)\s+(-->|->|--|\.\.>|\.\.)\s+(\S+?)(?:\s*:\s*(.*))?$/;
171
+ /**
172
+ * Read a composite structure diagram back into a model.
173
+ *
174
+ * Faithful to this module's renderer and tolerant of hand-written text: the classifier is
175
+ * the outermost block (named by the `compositeSelf` directive when the renderer wrote it),
176
+ * `port`/`portin`/`portout` lines are its ports, nestable declarations inside the block are
177
+ * its parts, and every arrow between two names is a connector. A construct the notation
178
+ * carries but this model does not - a nested block inside a part, a stereotype, a note - is
179
+ * reported in `discardedConstructs`, never dropped in silence.
180
+ */
181
+ export function parseComposite(text, notation) {
182
+ if (notation !== 'plantuml') {
183
+ throw new UmlError('a composite structure diagram can only be read from plantuml', 'UML_VIEW_UNREPRESENTABLE');
184
+ }
185
+ const all = text.split(/\r?\n/).map((raw, index) => ({ line: index + 1, text: raw.trim() }));
186
+ const warnings = [];
187
+ const discarded = [];
188
+ const labels = {};
189
+ let self;
190
+ let fromDirective;
191
+ const parts = [];
192
+ const ports = [];
193
+ const connectors = [];
194
+ let depth = 0;
195
+ let seenConnector = false;
196
+ for (const entry of all) {
197
+ const line = entry.text;
198
+ if (line === '' || line.startsWith('@') || line.startsWith("'")) {
199
+ // The renderer's own directives are read elsewhere; a comment is not a construct.
200
+ if (line.startsWith("'dsh-model-everything:")) {
201
+ const directive = line.slice("'dsh-model-everything:".length).trim();
202
+ const selfMatch = SELF_DIRECTIVE.exec(directive);
203
+ if (selfMatch !== null)
204
+ fromDirective = { kind: selfMatch[1].toLowerCase(), id: selfMatch[2] };
205
+ }
206
+ continue;
207
+ }
208
+ if (/^(left to right direction|top to bottom direction|skinparam|hide|show|title|scale|allowmixing|allow_mixing)\b/i.test(line)) {
209
+ discarded.push({ line: entry.line, text: line, construct: line.split(/\s+/)[0].toLowerCase() });
210
+ continue;
211
+ }
212
+ const port = PORT_DECLARATION.exec(line);
213
+ if (port !== null) {
214
+ if (depth === 0) {
215
+ discarded.push({ line: entry.line, text: line, construct: 'port outside a classifier' });
216
+ continue;
217
+ }
218
+ const id = port[2];
219
+ if (ports.some((entry) => entry.id === id) || parts.some((entry) => entry.id === id)) {
220
+ warnings.push('line ' + String(entry.line) + ': ' + id + ' is declared twice; the second declaration is ignored');
221
+ continue;
222
+ }
223
+ ports.push({ id, direction: PORT_KEYWORDS[port[1].toLowerCase()] });
224
+ continue;
225
+ }
226
+ const declaration = DECLARATION.exec(line);
227
+ if (declaration !== null) {
228
+ const kind = declaration[1].toLowerCase();
229
+ const quoted = declaration[2];
230
+ const bare = declaration[3];
231
+ const alias = declaration[4];
232
+ const opens = declaration[5] === '{';
233
+ const id = alias ?? bare ?? quoted ?? '';
234
+ // `component "App" as app` names the id in the alias and the label in the quotes.
235
+ // `component App as app` puts the label in the bare position instead, and that is a
236
+ // label rather than an id: the alias is what the rest of the diagram refers to.
237
+ const labelText = quoted ?? (alias === undefined ? undefined : bare);
238
+ if (labelText !== undefined)
239
+ labels[id] = labelText;
240
+ if (depth === 0) {
241
+ if (self !== undefined) {
242
+ discarded.push({ line: entry.line, text: line, construct: 'second top-level block' });
243
+ }
244
+ else {
245
+ self = { id, kind, ...(labelText === undefined ? {} : { label: labelText }) };
246
+ }
247
+ }
248
+ else if (depth === 1) {
249
+ if (opens) {
250
+ // A block inside the classifier would be a part with its own inside. The model
251
+ // has no nesting, so the part is kept and the block it opens is reported.
252
+ discarded.push({ line: entry.line, text: line, construct: 'nested block inside a part' });
253
+ }
254
+ if (!parts.some((entry) => entry.id === id) && !ports.some((entry) => entry.id === id)) {
255
+ parts.push({ id, ...(labelText === undefined ? {} : { label: labelText }) });
256
+ }
257
+ else {
258
+ warnings.push('line ' + String(entry.line) + ': ' + id + ' is declared twice; the second declaration is ignored');
259
+ }
260
+ }
261
+ else {
262
+ // Whatever is declared inside a part is that part's inside, and this model has no
263
+ // place for it. Reporting it is the only alternative to inventing a structure.
264
+ discarded.push({ line: entry.line, text: line, construct: 'declaration inside a part' });
265
+ }
266
+ if (opens)
267
+ depth += 1;
268
+ continue;
269
+ }
270
+ if (line === '}') {
271
+ depth = Math.max(0, depth - 1);
272
+ seenConnector = false;
273
+ continue;
274
+ }
275
+ const connector = CONNECTOR.exec(line);
276
+ if (connector !== null) {
277
+ const [, from, , to, label] = connector;
278
+ connectors.push({ from, to, ...(label === undefined || label === '' ? {} : { label }) });
279
+ seenConnector = true;
280
+ continue;
281
+ }
282
+ if (seenConnector || depth > 0) {
283
+ discarded.push({ line: entry.line, text: line, construct: 'line this model does not carry' });
284
+ }
285
+ else {
286
+ discarded.push({ line: entry.line, text: line, construct: 'statement outside any classifier' });
287
+ }
288
+ }
289
+ if (self === undefined) {
290
+ if (fromDirective === undefined) {
291
+ throw new UmlError('no structured classifier found: a composite structure diagram needs one block that holds its parts, such as `component Order as order { ... }`', 'UML_INPUT');
292
+ }
293
+ self = { id: fromDirective.id, kind: fromDirective.kind };
294
+ }
295
+ else if (fromDirective !== undefined) {
296
+ // The directive is the renderer's own record; it wins, because the block's alias can
297
+ // be a label PlantUML was given.
298
+ self = { ...self, id: fromDirective.id, kind: fromDirective.kind };
299
+ }
300
+ const declared = new Set([self.id, ...parts.map((part) => part.id), ...ports.map((port) => port.id)]);
301
+ const undeclared = [...new Set(connectors.flatMap((connector) => [connector.from, connector.to]))].filter((endpoint) => !declared.has(endpoint)).sort();
302
+ // No narrative is invented here. The labels a reader gave the drawing come back in
303
+ // `labels`, which is where a display name belongs; writing them into the model as well
304
+ // would add a field the model never carried, and the round trip would stop being exact.
305
+ const model = {
306
+ schemaVersion: 1,
307
+ self,
308
+ parts,
309
+ ports,
310
+ connectors,
311
+ ...(undeclared.length === 0 ? {} : { undeclared }),
312
+ };
313
+ return { model, labels, warnings, discardedConstructs: discarded, discardedEdges: 0 };
314
+ }
315
+ export const COMPOSITE_FINDING_CODES = [
316
+ 'UML057_COMPOSITE_NO_PARTS',
317
+ 'UML058_COMPOSITE_PORT_UNCONNECTED',
318
+ 'UML059_COMPOSITE_CONNECTOR_ENDPOINT_MISSING',
319
+ 'UML060_COMPOSITE_DUPLICATE_PART',
320
+ 'UML061_COMPOSITE_PORT_COLLISION',
321
+ 'UML062_COMPOSITE_UNDOCUMENTED',
322
+ ];
323
+ /**
324
+ * What a reader would get wrong about a composite structure.
325
+ *
326
+ * The questions are the ones the diagram exists to answer: is anything inside, does every
327
+ * port lead somewhere, does every connector join two things that exist, and is each part
328
+ * and port named after something in the system rather than after its drawing position.
329
+ * Whether the decomposition is *right* is a question about the design, and this does not
330
+ * answer it.
331
+ */
332
+ export function reviewCompositeModel(model, labels) {
333
+ const findings = [];
334
+ const name = (id) => labels[id] ?? id;
335
+ if (model.parts.length === 0) {
336
+ findings.push({
337
+ code: 'UML057_COMPOSITE_NO_PARTS',
338
+ severity: 'error',
339
+ message: 'the classifier is drawn as a composite structure but holds no part: a structure diagram with an empty inside says nothing about how it is built.',
340
+ evidence: { parts: 0 },
341
+ });
342
+ }
343
+ const connected = new Set(model.connectors.flatMap((connector) => [connector.from, connector.to]));
344
+ for (const port of model.ports) {
345
+ if (connected.has(port.id))
346
+ continue;
347
+ findings.push({
348
+ code: 'UML058_COMPOSITE_PORT_UNCONNECTED',
349
+ severity: 'warning',
350
+ message: 'port ' + name(port.id) + ' (' + port.direction + ') has no connector, so nothing reaches the classifier through it.',
351
+ evidence: { id: port.id, direction: port.direction },
352
+ });
353
+ }
354
+ const undeclared = model.undeclared ?? [];
355
+ if (undeclared.length > 0) {
356
+ findings.push({
357
+ code: 'UML059_COMPOSITE_CONNECTOR_ENDPOINT_MISSING',
358
+ severity: 'error',
359
+ message: 'a connector ends at ' + String(undeclared.length) + ' name(s) the classifier never declares, so the line points at nothing a reader can look up.',
360
+ detail: undeclared.join(', '),
361
+ evidence: { ids: undeclared },
362
+ });
363
+ }
364
+ const seen = new Set();
365
+ for (const part of model.parts) {
366
+ if (seen.has(part.id)) {
367
+ findings.push({
368
+ code: 'UML060_COMPOSITE_DUPLICATE_PART',
369
+ severity: 'error',
370
+ message: 'part ' + part.id + ' is declared more than once, so a connector that names it is ambiguous.',
371
+ evidence: { id: part.id },
372
+ });
373
+ }
374
+ seen.add(part.id);
375
+ }
376
+ if (seen.has(model.self.id)) {
377
+ findings.push({
378
+ code: 'UML061_COMPOSITE_PORT_COLLISION',
379
+ severity: 'error',
380
+ message: 'the classifier itself is also declared as a part (' + model.self.id + '): a nested element inside itself has no reading.',
381
+ evidence: { id: model.self.id },
382
+ });
383
+ }
384
+ for (const port of model.ports) {
385
+ if (seen.has(port.id)) {
386
+ findings.push({
387
+ code: 'UML061_COMPOSITE_PORT_COLLISION',
388
+ severity: 'error',
389
+ message: 'port ' + port.id + ' shares its name with a part: a connector that names it cannot be read.',
390
+ evidence: { id: port.id },
391
+ });
392
+ }
393
+ }
394
+ if (model.self.label === undefined && labels[model.self.id] === undefined) {
395
+ findings.push({
396
+ code: 'UML062_COMPOSITE_UNDOCUMENTED',
397
+ severity: 'info',
398
+ message: 'the classifier carries no name a reader can map back to the design: give it a display label or a narrative entry.',
399
+ evidence: { id: model.self.id },
400
+ });
401
+ }
402
+ for (const part of model.parts) {
403
+ if (part.label !== undefined || labels[part.id] !== undefined)
404
+ continue;
405
+ findings.push({
406
+ code: 'UML062_COMPOSITE_UNDOCUMENTED',
407
+ severity: 'info',
408
+ message: 'part ' + part.id + ' carries no name a reader can map back to the design: give it a display label or a narrative entry.',
409
+ evidence: { id: part.id },
410
+ });
411
+ }
412
+ return findings;
413
+ }
414
+ /** The composite structure family, as the engine's registry expects it. */
415
+ export const compositeModule = {
416
+ id: 'composite-structure',
417
+ render: (input, notation, _options) => renderComposite(input, notation),
418
+ parse: (text, notation) => parseComposite(text, notation),
419
+ // The self's `kind` is always drawn - the renderer writes it into the `compositeSelf` directive
420
+ // and into the declaration - so the parser always reads one back. A model that left it out is
421
+ // normalized to the same shape before the fidelity comparison, or the review reports its own
422
+ // default as drift: "self.kind invented by the diagram" on a diagram that says exactly what the
423
+ // model meant.
424
+ //
425
+ // `undeclared` is carried through deliberately. It is what `UML059` reads, and it is a fact about
426
+ // the supplied model rather than about the diagram, so normalizing it away would silence the one
427
+ // finding that says a connector ends at a name the classifier never declares - the silent pass
428
+ // this file's own comments promise not to produce.
429
+ normalize: (input) => {
430
+ try {
431
+ const normalized = validateCompositeModel(input);
432
+ const undeclared = input.undeclared;
433
+ if (!Array.isArray(undeclared))
434
+ return normalized;
435
+ return { ...normalized, undeclared: undeclared.filter((entry) => typeof entry === 'string') };
436
+ }
437
+ catch {
438
+ return undefined;
439
+ }
440
+ },
441
+ review: (model, labels) => reviewCompositeModel(model, labels),
442
+ };