@auto-engineer/narrative 1.157.0 → 1.158.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 (78) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -6
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +72 -0
  5. package/dist/src/fluent-builder.d.ts +9 -1
  6. package/dist/src/fluent-builder.d.ts.map +1 -1
  7. package/dist/src/fluent-builder.js +36 -0
  8. package/dist/src/fluent-builder.js.map +1 -1
  9. package/dist/src/id/addAutoIds.d.ts.map +1 -1
  10. package/dist/src/id/addAutoIds.js +14 -0
  11. package/dist/src/id/addAutoIds.js.map +1 -1
  12. package/dist/src/index.d.ts +1 -1
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js +1 -1
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/loader/index.d.ts.map +1 -1
  17. package/dist/src/loader/index.js +2 -0
  18. package/dist/src/loader/index.js.map +1 -1
  19. package/dist/src/model-level-registry.d.ts +42 -0
  20. package/dist/src/model-level-registry.d.ts.map +1 -0
  21. package/dist/src/model-level-registry.js +42 -0
  22. package/dist/src/model-level-registry.js.map +1 -0
  23. package/dist/src/narrative-context.d.ts +2 -0
  24. package/dist/src/narrative-context.d.ts.map +1 -1
  25. package/dist/src/narrative-context.js +10 -0
  26. package/dist/src/narrative-context.js.map +1 -1
  27. package/dist/src/narrative.d.ts +16 -0
  28. package/dist/src/narrative.d.ts.map +1 -1
  29. package/dist/src/narrative.js +39 -1
  30. package/dist/src/narrative.js.map +1 -1
  31. package/dist/src/schema.d.ts +879 -1
  32. package/dist/src/schema.d.ts.map +1 -1
  33. package/dist/src/schema.js +45 -1
  34. package/dist/src/schema.js.map +1 -1
  35. package/dist/src/transformers/model-to-narrative/generators/flow.d.ts.map +1 -1
  36. package/dist/src/transformers/model-to-narrative/generators/flow.js +45 -1
  37. package/dist/src/transformers/model-to-narrative/generators/flow.js.map +1 -1
  38. package/dist/src/transformers/model-to-narrative/generators/imports.d.ts +1 -1
  39. package/dist/src/transformers/model-to-narrative/generators/imports.d.ts.map +1 -1
  40. package/dist/src/transformers/model-to-narrative/generators/imports.js +6 -1
  41. package/dist/src/transformers/model-to-narrative/generators/imports.js.map +1 -1
  42. package/dist/src/transformers/model-to-narrative/generators/metadata.d.ts +10 -0
  43. package/dist/src/transformers/model-to-narrative/generators/metadata.d.ts.map +1 -0
  44. package/dist/src/transformers/model-to-narrative/generators/metadata.js +93 -0
  45. package/dist/src/transformers/model-to-narrative/generators/metadata.js.map +1 -0
  46. package/dist/src/transformers/model-to-narrative/index.d.ts.map +1 -1
  47. package/dist/src/transformers/model-to-narrative/index.js +21 -1
  48. package/dist/src/transformers/model-to-narrative/index.js.map +1 -1
  49. package/dist/src/transformers/narrative-to-model/assemble.d.ts +10 -2
  50. package/dist/src/transformers/narrative-to-model/assemble.d.ts.map +1 -1
  51. package/dist/src/transformers/narrative-to-model/assemble.js +47 -11
  52. package/dist/src/transformers/narrative-to-model/assemble.js.map +1 -1
  53. package/dist/src/transformers/narrative-to-model/index.d.ts.map +1 -1
  54. package/dist/src/transformers/narrative-to-model/index.js +9 -1
  55. package/dist/src/transformers/narrative-to-model/index.js.map +1 -1
  56. package/dist/tsconfig.tsbuildinfo +1 -1
  57. package/ketchup-plan.md +14 -4
  58. package/package.json +4 -4
  59. package/src/fluent-builder.specs.ts +60 -0
  60. package/src/fluent-builder.ts +53 -1
  61. package/src/id/addAutoIds.ts +15 -0
  62. package/src/index.ts +5 -0
  63. package/src/loader/index.ts +2 -0
  64. package/src/model-level-registry.specs.ts +217 -0
  65. package/src/model-level-registry.ts +60 -0
  66. package/src/model-to-narrative.specs.ts +102 -0
  67. package/src/narrative-context.ts +10 -0
  68. package/src/narrative.ts +48 -0
  69. package/src/schema.specs.ts +290 -0
  70. package/src/schema.ts +55 -0
  71. package/src/transformers/model-to-narrative/generators/flow.ts +52 -1
  72. package/src/transformers/model-to-narrative/generators/imports.ts +6 -1
  73. package/src/transformers/model-to-narrative/generators/metadata.specs.ts +40 -0
  74. package/src/transformers/model-to-narrative/generators/metadata.ts +140 -0
  75. package/src/transformers/model-to-narrative/index.ts +25 -2
  76. package/src/transformers/narrative-to-model/assemble.specs.ts +82 -0
  77. package/src/transformers/narrative-to-model/assemble.ts +61 -12
  78. package/src/transformers/narrative-to-model/index.ts +22 -1
