@domainlang/language 0.5.2 → 0.7.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/README.md +1 -1
  2. package/out/domain-lang-module.js +5 -1
  3. package/out/domain-lang-module.js.map +1 -1
  4. package/out/generated/ast.d.ts +24 -0
  5. package/out/generated/ast.js.map +1 -1
  6. package/out/generated/grammar.js +22 -32
  7. package/out/generated/grammar.js.map +1 -1
  8. package/out/index.d.ts +2 -5
  9. package/out/index.js +10 -6
  10. package/out/index.js.map +1 -1
  11. package/out/lsp/domain-lang-code-actions.js +14 -8
  12. package/out/lsp/domain-lang-code-actions.js.map +1 -1
  13. package/out/lsp/domain-lang-completion.d.ts +3 -0
  14. package/out/lsp/domain-lang-completion.js +41 -13
  15. package/out/lsp/domain-lang-completion.js.map +1 -1
  16. package/out/lsp/domain-lang-formatter.js +24 -18
  17. package/out/lsp/domain-lang-formatter.js.map +1 -1
  18. package/out/lsp/domain-lang-index-manager.d.ts +170 -0
  19. package/out/lsp/domain-lang-index-manager.js +389 -0
  20. package/out/lsp/domain-lang-index-manager.js.map +1 -0
  21. package/out/lsp/domain-lang-scope-provider.d.ts +67 -0
  22. package/out/lsp/domain-lang-scope-provider.js +95 -0
  23. package/out/lsp/domain-lang-scope-provider.js.map +1 -0
  24. package/out/lsp/domain-lang-scope.js +31 -17
  25. package/out/lsp/domain-lang-scope.js.map +1 -1
  26. package/out/lsp/domain-lang-workspace-manager.d.ts +76 -9
  27. package/out/lsp/domain-lang-workspace-manager.js +176 -54
  28. package/out/lsp/domain-lang-workspace-manager.js.map +1 -1
  29. package/out/lsp/hover/domain-lang-hover.d.ts +45 -1
  30. package/out/lsp/hover/domain-lang-hover.js +308 -232
  31. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  32. package/out/lsp/hover/domain-lang-keywords.d.ts +3 -7
  33. package/out/lsp/hover/domain-lang-keywords.js +115 -38
  34. package/out/lsp/hover/domain-lang-keywords.js.map +1 -1
  35. package/out/lsp/manifest-diagnostics.js +95 -50
  36. package/out/lsp/manifest-diagnostics.js.map +1 -1
  37. package/out/main.js +204 -17
  38. package/out/main.js.map +1 -1
  39. package/out/services/import-resolver.d.ts +39 -2
  40. package/out/services/import-resolver.js +77 -12
  41. package/out/services/import-resolver.js.map +1 -1
  42. package/out/services/types.d.ts +2 -2
  43. package/out/services/workspace-manager.d.ts +33 -31
  44. package/out/services/workspace-manager.js +92 -148
  45. package/out/services/workspace-manager.js.map +1 -1
  46. package/out/utils/document-utils.d.ts +41 -0
  47. package/out/utils/document-utils.js +64 -0
  48. package/out/utils/document-utils.js.map +1 -0
  49. package/out/utils/import-utils.d.ts +0 -17
  50. package/out/utils/import-utils.js +2 -32
  51. package/out/utils/import-utils.js.map +1 -1
  52. package/out/utils/manifest-utils.d.ts +56 -0
  53. package/out/utils/manifest-utils.js +119 -0
  54. package/out/utils/manifest-utils.js.map +1 -0
  55. package/out/validation/constants.d.ts +13 -0
  56. package/out/validation/constants.js +18 -0
  57. package/out/validation/constants.js.map +1 -1
  58. package/out/validation/import.d.ts +12 -2
  59. package/out/validation/import.js +95 -22
  60. package/out/validation/import.js.map +1 -1
  61. package/out/validation/maps.js +51 -2
  62. package/out/validation/maps.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/domain-lang-module.ts +6 -1
  65. package/src/domain-lang.langium +37 -13
  66. package/src/generated/ast.ts +24 -0
  67. package/src/generated/grammar.ts +22 -32
  68. package/src/index.ts +12 -6
  69. package/src/lsp/domain-lang-code-actions.ts +13 -8
  70. package/src/lsp/domain-lang-completion.ts +61 -13
  71. package/src/lsp/domain-lang-formatter.ts +28 -23
  72. package/src/lsp/domain-lang-index-manager.ts +447 -0
  73. package/src/lsp/domain-lang-scope-provider.ts +134 -0
  74. package/src/lsp/domain-lang-scope.ts +29 -17
  75. package/src/lsp/domain-lang-workspace-manager.ts +201 -53
  76. package/src/lsp/hover/domain-lang-hover.ts +332 -226
  77. package/src/lsp/hover/domain-lang-keywords.ts +129 -43
  78. package/src/lsp/manifest-diagnostics.ts +100 -59
  79. package/src/main.ts +258 -16
  80. package/src/services/import-resolver.ts +91 -12
  81. package/src/services/types.ts +2 -2
  82. package/src/services/workspace-manager.ts +101 -175
  83. package/src/utils/document-utils.ts +80 -0
  84. package/src/utils/import-utils.ts +2 -40
  85. package/src/utils/manifest-utils.ts +132 -0
  86. package/src/validation/constants.ts +24 -0
  87. package/src/validation/import.ts +107 -24
  88. package/src/validation/maps.ts +59 -2
  89. package/out/lsp/hover/ddd-pattern-explanations.d.ts +0 -50
  90. package/out/lsp/hover/ddd-pattern-explanations.js +0 -196
  91. package/out/lsp/hover/ddd-pattern-explanations.js.map +0 -1
  92. package/out/services/dependency-analyzer.d.ts +0 -58
  93. package/out/services/dependency-analyzer.js +0 -254
  94. package/out/services/dependency-analyzer.js.map +0 -1
  95. package/out/services/dependency-resolver.d.ts +0 -146
  96. package/out/services/dependency-resolver.js +0 -452
  97. package/out/services/dependency-resolver.js.map +0 -1
  98. package/out/services/git-url-resolver.browser.d.ts +0 -10
  99. package/out/services/git-url-resolver.browser.js +0 -19
  100. package/out/services/git-url-resolver.browser.js.map +0 -1
  101. package/out/services/git-url-resolver.d.ts +0 -158
  102. package/out/services/git-url-resolver.js +0 -416
  103. package/out/services/git-url-resolver.js.map +0 -1
  104. package/out/services/governance-validator.d.ts +0 -44
  105. package/out/services/governance-validator.js +0 -153
  106. package/out/services/governance-validator.js.map +0 -1
  107. package/out/services/semver.d.ts +0 -98
  108. package/out/services/semver.js +0 -195
  109. package/out/services/semver.js.map +0 -1
  110. package/src/lsp/hover/ddd-pattern-explanations.ts +0 -237
  111. package/src/services/dependency-analyzer.ts +0 -321
  112. package/src/services/dependency-resolver.ts +0 -551
  113. package/src/services/git-url-resolver.browser.ts +0 -26
  114. package/src/services/git-url-resolver.ts +0 -517
  115. package/src/services/governance-validator.ts +0 -177
  116. package/src/services/semver.ts +0 -213
