spine-framework 0.3.125 → 0.3.127

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 (319) hide show
  1. package/.framework/cli/commands/agents.ts +35 -37
  2. package/.framework/cli/commands/auth.ts +35 -34
  3. package/.framework/cli/commands/create-app.ts +104 -40
  4. package/.framework/cli/commands/dev.ts +104 -69
  5. package/.framework/cli/commands/doctor.ts +169 -97
  6. package/.framework/cli/commands/generate.ts +82 -161
  7. package/.framework/cli/commands/init.ts +72 -34
  8. package/.framework/cli/commands/install-app.ts +260 -125
  9. package/.framework/cli/commands/items.ts +33 -44
  10. package/.framework/cli/commands/migrate.ts +122 -40
  11. package/.framework/cli/commands/migrations.ts +33 -40
  12. package/.framework/cli/commands/pipelines.ts +31 -39
  13. package/.framework/cli/commands/status.ts +47 -23
  14. package/.framework/cli/commands/system.ts +47 -37
  15. package/.framework/cli/commands/test.ts +112 -68
  16. package/.framework/cli/commands/validate-app.ts +215 -121
  17. package/.framework/cli/context.ts +112 -130
  18. package/.framework/cli/env-loader.ts +47 -18
  19. package/.framework/cli/index.ts +81 -42
  20. package/.framework/docs/runtime-types.md +1024 -0
  21. package/.framework/functions/_shared/agent-runner.ts +1296 -961
  22. package/.framework/functions/_shared/app-manifest.ts +82 -68
  23. package/.framework/functions/_shared/audit.ts +135 -83
  24. package/.framework/functions/_shared/{cil-state.ts → cil.ts} +147 -54
  25. package/.framework/functions/_shared/db.ts +80 -126
  26. package/.framework/functions/_shared/hash-api-key.ts +63 -0
  27. package/.framework/functions/_shared/index.ts +222 -347
  28. package/.framework/functions/_shared/kb.ts +319 -0
  29. package/.framework/functions/_shared/llm.ts +92 -0
  30. package/.framework/functions/_shared/logging.ts +288 -0
  31. package/.framework/functions/_shared/middleware.ts +329 -365
  32. package/.framework/functions/_shared/outcomes.ts +36 -0
  33. package/.framework/functions/_shared/permissions.ts +359 -643
  34. package/.framework/functions/_shared/pipeline-runner.ts +243 -316
  35. package/.framework/functions/_shared/principal.ts +376 -468
  36. package/.framework/functions/_shared/resolve-credentials.ts +102 -0
  37. package/.framework/functions/_shared/resolve-ids.ts +162 -14
  38. package/.framework/functions/_shared/retry-deliveries.ts +104 -0
  39. package/.framework/functions/_shared/schema-utils.ts +611 -373
  40. package/.framework/functions/_shared/testing.ts +207 -137
  41. package/.framework/functions/_shared/tracing.ts +276 -0
  42. package/.framework/functions/_shared/trigger-engine.ts +159 -211
  43. package/.framework/functions/_shared/webhook-registration.ts +95 -81
  44. package/.framework/functions/_shared/webhook-registry.ts +153 -75
  45. package/.framework/functions/account-nodes.ts +31 -53
  46. package/.framework/functions/adapters/generic-webhook-inbound.ts +56 -0
  47. package/.framework/functions/adapters/generic-webhook-outbound.ts +43 -0
  48. package/.framework/functions/adapters/openai-outbound.ts +40 -0
  49. package/.framework/functions/admin-data.ts +687 -533
  50. package/.framework/functions/admin-funnel.ts +31 -20
  51. package/.framework/functions/admin-install-funnel.ts +47 -28
  52. package/.framework/functions/admin-signals.ts +47 -29
  53. package/.framework/functions/ai-agents.ts +122 -147
  54. package/.framework/functions/ai-interactions.ts +180 -90
  55. package/.framework/functions/api-keys.ts +250 -192
  56. package/.framework/functions/apps.ts +203 -197
  57. package/.framework/functions/auth.ts +98 -91
  58. package/.framework/functions/cil-agent-ingest.ts +773 -0
  59. package/.framework/functions/cil-agent.ts +592 -0
  60. package/.framework/functions/cil-callmap.ts +1227 -0
  61. package/.framework/functions/cil-config.ts +583 -0
  62. package/.framework/functions/cil-context.ts +445 -0
  63. package/.framework/functions/cil-doc.ts +1752 -0
  64. package/.framework/functions/cil-embeddings.ts +1202 -0
  65. package/.framework/functions/cil-graph.ts +585 -0
  66. package/.framework/functions/cil-ingest.ts +2416 -0
  67. package/.framework/functions/cil-pipeline.ts +318 -0
  68. package/.framework/functions/cil-project.ts +499 -0
  69. package/.framework/functions/cil-validate.ts +469 -0
  70. package/.framework/functions/debug-auth.ts +73 -53
  71. package/.framework/functions/deliveries.ts +117 -0
  72. package/.framework/functions/embeddings.ts +528 -516
  73. package/.framework/functions/eval-harness.ts +158 -106
  74. package/.framework/functions/integration-outbound.ts +311 -0
  75. package/.framework/functions/integration-routes.ts +240 -466
  76. package/.framework/functions/integrations.ts +132 -140
  77. package/.framework/functions/invites.ts +401 -201
  78. package/.framework/functions/item-progress.ts +113 -53
  79. package/.framework/functions/kb.ts +28 -0
  80. package/.framework/functions/logs.ts +239 -222
  81. package/.framework/functions/observability.ts +308 -259
  82. package/.framework/functions/pipeline-executions.ts +179 -235
  83. package/.framework/functions/pipelines.ts +163 -205
  84. package/.framework/functions/prompt-configs.ts +109 -156
  85. package/.framework/functions/roles.ts +97 -147
  86. package/.framework/functions/system-cron.ts +132 -218
  87. package/.framework/functions/system.ts +112 -52
  88. package/.framework/functions/tests.ts +32 -28
  89. package/.framework/functions/timers.ts +162 -210
  90. package/.framework/functions/triggers.ts +282 -346
  91. package/.framework/functions/types.ts +283 -400
  92. package/.framework/migrations/001_core_schema.sql +630 -0
  93. package/.framework/migrations/002_core_rls_and_functions.sql +532 -0
  94. package/.framework/migrations/{003_system_data.sql → 003_core_system_data.sql} +26 -153
  95. package/.framework/migrations/004_core_rpc_and_broker.sql +421 -0
  96. package/.framework/migrations/005_core_observability.sql +598 -0
  97. package/.framework/migrations/006_security_hardening_rls.sql +97 -0
  98. package/.framework/migrations/007_kb_article_list.sql +237 -0
  99. package/.framework/migrations/008_items_target.sql +8 -0
  100. package/.framework/migrations/009_roles_slug_unique.sql +11 -0
  101. package/.framework/public/spine.config.json +91 -0
  102. package/.framework/scripts/app-install-cli.ts +112 -98
  103. package/.framework/src/App.tsx +67 -20
  104. package/.framework/src/apps/admin/index.tsx +97 -27
  105. package/.framework/src/components/AppWrapper.tsx +50 -11
  106. package/.framework/src/components/CustomAppLoader.tsx +131 -22
  107. package/.framework/src/components/admin/AdminListPage.tsx +75 -8
  108. package/.framework/src/components/admin/AdminSidebar.tsx +148 -36
  109. package/.framework/src/components/admin/AdminStatsCard.tsx +48 -29
  110. package/.framework/src/components/admin/PageInstructions.tsx +49 -0
  111. package/.framework/src/components/admin/SortableTableHeader.tsx +49 -2
  112. package/.framework/src/components/app-shell/GenericAppShell.tsx +69 -11
  113. package/.framework/src/components/app-shell/GenericDetailPage.tsx +55 -10
  114. package/.framework/src/components/app-shell/GenericListPage.tsx +76 -9
  115. package/.framework/src/components/app-sidebar.tsx +50 -3
  116. package/.framework/src/components/auth/ProtectedRoute.tsx +53 -45
  117. package/.framework/src/components/kb/KBArticleEditor.tsx +409 -0
  118. package/.framework/src/components/kb/KBArticleList.tsx +557 -0
  119. package/.framework/src/components/layout/AppShell.tsx +54 -16
  120. package/.framework/src/components/layout/Header.tsx +50 -38
  121. package/.framework/src/components/layout/Layout.tsx +48 -32
  122. package/.framework/src/components/layout/Sidebar.tsx +167 -16
  123. package/.framework/src/components/runtime/DataDetailHeader.tsx +33 -1
  124. package/.framework/src/components/runtime/DataDetailPage.tsx +289 -4
  125. package/.framework/src/components/runtime/DataFilters.tsx +78 -1
  126. package/.framework/src/components/runtime/DataHeader.tsx +32 -29
  127. package/.framework/src/components/runtime/DataListPage.tsx +161 -2
  128. package/.framework/src/components/runtime/DataStats.tsx +48 -1
  129. package/.framework/src/components/runtime/DataTable.tsx +153 -1
  130. package/.framework/src/components/runtime/SchemaDetailForm.tsx +77 -46
  131. package/.framework/src/components/runtime/index.ts +16 -9
  132. package/.framework/src/components/search-form.tsx +33 -1
  133. package/.framework/src/components/shared/AgentView.tsx +48 -51
  134. package/.framework/src/components/shared/FieldRenderer.tsx +84 -67
  135. package/.framework/src/components/shared/InviteManager.tsx +113 -52
  136. package/.framework/src/components/shared/MemberList.tsx +182 -0
  137. package/.framework/src/components/shared/SchemaFields.tsx +96 -78
  138. package/.framework/src/components/ui/DataTable.tsx +86 -56
  139. package/.framework/src/components/ui/Form.tsx +188 -109
  140. package/.framework/src/components/ui/ItemCard.tsx +86 -58
  141. package/.framework/src/components/ui/ItemListView.tsx +88 -61
  142. package/.framework/src/components/ui/LoadingSpinner.tsx +52 -27
  143. package/.framework/src/components/ui/Modal.tsx +81 -4
  144. package/.framework/src/components/ui/RichTextEditor.tsx +101 -9
  145. package/.framework/src/components/ui/accordion.tsx +97 -5
  146. package/.framework/src/components/ui/alert-dialog.tsx +225 -13
  147. package/.framework/src/components/ui/alert.tsx +113 -6
  148. package/.framework/src/components/ui/aspect-ratio.tsx +49 -2
  149. package/.framework/src/components/ui/avatar.tsx +129 -7
  150. package/.framework/src/components/ui/badge.tsx +65 -3
  151. package/.framework/src/components/ui/breadcrumb.tsx +145 -8
  152. package/.framework/src/components/ui/button-group.tsx +81 -4
  153. package/.framework/src/components/ui/button.tsx +49 -2
  154. package/.framework/src/components/ui/calendar.tsx +65 -3
  155. package/.framework/src/components/ui/card.tsx +145 -8
  156. package/.framework/src/components/ui/carousel.tsx +129 -7
  157. package/.framework/src/components/ui/chart.tsx +161 -10
  158. package/.framework/src/components/ui/checkbox.tsx +49 -2
  159. package/.framework/src/components/ui/collapsible.tsx +81 -5
  160. package/.framework/src/components/ui/combobox.tsx +305 -20
  161. package/.framework/src/components/ui/command.tsx +177 -11
  162. package/.framework/src/components/ui/context-menu.tsx +273 -17
  163. package/.framework/src/components/ui/dialog.tsx +209 -27
  164. package/.framework/src/components/ui/direction.tsx +64 -4
  165. package/.framework/src/components/ui/drawer.tsx +193 -12
  166. package/.framework/src/components/ui/dropdown-menu.tsx +273 -18
  167. package/.framework/src/components/ui/empty.tsx +145 -9
  168. package/.framework/src/components/ui/field.tsx +209 -14
  169. package/.framework/src/components/ui/hover-card.tsx +81 -4
  170. package/.framework/src/components/ui/input-group.tsx +162 -10
  171. package/.framework/src/components/ui/input-otp.tsx +98 -5
  172. package/.framework/src/components/ui/input.tsx +49 -2
  173. package/.framework/src/components/ui/item.tsx +225 -13
  174. package/.framework/src/components/ui/kbd.tsx +65 -3
  175. package/.framework/src/components/ui/label.tsx +49 -2
  176. package/.framework/src/components/ui/menubar.tsx +289 -17
  177. package/.framework/src/components/ui/native-select.tsx +97 -5
  178. package/.framework/src/components/ui/navigation-menu.tsx +177 -10
  179. package/.framework/src/components/ui/pagination.tsx +161 -9
  180. package/.framework/src/components/ui/popover.tsx +145 -8
  181. package/.framework/src/components/ui/progress.tsx +49 -2
  182. package/.framework/src/components/ui/radio-group.tsx +65 -3
  183. package/.framework/src/components/ui/resizable.tsx +82 -4
  184. package/.framework/src/components/ui/scroll-area.tsx +65 -3
  185. package/.framework/src/components/ui/select.tsx +193 -11
  186. package/.framework/src/components/ui/separator.tsx +49 -2
  187. package/.framework/src/components/ui/sheet.tsx +193 -11
  188. package/.framework/src/components/ui/sidebar.tsx +481 -38
  189. package/.framework/src/components/ui/skeleton.tsx +49 -2
  190. package/.framework/src/components/ui/slider.tsx +49 -2
  191. package/.framework/src/components/ui/sonner.tsx +49 -2
  192. package/.framework/src/components/ui/spinner.tsx +49 -2
  193. package/.framework/src/components/ui/switch.tsx +49 -2
  194. package/.framework/src/components/ui/table-primitives.tsx +160 -33
  195. package/.framework/src/components/ui/table.tsx +161 -9
  196. package/.framework/src/components/ui/tabs.tsx +113 -6
  197. package/.framework/src/components/ui/textarea.tsx +49 -2
  198. package/.framework/src/components/ui/toggle-group.tsx +81 -4
  199. package/.framework/src/components/ui/toggle.tsx +65 -3
  200. package/.framework/src/components/ui/tooltip.tsx +97 -5
  201. package/.framework/src/contexts/AppContext.tsx +129 -45
  202. package/.framework/src/contexts/AuthContext.tsx +173 -145
  203. package/.framework/src/hooks/use-mobile.ts +48 -2
  204. package/.framework/src/hooks/useApi.ts +208 -159
  205. package/.framework/src/hooks/useAppPath.ts +32 -23
  206. package/.framework/src/hooks/useApps.ts +51 -20
  207. package/.framework/src/hooks/useEntityList.ts +32 -78
  208. package/.framework/src/hooks/useEntityRecord.ts +35 -96
  209. package/.framework/src/hooks/useForm.ts +32 -92
  210. package/.framework/src/hooks/useListSchema.ts +49 -78
  211. package/.framework/src/hooks/useSchemaRecord.ts +114 -105
  212. package/.framework/src/lib/api.ts +192 -115
  213. package/.framework/src/lib/supabase.ts +98 -64
  214. package/.framework/src/lib/utils.ts +287 -151
  215. package/.framework/src/main.tsx +49 -3
  216. package/.framework/src/pages/DashboardPage.tsx +52 -29
  217. package/.framework/src/pages/NotFoundPage.tsx +32 -10
  218. package/.framework/src/pages/SetupPage.tsx +32 -12
  219. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +119 -10
  220. package/.framework/src/pages/admin/AIAgentsPage.tsx +48 -19
  221. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +187 -17
  222. package/.framework/src/pages/admin/APIKeysPage.tsx +98 -22
  223. package/.framework/src/pages/admin/AlertsConfigPage.tsx +113 -26
  224. package/.framework/src/pages/admin/AppDetailPage.tsx +125 -8
  225. package/.framework/src/pages/admin/AppsPage.tsx +49 -2
  226. package/.framework/src/pages/admin/DeliveriesPage.tsx +196 -0
  227. package/.framework/src/pages/admin/DesignedPage.tsx +32 -18
  228. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +215 -484
  229. package/.framework/src/pages/admin/EmbeddingsPage.tsx +116 -319
  230. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +34 -9
  231. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +34 -9
  232. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -16
  233. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +183 -26
  234. package/.framework/src/pages/admin/IntegrationsPage.tsx +48 -19
  235. package/.framework/src/pages/admin/InvitesPage.tsx +48 -14
  236. package/.framework/src/pages/admin/LogsPage.tsx +422 -75
  237. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +34 -9
  238. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +113 -24
  239. package/.framework/src/pages/admin/PipelineDetailPage.tsx +206 -34
  240. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +50 -17
  241. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -21
  242. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +378 -88
  243. package/.framework/src/pages/admin/PromptConfigsPage.tsx +125 -16
  244. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +49 -10
  245. package/.framework/src/pages/admin/RoleDetailPage.tsx +130 -10
  246. package/.framework/src/pages/admin/RolesPage.tsx +171 -18
  247. package/.framework/src/pages/admin/SelectTestPage.tsx +34 -9
  248. package/.framework/src/pages/admin/ShadcnTestPage.tsx +49 -11
  249. package/.framework/src/pages/admin/SimpleDashboard.tsx +49 -10
  250. package/.framework/src/pages/admin/TestRunDetailPage.tsx +49 -4
  251. package/.framework/src/pages/admin/TestingDashboard.tsx +98 -28
  252. package/.framework/src/pages/admin/TimerDetailPage.tsx +143 -33
  253. package/.framework/src/pages/admin/TimersPage.tsx +48 -20
  254. package/.framework/src/pages/admin/TriggerDetailPage.tsx +118 -30
  255. package/.framework/src/pages/admin/TriggersPage.tsx +64 -19
  256. package/.framework/src/pages/admin/TypeDetailPage.tsx +138 -15
  257. package/.framework/src/pages/admin/TypesPage.tsx +48 -20
  258. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +465 -0
  259. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +787 -0
  260. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +699 -0
  261. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +647 -0
  262. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +616 -0
  263. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +1402 -0
  264. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +614 -0
  265. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +571 -0
  266. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +731 -0
  267. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  268. package/.framework/src/pages/auth/AuthConfirmPage.tsx +52 -51
  269. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +31 -22
  270. package/.framework/src/pages/auth/LoginPage.tsx +50 -23
  271. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -15
  272. package/.framework/src/pages/auth/RegisterPage.tsx +281 -317
  273. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -0
  274. package/.framework/src/pages/spine-framework/CLIPage.tsx +33 -0
  275. package/.framework/src/types/auth.ts +128 -71
  276. package/.framework/src/types/types.ts +373 -157
  277. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +39 -0
  278. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +36 -0
  279. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +46 -0
  280. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +28 -0
  281. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +38 -0
  282. package/docs/README.md +9 -130
  283. package/netlify.toml +3 -3
  284. package/package.json +13 -25
  285. package/scripts/kb-ingest-docs.ts +203 -0
  286. package/spine.config.json +65 -4
  287. package/.framework/migrations/001_schema.sql +0 -425
  288. package/.framework/migrations/002_rls_and_functions.sql +0 -763
  289. package/.framework/migrations/004_enhancements.sql +0 -43
  290. package/.framework/migrations/005_auth_user_trigger.sql +0 -105
  291. package/.framework/migrations/006_fix_current_actor_id.sql +0 -21
  292. package/.framework/migrations/016_invites.sql +0 -27
  293. package/.framework/migrations/017_fix_get_account_apps.sql +0 -78
  294. package/.framework/migrations/018_apps_required_roles.sql +0 -17
  295. package/.framework/migrations/019_triggers_unique_constraint.sql +0 -13
  296. package/.framework/migrations/020_get_account_apps_role_filter.sql +0 -117
  297. package/.framework/migrations/021_app_domains.sql +0 -112
  298. package/.framework/migrations/022_route_prefix_domain_unique.sql +0 -21
  299. package/.framework/migrations/023_match_embeddings_rpc.sql +0 -78
  300. package/.framework/migrations/024_messages_account_id.sql +0 -9
  301. package/.framework/migrations/025_retype_threads_messages.sql +0 -240
  302. package/.framework/migrations/026_fix_rls_policies.sql +0 -47
  303. package/.framework/migrations/027_embedding_lifecycle.sql +0 -176
  304. package/.framework/migrations/028_fix_message_data_permissions.sql +0 -49
  305. package/.framework/migrations/029_tighten_support_triage_prompt.sql +0 -41
  306. package/.framework/migrations/030_messages_updated_by.sql +0 -6
  307. package/.framework/migrations/031_seed_upsert_constraints.sql +0 -35
  308. package/docs/APPS.md +0 -232
  309. package/docs/ARCHITECTURE.md +0 -246
  310. package/docs/INSTALL.md +0 -155
  311. package/docs/design-schema-spec.md +0 -1837
  312. package/docs/dev-guides/01-create-an-app.md +0 -183
  313. package/docs/dev-guides/02-create-a-function.md +0 -178
  314. package/docs/dev-guides/03-create-a-component.md +0 -178
  315. package/docs/dev-guides/04-webhook-handlers.md +0 -129
  316. package/docs/dev-guides/05-testing.md +0 -168
  317. package/docs/dev-guides/README.md +0 -40
  318. package/docs/npm-packages-plan.md +0 -288
  319. package/docs/spine-core--api-reference.md +0 -822
