@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.
- package/LICENSE +20 -0
- package/README.md +142 -0
- package/dist/aot/aot-orchestrator.d.ts +75 -0
- package/dist/aot/aot-orchestrator.d.ts.map +1 -0
- package/dist/aot/domain-scanner.d.ts +27 -0
- package/dist/aot/domain-scanner.d.ts.map +1 -0
- package/dist/aot/index.d.ts +8 -0
- package/dist/aot/index.d.ts.map +1 -0
- package/dist/aot/types.d.ts +103 -0
- package/dist/aot/types.d.ts.map +1 -0
- package/dist/api/create-dsl.d.ts +91 -0
- package/dist/api/create-dsl.d.ts.map +1 -0
- package/dist/api/dispatcher.d.ts +108 -0
- package/dist/api/dispatcher.d.ts.map +1 -0
- package/dist/api/domain-registry.d.ts +152 -0
- package/dist/api/domain-registry.d.ts.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +2082 -0
- package/dist/api/index.js.map +1 -0
- package/dist/core/index.d.ts +7 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +2674 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +32 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/pattern-matching/index.d.ts +6 -0
- package/dist/core/pattern-matching/index.d.ts.map +1 -0
- package/dist/core/pattern-matching/index.js +1239 -0
- package/dist/core/pattern-matching/index.js.map +1 -0
- package/dist/core/pattern-matching/pattern-matcher.d.ts +239 -0
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -0
- package/dist/core/pattern-matching/utils/index.d.ts +6 -0
- package/dist/core/pattern-matching/utils/index.d.ts.map +1 -0
- package/dist/core/pattern-matching/utils/possessive-keywords.d.ts +38 -0
- package/dist/core/pattern-matching/utils/possessive-keywords.d.ts.map +1 -0
- package/dist/core/pattern-matching/utils/type-validation.d.ts +63 -0
- package/dist/core/pattern-matching/utils/type-validation.d.ts.map +1 -0
- package/dist/core/tokenization/base-tokenizer.d.ts +344 -0
- package/dist/core/tokenization/base-tokenizer.d.ts.map +1 -0
- package/dist/core/tokenization/char-classifiers.d.ts +56 -0
- package/dist/core/tokenization/char-classifiers.d.ts.map +1 -0
- package/dist/core/tokenization/default-extractors.d.ts +48 -0
- package/dist/core/tokenization/default-extractors.d.ts.map +1 -0
- package/dist/core/tokenization/extractors/index.d.ts +9 -0
- package/dist/core/tokenization/extractors/index.d.ts.map +1 -0
- package/dist/core/tokenization/extractors/operator.d.ts +23 -0
- package/dist/core/tokenization/extractors/operator.d.ts.map +1 -0
- package/dist/core/tokenization/extractors/punctuation.d.ts +22 -0
- package/dist/core/tokenization/extractors/punctuation.d.ts.map +1 -0
- package/dist/core/tokenization/extractors.d.ts +61 -0
- package/dist/core/tokenization/extractors.d.ts.map +1 -0
- package/dist/core/tokenization/index.d.ts +11 -0
- package/dist/core/tokenization/index.d.ts.map +1 -0
- package/dist/core/tokenization/index.js +1345 -0
- package/dist/core/tokenization/index.js.map +1 -0
- package/dist/core/tokenization/morphology/index.d.ts +5 -0
- package/dist/core/tokenization/morphology/index.d.ts.map +1 -0
- package/dist/core/tokenization/morphology/types.d.ts +110 -0
- package/dist/core/tokenization/morphology/types.d.ts.map +1 -0
- package/dist/core/tokenization/token-utils.d.ts +111 -0
- package/dist/core/tokenization/token-utils.d.ts.map +1 -0
- package/dist/core/types.d.ts +382 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +108 -0
- package/dist/core/types.js.map +1 -0
- package/dist/generation/diagnostics.d.ts +120 -0
- package/dist/generation/diagnostics.d.ts.map +1 -0
- package/dist/generation/index.d.ts +7 -0
- package/dist/generation/index.d.ts.map +1 -0
- package/dist/generation/index.js +339 -0
- package/dist/generation/index.js.map +1 -0
- package/dist/generation/pattern-generator.d.ts +48 -0
- package/dist/generation/pattern-generator.d.ts.map +1 -0
- package/dist/generation/renderer.d.ts +115 -0
- package/dist/generation/renderer.d.ts.map +1 -0
- package/dist/grammar/index.d.ts +10 -0
- package/dist/grammar/index.d.ts.map +1 -0
- package/dist/grammar/index.js +391 -0
- package/dist/grammar/index.js.map +1 -0
- package/dist/grammar/transformer.d.ts +56 -0
- package/dist/grammar/transformer.d.ts.map +1 -0
- package/dist/grammar/types.d.ts +236 -0
- package/dist/grammar/types.d.ts.map +1 -0
- package/dist/index.cjs +4454 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4336 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/dictionary.d.ts +82 -0
- package/dist/interfaces/dictionary.d.ts.map +1 -0
- package/dist/interfaces/index.d.ts +10 -0
- package/dist/interfaces/index.d.ts.map +1 -0
- package/dist/interfaces/profile-provider.d.ts +67 -0
- package/dist/interfaces/profile-provider.d.ts.map +1 -0
- package/dist/interfaces/value-extractor.d.ts +168 -0
- package/dist/interfaces/value-extractor.d.ts.map +1 -0
- package/dist/multilingual/index.d.ts +8 -0
- package/dist/multilingual/index.d.ts.map +1 -0
- package/dist/multilingual/index.js +1 -0
- package/dist/multilingual/index.js.map +1 -0
- package/dist/parsing/index.d.ts +8 -0
- package/dist/parsing/index.d.ts.map +1 -0
- package/dist/parsing/index.js +1415 -0
- package/dist/parsing/index.js.map +1 -0
- package/dist/parsing/multi-statement.d.ts +265 -0
- package/dist/parsing/multi-statement.d.ts.map +1 -0
- package/dist/schema/command-schema.d.ts +78 -0
- package/dist/schema/command-schema.d.ts.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +25 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/test-setup.d.ts +9 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/testing/index.d.ts +50 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16969 -0
- package/dist/testing/index.js.map +1 -0
- package/package.json +122 -0
- package/src/__test__/fixtures/sql-dsl.ts +232 -0
- package/src/__test__/sql-integration.test.ts +189 -0
- package/src/__test__/test-utils.ts +260 -0
- package/src/aot/aot-orchestrator.test.ts +413 -0
- package/src/aot/aot-orchestrator.ts +238 -0
- package/src/aot/domain-scanner.ts +178 -0
- package/src/aot/index.ts +8 -0
- package/src/aot/types.ts +124 -0
- package/src/api/create-dsl.ts +367 -0
- package/src/api/dispatcher.test.ts +336 -0
- package/src/api/dispatcher.ts +222 -0
- package/src/api/domain-registry.test.ts +336 -0
- package/src/api/domain-registry.ts +500 -0
- package/src/api/index.ts +7 -0
- package/src/core/index.ts +7 -0
- package/src/core/logger.ts +130 -0
- package/src/core/pattern-matching/index.ts +6 -0
- package/src/core/pattern-matching/pattern-matcher.test.ts +900 -0
- package/src/core/pattern-matching/pattern-matcher.ts +1548 -0
- package/src/core/pattern-matching/pattern-matcher.ts.backup +1267 -0
- package/src/core/pattern-matching/utils/index.ts +6 -0
- package/src/core/pattern-matching/utils/possessive-keywords.ts +55 -0
- package/src/core/pattern-matching/utils/type-validation.test.ts +316 -0
- package/src/core/pattern-matching/utils/type-validation.ts +134 -0
- package/src/core/tokenization/base-tokenizer.ts +916 -0
- package/src/core/tokenization/char-classifiers.ts +79 -0
- package/src/core/tokenization/create-simple-tokenizer.test.ts +260 -0
- package/src/core/tokenization/default-extractors.ts +69 -0
- package/src/core/tokenization/extractors/index.ts +9 -0
- package/src/core/tokenization/extractors/operator.ts +75 -0
- package/src/core/tokenization/extractors/punctuation.ts +39 -0
- package/src/core/tokenization/extractors.ts +452 -0
- package/src/core/tokenization/index.ts +11 -0
- package/src/core/tokenization/morphology/index.ts +5 -0
- package/src/core/tokenization/morphology/types.ts +211 -0
- package/src/core/tokenization/token-utils.ts +252 -0
- package/src/core/types.ts +589 -0
- package/src/generation/diagnostics.test.ts +171 -0
- package/src/generation/diagnostics.ts +239 -0
- package/src/generation/index.ts +7 -0
- package/src/generation/pattern-generator.test.ts +430 -0
- package/src/generation/pattern-generator.ts +315 -0
- package/src/generation/renderer.test.ts +266 -0
- package/src/generation/renderer.ts +244 -0
- package/src/grammar/index.ts +12 -0
- package/src/grammar/transformer.ts +159 -0
- package/src/grammar/types.ts +630 -0
- package/src/index.ts +157 -0
- package/src/interfaces/dictionary.ts +123 -0
- package/src/interfaces/index.ts +10 -0
- package/src/interfaces/profile-provider.ts +88 -0
- package/src/interfaces/value-extractor.ts +435 -0
- package/src/multilingual/index.ts +9 -0
- package/src/parsing/index.ts +27 -0
- package/src/parsing/multi-statement.test.ts +480 -0
- package/src/parsing/multi-statement.ts +648 -0
- package/src/schema/command-schema.ts +118 -0
- package/src/schema/index.ts +5 -0
- package/src/test-setup.ts +45 -0
- 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
|
+
});
|