@auto-engineer/server-generator-apollo-emmett 1.88.0 → 1.89.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 (64) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +5 -5
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +72 -0
  5. package/dist/src/codegen/extract/slice-normalizer.d.ts.map +1 -1
  6. package/dist/src/codegen/extract/slice-normalizer.js +14 -0
  7. package/dist/src/codegen/extract/slice-normalizer.js.map +1 -1
  8. package/dist/src/codegen/extract/type-helpers.d.ts +10 -0
  9. package/dist/src/codegen/extract/type-helpers.d.ts.map +1 -1
  10. package/dist/src/codegen/extract/type-helpers.js +17 -0
  11. package/dist/src/codegen/extract/type-helpers.js.map +1 -1
  12. package/dist/src/codegen/scaffoldFromSchema.d.ts.map +1 -1
  13. package/dist/src/codegen/scaffoldFromSchema.js +6 -4
  14. package/dist/src/codegen/scaffoldFromSchema.js.map +1 -1
  15. package/dist/src/codegen/templates/command/decide.specs.specs.ts +293 -34
  16. package/dist/src/codegen/templates/command/decide.specs.ts +34 -14
  17. package/dist/src/codegen/templates/command/decide.specs.ts.ejs +47 -14
  18. package/dist/src/codegen/templates/command/decide.ts.ejs +32 -4
  19. package/dist/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
  20. package/dist/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
  21. package/dist/src/codegen/templates/query/projection.specs.specs.ts +124 -0
  22. package/dist/src/codegen/templates/query/projection.specs.ts +20 -0
  23. package/dist/src/codegen/templates/query/projection.specs.ts.ejs +5 -1
  24. package/dist/src/codegen/templates/query/projection.ts.ejs +5 -0
  25. package/dist/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
  26. package/dist/src/codegen/templates/react/react.specs.specs.ts +115 -0
  27. package/dist/src/codegen/templates/react/react.specs.ts +9 -2
  28. package/dist/src/codegen/templates/react/react.specs.ts.ejs +1 -3
  29. package/dist/src/codegen/templates/react/react.ts.ejs +22 -9
  30. package/dist/src/codegen/templates/react/react.ts.specs.ts +253 -0
  31. package/dist/src/codegen/templates/react/register.specs.ts +27 -23
  32. package/dist/src/codegen/templates/react/register.ts.ejs +5 -1
  33. package/dist/tsconfig.tsbuildinfo +1 -1
  34. package/ketchup-plan.md +12 -3
  35. package/package.json +4 -4
  36. package/src/codegen/extract/slice-normalizer.specs.ts +83 -0
  37. package/src/codegen/extract/slice-normalizer.ts +15 -0
  38. package/src/codegen/extract/type-helpers.specs.ts +77 -1
  39. package/src/codegen/extract/type-helpers.ts +23 -0
  40. package/src/codegen/formatTsValueSimple.specs.ts +8 -0
  41. package/src/codegen/scaffoldFromSchema.ts +7 -3
  42. package/src/codegen/templates/command/decide.specs.specs.ts +293 -34
  43. package/src/codegen/templates/command/decide.specs.ts +34 -14
  44. package/src/codegen/templates/command/decide.specs.ts.ejs +47 -14
  45. package/src/codegen/templates/command/decide.ts.ejs +32 -4
  46. package/src/codegen/templates/command/mutation.resolver.specs.ts +72 -1
  47. package/src/codegen/templates/command/mutation.resolver.ts.ejs +1 -1
  48. package/src/codegen/templates/query/projection.specs.specs.ts +124 -0
  49. package/src/codegen/templates/query/projection.specs.ts +20 -0
  50. package/src/codegen/templates/query/projection.specs.ts.ejs +5 -1
  51. package/src/codegen/templates/query/projection.ts.ejs +5 -0
  52. package/src/codegen/templates/query/query.resolver.ts.ejs +1 -1
  53. package/src/codegen/templates/react/react.specs.specs.ts +115 -0
  54. package/src/codegen/templates/react/react.specs.ts +9 -2
  55. package/src/codegen/templates/react/react.specs.ts.ejs +1 -3
  56. package/src/codegen/templates/react/react.ts.ejs +22 -9
  57. package/src/codegen/templates/react/react.ts.specs.ts +253 -0
  58. package/src/codegen/templates/react/register.specs.ts +27 -23
  59. package/src/codegen/templates/react/register.ts.ejs +5 -1
  60. package/dist/src/codegen/extract/graphql.d.ts +0 -14
  61. package/dist/src/codegen/extract/graphql.d.ts.map +0 -1
  62. package/dist/src/codegen/extract/graphql.js +0 -81
  63. package/dist/src/codegen/extract/graphql.js.map +0 -1
  64. package/src/codegen/extract/graphql.ts +0 -103
