@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
@@ -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
+ });