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,439 @@
1
+ /**
2
+ * The profile diagram module.
3
+ *
4
+ * A profile is a package that extends the metamodel: stereotypes, the metaclasses they
5
+ * extend, and the profiles applied to a package. That is a shape of its own, so it has
6
+ * its own model rather than being flattened into a class diagram.
7
+ *
8
+ * PlantUML draws one *approximately*: it has `metaclass` and `stereotype` elements and
9
+ * `class X <<stereotype>>`, and the relation set of its class diagram, but no `profile`
10
+ * keyword and no Profile/ProfileApplication element. The drawing is therefore one-way.
11
+ * Reading the text back would have to guess which `--|>` is a stereotype extending a
12
+ * metaclass and which is one stereotype specialising another — two different facts about
13
+ * the profile, drawn identically — so the parse is refused by name instead of inventing
14
+ * the one the reader cannot see. That refusal is the family table's `parseable: false`.
15
+ *
16
+ * @module model-everything-uml-profile
17
+ */
18
+
19
+ import {
20
+ UmlError, UML_PLAIN_ID, directiveLine, type UmlNotation, type UmlFamilyModule,
21
+ type UmlFamilyParse, type UmlFamilyRender, type UmlFamilyFinding, type UmlFamilyRenderOptions,
22
+ } from './uml-syntax.js'
23
+
24
+ /** The profile itself: the package the stereotypes are defined in. */
25
+ export interface ProfileSpec {
26
+ id: string
27
+ label?: string
28
+ }
29
+
30
+ export interface ProfileStereotype {
31
+ id: string
32
+ /** Human-readable name, when the diagram gave one that differs from the id. */
33
+ label?: string
34
+ }
35
+
36
+ /** `ExtensionEnd` multiplicity, as the specification spells it: `0..1` or `1`. */
37
+ export type ProfileMultiplicity = '0..1' | '1'
38
+
39
+ /** A stereotype extending a metaclass: the `Extension` of the metamodel. */
40
+ export interface ProfileExtension {
41
+ stereotype: string
42
+ metaclass: string
43
+ /** The composite end's multiplicity. Absent when the diagram did not say. */
44
+ multiplicity?: ProfileMultiplicity
45
+ /** A role name on the extension end, when the profile gave one. */
46
+ label?: string
47
+ }
48
+
49
+ /** A profile made available to a package: `ProfileApplication`. */
50
+ export interface ProfileApplication {
51
+ profile: string
52
+ /** The package the profile applies to. */
53
+ applyTo: string
54
+ }
55
+
56
+ /**
57
+ * A profile diagram as a model.
58
+ *
59
+ * `applications` is carried even though this build's renderer leaves it to the
60
+ * directives: PlantUML has no ProfileApplication element, and a profile whose
61
+ * application is not written down is a profile nobody can tell is in use.
62
+ */
63
+ export interface ProfileModelV1 {
64
+ schemaVersion: 1
65
+ profile: ProfileSpec
66
+ stereotypes: ProfileStereotype[]
67
+ metaclasses: ProfileStereotype[]
68
+ extensions: ProfileExtension[]
69
+ applications: ProfileApplication[]
70
+ /** Stereotype-to-stereotype specialisation; a different fact from an extension. */
71
+ generalizations: Array<{ general: string; specific: string }>
72
+ /** Ids a relation named that no declaration introduced; the review reports them. */
73
+ undeclared?: string[]
74
+ narrative?: { profile?: string; stereotypes?: Record<string, string>; metaclasses?: Record<string, string> }
75
+ }
76
+
77
+ const MULTIPLICITIES: readonly ProfileMultiplicity[] = ['0..1', '1']
78
+
79
+ /** PlantUML ids that cannot stand alone in a declaration. */
80
+ const RESERVED_IDS = new Set(['class', 'metaclass', 'stereotype', 'enum', 'interface', 'abstract', 'package', 'end', 'note', 'hide', 'show', 'skinparam', 'title', 'left', 'right', 'top', 'bottom'])
81
+
82
+ function isPlainId(id: string): boolean {
83
+ return UML_PLAIN_ID.test(id) && !RESERVED_IDS.has(id.toLowerCase())
84
+ }
85
+
86
+ function quote(text: string): string {
87
+ return '"' + text.replace(/"/g, "'") + '"'
88
+ }
89
+
90
+ /**
91
+ * Validate a candidate model and normalize it.
92
+ *
93
+ * The contract every family keeps: an undeclared key or a wrong type is a refusal with
94
+ * `UML_INPUT`, never a silently dropped field.
95
+ */
96
+ export function validateProfileModel(input: unknown): ProfileModelV1 {
97
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
98
+ throw new UmlError('a profile model must be an object with schemaVersion 1, a profile, stereotypes, metaclasses and extensions', 'UML_INPUT')
99
+ }
100
+ const model = input as Partial<ProfileModelV1>
101
+ if (model.schemaVersion !== 1) throw new UmlError('a profile model must declare schemaVersion 1', 'UML_INPUT')
102
+ const profile = model.profile
103
+ if (profile === null || typeof profile !== 'object' || typeof profile.id !== 'string' || profile.id.trim() === '') {
104
+ throw new UmlError('a profile model needs a `profile` with a non-empty id', 'UML_INPUT')
105
+ }
106
+ const lists = {
107
+ stereotypes: model.stereotypes ?? [],
108
+ metaclasses: model.metaclasses ?? [],
109
+ extensions: model.extensions ?? [],
110
+ applications: model.applications ?? [],
111
+ generalizations: model.generalizations ?? [],
112
+ }
113
+ for (const [name, value] of Object.entries(lists)) {
114
+ if (!Array.isArray(value)) throw new UmlError(name + ' must be an array', 'UML_INPUT')
115
+ }
116
+
117
+ const stereotypeIds = new Set<string>()
118
+ for (const stereotype of lists.stereotypes) {
119
+ if (stereotype === null || typeof stereotype !== 'object' || typeof stereotype.id !== 'string' || stereotype.id.trim() === '') {
120
+ throw new UmlError('every stereotype needs a non-empty string id', 'UML_INPUT')
121
+ }
122
+ if (stereotypeIds.has(stereotype.id)) throw new UmlError('duplicate stereotype id ' + quote(stereotype.id), 'UML_INPUT')
123
+ stereotypeIds.add(stereotype.id)
124
+ }
125
+ const metaclassIds = new Set<string>()
126
+ for (const metaclass of lists.metaclasses) {
127
+ if (metaclass === null || typeof metaclass !== 'object' || typeof metaclass.id !== 'string' || metaclass.id.trim() === '') {
128
+ throw new UmlError('every metaclass needs a non-empty string id', 'UML_INPUT')
129
+ }
130
+ if (metaclassIds.has(metaclass.id)) throw new UmlError('duplicate metaclass id ' + quote(metaclass.id), 'UML_INPUT')
131
+ metaclassIds.add(metaclass.id)
132
+ }
133
+ for (const id of [...stereotypeIds]) {
134
+ if (metaclassIds.has(id)) throw new UmlError(quote(id) + ' is declared as both a stereotype and a metaclass', 'UML_INPUT')
135
+ }
136
+
137
+ const undeclared = new Set<string>()
138
+ const extensions: ProfileExtension[] = []
139
+ for (const extension of lists.extensions) {
140
+ if (extension === null || typeof extension !== 'object') throw new UmlError('every extension must be an object', 'UML_INPUT')
141
+ if (typeof extension.stereotype !== 'string' || extension.stereotype.trim() === '') throw new UmlError('every extension needs the stereotype that extends', 'UML_INPUT')
142
+ if (typeof extension.metaclass !== 'string' || extension.metaclass.trim() === '') throw new UmlError('every extension needs the metaclass it extends', 'UML_INPUT')
143
+ if (!stereotypeIds.has(extension.stereotype)) undeclared.add(extension.stereotype)
144
+ if (!metaclassIds.has(extension.metaclass)) undeclared.add(extension.metaclass)
145
+ if (extension.multiplicity !== undefined && !MULTIPLICITIES.includes(extension.multiplicity)) {
146
+ throw new UmlError('extension multiplicity must be ' + MULTIPLICITIES.join(' | ') + ', got ' + quote(String(extension.multiplicity)), 'UML_INPUT')
147
+ }
148
+ extensions.push({
149
+ stereotype: extension.stereotype,
150
+ metaclass: extension.metaclass,
151
+ ...(extension.multiplicity === undefined ? {} : { multiplicity: extension.multiplicity }),
152
+ ...(extension.label === undefined ? {} : { label: extension.label }),
153
+ })
154
+ }
155
+
156
+ const generalizations: Array<{ general: string; specific: string }> = []
157
+ for (const generalization of lists.generalizations) {
158
+ if (generalization === null || typeof generalization !== 'object') throw new UmlError('every generalization must be an object', 'UML_INPUT')
159
+ for (const side of ['general', 'specific'] as const) {
160
+ const id = generalization[side]
161
+ if (typeof id !== 'string' || id.trim() === '') throw new UmlError('every generalization needs a non-empty `' + side + '` stereotype id', 'UML_INPUT')
162
+ if (!stereotypeIds.has(id)) undeclared.add(id)
163
+ }
164
+ if (generalization.general === generalization.specific) {
165
+ throw new UmlError('stereotype ' + quote(generalization.general) + ' cannot generalize itself', 'UML_INPUT')
166
+ }
167
+ generalizations.push({ general: generalization.general, specific: generalization.specific })
168
+ }
169
+
170
+ const applications: ProfileApplication[] = []
171
+ for (const application of lists.applications) {
172
+ if (application === null || typeof application !== 'object') throw new UmlError('every application must be an object', 'UML_INPUT')
173
+ for (const side of ['profile', 'applyTo'] as const) {
174
+ const value = application[side]
175
+ if (typeof value !== 'string' || value.trim() === '') throw new UmlError('every application needs a non-empty `' + side + '`', 'UML_INPUT')
176
+ }
177
+ if (application.profile !== profile.id) undeclared.add(application.profile)
178
+ applications.push({ profile: application.profile, applyTo: application.applyTo })
179
+ }
180
+
181
+ return {
182
+ schemaVersion: 1,
183
+ profile: { id: profile.id, ...(profile.label === undefined ? {} : { label: profile.label }) },
184
+ stereotypes: lists.stereotypes.map((stereotype) => ({ id: stereotype.id, ...(stereotype.label === undefined ? {} : { label: stereotype.label }) })),
185
+ metaclasses: lists.metaclasses.map((metaclass) => ({ id: metaclass.id, ...(metaclass.label === undefined ? {} : { label: metaclass.label }) })),
186
+ extensions,
187
+ applications,
188
+ generalizations,
189
+ ...(undeclared.size === 0 ? {} : { undeclared: [...undeclared].sort() }),
190
+ ...(model.narrative === undefined ? {} : { narrative: model.narrative }),
191
+ }
192
+ }
193
+
194
+ function labelOf(element: ProfileStereotype): string {
195
+ return element.label ?? element.id
196
+ }
197
+
198
+ /**
199
+ * How one element is declared, and the alias the rest of the diagram uses.
200
+ *
201
+ * PlantUML puts the label where the name goes, so an id that needs no alias is used
202
+ * directly and one that does gets a generated id plus an `alias` directive. That is the
203
+ * same shape the use case module uses, for the same reason: the round trip has to come
204
+ * back with the id the model wrote.
205
+ */
206
+ function declaredAs(element: ProfileStereotype, taken: Set<string>): { id: string; alias: string } {
207
+ if (isPlainId(element.id) && !taken.has(element.id)) {
208
+ taken.add(element.id)
209
+ return { id: element.id, alias: element.id }
210
+ }
211
+ let index = 1
212
+ let candidate = 'E' + String(index)
213
+ while (taken.has(candidate)) { index += 1; candidate = 'E' + String(index) }
214
+ taken.add(candidate)
215
+ return { id: element.id, alias: candidate }
216
+ }
217
+
218
+ // ---------------------------------------------------------------------------
219
+ // Rendering
220
+ // ---------------------------------------------------------------------------
221
+
222
+ /**
223
+ * Render a profile as PlantUML.
224
+ *
225
+ * The drawing is the approximation the family table declares: stereotypes and
226
+ * metaclasses as the class diagram draws them, extensions and generalizations both as a
227
+ * `--|>` because the notation has no second arrow for the first. Everything the drawing
228
+ * cannot carry — which arrow is which, the extension multiplicity and role, the
229
+ * applications — is written as a directive, so the text is a faithful record of the
230
+ * model even though the picture is not.
231
+ *
232
+ * @param input - candidate `ProfileModelV1`.
233
+ * @param notation - only `plantuml` draws a profile; mermaid has no view of this family.
234
+ */
235
+ export function renderProfile(input: unknown, notation: UmlNotation): UmlFamilyRender {
236
+ if (notation !== 'plantuml') {
237
+ throw new UmlError('a profile diagram can only be drawn in plantuml; mermaid has no profile diagram: it has no metaclass or stereotype elements. Render it with notation "plantuml".', 'UML_VIEW_UNREPRESENTABLE')
238
+ }
239
+ const model = validateProfileModel(input)
240
+ const warnings: string[] = [
241
+ 'UML_RENDER_APPROXIMATE_VIEW: plantuml has no profile element, so the profile package itself is not drawn; stereotypes and metaclasses are drawn as a class diagram draws them, and an extension and a generalization are both a --|> arrow.',
242
+ ]
243
+ const lines: string[] = ['@startuml']
244
+ lines.push(directiveLine('plantuml', 'uml v1 notation=plantuml family=profile'))
245
+ lines.push(directiveLine('plantuml', 'profile ' + model.profile.id))
246
+ const taken = new Set<string>()
247
+ const aliasOf = new Map<string, string>()
248
+ for (const metaclass of model.metaclasses) {
249
+ const declared = declaredAs(metaclass, taken)
250
+ aliasOf.set(metaclass.id, declared.alias)
251
+ if (declared.alias !== metaclass.id) lines.push(directiveLine('plantuml', 'alias ' + declared.alias + ' ' + metaclass.id))
252
+ lines.push('metaclass ' + quote(labelOf(metaclass)) + ' as ' + declared.alias)
253
+ }
254
+ for (const stereotype of model.stereotypes) {
255
+ const declared = declaredAs(stereotype, taken)
256
+ aliasOf.set(stereotype.id, declared.alias)
257
+ if (declared.alias !== stereotype.id) lines.push(directiveLine('plantuml', 'alias ' + declared.alias + ' ' + stereotype.id))
258
+ lines.push('class ' + quote(labelOf(stereotype)) + ' <<stereotype>> as ' + declared.alias)
259
+ }
260
+ // The directives come before the arrows: they are what tells the arrows apart on the
261
+ // way back, and a reader of the text sees the legend before the drawing.
262
+ for (const extension of model.extensions) {
263
+ lines.push(directiveLine('plantuml', 'profileExtension ' + extension.stereotype + ' -> ' + extension.metaclass +
264
+ (extension.multiplicity === undefined ? '' : ' ' + extension.multiplicity) +
265
+ (extension.label === undefined ? '' : ' ' + extension.label)))
266
+ }
267
+ for (const application of model.applications) {
268
+ lines.push(directiveLine('plantuml', 'profileApply ' + application.profile + ' -> ' + application.applyTo))
269
+ }
270
+ for (const extension of model.extensions) {
271
+ lines.push((aliasOf.get(extension.stereotype) ?? extension.stereotype) + ' --|> ' + (aliasOf.get(extension.metaclass) ?? extension.metaclass))
272
+ }
273
+ for (const generalization of model.generalizations) {
274
+ lines.push((aliasOf.get(generalization.specific) ?? generalization.specific) + ' --|> ' + (aliasOf.get(generalization.general) ?? generalization.general))
275
+ }
276
+ lines.push('@enduml')
277
+ return { primary: lines.join('\n') + '\n', warnings }
278
+ }
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // Review
282
+ // ---------------------------------------------------------------------------
283
+
284
+ export type ProfileFindingCode =
285
+ | 'UML050_PROFILE_NO_STEREOTYPES'
286
+ | 'UML051_PROFILE_UNUSED_STEREOTYPE'
287
+ | 'UML052_PROFILE_EXTENSION_MISSING'
288
+ | 'UML053_PROFILE_GENERALIZATION_CYCLE'
289
+ | 'UML054_PROFILE_UNDECLARED_REFERENCE'
290
+ | 'UML055_PROFILE_APPLICATION_MISSING'
291
+ | 'UML056_PROFILE_UNDOCUMENTED'
292
+
293
+ export const PROFILE_FINDING_CODES: readonly ProfileFindingCode[] = [
294
+ 'UML050_PROFILE_NO_STEREOTYPES',
295
+ 'UML051_PROFILE_UNUSED_STEREOTYPE',
296
+ 'UML052_PROFILE_EXTENSION_MISSING',
297
+ 'UML053_PROFILE_GENERALIZATION_CYCLE',
298
+ 'UML054_PROFILE_UNDECLARED_REFERENCE',
299
+ 'UML055_PROFILE_APPLICATION_MISSING',
300
+ 'UML056_PROFILE_UNDOCUMENTED',
301
+ ]
302
+
303
+ /**
304
+ * What a reader would get wrong about a profile.
305
+ *
306
+ * The checks read the model alone. Whether a profile is *right* for the metamodel it
307
+ * extends is a question about the specification being profiled, and this does not answer
308
+ * it: the codes below are about the profile being internally complete and readable.
309
+ */
310
+ export function reviewProfileModel(model: ProfileModelV1, labels: Record<string, string>): UmlFamilyFinding[] {
311
+ const findings: UmlFamilyFinding[] = []
312
+ const name = (id: string): string => labels[id] ?? id
313
+
314
+ if (model.stereotypes.length === 0) {
315
+ findings.push({
316
+ code: 'UML050_PROFILE_NO_STEREOTYPES',
317
+ severity: 'error',
318
+ message: 'the profile declares no stereotype, so it extends the metamodel with nothing.',
319
+ evidence: { stereotypes: 0 },
320
+ })
321
+ }
322
+
323
+ const extended = new Set(model.extensions.map((extension) => extension.stereotype))
324
+ const specialized = new Set(model.generalizations.flatMap((generalization) => [generalization.general, generalization.specific]))
325
+ for (const stereotype of model.stereotypes) {
326
+ if (extended.has(stereotype.id) || specialized.has(stereotype.id)) continue
327
+ findings.push({
328
+ code: 'UML051_PROFILE_UNUSED_STEREOTYPE',
329
+ severity: 'warning',
330
+ message: 'stereotype ' + name(stereotype.id) + ' extends no metaclass and specialises no other stereotype: nothing in the profile uses it.',
331
+ evidence: { id: stereotype.id },
332
+ })
333
+ }
334
+
335
+ for (const metaclass of model.metaclasses) {
336
+ if (model.extensions.some((extension) => extension.metaclass === metaclass.id)) continue
337
+ findings.push({
338
+ code: 'UML052_PROFILE_EXTENSION_MISSING',
339
+ severity: 'warning',
340
+ message: 'metaclass ' + name(metaclass.id) + ' is declared but no stereotype extends it, so the profile does not change it.',
341
+ evidence: { id: metaclass.id },
342
+ })
343
+ }
344
+
345
+ // A stereotype hierarchy must be acyclic: the specification requires it, and a cycle
346
+ // means no reader can tell which stereotype is the general one.
347
+ const adjacency = new Map<string, string[]>()
348
+ for (const generalization of model.generalizations) {
349
+ const list = adjacency.get(generalization.specific)
350
+ if (list === undefined) adjacency.set(generalization.specific, [generalization.general])
351
+ else list.push(generalization.general)
352
+ }
353
+ const state = new Map<string, 'visiting' | 'done'>()
354
+ const path: string[] = []
355
+ const walk = (node: string): string[] | undefined => {
356
+ const seen = state.get(node)
357
+ if (seen === 'visiting') return [...path.slice(path.indexOf(node)), node]
358
+ if (seen === 'done') return undefined
359
+ state.set(node, 'visiting')
360
+ path.push(node)
361
+ for (const next of adjacency.get(node) ?? []) {
362
+ const cycle = walk(next)
363
+ if (cycle !== undefined) return cycle
364
+ }
365
+ path.pop()
366
+ state.set(node, 'done')
367
+ return undefined
368
+ }
369
+ for (const node of adjacency.keys()) {
370
+ const cycle = walk(node)
371
+ if (cycle === undefined) continue
372
+ findings.push({
373
+ code: 'UML053_PROFILE_GENERALIZATION_CYCLE',
374
+ severity: 'error',
375
+ message: 'the stereotype hierarchy is cyclic, so which stereotype is the general one has no answer.',
376
+ detail: cycle.map((id) => name(id)).join(' -> '),
377
+ evidence: { path: cycle },
378
+ })
379
+ break
380
+ }
381
+
382
+ const undeclared = model.undeclared ?? []
383
+ if (undeclared.length > 0) {
384
+ findings.push({
385
+ code: 'UML054_PROFILE_UNDECLARED_REFERENCE',
386
+ severity: 'error',
387
+ message: 'the profile refers to ' + String(undeclared.length) + ' element(s) it never declares, so the arrow points at nothing a reader can look up.',
388
+ detail: undeclared.join(', '),
389
+ evidence: { ids: undeclared },
390
+ })
391
+ }
392
+
393
+ if (model.applications.length === 0) {
394
+ findings.push({
395
+ code: 'UML055_PROFILE_APPLICATION_MISSING',
396
+ severity: 'info',
397
+ message: 'the profile is not applied to any package: nothing says where it is meant to be used.',
398
+ evidence: { applications: 0 },
399
+ })
400
+ }
401
+
402
+ if (model.profile.label === undefined && labels[model.profile.id] === undefined) {
403
+ findings.push({
404
+ code: 'UML056_PROFILE_UNDOCUMENTED',
405
+ severity: 'info',
406
+ message: 'the profile itself carries no name a reader can map back to the specification it extends: give it a display label or a narrative entry.',
407
+ evidence: { id: model.profile.id },
408
+ })
409
+ }
410
+ for (const stereotype of model.stereotypes) {
411
+ if (stereotype.label !== undefined || labels[stereotype.id] !== undefined) continue
412
+ findings.push({
413
+ code: 'UML056_PROFILE_UNDOCUMENTED',
414
+ severity: 'info',
415
+ message: 'stereotype ' + stereotype.id + ' carries no name a reader can map back to the specification: give it a display label or a narrative entry.',
416
+ evidence: { id: stereotype.id },
417
+ })
418
+ }
419
+ return findings
420
+ }
421
+
422
+ /**
423
+ * The profile family, as the engine's registry expects it.
424
+ *
425
+ * `parse` refuses with the family table's own words. It has to exist because the module
426
+ * contract requires it, and it must refuse rather than return something: a parse would
427
+ * have to decide which `--|>` is an extension and which is a generalization, and the
428
+ * notation does not say.
429
+ */
430
+ export const profileModule: UmlFamilyModule<ProfileModelV1> = {
431
+ id: 'profile',
432
+ render: (input: unknown, notation: UmlNotation, _options: UmlFamilyRenderOptions): UmlFamilyRender => renderProfile(input, notation),
433
+ parse: (): UmlFamilyParse<ProfileModelV1> => {
434
+ throw new UmlError(
435
+ 'a profile diagram cannot be read back out of plantuml: it has no profile element, and an extension and a stereotype generalization are both a --|> arrow, so a parse would have to guess which fact the arrow carries. Keep the profile as its own model and review that.',
436
+ 'UML_VIEW_UNREPRESENTABLE')
437
+ },
438
+ review: (model: ProfileModelV1, labels: Record<string, string>): UmlFamilyFinding[] => reviewProfileModel(model, labels),
439
+ }