spine-framework 0.3.144 → 0.4.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.
Files changed (451) hide show
  1. package/.env.example +76 -0
  2. package/.framework/.spine-manifest.json +19 -0
  3. package/.framework/cli/commands/agents.ts +61 -33
  4. package/.framework/cli/commands/auth.ts +60 -28
  5. package/.framework/cli/commands/create-app.ts +212 -80
  6. package/.framework/cli/commands/dev.ts +184 -68
  7. package/.framework/cli/commands/doctor.ts +324 -152
  8. package/.framework/cli/commands/generate.ts +119 -68
  9. package/.framework/cli/commands/init.ts +141 -242
  10. package/.framework/cli/commands/install-app.ts +1336 -314
  11. package/.framework/cli/commands/items.ts +31 -29
  12. package/.framework/cli/commands/migrate.ts +299 -156
  13. package/.framework/cli/commands/migrations.ts +62 -30
  14. package/.framework/cli/commands/pipelines.ts +31 -29
  15. package/.framework/cli/commands/status.ts +105 -45
  16. package/.framework/cli/commands/system.ts +47 -42
  17. package/.framework/cli/commands/test.ts +161 -102
  18. package/.framework/cli/commands/upgrade.ts +260 -0
  19. package/.framework/cli/commands/validate-app.ts +517 -161
  20. package/.framework/cli/context.ts +117 -85
  21. package/.framework/cli/env-loader.ts +74 -43
  22. package/.framework/cli/index.ts +70 -46
  23. package/.framework/cli/lib/project-files.ts +404 -0
  24. package/.framework/docs/guides/testing-guide.md +1 -1
  25. package/.framework/docs/runtime-types.md +72 -72
  26. package/.framework/functions/_shared/access.ts +84 -0
  27. package/.framework/functions/_shared/agent-runner.ts +5051 -1026
  28. package/.framework/functions/_shared/app-manifest.ts +214 -94
  29. package/.framework/functions/_shared/ast.ts +214 -0
  30. package/.framework/functions/_shared/audit.ts +91 -55
  31. package/.framework/functions/_shared/cil.ts +368 -127
  32. package/.framework/functions/_shared/db.ts +337 -68
  33. package/.framework/functions/_shared/funnel.ts +228 -0
  34. package/.framework/functions/_shared/geo.ts +105 -0
  35. package/.framework/functions/_shared/hash-api-key.ts +94 -17
  36. package/.framework/functions/_shared/index.ts +22 -224
  37. package/.framework/functions/_shared/kb.ts +832 -58
  38. package/.framework/functions/_shared/llm.ts +118 -18
  39. package/.framework/functions/_shared/logging.ts +317 -19
  40. package/.framework/functions/_shared/middleware.ts +701 -131
  41. package/.framework/functions/_shared/notify.ts +70 -0
  42. package/.framework/functions/_shared/outcomes.ts +34 -19
  43. package/.framework/functions/_shared/permissions.ts +501 -315
  44. package/.framework/functions/_shared/pipeline-runner.ts +704 -155
  45. package/.framework/functions/_shared/principal.ts +436 -263
  46. package/.framework/functions/_shared/rate-limit.ts +233 -0
  47. package/.framework/functions/_shared/resolve-credentials.ts +86 -19
  48. package/.framework/functions/_shared/resolve-ids.ts +203 -139
  49. package/.framework/functions/_shared/retry-deliveries.ts +51 -16
  50. package/.framework/functions/_shared/schema-utils.ts +602 -521
  51. package/.framework/functions/_shared/security.ts +345 -0
  52. package/.framework/functions/_shared/testing.ts +197 -172
  53. package/.framework/functions/_shared/tracing.ts +351 -21
  54. package/.framework/functions/_shared/trigger-engine.ts +425 -136
  55. package/.framework/functions/_shared/webhook-registration.ts +71 -70
  56. package/.framework/functions/_shared/webhook-registry.ts +126 -83
  57. package/.framework/functions/account-nodes.ts +30 -30
  58. package/.framework/functions/adapters/generic-webhook-inbound.ts +49 -16
  59. package/.framework/functions/adapters/generic-webhook-outbound.ts +17 -17
  60. package/.framework/functions/adapters/openai-outbound.ts +15 -16
  61. package/.framework/functions/admin-data.ts +596 -173
  62. package/.framework/functions/admin-funnel.ts +64 -60
  63. package/.framework/functions/admin-install-funnel.ts +84 -66
  64. package/.framework/functions/admin-signals.ts +69 -51
  65. package/.framework/functions/ai-agents.ts +431 -106
  66. package/.framework/functions/ai-interactions.ts +186 -112
  67. package/.framework/functions/api-keys.ts +107 -107
  68. package/.framework/functions/apps.ts +154 -136
  69. package/.framework/functions/auth.ts +83 -65
  70. package/.framework/functions/cil-agent-ingest.ts +189 -232
  71. package/.framework/functions/cil-agent.ts +433 -177
  72. package/.framework/functions/cil-callmap.ts +642 -430
  73. package/.framework/functions/cil-config.ts +139 -135
  74. package/.framework/functions/cil-context.ts +214 -127
  75. package/.framework/functions/cil-doc.ts +587 -458
  76. package/.framework/functions/cil-embeddings.ts +453 -364
  77. package/.framework/functions/cil-graph.ts +427 -142
  78. package/.framework/functions/cil-ingest.ts +1418 -1154
  79. package/.framework/functions/cil-pipeline.ts +276 -21
  80. package/.framework/functions/cil-project.ts +659 -132
  81. package/.framework/functions/cil-validate.ts +198 -164
  82. package/.framework/functions/debug-auth.ts +57 -55
  83. package/.framework/functions/deliveries.ts +80 -21
  84. package/.framework/functions/embeddings.ts +1023 -318
  85. package/.framework/functions/eval-harness.ts +146 -108
  86. package/.framework/functions/integration-outbound.ts +205 -75
  87. package/.framework/functions/integration-routes.ts +128 -23
  88. package/.framework/functions/integrations.ts +125 -100
  89. package/.framework/functions/invites.ts +1095 -460
  90. package/.framework/functions/item-progress.ts +158 -102
  91. package/.framework/functions/kb.ts +34 -10
  92. package/.framework/functions/logs.ts +133 -133
  93. package/.framework/functions/observability.ts +152 -122
  94. package/.framework/functions/pipeline-executions.ts +147 -132
  95. package/.framework/functions/pipelines.ts +134 -119
  96. package/.framework/functions/prompt-configs.ts +96 -80
  97. package/.framework/functions/roles.ts +112 -82
  98. package/.framework/functions/security.ts +450 -0
  99. package/.framework/functions/system-cron.ts +117 -80
  100. package/.framework/functions/system.ts +158 -98
  101. package/.framework/functions/tests.ts +29 -29
  102. package/.framework/functions/timers.ts +94 -95
  103. package/.framework/functions/trigger-scheduler.ts +10 -0
  104. package/.framework/functions/triggers.ts +179 -149
  105. package/.framework/functions/types.ts +137 -125
  106. package/.framework/mcp/index.ts +133 -0
  107. package/.framework/mcp/load-env.ts +60 -0
  108. package/.framework/mcp/shared/app-manifest.ts +275 -0
  109. package/.framework/mcp/shared/db.ts +477 -0
  110. package/.framework/mcp/shared/kb.ts +118 -0
  111. package/.framework/mcp/tools/architect.ts +431 -0
  112. package/.framework/mcp/tools/cil/context.ts +345 -0
  113. package/.framework/mcp/tools/cil/graph.ts +242 -0
  114. package/.framework/mcp/tools/cil/ingest.ts +1217 -0
  115. package/.framework/mcp/tools/cil/pipeline-orchestrator.ts +693 -0
  116. package/.framework/mcp/tools/cil/pipeline.ts +767 -0
  117. package/.framework/mcp/tools/cil/project.ts +791 -0
  118. package/.framework/mcp/tools/cil/retrieval.ts +336 -0
  119. package/.framework/mcp/tools/cil/search-core.ts +454 -0
  120. package/.framework/mcp/tools/cil/search.ts +97 -0
  121. package/.framework/mcp/tools/cil/suggest.ts +186 -0
  122. package/.framework/mcp/tools/cil/validate.ts +1369 -0
  123. package/.framework/mcp/tools/kb/delete.ts +139 -0
  124. package/.framework/mcp/tools/kb/index.ts +52 -0
  125. package/.framework/mcp/tools/kb/ingest.ts +84 -0
  126. package/.framework/mcp/tools/kb/list-types.ts +102 -0
  127. package/.framework/mcp/tools/kb/publish.ts +100 -0
  128. package/.framework/mcp/tools/kb/reindex.ts +86 -0
  129. package/.framework/mcp/tools/kb/revoke.ts +85 -0
  130. package/.framework/mcp/tools/kb/search.ts +101 -0
  131. package/.framework/mcp/tools/kb/upsert.ts +125 -0
  132. package/.framework/mcp/tools/observability.ts +510 -0
  133. package/.framework/migrations/001_core_schema.sql +15 -0
  134. package/.framework/migrations/002_core_rls_and_functions.sql +15 -0
  135. package/.framework/migrations/003_core_system_data.sql +16 -1
  136. package/.framework/migrations/004_core_rpc_and_broker.sql +17 -1
  137. package/.framework/migrations/005_core_observability.sql +17 -1
  138. package/.framework/migrations/006_security_hardening_rls.sql +15 -0
  139. package/.framework/migrations/007_kb_article_list.sql +17 -1
  140. package/.framework/migrations/008_items_target.sql +15 -0
  141. package/.framework/migrations/009_roles_slug_unique.sql +15 -0
  142. package/.framework/migrations/010_auth_user_trigger.sql +15 -0
  143. package/.framework/migrations/011_spine_system_account.sql +16 -0
  144. package/.framework/migrations/012_service_role_grants.sql +16 -0
  145. package/.framework/migrations/013_get_account_apps_role_filter.sql +15 -0
  146. package/.framework/migrations/014_apps_required_roles.sql +16 -0
  147. package/.framework/migrations/015_types_kind_slug_unique.sql +20 -0
  148. package/.framework/migrations/016_cil_types.sql +281 -0
  149. package/.framework/migrations/017_cil_stable_ids.sql +37 -0
  150. package/.framework/migrations/018_runtime_tables.sql +129 -0
  151. package/.framework/migrations/019_agent_prompt_columns.sql +56 -0
  152. package/.framework/migrations/020_schema_reconciliation.sql +67 -0
  153. package/.framework/migrations/021_kb_embedding_lifecycle.sql +31 -0
  154. package/.framework/migrations/032_cil_status_migration.sql +73 -0
  155. package/.framework/migrations/033_cil_versioning.sql +73 -0
  156. package/.framework/migrations/034_match_embeddings_knn.sql +115 -0
  157. package/.framework/migrations/035_rate_limits.sql +55 -0
  158. package/.framework/migrations/036_demo_record_flags.sql +72 -0
  159. package/.framework/migrations/037_kb_types_canonical.sql +244 -0
  160. package/.framework/migrations/038_drop_demo_record_flags.sql +38 -0
  161. package/.framework/migrations/042_registration_hierarchy.sql +309 -0
  162. package/.framework/migrations/043_policy_actor_cleanup.sql +39 -0
  163. package/.framework/migrations/044_match_embeddings_filtered_knn.sql +151 -0
  164. package/.framework/migrations/045_security_critical_rls.sql +139 -0
  165. package/.framework/migrations/046_security_missing_rls.sql +79 -0
  166. package/.framework/migrations/047_people_updated_by.sql +27 -0
  167. package/.framework/migrations/048_watcher_last_seen_at.sql +24 -0
  168. package/.framework/migrations/049_security_blocklist.sql +148 -0
  169. package/.framework/migrations/050_notifications.sql +49 -0
  170. package/.framework/migrations/051_message_agent_fields.sql +74 -0
  171. package/.framework/migrations/052_registration_security.sql +421 -0
  172. package/.framework/migrations/053_builtin_action_seeds.sql +66 -0
  173. package/.framework/migrations/054_kb_ingest_lease.sql +157 -0
  174. package/.framework/migrations/055_type_id_drift_reconciliation.sql +33 -0
  175. package/.framework/migrations/056_slug_column_reconciliation.sql +38 -0
  176. package/.framework/migrations/057_pipelines_slug_identity.sql +14 -0
  177. package/.framework/migrations/058_account_data_field_declarations.sql +53 -0
  178. package/.framework/migrations/059_registration_placement_fixes.sql +269 -0
  179. package/.framework/migrations/060_account_person_record_permissions.sql +38 -0
  180. package/.framework/migrations/061_fk_dedup.sql +32 -0
  181. package/.framework/public/spine-logo.svg +10 -3
  182. package/.framework/scripts/app-install-cli.ts +190 -93
  183. package/.framework/scripts/assemble-frontend.sh +127 -44
  184. package/.framework/scripts/assemble-functions.sh +128 -30
  185. package/.framework/scripts/boundary-check.sh +6 -2
  186. package/.framework/scripts/build-manifest.sh +24 -68
  187. package/.framework/scripts/check-core-integrity.sh +15 -76
  188. package/.framework/scripts/checksum-manifest.mts +343 -0
  189. package/.framework/scripts/cil-migrate-markers.ts +403 -0
  190. package/.framework/scripts/fix-cil-markers.mjs +176 -0
  191. package/.framework/scripts/verify-integrity.sh +21 -57
  192. package/.framework/scripts/verify-manifest.mts +305 -0
  193. package/.framework/seeds/types/funnel_signal_use.json +74 -21
  194. package/.framework/seeds/types/spine_instance.json +73 -21
  195. package/.framework/src/App.tsx +118 -72
  196. package/.framework/src/apps/admin/index.tsx +855 -58
  197. package/.framework/src/components/AppWrapper.tsx +43 -45
  198. package/.framework/src/components/CustomAppLoader.tsx +160 -116
  199. package/.framework/src/components/admin/AdminListPage.tsx +46 -44
  200. package/.framework/src/components/admin/AdminSidebar.tsx +57 -47
  201. package/.framework/src/components/admin/AdminStatsCard.tsx +46 -44
  202. package/.framework/src/components/admin/PageInstructions.tsx +98 -2
  203. package/.framework/src/components/admin/SortableTableHeader.tsx +44 -45
  204. package/.framework/src/components/app-shell/GenericAppShell.tsx +64 -65
  205. package/.framework/src/components/app-shell/GenericDetailPage.tsx +44 -47
  206. package/.framework/src/components/app-shell/GenericListPage.tsx +45 -49
  207. package/.framework/src/components/app-sidebar.tsx +42 -45
  208. package/.framework/src/components/auth/ProtectedRoute.tsx +48 -41
  209. package/.framework/src/components/auth/TurnstileWidget.tsx +27 -0
  210. package/.framework/src/components/kb/KBArticleEditor.tsx +231 -22
  211. package/.framework/src/components/kb/KBArticleList.tsx +251 -10
  212. package/.framework/src/components/layout/AppShell.tsx +51 -45
  213. package/.framework/src/components/layout/Header.tsx +42 -43
  214. package/.framework/src/components/layout/Layout.tsx +46 -46
  215. package/.framework/src/components/layout/Sidebar.tsx +196 -96
  216. package/.framework/src/components/runtime/DataDetailHeader.tsx +55 -30
  217. package/.framework/src/components/runtime/DataDetailPage.tsx +31 -269
  218. package/.framework/src/components/runtime/DataFilters.tsx +49 -74
  219. package/.framework/src/components/runtime/DataHeader.tsx +53 -32
  220. package/.framework/src/components/runtime/DataListPage.tsx +51 -155
  221. package/.framework/src/components/runtime/DataStats.tsx +66 -44
  222. package/.framework/src/components/runtime/DataTable.tsx +49 -149
  223. package/.framework/src/components/runtime/SchemaDetailForm.tsx +45 -75
  224. package/.framework/src/components/runtime/index.ts +119 -16
  225. package/.framework/src/components/search-form.tsx +46 -29
  226. package/.framework/src/components/shared/AgentView.tsx +65 -48
  227. package/.framework/src/components/shared/FieldRenderer.tsx +144 -50
  228. package/.framework/src/components/shared/InviteManager.tsx +207 -97
  229. package/.framework/src/components/shared/MemberList.tsx +97 -0
  230. package/.framework/src/components/shared/SchemaFields.tsx +107 -86
  231. package/.framework/src/components/ui/DataTable.tsx +80 -68
  232. package/.framework/src/components/ui/Form.tsx +157 -146
  233. package/.framework/src/components/ui/ItemCard.tsx +74 -67
  234. package/.framework/src/components/ui/ItemListView.tsx +73 -68
  235. package/.framework/src/components/ui/LoadingSpinner.tsx +43 -48
  236. package/.framework/src/components/ui/Modal.tsx +78 -68
  237. package/.framework/src/components/ui/RichTextEditor.tsx +89 -69
  238. package/.framework/src/components/ui/accordion.tsx +85 -86
  239. package/.framework/src/components/ui/alert-dialog.tsx +197 -184
  240. package/.framework/src/components/ui/alert.tsx +105 -94
  241. package/.framework/src/components/ui/aspect-ratio.tsx +57 -43
  242. package/.framework/src/components/ui/avatar.tsx +122 -107
  243. package/.framework/src/components/ui/badge.tsx +60 -55
  244. package/.framework/src/components/ui/breadcrumb.tsx +127 -128
  245. package/.framework/src/components/ui/button-group.tsx +86 -72
  246. package/.framework/src/components/ui/button.tsx +71 -50
  247. package/.framework/src/components/ui/calendar.tsx +72 -57
  248. package/.framework/src/components/ui/card.tsx +136 -129
  249. package/.framework/src/components/ui/carousel.tsx +226 -107
  250. package/.framework/src/components/ui/chart.tsx +252 -147
  251. package/.framework/src/components/ui/checkbox.tsx +43 -44
  252. package/.framework/src/components/ui/collapsible.tsx +86 -71
  253. package/.framework/src/components/ui/combobox.tsx +282 -249
  254. package/.framework/src/components/ui/command.tsx +158 -146
  255. package/.framework/src/components/ui/context-menu.tsx +239 -240
  256. package/.framework/src/components/ui/dialog.tsx +174 -157
  257. package/.framework/src/components/ui/direction.tsx +72 -56
  258. package/.framework/src/components/ui/drawer.tsx +168 -169
  259. package/.framework/src/components/ui/dropdown-menu.tsx +254 -223
  260. package/.framework/src/components/ui/empty.tsx +131 -120
  261. package/.framework/src/components/ui/field.tsx +205 -173
  262. package/.framework/src/components/ui/hover-card.tsx +71 -72
  263. package/.framework/src/components/ui/input-group.tsx +148 -133
  264. package/.framework/src/components/ui/input-otp.tsx +99 -86
  265. package/.framework/src/components/ui/input.tsx +43 -41
  266. package/.framework/src/components/ui/item.tsx +207 -185
  267. package/.framework/src/components/ui/kbd.tsx +57 -56
  268. package/.framework/src/components/ui/label.tsx +45 -41
  269. package/.framework/src/components/ui/menubar.tsx +268 -236
  270. package/.framework/src/components/ui/native-select.tsx +85 -86
  271. package/.framework/src/components/ui/navigation-menu.tsx +166 -156
  272. package/.framework/src/components/ui/pagination.tsx +131 -141
  273. package/.framework/src/components/ui/popover.tsx +125 -128
  274. package/.framework/src/components/ui/progress.tsx +47 -44
  275. package/.framework/src/components/ui/radio-group.tsx +60 -55
  276. package/.framework/src/components/ui/resizable.tsx +85 -72
  277. package/.framework/src/components/ui/scroll-area.tsx +57 -58
  278. package/.framework/src/components/ui/select.tsx +182 -169
  279. package/.framework/src/components/ui/separator.tsx +42 -43
  280. package/.framework/src/components/ui/sheet.tsx +170 -159
  281. package/.framework/src/components/ui/sidebar.tsx +395 -398
  282. package/.framework/src/components/ui/skeleton.tsx +42 -43
  283. package/.framework/src/components/ui/slider.tsx +61 -44
  284. package/.framework/src/components/ui/sonner.tsx +44 -44
  285. package/.framework/src/components/ui/spinner.tsx +47 -42
  286. package/.framework/src/components/ui/switch.tsx +56 -42
  287. package/.framework/src/components/ui/table-primitives.tsx +272 -132
  288. package/.framework/src/components/ui/table.tsx +140 -142
  289. package/.framework/src/components/ui/tabs.tsx +110 -93
  290. package/.framework/src/components/ui/textarea.tsx +43 -44
  291. package/.framework/src/components/ui/toggle-group.tsx +87 -71
  292. package/.framework/src/components/ui/toggle.tsx +56 -56
  293. package/.framework/src/components/ui/tooltip.tsx +99 -85
  294. package/.framework/src/contexts/AppContext.tsx +166 -114
  295. package/.framework/src/contexts/AuthContext.tsx +232 -137
  296. package/.framework/src/hooks/use-mobile.ts +43 -44
  297. package/.framework/src/hooks/useApi.ts +185 -172
  298. package/.framework/src/hooks/useAppPath.ts +31 -29
  299. package/.framework/src/hooks/useApps.ts +77 -42
  300. package/.framework/src/hooks/useEntityList.ts +66 -29
  301. package/.framework/src/hooks/useEntityRecord.ts +76 -32
  302. package/.framework/src/hooks/useForm.ts +62 -30
  303. package/.framework/src/hooks/useListSchema.ts +60 -43
  304. package/.framework/src/hooks/useSchemaRecord.ts +115 -95
  305. package/.framework/src/lib/api.ts +258 -134
  306. package/.framework/src/lib/registration.ts +181 -0
  307. package/.framework/src/lib/supabase.ts +118 -81
  308. package/.framework/src/lib/utils.ts +266 -266
  309. package/.framework/src/main.tsx +43 -44
  310. package/.framework/src/pages/DashboardPage.tsx +44 -43
  311. package/.framework/src/pages/NoAppPage.tsx +50 -0
  312. package/.framework/src/pages/NotFoundPage.tsx +32 -29
  313. package/.framework/src/pages/SetupPage.tsx +34 -33
  314. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +69 -58
  315. package/.framework/src/pages/admin/AIAgentsPage.tsx +43 -40
  316. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +63 -55
  317. package/.framework/src/pages/admin/APIKeysPage.tsx +44 -42
  318. package/.framework/src/pages/admin/AlertsConfigPage.tsx +117 -94
  319. package/.framework/src/pages/admin/AppDetailPage.tsx +48 -42
  320. package/.framework/src/pages/admin/AppsPage.tsx +45 -42
  321. package/.framework/src/pages/admin/DeliveriesPage.tsx +49 -17
  322. package/.framework/src/pages/admin/DesignedPage.tsx +31 -30
  323. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +58 -0
  324. package/.framework/src/pages/admin/EmbeddingsPage.tsx +49 -0
  325. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +31 -29
  326. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +46 -29
  327. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -29
  328. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +68 -55
  329. package/.framework/src/pages/admin/IntegrationsPage.tsx +45 -42
  330. package/.framework/src/pages/admin/InvitesPage.tsx +60 -44
  331. package/.framework/src/pages/admin/LogsPage.tsx +87 -137
  332. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +31 -29
  333. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +150 -89
  334. package/.framework/src/pages/admin/PipelineDetailPage.tsx +77 -104
  335. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +44 -43
  336. package/.framework/src/pages/admin/PipelinesPage.tsx +59 -145
  337. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +53 -42
  338. package/.framework/src/pages/admin/PromptConfigsPage.tsx +47 -117
  339. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +31 -44
  340. package/.framework/src/pages/admin/RoleDetailPage.tsx +49 -92
  341. package/.framework/src/pages/admin/RolesPage.tsx +47 -164
  342. package/.framework/src/pages/admin/SecurityPage.tsx +310 -0
  343. package/.framework/src/pages/admin/SelectTestPage.tsx +30 -32
  344. package/.framework/src/pages/admin/ShadcnTestPage.tsx +37 -44
  345. package/.framework/src/pages/admin/SimpleDashboard.tsx +32 -44
  346. package/.framework/src/pages/admin/TestRunDetailPage.tsx +97 -44
  347. package/.framework/src/pages/admin/TestingDashboard.tsx +126 -71
  348. package/.framework/src/pages/admin/TimerDetailPage.tsx +67 -55
  349. package/.framework/src/pages/admin/TimersPage.tsx +44 -42
  350. package/.framework/src/pages/admin/TriggerDetailPage.tsx +47 -42
  351. package/.framework/src/pages/admin/TriggersPage.tsx +58 -57
  352. package/.framework/src/pages/admin/TypeDetailPage.tsx +91 -55
  353. package/.framework/src/pages/admin/TypesPage.tsx +45 -42
  354. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +258 -128
  355. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +127 -123
  356. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +308 -235
  357. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +159 -110
  358. package/.framework/src/pages/admin/cil/CILFileSelectPage.tsx +432 -0
  359. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +200 -178
  360. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +286 -239
  361. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +211 -90
  362. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +156 -143
  363. package/.framework/src/pages/admin/cil/CILSuggestionsPage.tsx +379 -0
  364. package/.framework/src/pages/admin/cil/CILVersionsPage.tsx +337 -0
  365. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +219 -214
  366. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  367. package/.framework/src/pages/auth/AuthConfirmPage.tsx +135 -65
  368. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +39 -31
  369. package/.framework/src/pages/auth/LoginPage.tsx +38 -32
  370. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -28
  371. package/.framework/src/pages/auth/RegisterPage.tsx +429 -155
  372. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -30
  373. package/.framework/src/pages/spine-framework/CLIPage.tsx +32 -30
  374. package/.framework/src/types/auth.ts +101 -93
  375. package/.framework/src/types/types.ts +283 -262
  376. package/.framework/templates/project.json +87 -0
  377. package/.framework/templates/spine.config.neutral.json +7 -0
  378. package/.framework/tests/api/api-surface.test.ts +381 -0
  379. package/.framework/tests/fixtures/seed.ts +427 -0
  380. package/.framework/tests/fixtures/teardown.ts +228 -0
  381. package/.framework/tests/reporter.ts +351 -0
  382. package/.framework/tests/setup.ts +135 -0
  383. package/.framework/tests/ui/ui-sweep.ts +278 -0
  384. package/.framework/tests/unit/admin-data-logger.test.ts +310 -0
  385. package/.framework/tests/unit/agent-runner.test.ts +2389 -0
  386. package/.framework/tests/unit/ai-interactions.test.ts +81 -0
  387. package/.framework/tests/unit/app-manifest.test.ts +359 -0
  388. package/.framework/tests/unit/assembly-scripts.test.ts +172 -0
  389. package/.framework/tests/unit/cil-determinism.test.ts +85 -0
  390. package/.framework/tests/unit/core-isolation.test.ts +153 -0
  391. package/.framework/tests/unit/funnel-signals.test.ts +135 -0
  392. package/.framework/tests/unit/install-app.test.ts +1759 -0
  393. package/.framework/tests/unit/invites-security.test.ts +572 -0
  394. package/.framework/tests/unit/kb-ingest-lease.test.ts +287 -0
  395. package/.framework/tests/unit/kb-search.test.ts +119 -0
  396. package/.framework/tests/unit/migrate.test.ts +509 -0
  397. package/.framework/tests/unit/notify.test.ts +136 -0
  398. package/.framework/tests/unit/permissions.test.ts +269 -0
  399. package/.framework/tests/unit/pipeline-runner.test.ts +755 -0
  400. package/.framework/tests/unit/principal-cache.test.ts +138 -0
  401. package/.framework/tests/unit/retrieval-golden.test.ts +520 -0
  402. package/.framework/tests/unit/runtime-isolation.test.ts +642 -0
  403. package/.framework/tests/unit/schema-utils.test.ts +225 -0
  404. package/.framework/tests/unit/trigger-engine.test.ts +513 -0
  405. package/.framework/tests/unit/validate-app.test.ts +527 -0
  406. package/README.md +34 -3
  407. package/bin/spine-framework.cjs +1 -0
  408. package/config/tailwind.config.cjs +7 -0
  409. package/config/tsconfig.functions.json +42 -0
  410. package/config/tsconfig.json +37 -9
  411. package/config/vite.config.ts +31 -2
  412. package/netlify.toml +31 -3
  413. package/package.json +46 -29
  414. package/scripts/assemble-frontend.sh +127 -44
  415. package/scripts/assemble-functions.sh +127 -29
  416. package/scripts/boundary-check.sh +6 -2
  417. package/scripts/build-manifest.sh +24 -68
  418. package/scripts/checksum-manifest.mts +345 -0
  419. package/scripts/dev-clean.sh +86 -0
  420. package/scripts/dev-netlify.sh +49 -0
  421. package/scripts/netlify-dev-wrapper.sh +2 -0
  422. package/scripts/verify-install.ts +358 -0
  423. package/scripts/verify-integrity.sh +21 -57
  424. package/scripts/verify-manifest.mts +305 -0
  425. package/templates/project/gitignore +45 -0
  426. package/{config → templates/project}/package-lock.json +22335 -9366
  427. package/vitest.config.ts +46 -0
  428. package/.framework/public/spine.config.json +0 -91
  429. package/config/deno.lock +0 -108
  430. package/config/tsconfig.node.tsbuildinfo +0 -1
  431. package/config/tsconfig.tsbuildinfo +0 -1
  432. package/config/vite.config.d.ts +0 -2
  433. package/custom/.devin/AGENT.md +0 -357
  434. package/custom/.devin/skills/install-app/SKILL.md +0 -39
  435. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +0 -39
  436. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +0 -36
  437. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +0 -46
  438. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +0 -28
  439. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +0 -38
  440. package/custom/.devin/workflows/app-install.yaml +0 -83
  441. package/custom/README.md +0 -79
  442. package/docs/README.md +0 -15
  443. package/scripts/app-install-cli.ts +0 -286
  444. package/scripts/check-core-integrity.sh +0 -82
  445. package/scripts/ingest-chunks.cjs +0 -202
  446. package/scripts/kb-chunk-parser.cjs +0 -312
  447. package/scripts/kb-chunk-parser.ts +0 -330
  448. package/scripts/kb-ingest-docs.ts +0 -203
  449. package/scripts/load-test-app-install.ts +0 -484
  450. package/scripts/test-login.js +0 -33
  451. package/spine.config.json +0 -91
