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,520 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composite structure diagram module.
|
|
3
|
+
*
|
|
4
|
+
* A composite structure diagram is a structured classifier seen from the inside: the parts
|
|
5
|
+
* it is made of, the ports on its boundary, and the connectors between them. That reads
|
|
6
|
+
* back out of the text, which is what separates this family from the profile.
|
|
7
|
+
*
|
|
8
|
+
* PlantUML has no composite-structure page, so the view is an approximation built from the
|
|
9
|
+
* deployment diagram's vocabulary: a nested element for the classifier, the nestable
|
|
10
|
+
* elements for its parts, `port`/`portin`/`portout` for the boundary, and the arrows for
|
|
11
|
+
* connectors. Nesting is spelled, port direction is spelled, and both connector ends are
|
|
12
|
+
* spelled - so the model survives the round trip. What the notation cannot say (a part's
|
|
13
|
+
* type, a port's required interface, a connector's name) stays out of the model rather
|
|
14
|
+
* than being invented on the way back.
|
|
15
|
+
*
|
|
16
|
+
* @module model-everything-uml-composite
|
|
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
|
+
/** One end of a connector: the classifier itself, one of its ports, or one of its parts. */
|
|
25
|
+
export type CompositeEndpoint = string
|
|
26
|
+
|
|
27
|
+
/** The structured classifier being opened up. */
|
|
28
|
+
export interface CompositeSpec {
|
|
29
|
+
id: string
|
|
30
|
+
label?: string
|
|
31
|
+
/** The keyword it is drawn with; `component` is the deployment diagram's default. */
|
|
32
|
+
kind: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CompositePart {
|
|
36
|
+
id: string
|
|
37
|
+
label?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type CompositePortDirection = 'in' | 'out' | 'inout'
|
|
41
|
+
|
|
42
|
+
export interface CompositePort {
|
|
43
|
+
id: string
|
|
44
|
+
/** `in` is `portin`, `out` is `portout`, `inout` is the bare `port`. */
|
|
45
|
+
direction: CompositePortDirection
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CompositeConnector {
|
|
49
|
+
from: CompositeEndpoint
|
|
50
|
+
to: CompositeEndpoint
|
|
51
|
+
label?: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A composite structure diagram as a model.
|
|
56
|
+
*
|
|
57
|
+
* `self` is the classifier. Ports and parts belong to it, and a connector endpoint names
|
|
58
|
+
* one of the three - the classifier itself, a port, or a part. The model has no nesting:
|
|
59
|
+
* a part of a part is a second diagram, which is how a reader keeps each view readable.
|
|
60
|
+
*/
|
|
61
|
+
export interface CompositeModelV1 {
|
|
62
|
+
schemaVersion: 1
|
|
63
|
+
self: CompositeSpec
|
|
64
|
+
parts: CompositePart[]
|
|
65
|
+
ports: CompositePort[]
|
|
66
|
+
connectors: CompositeConnector[]
|
|
67
|
+
/** Ids a connector named that no declaration introduced; the review reports them. */
|
|
68
|
+
undeclared?: string[]
|
|
69
|
+
narrative?: { self?: string; parts?: Record<string, string>; ports?: Record<string, string> }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Elements PlantUML can nest; a part is declared with one of these. */
|
|
73
|
+
const NESTABLE = [
|
|
74
|
+
'component', 'node', 'artifact', 'database', 'file', 'folder', 'frame', 'cloud',
|
|
75
|
+
'package', 'queue', 'rectangle', 'stack', 'storage', 'card', 'hexagon', 'action',
|
|
76
|
+
'process', 'collections',
|
|
77
|
+
] as const
|
|
78
|
+
|
|
79
|
+
const PORT_KEYWORDS: Record<string, CompositePortDirection> = { port: 'inout', portin: 'in', portout: 'out' }
|
|
80
|
+
|
|
81
|
+
/** The port keyword a direction is drawn with. */
|
|
82
|
+
function portKeyword(direction: CompositePortDirection): string {
|
|
83
|
+
if (direction === 'in') return 'portin'
|
|
84
|
+
if (direction === 'out') return 'portout'
|
|
85
|
+
return 'port'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function quote(text: string): string {
|
|
89
|
+
return '"' + text.replace(/"/g, "'") + '"'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isPlainId(id: string): boolean {
|
|
93
|
+
return UML_PLAIN_ID.test(id) && !(NESTABLE as readonly string[]).includes(id.toLowerCase()) && id !== 'port' && id !== 'portin' && id !== 'portout'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate a candidate model and normalize it.
|
|
98
|
+
*
|
|
99
|
+
* A port id and a part id may not collide: a connector endpoint is a bare name, so two
|
|
100
|
+
* declarations sharing one would make the drawing say two things at once.
|
|
101
|
+
*/
|
|
102
|
+
export function validateCompositeModel(input: unknown): CompositeModelV1 {
|
|
103
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
104
|
+
throw new UmlError('a composite structure model must be an object with schemaVersion 1, a self, parts, ports and connectors', 'UML_INPUT')
|
|
105
|
+
}
|
|
106
|
+
const model = input as Partial<CompositeModelV1>
|
|
107
|
+
if (model.schemaVersion !== 1) throw new UmlError('a composite structure model must declare schemaVersion 1', 'UML_INPUT')
|
|
108
|
+
const self = model.self
|
|
109
|
+
if (self === null || typeof self !== 'object' || typeof self.id !== 'string' || self.id.trim() === '') {
|
|
110
|
+
throw new UmlError('a composite structure model needs a `self` with a non-empty id', 'UML_INPUT')
|
|
111
|
+
}
|
|
112
|
+
const kind = self.kind ?? 'component'
|
|
113
|
+
if (!(NESTABLE as readonly string[]).includes(kind)) {
|
|
114
|
+
throw new UmlError('self.kind must be one of ' + NESTABLE.join(' | ') + ', got ' + quote(String(kind)), 'UML_INPUT')
|
|
115
|
+
}
|
|
116
|
+
for (const [name, value] of Object.entries({ parts: model.parts ?? [], ports: model.ports ?? [], connectors: model.connectors ?? [] })) {
|
|
117
|
+
if (!Array.isArray(value)) throw new UmlError(name + ' must be an array', 'UML_INPUT')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const declared = new Set<string>([self.id])
|
|
121
|
+
const parts: CompositePart[] = []
|
|
122
|
+
for (const part of model.parts ?? []) {
|
|
123
|
+
if (part === null || typeof part !== 'object' || typeof part.id !== 'string' || part.id.trim() === '') {
|
|
124
|
+
throw new UmlError('every part needs a non-empty string id', 'UML_INPUT')
|
|
125
|
+
}
|
|
126
|
+
if (declared.has(part.id)) throw new UmlError(quote(part.id) + ' is declared more than once as a part or a port', 'UML_INPUT')
|
|
127
|
+
declared.add(part.id)
|
|
128
|
+
parts.push({ id: part.id, ...(part.label === undefined ? {} : { label: part.label }) })
|
|
129
|
+
}
|
|
130
|
+
const ports: CompositePort[] = []
|
|
131
|
+
for (const port of model.ports ?? []) {
|
|
132
|
+
if (port === null || typeof port !== 'object' || typeof port.id !== 'string' || port.id.trim() === '') {
|
|
133
|
+
throw new UmlError('every port needs a non-empty string id', 'UML_INPUT')
|
|
134
|
+
}
|
|
135
|
+
if (declared.has(port.id)) throw new UmlError(quote(port.id) + ' is declared more than once as a part or a port', 'UML_INPUT')
|
|
136
|
+
declared.add(port.id)
|
|
137
|
+
const direction = port.direction ?? 'inout'
|
|
138
|
+
if (direction !== 'in' && direction !== 'out' && direction !== 'inout') {
|
|
139
|
+
throw new UmlError('port direction must be in | out | inout, got ' + quote(String(direction)), 'UML_INPUT')
|
|
140
|
+
}
|
|
141
|
+
ports.push({ id: port.id, direction })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const undeclared = new Set<string>()
|
|
145
|
+
const connectors: CompositeConnector[] = []
|
|
146
|
+
for (const connector of model.connectors ?? []) {
|
|
147
|
+
if (connector === null || typeof connector !== 'object') throw new UmlError('every connector must be an object', 'UML_INPUT')
|
|
148
|
+
for (const side of ['from', 'to'] as const) {
|
|
149
|
+
const endpoint = connector[side]
|
|
150
|
+
if (typeof endpoint !== 'string' || endpoint.trim() === '') {
|
|
151
|
+
throw new UmlError('every connector needs a non-empty `' + side + '` end', 'UML_INPUT')
|
|
152
|
+
}
|
|
153
|
+
if (!declared.has(endpoint)) undeclared.add(endpoint)
|
|
154
|
+
}
|
|
155
|
+
if (connector.from === connector.to) {
|
|
156
|
+
throw new UmlError('a connector cannot join ' + quote(connector.from) + ' to itself', 'UML_INPUT')
|
|
157
|
+
}
|
|
158
|
+
connectors.push({
|
|
159
|
+
from: connector.from,
|
|
160
|
+
to: connector.to,
|
|
161
|
+
...(connector.label === undefined ? {} : { label: connector.label }),
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
schemaVersion: 1,
|
|
167
|
+
self: { id: self.id, kind, ...(self.label === undefined ? {} : { label: self.label }) },
|
|
168
|
+
parts,
|
|
169
|
+
ports,
|
|
170
|
+
connectors,
|
|
171
|
+
...(undeclared.size === 0 ? {} : { undeclared: [...undeclared].sort() }),
|
|
172
|
+
...(model.narrative === undefined ? {} : { narrative: model.narrative }),
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
// Rendering
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Render a composite structure diagram as PlantUML.
|
|
182
|
+
*
|
|
183
|
+
* The classifier is drawn as a nested element, its parts inside it, its ports on its
|
|
184
|
+
* boundary, and the connectors after the block. The `self` directive carries the
|
|
185
|
+
* classifier's own id, because a nested element's closing brace is the only thing that
|
|
186
|
+
* says where its inside ends.
|
|
187
|
+
*
|
|
188
|
+
* @param input - candidate `CompositeModelV1`.
|
|
189
|
+
* @param notation - only `plantuml` draws this family; mermaid has no ports or connectors.
|
|
190
|
+
*/
|
|
191
|
+
export function renderComposite(input: unknown, notation: UmlNotation): UmlFamilyRender {
|
|
192
|
+
if (notation !== 'plantuml') {
|
|
193
|
+
throw new UmlError('a composite structure diagram can only be drawn in plantuml; mermaid has no port or connector element and no nested element to put parts in. Render it with notation "plantuml".', 'UML_VIEW_UNREPRESENTABLE')
|
|
194
|
+
}
|
|
195
|
+
const model = validateCompositeModel(input)
|
|
196
|
+
const warnings: string[] = [
|
|
197
|
+
'UML_RENDER_APPROXIMATE_VIEW: plantuml has no composite structure diagram; this is a deployment-diagram drawing of one. Nesting, part names, port direction and both connector ends are carried; a part\'s type, a port\'s interfaces and a connector\'s name are not expressible and are not drawn.',
|
|
198
|
+
]
|
|
199
|
+
const lines: string[] = ['@startuml']
|
|
200
|
+
lines.push(directiveLine('plantuml', 'uml v1 notation=plantuml family=composite-structure'))
|
|
201
|
+
lines.push(directiveLine('plantuml', 'compositeSelf ' + model.self.kind + ' ' + model.self.id))
|
|
202
|
+
const label = model.self.label === undefined ? quote(model.self.id) : quote(model.self.label)
|
|
203
|
+
lines.push(model.self.kind + ' ' + label + ' as ' + model.self.id + ' {')
|
|
204
|
+
for (const port of model.ports) {
|
|
205
|
+
// A port declaration takes no alias, so its id is what the reader sees; a port whose
|
|
206
|
+
// id PlantUML cannot use is reported rather than silently renamed.
|
|
207
|
+
lines.push(' ' + portKeyword(port.direction) + ' ' + port.id)
|
|
208
|
+
}
|
|
209
|
+
for (const part of model.parts) {
|
|
210
|
+
if (part.label === undefined) lines.push(' component ' + part.id)
|
|
211
|
+
else lines.push(' component ' + quote(part.label) + ' as ' + part.id)
|
|
212
|
+
}
|
|
213
|
+
lines.push('}')
|
|
214
|
+
for (const connector of model.connectors) {
|
|
215
|
+
lines.push(connector.from + ' --> ' + connector.to + (connector.label === undefined ? '' : ' : ' + connector.label))
|
|
216
|
+
}
|
|
217
|
+
lines.push('@enduml')
|
|
218
|
+
return { primary: lines.join('\n') + '\n', warnings }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Parsing
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
interface CompositeLine {
|
|
226
|
+
line: number
|
|
227
|
+
text: string
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const SELF_DIRECTIVE = /^compositeSelf\s+([A-Za-z_][A-Za-z0-9_]*)\s+(\S+)\s*$/
|
|
231
|
+
const DECLARATION = new RegExp('^(' + NESTABLE.join('|') + ')\\s+(?:"([^"]*)"|(\\S+))(?:\\s+as\\s+(\\S+))?\\s*(\\{)?\\s*$', 'i')
|
|
232
|
+
const PORT_DECLARATION = /^(port|portin|portout)\s+(\S+)\s*$/i
|
|
233
|
+
const CONNECTOR = /^(\S+)\s+(-->|->|--|\.\.>|\.\.)\s+(\S+?)(?:\s*:\s*(.*))?$/
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Read a composite structure diagram back into a model.
|
|
237
|
+
*
|
|
238
|
+
* Faithful to this module's renderer and tolerant of hand-written text: the classifier is
|
|
239
|
+
* the outermost block (named by the `compositeSelf` directive when the renderer wrote it),
|
|
240
|
+
* `port`/`portin`/`portout` lines are its ports, nestable declarations inside the block are
|
|
241
|
+
* its parts, and every arrow between two names is a connector. A construct the notation
|
|
242
|
+
* carries but this model does not - a nested block inside a part, a stereotype, a note - is
|
|
243
|
+
* reported in `discardedConstructs`, never dropped in silence.
|
|
244
|
+
*/
|
|
245
|
+
export function parseComposite(text: string, notation: UmlNotation): UmlFamilyParse<CompositeModelV1> {
|
|
246
|
+
if (notation !== 'plantuml') {
|
|
247
|
+
throw new UmlError('a composite structure diagram can only be read from plantuml', 'UML_VIEW_UNREPRESENTABLE')
|
|
248
|
+
}
|
|
249
|
+
const all: CompositeLine[] = text.split(/\r?\n/).map((raw, index) => ({ line: index + 1, text: raw.trim() }))
|
|
250
|
+
const warnings: string[] = []
|
|
251
|
+
const discarded: Array<{ line: number; text: string; construct: string }> = []
|
|
252
|
+
const labels: Record<string, string> = {}
|
|
253
|
+
|
|
254
|
+
let self: CompositeSpec | undefined
|
|
255
|
+
let fromDirective: { kind: string; id: string } | undefined
|
|
256
|
+
const parts: CompositePart[] = []
|
|
257
|
+
const ports: CompositePort[] = []
|
|
258
|
+
const connectors: CompositeConnector[] = []
|
|
259
|
+
let depth = 0
|
|
260
|
+
let seenConnector = false
|
|
261
|
+
|
|
262
|
+
for (const entry of all) {
|
|
263
|
+
const line = entry.text
|
|
264
|
+
if (line === '' || line.startsWith('@') || line.startsWith("'")) {
|
|
265
|
+
// The renderer's own directives are read elsewhere; a comment is not a construct.
|
|
266
|
+
if (line.startsWith("'dsh-model-everything:")) {
|
|
267
|
+
const directive = line.slice("'dsh-model-everything:".length).trim()
|
|
268
|
+
const selfMatch = SELF_DIRECTIVE.exec(directive)
|
|
269
|
+
if (selfMatch !== null) fromDirective = { kind: selfMatch[1].toLowerCase(), id: selfMatch[2] }
|
|
270
|
+
}
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
if (/^(left to right direction|top to bottom direction|skinparam|hide|show|title|scale|allowmixing|allow_mixing)\b/i.test(line)) {
|
|
274
|
+
discarded.push({ line: entry.line, text: line, construct: line.split(/\s+/)[0].toLowerCase() })
|
|
275
|
+
continue
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const port = PORT_DECLARATION.exec(line)
|
|
279
|
+
if (port !== null) {
|
|
280
|
+
if (depth === 0) {
|
|
281
|
+
discarded.push({ line: entry.line, text: line, construct: 'port outside a classifier' })
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
const id = port[2]
|
|
285
|
+
if (ports.some((entry) => entry.id === id) || parts.some((entry) => entry.id === id)) {
|
|
286
|
+
warnings.push('line ' + String(entry.line) + ': ' + id + ' is declared twice; the second declaration is ignored')
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
ports.push({ id, direction: PORT_KEYWORDS[port[1].toLowerCase()] })
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const declaration = DECLARATION.exec(line)
|
|
294
|
+
if (declaration !== null) {
|
|
295
|
+
const kind = declaration[1].toLowerCase()
|
|
296
|
+
const quoted = declaration[2]
|
|
297
|
+
const bare = declaration[3]
|
|
298
|
+
const alias = declaration[4]
|
|
299
|
+
const opens = declaration[5] === '{'
|
|
300
|
+
const id = alias ?? bare ?? quoted ?? ''
|
|
301
|
+
// `component "App" as app` names the id in the alias and the label in the quotes.
|
|
302
|
+
// `component App as app` puts the label in the bare position instead, and that is a
|
|
303
|
+
// label rather than an id: the alias is what the rest of the diagram refers to.
|
|
304
|
+
const labelText = quoted ?? (alias === undefined ? undefined : bare)
|
|
305
|
+
if (labelText !== undefined) labels[id] = labelText
|
|
306
|
+
if (depth === 0) {
|
|
307
|
+
if (self !== undefined) {
|
|
308
|
+
discarded.push({ line: entry.line, text: line, construct: 'second top-level block' })
|
|
309
|
+
} else {
|
|
310
|
+
self = { id, kind, ...(labelText === undefined ? {} : { label: labelText }) }
|
|
311
|
+
}
|
|
312
|
+
} else if (depth === 1) {
|
|
313
|
+
if (opens) {
|
|
314
|
+
// A block inside the classifier would be a part with its own inside. The model
|
|
315
|
+
// has no nesting, so the part is kept and the block it opens is reported.
|
|
316
|
+
discarded.push({ line: entry.line, text: line, construct: 'nested block inside a part' })
|
|
317
|
+
}
|
|
318
|
+
if (!parts.some((entry) => entry.id === id) && !ports.some((entry) => entry.id === id)) {
|
|
319
|
+
parts.push({ id, ...(labelText === undefined ? {} : { label: labelText }) })
|
|
320
|
+
} else {
|
|
321
|
+
warnings.push('line ' + String(entry.line) + ': ' + id + ' is declared twice; the second declaration is ignored')
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
// Whatever is declared inside a part is that part's inside, and this model has no
|
|
325
|
+
// place for it. Reporting it is the only alternative to inventing a structure.
|
|
326
|
+
discarded.push({ line: entry.line, text: line, construct: 'declaration inside a part' })
|
|
327
|
+
}
|
|
328
|
+
if (opens) depth += 1
|
|
329
|
+
continue
|
|
330
|
+
}
|
|
331
|
+
if (line === '}') {
|
|
332
|
+
depth = Math.max(0, depth - 1)
|
|
333
|
+
seenConnector = false
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const connector = CONNECTOR.exec(line)
|
|
338
|
+
if (connector !== null) {
|
|
339
|
+
const [, from, , to, label] = connector
|
|
340
|
+
connectors.push({ from, to, ...(label === undefined || label === '' ? {} : { label }) })
|
|
341
|
+
seenConnector = true
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
if (seenConnector || depth > 0) {
|
|
345
|
+
discarded.push({ line: entry.line, text: line, construct: 'line this model does not carry' })
|
|
346
|
+
} else {
|
|
347
|
+
discarded.push({ line: entry.line, text: line, construct: 'statement outside any classifier' })
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (self === undefined) {
|
|
352
|
+
if (fromDirective === undefined) {
|
|
353
|
+
throw new UmlError('no structured classifier found: a composite structure diagram needs one block that holds its parts, such as `component Order as order { ... }`', 'UML_INPUT')
|
|
354
|
+
}
|
|
355
|
+
self = { id: fromDirective.id, kind: fromDirective.kind }
|
|
356
|
+
} else if (fromDirective !== undefined) {
|
|
357
|
+
// The directive is the renderer's own record; it wins, because the block's alias can
|
|
358
|
+
// be a label PlantUML was given.
|
|
359
|
+
self = { ...self, id: fromDirective.id, kind: fromDirective.kind }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const declared = new Set<string>([self.id, ...parts.map((part) => part.id), ...ports.map((port) => port.id)])
|
|
363
|
+
const undeclared = [...new Set(connectors.flatMap((connector) => [connector.from, connector.to]))].filter((endpoint) => !declared.has(endpoint)).sort()
|
|
364
|
+
// No narrative is invented here. The labels a reader gave the drawing come back in
|
|
365
|
+
// `labels`, which is where a display name belongs; writing them into the model as well
|
|
366
|
+
// would add a field the model never carried, and the round trip would stop being exact.
|
|
367
|
+
const model: CompositeModelV1 = {
|
|
368
|
+
schemaVersion: 1,
|
|
369
|
+
self,
|
|
370
|
+
parts,
|
|
371
|
+
ports,
|
|
372
|
+
connectors,
|
|
373
|
+
...(undeclared.length === 0 ? {} : { undeclared }),
|
|
374
|
+
}
|
|
375
|
+
return { model, labels, warnings, discardedConstructs: discarded, discardedEdges: 0 }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
// Review
|
|
380
|
+
// ---------------------------------------------------------------------------
|
|
381
|
+
|
|
382
|
+
export type CompositeFindingCode =
|
|
383
|
+
| 'UML057_COMPOSITE_NO_PARTS'
|
|
384
|
+
| 'UML058_COMPOSITE_PORT_UNCONNECTED'
|
|
385
|
+
| 'UML059_COMPOSITE_CONNECTOR_ENDPOINT_MISSING'
|
|
386
|
+
| 'UML060_COMPOSITE_DUPLICATE_PART'
|
|
387
|
+
| 'UML061_COMPOSITE_PORT_COLLISION'
|
|
388
|
+
| 'UML062_COMPOSITE_UNDOCUMENTED'
|
|
389
|
+
|
|
390
|
+
export const COMPOSITE_FINDING_CODES: readonly CompositeFindingCode[] = [
|
|
391
|
+
'UML057_COMPOSITE_NO_PARTS',
|
|
392
|
+
'UML058_COMPOSITE_PORT_UNCONNECTED',
|
|
393
|
+
'UML059_COMPOSITE_CONNECTOR_ENDPOINT_MISSING',
|
|
394
|
+
'UML060_COMPOSITE_DUPLICATE_PART',
|
|
395
|
+
'UML061_COMPOSITE_PORT_COLLISION',
|
|
396
|
+
'UML062_COMPOSITE_UNDOCUMENTED',
|
|
397
|
+
]
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* What a reader would get wrong about a composite structure.
|
|
401
|
+
*
|
|
402
|
+
* The questions are the ones the diagram exists to answer: is anything inside, does every
|
|
403
|
+
* port lead somewhere, does every connector join two things that exist, and is each part
|
|
404
|
+
* and port named after something in the system rather than after its drawing position.
|
|
405
|
+
* Whether the decomposition is *right* is a question about the design, and this does not
|
|
406
|
+
* answer it.
|
|
407
|
+
*/
|
|
408
|
+
export function reviewCompositeModel(model: CompositeModelV1, labels: Record<string, string>): UmlFamilyFinding[] {
|
|
409
|
+
const findings: UmlFamilyFinding[] = []
|
|
410
|
+
const name = (id: string): string => labels[id] ?? id
|
|
411
|
+
|
|
412
|
+
if (model.parts.length === 0) {
|
|
413
|
+
findings.push({
|
|
414
|
+
code: 'UML057_COMPOSITE_NO_PARTS',
|
|
415
|
+
severity: 'error',
|
|
416
|
+
message: 'the classifier is drawn as a composite structure but holds no part: a structure diagram with an empty inside says nothing about how it is built.',
|
|
417
|
+
evidence: { parts: 0 },
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const connected = new Set(model.connectors.flatMap((connector) => [connector.from, connector.to]))
|
|
422
|
+
for (const port of model.ports) {
|
|
423
|
+
if (connected.has(port.id)) continue
|
|
424
|
+
findings.push({
|
|
425
|
+
code: 'UML058_COMPOSITE_PORT_UNCONNECTED',
|
|
426
|
+
severity: 'warning',
|
|
427
|
+
message: 'port ' + name(port.id) + ' (' + port.direction + ') has no connector, so nothing reaches the classifier through it.',
|
|
428
|
+
evidence: { id: port.id, direction: port.direction },
|
|
429
|
+
})
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const undeclared = model.undeclared ?? []
|
|
433
|
+
if (undeclared.length > 0) {
|
|
434
|
+
findings.push({
|
|
435
|
+
code: 'UML059_COMPOSITE_CONNECTOR_ENDPOINT_MISSING',
|
|
436
|
+
severity: 'error',
|
|
437
|
+
message: 'a connector ends at ' + String(undeclared.length) + ' name(s) the classifier never declares, so the line points at nothing a reader can look up.',
|
|
438
|
+
detail: undeclared.join(', '),
|
|
439
|
+
evidence: { ids: undeclared },
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const seen = new Set<string>()
|
|
444
|
+
for (const part of model.parts) {
|
|
445
|
+
if (seen.has(part.id)) {
|
|
446
|
+
findings.push({
|
|
447
|
+
code: 'UML060_COMPOSITE_DUPLICATE_PART',
|
|
448
|
+
severity: 'error',
|
|
449
|
+
message: 'part ' + part.id + ' is declared more than once, so a connector that names it is ambiguous.',
|
|
450
|
+
evidence: { id: part.id },
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
seen.add(part.id)
|
|
454
|
+
}
|
|
455
|
+
if (seen.has(model.self.id)) {
|
|
456
|
+
findings.push({
|
|
457
|
+
code: 'UML061_COMPOSITE_PORT_COLLISION',
|
|
458
|
+
severity: 'error',
|
|
459
|
+
message: 'the classifier itself is also declared as a part (' + model.self.id + '): a nested element inside itself has no reading.',
|
|
460
|
+
evidence: { id: model.self.id },
|
|
461
|
+
})
|
|
462
|
+
}
|
|
463
|
+
for (const port of model.ports) {
|
|
464
|
+
if (seen.has(port.id)) {
|
|
465
|
+
findings.push({
|
|
466
|
+
code: 'UML061_COMPOSITE_PORT_COLLISION',
|
|
467
|
+
severity: 'error',
|
|
468
|
+
message: 'port ' + port.id + ' shares its name with a part: a connector that names it cannot be read.',
|
|
469
|
+
evidence: { id: port.id },
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (model.self.label === undefined && labels[model.self.id] === undefined) {
|
|
475
|
+
findings.push({
|
|
476
|
+
code: 'UML062_COMPOSITE_UNDOCUMENTED',
|
|
477
|
+
severity: 'info',
|
|
478
|
+
message: 'the classifier carries no name a reader can map back to the design: give it a display label or a narrative entry.',
|
|
479
|
+
evidence: { id: model.self.id },
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
for (const part of model.parts) {
|
|
483
|
+
if (part.label !== undefined || labels[part.id] !== undefined) continue
|
|
484
|
+
findings.push({
|
|
485
|
+
code: 'UML062_COMPOSITE_UNDOCUMENTED',
|
|
486
|
+
severity: 'info',
|
|
487
|
+
message: 'part ' + part.id + ' carries no name a reader can map back to the design: give it a display label or a narrative entry.',
|
|
488
|
+
evidence: { id: part.id },
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
return findings
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** The composite structure family, as the engine's registry expects it. */
|
|
495
|
+
export const compositeModule: UmlFamilyModule<CompositeModelV1> = {
|
|
496
|
+
id: 'composite-structure',
|
|
497
|
+
render: (input: unknown, notation: UmlNotation, _options: UmlFamilyRenderOptions): UmlFamilyRender => renderComposite(input, notation),
|
|
498
|
+
parse: (text: string, notation: UmlNotation): UmlFamilyParse<CompositeModelV1> => parseComposite(text, notation),
|
|
499
|
+
// The self's `kind` is always drawn - the renderer writes it into the `compositeSelf` directive
|
|
500
|
+
// and into the declaration - so the parser always reads one back. A model that left it out is
|
|
501
|
+
// normalized to the same shape before the fidelity comparison, or the review reports its own
|
|
502
|
+
// default as drift: "self.kind invented by the diagram" on a diagram that says exactly what the
|
|
503
|
+
// model meant.
|
|
504
|
+
//
|
|
505
|
+
// `undeclared` is carried through deliberately. It is what `UML059` reads, and it is a fact about
|
|
506
|
+
// the supplied model rather than about the diagram, so normalizing it away would silence the one
|
|
507
|
+
// finding that says a connector ends at a name the classifier never declares - the silent pass
|
|
508
|
+
// this file's own comments promise not to produce.
|
|
509
|
+
normalize: (input: unknown): CompositeModelV1 | undefined => {
|
|
510
|
+
try {
|
|
511
|
+
const normalized = validateCompositeModel(input)
|
|
512
|
+
const undeclared = (input as { undeclared?: unknown }).undeclared
|
|
513
|
+
if (!Array.isArray(undeclared)) return normalized
|
|
514
|
+
return { ...normalized, undeclared: undeclared.filter((entry): entry is string => typeof entry === 'string') }
|
|
515
|
+
} catch {
|
|
516
|
+
return undefined
|
|
517
|
+
}
|
|
518
|
+
},
|
|
519
|
+
review: (model: CompositeModelV1, labels: Record<string, string>): UmlFamilyFinding[] => reviewCompositeModel(model, labels),
|
|
520
|
+
}
|