@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +26 -0
- package/dist/src/getNarratives.js +1 -1
- package/dist/src/getNarratives.js.map +1 -1
- package/dist/src/id/addAutoIds.d.ts.map +1 -1
- package/dist/src/id/addAutoIds.js +15 -0
- package/dist/src/id/addAutoIds.js.map +1 -1
- package/dist/src/id/hasAllIds.d.ts.map +1 -1
- package/dist/src/id/hasAllIds.js +6 -1
- package/dist/src/id/hasAllIds.js.map +1 -1
- package/dist/src/index.d.ts +5 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/loader/runtime-cjs.d.ts.map +1 -1
- package/dist/src/loader/runtime-cjs.js +16 -3
- package/dist/src/loader/runtime-cjs.js.map +1 -1
- package/dist/src/schema.d.ts +301 -143
- package/dist/src/schema.d.ts.map +1 -1
- package/dist/src/schema.js +26 -0
- package/dist/src/schema.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/cross-module-imports.d.ts +6 -0
- package/dist/src/transformers/model-to-narrative/cross-module-imports.d.ts.map +1 -0
- package/dist/src/transformers/model-to-narrative/cross-module-imports.js +63 -0
- package/dist/src/transformers/model-to-narrative/cross-module-imports.js.map +1 -0
- package/dist/src/transformers/model-to-narrative/generators/flow.d.ts +1 -4
- package/dist/src/transformers/model-to-narrative/generators/flow.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/module-code.d.ts +9 -0
- package/dist/src/transformers/model-to-narrative/generators/module-code.d.ts.map +1 -0
- package/dist/src/transformers/model-to-narrative/generators/module-code.js +102 -0
- package/dist/src/transformers/model-to-narrative/generators/module-code.js.map +1 -0
- package/dist/src/transformers/model-to-narrative/index.d.ts +6 -4
- package/dist/src/transformers/model-to-narrative/index.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/index.js +10 -6
- package/dist/src/transformers/model-to-narrative/index.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/ordering.d.ts +10 -0
- package/dist/src/transformers/model-to-narrative/ordering.d.ts.map +1 -0
- package/dist/src/transformers/model-to-narrative/ordering.js +37 -0
- package/dist/src/transformers/model-to-narrative/ordering.js.map +1 -0
- package/dist/src/transformers/model-to-narrative/spec-traversal.d.ts +3 -0
- package/dist/src/transformers/model-to-narrative/spec-traversal.d.ts.map +1 -0
- package/dist/src/transformers/model-to-narrative/spec-traversal.js +54 -0
- package/dist/src/transformers/model-to-narrative/spec-traversal.js.map +1 -0
- package/dist/src/transformers/model-to-narrative/types.d.ts +12 -0
- package/dist/src/transformers/model-to-narrative/types.d.ts.map +1 -0
- package/dist/src/transformers/model-to-narrative/types.js +2 -0
- package/dist/src/transformers/model-to-narrative/types.js.map +1 -0
- package/dist/src/transformers/model-to-narrative/validate-modules.d.ts +8 -0
- package/dist/src/transformers/model-to-narrative/validate-modules.d.ts.map +1 -0
- package/dist/src/transformers/model-to-narrative/validate-modules.js +121 -0
- package/dist/src/transformers/model-to-narrative/validate-modules.js.map +1 -0
- package/dist/src/transformers/narrative-to-model/assemble.d.ts.map +1 -1
- package/dist/src/transformers/narrative-to-model/assemble.js +5 -1
- package/dist/src/transformers/narrative-to-model/assemble.js.map +1 -1
- package/dist/src/transformers/narrative-to-model/derive-modules.d.ts +3 -0
- package/dist/src/transformers/narrative-to-model/derive-modules.d.ts.map +1 -0
- package/dist/src/transformers/narrative-to-model/derive-modules.js +29 -0
- package/dist/src/transformers/narrative-to-model/derive-modules.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/getNarratives.specs.ts +214 -1
- package/src/getNarratives.ts +1 -1
- package/src/id/addAutoIds.specs.ts +180 -0
- package/src/id/addAutoIds.ts +16 -1
- package/src/id/hasAllIds.specs.ts +87 -0
- package/src/id/hasAllIds.ts +10 -2
- package/src/index.ts +7 -0
- package/src/loader/runtime-cjs.ts +17 -3
- package/src/model-to-narrative.specs.ts +467 -17
- package/src/schema.ts +28 -0
- package/src/transformers/model-to-narrative/cross-module-imports.specs.ts +450 -0
- package/src/transformers/model-to-narrative/cross-module-imports.ts +83 -0
- package/src/transformers/model-to-narrative/generators/flow.ts +11 -10
- package/src/transformers/model-to-narrative/generators/module-code.ts +186 -0
- package/src/transformers/model-to-narrative/index.ts +19 -7
- package/src/transformers/model-to-narrative/modules.specs.ts +625 -0
- package/src/transformers/model-to-narrative/ordering.specs.ts +104 -0
- package/src/transformers/model-to-narrative/ordering.ts +46 -0
- package/src/transformers/model-to-narrative/spec-traversal.specs.ts +418 -0
- package/src/transformers/model-to-narrative/spec-traversal.ts +63 -0
- package/src/transformers/model-to-narrative/types.ts +13 -0
- package/src/transformers/model-to-narrative/validate-modules.ts +159 -0
- package/src/transformers/narrative-to-model/assemble.ts +7 -2
- package/src/transformers/narrative-to-model/derive-modules.specs.ts +121 -0
- package/src/transformers/narrative-to-model/derive-modules.ts +36 -0
- 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
|
|
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
|
|