@domainlang/language 0.9.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.
- package/out/index.d.ts +1 -0
- package/out/index.js +1 -0
- package/out/index.js.map +1 -1
- package/out/lsp/explain.d.ts +18 -0
- package/out/lsp/explain.js +138 -0
- package/out/lsp/explain.js.map +1 -0
- package/out/lsp/tool-handlers.d.ts +113 -0
- package/out/lsp/tool-handlers.js +297 -0
- package/out/lsp/tool-handlers.js.map +1 -0
- package/out/main.js +3 -0
- package/out/main.js.map +1 -1
- package/out/sdk/index.d.ts +2 -0
- package/out/sdk/index.js +2 -0
- package/out/sdk/index.js.map +1 -1
- package/out/sdk/serializers.d.ts +110 -0
- package/out/sdk/serializers.js +158 -0
- package/out/sdk/serializers.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/lsp/explain.ts +172 -0
- package/src/lsp/tool-handlers.ts +443 -0
- package/src/main.ts +4 -0
- package/src/sdk/index.ts +11 -0
- package/src/sdk/serializers.ts +213 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP Custom Request Handlers for VS Code Language Model Tools (PRS-015)
|
|
3
|
+
*
|
|
4
|
+
* These handlers respond to custom LSP requests from the VS Code extension
|
|
5
|
+
* and return serialized model data suitable for Language Model Tools.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - Extension calls `client.sendRequest('domainlang/validate', params)`
|
|
9
|
+
* - LSP receives via `connection.onRequest('domainlang/validate', handler)`
|
|
10
|
+
* - Handler uses SDK `fromServices()` for zero-copy AST access
|
|
11
|
+
* - Handler returns plain JSON (no circular refs, no class instances)
|
|
12
|
+
*
|
|
13
|
+
* @module lsp/tool-handlers
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Connection } from 'vscode-languageserver';
|
|
17
|
+
import type { LangiumDocument } from 'langium';
|
|
18
|
+
import type { LangiumSharedServices } from 'langium/lsp';
|
|
19
|
+
import { URI } from 'langium';
|
|
20
|
+
import { fromDocument } from '../sdk/query.js';
|
|
21
|
+
import type { Query } from '../sdk/types.js';
|
|
22
|
+
import {
|
|
23
|
+
serializeNode,
|
|
24
|
+
serializeRelationship,
|
|
25
|
+
normalizeEntityType,
|
|
26
|
+
} from '../sdk/serializers.js';
|
|
27
|
+
import type { QueryEntityType, QueryFilters } from '../sdk/serializers.js';
|
|
28
|
+
import type { Model } from '../generated/ast.js';
|
|
29
|
+
import { generateExplanation } from './explain.js';
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Request/Response Types
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Request parameters for domainlang/validate.
|
|
37
|
+
* No parameters needed - validates entire workspace.
|
|
38
|
+
*/
|
|
39
|
+
export interface ValidateParams {
|
|
40
|
+
/** Optional: filter by file URI */
|
|
41
|
+
file?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Response from domainlang/validate.
|
|
46
|
+
*/
|
|
47
|
+
export interface ValidateResponse {
|
|
48
|
+
/** Total number of validation diagnostics */
|
|
49
|
+
count: number;
|
|
50
|
+
/** Validation diagnostics grouped by severity */
|
|
51
|
+
diagnostics: {
|
|
52
|
+
errors: DiagnosticInfo[];
|
|
53
|
+
warnings: DiagnosticInfo[];
|
|
54
|
+
info: DiagnosticInfo[];
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Diagnostic information for validation response.
|
|
60
|
+
*/
|
|
61
|
+
export interface DiagnosticInfo {
|
|
62
|
+
/** File URI */
|
|
63
|
+
file: string;
|
|
64
|
+
/** Line number (1-indexed) */
|
|
65
|
+
line: number;
|
|
66
|
+
/** Column number (1-indexed) */
|
|
67
|
+
column: number;
|
|
68
|
+
/** Diagnostic message */
|
|
69
|
+
message: string;
|
|
70
|
+
/** Severity level */
|
|
71
|
+
severity: 'error' | 'warning' | 'info';
|
|
72
|
+
/** Optional diagnostic code */
|
|
73
|
+
code?: string | number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Request parameters for domainlang/list.
|
|
78
|
+
*/
|
|
79
|
+
export interface ListParams {
|
|
80
|
+
/** Entity type to query */
|
|
81
|
+
type: string;
|
|
82
|
+
/** Optional filters */
|
|
83
|
+
filters?: QueryFilters;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Response from domainlang/list.
|
|
88
|
+
*/
|
|
89
|
+
export interface ListResponse {
|
|
90
|
+
/** Entity type queried */
|
|
91
|
+
entityType: QueryEntityType;
|
|
92
|
+
/** Number of results */
|
|
93
|
+
count: number;
|
|
94
|
+
/** Serialized results */
|
|
95
|
+
results: Record<string, unknown>[];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Request parameters for domainlang/get.
|
|
100
|
+
*/
|
|
101
|
+
export interface GetParams {
|
|
102
|
+
/** Fully qualified name of element to retrieve */
|
|
103
|
+
fqn?: string;
|
|
104
|
+
/** If true, return model summary instead of single element */
|
|
105
|
+
summary?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Response from domainlang/get.
|
|
110
|
+
*/
|
|
111
|
+
export interface GetResponse {
|
|
112
|
+
/** Serialized element or model summary */
|
|
113
|
+
result: Record<string, unknown> | null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Request parameters for domainlang/explain.
|
|
118
|
+
*/
|
|
119
|
+
export interface ExplainParams {
|
|
120
|
+
/** Fully qualified name of element to explain */
|
|
121
|
+
fqn: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Response from domainlang/explain.
|
|
126
|
+
*/
|
|
127
|
+
export interface ExplainResponse {
|
|
128
|
+
/** Rich markdown explanation */
|
|
129
|
+
explanation: string;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
133
|
+
// Handler Registration
|
|
134
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Registers all custom request handlers on the LSP connection.
|
|
138
|
+
* Call this from main.ts after creating the connection.
|
|
139
|
+
*
|
|
140
|
+
* @param connection - LSP connection
|
|
141
|
+
* @param sharedServices - Langium shared services for workspace access
|
|
142
|
+
*/
|
|
143
|
+
export function registerToolHandlers(
|
|
144
|
+
connection: Connection,
|
|
145
|
+
sharedServices: LangiumSharedServices
|
|
146
|
+
): void {
|
|
147
|
+
connection.onRequest('domainlang/validate', async (params: ValidateParams) => {
|
|
148
|
+
return handleValidate(params, sharedServices);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
connection.onRequest('domainlang/list', async (params: ListParams) => {
|
|
152
|
+
return handleList(params, sharedServices);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
connection.onRequest('domainlang/get', async (params: GetParams) => {
|
|
156
|
+
return handleGet(params, sharedServices);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
connection.onRequest('domainlang/explain', async (params: ExplainParams) => {
|
|
160
|
+
return handleExplain(params, sharedServices);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
// Handler Implementations
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Handles domainlang/validate requests.
|
|
170
|
+
* Aggregates all validation diagnostics from the workspace.
|
|
171
|
+
*/
|
|
172
|
+
async function handleValidate(
|
|
173
|
+
params: ValidateParams,
|
|
174
|
+
sharedServices: LangiumSharedServices
|
|
175
|
+
): Promise<ValidateResponse> {
|
|
176
|
+
try {
|
|
177
|
+
const langiumDocs = sharedServices.workspace.LangiumDocuments;
|
|
178
|
+
const documents = params.file
|
|
179
|
+
? [langiumDocs.getDocument(URI.parse(params.file))]
|
|
180
|
+
: Array.from(langiumDocs.all);
|
|
181
|
+
|
|
182
|
+
const errors: DiagnosticInfo[] = [];
|
|
183
|
+
const warnings: DiagnosticInfo[] = [];
|
|
184
|
+
const info: DiagnosticInfo[] = [];
|
|
185
|
+
|
|
186
|
+
for (const doc of documents) {
|
|
187
|
+
if (!doc) continue;
|
|
188
|
+
|
|
189
|
+
const diagnostics = doc.diagnostics ?? [];
|
|
190
|
+
for (const diag of diagnostics) {
|
|
191
|
+
const diagInfo: DiagnosticInfo = {
|
|
192
|
+
file: doc.uri.toString(),
|
|
193
|
+
line: diag.range.start.line + 1, // 1-indexed
|
|
194
|
+
column: diag.range.start.character + 1, // 1-indexed
|
|
195
|
+
message: diag.message,
|
|
196
|
+
severity: severityToString(diag.severity ?? 1),
|
|
197
|
+
code: diag.code,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
if (diag.severity === 1) {
|
|
201
|
+
errors.push(diagInfo);
|
|
202
|
+
} else if (diag.severity === 2) {
|
|
203
|
+
warnings.push(diagInfo);
|
|
204
|
+
} else {
|
|
205
|
+
info.push(diagInfo);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
count: errors.length + warnings.length + info.length,
|
|
212
|
+
diagnostics: { errors, warnings, info },
|
|
213
|
+
};
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('domainlang/validate handler error:', error);
|
|
216
|
+
return { count: 0, diagnostics: { errors: [], warnings: [], info: [] } };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Handles domainlang/list requests.
|
|
222
|
+
* Queries entities of a specific type and returns serialized results.
|
|
223
|
+
*/
|
|
224
|
+
async function handleList(
|
|
225
|
+
params: ListParams,
|
|
226
|
+
sharedServices: LangiumSharedServices
|
|
227
|
+
): Promise<ListResponse> {
|
|
228
|
+
try {
|
|
229
|
+
const entityType = normalizeEntityType(params.type);
|
|
230
|
+
const filters = params.filters ?? {};
|
|
231
|
+
|
|
232
|
+
// Get all documents and merge results
|
|
233
|
+
const langiumDocs = sharedServices.workspace.LangiumDocuments;
|
|
234
|
+
const documents = Array.from(langiumDocs.all);
|
|
235
|
+
|
|
236
|
+
const allResults: Record<string, unknown>[] = [];
|
|
237
|
+
const seen = new Set<string>(); // Deduplicate by FQN
|
|
238
|
+
|
|
239
|
+
for (const doc of documents) {
|
|
240
|
+
const query = fromDocument(doc as LangiumDocument<Model>);
|
|
241
|
+
const results = executeListQuery(query, entityType, filters);
|
|
242
|
+
|
|
243
|
+
for (const result of results) {
|
|
244
|
+
const fqn = result.fqn as string | undefined;
|
|
245
|
+
if (fqn && seen.has(fqn)) continue;
|
|
246
|
+
if (fqn) seen.add(fqn);
|
|
247
|
+
allResults.push(result);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
entityType,
|
|
253
|
+
count: allResults.length,
|
|
254
|
+
results: allResults,
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('domainlang/list handler error:', error);
|
|
258
|
+
return { entityType: normalizeEntityType(params.type), count: 0, results: [] };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Handles domainlang/get requests.
|
|
264
|
+
* Retrieves a single element by FQN or returns a model summary.
|
|
265
|
+
*/
|
|
266
|
+
async function handleGet(
|
|
267
|
+
params: GetParams,
|
|
268
|
+
sharedServices: LangiumSharedServices
|
|
269
|
+
): Promise<GetResponse> {
|
|
270
|
+
try {
|
|
271
|
+
if (params.summary) {
|
|
272
|
+
return { result: await getModelSummary(sharedServices) };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!params.fqn) {
|
|
276
|
+
return { result: null };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Search all documents for the element
|
|
280
|
+
const langiumDocs = sharedServices.workspace.LangiumDocuments;
|
|
281
|
+
const documents = Array.from(langiumDocs.all);
|
|
282
|
+
|
|
283
|
+
for (const doc of documents) {
|
|
284
|
+
const query = fromDocument(doc as LangiumDocument<Model>);
|
|
285
|
+
const element = query.byFqn(params.fqn);
|
|
286
|
+
if (element) {
|
|
287
|
+
return { result: serializeNode(element, query) };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { result: null };
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error('domainlang/get handler error:', error);
|
|
294
|
+
return { result: null };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Handles domainlang/explain requests.
|
|
300
|
+
* Returns rich markdown explanation of a model element.
|
|
301
|
+
*/
|
|
302
|
+
async function handleExplain(
|
|
303
|
+
params: ExplainParams,
|
|
304
|
+
sharedServices: LangiumSharedServices
|
|
305
|
+
): Promise<ExplainResponse> {
|
|
306
|
+
try {
|
|
307
|
+
const langiumDocs = sharedServices.workspace.LangiumDocuments;
|
|
308
|
+
const documents = Array.from(langiumDocs.all);
|
|
309
|
+
|
|
310
|
+
for (const doc of documents) {
|
|
311
|
+
const query = fromDocument(doc as LangiumDocument<Model>);
|
|
312
|
+
const element = query.byFqn(params.fqn);
|
|
313
|
+
if (element) {
|
|
314
|
+
const explanation = generateExplanation(element);
|
|
315
|
+
return { explanation };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
explanation: `Element not found: ${params.fqn}`,
|
|
321
|
+
};
|
|
322
|
+
} catch (error) {
|
|
323
|
+
console.error('domainlang/explain handler error:', error);
|
|
324
|
+
return { explanation: `Error explaining element: ${params.fqn}` };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
329
|
+
// Helper Functions
|
|
330
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Executes a list query for a specific entity type.
|
|
334
|
+
*/
|
|
335
|
+
function executeListQuery(
|
|
336
|
+
query: Query,
|
|
337
|
+
entityType: QueryEntityType,
|
|
338
|
+
filters: QueryFilters
|
|
339
|
+
): Record<string, unknown>[] {
|
|
340
|
+
switch (entityType) {
|
|
341
|
+
case 'domains': {
|
|
342
|
+
let builder = query.domains();
|
|
343
|
+
if (filters.name) builder = builder.withName(filters.name);
|
|
344
|
+
if (filters.fqn) builder = builder.withFqn(filters.fqn);
|
|
345
|
+
return builder.toArray().map((d) => serializeNode(d, query));
|
|
346
|
+
}
|
|
347
|
+
case 'bcs': {
|
|
348
|
+
let builder = query.boundedContexts();
|
|
349
|
+
if (filters.domain) builder = builder.inDomain(filters.domain);
|
|
350
|
+
if (filters.team) builder = builder.withTeam(filters.team);
|
|
351
|
+
if (filters.classification)
|
|
352
|
+
builder = builder.withClassification(filters.classification);
|
|
353
|
+
if (filters.metadata) {
|
|
354
|
+
const [key, value] = filters.metadata.split('=');
|
|
355
|
+
builder = builder.withMetadata(key, value);
|
|
356
|
+
}
|
|
357
|
+
if (filters.name) builder = builder.withName(filters.name) as ReturnType<Query['boundedContexts']>;
|
|
358
|
+
if (filters.fqn) builder = builder.withFqn(filters.fqn) as ReturnType<Query['boundedContexts']>;
|
|
359
|
+
return builder.toArray().map((bc) => serializeNode(bc, query));
|
|
360
|
+
}
|
|
361
|
+
case 'teams': {
|
|
362
|
+
let builder = query.teams();
|
|
363
|
+
if (filters.name) builder = builder.withName(filters.name);
|
|
364
|
+
return builder.toArray().map((t) => serializeNode(t, query));
|
|
365
|
+
}
|
|
366
|
+
case 'classifications': {
|
|
367
|
+
let builder = query.classifications();
|
|
368
|
+
if (filters.name) builder = builder.withName(filters.name);
|
|
369
|
+
return builder.toArray().map((c) => serializeNode(c, query));
|
|
370
|
+
}
|
|
371
|
+
case 'relationships': {
|
|
372
|
+
const rels = query.relationships().toArray();
|
|
373
|
+
return rels.map((r) => serializeRelationship(r));
|
|
374
|
+
}
|
|
375
|
+
case 'context-maps': {
|
|
376
|
+
let builder = query.contextMaps();
|
|
377
|
+
if (filters.name) builder = builder.withName(filters.name);
|
|
378
|
+
return builder.toArray().map((cm) => serializeNode(cm, query));
|
|
379
|
+
}
|
|
380
|
+
case 'domain-maps': {
|
|
381
|
+
let builder = query.domainMaps();
|
|
382
|
+
if (filters.name) builder = builder.withName(filters.name);
|
|
383
|
+
return builder.toArray().map((dm) => serializeNode(dm, query));
|
|
384
|
+
}
|
|
385
|
+
default:
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Generates a model summary with counts of major entities.
|
|
392
|
+
*/
|
|
393
|
+
async function getModelSummary(
|
|
394
|
+
sharedServices: LangiumSharedServices
|
|
395
|
+
): Promise<Record<string, unknown>> {
|
|
396
|
+
const langiumDocs = sharedServices.workspace.LangiumDocuments;
|
|
397
|
+
const documents = Array.from(langiumDocs.all);
|
|
398
|
+
|
|
399
|
+
let domains = 0;
|
|
400
|
+
let bcs = 0;
|
|
401
|
+
let teams = 0;
|
|
402
|
+
let classifications = 0;
|
|
403
|
+
let relationships = 0;
|
|
404
|
+
let contextMaps = 0;
|
|
405
|
+
let domainMaps = 0;
|
|
406
|
+
|
|
407
|
+
for (const doc of documents) {
|
|
408
|
+
const query = fromDocument(doc as LangiumDocument<Model>);
|
|
409
|
+
domains += query.domains().count();
|
|
410
|
+
bcs += query.boundedContexts().count();
|
|
411
|
+
teams += query.teams().count();
|
|
412
|
+
classifications += query.classifications().count();
|
|
413
|
+
relationships += query.relationships().count();
|
|
414
|
+
contextMaps += query.contextMaps().count();
|
|
415
|
+
domainMaps += query.domainMaps().count();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
$type: 'ModelSummary',
|
|
420
|
+
documentCount: documents.length,
|
|
421
|
+
domains,
|
|
422
|
+
boundedContexts: bcs,
|
|
423
|
+
teams,
|
|
424
|
+
classifications,
|
|
425
|
+
relationships,
|
|
426
|
+
contextMaps,
|
|
427
|
+
domainMaps,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Converts diagnostic severity number to string.
|
|
433
|
+
*/
|
|
434
|
+
function severityToString(severity: number): 'error' | 'warning' | 'info' {
|
|
435
|
+
switch (severity) {
|
|
436
|
+
case 1:
|
|
437
|
+
return 'error';
|
|
438
|
+
case 2:
|
|
439
|
+
return 'warning';
|
|
440
|
+
default:
|
|
441
|
+
return 'info';
|
|
442
|
+
}
|
|
443
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createConnection, ProposedFeatures, FileChangeType } from 'vscode-langu
|
|
|
4
4
|
import { createDomainLangServices } from './domain-lang-module.js';
|
|
5
5
|
import { ensureImportGraphFromEntryFile } from './utils/import-utils.js';
|
|
6
6
|
import { DomainLangIndexManager } from './lsp/domain-lang-index-manager.js';
|
|
7
|
+
import { registerToolHandlers } from './lsp/tool-handlers.js';
|
|
7
8
|
import { URI } from 'langium';
|
|
8
9
|
|
|
9
10
|
// Create a connection to the client
|
|
@@ -12,6 +13,9 @@ const connection = createConnection(ProposedFeatures.all);
|
|
|
12
13
|
// Inject the shared services and language-specific services
|
|
13
14
|
const { shared, DomainLang } = createDomainLangServices({ connection, ...NodeFileSystem });
|
|
14
15
|
|
|
16
|
+
// Register custom LSP request handlers for VS Code Language Model Tools (PRS-015)
|
|
17
|
+
registerToolHandlers(connection, shared);
|
|
18
|
+
|
|
15
19
|
// Initialize workspace manager when language server initializes
|
|
16
20
|
// Uses Langium's LanguageServer.onInitialize hook (not raw connection handler)
|
|
17
21
|
// This integrates properly with Langium's initialization flow
|
package/src/sdk/index.ts
CHANGED
|
@@ -142,6 +142,17 @@ export type {
|
|
|
142
142
|
RelationshipView,
|
|
143
143
|
} from './types.js';
|
|
144
144
|
|
|
145
|
+
// Serializers for tool responses (browser-safe)
|
|
146
|
+
export {
|
|
147
|
+
serializeNode,
|
|
148
|
+
serializeRelationship,
|
|
149
|
+
resolveName,
|
|
150
|
+
resolveMultiReference,
|
|
151
|
+
normalizeEntityType,
|
|
152
|
+
ENTITY_ALIASES,
|
|
153
|
+
} from './serializers.js';
|
|
154
|
+
export type { QueryEntityType, QueryEntityInput, QueryFilters } from './serializers.js';
|
|
155
|
+
|
|
145
156
|
// Node.js-specific exports (will fail in browser environments)
|
|
146
157
|
export { loadModel } from './loader-node.js';
|
|
147
158
|
export { validateFile, validateWorkspace } from './validator.js';
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST Serialization Utilities
|
|
3
|
+
*
|
|
4
|
+
* Converts Langium AST nodes to plain JSON objects suitable for:
|
|
5
|
+
* - LSP custom requests (JSON-RPC transport)
|
|
6
|
+
* - MCP tool responses (stdio JSON)
|
|
7
|
+
* - CLI output (JSON/YAML formats)
|
|
8
|
+
*
|
|
9
|
+
* ## Strategy
|
|
10
|
+
*
|
|
11
|
+
* Rather than maintaining a parallel DTO type hierarchy (DomainDto, BoundedContextDto, etc.),
|
|
12
|
+
* we use a **generic serializer** that:
|
|
13
|
+
* - Strips Langium internal properties ($container, $cstNode, $document)
|
|
14
|
+
* - Preserves $type for discriminated output
|
|
15
|
+
* - Resolves Reference<T> to referenced name strings
|
|
16
|
+
* - Resolves MultiReference<T> to arrays of names
|
|
17
|
+
* - Recursively serializes child AstNodes
|
|
18
|
+
* - Adds FQN for named elements via Query
|
|
19
|
+
*
|
|
20
|
+
* For types with SDK-augmented properties (computed values not on raw AST),
|
|
21
|
+
* use augmentation functions that enrich the generic output.
|
|
22
|
+
*
|
|
23
|
+
* @packageDocumentation
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { AstNode, Reference } from 'langium';
|
|
27
|
+
import { isAstNode, isReference } from 'langium';
|
|
28
|
+
import type { Query, RelationshipView } from './types.js';
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// Types
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Canonical entity types that can be queried.
|
|
36
|
+
* Moved from CLI to SDK for sharing with LSP tools.
|
|
37
|
+
*/
|
|
38
|
+
export type QueryEntityType =
|
|
39
|
+
| 'domains'
|
|
40
|
+
| 'bcs'
|
|
41
|
+
| 'teams'
|
|
42
|
+
| 'classifications'
|
|
43
|
+
| 'relationships'
|
|
44
|
+
| 'context-maps'
|
|
45
|
+
| 'domain-maps';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* All accepted entity type names, including aliases.
|
|
49
|
+
* Aliases are normalized to canonical types before query execution.
|
|
50
|
+
*/
|
|
51
|
+
export type QueryEntityInput = QueryEntityType
|
|
52
|
+
| 'bounded-contexts' | 'contexts'
|
|
53
|
+
| 'rels'
|
|
54
|
+
| 'cmaps'
|
|
55
|
+
| 'dmaps';
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Query filter options.
|
|
59
|
+
* Moved from CLI to SDK for sharing with LSP tools.
|
|
60
|
+
*/
|
|
61
|
+
export interface QueryFilters {
|
|
62
|
+
/** Filter by name (string or regex) */
|
|
63
|
+
name?: string;
|
|
64
|
+
/** Filter by fully qualified name */
|
|
65
|
+
fqn?: string;
|
|
66
|
+
/** Filter BCs by domain */
|
|
67
|
+
domain?: string;
|
|
68
|
+
/** Filter BCs by team */
|
|
69
|
+
team?: string;
|
|
70
|
+
/** Filter BCs by classification */
|
|
71
|
+
classification?: string;
|
|
72
|
+
/** Filter BCs by metadata key=value */
|
|
73
|
+
metadata?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Map of entity type aliases to their canonical form.
|
|
78
|
+
*/
|
|
79
|
+
export const ENTITY_ALIASES: Record<string, QueryEntityType> = {
|
|
80
|
+
'bounded-contexts': 'bcs',
|
|
81
|
+
'contexts': 'bcs',
|
|
82
|
+
'rels': 'relationships',
|
|
83
|
+
'cmaps': 'context-maps',
|
|
84
|
+
'dmaps': 'domain-maps',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Normalize an entity type input (which may be an alias) to its canonical form.
|
|
89
|
+
*/
|
|
90
|
+
export function normalizeEntityType(input: string): QueryEntityType {
|
|
91
|
+
if (input in ENTITY_ALIASES) {
|
|
92
|
+
return ENTITY_ALIASES[input];
|
|
93
|
+
}
|
|
94
|
+
return input as QueryEntityType;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
// Generic AST Serialization
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Serialize any Langium AST node to a plain JSON object.
|
|
103
|
+
*
|
|
104
|
+
* - Strips $-prefixed internal properties ($container, $cstNode, $document)
|
|
105
|
+
* - Preserves $type for discriminated output
|
|
106
|
+
* - Resolves Reference<T> to the referenced name (string)
|
|
107
|
+
* - Resolves MultiReference<T> to an array of names
|
|
108
|
+
* - Recursively serializes child AstNode properties
|
|
109
|
+
* - Serializes arrays of AstNodes/values
|
|
110
|
+
* - Adds FQN for named elements
|
|
111
|
+
*
|
|
112
|
+
* @param node - AST node to serialize
|
|
113
|
+
* @param query - Query instance for FQN resolution
|
|
114
|
+
* @returns Plain JSON object
|
|
115
|
+
*/
|
|
116
|
+
export function serializeNode(node: AstNode, query: Query): Record<string, unknown> {
|
|
117
|
+
const result: Record<string, unknown> = { $type: node.$type };
|
|
118
|
+
|
|
119
|
+
for (const [key, value] of Object.entries(node)) {
|
|
120
|
+
// Skip Langium internals (but preserve $type)
|
|
121
|
+
if (key.startsWith('$') && key !== '$type') {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (isReference(value)) {
|
|
126
|
+
// Reference<T> → name string
|
|
127
|
+
const ref = value.ref;
|
|
128
|
+
result[key] = (ref && 'name' in ref) ? (ref as { name?: string }).name : value.$refText;
|
|
129
|
+
} else if (isAstNode(value)) {
|
|
130
|
+
// Nested AstNode → recurse
|
|
131
|
+
result[key] = serializeNode(value, query);
|
|
132
|
+
} else if (Array.isArray(value)) {
|
|
133
|
+
// Array → map each item
|
|
134
|
+
result[key] = value.map(item => {
|
|
135
|
+
if (isReference(item)) {
|
|
136
|
+
const itemRef = item.ref;
|
|
137
|
+
return (itemRef && 'name' in itemRef) ? (itemRef as { name?: string }).name : item.$refText;
|
|
138
|
+
} else if (isAstNode(item)) {
|
|
139
|
+
return serializeNode(item, query);
|
|
140
|
+
} else {
|
|
141
|
+
return item; // primitive
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
// Primitives pass through
|
|
146
|
+
result[key] = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Always include FQN for named elements
|
|
151
|
+
if ('name' in node && typeof (node as { name?: unknown }).name === 'string') {
|
|
152
|
+
result.fqn = query.fqn(node);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Augment a serialized RelationshipView with computed properties.
|
|
160
|
+
*
|
|
161
|
+
* RelationshipView is already a clean DTO (not an AstNode), but we format it
|
|
162
|
+
* consistently with other serialized types.
|
|
163
|
+
*
|
|
164
|
+
* @param view - RelationshipView from query.relationships()
|
|
165
|
+
* @returns Serialized relationship object
|
|
166
|
+
*/
|
|
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;
|
|
171
|
+
return {
|
|
172
|
+
$type: 'Relationship',
|
|
173
|
+
name: `${leftName} ${view.arrow} ${rightName}`,
|
|
174
|
+
left: leftName,
|
|
175
|
+
right: rightName,
|
|
176
|
+
arrow: view.arrow,
|
|
177
|
+
leftPatterns: view.leftPatterns,
|
|
178
|
+
rightPatterns: view.rightPatterns,
|
|
179
|
+
inferredType: view.inferredType,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
// Helper: Resolve Reference
|
|
185
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve a Reference<T> to its name string.
|
|
189
|
+
* Returns undefined if reference is unresolved.
|
|
190
|
+
*
|
|
191
|
+
* @param ref - Reference to resolve
|
|
192
|
+
* @returns Referenced name or undefined
|
|
193
|
+
*/
|
|
194
|
+
export function resolveName<T extends AstNode & { name?: string }>(ref: Reference<T> | undefined): string | undefined {
|
|
195
|
+
if (!ref) return undefined;
|
|
196
|
+
return ref.ref?.name ?? ref.$refText;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Resolve a MultiReference (array of items with refs) to an array of names.
|
|
201
|
+
* Filters out unresolved references.
|
|
202
|
+
*
|
|
203
|
+
* @param multiRef - Array of items with ref property
|
|
204
|
+
* @returns Array of resolved names
|
|
205
|
+
*/
|
|
206
|
+
export function resolveMultiReference<T extends { ref?: Reference<AstNode & { name?: string }> }>(
|
|
207
|
+
multiRef: T[] | undefined
|
|
208
|
+
): string[] {
|
|
209
|
+
if (!multiRef) return [];
|
|
210
|
+
return multiRef
|
|
211
|
+
.map(item => item.ref?.ref?.name)
|
|
212
|
+
.filter((name): name is string => name !== undefined);
|
|
213
|
+
}
|