@lokascript/framework 2.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.
Files changed (181) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +142 -0
  3. package/dist/aot/aot-orchestrator.d.ts +75 -0
  4. package/dist/aot/aot-orchestrator.d.ts.map +1 -0
  5. package/dist/aot/domain-scanner.d.ts +27 -0
  6. package/dist/aot/domain-scanner.d.ts.map +1 -0
  7. package/dist/aot/index.d.ts +8 -0
  8. package/dist/aot/index.d.ts.map +1 -0
  9. package/dist/aot/types.d.ts +103 -0
  10. package/dist/aot/types.d.ts.map +1 -0
  11. package/dist/api/create-dsl.d.ts +91 -0
  12. package/dist/api/create-dsl.d.ts.map +1 -0
  13. package/dist/api/dispatcher.d.ts +108 -0
  14. package/dist/api/dispatcher.d.ts.map +1 -0
  15. package/dist/api/domain-registry.d.ts +152 -0
  16. package/dist/api/domain-registry.d.ts.map +1 -0
  17. package/dist/api/index.d.ts +7 -0
  18. package/dist/api/index.d.ts.map +1 -0
  19. package/dist/api/index.js +2082 -0
  20. package/dist/api/index.js.map +1 -0
  21. package/dist/core/index.d.ts +7 -0
  22. package/dist/core/index.d.ts.map +1 -0
  23. package/dist/core/index.js +2674 -0
  24. package/dist/core/index.js.map +1 -0
  25. package/dist/core/logger.d.ts +32 -0
  26. package/dist/core/logger.d.ts.map +1 -0
  27. package/dist/core/pattern-matching/index.d.ts +6 -0
  28. package/dist/core/pattern-matching/index.d.ts.map +1 -0
  29. package/dist/core/pattern-matching/index.js +1239 -0
  30. package/dist/core/pattern-matching/index.js.map +1 -0
  31. package/dist/core/pattern-matching/pattern-matcher.d.ts +239 -0
  32. package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -0
  33. package/dist/core/pattern-matching/utils/index.d.ts +6 -0
  34. package/dist/core/pattern-matching/utils/index.d.ts.map +1 -0
  35. package/dist/core/pattern-matching/utils/possessive-keywords.d.ts +38 -0
  36. package/dist/core/pattern-matching/utils/possessive-keywords.d.ts.map +1 -0
  37. package/dist/core/pattern-matching/utils/type-validation.d.ts +63 -0
  38. package/dist/core/pattern-matching/utils/type-validation.d.ts.map +1 -0
  39. package/dist/core/tokenization/base-tokenizer.d.ts +344 -0
  40. package/dist/core/tokenization/base-tokenizer.d.ts.map +1 -0
  41. package/dist/core/tokenization/char-classifiers.d.ts +56 -0
  42. package/dist/core/tokenization/char-classifiers.d.ts.map +1 -0
  43. package/dist/core/tokenization/default-extractors.d.ts +48 -0
  44. package/dist/core/tokenization/default-extractors.d.ts.map +1 -0
  45. package/dist/core/tokenization/extractors/index.d.ts +9 -0
  46. package/dist/core/tokenization/extractors/index.d.ts.map +1 -0
  47. package/dist/core/tokenization/extractors/operator.d.ts +23 -0
  48. package/dist/core/tokenization/extractors/operator.d.ts.map +1 -0
  49. package/dist/core/tokenization/extractors/punctuation.d.ts +22 -0
  50. package/dist/core/tokenization/extractors/punctuation.d.ts.map +1 -0
  51. package/dist/core/tokenization/extractors.d.ts +61 -0
  52. package/dist/core/tokenization/extractors.d.ts.map +1 -0
  53. package/dist/core/tokenization/index.d.ts +11 -0
  54. package/dist/core/tokenization/index.d.ts.map +1 -0
  55. package/dist/core/tokenization/index.js +1345 -0
  56. package/dist/core/tokenization/index.js.map +1 -0
  57. package/dist/core/tokenization/morphology/index.d.ts +5 -0
  58. package/dist/core/tokenization/morphology/index.d.ts.map +1 -0
  59. package/dist/core/tokenization/morphology/types.d.ts +110 -0
  60. package/dist/core/tokenization/morphology/types.d.ts.map +1 -0
  61. package/dist/core/tokenization/token-utils.d.ts +111 -0
  62. package/dist/core/tokenization/token-utils.d.ts.map +1 -0
  63. package/dist/core/types.d.ts +382 -0
  64. package/dist/core/types.d.ts.map +1 -0
  65. package/dist/core/types.js +108 -0
  66. package/dist/core/types.js.map +1 -0
  67. package/dist/generation/diagnostics.d.ts +120 -0
  68. package/dist/generation/diagnostics.d.ts.map +1 -0
  69. package/dist/generation/index.d.ts +7 -0
  70. package/dist/generation/index.d.ts.map +1 -0
  71. package/dist/generation/index.js +339 -0
  72. package/dist/generation/index.js.map +1 -0
  73. package/dist/generation/pattern-generator.d.ts +48 -0
  74. package/dist/generation/pattern-generator.d.ts.map +1 -0
  75. package/dist/generation/renderer.d.ts +115 -0
  76. package/dist/generation/renderer.d.ts.map +1 -0
  77. package/dist/grammar/index.d.ts +10 -0
  78. package/dist/grammar/index.d.ts.map +1 -0
  79. package/dist/grammar/index.js +391 -0
  80. package/dist/grammar/index.js.map +1 -0
  81. package/dist/grammar/transformer.d.ts +56 -0
  82. package/dist/grammar/transformer.d.ts.map +1 -0
  83. package/dist/grammar/types.d.ts +236 -0
  84. package/dist/grammar/types.d.ts.map +1 -0
  85. package/dist/index.cjs +4454 -0
  86. package/dist/index.cjs.map +1 -0
  87. package/dist/index.d.ts +46 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +4336 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/interfaces/dictionary.d.ts +82 -0
  92. package/dist/interfaces/dictionary.d.ts.map +1 -0
  93. package/dist/interfaces/index.d.ts +10 -0
  94. package/dist/interfaces/index.d.ts.map +1 -0
  95. package/dist/interfaces/profile-provider.d.ts +67 -0
  96. package/dist/interfaces/profile-provider.d.ts.map +1 -0
  97. package/dist/interfaces/value-extractor.d.ts +168 -0
  98. package/dist/interfaces/value-extractor.d.ts.map +1 -0
  99. package/dist/multilingual/index.d.ts +8 -0
  100. package/dist/multilingual/index.d.ts.map +1 -0
  101. package/dist/multilingual/index.js +1 -0
  102. package/dist/multilingual/index.js.map +1 -0
  103. package/dist/parsing/index.d.ts +8 -0
  104. package/dist/parsing/index.d.ts.map +1 -0
  105. package/dist/parsing/index.js +1415 -0
  106. package/dist/parsing/index.js.map +1 -0
  107. package/dist/parsing/multi-statement.d.ts +265 -0
  108. package/dist/parsing/multi-statement.d.ts.map +1 -0
  109. package/dist/schema/command-schema.d.ts +78 -0
  110. package/dist/schema/command-schema.d.ts.map +1 -0
  111. package/dist/schema/index.d.ts +5 -0
  112. package/dist/schema/index.d.ts.map +1 -0
  113. package/dist/schema/index.js +25 -0
  114. package/dist/schema/index.js.map +1 -0
  115. package/dist/test-setup.d.ts +9 -0
  116. package/dist/test-setup.d.ts.map +1 -0
  117. package/dist/testing/index.d.ts +50 -0
  118. package/dist/testing/index.d.ts.map +1 -0
  119. package/dist/testing/index.js +16969 -0
  120. package/dist/testing/index.js.map +1 -0
  121. package/package.json +122 -0
  122. package/src/__test__/fixtures/sql-dsl.ts +232 -0
  123. package/src/__test__/sql-integration.test.ts +189 -0
  124. package/src/__test__/test-utils.ts +260 -0
  125. package/src/aot/aot-orchestrator.test.ts +413 -0
  126. package/src/aot/aot-orchestrator.ts +238 -0
  127. package/src/aot/domain-scanner.ts +178 -0
  128. package/src/aot/index.ts +8 -0
  129. package/src/aot/types.ts +124 -0
  130. package/src/api/create-dsl.ts +367 -0
  131. package/src/api/dispatcher.test.ts +336 -0
  132. package/src/api/dispatcher.ts +222 -0
  133. package/src/api/domain-registry.test.ts +336 -0
  134. package/src/api/domain-registry.ts +500 -0
  135. package/src/api/index.ts +7 -0
  136. package/src/core/index.ts +7 -0
  137. package/src/core/logger.ts +130 -0
  138. package/src/core/pattern-matching/index.ts +6 -0
  139. package/src/core/pattern-matching/pattern-matcher.test.ts +900 -0
  140. package/src/core/pattern-matching/pattern-matcher.ts +1548 -0
  141. package/src/core/pattern-matching/pattern-matcher.ts.backup +1267 -0
  142. package/src/core/pattern-matching/utils/index.ts +6 -0
  143. package/src/core/pattern-matching/utils/possessive-keywords.ts +55 -0
  144. package/src/core/pattern-matching/utils/type-validation.test.ts +316 -0
  145. package/src/core/pattern-matching/utils/type-validation.ts +134 -0
  146. package/src/core/tokenization/base-tokenizer.ts +916 -0
  147. package/src/core/tokenization/char-classifiers.ts +79 -0
  148. package/src/core/tokenization/create-simple-tokenizer.test.ts +260 -0
  149. package/src/core/tokenization/default-extractors.ts +69 -0
  150. package/src/core/tokenization/extractors/index.ts +9 -0
  151. package/src/core/tokenization/extractors/operator.ts +75 -0
  152. package/src/core/tokenization/extractors/punctuation.ts +39 -0
  153. package/src/core/tokenization/extractors.ts +452 -0
  154. package/src/core/tokenization/index.ts +11 -0
  155. package/src/core/tokenization/morphology/index.ts +5 -0
  156. package/src/core/tokenization/morphology/types.ts +211 -0
  157. package/src/core/tokenization/token-utils.ts +252 -0
  158. package/src/core/types.ts +589 -0
  159. package/src/generation/diagnostics.test.ts +171 -0
  160. package/src/generation/diagnostics.ts +239 -0
  161. package/src/generation/index.ts +7 -0
  162. package/src/generation/pattern-generator.test.ts +430 -0
  163. package/src/generation/pattern-generator.ts +315 -0
  164. package/src/generation/renderer.test.ts +266 -0
  165. package/src/generation/renderer.ts +244 -0
  166. package/src/grammar/index.ts +12 -0
  167. package/src/grammar/transformer.ts +159 -0
  168. package/src/grammar/types.ts +630 -0
  169. package/src/index.ts +157 -0
  170. package/src/interfaces/dictionary.ts +123 -0
  171. package/src/interfaces/index.ts +10 -0
  172. package/src/interfaces/profile-provider.ts +88 -0
  173. package/src/interfaces/value-extractor.ts +435 -0
  174. package/src/multilingual/index.ts +9 -0
  175. package/src/parsing/index.ts +27 -0
  176. package/src/parsing/multi-statement.test.ts +480 -0
  177. package/src/parsing/multi-statement.ts +648 -0
  178. package/src/schema/command-schema.ts +118 -0
  179. package/src/schema/index.ts +5 -0
  180. package/src/test-setup.ts +45 -0
  181. package/src/testing/index.ts +137 -0
