@cosmicdrift/kumiko-framework 0.158.2 → 0.160.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 (168) hide show
  1. package/package.json +7 -2
  2. package/src/__tests__/consumer-cli.integration.test.ts +110 -0
  3. package/src/api/__tests__/api.test.ts +65 -0
  4. package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
  5. package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +237 -0
  6. package/src/api/__tests__/auth-routes-mfa-verify.test.ts +1 -0
  7. package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
  8. package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
  9. package/src/api/__tests__/jwt.test.ts +150 -1
  10. package/src/api/__tests__/login-rate-limiter-sweep.test.ts +41 -0
  11. package/src/api/__tests__/server-boot-guards.test.ts +71 -0
  12. package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
  13. package/src/api/api-constants.ts +5 -0
  14. package/src/api/auth-middleware.ts +48 -59
  15. package/src/api/auth-routes.ts +51 -17
  16. package/src/api/index.ts +3 -3
  17. package/src/api/jwt.ts +148 -7
  18. package/src/api/pii-leak-guard.ts +5 -2
  19. package/src/api/routes.ts +57 -0
  20. package/src/api/server.ts +19 -5
  21. package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
  22. package/src/bun-db/query.ts +46 -27
  23. package/src/consumer-cli.ts +87 -0
  24. package/src/crypto/__tests__/pii-field-encryption.test.ts +69 -13
  25. package/src/crypto/blind-index.ts +8 -4
  26. package/src/crypto/event-pii.ts +1 -0
  27. package/src/crypto/kms-adapter.ts +2 -118
  28. package/src/crypto/pii-field-encryption.ts +49 -15
  29. package/src/db/__tests__/build-filter-where.test.ts +34 -0
  30. package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
  31. package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +396 -0
  32. package/src/db/__tests__/event-store-executor.integration.test.ts +5 -5
  33. package/src/db/blind-index-cleanup.ts +3 -1
  34. package/src/db/connection.ts +3 -11
  35. package/src/db/cursor.ts +1 -18
  36. package/src/db/dialect.ts +8 -19
  37. package/src/db/encryption.ts +2 -3
  38. package/src/db/entity-table-meta-types.ts +2 -0
  39. package/src/db/entity-table-meta.ts +16 -90
  40. package/src/db/event-store-executor.ts +4 -96
  41. package/src/db/queries/backfill-pii.ts +1 -0
  42. package/src/db/queries/event-consumer.ts +35 -2
  43. package/src/db/table-builder.ts +2 -19
  44. package/src/db/tenant-db.ts +6 -55
  45. package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
  46. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -233
  47. package/src/engine/__tests__/boot-validator.test.ts +46 -0
  48. package/src/engine/__tests__/codemod-pipeline.test.ts +139 -10
  49. package/src/engine/__tests__/define-roles.test.ts +21 -0
  50. package/src/engine/__tests__/engine.test.ts +28 -0
  51. package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
  52. package/src/engine/__tests__/registry-facade-sweep.test.ts +80 -0
  53. package/src/engine/__tests__/registry.test.ts +40 -0
  54. package/src/engine/__tests__/store-table.test.ts +12 -0
  55. package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
  56. package/src/engine/boot-validator/action-wiring.ts +1 -1
  57. package/src/engine/boot-validator/boot-check.ts +21 -0
  58. package/src/engine/boot-validator/entity-handler.ts +10 -1
  59. package/src/engine/boot-validator/entity-list-screens.ts +1 -1
  60. package/src/engine/boot-validator/gdpr-storage.ts +0 -112
  61. package/src/engine/boot-validator/index.ts +3 -9
  62. package/src/engine/boot-validator/screens.ts +1 -1
  63. package/src/engine/define-feature.ts +2 -0
  64. package/src/engine/define-handler.ts +11 -91
  65. package/src/engine/entity-handlers.ts +15 -27
  66. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +11 -1
  67. package/src/engine/feature-ast/__tests__/parse.test.ts +983 -3
  68. package/src/engine/feature-ast/__tests__/patch.test.ts +168 -0
  69. package/src/engine/feature-ast/__tests__/patcher.test.ts +7 -0
  70. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +9 -0
  71. package/src/engine/feature-ast/extractors/handlers.ts +19 -2
  72. package/src/engine/feature-ast/extractors/index.ts +1 -0
  73. package/src/engine/feature-ast/index.ts +2 -0
  74. package/src/engine/feature-ast/parse.ts +3 -0
  75. package/src/engine/feature-ast/patch.ts +2 -0
  76. package/src/engine/feature-ast/patcher.ts +21 -0
  77. package/src/engine/feature-ast/patterns.ts +16 -0
  78. package/src/engine/feature-ast/render.ts +15 -0
  79. package/src/engine/feature-builder-state.ts +6 -0
  80. package/src/engine/feature-config-events-jobs.ts +1 -1
  81. package/src/engine/feature-entity-handlers.ts +36 -2
  82. package/src/engine/feature-ui-extensions.ts +5 -1
  83. package/src/engine/field-helpers.ts +31 -0
  84. package/src/engine/handler-helpers.ts +26 -0
  85. package/src/engine/hook-helpers.ts +14 -0
  86. package/src/engine/index.ts +5 -2
  87. package/src/engine/ownership.ts +22 -76
  88. package/src/engine/pattern-library/__tests__/library.test.ts +9 -0
  89. package/src/engine/pattern-library/library.ts +2 -0
  90. package/src/engine/pattern-library/mixed-schemas.ts +37 -0
  91. package/src/engine/registry-facade.ts +9 -0
  92. package/src/engine/registry-ingest.ts +10 -0
  93. package/src/engine/registry-state.ts +3 -0
  94. package/src/engine/registry-validate.ts +1 -1
  95. package/src/engine/screen-helpers.ts +54 -0
  96. package/src/engine/tier-resolver-extension.ts +3 -2
  97. package/src/engine/types/config.ts +2 -497
  98. package/src/engine/types/define-handler.ts +2 -0
  99. package/src/engine/types/entity-handlers.ts +2 -0
  100. package/src/engine/types/event-type-map.ts +1 -37
  101. package/src/engine/types/feature.ts +2 -976
  102. package/src/engine/types/fields.ts +2 -697
  103. package/src/engine/types/handlers.ts +2 -839
  104. package/src/engine/types/hooks.ts +2 -184
  105. package/src/engine/types/http-route.ts +1 -72
  106. package/src/engine/types/identifiers.ts +1 -47
  107. package/src/engine/types/index.ts +66 -33
  108. package/src/engine/types/nav.ts +2 -67
  109. package/src/engine/types/ownership.ts +2 -0
  110. package/src/engine/types/projection.ts +2 -165
  111. package/src/engine/types/relations.ts +1 -51
  112. package/src/engine/types/screen.ts +2 -793
  113. package/src/engine/types/step.ts +2 -334
  114. package/src/engine/types/target-ref.ts +1 -21
  115. package/src/engine/types/tree-node.ts +1 -129
  116. package/src/engine/types/workspace.ts +2 -42
  117. package/src/entrypoint/index.ts +2 -2
  118. package/src/errors/write-error-info.ts +6 -22
  119. package/src/event-store/__tests__/event-store.integration.test.ts +31 -0
  120. package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +43 -0
  121. package/src/event-store/errors.ts +2 -35
  122. package/src/event-store/event-store.ts +28 -51
  123. package/src/event-store/events-schema.ts +1 -10
  124. package/src/event-store/index.ts +3 -2
  125. package/src/event-store/snapshot.ts +11 -35
  126. package/src/event-store/types.ts +2 -0
  127. package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
  128. package/src/files/file-handle.ts +2 -19
  129. package/src/files/provider-resolver.ts +3 -5
  130. package/src/files/types.ts +5 -54
  131. package/src/i18n/required-surface-keys.ts +1 -1
  132. package/src/jobs/__tests__/jobs.integration.test.ts +102 -1
  133. package/src/logging/types.ts +1 -7
  134. package/src/observability/types/index.ts +1 -29
  135. package/src/observability/types/metric.ts +1 -56
  136. package/src/observability/types/provider.ts +1 -32
  137. package/src/observability/types/span.ts +1 -58
  138. package/src/pipeline/__tests__/dispatcher.test.ts +134 -1
  139. package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
  140. package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +180 -0
  141. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +208 -0
  142. package/src/pipeline/dispatch-shared.ts +51 -3
  143. package/src/pipeline/dispatch-stream.ts +74 -0
  144. package/src/pipeline/dispatcher-utils.ts +1 -1
  145. package/src/pipeline/dispatcher.ts +7 -0
  146. package/src/pipeline/entity-cache.ts +2 -33
  147. package/src/pipeline/event-consumer-state.ts +28 -3
  148. package/src/pipeline/event-dispatcher-admin.ts +4 -0
  149. package/src/pipeline/event-dispatcher-delivery.ts +29 -3
  150. package/src/pipeline/event-dispatcher.ts +27 -1
  151. package/src/pipeline/multi-stream-apply-context.ts +4 -42
  152. package/src/pipeline/system-hooks.ts +7 -0
  153. package/src/rate-limit/resolver.ts +10 -30
  154. package/src/search/types.ts +1 -39
  155. package/src/secrets/__tests__/envelope-cipher.test.ts +2 -30
  156. package/src/secrets/__tests__/envelope.test.ts +1 -1
  157. package/src/secrets/envelope-cipher.ts +17 -45
  158. package/src/secrets/types.ts +2 -177
  159. package/src/stack/__tests__/event-collector.test.ts +42 -0
  160. package/src/testing/__tests__/late-bound.test.ts +25 -0
  161. package/src/testing/__tests__/wait-for.test.ts +53 -0
  162. package/src/testing/boot-validator-fixture.ts +1 -1
  163. package/src/testing/file-provider-contract.ts +84 -0
  164. package/src/testing/handler-context.ts +1 -1
  165. package/src/testing/index.ts +1 -0
  166. package/src/time/geo-tz.ts +1 -32
  167. package/src/time/tz-context.ts +9 -56
  168. package/src/ui-types/index.ts +7 -7
