@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.
Files changed (49) 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 +57 -0
  5. package/dist/src/codegen/extract/data-sink.d.ts +1 -0
  6. package/dist/src/codegen/extract/data-sink.d.ts.map +1 -1
  7. package/dist/src/codegen/extract/data-sink.js +3 -0
  8. package/dist/src/codegen/extract/data-sink.js.map +1 -1
  9. package/dist/src/codegen/extract/messages.d.ts +1 -0
  10. package/dist/src/codegen/extract/messages.d.ts.map +1 -1
  11. package/dist/src/codegen/extract/messages.js +1 -1
  12. package/dist/src/codegen/extract/messages.js.map +1 -1
  13. package/dist/src/codegen/extract/sibling-events.d.ts +8 -0
  14. package/dist/src/codegen/extract/sibling-events.d.ts.map +1 -0
  15. package/dist/src/codegen/extract/sibling-events.js +58 -0
  16. package/dist/src/codegen/extract/sibling-events.js.map +1 -0
  17. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  18. package/dist/src/codegen/scaffoldFromSchema.js +37 -2
  19. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  20. package/dist/src/codegen/templates/command/decide.specs.specs.ts +125 -0
  21. package/dist/src/codegen/templates/command/decide.specs.ts +258 -3
  22. package/dist/src/codegen/templates/command/decide.specs.ts.ejs +42 -14
  23. package/dist/src/codegen/templates/command/decide.ts.ejs +63 -16
  24. package/dist/src/codegen/templates/command/evolve.specs.ts +217 -39
  25. package/dist/src/codegen/templates/command/evolve.ts.ejs +7 -7
  26. package/dist/src/codegen/templates/command/handle.specs.ts +7 -4
  27. package/dist/src/codegen/templates/command/handle.ts.ejs +17 -5
  28. package/dist/src/codegen/templates/command/state.specs.ts +125 -0
  29. package/dist/src/codegen/templates/command/state.ts.ejs +10 -2
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/ketchup-plan.md +3 -3
  32. package/package.json +4 -4
  33. package/src/codegen/extract/data-sink.specs.ts +23 -1
  34. package/src/codegen/extract/data-sink.ts +4 -0
  35. package/src/codegen/extract/messages.ts +1 -1
  36. package/src/codegen/extract/sibling-events.specs.ts +206 -0
  37. package/src/codegen/extract/sibling-events.ts +76 -0
  38. package/src/codegen/ketchup-plan.md +12 -0
  39. package/src/codegen/scaffoldFromSchema.ts +50 -0
  40. package/src/codegen/templates/command/decide.specs.specs.ts +125 -0
  41. package/src/codegen/templates/command/decide.specs.ts +258 -3
  42. package/src/codegen/templates/command/decide.specs.ts.ejs +42 -14
  43. package/src/codegen/templates/command/decide.ts.ejs +63 -16
  44. package/src/codegen/templates/command/evolve.specs.ts +217 -39
  45. package/src/codegen/templates/command/evolve.ts.ejs +7 -7
  46. package/src/codegen/templates/command/handle.specs.ts +7 -4
  47. package/src/codegen/templates/command/handle.ts.ejs +17 -5
  48. package/src/codegen/templates/command/state.specs.ts +125 -0
  49. package/src/codegen/templates/command/state.ts.ejs +10 -2
package/ketchup-plan.md CHANGED
@@ -1,10 +1,10 @@
1
- # Ketchup Plan: G10Deduplicate field completeness event assertions for multi-example commands
1
+ # Ketchup Plan: G1 Fix Coordinate stream UUID with event field UUID
2
2
 
3
3
  ## TODO
4
4
 
5
- (none)
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.154.0",
36
- "@auto-engineer/message-bus": "1.154.0"
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.154.0"
47
+ "@auto-engineer/cli": "1.156.0"
48
48
  },
49
- "version": "1.154.0",
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
  });