@@ -0,0 +1,900 @@
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
+ });
305
+
306
+ describe('Edge Cases', () => {
307
+ it('should return null for empty token stream', () => {
308
+ const schema = defineCommand({
309
+ action: 'toggle',
310
+ roles: [],
311
+ });
312
+
313
+ const profile: PatternGenLanguageProfile = {
314
+ code: 'en',
315
+ wordOrder: 'SVO',
316
+ keywords: {
317
+ toggle: { primary: 'toggle' },
318
+ },
319
+ };
320
+
321
+ const pattern = generatePattern(schema, profile);
322
+ const tokens = createMockTokenStream([]);
323
+
324
+ const result = matcher.matchPattern(tokens, pattern);
325
+
326
+ expect(result).toBeNull();
327
+ });
328
+
329
+ it('should reset token position on failed match', () => {
330
+ const schema = defineCommand({
331
+ action: 'select',
332
+ roles: [
333
+ defineRole({
334
+ role: 'source',
335
+ required: true,
336
+ expectedTypes: ['expression'],
337
+ markerOverride: { en: 'from' },
338
+ }),
339
+ ],
340
+ });
341
+
342
+ const profile: PatternGenLanguageProfile = {
343
+ code: 'en',
344
+ wordOrder: 'SVO',
345
+ keywords: {
346
+ select: { primary: 'select' },
347
+ },
348
+ roleMarkers: {
349
+ source: { primary: 'from', position: 'before' },
350
+ },
351
+ };
352
+
353
+ const pattern = generatePattern(schema, profile);
354
+
355
+ const tokens = createMockTokenStream([
356
+ createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
357
+ createToken({ kind: 'identifier', value: 'name', position: createPosition(7, 11) }),
358
+ // Missing 'from' marker - should fail
359
+ ]);
360
+
361
+ const initialPosition = tokens.position();
362
+ matcher.matchPattern(tokens, pattern);
363
+
364
+ // Token position should be reset after failed match
365
+ expect(tokens.position()).toBe(initialPosition);
366
+ });
367
+ });
368
+
369
+ describe('Backtracking', () => {
370
+ it('should skip optional unmarked role when next literal needs the token', () => {
371
+ // Pattern: select [columns? (expression)] 'from' [source (expression)]
372
+ // Input: "select from users"
373
+ // Without backtracking: columns="from", then literal 'from' fails
374
+ // With backtracking: columns skipped, from matches literal, source="users"
375
+ const schema = defineCommand({
376
+ action: 'select',
377
+ roles: [
378
+ defineRole({
379
+ role: 'columns',
380
+ required: false,
381
+ expectedTypes: ['expression'],
382
+ svoPosition: 2,
383
+ }),
384
+ defineRole({
385
+ role: 'source',
386
+ required: true,
387
+ expectedTypes: ['expression'],
388
+ svoPosition: 1,
389
+ markerOverride: { en: 'from' },
390
+ }),
391
+ ],
392
+ });
393
+
394
+ const profile: PatternGenLanguageProfile = {
395
+ code: 'en',
396
+ wordOrder: 'SVO',
397
+ keywords: {
398
+ select: { primary: 'select' },
399
+ },
400
+ roleMarkers: {
401
+ source: { primary: 'from', position: 'before' },
402
+ },
403
+ };
404
+
405
+ const pattern = generatePattern(schema, profile);
406
+
407
+ const tokens = createMockTokenStream([
408
+ createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
409
+ createToken({ kind: 'keyword', value: 'from', position: createPosition(7, 11) }),
410
+ createToken({ kind: 'identifier', value: 'users', position: createPosition(12, 17) }),
411
+ ]);
412
+
413
+ const result = matcher.matchPattern(tokens, pattern);
414
+
415
+ expect(result).not.toBeNull();
416
+ expect(result?.captured.has('columns')).toBe(false);
417
+ expect(result?.captured.has('source')).toBe(true);
418
+ });
419
+
420
+ it('should still capture optional role when token is not the next literal', () => {
421
+ // Pattern: select [columns? (expression)] 'from' [source (expression)]
422
+ // Input: "select name from users"
423
+ // columns="name" (not a literal match), from matches, source="users"
424
+ const schema = defineCommand({
425
+ action: 'select',
426
+ roles: [
427
+ defineRole({
428
+ role: 'columns',
429
+ required: false,
430
+ expectedTypes: ['expression'],
431
+ svoPosition: 2,
432
+ }),
433
+ defineRole({
434
+ role: 'source',
435
+ required: true,
436
+ expectedTypes: ['expression'],
437
+ svoPosition: 1,
438
+ markerOverride: { en: 'from' },
439
+ }),
440
+ ],
441
+ });
442
+
443
+ const profile: PatternGenLanguageProfile = {
444
+ code: 'en',
445
+ wordOrder: 'SVO',
446
+ keywords: {
447
+ select: { primary: 'select' },
448
+ },
449
+ roleMarkers: {
450
+ source: { primary: 'from', position: 'before' },
451
+ },
452
+ };
453
+
454
+ const pattern = generatePattern(schema, profile);
455
+
456
+ const tokens = createMockTokenStream([
457
+ createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
458
+ createToken({ kind: 'identifier', value: 'name', position: createPosition(7, 11) }),
459
+ createToken({ kind: 'keyword', value: 'from', position: createPosition(12, 16) }),
460
+ createToken({ kind: 'identifier', value: 'users', position: createPosition(17, 22) }),
461
+ ]);
462
+
463
+ const result = matcher.matchPattern(tokens, pattern);
464
+
465
+ expect(result).not.toBeNull();
466
+ expect(result?.captured.has('columns')).toBe(true);
467
+ expect(result?.captured.has('source')).toBe(true);
468
+ });
469
+
470
+ it('should backtrack optional role when keyword was consumed as identifier', () => {
471
+ // Pattern: command [target? (expression)] 'to' [destination (expression)]
472
+ // Input: "command to output"
473
+ // "to" is keyword, optional target consumes it, then literal 'to' fails
474
+ // Backtrack: skip target, 'to' matches literal, destination="output"
475
+ const schema = defineCommand({
476
+ action: 'send',
477
+ roles: [
478
+ defineRole({
479
+ role: 'target',
480
+ required: false,
481
+ expectedTypes: ['expression'],
482
+ svoPosition: 2,
483
+ }),
484
+ defineRole({
485
+ role: 'destination',
486
+ required: true,
487
+ expectedTypes: ['expression'],
488
+ svoPosition: 1,
489
+ markerOverride: { en: 'to' },
490
+ }),
491
+ ],
492
+ });
493
+
494
+ const profile: PatternGenLanguageProfile = {
495
+ code: 'en',
496
+ wordOrder: 'SVO',
497
+ keywords: {
498
+ send: { primary: 'send' },
499
+ },
500
+ roleMarkers: {
501
+ destination: { primary: 'to', position: 'before' },
502
+ },
503
+ };
504
+
505
+ const pattern = generatePattern(schema, profile);
506
+
507
+ const tokens = createMockTokenStream([
508
+ createToken({ kind: 'keyword', value: 'send', position: createPosition(0, 4) }),
509
+ createToken({ kind: 'keyword', value: 'to', position: createPosition(5, 7) }),
510
+ createToken({ kind: 'identifier', value: 'output', position: createPosition(8, 14) }),
511
+ ]);
512
+
513
+ const result = matcher.matchPattern(tokens, pattern);
514
+
515
+ expect(result).not.toBeNull();
516
+ expect(result?.captured.has('target')).toBe(false);
517
+ expect(result?.captured.has('destination')).toBe(true);
518
+ });
519
+
520
+ it('should not backtrack when optional role has a marker (group)', () => {
521
+ // When optional roles have markers, they form groups that already
522
+ // handle backtracking via matchGroupToken. Verify this still works.
523
+ // Pattern: command ['with' target?] 'from' [source]
524
+ // Input: "command from users"
525
+ // The optional group [with target] is skipped (no 'with' found)
526
+ const schema = defineCommand({
527
+ action: 'get',
528
+ roles: [
529
+ defineRole({
530
+ role: 'target',
531
+ required: false,
532
+ expectedTypes: ['expression'],
533
+ svoPosition: 2,
534
+ markerOverride: { en: 'with' },
535
+ }),
536
+ defineRole({
537
+ role: 'source',
538
+ required: true,
539
+ expectedTypes: ['expression'],
540
+ svoPosition: 1,
541
+ markerOverride: { en: 'from' },
542
+ }),
543
+ ],
544
+ });
545
+
546
+ const profile: PatternGenLanguageProfile = {
547
+ code: 'en',
548
+ wordOrder: 'SVO',
549
+ keywords: {
550
+ get: { primary: 'get' },
551
+ },
552
+ roleMarkers: {
553
+ target: { primary: 'with', position: 'before' },
554
+ source: { primary: 'from', position: 'before' },
555
+ },
556
+ };
557
+
558
+ const pattern = generatePattern(schema, profile);
559
+
560
+ const tokens = createMockTokenStream([
561
+ createToken({ kind: 'keyword', value: 'get', position: createPosition(0, 3) }),
562
+ createToken({ kind: 'keyword', value: 'from', position: createPosition(4, 8) }),
563
+ createToken({ kind: 'identifier', value: 'users', position: createPosition(9, 14) }),
564
+ ]);
565
+
566
+ const result = matcher.matchPattern(tokens, pattern);
567
+
568
+ expect(result).not.toBeNull();
569
+ expect(result?.captured.has('target')).toBe(false);
570
+ expect(result?.captured.has('source')).toBe(true);
571
+ });
572
+ });
573
+
574
+ describe('Marker scan for optional groups', () => {
575
+ // Schema: command [actor?] action ['on' target?] ['into' destination?]
576
+ function createScanSchema() {
577
+ return defineCommand({
578
+ action: 'do',
579
+ roles: [
580
+ defineRole({
581
+ role: 'actor',
582
+ required: false,
583
+ expectedTypes: ['expression'],
584
+ svoPosition: 3,
585
+ }),
586
+ defineRole({
587
+ role: 'verb',
588
+ required: true,
589
+ expectedTypes: ['expression'],
590
+ svoPosition: 2,
591
+ }),
592
+ defineRole({
593
+ role: 'target',
594
+ required: false,
595
+ expectedTypes: ['expression'],
596
+ svoPosition: 1,
597
+ markerOverride: { en: 'on' },
598
+ }),
599
+ defineRole({
600
+ role: 'destination',
601
+ required: false,
602
+ expectedTypes: ['expression'],
603
+ svoPosition: 0,
604
+ markerOverride: { en: 'into' },
605
+ }),
606
+ ],
607
+ });
608
+ }
609
+
610
+ function createScanProfile(): PatternGenLanguageProfile {
611
+ return {
612
+ code: 'en',
613
+ wordOrder: 'SVO',
614
+ keywords: {
615
+ do: { primary: 'do' },
616
+ },
617
+ roleMarkers: {
618
+ target: { primary: 'on', position: 'before' },
619
+ destination: { primary: 'into', position: 'before' },
620
+ },
621
+ };
622
+ }
623
+
624
+ it('should find marker past one intervening token', () => {
625
+ const pattern = generatePattern(createScanSchema(), createScanProfile());
626
+
627
+ const tokens = createMockTokenStream([
628
+ createToken({ kind: 'keyword', value: 'do', position: createPosition(0, 2) }),
629
+ createToken({ kind: 'identifier', value: 'user', position: createPosition(3, 7) }),
630
+ createToken({ kind: 'identifier', value: 'types', position: createPosition(8, 13) }),
631
+ createToken({ kind: 'identifier', value: 'hello', position: createPosition(14, 19) }),
632
+ createToken({ kind: 'keyword', value: 'into', position: createPosition(20, 24) }),
633
+ createToken({ kind: 'identifier', value: 'output', position: createPosition(25, 31) }),
634
+ ]);
635
+
636
+ const result = matcher.matchPattern(tokens, pattern);
637
+
638
+ expect(result).not.toBeNull();
639
+ expect(result?.captured.has('actor')).toBe(true);
640
+ expect(result?.captured.has('verb')).toBe(true);
641
+ expect(result?.captured.has('destination')).toBe(true);
642
+ expect(result?.captured.get('destination')).toMatchObject({ raw: 'output' });
643
+ });
644
+
645
+ it('should find marker past two intervening tokens', () => {
646
+ const pattern = generatePattern(createScanSchema(), createScanProfile());
647
+
648
+ const tokens = createMockTokenStream([
649
+ createToken({ kind: 'keyword', value: 'do', position: createPosition(0, 2) }),
650
+ createToken({ kind: 'identifier', value: 'user', position: createPosition(3, 7) }),
651
+ createToken({ kind: 'identifier', value: 'types', position: createPosition(8, 13) }),
652
+ createToken({ kind: 'identifier', value: 'hello', position: createPosition(14, 19) }),
653
+ createToken({ kind: 'identifier', value: 'world', position: createPosition(20, 25) }),
654
+ createToken({ kind: 'keyword', value: 'into', position: createPosition(26, 30) }),
655
+ createToken({ kind: 'identifier', value: 'output', position: createPosition(31, 37) }),
656
+ ]);
657
+
658
+ const result = matcher.matchPattern(tokens, pattern);
659
+
660
+ expect(result).not.toBeNull();
661
+ expect(result?.captured.has('destination')).toBe(true);
662
+ expect(result?.captured.get('destination')).toMatchObject({ raw: 'output' });
663
+ });
664
+
665
+ it('should not scan when group matches immediately', () => {
666
+ const pattern = generatePattern(createScanSchema(), createScanProfile());
667
+
668
+ const tokens = createMockTokenStream([
669
+ createToken({ kind: 'keyword', value: 'do', position: createPosition(0, 2) }),
670
+ createToken({ kind: 'identifier', value: 'user', position: createPosition(3, 7) }),
671
+ createToken({ kind: 'identifier', value: 'types', position: createPosition(8, 13) }),
672
+ createToken({ kind: 'keyword', value: 'on', position: createPosition(14, 16) }),
673
+ createToken({ kind: 'identifier', value: 'button', position: createPosition(17, 23) }),
674
+ ]);
675
+
676
+ const result = matcher.matchPattern(tokens, pattern);
677
+
678
+ expect(result).not.toBeNull();
679
+ expect(result?.captured.has('target')).toBe(true);
680
+ expect(result?.captured.get('target')).toMatchObject({ raw: 'button' });
681
+ });
682
+
683
+ it('should not exceed MAX_MARKER_SCAN (3 tokens)', () => {
684
+ const pattern = generatePattern(createScanSchema(), createScanProfile());
685
+
686
+ const tokens = createMockTokenStream([
687
+ createToken({ kind: 'keyword', value: 'do', position: createPosition(0, 2) }),
688
+ createToken({ kind: 'identifier', value: 'user', position: createPosition(3, 7) }),
689
+ createToken({ kind: 'identifier', value: 'types', position: createPosition(8, 13) }),
690
+ createToken({ kind: 'identifier', value: 'a', position: createPosition(14, 15) }),
691
+ createToken({ kind: 'identifier', value: 'b', position: createPosition(16, 17) }),
692
+ createToken({ kind: 'identifier', value: 'c', position: createPosition(18, 19) }),
693
+ createToken({ kind: 'identifier', value: 'd', position: createPosition(20, 21) }),
694
+ createToken({ kind: 'keyword', value: 'into', position: createPosition(22, 26) }),
695
+ createToken({ kind: 'identifier', value: 'output', position: createPosition(27, 33) }),
696
+ ]);
697
+
698
+ const result = matcher.matchPattern(tokens, pattern);
699
+
700
+ // Match succeeds (all required roles match) but destination not captured
701
+ // because 'into' is 4 tokens away (> MAX_MARKER_SCAN of 3)
702
+ expect(result).not.toBeNull();
703
+ expect(result?.captured.has('destination')).toBe(false);
704
+ });
705
+
706
+ it('should match both groups when both markers present', () => {
707
+ const pattern = generatePattern(createScanSchema(), createScanProfile());
708
+
709
+ const tokens = createMockTokenStream([
710
+ createToken({ kind: 'keyword', value: 'do', position: createPosition(0, 2) }),
711
+ createToken({ kind: 'identifier', value: 'user', position: createPosition(3, 7) }),
712
+ createToken({ kind: 'identifier', value: 'types', position: createPosition(8, 13) }),
713
+ createToken({ kind: 'keyword', value: 'on', position: createPosition(14, 16) }),
714
+ createToken({ kind: 'identifier', value: 'button', position: createPosition(17, 23) }),
715
+ createToken({ kind: 'keyword', value: 'into', position: createPosition(24, 28) }),
716
+ createToken({ kind: 'identifier', value: 'output', position: createPosition(29, 35) }),
717
+ ]);
718
+
719
+ const result = matcher.matchPattern(tokens, pattern);
720
+
721
+ expect(result).not.toBeNull();
722
+ expect(result?.captured.has('target')).toBe(true);
723
+ expect(result?.captured.get('target')).toMatchObject({ raw: 'button' });
724
+ expect(result?.captured.has('destination')).toBe(true);
725
+ expect(result?.captured.get('destination')).toMatchObject({ raw: 'output' });
726
+ });
727
+ });
728
+
729
+ describe('matchBest', () => {
730
+ it('should return highest confidence match from multiple patterns', () => {
731
+ const simpleSchema = defineCommand({
732
+ action: 'select',
733
+ roles: [
734
+ defineRole({
735
+ role: 'columns',
736
+ required: true,
737
+ expectedTypes: ['expression'],
738
+ }),
739
+ ],
740
+ });
741
+
742
+ const complexSchema = defineCommand({
743
+ action: 'select',
744
+ roles: [
745
+ defineRole({
746
+ role: 'columns',
747
+ required: true,
748
+ expectedTypes: ['expression'],
749
+ svoPosition: 2,
750
+ }),
751
+ defineRole({
752
+ role: 'source',
753
+ required: true,
754
+ expectedTypes: ['expression'],
755
+ svoPosition: 1,
756
+ markerOverride: { en: 'from' },
757
+ }),
758
+ ],
759
+ });
760
+
761
+ const profile: PatternGenLanguageProfile = {
762
+ code: 'en',
763
+ wordOrder: 'SVO',
764
+ keywords: {
765
+ select: { primary: 'select' },
766
+ },
767
+ roleMarkers: {
768
+ source: { primary: 'from', position: 'before' },
769
+ },
770
+ };
771
+
772
+ const pattern1 = generatePattern(simpleSchema, profile, { basePriority: 50 });
773
+ const pattern2 = generatePattern(complexSchema, profile, { basePriority: 50 });
774
+
775
+ const tokens = createMockTokenStream([
776
+ createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
777
+ createToken({ kind: 'identifier', value: 'name', position: createPosition(7, 11) }),
778
+ createToken({ kind: 'keyword', value: 'from', position: createPosition(12, 16) }),
779
+ createToken({ kind: 'identifier', value: 'users', position: createPosition(17, 22) }),
780
+ ]);
781
+
782
+ const result = matcher.matchBest(tokens, [pattern1, pattern2]);
783
+
784
+ expect(result).not.toBeNull();
785
+ // More complete pattern should have higher confidence
786
+ expect(result?.captured.has('source')).toBe(true);
787
+ });
788
+
789
+ it('should return null when no patterns match', () => {
790
+ const schema = defineCommand({
791
+ action: 'toggle',
792
+ roles: [],
793
+ });
794
+
795
+ const profile: PatternGenLanguageProfile = {
796
+ code: 'en',
797
+ wordOrder: 'SVO',
798
+ keywords: {
799
+ toggle: { primary: 'toggle' },
800
+ },
801
+ };
802
+
803
+ const pattern = generatePattern(schema, profile);
804
+
805
+ const tokens = createMockTokenStream([
806
+ createToken({ kind: 'identifier', value: 'unknown', position: createPosition(0, 7) }),
807
+ ]);
808
+
809
+ const result = matcher.matchBest(tokens, [pattern]);
810
+
811
+ expect(result).toBeNull();
812
+ });
813
+
814
+ it('should handle empty pattern array', () => {
815
+ const tokens = createMockTokenStream([
816
+ createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
817
+ ]);
818
+
819
+ const result = matcher.matchBest(tokens, []);
820
+
821
+ expect(result).toBeNull();
822
+ });
823
+ });
824
+
825
+ describe('Static Value Extraction', () => {
826
+ it('should apply static value extraction from extraction rules', () => {
827
+ const schema = defineCommand({
828
+ action: 'toggle',
829
+ roles: [
830
+ defineRole({
831
+ role: 'patient',
832
+ required: true,
833
+ expectedTypes: ['expression'],
834
+ }),
835
+ ],
836
+ });
837
+
838
+ const profile: PatternGenLanguageProfile = {
839
+ code: 'en',
840
+ wordOrder: 'SVO',
841
+ keywords: {
842
+ toggle: { primary: 'toggle' },
843
+ },
844
+ };
845
+
846
+ const pattern = generatePattern(schema, profile);
847
+
848
+ // Add static value extraction
849
+ pattern.extraction.action = { value: 'toggle' };
850
+
851
+ const tokens = createMockTokenStream([
852
+ createToken({ kind: 'keyword', value: 'toggle', position: createPosition(0, 6) }),
853
+ createToken({ kind: 'identifier', value: 'button', position: createPosition(7, 13) }),
854
+ ]);
855
+
856
+ const result = matcher.matchPattern(tokens, pattern);
857
+
858
+ expect(result).not.toBeNull();
859
+ expect(result?.captured.has('action')).toBe(true);
860
+ const action = result?.captured.get('action');
861
+ if (action && action.type === 'literal') {
862
+ expect(action.value).toBe('toggle');
863
+ }
864
+ });
865
+ });
866
+
867
+ describe('Confidence Scoring', () => {
868
+ it('should return high confidence for complete matches', () => {
869
+ const schema = defineCommand({
870
+ action: 'select',
871
+ roles: [
872
+ defineRole({
873
+ role: 'columns',
874
+ required: true,
875
+ expectedTypes: ['expression'],
876
+ }),
877
+ ],
878
+ });
879
+
880
+ const profile: PatternGenLanguageProfile = {
881
+ code: 'en',
882
+ wordOrder: 'SVO',
883
+ keywords: {
884
+ select: { primary: 'select' },
885
+ },
886
+ };
887
+
888
+ const pattern = generatePattern(schema, profile);
889
+
890
+ const tokens = createMockTokenStream([
891
+ createToken({ kind: 'keyword', value: 'select', position: createPosition(0, 6) }),
892
+ createToken({ kind: 'identifier', value: 'name', position: createPosition(7, 11) }),
893
+ ]);
894
+
895
+ const result = matcher.matchPattern(tokens, pattern);
896
+
897
+ expectConfidenceRange(result, 0.8, 1.0);
898
+ });
899
+ });
900
+ });