@@ -1,25 +1,177 @@
1
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:module_header:module_header" type="module_header" identity="module_header" parent="~/.framework/functions/_shared/kb.ts"]
2
+ // CIL[DOC-MACRO purpose="Shared knowledge base helpers for generating embeddings, chunking text, and searching vector embeddings."]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Provides shared utilities for KB ingestion, revocation, reindexing, and semantic search."]
4
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
5
+ // CIL[DOC-CONTRACT inputs="[]" outputs="[]" errors="[]" side_effects="[]"]
6
+ // CIL[DOC-MICRO logic_summary="Module header: imports database, middleware, permissions, and LLM helpers." notable_decisions="[]"]
7
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
8
+ // CIL[DOC-STABILITY stability="stable"]
9
+ // CIL[DOC-AUDIENCE audience="internal"]
10
+
11
+ import { createHash } from 'crypto'
1
12
  import { adminDb } from './db'
2
13
  import type { CoreContext } from './middleware'
3
14
  import { sanitizeRecordData } from './permissions'
4
15
  import { callOpenAIEmbeddings } from './llm'
16
+ // admin-data is imported lazily — a static edge closes the cycle
17
+ // middleware → security → rate-limit → funnel → trigger-engine →
18
+ // pipeline-runner → kb → admin-data → middleware, leaving createHandler
19
+ // undefined during module evaluation.
20
+ const adminUpdate = async (ctx: any, body: any) =>
21
+ (await import('../admin-data')).update(ctx, body)
22
+ // CIL[DOC-VECTOR type="context" content="purpose='Shared knowledge base helpers for generating embeddings, chunking text, and searching vector embeddings.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
23
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
24
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
25
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
26
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
27
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
28
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:module_header:module_header"]
5
29
 
