@auto-engineer/narrative 1.3.2 → 1.3.4

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
@@ -23,9 +23,9 @@
23
23
  "typescript": "^5.9.2",
24
24
  "zod": "^3.22.4",
25
25
  "zod-to-json-schema": "^3.22.3",
26
- "@auto-engineer/message-bus": "1.3.2",
27
- "@auto-engineer/id": "1.3.2",
28
- "@auto-engineer/file-store": "1.3.2"
26
+ "@auto-engineer/id": "1.3.4",
27
+ "@auto-engineer/file-store": "1.3.4",
28
+ "@auto-engineer/message-bus": "1.3.4"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^20.0.0",
@@ -35,7 +35,7 @@
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  },
38
- "version": "1.3.2",
38
+ "version": "1.3.4",
39
39
  "scripts": {
40
40
  "build": "tsx scripts/build.ts",
41
41
  "test": "vitest run --reporter=dot",
@@ -3148,5 +3148,296 @@ narrative('All Projection Types', 'ALL-PROJ', () => {
3148
3148
  const code = getCode(await modelToNarrative(modelWithSourceInstructions));
3149
3149
  expect(code).toContain(".additionalInstructions('Filter by active orders only')");
3150
3150
  });
3151
+
3152
+ it('should generate 3 narrative files for gym membership model', async () => {
3153
+ const gymModel: Model = {
3154
+ variant: 'specs',
3155
+ narratives: [
3156
+ {
3157
+ id: 'mrwDfHhDi',
3158
+ name: 'Gym Membership Registration',
3159
+ slices: [
3160
+ {
3161
+ type: 'command',
3162
+ name: 'Register New Member',
3163
+ id: 'YcOe0aHz3',
3164
+ client: { specs: [] },
3165
+ server: {
3166
+ description: '',
3167
+ specs: [
3168
+ {
3169
+ type: 'gherkin',
3170
+ feature: '',
3171
+ rules: [
3172
+ {
3173
+ id: 'ODJGjsU2m',
3174
+ name: 'Member account creation process',
3175
+ examples: [
3176
+ {
3177
+ id: 'Jdrjvn3HV',
3178
+ name: 'Create member account',
3179
+ steps: [
3180
+ {
3181
+ id: 'D1v4V3TEF',
3182
+ keyword: 'When',
3183
+ text: 'SubmitMembershipRegistration',
3184
+ docString: { name: 'John Doe', email: 'john@example.com', phone: '123-456-7890' },
3185
+ },
3186
+ {
3187
+ id: 'IeXOn8W9v',
3188
+ keyword: 'Then',
3189
+ text: 'MemberAccountCreated',
3190
+ docString: {
3191
+ memberId: 'mem_123',
3192
+ name: 'John Doe',
3193
+ email: 'john@example.com',
3194
+ phone: '123-456-7890',
3195
+ },
3196
+ },
3197
+ ],
3198
+ },
3199
+ ],
3200
+ },
3201
+ ],
3202
+ },
3203
+ ],
3204
+ data: {
3205
+ items: [
3206
+ {
3207
+ target: { type: 'Event', name: 'MemberAccountCreated' },
3208
+ destination: { type: 'stream', pattern: 'members' },
3209
+ },
3210
+ ],
3211
+ },
3212
+ },
3213
+ },
3214
+ ],
3215
+ sourceFile: '/narratives/gym.narrative.ts',
3216
+ },
3217
+ {
3218
+ id: 'exByqGILR',
3219
+ name: 'Gym Class Booking',
3220
+ slices: [
3221
+ {
3222
+ type: 'command',
3223
+ name: 'Book Gym Class',
3224
+ id: 'iEg3Qjbbp',
3225
+ client: { specs: [] },
3226
+ server: {
3227
+ description: '',
3228
+ specs: [
3229
+ {
3230
+ type: 'gherkin',
3231
+ feature: '',
3232
+ rules: [
3233
+ {
3234
+ id: 'c7TfHiadX',
3235
+ name: 'Class booking process',
3236
+ examples: [
3237
+ {
3238
+ id: '3MoVhLhAU',
3239
+ name: 'Book a class',
3240
+ steps: [
3241
+ {
3242
+ id: 'HVYnJHNCl',
3243
+ keyword: 'When',
3244
+ text: 'BookGymClass',
3245
+ docString: { memberId: 'mem_123', classId: 'cls_456', date: '2023-10-15' },
3246
+ },
3247
+ {
3248
+ id: 'FuS1S7AMA',
3249
+ keyword: 'Then',
3250
+ text: 'ClassReservationConfirmed',
3251
+ docString: {
3252
+ reservationId: 'res_789',
3253
+ memberId: 'mem_123',
3254
+ classId: 'cls_456',
3255
+ date: '2023-10-15',
3256
+ },
3257
+ },
3258
+ ],
3259
+ },
3260
+ ],
3261
+ },
3262
+ ],
3263
+ },
3264
+ ],
3265
+ data: {
3266
+ items: [
3267
+ {
3268
+ target: { type: 'Event', name: 'ClassReservationConfirmed' },
3269
+ destination: { type: 'stream', pattern: 'reservations' },
3270
+ },
3271
+ ],
3272
+ },
3273
+ },
3274
+ },
3275
+ ],
3276
+ sourceFile: '/narratives/untitled-1.narrative.ts',
3277
+ },
3278
+ {
3279
+ id: 'o0odruqZA',
3280
+ name: 'Gym Check-In',
3281
+ slices: [
3282
+ {
3283
+ type: 'command',
3284
+ name: 'Perform Check-In',
3285
+ id: 'Cxl4UHfbX',
3286
+ client: { specs: [] },
3287
+ server: {
3288
+ description: '',
3289
+ specs: [
3290
+ {
3291
+ type: 'gherkin',
3292
+ feature: '',
3293
+ rules: [
3294
+ {
3295
+ id: 'n81cBIt30',
3296
+ name: 'Check-in process',
3297
+ examples: [
3298
+ {
3299
+ id: 'QiMaBqctq',
3300
+ name: 'Record check-in',
3301
+ steps: [
3302
+ {
3303
+ id: 'Pkcushx04',
3304
+ keyword: 'When',
3305
+ text: 'PerformCheckIn',
3306
+ docString: { memberId: 'mem_123', dateTime: '2023-10-15T08:00:00Z' },
3307
+ },
3308
+ {
3309
+ id: 'Z8Ef9Yo2R',
3310
+ keyword: 'Then',
3311
+ text: 'CheckInRecorded',
3312
+ docString: {
3313
+ checkInId: 'chk_123',
3314
+ memberId: 'mem_123',
3315
+ dateTime: '2023-10-15T08:00:00Z',
3316
+ },
3317
+ },
3318
+ ],
3319
+ },
3320
+ ],
3321
+ },
3322
+ ],
3323
+ },
3324
+ ],
3325
+ data: {
3326
+ items: [
3327
+ {
3328
+ target: { type: 'Event', name: 'CheckInRecorded' },
3329
+ destination: { type: 'stream', pattern: 'checkins' },
3330
+ },
3331
+ ],
3332
+ },
3333
+ },
3334
+ },
3335
+ ],
3336
+ sourceFile: '/narratives/untitled-1-2.narrative.ts',
3337
+ },
3338
+ ],
3339
+ messages: [
3340
+ { type: 'command', name: 'SubmitMembershipRegistration', fields: [] },
3341
+ { type: 'command', name: 'BookGymClass', fields: [] },
3342
+ { type: 'command', name: 'PerformCheckIn', fields: [] },
3343
+ { type: 'event', name: 'MemberAccountCreated', fields: [], source: 'internal' },
3344
+ { type: 'event', name: 'ClassReservationConfirmed', fields: [], source: 'internal' },
3345
+ { type: 'event', name: 'CheckInRecorded', fields: [], source: 'internal' },
3346
+ ],
3347
+ modules: [],
3348
+ };
3349
+
3350
+ const result = await modelToNarrative(gymModel);
3351
+
3352
+ // Should generate 3 files, one for each narrative
3353
+ expect(result.files.length).toBe(3);
3354
+
3355
+ // Check file paths match the sourceFile from each narrative
3356
+ const filePaths = result.files.map((f) => f.path);
3357
+ expect(filePaths).toContain('/narratives/gym.narrative.ts');
3358
+ expect(filePaths).toContain('/narratives/untitled-1.narrative.ts');
3359
+ expect(filePaths).toContain('/narratives/untitled-1-2.narrative.ts');
3360
+
3361
+ // Verify each narrative has proper content
3362
+ const code = getCode(result);
3363
+ expect(code).toContain("narrative('Gym Membership Registration'");
3364
+ expect(code).toContain("narrative('Gym Class Booking'");
3365
+ expect(code).toContain("narrative('Gym Check-In'");
3366
+
3367
+ // Check slices are generated for each narrative
3368
+ expect(code).toContain("command('Register New Member'");
3369
+ expect(code).toContain("command('Book Gym Class'");
3370
+ expect(code).toContain("command('Perform Check-In'");
3371
+ });
3372
+ });
3373
+
3374
+ it('generates all declared types for authored modules regardless of usage analysis', async () => {
3375
+ const model: Model = {
3376
+ variant: 'specs',
3377
+ narratives: [
3378
+ {
3379
+ id: 'narrative-1',
3380
+ name: 'Gym Goal Setting',
3381
+ slices: [
3382
+ {
3383
+ type: 'command',
3384
+ name: 'Set Fitness Goal',
3385
+ client: { specs: [] },
3386
+ server: {
3387
+ description: 'Set a fitness goal',
3388
+ specs: [
3389
+ {
3390
+ type: 'gherkin',
3391
+ feature: 'Goal Setting',
3392
+ rules: [
3393
+ {
3394
+ name: 'Create goal',
3395
+ examples: [
3396
+ {
3397
+ name: 'Create fitness goal',
3398
+ steps: [
3399
+ { keyword: 'When', text: 'SetFitnessGoal', docString: { name: 'Lose weight' } },
3400
+ { keyword: 'Then', text: 'FitnessGoalCreated', docString: { goalId: '123' } },
3401
+ ],
3402
+ },
3403
+ ],
3404
+ },
3405
+ ],
3406
+ },
3407
+ ],
3408
+ },
3409
+ },
3410
+ ],
3411
+ sourceFile: '/narratives/goal-setting.narrative.ts',
3412
+ },
3413
+ ],
3414
+ messages: [
3415
+ { type: 'command', name: 'SetFitnessGoal', fields: [] },
3416
+ { type: 'event', name: 'FitnessGoalCreated', fields: [], source: 'internal' },
3417
+ { type: 'state', name: 'FitnessGoalsView', fields: [] },
3418
+ ],
3419
+ modules: [
3420
+ {
3421
+ sourceFile: '/narratives/goal-setting.narrative.ts',
3422
+ isDerived: false,
3423
+ contains: { narrativeIds: ['narrative-1'] },
3424
+ declares: {
3425
+ messages: [
3426
+ { kind: 'command', name: 'SetFitnessGoal' },
3427
+ { kind: 'event', name: 'FitnessGoalCreated' },
3428
+ { kind: 'state', name: 'FitnessGoalsView' },
3429
+ ],
3430
+ },
3431
+ },
3432
+ ],
3433
+ };
3434
+
3435
+ const result = await modelToNarrative(model);
3436
+ const code = getCode(result);
3437
+
3438
+ // All 3 declared types should be generated
3439
+ expect(code).toContain("type SetFitnessGoal = Command<'SetFitnessGoal'");
3440
+ expect(code).toContain("type FitnessGoalCreated = Event<'FitnessGoalCreated'");
3441
+ expect(code).toContain("type FitnessGoalsView = State<'FitnessGoalsView'");
3151
3442
  });
