@auto-engineer/narrative 1.156.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 +147 -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/ketchup-plan.md CHANGED
@@ -1,10 +1,20 @@
1
- # Ketchup Plan: Fix Scaffolder ListType Handling + node:crypto Protocol
1
+ # Ketchup Plan: Model Schema + DSL + Bidirectional Transformers
2
2
 
3
3
  ## TODO
4
4
 
5
- (none)
5
+ - [x] Burst 2b: initiator on BaseMomentSchema + .initiator() DSL method + tests [depends: none] (637390ac)
6
+ - [x] Burst 2c: buildMoment emission for initiator + fix stream/via debt + tests [depends: 2b] (0fc29650)
7
+ - [x] Burst 3: ModelLevelRegistry + actor()/entity() DSL + tests [depends: none] (b6fc86dd)
8
+ - [x] Burst 4: narrative() DSL (config object) + tests [depends: 3] (251b50a2)
9
+ - [x] Burst 5: assumptions()/requirements() 2-level dispatch + scene metadata + tests [depends: 3] (b31ab3b3)
10
+ - [x] Burst 6: Exports + loader integration [depends: 4, 5] (7f676bfa)
11
+ - [x] Burst 7: Fix assembleSpecs debt + wire model metadata + narrative definitions + tests [depends: 6] (55854ff5)
12
+ - [x] Burst 8: Metadata AST builders + model-level emission + tests [depends: none] (972c70dc)
13
+ - [x] Burst 9: Narrative block generation + metadata file wiring + tests [depends: 8] (2138e1b2)
14
+ - [x] Burst 10: Scene-level assumptions/requirements in flow generation + tests [depends: 8] (a0512fb9)
15
+ - [x] Burst 11: End-to-end round-trip verification tests [depends: 7, 2c, 9, 10] (11a91ac2)
6
16
 
7
17
  ## DONE
8
18
 
9
- - [x] Burst 1: Add isArray to getTypeName, handle ListType kind, update parseGraphQlRequest tsType wrapping — with test cases for [String!], [String!]!, [CustomType] (db71f007)
10
- - [x] Burst 2: Change 'crypto' to 'node:crypto' in handle.ts.ejs template
19
+ - [x] Burst 1: ActorSchema + EntitySchema + ImpactSchema + model root fields + tests [depends: none] (ed943ed7)
20
+ - [x] Burst 2: NarrativeSchema + SceneSchema + planning schema fields + tests [depends: 1] (2d83435b)
package/package.json CHANGED
@@ -26,9 +26,9 @@
26
26
  "typescript": "^5.9.2",
27
27
  "zod": "^3.22.4",
28
28
  "zod-to-json-schema": "^3.22.3",
29
- "@auto-engineer/file-store": "1.156.0",
30
- "@auto-engineer/id": "1.156.0",
31
- "@auto-engineer/message-bus": "1.156.0"
29
+ "@auto-engineer/file-store": "1.158.0",
30
+ "@auto-engineer/id": "1.158.0",
31
+ "@auto-engineer/message-bus": "1.158.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^20.0.0",
@@ -38,7 +38,7 @@
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "version": "1.156.0",
41
+ "version": "1.158.0",
42
42
  "scripts": {
43
43
  "build": "tsx scripts/build.ts",
44
44
  "test": "vitest run --reporter=dot",
@@ -80,3 +80,63 @@ describe('ui method', () => {
80
80
  }
81
81
  });
82
82
  });
