@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,281 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateSemanticJSON, jsonToSemanticNode, semanticNodeToJSON } from './json-schema';
|
|
3
|
+
import type { SemanticJSON } from './types';
|
|
4
|
+
import {
|
|
5
|
+
createCommandNode,
|
|
6
|
+
createSelector,
|
|
7
|
+
createLiteral,
|
|
8
|
+
createReference,
|
|
9
|
+
createFlag,
|
|
10
|
+
} from '../core/types';
|
|
11
|
+
|
|
12
|
+
describe('validateSemanticJSON', () => {
|
|
13
|
+
it('validates a correct input', () => {
|
|
14
|
+
const input: SemanticJSON = {
|
|
15
|
+
action: 'toggle',
|
|
16
|
+
roles: {
|
|
17
|
+
patient: { type: 'selector', value: '.active' },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
expect(validateSemanticJSON(input)).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rejects missing action', () => {
|
|
24
|
+
const input = { roles: {} } as unknown as SemanticJSON;
|
|
25
|
+
const diags = validateSemanticJSON(input);
|
|
26
|
+
expect(diags).toHaveLength(1);
|
|
27
|
+
expect(diags[0].code).toBe('INVALID_ACTION');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects invalid role value', () => {
|
|
31
|
+
const input: SemanticJSON = {
|
|
32
|
+
action: 'toggle',
|
|
33
|
+
roles: {
|
|
34
|
+
patient: 'not-an-object' as unknown as { type: 'selector'; value: string },
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const diags = validateSemanticJSON(input);
|
|
38
|
+
expect(diags.some(d => d.code === 'INVALID_ROLE_VALUE')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects invalid value type', () => {
|
|
42
|
+
const input: SemanticJSON = {
|
|
43
|
+
action: 'toggle',
|
|
44
|
+
roles: {
|
|
45
|
+
patient: { type: 'unknown' as 'selector', value: '.active' },
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const diags = validateSemanticJSON(input);
|
|
49
|
+
expect(diags.some(d => d.code === 'INVALID_VALUE_TYPE')).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('rejects missing value field', () => {
|
|
53
|
+
const input: SemanticJSON = {
|
|
54
|
+
action: 'toggle',
|
|
55
|
+
roles: {
|
|
56
|
+
patient: { type: 'selector' } as unknown as { type: 'selector'; value: string },
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const diags = validateSemanticJSON(input);
|
|
60
|
+
expect(diags.some(d => d.code === 'MISSING_VALUE')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('validates trigger', () => {
|
|
64
|
+
const input: SemanticJSON = {
|
|
65
|
+
action: 'toggle',
|
|
66
|
+
roles: { patient: { type: 'selector', value: '.active' } },
|
|
67
|
+
trigger: { event: '' },
|
|
68
|
+
};
|
|
69
|
+
const diags = validateSemanticJSON(input);
|
|
70
|
+
expect(diags.some(d => d.code === 'INVALID_TRIGGER')).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('accepts valid trigger', () => {
|
|
74
|
+
const input: SemanticJSON = {
|
|
75
|
+
action: 'toggle',
|
|
76
|
+
roles: { patient: { type: 'selector', value: '.active' } },
|
|
77
|
+
trigger: { event: 'click' },
|
|
78
|
+
};
|
|
79
|
+
expect(validateSemanticJSON(input)).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('jsonToSemanticNode', () => {
|
|
84
|
+
it('converts a basic command', () => {
|
|
85
|
+
const input: SemanticJSON = {
|
|
86
|
+
action: 'toggle',
|
|
87
|
+
roles: {
|
|
88
|
+
patient: { type: 'selector', value: '.active' },
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
const node = jsonToSemanticNode(input);
|
|
92
|
+
expect(node.kind).toBe('command');
|
|
93
|
+
expect(node.action).toBe('toggle');
|
|
94
|
+
expect(node.roles.get('patient')).toMatchObject({ type: 'selector', value: '.active' });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('converts selector values with kind detection', () => {
|
|
98
|
+
const input: SemanticJSON = {
|
|
99
|
+
action: 'add',
|
|
100
|
+
roles: {
|
|
101
|
+
patient: { type: 'selector', value: '.highlight' },
|
|
102
|
+
destination: { type: 'selector', value: '#output' },
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
const node = jsonToSemanticNode(input);
|
|
106
|
+
const dest = node.roles.get('destination');
|
|
107
|
+
expect(dest).toMatchObject({ type: 'selector', value: '#output', selectorKind: 'id' });
|
|
108
|
+
const pat = node.roles.get('patient');
|
|
109
|
+
expect(pat).toMatchObject({ type: 'selector', value: '.highlight', selectorKind: 'class' });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('converts literal values', () => {
|
|
113
|
+
const input: SemanticJSON = {
|
|
114
|
+
action: 'put',
|
|
115
|
+
roles: {
|
|
116
|
+
patient: { type: 'literal', value: 'hello' },
|
|
117
|
+
quantity: { type: 'literal', value: 42 },
|
|
118
|
+
flag: { type: 'literal', value: true },
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
const node = jsonToSemanticNode(input);
|
|
122
|
+
expect(node.roles.get('patient')).toMatchObject({
|
|
123
|
+
type: 'literal',
|
|
124
|
+
value: 'hello',
|
|
125
|
+
dataType: 'string',
|
|
126
|
+
});
|
|
127
|
+
expect(node.roles.get('quantity')).toMatchObject({
|
|
128
|
+
type: 'literal',
|
|
129
|
+
value: 42,
|
|
130
|
+
dataType: 'number',
|
|
131
|
+
});
|
|
132
|
+
expect(node.roles.get('flag')).toMatchObject({
|
|
133
|
+
type: 'literal',
|
|
134
|
+
value: true,
|
|
135
|
+
dataType: 'boolean',
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('converts reference values', () => {
|
|
140
|
+
const input: SemanticJSON = {
|
|
141
|
+
action: 'add',
|
|
142
|
+
roles: {
|
|
143
|
+
destination: { type: 'reference', value: 'me' },
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
const node = jsonToSemanticNode(input);
|
|
147
|
+
expect(node.roles.get('destination')).toMatchObject({ type: 'reference', value: 'me' });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('converts expression values', () => {
|
|
151
|
+
const input: SemanticJSON = {
|
|
152
|
+
action: 'set',
|
|
153
|
+
roles: {
|
|
154
|
+
goal: { type: 'expression', value: 'x + 1' },
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const node = jsonToSemanticNode(input);
|
|
158
|
+
expect(node.roles.get('goal')).toMatchObject({ type: 'expression', raw: 'x + 1' });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('wraps in event handler when trigger is present', () => {
|
|
162
|
+
const input: SemanticJSON = {
|
|
163
|
+
action: 'toggle',
|
|
164
|
+
roles: {
|
|
165
|
+
patient: { type: 'selector', value: '.active' },
|
|
166
|
+
},
|
|
167
|
+
trigger: { event: 'click' },
|
|
168
|
+
};
|
|
169
|
+
const node = jsonToSemanticNode(input);
|
|
170
|
+
expect(node.kind).toBe('event-handler');
|
|
171
|
+
expect(node.action).toBe('on');
|
|
172
|
+
const eventNode = node as { body: Array<{ action: string; roles: Map<string, unknown> }> };
|
|
173
|
+
expect(eventNode.body).toHaveLength(1);
|
|
174
|
+
expect(eventNode.body[0].action).toBe('toggle');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('sets sourceLanguage to json', () => {
|
|
178
|
+
const node = jsonToSemanticNode({
|
|
179
|
+
action: 'toggle',
|
|
180
|
+
roles: { patient: { type: 'selector', value: '.active' } },
|
|
181
|
+
});
|
|
182
|
+
expect(node.metadata?.sourceLanguage).toBe('json');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('semanticNodeToJSON', () => {
|
|
187
|
+
it('converts a basic command', () => {
|
|
188
|
+
const node = createCommandNode('toggle', {
|
|
189
|
+
patient: createSelector('.active'),
|
|
190
|
+
});
|
|
191
|
+
const json = semanticNodeToJSON(node);
|
|
192
|
+
expect(json.action).toBe('toggle');
|
|
193
|
+
expect(json.roles.patient).toEqual({ type: 'selector', value: '.active' });
|
|
194
|
+
expect(json.trigger).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('converts multiple role types', () => {
|
|
198
|
+
const node = createCommandNode('set', {
|
|
199
|
+
destination: createLiteral('count', 'string'),
|
|
200
|
+
goal: createLiteral(42, 'number'),
|
|
201
|
+
});
|
|
202
|
+
const json = semanticNodeToJSON(node);
|
|
203
|
+
expect(json.roles.destination).toEqual({ type: 'literal', value: 'count' });
|
|
204
|
+
expect(json.roles.goal).toEqual({ type: 'literal', value: 42 });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('handles reference values', () => {
|
|
208
|
+
const node = createCommandNode('add', {
|
|
209
|
+
patient: createSelector('.active'),
|
|
210
|
+
destination: createReference('me'),
|
|
211
|
+
});
|
|
212
|
+
const json = semanticNodeToJSON(node);
|
|
213
|
+
expect(json.roles.destination).toEqual({ type: 'reference', value: 'me' });
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('flag support in JSON', () => {
|
|
218
|
+
it('validates flag type as valid', () => {
|
|
219
|
+
const input: SemanticJSON = {
|
|
220
|
+
action: 'column',
|
|
221
|
+
roles: {
|
|
222
|
+
'primary-key': { type: 'flag', value: true },
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
expect(validateSemanticJSON(input)).toEqual([]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('converts flag JSON to FlagValue', () => {
|
|
229
|
+
const input: SemanticJSON = {
|
|
230
|
+
action: 'column',
|
|
231
|
+
roles: {
|
|
232
|
+
name: { type: 'literal', value: 'id' },
|
|
233
|
+
'primary-key': { type: 'flag', value: true },
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
const node = jsonToSemanticNode(input);
|
|
237
|
+
expect(node.roles.get('primary-key')).toEqual({
|
|
238
|
+
type: 'flag',
|
|
239
|
+
name: 'true',
|
|
240
|
+
enabled: true,
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('converts FlagValue to JSON', () => {
|
|
245
|
+
const node = createCommandNode('column', {
|
|
246
|
+
name: createLiteral('id', 'string'),
|
|
247
|
+
'primary-key': createFlag('primary-key', true),
|
|
248
|
+
'not-null': createFlag('not-null', true),
|
|
249
|
+
});
|
|
250
|
+
const json = semanticNodeToJSON(node);
|
|
251
|
+
expect(json.roles['primary-key']).toEqual({ type: 'flag', value: true });
|
|
252
|
+
expect(json.roles['not-null']).toEqual({ type: 'flag', value: true });
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('converts disabled FlagValue to JSON', () => {
|
|
256
|
+
const node = createCommandNode('field', {
|
|
257
|
+
nullable: createFlag('nullable', false),
|
|
258
|
+
});
|
|
259
|
+
const json = semanticNodeToJSON(node);
|
|
260
|
+
expect(json.roles.nullable).toEqual({ type: 'flag', value: false });
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('round-trip: JSON → SemanticNode → JSON', () => {
|
|
265
|
+
it('preserves command structure', () => {
|
|
266
|
+
const original: SemanticJSON = {
|
|
267
|
+
action: 'toggle',
|
|
268
|
+
roles: {
|
|
269
|
+
patient: { type: 'selector', value: '.active' },
|
|
270
|
+
destination: { type: 'selector', value: '#button' },
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
const node = jsonToSemanticNode(original);
|
|
274
|
+
const roundTripped = semanticNodeToJSON(node);
|
|
275
|
+
expect(roundTripped.action).toBe(original.action);
|
|
276
|
+
expect(roundTripped.roles.patient.type).toBe('selector');
|
|
277
|
+
expect(roundTripped.roles.patient.value).toBe('.active');
|
|
278
|
+
expect(roundTripped.roles.destination.type).toBe('selector');
|
|
279
|
+
expect(roundTripped.roles.destination.value).toBe('#button');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM JSON Schema Conversion (DEPRECATED)
|
|
3
|
+
*
|
|
4
|
+
* These functions are superseded by the unified protocol JSON functions in
|
|
5
|
+
* protocol-json.ts. `fromProtocolJSON()` now accepts optional `kind` (defaults
|
|
6
|
+
* to "command") and `trigger` sugar, making these wrappers unnecessary.
|
|
7
|
+
*
|
|
8
|
+
* Use instead:
|
|
9
|
+
* - `validateProtocolJSON()` instead of `validateSemanticJSON()`
|
|
10
|
+
* - `fromProtocolJSON()` instead of `jsonToSemanticNode()`
|
|
11
|
+
* - `toProtocolJSON()` instead of `semanticNodeToJSON()`
|
|
12
|
+
*
|
|
13
|
+
* These functions are kept for backward compatibility and will be removed
|
|
14
|
+
* in a future major version.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
SemanticNode,
|
|
19
|
+
SemanticValue,
|
|
20
|
+
SemanticRole,
|
|
21
|
+
EventHandlerSemanticNode,
|
|
22
|
+
CompoundSemanticNode,
|
|
23
|
+
} from '../core/types';
|
|
24
|
+
import {
|
|
25
|
+
createCommandNode,
|
|
26
|
+
createEventHandlerNode,
|
|
27
|
+
createSelector,
|
|
28
|
+
createLiteral,
|
|
29
|
+
createReference,
|
|
30
|
+
createExpression,
|
|
31
|
+
createFlag,
|
|
32
|
+
} from '../core/types';
|
|
33
|
+
import type { SemanticJSON, SemanticJSONValue, IRDiagnostic } from './types';
|
|
34
|
+
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Validation
|
|
37
|
+
// =============================================================================
|
|
38
|
+
|
|
39
|
+
const VALID_VALUE_TYPES = new Set([
|
|
40
|
+
'selector',
|
|
41
|
+
'literal',
|
|
42
|
+
'reference',
|
|
43
|
+
'expression',
|
|
44
|
+
'property-path',
|
|
45
|
+
'flag',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Validate LLM JSON input structure.
|
|
50
|
+
* Returns diagnostics (empty array = valid).
|
|
51
|
+
* @deprecated Use `validateProtocolJSON()` from `protocol-json.ts` instead.
|
|
52
|
+
*/
|
|
53
|
+
export function validateSemanticJSON(input: SemanticJSON): IRDiagnostic[] {
|
|
54
|
+
const diagnostics: IRDiagnostic[] = [];
|
|
55
|
+
|
|
56
|
+
// Action is required and must be a string
|
|
57
|
+
if (!input.action || typeof input.action !== 'string') {
|
|
58
|
+
diagnostics.push({
|
|
59
|
+
severity: 'error',
|
|
60
|
+
code: 'INVALID_ACTION',
|
|
61
|
+
message: 'Field "action" is required and must be a string.',
|
|
62
|
+
suggestion: 'Provide a command name like "toggle", "add", "set", etc.',
|
|
63
|
+
});
|
|
64
|
+
return diagnostics; // Can't proceed without action
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Roles validation
|
|
68
|
+
if (input.roles && typeof input.roles === 'object') {
|
|
69
|
+
for (const [role, value] of Object.entries(input.roles)) {
|
|
70
|
+
if (!value || typeof value !== 'object') {
|
|
71
|
+
diagnostics.push({
|
|
72
|
+
severity: 'error',
|
|
73
|
+
code: 'INVALID_ROLE_VALUE',
|
|
74
|
+
message: `Role "${role}" must be an object with "type" and "value" fields.`,
|
|
75
|
+
suggestion: 'Use: { "type": "selector", "value": ".active" }',
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const v = value as SemanticJSONValue;
|
|
81
|
+
if (!VALID_VALUE_TYPES.has(v.type)) {
|
|
82
|
+
diagnostics.push({
|
|
83
|
+
severity: 'error',
|
|
84
|
+
code: 'INVALID_VALUE_TYPE',
|
|
85
|
+
message: `Role "${role}" has invalid type "${v.type}".`,
|
|
86
|
+
suggestion: 'Valid types: selector, literal, reference, expression, property-path.',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (v.value === undefined || v.value === null) {
|
|
91
|
+
diagnostics.push({
|
|
92
|
+
severity: 'error',
|
|
93
|
+
code: 'MISSING_VALUE',
|
|
94
|
+
message: `Role "${role}" is missing the "value" field.`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Trigger validation (optional)
|
|
101
|
+
if (input.trigger) {
|
|
102
|
+
if (!input.trigger.event || typeof input.trigger.event !== 'string') {
|
|
103
|
+
diagnostics.push({
|
|
104
|
+
severity: 'error',
|
|
105
|
+
code: 'INVALID_TRIGGER',
|
|
106
|
+
message: 'Trigger "event" is required and must be a string.',
|
|
107
|
+
suggestion: 'Use an event name like "click", "mouseover", "keydown".',
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return diagnostics;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// =============================================================================
|
|
116
|
+
// JSON → SemanticNode
|
|
117
|
+
// =============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Convert validated SemanticJSON to a SemanticNode.
|
|
121
|
+
*
|
|
122
|
+
* Returns a properly typed SemanticNode using the framework's factory functions.
|
|
123
|
+
* If `trigger` is present, wraps the command in an event handler node.
|
|
124
|
+
* @deprecated Use `fromProtocolJSON()` from `protocol-json.ts` instead.
|
|
125
|
+
*/
|
|
126
|
+
export function jsonToSemanticNode(input: SemanticJSON): SemanticNode {
|
|
127
|
+
const roles = new Map<SemanticRole, SemanticValue>();
|
|
128
|
+
|
|
129
|
+
if (input.roles) {
|
|
130
|
+
for (const [role, value] of Object.entries(input.roles)) {
|
|
131
|
+
roles.set(role as SemanticRole, convertJSONValue(value));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Event handler wrapping
|
|
136
|
+
if (input.trigger) {
|
|
137
|
+
// Set the event role
|
|
138
|
+
roles.set('event' as SemanticRole, createLiteral(input.trigger.event, 'string'));
|
|
139
|
+
|
|
140
|
+
// Build body as a single command node
|
|
141
|
+
const bodyRoles = new Map(roles);
|
|
142
|
+
bodyRoles.delete('event' as SemanticRole);
|
|
143
|
+
const bodyNode = createCommandNode(input.action, bodyRoles);
|
|
144
|
+
|
|
145
|
+
return createEventHandlerNode(
|
|
146
|
+
'on',
|
|
147
|
+
roles,
|
|
148
|
+
[bodyNode],
|
|
149
|
+
{ sourceLanguage: 'json' },
|
|
150
|
+
(input.trigger.modifiers as Record<string, unknown> | undefined) ?? {}
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return createCommandNode(input.action, roles, {
|
|
155
|
+
sourceLanguage: 'json',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// =============================================================================
|
|
160
|
+
// SemanticNode → JSON
|
|
161
|
+
// =============================================================================
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Convert a SemanticNode to SemanticJSON format.
|
|
165
|
+
*
|
|
166
|
+
* This is the inverse of jsonToSemanticNode(). For event handlers,
|
|
167
|
+
* the event is extracted into the `trigger` field.
|
|
168
|
+
* @deprecated Use `toProtocolJSON()` from `protocol-json.ts` instead.
|
|
169
|
+
*/
|
|
170
|
+
export function semanticNodeToJSON(node: SemanticNode): SemanticJSON {
|
|
171
|
+
if (node.kind === 'compound') {
|
|
172
|
+
// Compound nodes: use first statement's action, flatten roles
|
|
173
|
+
const compound = node as CompoundSemanticNode;
|
|
174
|
+
if (compound.statements.length > 0) {
|
|
175
|
+
return semanticNodeToJSON(compound.statements[0]);
|
|
176
|
+
}
|
|
177
|
+
return { action: 'compound', roles: {} };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (node.kind === 'event-handler') {
|
|
181
|
+
const eventNode = node as EventHandlerSemanticNode;
|
|
182
|
+
const bodyAction = eventNode.body?.[0]?.action ?? node.action;
|
|
183
|
+
const roles: Record<string, SemanticJSONValue> = {};
|
|
184
|
+
|
|
185
|
+
// Convert body's roles (not the event handler's)
|
|
186
|
+
if (eventNode.body?.[0]) {
|
|
187
|
+
for (const [role, value] of eventNode.body[0].roles) {
|
|
188
|
+
roles[role] = semanticValueToJSON(value);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Extract event from roles
|
|
193
|
+
const eventValue = node.roles.get('event');
|
|
194
|
+
const eventName = eventValue
|
|
195
|
+
? 'value' in eventValue
|
|
196
|
+
? String(eventValue.value)
|
|
197
|
+
: 'raw' in eventValue
|
|
198
|
+
? eventValue.raw
|
|
199
|
+
: 'unknown'
|
|
200
|
+
: 'unknown';
|
|
201
|
+
|
|
202
|
+
const result: SemanticJSON = {
|
|
203
|
+
action: bodyAction === 'on' ? (eventNode.body?.[0]?.action ?? 'on') : bodyAction,
|
|
204
|
+
roles,
|
|
205
|
+
trigger: {
|
|
206
|
+
event: eventName,
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
if (eventNode.eventModifiers && Object.keys(eventNode.eventModifiers).length > 0) {
|
|
211
|
+
result.trigger!.modifiers = eventNode.eventModifiers as Record<string, unknown>;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Regular command
|
|
218
|
+
const roles: Record<string, SemanticJSONValue> = {};
|
|
219
|
+
for (const [role, value] of node.roles) {
|
|
220
|
+
roles[role] = semanticValueToJSON(value);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return { action: node.action, roles };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// =============================================================================
|
|
227
|
+
// Internal Helpers
|
|
228
|
+
// =============================================================================
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Convert a SemanticJSONValue to a SemanticValue.
|
|
232
|
+
*/
|
|
233
|
+
function convertJSONValue(value: SemanticJSONValue): SemanticValue {
|
|
234
|
+
switch (value.type) {
|
|
235
|
+
case 'selector':
|
|
236
|
+
return createSelector(String(value.value), detectSelectorKind(String(value.value)));
|
|
237
|
+
|
|
238
|
+
case 'literal':
|
|
239
|
+
return createLiteral(
|
|
240
|
+
value.value,
|
|
241
|
+
typeof value.value === 'number'
|
|
242
|
+
? 'number'
|
|
243
|
+
: typeof value.value === 'boolean'
|
|
244
|
+
? 'boolean'
|
|
245
|
+
: 'string'
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
case 'reference':
|
|
249
|
+
return createReference(String(value.value));
|
|
250
|
+
|
|
251
|
+
case 'expression':
|
|
252
|
+
return createExpression(String(value.value));
|
|
253
|
+
|
|
254
|
+
case 'property-path':
|
|
255
|
+
// Property paths in JSON are represented as dot-separated strings
|
|
256
|
+
return createExpression(String(value.value));
|
|
257
|
+
|
|
258
|
+
case 'flag':
|
|
259
|
+
return createFlag(String(value.value), true);
|
|
260
|
+
|
|
261
|
+
default:
|
|
262
|
+
return createLiteral(String(value.value), 'string');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Convert a SemanticValue to a SemanticJSONValue.
|
|
268
|
+
*/
|
|
269
|
+
function semanticValueToJSON(value: SemanticValue): SemanticJSONValue {
|
|
270
|
+
switch (value.type) {
|
|
271
|
+
case 'selector':
|
|
272
|
+
return { type: 'selector', value: value.value };
|
|
273
|
+
|
|
274
|
+
case 'literal':
|
|
275
|
+
return { type: 'literal', value: value.value };
|
|
276
|
+
|
|
277
|
+
case 'reference':
|
|
278
|
+
return { type: 'reference', value: value.value };
|
|
279
|
+
|
|
280
|
+
case 'expression':
|
|
281
|
+
return { type: 'expression', value: value.raw };
|
|
282
|
+
|
|
283
|
+
case 'property-path':
|
|
284
|
+
return {
|
|
285
|
+
type: 'property-path',
|
|
286
|
+
value: `${semanticValueToJSON(value.object).value}.${value.property}`,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
case 'flag':
|
|
290
|
+
return { type: 'flag', value: value.enabled };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function detectSelectorKind(selector: string): 'id' | 'class' | 'attribute' | 'complex' {
|
|
295
|
+
if (selector.startsWith('#')) return 'id';
|
|
296
|
+
if (selector.startsWith('.')) return 'class';
|
|
297
|
+
if (selector.startsWith('[')) return 'attribute';
|
|
298
|
+
return 'complex';
|
|
299
|
+
}
|