@@ -6,7 +6,7 @@ import type {
6
6
  MaybePromise,
7
7
  Reference
8
8
  } from 'langium';
9
- import { CstUtils, isAstNodeWithComment, isJSDoc, isReference, parseJSDoc } from 'langium';
9
+ import { CstUtils, isReference } from 'langium';
10
10
  import type { LangiumServices } from 'langium/lsp';
11
11
  import { AstNodeHoverProvider } from 'langium/lsp';
12
12
  import type { Hover, HoverParams } from 'vscode-languageserver';
@@ -16,20 +16,54 @@ import { QualifiedNameProvider } from '../domain-lang-naming.js';
16
16
  import { keywordExplanations } from './domain-lang-keywords.js';
17
17
  import { effectiveClassification, effectiveTeam } from '../../sdk/resolution.js';
18
18
 
19
+ /**
20
+ * Type-specific hover content generator.
21
+ * Returns undefined if the generator doesn't handle this node type.
22
+ */
23
+ type HoverContentGenerator = (node: AstNode, commentBlock: string) => string | undefined;
24
+
19
25
  /**
20
26
  * Provides hover information for DomainLang elements.
27
+ *
28
+ * Extends Langium's AstNodeHoverProvider with DDD-specific hover content
29
+ * for domains, bounded contexts, relationships, and other DSL constructs.
21
30
  */
