@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.
Files changed (54) hide show
  1. package/dist/api/index.js +29 -516
  2. package/dist/api/index.js.map +1 -1
  3. package/dist/core/index.js +20 -128
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/pattern-matching/index.js +19 -12
  6. package/dist/core/pattern-matching/index.js.map +1 -1
  7. package/dist/core/tokenization/index.js.map +1 -1
  8. package/dist/core/types.d.ts +7 -354
  9. package/dist/core/types.d.ts.map +1 -1
  10. package/dist/core/types.js +21 -129
  11. package/dist/core/types.js.map +1 -1
  12. package/dist/generation/diagnostics.d.ts +5 -115
  13. package/dist/generation/diagnostics.d.ts.map +1 -1
  14. package/dist/generation/index.js +20 -77
  15. package/dist/generation/index.js.map +1 -1
  16. package/dist/index.cjs +189 -1458
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.js +108 -1347
  19. package/dist/index.js.map +1 -1
  20. package/dist/interfaces/value-extractor.d.ts +20 -0
  21. package/dist/interfaces/value-extractor.d.ts.map +1 -1
  22. package/dist/ir/explicit-parser.d.ts +2 -83
  23. package/dist/ir/explicit-parser.d.ts.map +1 -1
  24. package/dist/ir/explicit-renderer.d.ts +2 -31
  25. package/dist/ir/explicit-renderer.d.ts.map +1 -1
  26. package/dist/ir/index.js +89 -1260
  27. package/dist/ir/index.js.map +1 -1
  28. package/dist/ir/protocol-json.d.ts +2 -70
  29. package/dist/ir/protocol-json.d.ts.map +1 -1
  30. package/dist/ir/references.d.ts +2 -16
  31. package/dist/ir/references.d.ts.map +1 -1
  32. package/dist/ir/types.d.ts +2 -148
  33. package/dist/ir/types.d.ts.map +1 -1
  34. package/dist/parsing/index.js +19 -12
  35. package/dist/parsing/index.js.map +1 -1
  36. package/dist/schema/command-schema.d.ts +3 -92
  37. package/dist/schema/command-schema.d.ts.map +1 -1
  38. package/dist/schema/index.js +3 -20
  39. package/dist/schema/index.js.map +1 -1
  40. package/dist/testing/index.js +1066 -712
  41. package/dist/testing/index.js.map +1 -1
  42. package/package.json +6 -2
  43. package/src/core/types.ts +71 -600
  44. package/src/generation/diagnostics.ts +12 -233
  45. package/src/interfaces/value-extractor.ts +32 -0
  46. package/src/ir/explicit-parser.ts +12 -792
  47. package/src/ir/explicit-renderer.ts +2 -195
  48. package/src/ir/from-interchange.test.ts +91 -4
  49. package/src/ir/from-interchange.ts +59 -0
  50. package/src/ir/protocol-json.test.ts +16 -7
  51. package/src/ir/protocol-json.ts +9 -748
  52. package/src/ir/references.ts +2 -30
  53. package/src/ir/types.ts +18 -190
  54. package/src/schema/command-schema.ts +3 -134
@@ -1,198 +1,5 @@
1
1
  /**
2
- * Explicit Syntax Renderer
3
- *
4
- * Serializes SemanticNode to the universal [command role:value ...] bracket syntax.
5
- * Zero dependencies beyond core types — no language-specific logic.
6
- *
7
- * Also renders annotations (@name(value)) and multi-line documents.
2
+ * Explicit Syntax Renderer — re-exported from @lokascript/intent
8
3
  */
9
4
 
