@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.
Files changed (126) hide show
  1. package/dist/aot/index.d.ts +1 -0
  2. package/dist/aot/index.d.ts.map +1 -1
  3. package/dist/aot/registry-bridge.d.ts +27 -0
  4. package/dist/aot/registry-bridge.d.ts.map +1 -0
  5. package/dist/api/create-dsl.d.ts.map +1 -1
  6. package/dist/api/dispatcher.d.ts +22 -0
  7. package/dist/api/dispatcher.d.ts.map +1 -1
  8. package/dist/api/domain-registry.d.ts +41 -0
  9. package/dist/api/domain-registry.d.ts.map +1 -1
  10. package/dist/api/index.js +1351 -21
  11. package/dist/api/index.js.map +1 -1
  12. package/dist/core/index.js +114 -10
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/pattern-matching/index.js +74 -10
  15. package/dist/core/pattern-matching/index.js.map +1 -1
  16. package/dist/core/pattern-matching/pattern-matcher.d.ts.map +1 -1
  17. package/dist/core/types.d.ts +80 -2
  18. package/dist/core/types.d.ts.map +1 -1
  19. package/dist/core/types.js +40 -0
  20. package/dist/core/types.js.map +1 -1
  21. package/dist/feedback/confidence-gate.d.ts +28 -0
  22. package/dist/feedback/confidence-gate.d.ts.map +1 -0
  23. package/dist/feedback/feedback-formatter.d.ts +21 -0
  24. package/dist/feedback/feedback-formatter.d.ts.map +1 -0
  25. package/dist/feedback/index.d.ts +11 -0
  26. package/dist/feedback/index.d.ts.map +1 -0
  27. package/dist/feedback/pattern-tracker.d.ts +47 -0
  28. package/dist/feedback/pattern-tracker.d.ts.map +1 -0
  29. package/dist/feedback/types.d.ts +113 -0
  30. package/dist/feedback/types.d.ts.map +1 -0
  31. package/dist/generation/index.js +2 -1
  32. package/dist/generation/index.js.map +1 -1
  33. package/dist/index.cjs +2685 -71
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.ts +6 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2652 -71
  38. package/dist/index.js.map +1 -1
  39. package/dist/ir/explicit-parser.d.ts +31 -0
  40. package/dist/ir/explicit-parser.d.ts.map +1 -0
  41. package/dist/ir/explicit-renderer.d.ts +17 -0
  42. package/dist/ir/explicit-renderer.d.ts.map +1 -0
  43. package/dist/ir/from-interchange.d.ts +51 -0
  44. package/dist/ir/from-interchange.d.ts.map +1 -0
  45. package/dist/ir/index.d.ts +29 -0
  46. package/dist/ir/index.d.ts.map +1 -0
  47. package/dist/ir/index.js +1557 -0
  48. package/dist/ir/index.js.map +1 -0
  49. package/dist/ir/json-schema.d.ts +40 -0
  50. package/dist/ir/json-schema.d.ts.map +1 -0
  51. package/dist/ir/protocol-json.d.ts +73 -0
  52. package/dist/ir/protocol-json.d.ts.map +1 -0
  53. package/dist/ir/references.d.ts +19 -0
  54. package/dist/ir/references.d.ts.map +1 -0
  55. package/dist/ir/types.d.ts +141 -0
  56. package/dist/ir/types.d.ts.map +1 -0
  57. package/dist/parsing/index.js +74 -10
  58. package/dist/parsing/index.js.map +1 -1
  59. package/dist/prompts/index.d.ts +9 -0
  60. package/dist/prompts/index.d.ts.map +1 -0
  61. package/dist/prompts/prompt-generator.d.ts +49 -0
  62. package/dist/prompts/prompt-generator.d.ts.map +1 -0
  63. package/dist/prompts/types.d.ts +64 -0
  64. package/dist/prompts/types.d.ts.map +1 -0
  65. package/dist/schema/command-schema.d.ts +17 -0
  66. package/dist/schema/command-schema.d.ts.map +1 -1
  67. package/dist/schema/index.js.map +1 -1
  68. package/dist/testing/index.js +3 -0
  69. package/dist/testing/index.js.map +1 -1
  70. package/dist/training/index.d.ts +11 -0
  71. package/dist/training/index.d.ts.map +1 -0
  72. package/dist/training/jsonl-writer.d.ts +42 -0
  73. package/dist/training/jsonl-writer.d.ts.map +1 -0
  74. package/dist/training/schema-synthesizer.d.ts +20 -0
  75. package/dist/training/schema-synthesizer.d.ts.map +1 -0
  76. package/dist/training/types.d.ts +73 -0
  77. package/dist/training/types.d.ts.map +1 -0
  78. package/package.json +5 -1
  79. package/src/aot/index.ts +1 -0
  80. package/src/aot/registry-bridge.test.ts +104 -0
  81. package/src/aot/registry-bridge.ts +66 -0
  82. package/src/api/create-dsl.test.ts +306 -0
  83. package/src/api/create-dsl.ts +21 -0
  84. package/src/api/dispatcher.test.ts +66 -0
  85. package/src/api/dispatcher.ts +77 -9
  86. package/src/api/domain-registry.ts +120 -4
  87. package/src/core/pattern-matching/pattern-matcher.test.ts +131 -0
  88. package/src/core/pattern-matching/pattern-matcher.ts +102 -19
  89. package/src/core/types.ts +145 -2
  90. package/src/feedback/confidence-gate.test.ts +111 -0
  91. package/src/feedback/confidence-gate.ts +70 -0
  92. package/src/feedback/feedback-formatter.test.ts +198 -0
  93. package/src/feedback/feedback-formatter.ts +298 -0
  94. package/src/feedback/index.ts +24 -0
  95. package/src/feedback/pattern-tracker.test.ts +198 -0
  96. package/src/feedback/pattern-tracker.ts +140 -0
  97. package/src/feedback/types.ts +149 -0
  98. package/src/generation/pattern-generator.ts +2 -2
  99. package/src/index.ts +22 -0
  100. package/src/integration/llm-lse-integration.test.ts +457 -0
  101. package/src/ir/explicit-parser.test.ts +306 -0
  102. package/src/ir/explicit-parser.ts +419 -0
  103. package/src/ir/explicit-renderer.test.ts +144 -0
  104. package/src/ir/explicit-renderer.ts +146 -0
  105. package/src/ir/from-interchange.test.ts +853 -0
  106. package/src/ir/from-interchange.ts +594 -0
  107. package/src/ir/index.ts +66 -0
  108. package/src/ir/json-schema.test.ts +281 -0
  109. package/src/ir/json-schema.ts +299 -0
  110. package/src/ir/protocol-json.test.ts +1185 -0
  111. package/src/ir/protocol-json.ts +745 -0
  112. package/src/ir/references.test.ts +38 -0
  113. package/src/ir/references.ts +33 -0
  114. package/src/ir/round-trip.test.ts +557 -0
  115. package/src/ir/types.ts +181 -0
  116. package/src/prompts/index.ts +15 -0
  117. package/src/prompts/prompt-generator.test.ts +386 -0
  118. package/src/prompts/prompt-generator.ts +531 -0
  119. package/src/prompts/types.ts +89 -0
  120. package/src/schema/command-schema.ts +19 -0
  121. package/src/training/index.ts +13 -0
  122. package/src/training/jsonl-writer.test.ts +138 -0
  123. package/src/training/jsonl-writer.ts +67 -0
  124. package/src/training/schema-synthesizer.test.ts +250 -0
  125. package/src/training/schema-synthesizer.ts +306 -0
  126. 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
+ }