22
31
  export class DomainLangHoverProvider extends AstNodeHoverProvider {
23
32
  protected readonly documentationProvider: DocumentationProvider;
24
33
  protected readonly commentProvider: CommentProvider;
25
34
  protected readonly qualifiedNameProvider: QualifiedNameProvider;
26
35
 
36
+ /**
37
+ * Registry of type-specific hover content generators.
38
+ * Each generator returns content for its node type, or undefined to skip.
39
+ */
40
+ private readonly hoverGenerators: HoverContentGenerator[];
41
+
27
42
  constructor(services: LangiumServices) {
28
43
  super(services);
29
44
  this.documentationProvider = services.documentation.DocumentationProvider;
30
45
  this.commentProvider = services.documentation.CommentProvider;
31
46
  const domainServices = services as DomainLangServices;
32
47
  this.qualifiedNameProvider = domainServices.references.QualifiedNameProvider;
48
+
49
+ // Register type-specific generators
50
+ this.hoverGenerators = [
51
+ (node, comment) => this.getDomainHover(node, comment),
52
+ (node, comment) => this.getThisRefHover(node, comment),
53
+ (node, comment) => this.getBoundedContextHover(node, comment),
54
+ (node, comment) => this.getNamespaceHover(node, comment),
55
+ (node, comment) => this.getContextMapHover(node, comment),
56
+ (node, comment) => this.getDomainMapHover(node, comment),
57
+ (node, comment) => this.getDecisionHover(node, comment),
58
+ (node, comment) => this.getPolicyHover(node, comment),
59
+ (node, comment) => this.getBusinessRuleHover(node, comment),
60
+ (node, comment) => this.getDomainTermHover(node, comment),
61
+ (node, comment) => this.getTeamHover(node, comment),
62
+ (node, comment) => this.getClassificationHover(node, comment),
63
+ (node, comment) => this.getMetadataHover(node, comment),
64
+ (node, comment) => this.getRelationshipHover(node, comment),
65
+ (node, comment) => this.getImportHover(node, comment),
66
+ ];
33
67
  }
34
68
 
35
69
  override async getHoverContent(document: LangiumDocument, params: HoverParams): Promise<Hover | undefined> {
@@ -45,35 +79,58 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
45
79
  return undefined;
46
80
  }
47
81
 
48
- const targetNodes = this.references.findDeclarations(cstNode);
49
- const targetNode = targetNodes?.[0];
50
- if (targetNode) {
51
- const content = await this.getAstNodeHoverContent(targetNode);
52
- if (content) {
53
- return { contents: { kind: 'markdown', value: content } };
54
- }
82
+ // Try declaration hover first
83
+ const declarationHover = await this.tryGetDeclarationHover(cstNode);
84
+ if (declarationHover) {
85
+ return declarationHover;
55
86
  }
56
87
 
57
- if (cstNode.astNode && ast.isThisRef(cstNode.astNode)) {
58
- const content = await this.getAstNodeHoverContent(cstNode.astNode);
59
- if (content) {
60
- return { contents: { kind: 'markdown', value: content } };
61
- }
62
- }
88
+ // Then try keyword hover
89
+ return await this.tryGetKeywordHover(cstNode);
90
+ } catch (error) {
91
+ console.error('Error in getHoverContent:', error);
92
+ return undefined;
93
+ }
94
+ }
63
95
 
