@domainlang/language 0.1.81

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 (188) hide show
  1. package/README.md +32 -0
  2. package/out/ast-augmentation.d.ts +6 -0
  3. package/out/ast-augmentation.js +2 -0
  4. package/out/ast-augmentation.js.map +1 -0
  5. package/out/domain-lang-module.d.ts +55 -0
  6. package/out/domain-lang-module.js +59 -0
  7. package/out/domain-lang-module.js.map +1 -0
  8. package/out/generated/ast.d.ts +770 -0
  9. package/out/generated/ast.js +565 -0
  10. package/out/generated/ast.js.map +1 -0
  11. package/out/generated/grammar.d.ts +6 -0
  12. package/out/generated/grammar.js +2502 -0
  13. package/out/generated/grammar.js.map +1 -0
  14. package/out/generated/module.d.ts +13 -0
  15. package/out/generated/module.js +21 -0
  16. package/out/generated/module.js.map +1 -0
  17. package/out/index.d.ts +13 -0
  18. package/out/index.js +17 -0
  19. package/out/index.js.map +1 -0
  20. package/out/lsp/domain-lang-completion.d.ts +37 -0
  21. package/out/lsp/domain-lang-completion.js +452 -0
  22. package/out/lsp/domain-lang-completion.js.map +1 -0
  23. package/out/lsp/domain-lang-formatter.d.ts +15 -0
  24. package/out/lsp/domain-lang-formatter.js +43 -0
  25. package/out/lsp/domain-lang-formatter.js.map +1 -0
  26. package/out/lsp/domain-lang-naming.d.ts +34 -0
  27. package/out/lsp/domain-lang-naming.js +49 -0
  28. package/out/lsp/domain-lang-naming.js.map +1 -0
  29. package/out/lsp/domain-lang-scope.d.ts +59 -0
  30. package/out/lsp/domain-lang-scope.js +102 -0
  31. package/out/lsp/domain-lang-scope.js.map +1 -0
  32. package/out/lsp/hover/ddd-pattern-explanations.d.ts +50 -0
  33. package/out/lsp/hover/ddd-pattern-explanations.js +196 -0
  34. package/out/lsp/hover/ddd-pattern-explanations.js.map +1 -0
  35. package/out/lsp/hover/domain-lang-hover.d.ts +19 -0
  36. package/out/lsp/hover/domain-lang-hover.js +306 -0
  37. package/out/lsp/hover/domain-lang-hover.js.map +1 -0
  38. package/out/lsp/hover/domain-lang-keywords.d.ts +13 -0
  39. package/out/lsp/hover/domain-lang-keywords.js +47 -0
  40. package/out/lsp/hover/domain-lang-keywords.js.map +1 -0
  41. package/out/main-browser.d.ts +1 -0
  42. package/out/main-browser.js +11 -0
  43. package/out/main-browser.js.map +1 -0
  44. package/out/main.d.ts +1 -0
  45. package/out/main.js +74 -0
  46. package/out/main.js.map +1 -0
  47. package/out/sdk/ast-augmentation.d.ts +136 -0
  48. package/out/sdk/ast-augmentation.js +62 -0
  49. package/out/sdk/ast-augmentation.js.map +1 -0
  50. package/out/sdk/index.d.ts +94 -0
  51. package/out/sdk/index.js +97 -0
  52. package/out/sdk/index.js.map +1 -0
  53. package/out/sdk/indexes.d.ts +16 -0
  54. package/out/sdk/indexes.js +97 -0
  55. package/out/sdk/indexes.js.map +1 -0
  56. package/out/sdk/loader-node.d.ts +47 -0
  57. package/out/sdk/loader-node.js +104 -0
  58. package/out/sdk/loader-node.js.map +1 -0
  59. package/out/sdk/loader.d.ts +49 -0
  60. package/out/sdk/loader.js +85 -0
  61. package/out/sdk/loader.js.map +1 -0
  62. package/out/sdk/patterns.d.ts +93 -0
  63. package/out/sdk/patterns.js +123 -0
  64. package/out/sdk/patterns.js.map +1 -0
  65. package/out/sdk/query.d.ts +90 -0
  66. package/out/sdk/query.js +679 -0
  67. package/out/sdk/query.js.map +1 -0
  68. package/out/sdk/resolution.d.ts +52 -0
  69. package/out/sdk/resolution.js +68 -0
  70. package/out/sdk/resolution.js.map +1 -0
  71. package/out/sdk/types.d.ts +301 -0
  72. package/out/sdk/types.js +8 -0
  73. package/out/sdk/types.js.map +1 -0
  74. package/out/services/dependency-analyzer.d.ts +94 -0
  75. package/out/services/dependency-analyzer.js +279 -0
  76. package/out/services/dependency-analyzer.js.map +1 -0
  77. package/out/services/dependency-resolver.d.ts +123 -0
  78. package/out/services/dependency-resolver.js +252 -0
  79. package/out/services/dependency-resolver.js.map +1 -0
  80. package/out/services/git-url-resolver.browser.d.ts +18 -0
  81. package/out/services/git-url-resolver.browser.js +15 -0
  82. package/out/services/git-url-resolver.browser.js.map +1 -0
  83. package/out/services/git-url-resolver.d.ts +192 -0
  84. package/out/services/git-url-resolver.js +382 -0
  85. package/out/services/git-url-resolver.js.map +1 -0
  86. package/out/services/governance-validator.d.ts +80 -0
  87. package/out/services/governance-validator.js +159 -0
  88. package/out/services/governance-validator.js.map +1 -0
  89. package/out/services/import-resolver.d.ts +18 -0
  90. package/out/services/import-resolver.js +22 -0
  91. package/out/services/import-resolver.js.map +1 -0
  92. package/out/services/performance-optimizer.d.ts +60 -0
  93. package/out/services/performance-optimizer.js +140 -0
  94. package/out/services/performance-optimizer.js.map +1 -0
  95. package/out/services/relationship-inference.d.ts +11 -0
  96. package/out/services/relationship-inference.js +98 -0
  97. package/out/services/relationship-inference.js.map +1 -0
  98. package/out/services/workspace-manager.d.ts +76 -0
  99. package/out/services/workspace-manager.js +323 -0
  100. package/out/services/workspace-manager.js.map +1 -0
  101. package/out/syntaxes/domain-lang.monarch.d.ts +76 -0
  102. package/out/syntaxes/domain-lang.monarch.js +29 -0
  103. package/out/syntaxes/domain-lang.monarch.js.map +1 -0
  104. package/out/utils/import-utils.d.ts +57 -0
  105. package/out/utils/import-utils.js +228 -0
  106. package/out/utils/import-utils.js.map +1 -0
  107. package/out/validation/bounded-context.d.ts +11 -0
  108. package/out/validation/bounded-context.js +79 -0
  109. package/out/validation/bounded-context.js.map +1 -0
  110. package/out/validation/classification.d.ts +3 -0
  111. package/out/validation/classification.js +3 -0
  112. package/out/validation/classification.js.map +1 -0
  113. package/out/validation/constants.d.ts +77 -0
  114. package/out/validation/constants.js +96 -0
  115. package/out/validation/constants.js.map +1 -0
  116. package/out/validation/domain-lang-validator.d.ts +2 -0
  117. package/out/validation/domain-lang-validator.js +27 -0
  118. package/out/validation/domain-lang-validator.js.map +1 -0
  119. package/out/validation/domain.d.ts +11 -0
  120. package/out/validation/domain.js +18 -0
  121. package/out/validation/domain.js.map +1 -0
  122. package/out/validation/import.d.ts +44 -0
  123. package/out/validation/import.js +135 -0
  124. package/out/validation/import.js.map +1 -0
  125. package/out/validation/maps.d.ts +21 -0
  126. package/out/validation/maps.js +56 -0
  127. package/out/validation/maps.js.map +1 -0
  128. package/out/validation/metadata.d.ts +7 -0
  129. package/out/validation/metadata.js +12 -0
  130. package/out/validation/metadata.js.map +1 -0
  131. package/out/validation/model.d.ts +12 -0
  132. package/out/validation/model.js +29 -0
  133. package/out/validation/model.js.map +1 -0
  134. package/out/validation/relationships.d.ts +12 -0
  135. package/out/validation/relationships.js +94 -0
  136. package/out/validation/relationships.js.map +1 -0
  137. package/out/validation/shared.d.ts +6 -0
  138. package/out/validation/shared.js +12 -0
  139. package/out/validation/shared.js.map +1 -0
  140. package/package.json +97 -0
  141. package/src/ast-augmentation.ts +5 -0
  142. package/src/domain-lang-module.ts +100 -0
  143. package/src/domain-lang.langium +356 -0
  144. package/src/generated/ast.ts +999 -0
  145. package/src/generated/grammar.ts +2504 -0
  146. package/src/generated/module.ts +25 -0
  147. package/src/index.ts +17 -0
  148. package/src/lsp/domain-lang-completion.ts +514 -0
  149. package/src/lsp/domain-lang-formatter.ts +51 -0
  150. package/src/lsp/domain-lang-naming.ts +56 -0
  151. package/src/lsp/domain-lang-scope.ts +137 -0
  152. package/src/lsp/hover/ddd-pattern-explanations.ts +237 -0
  153. package/src/lsp/hover/domain-lang-hover.ts +340 -0
  154. package/src/lsp/hover/domain-lang-keywords.ts +50 -0
  155. package/src/main-browser.ts +15 -0
  156. package/src/main.ts +85 -0
  157. package/src/sdk/README.md +297 -0
  158. package/src/sdk/ast-augmentation.ts +157 -0
  159. package/src/sdk/index.ts +128 -0
  160. package/src/sdk/indexes.ts +155 -0
  161. package/src/sdk/loader-node.ts +126 -0
  162. package/src/sdk/loader.ts +99 -0
  163. package/src/sdk/patterns.ts +147 -0
  164. package/src/sdk/query.ts +802 -0
  165. package/src/sdk/resolution.ts +78 -0
  166. package/src/sdk/types.ts +346 -0
  167. package/src/services/dependency-analyzer.ts +381 -0
  168. package/src/services/dependency-resolver.ts +334 -0
  169. package/src/services/git-url-resolver.browser.ts +31 -0
  170. package/src/services/git-url-resolver.ts +524 -0
  171. package/src/services/governance-validator.ts +219 -0
  172. package/src/services/import-resolver.ts +30 -0
  173. package/src/services/performance-optimizer.ts +170 -0
  174. package/src/services/relationship-inference.ts +121 -0
  175. package/src/services/workspace-manager.ts +416 -0
  176. package/src/syntaxes/domain-lang.monarch.ts +29 -0
  177. package/src/utils/import-utils.ts +274 -0
  178. package/src/validation/bounded-context.ts +99 -0
  179. package/src/validation/classification.ts +5 -0
  180. package/src/validation/constants.ts +124 -0
  181. package/src/validation/domain-lang-validator.ts +33 -0
  182. package/src/validation/domain.ts +24 -0
  183. package/src/validation/import.ts +171 -0
  184. package/src/validation/maps.ts +72 -0
  185. package/src/validation/metadata.ts +14 -0
  186. package/src/validation/model.ts +37 -0
  187. package/src/validation/relationships.ts +154 -0
  188. package/src/validation/shared.ts +14 -0
