@lokascript/framework 4.6.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 +87 -1
- package/dist/api/create-dsl.d.ts +13 -0
- package/dist/api/create-dsl.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 +1756 -710
- package/dist/api/index.js.map +1 -1
- 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 +1979 -907
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1961 -906
- 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 +6 -2
- package/src/__test__/fixtures/sql-dsl.ts +8 -2
- package/src/api/create-dsl-engine.test.ts +86 -0
- package/src/api/create-dsl.ts +141 -18
- package/src/api/index.ts +1 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lokascript/framework",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.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",
|
|
@@ -56,6 +56,10 @@
|
|
|
56
56
|
"types": "./dist/testing/index.d.ts",
|
|
57
57
|
"import": "./dist/testing/index.js"
|
|
58
58
|
},
|
|
59
|
+
"./testing/gate": {
|
|
60
|
+
"types": "./dist/testing/gate/index.d.ts",
|
|
61
|
+
"import": "./dist/testing/gate/index.js"
|
|
62
|
+
},
|
|
59
63
|
"./ir": {
|
|
60
64
|
"types": "./dist/ir/index.d.ts",
|
|
61
65
|
"import": "./dist/ir/index.js"
|
|
@@ -96,7 +100,7 @@
|
|
|
96
100
|
"author": "LokaScript Contributors",
|
|
97
101
|
"license": "MIT",
|
|
98
102
|
"dependencies": {
|
|
99
|
-
"@lokascript/intent": "^4.
|
|
103
|
+
"@lokascript/intent": "^4.7.0"
|
|
100
104
|
},
|
|
101
105
|
"devDependencies": {
|
|
102
106
|
"@lokascript/domains": "^4.0.0",
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
getDefaultExtractors,
|
|
14
14
|
type SemanticNode,
|
|
15
15
|
type MultilingualDSL,
|
|
16
|
+
type DSLConfig,
|
|
16
17
|
} from '../../index';
|
|
17
18
|
|
|
18
19
|
// =============================================================================
|
|
@@ -168,7 +169,12 @@ function generateSQL(node: SemanticNode): string {
|
|
|
168
169
|
// =============================================================================
|
|
169
170
|
|
|
170
171
|
export function createSQLDSL(): MultilingualDSL {
|
|
171
|
-
return createMultilingualDSL(
|
|
172
|
+
return createMultilingualDSL(sqlDSLConfig());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The fixture's config, for a test that builds the DSL with another factory. */
|
|
176
|
+
export function sqlDSLConfig(): DSLConfig {
|
|
177
|
+
return {
|
|
172
178
|
name: 'SQL DSL Test Fixture',
|
|
173
179
|
schemas: [selectCommand],
|
|
174
180
|
languages: [
|
|
@@ -190,7 +196,7 @@ export function createSQLDSL(): MultilingualDSL {
|
|
|
190
196
|
codeGenerator: {
|
|
191
197
|
generate: generateSQL,
|
|
192
198
|
},
|
|
193
|
-
}
|
|
199
|
+
};
|
|
194
200
|
}
|
|
195
201
|
|
|
196
202
|
// =============================================================================
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `engine: 'core'` (domains rethink Phase 4.7): the same config and DSL as the
|
|
3
|
+
* classic engine, read by the core. framework's SQL fixture reads, compiles and
|
|
4
|
+
* translates as it does on the classic engine, refuses what it does not read,
|
|
5
|
+
* and renders through its own patterns where no renderer is configured.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import { createMultilingualDSL, UnreadInputError, type DSLConfig } from './create-dsl';
|
|
9
|
+
import { sqlDSLConfig, testQueries, expectedSQL } from '../__test__/fixtures/sql-dsl';
|
|
10
|
+
import type { SemanticNode } from '../core/types';
|
|
11
|
+
import { generatePattern } from '../generation/pattern-generator';
|
|
12
|
+
import { createSchemaPatternGenerator } from '../generation/schema-patterns';
|
|
13
|
+
|
|
14
|
+
// Pass-through spies. On the SQL fixture the two engines read, compile and render alike, so
|
|
15
|
+
// these are what show which engine a DSL was built on.
|
|
16
|
+
vi.mock('../generation/pattern-generator', async importOriginal => {
|
|
17
|
+
const actual = await importOriginal<typeof import('../generation/pattern-generator')>();
|
|
18
|
+
return { ...actual, generatePattern: vi.fn(actual.generatePattern) };
|
|
19
|
+
});
|
|
20
|
+
vi.mock('../generation/schema-patterns', async importOriginal => {
|
|
21
|
+
const actual = await importOriginal<typeof import('../generation/schema-patterns')>();
|
|
22
|
+
return { ...actual, createSchemaPatternGenerator: vi.fn(actual.createSchemaPatternGenerator) };
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const core = createMultilingualDSL({ ...sqlDSLConfig(), engine: 'core' });
|
|
26
|
+
const classic = createMultilingualDSL(sqlDSLConfig());
|
|
27
|
+
|
|
28
|
+
const reading = (node: SemanticNode) =>
|
|
29
|
+
[
|
|
30
|
+
node.action,
|
|
31
|
+
...[...node.roles].map(
|
|
32
|
+
([role, v]) =>
|
|
33
|
+
`${role}=${String((v as { value?: unknown; raw?: unknown }).value ?? (v as { raw?: unknown }).raw)}`
|
|
34
|
+
),
|
|
35
|
+
].join(' ');
|
|
36
|
+
|
|
37
|
+
describe("createMultilingualDSL with engine: 'core'", () => {
|
|
38
|
+
const queries = [
|
|
39
|
+
['en', testQueries.english.simple, expectedSQL.simple],
|
|
40
|
+
['en', testQueries.english.withWhere, expectedSQL.withWhere],
|
|
41
|
+
['es', testQueries.spanish.simple, 'SELECT nombre FROM usuarios'],
|
|
42
|
+
['es', testQueries.spanish.withWhere, 'SELECT nombre FROM usuarios WHERE edad > 18'],
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
it.each(queries)('reads and compiles %s `%s` as the classic engine does', (lang, input, sql) => {
|
|
46
|
+
expect(reading(core.parse(input, lang))).toBe(reading(classic.parse(input, lang)));
|
|
47
|
+
expect(core.compile(input, lang).code).toBe(sql);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it.each(queries)('translates %s `%s` as the classic engine does', (lang, input) => {
|
|
51
|
+
const to = lang === 'en' ? 'es' : 'en';
|
|
52
|
+
expect(core.translate(input, lang, to)).toBe(classic.translate(input, lang, to));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('refuses input it does not read', () => {
|
|
56
|
+
expect(() => core.parse('select name from users banana', 'en')).toThrow(UnreadInputError);
|
|
57
|
+
expect(core.validate('select name from users banana', 'en')).toMatchObject({
|
|
58
|
+
valid: false,
|
|
59
|
+
errorCode: 'unread-input',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('renders through its patterns, in each language, what it reads back', () => {
|
|
64
|
+
const node = core.parse(testQueries.english.withWhere, 'en');
|
|
65
|
+
expect(core.render?.(node, 'en')).toBe('select name from users where age > 18');
|
|
66
|
+
expect(core.render?.(node, 'es')).toBe('seleccionar name de users donde age > 18');
|
|
67
|
+
expect(reading(core.parse(core.render!(node, 'es')!, 'es'))).toBe(reading(node));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it.each([
|
|
71
|
+
['core', { engine: 'core' }],
|
|
72
|
+
['classic', { engine: 'classic' }],
|
|
73
|
+
['classic', {}],
|
|
74
|
+
] as const)('builds on the %s engine with %o', (expected, option) => {
|
|
75
|
+
vi.mocked(generatePattern).mockClear();
|
|
76
|
+
vi.mocked(createSchemaPatternGenerator).mockClear();
|
|
77
|
+
createMultilingualDSL({ ...sqlDSLConfig(), ...option }).parse(testQueries.english.simple, 'en');
|
|
78
|
+
expect(vi.mocked(createSchemaPatternGenerator).mock.calls.length > 0).toBe(expected === 'core');
|
|
79
|
+
expect(vi.mocked(generatePattern).mock.calls.length > 0).toBe(expected === 'classic');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('throws on an engine it does not know, rather than reading with the classic one', () => {
|
|
83
|
+
const misspelled = { ...sqlDSLConfig(), engine: 'cor' } as unknown as DSLConfig;
|
|
84
|
+
expect(() => createMultilingualDSL(misspelled)).toThrow('Unknown DSL engine "cor"');
|
|
85
|
+
});
|
|
86
|
+
});
|
package/src/api/create-dsl.ts
CHANGED
|
@@ -7,11 +7,20 @@ import type {
|
|
|
7
7
|
LanguageTokenizer,
|
|
8
8
|
LanguagePattern,
|
|
9
9
|
PatternMatchResult,
|
|
10
|
+
RolePatternToken,
|
|
11
|
+
TokenStream,
|
|
10
12
|
} from '../core/types';
|
|
13
|
+
import { extractRoleValue } from '../core/types';
|
|
11
14
|
import type { CommandSchema } from '../schema';
|
|
12
|
-
import { PatternMatcher
|
|
15
|
+
import { PatternMatcher } from '../core/pattern-matching';
|
|
16
|
+
import { MatchSkeleton, createStockReader, stockConfidenceModel } from '../core/matching';
|
|
13
17
|
import { generatePattern, type PatternGenLanguageProfile } from '../generation/pattern-generator';
|
|
14
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';
|
|
15
24
|
import { GrammarTransformer, type LanguageProfile as GrammarProfile } from '../grammar';
|
|
16
25
|
import {
|
|
17
26
|
type Dictionary,
|
|
@@ -174,6 +183,20 @@ export interface DSLConfig {
|
|
|
174
183
|
|
|
175
184
|
/** Custom patterns to supplement generated ones */
|
|
176
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';
|
|
177
200
|
}
|
|
178
201
|
|
|
179
202
|
/**
|
|
@@ -308,7 +331,11 @@ class DSLRegistry {
|
|
|
308
331
|
* subset of the DSL's languages. A built-in command missing a keyword stays
|
|
309
332
|
* an error, since that is a domain-authoring mistake.
|
|
310
333
|
*/
|
|
311
|
-
constructor(
|
|
334
|
+
constructor(
|
|
335
|
+
config: DSLConfig,
|
|
336
|
+
partiallyTranslatedActions: ReadonlySet<string>,
|
|
337
|
+
private readonly generate: DSLEngine['generate']
|
|
338
|
+
) {
|
|
312
339
|
this.schemas = config.schemas;
|
|
313
340
|
this.partiallyTranslatedActions = partiallyTranslatedActions;
|
|
314
341
|
|
|
@@ -331,7 +358,7 @@ class DSLRegistry {
|
|
|
331
358
|
) {
|
|
332
359
|
continue; // this extension command was not translated into this language
|
|
333
360
|
}
|
|
334
|
-
const pattern =
|
|
361
|
+
const pattern = this.generate(schema, lang.patternProfile);
|
|
335
362
|
patterns.push(pattern);
|
|
336
363
|
}
|
|
337
364
|
|
|
@@ -356,7 +383,7 @@ class DSLRegistry {
|
|
|
356
383
|
*/
|
|
357
384
|
class MultilingualDSLImpl implements MultilingualDSL {
|
|
358
385
|
private registry: DSLRegistry;
|
|
359
|
-
private
|
|
386
|
+
private match: DSLEngine['match'];
|
|
360
387
|
private transformer: GrammarTransformer;
|
|
361
388
|
private profileProvider: ProfileProvider;
|
|
362
389
|
private codeGenerator?: CodeGenerator;
|
|
@@ -369,10 +396,11 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
369
396
|
config: DSLConfig,
|
|
370
397
|
registry: DSLRegistry,
|
|
371
398
|
transformer: GrammarTransformer,
|
|
372
|
-
profileProvider: ProfileProvider
|
|
399
|
+
profileProvider: ProfileProvider,
|
|
400
|
+
engine: DSLEngine
|
|
373
401
|
) {
|
|
374
402
|
this.registry = registry;
|
|
375
|
-
this.
|
|
403
|
+
this.match = engine.match;
|
|
376
404
|
this.transformer = transformer;
|
|
377
405
|
this.profileProvider = profileProvider;
|
|
378
406
|
if (config.codeGenerator) {
|
|
@@ -390,10 +418,7 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
390
418
|
// Last resort for render(): covers extension commands with no custom
|
|
391
419
|
// renderer, and any built-in action the domain renderer declines. Lazy, so
|
|
392
420
|
// a DSL that never renders pays nothing.
|
|
393
|
-
this.schemaFallbackRenderer =
|
|
394
|
-
schemas: config.schemas,
|
|
395
|
-
profiles: config.languages.map(l => l.patternProfile),
|
|
396
|
-
});
|
|
421
|
+
this.schemaFallbackRenderer = engine.fallbackRenderer(config, registry);
|
|
397
422
|
|
|
398
423
|
// Build SchemaLookup from config schemas for explicit syntax validation
|
|
399
424
|
const schemaMap = new Map(config.schemas.map(s => [s.action, s]));
|
|
@@ -430,11 +455,6 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
430
455
|
// Get patterns for language
|
|
431
456
|
const patterns = this.registry.getPatterns(language);
|
|
432
457
|
|
|
433
|
-
// Create profile for matcher
|
|
434
|
-
const profile: PatternMatcherProfile = {
|
|
435
|
-
code: language,
|
|
436
|
-
};
|
|
437
|
-
|
|
438
458
|
// Every pattern is tried, and only a match that reads the WHOLE input counts; of those, the
|
|
439
459
|
// highest priority wins, then the highest confidence, then the earliest pattern. The first match
|
|
440
460
|
// used to win however much it read, so a trailing word was dropped without a word (the domain
|
|
@@ -445,7 +465,7 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
445
465
|
let longestPartial: PatternMatchResult | null = null;
|
|
446
466
|
for (const pattern of patterns) {
|
|
447
467
|
const mark = tokens.mark();
|
|
448
|
-
const match = this.
|
|
468
|
+
const match = this.match(tokens, pattern, language);
|
|
449
469
|
tokens.reset(mark);
|
|
450
470
|
if (!match) continue;
|
|
451
471
|
if (match.consumedTokens < total) {
|
|
@@ -830,6 +850,109 @@ export function createMultilingualDSL(config: DSLConfig): MultilingualDSL {
|
|
|
830
850
|
});
|
|
831
851
|
|
|
832
852
|
// Create registry and implementation
|
|
833
|
-
const
|
|
834
|
-
|
|
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
|
+
};
|
|
835
958
|
}
|
package/src/api/index.ts
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The translation-loss checks, with rules of a toy grammar: each check in its
|
|
3
|
+
* place in the order, and the error a refusing translation throws.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import {
|
|
7
|
+
findTranslationLoss,
|
|
8
|
+
LossyTranslationError,
|
|
9
|
+
unconsumedSpans,
|
|
10
|
+
type TranslationLossRules,
|
|
11
|
+
} from './translation-loss';
|
|
12
|
+
|
|
13
|
+
interface Node {
|
|
14
|
+
readonly action: string;
|
|
15
|
+
readonly diagnostics?: ReadonlyArray<{ readonly code?: string; readonly message: string }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const unread = (text: string): Node => ({
|
|
19
|
+
action: 'go',
|
|
20
|
+
diagnostics: [{ code: 'unconsumed-input', message: `input not read: "${text}"` }],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const rules: TranslationLossRules<Node> = {
|
|
24
|
+
unwritable: node => (node.action === 'legacy' ? ['legacy'] : []),
|
|
25
|
+
differences: [(written, back) => (written.action === back.action ? [] : [`-${written.action}`])],
|
|
26
|
+
missingValues: (input, output) => (input.includes('#a') && !output.includes('#a') ? ['#a'] : []),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
describe('findTranslationLoss', () => {
|
|
30
|
+
const ok: Node = { action: 'go' };
|
|
31
|
+
|
|
32
|
+
it('finds nothing in a translation that reads back with its values', () => {
|
|
33
|
+
expect(findTranslationLoss('go #a', ok, 've #a', () => ok, rules)).toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('checks in order: truncation, core-only, read-back, invariant', () => {
|
|
37
|
+
expect(findTranslationLoss('go #a x', unread('x'), '', () => ok, rules)).toEqual({
|
|
38
|
+
kind: 'truncation',
|
|
39
|
+
lost: ['x'],
|
|
40
|
+
});
|
|
41
|
+
expect(findTranslationLoss('x', { action: 'legacy' }, '', () => ok, rules)?.kind).toBe(
|
|
42
|
+
'core-only'
|
|
43
|
+
);
|
|
44
|
+
expect(
|
|
45
|
+
findTranslationLoss(
|
|
46
|
+
'go #a',
|
|
47
|
+
ok,
|
|
48
|
+
'?',
|
|
49
|
+
() => {
|
|
50
|
+
throw new Error('no pattern');
|
|
51
|
+
},
|
|
52
|
+
rules
|
|
53
|
+
)
|
|
54
|
+
).toEqual({ kind: 'read-back', lost: ['the output does not read: no pattern'] });
|
|
55
|
+
expect(findTranslationLoss('go #a', ok, 've #a y', () => unread('y'), rules)).toEqual({
|
|
56
|
+
kind: 'read-back',
|
|
57
|
+
lost: ['y'],
|
|
58
|
+
});
|
|
59
|
+
expect(findTranslationLoss('go #a', ok, 'stop #a', () => ({ action: 'stop' }), rules)).toEqual({
|
|
60
|
+
kind: 'read-back',
|
|
61
|
+
lost: ['-go'],
|
|
62
|
+
});
|
|
63
|
+
expect(findTranslationLoss('go #a', ok, 've', () => ok, rules)).toEqual({
|
|
64
|
+
kind: 'invariant',
|
|
65
|
+
lost: ['#a'],
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('compares the read-back with what was written', () => {
|
|
70
|
+
const written: TranslationLossRules<Node> = { ...rules, written: () => ({ action: 'stop' }) };
|
|
71
|
+
expect(
|
|
72
|
+
findTranslationLoss('go #a', ok, 'stop #a', () => ({ action: 'stop' }), written)
|
|
73
|
+
).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('unconsumedSpans and LossyTranslationError', () => {
|
|
78
|
+
it('reads the unread text out of the diagnostics', () => {
|
|
79
|
+
expect(unconsumedSpans(unread('banana'))).toEqual(['banana']);
|
|
80
|
+
expect(unconsumedSpans({ action: 'go' })).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('says what the translation loses', () => {
|
|
84
|
+
const error = new LossyTranslationError('ve', { kind: 'invariant', lost: ['#a'] }, 'en', 'es');
|
|
85
|
+
expect(error.name).toBe('LossyTranslationError');
|
|
86
|
+
expect(error.partial).toBe('ve');
|
|
87
|
+
expect(error.message).toBe('translation en → es would lose part of the script (invariant): #a');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-loud translation: what a translation checks before it returns (domains
|
|
3
|
+
* rethink Phase 4.5, `docs-internal/DOMAINS_RETHINK.md`). Moved from
|
|
4
|
+
* `@lokascript/semantic`'s `explicit/lossy.ts`, whose `translate` runs it with
|
|
5
|
+
* hyperscript's rules; `createMultilingualDSL`'s `translate` keeps its own
|
|
6
|
+
* read-back (`TranslationReadBackError`).
|
|
7
|
+
*
|
|
8
|
+
* A parse that matches a prefix of a script and leaves the rest unread used to
|
|
9
|
+
* come back from `translate()` as though it were the whole: `toggle .a .b`
|
|
10
|
+
* became `toggle .a`. A surface that cannot carry a script now says so: the
|
|
11
|
+
* translation throws a {@link LossyTranslationError}, which carries the output
|
|
12
|
+
* it would have returned and what it loses. Four checks, cheapest first:
|
|
13
|
+
*
|
|
14
|
+
* - `truncation` the parse of the input left input bound to no role (its
|
|
15
|
+
* `unconsumed-input` diagnostics);
|
|
16
|
+
* - `core-only` the input holds a form the grammar's writer has no spelling
|
|
17
|
+
* for (hyperscript: a form only `@hyperfixi/core` ran, which
|
|
18
|
+
* upstream reads with another meaning) — `rules.unwritable`;
|
|
19
|
+
* - `read-back` the output, read in the target language, does not read,
|
|
20
|
+
* leaves input unread, or differs from what was written
|
|
21
|
+
* (`rules.differences`, in order);
|
|
22
|
+
* - `invariant` a value the translation must write verbatim is missing
|
|
23
|
+
* from the output (`rules.missingValues`).
|
|
24
|
+
*
|
|
25
|
+
* None of them can say a translation is right. Each says one way it is wrong.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type TranslationLossKind = 'truncation' | 'core-only' | 'read-back' | 'invariant';
|
|
29
|
+
|
|
30
|
+
/** One way a translation loses part of its input. */
|
|
31
|
+
export interface TranslationLoss {
|
|
32
|
+
kind: TranslationLossKind;
|
|
33
|
+
/**
|
|
34
|
+
* What is lost: the input left unread (`truncation`); the forms nothing
|
|
35
|
+
* writes (`core-only`, as `swap <strategy>`); the output left unread or what
|
|
36
|
+
* it reads back without or with in excess (`read-back`, as `-cmd` / `+cmd`);
|
|
37
|
+
* or the verbatim values missing from the output (`invariant`).
|
|
38
|
+
*/
|
|
39
|
+
lost: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class LossyTranslationError extends Error {
|
|
43
|
+
override readonly name = 'LossyTranslationError';
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
/** The output `translate()` would have returned: NOT the whole script. */
|
|
47
|
+
readonly partial: string,
|
|
48
|
+
readonly loss: TranslationLoss,
|
|
49
|
+
readonly from: string,
|
|
50
|
+
readonly to: string
|
|
51
|
+
) {
|
|
52
|
+
super(
|
|
53
|
+
`translation ${from} → ${to} would lose part of the script (${loss.kind}): ` +
|
|
54
|
+
loss.lost.join(', ')
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** What the checks read of a parse: its diagnostics. */
|
|
60
|
+
export interface LossCheckedNode {
|
|
61
|
+
readonly diagnostics?: ReadonlyArray<{ readonly code?: string; readonly message: string }>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The spans a parse left bound to no role (its top node carries them all). */
|
|
65
|
+
export function unconsumedSpans(node: LossCheckedNode): string[] {
|
|
66
|
+
const out: string[] = [];
|
|
67
|
+
for (const d of node.diagnostics ?? []) {
|
|
68
|
+
if (d.code !== 'unconsumed-input') continue;
|
|
69
|
+
out.push(/"(.*)"\s*$/s.exec(d.message)?.[1] ?? d.message);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A grammar's rules for `findTranslationLoss`, for its node type `N`. */
|
|
75
|
+
export interface TranslationLossRules<N extends LossCheckedNode> {
|
|
76
|
+
/** The forms in `node` the grammar's writer has no spelling for (`core-only`). */
|
|
77
|
+
unwritable?(node: N): string[];
|
|
78
|
+
/**
|
|
79
|
+
* `node` as it was written into the target language, to compare its
|
|
80
|
+
* read-back with (hyperscript: in upstream's spelling for English). Default:
|
|
81
|
+
* `node` itself.
|
|
82
|
+
*/
|
|
83
|
+
written?(node: N): N;
|
|
84
|
+
/**
|
|
85
|
+
* How the read-back differs from what was written, checked in order: the
|
|
86
|
+
* first that finds a difference is the loss (hyperscript: commands, then
|
|
87
|
+
* roles, then clauses kept as written).
|
|
88
|
+
*/
|
|
89
|
+
readonly differences: ReadonlyArray<(written: N, back: N) => string[]>;
|
|
90
|
+
/** The values `input` holds that `output` must write verbatim and does not (`invariant`). */
|
|
91
|
+
missingValues?(input: string, output: string): string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* How a translation of `input` (parsed as `node`) into `output` loses part of
|
|
96
|
+
* it, or undefined. `readBack` parses the output in the target language; it
|
|
97
|
+
* throws when the output does not read.
|
|
98
|
+
*/
|
|
99
|
+
export function findTranslationLoss<N extends LossCheckedNode>(
|
|
100
|
+
input: string,
|
|
101
|
+
node: N,
|
|
102
|
+
output: string,
|
|
103
|
+
readBack: (output: string) => N,
|
|
104
|
+
rules: TranslationLossRules<N>
|
|
105
|
+
): TranslationLoss | undefined {
|
|
106
|
+
const unread = unconsumedSpans(node);
|
|
107
|
+
if (unread.length) return { kind: 'truncation', lost: unread };
|
|
108
|
+
const unwritable = rules.unwritable?.(node) ?? [];
|
|
109
|
+
if (unwritable.length) return { kind: 'core-only', lost: unwritable };
|
|
110
|
+
|
|
111
|
+
let back: N;
|
|
112
|
+
try {
|
|
113
|
+
back = readBack(output);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return { kind: 'read-back', lost: [`the output does not read: ${(e as Error).message}`] };
|
|
116
|
+
}
|
|
117
|
+
const misread = unconsumedSpans(back);
|
|
118
|
+
if (misread.length) return { kind: 'read-back', lost: misread };
|
|
119
|
+
const written = rules.written ? rules.written(node) : node;
|
|
120
|
+
for (const difference of rules.differences) {
|
|
121
|
+
const lost = difference(written, back);
|
|
122
|
+
if (lost.length) return { kind: 'read-back', lost };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const values = rules.missingValues?.(input, output) ?? [];
|
|
126
|
+
if (values.length) return { kind: 'invariant', lost: values };
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The matcher's confidence model: an injectable score for a pattern's match
|
|
3
|
+
* (domains rethink Phase 4.3). The interface moved here with the skeleton
|
|
4
|
+
* (`match-skeleton.ts`), which calls `calculate` once per matched pattern;
|
|
5
|
+
* hyperscript's model is `@lokascript/semantic`'s `DefaultConfidenceModel`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { LanguagePattern, SemanticRole, SemanticValue } from '../types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Context passed to confidence model methods.
|
|
12
|
+
*/
|
|
13
|
+
export interface ConfidenceContext {
|
|
14
|
+
/** The pattern being matched */
|
|
15
|
+
readonly pattern: LanguagePattern;
|
|
16
|
+
/** Captured semantic roles */
|
|
17
|
+
readonly captured: ReadonlyMap<SemanticRole, SemanticValue>;
|
|
18
|
+
/** Number of stem (morphological) keyword matches */
|
|
19
|
+
readonly stemMatchCount: number;
|
|
20
|
+
/** Total keyword matches attempted */
|
|
21
|
+
readonly totalKeywordMatches: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Breakdown of confidence scoring factors (for diagnostics/debugging).
|
|
26
|
+
*/
|
|
27
|
+
export interface ConfidenceBreakdown {
|
|
28
|
+
/** Role coverage score (0-1) */
|
|
29
|
+
readonly roleCoverage: number;
|
|
30
|
+
/** Stem match penalty applied (0 to -0.15) */
|
|
31
|
+
readonly stemPenalty: number;
|
|
32
|
+
/** Language-specific boost (e.g., VSO verb-first) */
|
|
33
|
+
readonly languageBoost: number;
|
|
34
|
+
/** Language-specific adjustment (e.g., preposition disambiguation) */
|
|
35
|
+
readonly languageAdjustment: number;
|
|
36
|
+
/** Final clamped confidence (0-1) */
|
|
37
|
+
readonly final: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Injectable confidence scoring model.
|
|
42
|
+
*
|
|
43
|
+
* Each method computes one factor of the confidence score.
|
|
44
|
+
* Override individual methods to customize scoring without rewriting the pipeline.
|
|
45
|
+
*/
|
|
46
|
+
export interface ConfidenceModel {
|
|
47
|
+
/**
|
|
48
|
+
* Calculate the full confidence score.
|
|
49
|
+
* Default implementation calls each factor method and combines them.
|
|
50
|
+
*/
|
|
51
|
+
calculate(ctx: ConfidenceContext): number;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Calculate confidence with a detailed breakdown.
|
|
55
|
+
* Useful for diagnostics and debugging.
|
|
56
|
+
*/
|
|
57
|
+
calculateWithBreakdown(ctx: ConfidenceContext): ConfidenceBreakdown;
|
|
58
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pattern matcher's skeleton and its value-reader seam (domains rethink
|
|
3
|
+
* Phase 4.3): the matcher semantic's hyperscript parser runs, with
|
|
4
|
+
* hyperscript's reading behind `ValueReader`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { MatchSkeleton } from './match-skeleton';
|
|
8
|
+
export type { MatchView, RoleSlot, ValueReader } from './value-reader';
|
|
9
|
+
export type { ConfidenceBreakdown, ConfidenceContext, ConfidenceModel } from './confidence-model';
|
|
10
|
+
export { StockValueReader, createStockReader, greedyStopMarkers, readGreedy } from './stock-reader';
|
|
11
|
+
export { readRoleValue, type ReadingProfile } from './stock-reading';
|
|
12
|
+
export { stockConfidenceModel } from './stock-confidence';
|