@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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +147 -0
- package/dist/src/fluent-builder.d.ts +9 -1
- package/dist/src/fluent-builder.d.ts.map +1 -1
- package/dist/src/fluent-builder.js +36 -0
- package/dist/src/fluent-builder.js.map +1 -1
- package/dist/src/id/addAutoIds.d.ts.map +1 -1
- package/dist/src/id/addAutoIds.js +14 -0
- package/dist/src/id/addAutoIds.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- 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/index.d.ts.map +1 -1
- package/dist/src/loader/index.js +2 -0
- package/dist/src/loader/index.js.map +1 -1
- package/dist/src/model-level-registry.d.ts +42 -0
- package/dist/src/model-level-registry.d.ts.map +1 -0
- package/dist/src/model-level-registry.js +42 -0
- package/dist/src/model-level-registry.js.map +1 -0
- package/dist/src/narrative-context.d.ts +2 -0
- package/dist/src/narrative-context.d.ts.map +1 -1
- package/dist/src/narrative-context.js +10 -0
- package/dist/src/narrative-context.js.map +1 -1
- package/dist/src/narrative.d.ts +16 -0
- package/dist/src/narrative.d.ts.map +1 -1
- package/dist/src/narrative.js +39 -1
- package/dist/src/narrative.js.map +1 -1
- package/dist/src/schema.d.ts +879 -1
- package/dist/src/schema.d.ts.map +1 -1
- package/dist/src/schema.js +45 -1
- package/dist/src/schema.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.js +45 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/imports.d.ts +1 -1
- package/dist/src/transformers/model-to-narrative/generators/imports.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/imports.js +6 -1
- package/dist/src/transformers/model-to-narrative/generators/imports.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/metadata.d.ts +10 -0
- package/dist/src/transformers/model-to-narrative/generators/metadata.d.ts.map +1 -0
- package/dist/src/transformers/model-to-narrative/generators/metadata.js +93 -0
- package/dist/src/transformers/model-to-narrative/generators/metadata.js.map +1 -0
- package/dist/src/transformers/model-to-narrative/index.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/index.js +21 -1
- package/dist/src/transformers/model-to-narrative/index.js.map +1 -1
- package/dist/src/transformers/narrative-to-model/assemble.d.ts +10 -2
- package/dist/src/transformers/narrative-to-model/assemble.d.ts.map +1 -1
- package/dist/src/transformers/narrative-to-model/assemble.js +47 -11
- package/dist/src/transformers/narrative-to-model/assemble.js.map +1 -1
- package/dist/src/transformers/narrative-to-model/index.d.ts.map +1 -1
- package/dist/src/transformers/narrative-to-model/index.js +9 -1
- package/dist/src/transformers/narrative-to-model/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +14 -4
- package/package.json +4 -4
- package/src/fluent-builder.specs.ts +60 -0
- package/src/fluent-builder.ts +53 -1
- package/src/id/addAutoIds.ts +15 -0
- package/src/index.ts +5 -0
- package/src/loader/index.ts +2 -0
- package/src/model-level-registry.specs.ts +217 -0
- package/src/model-level-registry.ts +60 -0
- package/src/model-to-narrative.specs.ts +102 -0
- package/src/narrative-context.ts +10 -0
- package/src/narrative.ts +48 -0
- package/src/schema.specs.ts +290 -0
- package/src/schema.ts +55 -0
- package/src/transformers/model-to-narrative/generators/flow.ts +52 -1
- package/src/transformers/model-to-narrative/generators/imports.ts +6 -1
- package/src/transformers/model-to-narrative/generators/metadata.specs.ts +40 -0
- package/src/transformers/model-to-narrative/generators/metadata.ts +140 -0
- package/src/transformers/model-to-narrative/index.ts +25 -2
- package/src/transformers/narrative-to-model/assemble.specs.ts +82 -0
- package/src/transformers/narrative-to-model/assemble.ts +61 -12
- package/src/transformers/narrative-to-model/index.ts +22 -1
package/ketchup-plan.md
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
# Ketchup Plan:
|
|
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:
|
|
10
|
-
- [x] Burst 2:
|
|
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.
|
|
30
|
-
"@auto-engineer/id": "1.
|
|
31
|
-
"@auto-engineer/message-bus": "1.
|
|
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.
|
|
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
|
+
});
|
package/src/fluent-builder.ts
CHANGED
|
@@ -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 => {
|
package/src/id/addAutoIds.ts
CHANGED
|
@@ -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,
|
package/src/loader/index.ts
CHANGED
|
@@ -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
|
});
|
package/src/narrative-context.ts
CHANGED
|
@@ -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;
|