@auto-engineer/narrative 0.16.0 → 0.17.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 (87) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +26 -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 +16 -3
  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 +4 -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 +17 -3
  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.test.json +2 -1
@@ -0,0 +1,625 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Model } from '../../index';
3
+ import { modelToNarrative } from './index';
4
+ import { throwOnValidationErrors, validateModules } from './validate-modules';
5
+
6
+ describe('module functionality', () => {
7
+ describe('derived modules (from toModel)', () => {
8
+ it('produces self-contained files with duplicated types for each sourceFile', async () => {
9
+ const model: Model = {
10
+ variant: 'specs',
11
+ narratives: [
12
+ {
13
+ name: 'Orders',
14
+ id: 'orders-flow',
15
+ sourceFile: 'orders.narrative.ts',
16
+ slices: [],
17
+ },
18
+ {
19
+ name: 'Users',
20
+ id: 'users-flow',
21
+ sourceFile: 'users.narrative.ts',
22
+ slices: [],
23
+ },
24
+ ],
25
+ messages: [
26
+ {
27
+ type: 'event',
28
+ source: 'internal',
29
+ name: 'SharedEvent',
30
+ fields: [{ name: 'id', type: 'string', required: true }],
31
+ },
32
+ ],
33
+ integrations: [],
34
+ modules: [
35
+ {
36
+ id: 'orders.narrative.ts',
37
+ sourceFile: 'orders.narrative.ts',
38
+ isDerived: true,
39
+ contains: { narrativeIds: ['orders-flow'] },
40
+ declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
41
+ },
42
+ {
43
+ id: 'users.narrative.ts',
44
+ sourceFile: 'users.narrative.ts',
45
+ isDerived: true,
46
+ contains: { narrativeIds: ['users-flow'] },
47
+ declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
48
+ },
49
+ ],
50
+ };
51
+
52
+ const result = await modelToNarrative(model);
53
+
54
+ expect(result.files).toHaveLength(2);
55
+
56
+ // Both files should have paths matching sourceFile
57
+ const paths = result.files.map((f) => f.path);
58
+ expect(paths).toContain('orders.narrative.ts');
59
+ expect(paths).toContain('users.narrative.ts');
60
+
61
+ // Both files should declare SharedEvent (type duplication for derived modules)
62
+ for (const file of result.files) {
63
+ expect(file.code).toContain('type SharedEvent = Event<');
64
+ expect(file.code).toContain("'SharedEvent'");
65
+ }
66
+
67
+ // No cross-file imports for derived modules
68
+ for (const file of result.files) {
69
+ expect(file.code).not.toContain('import type { SharedEvent }');
70
+ }
71
+ });
72
+ });
73
+
74
+ describe('authored modules (hand-crafted)', () => {
75
+ it('generates cross-module imports for types declared by other modules', async () => {
76
+ const model: Model = {
77
+ variant: 'specs',
78
+ narratives: [
79
+ {
80
+ name: 'Shared Types',
81
+ id: 'shared-types',
82
+ slices: [],
83
+ },
84
+ {
85
+ name: 'Orders',
86
+ id: 'orders-flow',
87
+ slices: [
88
+ {
89
+ name: 'create order',
90
+ type: 'command',
91
+ client: { specs: [] },
92
+ server: {
93
+ description: 'Creates an order',
94
+ specs: [
95
+ {
96
+ type: 'gherkin',
97
+ feature: 'Order Creation',
98
+ rules: [
99
+ {
100
+ name: 'Valid order',
101
+ examples: [
102
+ {
103
+ name: 'Creates order',
104
+ steps: [
105
+ { keyword: 'When', text: 'CreateOrder' },
106
+ { keyword: 'Then', text: 'OrderCreated' },
107
+ ],
108
+ },
109
+ ],
110
+ },
111
+ ],
112
+ },
113
+ ],
114
+ },
115
+ },
116
+ ],
117
+ },
118
+ ],
119
+ messages: [
120
+ {
121
+ type: 'command',
122
+ name: 'CreateOrder',
123
+ fields: [{ name: 'id', type: 'string', required: true }],
124
+ },
125
+ {
126
+ type: 'event',
127
+ source: 'internal',
128
+ name: 'OrderCreated',
129
+ fields: [{ name: 'orderId', type: 'string', required: true }],
130
+ },
131
+ ],
132
+ integrations: [],
133
+ modules: [
134
+ {
135
+ id: 'shared',
136
+ sourceFile: 'shared/types.narrative.ts',
137
+ isDerived: false,
138
+ contains: { narrativeIds: ['shared-types'] },
139
+ declares: {
140
+ messages: [{ kind: 'event', name: 'OrderCreated' }],
141
+ },
142
+ },
143
+ {
144
+ id: 'orders',
145
+ sourceFile: 'features/orders.narrative.ts',
146
+ isDerived: false,
147
+ contains: { narrativeIds: ['orders-flow'] },
148
+ declares: {
149
+ messages: [{ kind: 'command', name: 'CreateOrder' }],
150
+ },
151
+ },
152
+ ],
153
+ };
154
+
155
+ const result = await modelToNarrative(model);
156
+
157
+ expect(result.files).toHaveLength(2);
158
+
159
+ // Find the orders file
160
+ const ordersFile = result.files.find((f) => f.path.includes('orders'));
161
+ expect(ordersFile).toBeDefined();
162
+
163
+ // Orders file should import OrderCreated from shared
164
+ expect(ordersFile!.code).toContain("import type { OrderCreated } from '../shared/types.narrative';");
165
+ });
166
+ });
167
+
168
+ describe('validation', () => {
169
+ it('detects duplicate module IDs', () => {
170
+ const model: Model = {
171
+ variant: 'specs',
172
+ narratives: [],
173
+ messages: [],
174
+ modules: [
175
+ {
176
+ id: 'same-id',
177
+ sourceFile: 'file1.ts',
178
+ isDerived: false,
179
+ contains: { narrativeIds: [] },
180
+ declares: { messages: [] },
181
+ },
182
+ {
183
+ id: 'same-id',
184
+ sourceFile: 'file2.ts',
185
+ isDerived: false,
186
+ contains: { narrativeIds: [] },
187
+ declares: { messages: [] },
188
+ },
189
+ ],
190
+ };
191
+
192
+ const errors = validateModules(model);
193
+
194
+ expect(errors).toHaveLength(1);
195
+ expect(errors[0].type).toBe('duplicate_id');
196
+ expect(errors[0].message).toContain('same-id');
197
+ });
198
+
199
+ it('detects derived module ID not matching sourceFile', () => {
200
+ const model: Model = {
201
+ variant: 'specs',
202
+ narratives: [],
203
+ messages: [],
204
+ modules: [
205
+ {
206
+ id: 'wrong-id',
207
+ sourceFile: 'correct-path.ts',
208
+ isDerived: true,
209
+ contains: { narrativeIds: [] },
210
+ declares: { messages: [] },
211
+ },
212
+ ],
213
+ };
214
+
215
+ const errors = validateModules(model);
216
+
217
+ expect(errors).toHaveLength(1);
218
+ expect(errors[0].type).toBe('derived_id_mismatch');
219
+ });
220
+
221
+ it('detects narrative assigned to multiple authored modules', () => {
222
+ const model: Model = {
223
+ variant: 'specs',
224
+ narratives: [{ name: 'Test', id: 'test-narrative', slices: [] }],
225
+ messages: [],
226
+ modules: [
227
+ {
228
+ id: 'module-a',
229
+ sourceFile: 'a.ts',
230
+ isDerived: false,
231
+ contains: { narrativeIds: ['test-narrative'] },
232
+ declares: { messages: [] },
233
+ },
234
+ {
235
+ id: 'module-b',
236
+ sourceFile: 'b.ts',
237
+ isDerived: false,
238
+ contains: { narrativeIds: ['test-narrative'] },
239
+ declares: { messages: [] },
240
+ },
241
+ ],
242
+ };
243
+
244
+ const errors = validateModules(model);
245
+
246
+ expect(errors.some((e) => e.type === 'narrative_multi_assigned')).toBe(true);
247
+ });
248
+
249
+ it('detects message declared by multiple authored modules', () => {
250
+ const model: Model = {
251
+ variant: 'specs',
252
+ narratives: [],
253
+ messages: [{ type: 'event', source: 'internal', name: 'SharedEvent', fields: [] }],
254
+ modules: [
255
+ {
256
+ id: 'module-a',
257
+ sourceFile: 'a.ts',
258
+ isDerived: false,
259
+ contains: { narrativeIds: [] },
260
+ declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
261
+ },
262
+ {
263
+ id: 'module-b',
264
+ sourceFile: 'b.ts',
265
+ isDerived: false,
266
+ contains: { narrativeIds: [] },
267
+ declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
268
+ },
269
+ ],
270
+ };
271
+
272
+ const errors = validateModules(model);
273
+
274
+ expect(errors.some((e) => e.type === 'message_multi_declared')).toBe(true);
275
+ });
276
+
277
+ it('allows type duplication in derived modules', () => {
278
+ const model: Model = {
279
+ variant: 'specs',
280
+ narratives: [],
281
+ messages: [{ type: 'event', source: 'internal', name: 'SharedEvent', fields: [] }],
282
+ modules: [
283
+ {
284
+ id: 'file1.ts',
285
+ sourceFile: 'file1.ts',
286
+ isDerived: true,
287
+ contains: { narrativeIds: [] },
288
+ declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
289
+ },
290
+ {
291
+ id: 'file2.ts',
292
+ sourceFile: 'file2.ts',
293
+ isDerived: true,
294
+ contains: { narrativeIds: [] },
295
+ declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
296
+ },
297
+ ],
298
+ };
299
+
300
+ const errors = validateModules(model);
301
+
302
+ expect(errors).toHaveLength(0);
303
+ });
304
+
305
+ it('returns no errors for empty modules array', () => {
306
+ const model: Model = {
307
+ variant: 'specs',
308
+ narratives: [{ name: 'Test', id: 'test-1', slices: [] }],
309
+ messages: [{ type: 'event', source: 'internal', name: 'TestEvent', fields: [] }],
310
+ modules: [],
311
+ };
312
+
313
+ const errors = validateModules(model);
314
+
315
+ expect(errors).toHaveLength(0);
316
+ });
317
+
318
+ it('detects narrative not found in model', () => {
319
+ const model: Model = {
320
+ variant: 'specs',
321
+ narratives: [],
322
+ messages: [],
323
+ modules: [
324
+ {
325
+ id: 'module-a',
326
+ sourceFile: 'a.ts',
327
+ isDerived: false,
328
+ contains: { narrativeIds: ['nonexistent-narrative'] },
329
+ declares: { messages: [] },
330
+ },
331
+ ],
332
+ };
333
+
334
+ const errors = validateModules(model);
335
+
336
+ expect(errors.some((e) => e.type === 'narrative_not_found')).toBe(true);
337
+ expect(errors[0].message).toContain('nonexistent-narrative');
338
+ });
339
+
340
+ it('detects unassigned narratives', () => {
341
+ const model: Model = {
342
+ variant: 'specs',
343
+ narratives: [{ name: 'Orphan', id: 'orphan-narrative', slices: [] }],
344
+ messages: [],
345
+ modules: [
346
+ {
347
+ id: 'module-a',
348
+ sourceFile: 'a.ts',
349
+ isDerived: false,
350
+ contains: { narrativeIds: [] },
351
+ declares: { messages: [] },
352
+ },
353
+ ],
354
+ };
355
+
356
+ const errors = validateModules(model);
357
+
358
+ expect(errors.some((e) => e.type === 'narrative_unassigned')).toBe(true);
359
+ expect(errors.find((e) => e.type === 'narrative_unassigned')!.message).toContain('orphan-narrative');
360
+ });
361
+
362
+ it('detects undeclared messages', () => {
363
+ const model: Model = {
364
+ variant: 'specs',
365
+ narratives: [],
366
+ messages: [{ type: 'event', source: 'internal', name: 'UndeclaredEvent', fields: [] }],
367
+ modules: [
368
+ {
369
+ id: 'module-a',
370
+ sourceFile: 'a.ts',
371
+ isDerived: false,
372
+ contains: { narrativeIds: [] },
373
+ declares: { messages: [] },
374
+ },
375
+ ],
376
+ };
377
+
378
+ const errors = validateModules(model);
379
+
380
+ expect(errors.some((e) => e.type === 'message_undeclared')).toBe(true);
381
+ expect(errors.find((e) => e.type === 'message_undeclared')!.message).toContain('event:UndeclaredEvent');
382
+ });
383
+
384
+ it('throwOnValidationErrors throws when errors exist', () => {
385
+ const errors = [{ type: 'duplicate_id' as const, message: 'Test error' }];
386
+
387
+ expect(() => throwOnValidationErrors(errors)).toThrow('Module validation failed');
388
+ expect(() => throwOnValidationErrors(errors)).toThrow('Test error');
389
+ });
390
+
391
+ it('throwOnValidationErrors does not throw for empty errors', () => {
392
+ expect(() => throwOnValidationErrors([])).not.toThrow();
393
+ });
394
+ });
395
+
396
+ describe('round-trip (model → narrative → model)', () => {
397
+ it('generates separate files for each sourceFile in derived modules', async () => {
398
+ const model: Model = {
399
+ variant: 'specs',
400
+ narratives: [
401
+ {
402
+ name: 'Orders',
403
+ id: 'orders-flow',
404
+ sourceFile: 'orders.narrative.ts',
405
+ slices: [],
406
+ },
407
+ {
408
+ name: 'Users',
409
+ id: 'users-flow',
410
+ sourceFile: 'users.narrative.ts',
411
+ slices: [],
412
+ },
413
+ ],
414
+ messages: [
415
+ { type: 'command', name: 'CreateOrder', fields: [{ name: 'orderId', type: 'string', required: true }] },
416
+ {
417
+ type: 'event',
418
+ source: 'internal',
419
+ name: 'OrderCreated',
420
+ fields: [{ name: 'orderId', type: 'string', required: true }],
421
+ },
422
+ ],
423
+ integrations: [],
424
+ modules: [
425
+ {
426
+ id: 'orders.narrative.ts',
427
+ sourceFile: 'orders.narrative.ts',
428
+ isDerived: true,
429
+ contains: { narrativeIds: ['orders-flow'] },
430
+ declares: {
431
+ messages: [
432
+ { kind: 'command', name: 'CreateOrder' },
433
+ { kind: 'event', name: 'OrderCreated' },
434
+ ],
435
+ },
436
+ },
437
+ {
438
+ id: 'users.narrative.ts',
439
+ sourceFile: 'users.narrative.ts',
440
+ isDerived: true,
441
+ contains: { narrativeIds: ['users-flow'] },
442
+ declares: {
443
+ messages: [
444
+ { kind: 'command', name: 'CreateOrder' },
445
+ { kind: 'event', name: 'OrderCreated' },
446
+ ],
447
+ },
448
+ },
449
+ ],
450
+ };
451
+
452
+ const result = await modelToNarrative(model);
453
+
454
+ expect(result.files).toHaveLength(2);
455
+ expect(result.files.map((f) => f.path).sort()).toEqual(['orders.narrative.ts', 'users.narrative.ts']);
456
+
457
+ const ordersFile = result.files.find((f) => f.path === 'orders.narrative.ts');
458
+ const usersFile = result.files.find((f) => f.path === 'users.narrative.ts');
459
+
460
+ expect(ordersFile?.code).toContain("narrative('Orders'");
461
+ expect(usersFile?.code).toContain("narrative('Users'");
462
+ });
463
+
464
+ it('duplicates types in each derived module file', async () => {
465
+ const model: Model = {
466
+ variant: 'specs',
467
+ narratives: [
468
+ { name: 'Flow A', id: 'flow-a', sourceFile: 'a.narrative.ts', slices: [] },
469
+ { name: 'Flow B', id: 'flow-b', sourceFile: 'b.narrative.ts', slices: [] },
470
+ ],
471
+ messages: [
472
+ {
473
+ type: 'event',
474
+ source: 'internal',
475
+ name: 'SharedEvent',
476
+ fields: [{ name: 'id', type: 'string', required: true }],
477
+ },
478
+ ],
479
+ integrations: [],
480
+ modules: [
481
+ {
482
+ id: 'a.narrative.ts',
483
+ sourceFile: 'a.narrative.ts',
484
+ isDerived: true,
485
+ contains: { narrativeIds: ['flow-a'] },
486
+ declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
487
+ },
488
+ {
489
+ id: 'b.narrative.ts',
490
+ sourceFile: 'b.narrative.ts',
491
+ isDerived: true,
492
+ contains: { narrativeIds: ['flow-b'] },
493
+ declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
494
+ },
495
+ ],
496
+ };
497
+
498
+ const result = await modelToNarrative(model);
499
+
500
+ for (const file of result.files) {
501
+ expect(file.code).toContain('type SharedEvent = Event<');
502
+ }
503
+ });
504
+
505
+ it('generates cross-module imports for authored modules', async () => {
506
+ const model: Model = {
507
+ variant: 'specs',
508
+ narratives: [
509
+ { name: 'Shared Types', id: 'shared-types', slices: [] },
510
+ {
511
+ name: 'Orders',
512
+ id: 'orders-flow',
513
+ slices: [
514
+ {
515
+ name: 'create order',
516
+ type: 'command',
517
+ client: { specs: [] },
518
+ server: {
519
+ description: 'Creates an order',
520
+ specs: [
521
+ {
522
+ type: 'gherkin',
523
+ feature: 'Order Creation',
524
+ rules: [
525
+ {
526
+ name: 'Valid order',
527
+ examples: [
528
+ {
529
+ name: 'Creates order',
530
+ steps: [
531
+ { keyword: 'When', text: 'CreateOrder' },
532
+ { keyword: 'Then', text: 'OrderCreated' },
533
+ ],
534
+ },
535
+ ],
536
+ },
537
+ ],
538
+ },
539
+ ],
540
+ },
541
+ },
542
+ ],
543
+ },
544
+ ],
545
+ messages: [
546
+ { type: 'command', name: 'CreateOrder', fields: [{ name: 'id', type: 'string', required: true }] },
547
+ {
548
+ type: 'event',
549
+ source: 'internal',
550
+ name: 'OrderCreated',
551
+ fields: [{ name: 'orderId', type: 'string', required: true }],
552
+ },
553
+ ],
554
+ integrations: [],
555
+ modules: [
556
+ {
557
+ id: 'shared',
558
+ sourceFile: 'shared/types.narrative.ts',
559
+ isDerived: false,
560
+ contains: { narrativeIds: ['shared-types'] },
561
+ declares: { messages: [{ kind: 'event', name: 'OrderCreated' }] },
562
+ },
563
+ {
564
+ id: 'orders',
565
+ sourceFile: 'features/orders.narrative.ts',
566
+ isDerived: false,
567
+ contains: { narrativeIds: ['orders-flow'] },
568
+ declares: { messages: [{ kind: 'command', name: 'CreateOrder' }] },
569
+ },
570
+ ],
571
+ };
572
+
573
+ const result = await modelToNarrative(model);
574
+
575
+ expect(result.files).toHaveLength(2);
576
+
577
+ const ordersFile = result.files.find((f) => f.path.includes('orders'));
578
+ expect(ordersFile).toBeDefined();
579
+ expect(ordersFile!.code).toContain("import type { OrderCreated } from '../shared/types.narrative';");
580
+ expect(ordersFile!.code).not.toContain('type OrderCreated = Event<');
581
+ });
582
+ });
583
+
584
+ describe('file ordering', () => {
585
+ it('sorts output files alphabetically by path', async () => {
586
+ const model: Model = {
587
+ variant: 'specs',
588
+ narratives: [
589
+ { name: 'Z', id: 'z', sourceFile: 'z.narrative.ts', slices: [] },
590
+ { name: 'A', id: 'a', sourceFile: 'a.narrative.ts', slices: [] },
591
+ { name: 'M', id: 'm', sourceFile: 'm.narrative.ts', slices: [] },
592
+ ],
593
+ messages: [],
594
+ integrations: [],
595
+ modules: [
596
+ {
597
+ id: 'z.narrative.ts',
598
+ sourceFile: 'z.narrative.ts',
599
+ isDerived: true,
600
+ contains: { narrativeIds: ['z'] },
601
+ declares: { messages: [] },
602
+ },
603
+ {
604
+ id: 'a.narrative.ts',
605
+ sourceFile: 'a.narrative.ts',
606
+ isDerived: true,
607
+ contains: { narrativeIds: ['a'] },
608
+ declares: { messages: [] },
609
+ },
610
+ {
611
+ id: 'm.narrative.ts',
612
+ sourceFile: 'm.narrative.ts',
613
+ isDerived: true,
614
+ contains: { narrativeIds: ['m'] },
615
+ declares: { messages: [] },
616
+ },
617
+ ],
618
+ };
619
+
620
+ const result = await modelToNarrative(model);
621
+
622
+ expect(result.files.map((f) => f.path)).toEqual(['a.narrative.ts', 'm.narrative.ts', 'z.narrative.ts']);
623
+ });
624
+ });
625
+ });