spine-framework-cortex 0.1.19 → 0.2.0
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.
- package/{functions/custom_cortex-handler.ts → api/cortex-handler.ts} +9 -9
- package/components/CliInstancesCard.tsx +144 -0
- package/components/CortexSidebar.tsx +27 -53
- package/hooks/useTypeRegistry.ts +74 -0
- package/index.tsx +13 -24
- package/manifest.json +1 -13
- package/package.json +11 -20
- package/pages/courses/CoursesPage.tsx +14 -4
- package/pages/crm/AccountDetailPage.tsx +149 -194
- package/pages/crm/ContactsPage.tsx +7 -7
- package/pages/intelligence/IntelligencePage.tsx +24 -31
- package/pages/kb/KBEditorPage.tsx +9 -2
- package/pages/operations/AuditFunnelPage.tsx +378 -0
- package/pages/operations/InstallFunnelPage.tsx +410 -0
- package/pages/operations/OperationsDashboard.tsx +275 -0
- package/pages/support/RedactionReview.tsx +11 -2
- package/seed/link-types.json +8 -42
- package/seed/package.json +27 -0
- package/seed/roles.json +1 -1
- package/seed/types.json +2711 -596
- package/CHANGELOG.md +0 -46
- package/LICENSE.md +0 -223
- package/README.md +0 -69
- package/functions/custom_anonymous-sessions.ts +0 -356
- package/functions/custom_case_analysis.ts +0 -507
- package/functions/custom_community-escalation.ts +0 -234
- package/functions/custom_cortex-chunks.ts +0 -52
- package/functions/custom_funnel-scoring.ts +0 -256
- package/functions/custom_funnel-signal.ts +0 -446
- package/functions/custom_funnel-timers.ts +0 -449
- package/functions/custom_kb-chunker-test.ts +0 -364
- package/functions/custom_kb-chunker.ts +0 -576
- package/functions/custom_kb-embeddings.ts +0 -481
- package/functions/custom_kb-ingestion.ts +0 -448
- package/functions/custom_support-triage.ts +0 -649
- package/functions/custom_tag_management.ts +0 -314
- package/functions/webhook-handlers.ts +0 -29
- package/lib/resolveTypeId.ts +0 -16
- package/pages/crm/ContactDetailPage.tsx +0 -184
- package/pages/ops/AuditFunnelPage.tsx +0 -191
- package/pages/ops/CommandCenterPage.tsx +0 -377
- package/pages/ops/InstallFunnelPage.tsx +0 -226
- package/seed/accounts.json +0 -9
- package/seed/integrations.json +0 -24
- package/seed/pipelines.json +0 -59
- 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
|
-
})
|