spine-framework 0.3.145 → 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 +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,642 @@
1
+ // CIL[BEGIN stable_id="~/.framework/tests/unit/runtime-isolation.test.ts:module_header:module_header" type="module_header" identity="module_header" parent="none"]
2
+ // CIL[DOC-MACRO purpose="Unit tests for framework runtime isolation: declared reply types, KB security levels, lineage config, generic record-flag filters, typed signals, account-driven rate limits, and the doctor wiring check"]
3
+ // CIL[DOC-RESPONSIBILITY responsibility="Verify agent-runner, kb, admin-data, funnel, rate-limit, and doctor read app-specific behavior from design_schema.functionality/accounts.data declarations instead of hardcoded custom-app knowledge"]
4
+ // CIL[DOC-CONTEXT module="tests/unit/runtime-isolation" layer="test-unit" related_files=".framework/functions/_shared/agent-runner.ts,.framework/functions/_shared/kb.ts,.framework/functions/admin-data.ts,.framework/functions/_shared/funnel.ts,.framework/functions/_shared/rate-limit.ts,.framework/cli/commands/doctor.ts"]
5
+ // CIL[DOC-CONTRACT inputs="none" outputs="vitest assertions" errors="none" side_effects="Installs vitest module mocks for db, audit, outcomes, llm, permissions, logging, trigger-engine, security, outbound, pipeline-runner, cli context, and the _shared barrel"]
6
+ // CIL[DOC-MICRO logic_summary="Mocks the data and side-effect layers, then exercises getReplyTypeSlug, resolveSecurityLevel, the admin-data get/attachLineage path, the list flag filters, emitSignal/accountDataFlag, resolveRateLimitPolicies, and checkRuntimeWiring" notable_decisions="admin-data handlers are invoked directly through createHandler's (event-with-principal) passthrough so no auth or HTTP plumbing is needed; funnel/rate-limit run for real against the mocked adminDb"]
7
+ // CIL[DOC-CONFIDENCE pass="first" score="0.85" flags="none"]
8
+ // CIL[DOC-STABILITY stability="stable"]
9
+ // CIL[DOC-AUDIENCE audience="test maintainers"]
10
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
11
+
12
+ vi.mock('../../functions/_shared/db.ts', () => ({
13
+ adminDb: { from: vi.fn(), rpc: vi.fn() },
14
+ getUserDb: vi.fn()
15
+ }))
16
+
17
+ vi.mock('../../functions/_shared/audit.ts', () => ({
18
+ emitAudit: vi.fn(async () => {}),
19
+ emitLog: vi.fn(async () => {}),
20
+ emitSpan: vi.fn(async () => {})
21
+ }))
22
+
23
+ vi.mock('../../functions/_shared/outcomes.ts', () => ({
24
+ emitOutcome: vi.fn(async () => {})
25
+ }))
26
+
27
+ vi.mock('../../functions/_shared/llm.ts', () => ({
28
+ callOpenAIChat: vi.fn(),
29
+ callOpenAIEmbeddings: vi.fn(async () => ({ data: [{ embedding: [0.1, 0.2] }] }))
30
+ }))
31
+
32
+ vi.mock('../../functions/_shared/permissions.ts', () => ({
33
+ sanitizeRecordData: vi.fn(async (_ctx: any, record: any) => record),
34
+ resolveFirstSurfacePermissions: vi.fn(async () => null),
35
+ validateUpdatePermissions: vi.fn(async () => ({ ok: true }))
36
+ }))
37
+
38
+ vi.mock('../../functions/_shared/logging.ts', () => ({
39
+ withAdminLogging: vi.fn((_ctx: any, _meta: any, fn: any) => fn()),
40
+ logAdminDataOperation: vi.fn(async () => {}),
41
+ resolveLogLevels: vi.fn(async () => []),
42
+ __resetLoggingSettingsCache: vi.fn()
43
+ }))
44
+
45
+ vi.mock('../../functions/_shared/trigger-engine.ts', () => ({
46
+ fireCreateTriggers: vi.fn(async () => {}),
47
+ fireUpdateTriggers: vi.fn(async () => {}),
48
+ fireDeleteTriggers: vi.fn(async () => {})
49
+ }))
50
+
51
+ vi.mock('../../functions/_shared/security.ts', () => ({
52
+ checkTypeSlugRateLimit: vi.fn(async () => {}),
53
+ isBlockedRequest: vi.fn(async () => null),
54
+ isBlockedIp: vi.fn(async () => false),
55
+ touchPersonGeo: vi.fn(async () => {})
56
+ }))
57
+
58
+ vi.mock('../../functions/integration-outbound.ts', () => ({ execute: vi.fn() }))
59
+ vi.mock('../../functions/_shared/pipeline-runner.ts', () => ({ runPipeline: vi.fn() }))
60
+ // funnel.ts (emitSignal/accountDataFlag) and rate-limit.ts are exercised for
61
+ // real — they only depend on adminDb (mocked) and trigger-engine (mocked).
62
+ vi.mock('../../cli/context.ts', () => ({
63
+ buildCliContext: vi.fn(),
64
+ handleError: vi.fn()
65
+ }))
66
+ vi.mock('../../functions/_shared/index.ts', async () => {
67
+ const db = await import('../../functions/_shared/db.ts')
68
+ return { adminDb: db.adminDb }
69
+ })
70
+
71
+ import { adminDb } from '../../functions/_shared/db.ts'
72
+ import { getReplyTypeSlug } from '../../functions/_shared/agent-runner.ts'
73
+ import { resolveSecurityLevel } from '../../functions/_shared/kb.ts'
74
+ import { list, get } from '../../functions/admin-data.ts'
75
+ import { emitSignal, accountDataFlag } from '../../functions/_shared/funnel.ts'
76
+ import { resolveRateLimitPolicies } from '../../functions/_shared/rate-limit.ts'
77
+ import { checkRuntimeWiring } from '../../cli/commands/doctor.ts'
78
+ // CIL[END stable_id="~/.framework/tests/unit/runtime-isolation.test.ts:module_header:module_header"]
79
+
80
+ // Chainable + thenable query-builder fake. Every chained call is recorded on
81
+ // _calls; terminal single()/maybeSingle() and any await resolve through the
82
+ // per-table responder.
83
+ function makeBuilder(table: string, respond: (table: string, calls: any[][]) => any, sink: any[][]) {
84
+ const builder: any = { _table: table, _calls: [] as any[][] }
85
+ const calls = builder._calls
86
+ for (const m of ['select', 'eq', 'neq', 'in', 'or', 'ilike', 'order', 'range', 'update', 'insert', 'upsert', 'delete', 'is', 'not', 'contains', 'textSearch', 'limit', 'gt', 'gte', 'lt', 'lte', 'filter']) {
87
+ builder[m] = (...args: any[]) => { calls.push([m, ...args]); return builder }
88
+ }
89
+ builder.single = () => { calls.push(['single']); return Promise.resolve(respond(table, calls)) }
90
+ builder.maybeSingle = () => { calls.push(['maybeSingle']); return Promise.resolve(respond(table, calls)) }
91
+ builder.then = (onF: any, onR: any) => Promise.resolve(respond(table, calls)).then(onF, onR)
92
+ sink.push(calls)
93
+ return builder
94
+ }
95
+
96
+ function makeDb(respond: (table: string, calls: any[][]) => any) {
97
+ const allCalls: any[][] = []
98
+ const from = vi.fn((table: string) => makeBuilder(table, respond, allCalls))
99
+ return { db: { from } as any, from, allCalls }
100
+ }
101
+
102
+ function makeCtx(db: any, query: Record<string, any> = {}) {
103
+ return {
104
+ requestId: 'req-test',
105
+ principal: {
106
+ id: 'person-1',
107
+ type: 'human',
108
+ accountId: 'acct-1',
109
+ isInternal: true,
110
+ roles: ['system_admin'],
111
+ provenance: { sourceType: 'jwt', createdBy: 'person-1', invokedAt: new Date().toISOString() }
112
+ },
113
+ accountId: 'acct-1',
114
+ db,
115
+ appId: 'app-1',
116
+ query
117
+ } as any
118
+ }
119
+
120
+ describe('getReplyTypeSlug (declared thread reply binding)', () => {
121
+ it('returns the declared reply_type_slug', () => {
122
+ const threadType = { slug: 'support_thread', design_schema: { functionality: { thread: { reply_type_slug: 'support_reply' } } } }
123
+ expect(getReplyTypeSlug(threadType)).toBe('support_reply')
124
+ })
125
+
126
+ it('honors app-specific slugs without hardcoding', () => {
127
+ const threadType = { slug: 'whatever', design_schema: { functionality: { thread: { reply_type_slug: 'x_reply' } } } }
128
+ expect(getReplyTypeSlug(threadType)).toBe('x_reply')
129
+ })
130
+
131
+ it('falls back to message when undeclared, null, or non-string', () => {
132
+ expect(getReplyTypeSlug({ slug: 'support_thread', design_schema: {} })).toBe('message')
133
+ expect(getReplyTypeSlug({ design_schema: { functionality: null } })).toBe('message')
134
+ expect(getReplyTypeSlug(null)).toBe('message')
135
+ expect(getReplyTypeSlug(undefined)).toBe('message')
136
+ expect(getReplyTypeSlug({ design_schema: { functionality: { thread: { reply_type_slug: 42 } } } })).toBe('message')
137
+ expect(getReplyTypeSlug({ design_schema: { functionality: { thread: { reply_type_slug: '' } } } })).toBe('message')
138
+ })
139
+ })
140
+
141
+ describe('resolveSecurityLevel (declared kb security level)', () => {
142
+ it('returns the declared functionality.kb.security_level', () => {
143
+ expect(resolveSecurityLevel({ functionality: { kb: { security_level: 'external' } } })).toBe('external')
144
+ expect(resolveSecurityLevel({ functionality: { kb: { security_level: 'restricted' } } })).toBe('restricted')
145
+ })
146
+
147
+ it('denies by default to internal when undeclared or malformed', () => {
148
+ expect(resolveSecurityLevel({})).toBe('internal')
149
+ expect(resolveSecurityLevel(null)).toBe('internal')
150
+ expect(resolveSecurityLevel(undefined)).toBe('internal')
151
+ expect(resolveSecurityLevel({ functionality: null })).toBe('internal')
152
+ expect(resolveSecurityLevel({ functionality: { kb: {} } })).toBe('internal')
153
+ expect(resolveSecurityLevel({ functionality: { kb: { security_level: 7 } } })).toBe('internal')
154
+ // A formerly-mapped slug with no declaration must NOT keep its old level
155
+ expect(resolveSecurityLevel({ functionality: { kb: {} } })).toBe('internal')
156
+ })
157
+ })
158
+
159
+ describe('attachLineage via admin-data get (declared lineage config)', () => {
160
+ const INITIATIVE_TYPE = {
161
+ id: 't-initiative',
162
+ slug: 'workbench_initiative',
163
+ kind: 'item',
164
+ design_schema: {
165
+ functionality: {
166
+ lineage: {
167
+ parent_fields: ['idea_id', 'discovery_id', 'initiative_id', 'epic_id', 'sprint_id'],
168
+ link_type: 'workbench_originated_from',
169
+ children: { workbench_epic: 'initiative_id', workbench_work_item: 'initiative_id' }
170
+ }
171
+ }
172
+ }
173
+ }
174
+ const PLAIN_TYPE = { id: 't-plain', slug: 'support_ticket', kind: 'item', design_schema: { functionality: null } }
175
+ const EPIC_TYPE = { id: 't-epic', slug: 'workbench_epic', kind: 'item', design_schema: {} }
176
+ const WI_TYPE = { id: 't-wi', slug: 'workbench_work_item', kind: 'item', design_schema: {} }
177
+ const DISCOVERY_TYPE = { id: 't-discovery', slug: 'workbench_discovery', kind: 'item', design_schema: {} }
178
+
179
+ function setup(opts: {
180
+ record: any
181
+ items?: any[]
182
+ types?: any[]
183
+ parentLinks?: any[]
184
+ childLinks?: any[]
185
+ itemsByField?: Record<string, Record<string, any[]>>
186
+ }) {
187
+ const itemsById: Record<string, any> = {}
188
+ for (const it of opts.items || []) itemsById[it.id] = it
189
+ itemsById[opts.record.id] = opts.record
190
+ const typesById: Record<string, any> = {}
191
+ for (const t of opts.types || []) typesById[t.id] = t
192
+ const allTypes = Object.values(typesById)
193
+ const itemsByField = opts.itemsByField || {}
194
+ const parentLinks = opts.parentLinks || []
195
+ const childLinks = opts.childLinks || []
196
+
197
+ vi.mocked(adminDb.from).mockImplementation((table: string) => {
198
+ const b = makeBuilder(table, (tbl, calls) => {
199
+ if (tbl === 'types') {
200
+ const eqId = calls.find(c => c[0] === 'eq' && c[1] === 'id')
201
+ const inIds = calls.find(c => c[0] === 'in' && c[1] === 'id')
202
+ const inSlugs = calls.find(c => c[0] === 'in' && c[1] === 'slug')
203
+ if (eqId && calls.some(c => c[0] === 'single')) {
204
+ return { data: typesById[eqId[2]] || null, error: null }
205
+ }
206
+ if (inIds) {
207
+ return { data: allTypes.filter(t => inIds[2].includes(t.id)).map(t => ({ id: t.id, slug: t.slug })), error: null }
208
+ }
209
+ if (inSlugs) {
210
+ return { data: allTypes.filter(t => inSlugs[2].includes(t.slug)).map(t => ({ id: t.id, kind: t.kind, slug: t.slug })), error: null }
211
+ }
212
+ return { data: null, error: null }
213
+ }
214
+ return { data: [], error: null }
215
+ }, [] as any)
216
+ return b
217
+ })
218
+
219
+ const ctxDb = makeDb((tbl, calls) => {
220
+ if (tbl === 'items') {
221
+ const fieldEq = calls.find(c => c[0] === 'eq' && typeof c[1] === 'string' && c[1].startsWith('data->>'))
222
+ const typeEq = calls.find(c => c[0] === 'eq' && c[1] === 'type_id')
223
+ const inIds = calls.find(c => c[0] === 'in' && c[1] === 'id')
224
+ const eqId = calls.find(c => c[0] === 'eq' && c[1] === 'id')
225
+ if (fieldEq) {
226
+ let rows = itemsByField[fieldEq[1]]?.[fieldEq[2]] || []
227
+ if (typeEq) rows = rows.filter((r: any) => r.type_id === typeEq[2])
228
+ return { data: rows, error: null }
229
+ }
230
+ if (inIds) return { data: (inIds[2] as string[]).map(id => itemsById[id]).filter(Boolean), error: null }
231
+ if (eqId && calls.some(c => c[0] === 'single')) return { data: itemsById[eqId[2]] || null, error: null }
232
+ return { data: [], error: null }
233
+ }
234
+ if (tbl === 'links') {
235
+ const sel = calls.find(c => c[0] === 'select')
236
+ const eqSource = calls.find(c => c[0] === 'eq' && c[1] === 'source_id')
237
+ const eqTarget = calls.find(c => c[0] === 'eq' && c[1] === 'target_id')
238
+ if (sel?.[1] === 'target_id') return { data: parentLinks.filter(l => l.source_id === eqSource?.[2]), error: null }
239
+ if (sel?.[1] === 'source_id') return { data: childLinks.filter(l => l.target_id === eqTarget?.[2]), error: null }
240
+ return { data: [], error: null }
241
+ }
242
+ return { data: [], error: null }
243
+ })
244
+ return ctxDb
245
+ }
246
+
247
+ it('walks parents via declared link_type first, then declared parent_fields', async () => {
248
+ const record = { id: 'r1', type_id: 't-initiative', data: { discovery_id: 'd-field' } }
249
+ const parent = { id: 'd-link', type_id: 't-discovery', data: {} }
250
+ const db = setup({
251
+ record,
252
+ items: [parent],
253
+ types: [INITIATIVE_TYPE, PLAIN_TYPE, EPIC_TYPE, WI_TYPE, DISCOVERY_TYPE],
254
+ parentLinks: [{ source_id: 'r1', target_id: 'd-link', link_type: 'workbench_originated_from' }],
255
+ childLinks: []
256
+ })
257
+ const result = await get(makeCtx(db.db, { entity: 'items', id: 'r1' }), null)
258
+
259
+ expect(result.parents.map((p: any) => p.id)).toEqual(['d-link'])
260
+ const linkCalls = db.allCalls.filter(c => c.some(x => x[0] === 'eq' && x[1] === 'link_type'))
261
+ expect(linkCalls.length).toBeGreaterThan(0)
262
+ expect(linkCalls[0].some(x => x[0] === 'eq' && x[1] === 'link_type' && x[2] === 'workbench_originated_from')).toBe(true)
263
+ })
264
+
265
+ it('falls back to declared parent_fields scan when no link edge exists', async () => {
266
+ const record = { id: 'r1', type_id: 't-initiative', data: { discovery_id: 'd-field' } }
267
+ const parent = { id: 'd-field', type_id: 't-discovery', data: {} }
268
+ const db = setup({
269
+ record,
270
+ items: [parent],
271
+ types: [INITIATIVE_TYPE, DISCOVERY_TYPE],
272
+ parentLinks: [],
273
+ childLinks: []
274
+ })
275
+ const result = await get(makeCtx(db.db, { entity: 'items', id: 'r1' }), null)
276
+ expect(result.parents.map((p: any) => p.id)).toEqual(['d-field'])
277
+ })
278
+
279
+ it('resolves children via declared children map fields and reverse links', async () => {
280
+ const record = { id: 'r1', type_id: 't-initiative', data: {} }
281
+ const epicChild = { id: 'e1', type_id: 't-epic', data: { initiative_id: 'r1' } }
282
+ const wiChild = { id: 'w1', type_id: 't-wi', data: { initiative_id: 'r1' } }
283
+ const linkChild = { id: 'l1', type_id: 't-discovery', data: {} }
284
+ const db = setup({
285
+ record,
286
+ items: [epicChild, wiChild, linkChild],
287
+ types: [INITIATIVE_TYPE, EPIC_TYPE, WI_TYPE, DISCOVERY_TYPE],
288
+ childLinks: [{ source_id: 'l1', target_id: 'r1', link_type: 'workbench_originated_from' }],
289
+ itemsByField: { 'data->>initiative_id': { r1: [epicChild, wiChild] } }
290
+ })
291
+ const result = await get(makeCtx(db.db, { entity: 'items', id: 'r1' }), null)
292
+ expect(new Set(result.children.map((c: any) => c.id))).toEqual(new Set(['e1', 'w1', 'l1']))
293
+ // child field lookups used the declared field per child type
294
+ const fieldEqs = db.allCalls.flat().filter(c => c[0] === 'eq' && c[1] === 'data->>initiative_id')
295
+ expect(fieldEqs.length).toBe(2)
296
+ })
297
+
298
+ it('attaches empty parents/children and skips link queries when the type declares no lineage', async () => {
299
+ const record = { id: 'r2', type_id: 't-plain', data: {} }
300
+ const db = setup({
301
+ record,
302
+ items: [],
303
+ types: [PLAIN_TYPE],
304
+ // even with a matching link present, no link_type is declared so no query runs
305
+ childLinks: [{ source_id: 'x', target_id: 'r2', link_type: 'workbench_originated_from' }]
306
+ })
307
+ const result = await get(makeCtx(db.db, { entity: 'items', id: 'r2' }), null)
308
+ expect(result.parents).toEqual([])
309
+ expect(result.children).toEqual([])
310
+ expect(result.type_slug).toBe('support_ticket')
311
+ expect(db.from.mock.calls.filter((c: any[]) => c[0] === 'links').length).toBe(0)
312
+ })
313
+ })
314
+
315
+ describe('list flag filters (generic record flags)', () => {
316
+ function setupListDb() {
317
+ return makeDb((_tbl, calls) => {
318
+ const sel = calls.find(c => c[0] === 'select')
319
+ if (sel?.[2]?.count === 'exact') return { count: 0, error: null }
320
+ return { data: [], error: null }
321
+ })
322
+ }
323
+
324
+ // list uses adminDb for internal principals — use a member principal so the
325
+ // exercised query builder is ctx.db.
326
+ function memberCtx(db: any, query: Record<string, any>) {
327
+ const ctx = makeCtx(db, query)
328
+ ctx.principal.isInternal = false
329
+ ctx.principal.roles = ['member']
330
+ return ctx
331
+ }
332
+
333
+ it('flag_only filters data->>field = true on flaggable entities', async () => {
334
+ const db = setupListDb()
335
+ await list(memberCtx(db.db, { entity: 'items', flag_only: 'is_demo' }), null)
336
+ const eqCalls = db.allCalls.flat().filter(c => c[0] === 'eq' && c[1] === 'data->>is_demo' && c[2] === 'true')
337
+ expect(eqCalls.length).toBe(1)
338
+ })
339
+
340
+ it('exclude_flag filters flagged rows out (null counts as unset)', async () => {
341
+ const db = setupListDb()
342
+ await list(memberCtx(db.db, { entity: 'items', exclude_flag: 'is_demo' }), null)
343
+ const orCalls = db.allCalls.flat().filter(c => c[0] === 'or' && c[1] === 'data->>is_demo.is.null,data->>is_demo.eq.false')
344
+ expect(orCalls.length).toBe(1)
345
+ })
346
+
347
+ it('rejects invalid flag field names with a 400 error', async () => {
348
+ const db = setupListDb()
349
+ await expect(list(memberCtx(db.db, { entity: 'items', flag_only: "x';drop" }), null)).rejects.toThrow('Invalid flag field name')
350
+ await expect(list(memberCtx(db.db, { entity: 'items', exclude_flag: 'Bad-Field' }), null)).rejects.toThrow('Invalid flag field name')
351
+ await expect(list(memberCtx(db.db, { entity: 'items', exclude_flag: '1field' }), null)).rejects.toThrow('Invalid flag field name')
352
+ })
353
+
354
+ it('ignores flag params on non-flaggable entities', async () => {
355
+ const db = setupListDb()
356
+ await list(memberCtx(db.db, { entity: 'accounts', exclude_flag: 'is_demo' }), null)
357
+ const orCalls = db.allCalls.flat().filter(c => c[0] === 'or' && String(c[1]).includes('data->>'))
358
+ const eqCalls = db.allCalls.flat().filter(c => c[0] === 'eq' && String(c[1]).startsWith('data->>'))
359
+ expect(orCalls.length).toBe(0)
360
+ expect(eqCalls.length).toBe(0)
361
+ })
362
+ })
363
+
364
+ describe('emitSignal (generic typed signal)', () => {
365
+ const ctx = { accountId: 'acct-1' } as any
366
+
367
+ function signalDb(opts: { typeId?: string | null; existing?: any[]; insertError?: string }) {
368
+ return makeDb((table, calls) => {
369
+ if (table === 'types') return { data: opts.typeId ? { id: opts.typeId } : null, error: null }
370
+ if (table === 'items') {
371
+ if (calls.some(c => c[0] === 'insert')) {
372
+ return opts.insertError
373
+ ? { data: null, error: { message: opts.insertError } }
374
+ : { data: { id: 'sig-1' }, error: null }
375
+ }
376
+ return { data: opts.existing ?? [], error: null }
377
+ }
378
+ return { data: null, error: null }
379
+ })
380
+ }
381
+
382
+ it('resolves the declared type by (kind=item, slug) and inserts the signal', async () => {
383
+ const db = signalDb({ typeId: 't-sig' })
384
+ vi.mocked(adminDb.from).mockImplementation(db.from)
385
+
386
+ const created = await emitSignal(ctx, { type_slug: 'custom_signal', action: 'my_action', data: { source: 'x' } })
387
+ expect(created).toEqual({ id: 'sig-1' })
388
+
389
+ const flat = db.allCalls.flat()
390
+ expect(flat.some(c => c[0] === 'eq' && c[1] === 'kind' && c[2] === 'item')).toBe(true)
391
+ expect(flat.some(c => c[0] === 'eq' && c[1] === 'slug' && c[2] === 'custom_signal')).toBe(true)
392
+ const insert = flat.find(c => c[0] === 'insert')
393
+ expect(insert?.[1]?.data?.action_type).toBe('my_action')
394
+ expect(insert?.[1]?.account_id).toBe('acct-1')
395
+ })
396
+
397
+ it('dedupes on (account_id, data.action_type) by default', async () => {
398
+ const db = signalDb({ typeId: 't-sig', existing: [{ id: 'prior' }] })
399
+ vi.mocked(adminDb.from).mockImplementation(db.from)
400
+
401
+ const result = await emitSignal(ctx, { type_slug: 'custom_signal', action: 'my_action' })
402
+ expect(result).toBeNull()
403
+ const flat = db.allCalls.flat()
404
+ expect(flat.some(c => c[0] === 'eq' && c[1] === 'data->>action_type' && c[2] === 'my_action')).toBe(true)
405
+ expect(flat.some(c => c[0] === 'eq' && c[1] === 'account_id' && c[2] === 'acct-1')).toBe(true)
406
+ expect(flat.some(c => c[0] === 'insert')).toBe(false)
407
+ })
408
+
409
+ it('dedupe:false skips the dedupe read and always inserts', async () => {
410
+ const db = signalDb({ typeId: 't-sig', existing: [{ id: 'prior' }] })
411
+ vi.mocked(adminDb.from).mockImplementation(db.from)
412
+
413
+ const created = await emitSignal(ctx, { type_slug: 'custom_signal', action: 'my_action', dedupe: false })
414
+ expect(created).toEqual({ id: 'sig-1' })
415
+ const flat = db.allCalls.flat()
416
+ expect(flat.some(c => c[0] === 'eq' && c[1] === 'data->>action_type')).toBe(false)
417
+ expect(flat.some(c => c[0] === 'insert')).toBe(true)
418
+ })
419
+
420
+ it('returns null when the type slug does not exist', async () => {
421
+ const db = signalDb({ typeId: null })
422
+ vi.mocked(adminDb.from).mockImplementation(db.from)
423
+
424
+ const result = await emitSignal(ctx, { type_slug: 'missing_type', action: 'my_action' })
425
+ expect(result).toBeNull()
426
+ expect(db.from.mock.calls.filter((c: any[]) => c[0] === 'items').length).toBe(0)
427
+ })
428
+
429
+ it('never throws — insert failures and DB errors return null', async () => {
430
+ const db = signalDb({ typeId: 't-sig', insertError: 'insert blew up' })
431
+ vi.mocked(adminDb.from).mockImplementation(db.from)
432
+ await expect(emitSignal(ctx, { type_slug: 'custom_signal', action: 'a' })).resolves.toBeNull()
433
+
434
+ vi.mocked(adminDb.from).mockImplementation(() => { throw new Error('db down') })
435
+ await expect(emitSignal(ctx, { type_slug: 'custom_signal', action: 'a' })).resolves.toBeNull()
436
+ })
437
+ })
438
+
439
+ describe('accountDataFlag (generic account flag read)', () => {
440
+ const ctx = { accountId: 'acct-1' } as any
441
+
442
+ function acctDb(data: any) {
443
+ return makeDb((table) =>
444
+ table === 'accounts' ? { data, error: null } : { data: null, error: null })
445
+ }
446
+
447
+ it('returns true only when accounts.data[flag] === true', async () => {
448
+ let db = acctDb({ data: { is_demo: true, other: false } })
449
+ vi.mocked(adminDb.from).mockImplementation(db.from)
450
+ await expect(accountDataFlag(ctx, 'is_demo')).resolves.toBe(true)
451
+ await expect(accountDataFlag(ctx, 'other')).resolves.toBe(false)
452
+
453
+ db = acctDb({ data: {} })
454
+ vi.mocked(adminDb.from).mockImplementation(db.from)
455
+ await expect(accountDataFlag(ctx, 'is_demo')).resolves.toBe(false)
456
+
457
+ db = acctDb(null)
458
+ vi.mocked(adminDb.from).mockImplementation(db.from)
459
+ await expect(accountDataFlag(ctx, 'is_demo')).resolves.toBe(false)
460
+ })
461
+
462
+ it('returns false on DB error (never throws)', async () => {
463
+ vi.mocked(adminDb.from).mockImplementation(() => { throw new Error('db down') })
464
+ await expect(accountDataFlag(ctx, 'is_demo')).resolves.toBe(false)
465
+ })
466
+ })
467
+
468
+ describe('resolveRateLimitPolicies (account-declared policy)', () => {
469
+ function policyDb(opts: { acctData?: any; appConfig?: any }) {
470
+ return makeDb((table) => {
471
+ if (table === 'accounts') {
472
+ return { data: opts.acctData === undefined ? null : { data: opts.acctData }, error: null }
473
+ }
474
+ if (table === 'apps') {
475
+ return { data: opts.appConfig === undefined ? null : { config: opts.appConfig }, error: null }
476
+ }
477
+ return { data: null, error: null }
478
+ })
479
+ }
480
+
481
+ it('account-declared rate_limits override wins and skips the apps read', async () => {
482
+ const db = policyDb({
483
+ acctData: { rate_limits: { agent_step: [{ limit: 5, window_seconds: 60 }] } },
484
+ appConfig: { rate_limits: { agent_step: [{ limit: 999, window_seconds: 1 }] } },
485
+ })
486
+ vi.mocked(adminDb.from).mockImplementation(db.from)
487
+
488
+ const policies = await resolveRateLimitPolicies({ accountId: 'acct-1', appId: 'app-1' } as any, 'agent_step')
489
+ expect(policies).toEqual([{ limit: 5, window_seconds: 60 }])
490
+ expect(db.from.mock.calls.filter((c: any[]) => c[0] === 'apps').length).toBe(0)
491
+ })
492
+
493
+ it('rate_limit_profile selects the named profile inside the calling app config', async () => {
494
+ const db = policyDb({
495
+ acctData: { rate_limit_profile: 'lite' },
496
+ appConfig: { rate_limits: {
497
+ profiles: { lite: { agent_step: [{ limit: 10, window_seconds: 120 }] } },
498
+ agent_step: [{ limit: 999, window_seconds: 1 }],
499
+ } },
500
+ })
501
+ vi.mocked(adminDb.from).mockImplementation(db.from)
502
+
503
+ const policies = await resolveRateLimitPolicies({ accountId: 'acct-1', appId: 'app-1' } as any, 'agent_step')
504
+ expect(policies).toEqual([{ limit: 10, window_seconds: 120 }])
505
+ })
506
+
507
+ it('profile falls back to the app base policy when the profile lacks the action', async () => {
508
+ const db = policyDb({
509
+ acctData: { rate_limit_profile: 'lite' },
510
+ appConfig: { rate_limits: {
511
+ profiles: { lite: {} },
512
+ agent_step: [{ limit: 30, window_seconds: 60 }],
513
+ } },
514
+ })
515
+ vi.mocked(adminDb.from).mockImplementation(db.from)
516
+
517
+ const policies = await resolveRateLimitPolicies({ accountId: 'acct-1', appId: 'app-1' } as any, 'agent_step')
518
+ expect(policies).toEqual([{ limit: 30, window_seconds: 60 }])
519
+ })
520
+
521
+ it('plain accounts use the calling app base policy and query exactly one app slug', async () => {
522
+ const db = policyDb({
523
+ acctData: {},
524
+ appConfig: { rate_limits: { agent_step: [{ limit: 200, window_seconds: 3600 }] } },
525
+ })
526
+ vi.mocked(adminDb.from).mockImplementation(db.from)
527
+
528
+ const policies = await resolveRateLimitPolicies({ accountId: 'acct-1', appId: 'app-1' } as any, 'agent_step')
529
+ expect(policies).toEqual([{ limit: 200, window_seconds: 3600 }])
530
+ const appsCalls = db.allCalls.filter(c => c.some(x => x[0] === 'eq' && x[1] === 'slug'))
531
+ const flat = db.allCalls.flat()
532
+ const slugEqs = flat.filter(c => c[0] === 'eq' && c[1] === 'slug').map(c => c[2])
533
+ expect(slugEqs).toEqual(['app-1'])
534
+ // exactly one apps query — no second lookup for a named app
535
+ const appsQueries = db.from.mock.calls.filter((c: any[]) => c[0] === 'apps')
536
+ expect(appsQueries.length).toBe(1)
537
+ void appsCalls
538
+ })
539
+
540
+ it('returns [] when nothing is configured and on lookup failure', async () => {
541
+ const db = policyDb({ acctData: {}, appConfig: {} })
542
+ vi.mocked(adminDb.from).mockImplementation(db.from)
543
+ await expect(resolveRateLimitPolicies({ accountId: 'acct-1', appId: 'app-1' } as any, 'agent_step'))
544
+ .resolves.toEqual([])
545
+
546
+ vi.mocked(adminDb.from).mockImplementation(() => { throw new Error('db down') })
547
+ await expect(resolveRateLimitPolicies({ accountId: 'acct-1', appId: 'app-1' } as any, 'agent_step'))
548
+ .resolves.toEqual([])
549
+ })
550
+ })
551
+
552
+ describe('doctor checkRuntimeWiring (generic referential integrity)', () => {
553
+ function wiringDb(opts: { triggers?: any[]; types?: any[]; pipelines?: any[]; actions?: any[] }) {
554
+ return makeDb((table) => {
555
+ if (table === 'triggers') return { data: opts.triggers ?? [], error: null }
556
+ if (table === 'types') return { data: opts.types ?? [], error: null }
557
+ if (table === 'pipelines') return { data: opts.pipelines ?? [], error: null }
558
+ if (table === 'actions') return { data: opts.actions ?? [], error: null }
559
+ return { data: [], error: null }
560
+ })
561
+ }
562
+
563
+ it('passes when every declared reference resolves', async () => {
564
+ const db = wiringDb({
565
+ triggers: [{ id: 't1', slug: 'trig-a', event_type: 'item_updated', pipeline_id: 'p1',
566
+ config: { type_slug: 'kb_article', entity_type: 'items' } }],
567
+ types: [{ slug: 'kb_article', kind: 'item' }],
568
+ pipelines: [{ id: 'p1', slug: 'embed', is_active: true, stages: [{ stage_type: 'update_item' }] }],
569
+ actions: [{ slug: 'update_item' }],
570
+ })
571
+ vi.mocked(adminDb.from).mockImplementation(db.from)
572
+
573
+ const res = await checkRuntimeWiring()
574
+ expect(res.name).toBe('runtime_wiring')
575
+ expect(res.status).toBe('pass')
576
+ })
577
+
578
+ it('reports dangling type, pipeline, and stage references', async () => {
579
+ const db = wiringDb({
580
+ triggers: [
581
+ { id: 't1', slug: 'trig-a', event_type: 'item_updated', pipeline_id: 'p-missing',
582
+ config: { type_slug: 'ghost_type', entity_type: 'items' } },
583
+ { id: 't2', slug: 'trig-b', event_type: 'item_created', pipeline_id: null,
584
+ config: { pipeline_slug: 'ghost_pipe' } },
585
+ { id: 't3', slug: 'trig-c', event_type: 'item_created', pipeline_id: 'p-inactive', config: {} },
586
+ ],
587
+ types: [{ slug: 'kb_article', kind: 'item' }],
588
+ pipelines: [
589
+ { id: 'p1', slug: 'good', is_active: true, stages: [{ stage_type: 'update_item' }, { stage_type: 'ghost_action' }] },
590
+ { id: 'p-inactive', slug: 'old', is_active: false, stages: [] },
591
+ ],
592
+ actions: [{ slug: 'update_item' }],
593
+ })
594
+ vi.mocked(adminDb.from).mockImplementation(db.from)
595
+
596
+ const res = await checkRuntimeWiring()
597
+ expect(res.name).toBe('runtime_wiring')
598
+ expect(res.status).toBe('fail')
599
+ const problems: string[] = res.details?.problems ?? []
600
+ expect(problems.some(p => p === "trigger 'trig-a': type 'ghost_type' not found")).toBe(true)
601
+ expect(problems.some(p => p.includes("pipeline_id 'p-missing' not found"))).toBe(true)
602
+ expect(problems.some(p => p === "trigger 'trig-b': pipeline 'ghost_pipe' not found")).toBe(true)
603
+ expect(problems.some(p => p === "trigger 'trig-c': pipeline 'old' is inactive")).toBe(true)
604
+ expect(problems.some(p => p === "pipeline 'good': stage 1 references missing action 'ghost_action'")).toBe(true)
605
+ })
606
+
607
+ it('scopes the type check by declared kind/entity when present', async () => {
608
+ const db = wiringDb({
609
+ triggers: [{ id: 't1', slug: 'trig-x', event_type: 'item_updated', pipeline_id: null,
610
+ config: { type_slug: 'kb_article', entity_type: 'threads' } }],
611
+ types: [{ slug: 'kb_article', kind: 'item' }],
612
+ pipelines: [],
613
+ actions: [],
614
+ })
615
+ vi.mocked(adminDb.from).mockImplementation(db.from)
616
+
617
+ const res = await checkRuntimeWiring()
618
+ expect(res.status).toBe('fail')
619
+ expect(res.details?.problems?.[0]).toBe("trigger 'trig-x': type 'kb_article' not found")
620
+ })
621
+
622
+ it('flags active pipelines with missing or empty stages but ignores inactive and staged ones', async () => {
623
+ const db = wiringDb({
624
+ pipelines: [
625
+ { id: 'p-empty', slug: 'noop', is_active: true, stages: [] },
626
+ { id: 'p-null', slug: 'no_stages_field', is_active: true },
627
+ { id: 'p-inactive', slug: 'off', is_active: false, stages: [] },
628
+ { id: 'p-ok', slug: 'good', is_active: true, stages: [{ stage_type: 'update_item' }] },
629
+ ],
630
+ actions: [{ slug: 'update_item' }],
631
+ })
632
+ vi.mocked(adminDb.from).mockImplementation(db.from)
633
+
634
+ const res = await checkRuntimeWiring()
635
+ expect(res.status).toBe('fail')
636
+ const problems: string[] = res.details?.problems ?? []
637
+ expect(problems).toContain("pipeline 'noop' is active but has no stages (completes without doing anything)")
638
+ expect(problems).toContain("pipeline 'no_stages_field' is active but has no stages (completes without doing anything)")
639
+ expect(problems.some(p => p.includes("'off'"))).toBe(false)
640
+ expect(problems.some(p => p.includes("'good'"))).toBe(false)
641
+ })
642
+ })