package/ketchup-plan.md CHANGED
@@ -2,10 +2,19 @@
2
2
 
3
3
  ## TODO
4
4
 
5
- - [x] Burst 8: Return array `idField` natively from extraction (d4633c71)
6
- - [x] Burst 9: Fix `findOne` fallback with safe value resolution (8d3ee9cd)
7
- - [x] Burst 10: Derive stable `metadata.now` with narrow trigger
5
+ (none)
8
6
 
9
7
  ## DONE
10
8
 
9
+ - [x] Burst 18: Filter events from react Then assertions (cb94eb5d)
11
10
  - [x] Burst 1: Slim ReadModel to find + findOne, update EJS template API docs, update all inline snapshots
11
+ - [x] Burst 8: Return array `idField` natively from extraction (d4633c71)
12
+ - [x] Burst 9: Fix `findOne` fallback with safe value resolution (8d3ee9cd)
13
+ - [x] Burst 10: Derive stable `metadata.now` with narrow trigger (b7bfd7bf)
14
+ - [x] Burst 11: Omit non-command fields from Then assertions + scaffold annotations (c6953364)
15
+ - [x] Burst 12: Implementer system prompt — anti-hardcoding rules for decide functions
16
+ - [x] Burst 13: Fix formatSpecValue and formatTsValueSimple value-type guards (2412e231)
17
+ - [x] Burst 14: Embed implementation instructions in scaffold templates (f211bd90)
18
+ - [x] Burst 15: Add expectEvents typing shim to decide.specs.ts.ejs (681db5a5)
19
+ - [x] Burst 16: Type-annotate aggregateStream callbacks + dynamic error listing + strengthen instructions (8693a4b3)
20
+ - [x] Burst 17: Filter linking field to primitive types only — skip array/object fields in react template linking (d48982d5)
package/package.json CHANGED
@@ -32,8 +32,8 @@
32
32
  "uuid": "^11.0.0",
33
33
  "web-streams-polyfill": "^4.1.0",
34
34
  "zod": "^3.22.4",
35
- "@auto-engineer/narrative": "1.88.0",
36
- "@auto-engineer/message-bus": "1.88.0"
35
+ "@auto-engineer/narrative": "1.89.0",
36
+ "@auto-engineer/message-bus": "1.89.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.88.0"
47
+ "@auto-engineer/cli": "1.89.0"
48
48
  },
49
- "version": "1.88.0",
49
+ "version": "1.89.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",
@@ -90,4 +90,87 @@ describe('slice-normalizer', () => {
90
90
  ]);
91
91
  });
92
92
  });
