@startsimpli/funnels 0.4.6 → 0.4.8

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 (46) hide show
  1. package/package.json +1 -1
  2. package/src/api/client.test.ts +76 -76
  3. package/src/api/client.ts +16 -16
  4. package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +24 -24
  5. package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +39 -39
  6. package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +1 -1
  7. package/src/components/FilterRuleEditor/RuleRow.tsx +8 -8
  8. package/src/components/FunnelCard/FunnelCard.stories.tsx +118 -118
  9. package/src/components/FunnelCard/FunnelCard.test.ts +48 -48
  10. package/src/components/FunnelCard/FunnelCard.test.tsx +40 -40
  11. package/src/components/FunnelCard/FunnelCard.tsx +3 -3
  12. package/src/components/FunnelPreview/FunnelPreview.test.tsx +63 -63
  13. package/src/components/FunnelPreview/FunnelPreview.tsx +11 -11
  14. package/src/components/FunnelPreview/StageBreakdown.tsx +5 -5
  15. package/src/components/FunnelPreview/example.tsx +43 -43
  16. package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +70 -70
  17. package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +63 -63
  18. package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +12 -12
  19. package/src/components/FunnelRunHistory/RunDetailsModal.tsx +5 -5
  20. package/src/components/FunnelRunHistory/RunFilters.tsx +8 -8
  21. package/src/components/FunnelRunHistory/RunRow.tsx +7 -7
  22. package/src/components/FunnelRunHistory/StageBreakdownList.tsx +9 -9
  23. package/src/components/FunnelRunHistory/types.ts +6 -6
  24. package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +41 -41
  25. package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +6 -6
  26. package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +5 -5
  27. package/src/components/FunnelStageBuilder/StageActions.test.tsx +7 -7
  28. package/src/components/FunnelStageBuilder/StageActions.tsx +4 -4
  29. package/src/components/FunnelStageBuilder/StageCard.tsx +3 -3
  30. package/src/components/FunnelStageBuilder/StageForm.tsx +14 -14
  31. package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +42 -42
  32. package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +31 -31
  33. package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +4 -4
  34. package/src/components/FunnelVisualFlow/StageNode.tsx +8 -8
  35. package/src/components/FunnelVisualFlow/example.tsx +7 -7
  36. package/src/core/engine.test.ts +271 -271
  37. package/src/core/engine.ts +60 -60
  38. package/src/core/evaluator.example.ts +31 -31
  39. package/src/core/evaluator.test.ts +92 -92
  40. package/src/core/evaluator.ts +23 -23
  41. package/src/core/field-resolver.example.ts +1 -1
  42. package/src/core/field-resolver.test.ts +4 -4
  43. package/src/store/create-funnel-store.test.ts +32 -32
  44. package/src/store/create-funnel-store.ts +3 -3
  45. package/src/store/types.ts +2 -2
  46. package/src/types/index.ts +64 -64
@@ -116,20 +116,20 @@ describe('FunnelEngine - Empty Funnel', () => {
116
116
  id: 'funnel-1',
117
117
  name: 'Empty Funnel',
118
118
  status: 'active',
119
- input_type: 'any',
119
+ inputType: 'any',
120
120
  stages: [],
121
- created_at: new Date().toISOString(),
122
- updated_at: new Date().toISOString(),
121
+ createdAt: new Date().toISOString(),
122
+ updatedAt: new Date().toISOString(),
123
123
  };
124
124
 
125
125
  const result = engine.execute(funnel, entities);
126
126
 
127
- expect(result.total_input).toBe(3);
128
- expect(result.total_matched).toBe(3); // No exclusions
129
- expect(result.total_excluded).toBe(0);
127
+ expect(result.totalInput).toBe(3);
128
+ expect(result.totalMatched).toBe(3); // No exclusions
129
+ expect(result.totalExcluded).toBe(0);
130
130
  expect(result.matched.length).toBe(3);
131
131
  expect(result.excluded.length).toBe(0);
132
- expect(Object.keys(result.stage_stats).length).toBe(0);
132
+ expect(Object.keys(result.stageStats).length).toBe(0);
133
133
  });
134
134
 
135
135
  it('should handle empty entity array', () => {
@@ -139,27 +139,27 @@ describe('FunnelEngine - Empty Funnel', () => {
139
139
  id: 'funnel-1',
140
140
  name: 'Test Funnel',
141
141
  status: 'active',
142
- input_type: 'any',
142
+ inputType: 'any',
143
143
  stages: [
144
144
  {
145
145
  id: 'stage-1',
146
146
  order: 0,
147
147
  name: 'Filter Stage',
148
- filter_logic: 'AND',
148
+ filterLogic: 'AND',
149
149
  rules: [],
150
- match_action: 'continue',
151
- no_match_action: 'exclude',
150
+ matchAction: 'continue',
151
+ noMatchAction: 'exclude',
152
152
  },
153
153
  ],
154
- created_at: new Date().toISOString(),
155
- updated_at: new Date().toISOString(),
154
+ createdAt: new Date().toISOString(),
155
+ updatedAt: new Date().toISOString(),
156
156
  };
157
157
 
158
158
  const result = engine.execute(funnel, []);
159
159
 
160
- expect(result.total_input).toBe(0);
161
- expect(result.total_matched).toBe(0);
162
- expect(result.total_excluded).toBe(0);
160
+ expect(result.totalInput).toBe(0);
161
+ expect(result.totalMatched).toBe(0);
162
+ expect(result.totalExcluded).toBe(0);
163
163
  });
164
164
  });
165
165
 
