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,353 @@
1
+ /**
2
+ * The communication diagram module.
3
+ *
4
+ * A communication diagram is a set of lifelines joined by links, with numbered messages
5
+ * travelling along the links. PlantUML has no communication-diagram type - the page is a
6
+ * 404 and the navigation lists none - but the drawing is expressible with the object
7
+ * diagram's vocabulary: objects for the lifelines, arrows for the links, and the sequence
8
+ * number in the arrow's label.
9
+ *
10
+ * That last point is why this family is *not* refused. An earlier version of the family
11
+ * table said neither notation spells the numbering or the link roles, so a parse would
12
+ * invent an order. The numbering is spelled - `C --> O : 1: create` carries both the link
13
+ * and the order - and the renderer writes it back the same way. What the notation cannot
14
+ * say is the *nesting* that a number like `1.1` implies (which message is a nested call of
15
+ * which), so the model keeps the number as the string the diagram wrote and makes no claim
16
+ * about the tree it suggests.
17
+ *
18
+ * @module model-everything-uml-communication
19
+ */
20
+ import { UmlError, UML_PLAIN_ID, directiveLine, readUmlDirectives, } from './uml-syntax.js';
21
+ /** The number a message carries, matched at the front of an arrow label. */
22
+ const SEQUENCE_PREFIX = /^\s*(\d+(?:\.\d+)*[a-zA-Z]?)\s*:\s*(.*)$/;
23
+ const OBJECT_DECLARATION = /^object\s+(?:"([^"]*)"|(\S+))(?:\s+as\s+(\S+))?\s*$/i;
24
+ const ARROW = /^(\S+?)\s*(-->|->|--|\.\.>|\.\.|<--|<-)\s*(\S+?)(?:\s*:\s*(.*))?$/;
25
+ function quote(text) {
26
+ return '"' + text.replace(/"/g, "'") + '"';
27
+ }
28
+ /**
29
+ * Validate a candidate model and normalize it.
30
+ *
31
+ * A sequence number must be unique: two messages with the same number is not a style
32
+ * question, it is a diagram whose order has two answers.
33
+ */
34
+ export function validateCommunicationModel(input) {
35
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
36
+ throw new UmlError('a communication model must be an object with schemaVersion 1, lifelines, links and messages', 'UML_INPUT');
37
+ }
38
+ const model = input;
39
+ if (model.schemaVersion !== 1)
40
+ throw new UmlError('a communication model must declare schemaVersion 1', 'UML_INPUT');
41
+ // A model from another family validated here: every list this family reads defaults to empty, so a structure
42
+ // model became "a communication diagram with nothing in it" - it rendered as an empty page, and the review threw
43
+ // a TypeError on `lifelines.length`. The three lists ARE the family, so a model that declares other fields and
44
+ // none of them is not this family's model, and naming both sides is the answer a reader can act on. A model with
45
+ // no fields at all is not a mismatch: it is an empty communication model, which `UML063` reports.
46
+ const declared = Object.keys(model).filter((key) => key !== 'schemaVersion');
47
+ const declaresLists = ['lifelines', 'links', 'messages'].some((key) => model[key] !== undefined);
48
+ if (!declaresLists && declared.length > 0) {
49
+ throw new UmlError('this is not a communication model: a communication diagram is lifelines, links and numbered messages, and this model declares none of them (it declares ' + declared.slice(0, 6).join(', ') + '). Pass the lifelines/links/messages model, or review it as the family it belongs to.', 'UML041_MODEL_FAMILY_MISMATCH');
50
+ }
51
+ for (const [name, value] of Object.entries({ lifelines: model.lifelines ?? [], links: model.links ?? [], messages: model.messages ?? [] })) {
52
+ if (!Array.isArray(value))
53
+ throw new UmlError(name + ' must be an array', 'UML_INPUT');
54
+ }
55
+ const lifelines = [];
56
+ const known = new Set();
57
+ for (const lifeline of model.lifelines ?? []) {
58
+ if (lifeline === null || typeof lifeline !== 'object' || typeof lifeline.id !== 'string' || lifeline.id.trim() === '') {
59
+ throw new UmlError('every lifeline needs a non-empty string id', 'UML_INPUT');
60
+ }
61
+ if (known.has(lifeline.id))
62
+ throw new UmlError('lifeline ' + quote(lifeline.id) + ' is declared more than once', 'UML_INPUT');
63
+ known.add(lifeline.id);
64
+ lifelines.push({
65
+ id: lifeline.id,
66
+ ...(lifeline.label === undefined ? {} : { label: lifeline.label }),
67
+ ...(lifeline.classifier === undefined ? {} : { classifier: lifeline.classifier }),
68
+ });
69
+ }
70
+ const undeclared = new Set();
71
+ const links = [];
72
+ for (const link of model.links ?? []) {
73
+ if (link === null || typeof link !== 'object')
74
+ throw new UmlError('every link must be an object', 'UML_INPUT');
75
+ for (const side of ['from', 'to']) {
76
+ const endpoint = link[side];
77
+ if (typeof endpoint !== 'string' || endpoint.trim() === '')
78
+ throw new UmlError('every link needs a non-empty `' + side + '` end', 'UML_INPUT');
79
+ if (!known.has(endpoint))
80
+ undeclared.add(endpoint);
81
+ }
82
+ if (link.from === link.to)
83
+ throw new UmlError('a link cannot join ' + quote(link.from) + ' to itself', 'UML_INPUT');
84
+ links.push({ from: link.from, to: link.to, ...(link.label === undefined ? {} : { label: link.label }) });
85
+ }
86
+ const messages = [];
87
+ const sequences = new Map();
88
+ for (const message of model.messages ?? []) {
89
+ if (message === null || typeof message !== 'object')
90
+ throw new UmlError('every message must be an object', 'UML_INPUT');
91
+ if (typeof message.sequence !== 'string' || message.sequence.trim() === '') {
92
+ throw new UmlError('every message needs a sequence number as a non-empty string', 'UML_INPUT');
93
+ }
94
+ const sequence = message.sequence.trim();
95
+ const previous = sequences.get(sequence);
96
+ if (previous !== undefined) {
97
+ throw new UmlError('sequence number ' + quote(sequence) + ' is used twice (message ' + String(previous + 1) + ' and this one): the order of the diagram would have two answers', 'UML_INPUT');
98
+ }
99
+ sequences.set(sequence, messages.length);
100
+ for (const side of ['from', 'to']) {
101
+ const endpoint = message[side];
102
+ if (typeof endpoint !== 'string' || endpoint.trim() === '')
103
+ throw new UmlError('every message needs a non-empty `' + side + '` end', 'UML_INPUT');
104
+ if (!known.has(endpoint))
105
+ undeclared.add(endpoint);
106
+ }
107
+ messages.push({ sequence, from: message.from, to: message.to, ...(message.label === undefined ? {} : { label: message.label }) });
108
+ }
109
+ return {
110
+ schemaVersion: 1,
111
+ lifelines,
112
+ links,
113
+ messages,
114
+ ...(undeclared.size === 0 ? {} : { undeclared: [...undeclared].sort() }),
115
+ ...(model.narrative === undefined ? {} : { narrative: model.narrative }),
116
+ };
117
+ }
118
+ /** The label a message is drawn with: `1: create`. */
119
+ function messageLabel(message) {
120
+ return message.sequence + ': ' + (message.label ?? '');
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // Rendering
124
+ // ---------------------------------------------------------------------------
125
+ /**
126
+ * Render a communication diagram as PlantUML.
127
+ *
128
+ * Objects for the lifelines, arrows for the links, and one arrow per message with its
129
+ * number in the label. A message whose lifelines have no declared link is still drawn -
130
+ * the notation allows it - and the review reports it rather than the renderer hiding it.
131
+ *
132
+ * @param input - candidate `CommunicationModelV1`.
133
+ * @param notation - only `plantuml` draws this family.
134
+ */
135
+ export function renderCommunication(input, notation) {
136
+ if (notation !== 'plantuml') {
137
+ throw new UmlError('a communication diagram can only be drawn in plantuml; mermaid has no lifeline-and-link notation with numbered messages. Render it with notation "plantuml".', 'UML_VIEW_UNREPRESENTABLE');
138
+ }
139
+ const model = validateCommunicationModel(input);
140
+ const warnings = [
141
+ 'UML_RENDER_APPROXIMATE_VIEW: plantuml has no communication diagram type; this is an object-diagram drawing of one. Lifelines, links and the sequence numbers are carried; the nesting a dotted number like 1.1 suggests is not drawn and not claimed.',
142
+ ];
143
+ const lines = ['@startuml'];
144
+ lines.push(directiveLine('plantuml', 'uml v1 notation=plantuml family=communication'));
145
+ for (const lifeline of model.lifelines) {
146
+ const shown = lifeline.classifier === undefined ? quote(lifeline.label ?? lifeline.id) : quote(':' + lifeline.classifier + ':');
147
+ const alias = lifeline.id === lifeline.label || lifeline.label === undefined ? lifeline.id : lifeline.id;
148
+ lines.push('object ' + shown + ' as ' + alias);
149
+ }
150
+ for (const link of model.links) {
151
+ lines.push(link.from + ' -- ' + link.to + (link.label === undefined ? '' : ' : ' + link.label));
152
+ }
153
+ for (const message of model.messages) {
154
+ lines.push(message.from + ' --> ' + message.to + ' : ' + messageLabel(message));
155
+ }
156
+ lines.push('@enduml');
157
+ return { primary: lines.join('\n') + '\n', warnings };
158
+ }
159
+ // ---------------------------------------------------------------------------
160
+ // Parsing
161
+ // ---------------------------------------------------------------------------
162
+ /**
163
+ * Read a communication diagram back into a model.
164
+ *
165
+ * Hand-written text is read the same way the renderer writes it: `object` declarations are
166
+ * the lifelines, `--` arrows are the links, and an arrow whose label starts with a number
167
+ * and a colon is a message. A line that is neither is reported in `discardedConstructs`.
168
+ */
169
+ export function parseCommunication(text, notation) {
170
+ if (notation !== 'plantuml') {
171
+ throw new UmlError('a communication diagram can only be read from plantuml', 'UML_VIEW_UNREPRESENTABLE');
172
+ }
173
+ const warnings = [];
174
+ const discarded = [];
175
+ const labels = {};
176
+ const lifelines = [];
177
+ const links = [];
178
+ const messages = [];
179
+ const raw = text.split(/\r?\n/);
180
+ for (let index = 0; index < raw.length; index += 1) {
181
+ const line = raw[index].trim();
182
+ if (line === '' || line.startsWith('@'))
183
+ continue;
184
+ if (line.startsWith("'")) {
185
+ // The renderer's directive line is not a construct; a comment is not either.
186
+ continue;
187
+ }
188
+ if (/^(left to right direction|top to bottom direction|skinparam|hide|show|title|scale|allowmixing)\b/i.test(line)) {
189
+ discarded.push({ line: index + 1, text: line, construct: line.split(/\s+/)[0].toLowerCase() });
190
+ continue;
191
+ }
192
+ const declaration = OBJECT_DECLARATION.exec(line);
193
+ if (declaration !== null) {
194
+ const shown = declaration[1] ?? declaration[2] ?? '';
195
+ const alias = declaration[3];
196
+ const id = alias ?? shown;
197
+ const classifier = /^:(.*):$/.exec(shown);
198
+ const entry = { id };
199
+ if (classifier !== null)
200
+ entry.classifier = classifier[1];
201
+ else if (shown !== '' && shown !== id)
202
+ entry.label = shown;
203
+ if (lifelines.some((existing) => existing.id === id)) {
204
+ warnings.push('line ' + String(index + 1) + ': lifeline ' + id + ' is declared twice; the second declaration is ignored');
205
+ continue;
206
+ }
207
+ lifelines.push(entry);
208
+ if (entry.label !== undefined)
209
+ labels[id] = entry.label;
210
+ continue;
211
+ }
212
+ const arrow = ARROW.exec(line);
213
+ if (arrow !== null) {
214
+ const [, from, operator, to, rawLabel] = arrow;
215
+ const [left, right] = operator.startsWith('<') ? [to, from] : [from, to];
216
+ const labelText = rawLabel === undefined ? undefined : rawLabel.trim();
217
+ const sequence = labelText === undefined ? null : SEQUENCE_PREFIX.exec(labelText);
218
+ if (operator.includes('>') && sequence !== null) {
219
+ messages.push({ sequence: sequence[1], from: left, to: right, ...(sequence[2] === '' ? {} : { label: sequence[2] }) });
220
+ }
221
+ else {
222
+ links.push({ from: left, to: right, ...(labelText === undefined || labelText === '' ? {} : { label: labelText }) });
223
+ }
224
+ continue;
225
+ }
226
+ discarded.push({ line: index + 1, text: line, construct: 'line this model does not carry' });
227
+ }
228
+ const known = new Set(lifelines.map((lifeline) => lifeline.id));
229
+ const undeclared = [...new Set([...links, ...messages].flatMap((entry) => [entry.from, entry.to]))]
230
+ .filter((endpoint) => !known.has(endpoint))
231
+ .sort();
232
+ const model = {
233
+ schemaVersion: 1,
234
+ lifelines,
235
+ links,
236
+ messages,
237
+ ...(undeclared.length === 0 ? {} : { undeclared }),
238
+ };
239
+ return { model, labels, warnings, discardedConstructs: discarded, discardedEdges: 0 };
240
+ }
241
+ export const COMMUNICATION_FINDING_CODES = [
242
+ 'UML063_COMMUNICATION_NO_LIFELINES',
243
+ 'UML064_COMMUNICATION_LIFELINE_UNLINKED',
244
+ 'UML065_COMMUNICATION_MESSAGE_WITHOUT_LINK',
245
+ 'UML066_COMMUNICATION_UNDECLARED_ENDPOINT',
246
+ 'UML067_COMMUNICATION_SEQUENCE_DUPLICATE',
247
+ 'UML068_COMMUNICATION_UNDOCUMENTED',
248
+ ];
249
+ /** Sequence numbers as they sort: `2` before `10`, `1.1` before `1.2`. */
250
+ function sequenceOrder(left, right) {
251
+ const parts = (value) => value.split('.').map((part) => Number.parseInt(part, 10)).map((part) => (Number.isNaN(part) ? 0 : part));
252
+ const a = parts(left);
253
+ const b = parts(right);
254
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
255
+ const difference = (a[index] ?? -1) - (b[index] ?? -1);
256
+ if (difference !== 0)
257
+ return difference;
258
+ }
259
+ return 0;
260
+ }
261
+ /**
262
+ * What a reader would get wrong about a communication diagram.
263
+ *
264
+ * The checks are the ones the diagram exists to answer: is every lifeline reachable, does
265
+ * every message travel a link that was drawn, does any number appear twice, and is each
266
+ * participant named after something in the design. The order the numbers imply is *not*
267
+ * checked against the drawing order - the numbers are the order, and the drawing order is
268
+ * not a claim.
269
+ */
270
+ export function reviewCommunicationModel(model, labels) {
271
+ const findings = [];
272
+ const name = (id) => labels[id] ?? id;
273
+ if (model.lifelines.length === 0) {
274
+ findings.push({
275
+ code: 'UML063_COMMUNICATION_NO_LIFELINES',
276
+ severity: 'error',
277
+ message: 'the diagram declares no lifeline, so no participant is named and nothing can travel.',
278
+ evidence: { lifelines: 0 },
279
+ });
280
+ }
281
+ const touched = new Set([...model.links, ...model.messages].flatMap((entry) => [entry.from, entry.to]));
282
+ for (const lifeline of model.lifelines) {
283
+ if (touched.has(lifeline.id))
284
+ continue;
285
+ findings.push({
286
+ code: 'UML064_COMMUNICATION_LIFELINE_UNLINKED',
287
+ severity: 'warning',
288
+ message: 'lifeline ' + name(lifeline.id) + ' takes part in no link and no message, so nothing reaches it.',
289
+ evidence: { id: lifeline.id },
290
+ });
291
+ }
292
+ const linked = new Set(model.links.map((link) => [link.from, link.to].sort().join('\u0000')));
293
+ for (const message of model.messages) {
294
+ if (message.from === message.to)
295
+ continue;
296
+ if (linked.has([message.from, message.to].sort().join('\u0000')))
297
+ continue;
298
+ findings.push({
299
+ code: 'UML065_COMMUNICATION_MESSAGE_WITHOUT_LINK',
300
+ severity: 'warning',
301
+ message: 'message ' + message.sequence + ' travels from ' + name(message.from) + ' to ' + name(message.to) + ', but the diagram draws no link between them.',
302
+ evidence: { sequence: message.sequence, from: message.from, to: message.to },
303
+ });
304
+ }
305
+ const undeclared = model.undeclared ?? [];
306
+ if (undeclared.length > 0) {
307
+ findings.push({
308
+ code: 'UML066_COMMUNICATION_UNDECLARED_ENDPOINT',
309
+ severity: 'error',
310
+ message: 'a link or message names ' + String(undeclared.length) + ' participant(s) the diagram never declares, so the line points at nothing a reader can look up.',
311
+ detail: undeclared.join(', '),
312
+ evidence: { ids: undeclared },
313
+ });
314
+ }
315
+ const seen = new Map();
316
+ for (const message of model.messages) {
317
+ const count = (seen.get(message.sequence) ?? 0) + 1;
318
+ seen.set(message.sequence, count);
319
+ if (count > 1) {
320
+ findings.push({
321
+ code: 'UML067_COMMUNICATION_SEQUENCE_DUPLICATE',
322
+ severity: 'error',
323
+ message: 'sequence number ' + message.sequence + ' is used more than once, so the order of the diagram has two answers.',
324
+ evidence: { sequence: message.sequence },
325
+ });
326
+ }
327
+ }
328
+ if (model.narrative?.lifelines === undefined) {
329
+ for (const lifeline of model.lifelines) {
330
+ if (lifeline.label !== undefined || lifeline.classifier !== undefined || labels[lifeline.id] !== undefined)
331
+ continue;
332
+ findings.push({
333
+ code: 'UML068_COMMUNICATION_UNDOCUMENTED',
334
+ severity: 'info',
335
+ message: 'lifeline ' + lifeline.id + ' carries no name a reader can map back to the design: give it a display label, a classifier or a narrative entry.',
336
+ evidence: { id: lifeline.id },
337
+ });
338
+ }
339
+ }
340
+ return findings;
341
+ }
342
+ /** The communication family, as the engine's registry expects it. */
343
+ export const communicationModule = {
344
+ id: 'communication',
345
+ render: (input, notation, _options) => renderCommunication(input, notation),
346
+ parse: (text, notation) => parseCommunication(text, notation),
347
+ // Validated, not trusted: the review path can hand over the model exactly as the caller wrote it, and every field
348
+ // this family reads is optional. Without this line `model.lifelines.length` is a TypeError on a foreign model -
349
+ // a crash where a review belongs, and one the validator already knows how to explain.
350
+ review: (model, labels) => reviewCommunicationModel(validateCommunicationModel(model), labels),
351
+ };
352
+ /** Sort two sequence numbers the way a reader reads them: `2` before `10`, `1.1` before `1.2`. */
353
+ export { sequenceOrder as communicationSequenceOrder };