@auto-engineer/narrative 1.5.1 → 1.5.2

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 (26) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +5 -5
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +15 -0
  5. package/dist/src/transformers/model-to-narrative/ast/emit-helpers.d.ts.map +1 -1
  6. package/dist/src/transformers/model-to-narrative/ast/emit-helpers.js +1 -8
  7. package/dist/src/transformers/model-to-narrative/ast/emit-helpers.js.map +1 -1
  8. package/dist/src/transformers/model-to-narrative/cross-module-imports.d.ts +12 -0
  9. package/dist/src/transformers/model-to-narrative/cross-module-imports.d.ts.map +1 -1
  10. package/dist/src/transformers/model-to-narrative/cross-module-imports.js +69 -21
  11. package/dist/src/transformers/model-to-narrative/cross-module-imports.js.map +1 -1
  12. package/dist/src/transformers/model-to-narrative/generators/module-code.d.ts.map +1 -1
  13. package/dist/src/transformers/model-to-narrative/generators/module-code.js +10 -7
  14. package/dist/src/transformers/model-to-narrative/generators/module-code.js.map +1 -1
  15. package/dist/src/transformers/model-to-narrative/generators/types.d.ts +1 -1
  16. package/dist/src/transformers/model-to-narrative/generators/types.d.ts.map +1 -1
  17. package/dist/src/transformers/model-to-narrative/generators/types.js +4 -3
  18. package/dist/src/transformers/model-to-narrative/generators/types.js.map +1 -1
  19. package/dist/tsconfig.tsbuildinfo +1 -1
  20. package/package.json +4 -4
  21. package/src/transformers/model-to-narrative/ast/emit-helpers.ts +1 -9
  22. package/src/transformers/model-to-narrative/cross-module-imports.specs.ts +191 -13
  23. package/src/transformers/model-to-narrative/cross-module-imports.ts +87 -24
  24. package/src/transformers/model-to-narrative/generators/module-code.ts +11 -4
  25. package/src/transformers/model-to-narrative/generators/types.ts +5 -7
  26. package/src/transformers/model-to-narrative/modules.specs.ts +3 -2
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": "1.5.1",
27
- "@auto-engineer/id": "1.5.1",
28
- "@auto-engineer/message-bus": "1.5.1"
26
+ "@auto-engineer/file-store": "1.5.2",
27
+ "@auto-engineer/id": "1.5.2",
28
+ "@auto-engineer/message-bus": "1.5.2"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.0.0",
@@ -35,7 +35,7 @@
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  },
38
- "version": "1.5.1",
38
+ "version": "1.5.2",
39
39
  "scripts": {
40
40
  "build": "tsx scripts/build.ts",
41
41
  "test": "vitest run --reporter=dot",
@@ -7,14 +7,6 @@ export interface FieldTypeInfo {
7
7
  [fieldName: string]: string; // field name -> type (e.g., 'Date', 'string', etc.)
8
8
  }
9
9
 
10
- /**
11
- * Check if a string is a valid ISO date string
12
- */
13
- function isISODateString(str: string): boolean {
14
- const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})?$/;
15
- return isoDateRegex.test(str);
16
- }
17
-
18
10
  /**
19
11
  * Emit a TS expression from a plain JSON-like value with optional type information for Date handling.
20
12
  */
