agentlang 0.10.2 → 0.10.4

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 (154) hide show
  1. package/README.md +7 -14
  2. package/out/api/http.d.ts +4 -0
  3. package/out/api/http.d.ts.map +1 -1
  4. package/out/api/http.js +171 -26
  5. package/out/api/http.js.map +1 -1
  6. package/out/cli/main.d.ts.map +1 -1
  7. package/out/cli/main.js +3 -0
  8. package/out/cli/main.js.map +1 -1
  9. package/out/extension/main.cjs +250 -250
  10. package/out/extension/main.cjs.map +2 -2
  11. package/out/language/agentlang-validator.d.ts.map +1 -1
  12. package/out/language/agentlang-validator.js +4 -0
  13. package/out/language/agentlang-validator.js.map +1 -1
  14. package/out/language/error-reporter.d.ts +53 -0
  15. package/out/language/error-reporter.d.ts.map +1 -0
  16. package/out/language/error-reporter.js +879 -0
  17. package/out/language/error-reporter.js.map +1 -0
  18. package/out/language/generated/ast.d.ts +66 -6
  19. package/out/language/generated/ast.d.ts.map +1 -1
  20. package/out/language/generated/ast.js +48 -0
  21. package/out/language/generated/ast.js.map +1 -1
  22. package/out/language/generated/grammar.d.ts.map +1 -1
  23. package/out/language/generated/grammar.js +320 -190
  24. package/out/language/generated/grammar.js.map +1 -1
  25. package/out/language/main.cjs +870 -694
  26. package/out/language/main.cjs.map +3 -3
  27. package/out/language/parser.d.ts +4 -2
  28. package/out/language/parser.d.ts.map +1 -1
  29. package/out/language/parser.js +31 -98
  30. package/out/language/parser.js.map +1 -1
  31. package/out/language/syntax.d.ts +2 -0
  32. package/out/language/syntax.d.ts.map +1 -1
  33. package/out/language/syntax.js +6 -0
  34. package/out/language/syntax.js.map +1 -1
  35. package/out/runtime/api.d.ts.map +1 -1
  36. package/out/runtime/api.js +22 -0
  37. package/out/runtime/api.js.map +1 -1
  38. package/out/runtime/defs.d.ts +1 -0
  39. package/out/runtime/defs.d.ts.map +1 -1
  40. package/out/runtime/defs.js +2 -1
  41. package/out/runtime/defs.js.map +1 -1
  42. package/out/runtime/document-retriever.d.ts +24 -0
  43. package/out/runtime/document-retriever.d.ts.map +1 -0
  44. package/out/runtime/document-retriever.js +258 -0
  45. package/out/runtime/document-retriever.js.map +1 -0
  46. package/out/runtime/embeddings/chunker.d.ts +18 -0
  47. package/out/runtime/embeddings/chunker.d.ts.map +1 -1
  48. package/out/runtime/embeddings/chunker.js +47 -15
  49. package/out/runtime/embeddings/chunker.js.map +1 -1
  50. package/out/runtime/embeddings/openai.d.ts.map +1 -1
  51. package/out/runtime/embeddings/openai.js +22 -9
  52. package/out/runtime/embeddings/openai.js.map +1 -1
  53. package/out/runtime/embeddings/provider.d.ts +1 -0
  54. package/out/runtime/embeddings/provider.d.ts.map +1 -1
  55. package/out/runtime/embeddings/provider.js +20 -1
  56. package/out/runtime/embeddings/provider.js.map +1 -1
  57. package/out/runtime/integration-client.d.ts +21 -0
  58. package/out/runtime/integration-client.d.ts.map +1 -0
  59. package/out/runtime/integration-client.js +112 -0
  60. package/out/runtime/integration-client.js.map +1 -0
  61. package/out/runtime/integrations.d.ts.map +1 -1
  62. package/out/runtime/integrations.js +20 -9
  63. package/out/runtime/integrations.js.map +1 -1
  64. package/out/runtime/interpreter.d.ts +1 -0
  65. package/out/runtime/interpreter.d.ts.map +1 -1
  66. package/out/runtime/interpreter.js +172 -19
  67. package/out/runtime/interpreter.js.map +1 -1
  68. package/out/runtime/loader.d.ts.map +1 -1
  69. package/out/runtime/loader.js +70 -7
  70. package/out/runtime/loader.js.map +1 -1
  71. package/out/runtime/logger.d.ts.map +1 -1
  72. package/out/runtime/logger.js +8 -1
  73. package/out/runtime/logger.js.map +1 -1
  74. package/out/runtime/module.d.ts +10 -0
  75. package/out/runtime/module.d.ts.map +1 -1
  76. package/out/runtime/module.js +68 -3
  77. package/out/runtime/module.js.map +1 -1
  78. package/out/runtime/modules/ai.d.ts +9 -2
  79. package/out/runtime/modules/ai.d.ts.map +1 -1
  80. package/out/runtime/modules/ai.js +219 -67
  81. package/out/runtime/modules/ai.js.map +1 -1
  82. package/out/runtime/modules/core.d.ts.map +1 -1
  83. package/out/runtime/modules/core.js +3 -0
  84. package/out/runtime/modules/core.js.map +1 -1
  85. package/out/runtime/modules/messaging.d.ts +10 -0
  86. package/out/runtime/modules/messaging.d.ts.map +1 -0
  87. package/out/runtime/modules/messaging.js +210 -0
  88. package/out/runtime/modules/messaging.js.map +1 -0
  89. package/out/runtime/resolvers/interface.d.ts +4 -0
  90. package/out/runtime/resolvers/interface.d.ts.map +1 -1
  91. package/out/runtime/resolvers/interface.js +14 -1
  92. package/out/runtime/resolvers/interface.js.map +1 -1
  93. package/out/runtime/resolvers/sqldb/database.d.ts +2 -0
  94. package/out/runtime/resolvers/sqldb/database.d.ts.map +1 -1
  95. package/out/runtime/resolvers/sqldb/database.js +142 -126
  96. package/out/runtime/resolvers/sqldb/database.js.map +1 -1
  97. package/out/runtime/resolvers/sqldb/dbutil.d.ts.map +1 -1
  98. package/out/runtime/resolvers/sqldb/dbutil.js +8 -0
  99. package/out/runtime/resolvers/sqldb/dbutil.js.map +1 -1
  100. package/out/runtime/resolvers/sqldb/impl.d.ts +1 -0
  101. package/out/runtime/resolvers/sqldb/impl.d.ts.map +1 -1
  102. package/out/runtime/resolvers/sqldb/impl.js +7 -0
  103. package/out/runtime/resolvers/sqldb/impl.js.map +1 -1
  104. package/out/runtime/resolvers/vector/lancedb-store.d.ts +16 -0
  105. package/out/runtime/resolvers/vector/lancedb-store.d.ts.map +1 -0
  106. package/out/runtime/resolvers/vector/lancedb-store.js +159 -0
  107. package/out/runtime/resolvers/vector/lancedb-store.js.map +1 -0
  108. package/out/runtime/resolvers/vector/types.d.ts +32 -0
  109. package/out/runtime/resolvers/vector/types.d.ts.map +1 -0
  110. package/out/runtime/resolvers/vector/types.js +2 -0
  111. package/out/runtime/resolvers/vector/types.js.map +1 -0
  112. package/out/runtime/services/documentFetcher.d.ts.map +1 -1
  113. package/out/runtime/services/documentFetcher.js +21 -6
  114. package/out/runtime/services/documentFetcher.js.map +1 -1
  115. package/out/runtime/state.d.ts +19 -1
  116. package/out/runtime/state.d.ts.map +1 -1
  117. package/out/runtime/state.js +36 -1
  118. package/out/runtime/state.js.map +1 -1
  119. package/out/syntaxes/agentlang.monarch.js +1 -1
  120. package/out/syntaxes/agentlang.monarch.js.map +1 -1
  121. package/package.json +19 -19
  122. package/src/api/http.ts +197 -37
  123. package/src/cli/main.ts +3 -0
  124. package/src/language/agentlang-validator.ts +3 -0
  125. package/src/language/agentlang.langium +3 -1
  126. package/src/language/error-reporter.ts +1028 -0
  127. package/src/language/generated/ast.ts +77 -5
  128. package/src/language/generated/grammar.ts +320 -190
  129. package/src/language/parser.ts +31 -100
  130. package/src/language/syntax.ts +8 -0
  131. package/src/runtime/api.ts +31 -0
  132. package/src/runtime/defs.ts +2 -1
  133. package/src/runtime/document-retriever.ts +311 -0
  134. package/src/runtime/embeddings/chunker.ts +52 -14
  135. package/src/runtime/embeddings/openai.ts +27 -9
  136. package/src/runtime/embeddings/provider.ts +22 -1
  137. package/src/runtime/integration-client.ts +158 -0
  138. package/src/runtime/integrations.ts +20 -11
  139. package/src/runtime/interpreter.ts +164 -14
  140. package/src/runtime/loader.ts +83 -5
  141. package/src/runtime/logger.ts +12 -1
  142. package/src/runtime/module.ts +78 -3
  143. package/src/runtime/modules/ai.ts +263 -76
  144. package/src/runtime/modules/core.ts +4 -0
  145. package/src/runtime/modules/messaging.ts +228 -0
  146. package/src/runtime/resolvers/interface.ts +19 -1
  147. package/src/runtime/resolvers/sqldb/database.ts +158 -130
  148. package/src/runtime/resolvers/sqldb/dbutil.ts +8 -0
  149. package/src/runtime/resolvers/sqldb/impl.ts +8 -0
  150. package/src/runtime/resolvers/vector/lancedb-store.ts +187 -0
  151. package/src/runtime/resolvers/vector/types.ts +39 -0
  152. package/src/runtime/services/documentFetcher.ts +21 -6
  153. package/src/runtime/state.ts +40 -1
  154. package/src/syntaxes/agentlang.monarch.ts +1 -1