package/src/narrative.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import createDebug from 'debug';
2
2
  import type { DataTargetItem } from './data-narrative-builders';
3
+ import type { NarrativeDefinition } from './model-level-registry';
4
+ import { modelLevelRegistry } from './model-level-registry';
3
5
  import {
6
+ addSceneAssumptions,
4
7
  clearCurrentScene,
5
8
  endClientBlock,
6
9
  endServerBlock,
7
10
  getCurrentMoment,
11
+ getCurrentScene,
8
12
  popDescribe,
9
13
  pushDescribe,
10
14
  pushSpec,
@@ -14,11 +18,13 @@ import {
14
18
  recordRule,
15
19
  recordStep,
16
20
  setMomentData,
21
+ setSceneRequirements,
17
22
  startClientBlock,
18
23
  startScene,
19
24
  startServerBlock,
20
25
  } from './narrative-context';
21
26
  import { registry } from './narrative-registry';
27
+ import { ActorSchema, EntitySchema, ImpactSchema } from './schema';
22
28
  import type { Data, DataItem } from './types';
23
29
 
24
30
  const debug = createDebug('auto:narrative:narrative');
@@ -234,3 +240,45 @@ export function data(config: Data | (DataItem | DataTargetItem)[]): void {
234
240
 
235
241
  setMomentData(dataConfig);
236
242
  }
243
+
244
+ export function actor(config: { name: string; kind: 'person' | 'system'; description: string }): void {
245
+ if (getCurrentScene()) throw new Error('actor() must be called at model level, not inside a scene');
246
+ modelLevelRegistry.addActor(ActorSchema.parse(config));
247
+ }
248
+
249
+ export function entity(config: { name: string; description: string; attributes?: string[] }): void {
250
+ if (getCurrentScene()) throw new Error('entity() must be called at model level, not inside a scene');
251
+ modelLevelRegistry.addEntity(EntitySchema.parse(config));
252
+ }
253
+
254
+ type NarrativeConfig = Omit<NarrativeDefinition, 'name' | 'id'>;
255
+
256
+ export function narrative(name: string, config: NarrativeConfig): void;
257
+ export function narrative(name: string, id: string, config: NarrativeConfig): void;
258
+ export function narrative(name: string, idOrConfig: string | NarrativeConfig, config?: NarrativeConfig): void {
259
+ const id = typeof idOrConfig === 'string' ? idOrConfig : undefined;
260
+ const cfg = typeof idOrConfig === 'string' ? config! : idOrConfig;
261
+
262
+ if (cfg.impact !== undefined) ImpactSchema.parse(cfg.impact);
263
+
264
+ const def: NarrativeDefinition = { name, ...cfg };
265
+ if (id !== undefined) def.id = id;
266
+
267
+ modelLevelRegistry.addNarrativeDefinition(def);
268
+ }
269
+
270
+ export function assumptions(...items: string[]): void {
271
+ if (getCurrentScene()) {
272
+ addSceneAssumptions(items);
273
+ } else {
274
+ modelLevelRegistry.addAssumptions(items);
275
+ }
276
+ }
277
+
278
+ export function requirements(doc: string): void {
279
+ if (getCurrentScene()) {
280
+ setSceneRequirements(doc);
281
+ } else {
282
+ modelLevelRegistry.setRequirements(doc);
283
+ }
284
+ }
@@ -1,6 +1,9 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import type {
3
+ Actor,
3
4
  ComponentDefinition,
5
+ Entity,
6
+ Impact,
4
7
  Narrative,
5
8
  NarrativePlanning,
6
9
  SceneClassification,
@@ -10,15 +13,19 @@ import type {
10
13
  UISpec,
11
14
  } from './schema';
12
15
  import {
16
+ ActorSchema,
13
17
  CommandMomentSchema,
14
18
  ComponentDefinitionSchema,
15
19
  DataSchema,
16
20
  DataTargetSchema,
17
21
  DesignSchema,
22
+ EntitySchema,
23
+ ImpactSchema,
18
24
  modelSchema,
19
25
  NarrativePlanningSchema,
20
26
  NarrativeSchema,
21
27
  QueryMomentSchema,
28
+ ReactMomentSchema,
22
29
  SceneClassificationSchema,
23
30
  SceneNamesOnlySchema,
24
31
  SceneRouteSchema,
@@ -57,6 +64,51 @@ describe('CommandMomentSchema', () => {
57
64
  });
58
65
  });
59
66
 
67
+ describe('BaseMomentSchema initiator field', () => {
68
+ it('should accept command moment with optional initiator', () => {
69
+ const input = {
70
+ type: 'command' as const,
71
+ name: 'Submit',
72
+ initiator: 'Operator',
73
+ client: { specs: [] },
74
+ server: { description: 'Submits', specs: [] },
75
+ };
76
+ const result = CommandMomentSchema.safeParse(input);
77
+ expect(result.success).toBe(true);
78
+ if (result.success) {
79
+ expect(result.data).toEqual(input);
80
+ }
81
+ });
82
+
83
+ it('should accept react moment with optional initiator', () => {
84
+ const input = {
85
+ type: 'react' as const,
86
+ name: 'Process',
87
+ initiator: 'Gateway',
88
+ server: { specs: [] },
89
+ };
90
+ const result = ReactMomentSchema.safeParse(input);
91
+ expect(result.success).toBe(true);
92
+ if (result.success) {
93
+ expect(result.data).toEqual(input);
94
+ }
95
+ });
96
+
97
+ it('should accept moment without initiator (backward compat)', () => {
98
+ const input = {
99
+ type: 'command' as const,
100
+ name: 'Submit',
101
+ client: { specs: [] },
102
+ server: { description: 'Submits', specs: [] },
103
+ };
104
+ const result = CommandMomentSchema.safeParse(input);
105
+ expect(result.success).toBe(true);
106
+ if (result.success) {
107
+ expect(result.data).toEqual(input);
108
+ }
109
+ });
110
+ });
111
+
60
112
  describe('CommandMomentSchema client.ui', () => {
61
113
  it('accepts ui block in client alongside specs', () => {
62
114
  const moment = {
@@ -206,6 +258,28 @@ describe('SceneSchema scene field', () => {
206
258
  expect(result.data.scene).toBeUndefined();
207
259
  }
208
260
  });
261
+
262
+ it('should accept scene with requirements and assumptions', () => {
263
+ const input = {
264
+ name: 'Process Item',
265
+ moments: [],
266
+ requirements: 'Must validate input before processing',
267
+ assumptions: ['Input is well-formed JSON'],
268
+ };
269
+ const result = SceneSchema.safeParse(input);
270
+ expect(result.success).toBe(true);
271
+ if (result.success) {
272
+ expect(result.data).toEqual(input);
273
+ }
274
+ });
275
+
276
+ it('should accept scene without requirements and assumptions (backward compat)', () => {
277
+ const result = SceneSchema.safeParse({ name: 'Simple', moments: [] });
278
+ expect(result.success).toBe(true);
279
+ if (result.success) {
280
+ expect(result.data).toEqual({ name: 'Simple', moments: [] });
281
+ }
282
+ });
209
283
  });
210
284
 
211
285
  describe('NarrativeSchema', () => {
@@ -246,6 +320,31 @@ describe('NarrativeSchema', () => {
246
320
  const result = NarrativeSchema.safeParse({ name: 'Onboarding' });
247
321
  expect(result.success).toBe(false);
248
322
  });
323
+
324
+ it('should accept narrative with outcome, impact, requirements, and assumptions', () => {
325
+ const input = {
326
+ name: 'Registration',
327
+ sceneIds: ['n-1'],
328
+ outcome: 'User gains access to the system',
329
+ impact: 'critical' as const,
330
+ requirements: 'Must complete within 60 seconds',
331
+ assumptions: ['Email service is reachable', 'Unique constraint on username'],
332
+ };
333
+ const result = NarrativeSchema.safeParse(input);
334
+ expect(result.success).toBe(true);
335
+ if (result.success) {
336
+ expect(result.data).toEqual(input);
337
+ }
338
+ });
339
+
340
+ it('should reject invalid impact value', () => {
341
+ const result = NarrativeSchema.safeParse({
342
+ name: 'X',
343
+ sceneIds: [],
344
+ impact: 'low',
345
+ });
346
+ expect(result.success).toBe(false);
347
+ });
249
348
  });
250
349
 
251
350
  describe('modelSchema narratives field', () => {
@@ -404,6 +503,44 @@ describe('NarrativePlanningSchema', () => {
404
503
 
405
504
  expect(result.success).toBe(false);
406
505
  });
506
+
507
+ it('should accept planning narrative with outcome, impact, and assumptions', () => {
508
+ const input = {
509
+ variant: 'narrative-planning' as const,
510
+ narratives: [
511
+ {
512
+ name: 'Setup',
513
+ sceneNames: ['Configure'],
514
+ outcome: 'System is configured',
515
+ impact: 'important' as const,
516
+ assumptions: ['Admin has credentials'],
517
+ },
518
+ ],
519
+ scenes: [{ name: 'Configure' }],
520
+ };
521
+ const result = NarrativePlanningSchema.safeParse(input);
522
+ expect(result.success).toBe(true);
523
+ if (result.success) {
524
+ expect(result.data).toEqual(input);
525
+ }
526
+ });
527
+
528
+ it('should accept planning schema with model-level actors, entities, assumptions, requirements', () => {
529
+ const input = {
530
+ variant: 'narrative-planning' as const,
531
+ narratives: [{ name: 'Flow', sceneNames: ['Step'] }],
532
+ scenes: [{ name: 'Step' }],
533
+ actors: [{ name: 'Operator', kind: 'person' as const, description: 'Runs the system' }],
534
+ entities: [{ name: 'Task', description: 'A unit of work' }],
535
+ assumptions: ['System is online'],
536
+ requirements: 'Must handle concurrent access',
537
+ };
538
+ const result = NarrativePlanningSchema.safeParse(input);
539
+ expect(result.success).toBe(true);
540
+ if (result.success) {
541
+ expect(result.data).toEqual(input);
542
+ }
543
+ });
407
544
  });
408
545
 
409
546
  describe('SceneNamesOnlySchema', () => {
@@ -771,3 +908,156 @@ describe('exported UI types', () => {
771
908
  expect(typed).toEqual(parsed);
772
909
  });
773
910
  });
911
+
912
+ describe('ActorSchema', () => {
913
+ it('should accept a valid person actor', () => {
914
+ const result = ActorSchema.safeParse({
915
+ name: 'Operator',
916
+ kind: 'person',
917
+ description: 'Manages the system',
918
+ });
919
+ expect(result.success).toBe(true);
920
+ if (result.success) {
921
+ expect(result.data).toEqual({
922
+ name: 'Operator',
923
+ kind: 'person',
924
+ description: 'Manages the system',
925
+ });
926
+ }
927
+ });
928
+
929
+ it('should accept a valid system actor', () => {
930
+ const result = ActorSchema.safeParse({
931
+ name: 'Gateway',
932
+ kind: 'system',
933
+ description: 'Routes requests',
934
+ });
935
+ expect(result.success).toBe(true);
936
+ if (result.success) {
937
+ expect(result.data).toEqual({
938
+ name: 'Gateway',
939
+ kind: 'system',
940
+ description: 'Routes requests',
941
+ });
942
+ }
943
+ });
944
+
945
+ it('should reject invalid kind', () => {
946
+ const result = ActorSchema.safeParse({
947
+ name: 'Bot',
948
+ kind: 'robot',
949
+ description: 'Does things',
950
+ });
951
+ expect(result.success).toBe(false);
952
+ });
953
+
954
+ it('should reject missing required fields', () => {
955
+ expect(ActorSchema.safeParse({ name: 'X' }).success).toBe(false);
956
+ expect(ActorSchema.safeParse({ kind: 'person' }).success).toBe(false);
957
+ expect(ActorSchema.safeParse({ description: 'Y' }).success).toBe(false);
958
+ });
959
+
960
+ it('Actor type matches ActorSchema inference', () => {
961
+ const parsed = ActorSchema.parse({ name: 'A', kind: 'person', description: 'D' });
962
+ const typed: Actor = parsed;
963
+ expect(typed).toEqual({ name: 'A', kind: 'person', description: 'D' });
964
+ });
965
+ });
966
+
967
+ describe('EntitySchema', () => {
968
+ it('should accept entity with name and description', () => {
969
+ const result = EntitySchema.safeParse({
970
+ name: 'Item',
971
+ description: 'A trackable item',
972
+ });
973
+ expect(result.success).toBe(true);
974
+ if (result.success) {
975
+ expect(result.data).toEqual({
976
+ name: 'Item',
977
+ description: 'A trackable item',
978
+ });
979
+ }
980
+ });
981
+
982
+ it('should accept entity with optional attributes', () => {
983
+ const result = EntitySchema.safeParse({
984
+ name: 'Record',
985
+ description: 'A data record',
986
+ attributes: ['status', 'priority', 'label'],
987
+ });
988
+ expect(result.success).toBe(true);
989
+ if (result.success) {
990
+ expect(result.data).toEqual({
991
+ name: 'Record',
992
+ description: 'A data record',
993
+ attributes: ['status', 'priority', 'label'],
994
+ });
995
+ }
996
+ });
997
+
998
+ it('should reject missing required fields', () => {
999
+ expect(EntitySchema.safeParse({ name: 'X' }).success).toBe(false);
1000
+ expect(EntitySchema.safeParse({ description: 'Y' }).success).toBe(false);
1001
+ });
1002
+
1003
+ it('Entity type matches EntitySchema inference', () => {
1004
+ const parsed = EntitySchema.parse({ name: 'E', description: 'D' });
1005
+ const typed: Entity = parsed;
1006
+ expect(typed).toEqual({ name: 'E', description: 'D' });
1007
+ });
1008
+ });
1009
+
1010
+ describe('ImpactSchema', () => {
1011
+ it('should accept all valid impact levels', () => {
1012
+ expect(ImpactSchema.safeParse('critical').success).toBe(true);
1013
+ expect(ImpactSchema.safeParse('important').success).toBe(true);
1014
+ expect(ImpactSchema.safeParse('nice-to-have').success).toBe(true);
1015
+ });
1016
+
1017
+ it('should reject invalid impact level', () => {
1018
+ expect(ImpactSchema.safeParse('low').success).toBe(false);
1019
+ expect(ImpactSchema.safeParse('high').success).toBe(false);
1020
+ });
1021
+
1022
+ it('Impact type matches ImpactSchema inference', () => {
1023
+ const parsed = ImpactSchema.parse('critical');
1024
+ const typed: Impact = parsed;
1025
+ expect(typed).toBe('critical');
1026
+ });
1027
+ });
1028
+
1029
+ describe('modelSchema model-level metadata fields', () => {
1030
+ const minimalModel = {
1031
+ variant: 'specs' as const,
1032
+ scenes: [],
1033
+ messages: [],
1034
+ modules: [],
1035
+ narratives: [],
1036
+ };
1037
+
1038
+ it('should accept model without new metadata fields (backward compat)', () => {
1039
+ const result = modelSchema.safeParse(minimalModel);
1040
+ expect(result.success).toBe(true);
1041
+ if (result.success) {
1042
+ expect(result.data).toEqual(minimalModel);
1043
+ }
1044
+ });
1045
+
1046
+ it('should accept model with all metadata fields', () => {
1047
+ const input = {
1048
+ ...minimalModel,
1049
+ actors: [
1050
+ { name: 'Operator', kind: 'person' as const, description: 'Manages system' },
1051
+ { name: 'Gateway', kind: 'system' as const, description: 'Routes requests' },
1052
+ ],
1053
+ entities: [{ name: 'Record', description: 'A data record', attributes: ['status', 'label'] }],
1054
+ assumptions: ['All users are authenticated', 'System runs in UTC'],
1055
+ requirements: '## Domain Requirements\n\nMust support multi-tenancy.',
1056
+ };
1057
+ const result = modelSchema.safeParse(input);
1058
+ expect(result.success).toBe(true);
1059
+ if (result.success) {
1060
+ expect(result.data).toEqual(input);
1061
+ }
1062
+ });
1063
+ });
package/src/schema.ts CHANGED
@@ -34,6 +34,24 @@ const IntegrationSchema = z
34
34
  })
35
35
  .describe('External service integration configuration');
36
36
 
37
+ const ActorSchema = z
38
+ .object({
39
+ name: z.string().describe('Actor name'),
40
+ kind: z.enum(['person', 'system']).describe('Whether this actor is a person or a system'),
41
+ description: z.string().describe('What this actor does in the domain'),
42
+ })
43
+ .describe('A person or system involved in the domain');
44
+
45
+ const EntitySchema = z
46
+ .object({
47
+ name: z.string().describe('Entity name'),
48
+ description: z.string().describe('What this entity represents'),
49
+ attributes: z.array(z.string()).optional().describe('Key attributes of this entity'),
50
+ })
51
+ .describe('A domain noun — something actors interact with');
52
+
53
+ const ImpactSchema = z.enum(['critical', 'important', 'nice-to-have']).describe('Priority level');
54
+
37
55
  // Data flow schemas for unified architecture
38
56
  export const MessageTargetSchema = z
39
57
  .object({
@@ -265,6 +283,17 @@ export const DesignSchema = z
265
283
  })
266
284
  .describe('Design fields for visual representation');
267
285
 
286
+ export const ExitSchema = z
287
+ .object({
288
+ id: z.string().optional().describe('Optional unique identifier for the exit'),
289
+ label: z.string().describe('Human-readable name for this exit (e.g., "Forgot Password")'),
290
+ condition: z
291
+ .string()
292
+ .describe('Condition under which this exit is taken (e.g., "User clicks forgot password link")'),
293
+ sceneId: z.string().describe('ID of the target scene to navigate to'),
294
+ })
295
+ .describe('Conditional exit from a moment to another scene');
296
+
268
297
  const BaseMomentSchema = z
269
298
  .object({
270
299
  name: z.string(),
@@ -274,6 +303,8 @@ const BaseMomentSchema = z
274
303
  via: z.array(z.string()).optional().describe('Integration names used by this moment'),
275
304
  additionalInstructions: z.string().optional().describe('Additional instructions'),
276
305
  design: DesignSchema.optional().describe('Design fields for visual representation'),
306
+ exits: z.array(ExitSchema).optional().describe('Conditional exits from this moment to other scenes'),
307
+ initiator: z.string().optional().describe('Which actor initiates this moment — references actor by name'),
277
308
  })
278
309
  .describe('Base properties shared by all moment types');
279
310
 
@@ -457,6 +488,10 @@ export const NarrativeSchema = z
457
488
  actors: z.array(z.string()).optional(),
458
489
  sceneIds: z.array(z.string()).describe('Ordered scene IDs composing this narrative'),
459
490
  design: DesignSchema.optional().describe('Design fields for visual representation'),
491
+ outcome: z.string().optional().describe('What value this journey delivers'),
492
+ impact: ImpactSchema.optional().describe('Priority — drives which narratives to build first'),
493
+ requirements: z.string().optional().describe('Markdown requirements document (narrative level)'),
494
+ assumptions: z.array(z.string()).optional().describe('Journey-specific assumptions'),
460
495
  })
