@startsimpli/funnels 0.1.3 → 0.1.5

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 -3243
  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 -3196
  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 -21
  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 -19
  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 -391
  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 -388
  151. package/dist/store/index.js.map +0 -1
@@ -0,0 +1,261 @@
1
+ /**
2
+ * @startsimpli/funnels - Rule Evaluator
3
+ *
4
+ * BRUTALLY GENERIC rule evaluation engine.
5
+ *
6
+ * Evaluates FilterRule against ANY entity type by:
7
+ * 1. Resolving field value from entity
8
+ * 2. Applying operator comparison
9
+ * 3. Handling negate flag
10
+ * 4. Returning boolean result
11
+ */
12
+
13
+ import type { FilterRule, RuleResult } from '../types';
14
+ import { resolveField } from './field-resolver';
15
+ import { applyOperator } from './operators';
16
+
17
+ /**
18
+ * Evaluate a single filter rule against an entity
19
+ *
20
+ * @param entity - The entity to evaluate
21
+ * @param rule - The filter rule to apply
22
+ * @returns Whether the rule matches
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const investor = { name: 'John', firm: { stage: 'Series A' } };
27
+ *
28
+ * evaluateRule(investor, {
29
+ * field_path: 'firm.stage',
30
+ * operator: 'eq',
31
+ * value: 'Series A'
32
+ * }); // true
33
+ *
34
+ * evaluateRule(investor, {
35
+ * field_path: 'firm.stage',
36
+ * operator: 'eq',
37
+ * value: 'Seed',
38
+ * negate: true
39
+ * }); // true (negated: stage !== 'Seed')
40
+ * ```
41
+ */
42
+ export function evaluateRule<T>(entity: T, rule: FilterRule): boolean {
43
+ // Resolve field value from entity
44
+ const actualValue = resolveField(entity, rule.field_path);
45
+
46
+ // Apply operator
47
+ const result = applyOperator(rule.operator, actualValue, rule.value);
48
+
49
+ // Handle negate flag
50
+ return rule.negate ? !result : result;
51
+ }
52
+
53
+ /**
54
+ * Evaluate a rule and return detailed result with diagnostics
55
+ *
56
+ * @param entity - The entity to evaluate
57
+ * @param rule - The filter rule to apply
58
+ * @returns Detailed rule result with actual value and match status
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * const result = evaluateRuleWithResult(investor, {
63
+ * field_path: 'firm.stage',
64
+ * operator: 'eq',
65
+ * value: 'Series A'
66
+ * });
67
+ * // {
68
+ * // field_path: 'firm.stage',
69
+ * // operator: 'eq',
70
+ * // value: 'Series A',
71
+ * // actual_value: 'Series A',
72
+ * // matched: true
73
+ * // }
74
+ * ```
75
+ */
76
+ export function evaluateRuleWithResult<T>(
77
+ entity: T,
78
+ rule: FilterRule
79
+ ): RuleResult {
80
+ try {
81
+ // Resolve field value
82
+ const actualValue = resolveField(entity, rule.field_path);
83
+
84
+ // Apply operator
85
+ const operatorResult = applyOperator(rule.operator, actualValue, rule.value);
86
+
87
+ // Handle negate flag
88
+ const matched = rule.negate ? !operatorResult : operatorResult;
89
+
90
+ return {
91
+ field_path: rule.field_path,
92
+ operator: rule.operator,
93
+ value: rule.value,
94
+ actual_value: actualValue,
95
+ matched,
96
+ };
97
+ } catch (error) {
98
+ // Handle evaluation errors gracefully
99
+ return {
100
+ field_path: rule.field_path,
101
+ operator: rule.operator,
102
+ value: rule.value,
103
+ actual_value: undefined,
104
+ matched: false,
105
+ error: error instanceof Error ? error.message : String(error),
106
+ };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Evaluate multiple rules with AND logic
112
+ *
113
+ * @param entity - The entity to evaluate
114
+ * @param rules - Array of filter rules
115
+ * @returns Whether ALL rules match
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * const investor = { name: 'John', firm: { stage: 'Series A', aum: 100000000 } };
120
+ *
121
+ * evaluateRulesAND(investor, [
122
+ * { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
123
+ * { field_path: 'firm.aum', operator: 'gte', value: 50000000 }
124
+ * ]); // true (both rules match)
125
+ * ```
126
+ */
127
+ export function evaluateRulesAND<T>(entity: T, rules: FilterRule[]): boolean {
128
+ if (!rules || rules.length === 0) return true; // Empty rules = match
129
+
130
+ return rules.every(rule => evaluateRule(entity, rule));
131
+ }
132
+
133
+ /**
134
+ * Evaluate multiple rules with OR logic
135
+ *
136
+ * @param entity - The entity to evaluate
137
+ * @param rules - Array of filter rules
138
+ * @returns Whether AT LEAST ONE rule matches
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * const investor = { name: 'John', firm: { stage: 'Seed' } };
143
+ *
144
+ * evaluateRulesOR(investor, [
145
+ * { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
146
+ * { field_path: 'firm.stage', operator: 'eq', value: 'Seed' }
147
+ * ]); // true (second rule matches)
148
+ * ```
149
+ */
150
+ export function evaluateRulesOR<T>(entity: T, rules: FilterRule[]): boolean {
151
+ if (!rules || rules.length === 0) return true; // Empty rules = match
152
+
153
+ return rules.some(rule => evaluateRule(entity, rule));
154
+ }
155
+
156
+ /**
157
+ * Evaluate multiple rules with specified logic (AND/OR)
158
+ *
159
+ * @param entity - The entity to evaluate
160
+ * @param rules - Array of filter rules
161
+ * @param logic - Combination logic ('AND' or 'OR')
162
+ * @returns Whether rules match based on logic
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * evaluateRules(investor, rules, 'AND'); // All must match
167
+ * evaluateRules(investor, rules, 'OR'); // At least one must match
168
+ * ```
169
+ */
170
+ export function evaluateRules<T>(
171
+ entity: T,
172
+ rules: FilterRule[],
173
+ logic: 'AND' | 'OR' = 'AND'
174
+ ): boolean {
175
+ return logic === 'AND'
176
+ ? evaluateRulesAND(entity, rules)
177
+ : evaluateRulesOR(entity, rules);
178
+ }
179
+
180
+ /**
181
+ * Evaluate rules and return detailed results for each rule
182
+ *
183
+ * @param entity - The entity to evaluate
184
+ * @param rules - Array of filter rules
185
+ * @param logic - Combination logic ('AND' or 'OR')
186
+ * @returns Object with overall match status and per-rule results
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * const result = evaluateRulesWithResults(investor, [
191
+ * { field_path: 'firm.stage', operator: 'eq', value: 'Series A' },
192
+ * { field_path: 'firm.aum', operator: 'gte', value: 50000000 }
193
+ * ], 'AND');
194
+ * // {
195
+ * // matched: true,
196
+ * // logic: 'AND',
197
+ * // rule_results: [
198
+ * // { field_path: 'firm.stage', operator: 'eq', value: 'Series A', actual_value: 'Series A', matched: true },
199
+ * // { field_path: 'firm.aum', operator: 'gte', value: 50000000, actual_value: 100000000, matched: true }
200
+ * // ]
201
+ * // }
202
+ * ```
203
+ */
204
+ export function evaluateRulesWithResults<T>(
205
+ entity: T,
206
+ rules: FilterRule[],
207
+ logic: 'AND' | 'OR' = 'AND'
208
+ ): {
209
+ matched: boolean;
210
+ logic: 'AND' | 'OR';
211
+ rule_results: RuleResult[];
212
+ } {
213
+ const ruleResults = rules.map(rule => evaluateRuleWithResult(entity, rule));
214
+
215
+ const matched =
216
+ logic === 'AND'
217
+ ? ruleResults.every(r => r.matched)
218
+ : ruleResults.some(r => r.matched);
219
+
220
+ return {
221
+ matched,
222
+ logic,
223
+ rule_results: ruleResults,
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Filter an array of entities using rules
229
+ *
230
+ * @param entities - Array of entities to filter
231
+ * @param rules - Filter rules to apply
232
+ * @param logic - Combination logic ('AND' or 'OR')
233
+ * @returns Entities that match the rules
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * const investors = [
238
+ * { name: 'John', firm: { stage: 'Seed' } },
239
+ * { name: 'Jane', firm: { stage: 'Series A' } },
240
+ * { name: 'Bob', firm: { stage: 'Series B' } }
241
+ * ];
242
+ *
243
+ * filterEntities(investors, [
244
+ * { field_path: 'firm.stage', operator: 'in', value: ['Series A', 'Series B'] }
245
+ * ]);
246
+ * // [
247
+ * // { name: 'Jane', firm: { stage: 'Series A' } },
248
+ * // { name: 'Bob', firm: { stage: 'Series B' } }
249
+ * // ]
250
+ * ```
251
+ */
252
+ export function filterEntities<T>(
253
+ entities: T[],
254
+ rules: FilterRule[],
255
+ logic: 'AND' | 'OR' = 'AND'
256
+ ): T[] {
257
+ if (!entities || entities.length === 0) return [];
258
+ if (!rules || rules.length === 0) return entities; // No rules = return all
259
+
260
+ return entities.filter(entity => evaluateRules(entity, rules, logic));
261
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * @startsimpli/funnels - Field Resolver Usage Examples
3
+ *
4
+ * This file demonstrates how to use the field resolver utilities
5
+ * for accessing and modifying entity properties.
6
+ */
7
+
8
+ import { resolveField, setField, hasField, getFields } from './field-resolver';
9
+
10
+ // Example 1: Simple investor entity
11
+ const investor = {
12
+ id: '123',
13
+ name: 'John Doe',
14
+ email: 'john@example.com',
15
+ firm: {
16
+ id: 'firm-456',
17
+ name: 'Sequoia Capital',
18
+ stage: 'Series A',
19
+ aum_usd: 100000000,
20
+ },
21
+ tags: ['active', 'qualified', 'enterprise'],
22
+ profile: {
23
+ linkedin_url: 'https://linkedin.com/in/johndoe',
24
+ bio: 'Venture capitalist specializing in early-stage fintech',
25
+ },
26
+ metrics: {
27
+ response_rate: 0.85,
28
+ deal_count: 12,
29
+ },
30
+ created_at: new Date('2024-01-01'),
31
+ };
32
+
33
+ // Resolve simple fields
34
+ console.log('Name:', resolveField(investor, 'name')); // 'John Doe'
35
+ console.log('Email:', resolveField(investor, 'email')); // 'john@example.com'
36
+
37
+ // Resolve nested fields
38
+ console.log('Firm Name:', resolveField(investor, 'firm.name')); // 'Sequoia Capital'
39
+ console.log('AUM:', resolveField(investor, 'firm.aum_usd')); // 100000000
40
+ console.log('LinkedIn:', resolveField(investor, 'profile.linkedin_url'));
41
+
42
+ // Resolve array elements
43
+ console.log('First Tag:', resolveField(investor, 'tags[0]')); // 'active'
44
+ console.log('Second Tag:', resolveField(investor, 'tags[1]')); // 'qualified'
45
+
46
+ // Check field existence
47
+ console.log('Has email?', hasField(investor, 'email')); // true
48
+ console.log('Has phone?', hasField(investor, 'phone')); // false
49
+
50
+ // Get multiple fields at once
51
+ const fields = getFields(investor, [
52
+ 'name',
53
+ 'firm.stage',
54
+ 'metrics.response_rate',
55
+ 'tags[0]',
56
+ ]);
57
+ console.log('Multiple fields:', fields);
58
+ // {
59
+ // name: 'John Doe',
60
+ // 'firm.stage': 'Series A',
61
+ // 'metrics.response_rate': 0.85,
62
+ // 'tags[0]': 'active'
63
+ // }
64
+
65
+ // Example 2: Recipe entity
66
+ const recipe = {
67
+ id: 'recipe-789',
68
+ title: 'Chocolate Chip Cookies',
69
+ cuisine: 'American',
70
+ difficulty: 'easy',
71
+ ingredients: [
72
+ { name: 'flour', amount: '2 cups', unit: 'cup' },
73
+ { name: 'sugar', amount: '1 cup', unit: 'cup' },
74
+ { name: 'chocolate chips', amount: '2 cups', unit: 'cup' },
75
+ ],
76
+ steps: [
77
+ { order: 1, instruction: 'Mix dry ingredients' },
78
+ { order: 2, instruction: 'Add wet ingredients' },
79
+ { order: 3, instruction: 'Bake at 350°F for 12 minutes' },
80
+ ],
81
+ metadata: {
82
+ prep_time_minutes: 15,
83
+ cook_time_minutes: 12,
84
+ servings: 24,
85
+ },
86
+ };
87
+
88
+ // Access nested array objects
89
+ console.log('First ingredient:', resolveField(recipe, 'ingredients[0].name')); // 'flour'
90
+ console.log('Ingredient amount:', resolveField(recipe, 'ingredients[2].amount')); // '2 cups'
91
+ console.log('Third step:', resolveField(recipe, 'steps[2].instruction'));
92
+
93
+ // Example 3: Using setField for tag assignment
94
+ const entity: any = {
95
+ id: '456',
96
+ name: 'Jane Smith',
97
+ };
98
+
99
+ // Add tags dynamically
100
+ setField(entity, 'tags', ['new_lead', 'high_priority']);
101
+ console.log('Entity with tags:', entity);
102
+ // { id: '456', name: 'Jane Smith', tags: ['new_lead', 'high_priority'] }
103
+
104
+ // Add nested metadata
105
+ setField(entity, 'metadata.score', 95);
106
+ setField(entity, 'metadata.tier', 'premium');
107
+ console.log('Entity with metadata:', entity);
108
+ // {
109
+ // id: '456',
110
+ // name: 'Jane Smith',
111
+ // tags: ['new_lead', 'high_priority'],
112
+ // metadata: { score: 95, tier: 'premium' }
113
+ // }
114
+
115
+ // Example 4: Real-world funnel rule evaluation
116
+ interface FunnelEntity {
117
+ id: string;
118
+ firm?: {
119
+ stage?: string;
120
+ check_size_usd?: number;
121
+ };
122
+ tags?: string[];
123
+ metrics?: {
124
+ response_rate?: number;
125
+ };
126
+ }
127
+
128
+ function evaluateRule(entity: FunnelEntity, fieldPath: string, operator: string, value: any): boolean {
129
+ const actualValue = resolveField(entity, fieldPath);
130
+
131
+ switch (operator) {
132
+ case 'eq':
133
+ return actualValue === value;
134
+ case 'gt':
135
+ return actualValue > value;
136
+ case 'in':
137
+ return Array.isArray(value) && value.includes(actualValue);
138
+ case 'has_tag':
139
+ return Array.isArray(actualValue) && actualValue.includes(value);
140
+ default:
141
+ return false;
142
+ }
143
+ }
144
+
145
+ // Test the rule evaluator
146
+ const testEntity: FunnelEntity = {
147
+ id: '1',
148
+ firm: {
149
+ stage: 'Series A',
150
+ check_size_usd: 5000000,
151
+ },
152
+ tags: ['qualified', 'enterprise'],
153
+ metrics: {
154
+ response_rate: 0.85,
155
+ },
156
+ };
157
+
158
+ console.log('Stage is Series A?', evaluateRule(testEntity, 'firm.stage', 'eq', 'Series A')); // true
159
+ console.log('Check size > $1M?', evaluateRule(testEntity, 'firm.check_size_usd', 'gt', 1000000)); // true
160
+ console.log('Has qualified tag?', evaluateRule(testEntity, 'tags', 'has_tag', 'qualified')); // true
161
+ console.log('Response rate > 0.9?', evaluateRule(testEntity, 'metrics.response_rate', 'gt', 0.9)); // false
162
+
163
+ // Example 5: Safe navigation with missing paths
164
+ const partialEntity = {
165
+ id: '999',
166
+ name: 'Incomplete Entity',
167
+ };
168
+
169
+ // These all return undefined safely (no errors)
170
+ console.log('Missing firm:', resolveField(partialEntity, 'firm.stage')); // undefined
171
+ console.log('Missing tags:', resolveField(partialEntity, 'tags[0]')); // undefined
172
+ console.log('Deep missing:', resolveField(partialEntity, 'a.b.c.d.e.f')); // undefined
173
+
174
+ // hasField returns false for missing paths
175
+ console.log('Has firm.stage?', hasField(partialEntity, 'firm.stage')); // false