@cosmicdrift/kumiko-framework 0.153.0 → 0.154.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 (31) hide show
  1. package/package.json +2 -2
  2. package/src/__tests__/anonymous-access.integration.test.ts +25 -0
  3. package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
  4. package/src/engine/__tests__/boot-validator.test.ts +7 -4
  5. package/src/engine/__tests__/event-migration-declarative.test.ts +9 -2
  6. package/src/engine/boot-validator/action-wiring.ts +99 -0
  7. package/src/engine/boot-validator/index.ts +7 -0
  8. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +7 -21
  9. package/src/engine/feature-ast/__tests__/parse.test.ts +33 -6
  10. package/src/engine/feature-ast/__tests__/patch.test.ts +8 -13
  11. package/src/engine/feature-ast/__tests__/patcher.test.ts +4 -14
  12. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +37 -1
  13. package/src/engine/feature-ast/extractors/index.ts +0 -1
  14. package/src/engine/feature-ast/extractors/round4.ts +92 -136
  15. package/src/engine/feature-ast/index.ts +0 -2
  16. package/src/engine/feature-ast/parse.ts +0 -3
  17. package/src/engine/feature-ast/patch.ts +0 -41
  18. package/src/engine/feature-ast/patcher.ts +14 -20
  19. package/src/engine/feature-ast/patterns.ts +10 -19
  20. package/src/engine/feature-ast/render.ts +10 -13
  21. package/src/engine/feature-config-events-jobs.ts +77 -51
  22. package/src/engine/pattern-library/__tests__/library.test.ts +0 -10
  23. package/src/engine/pattern-library/library.ts +0 -2
  24. package/src/engine/pattern-library/mixed-schemas.ts +8 -35
  25. package/src/engine/registry-validate.ts +6 -6
  26. package/src/engine/types/feature.ts +18 -17
  27. package/src/engine/types/handlers.ts +6 -3
  28. package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
  29. package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
  30. package/src/stack/redis.ts +8 -0
  31. package/src/stack/test-stack.ts +156 -136
@@ -114,7 +114,7 @@ function stripLocations(value: unknown): unknown {
114
114
  out[k] = { raw: normalizeBodyRaw((v as { raw: string }).raw) };
115
115
  continue;
116
116
  }
117
- if (k === "opaqueProps" && v && typeof v === "object") {
117
+ if ((k === "opaqueProps" || k === "migrations") && v && typeof v === "object") {
118
118
  const op: Record<string, unknown> = {};
119
119
  for (const [pk, pv] of Object.entries(v as Record<string, unknown>)) {
120
120
  if (pv && typeof pv === "object" && "raw" in pv) {
@@ -400,3 +400,39 @@ describe("render → parse roundtrip — r.screen({ nav }) inline sugar", () =>
400
400
  }
401
401
  });
402
402
  });
403
+
404
+ const DEFINE_EVENT_WITH_MIGRATIONS_FEATURE = `
405
+ import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
406
+
407
+ defineFeature("billing", (r) => {
408
+ r.defineEvent("invoicePaid", z.object({ totalCents: z.number() }), {
409
+ version: 2,
410
+ migrations: [
411
+ {
412
+ fromVersion: 1,
413
+ toVersion: 2,
414
+ transform: (payload) => ({ totalCents: Math.round(payload.total * 100) }),
415
+ },
416
+ ],
417
+ });
418
+ });
419
+ `;
420
+
421
+ describe("render → parse roundtrip — r.defineEvent({ migrations }) fold", () => {
422
+ test("migrations array survives parse → render → parse unchanged", () => {
423
+ const initial = parse(DEFINE_EVENT_WITH_MIGRATIONS_FEATURE);
424
+ const rendered = renderFeatureFile({
425
+ featureName: initial.featureName ?? "",
426
+ patterns: initial.patterns,
427
+ });
428
+ const reparsed = parse(rendered);
429
+ expect(reparsed.patterns.map(stripLocations)).toEqual(initial.patterns.map(stripLocations));
430
+
431
+ const eventPattern = reparsed.patterns.find((p) => p.kind === "defineEvent");
432
+ expect(eventPattern?.kind).toBe("defineEvent");
433
+ if (eventPattern?.kind === "defineEvent") {
434
+ expect(eventPattern.version).toBe(2);
435
+ expect(Object.keys(eventPattern.migrations ?? {})).toEqual(["1"]);
436
+ }
437
+ });
438
+ });
@@ -30,7 +30,6 @@ export {
30
30
  extractAuthClaims,
31
31
  extractDefineEvent,
32
32
  extractEntityHook,
33
- extractEventMigration,
34
33
  extractHook,
35
34
  extractHttpRoute,
36
35
  extractJob,
@@ -11,7 +11,6 @@ import type {
11
11
  AuthClaimsPattern,
12
12
  DefineEventPattern,
13
13
  EntityHookPattern,
14
- EventMigrationPattern,
15
14
  HookPattern,
16
15
  HttpRoutePattern,
17
16
  JobPattern,
@@ -805,6 +804,74 @@ export function extractHttpRoute(
805
804
  });
806
805
  }
