@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
|
@@ -1,1031 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pattern Matcher Tests
|
|
3
|
-
*
|
|
4
|
-
* Tests the core pattern matching algorithm that converts token streams
|
|
5
|
-
* into semantic representations by matching against language patterns.
|
|
6
|
-
*
|
|
7
|
-
* Uses PatternGenerator to create realistic patterns rather than manual construction.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
11
|
-
import { PatternMatcher, type PatternMatcherProfile } from './pattern-matcher';
|
|
12
|
-
import type { LanguagePattern } from '../types';
|
|
13
|
-
import { createMockTokenStream, expectConfidenceRange } from '../../__test__/test-utils';
|
|
14
|
-
import { createToken, createPosition, TokenStreamImpl } from '../tokenization';
|
|
15
|
-
import {
|
|
16
|
-
generatePattern,
|
|
17
|
-
type PatternGenLanguageProfile,
|
|
18
|
-
} from '../../generation/pattern-generator';
|
|
19
|
-
import { defineCommand, defineRole } from '../../schema';
|
|
20
|
-
|
|
21
|
-
describe('PatternMatcher', () => {
|
|
22
|
-
let matcher: PatternMatcher;
|
|
23
|
-
|
|
24
|
-
beforeEach(() => {
|
|
25
|
-
matcher = new PatternMatcher();
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('Core Matching Algorithm', () => {
|
|
29
|
-
describe('exact literal matching', () => {
|
|
30
|
-
it('should match a simple command with role', () => {
|
|
31
|
-
const schema = defineCommand({
|
|
32
|
-
action: 'toggle',
|
|
33
|
-
roles: [
|
|
34
|
-
defineRole({
|
|
35
|
-
role: 'patient',
|
|
36
|
-
required: true,
|
|
37
|
-
expectedTypes: ['expression'],
|
|
38
|
-
}),
|
|
39
|
-
],
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const profile: PatternGenLanguageProfile = {
|
|
43
|
-
code: 'en',
|
|
44
|
-
wordOrder: 'SVO',
|
|
45
|
-
keywords: {
|
|
46
|
-
toggle: { primary: 'toggle' },
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const pattern = generatePattern(schema, profile);
|
|
51
|
-
|
|
52
|
-
const tokens = createMockTokenStream([
|
|
53
|
-
createToken({ kind: 'keyword', value: 'toggle', position: createPosition(0, 6) }),
|
|
54
|
-
createToken({ kind: 'identifier', value: 'button', position: createPosition(7, 13) }),
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
58
|
-
|
|
59
|
-
expect(result).not.toBeNull();
|
|
60
|
-
expect(result?.captured.has('patient')).toBe(true);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should fail when keyword does not match', () => {
|
|
64
|
-
const schema = defineCommand({
|
|
65
|
-
action: 'toggle',
|
|
66
|
-
roles: [],
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const profile: PatternGenLanguageProfile = {
|
|
70
|
-
code: 'en',
|
|
71
|
-
wordOrder: 'SVO',
|
|
72
|
-
keywords: {
|
|
73
|
-
toggle: { primary: 'toggle' },
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const pattern = generatePattern(schema, profile);
|
|
78
|
-
|
|
79
|
-
const tokens = createMockTokenStream([
|
|
80
|
-
createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
|
|
81
|
-
]);
|
|
82
|
-
|
|
83
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
84
|
-
|
|
85
|
-
expect(result).toBeNull();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should match with role markers', () => {
|
|
89
|
-
const schema = defineCommand({
|
|
90
|
-
action: 'get',
|
|
91
|
-
roles: [
|
|
92
|
-
defineRole({
|
|
93
|
-
role: 'source',
|
|
94
|
-
required: true,
|
|
95
|
-
expectedTypes: ['expression'],
|
|
96
|
-
markerOverride: { en: 'from' },
|
|
97
|
-
}),
|
|
98
|
-
],
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const profile: PatternGenLanguageProfile = {
|
|
102
|
-
code: 'en',
|
|
103
|
-
wordOrder: 'SVO',
|
|
104
|
-
keywords: {
|
|
105
|
-
get: { primary: 'get' },
|
|
106
|
-
},
|
|
107
|
-
roleMarkers: {
|
|
108
|
-
source: { primary: 'from', position: 'before' },
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const pattern = generatePattern(schema, profile);
|
|
113
|
-
|
|
114
|
-
const tokens = createMockTokenStream([
|
|
115
|
-
createToken({ kind: 'keyword', value: 'get', position: createPosition(0, 3) }),
|
|
116
|
-
createToken({ kind: 'keyword', value: 'from', position: createPosition(4, 8) }),
|
|
117
|
-
createToken({ kind: 'identifier', value: 'users', position: createPosition(9, 14) }),
|
|
118
|
-
]);
|
|
119
|
-
|
|
120
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
121
|
-
|
|
122
|
-
expect(result).not.toBeNull();
|
|
123
|
-
expect(result?.captured.has('source')).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe('role capture', () => {
|
|
128
|
-
it('should capture multiple roles in SQL-like pattern', () => {
|
|
129
|
-
const schema = defineCommand({
|
|
130
|
-
action: 'select',
|
|
131
|
-
roles: [
|
|
132
|
-
defineRole({
|
|
133
|
-
role: 'columns',
|
|
134
|
-
required: true,
|
|
135
|
-
expectedTypes: ['expression'],
|
|
136
|
-
svoPosition: 2,
|
|
137
|
-
}),
|
|
138
|
-
defineRole({
|
|
139
|
-
role: 'source',
|
|
140
|
-
required: true,
|
|
141
|
-
expectedTypes: ['expression'],
|
|
142
|
-
svoPosition: 1,
|
|
143
|
-
markerOverride: { en: 'from' },
|
|
144
|
-
}),
|
|
145
|
-
],
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const profile: PatternGenLanguageProfile = {
|
|
149
|
-
code: 'en',
|
|
150
|
-
wordOrder: 'SVO',
|
|
151
|
-
keywords: {
|
|
152
|
-
select: { primary: 'select' },
|
|
153
|
-
},
|
|
154
|
-
roleMarkers: {
|
|
155
|
-
source: { primary: 'from', position: 'before' },
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const pattern = generatePattern(schema, profile);
|
|
160
|
-
|
|
161
|
-
const tokens = createMockTokenStream([
|
|
162
|
-
createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
|
|
163
|
-
createToken({ kind: 'identifier', value: 'name', position: createPosition(7, 11) }),
|
|
164
|
-
createToken({ kind: 'keyword', value: 'from', position: createPosition(12, 16) }),
|
|
165
|
-
createToken({ kind: 'identifier', value: 'users', position: createPosition(17, 22) }),
|
|
166
|
-
]);
|
|
167
|
-
|
|
168
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
169
|
-
|
|
170
|
-
expect(result).not.toBeNull();
|
|
171
|
-
expect(result?.captured.has('columns')).toBe(true);
|
|
172
|
-
expect(result?.captured.has('source')).toBe(true);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should match keyword tokens against roles expecting expression type', () => {
|
|
176
|
-
// Regression: keywords produce 'literal' semantic type but 'expression'
|
|
177
|
-
// in expectedTypes should act as a wildcard accepting any type.
|
|
178
|
-
const schema = defineCommand({
|
|
179
|
-
action: 'given',
|
|
180
|
-
roles: [
|
|
181
|
-
defineRole({
|
|
182
|
-
role: 'target',
|
|
183
|
-
required: true,
|
|
184
|
-
expectedTypes: ['expression', 'selector'],
|
|
185
|
-
svoPosition: 2,
|
|
186
|
-
}),
|
|
187
|
-
defineRole({
|
|
188
|
-
role: 'state',
|
|
189
|
-
required: true,
|
|
190
|
-
expectedTypes: ['expression'],
|
|
191
|
-
svoPosition: 1,
|
|
192
|
-
markerOverride: { en: 'is' },
|
|
193
|
-
}),
|
|
194
|
-
],
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const profile: PatternGenLanguageProfile = {
|
|
198
|
-
code: 'en',
|
|
199
|
-
wordOrder: 'SVO',
|
|
200
|
-
keywords: {
|
|
201
|
-
given: { primary: 'given' },
|
|
202
|
-
},
|
|
203
|
-
roleMarkers: {
|
|
204
|
-
state: { primary: 'is', position: 'before' },
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const pattern = generatePattern(schema, profile);
|
|
209
|
-
|
|
210
|
-
// 'exists' is a keyword token filling the state role
|
|
211
|
-
const tokens = createMockTokenStream([
|
|
212
|
-
createToken({ kind: 'keyword', value: 'given', position: createPosition(0, 5) }),
|
|
213
|
-
createToken({ kind: 'selector', value: '#button', position: createPosition(6, 13) }),
|
|
214
|
-
createToken({ kind: 'keyword', value: 'is', position: createPosition(14, 16) }),
|
|
215
|
-
createToken({ kind: 'keyword', value: 'exists', position: createPosition(17, 23) }),
|
|
216
|
-
]);
|
|
217
|
-
|
|
218
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
219
|
-
|
|
220
|
-
expect(result).not.toBeNull();
|
|
221
|
-
expect(result?.captured.has('target')).toBe(true);
|
|
222
|
-
expect(result?.captured.has('state')).toBe(true);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('should apply default values for optional roles', () => {
|
|
226
|
-
const schema = defineCommand({
|
|
227
|
-
action: 'select',
|
|
228
|
-
roles: [
|
|
229
|
-
defineRole({
|
|
230
|
-
role: 'columns',
|
|
231
|
-
required: false,
|
|
232
|
-
expectedTypes: ['expression'],
|
|
233
|
-
}),
|
|
234
|
-
],
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const profile: PatternGenLanguageProfile = {
|
|
238
|
-
code: 'en',
|
|
239
|
-
wordOrder: 'SVO',
|
|
240
|
-
keywords: {
|
|
241
|
-
select: { primary: 'select' },
|
|
242
|
-
},
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const pattern = generatePattern(schema, profile);
|
|
246
|
-
|
|
247
|
-
// Add default value in extraction
|
|
248
|
-
pattern.extraction.columns = {
|
|
249
|
-
...pattern.extraction.columns,
|
|
250
|
-
default: { type: 'literal', value: '*' },
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const tokens = createMockTokenStream([
|
|
254
|
-
createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
|
|
255
|
-
]);
|
|
256
|
-
|
|
257
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
258
|
-
|
|
259
|
-
expect(result).not.toBeNull();
|
|
260
|
-
// Default should be applied
|
|
261
|
-
expect(result?.captured.has('columns')).toBe(true);
|
|
262
|
-
const columns = result?.captured.get('columns');
|
|
263
|
-
if (columns && columns.type === 'literal') {
|
|
264
|
-
expect(columns.value).toBe('*');
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
describe('SOV word order', () => {
|
|
270
|
-
it('should match Japanese-style SOV pattern', () => {
|
|
271
|
-
const schema = defineCommand({
|
|
272
|
-
action: 'toggle',
|
|
273
|
-
roles: [
|
|
274
|
-
defineRole({
|
|
275
|
-
role: 'patient',
|
|
276
|
-
required: true,
|
|
277
|
-
expectedTypes: ['expression'],
|
|
278
|
-
sovPosition: 1,
|
|
279
|
-
}),
|
|
280
|
-
],
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
const profile: PatternGenLanguageProfile = {
|
|
284
|
-
code: 'ja',
|
|
285
|
-
wordOrder: 'SOV',
|
|
286
|
-
keywords: {
|
|
287
|
-
toggle: { primary: 'トグル' },
|
|
288
|
-
},
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const pattern = generatePattern(schema, profile);
|
|
292
|
-
|
|
293
|
-
const tokens = createMockTokenStream([
|
|
294
|
-
createToken({ kind: 'identifier', value: 'button', position: createPosition(0, 6) }),
|
|
295
|
-
createToken({ kind: 'keyword', value: 'トグル', position: createPosition(7, 11) }),
|
|
296
|
-
]);
|
|
297
|
-
|
|
298
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
299
|
-
|
|
300
|
-
expect(result).not.toBeNull();
|
|
301
|
-
expect(result?.captured.has('patient')).toBe(true);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('should stop greedy role inside group at parent verb keyword (SOV WHERE)', () => {
|
|
305
|
-
// SOV pattern: [source] から [columns] [GROUP: 条件 condition(greedy)] 選択
|
|
306
|
-
// Input: users から name 条件 age > 18 選択
|
|
307
|
-
// The greedy condition role must stop at 選択 (the verb keyword outside the group)
|
|
308
|
-
const schema = defineCommand({
|
|
309
|
-
action: 'select',
|
|
310
|
-
roles: [
|
|
311
|
-
defineRole({
|
|
312
|
-
role: 'columns',
|
|
313
|
-
required: true,
|
|
314
|
-
expectedTypes: ['expression'],
|
|
315
|
-
svoPosition: 2,
|
|
316
|
-
sovPosition: 1,
|
|
317
|
-
}),
|
|
318
|
-
defineRole({
|
|
319
|
-
role: 'source',
|
|
320
|
-
required: true,
|
|
321
|
-
expectedTypes: ['expression'],
|
|
322
|
-
svoPosition: 1,
|
|
323
|
-
sovPosition: 2,
|
|
324
|
-
markerOverride: { ja: 'から' },
|
|
325
|
-
}),
|
|
326
|
-
defineRole({
|
|
327
|
-
role: 'condition',
|
|
328
|
-
required: false,
|
|
329
|
-
expectedTypes: ['expression'],
|
|
330
|
-
greedy: true,
|
|
331
|
-
svoPosition: 0,
|
|
332
|
-
sovPosition: 0,
|
|
333
|
-
markerOverride: { ja: '条件' },
|
|
334
|
-
}),
|
|
335
|
-
],
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
const profile: PatternGenLanguageProfile = {
|
|
339
|
-
code: 'ja',
|
|
340
|
-
wordOrder: 'SOV',
|
|
341
|
-
keywords: {
|
|
342
|
-
select: { primary: '選択' },
|
|
343
|
-
},
|
|
344
|
-
roleMarkers: {
|
|
345
|
-
condition: { primary: '条件', position: 'before' },
|
|
346
|
-
},
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
const pattern = generatePattern(schema, profile);
|
|
350
|
-
|
|
351
|
-
// Input: users から name 条件 age > 18 選択
|
|
352
|
-
const tokens = createMockTokenStream([
|
|
353
|
-
createToken({ kind: 'identifier', value: 'users', position: createPosition(0, 5) }),
|
|
354
|
-
createToken({ kind: 'keyword', value: 'から', position: createPosition(6, 8) }),
|
|
355
|
-
createToken({ kind: 'identifier', value: 'name', position: createPosition(9, 13) }),
|
|
356
|
-
createToken({ kind: 'keyword', value: '条件', position: createPosition(14, 16) }),
|
|
357
|
-
createToken({ kind: 'identifier', value: 'age', position: createPosition(17, 20) }),
|
|
358
|
-
createToken({ kind: 'operator', value: '>', position: createPosition(21, 22) }),
|
|
359
|
-
createToken({ kind: 'literal', value: '18', position: createPosition(23, 25) }),
|
|
360
|
-
createToken({ kind: 'keyword', value: '選択', position: createPosition(26, 28) }),
|
|
361
|
-
]);
|
|
362
|
-
|
|
363
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
364
|
-
|
|
365
|
-
expect(result).not.toBeNull();
|
|
366
|
-
expect(result?.captured.has('source')).toBe(true);
|
|
367
|
-
expect(result?.captured.get('source')).toMatchObject({ raw: 'users' });
|
|
368
|
-
expect(result?.captured.has('columns')).toBe(true);
|
|
369
|
-
expect(result?.captured.get('columns')).toMatchObject({ raw: 'name' });
|
|
370
|
-
expect(result?.captured.has('condition')).toBe(true);
|
|
371
|
-
expect(result?.captured.get('condition')).toMatchObject({ raw: 'age > 18' });
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
it('should match SOV pattern without optional greedy group', () => {
|
|
375
|
-
// Same schema but input has no WHERE clause
|
|
376
|
-
// Input: users から name 選択
|
|
377
|
-
const schema = defineCommand({
|
|
378
|
-
action: 'select',
|
|
379
|
-
roles: [
|
|
380
|
-
defineRole({
|
|
381
|
-
role: 'columns',
|
|
382
|
-
required: true,
|
|
383
|
-
expectedTypes: ['expression'],
|
|
384
|
-
svoPosition: 2,
|
|
385
|
-
sovPosition: 1,
|
|
386
|
-
}),
|
|
387
|
-
defineRole({
|
|
388
|
-
role: 'source',
|
|
389
|
-
required: true,
|
|
390
|
-
expectedTypes: ['expression'],
|
|
391
|
-
svoPosition: 1,
|
|
392
|
-
sovPosition: 2,
|
|
393
|
-
markerOverride: { ja: 'から' },
|
|
394
|
-
}),
|
|
395
|
-
defineRole({
|
|
396
|
-
role: 'condition',
|
|
397
|
-
required: false,
|
|
398
|
-
expectedTypes: ['expression'],
|
|
399
|
-
greedy: true,
|
|
400
|
-
svoPosition: 0,
|
|
401
|
-
sovPosition: 0,
|
|
402
|
-
markerOverride: { ja: '条件' },
|
|
403
|
-
}),
|
|
404
|
-
],
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
const profile: PatternGenLanguageProfile = {
|
|
408
|
-
code: 'ja',
|
|
409
|
-
wordOrder: 'SOV',
|
|
410
|
-
keywords: {
|
|
411
|
-
select: { primary: '選択' },
|
|
412
|
-
},
|
|
413
|
-
roleMarkers: {
|
|
414
|
-
condition: { primary: '条件', position: 'before' },
|
|
415
|
-
},
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
const pattern = generatePattern(schema, profile);
|
|
419
|
-
|
|
420
|
-
const tokens = createMockTokenStream([
|
|
421
|
-
createToken({ kind: 'identifier', value: 'users', position: createPosition(0, 5) }),
|
|
422
|
-
createToken({ kind: 'keyword', value: 'から', position: createPosition(6, 8) }),
|
|
423
|
-
createToken({ kind: 'identifier', value: 'name', position: createPosition(9, 13) }),
|
|
424
|
-
createToken({ kind: 'keyword', value: '選択', position: createPosition(14, 16) }),
|
|
425
|
-
]);
|
|
426
|
-
|
|
427
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
428
|
-
|
|
429
|
-
expect(result).not.toBeNull();
|
|
430
|
-
expect(result?.captured.has('source')).toBe(true);
|
|
431
|
-
expect(result?.captured.has('columns')).toBe(true);
|
|
432
|
-
expect(result?.captured.has('condition')).toBe(false);
|
|
433
|
-
});
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
describe('Edge Cases', () => {
|
|
438
|
-
it('should return null for empty token stream', () => {
|
|
439
|
-
const schema = defineCommand({
|
|
440
|
-
action: 'toggle',
|
|
441
|
-
roles: [],
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
const profile: PatternGenLanguageProfile = {
|
|
445
|
-
code: 'en',
|
|
446
|
-
wordOrder: 'SVO',
|
|
447
|
-
keywords: {
|
|
448
|
-
toggle: { primary: 'toggle' },
|
|
449
|
-
},
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
const pattern = generatePattern(schema, profile);
|
|
453
|
-
const tokens = createMockTokenStream([]);
|
|
454
|
-
|
|
455
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
456
|
-
|
|
457
|
-
expect(result).toBeNull();
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it('should reset token position on failed match', () => {
|
|
461
|
-
const schema = defineCommand({
|
|
462
|
-
action: 'select',
|
|
463
|
-
roles: [
|
|
464
|
-
defineRole({
|
|
465
|
-
role: 'source',
|
|
466
|
-
required: true,
|
|
467
|
-
expectedTypes: ['expression'],
|
|
468
|
-
markerOverride: { en: 'from' },
|
|
469
|
-
}),
|
|
470
|
-
],
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
const profile: PatternGenLanguageProfile = {
|
|
474
|
-
code: 'en',
|
|
475
|
-
wordOrder: 'SVO',
|
|
476
|
-
keywords: {
|
|
477
|
-
select: { primary: 'select' },
|
|
478
|
-
},
|
|
479
|
-
roleMarkers: {
|
|
480
|
-
source: { primary: 'from', position: 'before' },
|
|
481
|
-
},
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
const pattern = generatePattern(schema, profile);
|
|
485
|
-
|
|
486
|
-
const tokens = createMockTokenStream([
|
|
487
|
-
createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
|
|
488
|
-
createToken({ kind: 'identifier', value: 'name', position: createPosition(7, 11) }),
|
|
489
|
-
// Missing 'from' marker - should fail
|
|
490
|
-
]);
|
|
491
|
-
|
|
492
|
-
const initialPosition = tokens.position();
|
|
493
|
-
matcher.matchPattern(tokens, pattern);
|
|
494
|
-
|
|
495
|
-
// Token position should be reset after failed match
|
|
496
|
-
expect(tokens.position()).toBe(initialPosition);
|
|
497
|
-
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
describe('Backtracking', () => {
|
|
501
|
-
it('should skip optional unmarked role when next literal needs the token', () => {
|
|
502
|
-
// Pattern: select [columns? (expression)] 'from' [source (expression)]
|
|
503
|
-
// Input: "select from users"
|
|
504
|
-
// Without backtracking: columns="from", then literal 'from' fails
|
|
505
|
-
// With backtracking: columns skipped, from matches literal, source="users"
|
|
506
|
-
const schema = defineCommand({
|
|
507
|
-
action: 'select',
|
|
508
|
-
roles: [
|
|
509
|
-
defineRole({
|
|
510
|
-
role: 'columns',
|
|
511
|
-
required: false,
|
|
512
|
-
expectedTypes: ['expression'],
|
|
513
|
-
svoPosition: 2,
|
|
514
|
-
}),
|
|
515
|
-
defineRole({
|
|
516
|
-
role: 'source',
|
|
517
|
-
required: true,
|
|
518
|
-
expectedTypes: ['expression'],
|
|
519
|
-
svoPosition: 1,
|
|
520
|
-
markerOverride: { en: 'from' },
|
|
521
|
-
}),
|
|
522
|
-
],
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
const profile: PatternGenLanguageProfile = {
|
|
526
|
-
code: 'en',
|
|
527
|
-
wordOrder: 'SVO',
|
|
528
|
-
keywords: {
|
|
529
|
-
select: { primary: 'select' },
|
|
530
|
-
},
|
|
531
|
-
roleMarkers: {
|
|
532
|
-
source: { primary: 'from', position: 'before' },
|
|
533
|
-
},
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
const pattern = generatePattern(schema, profile);
|
|
537
|
-
|
|
538
|
-
const tokens = createMockTokenStream([
|
|
539
|
-
createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
|
|
540
|
-
createToken({ kind: 'keyword', value: 'from', position: createPosition(7, 11) }),
|
|
541
|
-
createToken({ kind: 'identifier', value: 'users', position: createPosition(12, 17) }),
|
|
542
|
-
]);
|
|
543
|
-
|
|
544
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
545
|
-
|
|
546
|
-
expect(result).not.toBeNull();
|
|
547
|
-
expect(result?.captured.has('columns')).toBe(false);
|
|
548
|
-
expect(result?.captured.has('source')).toBe(true);
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
it('should still capture optional role when token is not the next literal', () => {
|
|
552
|
-
// Pattern: select [columns? (expression)] 'from' [source (expression)]
|
|
553
|
-
// Input: "select name from users"
|
|
554
|
-
// columns="name" (not a literal match), from matches, source="users"
|
|
555
|
-
const schema = defineCommand({
|
|
556
|
-
action: 'select',
|
|
557
|
-
roles: [
|
|
558
|
-
defineRole({
|
|
559
|
-
role: 'columns',
|
|
560
|
-
required: false,
|
|
561
|
-
expectedTypes: ['expression'],
|
|
562
|
-
svoPosition: 2,
|
|
563
|
-
}),
|
|
564
|
-
defineRole({
|
|
565
|
-
role: 'source',
|
|
566
|
-
required: true,
|
|
567
|
-
expectedTypes: ['expression'],
|
|
568
|
-
svoPosition: 1,
|
|
569
|
-
markerOverride: { en: 'from' },
|
|
570
|
-
}),
|
|
571
|
-
],
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
const profile: PatternGenLanguageProfile = {
|
|
575
|
-
code: 'en',
|
|
576
|
-
wordOrder: 'SVO',
|
|
577
|
-
keywords: {
|
|
578
|
-
select: { primary: 'select' },
|
|
579
|
-
},
|
|
580
|
-
roleMarkers: {
|
|
581
|
-
source: { primary: 'from', position: 'before' },
|
|
582
|
-
},
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
const pattern = generatePattern(schema, profile);
|
|
586
|
-
|
|
587
|
-
const tokens = createMockTokenStream([
|
|
588
|
-
createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
|
|
589
|
-
createToken({ kind: 'identifier', value: 'name', position: createPosition(7, 11) }),
|
|
590
|
-
createToken({ kind: 'keyword', value: 'from', position: createPosition(12, 16) }),
|
|
591
|
-
createToken({ kind: 'identifier', value: 'users', position: createPosition(17, 22) }),
|
|
592
|
-
]);
|
|
593
|
-
|
|
594
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
595
|
-
|
|
596
|
-
expect(result).not.toBeNull();
|
|
597
|
-
expect(result?.captured.has('columns')).toBe(true);
|
|
598
|
-
expect(result?.captured.has('source')).toBe(true);
|
|
599
|
-
});
|
|
600
|
-
|
|
601
|
-
it('should backtrack optional role when keyword was consumed as identifier', () => {
|
|
602
|
-
// Pattern: command [target? (expression)] 'to' [destination (expression)]
|
|
603
|
-
// Input: "command to output"
|
|
604
|
-
// "to" is keyword, optional target consumes it, then literal 'to' fails
|
|
605
|
-
// Backtrack: skip target, 'to' matches literal, destination="output"
|
|
606
|
-
const schema = defineCommand({
|
|
607
|
-
action: 'send',
|
|
608
|
-
roles: [
|
|
609
|
-
defineRole({
|
|
610
|
-
role: 'target',
|
|
611
|
-
required: false,
|
|
612
|
-
expectedTypes: ['expression'],
|
|
613
|
-
svoPosition: 2,
|
|
614
|
-
}),
|
|
615
|
-
defineRole({
|
|
616
|
-
role: 'destination',
|
|
617
|
-
required: true,
|
|
618
|
-
expectedTypes: ['expression'],
|
|
619
|
-
svoPosition: 1,
|
|
620
|
-
markerOverride: { en: 'to' },
|
|
621
|
-
}),
|
|
622
|
-
],
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
const profile: PatternGenLanguageProfile = {
|
|
626
|
-
code: 'en',
|
|
627
|
-
wordOrder: 'SVO',
|
|
628
|
-
keywords: {
|
|
629
|
-
send: { primary: 'send' },
|
|
630
|
-
},
|
|
631
|
-
roleMarkers: {
|
|
632
|
-
destination: { primary: 'to', position: 'before' },
|
|
633
|
-
},
|
|
634
|
-
};
|
|
635
|
-
|
|
636
|
-
const pattern = generatePattern(schema, profile);
|
|
637
|
-
|
|
638
|
-
const tokens = createMockTokenStream([
|
|
639
|
-
createToken({ kind: 'keyword', value: 'send', position: createPosition(0, 4) }),
|
|
640
|
-
createToken({ kind: 'keyword', value: 'to', position: createPosition(5, 7) }),
|
|
641
|
-
createToken({ kind: 'identifier', value: 'output', position: createPosition(8, 14) }),
|
|
642
|
-
]);
|
|
643
|
-
|
|
644
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
645
|
-
|
|
646
|
-
expect(result).not.toBeNull();
|
|
647
|
-
expect(result?.captured.has('target')).toBe(false);
|
|
648
|
-
expect(result?.captured.has('destination')).toBe(true);
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
it('should not backtrack when optional role has a marker (group)', () => {
|
|
652
|
-
// When optional roles have markers, they form groups that already
|
|
653
|
-
// handle backtracking via matchGroupToken. Verify this still works.
|
|
654
|
-
// Pattern: command ['with' target?] 'from' [source]
|
|
655
|
-
// Input: "command from users"
|
|
656
|
-
// The optional group [with target] is skipped (no 'with' found)
|
|
657
|
-
const schema = defineCommand({
|
|
658
|
-
action: 'get',
|
|
659
|
-
roles: [
|
|
660
|
-
defineRole({
|
|
661
|
-
role: 'target',
|
|
662
|
-
required: false,
|
|
663
|
-
expectedTypes: ['expression'],
|
|
664
|
-
svoPosition: 2,
|
|
665
|
-
markerOverride: { en: 'with' },
|
|
666
|
-
}),
|
|
667
|
-
defineRole({
|
|
668
|
-
role: 'source',
|
|
669
|
-
required: true,
|
|
670
|
-
expectedTypes: ['expression'],
|
|
671
|
-
svoPosition: 1,
|
|
672
|
-
markerOverride: { en: 'from' },
|
|
673
|
-
}),
|
|
674
|
-
],
|
|
675
|
-
});
|
|
676
|
-
|
|
677
|
-
const profile: PatternGenLanguageProfile = {
|
|
678
|
-
code: 'en',
|
|
679
|
-
wordOrder: 'SVO',
|
|
680
|
-
keywords: {
|
|
681
|
-
get: { primary: 'get' },
|
|
682
|
-
},
|
|
683
|
-
roleMarkers: {
|
|
684
|
-
target: { primary: 'with', position: 'before' },
|
|
685
|
-
source: { primary: 'from', position: 'before' },
|
|
686
|
-
},
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
const pattern = generatePattern(schema, profile);
|
|
690
|
-
|
|
691
|
-
const tokens = createMockTokenStream([
|
|
692
|
-
createToken({ kind: 'keyword', value: 'get', position: createPosition(0, 3) }),
|
|
693
|
-
createToken({ kind: 'keyword', value: 'from', position: createPosition(4, 8) }),
|
|
694
|
-
createToken({ kind: 'identifier', value: 'users', position: createPosition(9, 14) }),
|
|
695
|
-
]);
|
|
696
|
-
|
|
697
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
698
|
-
|
|
699
|
-
expect(result).not.toBeNull();
|
|
700
|
-
expect(result?.captured.has('target')).toBe(false);
|
|
701
|
-
expect(result?.captured.has('source')).toBe(true);
|
|
702
|
-
});
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
describe('Marker scan for optional groups', () => {
|
|
706
|
-
// Schema: command [actor?] action ['on' target?] ['into' destination?]
|
|
707
|
-
function createScanSchema() {
|
|
708
|
-
return defineCommand({
|
|
709
|
-
action: 'do',
|
|
710
|
-
roles: [
|
|
711
|
-
defineRole({
|
|
712
|
-
role: 'actor',
|
|
713
|
-
required: false,
|
|
714
|
-
expectedTypes: ['expression'],
|
|
715
|
-
svoPosition: 3,
|
|
716
|
-
}),
|
|
717
|
-
defineRole({
|
|
718
|
-
role: 'verb',
|
|
719
|
-
required: true,
|
|
720
|
-
expectedTypes: ['expression'],
|
|
721
|
-
svoPosition: 2,
|
|
722
|
-
}),
|
|
723
|
-
defineRole({
|
|
724
|
-
role: 'target',
|
|
725
|
-
required: false,
|
|
726
|
-
expectedTypes: ['expression'],
|
|
727
|
-
svoPosition: 1,
|
|
728
|
-
markerOverride: { en: 'on' },
|
|
729
|
-
}),
|
|
730
|
-
defineRole({
|
|
731
|
-
role: 'destination',
|
|
732
|
-
required: false,
|
|
733
|
-
expectedTypes: ['expression'],
|
|
734
|
-
svoPosition: 0,
|
|
735
|
-
markerOverride: { en: 'into' },
|
|
736
|
-
}),
|
|
737
|
-
],
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function createScanProfile(): PatternGenLanguageProfile {
|
|
742
|
-
return {
|
|
743
|
-
code: 'en',
|
|
744
|
-
wordOrder: 'SVO',
|
|
745
|
-
keywords: {
|
|
746
|
-
do: { primary: 'do' },
|
|
747
|
-
},
|
|
748
|
-
roleMarkers: {
|
|
749
|
-
target: { primary: 'on', position: 'before' },
|
|
750
|
-
destination: { primary: 'into', position: 'before' },
|
|
751
|
-
},
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
it('should find marker past one intervening token', () => {
|
|
756
|
-
const pattern = generatePattern(createScanSchema(), createScanProfile());
|
|
757
|
-
|
|
758
|
-
const tokens = createMockTokenStream([
|
|
759
|
-
createToken({ kind: 'keyword', value: 'do', position: createPosition(0, 2) }),
|
|
760
|
-
createToken({ kind: 'identifier', value: 'user', position: createPosition(3, 7) }),
|
|
761
|
-
createToken({ kind: 'identifier', value: 'types', position: createPosition(8, 13) }),
|
|
762
|
-
createToken({ kind: 'identifier', value: 'hello', position: createPosition(14, 19) }),
|
|
763
|
-
createToken({ kind: 'keyword', value: 'into', position: createPosition(20, 24) }),
|
|
764
|
-
createToken({ kind: 'identifier', value: 'output', position: createPosition(25, 31) }),
|
|
765
|
-
]);
|
|
766
|
-
|
|
767
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
768
|
-
|
|
769
|
-
expect(result).not.toBeNull();
|
|
770
|
-
expect(result?.captured.has('actor')).toBe(true);
|
|
771
|
-
expect(result?.captured.has('verb')).toBe(true);
|
|
772
|
-
expect(result?.captured.has('destination')).toBe(true);
|
|
773
|
-
expect(result?.captured.get('destination')).toMatchObject({ raw: 'output' });
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
it('should find marker past two intervening tokens', () => {
|
|
777
|
-
const pattern = generatePattern(createScanSchema(), createScanProfile());
|
|
778
|
-
|
|
779
|
-
const tokens = createMockTokenStream([
|
|
780
|
-
createToken({ kind: 'keyword', value: 'do', position: createPosition(0, 2) }),
|
|
781
|
-
createToken({ kind: 'identifier', value: 'user', position: createPosition(3, 7) }),
|
|
782
|
-
createToken({ kind: 'identifier', value: 'types', position: createPosition(8, 13) }),
|
|
783
|
-
createToken({ kind: 'identifier', value: 'hello', position: createPosition(14, 19) }),
|
|
784
|
-
createToken({ kind: 'identifier', value: 'world', position: createPosition(20, 25) }),
|
|
785
|
-
createToken({ kind: 'keyword', value: 'into', position: createPosition(26, 30) }),
|
|
786
|
-
createToken({ kind: 'identifier', value: 'output', position: createPosition(31, 37) }),
|
|
787
|
-
]);
|
|
788
|
-
|
|
789
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
790
|
-
|
|
791
|
-
expect(result).not.toBeNull();
|
|
792
|
-
expect(result?.captured.has('destination')).toBe(true);
|
|
793
|
-
expect(result?.captured.get('destination')).toMatchObject({ raw: 'output' });
|
|
794
|
-
});
|
|
795
|
-
|
|
796
|
-
it('should not scan when group matches immediately', () => {
|
|
797
|
-
const pattern = generatePattern(createScanSchema(), createScanProfile());
|
|
798
|
-
|
|
799
|
-
const tokens = createMockTokenStream([
|
|
800
|
-
createToken({ kind: 'keyword', value: 'do', position: createPosition(0, 2) }),
|
|
801
|
-
createToken({ kind: 'identifier', value: 'user', position: createPosition(3, 7) }),
|
|
802
|
-
createToken({ kind: 'identifier', value: 'types', position: createPosition(8, 13) }),
|
|
803
|
-
createToken({ kind: 'keyword', value: 'on', position: createPosition(14, 16) }),
|
|
804
|
-
createToken({ kind: 'identifier', value: 'button', position: createPosition(17, 23) }),
|
|
805
|
-
]);
|
|
806
|
-
|
|
807
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
808
|
-
|
|
809
|
-
expect(result).not.toBeNull();
|
|
810
|
-
expect(result?.captured.has('target')).toBe(true);
|
|
811
|
-
expect(result?.captured.get('target')).toMatchObject({ raw: 'button' });
|
|
812
|
-
});
|
|
813
|
-
|
|
814
|
-
it('should not exceed MAX_MARKER_SCAN (3 tokens)', () => {
|
|
815
|
-
const pattern = generatePattern(createScanSchema(), createScanProfile());
|
|
816
|
-
|
|
817
|
-
const tokens = createMockTokenStream([
|
|
818
|
-
createToken({ kind: 'keyword', value: 'do', position: createPosition(0, 2) }),
|
|
819
|
-
createToken({ kind: 'identifier', value: 'user', position: createPosition(3, 7) }),
|
|
820
|
-
createToken({ kind: 'identifier', value: 'types', position: createPosition(8, 13) }),
|
|
821
|
-
createToken({ kind: 'identifier', value: 'a', position: createPosition(14, 15) }),
|
|
822
|
-
createToken({ kind: 'identifier', value: 'b', position: createPosition(16, 17) }),
|
|
823
|
-
createToken({ kind: 'identifier', value: 'c', position: createPosition(18, 19) }),
|
|
824
|
-
createToken({ kind: 'identifier', value: 'd', position: createPosition(20, 21) }),
|
|
825
|
-
createToken({ kind: 'keyword', value: 'into', position: createPosition(22, 26) }),
|
|
826
|
-
createToken({ kind: 'identifier', value: 'output', position: createPosition(27, 33) }),
|
|
827
|
-
]);
|
|
828
|
-
|
|
829
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
830
|
-
|
|
831
|
-
// Match succeeds (all required roles match) but destination not captured
|
|
832
|
-
// because 'into' is 4 tokens away (> MAX_MARKER_SCAN of 3)
|
|
833
|
-
expect(result).not.toBeNull();
|
|
834
|
-
expect(result?.captured.has('destination')).toBe(false);
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
it('should match both groups when both markers present', () => {
|
|
838
|
-
const pattern = generatePattern(createScanSchema(), createScanProfile());
|
|
839
|
-
|
|
840
|
-
const tokens = createMockTokenStream([
|
|
841
|
-
createToken({ kind: 'keyword', value: 'do', position: createPosition(0, 2) }),
|
|
842
|
-
createToken({ kind: 'identifier', value: 'user', position: createPosition(3, 7) }),
|
|
843
|
-
createToken({ kind: 'identifier', value: 'types', position: createPosition(8, 13) }),
|
|
844
|
-
createToken({ kind: 'keyword', value: 'on', position: createPosition(14, 16) }),
|
|
845
|
-
createToken({ kind: 'identifier', value: 'button', position: createPosition(17, 23) }),
|
|
846
|
-
createToken({ kind: 'keyword', value: 'into', position: createPosition(24, 28) }),
|
|
847
|
-
createToken({ kind: 'identifier', value: 'output', position: createPosition(29, 35) }),
|
|
848
|
-
]);
|
|
849
|
-
|
|
850
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
851
|
-
|
|
852
|
-
expect(result).not.toBeNull();
|
|
853
|
-
expect(result?.captured.has('target')).toBe(true);
|
|
854
|
-
expect(result?.captured.get('target')).toMatchObject({ raw: 'button' });
|
|
855
|
-
expect(result?.captured.has('destination')).toBe(true);
|
|
856
|
-
expect(result?.captured.get('destination')).toMatchObject({ raw: 'output' });
|
|
857
|
-
});
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
describe('matchBest', () => {
|
|
861
|
-
it('should return highest confidence match from multiple patterns', () => {
|
|
862
|
-
const simpleSchema = defineCommand({
|
|
863
|
-
action: 'select',
|
|
864
|
-
roles: [
|
|
865
|
-
defineRole({
|
|
866
|
-
role: 'columns',
|
|
867
|
-
required: true,
|
|
868
|
-
expectedTypes: ['expression'],
|
|
869
|
-
}),
|
|
870
|
-
],
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
const complexSchema = defineCommand({
|
|
874
|
-
action: 'select',
|
|
875
|
-
roles: [
|
|
876
|
-
defineRole({
|
|
877
|
-
role: 'columns',
|
|
878
|
-
required: true,
|
|
879
|
-
expectedTypes: ['expression'],
|
|
880
|
-
svoPosition: 2,
|
|
881
|
-
}),
|
|
882
|
-
defineRole({
|
|
883
|
-
role: 'source',
|
|
884
|
-
required: true,
|
|
885
|
-
expectedTypes: ['expression'],
|
|
886
|
-
svoPosition: 1,
|
|
887
|
-
markerOverride: { en: 'from' },
|
|
888
|
-
}),
|
|
889
|
-
],
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
const profile: PatternGenLanguageProfile = {
|
|
893
|
-
code: 'en',
|
|
894
|
-
wordOrder: 'SVO',
|
|
895
|
-
keywords: {
|
|
896
|
-
select: { primary: 'select' },
|
|
897
|
-
},
|
|
898
|
-
roleMarkers: {
|
|
899
|
-
source: { primary: 'from', position: 'before' },
|
|
900
|
-
},
|
|
901
|
-
};
|
|
902
|
-
|
|
903
|
-
const pattern1 = generatePattern(simpleSchema, profile, { basePriority: 50 });
|
|
904
|
-
const pattern2 = generatePattern(complexSchema, profile, { basePriority: 50 });
|
|
905
|
-
|
|
906
|
-
const tokens = createMockTokenStream([
|
|
907
|
-
createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
|
|
908
|
-
createToken({ kind: 'identifier', value: 'name', position: createPosition(7, 11) }),
|
|
909
|
-
createToken({ kind: 'keyword', value: 'from', position: createPosition(12, 16) }),
|
|
910
|
-
createToken({ kind: 'identifier', value: 'users', position: createPosition(17, 22) }),
|
|
911
|
-
]);
|
|
912
|
-
|
|
913
|
-
const result = matcher.matchBest(tokens, [pattern1, pattern2]);
|
|
914
|
-
|
|
915
|
-
expect(result).not.toBeNull();
|
|
916
|
-
// More complete pattern should have higher confidence
|
|
917
|
-
expect(result?.captured.has('source')).toBe(true);
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
it('should return null when no patterns match', () => {
|
|
921
|
-
const schema = defineCommand({
|
|
922
|
-
action: 'toggle',
|
|
923
|
-
roles: [],
|
|
924
|
-
});
|
|
925
|
-
|
|
926
|
-
const profile: PatternGenLanguageProfile = {
|
|
927
|
-
code: 'en',
|
|
928
|
-
wordOrder: 'SVO',
|
|
929
|
-
keywords: {
|
|
930
|
-
toggle: { primary: 'toggle' },
|
|
931
|
-
},
|
|
932
|
-
};
|
|
933
|
-
|
|
934
|
-
const pattern = generatePattern(schema, profile);
|
|
935
|
-
|
|
936
|
-
const tokens = createMockTokenStream([
|
|
937
|
-
createToken({ kind: 'identifier', value: 'unknown', position: createPosition(0, 7) }),
|
|
938
|
-
]);
|
|
939
|
-
|
|
940
|
-
const result = matcher.matchBest(tokens, [pattern]);
|
|
941
|
-
|
|
942
|
-
expect(result).toBeNull();
|
|
943
|
-
});
|
|
944
|
-
|
|
945
|
-
it('should handle empty pattern array', () => {
|
|
946
|
-
const tokens = createMockTokenStream([
|
|
947
|
-
createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
|
|
948
|
-
]);
|
|
949
|
-
|
|
950
|
-
const result = matcher.matchBest(tokens, []);
|
|
951
|
-
|
|
952
|
-
expect(result).toBeNull();
|
|
953
|
-
});
|
|
954
|
-
});
|
|
955
|
-
|
|
956
|
-
describe('Static Value Extraction', () => {
|
|
957
|
-
it('should apply static value extraction from extraction rules', () => {
|
|
958
|
-
const schema = defineCommand({
|
|
959
|
-
action: 'toggle',
|
|
960
|
-
roles: [
|
|
961
|
-
defineRole({
|
|
962
|
-
role: 'patient',
|
|
963
|
-
required: true,
|
|
964
|
-
expectedTypes: ['expression'],
|
|
965
|
-
}),
|
|
966
|
-
],
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
const profile: PatternGenLanguageProfile = {
|
|
970
|
-
code: 'en',
|
|
971
|
-
wordOrder: 'SVO',
|
|
972
|
-
keywords: {
|
|
973
|
-
toggle: { primary: 'toggle' },
|
|
974
|
-
},
|
|
975
|
-
};
|
|
976
|
-
|
|
977
|
-
const pattern = generatePattern(schema, profile);
|
|
978
|
-
|
|
979
|
-
// Add static value extraction
|
|
980
|
-
pattern.extraction.action = { value: 'toggle' };
|
|
981
|
-
|
|
982
|
-
const tokens = createMockTokenStream([
|
|
983
|
-
createToken({ kind: 'keyword', value: 'toggle', position: createPosition(0, 6) }),
|
|
984
|
-
createToken({ kind: 'identifier', value: 'button', position: createPosition(7, 13) }),
|
|
985
|
-
]);
|
|
986
|
-
|
|
987
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
988
|
-
|
|
989
|
-
expect(result).not.toBeNull();
|
|
990
|
-
expect(result?.captured.has('action')).toBe(true);
|
|
991
|
-
const action = result?.captured.get('action');
|
|
992
|
-
if (action && action.type === 'literal') {
|
|
993
|
-
expect(action.value).toBe('toggle');
|
|
994
|
-
}
|
|
995
|
-
});
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
describe('Confidence Scoring', () => {
|
|
999
|
-
it('should return high confidence for complete matches', () => {
|
|
1000
|
-
const schema = defineCommand({
|
|
1001
|
-
action: 'select',
|
|
1002
|
-
roles: [
|
|
1003
|
-
defineRole({
|
|
1004
|
-
role: 'columns',
|
|
1005
|
-
required: true,
|
|
1006
|
-
expectedTypes: ['expression'],
|
|
1007
|
-
}),
|
|
1008
|
-
],
|
|
1009
|
-
});
|
|
1010
|
-
|
|
1011
|
-
const profile: PatternGenLanguageProfile = {
|
|
1012
|
-
code: 'en',
|
|
1013
|
-
wordOrder: 'SVO',
|
|
1014
|
-
keywords: {
|
|
1015
|
-
select: { primary: 'select' },
|
|
1016
|
-
},
|
|
1017
|
-
};
|
|
1018
|
-
|
|
1019
|
-
const pattern = generatePattern(schema, profile);
|
|
1020
|
-
|
|
1021
|
-
const tokens = createMockTokenStream([
|
|
1022
|
-
createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
|
|
1023
|
-
createToken({ kind: 'identifier', value: 'name', position: createPosition(7, 11) }),
|
|
1024
|
-
]);
|
|
1025
|
-
|
|
1026
|
-
const result = matcher.matchPattern(tokens, pattern);
|
|
1027
|
-
|
|
1028
|
-
expectConfidenceRange(result, 0.8, 1.0);
|
|
1029
|
-
});
|
|
1030
|
-
});
|
|
1031
|
-
});
|