64
- if (cstNode.grammarSource?.$type === 'Keyword') {
65
- const keywordHover = this.getKeywordHoverContent(cstNode.grammarSource);
66
- if (keywordHover) {
67
- return keywordHover;
68
- }
96
+ /**
97
+ * Try to get hover for a declaration node (AST node).
98
+ */
99
+ private async tryGetDeclarationHover(cstNode: ReturnType<typeof CstUtils.findDeclarationNodeAtOffset>): Promise<Hover | undefined> {
100
+ if (!cstNode) return undefined;
101
+
102
+ const targetNodes = this.references.findDeclarations(cstNode);
103
+ const targetNode = targetNodes?.[0];
104
+ if (targetNode) {
105
+ const content = await this.getAstNodeHoverContent(targetNode);
106
+ if (content) {
107
+ return { contents: { kind: 'markdown', value: content } };
108
+ }
109
+ }
69
110
 
70
- const explanation = keywordExplanations[cstNode.text.toLowerCase()];
71
- if (explanation) {
72
- return { contents: { kind: 'markdown', value: `💡 ${explanation}` } };
73
- }
111
+ if (cstNode.astNode && ast.isThisRef(cstNode.astNode)) {
112
+ const content = await this.getAstNodeHoverContent(cstNode.astNode);
113
+ if (content) {
114
+ return { contents: { kind: 'markdown', value: content } };
74
115
  }
75
- } catch (error) {
76
- console.error('Error in getHoverContent:', error);
116
+ }
117
+
118
+ return undefined;
119
+ }
120
+
121
+ /**
122
+ * Try to get hover for a keyword node.
123
+ * Uses the keyword dictionary for all keywords.
124
+ */
125
+ private async tryGetKeywordHover(cstNode: ReturnType<typeof CstUtils.findDeclarationNodeAtOffset>): Promise<Hover | undefined> {
126
+ if (!cstNode || cstNode.grammarSource?.$type !== 'Keyword') {
127
+ return undefined;
128
+ }
129
+
130
+ // Use the keyword dictionary for hover content
131
+ const explanation = keywordExplanations[cstNode.text.toLowerCase()];
132
+ if (explanation) {
133
+ return { contents: { kind: 'markdown', value: `💡 ${explanation}` } };
77
134
  }
78
135
 
79
136
  return undefined;
@@ -84,211 +141,276 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
84
141
  const content = this.documentationProvider.getDocumentation(node);
85
142
  const commentBlock = content ? `*${content}*\n\n` : '';
86
143
 
87
- if (ast.isDomain(node)) {
88
- const description = node.description ?? '';
89
- const vision = node.vision ?? '';
90
- const typeRef = node.type?.ref;
91
- const type = this.getRefName(typeRef);
92
-
93
- const signatureParts = ['Domain', node.name];
94
- if (node.parent?.ref?.name) signatureParts.push('in', node.parent.ref.name);
95
- const signature = `\`\`\`domain-lang\n${signatureParts.join(' ')}\n\`\`\``;
96
-
97
- const fields: string[] = [signature];
98
- if (description) fields.push(description);
99
- if (vision || type || node.parent) fields.push('---');
100
- if (vision) fields.push(`**Vision:** ${vision}`);
101
- if (type) fields.push(`**Type:** ${this.refLink(typeRef, type)}`);
102
- if (node.parent) fields.push(`**Parent:** ${this.refLink(node.parent)}`);
103
-
104
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
105
- `📁 **\`(domain)\` ${node.name}**\n\n` +
106
- fields.join('\n\n');
144
+ // Try each type-specific generator
145
+ for (const generator of this.hoverGenerators) {
146
+ const result = generator(node, commentBlock);
147
+ if (result !== undefined) {
148
+ return result;
149
+ }
107
150
  }
108
151
 
109
- if (ast.isThisRef(node)) {
110
- let parent = node.$container;
111
- while (parent) {
112
- if (
113
- ast.isDomain(parent) ||
114
- ast.isBoundedContext(parent) ||
115
- ast.isNamespaceDeclaration(parent) ||
116
- ast.isContextMap(parent) ||
117
- ast.isDomainMap(parent) ||
118
- ast.isModel(parent)
119
- ) {
120
- return this.getAstNodeHoverContent(parent);
121
- }
122
- parent = parent.$container;
123
- }
152
+ // Default fallback for unknown types
153
+ return this.getDefaultHover(node, commentBlock);
154
+ } catch (error) {
155
+ console.error('Error in getAstNodeHoverContent:', error);
156
+ return undefined;
157
+ }
158
+ }
124
159
 
125
- return '*this* refers to the current context';
126
- }
160
+ // ============================================================
161
+ // Type-specific hover generators
162
+ // ============================================================
127
163
 
128
- if (ast.isBoundedContext(node)) {
129
- const description = node.description ?? '';
130
- const classification = effectiveClassification(node);
131
- const team = effectiveTeam(node);
132
- const businessModel = node.businessModel?.ref;
133
- const evolution = node.evolution?.ref;
134
- const relationships = node.relationships ?? [];
135
- const terminology = node.terminology ?? [];
136
- const decisions = node.decisions ?? [];
137
- const classificationName = classification?.name;
138
- const teamName = team?.name;
139
-
140
- const signatureParts = ['boundedcontext', node.name];
141
- if (node.domain?.ref?.name) signatureParts.push('for', node.domain.ref.name);
142
- if (classificationName) signatureParts.push('as', classificationName);
143
- if (teamName) signatureParts.push('by', teamName);
144
- const signature = `\`\`\`domain-lang\n${signatureParts.join(' ')}\n\`\`\``;
145
-
146
- const fields: string[] = [signature];
147
- if (description) fields.push(description);
148
- if (classification || team || businessModel || evolution) fields.push('---');
149
- if (classification) fields.push(`🔖 **Classification:** ${this.refLink(classification)}`);
150
- if (team) fields.push(`👥 **Team:** ${this.refLink(team)}`);
151
- if (businessModel) fields.push(`💼 **Business Model:** ${this.refLink(businessModel)}`);
152
- if (evolution) fields.push(`🔄 **Evolution:** ${this.refLink(evolution)}`);
153
- if (relationships.length) {
154
- const relationshipLines = relationships.map(rel => `- ${this.refLink(rel.left?.link)} ${rel.arrow} ${this.refLink(rel.right?.link)}${rel.type ? ` \`${rel.type}\`` : ''}`);
155
- fields.push(`**Relationships:**\n${relationshipLines.join('\n')}`);
156
- }
157
- if (terminology.length) {
158
- const termLines = terminology.map(t => `- \`${t.name}\`: ${t.meaning ?? ''}`);
159
- fields.push(`**Terminology:**\n${termLines.join('\n')}`);
160
- }
161
- if (decisions.length) {
162
- const decisionLines = decisions.map(d => `- \`${d.name}\`: ${d.value ?? ''}`);
163
- fields.push(`**Decisions:**\n${decisionLines.join('\n')}`);
164
- }
164
+ private getDomainHover(node: AstNode, commentBlock: string): string | undefined {
165
+ if (!ast.isDomain(node)) return undefined;
165
166
 
166
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
167
- `📕 **\`(boundedcontext)\` ${node.name}**\n\n` +
168
- fields.join('\n\n');
169
- }
167
+ const description = node.description ?? '';
168
+ const vision = node.vision ?? '';
169
+ const typeRef = node.type?.ref;
170
+ const type = this.getRefName(typeRef);
170
171
 
171
- if (ast.isNamespaceDeclaration && ast.isNamespaceDeclaration(node)) {
172
- const fields: string[] = [`Contains ${node.children.length} elements.`];
173
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
174
- `🧭 **\`(namespace)\` ${node.name}**\n\n` +
175
- fields.join('\n\n');
176
- }
172
+ const signatureParts = ['Domain', node.name];
173
+ if (node.parent?.ref?.name) signatureParts.push('in', node.parent.ref.name);
174
+ const signature = this.codeBlock(signatureParts.join(' '));
177
175
 
178
- if (ast.isContextMap && ast.isContextMap(node)) {
179
- const fields: string[] = [];
180
- if (node.boundedContexts.length) {
181
- fields.push('---');
182
- fields.push(`**📕 Bounded Contexts**\n${node.boundedContexts.flatMap(bc => bc.items.map(item => `- ${this.refLink(item.ref)}`)).join('\n')}`);
183
- }
184
- if (node.relationships.length) {
185
- fields.push('---');
186
- fields.push(`**🔗 Relationships**\n${node.relationships.map(r => `- ${this.refLink(r.left?.link)} ${r.arrow} ${this.refLink(r.right?.link)}${r.type ? ` \`${r.type}\`` : ''}`).join('\n')}`);
187
- }
188
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
189
- `🗺️ **\`(contextmap)\` ${node.name}**\n\n` +
190
- fields.join('\n\n');
191
- }
176
+ const fields: string[] = [signature];
177
+ if (description) fields.push(description);
178
+ if (vision || type || node.parent) fields.push('---');
179
+ if (vision) fields.push(`**Vision:** ${vision}`);
180
+ if (type) fields.push(`**Type:** ${this.refLink(typeRef, type)}`);
181
+ if (node.parent) fields.push(`**Parent:** ${this.refLink(node.parent)}`);
192
182
 
193
- if (ast.isDomainMap && ast.isDomainMap(node)) {
194
- const fields: string[] = [];
195
- if (node.domains.length) {
196
- fields.push('---');
197
- fields.push(`**📁 Domains**\n${node.domains.flatMap(d => d.items.map(item => `- ${this.refLink(item.ref)}`)).join('\n')}`);
198
- }
199
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
200
- `🗺️ **\`(domainmap)\` ${node.name}**\n\n` +
201
- fields.join('\n\n');
202
- }
183
+ return this.formatHover(commentBlock, '📁', 'domain', node.name, fields);
184
+ }
203
185
 
