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,317 @@
1
+ /**
2
+ * The structure families: class, object, package, component and deployment diagrams.
3
+ *
4
+ * A structure diagram is not a machine and not a use case list: it is a graph of
5
+ * nodes and the edges between them. The five families differ in vocabulary, not in
6
+ * shape, so they share one module factory and one model.
7
+ *
8
+ * Parsing is not reimplemented here. `structure.ts` already reads this text as a
9
+ * dependency graph, and it is the single source for that reading: this module calls
10
+ * it, then gives the result the family's name. Reviewing does the same - the graph is
11
+ * rendered back and handed to `reviewStructure`, so a structure diagram reviewed
12
+ * through the UML front end and one reviewed through `uml_structure_verify`
13
+ * report the same defects with the same codes.
14
+ *
15
+ * @module model-everything-uml-structure
16
+ */
17
+
18
+ import {
19
+ UmlError, UML_PLAIN_ID, directiveLine, readUmlDirectives, umlSourceLines,
20
+ type UmlNotation, type UmlFamilyId, type UmlFamilyModule, type UmlFamilyParse, type UmlFamilyRender,
21
+ type UmlFamilyFinding, type UmlFamilyRenderOptions,
22
+ } from './uml-syntax.js'
23
+ import { parseStructure, reviewStructure, type StructureGraph } from './structure.js'
24
+
25
+ export type StructureFamilyId = 'class' | 'object' | 'package' | 'component' | 'deployment'
26
+
27
+ export interface StructureModelNode {
28
+ id: string
29
+ /** The declaration the node came from: class, interface, component, node, ... */
30
+ kind: string
31
+ label?: string
32
+ /** The container this node was declared inside, when the diagram drew one. */
33
+ parent?: string
34
+ }
35
+
36
+ export interface StructureModelEdge {
37
+ from: string
38
+ to: string
39
+ label?: string
40
+ }
41
+
42
+ /** A structure diagram as a model: the nodes it declares and the edges it draws. */
43
+ export interface StructureModelV1 {
44
+ schemaVersion: 1
45
+ family: StructureFamilyId
46
+ nodes: StructureModelNode[]
47
+ edges: StructureModelEdge[]
48
+ }
49
+
50
+ const STRUCTURE_FAMILIES: readonly StructureFamilyId[] = ['class', 'object', 'package', 'component', 'deployment']
51
+
52
+ /** PlantUML keyword each kind is drawn with; the first is the fallback for the family. */
53
+ const PLANTUML_KEYWORDS: Record<string, string> = {
54
+ class: 'class', interface: 'interface', enum: 'enum', object: 'object', package: 'package',
55
+ component: 'component', node: 'node', artifact: 'artifact', database: 'database', actor: 'actor',
56
+ rectangle: 'rectangle', cloud: 'cloud', queue: 'queue', usecase: 'usecase', unknown: 'class',
57
+ }
58
+
59
+ const FAMILY_FALLBACK_KIND: Record<StructureFamilyId, string> = {
60
+ class: 'class', object: 'object', package: 'package', component: 'component', deployment: 'node',
61
+ }
62
+
63
+ const RESERVED_IDS = new Set(['class', 'classDiagram', 'end', 'graph', 'subgraph', 'direction', 'style', 'click', 'note', 'hide', 'show', 'title', 'namespace'])
64
+
65
+ function isPlainId(id: string): boolean {
66
+ return UML_PLAIN_ID.test(id) && !RESERVED_IDS.has(id.toLowerCase())
67
+ }
68
+
69
+ export function validateStructureModel(input: unknown, family: StructureFamilyId): StructureModelV1 {
70
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
71
+ throw new UmlError('a structure model must be an object with schemaVersion 1, nodes and edges', 'UML_INPUT')
72
+ }
73
+ const model = input as Partial<StructureModelV1>
74
+ if (model.schemaVersion !== 1) throw new UmlError('a structure model must declare schemaVersion 1', 'UML_INPUT')
75
+ const declaredFamily = model.family
76
+ if (declaredFamily !== undefined && !STRUCTURE_FAMILIES.includes(declaredFamily)) {
77
+ throw new UmlError('unknown structure family "' + String(declaredFamily) + '"; expected ' + STRUCTURE_FAMILIES.join(' | '), 'UML_INPUT')
78
+ }
79
+ if (declaredFamily !== undefined && declaredFamily !== family) {
80
+ throw new UmlError('this model is a ' + declaredFamily + ' diagram, and it was handed to the ' + family + ' module: render and review it as the family it declares.', 'UML_INPUT')
81
+ }
82
+ if (!Array.isArray(model.nodes) || !Array.isArray(model.edges)) {
83
+ // A model with neither field is not an empty structure diagram: it is some other
84
+ // family's model, and reading it as an empty diagram would hide that.
85
+ throw new UmlError('a structure model must declare nodes and edges as arrays', 'UML_INPUT')
86
+ }
87
+ const nodes = model.nodes
88
+ const edges = model.edges
89
+ const ids = new Set<string>()
90
+ for (const node of nodes) {
91
+ if (node === null || typeof node !== 'object' || typeof node.id !== 'string' || node.id.trim() === '') {
92
+ throw new UmlError('every node needs a non-empty string id', 'UML_INPUT')
93
+ }
94
+ if (ids.has(node.id)) throw new UmlError('duplicate node id "' + node.id + '"', 'UML_INPUT')
95
+ ids.add(node.id)
96
+ }
97
+ for (const edge of edges) {
98
+ if (edge === null || typeof edge !== 'object') throw new UmlError('every edge must be an object', 'UML_INPUT')
99
+ if (!ids.has(edge.from) || !ids.has(edge.to)) {
100
+ throw new UmlError('edge ' + edge.from + ' -> ' + edge.to + ' names a node the model does not declare', 'UML_INPUT')
101
+ }
102
+ }
103
+ const entries = nodes.map((node) => ({ id: node.id, kind: typeof node.kind === 'string' && node.kind !== '' ? node.kind : FAMILY_FALLBACK_KIND[family], ...(node.label === undefined ? {} : { label: node.label }), ...(node.parent === undefined ? {} : { parent: node.parent }) }))
104
+ // Canonical order: a container precedes what it contains, which is the order the
105
+ // renderer can draw. A model that arrives with a child first is normalised rather
106
+ // than reported as a fidelity difference the notation cannot actually express.
107
+ const known = new Set(entries.map((entry) => entry.id))
108
+ const canonical: typeof entries = []
109
+ const emitted = new Set<string>()
110
+ const emit = (entry: typeof entries[number]): void => {
111
+ if (emitted.has(entry.id)) return
112
+ emitted.add(entry.id)
113
+ canonical.push(entry)
114
+ for (const child of entries) if (child.parent === entry.id) emit(child)
115
+ }
116
+ for (const entry of entries) if (entry.parent === undefined || entry.parent === entry.id || !known.has(entry.parent)) emit(entry)
117
+ for (const entry of entries) emit(entry)
118
+ return {
119
+ schemaVersion: 1,
120
+ family,
121
+ nodes: canonical,
122
+ edges: edges.map((edge) => ({ from: edge.from, to: edge.to, ...(edge.label === undefined ? {} : { label: edge.label }) })),
123
+ }
124
+ }
125
+
126
+ function graphToModel(graph: StructureGraph, family: StructureFamilyId): StructureModelV1 {
127
+ return {
128
+ schemaVersion: 1,
129
+ family,
130
+ nodes: graph.nodes.map((node) => ({
131
+ id: node.id,
132
+ kind: node.kind === 'unknown' ? FAMILY_FALLBACK_KIND[family] : node.kind,
133
+ ...(node.label === undefined ? {} : { label: node.label }),
134
+ ...(node.parent === undefined ? {} : { parent: node.parent }),
135
+ })),
136
+ edges: graph.edges.map((edge) => ({ from: edge.from, to: edge.to, ...(edge.label === undefined ? {} : { label: edge.label }) })),
137
+ }
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // Parsing
142
+ // ---------------------------------------------------------------------------
143
+
144
+ export function parseStructureFamily(text: string, notation: UmlNotation, family: StructureFamilyId): UmlFamilyParse<StructureModelV1> {
145
+ const graph = parseStructure(text, notation)
146
+ const directives = readUmlDirectives(text.split(/\r?\n/), notation)
147
+ const alias = new Map<string, string>()
148
+ for (const value of directives.values.get('alias') ?? []) {
149
+ const split = value.indexOf(' ')
150
+ if (split > 0) alias.set(value.slice(0, split).trim(), value.slice(split + 1).trim())
151
+ }
152
+ const parents = new Map<string, string>()
153
+ for (const value of directives.values.get('parent') ?? []) {
154
+ const split = value.indexOf(' ')
155
+ if (split > 0) parents.set(value.slice(0, split).trim(), value.slice(split + 1).trim())
156
+ }
157
+ const kinds = new Map<string, string>()
158
+ for (const value of directives.values.get('kind') ?? []) {
159
+ const split = value.indexOf(' ')
160
+ if (split > 0) kinds.set(value.slice(0, split).trim(), value.slice(split + 1).trim())
161
+ }
162
+ const restore = (id: string): string => alias.get(id) ?? id
163
+ const labels: Record<string, string> = {}
164
+ const nodes = graph.nodes.map((node) => {
165
+ const id = restore(node.id)
166
+ const label = node.label === undefined || node.label === id ? undefined : node.label
167
+ if (label !== undefined) labels[id] = label
168
+ const pinned = kinds.get(node.id)
169
+ const kind = pinned ?? (node.kind === 'unknown' ? FAMILY_FALLBACK_KIND[family] : node.kind)
170
+ const declaredParent = parents.get(node.id)
171
+ const parent = declaredParent !== undefined ? declaredParent : (node.parent === undefined ? undefined : restore(node.parent))
172
+ return { id, kind, ...(label === undefined ? {} : { label }), ...(parent === undefined ? {} : { parent }) }
173
+ })
174
+ const edges = graph.edges.map((edge) => ({
175
+ from: restore(edge.from),
176
+ to: restore(edge.to),
177
+ ...(edge.label === undefined ? {} : { label: edge.label }),
178
+ }))
179
+ return {
180
+ model: validateStructureModel({ schemaVersion: 1, family, nodes, edges }, family),
181
+ labels,
182
+ warnings: [...graph.warnings],
183
+ discardedConstructs: [],
184
+ discardedEdges: 0,
185
+ }
186
+ }
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // Rendering
190
+ // ---------------------------------------------------------------------------
191
+
192
+ /** Which keyword draws this kind, in the family it was declared in. */
193
+ function keywordFor(kind: string, family: StructureFamilyId): string {
194
+ return PLANTUML_KEYWORDS[kind] ?? FAMILY_FALLBACK_KIND[family]
195
+ }
196
+
197
+ export function renderStructureFamily(input: unknown, notation: UmlNotation, family: StructureFamilyId): UmlFamilyRender {
198
+ const model = validateStructureModel(input, family)
199
+ const warnings: string[] = []
200
+ const taken = new Set<string>()
201
+ const alias = new Map<string, string>()
202
+ const aliasFor = (id: string): string => {
203
+ if (isPlainId(id) && !taken.has(id)) { taken.add(id); return id }
204
+ let index = 1
205
+ let candidate = 'N' + String(index)
206
+ while (taken.has(candidate)) { index += 1; candidate = 'N' + String(index) }
207
+ taken.add(candidate)
208
+ alias.set(candidate, id)
209
+ return candidate
210
+ }
211
+ if (notation === 'mermaid') {
212
+ if (family !== 'class') {
213
+ throw new UmlError('mermaid has no ' + family + ' diagram: its nearer views (architecture-beta, C4) use another vocabulary, and drawing one here would mislabel the model. Render it with notation "plantuml".', 'UML_VIEW_UNREPRESENTABLE')
214
+ }
215
+ const lines = ['classDiagram']
216
+ lines.push(directiveLine('mermaid', 'uml v1 notation=mermaid family=' + family))
217
+ const notationIds = model.nodes.map((node) => aliasFor(node.id))
218
+ model.nodes.forEach((node, index) => lines.push(directiveLine('mermaid', 'kind ' + notationIds[index] + ' ' + node.kind)))
219
+ model.nodes.forEach((node, index) => { if (node.parent !== undefined) lines.push(directiveLine('mermaid', 'parent ' + notationIds[index] + ' ' + node.parent)) })
220
+ for (const [notationId, modelId] of alias) lines.push(directiveLine('mermaid', 'alias ' + notationId + ' ' + modelId))
221
+ model.nodes.forEach((node, index) => {
222
+ const id = notationIds[index]
223
+ if (/["\n\r]/.test(node.label ?? '')) {
224
+ warnings.push('UML_RENDER_LABEL_UNSAFE: the label of "' + node.id + '" contains a quote or a line break; it was written with single quotes instead and will not read back verbatim.')
225
+ }
226
+ const label = (node.label ?? '').replace(/["\n\r]+/g, "'")
227
+ lines.push(label === '' || label === id ? ' class ' + id : ' class ' + id + '["' + label + '"]')
228
+ })
229
+ for (const edge of model.edges) {
230
+ const idOf = new Map(model.nodes.map((node, index) => [node.id, notationIds[index]]))
231
+ lines.push(' ' + (idOf.get(edge.from) ?? edge.from) + ' --> ' + (idOf.get(edge.to) ?? edge.to) + (edge.label === undefined ? '' : ' : ' + edge.label))
232
+ }
233
+ return { primary: lines.join('\n') + '\n', warnings }
234
+ }
235
+
236
+ const lines = ['@startuml']
237
+ lines.push(directiveLine('plantuml', 'uml v1 notation=plantuml family=' + family))
238
+ const notationIds = model.nodes.map((node) => aliasFor(node.id))
239
+ model.nodes.forEach((node, index) => lines.push(directiveLine('plantuml', 'kind ' + notationIds[index] + ' ' + node.kind)))
240
+ model.nodes.forEach((node, index) => { if (node.parent !== undefined) lines.push(directiveLine('plantuml', 'parent ' + notationIds[index] + ' ' + node.parent)) })
241
+ for (const [notationId, modelId] of alias) lines.push(directiveLine('plantuml', 'alias ' + notationId + ' ' + modelId))
242
+ const position = new Map(model.nodes.map((node, index) => [node.id, index]))
243
+ const children = new Map<string, StructureModelNode[]>()
244
+ const roots: StructureModelNode[] = []
245
+ const known = new Set(model.nodes.map((node) => node.id))
246
+ for (const node of model.nodes) {
247
+ const parent = node.parent !== undefined && node.parent !== node.id && known.has(node.parent) ? node.parent : undefined
248
+ if (parent === undefined) { roots.push(node); continue }
249
+ const list = children.get(parent)
250
+ if (list === undefined) children.set(parent, [node])
251
+ else list.push(node)
252
+ }
253
+ const declare = (node: StructureModelNode, indent: string): void => {
254
+ const id = notationIds[position.get(node.id) as number]
255
+ const keyword = keywordFor(node.kind, family)
256
+ const label = (node.label ?? '').replace(/["\n\r]+/g, "'")
257
+ if ((node.label ?? '') !== label) warnings.push('UML_RENDER_LABEL_UNSAFE: the label of "' + node.id + '" contains a quote or a line break; it was written with single quotes instead and will not read back verbatim.')
258
+ const members = children.get(node.id) ?? []
259
+ const head = label === '' || label === id ? keyword + ' ' + id : keyword + ' "' + label + '" as ' + id
260
+ if (members.length === 0) { lines.push(indent + head); return }
261
+ lines.push(indent + head + ' {')
262
+ for (const member of members) declare(member, indent + ' ')
263
+ lines.push(indent + '}')
264
+ }
265
+ for (const root of roots) declare(root, '')
266
+ const idOf = new Map(model.nodes.map((node, index) => [node.id, notationIds[index]]))
267
+ for (const edge of model.edges) {
268
+ lines.push((idOf.get(edge.from) ?? edge.from) + ' --> ' + (idOf.get(edge.to) ?? edge.to) + (edge.label === undefined ? '' : ' : ' + edge.label))
269
+ }
270
+ lines.push('@enduml')
271
+ return { primary: lines.join('\n') + '\n', warnings }
272
+ }
273
+
274
+ // ---------------------------------------------------------------------------
275
+ // Review
276
+ // ---------------------------------------------------------------------------
277
+
278
+ /**
279
+ * The review is the structure tool's, reached through a render.
280
+ *
281
+ * A structure diagram's defects are properties of its graph, and `reviewStructure`
282
+ * already checks them (isolated node, dangling endpoint, cycle, and the matrix rules
283
+ * when one is declared). Re-deriving them here would be a second implementation of
284
+ * the same checks, and the two would drift. So the model is rendered back and handed
285
+ * over, which also proves the round trip on the way.
286
+ */
287
+ export function reviewStructureFamily(model: StructureModelV1, labels: Record<string, string>, notation: UmlNotation): UmlFamilyFinding[] {
288
+ const rendered = renderStructureFamily(model, notation, model.family)
289
+ const report = reviewStructure({ diagram: rendered.primary, notation })
290
+ const findings: UmlFamilyFinding[] = report.findings.map((finding) => ({
291
+ code: finding.code,
292
+ severity: finding.severity,
293
+ message: finding.message,
294
+ ...(finding.detail === undefined ? {} : { detail: finding.detail }),
295
+ ...(finding.evidence === undefined ? {} : { evidence: finding.evidence as Record<string, unknown> }),
296
+ }))
297
+ for (const node of model.nodes) {
298
+ if (node.label !== undefined || labels[node.id] !== undefined) continue
299
+ findings.push({
300
+ code: 'UML040_STRUCTURE_NODE_UNDOCUMENTED',
301
+ severity: 'info',
302
+ message: 'node ' + node.id + ' carries no name a reader can map back to the code: give it a display label.',
303
+ evidence: { id: node.id },
304
+ })
305
+ }
306
+ return findings
307
+ }
308
+
309
+ /** One module per structure family: they differ in vocabulary, not in shape. */
310
+ export function structureFamilyModule(family: StructureFamilyId): UmlFamilyModule<StructureModelV1> {
311
+ return {
312
+ id: family as UmlFamilyId,
313
+ render: (input: unknown, notation: UmlNotation, _options: UmlFamilyRenderOptions): UmlFamilyRender => renderStructureFamily(input, notation, family),
314
+ parse: (text: string, notation: UmlNotation): UmlFamilyParse<StructureModelV1> => parseStructureFamily(text, notation, family),
315
+ review: (model: StructureModelV1, labels: Record<string, string>, notation: UmlNotation): UmlFamilyFinding[] => reviewStructureFamily(model, labels, notation),
316
+ }
317
+ }