@lokascript/framework 2.1.0 → 2.2.1

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
@@ -1,7 +1,15 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { parseExplicit, isExplicitSyntax } from './explicit-parser';
2
+ import {
3
+ parseExplicit,
4
+ parseCompound,
5
+ parseDocument,
6
+ isExplicitSyntax,
7
+ isCompoundSyntax,
8
+ isDocumentSyntax,
9
+ } from './explicit-parser';
3
10
  import type { ParseExplicitOptions, SchemaLookup } from './types';
4
11
  import { defineCommand, defineRole } from '../schema/command-schema';
12
+ import type { CompoundSemanticNode } from '../core/types';
5
13
 
6
14
  describe('isExplicitSyntax', () => {
7
15
  it('detects bracket syntax', () => {
@@ -304,3 +312,301 @@ describe('parseExplicit — custom reference set', () => {
304
312
  });
305
313
  });
306
314
  });
315
+
316
+ // ===========================================================================
317
+ // collectDiagnostics mode (v1.2.1)
318
+ // ===========================================================================
319
+
320
+ describe('parseExplicit — collectDiagnostics', () => {
321
+ const toggleSchema = defineCommand({
322
+ action: 'toggle',
323
+ roles: [
324
+ defineRole({ role: 'patient', required: true, expectedTypes: ['selector'] }),
325
+ defineRole({ role: 'destination', required: false, expectedTypes: ['selector'] }),
326
+ ],
327
+ });
328
+
329
+ const schemaLookup: SchemaLookup = {
330
+ getSchema(action: string) {
331
+ if (action === 'toggle') return toggleSchema;
332
+ return undefined;
333
+ },
334
+ };
335
+
336
+ const collectOpts: ParseExplicitOptions = { schemaLookup, collectDiagnostics: true };
337
+
338
+ it('returns node with diagnostics for unknown role', () => {
339
+ const node = parseExplicit('[toggle patient:.active badRole:foo]', collectOpts);
340
+ expect(node.action).toBe('toggle');
341
+ expect(node.diagnostics).toHaveLength(1);
342
+ expect(node.diagnostics![0].severity).toBe('error');
343
+ expect(node.diagnostics![0].code).toBe('UNKNOWN_ROLE');
344
+ expect(node.diagnostics![0].message).toContain('badRole');
345
+ expect(node.diagnostics![0].source).toBe('schema');
346
+ expect(node.diagnostics![0].suggestions).toContain('patient');
347
+ });
348
+
349
+ it('returns node with diagnostics for missing required role', () => {
350
+ const node = parseExplicit('[toggle destination:.active]', collectOpts);
351
+ expect(node.action).toBe('toggle');
352
+ expect(node.diagnostics).toHaveLength(1);
353
+ expect(node.diagnostics![0].code).toBe('MISSING_REQUIRED_ROLE');
354
+ expect(node.diagnostics![0].message).toContain('patient');
355
+ });
356
+
357
+ it('returns node with diagnostics for unknown flag', () => {
358
+ const node = parseExplicit('[toggle patient:.active +badFlag]', collectOpts);
359
+ expect(node.diagnostics).toHaveLength(1);
360
+ expect(node.diagnostics![0].code).toBe('UNKNOWN_FLAG');
361
+ });
362
+
363
+ it('returns node with diagnostics for invalid role format', () => {
364
+ const node = parseExplicit('[toggle patient:.active noColonHere]', collectOpts);
365
+ expect(node.diagnostics).toHaveLength(1);
366
+ expect(node.diagnostics![0].code).toBe('INVALID_ROLE_FORMAT');
367
+ });
368
+
369
+ it('collects multiple diagnostics', () => {
370
+ const node = parseExplicit('[toggle badRole:foo +badFlag]', collectOpts);
371
+ // unknown role + unknown flag + missing required role (patient)
372
+ expect(node.diagnostics!.length).toBeGreaterThanOrEqual(2);
373
+ const codes = node.diagnostics!.map(d => d.code);
374
+ expect(codes).toContain('UNKNOWN_ROLE');
375
+ expect(codes).toContain('UNKNOWN_FLAG');
376
+ });
377
+
378
+ it('returns clean node when no errors (no diagnostics field)', () => {
379
+ const node = parseExplicit('[toggle patient:.active]', collectOpts);
380
+ expect(node.action).toBe('toggle');
381
+ expect(node.diagnostics).toBeUndefined();
382
+ });
383
+
384
+ it('still throws on fatal errors (missing brackets)', () => {
385
+ expect(() => parseExplicit('toggle patient:.active', collectOpts)).toThrow(
386
+ 'Explicit syntax must be wrapped in brackets'
387
+ );
388
+ });
389
+
390
+ it('still throws on empty input', () => {
391
+ expect(() => parseExplicit('[]', collectOpts)).toThrow('Empty explicit statement');
392
+ });
393
+
394
+ it('returns diagnostics on event handler missing event role', () => {
395
+ const node = parseExplicit('[on body:[toggle patient:.active]]', collectOpts);
396
+ expect(node.diagnostics).toHaveLength(1);
397
+ expect(node.diagnostics![0].code).toBe('MISSING_EVENT_ROLE');
398
+ });
399
+ });
400
+
401
+ // =============================================================================
402
+ // parseCompound
403
+ // =============================================================================
404
+
405
+ describe('parseCompound', () => {
406
+ it('passes through single bracket command to parseExplicit', () => {
407
+ const node = parseCompound('[toggle patient:.active]');
408
+ expect(node.kind).toBe('command');
409
+ expect(node.action).toBe('toggle');
410
+ expect(node.roles.get('patient')?.value).toBe('.active');
411
+ });
412
+
413
+ it('parses two commands chained with then', () => {
414
+ const node = parseCompound('[add patient:.loading] then [fetch source:"/api/data"]');
415
+ expect(node.kind).toBe('compound');
416
+ const compound = node as CompoundSemanticNode;
417
+ expect(compound.chainType).toBe('then');
418
+ expect(compound.statements).toHaveLength(2);
419
+ expect(compound.statements[0].action).toBe('add');
420
+ expect(compound.statements[1].action).toBe('fetch');
421
+ });
422
+
423
+ it('parses commands chained with and', () => {
424
+ const node = parseCompound('[add patient:.a] and [add patient:.b]');
425
+ expect(node.kind).toBe('compound');
426
+ expect((node as CompoundSemanticNode).chainType).toBe('and');
427
+ });
428
+
429
+ it('parses commands chained with async', () => {
430
+ const node = parseCompound('[fetch source:/api/a] async [fetch source:/api/b]');
431
+ expect(node.kind).toBe('compound');
432
+ expect((node as CompoundSemanticNode).chainType).toBe('async');
433
+ });
434
+
435
+ it('parses commands chained with sequential', () => {
436
+ const node = parseCompound('[add patient:.a] sequential [add patient:.b]');
437
+ expect(node.kind).toBe('compound');
438
+ expect((node as CompoundSemanticNode).chainType).toBe('sequential');
439
+ });
440
+
441
+ it('parses commands chained with |> pipe', () => {
442
+ const node = parseCompound('[fetch source:/api/users] |> [put destination:#list]');
443
+ expect(node.kind).toBe('compound');
444
+ const compound = node as CompoundSemanticNode;
445
+ expect(compound.chainType).toBe('pipe');
446
+ expect(compound.statements).toHaveLength(2);
447
+ expect(compound.statements[0].action).toBe('fetch');
448
+ expect(compound.statements[1].action).toBe('put');
449
+ });
450
+
451
+ it('handles three or more chained commands', () => {
452
+ const node = parseCompound(
453
+ '[add patient:.loading] then [fetch source:/api/data] then [remove patient:.loading]'
454
+ );
455
+ expect(node.kind).toBe('compound');
456
+ const compound = node as CompoundSemanticNode;
457
+ expect(compound.chainType).toBe('then');
458
+ expect(compound.statements).toHaveLength(3);
459
+ });
460
+
461
+ it('uses first chain operator for mixed operators', () => {
462
+ const node = parseCompound('[add patient:.a] then [add patient:.b] and [add patient:.c]');
463
+ expect(node.kind).toBe('compound');
464
+ expect((node as CompoundSemanticNode).chainType).toBe('then');
465
+ });
466
+ });
467
+
468
+ // =============================================================================
469
+ // Annotation parsing (via parseCompound)
470
+ // =============================================================================
471
+
472
+ describe('parseCompound — annotations', () => {
473
+ it('parses single annotation with value', () => {
474
+ const node = parseCompound('@timeout(5s) [fetch source:"/api/users"]');
475
+ expect(node.annotations).toHaveLength(1);
476
+ expect(node.annotations![0]).toEqual({ name: 'timeout', value: '5s' });
477
+ expect(node.action).toBe('fetch');
478
+ });
479
+
480
+ it('parses annotation without value (flag-style)', () => {
481
+ const node = parseCompound('@deprecated [toggle patient:.active]');
482
+ expect(node.annotations).toHaveLength(1);
483
+ expect(node.annotations![0]).toEqual({ name: 'deprecated' });
484
+ });
485
+
486
+ it('parses multiple annotations preserving order', () => {
487
+ const node = parseCompound('@retry(3) @timeout(10s) @cache(60s) [fetch source:"/api/data"]');
488
+ expect(node.annotations).toHaveLength(3);
489
+ expect(node.annotations![0].name).toBe('retry');
490
+ expect(node.annotations![1].name).toBe('timeout');
491
+ expect(node.annotations![2].name).toBe('cache');
492
+ });
493
+
494
+ it('handles annotation with path-like value', () => {
495
+ const node = parseCompound('@source(/api/v2/users) [fetch source:"/api"]');
496
+ expect(node.annotations![0]).toEqual({ name: 'source', value: '/api/v2/users' });
497
+ });
498
+
499
+ it('handles annotation with quoted string value', () => {
500
+ const node = parseCompound('@doc("Toggle active state on click") [toggle patient:.active]');
501
+ expect(node.annotations![0]).toEqual({
502
+ name: 'doc',
503
+ value: 'Toggle active state on click',
504
+ });
505
+ });
506
+
507
+ it('returns no annotations when none present', () => {
508
+ const node = parseCompound('[toggle patient:.active]');
509
+ expect(node.annotations).toBeUndefined();
510
+ });
511
+
512
+ it('attaches annotations to compound nodes', () => {
513
+ const node = parseCompound(
514
+ '@permission(admin) [add patient:.loading] then [fetch source:"/api/users"]'
515
+ );
516
+ expect(node.kind).toBe('compound');
517
+ expect(node.annotations).toHaveLength(1);
518
+ expect(node.annotations![0]).toEqual({ name: 'permission', value: 'admin' });
519
+ });
520
+ });
521
+
522
+ // =============================================================================
523
+ // parseDocument
524
+ // =============================================================================
525
+
526
+ describe('parseDocument', () => {
527
+ it('parses a simple document with version header', () => {
528
+ const envelope = parseDocument('#!lse 1.2\n[toggle patient:.active]\n[add patient:.highlight]');
529
+ expect(envelope.lseVersion).toBe('1.2');
530
+ expect(envelope.nodes).toHaveLength(2);
531
+ expect(envelope.nodes[0].action).toBe('toggle');
532
+ expect(envelope.nodes[1].action).toBe('add');
533
+ });
534
+
535
+ it('defaults to version 1.0 when no header present', () => {
536
+ const envelope = parseDocument('[toggle patient:.active]');
537
+ expect(envelope.lseVersion).toBe('1.0');
538
+ expect(envelope.nodes).toHaveLength(1);
539
+ });
540
+
541
+ it('skips // comments', () => {
542
+ const envelope = parseDocument(
543
+ '#!lse 1.2\n// This is a comment\n[toggle patient:.active]\n// Another comment'
544
+ );
545
+ expect(envelope.nodes).toHaveLength(1);
546
+ });
547
+
548
+ it('skips # comments', () => {
549
+ const envelope = parseDocument('#!lse 1.2\n# This is a comment\n[toggle patient:.active]');
550
+ expect(envelope.nodes).toHaveLength(1);
551
+ });
552
+
553
+ it('skips blank lines', () => {
554
+ const envelope = parseDocument(
555
+ '#!lse 1.2\n\n[toggle patient:.active]\n\n\n[add patient:.highlight]\n'
556
+ );
557
+ expect(envelope.nodes).toHaveLength(2);
558
+ });
559
+
560
+ it('handles three-part version numbers', () => {
561
+ const envelope = parseDocument('#!lse 1.2.0\n[toggle patient:.active]');
562
+ expect(envelope.lseVersion).toBe('1.2.0');
563
+ });
564
+
565
+ it('handles compound statements in document lines', () => {
566
+ const envelope = parseDocument(
567
+ '#!lse 1.2\n[add patient:.loading] then [fetch source:/api/data]'
568
+ );
569
+ expect(envelope.nodes).toHaveLength(1);
570
+ expect(envelope.nodes[0].kind).toBe('compound');
571
+ });
572
+
573
+ it('handles annotated statements in document lines', () => {
574
+ const envelope = parseDocument('#!lse 1.2\n@timeout(5s) [fetch source:"/api/users"]');
575
+ expect(envelope.nodes).toHaveLength(1);
576
+ expect(envelope.nodes[0].annotations).toHaveLength(1);
577
+ });
578
+ });
579
+
580
+ // =============================================================================
581
+ // Detection helpers
582
+ // =============================================================================
583
+
584
+ describe('isCompoundSyntax', () => {
585
+ it('detects compound statements', () => {
586
+ expect(isCompoundSyntax('[a patient:x] then [b patient:y]')).toBe(true);
587
+ expect(isCompoundSyntax('[a patient:x] |> [b patient:y]')).toBe(true);
588
+ });
589
+
590
+ it('rejects single commands', () => {
591
+ expect(isCompoundSyntax('[toggle patient:.active]')).toBe(false);
592
+ });
593
+ });
594
+
595
+ describe('isDocumentSyntax', () => {
596
+ it('detects multi-line input', () => {
597
+ expect(isDocumentSyntax('[a patient:x]\n[b patient:y]')).toBe(true);
598
+ });
599
+
600
+ it('detects version header', () => {
601
+ expect(isDocumentSyntax('#!lse 1.2')).toBe(true);
602
+ });
603
+
604
+ it('detects comment lines', () => {
605
+ expect(isDocumentSyntax('// a comment')).toBe(true);
606
+ expect(isDocumentSyntax('# a comment')).toBe(true);
607
+ });
608
+
609
+ it('rejects single bracket commands', () => {
610
+ expect(isDocumentSyntax('[toggle patient:.active]')).toBe(false);
611
+ });
612
+ });