@lokascript/framework 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/dist/aot/index.d.ts +1 -0
  2. package/dist/aot/index.d.ts.map +1 -1
  3. package/dist/aot/registry-bridge.d.ts +27 -0
  4. package/dist/aot/registry-bridge.d.ts.map +1 -0
  5. package/dist/api/create-dsl.d.ts.map +1 -1
  6. package/dist/api/dispatcher.d.ts +22 -0
  7. package/dist/api/dispatcher.d.ts.map +1 -1
  8. package/dist/api/domain-registry.d.ts +41 -0
  9. package/dist/api/domain-registry.d.ts.map +1 -1
  10. package/dist/api/index.js +1438 -21
  11. package/dist/api/index.js.map +1 -1
  12. package/dist/core/index.js +247 -10
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/pattern-matching/index.js +74 -10
  15. package/dist/core/pattern-matching/index.js.map +1 -1
  16. package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
  17. package/dist/core/tokenization/index.js +133 -0
  18. package/dist/core/tokenization/index.js.map +1 -1
  19. package/dist/core/tokenization/morphology/base-normalizer.d.ts +94 -0
  20. package/dist/core/tokenization/morphology/base-normalizer.d.ts.map +1 -0
  21. package/dist/core/tokenization/morphology/index.d.ts +3 -1
  22. package/dist/core/tokenization/morphology/index.d.ts.map +1 -1
  23. package/dist/core/types.d.ts +81 -2
  24. package/dist/core/types.d.ts.map +1 -1
  25. package/dist/core/types.js +40 -0
  26. package/dist/core/types.js.map +1 -1
  27. package/dist/feedback/confidence-gate.d.ts +28 -0
  28. package/dist/feedback/confidence-gate.d.ts.map +1 -0
  29. package/dist/feedback/feedback-formatter.d.ts +21 -0
  30. package/dist/feedback/feedback-formatter.d.ts.map +1 -0
  31. package/dist/feedback/index.d.ts +11 -0
  32. package/dist/feedback/index.d.ts.map +1 -0
  33. package/dist/feedback/pattern-tracker.d.ts +47 -0
  34. package/dist/feedback/pattern-tracker.d.ts.map +1 -0
  35. package/dist/feedback/types.d.ts +113 -0
  36. package/dist/feedback/types.d.ts.map +1 -0
  37. package/dist/generation/index.js +2 -1
  38. package/dist/generation/index.js.map +1 -1
  39. package/dist/index.cjs +3264 -132
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.ts +8 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +3223 -132
  44. package/dist/index.js.map +1 -1
  45. package/dist/ir/explicit-parser.d.ts +86 -0
  46. package/dist/ir/explicit-parser.d.ts.map +1 -0
  47. package/dist/ir/explicit-renderer.d.ts +34 -0
  48. package/dist/ir/explicit-renderer.d.ts.map +1 -0
  49. package/dist/ir/from-interchange.d.ts +51 -0
  50. package/dist/ir/from-interchange.d.ts.map +1 -0
  51. package/dist/ir/index.d.ts +31 -0
  52. package/dist/ir/index.d.ts.map +1 -0
  53. package/dist/ir/index.js +1934 -0
  54. package/dist/ir/index.js.map +1 -0
  55. package/dist/ir/json-schema.d.ts +40 -0
  56. package/dist/ir/json-schema.d.ts.map +1 -0
  57. package/dist/ir/protocol-json.d.ts +73 -0
  58. package/dist/ir/protocol-json.d.ts.map +1 -0
  59. package/dist/ir/references.d.ts +19 -0
  60. package/dist/ir/references.d.ts.map +1 -0
  61. package/dist/ir/to-runtime-ast.d.ts +53 -0
  62. package/dist/ir/to-runtime-ast.d.ts.map +1 -0
  63. package/dist/ir/types.d.ts +151 -0
  64. package/dist/ir/types.d.ts.map +1 -0
  65. package/dist/parsing/index.js +74 -10
  66. package/dist/parsing/index.js.map +1 -1
  67. package/dist/prompts/index.d.ts +9 -0
  68. package/dist/prompts/index.d.ts.map +1 -0
  69. package/dist/prompts/prompt-generator.d.ts +49 -0
  70. package/dist/prompts/prompt-generator.d.ts.map +1 -0
  71. package/dist/prompts/types.d.ts +64 -0
  72. package/dist/prompts/types.d.ts.map +1 -0
  73. package/dist/schema/command-schema.d.ts +17 -0
  74. package/dist/schema/command-schema.d.ts.map +1 -1
  75. package/dist/schema/index.js.map +1 -1
  76. package/dist/testing/index.js +8673 -8260
  77. package/dist/testing/index.js.map +1 -1
  78. package/dist/training/index.d.ts +11 -0
  79. package/dist/training/index.d.ts.map +1 -0
  80. package/dist/training/jsonl-writer.d.ts +42 -0
  81. package/dist/training/jsonl-writer.d.ts.map +1 -0
  82. package/dist/training/schema-synthesizer.d.ts +20 -0
  83. package/dist/training/schema-synthesizer.d.ts.map +1 -0
  84. package/dist/training/types.d.ts +73 -0
  85. package/dist/training/types.d.ts.map +1 -0
  86. package/package.json +5 -1
  87. package/src/aot/index.ts +1 -0
  88. package/src/aot/registry-bridge.test.ts +104 -0
  89. package/src/aot/registry-bridge.ts +66 -0
  90. package/src/api/create-dsl.test.ts +306 -0
  91. package/src/api/create-dsl.ts +21 -0
  92. package/src/api/dispatcher.test.ts +66 -0
  93. package/src/api/dispatcher.ts +77 -9
  94. package/src/api/domain-registry.ts +120 -4
  95. package/src/core/pattern-matching/pattern-matcher.test.ts +131 -0
  96. package/src/core/pattern-matching/pattern-matcher.ts +102 -19
  97. package/src/core/tokenization/morphology/base-normalizer.ts +249 -0
  98. package/src/core/tokenization/morphology/index.ts +3 -1
  99. package/src/core/types.ts +147 -2
  100. package/src/feedback/confidence-gate.test.ts +111 -0
  101. package/src/feedback/confidence-gate.ts +70 -0
  102. package/src/feedback/feedback-formatter.test.ts +198 -0
  103. package/src/feedback/feedback-formatter.ts +298 -0
  104. package/src/feedback/index.ts +24 -0
  105. package/src/feedback/pattern-tracker.test.ts +198 -0
  106. package/src/feedback/pattern-tracker.ts +140 -0
  107. package/src/feedback/types.ts +149 -0
  108. package/src/generation/pattern-generator.ts +2 -2
  109. package/src/index.ts +29 -0
  110. package/src/integration/llm-lse-integration.test.ts +457 -0
  111. package/src/ir/conformance.test.ts +120 -0
  112. package/src/ir/explicit-parser.test.ts +612 -0
  113. package/src/ir/explicit-parser.ts +793 -0
  114. package/src/ir/explicit-renderer.test.ts +229 -0
  115. package/src/ir/explicit-renderer.ts +198 -0
  116. package/src/ir/from-interchange.test.ts +853 -0
  117. package/src/ir/from-interchange.ts +594 -0
  118. package/src/ir/index.ts +77 -0
  119. package/src/ir/json-schema.test.ts +281 -0
  120. package/src/ir/json-schema.ts +299 -0
  121. package/src/ir/protocol-json.test.ts +1187 -0
  122. package/src/ir/protocol-json.ts +751 -0
  123. package/src/ir/references.test.ts +38 -0
  124. package/src/ir/references.ts +33 -0
  125. package/src/ir/round-trip.test.ts +557 -0
  126. package/src/ir/to-runtime-ast.ts +203 -0
  127. package/src/ir/types.ts +191 -0
  128. package/src/prompts/index.ts +15 -0
  129. package/src/prompts/prompt-generator.test.ts +386 -0
  130. package/src/prompts/prompt-generator.ts +531 -0
  131. package/src/prompts/types.ts +89 -0
  132. package/src/schema/command-schema.ts +19 -0
  133. package/src/training/index.ts +13 -0
  134. package/src/training/jsonl-writer.test.ts +138 -0
  135. package/src/training/jsonl-writer.ts +67 -0
  136. package/src/training/schema-synthesizer.test.ts +250 -0
  137. package/src/training/schema-synthesizer.ts +306 -0
  138. package/src/training/types.ts +103 -0