30
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:EMBEDDING_MODEL:constant" type="constant" identity="EMBEDDING_MODEL" parent="~/.framework/functions/_shared/kb.ts"]
31
+ // CIL[DOC-MACRO purpose="OpenAI embedding model identifier used for generating text embeddings."]
32
+ // CIL[DOC-RESPONSIBILITY responsibility="Defines the default embedding model string."]
33
+ // CIL[DOC-STABILITY stability="internal"]
34
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
35
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
36
+ // CIL[DOC-CONTRACT inputs="[]" outputs="string" errors="[]" side_effects="[]"]
37
+ // CIL[DOC-MICRO logic_summary="Constant string 'text-embedding-3-small'." notable_decisions="[]"]
6
38
  const EMBEDDING_MODEL = 'text-embedding-3-small'
39
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:EMBEDDING_MODEL:constant"]
40
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:DEFAULT_CHUNK_SIZE:constant" type="constant" identity="DEFAULT_CHUNK_SIZE" parent="~/.framework/functions/_shared/kb.ts"]
41
+ // CIL[DOC-VECTOR type="context" content="purpose='OpenAI embedding model identifier used for generating text embeddings.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
42
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
43
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
44
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
45
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
46
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
47
+ // CIL[DOC-MACRO purpose="Default maximum number of characters per KB text chunk."]
48
+ // CIL[DOC-RESPONSIBILITY responsibility="Defines the default chunk size for text splitting."]
49
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
50
+ // CIL[DOC-STABILITY stability="internal"]
51
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
52
+ // CIL[DOC-CONTRACT inputs="[]" outputs="number" errors="[]" side_effects="[]"]
53
+ // CIL[DOC-MICRO logic_summary="Constant number 800." notable_decisions="[]"]
7
54
  const DEFAULT_CHUNK_SIZE = 800
55
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:DEFAULT_CHUNK_SIZE:constant"]
56
+ // CIL[DOC-CONTEXT module="unknown" layer="unknown" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
57
+ // CIL[DOC-VECTOR type="context" content="purpose='Default maximum number of characters per KB text chunk.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
58
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
59
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
60
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
61
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
62
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
63
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:DEFAULT_CHUNK_OVERLAP:constant" type="constant" identity="DEFAULT_CHUNK_OVERLAP" parent="~/.framework/functions/_shared/kb.ts"]
64
+ // CIL[DOC-MACRO purpose="Default overlap in characters between consecutive text chunks."]
65
+ // CIL[DOC-RESPONSIBILITY responsibility="Defines the default chunk overlap for sliding window chunking."]
66
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
67
+ // CIL[DOC-STABILITY stability="internal"]
68
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
69
+ // CIL[DOC-CONTRACT inputs="[]" outputs="number" errors="[]" side_effects="[]"]
70
+ // CIL[DOC-MICRO logic_summary="Constant number 100." notable_decisions="[]"]
8
71
  const DEFAULT_CHUNK_OVERLAP = 100
