@cosmicdrift/kumiko-framework 0.154.0 → 0.154.2

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 (46) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/full-stack.integration.test.ts +1 -1
  3. package/src/api/__tests__/batch.integration.test.ts +9 -9
  4. package/src/engine/__tests__/engine.test.ts +16 -0
  5. package/src/engine/__tests__/event-migration-declarative.test.ts +9 -2
  6. package/src/engine/__tests__/hook-phases.test.ts +5 -5
  7. package/src/engine/__tests__/post-query-hook.test.ts +7 -7
  8. package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
  9. package/src/engine/define-feature.ts +22 -4
  10. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +15 -28
  11. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
  12. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +1 -1
  13. package/src/engine/feature-ast/__tests__/parse.test.ts +55 -14
  14. package/src/engine/feature-ast/__tests__/patch.test.ts +8 -13
  15. package/src/engine/feature-ast/__tests__/patcher.test.ts +12 -22
  16. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +247 -5
  17. package/src/engine/feature-ast/extractors/index.ts +0 -3
  18. package/src/engine/feature-ast/extractors/round4.ts +113 -287
  19. package/src/engine/feature-ast/index.ts +0 -4
  20. package/src/engine/feature-ast/parse.ts +0 -6
  21. package/src/engine/feature-ast/patch.ts +35 -45
  22. package/src/engine/feature-ast/patcher.ts +18 -40
  23. package/src/engine/feature-ast/patterns.ts +20 -41
  24. package/src/engine/feature-ast/render.ts +17 -27
  25. package/src/engine/feature-config-events-jobs.ts +150 -74
  26. package/src/engine/feature-entity-handlers.ts +24 -6
  27. package/src/engine/feature-ui-extensions.ts +116 -45
  28. package/src/engine/object-form.ts +12 -0
  29. package/src/engine/pattern-library/__tests__/library.test.ts +0 -19
  30. package/src/engine/pattern-library/library.ts +0 -4
  31. package/src/engine/pattern-library/mixed-schemas.ts +9 -80
  32. package/src/engine/pattern-library/shared-fields.ts +1 -6
  33. package/src/engine/registry-ingest.ts +7 -2
  34. package/src/engine/registry-validate.ts +6 -6
  35. package/src/engine/types/feature.ts +76 -45
  36. package/src/engine/types/handlers.ts +6 -3
  37. package/src/engine/types/nav.ts +4 -0
  38. package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
  39. package/src/jobs/__tests__/jobs.integration.test.ts +66 -1
  40. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
  41. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +1 -1
  42. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
  43. package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
  44. package/src/search/__tests__/meilisearch-adapter.integration.test.ts +200 -185
  45. package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
  46. package/src/search/meilisearch-adapter.ts +13 -12
@@ -10,8 +10,6 @@ import type { ScreenDefinition } from "../../types/screen";
10
10
  import type {
11
11
  AuthClaimsPattern,
12
12
  DefineEventPattern,
13
- EntityHookPattern,
14
- EventMigrationPattern,
15
13
  HookPattern,
16
14
  HttpRoutePattern,
17
15
  JobPattern,
@@ -91,6 +89,25 @@ export function readOptionalRateLimit(value: unknown): RateLimitOption | undefin
91
89
  return value as unknown as RateLimitOption;
92
90
  }
93
91
 
92
+ // r.hook's target: a NameOrRef, a list of them, or an entity-wide
93
+ // `{ allOf: entityRef }` (replaces the old r.entityHook(type, entity, fn)).
94
+ // Checked first so a malformed `{ allOf }` doesn't silently fall through
95
+ // to being read as some other object shape.
96
+ function readHookTarget(
97
+ node: Node,
98
+ ): string | readonly string[] | { readonly allOf: string } | undefined {
99
+ const obj = node.asKind(SyntaxKind.ObjectLiteralExpression);
100
+ if (obj) {
101
+ const allOfProp = obj.getProperty("allOf")?.asKind(SyntaxKind.PropertyAssignment);
102
+ if (allOfProp) {
103
+ const initializer = allOfProp.getInitializer();
104
+ const entityName = initializer && readNameOrRef(initializer);
105
+ return entityName ? { allOf: entityName } : undefined;
106
+ }
107
+ }
108
+ return readNameOrRefOrList(node);
109
+ }
110
+
94
111
  export function extractHook(
95
112
  call: CallExpression,
96
113
  sourceFile: SourceFile,
@@ -134,7 +151,7 @@ export function extractHook(
134
151
  "object form requires a `target` property",
135
152
  );
136
153
  }