10
- import type {
11
- SemanticNode,
12
- SemanticValue,
13
- CommandSemanticNode,
14
- CompoundSemanticNode,
15
- EventHandlerSemanticNode,
16
- ConditionalSemanticNode,
17
- LoopSemanticNode,
18
- LSEEnvelope,
19
- } from '../core/types';
20
-
21
- /**
22
- * Render a semantic node as explicit bracket syntax.
23
- *
24
- * Handles annotations (v1.2) — if the node has annotations, they are
25
- * prepended as @name or @name(value) before the bracket output.
26
- *
27
- * @example
28
- * ```typescript
29
- * renderExplicit(node) // "[toggle patient:.active destination:#button]"
30
- * // With annotations:
31
- * renderExplicit(annotatedNode) // "@timeout(5s) [fetch source:\"/api/users\"]"
32
- * ```
33
- */
34
- export function renderExplicit(node: SemanticNode): string {
35
- let result: string;
36
-
37
- // Handle compound nodes
38
- if (node.kind === 'compound') {
39
- const compoundNode = node as CompoundSemanticNode;
40
- const renderedStatements = compoundNode.statements.map(stmt => renderExplicit(stmt));
41
- result = renderedStatements.join(` ${compoundNode.chainType} `);
42
- } else {
43
- result = renderBracketCommand(node);
44
- }
45
-
46
- // Prepend annotations if present
47
- if (node.annotations && node.annotations.length > 0) {
48
- const annParts = node.annotations.map(ann =>
49
- ann.value !== undefined ? `@${ann.name}(${ann.value})` : `@${ann.name}`
50
- );
51
- return annParts.join(' ') + ' ' + result;
52
- }
53
-
54
- return result;
55
- }
56
-
57
- /**
58
- * Render an LSEEnvelope as a multi-line document string.
59
- *
60
- * @example
61
- * ```typescript
62
- * renderDocument(envelope)
63
- * // "#!lse 1.2\n[toggle patient:.active]\n[add patient:.highlight]"
64
- * ```
65
- */
66
- export function renderDocument(envelope: LSEEnvelope): string {
67
- const lines: string[] = [];
68
-
69
- // Emit version header if not the default
70
- if (envelope.lseVersion && envelope.lseVersion !== '1.0') {
71
- lines.push(`#!lse ${envelope.lseVersion}`);
72
- }
73
-
74
- // Emit each node on its own line
75
- for (const node of envelope.nodes) {
76
- lines.push(renderExplicit(node));
77
- }
78
-
79
- return lines.join('\n');
80
- }
81
-
82
- // =============================================================================
83
- // Internal Helpers
84
- // =============================================================================
85
-
86
- /**
87
- * Render a single bracket command (non-compound node).
88
- */
89
- function renderBracketCommand(node: SemanticNode): string {
90
- const parts: string[] = [node.action];
91
-
92
- // Add roles
93
- for (const [role, value] of node.roles) {
94
- if (value.type === 'flag') {
95
- parts.push(value.enabled ? `+${value.name}` : `~${value.name}`);
96
- } else {
97
- parts.push(`${role}:${valueToString(value)}`);
98
- }
99
- }
100
-
101
- // Handle event handler body
102
- if (node.kind === 'event-handler') {
103
- const eventNode = node as EventHandlerSemanticNode;
104
- if (eventNode.body && eventNode.body.length > 0) {
105
- const bodyParts = eventNode.body.map(n => renderExplicit(n));
106
- parts.push(`body:${bodyParts.join(' ')}`);
107
- }
108
- }
109
-
110
- // Handle conditional branches (v1.1)
111
- if (node.kind === 'conditional') {
112
- const condNode = node as ConditionalSemanticNode;
113
- if (condNode.thenBranch && condNode.thenBranch.length > 0) {
114
- const thenParts = condNode.thenBranch.map(n => renderExplicit(n));
115
- parts.push(`then:${thenParts.join(' ')}`);
116
- }
117
- if (condNode.elseBranch && condNode.elseBranch.length > 0) {
118
- const elseParts = condNode.elseBranch.map(n => renderExplicit(n));
119
- parts.push(`else:${elseParts.join(' ')}`);
120
- }
121
- }
122
-
123
- // Handle loop body and metadata (v1.1)
124
- if (node.kind === 'loop') {
125
- const loopNode = node as LoopSemanticNode;
126
- if (loopNode.loopVariant) {
127
- parts.push(`loop-variant:${loopNode.loopVariant}`);
128
- }
129
- if (loopNode.loopVariable) {
130
- parts.push(`variable:${loopNode.loopVariable}`);
131
- }
132
- if (loopNode.indexVariable) {
133
- parts.push(`index-variable:${loopNode.indexVariable}`);
134
- }
135
- if (loopNode.body && loopNode.body.length > 0) {
136
- const bodyParts = loopNode.body.map(n => renderExplicit(n));
137
- parts.push(`loop-body:${bodyParts.join(' ')}`);
138
- }
139
- }
140
-
141
- // Handle v1.2 command fields (try/catch/finally, async, match)
142
- if (node.kind === 'command') {
143
- const cmd = node as CommandSemanticNode;
144
- if (cmd.body && cmd.body.length > 0) {
145
- const bodyParts = cmd.body.map(n => renderExplicit(n));
146
- parts.push(`body:${bodyParts.join(' ')}`);
147
- }
148
- if (cmd.catchBranch && cmd.catchBranch.length > 0) {
149
- const catchParts = cmd.catchBranch.map(n => renderExplicit(n));
150
- parts.push(`catch:${catchParts.join(' ')}`);
151
- }
152
- if (cmd.finallyBranch && cmd.finallyBranch.length > 0) {
153
- const finallyParts = cmd.finallyBranch.map(n => renderExplicit(n));
154
- parts.push(`finally:${finallyParts.join(' ')}`);
155
- }
156
- if (cmd.asyncVariant) {
157
- parts.push(`async-variant:${cmd.asyncVariant}`);
158
- }
159
- if (cmd.asyncBody && cmd.asyncBody.length > 0) {
160
- const asyncParts = cmd.asyncBody.map(n => renderExplicit(n));
161
- parts.push(`async-body:${asyncParts.join(' ')}`);
162
- }
163
- }
164
-
165
- return `[${parts.join(' ')}]`;
166
- }
167
-
168
- /**
169
- * Convert a semantic value to its explicit syntax string form.
170
- */
171
- function valueToString(value: SemanticValue): string {
172
- switch (value.type) {
173
- case 'literal':
174
- if (typeof value.value === 'string') {
175
- // Quote strings that contain spaces or are explicitly typed as strings
176
- if (value.dataType === 'string' || /\s/.test(value.value)) {
177
- return `"${value.value}"`;
178
- }
179
- return value.value;
180
- }
181
- return String(value.value);
182
-
183
- case 'selector':
184
- return value.value;
185
-
186
- case 'reference':
187
- return value.value;
188
-
189
- case 'property-path':
190
- return `${valueToString(value.object)}'s ${value.property}`;
191
-
192
- case 'expression':
193
- return value.raw;
194
-
195
- case 'flag':
196
- return value.name;
197
- }
198
- }
5
+ export { renderExplicit, renderDocument } from '@lokascript/intent';
@@ -427,6 +427,89 @@ describe('fromInterchangeNode: command conversion', () => {
427
427
  });
