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,356 +0,0 @@
1
- // Anonymous Session Functions
2
- // Uses ONLY Spine APIs (ctx.db) - NO direct database access
3
- // Handles stitch operation: anonymous session → identified account
4
-
5
- import { createHandler } from './_shared/middleware'
6
- import { calculateRecency, calculateRawScore } from './custom_funnel-scoring'
7
- import { resolveTypeIds, resolveLinkTypeIds } from './_shared/resolve-ids'
8
-
9
- async function resolveIds() {
10
- const [types, linkTypes] = await Promise.all([
11
- resolveTypeIds([
12
- { kind: 'item', slug: 'anonymous_session' },
13
- { kind: 'item', slug: 'funnel_signal' },
14
- { kind: 'item', slug: 'opportunity_queue' },
15
- ]),
16
- resolveLinkTypeIds(['account_signals', 'account_opportunities']),
17
- ])
18
- return {
19
- TYPE_IDS: {
20
- anonymous_session: types['item/anonymous_session'],
21
- funnel_signal: types['item/funnel_signal'],
22
- opportunity_queue: types['item/opportunity_queue'],
23
- },
24
- LINK_TYPE_IDS: {
25
- account_signals: linkTypes['account_signals'],
26
- account_opportunities: linkTypes['account_opportunities'],
27
- },
28
- }
29
- }
30
-
31
- // ============================================
32
- // STITCH: Anonymous Session → Identified Account
33
- // ============================================
34
-
35
- export const stitchAnonymousToAccount = createHandler(async (ctx, body) => {
36
- const { anonymous_id, person_id, account_id } = body
37
-
38
- if (!anonymous_id || !person_id || !account_id) {
39
- return { status: 'error', error: 'Missing required fields: anonymous_id, person_id, account_id' }
40
- }
41
-
42
- const now = new Date().toISOString()
43
- const ids = await resolveIds()
44
-
45
- try {
46
- // 1. Get anonymous session using ctx.db
47
- const { data: session, error: sessionError } = await ctx.db
48
- .from('items')
49
- .select('id, data')
50
- .eq('type_id', ids.TYPE_IDS.anonymous_session)
51
- .eq('data->identity->>anonymous_id', anonymous_id)
52
- .eq('is_active', true)
53
- .order('created_at', { ascending: false })
54
- .limit(1)
55
- .single()
56
-
57
- if (sessionError || !session) {
58
- return { status: 'error', error: 'Anonymous session not found' }
59
- }
60
-
61
- const sessionData = session.data || {}
62
-
63
- // Check if already stitched
64
- if (sessionData.lifecycle?.stitched_at) {
65
- return { status: 'error', error: 'Session already stitched' }
66
- }
67
-
68
- // 2. Get account using ctx.db
69
- const { data: account, error: accountError } = await ctx.db
70
- .from('accounts')
71
- .select('id, data')
72
- .eq('id', account_id)
73
- .single()
74
-
75
- if (accountError || !account) {
76
- return { status: 'error', error: 'Account not found' }
77
- }
78
-
79
- // 3. Update all signals with account_id and person_id using ctx.db
80
- const { error: signalsError } = await ctx.db
81
- .from('items')
82
- .update({
83
- account_id: account_id,
84
- 'data->identity->>person_id': person_id,
85
- 'data->processing->>stitched_at': now,
86
- 'data->processing->>stitched_to_account_id': account_id,
87
- updated_at: now
88
- })
89
- .eq('type_id', ids.TYPE_IDS.funnel_signal)
90
- .eq('data->identity->>anonymous_id', anonymous_id)
91
- .is('account_id', null)
92
-
93
- if (signalsError) {
94
- console.error(`[Stitch] Failed to update signals: ${signalsError.message}`)
95
- }
96
-
97
- // 4. Get updated signals for recalculation
98
- const { data: updatedSignals } = await ctx.db
99
- .from('items')
100
- .select('data')
101
- .eq('type_id', ids.TYPE_IDS.funnel_signal)
102
- .eq('account_id', account_id)
103
- .eq('data->classification->>stage', 'identified')
104
- .eq('is_active', true)
105
-
106
- // 5. Recalculate identified rating with newly-stitched signals
107
- let identifiedRating = { rating: 0, raw_score: 0, calculated_at: now, best_signal_id: null as string | null }
108
-
109
- if (updatedSignals && updatedSignals.length > 0) {
110
- let bestSignal = updatedSignals[0]
111
- let bestScore = bestSignal.data?.scoring_components?.raw_score?.calculated || 0
112
-
113
- for (const signal of updatedSignals) {
114
- const score = signal.data?.scoring_components?.raw_score?.calculated || 0
115
- if (score > bestScore) {
116
- bestScore = score
117
- bestSignal = signal
118
- }
119
- }
120
-
121
- // Recalculate with current recency
122
- const signalDate = new Date(bestSignal.data?.processing?.scored_at || now)
123
- const recency = calculateRecency(signalDate, new Date(), 'identified')
124
-
125
- if (recency.divisor) {
126
- const newScore = calculateRawScore(
127
- bestSignal.data?.action?.action_value || 1,
128
- bestSignal.data?.scoring_components?.engagement?.type || 1,
129
- recency.divisor
130
- )
131
-
132
- identifiedRating = {
133
- rating: newScore.rating,
134
- raw_score: newScore.calculated,
135
- calculated_at: now,
136
- best_signal_id: bestSignal.data?.id || null
137
- }
138
- }
139
- }
140
-
141
- // 6. Update account with stitched data using ctx.db
142
- const currentFunnel = account.data?.funnel || {}
143
- const anonymousRating = sessionData.scoring?.ratings?.anonymous
144
-
145
- const updatedFunnel = {
146
- ...currentFunnel,
147
- current_stage: 'identified',
148
- ratings: {
149
- ...currentFunnel.ratings,
150
- anonymous: anonymousRating ? {
151
- ...anonymousRating,
152
- stitched_at: now,
153
- archived: true
154
- } : currentFunnel.ratings?.anonymous,
155
- identified: identifiedRating
156
- },
157
- attribution: {
158
- ...currentFunnel.attribution,
159
- anonymous_first_touch: sessionData.attribution?.first_touch
160
- },
161
- stage_history: [
162
- ...(currentFunnel.stage_history || []),
163
- { from: 'anonymous', to: 'identified', at: now }
164
- ]
165
- }
166
-
167
- await ctx.db
168
- .from('accounts')
169
- .update({
170
- data: { ...account.data, funnel: updatedFunnel }
171
- })
172
- .eq('id', account_id)
173
-
174
- // 7. Mark session as stitched using ctx.db
175
- const updatedSessionData = {
176
- ...sessionData,
177
- lifecycle: {
178
- ...sessionData.lifecycle,
179
- stitched_at: now,
180
- stitched_to_account_id: account_id,
181
- stitched_to_person_id: person_id
182
- }
183
- }
184
-
185
- await ctx.db
186
- .from('items')
187
- .update({
188
- data: updatedSessionData,
189
- updated_at: now
190
- })
191
- .eq('id', session.id)
192
-
193
- // 8. Check for immediate queue entry (strong anonymous activity)
194
- let queueEntry = null
195
- if (anonymousRating?.rating >= 4) {
196
- const inference = { type: 'implementation', confidence: 'high' }
197
-
198
- const queueData = {
199
- identity: {
200
- account_id: account_id,
201
- person_id: person_id
202
- },
203
- trigger: {
204
- source_signal_id: anonymousRating.best_signal_id,
205
- trigger_stage: 'anonymous',
206
- trigger_rating: anonymousRating.rating,
207
- trigger_raw_score: anonymousRating.raw_score,
208
- trigger_reason: 'High engagement during anonymous phase'
209
- },
210
- recommendation: {
211
- opportunity_type: inference.type,
212
- confidence: inference.confidence,
213
- suggested_priority: anonymousRating.rating
214
- },
215
- review: {
216
- status: 'pending',
217
- reviewed_by: null,
218
- reviewed_at: null,
219
- conversion_opportunity_id: null
220
- },
221
- notes: {
222
- reviewer_notes: null,
223
- auto_reason: 'Stitched from anonymous session with high engagement'
224
- }
225
- }
226
-
227
- const { data: queueItem } = await ctx.db
228
- .from('items')
229
- .insert({
230
- type_id: ids.TYPE_IDS.opportunity_queue,
231
- title: `${inference.type} - Stitched Session`,
232
- account_id: account_id,
233
- data: queueData
234
- })
235
- .select('id')
236
- .single()
237
-
238
- if (queueItem) {
239
- queueEntry = { id: queueItem.id }
240
-
241
- // Create link to account
242
- await ctx.db
243
- .from('links')
244
- .insert({
245
- link_type_id: ids.LINK_TYPE_IDS.account_opportunities,
246
- source_type: 'account',
247
- source_id: account_id,
248
- target_type: 'item',
249
- target_id: queueItem.id
250
- })
251
-
252
- // Update account queue reference
253
- await ctx.db
254
- .from('accounts')
255
- .update({
256
- data: {
257
- ...account.data,
258
- funnel: {
259
- ...updatedFunnel,
260
- queue: { pending_queue_entry_id: queueItem.id }
261
- }
262
- }
263
- })
264
- .eq('id', account_id)
265
- }
266
- }
267
-
268
- // 9. Create links between account and all stitched signals
269
- const { data: stitchedSignals } = await ctx.db
270
- .from('items')
271
- .select('id')
272
- .eq('type_id', ids.TYPE_IDS.funnel_signal)
273
- .eq('account_id', account_id)
274
- .eq('data->processing->>stitched_at', now)
275
-
276
- for (const signal of stitchedSignals || []) {
277
- await ctx.db
278
- .from('links')
279
- .insert({
280
- link_type_id: ids.LINK_TYPE_IDS.account_signals,
281
- source_type: 'account',
282
- source_id: account_id,
283
- target_type: 'item',
284
- target_id: signal.id,
285
- data: { created_at: now, stitched: true }
286
- })
287
- }
288
-
289
- return {
290
- status: 'success',
291
- session_id: session.id,
292
- account_id,
293
- person_id,
294
- stitched_signals: stitchedSignals?.length || 0,
295
- queue_entry: queueEntry,
296
- anonymous_rating: anonymousRating?.rating || 0,
297
- identified_rating: identifiedRating.rating
298
- }
299
-
300
- } catch (err) {
301
- console.error('[Stitch] Error:', err)
302
- return { status: 'error', error: err instanceof Error ? err.message : 'Unknown error' }
303
- }
304
- })
305
-
306
- // ============================================
307
- // GET ANONYMOUS SESSION DETAILS
308
- // ============================================
309
-
310
- export const getAnonymousSession = createHandler(async (ctx, body) => {
311
- const { anonymous_id } = body
312
-
313
- if (!anonymous_id) {
314
- return { status: 'error', error: 'Missing anonymous_id' }
315
- }
316
-
317
- const ids = await resolveIds()
318
-
319
- // Get session using ctx.db
320
- const { data: session, error } = await ctx.db
321
- .from('items')
322
- .select('id, data, created_at, updated_at')
323
- .eq('type_id', ids.TYPE_IDS.anonymous_session)
324
- .eq('data->identity->>anonymous_id', anonymous_id)
325
- .eq('is_active', true)
326
- .order('created_at', { ascending: false })
327
- .limit(1)
328
- .single()
329
-
330
- if (error || !session) {
331
- return { status: 'error', error: 'Session not found' }
332
- }
333
-
334
- // Get associated signals using ctx.db
335
- const { data: signals } = await ctx.db
336
- .from('items')
337
- .select('id, data, created_at')
338
- .eq('type_id', ids.TYPE_IDS.funnel_signal)
339
- .eq('data->identity->>anonymous_id', anonymous_id)
340
- .eq('is_active', true)
341
- .order('created_at', { ascending: false })
342
-
343
- return {
344
- status: 'success',
345
- session: {
346
- id: session.id,
347
- anonymous_id,
348
- attribution: session.data?.attribution,
349
- scoring: session.data?.scoring,
350
- lifecycle: session.data?.lifecycle,
351
- created_at: session.created_at,
352
- updated_at: session.updated_at
353
- },
354
- signals: signals || []
355
- }
356
- })