72
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:DEFAULT_CHUNK_OVERLAP:constant"]
73
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:SIMILARITY_THRESHOLD:constant" type="constant" identity="SIMILARITY_THRESHOLD" parent="~/.framework/functions/_shared/kb.ts"]
74
+ // CIL[DOC-VECTOR type="context" content="purpose='Default overlap in characters between consecutive text chunks.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
75
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
76
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
77
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
78
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
79
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
80
+ // CIL[DOC-MACRO purpose="Default cosine-similarity threshold for vector search matches."]
81
+ // CIL[DOC-RESPONSIBILITY responsibility="Defines the minimum similarity score used to filter search results."]
82
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
83
+ // CIL[DOC-CONTRACT inputs="[]" outputs="number" errors="[]" side_effects="[]"]
84
+ // CIL[DOC-MICRO logic_summary="Constant number 0.15." notable_decisions="[]"]
85
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
86
+ // CIL[DOC-STABILITY stability="stable"]
9
87
  const SIMILARITY_THRESHOLD = 0.15
88
+ // CIL[DOC-VECTOR type="context" content="purpose='Default cosine-similarity threshold for vector search matches.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
89
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
90
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
91
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
92
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
93
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
94
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:SIMILARITY_THRESHOLD:constant"]
95
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:DEFAULT_MATCH_COUNT:constant" type="constant" identity="DEFAULT_MATCH_COUNT" parent="~/.framework/functions/_shared/kb.ts"]
96
+ // CIL[DOC-MACRO purpose="Default number of vector search results to consider before pagination."]
97
+ // CIL[DOC-RESPONSIBILITY responsibility="Defines the default match count used in semantic queries."]
98
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
99
+ // CIL[DOC-CONTRACT inputs="[]" outputs="number" errors="[]" side_effects="[]"]
100
+ // CIL[DOC-MICRO logic_summary="Constant number 10." notable_decisions="[]"]
101
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
102
+ // CIL[DOC-STABILITY stability="stable"]
10
103
  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',
104
+ // CIL[DOC-VECTOR type="context" content="purpose='Default number of vector search results to consider before pagination.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
105
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
106
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
107
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
108
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
109
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
110
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:DEFAULT_MATCH_COUNT:constant"]
111
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:resolveSecurityLevel:function" type="function" identity="resolveSecurityLevel" parent="~/.framework/functions/_shared/kb.ts"]
112
+ // CIL[DOC-MACRO purpose="Resolve the vector security level for a KB item type from its declared design_schema."]
113
+ // CIL[DOC-RESPONSIBILITY responsibility="Reads functionality.kb.security_level off a type's design_schema; falls back to 'internal' (deny-by-default) when undeclared."]
114
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
115
+ // CIL[DOC-CONTRACT inputs="designSchema: any — a type's design_schema (or a record's stamped copy)" outputs="string — 'restricted' | 'internal' | 'external'" errors="[]" side_effects="[]"]
116
+ // CIL[DOC-MICRO logic_summary="Returns functionality.kb.security_level when it is a non-empty string; 'internal' otherwise." notable_decisions="Security level is declared per KB type in seed design_schema (functionality.kb.security_level); undeclared types deny by default to 'internal' — no hardcoded slug map."]
117
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
118
+ // CIL[DOC-STABILITY stability="internal"]
119
+ export function resolveSecurityLevel(designSchema: any): string {
120
+ const declared = designSchema?.functionality?.kb?.security_level
121
+ return typeof declared === 'string' && declared ? declared : 'internal'
16
122
  }
123
+ // CIL[DOC-VECTOR type="context" content="purpose='Resolve the vector security level for a KB item type from its declared design_schema.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
124
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
125
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
126
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
127
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
128
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
129
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:resolveSecurityLevel:function"]
130
+
17
131
 
132
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:generateEmbedding:function" type="function" identity="generateEmbedding" parent="~/.framework/functions/_shared/kb.ts"]
133
+ // CIL[DOC-MACRO purpose="Generates an embedding for the provided text using an external API."]
134
+ // CIL[DOC-RESPONSIBILITY responsibility="Transforms textual data into a numerical format suitable for machine learning tasks."]
135
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
136
+ // CIL[DOC-CONTRACT inputs="text: string (required), ctx: CoreContext (optional)" outputs="Promise of number array" errors="[]" side_effects="calls OpenAI API for embeddings"]
137
+ // CIL[DOC-MICRO logic_summary="Calls callOpenAIEmbeddings and returns the first embedding or an empty array." notable_decisions="Returns empty array if embedding not found."]
138
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
139
+ // CIL[DOC-STABILITY stability="internal"]
140
+ /**
141
+ * Generates an embedding for the provided text using an external API. This function is essential for transforming textual data into a numerical format suitable for machine learning tasks.
142
+ *
143
+ * @macro Async function that retrieves embeddings from an external service.
144
+ * @micro Calls an external API to obtain the embedding for the given text.
145
+ * @inputSpec
146
+ * text: string (required) — the input text to generate an embedding for.
147
+ * ctx: CoreContext (optional) — optional context for the API call.
148
+ * @outputSpec Promise<number[]> — an array of numbers representing the embedding, or an empty array if not found.
149
+ * @sideEffects calls OpenAI API for embeddings
150
+ * @stability internal
151
+ * @calls callOpenAIEmbeddings
152
+ * @cil-generated
153
+ */
18
154
  async function generateEmbedding(text: string, ctx?: CoreContext): Promise<number[]> {
19
155
  const json = await callOpenAIEmbeddings(ctx, [text], EMBEDDING_MODEL)
20
156
  return json.data?.[0]?.embedding || []
21
157
  }
158
+ // CIL[DOC-VECTOR type="context" content="purpose='Generates an embedding for the provided text using an external API.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
159
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
160
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
161
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
162
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
163
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
164
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:generateEmbedding:function"]
165
+
22
166
 
167
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:chunkText:function" type="function" identity="chunkText" parent="~/.framework/functions/_shared/kb.ts"]
168
+ // CIL[DOC-MACRO purpose="Splits text into overlapping chunks of a given size."]
169
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements sliding-window chunking for long text fields."]
170
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
171
+ // CIL[DOC-CONTRACT inputs="text: string, chunkSize: number, overlap: number" outputs="string array" errors="[]" side_effects="[]"]
172
+ // CIL[DOC-STABILITY stability="internal"]
173
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
174
+ // CIL[DOC-MICRO logic_summary="Iterates through text with a stride of chunkSize minus overlap and accumulates slices." notable_decisions="[]"]
23
175
  function chunkText(text: string, chunkSize: number, overlap: number): string[] {
24
176
  if (!text) return []
25
177
  if (text.length <= chunkSize) return [text]
@@ -35,7 +187,23 @@ function chunkText(text: string, chunkSize: number, overlap: number): string[] {
35
187
  }
36
188
  return chunks
37
189
  }
190
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:chunkText:function"]
38
191
 
