@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.
- 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 +72 -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/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
|
+
}
|
package/src/schema.specs.ts
CHANGED
|
@@ -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
|
|
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
|
-
'
|
|
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
|
+
});
|