@@ -176,31 +176,31 @@ describe('FunnelEngine - Single Stage', () => {
176
176
  id: 'funnel-1',
177
177
  name: 'Single Stage Funnel',
178
178
  status: 'active',
179
- input_type: 'any',
179
+ inputType: 'any',
180
180
  stages: [
181
181
  {
182
182
  id: 'stage-1',
183
183
  order: 0,
184
184
  name: 'Pass All',
185
- filter_logic: 'AND',
185
+ filterLogic: 'AND',
186
186
  rules: [], // No rules = always match
187
- match_action: 'continue',
188
- no_match_action: 'exclude',
187
+ matchAction: 'continue',
188
+ noMatchAction: 'exclude',
189
189
  },
190
190
  ],
191
- created_at: new Date().toISOString(),
192
- updated_at: new Date().toISOString(),
191
+ createdAt: new Date().toISOString(),
192
+ updatedAt: new Date().toISOString(),
193
193
  };
194
194
 
195
195
  const result = engine.execute(funnel, entities);
196
196
 
197
- expect(result.total_matched).toBe(3);
198
- expect(result.stage_stats['stage-1'].input_count).toBe(3);
199
- expect(result.stage_stats['stage-1'].matched_count).toBe(3);
200
- expect(result.stage_stats['stage-1'].not_matched_count).toBe(0);
197
+ expect(result.totalMatched).toBe(3);
198
+ expect(result.stageStats['stage-1'].inputCount).toBe(3);
199
+ expect(result.stageStats['stage-1'].matchedCount).toBe(3);
200
+ expect(result.stageStats['stage-1'].notMatchedCount).toBe(0);
201
201
  });
202
202
 
