@lokascript/framework 4.5.0 → 4.7.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 +220 -8
- package/dist/api/create-dsl.d.ts +59 -1
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/domain-registry.d.ts.map +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1938 -724
- package/dist/api/index.js.map +1 -1
- package/dist/api/reading.d.ts +8 -0
- package/dist/api/reading.d.ts.map +1 -0
- package/dist/api/translation-loss.d.ts +84 -0
- package/dist/api/translation-loss.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1047 -684
- package/dist/core/index.js.map +1 -1
- package/dist/core/matching/confidence-model.d.ts +54 -0
- package/dist/core/matching/confidence-model.d.ts.map +1 -0
- package/dist/core/matching/index.d.ts +12 -0
- package/dist/core/matching/index.d.ts.map +1 -0
- package/dist/core/matching/match-skeleton.d.ts +114 -0
- package/dist/core/matching/match-skeleton.d.ts.map +1 -0
- package/dist/core/matching/stock-confidence.d.ts +15 -0
- package/dist/core/matching/stock-confidence.d.ts.map +1 -0
- package/dist/core/matching/stock-reader.d.ts +56 -0
- package/dist/core/matching/stock-reader.d.ts.map +1 -0
- package/dist/core/matching/stock-reading.d.ts +44 -0
- package/dist/core/matching/stock-reading.d.ts.map +1 -0
- package/dist/core/matching/value-reader.d.ts +62 -0
- package/dist/core/matching/value-reader.d.ts.map +1 -0
- package/dist/core/pattern-matching/index.js +561 -660
- package/dist/core/pattern-matching/index.js.map +1 -1
- package/dist/core/pattern-matching/pattern-matcher.d.ts +0 -117
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
- package/dist/core/types.d.ts +48 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/generation/index.d.ts +2 -0
- package/dist/generation/index.d.ts.map +1 -1
- package/dist/generation/index.js +583 -1
- package/dist/generation/index.js.map +1 -1
- package/dist/generation/pattern-render.d.ts +72 -0
- package/dist/generation/pattern-render.d.ts.map +1 -0
- package/dist/generation/schema-patterns.d.ts +152 -0
- package/dist/generation/schema-patterns.d.ts.map +1 -0
- package/dist/index.cjs +2132 -890
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2110 -887
- package/dist/index.js.map +1 -1
- package/dist/ir/index.js.map +1 -1
- package/dist/parsing/index.js +561 -660
- package/dist/parsing/index.js.map +1 -1
- package/dist/testing/gate/index.d.ts +8 -0
- package/dist/testing/gate/index.d.ts.map +1 -0
- package/dist/testing/gate/index.js +632 -0
- package/dist/testing/gate/index.js.map +1 -0
- package/dist/testing/gate/lint/format.d.ts +7 -0
- package/dist/testing/gate/lint/format.d.ts.map +1 -0
- package/dist/testing/gate/lint/index.d.ts +30 -0
- package/dist/testing/gate/lint/index.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/extractor-coverage.d.ts +18 -0
- package/dist/testing/gate/lint/rules/extractor-coverage.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/index.d.ts +14 -0
- package/dist/testing/gate/lint/rules/index.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/keyword-classification.d.ts +19 -0
- package/dist/testing/gate/lint/rules/keyword-classification.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/keyword-coverage.d.ts +13 -0
- package/dist/testing/gate/lint/rules/keyword-coverage.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/marker-tokenization.d.ts +20 -0
- package/dist/testing/gate/lint/rules/marker-tokenization.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/position-ordering.d.ts +25 -0
- package/dist/testing/gate/lint/rules/position-ordering.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/renderer-coherence.d.ts +24 -0
- package/dist/testing/gate/lint/rules/renderer-coherence.d.ts.map +1 -0
- package/dist/testing/gate/lint/rules/schema-rules.d.ts +10 -0
- package/dist/testing/gate/lint/rules/schema-rules.d.ts.map +1 -0
- package/dist/testing/gate/lint/types.d.ts +74 -0
- package/dist/testing/gate/lint/types.d.ts.map +1 -0
- package/dist/testing/gate/round-trip.d.ts +163 -0
- package/dist/testing/gate/round-trip.d.ts.map +1 -0
- package/package.json +7 -3
- package/src/__test__/fixtures/sql-dsl.ts +11 -2
- package/src/__test__/sql-integration.test.ts +2 -2
- package/src/api/create-dsl-engine.test.ts +86 -0
- package/src/api/create-dsl.test.ts +120 -0
- package/src/api/create-dsl.ts +314 -39
- package/src/api/domain-registry.test.ts +70 -5
- package/src/api/domain-registry.ts +82 -1
- package/src/api/index.ts +1 -0
- package/src/api/reading.ts +29 -0
- package/src/api/translation-loss.test.ts +89 -0
- package/src/api/translation-loss.ts +128 -0
- package/src/core/index.ts +1 -0
- package/src/core/matching/confidence-model.ts +58 -0
- package/src/core/matching/index.ts +12 -0
- package/src/core/matching/match-skeleton.test.ts +179 -0
- package/src/core/matching/match-skeleton.ts +621 -0
- package/src/core/matching/stock-confidence.ts +238 -0
- package/src/core/matching/stock-reader.test.ts +181 -0
- package/src/core/matching/stock-reader.ts +186 -0
- package/src/core/matching/stock-reading.ts +681 -0
- package/src/core/matching/value-reader.ts +85 -0
- package/src/core/pattern-matching/pattern-matcher.ts +11 -876
- package/src/core/types.ts +48 -0
- package/src/generation/index.ts +2 -0
- package/src/generation/pattern-render.ts +378 -0
- package/src/generation/schema-patterns.test.ts +99 -0
- package/src/generation/schema-patterns.ts +962 -0
- package/src/testing/gate/index.ts +8 -0
- package/src/testing/gate/lint/format.ts +23 -0
- package/src/testing/gate/lint/index.ts +73 -0
- package/src/testing/gate/lint/lint.test.ts +131 -0
- package/src/testing/gate/lint/rules/extractor-coverage.ts +63 -0
- package/src/testing/gate/lint/rules/index.ts +37 -0
- package/src/testing/gate/lint/rules/keyword-classification.ts +52 -0
- package/src/testing/gate/lint/rules/keyword-coverage.ts +38 -0
- package/src/testing/gate/lint/rules/marker-tokenization.ts +129 -0
- package/src/testing/gate/lint/rules/position-ordering.ts +87 -0
- package/src/testing/gate/lint/rules/renderer-coherence.ts +100 -0
- package/src/testing/gate/lint/rules/schema-rules.ts +35 -0
- package/src/testing/gate/lint/types.ts +83 -0
- package/src/testing/gate/round-trip.test.ts +118 -0
- package/src/testing/gate/round-trip.ts +438 -0
package/src/api/create-dsl.ts
CHANGED
|
@@ -2,11 +2,25 @@
|
|
|
2
2
|
* Main API for creating multilingual DSLs
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
SemanticNode,
|
|
7
|
+
LanguageTokenizer,
|
|
8
|
+
LanguagePattern,
|
|
9
|
+
PatternMatchResult,
|
|
10
|
+
RolePatternToken,
|
|
11
|
+
TokenStream,
|
|
12
|
+
} from '../core/types';
|
|
13
|
+
import { extractRoleValue } from '../core/types';
|
|
6
14
|
import type { CommandSchema } from '../schema';
|
|
7
|
-
import { PatternMatcher
|
|
15
|
+
import { PatternMatcher } from '../core/pattern-matching';
|
|
16
|
+
import { MatchSkeleton, createStockReader, stockConfidenceModel } from '../core/matching';
|
|
8
17
|
import { generatePattern, type PatternGenLanguageProfile } from '../generation/pattern-generator';
|
|
9
18
|
import { createDomainRenderer, type DomainRenderFn } from '../generation/renderer';
|
|
19
|
+
import {
|
|
20
|
+
createSchemaPatternGenerator,
|
|
21
|
+
type SchemaPatternProfile,
|
|
22
|
+
} from '../generation/schema-patterns';
|
|
23
|
+
import { renderPattern, selectRenderPattern } from '../generation/pattern-render';
|
|
10
24
|
import { GrammarTransformer, type LanguageProfile as GrammarProfile } from '../grammar';
|
|
11
25
|
import {
|
|
12
26
|
type Dictionary,
|
|
@@ -16,6 +30,7 @@ import {
|
|
|
16
30
|
InMemoryProfileProvider,
|
|
17
31
|
} from '../interfaces';
|
|
18
32
|
import { isExplicitSyntax, parseExplicit } from '../ir';
|
|
33
|
+
import { readingDifference } from './reading';
|
|
19
34
|
import type { SchemaLookup } from '../ir';
|
|
20
35
|
|
|
21
36
|
/**
|
|
@@ -168,6 +183,20 @@ export interface DSLConfig {
|
|
|
168
183
|
|
|
169
184
|
/** Custom patterns to supplement generated ones */
|
|
170
185
|
readonly customPatterns?: LanguagePattern[];
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* What reads and writes the DSL (domains rethink Phase 4.7). `'classic'`, the
|
|
189
|
+
* default: framework's first pattern generator, matcher and schema renderer.
|
|
190
|
+
* `'core'`: the core semantic's hyperscript parser runs — patterns from
|
|
191
|
+
* `createSchemaPatternGenerator` (positions descending, as a domain's schemas
|
|
192
|
+
* read), matched by `MatchSkeleton` with the stock reader and confidence
|
|
193
|
+
* model, and rendered through those patterns where the domain's own renderer
|
|
194
|
+
* declines. Parsing's whole-input rule, the refusals, translation's
|
|
195
|
+
* read-back, compile, explicit syntax and extensions are the same on both.
|
|
196
|
+
* The domains move to `'core'` one at a time against their gate (Phase 5);
|
|
197
|
+
* `'classic'` is removed in a later major, once no domain uses it.
|
|
198
|
+
*/
|
|
199
|
+
readonly engine?: 'classic' | 'core';
|
|
171
200
|
}
|
|
172
201
|
|
|
173
202
|
/**
|
|
@@ -177,6 +206,8 @@ export interface ValidationResult {
|
|
|
177
206
|
readonly valid: boolean;
|
|
178
207
|
readonly node?: SemanticNode;
|
|
179
208
|
readonly errors?: string[];
|
|
209
|
+
/** Set when the input was refused for a known reason: `'unread-input'` (`UnreadInputError`). */
|
|
210
|
+
readonly errorCode?: string;
|
|
180
211
|
}
|
|
181
212
|
|
|
182
213
|
/**
|
|
@@ -186,6 +217,8 @@ export interface CompileResult {
|
|
|
186
217
|
readonly ok: boolean;
|
|
187
218
|
readonly code?: string;
|
|
188
219
|
readonly errors?: string[];
|
|
220
|
+
/** Set when the input was refused for a known reason: `'unread-input'` (`UnreadInputError`). */
|
|
221
|
+
readonly errorCode?: string;
|
|
189
222
|
readonly node?: SemanticNode;
|
|
190
223
|
readonly metadata?: {
|
|
191
224
|
readonly parser: string;
|
|
@@ -193,6 +226,55 @@ export interface CompileResult {
|
|
|
193
226
|
};
|
|
194
227
|
}
|
|
195
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Thrown by `parse` (and so `compile`, `validate`, `translate`) when the input reads as a command
|
|
231
|
+
* only by leaving some of it unread: `select name from users banana` is not `select name from users`.
|
|
232
|
+
* `reading` is what the longest such match read, `unread` the text after it.
|
|
233
|
+
*/
|
|
234
|
+
export class UnreadInputError extends Error {
|
|
235
|
+
readonly code = 'unread-input';
|
|
236
|
+
|
|
237
|
+
constructor(
|
|
238
|
+
readonly input: string,
|
|
239
|
+
readonly language: string,
|
|
240
|
+
readonly reading: SemanticNode,
|
|
241
|
+
readonly unread: string
|
|
242
|
+
) {
|
|
243
|
+
super(`"${input}" (${language}) reads as ${reading.action} only by leaving "${unread}" unread`);
|
|
244
|
+
this.name = 'UnreadInputError';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Options for `translate`. */
|
|
249
|
+
export interface TranslateOptions {
|
|
250
|
+
/**
|
|
251
|
+
* `'allow'` returns a translation that does not read back as the source instead of throwing
|
|
252
|
+
* `TranslationReadBackError`, as semantic's `translate` does with the same option.
|
|
253
|
+
*/
|
|
254
|
+
readonly lossy?: 'allow' | 'refuse';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Thrown by `translate` when its output, read in the target language, is not the source command:
|
|
259
|
+
* it does not read at all, or reads as another command, or with a role lost or changed. `partial` is
|
|
260
|
+
* what `translate` would have returned; `reading` what it reads as (null when it does not read).
|
|
261
|
+
*/
|
|
262
|
+
export class TranslationReadBackError extends Error {
|
|
263
|
+
readonly code = 'translation-does-not-read-back';
|
|
264
|
+
|
|
265
|
+
constructor(
|
|
266
|
+
readonly partial: string,
|
|
267
|
+
readonly from: string,
|
|
268
|
+
readonly to: string,
|
|
269
|
+
readonly source: SemanticNode,
|
|
270
|
+
readonly reading: SemanticNode | null,
|
|
271
|
+
readonly why: string
|
|
272
|
+
) {
|
|
273
|
+
super(`translation ${from} → ${to} does not read back as the source (${why}): "${partial}"`);
|
|
274
|
+
this.name = 'TranslationReadBackError';
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
196
278
|
/**
|
|
197
279
|
* Main multilingual DSL interface.
|
|
198
280
|
*/
|
|
@@ -208,7 +290,17 @@ export interface MultilingualDSL {
|
|
|
208
290
|
compile(input: string, language: string): CompileResult;
|
|
209
291
|
|
|
210
292
|
// Translation
|
|
211
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Parse in one language, render in another. The output must read back as the source command
|
|
295
|
+
* (the same action and role values, or the same compiled code); when it does not,
|
|
296
|
+
* `TranslationReadBackError`, unless `{ lossy: 'allow' }`.
|
|
297
|
+
*/
|
|
298
|
+
translate(
|
|
299
|
+
input: string,
|
|
300
|
+
fromLanguage: string,
|
|
301
|
+
toLanguage: string,
|
|
302
|
+
options?: TranslateOptions
|
|
303
|
+
): string;
|
|
212
304
|
|
|
213
305
|
/**
|
|
214
306
|
* Render a parsed node back to natural language.
|
|
@@ -239,7 +331,11 @@ class DSLRegistry {
|
|
|
239
331
|
* subset of the DSL's languages. A built-in command missing a keyword stays
|
|
240
332
|
* an error, since that is a domain-authoring mistake.
|
|
241
333
|
*/
|
|
242
|
-
constructor(
|
|
334
|
+
constructor(
|
|
335
|
+
config: DSLConfig,
|
|
336
|
+
partiallyTranslatedActions: ReadonlySet<string>,
|
|
337
|
+
private readonly generate: DSLEngine['generate']
|
|
338
|
+
) {
|
|
243
339
|
this.schemas = config.schemas;
|
|
244
340
|
this.partiallyTranslatedActions = partiallyTranslatedActions;
|
|
245
341
|
|
|
@@ -262,7 +358,7 @@ class DSLRegistry {
|
|
|
262
358
|
) {
|
|
263
359
|
continue; // this extension command was not translated into this language
|
|
264
360
|
}
|
|
265
|
-
const pattern =
|
|
361
|
+
const pattern = this.generate(schema, lang.patternProfile);
|
|
266
362
|
patterns.push(pattern);
|
|
267
363
|
}
|
|
268
364
|
|
|
@@ -287,7 +383,7 @@ class DSLRegistry {
|
|
|
287
383
|
*/
|
|
288
384
|
class MultilingualDSLImpl implements MultilingualDSL {
|
|
289
385
|
private registry: DSLRegistry;
|
|
290
|
-
private
|
|
386
|
+
private match: DSLEngine['match'];
|
|
291
387
|
private transformer: GrammarTransformer;
|
|
292
388
|
private profileProvider: ProfileProvider;
|
|
293
389
|
private codeGenerator?: CodeGenerator;
|
|
@@ -300,10 +396,11 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
300
396
|
config: DSLConfig,
|
|
301
397
|
registry: DSLRegistry,
|
|
302
398
|
transformer: GrammarTransformer,
|
|
303
|
-
profileProvider: ProfileProvider
|
|
399
|
+
profileProvider: ProfileProvider,
|
|
400
|
+
engine: DSLEngine
|
|
304
401
|
) {
|
|
305
402
|
this.registry = registry;
|
|
306
|
-
this.
|
|
403
|
+
this.match = engine.match;
|
|
307
404
|
this.transformer = transformer;
|
|
308
405
|
this.profileProvider = profileProvider;
|
|
309
406
|
if (config.codeGenerator) {
|
|
@@ -321,10 +418,7 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
321
418
|
// Last resort for render(): covers extension commands with no custom
|
|
322
419
|
// renderer, and any built-in action the domain renderer declines. Lazy, so
|
|
323
420
|
// a DSL that never renders pays nothing.
|
|
324
|
-
this.schemaFallbackRenderer =
|
|
325
|
-
schemas: config.schemas,
|
|
326
|
-
profiles: config.languages.map(l => l.patternProfile),
|
|
327
|
-
});
|
|
421
|
+
this.schemaFallbackRenderer = engine.fallbackRenderer(config, registry);
|
|
328
422
|
|
|
329
423
|
// Build SchemaLookup from config schemas for explicit syntax validation
|
|
330
424
|
const schemaMap = new Map(config.schemas.map(s => [s.action, s]));
|
|
@@ -361,35 +455,66 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
361
455
|
// Get patterns for language
|
|
362
456
|
const patterns = this.registry.getPatterns(language);
|
|
363
457
|
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
458
|
+
// Every pattern is tried, and only a match that reads the WHOLE input counts; of those, the
|
|
459
|
+
// highest priority wins, then the highest confidence, then the earliest pattern. The first match
|
|
460
|
+
// used to win however much it read, so a trailing word was dropped without a word (the domain
|
|
461
|
+
// gate measured 1,744 of 1,744 such inputs accepted) and an earlier, shorter pattern could
|
|
462
|
+
// shadow the one that reads everything.
|
|
463
|
+
const total = tokens.tokens.length;
|
|
464
|
+
let best: PatternMatchResult | null = null;
|
|
465
|
+
let longestPartial: PatternMatchResult | null = null;
|
|
370
466
|
for (const pattern of patterns) {
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
sourceLanguage: language,
|
|
380
|
-
sourceText: input,
|
|
381
|
-
patternId: pattern.id,
|
|
382
|
-
confidence: match.confidence,
|
|
383
|
-
},
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
return { node, confidence: match.confidence };
|
|
467
|
+
const mark = tokens.mark();
|
|
468
|
+
const match = this.match(tokens, pattern, language);
|
|
469
|
+
tokens.reset(mark);
|
|
470
|
+
if (!match) continue;
|
|
471
|
+
if (match.consumedTokens < total) {
|
|
472
|
+
if (!longestPartial || match.consumedTokens > longestPartial.consumedTokens)
|
|
473
|
+
longestPartial = match;
|
|
474
|
+
continue;
|
|
387
475
|
}
|
|
476
|
+
if (
|
|
477
|
+
!best ||
|
|
478
|
+
match.pattern.priority > best.pattern.priority ||
|
|
479
|
+
(match.pattern.priority === best.pattern.priority &&
|
|
480
|
+
match.confidence > best.confidence + 0.001)
|
|
481
|
+
)
|
|
482
|
+
best = match;
|
|
388
483
|
}
|
|
389
484
|
|
|
485
|
+
if (best) {
|
|
486
|
+
const node = this.nodeOf(best, input, language);
|
|
487
|
+
return { node, confidence: best.confidence };
|
|
488
|
+
}
|
|
489
|
+
if (longestPartial) {
|
|
490
|
+
const unread = tokens.tokens
|
|
491
|
+
.slice(longestPartial.consumedTokens)
|
|
492
|
+
.map(t => t.value)
|
|
493
|
+
.join(' ');
|
|
494
|
+
throw new UnreadInputError(
|
|
495
|
+
input,
|
|
496
|
+
language,
|
|
497
|
+
this.nodeOf(longestPartial, input, language),
|
|
498
|
+
unread
|
|
499
|
+
);
|
|
500
|
+
}
|
|
390
501
|
throw new Error(`No pattern matched for input: ${input}`);
|
|
391
502
|
}
|
|
392
503
|
|
|
504
|
+
private nodeOf(match: PatternMatchResult, input: string, language: string): SemanticNode {
|
|
505
|
+
return {
|
|
506
|
+
kind: 'command',
|
|
507
|
+
action: match.pattern.command,
|
|
508
|
+
roles: match.captured,
|
|
509
|
+
metadata: {
|
|
510
|
+
sourceLanguage: language,
|
|
511
|
+
sourceText: input,
|
|
512
|
+
patternId: match.pattern.id,
|
|
513
|
+
confidence: match.confidence,
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
393
518
|
validate(input: string, language: string): ValidationResult {
|
|
394
519
|
try {
|
|
395
520
|
const result = this.parseWithConfidence(input, language);
|
|
@@ -401,11 +526,17 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
401
526
|
return {
|
|
402
527
|
valid: false,
|
|
403
528
|
errors: [error instanceof Error ? error.message : String(error)],
|
|
529
|
+
...(error instanceof UnreadInputError ? { errorCode: error.code } : {}),
|
|
404
530
|
};
|
|
405
531
|
}
|
|
406
532
|
}
|
|
407
533
|
|
|
408
|
-
translate(
|
|
534
|
+
translate(
|
|
535
|
+
input: string,
|
|
536
|
+
fromLanguage: string,
|
|
537
|
+
toLanguage: string,
|
|
538
|
+
options?: TranslateOptions
|
|
539
|
+
): string {
|
|
409
540
|
// Explicit syntax is language-agnostic — return unchanged
|
|
410
541
|
if (isExplicitSyntax(input)) return input;
|
|
411
542
|
|
|
@@ -432,12 +563,18 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
432
563
|
const rendered = this.registry.getSupportedLanguages().includes(toLanguage)
|
|
433
564
|
? this.render(node, toLanguage)
|
|
434
565
|
: null;
|
|
435
|
-
if (rendered != null) return rendered;
|
|
566
|
+
if (rendered != null) return this.readsBack(rendered, node, fromLanguage, toLanguage, options);
|
|
436
567
|
|
|
437
568
|
// render() covered nothing for this action/language pair; fall back to the
|
|
438
569
|
// grammar transformer when profiles exist, otherwise name both remedies.
|
|
439
570
|
if (this.hasGrammarProfile(fromLanguage) && this.hasGrammarProfile(toLanguage)) {
|
|
440
|
-
return this.
|
|
571
|
+
return this.readsBack(
|
|
572
|
+
this.transformer.transform(input, fromLanguage, toLanguage),
|
|
573
|
+
node,
|
|
574
|
+
fromLanguage,
|
|
575
|
+
toLanguage,
|
|
576
|
+
options
|
|
577
|
+
);
|
|
441
578
|
}
|
|
442
579
|
throw new Error(
|
|
443
580
|
`translate() could not render action "${node.action}" in language "${toLanguage}": no ` +
|
|
@@ -448,6 +585,40 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
448
585
|
);
|
|
449
586
|
}
|
|
450
587
|
|
|
588
|
+
/**
|
|
589
|
+
* `rendered`, read in `to`, must be `source`: the same action and role values, or, when the DSL
|
|
590
|
+
* compiles, the same code (a parser may keep a translated word its generator normalizes: bdd ja
|
|
591
|
+
* `表示` is `visible`). The rule is the domain gate's (lokascript-domains). A translation used to be
|
|
592
|
+
* returned whatever it read as: ja/ko bdd `select` dropped its value, and nothing said so.
|
|
593
|
+
*/
|
|
594
|
+
private readsBack(
|
|
595
|
+
rendered: string,
|
|
596
|
+
source: SemanticNode,
|
|
597
|
+
from: string,
|
|
598
|
+
to: string,
|
|
599
|
+
options?: TranslateOptions
|
|
600
|
+
): string {
|
|
601
|
+
if (options?.lossy === 'allow') return rendered;
|
|
602
|
+
let back: SemanticNode;
|
|
603
|
+
try {
|
|
604
|
+
back = this.parseWithConfidence(rendered, to).node;
|
|
605
|
+
} catch (error) {
|
|
606
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
607
|
+
throw new TranslationReadBackError(rendered, from, to, source, null, why);
|
|
608
|
+
}
|
|
609
|
+
const difference = readingDifference(source, back);
|
|
610
|
+
if (!difference) return rendered;
|
|
611
|
+
if (this.codeGenerator) {
|
|
612
|
+
try {
|
|
613
|
+
if (this.codeGenerator.generate(back) === this.codeGenerator.generate(source))
|
|
614
|
+
return rendered;
|
|
615
|
+
} catch {
|
|
616
|
+
// a generator that cannot compile one of them does not vouch for either
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
throw new TranslationReadBackError(rendered, from, to, source, back, difference);
|
|
620
|
+
}
|
|
621
|
+
|
|
451
622
|
private hasGrammarProfile(language: string): boolean {
|
|
452
623
|
return this.profileProvider.getProfile(language) != null;
|
|
453
624
|
}
|
|
@@ -477,6 +648,7 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
477
648
|
return {
|
|
478
649
|
ok: false,
|
|
479
650
|
errors: [error instanceof Error ? error.message : String(error)],
|
|
651
|
+
...(error instanceof UnreadInputError ? { errorCode: error.code } : {}),
|
|
480
652
|
};
|
|
481
653
|
}
|
|
482
654
|
}
|
|
@@ -678,6 +850,109 @@ export function createMultilingualDSL(config: DSLConfig): MultilingualDSL {
|
|
|
678
850
|
});
|
|
679
851
|
|
|
680
852
|
// Create registry and implementation
|
|
681
|
-
const
|
|
682
|
-
|
|
853
|
+
const engine = engineFor(config);
|
|
854
|
+
const registry = new DSLRegistry(effectiveConfig, extensionActions, engine.generate);
|
|
855
|
+
return new MultilingualDSLImpl(effectiveConfig, registry, transformer, profileProvider, engine);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// =============================================================================
|
|
859
|
+
// The engines: how a DSL turns its schemas into patterns and reads with them
|
|
860
|
+
// =============================================================================
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* What differs between the engines {@link DSLConfig.engine} names: how each
|
|
864
|
+
* command's pattern is generated, how input is matched against it, and the
|
|
865
|
+
* renderer `render()` falls back to. Parsing's whole-input rule, refusals,
|
|
866
|
+
* translation's read-back, compile, explicit syntax and extensions are shared.
|
|
867
|
+
*/
|
|
868
|
+
interface DSLEngine {
|
|
869
|
+
generate(schema: CommandSchema, profile: PatternGenLanguageProfile): LanguagePattern;
|
|
870
|
+
match(tokens: TokenStream, pattern: LanguagePattern, language: string): PatternMatchResult | null;
|
|
871
|
+
fallbackRenderer(config: DSLConfig, registry: DSLRegistry): DomainRenderFn;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* The engine `config.engine` names. An unknown name throws rather than reading
|
|
876
|
+
* with the default: a domain that misspells `'core'` would otherwise measure
|
|
877
|
+
* the classic engine against its gate and take the result for the core's.
|
|
878
|
+
*/
|
|
879
|
+
function engineFor(config: DSLConfig): DSLEngine {
|
|
880
|
+
const name = config.engine ?? 'classic';
|
|
881
|
+
if (name === 'classic') return classicEngine();
|
|
882
|
+
if (name === 'core') return coreEngine();
|
|
883
|
+
throw new Error(`Unknown DSL engine "${String(name)}" (expected "classic" or "core")`);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/** framework's first generator and matcher, and its schema renderer (`engine: 'classic'`). */
|
|
887
|
+
function classicEngine(): DSLEngine {
|
|
888
|
+
const matcher = new PatternMatcher();
|
|
889
|
+
return {
|
|
890
|
+
generate: (schema, profile) => generatePattern(schema, profile),
|
|
891
|
+
match: (tokens, pattern, language) => matcher.matchPattern(tokens, pattern, { code: language }),
|
|
892
|
+
fallbackRenderer: config =>
|
|
893
|
+
createDomainRenderer({
|
|
894
|
+
schemas: config.schemas,
|
|
895
|
+
profiles: config.languages.map(l => l.patternProfile),
|
|
896
|
+
}),
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/**
|
|
901
|
+
* A domain profile as the schema-pattern generator reads it: every role marker
|
|
902
|
+
* with a side (the word order's default where the profile names none).
|
|
903
|
+
*/
|
|
904
|
+
function schemaProfileOf(profile: PatternGenLanguageProfile): SchemaPatternProfile {
|
|
905
|
+
const side = profile.wordOrder === 'SOV' ? 'after' : 'before';
|
|
906
|
+
const roleMarkers: Record<string, NonNullable<SchemaPatternProfile['roleMarkers'][string]>> = {};
|
|
907
|
+
for (const [role, marker] of Object.entries(profile.roleMarkers ?? {})) {
|
|
908
|
+
roleMarkers[role] = {
|
|
909
|
+
primary: marker.primary,
|
|
910
|
+
...(marker.alternatives ? { alternatives: marker.alternatives } : {}),
|
|
911
|
+
position: marker.position ?? side,
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
code: profile.code,
|
|
916
|
+
wordOrder: profile.wordOrder,
|
|
917
|
+
keywords: profile.keywords,
|
|
918
|
+
roleMarkers,
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* The core (domains rethink Phase 4, `engine: 'core'`): the schema-pattern
|
|
924
|
+
* generator, the skeleton, the stock reader.
|
|
925
|
+
*/
|
|
926
|
+
function coreEngine(): DSLEngine {
|
|
927
|
+
const generator = createSchemaPatternGenerator({ positionOrder: 'descending' });
|
|
928
|
+
const matcher = new MatchSkeleton(createStockReader(), stockConfidenceModel);
|
|
929
|
+
return {
|
|
930
|
+
generate: (schema, profile) => generator.generatePattern(schema, schemaProfileOf(profile)),
|
|
931
|
+
match: (tokens, pattern) => matcher.matchPattern(tokens, pattern),
|
|
932
|
+
fallbackRenderer: (config, registry) => {
|
|
933
|
+
const schemas = new Map(config.schemas.map(s => [s.action, s]));
|
|
934
|
+
const hooks = {
|
|
935
|
+
hasRealRole: (node: SemanticNode, role: string) => {
|
|
936
|
+
const value = node.roles.get(role) as { implicit?: unknown } | undefined;
|
|
937
|
+
return value !== undefined && value.implicit !== true;
|
|
938
|
+
},
|
|
939
|
+
writeRole: (token: RolePatternToken, node: SemanticNode) => {
|
|
940
|
+
if (!node.roles.has(token.role)) return null;
|
|
941
|
+
let value = extractRoleValue(node, token.role);
|
|
942
|
+
const spec = schemas.get(node.action)?.roles.find(r => r.role === token.role);
|
|
943
|
+
if (spec?.quoteMultiword && /\s/.test(value) && !/^(["']).*\1$/.test(value)) {
|
|
944
|
+
value = `"${value}"`;
|
|
945
|
+
}
|
|
946
|
+
return value || null;
|
|
947
|
+
},
|
|
948
|
+
};
|
|
949
|
+
return (node, language) => {
|
|
950
|
+
const patterns = registry.getPatterns(language).filter(p => p.command === node.action);
|
|
951
|
+
const pattern = selectRenderPattern(node, patterns, hooks);
|
|
952
|
+
if (!pattern) return null;
|
|
953
|
+
const text = renderPattern(node, pattern, hooks).join(' ');
|
|
954
|
+
return text || null;
|
|
955
|
+
};
|
|
956
|
+
},
|
|
957
|
+
};
|
|
683
958
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
2
|
import { DomainRegistry } from './domain-registry';
|
|
3
3
|
import type { DomainDescriptor, MCPToolDefinition } from './domain-registry';
|
|
4
|
-
import type
|
|
4
|
+
import { UnreadInputError, type MultilingualDSL } from './create-dsl';
|
|
5
5
|
import type { SemanticNode } from '../core/types';
|
|
6
6
|
|
|
7
7
|
// =============================================================================
|
|
@@ -11,10 +11,19 @@ import type { SemanticNode } from '../core/types';
|
|
|
11
11
|
function createMockDSL(): MultilingualDSL {
|
|
12
12
|
return {
|
|
13
13
|
parse(input: string, language: string): SemanticNode {
|
|
14
|
+
// A rendering is `[<lang>] <source>`: read the source back out of it.
|
|
15
|
+
const source = input.replace(/^\[\w+\] /, '');
|
|
16
|
+
if (source.includes(' zzqx'))
|
|
17
|
+
throw new UnreadInputError(
|
|
18
|
+
input,
|
|
19
|
+
language,
|
|
20
|
+
this.parse(source.replace(' zzqx', ''), language),
|
|
21
|
+
'zzqx'
|
|
22
|
+
);
|
|
14
23
|
return {
|
|
15
24
|
kind: 'command',
|
|
16
|
-
action:
|
|
17
|
-
roles: new Map([['raw', { type: 'expression' as const, raw:
|
|
25
|
+
action: source.split(/\s+/)[0].toLowerCase(),
|
|
26
|
+
roles: new Map([['raw', { type: 'expression' as const, raw: source }]]),
|
|
18
27
|
metadata: { sourceLanguage: language },
|
|
19
28
|
};
|
|
20
29
|
},
|
|
@@ -33,7 +42,9 @@ function createMockDSL(): MultilingualDSL {
|
|
|
33
42
|
try {
|
|
34
43
|
const node = this.parse(input, language);
|
|
35
44
|
return { ok: true, code: `compiled(${input})`, node };
|
|
36
|
-
} catch {
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof UnreadInputError)
|
|
47
|
+
return { ok: false, errors: [error.message], errorCode: error.code };
|
|
37
48
|
return { ok: false, errors: ['Compile failed'] };
|
|
38
49
|
}
|
|
39
50
|
},
|
|
@@ -46,7 +57,14 @@ function createMockDSL(): MultilingualDSL {
|
|
|
46
57
|
};
|
|
47
58
|
}
|
|
48
59
|
|
|
60
|
+
/** Writes the whole source, so a rendering reads back. */
|
|
49
61
|
function mockRenderer(node: SemanticNode, language: string): string {
|
|
62
|
+
const raw = (node.roles.get('raw') as { raw?: string } | undefined)?.raw ?? node.action;
|
|
63
|
+
return `[${language}] ${raw}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Writes the action alone: `select name` comes out as `select`, which does not read back. */
|
|
67
|
+
function lossyRenderer(node: SemanticNode, language: string): string {
|
|
50
68
|
return `[${language}] ${node.action}`;
|
|
51
69
|
}
|
|
52
70
|
|
|
@@ -236,6 +254,19 @@ describe('DomainRegistry', () => {
|
|
|
236
254
|
expect(data.ok).toBe(true);
|
|
237
255
|
expect(data.code).toBe('compiled(select name)');
|
|
238
256
|
});
|
|
257
|
+
|
|
258
|
+
it('reports input left unread as an UNREAD_INPUT diagnostic', async () => {
|
|
259
|
+
registry.register(createTestDescriptor());
|
|
260
|
+
const result = await registry.handleToolCall('compile_test', {
|
|
261
|
+
query: 'select name zzqx',
|
|
262
|
+
language: 'en',
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const data = JSON.parse(result!.content[0].text);
|
|
266
|
+
expect(data.ok).toBe(false);
|
|
267
|
+
expect(data.diagnostics[0].code).toBe('UNREAD_INPUT');
|
|
268
|
+
expect(data.diagnostics[0].message).toMatch(/zzqx/);
|
|
269
|
+
});
|
|
239
270
|
});
|
|
240
271
|
|
|
241
272
|
describe('validate', () => {
|
|
@@ -263,10 +294,44 @@ describe('DomainRegistry', () => {
|
|
|
263
294
|
const data = JSON.parse(result!.content[0].text);
|
|
264
295
|
expect(data.input.query).toBe('select name');
|
|
265
296
|
expect(data.input.language).toBe('en');
|
|
266
|
-
expect(data.
|
|
297
|
+
expect(data.ok).toBe(true);
|
|
298
|
+
expect(data.rendered.text).toBe('[ja] select name');
|
|
267
299
|
expect(data.rendered.language).toBe('ja');
|
|
268
300
|
});
|
|
269
301
|
|
|
302
|
+
it('refuses a rendering that does not read back, with its text under partial', async () => {
|
|
303
|
+
registry.register(createTestDescriptor({ getRenderer: () => lossyRenderer }));
|
|
304
|
+
const result = await registry.handleToolCall('translate_test', {
|
|
305
|
+
query: 'select name',
|
|
306
|
+
from: 'en',
|
|
307
|
+
to: 'ja',
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(result!.isError).toBe(true);
|
|
311
|
+
const data = JSON.parse(result!.content[0].text);
|
|
312
|
+
expect(data.ok).toBe(false);
|
|
313
|
+
expect(data.partial).toBe('[ja] select');
|
|
314
|
+
expect(data.rendered).toBeUndefined();
|
|
315
|
+
expect(data.diagnostics[0].code).toBe('TRANSLATION_DOES_NOT_READ_BACK');
|
|
316
|
+
expect(data.diagnostics[0].message).toMatch(/raw: select name → select/);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('refuses input left unread, naming it', async () => {
|
|
320
|
+
registry.register(createTestDescriptor());
|
|
321
|
+
const result = await registry.handleToolCall('translate_test', {
|
|
322
|
+
query: 'select name zzqx',
|
|
323
|
+
from: 'en',
|
|
324
|
+
to: 'ja',
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(result!.isError).toBe(true);
|
|
328
|
+
const data = JSON.parse(result!.content[0].text);
|
|
329
|
+
expect(data.ok).toBe(false);
|
|
330
|
+
expect(data.unread).toBe('zzqx');
|
|
331
|
+
expect(data.reading.action).toBe('select');
|
|
332
|
+
expect(data.diagnostics[0].code).toBe('UNREAD_INPUT');
|
|
333
|
+
});
|
|
334
|
+
|
|
270
335
|
it('returns error when from/to missing', async () => {
|
|
271
336
|
registry.register(createTestDescriptor());
|
|
272
337
|
|