@domainlang/language 0.8.0 β†’ 0.10.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.
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Model validation utilities for Node.js environments.
3
+ *
4
+ * **WARNING: This module is NOT browser-compatible.**
5
+ *
6
+ * Provides validation capabilities that leverage the LSP infrastructure
7
+ * for workspace initialization, import resolution, and document building.
8
+ *
9
+ * @module sdk/validator
10
+ */
11
+ import { NodeFileSystem } from 'langium/node';
12
+ import { URI } from 'langium';
13
+ import { createDomainLangServices } from '../domain-lang-module.js';
14
+ import { ensureImportGraphFromDocument } from '../utils/import-utils.js';
15
+ import { isModel } from '../generated/ast.js';
16
+ import { dirname, resolve, join } from 'node:path';
17
+ import { existsSync } from 'node:fs';
18
+ /**
19
+ * Convert Langium diagnostic to ValidationDiagnostic.
20
+ */
21
+ function toValidationDiagnostic(diagnostic, file) {
22
+ return {
23
+ severity: diagnostic.severity ?? 1,
24
+ message: diagnostic.message,
25
+ file,
26
+ line: diagnostic.range.start.line + 1,
27
+ column: diagnostic.range.start.character + 1,
28
+ };
29
+ }
30
+ /**
31
+ * Validates a DomainLang model file and all its imports.
32
+ *
33
+ * Uses the LSP infrastructure to:
34
+ * - Initialize the workspace
35
+ * - Resolve and load imports
36
+ * - Build and validate all documents
37
+ *
38
+ * @param filePath - Path to the entry .dlang file
39
+ * @param options - Validation options
40
+ * @returns Validation result with errors, warnings, and model statistics
41
+ * @throws Error if file doesn't exist or has invalid extension
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * import { validateFile } from '@domainlang/language/sdk';
46
+ *
47
+ * const result = await validateFile('./index.dlang');
48
+ *
49
+ * if (!result.valid) {
50
+ * for (const err of result.errors) {
51
+ * console.error(`${err.file}:${err.line}:${err.column}: ${err.message}`);
52
+ * }
53
+ * process.exit(1);
54
+ * }
55
+ *
56
+ * console.log(`βœ“ Validated ${result.fileCount} files`);
57
+ * console.log(` ${result.domainCount} domains, ${result.bcCount} bounded contexts`);
58
+ * ```
59
+ */
60
+ export async function validateFile(filePath, options = {}) {
61
+ // Resolve absolute path
62
+ const absolutePath = resolve(filePath);
63
+ // Check file exists
64
+ if (!existsSync(absolutePath)) {
65
+ throw new Error(`File not found: ${filePath}`);
66
+ }
67
+ // Create services with workspace support
68
+ const servicesObj = createDomainLangServices(NodeFileSystem);
69
+ const shared = servicesObj.shared;
70
+ const services = servicesObj.DomainLang;
71
+ // Check file extension
72
+ const extensions = services.LanguageMetaData.fileExtensions;
73
+ if (!extensions.some(ext => absolutePath.endsWith(ext))) {
74
+ throw new Error(`Invalid file extension. Expected: ${extensions.join(', ')}`);
75
+ }
76
+ // Initialize workspace with the specified directory or file's directory
77
+ const workspaceDir = options.workspaceDir ?? dirname(absolutePath);
78
+ const workspaceManager = services.imports.WorkspaceManager;
79
+ await workspaceManager.initialize(workspaceDir);
80
+ // Load and parse the document
81
+ const uri = URI.file(absolutePath);
82
+ const document = await shared.workspace.LangiumDocuments.getOrCreateDocument(uri);
83
+ // Build document initially without validation to load imports
84
+ await shared.workspace.DocumentBuilder.build([document], { validation: false });
85
+ // Load all imported documents via the import graph
86
+ const importResolver = services.imports.ImportResolver;
87
+ await ensureImportGraphFromDocument(document, shared.workspace.LangiumDocuments, importResolver);
88
+ // Build all documents with validation enabled
89
+ const allDocuments = Array.from(shared.workspace.LangiumDocuments.all);
90
+ await shared.workspace.DocumentBuilder.build(allDocuments, { validation: true });
91
+ // Collect diagnostics from the entry document
92
+ const diagnostics = document.diagnostics ?? [];
93
+ const errors = [];
94
+ const warnings = [];
95
+ for (const diagnostic of diagnostics) {
96
+ const validationDiag = toValidationDiagnostic(diagnostic, absolutePath);
97
+ if (diagnostic.severity === 1) {
98
+ errors.push(validationDiag);
99
+ }
100
+ else if (diagnostic.severity === 2) {
101
+ warnings.push(validationDiag);
102
+ }
103
+ }
104
+ // Count model elements across all documents
105
+ let domainCount = 0;
106
+ let bcCount = 0;
107
+ for (const doc of allDocuments) {
108
+ const model = doc.parseResult?.value;
109
+ if (isModel(model)) {
110
+ for (const element of model.children ?? []) {
111
+ if (element.$type === 'Domain') {
112
+ domainCount++;
113
+ }
114
+ else if (element.$type === 'BoundedContext') {
115
+ bcCount++;
116
+ }
117
+ }
118
+ }
119
+ }
120
+ return {
121
+ valid: errors.length === 0,
122
+ fileCount: allDocuments.length,
123
+ domainCount,
124
+ bcCount,
125
+ errors,
126
+ warnings,
127
+ };
128
+ }
129
+ /**
130
+ * Validates an entire DomainLang workspace.
131
+ *
132
+ * Uses the LSP infrastructure to:
133
+ * - Initialize the workspace from a directory containing model.yaml
134
+ * - Load the entry file (from manifest or default index.dlang)
135
+ * - Resolve and load all imports
136
+ * - Build and validate all documents in the workspace
137
+ * - Collect diagnostics from ALL documents (like VS Code Problems pane)
138
+ *
139
+ * @param workspaceDir - Path to the workspace directory (containing model.yaml)
140
+ * @returns Validation result with diagnostics from all files
141
+ * @throws Error if workspace directory doesn't exist or cannot be loaded
142
+ *
143
+ * @example
144
+ * ```typescript
145
+ * import { validateWorkspace } from '@domainlang/language/sdk';
146
+ *
147
+ * const result = await validateWorkspace('./my-workspace');
148
+ *
149
+ * if (!result.valid) {
150
+ * console.error(`Found ${result.errors.length} errors in ${result.fileCount} files`);
151
+ *
152
+ * for (const err of result.errors) {
153
+ * console.error(`${err.file}:${err.line}:${err.column}: ${err.message}`);
154
+ * }
155
+ * process.exit(1);
156
+ * }
157
+ *
158
+ * console.log(`βœ“ Validated ${result.fileCount} files`);
159
+ * console.log(` ${result.domainCount} domains, ${result.bcCount} bounded contexts`);
160
+ * console.log(` 0 errors, ${result.warnings.length} warnings`);
161
+ * ```
162
+ */
163
+ export async function validateWorkspace(workspaceDir) {
164
+ // Resolve absolute path
165
+ const absolutePath = resolve(workspaceDir);
166
+ // Check directory exists
167
+ if (!existsSync(absolutePath)) {
168
+ throw new Error(`Workspace directory not found: ${workspaceDir}`);
169
+ }
170
+ // Create services with workspace support
171
+ const servicesObj = createDomainLangServices(NodeFileSystem);
172
+ const shared = servicesObj.shared;
173
+ const services = servicesObj.DomainLang;
174
+ const workspaceManager = services.imports.WorkspaceManager;
175
+ try {
176
+ // Initialize workspace - this will find and load model.yaml
177
+ await workspaceManager.initialize(absolutePath);
178
+ }
179
+ catch (error) {
180
+ const message = error instanceof Error ? error.message : String(error);
181
+ throw new Error(`Failed to initialize workspace at ${workspaceDir}: ${message}`);
182
+ }
183
+ // Get the manifest to find the entry file
184
+ const manifest = await workspaceManager.getManifest();
185
+ let entryFile = 'index.dlang';
186
+ if (manifest?.model?.entry) {
187
+ entryFile = manifest.model.entry;
188
+ }
189
+ const entryPath = join(absolutePath, entryFile);
190
+ // Check if entry file exists
191
+ if (!existsSync(entryPath)) {
192
+ throw new Error(`Entry file not found: ${entryFile}\n` +
193
+ `Expected at: ${entryPath}\n` +
194
+ (manifest ? `Specified in manifest` : `Using default entry file`));
195
+ }
196
+ // Load and parse the entry document
197
+ const uri = URI.file(entryPath);
198
+ const document = await shared.workspace.LangiumDocuments.getOrCreateDocument(uri);
199
+ // Build document initially without validation to load imports
200
+ await shared.workspace.DocumentBuilder.build([document], { validation: false });
201
+ // Load all imported documents via the import graph
202
+ const importResolver = services.imports.ImportResolver;
203
+ await ensureImportGraphFromDocument(document, shared.workspace.LangiumDocuments, importResolver);
204
+ // Build all documents with validation enabled
205
+ const allDocuments = Array.from(shared.workspace.LangiumDocuments.all);
206
+ await shared.workspace.DocumentBuilder.build(allDocuments, { validation: true });
207
+ // Collect diagnostics from ALL documents (not just entry)
208
+ const errors = [];
209
+ const warnings = [];
210
+ for (const doc of allDocuments) {
211
+ const diagnostics = doc.diagnostics ?? [];
212
+ const docPath = doc.uri.fsPath;
213
+ for (const diagnostic of diagnostics) {
214
+ const validationDiag = toValidationDiagnostic(diagnostic, docPath);
215
+ if (diagnostic.severity === 1) {
216
+ errors.push(validationDiag);
217
+ }
218
+ else if (diagnostic.severity === 2) {
219
+ warnings.push(validationDiag);
220
+ }
221
+ }
222
+ }
223
+ // Count model elements across all documents
224
+ let domainCount = 0;
225
+ let bcCount = 0;
226
+ for (const doc of allDocuments) {
227
+ const model = doc.parseResult?.value;
228
+ if (isModel(model)) {
229
+ for (const element of model.children ?? []) {
230
+ if (element.$type === 'Domain') {
231
+ domainCount++;
232
+ }
233
+ else if (element.$type === 'BoundedContext') {
234
+ bcCount++;
235
+ }
236
+ }
237
+ }
238
+ }
239
+ return {
240
+ valid: errors.length === 0,
241
+ fileCount: allDocuments.length,
242
+ domainCount,
243
+ bcCount,
244
+ errors,
245
+ warnings,
246
+ totalDiagnostics: errors.length + warnings.length,
247
+ };
248
+ }
249
+ //# sourceMappingURL=validator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/sdk/validator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,GAAG,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,6BAA6B,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AA4CrC;;GAEG;AACH,SAAS,sBAAsB,CAC3B,UAAyG,EACzG,IAAY;IAEZ,OAAO;QACH,QAAQ,EAAE,UAAU,CAAC,QAAQ,IAAI,CAAC;QAClC,OAAO,EAAE,UAAU,CAAC,OAAO;QAC3B,IAAI;QACJ,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC;QACrC,MAAM,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC;KAC/C,CAAC;AACN,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAC9B,QAAgB,EAChB,UAA6B,EAAE;IAE/B,wBAAwB;IACxB,MAAM,YAAY,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEvC,oBAAoB;IACpB,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,mBAAmB,QAAQ,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,yCAAyC;IACzC,MAAM,WAAW,GAAG,wBAAwB,CAAC,cAAc,CAAC,CAAC;IAC7D,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC;IAClC,MAAM,QAAQ,GAAG,WAAW,CAAC,UAAU,CAAC;IAExC,uBAAuB;IACvB,MAAM,UAAU,GAAG,QAAQ,CAAC,gBAAgB,CAAC,cAAc,CAAC;IAC5D,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACtD,MAAM,IAAI,KAAK,CAAC,qCAAqC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,wEAAwE;IACxE,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC,CAAC;IACnE,MAAM,gBAAgB,GAAG,QAAQ,CAAC,OAAO,CAAC,gBAAgB,CAAC;IAC3D,MAAM,gBAAgB,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IAEhD,8BAA8B;IAC9B,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;IAElF,8DAA8D;IAC9D,MAAM,MAAM,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IAEhF,mDAAmD;IACnD,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC;IACvD,MAAM,6BAA6B,CAC/B,QAAQ,EACR,MAAM,CAAC,SAAS,CAAC,gBAAgB,EACjC,cAAc,CACjB,CAAC;IAEF,8CAA8C;IAC9C,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACvE,MAAM,MAAM,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAEjF,8CAA8C;IAC9C,MAAM,WAAW,GAAG,QAAQ,CAAC,WAAW,IAAI,EAAE,CAAC;IAC/C,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,MAAM,QAAQ,GAA2B,EAAE,CAAC;IAE5C,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACnC,MAAM,cAAc,GAAG,sBAAsB,CAAC,UAAU,EAAE,YAAY,CAAC,CAAC;QACxE,IAAI,UAAU,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;YAC5B,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAChC,CAAC;aAAM,IAAI,UAAU,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;YACnC,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAClC,CAAC;IACL,CAAC;IAED,4CAA4C;IAC5C,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC;QACrC,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjB,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;gBACzC,IAAI,OAAO,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;oBAC7B,WAAW,EAAE,CAAC;gBAClB,CAAC;qBAAM,IAAI,OAAO,CAAC,KAAK,KAAK,gBAAgB,EAAE,CAAC;oBAC5C,OAAO,EAAE,CAAC;gBACd,CAAC;YACL,CAAC;QACL,CAAC;IACL,CAAC;IAED,OAAO;QACH,KAAK,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;QAC1B,SAAS,EAAE,YAAY,CAAC,MAAM;QAC9B,WAAW;QACX,OAAO;QACP,MAAM;QACN,QAAQ;KACX,CAAC;AACN,CAAC;AAsBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACnC,YAAoB;IAEpB,wBAAwB;IACxB,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAE3C,yBAAyB;IACzB,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,kCAAkC,YAAY,EAAE,CAAC,CAAC;IACtE,CAAC;IAED,yCAAyC;IACzC,MAAM,WAAW,GAAG,wBAAwB,CAAC,cAAc,CAAC,CAAC;IAC7D,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC;IAClC,MAAM,QAAQ,GAAG,WAAW,CAAC,UAAU,CAAC;IACxC,MAAM,gBAAgB,GAAG,QAAQ,CAAC,OAAO,CAAC,gBAAgB,CAAC;IAE3D,IAAI,CAAC;QACD,4DAA4D;QAC5D,MAAM,gBAAgB,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;IACpD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,qCAAqC,YAAY,KAAK,OAAO,EAAE,CAAC,CAAC;IACrF,CAAC;IAED,0CAA0C;IAC1C,MAAM,QAAQ,GAAG,MAAM,gBAAgB,CAAC,WAAW,EAAE,CAAC;IACtD,IAAI,SAAS,GAAG,aAAa,CAAC;IAE9B,IAAI,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;QACzB,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC;IACrC,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;IAEhD,6BAA6B;IAC7B,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACX,yBAAyB,SAAS,IAAI;YACtC,gBAAgB,SAAS,IAAI;YAC7B,CAAC,QAAQ,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,0BAA0B,CAAC,CACpE,CAAC;IACN,CAAC;IAED,oCAAoC;IACpC,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAChC,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC;IAElF,8DAA8D;IAC9D,MAAM,MAAM,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,CAAC;IAEhF,mDAAmD;IACnD,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,cAAc,CAAC;IACvD,MAAM,6BAA6B,CAC/B,QAAQ,EACR,MAAM,CAAC,SAAS,CAAC,gBAAgB,EACjC,cAAc,CACjB,CAAC;IAEF,8CAA8C;IAC9C,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACvE,MAAM,MAAM,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC,YAAY,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAEjF,0DAA0D;IAC1D,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,MAAM,QAAQ,GAA2B,EAAE,CAAC;IAE5C,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC7B,MAAM,WAAW,GAAG,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC;QAC1C,MAAM,OAAO,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC;QAE/B,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;YACnC,MAAM,cAAc,GAAG,sBAAsB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAEnE,IAAI,UAAU,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;gBAC5B,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YAChC,CAAC;iBAAM,IAAI,UAAU,CAAC,QAAQ,KAAK,CAAC,EAAE,CAAC;gBACnC,QAAQ,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YAClC,CAAC;QACL,CAAC;IACL,CAAC;IAED,4CAA4C;IAC5C,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;QAC7B,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,KAAK,CAAC;QACrC,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACjB,KAAK,MAAM,OAAO,IAAI,KAAK,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;gBACzC,IAAI,OAAO,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;oBAC7B,WAAW,EAAE,CAAC;gBAClB,CAAC;qBAAM,IAAI,OAAO,CAAC,KAAK,KAAK,gBAAgB,EAAE,CAAC;oBAC5C,OAAO,EAAE,CAAC;gBACd,CAAC;YACL,CAAC;QACL,CAAC;IACL,CAAC;IAED,OAAO;QACH,KAAK,EAAE,MAAM,CAAC,MAAM,KAAK,CAAC;QAC1B,SAAS,EAAE,YAAY,CAAC,MAAM;QAC9B,WAAW;QACX,OAAO;QACP,MAAM;QACN,QAAQ;QACR,gBAAgB,EAAE,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM;KACpD,CAAC;AACN,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@domainlang/language",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "displayName": "DomainLang Language",
5
5
  "description": "Core language library for DomainLang - parse, validate, and query Domain-Driven Design models programmatically",
