@domainlang/language 0.12.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 (90) hide show
  1. package/out/ast-augmentation.d.ts +7 -2
  2. package/out/diagram/context-map-diagram-generator.d.ts +9 -2
  3. package/out/diagram/context-map-diagram-generator.js +112 -63
  4. package/out/diagram/context-map-diagram-generator.js.map +1 -1
  5. package/out/generated/ast.d.ts +323 -51
  6. package/out/generated/ast.js +194 -33
  7. package/out/generated/ast.js.map +1 -1
  8. package/out/generated/grammar.js +418 -172
  9. package/out/generated/grammar.js.map +1 -1
  10. package/out/lsp/domain-lang-completion.js +39 -15
  11. package/out/lsp/domain-lang-completion.js.map +1 -1
  12. package/out/lsp/domain-lang-formatter.js +32 -0
  13. package/out/lsp/domain-lang-formatter.js.map +1 -1
  14. package/out/lsp/domain-lang-index-manager.d.ts +2 -3
  15. package/out/lsp/domain-lang-index-manager.js +5 -8
  16. package/out/lsp/domain-lang-index-manager.js.map +1 -1
  17. package/out/lsp/domain-lang-workspace-manager.d.ts +1 -1
  18. package/out/lsp/domain-lang-workspace-manager.js +2 -26
  19. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  20. package/out/lsp/explain.js +9 -3
  21. package/out/lsp/explain.js.map +1 -1
  22. package/out/lsp/hover/domain-lang-hover.js +13 -11
  23. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  24. package/out/lsp/hover/domain-lang-keywords.js +29 -26
  25. package/out/lsp/hover/domain-lang-keywords.js.map +1 -1
  26. package/out/sdk/ast-augmentation.d.ts +29 -21
  27. package/out/sdk/ast-augmentation.js +11 -7
  28. package/out/sdk/ast-augmentation.js.map +1 -1
  29. package/out/sdk/index.d.ts +2 -2
  30. package/out/sdk/index.js +1 -1
  31. package/out/sdk/index.js.map +1 -1
  32. package/out/sdk/loader-node.js +2 -2
  33. package/out/sdk/loader-node.js.map +1 -1
  34. package/out/sdk/patterns.d.ts +50 -61
  35. package/out/sdk/patterns.js +92 -62
  36. package/out/sdk/patterns.js.map +1 -1
  37. package/out/sdk/query.js +54 -43
  38. package/out/sdk/query.js.map +1 -1
  39. package/out/sdk/serializers.js +20 -7
  40. package/out/sdk/serializers.js.map +1 -1
  41. package/out/sdk/types.d.ts +87 -18
  42. package/out/sdk/types.js.map +1 -1
  43. package/out/sdk/validator.js +48 -64
  44. package/out/sdk/validator.js.map +1 -1
  45. package/out/services/performance-optimizer.d.ts +3 -3
  46. package/out/services/performance-optimizer.js +1 -3
  47. package/out/services/performance-optimizer.js.map +1 -1
  48. package/out/services/relationship-inference.d.ts +4 -4
  49. package/out/services/relationship-inference.js +34 -46
  50. package/out/services/relationship-inference.js.map +1 -1
  51. package/out/syntaxes/domain-lang.monarch.js +1 -1
  52. package/out/syntaxes/domain-lang.monarch.js.map +1 -1
  53. package/out/utils/import-utils.d.ts +6 -20
  54. package/out/utils/import-utils.js +3 -63
  55. package/out/utils/import-utils.js.map +1 -1
  56. package/out/validation/constants.d.ts +23 -6
  57. package/out/validation/constants.js +24 -7
  58. package/out/validation/constants.js.map +1 -1
  59. package/out/validation/maps.js +10 -4
  60. package/out/validation/maps.js.map +1 -1
  61. package/out/validation/relationships.d.ts +4 -8
  62. package/out/validation/relationships.js +96 -48
  63. package/out/validation/relationships.js.map +1 -1
  64. package/package.json +1 -1
  65. package/src/ast-augmentation.ts +7 -2
  66. package/src/diagram/context-map-diagram-generator.ts +132 -70
  67. package/src/domain-lang.langium +62 -26
  68. package/src/generated/ast.ts +413 -63
  69. package/src/generated/grammar.ts +418 -172
  70. package/src/lsp/domain-lang-completion.ts +42 -15
  71. package/src/lsp/domain-lang-formatter.ts +34 -0
  72. package/src/lsp/domain-lang-index-manager.ts +6 -9
  73. package/src/lsp/domain-lang-workspace-manager.ts +3 -29
  74. package/src/lsp/explain.ts +10 -2
  75. package/src/lsp/hover/domain-lang-hover.ts +10 -8
  76. package/src/lsp/hover/domain-lang-keywords.ts +27 -24
  77. package/src/sdk/ast-augmentation.ts +30 -21
  78. package/src/sdk/index.ts +11 -1
  79. package/src/sdk/loader-node.ts +2 -2
  80. package/src/sdk/patterns.ts +114 -76
  81. package/src/sdk/query.ts +57 -48
  82. package/src/sdk/serializers.ts +20 -7
  83. package/src/sdk/types.ts +92 -17
  84. package/src/sdk/validator.ts +52 -69
  85. package/src/services/performance-optimizer.ts +4 -6
  86. package/src/services/relationship-inference.ts +43 -54
  87. package/src/utils/import-utils.ts +9 -74
  88. package/src/validation/constants.ts +32 -9
  89. package/src/validation/maps.ts +12 -4
  90. package/src/validation/relationships.ts +150 -71
