@auto-engineer/server-generator-apollo-emmett 1.154.0 → 1.156.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 +57 -0
- package/dist/src/codegen/extract/data-sink.d.ts +1 -0
- package/dist/src/codegen/extract/data-sink.d.ts.map +1 -1
- package/dist/src/codegen/extract/data-sink.js +3 -0
- package/dist/src/codegen/extract/data-sink.js.map +1 -1
- package/dist/src/codegen/extract/messages.d.ts +1 -0
- package/dist/src/codegen/extract/messages.d.ts.map +1 -1
- package/dist/src/codegen/extract/messages.js +1 -1
- package/dist/src/codegen/extract/messages.js.map +1 -1
- package/dist/src/codegen/extract/sibling-events.d.ts +8 -0
- package/dist/src/codegen/extract/sibling-events.d.ts.map +1 -0
- package/dist/src/codegen/extract/sibling-events.js +58 -0
- package/dist/src/codegen/extract/sibling-events.js.map +1 -0
- package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
- package/dist/src/codegen/scaffoldFromSchema.js +37 -2
- package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
- package/dist/src/codegen/templates/command/decide.specs.specs.ts +125 -0
- package/dist/src/codegen/templates/command/decide.specs.ts +258 -3
- package/dist/src/codegen/templates/command/decide.specs.ts.ejs +42 -14
- package/dist/src/codegen/templates/command/decide.ts.ejs +63 -16
- package/dist/src/codegen/templates/command/evolve.specs.ts +217 -39
- package/dist/src/codegen/templates/command/evolve.ts.ejs +7 -7
- package/dist/src/codegen/templates/command/handle.specs.ts +7 -4
- package/dist/src/codegen/templates/command/handle.ts.ejs +17 -5
- package/dist/src/codegen/templates/command/state.specs.ts +125 -0
- package/dist/src/codegen/templates/command/state.ts.ejs +10 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +3 -3
- package/package.json +4 -4
- package/src/codegen/extract/data-sink.specs.ts +23 -1
- package/src/codegen/extract/data-sink.ts +4 -0
- package/src/codegen/extract/messages.ts +1 -1
- package/src/codegen/extract/sibling-events.specs.ts +206 -0
- package/src/codegen/extract/sibling-events.ts +76 -0
- package/src/codegen/ketchup-plan.md +12 -0
- package/src/codegen/scaffoldFromSchema.ts +50 -0
- package/src/codegen/templates/command/decide.specs.specs.ts +125 -0
- package/src/codegen/templates/command/decide.specs.ts +258 -3
- package/src/codegen/templates/command/decide.specs.ts.ejs +42 -14
- package/src/codegen/templates/command/decide.ts.ejs +63 -16
- package/src/codegen/templates/command/evolve.specs.ts +217 -39
- package/src/codegen/templates/command/evolve.ts.ejs +7 -7
- package/src/codegen/templates/command/handle.specs.ts +7 -4
- package/src/codegen/templates/command/handle.ts.ejs +17 -5
- package/src/codegen/templates/command/state.specs.ts +125 -0
- package/src/codegen/templates/command/state.ts.ejs +10 -2
package/ketchup-plan.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
# Ketchup Plan:
|
|
1
|
+
# Ketchup Plan: G1 Fix — Coordinate stream UUID with event field UUID
|
|
2
2
|
|
|
3
3
|
## TODO
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- [ ] B1: scaffoldFromSchema compute uuidVars + handle.ts.ejs named constants + context passing + handle.specs.ts snapshots
|
|
6
|
+
- [ ] B2: decide.ts.ejs context parameter + uuid auto-assignments + decide.specs.ts snapshots + coordination test
|
|
6
7
|
|
|
7
8
|
## DONE
|
|
8
9
|
|
|
9
10
|
- [x] B1-B2: Test + fix deduplication of field completeness assertions for multi-example commands (4183cf6d)
|
|
10
|
-
|
package/package.json
CHANGED
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"uuid": "^13.0.0",
|
|
33
33
|
"web-streams-polyfill": "^4.1.0",
|
|
34
34
|
"zod": "^3.22.4",
|
|
35
|
-
"@auto-engineer/narrative": "1.
|
|
36
|
-
"@auto-engineer/message-bus": "1.
|
|
35
|
+
"@auto-engineer/narrative": "1.156.0",
|
|
36
|
+
"@auto-engineer/message-bus": "1.156.0"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
|
@@ -44,9 +44,9 @@
|
|
|
44
44
|
"typescript": "^5.8.3",
|
|
45
45
|
"vitest": "^3.2.4",
|
|
46
46
|
"tsx": "^4.19.2",
|
|
47
|
-
"@auto-engineer/cli": "1.
|
|
47
|
+
"@auto-engineer/cli": "1.156.0"
|
|
48
48
|
},
|
|
49
|
-
"version": "1.
|
|
49
|
+
"version": "1.156.0",
|
|
50
50
|
"scripts": {
|
|
51
51
|
"generate:server": "tsx src/cli/index.ts",
|
|
52
52
|
"build": "tsc && tsx ../../scripts/fix-esm-imports.ts && rm -rf dist/src/codegen/templates && mkdir -p dist/src/codegen && cp -r src/codegen/templates dist/src/codegen/templates && cp src/server.ts dist/src && cp -r src/utils dist/src && cp -r src/domain dist/src",
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { extractStreamIdFields } from './data-sink';
|
|
2
|
+
import { extractStreamIdFields, normalizeStreamPattern } from './data-sink';
|
|
3
|
+
|
|
4
|
+
describe('normalizeStreamPattern', () => {
|
|
5
|
+
it('replaces a single variable with wildcard', () => {
|
|
6
|
+
expect(normalizeStreamPattern('sessions-${id}')).toBe('sessions-*');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('replaces different variable names with the same wildcard', () => {
|
|
10
|
+
expect(normalizeStreamPattern('sessions-${sessionId}')).toBe('sessions-*');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('replaces multiple variables', () => {
|
|
14
|
+
expect(normalizeStreamPattern('user-${userId}-project-${projectId}')).toBe('user-*-project-*');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns pattern unchanged when no variables', () => {
|
|
18
|
+
expect(normalizeStreamPattern('static-stream')).toBe('static-stream');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns empty string for empty input', () => {
|
|
22
|
+
expect(normalizeStreamPattern('')).toBe('');
|
|
23
|
+
});
|
|
24
|
+
});
|
|
3
25
|
|
|
4
26
|
describe('extractStreamIdFields', () => {
|
|
5
27
|
it('extracts a single template variable from a stream pattern', () => {
|
|
@@ -62,6 +62,10 @@ function processStreamSink(item: unknown, exampleData: Record<string, unknown>)
|
|
|
62
62
|
return { streamPattern, streamId };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export function normalizeStreamPattern(pattern: string): string {
|
|
66
|
+
return pattern.replace(/\$\{[^}]+\}/g, '*');
|
|
67
|
+
}
|
|
68
|
+
|
|
65
69
|
export function extractStreamIdFields(pattern: string): string[] {
|
|
66
70
|
const fields: string[] = [];
|
|
67
71
|
const regex = /\$\{([^}]+)\}/g;
|
|
@@ -36,7 +36,7 @@ const EMPTY_EXTRACTED_MESSAGES: ExtractedMessages = {
|
|
|
36
36
|
commandSchemasByName: {},
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
function deduplicateMessages<T extends Message>(messages: T[]): T[] {
|
|
39
|
+
export function deduplicateMessages<T extends Message>(messages: T[]): T[] {
|
|
40
40
|
debugDedupe('Deduplicating %d messages', messages.length);
|
|
41
41
|
const uniqueMap = new Map<string, T>();
|
|
42
42
|
for (const message of messages) {
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { CommandMoment, ReactMoment, Scene, Step } from '@auto-engineer/narrative';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import type { MessageDefinition } from '../types';
|
|
4
|
+
import { findSiblingEventsForStream } from './sibling-events';
|
|
5
|
+
|
|
6
|
+
function makeCommandMoment(
|
|
7
|
+
name: string,
|
|
8
|
+
streamPattern: string,
|
|
9
|
+
whenCommand: string,
|
|
10
|
+
thenEvents: Array<{ text: string; exampleData: Record<string, unknown> }>,
|
|
11
|
+
): CommandMoment {
|
|
12
|
+
const steps: Step[] = [
|
|
13
|
+
{ keyword: 'When', text: whenCommand, docString: {} },
|
|
14
|
+
...thenEvents.map(
|
|
15
|
+
(e): Step => ({
|
|
16
|
+
keyword: 'Then',
|
|
17
|
+
text: e.text,
|
|
18
|
+
docString: e.exampleData,
|
|
19
|
+
}),
|
|
20
|
+
),
|
|
21
|
+
];
|
|
22
|
+
return {
|
|
23
|
+
type: 'command',
|
|
24
|
+
name,
|
|
25
|
+
client: { specs: [] },
|
|
26
|
+
server: {
|
|
27
|
+
description: '',
|
|
28
|
+
data: {
|
|
29
|
+
items: [
|
|
30
|
+
{
|
|
31
|
+
target: { type: 'Event', name: thenEvents[0]?.text ?? '' },
|
|
32
|
+
destination: { type: 'stream', pattern: streamPattern },
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
specs: [
|
|
37
|
+
{
|
|
38
|
+
type: 'gherkin',
|
|
39
|
+
feature: name,
|
|
40
|
+
rules: [
|
|
41
|
+
{
|
|
42
|
+
name: `${name} rule`,
|
|
43
|
+
examples: [{ name: `${name} example`, steps }],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('findSiblingEventsForStream', () => {
|
|
53
|
+
it('returns sibling events from moments sharing the same normalized stream', () => {
|
|
54
|
+
const startSession = makeCommandMoment('Start session', 'sessions-${id}', 'StartSession', [
|
|
55
|
+
{ text: 'SessionStarted', exampleData: { id: 's1', userId: 'u1' } },
|
|
56
|
+
]);
|
|
57
|
+
const addExercise = makeCommandMoment('Add exercise', 'sessions-${sessionId}', 'AddExercise', [
|
|
58
|
+
{ text: 'ExerciseAdded', exampleData: { sessionId: 's1', exercise: 'pushup' } },
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const scenes: Scene[] = [{ name: 'Gym', moments: [startSession, addExercise] }];
|
|
62
|
+
const messages: MessageDefinition[] = [
|
|
63
|
+
{
|
|
64
|
+
type: 'event',
|
|
65
|
+
name: 'SessionStarted',
|
|
66
|
+
fields: [
|
|
67
|
+
{ name: 'id', type: 'string', required: true },
|
|
68
|
+
{ name: 'userId', type: 'string', required: true },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'event',
|
|
73
|
+
name: 'ExerciseAdded',
|
|
74
|
+
fields: [
|
|
75
|
+
{ name: 'sessionId', type: 'string', required: true },
|
|
76
|
+
{ name: 'exercise', type: 'string', required: true },
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
{ type: 'command', name: 'StartSession', fields: [] },
|
|
80
|
+
{ type: 'command', name: 'AddExercise', fields: [] },
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const result = findSiblingEventsForStream(addExercise, scenes, messages);
|
|
84
|
+
|
|
85
|
+
expect(result.events).toEqual([
|
|
86
|
+
{
|
|
87
|
+
type: 'SessionStarted',
|
|
88
|
+
fields: [
|
|
89
|
+
{ name: 'id', tsType: 'string', required: true },
|
|
90
|
+
{ name: 'userId', tsType: 'string', required: true },
|
|
91
|
+
],
|
|
92
|
+
source: undefined,
|
|
93
|
+
sourceMomentName: 'Start session',
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
expect(result.exampleDataByType).toEqual({
|
|
97
|
+
SessionStarted: { id: 's1', userId: 'u1' },
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('excludes events from the current moment', () => {
|
|
102
|
+
const startSession = makeCommandMoment('Start session', 'sessions-${id}', 'StartSession', [
|
|
103
|
+
{ text: 'SessionStarted', exampleData: { id: 's1' } },
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const scenes: Scene[] = [{ name: 'Gym', moments: [startSession] }];
|
|
107
|
+
const messages: MessageDefinition[] = [
|
|
108
|
+
{ type: 'event', name: 'SessionStarted', fields: [{ name: 'id', type: 'string', required: true }] },
|
|
109
|
+
{ type: 'command', name: 'StartSession', fields: [] },
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const result = findSiblingEventsForStream(startSession, scenes, messages);
|
|
113
|
+
|
|
114
|
+
expect(result.events).toEqual([]);
|
|
115
|
+
expect(result.exampleDataByType).toEqual({});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('returns empty for moments on different streams', () => {
|
|
119
|
+
const startSession = makeCommandMoment('Start session', 'sessions-${id}', 'StartSession', [
|
|
120
|
+
{ text: 'SessionStarted', exampleData: { id: 's1' } },
|
|
121
|
+
]);
|
|
122
|
+
const createUser = makeCommandMoment('Create user', 'users-${id}', 'CreateUser', [
|
|
123
|
+
{ text: 'UserCreated', exampleData: { id: 'u1' } },
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
const scenes: Scene[] = [{ name: 'Scene1', moments: [startSession, createUser] }];
|
|
127
|
+
const messages: MessageDefinition[] = [
|
|
128
|
+
{ type: 'event', name: 'SessionStarted', fields: [{ name: 'id', type: 'string', required: true }] },
|
|
129
|
+
{ type: 'event', name: 'UserCreated', fields: [{ name: 'id', type: 'string', required: true }] },
|
|
130
|
+
{ type: 'command', name: 'StartSession', fields: [] },
|
|
131
|
+
{ type: 'command', name: 'CreateUser', fields: [] },
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const result = findSiblingEventsForStream(startSession, scenes, messages);
|
|
135
|
+
|
|
136
|
+
expect(result.events).toEqual([]);
|
|
137
|
+
expect(result.exampleDataByType).toEqual({});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns empty for moments with no stream', () => {
|
|
141
|
+
const noStreamMoment: CommandMoment = {
|
|
142
|
+
type: 'command',
|
|
143
|
+
name: 'No stream',
|
|
144
|
+
client: { specs: [] },
|
|
145
|
+
server: {
|
|
146
|
+
description: '',
|
|
147
|
+
specs: [],
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const scenes: Scene[] = [{ name: 'Scene', moments: [noStreamMoment] }];
|
|
152
|
+
|
|
153
|
+
const result = findSiblingEventsForStream(noStreamMoment, scenes, []);
|
|
154
|
+
|
|
155
|
+
expect(result.events).toEqual([]);
|
|
156
|
+
expect(result.exampleDataByType).toEqual({});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('does not include react moment then-commands as sibling events', () => {
|
|
160
|
+
const startSession = makeCommandMoment('Start session', 'sessions-${id}', 'StartSession', [
|
|
161
|
+
{ text: 'SessionStarted', exampleData: { id: 's1' } },
|
|
162
|
+
]);
|
|
163
|
+
const reactSteps: Step[] = [
|
|
164
|
+
{ keyword: 'When', text: 'SessionStarted', docString: { id: 's1' } },
|
|
165
|
+
{ keyword: 'Then', text: 'NotifyUser', docString: { userId: 'u1' } },
|
|
166
|
+
];
|
|
167
|
+
const reactMoment: ReactMoment = {
|
|
168
|
+
type: 'react',
|
|
169
|
+
name: 'React to session',
|
|
170
|
+
server: {
|
|
171
|
+
data: {
|
|
172
|
+
items: [
|
|
173
|
+
{
|
|
174
|
+
target: { type: 'Event', name: 'SessionStarted' },
|
|
175
|
+
destination: { type: 'stream', pattern: 'sessions-${id}' },
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
},
|
|
179
|
+
specs: [
|
|
180
|
+
{
|
|
181
|
+
type: 'gherkin',
|
|
182
|
+
feature: 'React',
|
|
183
|
+
rules: [
|
|
184
|
+
{
|
|
185
|
+
name: 'React rule',
|
|
186
|
+
examples: [{ name: 'React example', steps: reactSteps }],
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const scenes: Scene[] = [{ name: 'Gym', moments: [startSession, reactMoment] }];
|
|
195
|
+
const messages: MessageDefinition[] = [
|
|
196
|
+
{ type: 'event', name: 'SessionStarted', fields: [{ name: 'id', type: 'string', required: true }] },
|
|
197
|
+
{ type: 'command', name: 'StartSession', fields: [] },
|
|
198
|
+
{ type: 'command', name: 'NotifyUser', fields: [] },
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
const result = findSiblingEventsForStream(startSession, scenes, messages);
|
|
202
|
+
|
|
203
|
+
expect(result.events).toEqual([]);
|
|
204
|
+
expect(result.exampleDataByType).toEqual({});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Moment, Scene } from '@auto-engineer/narrative';
|
|
2
|
+
import type { Message, MessageDefinition } from '../types';
|
|
3
|
+
import { getStreamFromSink, normalizeStreamPattern } from './data-sink';
|
|
4
|
+
import { extractFieldsFromMessage } from './fields';
|
|
5
|
+
import { extractGwtSpecsFromMoment } from './step-converter';
|
|
6
|
+
|
|
7
|
+
export interface SiblingEventsResult {
|
|
8
|
+
events: Message[];
|
|
9
|
+
exampleDataByType: Record<string, Record<string, unknown>>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getProducedEvents(
|
|
13
|
+
moment: Moment,
|
|
14
|
+
allMessages: MessageDefinition[],
|
|
15
|
+
): Array<{ type: string; exampleData: Record<string, unknown> }> {
|
|
16
|
+
if (moment.type !== 'command') return [];
|
|
17
|
+
|
|
18
|
+
const gwtSpecs = extractGwtSpecsFromMoment(moment);
|
|
19
|
+
const produced: Array<{ type: string; exampleData: Record<string, unknown> }> = [];
|
|
20
|
+
const seen = new Set<string>();
|
|
21
|
+
|
|
22
|
+
for (const gwt of gwtSpecs) {
|
|
23
|
+
for (const t of gwt.then) {
|
|
24
|
+
if ('eventRef' in t && !seen.has(t.eventRef)) {
|
|
25
|
+
const isEvent = allMessages.some((m) => m.type === 'event' && m.name === t.eventRef);
|
|
26
|
+
if (isEvent) {
|
|
27
|
+
seen.add(t.eventRef);
|
|
28
|
+
produced.push({ type: t.eventRef, exampleData: t.exampleData ?? {} });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return produced;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function findSiblingEventsForStream(
|
|
38
|
+
currentMoment: Moment,
|
|
39
|
+
scenes: Scene[],
|
|
40
|
+
allMessages: MessageDefinition[],
|
|
41
|
+
): SiblingEventsResult {
|
|
42
|
+
const { streamPattern: currentStream } = getStreamFromSink(currentMoment);
|
|
43
|
+
if (!currentStream) return { events: [], exampleDataByType: {} };
|
|
44
|
+
|
|
45
|
+
const normalizedCurrent = normalizeStreamPattern(currentStream);
|
|
46
|
+
const events: Message[] = [];
|
|
47
|
+
const exampleDataByType: Record<string, Record<string, unknown>> = {};
|
|
48
|
+
const seen = new Set<string>();
|
|
49
|
+
|
|
50
|
+
for (const scene of scenes) {
|
|
51
|
+
for (const moment of scene.moments) {
|
|
52
|
+
if (moment.name === currentMoment.name) continue;
|
|
53
|
+
|
|
54
|
+
const { streamPattern } = getStreamFromSink(moment);
|
|
55
|
+
if (!streamPattern) continue;
|
|
56
|
+
if (normalizeStreamPattern(streamPattern) !== normalizedCurrent) continue;
|
|
57
|
+
|
|
58
|
+
const produced = getProducedEvents(moment, allMessages);
|
|
59
|
+
for (const { type, exampleData } of produced) {
|
|
60
|
+
if (seen.has(type)) continue;
|
|
61
|
+
seen.add(type);
|
|
62
|
+
|
|
63
|
+
const fields = extractFieldsFromMessage(type, 'event', allMessages);
|
|
64
|
+
events.push({
|
|
65
|
+
type,
|
|
66
|
+
fields,
|
|
67
|
+
source: undefined,
|
|
68
|
+
sourceMomentName: moment.name,
|
|
69
|
+
});
|
|
70
|
+
exampleDataByType[type] = exampleData;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { events, exampleDataByType };
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Ketchup Plan: Shared-stream sibling event awareness
|
|
2
|
+
|
|
3
|
+
## TODO
|
|
4
|
+
|
|
5
|
+
## DONE
|
|
6
|
+
|
|
7
|
+
- [x] B1: `normalizeStreamPattern()` utility in `data-sink.ts` [depends: none] (d27048c4)
|
|
8
|
+
- [x] B2: `findSiblingEventsForStream()` in new `sibling-events.ts` [depends: B1] (061af979)
|
|
9
|
+
- [x] B3: Wire `allStreamEvents` + `precedingSiblingEvents` into template data + evolve.ts uses it [depends: B1, B2] (3dad6640)
|
|
10
|
+
- [x] B4: decide.specs.ts uses `allStreamEvents` for given-resolution + state-ref fallback [depends: B3] (dbe3629b)
|
|
11
|
+
- [x] B5: decide.ts uses `allStreamEvents` for given-resolution + state-ref fallback [depends: B3] (14dab22e)
|
|
12
|
+
- [x] B6: state.ts sibling field hints + state-ref fix [depends: B3]
|
|
@@ -43,8 +43,10 @@ import {
|
|
|
43
43
|
sanitizeFieldType,
|
|
44
44
|
} from './extract';
|
|
45
45
|
import { getStreamFromSink } from './extract/data-sink';
|
|
46
|
+
import { deduplicateMessages } from './extract/messages';
|
|
46
47
|
import { buildEventIdFieldMap } from './extract/projection';
|
|
47
48
|
import { buildArgToStateFieldMap } from './extract/query';
|
|
49
|
+
import { findSiblingEventsForStream } from './extract/sibling-events';
|
|
48
50
|
import { normalizeMomentForTemplate } from './extract/slice-normalizer';
|
|
49
51
|
import { extractGwtSpecsFromMoment, type GwtResult } from './extract/step-converter';
|
|
50
52
|
import { buildKeepFieldNames, findDerivedDateInfo, isKeyTraceable, jsConstructorForType } from './templateHelpers';
|
|
@@ -643,6 +645,9 @@ async function prepareTemplateData(
|
|
|
643
645
|
allMessages?: MessageDefinition[],
|
|
644
646
|
integrations?: Model['integrations'],
|
|
645
647
|
scenes?: Scene[],
|
|
648
|
+
allStreamEvents?: Message[],
|
|
649
|
+
precedingSiblingEvents?: Message[],
|
|
650
|
+
siblingExampleData?: Record<string, Record<string, unknown>>,
|
|
646
651
|
): Promise<Record<string, unknown>> {
|
|
647
652
|
debug('Preparing template data for moment: %s (scene: %s)', slice.name, scene.name);
|
|
648
653
|
debug(' Commands: %d', commands.length);
|
|
@@ -671,7 +676,29 @@ async function prepareTemplateData(
|
|
|
671
676
|
const filteredCommands =
|
|
672
677
|
allowedForMoment.size > 0 ? uniqueCommands.filter((c) => allowedForMoment.has(c.type)) : uniqueCommands;
|
|
673
678
|
|
|
679
|
+
const uuidVars: string[] = (() => {
|
|
680
|
+
if (!streamPattern) return [];
|
|
681
|
+
const svRegex = /\$\{([^}]+)\}/g;
|
|
682
|
+
const vars: string[] = [];
|
|
683
|
+
let match: RegExpExecArray | null;
|
|
684
|
+
while ((match = svRegex.exec(streamPattern)) !== null) {
|
|
685
|
+
vars.push(match[1]);
|
|
686
|
+
}
|
|
687
|
+
if (vars.length === 0) return [];
|
|
688
|
+
const allFieldSets = filteredCommands.map((c) => new Set((c.fields ?? []).map((f) => f.name)));
|
|
689
|
+
return vars.filter((v) => !allFieldSets.some((fs) => fs.has(v)));
|
|
690
|
+
})();
|
|
691
|
+
|
|
692
|
+
const effectiveAllStreamEvents = allStreamEvents ?? events;
|
|
693
|
+
const effectivePrecedingSiblingEvents = precedingSiblingEvents ?? [];
|
|
694
|
+
const effectiveSiblingExampleData = siblingExampleData ?? {};
|
|
695
|
+
|
|
674
696
|
const eventImportGroups = groupEventImports({ currentMomentName: slice.name, currentSceneName: scene.name, events });
|
|
697
|
+
const allStreamEventImportGroups = groupEventImports({
|
|
698
|
+
currentMomentName: slice.name,
|
|
699
|
+
currentSceneName: scene.name,
|
|
700
|
+
events: effectiveAllStreamEvents,
|
|
701
|
+
});
|
|
675
702
|
const allEventTypesArray = getAllEventTypes(events);
|
|
676
703
|
const allEventTypes = createEventUnionType(events);
|
|
677
704
|
const selfImportedTypes = new Set(
|
|
@@ -750,6 +777,7 @@ async function prepareTemplateData(
|
|
|
750
777
|
momentName: slice.name,
|
|
751
778
|
slice: normalizedMoment,
|
|
752
779
|
stream: { pattern: streamPattern, id: streamId },
|
|
780
|
+
uuidVars,
|
|
753
781
|
commands: filteredCommands,
|
|
754
782
|
events,
|
|
755
783
|
states,
|
|
@@ -768,6 +796,10 @@ async function prepareTemplateData(
|
|
|
768
796
|
: undefined,
|
|
769
797
|
integrations,
|
|
770
798
|
eventImportGroups,
|
|
799
|
+
allStreamEvents: effectiveAllStreamEvents,
|
|
800
|
+
allStreamEventImportGroups,
|
|
801
|
+
precedingSiblingEvents: effectivePrecedingSiblingEvents,
|
|
802
|
+
siblingExampleData: effectiveSiblingExampleData,
|
|
771
803
|
allEventTypes,
|
|
772
804
|
allEventTypesArray,
|
|
773
805
|
localEvents,
|
|
@@ -912,6 +944,21 @@ async function generateFilesForMoment(
|
|
|
912
944
|
annotateEventSources(extracted.events, scenes, scene.name, slice.name, slice.type, sceneNameToDirName);
|
|
913
945
|
annotateCommandSources(extracted.commands, scenes, scene.name, slice.name, slice.type, sceneNameToDirName);
|
|
914
946
|
|
|
947
|
+
const { events: siblingEvents, exampleDataByType: siblingExampleData } = findSiblingEventsForStream(
|
|
948
|
+
slice,
|
|
949
|
+
scenes,
|
|
950
|
+
messages,
|
|
951
|
+
);
|
|
952
|
+
annotateEventSources(siblingEvents, scenes, scene.name, slice.name, slice.type, sceneNameToDirName);
|
|
953
|
+
const allStreamEvents = deduplicateMessages([...extracted.events, ...siblingEvents]);
|
|
954
|
+
|
|
955
|
+
const currentMomentIndex = scene.moments.findIndex((m) => m.name === slice.name);
|
|
956
|
+
const laterMomentNames = new Set(scene.moments.slice(currentMomentIndex + 1).map((m) => m.name));
|
|
957
|
+
const precedingSiblingEvents = siblingEvents.filter(
|
|
958
|
+
(e) =>
|
|
959
|
+
!(e.sourceSceneName === scene.name && e.sourceMomentName != null && laterMomentNames.has(e.sourceMomentName)),
|
|
960
|
+
);
|
|
961
|
+
|
|
915
962
|
let filteredTemplates = templates;
|
|
916
963
|
|
|
917
964
|
if (slice.type === 'command' && registeredCommands) {
|
|
@@ -958,6 +1005,9 @@ async function generateFilesForMoment(
|
|
|
958
1005
|
messages,
|
|
959
1006
|
integrations,
|
|
960
1007
|
scenes,
|
|
1008
|
+
allStreamEvents,
|
|
1009
|
+
precedingSiblingEvents,
|
|
1010
|
+
siblingExampleData,
|
|
961
1011
|
);
|
|
962
1012
|
|
|
963
1013
|
debugMoment(' Generating %d files from templates', filteredTemplates.length);
|
|
@@ -2793,4 +2793,129 @@ describe('spec.ts.ejs', () => {
|
|
|
2793
2793
|
"
|
|
2794
2794
|
`);
|
|
2795
2795
|
});
|
|
2796
|
+
|
|
2797
|
+
it('should use preceding sibling events for state-ref Given on shared stream', async () => {
|
|
2798
|
+
const spec: SpecsSchema = {
|
|
2799
|
+
variant: 'specs',
|
|
2800
|
+
scenes: [
|
|
2801
|
+
{
|
|
2802
|
+
name: 'Gym session',
|
|
2803
|
+
moments: [
|
|
2804
|
+
{
|
|
2805
|
+
type: 'command',
|
|
2806
|
+
name: 'Start session',
|
|
2807
|
+
client: { specs: [] },
|
|
2808
|
+
server: {
|
|
2809
|
+
description: '',
|
|
2810
|
+
data: {
|
|
2811
|
+
items: [
|
|
2812
|
+
{
|
|
2813
|
+
target: { type: 'Event', name: 'SessionStarted' },
|
|
2814
|
+
destination: { type: 'stream', pattern: 'sessions-${id}' },
|
|
2815
|
+
},
|
|
2816
|
+
],
|
|
2817
|
+
},
|
|
2818
|
+
specs: [
|
|
2819
|
+
{
|
|
2820
|
+
type: 'gherkin',
|
|
2821
|
+
feature: 'Start session',
|
|
2822
|
+
rules: [
|
|
2823
|
+
{
|
|
2824
|
+
name: 'Start session',
|
|
2825
|
+
examples: [
|
|
2826
|
+
{
|
|
2827
|
+
name: 'Start session example',
|
|
2828
|
+
steps: [
|
|
2829
|
+
{ keyword: 'When', text: 'StartSession', docString: { userId: 'u1' } },
|
|
2830
|
+
{ keyword: 'Then', text: 'SessionStarted', docString: { id: 's1', userId: 'u1' } },
|
|
2831
|
+
],
|
|
2832
|
+
},
|
|
2833
|
+
],
|
|
2834
|
+
},
|
|
2835
|
+
],
|
|
2836
|
+
},
|
|
2837
|
+
],
|
|
2838
|
+
},
|
|
2839
|
+
},
|
|
2840
|
+
{
|
|
2841
|
+
type: 'command',
|
|
2842
|
+
name: 'Complete session',
|
|
2843
|
+
client: { specs: [] },
|
|
2844
|
+
server: {
|
|
2845
|
+
description: '',
|
|
2846
|
+
data: {
|
|
2847
|
+
items: [
|
|
2848
|
+
{
|
|
2849
|
+
target: { type: 'Event', name: 'SessionCompleted' },
|
|
2850
|
+
destination: { type: 'stream', pattern: 'sessions-${sessionId}' },
|
|
2851
|
+
},
|
|
2852
|
+
],
|
|
2853
|
+
},
|
|
2854
|
+
specs: [
|
|
2855
|
+
{
|
|
2856
|
+
type: 'gherkin',
|
|
2857
|
+
feature: 'Complete session',
|
|
2858
|
+
rules: [
|
|
2859
|
+
{
|
|
2860
|
+
name: 'Complete session',
|
|
2861
|
+
examples: [
|
|
2862
|
+
{
|
|
2863
|
+
name: 'Complete session example',
|
|
2864
|
+
steps: [
|
|
2865
|
+
{ keyword: 'Given', text: 'ActiveSession', docString: { status: 'active' } },
|
|
2866
|
+
{ keyword: 'When', text: 'CompleteSession', docString: { sessionId: 's1' } },
|
|
2867
|
+
{
|
|
2868
|
+
keyword: 'Then',
|
|
2869
|
+
text: 'SessionCompleted',
|
|
2870
|
+
docString: { sessionId: 's1', completedAt: '2024-01-15' },
|
|
2871
|
+
},
|
|
2872
|
+
],
|
|
2873
|
+
},
|
|
2874
|
+
],
|
|
2875
|
+
},
|
|
2876
|
+
],
|
|
2877
|
+
},
|
|
2878
|
+
],
|
|
2879
|
+
},
|
|
2880
|
+
},
|
|
2881
|
+
],
|
|
2882
|
+
},
|
|
2883
|
+
],
|
|
2884
|
+
messages: [
|
|
2885
|
+
{ type: 'command', name: 'StartSession', fields: [{ name: 'userId', type: 'string', required: true }] },
|
|
2886
|
+
{ type: 'command', name: 'CompleteSession', fields: [{ name: 'sessionId', type: 'string', required: true }] },
|
|
2887
|
+
{
|
|
2888
|
+
type: 'event',
|
|
2889
|
+
name: 'SessionStarted',
|
|
2890
|
+
source: 'internal',
|
|
2891
|
+
fields: [
|
|
2892
|
+
{ name: 'id', type: 'string', required: true },
|
|
2893
|
+
{ name: 'userId', type: 'string', required: true },
|
|
2894
|
+
],
|
|
2895
|
+
},
|
|
2896
|
+
{
|
|
2897
|
+
type: 'event',
|
|
2898
|
+
name: 'SessionCompleted',
|
|
2899
|
+
source: 'internal',
|
|
2900
|
+
fields: [
|
|
2901
|
+
{ name: 'sessionId', type: 'string', required: true },
|
|
2902
|
+
{ name: 'completedAt', type: 'string', required: true },
|
|
2903
|
+
],
|
|
2904
|
+
},
|
|
2905
|
+
{ type: 'state', name: 'ActiveSession', fields: [{ name: 'status', type: 'string', required: true }] },
|
|
2906
|
+
],
|
|
2907
|
+
};
|
|
2908
|
+
|
|
2909
|
+
const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
|
|
2910
|
+
|
|
2911
|
+
const completeSpecs = plans.find(
|
|
2912
|
+
(p) => p.outputPath.includes('complete-session') && p.outputPath.endsWith('decide.specs.ts'),
|
|
2913
|
+
);
|
|
2914
|
+
const contents = completeSpecs?.contents ?? '';
|
|
2915
|
+
|
|
2916
|
+
expect(contents).toContain("import type { SessionStarted } from '../start-session/events';");
|
|
2917
|
+
expect(contents).toContain("type: 'SessionStarted'");
|
|
2918
|
+
expect(contents).toContain("id: 's1'");
|
|
2919
|
+
expect(contents).toContain("userId: 'u1'");
|
|
2920
|
+
});
|
|
2796
2921
|
});
|