@auto-engineer/narrative 0.15.0 → 0.17.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 (124) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +28 -0
  3. package/dist/src/getNarratives.js +1 -1
  4. package/dist/src/getNarratives.js.map +1 -1
  5. package/dist/src/id/addAutoIds.d.ts.map +1 -1
  6. package/dist/src/id/addAutoIds.js +15 -0
  7. package/dist/src/id/addAutoIds.js.map +1 -1
  8. package/dist/src/id/hasAllIds.d.ts.map +1 -1
  9. package/dist/src/id/hasAllIds.js +6 -1
  10. package/dist/src/id/hasAllIds.js.map +1 -1
  11. package/dist/src/index.d.ts +5 -2
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/index.js +1 -1
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/loader/runtime-cjs.d.ts.map +1 -1
  16. package/dist/src/loader/runtime-cjs.js +3 -2
  17. package/dist/src/loader/runtime-cjs.js.map +1 -1
  18. package/dist/src/schema.d.ts +301 -143
  19. package/dist/src/schema.d.ts.map +1 -1
  20. package/dist/src/schema.js +26 -0
  21. package/dist/src/schema.js.map +1 -1
  22. package/dist/src/transformers/model-to-narrative/cross-module-imports.d.ts +6 -0
  23. package/dist/src/transformers/model-to-narrative/cross-module-imports.d.ts.map +1 -0
  24. package/dist/src/transformers/model-to-narrative/cross-module-imports.js +63 -0
  25. package/dist/src/transformers/model-to-narrative/cross-module-imports.js.map +1 -0
  26. package/dist/src/transformers/model-to-narrative/generators/flow.d.ts +1 -4
  27. package/dist/src/transformers/model-to-narrative/generators/flow.d.ts.map +1 -1
  28. package/dist/src/transformers/model-to-narrative/generators/flow.js.map +1 -1
  29. package/dist/src/transformers/model-to-narrative/generators/module-code.d.ts +9 -0
  30. package/dist/src/transformers/model-to-narrative/generators/module-code.d.ts.map +1 -0
  31. package/dist/src/transformers/model-to-narrative/generators/module-code.js +102 -0
  32. package/dist/src/transformers/model-to-narrative/generators/module-code.js.map +1 -0
  33. package/dist/src/transformers/model-to-narrative/index.d.ts +6 -4
  34. package/dist/src/transformers/model-to-narrative/index.d.ts.map +1 -1
  35. package/dist/src/transformers/model-to-narrative/index.js +10 -6
  36. package/dist/src/transformers/model-to-narrative/index.js.map +1 -1
  37. package/dist/src/transformers/model-to-narrative/ordering.d.ts +10 -0
  38. package/dist/src/transformers/model-to-narrative/ordering.d.ts.map +1 -0
  39. package/dist/src/transformers/model-to-narrative/ordering.js +37 -0
  40. package/dist/src/transformers/model-to-narrative/ordering.js.map +1 -0
  41. package/dist/src/transformers/model-to-narrative/spec-traversal.d.ts +3 -0
  42. package/dist/src/transformers/model-to-narrative/spec-traversal.d.ts.map +1 -0
  43. package/dist/src/transformers/model-to-narrative/spec-traversal.js +54 -0
  44. package/dist/src/transformers/model-to-narrative/spec-traversal.js.map +1 -0
  45. package/dist/src/transformers/model-to-narrative/types.d.ts +12 -0
  46. package/dist/src/transformers/model-to-narrative/types.d.ts.map +1 -0
  47. package/dist/src/transformers/model-to-narrative/types.js +2 -0
  48. package/dist/src/transformers/model-to-narrative/types.js.map +1 -0
  49. package/dist/src/transformers/model-to-narrative/validate-modules.d.ts +8 -0
  50. package/dist/src/transformers/model-to-narrative/validate-modules.d.ts.map +1 -0
  51. package/dist/src/transformers/model-to-narrative/validate-modules.js +121 -0
  52. package/dist/src/transformers/model-to-narrative/validate-modules.js.map +1 -0
  53. package/dist/src/transformers/narrative-to-model/assemble.d.ts.map +1 -1
  54. package/dist/src/transformers/narrative-to-model/assemble.js +5 -1
  55. package/dist/src/transformers/narrative-to-model/assemble.js.map +1 -1
  56. package/dist/src/transformers/narrative-to-model/derive-modules.d.ts +3 -0
  57. package/dist/src/transformers/narrative-to-model/derive-modules.d.ts.map +1 -0
  58. package/dist/src/transformers/narrative-to-model/derive-modules.js +29 -0
  59. package/dist/src/transformers/narrative-to-model/derive-modules.js.map +1 -0
  60. package/dist/tsconfig.tsbuildinfo +1 -1
  61. package/package.json +5 -4
  62. package/src/getNarratives.specs.ts +214 -1
  63. package/src/getNarratives.ts +1 -1
  64. package/src/id/addAutoIds.specs.ts +180 -0
  65. package/src/id/addAutoIds.ts +16 -1
  66. package/src/id/hasAllIds.specs.ts +87 -0
  67. package/src/id/hasAllIds.ts +10 -2
  68. package/src/index.ts +7 -0
  69. package/src/loader/runtime-cjs.ts +3 -2
  70. package/src/model-to-narrative.specs.ts +467 -17
  71. package/src/schema.ts +28 -0
  72. package/src/transformers/model-to-narrative/cross-module-imports.specs.ts +450 -0
  73. package/src/transformers/model-to-narrative/cross-module-imports.ts +83 -0
  74. package/src/transformers/model-to-narrative/generators/flow.ts +11 -10
  75. package/src/transformers/model-to-narrative/generators/module-code.ts +186 -0
  76. package/src/transformers/model-to-narrative/index.ts +19 -7
  77. package/src/transformers/model-to-narrative/modules.specs.ts +625 -0
  78. package/src/transformers/model-to-narrative/ordering.specs.ts +104 -0
  79. package/src/transformers/model-to-narrative/ordering.ts +46 -0
  80. package/src/transformers/model-to-narrative/spec-traversal.specs.ts +418 -0
  81. package/src/transformers/model-to-narrative/spec-traversal.ts +63 -0
  82. package/src/transformers/model-to-narrative/types.ts +13 -0
  83. package/src/transformers/model-to-narrative/validate-modules.ts +159 -0
  84. package/src/transformers/narrative-to-model/assemble.ts +7 -2
  85. package/src/transformers/narrative-to-model/derive-modules.specs.ts +121 -0
  86. package/src/transformers/narrative-to-model/derive-modules.ts +36 -0
  87. package/tsconfig.json +1 -1
  88. package/tsconfig.test.json +2 -1
  89. package/dist/src/fluent-builder.specs.d.ts +0 -2
  90. package/dist/src/fluent-builder.specs.d.ts.map +0 -1
  91. package/dist/src/fluent-builder.specs.js +0 -28
  92. package/dist/src/fluent-builder.specs.js.map +0 -1
  93. package/dist/src/getNarratives.cache.specs.d.ts +0 -2
  94. package/dist/src/getNarratives.cache.specs.d.ts.map +0 -1
  95. package/dist/src/getNarratives.cache.specs.js +0 -234
  96. package/dist/src/getNarratives.cache.specs.js.map +0 -1
  97. package/dist/src/getNarratives.specs.d.ts +0 -2
  98. package/dist/src/getNarratives.specs.d.ts.map +0 -1
  99. package/dist/src/getNarratives.specs.js +0 -1307
  100. package/dist/src/getNarratives.specs.js.map +0 -1
  101. package/dist/src/id/addAutoIds.specs.d.ts +0 -2
  102. package/dist/src/id/addAutoIds.specs.d.ts.map +0 -1
  103. package/dist/src/id/addAutoIds.specs.js +0 -602
  104. package/dist/src/id/addAutoIds.specs.js.map +0 -1
  105. package/dist/src/id/hasAllIds.specs.d.ts +0 -2
  106. package/dist/src/id/hasAllIds.specs.d.ts.map +0 -1
  107. package/dist/src/id/hasAllIds.specs.js +0 -424
  108. package/dist/src/id/hasAllIds.specs.js.map +0 -1
  109. package/dist/src/model-to-narrative.specs.d.ts +0 -2
  110. package/dist/src/model-to-narrative.specs.d.ts.map +0 -1
  111. package/dist/src/model-to-narrative.specs.js +0 -2437
  112. package/dist/src/model-to-narrative.specs.js.map +0 -1
  113. package/dist/src/narrative-context.specs.d.ts +0 -2
  114. package/dist/src/narrative-context.specs.d.ts.map +0 -1
  115. package/dist/src/narrative-context.specs.js +0 -260
  116. package/dist/src/narrative-context.specs.js.map +0 -1
  117. package/dist/src/transformers/model-to-narrative/generators/gwt.specs.d.ts +0 -2
  118. package/dist/src/transformers/model-to-narrative/generators/gwt.specs.d.ts.map +0 -1
  119. package/dist/src/transformers/model-to-narrative/generators/gwt.specs.js +0 -142
  120. package/dist/src/transformers/model-to-narrative/generators/gwt.specs.js.map +0 -1
  121. package/dist/src/transformers/narrative-to-model/type-inference.specs.d.ts +0 -2
  122. package/dist/src/transformers/narrative-to-model/type-inference.specs.d.ts.map +0 -1
  123. package/dist/src/transformers/narrative-to-model/type-inference.specs.js +0 -177
  124. package/dist/src/transformers/narrative-to-model/type-inference.specs.js.map +0 -1
