@lokascript/framework 2.3.1 → 2.4.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/api/index.js +29 -516
- package/dist/api/index.js.map +1 -1
- package/dist/core/index.js +20 -128
- package/dist/core/index.js.map +1 -1
- package/dist/core/pattern-matching/index.js +19 -12
- package/dist/core/pattern-matching/index.js.map +1 -1
- package/dist/core/tokenization/index.js.map +1 -1
- package/dist/core/types.d.ts +7 -354
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +21 -129
- package/dist/core/types.js.map +1 -1
- package/dist/generation/diagnostics.d.ts +5 -115
- package/dist/generation/diagnostics.d.ts.map +1 -1
- package/dist/generation/index.js +20 -77
- package/dist/generation/index.js.map +1 -1
- package/dist/index.cjs +189 -1458
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +108 -1347
- package/dist/index.js.map +1 -1
- package/dist/interfaces/value-extractor.d.ts +20 -0
- package/dist/interfaces/value-extractor.d.ts.map +1 -1
- package/dist/ir/explicit-parser.d.ts +2 -83
- package/dist/ir/explicit-parser.d.ts.map +1 -1
- package/dist/ir/explicit-renderer.d.ts +2 -31
- package/dist/ir/explicit-renderer.d.ts.map +1 -1
- package/dist/ir/index.js +89 -1260
- package/dist/ir/index.js.map +1 -1
- package/dist/ir/protocol-json.d.ts +2 -70
- package/dist/ir/protocol-json.d.ts.map +1 -1
- package/dist/ir/references.d.ts +2 -16
- package/dist/ir/references.d.ts.map +1 -1
- package/dist/ir/types.d.ts +2 -148
- package/dist/ir/types.d.ts.map +1 -1
- package/dist/parsing/index.js +19 -12
- package/dist/parsing/index.js.map +1 -1
- package/dist/schema/command-schema.d.ts +3 -92
- package/dist/schema/command-schema.d.ts.map +1 -1
- package/dist/schema/index.js +3 -20
- package/dist/schema/index.js.map +1 -1
- package/dist/testing/index.js +1066 -712
- package/dist/testing/index.js.map +1 -1
- package/package.json +6 -2
- package/src/core/types.ts +71 -600
- package/src/generation/diagnostics.ts +12 -233
- package/src/interfaces/value-extractor.ts +32 -0
- package/src/ir/explicit-parser.ts +12 -792
- package/src/ir/explicit-renderer.ts +2 -195
- package/src/ir/from-interchange.test.ts +91 -4
- package/src/ir/from-interchange.ts +59 -0
- package/src/ir/protocol-json.test.ts +16 -7
- package/src/ir/protocol-json.ts +9 -748
- package/src/ir/references.ts +2 -30
- package/src/ir/types.ts +18 -190
- package/src/schema/command-schema.ts +3 -134
package/src/ir/protocol-json.ts
CHANGED
|
@@ -1,751 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Protocol JSON Serialization
|
|
3
|
-
*
|
|
4
|
-
* Converts between TypeScript SemanticNode (internal representation) and
|
|
5
|
-
* the protocol wire format defined in protocol/spec/wire-format.md.
|
|
6
|
-
*
|
|
7
|
-
* `kind` is optional and defaults to `"command"` when absent.
|
|
8
|
-
* `trigger` is accepted as convenience sugar for wrapping a command in an
|
|
9
|
-
* event handler (e.g., `{ action: "toggle", trigger: { event: "click" } }`).
|
|
10
|
-
*
|
|
11
|
-
* The TypeScript layer is a superset of the protocol spec:
|
|
12
|
-
* - TS has 5 node kinds; protocol has 3 (command, event-handler, compound)
|
|
13
|
-
* - TS uses ReadonlyMap<SemanticRole, SemanticValue>; protocol uses plain Record
|
|
14
|
-
* - TS SelectorValue has optional selectorKind; protocol preserves it when present (v1.1)
|
|
15
|
-
* - TS has PropertyPathValue; protocol flattens it to expression
|
|
16
|
-
* - TS conditional/loop nodes are losslessly encoded as command nodes with
|
|
17
|
-
* thenBranch/elseBranch/loopVariant/loopBody/loopVariable/indexVariable fields (v1.1)
|
|
18
|
-
* - v1.2 adds: annotations, diagnostics, try/catch/finally, async all/race,
|
|
19
|
-
* match/arms, pipe chainType, and LSEEnvelope
|
|
20
|
-
*
|
|
21
|
-
* Use toProtocolJSON() to serialize for tool-to-tool interchange.
|
|
22
|
-
* Use fromProtocolJSON() to deserialize from protocol-conformant sources.
|
|
2
|
+
* Protocol JSON Serialization — re-exported from @lokascript/intent
|
|
23
3
|
*/
|
|
24
4
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
LoopSemanticNode,
|
|
34
|
-
LoopVariant,
|
|
35
|
-
AsyncVariant,
|
|
36
|
-
LSEEnvelope,
|
|
37
|
-
} from '../core/types';
|
|
38
|
-
import {
|
|
39
|
-
createCommandNode,
|
|
40
|
-
createEventHandlerNode,
|
|
41
|
-
createCompoundNode,
|
|
42
|
-
createConditionalNode,
|
|
43
|
-
createLoopNode,
|
|
44
|
-
createTryNode,
|
|
45
|
-
createAsyncNode,
|
|
46
|
-
createMatchNode,
|
|
47
|
-
createSelector,
|
|
48
|
-
createLiteral,
|
|
49
|
-
createReference,
|
|
50
|
-
createExpression,
|
|
51
|
-
createFlag,
|
|
52
|
-
extractValue,
|
|
53
|
-
} from '../core/types';
|
|
54
|
-
import type {
|
|
55
|
-
ProtocolNodeJSON,
|
|
56
|
-
ProtocolValueJSON,
|
|
57
|
-
ProtocolDiagnosticJSON,
|
|
58
|
-
ProtocolChainType,
|
|
59
|
-
IRDiagnostic,
|
|
60
|
-
LSEEnvelopeJSON,
|
|
61
|
-
} from './types';
|
|
62
|
-
|
|
63
|
-
// =============================================================================
|
|
64
|
-
// Serialization: SemanticNode → ProtocolNodeJSON
|
|
65
|
-
// =============================================================================
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Serialize a SemanticNode to the protocol full-fidelity JSON wire format.
|
|
69
|
-
*
|
|
70
|
-
* TS-only node kinds are losslessly encoded as command nodes (v1.1):
|
|
71
|
-
* - `conditional` → `command` with thenBranch/elseBranch arrays
|
|
72
|
-
* - `loop` → `command` with loopVariant/loopBody/loopVariable/indexVariable fields
|
|
73
|
-
*
|
|
74
|
-
* v1.2 fields (annotations, diagnostics, try/catch/finally, async, match)
|
|
75
|
-
* are serialized when present on the source node.
|
|
76
|
-
*
|
|
77
|
-
* TS-only value fields:
|
|
78
|
-
* - `property-path` flattened to `expression` using extractValue()
|
|
79
|
-
*/
|
|
80
|
-
export function toProtocolJSON(node: SemanticNode): ProtocolNodeJSON {
|
|
81
|
-
const roles = rolesToProtocol(node.roles);
|
|
82
|
-
let result: ProtocolNodeJSON;
|
|
83
|
-
|
|
84
|
-
switch (node.kind) {
|
|
85
|
-
case 'command': {
|
|
86
|
-
const cmd = node as CommandSemanticNode;
|
|
87
|
-
result = { kind: 'command', action: node.action, roles };
|
|
88
|
-
// v1.2: try/catch/finally
|
|
89
|
-
if (cmd.body && cmd.body.length > 0) {
|
|
90
|
-
result.body = cmd.body.map(toProtocolJSON);
|
|
91
|
-
}
|
|
92
|
-
if (cmd.catchBranch && cmd.catchBranch.length > 0) {
|
|
93
|
-
result.catchBranch = cmd.catchBranch.map(toProtocolJSON);
|
|
94
|
-
}
|
|
95
|
-
if (cmd.finallyBranch && cmd.finallyBranch.length > 0) {
|
|
96
|
-
result.finallyBranch = cmd.finallyBranch.map(toProtocolJSON);
|
|
97
|
-
}
|
|
98
|
-
// v1.2: async coordination
|
|
99
|
-
if (cmd.asyncVariant) {
|
|
100
|
-
result.asyncVariant = cmd.asyncVariant;
|
|
101
|
-
}
|
|
102
|
-
if (cmd.asyncBody && cmd.asyncBody.length > 0) {
|
|
103
|
-
result.asyncBody = cmd.asyncBody.map(toProtocolJSON);
|
|
104
|
-
}
|
|
105
|
-
// v1.2: match
|
|
106
|
-
if (cmd.arms && cmd.arms.length > 0) {
|
|
107
|
-
result.arms = cmd.arms.map(arm => ({
|
|
108
|
-
pattern: valueToProtocol(arm.pattern),
|
|
109
|
-
body: arm.body.map(toProtocolJSON),
|
|
110
|
-
}));
|
|
111
|
-
}
|
|
112
|
-
if (cmd.defaultArm && cmd.defaultArm.length > 0) {
|
|
113
|
-
result.defaultArm = cmd.defaultArm.map(toProtocolJSON);
|
|
114
|
-
}
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
case 'event-handler': {
|
|
119
|
-
const eh = node as EventHandlerSemanticNode;
|
|
120
|
-
result = {
|
|
121
|
-
kind: 'event-handler',
|
|
122
|
-
action: node.action,
|
|
123
|
-
roles,
|
|
124
|
-
body: eh.body.map(toProtocolJSON),
|
|
125
|
-
};
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
case 'compound': {
|
|
130
|
-
const c = node as CompoundSemanticNode;
|
|
131
|
-
result = {
|
|
132
|
-
kind: 'compound',
|
|
133
|
-
action: 'compound',
|
|
134
|
-
statements: c.statements.map(toProtocolJSON),
|
|
135
|
-
chainType: c.chainType,
|
|
136
|
-
} as ProtocolNodeJSON;
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
case 'conditional': {
|
|
141
|
-
const cond = node as ConditionalSemanticNode;
|
|
142
|
-
result = { kind: 'command', action: node.action, roles };
|
|
143
|
-
result.thenBranch = cond.thenBranch.map(toProtocolJSON);
|
|
144
|
-
if (cond.elseBranch && cond.elseBranch.length > 0) {
|
|
145
|
-
result.elseBranch = cond.elseBranch.map(toProtocolJSON);
|
|
146
|
-
}
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
case 'loop': {
|
|
151
|
-
const loop = node as LoopSemanticNode;
|
|
152
|
-
result = { kind: 'command', action: node.action, roles };
|
|
153
|
-
result.loopVariant = loop.loopVariant;
|
|
154
|
-
result.loopBody = loop.body.map(toProtocolJSON);
|
|
155
|
-
if (loop.loopVariable) result.loopVariable = loop.loopVariable;
|
|
156
|
-
if (loop.indexVariable) result.indexVariable = loop.indexVariable;
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// v1.2: annotations (all node kinds)
|
|
162
|
-
if (node.annotations && node.annotations.length > 0) {
|
|
163
|
-
result.annotations = node.annotations.map(a =>
|
|
164
|
-
a.value !== undefined ? { name: a.name, value: a.value } : { name: a.name }
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// v1.2/v1.2.1: diagnostics (all node kinds)
|
|
169
|
-
if (node.diagnostics && node.diagnostics.length > 0) {
|
|
170
|
-
result.diagnostics = node.diagnostics.map(d => {
|
|
171
|
-
const json: ProtocolDiagnosticJSON = {
|
|
172
|
-
level: d.severity,
|
|
173
|
-
message: d.message,
|
|
174
|
-
};
|
|
175
|
-
if (d.code) json.code = d.code;
|
|
176
|
-
if (d.source) json.source = d.source;
|
|
177
|
-
if (d.suggestions && d.suggestions.length > 0) json.suggestions = [...d.suggestions];
|
|
178
|
-
return json;
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return result;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function rolesToProtocol(
|
|
186
|
-
roles: ReadonlyMap<SemanticRole, SemanticValue>
|
|
187
|
-
): Record<string, ProtocolValueJSON> {
|
|
188
|
-
const result: Record<string, ProtocolValueJSON> = {};
|
|
189
|
-
for (const [role, value] of roles) {
|
|
190
|
-
result[role] = valueToProtocol(value);
|
|
191
|
-
}
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function valueToProtocol(value: SemanticValue): ProtocolValueJSON {
|
|
196
|
-
switch (value.type) {
|
|
197
|
-
case 'selector': {
|
|
198
|
-
const PROTOCOL_SELECTOR_KINDS = new Set([
|
|
199
|
-
'id',
|
|
200
|
-
'class',
|
|
201
|
-
'attribute',
|
|
202
|
-
'element',
|
|
203
|
-
'complex',
|
|
204
|
-
] as const);
|
|
205
|
-
const sk = 'selectorKind' in value ? value.selectorKind : undefined;
|
|
206
|
-
const sv: ProtocolValueJSON = { type: 'selector', value: value.value };
|
|
207
|
-
if (sk && PROTOCOL_SELECTOR_KINDS.has(sk as 'id')) {
|
|
208
|
-
sv.selectorKind = sk as 'id' | 'class' | 'attribute' | 'element' | 'complex';
|
|
209
|
-
}
|
|
210
|
-
return sv;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
case 'literal':
|
|
214
|
-
return value.dataType
|
|
215
|
-
? { type: 'literal', value: value.value, dataType: value.dataType }
|
|
216
|
-
: { type: 'literal', value: value.value };
|
|
217
|
-
|
|
218
|
-
case 'reference':
|
|
219
|
-
return { type: 'reference', value: value.value };
|
|
220
|
-
|
|
221
|
-
case 'expression':
|
|
222
|
-
return { type: 'expression', raw: value.raw };
|
|
223
|
-
|
|
224
|
-
case 'property-path':
|
|
225
|
-
// Flatten to expression — property-path has no structured protocol representation
|
|
226
|
-
return { type: 'expression', raw: extractValue(value) };
|
|
227
|
-
|
|
228
|
-
case 'flag':
|
|
229
|
-
return { type: 'flag', name: value.name, enabled: value.enabled };
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// =============================================================================
|
|
234
|
-
// Deserialization: ProtocolNodeJSON → SemanticNode
|
|
235
|
-
// =============================================================================
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Deserialize a protocol wire format JSON object into a SemanticNode.
|
|
239
|
-
*
|
|
240
|
-
* Produces all 5 node kinds — detects v1.1/v1.2 fields on command nodes to
|
|
241
|
-
* reconstruct appropriate node types:
|
|
242
|
-
* - thenBranch present → `conditional` node via createConditionalNode()
|
|
243
|
-
* - loopVariant present → `loop` node via createLoopNode()
|
|
244
|
-
* - catchBranch/finallyBranch → try node via createTryNode() (v1.2)
|
|
245
|
-
* - asyncVariant → async node via createAsyncNode() (v1.2)
|
|
246
|
-
* - arms → match node via createMatchNode() (v1.2)
|
|
247
|
-
*
|
|
248
|
-
* Annotations and diagnostics (v1.2) are preserved on all node kinds.
|
|
249
|
-
*/
|
|
250
|
-
export function fromProtocolJSON(json: ProtocolNodeJSON): SemanticNode {
|
|
251
|
-
const roles = new Map<SemanticRole, SemanticValue>(
|
|
252
|
-
Object.entries(json.roles ?? {}).map(([role, val]) => [role, valueFromProtocol(val)])
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
// Deserialize v1.2 metadata (applies to all node kinds)
|
|
256
|
-
const annotations = json.annotations?.map(a =>
|
|
257
|
-
a.value !== undefined ? { name: a.name, value: a.value } : { name: a.name }
|
|
258
|
-
);
|
|
259
|
-
const diagnostics = json.diagnostics?.map(d => ({
|
|
260
|
-
severity: d.level as 'error' | 'warning' | 'info',
|
|
261
|
-
message: d.message,
|
|
262
|
-
...(d.code ? { code: d.code } : {}),
|
|
263
|
-
...(d.source ? { source: d.source } : {}),
|
|
264
|
-
...(d.suggestions && d.suggestions.length > 0 ? { suggestions: d.suggestions } : {}),
|
|
265
|
-
}));
|
|
266
|
-
|
|
267
|
-
// trigger sugar: wrap command in event handler (check before kind dispatch)
|
|
268
|
-
if (json.trigger) {
|
|
269
|
-
const eventName = json.trigger.event;
|
|
270
|
-
roles.set('event' as SemanticRole, createLiteral(eventName, 'string'));
|
|
271
|
-
const bodyRoles = new Map(roles);
|
|
272
|
-
bodyRoles.delete('event' as SemanticRole);
|
|
273
|
-
const bodyNode = createCommandNode(json.action, bodyRoles);
|
|
274
|
-
const node = createEventHandlerNode(
|
|
275
|
-
'on',
|
|
276
|
-
roles,
|
|
277
|
-
[bodyNode],
|
|
278
|
-
{ sourceLanguage: 'json' },
|
|
279
|
-
(json.trigger.modifiers as Record<string, unknown> | undefined) ?? {}
|
|
280
|
-
);
|
|
281
|
-
return applyV12Metadata(node, annotations, diagnostics);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const kind = json.kind ?? 'command';
|
|
285
|
-
|
|
286
|
-
if (kind === 'event-handler') {
|
|
287
|
-
const body = (json.body ?? []).map(fromProtocolJSON);
|
|
288
|
-
const node = createEventHandlerNode(json.action, roles, body);
|
|
289
|
-
return applyV12Metadata(node, annotations, diagnostics);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (kind === 'compound') {
|
|
293
|
-
const statements = (json.statements ?? []).map(fromProtocolJSON);
|
|
294
|
-
const node = createCompoundNode(
|
|
295
|
-
statements,
|
|
296
|
-
(json.chainType ?? 'sequential') as ProtocolChainType
|
|
297
|
-
);
|
|
298
|
-
return applyV12Metadata(node, annotations, diagnostics);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Detect v1.1 conditional encoding: command with thenBranch
|
|
302
|
-
if (json.thenBranch && json.thenBranch.length > 0) {
|
|
303
|
-
const thenBranch = json.thenBranch.map(fromProtocolJSON);
|
|
304
|
-
const elseBranch = json.elseBranch ? json.elseBranch.map(fromProtocolJSON) : undefined;
|
|
305
|
-
const node = createConditionalNode(json.action, roles, thenBranch, elseBranch);
|
|
306
|
-
return applyV12Metadata(node, annotations, diagnostics);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Detect v1.1 loop encoding: command with loopVariant
|
|
310
|
-
if (json.loopVariant) {
|
|
311
|
-
const loopBody = (json.loopBody ?? []).map(fromProtocolJSON);
|
|
312
|
-
const node = createLoopNode(
|
|
313
|
-
json.action,
|
|
314
|
-
roles,
|
|
315
|
-
json.loopVariant as LoopVariant,
|
|
316
|
-
loopBody,
|
|
317
|
-
json.loopVariable,
|
|
318
|
-
json.indexVariable
|
|
319
|
-
);
|
|
320
|
-
return applyV12Metadata(node, annotations, diagnostics);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// v1.2: Detect try/catch/finally (body + catchBranch or finallyBranch)
|
|
324
|
-
if (json.catchBranch || json.finallyBranch) {
|
|
325
|
-
const body = (json.body ?? []).map(fromProtocolJSON);
|
|
326
|
-
const catchBranch = json.catchBranch ? json.catchBranch.map(fromProtocolJSON) : undefined;
|
|
327
|
-
const finallyBranch = json.finallyBranch ? json.finallyBranch.map(fromProtocolJSON) : undefined;
|
|
328
|
-
const node = createTryNode(body, catchBranch, finallyBranch);
|
|
329
|
-
return applyV12Metadata(node, annotations, diagnostics);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// v1.2: Detect async coordination (asyncVariant)
|
|
333
|
-
if (json.asyncVariant) {
|
|
334
|
-
const asyncBody = (json.asyncBody ?? []).map(fromProtocolJSON);
|
|
335
|
-
const node = createAsyncNode(json.asyncVariant as AsyncVariant, asyncBody);
|
|
336
|
-
return applyV12Metadata(node, annotations, diagnostics);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// v1.2: Detect match (arms)
|
|
340
|
-
if (json.arms && json.arms.length > 0) {
|
|
341
|
-
const arms = json.arms.map(arm => ({
|
|
342
|
-
pattern: valueFromProtocol(arm.pattern),
|
|
343
|
-
body: arm.body.map(fromProtocolJSON),
|
|
344
|
-
}));
|
|
345
|
-
const defaultArm = json.defaultArm ? json.defaultArm.map(fromProtocolJSON) : undefined;
|
|
346
|
-
const node = createMatchNode(roles, arms, defaultArm);
|
|
347
|
-
return applyV12Metadata(node, annotations, diagnostics);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// v1.2: command with body only (try with no catch/finally)
|
|
351
|
-
if (json.body && json.body.length > 0 && kind === 'command') {
|
|
352
|
-
const body = json.body.map(fromProtocolJSON);
|
|
353
|
-
const node = createTryNode(body);
|
|
354
|
-
return applyV12Metadata(node, annotations, diagnostics);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Plain command
|
|
358
|
-
const node = createCommandNode(json.action, roles);
|
|
359
|
-
return applyV12Metadata(node, annotations, diagnostics);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Apply v1.2 annotations and diagnostics to a node.
|
|
364
|
-
* Returns the node unchanged if neither is present.
|
|
365
|
-
*/
|
|
366
|
-
function applyV12Metadata(
|
|
367
|
-
node: SemanticNode,
|
|
368
|
-
annotations: Array<{ name: string; value?: string }> | undefined,
|
|
369
|
-
diagnostics:
|
|
370
|
-
| Array<{
|
|
371
|
-
severity: 'error' | 'warning' | 'info';
|
|
372
|
-
message: string;
|
|
373
|
-
code?: string;
|
|
374
|
-
source?: string;
|
|
375
|
-
suggestions?: string[];
|
|
376
|
-
}>
|
|
377
|
-
| undefined
|
|
378
|
-
): SemanticNode {
|
|
379
|
-
if ((!annotations || annotations.length === 0) && (!diagnostics || diagnostics.length === 0)) {
|
|
380
|
-
return node;
|
|
381
|
-
}
|
|
382
|
-
return {
|
|
383
|
-
...node,
|
|
384
|
-
...(annotations && annotations.length > 0 ? { annotations } : {}),
|
|
385
|
-
...(diagnostics && diagnostics.length > 0 ? { diagnostics } : {}),
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function valueFromProtocol(value: ProtocolValueJSON): SemanticValue {
|
|
390
|
-
switch (value.type) {
|
|
391
|
-
case 'selector':
|
|
392
|
-
return createSelector(String(value.value ?? ''), value.selectorKind);
|
|
393
|
-
|
|
394
|
-
case 'literal':
|
|
395
|
-
return createLiteral(
|
|
396
|
-
value.value !== undefined ? value.value : '',
|
|
397
|
-
value.dataType as 'string' | 'number' | 'boolean' | 'duration' | undefined
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
case 'reference':
|
|
401
|
-
return createReference(String(value.value ?? ''));
|
|
402
|
-
|
|
403
|
-
case 'expression':
|
|
404
|
-
return createExpression(value.raw ?? String(value.value ?? ''));
|
|
405
|
-
|
|
406
|
-
case 'flag':
|
|
407
|
-
return createFlag(
|
|
408
|
-
value.name ?? String(value.value ?? ''),
|
|
409
|
-
value.enabled ?? (typeof value.value === 'boolean' ? value.value : true)
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
case 'property-path':
|
|
413
|
-
// Protocol property-path has no structured form; restore as expression
|
|
414
|
-
return createExpression(String(value.value ?? value.raw ?? ''));
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// =============================================================================
|
|
419
|
-
// Envelope (v1.2)
|
|
420
|
-
// =============================================================================
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Serialize an LSEEnvelope to its JSON wire format.
|
|
424
|
-
*/
|
|
425
|
-
export function toEnvelopeJSON(envelope: LSEEnvelope): LSEEnvelopeJSON {
|
|
426
|
-
const result: LSEEnvelopeJSON = {
|
|
427
|
-
lseVersion: envelope.lseVersion,
|
|
428
|
-
nodes: envelope.nodes.map(toProtocolJSON),
|
|
429
|
-
};
|
|
430
|
-
if (envelope.features && envelope.features.length > 0) {
|
|
431
|
-
result.features = [...envelope.features];
|
|
432
|
-
}
|
|
433
|
-
return result;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Deserialize an LSEEnvelopeJSON into an LSEEnvelope.
|
|
438
|
-
*/
|
|
439
|
-
export function fromEnvelopeJSON(json: LSEEnvelopeJSON): LSEEnvelope {
|
|
440
|
-
return {
|
|
441
|
-
lseVersion: json.lseVersion,
|
|
442
|
-
...(json.features && json.features.length > 0 ? { features: json.features } : {}),
|
|
443
|
-
nodes: json.nodes.map(fromProtocolJSON),
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Check whether an unknown JSON value is an LSE envelope (has lseVersion + nodes).
|
|
449
|
-
*/
|
|
450
|
-
export function isEnvelope(json: unknown): json is LSEEnvelopeJSON {
|
|
451
|
-
return (
|
|
452
|
-
typeof json === 'object' &&
|
|
453
|
-
json !== null &&
|
|
454
|
-
'lseVersion' in json &&
|
|
455
|
-
'nodes' in json &&
|
|
456
|
-
Array.isArray((json as Record<string, unknown>).nodes)
|
|
457
|
-
);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// =============================================================================
|
|
461
|
-
// Validation
|
|
462
|
-
// =============================================================================
|
|
463
|
-
|
|
464
|
-
const VALID_KINDS = new Set(['command', 'event-handler', 'compound']);
|
|
465
|
-
const VALID_VALUE_TYPES = new Set([
|
|
466
|
-
'selector',
|
|
467
|
-
'literal',
|
|
468
|
-
'reference',
|
|
469
|
-
'expression',
|
|
470
|
-
'property-path',
|
|
471
|
-
'flag',
|
|
472
|
-
]);
|
|
473
|
-
const VALID_CHAIN_TYPES = new Set(['then', 'and', 'async', 'sequential', 'pipe']);
|
|
474
|
-
const VALID_ASYNC_VARIANTS = new Set(['all', 'race']);
|
|
475
|
-
const VALID_DIAGNOSTIC_LEVELS = new Set(['error', 'warning', 'info']);
|
|
476
|
-
|
|
477
|
-
/**
|
|
478
|
-
* Validate that an unknown value conforms to the protocol JSON format.
|
|
479
|
-
* `kind` is optional (defaults to `"command"`). `trigger` is accepted as
|
|
480
|
-
* convenience sugar for wrapping a command in an event handler.
|
|
481
|
-
* Returns an array of diagnostics (empty array = valid).
|
|
482
|
-
*/
|
|
483
|
-
export function validateProtocolJSON(json: unknown): IRDiagnostic[] {
|
|
484
|
-
const diagnostics: IRDiagnostic[] = [];
|
|
485
|
-
|
|
486
|
-
if (typeof json !== 'object' || json === null) {
|
|
487
|
-
return [{ severity: 'error', code: 'INVALID_ROOT', message: 'Root must be a JSON object' }];
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const node = json as Record<string, unknown>;
|
|
491
|
-
|
|
492
|
-
// kind (optional, defaults to "command")
|
|
493
|
-
const kind = 'kind' in node ? String(node.kind) : 'command';
|
|
494
|
-
if ('kind' in node && !VALID_KINDS.has(kind)) {
|
|
495
|
-
diagnostics.push({
|
|
496
|
-
severity: 'error',
|
|
497
|
-
code: 'INVALID_KIND',
|
|
498
|
-
message: `Invalid kind "${node.kind}". Must be one of: command, event-handler, compound`,
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// action
|
|
503
|
-
if (!('action' in node)) {
|
|
504
|
-
diagnostics.push({
|
|
505
|
-
severity: 'error',
|
|
506
|
-
code: 'MISSING_ACTION',
|
|
507
|
-
message: 'Missing required field: action',
|
|
508
|
-
});
|
|
509
|
-
} else if (typeof node.action !== 'string' || node.action.length === 0) {
|
|
510
|
-
diagnostics.push({
|
|
511
|
-
severity: 'error',
|
|
512
|
-
code: 'INVALID_ACTION',
|
|
513
|
-
message: 'action must be a non-empty string',
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// roles (optional on compound nodes)
|
|
518
|
-
if (!('roles' in node) && kind !== 'compound') {
|
|
519
|
-
diagnostics.push({
|
|
520
|
-
severity: 'error',
|
|
521
|
-
code: 'MISSING_ROLES',
|
|
522
|
-
message: 'Missing required field: roles',
|
|
523
|
-
});
|
|
524
|
-
} else if (
|
|
525
|
-
'roles' in node &&
|
|
526
|
-
(typeof node.roles !== 'object' || node.roles === null || Array.isArray(node.roles))
|
|
527
|
-
) {
|
|
528
|
-
diagnostics.push({
|
|
529
|
-
severity: 'error',
|
|
530
|
-
code: 'INVALID_ROLES',
|
|
531
|
-
message: 'roles must be a plain object',
|
|
532
|
-
});
|
|
533
|
-
} else if ('roles' in node && node.roles) {
|
|
534
|
-
for (const [role, value] of Object.entries(node.roles as Record<string, unknown>)) {
|
|
535
|
-
if (typeof value !== 'object' || value === null) {
|
|
536
|
-
diagnostics.push({
|
|
537
|
-
severity: 'error',
|
|
538
|
-
code: 'INVALID_ROLE_VALUE',
|
|
539
|
-
message: `Role "${role}" value must be an object`,
|
|
540
|
-
});
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
const v = value as Record<string, unknown>;
|
|
544
|
-
if (!('type' in v) || !VALID_VALUE_TYPES.has(String(v.type))) {
|
|
545
|
-
diagnostics.push({
|
|
546
|
-
severity: 'error',
|
|
547
|
-
code: 'INVALID_VALUE_TYPE',
|
|
548
|
-
message: `Role "${role}" has invalid type "${v.type}"`,
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
// Flags: accept { name, enabled } or { value } (simplified form)
|
|
552
|
-
if (v.type === 'flag') {
|
|
553
|
-
const hasName = typeof v.name === 'string';
|
|
554
|
-
const hasEnabled = typeof v.enabled === 'boolean';
|
|
555
|
-
const hasValue = 'value' in v;
|
|
556
|
-
if (!hasName && !hasValue) {
|
|
557
|
-
diagnostics.push({
|
|
558
|
-
severity: 'error',
|
|
559
|
-
code: 'MISSING_FLAG_NAME',
|
|
560
|
-
message: `Flag role "${role}" missing required field: name (or value)`,
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
if (!hasEnabled && !hasValue) {
|
|
564
|
-
diagnostics.push({
|
|
565
|
-
severity: 'error',
|
|
566
|
-
code: 'MISSING_FLAG_ENABLED',
|
|
567
|
-
message: `Flag role "${role}" missing required field: enabled (or value)`,
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
// Expressions: accept { raw } or { value } (simplified form)
|
|
572
|
-
if (v.type === 'expression' && typeof v.raw !== 'string' && !('value' in v)) {
|
|
573
|
-
diagnostics.push({
|
|
574
|
-
severity: 'error',
|
|
575
|
-
code: 'MISSING_EXPRESSION_RAW',
|
|
576
|
-
message: `Expression role "${role}" missing required field: raw (or value)`,
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
// trigger validation (convenience sugar for event handlers)
|
|
583
|
-
if ('trigger' in node && node.trigger) {
|
|
584
|
-
const trigger = node.trigger as Record<string, unknown>;
|
|
585
|
-
if (typeof trigger !== 'object' || trigger === null) {
|
|
586
|
-
diagnostics.push({
|
|
587
|
-
severity: 'error',
|
|
588
|
-
code: 'INVALID_TRIGGER',
|
|
589
|
-
message: 'trigger must be an object',
|
|
590
|
-
});
|
|
591
|
-
} else if (!trigger.event || typeof trigger.event !== 'string') {
|
|
592
|
-
diagnostics.push({
|
|
593
|
-
severity: 'error',
|
|
594
|
-
code: 'INVALID_TRIGGER',
|
|
595
|
-
message: 'trigger.event is required and must be a non-empty string',
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// event-handler requires body array (unless using trigger sugar)
|
|
601
|
-
if (kind === 'event-handler' && !('trigger' in node)) {
|
|
602
|
-
if (!('body' in node) || !Array.isArray(node.body)) {
|
|
603
|
-
diagnostics.push({
|
|
604
|
-
severity: 'error',
|
|
605
|
-
code: 'MISSING_BODY',
|
|
606
|
-
message: 'event-handler node requires a body array',
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// compound requires statements array
|
|
612
|
-
if (kind === 'compound') {
|
|
613
|
-
if (!('statements' in node) || !Array.isArray(node.statements)) {
|
|
614
|
-
diagnostics.push({
|
|
615
|
-
severity: 'error',
|
|
616
|
-
code: 'MISSING_STATEMENTS',
|
|
617
|
-
message: 'compound node requires a statements array',
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
if ('chainType' in node && !VALID_CHAIN_TYPES.has(String(node.chainType))) {
|
|
621
|
-
diagnostics.push({
|
|
622
|
-
severity: 'error',
|
|
623
|
-
code: 'INVALID_CHAIN_TYPE',
|
|
624
|
-
message: `Invalid chainType "${node.chainType}". Must be one of: then, and, async, sequential, pipe`,
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// v1.2: validate diagnostics array
|
|
630
|
-
if ('diagnostics' in node) {
|
|
631
|
-
if (!Array.isArray(node.diagnostics)) {
|
|
632
|
-
diagnostics.push({
|
|
633
|
-
severity: 'error',
|
|
634
|
-
code: 'INVALID_DIAGNOSTICS',
|
|
635
|
-
message: 'diagnostics must be an array',
|
|
636
|
-
});
|
|
637
|
-
} else {
|
|
638
|
-
for (let i = 0; i < (node.diagnostics as unknown[]).length; i++) {
|
|
639
|
-
const d = (node.diagnostics as unknown[])[i] as Record<string, unknown> | null;
|
|
640
|
-
if (!d || typeof d !== 'object') {
|
|
641
|
-
diagnostics.push({
|
|
642
|
-
severity: 'error',
|
|
643
|
-
code: 'INVALID_DIAGNOSTIC_ENTRY',
|
|
644
|
-
message: `diagnostics[${i}] must be an object`,
|
|
645
|
-
});
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
if (!VALID_DIAGNOSTIC_LEVELS.has(String(d.level))) {
|
|
649
|
-
diagnostics.push({
|
|
650
|
-
severity: 'error',
|
|
651
|
-
code: 'INVALID_DIAGNOSTIC_LEVEL',
|
|
652
|
-
message: `diagnostics[${i}].level must be "error", "warning", or "info"`,
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
if (typeof d.message !== 'string') {
|
|
656
|
-
diagnostics.push({
|
|
657
|
-
severity: 'error',
|
|
658
|
-
code: 'MISSING_DIAGNOSTIC_MESSAGE',
|
|
659
|
-
message: `diagnostics[${i}] missing required field: message`,
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
// v1.2.1: role and code are now optional
|
|
663
|
-
if ('code' in d && typeof d.code !== 'string') {
|
|
664
|
-
diagnostics.push({
|
|
665
|
-
severity: 'warning',
|
|
666
|
-
code: 'INVALID_DIAGNOSTIC_CODE',
|
|
667
|
-
message: `diagnostics[${i}].code must be a string if present`,
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// v1.2: validate annotations array
|
|
675
|
-
if ('annotations' in node) {
|
|
676
|
-
if (!Array.isArray(node.annotations)) {
|
|
677
|
-
diagnostics.push({
|
|
678
|
-
severity: 'error',
|
|
679
|
-
code: 'INVALID_ANNOTATIONS',
|
|
680
|
-
message: 'annotations must be an array',
|
|
681
|
-
});
|
|
682
|
-
} else {
|
|
683
|
-
for (let i = 0; i < (node.annotations as unknown[]).length; i++) {
|
|
684
|
-
const a = (node.annotations as unknown[])[i] as Record<string, unknown> | null;
|
|
685
|
-
if (!a || typeof a !== 'object') {
|
|
686
|
-
diagnostics.push({
|
|
687
|
-
severity: 'error',
|
|
688
|
-
code: 'INVALID_ANNOTATION_ENTRY',
|
|
689
|
-
message: `annotations[${i}] must be an object`,
|
|
690
|
-
});
|
|
691
|
-
continue;
|
|
692
|
-
}
|
|
693
|
-
if (typeof a.name !== 'string' || a.name.length === 0) {
|
|
694
|
-
diagnostics.push({
|
|
695
|
-
severity: 'error',
|
|
696
|
-
code: 'MISSING_ANNOTATION_NAME',
|
|
697
|
-
message: `annotations[${i}] missing required field: name`,
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// v1.2: validate asyncVariant
|
|
705
|
-
if ('asyncVariant' in node && !VALID_ASYNC_VARIANTS.has(String(node.asyncVariant))) {
|
|
706
|
-
diagnostics.push({
|
|
707
|
-
severity: 'error',
|
|
708
|
-
code: 'INVALID_ASYNC_VARIANT',
|
|
709
|
-
message: `Invalid asyncVariant "${node.asyncVariant}". Must be "all" or "race"`,
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// v1.2: validate arms
|
|
714
|
-
if ('arms' in node) {
|
|
715
|
-
if (!Array.isArray(node.arms)) {
|
|
716
|
-
diagnostics.push({
|
|
717
|
-
severity: 'error',
|
|
718
|
-
code: 'INVALID_ARMS',
|
|
719
|
-
message: 'arms must be an array',
|
|
720
|
-
});
|
|
721
|
-
} else {
|
|
722
|
-
for (let i = 0; i < (node.arms as unknown[]).length; i++) {
|
|
723
|
-
const arm = (node.arms as unknown[])[i] as Record<string, unknown> | null;
|
|
724
|
-
if (!arm || typeof arm !== 'object') {
|
|
725
|
-
diagnostics.push({
|
|
726
|
-
severity: 'error',
|
|
727
|
-
code: 'INVALID_ARM_ENTRY',
|
|
728
|
-
message: `arms[${i}] must be an object`,
|
|
729
|
-
});
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
if (!arm.pattern || typeof arm.pattern !== 'object') {
|
|
733
|
-
diagnostics.push({
|
|
734
|
-
severity: 'error',
|
|
735
|
-
code: 'MISSING_ARM_PATTERN',
|
|
736
|
-
message: `arms[${i}] missing required field: pattern`,
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
if (!Array.isArray(arm.body)) {
|
|
740
|
-
diagnostics.push({
|
|
741
|
-
severity: 'error',
|
|
742
|
-
code: 'MISSING_ARM_BODY',
|
|
743
|
-
message: `arms[${i}] missing required field: body (array)`,
|
|
744
|
-
});
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
return diagnostics;
|
|
751
|
-
}
|
|
5
|
+
export {
|
|
6
|
+
toProtocolJSON,
|
|
7
|
+
fromProtocolJSON,
|
|
8
|
+
validateProtocolJSON,
|
|
9
|
+
toEnvelopeJSON,
|
|
10
|
+
fromEnvelopeJSON,
|
|
11
|
+
isEnvelope,
|
|
12
|
+
} from '@lokascript/intent';
|