461
496
  .describe('Narrative grouping scenes into an ordered flow');
462
497
 
@@ -469,6 +504,8 @@ const SceneSchema = z
469
504
  sourceFile: z.string().optional(),
470
505
  scene: SceneClassificationSchema.optional(),
471
506
  design: DesignSchema.optional().describe('Design fields for visual representation'),
507
+ requirements: z.string().optional().describe('Markdown requirements document (scene level)'),
508
+ assumptions: z.array(z.string()).optional().describe('Flow-specific assumptions'),
472
509
  })
473
510
  .describe('Business scene containing related moments');
474
511
 
@@ -537,6 +574,9 @@ const NarrativePlanningNarrativeSchema = z
537
574
  description: z.string().optional(),
538
575
  actors: z.array(z.string()).optional(),
539
576
  sceneNames: z.array(z.string()).describe('Ordered scene names'),
577
+ outcome: z.string().optional().describe('What value this journey delivers'),
578
+ impact: ImpactSchema.optional().describe('Priority — drives which narratives to build first'),
579
+ assumptions: z.array(z.string()).optional().describe('Journey-specific assumptions'),
540
580
  })
541
581
  .describe('Narrative with scene names for planning');
542
582
 
@@ -545,6 +585,10 @@ export const NarrativePlanningSchema = z
545
585
  variant: z.literal('narrative-planning').describe('Narrative-based planning with scene names'),