428
428
  });
429
429
 
430
+ it('infers destination role for scroll, skipping `to` marker', () => {
431
+ const node = fromInterchangeNode({
432
+ type: 'command',
433
+ name: 'scroll',
434
+ args: [
435
+ { type: 'identifier', value: 'to', name: 'to' },
436
+ { type: 'selector', value: '#target' },
437
+ ],
438
+ });
439
+
440
+ expect(node.action).toBe('scroll');
441
+ expect(node.roles.get('destination')).toEqual({
442
+ type: 'selector',
443
+ value: '#target',
444
+ selectorKind: 'id',
445
+ });
446
+ expect(node.roles.has('patient')).toBe(false);
447
+ });
448
+
449
+ it('infers destination for scroll across position/behavior markers', () => {
450
+ const node = fromInterchangeNode({
451
+ type: 'command',
452
+ name: 'scroll',
453
+ args: [
454
+ { type: 'identifier', value: 'to', name: 'to' },
455
+ { type: 'identifier', value: 'bottom', name: 'bottom' },
456
+ { type: 'identifier', value: 'of', name: 'of' },
457
+ { type: 'selector', value: '#chat' },
458
+ { type: 'identifier', value: 'smoothly', name: 'smoothly' },
459
+ ],
460
+ });
461
+
462
+ expect(node.roles.get('destination')).toEqual({
463
+ type: 'selector',
464
+ value: '#chat',
465
+ selectorKind: 'id',
466
+ });
467
+ });
468
+
469
+ it('infers patient role for push, skipping `url` marker', () => {
470
+ const node = fromInterchangeNode({
471
+ type: 'command',
472
+ name: 'push',
473
+ args: [
474
+ { type: 'identifier', value: 'url', name: 'url' },
475
+ { type: 'literal', value: '/page/2' },
476
+ ],
477
+ });
478
+
479
+ expect(node.action).toBe('push');
480
+ expect(node.roles.get('patient')).toEqual({ type: 'literal', value: '/page/2' });
481
+ });
482
+
483
+ it('infers patient role for replace, skipping `url` marker', () => {
484
+ const node = fromInterchangeNode({
485
+ type: 'command',
486
+ name: 'replace',
487
+ args: [
488
+ { type: 'identifier', value: 'url', name: 'url' },
489
+ { type: 'literal', value: '/new' },
490
+ ],
491
+ });
492
+
493
+ expect(node.action).toBe('replace');
494
+ expect(node.roles.get('patient')).toEqual({ type: 'literal', value: '/new' });
495
+ });
496
+
497
+ it('infers patient role for process, skipping `partials in` markers', () => {
498
+ const node = fromInterchangeNode({
499
+ type: 'command',
500
+ name: 'process',
501
+ args: [
502
+ { type: 'identifier', value: 'partials', name: 'partials' },
503
+ { type: 'identifier', value: 'in', name: 'in' },
504
+ { type: 'identifier', value: 'it' },
505
+ ],
506
+ });
507
+
508
+ expect(node.action).toBe('process');
509
+ // `it` is in DEFAULT_REFERENCES → wrapped as a reference value
510
+ expect(node.roles.get('patient')).toEqual({ type: 'reference', value: 'it' });
511
+ });
512
+
430
513
  it('wraps bare expression nodes as get command', () => {
431
514
  const node = fromInterchangeNode({
432
515
  type: 'binary',
@@ -759,17 +842,21 @@ describe('round-trip: InterchangeNode → SemanticNode → protocol JSON', () =>
759
842
  });
760
843
  });
761
844
 
762
- it('event handler produces valid protocol JSON', () => {
845
+ it('event handler produces valid protocol JSON (compact trigger form)', () => {
763
846
  const node = fromInterchangeNode({
764
847
  type: 'event',
765
848
  event: 'click',
766
849
  body: [{ type: 'command', name: 'log', args: [{ type: 'literal', value: 'clicked' }] }],
767
850
  });
768
851
 
852
+ // Single-command bodies are emitted in the compact form — the body
853
+ // command's action is hoisted to the top level and the event name moves
854
+ // under `trigger.event`. See protocol.ts:canEmitCompactTrigger.
769
855
  const proto = toProtocolJSON(node);
770
- expect(proto.kind).toBe('event-handler');
771
- expect(proto.body).toHaveLength(1);
772
- expect(proto.body![0].action).toBe('log');
856
+ expect(proto.kind).toBeUndefined();
857
+ expect(proto.action).toBe('log');
858
+ expect(proto.trigger).toEqual({ event: 'click' });
859
+ expect(proto.body).toBeUndefined();
773
860
  });
774
861
 
775
862
  it('if/else produces valid protocol JSON', () => {
@@ -428,6 +428,43 @@ export function renderExpr(node: INode): string {
428
428
  // Role Inference
429
429
  // =============================================================================
430
430
 
431
+ /**
432
+ * Marker-keyword sets — mirror the runtime command parsers' skip lists so the
433
+ * inferred roles point at the real value, not the literal marker word.
434
+ */
435
+ const SCROLL_MARKERS: ReadonlySet<string> = new Set([
436
+ 'to',
437
+ 'of',
438
+ 'the',
439
+ 'top',
440
+ 'bottom',
441
+ 'middle',
442
+ 'center',
443
+ 'nearest',
444
+ 'left',
445
+ 'right',
446
+ 'smoothly',
447
+ 'instantly',
448
+ ]);
449
+ const URL_MARKERS: ReadonlySet<string> = new Set(['url']);
450
+ const PARTIALS_IN_MARKERS: ReadonlySet<string> = new Set(['partials', 'in']);
451
+
452
+ /**
453
+ * Return the first arg that isn't a marker identifier from the given set.
454
+ * Identifier args carry the marker word in `name` (or `value` as a fallback).
455
+ */
456
+ function firstNonMarkerArg(args: INode[], markers: ReadonlySet<string>): INode | undefined {
457
+ for (const a of args) {
458
+ if (a.type === 'identifier') {
459
+ const ident = a as { name?: unknown; value?: unknown };
460
+ const word = typeof ident.name === 'string' ? ident.name : ident.value;
461
+ if (typeof word === 'string' && markers.has(word)) continue;
462
+ }
463
+ return a;
464
+ }
465
+ return undefined;
466
+ }
467
+
431
468
  /**
432
469
  * Infer semantic roles from command name, positional args, modifiers, and target.
433
470
  * Mirrors the heuristics in core's from-core.ts inferRoles() function.
@@ -526,6 +563,28 @@ function inferRoles(
526
563
  break;
527
564
  }
528
565
 
566
+ // scroll [to] [position] target [smoothly|instantly] → destination=target
567
+ case 'scroll': {
568
+ const t = firstNonMarkerArg(args, SCROLL_MARKERS);
569
+ if (t) roles.destination = convertValue(t);
570
+ break;
571
+ }
572
+
573
+ // push|replace url <X> → patient=X
574
+ case 'push':
575
+ case 'replace': {
576
+ const t = firstNonMarkerArg(args, URL_MARKERS);
577
+ if (t) roles.patient = convertValue(t);
578
+ break;
579
+ }
580
+
581
+ // process partials in <X> → patient=X
582
+ case 'process': {
583
+ const t = firstNonMarkerArg(args, PARTIALS_IN_MARKERS);
584
+ if (t) roles.patient = convertValue(t);
585
+ break;
586
+ }
587
+
529
588
  default: {
530
589
  // Generic fallback: first arg as patient, target as destination
531
590
  if (args[0]) roles.patient = convertValue(args[0]);
@@ -136,20 +136,25 @@ describe('toProtocolJSON', () => {
136
136
  });
137
137
 
138
138
  describe('event-handler node', () => {
139
- it('serializes body array recursively', () => {
139
+ it('serializes single-command body in compact form (trigger sugar)', () => {
140
+ // Single-command bodies round-trip through the compact form —
141
+ // `{action, roles, trigger: {event}}` — which is also what the runtime
142
+ // dispatch path reliably executes. See protocol.ts:canEmitCompactTrigger.
140
143
  const body = createCommandNode('toggle', { patient: createSelector('.active') });
141
144
  const node = createEventHandlerNode('on', { event: createLiteral('click', 'string') }, [
142
145
  body,
143
146
  ]);
144
147
  const json = toProtocolJSON(node);
145
- expect(json.kind).toBe('event-handler');
146
- expect(json.action).toBe('on');
147
- expect(json.body).toHaveLength(1);
148
- expect(json.body![0].kind).toBe('command');
149
- expect(json.body![0].action).toBe('toggle');
148
+ expect(json.kind).toBeUndefined(); // defaults to 'command'
149
+ expect(json.action).toBe('toggle'); // body action is hoisted
150
+ expect(json.trigger).toEqual({ event: 'click' });
151
+ expect(json.roles).toMatchObject({ patient: { type: 'selector', value: '.active' } });
152
+ expect(json.body).toBeUndefined();
150
153
  });
151
154
 
152
- it('serializes nested event-handler body', () => {
155
+ it('falls back to verbose form for multi-command event-handler body', () => {
156
+ // Compact form can only represent a single body command; multi-command
157
+ // bodies still use the verbose `kind: event-handler` form.
153
158
  const inner = createCommandNode('add', { patient: createSelector('.highlight') });
154
159
  const outer = createCommandNode('remove', { patient: createSelector('.active') });
155
160
  const node = createEventHandlerNode('on', { event: createLiteral('click', 'string') }, [
@@ -157,8 +162,12 @@ describe('toProtocolJSON', () => {
157
162
  outer,
158
163
  ]);
159
164
  const json = toProtocolJSON(node);
165
+ expect(json.kind).toBe('event-handler');
166
+ expect(json.action).toBe('on');
160
167
  expect(json.body).toHaveLength(2);
168
+ expect(json.body![0].action).toBe('add');
161
169
  expect(json.body![1].action).toBe('remove');
170
+ expect(json.trigger).toBeUndefined();
162
171
  });
163
172
  });
164
173