137
- const target = readNameOrRefOrList(targetInit);
154
+ const target = readHookTarget(targetInit);
138
155
  if (!target) {
139
156
  return fail(
140
157
  "hook",
@@ -196,7 +213,7 @@ export function extractHook(
196
213
  "expected a target (NameOrRef or array) as second argument",
197
214
  );
198
215
  }
199
- const target = readNameOrRefOrList(targetArg);
216
+ const target = readHookTarget(targetArg);
200
217
  if (!target) {
201
218
  return fail(
202
219
  "hook",
@@ -231,154 +248,6 @@ export function extractHook(
231
248
  });
232
249
  }
233
250
 
234
- export function isEntityHookType(value: string): value is "postSave" | "preDelete" | "postDelete" {
235
- return value === "postSave" || value === "preDelete" || value === "postDelete";
236
- }
237
-
238
- export function extractEntityHook(
239
- call: CallExpression,
240
- sourceFile: SourceFile,
241
- ): ExtractOutput<EntityHookPattern> {
242
- const args = call.getArguments();
243
- const first = args[0];
244
- if (!first) {
245
- return fail(
246
- "entityHook",
247
- sourceLocationFromNode(call, sourceFile),
248
- "expected at least one argument",
249
- );
250
- }
251
-
252
- const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
253
- if (obj && args.length === 1) {
254
- const typeInit = obj
255
- .getProperty("type")
256
- ?.asKind(SyntaxKind.PropertyAssignment)
257
- ?.getInitializer()
258
- ?.asKind(SyntaxKind.StringLiteral);
259
- if (!typeInit) {
260
- return fail(
261
- "entityHook",
262
- sourceLocationFromNode(call, sourceFile),
263
- "object form requires a string-literal `type` property",
264
- );
265
- }
266
- const hookType = typeInit.getLiteralValue();
267
- if (!isEntityHookType(hookType)) {
268
- return fail(
269
- "entityHook",
270
- sourceLocationFromNode(call, sourceFile),
271
- `entity hook type must be postSave, preDelete, or postDelete (got "${hookType}")`,
272
- );
273
- }
274
- const entityInit = obj
275
- .getProperty("entity")
276
- ?.asKind(SyntaxKind.PropertyAssignment)
277
- ?.getInitializer();
278
- if (!entityInit) {
279
- return fail(
280
- "entityHook",
281
- sourceLocationFromNode(call, sourceFile),
282
- "object form requires an `entity` property",
283
- );
284
- }
285
- const entityName = readNameOrRef(entityInit);
286
- if (!entityName) {
287
- return fail(
288
- "entityHook",
289
- sourceLocationFromNode(call, sourceFile),
290
- "`entity` must be a string literal or inline { name } object",
291
- );
292
- }
293
- const handlerInit = obj
294
- .getProperty("handler")
295
- ?.asKind(SyntaxKind.PropertyAssignment)
296
- ?.getInitializer();
297
- if (!handlerInit) {
298
- return fail(
299
- "entityHook",
300
- sourceLocationFromNode(call, sourceFile),
301
- "object form requires a `handler` property",
302
- );
303
- }
304
- const fn = findFunctionLiteral(handlerInit);
305
- if (!fn) {
306
- return fail(
307
- "entityHook",
308
- sourceLocationFromNode(call, sourceFile),
309
- "handler must be an inline arrow function or function expression",
310
- );
311
- }
312
- const phase = readOptionalPhase(obj);
313
- return ok({
314
- kind: "entityHook",
315
- source: sourceLocationFromNode(call, sourceFile),
316
- hookType,
317
- entityName,
318
- fnBody: sourceLocationFromNode(fn, sourceFile),
319
- ...(phase !== undefined && { phase }),
320
- });
321
- }
322
-
323
- const typeArg = first.asKind(SyntaxKind.StringLiteral);
324
- if (!typeArg) {
325
- return fail(
326
- "entityHook",
327
- sourceLocationFromNode(call, sourceFile),
328
- "first argument must be a string literal hook type (or use the object form)",
329
- );
330
- }
331
- const hookType = typeArg.getLiteralValue();
332
- if (!isEntityHookType(hookType)) {
333
- return fail(
334
- "entityHook",
335
- sourceLocationFromNode(call, sourceFile),
336
- `entity hook type must be postSave, preDelete, or postDelete (got "${hookType}")`,
337
- );
338
- }
339
- const entityArg = args[1];
340
- if (!entityArg) {
341
- return fail(
342
- "entityHook",
343
- sourceLocationFromNode(call, sourceFile),
344
- "expected an entity reference as second argument",
345
- );
346
- }
347
- const entityName = readNameOrRef(entityArg);
348
- if (!entityName) {
349
- return fail(
350
- "entityHook",
351
- sourceLocationFromNode(call, sourceFile),
352
- "second argument must be a string literal or inline { name } object",
353
- );
354
- }
355
- const fnArg = args[2];
356
- if (!fnArg) {
357
- return fail(
358
- "entityHook",
359
- sourceLocationFromNode(call, sourceFile),
360
- "expected a hook function as third argument",
361
- );
362
- }
363
- const fn = findFunctionLiteral(fnArg);
364
- if (!fn) {
365
- return fail(
366
- "entityHook",
367
- sourceLocationFromNode(call, sourceFile),
368
- "third argument must be an inline arrow function or function expression",
369
- );
370
- }
371
- const phase = readOptionalPhase(args[3]);
372
- return ok({
373
- kind: "entityHook",
374
- source: sourceLocationFromNode(call, sourceFile),
375
- hookType,
376
- entityName,
377
- fnBody: sourceLocationFromNode(fn, sourceFile),
378
- ...(phase !== undefined && { phase }),
379
- });
380
- }
381
-
382
251
  // guard:dup-ok — intentionale Parallele zu extractTree (round6); verschiedene Feature-AST-Extraktoren by design
383
252
  export function extractAuthClaims(
384
253
  call: CallExpression,
@@ -805,6 +674,74 @@ export function extractHttpRoute(
805
674
  });
806
675
  }
