@lokascript/framework 2.0.0 → 2.1.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 +1351 -21
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.js +114 -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/types.d.ts +80 -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 +2685 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2652 -71
- package/dist/index.js.map +1 -1
- package/dist/ir/explicit-parser.d.ts +31 -0
- package/dist/ir/explicit-parser.d.ts.map +1 -0
- package/dist/ir/explicit-renderer.d.ts +17 -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 +29 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +1557 -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/types.d.ts +141 -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 +3 -0
- 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/types.ts +145 -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 +22 -0
- package/src/integration/llm-lse-integration.test.ts +457 -0
- package/src/ir/explicit-parser.test.ts +306 -0
- package/src/ir/explicit-parser.ts +419 -0
- package/src/ir/explicit-renderer.test.ts +144 -0
- package/src/ir/explicit-renderer.ts +146 -0
- package/src/ir/from-interchange.test.ts +853 -0
- package/src/ir/from-interchange.ts +594 -0
- package/src/ir/index.ts +66 -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 +1185 -0
- package/src/ir/protocol-json.ts +745 -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/types.ts +181 -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,38 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { isValidReference, DEFAULT_REFERENCES } from './references';
|
|
3
|
+
|
|
4
|
+
describe('isValidReference', () => {
|
|
5
|
+
it('recognizes default references', () => {
|
|
6
|
+
expect(isValidReference('me')).toBe(true);
|
|
7
|
+
expect(isValidReference('you')).toBe(true);
|
|
8
|
+
expect(isValidReference('it')).toBe(true);
|
|
9
|
+
expect(isValidReference('result')).toBe(true);
|
|
10
|
+
expect(isValidReference('event')).toBe(true);
|
|
11
|
+
expect(isValidReference('target')).toBe(true);
|
|
12
|
+
expect(isValidReference('body')).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('rejects non-references', () => {
|
|
16
|
+
expect(isValidReference('foo')).toBe(false);
|
|
17
|
+
expect(isValidReference('hello')).toBe(false);
|
|
18
|
+
expect(isValidReference('')).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('is case-sensitive', () => {
|
|
22
|
+
expect(isValidReference('Me')).toBe(false);
|
|
23
|
+
expect(isValidReference('IT')).toBe(false);
|
|
24
|
+
expect(isValidReference('Result')).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('accepts custom reference set', () => {
|
|
28
|
+
const custom = new Set(['self', 'other', 'context']);
|
|
29
|
+
expect(isValidReference('self', custom)).toBe(true);
|
|
30
|
+
expect(isValidReference('other', custom)).toBe(true);
|
|
31
|
+
expect(isValidReference('me', custom)).toBe(false);
|
|
32
|
+
expect(isValidReference('it', custom)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('DEFAULT_REFERENCES contains exactly 7 entries', () => {
|
|
36
|
+
expect(DEFAULT_REFERENCES.size).toBe(7);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference Validation
|
|
3
|
+
*
|
|
4
|
+
* Configurable validation for reference values (me, you, it, result, etc.).
|
|
5
|
+
* Domain DSLs can provide custom reference sets.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default set of valid references.
|
|
10
|
+
* Covers the common hyperscript references; domain DSLs can extend or replace.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_REFERENCES: ReadonlySet<string> = new Set([
|
|
13
|
+
'me',
|
|
14
|
+
'you',
|
|
15
|
+
'it',
|
|
16
|
+
'result',
|
|
17
|
+
'event',
|
|
18
|
+
'target',
|
|
19
|
+
'body',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a value is a valid reference name.
|
|
24
|
+
*
|
|
25
|
+
* @param value - The string to check
|
|
26
|
+
* @param referenceSet - Custom reference set (defaults to DEFAULT_REFERENCES)
|
|
27
|
+
*/
|
|
28
|
+
export function isValidReference(
|
|
29
|
+
value: string,
|
|
30
|
+
referenceSet: ReadonlySet<string> = DEFAULT_REFERENCES
|
|
31
|
+
): boolean {
|
|
32
|
+
return referenceSet.has(value);
|
|
33
|
+
}
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Round-Trip Pipeline Tests
|
|
3
|
+
*
|
|
4
|
+
* Validates the end-to-end pipeline:
|
|
5
|
+
* InterchangeNode → SemanticNode → LSE bracket → parseExplicit → semantic equivalence
|
|
6
|
+
* InterchangeNode → SemanticNode → protocol JSON → fromProtocolJSON → semantic equivalence
|
|
7
|
+
*
|
|
8
|
+
* With the kind-aware parseExplicit, bracket round-trips now preserve:
|
|
9
|
+
* - `kind` for event-handler, conditional, and loop nodes
|
|
10
|
+
* - `selectorKind` (inferred from selector prefix)
|
|
11
|
+
* - `thenBranch`/`elseBranch` for conditionals
|
|
12
|
+
* - `loopVariant`/`body` for loops
|
|
13
|
+
*
|
|
14
|
+
* These tests verify that the action, roles, and structural kind survive the trip.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect } from 'vitest';
|
|
18
|
+
import { fromInterchangeNode } from './from-interchange';
|
|
19
|
+
import { renderExplicit } from './explicit-renderer';
|
|
20
|
+
import { parseExplicit } from './explicit-parser';
|
|
21
|
+
import { toProtocolJSON, fromProtocolJSON } from './protocol-json';
|
|
22
|
+
|
|
23
|
+
// =============================================================================
|
|
24
|
+
// Helpers
|
|
25
|
+
// =============================================================================
|
|
26
|
+
|
|
27
|
+
/** Round-trip through LSE bracket syntax */
|
|
28
|
+
function bracketRoundTrip(interchange: Record<string, unknown>) {
|
|
29
|
+
const semantic = fromInterchangeNode(interchange);
|
|
30
|
+
const lse = renderExplicit(semantic);
|
|
31
|
+
const parsed = parseExplicit(lse);
|
|
32
|
+
return { semantic, lse, parsed };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Round-trip through protocol JSON */
|
|
36
|
+
function protocolRoundTrip(interchange: Record<string, unknown>) {
|
|
37
|
+
const semantic = fromInterchangeNode(interchange);
|
|
38
|
+
const json = toProtocolJSON(semantic);
|
|
39
|
+
const restored = fromProtocolJSON(json);
|
|
40
|
+
return { semantic, json, restored };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Compare values by type and value only (ignores selectorKind, dataType, etc.) */
|
|
44
|
+
function valuesMatch(a: any, b: any): boolean {
|
|
45
|
+
if (!a || !b) return a === b;
|
|
46
|
+
return a.type === b.type && a.value === b.value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// 1. Simple Commands
|
|
51
|
+
// =============================================================================
|
|
52
|
+
|
|
53
|
+
describe('LLM round-trip: simple commands', () => {
|
|
54
|
+
it('toggle .active — bracket round-trip preserves action and patient', () => {
|
|
55
|
+
const { semantic, parsed } = bracketRoundTrip({
|
|
56
|
+
type: 'command',
|
|
57
|
+
name: 'toggle',
|
|
58
|
+
args: [{ type: 'selector', value: '.active' }],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(parsed.action).toBe('toggle');
|
|
62
|
+
expect(parsed.kind).toBe('command');
|
|
63
|
+
expect(valuesMatch(parsed.roles.get('patient'), semantic.roles.get('patient'))).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('toggle .active — protocol JSON round-trip preserves full structure', () => {
|
|
67
|
+
const { semantic, restored } = protocolRoundTrip({
|
|
68
|
+
type: 'command',
|
|
69
|
+
name: 'toggle',
|
|
70
|
+
args: [{ type: 'selector', value: '.active' }],
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(restored.action).toBe('toggle');
|
|
74
|
+
expect(restored.roles.get('patient')).toEqual(semantic.roles.get('patient'));
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('add .highlight to #button — bracket round-trip', () => {
|
|
78
|
+
const { parsed } = bracketRoundTrip({
|
|
79
|
+
type: 'command',
|
|
80
|
+
name: 'add',
|
|
81
|
+
args: [{ type: 'selector', value: '.highlight' }],
|
|
82
|
+
target: { type: 'selector', value: '#button' },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(parsed.action).toBe('add');
|
|
86
|
+
// Patient and destination survive the trip
|
|
87
|
+
const patient = parsed.roles.get('patient');
|
|
88
|
+
const dest = parsed.roles.get('destination');
|
|
89
|
+
expect(patient?.type).toBe('selector');
|
|
90
|
+
expect(patient?.value).toBe('.highlight');
|
|
91
|
+
expect(dest?.type).toBe('selector');
|
|
92
|
+
expect(dest?.value).toBe('#button');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('set :count to 42 — bracket round-trip', () => {
|
|
96
|
+
const { parsed } = bracketRoundTrip({
|
|
97
|
+
type: 'command',
|
|
98
|
+
name: 'set',
|
|
99
|
+
args: [
|
|
100
|
+
{ type: 'variable', name: 'count', scope: 'element' },
|
|
101
|
+
{ type: 'literal', value: 42 },
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(parsed.action).toBe('set');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('remove .error from me — bracket round-trip preserves roles', () => {
|
|
109
|
+
const { parsed } = bracketRoundTrip({
|
|
110
|
+
type: 'command',
|
|
111
|
+
name: 'remove',
|
|
112
|
+
args: [{ type: 'selector', value: '.error' }],
|
|
113
|
+
target: { type: 'identifier', value: 'me' },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(parsed.action).toBe('remove');
|
|
117
|
+
expect(parsed.roles.get('patient')?.value).toBe('.error');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('bracket round-trip preserves selectorKind', () => {
|
|
121
|
+
const { semantic, parsed } = bracketRoundTrip({
|
|
122
|
+
type: 'command',
|
|
123
|
+
name: 'toggle',
|
|
124
|
+
args: [{ type: 'selector', value: '.active' }],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const originalPatient = semantic.roles.get('patient');
|
|
128
|
+
const parsedPatient = parsed.roles.get('patient');
|
|
129
|
+
expect(originalPatient?.type).toBe('selector');
|
|
130
|
+
expect(parsedPatient?.type).toBe('selector');
|
|
131
|
+
// selectorKind now preserved through bracket round-trip
|
|
132
|
+
if (originalPatient?.type === 'selector' && parsedPatient?.type === 'selector') {
|
|
133
|
+
expect(parsedPatient.selectorKind).toBe(originalPatient.selectorKind);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// =============================================================================
|
|
139
|
+
// 2. Event Handlers
|
|
140
|
+
// =============================================================================
|
|
141
|
+
|
|
142
|
+
describe('LLM round-trip: event handlers', () => {
|
|
143
|
+
it('on click toggle .active — bracket round-trip preserves event-handler kind', () => {
|
|
144
|
+
const { lse, parsed } = bracketRoundTrip({
|
|
145
|
+
type: 'event',
|
|
146
|
+
event: 'click',
|
|
147
|
+
body: [
|
|
148
|
+
{
|
|
149
|
+
type: 'command',
|
|
150
|
+
name: 'toggle',
|
|
151
|
+
args: [{ type: 'selector', value: '.active' }],
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// The LSE bracket form includes the event name and command
|
|
157
|
+
expect(lse).toContain('on');
|
|
158
|
+
expect(lse).toContain('click');
|
|
159
|
+
expect(lse).toContain('toggle');
|
|
160
|
+
expect(lse).toContain('.active');
|
|
161
|
+
|
|
162
|
+
// Kind-aware parser produces event-handler node
|
|
163
|
+
expect(parsed.kind).toBe('event-handler');
|
|
164
|
+
expect(parsed.action).toBe('on');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('on click toggle .active — protocol JSON preserves event-handler kind', () => {
|
|
168
|
+
const { semantic, restored } = protocolRoundTrip({
|
|
169
|
+
type: 'event',
|
|
170
|
+
event: 'click',
|
|
171
|
+
body: [
|
|
172
|
+
{
|
|
173
|
+
type: 'command',
|
|
174
|
+
name: 'toggle',
|
|
175
|
+
args: [{ type: 'selector', value: '.active' }],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(restored.kind).toBe('event-handler');
|
|
181
|
+
// Protocol JSON preserves the full structure
|
|
182
|
+
expect(restored.action).toBe(semantic.action);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('multi-command event handler LSE contains all commands', () => {
|
|
186
|
+
const { lse } = bracketRoundTrip({
|
|
187
|
+
type: 'event',
|
|
188
|
+
event: 'click',
|
|
189
|
+
body: [
|
|
190
|
+
{
|
|
191
|
+
type: 'command',
|
|
192
|
+
name: 'add',
|
|
193
|
+
args: [{ type: 'selector', value: '.loading' }],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
type: 'command',
|
|
197
|
+
name: 'fetch',
|
|
198
|
+
args: [{ type: 'literal', value: '/api/data' }],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
type: 'command',
|
|
202
|
+
name: 'remove',
|
|
203
|
+
args: [{ type: 'selector', value: '.loading' }],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(lse).toContain('add');
|
|
209
|
+
expect(lse).toContain('fetch');
|
|
210
|
+
expect(lse).toContain('remove');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// =============================================================================
|
|
215
|
+
// 3. Control Flow
|
|
216
|
+
// =============================================================================
|
|
217
|
+
|
|
218
|
+
describe('LLM round-trip: control flow', () => {
|
|
219
|
+
it('if condition — bracket round-trip preserves conditional kind', () => {
|
|
220
|
+
// Use a simple condition (no operators) that round-trips cleanly
|
|
221
|
+
const { parsed } = bracketRoundTrip({
|
|
222
|
+
type: 'if',
|
|
223
|
+
condition: { type: 'identifier', value: 'visible' },
|
|
224
|
+
thenBranch: [
|
|
225
|
+
{
|
|
226
|
+
type: 'command',
|
|
227
|
+
name: 'show',
|
|
228
|
+
args: [{ type: 'selector', value: '#message' }],
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
elseBranch: [],
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Kind-aware parser produces conditional node
|
|
235
|
+
expect(parsed.kind).toBe('conditional');
|
|
236
|
+
expect(parsed.action).toBe('if');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('if condition with complex expression — renders but complex exprs need protocol JSON', () => {
|
|
240
|
+
// Note: bracket syntax for complex conditions (containing operators like ==)
|
|
241
|
+
// doesn't cleanly round-trip through parseExplicit because the parser treats
|
|
242
|
+
// tokens like "==" as role-format markers. Use protocol JSON for lossless round-trip.
|
|
243
|
+
const semantic = fromInterchangeNode({
|
|
244
|
+
type: 'if',
|
|
245
|
+
condition: {
|
|
246
|
+
type: 'binary',
|
|
247
|
+
operator: '==',
|
|
248
|
+
left: { type: 'identifier', value: 'x' },
|
|
249
|
+
right: { type: 'literal', value: 0 },
|
|
250
|
+
},
|
|
251
|
+
thenBranch: [
|
|
252
|
+
{
|
|
253
|
+
type: 'command',
|
|
254
|
+
name: 'show',
|
|
255
|
+
args: [{ type: 'selector', value: '#message' }],
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
elseBranch: [],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Verify it renders to bracket syntax (for LLM display)
|
|
262
|
+
const lse = renderExplicit(semantic);
|
|
263
|
+
expect(lse).toContain('if');
|
|
264
|
+
expect(lse).toContain('show');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('if condition — protocol JSON preserves conditional kind', () => {
|
|
268
|
+
const { restored } = protocolRoundTrip({
|
|
269
|
+
type: 'if',
|
|
270
|
+
condition: {
|
|
271
|
+
type: 'binary',
|
|
272
|
+
operator: '==',
|
|
273
|
+
left: { type: 'identifier', value: 'x' },
|
|
274
|
+
right: { type: 'literal', value: 0 },
|
|
275
|
+
},
|
|
276
|
+
thenBranch: [
|
|
277
|
+
{
|
|
278
|
+
type: 'command',
|
|
279
|
+
name: 'show',
|
|
280
|
+
args: [{ type: 'selector', value: '#message' }],
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
elseBranch: [],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(restored.kind).toBe('conditional');
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('repeat loop — bracket round-trip preserves loop kind', () => {
|
|
290
|
+
const { lse, parsed } = bracketRoundTrip({
|
|
291
|
+
type: 'repeat',
|
|
292
|
+
count: 5,
|
|
293
|
+
body: [
|
|
294
|
+
{
|
|
295
|
+
type: 'command',
|
|
296
|
+
name: 'log',
|
|
297
|
+
args: [{ type: 'literal', value: 'tick' }],
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
expect(lse).toContain('repeat');
|
|
303
|
+
expect(lse).toContain('log');
|
|
304
|
+
|
|
305
|
+
// Kind-aware parser produces loop node
|
|
306
|
+
expect(parsed.kind).toBe('loop');
|
|
307
|
+
expect(parsed.action).toBe('repeat');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('foreach loop — protocol JSON preserves loop kind', () => {
|
|
311
|
+
const { restored } = protocolRoundTrip({
|
|
312
|
+
type: 'foreach',
|
|
313
|
+
itemName: 'item',
|
|
314
|
+
collection: { type: 'selector', value: '.items' },
|
|
315
|
+
body: [
|
|
316
|
+
{
|
|
317
|
+
type: 'command',
|
|
318
|
+
name: 'add',
|
|
319
|
+
args: [{ type: 'selector', value: '.highlight' }],
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(restored.kind).toBe('loop');
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// =============================================================================
|
|
329
|
+
// 4. Protocol JSON Round-Trip (lossless)
|
|
330
|
+
// =============================================================================
|
|
331
|
+
|
|
332
|
+
describe('LLM round-trip: protocol JSON is lossless', () => {
|
|
333
|
+
const testCases = [
|
|
334
|
+
{
|
|
335
|
+
name: 'toggle .active',
|
|
336
|
+
node: {
|
|
337
|
+
type: 'command',
|
|
338
|
+
name: 'toggle',
|
|
339
|
+
args: [{ type: 'selector', value: '.active' }],
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: 'show #modal',
|
|
344
|
+
node: {
|
|
345
|
+
type: 'command',
|
|
346
|
+
name: 'show',
|
|
347
|
+
args: [{ type: 'selector', value: '#modal' }],
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
for (const tc of testCases) {
|
|
353
|
+
it(`${tc.name}: protocol JSON → fromProtocolJSON → toProtocolJSON is identity`, () => {
|
|
354
|
+
const semantic = fromInterchangeNode(tc.node);
|
|
355
|
+
const json1 = toProtocolJSON(semantic);
|
|
356
|
+
const restored = fromProtocolJSON(json1);
|
|
357
|
+
const json2 = toProtocolJSON(restored);
|
|
358
|
+
|
|
359
|
+
expect(json2).toEqual(json1);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// =============================================================================
|
|
365
|
+
// 5. Idempotency
|
|
366
|
+
// =============================================================================
|
|
367
|
+
|
|
368
|
+
describe('LLM round-trip: idempotency', () => {
|
|
369
|
+
it('bracket render → parse → render is idempotent for commands', () => {
|
|
370
|
+
const semantic = fromInterchangeNode({
|
|
371
|
+
type: 'command',
|
|
372
|
+
name: 'toggle',
|
|
373
|
+
args: [{ type: 'selector', value: '.active' }],
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const lse1 = renderExplicit(semantic);
|
|
377
|
+
const parsed = parseExplicit(lse1);
|
|
378
|
+
const lse2 = renderExplicit(parsed);
|
|
379
|
+
const parsed2 = parseExplicit(lse2);
|
|
380
|
+
const lse3 = renderExplicit(parsed2);
|
|
381
|
+
|
|
382
|
+
expect(lse2).toBe(lse1);
|
|
383
|
+
expect(lse3).toBe(lse1);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('bracket render → parse → render is idempotent for event handlers', () => {
|
|
387
|
+
const semantic = fromInterchangeNode({
|
|
388
|
+
type: 'event',
|
|
389
|
+
event: 'click',
|
|
390
|
+
body: [
|
|
391
|
+
{
|
|
392
|
+
type: 'command',
|
|
393
|
+
name: 'toggle',
|
|
394
|
+
args: [{ type: 'selector', value: '.active' }],
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const lse1 = renderExplicit(semantic);
|
|
400
|
+
const parsed = parseExplicit(lse1);
|
|
401
|
+
const lse2 = renderExplicit(parsed);
|
|
402
|
+
|
|
403
|
+
expect(lse2).toBe(lse1);
|
|
404
|
+
expect(parsed.kind).toBe('event-handler');
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('bracket render → parse → render is idempotent for loops', () => {
|
|
408
|
+
const semantic = fromInterchangeNode({
|
|
409
|
+
type: 'repeat',
|
|
410
|
+
count: 3,
|
|
411
|
+
body: [
|
|
412
|
+
{
|
|
413
|
+
type: 'command',
|
|
414
|
+
name: 'log',
|
|
415
|
+
args: [{ type: 'literal', value: 'tick' }],
|
|
416
|
+
},
|
|
417
|
+
],
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const lse1 = renderExplicit(semantic);
|
|
421
|
+
const parsed = parseExplicit(lse1);
|
|
422
|
+
const lse2 = renderExplicit(parsed);
|
|
423
|
+
|
|
424
|
+
expect(lse2).toBe(lse1);
|
|
425
|
+
expect(parsed.kind).toBe('loop');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('protocol JSON serialize → deserialize → serialize is idempotent', () => {
|
|
429
|
+
const semantic = fromInterchangeNode({
|
|
430
|
+
type: 'event',
|
|
431
|
+
event: 'click',
|
|
432
|
+
body: [
|
|
433
|
+
{
|
|
434
|
+
type: 'command',
|
|
435
|
+
name: 'add',
|
|
436
|
+
args: [{ type: 'selector', value: '.clicked' }],
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const json1 = toProtocolJSON(semantic);
|
|
442
|
+
const restored = fromProtocolJSON(json1);
|
|
443
|
+
const json2 = toProtocolJSON(restored);
|
|
444
|
+
|
|
445
|
+
expect(json2).toEqual(json1);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// =============================================================================
|
|
450
|
+
// 6. End-to-end: same input through both paths
|
|
451
|
+
// =============================================================================
|
|
452
|
+
|
|
453
|
+
describe('LLM round-trip: both paths from same input', () => {
|
|
454
|
+
it('simple command produces valid LSE through both bracket and JSON paths', () => {
|
|
455
|
+
const input = {
|
|
456
|
+
type: 'command',
|
|
457
|
+
name: 'toggle',
|
|
458
|
+
args: [{ type: 'selector', value: '.active' }],
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Path A: bracket
|
|
462
|
+
const semantic = fromInterchangeNode(input);
|
|
463
|
+
const lse = renderExplicit(semantic);
|
|
464
|
+
expect(lse).toContain('toggle');
|
|
465
|
+
expect(lse).toContain('.active');
|
|
466
|
+
|
|
467
|
+
// Path B: protocol JSON
|
|
468
|
+
const json = toProtocolJSON(semantic);
|
|
469
|
+
expect(json.action).toBe('toggle');
|
|
470
|
+
expect(json.kind).toBe('command');
|
|
471
|
+
|
|
472
|
+
// Both produce parseable output
|
|
473
|
+
const fromBracket = parseExplicit(lse);
|
|
474
|
+
const fromJSON = fromProtocolJSON(json);
|
|
475
|
+
expect(fromBracket.action).toBe('toggle');
|
|
476
|
+
expect(fromJSON.action).toBe('toggle');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('event handler produces consistent kind through both paths', () => {
|
|
480
|
+
const input = {
|
|
481
|
+
type: 'event',
|
|
482
|
+
event: 'click',
|
|
483
|
+
body: [
|
|
484
|
+
{
|
|
485
|
+
type: 'command',
|
|
486
|
+
name: 'toggle',
|
|
487
|
+
args: [{ type: 'selector', value: '.active' }],
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const semantic = fromInterchangeNode(input);
|
|
493
|
+
|
|
494
|
+
// Bracket path
|
|
495
|
+
const lse = renderExplicit(semantic);
|
|
496
|
+
const fromBracket = parseExplicit(lse);
|
|
497
|
+
expect(fromBracket.kind).toBe('event-handler');
|
|
498
|
+
|
|
499
|
+
// Protocol JSON path
|
|
500
|
+
const json = toProtocolJSON(semantic);
|
|
501
|
+
const fromJSON = fromProtocolJSON(json);
|
|
502
|
+
expect(fromJSON.kind).toBe('event-handler');
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// =============================================================================
|
|
507
|
+
// 7. Direct parseExplicit kind-awareness
|
|
508
|
+
// =============================================================================
|
|
509
|
+
|
|
510
|
+
describe('parseExplicit kind-awareness', () => {
|
|
511
|
+
it('parses [on ...] as event-handler', () => {
|
|
512
|
+
const node = parseExplicit('[on event:click body:[toggle patient:.active]]');
|
|
513
|
+
expect(node.kind).toBe('event-handler');
|
|
514
|
+
expect(node.action).toBe('on');
|
|
515
|
+
expect(node.roles.get('event')?.value).toBe('click');
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('parses [if ...] as conditional', () => {
|
|
519
|
+
const node = parseExplicit('[if condition:visible then:[show patient:#msg]]');
|
|
520
|
+
expect(node.kind).toBe('conditional');
|
|
521
|
+
expect(node.action).toBe('if');
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('parses [repeat ...] as loop with times variant', () => {
|
|
525
|
+
const node = parseExplicit('[repeat quantity:5 loop-body:[log patient:"tick"]]');
|
|
526
|
+
expect(node.kind).toBe('loop');
|
|
527
|
+
expect(node.action).toBe('repeat');
|
|
528
|
+
if (node.kind === 'loop') {
|
|
529
|
+
expect((node as any).loopVariant).toBe('times');
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('parses [repeat ...] as loop with for variant', () => {
|
|
534
|
+
const node = parseExplicit('[repeat source:.items variable:item loop-body:[add patient:.hl]]');
|
|
535
|
+
expect(node.kind).toBe('loop');
|
|
536
|
+
if (node.kind === 'loop') {
|
|
537
|
+
expect((node as any).loopVariant).toBe('for');
|
|
538
|
+
expect((node as any).loopVariable).toBe('item');
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
it('parses [repeat ...] with explicit loop-variant', () => {
|
|
543
|
+
const node = parseExplicit(
|
|
544
|
+
'[repeat loop-variant:while condition:running loop-body:[log patient:"waiting"]]'
|
|
545
|
+
);
|
|
546
|
+
expect(node.kind).toBe('loop');
|
|
547
|
+
if (node.kind === 'loop') {
|
|
548
|
+
expect((node as any).loopVariant).toBe('while');
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('parses regular commands as command kind', () => {
|
|
553
|
+
const node = parseExplicit('[toggle patient:.active]');
|
|
554
|
+
expect(node.kind).toBe('command');
|
|
555
|
+
expect(node.action).toBe('toggle');
|
|
556
|
+
});
|
|
557
|
+
});
|