@@ -1,147 +1,185 @@
1
- /**
2
- * Integration Patterns - Type-safe constants for DDD integration patterns.
3
- *
4
- * Use these constants instead of magic strings when checking relationship patterns.
5
- *
6
- * @example
7
- * ```typescript
8
- * import { Pattern } from '../sdk/patterns.js';
9
- *
10
- * // Instead of: hasPattern('SK') or hasPattern('SharedKernel')
11
- * // Use:
12
- * if (relationship.hasPattern(Pattern.SharedKernel)) {
13
- * // ...
14
- * }
15
- * ```
16
- */
1
+ import type { SidePattern, SymmetricPattern } from '../generated/ast.js';
2
+ import {
3
+ isOpenHostService,
4
+ isPublishedLanguage,
5
+ isConformist,
6
+ isAntiCorruptionLayer,
7
+ isSupplier,
8
+ isCustomer,
9
+ isBigBallOfMud,
10
+ isSharedKernel,
11
+ isPartnership,
12
+ isSeparateWays,
13
+ } from '../generated/ast.js';
17
14
 
18
15
  /**
19
- * Integration pattern abbreviations as used in the grammar.
20
- * These are the canonical abbreviations recognized by DomainLang.
16
+ * Pattern constants for programmatic use.
17
+ * Values match the AST $type names.
21
18
  */
22
19
  export const Pattern = {
23
- // Upstream patterns (provider side)
24
- /** Open Host Service - exposes a clean API for consumers */
25
- OHS: 'OHS',
26
- /** Published Language - shared data format/protocol */
27
- PL: 'PL',
28
-
29
- // Downstream patterns (consumer side)
30
- /** Conformist - accepts upstream model without translation */
31
- CF: 'CF',
32
- /** Anti-Corruption Layer - translates upstream model */
33
- ACL: 'ACL',
34
-
35
- // Mutual patterns (both sides)
36
- /** Shared Kernel - shared code/model ownership */
37
- SK: 'SK',
38
- /** Partnership - coordinated development */
39
- P: 'P',
40
-
41
- // Relationship types
42
- /** Customer/Supplier - negotiated contract */
43
- CustomerSupplier: 'Customer/Supplier',
44
- /** Separate Ways - no integration */
45
- SeparateWays: 'Separate Ways',
46
- /** Big Ball of Mud - legacy or unstructured */
47
- BigBallOfMud: 'Big Ball of Mud',
20
+ // Side patterns (directional)
21
+ OHS: 'OpenHostService',
22
+ PL: 'PublishedLanguage',
23
+ CF: 'Conformist',
24
+ ACL: 'AntiCorruptionLayer',
25
+ S: 'Supplier',
26
+ C: 'Customer',
27
+ BBoM: 'BigBallOfMud',
28
+ // Symmetric patterns
29
+ SK: 'SharedKernel',
30
+ P: 'Partnership',
31
+ SW: 'SeparateWays',
48
32
  } as const;
49
33
 
50
34
  /**
51
- * Full names for integration patterns.
52
- * Used when patterns are spelled out in documentation blocks.
35
+ * Mapping from short abbreviation to full $type name.
53
36
  */