package/package.json CHANGED
@@ -23,9 +23,9 @@
23
23
  "typescript": "^5.9.2",
24
24
  "zod": "^3.22.4",
25
25
  "zod-to-json-schema": "^3.22.3",
26
- "@auto-engineer/file-store": "0.15.0",
27
- "@auto-engineer/message-bus": "0.15.0",
28
- "@auto-engineer/id": "0.15.0"
26
+ "@auto-engineer/id": "0.17.0",
27
+ "@auto-engineer/file-store": "0.17.0",
28
+ "@auto-engineer/message-bus": "0.17.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.0.0",
@@ -35,10 +35,11 @@
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  },
38
- "version": "0.15.0",
38
+ "version": "0.17.0",
39
39
  "scripts": {
40
40
  "build": "tsx scripts/build.ts",
41
41
  "test": "vitest run --reporter=dot",
42
+ "test:coverage": "vitest run --reporter=dot --coverage --coverage.reporter=text",
42
43
  "type-check": "tsc --noEmit",
43
44
  "convert-narrative": "tsx scripts/convert-narrative.ts",
44
45
  "link:dev": "pnpm build && pnpm link --global",
@@ -660,7 +660,8 @@ flow('questionnaires-test', () => {
660
660
  });
661
661
 
662
662
  const model = flows.toModel();
663
- const code = await modelToNarrative(model);
663
+ const result = await modelToNarrative(model);
664
+ const code = result.files.map((f) => f.code).join('\n');
664
665
 
665
666
  expect(code).not.toMatch(/\.when<>\(\{\}\)/);
666
667
  expect(code).not.toMatch(/\.when<\s*\{\s*}\s*>\(\{}\)/);
@@ -1241,6 +1242,218 @@ function validateThenEvents(example: unknown): void {
1241
1242
  }
1242
1243
  }
