@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
@@ -3,9 +3,14 @@ import type { Model } from './index';
3
3
  import schema from './samples/seasonal-assistant.schema.json';
4
4
  import { modelToNarrative } from './transformers/model-to-narrative';
5
5
 
6
+ function getCode(result: Awaited<ReturnType<typeof modelToNarrative>>): string {
7
+ return result.files.map((f) => f.code).join('\n');
8
+ }
9
+
6
10
  describe('modelToNarrative', () => {
7
11
  it('should create a full flow DSL from a model', async () => {
8
- const code = await modelToNarrative(schema as Model);
12
+ const result = await modelToNarrative({ ...schema, modules: [] } as Model);
13
+ const code = getCode(result);
9
14
 
10
15
  expect(code).toEqual(`import {
11
16
  command,
@@ -384,9 +389,10 @@ narrative('Seasonal Assistant', () => {
384
389
  ],
385
390
  messages: [],
386
391
  integrations: [],
392
+ modules: [],
387
393
  };
388
394
 
389
- const code = await modelToNarrative(experienceModel);
395
+ const code = getCode(await modelToNarrative(experienceModel));
390
396
 
391
397
  expect(code).toEqual(`import { experience, it, narrative } from '@auto-engineer/narrative';
392
398
  narrative('Test Experience Flow', 'TEST-001', () => {
@@ -428,9 +434,10 @@ narrative('Test Experience Flow', 'TEST-001', () => {
428
434
  ],
429
435
  messages: [],
430
436
  integrations: [],
437
+ modules: [],
431
438
  };
432
439
 
433
- const code = await modelToNarrative(modelWithoutIds);
440
+ const code = getCode(await modelToNarrative(modelWithoutIds));
434
441
 
435
442
  expect(code).toEqual(`import { describe, experience, it, narrative } from '@auto-engineer/narrative';
436
443
  narrative('Test Flow without IDs', () => {
@@ -501,9 +508,10 @@ narrative('Test Flow without IDs', () => {
501
508
  ],
502
509
  messages: [],
503
510
  integrations: [],
511
+ modules: [],
504
512
  };
505
513
 
506
- const code = await modelToNarrative(modelWithIds);
514
+ const code = getCode(await modelToNarrative(modelWithIds));
507
515
 
508
516
  expect(code).toEqual(`import { describe, experience, it, narrative, query, specs } from '@auto-engineer/narrative';
509
517
  narrative('Test Flow with IDs', 'FLOW-123', () => {
@@ -601,9 +609,10 @@ narrative('Test Flow with IDs', 'FLOW-123', () => {
601
609
  },
602
610
  ],
603
611
  integrations: [],
612
+ modules: [],
604
613
  };
605
614
 
606
- const code = await modelToNarrative(modelWithRuleIds);
615
+ const code = getCode(await modelToNarrative(modelWithRuleIds));
607
616
 
608
617
  expect(code).toEqual(`import { command, example, narrative, rule, specs } from '@auto-engineer/narrative';
609
618
  import type { Command, Event } from '@auto-engineer/narrative';
@@ -673,9 +682,10 @@ narrative('Test Flow with Rule IDs', 'FLOW-456', () => {
673
682
  },
674
683
  ],
675
684
  integrations: [],
685
+ modules: [],
676
686
  };
677
687
 
678
- const code = await modelToNarrative(modelWithDateTypes);
688
+ const code = getCode(await modelToNarrative(modelWithDateTypes));
679
689
 
680
690
  expect(code).toEqual(`import { narrative } from '@auto-engineer/narrative';
681
691
  import type { Event } from '@auto-engineer/narrative';
@@ -926,9 +936,10 @@ narrative('Questionnaire Flow', 'QUEST-001', () => {});
926
936
  },
927
937
  ],
928
938
  integrations: [],
939
+ modules: [],
929
940
  };
930
941
 
931
- const code = await modelToNarrative(questionnairesModel);
942
+ const code = getCode(await modelToNarrative(questionnairesModel));
932
943
 
933
944
  expect(code).toEqual(`import {
934
945
  data,
@@ -1166,9 +1177,10 @@ narrative('Questionnaires', 'Q9m2Kp4Lx', () => {
1166
1177
  },
1167
1178
  ],
1168
1179
  integrations: [],
1180
+ modules: [],
1169
1181
  };
1170
1182
 
1171
- const code = await modelToNarrative(modelWithDuplicateRules);
1183
+ const code = getCode(await modelToNarrative(modelWithDuplicateRules));
1172
1184
 
1173
1185
  expect(code).toEqual(`import { example, narrative, query, rule, specs } from '@auto-engineer/narrative';
1174
1186
  import type { Event, State } from '@auto-engineer/narrative';
@@ -1370,9 +1382,10 @@ narrative('Test Flow', 'TEST-FLOW', () => {
1370
1382
  },
1371
1383
  ],
1372
1384
  integrations: [],
1385
+ modules: [],
1373
1386
  };
1374
1387
 
1375
- const code = await modelToNarrative(modelWithMultiGiven);
1388
+ const code = getCode(await modelToNarrative(modelWithMultiGiven));
1376
1389
 
1377
1390
  expect(code).toEqual(`import { example, narrative, query, rule, specs } from '@auto-engineer/narrative';
1378
1391
  import type { Event, State } from '@auto-engineer/narrative';
@@ -1581,9 +1594,10 @@ narrative('Multi Given Flow', 'MULTI-GIVEN', () => {
1581
1594
  },
1582
1595
  ],
1583
1596
  integrations: [],
1597
+ modules: [],
1584
1598
  };
1585
1599
 
1586
- const code = await modelToNarrative(modelWithReferencedStates);
1600
+ const code = getCode(await modelToNarrative(modelWithReferencedStates));
1587
1601
 
1588
1602
  expect(
1589
1603
  code,
@@ -1735,9 +1749,10 @@ narrative('Referenced States Flow', 'REF-STATES', () => {
1735
1749
  },
1736
1750
  ],
1737
1751
  integrations: [],
1752
+ modules: [],
1738
1753
  };
1739
1754
 
1740
- const code = await modelToNarrative(modelWithDateFields);
1755
+ const code = getCode(await modelToNarrative(modelWithDateFields));
1741
1756
 
1742
1757
  expect(code).toEqual(`import { example, narrative, query, rule, specs } from '@auto-engineer/narrative';
1743
1758
  import type { Event, State } from '@auto-engineer/narrative';
@@ -1839,9 +1854,10 @@ narrative('Date Handling Flow', 'DATE-FLOW', () => {
1839
1854
  ],
1840
1855
  messages: [],
1841
1856
  integrations: [],
1857
+ modules: [],
1842
1858
  };
1843
1859
 
1844
- const code = await modelToNarrative(modelWithMultipleFlowsSameSource);
1860
+ const code = getCode(await modelToNarrative(modelWithMultipleFlowsSameSource));
1845
1861
 
1846
1862
  expect(code).toEqual(`import { experience, it, narrative } from '@auto-engineer/narrative';
1847
1863
  narrative('Home Screen', () => {
@@ -2000,9 +2016,10 @@ narrative('Response Analytics', () => {
2000
2016
  },
2001
2017
  ],
2002
2018
  integrations: [],
2019
+ modules: [],
2003
2020
  };
2004
2021
 
2005
- const code = await modelToNarrative(modelWithEmptyWhen);
2022
+ const code = getCode(await modelToNarrative(modelWithEmptyWhen));
2006
2023
 
2007
2024
  expect(code).toEqual(`import { example, narrative, query, rule, specs } from '@auto-engineer/narrative';
2008
2025
  import type { Event, State } from '@auto-engineer/narrative';
@@ -2132,9 +2149,10 @@ narrative('Todo List Summary', 'TODO-001', () => {
2132
2149
  },
2133
2150
  ],
2134
2151
  integrations: [],
2152
+ modules: [],
2135
2153
  };
2136
2154
 
2137
- const code = await modelToNarrative(modelWithSingletonProjection);
2155
+ const code = getCode(await modelToNarrative(modelWithSingletonProjection));
2138
2156
 
2139
2157
  expect(code).toEqual(`import { data, narrative, query, source, specs } from '@auto-engineer/narrative';
2140
2158
  import type { State } from '@auto-engineer/narrative';
@@ -2208,9 +2226,10 @@ narrative('Todo Summary Flow', 'TODO-SUMMARY', () => {
2208
2226
  },
2209
2227
  ],
2210
2228
  integrations: [],
2229
+ modules: [],
2211
2230
  };
2212
2231
 
2213
- const code = await modelToNarrative(modelWithRegularProjection);
2232
+ const code = getCode(await modelToNarrative(modelWithRegularProjection));
2214
2233
 
2215
2234
  expect(code).toEqual(`import { data, narrative, query, source, specs } from '@auto-engineer/narrative';
2216
2235
  import type { State } from '@auto-engineer/narrative';
@@ -2285,9 +2304,10 @@ narrative('Todo Flow', 'TODO-FLOW', () => {
2285
2304
  },
2286
2305
  ],
2287
2306
  integrations: [],
2307
+ modules: [],
2288
2308
  };
2289
2309
 
2290
- const code = await modelToNarrative(modelWithCompositeProjection);
2310
+ const code = getCode(await modelToNarrative(modelWithCompositeProjection));
2291
2311
 
2292
2312
  expect(code).toEqual(`import { data, narrative, query, source, specs } from '@auto-engineer/narrative';
2293
2313
  import type { State } from '@auto-engineer/narrative';
@@ -2443,9 +2463,10 @@ narrative('User Project Flow', 'USER-PROJECT-FLOW', () => {
2443
2463
  },
2444
2464
  ],
2445
2465
  integrations: [],
2466
+ modules: [],
2446
2467
  };
2447
2468
 
2448
- const code = await modelToNarrative(modelWithAllProjectionTypes);
2469
+ const code = getCode(await modelToNarrative(modelWithAllProjectionTypes));
2449
2470
 
2450
2471
  expect(code).toEqual(`import { data, narrative, query, source, specs } from '@auto-engineer/narrative';
2451
2472
  import type { State } from '@auto-engineer/narrative';
@@ -2488,4 +2509,433 @@ narrative('All Projection Types', 'ALL-PROJ', () => {
2488
2509
  `);
2489
2510
  });
2490
2511
  });
2512
+
2513
+ describe('modules', () => {
2514
+ it('generates multiple files for derived modules with different sourceFiles', async () => {
2515
+ const model: Model = {
2516
+ variant: 'specs',
2517
+ narratives: [
2518
+ { name: 'Orders', id: 'orders-flow', sourceFile: 'orders.narrative.ts', slices: [] },
2519
+ { name: 'Users', id: 'users-flow', sourceFile: 'users.narrative.ts', slices: [] },
2520
+ ],
2521
+ messages: [],
2522
+ integrations: [],
2523
+ modules: [
2524
+ {
2525
+ id: 'orders.narrative.ts',
2526
+ sourceFile: 'orders.narrative.ts',
2527
+ isDerived: true,
2528
+ contains: { narrativeIds: ['orders-flow'] },
2529
+ declares: { messages: [] },
2530
+ },
2531
+ {
2532
+ id: 'users.narrative.ts',
2533
+ sourceFile: 'users.narrative.ts',
2534
+ isDerived: true,
2535
+ contains: { narrativeIds: ['users-flow'] },
2536
+ declares: { messages: [] },
2537
+ },
2538
+ ],
2539
+ };
2540
+
2541
+ const result = await modelToNarrative(model);
2542
+
2543
+ expect(result.files).toHaveLength(2);
2544
+ expect(result.files.map((f) => f.path).sort()).toEqual(['orders.narrative.ts', 'users.narrative.ts']);
2545
+
2546
+ const ordersFile = result.files.find((f) => f.path === 'orders.narrative.ts');
2547
+ const usersFile = result.files.find((f) => f.path === 'users.narrative.ts');
2548
+
2549
+ expect(ordersFile?.code).toContain("narrative('Orders', 'orders-flow'");
2550
+ expect(usersFile?.code).toContain("narrative('Users', 'users-flow'");
2551
+ });
2552
+
2553
+ it('duplicates types in each derived module file (no cross-module imports)', async () => {
2554
+ const model: Model = {
2555
+ variant: 'specs',
2556
+ narratives: [
2557
+ { name: 'Flow A', id: 'flow-a', sourceFile: 'a.narrative.ts', slices: [] },
2558
+ { name: 'Flow B', id: 'flow-b', sourceFile: 'b.narrative.ts', slices: [] },
2559
+ ],
2560
+ messages: [
2561
+ {
2562
+ type: 'event',
2563
+ source: 'internal',
2564
+ name: 'SharedEvent',
2565
+ fields: [{ name: 'id', type: 'string', required: true }],
2566
+ },
2567
+ ],
2568
+ integrations: [],
2569
+ modules: [
2570
+ {
2571
+ id: 'a.narrative.ts',
2572
+ sourceFile: 'a.narrative.ts',
2573
+ isDerived: true,
2574
+ contains: { narrativeIds: ['flow-a'] },
2575
+ declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
2576
+ },
2577
+ {
2578
+ id: 'b.narrative.ts',
2579
+ sourceFile: 'b.narrative.ts',
2580
+ isDerived: true,
2581
+ contains: { narrativeIds: ['flow-b'] },
2582
+ declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
2583
+ },
2584
+ ],
2585
+ };
2586
+
2587
+ const result = await modelToNarrative(model);
2588
+
2589
+ expect(result.files).toHaveLength(2);
2590
+
2591
+ for (const file of result.files) {
2592
+ expect(file.code).toContain('type SharedEvent = Event<');
2593
+ expect(file.code).not.toContain('import type { SharedEvent }');
2594
+ }
2595
+ });
2596
+
2597
+ it('generates cross-module type imports for authored modules', async () => {
2598
+ const model: Model = {
2599
+ variant: 'specs',
2600
+ narratives: [
2601
+ { name: 'Shared Types', id: 'shared-types', slices: [] },
2602
+ {
2603
+ name: 'Orders',
2604
+ id: 'orders-flow',
2605
+ slices: [
2606
+ {
2607
+ name: 'create order',
2608
+ type: 'command',
2609
+ client: { specs: [] },
2610
+ server: {
2611
+ description: 'Creates an order',
2612
+ specs: [
2613
+ {
2614
+ type: 'gherkin',
2615
+ feature: 'Order Creation',
2616
+ rules: [
2617
+ {
2618
+ name: 'Valid order',
2619
+ examples: [
2620
+ {
2621
+ name: 'Creates order',
2622
+ steps: [
2623
+ { keyword: 'When', text: 'CreateOrder' },
2624
+ { keyword: 'Then', text: 'OrderCreated' },
2625
+ ],
2626
+ },
2627
+ ],
2628
+ },
2629
+ ],
2630
+ },
2631
+ ],
2632
+ },
2633
+ },
2634
+ ],
2635
+ },
2636
+ ],
2637
+ messages: [
2638
+ {
2639
+ type: 'command',
2640
+ name: 'CreateOrder',
2641
+ fields: [{ name: 'orderId', type: 'string', required: true }],
2642
+ },
2643
+ {
2644
+ type: 'event',
2645
+ source: 'internal',
2646
+ name: 'OrderCreated',
2647
+ fields: [{ name: 'orderId', type: 'string', required: true }],
2648
+ },
2649
+ ],
2650
+ integrations: [],
2651
+ modules: [
2652
+ {
2653
+ id: 'shared',
2654
+ sourceFile: 'shared/types.narrative.ts',
2655
+ isDerived: false,
2656
+ contains: { narrativeIds: ['shared-types'] },
2657
+ declares: { messages: [{ kind: 'event', name: 'OrderCreated' }] },
2658
+ },
2659
+ {
2660
+ id: 'orders',
2661
+ sourceFile: 'features/orders.narrative.ts',
2662
+ isDerived: false,
2663
+ contains: { narrativeIds: ['orders-flow'] },
2664
+ declares: { messages: [{ kind: 'command', name: 'CreateOrder' }] },
2665
+ },
2666
+ ],
2667
+ };
2668
+
2669
+ const result = await modelToNarrative(model);
2670
+
2671
+ expect(result.files).toHaveLength(2);
2672
+
2673
+ const ordersFile = result.files.find((f) => f.path.includes('orders'));
2674
+ expect(ordersFile).toBeDefined();
2675
+
2676
+ expect(ordersFile!.code).toContain("import type { OrderCreated } from '../shared/types.narrative';");
2677
+ expect(ordersFile!.code).toContain('type CreateOrder = Command<');
2678
+ expect(ordersFile!.code).not.toContain('type OrderCreated = Event<');
2679
+ });
2680
+
2681
+ it('generates correct relative import paths for nested directories', async () => {
2682
+ const model: Model = {
2683
+ variant: 'specs',
2684
+ narratives: [
2685
+ { name: 'Core Types', id: 'core', slices: [] },
2686
+ {
2687
+ name: 'Feature',
2688
+ id: 'feature',
2689
+ slices: [
2690
+ {
2691
+ name: 'do something',
2692
+ type: 'command',
2693
+ client: { specs: [] },
2694
+ server: {
2695
+ description: 'Does something',
2696
+ specs: [
2697
+ {
2698
+ type: 'gherkin',
2699
+ feature: 'Feature',
2700
+ rules: [
2701
+ {
2702
+ name: 'Rule',
2703
+ examples: [
2704
+ {
2705
+ name: 'Example',
2706
+ steps: [{ keyword: 'Then', text: 'CoreEvent' }],
2707
+ },
2708
+ ],
2709
+ },
2710
+ ],
2711
+ },
2712
+ ],
2713
+ },
2714
+ },
2715
+ ],
2716
+ },
2717
+ ],
2718
+ messages: [
2719
+ {
2720
+ type: 'event',
2721
+ source: 'internal',
2722
+ name: 'CoreEvent',
2723
+ fields: [{ name: 'id', type: 'string', required: true }],
2724
+ },
2725
+ ],
2726
+ integrations: [],
2727
+ modules: [
2728
+ {
2729
+ id: 'core',
2730
+ sourceFile: 'src/core/types.narrative.ts',
2731
+ isDerived: false,
2732
+ contains: { narrativeIds: ['core'] },
2733
+ declares: { messages: [{ kind: 'event', name: 'CoreEvent' }] },
2734
+ },
2735
+ {
2736
+ id: 'feature',
2737
+ sourceFile: 'src/features/sub/feature.narrative.ts',
2738
+ isDerived: false,
2739
+ contains: { narrativeIds: ['feature'] },
2740
+ declares: { messages: [] },
2741
+ },
2742
+ ],
2743
+ };
2744
+
2745
+ const result = await modelToNarrative(model);
2746
+
2747
+ const featureFile = result.files.find((f) => f.path.includes('feature'));
2748
+ expect(featureFile).toBeDefined();
2749
+ expect(featureFile!.code).toContain("import type { CoreEvent } from '../../core/types.narrative';");
2750
+ });
2751
+
2752
+ it('groups multiple imported types from same module into single import', async () => {
2753
+ const model: Model = {
2754
+ variant: 'specs',
2755
+ narratives: [
2756
+ { name: 'Shared', id: 'shared', slices: [] },
2757
+ {
2758
+ name: 'Consumer',
2759
+ id: 'consumer',
2760
+ slices: [
2761
+ {
2762
+ name: 'process',
2763
+ type: 'command',
2764
+ client: { specs: [] },
2765
+ server: {
2766
+ description: 'Processes',
2767
+ specs: [
2768
+ {
2769
+ type: 'gherkin',
2770
+ feature: 'Processing',
2771
+ rules: [
2772
+ {
2773
+ name: 'Rule',
2774
+ examples: [
2775
+ {
2776
+ name: 'Example',
2777
+ steps: [
2778
+ { keyword: 'Given', text: 'EventA' },
2779
+ { keyword: 'Then', text: 'EventB' },
2780
+ ],
2781
+ },
2782
+ ],
2783
+ },
2784
+ ],
2785
+ },
2786
+ ],
2787
+ },
2788
+ },
2789
+ ],
2790
+ },
2791
+ ],
2792
+ messages: [
2793
+ { type: 'event', source: 'internal', name: 'EventA', fields: [] },
2794
+ { type: 'event', source: 'internal', name: 'EventB', fields: [] },
2795
+ ],
2796
+ integrations: [],
2797
+ modules: [
2798
+ {
2799
+ id: 'shared',
2800
+ sourceFile: 'shared.narrative.ts',
2801
+ isDerived: false,
2802
+ contains: { narrativeIds: ['shared'] },
2803
+ declares: {
2804
+ messages: [
2805
+ { kind: 'event', name: 'EventA' },
2806
+ { kind: 'event', name: 'EventB' },
2807
+ ],
2808
+ },
2809
+ },
2810
+ {
2811
+ id: 'consumer',
2812
+ sourceFile: 'consumer.narrative.ts',
2813
+ isDerived: false,
2814
+ contains: { narrativeIds: ['consumer'] },
2815
+ declares: { messages: [] },
2816
+ },
2817
+ ],
2818
+ };
2819
+
2820
+ const result = await modelToNarrative(model);
2821
+
2822
+ const consumerFile = result.files.find((f) => f.path.includes('consumer'));
2823
+ expect(consumerFile).toBeDefined();
2824
+
2825
+ expect(consumerFile!.code).toMatch(/import type \{ EventA, EventB \} from/);
2826
+ expect(consumerFile!.code.match(/import type \{/g)?.length).toBe(1);
2827
+ });
2828
+
2829
+ it('sorts cross-module imports alphabetically by path', async () => {
2830
+ const model: Model = {
2831
+ variant: 'specs',
2832
+ narratives: [
2833
+ { name: 'Types Z', id: 'z-types', slices: [] },
2834
+ { name: 'Types A', id: 'a-types', slices: [] },
2835
+ {
2836
+ name: 'Consumer',
2837
+ id: 'consumer',
2838
+ slices: [
2839
+ {
2840
+ name: 'process',
2841
+ type: 'command',
2842
+ client: { specs: [] },
2843
+ server: {
2844
+ description: 'Processes',
2845
+ specs: [
2846
+ {
2847
+ type: 'gherkin',
2848
+ feature: 'Processing',
2849
+ rules: [
2850
+ {
2851
+ name: 'Rule',
2852
+ examples: [
2853
+ {
2854
+ name: 'Example',
2855
+ steps: [
2856
+ { keyword: 'Given', text: 'ZEvent' },
2857
+ { keyword: 'Then', text: 'AEvent' },
2858
+ ],
2859
+ },
2860
+ ],
2861
+ },
2862
+ ],
2863
+ },
2864
+ ],
2865
+ },
2866
+ },
2867
+ ],
2868
+ },
2869
+ ],
2870
+ messages: [
2871
+ { type: 'event', source: 'internal', name: 'ZEvent', fields: [] },
2872
+ { type: 'event', source: 'internal', name: 'AEvent', fields: [] },
2873
+ ],
2874
+ integrations: [],
2875
+ modules: [
2876
+ {
2877
+ id: 'z-types',
2878
+ sourceFile: 'z-types.narrative.ts',
2879
+ isDerived: false,
2880
+ contains: { narrativeIds: ['z-types'] },
2881
+ declares: { messages: [{ kind: 'event', name: 'ZEvent' }] },
2882
+ },
2883
+ {
2884
+ id: 'a-types',
2885
+ sourceFile: 'a-types.narrative.ts',
2886
+ isDerived: false,
2887
+ contains: { narrativeIds: ['a-types'] },
2888
+ declares: { messages: [{ kind: 'event', name: 'AEvent' }] },
2889
+ },
2890
+ {
2891
+ id: 'consumer',
2892
+ sourceFile: 'consumer.narrative.ts',
2893
+ isDerived: false,
2894
+ contains: { narrativeIds: ['consumer'] },
2895
+ declares: { messages: [] },
2896
+ },
2897
+ ],
2898
+ };
2899
+
2900
+ const result = await modelToNarrative(model);
2901
+
2902
+ const consumerFile = result.files.find((f) => f.path.includes('consumer'));
2903
+ expect(consumerFile).toBeDefined();
2904
+
2905
+ const importLines = consumerFile!.code.split('\n').filter((line) => line.startsWith('import type {'));
2906
+
2907
+ expect(importLines).toHaveLength(2);
2908
+ expect(importLines[0]).toContain('a-types');
2909
+ expect(importLines[1]).toContain('z-types');
2910
+ });
2911
+
2912
+ it('derives modules when model has empty modules array', async () => {
2913
+ const model: Model = {
2914
+ variant: 'specs',
2915
+ narratives: [
2916
+ { name: 'Flow A', id: 'flow-a', sourceFile: 'a.narrative.ts', slices: [] },
2917
+ { name: 'Flow B', id: 'flow-b', sourceFile: 'b.narrative.ts', slices: [] },
2918
+ ],
2919
+ messages: [
2920
+ {
2921
+ type: 'event',
2922
+ source: 'internal',
2923
+ name: 'TestEvent',
2924
+ fields: [{ name: 'id', type: 'string', required: true }],
2925
+ },
2926
+ ],
2927
+ integrations: [],
2928
+ modules: [],
2929
+ };
2930
+
2931
+ const result = await modelToNarrative(model);
2932
+
2933
+ expect(result.files).toHaveLength(2);
2934
+ expect(result.files.map((f) => f.path).sort()).toEqual(['a.narrative.ts', 'b.narrative.ts']);
2935
+
2936
+ for (const file of result.files) {
2937
+ expect(file.code).toContain('type TestEvent = Event<');
2938
+ }
2939
+ });
2940
+ });
2491
2941
  });
package/src/schema.ts CHANGED
@@ -1,5 +1,32 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ // Message reference for module type ownership
4
+ export const MessageRefSchema = z
5
+ .object({
6
+ kind: z.enum(['command', 'event', 'state']).describe('Message kind'),
7
+ name: z.string().describe('Message name'),
8
+ })
9
+ .describe('Reference to a message type');
10
+
11
+ // Module schema for type ownership and file grouping
12
+ export const ModuleSchema = z
13
+ .object({
14
+ id: z.string().describe('Unique module identifier. For derived modules, equals sourceFile'),
15
+ sourceFile: z.string().describe('Output file path for this module'),
16
+ isDerived: z.boolean().describe('True if auto-derived from sourceFile grouping, false if user-authored'),
17
+ contains: z
18
+ .object({
19
+ narrativeIds: z.array(z.string()).describe('IDs of narratives in this module'),
20
+ })
21
+ .describe('Narratives contained in this module'),
22
+ declares: z
23
+ .object({
24
+ messages: z.array(MessageRefSchema).describe('Message types owned by this module'),
25
+ })
26
+ .describe('Types declared/owned by this module'),
27
+ })
28
+ .describe('Module for grouping narratives and owning types');
29
+
3
30
  const IntegrationSchema = z
4
31
  .object({
5
32
  name: z.string().describe('Integration name (e.g., MailChimp, Twilio)'),
@@ -373,6 +400,7 @@ export const modelSchema = z
373
400
  narratives: z.array(NarrativeSchema),
374
401
  messages: z.array(MessageSchema),
375
402
  integrations: z.array(IntegrationSchema).optional(),
403
+ modules: z.array(ModuleSchema).describe('Modules for type ownership and file grouping'),
376
404
  })
377
405
  .describe('Complete system specification with all implementation details');
378
406