807
806
 
807
+ // Reads defineEvent's `migrations` option — either the array-of-steps shape
808
+ // used by hand-authored calls (`[{ fromVersion, toVersion, transform }]`,
809
+ // toVersion is redundant on-disk since it is always fromVersion + 1) or the
810
+ // keyed-object canonical shape the Designer renders (`{ "1": transform }`).
811
+ // Keyed by fromVersion (as a string) → the transform closure's location.
812
+ // Skips malformed entries rather than failing the whole defineEvent extract
813
+ // — same "best-effort, degrade gracefully" posture as readDataLiteralNode.
814
+ function extractEventMigrationsFromArray(
815
+ arr: Node,
816
+ sourceFile: SourceFile,
817
+ ): Record<string, SourceLocation> {
818
+ const result: Record<string, SourceLocation> = {};
819
+ const arrLit = arr.asKindOrThrow(SyntaxKind.ArrayLiteralExpression);
820
+ for (const el of arrLit.getElements()) {
821
+ const obj = el.asKind(SyntaxKind.ObjectLiteralExpression);
822
+ if (!obj) continue;
823
+ const fromInit = obj
824
+ .getProperty("fromVersion")
825
+ ?.asKind(SyntaxKind.PropertyAssignment)
826
+ ?.getInitializer();
827
+ const fromVersion = fromInit ? readDataLiteralNode(fromInit) : undefined;
828
+ if (typeof fromVersion !== "number") continue;
829
+ const transformInit = obj
830
+ .getProperty("transform")
831
+ ?.asKind(SyntaxKind.PropertyAssignment)
832
+ ?.getInitializer();
833
+ const fn = transformInit ? findFunctionLiteral(transformInit) : undefined;
834
+ if (!fn) continue;
835
+ result[String(fromVersion)] = sourceLocationFromNode(fn, sourceFile);
836
+ }
837
+ return result;
838
+ }
839
+
840
+ function extractEventMigrationsFromKeyedObject(
841
+ objLit: Node,
842
+ sourceFile: SourceFile,
843
+ ): Record<string, SourceLocation> {
844
+ const result: Record<string, SourceLocation> = {};
845
+ const obj = objLit.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
846
+ for (const prop of obj.getProperties()) {
847
+ const propAssign = prop.asKind(SyntaxKind.PropertyAssignment);
848
+ const initializer = propAssign?.getInitializer();
849
+ const fn = initializer ? findFunctionLiteral(initializer) : undefined;
850
+ if (!propAssign || !fn) continue;
851
+ result[readPropertyKey(propAssign)] = sourceLocationFromNode(fn, sourceFile);
852
+ }
853
+ return result;
854
+ }
855
+
856
+ // Reads defineEvent's `migrations` option — either the array-of-steps shape
857
+ // used by hand-authored calls (`[{ fromVersion, toVersion, transform }]`,
858
+ // toVersion is redundant on-disk since it is always fromVersion + 1) or the
859
+ // keyed-object canonical shape the Designer renders (`{ "1": transform }`).
860
+ // Keyed by fromVersion (as a string) → the transform closure's location.
861
+ // Skips malformed entries rather than failing the whole defineEvent extract
862
+ // — same "best-effort, degrade gracefully" posture as readDataLiteralNode.
863
+ function extractEventMigrationsField(
864
+ node: Node,
865
+ sourceFile: SourceFile,
866
+ ): Readonly<Record<string, SourceLocation>> | undefined {
867
+ const result = node.asKind(SyntaxKind.ArrayLiteralExpression)
868
+ ? extractEventMigrationsFromArray(node, sourceFile)
869
+ : node.asKind(SyntaxKind.ObjectLiteralExpression)
870
+ ? extractEventMigrationsFromKeyedObject(node, sourceFile)
871
+ : undefined;
872
+ return result && Object.keys(result).length > 0 ? result : undefined;
873
+ }
874
+
808
875
  export function extractDefineEvent(
809
876
  call: CallExpression,
810
877
  sourceFile: SourceFile,
@@ -853,12 +920,20 @@ export function extractDefineEvent(
853
920
  const v = readDataLiteralNode(versionInit);
854
921
  if (typeof v === "number") version = v;
855
922
  }
923
+ const migrationsInit = obj
924
+ .getProperty("migrations")
925
+ ?.asKind(SyntaxKind.PropertyAssignment)
926
+ ?.getInitializer();
927
+ const migrations = migrationsInit
928
+ ? extractEventMigrationsField(migrationsInit, sourceFile)
929
+ : undefined;
856
930
  return ok({
857
931
  kind: "defineEvent",
858
932
  source: sourceLocationFromNode(call, sourceFile),
859
933
  eventName: nameInit.getLiteralValue(),
860
934
  schemaSource: sourceLocationFromNode(schemaInit, sourceFile),
861
935
  ...(version !== undefined && { version }),
936
+ ...(migrations !== undefined && { migrations }),
862
937
  });
863
938
  }