54
- export const PatternFullName = {
37
+ export const PatternFullName: Record<string, string> = {
55
38
  OHS: 'OpenHostService',
56
39
  PL: 'PublishedLanguage',
57
40
  CF: 'Conformist',
58
41
  ACL: 'AntiCorruptionLayer',
42
+ S: 'Supplier',
43
+ C: 'Customer',
44
+ BBoM: 'BigBallOfMud',
59
45
  SK: 'SharedKernel',
60
46
  P: 'Partnership',
61
- } as const;
47
+ SW: 'SeparateWays',
48
+ };
49
+
50
+ /**
51
+ * Mapping from $type name to short abbreviation.
52
+ */
53
+ export const PatternAbbreviation: Record<string, string> = {
54
+ OpenHostService: 'OHS',
55
+ PublishedLanguage: 'PL',
56
+ Conformist: 'CF',
57
+ AntiCorruptionLayer: 'ACL',
58
+ Supplier: 'S',
59
+ Customer: 'C',
60
+ BigBallOfMud: 'BBoM',
61
+ SharedKernel: 'SK',
62
+ Partnership: 'P',
63
+ SeparateWays: 'SW',
64
+ };
62
65
 
63
66
  /**
64
- * Mapping from abbreviations to full names and vice versa.
67
+ * All short+long forms that map to a given canonical $type name.
65
68
  */
66
69
  export const PatternAliases: Record<string, readonly string[]> = {
67
- // Abbreviation -> [abbreviation, fullName]
68
70
  OHS: ['OHS', 'OpenHostService'],
69
71
  PL: ['PL', 'PublishedLanguage'],
70
72
  CF: ['CF', 'Conformist'],
71
73
  ACL: ['ACL', 'AntiCorruptionLayer'],
74
+ S: ['S', 'Supplier'],
75
+ C: ['C', 'Customer'],
76
+ BBoM: ['BBoM', 'BigBallOfMud'],
72
77
  SK: ['SK', 'SharedKernel'],
73
78
  P: ['P', 'Partnership'],
74
-
75
- // Full names map to same
79
+ SW: ['SW', 'SeparateWays'],
76
80
  OpenHostService: ['OHS', 'OpenHostService'],
77
81
  PublishedLanguage: ['PL', 'PublishedLanguage'],
78
82
  Conformist: ['CF', 'Conformist'],
79
83
  AntiCorruptionLayer: ['ACL', 'AntiCorruptionLayer'],
84
+ Supplier: ['S', 'Supplier'],
85
+ Customer: ['C', 'Customer'],
86
+ BigBallOfMud: ['BBoM', 'BigBallOfMud'],
80
87
  SharedKernel: ['SK', 'SharedKernel'],
81
88
  Partnership: ['P', 'Partnership'],
89
+ SeparateWays: ['SW', 'SeparateWays'],
82
90
  };
83
91
 
84
- /**
85
- * Type representing any valid integration pattern (abbreviation or full name).
86
- */
87
- export type IntegrationPattern =
88
- | typeof Pattern[keyof typeof Pattern]
89
- | typeof PatternFullName[keyof typeof PatternFullName];
92
+ /** Union of all pattern type names */
93
+ export type IntegrationPattern = typeof Pattern[keyof typeof Pattern];
90
94
 
91
95
  /**
92
- * Checks if a pattern string matches any of the expected aliases.
93
- * Handles both abbreviations and full names case-insensitively.
94
- *
95
- * @param actual - Pattern string from the AST
96
- * @param expected - Pattern to match (abbreviation or full name)
97
- * @returns true if patterns match
96
+ * Checks if a pattern name matches an expected pattern.
97
+ * Works with both $type names and short abbreviations.
98
98
  */
99
99
  export function matchesPattern(actual: string, expected: string): boolean {
100
100
  const normalizedActual = actual.trim();
101
101
  const aliases = PatternAliases[expected];
102
-
103
102
  if (aliases) {
104
103
  return aliases.some(alias =>
105
104
  alias.toLowerCase() === normalizedActual.toLowerCase()
106
105
  );
107
106
  }
108
-
109
- // Fallback: direct comparison
110
107
  return normalizedActual.toLowerCase() === expected.toLowerCase();
111
108
  }
112
109
 
110
+ /** Side patterns that belong on the upstream side */
111
+ export const UpstreamPatterns: readonly string[] = ['OpenHostService', 'PublishedLanguage', 'Supplier'];
112
+ /** Side patterns that belong on the downstream side */
113
+ export const DownstreamPatterns: readonly string[] = ['Conformist', 'AntiCorruptionLayer', 'Customer'];
114
+ /** Symmetric patterns (mutual) */
115
+ export const SymmetricPatterns: readonly string[] = ['SharedKernel', 'Partnership', 'SeparateWays'];
116
+
113
117
  /**
114
- * Patterns that are typically on the upstream (provider) side.
118
+ * Checks if a side pattern AST node is an upstream pattern.
115
119
  */