83
+
84
+ describe('initiator method', () => {
85
+ beforeEach(() => {
86
+ startScene('test-flow');
87
+ });
88
+
89
+ afterEach(() => {
90
+ clearCurrentScene();
91
+ });
92
+
93
+ it('sets initiator on a command moment', () => {
94
+ command('Submit').initiator('Operator');
95
+
96
+ const scene = getCurrentScene();
97
+ expect(scene!.moments[0]).toEqual({
98
+ type: 'command',
99
+ name: 'Submit',
100
+ initiator: 'Operator',
101
+ client: { specs: [] },
102
+ server: { description: '', specs: [], data: undefined },
103
+ });
104
+ });
105
+
106
+ it('sets initiator on a query moment', () => {
107
+ query('Fetch').initiator('Operator');
108
+
109
+ const scene = getCurrentScene();
110
+ expect(scene!.moments[0]).toEqual({
111
+ type: 'query',
112
+ name: 'Fetch',
113
+ initiator: 'Operator',
114
+ client: { specs: [] },
115
+ server: { description: '', specs: [], data: undefined },
116
+ });
117
+ });
118
+
119
+ it('sets initiator on a react moment', () => {
120
+ react('Process').initiator('Gateway');
121
+
122
+ const scene = getCurrentScene();
123
+ expect(scene!.moments[0]).toEqual({
124
+ type: 'react',
125
+ name: 'Process',
126
+ initiator: 'Gateway',
127
+ server: { specs: [], data: undefined },
128
+ });
129
+ });
130
+
131
+ it('sets initiator on an experience moment', () => {
132
+ experience('Welcome').initiator('Visitor');
133
+
134
+ const scene = getCurrentScene();
135
+ expect(scene!.moments[0]).toEqual({
136
+ type: 'experience',
137
+ name: 'Welcome',
138
+ initiator: 'Visitor',
139
+ client: { specs: [] },
140
+ });
141
+ });
142
+ });
@@ -1,6 +1,6 @@
1
1
  import createDebug from 'debug';
2
2
  import { type ASTNode, print } from 'graphql';
