@domainlang/language 0.11.0 → 0.13.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/out/ast-augmentation.d.ts +7 -2
- package/out/diagram/context-map-diagram-generator.d.ts +72 -0
- package/out/diagram/context-map-diagram-generator.js +405 -0
- package/out/diagram/context-map-diagram-generator.js.map +1 -0
- package/out/diagram/context-map-layout-configurator.d.ts +15 -0
- package/out/diagram/context-map-layout-configurator.js +39 -0
- package/out/diagram/context-map-layout-configurator.js.map +1 -0
- package/out/diagram/elk-layout-factory.d.ts +43 -0
- package/out/diagram/elk-layout-factory.js +64 -0
- package/out/diagram/elk-layout-factory.js.map +1 -0
- package/out/domain-lang-module.d.ts +7 -0
- package/out/domain-lang-module.js +11 -2
- package/out/domain-lang-module.js.map +1 -1
- package/out/generated/ast.d.ts +323 -51
- package/out/generated/ast.js +194 -33
- package/out/generated/ast.js.map +1 -1
- package/out/generated/grammar.js +418 -172
- package/out/generated/grammar.js.map +1 -1
- package/out/index.d.ts +3 -0
- package/out/index.js +4 -0
- package/out/index.js.map +1 -1
- package/out/lsp/domain-lang-code-lens-provider.d.ts +8 -0
- package/out/lsp/domain-lang-code-lens-provider.js +48 -0
- package/out/lsp/domain-lang-code-lens-provider.js.map +1 -0
- package/out/lsp/domain-lang-completion.js +39 -15
- package/out/lsp/domain-lang-completion.js.map +1 -1
- package/out/lsp/domain-lang-document-symbol-provider.js +5 -5
- package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -1
- package/out/lsp/domain-lang-formatter.js +32 -0
- package/out/lsp/domain-lang-formatter.js.map +1 -1
- package/out/lsp/domain-lang-index-manager.d.ts +2 -3
- package/out/lsp/domain-lang-index-manager.js +5 -8
- package/out/lsp/domain-lang-index-manager.js.map +1 -1
- package/out/lsp/domain-lang-workspace-manager.d.ts +1 -1
- package/out/lsp/domain-lang-workspace-manager.js +2 -26
- package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
- package/out/lsp/explain.js +9 -3
- package/out/lsp/explain.js.map +1 -1
- package/out/lsp/hover/domain-lang-hover.js +13 -11
- package/out/lsp/hover/domain-lang-hover.js.map +1 -1
- package/out/lsp/hover/domain-lang-keywords.js +29 -26
- package/out/lsp/hover/domain-lang-keywords.js.map +1 -1
- package/out/lsp/tool-handlers.js +63 -57
- package/out/lsp/tool-handlers.js.map +1 -1
- package/out/sdk/ast-augmentation.d.ts +29 -21
- package/out/sdk/ast-augmentation.js +11 -7
- package/out/sdk/ast-augmentation.js.map +1 -1
- package/out/sdk/index.d.ts +2 -2
- package/out/sdk/index.js +1 -1
- package/out/sdk/index.js.map +1 -1
- package/out/sdk/loader-node.js +2 -2
- package/out/sdk/loader-node.js.map +1 -1
- package/out/sdk/patterns.d.ts +50 -61
- package/out/sdk/patterns.js +92 -62
- package/out/sdk/patterns.js.map +1 -1
- package/out/sdk/query.js +54 -43
- package/out/sdk/query.js.map +1 -1
- package/out/sdk/serializers.js +20 -7
- package/out/sdk/serializers.js.map +1 -1
- package/out/sdk/types.d.ts +87 -18
- package/out/sdk/types.js.map +1 -1
- package/out/sdk/validator.js +48 -64
- package/out/sdk/validator.js.map +1 -1
- package/out/services/performance-optimizer.d.ts +3 -3
- package/out/services/performance-optimizer.js +1 -3
- package/out/services/performance-optimizer.js.map +1 -1
- package/out/services/relationship-inference.d.ts +4 -4
- package/out/services/relationship-inference.js +34 -46
- package/out/services/relationship-inference.js.map +1 -1
- package/out/syntaxes/domain-lang.monarch.js +1 -1
- package/out/syntaxes/domain-lang.monarch.js.map +1 -1
- package/out/utils/import-utils.d.ts +6 -20
- package/out/utils/import-utils.js +3 -63
- package/out/utils/import-utils.js.map +1 -1
- package/out/validation/constants.d.ts +23 -6
- package/out/validation/constants.js +24 -7
- package/out/validation/constants.js.map +1 -1
- package/out/validation/maps.js +10 -4
- package/out/validation/maps.js.map +1 -1
- package/out/validation/relationships.d.ts +4 -8
- package/out/validation/relationships.js +96 -48
- package/out/validation/relationships.js.map +1 -1
- package/package.json +5 -2
- package/src/ast-augmentation.ts +7 -2
- package/src/diagram/context-map-diagram-generator.ts +513 -0
- package/src/diagram/context-map-layout-configurator.ts +43 -0
- package/src/diagram/elk-layout-factory.ts +83 -0
- package/src/domain-lang-module.ts +19 -2
- package/src/domain-lang.langium +62 -26
- package/src/generated/ast.ts +413 -63
- package/src/generated/grammar.ts +418 -172
- package/src/index.ts +5 -0
- package/src/lsp/domain-lang-code-lens-provider.ts +54 -0
- package/src/lsp/domain-lang-completion.ts +42 -15
- package/src/lsp/domain-lang-document-symbol-provider.ts +5 -5
- package/src/lsp/domain-lang-formatter.ts +34 -0
- package/src/lsp/domain-lang-index-manager.ts +6 -9
- package/src/lsp/domain-lang-workspace-manager.ts +3 -29
- package/src/lsp/explain.ts +10 -2
- package/src/lsp/hover/domain-lang-hover.ts +10 -8
- package/src/lsp/hover/domain-lang-keywords.ts +27 -24
- package/src/lsp/tool-handlers.ts +61 -47
- package/src/sdk/ast-augmentation.ts +30 -21
- package/src/sdk/index.ts +11 -1
- package/src/sdk/loader-node.ts +2 -2
- package/src/sdk/patterns.ts +114 -76
- package/src/sdk/query.ts +57 -48
- package/src/sdk/serializers.ts +20 -7
- package/src/sdk/types.ts +92 -17
- package/src/sdk/validator.ts +52 -69
- package/src/services/performance-optimizer.ts +4 -6
- package/src/services/relationship-inference.ts +43 -54
- package/src/utils/import-utils.ts +9 -74
- package/src/validation/constants.ts +32 -9
- package/src/validation/maps.ts +12 -4
- package/src/validation/relationships.ts +150 -71
package/src/sdk/types.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
Model,
|
|
14
14
|
NamespaceDeclaration,
|
|
15
15
|
Relationship,
|
|
16
|
+
SidePattern,
|
|
16
17
|
Team,
|
|
17
18
|
} from '../generated/ast.js';
|
|
18
19
|
import type { DomainLangServices } from '../domain-lang-module.js';
|
|
@@ -281,30 +282,104 @@ export interface BcQueryBuilder extends QueryBuilder<BoundedContext> {
|
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
/**
|
|
284
|
-
*
|
|
285
|
-
*
|
|
285
|
+
* One side of a relationship: the bounded context and its annotated integration patterns.
|
|
286
|
+
* For symmetric relationships `patterns` is always empty — the pattern lives on the relationship itself.
|
|
287
|
+
*/
|
|
288
|
+
export interface RelationshipSide {
|
|
289
|
+
/** The bounded context on this side */
|
|
290
|
+
readonly context: BoundedContext;
|
|
291
|
+
/** Integration patterns annotated on this side (directional relationships only) */
|
|
292
|
+
readonly patterns: readonly SidePattern[];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* DDD kind of a directional relationship.
|
|
297
|
+
* Fully discriminates all three structural cases.
|
|
298
|
+
*/
|
|
299
|
+
export type DirectionalKind = 'UpstreamDownstream' | 'CustomerSupplier' | 'Bidirectional';
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Directional relationship: has an arrow expressing dependency between two contexts.
|
|
303
|
+
* Discriminate on `type` to access directional or symmetric properties.
|
|
304
|
+
* Discriminate on `kind` to handle upstream/downstream vs customer/supplier vs bidirectional.
|
|
286
305
|
*/
|
|
287
|
-
export interface
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
306
|
+
export interface DirectionalRelationshipView {
|
|
307
|
+
readonly type: 'directional';
|
|
308
|
+
/** DDD semantic kind — determines which roles each side plays */
|
|
309
|
+
readonly kind: DirectionalKind;
|
|
310
|
+
/** Arrow as written in source (use for display/serialization) */
|
|
311
|
+
readonly arrow: '->' | '<-' | '<->';
|
|
312
|
+
/** Left side as written in source — always defined */
|
|
313
|
+
readonly left: RelationshipSide;
|
|
314
|
+
/** Right side as written in source — always defined */
|
|
315
|
+
readonly right: RelationshipSide;
|
|
316
|
+
/**
|
|
317
|
+
* The upstream (provider) side with its patterns.
|
|
318
|
+
* Defined when `kind` is `'UpstreamDownstream'` or `'CustomerSupplier'`.
|
|
319
|
+
* `undefined` when `kind` is `'Bidirectional'` — no upstream role exists.
|
|
320
|
+
*/
|
|
321
|
+
readonly upstream: RelationshipSide | undefined;
|
|
322
|
+
/**
|
|
323
|
+
* The downstream (consumer) side with its patterns.
|
|
324
|
+
* Defined when `kind` is `'UpstreamDownstream'` or `'CustomerSupplier'`.
|
|
325
|
+
* `undefined` when `kind` is `'Bidirectional'` — no downstream role exists.
|
|
326
|
+
*/
|
|
327
|
+
readonly downstream: RelationshipSide | undefined;
|
|
302
328
|
/** Source of the relationship definition */
|
|
303
329
|
readonly source: 'BoundedContext' | 'ContextMap';
|
|
304
330
|
/** Original AST relationship node */
|
|
305
331
|
readonly astNode: Relationship;
|
|
306
332
|
}
|
|
307
333
|
|
|
334
|
+
/** Resolved symmetric integration pattern kind. */
|
|
335
|
+
export type SymmetricKind = 'SharedKernel' | 'Partnership' | 'SeparateWays';
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Symmetric relationship: no arrow; neither context is upstream or downstream.
|
|
339
|
+
* Discriminate on `type` to access directional or symmetric properties.
|
|
340
|
+
*/
|
|
341
|
+
export interface SymmetricRelationshipView {
|
|
342
|
+
readonly type: 'symmetric';
|
|
343
|
+
/**
|
|
344
|
+
* Always-resolved symmetric integration pattern.
|
|
345
|
+
* `><` resolves to `'SeparateWays'`.
|
|
346
|
+
*/
|
|
347
|
+
readonly kind: SymmetricKind;
|
|
348
|
+
/**
|
|
349
|
+
* Left side as written in source.
|
|
350
|
+
* `patterns` is always empty — symmetric patterns attach to the relationship, not each side.
|
|
351
|
+
*/
|
|
352
|
+
readonly left: RelationshipSide;
|
|
353
|
+
/**
|
|
354
|
+
* Right side as written in source.
|
|
355
|
+
* `patterns` is always empty — symmetric patterns attach to the relationship, not each side.
|
|
356
|
+
*/
|
|
357
|
+
readonly right: RelationshipSide;
|
|
358
|
+
/** Source of the relationship definition */
|
|
359
|
+
readonly source: 'BoundedContext' | 'ContextMap';
|
|
360
|
+
/** Original AST relationship node */
|
|
361
|
+
readonly astNode: Relationship;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Unified view of a relationship between two BoundedContexts.
|
|
366
|
+
* Relationships can be defined in BoundedContext blocks or ContextMap.
|
|
367
|
+
* Discriminate on `type` to access directional or symmetric properties.
|
|
368
|
+
*
|
|
369
|
+
* @example
|
|
370
|
+
* ```typescript
|
|
371
|
+
* for (const rel of query.relationships()) {
|
|
372
|
+
* if (rel.type === 'symmetric') {
|
|
373
|
+
* console.log(rel.kind); // 'SharedKernel' | 'Partnership' | 'SeparateWays'
|
|
374
|
+
* } else {
|
|
375
|
+
* console.log(rel.kind); // 'UpstreamDownstream' | 'CustomerSupplier' | 'Bidirectional'
|
|
376
|
+
* console.log(rel.arrow); // '->' | '<-' | '<->'
|
|
377
|
+
* }
|
|
378
|
+
* }
|
|
379
|
+
* ```
|
|
380
|
+
*/
|
|
381
|
+
export type RelationshipView = DirectionalRelationshipView | SymmetricRelationshipView;
|
|
382
|
+
|
|
308
383
|
/**
|
|
309
384
|
* Internal index structure for O(1) lookups.
|
|
310
385
|
* Not exported from public API.
|
package/src/sdk/validator.ts
CHANGED
|
@@ -75,6 +75,52 @@ function toValidationDiagnostic(
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Collect errors and warnings from an array of Langium documents.
|
|
80
|
+
*/
|
|
81
|
+
function collectDiagnostics(
|
|
82
|
+
allDocuments: Iterable<{ uri: { fsPath: string }; diagnostics?: Array<{ severity?: number; message: string; range: { start: { line: number; character: number } } }> }>
|
|
83
|
+
): { errors: ValidationDiagnostic[]; warnings: ValidationDiagnostic[] } {
|
|
84
|
+
const errors: ValidationDiagnostic[] = [];
|
|
85
|
+
const warnings: ValidationDiagnostic[] = [];
|
|
86
|
+
for (const doc of allDocuments) {
|
|
87
|
+
const diagnostics = doc.diagnostics ?? [];
|
|
88
|
+
const docPath = doc.uri.fsPath;
|
|
89
|
+
for (const diagnostic of diagnostics) {
|
|
90
|
+
const validationDiag = toValidationDiagnostic(diagnostic, docPath);
|
|
91
|
+
if (diagnostic.severity === 1) {
|
|
92
|
+
errors.push(validationDiag);
|
|
93
|
+
} else if (diagnostic.severity === 2) {
|
|
94
|
+
warnings.push(validationDiag);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { errors, warnings };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Count Domain and BoundedContext elements across all documents.
|
|
103
|
+
*/
|
|
104
|
+
function countModelElements(
|
|
105
|
+
allDocuments: Iterable<{ parseResult?: { value: unknown } }>
|
|
106
|
+
): { domainCount: number; bcCount: number } {
|
|
107
|
+
let domainCount = 0;
|
|
108
|
+
let bcCount = 0;
|
|
109
|
+
for (const doc of allDocuments) {
|
|
110
|
+
const model = doc.parseResult?.value;
|
|
111
|
+
if (isModel(model)) {
|
|
112
|
+
for (const element of model.children ?? []) {
|
|
113
|
+
if (element.$type === 'Domain') {
|
|
114
|
+
domainCount++;
|
|
115
|
+
} else if (element.$type === 'BoundedContext') {
|
|
116
|
+
bcCount++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { domainCount, bcCount };
|
|
122
|
+
}
|
|
123
|
+
|
|
78
124
|
/**
|
|
79
125
|
* Validates a DomainLang model file and all its imports.
|
|
80
126
|
*
|
|
@@ -152,40 +198,9 @@ export async function validateFile(
|
|
|
152
198
|
const allDocuments = Array.from(shared.workspace.LangiumDocuments.all);
|
|
153
199
|
await shared.workspace.DocumentBuilder.build(allDocuments, { validation: true });
|
|
154
200
|
|
|
155
|
-
// Collect diagnostics from all loaded documents (entry + imports)
|
|
156
|
-
const errors
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
for (const doc of allDocuments) {
|
|
160
|
-
const diagnostics = doc.diagnostics ?? [];
|
|
161
|
-
const diagnosticFile = doc.uri.fsPath;
|
|
162
|
-
|
|
163
|
-
for (const diagnostic of diagnostics) {
|
|
164
|
-
const validationDiag = toValidationDiagnostic(diagnostic, diagnosticFile);
|
|
165
|
-
if (diagnostic.severity === 1) {
|
|
166
|
-
errors.push(validationDiag);
|
|
167
|
-
} else if (diagnostic.severity === 2) {
|
|
168
|
-
warnings.push(validationDiag);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Count model elements across all documents
|
|
174
|
-
let domainCount = 0;
|
|
175
|
-
let bcCount = 0;
|
|
176
|
-
|
|
177
|
-
for (const doc of allDocuments) {
|
|
178
|
-
const model = doc.parseResult?.value;
|
|
179
|
-
if (isModel(model)) {
|
|
180
|
-
for (const element of model.children ?? []) {
|
|
181
|
-
if (element.$type === 'Domain') {
|
|
182
|
-
domainCount++;
|
|
183
|
-
} else if (element.$type === 'BoundedContext') {
|
|
184
|
-
bcCount++;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
201
|
+
// Collect diagnostics and count elements from all loaded documents (entry + imports)
|
|
202
|
+
const { errors, warnings } = collectDiagnostics(allDocuments);
|
|
203
|
+
const { domainCount, bcCount } = countModelElements(allDocuments);
|
|
189
204
|
|
|
190
205
|
return {
|
|
191
206
|
valid: errors.length === 0,
|
|
@@ -314,41 +329,9 @@ export async function validateWorkspace(
|
|
|
314
329
|
const allDocuments = Array.from(shared.workspace.LangiumDocuments.all);
|
|
315
330
|
await shared.workspace.DocumentBuilder.build(allDocuments, { validation: true });
|
|
316
331
|
|
|
317
|
-
// Collect diagnostics from ALL documents (not just entry)
|
|
318
|
-
const errors
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
for (const doc of allDocuments) {
|
|
322
|
-
const diagnostics = doc.diagnostics ?? [];
|
|
323
|
-
const docPath = doc.uri.fsPath;
|
|
324
|
-
|
|
325
|
-
for (const diagnostic of diagnostics) {
|
|
326
|
-
const validationDiag = toValidationDiagnostic(diagnostic, docPath);
|
|
327
|
-
|
|
328
|
-
if (diagnostic.severity === 1) {
|
|
329
|
-
errors.push(validationDiag);
|
|
330
|
-
} else if (diagnostic.severity === 2) {
|
|
331
|
-
warnings.push(validationDiag);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Count model elements across all documents
|
|
337
|
-
let domainCount = 0;
|
|
338
|
-
let bcCount = 0;
|
|
339
|
-
|
|
340
|
-
for (const doc of allDocuments) {
|
|
341
|
-
const model = doc.parseResult?.value;
|
|
342
|
-
if (isModel(model)) {
|
|
343
|
-
for (const element of model.children ?? []) {
|
|
344
|
-
if (element.$type === 'Domain') {
|
|
345
|
-
domainCount++;
|
|
346
|
-
} else if (element.$type === 'BoundedContext') {
|
|
347
|
-
bcCount++;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
332
|
+
// Collect diagnostics and count elements from ALL documents (not just entry)
|
|
333
|
+
const { errors, warnings } = collectDiagnostics(allDocuments);
|
|
334
|
+
const { domainCount, bcCount } = countModelElements(allDocuments);
|
|
352
335
|
|
|
353
336
|
return {
|
|
354
337
|
valid: errors.length === 0,
|
|
@@ -23,8 +23,8 @@ import fs from 'node:fs/promises';
|
|
|
23
23
|
* always correct and immediate.
|
|
24
24
|
*/
|
|
25
25
|
export class PerformanceOptimizer {
|
|
26
|
-
private lockFileCache = new Map<string, LockFile>();
|
|
27
|
-
private manifestCache = new Map<string, unknown>();
|
|
26
|
+
private readonly lockFileCache = new Map<string, LockFile>();
|
|
27
|
+
private readonly manifestCache = new Map<string, unknown>();
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Gets a lock file from cache or loads it from disk.
|
|
@@ -55,7 +55,7 @@ export class PerformanceOptimizer {
|
|
|
55
55
|
/**
|
|
56
56
|
* Gets a manifest file from cache or loads it from disk.
|
|
57
57
|
*/
|
|
58
|
-
async getCachedManifest(manifestPath: string): Promise<unknown
|
|
58
|
+
async getCachedManifest(manifestPath: string): Promise<unknown> {
|
|
59
59
|
const cacheKey = this.normalizePath(manifestPath);
|
|
60
60
|
const cached = this.manifestCache.get(cacheKey);
|
|
61
61
|
|
|
@@ -121,9 +121,7 @@ let globalOptimizer: PerformanceOptimizer | undefined;
|
|
|
121
121
|
* Gets the global performance optimizer instance.
|
|
122
122
|
*/
|
|
123
123
|
export function getGlobalOptimizer(): PerformanceOptimizer {
|
|
124
|
-
|
|
125
|
-
globalOptimizer = new PerformanceOptimizer();
|
|
126
|
-
}
|
|
124
|
+
globalOptimizer ??= new PerformanceOptimizer();
|
|
127
125
|
return globalOptimizer;
|
|
128
126
|
}
|
|
129
127
|
|
|
@@ -3,20 +3,26 @@ import type {
|
|
|
3
3
|
Relationship,
|
|
4
4
|
StructureElement,
|
|
5
5
|
BoundedContext,
|
|
6
|
-
ContextMap
|
|
6
|
+
ContextMap,
|
|
7
|
+
DirectionalRelationship,
|
|
8
|
+
SymmetricRelationship
|
|
7
9
|
} from '../generated/ast.js';
|
|
8
10
|
import {
|
|
9
11
|
isBoundedContext,
|
|
10
12
|
isContextMap,
|
|
11
|
-
isNamespaceDeclaration
|
|
13
|
+
isNamespaceDeclaration,
|
|
14
|
+
isDirectionalRelationship,
|
|
15
|
+
isSymmetricRelationship,
|
|
16
|
+
isSupplier,
|
|
17
|
+
isCustomer
|
|
12
18
|
} from '../generated/ast.js';
|
|
13
19
|
|
|
14
20
|
/**
|
|
15
|
-
* Enriches relationships in the model by inferring relationship
|
|
16
|
-
* from
|
|
21
|
+
* Enriches relationships in the model by inferring relationship kinds
|
|
22
|
+
* from AST node type, patterns, and arrow direction.
|
|
17
23
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
24
|
+
* With the entity–relationship–entity grammar, symmetric relationships
|
|
25
|
+
* derive their kind directly from the AST node type (no heuristics).
|
|
20
26
|
*
|
|
21
27
|
* @param model - The root model to process
|
|
22
28
|
*/
|
|
@@ -26,17 +32,14 @@ export function setInferredRelationshipTypes(model: Model): void {
|
|
|
26
32
|
|
|
27
33
|
/**
|
|
28
34
|
* Recursively walks structure elements to find and enrich relationships.
|
|
29
|
-
*
|
|
30
|
-
* @param elements - Array of structure elements to process
|
|
31
|
-
* @param containerBc - Optional container bounded context (for nested contexts)
|
|
32
35
|
*/
|
|
33
36
|
function walkStructureElements(
|
|
34
37
|
elements: StructureElement[] = [],
|
|
35
|
-
|
|
38
|
+
_containerBc?: BoundedContext
|
|
36
39
|
): void {
|
|
37
40
|
for (const element of elements) {
|
|
38
|
-
|
|
39
|
-
walkStructureElements(element.children,
|
|
41
|
+
if (isNamespaceDeclaration(element)) {
|
|
42
|
+
walkStructureElements(element.children, _containerBc);
|
|
40
43
|
} else if (isBoundedContext(element)) {
|
|
41
44
|
processContextRelationships(element);
|
|
42
45
|
} else if (isContextMap(element)) {
|
|
@@ -47,8 +50,6 @@ function walkStructureElements(
|
|
|
47
50
|
|
|
48
51
|
/**
|
|
49
52
|
* Processes relationships within a bounded context.
|
|
50
|
-
*
|
|
51
|
-
* @param context - The bounded context to process
|
|
52
53
|
*/
|
|
53
54
|
function processContextRelationships(context: BoundedContext): void {
|
|
54
55
|
for (const rel of context.relationships) {
|
|
@@ -58,8 +59,6 @@ function processContextRelationships(context: BoundedContext): void {
|
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
61
|
* Processes relationships within a context map.
|
|
61
|
-
*
|
|
62
|
-
* @param map - The context map to process
|
|
63
62
|
*/
|
|
64
63
|
function processMapRelationships(map: ContextMap): void {
|
|
65
64
|
if (map.relationships) {
|
|
@@ -70,52 +69,42 @@ function processMapRelationships(map: ContextMap): void {
|
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
/**
|
|
73
|
-
* Enriches a single relationship by inferring its
|
|
74
|
-
*
|
|
75
|
-
* @param rel - The relationship to enrich
|
|
72
|
+
* Enriches a single relationship by inferring its kind.
|
|
76
73
|
*/
|
|
77
74
|
function enrichRelationship(rel: Relationship): void {
|
|
78
|
-
if (
|
|
79
|
-
rel.
|
|
75
|
+
if (isSymmetricRelationship(rel)) {
|
|
76
|
+
rel.inferredKind = inferSymmetricKind(rel);
|
|
77
|
+
} else if (isDirectionalRelationship(rel)) {
|
|
78
|
+
rel.inferredKind = inferDirectionalKind(rel);
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
/**
|
|
84
|
-
* Infers
|
|
85
|
-
*
|
|
86
|
-
* Inference rules:
|
|
87
|
-
* - `><` → SeparateWays
|
|
88
|
-
* - `<->` with P roles → Partnership
|
|
89
|
-
* - `<->` with SK roles → SharedKernel
|
|
90
|
-
* - `->` or `<-` → UpstreamDownstream
|
|
91
|
-
*
|
|
92
|
-
* @param relationship - The relationship to analyze
|
|
93
|
-
* @returns The inferred type or undefined if no rule matches
|
|
83
|
+
* Infers kind for symmetric relationships — derived from AST node type.
|
|
84
|
+
* No heuristics needed: the pattern is structurally part of the relationship.
|
|
94
85
|
*/
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (relationship.arrow === '><') {
|
|
100
|
-
return 'SeparateWays';
|
|
86
|
+
function inferSymmetricKind(rel: SymmetricRelationship): string {
|
|
87
|
+
if (rel.pattern) {
|
|
88
|
+
return rel.pattern.$type; // 'SharedKernel', 'Partnership', or 'SeparateWays'
|
|
101
89
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const noPatterns = leftPatterns.length === 0 && rightPatterns.length === 0;
|
|
105
|
-
const bothPartners = leftPatterns.includes('P') && rightPatterns.includes('P');
|
|
106
|
-
|
|
107
|
-
if (noPatterns || bothPartners) {
|
|
108
|
-
return 'Partnership';
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (leftPatterns.includes('SK') && rightPatterns.includes('SK')) {
|
|
112
|
-
return 'SharedKernel';
|
|
113
|
-
}
|
|
90
|
+
if (rel.arrow === '><') {
|
|
91
|
+
return 'SeparateWays';
|
|
114
92
|
}
|
|
93
|
+
return 'SeparateWays'; // `><` is the only non-pattern symmetric form
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Infers kind for directional relationships from side patterns.
|
|
98
|
+
*
|
|
99
|
+
* - Customer/Supplier: has [S] or [C] patterns
|
|
100
|
+
* - UpstreamDownstream: default for all directional relationships
|
|
101
|
+
*/
|
|
102
|
+
function inferDirectionalKind(rel: DirectionalRelationship): string {
|
|
103
|
+
const hasSupplier = rel.leftPatterns.some(isSupplier) || rel.rightPatterns.some(isSupplier);
|
|
104
|
+
const hasCustomer = rel.leftPatterns.some(isCustomer) || rel.rightPatterns.some(isCustomer);
|
|
115
105
|
|
|
116
|
-
if (
|
|
117
|
-
return '
|
|
106
|
+
if (hasSupplier || hasCustomer) {
|
|
107
|
+
return 'CustomerSupplier';
|
|
118
108
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
109
|
+
return 'UpstreamDownstream';
|
|
110
|
+
}
|
|
@@ -1,82 +1,21 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { URI, type LangiumDocument, type LangiumDocuments } from 'langium';
|
|
3
3
|
import type { Model } from '../generated/ast.js';
|
|
4
|
-
import {
|
|
5
|
-
import { ImportResolver } from '../services/import-resolver.js';
|
|
6
|
-
import type { DomainLangServices } from '../domain-lang-module.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Lazily initialized workspace manager for standalone (non-LSP) usage.
|
|
10
|
-
* Used by import graph building when no DI-injected ImportResolver is available.
|
|
11
|
-
*
|
|
12
|
-
* @deprecated Prefer passing an ImportResolver from the DI container.
|
|
13
|
-
* These singletons exist only for backwards compatibility with callers
|
|
14
|
-
* that haven't been updated to pass through DI services.
|
|
15
|
-
*/
|
|
16
|
-
let standaloneManifestManager: ManifestManager | undefined;
|
|
17
|
-
let standaloneImportResolver: ImportResolver | undefined;
|
|
18
|
-
let lastInitializedDir: string | undefined;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Gets or creates a standalone import resolver for non-LSP contexts.
|
|
22
|
-
* Creates its own ManifestManager if not previously initialized for this directory.
|
|
23
|
-
*
|
|
24
|
-
* @deprecated Prefer using services.imports.ImportResolver directly.
|
|
25
|
-
* @param startDir - Directory to start workspace search from
|
|
26
|
-
* @returns Promise resolving to the import resolver
|
|
27
|
-
*/
|
|
28
|
-
async function getStandaloneImportResolver(startDir: string): Promise<ImportResolver> {
|
|
29
|
-
// Re-initialize if directory changed (workspace boundary)
|
|
30
|
-
if (lastInitializedDir !== startDir || !standaloneImportResolver) {
|
|
31
|
-
standaloneManifestManager = new ManifestManager();
|
|
32
|
-
try {
|
|
33
|
-
await standaloneManifestManager.initialize(startDir);
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.warn(`Failed to initialize workspace: ${error instanceof Error ? error.message : String(error)}`);
|
|
36
|
-
}
|
|
37
|
-
const services = {
|
|
38
|
-
imports: { ManifestManager: standaloneManifestManager }
|
|
39
|
-
} as DomainLangServices;
|
|
40
|
-
standaloneImportResolver = new ImportResolver(services);
|
|
41
|
-
lastInitializedDir = startDir;
|
|
42
|
-
}
|
|
43
|
-
return standaloneImportResolver;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Resolves an import path to an absolute file URI.
|
|
48
|
-
*
|
|
49
|
-
* @deprecated Prefer using ImportResolver.resolveForDocument() from the DI container.
|
|
50
|
-
* This function creates standalone instances outside the DI system.
|
|
51
|
-
*
|
|
52
|
-
* @param importingDoc - The document containing the import statement
|
|
53
|
-
* @param rawImportPath - The raw import path from the import statement
|
|
54
|
-
* @returns Resolved URI to the imported file
|
|
55
|
-
* @throws {Error} If the import cannot be resolved
|
|
56
|
-
*/
|
|
57
|
-
export async function resolveImportPath(
|
|
58
|
-
importingDoc: LangiumDocument,
|
|
59
|
-
rawImportPath: string
|
|
60
|
-
): Promise<URI> {
|
|
61
|
-
const baseDir = path.dirname(importingDoc.uri.fsPath);
|
|
62
|
-
const resolver = await getStandaloneImportResolver(baseDir);
|
|
63
|
-
return resolver.resolveFrom(baseDir, rawImportPath);
|
|
64
|
-
}
|
|
4
|
+
import type { ImportResolver } from '../services/import-resolver.js';
|
|
65
5
|
|
|
66
6
|
/**
|
|
67
7
|
* Ensures the import graph is loaded from an entry file.
|
|
68
|
-
*
|
|
8
|
+
*
|
|
69
9
|
* @param entryFilePath - Absolute or workspace-relative path to entry file
|
|
70
10
|
* @param langiumDocuments - The Langium documents manager
|
|
71
|
-
* @param importResolver -
|
|
72
|
-
* uses it instead of creating standalone instances. Recommended for LSP contexts.
|
|
11
|
+
* @param importResolver - DI-injected ImportResolver from the language services
|
|
73
12
|
* @returns Set of URIs (as strings) for all documents in the import graph
|
|
74
13
|
* @throws {Error} If entry file cannot be resolved or loaded
|
|
75
14
|
*/
|
|
76
15
|
export async function ensureImportGraphFromEntryFile(
|
|
77
16
|
entryFilePath: string,
|
|
78
17
|
langiumDocuments: LangiumDocuments,
|
|
79
|
-
importResolver
|
|
18
|
+
importResolver: ImportResolver
|
|
80
19
|
): Promise<Set<string>> {
|
|
81
20
|
const entryUri = URI.file(path.resolve(entryFilePath));
|
|
82
21
|
const entryDoc = await langiumDocuments.getOrCreateDocument(entryUri);
|
|
@@ -85,17 +24,16 @@ export async function ensureImportGraphFromEntryFile(
|
|
|
85
24
|
|
|
86
25
|
/**
|
|
87
26
|
* Recursively builds the import graph from a document.
|
|
88
|
-
*
|
|
27
|
+
*
|
|
89
28
|
* @param document - The starting document
|
|
90
29
|
* @param langiumDocuments - The Langium documents manager
|
|
91
|
-
* @param importResolver -
|
|
92
|
-
* uses it instead of creating standalone instances. Recommended for LSP contexts.
|
|
30
|
+
* @param importResolver - DI-injected ImportResolver from the language services
|
|
93
31
|
* @returns Set of URIs (as strings) for all documents in the import graph
|
|
94
32
|
*/
|
|
95
33
|
export async function ensureImportGraphFromDocument(
|
|
96
34
|
document: LangiumDocument,
|
|
97
35
|
langiumDocuments: LangiumDocuments,
|
|
98
|
-
importResolver
|
|
36
|
+
importResolver: ImportResolver
|
|
99
37
|
): Promise<Set<string>> {
|
|
100
38
|
const visited = new Set<string>();
|
|
101
39
|
|
|
@@ -107,12 +45,9 @@ export async function ensureImportGraphFromDocument(
|
|
|
107
45
|
const model = doc.parseResult.value as unknown as Model;
|
|
108
46
|
for (const imp of model.imports ?? []) {
|
|
109
47
|
if (!imp.uri) continue;
|
|
110
|
-
|
|
48
|
+
|
|
111
49
|
try {
|
|
112
|
-
|
|
113
|
-
const resolvedUri = importResolver
|
|
114
|
-
? await importResolver.resolveForDocument(doc, imp.uri)
|
|
115
|
-
: await resolveImportPath(doc, imp.uri);
|
|
50
|
+
const resolvedUri = await importResolver.resolveForDocument(doc, imp.uri);
|
|
116
51
|
const childDoc = await langiumDocuments.getOrCreateDocument(resolvedUri);
|
|
117
52
|
await visit(childDoc);
|
|
118
53
|
} catch {
|
|
@@ -48,9 +48,12 @@ export const IssueCodes = {
|
|
|
48
48
|
BoundedContextTeamConflict: 'bounded-context-team-conflict',
|
|
49
49
|
|
|
50
50
|
// Integration Pattern Issues
|
|
51
|
-
SharedKernelNotBidirectional: 'shared-kernel-not-bidirectional',
|
|
52
51
|
AclOnWrongSide: 'acl-on-wrong-side',
|
|
53
52
|
ConformistOnWrongSide: 'conformist-on-wrong-side',
|
|
53
|
+
OhsOnWrongSide: 'ohs-on-wrong-side',
|
|
54
|
+
SupplierOnWrongSide: 'supplier-on-wrong-side',
|
|
55
|
+
CustomerOnWrongSide: 'customer-on-wrong-side',
|
|
56
|
+
SelfSymmetricRelationship: 'self-symmetric-relationship',
|
|
54
57
|
TooManyPatterns: 'too-many-patterns',
|
|
55
58
|
|
|
56
59
|
// Context/Domain Map Issues
|
|
@@ -167,13 +170,6 @@ export const ValidationMessages = {
|
|
|
167
170
|
// Integration Pattern & Relationship Validation
|
|
168
171
|
// ========================================================================
|
|
169
172
|
|
|
170
|
-
/**
|
|
171
|
-
* Warning when SharedKernel pattern uses incorrect arrow direction.
|
|
172
|
-
* SharedKernel requires bidirectional relationship.
|
|
173
|
-
*/
|
|
174
|
-
SHARED_KERNEL_MUST_BE_BIDIRECTIONAL: (leftContext: string, rightContext: string, arrow: string) =>
|
|
175
|
-
`SharedKernel between '${leftContext}' and '${rightContext}' requires bidirectional arrow '<->', not '${arrow}'.`,
|
|
176
|
-
|
|
177
173
|
/**
|
|
178
174
|
* Warning when Anti-Corruption Layer is on the wrong side of relationship.
|
|
179
175
|
* ACL should protect the consuming context (downstream).
|
|
@@ -188,6 +184,33 @@ export const ValidationMessages = {
|
|
|
188
184
|
CONFORMIST_ON_WRONG_SIDE: (context: string, side: 'left' | 'right') =>
|
|
189
185
|
`Conformist (CF) on '${context}' should be on downstream (consuming) side, not ${side} side.`,
|
|
190
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Warning when Open Host Service is on the wrong side of relationship.
|
|
189
|
+
* OHS should be on the upstream (providing) side.
|
|
190
|
+
*/
|
|
191
|
+
OHS_ON_WRONG_SIDE: (context: string, side: 'left' | 'right') =>
|
|
192
|
+
`Open Host Service (OHS) on '${context}' should be on upstream (providing) side, not ${side} side.`,
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Error when Supplier is on the wrong side of relationship.
|
|
196
|
+
* Supplier must always be upstream.
|
|
197
|
+
*/
|
|
198
|
+
SUPPLIER_ON_WRONG_SIDE: (context: string, _side: 'left' | 'right') =>
|
|
199
|
+
`Supplier (S) on '${context}' must be on the upstream side. Supplier is always the provider in a Customer/Supplier relationship.`,
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Error when Customer is on the wrong side of relationship.
|
|
203
|
+
* Customer must always be downstream.
|
|
204
|
+
*/
|
|
205
|
+
CUSTOMER_ON_WRONG_SIDE: (context: string, _side: 'left' | 'right') =>
|
|
206
|
+
`Customer (C) on '${context}' must be on the downstream side. Customer is always the consumer in a Customer/Supplier relationship.`,
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Warning when a symmetric relationship references the same context on both sides.
|
|
210
|
+
*/
|
|
211
|
+
SELF_SYMMETRIC_RELATIONSHIP: (context: string) =>
|
|
212
|
+
`Symmetric relationship with self: '${context}' has a symmetric relationship with itself. This is likely unintended.`,
|
|
213
|
+
|
|
191
214
|
/**
|
|
192
215
|
* Info message when relationship has too many integration patterns.
|
|
193
216
|
* Suggests possible syntax confusion.
|
|
@@ -311,7 +334,7 @@ export const ValidationMessages = {
|
|
|
311
334
|
*/
|
|
312
335
|
CONTEXT_MAP_NO_RELATIONSHIPS: (name: string, count: number) =>
|
|
313
336
|
`Context Map '${name}' contains ${count} contexts but no documented relationships.\n` +
|
|
314
|
-
`Hint: Add relationships to show how contexts integrate (e.g., '[OHS]
|
|
337
|
+
`Hint: Add relationships to show how contexts integrate (e.g., 'A [OHS] -> [CF] B').`,
|
|
315
338
|
|
|
316
339
|
/**
|
|
317
340
|
* Warning when a context map contains duplicate relationships.
|