1243
1244
 
1245
+ describe('modules in toModel()', () => {
1246
+ it('should derive modules from narratives with different sourceFiles', async () => {
1247
+ const memoryVfs = new InMemoryFileStore();
1248
+
1249
+ const ordersContent = `
1250
+ import { flow, command, specs, rule, example, type Command, type Event } from '@auto-engineer/narrative';
1251
+
1252
+ type CreateOrder = Command<'CreateOrder', { orderId: string }>;
1253
+ type OrderCreated = Event<'OrderCreated', { orderId: string; createdAt: Date }>;
1254
+
1255
+ flow('Orders', () => {
1256
+ command('create order')
1257
+ .server(() => {
1258
+ specs(() => {
1259
+ rule('creates an order', () => {
1260
+ example('order created')
1261
+ .when<CreateOrder>({ orderId: 'order-001' })
1262
+ .then<OrderCreated>({ orderId: 'order-001', createdAt: new Date('2030-01-01T09:00:00Z') });
1263
+ });
1264
+ });
1265
+ });
1266
+ });
1267
+ `;
1268
+
1269
+ const usersContent = `
1270
+ import { flow, command, specs, rule, example, type Command, type Event } from '@auto-engineer/narrative';
1271
+
1272
+ type CreateUser = Command<'CreateUser', { userId: string; name: string }>;
1273
+ type UserCreated = Event<'UserCreated', { userId: string; name: string; createdAt: Date }>;
1274
+
1275
+ flow('Users', () => {
1276
+ command('create user')
1277
+ .server(() => {
1278
+ specs(() => {
1279
+ rule('creates a user', () => {
1280
+ example('user created')
1281
+ .when<CreateUser>({ userId: 'user-001', name: 'Alice' })
1282
+ .then<UserCreated>({ userId: 'user-001', name: 'Alice', createdAt: new Date('2030-01-01T09:00:00Z') });
1283
+ });
1284
+ });
1285
+ });
1286
+ });
1287
+ `;
1288
+
1289
+ await memoryVfs.write('/test/orders.narrative.ts', new TextEncoder().encode(ordersContent));
1290
+ await memoryVfs.write('/test/users.narrative.ts', new TextEncoder().encode(usersContent));
1291
+
1292
+ const flows = await getNarratives({
1293
+ vfs: memoryVfs,
1294
+ root: '/test',
1295
+ pattern: /\.narrative\.ts$/,
1296
+ fastFsScan: true,
1297
+ });
1298
+ const model = flows.toModel();
1299
+
1300
+ expect(model.modules).toBeDefined();
1301
+ expect(model.modules.length).toBe(2);
1302
+
1303
+ const sourceFiles = model.modules.map((m) => m.sourceFile);
1304
+ expect(sourceFiles.some((sf) => sf.includes('orders.narrative.ts'))).toBe(true);
1305
+ expect(sourceFiles.some((sf) => sf.includes('users.narrative.ts'))).toBe(true);
1306
+
1307
+ const ordersModule = model.modules.find((m) => m.sourceFile.includes('orders.narrative.ts'));
1308
+ const usersModule = model.modules.find((m) => m.sourceFile.includes('users.narrative.ts'));
1309
+
1310
+ expect(ordersModule).toBeDefined();
1311
+ expect(ordersModule?.isDerived).toBe(true);
1312
+
1313
+ expect(usersModule).toBeDefined();
1314
+ expect(usersModule?.isDerived).toBe(true);
1315
+ });
1316
+
1317
+ it('should include all messages in derived module declarations', async () => {
1318
+ const memoryVfs = new InMemoryFileStore();
1319
+
1320
+ const content = `
1321
+ import { flow, command, specs, rule, example, type Command, type Event } from '@auto-engineer/narrative';
1322
+
1323
+ type CreateOrder = Command<'CreateOrder', { orderId: string }>;
1324
+ type OrderCreated = Event<'OrderCreated', { orderId: string }>;
1325
+
1326
+ flow('Orders', () => {
1327
+ command('create order')
1328
+ .server(() => {
1329
+ specs(() => {
1330
+ rule('creates order', () => {
1331
+ example('order created')
1332
+ .when<CreateOrder>({ orderId: 'order-001' })
1333
+ .then<OrderCreated>({ orderId: 'order-001' });
1334
+ });
1335
+ });
1336
+ });
1337
+ });
1338
+ `;
1339
+
1340
+ await memoryVfs.write('/test/orders.narrative.ts', new TextEncoder().encode(content));
1341
+
1342
+ const flows = await getNarratives({
1343
+ vfs: memoryVfs,
1344
+ root: '/test',
1345
+ pattern: /\.narrative\.ts$/,
1346
+ fastFsScan: true,
1347
+ });
1348
+ const model = flows.toModel();
1349
+
1350
+ expect(model.modules).toHaveLength(1);
1351
+ const mod = model.modules[0];
1352
+
1353
+ const declaredNames = mod.declares.messages.map((m) => m.name);
1354
+ expect(declaredNames).toContain('CreateOrder');
1355
+ expect(declaredNames).toContain('OrderCreated');
1356
+ });
1357
+
1358
+ it('should group narratives from same sourceFile into one module', async () => {
1359
+ const memoryVfs = new InMemoryFileStore();
1360
+
1361
+ const content = `
1362
+ import { flow, command, query, specs, rule, example, type Command, type Event, type State } from '@auto-engineer/narrative';
1363
+
1364
+ type CreateTodo = Command<'CreateTodo', { todoId: string }>;
1365
+ type TodoCreated = Event<'TodoCreated', { todoId: string }>;
1366
+ type TodoList = State<'TodoList', { todos: string[] }>;
1367
+
1368
+ flow('Create Todos', () => {
1369
+ command('add todo')
1370
+ .server(() => {
1371
+ specs(() => {
1372
+ rule('adds todo', () => {
1373
+ example('todo added')
1374
+ .when<CreateTodo>({ todoId: 'todo-001' })
1375
+ .then<TodoCreated>({ todoId: 'todo-001' });
1376
+ });
1377
+ });
1378
+ });
1379
+ });
1380
+
1381
+ flow('View Todos', () => {
1382
+ query('list todos')
1383
+ .server(() => {
1384
+ specs(() => {
1385
+ rule('shows todos', () => {
1386
+ example('todos listed')
1387
+ .given<TodoCreated>({ todoId: 'todo-001' })
1388
+ .when({})
1389
+ .then<TodoList>({ todos: ['todo-001'] });
1390
+ });
1391
+ });
1392
+ });
1393
+ });
1394
+ `;
1395
+
1396
+ await memoryVfs.write('/test/todos.narrative.ts', new TextEncoder().encode(content));
1397
+
1398
+ const flows = await getNarratives({
1399
+ vfs: memoryVfs,
1400
+ root: '/test',
1401
+ pattern: /\.narrative\.ts$/,
1402
+ fastFsScan: true,
1403
+ });
1404
+ const model = flows.toModel();
1405
+
1406
+ expect(model.modules).toHaveLength(1);
1407
+ expect(model.modules[0].contains.narrativeIds).toHaveLength(2);
1408
+
1409
+ const narrativeNames = model.narratives.map((n) => n.name);
1410
+ expect(narrativeNames).toContain('Create Todos');
1411
+ expect(narrativeNames).toContain('View Todos');
1412
+ });
1413
+
1414
+ it('should validate model with modules passes schema', async () => {
1415
+ const memoryVfs = new InMemoryFileStore();
1416
+
1417
+ const content = `
1418
+ import { flow, command, specs, rule, example, type Command, type Event } from '@auto-engineer/narrative';
1419
+
1420
+ type DoSomething = Command<'DoSomething', { id: string }>;
1421
+ type SomethingDone = Event<'SomethingDone', { id: string }>;
1422
+
1423
+ flow('Test', () => {
1424
+ command('do something')
1425
+ .server(() => {
1426
+ specs(() => {
1427
+ rule('does something', () => {
1428
+ example('something done')
1429
+ .when<DoSomething>({ id: '001' })
1430
+ .then<SomethingDone>({ id: '001' });
1431
+ });
1432
+ });
1433
+ });
1434
+ });
1435
+ `;
1436
+
1437
+ await memoryVfs.write('/test/test.narrative.ts', new TextEncoder().encode(content));
1438
+
1439
+ const flows = await getNarratives({
1440
+ vfs: memoryVfs,
1441
+ root: '/test',
1442
+ pattern: /\.narrative\.ts$/,
1443
+ fastFsScan: true,
1444
+ });
1445
+ const model = flows.toModel();
1446
+
1447
+ const parseResult = modelSchema.safeParse(model);
1448
+ if (!parseResult.success) {
1449
+ console.error('Schema validation errors:', parseResult.error.format());
1450
+ }
1451
+ expect(parseResult.success).toBe(true);
1452
+ expect(parseResult.data?.modules).toBeDefined();
1453
+ expect(parseResult.data?.modules.length).toBeGreaterThan(0);
1454
+ });
1455
+ });
1456
+
1244
1457
  describe('projection DSL methods', () => {
1245
1458
  it('should generate correct origin for singleton projection', async () => {
1246
1459
  const memoryVfs = new InMemoryFileStore();
@@ -151,7 +151,7 @@ export const getNarratives = async (
151
151
  typeMap: new Map(),
152
152
  typesByFile: new Map(),
153
153
  givenTypesByFile: new Map(),
154
- toModel: () => ({ variant: 'specs' as const, narratives: [], messages: [] }),
154
+ toModel: () => ({ variant: 'specs' as const, narratives: [], messages: [], modules: [] }),
155
155
  };
156
156
  }
157
157
 
@@ -79,6 +79,7 @@ describe('addAutoIds', () => {
79
79
  ],
80
80
  messages: [],
81
81
  integrations: [],
82
+ modules: [],
82
83
  };
83
84
 
84
85
  const AUTO_ID_REGEX = /^[A-Za-z0-9_]{9}$/;
@@ -161,6 +162,7 @@ describe('addAutoIds', () => {
161
162
  ],
162
163
  messages: [],
163
164
  integrations: [],
165
+ modules: [],
164
166
  };
165
167
 
166
168
  const result = addAutoIds(modelWithoutServer);
@@ -207,6 +209,7 @@ describe('addAutoIds', () => {
207
209
  ],
208
210
  messages: [],
209
211
  integrations: [],
212
+ modules: [],
210
213
  };
211
214
 
212
215
  const result = addAutoIds(modelWithExperienceSlice);
@@ -262,6 +265,7 @@ describe('addAutoIds', () => {
262
265
  ],
263
266
  messages: [],
264
267
  integrations: [],
268
+ modules: [],
265
269
  };
266
270
 
267
271
  const result = addAutoIds(modelWithMultipleFlowsSameSource);
@@ -320,6 +324,7 @@ describe('addAutoIds', () => {
320
324
  ],
321
325
  messages: [],
322
326
  integrations: [],
327
+ modules: [],
323
328
  };
324
329
 
325
330
  const result = addAutoIds(modelWithSpecs);
@@ -372,6 +377,7 @@ describe('addAutoIds', () => {
372
377
  ],
373
378
  messages: [],
374
379
  integrations: [],
380
+ modules: [],
375
381
  };
376
382
 
377
383
  const result = addAutoIds(modelWithSteps);
@@ -427,6 +433,7 @@ describe('addAutoIds', () => {
427
433
  ],
428
434
  messages: [],
429
435
  integrations: [],
436
+ modules: [],
430
437
  };
431
438
 
432
439
  const result = addAutoIds(modelWithExistingExampleId);
@@ -480,6 +487,7 @@ describe('addAutoIds', () => {
480
487
  ],
481
488
  messages: [],
482
489
  integrations: [],
490
+ modules: [],
483
491
  };
484
492
 
485
493
  const result = addAutoIds(modelWithErrorSteps);
@@ -515,6 +523,7 @@ describe('addAutoIds', () => {
515
523
  ],
516
524
  messages: [],
517
525
  integrations: [],
526
+ modules: [],
518
527
  };
519
528
 
520
529
  const result = addAutoIds(modelWithClientSpecs);
@@ -557,6 +566,7 @@ describe('addAutoIds', () => {
557
566
  ],
558
567
  messages: [],
559
568
  integrations: [],
569
+ modules: [],
560
570
  };
561
571
 
562
572
  const result = addAutoIds(modelWithDescribe);
@@ -603,6 +613,7 @@ describe('addAutoIds', () => {
603
613
  ],
604
614
  messages: [],
605
615
  integrations: [],
616
+ modules: [],
606
617
  };
607
618
 
608
619
  const result = addAutoIds(modelWithNestedSpecs);
@@ -647,6 +658,7 @@ describe('addAutoIds', () => {
647
658
  ],
648
659
  messages: [],
649
660
  integrations: [],
661
+ modules: [],
650
662
  };
651
663
 
652
664
  const originalSpec = modelWithClientSpecs.narratives[0].slices[0];
@@ -656,4 +668,172 @@ describe('addAutoIds', () => {
656
668
  expect(originalSpec.client.specs[0].id).toBeUndefined();
657
669
  }
658
670
  });
671
+
672
+ describe('module ID generation', () => {
673
+ const AUTO_ID_REGEX = /^[A-Za-z0-9_]{9}$/;
674
+
675
+ it('should assign ID to derived module equal to sourceFile', () => {
676
+ const model: Model = {
677
+ variant: 'specs',
678
+ narratives: [],
679
+ messages: [],
680
+ integrations: [],
681
+ modules: [
682
+ {
683
+ id: '',
684
+ sourceFile: 'orders.narrative.ts',
685
+ isDerived: true,
686
+ contains: { narrativeIds: [] },
687
+ declares: { messages: [] },
688
+ },
689
+ ],
690
+ };
691
+
692
+ const result = addAutoIds(model);
693
+
694
+ expect(result.modules[0].id).toBe('orders.narrative.ts');
695
+ });
696
+
697
+ it('should generate auto ID for authored module without ID', () => {
698
+ const model: Model = {
699
+ variant: 'specs',
700
+ narratives: [],
701
+ messages: [],
702
+ integrations: [],
703
+ modules: [
704
+ {
705
+ id: '',
706
+ sourceFile: 'features/orders.ts',
707
+ isDerived: false,
708
+ contains: { narrativeIds: [] },
709
+ declares: { messages: [] },
710
+ },
711
+ ],
712
+ };
713
+
714
+ const result = addAutoIds(model);
715
+
716
+ expect(result.modules[0].id).toMatch(AUTO_ID_REGEX);
717
+ });
718
+
719
+ it('should preserve existing ID for authored module', () => {
720
+ const model: Model = {
721
+ variant: 'specs',
722
+ narratives: [],
723
+ messages: [],
724
+ integrations: [],
725
+ modules: [
726
+ {
727
+ id: 'EXISTING-MODULE-001',
728
+ sourceFile: 'features/orders.ts',
729
+ isDerived: false,
730
+ contains: { narrativeIds: [] },
731
+ declares: { messages: [] },
732
+ },
733
+ ],
734
+ };
735
+
736
+ const result = addAutoIds(model);
737
+
738
+ expect(result.modules[0].id).toBe('EXISTING-MODULE-001');
739
+ });
740
+
741
+ it('should not mutate original modules', () => {
742
+ const model: Model = {
743
+ variant: 'specs',
744
+ narratives: [],
745
+ messages: [],
746
+ integrations: [],
747
+ modules: [
748
+ {
749
+ id: '',
750
+ sourceFile: 'test.ts',
751
+ isDerived: false,
752
+ contains: { narrativeIds: [] },
753
+ declares: { messages: [] },
754
+ },
755
+ ],
756
+ };
757
+
758
+ const originalId = model.modules[0].id;
759
+ addAutoIds(model);
760
+
761
+ expect(model.modules[0].id).toBe(originalId);
762
+ });
763
+
764
+ it('should generate unique IDs for multiple authored modules', () => {
765
+ const model: Model = {
766
+ variant: 'specs',
767
+ narratives: [],
768
+ messages: [],
769
+ integrations: [],
770
+ modules: [
771
+ {
772
+ id: '',
773
+ sourceFile: 'orders.ts',
774
+ isDerived: false,
775
+ contains: { narrativeIds: [] },
776
+ declares: { messages: [] },
777
+ },
778
+ {
779
+ id: '',
780
+ sourceFile: 'users.ts',
781
+ isDerived: false,
782
+ contains: { narrativeIds: [] },
783
+ declares: { messages: [] },
784
+ },
785
+ ],
786
+ };
787
+
788
+ const result = addAutoIds(model);
789
+
790
+ expect(result.modules[0].id).toMatch(AUTO_ID_REGEX);
791
+ expect(result.modules[1].id).toMatch(AUTO_ID_REGEX);
792
+ expect(result.modules[0].id).not.toBe(result.modules[1].id);
793
+ });
794
+
795
+ it('should handle mixed derived and authored modules', () => {
796
+ const model: Model = {
797
+ variant: 'specs',
798
+ narratives: [],
799
+ messages: [],
800
+ integrations: [],
801
+ modules: [
802
+ {
803
+ id: '',
804
+ sourceFile: 'derived.narrative.ts',
805
+ isDerived: true,
806
+ contains: { narrativeIds: [] },
807
+ declares: { messages: [] },
808
+ },
809
+ {
810
+ id: '',
811
+ sourceFile: 'authored.ts',
812
+ isDerived: false,
813
+ contains: { narrativeIds: [] },
814
+ declares: { messages: [] },
815
+ },
816
+ ],
817
+ };
818
+
819
+ const result = addAutoIds(model);
820
+
821
+ expect(result.modules[0].id).toBe('derived.narrative.ts');
822
+ expect(result.modules[1].id).toMatch(AUTO_ID_REGEX);
823
+ });
824
+
825
+ it('should handle empty modules array', () => {
826
+ const model: Model = {
827
+ variant: 'specs',
828
+ narratives: [],
829
+ messages: [],
830
+ integrations: [],
831
+ modules: [],
832
+ };
833
+
834
+ const result = addAutoIds(model);
835
+
836
+ expect(result.modules).toEqual([]);
837
+ });
838
+ });
659
839
  });
