@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.
- package/package.json +9 -31
- package/src/api/README.md +507 -0
- package/src/api/adapter.ts +106 -0
- package/src/api/client.test.ts +640 -0
- package/src/api/client.ts +385 -0
- package/src/api/default-adapter.ts +243 -0
- package/src/api/index.ts +24 -0
- package/src/components/FilterRuleEditor/ARCHITECTURE.md +354 -0
- package/src/components/FilterRuleEditor/FieldSelector.tsx +91 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.stories.tsx +462 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.test.tsx +520 -0
- package/src/components/FilterRuleEditor/FilterRuleEditor.tsx +225 -0
- package/src/components/FilterRuleEditor/LogicToggle.tsx +64 -0
- package/src/components/FilterRuleEditor/OperatorSelector.tsx +75 -0
- package/src/components/FilterRuleEditor/README.md +291 -0
- package/src/components/FilterRuleEditor/RuleRow.tsx +246 -0
- package/src/components/FilterRuleEditor/ValueInputs/BooleanValueInput.tsx +54 -0
- package/src/components/FilterRuleEditor/ValueInputs/ChoiceValueInput.tsx +83 -0
- package/src/components/FilterRuleEditor/ValueInputs/DateValueInput.tsx +70 -0
- package/src/components/FilterRuleEditor/ValueInputs/MultiChoiceValueInput.tsx +132 -0
- package/src/components/FilterRuleEditor/ValueInputs/NumberValueInput.tsx +73 -0
- package/src/components/FilterRuleEditor/ValueInputs/TextValueInput.tsx +50 -0
- package/src/components/FilterRuleEditor/ValueInputs/index.ts +12 -0
- package/src/components/FilterRuleEditor/constants.ts +64 -0
- package/src/components/FilterRuleEditor/index.ts +14 -0
- package/src/components/FunnelCard/DESIGN.md +447 -0
- package/src/components/FunnelCard/FunnelCard.stories.tsx +484 -0
- package/src/components/FunnelCard/FunnelCard.test.ts +257 -0
- package/src/components/FunnelCard/FunnelCard.test.tsx +336 -0
- package/src/components/FunnelCard/FunnelCard.tsx +204 -0
- package/src/components/FunnelCard/FunnelStats.tsx +68 -0
- package/src/components/FunnelCard/IMPLEMENTATION_SUMMARY.md +505 -0
- package/src/components/FunnelCard/INSTALLATION.md +304 -0
- package/src/components/FunnelCard/MatchBar.tsx +49 -0
- package/src/components/FunnelCard/README.md +294 -0
- package/src/components/FunnelCard/StageIndicator.tsx +62 -0
- package/src/components/FunnelCard/StatusBadge.tsx +52 -0
- package/src/components/FunnelCard/index.ts +14 -0
- package/src/components/FunnelPreview/EntityCard.tsx +72 -0
- package/src/components/FunnelPreview/FunnelPreview.stories.tsx +227 -0
- package/src/components/FunnelPreview/FunnelPreview.test.tsx +316 -0
- package/src/components/FunnelPreview/FunnelPreview.tsx +249 -0
- package/src/components/FunnelPreview/LoadingPreview.tsx +60 -0
- package/src/components/FunnelPreview/PreviewStats.tsx +78 -0
- package/src/components/FunnelPreview/README.md +337 -0
- package/src/components/FunnelPreview/StageBreakdown.tsx +94 -0
- package/src/components/FunnelPreview/example.tsx +286 -0
- package/src/components/FunnelPreview/index.ts +14 -0
- package/src/components/FunnelRunHistory/COMPONENT_SUMMARY.md +246 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.stories.tsx +272 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.test.tsx +323 -0
- package/src/components/FunnelRunHistory/FunnelRunHistory.tsx +329 -0
- package/src/components/FunnelRunHistory/README.md +325 -0
- package/src/components/FunnelRunHistory/RunActions.tsx +168 -0
- package/src/components/FunnelRunHistory/RunDetailsModal.tsx +221 -0
- package/src/components/FunnelRunHistory/RunFilters.tsx +128 -0
- package/src/components/FunnelRunHistory/RunRow.tsx +122 -0
- package/src/components/FunnelRunHistory/RunStatusBadge.tsx +75 -0
- package/src/components/FunnelRunHistory/StageBreakdownList.tsx +110 -0
- package/src/components/FunnelRunHistory/index.ts +51 -0
- package/src/components/FunnelRunHistory/types.ts +40 -0
- package/src/components/FunnelRunHistory/utils.test.ts +126 -0
- package/src/components/FunnelRunHistory/utils.ts +100 -0
- package/src/components/FunnelStageBuilder/AddStageButton.tsx +52 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.css +413 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.stories.tsx +312 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.test.tsx +304 -0
- package/src/components/FunnelStageBuilder/FunnelStageBuilder.tsx +321 -0
- package/src/components/FunnelStageBuilder/README.md +341 -0
- package/src/components/FunnelStageBuilder/StageActions.test.tsx +205 -0
- package/src/components/FunnelStageBuilder/StageActions.tsx +126 -0
- package/src/components/FunnelStageBuilder/StageCard.tsx +202 -0
- package/src/components/FunnelStageBuilder/StageForm.tsx +262 -0
- package/src/components/FunnelStageBuilder/TagInput.test.tsx +178 -0
- package/src/components/FunnelStageBuilder/TagInput.tsx +129 -0
- package/src/components/FunnelStageBuilder/index.ts +21 -0
- package/src/components/FunnelVisualFlow/FlowLegend.tsx +77 -0
- package/{dist/components/index.css → src/components/FunnelVisualFlow/FunnelVisualFlow.css} +89 -13
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.stories.tsx +254 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.test.tsx +208 -0
- package/src/components/FunnelVisualFlow/FunnelVisualFlow.tsx +229 -0
- package/src/components/FunnelVisualFlow/README.md +323 -0
- package/src/components/FunnelVisualFlow/StageNode.tsx +188 -0
- package/src/components/FunnelVisualFlow/example.tsx +227 -0
- package/src/components/FunnelVisualFlow/index.ts +10 -0
- package/src/components/index.ts +102 -0
- package/src/core/README.md +307 -0
- package/src/core/engine.test.ts +1087 -0
- package/src/core/engine.ts +329 -0
- package/src/core/evaluator.example.ts +353 -0
- package/src/core/evaluator.test.ts +639 -0
- package/src/core/evaluator.ts +261 -0
- package/src/core/field-resolver.example.ts +175 -0
- package/src/core/field-resolver.test.ts +541 -0
- package/src/core/field-resolver.ts +247 -0
- package/src/core/index.ts +34 -0
- package/src/core/operators.test.ts +539 -0
- package/src/core/operators.ts +241 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useDebouncedValue.ts +28 -0
- package/src/index.ts +155 -0
- package/src/store/README.md +342 -0
- package/src/store/create-funnel-store.test.ts +686 -0
- package/src/store/create-funnel-store.ts +538 -0
- package/src/store/index.ts +9 -0
- package/src/store/types.ts +294 -0
- package/src/stories/CrossDomain.stories.tsx +149 -0
- package/src/stories/Welcome.stories.tsx +81 -0
- package/src/stories/demo-data/index.ts +3 -0
- package/src/stories/demo-data/investors.ts +216 -0
- package/src/stories/demo-data/leads.ts +223 -0
- package/src/stories/demo-data/recipes.ts +217 -0
- package/src/test/setup.ts +5 -0
- package/src/types/index.ts +843 -0
- package/dist/client-3ESO2NHy.d.ts +0 -310
- package/dist/client-CZu03ACp.d.cts +0 -310
- package/dist/components/index.cjs +0 -3241
- package/dist/components/index.cjs.map +0 -1
- package/dist/components/index.css.map +0 -1
- package/dist/components/index.d.cts +0 -726
- package/dist/components/index.d.ts +0 -726
- package/dist/components/index.js +0 -3194
- package/dist/components/index.js.map +0 -1
- package/dist/core/index.cjs +0 -500
- package/dist/core/index.cjs.map +0 -1
- package/dist/core/index.d.cts +0 -359
- package/dist/core/index.d.ts +0 -359
- package/dist/core/index.js +0 -486
- package/dist/core/index.js.map +0 -1
- package/dist/hooks/index.cjs +0 -20
- package/dist/hooks/index.cjs.map +0 -1
- package/dist/hooks/index.d.cts +0 -11
- package/dist/hooks/index.d.ts +0 -11
- package/dist/hooks/index.js +0 -18
- package/dist/hooks/index.js.map +0 -1
- package/dist/index-BGDEXbuz.d.cts +0 -434
- package/dist/index-BGDEXbuz.d.ts +0 -434
- package/dist/index.cjs +0 -4499
- package/dist/index.cjs.map +0 -1
- package/dist/index.css +0 -198
- package/dist/index.css.map +0 -1
- package/dist/index.d.cts +0 -99
- package/dist/index.d.ts +0 -99
- package/dist/index.js +0 -4421
- package/dist/index.js.map +0 -1
- package/dist/store/index.cjs +0 -389
- package/dist/store/index.cjs.map +0 -1
- package/dist/store/index.d.cts +0 -225
- package/dist/store/index.d.ts +0 -225
- package/dist/store/index.js +0 -386
- 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));
|