dsh-model-everything 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.en-US.md +332 -0
  3. package/README.md +332 -3
  4. package/cordis.patch.yml +20 -0
  5. package/examples/README.md +48 -0
  6. package/examples/control-dispatcher.json +113 -0
  7. package/examples/control-dispatcher.overbudget.json +113 -0
  8. package/examples/ecommerce-data-model.json +185 -0
  9. package/examples/order-state-machine.after.json +116 -0
  10. package/examples/order-state-machine.before.json +101 -0
  11. package/examples/user-migration.after.json +29 -0
  12. package/examples/user-migration.before.json +29 -0
  13. package/gate/locale/en.json +6 -0
  14. package/gate/locale/zh.json +6 -0
  15. package/gate/package.json +5 -0
  16. package/lib/baseline.js +249 -0
  17. package/lib/compose-tool.js +80 -0
  18. package/lib/concurrency-tool.js +40 -0
  19. package/lib/concurrency.js +206 -0
  20. package/lib/data-engine.js +1141 -0
  21. package/lib/data-tool.js +87 -0
  22. package/lib/design-engine.js +1149 -0
  23. package/lib/design-tool.js +63 -0
  24. package/lib/engine.js +4343 -0
  25. package/lib/export-tool.js +78 -0
  26. package/lib/exporters.js +434 -0
  27. package/lib/gate.js +274 -0
  28. package/lib/granularity.js +237 -0
  29. package/lib/index.js +237 -0
  30. package/lib/json-value.js +18 -0
  31. package/lib/model-file.js +48 -0
  32. package/lib/option-shape.js +62 -0
  33. package/lib/report-diff-tool.js +63 -0
  34. package/lib/report-view.js +433 -0
  35. package/lib/structure-tool.js +91 -0
  36. package/lib/structure.js +711 -0
  37. package/lib/tool.js +94 -0
  38. package/lib/types/baseline.d.ts +65 -0
  39. package/lib/types/compose-tool.d.ts +7 -0
  40. package/lib/types/concurrency-tool.d.ts +8 -0
  41. package/lib/types/concurrency.d.ts +37 -0
  42. package/lib/types/data-engine.d.ts +211 -0
  43. package/lib/types/data-tool.d.ts +10 -0
  44. package/lib/types/design-engine.d.ts +215 -0
  45. package/lib/types/design-tool.d.ts +11 -0
  46. package/lib/types/engine.d.ts +494 -0
  47. package/lib/types/export-tool.d.ts +7 -0
  48. package/lib/types/exporters.d.ts +37 -0
  49. package/lib/types/gate.d.ts +91 -0
  50. package/lib/types/granularity.d.ts +106 -0
  51. package/lib/types/index.d.ts +45 -0
  52. package/lib/types/json-value.d.ts +21 -0
  53. package/lib/types/model-file.d.ts +7 -0
  54. package/lib/types/option-shape.d.ts +22 -0
  55. package/lib/types/report-diff-tool.d.ts +8 -0
  56. package/lib/types/report-view.d.ts +50 -0
  57. package/lib/types/structure-tool.d.ts +10 -0
  58. package/lib/types/structure.d.ts +152 -0
  59. package/lib/types/tool.d.ts +11 -0
  60. package/lib/types/uml-communication.d.ts +109 -0
  61. package/lib/types/uml-composite.d.ts +107 -0
  62. package/lib/types/uml-interaction-overview.d.ts +91 -0
  63. package/lib/types/uml-profile.d.ts +112 -0
  64. package/lib/types/uml-structure.d.ts +53 -0
  65. package/lib/types/uml-syntax.d.ts +296 -0
  66. package/lib/types/uml-timing.d.ts +152 -0
  67. package/lib/types/uml-tool.d.ts +12 -0
  68. package/lib/types/uml-usecase.d.ts +58 -0
  69. package/lib/types/uml.d.ts +303 -0
  70. package/lib/uml-communication.js +353 -0
  71. package/lib/uml-composite.js +442 -0
  72. package/lib/uml-interaction-overview.js +277 -0
  73. package/lib/uml-profile.js +373 -0
  74. package/lib/uml-structure.js +306 -0
  75. package/lib/uml-syntax.js +545 -0
  76. package/lib/uml-timing.js +974 -0
  77. package/lib/uml-tool.js +213 -0
  78. package/lib/uml-usecase.js +532 -0
  79. package/lib/uml.js +2110 -0
  80. package/locale/en.json +6 -0
  81. package/locale/zh.json +6 -0
  82. package/package.json +128 -6
  83. package/skills/model-everything/SKILL.md +450 -0
  84. package/skills/model-everything/references/dsh-model-schema.md +437 -0
  85. package/skills/model-everything/references/gap-routing-guide.md +42 -0
  86. package/skills/model-everything/references/hash-spec.md +152 -0
  87. package/skills/model-everything/references/logic-verification-guide.md +422 -0
  88. package/skills/model-everything-datamodel/SKILL.md +130 -0
  89. package/skills/model-everything-datamodel/references/data-model-guide.md +84 -0
  90. package/skills/model-everything-datamodel/references/data-model-schema.md +128 -0
  91. package/skills/model-everything-design/SKILL.md +115 -0
  92. package/skills/model-everything-interleaving/SKILL.md +63 -0
  93. package/skills/model-everything-interleaving/SKILL.zh-CN.md +60 -0
  94. package/skills/model-everything-interleaving/references/concurrency-risk-guide.md +63 -0
  95. package/skills/model-everything-report-reading/SKILL.md +137 -0
  96. package/skills/model-everything-report-reading/references/reading-a-report.md +166 -0
  97. package/skills/model-everything-report-reading/references/reading-a-report.mmd +59 -0
  98. package/skills/model-everything-structure/SKILL.md +96 -0
  99. package/skills/model-everything-structure/references/structure-review-guide.md +201 -0
  100. package/skills/model-everything-uml/SKILL.md +60 -0
  101. package/skills/model-everything-uml/references/uml-modeling-guide.md +551 -0
  102. package/src/baseline.ts +288 -0
  103. package/src/compose-tool.ts +82 -0
  104. package/src/concurrency-tool.ts +44 -0
  105. package/src/concurrency.ts +254 -0
  106. package/src/data-engine.ts +1208 -0
  107. package/src/data-tool.ts +91 -0
  108. package/src/design-engine.ts +1263 -0
  109. package/src/design-tool.ts +68 -0
  110. package/src/engine.ts +4564 -0
  111. package/src/export-tool.ts +81 -0
  112. package/src/exporters.ts +399 -0
  113. package/src/gate.ts +330 -0
  114. package/src/granularity.ts +312 -0
  115. package/src/index.ts +289 -0
  116. package/src/json-value.ts +20 -0
  117. package/src/model-file.ts +49 -0
  118. package/src/option-shape.ts +64 -0
  119. package/src/report-diff-tool.ts +67 -0
  120. package/src/report-view.ts +483 -0
  121. package/src/structure-tool.ts +93 -0
  122. package/src/structure.ts +786 -0
  123. package/src/tool.ts +103 -0
  124. package/src/uml-communication.ts +428 -0
  125. package/src/uml-composite.ts +520 -0
  126. package/src/uml-interaction-overview.ts +321 -0
  127. package/src/uml-profile.ts +439 -0
  128. package/src/uml-structure.ts +317 -0
  129. package/src/uml-syntax.ts +764 -0
  130. package/src/uml-timing.ts +1106 -0
  131. package/src/uml-tool.ts +230 -0
  132. package/src/uml-usecase.ts +566 -0
  133. package/src/uml.ts +2139 -0