807
676
 
677
+ // Reads defineEvent's `migrations` option — either the array-of-steps shape
678
+ // used by hand-authored calls (`[{ fromVersion, toVersion, transform }]`,
679
+ // toVersion is redundant on-disk since it is always fromVersion + 1) or the
680
+ // keyed-object canonical shape the Designer renders (`{ "1": transform }`).
681
+ // Keyed by fromVersion (as a string) → the transform closure's location.
682
+ // Skips malformed entries rather than failing the whole defineEvent extract
683
+ // — same "best-effort, degrade gracefully" posture as readDataLiteralNode.
684
+ function extractEventMigrationsFromArray(
685
+ arr: Node,
686
+ sourceFile: SourceFile,
687
+ ): Record<string, SourceLocation> {
688
+ const result: Record<string, SourceLocation> = {};
689
+ const arrLit = arr.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
690
+ for (const el of arrLit.getElements()) {
691
+ const obj = el.asKind(SyntaxKind.ObjectLiteralExpression);
692
+ if (!obj) continue;
693
+ const fromInit = obj
694
+ .getProperty("fromVersion")
695
+ ?.asKind(SyntaxKind.PropertyAssignment)
696
+ ?.getInitializer();
697
+ const fromVersion = fromInit ? readDataLiteralNode(fromInit) : undefined;
698
+ if (typeof fromVersion !== "number") continue;
699
+ const transformInit = obj
700
+ .getProperty("transform")
701
+ ?.asKind(SyntaxKind.PropertyAssignment)
702
+ ?.getInitializer();
703
+ const fn = transformInit ? findFunctionLiteral(transformInit) : undefined;
704
+ if (!fn) continue;
705
+ result[String(fromVersion)] = sourceLocationFromNode(fn, sourceFile);
706
+ }
707
+ return result;
708
+ }
709
+
710
+ function extractEventMigrationsFromKeyedObject(
711
+ objLit: Node,
712
+ sourceFile: SourceFile,
713
+ ): Record<string, SourceLocation> {
714
+ const result: Record<string, SourceLocation> = {};
715
+ const obj = objLit.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
716
+ for (const prop of obj.getProperties()) {
717
+ const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
718
+ const initializer = propAssign?.getInitializer();
719
+ const fn = initializer ? findFunctionLiteral(initializer) : undefined;
720
+ if (!propAssign || !fn) continue;
721
+ result[readPropertyKey(propAssign)] = sourceLocationFromNode(fn, sourceFile);
722
+ }
723
+ return result;
724
+ }
725
+
726
+ // Reads defineEvent's `migrations` option — either the array-of-steps shape
727
+ // used by hand-authored calls (`[{ fromVersion, toVersion, transform }]`,
728
+ // toVersion is redundant on-disk since it is always fromVersion + 1) or the
729
+ // keyed-object canonical shape the Designer renders (`{ "1": transform }`).
730
+ // Keyed by fromVersion (as a string) → the transform closure's location.
731
+ // Skips malformed entries rather than failing the whole defineEvent extract
732
+ // — same "best-effort, degrade gracefully" posture as readDataLiteralNode.
733
+ function extractEventMigrationsField(
734
+ node: Node,
735
+ sourceFile: SourceFile,
736
+ ): Readonly<Record<string, SourceLocation>> | undefined {
737
+ const result = node.asKind(SyntaxKind.ArrayLiteralExpression)
738
+ ? extractEventMigrationsFromArray(node, sourceFile)
739
+ : node.asKind(SyntaxKind.ObjectLiteralExpression)
740
+ ? extractEventMigrationsFromKeyedObject(node, sourceFile)
741
+ : undefined;
742
+ return result && Object.keys(result).length > 0 ? result : undefined;
743
+ }
744
+
808
745
  export function extractDefineEvent(
809
746
  call: CallExpression,
810
747
  sourceFile: SourceFile,
@@ -853,12 +790,20 @@ export function extractDefineEvent(
853
790
  const v = readDataLiteralNode(versionInit);
854
791
  if (typeof v === "number") version = v;
855
792
  }
793
+ const migrationsInit = obj
794
+ .getProperty("migrations")
795
+ ?.asKind(SyntaxKind.PropertyAssignment)
796
+ ?.getInitializer();
797
+ const migrations = migrationsInit
798
+ ? extractEventMigrationsField(migrationsInit, sourceFile)
799
+ : undefined;
856
800
  return ok({
857
801
  kind: "defineEvent",
858
802
  source: sourceLocationFromNode(call, sourceFile),
859
803
  eventName: nameInit.getLiteralValue(),
860
804
  schemaSource: sourceLocationFromNode(schemaInit, sourceFile),
861
805
  ...(version !== undefined && { version }),
806
+ ...(migrations !== undefined && { migrations }),
862
807
  });
863
808
  }
