@lokascript/framework 2.1.0 → 2.2.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 (52) hide show
  1. package/dist/api/index.js +111 -24
  2. package/dist/api/index.js.map +1 -1
  3. package/dist/core/index.js +133 -0
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/pattern-matching/index.js.map +1 -1
  6. package/dist/core/tokenization/index.js +133 -0
  7. package/dist/core/tokenization/index.js.map +1 -1
  8. package/dist/core/tokenization/morphology/base-normalizer.d.ts +94 -0
  9. package/dist/core/tokenization/morphology/base-normalizer.d.ts.map +1 -0
  10. package/dist/core/tokenization/morphology/index.d.ts +3 -1
  11. package/dist/core/tokenization/morphology/index.d.ts.map +1 -1
  12. package/dist/core/types.d.ts +3 -2
  13. package/dist/core/types.d.ts.map +1 -1
  14. package/dist/core/types.js.map +1 -1
  15. package/dist/generation/index.js.map +1 -1
  16. package/dist/index.cjs +564 -46
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +556 -46
  21. package/dist/index.js.map +1 -1
  22. package/dist/ir/explicit-parser.d.ts +62 -7
  23. package/dist/ir/explicit-parser.d.ts.map +1 -1
  24. package/dist/ir/explicit-renderer.d.ts +18 -1
  25. package/dist/ir/explicit-renderer.d.ts.map +1 -1
  26. package/dist/ir/index.d.ts +4 -2
  27. package/dist/ir/index.d.ts.map +1 -1
  28. package/dist/ir/index.js +423 -46
  29. package/dist/ir/index.js.map +1 -1
  30. package/dist/ir/protocol-json.d.ts.map +1 -1
  31. package/dist/ir/to-runtime-ast.d.ts +53 -0
  32. package/dist/ir/to-runtime-ast.d.ts.map +1 -0
  33. package/dist/ir/types.d.ts +14 -4
  34. package/dist/ir/types.d.ts.map +1 -1
  35. package/dist/parsing/index.js.map +1 -1
  36. package/dist/testing/index.js +8671 -8261
  37. package/dist/testing/index.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/core/tokenization/morphology/base-normalizer.ts +249 -0
  40. package/src/core/tokenization/morphology/index.ts +3 -1
  41. package/src/core/types.ts +4 -2
  42. package/src/index.ts +7 -0
  43. package/src/ir/conformance.test.ts +120 -0
  44. package/src/ir/explicit-parser.test.ts +307 -1
  45. package/src/ir/explicit-parser.ts +403 -29
  46. package/src/ir/explicit-renderer.test.ts +87 -2
  47. package/src/ir/explicit-renderer.ts +57 -5
  48. package/src/ir/index.ts +13 -2
  49. package/src/ir/protocol-json.test.ts +4 -2
  50. package/src/ir/protocol-json.ts +30 -24
  51. package/src/ir/to-runtime-ast.ts +203 -0
  52. package/src/ir/types.ts +14 -4
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Serializes SemanticNode to the universal [command role:value ...] bracket syntax.
5
5
  * Zero dependencies beyond core types — no language-specific logic.
6
+ *
7
+ * Also renders annotations (@name(value)) and multi-line documents.
6
8
  */
7
9
 
8
10
  import type {
@@ -13,24 +15,78 @@ import type {
13
15
  EventHandlerSemanticNode,
14
16
  ConditionalSemanticNode,
15
17
  LoopSemanticNode,
18
+ LSEEnvelope,
16
19
  } from '../core/types';
17
20
 
18
21
  /**
19
22
  * Render a semantic node as explicit bracket syntax.
20
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
+ *
21
27
  * @example
22
28
  * ```typescript
23
29
  * renderExplicit(node) // "[toggle patient:.active destination:#button]"
30
+ * // With annotations:
31
+ * renderExplicit(annotatedNode) // "@timeout(5s) [fetch source:\"/api/users\"]"
24
32
  * ```
25
33
  */
26
34
  export function renderExplicit(node: SemanticNode): string {
35
+ let result: string;
36
+
27
37
  // Handle compound nodes
28
38
  if (node.kind === 'compound') {
29
39
  const compoundNode = node as CompoundSemanticNode;
30
40
  const renderedStatements = compoundNode.statements.map(stmt => renderExplicit(stmt));
31
- return renderedStatements.join(` ${compoundNode.chainType} `);
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));
32
77
  }
