@auto-engineer/server-generator-apollo-emmett 1.146.0 → 1.148.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 (43) 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 +68 -0
  5. package/dist/src/codegen/extract/messages.d.ts.map +1 -1
  6. package/dist/src/codegen/extract/messages.js +4 -12
  7. package/dist/src/codegen/extract/messages.js.map +1 -1
  8. package/dist/src/codegen/scaffoldFromSchema.d.ts +1 -11
  9. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  10. package/dist/src/codegen/scaffoldFromSchema.js +7 -38
  11. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  12. package/dist/src/codegen/templateHelpers.d.ts +11 -0
  13. package/dist/src/codegen/templateHelpers.d.ts.map +1 -0
  14. package/dist/src/codegen/templateHelpers.js +47 -0
  15. package/dist/src/codegen/templateHelpers.js.map +1 -0
  16. package/dist/src/codegen/templates/command/decide.specs.specs.ts +662 -0
  17. package/dist/src/codegen/templates/command/decide.specs.ts +1 -10
  18. package/dist/src/codegen/templates/command/decide.specs.ts.ejs +3 -47
  19. package/dist/src/codegen/templates/command/decide.ts.ejs +31 -2
  20. package/dist/src/codegen/templates/command/handle.specs.ts +0 -201
  21. package/dist/src/codegen/templates/command/handle.ts.ejs +0 -34
  22. package/dist/src/codegen/templates/command/state.specs.ts +89 -0
  23. package/dist/src/codegen/templates/command/state.ts.ejs +20 -0
  24. package/dist/src/codegen/templates/query/events.specs.ts +133 -0
  25. package/dist/tsconfig.tsbuildinfo +1 -1
  26. package/ketchup-plan.md +5 -4
  27. package/package.json +4 -4
  28. package/src/codegen/extract/messages.specs.ts +9 -8
  29. package/src/codegen/extract/messages.ts +4 -12
  30. package/src/codegen/extract/states.specs.ts +53 -0
  31. package/src/codegen/scaffoldFromSchema.ts +7 -58
  32. package/src/codegen/templateHelpers.specs.ts +98 -0
  33. package/src/codegen/templateHelpers.ts +58 -0
  34. package/src/codegen/templates/command/decide.specs.specs.ts +662 -0
  35. package/src/codegen/templates/command/decide.specs.ts +1 -10
  36. package/src/codegen/templates/command/decide.specs.ts.ejs +3 -47
  37. package/src/codegen/templates/command/decide.ts.ejs +31 -2
  38. package/src/codegen/templates/command/handle.specs.ts +0 -201
  39. package/src/codegen/templates/command/handle.ts.ejs +0 -34
  40. package/src/codegen/templates/command/state.specs.ts +89 -0
  41. package/src/codegen/templates/command/state.ts.ejs +20 -0
  42. package/src/codegen/templates/query/events.specs.ts +133 -0
  43. package/src/codegen/buildCrossSceneGivens.specs.ts +0 -263
package/ketchup-plan.md CHANGED
@@ -1,9 +1,10 @@
1
- # Ketchup Plan: Cross-Scene Given Events in handle.ts
1
+ # Ketchup Plan: G4 Fix remaining decide.ts implementer failures
2
2
 
3
3
  ## TODO
4
4
 
5
5
  ## DONE
6
6
 
7
- - [x] Burst 1: Compute `crossSceneGivens` in template data (d5786a13)
8
- - [x] Burst 2: Generate cross-scene handler in handle.ts.ejs (f919ab8b)
9
- - [x] Burst 3: End-to-end verification all 299 tests pass, existing snapshots unchanged
7
+ - [x] B1: Extract shared template helpers and wire into template data [depends: none] (78f159a4)
8
+ - [x] B3: Add hasGivenStates + state context instruction to decide.ts.ejs [depends: B1] (42f66c71)
9
+ - [x] B4: Add context-aware nonCommandField classification to decide.ts.ejs [depends: B3] (65212e33)
10
+ - [x] B5: Add Given state ref hints to state.ts.ejs [depends: none]
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.146.0",
36
- "@auto-engineer/message-bus": "1.146.0"
35
+ "@auto-engineer/narrative": "1.148.0",
36
+ "@auto-engineer/message-bus": "1.148.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.146.0"
47
+ "@auto-engineer/cli": "1.148.0"
48
48
  },
49
- "version": "1.146.0",
49
+ "version": "1.148.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",
@@ -190,7 +190,7 @@ describe('extractMessagesFromSpecs (react slice)', () => {
190
190
  });