@@ -14,6 +14,8 @@ import {
14
14
  InMemoryDictionary,
15
15
  InMemoryProfileProvider,
16
16
  } from '../interfaces';
17
+ import { isExplicitSyntax, parseExplicit } from '../ir';
18
+ import type { SchemaLookup } from '../ir';
17
19
 
18
20
  /**
19
21
  * Language configuration for a DSL.
@@ -172,6 +174,7 @@ class MultilingualDSLImpl implements MultilingualDSL {
172
174
  private matcher: PatternMatcher;
173
175
  private transformer: GrammarTransformer;
174
176
  private codeGenerator?: CodeGenerator;
177
+ private schemaLookup: SchemaLookup;
175
178
 
176
179
  constructor(config: DSLConfig, registry: DSLRegistry, transformer: GrammarTransformer) {
177
180
  this.registry = registry;
@@ -180,6 +183,12 @@ class MultilingualDSLImpl implements MultilingualDSL {
180
183
  if (config.codeGenerator) {
181
184
  this.codeGenerator = config.codeGenerator;
182
185
  }
186
+
187
+ // Build SchemaLookup from config schemas for explicit syntax validation
188
+ const schemaMap = new Map(config.schemas.map(s => [s.action, s]));
189
+ this.schemaLookup = {
190
+ getSchema: (action: string) => schemaMap.get(action),
191
+ };
183
192
  }
184
193
 
185
194
  parse(input: string, language: string): SemanticNode {
@@ -188,6 +197,16 @@ class MultilingualDSLImpl implements MultilingualDSL {
188
197
  }
189
198
 
190
199
  parseWithConfidence(input: string, language: string): { node: SemanticNode; confidence: number } {
200
+ // Try explicit bracket syntax first (language-agnostic)
201
+ if (isExplicitSyntax(input)) {
202
+ const node = parseExplicit(input, { schemaLookup: this.schemaLookup });
203
+ // Only match if this DSL has a schema for the action
204
+ if (!this.schemaLookup.getSchema(node.action)) {
205
+ throw new Error(`No schema for action "${node.action}" in this DSL`);
206
+ }
207
+ return { node, confidence: 1.0 };
208
+ }
209
+
191
210
  // Get tokenizer for language
192
211
  const tokenizer = this.registry.getTokenizer(language);
193
212
  if (!tokenizer) {
@@ -245,6 +264,8 @@ class MultilingualDSLImpl implements MultilingualDSL {
245
264
  }
246
265
 
247
266
  translate(input: string, fromLanguage: string, toLanguage: string): string {
267
+ // Explicit syntax is language-agnostic — return unchanged
268
+ if (isExplicitSyntax(input)) return input;
248
269
  // Use injected grammar transformer
249
270
  return this.transformer.transform(input, fromLanguage, toLanguage);
250
271
  }
@@ -333,4 +333,70 @@ describe('CrossDomainDispatcher', () => {
333
333
  expect(callCount).toBe(1);
334
334
  });
335
335
  });
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // compileComposite()
339
+ // ---------------------------------------------------------------------------
340
+
341
+ describe('compileComposite', () => {
342
+ it('compiles multi-line input across domains', async () => {
343
+ registry.register(createDescriptor('sql', 'select'));
344
+ registry.register(createDescriptor('bdd', 'given'));
345
+ const dispatcher = new CrossDomainDispatcher(registry);
346
+
347
+ const result = await dispatcher.compileComposite(
348
+ 'select name from users\ngiven page http://example.com'
349
+ );
350
+
351
+ expect(result.statements).toHaveLength(2);
352
+ expect(result.errors).toHaveLength(0);
353
+ expect(result.statements[0].domain).toBe('sql');
354
+ expect(result.statements[0].ok).toBe(true);
355
+ expect(result.statements[0].code).toContain('compiled:select');
356
+ expect(result.statements[1].domain).toBe('bdd');
357
+ expect(result.statements[1].ok).toBe(true);
358
+ });
359
+
360
+ it('skips comments and empty lines', async () => {
361
+ registry.register(createDescriptor('sql', 'select'));
362
+ const dispatcher = new CrossDomainDispatcher(registry);
363
+
364
+ const result = await dispatcher.compileComposite(
365
+ '// comment\n\nselect name\n-- another comment\n'
366
+ );
367
+
368
+ expect(result.statements).toHaveLength(1);
369
+ expect(result.statements[0].line).toBe(3);
370
+ expect(result.errors).toHaveLength(0);
371
+ });
372
+
373
+ it('collects unmatched lines as errors', async () => {
374
+ registry.register(createDescriptor('sql', 'select'));
375
+ const dispatcher = new CrossDomainDispatcher(registry);
376
+
377
+ const result = await dispatcher.compileComposite('select name\nunknown command\nselect id');
378
+
379
+ expect(result.statements).toHaveLength(2);
380
+ expect(result.errors).toHaveLength(1);
381
+ expect(result.errors[0].line).toBe(2);
382
+ expect(result.errors[0].message).toBe('No domain matched this input');
383
+ });
384
+
385
+ it('handles empty input', async () => {
386
+ const dispatcher = new CrossDomainDispatcher(registry);
387
+ const result = await dispatcher.compileComposite('');
388
+ expect(result.statements).toHaveLength(0);
389
+ expect(result.errors).toHaveLength(0);
390
+ });
391
+
392
+ it('preserves line numbers', async () => {
393
+ registry.register(createDescriptor('sql', 'select'));
394
+ const dispatcher = new CrossDomainDispatcher(registry);
395
+
396
+ const result = await dispatcher.compileComposite('// header\n\nselect name\n\nselect id');
397
+
398
+ expect(result.statements[0].line).toBe(3);
399
+ expect(result.statements[1].line).toBe(5);
400
+ });
401
+ });
336
402
  });
@@ -71,6 +71,24 @@ export interface CompositeParseResult {
71
71
  readonly errors: readonly CompositeError[];
72
72
  }
73
73
 
74
+ /** A single compiled statement in a composite compile result */
75
+ export interface CompositeCompileStatement {
76
+ readonly line: number;
77
+ readonly input: string;
78
+ readonly domain: string;
79
+ readonly ok: boolean;
80
+ readonly code?: string | undefined;
81
+ readonly errors?: string[] | undefined;
82
+ }
83
+
84
+ /** Result of multi-domain composite compilation */
85
+ export interface CompositeCompileResult {
86
+ /** Successfully detected and compiled statements */
87
+ readonly statements: readonly CompositeCompileStatement[];
88
+ /** Lines that could not be matched to any domain */
89
+ readonly errors: readonly CompositeError[];
90
+ }
91
+
74
92
  /** Dispatcher configuration options */