@@ -0,0 +1,545 @@
1
+ /**
2
+ * The notation syntax engine.
3
+ *
4
+ * One place knows what Mermaid and PlantUML can spell, which UML 2.5 diagram type a
5
+ * piece of text is, and whether a requested (notation, family, action) cell exists at
6
+ * all. Family modules know what to do with the elements this engine finds.
7
+ *
8
+ * Before this module, detection fell through: any PlantUML text that was not a
9
+ * sequence diagram went to the state parser. A use case diagram drawn with
10
+ * `:Actor: --> (UseCase)` came back as a state machine with no finding, which is
11
+ * the false guarantee this plugin exists to prevent. Detection is now total. A text
12
+ * either names a family this engine knows, or it is refused by name.
13
+ *
14
+ * What lives here:
15
+ * - the notation table: comment prefixes, headers, chrome and edge operators;
16
+ * - the family table: all fourteen UML 2.5 diagram types, their group, and what each
17
+ * notation can express for them;
18
+ * - the capability decision: (notation, family, action) -> supported | refusal,
19
+ * separate from whether this build implements the family (that is the registry's
20
+ * job, in `uml.ts`);
21
+ * - notation-agnostic lexical primitives: directives, line scanning, edge splitting,
22
+ * identifier and label rules.
23
+ *
24
+ * What deliberately does not live here: family semantics. A family module turns the
25
+ * elements this engine finds into its own model, renders that model back, and reviews
26
+ * it. `uml-usecase.ts` is the first such module. The machine families (state,
27
+ * activity) and the trace family (sequence) predate the module contract and keep their
28
+ * own path in `uml.ts`.
29
+ *
30
+ * @module model-everything-uml-syntax
31
+ */
32
+ /**
33
+ * A UML front-end refusal. `code` lets the tool report *why* it refused instead
34
+ * of collapsing every refusal into a generic input error.
35
+ */
36
+ export class UmlError extends Error {
37
+ code;
38
+ constructor(message, code = 'UML_INPUT') {
39
+ super(message);
40
+ this.code = code;
41
+ }
42
+ }
43
+ export const UML_NOTATIONS = ['mermaid', 'plantuml'];
44
+ export function isUmlNotation(value) {
45
+ return typeof value === 'string' && UML_NOTATIONS.includes(value);
46
+ }
47
+ /** Mermaid node ids that cannot stand alone; the renderer aliases around them. */
48
+ export const MERMAID_RESERVED_NODE_IDS = new Set(['end', 'graph', 'subgraph', 'class', 'classDef', 'click', 'style', 'linkStyle', 'direction']);
49
+ /** Ids a notation can spell without an alias. */
50
+ export const UML_PLAIN_ID = /^[A-Za-z_][A-Za-z0-9_]*$/;
51
+ /** Characters that would break a generated label, whichever notation is used. */
52
+ export const UML_UNSAFE_LABEL = /[[\]/\n\r\t]/;
53
+ /** The comment prefix each notation uses for its dsh-model-everything: directives. */
54
+ export function commentPrefix(notation) {
55
+ return notation === 'mermaid' ? '%%' : "'";
56
+ }
57
+ export function escapeRegExp(text) {
58
+ return text.replace(/[.*+?^${{}()|[\]\\]/g, '\\$&');
59
+ }
60
+ const PLANTUML_ACTIVITY_NOTE = 'plantuml has no faithful activity view here (its activity syntax is a structured flowchart language; a graph with merges or cycles needs a while/if reconstruction this plugin does not perform) — use notation "mermaid" for the activity view, or diagram "state"';
61
+ const SEQUENCE_PARSE_REFUSAL = 'a sequence diagram is a trace, not a machine: parsing it would drop every branch the trace did not walk. Render diagram "state" or "activity" and parse that instead.';
62
+ /**
63
+ * The family table. Order matters twice: a Mermaid header is matched in this order,
64
+ * and a PlantUML declaration keyword is too. The machine families come first because
65
+ * their headers are the most specific.
66
+ */
67
+ export const UML_FAMILIES = [
68
+ {
69
+ id: 'state', name: 'state machine diagram', nameZh: '状态机图', group: 'behaviour', machine: true, parseable: true,
70
+ mermaid: { fidelity: 'faithful', headers: ['stateDiagram-v2', 'stateDiagram'] },
71
+ plantuml: { fidelity: 'faithful', declarations: ['state'] },
72
+ },
73
+ {
74
+ id: 'activity', name: 'activity diagram', nameZh: '活动图', group: 'behaviour', machine: true, parseable: true,
75
+ mermaid: { fidelity: 'faithful', headers: ['flowchart', 'graph'] },
76
+ plantuml: {
77
+ // The syntax exists, the faithful view does not: a structured flowchart cannot
78
+ // carry a graph with merges or cycles, so this cell is refused both ways.
79
+ fidelity: 'none',
80
+ declarations: ['start', 'stop', 'end', 'if', 'else', 'elseif', 'endif', 'while', 'endwhile', 'repeat', 'repeatwhile', 'fork', 'split', 'partition'],
81
+ note: PLANTUML_ACTIVITY_NOTE,
82
+ },
83
+ },
84
+ {
85
+ id: 'sequence', name: 'sequence diagram', nameZh: '顺序图', group: 'interaction', machine: false, parseable: false,
86
+ parseRefusal: SEQUENCE_PARSE_REFUSAL, parseRefusalCode: 'UML_NOT_A_STATE_DIAGRAM',
87
+ mermaid: { fidelity: 'faithful', headers: ['sequenceDiagram'] },
88
+ plantuml: { fidelity: 'faithful', declarations: ['participant', 'boundary', 'control', 'entity', 'database', 'collections', 'queue'] },
89
+ },
90
+ {
91
+ id: 'use-case', name: 'use case diagram', nameZh: '用例图', group: 'behaviour', machine: false, parseable: true,
92
+ plantuml: { fidelity: 'faithful', declarations: ['usecase', 'actor'] },
93
+ mermaid: {
94
+ fidelity: 'approximate',
95
+ headers: ['usecase-beta'],
96
+ note: 'mermaid draws a use case diagram with usecase-beta (Mermaid 12+): actors, a system boundary, associations, include and extend. This build renders that view; reading it back is not implemented yet, so a parse stays in plantuml. Render it with notation "plantuml" for the round trip.',
97
+ },
98
+ },
99
+ {
100
+ id: 'class', name: 'class diagram', nameZh: '类图', group: 'structure', machine: false, parseable: true,
101
+ mermaid: { fidelity: 'faithful', headers: ['classDiagram'] },
102
+ plantuml: { fidelity: 'faithful', declarations: ['class', 'interface', 'enum', 'enumeration', 'abstract', 'annotation', 'struct', 'protocol', 'exception', 'metaclass', 'stereotype'] },
103
+ },
104
+ {
105
+ id: 'object', name: 'object diagram', nameZh: '对象图', group: 'structure', machine: false, parseable: true,
106
+ plantuml: { fidelity: 'faithful', declarations: ['object'] },
107
+ mermaid: { fidelity: 'none', note: 'mermaid has no object diagram: classDiagram draws classifiers, not instances. Render it with notation "plantuml".' },
108
+ },
109
+ {
110
+ id: 'package', name: 'package diagram', nameZh: '包图', group: 'structure', machine: false, parseable: true,
111
+ plantuml: { fidelity: 'faithful', declarations: ['package', 'namespace', 'folder', 'frame', 'module'] },
112
+ mermaid: { fidelity: 'none', note: 'mermaid has no package diagram. Render it with notation "plantuml", or review the same text as a dependency graph with uml_structure_verify.' },
113
+ },
114
+ {
115
+ id: 'component', name: 'component diagram', nameZh: '组件图', group: 'structure', machine: false, parseable: true,
116
+ plantuml: { fidelity: 'faithful', declarations: ['component', 'componentDiagram', 'artifact', 'boundary', 'control', 'entity', 'agent', 'circle', 'hexagon', 'label'] },
117
+ mermaid: { fidelity: 'none', note: 'mermaid architecture-beta draws a cloud architecture, not UML components: the shapes and the ports do not line up, and this build does not emit it under the component name. Render it with notation "plantuml".' },
118
+ },
119
+ {
120
+ id: 'composite-structure', name: 'composite structure diagram', nameZh: '复合结构图', group: 'structure', machine: false, parseable: true, parseThroughApproximation: true,
121
+ plantuml: { fidelity: 'approximate', declarations: ['port', 'portin', 'portout'], note: 'plantuml documents no composite structure diagram; this build draws one with the deployment diagram\'s vocabulary - a nested element for the classifier, the nestable elements for its parts, `port`/`portin`/`portout` for its boundary, and the arrows for its connectors. Nesting, port direction and both connector ends are spelled, so the text reads back; a part\'s type, a port\'s interfaces, a connector\'s name and a collaboration are not expressible and do not enter the model.' },
122
+ mermaid: { fidelity: 'none', note: 'mermaid has no composite structure diagram. Render it with notation "plantuml".' },
123
+ },
124
+ {
125
+ id: 'deployment', name: 'deployment diagram', nameZh: '部署图', group: 'structure', machine: false, parseable: true,
126
+ plantuml: { fidelity: 'faithful', declarations: ['node', 'deployment', 'database', 'storage', 'cloud', 'stack'] },
127
+ mermaid: { fidelity: 'none', note: 'mermaid architecture-beta draws a service topology, not UML deployment nodes and artifacts, and this build does not emit it under the deployment name. Render it with notation "plantuml".' },
128
+ },
129
+ {
130
+ id: 'profile', name: 'profile diagram', nameZh: 'Profile 图', group: 'structure', machine: false, parseable: false,
131
+ parseRefusal: 'plantuml draws a profile with the class diagram\'s elements and has no profile element, and an extension and a stereotype generalization are both a --|> arrow: a parse would have to guess which fact the arrow carries. Keep the profile as its own model and review that.',
132
+ plantuml: { fidelity: 'approximate', declarations: ['stereotype'], note: 'plantuml has metaclass and stereotype elements and draws `class X <<stereotype>>`, but no profile element and no ProfileApplication: the profile package itself is not drawn, and an extension is drawn as the same --|> arrow as a generalization. This build draws that approximation and never reads it back.' },
133
+ mermaid: { fidelity: 'none', note: 'mermaid has no profile diagram: classDiagram draws classifiers and carries <<annotation>> markers as labels, with no metaclass, stereotype or extension element. Render it with notation "plantuml".' },
134
+ },
135
+ {
136
+ id: 'communication', name: 'communication diagram', nameZh: '通信图', group: 'interaction', machine: false, parseable: true, parseThroughApproximation: true,
137
+ plantuml: { fidelity: 'approximate', declarations: ['object'], note: 'plantuml has no communication diagram type - the page is a 404 and the navigation lists none - but this build draws one with the object diagram\'s vocabulary: objects for the lifelines, arrows for the links, and the sequence number in the arrow\'s label, `C --> O : 1: create`. Lifelines, links and the numbers read back; the nesting a dotted number like `1.1` suggests is a claim about the design, so the model keeps the number as written and says nothing about the tree.' },
138
+ mermaid: { fidelity: 'none', note: 'mermaid has no communication diagram: nothing documents lifelines-plus-links with numbered messages. Model the same interaction as a sequence diagram, or render it with notation "plantuml".' },
139
+ },
140
+ {
141
+ id: 'interaction-overview', name: 'interaction overview diagram', nameZh: '交互概览图', group: 'interaction', machine: false, parseable: false,
142
+ parseRefusal: 'an interaction overview is drawn here as a mermaid flowchart whose nodes name interactions, 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.',
143
+ mermaid: { fidelity: 'approximate', declarations: ['flowchart'], note: 'mermaid has no interaction overview diagram, but this build draws one as a flowchart whose nodes name interactions and whose edges carry the branch conditions - the flow, the references and the conditions are all carried. The combined fragments of an inline interaction are not drawn: the drawing has one node for it.' },
144
+ plantuml: { fidelity: 'none', note: 'plantuml has no interaction overview page, and the one place a `ref` step exists - its activity syntax - is refused by this build, because a structured flowchart language cannot carry a graph with merges or cycles. Render it with notation "mermaid".' },
145
+ },
146
+ {
147
+ id: 'timing', name: 'timing diagram', nameZh: '定时图', group: 'interaction', machine: false, parseable: true,
148
+ plantuml: { fidelity: 'faithful', declarations: ['robust', 'concise', 'binary', 'clock'] },
149
+ mermaid: { fidelity: 'none', note: 'mermaid has no timing diagram. Render it with notation "plantuml", or model the deadline as a LogicModelV1 with maxTicks and tickEvents and let model_verify check it.' },
150
+ },
151
+ ];
152
+ export const UML_FAMILY_IDS = UML_FAMILIES.map((family) => family.id);
153
+ export function isUmlFamilyId(value) {
154
+ return typeof value === 'string' && UML_FAMILY_IDS.includes(value);
155
+ }
156
+ export function umlFamilySpec(id) {
157
+ const spec = UML_FAMILIES.find((family) => family.id === id);
158
+ if (spec === undefined)
159
+ throw new UmlError('unknown diagram family "' + String(id) + '"; expected ' + UML_FAMILY_IDS.join(' | '));
160
+ return spec;
161
+ }
162
+ /** What one notation can do with one family. `none` when the table says nothing. */
163
+ export function umlNotationSupport(notation, family) {
164
+ const spec = umlFamilySpec(family);
165
+ const support = notation === 'mermaid' ? spec.mermaid : spec.plantuml;
166
+ return support ?? {
167
+ fidelity: 'none',
168
+ note: notation + ' has no ' + spec.name + ': there is no view to render and no text to read back.',
169
+ };
170
+ }
171
+ // ---------------------------------------------------------------------------
172
+ // Mermaid families that are not UML at all
173
+ // ---------------------------------------------------------------------------
174
+ /**
175
+ * Mermaid headers that name something other than a UML diagram type. Recognizing them
176
+ * matters: the text must not be reported as "cannot tell whether this is Mermaid or
177
+ * PlantUML", and it must not be parsed as a state diagram, whose every line would be
178
+ * ignored or misread. ER, gantt, mindmap and the C4 family are not UML 2.5 diagrams.
179
+ */
180
+ export const FOREIGN_MERMAID_FAMILIES = [
181
+ 'erDiagram', 'gantt', 'pie', 'journey', 'mindmap', 'gitGraph',
182
+ 'C4Context', 'C4Container', 'C4Component', 'C4Dynamic', 'C4Deployment',
183
+ 'requirementDiagram', 'timeline', 'quadrantChart', 'sankey-beta', 'block-beta',
184
+ 'packet-beta', 'radar-beta', 'treemap-beta', 'xychart-beta',
185
+ ];
186
+ /** The non-UML Mermaid family this text declares, if any. */
187
+ export function foreignMermaidFamily(text) {
188
+ for (const family of FOREIGN_MERMAID_FAMILIES) {
189
+ if (new RegExp('^\\s*' + escapeRegExp(family) + '\\b', 'm').test(text))
190
+ return family;
191
+ }
192
+ return undefined;
193
+ }
194
+ // ---------------------------------------------------------------------------
195
+ // Detection
196
+ // ---------------------------------------------------------------------------
197
+ /** Every Mermaid header this engine knows, for the "expected one of" message. */
198
+ export function mermaidHeaderList() {
199
+ const headers = [];
200
+ for (const spec of UML_FAMILIES)
201
+ for (const header of spec.mermaid?.headers ?? [])
202
+ headers.push('`' + header + '`');
203
+ for (const family of FOREIGN_MERMAID_FAMILIES)
204
+ headers.push('`' + family + '`');
205
+ return headers.join(' / ');
206
+ }
207
+ export function detectUmlNotation(text) {
208
+ if (/^\s*@start/m.test(text))
209
+ return 'plantuml';
210
+ for (const spec of UML_FAMILIES) {
211
+ for (const header of spec.mermaid?.headers ?? []) {
212
+ if (new RegExp('^\\s*' + escapeRegExp(header) + '\\b', 'm').test(text))
213
+ return 'mermaid';
214
+ }
215
+ }
216
+ // A known Mermaid family is still Mermaid: say so, and let family detection refuse
217
+ // it by name instead of blaming the notation.
218
+ if (foreignMermaidFamily(text) !== undefined)
219
+ return 'mermaid';
220
+ throw new UmlError('cannot tell whether this is Mermaid or PlantUML text: expected ' + mermaidHeaderList() + ', or `@startuml`');
221
+ }
222
+ /** A Mermaid header: the most specific evidence there is. */
223
+ function mermaidHeader(text) {
224
+ for (const spec of UML_FAMILIES) {
225
+ for (const header of spec.mermaid?.headers ?? []) {
226
+ if (new RegExp('^\\s*' + escapeRegExp(header) + '\\b', 'm').test(text))
227
+ return { family: spec.id, evidence: header };
228
+ }
229
+ }
230
+ return undefined;
231
+ }
232
+ function plantumlDeclaration(text, family) {
233
+ for (const keyword of umlFamilySpec(family).plantuml?.declarations ?? []) {
234
+ if (new RegExp('^\\s*' + escapeRegExp(keyword) + '\\b', 'im').test(text))
235
+ return keyword;
236
+ }
237
+ return undefined;
238
+ }
239
+ /**
240
+ * Use case syntax that carries no keyword: `:Actor:`, `(UseCase)`, and either of them
241
+ * with a relation on the same line (`:User: --> (Login)`). The node reference has to be
242
+ * recognized inside the relation line too, or the most common hand-written form would
243
+ * fall through to the state parser - which is the defect this engine exists to fix.
244
+ */
245
+ const PLANTUML_ACTOR_REF = /^\s*:[^:\n]+:(?:\s+as\s+[A-Za-z_][A-Za-z0-9_]*)?\s*(?:$|<\|--|--\|>|\.\.>|\.>|-->|->|--)/;
246
+ const PLANTUML_USE_CASE_REF = /^\s*\([^()\n]+\)(?:\s+as\s+\(?[^()\n]+\)?)?\s*(?:$|<\|--|--\|>|\.\.>|\.>|-->|->|--)/;
247
+ export function plantumlUseCaseSyntax(text) {
248
+ return text.split(/\r?\n/).some((line) => PLANTUML_ACTOR_REF.test(line) || PLANTUML_USE_CASE_REF.test(line));
249
+ }
250
+ /** PlantUML timing syntax: a declared timeline, or a tick. */
251
+ const PLANTUML_TIMING_LINE = /^\s*(?:@\d+|\d+\s+is\s+|(?:robust|concise|binary|clock)\s+)/;
252
+ export function plantumlTimingSyntax(text) {
253
+ return text.split(/\r?\n/).some((line) => PLANTUML_TIMING_LINE.test(line));
254
+ }
255
+ /** Profile markers: a profile is drawn as a class diagram carrying these stereotypes. */
256
+ const PLANTUML_PROFILE_MARKER = /<<\s*(stereotype|metaclass)\s*>>/i;
257
+ /**
258
+ * Composite structure syntax: a port declaration.
259
+ *
260
+ * `port`, `portin` and `portout` exist nowhere else in PlantUML, so one of them inside a
261
+ * block says what the diagram is. Without this the text is read as a component diagram -
262
+ * the classifier is a component and the ports become components beside it - which is a
263
+ * different model that happens to parse.
264
+ */
265
+ const PLANTUML_COMPOSITE_PORT = /^\s*(?:port|portin|portout)\s+[A-Za-z_][A-Za-z0-9_]*\s*$/im;
266
+ export function plantumlCompositeSyntax(text) {
267
+ return PLANTUML_COMPOSITE_PORT.test(text);
268
+ }
269
+ /**
270
+ * ER-diagram syntax: a crow's-foot cardinality marker on a link.
271
+ *
272
+ * An entity-relationship diagram is not a UML 2.5 diagram family, so this plugin does not
273
+ * model one. It has to be *refused* rather than read, because PlantUML's ER vocabulary
274
+ * overlaps the class diagram's: `USER ||--o{ ORDER` - a cardinality, not a node - becomes
275
+ * two components called `USER ||` and `o{ ORDER`, and the result is a graph that looks
276
+ * reviewed while saying nothing about the schema.
277
+ *
278
+ * The marker is the only signal that is not ambiguous. A raised `entity X { ... }` block
279
+ * is *not*: `entity` is a class-diagram keyword too, its members are spelled the same way,
280
+ * and its `--` separator is also valid inside a class. Keying on that would refuse a class
281
+ * diagram that happens to use the keyword.
282
+ */
283
+ const PLANTUML_ER_CARDINALITY = /(?:^|\s)(?:\|\||\}\||\}\o|\|\{|o\{|o\||\|o)(?:--|\.\.)/m;
284
+ export function plantumlErSyntax(text) {
285
+ return PLANTUML_ER_CARDINALITY.test(text);
286
+ }
287
+ /**
288
+ * Communication-diagram syntax: an arrow whose label is a sequence number.
289
+ *
290
+ * `C --> O : 1: create` is the whole tell. An object diagram's arrows carry names
291
+ * (`C --> O : owns`), and only a communication diagram numbers them. Without this the text
292
+ * reads as an object diagram - which parses, and which drops every number into a label
293
+ * nobody checks.
294
+ */
295
+ const PLANTUML_COMMUNICATION_ARROW = /^\s*\S+\s*(?:-->|->|--|\.\.>|\.\.)\s*\S+\s*:\s*\d+(?:\.\d+)*[a-zA-Z]?\s*:/m;
296
+ export function plantumlCommunicationSyntax(text) {
297
+ return PLANTUML_COMMUNICATION_ARROW.test(text);
298
+ }
299
+ function sequenceMarkers(text) {
300
+ return /^\s*participant\b/m.test(text) || /->>\s*/.test(text);
301
+ }
302
+ /**
303
+ * Which UML diagram type this text is, in this notation.
304
+ *
305
+ * Total by construction: every branch either names a family or throws. A foreign
306
+ * Mermaid family (ER, gantt, mindmap, C4, ...) is refused by name, so the caller can
307
+ * report *what* the text is instead of blaming the notation.
308
+ */
309
+ export function detectUmlFamily(text, notation, options = {}) {
310
+ if (options.hint !== undefined)
311
+ return { family: options.hint, evidence: 'dsh-model-everything: directive' };
312
+ if (notation === 'mermaid') {
313
+ const header = mermaidHeader(text);
314
+ if (header !== undefined)
315
+ return header;
316
+ const foreign = foreignMermaidFamily(text);
317
+ if (foreign !== undefined) {
318
+ throw new UmlError('`' + foreign + '` is not a UML diagram type: this plugin models the UML 2.5 diagram families, so this text cannot be read into one of them. Discarded: ' + foreign + ' x1. Render the diagram you mean with uml action=render and keep this one as its own view.', 'UML_NOT_A_STATE_DIAGRAM');
319
+ }
320
+ throw new UmlError('cannot tell which UML diagram type this Mermaid text declares: expected a header such as ' + mermaidHeaderList());
321
+ }
322
+ // PlantUML declares most families by a keyword, and a few by their syntax alone.
323
+ // The keyword-free syntaxes come first: a use case diagram has no header at all.
324
+ if (plantumlUseCaseSyntax(text))
325
+ return { family: 'use-case', evidence: 'use case syntax (:Actor: / (UseCase))' };
326
+ if (PLANTUML_PROFILE_MARKER.test(text))
327
+ return { family: 'profile', evidence: 'profile marker (stereotype / metaclass)' };
328
+ // An ER diagram is not one of the fourteen UML 2.5 families, so it is refused before any
329
+ // family is picked: its syntax overlaps the class and component diagrams closely enough
330
+ // that reading it produces a wrong graph rather than an error.
331
+ if (plantumlErSyntax(text)) {
332
+ throw new UmlError('this is an entity-relationship diagram, which is not a UML 2.5 diagram family: this plugin models the fourteen UML diagram types. Its notation overlaps the class and component diagrams - `USER ||--o{ ORDER` is a cardinality, not a node - so reading it would report on a graph the diagram does not draw. Review the schema with dsh-model-everything-datamodel instead.', 'UML_NOT_A_STATE_DIAGRAM');
333
+ }
334
+ // A port declaration outranks a nested-element keyword: a composite structure is drawn
335
+ // with a component block, and the ports are what make it that diagram rather than a
336
+ // component diagram.
337
+ if (plantumlCompositeSyntax(text))
338
+ return { family: 'composite-structure', evidence: 'composite syntax (port / portin / portout)' };
339
+ // A numbered arrow outranks the object declaration that carries it: the numbering is what
340
+ // makes the diagram a communication diagram.
341
+ if (plantumlCommunicationSyntax(text))
342
+ return { family: 'communication', evidence: 'numbered message labels (C --> O : 1: create)' };
343
+ if (plantumlTimingSyntax(text))
344
+ return { family: 'timing', evidence: 'timing syntax (robust/concise/binary/clock or @tick)' };
345
+ // Declaration keywords, most specific family first. component before package
346
+ // matters: a component diagram may contain packages, and the component is the topic.
347
+ const order = ['class', 'object', 'component', 'deployment', 'package', 'sequence', 'activity'];
348
+ for (const family of order) {
349
+ const keyword = plantumlDeclaration(text, family);
350
+ if (keyword === undefined)
351
+ continue;
352
+ // actor and database are lifelines in a sequence diagram: the sequence markers
353
+ // win when both are present.
354
+ if (family !== 'sequence' && sequenceMarkers(text))
355
+ return { family: 'sequence', evidence: 'sequence markers (participant / ->>)' };
356
+ return { family, evidence: 'declaration: ' + keyword };
357
+ }
358
+ if (sequenceMarkers(text))
359
+ return { family: 'sequence', evidence: 'sequence markers (participant / ->>)' };
360
+ // PlantUML state diagrams declare no kind: [*] --> A is the whole header. That is the
361
+ // default, and it is now reached only after every other family said no.
362
+ return { family: 'state', evidence: 'state syntax ([*], state, -->)' };
363
+ }
364
+ /**
365
+ * Whether this (notation, family, action) cell exists.
366
+ *
367
+ * This is the notation's answer, not this build's: a family the table says is faithful
368
+ * is still refused when no module implements it, and that refusal belongs to the
369
+ * registry (see `uml.ts`). Keeping the two apart is what lets the engine document
370
+ * all fourteen diagram types while the implementation lands family by family.
371
+ */
372
+ export function umlCapability(notation, family, action, options = {}) {
373
+ if (!isUmlNotation(notation)) {
374
+ return { ok: false, errorCode: 'UML_INPUT', message: 'unknown notation "' + String(notation) + '"; expected ' + UML_NOTATIONS.join(' | ') };
375
+ }
376
+ if (!isUmlFamilyId(family)) {
377
+ return { ok: false, errorCode: 'UML_INPUT', message: 'unknown diagram "' + String(family) + '"; expected ' + UML_FAMILY_IDS.join(' | ') };
378
+ }
379
+ if (action === 'labels')
380
+ return { ok: true };
381
+ const spec = umlFamilySpec(family);
382
+ const support = umlNotationSupport(notation, family);
383
+ if (action === 'render') {
384
+ if (support.fidelity === 'none') {
385
+ return { ok: false, errorCode: 'UML_VIEW_UNREPRESENTABLE', message: support.note ?? (notation + ' cannot draw a ' + spec.name) };
386
+ }
387
+ if (support.fidelity === 'approximate') {
388
+ return { ok: true, warning: 'UML_RENDER_APPROXIMATE_VIEW: ' + (support.note ?? (notation + ' draws an approximation of a ' + spec.name)) };
389
+ }
390
+ return { ok: true };
391
+ }
392
+ if (options.requireParse === false)
393
+ return { ok: true };
394
+ if (!spec.parseable) {
395
+ return { ok: false, errorCode: spec.parseRefusalCode ?? 'UML_NOT_A_STATE_DIAGRAM', message: spec.parseRefusal ?? ('a ' + spec.name + ' cannot be read back into a model') };
396
+ }
397
+ if (support.fidelity !== 'faithful') {
398
+ if (support.fidelity === 'none')
399
+ return { ok: false, errorCode: 'UML_VIEW_UNREPRESENTABLE', message: support.note ?? (notation + ' cannot draw a ' + spec.name) };
400
+ // An approximation is readable only when the family says so. The families that do are
401
+ // the ones whose round trip has been measured; the rest refuse rather than guess.
402
+ if (spec.parseThroughApproximation === true)
403
+ return { ok: true, warning: 'UML_PARSE_APPROXIMATE_VIEW: ' + (support.note ?? (notation + ' draws an approximation of a ' + spec.name)) };
404
+ return {
405
+ ok: false,
406
+ errorCode: 'UML_VIEW_UNREPRESENTABLE',
407
+ message: 'reading a ' + spec.name + ' back out of ' + notation + ' would produce a model of a different diagram: ' + (support.note ?? 'the notation only approximates it') + ' Render the diagram instead, and keep the model in the form the notation can carry.',
408
+ };
409
+ }
410
+ return { ok: true };
411
+ }
412
+ /** `umlCapability`, as a throw. */
413
+ export function assertUmlCapability(notation, family, action) {
414
+ const capability = umlCapability(notation, family, action);
415
+ if (!capability.ok)
416
+ throw new UmlError(capability.message, capability.errorCode);
417
+ return capability;
418
+ }
419
+ // ---------------------------------------------------------------------------
420
+ // Directives
421
+ // ---------------------------------------------------------------------------
422
+ /** Marker every generated diagram carries; renderers ignore it, the parser uses it. */
423
+ export const DIRECTIVE_NAMESPACE = 'dsh-model-everything:';
424
+ /** The directive body of a comment line, or null when the line is not a directive. */
425
+ export function directiveBody(line, notation) {
426
+ const prefix = commentPrefix(notation);
427
+ const trimmed = line.trim();
428
+ if (!trimmed.startsWith(prefix))
429
+ return null;
430
+ const body = trimmed.slice(prefix.length).trim();
431
+ if (!body.startsWith(DIRECTIVE_NAMESPACE))
432
+ return null;
433
+ return body.slice(DIRECTIVE_NAMESPACE.length);
434
+ }
435
+ /** One directive line, in the notation's own comment syntax. */
436
+ export function directiveLine(notation, text) {
437
+ return commentPrefix(notation) + DIRECTIVE_NAMESPACE + text;
438
+ }
439
+ /**
440
+ * Read the `dsh-model-everything:` directives out of a diagram.
441
+ *
442
+ * `diagram=` is the pre-family spelling and is still read: a file rendered by an
443
+ * earlier version must parse the same way it did.
444
+ */
445
+ export function readUmlDirectives(lines, notation) {
446
+ const declared = {};
447
+ const values = new Map();
448
+ const body = [];
449
+ for (const line of lines) {
450
+ const text = directiveBody(line, notation);
451
+ if (text === null) {
452
+ body.push(line);
453
+ continue;
454
+ }
455
+ if (text.startsWith('uml ')) {
456
+ const notationMatch = /notation=([a-z]+)/.exec(text);
457
+ const familyMatch = /family=([a-z-]+)/.exec(text);
458
+ const diagramMatch = /diagram=([a-z-]+)/.exec(text);
459
+ if (notationMatch !== null)
460
+ declared.notation = notationMatch[1];
461
+ if (familyMatch !== null)
462
+ declared.family = familyMatch[1];
463
+ if (diagramMatch !== null)
464
+ declared.diagram = diagramMatch[1];
465
+ continue;
466
+ }
467
+ const split = text.indexOf(' ');
468
+ const key = split < 0 ? text.trim() : text.slice(0, split).trim();
469
+ const value = split < 0 ? '' : text.slice(split + 1).trim();
470
+ if (key === '')
471
+ continue;
472
+ const list = values.get(key);
473
+ if (list === undefined)
474
+ values.set(key, [value]);
475
+ else
476
+ list.push(value);
477
+ }
478
+ return { declared, values, body };
479
+ }
480
+ /** The family a rendered diagram declares, in either spelling. */
481
+ export function declaredFamily(directives) {
482
+ const raw = directives.declared.family ?? directives.declared.diagram;
483
+ if (raw === undefined)
484
+ return undefined;
485
+ return isUmlFamilyId(raw) ? raw : undefined;
486
+ }
487
+ /**
488
+ * The content lines of a diagram: blank lines, comments and notation chrome removed,
489
+ * each with its source line number. Chrome is what a renderer needs and a reader does
490
+ * not: `@startuml`, `@enduml`, `direction`, `classDef`, `skinparam`,
491
+ * `scale`, `title`, `hide` and `autonumber`.
492
+ */
493
+ export function umlSourceLines(text, notation, skip) {
494
+ const chrome = notation === 'mermaid'
495
+ ? /^(classDef|class|style|linkStyle|click|direction|accTitle|accDescr)\b/
496
+ : /^(@startuml|@enduml|scale|skinparam|title|hide|autonumber|!theme|left to right direction|top to bottom direction)\b/;
497
+ const out = [];
498
+ const lines = text.split(/\r?\n/);
499
+ const prefix = commentPrefix(notation);
500
+ for (let index = 0; index < lines.length; index += 1) {
501
+ const trimmed = lines[index].trim();
502
+ if (trimmed === '')
503
+ continue;
504
+ if (trimmed.startsWith(prefix))
505
+ continue;
506
+ if (chrome.test(trimmed))
507
+ continue;
508
+ if (skip !== undefined && skip.test(trimmed))
509
+ continue;
510
+ out.push({ number: index + 1, text: trimmed });
511
+ }
512
+ return out;
513
+ }
514
+ const MERMAID_EDGE = /^(.*?)\s*(<\|--|--\|>|\.\.\|>|<\|\.\.|-->>|->>|-->|==>|-\.->|\.\.>|---|--|\.\.)\s*(.*)$/;
515
+ const PLANTUML_EDGE = /^(.*?)\s*(<\|--|--\|>|\.\.\|>|<\|\.\.|-->|->>|->|\.\.>|\.>|==>|--|==|\.\.|\*--|o--|--\*|--o)\s*(.*)$/;
516
+ /**
517
+ * Split one line into from / operator / to, with a trailing `: label` separated.
518
+ *
519
+ * The operator tables are the notation's, not the family's: a class diagram writes
520
+ * `A <|-- B` and a use case diagram writes `A --> B` in the same notation, and both
521
+ * come through here. Whether an operator *means* a dependency, an association or a
522
+ * generalization is the family module's call.
523
+ */
524
+ export function splitUmlEdge(line, notation) {
525
+ const match = (notation === 'mermaid' ? MERMAID_EDGE : PLANTUML_EDGE).exec(line);
526
+ if (match === null)
527
+ return undefined;
528
+ const from = stripEdgeEndpoints(match[1]);
529
+ if (from === '')
530
+ return undefined;
531
+ let rest = match[3].trim();
532
+ let label;
533
+ const colon = rest.indexOf(':');
534
+ if (colon >= 0) {
535
+ label = rest.slice(colon + 1).trim().replace(/^"|"$/g, '').trim();
536
+ rest = rest.slice(0, colon).trim();
537
+ }
538
+ const to = stripEdgeEndpoints(rest);
539
+ if (to === '')
540
+ return undefined;
541
+ return { from, operator: match[2], to, ...(label === undefined || label === '' ? {} : { label }) };
542
+ }
543
+ function stripEdgeEndpoints(text) {
544
+ return text.trim().replace(/^["']|["']$/g, '').trim();
545
+ }