@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
@@ -593,7 +593,7 @@ describe('decide.ts.ejs', () => {
593
593
  import type { ItemsSuggested } from './events';
594
594
  import type { Products } from '@auto-engineer/product-catalogue-integration';
595
595
 
596
- export const decide = (command: SuggestItems, _state: State, products?: Products): ItemsSuggested => {
596
+ export const decide = (command: SuggestItems, _state: State, context?: { products?: Products }): ItemsSuggested => {
597
597
  switch (command.type) {
598
598
  case 'SuggestItems': {
599
599
  /**
@@ -604,7 +604,7 @@ describe('decide.ts.ejs', () => {
604
604
  * You should:
605
605
  * - Validate the command input fields
606
606
  * - NEVER use \`as SomeType\` type assertions — not \`as any\`, not \`as EventType\`, no casts at all. Use typed variable declarations.
607
- * - Use \`products\` (integration result) to enrich or filter the output
607
+ * - Use \`context?.products\` (integration result) to enrich or filter the output
608
608
  * - If invalid, throw one of the following domain errors: \`IllegalStateError\`
609
609
  * ⚠️ Error constructors: IllegalStateError takes a string message
610
610
  * - If valid, return one or more events with the correct structure
@@ -613,7 +613,7 @@ describe('decide.ts.ejs', () => {
613
613
  * ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
614
614
  *
615
615
  * Integration result shape (Products):
616
- * products?.data = {
616
+ * context?.products?.data = {
617
617
  * products: Array<{
618
618
  * id: string;
619
619
  * name: string;
@@ -648,4 +648,259 @@ describe('decide.ts.ejs', () => {
648
648
  "
649
649
  `);
650
650
  });
651
+
652
+ it('should generate context parameter and uuid assignment for stream uuid vars that are event fields', async () => {
653
+ const spec: SpecsSchema = {
654
+ variant: 'specs',
655
+ scenes: [
656
+ {
657
+ name: 'Fitness tracker',
658
+ moments: [
659
+ {
660
+ type: 'command',
661
+ name: 'Log workout',
662
+ stream: 'workouts-${workoutId}',
663
+ client: { specs: [] },
664
+ server: {
665
+ description: 'test',
666
+ specs: [
667
+ {
668
+ type: 'gherkin',
669
+ feature: 'Log workout',
670
+ rules: [
671
+ {
672
+ name: 'Should log workout',
673
+ examples: [
674
+ {
675
+ name: 'Workout logged',
676
+ steps: [
677
+ {
678
+ keyword: 'When',
679
+ text: 'LogWorkout',
680
+ docString: { exercise: 'squat', reps: 10 },
681
+ },
682
+ {
683
+ keyword: 'Then',
684
+ text: 'WorkoutLogged',
685
+ docString: { workoutId: 'wk-1', exercise: 'squat', reps: 10 },
686
+ },
687
+ ],
688
+ },
689
+ ],
690
+ },
691
+ ],
692
+ },
693
+ ],
694
+ data: {
695
+ items: [
696
+ {
697
+ target: { type: 'Event', name: 'WorkoutLogged' },
698
+ destination: { type: 'stream', pattern: 'workouts-${workoutId}' },
699
+ },
700
+ ],
701
+ },
702
+ },
703
+ },
704
+ ],
705
+ },
706
+ ],
707
+ messages: [
708
+ {
709
+ type: 'command',
710
+ name: 'LogWorkout',
711
+ fields: [
712
+ { name: 'exercise', type: 'string', required: true },
713
+ { name: 'reps', type: 'number', required: true },
714
+ ],
715
+ },
716
+ {
717
+ type: 'event',
718
+ name: 'WorkoutLogged',
719
+ source: 'internal',
720
+ fields: [
721
+ { name: 'workoutId', type: 'string', required: true },
722
+ { name: 'exercise', type: 'string', required: true },
723
+ { name: 'reps', type: 'number', required: true },
724
+ ],
725
+ },
726
+ ],
727
+ };
728
+
729
+ const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
730
+ const decideFile = plans.find((p) => p.outputPath.endsWith('decide.ts'));
731
+
732
+ expect(decideFile?.contents).toMatchInlineSnapshot(`
733
+ "import { randomUUID } from 'node:crypto';
734
+ import { IllegalStateError } from '@event-driven-io/emmett';
735
+ import type { State } from './state';
736
+ import type { LogWorkout } from './commands';
737
+ import type { WorkoutLogged } from './events';
738
+
739
+ export const decide = (
740
+ command: LogWorkout,
741
+ _state: State,
742
+ context?: { generated?: { workoutId: string } },
743
+ ): WorkoutLogged => {
744
+ switch (command.type) {
745
+ case 'LogWorkout': {
746
+ /**
747
+ * ## IMPLEMENTATION INSTRUCTIONS ##
748
+ *
749
+ * This command can directly emit one or more events based on the input.
750
+ *
751
+ * You should:
752
+ * - Validate the command input fields
753
+ * - NEVER use \`as SomeType\` type assertions — not \`as any\`, not \`as EventType\`, no casts at all. Use typed variable declarations.
754
+ * - If invalid, throw one of the following domain errors: \`IllegalStateError\`
755
+ * ⚠️ Error constructors: IllegalStateError takes a string message
756
+ * - If valid, return one or more events with the correct structure
757
+ *
758
+ * - Only destructure/reference command.data fields that exist in the command type (imported from ./commands). Do NOT access fields not declared in the type, even if example data includes them.
759
+ * ⚠️ Only read from inputs — never mutate them. \`evolve.ts\` handles state updates.
760
+
761
+ * Business rules:
762
+ * - Should log workout
763
+ */
764
+
765
+ const workoutId = context?.generated?.workoutId ?? randomUUID();
766
+
767
+ // IMPLEMENT: Use a typed variable to prevent type widening:
768
+ // const result: WorkoutLogged = {
769
+ // type: 'WorkoutLogged',
770
+ // data: { ...command.data, workoutId },
771
+ // };
772
+ // return result;
773
+
774
+ throw new IllegalStateError('Not yet implemented: ' + command.type);
775
+ }
776
+ default:
777
+ throw new IllegalStateError('Unexpected command type');
778
+ }
779
+ };
780
+ "
781
+ `);
782
+ });
783
+
784
+ it('should say Inspect _state for state-ref Given with preceding siblings', async () => {
785
+ const spec: SpecsSchema = {
786
+ variant: 'specs',
787
+ scenes: [
788
+ {
789
+ name: 'Gym session',
790
+ moments: [
791
+ {
792
+ type: 'command',
793
+ name: 'Start session',
794
+ client: { specs: [] },
795
+ server: {
796
+ description: '',
797
+ data: {
798
+ items: [
799
+ {
800
+ target: { type: 'Event', name: 'SessionStarted' },
801
+ destination: { type: 'stream', pattern: 'sessions-${id}' },
802
+ },
803
+ ],
804
+ },
805
+ specs: [
806
+ {
807
+ type: 'gherkin',
808
+ feature: 'Start',
809
+ rules: [
810
+ {
811
+ name: 'Start',
812
+ examples: [
813
+ {
814
+ name: 'Start',
815
+ steps: [
816
+ { keyword: 'When', text: 'StartSession', docString: { userId: 'u1' } },
817
+ { keyword: 'Then', text: 'SessionStarted', docString: { id: 's1', userId: 'u1' } },
818
+ ],
819
+ },
820
+ ],
821
+ },
822
+ ],
823
+ },
824
+ ],
825
+ },
826
+ },
827
+ {
828
+ type: 'command',
829
+ name: 'Complete session',
830
+ client: { specs: [] },
831
+ server: {
832
+ description: '',
833
+ data: {
834
+ items: [
835
+ {
836
+ target: { type: 'Event', name: 'SessionCompleted' },
837
+ destination: { type: 'stream', pattern: 'sessions-${sessionId}' },
838
+ },
839
+ ],
840
+ },
841
+ specs: [
842
+ {
843
+ type: 'gherkin',
844
+ feature: 'Complete',
845
+ rules: [
846
+ {
847
+ name: 'Complete',
848
+ examples: [
849
+ {
850
+ name: 'Complete',
851
+ steps: [
852
+ { keyword: 'Given', text: 'ActiveSession', docString: { status: 'active' } },
853
+ { keyword: 'When', text: 'CompleteSession', docString: { sessionId: 's1' } },
854
+ {
855
+ keyword: 'Then',
856
+ text: 'SessionCompleted',
857
+ docString: { sessionId: 's1', completedAt: '2024-01-15' },
858
+ },
859
+ ],
860
+ },
861
+ ],
862
+ },
863
+ ],
864
+ },
865
+ ],
866
+ },
867
+ },
868
+ ],
869
+ },
870
+ ],
871
+ messages: [
872
+ { type: 'command', name: 'StartSession', fields: [{ name: 'userId', type: 'string', required: true }] },
873
+ { type: 'command', name: 'CompleteSession', fields: [{ name: 'sessionId', type: 'string', required: true }] },
874
+ {
875
+ type: 'event',
876
+ name: 'SessionStarted',
877
+ source: 'internal',
878
+ fields: [
879
+ { name: 'id', type: 'string', required: true },
880
+ { name: 'userId', type: 'string', required: true },
881
+ ],
882
+ },
883
+ {
884
+ type: 'event',
885
+ name: 'SessionCompleted',
886
+ source: 'internal',
887
+ fields: [
888
+ { name: 'sessionId', type: 'string', required: true },
889
+ { name: 'completedAt', type: 'string', required: true },
890
+ ],
891
+ },
892
+ { type: 'state', name: 'ActiveSession', fields: [{ name: 'status', type: 'string', required: true }] },
893
+ ],
894
+ };
895
+
896
+ const { plans } = await generateScaffoldFilePlans(spec.scenes, spec.messages, undefined, 'src/domain/narratives');
897
+ const decideFile = plans.find(
898
+ (p) => p.outputPath.includes('complete-session') && p.outputPath.endsWith('decide.ts'),
899
+ );
900
+ const contents = decideFile?.contents ?? '';
901
+
902
+ expect(contents).toContain('requires evaluating prior state');
903
+ expect(contents).toContain('Inspect the current domain');
904
+ expect(contents).not.toContain('succeed from initial state');
905
+ });
651
906
  });
@@ -1,6 +1,7 @@
1
1
  <%
2
2
  const allEvents = [];
3
3
  const ruleGroups = new Map();
4
+ const _stateRefGivenSeen = new Set();
4
5
  for (const commandName in gwtMapping) {
5
6
  const cases = gwtMapping[commandName];
6
7
  for (const gwt of cases) {
@@ -12,8 +13,17 @@ for (const commandName in gwtMapping) {
12
13
  if (gwt.given && gwt.given.length) {
13
14
  for (const g of gwt.given) {
14
15
  if (g.eventRef) {
15
- const event = events.find(e => e.type === g.eventRef);
16
- if (event) allEvents.push(event);
16
+ const event = allStreamEvents.find(e => e.type === g.eventRef);
17
+ if (event) {
18
+ allEvents.push(event);
19
+ } else if (precedingSiblingEvents.length > 0 && !_stateRefGivenSeen.has(g.eventRef)) {
20
+ _stateRefGivenSeen.add(g.eventRef);
21
+ for (const se of precedingSiblingEvents) {
22
+ if (!allEvents.some(e => e.type === se.type)) {
23
+ allEvents.push(se);
24
+ }
25
+ }
26
+ }
17
27
  }
18
28
  }
19
29
  }
@@ -33,7 +43,7 @@ const testEventsByPath = new Map();
33
43
 
34
44
  for (const event of allEvents) {
35
45
  if (!event.type) continue;
36
- const importGroup = eventImportGroups.find(group =>
46
+ const importGroup = allStreamEventImportGroups.find(group =>
37
47
  group.eventTypes.includes(event.type)
38
48
  );
39
49
 
@@ -64,7 +74,7 @@ for (const commandName in gwtMapping) {
64
74
  const eventResults = gwt.then.filter(t => 'eventRef' in t);
65
75
  const errorResult = gwt.then.find(t => 'errorType' in t);
66
76
  if (errorResult) continue;
67
- const givenEventRefs = (gwt.given || []).filter(g => events.some(ev => ev.type === g.eventRef));
77
+ const givenEventRefs = (gwt.given || []).filter(g => allStreamEvents.some(ev => ev.type === g.eventRef));
68
78
  for (const e of eventResults) {
69
79
  const dedupeKey = `${commandName}::${e.eventRef}`;
70
80
  if (seenCommandEvents.has(dedupeKey)) continue;
@@ -116,12 +126,22 @@ describe('<%= escapeJsString(ruleDescription) %>', () => {
116
126
  %>
117
127
  it('<%= escapeJsString(testDescription) %>', () => {
118
128
  given([
119
- <%_ const givenEvents = (gwt.given || []).filter(g => events.some(e => e.type === g.eventRef)); _%>
129
+ <%_
130
+ let givenEvents = (gwt.given || []).filter(g => allStreamEvents.some(e => e.type === g.eventRef));
131
+ const hasStateRefGiven = (gwt.given || []).some(g => !allStreamEvents.some(e => e.type === g.eventRef));
132
+ if (givenEvents.length === 0 && hasStateRefGiven && precedingSiblingEvents.length > 0) {
133
+ givenEvents = precedingSiblingEvents;
134
+ }
135
+ _%>
120
136
  <%_ if (givenEvents.length) { _%>
121
- <%- givenEvents.map(g => `{
122
- type: '${g.eventRef}',
123
- data: ${formatDataObject(g.exampleData, events.find(e => e.type === g.eventRef))}
124
- }`).join(',\n ') %>
137
+ <%- givenEvents.map(g => {
138
+ const schema = allStreamEvents.find(e => e.type === (g.eventRef || g.type));
139
+ const data = g.exampleData || siblingExampleData[g.type] || {};
140
+ return `{
141
+ type: '${g.eventRef || g.type}',
142
+ data: ${formatDataObject(data, schema)}
143
+ }`;
144
+ }).join(',\n ') %>
125
145
  <%_ } _%>
126
146
  ])
127
147
  .when({
@@ -171,7 +191,11 @@ describe('field completeness', () => {
171
191
  const firstScenario = scenarios[0];
172
192
  const gwt = firstScenario.gwt;
173
193
  const schema = commandSchemasByName[cmdName];
174
- const givenEvents = (gwt.given || []).filter(g => events.some(e => e.type === g.eventRef));
194
+ let givenEvents = (gwt.given || []).filter(g => allStreamEvents.some(e => e.type === g.eventRef));
195
+ const hasStateRefGiven = (gwt.given || []).some(g => !allStreamEvents.some(e => e.type === g.eventRef));
196
+ if (givenEvents.length === 0 && hasStateRefGiven && precedingSiblingEvents.length > 0) {
197
+ givenEvents = precedingSiblingEvents;
198
+ }
175
199
  const example = gwt.when;
176
200
  const cmdFieldNames = firstScenario.cmdFieldNames;
177
201
  const filteredCmdData = Object.fromEntries(
@@ -181,10 +205,14 @@ describe('field completeness', () => {
181
205
  it('should include all required fields in <%= scenarios.map(s => s.eventRef).join(', ') %>', () => {
182
206
  <%_ if (givenEvents.length) { _%>
183
207
  const state = [
184
- <%- givenEvents.map(g => `{
185
- type: '${g.eventRef}' as const,
186
- data: ${formatDataObject(g.exampleData, events.find(e => e.type === g.eventRef))}
187
- }`).join(',\n ') %>
208
+ <%- givenEvents.map(g => {
209
+ const schema = allStreamEvents.find(e => e.type === (g.eventRef || g.type));
210
+ const data = g.exampleData || siblingExampleData[g.type] || {};
211
+ return `{
212
+ type: '${g.eventRef || g.type}' as const,
213
+ data: ${formatDataObject(data, schema)}
214
+ }`;
215
+ }).join(',\n ') %>
188
216
  ].reduce((s, e) => evolve(s, e), initialState());
189
217
  <%_ } else { _%>
190
218
  const state = initialState();
@@ -1,3 +1,24 @@
1
+ <%
2
+ const needsRandomUuidImport = (() => {
3
+ for (const cmdName of Object.keys(gwtMapping)) {
4
+ const cmdFieldSet = new Set((commandSchemasByName?.[cmdName]?.fields || []).map(f => f.name));
5
+ const scenarios = gwtMapping?.[cmdName] ?? [];
6
+ for (const v of (uuidVars || [])) {
7
+ if (cmdFieldSet.has(v)) continue;
8
+ for (const scenario of scenarios) {
9
+ for (const t of scenario.then.filter(t => 'eventRef' in t)) {
10
+ const eventMsg = (messages || []).find(m => m.name === t.eventRef && m.type === 'event');
11
+ if ((eventMsg?.fields || []).some(f => f.name === v)) return true;
12
+ }
13
+ }
14
+ }
15
+ }
16
+ return false;
17
+ })();
18
+ -%>
19
+ <% if (needsRandomUuidImport) { -%>
20
+ import { randomUUID } from 'node:crypto';
21
+ <% } -%>
1
22
  import {
2
23
  IllegalStateError<% if (usedErrors.includes('ValidationError')) { %>, ValidationError<% } %><% if (usedErrors.includes('NotFoundError')) { %>, NotFoundError<% } %>
3
24
  } from '@event-driven-io/emmett';
@@ -16,6 +37,19 @@ const integrationReturnSystem = integration?._withState?.origin?.systems?.[0];
16
37
  const integrationReturnImportSource = integrations?.find(i => i.name === integrationReturnSystem)?.source;
17
38
  const integrationReturnFields = messages?.find(m => m.name === integrationReturnType && m.type === 'state')?.fields ?? [];
18
39
 
40
+ const hasContextGenerated = (uuidVars || []).length > 0;
41
+ const hasContextIntegration = !!integrationReturnType;
42
+ const hasContext = hasContextGenerated || hasContextIntegration;
43
+ const contextTypeParts = [];
44
+ if (hasContextGenerated) {
45
+ const generatedFields = (uuidVars || []).map(v => `${v}: string`).join('; ');
46
+ contextTypeParts.push(`generated?: { ${generatedFields} }`);
47
+ }
48
+ if (hasContextIntegration) {
49
+ contextTypeParts.push(`${camelCase(integrationReturnType)}?: ${integrationReturnType}`);
50
+ }
51
+ const contextTypeStr = contextTypeParts.join('; ');
52
+
19
53
  function formatFieldDocLine(field) {
20
54
  const optional = field.required ? '' : '?';
21
55
  const name = `${field.name}${optional}`;
@@ -39,7 +73,7 @@ function formatFieldDocLine(field) {
39
73
 
40
74
  export const decide = (
41
75
  command: <%= Object.keys(gwtMapping).map(pascalCase).join(' | ') %>,
42
- _state: State<%= integrationReturnType ? `,\n ${camelCase(integrationReturnType)}?: ${integrationReturnType}` : '' %>
76
+ _state: State<%= hasContext ? `,\ncontext?: { ${contextTypeStr} }` : '' %>
43
77
  ): <%= uniqueEventTypes.length === 0
44
78
  ? 'never'
45
79
  : uniqueEventTypes.length === 1
@@ -49,11 +83,13 @@ switch (command.type) {
49
83
  <% for (const command of Object.keys(gwtMapping)) {
50
84
  const scenarios = gwtMapping?.[command] ?? [];
51
85
  const hasGivenEvents = scenarios.some(s =>
52
- (s.given || []).some(g => events.some(e => e.type === g.eventRef))
86
+ (s.given || []).some(g => allStreamEvents.some(e => e.type === g.eventRef))
53
87
  );
54
88
  const hasGivenStates = scenarios.some(s =>
55
- (s.given || []).some(g => !events.some(e => e.type === g.eventRef))
89
+ (s.given || []).some(g => !allStreamEvents.some(e => e.type === g.eventRef))
56
90
  );
91
+ const hasPrecedingSiblings = precedingSiblingEvents.length > 0;
92
+ const effectiveHasGivenEvents = hasGivenEvents || (hasGivenStates && hasPrecedingSiblings);
57
93
  const fallbackEvents = scenarios.flatMap(s => s.then.filter(t => 'eventRef' in t));
58
94
  const fallbackEventTypes = [...new Set(fallbackEvents.map(e => e.eventRef))];
59
95
  -%>
@@ -61,22 +97,22 @@ case '<%= command %>': {
61
97
  /**
62
98
  * ## IMPLEMENTATION INSTRUCTIONS ##
63
99
  *
64
- * This command <%= hasGivenEvents ? 'requires evaluating prior state to determine if it can proceed' : 'can directly emit one or more events based on the input' %>.
100
+ * This command <%= effectiveHasGivenEvents ? 'requires evaluating prior state to determine if it can proceed' : 'can directly emit one or more events based on the input' %>.
65
101
  *
66
102
  * You should:
67
103
  * - Validate the command input fields
68
- <% if (hasGivenEvents) { -%>
104
+ <% if (effectiveHasGivenEvents) { -%>
69
105
  * - Inspect the current domain `_state` to determine if the command is allowed
70
106
  <% } -%>
71
107
  * - NEVER use `as SomeType` type assertions — not `as any`, not `as EventType`, no casts at all. Use typed variable declarations.
72
- <% if (hasGivenEvents) { -%>
108
+ <% if (effectiveHasGivenEvents) { -%>
73
109
  * - If State is a discriminated union, narrow with the discriminant: `if (_state.status !== 'active') throw new IllegalStateError('...');`
74
110
  * After narrowing, access variant fields directly — TypeScript infers the correct type.
75
111
  <% } else if (hasGivenStates) { -%>
76
112
  * - The test uses given([]) with initialState(). Do not narrow or guard on _state — the command should succeed from initial state.
77
113
  <% } -%>
78
114
  <% if (integrationReturnType) { -%>
79
- * - Use `<%= camelCase(integrationReturnType) %>` (integration result) to enrich or filter the output
115
+ * - Use `context?.<%= camelCase(integrationReturnType) %>` (integration result) to enrich or filter the output
80
116
  <% } -%>
81
117
  * - If invalid, throw one of the following domain errors: `IllegalStateError`<% if (usedErrors.includes('ValidationError')) { %>, `ValidationError`<% } %><% if (usedErrors.includes('NotFoundError')) { %>, `NotFoundError`<% } %>
82
118
  * ⚠️ Error constructors: IllegalStateError takes a string message<% if (usedErrors.includes('ValidationError')) { %>, ValidationError takes a string message<% } %><% if (usedErrors.includes('NotFoundError')) { %>, NotFoundError takes { id, type, message? }<% } %>
@@ -87,7 +123,7 @@ case '<%= command %>': {
87
123
  <% if (integrationReturnFields.length > 0) { -%>
88
124
  *
89
125
  * Integration result shape (<%= integrationReturnType %>):
90
- * <%= camelCase(integrationReturnType) %>?.data = {
126
+ * context?.<%= camelCase(integrationReturnType) %>?.data = {
91
127
  <%= integrationReturnFields.flatMap(formatFieldDocLine).join('\n') %>
92
128
  * }
93
129
  <% } -%>
@@ -123,28 +159,39 @@ for (const scenario of scenarios) {
123
159
  }
124
160
  }
125
161
  }
162
+ const uuidAssignments = (uuidVars || []).filter(v => nonCommandFields.some(f => f.name === v));
163
+ const filteredNonCommandFields = nonCommandFields.filter(f => !(uuidVars || []).includes(f.name));
126
164
  const eventResults = scenarios.flatMap(s => s.then.filter(t => 'eventRef' in t));
127
- const givenEventsForCmd = scenarios.flatMap(s =>
128
- (s.given || []).filter(g => events.some(e => e.type === g.eventRef))
165
+ let givenEventsForCmd = scenarios.flatMap(s =>
166
+ (s.given || []).filter(g => allStreamEvents.some(e => e.type === g.eventRef))
129
167
  );
168
+ if (givenEventsForCmd.length === 0 && hasGivenStates && hasPrecedingSiblings) {
169
+ givenEventsForCmd = precedingSiblingEvents.map(e => ({
170
+ eventRef: e.type,
171
+ exampleData: siblingExampleData[e.type] || {},
172
+ }));
173
+ }
130
174
  const { fields: derivedDateFieldNames } = findDerivedDateInfo(eventResults, cmdFieldSet, givenEventsForCmd);
131
175
  const derivedDateFields = new Set(derivedDateFieldNames);
132
176
  const keepFieldNamesSet = buildKeepFieldNames(eventResults, cmdFieldSet, derivedDateFieldNames, givenEventsForCmd);
133
177
  -%>
134
- <% if (nonCommandFields.length > 0) { -%>
178
+ <% for (const v of uuidAssignments) { -%>
179
+ const <%= v %> = context?.generated?.<%= v %> ?? randomUUID();
180
+ <% } -%>
181
+ <% if (filteredNonCommandFields.length > 0) { -%>
135
182
  // ⚠️ REQUIRED: Your return value MUST include ALL fields defined in the event type.
136
183
  // Tests use partial matching and may not check every field — passing tests does NOT mean all fields are present.
137
184
  // Do NOT use 'as <%= fallbackEventTypes[0] ? pascalCase(fallbackEventTypes[0]) : 'EventType' %>' to silence missing fields.
138
185
  //
139
186
  // Fields from command input → use ...command.data or command.data.<fieldName>
140
187
  // Fields NOT in command input → produce dynamically (never hardcode):
141
- <% for (const f of nonCommandFields) {
188
+ <% for (const f of filteredNonCommandFields) {
142
189
  const isDerivedDate = derivedDateFields.has(f.name);
143
190
  const isAssertedByTest = keepFieldNamesSet.has(f.name);
144
191
  -%>
145
192
  <% if (isDerivedDate) { -%>
146
193
  // <%= f.name %>: <%= f.type || f.tsType %> — derive from command.metadata?.now (convert Date to ISO string, e.g., .toISOString().substring(0, 10))
147
- <% } else if (hasGivenEvents) { -%>
194
+ <% } else if (effectiveHasGivenEvents) { -%>
148
195
  // <%= f.name %>: <%= f.type || f.tsType %> — derive from _state, generate at runtime (e.g., crypto.randomUUID()), or compute from command.data
149
196
  <% } else if (!isAssertedByTest) { -%>
150
197
  // <%= f.name %>: <%= f.type || f.tsType %> — not asserted by test; produce a valid runtime value (e.g., crypto.randomUUID() for IDs, '' for other strings)
@@ -152,14 +199,14 @@ const keepFieldNamesSet = buildKeepFieldNames(eventResults, cmdFieldSet, derived
152
199
  // <%= f.name %>: <%= f.type || f.tsType %> — generate at runtime (e.g., crypto.randomUUID()), or compute from command.data
153
200
  <% } -%>
154
201
  <% } -%>
155
- <% } else { -%>
202
+ <% } else if (uuidAssignments.length === 0) { -%>
156
203
  // All event fields come from command input — use ...command.data to pass them through.
157
204
  <% } -%>
158
205
 
159
206
  // IMPLEMENT: Use a typed variable to prevent type widening:
160
207
  // const result: <%= pascalCase(fallbackEventTypes[0] ?? 'TODO_EVENT_TYPE') %> = {
161
208
  // type: '<%= fallbackEventTypes[0] %>',
162
- // data: { ...command.data<%= nonCommandFields.length > 0 ? `, /* + produce: ${nonCommandFields.map(f => f.name).join(', ')} */` : '' %> },
209
+ // data: { ...command.data<%= uuidAssignments.length > 0 ? `, ${uuidAssignments.join(', ')}` : '' %><%= filteredNonCommandFields.length > 0 ? `, /* + produce: ${filteredNonCommandFields.map(f => f.name).join(', ')} */` : '' %> },
163
210
  // };
164
211
  // return result;
165
212
 
@@ -169,4 +216,4 @@ throw new IllegalStateError('Not yet implemented: ' + command.type);
169
216
  default:
170
217
  throw new IllegalStateError('Unexpected command type');
171
218
  }
172
- };
219
+ };