@startsimpli/funnels 0.1.2
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/LICENSE +21 -0
- package/README.md +564 -0
- package/dist/client-3ESO2NHy.d.ts +310 -0
- package/dist/client-CZu03ACp.d.cts +310 -0
- package/dist/components/index.cjs +3243 -0
- package/dist/components/index.cjs.map +1 -0
- package/dist/components/index.css +198 -0
- package/dist/components/index.css.map +1 -0
- package/dist/components/index.d.cts +726 -0
- package/dist/components/index.d.ts +726 -0
- package/dist/components/index.js +3196 -0
- package/dist/components/index.js.map +1 -0
- package/dist/core/index.cjs +500 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +359 -0
- package/dist/core/index.d.ts +359 -0
- package/dist/core/index.js +486 -0
- package/dist/core/index.js.map +1 -0
- package/dist/hooks/index.cjs +21 -0
- package/dist/hooks/index.cjs.map +1 -0
- package/dist/hooks/index.d.cts +11 -0
- package/dist/hooks/index.d.ts +11 -0
- package/dist/hooks/index.js +19 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index-BGDEXbuz.d.cts +434 -0
- package/dist/index-BGDEXbuz.d.ts +434 -0
- package/dist/index.cjs +4499 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +198 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.cts +99 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +4421 -0
- package/dist/index.js.map +1 -0
- package/dist/store/index.cjs +391 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.cts +225 -0
- package/dist/store/index.d.ts +225 -0
- package/dist/store/index.js +388 -0
- package/dist/store/index.js.map +1 -0
- package/package.json +122 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 StartSimpli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
# @simpli/funnels
|
|
2
|
+
|
|
3
|
+
> Brutally generic filtering pipeline package for any Simpli product
|
|
4
|
+
|
|
5
|
+
[]()
|
|
6
|
+
[]()
|
|
7
|
+
[]()
|
|
8
|
+
[]()
|
|
9
|
+
|
|
10
|
+
## What is this?
|
|
11
|
+
|
|
12
|
+
A reusable package for building multi-stage filtering funnels. Works with **ANY entity type** - investors, recipes, leads, tasks, GitHub issues, or whatever you dream up.
|
|
13
|
+
|
|
14
|
+
### The Philosophy
|
|
15
|
+
|
|
16
|
+
**BRUTALLY GENERIC** - No domain-specific types. No investor-specific fields. No recipe-specific logic.
|
|
17
|
+
|
|
18
|
+
It's just:
|
|
19
|
+
1. Start with entities (any type)
|
|
20
|
+
2. Apply sequential filter stages
|
|
21
|
+
3. Each stage: keep/exclude/tag based on rules
|
|
22
|
+
4. End with filtered subset + accumulated tags/context
|
|
23
|
+
|
|
24
|
+
### Why Use This?
|
|
25
|
+
|
|
26
|
+
- **Zero domain coupling** - Works for investors, recipes, leads, products, tasks, anything
|
|
27
|
+
- **Type-safe** - Full TypeScript support with generics
|
|
28
|
+
- **Modular** - Import only what you need (tree-shakeable)
|
|
29
|
+
- **Server-compatible** - Core engine has NO React dependencies
|
|
30
|
+
- **Battle-tested** - 487 tests passing
|
|
31
|
+
- **Production-ready** - Used across multiple Simpli products
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @simpli/funnels
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
### Example 1: Investor Funnel
|
|
42
|
+
|
|
43
|
+
Filter investors for a Series A fundraise:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { Funnel, FunnelEngine } from '@simpli/funnels';
|
|
47
|
+
|
|
48
|
+
interface Investor {
|
|
49
|
+
name: string;
|
|
50
|
+
firm: {
|
|
51
|
+
stage: string;
|
|
52
|
+
check_size_min: number;
|
|
53
|
+
check_size_max: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const funnel: Funnel<Investor> = {
|
|
58
|
+
id: 'series-a-funnel',
|
|
59
|
+
name: 'Series A Investor Qualification',
|
|
60
|
+
status: 'active',
|
|
61
|
+
input_type: 'contacts',
|
|
62
|
+
stages: [
|
|
63
|
+
{
|
|
64
|
+
id: 'stage-1',
|
|
65
|
+
order: 0,
|
|
66
|
+
name: 'Stage Filter',
|
|
67
|
+
filter_logic: 'OR',
|
|
68
|
+
rules: [
|
|
69
|
+
{ field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
|
|
70
|
+
{ field_path: 'firm.stage', operator: 'eq', value: 'Multi-Stage' }
|
|
71
|
+
],
|
|
72
|
+
match_action: 'tag_continue',
|
|
73
|
+
no_match_action: 'exclude',
|
|
74
|
+
match_tags: ['qualified_stage']
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'stage-2',
|
|
78
|
+
order: 1,
|
|
79
|
+
name: 'Check Size',
|
|
80
|
+
filter_logic: 'AND',
|
|
81
|
+
rules: [
|
|
82
|
+
{ field_path: 'firm.check_size_min', operator: 'lte', value: 5000000 },
|
|
83
|
+
{ field_path: 'firm.check_size_max', operator: 'gte', value: 3000000 }
|
|
84
|
+
],
|
|
85
|
+
match_action: 'output',
|
|
86
|
+
no_match_action: 'exclude',
|
|
87
|
+
match_tags: ['qualified']
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
created_at: new Date().toISOString(),
|
|
91
|
+
updated_at: new Date().toISOString()
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Execute funnel
|
|
95
|
+
const engine = new FunnelEngine();
|
|
96
|
+
const investors = [/* your data */];
|
|
97
|
+
const results = engine.executeSync(funnel, investors);
|
|
98
|
+
|
|
99
|
+
console.log(`Matched: ${results.matched.length}`);
|
|
100
|
+
console.log(`Excluded: ${results.excluded.length}`);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Example 2: Recipe Funnel
|
|
104
|
+
|
|
105
|
+
Find quick, easy, vegetarian recipes:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { Funnel } from '@simpli/funnels';
|
|
109
|
+
|
|
110
|
+
interface Recipe {
|
|
111
|
+
name: string;
|
|
112
|
+
prep_time_minutes: number;
|
|
113
|
+
difficulty: 'easy' | 'medium' | 'hard';
|
|
114
|
+
dietary_restrictions: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const funnel: Funnel<Recipe> = {
|
|
118
|
+
id: 'quick-dinner',
|
|
119
|
+
name: 'Quick Weeknight Dinner',
|
|
120
|
+
status: 'active',
|
|
121
|
+
input_type: 'any',
|
|
122
|
+
stages: [
|
|
123
|
+
{
|
|
124
|
+
id: 'dietary',
|
|
125
|
+
order: 0,
|
|
126
|
+
name: 'Dietary Restrictions',
|
|
127
|
+
filter_logic: 'AND',
|
|
128
|
+
rules: [
|
|
129
|
+
{ field_path: 'dietary_restrictions', operator: 'has_all', value: ['vegetarian'] }
|
|
130
|
+
],
|
|
131
|
+
match_action: 'tag_continue',
|
|
132
|
+
no_match_action: 'exclude',
|
|
133
|
+
match_tags: ['vegetarian']
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: 'time',
|
|
137
|
+
order: 1,
|
|
138
|
+
name: 'Quick Prep',
|
|
139
|
+
filter_logic: 'AND',
|
|
140
|
+
rules: [
|
|
141
|
+
{ field_path: 'prep_time_minutes', operator: 'lte', value: 30 }
|
|
142
|
+
],
|
|
143
|
+
match_action: 'tag_continue',
|
|
144
|
+
no_match_action: 'exclude',
|
|
145
|
+
match_tags: ['quick']
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'difficulty',
|
|
149
|
+
order: 2,
|
|
150
|
+
name: 'Easy to Make',
|
|
151
|
+
filter_logic: 'OR',
|
|
152
|
+
rules: [
|
|
153
|
+
{ field_path: 'difficulty', operator: 'eq', value: 'easy' },
|
|
154
|
+
{ field_path: 'difficulty', operator: 'eq', value: 'medium' }
|
|
155
|
+
],
|
|
156
|
+
match_action: 'output',
|
|
157
|
+
no_match_action: 'exclude',
|
|
158
|
+
match_tags: ['beginner_friendly']
|
|
159
|
+
}
|
|
160
|
+
],
|
|
161
|
+
created_at: new Date().toISOString(),
|
|
162
|
+
updated_at: new Date().toISOString()
|
|
163
|
+
};
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Example 3: Lead Scoring
|
|
167
|
+
|
|
168
|
+
Score sales leads based on company size and engagement:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { Funnel } from '@simpli/funnels';
|
|
172
|
+
|
|
173
|
+
interface Lead {
|
|
174
|
+
company: { size: number };
|
|
175
|
+
engagement: {
|
|
176
|
+
email_opens: number;
|
|
177
|
+
demo_requested: boolean;
|
|
178
|
+
};
|
|
179
|
+
tags: string[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const funnel: Funnel<Lead> = {
|
|
183
|
+
id: 'lead-scoring',
|
|
184
|
+
name: 'Enterprise Lead Scoring',
|
|
185
|
+
status: 'active',
|
|
186
|
+
input_type: 'any',
|
|
187
|
+
stages: [
|
|
188
|
+
{
|
|
189
|
+
id: 'company-size',
|
|
190
|
+
order: 0,
|
|
191
|
+
name: 'Enterprise Size',
|
|
192
|
+
filter_logic: 'AND',
|
|
193
|
+
rules: [
|
|
194
|
+
{ field_path: 'company.size', operator: 'gte', value: 100 }
|
|
195
|
+
],
|
|
196
|
+
match_action: 'tag_continue',
|
|
197
|
+
no_match_action: 'tag_continue',
|
|
198
|
+
match_tags: ['enterprise'],
|
|
199
|
+
no_match_tags: ['smb']
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'engagement',
|
|
203
|
+
order: 1,
|
|
204
|
+
name: 'High Engagement',
|
|
205
|
+
filter_logic: 'OR',
|
|
206
|
+
rules: [
|
|
207
|
+
{ field_path: 'engagement.email_opens', operator: 'gte', value: 5 },
|
|
208
|
+
{ field_path: 'engagement.demo_requested', operator: 'is_true', value: null }
|
|
209
|
+
],
|
|
210
|
+
match_action: 'output',
|
|
211
|
+
no_match_action: 'output',
|
|
212
|
+
match_tags: ['hot_lead'],
|
|
213
|
+
match_context: { tier: 'A', score: 100 }
|
|
214
|
+
}
|
|
215
|
+
],
|
|
216
|
+
created_at: new Date().toISOString(),
|
|
217
|
+
updated_at: new Date().toISOString()
|
|
218
|
+
};
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Core Concepts
|
|
222
|
+
|
|
223
|
+
### Funnel
|
|
224
|
+
|
|
225
|
+
A sequential pipeline with multiple filtering stages. Each funnel has:
|
|
226
|
+
- **Stages**: Ordered sequence of filter conditions
|
|
227
|
+
- **Status**: draft | active | paused | archived
|
|
228
|
+
- **Metadata**: Tags, context, ownership info
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
interface Funnel<TEntity = any> {
|
|
232
|
+
id: string;
|
|
233
|
+
name: string;
|
|
234
|
+
status: 'draft' | 'active' | 'paused' | 'archived';
|
|
235
|
+
stages: FunnelStage<TEntity>[];
|
|
236
|
+
// ... more fields
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Stage
|
|
241
|
+
|
|
242
|
+
A single filtering step with rules, actions, and tags:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
interface FunnelStage<TEntity = any> {
|
|
246
|
+
id: string;
|
|
247
|
+
order: number;
|
|
248
|
+
name: string;
|
|
249
|
+
filter_logic: 'AND' | 'OR';
|
|
250
|
+
rules: FilterRule[];
|
|
251
|
+
match_action: 'continue' | 'tag' | 'tag_continue' | 'output';
|
|
252
|
+
no_match_action: 'continue' | 'exclude' | 'tag_exclude';
|
|
253
|
+
match_tags?: string[];
|
|
254
|
+
no_match_tags?: string[];
|
|
255
|
+
custom_evaluator?: (entity: TEntity) => boolean;
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Filter Rule
|
|
260
|
+
|
|
261
|
+
A single condition with field path, operator, and value:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
interface FilterRule {
|
|
265
|
+
field_path: string; // 'firm.stage', 'recipe.cuisine', 'tags'
|
|
266
|
+
operator: Operator; // 'eq', 'gt', 'contains', 'has_any', etc.
|
|
267
|
+
value: any; // Value to compare against
|
|
268
|
+
negate?: boolean; // Optional negation
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
**Supported operators:**
|
|
273
|
+
- **Equality**: `eq`, `ne`
|
|
274
|
+
- **Comparison**: `gt`, `lt`, `gte`, `lte`
|
|
275
|
+
- **String**: `contains`, `not_contains`, `startswith`, `endswith`, `matches`
|
|
276
|
+
- **Array**: `in`, `not_in`, `has_any`, `has_all`
|
|
277
|
+
- **Null**: `isnull`, `isnotnull`
|
|
278
|
+
- **Tags**: `has_tag`, `not_has_tag`
|
|
279
|
+
- **Boolean**: `is_true`, `is_false`
|
|
280
|
+
|
|
281
|
+
### Field Registry
|
|
282
|
+
|
|
283
|
+
Defines what fields are available for filtering in your domain:
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
const investorRegistry: FieldRegistry = {
|
|
287
|
+
entity_type: 'investor',
|
|
288
|
+
fields: [
|
|
289
|
+
{
|
|
290
|
+
name: 'firm.stage',
|
|
291
|
+
label: 'Investment Stage',
|
|
292
|
+
type: 'string',
|
|
293
|
+
operators: ['eq', 'ne', 'in', 'not_in'],
|
|
294
|
+
category: 'Firm Details',
|
|
295
|
+
constraints: {
|
|
296
|
+
choices: ['Seed', 'Series A', 'Series B', 'Growth']
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'firm.check_size_min',
|
|
301
|
+
label: 'Min Check Size',
|
|
302
|
+
type: 'number',
|
|
303
|
+
operators: ['eq', 'ne', 'gt', 'lt', 'gte', 'lte'],
|
|
304
|
+
category: 'Firm Details'
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
};
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Modular Exports
|
|
311
|
+
|
|
312
|
+
Import only what you need for optimal bundle size:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// Full package (everything)
|
|
316
|
+
import { FunnelEngine, FunnelPreview, createFunnelStore } from '@simpli/funnels';
|
|
317
|
+
|
|
318
|
+
// Core only (NO React dependencies - perfect for workers, CLI, Node.js)
|
|
319
|
+
import { FunnelEngine, evaluateRule, applyOperator } from '@simpli/funnels/core';
|
|
320
|
+
|
|
321
|
+
// Components only (React UI)
|
|
322
|
+
import { FunnelCard, FunnelPreview, FunnelStageBuilder } from '@simpli/funnels/components';
|
|
323
|
+
|
|
324
|
+
// Hooks only (React hooks)
|
|
325
|
+
import { useDebouncedValue } from '@simpli/funnels/hooks';
|
|
326
|
+
|
|
327
|
+
// State only (Zustand store)
|
|
328
|
+
import { createFunnelStore } from '@simpli/funnels/store';
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Use cases:**
|
|
332
|
+
- **Server-side worker**: `@simpli/funnels/core` (no React, no DOM)
|
|
333
|
+
- **Next.js app**: `@simpli/funnels` (full package)
|
|
334
|
+
- **Component library**: `@simpli/funnels/components` (UI only)
|
|
335
|
+
- **State management**: `@simpli/funnels/store` (Zustand store)
|
|
336
|
+
|
|
337
|
+
## API Reference
|
|
338
|
+
|
|
339
|
+
See [API_REFERENCE.md](./API_REFERENCE.md) for complete API documentation.
|
|
340
|
+
|
|
341
|
+
### Core Engine
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { FunnelEngine } from '@simpli/funnels/core';
|
|
345
|
+
|
|
346
|
+
const engine = new FunnelEngine();
|
|
347
|
+
|
|
348
|
+
// Synchronous execution
|
|
349
|
+
const results = engine.executeSync(funnel, entities);
|
|
350
|
+
|
|
351
|
+
// Get matched entities
|
|
352
|
+
const matched = results.matched.map(r => r.entity);
|
|
353
|
+
|
|
354
|
+
// Get excluded entities
|
|
355
|
+
const excluded = results.excluded.map(r => r.entity);
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### React Components
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import { FunnelCard, FunnelPreview, FunnelStageBuilder } from '@simpli/funnels/components';
|
|
362
|
+
|
|
363
|
+
// Display funnel card
|
|
364
|
+
<FunnelCard funnel={myFunnel} onEdit={handleEdit} onDelete={handleDelete} />
|
|
365
|
+
|
|
366
|
+
// Preview funnel results
|
|
367
|
+
<FunnelPreview funnel={myFunnel} entities={myData} />
|
|
368
|
+
|
|
369
|
+
// Build funnel stages
|
|
370
|
+
<FunnelStageBuilder
|
|
371
|
+
funnel={myFunnel}
|
|
372
|
+
fieldRegistry={registry}
|
|
373
|
+
onChange={handleChange}
|
|
374
|
+
/>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### State Management
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
import { createFunnelStore } from '@simpli/funnels/store';
|
|
381
|
+
|
|
382
|
+
const useFunnelStore = createFunnelStore();
|
|
383
|
+
|
|
384
|
+
function MyComponent() {
|
|
385
|
+
const { funnel, updateStage, addStage } = useFunnelStore();
|
|
386
|
+
|
|
387
|
+
// Use store methods
|
|
388
|
+
addStage(newStage);
|
|
389
|
+
updateStage(stage.id, { name: 'New Name' });
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## Architecture
|
|
394
|
+
|
|
395
|
+
### Why Brutally Generic?
|
|
396
|
+
|
|
397
|
+
Traditional filtering systems hardcode domain models:
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// ❌ Domain-specific (not reusable)
|
|
401
|
+
interface InvestorFilter {
|
|
402
|
+
firm_stage: string[];
|
|
403
|
+
check_size_min: number;
|
|
404
|
+
geography: string[];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
interface RecipeFilter {
|
|
408
|
+
cuisine: string[];
|
|
409
|
+
prep_time_max: number;
|
|
410
|
+
dietary: string[];
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
With @simpli/funnels, one system handles everything:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// ✅ Brutally generic (reusable everywhere)
|
|
418
|
+
interface FilterRule {
|
|
419
|
+
field_path: string; // Works for ANY field
|
|
420
|
+
operator: Operator; // Works for ANY comparison
|
|
421
|
+
value: any; // Works for ANY value
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**Benefits:**
|
|
426
|
+
- Write filtering logic once, use everywhere
|
|
427
|
+
- Add new domains without code changes
|
|
428
|
+
- Type-safe with TypeScript generics
|
|
429
|
+
- Test once, trust everywhere
|
|
430
|
+
|
|
431
|
+
### Sequential Stage Processing
|
|
432
|
+
|
|
433
|
+
Stages execute in order (0, 1, 2, ...). Each stage can:
|
|
434
|
+
|
|
435
|
+
1. **Continue** - Pass entity to next stage
|
|
436
|
+
2. **Exclude** - Remove entity from output (stop processing)
|
|
437
|
+
3. **Tag** - Add tags and stop processing
|
|
438
|
+
4. **Tag + Continue** - Add tags and pass to next stage
|
|
439
|
+
5. **Output** - Mark as matched (final stage)
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
Stage 0: Filter by investment stage
|
|
443
|
+
├─ Match → tag 'qualified_stage', continue
|
|
444
|
+
└─ No match → exclude
|
|
445
|
+
|
|
446
|
+
Stage 1: Filter by check size
|
|
447
|
+
├─ Match → tag 'qualified_check_size', continue
|
|
448
|
+
└─ No match → tag 'excluded_check_size', exclude
|
|
449
|
+
|
|
450
|
+
Stage 2: Filter by geography
|
|
451
|
+
├─ Match → output (final)
|
|
452
|
+
└─ No match → exclude
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Accumulated State
|
|
456
|
+
|
|
457
|
+
Tags and context accumulate across stages:
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
{
|
|
461
|
+
entity: { /* investor data */ },
|
|
462
|
+
matched: true,
|
|
463
|
+
accumulated_tags: ['qualified_stage', 'qualified_check_size', 'qualified_geography'],
|
|
464
|
+
context: {
|
|
465
|
+
stage: 'qualified',
|
|
466
|
+
tier: 'A',
|
|
467
|
+
score: 100
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
## Examples
|
|
473
|
+
|
|
474
|
+
See [EXAMPLES.md](./EXAMPLES.md) for 6+ real-world examples:
|
|
475
|
+
|
|
476
|
+
- Investor qualification funnel
|
|
477
|
+
- Recipe recommendation funnel
|
|
478
|
+
- Lead scoring funnel
|
|
479
|
+
- GitHub issue triage funnel
|
|
480
|
+
- E-commerce product filtering
|
|
481
|
+
- Task prioritization funnel
|
|
482
|
+
|
|
483
|
+
## Integration
|
|
484
|
+
|
|
485
|
+
See [INTEGRATION_GUIDE.md](./INTEGRATION_GUIDE.md) for step-by-step integration instructions.
|
|
486
|
+
|
|
487
|
+
Quick summary:
|
|
488
|
+
|
|
489
|
+
1. Install package: `npm install @simpli/funnels`
|
|
490
|
+
2. Create field registry for your domain
|
|
491
|
+
3. Use components in your UI
|
|
492
|
+
4. Connect to your API/backend
|
|
493
|
+
5. Configure Tailwind CSS (if using components)
|
|
494
|
+
|
|
495
|
+
## Storybook
|
|
496
|
+
|
|
497
|
+
Explore 50+ interactive examples:
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
cd packages/funnels
|
|
501
|
+
npm run storybook
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Or view the built Storybook in `./storybook-static/index.html`
|
|
505
|
+
|
|
506
|
+
See [STORYBOOK.md](./STORYBOOK.md) for details.
|
|
507
|
+
|
|
508
|
+
## Testing
|
|
509
|
+
|
|
510
|
+
The package includes comprehensive test coverage:
|
|
511
|
+
|
|
512
|
+
```bash
|
|
513
|
+
npm run test # Run all tests
|
|
514
|
+
npm run test:watch # Watch mode
|
|
515
|
+
npm run test:coverage # Coverage report
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
**Test stats:**
|
|
519
|
+
- **487 tests** passing
|
|
520
|
+
- **Core engine**: 262 tests
|
|
521
|
+
- **Components**: 185 tests
|
|
522
|
+
- **Store**: 29 tests
|
|
523
|
+
- **API client**: 24 tests
|
|
524
|
+
|
|
525
|
+
## Contributing
|
|
526
|
+
|
|
527
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for contribution guidelines.
|
|
528
|
+
|
|
529
|
+
Quick summary:
|
|
530
|
+
|
|
531
|
+
1. Fork the repo
|
|
532
|
+
2. Create a feature branch
|
|
533
|
+
3. Make your changes
|
|
534
|
+
4. Add tests
|
|
535
|
+
5. Run `npm test` and `npm run type-check`
|
|
536
|
+
6. Submit a PR
|
|
537
|
+
|
|
538
|
+
## Changelog
|
|
539
|
+
|
|
540
|
+
See [CHANGELOG.md](./CHANGELOG.md) for version history.
|
|
541
|
+
|
|
542
|
+
## License
|
|
543
|
+
|
|
544
|
+
MIT - See [LICENSE](./LICENSE) for details.
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## Why "Brutally Generic"?
|
|
549
|
+
|
|
550
|
+
Because it works for **literally anything**:
|
|
551
|
+
|
|
552
|
+
- ✅ Investors (VC fundraising)
|
|
553
|
+
- ✅ Recipes (cooking app)
|
|
554
|
+
- ✅ Leads (sales CRM)
|
|
555
|
+
- ✅ GitHub issues (project management)
|
|
556
|
+
- ✅ Products (e-commerce)
|
|
557
|
+
- ✅ Tasks (task manager)
|
|
558
|
+
- ✅ Your domain (whatever it is)
|
|
559
|
+
|
|
560
|
+
**Zero domain-specific types. Just generic entity processing with rich filtering capabilities.**
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
Built with ❤️ by the Simpli team
|