@@ -0,0 +1,319 @@
1
+ import { adminDb } from './db'
2
+ import type { CoreContext } from './middleware'
3
+ import { sanitizeRecordData } from './permissions'
4
+ import { callOpenAIEmbeddings } from './llm'
5
+
6
+ const EMBEDDING_MODEL = 'text-embedding-3-small'
7
+ const DEFAULT_CHUNK_SIZE = 800
8
+ const DEFAULT_CHUNK_OVERLAP = 100
9
+ const SIMILARITY_THRESHOLD = 0.15
10
+ const DEFAULT_MATCH_COUNT = 10
11
+ const SECURITY_LEVELS: Record<string, string> = {
12
+ 'kb-spine-admin': 'restricted',
13
+ 'kb-spine-internal': 'internal',
14
+ 'kb-spine-public': 'external',
15
+ 'kb-account': 'external',
16
+ }
17
+
18
+ async function generateEmbedding(text: string, ctx?: CoreContext): Promise<number[]> {
19
+ const json = await callOpenAIEmbeddings(ctx, [text], EMBEDDING_MODEL)
20
+ return json.data?.[0]?.embedding || []
21
+ }
22
+
23
+ function chunkText(text: string, chunkSize: number, overlap: number): string[] {
24
+ if (!text) return []
25
+ if (text.length <= chunkSize) return [text]
26
+
27
+ const chunks: string[] = []
28
+ let start = 0
29
+ while (start < text.length) {
30
+ const end = Math.min(start + chunkSize, text.length)
31
+ const chunk = text.slice(start, end)
32
+ chunks.push(chunk)
33
+ start += chunkSize - overlap
34
+ if (start >= end) break
35
+ }
36
+ return chunks
37
+ }
38
+
39
+ function getFieldValue(record: any, field: string): string {
40
+ if (record[field] !== undefined && record[field] !== null) {
41
+ const value = record[field]
42
+ if (Array.isArray(value)) return value.join(' ')
43
+ return String(value)
44
+ }
45
+ const data = record.data || {}
46
+ if (data[field] !== undefined && data[field] !== null) {
47
+ const value = data[field]
48
+ if (Array.isArray(value)) return value.join(' ')
49
+ return String(value)
50
+ }
51
+ return ''
52
+ }
53
+
54
+ function getRawFieldValue(record: any, field: string): any {
55
+ if (record[field] !== undefined && record[field] !== null) return record[field]
56
+ const data = record.data || {}
57
+ if (data[field] !== undefined && data[field] !== null) return data[field]
58
+ return undefined
59
+ }
60
+
61
+ function matchesFilters(record: any, filters: Record<string, any>): boolean {
62
+ if (!filters || Object.keys(filters).length === 0) return true
63
+
64
+ for (const [field, expected] of Object.entries(filters)) {
65
+ const raw = getRawFieldValue(record, field)
66
+ const actual = Array.isArray(raw) ? raw : (raw !== undefined ? [raw] : [])
67
+ const expectedArr = Array.isArray(expected) ? expected : (expected !== undefined ? [expected] : [])
68
+ if (expectedArr.length === 0) continue
69
+
70
+ const hasOverlap = expectedArr.some((ev: any) => actual.includes(ev))
71
+ if (!hasOverlap) return false
72
+ }
73
+
74
+ return true
75
+ }
76
+
77
+ async function ensurePlatformAccountId(): Promise<string | null> {
78
+ const { data } = await adminDb
79
+ .from('accounts')
80
+ .select('id')
81
+ .eq('slug', 'spine-system')
82
+ .maybeSingle()
83
+ return data?.id ?? null
84
+ }
85
+
86
+ export async function kbIngest(ctx: CoreContext, body: { id: string }): Promise<{ ok: boolean; message: string; count?: number }> {
87
+ const { id } = body
88
+ if (!id) throw new Error('id is required')
89
+
90
+ const { data: record, error } = await adminDb
91
+ .from('items')
92
+ .select('*')
93
+ .eq('id', id)
94
+ .eq('account_id', ctx.accountId)
95
+ .maybeSingle()
96
+
97
+ if (error) throw error
98
+ if (!record) throw new Error('record not found')
99
+
100
+ if (record.status !== 'published') {
101
+ await kbRevoke(ctx, { id })
102
+ return { ok: true, message: 'record is not published; embeddings revoked if any' }
103
+ }
104
+
105
+ const designSchema = record.design_schema || {}
106
+ const kbConfig = designSchema.functionality?.kb || {}
107
+ const vectorFields = kbConfig.vector_fields || ['title', 'content']
108
+ const chunkField = kbConfig.chunk_field || 'content'
109
+ const chunkSize = kbConfig.chunk_size || DEFAULT_CHUNK_SIZE
110
+ const chunkOverlap = kbConfig.chunk_overlap || DEFAULT_CHUNK_OVERLAP
111
+
112
+ const { data: typeRecord, error: typeSlugError } = await adminDb
113
+ .from('types')
114
+ .select('slug, app_id')
115
+ .eq('id', record.type_id)
116
+ .single()
117
+ if (typeSlugError) throw typeSlugError
118
+
119
+ const typeSlug = typeRecord?.slug || ''
120
+ const appId = record.app_id || typeRecord?.app_id || null
121
+ const securityLevel = SECURITY_LEVELS[typeSlug] || 'internal'
122
+
123
+ await kbRevoke(ctx, { id })
124
+
125
+ const embeddingsToInsert: any[] = []
126
+ let chunkIndex = 0
127
+
128
+ for (const field of vectorFields) {
129
+ const raw = getFieldValue(record, field)
130
+ if (!raw.trim()) continue
131
+
132
+ const isChunked = field === chunkField && raw.length > chunkSize
133
+ const chunks = isChunked ? chunkText(raw, chunkSize, chunkOverlap) : [raw]
134
+
135
+ for (const chunk of chunks) {
136
+ if (!chunk.trim()) continue
137
+ const embedding = await generateEmbedding(chunk, ctx)
138
+ if (!embedding.length) continue
139
+
140
+ embeddingsToInsert.push({
141
+ account_id: record.account_id,
142
+ model_id: EMBEDDING_MODEL,
143
+ document_id: id,
144
+ chunk_index: chunkIndex++,
145
+ content: chunk,
146
+ embedding: JSON.stringify(embedding),
147
+ metadata: {
148
+ type_slug: typeSlug,
149
+ item_type: typeSlug,
150
+ app_id: appId,
151
+ vector_type: field,
152
+ security_level: securityLevel,
153
+ status: record.status,
154
+ },
155
+ approved_at: new Date().toISOString(),
156
+ revoked_at: null,
157
+ })
158
+ }
159
+ }
160
+
161
+ if (embeddingsToInsert.length === 0) {
162
+ return { ok: true, message: 'no content to embed', count: 0 }
163
+ }
164
+
165
+ const { error: insertError } = await adminDb.from('embeddings').insert(embeddingsToInsert)
166
+ if (insertError) throw insertError
167
+
168
+ return { ok: true, message: 'embeddings generated', count: embeddingsToInsert.length }
169
+ }
170
+
171
+ export async function kbRevoke(ctx: CoreContext, body: { id: string }): Promise<{ ok: boolean; message: string; count?: number }> {
172
+ const { id } = body
173
+ if (!id) throw new Error('id is required')
174
+
175
+ const { data, error } = await adminDb.rpc('revoke_embeddings_for_item', { p_item_id: id })
176
+ if (error) throw error
177
+
178
+ const { error: deleteError } = await adminDb
179
+ .from('embeddings')
180
+ .delete()
181
+ .eq('document_id', id)
182
+
183
+ if (deleteError) throw deleteError
184
+
185
+ return { ok: true, message: 'embeddings revoked', count: data || 0 }
186
+ }
187
+
188
+ export async function kbReindex(ctx: CoreContext, body: { type_slug: string; app_id?: string }): Promise<{ ok: boolean; count: number }> {
189
+ const { type_slug } = body
190
+ if (!type_slug) throw new Error('type_slug is required')
191
+
192
+ const { data: typeRecord } = await adminDb
193
+ .from('types')
194
+ .select('id')
195
+ .eq('slug', type_slug)
196
+ .maybeSingle()
197
+
198
+ if (!typeRecord) throw new Error(`type not found: ${type_slug}`)
199
+
200
+ let query = adminDb.from('items').select('id').eq('type_id', typeRecord.id).eq('status', 'published')
201
+ if (body.app_id) query = query.eq('app_id', body.app_id)
202
+
203
+ const { data: records, error } = await query
204
+ if (error) throw error
205
+
206
+ let count = 0
207
+ for (const record of records || []) {
208
+ await kbIngest(ctx, { id: record.id })
209
+ count++
210
+ }
211
+
212
+ return { ok: true, count }
213
+ }
214
+
215
+ export async function kbSearch(ctx: CoreContext, body: any): Promise<{ results: any[]; count: number }> {
216
+ const query = body.query
217
+ const typeSlugs = body.type_slugs || []
218
+ const sources = body.sources || ['kb']
219
+ const appId = body.app_id || null
220
+ const vectorFields = body.vector_fields || []
221
+ const limit = body.limit || DEFAULT_MATCH_COUNT
222
+ const offset = body.offset || 0
223
+ const filters = body.filters || {}
224
+ const threshold = typeof body.similarity_threshold === 'number' ? body.similarity_threshold : SIMILARITY_THRESHOLD
225
+
226
+ if (!query) throw new Error('query is required')
227
+
228
+ const accountIds = [ctx.accountId]
229
+ const platformAccountId = await ensurePlatformAccountId()
230
+ if (platformAccountId && platformAccountId !== ctx.accountId) {
231
+ accountIds.push(platformAccountId)
232
+ }
233
+
234
+ const queryEmbedding = await generateEmbedding(query, ctx)
235
+ if (!queryEmbedding.length) return { results: [], count: 0 }
236
+
237
+ const allowedVectorTypes = new Set(vectorFields)
238
+ const allowedTypeSlugs = new Set(typeSlugs)
239
+ const includeKb = sources.includes('kb')
240
+ const includeCode = sources.includes('code')
241
+
242
+ const allMatches: any[] = []
243
+ const vectorTypesToSearch = vectorFields.length ? vectorFields : [null]
244
+ const matchCount = Math.max(offset + limit, 50)
245
+
246
+ for (const vt of vectorTypesToSearch) {
247
+ const { data: matches, error: rpcError } = await adminDb.rpc('match_embeddings', {
248
+ query_embedding: queryEmbedding,
249
+ match_count: matchCount,
250
+ filter_account_ids: accountIds,
251
+ filter_vector_type: vt,
252
+ similarity_threshold: threshold,
253
+ filter_item_types: null,
254
+ })
255
+
256
+ if (rpcError) throw new Error(`rpc_${rpcError.message}`)
257
+ allMatches.push(...(matches || []))
258
+ }
259
+
260
+ const filtered = allMatches.filter((m: any) => {
261
+ if (m.similarity < threshold) return false
262
+ const meta = m.metadata || {}
263
+ const vectorType = meta.vector_type || ''
264
+ const matchAppId = meta.app_id || null
265
+
266
+ if (vectorFields.length && !allowedVectorTypes.has(vectorType)) return false
267
+
268
+ if (appId && matchAppId !== appId) return false
269
+
270
+ return true
271
+ })
272
+
273
+ const bestByDoc = new Map<string, any>()
274
+ for (const m of filtered) {
275
+ const existing = bestByDoc.get(m.document_id)
276
+ if (!existing || m.similarity > existing.similarity) bestByDoc.set(m.document_id, m)
277
+ }
278
+
279
+ const documentIds = Array.from(bestByDoc.keys()).slice(offset, offset + limit)
280
+ if (documentIds.length === 0) return { results: [], count: 0 }
281
+
282
+ const { data: records, error: recordsError } = await (ctx.db || adminDb)
283
+ .from('items')
284
+ .select('*, types!inner(slug)')
285
+ .in('id', documentIds)
286
+ .in('account_id', accountIds)
287
+
288
+ if (recordsError) throw recordsError
289
+
290
+ const results: any[] = []
291
+ for (const match of documentIds.map((id) => bestByDoc.get(id))) {
292
+ const record = (records || []).find((r: any) => r.id === match.document_id)
293
+ if (!record) continue
294
+ if (record.status !== 'published') continue
295
+ if (!matchesFilters(record, filters)) continue
296
+
297
+ const sanitized = await sanitizeRecordData(ctx, record, 'items')
298
+ if (!sanitized.data || Object.keys(sanitized.data).length === 0) continue
299
+ const recordTypeSlug = (record as any).types?.slug || sanitized.type_slug || record.type_slug
300
+ if (typeSlugs.length > 0 && !allowedTypeSlugs.has(recordTypeSlug || '')) continue
301
+
302
+ const source = match.metadata?.item_type === 'cil_chunk' ? 'code' : 'kb'
303
+ results.push({
304
+ id: record.id,
305
+ type_slug: recordTypeSlug,
306
+ title: sanitized.title || record.title,
307
+ snippet: match.content,
308
+ app_id: record.app_id || match.metadata?.app_id || null,
309
+ similarity: match.similarity,
310
+ status: record.status,
311
+ created_at: record.created_at,
312
+ source,
313
+ data: sanitized.data,
314
+ })
315
+ }
316
+
317
+ return { results: results.sort((a, b) => b.similarity - a.similarity), count: results.length }
318
+ }
319
+
@@ -0,0 +1,92 @@
1
+ // CIL[BEGIN chunk_id="llm_module_header_1784700002" type="module_header" identity="module_header" parent="none"]
2
+ // CIL[DOC-MACRO purpose="Broker-backed LLM helpers for core functions"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Route OpenAI chat and embedding calls through integration-outbound"]
4
+ // CIL[DOC-CONTEXT module="llm" layer="infrastructure" related_files="[".framework/functions/_shared/agent-runner.ts",".framework/functions/_shared/kb.ts",".framework/functions/embeddings.ts",".framework/functions/integration-outbound.ts",".framework/migrations/051_integration_broker.sql"]"]
5
+ // CIL[DOC-CONTRACT inputs="optional ctx, endpoint, body" outputs="parsed API response" errors="throws on missing key or HTTP failure" side_effects="Calls integration-outbound, writes webhook_deliveries"]
6
+ // CIL[DOC-MICRO logic_summary="Provides callOpenAI, callOpenAIEmbeddings, and callOpenAIChat. Falls back to a system CoreContext when none is provided." notable_decisions="["All OpenAI calls now create an outbound delivery record for audit/retry.","A default system context lets non-handler call sites reuse the helper."]"]
7
+ // CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
8
+ // CIL[DOC-STABILITY stability="experimental"]
9
+ // CIL[DOC-AUDIENCE audience="developer"]
10
+ // CIL[DOC-VECTOR type="context" content="Layer: infrastructure. Direct callers: 0. Direct calls: 0. Related files: .framework/functions/_shared/agent-runner.ts, .framework/functions/_shared/kb.ts, .framework/functions/embeddings.ts, .framework/functions/integration-outbound.ts, .framework/migrations/051_integration_broker.sql."]
11
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
12
+ // CIL[DOC-VECTOR type="configuration" content="Requires the system 'openai' integration to be seeded."]
13
+ // CIL[DOC-VECTOR type="constraint" content="Non-200 responses throw."]
14
+ // CIL[DOC-VECTOR type="implementation_pattern" content="Replaces direct process.env OPENAI_API_KEY fetches in core."]
15
+ // CIL[DOC-VECTOR type="user_journey" content="AI agents and embedding pipelines use the broker."]
16
+
17
+ import { execute as executeOutbound } from '../integration-outbound'
18
+ import { adminDb } from './db'
19
+ import { SYSTEM_PRINCIPAL } from './principal'
20
+
21
+ export interface LlmContext {
22
+ db?: any
23
+ principal?: any
24
+ accountId?: string | null
25
+ requestId?: string
26
+ integration?: any
27
+ }
28
+
29
+ function buildCoreContext(ctx?: LlmContext): any {
30
+ return ctx || {
31
+ db: adminDb,
32
+ principal: SYSTEM_PRINCIPAL,
33
+ accountId: null,
34
+ requestId: crypto.randomUUID()
35
+ }
36
+ }
37
+
38
+ export async function callOpenAI(ctx: LlmContext | undefined, endpoint: string, body: any): Promise<any> {
39
+ const coreCtx = buildCoreContext(ctx)
40
+ const result = await executeOutbound(coreCtx, {
41
+ integration_slug: 'openai',
42
+ action_config: { endpoint, body, credential_field: 'api_token' }
43
+ })
44
+ return result.response_body
45
+ }
46
+
47
+ export async function callOpenAIEmbeddings(
48
+ ctx: LlmContext | undefined,
49
+ inputs: string[],
50
+ model = 'text-embedding-3-small'
51
+ ): Promise<any> {
52
+ const integration = ctx?.integration
53
+ const resolvedModel = integration?.config?.models?.embedding || model
54
+ return callOpenAI(ctx, '/embeddings', {
55
+ model: resolvedModel,
56
+ input: inputs
57
+ })
58
+ }
59
+
60
+ export async function callOpenAIChat(
61
+ ctx: LlmContext | undefined,
62
+ messages: any[],
63
+ model = 'gpt-4o-mini',
64
+ options: { temperature?: number; max_tokens?: number; tools?: any[]; tool_choice?: any; response_format?: any } = {}
65
+ ): Promise<any> {
66
+ const integration = ctx?.integration
67
+ const resolvedModel = integration?.config?.models?.chat || model
68
+ const body: any = {
69
+ model: resolvedModel,
70
+ messages,
71
+ temperature: options.temperature ?? 0.7,
72
+ max_tokens: options.max_tokens ?? 4000
73
+ }
74
+ if (options.tools?.length) {
75
+ body.tools = options.tools
76
+ body.tool_choice = options.tool_choice ?? 'auto'
77
+ }
78
+ if (options.response_format) {
79
+ body.response_format = options.response_format
80
+ }
81
+ return callOpenAI(ctx, '/chat/completions', body)
82
+ }
83
+
84
+ export async function callEmbeddingApi(inputs: string[], model = 'text-embedding-3-small'): Promise<number[][]> {
85
+ if (!inputs.length) throw new Error('No inputs provided')
86
+ const j = await callOpenAIEmbeddings(undefined, inputs, model)
87
+ return (j.data || [])
88
+ .sort((a: any, b: any) => a.index - b.index)
89
+ .map((d: any) => d.embedding)
90
+ }
91
+
92
+ // CIL[END chunk_id="llm_module_header_1784700002"]
@@ -0,0 +1,288 @@
1
+ import { CoreContext } from './middleware'
2
+ import { adminDb } from './db'
3
+ import { formatPrincipalForAudit } from './principal'
4
+
5
+ export type LogLevel = 'off' | 'timing' | 'warnings' | 'errors' | 'all'
6
+
7
+ export interface LogContext extends CoreContext {
8
+ appId?: string | null
9
+ source?: string
10
+ logLevel?: string
11
+ }
12
+
13
+ export interface AdminDataLogPayload {
14
+ entity: string
15
+ action: string
16
+ method?: string
17
+ durationMs?: number
18
+ recordId?: string
19
+ rowCount?: number
20
+ filters?: Record<string, any>
21
+ typeSlug?: string
22
+ error?: any
23
+ warning?: string
24
+ }
25
+
26
+ const VALID_LEVELS: LogLevel[] = ['off', 'timing', 'warnings', 'errors', 'all']
27
+
28
+ function parseLogLevelHeader(value: string | undefined): LogLevel[] {
29
+ if (!value) return []
30
+ const parts = value.split(/[,|]/).map(p => p.trim()).filter(Boolean)
31
+ if (parts.length === 0) {
32
+ const single = value.trim() as LogLevel
33
+ return VALID_LEVELS.includes(single) ? [single] : []
34
+ }
35
+ const levels = parts.filter(p => VALID_LEVELS.includes(p as LogLevel)) as LogLevel[]
36
+ return levels
37
+ }
38
+
39
+ function getCache(ctx: LogContext): Map<string, any> | undefined {
40
+ return (ctx as any).__loggingCache
41
+ }
42
+
43
+ function getOrCreateCache(ctx: LogContext): Map<string, any> {
44
+ if (!(ctx as any).__loggingCache) {
45
+ ;(ctx as any).__loggingCache = new Map()
46
+ }
47
+ return (ctx as any).__loggingCache
48
+ }
49
+
50
+ function findSetting(
51
+ rows: any[],
52
+ scope: string,
53
+ scopeId?: string | null
54
+ ): LogLevel[] | undefined {
55
+ const row = rows.find(
56
+ (r) => r.scope === scope && (scopeId === undefined ? r.scope_id == null : r.scope_id === scopeId)
57
+ )
58
+ return row?.value?.levels as LogLevel[] | undefined
59
+ }
60
+
61
+ export async function resolveLogLevels(
62
+ ctx: LogContext,
63
+ options?: { entity?: string; action?: string }
64
+ ): Promise<LogLevel[]> {
65
+ const cache = getOrCreateCache(ctx)
66
+ const cacheKey = `levels:${options?.entity || ''}:${options?.action || ''}`
67
+ if (cache.has(cacheKey)) {
68
+ return cache.get(cacheKey)
69
+ }
70
+
71
+ const defaultLevels: LogLevel[] = ['timing', 'errors']
72
+
73
+ const { data: rows, error } = await adminDb
74
+ .from('system_settings')
75
+ .select('scope, scope_id, value')
76
+ .eq('key', 'logging')
77
+
78
+ if (error || !rows || rows.length === 0) {
79
+ cache.set(cacheKey, defaultLevels)
80
+ return defaultLevels
81
+ }
82
+
83
+ const headerLevels = parseLogLevelHeader(ctx.logLevel)
84
+ if (headerLevels.length > 0) {
85
+ cache.set(cacheKey, headerLevels)
86
+ return headerLevels
87
+ }
88
+
89
+ let levels: LogLevel[] = findSetting(rows, 'global') || defaultLevels
90
+
91
+ function apply(scope: string, scopeId?: string | null) {
92
+ const found = findSetting(rows, scope, scopeId)
93
+ if (found && found.length > 0) {
94
+ levels = found
95
+ }
96
+ }
97
+
98
+ const source = ctx.source || ctx.principal?.provenance?.sourceType
99
+ if (source) apply('source', source)
100
+
101
+ if (ctx.principal?.id) apply('principal', ctx.principal.id)
102
+
103
+ if (ctx.accountId) apply('account', ctx.accountId)
104
+
105
+ if (ctx.appId) apply('app', ctx.appId)
106
+
107
+ if (options?.entity && options?.action) {
108
+ apply('tool', `${options.entity}:${options.action}`)
109
+ }
110
+
111
+ if (options?.entity) {
112
+ apply('tool', options.entity)
113
+ }
114
+
115
+ cache.set(cacheKey, levels)
116
+ return levels
117
+ }
118
+
119
+ function buildMessage(payload: AdminDataLogPayload): string {
120
+ const { entity, action, method, durationMs, recordId, error, warning } = payload
121
+ let msg = `admin-data ${action} ${entity}`
122
+ if (method) msg += ` (${method})`
123
+ if (recordId) msg += ` id:${recordId}`
124
+ if (durationMs !== undefined) msg += ` in ${durationMs}ms`
125
+ if (warning) msg += ` warning: ${warning}`
126
+ if (error) msg += ` failed: ${error.message || String(error)}`
127
+ return msg
128
+ }
129
+
130
+ function buildContext(payload: AdminDataLogPayload): Record<string, any> {
131
+ const ctx: Record<string, any> = {}
132
+ if (payload.durationMs !== undefined) ctx.duration_ms = payload.durationMs
133
+ if (payload.method) ctx.method = payload.method
134
+ if (payload.rowCount !== undefined) ctx.row_count = payload.rowCount
135
+ if (payload.filters && Object.keys(payload.filters).length > 0) ctx.filters = payload.filters
136
+ if (payload.typeSlug) ctx.type_slug = payload.typeSlug
137
+ if (payload.warning) ctx.warning = payload.warning
138
+ if (payload.error) ctx.error_message = payload.error.message || String(payload.error)
139
+ return ctx
140
+ }
141
+
142
+ function traceLogFields(ctx: LogContext): Record<string, any> {
143
+ const trace = ctx.trace
144
+ if (!trace) return {}
145
+ return {
146
+ trace_id: trace.traceId,
147
+ span_id: trace.spanId,
148
+ parent_span_id: trace.parentSpanId,
149
+ trace_flags: trace.traceFlags,
150
+ tracestate: trace.traceState,
151
+ sampled: trace.sampled
152
+ }
153
+ }
154
+
155
+ function buildMetadata(
156
+ ctx: LogContext,
157
+ payload: AdminDataLogPayload
158
+ ): Record<string, any> {
159
+ const meta: Record<string, any> = {
160
+ request_id: ctx.requestId,
161
+ source: ctx.source || ctx.principal?.provenance?.sourceType,
162
+ entity: payload.entity,
163
+ action: payload.action,
164
+ principal: ctx.principal ? formatPrincipalForAudit(ctx.principal) : null,
165
+ ...traceLogFields(ctx)
166
+ }
167
+ if (payload.recordId) meta.record_id = payload.recordId
168
+ if (payload.error && payload.error.stack) {
169
+ meta.error_stack = String(payload.error.stack).split('\n').slice(0, 5).join('\n')
170
+ }
171
+ return meta
172
+ }
173
+
174
+ function extractLogResult(result: any): { recordId?: string; rowCount?: number } {
175
+ if (!result || typeof result !== 'object') return {}
176
+
177
+ if (Array.isArray(result)) {
178
+ return { rowCount: result.length }
179
+ }
180
+
181
+ if (typeof result.id === 'string') {
182
+ return { recordId: result.id }
183
+ }
184
+
185
+ if (result.data && Array.isArray(result.data)) {
186
+ const out: any = { rowCount: result.data.length }
187
+ if (result.data.length === 1 && result.data[0]?.id) {
188
+ out.recordId = result.data[0].id
189
+ }
190
+ return out
191
+ }
192
+
193
+ if (result.data && typeof result.data === 'object' && typeof result.data.id === 'string') {
194
+ return { recordId: result.data.id }
195
+ }
196
+
197
+ if (typeof result.count === 'number') {
198
+ return { rowCount: result.count }
199
+ }
200
+
201
+ return {}
202
+ }
203
+
204
+ export async function logAdminDataOperation(
205
+ ctx: LogContext,
206
+ payload: AdminDataLogPayload
207
+ ): Promise<void> {
208
+ try {
209
+ if (payload.entity === 'logs' || payload.entity === 'observability') {
210
+ return
211
+ }
212
+
213
+ const levels = await resolveLogLevels(ctx, payload)
214
+
215
+ if (levels.includes('off') || levels.length === 0) {
216
+ return
217
+ }
218
+
219
+ const isAll = levels.includes('all')
220
+ const isError = !!payload.error
221
+ const isWarning = !!payload.warning
222
+
223
+ if (isError && !isAll && !levels.includes('errors')) return
224
+ if (isWarning && !isAll && !levels.includes('warnings')) return
225
+ if (!isError && !isWarning && !isAll && !levels.includes('timing')) return
226
+
227
+ let level: 'info' | 'warn' | 'error' = 'info'
228
+ if (isError) level = 'error'
229
+ else if (isWarning) level = 'warn'
230
+
231
+ const logRow = {
232
+ level,
233
+ source: 'admin-data',
234
+ source_type: payload.entity,
235
+ source_id: payload.recordId || null,
236
+ person_id: ctx.principal?.id || null,
237
+ created_by: ctx.principal?.id || null,
238
+ app_id: ctx.appId || null,
239
+ account_id: ctx.accountId || null,
240
+ span_name: `admin-data.${payload.action}`,
241
+ kind: 'event',
242
+ started_at: new Date().toISOString(),
243
+ duration_ms: payload.durationMs || null,
244
+ ...traceLogFields(ctx),
245
+ message: buildMessage(payload),
246
+ context: buildContext(payload),
247
+ metadata: buildMetadata(ctx, payload),
248
+ created_at: new Date().toISOString()
249
+ }
250
+
251
+ const logDb = ctx.db || adminDb
252
+ await logDb.from('logs').insert(logRow)
253
+ } catch (err) {
254
+ console.error('Failed to write admin-data log:', err)
255
+ }
256
+ }
257
+
258
+ export async function withAdminLogging<T>(
259
+ ctx: LogContext,
260
+ params: Omit<AdminDataLogPayload, 'durationMs' | 'error' | 'recordId' | 'rowCount'>,
261
+ operation: () => Promise<T>
262
+ ): Promise<T> {
263
+ const start = Date.now()
264
+ let result: T
265
+ let error: any
266
+ let recordId: string | undefined
267
+ let rowCount: number | undefined
268
+
269
+ try {
270
+ result = await operation()
271
+ const extracted = extractLogResult(result)
272
+ recordId = extracted.recordId
273
+ rowCount = extracted.rowCount
274
+ return result
275
+ } catch (err) {
276
+ error = err
277
+ throw err
278
+ } finally {
279
+ const durationMs = Date.now() - start
280
+ logAdminDataOperation(ctx, {
281
+ ...params,
282
+ durationMs,
283
+ error,
284
+ recordId,
285
+ rowCount
286
+ }).catch(console.error)
287
+ }
288
+ }