93
+
94
+ describe('filterReactThenToCommands — removes mislabeled events from Then', () => {
95
+ const allMessages: MessageDefinition[] = [
96
+ {
97
+ type: 'event',
98
+ name: 'PointsEarned',
99
+ fields: [
100
+ { name: 'memberId', type: 'string', required: true },
101
+ { name: 'points', type: 'number', required: true },
102
+ ],
103
+ },
104
+ {
105
+ type: 'command',
106
+ name: 'NotifyAchievementOpportunity',
107
+ fields: [{ name: 'memberId', type: 'string', required: true }],
108
+ },
109
+ {
110
+ type: 'event',
111
+ name: 'AchievementOpportunityNotified',
112
+ fields: [{ name: 'memberId', type: 'string', required: true }],
113
+ },
114
+ ];
115
+
116
+ it('should remove event And-step after command Then-step', () => {
117
+ const specs: Spec[] = [
118
+ {
119
+ type: 'gherkin',
120
+ feature: 'React to points milestone',
121
+ rules: [
122
+ {
123
+ name: 'When points are earned, notify achievement',
124
+ examples: [
125
+ {
126
+ name: 'command first, event second',
127
+ steps: [
128
+ { keyword: 'When', text: 'PointsEarned', docString: { memberId: 'm1', points: 100 } },
129
+ { keyword: 'Then', text: 'NotifyAchievementOpportunity', docString: { memberId: 'm1' } },
130
+ { keyword: 'And', text: 'AchievementOpportunityNotified', docString: { memberId: 'm1' } },
131
+ ],
132
+ },
133
+ ],
134
+ },
135
+ ],
136
+ },
137
+ ];
138
+
139
+ const slice = createReactSlice(specs);
140
+ const result = normalizeSliceForTemplate(slice, allMessages);
141
+ const example = result.server?.specs?.rules[0].examples[0];
142
+
143
+ expect(example?.then).toEqual([{ commandRef: 'NotifyAchievementOpportunity', exampleData: { memberId: 'm1' } }]);
144
+ });
145
+
146
+ it('should remove event even when it appears before the command in Then', () => {
147
+ const specs: Spec[] = [
148
+ {
149
+ type: 'gherkin',
150
+ feature: 'React to points milestone',
151
+ rules: [
152
+ {
153
+ name: 'When points are earned, notify achievement',
154
+ examples: [
155
+ {
156
+ name: 'event first, command second',
157
+ steps: [
158
+ { keyword: 'When', text: 'PointsEarned', docString: { memberId: 'm1', points: 100 } },
159
+ { keyword: 'Then', text: 'AchievementOpportunityNotified', docString: { memberId: 'm1' } },
160
+ { keyword: 'And', text: 'NotifyAchievementOpportunity', docString: { memberId: 'm1' } },
161
+ ],
162
+ },
163
+ ],
164
+ },
165
+ ],
166
+ },
167
+ ];
168
+
169
+ const slice = createReactSlice(specs);
170
+ const result = normalizeSliceForTemplate(slice, allMessages);
171
+ const example = result.server?.specs?.rules[0].examples[0];
172
+
173
+ expect(example?.then).toEqual([{ commandRef: 'NotifyAchievementOpportunity', exampleData: { memberId: 'm1' } }]);
174
+ });
175
+ });
93
176
  });
@@ -80,6 +80,20 @@ function normalizeReactPatternB(rules: NormalizedRule[], allMessages: MessageDef
80
80
  }
81
81
  }
82
82
 
