@startsimpli/funnels 0.4.6 → 0.4.7

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
@@ -38,22 +38,22 @@ export interface ExecutionResult<T = any> {
38
38
  excluded: FunnelResult<T>[];
39
39
 
40
40
  /** Total entities processed */
41
- total_input: number;
41
+ totalInput: number;
42
42
 
43
43
  /** Total entities that matched */
44
- total_matched: number;
44
+ totalMatched: number;
45
45
 
46
46
  /** Total entities excluded */
47
- total_excluded: number;
47
+ totalExcluded: number;
48
48
 
49
49
  /** Total entities tagged */
50
- total_tagged: number;
50
+ totalTagged: number;
51
51
 
52
52
  /** Per-stage statistics */
53
- stage_stats: Record<string, StageStats>;
53
+ stageStats: Record<string, StageStats>;
54
54
 
55
55
  /** Execution duration in milliseconds */
56
- duration_ms: number;
56
+ durationMs: number;
57
57
 
58
58
  /** Errors encountered (non-fatal) */
59
59
  errors?: string[];
@@ -68,10 +68,10 @@ export interface ExecutionResult<T = any> {
68
68
  function evaluateRule<T>(_entity: T, _rule: FilterRule): RuleResult {
69
69
  throw new Error('Not implemented - BEAD: fund-your-startup-a0b8. evaluateRule must import from rule evaluator.');
70
70
  // return {
71
- // field_path: _rule.field_path,
71
+ // fieldPath: _rule.fieldPath,
72
72
  // operator: _rule.operator,
73
73
  // value: _rule.value,
74
- // actual_value: undefined,
74
+ // actualValue: undefined,
75
75
  // matched: false,
76
76
  // error: 'Evaluator not implemented yet',
77
77
  // };
@@ -115,9 +115,9 @@ export class FunnelEngine<T = any> {
115
115
  const results: FunnelResult<T>[] = entities.map(entity => ({
116
116
  entity,
117
117
  matched: true, // Start as matched, exclude as needed
118
- accumulated_tags: [],
118
+ accumulatedTags: [],
119
119
  context: {},
120
- stage_results: [],
120
+ stageResults: [],
121
121
  }));
122
122
 
123
123
  const stageStats: Record<string, StageStats> = {};
@@ -131,18 +131,18 @@ export class FunnelEngine<T = any> {
131
131
  const stageStartTime = Date.now();
132
132
 
133
133
  // Track entities entering this stage
134
- const inputEntities = results.filter(r => r.matched && !r.excluded_at_stage);
134
+ const inputEntities = results.filter(r => r.matched && !r.excludedAtStage);
135
135
 
136
136
  const stats: StageStats = {
137
- stage_id: stage.id,
138
- stage_name: stage.name,
139
- input_count: inputEntities.length,
140
- matched_count: 0,
141
- not_matched_count: 0,
142
- excluded_count: 0,
143
- tagged_count: 0,
144
- continued_count: 0,
145
- error_count: 0,
137
+ stageId: stage.id,
138
+ stageName: stage.name,
139
+ inputCount: inputEntities.length,
140
+ matchedCount: 0,
141
+ notMatchedCount: 0,
142
+ excludedCount: 0,
143
+ taggedCount: 0,
144
+ continuedCount: 0,
145
+ errorCount: 0,
146
146
  };
147
147
 
148
148
  // Process each entity through this stage
@@ -151,46 +151,46 @@ export class FunnelEngine<T = any> {
151
151
  const stageResult = this.processStage(stage, result.entity);
152
152
 
153
153
  // Update entity result
154
- result.stage_results!.push(stageResult);
154
+ result.stageResults!.push(stageResult);
155
155
 
156
156
  // Apply stage result to entity
157
157
  if (stageResult.matched) {
158
- stats.matched_count++;
158
+ stats.matchedCount++;
159
159
  } else {
160
- stats.not_matched_count++;
160
+ stats.notMatchedCount++;
161
161
  }
162
162
 
163
- if (stageResult.tags_added && stageResult.tags_added.length > 0) {
164
- result.accumulated_tags.push(...stageResult.tags_added);
165
- stats.tagged_count++;
163
+ if (stageResult.tagsAdded && stageResult.tagsAdded.length > 0) {
164
+ result.accumulatedTags.push(...stageResult.tagsAdded);
165
+ stats.taggedCount++;
166
166
  }
167
167
 
168
- if (stageResult.context_added) {
169
- result.context = { ...result.context, ...stageResult.context_added };
168
+ if (stageResult.contextAdded) {
169
+ result.context = { ...result.context, ...stageResult.contextAdded };
170
170
  }
171
171
 
172
172
  if (stageResult.excluded) {
173
173
  result.matched = false;
174
- result.excluded_at_stage = stage.id;
175
- stats.excluded_count++;
174
+ result.excludedAtStage = stage.id;
175
+ stats.excludedCount++;
176
176
  } else if (stageResult.continued) {
177
- stats.continued_count++;
177
+ stats.continuedCount++;
178
178
  }
179
179
  } catch (error) {
180
- stats.error_count!++;
180
+ stats.errorCount!++;
181
181
  errors.push(`Stage ${stage.name}: ${error instanceof Error ? error.message : String(error)}`);
182
182
  }
183
183
  }
184
184
 
185
- stats.duration_ms = Date.now() - stageStartTime;
185
+ stats.durationMs = Date.now() - stageStartTime;
186
186
  stageStats[stage.id] = stats;
187
187
  }
188
188
 
189
189
  // Apply completion tags to matched entities
190
- if (funnel.completion_tags && funnel.completion_tags.length > 0) {
190
+ if (funnel.completionTags && funnel.completionTags.length > 0) {
191
191
  for (const result of results) {
192
192
  if (result.matched) {
193
- result.accumulated_tags.push(...funnel.completion_tags);
193
+ result.accumulatedTags.push(...funnel.completionTags);
194
194
  }
195
195
  }
196
196
  }
@@ -200,17 +200,17 @@ export class FunnelEngine<T = any> {
200
200
  const excluded = results.filter(r => !r.matched);
201
201
 
202
202
  // Count unique tagged entities
203
- const totalTagged = results.filter(r => r.accumulated_tags.length > 0).length;
203
+ const totalTagged = results.filter(r => r.accumulatedTags.length > 0).length;
204
204
 
205
205
  return {
206
206
  matched,
207
207
  excluded,
208
- total_input: entities.length,
209
- total_matched: matched.length,
210
- total_excluded: excluded.length,
211
- total_tagged: totalTagged,
212
- stage_stats: stageStats,
213
- duration_ms: Date.now() - startTime,
208
+ totalInput: entities.length,
209
+ totalMatched: matched.length,
210
+ totalExcluded: excluded.length,
211
+ totalTagged: totalTagged,
212
+ stageStats: stageStats,
213
+ durationMs: Date.now() - startTime,
214
214
  errors: errors.length > 0 ? errors : undefined,
215
215
  };
216
216
  }
@@ -227,10 +227,10 @@ export class FunnelEngine<T = any> {
227
227
  const ruleResults: RuleResult[] = [];
228
228
  let matched = false;
229
229
 
230
- if (stage.custom_evaluator) {
230
+ if (stage.customEvaluator) {
231
231
  // Use custom evaluator if provided (highest priority)
232
232
  try {
233
- matched = stage.custom_evaluator(entity);
233
+ matched = stage.customEvaluator(entity);
234
234
  } catch (error) {
235
235
  matched = false;
236
236
  }
@@ -245,7 +245,7 @@ export class FunnelEngine<T = any> {
245
245
  }
246
246
 
247
247
  // Apply filter logic (AND/OR)
248
- if (stage.filter_logic === 'AND') {
248
+ if (stage.filterLogic === 'AND') {
249
249
  // All rules must match
250
250
  matched = ruleResults.every(r => r.matched);
251
251
  } else {
@@ -262,20 +262,20 @@ export class FunnelEngine<T = any> {
262
262
  let continued = false;
263
263
 
264
264
  if (matched) {
265
- action = stage.match_action;
265
+ action = stage.matchAction;
266
266
 
267
267
  // Add match tags
268
- if (stage.match_tags && stage.match_tags.length > 0) {
269
- tagsAdded = [...stage.match_tags];
268
+ if (stage.matchTags && stage.matchTags.length > 0) {
269
+ tagsAdded = [...stage.matchTags];
270
270
  }
271
271
 
272
272
  // Add match context
273
- if (stage.match_context) {
274
- contextAdded = stage.match_context;
273
+ if (stage.matchContext) {
274
+ contextAdded = stage.matchContext;
275
275
  }
276
276
 
277
277
  // Apply match action
278
- switch (stage.match_action) {
278
+ switch (stage.matchAction) {
279
279
  case 'continue':
280
280
  continued = true;
281
281
  break;
@@ -293,15 +293,15 @@ export class FunnelEngine<T = any> {
293
293
  break;
294
294
  }
295
295
  } else {
296
- action = stage.no_match_action;
296
+ action = stage.noMatchAction;
297
297
 
298
298
  // Add no-match tags
299
- if (stage.no_match_tags && stage.no_match_tags.length > 0) {
300
- tagsAdded = [...stage.no_match_tags];
299
+ if (stage.noMatchTags && stage.noMatchTags.length > 0) {
300
+ tagsAdded = [...stage.noMatchTags];
301
301
  }
302
302
 
303
303
  // Apply no-match action
304
- switch (stage.no_match_action) {
304
+ switch (stage.noMatchAction) {
305
305
  case 'continue':
306
306
  continued = true;
307
307
  break;
@@ -315,13 +315,13 @@ export class FunnelEngine<T = any> {
315
315
  }
316
316
 
317
317
  return {
318
- stage_id: stage.id,
319
- stage_name: stage.name,
318
+ stageId: stage.id,
319
+ stageName: stage.name,
320
320
  matched,
321
- rule_results: ruleResults.length > 0 ? ruleResults : undefined,
321
+ ruleResults: ruleResults.length > 0 ? ruleResults : undefined,
322
322
  action,
323
- tags_added: tagsAdded.length > 0 ? tagsAdded : undefined,
324
- context_added: contextAdded,
323
+ tagsAdded: tagsAdded.length > 0 ? tagsAdded : undefined,
324
+ contextAdded: contextAdded,
325
325
  excluded,
326
326
  continued,
327
327
  };
@@ -27,7 +27,7 @@ interface Investor {
27
27
  geography: string[];
28
28
  };
29
29
  tags: string[];
30
- created_at: Date;
30
+ createdAt: Date;
31
31
  }
32
32
 
33
33
  const investors: Investor[] = [
@@ -41,7 +41,7 @@ const investors: Investor[] = [
41
41
  geography: ['US', 'Canada'],
42
42
  },
43
43
  tags: ['qualified', 'active'],
44
- created_at: new Date('2024-01-15'),
44
+ createdAt: new Date('2024-01-15'),
45
45
  },
46
46
  {
47
47
  name: 'Jane Smith',
@@ -53,7 +53,7 @@ const investors: Investor[] = [
53
53
  geography: ['US'],
54
54
  },
55
55
  tags: ['active'],
56
- created_at: new Date('2024-02-01'),
56
+ createdAt: new Date('2024-02-01'),
57
57
  },
58
58
  {
59
59
  name: 'Bob Johnson',
@@ -65,14 +65,14 @@ const investors: Investor[] = [
65
65
  geography: ['US', 'Europe', 'Asia'],
66
66
  },
67
67
  tags: ['qualified', 'premium'],
68
- created_at: new Date('2024-01-20'),
68
+ createdAt: new Date('2024-01-20'),
69
69
  },
70
70
  ];
71
71
 
72
72
  // Example 1a: Single rule evaluation
73
73
  console.log('=== Example 1a: Single Rule ===');
74
74
  const rule1: FilterRule = {
75
- field_path: 'firm.aum',
75
+ fieldPath: 'firm.aum',
76
76
  operator: 'gte',
77
77
  value: 100000000,
78
78
  };
@@ -85,9 +85,9 @@ investors.forEach(investor => {
85
85
  // Example 1b: Multiple rules with AND logic
86
86
  console.log('\n=== Example 1b: Multiple Rules (AND) ===');
87
87
  const rules1: FilterRule[] = [
88
- { field_path: 'firm.stage', operator: 'in', value: ['Series A', 'Series B'] },
89
- { field_path: 'firm.aum', operator: 'gte', value: 100000000 },
90
- { field_path: 'tags', operator: 'has_tag', value: 'qualified' },
88
+ { fieldPath: 'firm.stage', operator: 'in', value: ['Series A', 'Series B'] },
89
+ { fieldPath: 'firm.aum', operator: 'gte', value: 100000000 },
90
+ { fieldPath: 'tags', operator: 'has_tag', value: 'qualified' },
91
91
  ];
92
92
 
93
93
  const qualified = filterEntities(investors, rules1, 'AND');
@@ -99,9 +99,9 @@ const result1 = evaluateRulesWithResults(investors[0], rules1, 'AND');
99
99
  console.log('Evaluation for John Doe:');
100
100
  console.log('Overall match:', result1.matched);
101
101
  console.log('Rule results:');
102
- result1.rule_results.forEach(r => {
103
- console.log(` - ${r.field_path} ${r.operator} ${JSON.stringify(r.value)}`);
104
- console.log(` Actual: ${JSON.stringify(r.actual_value)}, Matched: ${r.matched}`);
102
+ result1.ruleResults.forEach(r => {
103
+ console.log(` - ${r.fieldPath} ${r.operator} ${JSON.stringify(r.value)}`);
104
+ console.log(` Actual: ${JSON.stringify(r.actualValue)}, Matched: ${r.matched}`);
105
105
  });
106
106
 
107
107
  // ============================================================================
@@ -151,7 +151,7 @@ const recipes: Recipe[] = [
151
151
  // Example 2a: Filter by dietary restrictions
152
152
  console.log('\n=== Example 2: Recipe Filtering ===');
153
153
  const veganRules: FilterRule[] = [
154
- { field_path: 'dietary', operator: 'has_tag', value: 'vegan' },
154
+ { fieldPath: 'dietary', operator: 'has_tag', value: 'vegan' },
155
155
  ];
156
156
 
157
157
  const veganRecipes = filterEntities(recipes, veganRules);
@@ -159,9 +159,9 @@ console.log('Vegan recipes:', veganRecipes.map(r => r.name));
159
159
 
160
160
  // Example 2b: Filter by multiple criteria (quick meals)
161
161
  const quickMealRules: FilterRule[] = [
162
- { field_path: 'prep_time_minutes', operator: 'lte', value: 30 },
163
- { field_path: 'difficulty', operator: 'in', value: ['easy', 'medium'] },
164
- { field_path: 'rating', operator: 'gte', value: 4.0 },
162
+ { fieldPath: 'prep_time_minutes', operator: 'lte', value: 30 },
163
+ { fieldPath: 'difficulty', operator: 'in', value: ['easy', 'medium'] },
164
+ { fieldPath: 'rating', operator: 'gte', value: 4.0 },
165
165
  ];
166
166
 
167
167
  const quickMeals = filterEntities(recipes, quickMealRules, 'AND');
@@ -222,9 +222,9 @@ const repos: GitHubRepo[] = [
222
222
  // Example 3a: Find popular active repos
223
223
  console.log('\n=== Example 3: GitHub Repos ===');
224
224
  const popularRepoRules: FilterRule[] = [
225
- { field_path: 'stars', operator: 'gte', value: 100 },
226
- { field_path: 'is_archived', operator: 'is_false', value: null },
227
- { field_path: 'last_updated', operator: 'gte', value: new Date('2024-01-01') },
225
+ { fieldPath: 'stars', operator: 'gte', value: 100 },
226
+ { fieldPath: 'is_archived', operator: 'is_false', value: null },
227
+ { fieldPath: 'last_updated', operator: 'gte', value: new Date('2024-01-01') },
228
228
  ];
229
229
 
230
230
  const popularRepos = filterEntities(repos, popularRepoRules, 'AND');
@@ -232,8 +232,8 @@ console.log('Popular active repos:', popularRepos.map(r => r.name));
232
232
 
233
233
  // Example 3b: Find TypeScript projects with React
234
234
  const tsReactRules: FilterRule[] = [
235
- { field_path: 'language', operator: 'eq', value: 'TypeScript' },
236
- { field_path: 'topics', operator: 'has_tag', value: 'react' },
235
+ { fieldPath: 'language', operator: 'eq', value: 'TypeScript' },
236
+ { fieldPath: 'topics', operator: 'has_tag', value: 'react' },
237
237
  ];
238
238
 
239
239
  const tsReactRepos = filterEntities(repos, tsReactRules, 'AND');
@@ -247,7 +247,7 @@ console.log('\n=== Example 4: Advanced Patterns ===');
247
247
 
248
248
  // Pattern 1: Negation
249
249
  const notArchivedRule: FilterRule = {
250
- field_path: 'is_archived',
250
+ fieldPath: 'is_archived',
251
251
  operator: 'is_true',
252
252
  value: null,
253
253
  negate: true, // Negate the result
@@ -258,7 +258,7 @@ console.log('Active repos (using negate):', activeRepos.map(r => r.name));
258
258
 
259
259
  // Pattern 2: Complex string matching
260
260
  const emailDomainRule: FilterRule = {
261
- field_path: 'email',
261
+ fieldPath: 'email',
262
262
  operator: 'matches',
263
263
  value: '@acme\\.vc$', // Matches emails ending with @acme.vc
264
264
  };
@@ -268,8 +268,8 @@ console.log('Acme VC investors:', acmeInvestors.map(i => i.name));
268
268
 
269
269
  // Pattern 3: OR logic (match any condition)
270
270
  const flexibleStageRules: FilterRule[] = [
271
- { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
272
- { field_path: 'firm.aum', operator: 'gte', value: 500000000 }, // OR very large fund
271
+ { fieldPath: 'firm.stage', operator: 'eq', value: 'Series A' },
272
+ { fieldPath: 'firm.aum', operator: 'gte', value: 500000000 }, // OR very large fund
273
273
  ];
274
274
 
275
275
  const flexibleMatch = filterEntities(investors, flexibleStageRules, 'OR');
@@ -282,11 +282,11 @@ investorsWithoutEmail.push({
282
282
  email: null as any,
283
283
  firm: { name: 'Test', stage: 'Seed', aum: 0, geography: [] },
284
284
  tags: [],
285
- created_at: new Date(),
285
+ createdAt: new Date(),
286
286
  });
287
287
 
288
288
  const hasEmailRule: FilterRule = {
289
- field_path: 'email',
289
+ fieldPath: 'email',
290
290
  operator: 'isnotnull',
291
291
  value: null,
292
292
  };
@@ -296,7 +296,7 @@ console.log('Investors with email:', withEmail.map(i => i.name));
296
296
 
297
297
  // Pattern 5: Array operations
298
298
  const multiGeographyRule: FilterRule = {
299
- field_path: 'firm.geography',
299
+ fieldPath: 'firm.geography',
300
300
  operator: 'has_all',
301
301
  value: ['US', 'Europe'],
302
302
  };
@@ -329,22 +329,22 @@ function simulateFunnel<T>(entities: T[], stages: Array<{ name: string; rules: F
329
329
  const investorFunnelStages = [
330
330
  {
331
331
  name: 'Active & Not Archived',
332
- rules: [{ field_path: 'tags', operator: 'has_tag', value: 'active' } as FilterRule],
332
+ rules: [{ fieldPath: 'tags', operator: 'has_tag', value: 'active' } as FilterRule],
333
333
  logic: 'AND' as const,
334
334
  },
335
335
  {
336
336
  name: 'Series A+ Stage',
337
- rules: [{ field_path: 'firm.stage', operator: 'in', value: ['Series A', 'Series B', 'Series C'] } as FilterRule],
337
+ rules: [{ fieldPath: 'firm.stage', operator: 'in', value: ['Series A', 'Series B', 'Series C'] } as FilterRule],
338
338
  logic: 'AND' as const,
339
339
  },
340
340
  {
341
341
  name: 'Minimum AUM $100M',
342
- rules: [{ field_path: 'firm.aum', operator: 'gte', value: 100000000 } as FilterRule],
342
+ rules: [{ fieldPath: 'firm.aum', operator: 'gte', value: 100000000 } as FilterRule],
343
343
  logic: 'AND' as const,
344
344
  },
345
345
  {
346
346
  name: 'Qualified Tag',
347
- rules: [{ field_path: 'tags', operator: 'has_tag', value: 'qualified' } as FilterRule],
347
+ rules: [{ fieldPath: 'tags', operator: 'has_tag', value: 'qualified' } as FilterRule],
348
348
  logic: 'AND' as const,
349
349
  },
350
350
  ];