3152
3443
  });
@@ -29,6 +29,9 @@ export function jsonToExpr(
29
29
  case 'string':
30
30
  return f.createStringLiteral(v);
31
31
  case 'number':
32
+ if (v < 0) {
33
+ return f.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, f.createNumericLiteral(String(Math.abs(v))));
34
+ }
32
35
  return f.createNumericLiteral(String(v));
33
36
  case 'boolean':
34
37
  return v ? f.createTrue() : f.createFalse();
@@ -122,6 +122,31 @@ describe('buildGwtSpecBlock', () => {
122
122
 
123
123
  expect(code).toMatch(/rule\([^)]+\)\s*=>/);
124
124
  });
125
+
126
+ it('handles negative numbers in example data', () => {
127
+ const gwtBlock: GWTBlock & { ruleDescription: string; exampleDescription: string; ruleId: string } = {
128
+ when: {
129
+ commandRef: 'AdjustBalance',
130
+ exampleData: { accountId: 'acc-001', amount: -100, adjustment: -50.5 },
131
+ },
132
+ then: [
133
+ {
134
+ eventRef: 'BalanceAdjusted',
135
+ exampleData: { accountId: 'acc-001', newBalance: -150.5, change: -100 },
136
+ },
137
+ ],
138
+ ruleDescription: 'balance can be adjusted with negative amounts',
139
+ exampleDescription: 'adjusts balance with negative values',
140
+ ruleId: 'rNegNum01',
141
+ };
142
+
143
+ const result = buildGwtSpecBlock(ts, ts.factory, gwtBlock, 'command');
144
+ const code = printNode(result);
145
+
146
+ expect(code).toContain('-100');
147
+ expect(code).toContain('-50.5');
148
+ expect(code).toContain('-150.5');
149
+ });
125
150
  });
