@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,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test utilities for @lokascript/framework
|
|
3
|
+
* Provides mock factories and assertion helpers for testing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { expect } from 'vitest';
|
|
7
|
+
import type {
|
|
8
|
+
LanguageTokenizer,
|
|
9
|
+
LanguageToken,
|
|
10
|
+
TokenStream,
|
|
11
|
+
LanguagePattern,
|
|
12
|
+
SemanticNode,
|
|
13
|
+
CommandSemanticNode,
|
|
14
|
+
SemanticValue,
|
|
15
|
+
PatternMatchResult,
|
|
16
|
+
} from '../core/types';
|
|
17
|
+
import type { CommandSchema, RoleSpec } from '../schema';
|
|
18
|
+
import type { PatternGenLanguageProfile } from '../generation/pattern-generator';
|
|
19
|
+
import type { LanguageConfig, MultilingualDSL } from '../api';
|
|
20
|
+
import { TokenStreamImpl, createToken, createPosition } from '../core/tokenization';
|
|
21
|
+
import { createMultilingualDSL } from '../api';
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// Mock Factories
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a mock tokenizer for testing
|
|
29
|
+
*/
|
|
30
|
+
export function createMockTokenizer(overrides?: Partial<LanguageTokenizer>): LanguageTokenizer {
|
|
31
|
+
return {
|
|
32
|
+
language: 'test',
|
|
33
|
+
direction: 'ltr',
|
|
34
|
+
tokenize(input: string): TokenStream {
|
|
35
|
+
// Simple whitespace tokenization for testing
|
|
36
|
+
const tokens: LanguageToken[] = input
|
|
37
|
+
.split(/\s+/)
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map((value, index) =>
|
|
40
|
+
createToken({
|
|
41
|
+
kind: 'identifier',
|
|
42
|
+
value,
|
|
43
|
+
position: createPosition(index * value.length, (index + 1) * value.length),
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
return new TokenStreamImpl(tokens);
|
|
47
|
+
},
|
|
48
|
+
classifyToken(token: string): 'keyword' | 'identifier' | 'literal' | 'operator' {
|
|
49
|
+
return 'identifier';
|
|
50
|
+
},
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a mock token stream from an array of tokens
|
|
57
|
+
*/
|
|
58
|
+
export function createMockTokenStream(tokens: LanguageToken[]): TokenStream {
|
|
59
|
+
return new TokenStreamImpl(tokens);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create a mock language pattern for testing
|
|
64
|
+
*/
|
|
65
|
+
export function createMockPattern(overrides?: Partial<LanguagePattern>): LanguagePattern {
|
|
66
|
+
return {
|
|
67
|
+
id: 'test-pattern',
|
|
68
|
+
command: 'test',
|
|
69
|
+
language: 'en',
|
|
70
|
+
priority: 0,
|
|
71
|
+
template: {
|
|
72
|
+
format: 'test pattern',
|
|
73
|
+
tokens: [],
|
|
74
|
+
},
|
|
75
|
+
extraction: {},
|
|
76
|
+
...overrides,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a mock language profile for pattern generation
|
|
82
|
+
*/
|
|
83
|
+
export function createMockProfile(
|
|
84
|
+
code: string,
|
|
85
|
+
wordOrder: 'SVO' | 'SOV' | 'VSO' = 'SVO'
|
|
86
|
+
): PatternGenLanguageProfile {
|
|
87
|
+
return {
|
|
88
|
+
code,
|
|
89
|
+
wordOrder,
|
|
90
|
+
keywords: {},
|
|
91
|
+
roleMarkers: {},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Create a test DSL with provided schemas and languages
|
|
97
|
+
*/
|
|
98
|
+
export function createTestDSL(
|
|
99
|
+
schemas: CommandSchema[],
|
|
100
|
+
languages: LanguageConfig[]
|
|
101
|
+
): MultilingualDSL {
|
|
102
|
+
return createMultilingualDSL({
|
|
103
|
+
name: 'Test DSL',
|
|
104
|
+
schemas,
|
|
105
|
+
languages,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// Assertion Helpers
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Assert that a token stream matches expected token values
|
|
115
|
+
*/
|
|
116
|
+
export function expectTokenSequence(tokens: TokenStream, expected: string[]): void {
|
|
117
|
+
const actual: string[] = [];
|
|
118
|
+
while (!tokens.eof()) {
|
|
119
|
+
const token = tokens.consume();
|
|
120
|
+
if (token) {
|
|
121
|
+
actual.push(token.value);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
expect(actual).toEqual(expected);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Assert that a semantic node has the expected action and roles
|
|
129
|
+
*/
|
|
130
|
+
export function expectSemanticNode(
|
|
131
|
+
node: SemanticNode,
|
|
132
|
+
action: string,
|
|
133
|
+
roles: Record<string, any>
|
|
134
|
+
): void {
|
|
135
|
+
expect(node.kind).toBe('command');
|
|
136
|
+
expect(node.action).toBe(action);
|
|
137
|
+
|
|
138
|
+
const commandNode = node as CommandSemanticNode;
|
|
139
|
+
|
|
140
|
+
// Check each expected role
|
|
141
|
+
for (const [role, expectedValue] of Object.entries(roles)) {
|
|
142
|
+
const actualValue = commandNode.roles.get(role as any);
|
|
143
|
+
expect(actualValue).toBeDefined();
|
|
144
|
+
|
|
145
|
+
if (typeof expectedValue === 'string') {
|
|
146
|
+
// Simple value comparison
|
|
147
|
+
if (actualValue && 'value' in actualValue) {
|
|
148
|
+
expect(actualValue.value).toBe(expectedValue);
|
|
149
|
+
} else if (actualValue && 'raw' in actualValue) {
|
|
150
|
+
expect(actualValue.raw).toBe(expectedValue);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
// Object comparison for complex semantic values
|
|
154
|
+
expect(actualValue).toEqual(expectedValue);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Assert that a pattern match result has confidence within a range
|
|
161
|
+
*/
|
|
162
|
+
export function expectConfidenceRange(
|
|
163
|
+
result: PatternMatchResult | null,
|
|
164
|
+
min: number,
|
|
165
|
+
max: number
|
|
166
|
+
): void {
|
|
167
|
+
expect(result).not.toBeNull();
|
|
168
|
+
if (result) {
|
|
169
|
+
expect(result.confidence).toBeGreaterThanOrEqual(min);
|
|
170
|
+
expect(result.confidence).toBeLessThanOrEqual(max);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Assert that a value is a literal semantic value
|
|
176
|
+
*/
|
|
177
|
+
export function expectLiteralValue(value: SemanticValue | undefined, expectedValue: any): void {
|
|
178
|
+
expect(value).toBeDefined();
|
|
179
|
+
expect(value).toHaveProperty('type', 'literal');
|
|
180
|
+
expect(value).toHaveProperty('value', expectedValue);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Assert that a value is a selector semantic value
|
|
185
|
+
*/
|
|
186
|
+
export function expectSelectorValue(
|
|
187
|
+
value: SemanticValue | undefined,
|
|
188
|
+
expectedSelector: string,
|
|
189
|
+
expectedKind?: 'id' | 'class' | 'attribute' | 'element'
|
|
190
|
+
): void {
|
|
191
|
+
expect(value).toBeDefined();
|
|
192
|
+
expect(value).toHaveProperty('type', 'selector');
|
|
193
|
+
expect(value).toHaveProperty('value', expectedSelector);
|
|
194
|
+
if (expectedKind) {
|
|
195
|
+
expect(value).toHaveProperty('selectorKind', expectedKind);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Assert that a value is an expression semantic value
|
|
201
|
+
*/
|
|
202
|
+
export function expectExpressionValue(value: SemanticValue | undefined, expectedRaw: string): void {
|
|
203
|
+
expect(value).toBeDefined();
|
|
204
|
+
expect(value).toHaveProperty('type', 'expression');
|
|
205
|
+
expect(value).toHaveProperty('raw', expectedRaw);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Assert that a value is a reference semantic value
|
|
210
|
+
*/
|
|
211
|
+
export function expectReferenceValue(value: SemanticValue | undefined, expectedRef: string): void {
|
|
212
|
+
expect(value).toBeDefined();
|
|
213
|
+
expect(value).toHaveProperty('type', 'reference');
|
|
214
|
+
expect(value).toHaveProperty('value', expectedRef);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// =============================================================================
|
|
218
|
+
// Type Guards for Testing
|
|
219
|
+
// =============================================================================
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Type guard to check if a value is a literal semantic value
|
|
223
|
+
*/
|
|
224
|
+
export function isLiteralValue(value: SemanticValue): value is { type: 'literal'; value: any } {
|
|
225
|
+
return 'type' in value && value.type === 'literal';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Type guard to check if a value is a selector semantic value
|
|
230
|
+
*/
|
|
231
|
+
export function isSelectorValue(
|
|
232
|
+
value: SemanticValue
|
|
233
|
+
): value is { type: 'selector'; value: string; selectorKind?: string } {
|
|
234
|
+
return 'type' in value && value.type === 'selector';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Type guard to check if a value is an expression semantic value
|
|
239
|
+
*/
|
|
240
|
+
export function isExpressionValue(
|
|
241
|
+
value: SemanticValue
|
|
242
|
+
): value is { type: 'expression'; raw: string } {
|
|
243
|
+
return 'type' in value && value.type === 'expression';
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Type guard to check if a value is a reference semantic value
|
|
248
|
+
*/
|
|
249
|
+
export function isReferenceValue(
|
|
250
|
+
value: SemanticValue
|
|
251
|
+
): value is { type: 'reference'; value: string } {
|
|
252
|
+
return 'type' in value && value.type === 'reference';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Type guard to check if a node is a command node
|
|
257
|
+
*/
|
|
258
|
+
export function isCommandNode(node: SemanticNode): node is CommandSemanticNode {
|
|
259
|
+
return node.kind === 'command';
|
|
260
|
+
}
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { DomainAwareScanner } from './domain-scanner';
|
|
3
|
+
import { AOTOrchestrator } from './aot-orchestrator';
|
|
4
|
+
import type { DomainScanConfig, ExtractedSnippet } from './types';
|
|
5
|
+
import type { DomainCompilationBackend } from './aot-orchestrator';
|
|
6
|
+
import type { MultilingualDSL, CodeGenerator } from '../api/create-dsl';
|
|
7
|
+
import type { SemanticNode } from '../core/types';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Test Helpers
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
function createMockDSL(keyword: string): MultilingualDSL {
|
|
14
|
+
return {
|
|
15
|
+
parse(input: string, language: string): SemanticNode {
|
|
16
|
+
return {
|
|
17
|
+
kind: 'command',
|
|
18
|
+
action: keyword,
|
|
19
|
+
roles: new Map([['raw', { type: 'expression' as const, raw: input }]]),
|
|
20
|
+
metadata: { sourceLanguage: language },
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
parseWithConfidence(input: string, language: string) {
|
|
24
|
+
return { node: this.parse(input, language), confidence: 0.9 };
|
|
25
|
+
},
|
|
26
|
+
validate(input: string, language: string) {
|
|
27
|
+
try {
|
|
28
|
+
this.parse(input, language);
|
|
29
|
+
return { valid: true };
|
|
30
|
+
} catch {
|
|
31
|
+
return { valid: false, errors: ['Parse failed'] };
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
compile(input: string, language: string) {
|
|
35
|
+
const node = this.parse(input, language);
|
|
36
|
+
return { ok: true, code: `${keyword}_output(${input})`, node };
|
|
37
|
+
},
|
|
38
|
+
translate(input: string, _from: string, to: string) {
|
|
39
|
+
return `[${to}] ${input}`;
|
|
40
|
+
},
|
|
41
|
+
getSupportedLanguages() {
|
|
42
|
+
return ['en'];
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createMockCodeGenerator(prefix: string): CodeGenerator {
|
|
48
|
+
return {
|
|
49
|
+
generate(node: SemanticNode): string {
|
|
50
|
+
return `${prefix}: ${node.action}`;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function createBackend(
|
|
56
|
+
domain: string,
|
|
57
|
+
attributes: string[],
|
|
58
|
+
scriptTypes?: string[]
|
|
59
|
+
): DomainCompilationBackend {
|
|
60
|
+
return {
|
|
61
|
+
domain,
|
|
62
|
+
dsl: createMockDSL(domain),
|
|
63
|
+
codeGenerator: createMockCodeGenerator(domain),
|
|
64
|
+
scanConfig: {
|
|
65
|
+
domain,
|
|
66
|
+
attributes,
|
|
67
|
+
scriptTypes,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// DomainAwareScanner Tests
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
describe('DomainAwareScanner', () => {
|
|
77
|
+
it('extracts snippets from custom attributes', () => {
|
|
78
|
+
const scanner = new DomainAwareScanner([{ domain: 'sql', attributes: ['data-sql'] }]);
|
|
79
|
+
|
|
80
|
+
const html = `<button data-sql="select name from users">Load</button>`;
|
|
81
|
+
const snippets = scanner.extract(html, 'test.html');
|
|
82
|
+
|
|
83
|
+
expect(snippets).toHaveLength(1);
|
|
84
|
+
expect(snippets[0].domain).toBe('sql');
|
|
85
|
+
expect(snippets[0].code).toBe('select name from users');
|
|
86
|
+
expect(snippets[0].file).toBe('test.html');
|
|
87
|
+
expect(snippets[0].language).toBe('en');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('extracts from single-quoted attributes', () => {
|
|
91
|
+
const scanner = new DomainAwareScanner([{ domain: 'sql', attributes: ['data-sql'] }]);
|
|
92
|
+
|
|
93
|
+
const html = `<button data-sql='select id'>Load</button>`;
|
|
94
|
+
const snippets = scanner.extract(html, 'test.html');
|
|
95
|
+
|
|
96
|
+
expect(snippets).toHaveLength(1);
|
|
97
|
+
expect(snippets[0].code).toBe('select id');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('handles multiple domains in the same file', () => {
|
|
101
|
+
const scanner = new DomainAwareScanner([
|
|
102
|
+
{ domain: 'sql', attributes: ['data-sql'] },
|
|
103
|
+
{ domain: 'bdd', attributes: ['data-bdd'] },
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const html = `
|
|
107
|
+
<button data-sql="select name from users">SQL</button>
|
|
108
|
+
<div data-bdd="given page http://example.com">BDD</div>
|
|
109
|
+
`;
|
|
110
|
+
const snippets = scanner.extract(html, 'test.html');
|
|
111
|
+
|
|
112
|
+
expect(snippets).toHaveLength(2);
|
|
113
|
+
expect(snippets[0].domain).toBe('sql');
|
|
114
|
+
expect(snippets[1].domain).toBe('bdd');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('extracts from script tags', () => {
|
|
118
|
+
const scanner = new DomainAwareScanner([
|
|
119
|
+
{ domain: 'bdd', attributes: [], scriptTypes: ['text/behaviorspec'] },
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const html = `<script type="text/behaviorspec">given page http://example.com</script>`;
|
|
123
|
+
const snippets = scanner.extract(html, 'test.html');
|
|
124
|
+
|
|
125
|
+
expect(snippets).toHaveLength(1);
|
|
126
|
+
expect(snippets[0].domain).toBe('bdd');
|
|
127
|
+
expect(snippets[0].code).toBe('given page http://example.com');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('picks up lang attribute from element', () => {
|
|
131
|
+
const scanner = new DomainAwareScanner([{ domain: 'sql', attributes: ['data-sql'] }]);
|
|
132
|
+
|
|
133
|
+
const html = `<button lang="ja" data-sql="select name">Load</button>`;
|
|
134
|
+
const snippets = scanner.extract(html, 'test.html');
|
|
135
|
+
|
|
136
|
+
expect(snippets).toHaveLength(1);
|
|
137
|
+
expect(snippets[0].language).toBe('ja');
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('uses defaultLanguage when no lang attribute', () => {
|
|
141
|
+
const scanner = new DomainAwareScanner([
|
|
142
|
+
{ domain: 'sql', attributes: ['data-sql'], defaultLanguage: 'es' },
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
const html = `<button data-sql="select name">Load</button>`;
|
|
146
|
+
const snippets = scanner.extract(html, 'test.html');
|
|
147
|
+
|
|
148
|
+
expect(snippets).toHaveLength(1);
|
|
149
|
+
expect(snippets[0].language).toBe('es');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('picks up element ID', () => {
|
|
153
|
+
const scanner = new DomainAwareScanner([{ domain: 'sql', attributes: ['data-sql'] }]);
|
|
154
|
+
|
|
155
|
+
const html = `<button id="load-btn" data-sql="select name">Load</button>`;
|
|
156
|
+
const snippets = scanner.extract(html, 'test.html');
|
|
157
|
+
|
|
158
|
+
expect(snippets).toHaveLength(1);
|
|
159
|
+
expect(snippets[0].elementId).toBe('load-btn');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('skips empty attribute values', () => {
|
|
163
|
+
const scanner = new DomainAwareScanner([{ domain: 'sql', attributes: ['data-sql'] }]);
|
|
164
|
+
|
|
165
|
+
const html = `<button data-sql="">Load</button>`;
|
|
166
|
+
const snippets = scanner.extract(html, 'test.html');
|
|
167
|
+
|
|
168
|
+
expect(snippets).toHaveLength(0);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('returns empty array for no matches', () => {
|
|
172
|
+
const scanner = new DomainAwareScanner([{ domain: 'sql', attributes: ['data-sql'] }]);
|
|
173
|
+
|
|
174
|
+
const html = `<button class="btn">Load</button>`;
|
|
175
|
+
const snippets = scanner.extract(html, 'test.html');
|
|
176
|
+
|
|
177
|
+
expect(snippets).toHaveLength(0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('extracts from multiple files', async () => {
|
|
181
|
+
const scanner = new DomainAwareScanner([{ domain: 'sql', attributes: ['data-sql'] }]);
|
|
182
|
+
|
|
183
|
+
const files: Record<string, string> = {
|
|
184
|
+
'a.html': `<button data-sql="select name">A</button>`,
|
|
185
|
+
'b.html': `<button data-sql="select id">B</button>`,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const snippets = await scanner.extractFromFiles(Object.keys(files), async path => files[path]);
|
|
189
|
+
|
|
190
|
+
expect(snippets).toHaveLength(2);
|
|
191
|
+
expect(snippets[0].file).toBe('a.html');
|
|
192
|
+
expect(snippets[1].file).toBe('b.html');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('skips unreadable files gracefully', async () => {
|
|
196
|
+
const scanner = new DomainAwareScanner([{ domain: 'sql', attributes: ['data-sql'] }]);
|
|
197
|
+
|
|
198
|
+
const snippets = await scanner.extractFromFiles(['good.html', 'bad.html'], async path => {
|
|
199
|
+
if (path === 'bad.html') throw new Error('Cannot read');
|
|
200
|
+
return `<button data-sql="select name">A</button>`;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(snippets).toHaveLength(1);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// =============================================================================
|
|
208
|
+
// AOTOrchestrator Tests
|
|
209
|
+
// =============================================================================
|
|
210
|
+
|
|
211
|
+
describe('AOTOrchestrator', () => {
|
|
212
|
+
it('compiles a single snippet', () => {
|
|
213
|
+
const orchestrator = new AOTOrchestrator();
|
|
214
|
+
orchestrator.registerBackend(createBackend('sql', ['data-sql']));
|
|
215
|
+
|
|
216
|
+
const snippet: ExtractedSnippet = {
|
|
217
|
+
domain: 'sql',
|
|
218
|
+
code: 'select name from users',
|
|
219
|
+
language: 'en',
|
|
220
|
+
file: 'test.html',
|
|
221
|
+
line: 1,
|
|
222
|
+
column: 1,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const result = orchestrator.compileSnippet(snippet);
|
|
226
|
+
expect('error' in result).toBe(false);
|
|
227
|
+
if (!('error' in result)) {
|
|
228
|
+
expect(result.domain).toBe('sql');
|
|
229
|
+
expect(result.compiled).toBe('sql_output(select name from users)');
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('returns error for unknown domain', () => {
|
|
234
|
+
const orchestrator = new AOTOrchestrator();
|
|
235
|
+
|
|
236
|
+
const snippet: ExtractedSnippet = {
|
|
237
|
+
domain: 'unknown',
|
|
238
|
+
code: 'test',
|
|
239
|
+
language: 'en',
|
|
240
|
+
file: 'test.html',
|
|
241
|
+
line: 1,
|
|
242
|
+
column: 1,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const result = orchestrator.compileSnippet(snippet);
|
|
246
|
+
expect('error' in result).toBe(true);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('throws on duplicate backend registration', () => {
|
|
250
|
+
const orchestrator = new AOTOrchestrator();
|
|
251
|
+
orchestrator.registerBackend(createBackend('sql', ['data-sql']));
|
|
252
|
+
expect(() => orchestrator.registerBackend(createBackend('sql', ['data-sql2']))).toThrow(
|
|
253
|
+
'Backend already registered for domain: sql'
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('compiles files with multiple domains', async () => {
|
|
258
|
+
const orchestrator = new AOTOrchestrator();
|
|
259
|
+
orchestrator.registerBackend(createBackend('sql', ['data-sql']));
|
|
260
|
+
orchestrator.registerBackend(createBackend('bdd', ['data-bdd']));
|
|
261
|
+
|
|
262
|
+
const files: Record<string, string> = {
|
|
263
|
+
'index.html': `
|
|
264
|
+
<button data-sql="select name from users">SQL</button>
|
|
265
|
+
<div data-bdd="given page http://example.com">BDD</div>
|
|
266
|
+
`,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const result = await orchestrator.compileFiles(Object.keys(files), async path => files[path]);
|
|
270
|
+
|
|
271
|
+
expect(result.compiled).toHaveLength(2);
|
|
272
|
+
expect(result.errors).toHaveLength(0);
|
|
273
|
+
expect(result.stats.totalSnippets).toBe(2);
|
|
274
|
+
expect(result.stats.compiledCount).toBe(2);
|
|
275
|
+
expect(result.stats.errorCount).toBe(0);
|
|
276
|
+
expect(result.stats.domainBreakdown).toEqual({ sql: 1, bdd: 1 });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('collects errors and continues by default', async () => {
|
|
280
|
+
const orchestrator = new AOTOrchestrator();
|
|
281
|
+
|
|
282
|
+
// Create a backend that fails compilation
|
|
283
|
+
const failingDSL = createMockDSL('sql');
|
|
284
|
+
failingDSL.compile = () => ({ ok: false, errors: ['Syntax error'] });
|
|
285
|
+
|
|
286
|
+
orchestrator.registerBackend({
|
|
287
|
+
domain: 'sql',
|
|
288
|
+
dsl: failingDSL,
|
|
289
|
+
codeGenerator: createMockCodeGenerator('sql'),
|
|
290
|
+
scanConfig: { domain: 'sql', attributes: ['data-sql'] },
|
|
291
|
+
});
|
|
292
|
+
orchestrator.registerBackend(createBackend('bdd', ['data-bdd']));
|
|
293
|
+
|
|
294
|
+
const files: Record<string, string> = {
|
|
295
|
+
'test.html': `
|
|
296
|
+
<button data-sql="bad query">SQL</button>
|
|
297
|
+
<div data-bdd="given page http://example.com">BDD</div>
|
|
298
|
+
`,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const result = await orchestrator.compileFiles(Object.keys(files), async path => files[path]);
|
|
302
|
+
|
|
303
|
+
expect(result.compiled).toHaveLength(1);
|
|
304
|
+
expect(result.errors).toHaveLength(1);
|
|
305
|
+
expect(result.errors[0].domain).toBe('sql');
|
|
306
|
+
expect(result.errors[0].message).toBe('Syntax error');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('handles empty file list', async () => {
|
|
310
|
+
const orchestrator = new AOTOrchestrator();
|
|
311
|
+
orchestrator.registerBackend(createBackend('sql', ['data-sql']));
|
|
312
|
+
|
|
313
|
+
const result = await orchestrator.compileFiles([], async () => '');
|
|
314
|
+
|
|
315
|
+
expect(result.compiled).toHaveLength(0);
|
|
316
|
+
expect(result.errors).toHaveLength(0);
|
|
317
|
+
expect(result.stats.totalSnippets).toBe(0);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// generateOutput()
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
describe('generateOutput', () => {
|
|
325
|
+
function createBatchResult(): import('./types').AOTBatchResult {
|
|
326
|
+
return {
|
|
327
|
+
compiled: [
|
|
328
|
+
{
|
|
329
|
+
domain: 'sql',
|
|
330
|
+
source: 'select name',
|
|
331
|
+
compiled: 'SELECT name',
|
|
332
|
+
language: 'en',
|
|
333
|
+
file: 'a.html',
|
|
334
|
+
line: 1,
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
domain: 'sql',
|
|
338
|
+
source: 'select id',
|
|
339
|
+
compiled: 'SELECT id',
|
|
340
|
+
language: 'en',
|
|
341
|
+
file: 'a.html',
|
|
342
|
+
line: 5,
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
domain: 'bdd',
|
|
346
|
+
source: 'given page',
|
|
347
|
+
compiled: 'test("given page")',
|
|
348
|
+
language: 'en',
|
|
349
|
+
file: 'b.html',
|
|
350
|
+
line: 1,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
errors: [],
|
|
354
|
+
stats: {
|
|
355
|
+
totalSnippets: 3,
|
|
356
|
+
compiledCount: 3,
|
|
357
|
+
errorCount: 0,
|
|
358
|
+
domainBreakdown: { sql: 2, bdd: 1 },
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
it('generates ESM output grouped by domain', () => {
|
|
364
|
+
const orchestrator = new AOTOrchestrator();
|
|
365
|
+
const output = orchestrator.generateOutput(createBatchResult());
|
|
366
|
+
|
|
367
|
+
expect(output).toContain('=== Domain: sql ===');
|
|
368
|
+
expect(output).toContain('=== Domain: bdd ===');
|
|
369
|
+
expect(output).toContain('SELECT name');
|
|
370
|
+
expect(output).toContain('test("given page")');
|
|
371
|
+
expect(output).not.toContain("'use strict'");
|
|
372
|
+
expect(output).not.toContain('(function()');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('generates CJS output', () => {
|
|
376
|
+
const orchestrator = new AOTOrchestrator();
|
|
377
|
+
const output = orchestrator.generateOutput(createBatchResult(), { format: 'cjs' });
|
|
378
|
+
|
|
379
|
+
expect(output).toContain("'use strict'");
|
|
380
|
+
expect(output).toContain('SELECT name');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('generates IIFE output', () => {
|
|
384
|
+
const orchestrator = new AOTOrchestrator();
|
|
385
|
+
const output = orchestrator.generateOutput(createBatchResult(), { format: 'iife' });
|
|
386
|
+
|
|
387
|
+
expect(output).toContain('(function()');
|
|
388
|
+
expect(output).toContain('})();');
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('omits comments when includeComments is false', () => {
|
|
392
|
+
const orchestrator = new AOTOrchestrator();
|
|
393
|
+
const output = orchestrator.generateOutput(createBatchResult(), {
|
|
394
|
+
includeComments: false,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
expect(output).not.toContain('===');
|
|
398
|
+
expect(output).not.toContain('Source:');
|
|
399
|
+
expect(output).toContain('SELECT name');
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('flattens output when groupByDomain is false', () => {
|
|
403
|
+
const orchestrator = new AOTOrchestrator();
|
|
404
|
+
const output = orchestrator.generateOutput(createBatchResult(), {
|
|
405
|
+
groupByDomain: false,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(output).not.toContain('=== Domain:');
|
|
409
|
+
expect(output).toContain('[sql]');
|
|
410
|
+
expect(output).toContain('[bdd]');
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
});
|