@@ -0,0 +1,1028 @@
1
+ import { LangiumDocument } from 'langium';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Types
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export type ErrorCategory =
8
+ | 'SYNTAX ERROR'
9
+ | 'UNEXPECTED TOKEN'
10
+ | 'MISSING TOKEN'
11
+ | 'VALIDATION ERROR';
12
+
13
+ export interface ErrorRegion {
14
+ startLine: number; // 1-based
15
+ startCol: number; // 1-based
16
+ endLine: number; // 1-based
17
+ endCol: number; // 1-based
18
+ }
19
+
20
+ export interface AgentlangError {
21
+ category: ErrorCategory;
22
+ file: string;
23
+ region: ErrorRegion;
24
+ message: string;
25
+ hint?: string;
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Error collection – convert Langium/Chevrotain errors to AgentlangError[]
30
+ // ---------------------------------------------------------------------------
31
+
32
+ export function collectErrors(document: LangiumDocument): AgentlangError[] {
33
+ const errors: AgentlangError[] = [];
34
+ const uri = document.uri.toString();
35
+ const file = uri.replace(/^file:\/\/\//, '');
36
+
37
+ // Lexer errors
38
+ for (const err of document.parseResult.lexerErrors) {
39
+ const le = err as any;
40
+ const line = le.line ?? 1;
41
+ const col = le.column ?? 1;
42
+ const length = le.length ?? 1;
43
+ const { message: lexMsg, hint: lexHint } = humanizeLexerError(le.message);
44
+ errors.push({
45
+ category: 'UNEXPECTED TOKEN',
46
+ file,
47
+ region: {
48
+ startLine: line,
49
+ startCol: col,
50
+ endLine: line,
51
+ endCol: col + length - 1,
52
+ },
53
+ message: lexMsg,
54
+ hint: lexHint,
55
+ });
56
+ }
57
+
58
+ // Parser errors
59
+ const sourceLines = document.textDocument.getText().split('\n');
60
+ for (const err of document.parseResult.parserErrors) {
61
+ const pe = err as any;
62
+ const token = pe.token;
63
+ let startLine = token?.startLine;
64
+ let startCol = token?.startColumn;
65
+ let endLine = token?.endLine;
66
+ let endCol = token?.endColumn;
67
+
68
+ // When the error token is EOF, positions are NaN.
69
+ // Fall back to the previous token's end position, or the last line.
70
+ if (!startLine || isNaN(startLine)) {
71
+ const prev = pe.previousToken;
72
+ if (prev?.endLine && !isNaN(prev.endLine)) {
73
+ startLine = prev.endLine;
74
+ startCol = (prev.endColumn ?? 0) + 1;
75
+ endLine = startLine;
76
+ endCol = startCol;
77
+ } else {
78
+ // Last resort: point to end of source
79
+ startLine = sourceLines.length;
80
+ startCol = (sourceLines[sourceLines.length - 1]?.length ?? 0) + 1;
81
+ endLine = startLine;
82
+ endCol = startCol;
83
+ }
84
+ }
85
+
86
+ const source = document.textDocument.getText();
87
+ const { category, message, hint, regionOverride } = classifyParserError(pe, source);
88
+ errors.push({
89
+ category,
90
+ file,
91
+ region: regionOverride ?? {
92
+ startLine: startLine ?? 1,
93
+ startCol: startCol ?? 1,
94
+ endLine: endLine ?? startLine ?? 1,
95
+ endCol: endCol ?? startCol ?? 1,
96
+ },
97
+ message,
98
+ hint,
99
+ });
100
+ }
101
+
102
+ // Validation errors (severity 1 = error)
103
+ const validationErrors = (document.diagnostics ?? []).filter(e => e.severity === 1);
104
+ for (const ve of validationErrors) {
105
+ const text = document.textDocument.getText(ve.range);
106
+ errors.push({
107
+ category: 'VALIDATION ERROR',
108
+ file,
109
+ region: {
110
+ startLine: ve.range.start.line + 1,
111
+ startCol: ve.range.start.character + 1,
112
+ endLine: ve.range.end.line + 1,
113
+ endCol: ve.range.end.character + 1,
114
+ },
115
+ message: ve.message || `Unexpected token '${text}'.`,
116
+ });
117
+ }
118
+
119
+ return errors;
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Error classification – turn raw Chevrotain messages into categories + plain English
124
+ // ---------------------------------------------------------------------------
125
+
126
+ type ClassifiedError = {
127
+ category: ErrorCategory;
128
+ message: string;
129
+ hint?: string;
130
+ regionOverride?: ErrorRegion;
131
+ };
132
+
133
+ // Human-readable names for grammar rules found in Chevrotain's ruleStack
134
+ const RULE_NAMES: Record<string, string> = {
135
+ ModuleDefinition: 'module',
136
+ EntityDefinition: 'entity',
137
+ EventDefinition: 'event',
138
+ RecordDefinition: 'record',
139
+ WorkflowDefinition: 'workflow',
140
+ AgentDefinition: 'agent',
141
+ FlowDefinition: 'flow',
142
+ DecisionDefinition: 'decision',
143
+ RelationshipDefinition: 'relationship',
144
+ ResolverDefinition: 'resolver',
145
+ ScenarioDefinition: 'scenario',
146
+ DirectiveDefinition: 'directive',
147
+ RecordSchemaDefinition: 'schema body',
148
+ RecDef: 'definition',
149
+ CrudMap: 'data pattern',
150
+ CrudMapBody: 'data pattern body',
151
+ If: 'if expression',
152
+ ForEach: 'for-each loop',
153
+ Body: 'body',
154
+ QualifiedName: 'name',
155
+ AttributeDefinition: 'attribute',
156
+ GenericDefBody: 'definition body',
157
+ };
158
+
159
+ // The known decorator keywords in agentlang (used for suggestions in later phases)
160
+ export const KNOWN_DECORATORS = [
161
+ '@public',
162
+ '@id',
163
+ '@ref',
164
+ '@enum',
165
+ '@oneof',
166
+ '@expr',
167
+ '@optional',
168
+ '@unique',
169
+ '@default',
170
+ '@as',
171
+ '@catch',
172
+ '@empty',
173
+ '@then',
174
+ '@async',
175
+ '@after',
176
+ '@before',
177
+ '@when',
178
+ '@rbac',
179
+ '@meta',
180
+ '@with_unique',
181
+ '@actions',
182
+ '@where',
183
+ '@into',
184
+ '@join',
185
+ '@inner_join',
186
+ '@left_join',
187
+ '@right_join',
188
+ '@full_join',
189
+ '@groupBy',
190
+ '@orderBy',
191
+ '@limit',
192
+ '@offset',
193
+ '@upsert',
194
+ '@distinct',
195
+ '@withRole',
196
+ ];
197
+
198
+ function classifyParserError(err: any, source: string): ClassifiedError {
199
+ const raw: string = err.message ?? '';
200
+ const token = err.token;
201
+ const prevToken = err.previousToken;
202
+ // Langium appends U+200B (zero-width space) to rule names; strip it for matching
203
+ const ruleStack: string[] = (err.context?.ruleStack ?? []).map((s: string) =>
204
+ s.replace(/\u200B/g, '')
205
+ );
206
+ const found = token?.image ?? '';
207
+ const prevImage = prevToken?.image ?? '';
208
+ const isEOF = !found || found === '' || token?.tokenType?.name === 'EOF';
209
+ const errorName: string = err.name ?? '';
210
+
211
+ // -----------------------------------------------------------------------
212
+ // 1. NotAllInputParsedException – extra tokens after valid parse
213
+ // -----------------------------------------------------------------------
214
+ if (errorName === 'NotAllInputParsedException' || raw.includes('Expecting end of file')) {
215
+ if (found === '@' || found.startsWith('@')) {
216
+ const attempted = extractDecoratorAtPosition(source, token);
217
+ const decoratorHint = buildDecoratorSuggestion(attempted);
218
+ return {
219
+ category: 'SYNTAX ERROR',
220
+ message: attempted
221
+ ? `I don't recognize \`${attempted}\` as a valid decorator or keyword here.`
222
+ : `I don't recognize '${found}' as a valid decorator or keyword here.`,
223
+ hint: decoratorHint,
224
+ regionOverride: decoratorRegion(token, attempted),
225
+ };
226
+ }
227
+ return {
228
+ category: 'SYNTAX ERROR',
229
+ message: `There is unexpected input '${found}' after what I expected to be the end.`,
230
+ hint: `This usually means a closing brace '}' is missing earlier, or there are extra characters that don't belong.`,
231
+ };
232
+ }
233
+
234
+ // -----------------------------------------------------------------------
235
+ // 2. MismatchedTokenException – expected a specific token, got something else
236
+ // -----------------------------------------------------------------------
237
+ if (errorName === 'MismatchedTokenException' || raw.includes('Expecting token of type')) {
238
+ return classifyMismatch(raw, found, prevImage, ruleStack, isEOF, source, token);
239
+ }
240
+
241
+ // -----------------------------------------------------------------------
242
+ // 3. NoViableAltException – none of the grammar alternatives matched
243
+ // -----------------------------------------------------------------------
244
+ if (
245
+ errorName === 'NoViableAltException' ||
246
+ raw.includes('one of these possible Token sequences')
247
+ ) {
248
+ return classifyNoViableAlt(found, prevImage, ruleStack, isEOF, source, token);
249
+ }
250
+
251
+ // -----------------------------------------------------------------------
252
+ // 4. EarlyExitException – a required repetition had zero matches
253
+ // -----------------------------------------------------------------------
254
+ if (errorName === 'EarlyExitException' || raw.includes('EarlyExit')) {
255
+ const ctx = innermostContext(ruleStack);
256
+ return {
257
+ category: 'SYNTAX ERROR',
258
+ message: `I was expecting more input here${ctx ? ` while parsing ${ctx}` : ''}, but the definition ended too soon.`,
259
+ hint: `A required element may be missing. Check that all definitions are complete.`,
260
+ };
261
+ }
262
+
263
+ // -----------------------------------------------------------------------
264
+ // Fallback
265
+ // -----------------------------------------------------------------------
266
+ const fallbackCtx = innermostContext(ruleStack);
267
+ return {
268
+ category: 'SYNTAX ERROR',
269
+ message: humanizeFallback(raw),
270
+ hint: fallbackCtx
271
+ ? `This error occurred while parsing ${fallbackCtx}. Check for typos, missing punctuation, or incomplete definitions.`
272
+ : `Check for typos, missing commas, unclosed braces, or other syntax issues near this location.`,
273
+ };
274
+ }
275
+
276
+ // ---------------------------------------------------------------------------
277
+ // MismatchedTokenException handler
278
+ // ---------------------------------------------------------------------------
279
+
280
+ function classifyMismatch(
281
+ raw: string,
282
+ found: string,
283
+ prevImage: string,
284
+ ruleStack: string[],
285
+ isEOF: boolean,
286
+ source: string,
287
+ token: any
288
+ ): ClassifiedError {
289
+ // Extract the expected token from the raw message
290
+ const expectMatch = raw.match(/Expecting token of type '([^']+)'/);
291
+ const expectedToken = expectMatch ? expectMatch[1] : '';
292
+
293
+ // --- Missing closing brace at EOF ---
294
+ if (expectedToken === '}' && isEOF) {
295
+ const ctx = innermostDefinition(ruleStack);
296
+ if (ctx) {
297
+ return {
298
+ category: 'MISSING TOKEN',
299
+ message: `I was parsing ${aOrAn(ctx)} ${ctx} definition but never found a closing \`}\`.`,
300
+ hint: `Check that every opening \`{\` has a matching closing \`}\`.`,
301
+ };
302
+ }
303
+ return {
304
+ category: 'MISSING TOKEN',
305
+ message: `I reached the end of the file but was expecting a closing \`}\`.`,
306
+ hint: `Check that every opening \`{\` has a matching closing \`}\`.`,
307
+ };
308
+ }
309
+
310
+ // --- Missing closing paren at EOF ---
311
+ if (expectedToken === ')' && isEOF) {
312
+ return {
313
+ category: 'MISSING TOKEN',
314
+ message: `I reached the end of the file but was expecting a closing \`)\`.`,
315
+ hint: `Check that every opening \`(\` has a matching closing \`)\`.`,
316
+ };
317
+ }
318
+
319
+ // --- Semicolon instead of comma in entity attributes ---
320
+ if (expectedToken === '}' && found === ';' && ruleStack.includes('RecordSchemaDefinition')) {
321
+ return {
322
+ category: 'SYNTAX ERROR',
323
+ message: `I found a semicolon \`;\` but attributes should be separated by commas.`,
324
+ hint: `Use commas between attributes, not semicolons. Example:\n\n entity E {\n name String,\n age Int\n }`,
325
+ };
326
+ }
327
+
328
+ // --- Duplicate module declaration ---
329
+ if (expectedToken === 'EOF' && found === 'module') {
330
+ return {
331
+ category: 'SYNTAX ERROR',
332
+ message: `I found a second \`module\` declaration, but only one is allowed per file.`,
333
+ hint: `Each file should contain exactly one \`module\` declaration at the top.`,
334
+ };
335
+ }
336
+
337
+ // --- Expected '}' but found a decorator like '@ref' ---
338
+ if (expectedToken === '}' && found.startsWith('@')) {
339
+ const ctx = innermostDefinition(ruleStack);
340
+ if (ctx) {
341
+ return {
342
+ category: 'SYNTAX ERROR',
343
+ message: `I was expecting \`}\` to close the ${ctx}, but found \`${found}\`.`,
344
+ hint: `'${found}' may need a preceding comma, or you may be missing a closing \`}\` before it.`,
345
+ };
346
+ }
347
+ }
348
+
349
+ // --- Expected EOF but found '@' (bad decorator) ---
350
+ if (expectedToken === 'EOF' && (found === '@' || found.startsWith('@'))) {
351
+ const attempted = extractDecoratorAtPosition(source, token);
352
+ const decoratorHint = buildDecoratorSuggestion(attempted);
353
+ return {
354
+ category: 'SYNTAX ERROR',
355
+ message: attempted
356
+ ? `I don't recognize \`${attempted}\` as a valid decorator or keyword here.`
357
+ : `I don't recognize '${found}' as a valid decorator or keyword here.`,
358
+ hint: decoratorHint,
359
+ regionOverride: decoratorRegion(token, attempted),
360
+ };
361
+ }
362
+
363
+ // --- Generic mismatch at EOF ---
364
+ if (isEOF) {
365
+ const ctx = innermostContext(ruleStack);
366
+ return {
367
+ category: 'MISSING TOKEN',
368
+ message: `I reached the end of the file${ctx ? ` while parsing ${ctx}` : ''}, but I was expecting \`${expectedToken}\`.`,
369
+ hint: `The definition may be incomplete. Check that nothing is missing at the end.`,
370
+ };
371
+ }
372
+
373
+ // --- Generic mismatch ---
374
+ const humanExpected = humanizeTokenName(expectedToken);
375
+ const mismatchCtx = innermostContext(ruleStack);
376
+ const defType = innermostDefinition(ruleStack);
377
+ let mismatchHint: string;
378
+ if (defType && expectedToken === '{') {
379
+ mismatchHint = `${capitalize(defType)} definitions need braces around their body. Example:\n\n ${defType} MyName {\n ...\n }`;
380
+ } else if (defType) {
381
+ mismatchHint = `Check the syntax of your ${defType} definition. There may be a typo, a missing comma, or an extra token before \`${found}\`.`;
382
+ } else if (mismatchCtx) {
383
+ mismatchHint = `This error occurred while parsing ${mismatchCtx}. Check for typos or missing punctuation near \`${found}\`.`;
384
+ } else {
385
+ mismatchHint = `There may be a typo or missing punctuation near \`${found}\`.`;
386
+ }
387
+ return {
388
+ category: 'SYNTAX ERROR',
389
+ message: `I was expecting ${humanExpected} but found \`${found}\`.`,
390
+ hint: mismatchHint,
391
+ };
392
+ }
393
+
394
+ // ---------------------------------------------------------------------------
395
+ // NoViableAltException handler
396
+ // ---------------------------------------------------------------------------
397
+
398
+ function classifyNoViableAlt(
399
+ found: string,
400
+ prevImage: string,
401
+ ruleStack: string[],
402
+ isEOF: boolean,
403
+ _source: string,
404
+ _token: any
405
+ ): ClassifiedError {
406
+ // --- Missing module name (after 'module' keyword) ---
407
+ if (ruleStack.includes('QualifiedName') && prevImage === 'module') {
408
+ if (isEOF) {
409
+ return {
410
+ category: 'MISSING TOKEN',
411
+ message: `I was expecting a module name after \`module\`, but reached the end of the file.`,
412
+ hint: `Every module needs a name, like:\n\n module MyApp\n module acme.core`,
413
+ };
414
+ }
415
+ return {
416
+ category: 'SYNTAX ERROR',
417
+ message: `I was expecting a module name after \`module\`, but found \`${found}\`.`,
418
+ hint: `Module names must start with a letter. Example:\n\n module MyApp\n module acme.core`,
419
+ };
420
+ }
421
+
422
+ // --- Missing entity/event/record name ---
423
+ if (ruleStack.includes('QualifiedName') && ruleStack.includes('RecDef')) {
424
+ const defType = ruleStack.includes('EntityDefinition')
425
+ ? 'entity'
426
+ : ruleStack.includes('EventDefinition')
427
+ ? 'event'
428
+ : ruleStack.includes('RecordDefinition')
429
+ ? 'record'
430
+ : 'definition';
431
+ if (isEOF) {
432
+ return {
433
+ category: 'MISSING TOKEN',
434
+ message: `I was expecting ${aOrAn(defType)} ${defType} name after \`${prevImage}\`, but reached the end of the file.`,
435
+ hint: `Every ${defType} needs a name and a body. Example:\n\n ${defType} My${capitalize(defType)} {\n name String\n }`,
436
+ };
437
+ }
438
+ if (RESERVED_KEYWORDS.has(found)) {
439
+ return {
440
+ category: 'SYNTAX ERROR',
441
+ message: `\`${found}\` is a reserved keyword and cannot be used as ${aOrAn(defType)} ${defType} name.`,
442
+ hint: `Choose a different name, for example:\n\n ${defType} My${capitalize(found)} { ... }`,
443
+ };
444
+ }
445
+ return {
446
+ category: 'SYNTAX ERROR',
447
+ message: `I was expecting ${aOrAn(defType)} ${defType} name after \`${prevImage}\`, but found \`${found}\`.`,
448
+ hint: `Names must start with a letter. Example:\n\n ${defType} My${capitalize(defType)} { ... }`,
449
+ };
450
+ }
451
+
452
+ // --- Missing type on attribute (found comma or '}' when parsing AttributeDefinition) ---
453
+ if (
454
+ (found === ',' || found === '}') &&
455
+ ruleStack.includes('AttributeDefinition') &&
456
+ ruleStack.includes('RecordSchemaDefinition')
457
+ ) {
458
+ return {
459
+ category: 'SYNTAX ERROR',
460
+ message: `The attribute \`${prevImage}\` is missing a type.`,
461
+ hint: `Each attribute needs a name followed by a type. Example:\n\n name String,\n age Int`,
462
+ };
463
+ }
464
+
465
+ // --- Colon after attribute name (coming from another language) ---
466
+ if (
467
+ found === ':' &&
468
+ ruleStack.includes('AttributeDefinition') &&
469
+ ruleStack.includes('RecordSchemaDefinition')
470
+ ) {
471
+ return {
472
+ category: 'SYNTAX ERROR',
473
+ message: `Attributes don't use colons between the name and type.`,
474
+ hint: `Write the type directly after the name, without a colon:\n\n name String (not name: String)`,
475
+ };
476
+ }
477
+
478
+ // --- Equals in attribute definition (coming from another language) ---
479
+ if (
480
+ found === '=' &&
481
+ ruleStack.includes('AttributeDefinition') &&
482
+ ruleStack.includes('RecordSchemaDefinition')
483
+ ) {
484
+ return {
485
+ category: 'SYNTAX ERROR',
486
+ message: `Attributes don't use \`=\` for assignment.`,
487
+ hint: `To declare an attribute, write: \`name String\`\nTo set a default value, use: \`name String @default("value")\``,
488
+ };
489
+ }
490
+
491
+ // --- Missing opening brace for entity/record/event body ---
492
+ if (
493
+ ruleStack.includes('RecordSchemaDefinition') &&
494
+ !ruleStack.includes('AttributeDefinition') &&
495
+ !isEOF &&
496
+ found !== ',' &&
497
+ found !== '}'
498
+ ) {
499
+ const defType = innermostDefinition(ruleStack) ?? 'definition';
500
+ return {
501
+ category: 'SYNTAX ERROR',
502
+ message: `I was expecting \`{\` to start the ${defType} body, but found \`${found}\`.`,
503
+ hint: `${capitalize(defType)} definitions need braces around their attributes. Example:\n\n ${defType} MyName {\n name String\n }`,
504
+ };
505
+ }
506
+
507
+ // --- Trailing comma (found '}' in RecordExtraDefinition context) ---
508
+ if (found === '}' && ruleStack.includes('RecordExtraDefinition')) {
509
+ return {
510
+ category: 'SYNTAX ERROR',
511
+ message: `There is a trailing comma before the closing \`}\`.`,
512
+ hint: `Remove the comma after the last attribute:\n\n entity E {\n name String,\n age Int\n }`,
513
+ };
514
+ }
515
+
516
+ // --- Double comma / unexpected comma in entity attributes ---
517
+ if (found === ',' && ruleStack.includes('RecordExtraDefinition')) {
518
+ return {
519
+ category: 'SYNTAX ERROR',
520
+ message: `I found an unexpected comma here.`,
521
+ hint: `It looks like there may be a double comma in the attribute list, or an attribute is missing between two commas.`,
522
+ };
523
+ }
524
+
525
+ // --- Unexpected token in entity attribute list (cascading from double comma) ---
526
+ if (ruleStack.includes('RecordExtraDefinition') && !isEOF) {
527
+ return {
528
+ category: 'SYNTAX ERROR',
529
+ message: `I found unexpected \`${found}\` in the attribute list.`,
530
+ hint: `Check for double commas or missing attribute definitions. Each attribute needs a name and a type:\n\n name String,\n age Int`,
531
+ };
532
+ }
533
+
534
+ // --- Workflow body parse failure ---
535
+ if (ruleStack.includes('WorkflowDefinition') && ruleStack.includes('Body')) {
536
+ return {
537
+ category: 'SYNTAX ERROR',
538
+ message: `I had trouble parsing the workflow body starting at \`${found}\`.`,
539
+ hint: `Check for missing closing parentheses \`)\`, unclosed braces \`}\`, or incorrect statement syntax inside the workflow.`,
540
+ };
541
+ }
542
+
543
+ // --- Reserved keyword used as workflow/agent/flow/decision name ---
544
+ if (!isEOF && RESERVED_KEYWORDS.has(found)) {
545
+ const nameDefRules: [string, string][] = [
546
+ ['WorkflowDefinition', 'workflow'],
547
+ ['AgentDefinition', 'agent'],
548
+ ['FlowDefinition', 'flow'],
549
+ ['DecisionDefinition', 'decision'],
550
+ ];
551
+ for (const [rule, defType] of nameDefRules) {
552
+ if (
553
+ ruleStack.includes(rule) &&
554
+ !ruleStack.includes('Body') &&
555
+ !ruleStack.includes('GenericDefBody') &&
556
+ !ruleStack.includes('FlowDefBody') &&
557
+ !ruleStack.includes('DecisionDefBody')
558
+ ) {
559
+ return {
560
+ category: 'SYNTAX ERROR',
561
+ message: `\`${found}\` is a reserved keyword and cannot be used as ${aOrAn(defType)} ${defType} name.`,
562
+ hint: `Choose a different name, for example:\n\n ${defType} My${capitalize(found)} { ... }`,
563
+ };
564
+ }
565
+ }
566
+ }
567
+
568
+ // --- Definition-level failure (can't match any definition type) ---
569
+ if (
570
+ ruleStack.length >= 2 &&
571
+ ruleStack[ruleStack.length - 1] === 'Definition' &&
572
+ ruleStack[ruleStack.length - 2] === 'ModuleDefinition'
573
+ ) {
574
+ const keywordSuggestions = suggest(found, KNOWN_KEYWORDS);
575
+ const suggestionText = formatSuggestions(keywordSuggestions);
576
+ return {
577
+ category: 'SYNTAX ERROR',
578
+ message: `I don't recognize what kind of definition starts with \`${found}\`.`,
579
+ hint: suggestionText
580
+ ? suggestionText
581
+ : `Definitions must start with a keyword like: entity, event, record, workflow, agent, flow, relationship, or a decorator like @public.`,
582
+ };
583
+ }
584
+
585
+ // --- Generic NoViableAlt at EOF ---
586
+ if (isEOF) {
587
+ const ctx = innermostContext(ruleStack);
588
+ return {
589
+ category: 'MISSING TOKEN',
590
+ message: `I reached the end of the file${ctx ? ` while parsing ${ctx}` : ''}, but I was expecting more input.`,
591
+ hint: `The definition may be incomplete. Check that nothing is missing at the end.`,
592
+ };
593
+ }
594
+
595
+ // --- Generic NoViableAlt ---
596
+ const ctx = innermostContext(ruleStack);
597
+ if (!isEOF && RESERVED_KEYWORDS.has(found)) {
598
+ return {
599
+ category: 'SYNTAX ERROR',
600
+ message: `I got stuck on \`${found}\`${ctx ? ` while parsing ${ctx}` : ''}.`,
601
+ hint: `\`${found}\` is a reserved keyword and cannot be used as a name. Choose a different name.`,
602
+ };
603
+ }
604
+ return {
605
+ category: 'SYNTAX ERROR',
606
+ message: `I got stuck on \`${found}\`${ctx ? ` while parsing ${ctx}` : ''}.`,
607
+ hint: `Check for typos, missing commas, or incorrect punctuation around this location.`,
608
+ };
609
+ }
610
+
611
+ // ---------------------------------------------------------------------------
612
+ // Helpers for context extraction from ruleStack
613
+ // ---------------------------------------------------------------------------
614
+
615
+ /** Find the innermost "definition" rule (entity, workflow, agent, etc.) */
616
+ function innermostDefinition(ruleStack: string[]): string | undefined {
617
+ for (let i = ruleStack.length - 1; i >= 0; i--) {
618
+ const name = RULE_NAMES[ruleStack[i]];
619
+ if (
620
+ name &&
621
+ name !== 'name' &&
622
+ name !== 'body' &&
623
+ name !== 'schema body' &&
624
+ name !== 'definition' &&
625
+ name !== 'attribute'
626
+ ) {
627
+ return name;
628
+ }
629
+ }
630
+ return undefined;
631
+ }
632
+
633
+ /** Get a human-readable description of the innermost parsing context */
634
+ function innermostContext(ruleStack: string[]): string | undefined {
635
+ for (let i = ruleStack.length - 1; i >= 0; i--) {
636
+ const name = RULE_NAMES[ruleStack[i]];
637
+ if (name && name !== 'name' && name !== 'definition') {
638
+ return aOrAn(name) + ' ' + name;
639
+ }
640
+ }
641
+ return undefined;
642
+ }
643
+
644
+ function aOrAn(word: string): string {
645
+ return /^[aeiou]/i.test(word) ? 'an' : 'a';
646
+ }
647
+
648
+ function capitalize(s: string): string {
649
+ return s.charAt(0).toUpperCase() + s.slice(1);
650
+ }
651
+
652
+ // ---------------------------------------------------------------------------
653
+ // Decorator suggestion helpers
654
+ // ---------------------------------------------------------------------------
655
+
656
+ /**
657
+ * When the lexer produces a bare `@` token (because `@pubic` isn't a known
658
+ * decorator keyword), look at the source text starting at the `@` to extract
659
+ * the full attempted decorator string like `@pubic`.
660
+ */
661
+ function extractDecoratorAtPosition(source: string, token: any): string | undefined {
662
+ const line = token?.startLine;
663
+ const col = token?.startColumn;
664
+ if (!line || !col || isNaN(line) || isNaN(col)) return undefined;
665
+
666
+ const lines = source.split('\n');
667
+ const lineText = lines[line - 1];
668
+ if (!lineText) return undefined;
669
+
670
+ // Starting from the @ position, grab the @ and the word that follows it
671
+ const rest = lineText.substring(col - 1); // col is 1-based
672
+ const match = rest.match(/^@[a-zA-Z_]\w*/);
673
+ return match ? match[0] : undefined;
674
+ }
675
+
676
+ /**
677
+ * Expand the error region to cover the full `@xxx` text instead of just `@`.
678
+ */
679
+ function decoratorRegion(token: any, attempted: string | undefined): ErrorRegion | undefined {
680
+ if (!attempted || !token?.startLine || isNaN(token.startLine)) return undefined;
681
+ return {
682
+ startLine: token.startLine,
683
+ startCol: token.startColumn,
684
+ endLine: token.startLine,
685
+ endCol: token.startColumn + attempted.length - 1,
686
+ };
687
+ }
688
+
689
+ /**
690
+ * Build a hint string for an unrecognized decorator, including
691
+ * "Did you mean?" suggestions when a close match exists.
692
+ */
693
+ function buildDecoratorSuggestion(attempted: string | undefined): string {
694
+ if (attempted) {
695
+ const suggestions = suggest(attempted, KNOWN_DECORATORS);
696
+ const suggestionText = formatSuggestions(suggestions);
697
+ if (suggestionText) {
698
+ return suggestionText;
699
+ }
700
+ }
701
+ return `Valid decorators include: @public, @id, @ref, @optional, @expr, @as, @catch, and others.\nMake sure the decorator is spelled correctly.`;
702
+ }
703
+
704
+ // ---------------------------------------------------------------------------
705
+ // Lexer error humanizer
706
+ // ---------------------------------------------------------------------------
707
+
708
+ function humanizeLexerError(raw: string): { message: string; hint?: string } {
709
+ const charMatch = raw.match(/unexpected character:\s*(.+?)\s*at offset/i);
710
+ if (charMatch) {
711
+ const ch = charMatch[1].trim();
712
+ // Chevrotain wraps chars in arrows: ->"<- — extract the inner character
713
+ const innerMatch = ch.match(/^->(.+)<-$/);
714
+ const actualChar = innerMatch ? innerMatch[1] : ch;
715
+ // Unclosed string literal
716
+ if (actualChar === '"' || actualChar === '`') {
717
+ const quote = actualChar === '"' ? 'double' : 'backtick';
718
+ return {
719
+ message: `This string literal is missing its closing ${quote} quote \`${actualChar}\`.`,
720
+ hint: `Add the matching closing \`${actualChar}\` at the end of the string. Example:\n\n name ${actualChar}hello world${actualChar}`,
721
+ };
722
+ }
723
+ return {
724
+ message: `I found an unexpected character \`${actualChar}\` that I don't recognize.`,
725
+ hint: `This character isn't valid in agentlang. Check for accidental keystrokes or characters copied from another source.`,
726
+ };
727
+ }
728
+ return { message: raw };
729
+ }
730
+
731
+ // ---------------------------------------------------------------------------
732
+ // Token name humanizer
733
+ // ---------------------------------------------------------------------------
734
+
735
+ function humanizeTokenName(token: string): string {
736
+ const map: Record<string, string> = {
737
+ ID: 'a name',
738
+ NAME: 'a name',
739
+ INT: 'a number',
740
+ STRING: 'a string',
741
+ QUOTED_STRING: 'a quoted string',
742
+ TICK_QUOTED_STRING: 'a backtick-quoted string',
743
+ EOF: 'the end of the file',
744
+ WS: 'whitespace',
745
+ };
746
+ // Punctuation tokens — show them as literals
747
+ if (token.length <= 3 && /^[^a-zA-Z]/.test(token)) {
748
+ return `\`${token}\``;
749
+ }
750
+ return map[token] ?? `\`${token}\``;
751
+ }
752
+
753
+ // ---------------------------------------------------------------------------
754
+ // Fallback humanizer
755
+ // ---------------------------------------------------------------------------
756
+
757
+ function humanizeFallback(raw: string): string {
758
+ const msg = raw.replace(/^\w+Exception:\s*/i, '');
759
+ const expMatch = msg.match(/Expecting[:\s]+(.+?),?\s*but found[:\s]+'([^']*)'/i);
760
+ if (expMatch) {
761
+ return `I was expecting ${expMatch[1].trim().toLowerCase()}, but found \`${expMatch[2]}\`.`;
762
+ }
763
+ return msg || 'I encountered an unexpected error while parsing.';
764
+ }
765
+
766
+ // ---------------------------------------------------------------------------
767
+ // Edit distance & suggestions ("Did you mean?")
768
+ // ---------------------------------------------------------------------------
769
+
770
+ /**
771
+ * Restricted Damerau-Levenshtein distance between two strings.
772
+ * Handles insertions, deletions, substitutions, and adjacent transpositions.
773
+ * Comparison is case-insensitive.
774
+ */
775
+ export function editDistance(a: string, b: string): number {
776
+ const s = a.toLowerCase();
777
+ const t = b.toLowerCase();
778
+ const sLen = s.length;
779
+ const tLen = t.length;
780
+
781
+ // Quick exits
782
+ if (s === t) return 0;
783
+ if (sLen === 0) return tLen;
784
+ if (tLen === 0) return sLen;
785
+
786
+ // Full matrix for restricted Damerau-Levenshtein
787
+ const d: number[][] = Array.from({ length: sLen + 1 }, () => new Array(tLen + 1).fill(0));
788
+
789
+ for (let i = 0; i <= sLen; i++) d[i][0] = i;
790
+ for (let j = 0; j <= tLen; j++) d[0][j] = j;
791
+
792
+ for (let i = 1; i <= sLen; i++) {
793
+ for (let j = 1; j <= tLen; j++) {
794
+ const cost = s[i - 1] === t[j - 1] ? 0 : 1;
795
+ d[i][j] = Math.min(
796
+ d[i - 1][j] + 1, // deletion
797
+ d[i][j - 1] + 1, // insertion
798
+ d[i - 1][j - 1] + cost // substitution
799
+ );
800
+ // Transposition
801
+ if (i > 1 && j > 1 && s[i - 1] === t[j - 2] && s[i - 2] === t[j - 1]) {
802
+ d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost);
803
+ }
804
+ }
805
+ }
806
+ return d[sLen][tLen];
807
+ }
808
+
809
+ /**
810
+ * Find the closest matches to `input` from a list of `candidates`.
811
+ * Returns up to `maxResults` candidates sorted by ascending distance.
812
+ *
813
+ * The effective max distance scales with input length to avoid noisy
814
+ * suggestions on short inputs:
815
+ * length 1-3 → max 1, length 4-6 → max 2, length 7+ → max 3
816
+ */
817
+ export function suggest(
818
+ input: string,
819
+ candidates: string[],
820
+ maxDistance?: number,
821
+ maxResults: number = 4
822
+ ): string[] {
823
+ if (!input) return [];
824
+ const effectiveMax = maxDistance ?? (input.length <= 3 ? 1 : input.length <= 6 ? 2 : 3);
825
+ const scored = candidates
826
+ .map(c => ({ candidate: c, distance: editDistance(input, c) }))
827
+ .filter(({ distance }) => distance > 0 && distance <= effectiveMax)
828
+ .sort((a, b) => a.distance - b.distance);
829
+ return scored.slice(0, maxResults).map(s => s.candidate);
830
+ }
831
+
832
+ /**
833
+ * Format suggestion list into a hint string:
834
+ * - 0 matches → undefined
835
+ * - 1 match → "Did you mean `X`?"
836
+ * - 2+ matches → "These names seem close:\n X\n Y"
837
+ */
838
+ export function formatSuggestions(suggestions: string[]): string | undefined {
839
+ if (suggestions.length === 0) return undefined;
840
+ if (suggestions.length === 1) return `Did you mean \`${suggestions[0]}\`?`;
841
+ const list = suggestions.map(s => ` ${s}`).join('\n');
842
+ return `These names seem close though:\n\n${list}`;
843
+ }
844
+
845
+ // ---------------------------------------------------------------------------
846
+ // Known keywords for suggestion matching
847
+ // ---------------------------------------------------------------------------
848
+
849
+ /** Top-level definition keywords in agentlang */
850
+ export const KNOWN_KEYWORDS = [
851
+ 'entity',
852
+ 'event',
853
+ 'record',
854
+ 'workflow',
855
+ 'agent',
856
+ 'flow',
857
+ 'relationship',
858
+ 'resolver',
859
+ 'scenario',
860
+ 'directive',
861
+ 'glossaryEntry',
862
+ 'eval',
863
+ 'decision',
864
+ 'import',
865
+ 'module',
866
+ ];
867
+
868
+ /**
869
+ * All grammar keywords that conflict with identifier (ID) positions.
870
+ * When a user writes e.g. `entity query { ... }`, the lexer tokenizes `query`
871
+ * as a keyword token instead of ID, causing a confusing parse error.
872
+ * This set lets us detect that case and produce a clear message.
873
+ */
874
+ export const RESERVED_KEYWORDS = new Set([
875
+ // Top-level definition keywords
876
+ ...KNOWN_KEYWORDS,
877
+ // Control flow
878
+ 'if',
879
+ 'else',
880
+ 'for',
881
+ 'in',
882
+ 'return',
883
+ 'throw',
884
+ 'await',
885
+ // CRUD / resolver operations
886
+ 'create',
887
+ 'update',
888
+ 'delete',
889
+ 'read',
890
+ 'query',
891
+ 'purge',
892
+ 'upsert',
893
+ // Logical / boolean
894
+ 'true',
895
+ 'false',
896
+ 'not',
897
+ 'or',
898
+ 'and',
899
+ // Schema / relationship
900
+ 'extends',
901
+ 'contains',
902
+ 'between',
903
+ // Decision / case
904
+ 'case',
905
+ // RBAC
906
+ 'roles',
907
+ 'allow',
908
+ 'where',
909
+ // SQL-like
910
+ 'like',
911
+ // Misc
912
+ 'backoff',
913
+ 'attempts',
914
+ 'subscribe',
915
+ ]);
916
+
917
+ // ---------------------------------------------------------------------------
918
+ // Snippet renderer – show source context with ^^^ underlines
919
+ // ---------------------------------------------------------------------------
920
+
921
+ export function renderSnippet(
922
+ source: string,
923
+ region: ErrorRegion,
924
+ contextLines: number = 2
925
+ ): string {
926
+ const lines = source.split('\n');
927
+ const totalLines = lines.length;
928
+
929
+ // Compute visible line range (1-based internally, but array is 0-based)
930
+ const firstVisible = Math.max(1, region.startLine - contextLines);
931
+ const lastVisible = Math.min(totalLines, region.endLine + contextLines);
932
+
933
+ // Width of the widest line number for alignment
934
+ const gutterWidth = String(lastVisible).length;
935
+
936
+ const output: string[] = [];
937
+
938
+ for (let lineNum = firstVisible; lineNum <= lastVisible; lineNum++) {
939
+ const lineText = lines[lineNum - 1] ?? '';
940
+ const numStr = String(lineNum).padStart(gutterWidth);
941
+ const isErrorLine = lineNum >= region.startLine && lineNum <= region.endLine;
942
+ const prefix = isErrorLine ? `${numStr} | ` : `${numStr} | `;
943
+
944
+ output.push(`${prefix}${lineText}`);
945
+
946
+ // Add underline on error lines
947
+ if (isErrorLine) {
948
+ const underlineStart = lineNum === region.startLine ? region.startCol : 1;
949
+ const underlineEnd = lineNum === region.endLine ? region.endCol : lineText.length;
950
+ const caretCount = Math.max(1, underlineEnd - underlineStart + 1);
951
+ const padding = ' '.repeat(gutterWidth) + ' ' + ' '.repeat(underlineStart - 1);
952
+ output.push(`${padding}${'~'.repeat(caretCount)}`);
953
+ }
954
+ }
955
+
956
+ return output.join('\n');
957
+ }
958
+
959
+ // ---------------------------------------------------------------------------
960
+ // Error formatter
961
+ // ---------------------------------------------------------------------------
962
+
963
+ const SEPARATOR_WIDTH = 60;
964
+
965
+ function header(category: ErrorCategory, file: string): string {
966
+ const left = `-- ${category} `;
967
+ const right = ` ${file}`;
968
+ const dashCount = Math.max(3, SEPARATOR_WIDTH - left.length - right.length);
969
+ return `${left}${'-'.repeat(dashCount)}${right}`;
970
+ }
971
+
972
+ export function formatError(err: AgentlangError, source: string): string {
973
+ const parts: string[] = [];
974
+
975
+ // 1. Header
976
+ parts.push(header(err.category, err.file));
977
+ parts.push('');
978
+
979
+ // 2. Message
980
+ parts.push(err.message);
981
+ parts.push('');
982
+
983
+ // 3. Source snippet
984
+ parts.push(renderSnippet(source, err.region));
985
+
986
+ // 4. Hint
987
+ if (err.hint) {
988
+ parts.push('');
989
+ parts.push(`Hint: ${err.hint}`);
990
+ }
991
+
992
+ return parts.join('\n');
993
+ }
994
+
995
+ export function formatErrors(errors: AgentlangError[], source: string): string {
996
+ if (errors.length === 0) return '';
997
+ // Show only the first error to avoid cascading noise
998
+ // but include up to 3 for genuinely independent errors.
999
+ const toShow = deduplicateErrors(errors).slice(0, 3);
1000
+ return toShow.map(err => formatError(err, source)).join('\n\n');
1001
+ }
1002
+
1003
+ // ---------------------------------------------------------------------------
1004
+ // Deduplication – avoid cascading / duplicate errors on the same line
1005
+ // ---------------------------------------------------------------------------
1006
+
1007
+ function deduplicateErrors(errors: AgentlangError[]): AgentlangError[] {
1008
+ const seen = new Set<number>();
1009
+ const result: AgentlangError[] = [];
1010
+ for (const err of errors) {
1011
+ if (!seen.has(err.region.startLine)) {
1012
+ seen.add(err.region.startLine);
1013
+ result.push(err);
1014
+ }
1015
+ }
1016
+ return result;
1017
+ }
1018
+
1019
+ // ---------------------------------------------------------------------------
1020
+ // Main entry point – collect + format from a LangiumDocument
1021
+ // ---------------------------------------------------------------------------
1022
+
1023
+ export function getFormattedErrors(document: LangiumDocument): string | undefined {
1024
+ const errors = collectErrors(document);
1025
+ if (errors.length === 0) return undefined;
1026
+ const source = document.textDocument.getText();
1027
+ return formatErrors(errors, source);
1028
+ }