126
151
 
127
152
  describe('buildConsolidatedGwtSpecBlock', () => {
@@ -102,13 +102,22 @@ function generateModuleCode(
102
102
 
103
103
  const usedMessages = messages.filter((msg) => {
104
104
  const isImportedFromIntegration = usedTypeIntegrationNames.includes(msg.name);
105
- const isUsedInFlow = usageAnalysis.usedTypes.has(msg.name);
106
- const hasEmptyFlowSlices = narratives.length === 0 || narratives.every((flow) => flow.slices.length === 0);
107
105
 
106
+ // Don't generate local definitions for types imported from integrations
108
107
  if (isImportedFromIntegration) {
109
108
  return false;
110
109
  }
111
110
 
111
+ // For authored modules, trust the declares list - all declared messages should be generated
112
+ // (messages is already filtered to only include declared messages at line 60-61)
113
+ if (!module.isDerived) {
114
+ return true;
115
+ }
116
+
117
+ // For derived modules, only include types that are actually used in flow code
118
+ // or when there's no flow code (hasEmptyFlowSlices)
119
+ const isUsedInFlow = usageAnalysis.usedTypes.has(msg.name);
120
+ const hasEmptyFlowSlices = narratives.length === 0 || narratives.every((flow) => flow.slices.length === 0);
112
121
  return isUsedInFlow || hasEmptyFlowSlices;
113
122
  });
114
123