@@ -479,6 +479,60 @@ const ENTITY = "task";
479
479
  defineFeature("f", (r) => {
480
480
  r.entity(ENTITY, { fields: {} });
481
481
  });
482
+ `);
483
+
484
+ expect(result.errors[0]?.methodName).toBe("entity");
485
+ });
486
+
487
+ test("object form captures name + definition", () => {
488
+ const result = parseInline(`
489
+ defineFeature("f", (r) => {
490
+ r.entity({ name: "task", fields: { title: { type: "text" } } });
491
+ });
492
+ `);
493
+
494
+ expect(result.patterns[0]).toMatchObject({
495
+ kind: "entity",
496
+ entityName: "task",
497
+ definition: { fields: { title: { type: "text" } } },
498
+ });
499
+ });
500
+
501
+ test("emits ParseError when called with no arguments", () => {
502
+ const result = parseInline(`
503
+ defineFeature("f", (r) => {
504
+ r.entity();
505
+ });
506
+ `);
507
+
508
+ expect(result.errors[0]?.methodName).toBe("entity");
509
+ });
510
+
511
+ test("emits ParseError when object form is missing the name property", () => {
512
+ const result = parseInline(`
513
+ defineFeature("f", (r) => {
514
+ r.entity({ fields: {} });
515
+ });
516
+ `);
517
+
518
+ expect(result.errors[0]?.methodName).toBe("entity");
519
+ });
520
+
521
+ test("emits ParseError when object form definition contains a function value", () => {
522
+ const result = parseInline(`
523
+ defineFeature("f", (r) => {
524
+ r.entity({ name: "task", fields: { title: { type: "text", default: () => "x" } } });
525
+ });
526
+ `);
527
+
528
+ expect(result.errors[0]?.methodName).toBe("entity");
529
+ });
530
+
531
+ test("emits ParseError when the positional definition argument is missing", () => {
532
+ const result = parseInline(`
533
+ defineFeature("f", (r) => {
534
+ r.entity("task");
535
+ });
482
536
  `);
483
537
 
484
538
  expect(result.errors[0]?.methodName).toBe("entity");
@@ -521,6 +575,71 @@ const taskRef = { name: "task" };
521
575
  defineFeature("f", (r) => {
522
576
  r.relation(taskRef, "owner", { kind: "belongsTo", to: "user" });
523
577
  });
578
+ `);
579
+
580
+ expect(result.errors[0]?.methodName).toBe("relation");
581
+ });
582
+
583
+ test("object form captures entity, name and definition", () => {
584
+ const result = parseInline(`
585
+ defineFeature("f", (r) => {
586
+ r.relation({ entity: "task", name: "owner", kind: "belongsTo", to: "user" });
587
+ });
588
+ `);
589
+
590
+ expect(result.patterns[0]).toMatchObject({
591
+ kind: "relation",
592
+ entityName: "task",
593
+ relationName: "owner",
594
+ definition: { kind: "belongsTo", to: "user" },
595
+ });
596
+ });
597
+
598
+ test("emits ParseError when called with no arguments", () => {
599
+ const result = parseInline(`
600
+ defineFeature("f", (r) => {
601
+ r.relation();
602
+ });
603
+ `);
604
+
605
+ expect(result.errors[0]?.methodName).toBe("relation");
606
+ });
607
+
608
+ test("emits ParseError when object form is missing the entity property", () => {
609
+ const result = parseInline(`
610
+ defineFeature("f", (r) => {
611
+ r.relation({ name: "owner", kind: "belongsTo" });
612
+ });
613
+ `);
614
+
615
+ expect(result.errors[0]?.methodName).toBe("relation");
616
+ });
617
+
618
+ test("emits ParseError when object form is missing the name property", () => {
619
+ const result = parseInline(`
620
+ defineFeature("f", (r) => {
621
+ r.relation({ entity: "task", kind: "belongsTo" });
622
+ });
623
+ `);
624
+
625
+ expect(result.errors[0]?.methodName).toBe("relation");
626
+ });
627
+
628
+ test("emits ParseError when the relation name is not a string literal", () => {
629
+ const result = parseInline(`
630
+ defineFeature("f", (r) => {
631
+ r.relation("task", REL, { kind: "belongsTo" });
632
+ });
633
+ `);
634
+
635
+ expect(result.errors[0]?.methodName).toBe("relation");
636
+ });
637
+
638
+ test("emits ParseError when the positional definition argument is missing", () => {
639
+ const result = parseInline(`
640
+ defineFeature("f", (r) => {
641
+ r.relation("task", "owner");
642
+ });
524
643
  `);
525
644
 
526
645
  expect(result.errors[0]?.methodName).toBe("relation");
@@ -540,6 +659,26 @@ defineFeature("f", (r) => {
540
659
  definition: { id: "tasks", label: "Tasks", screen: "tasks:screen:list" },
541
660
  });
542
661
  });