864
809
 
@@ -879,152 +824,33 @@ export function extractDefineEvent(
879
824
  );
880
825
  }
881
826
  let version: number | undefined;
827
+ let migrations: Readonly<Record<string, SourceLocation>> | undefined;
882
828
  const optionsArg = args[2];
883
- if (optionsArg) {
884
- const options = readDataLiteralNode(optionsArg);
885
- if (isPlainObject(options) && typeof options["version"] === "number") {
886
- version = options["version"];
887
- }
888
- }
889
- return ok({
890
- kind: "defineEvent",
891
- source: sourceLocationFromNode(call, sourceFile),
892
- eventName: nameArg.getLiteralValue(),
893
- schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
894
- ...(version !== undefined && { version }),
895
- });
896
- }
897
-
898
- export function extractEventMigration(
899
- call: CallExpression,
900
- sourceFile: SourceFile,
901
- ): ExtractOutput<EventMigrationPattern> {
902
- const args = call.getArguments();
903
- const first = args[0];
904
- if (!first) {
905
- return fail(
906
- "eventMigration",
907
- sourceLocationFromNode(call, sourceFile),
908
- "expected at least one argument",
909
- );
910
- }
911
-
912
- const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
913
- if (obj && args.length === 1) {
914
- const eventInit = obj
915
- .getProperty("event")
916
- ?.asKind(SyntaxKind.PropertyAssignment)
917
- ?.getInitializer()
918
- ?.asKind(SyntaxKind.StringLiteral);
919
- if (!eventInit) {
920
- return fail(
921
- "eventMigration",
922
- sourceLocationFromNode(call, sourceFile),
923
- "object form requires a string-literal `event` property",
924
- );
925
- }
926
- const fromInit = obj
927
- .getProperty("fromVersion")
928
- ?.asKind(SyntaxKind.PropertyAssignment)
929
- ?.getInitializer();
930
- const fromVersion = fromInit ? readDataLiteralNode(fromInit) : undefined;
931
- if (typeof fromVersion !== "number") {
932
- return fail(
933
- "eventMigration",
934
- sourceLocationFromNode(call, sourceFile),
935
- "fromVersion must be a numeric literal",
936
- );
937
- }
938
- const toInit = obj
939
- .getProperty("toVersion")
829
+ const optionsObj = optionsArg?.asKind(SyntaxKind.ObjectLiteralExpression);
830
+ if (optionsObj) {
831
+ const versionInit = optionsObj
832
+ .getProperty("version")
940
833
  ?.asKind(SyntaxKind.PropertyAssignment)
941
834
  ?.getInitializer();
942
- const toVersion = toInit ? readDataLiteralNode(toInit) : undefined;
943
- if (typeof toVersion !== "number") {
944
- return fail(
945
- "eventMigration",
946
- sourceLocationFromNode(call, sourceFile),
947
- "toVersion must be a numeric literal",
948
- );
835
+ if (versionInit) {
836
+ const v = readDataLiteralNode(versionInit);
837
+ if (typeof v === "number") version = v;
949
838
  }
950
- const transformInit = obj
951
- .getProperty("transform")
839
+ const migrationsInit = optionsObj
840
+ .getProperty("migrations")
952
841
  ?.asKind(SyntaxKind.PropertyAssignment)
953
842
  ?.getInitializer();
954
- if (!transformInit) {
955
- return fail(
956
- "eventMigration",
957
- sourceLocationFromNode(call, sourceFile),
958
- "object form requires a `transform` property",
959
- );
843
+ if (migrationsInit) {
844
+ migrations = extractEventMigrationsField(migrationsInit, sourceFile);
960
845
  }
961
- const fn = findFunctionLiteral(transformInit);
962
- if (!fn) {
963
- return fail(
964
- "eventMigration",
965
- sourceLocationFromNode(call, sourceFile),
966
- "transform must be an inline arrow function or function expression",
967
- );
968
- }
969
- return ok({
970
- kind: "eventMigration",
971
- source: sourceLocationFromNode(call, sourceFile),
972
- eventName: eventInit.getLiteralValue(),
973
- fromVersion,
974
- toVersion,
975
- transformBody: sourceLocationFromNode(fn, sourceFile),
976
- });
977
- }
978
-
979
- const nameArg = first.asKind(SyntaxKind.StringLiteral);
980
- if (!nameArg) {
981
- return fail(
982
- "eventMigration",
983
- sourceLocationFromNode(call, sourceFile),
984
- "first argument must be a string literal event name (or use the object form)",
985
- );
986
- }
987
- const fromArg = args[1];
988
- const fromVersion = fromArg ? readDataLiteralNode(fromArg) : undefined;
989
- if (typeof fromVersion !== "number") {
990
- return fail(
991
- "eventMigration",
992
- sourceLocationFromNode(call, sourceFile),
993
- "fromVersion must be a numeric literal",
994
- );
995
- }
996
- const toArg = args[2];
997
- const toVersion = toArg ? readDataLiteralNode(toArg) : undefined;
998
- if (typeof toVersion !== "number") {
999
- return fail(
1000
- "eventMigration",
1001
- sourceLocationFromNode(call, sourceFile),
1002
- "toVersion must be a numeric literal",
1003
- );
1004
- }
1005
- const transformArg = args[3];
1006
- if (!transformArg) {
1007
- return fail(
1008
- "eventMigration",
1009
- sourceLocationFromNode(call, sourceFile),
1010
- "expected a transform function as fourth argument",
1011
- );
1012
- }
1013
- const fn = findFunctionLiteral(transformArg);
1014
- if (!fn) {
1015
- return fail(
1016
- "eventMigration",
1017
- sourceLocationFromNode(call, sourceFile),
1018
- "transform must be an inline arrow function or function expression",
1019
- );
1020
846
  }
1021
847
  return ok({
1022
- kind: "eventMigration",
848
+ kind: "defineEvent",
1023
849
  source: sourceLocationFromNode(call, sourceFile),
1024
850
  eventName: nameArg.getLiteralValue(),
1025
- fromVersion,
1026
- toVersion,
1027
- transformBody: sourceLocationFromNode(fn, sourceFile),
851
+ schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
852
+ ...(version !== undefined && { version }),
853
+ ...(migrations !== undefined && { migrations }),
1028
854
  });
1029
855
  }
1030
856
 
@@ -27,8 +27,6 @@ export type {
27
27
  AddConfigArgs,
28
28
  AddDefineEventArgs,
29
29
  AddEntityArgs,
30
- AddEntityHookArgs,
31
- AddEventMigrationArgs,
32
30
  AddHookArgs,
33
31
  AddHttpRouteArgs,
34
32
  AddJobArgs,
@@ -59,10 +57,8 @@ export type {
59
57
  ConfigPattern,
60
58
  DefineEventPattern,
61
59
  Editability,
62
- EntityHookPattern,
63
60
  // Static patterns
64
61
  EntityPattern,
65
- EventMigrationPattern,
66
62
  ExtendsRegistrarPattern,
67
63
  FeaturePattern,
68
64
  FeaturePatternKind,
@@ -38,9 +38,7 @@ import {
38
38
  extractDefineEvent,
39
39
  extractDescribe,
40
40
  extractEntity,
41
- extractEntityHook,
42
41
  extractEnvSchema,
43
- extractEventMigration,
44
42
  extractExposesApi,
45
43
  extractExtendsRegistrar,
46
44
  extractHook,
@@ -443,8 +441,6 @@ function dispatchExtractor(
443
441
  // Round 4 — mixed (header + body) patterns
444
442
  case "hook":
445
443
  return extractHook(call, sourceFile);
446
- case "entityHook":
447
- return extractEntityHook(call, sourceFile);
448
444
  case "authClaims":
449
445
  return extractAuthClaims(call, sourceFile);
450
446
  case "writeHandler":
@@ -457,8 +453,6 @@ function dispatchExtractor(
457
453
  return extractHttpRoute(call, sourceFile);
458
454
  case "defineEvent":
459
455
  return extractDefineEvent(call, sourceFile);
460
- case "eventMigration":
461
- return extractEventMigration(call, sourceFile);
462
456
  case "notification":
463
457
  return extractNotification(call, sourceFile);
464
458
  case "projection":
@@ -53,8 +53,11 @@ export type PatternId =
53
53
  | { readonly kind: "screen"; readonly id: string }
54
54
  | { readonly kind: "writeHandler"; readonly handlerName: string }
55
55
  | { readonly kind: "queryHandler"; readonly handlerName: string }
56
- | { readonly kind: "hook"; readonly hookType: string; readonly target: string }
57
- | { readonly kind: "entityHook"; readonly hookType: string; readonly entityName: string }
56
+ | {
57
+ readonly kind: "hook";
58
+ readonly hookType: string;
59
+ readonly target: string | { readonly allOf: string };
60
+ }
58
61
  | { readonly kind: "metric"; readonly shortName: string }
59
62
  | { readonly kind: "secret"; readonly shortName: string }
60
63
  | { readonly kind: "claimKey"; readonly shortName: string }
@@ -66,12 +69,6 @@ export type PatternId =
66
69
  | { readonly kind: "projection"; readonly name: string }
67
70
  | { readonly kind: "multiStreamProjection"; readonly name: string }
68
71
  | { readonly kind: "defineEvent"; readonly eventName: string }
69
- | {
70
- readonly kind: "eventMigration";
71
- readonly eventName: string;
72
- readonly fromVersion: number;
73
- readonly toVersion: number;
74
- }
75
72
  | { readonly kind: "extendsRegistrar"; readonly extensionName: string }
76
73
  // Singleton patterns — only one per feature, kind alone identifies them.
77
74
  | { readonly kind: "requires" }
@@ -362,6 +359,18 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
362
359
  );
363
360
  case "hook":
364
361
  // Positional: r.hook(type, target, fn) | Object: { type, target }
362
+ if (typeof id.target === "object") {
363
+ // Entity-wide { allOf } target — only ever appears as the object
364
+ // literal { allOf: entity } (positional or nested in the object
365
+ // form), never as a bare string, so check both call shapes.
366
+ if (matchFirstArgString(call, id.hookType)) {
367
+ return matchAllOfArg(call.getArguments()[1], id.target.allOf);
368
+ }
369
+ return (
370
+ matchObjectProperty(call, "type", id.hookType) &&
371
+ matchObjectAllOfProperty(call, "target", id.target.allOf)
372
+ );
373
+ }
365
374
  if (matchFirstArgString(call, id.hookType)) {
366
375
  const target = call.getArguments()[1]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
367
376
  return target === id.target;
@@ -370,15 +379,6 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
370
379
  matchObjectProperty(call, "type", id.hookType) &&
371
380
  matchObjectProperty(call, "target", id.target)
372
381
  );
373
- case "entityHook":
374
- if (matchFirstArgString(call, id.hookType)) {
375
- const ent = call.getArguments()[1]?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
376
- return ent === id.entityName;
377
- }
378
- return (
379
- matchObjectProperty(call, "type", id.hookType) &&
380
- matchObjectProperty(call, "entity", id.entityName)
381
- );
382
382
  case "metric":
383
383
  case "secret":
384
384
  case "claimKey":
@@ -419,20 +419,6 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
419
419
  return (
420
420
  matchFirstArgString(call, id.eventName) || matchObjectProperty(call, "name", id.eventName)
421
421
  );
422
- case "eventMigration": {
423
- // Positional: r.eventMigration(name, from, to, fn)
424
- if (matchFirstArgString(call, id.eventName)) {
425
- const from = numericArg(call, 1);
426
- const to = numericArg(call, 2);
427
- return from === id.fromVersion && to === id.toVersion;
428
- }
429
- // Object: { event, fromVersion, toVersion }
430
- return (
431
- matchObjectProperty(call, "event", id.eventName) &&
432
- matchObjectNumericProperty(call, "fromVersion", id.fromVersion) &&
433
- matchObjectNumericProperty(call, "toVersion", id.toVersion)
434
- );
435
- }
436
422
  case "extendsRegistrar":
437
423
  return matchFirstArgString(call, id.extensionName);
438
424
  default: {
@@ -459,25 +445,29 @@ function matchObjectProperty(call: CallExpression, propName: string, expected: s
459
445
  return init?.getLiteralValue() === expected;
460
446
  }
461
447
 
462
- function matchObjectNumericProperty(
463
- call: CallExpression,
464
- propName: string,
465
- expected: number,
466
- ): boolean {
467
- const obj = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
468
- if (!obj) return false;
448
+ // Matches an r.hook `{ allOf: entity }` target — as the arg node directly
449
+ // (positional call form) or as a nested object property (object call form).
450
+ function matchAllOfArg(node: Node | undefined, expectedEntity: string): boolean {
451
+ const obj = node?.asKind(SyntaxKind.ObjectLiteralExpression);
469
452
  const init = obj
470
- .getProperty(propName)
453
+ ?.getProperty("allOf")
471
454
  ?.asKind(SyntaxKind.PropertyAssignment)
472
455
  ?.getInitializer()
473
- ?.asKind(SyntaxKind.NumericLiteral);
474
- return init !== undefined && Number(init.getText()) === expected;
456
+ ?.asKind(SyntaxKind.StringLiteral);
457
+ return init?.getLiteralValue() === expectedEntity;
475
458
  }
476
459
 
477
- function numericArg(call: CallExpression, idx: number): number | undefined {
478
- const lit = call.getArguments()[idx]?.asKind(SyntaxKind.NumericLiteral);
479
- if (!lit) return undefined;
480
- return Number(lit.getText());
460
+ function matchObjectAllOfProperty(
461
+ call: CallExpression,
462
+ propName: string,
463
+ expectedEntity: string,
464
+ ): boolean {
465
+ const obj = call.getArguments()[0]?.asKind(SyntaxKind.ObjectLiteralExpression);
466
+ const propInit = obj
467
+ ?.getProperty(propName)
468
+ ?.asKind(SyntaxKind.PropertyAssignment)
469
+ ?.getInitializer();
470
+ return matchAllOfArg(propInit, expectedEntity);
481
471
  }
482
472
 
483
473
  // =============================================================================