@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,315 @@
1
+ /**
2
+ * Pattern Generator
3
+ *
4
+ * Generates LanguagePattern objects from CommandSchema + LanguageProfile.
5
+ * This solves the pattern explosion problem by deriving patterns from
6
+ * high-level schema definitions rather than hand-writing each one.
7
+ */
8
+
9
+ import type { LanguagePattern, PatternToken, ExtractionRule } from '../core/types';
10
+ import type { CommandSchema, RoleSpec } from '../schema';
11
+
12
+ /**
13
+ * Minimal language profile interface for pattern generation.
14
+ */
15
+ export interface PatternGenLanguageProfile {
16
+ readonly code: string;
17
+ readonly wordOrder: 'SVO' | 'SOV' | 'VSO' | 'VOS' | 'OSV' | 'OVS' | 'free';
18
+ readonly keywords: Record<string, { primary: string; alternatives?: string[] }>;
19
+ readonly roleMarkers?: Record<
20
+ string,
21
+ { primary: string; alternatives?: string[]; position?: 'before' | 'after' }
22
+ >;
23
+ }
24
+
25
+ /**
26
+ * Configuration for pattern generation.
27
+ */
28
+ export interface GeneratorConfig {
29
+ /** Base priority for generated patterns (default: 100) */
30
+ readonly basePriority?: number;
31
+ /** Include optional role variants (default: true) */
32
+ readonly includeOptionalVariants?: boolean;
33
+ }
34
+
35
+ const defaultConfig: GeneratorConfig = {
36
+ basePriority: 100,
37
+ includeOptionalVariants: true,
38
+ };
39
+
40
+ /**
41
+ * Generate a language pattern from a command schema and language profile.
42
+ *
43
+ * @param schema - Command schema defining the command structure
44
+ * @param profile - Language profile with word order and keywords
45
+ * @param config - Optional configuration
46
+ * @returns Generated language pattern
47
+ */
48
+ export function generatePattern(
49
+ schema: CommandSchema,
50
+ profile: PatternGenLanguageProfile,
51
+ config: GeneratorConfig = defaultConfig
52
+ ): LanguagePattern {
53
+ const id = `${schema.action}-${profile.code}-generated`;
54
+ const priority = config.basePriority ?? 100;
55
+
56
+ // Get keyword translation
57
+ const keyword = profile.keywords[schema.action];
58
+ if (!keyword) {
59
+ throw new Error(`No keyword translation for '${schema.action}' in ${profile.code}`);
60
+ }
61
+
62
+ // Build tokens based on word order
63
+ const tokens = buildTokens(schema, profile, keyword.primary, keyword.alternatives);
64
+
65
+ // Build extraction rules
66
+ const extraction = buildExtractionRules(schema, profile);
67
+
68
+ // Build format string for documentation
69
+ const format = buildFormatString(schema, profile, keyword.primary);
70
+
71
+ return {
72
+ id,
73
+ language: profile.code,
74
+ command: schema.action,
75
+ priority,
76
+ template: {
77
+ format,
78
+ tokens,
79
+ },
80
+ extraction,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Build pattern tokens based on word order and role markers.
86
+ */
87
+ function buildTokens(
88
+ schema: CommandSchema,
89
+ profile: PatternGenLanguageProfile,
90
+ keyword: string,
91
+ alternatives?: string[]
92
+ ): PatternToken[] {
93
+ const tokens: PatternToken[] = [];
94
+ const keywordToken: PatternToken = {
95
+ type: 'literal',
96
+ value: keyword,
97
+ ...(alternatives?.length && { alternatives }),
98
+ };
99
+
100
+ // Sort roles by word order
101
+ const sortedRoles = sortRolesByWordOrder(schema.roles, profile.wordOrder);
102
+
103
+ // For SVO: [action, patient, destination, source, ...]
104
+ if (profile.wordOrder === 'SVO') {
105
+ // Add keyword first
106
+ tokens.push(keywordToken);
107
+
108
+ // Add roles with markers
109
+ for (const role of sortedRoles) {
110
+ addRoleWithMarker(tokens, role, profile);
111
+ }
112
+ }
113
+ // For SOV: [patient, destination, source, ..., action]
114
+ else if (profile.wordOrder === 'SOV') {
115
+ // Add roles with markers first
116
+ for (const role of sortedRoles) {
117
+ addRoleWithMarker(tokens, role, profile);
118
+ }
119
+
120
+ // Add keyword last
121
+ tokens.push(keywordToken);
122
+ }
123
+ // For VSO: [action, destination, patient, source, ...]
124
+ else if (profile.wordOrder === 'VSO') {
125
+ // Add keyword first
126
+ tokens.push(keywordToken);
127
+
128
+ // Add roles with markers
129
+ for (const role of sortedRoles) {
130
+ addRoleWithMarker(tokens, role, profile);
131
+ }
132
+ }
133
+ // Other word orders can be added as needed
134
+ else {
135
+ // Fallback to SVO-style ordering
136
+ tokens.push(keywordToken);
137
+ for (const role of sortedRoles) {
138
+ addRoleWithMarker(tokens, role, profile);
139
+ }
140
+ }
141
+
142
+ return tokens;
143
+ }
144
+
145
+ /**
146
+ * Add a role token with its grammatical marker if needed.
147
+ * For optional roles, wraps the role + marker in a group.
148
+ */
149
+ function addRoleWithMarker(
150
+ tokens: PatternToken[],
151
+ roleSpec: RoleSpec,
152
+ profile: PatternGenLanguageProfile
153
+ ): void {
154
+ const marker = getMarkerForRole(roleSpec, profile);
155
+ const isOptional = !roleSpec.required;
156
+
157
+ // Build the role token
158
+ const roleToken: PatternToken = {
159
+ type: 'role',
160
+ role: roleSpec.role,
161
+ optional: isOptional,
162
+ expectedTypes: roleSpec.expectedTypes,
163
+ ...(roleSpec.greedy && { greedy: true }),
164
+ };
165
+
166
+ if (marker) {
167
+ const markerInfo = profile.roleMarkers?.[roleSpec.role];
168
+ // Default: SOV languages use postpositions (after), others prepositions (before)
169
+ const defaultPosition = profile.wordOrder === 'SOV' ? 'after' : 'before';
170
+ const position = markerInfo?.position ?? defaultPosition;
171
+
172
+ if (isOptional) {
173
+ // Optional role: wrap marker + role in optional group
174
+ if (position === 'before') {
175
+ tokens.push({
176
+ type: 'group',
177
+ optional: true,
178
+ tokens: [{ type: 'literal', value: marker }, roleToken],
179
+ });
180
+ } else {
181
+ tokens.push({
182
+ type: 'group',
183
+ optional: true,
184
+ tokens: [roleToken, { type: 'literal', value: marker }],
185
+ });
186
+ }
187
+ } else {
188
+ // Required role: add marker + role separately
189
+ if (position === 'before') {
190
+ tokens.push({ type: 'literal', value: marker });
191
+ tokens.push(roleToken);
192
+ } else {
193
+ tokens.push(roleToken);
194
+ tokens.push({ type: 'literal', value: marker });
195
+ }
196
+ }
197
+ } else {
198
+ // No marker - just add the role
199
+ tokens.push(roleToken);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Get the grammatical marker for a role from the profile.
205
+ */
206
+ function getMarkerForRole(
207
+ roleSpec: RoleSpec,
208
+ profile: PatternGenLanguageProfile
209
+ ): string | undefined {
210
+ // Check for role-specific override first
211
+ if (roleSpec.markerOverride?.[profile.code]) {
212
+ return roleSpec.markerOverride[profile.code];
213
+ }
214
+
215
+ // Use profile's default marker for this role
216
+ return profile.roleMarkers?.[roleSpec.role]?.primary;
217
+ }
218
+
219
+ /**
220
+ * Sort roles by word order for pattern building.
221
+ */
222
+ function sortRolesByWordOrder(roles: RoleSpec[], wordOrder: string): RoleSpec[] {
223
+ const sorted = [...roles];
224
+
225
+ if (wordOrder === 'SVO') {
226
+ // Sort by svoPosition (higher = earlier)
227
+ sorted.sort((a, b) => (b.svoPosition ?? 0) - (a.svoPosition ?? 0));
228
+ } else if (wordOrder === 'SOV') {
229
+ // Sort by sovPosition (higher = earlier)
230
+ sorted.sort((a, b) => (b.sovPosition ?? 0) - (a.sovPosition ?? 0));
231
+ }
232
+ // VSO and others use SVO-style ordering
233
+
234
+ return sorted;
235
+ }
236
+
237
+ /**
238
+ * Build extraction rules for capturing role values during parsing.
239
+ */
240
+ function buildExtractionRules(
241
+ schema: CommandSchema,
242
+ profile: PatternGenLanguageProfile
243
+ ): Record<string, ExtractionRule> {
244
+ const rules: Record<string, ExtractionRule> = {};
245
+
246
+ for (const roleSpec of schema.roles) {
247
+ // Build the rule object correctly with readonly properties
248
+ const marker = getMarkerForRole(roleSpec, profile);
249
+
250
+ const rule: ExtractionRule = {
251
+ ...(roleSpec.default !== undefined && { default: roleSpec.default }),
252
+ ...(marker && { marker }),
253
+ };
254
+
255
+ rules[roleSpec.role] = rule;
256
+ }
257
+
258
+ return rules;
259
+ }
260
+
261
+ /**
262
+ * Build a human-readable format string for the pattern.
263
+ */
264
+ function buildFormatString(
265
+ schema: CommandSchema,
266
+ profile: PatternGenLanguageProfile,
267
+ keyword: string
268
+ ): string {
269
+ const parts: string[] = [];
270
+
271
+ if (profile.wordOrder === 'SVO' || profile.wordOrder === 'VSO') {
272
+ parts.push(keyword);
273
+ }
274
+
275
+ for (const role of schema.roles) {
276
+ const marker = getMarkerForRole(role, profile);
277
+ const roleName = `{${role.role}}`;
278
+
279
+ if (marker) {
280
+ const markerInfo = profile.roleMarkers?.[role.role];
281
+ if (markerInfo?.position === 'after') {
282
+ parts.push(`${roleName} ${marker}`);
283
+ } else {
284
+ parts.push(`${marker} ${roleName}`);
285
+ }
286
+ } else {
287
+ parts.push(roleName);
288
+ }
289
+ }
290
+
291
+ if (profile.wordOrder === 'SOV') {
292
+ parts.push(keyword);
293
+ }
294
+
295
+ return parts.join(' ');
296
+ }
297
+
298
+ /**
299
+ * Generate multiple pattern variants (with and without optional roles).
300
+ */
301
+ export function generatePatternVariants(
302
+ schema: CommandSchema,
303
+ profile: PatternGenLanguageProfile,
304
+ config: GeneratorConfig = defaultConfig
305
+ ): LanguagePattern[] {
306
+ const patterns: LanguagePattern[] = [];
307
+
308
+ // Main pattern (all roles)
309
+ patterns.push(generatePattern(schema, profile, config));
310
+
311
+ // TODO: Generate variants with optional roles omitted
312
+ // This can be added later when needed
313
+
314
+ return patterns;
315
+ }
@@ -0,0 +1,266 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ lookupKeyword,
4
+ lookupMarker,
5
+ buildPhrase,
6
+ buildTablesFromProfiles,
7
+ detectWordOrders,
8
+ createSchemaRenderer,
9
+ } from './renderer';
10
+ import type { KeywordTable, MarkerTable } from './renderer';
11
+ import type { SemanticNode } from '../core/types';
12
+ import type { CommandSchema } from '../schema';
13
+ import { defineCommand, defineRole } from '../schema';
14
+ import type { PatternGenLanguageProfile } from './pattern-generator';
15
+
16
+ // =============================================================================
17
+ // Test Data
18
+ // =============================================================================
19
+
20
+ const KEYWORDS: KeywordTable = {
21
+ select: { en: 'select', ja: '選択', es: 'seleccionar' },
22
+ delete: { en: 'delete', ja: '削除', es: 'eliminar' },
23
+ };
24
+
25
+ const MARKERS: MarkerTable = {
26
+ from: { en: 'from', ja: 'から', es: 'de' },
27
+ where: { en: 'where', ja: '条件', es: 'donde' },
28
+ };
29
+
30
+ function makeNode(action: string, roles: Record<string, string>): SemanticNode {
31
+ const rolesMap = new Map<string, { type: 'expression'; raw: string }>();
32
+ for (const [k, v] of Object.entries(roles)) {
33
+ rolesMap.set(k, { type: 'expression', raw: v });
34
+ }
35
+ return { kind: 'command', action, roles: rolesMap };
36
+ }
37
+
38
+ // =============================================================================
39
+ // lookupKeyword / lookupMarker
40
+ // =============================================================================
41
+
42
+ describe('lookupKeyword', () => {
43
+ it('returns translated keyword', () => {
44
+ expect(lookupKeyword(KEYWORDS, 'select', 'ja')).toBe('選択');
45
+ });
46
+
47
+ it('falls back to action key if no translation', () => {
48
+ expect(lookupKeyword(KEYWORDS, 'select', 'fr')).toBe('select');
49
+ });
50
+
51
+ it('falls back to action key if unknown action', () => {
52
+ expect(lookupKeyword(KEYWORDS, 'unknown', 'en')).toBe('unknown');
53
+ });
54
+ });
55
+
56
+ describe('lookupMarker', () => {
57
+ it('returns translated marker', () => {
58
+ expect(lookupMarker(MARKERS, 'from', 'ja')).toBe('から');
59
+ });
60
+
61
+ it('falls back to marker key', () => {
62
+ expect(lookupMarker(MARKERS, 'from', 'fr')).toBe('from');
63
+ });
64
+ });
65
+
66
+ // =============================================================================
67
+ // buildPhrase
68
+ // =============================================================================
69
+
70
+ describe('buildPhrase', () => {
71
+ it('joins parts with spaces', () => {
72
+ expect(buildPhrase('select', 'name', 'from', 'users')).toBe('select name from users');
73
+ });
74
+
75
+ it('filters empty strings', () => {
76
+ expect(buildPhrase('select', '', 'name', '', 'from', 'users')).toBe('select name from users');
77
+ });
78
+
79
+ it('returns empty string for all empty parts', () => {
80
+ expect(buildPhrase('', '', '')).toBe('');
81
+ });
82
+ });
83
+
84
+ // =============================================================================
85
+ // buildTablesFromProfiles
86
+ // =============================================================================
87
+
88
+ describe('buildTablesFromProfiles', () => {
89
+ const schemas: CommandSchema[] = [
90
+ defineCommand({
91
+ action: 'select',
92
+ description: 'Select data',
93
+ category: 'query',
94
+ primaryRole: 'columns',
95
+ roles: [
96
+ defineRole({
97
+ role: 'columns',
98
+ required: true,
99
+ expectedTypes: ['expression'],
100
+ svoPosition: 1,
101
+ }),
102
+ defineRole({
103
+ role: 'source',
104
+ required: true,
105
+ expectedTypes: ['expression'],
106
+ svoPosition: 2,
107
+ markerOverride: { en: 'from', ja: 'から' },
108
+ }),
109
+ ],
110
+ }),
111
+ ];
112
+
113
+ const profiles: PatternGenLanguageProfile[] = [
114
+ {
115
+ code: 'en',
116
+ wordOrder: 'SVO',
117
+ keywords: { select: { primary: 'select' } },
118
+ roleMarkers: {},
119
+ },
120
+ {
121
+ code: 'ja',
122
+ wordOrder: 'SOV',
123
+ keywords: { select: { primary: '選択' } },
124
+ roleMarkers: {},
125
+ },
126
+ ];
127
+
128
+ it('builds keyword table from profiles', () => {
129
+ const { keywords } = buildTablesFromProfiles(schemas, profiles);
130
+ expect(keywords.select.en).toBe('select');
131
+ expect(keywords.select.ja).toBe('選択');
132
+ });
133
+
134
+ it('builds marker table from schema markerOverrides', () => {
135
+ const { markers } = buildTablesFromProfiles(schemas, profiles);
136
+ expect(markers.source.en).toBe('from');
137
+ expect(markers.source.ja).toBe('から');
138
+ });
139
+ });
140
+
141
+ // =============================================================================
142
+ // detectWordOrders
143
+ // =============================================================================
144
+
145
+ describe('detectWordOrders', () => {
146
+ const profiles: PatternGenLanguageProfile[] = [
147
+ { code: 'en', wordOrder: 'SVO', keywords: {} },
148
+ { code: 'ja', wordOrder: 'SOV', keywords: {} },
149
+ { code: 'ar', wordOrder: 'VSO', keywords: {} },
150
+ { code: 'es', wordOrder: 'SVO', keywords: {} },
151
+ ];
152
+
153
+ it('detects SOV languages', () => {
154
+ const { sovLanguages } = detectWordOrders(profiles);
155
+ expect(sovLanguages.has('ja')).toBe(true);
156
+ expect(sovLanguages.has('en')).toBe(false);
157
+ });
158
+
159
+ it('detects VSO languages', () => {
160
+ const { vsoLanguages } = detectWordOrders(profiles);
161
+ expect(vsoLanguages.has('ar')).toBe(true);
162
+ expect(vsoLanguages.has('en')).toBe(false);
163
+ });
164
+ });
165
+
166
+ // =============================================================================
167
+ // createSchemaRenderer
168
+ // =============================================================================
169
+
170
+ describe('createSchemaRenderer', () => {
171
+ const schemas: CommandSchema[] = [
172
+ defineCommand({
173
+ action: 'select',
174
+ description: 'Select data',
175
+ category: 'query',
176
+ primaryRole: 'columns',
177
+ roles: [
178
+ defineRole({
179
+ role: 'columns',
180
+ required: true,
181
+ expectedTypes: ['expression'],
182
+ svoPosition: 1,
183
+ }),
184
+ defineRole({
185
+ role: 'source',
186
+ required: true,
187
+ expectedTypes: ['expression'],
188
+ svoPosition: 2,
189
+ markerOverride: { en: 'from', ja: 'から' },
190
+ }),
191
+ ],
192
+ }),
193
+ ];
194
+
195
+ const profiles: PatternGenLanguageProfile[] = [
196
+ {
197
+ code: 'en',
198
+ wordOrder: 'SVO',
199
+ keywords: { select: { primary: 'select' } },
200
+ roleMarkers: {},
201
+ },
202
+ {
203
+ code: 'ja',
204
+ wordOrder: 'SOV',
205
+ keywords: { select: { primary: '選択' } },
206
+ roleMarkers: {},
207
+ },
208
+ ];
209
+
210
+ it('renders SVO (English)', () => {
211
+ const renderer = createSchemaRenderer(schemas, profiles);
212
+ const node = makeNode('select', { columns: 'name', source: 'users' });
213
+ expect(renderer.render(node, 'en')).toBe('select name from users');
214
+ });
215
+
216
+ it('renders SOV (Japanese)', () => {
217
+ const renderer = createSchemaRenderer(schemas, profiles);
218
+ const node = makeNode('select', { columns: 'name', source: 'users' });
219
+ const result = renderer.render(node, 'ja');
220
+ // SOV: roles first (value marker), then keyword
221
+ expect(result).toBe('name users から 選択');
222
+ });
223
+
224
+ it('handles unknown action gracefully', () => {
225
+ const renderer = createSchemaRenderer(schemas, profiles);
226
+ const node = makeNode('unknown', {});
227
+ expect(renderer.render(node, 'en')).toBe('unknown');
228
+ });
229
+
230
+ it('skips optional roles that are empty', () => {
231
+ const schemasWithOptional: CommandSchema[] = [
232
+ defineCommand({
233
+ action: 'select',
234
+ description: 'Select data',
235
+ category: 'query',
236
+ primaryRole: 'columns',
237
+ roles: [
238
+ defineRole({
239
+ role: 'columns',
240
+ required: true,
241
+ expectedTypes: ['expression'],
242
+ svoPosition: 1,
243
+ }),
244
+ defineRole({
245
+ role: 'source',
246
+ required: true,
247
+ expectedTypes: ['expression'],
248
+ svoPosition: 2,
249
+ markerOverride: { en: 'from' },
250
+ }),
251
+ defineRole({
252
+ role: 'condition',
253
+ required: false,
254
+ expectedTypes: ['expression'],
255
+ svoPosition: 3,
256
+ markerOverride: { en: 'where' },
257
+ }),
258
+ ],
259
+ }),
260
+ ];
261
+
262
+ const renderer = createSchemaRenderer(schemasWithOptional, profiles);
263
+ const node = makeNode('select', { columns: 'name', source: 'users' });
264
+ expect(renderer.render(node, 'en')).toBe('select name from users');
265
+ });
266
+ });