@@ -50,7 +42,7 @@ export function jsonToExpr(
50
42
  const fieldType = typeInfo?.[k];
51
43
  let valueExpr: tsNS.Expression;
52
44
 
53
- if (fieldType === 'Date' && typeof x === 'string' && isISODateString(x)) {
45
+ if (fieldType === 'Date' && typeof x === 'string') {
54
46
  // Generate new Date('...') for Date fields
55
47
  valueExpr = f.createNewExpression(f.createIdentifier('Date'), undefined, [f.createStringLiteral(x)]);
56
48
  } else if (fieldType === 'Date' && x instanceof Date) {
@@ -1,6 +1,10 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import type { Model } from '../../index';
3
- import { computeCrossModuleImports, resolveRelativeImport } from './cross-module-imports';
3
+ import {
4
+ computeAllCrossModuleDependencies,
5
+ computeCrossModuleImports,
6
+ resolveRelativeImport,
7
+ } from './cross-module-imports';
4
8
 
5
9
  describe('resolveRelativeImport', () => {
6
10
  it('resolves same directory imports', () => {
@@ -49,7 +53,6 @@ describe('computeCrossModuleImports', () => {
49
53
  integrations: [],
50
54
  modules: [
51
55
  {
52
- id: 'derived.ts',
53
56
  sourceFile: 'derived.ts',
54
57
  isDerived: true,
55
58
  contains: { narrativeIds: [] },
@@ -103,7 +106,6 @@ describe('computeCrossModuleImports', () => {
103
106
  integrations: [],
104
107
  modules: [
105
108
  {
106
- id: 'self-contained',
107
109
  sourceFile: 'self-contained.ts',
108
110
  isDerived: false,
109
111
  contains: { narrativeIds: ['test-1'] },
@@ -157,7 +159,6 @@ describe('computeCrossModuleImports', () => {
157
159
  integrations: [],
158
160
  modules: [
159
161
  {
160
- id: 'consumer',
161
162
  sourceFile: 'consumer.ts',
162
163
  isDerived: false,
163
164
  contains: { narrativeIds: ['test-1'] },
@@ -212,14 +213,12 @@ describe('computeCrossModuleImports', () => {
212
213
  integrations: [],
213
214
  modules: [
214
215
  {
215
- id: 'shared',
216
216
  sourceFile: 'shared/types.ts',
217
217
  isDerived: false,
218
218
  contains: { narrativeIds: ['shared-1'] },
219
219
  declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
220
220
  },
221
221
  {
222
- id: 'consumer',
223
222
  sourceFile: 'features/consumer.ts',
224
223
  isDerived: false,
225
224
  contains: { narrativeIds: ['consumer-1'] },
@@ -281,7 +280,6 @@ describe('computeCrossModuleImports', () => {
281
280
  integrations: [],
282
281
  modules: [
283
282
  {
284
- id: 'shared',
285
283
  sourceFile: 'shared.ts',
286
284
  isDerived: false,
287
285
  contains: { narrativeIds: ['shared-1'] },
@@ -293,7 +291,6 @@ describe('computeCrossModuleImports', () => {
293
291
  },
294
292
  },
295
293
  {
296
- id: 'consumer',
297
294
  sourceFile: 'consumer.ts',
298
295
  isDerived: false,
299
296
  contains: { narrativeIds: ['consumer-1'] },
@@ -349,14 +346,12 @@ describe('computeCrossModuleImports', () => {
349
346
  integrations: [],
350
347
  modules: [
351
348
  {
352
- id: 'derived.ts',
353
349
  sourceFile: 'derived.ts',
354
350
  isDerived: true,
355
351
  contains: { narrativeIds: [] },
356
352
  declares: { messages: [{ kind: 'event', name: 'DerivedEvent' }] },
357
353
  },
358
354
  {
359
- id: 'consumer',
360
355
  sourceFile: 'consumer.ts',
361
356
  isDerived: false,
362
357
  contains: { narrativeIds: ['consumer-1'] },
@@ -419,21 +414,18 @@ describe('computeCrossModuleImports', () => {
419
414
  integrations: [],
420
415
  modules: [
421
416
  {
422
- id: 'z-types',
423
417
  sourceFile: 'z-types.ts',
424
418
  isDerived: false,
425
419
  contains: { narrativeIds: ['types-1'] },
426
420
  declares: { messages: [{ kind: 'event', name: 'ZEvent' }] },
427
421
  },
428
422
  {
429
- id: 'a-types',
430
423
  sourceFile: 'a-types.ts',
431
424
  isDerived: false,
432
425
  contains: { narrativeIds: ['types-2'] },
433
426
  declares: { messages: [{ kind: 'event', name: 'AEvent' }] },
434
427
  },
435
428
  {
436
- id: 'consumer',
437
429
  sourceFile: 'consumer.ts',
438
430
  isDerived: false,
439
431
  contains: { narrativeIds: ['consumer-1'] },
@@ -448,3 +440,189 @@ describe('computeCrossModuleImports', () => {
448
440
  expect(imports.map((i) => i.fromPath)).toEqual(['./a-types', './z-types']);
449
441
  });
450
442
  });
443
+
444
+ describe('computeAllCrossModuleDependencies', () => {
445
+ it('returns empty maps when no cross-module imports exist', () => {
446
+ const model: Model = {
447
+ variant: 'specs',
448
+ narratives: [{ name: 'Test', id: 'test-1', slices: [] }],
449
+ messages: [{ type: 'event', source: 'internal', name: 'LocalEvent', fields: [] }],
450
+ integrations: [],
451
+ modules: [
452
+ {
453
+ sourceFile: 'self-contained.ts',
454
+ isDerived: false,
455
+ contains: { narrativeIds: ['test-1'] },
456
+ declares: { messages: [{ kind: 'event', name: 'LocalEvent' }] },
457
+ },
458
+ ],
459
+ };
460
+
461
+ const { importsPerModule, exportsPerModule } = computeAllCrossModuleDependencies(model.modules, model);
462
+
463
+ expect(importsPerModule.get('self-contained.ts')).toEqual([]);
464
+ expect(exportsPerModule.size).toBe(0);
465
+ });
466
+
467
+ it('computes imports and exports in a single pass', () => {
468
+ const model: Model = {
469
+ variant: 'specs',
470
+ narratives: [
471
+ { name: 'Shared', id: 'shared-1', slices: [] },
472
+ {
473
+ name: 'Consumer',
474
+ id: 'consumer-1',
475
+ slices: [
476
+ {
477
+ name: 'test',
478
+ type: 'command',
479
+ client: { specs: [] },
480
+ server: {
481
+ description: 'Test',
482
+ specs: [
483
+ {
484
+ type: 'gherkin',
485
+ feature: 'Test',
486
+ rules: [
487
+ {
488
+ name: 'rule',
489
+ examples: [
490
+ {
491
+ name: 'example',
492
+ steps: [{ keyword: 'Then', text: 'SharedEvent' }],
493
+ },
494
+ ],
495
+ },
496
+ ],
497
+ },
498
+ ],
499
+ },
500
+ },
501
+ ],
502
+ },
503
+ ],
504
+ messages: [{ type: 'event', source: 'internal', name: 'SharedEvent', fields: [] }],
505
+ integrations: [],
506
+ modules: [
507
+ {
508
+ sourceFile: 'shared/types.ts',
509
+ isDerived: false,
510
+ contains: { narrativeIds: ['shared-1'] },
511
+ declares: { messages: [{ kind: 'event', name: 'SharedEvent' }] },
512
+ },
513
+ {
514
+ sourceFile: 'features/consumer.ts',
515
+ isDerived: false,
516
+ contains: { narrativeIds: ['consumer-1'] },
517
+ declares: { messages: [] },
518
+ },
519
+ ],
520
+ };
521
+
522
+ const { importsPerModule, exportsPerModule } = computeAllCrossModuleDependencies(model.modules, model);
523
+
524
+ // Consumer should import SharedEvent
525
+ expect(importsPerModule.get('features/consumer.ts')).toEqual([
526
+ { fromPath: '../shared/types', typeNames: ['SharedEvent'] },
527
+ ]);
528
+
529
+ // Shared module should export SharedEvent
530
+ expect(exportsPerModule.get('shared/types.ts')).toEqual(new Set(['SharedEvent']));
531
+ expect(exportsPerModule.has('features/consumer.ts')).toBe(false);
532
+ });
533
+
534
+ it('aggregates exports from multiple importing modules', () => {
535
+ const model: Model = {
536
+ variant: 'specs',
537
+ narratives: [
538
+ { name: 'Shared', id: 'shared-1', slices: [] },
539
+ {
540
+ name: 'Consumer1',
541
+ id: 'consumer-1',
542
+ slices: [
543
+ {
544
+ name: 'test',
545
+ type: 'command',
546
+ client: { specs: [] },
547
+ server: {
548
+ description: 'Test',
549
+ specs: [
550
+ {
551
+ type: 'gherkin',
552
+ feature: 'Test',
553
+ rules: [
554
+ {
555
+ name: 'rule',
556
+ examples: [{ name: 'ex', steps: [{ keyword: 'Then', text: 'EventA' }] }],
557
+ },
558
+ ],
559
+ },
560
+ ],
561
+ },
562
+ },
563
+ ],
564
+ },
565
+ {
566
+ name: 'Consumer2',
567
+ id: 'consumer-2',
568
+ slices: [
569
+ {
570
+ name: 'test',
571
+ type: 'command',
572
+ client: { specs: [] },
573
+ server: {
574
+ description: 'Test',
575
+ specs: [
576
+ {
577
+ type: 'gherkin',
578
+ feature: 'Test',
579
+ rules: [
580
+ {
581
+ name: 'rule',
582
+ examples: [{ name: 'ex', steps: [{ keyword: 'Then', text: 'EventB' }] }],
583
+ },
584
+ ],
585
+ },
586
+ ],
587
+ },
588
+ },
589
+ ],
590
+ },
591
+ ],
592
+ messages: [
593
+ { type: 'event', source: 'internal', name: 'EventA', fields: [] },
594
+ { type: 'event', source: 'internal', name: 'EventB', fields: [] },
595
+ ],
596
+ integrations: [],
597
+ modules: [
598
+ {
599
+ sourceFile: 'shared.ts',
600
+ isDerived: false,
601
+ contains: { narrativeIds: ['shared-1'] },
602
+ declares: {
603
+ messages: [
604
+ { kind: 'event', name: 'EventA' },
605
+ { kind: 'event', name: 'EventB' },
606
+ ],
607
+ },
608
+ },
609
+ {
610
+ sourceFile: 'consumer1.ts',
611
+ isDerived: false,
612
+ contains: { narrativeIds: ['consumer-1'] },
613
+ declares: { messages: [] },
614
+ },
615
+ {
616
+ sourceFile: 'consumer2.ts',
617
+ isDerived: false,
618
+ contains: { narrativeIds: ['consumer-2'] },
619
+ declares: { messages: [] },
620
+ },
621
+ ],
622
+ };
623
+
624
+ const { exportsPerModule } = computeAllCrossModuleDependencies(model.modules, model);
625
+
626
+ expect(exportsPerModule.get('shared.ts')).toEqual(new Set(['EventA', 'EventB']));
627
+ });
628
+ });
@@ -6,20 +6,91 @@ import type { CrossModuleImport } from './types';
6
6
 
7
7
  export type { CrossModuleImport };
8
8
 
9
+ /**
10
+ * Computes cross-module imports for a single module.
11
+ * Returns the list of imports needed (with relative paths).
12
+ */
9
13
  export function computeCrossModuleImports(module: Module, allModules: Module[], model: Model): CrossModuleImport[] {
10
- if (module.isDerived) {
14
+ const dependencyMap = computeModuleDependencies(module, allModules, model);
15
+ if (!dependencyMap) {
11
16
  return [];
12
17
  }
18
+ return convertToImports(module.sourceFile, dependencyMap);
19
+ }
20
+
21
+ /**
22
+ * Computes cross-module imports for all modules and derives which types need to be exported.
23
+ * Returns both the imports per module and the exports per module in a single pass.
24
+ */
25
+ export function computeAllCrossModuleDependencies(
26
+ modules: Module[],
27
+ model: Model,
28
+ ): {
29
+ importsPerModule: Map<string, CrossModuleImport[]>;
30
+ exportsPerModule: Map<string, Set<string>>;
31
+ } {
32
+ const importsPerModule = new Map<string, CrossModuleImport[]>();
33
+ const exportsPerModule = new Map<string, Set<string>>();
34
+
35
+ for (const module of modules) {
36
+ const dependencyMap = computeModuleDependencies(module, modules, model);
37
+
38
+ if (!dependencyMap) {
39
+ importsPerModule.set(module.sourceFile, []);
40
+ continue;
41
+ }
42
+
43
+ // Convert to imports with relative paths
44
+ importsPerModule.set(module.sourceFile, convertToImports(module.sourceFile, dependencyMap));
45
+
46
+ // Track which types need to be exported from each declaring module
47
+ for (const [declaringSourceFile, typeNames] of dependencyMap) {
48
+ if (!exportsPerModule.has(declaringSourceFile)) {
49
+ exportsPerModule.set(declaringSourceFile, new Set());
50
+ }
51
+ for (const typeName of typeNames) {
52
+ exportsPerModule.get(declaringSourceFile)!.add(typeName);
53
+ }
54
+ }
55
+ }
56
+
57
+ return { importsPerModule, exportsPerModule };
58
+ }
59
+
60
+ export function resolveRelativeImport(fromPath: string, toPath: string): string {
61
+ const fromDir = dirname(fromPath);
62
+ const toDir = dirname(toPath);
63
+ const toFile = basename(toPath, extname(toPath));
64
+
65
+ const relativePath = relative(fromDir, toDir);
66
+ if (relativePath === '') {
67
+ return `./${toFile}`;
68
+ }
69
+ if (!relativePath.startsWith('.')) {
70
+ return `./${relativePath}/${toFile}`;
71
+ }
72
+
73
+ return join(relativePath, toFile);
74
+ }
75
+
76
+ /**
77
+ * Core logic: computes which types a module needs from other modules.
78
+ * Returns a map of declaringModuleSourceFile -> typeNames[], or null if no dependencies.
79
+ */
80
+ function computeModuleDependencies(module: Module, allModules: Module[], model: Model): Map<string, string[]> | null {
81
+ if (module.isDerived) {
82
+ return null;
83
+ }
13
84
 
14
85
  const declaredKeys = new Set(module.declares.messages.map((m) => toMessageKey(m.kind, m.name)));
15
86
  const usedKeys = collectUsedMessageKeysForModule(module, model);
16
87
  const neededKeys = new Set([...usedKeys].filter((k) => !declaredKeys.has(k)));
17
88
 
18
89
  if (neededKeys.size === 0) {
19
- return [];
90
+ return null;
20
91
  }
21
92
 
22
- const importsByModule = new Map<string, string[]>();
93
+ const dependencyMap = new Map<string, string[]>();
23
94
 
24
95
  for (const msgKey of neededKeys) {
25
96
  const { name } = parseMessageKey(msgKey);
@@ -27,36 +98,28 @@ export function computeCrossModuleImports(module: Module, allModules: Module[],
27
98
 
28
99
  if (declaringModule) {
29
100
  const modulePath = declaringModule.sourceFile;
30
- if (!importsByModule.has(modulePath)) {
31
- importsByModule.set(modulePath, []);
101
+ if (!dependencyMap.has(modulePath)) {
102
+ dependencyMap.set(modulePath, []);
32
103
  }
33
- importsByModule.get(modulePath)!.push(name);
104
+ dependencyMap.get(modulePath)!.push(name);
34
105
  }
35
106
  }
36
107
 
37
- const imports: CrossModuleImport[] = [];
38
- for (const [modulePath, typeNames] of importsByModule) {
39
- const relativePath = resolveRelativeImport(module.sourceFile, modulePath);
40
- imports.push({ fromPath: relativePath, typeNames });
41
- }
42
-
43
- return sortImportsBySource(imports);
108
+ return dependencyMap.size > 0 ? dependencyMap : null;
44
109
  }
45
110
 
46
- export function resolveRelativeImport(fromPath: string, toPath: string): string {
47
- const fromDir = dirname(fromPath);
48
- const toDir = dirname(toPath);
49
- const toFile = basename(toPath, extname(toPath));
111
+ /**
112
+ * Converts a dependency map (absolute paths) to CrossModuleImport[] (relative paths).
113
+ */
114
+ function convertToImports(fromSourceFile: string, dependencyMap: Map<string, string[]>): CrossModuleImport[] {
115
+ const imports: CrossModuleImport[] = [];
50
116
 
51
- const relativePath = relative(fromDir, toDir);
52
- if (relativePath === '') {
53
- return `./${toFile}`;
54
- }
55
- if (!relativePath.startsWith('.')) {
56
- return `./${relativePath}/${toFile}`;
117
+ for (const [modulePath, typeNames] of dependencyMap) {
118
+ const relativePath = resolveRelativeImport(fromSourceFile, modulePath);
119
+ imports.push({ fromPath: relativePath, typeNames });
57
120
  }
58
121
 
59
- return join(relativePath, toFile);
122
+ return sortImportsBySource(imports);
60
123
  }
61
124
 
62
125
  function collectUsedMessageKeysForModule(module: Module, model: Model): Set<string> {
@@ -2,7 +2,7 @@ import ts from 'typescript';
2
2
  import type { Model, Module } from '../../../index';
3
3
  import { deriveModules } from '../../narrative-to-model/derive-modules';
4
4
  import { analyzeCodeUsage } from '../analysis/usage';
5
- import { computeCrossModuleImports } from '../cross-module-imports';
5
+ import { computeAllCrossModuleDependencies } from '../cross-module-imports';
6
6
  import { sortFilesByPath } from '../ordering';
7
7
  import type { CrossModuleImport, GeneratedFile } from '../types';
8
8
  import { extractTypeIntegrationNames } from '../utils/integration-extractor';
@@ -22,11 +22,15 @@ export function generateAllModulesCode(model: Model, opts: GenerateModuleCodeOpt
22
22
  const modules =
23
23
  model.modules && model.modules.length > 0 ? model.modules : deriveModules(model.narratives, model.messages);
24
24
 
25
+ // Compute all cross-module dependencies in a single pass
26
+ const { importsPerModule, exportsPerModule } = computeAllCrossModuleDependencies(modules, model);
27
+
25
28
  const files: GeneratedFile[] = [];
26
29
 
27
30
  for (const module of modules) {
28
- const crossModuleImports = computeCrossModuleImports(module, modules, model);
29
- const code = generateModuleCode(module, model, opts, crossModuleImports);
31
+ const crossModuleImports = importsPerModule.get(module.sourceFile) ?? [];
32
+ const exportedTypes = exportsPerModule.get(module.sourceFile);
33
+ const code = generateModuleCode(module, model, opts, crossModuleImports, exportedTypes);
30
34
  files.push({
31
35
  path: module.sourceFile,
32
36
  code,
@@ -53,6 +57,7 @@ function generateModuleCode(
53
57
  model: Model,
54
58
  opts: GenerateModuleCodeOptions,
55
59
  crossModuleImports: CrossModuleImport[],
60
+ exportedTypes?: Set<string>,
56
61
  ): string {
57
62
  const f = ts.factory;
58
63
 
@@ -130,6 +135,7 @@ function generateModuleCode(
130
135
  usedFlowFunctionNames,
131
136
  narratives,
132
137
  crossModuleImports,
138
+ exportedTypes,
133
139
  );
134
140
 
135
141
  const file = f.createSourceFile(statements, f.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
@@ -146,6 +152,7 @@ function buildStatements(
146
152
  usedFlowFunctionNames: string[],
147
153
  flows: Model['narratives'],
148
154
  crossModuleImports: CrossModuleImport[],
155
+ exportedTypes?: Set<string>,
149
156
  ): ts.Statement[] {
150
157
  const f = tsModule.factory;
151
158
  const statements: ts.Statement[] = [];
@@ -185,7 +192,7 @@ function buildStatements(
185
192
  required: field.required,
186
193
  })),
187
194
  }));
188
- statements.push(...buildTypeAliases(tsModule, adaptedMessages));
195
+ statements.push(...buildTypeAliases(tsModule, adaptedMessages, exportedTypes));
189
196
 
190
197
  for (const flow of flows) {
191
198
  statements.push(...buildFlowStatements(tsModule, flow, messages));
@@ -7,7 +7,7 @@ type Message = {
7
7
  fields: { name: string; type: string; required: boolean }[];
8
8
  };
9
9
 
10
- export function buildTypeAliases(ts: typeof tsNS, messages: Message[]): tsNS.Statement[] {
10
+ export function buildTypeAliases(ts: typeof tsNS, messages: Message[], exportedTypes?: Set<string>): tsNS.Statement[] {
11
11
  const f = ts.factory;
12
12
 
13
13
  const mkK = (s: string) => f.createLiteralTypeNode(f.createStringLiteral(s, true));
@@ -37,11 +37,9 @@ export function buildTypeAliases(ts: typeof tsNS, messages: Message[]): tsNS.Sta
37
37
  m.type === 'event' ? 'Event' : m.type === 'command' ? 'Command' : m.type === 'query' ? 'Query' : 'State';
38
38
  const rhs = f.createTypeReferenceNode(baseTypeName, typeArgs);
39
39
 
40
- return f.createTypeAliasDeclaration(
41
- undefined, // No export keyword
42
- name,
43
- [],
44
- rhs,
45
- );
40
+ // Add export modifier if this type is imported by other modules
41
+ const modifiers = exportedTypes?.has(m.name) ? [f.createModifier(ts.SyntaxKind.ExportKeyword)] : undefined;
42
+
43
+ return f.createTypeAliasDeclaration(modifiers, name, [], rhs);
46
44
  });
47
45
  }
@@ -152,12 +152,13 @@ describe('module functionality', () => {
152
152
 
153
153
  expect(result.files).toHaveLength(2);
154
154
 
155
- // Find the orders file
156
155
  const ordersFile = result.files.find((f) => f.path.includes('orders'));
157
156
  expect(ordersFile).toBeDefined();
158
157
 
159
- // Orders file should import OrderCreated from shared
160
158
  expect(ordersFile!.code).toContain("import type { OrderCreated } from '../shared/types.narrative';");
159
+ const sharedFile = result.files.find((f) => f.path.includes('types'));
160
+ expect(sharedFile).toBeDefined();
161
+ expect(sharedFile!.code).toContain('export type OrderCreated = Event<');
161
162
  });
162
163
  });
163
164