@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,227 @@
1
+ /**
2
+ * FunnelVisualFlow Example Usage
3
+ *
4
+ * This example demonstrates how to integrate FunnelVisualFlow
5
+ * into a real application with state management, API calls, etc.
6
+ */
7
+
8
+ import React, { useState, useEffect } from 'react';
9
+ import { FunnelVisualFlow } from './FunnelVisualFlow';
10
+ import { Funnel, FunnelStage, FunnelRun } from '../../types';
11
+
12
+ /**
13
+ * Example: Funnel Details Page
14
+ *
15
+ * Shows a funnel's visual flow with latest run data.
16
+ * Clicking a stage opens a details modal.
17
+ * Clicking an edge shows excluded entities.
18
+ */
19
+ export function FunnelDetailsPage({ funnelId }: { funnelId: string }) {
20
+ const [funnel, setFunnel] = useState<Funnel | null>(null);
21
+ const [latestRun, setLatestRun] = useState<FunnelRun | null>(null);
22
+ const [selectedStage, setSelectedStage] = useState<FunnelStage | null>(null);
23
+ const [isLoading, setIsLoading] = useState(true);
24
+
25
+ // Load funnel and latest run
26
+ useEffect(() => {
27
+ async function loadData() {
28
+ setIsLoading(true);
29
+ try {
30
+ // Fetch funnel
31
+ const funnelRes = await fetch(`/api/funnels/${funnelId}`);
32
+ const funnelData = await funnelRes.json();
33
+ setFunnel(funnelData);
34
+
35
+ // Fetch latest run
36
+ const runsRes = await fetch(`/api/funnels/${funnelId}/runs?limit=1`);
37
+ const runsData = await runsRes.json();
38
+ if (runsData.results.length > 0) {
39
+ setLatestRun(runsData.results[0]);
40
+ }
41
+ } catch (error) {
42
+ console.error('Failed to load funnel:', error);
43
+ } finally {
44
+ setIsLoading(false);
45
+ }
46
+ }
47
+
48
+ loadData();
49
+ }, [funnelId]);
50
+
51
+ const handleStageClick = (stage: FunnelStage) => {
52
+ setSelectedStage(stage);
53
+ // Could open a modal, navigate to stage details, etc.
54
+ };
55
+
56
+ const handleEdgeClick = (fromStageId: string, toStageId: string) => {
57
+ // Show excluded entities between these stages
58
+ console.log('Show excluded entities:', fromStageId, '->', toStageId);
59
+ // Could fetch excluded entities and show in a modal
60
+ };
61
+
62
+ if (isLoading) {
63
+ return <div>Loading funnel...</div>;
64
+ }
65
+
66
+ if (!funnel) {
67
+ return <div>Funnel not found</div>;
68
+ }
69
+
70
+ return (
71
+ <div className="funnel-details-page">
72
+ <header className="page-header">
73
+ <h1>{funnel.name}</h1>
74
+ {funnel.description && <p>{funnel.description}</p>}
75
+ </header>
76
+
77
+ <div className="funnel-flow-container">
78
+ <FunnelVisualFlow
79
+ funnel={funnel}
80
+ runData={latestRun || undefined}
81
+ onStageClick={handleStageClick}
82
+ onEdgeClick={handleEdgeClick}
83
+ height={800}
84
+ className="shadow-lg"
85
+ />
86
+ </div>
87
+
88
+ {/* Stage Details Modal */}
89
+ {selectedStage && (
90
+ <StageDetailsModal
91
+ stage={selectedStage}
92
+ onClose={() => setSelectedStage(null)}
93
+ />
94
+ )}
95
+
96
+ {/* Latest Run Info */}
97
+ {latestRun && (
98
+ <div className="run-info">
99
+ <h2>Latest Run</h2>
100
+ <p>Status: {latestRun.status}</p>
101
+ <p>Started: {new Date(latestRun.started_at).toLocaleString()}</p>
102
+ <p>
103
+ Results: {latestRun.total_matched} / {latestRun.total_input} matched
104
+ </p>
105
+ </div>
106
+ )}
107
+ </div>
108
+ );
109
+ }
110
+
111
+ /**
112
+ * Example: Stage Details Modal
113
+ */
114
+ function StageDetailsModal({
115
+ stage,
116
+ onClose,
117
+ }: {
118
+ stage: FunnelStage;
119
+ onClose: () => void;
120
+ }) {
121
+ return (
122
+ <div className="modal-overlay" onClick={onClose}>
123
+ <div className="modal-content" onClick={(e) => e.stopPropagation()}>
124
+ <header className="modal-header">
125
+ <h2>{stage.name}</h2>
126
+ <button onClick={onClose}>×</button>
127
+ </header>
128
+
129
+ <div className="modal-body">
130
+ {stage.description && <p>{stage.description}</p>}
131
+
132
+ <h3>Rules ({stage.rules.length})</h3>
133
+ <ul>
134
+ {stage.rules.map((rule, index) => (
135
+ <li key={index}>
136
+ {rule.field_path} {rule.operator} {JSON.stringify(rule.value)}
137
+ </li>
138
+ ))}
139
+ </ul>
140
+
141
+ <h3>Actions</h3>
142
+ <p>Match: {stage.match_action}</p>
143
+ <p>No Match: {stage.no_match_action}</p>
144
+
145
+ {stage.match_tags && stage.match_tags.length > 0 && (
146
+ <>
147
+ <h3>Match Tags</h3>
148
+ <ul>
149
+ {stage.match_tags.map((tag) => (
150
+ <li key={tag}>{tag}</li>
151
+ ))}
152
+ </ul>
153
+ </>
154
+ )}
155
+ </div>
156
+ </div>
157
+ </div>
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Example: Embedded in Dashboard
163
+ *
164
+ * Shows multiple funnels with their visual flows in a grid.
165
+ */
166
+ export function FunnelsDashboard() {
167
+ const [funnels, setFunnels] = useState<Funnel[]>([]);
168
+
169
+ useEffect(() => {
170
+ async function loadFunnels() {
171
+ const res = await fetch('/api/funnels');
172
+ const data = await res.json();
173
+ setFunnels(data.results);
174
+ }
175
+ loadFunnels();
176
+ }, []);
177
+
178
+ return (
179
+ <div className="funnels-dashboard">
180
+ <h1>Active Funnels</h1>
181
+
182
+ <div className="funnels-grid">
183
+ {funnels.map((funnel) => (
184
+ <div key={funnel.id} className="funnel-card">
185
+ <h2>{funnel.name}</h2>
186
+ <FunnelVisualFlow
187
+ funnel={funnel}
188
+ height={400}
189
+ onStageClick={(stage) => {
190
+ console.log('Stage clicked:', stage.name);
191
+ }}
192
+ />
193
+ </div>
194
+ ))}
195
+ </div>
196
+ </div>
197
+ );
198
+ }
199
+
200
+ /**
201
+ * Example: Side-by-Side Comparison
202
+ *
203
+ * Compare two funnels visually.
204
+ */
205
+ export function FunnelComparison({
206
+ funnelA,
207
+ funnelB,
208
+ }: {
209
+ funnelA: Funnel;
210
+ funnelB: Funnel;
211
+ }) {
212
+ return (
213
+ <div className="funnel-comparison">
214
+ <div className="comparison-side">
215
+ <h2>{funnelA.name}</h2>
216
+ <FunnelVisualFlow funnel={funnelA} height={600} />
217
+ </div>
218
+
219
+ <div className="comparison-divider" />
220
+
221
+ <div className="comparison-side">
222
+ <h2>{funnelB.name}</h2>
223
+ <FunnelVisualFlow funnel={funnelB} height={600} />
224
+ </div>
225
+ </div>
226
+ );
227
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * FunnelVisualFlow Component Exports
3
+ */
4
+
5
+ export { FunnelVisualFlow } from './FunnelVisualFlow';
6
+ export type { FunnelVisualFlowProps } from './FunnelVisualFlow';
7
+ export { StageNode } from './StageNode';
8
+ export type { StageNodeData } from './StageNode';
9
+ export { FlowLegend } from './FlowLegend';
10
+ export { getCircledNumber } from './FunnelVisualFlow';
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @startsimpli/funnels/components
3
+ *
4
+ * React components for building funnel UIs
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ // Export FunnelPreview
10
+ export {
11
+ FunnelPreview,
12
+ PreviewStats,
13
+ StageBreakdown,
14
+ EntityCard,
15
+ LoadingPreview,
16
+ } from './FunnelPreview';
17
+ export type {
18
+ FunnelPreviewProps,
19
+ PreviewResult,
20
+ StagePreviewStats,
21
+ } from './FunnelPreview';
22
+
23
+ // Export FunnelCard
24
+ export {
25
+ FunnelCard,
26
+ StatusBadge,
27
+ StageIndicator,
28
+ MatchBar,
29
+ FunnelStats,
30
+ } from './FunnelCard';
31
+ export type { FunnelCardProps } from './FunnelCard';
32
+
33
+ // Export FunnelVisualFlow
34
+ export {
35
+ FunnelVisualFlow,
36
+ StageNode,
37
+ FlowLegend,
38
+ getCircledNumber,
39
+ } from './FunnelVisualFlow';
40
+ export type {
41
+ FunnelVisualFlowProps,
42
+ StageNodeData,
43
+ } from './FunnelVisualFlow';
44
+
45
+ // Export FilterRuleEditor
46
+ export {
47
+ FilterRuleEditor,
48
+ LogicToggle,
49
+ FieldSelector,
50
+ OperatorSelector,
51
+ RuleRow,
52
+ TextValueInput,
53
+ NumberValueInput,
54
+ DateValueInput,
55
+ BooleanValueInput,
56
+ ChoiceValueInput,
57
+ MultiChoiceValueInput,
58
+ OPERATOR_LABELS,
59
+ NULL_VALUE_OPERATORS,
60
+ MULTI_VALUE_OPERATORS,
61
+ } from './FilterRuleEditor';
62
+ export type { FilterRuleEditorProps } from './FilterRuleEditor';
63
+
64
+ // Export FunnelStageBuilder
65
+ export {
66
+ FunnelStageBuilder,
67
+ StageCard,
68
+ StageForm,
69
+ StageActions,
70
+ TagInput,
71
+ AddStageButton,
72
+ } from './FunnelStageBuilder';
73
+ export type {
74
+ FunnelStageBuilderProps,
75
+ StageCardProps,
76
+ StageFormProps,
77
+ StageActionsProps,
78
+ TagInputProps,
79
+ AddStageButtonProps,
80
+ } from './FunnelStageBuilder';
81
+
82
+ // Export FunnelRunHistory
83
+ export {
84
+ FunnelRunHistory,
85
+ RunStatusBadge,
86
+ RunFilters,
87
+ RunRow,
88
+ RunActions,
89
+ RunDetailsModal,
90
+ StageBreakdownList,
91
+ formatDuration,
92
+ formatRelativeTime,
93
+ calculateMatchRate,
94
+ formatNumber,
95
+ formatFullTimestamp,
96
+ } from './FunnelRunHistory';
97
+ export type {
98
+ RunFiltersType,
99
+ RunSort,
100
+ Pagination,
101
+ RunAction,
102
+ } from './FunnelRunHistory';
@@ -0,0 +1,307 @@
1
+ # @simpli/funnels - Core Evaluation Engine
2
+
3
+ BRUTALLY GENERIC rule evaluation engine for filtering ANY entity type.
4
+
5
+ ## Overview
6
+
7
+ The core evaluation engine consists of three main components:
8
+
9
+ 1. **Field Resolver** (`field-resolver.ts`) - Resolves field values from entities using dot-notation paths
10
+ 2. **Operators** (`operators.ts`) - Implements all comparison operators (eq, gt, contains, in, etc.)
11
+ 3. **Evaluator** (`evaluator.ts`) - Combines field resolution + operators to evaluate filter rules
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { evaluateRule, filterEntities } from '@simpli/funnels';
17
+ import type { FilterRule } from '@simpli/funnels';
18
+
19
+ // Define your entity type (any shape!)
20
+ interface Investor {
21
+ name: string;
22
+ firm: {
23
+ stage: string;
24
+ aum: number;
25
+ };
26
+ tags: string[];
27
+ }
28
+
29
+ const investors: Investor[] = [
30
+ { name: 'John', firm: { stage: 'Series A', aum: 100000000 }, tags: ['qualified'] },
31
+ { name: 'Jane', firm: { stage: 'Seed', aum: 50000000 }, tags: ['active'] },
32
+ ];
33
+
34
+ // Single rule evaluation
35
+ const rule: FilterRule = {
36
+ field_path: 'firm.aum',
37
+ operator: 'gte',
38
+ value: 100000000
39
+ };
40
+
41
+ console.log(evaluateRule(investors[0], rule)); // true
42
+ console.log(evaluateRule(investors[1], rule)); // false
43
+
44
+ // Filter array of entities
45
+ const rules: FilterRule[] = [
46
+ { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
47
+ { field_path: 'tags', operator: 'has_tag', value: 'qualified' }
48
+ ];
49
+
50
+ const qualified = filterEntities(investors, rules, 'AND');
51
+ // Returns: [{ name: 'John', ... }]
52
+ ```
53
+
54
+ ## Supported Operators
55
+
56
+ ### Equality
57
+ - `eq` - Equal to
58
+ - `ne` - Not equal to
59
+
60
+ ### Comparison (numbers, dates, strings)
61
+ - `gt` - Greater than
62
+ - `lt` - Less than
63
+ - `gte` - Greater than or equal
64
+ - `lte` - Less than or equal
65
+
66
+ ### String Operations (case-insensitive)
67
+ - `contains` - String contains substring
68
+ - `not_contains` - String does not contain substring
69
+ - `startswith` - String starts with
70
+ - `endswith` - String ends with
71
+ - `matches` - Regex match
72
+
73
+ ### Array Operations
74
+ - `in` - Value is in array
75
+ - `not_in` - Value is not in array
76
+ - `has_any` - Array has any of these values
77
+ - `has_all` - Array has all of these values
78
+
79
+ ### Null Checks
80
+ - `isnull` - Field is null/undefined
81
+ - `isnotnull` - Field is not null/undefined
82
+
83
+ ### Tag Operations
84
+ - `has_tag` - Entity has tag (case-insensitive)
85
+ - `not_has_tag` - Entity does not have tag
86
+
87
+ ### Boolean
88
+ - `is_true` - Boolean is true
89
+ - `is_false` - Boolean is false
90
+
91
+ ## Field Resolution
92
+
93
+ The evaluator uses dot-notation paths to access nested fields:
94
+
95
+ ```typescript
96
+ const entity = {
97
+ name: 'John',
98
+ firm: {
99
+ stage: 'Series A',
100
+ aum: 100000000
101
+ },
102
+ tags: ['qualified', 'active'],
103
+ metrics: {
104
+ arr_usd: 5000000
105
+ }
106
+ };
107
+
108
+ // Simple field
109
+ { field_path: 'name', operator: 'eq', value: 'John' }
110
+
111
+ // Nested field
112
+ { field_path: 'firm.stage', operator: 'eq', value: 'Series A' }
113
+
114
+ // Deep nesting
115
+ { field_path: 'metrics.arr_usd', operator: 'gte', value: 1000000 }
116
+
117
+ // Array field
118
+ { field_path: 'tags', operator: 'has_tag', value: 'qualified' }
119
+ ```
120
+
121
+ ## Type Handling
122
+
123
+ The evaluator automatically handles type conversions:
124
+
125
+ ### Numbers
126
+ ```typescript
127
+ // Direct comparison
128
+ { field_path: 'age', operator: 'gt', value: 30 }
129
+
130
+ // Numeric string comparison
131
+ { field_path: 'age', operator: 'gt', value: '30' } // Coerced to number
132
+ ```
133
+
134
+ ### Dates
135
+ ```typescript
136
+ // Date object
137
+ { field_path: 'created_at', operator: 'gte', value: new Date('2024-01-01') }
138
+
139
+ // Date string
140
+ { field_path: 'created_at', operator: 'gte', value: '2024-01-01' }
141
+
142
+ // Mixed (Date vs string)
143
+ { field_path: 'created_at', operator: 'eq', value: '2024-01-15' } // Works!
144
+ ```
145
+
146
+ ### Strings
147
+ ```typescript
148
+ // Case-insensitive operations
149
+ { field_path: 'name', operator: 'contains', value: 'john' } // Matches 'John Doe'
150
+ { field_path: 'email', operator: 'endswith', value: '@ACME.COM' } // Matches '@acme.com'
151
+
152
+ // Regex
153
+ { field_path: 'email', operator: 'matches', value: '^[a-z]+@example\\.com$' }
154
+ ```
155
+
156
+ ### Arrays
157
+ ```typescript
158
+ // Check if value is in array
159
+ { field_path: 'firm.stage', operator: 'in', value: ['Series A', 'Series B'] }
160
+
161
+ // Check if array has value
162
+ { field_path: 'tags', operator: 'has_tag', value: 'qualified' }
163
+
164
+ // Check if array has all values
165
+ { field_path: 'tags', operator: 'has_all', value: ['active', 'qualified'] }
166
+
167
+ // Check if array has any value
168
+ { field_path: 'tags', operator: 'has_any', value: ['premium', 'enterprise'] }
169
+ ```
170
+
171
+ ### Null/Undefined
172
+ ```typescript
173
+ // Check if field is null
174
+ { field_path: 'email', operator: 'isnull', value: null }
175
+
176
+ // Check if field exists
177
+ { field_path: 'profile.linkedin_url', operator: 'isnotnull', value: null }
178
+ ```
179
+
180
+ ## Multiple Rules
181
+
182
+ Combine multiple rules with AND/OR logic:
183
+
184
+ ### AND Logic (all rules must match)
185
+ ```typescript
186
+ const rules: FilterRule[] = [
187
+ { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
188
+ { field_path: 'firm.aum', operator: 'gte', value: 100000000 },
189
+ { field_path: 'tags', operator: 'has_tag', value: 'qualified' }
190
+ ];
191
+
192
+ // All rules must pass
193
+ const qualified = filterEntities(investors, rules, 'AND');
194
+
195
+ // Or using helper
196
+ const match = evaluateRulesAND(investor, rules);
197
+ ```
198
+
199
+ ### OR Logic (at least one rule must match)
200
+ ```typescript
201
+ const rules: FilterRule[] = [
202
+ { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
203
+ { field_path: 'firm.aum', operator: 'gte', value: 500000000 } // OR very large
204
+ ];
205
+
206
+ // Any rule can pass
207
+ const flexible = filterEntities(investors, rules, 'OR');
208
+
209
+ // Or using helper
210
+ const match = evaluateRulesOR(investor, rules);
211
+ ```
212
+
213
+ ## Negation
214
+
215
+ Use the `negate` flag to invert a rule's result:
216
+
217
+ ```typescript
218
+ // Match investors NOT in Series A
219
+ const rule: FilterRule = {
220
+ field_path: 'firm.stage',
221
+ operator: 'eq',
222
+ value: 'Series A',
223
+ negate: true // Invert the result
224
+ };
225
+
226
+ // Match investors WITHOUT 'archived' tag
227
+ const notArchived: FilterRule = {
228
+ field_path: 'tags',
229
+ operator: 'has_tag',
230
+ value: 'archived',
231
+ negate: true
232
+ };
233
+ ```
234
+
235
+ ## Detailed Results
236
+
237
+ Get diagnostic information about rule evaluation:
238
+
239
+ ```typescript
240
+ import { evaluateRuleWithResult, evaluateRulesWithResults } from '@simpli/funnels';
241
+
242
+ // Single rule with details
243
+ const result = evaluateRuleWithResult(investor, {
244
+ field_path: 'firm.aum',
245
+ operator: 'gte',
246
+ value: 100000000
247
+ });
248
+
249
+ console.log(result);
250
+ // {
251
+ // field_path: 'firm.aum',
252
+ // operator: 'gte',
253
+ // value: 100000000,
254
+ // actual_value: 150000000,
255
+ // matched: true
256
+ // }
257
+
258
+ // Multiple rules with details
259
+ const results = evaluateRulesWithResults(investor, rules, 'AND');
260
+
261
+ console.log(results);
262
+ // {
263
+ // matched: true,
264
+ // logic: 'AND',
265
+ // rule_results: [
266
+ // { field_path: 'firm.stage', operator: 'eq', value: 'Series A', actual_value: 'Series A', matched: true },
267
+ // { field_path: 'firm.aum', operator: 'gte', value: 100000000, actual_value: 150000000, matched: true }
268
+ // ]
269
+ // }
270
+ ```
271
+
272
+ ## Testing
273
+
274
+ The evaluation engine has comprehensive test coverage:
275
+
276
+ - `operators.test.ts` - 105 tests covering all operators with various data types
277
+ - `evaluator.test.ts` - 76 tests covering rule evaluation, multiple rules, filtering
278
+ - `field-resolver.test.ts` - 57 tests covering field resolution logic
279
+
280
+ Total: **286 tests** ensuring robust operation.
281
+
282
+ Run tests:
283
+ ```bash
284
+ npm test
285
+ ```
286
+
287
+ ## Examples
288
+
289
+ See `evaluator.example.ts` for comprehensive examples including:
290
+ - Investor qualification funnels
291
+ - Recipe filtering with dietary restrictions
292
+ - GitHub repository filtering
293
+ - Multi-stage funnel simulation
294
+ - Advanced patterns (negation, regex, OR logic)
295
+
296
+ ## Philosophy
297
+
298
+ This evaluator follows the **BRUTALLY GENERIC** principle:
299
+
300
+ - Works with ANY entity type (no domain-specific code)
301
+ - Field paths use simple dot-notation (no magic)
302
+ - Operators are pure functions (no side effects)
303
+ - Type coercion is automatic and sensible
304
+ - Null/undefined handling is consistent
305
+ - Errors are graceful (no crashes)
306
+
307
+ **The goal: Write rules once, filter anything.**