@lokascript/framework 4.7.0 → 5.0.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 +76 -1
- package/dist/api/create-dsl.d.ts +14 -12
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/index.js +888 -1683
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.d.ts +0 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +560 -1133
- package/dist/core/index.js.map +1 -1
- package/dist/core/matching/index.d.ts +2 -0
- package/dist/core/matching/index.d.ts.map +1 -1
- package/dist/core/matching/possessive-keywords.d.ts.map +1 -0
- package/dist/core/matching/stock-confidence.d.ts +3 -4
- package/dist/core/matching/stock-confidence.d.ts.map +1 -1
- package/dist/core/matching/stock-reader.d.ts +8 -1
- package/dist/core/matching/stock-reader.d.ts.map +1 -1
- package/dist/core/matching/stock-reading.d.ts +4 -4
- package/dist/core/{pattern-matching/utils → matching}/type-validation.d.ts +1 -1
- package/dist/core/matching/type-validation.d.ts.map +1 -0
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/generation/index.d.ts +1 -1
- package/dist/generation/index.d.ts.map +1 -1
- package/dist/generation/index.js +32 -171
- package/dist/generation/index.js.map +1 -1
- package/dist/generation/profile.d.ts +30 -0
- package/dist/generation/profile.d.ts.map +1 -0
- package/dist/generation/renderer.d.ts +1 -1
- package/dist/generation/renderer.d.ts.map +1 -1
- package/dist/generation/schema-patterns.d.ts +3 -3
- package/dist/index.cjs +969 -1674
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +969 -1668
- package/dist/index.js.map +1 -1
- package/dist/ir/index.js.map +1 -1
- package/dist/multilingual/builders.d.ts +3 -3
- package/dist/multilingual/builders.d.ts.map +1 -1
- package/dist/multilingual/index.js.map +1 -1
- package/dist/parsing/index.d.ts +0 -1
- package/dist/parsing/index.d.ts.map +1 -1
- package/dist/parsing/index.js +0 -1145
- package/dist/parsing/index.js.map +1 -1
- package/dist/testing/gate/index.js +44 -12
- package/dist/testing/gate/index.js.map +1 -1
- package/dist/testing/gate/lint/types.d.ts +1 -1
- package/dist/testing/gate/lint/types.d.ts.map +1 -1
- package/dist/testing/gate/round-trip.d.ts +24 -3
- package/dist/testing/gate/round-trip.d.ts.map +1 -1
- package/package.json +2 -7
- package/src/__test__/test-utils.ts +1 -1
- package/src/api/create-dsl-engine.test.ts +123 -34
- package/src/api/create-dsl.test.ts +7 -0
- package/src/api/create-dsl.ts +56 -39
- package/src/core/index.ts +0 -1
- package/src/core/matching/index.ts +2 -0
- package/src/core/matching/stock-confidence.ts +3 -4
- package/src/core/matching/stock-reader.test.ts +87 -3
- package/src/core/matching/stock-reader.ts +20 -1
- package/src/core/matching/stock-reading.ts +6 -6
- package/src/core/{pattern-matching/utils → matching}/type-validation.test.ts +2 -2
- package/src/core/{pattern-matching/utils → matching}/type-validation.ts +1 -1
- package/src/core/types.ts +5 -0
- package/src/generation/domain-renderer.test.ts +1 -1
- package/src/generation/index.ts +1 -1
- package/src/generation/profile.ts +41 -0
- package/src/generation/renderer.test.ts +1 -1
- package/src/generation/renderer.ts +1 -1
- package/src/generation/schema-patterns.test.ts +41 -1
- package/src/generation/schema-patterns.ts +15 -3
- package/src/multilingual/bridge.test.ts +31 -23
- package/src/multilingual/builders.ts +3 -3
- package/src/parsing/index.ts +0 -1
- package/src/testing/gate/lint/types.ts +1 -1
- package/src/testing/gate/round-trip.test.ts +99 -9
- package/src/testing/gate/round-trip.ts +76 -11
- package/dist/core/pattern-matching/index.d.ts +0 -6
- package/dist/core/pattern-matching/index.d.ts.map +0 -1
- package/dist/core/pattern-matching/index.js +0 -1211
- package/dist/core/pattern-matching/index.js.map +0 -1
- package/dist/core/pattern-matching/pattern-matcher.d.ts +0 -122
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +0 -1
- package/dist/core/pattern-matching/utils/index.d.ts +0 -6
- package/dist/core/pattern-matching/utils/index.d.ts.map +0 -1
- package/dist/core/pattern-matching/utils/possessive-keywords.d.ts.map +0 -1
- package/dist/core/pattern-matching/utils/type-validation.d.ts.map +0 -1
- package/dist/generation/pattern-generator.d.ts +0 -55
- package/dist/generation/pattern-generator.d.ts.map +0 -1
- package/src/core/pattern-matching/index.ts +0 -6
- package/src/core/pattern-matching/pattern-matcher.test.ts +0 -1031
- package/src/core/pattern-matching/pattern-matcher.ts +0 -765
- package/src/core/pattern-matching/utils/index.ts +0 -6
- package/src/generation/pattern-generator.test.ts +0 -532
- package/src/generation/pattern-generator.ts +0 -327
- /package/dist/core/{pattern-matching/utils → matching}/possessive-keywords.d.ts +0 -0
- /package/src/core/{pattern-matching/utils → matching}/possessive-keywords.ts +0 -0
package/src/generation/index.ts
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A domain's language profile, and the role order its sentences use.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { RoleSpec } from '../schema';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A domain's language profile as framework reads it: word order, command
|
|
9
|
+
* keywords, and role markers (with the side of the value each attaches to).
|
|
10
|
+
*/
|
|
11
|
+
export interface PatternGenLanguageProfile {
|
|
12
|
+
readonly code: string;
|
|
13
|
+
readonly wordOrder: 'SVO' | 'SOV' | 'VSO' | 'VOS' | 'OSV' | 'OVS' | 'free';
|
|
14
|
+
readonly keywords: Record<string, { primary: string; alternatives?: string[] }>;
|
|
15
|
+
readonly roleMarkers?: Record<
|
|
16
|
+
string,
|
|
17
|
+
{ primary: string; alternatives?: string[]; position?: 'before' | 'after' }
|
|
18
|
+
>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sort roles by word order: descending `svoPosition` (or `sovPosition` in an
|
|
23
|
+
* SOV language) — higher values come first. The schema renderer
|
|
24
|
+
* (`createSchemaRenderer`) orders roles with it, as the core's pattern
|
|
25
|
+
* generator does (`createSchemaPatternGenerator` with `positionOrder:
|
|
26
|
+
* 'descending'`), so a rendered surface re-parses.
|
|
27
|
+
*/
|
|
28
|
+
export function sortRolesByWordOrder(roles: RoleSpec[], wordOrder: string): RoleSpec[] {
|
|
29
|
+
const sorted = [...roles];
|
|
30
|
+
|
|
31
|
+
if (wordOrder === 'SVO') {
|
|
32
|
+
// Sort by svoPosition (higher = earlier)
|
|
33
|
+
sorted.sort((a, b) => (b.svoPosition ?? 0) - (a.svoPosition ?? 0));
|
|
34
|
+
} else if (wordOrder === 'SOV') {
|
|
35
|
+
// Sort by sovPosition (higher = earlier)
|
|
36
|
+
sorted.sort((a, b) => (b.sovPosition ?? 0) - (a.sovPosition ?? 0));
|
|
37
|
+
}
|
|
38
|
+
// VSO and others use SVO-style ordering
|
|
39
|
+
|
|
40
|
+
return sorted;
|
|
41
|
+
}
|
|
@@ -11,7 +11,7 @@ import type { KeywordTable, MarkerTable } from './renderer';
|
|
|
11
11
|
import type { SemanticNode } from '../core/types';
|
|
12
12
|
import type { CommandSchema } from '../schema';
|
|
13
13
|
import { defineCommand, defineRole } from '../schema';
|
|
14
|
-
import type { PatternGenLanguageProfile } from './
|
|
14
|
+
import type { PatternGenLanguageProfile } from './profile';
|
|
15
15
|
|
|
16
16
|
// =============================================================================
|
|
17
17
|
// Test Data
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { SemanticNode } from '../core/types';
|
|
12
12
|
import { extractRoleValue, extractValue } from '../core/types';
|
|
13
13
|
import type { CommandSchema } from '../schema';
|
|
14
|
-
import { sortRolesByWordOrder, type PatternGenLanguageProfile } from './
|
|
14
|
+
import { sortRolesByWordOrder, type PatternGenLanguageProfile } from './profile';
|
|
15
15
|
|
|
16
16
|
// =============================================================================
|
|
17
17
|
// Renderer Interface
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The schema-pattern generator semantic's hyperscript parser runs (domains
|
|
3
3
|
* rethink Phase 4.4), on a schema of its own: the grammar's position order,
|
|
4
|
-
* the variants,
|
|
4
|
+
* the variants, a profile's optional markers, and a role's word list.
|
|
5
5
|
*/
|
|
6
6
|
import { describe, expect, it } from 'vitest';
|
|
7
7
|
import { defineCommand, defineRole } from '../schema';
|
|
@@ -80,6 +80,46 @@ describe('createSchemaPatternGenerator', () => {
|
|
|
80
80
|
]);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
it("puts a role's word list for the pattern's language on its token, in every variant", () => {
|
|
84
|
+
const close = defineCommand({
|
|
85
|
+
action: 'send',
|
|
86
|
+
roles: [
|
|
87
|
+
defineRole({
|
|
88
|
+
role: 'patient',
|
|
89
|
+
required: true,
|
|
90
|
+
expectedTypes: ['expression'],
|
|
91
|
+
words: { '*': ['tab'], tr: ['sekme'] },
|
|
92
|
+
}),
|
|
93
|
+
defineRole({ role: 'destination', required: false, expectedTypes: ['selector'] }),
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
const generator = createSchemaPatternGenerator({ positionOrder: 'ascending' });
|
|
97
|
+
const wordsOf = (tokens: readonly PatternToken[]): unknown[] =>
|
|
98
|
+
tokens.flatMap(t =>
|
|
99
|
+
t.type === 'role' ? [[t.role, t.words]] : t.type === 'group' ? wordsOf(t.tokens) : []
|
|
100
|
+
);
|
|
101
|
+
const at = (profile: SchemaPatternProfile) => {
|
|
102
|
+
const variants = generator.generatePatternVariants(close, profile, {
|
|
103
|
+
basePriority: 100,
|
|
104
|
+
generateSimpleVariants: true,
|
|
105
|
+
});
|
|
106
|
+
const roles = variants.flatMap(p => wordsOf(p.template.tokens)) as [string, unknown][];
|
|
107
|
+
return {
|
|
108
|
+
variants: variants.length,
|
|
109
|
+
patient: [
|
|
110
|
+
...new Set(roles.filter(([r]) => r === 'patient').map(([, w]) => JSON.stringify(w))),
|
|
111
|
+
],
|
|
112
|
+
destination: [
|
|
113
|
+
...new Set(roles.filter(([r]) => r === 'destination').map(([, w]) => JSON.stringify(w))),
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
expect(at(en)).toEqual({ variants: 2, patient: ['["tab"]'], destination: [undefined] });
|
|
118
|
+
expect(at(tr)).toEqual({ variants: 3, patient: ['["tab","sekme"]'], destination: [undefined] });
|
|
119
|
+
// No list, no field: the patterns of a grammar that lists no words are unchanged.
|
|
120
|
+
expect(JSON.stringify(generator.generatePattern(send, en))).not.toContain('words');
|
|
121
|
+
});
|
|
122
|
+
|
|
83
123
|
it('drops variant-only roles', () => {
|
|
84
124
|
const scroll = defineCommand({
|
|
85
125
|
action: 'scroll',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Patterns from command schemas: the generator semantic's hyperscript parser
|
|
3
3
|
* runs (domains rethink Phase 4.4, `docs-internal/DOMAINS_RETHINK.md`), moved
|
|
4
|
-
* here from `@lokascript/semantic`
|
|
5
|
-
* (
|
|
4
|
+
* here from `@lokascript/semantic`; `createMultilingualDSL` generates every
|
|
5
|
+
* domain's patterns with it (the classic generator went in 5.0, Phase 6).
|
|
6
6
|
*
|
|
7
7
|
* Per command and language it builds the main pattern and its variants: the
|
|
8
8
|
* simple variant (required roles only), the omit-role, widened-type and
|
|
@@ -21,7 +21,7 @@ export interface SchemaPatternGrammar {
|
|
|
21
21
|
/**
|
|
22
22
|
* How `svoPosition` / `sovPosition` sort: `'ascending'` puts the lowest first
|
|
23
23
|
* (hyperscript's schemas, 1-based), `'descending'` the highest (a domain's
|
|
24
|
-
* schemas, as
|
|
24
|
+
* schemas, as `sortRolesByWordOrder` and the schema renderer read them).
|
|
25
25
|
*/
|
|
26
26
|
readonly positionOrder: 'ascending' | 'descending';
|
|
27
27
|
}
|
|
@@ -382,11 +382,13 @@ function generateVerbFirstPattern(
|
|
|
382
382
|
// verb-first, which is exactly this shape (trailing particle tolerated).
|
|
383
383
|
const roleTokens: PatternToken[] = requiredRoles.flatMap(r => {
|
|
384
384
|
const prefix = r.valuePrefixLiteral?.[profile.code];
|
|
385
|
+
const words = wordsFor(r, profile.code);
|
|
385
386
|
const roleToken: PatternToken = {
|
|
386
387
|
type: 'role',
|
|
387
388
|
role: r.role,
|
|
388
389
|
optional: false,
|
|
389
390
|
expectedTypes: r.expectedTypes,
|
|
391
|
+
...(words ? { words } : {}),
|
|
390
392
|
};
|
|
391
393
|
return prefix ? [{ type: 'literal', value: prefix } as PatternToken, roleToken] : [roleToken];
|
|
392
394
|
});
|
|
@@ -673,6 +675,13 @@ function buildRoleTokens(
|
|
|
673
675
|
* use — a hand-rolled copy there is what let the two drift apart in the first
|
|
674
676
|
* place (see `appendRemainingOptionalRoles` in event-handlers-sov.ts).
|
|
675
677
|
*/
|
|
678
|
+
/** The words a role reads in `language` (`RoleSpec.words`: its `'*'` list and the language's own). */
|
|
679
|
+
function wordsFor(roleSpec: RoleSpec, language: string): readonly string[] | undefined {
|
|
680
|
+
if (!roleSpec.words) return undefined;
|
|
681
|
+
const words = [...(roleSpec.words['*'] ?? []), ...(roleSpec.words[language] ?? [])];
|
|
682
|
+
return words.length > 0 ? words : undefined;
|
|
683
|
+
}
|
|
684
|
+
|
|
676
685
|
function buildRoleToken(roleSpec: RoleSpec, profile: SchemaPatternProfile): PatternToken[] {
|
|
677
686
|
const tokens: PatternToken[] = [];
|
|
678
687
|
|
|
@@ -686,6 +695,7 @@ function buildRoleToken(roleSpec: RoleSpec, profile: SchemaPatternProfile): Patt
|
|
|
686
695
|
// `fetch "/api"`). Tag the emitted marker literal so the renderer skips it;
|
|
687
696
|
// the pattern matcher ignores the flag, so parsing is unchanged.
|
|
688
697
|
const suppressMarker = roleSpec.renderOverride?.[profile.code] === '';
|
|
698
|
+
const words = wordsFor(roleSpec, profile.code);
|
|
689
699
|
|
|
690
700
|
// Role value token
|
|
691
701
|
const roleValueToken: PatternToken = {
|
|
@@ -700,6 +710,8 @@ function buildRoleToken(roleSpec: RoleSpec, profile: SchemaPatternProfile): Patt
|
|
|
700
710
|
// A greedy role reads up to the next marker the pattern owes (a domain's
|
|
701
711
|
// SQL condition); its value reader does the reading.
|
|
702
712
|
...(roleSpec.greedy ? { greedy: true } : {}),
|
|
713
|
+
// A role with a word list reads only one of its words.
|
|
714
|
+
...(words ? { words } : {}),
|
|
703
715
|
};
|
|
704
716
|
|
|
705
717
|
// Required literal immediately before the role VALUE (rolePrefixLiteralVariants
|
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
deriveRoleMarkers,
|
|
15
15
|
} from './builders';
|
|
16
16
|
import type { GrammarProfileSlice, DomainVocabulary } from './types';
|
|
17
|
-
import { generatePattern } from '../generation/pattern-generator';
|
|
18
17
|
import { defineCommand, defineRole } from '../schema';
|
|
19
18
|
import { createMultilingualDSL } from '../api/create-dsl';
|
|
19
|
+
import { extractValue } from '../core/types';
|
|
20
20
|
|
|
21
21
|
// =============================================================================
|
|
22
22
|
// Fixture slices (mirror the shape of semantic KNOWN_PROFILES entries)
|
|
@@ -189,7 +189,7 @@ describe('buildPatternProfile', () => {
|
|
|
189
189
|
expect(profile.keywords.select.alternatives).toEqual([...alternatives]);
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
-
describe('
|
|
192
|
+
describe('a DSL built from the profile (the core engine)', () => {
|
|
193
193
|
const moveSchema = defineCommand({
|
|
194
194
|
action: 'move',
|
|
195
195
|
description: 'Move a thing',
|
|
@@ -212,37 +212,45 @@ describe('buildPatternProfile', () => {
|
|
|
212
212
|
}),
|
|
213
213
|
],
|
|
214
214
|
});
|
|
215
|
+
const dsl = createMultilingualDSL({
|
|
216
|
+
name: 'Move',
|
|
217
|
+
schemas: [moveSchema],
|
|
218
|
+
languages: [
|
|
219
|
+
buildLanguageConfig(EN_SLICE, { keywords: { move: { primary: 'move' } } }),
|
|
220
|
+
buildLanguageConfig(JA_SLICE, { keywords: { move: { primary: '移動' } } }),
|
|
221
|
+
buildLanguageConfig(AR_SLICE, { keywords: { move: { primary: 'انقل' } } }),
|
|
222
|
+
],
|
|
223
|
+
});
|
|
224
|
+
const roles = (input: string, language: string) => {
|
|
225
|
+
const node = dsl.parse(input, language);
|
|
226
|
+
return [
|
|
227
|
+
node.action,
|
|
228
|
+
...[...node.roles].map(([role, value]) => `${role}=${extractValue(value)}`),
|
|
229
|
+
];
|
|
230
|
+
};
|
|
215
231
|
|
|
216
232
|
it('SVO (en-like): keyword first, prepositional marker before its role', () => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
{ type: 'literal', value: 'move' },
|
|
222
|
-
{ type: 'role', role: 'patient', optional: false, expectedTypes: ['expression'] },
|
|
223
|
-
{ type: 'literal', value: 'into' },
|
|
224
|
-
{ type: 'role', role: 'destination', optional: false, expectedTypes: ['expression'] },
|
|
233
|
+
expect(roles('move box into shelf', 'en')).toEqual([
|
|
234
|
+
'move',
|
|
235
|
+
'patient=box',
|
|
236
|
+
'destination=shelf',
|
|
225
237
|
]);
|
|
226
238
|
});
|
|
227
239
|
|
|
228
240
|
it('SOV (ja-like): particles after their roles, verb last', () => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
{ type: 'role', role: 'patient', optional: false, expectedTypes: ['expression'] },
|
|
234
|
-
{ type: 'literal', value: 'を' },
|
|
235
|
-
{ type: 'role', role: 'destination', optional: false, expectedTypes: ['expression'] },
|
|
236
|
-
{ type: 'literal', value: 'に' },
|
|
237
|
-
{ type: 'literal', value: '移動' },
|
|
241
|
+
expect(roles('box を shelf に 移動', 'ja')).toEqual([
|
|
242
|
+
'move',
|
|
243
|
+
'patient=box',
|
|
244
|
+
'destination=shelf',
|
|
238
245
|
]);
|
|
239
246
|
});
|
|
240
247
|
|
|
241
248
|
it('VSO (ar-like): verb first', () => {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
249
|
+
expect(roles('انقل box في shelf', 'ar')).toEqual([
|
|
250
|
+
'move',
|
|
251
|
+
'patient=box',
|
|
252
|
+
'destination=shelf',
|
|
253
|
+
]);
|
|
246
254
|
});
|
|
247
255
|
});
|
|
248
256
|
});
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { LanguageTokenizer } from '../core/types';
|
|
13
|
-
import type { PatternGenLanguageProfile } from '../generation/
|
|
13
|
+
import type { PatternGenLanguageProfile } from '../generation/profile';
|
|
14
14
|
import type { LanguageConfig } from '../api/create-dsl';
|
|
15
15
|
import type { LanguageProfile as GrammarProfile } from '../grammar';
|
|
16
16
|
import { createSimpleTokenizer, type TokenizerProfile } from '../core/tokenization/base-tokenizer';
|
|
@@ -51,8 +51,8 @@ function mergeRoleMarkers(
|
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
53
|
* Build a pattern-generation profile: domain keywords + slice grammar
|
|
54
|
-
* (word order, role markers). The result feeds `
|
|
55
|
-
*
|
|
54
|
+
* (word order, role markers). The result feeds `LanguageConfig.patternProfile`
|
|
55
|
+
* and the schema renderer.
|
|
56
56
|
*/
|
|
57
57
|
export function buildPatternProfile(
|
|
58
58
|
slice: GrammarProfileSlice,
|
package/src/parsing/index.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
// Re-export core parsing types
|
|
6
6
|
export type { SemanticNode, TokenStream, LanguageTokenizer } from '../core/types';
|
|
7
|
-
export { PatternMatcher } from '../core/pattern-matching';
|
|
8
7
|
|
|
9
8
|
// Multi-statement parser
|
|
10
9
|
export { createMultiStatementParser, accumulateBlocks } from './multi-statement';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { CommandSchema } from '../../../schema';
|
|
6
6
|
import type { LanguageTokenizer } from '../../../core/types';
|
|
7
|
-
import type { PatternGenLanguageProfile } from '../../../generation/
|
|
7
|
+
import type { PatternGenLanguageProfile } from '../../../generation/profile';
|
|
8
8
|
|
|
9
9
|
export type Severity = 'error' | 'warning' | 'note';
|
|
10
10
|
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The round-trip gate kit on framework's SQL fixture DSL: the commands it writes,
|
|
3
|
-
* the outcomes it reads
|
|
3
|
+
* the outcomes it reads (an unknown word read into a role among them), and the
|
|
4
|
+
* baseline and snapshot it compares with.
|
|
4
5
|
*/
|
|
5
6
|
import { describe, expect, it } from 'vitest';
|
|
6
|
-
import { createSQLDSL, selectCommand } from '../../__test__/fixtures/sql-dsl';
|
|
7
|
+
import { createSQLDSL, selectCommand, sqlDSLConfig } from '../../__test__/fixtures/sql-dsl';
|
|
8
|
+
import { createMultilingualDSL } from '../../api/create-dsl';
|
|
7
9
|
import {
|
|
8
10
|
commandsFor,
|
|
9
11
|
compare,
|
|
10
12
|
countsOf,
|
|
11
13
|
listingOf,
|
|
14
|
+
readsWord,
|
|
12
15
|
runRoundTrip,
|
|
13
16
|
snapshotChanges,
|
|
14
17
|
snapshotOf,
|
|
@@ -42,11 +45,19 @@ describe('runRoundTrip', () => {
|
|
|
42
45
|
]);
|
|
43
46
|
});
|
|
44
47
|
|
|
48
|
+
it("writes a fixture's own commands after the generated ones, once each", () => {
|
|
49
|
+
const own = '[select columns:email source:users condition:age]';
|
|
50
|
+
expect(
|
|
51
|
+
commandsFor([selectCommand], { ...fixture, commands: [own, '[select columns:id source:t]'] })
|
|
52
|
+
).toEqual([...commandsFor([selectCommand], fixture), '[select columns:id source:t]']);
|
|
53
|
+
expect(commandsFor([selectCommand], fixture)).toContain(own);
|
|
54
|
+
});
|
|
55
|
+
|
|
45
56
|
it('reads every render back, in every language, and tries each with an unknown word after it', () => {
|
|
46
57
|
expect(countsOf(results)).toEqual({
|
|
47
58
|
roundTrip: { cases: 8, pass: 8, refused: 0, silent: 0 },
|
|
48
|
-
// The `where` condition is greedy: it takes the unknown word as text
|
|
49
|
-
leftover: { cases: 8, pass:
|
|
59
|
+
// The `where` condition is greedy: it takes the unknown word as text, into the SQL (`pass`).
|
|
60
|
+
leftover: { cases: 8, pass: 4, refused: 4, silent: 0 },
|
|
50
61
|
});
|
|
51
62
|
expect(results.find(r => r.kind === 'roundTrip' && r.lang === 'es')).toMatchObject({
|
|
52
63
|
text: 'seleccionar name de users',
|
|
@@ -65,12 +76,14 @@ describe('compare', () => {
|
|
|
65
76
|
});
|
|
66
77
|
|
|
67
78
|
it('reports a case worse than listed, one better, and a listed case that is gone', () => {
|
|
79
|
+
// Two leftover cases accepted that the baseline lists as refused.
|
|
80
|
+
const accepted = '[select columns:name source:users]';
|
|
81
|
+
const now = results.map(r =>
|
|
82
|
+
r.kind === 'leftover' && r.source === accepted ? { ...r, outcome: 'silent' as const } : r
|
|
83
|
+
);
|
|
68
84
|
const listing = listingOf(results);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
delete better[accepted];
|
|
72
|
-
const drift = compare('sql', results, {
|
|
73
|
-
...baselineOf({ roundTrip: listing.roundTrip, leftover: better }),
|
|
85
|
+
const drift = compare('sql', now, {
|
|
86
|
+
...baselineOf(listing),
|
|
74
87
|
roundTrip: {
|
|
75
88
|
sql: {
|
|
76
89
|
'[select columns:name source:users]': {
|
|
@@ -97,6 +110,83 @@ describe('compare', () => {
|
|
|
97
110
|
});
|
|
98
111
|
});
|
|
99
112
|
|
|
113
|
+
describe('an unknown word read into a role', () => {
|
|
114
|
+
const leftover = (rs: typeof results) =>
|
|
115
|
+
rs
|
|
116
|
+
.filter(r => r.kind === 'leftover' && r.source.includes('condition'))
|
|
117
|
+
.map(r => [r.lang, r.outcome, r.why]);
|
|
118
|
+
|
|
119
|
+
it('is read (`pass`) where it extends an open-text role and reaches the code', () => {
|
|
120
|
+
expect(leftover(results)).toEqual([
|
|
121
|
+
['en', 'pass', undefined],
|
|
122
|
+
['es', 'pass', undefined],
|
|
123
|
+
['en', 'pass', undefined],
|
|
124
|
+
['es', 'pass', undefined],
|
|
125
|
+
]);
|
|
126
|
+
expect(results.find(r => r.kind === 'leftover' && r.source.includes('condition'))?.read).toBe(
|
|
127
|
+
'select columns:expression=name condition:expression="age zzqx" source:expression=users'
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('is accepted (`silent`) where it does not reach the code', () => {
|
|
132
|
+
const dropsCondition = createMultilingualDSL({
|
|
133
|
+
...sqlDSLConfig(),
|
|
134
|
+
codeGenerator: { generate: () => 'SELECT name FROM users' },
|
|
135
|
+
});
|
|
136
|
+
const rs = runRoundTrip('sql', dropsCondition, [selectCommand], fixture);
|
|
137
|
+
expect(leftover(rs)[0]).toEqual([
|
|
138
|
+
'en',
|
|
139
|
+
'silent',
|
|
140
|
+
'accepted as select; the word does not reach the code',
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('is refused where the input stops compiling, unless the command never compiles on its own', () => {
|
|
145
|
+
const refusesTheWord = createMultilingualDSL({
|
|
146
|
+
...sqlDSLConfig(),
|
|
147
|
+
codeGenerator: {
|
|
148
|
+
generate: n => {
|
|
149
|
+
const c = n.roles.get('condition') as { raw?: string } | undefined;
|
|
150
|
+
if (c?.raw?.includes('zzqx')) throw new Error('not a condition');
|
|
151
|
+
return 'SELECT';
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const rs = runRoundTrip('sql', refusesTheWord, [selectCommand], fixture);
|
|
156
|
+
expect(leftover(rs)[0]).toEqual(['en', 'refused', 'does not compile']);
|
|
157
|
+
const alone = runRoundTrip('sql', refusesTheWord, [selectCommand], {
|
|
158
|
+
...fixture,
|
|
159
|
+
englishDoesNotCompile: ['select'],
|
|
160
|
+
});
|
|
161
|
+
expect(leftover(alone)[0]).toEqual(['en', 'pass', undefined]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const node = (action: string, roles: Record<string, string>) => ({
|
|
165
|
+
action,
|
|
166
|
+
roles: new Map(Object.entries(roles).map(([k, v]) => [k, { type: 'expression', raw: v }])),
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it.each([
|
|
170
|
+
['extends one role', { condition: 'age' }, { condition: 'age zzqx' }, true],
|
|
171
|
+
['fills a role the text left empty', {}, { list: 'zzqx' }, true],
|
|
172
|
+
[
|
|
173
|
+
'moves a value to make room for it (`when clicks zzqx`)',
|
|
174
|
+
{ action: 'clicks' },
|
|
175
|
+
{ actor: 'clicks', action: 'zzqx' },
|
|
176
|
+
false,
|
|
177
|
+
],
|
|
178
|
+
['replaces a value', { quantity: '2' }, { quantity: 'zzqx' }, false],
|
|
179
|
+
['is in two roles', {}, { a: 'zzqx', b: 'zzqx' }, false],
|
|
180
|
+
['is not read at all', { a: 'x' }, { a: 'x' }, false],
|
|
181
|
+
] as const)('a parse that %s is the text read with the word: %s', (_, was, now, want) => {
|
|
182
|
+
expect(readsWord(node('when', was), node('when', now), 'zzqx')).toBe(want);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('a parse as another command is not', () => {
|
|
186
|
+
expect(readsWord(node('when', {}), node('given', { a: 'zzqx' }), 'zzqx')).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
100
190
|
describe('the parse snapshot', () => {
|
|
101
191
|
it('shows a parse that moved', () => {
|
|
102
192
|
const parses = snapshotOf(results);
|
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
* generator normalizes: bdd ja `#box0 が 表示 前提` is `visible`). English is a lane like the others.
|
|
14
14
|
* `refused` when the render is null or the parse throws: the caller is told. `silent` otherwise: it
|
|
15
15
|
* reads, or compiles to, something else.
|
|
16
|
-
* - **leftover**: the same render plus one unknown word. `refused` is the expected outcome
|
|
17
|
-
*
|
|
16
|
+
* - **leftover**: the same render plus one unknown word. `refused` is the expected outcome: the parse
|
|
17
|
+
* throws, or the input does not compile. `pass` where the word is READ: the parse is the text's own
|
|
18
|
+
* with the word in one role (an open-text role extended by it, `where active zzqx`, or a role the
|
|
19
|
+
* text left empty, `list zzqx`), and the word is in the code (for a command that never compiles on
|
|
20
|
+
* its own, `englishDoesNotCompile`, the parse alone). `silent` otherwise: the input is accepted and
|
|
21
|
+
* read as something else (`when clicks zzqx`: actor `clicks`, action `zzqx`), or the word goes
|
|
22
|
+
* nowhere (voice `back zzqx` compiles as `back`).
|
|
18
23
|
*
|
|
19
24
|
* The outcomes are the command-shape gate's (hyperfixi `testing-framework`). A baseline lists every
|
|
20
25
|
* case that is not at its best outcome; `compare` reports a case worse than listed and one better than
|
|
@@ -93,7 +98,16 @@ export interface GateFixture {
|
|
|
93
98
|
oracle: 'compile' | 'node';
|
|
94
99
|
/** Keys are `<action>.<role>` or `<role>` (every action). The first value is the one the other cases use. */
|
|
95
100
|
values: Record<string, string[]>;
|
|
96
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* Commands (bracket syntax) written besides the generated ones, where one role's value depends on
|
|
103
|
+
* another's and no single list fits both: behaviorspec's `[given subject:#box value:visible]` beside
|
|
104
|
+
* `[given subject:viewport value:1024x768]`.
|
|
105
|
+
*/
|
|
106
|
+
commands?: string[];
|
|
107
|
+
/**
|
|
108
|
+
* Actions that do not compile on their own (a gate may check every other one does). Leftover input
|
|
109
|
+
* in such a command is judged by its parse alone.
|
|
110
|
+
*/
|
|
97
111
|
englishDoesNotCompile?: string[];
|
|
98
112
|
}
|
|
99
113
|
|
|
@@ -132,6 +146,7 @@ export function commandsFor(schemas: readonly GateSchema[], fixture: GateFixture
|
|
|
132
146
|
});
|
|
133
147
|
}
|
|
134
148
|
}
|
|
149
|
+
for (const command of fixture.commands ?? []) out.add(command);
|
|
135
150
|
return [...out];
|
|
136
151
|
}
|
|
137
152
|
|
|
@@ -181,6 +196,35 @@ function describeRead(want: Record<string, string>, ref: GateNode, back: GateNod
|
|
|
181
196
|
|
|
182
197
|
const message = (e: unknown) => String((e as Error)?.message ?? e).slice(0, 160);
|
|
183
198
|
|
|
199
|
+
/** What `text` compiles to, or `undefined` when it does not compile. */
|
|
200
|
+
function codeOf(dsl: GateDSL, text: string, lang: string): string | undefined {
|
|
201
|
+
try {
|
|
202
|
+
const c = dsl.compile(text, lang);
|
|
203
|
+
return c.ok && typeof c.code === 'string' ? c.code : undefined;
|
|
204
|
+
} catch {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Whether `read`, the parse of a text plus `word`, is `back`, the text's own parse, with the word read
|
|
211
|
+
* into it: the same action and roles, except one role, whose value is its value in `back` followed by
|
|
212
|
+
* the word (an open-text role at the end: sql `where active zzqx`) or, where `back` has no such role,
|
|
213
|
+
* the word itself (todo `list zzqx`).
|
|
214
|
+
*/
|
|
215
|
+
export function readsWord(back: GateNode, read: GateNode, word: string): boolean {
|
|
216
|
+
if (read.action !== back.action) return false;
|
|
217
|
+
const was = roleValues(back);
|
|
218
|
+
const now = roleValues(read);
|
|
219
|
+
let changed = 0;
|
|
220
|
+
for (const role of new Set([...Object.keys(was), ...Object.keys(now)])) {
|
|
221
|
+
if (was[role] === now[role]) continue;
|
|
222
|
+
if (now[role] !== (role in was ? `${was[role]} ${word}` : word)) return false;
|
|
223
|
+
changed++;
|
|
224
|
+
}
|
|
225
|
+
return changed === 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
184
228
|
/** What a command (bracket syntax) compiles to; null when it does not compile on its own. */
|
|
185
229
|
export function referenceCode(dsl: GateDSL, source: string): string | null {
|
|
186
230
|
try {
|
|
@@ -253,18 +297,39 @@ export function runRoundTrip(
|
|
|
253
297
|
|
|
254
298
|
// Leftover input: only where the text itself reads, or a refusal would say nothing.
|
|
255
299
|
const extended = `${text} ${UNKNOWN_WORD}`;
|
|
300
|
+
let read: GateNode;
|
|
256
301
|
try {
|
|
257
|
-
|
|
258
|
-
results.push({
|
|
259
|
-
...at,
|
|
260
|
-
kind: 'leftover',
|
|
261
|
-
outcome: 'silent',
|
|
262
|
-
text: extended,
|
|
263
|
-
why: `accepted as ${read.action}`,
|
|
264
|
-
});
|
|
302
|
+
read = dsl.parse(extended, lang);
|
|
265
303
|
} catch {
|
|
266
304
|
results.push({ ...at, kind: 'leftover', outcome: 'refused', text: extended });
|
|
305
|
+
continue;
|
|
267
306
|
}
|
|
307
|
+
// Read with the word, the word must reach the code. Where the input does not compile, compile
|
|
308
|
+
// tells the caller (`refused`), except for a command that never compiles on its own (bdd's
|
|
309
|
+
// `and`), where only the parse can say.
|
|
310
|
+
const kept = readsWord(back, read, UNKNOWN_WORD);
|
|
311
|
+
const code = kept ? codeOf(dsl, extended, lang) : undefined;
|
|
312
|
+
const alone = fixture.englishDoesNotCompile?.includes(read.action) ?? false;
|
|
313
|
+
const [left, leftWhy]: [Outcome, string | undefined] = !kept
|
|
314
|
+
? [
|
|
315
|
+
'silent',
|
|
316
|
+
`accepted as ${read.action}; reads ${describeRead(roleValues(back), back, read)}`,
|
|
317
|
+
]
|
|
318
|
+
: code !== undefined
|
|
319
|
+
? code.includes(UNKNOWN_WORD)
|
|
320
|
+
? ['pass', undefined]
|
|
321
|
+
: ['silent', `accepted as ${read.action}; the word does not reach the code`]
|
|
322
|
+
: alone
|
|
323
|
+
? ['pass', undefined]
|
|
324
|
+
: ['refused', 'does not compile'];
|
|
325
|
+
results.push({
|
|
326
|
+
...at,
|
|
327
|
+
kind: 'leftover',
|
|
328
|
+
outcome: left,
|
|
329
|
+
text: extended,
|
|
330
|
+
...(leftWhy ? { why: leftWhy } : {}),
|
|
331
|
+
read: summarize(read),
|
|
332
|
+
});
|
|
268
333
|
}
|
|
269
334
|
}
|
|
270
335
|
return results;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/pattern-matching/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC"}
|