spine-framework-cortex 0.1.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 (40) hide show
  1. package/components/CortexSidebar.tsx +130 -0
  2. package/functions/custom_anonymous-sessions.ts +356 -0
  3. package/functions/custom_case_analysis.ts +507 -0
  4. package/functions/custom_community-escalation.ts +234 -0
  5. package/functions/custom_cortex-chunks.ts +52 -0
  6. package/functions/custom_cortex-handler.ts +35 -0
  7. package/functions/custom_funnel-scoring.ts +256 -0
  8. package/functions/custom_funnel-signal.ts +678 -0
  9. package/functions/custom_funnel-timers.ts +449 -0
  10. package/functions/custom_kb-chunker-test.ts +364 -0
  11. package/functions/custom_kb-chunker.ts +576 -0
  12. package/functions/custom_kb-embeddings.ts +481 -0
  13. package/functions/custom_kb-ingestion.ts +448 -0
  14. package/functions/custom_support-triage.ts +649 -0
  15. package/functions/custom_tag_management.ts +314 -0
  16. package/index.tsx +103 -0
  17. package/manifest.json +82 -0
  18. package/package.json +29 -0
  19. package/pages/CortexDashboard.tsx +97 -0
  20. package/pages/community/CommunityPage.tsx +159 -0
  21. package/pages/courses/CoursesPage.tsx +231 -0
  22. package/pages/crm/AccountDetailPage.tsx +393 -0
  23. package/pages/crm/AccountsPage.tsx +164 -0
  24. package/pages/crm/ActivityPage.tsx +82 -0
  25. package/pages/crm/ContactDetailPage.tsx +184 -0
  26. package/pages/crm/ContactsPage.tsx +87 -0
  27. package/pages/crm/DealDetailPage.tsx +191 -0
  28. package/pages/crm/DealsPage.tsx +169 -0
  29. package/pages/crm/HealthPage.tsx +109 -0
  30. package/pages/intelligence/IntelligencePage.tsx +314 -0
  31. package/pages/kb/KBEditorPage.tsx +328 -0
  32. package/pages/kb/KBIngestionPage.tsx +409 -0
  33. package/pages/kb/KBPage.tsx +258 -0
  34. package/pages/support/RedactionReview.tsx +562 -0
  35. package/pages/support/SupportPage.tsx +395 -0
  36. package/pages/support/TicketDetailPage.tsx +919 -0
  37. package/seed/accounts.json +9 -0
  38. package/seed/link-types.json +44 -0
  39. package/seed/triggers.json +80 -0
  40. package/seed/types.json +352 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @module src/components/cortex/CortexSidebar
3
+ * @audience installer
4
+ * @layer frontend-component
5
+ * @stability stable
6
+ *
7
+ * Sidebar for the Cortex internal operations application.
8
+ */
9
+
10
+ import * as React from "react"
11
+ import { useNavigate, useLocation } from "react-router-dom"
12
+ import { useAuth } from "@core/contexts/AuthContext"
13
+ import {
14
+ LayoutDashboard,
15
+ Building2,
16
+ Headphones,
17
+ Users,
18
+ BookOpen,
19
+ GraduationCap,
20
+ Handshake,
21
+ Activity,
22
+ Heart,
23
+ } from "lucide-react"
24
+ import {
25
+ Sidebar,
26
+ SidebarContent,
27
+ SidebarFooter,
28
+ SidebarGroup,
29
+ SidebarGroupContent,
30
+ SidebarGroupLabel,
31
+ SidebarHeader,
32
+ SidebarMenu,
33
+ SidebarMenuButton,
34
+ SidebarMenuItem,
35
+ SidebarRail,
36
+ } from "@core/components/ui/sidebar"
37
+
38
+ const crmItems = [
39
+ { title: "Dashboard", url: "/cortex/dashboard", icon: LayoutDashboard },
40
+ { title: "Accounts", url: "/cortex/crm/accounts", icon: Building2 },
41
+ { title: "Contacts", url: "/cortex/crm/contacts", icon: Users },
42
+ { title: "Deals", url: "/cortex/crm/deals", icon: Handshake },
43
+ { title: "Health", url: "/cortex/crm/health", icon: Heart },
44
+ { title: "Activity", url: "/cortex/crm/activity", icon: Activity },
45
+ ]
46
+
47
+ const opsItems = [
48
+ { title: "Support", url: "/cortex/support", icon: Headphones },
49
+ { title: "Community", url: "/cortex/community", icon: Users },
50
+ { title: "Knowledge Base", url: "/cortex/kb", icon: BookOpen },
51
+ { title: "Courses", url: "/cortex/courses", icon: GraduationCap },
52
+ ]
53
+
54
+ export function CortexSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
55
+ const navigate = useNavigate()
56
+ const location = useLocation()
57
+ const { user } = useAuth()
58
+
59
+ const isActive = (url: string) => location.pathname.startsWith(url)
60
+
61
+ return (
62
+ <Sidebar {...props}>
63
+ <SidebarHeader>
64
+ <SidebarMenu>
65
+ <SidebarMenuItem>
66
+ <SidebarMenuButton size="lg" onClick={() => navigate("/cortex/dashboard")}>
67
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
68
+ <span className="text-sm font-bold">Cx</span>
69
+ </div>
70
+ <div className="flex flex-col gap-0.5 leading-none">
71
+ <span className="font-semibold">Cortex</span>
72
+ <span className="text-xs text-muted-foreground">Operations</span>
73
+ </div>
74
+ </SidebarMenuButton>
75
+ </SidebarMenuItem>
76
+ </SidebarMenu>
77
+ </SidebarHeader>
78
+
79
+ <SidebarContent>
80
+ <SidebarGroup>
81
+ <SidebarGroupLabel>CRM</SidebarGroupLabel>
82
+ <SidebarGroupContent>
83
+ <SidebarMenu>
84
+ {crmItems.map((item) => (
85
+ <SidebarMenuItem key={item.title}>
86
+ <SidebarMenuButton
87
+ asChild
88
+ isActive={isActive(item.url)}
89
+ >
90
+ <a href={item.url}>
91
+ <item.icon className="h-4 w-4" />
92
+ <span>{item.title}</span>
93
+ </a>
94
+ </SidebarMenuButton>
95
+ </SidebarMenuItem>
96
+ ))}
97
+ </SidebarMenu>
98
+ </SidebarGroupContent>
99
+ </SidebarGroup>
100
+
101
+ <SidebarGroup>
102
+ <SidebarGroupLabel>Operations</SidebarGroupLabel>
103
+ <SidebarGroupContent>
104
+ <SidebarMenu>
105
+ {opsItems.map((item) => (
106
+ <SidebarMenuItem key={item.title}>
107
+ <SidebarMenuButton
108
+ asChild
109
+ isActive={isActive(item.url)}
110
+ >
111
+ <a href={item.url}>
112
+ <item.icon className="h-4 w-4" />
113
+ <span>{item.title}</span>
114
+ </a>
115
+ </SidebarMenuButton>
116
+ </SidebarMenuItem>
117
+ ))}
118
+ </SidebarMenu>
119
+ </SidebarGroupContent>
120
+ </SidebarGroup>
121
+ </SidebarContent>
122
+
123
+ <SidebarFooter className="p-4">
124
+ <div className="text-xs text-muted-foreground truncate">{user?.email || ''}</div>
125
+ </SidebarFooter>
126
+
127
+ <SidebarRail />
128
+ </Sidebar>
129
+ )
130
+ }
@@ -0,0 +1,356 @@
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
+ })