@lokascript/framework 2.9.4 → 2.11.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 +21 -1
- package/README.md +0 -2
- package/dist/api/create-dsl.d.ts +4 -2
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/index.js +18 -6
- package/dist/api/index.js.map +1 -1
- package/dist/index.cjs +18 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +18 -6
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/api-surface.test.ts +35 -0
- package/src/aot/registry-bridge.test.ts +5 -5
- package/src/api/create-dsl.test.ts +87 -5
- package/src/api/create-dsl.ts +42 -16
- package/src/api/domain-registry.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lokascript/framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.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,9 +96,10 @@
|
|
|
96
96
|
"author": "LokaScript Contributors",
|
|
97
97
|
"license": "MIT",
|
|
98
98
|
"dependencies": {
|
|
99
|
-
"@lokascript/intent": "^2.
|
|
99
|
+
"@lokascript/intent": "^2.11.0"
|
|
100
100
|
},
|
|
101
101
|
"devDependencies": {
|
|
102
|
+
"@lokascript/domains": "^2.11.1",
|
|
102
103
|
"@types/node": "^26.1.2",
|
|
103
104
|
"@vitest/coverage-v8": "^4.0.17",
|
|
104
105
|
"tsup": "^8.0.0",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API-surface snapshot: the third-party extension contract.
|
|
3
|
+
*
|
|
4
|
+
* Domain packages now live OUTSIDE this repo (lokascript-domains, published as
|
|
5
|
+
* @lokascript/domains), so a signature change here no longer breaks anything
|
|
6
|
+
* in-tree at build time — this test is what makes an accidental change to the
|
|
7
|
+
* promised surface fail loudly instead. DOMAIN_AUTHOR_GUIDE.md documents the
|
|
8
|
+
* promise; docs/API_SURFACE.md is the committed snapshot.
|
|
9
|
+
*
|
|
10
|
+
* On a DELIBERATE contract change: regenerate with
|
|
11
|
+
* `npx tsx scripts/api-surface-report.ts`, commit the diff, and note the
|
|
12
|
+
* change in DOMAIN_AUTHOR_GUIDE.md.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import { extractApiSurface } from '../../scripts/api-surface-lib';
|
|
18
|
+
|
|
19
|
+
const PKG_ROOT = path.resolve(__dirname, '..', '..');
|
|
20
|
+
|
|
21
|
+
describe('extension-contract API surface', () => {
|
|
22
|
+
it('matches the committed docs/API_SURFACE.md snapshot', () => {
|
|
23
|
+
const committed = readFileSync(path.join(PKG_ROOT, 'docs', 'API_SURFACE.md'), 'utf8');
|
|
24
|
+
const current = extractApiSurface(PKG_ROOT) + '\n';
|
|
25
|
+
expect(
|
|
26
|
+
current,
|
|
27
|
+
'API surface drifted from docs/API_SURFACE.md — if deliberate, regenerate with `npx tsx scripts/api-surface-report.ts` and update DOMAIN_AUTHOR_GUIDE.md'
|
|
28
|
+
).toBe(committed);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('the snapshot pins every promised export (no MISSING markers)', () => {
|
|
32
|
+
const committed = readFileSync(path.join(PKG_ROOT, 'docs', 'API_SURFACE.md'), 'utf8');
|
|
33
|
+
expect(committed).not.toContain('MISSING');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -12,7 +12,7 @@ describe('registryToAOTBackends', () => {
|
|
|
12
12
|
languages: ['en'],
|
|
13
13
|
inputLabel: 'input',
|
|
14
14
|
inputDescription: 'Test input',
|
|
15
|
-
getDSL: () => import('@lokascript/
|
|
15
|
+
getDSL: () => import('@lokascript/domains/sql').then(m => m.createSQLDSL()),
|
|
16
16
|
scanConfig: {
|
|
17
17
|
attributes: ['data-test'],
|
|
18
18
|
scriptTypes: ['text/test'],
|
|
@@ -38,7 +38,7 @@ describe('registryToAOTBackends', () => {
|
|
|
38
38
|
languages: ['en'],
|
|
39
39
|
inputLabel: 'input',
|
|
40
40
|
inputDescription: 'Test',
|
|
41
|
-
getDSL: () => import('@lokascript/
|
|
41
|
+
getDSL: () => import('@lokascript/domains/sql').then(m => m.createSQLDSL()),
|
|
42
42
|
scanConfig: {
|
|
43
43
|
attributes: ['data-with'],
|
|
44
44
|
defaultLanguage: 'en',
|
|
@@ -51,7 +51,7 @@ describe('registryToAOTBackends', () => {
|
|
|
51
51
|
languages: ['en'],
|
|
52
52
|
inputLabel: 'input',
|
|
53
53
|
inputDescription: 'Test',
|
|
54
|
-
getDSL: () => import('@lokascript/
|
|
54
|
+
getDSL: () => import('@lokascript/domains/sql').then(m => m.createSQLDSL()),
|
|
55
55
|
// no scanConfig
|
|
56
56
|
});
|
|
57
57
|
|
|
@@ -69,7 +69,7 @@ describe('registryToAOTBackends', () => {
|
|
|
69
69
|
languages: ['en'],
|
|
70
70
|
inputLabel: 'input',
|
|
71
71
|
inputDescription: 'Test',
|
|
72
|
-
getDSL: () => import('@lokascript/
|
|
72
|
+
getDSL: () => import('@lokascript/domains/sql').then(m => m.createSQLDSL()),
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
const backends = await registryToAOTBackends(registry);
|
|
@@ -85,7 +85,7 @@ describe('registryToAOTBackends', () => {
|
|
|
85
85
|
languages: ['en'],
|
|
86
86
|
inputLabel: 'input',
|
|
87
87
|
inputDescription: 'Test',
|
|
88
|
-
getDSL: () => import('@lokascript/
|
|
88
|
+
getDSL: () => import('@lokascript/domains/sql').then(m => m.createSQLDSL()),
|
|
89
89
|
scanConfig: {
|
|
90
90
|
attributes: ['data-gen'],
|
|
91
91
|
defaultLanguage: 'en',
|
|
@@ -39,7 +39,7 @@ const selectSchema = defineCommand({
|
|
|
39
39
|
required: true,
|
|
40
40
|
expectedTypes: ['expression'],
|
|
41
41
|
svoPosition: 1,
|
|
42
|
-
markerOverride: { en: 'from' },
|
|
42
|
+
markerOverride: { en: 'from', es: 'de' },
|
|
43
43
|
}),
|
|
44
44
|
],
|
|
45
45
|
});
|
|
@@ -100,6 +100,62 @@ const englishProfile = {
|
|
|
100
100
|
roleMarkers: {},
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
+
class SpanishTestTokenizer extends BaseTokenizer {
|
|
104
|
+
readonly language = 'es';
|
|
105
|
+
readonly direction = 'ltr' as const;
|
|
106
|
+
|
|
107
|
+
constructor() {
|
|
108
|
+
super();
|
|
109
|
+
this.registerExtractors(getDefaultExtractors());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
classifyToken(token: string): 'keyword' | 'identifier' | 'literal' | 'operator' {
|
|
113
|
+
const keywords = ['seleccionar', 'insertar', 'de', 'en'];
|
|
114
|
+
if (keywords.includes(token.toLowerCase())) return 'keyword';
|
|
115
|
+
return 'identifier';
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const spanishProfile = {
|
|
120
|
+
code: 'es',
|
|
121
|
+
name: 'Spanish',
|
|
122
|
+
nativeName: 'Español',
|
|
123
|
+
wordOrder: 'SVO' as const,
|
|
124
|
+
direction: 'ltr' as const,
|
|
125
|
+
caseMarking: 'preposition' as const,
|
|
126
|
+
keywords: {
|
|
127
|
+
select: { primary: 'seleccionar', aliases: [] },
|
|
128
|
+
insert: { primary: 'insertar', aliases: [] },
|
|
129
|
+
from: { primary: 'de', aliases: [] },
|
|
130
|
+
into: { primary: 'en', aliases: [] },
|
|
131
|
+
},
|
|
132
|
+
roleMarkers: {},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Two languages, NO grammarProfile on either — the nine-domains shape. */
|
|
136
|
+
function createBilingualTestDSL() {
|
|
137
|
+
return createMultilingualDSL({
|
|
138
|
+
name: 'TestSQL-bilingual',
|
|
139
|
+
schemas: [selectSchema, insertSchema],
|
|
140
|
+
languages: [
|
|
141
|
+
{
|
|
142
|
+
code: 'en',
|
|
143
|
+
name: 'English',
|
|
144
|
+
nativeName: 'English',
|
|
145
|
+
tokenizer: new EnglishTestTokenizer(),
|
|
146
|
+
patternProfile: englishProfile,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
code: 'es',
|
|
150
|
+
name: 'Spanish',
|
|
151
|
+
nativeName: 'Español',
|
|
152
|
+
tokenizer: new SpanishTestTokenizer(),
|
|
153
|
+
patternProfile: spanishProfile,
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
103
159
|
function createTestDSL() {
|
|
104
160
|
return createMultilingualDSL({
|
|
105
161
|
name: 'TestSQL',
|
|
@@ -273,23 +329,49 @@ describe('MultilingualDSL: Explicit Syntax Support', () => {
|
|
|
273
329
|
// translate
|
|
274
330
|
// ---------------------------------------------------------------------------
|
|
275
331
|
|
|
276
|
-
describe('translate', () => {
|
|
332
|
+
describe('translate (parse→render)', () => {
|
|
277
333
|
it('returns explicit syntax unchanged', () => {
|
|
278
334
|
const dsl = createTestDSL();
|
|
279
335
|
const result = dsl.translate('[select columns:name source:users]', 'en', 'ja');
|
|
280
336
|
expect(result).toBe('[select columns:name source:users]');
|
|
281
337
|
});
|
|
282
338
|
|
|
283
|
-
|
|
284
|
-
|
|
339
|
+
it('translates between configured languages with NO grammar profiles (the nine-domains shape)', () => {
|
|
340
|
+
const dsl = createBilingualTestDSL();
|
|
341
|
+
const result = dsl.translate('select name from users', 'en', 'es');
|
|
342
|
+
expect(result).toContain('seleccionar');
|
|
343
|
+
expect(result).toContain('name');
|
|
344
|
+
expect(result).toContain('users');
|
|
345
|
+
// The rendered Spanish is real DSL surface — it re-parses.
|
|
346
|
+
expect(dsl.validate(result, 'es').valid).toBe(true);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('round-trips to the source language through the schema renderer', () => {
|
|
350
|
+
const dsl = createTestDSL();
|
|
351
|
+
const result = dsl.translate('select name from users', 'en', 'en');
|
|
352
|
+
expect(result.toLowerCase()).toContain('select');
|
|
353
|
+
expect(result).toContain('name');
|
|
354
|
+
expect(result).toContain('users');
|
|
355
|
+
expect(dsl.validate(result, 'en').valid).toBe(true);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// No renderer coverage for 'ja' (not a configured language) and no
|
|
359
|
+
// grammarProfile for the transformer fallback: the error must name the
|
|
285
360
|
// field to set rather than leaving the caller with "No profile found".
|
|
286
|
-
it('throws an error naming grammarProfile when
|
|
361
|
+
it('throws an error naming grammarProfile when render cannot cover the target', () => {
|
|
287
362
|
const dsl = createTestDSL();
|
|
288
363
|
expect(() => dsl.translate('select name from users', 'en', 'ja')).toThrow(/grammarProfile/);
|
|
289
364
|
expect(() => dsl.translate('select name from users', 'en', 'ja')).toThrow(
|
|
290
365
|
/parse\/validate\/compile do not need it/
|
|
291
366
|
);
|
|
292
367
|
});
|
|
368
|
+
|
|
369
|
+
it('propagates the parse error for unparseable input when no fallback exists', () => {
|
|
370
|
+
const dsl = createTestDSL();
|
|
371
|
+
expect(() => dsl.translate('gibberish matching nothing', 'en', 'en')).toThrow(
|
|
372
|
+
/No pattern matched/
|
|
373
|
+
);
|
|
374
|
+
});
|
|
293
375
|
});
|
|
294
376
|
|
|
295
377
|
// ---------------------------------------------------------------------------
|
package/src/api/create-dsl.ts
CHANGED
|
@@ -35,8 +35,10 @@ export interface LanguageConfig {
|
|
|
35
35
|
/**
|
|
36
36
|
* Language profile for grammar transformation.
|
|
37
37
|
*
|
|
38
|
-
* Optional
|
|
39
|
-
*
|
|
38
|
+
* Optional everywhere: `translate()` primarily works parse→render through
|
|
39
|
+
* the schema-driven renderer, which needs no grammar profile. Configure this
|
|
40
|
+
* only to enable the whole-string GrammarTransformer FALLBACK for input the
|
|
41
|
+
* pattern matcher cannot parse (or actions render() cannot cover).
|
|
40
42
|
*/
|
|
41
43
|
readonly grammarProfile?: GrammarProfile;
|
|
42
44
|
}
|
|
@@ -407,23 +409,47 @@ class MultilingualDSLImpl implements MultilingualDSL {
|
|
|
407
409
|
// Explicit syntax is language-agnostic — return unchanged
|
|
408
410
|
if (isExplicitSyntax(input)) return input;
|
|
409
411
|
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
for
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
412
|
+
// Primary route: parse in the source language, render in the target — the
|
|
413
|
+
// same parse→render path DomainRegistry.translate uses. The schema-driven
|
|
414
|
+
// renderer covers every configured language, so this needs NO grammar
|
|
415
|
+
// profiles (before 2026-08 translate() required them and therefore threw
|
|
416
|
+
// for every domain in the family — none configures one).
|
|
417
|
+
let node: SemanticNode;
|
|
418
|
+
try {
|
|
419
|
+
node = this.parseWithConfidence(input, fromLanguage).node;
|
|
420
|
+
} catch (parseError) {
|
|
421
|
+
// Whole-string grammar transformation can sometimes translate input the
|
|
422
|
+
// pattern matcher cannot parse — but only when profiles are configured.
|
|
423
|
+
if (this.hasGrammarProfile(fromLanguage) && this.hasGrammarProfile(toLanguage)) {
|
|
424
|
+
return this.transformer.transform(input, fromLanguage, toLanguage);
|
|
422
425
|
}
|
|
426
|
+
throw parseError;
|
|
423
427
|
}
|
|
424
428
|
|
|
425
|
-
//
|
|
426
|
-
|
|
429
|
+
// Only consult render() for a CONFIGURED target language — the schema
|
|
430
|
+
// renderer's lookup tables fall back to raw action/marker names for
|
|
431
|
+
// unknown languages, which would silently return wrong-language output.
|
|
432
|
+
const rendered = this.registry.getSupportedLanguages().includes(toLanguage)
|
|
433
|
+
? this.render(node, toLanguage)
|
|
434
|
+
: null;
|
|
435
|
+
if (rendered != null) return rendered;
|
|
436
|
+
|
|
437
|
+
// render() covered nothing for this action/language pair; fall back to the
|
|
438
|
+
// grammar transformer when profiles exist, otherwise name both remedies.
|
|
439
|
+
if (this.hasGrammarProfile(fromLanguage) && this.hasGrammarProfile(toLanguage)) {
|
|
440
|
+
return this.transformer.transform(input, fromLanguage, toLanguage);
|
|
441
|
+
}
|
|
442
|
+
throw new Error(
|
|
443
|
+
`translate() could not render action "${node.action}" in language "${toLanguage}": no ` +
|
|
444
|
+
`custom renderer, domain renderer, or schema pattern covers it, and no grammar ` +
|
|
445
|
+
`profile is configured for the transformer fallback. Either add "${toLanguage}" to ` +
|
|
446
|
+
`the DSL's languages/renderer, or set 'grammarProfile' on its LanguageConfig in ` +
|
|
447
|
+
`createMultilingualDSL() (parse/validate/compile do not need it).`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private hasGrammarProfile(language: string): boolean {
|
|
452
|
+
return this.profileProvider.getProfile(language) != null;
|
|
427
453
|
}
|
|
428
454
|
|
|
429
455
|
compile(input: string, language: string): CompileResult {
|
|
@@ -117,7 +117,7 @@ export interface DomainDescriptor {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// =============================================================================
|
|
120
|
-
// MCP Tool Types (minimal — compatible with @modelcontextprotocol/
|
|
120
|
+
// MCP Tool Types (minimal — compatible with @modelcontextprotocol/server)
|
|
121
121
|
// =============================================================================
|
|
122
122
|
|
|
123
123
|
/**
|