546
586
  narratives: z.array(NarrativePlanningNarrativeSchema),
547
587
  scenes: z.array(SceneNamesOnlySchema),
588
+ actors: z.array(ActorSchema).optional().describe('People and systems involved in the domain'),
589
+ entities: z.array(EntitySchema).optional().describe('Domain nouns — things actors interact with'),
590
+ assumptions: z.array(z.string()).optional().describe('Domain-wide assumptions'),
591
+ requirements: z.string().optional().describe('Markdown requirements document (domain level)'),
548
592
  })
549
593
  .describe('Progressive disclosure variant for narrative-based planning');
550
594
 
@@ -582,6 +626,10 @@ export const modelSchema = z
582
626
  modules: z.array(ModuleSchema).describe('Modules for type ownership and file grouping'),
583
627
  narratives: z.array(NarrativeSchema),
584
628
  design: ModelDesignSchema.optional().describe('Design fields for visual representation'),
629
+ actors: z.array(ActorSchema).optional().describe('People and systems involved in the domain'),
630
+ entities: z.array(EntitySchema).optional().describe('Domain nouns — things actors interact with'),
631
+ assumptions: z.array(z.string()).optional().describe('Domain-wide assumptions'),
632
+ requirements: z.string().optional().describe('Markdown requirements document (domain level)'),
585
633
  })