191
191
 
192
192
  describe('extractMessagesFromSpecs (command slice)', () => {
193
- it('should create state-as-event with source then for Given-step state refs', () => {
193
+ it('should extract Given-step state refs into states array, not events', () => {
194
194
  const slice: Moment = {
195
195
  type: 'command',
196
196
  name: 'book barber appointment',
@@ -263,23 +263,24 @@ describe('extractMessagesFromSpecs (command slice)', () => {
263
263
 
264
264
  expect(result.events).toEqual([
265
265
  {
266
- type: 'CustomerAppointments',
266
+ type: 'BarberAppointmentBooked',
267
267
  fields: [
268
268
  { name: 'customerId', tsType: 'string', required: true },
269
- { name: 'appointments', tsType: 'object[]', required: true },
269
+ { name: 'barberId', tsType: 'string', required: true },
270
270
  ],
271
271
  source: 'then',
272
+ sourceSceneName: undefined,
272
273
  sourceMomentName: 'book barber appointment',
273
274
  },
275
+ ]);
276
+
277
+ expect(result.states).toEqual([
274
278
  {
275
- type: 'BarberAppointmentBooked',
279
+ type: 'CustomerAppointments',
276
280
  fields: [
277
281
  { name: 'customerId', tsType: 'string', required: true },
278
- { name: 'barberId', tsType: 'string', required: true },
282
+ { name: 'appointments', tsType: 'object[]', required: true },
279
283
  ],
280
- source: 'then',
281
- sourceSceneName: undefined,
282
- sourceMomentName: 'book barber appointment',
283
284
  },
284
285
  ]);
285
286
  });
@@ -91,16 +91,8 @@ function extractMessagesForCommand(slice: Moment, allMessages: MessageDefinition
91
91
  debugCommand(' Command schemas: %o', Object.keys(commandSchemasByName));
92
92
 
93
93
  const allGivenRefs = gwtSpecs.flatMap((gwt) => gwt.given);
94
- const stateAsEvents: Message[] = allGivenRefs
95
- .filter((ref) => !allMessages.some((m) => m.type === 'event' && m.name === ref.eventRef))
96
- .filter((ref) => allMessages.some((m) => m.type === 'state' && m.name === ref.eventRef))
97
- .map((ref) => ({
98
- type: ref.eventRef,
99
- fields: extractFieldsFromMessage(ref.eventRef, 'state', allMessages),
100
- source: 'then' as const,
101
- sourceMomentName: slice.name,
102
- }));
103
- debugCommand(' State-as-events from Given: %d', stateAsEvents.length);
94
+ const givenStates = extractStatesFromGiven(allGivenRefs, allMessages);
95
+ debugCommand(' Given states: %d', givenStates.length);
104
96
 
105
97
  const events: Message[] = gwtSpecs.flatMap((gwt): Message[] => {
106
98
  const eventOnlyGiven = gwt.given.filter((ref) =>
@@ -120,8 +112,8 @@ function extractMessagesForCommand(slice: Moment, allMessages: MessageDefinition
120
112
 
121
113
  const result = {
122
114
  commands,
123
- events: deduplicateMessages([...stateAsEvents, ...events, ...dataTargetEvents]),
124
- states: [],
115
+ events: deduplicateMessages([...events, ...dataTargetEvents]),
116
+ states: givenStates,
125
117
  commandSchemasByName,
126
118
  };
127
119
 
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { EventRef, MessageDefinition } from '../types';
3
+ import { extractStatesFromGiven } from './states';
4
+
5
+ describe('extractStatesFromGiven', () => {
6
+ it('should extract state refs from given and return state messages', () => {
7
+ const givenRefs: EventRef[] = [{ eventRef: 'CustomerAppointments' }, { eventRef: 'OrderPlaced' }];
8
+
9
+ const allMessages: MessageDefinition[] = [
10
+ {
11
+ type: 'state',
12
+ name: 'CustomerAppointments',
13
+ fields: [
14
+ { name: 'customerId', type: 'string', required: true },
15
+ { name: 'appointments', type: 'object[]', required: true },
16
+ ],
17
+ },
18
+ {
19
+ type: 'event',
20
+ name: 'OrderPlaced',
21
+ fields: [{ name: 'orderId', type: 'string', required: true }],
22
+ },
23
+ ];
24
+
25
+ const result = extractStatesFromGiven(givenRefs, allMessages);
26
+
27
+ expect(result).toEqual([
28
+ {
29
+ type: 'CustomerAppointments',
30
+ fields: [
31
+ { name: 'customerId', tsType: 'string', required: true },
32
+ { name: 'appointments', tsType: 'object[]', required: true },
33
+ ],
34
+ },
35
+ ]);
36
+ });
37
+
38
+ it('should return empty array when no given refs match state messages', () => {
39
+ const givenRefs: EventRef[] = [{ eventRef: 'OrderPlaced' }];
40
+
41
+ const allMessages: MessageDefinition[] = [
42
+ {
43
+ type: 'event',
44
+ name: 'OrderPlaced',
45
+ fields: [{ name: 'orderId', type: 'string', required: true }],
46
+ },
47
+ ];
48
+
49
+ const result = extractStatesFromGiven(givenRefs, allMessages);
50
+
51
+ expect(result).toEqual([]);
52
+ });
53
+ });
@@ -42,11 +42,12 @@ import {
42
42
  isValidTsIdentifier,
43
43
  sanitizeFieldType,
44
44
  } from './extract';
45
- import { extractStreamIdFields, getStreamFromSink } from './extract/data-sink';
45
+ import { getStreamFromSink } from './extract/data-sink';
46
46
  import { buildEventIdFieldMap } from './extract/projection';
47
47
  import { buildArgToStateFieldMap } from './extract/query';
48
48
  import { normalizeMomentForTemplate } from './extract/slice-normalizer';
49
49
  import { extractGwtSpecsFromMoment, type GwtResult } from './extract/step-converter';
50
+ import { buildKeepFieldNames, findDerivedDateInfo, isKeyTraceable } from './templateHelpers';
50
51
  import type { GwtCondition, Message, MessageDefinition } from './types';
51
52
 
52
53
  export class TemplateRenderError extends Error {
@@ -431,6 +432,9 @@ async function renderTemplate(
431
432
  isReferencedMessageTypeArray,
432
433
  extractReferencedTypeName,
433
434
  referencedTypes: data.referencedTypes,
435
+ findDerivedDateInfo,
436
+ isKeyTraceable,
437
+ buildKeepFieldNames,
434
438
  });
435
439
 
436
440
  debugTemplate('Template rendered, output size: %d bytes', result.length);
@@ -709,8 +713,6 @@ async function prepareTemplateData(
709
713
  const argToStateFieldMap =
710
714
  slice.type === 'query' ? buildArgToStateFieldMap(queryGwtMapping, stateFieldNames, slice.mappings) : undefined;
711
715
 
712
- const crossSceneGivens = buildCrossSceneGivens(gwtMapping, events, scene, scenes ?? [], filteredCommands);
713
-
714
716
  return {
715
717
  sceneName: scene.name,
716
718
  momentName: slice.name,
@@ -741,63 +743,9 @@ async function prepareTemplateData(
741
743
  eventCommandPairs,
742
744
  eventIdFieldMap,
743
745
  argToStateFieldMap,
744
- crossSceneGivens,
745
746
  };
746
747
  }
747
748
 
748
- interface CrossSceneGiven {
749
- sourceStreamPattern: string;
750
- linkingFields: Array<{
751
- streamVar: string;
752
- commandField: string;
753
- }>;
754
- allVarsResolved: boolean;
755
- }
756
-
757
- export function buildCrossSceneGivens(
758
- gwtMapping: Record<string, GwtCondition[]>,
759
- events: Message[],
760
- currentScene: Scene,
761
- scenes: Scene[],
762
- filteredCommands: Message[],
763
- ): CrossSceneGiven[] {
764
- const commandFieldNames = new Set(filteredCommands.flatMap((c) => c.fields.map((f) => f.name)));
765
- const seenPatterns = new Set<string>();
766
- const result: CrossSceneGiven[] = [];
767
-
768
- for (const conditions of Object.values(gwtMapping)) {
769
- for (const gwt of conditions) {
770
- for (const givenRef of gwt.given ?? []) {
771
- if (!('eventRef' in givenRef)) continue;
772
-
773
- const event = events.find((e) => e.type === givenRef.eventRef);
774
- if (!event || event.sourceSceneName === currentScene.name) continue;
775
-
776
- const sourceScene = scenes.find((s) => s.name === event.sourceSceneName);
777
- const sourceMoment = sourceScene?.moments.find((m) => m.name === event.sourceMomentName);
778
- if (!sourceMoment) continue;
779
-
780
- const { streamPattern } = getStreamFromSink(sourceMoment);
781
- if (!streamPattern || seenPatterns.has(streamPattern)) continue;
782
- seenPatterns.add(streamPattern);
783
-
784
- const streamVars = extractStreamIdFields(streamPattern);
785
- const linkingFields = streamVars
786
- .filter((v) => commandFieldNames.has(v))
787
- .map((v) => ({ streamVar: v, commandField: v }));
788
-
789
- result.push({
790
- sourceStreamPattern: streamPattern,
791
- linkingFields,
792
- allVarsResolved: linkingFields.length === streamVars.length,
793
- });
794
- }
795
- }
796
- }
797
-
798
- return result;
799
- }
800
-
801
749
  function annotateEventSources(
802
750
  events: Message[],
803
751
  scenes: Scene[],
@@ -980,11 +928,12 @@ async function generateFilesForMoment(
980
928
  );
981
929
 
982
930
  debugMoment(' Generating %d files from templates', filteredTemplates.length);
983
- const plans = await Promise.all(
931
+ const allPlans = await Promise.all(
984
932
  filteredTemplates.map((template) =>
985
933
  generateFileForTemplate(template, slice, momentDir, templateData, unionToEnumName),
986
934
  ),
987
935
  );
936
+ const plans = allPlans.filter((p) => p.contents.trim().length > 0);
988
937
  debugMoment(' Generated %d file plans for moment: %s', plans.length, slice.name);
989
938
  return { plans, duplicateCommands };
990
939
  }
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildKeepFieldNames, findDerivedDateInfo, isKeyTraceable } from './templateHelpers';
3
+
4
+ describe('findDerivedDateInfo', () => {
5
+ it('should detect a date field not in command fields and not in given events', () => {
6
+ const eventResults = [{ exampleData: { workoutId: 'w1', date: '2024-01-01', duration: 30 } }];
7
+ const commandFieldNames = new Set(['workoutId', 'duration']);
8
+
9
+ const result = findDerivedDateInfo(eventResults, commandFieldNames, undefined);
10
+
11
+ expect(result).toEqual({ date: '2024-01-01', fields: ['date'] });
12
+ });
13
+
14
+ it('should return null date when no date-like fields exist', () => {
15
+ const eventResults = [{ exampleData: { workoutId: 'w1', duration: 30 } }];
16
+ const commandFieldNames = new Set(['workoutId', 'duration']);
17
+
18
+ const result = findDerivedDateInfo(eventResults, commandFieldNames, undefined);
19
+
20
+ expect(result).toEqual({ date: null, fields: [] });
21
+ });
22
+
23
+ it('should exclude date values that appear in given events', () => {
24
+ const eventResults = [{ exampleData: { id: 'x', startDate: '2024-01-01' } }];
25
+ const commandFieldNames = new Set(['id']);
26
+ const givenEvents = [{ exampleData: { startDate: '2024-01-01' } }];
27
+
28
+ const result = findDerivedDateInfo(eventResults, commandFieldNames, givenEvents);
29
+
30
+ expect(result).toEqual({ date: null, fields: [] });
31
+ });
32
+
33
+ it('should return null when multiple distinct dates exist', () => {
34
+ const eventResults = [{ exampleData: { startDate: '2024-01-01', endDate: '2024-02-01' } }];
35
+ const commandFieldNames = new Set<string>();
36
+
37
+ const result = findDerivedDateInfo(eventResults, commandFieldNames, undefined);
38
+
39
+ expect(result).toEqual({ date: null, fields: [] });
40
+ });
41
+
42
+ it('should group multiple fields sharing the same date', () => {
43
+ const eventResults = [{ exampleData: { createdAt: '2024-01-01', updatedAt: '2024-01-01' } }];
44
+ const commandFieldNames = new Set<string>();
45
+
46
+ const result = findDerivedDateInfo(eventResults, commandFieldNames, undefined);
47
+
48
+ expect(result).toEqual({ date: '2024-01-01', fields: ['createdAt', 'updatedAt'] });
49
+ });
50
+ });
51
+
52
+ describe('isKeyTraceable', () => {
53
+ it('should return true when key+value match a given event', () => {
54
+ const givenEvents = [{ exampleData: { userId: 'u1' } }];
55
+
56
+ expect(isKeyTraceable('userId', 'u1', givenEvents)).toBe(true);
57
+ });
58
+
59
+ it('should return false when value does not match', () => {
60
+ const givenEvents = [{ exampleData: { userId: 'u1' } }];
61
+
62
+ expect(isKeyTraceable('userId', 'u2', givenEvents)).toBe(false);
63
+ });
64
+
65
+ it('should return false for null/undefined/object values', () => {
66
+ expect(isKeyTraceable('x', null, [{ exampleData: { x: null } }])).toBe(false);
67
+ expect(isKeyTraceable('x', undefined, [{ exampleData: { x: undefined } }])).toBe(false);
68
+ expect(isKeyTraceable('x', { a: 1 }, [{ exampleData: { x: { a: 1 } } }])).toBe(false);
69
+ });
70
+
71
+ it('should return false when no given events', () => {
72
+ expect(isKeyTraceable('x', 'v', undefined)).toBe(false);
73
+ });
74
+ });
75
+
76
+ describe('buildKeepFieldNames', () => {
77
+ it('should include command fields, derived date fields, and traceable fields', () => {
78
+ const eventResults = [{ exampleData: { workoutId: 'w1', userId: 'u1', date: '2024-01-01', duration: 30 } }];
79
+ const commandFieldNames = new Set(['workoutId', 'duration']);
80
+ const derivedDateFieldNames = ['date'];
81
+ const givenEvents = [{ exampleData: { userId: 'u1' } }];
82
+
83
+ const result = buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames, givenEvents);
84
+
85
+ expect(result).toEqual(new Set(['workoutId', 'duration', 'date', 'userId']));
86
+ });
87
+
88
+ it('should not include untraceable fields', () => {
89
+ const eventResults = [{ exampleData: { id: 'x', mystery: 'abc' } }];
90
+ const commandFieldNames = new Set(['id']);
91
+ const derivedDateFieldNames: string[] = [];
92
+ const givenEvents: Array<{ exampleData?: Record<string, unknown> }> = [];
93
+
94
+ const result = buildKeepFieldNames(eventResults, commandFieldNames, derivedDateFieldNames, givenEvents);
95
+
96
+ expect(result).toEqual(new Set(['id']));
97
+ });
98
+ });
@@ -0,0 +1,58 @@
1
+ interface EventData {
2
+ exampleData?: Record<string, unknown>;
3
+ }
4
+
5
+ export function findDerivedDateInfo(
6
+ eventResults: EventData[],
7
+ commandFieldNames: Set<string>,
8
+ givenEvents: EventData[] | undefined,
9
+ ): { date: string | null; fields: string[] } {
10
+ const givenValues = new Set<string>();
11
+ for (const g of givenEvents ?? []) {
12
+ for (const val of Object.values(g.exampleData ?? {})) {
13
+ if (typeof val === 'string') givenValues.add(val);
14
+ }
15
+ }
16
+ const fieldsByDate = new Map<string, string[]>();
17
+ for (const e of eventResults) {
18
+ for (const [key, val] of Object.entries(e.exampleData ?? {})) {
19
+ if (
20
+ !commandFieldNames.has(key) &&
21
+ typeof val === 'string' &&
22
+ /^\d{4}-\d{2}-\d{2}$/.test(val) &&
23
+ !givenValues.has(val)
24
+ ) {
25
+ if (!fieldsByDate.has(val)) fieldsByDate.set(val, []);
26
+ fieldsByDate.get(val)!.push(key);
27
+ }
28
+ }
29
+ }
30
+ if (fieldsByDate.size !== 1) return { date: null, fields: [] };
31
+ const [date, fields] = [...fieldsByDate.entries()][0];
32
+ return { date, fields };
33
+ }
34
+
35
+ export function isKeyTraceable(key: string, value: unknown, givenEvents: EventData[] | undefined): boolean {
36
+ if (value === null || value === undefined || typeof value === 'object') return false;
37
+ for (const g of givenEvents ?? []) {
38
+ if ((g.exampleData ?? {})[key] === value) return true;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ export function buildKeepFieldNames(
44
+ eventResults: EventData[],
45
+ commandFieldNames: Set<string>,
46
+ derivedDateFieldNames: string[],
47
+ givenEvents: EventData[] | undefined,
48
+ ): Set<string> {
49
+ const keep = new Set([...commandFieldNames, ...derivedDateFieldNames]);
50
+ for (const e of eventResults) {
51
+ for (const [key, value] of Object.entries(e.exampleData ?? {})) {
52
+ if (!keep.has(key) && isKeyTraceable(key, value, givenEvents)) {
53
+ keep.add(key);
54
+ }
55
+ }
56
+ }
57
+ return keep;
58
+ }