@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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +5 -5
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +30 -0
- package/dist/src/transformers/model-to-narrative/ast/emit-helpers.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/ast/emit-helpers.js +3 -0
- package/dist/src/transformers/model-to-narrative/ast/emit-helpers.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/module-code.js +10 -2
- package/dist/src/transformers/model-to-narrative/generators/module-code.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/model-to-narrative.specs.ts +291 -0
- package/src/transformers/model-to-narrative/ast/emit-helpers.ts +3 -0
- package/src/transformers/model-to-narrative/generators/gwt.specs.ts +25 -0
- package/src/transformers/model-to-narrative/generators/module-code.ts +11 -2
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/
|
|
27
|
-
"@auto-engineer/
|
|
28
|
-
"@auto-engineer/
|
|
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.
|
|
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
|
|