586
634
  .describe('Complete system specification with all implementation details');
587
635
 
@@ -613,6 +661,9 @@ export {
613
661
  StepWithErrorSchema,
614
662
  UISchema,
615
663
  ComponentDefinitionSchema,
664
+ ActorSchema,
665
+ EntitySchema,
666
+ ImpactSchema,
616
667
  };
617
668
 
618
669
  export type Model = z.infer<typeof modelSchema>;
@@ -644,3 +695,7 @@ export type UISpec = z.infer<typeof UISpecSchema>;
644
695
  export type UI = z.infer<typeof UISchema>;
645
696
  export type ComponentDefinition = z.infer<typeof ComponentDefinitionSchema>;
646
697
  export type UiBlock = z.infer<typeof UiBlockSchema>;
698
+ export type Exit = z.infer<typeof ExitSchema>;
699
+ export type Actor = z.infer<typeof ActorSchema>;
700
+ export type Entity = z.infer<typeof EntitySchema>;
701
+ export type Impact = z.infer<typeof ImpactSchema>;
@@ -17,7 +17,9 @@ import type {
17
17
  OriginSchema,
18
18
  } from '../../../schema';
19
19
  import { jsonToExpr } from '../ast/emit-helpers';
20
+ import { integrationNameToPascalCase } from '../utils/strings';
20
21
  import { buildConsolidatedGwtSpecBlock, type GWTBlock } from './gwt';