33
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 {
34
90
  const parts: string[] = [node.action];
35
91
 
36
92
  // Add roles
@@ -109,10 +165,6 @@ export function renderExplicit(node: SemanticNode): string {
109
165
  return `[${parts.join(' ')}]`;
110
166
  }
111
167
 
112
- // =============================================================================
113
- // Internal Helpers
114
- // =============================================================================
115
-
116
168
  /**
117
169
  * Convert a semantic value to its explicit syntax string form.
118
170
  */
package/src/ir/index.ts CHANGED
@@ -32,10 +32,17 @@ export type {
32
32
  export { DEFAULT_REFERENCES, isValidReference } from './references';
33
33
 
34
34
  // Explicit syntax parser
35
- export { parseExplicit, isExplicitSyntax } from './explicit-parser';
35
+ export {
36
+ parseExplicit,
37
+ parseCompound,
38
+ parseDocument,
39
+ isExplicitSyntax,
40
+ isCompoundSyntax,
41
+ isDocumentSyntax,
42
+ } from './explicit-parser';
36
43
 
37
44
  // Explicit syntax renderer
38
- export { renderExplicit } from './explicit-renderer';
45
+ export { renderExplicit, renderDocument } from './explicit-renderer';
39
46
 
40
47
  // JSON schema conversion (deprecated — use fromProtocolJSON/validateProtocolJSON/toProtocolJSON)
41
48
  export { jsonToSemanticNode, validateSemanticJSON, semanticNodeToJSON } from './json-schema';
@@ -64,3 +71,7 @@ export {
64
71
 
65
72
  // Interchange AST → SemanticNode converter
66
73
  export { fromInterchangeNode, convertValue, renderExpr } from './from-interchange';
74
+
75
+ // SemanticNode → Runtime AST converter (for core runtime execution)
76
+ export { semanticNodeToRuntimeAST, semanticValueToAST } from './to-runtime-ast';
77
+ export type { RuntimeASTNode, RuntimeCommandNode, RuntimeEventNode } from './to-runtime-ast';
@@ -995,10 +995,10 @@ describe('v1.2: diagnostics', () => {
995
995
  ...createCommandNode('toggle', { patient: createSelector('#button', 'id') }),
996
996
  diagnostics: [
997
997
  {
998
- level: 'error',
999
- role: 'patient',
998
+ severity: 'error',
1000
999
  message: "toggle.patient expects selector kind [class, attribute], got 'id'",
1001
1000
  code: 'SCHEMA_SELECTOR_KIND_MISMATCH',
1001
+ source: 'schema',
1002
1002
  },
1003
1003
  ],
1004
1004
  };
@@ -1006,6 +1006,7 @@ describe('v1.2: diagnostics', () => {
1006
1006
  expect(json.diagnostics).toHaveLength(1);
1007
1007
  expect(json.diagnostics![0].level).toBe('error');
1008
1008
  expect(json.diagnostics![0].code).toBe('SCHEMA_SELECTOR_KIND_MISMATCH');
1009
+ expect(json.diagnostics![0].source).toBe('schema');
1009
1010
  });
1010
1011
 
1011
1012
  it('deserializes diagnostics from protocol JSON', () => {
@@ -1025,6 +1026,7 @@ describe('v1.2: diagnostics', () => {
1025
1026
  const node = fromProtocolJSON(json);
1026
1027
  expect(node.diagnostics).toHaveLength(1);
1027
1028
  expect(node.diagnostics![0].code).toBe('SCHEMA_VALUE_TYPE_MISMATCH');
1029
+ expect(node.diagnostics![0].severity).toBe('error');
1028
1030
  });
1029
1031
 
1030
1032
  describe('fixture conformance: type-constraints.json', () => {
@@ -54,6 +54,7 @@ import {
54
54
  import type {
55
55
  ProtocolNodeJSON,
56
56
  ProtocolValueJSON,
57
+ ProtocolDiagnosticJSON,
57
58
  ProtocolChainType,
58
59
  IRDiagnostic,
59
60
  LSEEnvelopeJSON,
@@ -164,14 +165,18 @@ export function toProtocolJSON(node: SemanticNode): ProtocolNodeJSON {
164
165
  );
165
166
  }
166
167
 
167
- // v1.2: diagnostics (all node kinds)
168
+ // v1.2/v1.2.1: diagnostics (all node kinds)
168
169
  if (node.diagnostics && node.diagnostics.length > 0) {
169
- result.diagnostics = node.diagnostics.map(d => ({
170
- level: d.level,
171
- role: d.role,
172
- message: d.message,
173
- code: d.code,
174
- }));
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
+ });
175
180
  }
176
181
 
177
182
  return result;
@@ -252,10 +257,11 @@ export function fromProtocolJSON(json: ProtocolNodeJSON): SemanticNode {
252
257
  a.value !== undefined ? { name: a.name, value: a.value } : { name: a.name }
253
258
  );
254
259
  const diagnostics = json.diagnostics?.map(d => ({
255
- level: d.level as 'error' | 'warning',
256
- role: d.role,
260
+ severity: d.level as 'error' | 'warning' | 'info',
257
261
  message: d.message,
258
- code: d.code,
262
+ ...(d.code ? { code: d.code } : {}),
263
+ ...(d.source ? { source: d.source } : {}),
264
+ ...(d.suggestions && d.suggestions.length > 0 ? { suggestions: d.suggestions } : {}),
259
265
  }));
260
266
 
261
267
  // trigger sugar: wrap command in event handler (check before kind dispatch)
@@ -361,7 +367,13 @@ function applyV12Metadata(
361
367
  node: SemanticNode,
362
368
  annotations: Array<{ name: string; value?: string }> | undefined,
363
369
  diagnostics:
364
- | Array<{ level: 'error' | 'warning'; role: string; message: string; code: string }>
370
+ | Array<{
371
+ severity: 'error' | 'warning' | 'info';
372
+ message: string;
373
+ code?: string;
374
+ source?: string;
375
+ suggestions?: string[];
376
+ }>
365
377
  | undefined
366
378
  ): SemanticNode {
367
379
  if ((!annotations || annotations.length === 0) && (!diagnostics || diagnostics.length === 0)) {
@@ -460,7 +472,7 @@ const VALID_VALUE_TYPES = new Set([
460
472
  ]);
461
473
  const VALID_CHAIN_TYPES = new Set(['then', 'and', 'async', 'sequential', 'pipe']);
462
474
  const VALID_ASYNC_VARIANTS = new Set(['all', 'race']);
463
- const VALID_DIAGNOSTIC_LEVELS = new Set(['error', 'warning']);
475
+ const VALID_DIAGNOSTIC_LEVELS = new Set(['error', 'warning', 'info']);
464
476
 
465
477
  /**
466
478
  * Validate that an unknown value conforms to the protocol JSON format.
@@ -637,14 +649,7 @@ export function validateProtocolJSON(json: unknown): IRDiagnostic[] {
637
649
  diagnostics.push({
638
650
  severity: 'error',
639
651
  code: 'INVALID_DIAGNOSTIC_LEVEL',
640
- message: `diagnostics[${i}].level must be "error" or "warning"`,
641
- });
642
- }
643
- if (typeof d.role !== 'string') {
644
- diagnostics.push({
645
- severity: 'error',
646
- code: 'MISSING_DIAGNOSTIC_ROLE',
647
- message: `diagnostics[${i}] missing required field: role`,
652
+ message: `diagnostics[${i}].level must be "error", "warning", or "info"`,
648
653
  });
649
654
  }
650
655
  if (typeof d.message !== 'string') {
@@ -654,11 +659,12 @@ export function validateProtocolJSON(json: unknown): IRDiagnostic[] {
654
659
  message: `diagnostics[${i}] missing required field: message`,
655
660
  });
656
661
  }
657
- if (typeof d.code !== 'string') {
662
+ // v1.2.1: role and code are now optional
663
+ if ('code' in d && typeof d.code !== 'string') {
658
664
  diagnostics.push({
659
- severity: 'error',
660
- code: 'MISSING_DIAGNOSTIC_CODE',
661
- message: `diagnostics[${i}] missing required field: code`,
665
+ severity: 'warning',
666
+ code: 'INVALID_DIAGNOSTIC_CODE',
667
+ message: `diagnostics[${i}].code must be a string if present`,
662
668
  });
663
669
  }
664
670
  }
@@ -0,0 +1,203 @@
1
+ /**
2
+ * SemanticNode → Runtime AST Converter
3
+ *
4
+ * Converts framework SemanticNode (LSE IR) to a structural AST format
5
+ * that the core runtime can execute directly. Uses structural typing
6
+ * to avoid a dependency on the core package.
7
+ *
8
+ * This is the canonical converter — AOT compiler delegates to it.
9
+ *
10
+ * The output matches the core runtime's expected node shapes:
11
+ * - `{ type: 'command', name, args, roles }` → processCommand()
12
+ * - `{ type: 'event', event, modifiers, body }` → adapted to executeEventHandler()
13
+ */
14
+
15
+ import type {
16
+ SemanticNode,
17
+ SemanticValue,
18
+ EventHandlerSemanticNode,
19
+ ConditionalSemanticNode,
20
+ LoopSemanticNode,
21
+ } from '../core/types';
22
+
23
+ // =============================================================================
24
+ // Structural output types (no core package dependency)
25
+ // =============================================================================
26
+
27
+ /** Minimal AST node that the core runtime can execute. */
28
+ export interface RuntimeASTNode {
29
+ readonly type: string;
30
+ readonly [key: string]: unknown;
31
+ }
32
+
33
+ /** Command node shape expected by runtime's processCommand(). */
34
+ export interface RuntimeCommandNode extends RuntimeASTNode {
35
+ readonly type: 'command';
36
+ readonly name: string;
37
+ readonly args: RuntimeASTNode[];
38
+ readonly roles?: Readonly<Record<string, RuntimeASTNode>>;
39
+ readonly modifiers?: Record<string, unknown>;
40
+ }
41
+
42
+ /** Event node shape expected by runtime's event adapter. */
43
+ export interface RuntimeEventNode extends RuntimeASTNode {
44
+ readonly type: 'event';
45
+ readonly event: string;
46
+ readonly modifiers: Record<string, unknown>;
47
+ readonly body: RuntimeASTNode[];
48
+ }
49
+
50
+ // =============================================================================
51
+ // Converter
52
+ // =============================================================================
53
+
54
+ /**
55
+ * Convert a SemanticNode to a RuntimeASTNode that the core runtime can execute.
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * import { parseExplicit } from '@lokascript/framework/ir';
60
+ * import { semanticNodeToRuntimeAST } from '@lokascript/framework/ir';
61
+ *
62
+ * const node = parseExplicit('[toggle patient:.active]');
63
+ * const ast = semanticNodeToRuntimeAST(node);
64
+ * // { type: 'command', name: 'toggle', args: [{ type: 'selector', value: '.active' }], roles: { patient: ... } }
65
+ * ```
66
+ */
67
+ export function semanticNodeToRuntimeAST(node: SemanticNode): RuntimeASTNode {
68
+ switch (node.kind) {
69
+ case 'event-handler':
70
+ return convertEventHandler(node as EventHandlerSemanticNode);
71
+
72
+ case 'conditional':
73
+ return convertConditional(node as ConditionalSemanticNode);
74
+
75
+ case 'loop':
76
+ return convertLoop(node as LoopSemanticNode);
77
+
78
+ case 'compound': {
79
+ const compound = node as SemanticNode & { statements?: SemanticNode[]; chainType?: string };
80
+ const commands = (compound.statements ?? []).map(semanticNodeToRuntimeAST);
81
+ return {
82
+ type: 'CommandSequence',
83
+ commands,
84
+ };
85
+ }
86
+
87
+ case 'command':
88
+ default:
89
+ return convertCommand(node);
90
+ }
91
+ }
92
+
93
+ function convertEventHandler(eh: EventHandlerSemanticNode): RuntimeEventNode {
94
+ const eventValue = eh.roles.get('event');
95
+ const eventName = eventValue && 'value' in eventValue ? String(eventValue.value) : 'click';
96
+
97
+ const modifiers: Record<string, unknown> = {};
98
+ if (eh.eventModifiers) {
99
+ if (eh.eventModifiers.once) modifiers.once = true;
100
+ if (eh.eventModifiers.debounce) modifiers.debounce = eh.eventModifiers.debounce;
101
+ if (eh.eventModifiers.throttle) modifiers.throttle = eh.eventModifiers.throttle;
102
+ if (eh.eventModifiers.queue) modifiers.queue = eh.eventModifiers.queue;
103
+ }
104
+
105
+ return {
106
+ type: 'event',
107
+ event: eventName,
108
+ modifiers,
109
+ body: (eh.body ?? []).map(semanticNodeToRuntimeAST),
110
+ };
111
+ }
112
+
113
+ function convertCommand(node: SemanticNode): RuntimeCommandNode {
114
+ const roles: Record<string, RuntimeASTNode> = {};
115
+ const args: RuntimeASTNode[] = [];
116
+
117
+ for (const [roleName, value] of node.roles) {
118
+ const astValue = semanticValueToAST(value);
119
+ roles[roleName] = astValue;
120
+ args.push(astValue);
121
+ }
122
+
123
+ return {
124
+ type: 'command',
125
+ name: node.action,
126
+ args,
127
+ roles,
128
+ };
129
+ }
130
+
131
+ function convertConditional(node: ConditionalSemanticNode): RuntimeASTNode {
132
+ const conditionValue = node.roles.get('condition');
133
+ return {
134
+ type: 'command',
135
+ name: 'if',
136
+ args: conditionValue ? [semanticValueToAST(conditionValue)] : [],
137
+ condition: conditionValue ? semanticValueToAST(conditionValue) : undefined,
138
+ thenBranch: (node.thenBranch ?? []).map(semanticNodeToRuntimeAST),
139
+ elseBranch: (node.elseBranch ?? []).map(semanticNodeToRuntimeAST),
140
+ };
141
+ }
142
+
143
+ function convertLoop(node: LoopSemanticNode): RuntimeASTNode {
144
+ const countValue = node.roles.get('patient') || node.roles.get('count');
145
+ return {
146
+ type: 'command',
147
+ name: 'repeat',
148
+ args: countValue ? [semanticValueToAST(countValue)] : [],
149
+ loopVariant: node.loopVariant ?? 'forever',
150
+ body: (node.body ?? []).map(semanticNodeToRuntimeAST),
151
+ loopVariable: node.loopVariable,
152
+ indexVariable: node.indexVariable,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Convert a SemanticValue to a RuntimeASTNode.
158
+ */
159
+ export function semanticValueToAST(value: SemanticValue): RuntimeASTNode {
160
+ switch (value.type) {
161
+ case 'selector':
162
+ return { type: 'selector', value: value.value as string };
163
+
164
+ case 'reference':
165
+ return { type: 'identifier', value: value.value as string };
166
+
167
+ case 'literal': {
168
+ const lit = value as { value: unknown; dataType?: string };
169
+ if (lit.dataType === 'duration') {
170
+ const str = String(lit.value);
171
+ const match = /^(\d+(?:\.\d+)?)(ms|s)$/.exec(str);
172
+ if (match) {
173
+ const ms = match[2] === 's' ? parseFloat(match[1]) * 1000 : parseFloat(match[1]);
174
+ return { type: 'literal', value: ms };
175
+ }
176
+ }
177
+ return { type: 'literal', value: lit.value as string | number | boolean | null };
178
+ }
179
+
180
+ case 'property-path': {
181
+ const pp = value as import('../core/types').PropertyPathValue;
182
+ const objectAST = semanticValueToAST(pp.object);
183
+ return {
184
+ type: 'member',
185
+ object: objectAST,
186
+ property: pp.property,
187
+ };
188
+ }
189
+
190
+ case 'expression': {
191
+ const expr = value as { raw: string };
192
+ return { type: 'expression', value: expr.raw };
193
+ }
194
+
195
+ case 'flag': {
196
+ const flag = value as { name: string; enabled: boolean };
197
+ return { type: 'literal', value: flag.enabled };
198
+ }
199
+
200
+ default:
201
+ return { type: 'literal', value: String((value as { value?: unknown }).value ?? '') };
202
+ }
203
+ }
package/src/ir/types.ts CHANGED
@@ -67,6 +67,12 @@ export interface ParseExplicitOptions {
67
67
  schemaLookup?: SchemaLookup;
68
68
  /** Custom set of valid reference names (defaults to DEFAULT_REFERENCES) */
69
69
  referenceSet?: ReadonlySet<string>;
70
+ /**
71
+ * When true, validation errors are collected as diagnostics on the
72
+ * returned node instead of throwing. Fatal parse errors (missing brackets,
73
+ * empty input) still throw. Default: false.
74
+ */
75
+ collectDiagnostics?: boolean;
70
76
  }
71
77
 
72
78
  // =============================================================================
@@ -153,12 +159,16 @@ export interface ProtocolNodeJSON {
153
159
  // v1.2 Sub-types
154
160
  // =============================================================================
155
161
 
156
- /** A type constraint diagnostic in protocol wire format (v1.2). */
162
+ /** A diagnostic in protocol wire format (v1.2, extended v1.2.1). */
157
163
  export interface ProtocolDiagnosticJSON {
158
- level: 'error' | 'warning';
159
- role: string;
164
+ level: 'error' | 'warning' | 'info';
165
+ role?: string;
160
166
  message: string;
161
- code: string;
167
+ code?: string;
168
+ /** v1.2.1: source parser/stage that produced this diagnostic. */
169
+ source?: string;
170
+ /** v1.2.1: actionable fix suggestions. */
171
+ suggestions?: string[];
162
172
  }
163
173
 
164
174
  /** A metadata annotation in protocol wire format (v1.2). */