192
+
193
+ // CIL[DOC-VECTOR type="context" content="purpose='Splits text into overlapping chunks of a given size.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
194
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
195
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
196
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
197
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
198
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
199
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:getFieldValue:function" type="function" identity="getFieldValue" parent="~/.framework/functions/_shared/kb.ts"]
200
+ // CIL[DOC-MACRO purpose="Retrieves a flattened string value from a record or its data object."]
201
+ // CIL[DOC-RESPONSIBILITY responsibility="Normalizes record fields into a searchable text representation."]
202
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
203
+ // CIL[DOC-CONTRACT inputs="record: any, field: string" outputs="string" errors="[]" side_effects="[]"]
204
+ // CIL[DOC-MICRO logic_summary="Checks record[field] then record.data[field]; joins arrays with spaces and coerces to string." notable_decisions="[]"]
205
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
206
+ // CIL[DOC-STABILITY stability="internal"]
39
207
  function getFieldValue(record: any, field: string): string {
40
208
  if (record[field] !== undefined && record[field] !== null) {
41
209
  const value = record[field]
@@ -50,14 +218,49 @@ function getFieldValue(record: any, field: string): string {
50
218
  }
51
219
  return ''
52
220
  }
221
+ // CIL[DOC-VECTOR type="context" content="purpose='Retrieves a flattened string value from a record or its data object.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
222
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
223
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
224
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
225
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
226
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
227
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:getFieldValue:function"]
228
+
53
229
 
230
+ // CIL[DOC-STABILITY stability="internal"]
231
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
232
+ // CIL[DOC-CONTEXT module="unknown" layer="unknown" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
233
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:getRawFieldValue:function" type="function" identity="getRawFieldValue" parent="~/.framework/functions/_shared/kb.ts"]
234
+ // CIL[DOC-MACRO purpose="Retrieves the raw value of a record field without string coercion."]
235
+ // CIL[DOC-RESPONSIBILITY responsibility="Returns the original value from record or record.data."]
236
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
237
+ // CIL[DOC-CONTRACT inputs="record: any, field: string" outputs="any" errors="[]" side_effects="[]"]
238
+ // CIL[DOC-MICRO logic_summary="Checks record[field] then record.data[field]; returns undefined if not present." notable_decisions="[]"]
239
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
240
+ // CIL[DOC-STABILITY stability="internal"]
54
241
  function getRawFieldValue(record: any, field: string): any {
55
242
  if (record[field] !== undefined && record[field] !== null) return record[field]
56
243
  const data = record.data || {}
57
244
  if (data[field] !== undefined && data[field] !== null) return data[field]
58
245
  return undefined
59
246
  }
247
+ // CIL[DOC-VECTOR type="context" content="purpose='Retrieves the raw value of a record field without string coercion.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
248
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
249
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
250
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
251
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
252
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
253
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:getRawFieldValue:function"]
254
+
60
255
 
256
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:matchesFilters:function" type="function" identity="matchesFilters" parent="~/.framework/functions/_shared/kb.ts"]
257
+ // CIL[DOC-MACRO purpose="Checks whether a record matches a set of filter values."]
258
+ // CIL[DOC-RESPONSIBILITY responsibility="Filters vector search results by field values."]
259
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
260
+ // CIL[DOC-CONTRACT inputs="record: any, filters: Record<string, any>" outputs="boolean" errors="[]" side_effects="[]"]
261
+ // CIL[DOC-MICRO logic_summary="Compares expected values to actual values; returns false if any expected value is absent." notable_decisions="[]"]
262
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
263
+ // CIL[DOC-STABILITY stability="internal"]
61
264
  function matchesFilters(record: any, filters: Record<string, any>): boolean {
62
265
  if (!filters || Object.keys(filters).length === 0) return true
63
266
 
@@ -73,25 +276,358 @@ function matchesFilters(record: any, filters: Record<string, any>): boolean {
73
276
 
74
277
  return true
75
278
  }
279
+ // CIL[DOC-VECTOR type="context" content="purpose='Checks whether a record matches a set of filter values.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
280
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
281
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
282
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
283
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
284
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
285
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:matchesFilters:function"]
76
286
 
287
+
288
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:ensurePlatformAccountId:function" type="function" identity="ensurePlatformAccountId" parent="~/.framework/functions/_shared/kb.ts"]
289
+ // CIL[DOC-MACRO purpose="Resolves the global spine-system account ID from the accounts table."]
290
+ // CIL[DOC-RESPONSIBILITY responsibility="Provides the platform account ID for cross-account KB search."]
291
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
292
+ // CIL[DOC-CONTRACT inputs="[]" outputs="string or null" errors="[]" side_effects="[]"]
293
+ // CIL[DOC-MICRO logic_summary="Queries the accounts table for slug spine-system and returns the id or null." notable_decisions="[]"]
294
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
295
+ // CIL[DOC-STABILITY stability="internal"]
77
296
  async function ensurePlatformAccountId(): Promise<string | null> {
78
- const { data } = await adminDb
297
+ const { data, error } = await adminDb
79
298
  .from('accounts')
80
299
  .select('id')
81
300
  .eq('slug', 'spine-system')
82
301
  .maybeSingle()
302
+ if (error) throw new Error(`platform_account_lookup_failed: ${error.message}`)
83
303
  return data?.id ?? null
84
304
  }
305
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:ensurePlatformAccountId:function"]
306
+
307
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:knowledgeenvelope:interface" type="interface" identity="KnowledgeEnvelope" parent="~/.framework/functions/_shared/kb.ts"]
308
+ // CIL[DOC-MACRO purpose="App-level knowledge access envelope declared in manifest config.knowledge"]
309
+ // CIL[DOC-RESPONSIBILITY responsibility="Declares the maximum set of KB/platform/CIL corpora any agent in an app may reach; deny-by-default when absent"]
310
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/agent-runner.ts,.framework/functions/_shared/pipeline-runner.ts,.framework/cli/commands/install-app.ts"]
311
+ // CIL[DOC-CONTRACT inputs="apps.config.knowledge (persisted from manifest.config.knowledge)" outputs="KnowledgeEnvelope" errors="none" side_effects="none"]
312
+ // CIL[DOC-MICRO logic_summary="account gates own/descendant corpora (item_types supports trailing-* prefixes, scope self|self_and_descendants); platform.sources allowlists platform-account item types; cil requires explicit project_ids and governs every cil_chunk row" notable_decisions="missing envelope = no access; envelope is the ceiling, per-agent retrieval_policy picks may only narrow it"]
313
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
314
+ // CIL[DOC-STABILITY stability="experimental"]
315
+ export interface KnowledgeEnvelope {
316
+ /** Own-account corpus. true/object enables; object may carry
317
+ * scope ('self' | 'self_and_descendants') and item_types patterns
318
+ * (exact slug or 'prefix*'). Omitted/false = no account KB access. */
319
+ account?: boolean | { scope?: 'self' | 'self_and_descendants'; item_types?: string[] }
320
+ /** Platform (spine-system) corpus. sources = allowed item_type patterns. */
321
+ platform?: boolean | { sources?: string[] }
322
+ /** CIL corpus. Only granted when an explicit project id list resolves. */
323
+ cil?: boolean | { project_ids?: string[] }
324
+ }
325
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:knowledgeenvelope:interface"]
326
+
327
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:knowledgescope:interface" type="interface" identity="KnowledgeScope" parent="~/.framework/functions/_shared/kb.ts"]
328
+ // CIL[DOC-MACRO purpose="Resolved effective knowledge access for one request/run"]
329
+ // CIL[DOC-RESPONSIBILITY responsibility="Carries the envelope∩pick intersection: permitted account ids, per-corpus item type allowlists, CIL project ids, and out-of-envelope violations"]
330
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/kb.ts"]
331
+ // CIL[DOC-CONTRACT inputs="KnowledgeEnvelope + agent retrieval_policy pick + caller account tree" outputs="KnowledgeScope" errors="violations[] for picks outside the envelope" side_effects="none"]
332
+ // CIL[DOC-STABILITY stability="experimental"]
333
+ export interface KnowledgeScope {
334
+ allowed: boolean
335
+ deniedReason?: string
336
+ /** All account ids permitted to appear in results (self + descendants + platform when granted). */
337
+ accountIds: string[]
338
+ /** Self (+ descendants under hierarchy scope). */
339
+ selfAccountIds: string[]
340
+ platformAccountId: string | null
341
+ /** null = unrestricted within the corpus; [] = corpus disabled. */
342
+ accountItemTypes: string[] | null
343
+ platformItemTypes: string[] | null
344
+ /** null = CIL denied. Always an explicit list when enabled. */
345
+ cilProjectIds: string[] | null
346
+ /** Safe filter_item_types for match_embeddings, or null when a wildcard/unrestricted corpus exists. */
347
+ rpcItemTypes: string[] | null
348
+ /** Pick entries outside the envelope — callers must fail visibly, not clamp. */
349
+ violations: string[]
350
+ /** Set when the platform account lookup errored — callers may surface an error span while continuing with self scope. */
351
+ platformLookupError?: string
352
+ }
353
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:knowledgescope:interface"]
354
+
355
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
356
+
357
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:matchitemtype:function" type="function" identity="matchItemType" parent="~/.framework/functions/_shared/kb.ts"]
358
+ // CIL[DOC-MACRO purpose="Match an item_type against a list of exact slugs or 'prefix*' wildcard patterns"]
359
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements pattern matching for knowledge envelope item_type lists"]
360
+ // CIL[DOC-CONTRACT inputs="patterns: string[] | null (null = unrestricted, [] = match nothing), itemType" outputs="boolean" errors="none" side_effects="none"]
361
+ // CIL[DOC-STABILITY stability="experimental"]
362
+ export function matchItemType(patterns: string[] | null | undefined, itemType: string | undefined | null): boolean {
363
+ if (patterns == null) return true
364
+ if (!itemType) return false
365
+ return patterns.some((p) =>
366
+ p === '*' ? true : p.endsWith('*') ? itemType.startsWith(p.slice(0, -1)) : p === itemType
367
+ )
368
+ }
369
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:matchitemtype:function"]
370
+
371
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:knowledgerowallowed:function" type="function" identity="knowledgeRowAllowed" parent="~/.framework/functions/_shared/kb.ts"]
372
+ // CIL[DOC-MACRO purpose="Decide whether a single embeddings/items row is inside the resolved knowledge scope"]
373
+ // CIL[DOC-RESPONSIBILITY responsibility="Applies per-corpus rules: account rows match accountItemTypes, platform rows match platformItemTypes, and cil_chunk rows are governed solely by cilProjectIds regardless of corpus"]
374
+ // CIL[DOC-CONTRACT inputs="scope: KnowledgeScope, row: { account_id?, metadata? }" outputs="boolean" errors="none" side_effects="none"]
375
+ // CIL[DOC-MICRO logic_summary="cil_chunk rows require an explicit project grant even in unrestricted account corpora — this prevents the platform codebase leaking through a broad account allowlist" notable_decisions="cil_chunk is always project-gated"]
376
+ // CIL[DOC-STABILITY stability="experimental"]
377
+ export function knowledgeRowAllowed(scope: KnowledgeScope, row: { account_id?: string | null; metadata?: any }): boolean {
378
+ const meta = row.metadata || {}
379
+ if (meta.item_type === 'cil_chunk') {
380
+ return !!scope.cilProjectIds?.length && !!meta.project_id && scope.cilProjectIds.includes(meta.project_id)
381
+ }
382
+ const isPlatformRow = !!scope.platformAccountId && row.account_id === scope.platformAccountId
383
+ const isOwnRow = scope.selfAccountIds.includes(row.account_id || '')
384
+ if (isPlatformRow && !isOwnRow) return matchItemType(scope.platformItemTypes, meta.item_type)
385
+ if (isOwnRow || isPlatformRow) return matchItemType(scope.accountItemTypes, meta.item_type)
386
+ return false
387
+ }
388
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:knowledgerowallowed:function"]
389
+
390
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:resolveknowledgescope:function" type="function" identity="resolveKnowledgeScope" parent="~/.framework/functions/_shared/kb.ts"]
391
+ // CIL[DOC-MACRO purpose="Resolve the effective knowledge scope for a request/agent run from the app's installed envelope and the agent's pick"]
392
+ // CIL[DOC-RESPONSIBILITY responsibility="Server-side enforcement point: loads apps.config.knowledge, intersects it with the per-agent retrieval pick, and returns the account ids + per-corpus allowlists that callers must apply"]
393
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/agent-runner.ts,.framework/functions/_shared/pipeline-runner.ts"]
394
+ // CIL[DOC-CONTRACT inputs="ctx: { accountId?, principal?, appId? }, appRef: app slug or uuid, pick: { item_types?, project_ids? }" outputs="KnowledgeScope (allowed=false when no envelope or nothing permitted)" errors="never throws for missing config — fails closed" side_effects="DB reads: apps, accounts, get_accessible_accounts"]
395
+ // CIL[DOC-MICRO logic_summary="Missing app/envelope => denied. account corpus adds self (+descendants when scope=self_and_descendants via get_accessible_accounts). platform corpus adds the spine-system account with its own item_type allowlist. cil only resolves when an explicit non-empty project list survives the envelope∩pick intersection. Pick item_types are intersected per corpus and any pick entry outside the envelope is recorded in violations." notable_decisions="deny-by-default; violations are surfaced for visible failure rather than silently clamped"]
396
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
397
+ // CIL[DOC-STABILITY stability="experimental"]
398
+ export async function resolveKnowledgeScope(
399
+ ctx: { accountId?: string | null; appId?: string | null; principal?: { id?: string } },
400
+ appRef?: string | null,
401
+ pick: { item_types?: string[]; project_ids?: string[] } = {}
402
+ ): Promise<KnowledgeScope> {
403
+ const denied = (reason: string): KnowledgeScope => ({
404
+ allowed: false,
405
+ deniedReason: reason,
406
+ accountIds: [],
407
+ selfAccountIds: [],
408
+ platformAccountId: null,
409
+ accountItemTypes: [],
410
+ platformItemTypes: [],
411
+ cilProjectIds: null,
412
+ rpcItemTypes: null,
413
+ violations: [],
414
+ })
415
+
416
+ const ref = appRef || ctx.appId || null
417
+ if (!ref) return denied('no_app: cannot resolve the app for knowledge access')
418
+ if (!ctx.accountId) return denied('no_account: no account context for knowledge access')
419
+
420
+ let app: any = null
421
+ try {
422
+ const res = await adminDb
423
+ .from('apps')
424
+ .select('id, slug, config, required_roles')
425
+ .eq(UUID_RE.test(ref) ? 'id' : 'slug', ref)
426
+ .maybeSingle()
427
+ app = res?.data
428
+ } catch (e: any) {
429
+ console.warn('resolveKnowledgeScope: apps lookup failed:', e?.message)
430
+ return denied('envelope_lookup_failed: could not load app config')
431
+ }
432
+ if (!app) return denied(`unknown_app: '${ref}' is not an installed app`)
433
+
434
+ // The caller's app claim (x-app-id header) is client-controllable, so bind
435
+ // it: when the app declares required_roles, the principal must hold at least
436
+ // one. Otherwise a low-privilege caller could claim a staff app's envelope.
437
+ const requiredRoles: string[] = Array.isArray(app.required_roles) ? app.required_roles : []
438
+ const principalRoles: string[] = Array.isArray(ctx.principal?.roles) ? ctx.principal!.roles! : []
439
+ const norm = (r: string) => String(r).replace(/-/g, '_')
440
+ // system_admin is the platform superuser — same bypass convention as
441
+ // useApps/AppWrapper/get_account_apps; a god-mode principal must not be
442
+ // blocked by an app's required_roles envelope binding.
443
+ const isSystemAdmin = principalRoles.some((pr) => norm(pr) === 'system_admin')
444
+ if (requiredRoles.length && principalRoles.length && !isSystemAdmin) {
445
+ const ok = requiredRoles.some((rr) => principalRoles.some((pr) => norm(pr) === norm(rr)))
446
+ if (!ok) return denied(`app_role_mismatch: principal lacks any required role for app '${app.slug}'`)
447
+ }
448
+
449
+ const envelope = (app.config as any)?.knowledge as KnowledgeEnvelope | undefined
450
+ if (!envelope || typeof envelope !== 'object') {
451
+ return denied(`no_knowledge_envelope: app '${app.slug}' declares no knowledge access`)
452
+ }
453
+
454
+ const accountEnabled = !!envelope.account
455
+ const accountBlock = envelope.account === true ? {} : (envelope.account || {})
456
+ const platformEnabled = !!envelope.platform
457
+ const platformBlock = envelope.platform === true ? {} : (envelope.platform || {})
458
+ const cilEnabled = !!envelope.cil
459
+ const cilBlock = envelope.cil === true ? {} : (envelope.cil || {})
460
+
461
+ // Account corpus: self, or self + descendants under hierarchy scope.
462
+ let selfAccountIds: string[] = accountEnabled ? [ctx.accountId] : []
463
+ if (accountEnabled && accountBlock.scope === 'self_and_descendants' && ctx.principal?.id) {
464
+ try {
465
+ const { data } = await adminDb.rpc('get_accessible_accounts', { actor_id: ctx.principal.id })
466
+ const ids = (data || []).map((r: any) => r.account_id ?? r).filter(Boolean)
467
+ if (ids.length) selfAccountIds = ids
468
+ } catch (e: any) {
469
+ console.warn('resolveKnowledgeScope: get_accessible_accounts failed, using self only:', e?.message)
470
+ }
471
+ }
472
+
473
+ // CIL: only granted when an explicit project list survives envelope ∩ pick.
474
+ // `cil: true` without project_ids grants nothing — the pick (thread data)
475
+ // is caller-influenceable, so an id-less envelope must not pass it through.
476
+ const envCilIds = cilEnabled ? (cilBlock.project_ids ?? []) : null
477
+ const pickCilIds = pick.project_ids?.length ? pick.project_ids : null
478
+ const effectiveCilIds = envCilIds?.length && pickCilIds
479
+ ? pickCilIds.filter((id) => envCilIds.includes(id))
480
+ : (envCilIds?.length ? envCilIds : [])
481
+ const cilProjectIds = cilEnabled && effectiveCilIds?.length ? effectiveCilIds : null
482
+ const cilDeniedPicks = pickCilIds?.filter((id) => !cilProjectIds?.includes(id)) ?? []
483
+
484
+ let platformAccountId: string | null = null
485
+ let platformLookupError: string | undefined
486
+ if (platformEnabled || cilProjectIds) {
487
+ try {
488
+ platformAccountId = await ensurePlatformAccountId()
489
+ } catch (e: any) {
490
+ platformLookupError = e?.message || String(e)
491
+ console.warn('resolveKnowledgeScope: platform account lookup failed:', platformLookupError)
492
+ }
493
+ }
494
+
495
+ // Envelope item types per corpus (null = unrestricted within that corpus).
496
+ const envAccountTypes = accountEnabled ? (accountBlock.item_types ?? null) : []
497
+ const envPlatformTypes = platformEnabled ? (platformBlock.sources ?? null) : []
498
+
499
+ // Intersect the agent's pick with the envelope per corpus; record pick
500
+ // entries that match nothing the app permits.
501
+ const violations: string[] = []
502
+ const pickTypes = pick.item_types?.length ? pick.item_types : null
503
+ if (pickTypes) {
504
+ for (const t of pickTypes) {
505
+ const ok =
506
+ matchItemType(envAccountTypes, t) ||
507
+ matchItemType(envPlatformTypes, t) ||
508
+ (t === 'cil_chunk' && !!cilProjectIds?.length)
509
+ if (!ok) violations.push(t)
510
+ }
511
+ }
512
+ const accountItemTypes = pickTypes ? pickTypes.filter((t) => matchItemType(envAccountTypes, t)) : envAccountTypes
513
+ const platformItemTypes = pickTypes ? pickTypes.filter((t) => matchItemType(envPlatformTypes, t)) : envPlatformTypes
514
+ if (cilDeniedPicks.length) violations.push(`cil projects outside envelope: ${cilDeniedPicks.join(',')}`)
515
+
516
+ const accountIds = [
517
+ ...(selfAccountIds),
518
+ ...(platformAccountId && !selfAccountIds.includes(platformAccountId) ? [platformAccountId] : []),
519
+ ]
520
+
521
+ // Best-effort SQL-level item_type filter: only expressible when every
522
+ // enabled corpus has an explicit, wildcard-free list.
523
+ let rpcItemTypes: string[] | null
524
+ if (violations.length || (!accountEnabled && !platformEnabled && !cilProjectIds)) {
525
+ rpcItemTypes = []
526
+ } else {
527
+ const parts: string[] = []
528
+ let unrestricted = false
529
+ if (accountEnabled) {
530
+ if (accountItemTypes == null) unrestricted = true
531
+ else parts.push(...accountItemTypes)
532
+ }
533
+ if (platformEnabled) {
534
+ if (platformItemTypes == null) unrestricted = true
535
+ else parts.push(...platformItemTypes)
536
+ }
537
+ if (cilProjectIds?.length) parts.push('cil_chunk')
538
+ const uniq = [...new Set(parts)]
539
+ rpcItemTypes = unrestricted || uniq.some((t) => t.includes('*')) ? null : uniq
540
+ }
541
+
542
+ const allowed = accountIds.length > 0 &&
543
+ (accountEnabled || platformEnabled || !!cilProjectIds?.length)
544
+
545
+ return {
546
+ allowed,
547
+ deniedReason: allowed
548
+ ? (violations.length ? `pick_outside_envelope: ${violations.join(',')}` : undefined)
549
+ : 'empty_scope: knowledge envelope permits no corpora',
550
+ accountIds,
551
+ selfAccountIds,
552
+ platformAccountId,
553
+ accountItemTypes,
554
+ platformItemTypes,
555
+ cilProjectIds,
556
+ rpcItemTypes,
557
+ violations,
558
+ platformLookupError,
559
+ }
560
+ }
561
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:resolveknowledgescope:function"]
562
+ // CIL[DOC-VECTOR type="context" content="purpose='Resolves the global spine-system account ID from the accounts table.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
563
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
564
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
565
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
566
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
567
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
568
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:ensurePlatformAccountId:function"]
569
+
570
+
571
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:clearembeddingsforitem:function" type="function" identity="clearEmbeddingsForItem" parent="~/.framework/functions/_shared/kb.ts"]
572
+ // CIL[DOC-MACRO purpose="Revoke + delete every embeddings row for a document without touching document_leases."]
573
+ // CIL[DOC-RESPONSIBILITY responsibility="Internal embedding-clearing primitive shared by kbRevoke (which additionally clears the lease row) and kbIngest (which must NOT delete the lease it just acquired)."]
574
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/kb.ts,.framework/migrations/054_kb_ingest_lease.sql"]
575
+ // CIL[DOC-CONTRACT inputs="documentId: string (items.id / embeddings.document_id)" outputs="revoked count" errors="rpc/delete errors propagate" side_effects="revoke_embeddings_for_item RPC + embeddings row delete"]
576
+ // CIL[DOC-MICRO logic_summary="Calls revoke_embeddings_for_item then hard-deletes embeddings rows for document_id." notable_decisions="Extracted so kbIngest can clear stale embeddings inside its lease critical section — routing through kbRevoke would delete the caller's own document_leases row and break mutual exclusion."]
577
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
578
+ // CIL[DOC-STABILITY stability="internal"]
579
+ async function clearEmbeddingsForItem(documentId: string): Promise<number> {
580
+ const { data, error } = await adminDb.rpc('revoke_embeddings_for_item', { p_item_id: documentId })
581
+ if (error) throw error
582
+
583
+ const { error: deleteError } = await adminDb
584
+ .from('embeddings')
585
+ .delete()
586
+ .eq('document_id', documentId)
587
+
588
+ if (deleteError) throw deleteError
589
+ return data || 0
590
+ }
591
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:clearembeddingsforitem:function"]
85
592
 
86
- export async function kbIngest(ctx: CoreContext, body: { id: string }): Promise<{ ok: boolean; message: string; count?: number }> {
593
+
594
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:kbIngest:function" type="function" identity="kbIngest" parent="~/.framework/functions/_shared/kb.ts"]
595
+ // CIL[DOC-MACRO purpose="Ingests a knowledge base item by generating embeddings from its content and storing them in the database."]
596
+ // CIL[DOC-RESPONSIBILITY responsibility="Processes a published KB item, chunks vector fields, and persists embeddings under a content-hash document lease."]
597
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts,.framework/migrations/054_kb_ingest_lease.sql"]
598
+ // CIL[DOC-CONTRACT inputs="ctx: CoreContext, body with id: string and optional account_id" outputs="ok: boolean; message: string; count?: number; in_progress?: boolean" errors="id is required; record not found; database errors" side_effects="revokes previous embeddings; inserts rows into embeddings table; acquires/releases a document_leases row; archives+revokes superseded documents"]
599
+ // CIL[DOC-MICRO logic_summary="Fetches the item, validates status, hashes the declared vector-field values, acquires a document lease ('reused' skips unchanged work, 'processing' reports another worker owns it), then inside try/finally clears stale embeddings, batches chunks into one embeddings call, inserts, and runs supersede_keys-driven supersession before releasing the lease token." notable_decisions="Revokes existing embeddings via clearEmbeddingsForItem (not kbRevoke — that would delete the caller's own lease); supersession is driven by functionality.kb.supersede_keys declared data fields instead of a hardcoded type-slug gate; a fenced release (token mismatch) warns without throwing — the work was written but ownership was lost to a superseding acquire."]
600
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
601
+ // CIL[DOC-STABILITY stability="internal"]
602
+ // CIL[DOC-NOTE type="contract" content="Lease outcomes: 'reused' returns a live-embeddings count so callers may stamp freshness; 'processing' returns in_progress:true with NO success marker — callers that stamp embedding state must not stamp success or failure for it, the owning worker's completion reconciles the corpus."]
603
+ /**
604
+ * The kbIngest function processes a knowledge base item by generating embeddings from its content and storing them in a database. It ensures the item is published before proceeding with embedding generation.
605
+ *
606
+ * @macro Async function responsible for ingesting knowledge base items and generating embeddings.
607
+ * @micro Retrieves item details from the database, processes its content into chunks, and generates embeddings for each chunk.
608
+ * @inputSpec
609
+ * - ctx: CoreContext (required) — the context containing account information.
610
+ * - body: { id: string } (required) — an object containing the ID of the knowledge base item.
611
+ * @outputSpec Promise<{ ok: boolean; message: string; count?: number; in_progress?: boolean }> — indicates success status, a message, and an optional count of embeddings generated; in_progress marks a lease 'processing' outcome another worker owns.
612
+ * @throws Error — id is required; Error — record not found; Error — error from database operations.
613
+ * @sideEffects Writes embeddings to the database and may revoke previous embeddings; acquires and releases a document_leases row.
614
+ * @stability internal
615
+ * @calls adminDb.from('items'), adminDb.from('types'), adminDb.rpc('acquire_document_lease'), clearEmbeddingsForItem, callOpenAIEmbeddings, adminDb.from('embeddings').insert, adminDb.rpc('release_document_lease')
616
+ * @cil-generated
617
+ */
618
+ export async function kbIngest(ctx: CoreContext, body: { id: string; account_id?: string }): Promise<{ ok: boolean; message: string; count?: number; in_progress?: boolean }> {
87
619
  const { id } = body
88
620
  if (!id) throw new Error('id is required')
89
621
 
622
+ // Multi-tenant support callers ingest articles owned by
623
+ // customer accounts — the caller passes the record's account_id explicitly.
624
+ // Default remains the caller's own account.
625
+ const recordAccountId = body.account_id || ctx.accountId
90
626
  const { data: record, error } = await adminDb
91
627
  .from('items')
92
628
  .select('*')
93
629
  .eq('id', id)
94
- .eq('account_id', ctx.accountId)
630
+ .eq('account_id', recordAccountId)
95
631
  .maybeSingle()
96
632
 
97
633
  if (error) throw error
@@ -118,37 +654,87 @@ export async function kbIngest(ctx: CoreContext, body: { id: string }): Promise<
118
654
 
119
655
  const typeSlug = typeRecord?.slug || ''
120
656
  const appId = record.app_id || typeRecord?.app_id || null
121
- const securityLevel = SECURITY_LEVELS[typeSlug] || 'internal'
657
+ // Apps declare the corpus security level on their type's
658
+ // design_schema.functionality.kb.security_level; 'internal' is the
659
+ // deny-by-default fallback when undeclared.
660
+ const securityLevel = resolveSecurityLevel(designSchema)
661
+
662
+ // Content-hash lease (document_leases, migration 054): sha256 over the
663
+ // declared vector-field values in order — the same text that feeds the
664
+ // embedder. The lease dedupes unchanged re-embeds and fences concurrent
665
+ // workers (publish trigger + detached ingest both arrive here).
666
+ const contentHash = createHash('sha256')
667
+ .update(vectorFields.map((field: string) => getFieldValue(record, field)).join('\n'))
668
+ .digest('hex')
669
+
670
+ const { data: lease, error: leaseError } = await adminDb.rpc('acquire_document_lease', {
671
+ p_document_id: id,
672
+ p_account_id: record.account_id,
673
+ p_content_hash: contentHash,
674
+ })
675
+ if (leaseError) throw leaseError
676
+
677
+ if (lease?.outcome === 'reused') {
678
+ // Same-hash completed work exists — reuse it. Count live embeddings so
679
+ // callers that stamp freshness get an honest count.
680
+ const { count } = await adminDb
681
+ .from('embeddings')
682
+ .select('id', { count: 'exact', head: true })
683
+ .eq('document_id', id)
684
+ .is('revoked_at', null)
685
+ return { ok: true, message: 'embeddings reused (unchanged content)', count: count ?? 0 }
686
+ }
122
687
 
123
- await kbRevoke(ctx, { id })
688
+ if (lease?.outcome === 'processing') {
689
+ // A live worker owns this content's embed — nothing to do. The caller
690
+ // must NOT stamp success; in_progress lets stampers keep their pending
691
+ // marker instead of writing a false terminal state.
692
+ return { ok: true, message: 'embedding already in progress', in_progress: true }
693
+ }
694
+
695
+ const leaseId: string | null = lease?.lease_id || null
696
+ let succeeded = false
697
+ try {
698
+ // Clear prior embeddings WITHOUT deleting the lease row we just hold —
699
+ // kbRevoke clears document_leases for external callers (so a revoked
700
+ // doc re-ingests cleanly); routing through it here would release our
701
+ // own lease mid-flight and reopen the race the lease exists to close.
702
+ await clearEmbeddingsForItem(id)
124
703
 
125
- const embeddingsToInsert: any[] = []
126
- let chunkIndex = 0
704
+ const toEmbed: Array<{ field: string; chunk: string }> = []
705
+ for (const field of vectorFields) {
706
+ const raw = getFieldValue(record, field)
707
+ if (!raw.trim()) continue
127
708
 
128
- for (const field of vectorFields) {
129
- const raw = getFieldValue(record, field)
130
- if (!raw.trim()) continue
709
+ const isChunked = field === chunkField && raw.length > chunkSize
710
+ const chunks = isChunked ? chunkText(raw, chunkSize, chunkOverlap) : [raw]
131
711
 
132
- const isChunked = field === chunkField && raw.length > chunkSize
133
- const chunks = isChunked ? chunkText(raw, chunkSize, chunkOverlap) : [raw]
712
+ for (const chunk of chunks) {
713
+ if (chunk.trim()) toEmbed.push({ field, chunk })
714
+ }
715
+ }
134
716
 
135
- for (const chunk of chunks) {
136
- if (!chunk.trim()) continue
137
- const embedding = await generateEmbedding(chunk, ctx)
138
- if (!embedding.length) continue
717
+ // One batched embedding call per item — a sequential call per chunk makes
718
+ // bulk reindex (N items × ~10 chunks) blow past the function timeout.
719
+ const embedJson = await callOpenAIEmbeddings(ctx, toEmbed.map(t => t.chunk), EMBEDDING_MODEL)
720
+ const vectors: number[][] = (embedJson.data || []).map((d: any) => d.embedding)
139
721
 
722
+ const embeddingsToInsert: any[] = []
723
+ for (let i = 0; i < toEmbed.length; i++) {
724
+ const embedding = vectors[i]
725
+ if (!embedding?.length) continue
140
726
  embeddingsToInsert.push({
141
727
  account_id: record.account_id,
142
728
  model_id: EMBEDDING_MODEL,
143
729
  document_id: id,
144
- chunk_index: chunkIndex++,
145
- content: chunk,
730
+ chunk_index: i,
731
+ content: toEmbed[i].chunk,
146
732
  embedding: JSON.stringify(embedding),
147
733
  metadata: {
148
734
  type_slug: typeSlug,
149
735
  item_type: typeSlug,
150
736
  app_id: appId,
151
- vector_type: field,
737
+ vector_type: toEmbed[i].field,
152
738
  security_level: securityLevel,
153
739
  status: record.status,
154
740
  },
@@ -156,36 +742,138 @@ export async function kbIngest(ctx: CoreContext, body: { id: string }): Promise<
156
742
  revoked_at: null,
157
743
  })
158
744
  }
159
- }
160
745
 
161
- if (embeddingsToInsert.length === 0) {
162
- return { ok: true, message: 'no content to embed', count: 0 }
163
- }
746
+ if (embeddingsToInsert.length === 0) {
747
+ succeeded = true
748
+ return { ok: true, message: 'no content to embed', count: 0 }
749
+ }
164
750
 
165
- const { error: insertError } = await adminDb.from('embeddings').insert(embeddingsToInsert)
166
- if (insertError) throw insertError
751
+ const { error: insertError } = await adminDb.from('embeddings').insert(embeddingsToInsert)
752
+ if (insertError) throw insertError
753
+
754
+ // Supersession: a newly published document with the same logical
755
+ // identity replaces older published versions. Types declare the data
756
+ // fields that identify a logical document via
757
+ // design_schema.functionality.kb.supersede_keys (e.g. ['slug'],
758
+ // ['source_ticket_id']); undeclared types skip supersession entirely.
759
+ const supersedeKeys: string[] = Array.isArray(kbConfig.supersede_keys) ? kbConfig.supersede_keys : []
760
+ const orParts: string[] = []
761
+ for (const key of supersedeKeys) {
762
+ const value = record.data?.[key]
763
+ if (value !== undefined && value !== null && value !== '') {
764
+ orParts.push(`data->>${key}.eq.${value}`)
765
+ }
766
+ }
767
+ if (orParts.length) {
768
+ const { data: stale } = await adminDb
769
+ .from('items')
770
+ .select('id, status, data')
771
+ .eq('account_id', record.account_id)
772
+ .eq('type_id', record.type_id)
773
+ .eq('status', 'published')
774
+ .neq('id', id)
775
+ .or(orParts.join(','))
776
+ for (const old of stale || []) {
777
+ await adminDb
778
+ .from('items')
779
+ .update({
780
+ status: 'archived',
781
+ data: { ...(old.data || {}), superseded_by: id, superseded_at: new Date().toISOString() },
782
+ updated_at: new Date().toISOString(),
783
+ })
784
+ .eq('id', old.id)
785
+ await kbRevoke(ctx, { id: old.id }).catch((err) =>
786
+ console.warn(`supersession revoke failed for ${old.id}:`, err?.message)
787
+ )
788
+ }
789
+ }
167
790
 
168
- return { ok: true, message: 'embeddings generated', count: embeddingsToInsert.length }
791
+ succeeded = true
792
+ return { ok: true, message: 'embeddings generated', count: embeddingsToInsert.length }
793
+ } finally {
794
+ // Token-gated release. A false return means a superseding acquire took
795
+ // ownership mid-flight (new content hash or expired lease) — the work
796
+ // was already written but ownership was lost; warn, do not throw, and
797
+ // let the next run reconcile.
798
+ const { data: released, error: releaseError } = await adminDb.rpc('release_document_lease', {
799
+ p_document_id: id,
800
+ p_lease_id: leaseId,
801
+ p_status: succeeded ? 'completed' : 'failed',
802
+ })
803
+ if (releaseError) {
804
+ console.warn(`release_document_lease failed for ${id}:`, releaseError.message)
805
+ } else if (released === false) {
806
+ console.warn(`document lease fenced for ${id} — a superseding acquire took ownership before release`)
807
+ }
808
+ }
169
809
  }
810
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:kbIngest:function"]
170
811
 
812
+
813
+ // CIL[DOC-VECTOR type="context" content="purpose='Ingests a knowledge base item by generating embeddings from its content and storing them in the database.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
814
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
815
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
816
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
817
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
818
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
819
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:kbRevoke:function" type="function" identity="kbRevoke" parent="~/.framework/functions/_shared/kb.ts"]
820
+ // CIL[DOC-MACRO purpose="Revokes all embeddings associated with a given knowledge base item."]
821
+ // CIL[DOC-RESPONSIBILITY responsibility="Removes embeddings for an item from the vector store."]
822
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
823
+ // CIL[DOC-CONTRACT inputs="ctx: CoreContext, body with id: string" outputs="ok: boolean; message: string; count?: number" errors="id is required; database errors" side_effects="deletes rows from embeddings table; deletes the document_leases row so a revoked doc re-ingests cleanly"]
824
+ // CIL[DOC-MICRO logic_summary="Clears embeddings via clearEmbeddingsForItem, then deletes the document_leases row (warn-on-error) so a 'completed' lease cannot force 'reused' on a now-empty corpus." notable_decisions="Lease delete is unconditional — an in-flight ingest racing an external revoke is already racy at the status level."]
825
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
826
+ // CIL[DOC-STABILITY stability="internal"]
827
+ /**
828
+ * The kbRevoke function revokes embeddings associated with a specific item ID. It ensures that the item ID is provided and performs database operations to delete the embeddings.
829
+ *
830
+ * @inputSpec
831
+ * ctx: CoreContext (required) — the context object for core operations.
832
+ * body: { id: string } (required) — an object containing the ID of the item whose embeddings are to be revoked.
833
+ * @outputSpec Promise<{ ok: boolean; message: string; count?: number }> — returns an object indicating success, a message, and an optional count of revoked embeddings.
834
+ * @throws Error — id is required, Error — error from revoke_embeddings_for_item, Error — deleteError from embeddings deletion
835
+ * @sideEffects DB writes to revoke embeddings, delete from the embeddings table, and delete the document_leases row.
836
+ * @stability internal
837
+ * @calls clearEmbeddingsForItem, adminDb.from('document_leases').delete()
838
+ * @cil-generated
839
+ */
171
840
  export async function kbRevoke(ctx: CoreContext, body: { id: string }): Promise<{ ok: boolean; message: string; count?: number }> {
172
841
  const { id } = body
173
842
  if (!id) throw new Error('id is required')
174
843
 
175
- const { data, error } = await adminDb.rpc('revoke_embeddings_for_item', { p_item_id: id })
176
- if (error) throw error
844
+ const count = await clearEmbeddingsForItem(id)
177
845
 
178
- const { error: deleteError } = await adminDb
179
- .from('embeddings')
846
+ // Clear any stored lease — a 'completed' lease would otherwise force
847
+ // 'reused' on the next ingest of this now-empty corpus. Warn-only: a
848
+ // lease-delete failure must not fail revocation.
849
+ const { error: leaseDeleteError } = await adminDb
850
+ .from('document_leases')
180
851
  .delete()
181
852
  .eq('document_id', id)
853
+ if (leaseDeleteError) {
854
+ console.warn(`document_leases delete failed for ${id}:`, leaseDeleteError.message)
855
+ }
182
856
 
183
- if (deleteError) throw deleteError
184
-
185
- return { ok: true, message: 'embeddings revoked', count: data || 0 }
857
+ return { ok: true, message: 'embeddings revoked', count }
186
858
  }
859
+ // CIL[DOC-VECTOR type="context" content="purpose='Revokes all embeddings associated with a given knowledge base item.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
860
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
861
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
862
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
863
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
864
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
865
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:kbRevoke:function"]
866
+
187
867
 
188
- export async function kbReindex(ctx: CoreContext, body: { type_slug: string; app_id?: string }): Promise<{ ok: boolean; count: number }> {
868
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:kbReindex:function" type="function" identity="kbReindex" parent="~/.framework/functions/_shared/kb.ts"]
869
+ // CIL[DOC-MACRO purpose="Re-ingests all published items of a given type to refresh their embeddings."]
870
+ // CIL[DOC-RESPONSIBILITY responsibility="Iterates over published items for a type and re-runs kbIngest."]
871
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
872
+ // CIL[DOC-CONTRACT inputs="ctx: CoreContext, body with type_slug: string, app_id?: string" outputs="ok: boolean; count: number" errors="type_slug is required; type not found; database errors" side_effects="[]"]
873
+ // CIL[DOC-MICRO logic_summary="Resolves the type ID, queries published items, and calls kbIngest for each." notable_decisions="[]"]
874
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
875
+ // CIL[DOC-STABILITY stability="internal"]
876
+ export async function kbReindex(ctx: CoreContext, body: { type_slug: string; app_id?: string }): Promise<{ ok: boolean; count: number; errors?: Array<{ id: string; error: string }> }> {
189
877
  const { type_slug } = body
190
878
  if (!type_slug) throw new Error('type_slug is required')
191
879
 
@@ -203,16 +891,41 @@ export async function kbReindex(ctx: CoreContext, body: { type_slug: string; app
203
891
  const { data: records, error } = await query
204
892
  if (error) throw error
205
893
 
894
+ // Bounded parallelism — sequential ingest of a full corpus exceeds the
895
+ // function timeout, and one bad record should not abort the whole reindex.
896
+ const BATCH = 10
206
897
  let count = 0
207
- for (const record of records || []) {
208
- await kbIngest(ctx, { id: record.id })
209
- count++
898
+ const errors: Array<{ id: string; error: string }> = []
899
+ for (let i = 0; i < (records || []).length; i += BATCH) {
900
+ const batch = (records || []).slice(i, i + BATCH)
901
+ const results = await Promise.allSettled(batch.map(r => kbIngest(ctx, { id: r.id })))
902
+ for (let j = 0; j < results.length; j++) {
903
+ const r = results[j]
904
+ if (r.status === 'fulfilled') count++
905
+ else errors.push({ id: batch[j].id, error: String(r.reason?.message || r.reason) })
906
+ }
210
907
  }
211
908
 
212
- return { ok: true, count }
909
+ return { ok: errors.length === 0, count, errors }
213
910
  }
911
+ // CIL[DOC-VECTOR type="context" content="purpose='Re-ingests all published items of a given type to refresh their embeddings.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
912
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
913
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
914
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
915
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
916
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
917
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:kbReindex:function"]
214
918
 
215
- export async function kbSearch(ctx: CoreContext, body: any): Promise<{ results: any[]; count: number }> {
919
+
920
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:kbSearch:function" type="function" identity="kbSearch" parent="~/.framework/functions/_shared/kb.ts"]
921
+ // CIL[DOC-MACRO purpose="Performs semantic search over knowledge base embeddings and returns matching published records."]
922
+ // CIL[DOC-RESPONSIBILITY responsibility="Executes vector search, filters by metadata, and returns sanitized results."]
923
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/db.ts,.framework/functions/_shared/kb.ts,.framework/functions/_shared/llm.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
924
+ // CIL[DOC-CONTRACT inputs="ctx: CoreContext, body: any" outputs="results array; count: number" errors="query is required; database errors" side_effects="[]"]
925
+ // CIL[DOC-MICRO logic_summary="Generates query embedding, calls match_embeddings, filters, deduplicates, and builds result objects." notable_decisions="Includes platform account in search; limits to best match per document."]
926
+ // CIL[DOC-CONFIDENCE pass="second" score="0.90" flags="none"]
927
+ // CIL[DOC-STABILITY stability="internal"]
928
+ export async function kbSearch(ctx: CoreContext, body: any): Promise<{ results: any[]; count: number; error?: string }> {
216
929
  const query = body.query
217
930
  const typeSlugs = body.type_slugs || []
218
931
  const sources = body.sources || ['kb']
@@ -225,19 +938,24 @@ export async function kbSearch(ctx: CoreContext, body: any): Promise<{ results:
225
938
 
226
939
  if (!query) throw new Error('query is required')
227
940
 
228
- const accountIds = [ctx.accountId]
229
- const platformAccountId = await ensurePlatformAccountId()
230
- if (platformAccountId && platformAccountId !== ctx.accountId) {
231
- accountIds.push(platformAccountId)
941
+ // App knowledge envelope is the ceiling; body.type_slugs is the caller's
942
+ // pick within it. Deny-by-default: no envelope, no retrieval.
943
+ const scope = await resolveKnowledgeScope(ctx, (ctx as any).appId || body.app_id, { item_types: typeSlugs })
944
+ if (!scope.allowed) {
945
+ return { results: [], count: 0, error: `knowledge_denied: ${scope.deniedReason}` }
946
+ }
947
+ if (scope.violations.length) {
948
+ return { results: [], count: 0, error: `knowledge_denied: sources outside app envelope: ${scope.violations.join(', ')}` }
232
949
  }
950
+ const accountIds = scope.accountIds
233
951
 
234
952
  const queryEmbedding = await generateEmbedding(query, ctx)
235
953
  if (!queryEmbedding.length) return { results: [], count: 0 }
236
954
 
237
955
  const allowedVectorTypes = new Set(vectorFields)
238
956
  const allowedTypeSlugs = new Set(typeSlugs)
239
- const includeKb = sources.includes('kb')
240
- const includeCode = sources.includes('code')
957
+ const includeKb = !sources.length || sources.includes('kb')
958
+ const includeCode = !sources.length || sources.includes('code')
241
959
 
242
960
  const allMatches: any[] = []
243
961
  const vectorTypesToSearch = vectorFields.length ? vectorFields : [null]
@@ -250,7 +968,7 @@ export async function kbSearch(ctx: CoreContext, body: any): Promise<{ results:
250
968
  filter_account_ids: accountIds,
251
969
  filter_vector_type: vt,
252
970
  similarity_threshold: threshold,
253
- filter_item_types: null,
971
+ filter_item_types: scope.rpcItemTypes,
254
972
  })
255
973
 
256
974
  if (rpcError) throw new Error(`rpc_${rpcError.message}`)
@@ -259,6 +977,7 @@ export async function kbSearch(ctx: CoreContext, body: any): Promise<{ results:
259
977
 
260
978
  const filtered = allMatches.filter((m: any) => {
261
979
  if (m.similarity < threshold) return false
980
+ if (!knowledgeRowAllowed(scope, m)) return false
262
981
  const meta = m.metadata || {}
263
982
  const vectorType = meta.vector_type || ''
264
983
  const matchAppId = meta.app_id || null
@@ -267,6 +986,10 @@ export async function kbSearch(ctx: CoreContext, body: any): Promise<{ results:
267
986
 
268
987
  if (appId && matchAppId !== appId) return false
269
988
 
989
+ const isCode = meta.item_type === 'cil_chunk'
990
+ if (isCode && !includeCode) return false
991
+ if (!isCode && !includeKb) return false
992
+
270
993
  return true
271
994
  })
272
995
 
@@ -279,17 +1002,42 @@ export async function kbSearch(ctx: CoreContext, body: any): Promise<{ results:
279
1002
  const documentIds = Array.from(bestByDoc.keys()).slice(offset, offset + limit)
280
1003
  if (documentIds.length === 0) return { results: [], count: 0 }
281
1004
 
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)
1005
+ // document_id is a UUID for regular records but a CIL stable_id for
1006
+ // cil_chunk embeddings — feeding stable_ids to .in('id', ...) errors with
1007
+ // 'invalid input syntax for type uuid', so split the lookup like
1008
+ // retrieveKnowledge does.
1009
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
1010
+ const uuidIds = documentIds.filter((id) => UUID_RE.test(id))
1011
+ const stableIds = documentIds.filter((id) => !UUID_RE.test(id))
1012
+ const records: any[] = []
1013
+ if (uuidIds.length) {
1014
+ const { data, error } = await (ctx.db || adminDb)
1015
+ .from('items')
1016
+ .select('*, types!inner(slug)')
1017
+ .in('id', uuidIds)
1018
+ .in('account_id', accountIds)
1019
+ if (error) throw error
1020
+ records.push(...(data || []))
1021
+ }
1022
+ if (stableIds.length) {
1023
+ const { data, error } = await (ctx.db || adminDb)
1024
+ .from('items')
1025
+ .select('*, types!inner(slug)')
1026
+ .in('data->>stable_id', stableIds)
1027
+ .in('account_id', accountIds)
1028
+ if (error) throw error
1029
+ records.push(...(data || []))
1030
+ }
287
1031
 
288
- if (recordsError) throw recordsError
1032
+ const recordByDocId = new Map<string, any>()
1033
+ for (const r of records) {
1034
+ recordByDocId.set(r.id, r)
1035
+ if (r.data?.stable_id) recordByDocId.set(r.data.stable_id, r)
1036
+ }
289
1037
 
290
1038
  const results: any[] = []
291
1039
  for (const match of documentIds.map((id) => bestByDoc.get(id))) {
292
- const record = (records || []).find((r: any) => r.id === match.document_id)
1040
+ const record = recordByDocId.get(match.document_id)
293
1041
  if (!record) continue
294
1042
  if (record.status !== 'published') continue
295
1043
  if (!matchesFilters(record, filters)) continue
@@ -302,6 +1050,7 @@ export async function kbSearch(ctx: CoreContext, body: any): Promise<{ results:
302
1050
  const source = match.metadata?.item_type === 'cil_chunk' ? 'code' : 'kb'
303
1051
  results.push({
304
1052
  id: record.id,
1053
+ document_id: match.document_id,
305
1054
  type_slug: recordTypeSlug,
306
1055
  title: sanitized.title || record.title,
307
1056
  snippet: match.content,
@@ -316,4 +1065,29 @@ export async function kbSearch(ctx: CoreContext, body: any): Promise<{ results:
316
1065
 
317
1066
  return { results: results.sort((a, b) => b.similarity - a.similarity), count: results.length }
318
1067
  }
1068
+ // CIL[DOC-VECTOR type="context" content="purpose='Performs semantic search over knowledge base embeddings and returns matching published records.'; No verified external callers; imports: .framework/functions/_shared/db.ts,.framework/functions/_shared/middleware.ts,.framework/functions/_shared/permissions.ts"]
1069
+ // CIL[DOC-VECTOR type="blast_radius" content="No direct caller data from CIL graph; local impact only"]
1070
+ // CIL[DOC-VECTOR type="configuration" content="No environment or build configuration dependencies identified"]
1071
+ // CIL[DOC-VECTOR type="constraint" content="Standard runtime; no special invariants documented"]
1072
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
1073
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
1074
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:kbSearch:function"]
1075
+
1076
+
1077
+ // CIL[BEGIN stable_id="~/.framework/functions/_shared/kb.ts:kbPublish:function" type="function" identity="kbPublish" parent="~/.framework/functions/_shared/kb.ts"]
1078
+ // CIL[DOC-MACRO purpose="Publish a KB record through admin-data and immediately ingest it for vector search"]
1079
+ // CIL[DOC-RESPONSIBILITY responsibility="Implements kbPublish"]
1080
+ // CIL[DOC-CONTEXT module="functions/_shared" layer="kb" related_files=".framework/functions/_shared/kb.ts,.framework/functions/admin-data.ts"]
1081
+ // CIL[DOC-CONTRACT inputs="ctx: CoreContext, body with id: string" outputs="{ record, ingest }" errors="id is required; admin-data validation/permission errors" side_effects="PATCHes items.status to 'published' via admin-data; writes/revokes embeddings via kbIngest"]
1082
+ // CIL[DOC-MICRO logic_summary="Builds a synthetic PATCH context targeting items/:id, flips status to published through admin-data so permissions/audit/triggers run, then calls kbIngest to embed the published record" notable_decisions="Publish goes through admin-data rather than adminDb so field-level permissions and triggers apply"]
1083
+ // CIL[DOC-STABILITY stability="stable"]
1084
+ export async function kbPublish(ctx: CoreContext, body: { id: string }) {
1085
+ const { id } = body
1086
+ if (!id) throw new Error('id is required')
1087
+ const updateCtx = { ...ctx, query: { ...ctx.query, entity: 'items', id, method: 'PATCH' } }
1088
+ const record = await adminUpdate(updateCtx as any, { status: 'published' })
1089
+ const ingest = await kbIngest(ctx, { id })
1090
+ return { record, ingest }
1091
+ }
1092
+ // CIL[END stable_id="~/.framework/functions/_shared/kb.ts:kbPublish:function"]
319
1093