@auto-engineer/narrative 1.157.0 → 1.158.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -6
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +72 -0
  5. package/dist/src/fluent-builder.d.ts +9 -1
  6. package/dist/src/fluent-builder.d.ts.map +1 -1
  7. package/dist/src/fluent-builder.js +36 -0
  8. package/dist/src/fluent-builder.js.map +1 -1
  9. package/dist/src/id/addAutoIds.d.ts.map +1 -1
  10. package/dist/src/id/addAutoIds.js +14 -0
  11. package/dist/src/id/addAutoIds.js.map +1 -1
  12. package/dist/src/index.d.ts +1 -1
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js +1 -1
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/loader/index.d.ts.map +1 -1
  17. package/dist/src/loader/index.js +2 -0
  18. package/dist/src/loader/index.js.map +1 -1
  19. package/dist/src/model-level-registry.d.ts +42 -0
  20. package/dist/src/model-level-registry.d.ts.map +1 -0
  21. package/dist/src/model-level-registry.js +42 -0
  22. package/dist/src/model-level-registry.js.map +1 -0
  23. package/dist/src/narrative-context.d.ts +2 -0
  24. package/dist/src/narrative-context.d.ts.map +1 -1
  25. package/dist/src/narrative-context.js +10 -0
  26. package/dist/src/narrative-context.js.map +1 -1
  27. package/dist/src/narrative.d.ts +16 -0
  28. package/dist/src/narrative.d.ts.map +1 -1
  29. package/dist/src/narrative.js +39 -1
  30. package/dist/src/narrative.js.map +1 -1
  31. package/dist/src/schema.d.ts +879 -1
  32. package/dist/src/schema.d.ts.map +1 -1
  33. package/dist/src/schema.js +45 -1
  34. package/dist/src/schema.js.map +1 -1
  35. package/dist/src/transformers/model-to-narrative/generators/flow.d.ts.map +1 -1
  36. package/dist/src/transformers/model-to-narrative/generators/flow.js +45 -1
  37. package/dist/src/transformers/model-to-narrative/generators/flow.js.map +1 -1
  38. package/dist/src/transformers/model-to-narrative/generators/imports.d.ts +1 -1
  39. package/dist/src/transformers/model-to-narrative/generators/imports.d.ts.map +1 -1
  40. package/dist/src/transformers/model-to-narrative/generators/imports.js +6 -1
  41. package/dist/src/transformers/model-to-narrative/generators/imports.js.map +1 -1
  42. package/dist/src/transformers/model-to-narrative/generators/metadata.d.ts +10 -0
  43. package/dist/src/transformers/model-to-narrative/generators/metadata.d.ts.map +1 -0
  44. package/dist/src/transformers/model-to-narrative/generators/metadata.js +93 -0
  45. package/dist/src/transformers/model-to-narrative/generators/metadata.js.map +1 -0
  46. package/dist/src/transformers/model-to-narrative/index.d.ts.map +1 -1
  47. package/dist/src/transformers/model-to-narrative/index.js +21 -1
  48. package/dist/src/transformers/model-to-narrative/index.js.map +1 -1
  49. package/dist/src/transformers/narrative-to-model/assemble.d.ts +10 -2
  50. package/dist/src/transformers/narrative-to-model/assemble.d.ts.map +1 -1
  51. package/dist/src/transformers/narrative-to-model/assemble.js +47 -11
  52. package/dist/src/transformers/narrative-to-model/assemble.js.map +1 -1
  53. package/dist/src/transformers/narrative-to-model/index.d.ts.map +1 -1
  54. package/dist/src/transformers/narrative-to-model/index.js +9 -1
  55. package/dist/src/transformers/narrative-to-model/index.js.map +1 -1
  56. package/dist/tsconfig.tsbuildinfo +1 -1
  57. package/ketchup-plan.md +14 -4
  58. package/package.json +4 -4
  59. package/src/fluent-builder.specs.ts +60 -0
  60. package/src/fluent-builder.ts +53 -1
  61. package/src/id/addAutoIds.ts +15 -0
  62. package/src/index.ts +5 -0
  63. package/src/loader/index.ts +2 -0
  64. package/src/model-level-registry.specs.ts +217 -0
  65. package/src/model-level-registry.ts +60 -0
  66. package/src/model-to-narrative.specs.ts +102 -0
  67. package/src/narrative-context.ts +10 -0
  68. package/src/narrative.ts +48 -0
  69. package/src/schema.specs.ts +290 -0
  70. package/src/schema.ts +55 -0
  71. package/src/transformers/model-to-narrative/generators/flow.ts +52 -1
  72. package/src/transformers/model-to-narrative/generators/imports.ts +6 -1
  73. package/src/transformers/model-to-narrative/generators/metadata.specs.ts +40 -0
  74. package/src/transformers/model-to-narrative/generators/metadata.ts +140 -0
  75. package/src/transformers/model-to-narrative/index.ts +25 -2
  76. package/src/transformers/narrative-to-model/assemble.specs.ts +82 -0
  77. package/src/transformers/narrative-to-model/assemble.ts +61 -12
  78. package/src/transformers/narrative-to-model/index.ts +22 -1