@@ -0,0 +1,274 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { URI, type LangiumDocument, type LangiumDocuments } from 'langium';
4
+ import type { Model } from '../generated/ast.js';
5
+ import { GitUrlParser } from '../services/git-url-resolver.js';
6
+ import type { GitUrlResolver } from '../services/git-url-resolver.js';
7
+ import { WorkspaceManager } from '../services/workspace-manager.js';
8
+
9
+ // Singleton workspace manager instance
10
+ let workspaceManager: WorkspaceManager | undefined;
11
+
12
+ /**
13
+ * Gets or creates the global workspace manager instance.
14
+ *
15
+ * @param startDir - Directory to start workspace search from
16
+ * @returns Promise resolving to the workspace manager
17
+ */
18
+ async function getWorkspaceManager(startDir: string): Promise<WorkspaceManager> {
19
+ if (!workspaceManager) {
20
+ workspaceManager = new WorkspaceManager();
21
+ const root = await findWorkspaceRoot(startDir);
22
+ try {
23
+ await workspaceManager.initialize(root);
24
+ } catch (error) {
25
+ console.warn(`Failed to initialize workspace: ${error instanceof Error ? error.message : String(error)}`);
26
+ // Continue without workspace - local imports will still work
27
+ }
28
+ }
29
+ return workspaceManager;
30
+ }
31
+
32
+ /**
33
+ * Gets the git URL resolver from the workspace manager.
34
+ *
35
+ * @param startDir - Directory to start workspace search from
36
+ * @returns Promise resolving to the git URL resolver
37
+ */
38
+ async function getGitResolver(startDir: string): Promise<GitUrlResolver> {
39
+ const manager = await getWorkspaceManager(startDir);
40
+ return await manager.getGitResolver();
41
+ }
42
+
43
+ /**
44
+ * Ensures a file path has the .dlang extension.
45
+ *
46
+ * @param filePath - The raw file path
47
+ * @returns Normalized path with .dlang extension
48
+ * @throws {Error} If path has an invalid extension
49
+ */
50
+ function ensureDlangExtension(filePath: string): string {
51
+ const ext = path.extname(filePath);
52
+
53
+ if (!ext) {
54
+ return `${filePath}.dlang`;
55
+ }
56
+
57
+ if (ext !== '.dlang') {
58
+ throw new Error(
59
+ `Invalid file extension: ${ext}. Expected .dlang for file: ${filePath}`
60
+ );
61
+ }
62
+
63
+ return filePath;
64
+ }
65
+
66
+ /**
67
+ * Resolves workspace-relative paths (starting with ~/).
68
+ *
69
+ * @param importPath - Import path that may start with ~/
70
+ * @param workspaceRoot - The workspace root directory
71
+ * @returns Resolved absolute path
72
+ */
73
+ function resolveWorkspacePath(importPath: string, workspaceRoot: string): string {
74
+ if (importPath.startsWith('~/')) {
75
+ return path.join(workspaceRoot, importPath.slice(2));
76
+ }
77
+ return importPath;
78
+ }
79
+
80
+ /**
81
+ * Finds the workspace root by looking for common workspace indicators.
82
+ *
83
+ * Searches upward from the document's directory looking for:
84
+ * - dlang.toml
85
+ * - .git directory
86
+ * - package.json with dlang configuration
87
+ *
88
+ * @param startDir - Directory to start searching from
89
+ * @returns Workspace root path or the start directory if not found
90
+ */
91
+ async function findWorkspaceRoot(startDir: string): Promise<string> {
92
+ let currentDir = startDir;
93
+ const root = path.parse(currentDir).root;
94
+
95
+ while (currentDir !== root) {
96
+ // Check for workspace indicators
97
+ const indicators = ['dlang.toml', '.git', 'package.json'];
98
+
99
+ for (const indicator of indicators) {
100
+ const indicatorPath = path.join(currentDir, indicator);
101
+ try {
102
+ await fs.access(indicatorPath);
103
+ return currentDir; // Found workspace root
104
+ } catch {
105
+ // Continue searching
106
+ }
107
+ }
108
+
109
+ // Move up one directory
110
+ const parentDir = path.dirname(currentDir);
111
+ if (parentDir === currentDir) break; // Reached root
112
+ currentDir = parentDir;
113
+ }
114
+
115
+ // Default to start directory if no workspace root found
116
+ return startDir;
117
+ }
118
+
119
+ /**
120
+ * Resolves an import path to an absolute file URI.
121
+ *
122
+ * Supports:
123
+ * - Local relative paths: ./file.dlang, ../other/file.dlang
124
+ * - Workspace-relative paths: ~/contexts/sales.dlang
125
+ * - Git URLs: gh:owner/repo@v1.0.0/file.dlang
126
+ * - Full URLs: https://github.com/owner/repo/blob/v1.0.0/file.dlang
127
+ *
128
+ * @param importingDoc - The document containing the import statement
129
+ * @param rawImportPath - The raw import path from the import statement
130
+ * @returns Resolved URI to the imported file
131
+ * @throws {Error} If the import cannot be resolved
132
+ */
133
+ export async function resolveImportPath(
134
+ importingDoc: LangiumDocument,
135
+ rawImportPath: string
136
+ ): Promise<URI> {
137
+ const baseDir = path.dirname(importingDoc.uri.fsPath);
138
+
139
+ // Handle manifest dependency aliases (friendly names)
140
+ const manager = await getWorkspaceManager(baseDir);
141
+ let gitResolver: GitUrlResolver | undefined;
142
+
143
+ try {
144
+ const manifestImport = await manager.resolveDependencyImport(rawImportPath);
145
+ if (manifestImport) {
146
+ gitResolver = await manager.getGitResolver();
147
+ return await gitResolver.resolve(manifestImport);
148
+ }
149
+ } catch {
150
+ // Ignore manifest resolution issues; fall back to other strategies
151
+ }
152
+
153
+ // Handle git URLs (shorthand or full)
154
+ if (GitUrlParser.isGitUrl(rawImportPath)) {
155
+ gitResolver = gitResolver ?? await manager.getGitResolver();
156
+ return await gitResolver.resolve(rawImportPath);
157
+ }
158
+
159
+ // Handle workspace-relative paths (~/)
160
+ let resolvedPath = rawImportPath;
161
+
162
+ if (rawImportPath.startsWith('~/')) {
163
+ const workspaceRoot = await findWorkspaceRoot(baseDir);
164
+ resolvedPath = resolveWorkspacePath(rawImportPath, workspaceRoot);
165
+ } else if (!path.isAbsolute(rawImportPath)) {
166
+ // Handle relative paths
167
+ resolvedPath = path.resolve(baseDir, rawImportPath);
168
+ }
169
+
170
+ // Ensure .dlang extension
171
+ const normalized = ensureDlangExtension(resolvedPath);
172
+
173
+ // Verify file exists
174
+ try {
175
+ await fs.access(normalized);
176
+ } catch {
177
+ throw new Error(
178
+ `Import file not found: ${rawImportPath} (resolved to ${normalized})`
179
+ );
180
+ }
181
+
182
+ return URI.file(normalized);
183
+ }
184
+
185
+ /**
186
+ * Legacy function for backward compatibility.
187
+ * Use resolveImportPath instead.
188
+ *
189
+ * @deprecated Use resolveImportPath which supports git URLs and workspace paths
190
+ */
191
+ export async function resolveLocalImportPath(
192
+ importingDoc: LangiumDocument,
193
+ rawImportPath: string
194
+ ): Promise<string> {
195
+ const uri = await resolveImportPath(importingDoc, rawImportPath);
196
+ return uri.fsPath;
197
+ }
198
+
199
+ /**
200
+ * Ensures the import graph is loaded from an entry file.
201
+ *
202
+ * @param entryFilePath - Absolute or workspace-relative path to entry file
203
+ * @param langiumDocuments - The Langium documents manager
204
+ * @returns Set of URIs (as strings) for all documents in the import graph
205
+ * @throws {Error} If entry file cannot be resolved or loaded
206
+ */
207
+ export async function ensureImportGraphFromEntryFile(
208
+ entryFilePath: string,
209
+ langiumDocuments: LangiumDocuments
210
+ ): Promise<Set<string>> {
211
+ const entryUri = URI.file(path.resolve(entryFilePath));
212
+ const entryDoc = await langiumDocuments.getOrCreateDocument(entryUri);
213
+ return ensureImportGraphFromDocument(entryDoc, langiumDocuments);
214
+ }
215
+
216
+ /**
217
+ * Recursively builds the import graph from a document.
218
+ *
219
+ * @param document - The starting document
220
+ * @param langiumDocuments - The Langium documents manager
221
+ * @returns Set of URIs (as strings) for all documents in the import graph
222
+ */
223
+ export async function ensureImportGraphFromDocument(
224
+ document: LangiumDocument,
225
+ langiumDocuments: LangiumDocuments
226
+ ): Promise<Set<string>> {
227
+ const visited = new Set<string>();
228
+
229
+ async function visit(doc: LangiumDocument): Promise<void> {
230
+ const uriString = doc.uri.toString();
231
+ if (visited.has(uriString)) return;
232
+ visited.add(uriString);
233
+
234
+ const model = doc.parseResult.value as unknown as Model;
235
+ for (const imp of model.imports ?? []) {
236
+ if (!imp.uri) continue;
237
+
238
+ // Use new resolveImportPath that supports git URLs
239
+ const resolvedUri = await resolveImportPath(doc, imp.uri);
240
+ const childDoc = await langiumDocuments.getOrCreateDocument(resolvedUri);
241
+ await visit(childDoc);
242
+ }
243
+ }
244
+
245
+ await visit(document);
246
+ return visited;
247
+ }
248
+
249
+ /**
250
+ * Gets cache statistics for git imports.
251
+ *
252
+ * @returns Cache statistics including size and number of cached repositories
253
+ */
254
+ export async function getGitCacheStats(startDir: string = process.cwd()): Promise<{
255
+ totalSize: number;
256
+ repoCount: number;
257
+ cacheDir: string;
258
+ }> {
259
+ const resolver = await getGitResolver(startDir);
260
+ return await resolver.getCacheStats();
261
+ }
262
+
263
+ /**
264
+ * Clears the git import cache.
265
+ *
266
+ * @param startDir - Starting directory for workspace resolution
267
+ * @returns Promise that resolves when cache is cleared
268
+ */
269
+ export async function clearGitCache(startDir: string = process.cwd()): Promise<void> {
270
+ const resolver = await getGitResolver(startDir);
271
+ return await resolver.clearCache();
272
+ }
273
+
274
+
@@ -0,0 +1,99 @@
1
+ import type { ValidationAcceptor } from 'langium';
2
+ import type { BoundedContext } from '../generated/ast.js';
3
+ import { ValidationMessages, buildCodeDescription } from './constants.js';
4
+
5
+ /**
6
+ * Validates that a bounded context has a description.
7
+ *
8
+ * @param bc - The bounded context to validate
9
+ * @param accept - The validation acceptor for reporting issues
10
+ */
11
+ function validateBoundedContextHasDescription(
12
+ bc: BoundedContext,
13
+ accept: ValidationAcceptor
14
+ ): void {
15
+ if (!bc.description) {
16
+ accept('warning', ValidationMessages.BOUNDED_CONTEXT_NO_DESCRIPTION(bc.name), {
17
+ node: bc,
18
+ keyword: 'BoundedContext',
19
+ codeDescription: buildCodeDescription('language.md', 'bounded-contexts')
20
+ });
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Validates that a bounded context belongs to a domain.
26
+ * A BoundedContext must have a 'for Domain' clause.
27
+ *
28
+ * @param bc - The bounded context to validate
29
+ * @param accept - The validation acceptor for reporting issues
30
+ */
31
+ function validateBoundedContextHasDomain(
32
+ bc: BoundedContext,
33
+ accept: ValidationAcceptor
34
+ ): void {
35
+ if (!bc.domain) {
36
+ accept('warning', ValidationMessages.BOUNDED_CONTEXT_NO_DOMAIN(bc.name), {
37
+ node: bc,
38
+ keyword: 'for',
39
+ codeDescription: buildCodeDescription('language.md', 'bounded-contexts')
40
+ });
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Validates conflicts between inline and block classification assignment.
46
+ * Warns when both inline ('as') and block ('classification:') are used.
47
+ * Inline values take precedence per PRS-008.
48
+ *
49
+ * FR-9.2: Inline/Block Conflict Validation
50
+ */
51
+ function validateBoundedContextClassificationConflict(
52
+ bc: BoundedContext,
53
+ accept: ValidationAcceptor
54
+ ): void {
55
+ if (bc.classification.length > 1) {
56
+ const inlineClassificationName = bc.classification[0].ref?.name;
57
+ const blockClassificationName = bc.classification[1].ref?.name;
58
+
59
+ // Warn if defined multiple times
60
+ accept('warning', ValidationMessages.BOUNDED_CONTEXT_CLASSIFICATION_CONFLICT(bc.name, inlineClassificationName, blockClassificationName), {
61
+ node: bc,
62
+ property: 'classification',
63
+ index: 1,
64
+ codeDescription: buildCodeDescription('language.md', 'bounded-contexts')
65
+ });
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Validates conflicts between inline and block team assignment.
71
+ * Warns when both inline ('by') and block ('team:') are used.
72
+ * Inline values take precedence per PRS-008.
73
+ *
74
+ * FR-2.3: Inline/Block Conflict Validation
75
+ */
76
+ function validateBoundedContextTeamConflict(
77
+ bc: BoundedContext,
78
+ accept: ValidationAcceptor
79
+ ): void {
80
+ if (bc.team.length > 1) {
81
+ const inlineTeamName = bc.team[0].ref?.name;
82
+ const blockTeamName = bc.team[1].ref?.name;
83
+
84
+ // Warn if defined multiple times
85
+ accept('warning', ValidationMessages.BOUNDED_CONTEXT_TEAM_CONFLICT(bc.name, inlineTeamName, blockTeamName), {
86
+ node: bc,
87
+ property: 'team',
88
+ index: 1,
89
+ codeDescription: buildCodeDescription('language.md', 'bounded-contexts')
90
+ });
91
+ }
92
+ }
93
+
94
+ export const boundedContextChecks = [
95
+ validateBoundedContextHasDescription,
96
+ validateBoundedContextHasDomain,
97
+ validateBoundedContextClassificationConflict,
98
+ validateBoundedContextTeamConflict
99
+ ];
@@ -0,0 +1,5 @@
1
+ import type { ValidationCheck } from 'langium';
2
+ import type { Classification } from '../generated/ast.js';
3
+
4
+ // No validation checks needed for classifications currently
5
+ export const classificationChecks: ValidationCheck<Classification>[] = [];
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Validation message constants for DomainLang.
3
+ *
4
+ * Centralizes all validation messages to ensure consistency
5
+ * and facilitate internationalization in the future.
6
+ *
7
+ * Messages follow VS Code conventions:
8
+ * - Clear problem statement
9
+ * - Brief DDD context explaining why it matters
10
+ * - Inline example showing the fix
11
+ * - Clickable documentation link via CodeDescription
12
+ */
13
+
14
+ import type { CodeDescription } from 'vscode-languageserver-types';
15
+
16
+ // ============================================================================
17
+ // Documentation Link Utilities
18
+ // ============================================================================
19
+
20
+ const REPO_BASE = 'https://github.com/larsbaunwall/DomainLang/blob/main';
21
+ const DOCS_BASE = `${REPO_BASE}/dsl/domain-lang/docs`;
22
+
23
+ /**
24
+ * Builds a documentation URL for error messages.
25
+ * @param docPath - Relative path from docs/ folder
26
+ * @param anchor - Optional section anchor (without #)
27
+ */
28
+ const buildDocLink = (docPath: string, anchor?: string): string =>
29
+ `${DOCS_BASE}/${docPath}${anchor ? `#${anchor}` : ''}`;
30
+
31
+ /**
32
+ * Creates a CodeDescription for clickable documentation links in VS Code.
33
+ * @param docPath - Relative path from docs/ folder
34
+ * @param anchor - Optional section anchor (without #)
35
+ */
36
+ export const buildCodeDescription = (docPath: string, anchor?: string): CodeDescription => ({
37
+ href: buildDocLink(docPath, anchor)
38
+ });
39
+
40
+ // ============================================================================
41
+ // Enhanced Validation Messages
42
+ // ============================================================================
43
+
44
+ export const ValidationMessages = {
45
+ /**
46
+ * Warning message when a domain lacks a vision statement.
47
+ * @param name - The name of the domain
48
+ */
49
+ DOMAIN_NO_VISION: (name: string) =>
50
+ `Domain '${name}' is missing a vision statement.`,
51
+
52
+ /**
53
+ * Warning message when a bounded context lacks a description.
54
+ * @param name - The name of the bounded context
55
+ */
56
+ BOUNDED_CONTEXT_NO_DESCRIPTION: (name: string) =>
57
+ `Bounded Context '${name}' is missing a description.`,
58
+
59
+ /**
60
+ * Warning message when a bounded context lacks a domain reference.
61
+ * @param name - The name of the bounded context
62
+ */
63
+ BOUNDED_CONTEXT_NO_DOMAIN: (name: string) =>
64
+ `Bounded Context '${name}' must belong to a domain. Use 'for DomainName'.`,
65
+
66
+ /**
67
+ * Warning when classification is specified both inline and in a block.
68
+ * Inline value takes precedence.
69
+ * @param bcName - The name of the bounded context
70
+ * @param inlineClassification - The inline classification name (from 'as')
71
+ * @param blockClassification - The block classification name (from 'classification:')
72
+ */
73
+ BOUNDED_CONTEXT_CLASSIFICATION_CONFLICT: (bcName: string, inlineClassification?: string, blockClassification?: string) =>
74
+ `Classification specified both inline${inlineClassification ? ` ('as ${inlineClassification}')` : ''} and in block${blockClassification ? ` ('classification: ${blockClassification}')` : ''}. Inline value takes precedence.`,
75
+
76
+ /**
77
+ * Warning when team is specified both inline and in a block.
78
+ * Inline value takes precedence.
79
+ * @param bcName - The name of the bounded context
80
+ * @param inlineTeam - The inline team name (from 'by')
81
+ * @param blockTeam - The block team name (from 'team:')
82
+ */
83
+ BOUNDED_CONTEXT_TEAM_CONFLICT: (bcName: string, inlineTeam?: string, blockTeam?: string) =>
84
+ `Team specified both inline${inlineTeam ? ` ('by ${inlineTeam}')` : ''} and in block${blockTeam ? ` ('team: ${blockTeam}')` : ''}. Inline value takes precedence.`,
85
+
86
+ /**
87
+ * Error message when an element is defined multiple times.
88
+ * @param fqn - The fully qualified name of the duplicate element
89
+ */
90
+ DUPLICATE_ELEMENT: (fqn: string) =>
91
+ `Duplicate element: '${fqn}' is already defined.`,
92
+
93
+ // ========================================================================
94
+ // Integration Pattern & Relationship Validation
95
+ // ========================================================================
96
+
97
+ /**
98
+ * Warning when SharedKernel pattern uses incorrect arrow direction.
99
+ * SharedKernel requires bidirectional relationship.
100
+ */
101
+ SHARED_KERNEL_MUST_BE_BIDIRECTIONAL: (leftContext: string, rightContext: string, arrow: string) =>
102
+ `SharedKernel between '${leftContext}' and '${rightContext}' requires bidirectional arrow '<->', not '${arrow}'.`,
103
+
104
+ /**
105
+ * Warning when Anti-Corruption Layer is on the wrong side of relationship.
106
+ * ACL should protect the consuming context (downstream).
107
+ */
108
+ ACL_ON_WRONG_SIDE: (context: string, side: 'left' | 'right') =>
109
+ `Anti-Corruption Layer (ACL) on '${context}' should be on downstream (consuming) side, not ${side} side.`,
110
+
111
+ /**
112
+ * Warning when Conformist pattern is on the wrong side.
113
+ * Conformist accepts upstream model without translation.
114
+ */
115
+ CONFORMIST_ON_WRONG_SIDE: (context: string, side: 'left' | 'right') =>
116
+ `Conformist (CF) on '${context}' should be on downstream (consuming) side, not ${side} side.`,
117
+
118
+ /**
119
+ * Info message when relationship has too many integration patterns.
120
+ * Suggests possible syntax confusion.
121
+ */
122
+ TOO_MANY_PATTERNS: (count: number, side: 'left' | 'right') =>
123
+ `Too many integration patterns (${count}) on ${side} side. Typically use 1-2 patterns per side.`
124
+ } as const;
@@ -0,0 +1,33 @@
1
+ import { modelChecks } from './model.js';
2
+ import { domainChecks } from './domain.js';
3
+ import { boundedContextChecks } from './bounded-context.js';
4
+ import { classificationChecks } from './classification.js';
5
+ import { metadataChecks } from './metadata.js';
6
+ import { contextMapChecks, domainMapChecks } from './maps.js';
7
+ import { relationshipChecks } from './relationships.js';
8
+ import { createImportChecks } from './import.js';
9
+ import type { ValidationChecks } from 'langium';
10
+ import type { DomainLangAstType } from '../generated/ast.js';
11
+ import { DomainLangServices } from '../domain-lang-module.js';
12
+
13
+ export function registerValidationChecks(services: DomainLangServices): void {
14
+ const registry = services.validation.ValidationRegistry;
15
+
16
+ // Get import checks
17
+ const importChecks = createImportChecks(services);
18
+
19
+ // Compose the pipeline for each type
20
+ const pipeline: ValidationChecks<DomainLangAstType> = {
21
+ Model: modelChecks,
22
+ Domain: domainChecks,
23
+ BoundedContext: boundedContextChecks,
24
+ Classification: classificationChecks,
25
+ ContextMap: contextMapChecks,
26
+ DomainMap: domainMapChecks,
27
+ Metadata: metadataChecks.Metadata,
28
+ ImportStatement: importChecks.ImportStatement,
29
+ Relationship: relationshipChecks,
30
+ };
31
+
32
+ registry.register(pipeline);
33
+ }
@@ -0,0 +1,24 @@
1
+ import type { ValidationAcceptor } from 'langium';
2
+ import type { Domain } from '../generated/ast.js';
3
+ import { ValidationMessages, buildCodeDescription } from './constants.js';
4
+
5
+ /**
6
+ * Validates that a domain has a vision statement.
7
+ *
8
+ * @param domain - The domain to validate
9
+ * @param accept - The validation acceptor for reporting issues
10
+ */
11
+ function validateDomainHasVision(
12
+ domain: Domain,
13
+ accept: ValidationAcceptor
14
+ ): void {
15
+ if (!domain.vision) {
16
+ accept('warning', ValidationMessages.DOMAIN_NO_VISION(domain.name), {
17
+ node: domain,
18
+ keyword: 'Domain',
19
+ codeDescription: buildCodeDescription('language.md', 'domain-vision')
20
+ });
21
+ }
22
+ }
23
+
24
+ export const domainChecks = [validateDomainHasVision];