@@ -1,4 +1,4 @@
1
- import type { ClientSpecNode, Example, Model, Rule, Slice, Spec, Step } from '../index';
1
+ import type { ClientSpecNode, Example, Model, Module, Rule, Slice, Spec, Step } from '../index';
2
2
  import { generateAutoId } from './generators';
3
3
 
4
4
  function ensureId(item: { id?: string }): void {
@@ -85,6 +85,18 @@ function processSlice(slice: Slice): Slice {
85
85
  return sliceCopy;
86
86
  }
87
87
 
88
+ function processModules(modules: Module[]): Module[] {
89
+ return modules.map((module) => {
90
+ const moduleCopy = { ...module };
91
+ if (module.isDerived) {
92
+ moduleCopy.id = module.sourceFile;
93
+ } else {
94
+ ensureId(moduleCopy);
95
+ }
96
+ return moduleCopy;
97
+ });
98
+ }
99
+
88
100
  export function addAutoIds(specs: Model): Model {
89
101
  const result = structuredClone(specs);
90
102
  result.narratives = result.narratives.map((narrative) => {
@@ -93,5 +105,8 @@ export function addAutoIds(specs: Model): Model {
93
105
  narrativeCopy.slices = narrative.slices.map(processSlice);
94
106
  return narrativeCopy;
95
107
  });
108
+ if (result.modules) {
109
+ result.modules = processModules(result.modules);
110
+ }
96
111
  return result;
97
112
  }
@@ -34,6 +34,7 @@ describe('hasAllIds', () => {
34
34
  ],
35
35
  messages: [],
36
36
  integrations: [],
37
+ modules: [],
37
38
  });
38
39
 
39
40
  const createModelWithIds = (): Model => ({
@@ -71,6 +72,7 @@ describe('hasAllIds', () => {
71
72
  ],
72
73
  messages: [],
73
74
  integrations: [],
75
+ modules: [],
74
76
  });
75
77
 
76
78
  const createModelWithFullIds = (): Model => ({
@@ -131,6 +133,7 @@ describe('hasAllIds', () => {
131
133
  ],
132
134
  messages: [],
133
135
  integrations: [],
136
+ modules: [],
134
137
  });
135
138
 
136
139
  const createMultipleFlowsModel = (includeAllIds: boolean, includeAllSliceIds: boolean): Model => ({
@@ -178,6 +181,7 @@ describe('hasAllIds', () => {
178
181
  ],
179
182
  messages: [],
180
183
  integrations: [],
184
+ modules: [],
181
185
  });
182
186
 
183
187
  it('should return false for models without IDs', () => {
@@ -310,6 +314,7 @@ describe('hasAllIds', () => {
310
314
  ],
311
315
  messages: [],
312
316
  integrations: [],
317
+ modules: [],
313
318
  };
314
319
  expect(hasAllIds(model)).toBe(false);
315
320
  });
@@ -335,6 +340,7 @@ describe('hasAllIds', () => {
335
340
  ],
336
341
  messages: [],
337
342
  integrations: [],
343
+ modules: [],
338
344
  };
339
345
  expect(hasAllIds(model)).toBe(false);
340
346
  });
@@ -366,6 +372,7 @@ describe('hasAllIds', () => {
366
372
  ],
367
373
  messages: [],
368
374
  integrations: [],
375
+ modules: [],
369
376
  };
370
377
  expect(hasAllIds(model)).toBe(false);
371
378
  });