@@ -0,0 +1,140 @@
1
+ import type tsNS from 'typescript';
2
+ import type { Model, Narrative } from '../../../index';
3
+ import { jsonToExpr } from '../ast/emit-helpers';
4
+
5
+ export function buildAssumptionsCall(
6
+ ts: typeof import('typescript'),
7
+ f: tsNS.NodeFactory,
8
+ items: string[],
9
+ ): tsNS.Statement {
10
+ return f.createExpressionStatement(
11
+ f.createCallExpression(
12
+ f.createIdentifier('assumptions'),
13
+ undefined,
14
+ items.map((s) => f.createStringLiteral(s)),
15
+ ),
16
+ );
17
+ }
18
+
19
+ export function buildRequirementsCall(f: tsNS.NodeFactory, doc: string): tsNS.Statement {
20
+ return f.createExpressionStatement(
21
+ f.createCallExpression(f.createIdentifier('requirements'), undefined, [f.createStringLiteral(doc)]),
22
+ );
23
+ }
24
+
25
+ function buildActorCall(
26
+ ts: typeof import('typescript'),
27
+ f: tsNS.NodeFactory,
28
+ actorDef: Model['actors'] extends (infer T)[] | undefined ? T : never,
29
+ ): tsNS.Statement {
30
+ return f.createExpressionStatement(
31
+ f.createCallExpression(f.createIdentifier('actor'), undefined, [jsonToExpr(ts, f, actorDef)]),
32
+ );
33
+ }
34
+
35
+ function buildEntityCall(
36
+ ts: typeof import('typescript'),
37
+ f: tsNS.NodeFactory,
38
+ entityDef: Model['entities'] extends (infer T)[] | undefined ? T : never,
39
+ ): tsNS.Statement {
40
+ return f.createExpressionStatement(
41
+ f.createCallExpression(f.createIdentifier('entity'), undefined, [jsonToExpr(ts, f, entityDef)]),
42
+ );
43
+ }
44
+
45
+ export function buildModelMetadataStatements(ts: typeof import('typescript'), model: Model): tsNS.Statement[] {
46
+ const f = ts.factory;
47
+ const statements: tsNS.Statement[] = [];
48
+
49
+ if (model.actors?.length) {
50
+ for (const a of model.actors) statements.push(buildActorCall(ts, f, a));
51
+ }
52
+
53
+ if (model.entities?.length) {
54
+ for (const e of model.entities) statements.push(buildEntityCall(ts, f, e));
55
+ }
56
+
57
+ if (model.assumptions?.length) {
58
+ statements.push(buildAssumptionsCall(ts, f, model.assumptions));
59
+ }
60
+
61
+ if (model.requirements) {
62
+ statements.push(buildRequirementsCall(f, model.requirements));
63
+ }
64
+
65
+ return statements;
66
+ }
67
+
68
+ function buildNarrativeCall(
69
+ ts: typeof import('typescript'),
70
+ f: tsNS.NodeFactory,
71
+ nar: Narrative,
72
+ sceneIdToName: Map<string, string>,
73
+ ): tsNS.Statement {
74
+ const configProps: tsNS.ObjectLiteralElementLike[] = [];
75
+
76
+ if (nar.outcome) configProps.push(f.createPropertyAssignment('outcome', f.createStringLiteral(nar.outcome)));
77
+ if (nar.impact) configProps.push(f.createPropertyAssignment('impact', f.createStringLiteral(nar.impact)));
78
+ if (nar.actors?.length) configProps.push(f.createPropertyAssignment('actors', jsonToExpr(ts, f, nar.actors)));
79
+ if (nar.sceneIds.length > 0) {
80
+ const sceneNames = nar.sceneIds.map((id) => sceneIdToName.get(id) ?? id);
81
+ configProps.push(f.createPropertyAssignment('scenes', jsonToExpr(ts, f, sceneNames)));
82
+ }
83
+ if (nar.assumptions?.length)
84
+ configProps.push(f.createPropertyAssignment('assumptions', jsonToExpr(ts, f, nar.assumptions)));
85
+ if (nar.requirements)
86
+ configProps.push(f.createPropertyAssignment('requirements', f.createStringLiteral(nar.requirements)));
87
+
88
+ const args: tsNS.Expression[] = [f.createStringLiteral(nar.name)];
89
+ if (nar.id) args.push(f.createStringLiteral(nar.id));
90
+ args.push(f.createObjectLiteralExpression(configProps, configProps.length > 2));
91
+
92
+ return f.createExpressionStatement(f.createCallExpression(f.createIdentifier('narrative'), undefined, args));
93
+ }
94
+
95
+ function hasNarrativeMetadata(nar: Narrative): boolean {
96
+ return (
97
+ nar.outcome !== undefined ||
98
+ nar.impact !== undefined ||
99
+ (nar.actors?.length ?? 0) > 0 ||
100
+ (nar.assumptions?.length ?? 0) > 0 ||
101
+ nar.requirements !== undefined
102
+ );
103
+ }
104
+
105
+ function modelHasMetadata(model: Model): boolean {
106
+ return (
107
+ (model.actors?.length ?? 0) > 0 ||
108
+ (model.entities?.length ?? 0) > 0 ||
109
+ (model.assumptions?.length ?? 0) > 0 ||
110
+ model.requirements !== undefined ||
111
+ model.narratives?.some(hasNarrativeMetadata)
112
+ );
113
+ }
114
+
115
+ export function buildAllMetadataStatements(
116
+ ts: typeof import('typescript'),
117
+ model: Model,
118
+ ): { statements: tsNS.Statement[]; usedFunctions: Set<string> } | null {
119
+ if (!modelHasMetadata(model)) return null;
120
+
121
+ const f = ts.factory;
122
+ const usedFunctions = new Set<string>();
123
+ const modelStatements = buildModelMetadataStatements(ts, model);
124
+
125
+ if (model.actors?.length) usedFunctions.add('actor');
126
+ if (model.entities?.length) usedFunctions.add('entity');
127
+ if (model.assumptions?.length) usedFunctions.add('assumptions');
128
+ if (model.requirements) usedFunctions.add('requirements');
129
+
130
+ const sceneIdToName = new Map(model.scenes.map((s) => [s.id ?? s.name, s.name]));
131
+ const narrativeStatements: tsNS.Statement[] = [];
132
+ for (const nar of model.narratives) {
133
+ if (hasNarrativeMetadata(nar)) {
134
+ narrativeStatements.push(buildNarrativeCall(ts, f, nar, sceneIdToName));
135
+ usedFunctions.add('narrative');
136
+ }
137
+ }
138
+
139
+ return { statements: [...modelStatements, ...narrativeStatements], usedFunctions };
140
+ }
@@ -1,7 +1,10 @@
1
+ import ts from 'typescript';
1
2
  import type { Model } from '../../index';
