@startsimpli/funnels 0.1.4 → 0.1.6

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 (151) hide show
  1. package/package.json +9 -31
  2. package/src/api/README.md +507 -0
  3. package/src/api/adapter.ts +106 -0
  4. package/src/api/client.test.ts +640 -0
  5. package/src/api/client.ts +385 -0
  6. package/src/api/default-adapter.ts +243 -0
  7. package/src/api/index.ts +24 -0
  8. package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
  9. package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
  10. package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
  11. package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
  12. package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
  13. package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
  14. package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
  15. package/src/components/FilterRuleEditor/README.md +291 -0
  16. package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
  17. package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
  18. package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
  19. package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
  20. package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
  21. package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
  22. package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
  23. package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
  24. package/src/components/FilterRuleEditor/constants.ts +64 -0
  25. package/src/components/FilterRuleEditor/index.ts +14 -0
  26. package/src/components/FunnelCard/DESIGN.md +447 -0
  27. package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
  28. package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
  29. package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
  30. package/src/components/FunnelCard/FunnelCard.tsx +204 -0
  31. package/src/components/FunnelCard/FunnelStats.tsx +68 -0
  32. package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
  33. package/src/components/FunnelCard/INSTALLATION.md +304 -0
  34. package/src/components/FunnelCard/MatchBar.tsx +49 -0
  35. package/src/components/FunnelCard/README.md +294 -0
  36. package/src/components/FunnelCard/StageIndicator.tsx +62 -0
  37. package/src/components/FunnelCard/StatusBadge.tsx +52 -0
  38. package/src/components/FunnelCard/index.ts +14 -0
  39. package/src/components/FunnelPreview/EntityCard.tsx +72 -0
  40. package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
  41. package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
  42. package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
  43. package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
  44. package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
  45. package/src/components/FunnelPreview/README.md +337 -0
  46. package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
  47. package/src/components/FunnelPreview/example.tsx +286 -0
  48. package/src/components/FunnelPreview/index.ts +14 -0
  49. package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
  50. package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
  51. package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
  52. package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
  53. package/src/components/FunnelRunHistory/README.md +325 -0
  54. package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
  55. package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
  56. package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
  57. package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
  58. package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
  59. package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
  60. package/src/components/FunnelRunHistory/index.ts +51 -0
  61. package/src/components/FunnelRunHistory/types.ts +40 -0
  62. package/src/components/FunnelRunHistory/utils.test.ts +126 -0
  63. package/src/components/FunnelRunHistory/utils.ts +100 -0
  64. package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
  65. package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
  66. package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
  67. package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
  68. package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
  69. package/src/components/FunnelStageBuilder/README.md +341 -0
  70. package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
  71. package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
  72. package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
  73. package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
  74. package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
  75. package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
  76. package/src/components/FunnelStageBuilder/index.ts +21 -0
  77. package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
  78. package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
  79. package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
  80. package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
  81. package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
  82. package/src/components/FunnelVisualFlow/README.md +323 -0
  83. package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
  84. package/src/components/FunnelVisualFlow/example.tsx +227 -0
  85. package/src/components/FunnelVisualFlow/index.ts +10 -0
  86. package/src/components/index.ts +102 -0
  87. package/src/core/README.md +307 -0
  88. package/src/core/engine.test.ts +1087 -0
  89. package/src/core/engine.ts +329 -0
  90. package/src/core/evaluator.example.ts +353 -0
  91. package/src/core/evaluator.test.ts +639 -0
  92. package/src/core/evaluator.ts +261 -0
  93. package/src/core/field-resolver.example.ts +175 -0
  94. package/src/core/field-resolver.test.ts +541 -0
  95. package/src/core/field-resolver.ts +247 -0
  96. package/src/core/index.ts +34 -0
  97. package/src/core/operators.test.ts +539 -0
  98. package/src/core/operators.ts +241 -0
  99. package/src/hooks/index.ts +5 -0
  100. package/src/hooks/useDebouncedValue.ts +28 -0
  101. package/src/index.ts +155 -0
  102. package/src/store/README.md +342 -0
  103. package/src/store/create-funnel-store.test.ts +686 -0
  104. package/src/store/create-funnel-store.ts +538 -0
  105. package/src/store/index.ts +9 -0
  106. package/src/store/types.ts +294 -0
  107. package/src/stories/CrossDomain.stories.tsx +149 -0
  108. package/src/stories/Welcome.stories.tsx +81 -0
  109. package/src/stories/demo-data/index.ts +3 -0
  110. package/src/stories/demo-data/investors.ts +216 -0
  111. package/src/stories/demo-data/leads.ts +223 -0
  112. package/src/stories/demo-data/recipes.ts +217 -0
  113. package/src/test/setup.ts +5 -0
  114. package/src/types/index.ts +843 -0
  115. package/dist/client-3ESO2NHy.d.ts +0 -310
  116. package/dist/client-CZu03ACp.d.cts +0 -310
  117. package/dist/components/index.cjs +0 -3241
  118. package/dist/components/index.cjs.map +0 -1
  119. package/dist/components/index.css.map +0 -1
  120. package/dist/components/index.d.cts +0 -726
  121. package/dist/components/index.d.ts +0 -726
  122. package/dist/components/index.js +0 -3194
  123. package/dist/components/index.js.map +0 -1
  124. package/dist/core/index.cjs +0 -500
  125. package/dist/core/index.cjs.map +0 -1
  126. package/dist/core/index.d.cts +0 -359
  127. package/dist/core/index.d.ts +0 -359
  128. package/dist/core/index.js +0 -486
  129. package/dist/core/index.js.map +0 -1
  130. package/dist/hooks/index.cjs +0 -20
  131. package/dist/hooks/index.cjs.map +0 -1
  132. package/dist/hooks/index.d.cts +0 -11
  133. package/dist/hooks/index.d.ts +0 -11
  134. package/dist/hooks/index.js +0 -18
  135. package/dist/hooks/index.js.map +0 -1
  136. package/dist/index-BGDEXbuz.d.cts +0 -434
  137. package/dist/index-BGDEXbuz.d.ts +0 -434
  138. package/dist/index.cjs +0 -4499
  139. package/dist/index.cjs.map +0 -1
  140. package/dist/index.css +0 -198
  141. package/dist/index.css.map +0 -1
  142. package/dist/index.d.cts +0 -99
  143. package/dist/index.d.ts +0 -99
  144. package/dist/index.js +0 -4421
  145. package/dist/index.js.map +0 -1
  146. package/dist/store/index.cjs +0 -389
  147. package/dist/store/index.cjs.map +0 -1
  148. package/dist/store/index.d.cts +0 -225
  149. package/dist/store/index.d.ts +0 -225
  150. package/dist/store/index.js +0 -386
  151. package/dist/store/index.js.map +0 -1
