@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.
Files changed (116) hide show
  1. package/out/ast-augmentation.d.ts +7 -2
  2. package/out/diagram/context-map-diagram-generator.d.ts +72 -0
  3. package/out/diagram/context-map-diagram-generator.js +405 -0
  4. package/out/diagram/context-map-diagram-generator.js.map +1 -0
  5. package/out/diagram/context-map-layout-configurator.d.ts +15 -0
  6. package/out/diagram/context-map-layout-configurator.js +39 -0
  7. package/out/diagram/context-map-layout-configurator.js.map +1 -0
  8. package/out/diagram/elk-layout-factory.d.ts +43 -0
  9. package/out/diagram/elk-layout-factory.js +64 -0
  10. package/out/diagram/elk-layout-factory.js.map +1 -0
  11. package/out/domain-lang-module.d.ts +7 -0
  12. package/out/domain-lang-module.js +11 -2
  13. package/out/domain-lang-module.js.map +1 -1
  14. package/out/generated/ast.d.ts +323 -51
  15. package/out/generated/ast.js +194 -33
  16. package/out/generated/ast.js.map +1 -1
  17. package/out/generated/grammar.js +418 -172
  18. package/out/generated/grammar.js.map +1 -1
  19. package/out/index.d.ts +3 -0
  20. package/out/index.js +4 -0
  21. package/out/index.js.map +1 -1
  22. package/out/lsp/domain-lang-code-lens-provider.d.ts +8 -0
  23. package/out/lsp/domain-lang-code-lens-provider.js +48 -0
  24. package/out/lsp/domain-lang-code-lens-provider.js.map +1 -0
  25. package/out/lsp/domain-lang-completion.js +39 -15
  26. package/out/lsp/domain-lang-completion.js.map +1 -1
  27. package/out/lsp/domain-lang-document-symbol-provider.js +5 -5
  28. package/out/lsp/domain-lang-document-symbol-provider.js.map +1 -1
  29. package/out/lsp/domain-lang-formatter.js +32 -0
  30. package/out/lsp/domain-lang-formatter.js.map +1 -1
  31. package/out/lsp/domain-lang-index-manager.d.ts +2 -3
  32. package/out/lsp/domain-lang-index-manager.js +5 -8
  33. package/out/lsp/domain-lang-index-manager.js.map +1 -1
  34. package/out/lsp/domain-lang-workspace-manager.d.ts +1 -1
  35. package/out/lsp/domain-lang-workspace-manager.js +2 -26
  36. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  37. package/out/lsp/explain.js +9 -3
  38. package/out/lsp/explain.js.map +1 -1
  39. package/out/lsp/hover/domain-lang-hover.js +13 -11
  40. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  41. package/out/lsp/hover/domain-lang-keywords.js +29 -26
  42. package/out/lsp/hover/domain-lang-keywords.js.map +1 -1
  43. package/out/lsp/tool-handlers.js +63 -57
  44. package/out/lsp/tool-handlers.js.map +1 -1
  45. package/out/sdk/ast-augmentation.d.ts +29 -21
  46. package/out/sdk/ast-augmentation.js +11 -7
  47. package/out/sdk/ast-augmentation.js.map +1 -1
  48. package/out/sdk/index.d.ts +2 -2
  49. package/out/sdk/index.js +1 -1
  50. package/out/sdk/index.js.map +1 -1
  51. package/out/sdk/loader-node.js +2 -2
  52. package/out/sdk/loader-node.js.map +1 -1
  53. package/out/sdk/patterns.d.ts +50 -61
  54. package/out/sdk/patterns.js +92 -62
  55. package/out/sdk/patterns.js.map +1 -1
  56. package/out/sdk/query.js +54 -43
  57. package/out/sdk/query.js.map +1 -1
  58. package/out/sdk/serializers.js +20 -7
  59. package/out/sdk/serializers.js.map +1 -1
  60. package/out/sdk/types.d.ts +87 -18
  61. package/out/sdk/types.js.map +1 -1
  62. package/out/sdk/validator.js +48 -64
  63. package/out/sdk/validator.js.map +1 -1
  64. package/out/services/performance-optimizer.d.ts +3 -3
  65. package/out/services/performance-optimizer.js +1 -3
  66. package/out/services/performance-optimizer.js.map +1 -1
  67. package/out/services/relationship-inference.d.ts +4 -4
  68. package/out/services/relationship-inference.js +34 -46
  69. package/out/services/relationship-inference.js.map +1 -1
  70. package/out/syntaxes/domain-lang.monarch.js +1 -1
  71. package/out/syntaxes/domain-lang.monarch.js.map +1 -1
  72. package/out/utils/import-utils.d.ts +6 -20
  73. package/out/utils/import-utils.js +3 -63
  74. package/out/utils/import-utils.js.map +1 -1
  75. package/out/validation/constants.d.ts +23 -6
  76. package/out/validation/constants.js +24 -7
  77. package/out/validation/constants.js.map +1 -1
  78. package/out/validation/maps.js +10 -4
  79. package/out/validation/maps.js.map +1 -1
  80. package/out/validation/relationships.d.ts +4 -8
  81. package/out/validation/relationships.js +96 -48
  82. package/out/validation/relationships.js.map +1 -1
  83. package/package.json +5 -2
  84. package/src/ast-augmentation.ts +7 -2
  85. package/src/diagram/context-map-diagram-generator.ts +513 -0
  86. package/src/diagram/context-map-layout-configurator.ts +43 -0
  87. package/src/diagram/elk-layout-factory.ts +83 -0
  88. package/src/domain-lang-module.ts +19 -2
  89. package/src/domain-lang.langium +62 -26
  90. package/src/generated/ast.ts +413 -63
  91. package/src/generated/grammar.ts +418 -172
  92. package/src/index.ts +5 -0
  93. package/src/lsp/domain-lang-code-lens-provider.ts +54 -0
  94. package/src/lsp/domain-lang-completion.ts +42 -15
  95. package/src/lsp/domain-lang-document-symbol-provider.ts +5 -5
  96. package/src/lsp/domain-lang-formatter.ts +34 -0
  97. package/src/lsp/domain-lang-index-manager.ts +6 -9
  98. package/src/lsp/domain-lang-workspace-manager.ts +3 -29
  99. package/src/lsp/explain.ts +10 -2
  100. package/src/lsp/hover/domain-lang-hover.ts +10 -8
  101. package/src/lsp/hover/domain-lang-keywords.ts +27 -24
  102. package/src/lsp/tool-handlers.ts +61 -47
  103. package/src/sdk/ast-augmentation.ts +30 -21
  104. package/src/sdk/index.ts +11 -1
  105. package/src/sdk/loader-node.ts +2 -2
  106. package/src/sdk/patterns.ts +114 -76
  107. package/src/sdk/query.ts +57 -48
  108. package/src/sdk/serializers.ts +20 -7
  109. package/src/sdk/types.ts +92 -17
  110. package/src/sdk/validator.ts +52 -69
  111. package/src/services/performance-optimizer.ts +4 -6
  112. package/src/services/relationship-inference.ts +43 -54
  113. package/src/utils/import-utils.ts +9 -74
  114. package/src/validation/constants.ts +32 -9
  115. package/src/validation/maps.ts +12 -4
  116. 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
