spine-framework 0.3.145 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +5067 -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 +15 -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
@@ -0,0 +1,138 @@
1
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/principal-cache.test.ts:module_header:module_header" type="module_header" identity="module_header" parent="none"]
2
+ // CIL[DOC-MACRO purpose="Unit tests for the human principal cache placement guard"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Verify principals missing both account_id and role_id (mid-registration) are never cached, while placed principals and account-scoped contacts cache normally"]
4
+ // CIL[DOC-CONTEXT module="tests/unit/principal-cache" layer="test-unit" related_files=".framework/functions/_shared/principal.ts,.framework/migrations/052_registration_security.sql"]
5
+ // CIL[DOC-CONTRACT inputs="none" outputs="vitest assertions" errors="none" side_effects="Mocks @supabase/supabase-js createClient and db module"]
6
+ // CIL[DOC-MICRO logic_summary="Drives resolvePrincipal with Bearer tokens while toggling the mocked people row between unplaced and placed states; asserts re-resolution instead of stale cache replay" notable_decisions="Regression coverage for the ~60s 'Account context required' stall documented in app install docs"]
7
+ // CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
8
+ // CIL[DOC-STABILITY stability="stable"]
9
+ // CIL[DOC-AUDIENCE audience="test-engineers"]
10
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
11
+
12
+ const { mockGetUser, mockPeopleSingle, mockPersonByAuthUid } = vi.hoisted(() => ({
13
+ mockGetUser: vi.fn(),
14
+ mockPeopleSingle: vi.fn(),
15
+ mockPersonByAuthUid: vi.fn(),
16
+ }))
17
+
18
+ vi.mock('@supabase/supabase-js', () => ({
19
+ createClient: vi.fn(() => ({
20
+ auth: { getUser: mockGetUser },
21
+ })),
22
+ }))
23
+
24
+ vi.mock('../../functions/_shared/db.ts', () => ({
25
+ adminDb: {
26
+ from: vi.fn(() => ({
27
+ select: vi.fn(() => ({
28
+ eq: vi.fn(() => ({ maybeSingle: mockPersonByAuthUid })),
29
+ })),
30
+ })),
31
+ rpc: vi.fn(),
32
+ },
33
+ getUserDb: vi.fn(() => ({
34
+ from: vi.fn(() => ({
35
+ select: vi.fn(() => ({
36
+ eq: vi.fn(() => ({ single: mockPeopleSingle })),
37
+ })),
38
+ })),
39
+ })),
40
+ }))
41
+
42
+ import { resolvePrincipal } from '../../functions/_shared/principal.ts'
43
+ // CIL[DOC-VECTOR type="context" content="purpose='Unit tests for the human principal cache placement guard'; No verified external callers; imports: .framework/functions/_shared/principal.ts" status="ready"]
44
+
45
+ const event = (token: string) => ({
46
+ headers: { authorization: `Bearer ${token}`, 'user-agent': 'vitest' },
47
+ })
48
+
49
+ const authUser = { id: 'auth-1', email: 'new-user@example.com' }
50
+
51
+ function setPerson(person: any) {
52
+ mockPeopleSingle.mockResolvedValue({ data: person, error: null })
53
+ }
54
+
55
+ beforeEach(() => {
56
+ vi.clearAllMocks()
57
+ mockGetUser.mockResolvedValue({ data: { user: authUser }, error: null })
58
+ mockPersonByAuthUid.mockResolvedValue({ data: { id: 'person-1' } })
59
+ })
60
+
61
+ describe('resolvePrincipal human cache — mid-registration guard', () => {
62
+ it('does not cache a principal with both account_id and role_id null', async () => {
63
+ // First call lands mid-registration (placement RPC has not run yet).
64
+ setPerson({
65
+ id: 'person-1',
66
+ email: authUser.email,
67
+ account_id: null,
68
+ role_id: null,
69
+ role: null,
70
+ })
71
+ const first = await resolvePrincipal(event('tok-unplaced'))
72
+ expect(first.type).toBe('human')
73
+ expect(first.accountId).toBeNull()
74
+
75
+ // Placement commits — same token must re-resolve and see the account.
76
+ setPerson({
77
+ id: 'person-1',
78
+ email: authUser.email,
79
+ account_id: 'acct-1',
80
+ role_id: 'role-1',
81
+ role: { id: 'role-1', slug: 'member_admin', name: 'Admin', is_system: false, permissions: [] },
82
+ })
83
+ const second = await resolvePrincipal(event('tok-unplaced'))
84
+ expect(second.accountId).toBe('acct-1')
85
+ expect(second.roles).toEqual(['member_admin'])
86
+ expect(mockPeopleSingle).toHaveBeenCalledTimes(2)
87
+ })
88
+
89
+ it('caches a fully placed principal on subsequent requests', async () => {
90
+ setPerson({
91
+ id: 'person-1',
92
+ email: authUser.email,
93
+ account_id: 'acct-1',
94
+ role_id: 'role-1',
95
+ role: { id: 'role-1', slug: 'member', name: 'Member', is_system: false, permissions: [] },
96
+ })
97
+ const first = await resolvePrincipal(event('tok-placed'))
98
+ expect(first.accountId).toBe('acct-1')
99
+
100
+ // Second call must hit the cache — the people lookup is not repeated.
101
+ setPerson({
102
+ id: 'person-1',
103
+ email: authUser.email,
104
+ account_id: 'acct-changed',
105
+ role_id: 'role-1',
106
+ role: { id: 'role-1', slug: 'member', name: 'Member', is_system: false, permissions: [] },
107
+ })
108
+ const second = await resolvePrincipal(event('tok-placed'))
109
+ expect(second.accountId).toBe('acct-1')
110
+ expect(mockPeopleSingle).toHaveBeenCalledTimes(1)
111
+ })
112
+
113
+ it('caches an account-scoped contact that has no role', async () => {
114
+ // Directory contacts legitimately have account_id without role_id —
115
+ // the both-null guard must not skip caching for this steady state.
116
+ setPerson({
117
+ id: 'person-1',
118
+ email: authUser.email,
119
+ account_id: 'acct-1',
120
+ role_id: null,
121
+ role: null,
122
+ })
123
+ const first = await resolvePrincipal(event('tok-contact'))
124
+ expect(first.accountId).toBe('acct-1')
125
+
126
+ setPerson({
127
+ id: 'person-1',
128
+ email: authUser.email,
129
+ account_id: 'acct-other',
130
+ role_id: null,
131
+ role: null,
132
+ })
133
+ const second = await resolvePrincipal(event('tok-contact'))
134
+ expect(second.accountId).toBe('acct-1')
135
+ expect(mockPeopleSingle).toHaveBeenCalledTimes(1)
136
+ })
137
+ })
138
+ // CIL[END stable_id="~/.framework/tests/unit/principal-cache.test.ts:module_header:module_header"]
@@ -0,0 +1,520 @@
1
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/retrieval-golden.test.ts:module_header:module_header" type="module_header" identity="module_header" parent="none"]
2
+ // CIL[DOC-MACRO purpose="Golden retrieval contract — pins retrieveKnowledge's observable result set (order, ids, similarity, lexical flags, scope filtering) so latency optimizations can never silently alter ranking or eligibility"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Snapshot the exact doc lists, mean similarity computation, RRF fusion ordering, security/item-type/source filtering, and denied paths for a fixed mock corpus"]
4
+ // CIL[DOC-CONTEXT module="tests/unit/retrieval-golden" layer="test-unit" related_files=".framework/functions/_shared/agent-runner.ts,.framework/functions/_shared/kb.ts"]
5
+ // CIL[DOC-CONTRACT inputs="none" outputs="assertions on retrieveKnowledge return shape" errors="none" side_effects="none"]
6
+ // CIL[DOC-MICRO logic_summary="Synchronous mock harness (no deferreds) — apps/envelope, embeddings lexical rows, match_embeddings per vector type, items hydration; asserts exact document_id ordering and similarity/meanSimilarity values so perf refactors must produce byte-equivalent result sets"]
7
+ // CIL[DOC-CONFIDENCE pass="first" score="0.9" flags="none"]
8
+ // CIL[DOC-STABILITY stability="stable"]
9
+ // CIL[DOC-AUDIENCE audience="framework maintainers"]
10
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
11
+ import type { CoreContext } from '../../functions/_shared/middleware.ts'
12
+
13
+ vi.mock('../../functions/_shared/db.ts', () => ({
14
+ adminDb: { from: vi.fn(), rpc: vi.fn() }
15
+ }))
16
+ vi.mock('../../functions/_shared/audit.ts', () => ({
17
+ emitAudit: vi.fn().mockResolvedValue(undefined),
18
+ emitLog: vi.fn().mockResolvedValue(undefined),
19
+ emitSpan: vi.fn().mockResolvedValue(undefined)
20
+ }))
21
+ vi.mock('../../functions/_shared/outcomes.ts', () => ({
22
+ emitOutcome: vi.fn().mockResolvedValue(undefined)
23
+ }))
24
+ vi.mock('../../functions/_shared/llm.ts', () => ({
25
+ callOpenAIChat: vi.fn(),
26
+ callOpenAIEmbeddings: vi.fn().mockResolvedValue({ data: [{ embedding: [0.1, 0.2, 0.3] }] })
27
+ }))
28
+ vi.mock('../../functions/admin-data.ts', () => ({ create: vi.fn() }))
29
+ vi.mock('../../functions/integration-outbound.ts', () => ({ execute: vi.fn() }))
30
+ vi.mock('../../functions/_shared/pipeline-runner.ts', () => ({ runPipeline: vi.fn() }))
31
+ vi.mock('../../functions/_shared/funnel.ts', () => ({
32
+ emitSignal: vi.fn().mockResolvedValue(undefined),
33
+ accountDataFlag: vi.fn().mockResolvedValue(false)
34
+ }))
35
+
36
+ import { adminDb } from '../../functions/_shared/db.ts'
37
+ import { retrieveKnowledge, executeToolAction, computeEvidenceState, hasInRangeCitationMarker, ensureInlineCitations } from '../../functions/_shared/agent-runner.ts'
38
+ import { callOpenAIEmbeddings } from '../../functions/_shared/llm.ts'
39
+
40
+ function makeCtx(overrides: Partial<CoreContext> = {}): CoreContext {
41
+ return {
42
+ principal: { id: 'person-1', type: 'human', accountId: 'acct-1', roles: ['member'] },
43
+ accountId: 'acct-1',
44
+ requestId: 'req-test',
45
+ ...overrides
46
+ } as CoreContext
47
+ }
48
+
49
+ // ─── Fixture corpus ──────────────────────────────────────────────────────────
50
+ // IDs are real-shaped UUIDs so hydration goes through the uuid branch.
51
+ const DOC = {
52
+ strong: '11111111-1111-4111-8111-111111111111',
53
+ mid: '22222222-2222-4222-8222-222222222222',
54
+ weak: '33333333-3333-4333-8333-333333333333',
55
+ restricted: '44444444-4444-4444-8444-444444444444',
56
+ otherAcct: '55555555-5555-4555-8555-555555555555',
57
+ lexicalOnly: '66666666-6666-4666-8666-666666666666',
58
+ }
59
+
60
+ const ITEM_ROWS = [
61
+ { id: DOC.strong, title: 'Strong Article', data: { content: 'Full body of the strong article' } },
62
+ { id: DOC.mid, title: 'Mid Article', data: {} },
63
+ { id: DOC.weak, title: 'Weak Article', data: {} },
64
+ { id: DOC.restricted, title: 'Restricted Doc', data: {} },
65
+ { id: DOC.otherAcct, title: 'Other Acct Doc', data: {} },
66
+ { id: DOC.lexicalOnly, title: 'Lexical Only', data: {} },
67
+ ]
68
+
69
+ function vectorRow(id: string, similarity: number, opts: {
70
+ vectorType?: string, itemType?: string, security?: string, accountId?: string, content?: string
71
+ } = {}) {
72
+ return {
73
+ document_id: id,
74
+ content: opts.content ?? `content for ${id}`,
75
+ metadata: {
76
+ item_type: opts.itemType ?? 'kb_article',
77
+ vector_type: opts.vectorType ?? 'semantic',
78
+ ...(opts.security ? { security_level: opts.security } : {}),
79
+ },
80
+ similarity,
81
+ account_id: opts.accountId ?? 'acct-1',
82
+ }
83
+ }
84
+
85
+ // Mock wiring: apps returns the envelope; embeddings returns lexical rows via
86
+ // textSearch; match_embeddings returns rows keyed on filter_vector_type;
87
+ // items hydrates document ids.
88
+ function setupRetrieval(opts: {
89
+ envelope?: any
90
+ appsError?: any
91
+ vectorRows?: Record<string, any[]> // keyed by vector_type
92
+ lexicalRows?: any[]
93
+ lexicalError?: any
94
+ items?: any[]
95
+ } = {}) {
96
+ // envelope: undefined → default {account:true}; null → config.knowledge absent; {} → empty envelope
97
+ const envelope = opts.envelope === undefined ? { account: true } : opts.envelope
98
+ vi.mocked(adminDb.from).mockImplementation((table: string) => {
99
+ if (table === 'apps') {
100
+ const q: any = {
101
+ select: vi.fn(() => q), eq: vi.fn(() => q),
102
+ maybeSingle: vi.fn(() => Promise.resolve(
103
+ opts.appsError
104
+ ? { data: null, error: opts.appsError }
105
+ : { data: { id: 'app-1', slug: 'test-app', required_roles: [], config: envelope === null ? {} : { knowledge: envelope } }, error: null }
106
+ )),
107
+ }
108
+ return q
109
+ }
110
+ if (table === 'accounts') {
111
+ const q: any = {
112
+ select: vi.fn(() => q), eq: vi.fn(() => q),
113
+ maybeSingle: vi.fn(() => Promise.resolve({ data: { id: 'sys-acct' }, error: null })),
114
+ }
115
+ return q
116
+ }
117
+ if (table === 'embeddings') {
118
+ const q: any = {
119
+ select: vi.fn(() => q), in: vi.fn(() => q), is: vi.fn(() => q),
120
+ not: vi.fn(() => q), or: vi.fn(() => q), textSearch: vi.fn(() => q),
121
+ limit: vi.fn(() => q),
122
+ then: (onF: any, onR: any) => Promise.resolve(
123
+ opts.lexicalError ? { data: null, error: opts.lexicalError } : { data: opts.lexicalRows ?? [], error: null }
124
+ ).then(onF, onR),
125
+ }
126
+ return q
127
+ }
128
+ if (table === 'items') {
129
+ const q: any = {
130
+ select: vi.fn(() => q),
131
+ in: vi.fn((_field: string, ids: any[]) =>
132
+ Promise.resolve({ data: (opts.items ?? ITEM_ROWS).filter((i) => ids.includes(i.id)), error: null })),
133
+ }
134
+ return q
135
+ }
136
+ return {} as any
137
+ })
138
+ vi.mocked(adminDb.rpc).mockImplementation((name: string, args: any) => {
139
+ if (name !== 'match_embeddings') return Promise.resolve({ data: null, error: null }) as any
140
+ const rows = opts.vectorRows?.[args?.filter_vector_type] ?? []
141
+ return Promise.resolve({ data: rows, error: null }) as any
142
+ })
143
+ }
144
+
145
+ function lexicalRow(id: string, opts: { itemType?: string; accountId?: string } = {}) {
146
+ return {
147
+ document_id: id,
148
+ content: `lexical content for ${id}`,
149
+ metadata: { item_type: opts.itemType ?? 'kb_article' },
150
+ account_id: opts.accountId ?? 'acct-1',
151
+ }
152
+ }
153
+
154
+ const POLICY = { vector_types: ['semantic'] }
155
+ const APP = 'test-app'
156
+
157
+ describe('retrieveKnowledge — golden result sets', () => {
158
+ beforeEach(() => vi.clearAllMocks())
159
+
160
+ it('orders by RRF-fused rank: vector+lexical hit beats single-source hits', async () => {
161
+ setupRetrieval({
162
+ vectorRows: { semantic: [vectorRow(DOC.strong, 0.9), vectorRow(DOC.mid, 0.6)] },
163
+ lexicalRows: [lexicalRow(DOC.strong), lexicalRow(DOC.lexicalOnly)],
164
+ })
165
+ const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
166
+ const ids = docs.map((d: any) => d.document_id)
167
+ // strong: vector#0 + lexical#0 → 2/(61). mid: vector#1 → 1/62.
168
+ // lexicalOnly: lexical#1 → 1/62. mid vs lexicalOnly tie on score — order
169
+ // by insertion (mid inserted first via vector list).
170
+ expect(ids).toEqual([DOC.strong, DOC.mid, DOC.lexicalOnly])
171
+ expect(docs.find((d: any) => d.document_id === DOC.strong).matched_lexically).toBe(true)
172
+ expect(docs.find((d: any) => d.document_id === DOC.mid).matched_lexically).toBe(false)
173
+ expect(docs.find((d: any) => d.document_id === DOC.lexicalOnly).matched_lexically).toBe(true)
174
+ })
175
+
176
+ it('computes meanSimilarity over top-3 RRF docs that have similarity > 0 (lexical-only excluded)', async () => {
177
+ setupRetrieval({
178
+ vectorRows: { semantic: [vectorRow(DOC.strong, 0.9), vectorRow(DOC.mid, 0.6)] },
179
+ lexicalRows: [lexicalRow(DOC.strong), lexicalRow(DOC.lexicalOnly)],
180
+ })
181
+ const { meanSimilarity } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
182
+ expect(meanSimilarity).toBeCloseTo((0.9 + 0.6) / 2, 6)
183
+ })
184
+
185
+ it('drops rows below the 0.15 similarity floor and restricted rows from other accounts', async () => {
186
+ setupRetrieval({
187
+ vectorRows: {
188
+ semantic: [
189
+ vectorRow(DOC.strong, 0.9),
190
+ vectorRow(DOC.weak, 0.10), // below floor
191
+ vectorRow(DOC.restricted, 0.95, { security: 'restricted', accountId: 'acct-other' }),
192
+ ],
193
+ },
194
+ lexicalRows: [],
195
+ })
196
+ const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
197
+ expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong])
198
+ })
199
+
200
+ it('honors max_security_level — absent security_level defaults to internal, so external-only excludes it', async () => {
201
+ setupRetrieval({
202
+ vectorRows: {
203
+ semantic: [
204
+ vectorRow(DOC.strong, 0.9), // internal — excluded at 'external'
205
+ vectorRow(DOC.lexicalOnly, 0.8, { security: 'external' }), // allowed
206
+ vectorRow(DOC.mid, 0.95, { security: 'restricted', accountId: 'acct-1' }), // own restricted — still excluded
207
+ ],
208
+ },
209
+ lexicalRows: [],
210
+ })
211
+ const { docs } = await retrieveKnowledge(
212
+ 'query', [], 'acct-1', { ...POLICY, max_security_level: 'external' }, makeCtx(), APP)
213
+ expect(docs.map((d: any) => d.document_id)).toEqual([DOC.lexicalOnly])
214
+ })
215
+
216
+ it('max_security_level internal admits internal + external but not restricted', async () => {
217
+ setupRetrieval({
218
+ vectorRows: {
219
+ semantic: [
220
+ vectorRow(DOC.strong, 0.9), // default internal — allowed
221
+ vectorRow(DOC.mid, 0.95, { security: 'restricted', accountId: 'acct-1' }), // own restricted — excluded
222
+ ],
223
+ },
224
+ lexicalRows: [],
225
+ })
226
+ const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
227
+ expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong])
228
+ })
229
+
230
+ it('scopes to knowledge_sources pick when provided', async () => {
231
+ setupRetrieval({
232
+ vectorRows: { semantic: [vectorRow(DOC.strong, 0.9), vectorRow(DOC.mid, 0.8)] },
233
+ lexicalRows: [],
234
+ })
235
+ const { docs } = await retrieveKnowledge('query', [DOC.strong], 'acct-1', POLICY, makeCtx(), APP)
236
+ expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong])
237
+ })
238
+
239
+ it('returns empty + zero mean when nothing matches (no-result query)', async () => {
240
+ setupRetrieval({ vectorRows: { semantic: [] }, lexicalRows: [] })
241
+ const res = await retrieveKnowledge('zzzz no matches', [], 'acct-1', POLICY, makeCtx(), APP)
242
+ expect(res.docs).toEqual([])
243
+ expect(res.meanSimilarity).toBe(0)
244
+ })
245
+
246
+ it('survives lexical failure — vector results still return', async () => {
247
+ setupRetrieval({
248
+ vectorRows: { semantic: [vectorRow(DOC.strong, 0.9)] },
249
+ lexicalError: { message: 'fts error' },
250
+ })
251
+ const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
252
+ expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong])
253
+ })
254
+
255
+ it('survives vector/embedding failure — lexical results still return', async () => {
256
+ const { callOpenAIEmbeddings } = await import('../../functions/_shared/llm.ts')
257
+ vi.mocked(callOpenAIEmbeddings).mockRejectedValueOnce(new Error('embed fail'))
258
+ setupRetrieval({ lexicalRows: [lexicalRow(DOC.lexicalOnly)] })
259
+ const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
260
+ expect(docs.map((d: any) => d.document_id)).toEqual([DOC.lexicalOnly])
261
+ expect(docs[0].matched_lexically).toBe(true)
262
+ })
263
+
264
+ it('denies when the app has a knowledge key permitting no corpora (empty envelope)', async () => {
265
+ setupRetrieval({ envelope: {} })
266
+ const res = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
267
+ expect(res.docs).toEqual([])
268
+ expect(res.denied).toBe(true)
269
+ expect(res.deniedReason).toContain('empty_scope')
270
+ })
271
+
272
+ it('denies when the app declares no knowledge envelope at all', async () => {
273
+ // config.knowledge absent entirely — stronger deny reason
274
+ setupRetrieval({ envelope: null })
275
+ const res = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
276
+ expect(res.docs).toEqual([])
277
+ expect(res.denied).toBe(true)
278
+ expect(res.deniedReason).toContain('no_knowledge_envelope')
279
+ })
280
+
281
+ it('multi-vector-type: merges channels, dedupes by document_id keeping max similarity', async () => {
282
+ setupRetrieval({
283
+ vectorRows: {
284
+ semantic: [
285
+ vectorRow(DOC.strong, 0.7, { vectorType: 'semantic', content: 'SEMANTIC body for strong' }),
286
+ vectorRow(DOC.mid, 0.6, { vectorType: 'semantic' }),
287
+ ],
288
+ title: [
289
+ vectorRow(DOC.strong, 0.95, { vectorType: 'title', content: 'TITLE string only' }),
290
+ vectorRow(DOC.weak, 0.5, { vectorType: 'title' }),
291
+ ],
292
+ },
293
+ lexicalRows: [],
294
+ // Items without data.content so the title→non-title content swap is
295
+ // observable (full-document expansion would otherwise overwrite it).
296
+ items: [
297
+ { id: DOC.strong, title: 'Strong Article', data: {} },
298
+ { id: DOC.mid, title: 'Mid Article', data: {} },
299
+ { id: DOC.weak, title: 'Weak Article', data: {} },
300
+ ],
301
+ })
302
+ const { docs } = await retrieveKnowledge('query', [], 'acct-1', { vector_types: ['semantic', 'title'] }, makeCtx(), APP)
303
+ const strong = docs.find((d: any) => d.document_id === DOC.strong)
304
+ expect(strong.similarity).toBe(0.95) // max across channels
305
+ // Title match swaps in the semantic channel's body content
306
+ expect(strong.content).toContain('SEMANTIC body for strong')
307
+ expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong, DOC.mid, DOC.weak])
308
+ })
309
+
310
+ it('envelope item_types restrict results even when the RPC returns them', async () => {
311
+ setupRetrieval({
312
+ envelope: { account: { item_types: ['kb_article'] } },
313
+ vectorRows: {
314
+ semantic: [
315
+ vectorRow(DOC.strong, 0.9, { itemType: 'kb_article' }),
316
+ vectorRow(DOC.mid, 0.95, { itemType: 'secret_doc' }), // outside envelope types
317
+ ],
318
+ },
319
+ lexicalRows: [],
320
+ })
321
+ const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
322
+ expect(docs.map((d: any) => d.document_id)).toEqual([DOC.strong])
323
+ })
324
+
325
+ it('hydrates titles and expands full document body for small articles', async () => {
326
+ setupRetrieval({
327
+ vectorRows: { semantic: [vectorRow(DOC.strong, 0.9)] },
328
+ lexicalRows: [],
329
+ })
330
+ const { docs } = await retrieveKnowledge('query', [], 'acct-1', POLICY, makeCtx(), APP)
331
+ expect(docs[0].title).toBe('Strong Article')
332
+ expect(docs[0].content).toBe('Full body of the strong article')
333
+ expect(docs[0].full_document).toBe(true)
334
+ })
335
+ })
336
+
337
+
338
+ // ─── Episode-scoped retrieval cache (P1) ─────────────────────────────────────
339
+ // search_knowledge tool calls keyed by normalized query + policy + scope reuse
340
+ // the hydrated docs already fetched for this episode — identical identity hits,
341
+ // reformulated queries miss by design.
342
+ describe('episode retrieval cache — search_knowledge', () => {
343
+ beforeEach(() => vi.clearAllMocks())
344
+
345
+ const toolAction = { handler: 'search_knowledge' }
346
+
347
+ it('second identical query returns cached docs without a new embed/search', async () => {
348
+ setupRetrieval({
349
+ vectorRows: { semantic: [vectorRow(DOC.strong, 0.9), vectorRow(DOC.mid, 0.7)] },
350
+ lexicalRows: [],
351
+ })
352
+ const cache: Record<string, any> = {}
353
+ const ctx = makeCtx({ appId: APP } as any)
354
+ const runCtx = { retrievalCache: cache }
355
+
356
+ const r1 = await executeToolAction(toolAction, { query: 'what is cil', limit: 5 }, ctx, runCtx)
357
+ const embedsAfterFirst = vi.mocked(callOpenAIEmbeddings).mock.calls.length
358
+ const rpcAfterFirst = vi.mocked(adminDb.rpc).mock.calls.length
359
+ expect(r1.results.map((d: any) => d.document_id)).toEqual([DOC.strong, DOC.mid])
360
+ expect(r1.cached).toBeUndefined()
361
+
362
+ const r2 = await executeToolAction(toolAction, { query: 'what is cil', limit: 5 }, ctx, runCtx)
363
+ expect(r2.results.map((d: any) => d.document_id)).toEqual([DOC.strong, DOC.mid])
364
+ expect(r2.cached).toBe(true)
365
+ expect(vi.mocked(callOpenAIEmbeddings).mock.calls.length).toBe(embedsAfterFirst)
366
+ expect(vi.mocked(adminDb.rpc).mock.calls.length).toBe(rpcAfterFirst)
367
+ })
368
+
369
+ it('normalization: case/whitespace variants of the same query hit', async () => {
370
+ setupRetrieval({
371
+ vectorRows: { semantic: [vectorRow(DOC.strong, 0.9)] },
372
+ lexicalRows: [],
373
+ })
374
+ const cache: Record<string, any> = {}
375
+ const ctx = makeCtx({ appId: APP } as any)
376
+ const runCtx = { retrievalCache: cache }
377
+
378
+ await executeToolAction(toolAction, { query: 'what is cil' }, ctx, runCtx)
379
+ const embeds = vi.mocked(callOpenAIEmbeddings).mock.calls.length
380
+
381
+ const r = await executeToolAction(toolAction, { query: ' What Is CIL ' }, ctx, runCtx)
382
+ expect(r.cached).toBe(true)
383
+ expect(vi.mocked(callOpenAIEmbeddings).mock.calls.length).toBe(embeds)
384
+ })
385
+
386
+ it('a reformulated query is a deliberate miss — real retrieval runs', async () => {
387
+ setupRetrieval({
388
+ vectorRows: { semantic: [vectorRow(DOC.strong, 0.9)] },
389
+ lexicalRows: [],
390
+ })
391
+ const cache: Record<string, any> = {}
392
+ const ctx = makeCtx({ appId: APP } as any)
393
+ const runCtx = { retrievalCache: cache }
394
+
395
+ await executeToolAction(toolAction, { query: 'what is cil' }, ctx, runCtx)
396
+ const embeds = vi.mocked(callOpenAIEmbeddings).mock.calls.length
397
+
398
+ const r = await executeToolAction(toolAction, { query: 'code intelligence layer markers' }, ctx, runCtx)
399
+ expect(r.cached).toBeUndefined()
400
+ expect(vi.mocked(callOpenAIEmbeddings).mock.calls.length).toBeGreaterThan(embeds)
401
+ // …and that second query is now cached too
402
+ const r2 = await executeToolAction(toolAction, { query: 'code intelligence layer markers' }, ctx, runCtx)
403
+ expect(r2.cached).toBe(true)
404
+ })
405
+
406
+ it('policy difference (vector_types) produces a different key → miss', async () => {
407
+ setupRetrieval({
408
+ vectorRows: {
409
+ semantic: [vectorRow(DOC.strong, 0.9)],
410
+ title: [vectorRow(DOC.mid, 0.8, { vectorType: 'title' })],
411
+ },
412
+ lexicalRows: [],
413
+ })
414
+ const cache: Record<string, any> = {}
415
+ const ctx = makeCtx({ appId: APP } as any)
416
+ const semanticCtx = {
417
+ retrievalCache: cache,
418
+ config: { promptConfig: { metadata: { retrieval_policy: { vector_types: ['semantic'] } } }, agent: {} },
419
+ }
420
+ const titleCtx = {
421
+ retrievalCache: cache,
422
+ config: { promptConfig: { metadata: { retrieval_policy: { vector_types: ['title'] } } }, agent: {} },
423
+ }
424
+ await executeToolAction(toolAction, { query: 'q' }, ctx, semanticCtx as any)
425
+ const embeds = vi.mocked(callOpenAIEmbeddings).mock.calls.length
426
+ const r = await executeToolAction(toolAction, { query: 'q' }, ctx, titleCtx as any)
427
+ expect(r.cached).toBeUndefined()
428
+ expect(vi.mocked(callOpenAIEmbeddings).mock.calls.length).toBeGreaterThan(embeds)
429
+ expect(r.results.map((d: any) => d.document_id)).toEqual([DOC.mid])
430
+ })
431
+
432
+ it('denied results are cached and replay the denial, not an empty success', async () => {
433
+ setupRetrieval({ envelope: null }) // no knowledge envelope → denied
434
+ const cache: Record<string, any> = {}
435
+ const ctx = makeCtx({ appId: APP } as any)
436
+ const runCtx = { retrievalCache: cache }
437
+
438
+ const r1 = await executeToolAction(toolAction, { query: 'q' }, ctx, runCtx)
439
+ expect(r1.error).toContain('knowledge_denied')
440
+ const embeds = vi.mocked(callOpenAIEmbeddings).mock.calls.length
441
+
442
+ const r2 = await executeToolAction(toolAction, { query: 'q' }, ctx, runCtx)
443
+ expect(r2.error).toContain('knowledge_denied')
444
+ expect(r2.cached).toBe(true)
445
+ expect(vi.mocked(callOpenAIEmbeddings).mock.calls.length).toBe(embeds)
446
+ })
447
+
448
+ it('limit slices post-cache — different limits share one entry', async () => {
449
+ setupRetrieval({
450
+ vectorRows: { semantic: [vectorRow(DOC.strong, 0.9), vectorRow(DOC.mid, 0.7)] },
451
+ lexicalRows: [],
452
+ })
453
+ const cache: Record<string, any> = {}
454
+ const ctx = makeCtx({ appId: APP } as any)
455
+ const runCtx = { retrievalCache: cache }
456
+
457
+ const r1 = await executeToolAction(toolAction, { query: 'q', limit: 1 }, ctx, runCtx)
458
+ expect(r1.results).toHaveLength(1)
459
+ const r2 = await executeToolAction(toolAction, { query: 'q', limit: 5 }, ctx, runCtx)
460
+ expect(r2.cached).toBe(true)
461
+ expect(r2.results).toHaveLength(2)
462
+ })
463
+ })
464
+
465
+
466
+ // ─── Evidence-state recalibration + citation proof (P6) ──────────────────────
467
+ // evidence_found requires aggregate mean >= 0.35 OR a decisive top hit >= 0.5 —
468
+ // a paraphrased query that puts the right article at rank 1 with a noisy tail
469
+ // must not read 'incomplete'. And any evidence_found answer must carry a
470
+ // resolvable [n] marker; citation repair runs before the gate.
471
+ describe('computeEvidenceState — top-hit path', () => {
472
+ const doc = (similarity: number) => ({ document_id: 'x', similarity })
473
+
474
+ it('strong top hit + weak tail → evidence_found (the paraphrase case)', () => {
475
+ const docs = [doc(0.62), doc(0.22), doc(0.18), doc(0.15)]
476
+ const mean = 0.29 // (0.62+0.22+0.18+0.15)/4 — below 0.35
477
+ expect(computeEvidenceState(docs, mean)).toBe('evidence_found')
478
+ })
479
+
480
+ it('strong mean still passes without a top hit', () => {
481
+ const docs = [doc(0.4), doc(0.38), doc(0.3)]
482
+ expect(computeEvidenceState(docs, 0.36)).toBe('evidence_found')
483
+ })
484
+
485
+ it('all-weak results stay incomplete', () => {
486
+ const docs = [doc(0.3), doc(0.25), doc(0.2)]
487
+ expect(computeEvidenceState(docs, 0.25)).toBe('incomplete')
488
+ })
489
+
490
+ it('a lone top hit below the decisive bar is still incomplete', () => {
491
+ const docs = [doc(0.45), doc(0.2), doc(0.18)]
492
+ expect(computeEvidenceState(docs, 0.27)).toBe('incomplete')
493
+ })
494
+
495
+ it('no docs → not_established', () => {
496
+ expect(computeEvidenceState([], 0)).toBe('not_established')
497
+ expect(computeEvidenceState(undefined, 0)).toBe('not_established')
498
+ })
499
+ })
500
+
501
+ describe('citation proof — hasInRangeCitationMarker + ensureInlineCitations', () => {
502
+ const sources = [{ document_id: 'a' }, { document_id: 'b' }]
503
+
504
+ it('an in-range marker is proof', () => {
505
+ expect(hasInRangeCitationMarker('The answer is X [1].', 2)).toBe(true)
506
+ })
507
+
508
+ it('out-of-range markers are not proof', () => {
509
+ expect(hasInRangeCitationMarker('See [9] for details.', 2)).toBe(false)
510
+ })
511
+
512
+ it('no markers is not proof (unlike extractCitedSources fallback)', () => {
513
+ expect(hasInRangeCitationMarker('A plain answer with no citations.', 2)).toBe(false)
514
+ })
515
+
516
+ it('ensureInlineCitations repair produces a provable marker', () => {
517
+ const repaired = ensureInlineCitations('A grounded answer with no markers.', sources)
518
+ expect(hasInRangeCitationMarker(repaired, sources.length)).toBe(true)
519
+ })
520
+ })