864
939
 
@@ -879,152 +954,33 @@ export function extractDefineEvent(
879
954
  );
880
955
  }
881
956
  let version: number | undefined;
957
+ let migrations: Readonly<Record<string, SourceLocation>> | undefined;
882
958
  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")
959
+ const optionsObj = optionsArg?.asKind(SyntaxKind.ObjectLiteralExpression);
960
+ if (optionsObj) {
961
+ const versionInit = optionsObj
962
+ .getProperty("version")
940
963
  ?.asKind(SyntaxKind.PropertyAssignment)
941
964
  ?.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
- );
965
+ if (versionInit) {
966
+ const v = readDataLiteralNode(versionInit);
967
+ if (typeof v === "number") version = v;
949
968
  }
950
- const transformInit = obj
951
- .getProperty("transform")
969
+ const migrationsInit = optionsObj
970
+ .getProperty("migrations")
952
971
  ?.asKind(SyntaxKind.PropertyAssignment)
953
972
  ?.getInitializer();
954
- if (!transformInit) {
955
- return fail(
956
- "eventMigration",
957
- sourceLocationFromNode(call, sourceFile),
958
- "object form requires a `transform` property",
959
- );
973
+ if (migrationsInit) {
974
+ migrations = extractEventMigrationsField(migrationsInit, sourceFile);
960
975
  }
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
976
  }
1021
977
  return ok({
1022
- kind: "eventMigration",
978
+ kind: "defineEvent",
1023
979
  source: sourceLocationFromNode(call, sourceFile),
1024
980
  eventName: nameArg.getLiteralValue(),
1025
- fromVersion,
1026
- toVersion,
1027
- transformBody: sourceLocationFromNode(fn, sourceFile),
981
+ schemaSource: sourceLocationFromNode(schemaArg, sourceFile),
982
+ ...(version !== undefined && { version }),
983
+ ...(migrations !== undefined && { migrations }),
1028
984
  });
1029
985
  }
1030
986
 
