@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,843 @@
1
+ /**
2
+ * @startsimpli/funnels - BRUTALLY GENERIC Funnel Type System
3
+ *
4
+ * This type system 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
+ * Zero domain-specific types. All filtering is based on field paths and operators.
11
+ */
12
+
13
+ // ============================================================================
14
+ // Core Operators
15
+ // ============================================================================
16
+
17
+ /**
18
+ * Filter operators - works with any data type
19
+ */
20
+ export type Operator =
21
+ // Equality
22
+ | 'eq' // Equal to
23
+ | 'ne' // Not equal to
24
+
25
+ // Comparison (numbers, dates, strings)
26
+ | 'gt' // Greater than
27
+ | 'lt' // Less than
28
+ | 'gte' // Greater than or equal
29
+ | 'lte' // Less than or equal
30
+
31
+ // String operations
32
+ | 'contains' // String contains substring
33
+ | 'not_contains' // String does not contain substring
34
+ | 'startswith' // String starts with
35
+ | 'endswith' // String ends with
36
+ | 'matches' // Regex match
37
+
38
+ // Array/Set operations
39
+ | 'in' // Value is in array
40
+ | 'not_in' // Value is not in array
41
+ | 'has_any' // Array has any of these values
42
+ | 'has_all' // Array has all of these values
43
+
44
+ // Null checks
45
+ | 'isnull' // Field is null/undefined
46
+ | 'isnotnull' // Field is not null/undefined
47
+
48
+ // Tag operations
49
+ | 'has_tag' // Entity has tag
50
+ | 'not_has_tag' // Entity does not have tag
51
+
52
+ // Boolean
53
+ | 'is_true' // Boolean is true
54
+ | 'is_false'; // Boolean is false
55
+
56
+ /**
57
+ * Field data types - for operator validation
58
+ */
59
+ export type FieldType =
60
+ | 'string'
61
+ | 'number'
62
+ | 'boolean'
63
+ | 'date'
64
+ | 'array'
65
+ | 'object'
66
+ | 'tag'
67
+ | 'any';
68
+
69
+ // ============================================================================
70
+ // Filter Rules
71
+ // ============================================================================
72
+
73
+ /**
74
+ * A single filter rule
75
+ *
76
+ * Examples:
77
+ * - { field_path: 'firm.stage', operator: 'eq', value: 'Series A' }
78
+ * - { field_path: 'recipe.cuisine', operator: 'in', value: ['Italian', 'French'] }
79
+ * - { field_path: 'contact.email', operator: 'isnotnull', value: null }
80
+ * - { field_path: 'organization.tags', operator: 'has_tag', value: 'enterprise' }
81
+ */
82
+ export interface FilterRule {
83
+ /**
84
+ * Dot-notation path to field
85
+ * Examples: 'name', 'firm.stage', 'profile.linkedin_url', 'tags', 'metrics.arr_usd'
86
+ */
87
+ field_path: string;
88
+
89
+ /** Comparison operator */
90
+ operator: Operator;
91
+
92
+ /**
93
+ * Value to compare against
94
+ * Type depends on operator:
95
+ * - eq/ne/gt/lt/gte/lte: any primitive
96
+ * - contains/startswith/endswith: string
97
+ * - in/not_in: array
98
+ * - isnull/isnotnull: null (value ignored)
99
+ * - has_tag/not_has_tag: string (tag name)
100
+ */
101
+ value: any;
102
+
103
+ /**
104
+ * Optional: negate the rule result
105
+ * Default: false
106
+ */
107
+ negate?: boolean;
108
+ }
109
+
110
+ /**
111
+ * Filter logic for combining rules
112
+ */
113
+ export type FilterLogic = 'AND' | 'OR';
114
+
115
+ /**
116
+ * Action to take when stage rules match
117
+ */
118
+ export type MatchAction =
119
+ | 'continue' // Continue to next stage
120
+ | 'tag' // Add tags and stop processing
121
+ | 'tag_continue' // Add tags and continue to next stage
122
+ | 'output'; // Add to output and stop processing
123
+
124
+ /**
125
+ * Action to take when stage rules don't match
126
+ */
127
+ export type NoMatchAction =
128
+ | 'continue' // Continue to next stage
129
+ | 'exclude' // Exclude from output and stop processing
130
+ | 'tag_exclude'; // Add tags, exclude from output, stop processing
131
+
132
+ // ============================================================================
133
+ // Funnel Stages
134
+ // ============================================================================
135
+
136
+ /**
137
+ * A single stage in a funnel
138
+ *
139
+ * Generic over entity type TEntity
140
+ */
141
+ export interface FunnelStage<TEntity = any> {
142
+ /** Unique stage identifier */
143
+ id: string;
144
+
145
+ /** Stage execution order (0-indexed) */
146
+ order: number;
147
+
148
+ /** Human-readable stage name */
149
+ name: string;
150
+
151
+ /** Optional description */
152
+ description?: string;
153
+
154
+ /**
155
+ * How to combine filter rules
156
+ * - AND: All rules must match
157
+ * - OR: At least one rule must match
158
+ */
159
+ filter_logic: FilterLogic;
160
+
161
+ /** Filter rules to evaluate */
162
+ rules: FilterRule[];
163
+
164
+ /** Action when rules match */
165
+ match_action: MatchAction;
166
+
167
+ /** Action when rules don't match */
168
+ no_match_action: NoMatchAction;
169
+
170
+ /** Tags to add when rules match */
171
+ match_tags?: string[];
172
+
173
+ /** Tags to add when rules don't match */
174
+ no_match_tags?: string[];
175
+
176
+ /**
177
+ * Context to add to entity when rules match
178
+ * Can be used to track why entity matched
179
+ * Examples:
180
+ * - { stage: 'qualified_leads', reason: 'high_fit_score' }
181
+ * - { tier: 'premium', discount: 0.2 }
182
+ */
183
+ match_context?: Record<string, any>;
184
+
185
+ /**
186
+ * Optional: Custom evaluation function
187
+ * For complex logic that can't be expressed with rules
188
+ */
189
+ custom_evaluator?: (entity: TEntity) => boolean;
190
+ }
191
+
192
+ // ============================================================================
193
+ // Funnel Definition
194
+ // ============================================================================
195
+
196
+ /**
197
+ * Funnel status
198
+ */
199
+ export type FunnelStatus = 'draft' | 'active' | 'paused' | 'archived';
200
+
201
+ /**
202
+ * Input entity types
203
+ */
204
+ export type InputType = 'contacts' | 'organizations' | 'both' | 'any';
205
+
206
+ /**
207
+ * A complete funnel definition
208
+ *
209
+ * Generic over entity type TEntity
210
+ */
211
+ export interface Funnel<TEntity = any> {
212
+ /** Unique funnel identifier */
213
+ id: string;
214
+
215
+ /** Human-readable funnel name */
216
+ name: string;
217
+
218
+ /** Optional description */
219
+ description?: string;
220
+
221
+ /** Funnel status */
222
+ status: FunnelStatus;
223
+
224
+ /**
225
+ * Type of entities this funnel processes
226
+ * Used for field registry lookup
227
+ */
228
+ input_type: InputType;
229
+
230
+ /** Ordered stages */
231
+ stages: FunnelStage<TEntity>[];
232
+
233
+ /** Created timestamp */
234
+ created_at: Date | string;
235
+
236
+ /** Last updated timestamp */
237
+ updated_at: Date | string;
238
+
239
+ /** Owner user ID */
240
+ owner_id?: string;
241
+
242
+ /** Team/company ID for multi-tenancy */
243
+ team_id?: string;
244
+
245
+ /**
246
+ * Optional: Tags to automatically add to all entities
247
+ * that complete the funnel
248
+ */
249
+ completion_tags?: string[];
250
+
251
+ /**
252
+ * Optional: Metadata for custom integrations
253
+ */
254
+ metadata?: Record<string, any>;
255
+ }
256
+
257
+ // ============================================================================
258
+ // Funnel Execution
259
+ // ============================================================================
260
+
261
+ /**
262
+ * Funnel run status
263
+ */
264
+ export type FunnelRunStatus =
265
+ | 'pending' // Queued, not started
266
+ | 'running' // Currently executing
267
+ | 'completed' // Finished successfully
268
+ | 'failed' // Error occurred
269
+ | 'cancelled'; // Manually stopped
270
+
271
+ /**
272
+ * How the funnel run was triggered
273
+ */
274
+ export type TriggerType =
275
+ | 'manual' // User initiated
276
+ | 'scheduled' // Cron/scheduled run
277
+ | 'webhook' // External event
278
+ | 'api'; // API call
279
+
280
+ /**
281
+ * Statistics for a single stage execution
282
+ */
283
+ export interface StageStats {
284
+ /** Stage ID */
285
+ stage_id: string;
286
+
287
+ /** Stage name */
288
+ stage_name: string;
289
+
290
+ /** Entities that entered this stage */
291
+ input_count: number;
292
+
293
+ /** Entities that matched rules */
294
+ matched_count: number;
295
+
296
+ /** Entities that didn't match rules */
297
+ not_matched_count: number;
298
+
299
+ /** Entities excluded at this stage */
300
+ excluded_count: number;
301
+
302
+ /** Entities tagged at this stage */
303
+ tagged_count: number;
304
+
305
+ /** Entities that continued to next stage */
306
+ continued_count: number;
307
+
308
+ /** Execution time in milliseconds */
309
+ duration_ms?: number;
310
+
311
+ /** Error count */
312
+ error_count?: number;
313
+ }
314
+
315
+ /**
316
+ * A funnel execution run
317
+ */
318
+ export interface FunnelRun {
319
+ /** Unique run identifier */
320
+ id: string;
321
+
322
+ /** Funnel ID */
323
+ funnel_id: string;
324
+
325
+ /** Funnel snapshot (copy of funnel at run time) */
326
+ funnel?: Funnel;
327
+
328
+ /** Run status */
329
+ status: FunnelRunStatus;
330
+
331
+ /** How run was triggered */
332
+ trigger_type: TriggerType;
333
+
334
+ /** User who triggered (if manual) */
335
+ triggered_by?: string;
336
+
337
+ /** Started timestamp */
338
+ started_at: Date | string;
339
+
340
+ /** Completed timestamp */
341
+ completed_at?: Date | string;
342
+
343
+ /** Duration in milliseconds */
344
+ duration_ms?: number;
345
+
346
+ /** Total entities input */
347
+ total_input: number;
348
+
349
+ /** Total entities matched (in output) */
350
+ total_matched: number;
351
+
352
+ /** Total entities excluded */
353
+ total_excluded: number;
354
+
355
+ /** Total entities tagged */
356
+ total_tagged: number;
357
+
358
+ /** Per-stage statistics */
359
+ stage_stats: Record<string, StageStats>;
360
+
361
+ /** Error message (if failed) */
362
+ error?: string;
363
+
364
+ /** Stack trace (if failed) */
365
+ stack_trace?: string;
366
+
367
+ /** Metadata */
368
+ metadata?: Record<string, any>;
369
+ }
370
+
371
+ /**
372
+ * Result for a single entity processed through funnel
373
+ */
374
+ export interface FunnelResult<TEntity = any> {
375
+ /** The entity that was processed */
376
+ entity: TEntity;
377
+
378
+ /** Whether entity matched and is in output */
379
+ matched: boolean;
380
+
381
+ /** Stage where entity was excluded (if excluded) */
382
+ excluded_at_stage?: string;
383
+
384
+ /** Tags accumulated during processing */
385
+ accumulated_tags: string[];
386
+
387
+ /** Context accumulated during processing */
388
+ context: Record<string, any>;
389
+
390
+ /** Stage-by-stage results */
391
+ stage_results?: StageResult[];
392
+ }
393
+
394
+ /**
395
+ * Result for a single stage evaluation
396
+ */
397
+ export interface StageResult {
398
+ /** Stage ID */
399
+ stage_id: string;
400
+
401
+ /** Stage name */
402
+ stage_name: string;
403
+
404
+ /** Whether rules matched */
405
+ matched: boolean;
406
+
407
+ /** Rule evaluation results */
408
+ rule_results?: RuleResult[];
409
+
410
+ /** Action taken */
411
+ action: MatchAction | NoMatchAction;
412
+
413
+ /** Tags added */
414
+ tags_added?: string[];
415
+
416
+ /** Context added */
417
+ context_added?: Record<string, any>;
418
+
419
+ /** Whether entity was excluded */
420
+ excluded: boolean;
421
+
422
+ /** Whether entity continued to next stage */
423
+ continued: boolean;
424
+ }
425
+
426
+ /**
427
+ * Result for a single rule evaluation
428
+ */
429
+ export interface RuleResult {
430
+ /** Rule field path */
431
+ field_path: string;
432
+
433
+ /** Rule operator */
434
+ operator: Operator;
435
+
436
+ /** Value compared against */
437
+ value: any;
438
+
439
+ /** Actual value from entity */
440
+ actual_value: any;
441
+
442
+ /** Whether rule matched */
443
+ matched: boolean;
444
+
445
+ /** Error (if evaluation failed) */
446
+ error?: string;
447
+ }
448
+
449
+ // ============================================================================
450
+ // Field Registry
451
+ // ============================================================================
452
+
453
+ /**
454
+ * Valid operators for a field type
455
+ */
456
+ export type ValidOperators = Operator[];
457
+
458
+ /**
459
+ * Field constraints
460
+ */
461
+ export interface FieldConstraints {
462
+ /** Minimum value (numbers, dates) */
463
+ min_value?: number | string;
464
+
465
+ /** Maximum value (numbers, dates) */
466
+ max_value?: number | string;
467
+
468
+ /** Allowed values (enums) */
469
+ choices?: any[];
470
+
471
+ /** Regex pattern */
472
+ pattern?: string;
473
+
474
+ /** Required field */
475
+ required?: boolean;
476
+ }
477
+
478
+ /**
479
+ * Field definition in registry
480
+ *
481
+ * Describes what fields are available for filtering
482
+ * on a given entity type
483
+ */
484
+ export interface FieldDefinition {
485
+ /** Unique field identifier (dot-notation path) */
486
+ name: string;
487
+
488
+ /** Human-readable label */
489
+ label: string;
490
+
491
+ /** Field data type */
492
+ type: FieldType;
493
+
494
+ /** Valid operators for this field */
495
+ operators: ValidOperators;
496
+
497
+ /** Field category (for UI grouping) */
498
+ category?: string;
499
+
500
+ /** Optional description */
501
+ description?: string;
502
+
503
+ /** Field constraints */
504
+ constraints?: FieldConstraints;
505
+
506
+ /** Whether field is sortable */
507
+ sortable?: boolean;
508
+
509
+ /** Whether field is searchable */
510
+ searchable?: boolean;
511
+
512
+ /** Example values (for UI help) */
513
+ examples?: any[];
514
+
515
+ /** Related fields (for UI suggestions) */
516
+ related_fields?: string[];
517
+ }
518
+
519
+ /**
520
+ * Field registry for an entity type
521
+ *
522
+ * Maps field paths to their definitions
523
+ */
524
+ export interface FieldRegistry {
525
+ /** Entity type this registry is for */
526
+ entity_type: string;
527
+
528
+ /** Available fields */
529
+ fields: FieldDefinition[];
530
+
531
+ /** Field lookup by name */
532
+ field_map?: Map<string, FieldDefinition>;
533
+
534
+ /** Categories (for UI grouping) */
535
+ categories?: string[];
536
+ }
537
+
538
+ // ============================================================================
539
+ // Utility Types
540
+ // ============================================================================
541
+
542
+ /**
543
+ * Filter configuration (rules + logic)
544
+ */
545
+ export interface FilterConfig {
546
+ logic: FilterLogic;
547
+ rules: FilterRule[];
548
+ }
549
+
550
+ /**
551
+ * Partial type helper
552
+ */
553
+ export type DeepPartial<T> = {
554
+ [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
555
+ };
556
+
557
+ /**
558
+ * Extract field paths from object type
559
+ * Helper for type-safe field path completion
560
+ */
561
+ export type FieldPath<T, Prefix extends string = ''> = {
562
+ [K in keyof T]: T[K] extends object
563
+ ? K extends string
564
+ ? `${Prefix}${K}` | FieldPath<T[K], `${Prefix}${K}.`>
565
+ : never
566
+ : K extends string
567
+ ? `${Prefix}${K}`
568
+ : never;
569
+ }[keyof T];
570
+
571
+ /**
572
+ * Funnel creation input (omits auto-generated fields)
573
+ */
574
+ export type CreateFunnelInput<TEntity = any> = Omit<
575
+ Funnel<TEntity>,
576
+ 'id' | 'created_at' | 'updated_at'
577
+ >;
578
+
579
+ /**
580
+ * Funnel update input (all fields optional except id)
581
+ */
582
+ export type UpdateFunnelInput<TEntity = any> = DeepPartial<Funnel<TEntity>> & {
583
+ id: string;
584
+ };
585
+
586
+ /**
587
+ * Stage creation input
588
+ */
589
+ export type CreateStageInput<TEntity = any> = Omit<FunnelStage<TEntity>, 'id'>;
590
+
591
+ /**
592
+ * Stage update input
593
+ */
594
+ export type UpdateStageInput<TEntity = any> = DeepPartial<FunnelStage<TEntity>> & {
595
+ id: string;
596
+ };
597
+
598
+ // ============================================================================
599
+ // Type Guards
600
+ // ============================================================================
601
+
602
+ /**
603
+ * Type guard: is value a valid Funnel?
604
+ */
605
+ export function isFunnel<TEntity = any>(value: unknown): value is Funnel<TEntity> {
606
+ const f = value as Funnel<TEntity>;
607
+ return (
608
+ typeof f === 'object' &&
609
+ f !== null &&
610
+ typeof f.id === 'string' &&
611
+ typeof f.name === 'string' &&
612
+ ['draft', 'active', 'paused', 'archived'].includes(f.status) &&
613
+ Array.isArray(f.stages)
614
+ );
615
+ }
616
+
617
+ /**
618
+ * Type guard: is value a valid FunnelStage?
619
+ */
620
+ export function isStage<TEntity = any>(value: unknown): value is FunnelStage<TEntity> {
621
+ const s = value as FunnelStage<TEntity>;
622
+ return (
623
+ typeof s === 'object' &&
624
+ s !== null &&
625
+ typeof s.id === 'string' &&
626
+ typeof s.name === 'string' &&
627
+ typeof s.order === 'number' &&
628
+ ['AND', 'OR'].includes(s.filter_logic) &&
629
+ Array.isArray(s.rules)
630
+ );
631
+ }
632
+
633
+ /**
634
+ * Type guard: is value a valid FilterRule?
635
+ */
636
+ export function isFilterRule(value: unknown): value is FilterRule {
637
+ const r = value as FilterRule;
638
+ return (
639
+ typeof r === 'object' &&
640
+ r !== null &&
641
+ typeof r.field_path === 'string' &&
642
+ typeof r.operator === 'string' &&
643
+ r.value !== undefined
644
+ );
645
+ }
646
+
647
+ /**
648
+ * Type guard: is value a valid FunnelRun?
649
+ */
650
+ export function isFunnelRun(value: unknown): value is FunnelRun {
651
+ const r = value as FunnelRun;
652
+ return (
653
+ typeof r === 'object' &&
654
+ r !== null &&
655
+ typeof r.id === 'string' &&
656
+ typeof r.funnel_id === 'string' &&
657
+ ['pending', 'running', 'completed', 'failed', 'cancelled'].includes(r.status)
658
+ );
659
+ }
660
+
661
+ /**
662
+ * Type guard: is value a valid FunnelResult?
663
+ */
664
+ export function isFunnelResult<TEntity = any>(
665
+ value: unknown
666
+ ): value is FunnelResult<TEntity> {
667
+ const r = value as FunnelResult<TEntity>;
668
+ return (
669
+ typeof r === 'object' &&
670
+ r !== null &&
671
+ r.entity !== undefined &&
672
+ typeof r.matched === 'boolean' &&
673
+ Array.isArray(r.accumulated_tags)
674
+ );
675
+ }
676
+
677
+ /**
678
+ * Type guard: is value a valid FieldDefinition?
679
+ */
680
+ export function isFieldDefinition(value: unknown): value is FieldDefinition {
681
+ const f = value as FieldDefinition;
682
+ return (
683
+ typeof f === 'object' &&
684
+ f !== null &&
685
+ typeof f.name === 'string' &&
686
+ typeof f.label === 'string' &&
687
+ typeof f.type === 'string' &&
688
+ Array.isArray(f.operators)
689
+ );
690
+ }
691
+
692
+ // ============================================================================
693
+ // Validation Helpers
694
+ // ============================================================================
695
+
696
+ /**
697
+ * Get valid operators for a field type
698
+ */
699
+ export function getValidOperators(fieldType: FieldType): ValidOperators {
700
+ switch (fieldType) {
701
+ case 'string':
702
+ return [
703
+ 'eq', 'ne', 'contains', 'not_contains', 'startswith', 'endswith',
704
+ 'matches', 'in', 'not_in', 'isnull', 'isnotnull'
705
+ ];
706
+
707
+ case 'number':
708
+ return [
709
+ 'eq', 'ne', 'gt', 'lt', 'gte', 'lte',
710
+ 'in', 'not_in', 'isnull', 'isnotnull'
711
+ ];
712
+
713
+ case 'boolean':
714
+ return ['eq', 'ne', 'is_true', 'is_false', 'isnull', 'isnotnull'];
715
+
716
+ case 'date':
717
+ return [
718
+ 'eq', 'ne', 'gt', 'lt', 'gte', 'lte',
719
+ 'isnull', 'isnotnull'
720
+ ];
721
+
722
+ case 'array':
723
+ return [
724
+ 'in', 'not_in', 'has_any', 'has_all',
725
+ 'isnull', 'isnotnull'
726
+ ];
727
+
728
+ case 'tag':
729
+ return ['has_tag', 'not_has_tag'];
730
+
731
+ case 'object':
732
+ return ['isnull', 'isnotnull'];
733
+
734
+ case 'any':
735
+ default:
736
+ return [
737
+ 'eq', 'ne', 'gt', 'lt', 'gte', 'lte',
738
+ 'contains', 'not_contains', 'startswith', 'endswith',
739
+ 'in', 'not_in', 'isnull', 'isnotnull'
740
+ ];
741
+ }
742
+ }
743
+
744
+ /**
745
+ * Validate operator is allowed for field type
746
+ */
747
+ export function isValidOperator(
748
+ operator: Operator,
749
+ fieldType: FieldType
750
+ ): boolean {
751
+ const validOps = getValidOperators(fieldType);
752
+ return validOps.includes(operator);
753
+ }
754
+
755
+ /**
756
+ * Validate filter rule
757
+ */
758
+ export function validateFilterRule(rule: FilterRule): string[] {
759
+ const errors: string[] = [];
760
+
761
+ if (!rule.field_path) {
762
+ errors.push('field_path is required');
763
+ }
764
+
765
+ if (!rule.operator) {
766
+ errors.push('operator is required');
767
+ }
768
+
769
+ // Value required for most operators
770
+ const nullOps = ['isnull', 'isnotnull', 'is_true', 'is_false'];
771
+ if (!nullOps.includes(rule.operator) && rule.value === undefined) {
772
+ errors.push(`value is required for operator '${rule.operator}'`);
773
+ }
774
+
775
+ return errors;
776
+ }
777
+
778
+ /**
779
+ * Validate funnel stage
780
+ */
781
+ export function validateStage<TEntity = any>(
782
+ stage: FunnelStage<TEntity>
783
+ ): string[] {
784
+ const errors: string[] = [];
785
+
786
+ if (!stage.name) {
787
+ errors.push('name is required');
788
+ }
789
+
790
+ if (typeof stage.order !== 'number') {
791
+ errors.push('order must be a number');
792
+ }
793
+
794
+ if (!['AND', 'OR'].includes(stage.filter_logic)) {
795
+ errors.push('filter_logic must be AND or OR');
796
+ }
797
+
798
+ if (!Array.isArray(stage.rules)) {
799
+ errors.push('rules must be an array');
800
+ } else {
801
+ stage.rules.forEach((rule, i) => {
802
+ const ruleErrors = validateFilterRule(rule);
803
+ ruleErrors.forEach(err => errors.push(`rules[${i}]: ${err}`));
804
+ });
805
+ }
806
+
807
+ return errors;
808
+ }
809
+
810
+ /**
811
+ * Validate funnel
812
+ */
813
+ export function validateFunnel<TEntity = any>(
814
+ funnel: Funnel<TEntity>
815
+ ): string[] {
816
+ const errors: string[] = [];
817
+
818
+ if (!funnel.name) {
819
+ errors.push('name is required');
820
+ }
821
+
822
+ if (!['draft', 'active', 'paused', 'archived'].includes(funnel.status)) {
823
+ errors.push('status must be draft, active, paused, or archived');
824
+ }
825
+
826
+ if (!Array.isArray(funnel.stages)) {
827
+ errors.push('stages must be an array');
828
+ } else {
829
+ funnel.stages.forEach((stage, i) => {
830
+ const stageErrors = validateStage(stage);
831
+ stageErrors.forEach(err => errors.push(`stages[${i}]: ${err}`));
832
+ });
833
+
834
+ // Check stage order is sequential
835
+ const orders = funnel.stages.map(s => s.order).sort((a, b) => a - b);
836
+ const expectedOrders = Array.from({ length: orders.length }, (_, i) => i);
837
+ if (JSON.stringify(orders) !== JSON.stringify(expectedOrders)) {
838
+ errors.push('stage orders must be sequential starting from 0');
839
+ }
840
+ }
841
+
842
+ return errors;
843
+ }