@cosmicdrift/kumiko-framework 0.154.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.154.0",
3
+ "version": "0.154.1",
4
4
  "description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -193,7 +193,7 @@
193
193
  "zod": "^4.4.3"
194
194
  },
195
195
  "devDependencies": {
196
- "@cosmicdrift/kumiko-dispatcher-live": "0.154.0",
196
+ "@cosmicdrift/kumiko-dispatcher-live": "0.154.1",
197
197
  "bun-types": "^1.3.13",
198
198
  "pino-pretty": "^13.1.3"
199
199
  },
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { z } from "zod";
2
3
  import { defineFeature } from "../define-feature";
3
4
  import type { DeclarativeEventMigration, EventUpcastCtx } from "../types";
4
5
 
@@ -7,7 +8,10 @@ const upcastCtx = {} as EventUpcastCtx;
7
8
 
8
9
  function compile(spec: DeclarativeEventMigration) {
9
10
  const feature = defineFeature("billing", (r) => {
10
- r.eventMigration("invoicePaid", 1, 2, spec);
11
+ r.defineEvent("invoicePaid", z.unknown(), {
12
+ version: 2,
13
+ migrations: [{ fromVersion: 1, toVersion: 2, transform: spec }],
14
+ });
11
15
  });
12
16
  const def = feature.eventMigrations["invoicePaid"]?.[0];
13
17
  if (!def) throw new Error("migration not registered");
@@ -51,7 +55,10 @@ describe("declarative eventMigration", () => {
51
55
  test("imperative function variant is stored untouched", () => {
52
56
  const fn = (payload: unknown) => payload;
53
57
  const feature = defineFeature("billing", (r) => {
54
- r.eventMigration("invoicePaid", 1, 2, fn);
58
+ r.defineEvent("invoicePaid", z.unknown(), {
59
+ version: 2,
60
+ migrations: [{ fromVersion: 1, toVersion: 2, transform: fn }],
61
+ });
55
62
  });
56
63
  expect(feature.eventMigrations["invoicePaid"]?.[0]?.transform).toBe(fn);
57
64
  });
@@ -56,7 +56,10 @@ defineFeature("todoList", (r) => {
56
56
  r.defineEvent({
57
57
  name: "taskCompleted",
58
58
  schema: z.object({ id: z.string() }),
59
- version: 1,
59
+ version: 2,
60
+ migrations: {
61
+ "1": (old) => ({ ...old, done: true }),
62
+ },
60
63
  });
61
64
 
62
65
  r.writeHandler({
@@ -127,13 +130,6 @@ defineFeature("todoList", (r) => {
127
130
 
128
131
  r.useExtension({ name: "auditLog", entity: "task" });
129
132
 
130
- r.eventMigration({
131
- event: "taskCompleted",
132
- fromVersion: 1,
133
- toVersion: 2,
134
- transform: (old) => ({ ...old, done: true }),
135
- });
136
-
137
133
  r.job({
138
134
  name: "cleanupExpired",
139
135
  schedule: { cron: "0 3 * * *" },
@@ -232,7 +228,6 @@ describe("Canonical Object-Form — parser akzeptiert + extrahiert", () => {
232
228
  "projection",
233
229
  "multiStreamProjection",
234
230
  "defineEvent",
235
- "eventMigration",
236
231
  ];
237
232
  for (const e of expected) {
238
233
  expect(kinds.has(e), `expected kind "${e}"`).toBe(true);
@@ -283,12 +278,13 @@ describe("Canonical Object-Form — parser akzeptiert + extrahiert", () => {
283
278
  expect(claim).toMatchObject({ kind: "claimKey", shortName: "teamId" });
284
279
  });
285
280
 
286
- test("defineEvent: name + version aus Object-Form", () => {
281
+ test("defineEvent: name + version + migrations aus Object-Form", () => {
287
282
  const ev = result.patterns.find((p) => p.kind === "defineEvent");
288
283
  expect(ev).toMatchObject({
289
284
  kind: "defineEvent",
290
285
  eventName: "taskCompleted",
291
- version: 1,
286
+ version: 2,
287
+ migrations: { "1": expect.anything() },
292
288
  });
293
289
  });
294
290
 
@@ -334,16 +330,6 @@ describe("Canonical Object-Form — parser akzeptiert + extrahiert", () => {
334
330
  });
335
331
  });
336
332
 
337
- test("eventMigration: event + fromVersion + toVersion aus Object-Form", () => {
338
- const em = result.patterns.find((p) => p.kind === "eventMigration");
339
- expect(em).toMatchObject({
340
- kind: "eventMigration",
341
- eventName: "taskCompleted",
342
- fromVersion: 1,
343
- toVersion: 2,
344
- });
345
- });
346
-
347
333
  test("job: name + handlerBody.raw enthält den Closure-Body", () => {
348
334
  const job = result.patterns.find((p) => p.kind === "job");
349
335
  expect(job).toMatchObject({ kind: "job", jobName: "cleanupExpired" });
@@ -961,19 +961,46 @@ defineFeature("f", (r) => {
961
961
  });
962
962
  });
963
963
 
964
- describe("extractEventMigration", () => {
965
- test("captures fromVersion / toVersion / transform body", () => {
964
+ describe("extractDefineEvent — migrations (formerly extractEventMigration)", () => {
965
+ test("positional-form migrations array captures the fromVersion→transform step", () => {
966
966
  const result = parseInline(`
967
967
  defineFeature("f", (r) => {
968
- r.eventMigration("incidentOpened", 1, 2, (payload) => ({ ...payload, severity: "low" }));
968
+ r.defineEvent("incidentOpened", z.object({ id: z.string() }), {
969
+ version: 2,
970
+ migrations: [
971
+ { fromVersion: 1, toVersion: 2, transform: (payload) => ({ ...payload, severity: "low" }) },
972
+ ],
973
+ });
969
974
  });
970
975
  `);
971
976
 
972
977
  expect(result.patterns[0]).toMatchObject({
973
- kind: "eventMigration",
978
+ kind: "defineEvent",
974
979
  eventName: "incidentOpened",
975
- fromVersion: 1,
976
- toVersion: 2,
980
+ version: 2,
981
+ migrations: { "1": expect.anything() },
982
+ });
983
+ });
984
+
985
+ test("object-form migrations map captures the fromVersion→transform step", () => {
986
+ const result = parseInline(`
987
+ defineFeature("f", (r) => {
988
+ r.defineEvent({
989
+ name: "incidentOpened",
990
+ schema: z.object({ id: z.string() }),
991
+ version: 2,
992
+ migrations: {
993
+ "1": (payload) => ({ ...payload, severity: "low" }),
994
+ },
995
+ });
996
+ });
997
+ `);
998
+
999
+ expect(result.patterns[0]).toMatchObject({
1000
+ kind: "defineEvent",
1001
+ eventName: "incidentOpened",
1002
+ version: 2,
1003
+ migrations: { "1": expect.anything() },
977
1004
  });
978
1005
  });
979
1006
  });
@@ -301,21 +301,16 @@ describe("patch coverage for the remaining pattern-kinds", () => {
301
301
  expect(parseSourceFile(sf).patterns.find((p) => p.kind === "authClaims")).toBeUndefined();
302
302
  });
303
303
 
304
- test("add + remove via PatternId for eventMigration (event+versions key)", () => {
304
+ test("add + remove via PatternId for defineEvent with migrations", () => {
305
305
  const sf = makeSourceFile(STARTER);
306
- createFeaturePatcher(sf).addEventMigration({
307
- event: "itemCreated",
308
- fromVersion: 1,
309
- toVersion: 2,
310
- transformSource: "(old) => old",
311
- });
312
- removePattern(sf, {
313
- kind: "eventMigration",
314
- eventName: "itemCreated",
315
- fromVersion: 1,
316
- toVersion: 2,
306
+ createFeaturePatcher(sf).addDefineEvent({
307
+ name: "itemCreated",
308
+ schemaSource: "z.object({ id: z.string() })",
309
+ version: 2,
310
+ migrations: { "1": "(old) => old" },
317
311
  });
318
- expect(parseSourceFile(sf).patterns.find((p) => p.kind === "eventMigration")).toBeUndefined();
312
+ removePattern(sf, { kind: "defineEvent", eventName: "itemCreated" });
313
+ expect(parseSourceFile(sf).patterns.find((p) => p.kind === "defineEvent")).toBeUndefined();
319
314
  });
320
315
  });
321
316
 
@@ -172,33 +172,23 @@ describe("FeaturePatcher — typed add helpers for mixed (closure-bearing) patte
172
172
  });
173
173
  });
174
174
 
175
- test("addDefineEvent + addEventMigration form a complete event-versioning chain", () => {
175
+ test("addDefineEvent with migrations forms a complete event-versioning chain", () => {
176
176
  const sf = makeSourceFile(STARTER);
177
177
  const p = createFeaturePatcher(sf);
178
178
  p.addDefineEvent({
179
179
  name: "stepCompleted",
180
180
  schemaSource: "z.object({ id: z.string() })",
181
181
  version: 2,
182
- });
183
- p.addEventMigration({
184
- event: "stepCompleted",
185
- fromVersion: 1,
186
- toVersion: 2,
187
- transformSource: '(old) => ({ id: old.id ?? "" })',
182
+ migrations: { "1": '(old) => ({ id: old.id ?? "" })' },
188
183
  });
189
184
  const result = parseSourceFile(sf);
190
185
  expect(result.errors).toEqual([]);
191
- expect(result.patterns).toHaveLength(2);
186
+ expect(result.patterns).toHaveLength(1);
192
187
  expect(result.patterns[0]).toMatchObject({
193
188
  kind: "defineEvent",
194
189
  eventName: "stepCompleted",
195
190
  version: 2,
196
- });
197
- expect(result.patterns[1]).toMatchObject({
198
- kind: "eventMigration",
199
- eventName: "stepCompleted",
200
- fromVersion: 1,
201
- toVersion: 2,
191
+ migrations: { "1": expect.anything() },
202
192
  });
203
193
  });
204
194
  });
@@ -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.)