@@ -28,7 +28,6 @@ export type {
28
28
  AddDefineEventArgs,
29
29
  AddEntityArgs,
30
30
  AddEntityHookArgs,
31
- AddEventMigrationArgs,
32
31
  AddHookArgs,
33
32
  AddHttpRouteArgs,
34
33
  AddJobArgs,
@@ -62,7 +61,6 @@ export type {
62
61
  EntityHookPattern,
63
62
  // Static patterns
64
63
  EntityPattern,
65
- EventMigrationPattern,
66
64
  ExtendsRegistrarPattern,
67
65
  FeaturePattern,
68
66
  FeaturePatternKind,
@@ -40,7 +40,6 @@ import {
40
40
  extractEntity,
41
41
  extractEntityHook,
42
42
  extractEnvSchema,
43
- extractEventMigration,
44
43
  extractExposesApi,
45
44
  extractExtendsRegistrar,
46
45
  extractHook,
@@ -457,8 +456,6 @@ function dispatchExtractor(
457
456
  return extractHttpRoute(call, sourceFile);
458
457
  case "defineEvent":
459
458
  return extractDefineEvent(call, sourceFile);
460
- case "eventMigration":
461
- return extractEventMigration(call, sourceFile);
462
459
  case "notification":
463
460
  return extractNotification(call, sourceFile);
464
461
  case "projection":
@@ -66,12 +66,6 @@ export type PatternId =
66
66
  | { readonly kind: "projection"; readonly name: string }
67
67
  | { readonly kind: "multiStreamProjection"; readonly name: string }
68
68
  | { 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
69
  | { readonly kind: "extendsRegistrar"; readonly extensionName: string }
76
70
  // Singleton patterns — only one per feature, kind alone identifies them.
77
71
  | { readonly kind: "requires" }
@@ -419,20 +413,6 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
419
413
  return (
420
414
  matchFirstArgString(call, id.eventName) || matchObjectProperty(call, "name", id.eventName)
421
415
  );
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
416
  case "extendsRegistrar":
437
417
  return matchFirstArgString(call, id.extensionName);
438
418
  default: {
@@ -459,27 +439,6 @@ function matchObjectProperty(call: CallExpression, propName: string, expected: s
459
439
  return init?.getLiteralValue() === expected;
460
440
  }
461
441
 
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;
469
- const init = obj
470
- .getProperty(propName)
471
- ?.asKind(SyntaxKind.PropertyAssignment)
472
- ?.getInitializer()
473
- ?.asKind(SyntaxKind.NumericLiteral);
474
- return init !== undefined && Number(init.getText()) === expected;
475
- }
476
-
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());
481
- }
482
-
483
442
  // =============================================================================
484
443
  // Format helpers — line boundaries / blank-line collapse
485
444
  // (indent / PATTERN_INDENT live in render.ts and are imported above.)
@@ -144,13 +144,10 @@ export type AddDefineEventArgs = {
144
144
  readonly name: string;
145
145
  readonly schemaSource: string;
146
146
  readonly version?: number;
147
- };
148
-
149
- export type AddEventMigrationArgs = {
150
- readonly event: string;
151
- readonly fromVersion: number;
152
- readonly toVersion: number;
153
- readonly transformSource: string;
147
+ // Keyed by fromVersion (as a string) → the transform source text for the
148
+ // fromVersion -> fromVersion+1 step. Folded in from the former
149
+ // addEventMigration (#1082 step 8).
150
+ readonly migrations?: Readonly<Record<string, string>>;
154
151
  };
155
152
 
156
153
  export type AddProjectionArgs = {
@@ -249,7 +246,6 @@ export type FeaturePatcher = {
249
246
  readonly addProjection: (args: AddProjectionArgs) => void;
250
247
  readonly addMultiStreamProjection: (args: AddMultiStreamProjectionArgs) => void;
251
248
  readonly addDefineEvent: (args: AddDefineEventArgs) => void;
252
- readonly addEventMigration: (args: AddEventMigrationArgs) => void;
253
249
  // --- Symmetric ops (id-driven) ---
254
250
  readonly replace: (id: PatternId, pattern: FeaturePattern) => void;
255
251
  readonly remove: (id: PatternId) => void;
@@ -495,24 +491,22 @@ export function createFeaturePatcher(sourceFile: SourceFile): FeaturePatcher {
495
491
  });
496
492
  },
497
493
 
498
- addDefineEvent({ name, schemaSource, version }) {
494
+ addDefineEvent({ name, schemaSource, version, migrations }) {
495
+ const migrationLocs = migrations
496
+ ? Object.fromEntries(
497
+ Object.entries(migrations).map(([fromVersion, source]) => [
498
+ fromVersion,
499
+ rawLoc(source),
500
+ ]),
501
+ )
502
+ : undefined;
499
503
  add({
500
504
  kind: "defineEvent",
501
505
  source: SYNTHETIC_LOC,
502
506
  eventName: name,
503
507
  schemaSource: rawLoc(schemaSource),
504
508
  ...(version !== undefined && { version }),
505
- });
506
- },
507
-
508
- addEventMigration({ event, fromVersion, toVersion, transformSource }) {
509
- add({
510
- kind: "eventMigration",
511
- source: SYNTHETIC_LOC,
512
- eventName: event,
513
- fromVersion,
514
- toVersion,
515
- transformBody: rawLoc(transformSource),
509
+ ...(migrationLocs !== undefined && { migrations: migrationLocs }),
516
510
  });
517
511
  },
518
512
 
@@ -501,29 +501,22 @@ export type MultiStreamProjectionPattern = {
501
501
  // `r.defineEvent(name, schema, options?)` — registers an event payload
502
502
  // shape and returns the qualified `EventDef`, so callers pass `.name` to
503
503
  // `ctx.appendEvent` instead of hand-building `<feature>:event:<short>`.
504
- // `options.version` declares the CURRENT schema generation (default 1);
505
- // bump it together with an `r.eventMigration` step the framework refuses
506
- // to boot if the chain from 1 to the current version has gaps.
504
+ // `options.version` declares the CURRENT schema generation (default 1).
505
+ // `options.migrations` is a step-wise upcast chainkeyed by fromVersion
506
+ // (toVersion is always fromVersion + 1, enforced at registration, so it is
507
+ // not stored separately); the framework refuses to boot if the chain from
508
+ // 1 to `version` has gaps. Formerly a separate `r.eventMigration()` call
509
+ // per step, folded in here (#1082 step 8) — an event and its schema
510
+ // evolution are one lifecycle, not two registrar concepts.
507
511
  export type DefineEventPattern = {
508
512
  readonly kind: "defineEvent";
509
513
  readonly source: SourceLocation;
510
514
  readonly eventName: string;
511
515
  readonly schemaSource: SourceLocation;
512
516
  readonly version?: number;
513
- };
514
-
515
- // `r.eventMigration(eventName, fromVersion, toVersion, transform)` —
516
- // registers a step-wise payload upcast for event-schema evolution.
517
- // `toVersion` must be `fromVersion + 1`; chain larger jumps step by step.
518
- // Transforms are pure old-payload-in/new-payload-out functions and run once
519
- // per READ, not once per event persisted — keep them cheap.
520
- export type EventMigrationPattern = {
521
- readonly kind: "eventMigration";
522
- readonly source: SourceLocation;
523
- readonly eventName: string;
524
- readonly fromVersion: number;
525
- readonly toVersion: number;
526
- readonly transformBody: SourceLocation;
517
+ // Map fromVersion (as string, e.g. "1") → SourceLocation of the transform
518
+ // closure for the fromVersion → fromVersion+1 step.
519
+ readonly migrations?: Readonly<Record<string, SourceLocation>>;
527
520
  };
528
521
 
529
522
  // `r.extendsRegistrar(extensionName, def)` — declares a named, globally
@@ -618,7 +611,6 @@ export type FeaturePattern =
618
611
  | ProjectionPattern
619
612
  | MultiStreamProjectionPattern
620
613
  | DefineEventPattern
621
- | EventMigrationPattern
622
614
  | ExtendsRegistrarPattern
623
615
  | EnvSchemaPattern
624
616
  // Catch-all
@@ -674,7 +666,6 @@ export function getEditability(pattern: FeaturePattern): Editability {
674
666
  case "projection":
675
667
  case "multiStreamProjection":
676
668
  case "defineEvent":
677
- case "eventMigration":
678
669
  return "mixed";
679
670
  case "authClaims":
680
671
  case "extendsRegistrar":
@@ -26,7 +26,6 @@ import type {
26
26
  EntityHookPattern,
27
27
  EntityPattern,
28
28
  EnvSchemaPattern,
29
- EventMigrationPattern,
30
29
  ExposesApiPattern,
31
30
  ExtendsRegistrarPattern,
32
31
  FeaturePattern,
@@ -129,8 +128,6 @@ export function renderPattern(pattern: FeaturePattern): string {
129
128
  return renderMultiStreamProjection(pattern);
130
129
  case "defineEvent":
131
130
  return renderDefineEvent(pattern);
132
- case "eventMigration":
133
- return renderEventMigration(pattern);
134
131
  case "extendsRegistrar":
135
132
  return renderExtendsRegistrar(pattern);
136
133
  case "usesApi":
@@ -505,16 +502,16 @@ function renderDefineEvent(p: DefineEventPattern): string {
505
502
  lines.push(` name: ${JSON.stringify(p.eventName)},`);
506
503
  lines.push(` schema: ${p.schemaSource.raw},`);
507
504
  if (p.version !== undefined) lines.push(` version: ${p.version},`);
508
- lines.push("});");
509
- return lines.join("\n");
510
- }
511
-
512
- function renderEventMigration(p: EventMigrationPattern): string {
513
- const lines: string[] = ["r.eventMigration({"];
514
- lines.push(` event: ${JSON.stringify(p.eventName)},`);
515
- lines.push(` fromVersion: ${p.fromVersion},`);
516
- lines.push(` toVersion: ${p.toVersion},`);
517
- lines.push(` transform: ${p.transformBody.raw},`);
505
+ if (p.migrations !== undefined) {
506
+ const entries = Object.entries(p.migrations);
507
+ if (entries.length > 0) {
508
+ lines.push(" migrations: {");
509
+ for (const [fromVersion, transformBody] of entries) {
510
+ lines.push(` "${fromVersion}": ${transformBody.raw},`);
511
+ }
512
+ lines.push(" },");
513
+ }
514
+ }
518
515
  lines.push("});");
519
516
  return lines.join("\n");
520
517
  }