2
3
  import { formatWithPrettier } from './formatting/prettier';
4
+ import { buildImports } from './generators/imports';
5
+ import { buildAllMetadataStatements } from './generators/metadata';
3
6
  import { generateAllModulesCode } from './generators/module-code';
4
- import type { GeneratedScenes } from './types';
7
+ import type { GeneratedFile, GeneratedScenes } from './types';
5
8
 
6
9
  /**
7
10
  * Converts a schema specification to TypeScript flow DSL code files.
@@ -17,7 +20,9 @@ export async function modelToNarrative(model: Model): Promise<GeneratedScenes> {
17
20
  const flowImport = '@auto-engineer/narrative';
18
21
  const integrationImport = extractIntegrationImportFromModel(model);
19
22
 
20
- const rawFiles = generateAllModulesCode(model, { flowImport, integrationImport });
23
+ const moduleFiles = generateAllModulesCode(model, { flowImport, integrationImport });
24
+ const metadataFile = generateMetadataFile(model, flowImport);
25
+ const rawFiles = metadataFile ? [metadataFile, ...moduleFiles] : moduleFiles;
21
26
 
22
27
  const formattedFiles = await Promise.all(
23
28
  rawFiles.map(async (file) => ({
@@ -29,6 +34,24 @@ export async function modelToNarrative(model: Model): Promise<GeneratedScenes> {
29
34
  return { files: formattedFiles };
30
35
  }
31
36
 
37
+ function generateMetadataFile(model: Model, flowImport: string): GeneratedFile | null {
38
+ const result = buildAllMetadataStatements(ts, model);
39
+ if (!result) return null;
40
+
41
+ const f = ts.factory;
42
+ const { statements, usedFunctions } = result;
43
+
44
+ const imports = buildImports(ts, { flowImport, integrationImport: '' }, [], [], [], Array.from(usedFunctions));
45
+ const allStatements: ts.Statement[] = [];
46
+ if (imports.importFlowValues) allStatements.push(imports.importFlowValues);
47
+ allStatements.push(...statements);
48
+
49
+ const file = f.createSourceFile(allStatements, f.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
50
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
51
+
52
+ return { path: 'model.narrative.ts', code: printer.printFile(file) };
53
+ }
54
+
32
55
  function extractIntegrationImportFromModel(model: Model): string {
33
56
  if (model.integrations && model.integrations.length > 0) {
34
57
  return model.integrations[0].source;
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { NarrativeDefinition } from '../../model-level-registry';
3
+ import { assembleSpecs } from './assemble';
4
+
5
+ describe('assembleSpecs', () => {
6
+ it('backward compat: no metadata produces Default narrative', () => {
7
+ const scenes = [{ name: 'Step', id: 's-1', moments: [] }];
8
+ const messages = [{ type: 'command' as const, name: 'DoIt', fields: [] }];
9
+
10
+ const model = assembleSpecs(scenes, messages);
11
+
12
+ expect(model).toEqual({
13
+ variant: 'specs',
14
+ scenes,
15
+ messages,
16
+ integrations: undefined,
17
+ modules: expect.any(Array),
18
+ narratives: [{ name: 'Default', sceneIds: ['s-1'] }],
19
+ });
20
+ });
21
+
22
+ it('passes through model-level metadata', () => {
23
+ const model = assembleSpecs([], [], undefined, {
24
+ actors: [{ name: 'Op', kind: 'person' as const, description: 'Runs it' }],
25
+ entities: [{ name: 'Item', description: 'A thing' }],
26
+ assumptions: ['Always online'],
27
+ requirements: 'Must be fast',
28
+ });
29
+
30
+ expect(model.actors).toEqual([{ name: 'Op', kind: 'person', description: 'Runs it' }]);
31
+ expect(model.entities).toEqual([{ name: 'Item', description: 'A thing' }]);
32
+ expect(model.assumptions).toEqual(['Always online']);
33
+ expect(model.requirements).toBe('Must be fast');
34
+ });
35
+
36
+ it('resolves narrative sceneNames to sceneIds', () => {
37
+ const scenes = [
38
+ { name: 'Add to Cart', id: 'n-1', moments: [] },
39
+ { name: 'Payment', id: 'n-2', moments: [] },
40
+ ];
41
+ const defs: NarrativeDefinition[] = [
42
+ {
43
+ name: 'Checkout',
44
+ outcome: 'Items purchased',
45
+ impact: 'critical',
46
+ actors: ['Buyer'],
47
+ scenes: ['Add to Cart', 'Payment'],
48
+ assumptions: ['Gateway up'],
49
+ requirements: 'PCI',
50
+ },
51
+ ];
52
+
53
+ const model = assembleSpecs(scenes, [], undefined, undefined, defs);
54
+
55
+ expect(model.narratives).toEqual([
56
+ {
57
+ name: 'Checkout',
58
+ sceneIds: ['n-1', 'n-2'],
59
+ outcome: 'Items purchased',
60
+ impact: 'critical',
61
+ actors: ['Buyer'],
62
+ assumptions: ['Gateway up'],
63
+ requirements: 'PCI',
64
+ },
65
+ ]);
66
+ });
67
+
68
+ it('creates Default narrative for uncovered scenes', () => {
69
+ const scenes = [
70
+ { name: 'Covered', id: 'n-1', moments: [] },
71
+ { name: 'Uncovered', id: 'n-2', moments: [] },
72
+ ];
73
+ const defs: NarrativeDefinition[] = [{ name: 'Flow', scenes: ['Covered'] }];
74
+
75
+ const model = assembleSpecs(scenes, [], undefined, undefined, defs);
76
+
77
+ expect(model.narratives).toEqual([
78
+ { name: 'Flow', sceneIds: ['n-1'] },
79
+ { name: 'Default', sceneIds: ['n-2'] },
80
+ ]);
81
+ });
82
+ });
@@ -1,23 +1,72 @@
1
- import type { Message, Model, Narrative, Scene } from '../../index';
1
+ import type { Actor, Entity, Message, Model, Narrative, Scene } from '../../index';
2
+ import type { NarrativeDefinition } from '../../model-level-registry';
2
3
  import { deriveModules } from './derive-modules';
3
4
 
4
- export function assembleSpecs(scenes: Scene[], messages: unknown[], integrations: unknown[]): Model {
5
- const typedMessages = messages as Message[];
6
- const modules = deriveModules(scenes, typedMessages);
5
+ type ModelMetadata = {
6
+ actors?: Actor[];
7
+ entities?: Entity[];
8
+ assumptions?: string[];
9
+ requirements?: string;
10
+ };
7
11
 
8
- const narratives: Narrative[] = [
9
- {
10
- name: 'Default',
11
- sceneIds: scenes.filter((s) => s.id).map((s) => s.id!),
12
- },
13
- ];
12
+ export function assembleSpecs(
13
+ scenes: Scene[],
14
+ messages: Message[],
15
+ integrations?: Model['integrations'],
16
+ modelMetadata?: ModelMetadata,
17
+ narrativeDefinitions?: NarrativeDefinition[],
18
+ ): Model {
19
+ const modules = deriveModules(scenes, messages);
20
+
21
+ const narratives = buildNarratives(scenes, narrativeDefinitions);
14
22
 
15
23
  return {
16
24
  variant: 'specs' as const,
17
25
  scenes,
18
- messages: typedMessages,
19
- integrations: integrations as Model['integrations'],
26
+ messages,
27
+ integrations,
20
28
  modules,
21
29
  narratives,
30
+ ...(modelMetadata?.actors?.length ? { actors: modelMetadata.actors } : {}),
31
+ ...(modelMetadata?.entities?.length ? { entities: modelMetadata.entities } : {}),
32
+ ...(modelMetadata?.assumptions?.length ? { assumptions: modelMetadata.assumptions } : {}),
33
+ ...(modelMetadata?.requirements ? { requirements: modelMetadata.requirements } : {}),
22
34
  };
23
35
  }
36
+
37
+ function buildNarratives(scenes: Scene[], definitions?: NarrativeDefinition[]): Narrative[] {
38
+ if (!definitions || definitions.length === 0) {
39
+ return [
40
+ {
41
+ name: 'Default',
42
+ sceneIds: scenes.filter((s) => s.id).map((s) => s.id!),
43
+ },
44
+ ];
45
+ }
46
+
47
+ const nameToId = new Map(scenes.filter((s) => s.id).map((s) => [s.name, s.id!]));
48
+ const coveredSceneIds = new Set<string>();
49
+
50
+ const narratives: Narrative[] = definitions.map((def) => {
51
+ const sceneIds = (def.scenes ?? [])
52
+ .map((name) => nameToId.get(name))
53
+ .filter((id): id is string => id !== undefined);
54
+ for (const id of sceneIds) coveredSceneIds.add(id);
55
+
56
+ const nar: Narrative = { name: def.name, sceneIds };
57
+ if (def.id) nar.id = def.id;
58
+ if (def.outcome) nar.outcome = def.outcome;
59
+ if (def.impact) nar.impact = def.impact;
60
+ if (def.actors?.length) nar.actors = def.actors;
61
+ if (def.assumptions?.length) nar.assumptions = def.assumptions;
62
+ if (def.requirements) nar.requirements = def.requirements;
63
+ return nar;
64
+ });
65
+
66
+ const uncoveredIds = scenes.filter((s) => s.id && !coveredSceneIds.has(s.id)).map((s) => s.id!);
67
+ if (uncoveredIds.length > 0) {
68
+ narratives.push({ name: 'Default', sceneIds: uncoveredIds });
69
+ }
70
+
71
+ return narratives;
72
+ }
@@ -2,6 +2,7 @@ import type { Message, Model, Moment, Scene } from '../../index';
2
2
  import { integrationExportRegistry } from '../../integration-export-registry';
3
3
  import { globalIntegrationRegistry } from '../../integration-registry';
4
4
  import type { TypeInfo } from '../../loader/ts-utils';
5
+ import { modelLevelRegistry } from '../../model-level-registry';
5
6
  import { assembleSpecs } from './assemble';
6
7
  import { applyExampleShapeHints, type ExampleShapeHints } from './example-shapes';
7
8
  import { inlineAllMessageFieldTypes } from './inlining';
@@ -340,5 +341,25 @@ export const scenesToModel = (scenes: Scene[], typesByFile?: Map<string, Map<str
340
341
  inlineAllMessageFieldTypes(messages, unionTypes);
341
342
  }
342
343
 
343
- return assembleSpecs(scenes, Array.from(messages.values()), Array.from(integrations.values()));
344
+ const {
345
+ actors,
346
+ entities,
347
+ assumptions: modelAssumptions,
348
+ requirements: modelRequirements,
349
+ narrativeDefinitions,
350
+ } = modelLevelRegistry.getAll();
351
+ const modelMetadata = {
352
+ ...(actors.length > 0 ? { actors } : {}),
353
+ ...(entities.length > 0 ? { entities } : {}),
354
+ ...(modelAssumptions.length > 0 ? { assumptions: modelAssumptions } : {}),
355
+ ...(modelRequirements ? { requirements: modelRequirements } : {}),
356
+ };
357
+
358
+ return assembleSpecs(
359
+ scenes,
360
+ Array.from(messages.values()),
361
+ Array.from(integrations.values()),
362
+ Object.keys(modelMetadata).length > 0 ? modelMetadata : undefined,
363
+ narrativeDefinitions.length > 0 ? narrativeDefinitions : undefined,
364
+ );
344
365
  };