spine-framework-cortex 0.1.19 → 0.2.1

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 (46) hide show
  1. package/{functions/custom_cortex-handler.ts → api/cortex-handler.ts} +9 -9
  2. package/components/CliInstancesCard.tsx +144 -0
  3. package/components/CortexSidebar.tsx +27 -53
  4. package/hooks/useTypeRegistry.ts +74 -0
  5. package/index.tsx +13 -24
  6. package/manifest.json +1 -13
  7. package/package.json +11 -20
  8. package/pages/courses/CoursesPage.tsx +14 -4
  9. package/pages/crm/AccountDetailPage.tsx +149 -194
  10. package/pages/crm/ContactsPage.tsx +7 -7
  11. package/pages/intelligence/IntelligencePage.tsx +24 -31
  12. package/pages/kb/KBEditorPage.tsx +9 -2
  13. package/pages/operations/AuditFunnelPage.tsx +378 -0
  14. package/pages/operations/InstallFunnelPage.tsx +410 -0
  15. package/pages/operations/OperationsDashboard.tsx +275 -0
  16. package/pages/support/RedactionReview.tsx +11 -2
  17. package/seed/link-types.json +4 -42
  18. package/seed/package.json +27 -0
  19. package/seed/roles.json +1 -1
  20. package/seed/types.json +2711 -596
  21. package/CHANGELOG.md +0 -46
  22. package/LICENSE.md +0 -223
  23. package/README.md +0 -69
  24. package/functions/custom_anonymous-sessions.ts +0 -356
  25. package/functions/custom_case_analysis.ts +0 -507
  26. package/functions/custom_community-escalation.ts +0 -234
  27. package/functions/custom_cortex-chunks.ts +0 -52
  28. package/functions/custom_funnel-scoring.ts +0 -256
  29. package/functions/custom_funnel-signal.ts +0 -446
  30. package/functions/custom_funnel-timers.ts +0 -449
  31. package/functions/custom_kb-chunker-test.ts +0 -364
  32. package/functions/custom_kb-chunker.ts +0 -576
  33. package/functions/custom_kb-embeddings.ts +0 -481
  34. package/functions/custom_kb-ingestion.ts +0 -448
  35. package/functions/custom_support-triage.ts +0 -649
  36. package/functions/custom_tag_management.ts +0 -314
  37. package/functions/webhook-handlers.ts +0 -29
  38. package/lib/resolveTypeId.ts +0 -16
  39. package/pages/crm/ContactDetailPage.tsx +0 -184
  40. package/pages/ops/AuditFunnelPage.tsx +0 -191
  41. package/pages/ops/CommandCenterPage.tsx +0 -377
  42. package/pages/ops/InstallFunnelPage.tsx +0 -226
  43. package/seed/accounts.json +0 -9
  44. package/seed/integrations.json +0 -24
  45. package/seed/pipelines.json +0 -59
  46. package/seed/triggers.json +0 -125