@@ -398,6 +405,7 @@ describe('hasAllIds', () => {
398
405
  ],
399
406
  messages: [],
400
407
  integrations: [],
408
+ modules: [],
401
409
  };
402
410
  expect(hasAllIds(model)).toBe(false);
403
411
  });
@@ -438,7 +446,86 @@ describe('hasAllIds', () => {
438
446
  ],
439
447
  messages: [],
440
448
  integrations: [],
449
+ modules: [],
441
450
  };
442
451
  expect(hasAllIds(model)).toBe(true);
443
452
  });
453
+
454
+ describe('module ID validation', () => {
455
+ it('should return true when all modules have IDs', () => {
456
+ const model: Model = {
457
+ variant: 'specs',
458
+ narratives: [],
459
+ messages: [],
460
+ integrations: [],
461
+ modules: [
462
+ {
463
+ id: 'module-1',
464
+ sourceFile: 'test.ts',
465
+ isDerived: false,
466
+ contains: { narrativeIds: [] },
467
+ declares: { messages: [] },
468
+ },
469
+ ],
470
+ };
471
+ expect(hasAllIds(model)).toBe(true);
472
+ });
473
+
474
+ it('should return false when module has empty ID', () => {
475
+ const model: Model = {
476
+ variant: 'specs',
477
+ narratives: [],
478
+ messages: [],
479
+ integrations: [],
480
+ modules: [
481
+ {
482
+ id: '',
483
+ sourceFile: 'test.ts',
484
+ isDerived: false,
485
+ contains: { narrativeIds: [] },
486
+ declares: { messages: [] },
487
+ },
488
+ ],
489
+ };
490
+ expect(hasAllIds(model)).toBe(false);
491
+ });
492
+
493
+ it('should return false when any module is missing ID among valid narratives', () => {
494
+ const model: Model = {
495
+ variant: 'specs',
496
+ narratives: [{ name: 'Test', id: 'FLOW-001', slices: [] }],
497
+ messages: [],
498
+ integrations: [],
499
+ modules: [
500
+ {
501
+ id: '',
502
+ sourceFile: 'test.ts',
503
+ isDerived: true,
504
+ contains: { narrativeIds: ['FLOW-001'] },
505
+ declares: { messages: [] },
506
+ },
507
+ ],
508
+ };
509
+ expect(hasAllIds(model)).toBe(false);
510
+ });
511
+
512
+ it('should return true for derived module with valid ID', () => {
513
+ const model: Model = {
514
+ variant: 'specs',
515
+ narratives: [],
516
+ messages: [],
517
+ integrations: [],
518
+ modules: [
519
+ {
520
+ id: 'derived.ts',
521
+ sourceFile: 'derived.ts',
522
+ isDerived: true,
523
+ contains: { narrativeIds: [] },
524
+ declares: { messages: [] },
525
+ },
526
+ ],
527
+ };
528
+ expect(hasAllIds(model)).toBe(true);
529
+ });
530
+ });
444
531
  });