@@ -0,0 +1,329 @@
1
+ /**
2
+ * @startsimpli/funnels - Core Funnel Execution Engine
3
+ *
4
+ * BRUTALLY GENERIC - Works for ANY entity type:
5
+ * - Investors, firms, contacts, organizations
6
+ * - Recipes, ingredients, users
7
+ * - Leads, tasks, projects
8
+ * - GitHub repos, pull requests, issues
9
+ *
10
+ * The engine processes entities through stages sequentially,
11
+ * evaluating rules and applying actions based on match results.
12
+ */
13
+
14
+ import type {
15
+ Funnel,
16
+ FunnelStage,
17
+ FilterRule,
18
+ MatchAction,
19
+ NoMatchAction,
20
+ FunnelResult,
21
+ StageResult,
22
+ RuleResult,
23
+ StageStats,
24
+ } from '../types';
25
+
26
+ // ============================================================================
27
+ // Execution Result Types
28
+ // ============================================================================
29
+
30
+ /**
31
+ * Complete result of funnel execution
32
+ */
33
+ export interface ExecutionResult<T = any> {
34
+ /** Entities that matched and are in output */
35
+ matched: FunnelResult<T>[];
36
+
37
+ /** Entities that were excluded */
38
+ excluded: FunnelResult<T>[];
39
+
40
+ /** Total entities processed */
41
+ total_input: number;
42
+
43
+ /** Total entities that matched */
44
+ total_matched: number;
45
+
46
+ /** Total entities excluded */
47
+ total_excluded: number;
48
+
49
+ /** Total entities tagged */
50
+ total_tagged: number;
51
+
52
+ /** Per-stage statistics */
53
+ stage_stats: Record<string, StageStats>;
54
+
55
+ /** Execution duration in milliseconds */
56
+ duration_ms: number;
57
+
58
+ /** Errors encountered (non-fatal) */
59
+ errors?: string[];
60
+ }
61
+
62
+ // ============================================================================
63
+ // BEAD: fund-your-startup-a0b8 - Funnel engine evaluateRule is a non-functional stub
64
+ // evaluateRule always returns error='Evaluator not implemented yet' — funnels cannot match anything
65
+ // resolveField should import from /packages/funnels/src/core/field-resolver.ts
66
+ // ============================================================================
67
+
68
+ function evaluateRule<T>(_entity: T, _rule: FilterRule): RuleResult {
69
+ throw new Error('Not implemented - BEAD: fund-your-startup-a0b8. evaluateRule must import from rule evaluator.');
70
+ // return {
71
+ // field_path: _rule.field_path,
72
+ // operator: _rule.operator,
73
+ // value: _rule.value,
74
+ // actual_value: undefined,
75
+ // matched: false,
76
+ // error: 'Evaluator not implemented yet',
77
+ // };
78
+ }
79
+
80
+ function resolveField<T>(_entity: T, _fieldPath: string): any {
81
+ throw new Error('Not implemented - BEAD: fund-your-startup-a0b8. resolveField must import from field-resolver.ts.');
82
+ // const keys = _fieldPath.split('.');
83
+ // let value: any = _entity;
84
+ // for (const key of keys) {
85
+ // if (value === null || value === undefined) return undefined;
86
+ // value = value[key];
87
+ // }
88
+ // return value;
89
+ }
90
+
91
+ // ============================================================================
92
+ // Funnel Engine
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Core funnel execution engine
97
+ *
98
+ * Processes entities through stages sequentially, evaluating rules
99
+ * and applying actions based on match results.
100
+ *
101
+ * Generic over entity type T - works with ANY data structure.
102
+ */
103
+ export class FunnelEngine<T = any> {
104
+ /**
105
+ * Execute a funnel on a set of entities
106
+ *
107
+ * @param funnel - The funnel definition to execute
108
+ * @param entities - Input entities to process
109
+ * @returns ExecutionResult with matched/excluded entities and stats
110
+ */
111
+ execute(funnel: Funnel<T>, entities: T[]): ExecutionResult<T> {
112
+ const startTime = Date.now();
113
+
114
+ // Initialize results
115
+ const results: FunnelResult<T>[] = entities.map(entity => ({
116
+ entity,
117
+ matched: true, // Start as matched, exclude as needed
118
+ accumulated_tags: [],
119
+ context: {},
120
+ stage_results: [],
121
+ }));
122
+
123
+ const stageStats: Record<string, StageStats> = {};
124
+ const errors: string[] = [];
125
+
126
+ // Sort stages by order
127
+ const sortedStages = [...funnel.stages].sort((a, b) => a.order - b.order);
128
+
129
+ // Process each stage sequentially
130
+ for (const stage of sortedStages) {
131
+ const stageStartTime = Date.now();
132
+
133
+ // Track entities entering this stage
134
+ const inputEntities = results.filter(r => r.matched && !r.excluded_at_stage);
135
+
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,
146
+ };
147
+
148
+ // Process each entity through this stage
149
+ for (const result of inputEntities) {
150
+ try {
151
+ const stageResult = this.processStage(stage, result.entity);
152
+
153
+ // Update entity result
154
+ result.stage_results!.push(stageResult);
155
+
156
+ // Apply stage result to entity
157
+ if (stageResult.matched) {
158
+ stats.matched_count++;
159
+ } else {
160
+ stats.not_matched_count++;
161
+ }
162
+
163
+ if (stageResult.tags_added && stageResult.tags_added.length > 0) {
164
+ result.accumulated_tags.push(...stageResult.tags_added);
165
+ stats.tagged_count++;
166
+ }
167
+
168
+ if (stageResult.context_added) {
169
+ result.context = { ...result.context, ...stageResult.context_added };
170
+ }
171
+
172
+ if (stageResult.excluded) {
173
+ result.matched = false;
174
+ result.excluded_at_stage = stage.id;
175
+ stats.excluded_count++;
176
+ } else if (stageResult.continued) {
177
+ stats.continued_count++;
178
+ }
179
+ } catch (error) {
180
+ stats.error_count!++;
181
+ errors.push(`Stage ${stage.name}: ${error instanceof Error ? error.message : String(error)}`);
182
+ }
183
+ }
184
+
185
+ stats.duration_ms = Date.now() - stageStartTime;
186
+ stageStats[stage.id] = stats;
187
+ }
188
+
189
+ // Apply completion tags to matched entities
190
+ if (funnel.completion_tags && funnel.completion_tags.length > 0) {
191
+ for (const result of results) {
192
+ if (result.matched) {
193
+ result.accumulated_tags.push(...funnel.completion_tags);
194
+ }
195
+ }
196
+ }
197
+
198
+ // Separate matched and excluded
199
+ const matched = results.filter(r => r.matched);
200
+ const excluded = results.filter(r => !r.matched);
201
+
202
+ // Count unique tagged entities
203
+ const totalTagged = results.filter(r => r.accumulated_tags.length > 0).length;
204
+
205
+ return {
206
+ matched,
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,
214
+ errors: errors.length > 0 ? errors : undefined,
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Process a single entity through a stage
220
+ *
221
+ * @param stage - The stage to process
222
+ * @param entity - The entity to evaluate
223
+ * @returns StageResult with match status and actions taken
224
+ */
225
+ private processStage(stage: FunnelStage<T>, entity: T): StageResult {
226
+ // Evaluate all rules
227
+ const ruleResults: RuleResult[] = [];
228
+ let matched = false;
229
+
230
+ if (stage.custom_evaluator) {
231
+ // Use custom evaluator if provided (highest priority)
232
+ try {
233
+ matched = stage.custom_evaluator(entity);
234
+ } catch (error) {
235
+ matched = false;
236
+ }
237
+ } else if (stage.rules.length === 0) {
238
+ // No rules and no custom evaluator = always match
239
+ matched = true;
240
+ } else {
241
+ // Evaluate rules with filter logic
242
+ for (const rule of stage.rules) {
243
+ const ruleResult = evaluateRule(entity, rule);
244
+ ruleResults.push(ruleResult);
245
+ }
246
+
247
+ // Apply filter logic (AND/OR)
248
+ if (stage.filter_logic === 'AND') {
249
+ // All rules must match
250
+ matched = ruleResults.every(r => r.matched);
251
+ } else {
252
+ // At least one rule must match
253
+ matched = ruleResults.some(r => r.matched);
254
+ }
255
+ }
256
+
257
+ // Determine action based on match result
258
+ let action: MatchAction | NoMatchAction;
259
+ let tagsAdded: string[] = [];
260
+ let contextAdded: Record<string, any> | undefined;
261
+ let excluded = false;
262
+ let continued = false;
263
+
264
+ if (matched) {
265
+ action = stage.match_action;
266
+
267
+ // Add match tags
268
+ if (stage.match_tags && stage.match_tags.length > 0) {
269
+ tagsAdded = [...stage.match_tags];
270
+ }
271
+
272
+ // Add match context
273
+ if (stage.match_context) {
274
+ contextAdded = stage.match_context;
275
+ }
276
+
277
+ // Apply match action
278
+ switch (stage.match_action) {
279
+ case 'continue':
280
+ continued = true;
281
+ break;
282
+ case 'tag':
283
+ // Tags added, stop processing
284
+ excluded = true;
285
+ break;
286
+ case 'tag_continue':
287
+ // Tags added, continue processing
288
+ continued = true;
289
+ break;
290
+ case 'output':
291
+ // Add to output, stop processing (but not excluded)
292
+ continued = false;
293
+ break;
294
+ }
295
+ } else {
296
+ action = stage.no_match_action;
297
+
298
+ // Add no-match tags
299
+ if (stage.no_match_tags && stage.no_match_tags.length > 0) {
300
+ tagsAdded = [...stage.no_match_tags];
301
+ }
302
+
303
+ // Apply no-match action
304
+ switch (stage.no_match_action) {
305
+ case 'continue':
306
+ continued = true;
307
+ break;
308
+ case 'exclude':
309
+ excluded = true;
310
+ break;
311
+ case 'tag_exclude':
312
+ excluded = true;
313
+ break;
314
+ }
315
+ }
316
+
317
+ return {
318
+ stage_id: stage.id,
319
+ stage_name: stage.name,
320
+ matched,
321
+ rule_results: ruleResults.length > 0 ? ruleResults : undefined,
322
+ action,
323
+ tags_added: tagsAdded.length > 0 ? tagsAdded : undefined,
324
+ context_added: contextAdded,
325
+ excluded,
326
+ continued,
327
+ };
328
+ }
329
+ }
@@ -0,0 +1,353 @@
1
+ /**
2
+ * @startsimpli/funnels - Evaluator Usage Examples
3
+ *
4
+ * This file demonstrates how to use the rule evaluation engine
5
+ * with various entity types and operators.
6
+ */
7
+
8
+ import {
9
+ evaluateRule,
10
+ evaluateRules,
11
+ evaluateRulesWithResults,
12
+ filterEntities,
13
+ } from './evaluator';
14
+ import type { FilterRule } from '../types';
15
+
16
+ // ============================================================================
17
+ // Example 1: Investor Qualification
18
+ // ============================================================================
19
+
20
+ interface Investor {
21
+ name: string;
22
+ email: string;
23
+ firm: {
24
+ name: string;
25
+ stage: string;
26
+ aum: number;
27
+ geography: string[];
28
+ };
29
+ tags: string[];
30
+ created_at: Date;
31
+ }
32
+
33
+ const investors: Investor[] = [
34
+ {
35
+ name: 'John Doe',
36
+ email: 'john@acme.vc',
37
+ firm: {
38
+ name: 'Acme Ventures',
39
+ stage: 'Series A',
40
+ aum: 100000000,
41
+ geography: ['US', 'Canada'],
42
+ },
43
+ tags: ['qualified', 'active'],
44
+ created_at: new Date('2024-01-15'),
45
+ },
46
+ {
47
+ name: 'Jane Smith',
48
+ email: 'jane@seed.fund',
49
+ firm: {
50
+ name: 'Seed Fund',
51
+ stage: 'Seed',
52
+ aum: 50000000,
53
+ geography: ['US'],
54
+ },
55
+ tags: ['active'],
56
+ created_at: new Date('2024-02-01'),
57
+ },
58
+ {
59
+ name: 'Bob Johnson',
60
+ email: 'bob@growth.capital',
61
+ firm: {
62
+ name: 'Growth Capital',
63
+ stage: 'Series B',
64
+ aum: 500000000,
65
+ geography: ['US', 'Europe', 'Asia'],
66
+ },
67
+ tags: ['qualified', 'premium'],
68
+ created_at: new Date('2024-01-20'),
69
+ },
70
+ ];
71
+
72
+ // Example 1a: Single rule evaluation
73
+ console.log('=== Example 1a: Single Rule ===');
74
+ const rule1: FilterRule = {
75
+ field_path: 'firm.aum',
76
+ operator: 'gte',
77
+ value: 100000000,
78
+ };
79
+
80
+ investors.forEach(investor => {
81
+ const matches = evaluateRule(investor, rule1);
82
+ console.log(`${investor.name}: AUM >= $100M? ${matches}`);
83
+ });
84
+
85
+ // Example 1b: Multiple rules with AND logic
86
+ console.log('\n=== Example 1b: Multiple Rules (AND) ===');
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' },
91
+ ];
92
+
93
+ const qualified = filterEntities(investors, rules1, 'AND');
94
+ console.log('Qualified investors:', qualified.map(i => i.name));
95
+
96
+ // Example 1c: Detailed results with diagnostics
97
+ console.log('\n=== Example 1c: Detailed Results ===');
98
+ const result1 = evaluateRulesWithResults(investors[0], rules1, 'AND');
99
+ console.log('Evaluation for John Doe:');
100
+ console.log('Overall match:', result1.matched);
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}`);
105
+ });
106
+
107
+ // ============================================================================
108
+ // Example 2: Recipe Filtering
109
+ // ============================================================================
110
+
111
+ interface Recipe {
112
+ name: string;
113
+ cuisine: string;
114
+ difficulty: 'easy' | 'medium' | 'hard';
115
+ prep_time_minutes: number;
116
+ ingredients: string[];
117
+ dietary: string[];
118
+ rating: number;
119
+ }
120
+
121
+ const recipes: Recipe[] = [
122
+ {
123
+ name: 'Pasta Carbonara',
124
+ cuisine: 'Italian',
125
+ difficulty: 'easy',
126
+ prep_time_minutes: 20,
127
+ ingredients: ['pasta', 'eggs', 'bacon', 'cheese'],
128
+ dietary: [],
129
+ rating: 4.5,
130
+ },
131
+ {
132
+ name: 'Vegan Buddha Bowl',
133
+ cuisine: 'Fusion',
134
+ difficulty: 'medium',
135
+ prep_time_minutes: 30,
136
+ ingredients: ['quinoa', 'chickpeas', 'avocado', 'vegetables'],
137
+ dietary: ['vegan', 'gluten-free'],
138
+ rating: 4.8,
139
+ },
140
+ {
141
+ name: 'Beef Wellington',
142
+ cuisine: 'British',
143
+ difficulty: 'hard',
144
+ prep_time_minutes: 120,
145
+ ingredients: ['beef', 'pastry', 'mushrooms', 'pate'],
146
+ dietary: [],
147
+ rating: 4.9,
148
+ },
149
+ ];
150
+
151
+ // Example 2a: Filter by dietary restrictions
152
+ console.log('\n=== Example 2: Recipe Filtering ===');
153
+ const veganRules: FilterRule[] = [
154
+ { field_path: 'dietary', operator: 'has_tag', value: 'vegan' },
155
+ ];
156
+
157
+ const veganRecipes = filterEntities(recipes, veganRules);
158
+ console.log('Vegan recipes:', veganRecipes.map(r => r.name));
159
+
160
+ // Example 2b: Filter by multiple criteria (quick meals)
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 },
165
+ ];
166
+
167
+ const quickMeals = filterEntities(recipes, quickMealRules, 'AND');
168
+ console.log('Quick high-rated meals:', quickMeals.map(r => r.name));
169
+
170
+ // ============================================================================
171
+ // Example 3: GitHub Repository Filtering
172
+ // ============================================================================
173
+
174
+ interface GitHubRepo {
175
+ name: string;
176
+ owner: string;
177
+ stars: number;
178
+ forks: number;
179
+ language: string;
180
+ topics: string[];
181
+ has_issues: boolean;
182
+ is_archived: boolean;
183
+ last_updated: Date;
184
+ }
185
+
186
+ const repos: GitHubRepo[] = [
187
+ {
188
+ name: 'awesome-project',
189
+ owner: 'acme',
190
+ stars: 1500,
191
+ forks: 200,
192
+ language: 'TypeScript',
193
+ topics: ['frontend', 'react', 'nextjs'],
194
+ has_issues: true,
195
+ is_archived: false,
196
+ last_updated: new Date('2024-02-01'),
197
+ },
198
+ {
199
+ name: 'legacy-tool',
200
+ owner: 'acme',
201
+ stars: 50,
202
+ forks: 10,
203
+ language: 'JavaScript',
204
+ topics: ['legacy'],
205
+ has_issues: false,
206
+ is_archived: true,
207
+ last_updated: new Date('2020-01-01'),
208
+ },
209
+ {
210
+ name: 'python-ml-lib',
211
+ owner: 'datascience',
212
+ stars: 5000,
213
+ forks: 800,
214
+ language: 'Python',
215
+ topics: ['machine-learning', 'ai', 'data-science'],
216
+ has_issues: true,
217
+ is_archived: false,
218
+ last_updated: new Date('2024-02-10'),
219
+ },
220
+ ];
221
+
222
+ // Example 3a: Find popular active repos
223
+ console.log('\n=== Example 3: GitHub Repos ===');
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') },
228
+ ];
229
+
230
+ const popularRepos = filterEntities(repos, popularRepoRules, 'AND');
231
+ console.log('Popular active repos:', popularRepos.map(r => r.name));
232
+
233
+ // Example 3b: Find TypeScript projects with React
234
+ const tsReactRules: FilterRule[] = [
235
+ { field_path: 'language', operator: 'eq', value: 'TypeScript' },
236
+ { field_path: 'topics', operator: 'has_tag', value: 'react' },
237
+ ];
238
+
239
+ const tsReactRepos = filterEntities(repos, tsReactRules, 'AND');
240
+ console.log('TypeScript + React repos:', tsReactRepos.map(r => r.name));
241
+
242
+ // ============================================================================
243
+ // Example 4: Advanced Patterns
244
+ // ============================================================================
245
+
246
+ console.log('\n=== Example 4: Advanced Patterns ===');
247
+
248
+ // Pattern 1: Negation
249
+ const notArchivedRule: FilterRule = {
250
+ field_path: 'is_archived',
251
+ operator: 'is_true',
252
+ value: null,
253
+ negate: true, // Negate the result
254
+ };
255
+
256
+ const activeRepos = filterEntities(repos, [notArchivedRule]);
257
+ console.log('Active repos (using negate):', activeRepos.map(r => r.name));
258
+
259
+ // Pattern 2: Complex string matching
260
+ const emailDomainRule: FilterRule = {
261
+ field_path: 'email',
262
+ operator: 'matches',
263
+ value: '@acme\\.vc$', // Matches emails ending with @acme.vc
264
+ };
265
+
266
+ const acmeInvestors = filterEntities(investors, [emailDomainRule]);
267
+ console.log('Acme VC investors:', acmeInvestors.map(i => i.name));
268
+
269
+ // Pattern 3: OR logic (match any condition)
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
273
+ ];
274
+
275
+ const flexibleMatch = filterEntities(investors, flexibleStageRules, 'OR');
276
+ console.log('Series A OR large fund:', flexibleMatch.map(i => i.name));
277
+
278
+ // Pattern 4: Null handling
279
+ const investorsWithoutEmail = investors.filter(i => !i.email);
280
+ investorsWithoutEmail.push({
281
+ name: 'No Email User',
282
+ email: null as any,
283
+ firm: { name: 'Test', stage: 'Seed', aum: 0, geography: [] },
284
+ tags: [],
285
+ created_at: new Date(),
286
+ });
287
+
288
+ const hasEmailRule: FilterRule = {
289
+ field_path: 'email',
290
+ operator: 'isnotnull',
291
+ value: null,
292
+ };
293
+
294
+ const withEmail = filterEntities(investorsWithoutEmail, [hasEmailRule]);
295
+ console.log('Investors with email:', withEmail.map(i => i.name));
296
+
297
+ // Pattern 5: Array operations
298
+ const multiGeographyRule: FilterRule = {
299
+ field_path: 'firm.geography',
300
+ operator: 'has_all',
301
+ value: ['US', 'Europe'],
302
+ };
303
+
304
+ const globalFunds = filterEntities(investors, [multiGeographyRule]);
305
+ console.log('Global funds (US + Europe):', globalFunds.map(i => i.name));
306
+
307
+ console.log('\n=== All Examples Complete ===');
308
+
309
+ // ============================================================================
310
+ // Example 5: Real-World Funnel Stage Simulation
311
+ // ============================================================================
312
+
313
+ console.log('\n=== Example 5: Funnel Stage Simulation ===');
314
+
315
+ // Simulate a multi-stage funnel
316
+ function simulateFunnel<T>(entities: T[], stages: Array<{ name: string; rules: FilterRule[]; logic: 'AND' | 'OR' }>) {
317
+ let remaining = entities;
318
+
319
+ stages.forEach((stage, index) => {
320
+ const before = remaining.length;
321
+ remaining = filterEntities(remaining, stage.rules, stage.logic);
322
+ const after = remaining.length;
323
+ console.log(`Stage ${index + 1} (${stage.name}): ${before} → ${after} (${before - after} excluded)`);
324
+ });
325
+
326
+ return remaining;
327
+ }
328
+
329
+ const investorFunnelStages = [
330
+ {
331
+ name: 'Active & Not Archived',
332
+ rules: [{ field_path: 'tags', operator: 'has_tag', value: 'active' } as FilterRule],
333
+ logic: 'AND' as const,
334
+ },
335
+ {
336
+ name: 'Series A+ Stage',
337
+ rules: [{ field_path: 'firm.stage', operator: 'in', value: ['Series A', 'Series B', 'Series C'] } as FilterRule],
338
+ logic: 'AND' as const,
339
+ },
340
+ {
341
+ name: 'Minimum AUM $100M',
342
+ rules: [{ field_path: 'firm.aum', operator: 'gte', value: 100000000 } as FilterRule],
343
+ logic: 'AND' as const,
344
+ },
345
+ {
346
+ name: 'Qualified Tag',
347
+ rules: [{ field_path: 'tags', operator: 'has_tag', value: 'qualified' } as FilterRule],
348
+ logic: 'AND' as const,
349
+ },
350
+ ];
351
+
352
+ const finalInvestors = simulateFunnel(investors, investorFunnelStages);
353
+ console.log('Final qualified investors:', finalInvestors.map(i => i.name));