@auto-engineer/narrative 1.129.0 → 1.131.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/package.json CHANGED
@@ -26,9 +26,9 @@
26
26
  "typescript": "^5.9.2",
27
27
  "zod": "^3.22.4",
28
28
  "zod-to-json-schema": "^3.22.3",
29
- "@auto-engineer/file-store": "1.129.0",
30
- "@auto-engineer/id": "1.129.0",
31
- "@auto-engineer/message-bus": "1.129.0"
29
+ "@auto-engineer/file-store": "1.131.0",
30
+ "@auto-engineer/id": "1.131.0",
31
+ "@auto-engineer/message-bus": "1.131.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^20.0.0",
@@ -38,7 +38,7 @@
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "version": "1.129.0",
41
+ "version": "1.131.0",
42
42
  "scripts": {
43
43
  "build": "tsx scripts/build.ts",
44
44
  "test": "vitest run --reporter=dot",
@@ -8,7 +8,7 @@ import { inlineAllMessageFieldTypes } from './inlining';
8
8
  import { extractMessagesFromIntegrations, processDataItemIntegrations } from './integrations';
9
9
  import { processGiven, processThen, processWhen } from './spec-processors';
10
10
  import { matchesNarrativePattern } from './strings';
11
- import { resolveInferredType } from './type-inference';
11
+ import { buildTypeInfoFromMessages, resolveInferredType } from './type-inference';
12
12
 
13
13
  type TypeResolver = (
14
14
  t: string,
@@ -107,25 +107,34 @@ function tryResolveFromUnionTypes(
107
107
  function createTypeResolver(
108
108
  narrativeSpecificTypes: Map<string, TypeInfo> | undefined,
109
109
  unionTypes: Map<string, TypeInfo> | undefined,
110
+ messages: Map<string, Message>,
110
111
  ) {
111
112
  return (
112
113
  t: string,
113
114
  expected?: 'command' | 'event' | 'state' | 'query',
114
115
  exampleData?: unknown,
115
116
  ): { resolvedName: string; typeInfo: TypeInfo | undefined } => {
117
+ let result: { resolvedName: string; typeInfo: TypeInfo | undefined } | undefined;
118
+
116
119
  if (narrativeSpecificTypes) {
117
- const result = tryResolveFromNarrativeTypes(t, narrativeSpecificTypes, expected, exampleData);
118
- if (unionTypes) {
119
- return tryFallbackToUnionTypes(t, result.resolvedName, result.typeInfo, unionTypes, expected, exampleData);
120
- }
120
+ const nr = tryResolveFromNarrativeTypes(t, narrativeSpecificTypes, expected, exampleData);
121
+ result = unionTypes
122
+ ? tryFallbackToUnionTypes(t, nr.resolvedName, nr.typeInfo, unionTypes, expected, exampleData)
123
+ : nr;
124
+ } else if (unionTypes) {
125
+ result = tryResolveFromUnionTypes(t, unionTypes, expected, exampleData);
126
+ }
127
+
128
+ if (result && result.resolvedName !== 'InferredType') {
121
129
  return result;
122
130
  }
123
131
 
124
- if (unionTypes) {
125
- return tryResolveFromUnionTypes(t, unionTypes, expected, exampleData);
132
+ const messagesTypeMap = buildTypeInfoFromMessages(messages);
133
+ if (messagesTypeMap) {
134
+ return tryResolveFromUnionTypes(t, messagesTypeMap, expected, exampleData);
126
135
  }
127
136
 
128
- return { resolvedName: t, typeInfo: undefined };
137
+ return result ?? { resolvedName: t, typeInfo: undefined };
129
138
  };
130
139
  }
131
140
 
@@ -272,7 +281,7 @@ function processNarrative(
272
281
  exampleShapeHints: ExampleShapeHints,
273
282
  ): void {
274
283
  const narrativeSpecificTypes = getNarrativeSpecificTypes(flow);
275
- const resolveTypeAndInfo = createTypeResolver(narrativeSpecificTypes, unionTypes);
284
+ const resolveTypeAndInfo = createTypeResolver(narrativeSpecificTypes, unionTypes, messages);
276
285
 
277
286
  flow.slices.forEach((slice: Narrative['slices'][number]) => {
278
287
  processSliceSpecs(slice, resolveTypeAndInfo, messages, exampleShapeHints);
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import type { Narrative } from '../../index';
2
+ import type { Message, Narrative } from '../../index';
3
3
  import { narrativesToModel } from './index';
4
+ import { buildTypeInfoFromMessages, messageToTypeInfo } from './type-inference';
4
5
 
5
6
  describe('Type inference in narrative-to-model transformer', () => {
6
7
  it('should correctly extract command types from when clauses', () => {
@@ -184,3 +185,158 @@ describe('Type inference in narrative-to-model transformer', () => {
184
185
  expect(model.messages.some((msg) => msg.name === 'InferredType')).toBe(false);
185
186
  });
186
187
  });
188
+
189
+ describe('Messages-derived fallback in type resolution', () => {
190
+ it('resolves InferredType in second example When step via messages populated by first example', () => {
191
+ const flows: Narrative[] = [
192
+ {
193
+ name: 'Booking Flow',
194
+ id: 'FLOW-001',
195
+ slices: [
196
+ {
197
+ id: 'SLICE-001',
198
+ type: 'command',
199
+ name: 'Request Booking',
200
+ client: { specs: [] },
201
+ server: {
202
+ description: 'Request booking server',
203
+ specs: [
204
+ {
205
+ type: 'gherkin',
206
+ feature: 'Request Booking Specs',
207
+ rules: [
208
+ {
209
+ id: 'RULE-001',
210
+ name: 'Happy path',
211
+ examples: [
212
+ {
213
+ name: 'Room available',
214
+ steps: [
215
+ {
216
+ keyword: 'When',
217
+ text: 'RequestBooking',
218
+ docString: { guestId: 'g-1', roomType: 'suite' },
219
+ },
220
+ {
221
+ keyword: 'Then',
222
+ text: 'BookingRequested',
223
+ docString: { guestId: 'g-1', roomType: 'suite' },
224
+ },
225
+ ],
226
+ },
227
+ {
228
+ name: 'Capacity exceeded',
229
+ steps: [
230
+ {
231
+ keyword: 'When',
232
+ text: 'InferredType',
233
+ docString: { guestId: 'g-2', roomType: 'double' },
234
+ },
235
+ {
236
+ keyword: 'Then',
237
+ text: 'BookingDenied',
238
+ docString: { guestId: 'g-2', reason: 'no availability' },
239
+ },
240
+ ],
241
+ },
242
+ ],
243
+ },
244
+ ],
245
+ },
246
+ ],
247
+ },
248
+ },
249
+ ],
250
+ },
251
+ ];
252
+
253
+ const model = narrativesToModel(flows);
254
+
255
+ expect(model.messages.some((msg) => msg.name === 'InferredType')).toBe(false);
256
+ expect(model.messages.some((msg) => msg.name === 'RequestBooking')).toBe(true);
257
+ expect(model.messages.some((msg) => msg.name === 'BookingRequested')).toBe(true);
258
+ expect(model.messages.some((msg) => msg.name === 'BookingDenied')).toBe(true);
259
+ });
260
+ });
261
+
262
+ describe('messageToTypeInfo', () => {
263
+ it('converts a Message to TypeInfo with correct field mapping', () => {
264
+ const msg: Message = {
265
+ name: 'RequestBooking',
266
+ type: 'command',
267
+ fields: [
268
+ { name: 'guestId', type: 'string', required: true },
269
+ { name: 'roomType', type: 'string', required: false },
270
+ ],
271
+ };
272
+
273
+ const result = messageToTypeInfo(msg);
274
+
275
+ expect(result).toEqual({
276
+ stringLiteral: 'RequestBooking',
277
+ classification: 'command',
278
+ dataFields: [
279
+ { name: 'guestId', type: 'string', required: true },
280
+ { name: 'roomType', type: 'string', required: false },
281
+ ],
282
+ });
283
+ });
284
+ });
285
+
286
+ describe('buildTypeInfoFromMessages', () => {
287
+ it('returns undefined for an empty messages map', () => {
288
+ const messages = new Map<string, Message>();
289
+
290
+ expect(buildTypeInfoFromMessages(messages)).toEqual(undefined);
291
+ });
292
+
293
+ it('builds a TypeInfo map from a populated messages map', () => {
294
+ const messages = new Map<string, Message>([
295
+ [
296
+ 'RequestBooking',
297
+ {
298
+ name: 'RequestBooking',
299
+ type: 'command',
300
+ fields: [{ name: 'guestId', type: 'string', required: true }],
301
+ },
302
+ ],
303
+ [
304
+ 'BookingRequested',
305
+ {
306
+ name: 'BookingRequested',
307
+ type: 'event',
308
+ fields: [
309
+ { name: 'guestId', type: 'string', required: true },
310
+ { name: 'bookedAt', type: 'Date', required: true },
311
+ ],
312
+ },
313
+ ],
314
+ ]);
315
+
316
+ const result = buildTypeInfoFromMessages(messages);
317
+
318
+ expect(result).toEqual(
319
+ new Map([
320
+ [
321
+ 'RequestBooking',
322
+ {
323
+ stringLiteral: 'RequestBooking',
324
+ classification: 'command',
325
+ dataFields: [{ name: 'guestId', type: 'string', required: true }],
326
+ },
327
+ ],
328
+ [
329
+ 'BookingRequested',
330
+ {
331
+ stringLiteral: 'BookingRequested',
332
+ classification: 'event',
333
+ dataFields: [
334
+ { name: 'guestId', type: 'string', required: true },
335
+ { name: 'bookedAt', type: 'Date', required: true },
336
+ ],
337
+ },
338
+ ],
339
+ ]),
340
+ );
341
+ });
342
+ });
@@ -1,4 +1,5 @@
1
1
  import type { TypeInfo } from '../../loader/ts-utils';
2
+ import type { Message } from '../../schema';
2
3
 
3
4
  function tryDataFieldMatch(typeInfo: TypeInfo, dataKeys: Set<string>): boolean {
4
5
  const dataField = typeInfo.dataFields?.find((f) => f.name === 'data');
@@ -125,3 +126,20 @@ export function resolveInferredType(
125
126
  const resolved = resolveFromCandidates(candidates, all, expectedMessageType, exampleData);
126
127
  return resolved ?? typeName;
127
128
  }
129
+
130
+ export function messageToTypeInfo(msg: Message): TypeInfo {
131
+ return {
132
+ stringLiteral: msg.name,
133
+ classification: msg.type,
134
+ dataFields: msg.fields.map((f) => ({ name: f.name, type: f.type, required: f.required })),
135
+ };
136
+ }
137
+
138
+ export function buildTypeInfoFromMessages(messages: Map<string, Message>): Map<string, TypeInfo> | undefined {
139
+ if (messages.size === 0) return undefined;
140
+ const map = new Map<string, TypeInfo>();
141
+ for (const [name, msg] of messages) {
142
+ map.set(name, messageToTypeInfo(msg));
143
+ }
144
+ return map;
145
+ }