@@ -1,449 +0,0 @@
1
- // Funnel Timer Functions
2
- // Uses ONLY Spine APIs (ctx.db) - NO direct database access
3
-
4
- import { createHandler } from './_shared/middleware'
5
- import { calculateRecency, calculateRawScore, inferOpportunityType } from './custom_funnel-scoring'
6
- import { resolveTypeIds } from './_shared/resolve-ids'
7
-
8
- async function resolveIds() {
9
- const types = await resolveTypeIds([
10
- { kind: 'item', slug: 'anonymous_session' },
11
- { kind: 'item', slug: 'funnel_signal' },
12
- { kind: 'item', slug: 'funnel_aggregation' },
13
- { kind: 'item', slug: 'opportunity_queue' },
14
- ])
15
- return {
16
- TYPE_IDS: {
17
- anonymous_session: types['item/anonymous_session'],
18
- funnel_signal: types['item/funnel_signal'],
19
- funnel_aggregation: types['item/funnel_aggregation'],
20
- opportunity_queue: types['item/opportunity_queue'],
21
- },
22
- }
23
- }
24
-
25
- // ============================================
26
- // TIMER 1: Score Decay (Daily at 11:59:59 PM)
27
- // Updates account ratings based on recency decay
28
- // ============================================
29
-
30
- export const scoreDecay = createHandler(async (ctx, _body) => {
31
- console.log('[FunnelTimer] Starting score decay recalculation')
32
- const { TYPE_IDS } = await resolveIds()
33
-
34
- const startTime = Date.now()
35
- const yesterday = new Date()
36
- yesterday.setDate(yesterday.getDate() - 1)
37
-
38
- // Find accounts needing recalculation using ctx.db
39
- const { data: accounts, error } = await ctx.db
40
- .from('accounts')
41
- .select('id, data')
42
- .not('data->funnel->>current_stage', 'is', null)
43
- .eq('is_active', true)
44
-
45
- if (error) {
46
- return { status: 'error', error: error.message, task: 'score_decay' }
47
- }
48
-
49
- let updated = 0
50
- let skipped = 0
51
- let errors = 0
52
-
53
- for (const account of accounts || []) {
54
- try {
55
- const funnel = account.data?.funnel
56
- if (!funnel) {
57
- skipped++
58
- continue
59
- }
60
-
61
- const ratings = funnel.ratings || {}
62
- let hasChanges = false
63
- const updatedRatings = { ...ratings }
64
-
65
- // Check each stage for decay
66
- for (const stage of ['anonymous', 'identified', 'installed'] as const) {
67
- const stageRating = ratings[stage]
68
- if (!stageRating) continue
69
-
70
- // Get the best signal for this stage
71
- const { data: signals } = await ctx.db
72
- .from('items')
73
- .select('data')
74
- .eq('type_id', TYPE_IDS.funnel_signal)
75
- .eq('account_id', account.id)
76
- .eq('data->classification->>stage', stage)
77
- .eq('is_active', true)
78
- .order('data->processing->>scored_at', { ascending: false })
79
- .limit(50)
80
-
81
- if (!signals || signals.length === 0) continue
82
-
83
- // Find best signal
84
- let bestSignal = signals[0]
85
- let bestScore = bestSignal.data?.scoring_components?.raw_score?.calculated || 0
86
-
87
- for (const signal of signals) {
88
- const score = signal.data?.scoring_components?.raw_score?.calculated || 0
89
- if (score > bestScore) {
90
- bestScore = score
91
- bestSignal = signal
92
- }
93
- }
94
-
95
- // Recalculate recency
96
- const signalDate = new Date(bestSignal.data?.processing?.scored_at || bestSignal.created_at)
97
- const recency = calculateRecency(signalDate, new Date(), stage)
98
-
99
- if (recency.window === 'expired') {
100
- // Score expired - rating drops to 0
101
- if (stageRating.rating > 0) {
102
- updatedRatings[stage] = {
103
- ...stageRating,
104
- rating: 0,
105
- raw_score: 0,
106
- calculated_at: new Date().toISOString()
107
- }
108
- hasChanges = true
109
- }
110
- continue
111
- }
112
-
113
- // Recalculate score with current recency
114
- const actionValue = bestSignal.data?.action?.action_value || 1
115
- const engagementType = bestSignal.data?.scoring_components?.engagement?.type || 1
116
-
117
- const newScore = calculateRawScore(
118
- actionValue,
119
- engagementType,
120
- recency.divisor || 5
121
- )
122
-
123
- // Update if rating changed
124
- if (newScore.rating !== stageRating.rating) {
125
- updatedRatings[stage] = {
126
- ...stageRating,
127
- rating: newScore.rating,
128
- raw_score: newScore.calculated,
129
- calculated_at: new Date().toISOString()
130
- }
131
- hasChanges = true
132
- }
133
- }
134
-
135
- // Update account if changes were made
136
- if (hasChanges) {
137
- const maxRating = Math.max(
138
- updatedRatings.anonymous?.rating || 0,
139
- updatedRatings.identified?.rating || 0,
140
- updatedRatings.installed?.rating || 0
141
- )
142
-
143
- const updatedFunnel = {
144
- ...funnel,
145
- ratings: updatedRatings,
146
- temperature: ratingToTemperature(maxRating)
147
- }
148
-
149
- await ctx.db
150
- .from('accounts')
151
- .update({
152
- data: { ...account.data, funnel: updatedFunnel }
153
- })
154
- .eq('id', account.id)
155
-
156
- updated++
157
- } else {
158
- skipped++
159
- }
160
- } catch (err) {
161
- errors++
162
- console.error(`[FunnelTimer] Failed to recalculate account ${account.id}:`, err)
163
- }
164
- }
165
-
166
- const duration = Date.now() - startTime
167
- console.log(`[FunnelTimer] Score decay complete: ${updated} updated, ${skipped} skipped, ${errors} errors, ${duration}ms`)
168
-
169
- return {
170
- status: 'success',
171
- task: 'score_decay',
172
- updated_count: updated,
173
- skipped_count: skipped,
174
- error_count: errors,
175
- duration_ms: duration
176
- }
177
- })
178
-
179
- // ============================================
180
- // TIMER 2: Session Cleanup (Daily at 2:00 AM)
181
- // Soft-deletes expired anonymous sessions
182
- // ============================================
183
-
184
- export const sessionCleanup = createHandler(async (ctx, _body) => {
185
- console.log('[FunnelTimer] Starting anonymous session cleanup')
186
- const { TYPE_IDS } = await resolveIds()
187
-
188
- const startTime = Date.now()
189
- const now = new Date().toISOString()
190
-
191
- // Find expired sessions using ctx.db
192
- const { data: sessions, error } = await ctx.db
193
- .from('items')
194
- .select('id, data')
195
- .eq('type_id', TYPE_IDS.anonymous_session)
196
- .eq('is_active', true)
197
- .lt('data->retention->>purge_after', now)
198
- .is('data->lifecycle->>stitched_at', null)
199
-
200
- if (error) {
201
- return { status: 'error', error: error.message, task: 'session_cleanup' }
202
- }
203
-
204
- let purged = 0
205
- let errors = 0
206
-
207
- for (const session of sessions || []) {
208
- try {
209
- // Soft delete - mark as inactive
210
- await ctx.db
211
- .from('items')
212
- .update({ is_active: false, updated_at: now })
213
- .eq('id', session.id)
214
-
215
- purged++
216
- console.log(`[FunnelTimer] Purged session: ${session.id}`)
217
- } catch (err) {
218
- errors++
219
- console.error(`[FunnelTimer] Failed to purge session ${session.id}:`, err)
220
- }
221
- }
222
-
223
- const duration = Date.now() - startTime
224
- console.log(`[FunnelTimer] Cleanup complete: ${purged} purged, ${errors} errors, ${duration}ms`)
225
-
226
- return {
227
- status: 'success',
228
- task: 'session_cleanup',
229
- purged_count: purged,
230
- error_count: errors,
231
- duration_ms: duration
232
- }
233
- })
234
-
235
- // ============================================
236
- // TIMER 3: Aggregation (Hourly)
237
- // Creates/updates funnel_aggregation items for dashboard cache
238
- // ============================================
239
-
240
- export const aggregation = createHandler(async (ctx, _body) => {
241
- console.log('[FunnelTimer] Starting funnel aggregation')
242
- const { TYPE_IDS } = await resolveIds()
243
-
244
- const startTime = Date.now()
245
- const now = new Date()
246
- const periodEnd = now.toISOString()
247
- const periodStart = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
248
-
249
- // 1. System-wide aggregation
250
- try {
251
- // Count accounts by stage
252
- const { data: accounts } = await ctx.db
253
- .from('accounts')
254
- .select('data->funnel->>current_stage as stage, data->funnel->>temperature as temperature')
255
- .eq('is_active', true)
256
-
257
- const stageDistribution: Record<string, number> = { anonymous: 0, identified: 0, installed: 0, null: 0 }
258
- const tempDistribution: Record<string, number> = { cold: 0, warm: 0, hot: 0, null: 0 }
259
-
260
- for (const account of accounts || []) {
261
- const stage = account.stage || 'null'
262
- const temp = account.temperature || 'null'
263
- stageDistribution[stage] = (stageDistribution[stage] || 0) + 1
264
- tempDistribution[temp] = (tempDistribution[temp] || 0) + 1
265
- }
266
-
267
- // Count signals by source
268
- const { data: signals } = await ctx.db
269
- .from('items')
270
- .select('data->classification->>source as source')
271
- .eq('type_id', TYPE_IDS.funnel_signal)
272
- .eq('is_active', true)
273
- .gte('created_at', periodStart)
274
-
275
- const signalVolume: Record<string, number> = {}
276
- for (const signal of signals || []) {
277
- const source = signal.source || 'unknown'
278
- signalVolume[source] = (signalVolume[source] || 0) + 1
279
- }
280
-
281
- // Count queue entries
282
- const { count: pendingQueue } = await ctx.db
283
- .from('items')
284
- .select('*', { count: 'exact', head: true })
285
- .eq('type_id', TYPE_IDS.opportunity_queue)
286
- .eq('data->review->>status', 'pending')
287
- .eq('is_active', true)
288
-
289
- // Check for existing system aggregation
290
- const { data: existingSystemAgg } = await ctx.db
291
- .from('items')
292
- .select('id')
293
- .eq('type_id', TYPE_IDS.funnel_aggregation)
294
- .eq('data->identity->>aggregation_scope', 'system')
295
- .eq('is_active', true)
296
- .order('created_at', { ascending: false })
297
- .limit(1)
298
-
299
- const aggData = {
300
- identity: {
301
- account_id: null,
302
- aggregation_scope: 'system'
303
- },
304
- metadata: {
305
- computed_at: now.toISOString(),
306
- period_start: periodStart,
307
- period_end: periodEnd,
308
- ttl_hours: 1
309
- },
310
- metrics: {
311
- stage_distribution: stageDistribution,
312
- temperature_distribution: tempDistribution,
313
- signal_volume: signalVolume,
314
- queue_summary: {
315
- pending: pendingQueue || 0
316
- },
317
- total_accounts: accounts?.length || 0
318
- }
319
- }
320
-
321
- if (existingSystemAgg?.[0]) {
322
- // Update existing
323
- await ctx.db
324
- .from('items')
325
- .update({
326
- data: aggData,
327
- updated_at: now.toISOString()
328
- })
329
- .eq('id', existingSystemAgg[0].id)
330
- } else {
331
- // Create new
332
- await ctx.db
333
- .from('items')
334
- .insert({
335
- type_id: TYPE_IDS.funnel_aggregation,
336
- title: 'System Funnel Aggregation',
337
- data: aggData
338
- })
339
- }
340
- } catch (err) {
341
- console.error('[FunnelTimer] Failed to create system aggregation:', err)
342
- }
343
-
344
- // 2. Per-account aggregation (top 100 accounts by rating)
345
- try {
346
- const { data: accounts } = await ctx.db
347
- .from('accounts')
348
- .select('id, data->funnel as funnel')
349
- .not('data->funnel', 'is', null)
350
- .eq('is_active', true)
351
- .order('data->funnel->ratings->identified->>rating', { ascending: false })
352
- .limit(100)
353
-
354
- for (const account of accounts || []) {
355
- try {
356
- // Get recent signals for this account
357
- const { data: accountSignals } = await ctx.db
358
- .from('items')
359
- .select('data->classification->>stage as stage, data->action->>action_type as action_type')
360
- .eq('type_id', TYPE_IDS.funnel_signal)
361
- .eq('account_id', account.id)
362
- .eq('is_active', true)
363
- .gte('created_at', periodStart)
364
-
365
- const signalCounts: Record<string, number> = {}
366
- for (const signal of accountSignals || []) {
367
- const stage = signal.stage || 'unknown'
368
- signalCounts[stage] = (signalCounts[stage] || 0) + 1
369
- }
370
-
371
- // Check for existing aggregation
372
- const { data: existingAgg } = await ctx.db
373
- .from('items')
374
- .select('id')
375
- .eq('type_id', TYPE_IDS.funnel_aggregation)
376
- .eq('data->identity->>aggregation_scope', 'account')
377
- .eq('data->identity->>account_id', account.id)
378
- .eq('is_active', true)
379
- .order('created_at', { ascending: false })
380
- .limit(1)
381
-
382
- const aggData = {
383
- identity: {
384
- account_id: account.id,
385
- aggregation_scope: 'account'
386
- },
387
- metadata: {
388
- computed_at: now.toISOString(),
389
- period_start: periodStart,
390
- period_end: periodEnd,
391
- ttl_hours: 1
392
- },
393
- metrics: {
394
- signal_counts: signalCounts,
395
- current_stage: account.funnel?.current_stage,
396
- temperature: account.funnel?.temperature,
397
- best_rating: Math.max(
398
- account.funnel?.ratings?.anonymous?.rating || 0,
399
- account.funnel?.ratings?.identified?.rating || 0,
400
- account.funnel?.ratings?.installed?.rating || 0
401
- )
402
- }
403
- }
404
-
405
- if (existingAgg?.[0]) {
406
- await ctx.db
407
- .from('items')
408
- .update({
409
- data: aggData,
410
- updated_at: now.toISOString()
411
- })
412
- .eq('id', existingAgg[0].id)
413
- } else {
414
- await ctx.db
415
- .from('items')
416
- .insert({
417
- type_id: TYPE_IDS.funnel_aggregation,
418
- title: `Funnel Aggregation: ${account.id.slice(0, 8)}`,
419
- account_id: account.id,
420
- data: aggData
421
- })
422
- }
423
- } catch (err) {
424
- console.error(`[FunnelTimer] Failed to aggregate account ${account.id}:`, err)
425
- }
426
- }
427
- } catch (err) {
428
- console.error('[FunnelTimer] Failed to create account aggregations:', err)
429
- }
430
-
431
- const duration = Date.now() - startTime
432
- console.log(`[FunnelTimer] Aggregation complete: ${duration}ms`)
433
-
434
- return {
435
- status: 'success',
436
- task: 'aggregation',
437
- duration_ms: duration
438
- }
439
- })
440
-
441
- // ============================================
442
- // UTILITY FUNCTIONS
443
- // ============================================
444
-
445
- function ratingToTemperature(rating: number): 'cold' | 'warm' | 'hot' {
446
- if (rating <= 2) return 'cold'
447
- if (rating <= 3) return 'warm'
448
- return 'hot'
449
- }