- * Unified view of a relationship between two BoundedContexts.
285
- * Relationships can be defined in BoundedContext blocks or ContextMap.
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 RelationshipView {
288
- /** Left-hand side BoundedContext */
289
- readonly left: BoundedContext;
290
- /** Right-hand side BoundedContext */
291
- readonly right: BoundedContext;
292
- /** Relationship direction arrow */
293
- readonly arrow: '->' | '<-' | '<->' | '><';
294
- /** Integration patterns on left side (e.g., ['OHS', 'PL']) */
295
- readonly leftPatterns: readonly string[];
296
- /** Integration patterns on right side (e.g., ['CF', 'ACL']) */
297
- readonly rightPatterns: readonly string[];
298
- /** Explicit relationship type if specified */
299
- readonly type?: string;
300
- /** SDK-inferred relationship type based on patterns */
301
- readonly inferredType?: string;
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.
@@ -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: ValidationDiagnostic[] = [];
157
- const warnings: ValidationDiagnostic[] = [];
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: ValidationDiagnostic[] = [];
319
- const warnings: ValidationDiagnostic[] = [];
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 | undefined> {
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
- if (!globalOptimizer) {
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 types
16
- * from roles and arrow directions.
21
+ * Enriches relationships in the model by inferring relationship kinds
22
+ * from AST node type, patterns, and arrow direction.
17
23
  *
18
- * This service walks the entire model structure and applies inference
19
- * rules to relationships that don't have an explicit type.
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
- containerBc?: BoundedContext
38
+ _containerBc?: BoundedContext
36
39
  ): void {
37
40
  for (const element of elements) {
38
- if (isNamespaceDeclaration(element)) {
39
- walkStructureElements(element.children, containerBc);
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 type if not explicitly set.
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 (!rel.type) {
79
- rel.inferredType = inferRelationshipType(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 relationship type from arrow direction and roles.
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 inferRelationshipType(relationship: Relationship): string | undefined {
96
- const leftPatterns = (relationship.leftPatterns ?? []).map((r: string) => r.toUpperCase());
97
- const rightPatterns = (relationship.rightPatterns ?? []).map((r: string) => r.toUpperCase());
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
- if (relationship.arrow === '<->') {
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 (relationship.arrow === '->' || relationship.arrow === '<-') {
117
- return 'UpstreamDownstream';
106
+ if (hasSupplier || hasCustomer) {
107
+ return 'CustomerSupplier';
118
108
  }
119
-
120
- return undefined;
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 { ManifestManager } from '../services/workspace-manager.js';
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 - Optional DI-injected ImportResolver. When provided,
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?: 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 - Optional DI-injected ImportResolver. When provided,
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?: 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
- // Use DI-injected resolver when available, falling back to standalone
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] A -> [CF] B').`,
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.