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,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
|
+
}
|