3
- import type { CommandMoment, ExperienceMoment, QueryMoment, ReactMoment, UiBlock } from './index';
3
+ import type { CommandMoment, Exit, ExperienceMoment, QueryMoment, ReactMoment, UiBlock } from './index';
4
4
  import {
5
5
  addMoment,
6
6
  endClientBlock,
@@ -42,6 +42,8 @@ export interface FluentCommandMomentBuilder {
42
42
  via(integration: Integration | Integration[]): FluentCommandMomentBuilder;
43
43
  retries(count: number): FluentCommandMomentBuilder;
44
44
  request(mutation: unknown): FluentCommandMomentBuilder;
45
+ exits(exits: Exit[]): FluentCommandMomentBuilder;
46
+ initiator(name: string): FluentCommandMomentBuilder;
45
47
  }
46
48
 
47
49
  export interface FluentQueryMomentBuilder {
@@ -51,6 +53,8 @@ export interface FluentQueryMomentBuilder {
51
53
  server(description: string, fn: () => void): FluentQueryMomentBuilder;
52
54
  ui(spec: UiBlock): FluentQueryMomentBuilder;
53
55
  request(query: unknown): FluentQueryMomentBuilder;
56
+ exits(exits: Exit[]): FluentQueryMomentBuilder;
57
+ initiator(name: string): FluentQueryMomentBuilder;
54
58
  }
55
59
 
56
60
  export interface FluentReactionMomentBuilder {
@@ -58,12 +62,16 @@ export interface FluentReactionMomentBuilder {
58
62
  server(description: string, fn: () => void): FluentReactionMomentBuilder;
59
63
  via(integration: Integration | Integration[]): FluentReactionMomentBuilder;
60
64
  retries(count: number): FluentReactionMomentBuilder;
65
+ exits(exits: Exit[]): FluentReactionMomentBuilder;
66
+ initiator(name: string): FluentReactionMomentBuilder;
61
67
  }
62
68
 
63
69
  export interface FluentExperienceMomentBuilder {
64
70
  client(fn: () => void): FluentExperienceMomentBuilder;
65
71
  client(description: string, fn: () => void): FluentExperienceMomentBuilder;
66
72
  ui(spec: UiBlock): FluentExperienceMomentBuilder;
73
+ exits(exits: Exit[]): FluentExperienceMomentBuilder;
74
+ initiator(name: string): FluentExperienceMomentBuilder;
67
75
  }
68
76
 
69
77
  class CommandMomentBuilderImpl implements FluentCommandMomentBuilder {
@@ -176,6 +184,17 @@ class CommandMomentBuilderImpl implements FluentCommandMomentBuilder {
176
184
  }
177
185
  return this;
178
186
  }
187
+
188
+ exits(exits: Exit[]): FluentCommandMomentBuilder {
189
+ debugCommand('Setting exits for moment %s: %d exits', this.moment.name, exits.length);
190
+ this.moment.exits = exits;
191
+ return this;
192
+ }
193
+
194
+ initiator(name: string): FluentCommandMomentBuilder {
195
+ this.moment.initiator = name;
196
+ return this;
197
+ }
179
198
  }
180
199
 
181
200
  class QueryMomentBuilderImpl implements FluentQueryMomentBuilder {
@@ -268,6 +287,17 @@ class QueryMomentBuilderImpl implements FluentQueryMomentBuilder {
268
287
  }
269
288
  return this;
270
289
  }
290
+
291
+ exits(exits: Exit[]): FluentQueryMomentBuilder {
292
+ debugQuery('Setting exits for moment %s: %d exits', this.moment.name, exits.length);
293
+ this.moment.exits = exits;
294
+ return this;
295
+ }
296
+
297
+ initiator(name: string): FluentQueryMomentBuilder {
298
+ this.moment.initiator = name;
299
+ return this;
300
+ }
271
301
  }
272
302
 
273
303
  class ReactionMomentBuilderImpl implements FluentReactionMomentBuilder {
@@ -322,6 +352,17 @@ class ReactionMomentBuilderImpl implements FluentReactionMomentBuilder {
322
352
  this.moment.additionalInstructions = `retries: ${count}`;
323
353
  return this;
324
354
  }
355
+
356
+ exits(exits: Exit[]): FluentReactionMomentBuilder {
357
+ debugReact('Setting exits for moment %s: %d exits', this.moment.name, exits.length);
358
+ this.moment.exits = exits;
359
+ return this;
360
+ }
361
+
362
+ initiator(name: string): FluentReactionMomentBuilder {
363
+ this.moment.initiator = name;
364
+ return this;
365
+ }
325
366
  }
326
367
 
327
368
  class ExperienceMomentBuilderImpl implements FluentExperienceMomentBuilder {
@@ -367,6 +408,17 @@ class ExperienceMomentBuilderImpl implements FluentExperienceMomentBuilder {
367
408
  this.moment.client.ui = spec;
368
409
  return this;
369
410
  }
411
+
412
+ exits(exits: Exit[]): FluentExperienceMomentBuilder {
413
+ debugExperience('Setting exits for moment %s: %d exits', this.moment.name, exits.length);
414
+ this.moment.exits = exits;
415
+ return this;
416
+ }
417
+
418
+ initiator(name: string): FluentExperienceMomentBuilder {
419
+ this.moment.initiator = name;
420
+ return this;
421
+ }
370
422
  }
371
423
 
372
424
  export const command = (name: string, id?: string): FluentCommandMomentBuilder => {
@@ -101,12 +101,27 @@ function processDataItems(slice: Moment): Moment {
101
101
  return modifiedMoment;
102
102
  }
103
103
 
104
+ function processExits(slice: Moment): Moment {
105
+ if (!slice.exits || !Array.isArray(slice.exits) || slice.exits.length === 0) return slice;
106
+
107
+ const modifiedMoment = structuredClone(slice);
108
+ if (modifiedMoment.exits) {
109
+ modifiedMoment.exits = modifiedMoment.exits.map((exit) => {
110
+ const exitCopy = { ...exit };
111
+ ensureId(exitCopy);
112
+ return exitCopy;
113
+ });
114
+ }
115
+ return modifiedMoment;
116
+ }
117
+
104
118
  function processMoment(slice: Moment): Moment {
105
119
  let sliceCopy = { ...slice };
106
120
  ensureId(sliceCopy);
107
121
  sliceCopy = processServerSpecs(sliceCopy);
108
122
  sliceCopy = processClientSpecs(sliceCopy);
109
123
  sliceCopy = processDataItems(sliceCopy);
124
+ sliceCopy = processExits(sliceCopy);
110
125
  return sliceCopy;
111
126
  }
112
127
 
package/src/index.ts CHANGED
@@ -38,13 +38,18 @@ export { getScenes } from './getScenes';
38
38
  export { addAutoIds, hasAllIds } from './id';
39
39
  export type { ExampleBuilder, GivenBuilder, MomentTypeValueInterface, ThenBuilder, WhenBuilder } from './narrative';
40
40
  export {
41
+ actor,
42
+ assumptions,
41
43
  client,
42
44
  data,
43
45
  describe,
46
+ entity,
44
47
  example,
45
48
  it,
46
49
  MomentType,
50
+ narrative,
47
51
  request,
52
+ requirements,
48
53
  rule,
49
54
  scene,
50
55
  server,
@@ -1,6 +1,7 @@
1
1
  import createDebug from 'debug';
2
2
  import { integrationExportRegistry } from '../integration-export-registry';
3
3
  import { integrationRegistry } from '../integration-registry';
4
+ import { modelLevelRegistry } from '../model-level-registry';
4
5
  import { setGivenTypesByFile } from '../narrative-context';
5
6
  import { registry } from '../narrative-registry';
6
7
  import { type BuildGraphResult, buildGraph } from './graph';
@@ -32,6 +33,7 @@ export async function executeAST(
32
33
  registry.clearAll();
33
34
  integrationRegistry.clear();
34
35
  integrationExportRegistry.clear();
36
+ modelLevelRegistry.clearAll();
35
37
 
36
38
  // seed with built-ins (browser-safe shims included)
37
39
  let enhanced = await createEnhancedImportMap(importMap);
@@ -0,0 +1,217 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { command } from './fluent-builder';
3
+ import { modelLevelRegistry } from './model-level-registry';
4
+ import { actor, assumptions, entity, narrative, requirements } from './narrative';
5
+ import { clearCurrentScene, getCurrentScene, startScene } from './narrative-context';
6
+ import { registry } from './narrative-registry';
7
+ import { scenesToModel } from './transformers/narrative-to-model';
8
+
9
+ describe('ModelLevelRegistry', () => {
10
+ afterEach(() => {
11
+ modelLevelRegistry.clearAll();
12
+ });
13
+
14
+ it('accumulates actors via actor() DSL function', () => {
15
+ actor({ name: 'Operator', kind: 'person', description: 'Runs the system' });
16
+ actor({ name: 'Gateway', kind: 'system', description: 'Routes traffic' });
17
+
18
+ expect(modelLevelRegistry.getAll()).toEqual({
19
+ actors: [
20
+ { name: 'Operator', kind: 'person', description: 'Runs the system' },
21
+ { name: 'Gateway', kind: 'system', description: 'Routes traffic' },
22
+ ],
23
+ entities: [],
24
+ assumptions: [],
25
+ requirements: undefined,
26
+ narrativeDefinitions: [],
27
+ });
28
+ });
29
+
30
+ it('accumulates entities via entity() DSL function', () => {
31
+ entity({ name: 'Record', description: 'A data record', attributes: ['status'] });
32
+
33
+ expect(modelLevelRegistry.getAll()).toEqual({
34
+ actors: [],
35
+ entities: [{ name: 'Record', description: 'A data record', attributes: ['status'] }],
36
+ assumptions: [],
37
+ requirements: undefined,
38
+ narrativeDefinitions: [],
39
+ });
40
+ });
41
+
42
+ it('returns empty state after clearAll', () => {
43
+ actor({ name: 'X', kind: 'person', description: 'Y' });
44
+ entity({ name: 'Z', description: 'W' });
45
+ modelLevelRegistry.clearAll();
46
+
47
+ expect(modelLevelRegistry.getAll()).toEqual({
48
+ actors: [],
49
+ entities: [],
50
+ assumptions: [],
51
+ requirements: undefined,
52
+ narrativeDefinitions: [],
53
+ });
54
+ });
55
+
56
+ it('actor() throws inside scene context', () => {
57
+ startScene('test');
58
+ expect(() => actor({ name: 'X', kind: 'person', description: 'Y' })).toThrow();
59
+ clearCurrentScene();
60
+ });
61
+
62
+ it('entity() throws inside scene context', () => {
63
+ startScene('test');
64
+ expect(() => entity({ name: 'X', description: 'Y' })).toThrow();
65
+ clearCurrentScene();
66
+ });
67
+ });
68
+
69
+ describe('narrative() DSL', () => {
70
+ afterEach(() => {
71
+ modelLevelRegistry.clearAll();
72
+ });
73
+
74
+ it('registers narrative definition with all config fields', () => {
75
+ narrative('Checkout', {
76
+ outcome: 'Items purchased',
77
+ impact: 'critical',
78
+ actors: ['Buyer', 'Gateway'],
79
+ scenes: ['Add to Cart', 'Payment'],
80
+ assumptions: ['Gateway reachable'],
81
+ requirements: 'PCI compliance',
82
+ });
83
+
84
+ expect(modelLevelRegistry.getAll().narrativeDefinitions).toEqual([
85
+ {
86
+ name: 'Checkout',
87
+ outcome: 'Items purchased',
88
+ impact: 'critical',
89
+ actors: ['Buyer', 'Gateway'],
90
+ scenes: ['Add to Cart', 'Payment'],
91
+ assumptions: ['Gateway reachable'],
92
+ requirements: 'PCI compliance',
93
+ },
94
+ ]);
95
+ });
96
+
97
+ it('supports id overload', () => {
98
+ narrative('Setup', 'nar-1', { scenes: ['Configure'] });
99
+
100
+ expect(modelLevelRegistry.getAll().narrativeDefinitions).toEqual([
101
+ { name: 'Setup', id: 'nar-1', scenes: ['Configure'] },
102
+ ]);
103
+ });
104
+
105
+ it('registers minimal narrative with no optional fields', () => {
106
+ narrative('Simple', {});
107
+
108
+ expect(modelLevelRegistry.getAll().narrativeDefinitions).toEqual([{ name: 'Simple' }]);
109
+ });
110
+ });
111
+
112
+ describe('assumptions() and requirements() 2-level dispatch', () => {
113
+ afterEach(() => {
114
+ modelLevelRegistry.clearAll();
115
+ clearCurrentScene();
116
+ });
117
+
118
+ it('assumptions() at model level adds to registry', () => {
119
+ assumptions('System is online', 'Data is consistent');
120
+
121
+ expect(modelLevelRegistry.getAll().assumptions).toEqual(['System is online', 'Data is consistent']);
122
+ });
123
+
124
+ it('assumptions() inside scene adds to scene object', () => {
125
+ startScene('Process');
126
+ assumptions('Input is valid');
127
+
128
+ expect(getCurrentScene()).toEqual({
129
+ name: 'Process',
130
+ moments: [],
131
+ assumptions: ['Input is valid'],
132
+ });
133
+ });
134
+
135
+ it('requirements() at model level sets on registry', () => {
136
+ requirements('Must support multi-tenancy');
137
+
138
+ expect(modelLevelRegistry.getAll().requirements).toBe('Must support multi-tenancy');
139
+ });
140
+
141
+ it('requirements() inside scene sets on scene object', () => {
142
+ startScene('Process');
143
+ requirements('Must validate before processing');
144
+
145
+ expect(getCurrentScene()).toEqual({
146
+ name: 'Process',
147
+ moments: [],
148
+ requirements: 'Must validate before processing',
149
+ });
150
+ });
151
+
152
+ it('requirements() replaces previous value at model level', () => {
153
+ requirements('First');
154
+ requirements('Second');
155
+
156
+ expect(modelLevelRegistry.getAll().requirements).toBe('Second');
157
+ });
158
+
159
+ it('assumptions() accumulates across multiple calls', () => {
160
+ assumptions('A');
161
+ assumptions('B', 'C');
162
+
163
+ expect(modelLevelRegistry.getAll().assumptions).toEqual(['A', 'B', 'C']);
164
+ });
165
+ });
166
+
167
+ describe('DSL → scenesToModel round-trip', () => {
168
+ afterEach(() => {
169
+ modelLevelRegistry.clearAll();
170
+ registry.clearAll();
171
+ clearCurrentScene();
172
+ });
173
+
174
+ it('produces a Model with all metadata from DSL calls', () => {
175
+ actor({ name: 'Operator', kind: 'person', description: 'Runs system' });
176
+ entity({ name: 'Item', description: 'A thing' });
177
+ assumptions('System online');
178
+ requirements('Must be fast');
179
+ narrative('Flow', {
180
+ outcome: 'Goal met',
181
+ impact: 'critical',
182
+ actors: ['Operator'],
183
+ scenes: ['Step'],
184
+ assumptions: ['Data ready'],
185
+ requirements: 'Sub-second',
186
+ });
187
+
188
+ startScene('Step', 'n-1');
189
+ assumptions('Input valid');
190
+ requirements('Validate first');
191
+ command('Do').initiator('Operator');
192
+ const sceneObj = getCurrentScene()!;
193
+ registry.register(sceneObj);
194
+ clearCurrentScene();
195
+
196
+ const model = scenesToModel(registry.getAllScenes());
197
+
198
+ expect(model.actors).toEqual([{ name: 'Operator', kind: 'person', description: 'Runs system' }]);
199
+ expect(model.entities).toEqual([{ name: 'Item', description: 'A thing' }]);
200
+ expect(model.assumptions).toEqual(['System online']);
201
+ expect(model.requirements).toBe('Must be fast');
202
+ expect(model.narratives).toEqual([
203
+ {
204
+ name: 'Flow',
205
+ sceneIds: ['n-1'],
206
+ outcome: 'Goal met',
207
+ impact: 'critical',
208
+ actors: ['Operator'],
209
+ assumptions: ['Data ready'],
210
+ requirements: 'Sub-second',
211
+ },
212
+ ]);
213
+ expect(model.scenes[0].assumptions).toEqual(['Input valid']);
214
+ expect(model.scenes[0].requirements).toBe('Validate first');
215
+ expect(model.scenes[0].moments[0].initiator).toBe('Operator');
216
+ });
217
+ });
@@ -0,0 +1,60 @@
1
+ import type { Actor, Entity, Impact } from './schema';
2
+
3
+ export type NarrativeDefinition = {
4
+ name: string;
5
+ id?: string;
6
+ scenes?: string[];
7
+ outcome?: string;
8
+ impact?: Impact;
9
+ actors?: string[];
10
+ assumptions?: string[];
11
+ requirements?: string;
12
+ };
13
+
14
+ class ModelLevelRegistry {
15
+ private actors: Actor[] = [];
16
+ private entities: Entity[] = [];
17
+ private assumptions: string[] = [];
18
+ private requirements: string | undefined = undefined;
19
+ private narrativeDefinitions: NarrativeDefinition[] = [];
20
+
21
+ addActor(actorDef: Actor) {
22
+ this.actors.push(actorDef);
23
+ }
24
+
25
+ addEntity(entityDef: Entity) {
26
+ this.entities.push(entityDef);
27
+ }
28
+
29
+ addAssumptions(items: string[]) {
30
+ this.assumptions.push(...items);
31
+ }
32
+
33
+ setRequirements(doc: string) {
34
+ this.requirements = doc;
35
+ }
36
+
37
+ addNarrativeDefinition(def: NarrativeDefinition) {
38
+ this.narrativeDefinitions.push(def);
39
+ }
40
+
41
+ getAll() {
42
+ return {
43
+ actors: [...this.actors],
44
+ entities: [...this.entities],
45
+ assumptions: [...this.assumptions],
46
+ requirements: this.requirements,
47
+ narrativeDefinitions: [...this.narrativeDefinitions],
48
+ };
49
+ }
50
+
51
+ clearAll() {
52
+ this.actors = [];
53
+ this.entities = [];
54
+ this.assumptions = [];
55
+ this.requirements = undefined;
56
+ this.narrativeDefinitions = [];
57
+ }
58
+ }
59
+
60
+ export const modelLevelRegistry = new ModelLevelRegistry();
@@ -3607,4 +3607,106 @@ scene('All Projection Types', 'ALL-PROJ', () => {
3607
3607
  expect(code).toContain("surface: 'route'");
3608
3608
  expect(code).toContain("root: 'layout-root'");
3609
3609
  });
3610
+
3611
+ it('should emit .stream(), .initiator(), and .via() on moments', async () => {
3612
+ const model: Model = {
3613
+ variant: 'specs',
3614
+ scenes: [
3615
+ {
3616
+ name: 'Process',
3617
+ id: 'n-1',
3618
+ moments: [
3619
+ {
3620
+ type: 'command',
3621
+ name: 'Submit',
3622
+ stream: 'item-${itemId}',
3623
+ initiator: 'Operator',
3624
+ via: ['notifier'],
3625
+ client: { specs: [] },
3626
+ server: { description: 'Submits item', specs: [] },
3627
+ },
3628
+ ],
3629
+ },
3630
+ ],
3631
+ messages: [],
3632
+ integrations: [{ name: 'notifier', source: './integrations' }],
3633
+ modules: [],
3634
+ narratives: [],
3635
+ };
3636
+
3637
+ const result = await modelToNarrative(model);
3638
+ const code = getCode(result);
3639
+
3640
+ expect(code).toContain(".stream('item-${itemId}')");
3641
+ expect(code).toContain(".initiator('Operator')");
3642
+ expect(code).toContain('.via(Notifier)');
3643
+ });
3644
+
3645
+ it('should generate metadata file with actors, entities, and narratives', async () => {
3646
+ const model: Model = {
3647
+ variant: 'specs',
3648
+ scenes: [{ name: 'Step A', id: 'n-1', moments: [] }],
3649
+ messages: [],
3650
+ modules: [],
3651
+ narratives: [
3652
+ {
3653
+ name: 'Flow',
3654
+ sceneIds: ['n-1'],
3655
+ outcome: 'Goal achieved',
3656
+ impact: 'critical',
3657
+ actors: ['Op'],
3658
+ },
3659
+ ],
3660
+ actors: [{ name: 'Op', kind: 'person', description: 'Runs it' }],
3661
+ entities: [{ name: 'Item', description: 'A thing' }],
3662
+ assumptions: ['Online'],
3663
+ requirements: 'Fast',
3664
+ };
3665
+
3666
+ const result = await modelToNarrative(model);
3667
+ const metadataFile = result.files.find((f) => f.path === 'model.narrative.ts');
3668
+
3669
+ expect(metadataFile).toEqual({
3670
+ path: 'model.narrative.ts',
3671
+ code: expect.stringMatching(/actor\([\s\S]*entity\([\s\S]*assumptions\([\s\S]*requirements\([\s\S]*narrative\(/),
3672
+ });
3673
+ });
3674
+
3675
+ it('should not generate metadata file when model has no metadata', async () => {
3676
+ const model: Model = {
3677
+ variant: 'specs',
3678
+ scenes: [{ name: 'Simple', id: 'n-1', moments: [] }],
3679
+ messages: [],
3680
+ modules: [],
3681
+ narratives: [{ name: 'Default', sceneIds: ['n-1'] }],
3682
+ };
3683
+
3684
+ const result = await modelToNarrative(model);
3685
+ const metadataPaths = result.files.filter((f) => f.path === 'model.narrative.ts');
3686
+
3687
+ expect(metadataPaths).toEqual([]);
3688
+ });
3689
+
3690
+ it('should emit assumptions() and requirements() inside scene callbacks', async () => {
3691
+ const model: Model = {
3692
+ variant: 'specs',
3693
+ scenes: [
3694
+ {
3695
+ name: 'Process',
3696
+ id: 'n-1',
3697
+ moments: [],
3698
+ assumptions: ['Input valid'],
3699
+ requirements: 'Must validate first',
3700
+ },
3701
+ ],
3702
+ messages: [],
3703
+ modules: [],
3704
+ narratives: [],
3705
+ };
3706
+
3707
+ const result = await modelToNarrative(model);
3708
+ const code = getCode(result);
3709
+
3710
+ expect(code).toMatch(/scene\([\s\S]*assumptions\([\s\S]*requirements\(/);
3711
+ });
3610
3712
  });
@@ -87,6 +87,16 @@ export function clearCurrentScene(): void {
87
87
  context = null;
88
88
  }
89
89
 
90
+ export function addSceneAssumptions(items: string[]): void {
91
+ if (!context) throw new Error('No active scene');
92
+ context.scene.assumptions = [...(context.scene.assumptions ?? []), ...items];
93
+ }
94
+
95
+ export function setSceneRequirements(doc: string): void {
96
+ if (!context) throw new Error('No active scene');
97
+ context.scene.requirements = doc;
98
+ }
99
+
90
100
  export function getCurrentMoment(): Moment | null {
91
101
  if (!context || context.currentMomentIndex === null) return null;
92
102
  return context.scene.moments[context.currentMomentIndex] ?? null;