662
+
663
+ test("emits ParseError when called with no arguments", () => {
664
+ const result = parseInline(`
665
+ defineFeature("f", (r) => {
666
+ r.nav();
667
+ });
668
+ `);
669
+
670
+ expect(result.errors[0]?.methodName).toBe("nav");
671
+ });
672
+
673
+ test("emits ParseError when the definition is not a plain object", () => {
674
+ const result = parseInline(`
675
+ defineFeature("f", (r) => {
676
+ r.nav(() => {});
677
+ });
678
+ `);
679
+
680
+ expect(result.errors[0]?.methodName).toBe("nav");
681
+ });
543
682
  });
544
683
 
545
684
  describe("extractWorkspace", () => {
@@ -555,6 +694,16 @@ defineFeature("f", (r) => {
555
694
  definition: { id: "admin", label: "Admin" },
556
695
  });
557
696
  });
697
+
698
+ test("emits ParseError when called with no arguments", () => {
699
+ const result = parseInline(`
700
+ defineFeature("f", (r) => {
701
+ r.workspace();
702
+ });
703
+ `);
704
+
705
+ expect(result.errors[0]?.methodName).toBe("workspace");
706
+ });
558
707
  });
559
708
 
560
709
  // =============================================================================
@@ -588,6 +737,26 @@ defineFeature("f", (r) => {
588
737
  defineFeature("f", (r) => {
589
738
  r.config({});
590
739
  });
740
+ `);
741
+
742
+ expect(result.errors[0]?.methodName).toBe("config");
743
+ });
744
+
745
+ test("emits ParseError when called with no arguments", () => {
746
+ const result = parseInline(`
747
+ defineFeature("f", (r) => {
748
+ r.config();
749
+ });
750
+ `);
751
+
752
+ expect(result.errors[0]?.methodName).toBe("config");
753
+ });
754
+
755
+ test("emits ParseError when the argument is not a plain object", () => {
756
+ const result = parseInline(`
757
+ defineFeature("f", (r) => {
758
+ r.config(() => {});
759
+ });
591
760
  `);
592
761
 
593
762
  expect(result.errors[0]?.methodName).toBe("config");
@@ -615,6 +784,26 @@ defineFeature("f", (r) => {
615
784
  },
616
785
  });
617
786
  });
787
+
788
+ test("emits ParseError when called with no arguments", () => {
789
+ const result = parseInline(`
790
+ defineFeature("f", (r) => {
791
+ r.translations();
792
+ });
793
+ `);
794
+
795
+ expect(result.errors[0]?.methodName).toBe("translations");
796
+ });
797
+
798
+ test("emits ParseError when keys property is missing", () => {
799
+ const result = parseInline(`
800
+ defineFeature("f", (r) => {
801
+ r.translations({});
802
+ });
803
+ `);
804
+
805
+ expect(result.errors[0]?.methodName).toBe("translations");
806
+ });
618
807
  });
619
808
 
620
809
  describe("extractMetric", () => {
@@ -631,6 +820,70 @@ defineFeature("f", (r) => {
631
820
  options: { type: "counter", description: "API requests" },
632
821
  });
633
822
  });
823
+
824
+ test("object form captures name + options", () => {
825
+ const result = parseInline(`
826
+ defineFeature("f", (r) => {
827
+ r.metric({ name: "requests", type: "counter", description: "API requests" });
828
+ });
829
+ `);
830
+
831
+ expect(result.patterns[0]).toMatchObject({
832
+ kind: "metric",
833
+ shortName: "requests",
834
+ options: { type: "counter", description: "API requests" },
835
+ });
836
+ });
837
+
838
+ test("emits ParseError when called with no arguments", () => {
839
+ const result = parseInline(`
840
+ defineFeature("f", (r) => {
841
+ r.metric();
842
+ });
843
+ `);
844
+
845
+ expect(result.errors[0]?.methodName).toBe("metric");
846
+ });
847
+
848
+ test("emits ParseError when object form is missing the name property", () => {
849
+ const result = parseInline(`
850
+ defineFeature("f", (r) => {
851
+ r.metric({ type: "counter" });
852
+ });
853
+ `);
854
+
855
+ expect(result.errors[0]?.methodName).toBe("metric");
856
+ });
857
+
858
+ test("emits ParseError when the name is not a string literal", () => {
859
+ const result = parseInline(`
860
+ defineFeature("f", (r) => {
861
+ r.metric(NAME, { type: "counter" });
862
+ });
863
+ `);
864
+
865
+ expect(result.errors[0]?.methodName).toBe("metric");
866
+ });
867
+
868
+ test("emits ParseError when the options argument is missing", () => {
869
+ const result = parseInline(`
870
+ defineFeature("f", (r) => {
871
+ r.metric("requests");
872
+ });
873
+ `);
874
+
875
+ expect(result.errors[0]?.methodName).toBe("metric");
876
+ });
877
+
878
+ test("emits ParseError when options is not a plain object", () => {
879
+ const result = parseInline(`
880
+ defineFeature("f", (r) => {
881
+ r.metric("requests", () => {});
882
+ });
883
+ `);
884
+
885
+ expect(result.errors[0]?.methodName).toBe("metric");
886
+ });
634
887
  });
635
888
 
636
889
  describe("extractSecret", () => {
@@ -647,6 +900,30 @@ defineFeature("f", (r) => {
647
900
  options: { description: "Stripe API key" },
648
901
  });
649
902
  });
903
+
904
+ test("object form captures name + options", () => {
905
+ const result = parseInline(`
906
+ defineFeature("f", (r) => {
907
+ r.secret({ name: "apiKey", description: "Stripe API key" });
908
+ });
909
+ `);
910
+
911
+ expect(result.patterns[0]).toMatchObject({
912
+ kind: "secret",
913
+ shortName: "apiKey",
914
+ options: { description: "Stripe API key" },
915
+ });
916
+ });
917
+
918
+ test("emits ParseError when called with no arguments", () => {
919
+ const result = parseInline(`
920
+ defineFeature("f", (r) => {
921
+ r.secret();
922
+ });
923
+ `);
924
+
925
+ expect(result.errors[0]?.methodName).toBe("secret");
926
+ });
650
927
  });
651
928
 
652
929
  describe("extractClaimKey", () => {
@@ -669,6 +946,30 @@ defineFeature("f", (r) => {
669
946
  defineFeature("f", (r) => {
670
947
  r.claimKey("teamId", { type: "bigint" });
671
948
  });
949
+ `);
950
+
951
+ expect(result.errors[0]?.methodName).toBe("claimKey");
952
+ });
953
+
954
+ test("object form captures name + type", () => {
955
+ const result = parseInline(`
956
+ defineFeature("f", (r) => {
957
+ r.claimKey({ name: "teamId", type: "string" });
958
+ });
959
+ `);
960
+
961
+ expect(result.patterns[0]).toMatchObject({
962
+ kind: "claimKey",
963
+ shortName: "teamId",
964
+ claimType: "string",
965
+ });
966
+ });
967
+
968
+ test("emits ParseError when called with no arguments", () => {
969
+ const result = parseInline(`
970
+ defineFeature("f", (r) => {
971
+ r.claimKey();
972
+ });
672
973
  `);
673
974
 
674
975
  expect(result.errors[0]?.methodName).toBe("claimKey");
@@ -767,6 +1068,16 @@ defineFeature("f", (r) => {
767
1068
  expect(result.errors[0]?.methodName).toBe("referenceData");
768
1069
  });
769
1070
 
1071
+ test("emits ParseError when object form is missing the entity property", () => {
1072
+ const result = parseInline(`
1073
+ defineFeature("f", (r) => {
1074
+ r.referenceData({ data: [{ id: "open" }] });
1075
+ });
1076
+ `);
1077
+
1078
+ expect(result.errors[0]?.methodName).toBe("referenceData");
1079
+ });
1080
+
770
1081
  test("emits ParseError when object form's data is not an array of plain objects", () => {
771
1082
  const result = parseInline(`
772
1083
  defineFeature("f", (r) => {
@@ -796,10 +1107,41 @@ defineFeature("f", (r) => {
796
1107
 
797
1108
  expect(result.errors[0]?.methodName).toBe("referenceData");
798
1109
  });
799
- });
800
1110
 
801
- describe("extractUseExtension", () => {
802
- test("captures extension name + entity ref", () => {
1111
+ test("emits ParseError when object form entity ref is unresolvable", () => {
1112
+ const result = parseInline(`
1113
+ const ent = { name: "status" };
1114
+ defineFeature("f", (r) => {
1115
+ r.referenceData({ entity: ent, data: [{ id: "open" }] });
1116
+ });
1117
+ `);
1118
+
1119
+ expect(result.errors[0]?.methodName).toBe("referenceData");
1120
+ });
1121
+
1122
+ test("emits ParseError when positional data is not an array of plain objects", () => {
1123
+ const result = parseInline(`
1124
+ defineFeature("f", (r) => {
1125
+ r.referenceData("status", "not-an-array");
1126
+ });
1127
+ `);
1128
+
1129
+ expect(result.errors[0]?.methodName).toBe("referenceData");
1130
+ });
1131
+
1132
+ test("emits ParseError when positional options is not a plain object", () => {
1133
+ const result = parseInline(`
1134
+ defineFeature("f", (r) => {
1135
+ r.referenceData("status", [{ id: "open" }], () => {});
1136
+ });
1137
+ `);
1138
+
1139
+ expect(result.errors[0]?.methodName).toBe("referenceData");
1140
+ });
1141
+ });
1142
+
1143
+ describe("extractUseExtension", () => {
1144
+ test("captures extension name + entity ref", () => {
803
1145
  const result = parseInline(`
804
1146
  defineFeature("f", (r) => {
805
1147
  r.useExtension("audit", "task");
@@ -906,6 +1248,37 @@ defineFeature("f", (r) => {
906
1248
  defineFeature("f", (r) => {
907
1249
  r.useExtension("audit", "task", "not-an-object");
908
1250
  });
1251
+ `);
1252
+
1253
+ expect(result.errors[0]?.methodName).toBe("useExtension");
1254
+ });
1255
+
1256
+ test("emits ParseError when the extension name is not a string literal", () => {
1257
+ const result = parseInline(`
1258
+ defineFeature("f", (r) => {
1259
+ r.useExtension(EXT, "task");
1260
+ });
1261
+ `);
1262
+
1263
+ expect(result.errors[0]?.methodName).toBe("useExtension");
1264
+ });
1265
+
1266
+ test("emits ParseError when the positional entity ref is unresolvable", () => {
1267
+ const result = parseInline(`
1268
+ const ent = { name: "task" };
1269
+ defineFeature("f", (r) => {
1270
+ r.useExtension("audit", ent);
1271
+ });
1272
+ `);
1273
+
1274
+ expect(result.errors[0]?.methodName).toBe("useExtension");
1275
+ });
1276
+
1277
+ test("emits ParseError when object form is missing the name property", () => {
1278
+ const result = parseInline(`
1279
+ defineFeature("f", (r) => {
1280
+ r.useExtension({ entity: "task" });
1281
+ });
909
1282
  `);
910
1283
 
911
1284
  expect(result.errors[0]?.methodName).toBe("useExtension");
@@ -948,6 +1321,143 @@ defineFeature("f", (r) => {
948
1321
  defineFeature("f", (r) => {
949
1322
  r.hook("postCommit", "task", () => {});
950
1323
  });
1324
+ `);
1325
+
1326
+ expect(result.errors[0]?.methodName).toBe("hook");
1327
+ });
1328
+
1329
+ test("object form captures type, target and handler", () => {
1330
+ const result = parseInline(`
1331
+ defineFeature("f", (r) => {
1332
+ r.hook({ type: "preSave", target: "task", handler: (event, ctx) => {} });
1333
+ });
1334
+ `);
1335
+
1336
+ expect(result.patterns[0]).toMatchObject({
1337
+ kind: "hook",
1338
+ hookType: "preSave",
1339
+ target: "task",
1340
+ });
1341
+ });
1342
+
1343
+ test("emits ParseError when object form has an unknown hook type", () => {
1344
+ const result = parseInline(`
1345
+ defineFeature("f", (r) => {
1346
+ r.hook({ type: "postCommit", target: "task", handler: () => {} });
1347
+ });
1348
+ `);
1349
+
1350
+ expect(result.errors[0]?.methodName).toBe("hook");
1351
+ });
1352
+
1353
+ test("emits ParseError when object form is missing the handler property", () => {
1354
+ const result = parseInline(`
1355
+ defineFeature("f", (r) => {
1356
+ r.hook({ type: "postSave", target: "task" });
1357
+ });
1358
+ `);
1359
+
1360
+ expect(result.errors[0]?.methodName).toBe("hook");
1361
+ });
1362
+
1363
+ test("emits ParseError when object form handler is not an inline function", () => {
1364
+ const result = parseInline(`
1365
+ defineFeature("f", (r) => {
1366
+ r.hook({ type: "postSave", target: "task", handler: handlerRef });
1367
+ });
1368
+ `);
1369
+
1370
+ expect(result.errors[0]?.methodName).toBe("hook");
1371
+ });
1372
+
1373
+ test("captures a multi-entity target array", () => {
1374
+ const result = parseInline(`
1375
+ defineFeature("f", (r) => {
1376
+ r.hook("postSave", ["task", "note"], () => {});
1377
+ });
1378
+ `);
1379
+
1380
+ expect(result.patterns[0]).toMatchObject({
1381
+ kind: "hook",
1382
+ target: ["task", "note"],
1383
+ });
1384
+ });
1385
+
1386
+ test("emits ParseError when called with no arguments", () => {
1387
+ const result = parseInline(`
1388
+ defineFeature("f", (r) => {
1389
+ r.hook();
1390
+ });
1391
+ `);
1392
+
1393
+ expect(result.errors[0]?.methodName).toBe("hook");
1394
+ });
1395
+
1396
+ test("emits ParseError when object form is missing the type property", () => {
1397
+ const result = parseInline(`
1398
+ defineFeature("f", (r) => {
1399
+ r.hook({ target: "task", handler: () => {} });
1400
+ });
1401
+ `);
1402
+
1403
+ expect(result.errors[0]?.methodName).toBe("hook");
1404
+ });
1405
+
1406
+ test("emits ParseError when object form is missing the target property", () => {
1407
+ const result = parseInline(`
1408
+ defineFeature("f", (r) => {
1409
+ r.hook({ type: "postSave", handler: () => {} });
1410
+ });
1411
+ `);
1412
+
1413
+ expect(result.errors[0]?.methodName).toBe("hook");
1414
+ });
1415
+
1416
+ test("emits ParseError when the handler is not an inline function", () => {
1417
+ const result = parseInline(`
1418
+ defineFeature("f", (r) => {
1419
+ r.hook("postSave", "task", handlerRef);
1420
+ });
1421
+ `);
1422
+
1423
+ expect(result.errors[0]?.methodName).toBe("hook");
1424
+ });
1425
+
1426
+ test("emits ParseError when the hook type is not a string literal", () => {
1427
+ const result = parseInline(`
1428
+ defineFeature("f", (r) => {
1429
+ r.hook(TYPE, "task", () => {});
1430
+ });
1431
+ `);
1432
+
1433
+ expect(result.errors[0]?.methodName).toBe("hook");
1434
+ });
1435
+
1436
+ test("emits ParseError when the target argument is missing", () => {
1437
+ const result = parseInline(`
1438
+ defineFeature("f", (r) => {
1439
+ r.hook("postSave");
1440
+ });
1441
+ `);
1442
+
1443
+ expect(result.errors[0]?.methodName).toBe("hook");
1444
+ });
1445
+
1446
+ test("emits ParseError when object form target is not a valid ref", () => {
1447
+ const result = parseInline(`
1448
+ defineFeature("f", (r) => {
1449
+ r.hook({ type: "postSave", target: 123, handler: () => {} });
1450
+ });
1451
+ `);
1452
+
1453
+ expect(result.errors[0]?.methodName).toBe("hook");
1454
+ });
1455
+
1456
+ test("emits ParseError when the hook function argument is missing", () => {
1457
+ const result = parseInline(`
1458
+ defineFeature("f", (r) => {
1459
+ r.hook("postSave", "task");
1460
+ });
951
1461
  `);
952
1462
 
953
1463
  expect(result.errors[0]?.methodName).toBe("hook");
@@ -1004,6 +1514,26 @@ defineFeature("f", (r) => {
1004
1514
 
1005
1515
  expect(result.patterns[0]).toMatchObject({ kind: "authClaims" });
1006
1516
  });
1517
+
1518
+ test("emits ParseError when called with no arguments", () => {
1519
+ const result = parseInline(`
1520
+ defineFeature("f", (r) => {
1521
+ r.authClaims();
1522
+ });
1523
+ `);
1524
+
1525
+ expect(result.errors[0]?.methodName).toBe("authClaims");
1526
+ });
1527
+
1528
+ test("emits ParseError when the argument is not an inline function", () => {
1529
+ const result = parseInline(`
1530
+ defineFeature("f", (r) => {
1531
+ r.authClaims(handlerRef);
1532
+ });
1533
+ `);
1534
+
1535
+ expect(result.errors[0]?.methodName).toBe("authClaims");
1536
+ });
1007
1537
  });
1008
1538
 
1009
1539
  describe("extractWriteHandler", () => {
@@ -1092,6 +1622,58 @@ defineFeature("f", (r) => {
1092
1622
  });
1093
1623
  });
1094
1624
 
1625
+ describe("extractStreamHandler", () => {
1626
+ test("inline form: name, schema, handler, options", () => {
1627
+ const result = parseInline(`
1628
+ defineFeature("f", (r) => {
1629
+ r.streamHandler("chat:complete", z.object({ prompt: z.string() }), async function* (input, ctx) {
1630
+ yield "token";
1631
+ }, { access: { roles: ["User"] } });
1632
+ });
1633
+ `);
1634
+
1635
+ expect(result.patterns[0]).toMatchObject({
1636
+ kind: "streamHandler",
1637
+ handlerName: "chat:complete",
1638
+ access: { roles: ["User"] },
1639
+ });
1640
+ });
1641
+
1642
+ test("object form: name, schema, handler", () => {
1643
+ const result = parseInline(`
1644
+ defineFeature("f", (r) => {
1645
+ r.streamHandler({
1646
+ name: "chat:complete",
1647
+ schema: z.object({ prompt: z.string() }),
1648
+ handler: async function* (input, ctx) { yield "token"; },
1649
+ access: { openToAll: true },
1650
+ });
1651
+ });
1652
+ `);
1653
+
1654
+ expect(result.patterns[0]).toMatchObject({
1655
+ kind: "streamHandler",
1656
+ handlerName: "chat:complete",
1657
+ access: { openToAll: true },
1658
+ });
1659
+ });
1660
+
1661
+ test("keeps an unresolvable ref as an opaque pattern instead of failing", () => {
1662
+ const result = parseInline(`
1663
+ defineFeature("f", (r) => {
1664
+ r.streamHandler(chatCompleteHandler);
1665
+ });
1666
+ `);
1667
+
1668
+ expect(result.patterns[0]).toMatchObject({
1669
+ kind: "streamHandler",
1670
+ handlerName: undefined,
1671
+ source: { raw: "r.streamHandler(chatCompleteHandler)" },
1672
+ });
1673
+ expect(result.errors).toEqual([]);
1674
+ });
1675
+ });
1676
+
1095
1677
  describe("extractJob", () => {
1096
1678
  test("captures jobName, options and handler body", () => {
1097
1679
  const result = parseInline(`
@@ -1105,6 +1687,124 @@ defineFeature("f", (r) => {
1105
1687
  jobName: "daily-cleanup",
1106
1688
  });
1107
1689
  });
1690
+
1691
+ test("object form captures name, handler and extra options", () => {
1692
+ const result = parseInline(`
1693
+ defineFeature("f", (r) => {
1694
+ r.job({
1695
+ name: "daily-cleanup",
1696
+ trigger: { cron: "0 3 * * *" },
1697
+ handler: async (ctx) => {},
1698
+ });
1699
+ });
1700
+ `);
1701
+
1702
+ expect(result.patterns[0]).toMatchObject({
1703
+ kind: "job",
1704
+ jobName: "daily-cleanup",
1705
+ options: { trigger: { cron: "0 3 * * *" } },
1706
+ });
1707
+ });
1708
+
1709
+ test("emits ParseError when called with no arguments", () => {
1710
+ const result = parseInline(`
1711
+ defineFeature("f", (r) => {
1712
+ r.job();
1713
+ });
1714
+ `);
1715
+
1716
+ expect(result.errors[0]?.methodName).toBe("job");
1717
+ });
1718
+
1719
+ test("emits ParseError when object form is missing the name property", () => {
1720
+ const result = parseInline(`
1721
+ defineFeature("f", (r) => {
1722
+ r.job({ handler: async () => {} });
1723
+ });
1724
+ `);
1725
+
1726
+ expect(result.errors[0]?.methodName).toBe("job");
1727
+ });
1728
+
1729
+ test("emits ParseError when object form is missing the handler property", () => {
1730
+ const result = parseInline(`
1731
+ defineFeature("f", (r) => {
1732
+ r.job({ name: "daily-cleanup", trigger: { cron: "0 3 * * *" } });
1733
+ });
1734
+ `);
1735
+
1736
+ expect(result.errors[0]?.methodName).toBe("job");
1737
+ });
1738
+
1739
+ test("emits ParseError when object form option value is not readable", () => {
1740
+ const result = parseInline(`
1741
+ defineFeature("f", (r) => {
1742
+ r.job({ name: "daily-cleanup", handler: async () => {}, onComplete: () => {} });
1743
+ });
1744
+ `);
1745
+
1746
+ expect(result.errors[0]?.methodName).toBe("job");
1747
+ });
1748
+
1749
+ test("emits ParseError when object form handler is not an inline function", () => {
1750
+ const result = parseInline(`
1751
+ defineFeature("f", (r) => {
1752
+ r.job({ name: "daily-cleanup", handler: handlerRef });
1753
+ });
1754
+ `);
1755
+
1756
+ expect(result.errors[0]?.methodName).toBe("job");
1757
+ });
1758
+
1759
+ test("emits ParseError when the job name is not a string literal", () => {
1760
+ const result = parseInline(`
1761
+ defineFeature("f", (r) => {
1762
+ r.job(NAME, {}, async () => {});
1763
+ });
1764
+ `);
1765
+
1766
+ expect(result.errors[0]?.methodName).toBe("job");
1767
+ });
1768
+
1769
+ test("emits ParseError when the options argument is missing", () => {
1770
+ const result = parseInline(`
1771
+ defineFeature("f", (r) => {
1772
+ r.job("daily-cleanup");
1773
+ });
1774
+ `);
1775
+
1776
+ expect(result.errors[0]?.methodName).toBe("job");
1777
+ });
1778
+
1779
+ test("emits ParseError when the handler argument is missing", () => {
1780
+ const result = parseInline(`
1781
+ defineFeature("f", (r) => {
1782
+ r.job("daily-cleanup", { trigger: { cron: "0 3 * * *" } });
1783
+ });
1784
+ `);
1785
+
1786
+ expect(result.errors[0]?.methodName).toBe("job");
1787
+ });
1788
+
1789
+ test("emits ParseError when options is not a plain object", () => {
1790
+ const result = parseInline(`
1791
+ defineFeature("f", (r) => {
1792
+ r.job("daily-cleanup", () => {}, async () => {});
1793
+ });
1794
+ `);
1795
+
1796
+ expect(result.errors[0]?.methodName).toBe("job");
1797
+ });
1798
+
1799
+ test("emits ParseError when the positional handler is not an inline function", () => {
1800
+ const result = parseInline(`
1801
+ defineFeature("f", (r) => {
1802
+ r.job("daily-cleanup", { trigger: { cron: "0 3 * * *" } }, handlerRef);
1803
+ });
1804
+ `);
1805
+
1806
+ expect(result.errors[0]?.methodName).toBe("job");
1807
+ });
1108
1808
  });
1109
1809
 
1110
1810
  describe("extractHttpRoute", () => {
@@ -1127,6 +1827,66 @@ defineFeature("f", (r) => {
1127
1827
  anonymous: true,
1128
1828
  });
1129
1829
  });
1830
+
1831
+ test("emits ParseError when the argument is not an inline object", () => {
1832
+ const result = parseInline(`
1833
+ defineFeature("f", (r) => {
1834
+ r.httpRoute("GET", "/feed.xml");
1835
+ });
1836
+ `);
1837
+
1838
+ expect(result.errors[0]?.methodName).toBe("httpRoute");
1839
+ });
1840
+
1841
+ test("emits ParseError when method is missing", () => {
1842
+ const result = parseInline(`
1843
+ defineFeature("f", (r) => {
1844
+ r.httpRoute({ path: "/feed.xml", handler: async () => {} });
1845
+ });
1846
+ `);
1847
+
1848
+ expect(result.errors[0]?.methodName).toBe("httpRoute");
1849
+ });
1850
+
1851
+ test("emits ParseError when method is not a valid HTTP verb", () => {
1852
+ const result = parseInline(`
1853
+ defineFeature("f", (r) => {
1854
+ r.httpRoute({ method: "FETCH", path: "/feed.xml", handler: async () => {} });
1855
+ });
1856
+ `);
1857
+
1858
+ expect(result.errors[0]?.methodName).toBe("httpRoute");
1859
+ });
1860
+
1861
+ test("emits ParseError when path is missing", () => {
1862
+ const result = parseInline(`
1863
+ defineFeature("f", (r) => {
1864
+ r.httpRoute({ method: "GET", handler: async () => {} });
1865
+ });
1866
+ `);
1867
+
1868
+ expect(result.errors[0]?.methodName).toBe("httpRoute");
1869
+ });
1870
+
1871
+ test("emits ParseError when handler is missing", () => {
1872
+ const result = parseInline(`
1873
+ defineFeature("f", (r) => {
1874
+ r.httpRoute({ method: "GET", path: "/feed.xml" });
1875
+ });
1876
+ `);
1877
+
1878
+ expect(result.errors[0]?.methodName).toBe("httpRoute");
1879
+ });
1880
+
1881
+ test("emits ParseError when handler is not an inline function", () => {
1882
+ const result = parseInline(`
1883
+ defineFeature("f", (r) => {
1884
+ r.httpRoute({ method: "GET", path: "/feed.xml", handler: handlerRef });
1885
+ });
1886
+ `);
1887
+
1888
+ expect(result.errors[0]?.methodName).toBe("httpRoute");
1889
+ });
1130
1890
  });
1131
1891
 
1132
1892
  describe("extractDefineEvent", () => {
@@ -1143,6 +1903,76 @@ defineFeature("f", (r) => {
1143
1903
  version: 2,
1144
1904
  });
1145
1905
  });
1906
+
1907
+ test("object form captures name, schema, version and migrations", () => {
1908
+ const result = parseInline(`
1909
+ defineFeature("f", (r) => {
1910
+ r.defineEvent({
1911
+ name: "incidentOpened",
1912
+ schema: z.object({ id: z.string() }),
1913
+ version: 2,
1914
+ migrations: { "1": (payload) => payload },
1915
+ });
1916
+ });
1917
+ `);
1918
+
1919
+ expect(result.patterns[0]).toMatchObject({
1920
+ kind: "defineEvent",
1921
+ eventName: "incidentOpened",
1922
+ version: 2,
1923
+ migrations: { "1": expect.anything() },
1924
+ });
1925
+ });
1926
+
1927
+ test("emits ParseError when called with no arguments", () => {
1928
+ const result = parseInline(`
1929
+ defineFeature("f", (r) => {
1930
+ r.defineEvent();
1931
+ });
1932
+ `);
1933
+
1934
+ expect(result.errors[0]?.methodName).toBe("defineEvent");
1935
+ });
1936
+
1937
+ test("emits ParseError when object form is missing the name property", () => {
1938
+ const result = parseInline(`
1939
+ defineFeature("f", (r) => {
1940
+ r.defineEvent({ schema: z.object({ id: z.string() }) });
1941
+ });
1942
+ `);
1943
+
1944
+ expect(result.errors[0]?.methodName).toBe("defineEvent");
1945
+ });
1946
+
1947
+ test("emits ParseError when object form is missing the schema property", () => {
1948
+ const result = parseInline(`
1949
+ defineFeature("f", (r) => {
1950
+ r.defineEvent({ name: "incidentOpened" });
1951
+ });
1952
+ `);
1953
+
1954
+ expect(result.errors[0]?.methodName).toBe("defineEvent");
1955
+ });
1956
+
1957
+ test("emits ParseError when the event name is not a string literal", () => {
1958
+ const result = parseInline(`
1959
+ defineFeature("f", (r) => {
1960
+ r.defineEvent(EVENT, z.object({ id: z.string() }));
1961
+ });
1962
+ `);
1963
+
1964
+ expect(result.errors[0]?.methodName).toBe("defineEvent");
1965
+ });
1966
+
1967
+ test("emits ParseError when the schema argument is missing", () => {
1968
+ const result = parseInline(`
1969
+ defineFeature("f", (r) => {
1970
+ r.defineEvent("incidentOpened");
1971
+ });
1972
+ `);
1973
+
1974
+ expect(result.errors[0]?.methodName).toBe("defineEvent");
1975
+ });
1146
1976
  });
1147
1977
 
1148
1978
  describe("extractDefineEvent — migrations (formerly extractEventMigration)", () => {
@@ -1187,6 +2017,26 @@ defineFeature("f", (r) => {
1187
2017
  migrations: { "1": expect.anything() },
1188
2018
  });
1189
2019
  });
2020
+
2021
+ test("skips malformed migration array entries instead of failing the whole extract", () => {
2022
+ const result = parseInline(`
2023
+ defineFeature("f", (r) => {
2024
+ r.defineEvent("incidentOpened", z.object({ id: z.string() }), {
2025
+ migrations: [
2026
+ "skip-me",
2027
+ { fromVersion: "bad", transform: (payload) => payload },
2028
+ { fromVersion: 1, transform: (payload) => ({ ...payload, ok: true }) },
2029
+ ],
2030
+ });
2031
+ });
2032
+ `);
2033
+
2034
+ expect(result.errors).toEqual([]);
2035
+ expect(result.patterns[0]).toMatchObject({
2036
+ kind: "defineEvent",
2037
+ migrations: { "1": expect.anything() },
2038
+ });
2039
+ });
1190
2040
  });
1191
2041
 
1192
2042
  describe("extractNotification", () => {
@@ -1207,6 +2057,136 @@ defineFeature("f", (r) => {
1207
2057
  trigger: { on: "incident:create" },
1208
2058
  });
1209
2059
  });
2060
+
2061
+ test("object form captures name + trigger/recipient/data", () => {
2062
+ const result = parseInline(`
2063
+ defineFeature("f", (r) => {
2064
+ r.notification({
2065
+ name: "incidentOpened",
2066
+ trigger: { on: "incident:create" },
2067
+ recipient: (event) => ({ tenant: event.tenantId }),
2068
+ data: (event) => ({ id: event.id }),
2069
+ });
2070
+ });
2071
+ `);
2072
+
2073
+ expect(result.patterns[0]).toMatchObject({
2074
+ kind: "notification",
2075
+ notificationName: "incidentOpened",
2076
+ });
2077
+ });
2078
+
2079
+ test("captures optional template bodies", () => {
2080
+ const result = parseInline(`
2081
+ defineFeature("f", (r) => {
2082
+ r.notification("incidentOpened", {
2083
+ trigger: { on: "incident:create" },
2084
+ recipient: (event) => ({}),
2085
+ data: (event) => ({}),
2086
+ templates: {
2087
+ email: (payload) => payload.subject,
2088
+ inApp: (payload) => payload.title,
2089
+ },
2090
+ });
2091
+ });
2092
+ `);
2093
+
2094
+ const templates = (result.patterns[0] as { templates?: Record<string, unknown> } | undefined)
2095
+ ?.templates;
2096
+ expect(Object.keys(templates ?? {})).toEqual(["email", "inApp"]);
2097
+ });
2098
+
2099
+ test("emits ParseError when called with no arguments", () => {
2100
+ const result = parseInline(`
2101
+ defineFeature("f", (r) => {
2102
+ r.notification();
2103
+ });
2104
+ `);
2105
+
2106
+ expect(result.errors[0]?.methodName).toBe("notification");
2107
+ });
2108
+
2109
+ test("emits ParseError when object form is missing the name property", () => {
2110
+ const result = parseInline(`
2111
+ defineFeature("f", (r) => {
2112
+ r.notification({ trigger: { on: "x" }, recipient: () => ({}), data: () => ({}) });
2113
+ });
2114
+ `);
2115
+
2116
+ expect(result.errors[0]?.methodName).toBe("notification");
2117
+ });
2118
+
2119
+ test("emits ParseError when the notification name is not a string literal", () => {
2120
+ const result = parseInline(`
2121
+ defineFeature("f", (r) => {
2122
+ r.notification(NAME, { trigger: { on: "x" }, recipient: () => ({}), data: () => ({}) });
2123
+ });
2124
+ `);
2125
+
2126
+ expect(result.errors[0]?.methodName).toBe("notification");
2127
+ });
2128
+
2129
+ test("emits ParseError when the definition object is missing", () => {
2130
+ const result = parseInline(`
2131
+ defineFeature("f", (r) => {
2132
+ r.notification("incidentOpened");
2133
+ });
2134
+ `);
2135
+
2136
+ expect(result.errors[0]?.methodName).toBe("notification");
2137
+ });
2138
+
2139
+ test("emits ParseError when trigger is missing", () => {
2140
+ const result = parseInline(`
2141
+ defineFeature("f", (r) => {
2142
+ r.notification("incidentOpened", { recipient: () => ({}), data: () => ({}) });
2143
+ });
2144
+ `);
2145
+
2146
+ expect(result.errors[0]?.methodName).toBe("notification");
2147
+ });
2148
+
2149
+ test("emits ParseError when trigger.on is not a string/ref", () => {
2150
+ const result = parseInline(`
2151
+ defineFeature("f", (r) => {
2152
+ r.notification("incidentOpened", {
2153
+ trigger: { on: 123 },
2154
+ recipient: () => ({}),
2155
+ data: () => ({}),
2156
+ });
2157
+ });
2158
+ `);
2159
+
2160
+ expect(result.errors[0]?.methodName).toBe("notification");
2161
+ });
2162
+
2163
+ test("emits ParseError when recipient is not an inline function", () => {
2164
+ const result = parseInline(`
2165
+ defineFeature("f", (r) => {
2166
+ r.notification("incidentOpened", {
2167
+ trigger: { on: "incident:create" },
2168
+ recipient: recipientRef,
2169
+ data: () => ({}),
2170
+ });
2171
+ });
2172
+ `);
2173
+
2174
+ expect(result.errors[0]?.methodName).toBe("notification");
2175
+ });
2176
+
2177
+ test("emits ParseError when data is not an inline function", () => {
2178
+ const result = parseInline(`
2179
+ defineFeature("f", (r) => {
2180
+ r.notification("incidentOpened", {
2181
+ trigger: { on: "incident:create" },
2182
+ recipient: () => ({}),
2183
+ data: dataRef,
2184
+ });
2185
+ });
2186
+ `);
2187
+
2188
+ expect(result.errors[0]?.methodName).toBe("notification");
2189
+ });
1210
2190
  });
1211
2191
 
1212
2192
  describe("extractProjection", () => {