203
- it('should apply match_action: output (stop processing)', () => {
203
+ it('should apply matchAction: output (stop processing)', () => {
204
204
  const engine = new FunnelEngine<MockInvestor>();
205
205
  const entities = createMockInvestors();
206
206
 
@@ -208,30 +208,30 @@ describe('FunnelEngine - Single Stage', () => {
208
208
  id: 'funnel-1',
209
209
  name: 'Output Stage',
210
210
  status: 'active',
211
- input_type: 'any',
211
+ inputType: 'any',
212
212
  stages: [
213
213
  {
214
214
  id: 'stage-1',
215
215
  order: 0,
216
216
  name: 'Output All',
217
- filter_logic: 'AND',
217
+ filterLogic: 'AND',
218
218
  rules: [],
219
- match_action: 'output', // Stop processing
220
- no_match_action: 'exclude',
219
+ matchAction: 'output', // Stop processing
220
+ noMatchAction: 'exclude',
221
221
  },
222
222
  ],
223
- created_at: new Date().toISOString(),
224
- updated_at: new Date().toISOString(),
223
+ createdAt: new Date().toISOString(),
224
+ updatedAt: new Date().toISOString(),
225
225
  };
226
226
 
227
227
  const result = engine.execute(funnel, entities);
228
228
 
229
- expect(result.total_matched).toBe(3);
230
- expect(result.matched.every(r => r.stage_results?.length === 1)).toBe(true);
231
- expect(result.matched.every(r => r.stage_results![0].continued === false)).toBe(true);
229
+ expect(result.totalMatched).toBe(3);
230
+ expect(result.matched.every(r => r.stageResults?.length === 1)).toBe(true);
231
+ expect(result.matched.every(r => r.stageResults![0].continued === false)).toBe(true);
232
232
  });
233
233
 
234
- it('should apply match_action: tag (add tags and exclude)', () => {
234
+ it('should apply matchAction: tag (add tags and exclude)', () => {
235
235
  const engine = new FunnelEngine<MockInvestor>();
236
236
  const entities = createMockInvestors();
237
237
 
@@ -239,31 +239,31 @@ describe('FunnelEngine - Single Stage', () => {
239
239
  id: 'funnel-1',
240
240
  name: 'Tag Stage',
241
241
  status: 'active',
242
- input_type: 'any',
242
+ inputType: 'any',
243
243
  stages: [
244
244
  {
245
245
  id: 'stage-1',
246
246
  order: 0,
247
247
  name: 'Tag All',
248
- filter_logic: 'AND',
248
+ filterLogic: 'AND',
249
249
  rules: [],
250
- match_action: 'tag',
251
- no_match_action: 'exclude',
252
- match_tags: ['qualified'],
250
+ matchAction: 'tag',
251
+ noMatchAction: 'exclude',
252
+ matchTags: ['qualified'],
253
253
  },
254
254
  ],
255
- created_at: new Date().toISOString(),
256
- updated_at: new Date().toISOString(),
255
+ createdAt: new Date().toISOString(),
256
+ updatedAt: new Date().toISOString(),
257
257
  };
258
258
 
259
259
  const result = engine.execute(funnel, entities);
260
260
 
261
- // Entities are tagged but excluded (match_action: tag stops processing)
262
- expect(result.total_excluded).toBe(3);
263
- expect(result.excluded.every(r => r.accumulated_tags.includes('qualified'))).toBe(true);
261
+ // Entities are tagged but excluded (matchAction: tag stops processing)
262
+ expect(result.totalExcluded).toBe(3);
263
+ expect(result.excluded.every(r => r.accumulatedTags.includes('qualified'))).toBe(true);
264
264
  });
265
265
 
266
- it('should apply match_action: tag_continue (add tags and continue)', () => {
266
+ it('should apply matchAction: tag_continue (add tags and continue)', () => {
267
267
  const engine = new FunnelEngine<MockInvestor>();
268
268
  const entities = createMockInvestors();
269
269
 
@@ -271,31 +271,31 @@ describe('FunnelEngine - Single Stage', () => {
271
271
  id: 'funnel-1',
272
272
  name: 'Tag Continue Stage',
273
273
  status: 'active',
274
- input_type: 'any',
274
+ inputType: 'any',
275
275
  stages: [
276
276
  {
277
277
  id: 'stage-1',
278
278
  order: 0,
279
279
  name: 'Tag and Continue',
280
- filter_logic: 'AND',
280
+ filterLogic: 'AND',
281
281
  rules: [],
282
- match_action: 'tag_continue',
283
- no_match_action: 'exclude',
284
- match_tags: ['processed'],
282
+ matchAction: 'tag_continue',
283
+ noMatchAction: 'exclude',
284
+ matchTags: ['processed'],
285
285
  },
286
286
  ],
287
- created_at: new Date().toISOString(),
288
- updated_at: new Date().toISOString(),
287
+ createdAt: new Date().toISOString(),
288
+ updatedAt: new Date().toISOString(),
289
289
  };
290
290
 
291
291
  const result = engine.execute(funnel, entities);
292
292
 
293
- expect(result.total_matched).toBe(3);
294
- expect(result.matched.every(r => r.accumulated_tags.includes('processed'))).toBe(true);
295
- expect(result.matched.every(r => r.stage_results![0].continued === true)).toBe(true);
293
+ expect(result.totalMatched).toBe(3);
294
+ expect(result.matched.every(r => r.accumulatedTags.includes('processed'))).toBe(true);
295
+ expect(result.matched.every(r => r.stageResults![0].continued === true)).toBe(true);
296
296
  });
297
297
 
298
- it('should apply no_match_action: exclude', () => {
298
+ it('should apply noMatchAction: exclude', () => {
299
299
  const engine = new FunnelEngine<MockInvestor>();
300
300
  const entities = createMockInvestors();
301
301
 
@@ -303,30 +303,30 @@ describe('FunnelEngine - Single Stage', () => {
303
303
  id: 'funnel-1',
304
304
  name: 'Exclude Stage',
305
305
  status: 'active',
306
- input_type: 'any',
306
+ inputType: 'any',
307
307
  stages: [
308
308
  {
309
309
  id: 'stage-1',
310
310
  order: 0,
311
311
  name: 'Never Match',
312
- filter_logic: 'AND',
312
+ filterLogic: 'AND',
313
313
  rules: [],
314
- match_action: 'continue',
315
- no_match_action: 'exclude',
316
- custom_evaluator: () => false, // Always false
314
+ matchAction: 'continue',
315
+ noMatchAction: 'exclude',
316
+ customEvaluator: () => false, // Always false
317
317
  },
318
318
  ],
319
- created_at: new Date().toISOString(),
320
- updated_at: new Date().toISOString(),
319
+ createdAt: new Date().toISOString(),
320
+ updatedAt: new Date().toISOString(),
321
321
  };
322
322
 
323
323
  const result = engine.execute(funnel, entities);
324
324
 
325
- expect(result.total_excluded).toBe(3);
326
- expect(result.excluded.every(r => r.excluded_at_stage === 'stage-1')).toBe(true);
325
+ expect(result.totalExcluded).toBe(3);
326
+ expect(result.excluded.every(r => r.excludedAtStage === 'stage-1')).toBe(true);
327
327
  });
328
328
 
329
- it('should apply no_match_action: tag_exclude', () => {
329
+ it('should apply noMatchAction: tag_exclude', () => {
330
330
  const engine = new FunnelEngine<MockInvestor>();
331
331
  const entities = createMockInvestors();
332
332
 
@@ -334,31 +334,31 @@ describe('FunnelEngine - Single Stage', () => {
334
334
  id: 'funnel-1',
335
335
  name: 'Tag Exclude Stage',
336
336
  status: 'active',
337
- input_type: 'any',
337
+ inputType: 'any',
338
338
  stages: [
339
339
  {
340
340
  id: 'stage-1',
341
341
  order: 0,
342
342
  name: 'Tag on No Match',
343
- filter_logic: 'AND',
343
+ filterLogic: 'AND',
344
344
  rules: [],
345
- match_action: 'continue',
346
- no_match_action: 'tag_exclude',
347
- no_match_tags: ['rejected'],
348
- custom_evaluator: () => false,
345
+ matchAction: 'continue',
346
+ noMatchAction: 'tag_exclude',
347
+ noMatchTags: ['rejected'],
348
+ customEvaluator: () => false,
349
349
  },
350
350
  ],
351
- created_at: new Date().toISOString(),
352
- updated_at: new Date().toISOString(),
351
+ createdAt: new Date().toISOString(),
352
+ updatedAt: new Date().toISOString(),
353
353
  };
354
354
 
355
355
  const result = engine.execute(funnel, entities);
356
356
 
357
- expect(result.total_excluded).toBe(3);
358
- expect(result.excluded.every(r => r.accumulated_tags.includes('rejected'))).toBe(true);
357
+ expect(result.totalExcluded).toBe(3);
358
+ expect(result.excluded.every(r => r.accumulatedTags.includes('rejected'))).toBe(true);
359
359
  });
360
360
 
361
- it('should add match_context when rules match', () => {
361
+ it('should add matchContext when rules match', () => {
362
362
  const engine = new FunnelEngine<MockInvestor>();
363
363
  const entities = createMockInvestors();
364
364
 
@@ -366,21 +366,21 @@ describe('FunnelEngine - Single Stage', () => {
366
366
  id: 'funnel-1',
367
367
  name: 'Context Stage',
368
368
  status: 'active',
369
- input_type: 'any',
369
+ inputType: 'any',
370
370
  stages: [
371
371
  {
372
372
  id: 'stage-1',
373
373
  order: 0,
374
374
  name: 'Add Context',
375
- filter_logic: 'AND',
375
+ filterLogic: 'AND',
376
376
  rules: [],
377
- match_action: 'continue',
378
- no_match_action: 'exclude',
379
- match_context: { tier: 'premium', score: 100 },
377
+ matchAction: 'continue',
378
+ noMatchAction: 'exclude',
379
+ matchContext: { tier: 'premium', score: 100 },
380
380
  },
381
381
  ],
382
- created_at: new Date().toISOString(),
383
- updated_at: new Date().toISOString(),
382
+ createdAt: new Date().toISOString(),
383
+ updatedAt: new Date().toISOString(),
384
384
  };
385
385
 
386
386
  const result = engine.execute(funnel, entities);
@@ -403,51 +403,51 @@ describe('FunnelEngine - Multi-Stage', () => {
403
403
  id: 'funnel-1',
404
404
  name: 'Multi-Stage Funnel',
405
405
  status: 'active',
406
- input_type: 'any',
406
+ inputType: 'any',
407
407
  stages: [
408
408
  {
409
409
  id: 'stage-1',
410
410
  order: 0,
411
411
  name: 'Stage 1',
412
- filter_logic: 'AND',
412
+ filterLogic: 'AND',
413
413
  rules: [],
414
- match_action: 'continue',
415
- no_match_action: 'exclude',
416
- match_tags: ['stage-1'],
414
+ matchAction: 'continue',
415
+ noMatchAction: 'exclude',
416
+ matchTags: ['stage-1'],
417
417
  },
418
418
  {
419
419
  id: 'stage-2',
420
420
  order: 1,
421
421
  name: 'Stage 2',
422
- filter_logic: 'AND',
422
+ filterLogic: 'AND',
423
423
  rules: [],
424
- match_action: 'continue',
425
- no_match_action: 'exclude',
426
- match_tags: ['stage-2'],
424
+ matchAction: 'continue',
425
+ noMatchAction: 'exclude',
426
+ matchTags: ['stage-2'],
427
427
  },
428
428
  {
429
429
  id: 'stage-3',
430
430
  order: 2,
431
431
  name: 'Stage 3',
432
- filter_logic: 'AND',
432
+ filterLogic: 'AND',
433
433
  rules: [],
434
- match_action: 'output',
435
- no_match_action: 'exclude',
436
- match_tags: ['stage-3'],
434
+ matchAction: 'output',
435
+ noMatchAction: 'exclude',
436
+ matchTags: ['stage-3'],
437
437
  },
438
438
  ],
439
- created_at: new Date().toISOString(),
440
- updated_at: new Date().toISOString(),
439
+ createdAt: new Date().toISOString(),
440
+ updatedAt: new Date().toISOString(),
441
441
  };
442
442
 
443
443
  const result = engine.execute(funnel, entities);
444
444
 
445
- expect(result.total_matched).toBe(3);
446
- expect(result.matched.every(r => r.stage_results?.length === 3)).toBe(true);
445
+ expect(result.totalMatched).toBe(3);
446
+ expect(result.matched.every(r => r.stageResults?.length === 3)).toBe(true);
447
447
  expect(result.matched.every(r =>
448
- r.accumulated_tags.includes('stage-1') &&
449
- r.accumulated_tags.includes('stage-2') &&
450
- r.accumulated_tags.includes('stage-3')
448
+ r.accumulatedTags.includes('stage-1') &&
449
+ r.accumulatedTags.includes('stage-2') &&
450
+ r.accumulatedTags.includes('stage-3')
451
451
  )).toBe(true);
452
452
  });
453
453
 
@@ -459,51 +459,51 @@ describe('FunnelEngine - Multi-Stage', () => {
459
459
  id: 'funnel-1',
460
460
  name: 'Filtering Funnel',
461
461
  status: 'active',
462
- input_type: 'any',
462
+ inputType: 'any',
463
463
  stages: [
464
464
  {
465
465
  id: 'stage-1',
466
466
  order: 0,
467
467
  name: 'Pass All',
468
- filter_logic: 'AND',
468
+ filterLogic: 'AND',
469
469
  rules: [],
470
- match_action: 'continue',
471
- no_match_action: 'exclude',
470
+ matchAction: 'continue',
471
+ noMatchAction: 'exclude',
472
472
  },
473
473
  {
474
474
  id: 'stage-2',
475
475
  order: 1,
476
476
  name: 'Filter Some',
477
- filter_logic: 'AND',
477
+ filterLogic: 'AND',
478
478
  rules: [],
479
- match_action: 'continue',
480
- no_match_action: 'exclude',
481
- custom_evaluator: (entity) => entity.id === '1' || entity.id === '2',
479
+ matchAction: 'continue',
480
+ noMatchAction: 'exclude',
481
+ customEvaluator: (entity) => entity.id === '1' || entity.id === '2',
482
482
  },
483
483
  {
484
484
  id: 'stage-3',
485
485
  order: 2,
486
486
  name: 'Final Filter',
487
- filter_logic: 'AND',
487
+ filterLogic: 'AND',
488
488
  rules: [],
489
- match_action: 'output',
490
- no_match_action: 'exclude',
491
- custom_evaluator: (entity) => entity.id === '1',
489
+ matchAction: 'output',
490
+ noMatchAction: 'exclude',
491
+ customEvaluator: (entity) => entity.id === '1',
492
492
  },
493
493
  ],
494
- created_at: new Date().toISOString(),
495
- updated_at: new Date().toISOString(),
494
+ createdAt: new Date().toISOString(),
495
+ updatedAt: new Date().toISOString(),
496
496
  };
497
497
 
498
498
  const result = engine.execute(funnel, entities);
499
499
 
500
- expect(result.total_matched).toBe(1);
500
+ expect(result.totalMatched).toBe(1);
501
501
  expect(result.matched[0].entity.id).toBe('1');
502
- expect(result.total_excluded).toBe(2);
502
+ expect(result.totalExcluded).toBe(2);
503
503
 
504
504
  // Check exclusion stages
505
- const excludedAtStage2 = result.excluded.find(r => r.excluded_at_stage === 'stage-2');
506
- const excludedAtStage3 = result.excluded.find(r => r.excluded_at_stage === 'stage-3');
505
+ const excludedAtStage2 = result.excluded.find(r => r.excludedAtStage === 'stage-2');
506
+ const excludedAtStage3 = result.excluded.find(r => r.excludedAtStage === 'stage-3');
507
507
 
508
508
  expect(excludedAtStage2?.entity.id).toBe('3');
509
509
  expect(excludedAtStage3?.entity.id).toBe('2');
@@ -520,47 +520,47 @@ describe('FunnelEngine - Multi-Stage', () => {
520
520
  id: 'funnel-1',
521
521
  name: 'Unsorted Stages',
522
522
  status: 'active',
523
- input_type: 'any',
523
+ inputType: 'any',
524
524
  stages: [
525
525
  {
526
526
  id: 'stage-3',
527
527
  order: 2,
528
528
  name: 'Third Stage',
529
- filter_logic: 'AND',
529
+ filterLogic: 'AND',
530
530
  rules: [],
531
- match_action: 'continue',
532
- no_match_action: 'exclude',
533
- match_tags: ['third'],
531
+ matchAction: 'continue',
532
+ noMatchAction: 'exclude',
533
+ matchTags: ['third'],
534
534
  },
535
535
  {
536
536
  id: 'stage-1',
537
537
  order: 0,
538
538
  name: 'First Stage',
539
- filter_logic: 'AND',
539
+ filterLogic: 'AND',
540
540
  rules: [],
541
- match_action: 'continue',
542
- no_match_action: 'exclude',
543
- match_tags: ['first'],
541
+ matchAction: 'continue',
542
+ noMatchAction: 'exclude',
543
+ matchTags: ['first'],
544
544
  },
545
545
  {
546
546
  id: 'stage-2',
547
547
  order: 1,
548
548
  name: 'Second Stage',
549
- filter_logic: 'AND',
549
+ filterLogic: 'AND',
550
550
  rules: [],
551
- match_action: 'continue',
552
- no_match_action: 'exclude',
553
- match_tags: ['second'],
551
+ matchAction: 'continue',
552
+ noMatchAction: 'exclude',
553
+ matchTags: ['second'],
554
554
  },
555
555
  ],
556
- created_at: new Date().toISOString(),
557
- updated_at: new Date().toISOString(),
556
+ createdAt: new Date().toISOString(),
557
+ updatedAt: new Date().toISOString(),
558
558
  };
559
559
 
560
560
  const result = engine.execute(funnel, entities);
561
561
 
562
562
  // Tags should be added in order: first, second, third
563
- expect(result.matched[0].accumulated_tags).toEqual(['first', 'second', 'third']);
563
+ expect(result.matched[0].accumulatedTags).toEqual(['first', 'second', 'third']);
564
564
  });
565
565
  });
566
566
 
@@ -581,29 +581,29 @@ describe('FunnelEngine - AND Logic', () => {
581
581
  id: 'funnel-1',
582
582
  name: 'AND Logic Funnel',
583
583
  status: 'active',
584
- input_type: 'any',
584
+ inputType: 'any',
585
585
  stages: [
586
586
  {
587
587
  id: 'stage-1',
588
588
  order: 0,
589
589
  name: 'AND Stage',
590
- filter_logic: 'AND',
590
+ filterLogic: 'AND',
591
591
  rules: [
592
- { field_path: 'value', operator: 'gte', value: 20 },
593
- { field_path: 'active', operator: 'is_true', value: null },
592
+ { fieldPath: 'value', operator: 'gte', value: 20 },
593
+ { fieldPath: 'active', operator: 'is_true', value: null },
594
594
  ],
595
- match_action: 'output',
596
- no_match_action: 'exclude',
595
+ matchAction: 'output',
596
+ noMatchAction: 'exclude',
597
597
  },
598
598
  ],
599
- created_at: new Date().toISOString(),
600
- updated_at: new Date().toISOString(),
599
+ createdAt: new Date().toISOString(),
600
+ updatedAt: new Date().toISOString(),
601
601
  };
602
602
 
603
- // Mock rule evaluator to use custom_evaluator
603
+ // Mock rule evaluator to use customEvaluator
604
604
  const stageWithEvaluator: FunnelStage<GenericEntity> = {
605
605
  ...funnel.stages[0],
606
- custom_evaluator: (entity) => entity.value >= 20 && entity.active === true,
606
+ customEvaluator: (entity) => entity.value >= 20 && entity.active === true,
607
607
  };
608
608
 
609
609
  funnel.stages[0] = stageWithEvaluator;
@@ -611,7 +611,7 @@ describe('FunnelEngine - AND Logic', () => {
611
611
  const result = engine.execute(funnel, entities);
612
612
 
613
613
  // Only entity 3 matches both conditions
614
- expect(result.total_matched).toBe(1);
614
+ expect(result.totalMatched).toBe(1);
615
615
  expect(result.matched[0].entity.id).toBe('3');
616
616
  });
617
617
  });
@@ -633,29 +633,29 @@ describe('FunnelEngine - OR Logic', () => {
633
633
  id: 'funnel-1',
634
634
  name: 'OR Logic Funnel',
635
635
  status: 'active',
636
- input_type: 'any',
636
+ inputType: 'any',
637
637
  stages: [
638
638
  {
639
639
  id: 'stage-1',
640
640
  order: 0,
641
641
  name: 'OR Stage',
642
- filter_logic: 'OR',
642
+ filterLogic: 'OR',
643
643
  rules: [
644
- { field_path: 'value', operator: 'gte', value: 20 },
645
- { field_path: 'active', operator: 'is_true', value: null },
644
+ { fieldPath: 'value', operator: 'gte', value: 20 },
645
+ { fieldPath: 'active', operator: 'is_true', value: null },
646
646
  ],
647
- match_action: 'output',
648
- no_match_action: 'exclude',
647
+ matchAction: 'output',
648
+ noMatchAction: 'exclude',
649
649
  },
650
650
  ],
651
- created_at: new Date().toISOString(),
652
- updated_at: new Date().toISOString(),
651
+ createdAt: new Date().toISOString(),
652
+ updatedAt: new Date().toISOString(),
653
653
  };
654
654
 
655
655
  // Mock rule evaluator
656
656
  const stageWithEvaluator: FunnelStage<GenericEntity> = {
657
657
  ...funnel.stages[0],
658
- custom_evaluator: (entity) => entity.value >= 20 || entity.active === true,
658
+ customEvaluator: (entity) => entity.value >= 20 || entity.active === true,
659
659
  };
660
660
 
661
661
  funnel.stages[0] = stageWithEvaluator;
@@ -663,7 +663,7 @@ describe('FunnelEngine - OR Logic', () => {
663
663
  const result = engine.execute(funnel, entities);
664
664
 
665
665
  // Entities 1 and 2 match (1 has active=true, 2 has value>=20)
666
- expect(result.total_matched).toBe(2);
666
+ expect(result.totalMatched).toBe(2);
667
667
  expect(result.matched.map(r => r.entity.id).sort()).toEqual(['1', '2']);
668
668
  });
669
669
  });
@@ -681,36 +681,36 @@ describe('FunnelEngine - Tags', () => {
681
681
  id: 'funnel-1',
682
682
  name: 'Tag Accumulation',
683
683
  status: 'active',
684
- input_type: 'any',
684
+ inputType: 'any',
685
685
  stages: [
686
686
  {
687
687
  id: 'stage-1',
688
688
  order: 0,
689
689
  name: 'Stage 1',
690
- filter_logic: 'AND',
690
+ filterLogic: 'AND',
691
691
  rules: [],
692
- match_action: 'continue',
693
- no_match_action: 'exclude',
694
- match_tags: ['tag-1', 'tag-2'],
692
+ matchAction: 'continue',
693
+ noMatchAction: 'exclude',
694
+ matchTags: ['tag-1', 'tag-2'],
695
695
  },
696
696
  {
697
697
  id: 'stage-2',
698
698
  order: 1,
699
699
  name: 'Stage 2',
700
- filter_logic: 'AND',
700
+ filterLogic: 'AND',
701
701
  rules: [],
702
- match_action: 'continue',
703
- no_match_action: 'exclude',
704
- match_tags: ['tag-3'],
702
+ matchAction: 'continue',
703
+ noMatchAction: 'exclude',
704
+ matchTags: ['tag-3'],
705
705
  },
706
706
  ],
707
- created_at: new Date().toISOString(),
708
- updated_at: new Date().toISOString(),
707
+ createdAt: new Date().toISOString(),
708
+ updatedAt: new Date().toISOString(),
709
709
  };
710
710
 
711
711
  const result = engine.execute(funnel, entities);
712
712
 
713
- expect(result.matched[0].accumulated_tags).toEqual(['tag-1', 'tag-2', 'tag-3']);
713
+ expect(result.matched[0].accumulatedTags).toEqual(['tag-1', 'tag-2', 'tag-3']);
714
714
  });
715
715
 
716
716
  it('should apply completion tags to matched entities', () => {
@@ -724,29 +724,29 @@ describe('FunnelEngine - Tags', () => {
724
724
  id: 'funnel-1',
725
725
  name: 'Completion Tags',
726
726
  status: 'active',
727
- input_type: 'any',
727
+ inputType: 'any',
728
728
  stages: [
729
729
  {
730
730
  id: 'stage-1',
731
731
  order: 0,
732
732
  name: 'Filter',
733
- filter_logic: 'AND',
733
+ filterLogic: 'AND',
734
734
  rules: [],
735
- match_action: 'output',
736
- no_match_action: 'exclude',
737
- custom_evaluator: (entity) => entity.value >= 20,
735
+ matchAction: 'output',
736
+ noMatchAction: 'exclude',
737
+ customEvaluator: (entity) => entity.value >= 20,
738
738
  },
739
739
  ],
740
- completion_tags: ['completed', 'qualified'],
741
- created_at: new Date().toISOString(),
742
- updated_at: new Date().toISOString(),
740
+ completionTags: ['completed', 'qualified'],
741
+ createdAt: new Date().toISOString(),
742
+ updatedAt: new Date().toISOString(),
743
743
  };
744
744
 
745
745
  const result = engine.execute(funnel, entities);
746
746
 
747
- expect(result.total_matched).toBe(1);
748
- expect(result.matched[0].accumulated_tags).toContain('completed');
749
- expect(result.matched[0].accumulated_tags).toContain('qualified');
747
+ expect(result.totalMatched).toBe(1);
748
+ expect(result.matched[0].accumulatedTags).toContain('completed');
749
+ expect(result.matched[0].accumulatedTags).toContain('qualified');
750
750
  });
751
751
 
752
752
  it('should count tagged entities correctly', () => {
@@ -761,27 +761,27 @@ describe('FunnelEngine - Tags', () => {
761
761
  id: 'funnel-1',
762
762
  name: 'Tag Counting',
763
763
  status: 'active',
764
- input_type: 'any',
764
+ inputType: 'any',
765
765
  stages: [
766
766
  {
767
767
  id: 'stage-1',
768
768
  order: 0,
769
769
  name: 'Tag Some',
770
- filter_logic: 'AND',
770
+ filterLogic: 'AND',
771
771
  rules: [],
772
- match_action: 'continue',
773
- no_match_action: 'continue',
774
- match_tags: ['tagged'],
775
- custom_evaluator: (entity) => entity.value >= 20,
772
+ matchAction: 'continue',
773
+ noMatchAction: 'continue',
774
+ matchTags: ['tagged'],
775
+ customEvaluator: (entity) => entity.value >= 20,
776
776
  },
777
777
  ],
778
- created_at: new Date().toISOString(),
779
- updated_at: new Date().toISOString(),
778
+ createdAt: new Date().toISOString(),
779
+ updatedAt: new Date().toISOString(),
780
780
  };
781
781
 
782
782
  const result = engine.execute(funnel, entities);
783
783
 
784
- expect(result.total_tagged).toBe(2); // Entities 2 and 3
784
+ expect(result.totalTagged).toBe(2); // Entities 2 and 3
785
785
  });
786
786
  });
787
787
 
@@ -802,40 +802,40 @@ describe('FunnelEngine - Exclusions', () => {
802
802
  id: 'funnel-1',
803
803
  name: 'Exclusion Tracking',
804
804
  status: 'active',
805
- input_type: 'any',
805
+ inputType: 'any',
806
806
  stages: [
807
807
  {
808
808
  id: 'stage-1',
809
809
  order: 0,
810
810
  name: 'Stage 1',
811
- filter_logic: 'AND',
811
+ filterLogic: 'AND',
812
812
  rules: [],
813
- match_action: 'continue',
814
- no_match_action: 'exclude',
815
- custom_evaluator: (entity) => entity.value >= 20,
813
+ matchAction: 'continue',
814
+ noMatchAction: 'exclude',
815
+ customEvaluator: (entity) => entity.value >= 20,
816
816
  },
817
817
  {
818
818
  id: 'stage-2',
819
819
  order: 1,
820
820
  name: 'Stage 2',
821
- filter_logic: 'AND',
821
+ filterLogic: 'AND',
822
822
  rules: [],
823
- match_action: 'output',
824
- no_match_action: 'exclude',
825
- custom_evaluator: (entity) => entity.value >= 30,
823
+ matchAction: 'output',
824
+ noMatchAction: 'exclude',
825
+ customEvaluator: (entity) => entity.value >= 30,
826
826
  },
827
827
  ],
828
- created_at: new Date().toISOString(),
829
- updated_at: new Date().toISOString(),
828
+ createdAt: new Date().toISOString(),
829
+ updatedAt: new Date().toISOString(),
830
830
  };
831
831
 
832
832
  const result = engine.execute(funnel, entities);
833
833
 
834
- expect(result.total_matched).toBe(1);
834
+ expect(result.totalMatched).toBe(1);
835
835
  expect(result.matched[0].entity.id).toBe('3');
836
836
 
837
- const excludedAtStage1 = result.excluded.find(r => r.excluded_at_stage === 'stage-1');
838
- const excludedAtStage2 = result.excluded.find(r => r.excluded_at_stage === 'stage-2');
837
+ const excludedAtStage1 = result.excluded.find(r => r.excludedAtStage === 'stage-1');
838
+ const excludedAtStage2 = result.excluded.find(r => r.excludedAtStage === 'stage-2');
839
839
 
840
840
  expect(excludedAtStage1?.entity.id).toBe('1');
841
841
  expect(excludedAtStage2?.entity.id).toBe('2');
@@ -849,37 +849,37 @@ describe('FunnelEngine - Exclusions', () => {
849
849
  id: 'funnel-1',
850
850
  name: 'Stop After Exclusion',
851
851
  status: 'active',
852
- input_type: 'any',
852
+ inputType: 'any',
853
853
  stages: [
854
854
  {
855
855
  id: 'stage-1',
856
856
  order: 0,
857
857
  name: 'Exclude',
858
- filter_logic: 'AND',
858
+ filterLogic: 'AND',
859
859
  rules: [],
860
- match_action: 'continue',
861
- no_match_action: 'exclude',
862
- custom_evaluator: () => false,
860
+ matchAction: 'continue',
861
+ noMatchAction: 'exclude',
862
+ customEvaluator: () => false,
863
863
  },
864
864
  {
865
865
  id: 'stage-2',
866
866
  order: 1,
867
867
  name: 'Should Not Reach',
868
- filter_logic: 'AND',
868
+ filterLogic: 'AND',
869
869
  rules: [],
870
- match_action: 'output',
871
- no_match_action: 'exclude',
872
- match_tags: ['unreachable'],
870
+ matchAction: 'output',
871
+ noMatchAction: 'exclude',
872
+ matchTags: ['unreachable'],
873
873
  },
874
874
  ],
875
- created_at: new Date().toISOString(),
876
- updated_at: new Date().toISOString(),
875
+ createdAt: new Date().toISOString(),
876
+ updatedAt: new Date().toISOString(),
877
877
  };
878
878
 
879
879
  const result = engine.execute(funnel, entities);
880
880
 
881
- expect(result.excluded[0].stage_results?.length).toBe(1); // Only stage-1
882
- expect(result.excluded[0].accumulated_tags).not.toContain('unreachable');
881
+ expect(result.excluded[0].stageResults?.length).toBe(1); // Only stage-1
882
+ expect(result.excluded[0].accumulatedTags).not.toContain('unreachable');
883
883
  });
884
884
  });
885
885
 
@@ -900,35 +900,35 @@ describe('FunnelEngine - Statistics', () => {
900
900
  id: 'funnel-1',
901
901
  name: 'Stats Funnel',
902
902
  status: 'active',
903
- input_type: 'any',
903
+ inputType: 'any',
904
904
  stages: [
905
905
  {
906
906
  id: 'stage-1',
907
907
  order: 0,
908
908
  name: 'Filter Stage',
909
- filter_logic: 'AND',
909
+ filterLogic: 'AND',
910
910
  rules: [],
911
- match_action: 'continue',
912
- no_match_action: 'exclude',
913
- match_tags: ['qualified'],
914
- custom_evaluator: (entity) => entity.value >= 20,
911
+ matchAction: 'continue',
912
+ noMatchAction: 'exclude',
913
+ matchTags: ['qualified'],
914
+ customEvaluator: (entity) => entity.value >= 20,
915
915
  },
916
916
  ],
917
- created_at: new Date().toISOString(),
918
- updated_at: new Date().toISOString(),
917
+ createdAt: new Date().toISOString(),
918
+ updatedAt: new Date().toISOString(),
919
919
  };
920
920
 
921
921
  const result = engine.execute(funnel, entities);
922
922
 
923
- const stats = result.stage_stats['stage-1'];
923
+ const stats = result.stageStats['stage-1'];
924
924
 
925
- expect(stats.input_count).toBe(3);
926
- expect(stats.matched_count).toBe(2);
927
- expect(stats.not_matched_count).toBe(1);
928
- expect(stats.excluded_count).toBe(1);
929
- expect(stats.tagged_count).toBe(2);
930
- expect(stats.continued_count).toBe(2);
931
- expect(stats.duration_ms).toBeGreaterThanOrEqual(0);
925
+ expect(stats.inputCount).toBe(3);
926
+ expect(stats.matchedCount).toBe(2);
927
+ expect(stats.notMatchedCount).toBe(1);
928
+ expect(stats.excludedCount).toBe(1);
929
+ expect(stats.taggedCount).toBe(2);
930
+ expect(stats.continuedCount).toBe(2);
931
+ expect(stats.durationMs).toBeGreaterThanOrEqual(0);
932
932
  });
933
933
 
934
934
  it('should track execution duration', () => {
@@ -939,26 +939,26 @@ describe('FunnelEngine - Statistics', () => {
939
939
  id: 'funnel-1',
940
940
  name: 'Duration Test',
941
941
  status: 'active',
942
- input_type: 'any',
942
+ inputType: 'any',
943
943
  stages: [
944
944
  {
945
945
  id: 'stage-1',
946
946
  order: 0,
947
947
  name: 'Test Stage',
948
- filter_logic: 'AND',
948
+ filterLogic: 'AND',
949
949
  rules: [],
950
- match_action: 'output',
951
- no_match_action: 'exclude',
950
+ matchAction: 'output',
951
+ noMatchAction: 'exclude',
952
952
  },
953
953
  ],
954
- created_at: new Date().toISOString(),
955
- updated_at: new Date().toISOString(),
954
+ createdAt: new Date().toISOString(),
955
+ updatedAt: new Date().toISOString(),
956
956
  };
957
957
 
958
958
  const result = engine.execute(funnel, entities);
959
959
 
960
- expect(result.duration_ms).toBeGreaterThanOrEqual(0);
961
- expect(typeof result.duration_ms).toBe('number');
960
+ expect(result.durationMs).toBeGreaterThanOrEqual(0);
961
+ expect(typeof result.durationMs).toBe('number');
962
962
  });
963
963
  });
964
964
 
@@ -967,7 +967,7 @@ describe('FunnelEngine - Statistics', () => {
967
967
  // ============================================================================
968
968
 
969
969
  describe('FunnelEngine - Custom Evaluators', () => {
970
- it('should use custom_evaluator when provided', () => {
970
+ it('should use customEvaluator when provided', () => {
971
971
  const engine = new FunnelEngine<MockRecipe>();
972
972
  const entities = createMockRecipes();
973
973
 
@@ -975,30 +975,30 @@ describe('FunnelEngine - Custom Evaluators', () => {
975
975
  id: 'funnel-1',
976
976
  name: 'Custom Eval Funnel',
977
977
  status: 'active',
978
- input_type: 'any',
978
+ inputType: 'any',
979
979
  stages: [
980
980
  {
981
981
  id: 'stage-1',
982
982
  order: 0,
983
983
  name: 'Quick Recipes',
984
- filter_logic: 'AND',
984
+ filterLogic: 'AND',
985
985
  rules: [],
986
- match_action: 'output',
987
- no_match_action: 'exclude',
988
- custom_evaluator: (recipe) => recipe.prep_time_minutes <= 45,
986
+ matchAction: 'output',
987
+ noMatchAction: 'exclude',
988
+ customEvaluator: (recipe) => recipe.prep_time_minutes <= 45,
989
989
  },
990
990
  ],
991
- created_at: new Date().toISOString(),
992
- updated_at: new Date().toISOString(),
991
+ createdAt: new Date().toISOString(),
992
+ updatedAt: new Date().toISOString(),
993
993
  };
994
994
 
995
995
  const result = engine.execute(funnel, entities);
996
996
 
997
- expect(result.total_matched).toBe(2); // Pasta and Curry
997
+ expect(result.totalMatched).toBe(2); // Pasta and Curry
998
998
  expect(result.matched.every(r => r.entity.prep_time_minutes <= 45)).toBe(true);
999
999
  });
1000
1000
 
1001
- it('should handle custom_evaluator errors gracefully', () => {
1001
+ it('should handle customEvaluator errors gracefully', () => {
1002
1002
  const engine = new FunnelEngine<GenericEntity>();
1003
1003
  const entities: GenericEntity[] = [{ id: '1', value: 10 }];
1004
1004
 
@@ -1006,30 +1006,30 @@ describe('FunnelEngine - Custom Evaluators', () => {
1006
1006
  id: 'funnel-1',
1007
1007
  name: 'Error Handling',
1008
1008
  status: 'active',
1009
- input_type: 'any',
1009
+ inputType: 'any',
1010
1010
  stages: [
1011
1011
  {
1012
1012
  id: 'stage-1',
1013
1013
  order: 0,
1014
1014
  name: 'Throw Error',
1015
- filter_logic: 'AND',
1015
+ filterLogic: 'AND',
1016
1016
  rules: [],
1017
- match_action: 'output',
1018
- no_match_action: 'exclude',
1019
- custom_evaluator: () => {
1017
+ matchAction: 'output',
1018
+ noMatchAction: 'exclude',
1019
+ customEvaluator: () => {
1020
1020
  throw new Error('Evaluator failed');
1021
1021
  },
1022
1022
  },
1023
1023
  ],
1024
- created_at: new Date().toISOString(),
1025
- updated_at: new Date().toISOString(),
1024
+ createdAt: new Date().toISOString(),
1025
+ updatedAt: new Date().toISOString(),
1026
1026
  };
1027
1027
 
1028
1028
  const result = engine.execute(funnel, entities);
1029
1029
 
1030
1030
  // Should treat error as no match
1031
- expect(result.total_matched).toBe(0);
1032
- expect(result.total_excluded).toBe(1);
1031
+ expect(result.totalMatched).toBe(0);
1032
+ expect(result.totalExcluded).toBe(1);
1033
1033
  });
1034
1034
  });
1035
1035
 
@@ -1062,25 +1062,25 @@ describe('FunnelEngine - Generic Entity Types', () => {
1062
1062
  id: 'funnel-1',
1063
1063
  name: 'Arbitrary Funnel',
1064
1064
  status: 'active',
1065
- input_type: 'any',
1065
+ inputType: 'any',
1066
1066
  stages: [
1067
1067
  {
1068
1068
  id: 'stage-1',
1069
1069
  order: 0,
1070
1070
  name: 'Test Stage',
1071
- filter_logic: 'AND',
1071
+ filterLogic: 'AND',
1072
1072
  rules: [],
1073
- match_action: 'output',
1074
- no_match_action: 'exclude',
1073
+ matchAction: 'output',
1074
+ noMatchAction: 'exclude',
1075
1075
  },
1076
1076
  ],
1077
- created_at: new Date().toISOString(),
1078
- updated_at: new Date().toISOString(),
1077
+ createdAt: new Date().toISOString(),
1078
+ updatedAt: new Date().toISOString(),
1079
1079
  };
1080
1080
 
1081
1081
  const result = engine.execute(funnel, entities);
1082
1082
 
1083
- expect(result.total_matched).toBe(1);
1083
+ expect(result.totalMatched).toBe(1);
1084
1084
  expect(result.matched[0].entity.uuid).toBe('abc-123');
1085
1085
  expect(result.matched[0].entity.nested.deep.value).toBe(42);
1086
1086
  });