@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.
- package/dist/aot/index.d.ts +1 -0
- package/dist/aot/index.d.ts.map +1 -1
- package/dist/aot/registry-bridge.d.ts +27 -0
- package/dist/aot/registry-bridge.d.ts.map +1 -0
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/dispatcher.d.ts +22 -0
- package/dist/api/dispatcher.d.ts.map +1 -1
- package/dist/api/domain-registry.d.ts +41 -0
- package/dist/api/domain-registry.d.ts.map +1 -1
- package/dist/api/index.js +1438 -21
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.js +247 -10
- package/dist/core/index.js.map +1 -1
- package/dist/core/pattern-matching/index.js +74 -10
- package/dist/core/pattern-matching/index.js.map +1 -1
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
- package/dist/core/tokenization/index.js +133 -0
- package/dist/core/tokenization/index.js.map +1 -1
- package/dist/core/tokenization/morphology/base-normalizer.d.ts +94 -0
- package/dist/core/tokenization/morphology/base-normalizer.d.ts.map +1 -0
- package/dist/core/tokenization/morphology/index.d.ts +3 -1
- package/dist/core/tokenization/morphology/index.d.ts.map +1 -1
- package/dist/core/types.d.ts +81 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +40 -0
- package/dist/core/types.js.map +1 -1
- package/dist/feedback/confidence-gate.d.ts +28 -0
- package/dist/feedback/confidence-gate.d.ts.map +1 -0
- package/dist/feedback/feedback-formatter.d.ts +21 -0
- package/dist/feedback/feedback-formatter.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +11 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/pattern-tracker.d.ts +47 -0
- package/dist/feedback/pattern-tracker.d.ts.map +1 -0
- package/dist/feedback/types.d.ts +113 -0
- package/dist/feedback/types.d.ts.map +1 -0
- package/dist/generation/index.js +2 -1
- package/dist/generation/index.js.map +1 -1
- package/dist/index.cjs +3264 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3223 -132
- package/dist/index.js.map +1 -1
- package/dist/ir/explicit-parser.d.ts +86 -0
- package/dist/ir/explicit-parser.d.ts.map +1 -0
- package/dist/ir/explicit-renderer.d.ts +34 -0
- package/dist/ir/explicit-renderer.d.ts.map +1 -0
- package/dist/ir/from-interchange.d.ts +51 -0
- package/dist/ir/from-interchange.d.ts.map +1 -0
- package/dist/ir/index.d.ts +31 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +1934 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/json-schema.d.ts +40 -0
- package/dist/ir/json-schema.d.ts.map +1 -0
- package/dist/ir/protocol-json.d.ts +73 -0
- package/dist/ir/protocol-json.d.ts.map +1 -0
- package/dist/ir/references.d.ts +19 -0
- package/dist/ir/references.d.ts.map +1 -0
- package/dist/ir/to-runtime-ast.d.ts +53 -0
- package/dist/ir/to-runtime-ast.d.ts.map +1 -0
- package/dist/ir/types.d.ts +151 -0
- package/dist/ir/types.d.ts.map +1 -0
- package/dist/parsing/index.js +74 -10
- package/dist/parsing/index.js.map +1 -1
- package/dist/prompts/index.d.ts +9 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/prompt-generator.d.ts +49 -0
- package/dist/prompts/prompt-generator.d.ts.map +1 -0
- package/dist/prompts/types.d.ts +64 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/schema/command-schema.d.ts +17 -0
- package/dist/schema/command-schema.d.ts.map +1 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/testing/index.js +8673 -8260
- package/dist/testing/index.js.map +1 -1
- package/dist/training/index.d.ts +11 -0
- package/dist/training/index.d.ts.map +1 -0
- package/dist/training/jsonl-writer.d.ts +42 -0
- package/dist/training/jsonl-writer.d.ts.map +1 -0
- package/dist/training/schema-synthesizer.d.ts +20 -0
- package/dist/training/schema-synthesizer.d.ts.map +1 -0
- package/dist/training/types.d.ts +73 -0
- package/dist/training/types.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/aot/index.ts +1 -0
- package/src/aot/registry-bridge.test.ts +104 -0
- package/src/aot/registry-bridge.ts +66 -0
- package/src/api/create-dsl.test.ts +306 -0
- package/src/api/create-dsl.ts +21 -0
- package/src/api/dispatcher.test.ts +66 -0
- package/src/api/dispatcher.ts +77 -9
- package/src/api/domain-registry.ts +120 -4
- package/src/core/pattern-matching/pattern-matcher.test.ts +131 -0
- package/src/core/pattern-matching/pattern-matcher.ts +102 -19
- package/src/core/tokenization/morphology/base-normalizer.ts +249 -0
- package/src/core/tokenization/morphology/index.ts +3 -1
- package/src/core/types.ts +147 -2
- package/src/feedback/confidence-gate.test.ts +111 -0
- package/src/feedback/confidence-gate.ts +70 -0
- package/src/feedback/feedback-formatter.test.ts +198 -0
- package/src/feedback/feedback-formatter.ts +298 -0
- package/src/feedback/index.ts +24 -0
- package/src/feedback/pattern-tracker.test.ts +198 -0
- package/src/feedback/pattern-tracker.ts +140 -0
- package/src/feedback/types.ts +149 -0
- package/src/generation/pattern-generator.ts +2 -2
- package/src/index.ts +29 -0
- package/src/integration/llm-lse-integration.test.ts +457 -0
- package/src/ir/conformance.test.ts +120 -0
- package/src/ir/explicit-parser.test.ts +612 -0
- package/src/ir/explicit-parser.ts +793 -0
- package/src/ir/explicit-renderer.test.ts +229 -0
- package/src/ir/explicit-renderer.ts +198 -0
- package/src/ir/from-interchange.test.ts +853 -0
- package/src/ir/from-interchange.ts +594 -0
- package/src/ir/index.ts +77 -0
- package/src/ir/json-schema.test.ts +281 -0
- package/src/ir/json-schema.ts +299 -0
- package/src/ir/protocol-json.test.ts +1187 -0
- package/src/ir/protocol-json.ts +751 -0
- package/src/ir/references.test.ts +38 -0
- package/src/ir/references.ts +33 -0
- package/src/ir/round-trip.test.ts +557 -0
- package/src/ir/to-runtime-ast.ts +203 -0
- package/src/ir/types.ts +191 -0
- package/src/prompts/index.ts +15 -0
- package/src/prompts/prompt-generator.test.ts +386 -0
- package/src/prompts/prompt-generator.ts +531 -0
- package/src/prompts/types.ts +89 -0
- package/src/schema/command-schema.ts +19 -0
- package/src/training/index.ts +13 -0
- package/src/training/jsonl-writer.test.ts +138 -0
- package/src/training/jsonl-writer.ts +67 -0
- package/src/training/schema-synthesizer.test.ts +250 -0
- package/src/training/schema-synthesizer.ts +306 -0
- package/src/training/types.ts +103 -0
package/src/api/create-dsl.ts
CHANGED
|
@@ -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
|
});
|
package/src/api/dispatcher.ts
CHANGED
|
@@ -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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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: `
|
|
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
|
|
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
|
|