@meltemi/diagrams-model 0.0.0-stage → 1.9.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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -2
  3. package/dist/c4.d.ts +44 -0
  4. package/dist/c4.d.ts.map +1 -0
  5. package/dist/c4.js +803 -0
  6. package/dist/c4.js.map +1 -0
  7. package/dist/compile.d.ts +20 -0
  8. package/dist/compile.d.ts.map +1 -0
  9. package/dist/compile.js +525 -0
  10. package/dist/compile.js.map +1 -0
  11. package/dist/diagram-types.d.ts +67 -0
  12. package/dist/diagram-types.d.ts.map +1 -0
  13. package/dist/diagram-types.js +282 -0
  14. package/dist/diagram-types.js.map +1 -0
  15. package/dist/diff.d.ts +91 -0
  16. package/dist/diff.d.ts.map +1 -0
  17. package/dist/diff.js +575 -0
  18. package/dist/diff.js.map +1 -0
  19. package/dist/export.d.ts +22 -0
  20. package/dist/export.d.ts.map +1 -0
  21. package/dist/export.js +243 -0
  22. package/dist/export.js.map +1 -0
  23. package/dist/flow.d.ts +247 -0
  24. package/dist/flow.d.ts.map +1 -0
  25. package/dist/flow.js +1693 -0
  26. package/dist/flow.js.map +1 -0
  27. package/dist/index.d.ts +16 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +16 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/json.d.ts +400 -0
  32. package/dist/json.d.ts.map +1 -0
  33. package/dist/json.js +433 -0
  34. package/dist/json.js.map +1 -0
  35. package/dist/messages/changes.d.ts +64 -0
  36. package/dist/messages/changes.d.ts.map +1 -0
  37. package/dist/messages/changes.js +117 -0
  38. package/dist/messages/changes.js.map +1 -0
  39. package/dist/messages/en.d.ts +554 -0
  40. package/dist/messages/en.d.ts.map +1 -0
  41. package/dist/messages/en.js +566 -0
  42. package/dist/messages/en.js.map +1 -0
  43. package/dist/messages/es.d.ts +3 -0
  44. package/dist/messages/es.d.ts.map +1 -0
  45. package/dist/messages/es.js +551 -0
  46. package/dist/messages/es.js.map +1 -0
  47. package/dist/messages/index.d.ts +449 -0
  48. package/dist/messages/index.d.ts.map +1 -0
  49. package/dist/messages/index.js +11 -0
  50. package/dist/messages/index.js.map +1 -0
  51. package/dist/model.d.ts +11 -0
  52. package/dist/model.d.ts.map +1 -0
  53. package/dist/model.js +363 -0
  54. package/dist/model.js.map +1 -0
  55. package/dist/scenario.d.ts +11 -0
  56. package/dist/scenario.d.ts.map +1 -0
  57. package/dist/scenario.js +91 -0
  58. package/dist/scenario.js.map +1 -0
  59. package/dist/schema.d.ts +311 -0
  60. package/dist/schema.d.ts.map +1 -0
  61. package/dist/schema.js +658 -0
  62. package/dist/schema.js.map +1 -0
  63. package/dist/sequence.d.ts +8 -0
  64. package/dist/sequence.d.ts.map +1 -0
  65. package/dist/sequence.js +722 -0
  66. package/dist/sequence.js.map +1 -0
  67. package/dist/story.d.ts +18 -0
  68. package/dist/story.d.ts.map +1 -0
  69. package/dist/story.js +406 -0
  70. package/dist/story.js.map +1 -0
  71. package/dist/style-values.d.ts +107 -0
  72. package/dist/style-values.d.ts.map +1 -0
  73. package/dist/style-values.js +558 -0
  74. package/dist/style-values.js.map +1 -0
  75. package/dist/theme-block.d.ts +10 -0
  76. package/dist/theme-block.d.ts.map +1 -0
  77. package/dist/theme-block.js +135 -0
  78. package/dist/theme-block.js.map +1 -0
  79. package/dist/types.d.ts +785 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +2 -0
  82. package/dist/types.js.map +1 -0
  83. package/package.json +36 -3