83
+ function filterReactThenToCommands(rules: NormalizedRule[], allMessages: MessageDefinition[]): void {
84
+ const commandNames = new Set(allMessages.filter((m) => m.type === 'command').map((m) => m.name));
85
+ for (const rule of rules) {
86
+ for (const example of rule.examples) {
87
+ example.then = example.then.filter((item) => {
88
+ if ('commandRef' in item) {
89
+ return commandNames.has(item.commandRef);
90
+ }
91
+ return true;
92
+ });
93
+ }
94
+ }
95
+ }
96
+
83
97
  function normalizeSpecsForTemplate(
84
98
  specs: Spec[],
85
99
  sliceType: SliceType,
@@ -99,6 +113,7 @@ function normalizeSpecsForTemplate(
99
113
 
100
114
  if (sliceType === 'react' && allMessages) {
101
115
  normalizeReactPatternB(rules, allMessages);
116
+ filterReactThenToCommands(rules, allMessages);
102
117
  }
103
118
 
104
119
  return {
@@ -1,5 +1,11 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { isValidTsIdentifier, parseInlineObjectFields, sanitizeFieldType } from './type-helpers';
2
+ import {
3
+ findPrimitiveLinkingField,
4
+ isPrimitiveTsType,
5
+ isValidTsIdentifier,
6
+ parseInlineObjectFields,
7
+ sanitizeFieldType,
8
+ } from './type-helpers';
3
9
 
4
10
  describe('parseInlineObjectFields', () => {
5
11
  it('should parse simple inline object fields', () => {
@@ -94,3 +100,73 @@ describe('sanitizeFieldType', () => {
94
100
  expect(sanitizeFieldType('string')).toBe('string');
95
101
  });
96
102
  });
103
+
104
+ describe('isPrimitiveTsType', () => {
105
+ it('accepts primitive types', () => {
106
+ expect(isPrimitiveTsType('string')).toBe(true);
107
+ expect(isPrimitiveTsType('number')).toBe(true);
108
+ expect(isPrimitiveTsType('boolean')).toBe(true);
109
+ expect(isPrimitiveTsType('Date')).toBe(true);
110
+ expect(isPrimitiveTsType('ID')).toBe(true);
111
+ });
112
+
113
+ it('accepts nullable primitives', () => {
114
+ expect(isPrimitiveTsType('string | null')).toBe(true);
115
+ expect(isPrimitiveTsType('Date | null')).toBe(true);
116
+ });
117
+
118
+ it('rejects complex types', () => {
119
+ expect(isPrimitiveTsType('{exercise:string}[]')).toBe(false);
120
+ expect(isPrimitiveTsType('{exercise:string,sets:number}[]')).toBe(false);
121
+ expect(isPrimitiveTsType('unknown')).toBe(false);
122
+ expect(isPrimitiveTsType('Array<string>')).toBe(false);
123
+ });
124
+ });
125
+
126
+ describe('findPrimitiveLinkingField', () => {
127
+ it('matches primitive fields by name', () => {
128
+ expect(
129
+ findPrimitiveLinkingField([{ name: 'memberId', tsType: 'string' }], [{ name: 'memberId', type: 'string' }]),
130
+ ).toBe('memberId');
131
+ });
132
+
133
+ it('skips array/object fields even when names match', () => {
134
+ expect(
135
+ findPrimitiveLinkingField(
136
+ [
137
+ { name: 'exercises', tsType: '{exercise:string}[]' },
138
+ { name: 'memberId', tsType: 'string' },
139
+ ],
140
+ [
141
+ { name: 'exercises', type: '{exercise:string}[]' },
142
+ { name: 'memberId', type: 'string' },
143
+ ],
144
+ ),
145
+ ).toBe('memberId');
146
+ });
147
+
148
+ it('returns undefined when only non-primitive fields match', () => {
149
+ expect(
150
+ findPrimitiveLinkingField(
151
+ [{ name: 'exercises', tsType: '{exercise:string}[]' }],
152
+ [{ name: 'exercises', type: '{exercise:string}[]' }],
153
+ ),
154
+ ).toBeUndefined();
155
+ });
156
+
157
+ it('returns undefined when no field names match', () => {
158
+ expect(
159
+ findPrimitiveLinkingField([{ name: 'barberId', tsType: 'string' }], [{ name: 'appointmentId', type: 'string' }]),
160
+ ).toBeUndefined();
161
+ });
162
+
163
+ it('handles fields with tsType property (extracted state fields)', () => {
164
+ expect(findPrimitiveLinkingField([{ name: 'id', tsType: 'string' }], [{ name: 'id', tsType: 'string' }])).toBe(
165
+ 'id',
166
+ );
167
+ });
168
+
169
+ it('handles fields with type property (raw message fields)', () => {
170
+ expect(findPrimitiveLinkingField([{ name: 'id', type: 'string' }], [{ name: 'id', type: 'string' }])).toBe('id');
171
+ });
172
+ });
@@ -155,3 +155,26 @@ export function createCollectEnumNames(isEnumType: (tsType: string) => boolean,
155
155
  return Array.from(enumNames).sort();
156
156
  };
157
157
  }
158
+
159
+ export function isPrimitiveTsType(tsType: string): boolean {
160
+ return ['string', 'number', 'boolean', 'Date', 'ID'].includes(baseTs(tsType));
161
+ }
162
+
163
+ export function findPrimitiveLinkingField(
164
+ stateFields: Array<{ name: string; type?: string; tsType?: string }>,
165
+ eventFields: Array<{ name: string; type?: string; tsType?: string }>,
166
+ ): string | undefined {
167
+ for (const sf of stateFields) {
168
+ const sfType = sf.type ?? sf.tsType ?? '';
169
+ if (!isPrimitiveTsType(sfType)) continue;
170
+ if (
171
+ eventFields.some((ef) => {
172
+ const efType = ef.type ?? ef.tsType ?? '';
173
+ return ef.name === sf.name && isPrimitiveTsType(efType);
174
+ })
175
+ ) {
176
+ return sf.name;
177
+ }
178
+ }
179
+ return undefined;
180
+ }
@@ -45,4 +45,12 @@ describe('formatTsValueSimple', () => {
45
45
  it('should return new Date(...) for Date tsType', () => {
46
46
  expect(formatTsValueSimple('2025-01-01', 'Date')).toBe('new Date("2025-01-01")');
47
47
  });
48
+
49
+ it('should fall through to JSON.stringify for array value when tsType is string', () => {
50
+ expect(formatTsValueSimple([{ memberId: 'm1', points: 100 }], 'string')).toBe('[{"memberId":"m1","points":100}]');
51
+ });
52
+
53
+ it('should fall through to JSON.stringify for object value when tsType is string', () => {
54
+ expect(formatTsValueSimple({ memberId: 'm1' }, 'string')).toBe('{"memberId":"m1"}');
55
+ });
48
56
  });
@@ -16,6 +16,7 @@ const debugSlice = createDebug('auto:server-generator-apollo-emmett:scaffold:sli
16
16
  const debugPlan = createDebug('auto:server-generator-apollo-emmett:scaffold:plan');
17
17
  const debugFormat = createDebug('auto:server-generator-apollo-emmett:scaffold:format');
18
18
 
19
+ import { parseSliceRequest } from '@auto-engineer/narrative';
19
20
  import {
20
21
  baseTs,
21
22
  buildCommandGwtMapping,
@@ -28,17 +29,18 @@ import {
28
29
  createIsEnumType,
29
30
  extractMessagesFromSpecs,
30
31
  extractProjectionName,
32
+ findPrimitiveLinkingField,
31
33
  getAllEventTypes,
32
34
  getLocalEvents,
33
35
  groupEventImports,
34
36
  isInlineObjectArray as isInlineObjectArrayHelper,
35
37
  isInlineObject as isInlineObjectHelper,
38
+ isPrimitiveTsType,
36
39
  isValidTsIdentifier,
37
40
  parseInlineObjectFields,
38
41
  sanitizeFieldType,
39
42
  } from './extract';
40
43
  import { getStreamFromSink } from './extract/data-sink';
41
- import { parseGraphQlRequest } from './extract/graphql';
42
44
  import { normalizeSliceForTemplate } from './extract/slice-normalizer';
43
45
  import { extractGwtSpecsFromSlice, type GwtResult } from './extract/step-converter';
44
46
  import type { GwtCondition, Message, MessageDefinition } from './types';
@@ -392,6 +394,8 @@ async function renderTemplate(
392
394
  parseInlineObjectFields,
393
395
  baseTs,
394
396
  isEnumType,
397
+ isPrimitiveTsType,
398
+ findPrimitiveLinkingField,
395
399
  fieldUsesDate,
396
400
  fieldUsesJSON,
397
401
  fieldUsesFloat,
@@ -412,7 +416,7 @@ export function formatTsValueSimple(value: unknown, tsType: string): string {
412
416
  if (tsType === 'Date') {
413
417
  return `new Date(${JSON.stringify(value)})`;
414
418
  }
415
- if (tsType === 'string') {
419
+ if (tsType === 'string' && typeof value !== 'object') {
416
420
  return JSON.stringify(typeof value === 'string' ? value : String(value));
417
421
  }
418
422
  if (tsType === 'number') {
@@ -608,7 +612,7 @@ async function prepareTemplateData(
608
612
  projectionIdField,
609
613
  projectionSingleton,
610
614
  projectionName,
611
- parsedRequest: slice.type === 'query' && slice.request != null ? parseGraphQlRequest(slice.request) : undefined,
615
+ parsedRequest: parseSliceRequest(slice),
612
616
  messages: allMessages,
613
617
  message:
614
618
  slice.type === 'query' && allMessages