6
6
  "author": "larsbaunwall",
@@ -63,16 +63,15 @@
63
63
  "./syntaxes/domain-lang.monarch": {
64
64
  "types": "./out/syntaxes/domain-lang.monarch.d.ts",
65
65
  "default": "./out/syntaxes/domain-lang.monarch.js"
66
- },
67
- "./src/services/git-url-resolver.js": {
68
- "browser": "./src/services/git-url-resolver.browser.js",
69
- "default": "./src/services/git-url-resolver.js"
70
66
  }
71
67
  },
72
68
  "typesVersions": {
73
69
  "*": {
74
70
  ".": [
75
71
  "out/index"
72
+ ],
73
+ "sdk": [
74
+ "out/sdk/index"
76
75
  ]
77
76
  }
78
77
  },
package/src/index.ts CHANGED
@@ -28,3 +28,4 @@ export * from './utils/manifest-utils.js';
28
28
  // Export LSP services
29
29
  export * from './lsp/manifest-diagnostics.js';
30
30
  export { DomainLangIndexManager } from './lsp/domain-lang-index-manager.js';
31
+ export { registerToolHandlers } from './lsp/tool-handlers.js';
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Explanation generation for model elements (PRS-015)
3
+ *
4
+ * Reuses hover builder functions to generate rich markdown explanations
5
+ * of any model element. This provides the same content that appears in
6
+ * hover tooltips, but in a format suitable for Language Model Tools.
7
+ *
8
+ * @module lsp/explain
9
+ */
10
+
11
+ import type { AstNode } from 'langium';
12
+ import type {
13
+ BoundedContext,
14
+ Classification,
15
+ ContextMap,
16
+ Domain,
17
+ DomainMap,
18
+ Relationship,
19
+ Team,
20
+ } from '../generated/ast.js';
21
+ import {
22
+ isBoundedContext,
23
+ isClassification,
24
+ isContextMap,
25
+ isDomain,
26
+ isDomainMap,
27
+ isRelationship,
28
+ isTeam,
29
+ } from '../generated/ast.js';
30
+ import {
31
+ formatHoverContent,
32
+ buildDomainFields,
33
+ buildBcSignature,
34
+ codeBlock,
35
+ } from './hover/hover-builders.js';
36
+ import type { RefLinkFn } from './hover/hover-builders.js';
37
+ import { effectiveClassification, effectiveTeam } from '../sdk/resolution.js';
38
+
39
+ /**
40
+ * Creates a plain-text reference link function for explain output.
41
+ * Unlike the hover provider (which creates clickable links), this returns
42
+ * plain names suitable for Language Model consumption.
43
+ */
44
+ function createRefLink(): RefLinkFn {
45
+ return (ref, label) => {
46
+ if (!ref) return label ?? 'unknown';
47
+ return label ?? ref.name;
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Generates a rich markdown explanation for any model element.
53
+ * Delegates to the appropriate builder based on element type.
54
+ *
55
+ * @param node - AST node to explain
56
+ * @returns Markdown explanation
57
+ */
58
+ export function generateExplanation(node: AstNode): string {
59
+ const refLink = createRefLink();
60
+
61
+ if (isDomain(node)) {
62
+ return explainDomain(node, refLink);
63
+ } else if (isBoundedContext(node)) {
64
+ return explainBoundedContext(node, refLink);
65
+ } else if (isTeam(node)) {
66
+ return explainTeam(node);
67
+ } else if (isClassification(node)) {
68
+ return explainClassification(node);
69
+ } else if (isRelationship(node)) {
70
+ return explainRelationship(node);
71
+ } else if (isContextMap(node)) {
72
+ return explainContextMap(node);
73
+ } else if (isDomainMap(node)) {
74
+ return explainDomainMap(node);
75
+ } else {
76
+ return `**Unknown element type:** ${node.$type}`;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Explains a Domain element.
82
+ */
83
+ function explainDomain(domain: Domain, refLink: RefLinkFn): string {
84
+ const fields = buildDomainFields(domain, refLink);
85
+ return formatHoverContent('', 'πŸ›οΈ', 'domain', domain.name, fields);
86
+ }
87
+
88
+ /**
89
+ * Explains a BoundedContext element.
90
+ */
91
+ function explainBoundedContext(bc: BoundedContext, refLink: RefLinkFn): string {
92
+ const description = bc.description ?? '';
93
+ const classification = effectiveClassification(bc);
94
+ const team = effectiveTeam(bc);
95
+
96
+ const signature = codeBlock(buildBcSignature(bc));
97
+ const fields: string[] = [signature];
98
+
99
+ if (description) fields.push(description);
100
+
101
+ const extraFields: string[] = [];
102
+ if (bc.domain?.ref) {
103
+ extraFields.push('---', `πŸ“ **Domain:** ${refLink(bc.domain.ref)}`);
104
+ }
105
+ if (classification) {
106
+ extraFields.push(`πŸ”– **Classification:** ${refLink(classification)}`);
107
+ }
108
+ if (team) {
109
+ extraFields.push(`πŸ‘₯ **Team:** ${refLink(team)}`);
110
+ }
111
+ fields.push(...extraFields);
112
+
113
+ return formatHoverContent('', 'πŸ“¦', 'bounded context', bc.name, fields);
114
+ }
115
+
116
+ /**
117
+ * Explains a Team element.
118
+ */
119
+ function explainTeam(team: Team): string {
120
+ return formatHoverContent('', 'πŸ‘₯', 'team', team.name, []);
121
+ }
122
+
123
+ /**
124
+ * Explains a Classification element.
125
+ */
126
+ function explainClassification(classification: Classification): string {
127
+ return formatHoverContent('', '🏷️', 'classification', classification.name, []);
128
+ }
129
+
130
+ /**
131
+ * Explains a Relationship element.
132
+ */
133
+ function explainRelationship(relationship: Relationship): string {
134
+ const leftName = relationship.left.link?.ref?.name ?? 'unknown';
135
+ const rightName = relationship.right.link?.ref?.name ?? 'unknown';
136
+ const arrow = relationship.arrow;
137
+
138
+ const description = `Relationship from **${leftName}** ${arrow} **${rightName}**`;
139
+ return formatHoverContent('', 'πŸ”—', 'relationship', undefined, [description]);
140
+ }
141
+
142
+ /**
143
+ * Explains a ContextMap element.
144
+ */
145
+ function explainContextMap(contextMap: ContextMap): string {
146
+ const bcNames = contextMap.boundedContexts
147
+ .flatMap(mr => mr.items.map(item => item.ref?.name ?? 'unknown'));
148
+ const relCount = contextMap.relationships.length;
149
+
150
+ const fields: string[] = [];
151
+ if (bcNames.length > 0) {
152
+ fields.push(`**Bounded contexts:** ${bcNames.join(', ')}`);
153
+ }
154
+ fields.push(`**Relationships:** ${relCount}`);
155
+
156
+ return formatHoverContent('', 'πŸ—ΊοΈ', 'context map', contextMap.name, fields);
157
+ }
158
+
159
+ /**
160
+ * Explains a DomainMap element.
161
+ */
162
+ function explainDomainMap(domainMap: DomainMap): string {
163
+ const domainNames = domainMap.domains
164
+ .flatMap(mr => mr.items.map(item => item.ref?.name ?? 'unknown'));
165
+
166
+ const fields: string[] = [];
167
+ if (domainNames.length > 0) {
168
+ fields.push(`**Domains:** ${domainNames.join(', ')}`);
169
+ }
170
+
171
+ return formatHoverContent('', '🌐', 'domain map', domainMap.name, fields);
172
+ }