@lokascript/framework 2.0.0 → 2.2.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/dist/aot/index.d.ts +1 -0
- package/dist/aot/index.d.ts.map +1 -1
- package/dist/aot/registry-bridge.d.ts +27 -0
- package/dist/aot/registry-bridge.d.ts.map +1 -0
- package/dist/api/create-dsl.d.ts.map +1 -1
- package/dist/api/dispatcher.d.ts +22 -0
- package/dist/api/dispatcher.d.ts.map +1 -1
- package/dist/api/domain-registry.d.ts +41 -0
- package/dist/api/domain-registry.d.ts.map +1 -1
- package/dist/api/index.js +1438 -21
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.js +247 -10
- package/dist/core/index.js.map +1 -1
- package/dist/core/pattern-matching/index.js +74 -10
- package/dist/core/pattern-matching/index.js.map +1 -1
- package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
- package/dist/core/tokenization/index.js +133 -0
- package/dist/core/tokenization/index.js.map +1 -1
- package/dist/core/tokenization/morphology/base-normalizer.d.ts +94 -0
- package/dist/core/tokenization/morphology/base-normalizer.d.ts.map +1 -0
- package/dist/core/tokenization/morphology/index.d.ts +3 -1
- package/dist/core/tokenization/morphology/index.d.ts.map +1 -1
- package/dist/core/types.d.ts +81 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +40 -0
- package/dist/core/types.js.map +1 -1
- package/dist/feedback/confidence-gate.d.ts +28 -0
- package/dist/feedback/confidence-gate.d.ts.map +1 -0
- package/dist/feedback/feedback-formatter.d.ts +21 -0
- package/dist/feedback/feedback-formatter.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +11 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/pattern-tracker.d.ts +47 -0
- package/dist/feedback/pattern-tracker.d.ts.map +1 -0
- package/dist/feedback/types.d.ts +113 -0
- package/dist/feedback/types.d.ts.map +1 -0
- package/dist/generation/index.js +2 -1
- package/dist/generation/index.js.map +1 -1
- package/dist/index.cjs +3264 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3223 -132
- package/dist/index.js.map +1 -1
- package/dist/ir/explicit-parser.d.ts +86 -0
- package/dist/ir/explicit-parser.d.ts.map +1 -0
- package/dist/ir/explicit-renderer.d.ts +34 -0
- package/dist/ir/explicit-renderer.d.ts.map +1 -0
- package/dist/ir/from-interchange.d.ts +51 -0
- package/dist/ir/from-interchange.d.ts.map +1 -0
- package/dist/ir/index.d.ts +31 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +1934 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/json-schema.d.ts +40 -0
- package/dist/ir/json-schema.d.ts.map +1 -0
- package/dist/ir/protocol-json.d.ts +73 -0
- package/dist/ir/protocol-json.d.ts.map +1 -0
- package/dist/ir/references.d.ts +19 -0
- package/dist/ir/references.d.ts.map +1 -0
- package/dist/ir/to-runtime-ast.d.ts +53 -0
- package/dist/ir/to-runtime-ast.d.ts.map +1 -0
- package/dist/ir/types.d.ts +151 -0
- package/dist/ir/types.d.ts.map +1 -0
- package/dist/parsing/index.js +74 -10
- package/dist/parsing/index.js.map +1 -1
- package/dist/prompts/index.d.ts +9 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/prompt-generator.d.ts +49 -0
- package/dist/prompts/prompt-generator.d.ts.map +1 -0
- package/dist/prompts/types.d.ts +64 -0
- package/dist/prompts/types.d.ts.map +1 -0
- package/dist/schema/command-schema.d.ts +17 -0
- package/dist/schema/command-schema.d.ts.map +1 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/testing/index.js +8673 -8260
- package/dist/testing/index.js.map +1 -1
- package/dist/training/index.d.ts +11 -0
- package/dist/training/index.d.ts.map +1 -0
- package/dist/training/jsonl-writer.d.ts +42 -0
- package/dist/training/jsonl-writer.d.ts.map +1 -0
- package/dist/training/schema-synthesizer.d.ts +20 -0
- package/dist/training/schema-synthesizer.d.ts.map +1 -0
- package/dist/training/types.d.ts +73 -0
- package/dist/training/types.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/aot/index.ts +1 -0
- package/src/aot/registry-bridge.test.ts +104 -0
- package/src/aot/registry-bridge.ts +66 -0
- package/src/api/create-dsl.test.ts +306 -0
- package/src/api/create-dsl.ts +21 -0
- package/src/api/dispatcher.test.ts +66 -0
- package/src/api/dispatcher.ts +77 -9
- package/src/api/domain-registry.ts +120 -4
- package/src/core/pattern-matching/pattern-matcher.test.ts +131 -0
- package/src/core/pattern-matching/pattern-matcher.ts +102 -19
- package/src/core/tokenization/morphology/base-normalizer.ts +249 -0
- package/src/core/tokenization/morphology/index.ts +3 -1
- package/src/core/types.ts +147 -2
- package/src/feedback/confidence-gate.test.ts +111 -0
- package/src/feedback/confidence-gate.ts +70 -0
- package/src/feedback/feedback-formatter.test.ts +198 -0
- package/src/feedback/feedback-formatter.ts +298 -0
- package/src/feedback/index.ts +24 -0
- package/src/feedback/pattern-tracker.test.ts +198 -0
- package/src/feedback/pattern-tracker.ts +140 -0
- package/src/feedback/types.ts +149 -0
- package/src/generation/pattern-generator.ts +2 -2
- package/src/index.ts +29 -0
- package/src/integration/llm-lse-integration.test.ts +457 -0
- package/src/ir/conformance.test.ts +120 -0
- package/src/ir/explicit-parser.test.ts +612 -0
- package/src/ir/explicit-parser.ts +793 -0
- package/src/ir/explicit-renderer.test.ts +229 -0
- package/src/ir/explicit-renderer.ts +198 -0
- package/src/ir/from-interchange.test.ts +853 -0
- package/src/ir/from-interchange.ts +594 -0
- package/src/ir/index.ts +77 -0
- package/src/ir/json-schema.test.ts +281 -0
- package/src/ir/json-schema.ts +299 -0
- package/src/ir/protocol-json.test.ts +1187 -0
- package/src/ir/protocol-json.ts +751 -0
- package/src/ir/references.test.ts +38 -0
- package/src/ir/references.ts +33 -0
- package/src/ir/round-trip.test.ts +557 -0
- package/src/ir/to-runtime-ast.ts +203 -0
- package/src/ir/types.ts +191 -0
- package/src/prompts/index.ts +15 -0
- package/src/prompts/prompt-generator.test.ts +386 -0
- package/src/prompts/prompt-generator.ts +531 -0
- package/src/prompts/types.ts +89 -0
- package/src/schema/command-schema.ts +19 -0
- package/src/training/index.ts +13 -0
- package/src/training/jsonl-writer.test.ts +138 -0
- package/src/training/jsonl-writer.ts +67 -0
- package/src/training/schema-synthesizer.test.ts +250 -0
- package/src/training/schema-synthesizer.ts +306 -0
- package/src/training/types.ts +103 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-End Integration Tests — LLM ↔ LSE Pipeline
|
|
3
|
+
*
|
|
4
|
+
* Tests the full integration of system prompts, training data,
|
|
5
|
+
* feedback loops, and registry convenience methods.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
DomainRegistry,
|
|
11
|
+
generatePrompt,
|
|
12
|
+
synthesizeFromSchemas,
|
|
13
|
+
toJSONL,
|
|
14
|
+
parseJSONL,
|
|
15
|
+
buildFeedback,
|
|
16
|
+
needsDisambiguation,
|
|
17
|
+
buildDisambiguation,
|
|
18
|
+
PatternTracker,
|
|
19
|
+
parseExplicit,
|
|
20
|
+
renderExplicit,
|
|
21
|
+
semanticNodeToJSON,
|
|
22
|
+
createCommandNode,
|
|
23
|
+
createSelector,
|
|
24
|
+
createLiteral,
|
|
25
|
+
createExpression,
|
|
26
|
+
defineCommand,
|
|
27
|
+
defineRole,
|
|
28
|
+
} from '../index';
|
|
29
|
+
|
|
30
|
+
// =============================================================================
|
|
31
|
+
// Test schemas (minimal — for fast, self-contained tests)
|
|
32
|
+
// =============================================================================
|
|
33
|
+
|
|
34
|
+
const testSchemas = [
|
|
35
|
+
defineCommand({
|
|
36
|
+
action: 'fetch',
|
|
37
|
+
description: 'Fetch data from a URL',
|
|
38
|
+
category: 'source',
|
|
39
|
+
primaryRole: 'source',
|
|
40
|
+
roles: [
|
|
41
|
+
defineRole({
|
|
42
|
+
role: 'source',
|
|
43
|
+
description: 'URL to fetch from',
|
|
44
|
+
required: true,
|
|
45
|
+
expectedTypes: ['expression'],
|
|
46
|
+
}),
|
|
47
|
+
defineRole({
|
|
48
|
+
role: 'style',
|
|
49
|
+
description: 'Response format',
|
|
50
|
+
required: false,
|
|
51
|
+
expectedTypes: ['expression'],
|
|
52
|
+
}),
|
|
53
|
+
defineRole({
|
|
54
|
+
role: 'destination',
|
|
55
|
+
description: 'Target element',
|
|
56
|
+
required: false,
|
|
57
|
+
expectedTypes: ['selector'],
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
}),
|
|
61
|
+
defineCommand({
|
|
62
|
+
action: 'toggle',
|
|
63
|
+
description: 'Toggle a class on an element',
|
|
64
|
+
category: 'mutation',
|
|
65
|
+
primaryRole: 'patient',
|
|
66
|
+
roles: [
|
|
67
|
+
defineRole({
|
|
68
|
+
role: 'patient',
|
|
69
|
+
description: 'CSS class or attribute to toggle',
|
|
70
|
+
required: true,
|
|
71
|
+
expectedTypes: ['selector'],
|
|
72
|
+
}),
|
|
73
|
+
defineRole({
|
|
74
|
+
role: 'destination',
|
|
75
|
+
description: 'Target element',
|
|
76
|
+
required: false,
|
|
77
|
+
expectedTypes: ['selector'],
|
|
78
|
+
}),
|
|
79
|
+
],
|
|
80
|
+
}),
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// =============================================================================
|
|
84
|
+
// Pillar 1: System Prompt Generation → Parse round-trip
|
|
85
|
+
// =============================================================================
|
|
86
|
+
|
|
87
|
+
describe('Pillar 1: System Prompt Generation', () => {
|
|
88
|
+
it('generates prompt containing all commands', () => {
|
|
89
|
+
const prompt = generatePrompt({
|
|
90
|
+
domain: 'test',
|
|
91
|
+
description: 'Test domain',
|
|
92
|
+
schemas: testSchemas,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(prompt.text).toContain('fetch');
|
|
96
|
+
expect(prompt.text).toContain('toggle');
|
|
97
|
+
expect(prompt.metadata.commandCount).toBe(2);
|
|
98
|
+
expect(prompt.metadata.domain).toBe('test');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('prompt includes role documentation', () => {
|
|
102
|
+
const prompt = generatePrompt({
|
|
103
|
+
domain: 'test',
|
|
104
|
+
description: 'Test domain',
|
|
105
|
+
schemas: testSchemas,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(prompt.text).toContain('source');
|
|
109
|
+
expect(prompt.text).toContain('patient');
|
|
110
|
+
expect(prompt.text).toContain('URL to fetch from');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('prompt includes examples in bracket syntax', () => {
|
|
114
|
+
const prompt = generatePrompt({
|
|
115
|
+
domain: 'test',
|
|
116
|
+
description: 'Test domain',
|
|
117
|
+
schemas: testSchemas,
|
|
118
|
+
outputFormat: 'explicit',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(prompt.text).toContain('[fetch');
|
|
122
|
+
expect(prompt.text).toContain('[toggle');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('prompt includes JSON format examples', () => {
|
|
126
|
+
const prompt = generatePrompt({
|
|
127
|
+
domain: 'test',
|
|
128
|
+
description: 'Test domain',
|
|
129
|
+
schemas: testSchemas,
|
|
130
|
+
outputFormat: 'json',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(prompt.text).toContain('"action"');
|
|
134
|
+
expect(prompt.text).toContain('"roles"');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('respects token budget', () => {
|
|
138
|
+
const small = generatePrompt({
|
|
139
|
+
domain: 'test',
|
|
140
|
+
description: 'Test domain',
|
|
141
|
+
schemas: testSchemas,
|
|
142
|
+
maxTokens: 200,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const large = generatePrompt({
|
|
146
|
+
domain: 'test',
|
|
147
|
+
description: 'Test domain',
|
|
148
|
+
schemas: testSchemas,
|
|
149
|
+
maxTokens: 10000,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(small.text.length).toBeLessThan(large.text.length);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// =============================================================================
|
|
157
|
+
// Pillar 2: Training Data → JSONL Round-trip
|
|
158
|
+
// =============================================================================
|
|
159
|
+
|
|
160
|
+
describe('Pillar 2: Training Data Generation', () => {
|
|
161
|
+
it('synthesizes pairs from schemas', () => {
|
|
162
|
+
const result = synthesizeFromSchemas(testSchemas, { domain: 'test' });
|
|
163
|
+
|
|
164
|
+
expect(result.pairs.length).toBeGreaterThan(0);
|
|
165
|
+
expect(result.metadata.domain).toBe('test');
|
|
166
|
+
expect(result.metadata.commandCount).toBe(2);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('each pair has required fields', () => {
|
|
170
|
+
const result = synthesizeFromSchemas(testSchemas, { domain: 'test' });
|
|
171
|
+
|
|
172
|
+
for (const pair of result.pairs) {
|
|
173
|
+
expect(pair.domain).toBe('test');
|
|
174
|
+
expect(pair.action).toBeTruthy();
|
|
175
|
+
expect(pair.explicit).toContain('[');
|
|
176
|
+
expect(pair.natural).toBeTruthy();
|
|
177
|
+
expect(pair.quality).toBeGreaterThanOrEqual(0);
|
|
178
|
+
expect(pair.quality).toBeLessThanOrEqual(1);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('JSONL round-trips correctly', () => {
|
|
183
|
+
const result = synthesizeFromSchemas(testSchemas, { domain: 'test' });
|
|
184
|
+
const jsonl = toJSONL(result.pairs);
|
|
185
|
+
const parsed = parseJSONL(jsonl);
|
|
186
|
+
|
|
187
|
+
expect(parsed.length).toBe(result.pairs.length);
|
|
188
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
189
|
+
expect(parsed[i].metadata.action).toBe(result.pairs[i].action);
|
|
190
|
+
expect(parsed[i].completion).toBe(result.pairs[i].explicit);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('synthetic examples parse back at full confidence', () => {
|
|
195
|
+
const result = synthesizeFromSchemas(testSchemas, { domain: 'test' });
|
|
196
|
+
|
|
197
|
+
for (const pair of result.pairs) {
|
|
198
|
+
// Each explicit syntax should parse without error
|
|
199
|
+
const node = parseExplicit(pair.explicit);
|
|
200
|
+
expect(node.action).toBe(pair.action);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('respects maxPairsPerCommand', () => {
|
|
205
|
+
const result = synthesizeFromSchemas(testSchemas, {
|
|
206
|
+
domain: 'test',
|
|
207
|
+
maxPairsPerCommand: 2,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Count pairs per action
|
|
211
|
+
const counts = new Map<string, number>();
|
|
212
|
+
for (const pair of result.pairs) {
|
|
213
|
+
counts.set(pair.action, (counts.get(pair.action) || 0) + 1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
for (const count of counts.values()) {
|
|
217
|
+
expect(count).toBeLessThanOrEqual(2);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// =============================================================================
|
|
223
|
+
// Pillar 3: Feedback Loop
|
|
224
|
+
// =============================================================================
|
|
225
|
+
|
|
226
|
+
describe('Pillar 3: Feedback Loop', () => {
|
|
227
|
+
// --- 3A: Structured Error Feedback ---
|
|
228
|
+
|
|
229
|
+
it('invalid bracket syntax → structured feedback with hints', () => {
|
|
230
|
+
const feedback = buildFeedback(
|
|
231
|
+
'[fetch]',
|
|
232
|
+
'explicit',
|
|
233
|
+
[{ severity: 'error', message: 'Required role source missing', code: 'missing-role' }],
|
|
234
|
+
{ getSchema: a => testSchemas.find(s => s.action === a) },
|
|
235
|
+
'fetch'
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(feedback.accepted).toBe(false);
|
|
239
|
+
expect(feedback.diagnostics[0].fixType).toBe('missing_role');
|
|
240
|
+
expect(feedback.hints.length).toBeGreaterThan(0);
|
|
241
|
+
expect(feedback.schema?.requiredRoles).toContain('source');
|
|
242
|
+
expect(feedback.correctedExample?.explicit).toContain('[fetch');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('accepted input → accepted: true, no corrected example', () => {
|
|
246
|
+
const feedback = buildFeedback(
|
|
247
|
+
'[fetch source:/api/users]',
|
|
248
|
+
'explicit',
|
|
249
|
+
[],
|
|
250
|
+
{ getSchema: a => testSchemas.find(s => s.action === a) },
|
|
251
|
+
'fetch'
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(feedback.accepted).toBe(true);
|
|
255
|
+
expect(feedback.correctedExample).toBeUndefined();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// --- 3B: Confidence-Aware Disambiguation ---
|
|
259
|
+
|
|
260
|
+
it('borderline confidence → disambiguation', () => {
|
|
261
|
+
expect(needsDisambiguation(0.6)).toBe(true);
|
|
262
|
+
expect(needsDisambiguation(0.9)).toBe(false);
|
|
263
|
+
expect(needsDisambiguation(0.3)).toBe(false);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('buildDisambiguation produces sorted candidates', () => {
|
|
267
|
+
const nodeA = createCommandNode('toggle', { patient: createSelector('#button') });
|
|
268
|
+
const nodeB = createCommandNode('toggle', { patient: createSelector('.button') });
|
|
269
|
+
|
|
270
|
+
const result = buildDisambiguation('toggle button', [
|
|
271
|
+
{ action: 'toggle', confidence: 0.55, node: nodeB },
|
|
272
|
+
{ action: 'toggle', confidence: 0.65, node: nodeA },
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
expect(result.candidates[0].confidence).toBe(0.65);
|
|
276
|
+
expect(result.candidates[1].confidence).toBe(0.55);
|
|
277
|
+
expect(result.question).toContain('Which did you mean?');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// --- 3C: Pattern Hit-Rate Tracking ---
|
|
281
|
+
|
|
282
|
+
it('tracker records events and computes hit rates', () => {
|
|
283
|
+
const tracker = new PatternTracker();
|
|
284
|
+
|
|
285
|
+
tracker.record({
|
|
286
|
+
timestamp: Date.now(),
|
|
287
|
+
domain: 'flow',
|
|
288
|
+
action: 'fetch',
|
|
289
|
+
language: 'en',
|
|
290
|
+
inputFormat: 'explicit',
|
|
291
|
+
confidence: 0.95,
|
|
292
|
+
outcome: 'accepted',
|
|
293
|
+
});
|
|
294
|
+
tracker.record({
|
|
295
|
+
timestamp: Date.now(),
|
|
296
|
+
domain: 'flow',
|
|
297
|
+
action: 'fetch',
|
|
298
|
+
language: 'en',
|
|
299
|
+
inputFormat: 'explicit',
|
|
300
|
+
confidence: 0.4,
|
|
301
|
+
outcome: 'rejected',
|
|
302
|
+
diagnosticCodes: ['missing-role'],
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const summary = tracker.getSummary();
|
|
306
|
+
expect(summary.totalEvents).toBe(2);
|
|
307
|
+
expect(summary.byCommand['fetch'].rate).toBe(0.5);
|
|
308
|
+
expect(summary.byOutcome['accepted']).toBe(1);
|
|
309
|
+
expect(summary.byOutcome['rejected']).toBe(1);
|
|
310
|
+
expect(summary.topFailures[0].code).toBe('missing-role');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('tracker exports as JSONL', () => {
|
|
314
|
+
const tracker = new PatternTracker();
|
|
315
|
+
tracker.record({
|
|
316
|
+
timestamp: 1000,
|
|
317
|
+
domain: 'flow',
|
|
318
|
+
action: 'fetch',
|
|
319
|
+
language: 'en',
|
|
320
|
+
inputFormat: 'explicit',
|
|
321
|
+
confidence: 0.9,
|
|
322
|
+
outcome: 'accepted',
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const jsonl = tracker.exportJSONL();
|
|
326
|
+
const parsed = JSON.parse(jsonl);
|
|
327
|
+
expect(parsed.action).toBe('fetch');
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// =============================================================================
|
|
332
|
+
// Pillar 4: Registry Convenience Methods
|
|
333
|
+
// =============================================================================
|
|
334
|
+
|
|
335
|
+
describe('Pillar 4: Registry Convenience Methods', () => {
|
|
336
|
+
let registry: DomainRegistry;
|
|
337
|
+
|
|
338
|
+
beforeAll(() => {
|
|
339
|
+
registry = new DomainRegistry();
|
|
340
|
+
registry.register({
|
|
341
|
+
name: 'test-domain',
|
|
342
|
+
description: 'Integration test domain',
|
|
343
|
+
languages: ['en'],
|
|
344
|
+
inputLabel: 'command',
|
|
345
|
+
inputDescription: 'Test command',
|
|
346
|
+
schemas: testSchemas,
|
|
347
|
+
getDSL: () => {
|
|
348
|
+
throw new Error('Not implemented for this test');
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('generatePrompt returns prompt for domain with schemas', () => {
|
|
354
|
+
const prompt = registry.generatePrompt('test-domain');
|
|
355
|
+
expect(prompt).not.toBeNull();
|
|
356
|
+
expect(prompt!.text).toContain('fetch');
|
|
357
|
+
expect(prompt!.text).toContain('toggle');
|
|
358
|
+
expect(prompt!.metadata.commandCount).toBe(2);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('generatePrompt returns null for unknown domain', () => {
|
|
362
|
+
expect(registry.generatePrompt('nonexistent')).toBeNull();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('generateTrainingData returns pairs for domain with schemas', () => {
|
|
366
|
+
const result = registry.generateTrainingData('test-domain');
|
|
367
|
+
expect(result).not.toBeNull();
|
|
368
|
+
expect(result!.pairs.length).toBeGreaterThan(0);
|
|
369
|
+
expect(result!.metadata.domain).toBe('test-domain');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('generateTrainingData returns null for unknown domain', () => {
|
|
373
|
+
expect(registry.generateTrainingData('nonexistent')).toBeNull();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('buildFeedback returns structured feedback', () => {
|
|
377
|
+
const feedback = registry.buildFeedback(
|
|
378
|
+
'test-domain',
|
|
379
|
+
'[fetch source:/api/users]',
|
|
380
|
+
'explicit',
|
|
381
|
+
[{ severity: 'error', message: 'Required role source missing', code: 'missing-role' }]
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
expect(feedback).not.toBeNull();
|
|
385
|
+
expect(feedback!.accepted).toBe(false);
|
|
386
|
+
expect(feedback!.diagnostics[0].fixType).toBe('missing_role');
|
|
387
|
+
// Schema info is included because action was detected from input
|
|
388
|
+
expect(feedback!.schema?.action).toBe('fetch');
|
|
389
|
+
expect(feedback!.hints.length).toBeGreaterThan(0);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('buildFeedback returns null for unknown domain', () => {
|
|
393
|
+
expect(registry.buildFeedback('nonexistent', '[test]', 'explicit', [])).toBeNull();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('getSchemas returns schemas for domain', () => {
|
|
397
|
+
const schemas = registry.getSchemas('test-domain');
|
|
398
|
+
expect(schemas).not.toBeNull();
|
|
399
|
+
expect(schemas!.length).toBe(2);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it('setSchemas attaches schemas post-registration', () => {
|
|
403
|
+
const reg = new DomainRegistry();
|
|
404
|
+
reg.register({
|
|
405
|
+
name: 'lazy',
|
|
406
|
+
description: 'Lazy schema domain',
|
|
407
|
+
languages: ['en'],
|
|
408
|
+
inputLabel: 'input',
|
|
409
|
+
inputDescription: 'Test input',
|
|
410
|
+
getDSL: () => {
|
|
411
|
+
throw new Error('Not implemented');
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Initially no schemas
|
|
416
|
+
expect(reg.getSchemas('lazy')).toBeNull();
|
|
417
|
+
|
|
418
|
+
// Attach schemas
|
|
419
|
+
reg.setSchemas('lazy', testSchemas);
|
|
420
|
+
|
|
421
|
+
expect(reg.getSchemas('lazy')!.length).toBe(2);
|
|
422
|
+
const prompt = reg.generatePrompt('lazy');
|
|
423
|
+
expect(prompt).not.toBeNull();
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// =============================================================================
|
|
428
|
+
// Cross-Cutting: Explicit Syntax ↔ JSON Round-trip
|
|
429
|
+
// =============================================================================
|
|
430
|
+
|
|
431
|
+
describe('Cross-Cutting: Explicit ↔ JSON Round-trip', () => {
|
|
432
|
+
it('node → explicit → parse → node preserves structure', () => {
|
|
433
|
+
const original = createCommandNode('fetch', {
|
|
434
|
+
source: createExpression('/api/users'),
|
|
435
|
+
style: createLiteral('json'),
|
|
436
|
+
destination: createSelector('#list'),
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const explicit = renderExplicit(original);
|
|
440
|
+
const parsed = parseExplicit(explicit);
|
|
441
|
+
|
|
442
|
+
expect(parsed.action).toBe('fetch');
|
|
443
|
+
expect(parsed.roles.get('source')).toBeDefined();
|
|
444
|
+
expect(parsed.roles.get('style')).toBeDefined();
|
|
445
|
+
expect(parsed.roles.get('destination')).toBeDefined();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('node → JSON → node preserves action and roles', () => {
|
|
449
|
+
const original = createCommandNode('toggle', {
|
|
450
|
+
patient: createSelector('.active'),
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const json = semanticNodeToJSON(original);
|
|
454
|
+
expect(json.action).toBe('toggle');
|
|
455
|
+
expect(json.roles['patient']).toBeDefined();
|
|
456
|
+
});
|
|
457
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conformance Fixture Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Drives the protocol test fixtures through the bracket-syntax
|
|
5
|
+
* parseCompound/renderExplicit round-trip pipeline.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { parseCompound, parseDocument } from './explicit-parser';
|
|
10
|
+
import { renderExplicit, renderDocument } from './explicit-renderer';
|
|
11
|
+
import { fromProtocolJSON, toProtocolJSON } from './protocol-json';
|
|
12
|
+
import type { CompoundSemanticNode } from '../core/types';
|
|
13
|
+
|
|
14
|
+
import compoundFixtures from '../../../../protocol/test-fixtures/compound.json';
|
|
15
|
+
import annotationFixtures from '../../../../protocol/test-fixtures/annotations.json';
|
|
16
|
+
import versionFixtures from '../../../../protocol/test-fixtures/version-envelope.json';
|
|
17
|
+
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Compound Fixtures
|
|
20
|
+
// =============================================================================
|
|
21
|
+
|
|
22
|
+
describe('conformance: compound.json', () => {
|
|
23
|
+
for (const fixture of compoundFixtures) {
|
|
24
|
+
it(`${fixture.id}: ${fixture.description}`, () => {
|
|
25
|
+
// Verify rendering matches expected output
|
|
26
|
+
const node = fromProtocolJSON(fixture.node as any);
|
|
27
|
+
const rendered = renderExplicit(node);
|
|
28
|
+
expect(rendered).toBe(fixture.rendered);
|
|
29
|
+
|
|
30
|
+
// Now verify we can PARSE it back (previously renderOnly)
|
|
31
|
+
const reparsed = parseCompound(fixture.rendered);
|
|
32
|
+
expect(reparsed.kind).toBe('compound');
|
|
33
|
+
const compound = reparsed as CompoundSemanticNode;
|
|
34
|
+
expect(compound.chainType).toBe(fixture.node.chainType);
|
|
35
|
+
expect(compound.statements).toHaveLength(fixture.node.statements!.length);
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < compound.statements.length; i++) {
|
|
38
|
+
expect(compound.statements[i].action).toBe(fixture.node.statements![i].action);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Annotation Fixtures
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
describe('conformance: annotations.json', () => {
|
|
49
|
+
for (const fixture of annotationFixtures) {
|
|
50
|
+
it(`${fixture.id}: ${fixture.description}`, () => {
|
|
51
|
+
// Convert JSON → SemanticNode → render → parse → compare
|
|
52
|
+
const node = fromProtocolJSON(fixture.jsonInput as any);
|
|
53
|
+
const rendered = renderExplicit(node);
|
|
54
|
+
const reparsed = parseCompound(rendered);
|
|
55
|
+
|
|
56
|
+
// Verify action preserved
|
|
57
|
+
expect(reparsed.action).toBe(fixture.jsonInput.action);
|
|
58
|
+
|
|
59
|
+
// Verify annotations round-trip
|
|
60
|
+
if (fixture.jsonInput.annotations && fixture.jsonInput.annotations.length > 0) {
|
|
61
|
+
expect(reparsed.annotations).toHaveLength(fixture.jsonInput.annotations.length);
|
|
62
|
+
for (let i = 0; i < fixture.jsonInput.annotations.length; i++) {
|
|
63
|
+
expect(reparsed.annotations![i].name).toBe(fixture.jsonInput.annotations[i].name);
|
|
64
|
+
if (fixture.jsonInput.annotations[i].value !== undefined) {
|
|
65
|
+
expect(reparsed.annotations![i].value).toBe(fixture.jsonInput.annotations[i].value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// ann-006: no annotations → should be undefined after parse
|
|
70
|
+
expect(reparsed.annotations).toBeUndefined();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Verify re-render produces same output
|
|
74
|
+
const rerendered = renderExplicit(reparsed);
|
|
75
|
+
expect(rerendered).toBe(rendered);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// Version Envelope Fixtures (streaming format)
|
|
82
|
+
// =============================================================================
|
|
83
|
+
|
|
84
|
+
describe('conformance: version-envelope.json (streaming)', () => {
|
|
85
|
+
for (const fixture of versionFixtures) {
|
|
86
|
+
const f = fixture as any;
|
|
87
|
+
if (!f.streamingInput) continue;
|
|
88
|
+
|
|
89
|
+
// ver-007 describes v1.0/v1.1 fallback behavior — our parser is v1.2,
|
|
90
|
+
// so we parse the version header correctly instead of treating it as a comment.
|
|
91
|
+
if (f.v1Fallback) {
|
|
92
|
+
it(`${f.id}: v1.2 parser parses version header — ${f.description}`, () => {
|
|
93
|
+
const envelope = parseDocument(f.streamingInput);
|
|
94
|
+
// v1.2 parser extracts the version (v1.0 parsers would ignore it)
|
|
95
|
+
expect(envelope.lseVersion).toBe('1.2');
|
|
96
|
+
expect(envelope.nodes).toHaveLength(f.v1Fallback.expectedNodeCount);
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
it(`${f.id}: ${f.description}`, () => {
|
|
102
|
+
const envelope = parseDocument(f.streamingInput);
|
|
103
|
+
expect(envelope.lseVersion).toBe(f.expectedVersion);
|
|
104
|
+
expect(envelope.nodes).toHaveLength(f.expectedNodeCount);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Also test JSON-based envelope round-trips
|
|
109
|
+
for (const fixture of versionFixtures) {
|
|
110
|
+
const f = fixture as any;
|
|
111
|
+
if (f.streamingInput) continue;
|
|
112
|
+
if (!f.expectedRoundTrip) continue;
|
|
113
|
+
|
|
114
|
+
it(`${f.id}: JSON envelope round-trip — ${f.description}`, () => {
|
|
115
|
+
// For JSON envelopes: verify node count and version
|
|
116
|
+
expect(f.jsonInput.lseVersion).toBe(f.expectedVersion);
|
|
117
|
+
expect(f.jsonInput.nodes).toHaveLength(f.expectedNodeCount);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|