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.
- package/LICENSE +21 -0
- package/README.en-US.md +332 -0
- package/README.md +332 -3
- package/cordis.patch.yml +20 -0
- package/examples/README.md +48 -0
- package/examples/control-dispatcher.json +113 -0
- package/examples/control-dispatcher.overbudget.json +113 -0
- package/examples/ecommerce-data-model.json +185 -0
- package/examples/order-state-machine.after.json +116 -0
- package/examples/order-state-machine.before.json +101 -0
- package/examples/user-migration.after.json +29 -0
- package/examples/user-migration.before.json +29 -0
- package/gate/locale/en.json +6 -0
- package/gate/locale/zh.json +6 -0
- package/gate/package.json +5 -0
- package/lib/baseline.js +249 -0
- package/lib/compose-tool.js +80 -0
- package/lib/concurrency-tool.js +40 -0
- package/lib/concurrency.js +206 -0
- package/lib/data-engine.js +1141 -0
- package/lib/data-tool.js +87 -0
- package/lib/design-engine.js +1149 -0
- package/lib/design-tool.js +63 -0
- package/lib/engine.js +4343 -0
- package/lib/export-tool.js +78 -0
- package/lib/exporters.js +434 -0
- package/lib/gate.js +274 -0
- package/lib/granularity.js +237 -0
- package/lib/index.js +237 -0
- package/lib/json-value.js +18 -0
- package/lib/model-file.js +48 -0
- package/lib/option-shape.js +62 -0
- package/lib/report-diff-tool.js +63 -0
- package/lib/report-view.js +433 -0
- package/lib/structure-tool.js +91 -0
- package/lib/structure.js +711 -0
- package/lib/tool.js +94 -0
- package/lib/types/baseline.d.ts +65 -0
- package/lib/types/compose-tool.d.ts +7 -0
- package/lib/types/concurrency-tool.d.ts +8 -0
- package/lib/types/concurrency.d.ts +37 -0
- package/lib/types/data-engine.d.ts +211 -0
- package/lib/types/data-tool.d.ts +10 -0
- package/lib/types/design-engine.d.ts +215 -0
- package/lib/types/design-tool.d.ts +11 -0
- package/lib/types/engine.d.ts +494 -0
- package/lib/types/export-tool.d.ts +7 -0
- package/lib/types/exporters.d.ts +37 -0
- package/lib/types/gate.d.ts +91 -0
- package/lib/types/granularity.d.ts +106 -0
- package/lib/types/index.d.ts +45 -0
- package/lib/types/json-value.d.ts +21 -0
- package/lib/types/model-file.d.ts +7 -0
- package/lib/types/option-shape.d.ts +22 -0
- package/lib/types/report-diff-tool.d.ts +8 -0
- package/lib/types/report-view.d.ts +50 -0
- package/lib/types/structure-tool.d.ts +10 -0
- package/lib/types/structure.d.ts +152 -0
- package/lib/types/tool.d.ts +11 -0
- package/lib/types/uml-communication.d.ts +109 -0
- package/lib/types/uml-composite.d.ts +107 -0
- package/lib/types/uml-interaction-overview.d.ts +91 -0
- package/lib/types/uml-profile.d.ts +112 -0
- package/lib/types/uml-structure.d.ts +53 -0
- package/lib/types/uml-syntax.d.ts +296 -0
- package/lib/types/uml-timing.d.ts +152 -0
- package/lib/types/uml-tool.d.ts +12 -0
- package/lib/types/uml-usecase.d.ts +58 -0
- package/lib/types/uml.d.ts +303 -0
- package/lib/uml-communication.js +353 -0
- package/lib/uml-composite.js +442 -0
- package/lib/uml-interaction-overview.js +277 -0
- package/lib/uml-profile.js +373 -0
- package/lib/uml-structure.js +306 -0
- package/lib/uml-syntax.js +545 -0
- package/lib/uml-timing.js +974 -0
- package/lib/uml-tool.js +213 -0
- package/lib/uml-usecase.js +532 -0
- package/lib/uml.js +2110 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +128 -6
- package/skills/model-everything/SKILL.md +450 -0
- package/skills/model-everything/references/dsh-model-schema.md +437 -0
- package/skills/model-everything/references/gap-routing-guide.md +42 -0
- package/skills/model-everything/references/hash-spec.md +152 -0
- package/skills/model-everything/references/logic-verification-guide.md +422 -0
- package/skills/model-everything-datamodel/SKILL.md +130 -0
- package/skills/model-everything-datamodel/references/data-model-guide.md +84 -0
- package/skills/model-everything-datamodel/references/data-model-schema.md +128 -0
- package/skills/model-everything-design/SKILL.md +115 -0
- package/skills/model-everything-interleaving/SKILL.md +63 -0
- package/skills/model-everything-interleaving/SKILL.zh-CN.md +60 -0
- package/skills/model-everything-interleaving/references/concurrency-risk-guide.md +63 -0
- package/skills/model-everything-report-reading/SKILL.md +137 -0
- package/skills/model-everything-report-reading/references/reading-a-report.md +166 -0
- package/skills/model-everything-report-reading/references/reading-a-report.mmd +59 -0
- package/skills/model-everything-structure/SKILL.md +96 -0
- package/skills/model-everything-structure/references/structure-review-guide.md +201 -0
- package/skills/model-everything-uml/SKILL.md +60 -0
- package/skills/model-everything-uml/references/uml-modeling-guide.md +551 -0
- package/src/baseline.ts +288 -0
- package/src/compose-tool.ts +82 -0
- package/src/concurrency-tool.ts +44 -0
- package/src/concurrency.ts +254 -0
- package/src/data-engine.ts +1208 -0
- package/src/data-tool.ts +91 -0
- package/src/design-engine.ts +1263 -0
- package/src/design-tool.ts +68 -0
- package/src/engine.ts +4564 -0
- package/src/export-tool.ts +81 -0
- package/src/exporters.ts +399 -0
- package/src/gate.ts +330 -0
- package/src/granularity.ts +312 -0
- package/src/index.ts +289 -0
- package/src/json-value.ts +20 -0
- package/src/model-file.ts +49 -0
- package/src/option-shape.ts +64 -0
- package/src/report-diff-tool.ts +67 -0
- package/src/report-view.ts +483 -0
- package/src/structure-tool.ts +93 -0
- package/src/structure.ts +786 -0
- package/src/tool.ts +103 -0
- package/src/uml-communication.ts +428 -0
- package/src/uml-composite.ts +520 -0
- package/src/uml-interaction-overview.ts +321 -0
- package/src/uml-profile.ts +439 -0
- package/src/uml-structure.ts +317 -0
- package/src/uml-syntax.ts +764 -0
- package/src/uml-timing.ts +1106 -0
- package/src/uml-tool.ts +230 -0
- package/src/uml-usecase.ts +566 -0
- package/src/uml.ts +2139 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interaction overview diagram module.
|
|
3
|
+
*
|
|
4
|
+
* An interaction overview is a control-flow graph whose nodes are interactions: a start, a
|
|
5
|
+
* decision, one or more references to interactions, and a stop. PlantUML has no page for it
|
|
6
|
+
* and no dedicated type, but the drawing is expressible with the activity diagram's
|
|
7
|
+
* vocabulary plus a reference node - `start`, `:ref Login sequence;`, `if (ok?) then (yes)`
|
|
8
|
+
* - which is what the notation research in this repository recorded as the manual form.
|
|
9
|
+
*
|
|
10
|
+
* So this family is drawn approximately, like the composite structure and the
|
|
11
|
+
* communication diagram, and for the same reason the family table used to refuse it on
|
|
12
|
+
* grounds that were simply wrong ("neither notation spells the inline interaction
|
|
13
|
+
* references": it does).
|
|
14
|
+
*
|
|
15
|
+
* Reading it back is partial, and the parser refuses the part it cannot do. A sequence and
|
|
16
|
+
* an `if`/`else` are spelled clearly enough to reconstruct. A `fork` is not: the notation
|
|
17
|
+
* opens a `fork` block, lists the flows, and closes it with `end fork`, and the closing
|
|
18
|
+
* marker carries no label tying it to the opening one, so nested or adjacent forks cannot
|
|
19
|
+
* be told apart. Rather than guess, a diagram containing one is refused with that reason.
|
|
20
|
+
*
|
|
21
|
+
* @module model-everything-uml-interaction-overview
|
|
22
|
+
*/
|
|
23
|
+
import { UmlError, directiveLine, } from './uml-syntax.js';
|
|
24
|
+
const NODE_KINDS = ['initial', 'final', 'decision', 'merge', 'interaction', 'reference'];
|
|
25
|
+
/**
|
|
26
|
+
* Validate a candidate model and normalize it.
|
|
27
|
+
*
|
|
28
|
+
* One initial node, and every node reachable from it: an overview whose flow starts nowhere
|
|
29
|
+
* is a drawing, not a flow. That is a refusal rather than a finding because every later
|
|
30
|
+
* check would be reading a graph with no entry.
|
|
31
|
+
*/
|
|
32
|
+
export function validateInteractionOverviewModel(input) {
|
|
33
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
34
|
+
throw new UmlError('an interaction overview model must be an object with schemaVersion 1, nodes and edges', 'UML_INPUT');
|
|
35
|
+
}
|
|
36
|
+
const model = input;
|
|
37
|
+
if (model.schemaVersion !== 1)
|
|
38
|
+
throw new UmlError('an interaction overview model must declare schemaVersion 1', 'UML_INPUT');
|
|
39
|
+
for (const [name, value] of Object.entries({ nodes: model.nodes ?? [], edges: model.edges ?? [] })) {
|
|
40
|
+
if (!Array.isArray(value))
|
|
41
|
+
throw new UmlError(name + ' must be an array', 'UML_INPUT');
|
|
42
|
+
}
|
|
43
|
+
const nodes = [];
|
|
44
|
+
const known = new Set();
|
|
45
|
+
for (const node of model.nodes ?? []) {
|
|
46
|
+
if (node === null || typeof node !== 'object' || typeof node.id !== 'string' || node.id.trim() === '') {
|
|
47
|
+
throw new UmlError('every node needs a non-empty string id', 'UML_INPUT');
|
|
48
|
+
}
|
|
49
|
+
if (known.has(node.id))
|
|
50
|
+
throw new UmlError('node ' + JSON.stringify(node.id) + ' is declared more than once', 'UML_INPUT');
|
|
51
|
+
known.add(node.id);
|
|
52
|
+
const kind = node.kind ?? 'interaction';
|
|
53
|
+
if (!NODE_KINDS.includes(kind)) {
|
|
54
|
+
throw new UmlError('node kind must be one of ' + NODE_KINDS.join(' | ') + ', got ' + JSON.stringify(String(kind)), 'UML_INPUT');
|
|
55
|
+
}
|
|
56
|
+
if ((kind === 'interaction' || kind === 'reference') && (typeof node.interaction !== 'string' || node.interaction.trim() === '')) {
|
|
57
|
+
throw new UmlError('a ' + kind + ' node needs a non-empty `interaction` name', 'UML_INPUT');
|
|
58
|
+
}
|
|
59
|
+
nodes.push({
|
|
60
|
+
id: node.id,
|
|
61
|
+
kind,
|
|
62
|
+
...(node.interaction === undefined ? {} : { interaction: node.interaction }),
|
|
63
|
+
...(node.label === undefined ? {} : { label: node.label }),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const initial = nodes.filter((node) => node.kind === 'initial');
|
|
67
|
+
if (initial.length !== 1) {
|
|
68
|
+
throw new UmlError('an interaction overview needs exactly one initial node, got ' + String(initial.length), 'UML_INPUT');
|
|
69
|
+
}
|
|
70
|
+
const undeclared = new Set();
|
|
71
|
+
const edges = [];
|
|
72
|
+
for (const edge of model.edges ?? []) {
|
|
73
|
+
if (edge === null || typeof edge !== 'object')
|
|
74
|
+
throw new UmlError('every edge must be an object', 'UML_INPUT');
|
|
75
|
+
for (const side of ['from', 'to']) {
|
|
76
|
+
const endpoint = edge[side];
|
|
77
|
+
if (typeof endpoint !== 'string' || endpoint.trim() === '')
|
|
78
|
+
throw new UmlError('every edge needs a non-empty `' + side + '` end', 'UML_INPUT');
|
|
79
|
+
if (!known.has(endpoint))
|
|
80
|
+
undeclared.add(endpoint);
|
|
81
|
+
}
|
|
82
|
+
edges.push({
|
|
83
|
+
from: edge.from,
|
|
84
|
+
to: edge.to,
|
|
85
|
+
...(edge.condition === undefined ? {} : { condition: edge.condition }),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
schemaVersion: 1,
|
|
90
|
+
nodes,
|
|
91
|
+
edges,
|
|
92
|
+
...(undeclared.size === 0 ? {} : { undeclared: [...undeclared].sort() }),
|
|
93
|
+
...(model.narrative === undefined ? {} : { narrative: model.narrative }),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function quote(text) {
|
|
97
|
+
return '"' + text.replace(/"/g, "'") + '"';
|
|
98
|
+
}
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Rendering
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Render an interaction overview as Mermaid.
|
|
104
|
+
*
|
|
105
|
+
* Mermaid is the notation this family is drawn in, because the view is an activity graph
|
|
106
|
+
* and PlantUML's activity syntax is refused by this build (a structured flowchart language
|
|
107
|
+
* cannot carry a graph with merges or cycles without a while/if reconstruction). A Mermaid
|
|
108
|
+
* flowchart carries exactly what an overview is: nodes, labelled branches, and a start and
|
|
109
|
+
* an end.
|
|
110
|
+
*
|
|
111
|
+
* @param input - candidate `InteractionOverviewModelV1`.
|
|
112
|
+
* @param notation - only `mermaid` draws this family.
|
|
113
|
+
*/
|
|
114
|
+
export function renderInteractionOverview(input, notation) {
|
|
115
|
+
if (notation !== 'mermaid') {
|
|
116
|
+
throw new UmlError('an interaction overview can only be drawn in mermaid; plantuml has no interaction reference node, and its activity syntax - the only place a `ref` step exists - is refused by this build because a structured flowchart language cannot carry a graph with merges or cycles. Render it with notation "mermaid".', 'UML_VIEW_UNREPRESENTABLE');
|
|
117
|
+
}
|
|
118
|
+
const model = validateInteractionOverviewModel(input);
|
|
119
|
+
const warnings = [
|
|
120
|
+
'UML_RENDER_APPROXIMATE_VIEW: mermaid has no interaction overview diagram; this is a flowchart whose nodes name interactions. The flow, the references and the branch conditions are carried; the combined fragments of an inline interaction are not drawn, because the drawing has one node for it.',
|
|
121
|
+
];
|
|
122
|
+
const lines = ['flowchart TD'];
|
|
123
|
+
lines.push(directiveLine('mermaid', 'uml v1 notation=mermaid family=interaction-overview'));
|
|
124
|
+
const alias = (id) => id.replace(/[^A-Za-z0-9_]/g, '_');
|
|
125
|
+
for (const node of model.nodes) {
|
|
126
|
+
const a = alias(node.id);
|
|
127
|
+
if (node.kind === 'initial') {
|
|
128
|
+
lines.push(' ' + a + '(["start"])');
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (node.kind === 'final') {
|
|
132
|
+
lines.push(' ' + a + '(["stop"])');
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (node.kind === 'decision') {
|
|
136
|
+
lines.push(' ' + a + '{"' + (node.label ?? 'branch?').replace(/"/g, "'") + '"}');
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (node.kind === 'merge') {
|
|
140
|
+
lines.push(' ' + a + '["merge"]');
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const name = (node.interaction ?? node.label ?? node.id).replace(/"/g, "'");
|
|
144
|
+
lines.push(' ' + a + (node.kind === 'reference' ? '[/"' + name + '"/]' : '["' + name + '"]'));
|
|
145
|
+
}
|
|
146
|
+
for (const edge of model.edges) {
|
|
147
|
+
const label = edge.condition === undefined || edge.condition === '' ? '' : '|"' + edge.condition.replace(/[|"]/g, "'") + '"|';
|
|
148
|
+
lines.push(' ' + alias(edge.from) + ' -->' + label + ' ' + alias(edge.to));
|
|
149
|
+
}
|
|
150
|
+
return { primary: lines.join('\n') + '\n', warnings };
|
|
151
|
+
}
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// Parsing
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Review
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
export function parseInteractionOverview(_text, _notation) {
|
|
159
|
+
throw new UmlError('an interaction overview is drawn here as a mermaid flowchart, and a flowchart node carries a name rather than the interaction it stands for: reading one back would have to guess whether a node is a reference to another diagram, an inline interaction, or a plain step. Keep the overview as its own model and review that.', 'UML_VIEW_UNREPRESENTABLE');
|
|
160
|
+
}
|
|
161
|
+
export const INTERACTION_OVERVIEW_FINDING_CODES = [
|
|
162
|
+
'UML069_OVERVIEW_NO_INITIAL',
|
|
163
|
+
'UML070_OVERVIEW_NODE_UNREACHABLE',
|
|
164
|
+
'UML071_OVERVIEW_DECISION_ONE_BRANCH',
|
|
165
|
+
'UML072_OVERVIEW_REFERENCE_UNDECLARED',
|
|
166
|
+
'UML073_OVERVIEW_NODE_WITHOUT_INTERACTION',
|
|
167
|
+
'UML074_OVERVIEW_UNDOCUMENTED',
|
|
168
|
+
];
|
|
169
|
+
/**
|
|
170
|
+
* What a reader would get wrong about an interaction overview.
|
|
171
|
+
*
|
|
172
|
+
* The overview's job is to say how the interactions fit together, so the checks are about
|
|
173
|
+
* the flow being a flow: one way in, every node on some path from it, a decision that
|
|
174
|
+
* actually decides. Whether the referenced interactions exist is a question about the other
|
|
175
|
+
* diagrams - this plugin holds no registry of them - so an undeclared reference is reported
|
|
176
|
+
* as a name nothing here can resolve, not as a missing interaction.
|
|
177
|
+
*/
|
|
178
|
+
export function reviewInteractionOverviewModel(model, labels) {
|
|
179
|
+
const findings = [];
|
|
180
|
+
const name = (id) => labels[id] ?? id;
|
|
181
|
+
const byId = new Map(model.nodes.map((node) => [node.id, node]));
|
|
182
|
+
const initial = model.nodes.filter((node) => node.kind === 'initial');
|
|
183
|
+
if (initial.length === 0) {
|
|
184
|
+
findings.push({
|
|
185
|
+
code: 'UML069_OVERVIEW_NO_INITIAL',
|
|
186
|
+
severity: 'error',
|
|
187
|
+
message: 'the overview has no initial node, so the flow has no entry and nothing below it can be read as a sequence.',
|
|
188
|
+
evidence: { initial: 0 },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const outgoing = new Map();
|
|
192
|
+
for (const edge of model.edges) {
|
|
193
|
+
const list = outgoing.get(edge.from);
|
|
194
|
+
if (list === undefined)
|
|
195
|
+
outgoing.set(edge.from, [edge.to]);
|
|
196
|
+
else
|
|
197
|
+
list.push(edge.to);
|
|
198
|
+
}
|
|
199
|
+
const reached = new Set();
|
|
200
|
+
const queue = initial.map((node) => node.id);
|
|
201
|
+
while (queue.length > 0) {
|
|
202
|
+
const id = queue.shift();
|
|
203
|
+
if (reached.has(id))
|
|
204
|
+
continue;
|
|
205
|
+
reached.add(id);
|
|
206
|
+
for (const next of outgoing.get(id) ?? [])
|
|
207
|
+
queue.push(next);
|
|
208
|
+
}
|
|
209
|
+
for (const node of model.nodes) {
|
|
210
|
+
if (reached.has(node.id))
|
|
211
|
+
continue;
|
|
212
|
+
findings.push({
|
|
213
|
+
code: 'UML070_OVERVIEW_NODE_UNREACHABLE',
|
|
214
|
+
severity: 'error',
|
|
215
|
+
message: 'node ' + name(node.id) + ' is on no path from the initial node, so the overview does not run it.',
|
|
216
|
+
evidence: { id: node.id, kind: node.kind },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
for (const node of model.nodes) {
|
|
220
|
+
if (node.kind !== 'decision')
|
|
221
|
+
continue;
|
|
222
|
+
const branches = outgoing.get(node.id) ?? [];
|
|
223
|
+
if (branches.length > 1)
|
|
224
|
+
continue;
|
|
225
|
+
findings.push({
|
|
226
|
+
code: 'UML071_OVERVIEW_DECISION_ONE_BRANCH',
|
|
227
|
+
severity: 'warning',
|
|
228
|
+
message: 'decision ' + name(node.id) + ' has ' + String(branches.length) + ' outgoing edge(s), so it does not decide anything.',
|
|
229
|
+
evidence: { id: node.id, branches: branches.length },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const undeclared = model.undeclared ?? [];
|
|
233
|
+
if (undeclared.length > 0) {
|
|
234
|
+
findings.push({
|
|
235
|
+
code: 'UML072_OVERVIEW_REFERENCE_UNDECLARED',
|
|
236
|
+
severity: 'error',
|
|
237
|
+
message: 'an edge names ' + String(undeclared.length) + ' node(s) the overview never declares. A reference whose target lives in another diagram cannot be resolved here at all.',
|
|
238
|
+
detail: undeclared.join(', '),
|
|
239
|
+
evidence: { ids: undeclared },
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
for (const node of model.nodes) {
|
|
243
|
+
if (node.kind !== 'interaction' && node.kind !== 'reference')
|
|
244
|
+
continue;
|
|
245
|
+
if (typeof node.interaction === 'string' && node.interaction.trim() !== '')
|
|
246
|
+
continue;
|
|
247
|
+
findings.push({
|
|
248
|
+
code: 'UML073_OVERVIEW_NODE_WITHOUT_INTERACTION',
|
|
249
|
+
severity: 'error',
|
|
250
|
+
message: 'node ' + name(node.id) + ' is declared as an ' + node.kind + ' but names no interaction, so the overview does not say what runs there.',
|
|
251
|
+
evidence: { id: node.id, kind: node.kind },
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (model.narrative?.nodes === undefined) {
|
|
255
|
+
for (const node of model.nodes) {
|
|
256
|
+
if (node.label !== undefined || node.interaction !== undefined || labels[node.id] !== undefined)
|
|
257
|
+
continue;
|
|
258
|
+
findings.push({
|
|
259
|
+
code: 'UML074_OVERVIEW_UNDOCUMENTED',
|
|
260
|
+
severity: 'info',
|
|
261
|
+
message: 'node ' + node.id + ' carries no name a reader can map back to the design: give it a label, an interaction or a narrative entry.',
|
|
262
|
+
evidence: { id: node.id },
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return findings;
|
|
267
|
+
}
|
|
268
|
+
/** The interaction overview family, as the engine's registry expects it. */
|
|
269
|
+
export const interactionOverviewModule = {
|
|
270
|
+
id: 'interaction-overview',
|
|
271
|
+
// Mermaid is the only notation this family is drawn in: PlantUML's activity syntax, the
|
|
272
|
+
// one place a `ref` step exists, is refused by this build.
|
|
273
|
+
notation: 'mermaid',
|
|
274
|
+
render: (input, notation, _options) => renderInteractionOverview(input, notation),
|
|
275
|
+
parse: (text, notation) => parseInteractionOverview(text, notation),
|
|
276
|
+
review: (model, labels) => reviewInteractionOverviewModel(model, labels),
|
|
277
|
+
};
|
|
@@ -0,0 +1,373 @@
|
|
|
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
|
+
import { UmlError, UML_PLAIN_ID, directiveLine, } from './uml-syntax.js';
|
|
19
|
+
const MULTIPLICITIES = ['0..1', '1'];
|
|
20
|
+
/** PlantUML ids that cannot stand alone in a declaration. */
|
|
21
|
+
const RESERVED_IDS = new Set(['class', 'metaclass', 'stereotype', 'enum', 'interface', 'abstract', 'package', 'end', 'note', 'hide', 'show', 'skinparam', 'title', 'left', 'right', 'top', 'bottom']);
|
|
22
|
+
function isPlainId(id) {
|
|
23
|
+
return UML_PLAIN_ID.test(id) && !RESERVED_IDS.has(id.toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
function quote(text) {
|
|
26
|
+
return '"' + text.replace(/"/g, "'") + '"';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate a candidate model and normalize it.
|
|
30
|
+
*
|
|
31
|
+
* The contract every family keeps: an undeclared key or a wrong type is a refusal with
|
|
32
|
+
* `UML_INPUT`, never a silently dropped field.
|
|
33
|
+
*/
|
|
34
|
+
export function validateProfileModel(input) {
|
|
35
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
36
|
+
throw new UmlError('a profile model must be an object with schemaVersion 1, a profile, stereotypes, metaclasses and extensions', 'UML_INPUT');
|
|
37
|
+
}
|
|
38
|
+
const model = input;
|
|
39
|
+
if (model.schemaVersion !== 1)
|
|
40
|
+
throw new UmlError('a profile model must declare schemaVersion 1', 'UML_INPUT');
|
|
41
|
+
const profile = model.profile;
|
|
42
|
+
if (profile === null || typeof profile !== 'object' || typeof profile.id !== 'string' || profile.id.trim() === '') {
|
|
43
|
+
throw new UmlError('a profile model needs a `profile` with a non-empty id', 'UML_INPUT');
|
|
44
|
+
}
|
|
45
|
+
const lists = {
|
|
46
|
+
stereotypes: model.stereotypes ?? [],
|
|
47
|
+
metaclasses: model.metaclasses ?? [],
|
|
48
|
+
extensions: model.extensions ?? [],
|
|
49
|
+
applications: model.applications ?? [],
|
|
50
|
+
generalizations: model.generalizations ?? [],
|
|
51
|
+
};
|
|
52
|
+
for (const [name, value] of Object.entries(lists)) {
|
|
53
|
+
if (!Array.isArray(value))
|
|
54
|
+
throw new UmlError(name + ' must be an array', 'UML_INPUT');
|
|
55
|
+
}
|
|
56
|
+
const stereotypeIds = new Set();
|
|
57
|
+
for (const stereotype of lists.stereotypes) {
|
|
58
|
+
if (stereotype === null || typeof stereotype !== 'object' || typeof stereotype.id !== 'string' || stereotype.id.trim() === '') {
|
|
59
|
+
throw new UmlError('every stereotype needs a non-empty string id', 'UML_INPUT');
|
|
60
|
+
}
|
|
61
|
+
if (stereotypeIds.has(stereotype.id))
|
|
62
|
+
throw new UmlError('duplicate stereotype id ' + quote(stereotype.id), 'UML_INPUT');
|
|
63
|
+
stereotypeIds.add(stereotype.id);
|
|
64
|
+
}
|
|
65
|
+
const metaclassIds = new Set();
|
|
66
|
+
for (const metaclass of lists.metaclasses) {
|
|
67
|
+
if (metaclass === null || typeof metaclass !== 'object' || typeof metaclass.id !== 'string' || metaclass.id.trim() === '') {
|
|
68
|
+
throw new UmlError('every metaclass needs a non-empty string id', 'UML_INPUT');
|
|
69
|
+
}
|
|
70
|
+
if (metaclassIds.has(metaclass.id))
|
|
71
|
+
throw new UmlError('duplicate metaclass id ' + quote(metaclass.id), 'UML_INPUT');
|
|
72
|
+
metaclassIds.add(metaclass.id);
|
|
73
|
+
}
|
|
74
|
+
for (const id of [...stereotypeIds]) {
|
|
75
|
+
if (metaclassIds.has(id))
|
|
76
|
+
throw new UmlError(quote(id) + ' is declared as both a stereotype and a metaclass', 'UML_INPUT');
|
|
77
|
+
}
|
|
78
|
+
const undeclared = new Set();
|
|
79
|
+
const extensions = [];
|
|
80
|
+
for (const extension of lists.extensions) {
|
|
81
|
+
if (extension === null || typeof extension !== 'object')
|
|
82
|
+
throw new UmlError('every extension must be an object', 'UML_INPUT');
|
|
83
|
+
if (typeof extension.stereotype !== 'string' || extension.stereotype.trim() === '')
|
|
84
|
+
throw new UmlError('every extension needs the stereotype that extends', 'UML_INPUT');
|
|
85
|
+
if (typeof extension.metaclass !== 'string' || extension.metaclass.trim() === '')
|
|
86
|
+
throw new UmlError('every extension needs the metaclass it extends', 'UML_INPUT');
|
|
87
|
+
if (!stereotypeIds.has(extension.stereotype))
|
|
88
|
+
undeclared.add(extension.stereotype);
|
|
89
|
+
if (!metaclassIds.has(extension.metaclass))
|
|
90
|
+
undeclared.add(extension.metaclass);
|
|
91
|
+
if (extension.multiplicity !== undefined && !MULTIPLICITIES.includes(extension.multiplicity)) {
|
|
92
|
+
throw new UmlError('extension multiplicity must be ' + MULTIPLICITIES.join(' | ') + ', got ' + quote(String(extension.multiplicity)), 'UML_INPUT');
|
|
93
|
+
}
|
|
94
|
+
extensions.push({
|
|
95
|
+
stereotype: extension.stereotype,
|
|
96
|
+
metaclass: extension.metaclass,
|
|
97
|
+
...(extension.multiplicity === undefined ? {} : { multiplicity: extension.multiplicity }),
|
|
98
|
+
...(extension.label === undefined ? {} : { label: extension.label }),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const generalizations = [];
|
|
102
|
+
for (const generalization of lists.generalizations) {
|
|
103
|
+
if (generalization === null || typeof generalization !== 'object')
|
|
104
|
+
throw new UmlError('every generalization must be an object', 'UML_INPUT');
|
|
105
|
+
for (const side of ['general', 'specific']) {
|
|
106
|
+
const id = generalization[side];
|
|
107
|
+
if (typeof id !== 'string' || id.trim() === '')
|
|
108
|
+
throw new UmlError('every generalization needs a non-empty `' + side + '` stereotype id', 'UML_INPUT');
|
|
109
|
+
if (!stereotypeIds.has(id))
|
|
110
|
+
undeclared.add(id);
|
|
111
|
+
}
|
|
112
|
+
if (generalization.general === generalization.specific) {
|
|
113
|
+
throw new UmlError('stereotype ' + quote(generalization.general) + ' cannot generalize itself', 'UML_INPUT');
|
|
114
|
+
}
|
|
115
|
+
generalizations.push({ general: generalization.general, specific: generalization.specific });
|
|
116
|
+
}
|
|
117
|
+
const applications = [];
|
|
118
|
+
for (const application of lists.applications) {
|
|
119
|
+
if (application === null || typeof application !== 'object')
|
|
120
|
+
throw new UmlError('every application must be an object', 'UML_INPUT');
|
|
121
|
+
for (const side of ['profile', 'applyTo']) {
|
|
122
|
+
const value = application[side];
|
|
123
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
124
|
+
throw new UmlError('every application needs a non-empty `' + side + '`', 'UML_INPUT');
|
|
125
|
+
}
|
|
126
|
+
if (application.profile !== profile.id)
|
|
127
|
+
undeclared.add(application.profile);
|
|
128
|
+
applications.push({ profile: application.profile, applyTo: application.applyTo });
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
schemaVersion: 1,
|
|
132
|
+
profile: { id: profile.id, ...(profile.label === undefined ? {} : { label: profile.label }) },
|
|
133
|
+
stereotypes: lists.stereotypes.map((stereotype) => ({ id: stereotype.id, ...(stereotype.label === undefined ? {} : { label: stereotype.label }) })),
|
|
134
|
+
metaclasses: lists.metaclasses.map((metaclass) => ({ id: metaclass.id, ...(metaclass.label === undefined ? {} : { label: metaclass.label }) })),
|
|
135
|
+
extensions,
|
|
136
|
+
applications,
|
|
137
|
+
generalizations,
|
|
138
|
+
...(undeclared.size === 0 ? {} : { undeclared: [...undeclared].sort() }),
|
|
139
|
+
...(model.narrative === undefined ? {} : { narrative: model.narrative }),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function labelOf(element) {
|
|
143
|
+
return element.label ?? element.id;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* How one element is declared, and the alias the rest of the diagram uses.
|
|
147
|
+
*
|
|
148
|
+
* PlantUML puts the label where the name goes, so an id that needs no alias is used
|
|
149
|
+
* directly and one that does gets a generated id plus an `alias` directive. That is the
|
|
150
|
+
* same shape the use case module uses, for the same reason: the round trip has to come
|
|
151
|
+
* back with the id the model wrote.
|
|
152
|
+
*/
|
|
153
|
+
function declaredAs(element, taken) {
|
|
154
|
+
if (isPlainId(element.id) && !taken.has(element.id)) {
|
|
155
|
+
taken.add(element.id);
|
|
156
|
+
return { id: element.id, alias: element.id };
|
|
157
|
+
}
|
|
158
|
+
let index = 1;
|
|
159
|
+
let candidate = 'E' + String(index);
|
|
160
|
+
while (taken.has(candidate)) {
|
|
161
|
+
index += 1;
|
|
162
|
+
candidate = 'E' + String(index);
|
|
163
|
+
}
|
|
164
|
+
taken.add(candidate);
|
|
165
|
+
return { id: element.id, alias: candidate };
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Rendering
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Render a profile as PlantUML.
|
|
172
|
+
*
|
|
173
|
+
* The drawing is the approximation the family table declares: stereotypes and
|
|
174
|
+
* metaclasses as the class diagram draws them, extensions and generalizations both as a
|
|
175
|
+
* `--|>` because the notation has no second arrow for the first. Everything the drawing
|
|
176
|
+
* cannot carry — which arrow is which, the extension multiplicity and role, the
|
|
177
|
+
* applications — is written as a directive, so the text is a faithful record of the
|
|
178
|
+
* model even though the picture is not.
|
|
179
|
+
*
|
|
180
|
+
* @param input - candidate `ProfileModelV1`.
|
|
181
|
+
* @param notation - only `plantuml` draws a profile; mermaid has no view of this family.
|
|
182
|
+
*/
|
|
183
|
+
export function renderProfile(input, notation) {
|
|
184
|
+
if (notation !== 'plantuml') {
|
|
185
|
+
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');
|
|
186
|
+
}
|
|
187
|
+
const model = validateProfileModel(input);
|
|
188
|
+
const warnings = [
|
|
189
|
+
'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.',
|
|
190
|
+
];
|
|
191
|
+
const lines = ['@startuml'];
|
|
192
|
+
lines.push(directiveLine('plantuml', 'uml v1 notation=plantuml family=profile'));
|
|
193
|
+
lines.push(directiveLine('plantuml', 'profile ' + model.profile.id));
|
|
194
|
+
const taken = new Set();
|
|
195
|
+
const aliasOf = new Map();
|
|
196
|
+
for (const metaclass of model.metaclasses) {
|
|
197
|
+
const declared = declaredAs(metaclass, taken);
|
|
198
|
+
aliasOf.set(metaclass.id, declared.alias);
|
|
199
|
+
if (declared.alias !== metaclass.id)
|
|
200
|
+
lines.push(directiveLine('plantuml', 'alias ' + declared.alias + ' ' + metaclass.id));
|
|
201
|
+
lines.push('metaclass ' + quote(labelOf(metaclass)) + ' as ' + declared.alias);
|
|
202
|
+
}
|
|
203
|
+
for (const stereotype of model.stereotypes) {
|
|
204
|
+
const declared = declaredAs(stereotype, taken);
|
|
205
|
+
aliasOf.set(stereotype.id, declared.alias);
|
|
206
|
+
if (declared.alias !== stereotype.id)
|
|
207
|
+
lines.push(directiveLine('plantuml', 'alias ' + declared.alias + ' ' + stereotype.id));
|
|
208
|
+
lines.push('class ' + quote(labelOf(stereotype)) + ' <<stereotype>> as ' + declared.alias);
|
|
209
|
+
}
|
|
210
|
+
// The directives come before the arrows: they are what tells the arrows apart on the
|
|
211
|
+
// way back, and a reader of the text sees the legend before the drawing.
|
|
212
|
+
for (const extension of model.extensions) {
|
|
213
|
+
lines.push(directiveLine('plantuml', 'profileExtension ' + extension.stereotype + ' -> ' + extension.metaclass +
|
|
214
|
+
(extension.multiplicity === undefined ? '' : ' ' + extension.multiplicity) +
|
|
215
|
+
(extension.label === undefined ? '' : ' ' + extension.label)));
|
|
216
|
+
}
|
|
217
|
+
for (const application of model.applications) {
|
|
218
|
+
lines.push(directiveLine('plantuml', 'profileApply ' + application.profile + ' -> ' + application.applyTo));
|
|
219
|
+
}
|
|
220
|
+
for (const extension of model.extensions) {
|
|
221
|
+
lines.push((aliasOf.get(extension.stereotype) ?? extension.stereotype) + ' --|> ' + (aliasOf.get(extension.metaclass) ?? extension.metaclass));
|
|
222
|
+
}
|
|
223
|
+
for (const generalization of model.generalizations) {
|
|
224
|
+
lines.push((aliasOf.get(generalization.specific) ?? generalization.specific) + ' --|> ' + (aliasOf.get(generalization.general) ?? generalization.general));
|
|
225
|
+
}
|
|
226
|
+
lines.push('@enduml');
|
|
227
|
+
return { primary: lines.join('\n') + '\n', warnings };
|
|
228
|
+
}
|
|
229
|
+
export const PROFILE_FINDING_CODES = [
|
|
230
|
+
'UML050_PROFILE_NO_STEREOTYPES',
|
|
231
|
+
'UML051_PROFILE_UNUSED_STEREOTYPE',
|
|
232
|
+
'UML052_PROFILE_EXTENSION_MISSING',
|
|
233
|
+
'UML053_PROFILE_GENERALIZATION_CYCLE',
|
|
234
|
+
'UML054_PROFILE_UNDECLARED_REFERENCE',
|
|
235
|
+
'UML055_PROFILE_APPLICATION_MISSING',
|
|
236
|
+
'UML056_PROFILE_UNDOCUMENTED',
|
|
237
|
+
];
|
|
238
|
+
/**
|
|
239
|
+
* What a reader would get wrong about a profile.
|
|
240
|
+
*
|
|
241
|
+
* The checks read the model alone. Whether a profile is *right* for the metamodel it
|
|
242
|
+
* extends is a question about the specification being profiled, and this does not answer
|
|
243
|
+
* it: the codes below are about the profile being internally complete and readable.
|
|
244
|
+
*/
|
|
245
|
+
export function reviewProfileModel(model, labels) {
|
|
246
|
+
const findings = [];
|
|
247
|
+
const name = (id) => labels[id] ?? id;
|
|
248
|
+
if (model.stereotypes.length === 0) {
|
|
249
|
+
findings.push({
|
|
250
|
+
code: 'UML050_PROFILE_NO_STEREOTYPES',
|
|
251
|
+
severity: 'error',
|
|
252
|
+
message: 'the profile declares no stereotype, so it extends the metamodel with nothing.',
|
|
253
|
+
evidence: { stereotypes: 0 },
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
const extended = new Set(model.extensions.map((extension) => extension.stereotype));
|
|
257
|
+
const specialized = new Set(model.generalizations.flatMap((generalization) => [generalization.general, generalization.specific]));
|
|
258
|
+
for (const stereotype of model.stereotypes) {
|
|
259
|
+
if (extended.has(stereotype.id) || specialized.has(stereotype.id))
|
|
260
|
+
continue;
|
|
261
|
+
findings.push({
|
|
262
|
+
code: 'UML051_PROFILE_UNUSED_STEREOTYPE',
|
|
263
|
+
severity: 'warning',
|
|
264
|
+
message: 'stereotype ' + name(stereotype.id) + ' extends no metaclass and specialises no other stereotype: nothing in the profile uses it.',
|
|
265
|
+
evidence: { id: stereotype.id },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
for (const metaclass of model.metaclasses) {
|
|
269
|
+
if (model.extensions.some((extension) => extension.metaclass === metaclass.id))
|
|
270
|
+
continue;
|
|
271
|
+
findings.push({
|
|
272
|
+
code: 'UML052_PROFILE_EXTENSION_MISSING',
|
|
273
|
+
severity: 'warning',
|
|
274
|
+
message: 'metaclass ' + name(metaclass.id) + ' is declared but no stereotype extends it, so the profile does not change it.',
|
|
275
|
+
evidence: { id: metaclass.id },
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
// A stereotype hierarchy must be acyclic: the specification requires it, and a cycle
|
|
279
|
+
// means no reader can tell which stereotype is the general one.
|
|
280
|
+
const adjacency = new Map();
|
|
281
|
+
for (const generalization of model.generalizations) {
|
|
282
|
+
const list = adjacency.get(generalization.specific);
|
|
283
|
+
if (list === undefined)
|
|
284
|
+
adjacency.set(generalization.specific, [generalization.general]);
|
|
285
|
+
else
|
|
286
|
+
list.push(generalization.general);
|
|
287
|
+
}
|
|
288
|
+
const state = new Map();
|
|
289
|
+
const path = [];
|
|
290
|
+
const walk = (node) => {
|
|
291
|
+
const seen = state.get(node);
|
|
292
|
+
if (seen === 'visiting')
|
|
293
|
+
return [...path.slice(path.indexOf(node)), node];
|
|
294
|
+
if (seen === 'done')
|
|
295
|
+
return undefined;
|
|
296
|
+
state.set(node, 'visiting');
|
|
297
|
+
path.push(node);
|
|
298
|
+
for (const next of adjacency.get(node) ?? []) {
|
|
299
|
+
const cycle = walk(next);
|
|
300
|
+
if (cycle !== undefined)
|
|
301
|
+
return cycle;
|
|
302
|
+
}
|
|
303
|
+
path.pop();
|
|
304
|
+
state.set(node, 'done');
|
|
305
|
+
return undefined;
|
|
306
|
+
};
|
|
307
|
+
for (const node of adjacency.keys()) {
|
|
308
|
+
const cycle = walk(node);
|
|
309
|
+
if (cycle === undefined)
|
|
310
|
+
continue;
|
|
311
|
+
findings.push({
|
|
312
|
+
code: 'UML053_PROFILE_GENERALIZATION_CYCLE',
|
|
313
|
+
severity: 'error',
|
|
314
|
+
message: 'the stereotype hierarchy is cyclic, so which stereotype is the general one has no answer.',
|
|
315
|
+
detail: cycle.map((id) => name(id)).join(' -> '),
|
|
316
|
+
evidence: { path: cycle },
|
|
317
|
+
});
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
const undeclared = model.undeclared ?? [];
|
|
321
|
+
if (undeclared.length > 0) {
|
|
322
|
+
findings.push({
|
|
323
|
+
code: 'UML054_PROFILE_UNDECLARED_REFERENCE',
|
|
324
|
+
severity: 'error',
|
|
325
|
+
message: 'the profile refers to ' + String(undeclared.length) + ' element(s) it never declares, so the arrow points at nothing a reader can look up.',
|
|
326
|
+
detail: undeclared.join(', '),
|
|
327
|
+
evidence: { ids: undeclared },
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
if (model.applications.length === 0) {
|
|
331
|
+
findings.push({
|
|
332
|
+
code: 'UML055_PROFILE_APPLICATION_MISSING',
|
|
333
|
+
severity: 'info',
|
|
334
|
+
message: 'the profile is not applied to any package: nothing says where it is meant to be used.',
|
|
335
|
+
evidence: { applications: 0 },
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (model.profile.label === undefined && labels[model.profile.id] === undefined) {
|
|
339
|
+
findings.push({
|
|
340
|
+
code: 'UML056_PROFILE_UNDOCUMENTED',
|
|
341
|
+
severity: 'info',
|
|
342
|
+
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.',
|
|
343
|
+
evidence: { id: model.profile.id },
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
for (const stereotype of model.stereotypes) {
|
|
347
|
+
if (stereotype.label !== undefined || labels[stereotype.id] !== undefined)
|
|
348
|
+
continue;
|
|
349
|
+
findings.push({
|
|
350
|
+
code: 'UML056_PROFILE_UNDOCUMENTED',
|
|
351
|
+
severity: 'info',
|
|
352
|
+
message: 'stereotype ' + stereotype.id + ' carries no name a reader can map back to the specification: give it a display label or a narrative entry.',
|
|
353
|
+
evidence: { id: stereotype.id },
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return findings;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* The profile family, as the engine's registry expects it.
|
|
360
|
+
*
|
|
361
|
+
* `parse` refuses with the family table's own words. It has to exist because the module
|
|
362
|
+
* contract requires it, and it must refuse rather than return something: a parse would
|
|
363
|
+
* have to decide which `--|>` is an extension and which is a generalization, and the
|
|
364
|
+
* notation does not say.
|
|
365
|
+
*/
|
|
366
|
+
export const profileModule = {
|
|
367
|
+
id: 'profile',
|
|
368
|
+
render: (input, notation, _options) => renderProfile(input, notation),
|
|
369
|
+
parse: () => {
|
|
370
|
+
throw new UmlError('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.', 'UML_VIEW_UNREPRESENTABLE');
|
|
371
|
+
},
|
|
372
|
+
review: (model, labels) => reviewProfileModel(model, labels),
|
|
373
|
+
};
|