@lokascript/framework 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +142 -0
  3. package/dist/aot/aot-orchestrator.d.ts +75 -0
  4. package/dist/aot/aot-orchestrator.d.ts.map +1 -0
  5. package/dist/aot/domain-scanner.d.ts +27 -0
  6. package/dist/aot/domain-scanner.d.ts.map +1 -0
  7. package/dist/aot/index.d.ts +8 -0
  8. package/dist/aot/index.d.ts.map +1 -0
  9. package/dist/aot/types.d.ts +103 -0
  10. package/dist/aot/types.d.ts.map +1 -0
  11. package/dist/api/create-dsl.d.ts +91 -0
  12. package/dist/api/create-dsl.d.ts.map +1 -0
  13. package/dist/api/dispatcher.d.ts +108 -0
  14. package/dist/api/dispatcher.d.ts.map +1 -0
  15. package/dist/api/domain-registry.d.ts +152 -0
  16. package/dist/api/domain-registry.d.ts.map +1 -0
  17. package/dist/api/index.d.ts +7 -0
  18. package/dist/api/index.d.ts.map +1 -0
  19. package/dist/api/index.js +2082 -0
  20. package/dist/api/index.js.map +1 -0
  21. package/dist/core/index.d.ts +7 -0
  22. package/dist/core/index.d.ts.map +1 -0
  23. package/dist/core/index.js +2674 -0
  24. package/dist/core/index.js.map +1 -0
  25. package/dist/core/logger.d.ts +32 -0
  26. package/dist/core/logger.d.ts.map +1 -0
  27. package/dist/core/pattern-matching/index.d.ts +6 -0
  28. package/dist/core/pattern-matching/index.d.ts.map +1 -0
  29. package/dist/core/pattern-matching/index.js +1239 -0
  30. package/dist/core/pattern-matching/index.js.map +1 -0
  31. package/dist/core/pattern-matching/pattern-matcher.d.ts +239 -0
  32. package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -0
  33. package/dist/core/pattern-matching/utils/index.d.ts +6 -0
  34. package/dist/core/pattern-matching/utils/index.d.ts.map +1 -0
  35. package/dist/core/pattern-matching/utils/possessive-keywords.d.ts +38 -0
  36. package/dist/core/pattern-matching/utils/possessive-keywords.d.ts.map +1 -0
  37. package/dist/core/pattern-matching/utils/type-validation.d.ts +63 -0
  38. package/dist/core/pattern-matching/utils/type-validation.d.ts.map +1 -0
  39. package/dist/core/tokenization/base-tokenizer.d.ts +344 -0
  40. package/dist/core/tokenization/base-tokenizer.d.ts.map +1 -0
  41. package/dist/core/tokenization/char-classifiers.d.ts +56 -0
  42. package/dist/core/tokenization/char-classifiers.d.ts.map +1 -0
  43. package/dist/core/tokenization/default-extractors.d.ts +48 -0
  44. package/dist/core/tokenization/default-extractors.d.ts.map +1 -0
  45. package/dist/core/tokenization/extractors/index.d.ts +9 -0
  46. package/dist/core/tokenization/extractors/index.d.ts.map +1 -0
  47. package/dist/core/tokenization/extractors/operator.d.ts +23 -0
  48. package/dist/core/tokenization/extractors/operator.d.ts.map +1 -0
  49. package/dist/core/tokenization/extractors/punctuation.d.ts +22 -0
  50. package/dist/core/tokenization/extractors/punctuation.d.ts.map +1 -0
  51. package/dist/core/tokenization/extractors.d.ts +61 -0
  52. package/dist/core/tokenization/extractors.d.ts.map +1 -0
  53. package/dist/core/tokenization/index.d.ts +11 -0
  54. package/dist/core/tokenization/index.d.ts.map +1 -0
  55. package/dist/core/tokenization/index.js +1345 -0
  56. package/dist/core/tokenization/index.js.map +1 -0
  57. package/dist/core/tokenization/morphology/index.d.ts +5 -0
  58. package/dist/core/tokenization/morphology/index.d.ts.map +1 -0
  59. package/dist/core/tokenization/morphology/types.d.ts +110 -0
  60. package/dist/core/tokenization/morphology/types.d.ts.map +1 -0
  61. package/dist/core/tokenization/token-utils.d.ts +111 -0
  62. package/dist/core/tokenization/token-utils.d.ts.map +1 -0
  63. package/dist/core/types.d.ts +382 -0
  64. package/dist/core/types.d.ts.map +1 -0
  65. package/dist/core/types.js +108 -0
  66. package/dist/core/types.js.map +1 -0
  67. package/dist/generation/diagnostics.d.ts +120 -0
  68. package/dist/generation/diagnostics.d.ts.map +1 -0
  69. package/dist/generation/index.d.ts +7 -0
  70. package/dist/generation/index.d.ts.map +1 -0
  71. package/dist/generation/index.js +339 -0
  72. package/dist/generation/index.js.map +1 -0
  73. package/dist/generation/pattern-generator.d.ts +48 -0
  74. package/dist/generation/pattern-generator.d.ts.map +1 -0
  75. package/dist/generation/renderer.d.ts +115 -0
  76. package/dist/generation/renderer.d.ts.map +1 -0
  77. package/dist/grammar/index.d.ts +10 -0
  78. package/dist/grammar/index.d.ts.map +1 -0
  79. package/dist/grammar/index.js +391 -0
  80. package/dist/grammar/index.js.map +1 -0
  81. package/dist/grammar/transformer.d.ts +56 -0
  82. package/dist/grammar/transformer.d.ts.map +1 -0
  83. package/dist/grammar/types.d.ts +236 -0
  84. package/dist/grammar/types.d.ts.map +1 -0
  85. package/dist/index.cjs +4454 -0
  86. package/dist/index.cjs.map +1 -0
  87. package/dist/index.d.ts +46 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +4336 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/interfaces/dictionary.d.ts +82 -0
  92. package/dist/interfaces/dictionary.d.ts.map +1 -0
  93. package/dist/interfaces/index.d.ts +10 -0
  94. package/dist/interfaces/index.d.ts.map +1 -0
  95. package/dist/interfaces/profile-provider.d.ts +67 -0
  96. package/dist/interfaces/profile-provider.d.ts.map +1 -0
  97. package/dist/interfaces/value-extractor.d.ts +168 -0
  98. package/dist/interfaces/value-extractor.d.ts.map +1 -0
  99. package/dist/multilingual/index.d.ts +8 -0
  100. package/dist/multilingual/index.d.ts.map +1 -0
  101. package/dist/multilingual/index.js +1 -0
  102. package/dist/multilingual/index.js.map +1 -0
  103. package/dist/parsing/index.d.ts +8 -0
  104. package/dist/parsing/index.d.ts.map +1 -0
  105. package/dist/parsing/index.js +1415 -0
  106. package/dist/parsing/index.js.map +1 -0
  107. package/dist/parsing/multi-statement.d.ts +265 -0
  108. package/dist/parsing/multi-statement.d.ts.map +1 -0
  109. package/dist/schema/command-schema.d.ts +78 -0
  110. package/dist/schema/command-schema.d.ts.map +1 -0
  111. package/dist/schema/index.d.ts +5 -0
  112. package/dist/schema/index.d.ts.map +1 -0
  113. package/dist/schema/index.js +25 -0
  114. package/dist/schema/index.js.map +1 -0
  115. package/dist/test-setup.d.ts +9 -0
  116. package/dist/test-setup.d.ts.map +1 -0
  117. package/dist/testing/index.d.ts +50 -0
  118. package/dist/testing/index.d.ts.map +1 -0
  119. package/dist/testing/index.js +16969 -0
  120. package/dist/testing/index.js.map +1 -0
  121. package/package.json +122 -0
  122. package/src/__test__/fixtures/sql-dsl.ts +232 -0
  123. package/src/__test__/sql-integration.test.ts +189 -0
  124. package/src/__test__/test-utils.ts +260 -0
  125. package/src/aot/aot-orchestrator.test.ts +413 -0
  126. package/src/aot/aot-orchestrator.ts +238 -0
  127. package/src/aot/domain-scanner.ts +178 -0
  128. package/src/aot/index.ts +8 -0
  129. package/src/aot/types.ts +124 -0
  130. package/src/api/create-dsl.ts +367 -0
  131. package/src/api/dispatcher.test.ts +336 -0
  132. package/src/api/dispatcher.ts +222 -0
  133. package/src/api/domain-registry.test.ts +336 -0
  134. package/src/api/domain-registry.ts +500 -0
  135. package/src/api/index.ts +7 -0
  136. package/src/core/index.ts +7 -0
  137. package/src/core/logger.ts +130 -0
  138. package/src/core/pattern-matching/index.ts +6 -0
  139. package/src/core/pattern-matching/pattern-matcher.test.ts +900 -0
  140. package/src/core/pattern-matching/pattern-matcher.ts +1548 -0
  141. package/src/core/pattern-matching/pattern-matcher.ts.backup +1267 -0
  142. package/src/core/pattern-matching/utils/index.ts +6 -0
  143. package/src/core/pattern-matching/utils/possessive-keywords.ts +55 -0
  144. package/src/core/pattern-matching/utils/type-validation.test.ts +316 -0
  145. package/src/core/pattern-matching/utils/type-validation.ts +134 -0
  146. package/src/core/tokenization/base-tokenizer.ts +916 -0
  147. package/src/core/tokenization/char-classifiers.ts +79 -0
  148. package/src/core/tokenization/create-simple-tokenizer.test.ts +260 -0
  149. package/src/core/tokenization/default-extractors.ts +69 -0
  150. package/src/core/tokenization/extractors/index.ts +9 -0
  151. package/src/core/tokenization/extractors/operator.ts +75 -0
  152. package/src/core/tokenization/extractors/punctuation.ts +39 -0
  153. package/src/core/tokenization/extractors.ts +452 -0
  154. package/src/core/tokenization/index.ts +11 -0
  155. package/src/core/tokenization/morphology/index.ts +5 -0
  156. package/src/core/tokenization/morphology/types.ts +211 -0
  157. package/src/core/tokenization/token-utils.ts +252 -0
  158. package/src/core/types.ts +589 -0
  159. package/src/generation/diagnostics.test.ts +171 -0
  160. package/src/generation/diagnostics.ts +239 -0
  161. package/src/generation/index.ts +7 -0
  162. package/src/generation/pattern-generator.test.ts +430 -0
  163. package/src/generation/pattern-generator.ts +315 -0
  164. package/src/generation/renderer.test.ts +266 -0
  165. package/src/generation/renderer.ts +244 -0
  166. package/src/grammar/index.ts +12 -0
  167. package/src/grammar/transformer.ts +159 -0
  168. package/src/grammar/types.ts +630 -0
  169. package/src/index.ts +157 -0
  170. package/src/interfaces/dictionary.ts +123 -0
  171. package/src/interfaces/index.ts +10 -0
  172. package/src/interfaces/profile-provider.ts +88 -0
  173. package/src/interfaces/value-extractor.ts +435 -0
  174. package/src/multilingual/index.ts +9 -0
  175. package/src/parsing/index.ts +27 -0
  176. package/src/parsing/multi-statement.test.ts +480 -0
  177. package/src/parsing/multi-statement.ts +648 -0
  178. package/src/schema/command-schema.ts +118 -0
  179. package/src/schema/index.ts +5 -0
  180. package/src/test-setup.ts +45 -0
  181. package/src/testing/index.ts +137 -0
@@ -0,0 +1,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
+ });