package/dist/c4.js ADDED
@@ -0,0 +1,803 @@
1
+ import { isToken, } from "@meltemi/diagrams-language";
2
+ import { FlowAnalyzer } from "./flow.js";
3
+ import { messages } from "./messages/index.js";
4
+ import { closePaths, isAncestorPath } from "./model.js";
5
+ import { C4_LEVELS, C4_PROFILE, PORTS } from "./schema.js";
6
+ import { headerHasError, singleIdent } from "./style-values.js";
7
+ /** Analyzes `diagram <id>: c4 using <model> { }` into a view. */
8
+ export function analyzeC4View(node, id, lines, bag, model) {
9
+ const analyzer = new C4Analyzer(node, id, lines, bag, model);
10
+ return { diagram: analyzer.run(), errors: analyzer.errors };
11
+ }
12
+ /**
13
+ * The entities a C4 view of a level and scope may draw: persons and systems at the context level;
14
+ * the scope's containers and datastores, persons and other systems at the container level; the
15
+ * scope's components, the other containers of its system, persons and other systems at the
16
+ * component level.
17
+ */
18
+ export function allowedEntities(model, level, scopePath) {
19
+ const allowed = new Set();
20
+ const scope = scopePath === null ? null : (model.entitiesByPath.get(scopePath) ?? null);
21
+ for (const entity of model.entities) {
22
+ const kind = entity.kind;
23
+ switch (level) {
24
+ case "context":
25
+ if (kind === "person" || kind === "system")
26
+ allowed.add(entity.path);
27
+ break;
28
+ case "container":
29
+ if (kind === "person")
30
+ allowed.add(entity.path);
31
+ else if (kind === "system" && entity.path !== scope?.path)
32
+ allowed.add(entity.path);
33
+ else if ((kind === "container" || kind === "datastore") && entity.parent === scope?.path)
34
+ allowed.add(entity.path);
35
+ break;
36
+ case "component": {
37
+ const system = scope?.parent ?? null;
38
+ if (kind === "person")
39
+ allowed.add(entity.path);
40
+ else if (kind === "system" && entity.path !== system)
41
+ allowed.add(entity.path);
42
+ else if ((kind === "container" || kind === "datastore") &&
43
+ entity.parent === system &&
44
+ entity.path !== scope?.path)
45
+ allowed.add(entity.path);
46
+ else if (kind === "component" && entity.parent === scope?.path)
47
+ allowed.add(entity.path);
48
+ break;
49
+ }
50
+ }
51
+ }
52
+ return allowed;
53
+ }
54
+ /**
55
+ * The level of the view that opens an entity of a kind: a system opens a container view scoped
56
+ * to it, a container a component view scoped to it; persons, datastores and components open
57
+ * nothing.
58
+ */
59
+ export function childLevel(kind) {
60
+ if (kind === "system")
61
+ return "container";
62
+ if (kind === "container")
63
+ return "component";
64
+ return null;
65
+ }
66
+ /**
67
+ * Whether a compiled C4 view is the child view of an entity: it uses the same model (the model's
68
+ * file and id; `owner` is the view's own file, standing for a model resolved in that file), has
69
+ * the level `childLevel` gives the entity's kind, and is scoped to the entity. A context view
70
+ * scoped to a system is not its child view.
71
+ */
72
+ export function isChildView(view, owner, model, entity) {
73
+ const level = childLevel(entity.kind);
74
+ if (level === null || view.level !== level || view.scope !== entity.path)
75
+ return false;
76
+ const resolved = view.resolved;
77
+ if (!resolved || resolved.definition.id !== model.id)
78
+ return false;
79
+ return (resolved.file ?? owner) === model.file;
80
+ }
81
+ /** A token standing for a whole qualified reference: its text is the joined path. */
82
+ function wholeToken(ref) {
83
+ const first = ref.parts[0];
84
+ return {
85
+ kind: first.kind,
86
+ text: ref.parts.map((p) => p.text).join("."),
87
+ start: ref.start,
88
+ end: ref.end,
89
+ fullStart: first.fullStart,
90
+ leadingTrivia: first.leadingTrivia,
91
+ };
92
+ }
93
+ const ENTITY_TYPES = {
94
+ person: "person",
95
+ system: "system",
96
+ container: "container",
97
+ datastore: "datastore",
98
+ component: "component",
99
+ };
100
+ /** The statements of C4 views, as their hint lists them. */
101
+ const VIEW_STATEMENTS = [
102
+ "level",
103
+ "scope",
104
+ "include",
105
+ "exclude",
106
+ "label",
107
+ "direction",
108
+ "layout",
109
+ "routing",
110
+ "theme",
111
+ "title",
112
+ "description",
113
+ "preset",
114
+ "canvas",
115
+ "placement",
116
+ "style",
117
+ "animate",
118
+ "class",
119
+ "rule",
120
+ ];
121
+ class C4Analyzer extends FlowAnalyzer {
122
+ model;
123
+ definition;
124
+ level = "context";
125
+ levelToken;
126
+ scope = null;
127
+ labels = [];
128
+ labelStatements = [];
129
+ includes = null;
130
+ excludes = [];
131
+ includeNode = null;
132
+ excludeNode = null;
133
+ combined = [];
134
+ /** Tokens of `include` entries by path, which stand for the elements in this file. */
135
+ includeTokens = new Map();
136
+ constructor(node, id, lines, bag, model) {
137
+ super(node, id, lines, bag, C4_PROFILE);
138
+ this.model = model;
139
+ this.definition = model.resolved?.definition ?? null;
140
+ }
141
+ // ---------------------------------------------------------------- entity references
142
+ entityRef(token, path, role) {
143
+ this.entityReferences.push({ token, model: this.model.name, path, role });
144
+ }
145
+ /** The entity a qualified reference names, from the model's root, with a reference per part. */
146
+ entity(ref, role, report = true) {
147
+ const model = this.definition;
148
+ const path = ref.parts.map((p) => p.text).join(".");
149
+ const entity = model.entitiesByPath.get(path);
150
+ if (!entity) {
151
+ if (report) {
152
+ const close = closePaths(path, model.entitiesByPath.keys());
153
+ const m = messages();
154
+ this.error("ref.unknown-entity", m.diagnostics.common.unknownEntity(path, model.id), ref, {
155
+ data: { candidates: close },
156
+ hints: [close.length > 0 ? m.hints.didYouMean(close) : m.hints.c4.entityPath],
157
+ });
158
+ }
159
+ return undefined;
160
+ }
161
+ ref.parts.forEach((part, i) => {
162
+ this.entityRef(part, ref.parts
163
+ .slice(0, i + 1)
164
+ .map((p) => p.text)
165
+ .join("."), role);
166
+ });
167
+ return entity;
168
+ }
169
+ targetToken(arg, role) {
170
+ if (arg?.kind !== "QualifiedRef")
171
+ return undefined;
172
+ const path = arg.parts.map((p) => p.text).join(".");
173
+ // Relation ids are single words; entity paths may have several parts.
174
+ if (this.elementsById.has(path) && this.definition)
175
+ this.entity(arg, role, false);
176
+ return arg.parts.length === 1 ? arg.parts[0] : wholeToken(arg);
177
+ }
178
+ readEndpoint(ref) {
179
+ // The longest prefix that is an element of the view; one more part is a port.
180
+ for (let n = ref.parts.length; n >= 1; n--) {
181
+ const path = ref.parts
182
+ .slice(0, n)
183
+ .map((p) => p.text)
184
+ .join(".");
185
+ const element = this.elementsById.get(path);
186
+ if (!element)
187
+ continue;
188
+ const rest = ref.parts.slice(n);
189
+ if (rest.length > 1)
190
+ break;
191
+ const portToken = rest[0];
192
+ let port = "auto";
193
+ let named = false;
194
+ if (portToken) {
195
+ if (PORTS.includes(portToken.text))
196
+ port = portToken.text;
197
+ else if (element.ports.some((p) => p.name === portToken.text)) {
198
+ port = portToken.text;
199
+ named = true;
200
+ }
201
+ else {
202
+ const m = messages();
203
+ this.error("placement.unknown-port", m.diagnostics.common.unknownPort(path, portToken.text), portToken, { hints: [m.hints.c4.ports(PORTS)], data: { ids: [path, portToken.text] } });
204
+ return undefined;
205
+ }
206
+ }
207
+ const partial = { ...ref, parts: ref.parts.slice(0, n) };
208
+ if (this.definition)
209
+ this.entity(partial, "placement", false);
210
+ return {
211
+ endpoint: { element: path, port, named, explicitPort: portToken !== undefined },
212
+ token: n === 1 ? ref.parts[0] : wholeToken(partial),
213
+ };
214
+ }
215
+ const m = messages();
216
+ this.error("ref.unknown-element", m.diagnostics.c4.notDrawn(ref.text), ref, {
217
+ hints: [m.hints.c4.routeEnds],
218
+ data: { ids: [ref.text] },
219
+ });
220
+ return undefined;
221
+ }
222
+ resolveElement(token, statement, stmt) {
223
+ if (this.elementsById.has(token.text) || this.relationsByKey.has(token.text)) {
224
+ return super.resolveElement(token, statement, stmt);
225
+ }
226
+ this.error("ref.unknown-element", messages().diagnostics.c4.notDrawn(token.text), token, {
227
+ hints: [this.notDrawnHint(token.text)],
228
+ data: { ids: [token.text] },
229
+ });
230
+ return undefined;
231
+ }
232
+ /** Why a path is not an element of the view, as a hint in the interface language. */
233
+ notDrawnHint(path) {
234
+ const hints = messages().hints.c4;
235
+ const entity = this.definition?.entitiesByPath.get(path);
236
+ if (!entity)
237
+ return hints.nameEntity;
238
+ if (this.allowed().has(path))
239
+ return hints.include(path);
240
+ return hints.notAtLevel(path, entity.kind, this.level);
241
+ }
242
+ // ---------------------------------------------------------------- levels and sets
243
+ /** The entities a view of this level and scope may draw. */
244
+ allowed() {
245
+ return allowedEntities(this.definition, this.level, this.scope?.path ?? null);
246
+ }
247
+ /** The nearest ancestor-or-self of a path in a set. */
248
+ lift(path, set) {
249
+ let current = path;
250
+ const model = this.definition;
251
+ while (current !== null) {
252
+ if (set.has(current))
253
+ return current;
254
+ current = model.entitiesByPath.get(current)?.parent ?? null;
255
+ }
256
+ return undefined;
257
+ }
258
+ /** The default set: everything at context without scope; else the core and its neighbours. */
259
+ defaults(allowed) {
260
+ const model = this.definition;
261
+ const scope = this.scope;
262
+ if (this.level === "context" && !scope)
263
+ return new Set(allowed);
264
+ // Without its scope a container or component view has no core; its error is reported.
265
+ if (!scope)
266
+ return new Set();
267
+ const core = new Set();
268
+ if (this.level === "context")
269
+ core.add(scope.path);
270
+ else
271
+ for (const path of allowed)
272
+ if (model.entitiesByPath.get(path).parent === scope.path)
273
+ core.add(path);
274
+ const set = new Set(core);
275
+ for (const relation of model.relations) {
276
+ const a = this.lift(relation.from, allowed);
277
+ const b = this.lift(relation.to, allowed);
278
+ if (!a || !b || a === b)
279
+ continue;
280
+ if (core.has(a))
281
+ set.add(b);
282
+ if (core.has(b))
283
+ set.add(a);
284
+ }
285
+ return set;
286
+ }
287
+ // ---------------------------------------------------------------- statements
288
+ readLevel(stmt) {
289
+ const word = singleIdent(stmt.args[0]);
290
+ if (!word ||
291
+ stmt.args.length > 1 ||
292
+ stmt.block ||
293
+ !C4_LEVELS.includes(word.text)) {
294
+ this.error("c4.invalid-level", messages().diagnostics.c4.invalidLevel, stmt.args[0] ?? stmt.keyword);
295
+ return;
296
+ }
297
+ this.level = word.text;
298
+ this.levelToken = stmt.keyword;
299
+ }
300
+ readList(stmt, which, allowed) {
301
+ const node = which === "include" ? this.includeNode : this.excludeNode;
302
+ if (node) {
303
+ const m = messages();
304
+ this.error("prop.duplicate", m.diagnostics.c4.listTwice(which), stmt.keyword, {
305
+ related: [{ message: m.related.firstWritten, at: node.keyword }],
306
+ });
307
+ return;
308
+ }
309
+ if (which === "include")
310
+ this.includeNode = stmt;
311
+ else
312
+ this.excludeNode = stmt;
313
+ if (stmt.block || stmt.chain || stmt.args.length === 0) {
314
+ this.invalid(messages().diagnostics.c4.listShape(which), stmt.block ?? stmt.chain ?? stmt.keyword);
315
+ return;
316
+ }
317
+ const entries = [];
318
+ const seen = new Set();
319
+ for (const arg of stmt.args) {
320
+ if (arg.kind !== "QualifiedRef") {
321
+ this.invalid(messages().diagnostics.c4.listByPath(which), arg);
322
+ continue;
323
+ }
324
+ const entity = this.entity(arg, which);
325
+ if (!entity)
326
+ continue;
327
+ if (seen.has(entity.path)) {
328
+ this.invalid(messages().diagnostics.c4.listedTwice(entity.path, which), arg);
329
+ continue;
330
+ }
331
+ seen.add(entity.path);
332
+ if (which === "include" && !allowed.has(entity.path)) {
333
+ const boundary = entity.path === this.scope?.path;
334
+ const m = messages();
335
+ this.error("c4.out-of-level", boundary
336
+ ? m.diagnostics.c4.scopeIncluded(entity.path, this.level)
337
+ : m.diagnostics.c4.outOfLevel(this.level, entity.kind, entity.path), arg, { hints: [boundary ? m.hints.c4.removeScope(entity.path) : this.levelHint()] });
338
+ continue;
339
+ }
340
+ entries.push({ path: entity.path, range: this.range(arg) });
341
+ if (which === "include") {
342
+ this.includeTokens.set(entity.path, arg.parts.length === 1 ? arg.parts[0] : wholeToken(arg));
343
+ }
344
+ }
345
+ if (which === "include")
346
+ this.includes = entries;
347
+ else
348
+ this.excludes.push(...entries);
349
+ }
350
+ /** What a view of this level draws, as a hint in the interface language. */
351
+ levelHint() {
352
+ const hints = messages().hints.c4;
353
+ const scope = this.scope?.path ?? null;
354
+ switch (this.level) {
355
+ case "context":
356
+ return hints.contextLevel;
357
+ case "container":
358
+ return hints.containerLevel(scope);
359
+ case "component":
360
+ return hints.componentLevel(scope);
361
+ }
362
+ }
363
+ // ---------------------------------------------------------------- run
364
+ run() {
365
+ const block = this.node.block.kind === "Block" ? this.node.block : undefined;
366
+ const statements = block?.statements ?? [];
367
+ const properties = [];
368
+ const blocks = [];
369
+ const lists = [];
370
+ const relationStatements = [];
371
+ let scopeStmt;
372
+ let levelStmt;
373
+ for (const stmt of statements) {
374
+ if (stmt.kind === "Error" || headerHasError(stmt))
375
+ continue;
376
+ if (stmt.kind === "RelationStatement") {
377
+ relationStatements.push(stmt);
378
+ continue;
379
+ }
380
+ if (stmt.kind === "ShorthandDeclaration") {
381
+ const m = messages();
382
+ this.error("prop.unknown", m.diagnostics.c4.viewDeclares, stmt.id, {
383
+ hints: [m.hints.c4.declareInModel(stmt.id.text, this.model.name)],
384
+ });
385
+ continue;
386
+ }
387
+ const keyword = stmt.keyword.text;
388
+ if (keyword === "level") {
389
+ if (levelStmt) {
390
+ this.error("prop.duplicate", messages().diagnostics.common.setMoreThanOnce("level"), stmt.keyword);
391
+ }
392
+ else
393
+ levelStmt = stmt;
394
+ }
395
+ else if (keyword === "scope") {
396
+ if (scopeStmt) {
397
+ this.error("prop.duplicate", messages().diagnostics.common.setMoreThanOnce("scope"), stmt.keyword);
398
+ }
399
+ else
400
+ scopeStmt = stmt;
401
+ }
402
+ else if (keyword === "include" || keyword === "exclude")
403
+ lists.push(stmt);
404
+ else if (keyword === "label")
405
+ this.labelStatements.push(stmt);
406
+ else if (keyword in this.profile.diagramProperties)
407
+ properties.push(stmt);
408
+ else if (this.profile.diagramBlocks.includes(keyword))
409
+ blocks.push(stmt);
410
+ else {
411
+ const m = messages();
412
+ this.error("prop.unknown", m.diagnostics.c4.unknownInView(keyword), stmt.keyword, {
413
+ data: { candidates: ENTITY_TYPES[keyword] ? [] : VIEW_STATEMENTS },
414
+ hints: [
415
+ ENTITY_TYPES[keyword]
416
+ ? m.hints.c4.entitiesInModel(this.model.name)
417
+ : m.hints.c4.viewStatements,
418
+ ],
419
+ });
420
+ }
421
+ }
422
+ for (const stmt of properties)
423
+ this.diagramProperty(stmt);
424
+ if (levelStmt)
425
+ this.readLevel(levelStmt);
426
+ else {
427
+ const m = messages();
428
+ this.error("c4.invalid-level", m.diagnostics.c4.missingLevel, this.node.keyword, {
429
+ hints: [m.hints.c4.addLevel],
430
+ });
431
+ }
432
+ if (this.definition) {
433
+ this.readScope(scopeStmt, levelStmt);
434
+ const allowed = this.allowed();
435
+ for (const stmt of lists)
436
+ this.readList(stmt, stmt.keyword.text, allowed);
437
+ const visible = this.includes
438
+ ? new Set(this.includes.map((e) => e.path))
439
+ : this.defaults(allowed);
440
+ for (const entry of this.excludes)
441
+ visible.delete(entry.path);
442
+ this.buildElements(visible);
443
+ this.buildRelations(relationStatements);
444
+ this.readLabels();
445
+ }
446
+ // Placement and styles name the elements and relations built above; a view of a model that
447
+ // is not resolved has none, and stays silent.
448
+ for (const stmt of blocks) {
449
+ const keyword = stmt.keyword.text;
450
+ if (!this.definition && keyword !== "class" && keyword !== "rule")
451
+ continue;
452
+ if (keyword === "placement")
453
+ this.placementBlock(stmt);
454
+ else if (keyword === "class")
455
+ this.definitions.addClass(stmt);
456
+ else if (keyword === "rule")
457
+ this.definitions.addRule(stmt);
458
+ else
459
+ this.styleOrAnimate(stmt, keyword);
460
+ }
461
+ const base = this.diagram(block);
462
+ return {
463
+ ...base,
464
+ c4: {
465
+ model: this.model.name,
466
+ modelRange: this.range(this.model.ref),
467
+ resolved: this.model.resolved,
468
+ level: this.level,
469
+ scope: this.scope?.path ?? null,
470
+ includes: this.includes,
471
+ excludes: this.excludes,
472
+ includeNode: this.includeNode,
473
+ excludeNode: this.excludeNode,
474
+ labels: this.labels,
475
+ combined: this.combined,
476
+ },
477
+ // A view of a model that is not resolved draws nothing; the project reports its import.
478
+ hasErrors: base.hasErrors || this.definition === null,
479
+ };
480
+ }
481
+ /** The diagram the base analyser would return, from the state built here. */
482
+ diagram(block) {
483
+ return {
484
+ type: "c4",
485
+ id: this.id.text,
486
+ range: this.range(this.node),
487
+ idRange: this.range(this.id),
488
+ node: this.node,
489
+ block,
490
+ properties: { ...this.properties, layers: [...this.properties.layers] },
491
+ elements: this.elements,
492
+ relations: this.relations,
493
+ placement: this.placement,
494
+ constraints: this.constraints,
495
+ styles: this.styles,
496
+ animations: this.animations,
497
+ classes: this.definitions.classes,
498
+ rules: this.definitions.rules,
499
+ icons: [...this.icons, ...this.definitions.icons].sort((a, b) => a.range.start.offset - b.range.start.offset),
500
+ images: [],
501
+ links: [],
502
+ layerRanges: this.layerRanges,
503
+ events: [],
504
+ eventsByKey: new Map(),
505
+ scenarios: [],
506
+ references: this.references,
507
+ c4: null,
508
+ entityReferences: this.entityReferences,
509
+ placementBlocks: this.placementBlocks,
510
+ elementsById: this.elementsById,
511
+ relationsByKey: this.relationsByKey,
512
+ definition: null,
513
+ hasSyntaxErrors: this.node.hasError,
514
+ hasErrors: this.node.hasError || this.errors > 0,
515
+ };
516
+ }
517
+ readScope(stmt, levelStmt) {
518
+ if (!stmt) {
519
+ if (this.level !== "context" && levelStmt && this.levelToken) {
520
+ const m = messages();
521
+ this.error("c4.scope-required", m.diagnostics.c4.scopeRequired(this.level), levelStmt, {
522
+ hints: [this.level === "container" ? m.hints.c4.systemScope : m.hints.c4.containerScope],
523
+ });
524
+ }
525
+ return;
526
+ }
527
+ const [arg, extra] = stmt.args;
528
+ if (arg?.kind !== "QualifiedRef" || extra || stmt.block) {
529
+ this.invalid(messages().diagnostics.c4.scopeShape, extra ?? arg ?? stmt.keyword);
530
+ return;
531
+ }
532
+ const entity = this.entity(arg, "scope");
533
+ if (!entity)
534
+ return;
535
+ const expected = this.level === "component" ? "container" : "system";
536
+ if (entity.kind !== expected) {
537
+ const m = messages().diagnostics.c4;
538
+ this.error("c4.invalid-scope", entity.kind === "datastore" && this.level === "component"
539
+ ? m.datastoreScope(entity.path)
540
+ : m.invalidScope(this.level, expected, entity.path, entity.kind), arg);
541
+ return;
542
+ }
543
+ this.scope = entity;
544
+ }
545
+ /** The token that stands for an element in this file. */
546
+ tokenFor(entity) {
547
+ const included = this.includeTokens.get(entity.path);
548
+ if (included)
549
+ return included;
550
+ if (this.model.resolved?.file === null)
551
+ return entity.idToken;
552
+ return wholeToken(this.model.ref);
553
+ }
554
+ buildElements(visible) {
555
+ const model = this.definition;
556
+ const scope = this.scope;
557
+ const boundary = scope !== null &&
558
+ this.level !== "context" &&
559
+ [...visible].some((p) => model.entitiesByPath.get(p).parent === scope.path);
560
+ const local = this.model.resolved?.file === null;
561
+ const add = (entity, kind, groupPath) => {
562
+ const token = this.tokenFor(entity);
563
+ const inFile = this.includeTokens.has(entity.path) || local;
564
+ this.addElement({
565
+ id: entity.path,
566
+ kind: kind,
567
+ label: entity.label,
568
+ implicit: false,
569
+ description: entity.description,
570
+ url: entity.url,
571
+ classes: [],
572
+ ports: [],
573
+ layer: null,
574
+ layerRange: null,
575
+ link: null,
576
+ source: null,
577
+ sourceRange: null,
578
+ labelWritten: true,
579
+ entity: { model: model.id, path: entity.path, file: this.model.resolved?.file ?? null },
580
+ technology: entity.technology,
581
+ external: entity.external,
582
+ shape: null,
583
+ typeKind: null,
584
+ data: {},
585
+ groupPath,
586
+ range: inFile && this.includeTokens.has(entity.path)
587
+ ? this.range(token)
588
+ : local
589
+ ? entity.range
590
+ : this.range(token),
591
+ idRange: this.range(token),
592
+ idToken: token,
593
+ node: undefined,
594
+ });
595
+ if (this.includeTokens.has(entity.path)) {
596
+ this.reference(token, entity.path, "element", "declaration", this.includeNode);
597
+ }
598
+ };
599
+ if (boundary)
600
+ add(scope, "group", []);
601
+ for (const entity of model.entities) {
602
+ if (!visible.has(entity.path) || (boundary && entity.path === scope.path))
603
+ continue;
604
+ add(entity, entity.kind, boundary && entity.parent === scope.path ? [scope.path] : []);
605
+ }
606
+ }
607
+ buildRelations(statements) {
608
+ const model = this.definition;
609
+ // The boundary stands for its children; relations land on drawn entities only.
610
+ const visible = new Set(this.elements.filter((e) => e.kind !== "group").map((e) => e.id));
611
+ // Explicit combinations first: they claim their pairs.
612
+ const claims = new Map();
613
+ const ids = new Map();
614
+ for (const stmt of statements) {
615
+ if (stmt.target.kind !== "QualifiedRef")
616
+ continue;
617
+ const ends = [];
618
+ for (const ref of [stmt.source, stmt.target]) {
619
+ const path = ref.parts.map((p) => p.text).join(".");
620
+ if (!visible.has(path)) {
621
+ this.error("ref.unknown-element", messages().diagnostics.c4.notDrawn(path), ref, {
622
+ hints: [this.notDrawnHint(path)],
623
+ data: { ids: [path] },
624
+ });
625
+ continue;
626
+ }
627
+ this.entity(ref, "relation", false);
628
+ this.reference(ref.parts.length === 1 ? ref.parts[0] : wholeToken(ref), path, "element", "relation-endpoint", stmt);
629
+ ends.push(path);
630
+ }
631
+ if (ends.length !== 2)
632
+ continue;
633
+ const [from, to] = ends;
634
+ const pair = `${from}->${to}`;
635
+ if (claims.has(pair)) {
636
+ this.invalid(messages().diagnostics.c4.alreadyCombined(from, to), stmt);
637
+ continue;
638
+ }
639
+ const idToken = stmt.id && isToken(stmt.id) ? stmt.id : undefined;
640
+ let id = null;
641
+ if (idToken) {
642
+ if (ids.has(idToken.text) || model.relationsByKey.has(idToken.text)) {
643
+ this.error("ref.duplicate-id", messages().diagnostics.c4.relationIdUsed(idToken.text), idToken);
644
+ }
645
+ else {
646
+ ids.set(idToken.text, idToken);
647
+ id = idToken.text;
648
+ }
649
+ }
650
+ const { description, values } = this.elementBlock(stmt.block, this.profile.relationProperties, "Relation", undefined);
651
+ const relation = {
652
+ key: "",
653
+ id,
654
+ from,
655
+ to,
656
+ line: "solid",
657
+ twoWay: false,
658
+ async: false,
659
+ technology: typeof values.technology === "string" ? values.technology : null,
660
+ sources: [],
661
+ derived: true,
662
+ label: stmt.label && isToken(stmt.label) ? String(stmt.label.value ?? "") : null,
663
+ description,
664
+ assumption: typeof values.assumption === "string" ? values.assumption : null,
665
+ data: {},
666
+ classes: [],
667
+ layer: null,
668
+ layerRange: null,
669
+ groupPath: [],
670
+ range: this.range(stmt),
671
+ idRange: id !== null && idToken ? this.range(idToken) : null,
672
+ node: stmt,
673
+ };
674
+ claims.set(pair, relation);
675
+ if (id !== null && idToken)
676
+ this.reference(idToken, id, "relation", "relation-id", stmt);
677
+ }
678
+ const local = this.model.resolved?.file === null;
679
+ const usingRange = this.range(this.model.ref);
680
+ for (const relation of model.relations) {
681
+ const a = this.lift(relation.from, visible);
682
+ const b = this.lift(relation.to, visible);
683
+ if (!a || !b || a === b)
684
+ continue;
685
+ const claim = claims.get(`${a}->${b}`);
686
+ if (claim) {
687
+ claim.sources.push(relation.key);
688
+ continue;
689
+ }
690
+ this.relations.push({
691
+ key: relation.key,
692
+ id: relation.id,
693
+ from: a,
694
+ to: b,
695
+ line: "solid",
696
+ twoWay: false,
697
+ async: false,
698
+ technology: relation.technology,
699
+ sources: [relation.key],
700
+ derived: a !== relation.from || b !== relation.to,
701
+ label: relation.label,
702
+ description: relation.description,
703
+ assumption: relation.assumption,
704
+ data: {},
705
+ classes: [],
706
+ layer: null,
707
+ layerRange: null,
708
+ groupPath: [],
709
+ range: local ? relation.range : usingRange,
710
+ idRange: local ? relation.idRange : null,
711
+ node: relation.node,
712
+ });
713
+ }
714
+ const counts = new Map();
715
+ for (const [pair, claim] of claims) {
716
+ if (claim.sources.length === 0) {
717
+ const m = messages();
718
+ this.error("c4.unsupported-relation", m.diagnostics.c4.unsupportedRelation(claim.from, claim.to), claim.node, { hints: [m.hints.c4.addToModel] });
719
+ if (claim.id)
720
+ this.brokenRelationIds.add(claim.id);
721
+ continue;
722
+ }
723
+ const n = (counts.get(pair) ?? 0) + 1;
724
+ counts.set(pair, n);
725
+ claim.key = claim.id ?? `${pair}#${n}`;
726
+ // A relation that only combines assumptions is one itself.
727
+ if (claim.assumption === null) {
728
+ const combined = claim.sources.map((key) => model.relationsByKey.get(key)?.assumption ?? null);
729
+ if (combined.every((a) => a !== null))
730
+ claim.assumption = combined[0] ?? null;
731
+ }
732
+ this.relations.push(claim);
733
+ this.combined.push(claim.key);
734
+ }
735
+ for (const relation of this.relations) {
736
+ if (!this.relationsByKey.has(relation.key))
737
+ this.relationsByKey.set(relation.key, relation);
738
+ }
739
+ }
740
+ readLabels() {
741
+ const seen = new Map();
742
+ for (const stmt of this.labelStatements) {
743
+ const [target, text, extra] = stmt.args;
744
+ if (target?.kind !== "QualifiedRef" ||
745
+ text?.kind !== "StringLiteral" ||
746
+ extra ||
747
+ stmt.block) {
748
+ this.invalid(messages().diagnostics.c4.labelShape, extra ?? text ?? target ?? stmt.keyword);
749
+ continue;
750
+ }
751
+ const path = target.parts.map((p) => p.text).join(".");
752
+ const token = target.parts.length === 1 ? target.parts[0] : wholeToken(target);
753
+ const element = this.elementsById.get(path);
754
+ const relation = element ? undefined : this.relationsByKey.get(path);
755
+ if (!element && !relation) {
756
+ const known = this.definition.entitiesByPath.has(path) || this.definition.relationsByKey.has(path);
757
+ this.error(known && this.definition.relationsByKey.has(path)
758
+ ? "ref.unknown-relation"
759
+ : "ref.unknown-element", messages().diagnostics.c4.notDrawn(path), target, {
760
+ data: { ids: [path] },
761
+ hints: [
762
+ this.definition.entitiesByPath.has(path)
763
+ ? this.notDrawnHint(path)
764
+ : messages().hints.c4.localLabel,
765
+ ],
766
+ });
767
+ continue;
768
+ }
769
+ const first = seen.get(path);
770
+ if (first) {
771
+ const m = messages();
772
+ this.error("prop.duplicate", m.diagnostics.c4.labelTwice(path), stmt.keyword, {
773
+ related: [{ message: m.related.firstSet, at: first }],
774
+ });
775
+ continue;
776
+ }
777
+ seen.set(path, stmt.keyword);
778
+ if (element) {
779
+ this.entity(target, "label", false);
780
+ this.reference(token, path, "element", "label", stmt);
781
+ const index = this.elements.indexOf(element);
782
+ const updated = { ...element, label: text.value };
783
+ this.elements[index] = updated;
784
+ this.elementsById.set(path, updated);
785
+ }
786
+ else {
787
+ this.reference(token, relation.key, "relation", "label", stmt);
788
+ relation.label = text.value;
789
+ }
790
+ this.labels.push({
791
+ target: path,
792
+ targetKind: element ? "element" : "relation",
793
+ text: text.value,
794
+ range: this.range(stmt),
795
+ targetRange: this.range(target),
796
+ node: stmt,
797
+ });
798
+ }
799
+ }
800
+ classify(_statements, _groupPath, _context, _out) { }
801
+ }
802
+ export { isAncestorPath };
803
+ //# sourceMappingURL=c4.js.map