116
- export const UpstreamPatterns: readonly string[] = ['OHS', 'OpenHostService', 'PL', 'PublishedLanguage'];
120
+ export function isUpstreamSidePattern(pattern: SidePattern): boolean {
121
+ return isOpenHostService(pattern) || isPublishedLanguage(pattern) || isSupplier(pattern);
122
+ }
117
123
 
118
124
  /**
119
- * Patterns that are typically on the downstream (consumer) side.
125
+ * Checks if a side pattern AST node is a downstream pattern.
120
126
  */
121
- export const DownstreamPatterns: readonly string[] = ['CF', 'Conformist', 'ACL', 'AntiCorruptionLayer'];
127
+ export function isDownstreamSidePattern(pattern: SidePattern): boolean {
128
+ return isConformist(pattern) || isAntiCorruptionLayer(pattern) || isCustomer(pattern);
129
+ }
122
130
 
123
131
  /**
124
- * Patterns that require mutual/bidirectional relationships.
132
+ * Checks if a side pattern AST node is a Big Ball of Mud.
125
133
  */
126
- export const MutualPatterns: readonly string[] = ['SK', 'SharedKernel', 'P', 'Partnership'];
134
+ export function isBBoMSidePattern(pattern: SidePattern): boolean {
135
+ return isBigBallOfMud(pattern);
136
+ }
127
137
 
128
138
  /**
129
- * Checks if a pattern is typically an upstream pattern.
139
+ * Checks if a pattern string name is an upstream pattern.
130
140
  */
131
141
  export function isUpstreamPattern(pattern: string): boolean {
132
142
  return UpstreamPatterns.some(p => matchesPattern(pattern, p));
133
143
  }
134
144
 
135
145
  /**
136
- * Checks if a pattern is typically a downstream pattern.
146
+ * Checks if a pattern string name is a downstream pattern.
137
147
  */
138
148
  export function isDownstreamPattern(pattern: string): boolean {
139
149
  return DownstreamPatterns.some(p => matchesPattern(pattern, p));
140
150
  }
141
151
 
142
152
  /**
143
- * Checks if a pattern requires bidirectional relationships.
153
+ * Checks if a pattern string name is a mutual/symmetric pattern.
144
154
  */
145
155
  export function isMutualPattern(pattern: string): boolean {
146
- return MutualPatterns.some(p => matchesPattern(pattern, p));
156
+ return SymmetricPatterns.some(p => matchesPattern(pattern, p));
157
+ }
158
+
159
+ /**
160
+ * Gets the short abbreviation for a pattern $type name.
161
+ */
162
+ export function getPatternAbbreviation(typeName: string): string {
163
+ return PatternAbbreviation[typeName] ?? typeName;
164
+ }
165
+
166
+ /**
167
+ * Checks if a symmetric pattern is a Shared Kernel.
168
+ */
169
+ export function isSharedKernelPattern(pattern: SymmetricPattern): boolean {
170
+ return isSharedKernel(pattern);
171
+ }
172
+
173
+ /**
174
+ * Checks if a symmetric pattern is a Partnership.
175
+ */
176
+ export function isPartnershipPattern(pattern: SymmetricPattern): boolean {
177
+ return isPartnership(pattern);
178
+ }
179
+
180
+ /**
181
+ * Checks if a symmetric pattern is Separate Ways.
182
+ */
183
+ export function isSeparateWaysPattern(pattern: SymmetricPattern): boolean {
184
+ return isSeparateWays(pattern);
147
185
  }
package/src/sdk/query.ts CHANGED
@@ -21,12 +21,16 @@ import {
21
21
  isBoundedContext,
22
22
  isClassification,
23
23
  isContextMap,
24
+ isDirectionalRelationship,
24
25
  isDomain,
25
26
  isDomainMap,
26
27
  isModel,
27
28
  isNamespaceDeclaration,
29
+ isSymmetricRelationship,
28
30
  isTeam,
29
31
  isThisRef,
32
+ isSupplier,
33
+ isCustomer,
30
34
  } from '../generated/ast.js';
31
35
  import { QualifiedNameProvider } from '../lsp/domain-lang-naming.js';