204
- if (ast.isDecision && ast.isDecision(node)) {
205
- const fields: string[] = [];
206
- if (node.value) {
207
- fields.push('---');
208
- fields.push(`*Definition:* ${node.value}`);
209
- }
210
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
211
- `⚖️ **\`(decision)\` ${node.name}**\n\n` +
212
- fields.join('\n\n');
186
+ private getThisRefHover(node: AstNode, _commentBlock: string): string | undefined {
187
+ if (!ast.isThisRef(node)) return undefined;
188
+
189
+ let parent = node.$container;
190
+ while (parent) {
191
+ if (
192
+ ast.isDomain(parent) ||
193
+ ast.isBoundedContext(parent) ||
194
+ ast.isNamespaceDeclaration(parent) ||
195
+ ast.isContextMap(parent) ||
196
+ ast.isDomainMap(parent) ||
197
+ ast.isModel(parent)
198
+ ) {
199
+ return this.getAstNodeHoverContent(parent) as string | undefined;
213
200
  }
201
+ parent = parent.$container;
202
+ }
214
203
 
215
- if (ast.isPolicy && ast.isPolicy(node)) {
216
- const fields: string[] = [];
217
- if (node.value) {
218
- fields.push('---');
219
- fields.push(`*Definition:* ${node.value}`);
220
- }
221
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
222
- `📜 **\`(policy)\` ${node.name}**\n\n` +
223
- fields.join('\n\n');
224
- }
204
+ return '*this* refers to the current context';
205
+ }
225
206
 
226
- if (ast.isBusinessRule && ast.isBusinessRule(node)) {
227
- const fields: string[] = [];
228
- if (node.value) {
229
- fields.push('---');
230
- fields.push(`*Definition:* ${node.value}`);
231
- }
232
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
233
- `⚖️ **\`(rule)\` ${node.name}**\n\n` +
234
- fields.join('\n\n');
235
- }
207
+ private getBoundedContextHover(node: AstNode, commentBlock: string): string | undefined {
208
+ if (!ast.isBoundedContext(node)) return undefined;
209
+
210
+ const description = node.description ?? '';
211
+ const classification = effectiveClassification(node);
212
+ const team = effectiveTeam(node);
213
+ const businessModel = node.businessModel?.ref;
214
+ const evolution = node.evolution?.ref;
215
+ const relationships = node.relationships ?? [];
216
+ const terminology = node.terminology ?? [];
217
+ const decisions = node.decisions ?? [];
218
+ const classificationName = classification?.name;
219
+ const teamName = team?.name;
220
+
221
+ const signatureParts = ['boundedcontext', node.name];
222
+ if (node.domain?.ref?.name) signatureParts.push('for', node.domain.ref.name);
223
+ if (classificationName) signatureParts.push('as', classificationName);
224
+ if (teamName) signatureParts.push('by', teamName);
225
+ const signature = this.codeBlock(signatureParts.join(' '));
226
+
227
+ const fields: string[] = [signature];
228
+ if (description) fields.push(description);
229
+ if (classification || team || businessModel || evolution) fields.push('---');
230
+ if (classification) fields.push(`🔖 **Classification:** ${this.refLink(classification)}`);
231
+ if (team) fields.push(`👥 **Team:** ${this.refLink(team)}`);
232
+ if (businessModel) fields.push(`💼 **Business Model:** ${this.refLink(businessModel)}`);
233
+ if (evolution) fields.push(`🔄 **Evolution:** ${this.refLink(evolution)}`);
234
+
235
+ if (relationships.length > 0) {
236
+ const lines = relationships.map(rel => this.formatRelationshipLine(rel));
237
+ fields.push(`**Relationships:**\n${lines.join('\n')}`);
238
+ }
239
+ if (terminology.length > 0) {
240
+ const lines = terminology.map(t => `- \`${t.name}\`: ${t.meaning ?? ''}`);
241
+ fields.push(`**Terminology:**\n${lines.join('\n')}`);
242
+ }
243
+ if (decisions.length > 0) {
244
+ const lines = decisions.map(d => `- \`${d.name}\`: ${d.value ?? ''}`);
245
+ fields.push(`**Decisions:**\n${lines.join('\n')}`);
246
+ }
236
247
 
237
- if (ast.isDomainTerm && ast.isDomainTerm(node)) {
238
- const fields: string[] = [];
239
- if (node.meaning) {
240
- fields.push('---');
241
- fields.push(`*${node.meaning}*`);
242
- }
243
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
244
- `🗝️ **\`(term)\` ${node.name}**\n\n` +
245
- fields.join('\n\n');
246
- }
248
+ return this.formatHover(commentBlock, '📕', 'boundedcontext', node.name, fields);
249
+ }
247
250
 
248
- if (ast.isTeam && ast.isTeam(node)) {
249
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
250
- `👥 **\`(team)\` ${node.name}**`;
251
- }
251
+ private getNamespaceHover(node: AstNode, commentBlock: string): string | undefined {
252
+ if (!ast.isNamespaceDeclaration(node)) return undefined;
252
253
 
253
- if (ast.isClassification && ast.isClassification(node)) {
254
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
255
- `🏷️ **\`(classification)\` ${node.name}**`;
256
- }
254
+ const fields: string[] = [`Contains ${node.children.length} elements.`];
255
+ return this.formatHover(commentBlock, '🧭', 'namespace', node.name, fields);
256
+ }
257
257
 
258
- if (ast.isMetadata && ast.isMetadata(node)) {
259
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
260
- `🔖 **\`(metadata)\` ${node.name}**`;
261
- }
258
+ private getContextMapHover(node: AstNode, commentBlock: string): string | undefined {
259
+ if (!ast.isContextMap(node)) return undefined;
262
260
 
263
- if (ast.isRelationship && ast.isRelationship(node)) {
264
- const leftPatterns = node.leftPatterns.join(', ');
265
- const rightPatterns = node.rightPatterns.join(', ');
266
- const fields: string[] = [];
267
- fields.push(`${this.refLink(node.left.link)} ${node.arrow} ${this.refLink(node.right.link)}`);
268
- if (node.type) fields.push(`**Type:** \`${node.type}\``);
269
- if (leftPatterns) fields.push(`**Left patterns:** ${leftPatterns}`);
270
- if (rightPatterns) fields.push(`**Right patterns:** ${rightPatterns}`);
271
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
272
- `🔗 **\`(relationship)\`**\n\n` +
273
- fields.join('\n\n');
274
- }
261
+ const fields: string[] = [];
262
+ if (node.boundedContexts.length > 0) {
263
+ fields.push('---');
264
+ const items = node.boundedContexts.flatMap(bc =>
265
+ bc.items.map(item => `- ${this.refLink(item.ref)}`)
266
+ );
267
+ fields.push(`**📕 Bounded Contexts**\n${items.join('\n')}`);
268
+ }
269
+ if (node.relationships.length > 0) {
270
+ fields.push('---');
271
+ const lines = node.relationships.map(r => this.formatRelationshipLine(r));
272
+ fields.push(`**🔗 Relationships**\n${lines.join('\n')}`);
273
+ }
274
+ return this.formatHover(commentBlock, '🗺️', 'contextmap', node.name, fields);
275
+ }
275
276
 
276
- if (ast.isImportStatement && ast.isImportStatement(node)) {
277
- const fields: string[] = [];
278
- fields.push(`**URI:** \`${node.uri}\``);
279
- if (node.alias) fields.push(`**Alias:** \`${node.alias}\``);
280
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
281
- `📦 **\`(import)\`**\n\n` +
282
- fields.join('\n\n');
283
- }
277
+ private getDomainMapHover(node: AstNode, commentBlock: string): string | undefined {
278
+ if (!ast.isDomainMap(node)) return undefined;
284
279
 
285
- const title = ast.isType(node) ? node.name : node.$type.toLowerCase();
286
- return (commentBlock ? `${commentBlock}\n\n---\n\n` : '') +
287
- `ℹ️ **\`(${node.$type.toLowerCase()})\`${ast.isType(node) ? ` ${title}` : ''}**`;
288
- } catch (error) {
289
- console.error('Error in getAstNodeHoverContent:', error);
290
- return 'Unable to display complete information.';
280
+ const fields: string[] = [];
281
+ if (node.domains.length > 0) {
282
+ fields.push('---');
283
+ const items = node.domains.flatMap(d =>
284
+ d.items.map(item => `- ${this.refLink(item.ref)}`)
285
+ );
286
+ fields.push(`**📁 Domains**\n${items.join('\n')}`);
287
+ }
288
+ return this.formatHover(commentBlock, '🗺️', 'domainmap', node.name, fields);
289
+ }
290
+
291
+ private getDecisionHover(node: AstNode, commentBlock: string): string | undefined {
292
+ if (!ast.isDecision(node)) return undefined;
293
+
294
+ const fields: string[] = [];
295
+ if (node.value) {
296
+ fields.push('---', `*Definition:* ${node.value}`);
297
+ }
298
+ return this.formatHover(commentBlock, '⚖️', 'decision', node.name, fields);
299
+ }
300
+
301
+ private getPolicyHover(node: AstNode, commentBlock: string): string | undefined {
302
+ if (!ast.isPolicy(node)) return undefined;
303
+
304
+ const fields: string[] = [];
305
+ if (node.value) {
306
+ fields.push('---', `*Definition:* ${node.value}`);
307
+ }
308
+ return this.formatHover(commentBlock, '📜', 'policy', node.name, fields);
309
+ }
310
+
311
+ private getBusinessRuleHover(node: AstNode, commentBlock: string): string | undefined {
312
+ if (!ast.isBusinessRule(node)) return undefined;
313
+
314
+ const fields: string[] = [];
315
+ if (node.value) {
316
+ fields.push('---', `*Definition:* ${node.value}`);
291
317
  }
318
+ return this.formatHover(commentBlock, '⚖️', 'rule', node.name, fields);
319
+ }
320
+
321
+ private getDomainTermHover(node: AstNode, commentBlock: string): string | undefined {
322
+ if (!ast.isDomainTerm(node)) return undefined;
323
+
324
+ const fields: string[] = [];
325
+ if (node.meaning) {
326
+ fields.push('---', `*${node.meaning}*`);
327
+ }
328
+ return this.formatHover(commentBlock, '🗝️', 'term', node.name, fields);
329
+ }
330
+
331
+ private getTeamHover(node: AstNode, commentBlock: string): string | undefined {
332
+ if (!ast.isTeam(node)) return undefined;
333
+ return this.formatHover(commentBlock, '👥', 'team', node.name, []);
334
+ }
335
+
336
+ private getClassificationHover(node: AstNode, commentBlock: string): string | undefined {
337
+ if (!ast.isClassification(node)) return undefined;
338
+ return this.formatHover(commentBlock, '🏷️', 'classification', node.name, []);
339
+ }
340
+
341
+ private getMetadataHover(node: AstNode, commentBlock: string): string | undefined {
342
+ if (!ast.isMetadata(node)) return undefined;
343
+ return this.formatHover(commentBlock, '🔖', 'metadata', node.name, []);
344
+ }
345
+
346
+ private getRelationshipHover(node: AstNode, commentBlock: string): string | undefined {
347
+ if (!ast.isRelationship(node)) return undefined;
348
+
349
+ const leftPatterns = node.leftPatterns.join(', ');
350
+ const rightPatterns = node.rightPatterns.join(', ');
351
+ const fields: string[] = [
352
+ `${this.refLink(node.left.link)} ${node.arrow} ${this.refLink(node.right.link)}`
353
+ ];
354
+ if (node.type) fields.push(`**Type:** \`${node.type}\``);
355
+ if (leftPatterns) fields.push(`**Left patterns:** ${leftPatterns}`);
356
+ if (rightPatterns) fields.push(`**Right patterns:** ${rightPatterns}`);
357
+
358
+ return this.formatHover(commentBlock, '🔗', 'relationship', undefined, fields);
359
+ }
360
+
361
+ private getImportHover(node: AstNode, commentBlock: string): string | undefined {
362
+ if (!ast.isImportStatement(node)) return undefined;
363
+
364
+ const fields: string[] = [`**URI:** \`${node.uri}\``];
365
+ if (node.alias) fields.push(`**Alias:** \`${node.alias}\``);
366
+ return this.formatHover(commentBlock, '📦', 'import', undefined, fields);
367
+ }
368
+
369
+ private getDefaultHover(node: AstNode, commentBlock: string): string {
370
+ const title = ast.isType(node) ? node.name : node.$type.toLowerCase();
371
+ const typeName = node.$type.toLowerCase();
372
+ const name = ast.isType(node) ? ` ${title}` : '';
373
+
374
+ const separator = commentBlock ? `${commentBlock}\n\n---\n\n` : '';
375
+ return `${separator}ℹ️ **\`(${typeName})\`${name}**`;
376
+ }
377
+
378
+ // ============================================================
379
+ // Helper methods
380
+ // ============================================================
381
+
382
+ /**
383
+ * Formats a relationship line for hover display.
384
+ */
385
+ private formatRelationshipLine(rel: ast.Relationship): string {
386
+ const left = this.refLink(rel.left?.link);
387
+ const right = this.refLink(rel.right?.link);
388
+ const type = rel.type ? ` \`${rel.type}\`` : '';
389
+ return `- ${left} ${rel.arrow} ${right}${type}`;
390
+ }
391
+
392
+ /**
393
+ * Wraps text in a domain-lang code block.
394
+ */
395
+ private codeBlock(text: string): string {
396
+ return `\`\`\`domain-lang\n${text}\n\`\`\``;
397
+ }
398
+
399
+ /**
400
+ * Formats the final hover content with consistent structure.
401
+ */
402
+ private formatHover(
403
+ commentBlock: string,
404
+ emoji: string,
405
+ typeName: string,
406
+ name: string | undefined,
407
+ fields: string[]
408
+ ): string {
409
+ const separator = commentBlock ? `${commentBlock}\n\n---\n\n` : '';
410
+ const nameDisplay = name ? ` ${name}` : '';
411
+ const header = `${emoji} **\`(${typeName})\`${nameDisplay}**`;
412
+ const body = fields.length > 0 ? `\n\n${fields.join('\n\n')}` : '';
413
+ return `${separator}${header}${body}`;
292
414
  }
293
415
 
294
416
  private getRefName(ref: ast.Type | Reference<ast.Type> | undefined): string {
@@ -318,21 +440,5 @@ export class DomainLangHoverProvider extends AstNodeHoverProvider {
318
440
  return '';
319
441
  }
320
442
 
321
- protected override getKeywordHoverContent(node: AstNode): MaybePromise<Hover | undefined> {
322
- let comment = isAstNodeWithComment(node) ? node.$comment : undefined;
323
-
324
- if (!comment) {
325
- comment = CstUtils.findCommentNode(node.$cstNode, ['ML_COMMENT'])?.text;
326
- }
327
-
328
- if (comment && isJSDoc(comment)) {
329
- const markdown = parseJSDoc(comment).toMarkdown();
330
- if (markdown) {
331
- return { contents: { kind: 'markdown', value: markdown } };
332
- }
333
- }
334
-
335
- return undefined;
336
- }
337
443
  }
338
444