@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,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main API for creating multilingual DSLs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { SemanticNode, LanguageTokenizer, LanguagePattern } from '../core/types';
|
|
6
|
+
import type { CommandSchema } from '../schema';
|
|
7
|
+
import { PatternMatcher, type PatternMatcherProfile } from '../core/pattern-matching';
|
|
8
|
+
import { generatePattern, type PatternGenLanguageProfile } from '../generation/pattern-generator';
|
|
9
|
+
import { GrammarTransformer, type LanguageProfile as GrammarProfile } from '../grammar';
|
|
10
|
+
import {
|
|
11
|
+
type Dictionary,
|
|
12
|
+
type ProfileProvider,
|
|
13
|
+
type ValueExtractor,
|
|
14
|
+
InMemoryDictionary,
|
|
15
|
+
InMemoryProfileProvider,
|
|
16
|
+
} from '../interfaces';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Language configuration for a DSL.
|
|
20
|
+
*/
|
|
21
|
+
export interface LanguageConfig {
|
|
22
|
+
/** ISO 639-1 language code */
|
|
23
|
+
readonly code: string;
|
|
24
|
+
/** English name of the language */
|
|
25
|
+
readonly name: string;
|
|
26
|
+
/** Native name of the language */
|
|
27
|
+
readonly nativeName: string;
|
|
28
|
+
/** Language tokenizer */
|
|
29
|
+
readonly tokenizer: LanguageTokenizer;
|
|
30
|
+
/** Language profile for pattern generation */
|
|
31
|
+
readonly patternProfile: PatternGenLanguageProfile;
|
|
32
|
+
/** Language profile for grammar transformation (optional) */
|
|
33
|
+
readonly grammarProfile?: GrammarProfile;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Code generator function that transforms semantic AST to target code.
|
|
38
|
+
*/
|
|
39
|
+
export interface CodeGenerator {
|
|
40
|
+
generate(node: SemanticNode): string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* DSL configuration with dependency injection support.
|
|
45
|
+
*/
|
|
46
|
+
export interface DSLConfig {
|
|
47
|
+
/** DSL name (for debugging/documentation) */
|
|
48
|
+
readonly name?: string;
|
|
49
|
+
|
|
50
|
+
/** Command schemas defining the DSL grammar */
|
|
51
|
+
readonly schemas: readonly CommandSchema[];
|
|
52
|
+
|
|
53
|
+
/** Language configurations */
|
|
54
|
+
readonly languages: readonly LanguageConfig[];
|
|
55
|
+
|
|
56
|
+
// === Dependency Injection (Optional) ===
|
|
57
|
+
|
|
58
|
+
/** Dictionary for keyword translation (default: built from language configs) */
|
|
59
|
+
readonly dictionary?: Dictionary;
|
|
60
|
+
|
|
61
|
+
/** Profile provider for grammar transformation (default: built from language configs) */
|
|
62
|
+
readonly profileProvider?: ProfileProvider;
|
|
63
|
+
|
|
64
|
+
/** Value extractors for tokenization (default: generic extractors) */
|
|
65
|
+
readonly valueExtractors?: ValueExtractor[];
|
|
66
|
+
|
|
67
|
+
/** Code generator for compilation (default: none) */
|
|
68
|
+
readonly codeGenerator?: CodeGenerator;
|
|
69
|
+
|
|
70
|
+
// === Options ===
|
|
71
|
+
|
|
72
|
+
/** Auto-generate patterns from schemas (default: true) */
|
|
73
|
+
readonly generatePatterns?: boolean;
|
|
74
|
+
|
|
75
|
+
/** Custom patterns to supplement generated ones */
|
|
76
|
+
readonly customPatterns?: LanguagePattern[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validation result.
|
|
81
|
+
*/
|
|
82
|
+
export interface ValidationResult {
|
|
83
|
+
readonly valid: boolean;
|
|
84
|
+
readonly node?: SemanticNode;
|
|
85
|
+
readonly errors?: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Compilation result.
|
|
90
|
+
*/
|
|
91
|
+
export interface CompileResult {
|
|
92
|
+
readonly ok: boolean;
|
|
93
|
+
readonly code?: string;
|
|
94
|
+
readonly errors?: string[];
|
|
95
|
+
readonly node?: SemanticNode;
|
|
96
|
+
readonly metadata?: {
|
|
97
|
+
readonly parser: string;
|
|
98
|
+
readonly confidence: number;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Main multilingual DSL interface.
|
|
104
|
+
*/
|
|
105
|
+
export interface MultilingualDSL {
|
|
106
|
+
// Parsing
|
|
107
|
+
parse(input: string, language: string): SemanticNode;
|
|
108
|
+
parseWithConfidence(input: string, language: string): { node: SemanticNode; confidence: number };
|
|
109
|
+
|
|
110
|
+
// Validation
|
|
111
|
+
validate(input: string, language: string): ValidationResult;
|
|
112
|
+
|
|
113
|
+
// Compilation
|
|
114
|
+
compile(input: string, language: string): CompileResult;
|
|
115
|
+
|
|
116
|
+
// Translation
|
|
117
|
+
translate(input: string, fromLanguage: string, toLanguage: string): string;
|
|
118
|
+
|
|
119
|
+
// Language support
|
|
120
|
+
getSupportedLanguages(): string[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Internal registry for DSL configuration.
|
|
125
|
+
*/
|
|
126
|
+
class DSLRegistry {
|
|
127
|
+
private patterns = new Map<string, LanguagePattern[]>();
|
|
128
|
+
private tokenizers = new Map<string, LanguageTokenizer>();
|
|
129
|
+
private schemas: readonly CommandSchema[];
|
|
130
|
+
|
|
131
|
+
constructor(config: DSLConfig) {
|
|
132
|
+
this.schemas = config.schemas;
|
|
133
|
+
|
|
134
|
+
// Register each language
|
|
135
|
+
for (const lang of config.languages) {
|
|
136
|
+
this.registerLanguage(lang);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private registerLanguage(lang: LanguageConfig): void {
|
|
141
|
+
// Register tokenizer
|
|
142
|
+
this.tokenizers.set(lang.code, lang.tokenizer);
|
|
143
|
+
|
|
144
|
+
// Generate patterns for this language
|
|
145
|
+
const patterns: LanguagePattern[] = [];
|
|
146
|
+
for (const schema of this.schemas) {
|
|
147
|
+
const pattern = generatePattern(schema, lang.patternProfile);
|
|
148
|
+
patterns.push(pattern);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.patterns.set(lang.code, patterns);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getPatterns(language: string): LanguagePattern[] {
|
|
155
|
+
return this.patterns.get(language) || [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
getTokenizer(language: string): LanguageTokenizer | undefined {
|
|
159
|
+
return this.tokenizers.get(language);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
getSupportedLanguages(): string[] {
|
|
163
|
+
return Array.from(this.patterns.keys());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Implementation of MultilingualDSL.
|
|
169
|
+
*/
|
|
170
|
+
class MultilingualDSLImpl implements MultilingualDSL {
|
|
171
|
+
private registry: DSLRegistry;
|
|
172
|
+
private matcher: PatternMatcher;
|
|
173
|
+
private transformer: GrammarTransformer;
|
|
174
|
+
private codeGenerator?: CodeGenerator;
|
|
175
|
+
|
|
176
|
+
constructor(config: DSLConfig, registry: DSLRegistry, transformer: GrammarTransformer) {
|
|
177
|
+
this.registry = registry;
|
|
178
|
+
this.matcher = new PatternMatcher();
|
|
179
|
+
this.transformer = transformer;
|
|
180
|
+
if (config.codeGenerator) {
|
|
181
|
+
this.codeGenerator = config.codeGenerator;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
parse(input: string, language: string): SemanticNode {
|
|
186
|
+
const result = this.parseWithConfidence(input, language);
|
|
187
|
+
return result.node;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
parseWithConfidence(input: string, language: string): { node: SemanticNode; confidence: number } {
|
|
191
|
+
// Get tokenizer for language
|
|
192
|
+
const tokenizer = this.registry.getTokenizer(language);
|
|
193
|
+
if (!tokenizer) {
|
|
194
|
+
throw new Error(`No tokenizer registered for language: ${language}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Tokenize input
|
|
198
|
+
const tokens = tokenizer.tokenize(input);
|
|
199
|
+
|
|
200
|
+
// Get patterns for language
|
|
201
|
+
const patterns = this.registry.getPatterns(language);
|
|
202
|
+
|
|
203
|
+
// Create profile for matcher
|
|
204
|
+
const profile: PatternMatcherProfile = {
|
|
205
|
+
code: language,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Try to match each pattern
|
|
209
|
+
for (const pattern of patterns) {
|
|
210
|
+
const match = this.matcher.matchPattern(tokens, pattern, profile);
|
|
211
|
+
if (match) {
|
|
212
|
+
// Build semantic node
|
|
213
|
+
const node: SemanticNode = {
|
|
214
|
+
kind: 'command',
|
|
215
|
+
action: pattern.command,
|
|
216
|
+
roles: match.captured,
|
|
217
|
+
metadata: {
|
|
218
|
+
sourceLanguage: language,
|
|
219
|
+
sourceText: input,
|
|
220
|
+
patternId: pattern.id,
|
|
221
|
+
confidence: match.confidence,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return { node, confidence: match.confidence };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
throw new Error(`No pattern matched for input: ${input}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
validate(input: string, language: string): ValidationResult {
|
|
233
|
+
try {
|
|
234
|
+
const result = this.parseWithConfidence(input, language);
|
|
235
|
+
return {
|
|
236
|
+
valid: true,
|
|
237
|
+
node: result.node,
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return {
|
|
241
|
+
valid: false,
|
|
242
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
translate(input: string, fromLanguage: string, toLanguage: string): string {
|
|
248
|
+
// Use injected grammar transformer
|
|
249
|
+
return this.transformer.transform(input, fromLanguage, toLanguage);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
compile(input: string, language: string): CompileResult {
|
|
253
|
+
if (!this.codeGenerator) {
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
errors: ['No code generator configured for this DSL'],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const result = this.parseWithConfidence(input, language);
|
|
262
|
+
const code = this.codeGenerator.generate(result.node);
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
ok: true,
|
|
266
|
+
code,
|
|
267
|
+
node: result.node,
|
|
268
|
+
metadata: {
|
|
269
|
+
parser: 'semantic',
|
|
270
|
+
confidence: result.confidence,
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return {
|
|
275
|
+
ok: false,
|
|
276
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
getSupportedLanguages(): string[] {
|
|
282
|
+
return this.registry.getSupportedLanguages();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Create a new multilingual DSL instance.
|
|
288
|
+
*
|
|
289
|
+
* @param config - DSL configuration
|
|
290
|
+
* @returns MultilingualDSL instance
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```typescript
|
|
294
|
+
* const myDSL = createMultilingualDSL({
|
|
295
|
+
* schemas: [
|
|
296
|
+
* defineCommand('select', [
|
|
297
|
+
* defineRole('field', 'identifier', true),
|
|
298
|
+
* defineRole('source', 'identifier', true),
|
|
299
|
+
* ]),
|
|
300
|
+
* ],
|
|
301
|
+
* languages: [
|
|
302
|
+
* {
|
|
303
|
+
* code: 'en',
|
|
304
|
+
* name: 'English',
|
|
305
|
+
* nativeName: 'English',
|
|
306
|
+
* tokenizer: englishTokenizer,
|
|
307
|
+
* patternProfile: englishProfile,
|
|
308
|
+
* },
|
|
309
|
+
* ],
|
|
310
|
+
* codeGenerator: {
|
|
311
|
+
* generate: (node) => generateSQL(node),
|
|
312
|
+
* },
|
|
313
|
+
* });
|
|
314
|
+
*
|
|
315
|
+
* const result = myDSL.compile('select name from users', 'en');
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
/**
|
|
319
|
+
* Create default dictionary from language configurations.
|
|
320
|
+
*/
|
|
321
|
+
function createDefaultDictionary(config: DSLConfig): Dictionary {
|
|
322
|
+
const translations: Record<string, Record<string, string>> = {};
|
|
323
|
+
|
|
324
|
+
for (const lang of config.languages) {
|
|
325
|
+
// Convert keyword objects to simple string mappings
|
|
326
|
+
const keywords: Record<string, string> = {};
|
|
327
|
+
for (const [canonical, keywordDef] of Object.entries(lang.patternProfile.keywords)) {
|
|
328
|
+
keywords[canonical] = keywordDef.primary;
|
|
329
|
+
}
|
|
330
|
+
translations[lang.code] = keywords;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return new InMemoryDictionary(translations);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Create default profile provider from language configurations.
|
|
338
|
+
*/
|
|
339
|
+
function createDefaultProfileProvider(config: DSLConfig): ProfileProvider {
|
|
340
|
+
const profiles: Record<string, GrammarProfile> = {};
|
|
341
|
+
|
|
342
|
+
for (const lang of config.languages) {
|
|
343
|
+
if (lang.grammarProfile) {
|
|
344
|
+
profiles[lang.code] = lang.grammarProfile;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return new InMemoryProfileProvider(profiles);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function createMultilingualDSL(config: DSLConfig): MultilingualDSL {
|
|
352
|
+
// Create or use provided dictionary
|
|
353
|
+
const dictionary = config.dictionary ?? createDefaultDictionary(config);
|
|
354
|
+
|
|
355
|
+
// Create or use provided profile provider
|
|
356
|
+
const profileProvider = config.profileProvider ?? createDefaultProfileProvider(config);
|
|
357
|
+
|
|
358
|
+
// Create grammar transformer with injected dependencies
|
|
359
|
+
const transformer = new GrammarTransformer({
|
|
360
|
+
dictionary,
|
|
361
|
+
profileProvider,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Create registry and implementation
|
|
365
|
+
const registry = new DSLRegistry(config);
|
|
366
|
+
return new MultilingualDSLImpl(config, registry, transformer);
|
|
367
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { CrossDomainDispatcher } from './dispatcher';
|
|
3
|
+
import { DomainRegistry } from './domain-registry';
|
|
4
|
+
import type { DomainDescriptor } from './domain-registry';
|
|
5
|
+
import type { MultilingualDSL } from './create-dsl';
|
|
6
|
+
import type { SemanticNode } from '../core/types';
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Test Helpers
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a mock DSL that parses input starting with a given keyword.
|
|
14
|
+
* Confidence is configurable. Throws on non-matching input.
|
|
15
|
+
*/
|
|
16
|
+
function createMockDSL(keyword: string, confidence = 0.9): MultilingualDSL {
|
|
17
|
+
return {
|
|
18
|
+
parse(input: string, language: string): SemanticNode {
|
|
19
|
+
const lower = input.toLowerCase().trim();
|
|
20
|
+
if (!lower.startsWith(keyword)) {
|
|
21
|
+
throw new Error(`Input does not start with "${keyword}"`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
kind: 'command',
|
|
25
|
+
action: keyword,
|
|
26
|
+
roles: new Map([['raw', { type: 'expression' as const, raw: input }]]),
|
|
27
|
+
metadata: { sourceLanguage: language },
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
parseWithConfidence(input: string, language: string) {
|
|
31
|
+
const node = this.parse(input, language);
|
|
32
|
+
return { node, confidence };
|
|
33
|
+
},
|
|
34
|
+
validate(input: string, language: string) {
|
|
35
|
+
try {
|
|
36
|
+
this.parse(input, language);
|
|
37
|
+
return { valid: true };
|
|
38
|
+
} catch {
|
|
39
|
+
return { valid: false, errors: ['Parse failed'] };
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
compile(input: string, language: string) {
|
|
43
|
+
try {
|
|
44
|
+
const node = this.parse(input, language);
|
|
45
|
+
return { ok: true, code: `compiled:${keyword}(${input})`, node };
|
|
46
|
+
} catch {
|
|
47
|
+
return { ok: false, errors: ['Compile failed'] };
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
translate(input: string, _from: string, to: string) {
|
|
51
|
+
return `[${to}] ${input}`;
|
|
52
|
+
},
|
|
53
|
+
getSupportedLanguages() {
|
|
54
|
+
return ['en', 'ja'];
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createDescriptor(name: string, keyword: string, confidence = 0.9): DomainDescriptor {
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
description: `${name} domain`,
|
|
63
|
+
languages: ['en', 'ja'],
|
|
64
|
+
inputLabel: 'input',
|
|
65
|
+
inputDescription: `${name} input`,
|
|
66
|
+
getDSL: () => createMockDSL(keyword, confidence),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Tests
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
74
|
+
describe('CrossDomainDispatcher', () => {
|
|
75
|
+
let registry: DomainRegistry;
|
|
76
|
+
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
registry = new DomainRegistry();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// detect()
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
describe('detect', () => {
|
|
86
|
+
it('returns null when no domains are registered', async () => {
|
|
87
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
88
|
+
const result = await dispatcher.detect('anything');
|
|
89
|
+
expect(result).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('detects the correct domain for matching input', async () => {
|
|
93
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
94
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
95
|
+
|
|
96
|
+
const result = await dispatcher.detect('select name from users');
|
|
97
|
+
expect(result).not.toBeNull();
|
|
98
|
+
expect(result!.domain).toBe('sql');
|
|
99
|
+
expect(result!.node.action).toBe('select');
|
|
100
|
+
expect(result!.confidence).toBe(0.9);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('returns null when no domain matches', async () => {
|
|
104
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
105
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
106
|
+
|
|
107
|
+
const result = await dispatcher.detect('toggle .active');
|
|
108
|
+
expect(result).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('picks the highest confidence when multiple domains match', async () => {
|
|
112
|
+
// Both domains accept "select" but with different confidence
|
|
113
|
+
registry.register(createDescriptor('sql', 'select', 0.95));
|
|
114
|
+
registry.register(createDescriptor('query', 'select', 0.7));
|
|
115
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
116
|
+
|
|
117
|
+
const result = await dispatcher.detect('select name from users');
|
|
118
|
+
expect(result).not.toBeNull();
|
|
119
|
+
expect(result!.domain).toBe('sql');
|
|
120
|
+
expect(result!.confidence).toBe(0.95);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('breaks confidence ties using priority order', async () => {
|
|
124
|
+
registry.register(createDescriptor('alpha', 'select', 0.9));
|
|
125
|
+
registry.register(createDescriptor('beta', 'select', 0.9));
|
|
126
|
+
const dispatcher = new CrossDomainDispatcher(registry, {
|
|
127
|
+
priority: ['beta', 'alpha'],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const result = await dispatcher.detect('select name from users');
|
|
131
|
+
expect(result).not.toBeNull();
|
|
132
|
+
expect(result!.domain).toBe('beta');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('respects minConfidence threshold', async () => {
|
|
136
|
+
registry.register(createDescriptor('sql', 'select', 0.4));
|
|
137
|
+
const dispatcher = new CrossDomainDispatcher(registry, {
|
|
138
|
+
minConfidence: 0.5,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const result = await dispatcher.detect('select name from users');
|
|
142
|
+
expect(result).toBeNull();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('uses default minConfidence of 0.5', async () => {
|
|
146
|
+
registry.register(createDescriptor('sql', 'select', 0.5));
|
|
147
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
148
|
+
|
|
149
|
+
const result = await dispatcher.detect('select name from users');
|
|
150
|
+
expect(result).not.toBeNull();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('handles domains that throw on parse', async () => {
|
|
154
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
155
|
+
registry.register(createDescriptor('bdd', 'given'));
|
|
156
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
157
|
+
|
|
158
|
+
// "given" input — sql will throw, bdd will match
|
|
159
|
+
const result = await dispatcher.detect('given page http://example.com');
|
|
160
|
+
expect(result).not.toBeNull();
|
|
161
|
+
expect(result!.domain).toBe('bdd');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('uses default language "en"', async () => {
|
|
165
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
166
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
167
|
+
|
|
168
|
+
const result = await dispatcher.detect('select name');
|
|
169
|
+
expect(result!.node.metadata?.sourceLanguage).toBe('en');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('passes language parameter through', async () => {
|
|
173
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
174
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
175
|
+
|
|
176
|
+
const result = await dispatcher.detect('select name', 'ja');
|
|
177
|
+
expect(result!.node.metadata?.sourceLanguage).toBe('ja');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// parseComposite()
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
describe('parseComposite', () => {
|
|
186
|
+
it('parses multi-line input across domains', async () => {
|
|
187
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
188
|
+
registry.register(createDescriptor('bdd', 'given'));
|
|
189
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
190
|
+
|
|
191
|
+
const result = await dispatcher.parseComposite(
|
|
192
|
+
'select name from users\ngiven page http://example.com'
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(result.statements).toHaveLength(2);
|
|
196
|
+
expect(result.errors).toHaveLength(0);
|
|
197
|
+
expect(result.statements[0].domain).toBe('sql');
|
|
198
|
+
expect(result.statements[0].line).toBe(1);
|
|
199
|
+
expect(result.statements[1].domain).toBe('bdd');
|
|
200
|
+
expect(result.statements[1].line).toBe(2);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('skips empty lines', async () => {
|
|
204
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
205
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
206
|
+
|
|
207
|
+
const result = await dispatcher.parseComposite('\n\nselect name\n\n');
|
|
208
|
+
expect(result.statements).toHaveLength(1);
|
|
209
|
+
expect(result.statements[0].line).toBe(3);
|
|
210
|
+
expect(result.errors).toHaveLength(0);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('skips comment lines (// and -- and #)', async () => {
|
|
214
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
215
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
216
|
+
|
|
217
|
+
const result = await dispatcher.parseComposite(
|
|
218
|
+
'// this is a comment\n-- another comment\n# hash comment\nselect name'
|
|
219
|
+
);
|
|
220
|
+
expect(result.statements).toHaveLength(1);
|
|
221
|
+
expect(result.statements[0].line).toBe(4);
|
|
222
|
+
expect(result.errors).toHaveLength(0);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('collects unrecognized lines as errors', async () => {
|
|
226
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
227
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
228
|
+
|
|
229
|
+
const result = await dispatcher.parseComposite('select name\nunknown command\nselect id');
|
|
230
|
+
expect(result.statements).toHaveLength(2);
|
|
231
|
+
expect(result.errors).toHaveLength(1);
|
|
232
|
+
expect(result.errors[0].line).toBe(2);
|
|
233
|
+
expect(result.errors[0].input).toBe('unknown command');
|
|
234
|
+
expect(result.errors[0].message).toBe('No domain matched this input');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('handles fully empty input', async () => {
|
|
238
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
239
|
+
const result = await dispatcher.parseComposite('');
|
|
240
|
+
expect(result.statements).toHaveLength(0);
|
|
241
|
+
expect(result.errors).toHaveLength(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('trims line whitespace before parsing', async () => {
|
|
245
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
246
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
247
|
+
|
|
248
|
+
const result = await dispatcher.parseComposite(' select name ');
|
|
249
|
+
expect(result.statements).toHaveLength(1);
|
|
250
|
+
expect(result.statements[0].input).toBe('select name');
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
// compile()
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
describe('compile', () => {
|
|
259
|
+
it('compiles using auto-detected domain', async () => {
|
|
260
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
261
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
262
|
+
|
|
263
|
+
const result = await dispatcher.compile('select name from users');
|
|
264
|
+
expect(result).not.toBeNull();
|
|
265
|
+
expect(result!.ok).toBe(true);
|
|
266
|
+
expect(result!.domain).toBe('sql');
|
|
267
|
+
expect(result!.code).toBe('compiled:select(select name from users)');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('returns null when no domain matches', async () => {
|
|
271
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
272
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
273
|
+
|
|
274
|
+
const result = await dispatcher.compile('unknown input');
|
|
275
|
+
expect(result).toBeNull();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// validate()
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
describe('validate', () => {
|
|
284
|
+
it('validates using auto-detected domain', async () => {
|
|
285
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
286
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
287
|
+
|
|
288
|
+
const result = await dispatcher.validate('select name from users');
|
|
289
|
+
expect(result.valid).toBe(true);
|
|
290
|
+
expect(result.domain).toBe('sql');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('returns invalid when no domain matches', async () => {
|
|
294
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
295
|
+
const dispatcher = new CrossDomainDispatcher(registry);
|
|
296
|
+
|
|
297
|
+
const result = await dispatcher.validate('unknown input');
|
|
298
|
+
expect(result.valid).toBe(false);
|
|
299
|
+
expect(result.domain).toBeUndefined();
|
|
300
|
+
expect(result.errors).toContain('No domain matched this input');
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// getDSLForDomain (DomainRegistry extension)
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
describe('DomainRegistry.getDSLForDomain', () => {
|
|
309
|
+
it('returns DSL for registered domain', async () => {
|
|
310
|
+
registry.register(createDescriptor('sql', 'select'));
|
|
311
|
+
const dsl = await registry.getDSLForDomain('sql');
|
|
312
|
+
expect(dsl).not.toBeNull();
|
|
313
|
+
expect(dsl!.getSupportedLanguages()).toContain('en');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('returns null for unregistered domain', async () => {
|
|
317
|
+
const dsl = await registry.getDSLForDomain('unknown');
|
|
318
|
+
expect(dsl).toBeNull();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('caches DSL instance across calls', async () => {
|
|
322
|
+
let callCount = 0;
|
|
323
|
+
registry.register({
|
|
324
|
+
...createDescriptor('sql', 'select'),
|
|
325
|
+
getDSL: () => {
|
|
326
|
+
callCount++;
|
|
327
|
+
return createMockDSL('select');
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
await registry.getDSLForDomain('sql');
|
|
332
|
+
await registry.getDSLForDomain('sql');
|
|
333
|
+
expect(callCount).toBe(1);
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
});
|