32
36
  import type { DomainLangServices } from '../domain-lang-module.js';
@@ -39,6 +43,7 @@ import {
39
43
  import { isDownstreamPattern, isUpstreamPattern, matchesPattern } from './patterns.js';
40
44
  import type {
41
45
  BcQueryBuilder,
46
+ DirectionalKind,
42
47
  ModelIndexes,
43
48
  Query,
44
49
  QueryBuilder,
@@ -331,14 +336,39 @@ class QueryImpl implements Query {
331
336
  return undefined;
332
337
  }
333
338
 
339
+ if (isSymmetricRelationship(rel)) {
340
+ // Grammar invariant: either `arrow === '><'` (SeparateWays shorthand, pattern undefined)
341
+ // OR `pattern` is set (explicit [SK]/[P]/[SW] brackets). Never both; never neither
342
+ // (validation rejects a bare "A B" without pattern or arrow).
343
+ // The fallback to 'SeparateWays' correctly handles the `><` case.
344
+ return {
345
+ type: 'symmetric' as const,
346
+ kind: (rel.pattern?.$type ?? 'SeparateWays') as 'SharedKernel' | 'Partnership' | 'SeparateWays',
347
+ left: { context: left, patterns: [] },
348
+ right: { context: right, patterns: [] },
349
+ source,
350
+ astNode: rel,
351
+ };
352
+ }
353
+
354
+ const arrow = rel.arrow as '->' | '<-' | '<->';
355
+ const leftSide = { context: left, patterns: rel.leftPatterns };
356
+ const rightSide = { context: right, patterns: rel.rightPatterns };
357
+ const hasSupplier = rel.leftPatterns.some(isSupplier) || rel.rightPatterns.some(isSupplier);
358
+ const hasCustomer = rel.leftPatterns.some(isCustomer) || rel.rightPatterns.some(isCustomer);
359
+ const kind: DirectionalKind = arrow === '<->'
360
+ ? 'Bidirectional'
361
+ : (hasSupplier || hasCustomer) ? 'CustomerSupplier' : 'UpstreamDownstream';
362
+ const upstreamSide = arrow === '->' ? leftSide : arrow === '<-' ? rightSide : undefined;
363
+ const downstreamSide = arrow === '->' ? rightSide : arrow === '<-' ? leftSide : undefined;
334
364
  return {
335
- left,
336
- right,
337
- arrow: rel.arrow,
338
- leftPatterns: rel.leftPatterns,
339
- rightPatterns: rel.rightPatterns,
340
- type: rel.type,
341
- inferredType: this.inferRelationshipType(rel),
365
+ type: 'directional' as const,
366
+ kind,
367
+ arrow,
368
+ left: leftSide,
369
+ right: rightSide,
370
+ upstream: upstreamSide,
371
+ downstream: downstreamSide,
342
372
  source,
343
373
  astNode: rel,
344
374
  };
@@ -358,41 +388,6 @@ class QueryImpl implements Query {
358
388
  return ref.link?.ref;
359
389
  }
360
390
 
361
- /**
362
- * Infers relationship type from integration patterns.
363
- * Simple heuristic based on common DDD pattern combinations.
364
- */
365
- private inferRelationshipType(rel: Relationship): string | undefined {
366
- const leftPatterns = rel.leftPatterns;
367
- const rightPatterns = rel.rightPatterns;
368
-
369
- // Partnership: Bidirectional with P or SK
370
- if (rel.arrow === '<->' && (leftPatterns.includes('P') || leftPatterns.includes('SK'))) {
371
- return 'Partnership';
372
- }
373
-
374
- // Shared Kernel: SK pattern
375
- if (leftPatterns.includes('SK') || rightPatterns.includes('SK')) {
376
- return 'SharedKernel';
377
- }
378
-
379
- // Customer-Supplier: OHS + CF
380
- if (leftPatterns.includes('OHS') && rightPatterns.includes('CF')) {
381
- return 'CustomerSupplier';
382
- }
383
-
384
- // Upstream-Downstream: directional arrow
385
- if (rel.arrow === '->') {
386
- return 'UpstreamDownstream';
387
- }
388
-
389
- // Separate Ways
390
- if (rel.arrow === '><') {
391
- return 'SeparateWays';
392
- }
393
-
394
- return undefined;
395
- }
396
391
  }
397
392
 
398
393
  /**
@@ -726,38 +721,52 @@ export function augmentRelationship(rel: Relationship, containingBc?: BoundedCon
726
721
  // Helper methods for pattern matching (type-safe, no magic strings)
727
722
  hasPattern: {
728
723
  value: (pattern: string): boolean => {
729
- return rel.leftPatterns.some(p => matchesPattern(p, pattern)) ||
730
- rel.rightPatterns.some(p => matchesPattern(p, pattern));
724
+ if (isDirectionalRelationship(rel)) {
725
+ return rel.leftPatterns.some(p => matchesPattern(p.$type, pattern)) ||
726
+ rel.rightPatterns.some(p => matchesPattern(p.$type, pattern));
727
+ }
728
+ if (isSymmetricRelationship(rel) && rel.pattern) {
729
+ return matchesPattern(rel.pattern.$type, pattern);
730
+ }
731
+ return false;
731
732
  },
732
733
  enumerable: false,
733
734
  configurable: true,
734
735
  },
735
736
  hasLeftPattern: {
736
737
  value: (pattern: string): boolean => {
737
- return rel.leftPatterns.some(p => matchesPattern(p, pattern));
738
+ if (isDirectionalRelationship(rel)) {
739
+ return rel.leftPatterns.some(p => matchesPattern(p.$type, pattern));
740
+ }
741
+ return false;
738
742
  },
739
743
  enumerable: false,
740
744
  configurable: true,
741
745
  },
742
746
  hasRightPattern: {
743
747
  value: (pattern: string): boolean => {
744
- return rel.rightPatterns.some(p => matchesPattern(p, pattern));
748
+ if (isDirectionalRelationship(rel)) {
749
+ return rel.rightPatterns.some(p => matchesPattern(p.$type, pattern));
750
+ }
751
+ return false;
745
752
  },
746
753
  enumerable: false,
747
754
  configurable: true,
748
755
  },
749
756
  isUpstream: {
750
757
  value: (side: 'left' | 'right'): boolean => {
758
+ if (!isDirectionalRelationship(rel)) return false;
751
759
  const patterns = side === 'left' ? rel.leftPatterns : rel.rightPatterns;
752
- return patterns.some(p => isUpstreamPattern(p));
760
+ return patterns.some(p => isUpstreamPattern(p.$type));
753
761
  },
754
762
  enumerable: false,
755
763
  configurable: true,
756
764
  },
757
765
  isDownstream: {
758
766
  value: (side: 'left' | 'right'): boolean => {
767
+ if (!isDirectionalRelationship(rel)) return false;
759
768
  const patterns = side === 'left' ? rel.leftPatterns : rel.rightPatterns;
760
- return patterns.some(p => isDownstreamPattern(p));
769
+ return patterns.some(p => isDownstreamPattern(p.$type));
761
770
  },
762
771
  enumerable: false,
763
772
  configurable: true,
@@ -165,18 +165,31 @@ export function serializeNode(node: AstNode, query: Query): Record<string, unkno
165
165
  * @returns Serialized relationship object
166
166
  */
167
167
  export function serializeRelationship(view: RelationshipView): Record<string, unknown> {
168
- // RelationshipView.left and .right are BoundedContext (which have name property)
169
- const leftName = view.left.name;
170
- const rightName = view.right.name;
168
+ const leftName = view.left.context.name;
169
+ const rightName = view.right.context.name;
170
+ if (view.type === 'symmetric') {
171
+ const patternDisplay = view.kind === 'SeparateWays' ? '><' : `[${view.kind}]`;
172
+ return {
173
+ type: 'symmetric',
174
+ name: `${leftName} ${patternDisplay} ${rightName}`,
175
+ left: leftName,
176
+ right: rightName,
177
+ kind: view.kind,
178
+ source: view.source,
179
+ };
180
+ }
171
181
  return {
172
- $type: 'Relationship',
182
+ type: 'directional',
183
+ kind: view.kind,
173
184
  name: `${leftName} ${view.arrow} ${rightName}`,
174
185
  left: leftName,
175
186
  right: rightName,
176
187
  arrow: view.arrow,
177
- leftPatterns: view.leftPatterns,
178
- rightPatterns: view.rightPatterns,
179
- inferredType: view.inferredType,
188
+ leftPatterns: view.left.patterns.map(p => p.$type),
189
+ rightPatterns: view.right.patterns.map(p => p.$type),
190
+ upstreamPatterns: view.upstream?.patterns.map(p => p.$type),
191
+ downstreamPatterns: view.downstream?.patterns.map(p => p.$type),
192
+ source: view.source,
180
193
  };
181
194
  }
182
195
 
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.