@lokascript/framework 2.8.0 → 2.9.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/CHANGELOG.md +393 -0
- package/dist/api/create-dsl.d.ts +93 -1
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/domain-registry.d.ts +5 -3
- package/dist/api/domain-registry.d.ts.map +1 -1
- package/dist/api/index.js +232 -18
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.js +26 -6
- package/dist/core/index.js.map +1 -1
- package/dist/core/tokenization/base-tokenizer.d.ts +15 -3
- package/dist/core/tokenization/base-tokenizer.d.ts.map +1 -1
- package/dist/core/tokenization/index.js +26 -6
- package/dist/core/tokenization/index.js.map +1 -1
- package/dist/core/tokenization/token-utils.d.ts +15 -0
- package/dist/core/tokenization/token-utils.d.ts.map +1 -1
- package/dist/generation/index.js +73 -47
- package/dist/generation/index.js.map +1 -1
- package/dist/generation/pattern-generator.d.ts +8 -1
- package/dist/generation/pattern-generator.d.ts.map +1 -1
- package/dist/generation/renderer.d.ts +53 -1
- package/dist/generation/renderer.d.ts.map +1 -1
- package/dist/index.cjs +302 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +298 -115
- package/dist/index.js.map +1 -1
- package/dist/interfaces/value-extractor.d.ts +5 -0
- package/dist/interfaces/value-extractor.d.ts.map +1 -1
- package/dist/multilingual/index.js +25 -6
- package/dist/multilingual/index.js.map +1 -1
- package/package.json +4 -3
- package/src/api/create-dsl.test.ts +11 -0
- package/src/api/create-dsl.ts +278 -9
- package/src/api/domain-registry.ts +15 -10
- package/src/api/extensions.test.ts +322 -0
- package/src/core/tokenization/base-tokenizer.ts +23 -8
- package/src/core/tokenization/css-selector-extractor.test.ts +67 -0
- package/src/core/tokenization/token-utils.ts +18 -0
- package/src/generation/domain-renderer.test.ts +172 -0
- package/src/generation/pattern-generator.test.ts +102 -0
- package/src/generation/pattern-generator.ts +27 -19
- package/src/generation/renderer.test.ts +243 -4
- package/src/generation/renderer.ts +188 -45
- package/src/index.ts +9 -1
- package/src/interfaces/value-extractor.ts +50 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lokascript/framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "Generic framework for building multilingual DSLs with semantic parsing and grammar transformation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
"author": "LokaScript Contributors",
|
|
97
97
|
"license": "MIT",
|
|
98
98
|
"dependencies": {
|
|
99
|
-
"@lokascript/intent": "^2.
|
|
99
|
+
"@lokascript/intent": "^2.9.0"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|
|
102
102
|
"@types/node": "^20.0.0",
|
|
@@ -109,7 +109,8 @@
|
|
|
109
109
|
"dist",
|
|
110
110
|
"src",
|
|
111
111
|
"LICENSE",
|
|
112
|
-
"README.md"
|
|
112
|
+
"README.md",
|
|
113
|
+
"CHANGELOG.md"
|
|
113
114
|
],
|
|
114
115
|
"repository": {
|
|
115
116
|
"type": "git",
|
|
@@ -279,6 +279,17 @@ describe('MultilingualDSL: Explicit Syntax Support', () => {
|
|
|
279
279
|
const result = dsl.translate('[select columns:name source:users]', 'en', 'ja');
|
|
280
280
|
expect(result).toBe('[select columns:name source:users]');
|
|
281
281
|
});
|
|
282
|
+
|
|
283
|
+
// The test DSL configures no `grammarProfile`, which is legal for
|
|
284
|
+
// parse/validate/compile but not for translate. The error must name the
|
|
285
|
+
// field to set rather than leaving the caller with "No profile found".
|
|
286
|
+
it('throws an error naming grammarProfile when the profile is missing', () => {
|
|
287
|
+
const dsl = createTestDSL();
|
|
288
|
+
expect(() => dsl.translate('select name from users', 'en', 'ja')).toThrow(/grammarProfile/);
|
|
289
|
+
expect(() => dsl.translate('select name from users', 'en', 'ja')).toThrow(
|
|
290
|
+
/parse\/validate\/compile do not need it/
|
|
291
|
+
);
|
|
292
|
+
});
|
|
282
293
|
});
|
|
283
294
|
|
|
284
295
|
// ---------------------------------------------------------------------------
|
package/src/api/create-dsl.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { SemanticNode, LanguageTokenizer, LanguagePattern } from '../core/t
|
|
|
6
6
|
import type { CommandSchema } from '../schema';
|
|
7
7
|
import { PatternMatcher, type PatternMatcherProfile } from '../core/pattern-matching';
|
|
8
8
|
import { generatePattern, type PatternGenLanguageProfile } from '../generation/pattern-generator';
|
|
9
|
+
import { createDomainRenderer, type DomainRenderFn } from '../generation/renderer';
|
|
9
10
|
import { GrammarTransformer, type LanguageProfile as GrammarProfile } from '../grammar';
|
|
10
11
|
import {
|
|
11
12
|
type Dictionary,
|
|
@@ -31,7 +32,12 @@ export interface LanguageConfig {
|
|
|
31
32
|
readonly tokenizer: LanguageTokenizer;
|
|
32
33
|
/** Language profile for pattern generation */
|
|
33
34
|
readonly patternProfile: PatternGenLanguageProfile;
|
|
34
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Language profile for grammar transformation.
|
|
37
|
+
*
|
|
38
|
+
* Optional for `parse()`, `validate()` and `compile()`, but **required for
|
|
39
|
+
* `translate()`** — omitting it makes `translate()` throw for this language.
|
|
40
|
+
*/
|
|
35
41
|
readonly grammarProfile?: GrammarProfile;
|
|
36
42
|
}
|
|
37
43
|
|
|
@@ -42,6 +48,77 @@ export interface CodeGenerator {
|
|
|
42
48
|
generate(node: SemanticNode): string;
|
|
43
49
|
}
|
|
44
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Per-language vocabulary for a command added via {@link DomainExtension}.
|
|
53
|
+
*
|
|
54
|
+
* This is the same shape a language profile uses, narrowed to the parts a
|
|
55
|
+
* single new command needs.
|
|
56
|
+
*/
|
|
57
|
+
export interface ExtensionVocabulary {
|
|
58
|
+
/** The command's verb in this language. */
|
|
59
|
+
readonly keyword: PatternGenLanguageProfile['keywords'][string];
|
|
60
|
+
/**
|
|
61
|
+
* Markers for roles this language does not already cover in its profile.
|
|
62
|
+
* Usually unnecessary: role markers are shared across a domain's commands.
|
|
63
|
+
*/
|
|
64
|
+
readonly roleMarkers?: PatternGenLanguageProfile['roleMarkers'];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A command added to a DSL from outside the package that defines it.
|
|
69
|
+
*
|
|
70
|
+
* Supplying a schema plus one vocabulary entry per language is enough to parse,
|
|
71
|
+
* render and translate the command in every one of them — word order, role
|
|
72
|
+
* markers and keyword placement all follow from the schema and the language
|
|
73
|
+
* profiles. `render` and `generate` are only needed when the command's surface
|
|
74
|
+
* form or output cannot be derived.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const research: DomainExtension = {
|
|
79
|
+
* schema: defineCommand({
|
|
80
|
+
* action: 'research',
|
|
81
|
+
* roles: [
|
|
82
|
+
* defineRole({ role: 'patient', required: true, expectedTypes: ['expression'] }),
|
|
83
|
+
* defineRole({
|
|
84
|
+
* role: 'source', required: true, expectedTypes: ['expression'],
|
|
85
|
+
* markerOverride: { en: 'from', ja: 'から', ar: 'من' },
|
|
86
|
+
* }),
|
|
87
|
+
* ],
|
|
88
|
+
* }),
|
|
89
|
+
* vocabulary: {
|
|
90
|
+
* en: { keyword: { primary: 'research' } },
|
|
91
|
+
* ja: { keyword: { primary: '調査' } },
|
|
92
|
+
* ar: { keyword: { primary: 'ابحث' } },
|
|
93
|
+
* },
|
|
94
|
+
* };
|
|
95
|
+
*
|
|
96
|
+
* const llm = createLLMDSL({ extensions: [research] });
|
|
97
|
+
* llm.parse('research "climate" from #wiki', 'en');
|
|
98
|
+
* llm.render(node, 'ja'); // → '"climate" #wiki から 調査'
|
|
99
|
+
* ```
|
|
100
|
+
*/
|
|
101
|
+
export interface DomainExtension {
|
|
102
|
+
/** Schema for the new command. Its action must not collide with an existing one. */
|
|
103
|
+
readonly schema: CommandSchema;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Vocabulary per language code. Codes must be configured on the DSL; an
|
|
107
|
+
* unknown code is a configuration error, not a silent no-op.
|
|
108
|
+
*
|
|
109
|
+
* A subset is allowed — a command can be added in six of a DSL's eleven
|
|
110
|
+
* languages. The languages left out simply do not parse it, so vocabulary
|
|
111
|
+
* can be filled in over time rather than all at once.
|
|
112
|
+
*/
|
|
113
|
+
readonly vocabulary: Readonly<Record<string, ExtensionVocabulary>>;
|
|
114
|
+
|
|
115
|
+
/** Custom rendering for this action; takes precedence over the schema-driven path. */
|
|
116
|
+
readonly render?: DomainRenderFn;
|
|
117
|
+
|
|
118
|
+
/** Custom code generation for this action; takes precedence over the DSL's generator. */
|
|
119
|
+
readonly generate?: (node: SemanticNode) => string;
|
|
120
|
+
}
|
|
121
|
+
|
|
45
122
|
/**
|
|
46
123
|
* DSL configuration with dependency injection support.
|
|
47
124
|
*/
|
|
@@ -69,6 +146,19 @@ export interface DSLConfig {
|
|
|
69
146
|
/** Code generator for compilation (default: none) */
|
|
70
147
|
readonly codeGenerator?: CodeGenerator;
|
|
71
148
|
|
|
149
|
+
/**
|
|
150
|
+
* The domain's natural-language renderer, consulted by {@link MultilingualDSL.render}.
|
|
151
|
+
* Actions it returns `null` for fall through to the schema-driven renderer.
|
|
152
|
+
*/
|
|
153
|
+
readonly renderer?: DomainRenderFn;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Commands added from outside this package. Their schemas and vocabulary are
|
|
157
|
+
* merged into the DSL before pattern generation, so they parse, render and
|
|
158
|
+
* compile like built-in commands.
|
|
159
|
+
*/
|
|
160
|
+
readonly extensions?: readonly DomainExtension[];
|
|
161
|
+
|
|
72
162
|
// === Options ===
|
|
73
163
|
|
|
74
164
|
/** Auto-generate patterns from schemas (default: true) */
|
|
@@ -118,6 +208,16 @@ export interface MultilingualDSL {
|
|
|
118
208
|
// Translation
|
|
119
209
|
translate(input: string, fromLanguage: string, toLanguage: string): string;
|
|
120
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Render a parsed node back to natural language.
|
|
213
|
+
*
|
|
214
|
+
* Returns `null` when the action cannot be rendered — no custom renderer, no
|
|
215
|
+
* domain renderer, and no schema. Optional so that third-party implementers
|
|
216
|
+
* of this interface are not broken by its addition; `createMultilingualDSL`
|
|
217
|
+
* always provides it.
|
|
218
|
+
*/
|
|
219
|
+
render?(node: SemanticNode, language: string): string | null;
|
|
220
|
+
|
|
121
221
|
// Language support
|
|
122
222
|
getSupportedLanguages(): string[];
|
|
123
223
|
}
|
|
@@ -129,9 +229,17 @@ class DSLRegistry {
|
|
|
129
229
|
private patterns = new Map<string, LanguagePattern[]>();
|
|
130
230
|
private tokenizers = new Map<string, LanguageTokenizer>();
|
|
131
231
|
private schemas: readonly CommandSchema[];
|
|
132
|
-
|
|
133
|
-
|
|
232
|
+
private partiallyTranslatedActions: ReadonlySet<string>;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param partiallyTranslatedActions Actions permitted to lack a keyword in
|
|
236
|
+
* some configured languages — extension commands, which may be added for a
|
|
237
|
+
* subset of the DSL's languages. A built-in command missing a keyword stays
|
|
238
|
+
* an error, since that is a domain-authoring mistake.
|
|
239
|
+
*/
|
|
240
|
+
constructor(config: DSLConfig, partiallyTranslatedActions: ReadonlySet<string> = new Set()) {
|
|
134
241
|
this.schemas = config.schemas;
|
|
242
|
+
this.partiallyTranslatedActions = partiallyTranslatedActions;
|
|
135
243
|
|
|
136
244
|
// Register each language
|
|
137
245
|
for (const lang of config.languages) {
|
|
@@ -146,6 +254,12 @@ class DSLRegistry {
|
|
|
146
254
|
// Generate patterns for this language
|
|
147
255
|
const patterns: LanguagePattern[] = [];
|
|
148
256
|
for (const schema of this.schemas) {
|
|
257
|
+
if (
|
|
258
|
+
this.partiallyTranslatedActions.has(schema.action) &&
|
|
259
|
+
!lang.patternProfile.keywords[schema.action]
|
|
260
|
+
) {
|
|
261
|
+
continue; // this extension command was not translated into this language
|
|
262
|
+
}
|
|
149
263
|
const pattern = generatePattern(schema, lang.patternProfile);
|
|
150
264
|
patterns.push(pattern);
|
|
151
265
|
}
|
|
@@ -173,16 +287,42 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
173
287
|
private registry: DSLRegistry;
|
|
174
288
|
private matcher: PatternMatcher;
|
|
175
289
|
private transformer: GrammarTransformer;
|
|
290
|
+
private profileProvider: ProfileProvider;
|
|
176
291
|
private codeGenerator?: CodeGenerator;
|
|
177
292
|
private schemaLookup: SchemaLookup;
|
|
178
|
-
|
|
179
|
-
|
|
293
|
+
private domainRenderer?: DomainRenderFn;
|
|
294
|
+
private extensionRenderers: Map<string, DomainRenderFn>;
|
|
295
|
+
private schemaFallbackRenderer: DomainRenderFn;
|
|
296
|
+
|
|
297
|
+
constructor(
|
|
298
|
+
config: DSLConfig,
|
|
299
|
+
registry: DSLRegistry,
|
|
300
|
+
transformer: GrammarTransformer,
|
|
301
|
+
profileProvider: ProfileProvider
|
|
302
|
+
) {
|
|
180
303
|
this.registry = registry;
|
|
181
304
|
this.matcher = new PatternMatcher();
|
|
182
305
|
this.transformer = transformer;
|
|
306
|
+
this.profileProvider = profileProvider;
|
|
183
307
|
if (config.codeGenerator) {
|
|
184
308
|
this.codeGenerator = config.codeGenerator;
|
|
185
309
|
}
|
|
310
|
+
if (config.renderer) {
|
|
311
|
+
this.domainRenderer = config.renderer;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.extensionRenderers = new Map();
|
|
315
|
+
for (const extension of config.extensions ?? []) {
|
|
316
|
+
if (extension.render) this.extensionRenderers.set(extension.schema.action, extension.render);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Last resort for render(): covers extension commands with no custom
|
|
320
|
+
// renderer, and any built-in action the domain renderer declines. Lazy, so
|
|
321
|
+
// a DSL that never renders pays nothing.
|
|
322
|
+
this.schemaFallbackRenderer = createDomainRenderer({
|
|
323
|
+
schemas: config.schemas,
|
|
324
|
+
profiles: config.languages.map(l => l.patternProfile),
|
|
325
|
+
});
|
|
186
326
|
|
|
187
327
|
// Build SchemaLookup from config schemas for explicit syntax validation
|
|
188
328
|
const schemaMap = new Map(config.schemas.map(s => [s.action, s]));
|
|
@@ -266,6 +406,22 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
266
406
|
translate(input: string, fromLanguage: string, toLanguage: string): string {
|
|
267
407
|
// Explicit syntax is language-agnostic — return unchanged
|
|
268
408
|
if (isExplicitSyntax(input)) return input;
|
|
409
|
+
|
|
410
|
+
// translate() is the only path that needs grammar profiles; parse/validate/
|
|
411
|
+
// compile work without them. `grammarProfile` is optional on LanguageConfig,
|
|
412
|
+
// so omitting it fails here rather than at construction — name the missing
|
|
413
|
+
// field instead of leaving the transformer's bare "No profile found".
|
|
414
|
+
for (const language of [fromLanguage, toLanguage]) {
|
|
415
|
+
if (!this.profileProvider.getProfile(language)) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`translate() requires a grammar profile for language "${language}", but none is ` +
|
|
418
|
+
`configured. Set 'grammarProfile' on the LanguageConfig for "${language}" in ` +
|
|
419
|
+
`createMultilingualDSL() (parse/validate/compile do not need it), or inject a ` +
|
|
420
|
+
`custom 'profileProvider'.`
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
269
425
|
// Use injected grammar transformer
|
|
270
426
|
return this.transformer.transform(input, fromLanguage, toLanguage);
|
|
271
427
|
}
|
|
@@ -299,6 +455,24 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
299
455
|
}
|
|
300
456
|
}
|
|
301
457
|
|
|
458
|
+
render(node: SemanticNode, language: string): string | null {
|
|
459
|
+
// 1. An extension's own renderer, if it supplied one
|
|
460
|
+
const extensionRenderer = this.extensionRenderers.get(node.action);
|
|
461
|
+
if (extensionRenderer) {
|
|
462
|
+
const rendered = extensionRenderer(node, language);
|
|
463
|
+
if (rendered != null) return rendered;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 2. The domain's renderer — its hand-written cases are authoritative
|
|
467
|
+
if (this.domainRenderer) {
|
|
468
|
+
const rendered = this.domainRenderer(node, language);
|
|
469
|
+
if (rendered != null) return rendered;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 3. Schema-driven, which is how extension commands render by default
|
|
473
|
+
return this.schemaFallbackRenderer(node, language);
|
|
474
|
+
}
|
|
475
|
+
|
|
302
476
|
getSupportedLanguages(): string[] {
|
|
303
477
|
return this.registry.getSupportedLanguages();
|
|
304
478
|
}
|
|
@@ -369,12 +543,107 @@ function createDefaultProfileProvider(config: DSLConfig): ProfileProvider {
|
|
|
369
543
|
return new InMemoryProfileProvider(profiles);
|
|
370
544
|
}
|
|
371
545
|
|
|
546
|
+
/**
|
|
547
|
+
* Fold extensions into the config so that everything downstream — pattern
|
|
548
|
+
* generation, the dictionary, explicit-syntax lookup, rendering — sees
|
|
549
|
+
* extension commands exactly as it sees built-in ones.
|
|
550
|
+
*
|
|
551
|
+
* Done up front rather than by mutating a live DSL: `DSLRegistry` generates
|
|
552
|
+
* every language's patterns in its constructor, so a command registered later
|
|
553
|
+
* would compile and render but silently fail to parse.
|
|
554
|
+
*/
|
|
555
|
+
function applyExtensions(config: DSLConfig): DSLConfig {
|
|
556
|
+
const extensions = config.extensions;
|
|
557
|
+
if (!extensions || extensions.length === 0) return config;
|
|
558
|
+
|
|
559
|
+
const configuredLanguages = new Set(config.languages.map(l => l.code));
|
|
560
|
+
const actions = new Set(config.schemas.map(s => s.action));
|
|
561
|
+
|
|
562
|
+
for (const extension of extensions) {
|
|
563
|
+
const { action } = extension.schema;
|
|
564
|
+
if (actions.has(action)) {
|
|
565
|
+
throw new Error(
|
|
566
|
+
`Extension command "${action}" collides with a command this DSL already defines. ` +
|
|
567
|
+
`Pick a different action name.`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
actions.add(action);
|
|
571
|
+
|
|
572
|
+
for (const code of Object.keys(extension.vocabulary)) {
|
|
573
|
+
if (!configuredLanguages.has(code)) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`Extension command "${action}" supplies vocabulary for language "${code}", which is ` +
|
|
576
|
+
`not configured on this DSL. Configured languages: ` +
|
|
577
|
+
`${[...configuredLanguages].join(', ')}.`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const schemas = [...config.schemas, ...extensions.map(e => e.schema)];
|
|
584
|
+
|
|
585
|
+
const languages = config.languages.map(lang => {
|
|
586
|
+
const keywords = { ...lang.patternProfile.keywords };
|
|
587
|
+
let roleMarkers = lang.patternProfile.roleMarkers;
|
|
588
|
+
|
|
589
|
+
for (const extension of extensions) {
|
|
590
|
+
const vocabulary = extension.vocabulary[lang.code];
|
|
591
|
+
if (!vocabulary) continue;
|
|
592
|
+
keywords[extension.schema.action] = { ...vocabulary.keyword };
|
|
593
|
+
if (vocabulary.roleMarkers) {
|
|
594
|
+
roleMarkers = { ...roleMarkers, ...vocabulary.roleMarkers };
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return {
|
|
599
|
+
...lang,
|
|
600
|
+
patternProfile: {
|
|
601
|
+
...lang.patternProfile,
|
|
602
|
+
keywords,
|
|
603
|
+
...(roleMarkers !== undefined && { roleMarkers }),
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// Extension code generators dispatch ahead of the domain's own generator,
|
|
609
|
+
// whose `default` branch would otherwise throw on an action it never knew.
|
|
610
|
+
const extensionGenerators = new Map(
|
|
611
|
+
extensions.filter(e => e.generate).map(e => [e.schema.action, e.generate!])
|
|
612
|
+
);
|
|
613
|
+
const baseGenerator = config.codeGenerator;
|
|
614
|
+
const codeGenerator: CodeGenerator | undefined =
|
|
615
|
+
extensionGenerators.size > 0
|
|
616
|
+
? {
|
|
617
|
+
generate(node: SemanticNode): string {
|
|
618
|
+
const generate = extensionGenerators.get(node.action);
|
|
619
|
+
if (generate) return generate(node);
|
|
620
|
+
if (!baseGenerator) {
|
|
621
|
+
throw new Error(`No code generator for action "${node.action}"`);
|
|
622
|
+
}
|
|
623
|
+
return baseGenerator.generate(node);
|
|
624
|
+
},
|
|
625
|
+
}
|
|
626
|
+
: baseGenerator;
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
...config,
|
|
630
|
+
schemas,
|
|
631
|
+
languages,
|
|
632
|
+
...(codeGenerator !== undefined && { codeGenerator }),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
372
636
|
export function createMultilingualDSL(config: DSLConfig): MultilingualDSL {
|
|
637
|
+
// Extensions must be folded in before anything reads the config
|
|
638
|
+
const effectiveConfig = applyExtensions(config);
|
|
639
|
+
const extensionActions = new Set((config.extensions ?? []).map(e => e.schema.action));
|
|
640
|
+
|
|
373
641
|
// Create or use provided dictionary
|
|
374
|
-
const dictionary =
|
|
642
|
+
const dictionary = effectiveConfig.dictionary ?? createDefaultDictionary(effectiveConfig);
|
|
375
643
|
|
|
376
644
|
// Create or use provided profile provider
|
|
377
|
-
const profileProvider =
|
|
645
|
+
const profileProvider =
|
|
646
|
+
effectiveConfig.profileProvider ?? createDefaultProfileProvider(effectiveConfig);
|
|
378
647
|
|
|
379
648
|
// Create grammar transformer with injected dependencies
|
|
380
649
|
const transformer = new GrammarTransformer({
|
|
@@ -383,6 +652,6 @@ export function createMultilingualDSL(config: DSLConfig): MultilingualDSL {
|
|
|
383
652
|
});
|
|
384
653
|
|
|
385
654
|
// Create registry and implementation
|
|
386
|
-
const registry = new DSLRegistry(
|
|
387
|
-
return new MultilingualDSLImpl(
|
|
655
|
+
const registry = new DSLRegistry(effectiveConfig, extensionActions);
|
|
656
|
+
return new MultilingualDSLImpl(effectiveConfig, registry, transformer, profileProvider);
|
|
388
657
|
}
|
|
@@ -30,9 +30,8 @@
|
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
import type { SemanticNode } from '../core/types';
|
|
34
33
|
import type { MultilingualDSL, CompileResult, ValidationResult } from './create-dsl';
|
|
35
|
-
import type { NaturalLanguageRenderer } from '../generation/renderer';
|
|
34
|
+
import type { NaturalLanguageRenderer, DomainRenderFn } from '../generation/renderer';
|
|
36
35
|
import type { CommandSchema } from '../schema/command-schema';
|
|
37
36
|
import type { SchemaLookup } from '../ir/types';
|
|
38
37
|
import type { Diagnostic } from '../generation/diagnostics';
|
|
@@ -82,11 +81,12 @@ export interface DomainDescriptor {
|
|
|
82
81
|
/**
|
|
83
82
|
* Factory to lazily create the renderer.
|
|
84
83
|
* Required for translate_{name} tool. Called once on first use.
|
|
84
|
+
*
|
|
85
|
+
* The function form may return `null` for an action it cannot render; the
|
|
86
|
+
* translate tool reports that as an unsuccessful translation.
|
|
85
87
|
*/
|
|
86
88
|
readonly getRenderer?: () =>
|
|
87
|
-
| NaturalLanguageRenderer
|
|
88
|
-
| ((node: SemanticNode, language: string) => string)
|
|
89
|
-
| Promise<NaturalLanguageRenderer | ((node: SemanticNode, language: string) => string)>;
|
|
89
|
+
NaturalLanguageRenderer | DomainRenderFn | Promise<NaturalLanguageRenderer | DomainRenderFn>;
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* Which standard tools to generate. Default: all four.
|
|
@@ -154,10 +154,7 @@ export interface MCPToolResponse {
|
|
|
154
154
|
export class DomainRegistry {
|
|
155
155
|
private descriptors = new Map<string, DomainDescriptor>();
|
|
156
156
|
private dslCache = new Map<string, MultilingualDSL>();
|
|
157
|
-
private rendererCache = new Map<
|
|
158
|
-
string,
|
|
159
|
-
NaturalLanguageRenderer | ((node: SemanticNode, language: string) => string)
|
|
160
|
-
>();
|
|
157
|
+
private rendererCache = new Map<string, NaturalLanguageRenderer | DomainRenderFn>();
|
|
161
158
|
|
|
162
159
|
/**
|
|
163
160
|
* Register a domain.
|
|
@@ -361,7 +358,7 @@ export class DomainRegistry {
|
|
|
361
358
|
|
|
362
359
|
private async getRenderer(
|
|
363
360
|
descriptor: DomainDescriptor
|
|
364
|
-
): Promise<NaturalLanguageRenderer |
|
|
361
|
+
): Promise<NaturalLanguageRenderer | DomainRenderFn | null> {
|
|
365
362
|
if (!descriptor.getRenderer) return null;
|
|
366
363
|
|
|
367
364
|
const cached = this.rendererCache.get(descriptor.name);
|
|
@@ -466,6 +463,14 @@ export class DomainRegistry {
|
|
|
466
463
|
} catch {
|
|
467
464
|
// Fall through with rendered = null
|
|
468
465
|
}
|
|
466
|
+
} else if (dsl.render) {
|
|
467
|
+
// A descriptor without an explicit renderer can still render: the DSL
|
|
468
|
+
// knows its own domain renderer and its schema fallback.
|
|
469
|
+
try {
|
|
470
|
+
rendered = dsl.render(node, to);
|
|
471
|
+
} catch {
|
|
472
|
+
// Fall through with rendered = null
|
|
473
|
+
}
|
|
469
474
|
}
|
|
470
475
|
|
|
471
476
|
const roles: Record<string, unknown> = {};
|