75
93
  export interface DispatcherOptions {
76
94
  /** Minimum confidence to accept a match (default: 0.5) */
@@ -123,26 +141,37 @@ export class CrossDomainDispatcher {
123
141
  const candidates: DispatchResult[] = [];
124
142
 
125
143
  for (const name of domainNames) {
126
- const dsl = await this.registry.getDSLForDomain(name);
127
- if (!dsl) continue;
128
-
129
144
  try {
145
+ const dsl = await this.registry.getDSLForDomain(name);
146
+ if (!dsl) continue;
147
+
130
148
  const { node, confidence } = dsl.parseWithConfidence(input, language);
131
149
  if (confidence >= this.minConfidence) {
132
150
  candidates.push({ domain: name, node, confidence, dsl });
133
151
  }
134
152
  } catch {
135
- // Domain could not parse this input — skip
153
+ // Domain could not be loaded or parse this input — skip
136
154
  }
137
155
  }
138
156
 
139
157
  if (candidates.length === 0) return null;
140
158
 
141
- // Sort by confidence descending, break ties by priority order
142
- candidates.sort((a, b) => {
143
- if (b.confidence !== a.confidence) return b.confidence - a.confidence;
144
- return this.getPriorityIndex(a.domain) - this.getPriorityIndex(b.domain);
145
- });
159
+ // Sort by priority first, then confidence within each priority tier.
160
+ // Simpler schemas (fewer roles) often score higher confidence than more
161
+ // specific ones — e.g., voice's "select [patient:greedy]" gets 1.0 while
162
+ // sql's "select [columns] from [source]" gets 0.71 for the same input.
163
+ // Priority ordering ensures domain specificity wins when both match.
164
+ if (this.priority.length > 0) {
165
+ candidates.sort((a, b) => {
166
+ const pa = this.getPriorityIndex(a.domain);
167
+ const pb = this.getPriorityIndex(b.domain);
168
+ if (pa !== pb) return pa - pb;
169
+ return b.confidence - a.confidence;
170
+ });
171
+ } else {
172
+ // No priority configured: pure confidence ordering
173
+ candidates.sort((a, b) => b.confidence - a.confidence);
174
+ }
146
175
 
147
176
  return candidates[0];
148
177
  }
@@ -198,6 +227,45 @@ export class CrossDomainDispatcher {
198
227
  return { ...result, domain: detected.domain };
199
228
  }
200
229
 
230
+ /**
231
+ * Compile multi-line input where each line may belong to a different domain.
232
+ * Detects the domain for each line and compiles it, returning per-line results.
233
+ * Empty lines and comment lines are skipped.
234
+ */
235
+ async compileComposite(input: string, language = 'en'): Promise<CompositeCompileResult> {
236
+ const lines = input.split('\n');
237
+ const statements: CompositeCompileStatement[] = [];
238
+ const errors: CompositeError[] = [];
239
+
240
+ for (let i = 0; i < lines.length; i++) {
241
+ const line = lines[i];
242
+ if (isSkippableLine(line)) continue;
243
+
244
+ const trimmed = line.trim();
245
+ const detected = await this.detect(trimmed, language);
246
+
247
+ if (detected) {
248
+ const result = detected.dsl.compile(trimmed, language);
249
+ statements.push({
250
+ line: i + 1,
251
+ input: trimmed,
252
+ domain: detected.domain,
253
+ ok: result.ok,
254
+ code: result.code,
255
+ errors: result.errors,
256
+ });
257
+ } else {
258
+ errors.push({
259
+ line: i + 1,
260
+ input: trimmed,
261
+ message: 'No domain matched this input',
262
+ });
263
+ }
264
+ }
265
+
266
+ return { statements, errors };
267
+ }
268
+
201
269
  /**
202
270
  * Validate input against all domains, returning the best match.
203
271
  */
@@ -33,6 +33,16 @@
33
33
  import type { SemanticNode } from '../core/types';
34
34
  import type { MultilingualDSL, CompileResult, ValidationResult } from './create-dsl';
35
35
  import type { NaturalLanguageRenderer } from '../generation/renderer';
36
+ import type { CommandSchema } from '../schema/command-schema';
37
+ import type { SchemaLookup } from '../ir/types';
38
+ import type { Diagnostic } from '../generation/diagnostics';
39
+ import type { GeneratedPrompt, PromptGeneratorConfig } from '../prompts/types';
40
+ import type { SynthesisConfig, SynthesisResult } from '../training/types';
41
+ import type { LSEFeedback } from '../feedback/types';
42
+ import { generatePrompt as _generatePrompt } from '../prompts/prompt-generator';
43
+ import { synthesizeFromSchemas as _synthesizeFromSchemas } from '../training/schema-synthesizer';
44
+ import { buildFeedback as _buildFeedback } from '../feedback/feedback-formatter';
45
+ import { parseExplicit as _parseExplicit } from '../ir/explicit-parser';
36
46
 
37
47
  // =============================================================================
38
48
  // Descriptor Interface
@@ -60,6 +70,9 @@ export interface DomainDescriptor {
60
70
  /** Description of the primary input parameter */
61
71
  readonly inputDescription: string;
62
72
 
73
+ /** What compile_{name} produces (e.g., "a SQL query string"). Used in tool description. */
74
+ readonly outputDescription?: string;
75
+
63
76
  /**
64
77
  * Factory to lazily create the DSL instance.
65
78
  * Called once on first tool use. The result is cached.
@@ -80,6 +93,13 @@ export interface DomainDescriptor {
80
93
  */
81
94
  readonly tools?: readonly ('parse' | 'compile' | 'validate' | 'translate')[];
82
95
 
96
+ /**
97
+ * Optional command schemas for this domain.
98
+ * When provided, enables system prompt generation, training data synthesis,
99
+ * and structured feedback via the registry's convenience methods.
100
+ */
101
+ readonly schemas?: readonly CommandSchema[];
102
+
83
103
  /**
84
104
  * Optional scan configuration for AOT compilation and Vite plugin.
85
105
  * Declares which HTML attributes/script types this domain uses.
@@ -150,6 +170,19 @@ export class DomainRegistry {
150
170
  this.descriptors.set(descriptor.name, descriptor);
151
171
  }
152
172
 
173
+ /**
174
+ * Attach schemas to an already-registered domain.
175
+ * Useful for lazy loading schemas after initial registration.
176
+ */
177
+ setSchemas(name: string, schemas: readonly CommandSchema[]): void {
178
+ const existing = this.descriptors.get(name);
179
+ if (!existing) {
180
+ throw new Error(`Domain not registered: ${name}`);
181
+ }
182
+ // Create a new descriptor with schemas attached
183
+ this.descriptors.set(name, { ...existing, schemas });
184
+ }
185
+
153
186
  /**
154
187
  * Get all registered domain names.
155
188
  */
@@ -230,6 +263,89 @@ export class DomainRegistry {
230
263
  return this.getDSL(descriptor);
231
264
  }
232
265
 
266
+ // ---------------------------------------------------------------------------
267
+ // LLM ↔ LSE Convenience Methods
268
+ // ---------------------------------------------------------------------------
269
+
270
+ /**
271
+ * Generate an LLM system prompt for a domain from its schemas.
272
+ * Returns null if the domain is not registered or has no schemas.
273
+ */
274
+ generatePrompt(domain: string, config?: Partial<PromptGeneratorConfig>): GeneratedPrompt | null {
275
+ const descriptor = this.descriptors.get(domain);
276
+ if (!descriptor?.schemas || descriptor.schemas.length === 0) return null;
277
+
278
+ return _generatePrompt({
279
+ domain,
280
+ description: descriptor.description,
281
+ schemas: descriptor.schemas,
282
+ ...config,
283
+ });
284
+ }
285
+
286
+ /**
287
+ * Synthesize training data for a domain from its schemas.
288
+ * Returns null if the domain is not registered or has no schemas.
289
+ */
290
+ generateTrainingData(domain: string, config?: Partial<SynthesisConfig>): SynthesisResult | null {
291
+ const descriptor = this.descriptors.get(domain);
292
+ if (!descriptor?.schemas || descriptor.schemas.length === 0) return null;
293
+
294
+ return _synthesizeFromSchemas(descriptor.schemas, {
295
+ domain,
296
+ ...config,
297
+ });
298
+ }
299
+
300
+ /**
301
+ * Build structured feedback for a failed LSE input in a domain.
302
+ * Returns null if the domain is not registered or has no schemas.
303
+ */
304
+ buildFeedback(
305
+ domain: string,
306
+ input: string,
307
+ inputFormat: 'explicit' | 'json' | 'natural',
308
+ diagnostics: readonly Diagnostic[]
309
+ ): LSEFeedback | null {
310
+ const descriptor = this.descriptors.get(domain);
311
+ if (!descriptor?.schemas || descriptor.schemas.length === 0) return null;
312
+
313
+ const schemaLookup = this.createSchemaLookup(descriptor.schemas);
314
+
315
+ // Try to extract the action from the input
316
+ let action: string | undefined;
317
+ try {
318
+ if (inputFormat === 'explicit') {
319
+ const node = _parseExplicit(input, { schemaLookup });
320
+ action = node.action;
321
+ }
322
+ } catch {
323
+ // Action detection failed — still build feedback without it
324
+ }
325
+
326
+ return _buildFeedback(input, inputFormat, diagnostics, schemaLookup, action);
327
+ }
328
+
329
+ /**
330
+ * Get schemas for a domain (if available).
331
+ */
332
+ getSchemas(domain: string): readonly CommandSchema[] | null {
333
+ const descriptor = this.descriptors.get(domain);
334
+ if (!descriptor?.schemas) return null;
335
+ return descriptor.schemas;
336
+ }
337
+
338
+ /**
339
+ * Create a SchemaLookup from a domain's schemas.
340
+ */
341
+ private createSchemaLookup(schemas: readonly CommandSchema[]): SchemaLookup {
342
+ const map = new Map<string, CommandSchema>();
343
+ for (const s of schemas) {
344
+ map.set(s.action, s);
345
+ }
346
+ return { getSchema: (action: string) => map.get(action) };
347
+ }
348
+
233
349
  // ---------------------------------------------------------------------------
234
350
  // Private: DSL/renderer lifecycle
235
351
  // ---------------------------------------------------------------------------
@@ -380,7 +496,7 @@ function generateToolDefinitions(desc: DomainDescriptor): MCPToolDefinition[] {
380
496
  case 'parse':
381
497
  tools.push({
382
498
  name: `parse_${desc.name}`,
383
- description: `Parse a ${desc.description} input into a semantic representation. Supports: ${langList}.`,
499
+ description: `Parse a ${desc.description} input into a semantic representation (action, roles, confidence). Supports: ${langList}.`,
384
500
  inputSchema: {
385
501
  type: 'object',
386
502
  properties: {
@@ -402,7 +518,7 @@ function generateToolDefinitions(desc: DomainDescriptor): MCPToolDefinition[] {
402
518
  case 'compile':
403
519
  tools.push({
404
520
  name: `compile_${desc.name}`,
405
- description: `Compile a ${desc.description} input to target code. Supports: ${langList}.`,
521
+ description: `Compile a ${desc.description} input to ${desc.outputDescription ?? 'target code'}. Supports: ${langList}.`,
406
522
  inputSchema: {
407
523
  type: 'object',
408
524
  properties: {
@@ -424,7 +540,7 @@ function generateToolDefinitions(desc: DomainDescriptor): MCPToolDefinition[] {
424
540
  case 'validate':
425
541
  tools.push({
426
542
  name: `validate_${desc.name}`,
427
- description: `Validate ${desc.description} syntax. Returns whether it parses successfully and any errors. Supports: ${langList}.`,
543
+ description: `Check ${desc.description} syntax for errors. Returns {valid, errors[]}. Supports: ${langList}.`,
428
544
  inputSchema: {
429
545
  type: 'object',
430
546
  properties: {
@@ -446,7 +562,7 @@ function generateToolDefinitions(desc: DomainDescriptor): MCPToolDefinition[] {
446
562
  case 'translate':
447
563
  tools.push({
448
564
  name: `translate_${desc.name}`,
449
- description: `Translate ${desc.description} input between natural languages. Parses in source language and renders in target language.`,
565
+ description: `Translate ${desc.description} input between natural languages (requires 'from' and 'to' codes). Supports: ${langList}.`,
450
566
  inputSchema: {
451
567
  type: 'object',
452
568
  properties: {
@@ -300,6 +300,137 @@ describe('PatternMatcher', () => {
300
300
  expect(result).not.toBeNull();
301
301
  expect(result?.captured.has('patient')).toBe(true);
302
302
  });
303
+
304
+ it('should stop greedy role inside group at parent verb keyword (SOV WHERE)', () => {
305
+ // SOV pattern: [source] から [columns] [GROUP: 条件 condition(greedy)] 選択
306
+ // Input: users から name 条件 age > 18 選択
307
+ // The greedy condition role must stop at 選択 (the verb keyword outside the group)
308
+ const schema = defineCommand({
309
+ action: 'select',
310
+ roles: [
311
+ defineRole({
312
+ role: 'columns',
313
+ required: true,
314
+ expectedTypes: ['expression'],
315
+ svoPosition: 2,
316
+ sovPosition: 1,
317
+ }),
318
+ defineRole({
319
+ role: 'source',
320
+ required: true,
321
+ expectedTypes: ['expression'],
322
+ svoPosition: 1,
323
+ sovPosition: 2,
324
+ markerOverride: { ja: 'から' },
325
+ }),
326
+ defineRole({
327
+ role: 'condition',
328
+ required: false,
329
+ expectedTypes: ['expression'],
330
+ greedy: true,
331
+ svoPosition: 0,
332
+ sovPosition: 0,
333
+ markerOverride: { ja: '条件' },
334
+ }),
335
+ ],
336
+ });
337
+
338
+ const profile: PatternGenLanguageProfile = {
339
+ code: 'ja',
340
+ wordOrder: 'SOV',
341
+ keywords: {
342
+ select: { primary: '選択' },
343
+ },
344
+ roleMarkers: {
345
+ condition: { primary: '条件', position: 'before' },
346
+ },
347
+ };
348
+
349
+ const pattern = generatePattern(schema, profile);
350
+
351
+ // Input: users から name 条件 age > 18 選択
352
+ const tokens = createMockTokenStream([
353
+ createToken({ kind: 'identifier', value: 'users', position: createPosition(0, 5) }),
354
+ createToken({ kind: 'keyword', value: 'から', position: createPosition(6, 8) }),
355
+ createToken({ kind: 'identifier', value: 'name', position: createPosition(9, 13) }),
356
+ createToken({ kind: 'keyword', value: '条件', position: createPosition(14, 16) }),
357
+ createToken({ kind: 'identifier', value: 'age', position: createPosition(17, 20) }),
358
+ createToken({ kind: 'operator', value: '>', position: createPosition(21, 22) }),
359
+ createToken({ kind: 'literal', value: '18', position: createPosition(23, 25) }),
360
+ createToken({ kind: 'keyword', value: '選択', position: createPosition(26, 28) }),
361
+ ]);
362
+
363
+ const result = matcher.matchPattern(tokens, pattern);
364
+
365
+ expect(result).not.toBeNull();
366
+ expect(result?.captured.has('source')).toBe(true);
367
+ expect(result?.captured.get('source')).toMatchObject({ raw: 'users' });
368
+ expect(result?.captured.has('columns')).toBe(true);
369
+ expect(result?.captured.get('columns')).toMatchObject({ raw: 'name' });
370
+ expect(result?.captured.has('condition')).toBe(true);
371
+ expect(result?.captured.get('condition')).toMatchObject({ raw: 'age > 18' });
372
+ });
373
+
374
+ it('should match SOV pattern without optional greedy group', () => {
375
+ // Same schema but input has no WHERE clause
376
+ // Input: users から name 選択
377
+ const schema = defineCommand({
378
+ action: 'select',
379
+ roles: [
380
+ defineRole({
381
+ role: 'columns',
382
+ required: true,
383
+ expectedTypes: ['expression'],
384
+ svoPosition: 2,
385
+ sovPosition: 1,
386
+ }),
387
+ defineRole({
388
+ role: 'source',
389
+ required: true,
390
+ expectedTypes: ['expression'],
391
+ svoPosition: 1,
392
+ sovPosition: 2,
393
+ markerOverride: { ja: 'から' },
394
+ }),
395
+ defineRole({
396
+ role: 'condition',
397
+ required: false,
398
+ expectedTypes: ['expression'],
399
+ greedy: true,
400
+ svoPosition: 0,
401
+ sovPosition: 0,
402
+ markerOverride: { ja: '条件' },
403
+ }),
404
+ ],
405
+ });
406
+
407
+ const profile: PatternGenLanguageProfile = {
408
+ code: 'ja',
409
+ wordOrder: 'SOV',
410
+ keywords: {
411
+ select: { primary: '選択' },
412
+ },
413
+ roleMarkers: {
414
+ condition: { primary: '条件', position: 'before' },
415
+ },
416
+ };
417
+
418
+ const pattern = generatePattern(schema, profile);
419
+
420
+ const tokens = createMockTokenStream([
421
+ createToken({ kind: 'identifier', value: 'users', position: createPosition(0, 5) }),
422
+ createToken({ kind: 'keyword', value: 'から', position: createPosition(6, 8) }),
423
+ createToken({ kind: 'identifier', value: 'name', position: createPosition(9, 13) }),
424
+ createToken({ kind: 'keyword', value: '選択', position: createPosition(14, 16) }),
425
+ ]);
426
+
427
+ const result = matcher.matchPattern(tokens, pattern);
428
+
429
+ expect(result).not.toBeNull();
430
+ expect(result?.captured.has('source')).toBe(true);
431
+ expect(result?.captured.has('columns')).toBe(true);
432
+ expect(result?.captured.has('condition')).toBe(false);
433
+ });
303
434
  });
304
435
  });
305
436