22
+ import { buildAssumptionsCall, buildRequirementsCall } from './metadata';
21
23
 
22
24
  type CommandMoment = CommandMomentType;
23
25
  type QueryMoment = QueryMomentType;
@@ -438,6 +440,20 @@ function addUiToChain(
438
440
  return chain;
439
441
  }
440
442
 
443
+ function addExitsToChain(
444
+ ts: typeof import('typescript'),
445
+ f: tsNS.NodeFactory,
446
+ chain: tsNS.Expression,
447
+ slice: Moment,
448
+ ): tsNS.Expression {
449
+ if (slice.exits !== undefined && slice.exits.length > 0) {
450
+ return f.createCallExpression(f.createPropertyAccessExpression(chain, f.createIdentifier('exits')), undefined, [
451
+ jsonToExpr(ts, f, slice.exits),
452
+ ]);
453
+ }
454
+ return chain;
455
+ }
456
+
441
457
  function addRequestToChain(
442
458
  f: tsNS.NodeFactory,
443
459
  chain: tsNS.Expression,
@@ -758,6 +774,33 @@ function addServerToChain(
758
774
  return chain;
759
775
  }
760
776
 
777
+ function addStreamToChain(f: tsNS.NodeFactory, chain: tsNS.Expression, slice: Moment): tsNS.Expression {
778
+ if (slice.stream !== undefined) {
779
+ return f.createCallExpression(f.createPropertyAccessExpression(chain, f.createIdentifier('stream')), undefined, [
780
+ f.createStringLiteral(slice.stream),
781
+ ]);
782
+ }
783
+ return chain;
784
+ }
785
+
786
+ function addInitiatorToChain(f: tsNS.NodeFactory, chain: tsNS.Expression, slice: Moment): tsNS.Expression {
787
+ if (slice.initiator !== undefined) {
788
+ return f.createCallExpression(f.createPropertyAccessExpression(chain, f.createIdentifier('initiator')), undefined, [
789
+ f.createStringLiteral(slice.initiator),
790
+ ]);
791
+ }
792
+ return chain;
793
+ }
794
+
795
+ function addViaToChain(f: tsNS.NodeFactory, chain: tsNS.Expression, slice: Moment): tsNS.Expression {
796
+ if (slice.via !== undefined && slice.via.length > 0) {
797
+ const identifiers = slice.via.map((name) => f.createIdentifier(integrationNameToPascalCase(name)));
798
+ const arg = identifiers.length === 1 ? identifiers[0] : f.createArrayLiteralExpression(identifiers);
799
+ return f.createCallExpression(f.createPropertyAccessExpression(chain, f.createIdentifier('via')), undefined, [arg]);
800
+ }
801
+ return chain;
802
+ }
803
+
761
804
  function buildMoment(
762
805
  ts: typeof import('typescript'),
763
806
  f: tsNS.NodeFactory,
@@ -780,10 +823,14 @@ function buildMoment(
780
823
 
781
824
  let chain: tsNS.Expression = f.createCallExpression(f.createIdentifier(sliceCtor), undefined, args);
782
825
 
826
+ chain = addStreamToChain(f, chain, slice);
827
+ chain = addInitiatorToChain(f, chain, slice);
828
+ chain = addViaToChain(f, chain, slice);
783
829
  chain = addClientToChain(ts, f, chain, slice);
784
830
  chain = addUiToChain(ts, f, chain, slice);
785
831
  chain = addRequestToChain(f, chain, slice);
786
832
  chain = addServerToChain(ts, f, chain, slice, messages);
833
+ chain = addExitsToChain(ts, f, chain, slice);
787
834
 
788
835
  return f.createExpressionStatement(chain);
789
836
  }
@@ -795,7 +842,11 @@ export function buildFlowStatements(
795
842
  ): tsNS.Statement[] {
796
843
  const f = ts.factory;
797
844
 
798
- const body = (flow.moments ?? []).map((sl: Moment) => buildMoment(ts, f, sl, messages));
845
+ const sceneMetadata: tsNS.Statement[] = [];
846
+ if (flow.assumptions?.length) sceneMetadata.push(buildAssumptionsCall(ts, f, flow.assumptions));
847
+ if (flow.requirements) sceneMetadata.push(buildRequirementsCall(f, flow.requirements));
848
+ const momentStatements = (flow.moments ?? []).map((sl: Moment) => buildMoment(ts, f, sl, messages));
849
+ const body = [...sceneMetadata, ...momentStatements];
799
850
 
800
851
  const flowArgs: tsNS.Expression[] = [f.createStringLiteral(flow.name)];
801
852
  if (flow.id !== null && flow.id !== undefined) {
@@ -1,17 +1,22 @@
1
1
  type BuildImportsOpts = { flowImport: string; integrationImport: string };
2
2
 
3
3
  export const ALL_FLOW_FUNCTION_NAMES = [
4
+ 'actor',
5
+ 'assumptions',
4
6
  'command',
5
7
  'data',
6
8
  'describe',
9
+ 'entity',
7
10
  'example',
8
11
  'experience',
9
12
  'gql',
10
13
  'it',
11
- 'scene',
14
+ 'narrative',
12
15
  'query',
13
16
  'react',
17
+ 'requirements',
14
18
  'rule',
19
+ 'scene',
15
20
  'sink',
16
21
  'source',
17
22
  'specs',
@@ -0,0 +1,40 @@
1
+ import ts from 'typescript';
2
+ import { describe, expect, it } from 'vitest';
3
+ import type { Model } from '../../../index';
4
+ import { buildModelMetadataStatements } from './metadata';
5
+
6
+ function printStatements(statements: ts.Statement[]): string {
7
+ const f = ts.factory;
8
+ const file = f.createSourceFile(statements, f.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
9
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
10
+ return printer.printFile(file);
11
+ }
12
+
13
+ describe('buildModelMetadataStatements', () => {
14
+ it('generates actor, entity, assumptions, and requirements calls', () => {
15
+ const model = {
16
+ actors: [{ name: 'Operator', kind: 'person', description: 'Runs it' }],
17
+ entities: [{ name: 'Item', description: 'A thing', attributes: ['status'] }],
18
+ assumptions: ['Always online'],
19
+ requirements: 'Must be fast',
20
+ } as Model;
21
+
22
+ const statements = buildModelMetadataStatements(ts, model);
23
+ const code = printStatements(statements);
24
+
25
+ expect(code).toEqual(
26
+ 'actor({ name: "Operator", kind: "person", description: "Runs it" });\n' +
27
+ 'entity({ name: "Item", description: "A thing", attributes: ["status"] });\n' +
28
+ 'assumptions("Always online");\n' +
29
+ 'requirements("Must be fast");\n',
30
+ );
31
+ });
32
+
33
+ it('returns empty array when model has no metadata', () => {
34
+ const model = {} as Model;
35
+
36
+ const statements = buildModelMetadataStatements(ts, model);
37
+
38
+ expect(statements).toEqual([]);
39
+ });
40
+ });