spine-framework 0.3.124 → 0.3.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/.framework/cli/commands/agents.ts +35 -37
  2. package/.framework/cli/commands/auth.ts +35 -34
  3. package/.framework/cli/commands/create-app.ts +104 -40
  4. package/.framework/cli/commands/dev.ts +104 -69
  5. package/.framework/cli/commands/doctor.ts +179 -69
  6. package/.framework/cli/commands/generate.ts +83 -143
  7. package/.framework/cli/commands/init.ts +72 -34
  8. package/.framework/cli/commands/install-app.ts +260 -125
  9. package/.framework/cli/commands/items.ts +33 -44
  10. package/.framework/cli/commands/migrate.ts +122 -40
  11. package/.framework/cli/commands/migrations.ts +33 -40
  12. package/.framework/cli/commands/pipelines.ts +31 -39
  13. package/.framework/cli/commands/status.ts +47 -23
  14. package/.framework/cli/commands/system.ts +47 -37
  15. package/.framework/cli/commands/test.ts +112 -68
  16. package/.framework/cli/commands/validate-app.ts +215 -121
  17. package/.framework/cli/context.ts +112 -130
  18. package/.framework/cli/env-loader.ts +47 -18
  19. package/.framework/cli/index.ts +81 -42
  20. package/.framework/docs/runtime-types.md +1024 -0
  21. package/.framework/functions/_shared/agent-runner.ts +1296 -961
  22. package/.framework/functions/_shared/app-manifest.ts +82 -68
  23. package/.framework/functions/_shared/audit.ts +135 -83
  24. package/.framework/functions/_shared/{cil-state.ts → cil.ts} +158 -56
  25. package/.framework/functions/_shared/db.ts +80 -126
  26. package/.framework/functions/_shared/hash-api-key.ts +63 -0
  27. package/.framework/functions/_shared/index.ts +224 -335
  28. package/.framework/functions/_shared/kb.ts +319 -0
  29. package/.framework/functions/_shared/llm.ts +92 -0
  30. package/.framework/functions/_shared/logging.ts +288 -0
  31. package/.framework/functions/_shared/middleware.ts +329 -365
  32. package/.framework/functions/_shared/outcomes.ts +36 -0
  33. package/.framework/functions/_shared/permissions.ts +359 -643
  34. package/.framework/functions/_shared/pipeline-runner.ts +243 -316
  35. package/.framework/functions/_shared/principal.ts +376 -468
  36. package/.framework/functions/_shared/resolve-credentials.ts +102 -0
  37. package/.framework/functions/_shared/resolve-ids.ts +188 -16
  38. package/.framework/functions/_shared/retry-deliveries.ts +104 -0
  39. package/.framework/functions/_shared/schema-utils.ts +611 -373
  40. package/.framework/functions/_shared/testing.ts +207 -137
  41. package/.framework/functions/_shared/tracing.ts +276 -0
  42. package/.framework/functions/_shared/trigger-engine.ts +159 -211
  43. package/.framework/functions/_shared/webhook-registration.ts +95 -81
  44. package/.framework/functions/_shared/webhook-registry.ts +153 -75
  45. package/.framework/functions/account-nodes.ts +31 -53
  46. package/.framework/functions/adapters/generic-webhook-inbound.ts +56 -0
  47. package/.framework/functions/adapters/generic-webhook-outbound.ts +43 -0
  48. package/.framework/functions/adapters/openai-outbound.ts +40 -0
  49. package/.framework/functions/admin-data.ts +685 -537
  50. package/.framework/functions/admin-funnel.ts +31 -20
  51. package/.framework/functions/admin-install-funnel.ts +47 -28
  52. package/.framework/functions/admin-signals.ts +47 -29
  53. package/.framework/functions/ai-agents.ts +122 -147
  54. package/.framework/functions/ai-interactions.ts +180 -90
  55. package/.framework/functions/api-keys.ts +250 -192
  56. package/.framework/functions/apps.ts +203 -197
  57. package/.framework/functions/auth.ts +98 -91
  58. package/.framework/functions/cil-agent-ingest.ts +773 -0
  59. package/.framework/functions/cil-agent.ts +592 -0
  60. package/.framework/functions/cil-callmap.ts +1227 -0
  61. package/.framework/functions/cil-config.ts +583 -0
  62. package/.framework/functions/cil-context.ts +445 -0
  63. package/.framework/functions/cil-doc.ts +1752 -0
  64. package/.framework/functions/cil-embeddings.ts +1202 -0
  65. package/.framework/functions/cil-graph.ts +585 -0
  66. package/.framework/functions/cil-ingest.ts +2416 -0
  67. package/.framework/functions/cil-pipeline.ts +318 -0
  68. package/.framework/functions/cil-project.ts +499 -0
  69. package/.framework/functions/cil-validate.ts +469 -0
  70. package/.framework/functions/debug-auth.ts +73 -53
  71. package/.framework/functions/deliveries.ts +117 -0
  72. package/.framework/functions/embeddings.ts +528 -516
  73. package/.framework/functions/eval-harness.ts +158 -106
  74. package/.framework/functions/integration-outbound.ts +311 -0
  75. package/.framework/functions/integration-routes.ts +240 -466
  76. package/.framework/functions/integrations.ts +132 -140
  77. package/.framework/functions/invites.ts +401 -201
  78. package/.framework/functions/item-progress.ts +113 -53
  79. package/.framework/functions/kb.ts +28 -0
  80. package/.framework/functions/logs.ts +239 -222
  81. package/.framework/functions/observability.ts +308 -259
  82. package/.framework/functions/pipeline-executions.ts +179 -235
  83. package/.framework/functions/pipelines.ts +163 -205
  84. package/.framework/functions/prompt-configs.ts +109 -156
  85. package/.framework/functions/roles.ts +97 -147
  86. package/.framework/functions/system-cron.ts +132 -218
  87. package/.framework/functions/system.ts +112 -52
  88. package/.framework/functions/tests.ts +32 -28
  89. package/.framework/functions/timers.ts +162 -210
  90. package/.framework/functions/triggers.ts +282 -346
  91. package/.framework/functions/types.ts +283 -400
  92. package/.framework/migrations/001_core_schema.sql +630 -0
  93. package/.framework/migrations/002_core_rls_and_functions.sql +532 -0
  94. package/.framework/migrations/{003_system_data.sql → 003_core_system_data.sql} +26 -153
  95. package/.framework/migrations/004_core_rpc_and_broker.sql +421 -0
  96. package/.framework/migrations/005_core_observability.sql +598 -0
  97. package/.framework/migrations/006_security_hardening_rls.sql +97 -0
  98. package/.framework/migrations/007_kb_article_list.sql +237 -0
  99. package/.framework/migrations/008_items_target.sql +8 -0
  100. package/.framework/migrations/009_roles_slug_unique.sql +11 -0
  101. package/.framework/public/spine.config.json +91 -0
  102. package/.framework/scripts/app-install-cli.ts +112 -98
  103. package/.framework/src/App.tsx +67 -20
  104. package/.framework/src/apps/admin/index.tsx +97 -27
  105. package/.framework/src/components/AppWrapper.tsx +50 -11
  106. package/.framework/src/components/CustomAppLoader.tsx +131 -22
  107. package/.framework/src/components/admin/AdminListPage.tsx +75 -8
  108. package/.framework/src/components/admin/AdminSidebar.tsx +148 -36
  109. package/.framework/src/components/admin/AdminStatsCard.tsx +48 -29
  110. package/.framework/src/components/admin/PageInstructions.tsx +49 -0
  111. package/.framework/src/components/admin/SortableTableHeader.tsx +49 -2
  112. package/.framework/src/components/app-shell/GenericAppShell.tsx +69 -11
  113. package/.framework/src/components/app-shell/GenericDetailPage.tsx +55 -10
  114. package/.framework/src/components/app-shell/GenericListPage.tsx +76 -9
  115. package/.framework/src/components/app-sidebar.tsx +50 -3
  116. package/.framework/src/components/auth/ProtectedRoute.tsx +53 -45
  117. package/.framework/src/components/kb/KBArticleEditor.tsx +409 -0
  118. package/.framework/src/components/kb/KBArticleList.tsx +557 -0
  119. package/.framework/src/components/layout/AppShell.tsx +54 -16
  120. package/.framework/src/components/layout/Header.tsx +50 -38
  121. package/.framework/src/components/layout/Layout.tsx +48 -32
  122. package/.framework/src/components/layout/Sidebar.tsx +167 -16
  123. package/.framework/src/components/runtime/DataDetailHeader.tsx +33 -1
  124. package/.framework/src/components/runtime/DataDetailPage.tsx +289 -4
  125. package/.framework/src/components/runtime/DataFilters.tsx +78 -1
  126. package/.framework/src/components/runtime/DataHeader.tsx +32 -29
  127. package/.framework/src/components/runtime/DataListPage.tsx +161 -2
  128. package/.framework/src/components/runtime/DataStats.tsx +48 -1
  129. package/.framework/src/components/runtime/DataTable.tsx +153 -1
  130. package/.framework/src/components/runtime/SchemaDetailForm.tsx +77 -46
  131. package/.framework/src/components/runtime/index.ts +16 -9
  132. package/.framework/src/components/search-form.tsx +33 -1
  133. package/.framework/src/components/shared/AgentView.tsx +48 -51
  134. package/.framework/src/components/shared/FieldRenderer.tsx +84 -67
  135. package/.framework/src/components/shared/InviteManager.tsx +113 -52
  136. package/.framework/src/components/shared/MemberList.tsx +182 -0
  137. package/.framework/src/components/shared/SchemaFields.tsx +96 -78
  138. package/.framework/src/components/ui/DataTable.tsx +86 -56
  139. package/.framework/src/components/ui/Form.tsx +188 -109
  140. package/.framework/src/components/ui/ItemCard.tsx +86 -58
  141. package/.framework/src/components/ui/ItemListView.tsx +88 -61
  142. package/.framework/src/components/ui/LoadingSpinner.tsx +52 -27
  143. package/.framework/src/components/ui/Modal.tsx +81 -4
  144. package/.framework/src/components/ui/RichTextEditor.tsx +101 -9
  145. package/.framework/src/components/ui/accordion.tsx +97 -5
  146. package/.framework/src/components/ui/alert-dialog.tsx +225 -13
  147. package/.framework/src/components/ui/alert.tsx +113 -6
  148. package/.framework/src/components/ui/aspect-ratio.tsx +49 -2
  149. package/.framework/src/components/ui/avatar.tsx +129 -7
  150. package/.framework/src/components/ui/badge.tsx +65 -3
  151. package/.framework/src/components/ui/breadcrumb.tsx +145 -8
  152. package/.framework/src/components/ui/button-group.tsx +81 -4
  153. package/.framework/src/components/ui/button.tsx +49 -2
  154. package/.framework/src/components/ui/calendar.tsx +65 -3
  155. package/.framework/src/components/ui/card.tsx +145 -8
  156. package/.framework/src/components/ui/carousel.tsx +129 -7
  157. package/.framework/src/components/ui/chart.tsx +161 -10
  158. package/.framework/src/components/ui/checkbox.tsx +49 -2
  159. package/.framework/src/components/ui/collapsible.tsx +81 -5
  160. package/.framework/src/components/ui/combobox.tsx +305 -20
  161. package/.framework/src/components/ui/command.tsx +177 -11
  162. package/.framework/src/components/ui/context-menu.tsx +273 -17
  163. package/.framework/src/components/ui/dialog.tsx +209 -27
  164. package/.framework/src/components/ui/direction.tsx +64 -4
  165. package/.framework/src/components/ui/drawer.tsx +193 -12
  166. package/.framework/src/components/ui/dropdown-menu.tsx +273 -18
  167. package/.framework/src/components/ui/empty.tsx +145 -9
  168. package/.framework/src/components/ui/field.tsx +209 -14
  169. package/.framework/src/components/ui/hover-card.tsx +81 -4
  170. package/.framework/src/components/ui/input-group.tsx +162 -10
  171. package/.framework/src/components/ui/input-otp.tsx +98 -5
  172. package/.framework/src/components/ui/input.tsx +49 -2
  173. package/.framework/src/components/ui/item.tsx +225 -13
  174. package/.framework/src/components/ui/kbd.tsx +65 -3
  175. package/.framework/src/components/ui/label.tsx +49 -2
  176. package/.framework/src/components/ui/menubar.tsx +289 -17
  177. package/.framework/src/components/ui/native-select.tsx +97 -5
  178. package/.framework/src/components/ui/navigation-menu.tsx +177 -10
  179. package/.framework/src/components/ui/pagination.tsx +161 -9
  180. package/.framework/src/components/ui/popover.tsx +145 -8
  181. package/.framework/src/components/ui/progress.tsx +49 -2
  182. package/.framework/src/components/ui/radio-group.tsx +65 -3
  183. package/.framework/src/components/ui/resizable.tsx +82 -4
  184. package/.framework/src/components/ui/scroll-area.tsx +65 -3
  185. package/.framework/src/components/ui/select.tsx +193 -11
  186. package/.framework/src/components/ui/separator.tsx +49 -2
  187. package/.framework/src/components/ui/sheet.tsx +193 -11
  188. package/.framework/src/components/ui/sidebar.tsx +481 -38
  189. package/.framework/src/components/ui/skeleton.tsx +49 -2
  190. package/.framework/src/components/ui/slider.tsx +49 -2
  191. package/.framework/src/components/ui/sonner.tsx +49 -2
  192. package/.framework/src/components/ui/spinner.tsx +49 -2
  193. package/.framework/src/components/ui/switch.tsx +49 -2
  194. package/.framework/src/components/ui/table-primitives.tsx +160 -33
  195. package/.framework/src/components/ui/table.tsx +161 -9
  196. package/.framework/src/components/ui/tabs.tsx +113 -6
  197. package/.framework/src/components/ui/textarea.tsx +49 -2
  198. package/.framework/src/components/ui/toggle-group.tsx +81 -4
  199. package/.framework/src/components/ui/toggle.tsx +65 -3
  200. package/.framework/src/components/ui/tooltip.tsx +97 -5
  201. package/.framework/src/contexts/AppContext.tsx +129 -45
  202. package/.framework/src/contexts/AuthContext.tsx +187 -144
  203. package/.framework/src/hooks/use-mobile.ts +48 -2
  204. package/.framework/src/hooks/useApi.ts +208 -159
  205. package/.framework/src/hooks/useAppPath.ts +32 -23
  206. package/.framework/src/hooks/useApps.ts +51 -20
  207. package/.framework/src/hooks/useEntityList.ts +32 -78
  208. package/.framework/src/hooks/useEntityRecord.ts +35 -96
  209. package/.framework/src/hooks/useForm.ts +32 -92
  210. package/.framework/src/hooks/useListSchema.ts +49 -78
  211. package/.framework/src/hooks/useSchemaRecord.ts +114 -105
  212. package/.framework/src/lib/api.ts +195 -116
  213. package/.framework/src/lib/supabase.ts +98 -64
  214. package/.framework/src/lib/utils.ts +287 -151
  215. package/.framework/src/main.tsx +49 -3
  216. package/.framework/src/pages/DashboardPage.tsx +52 -29
  217. package/.framework/src/pages/NotFoundPage.tsx +32 -10
  218. package/.framework/src/pages/SetupPage.tsx +32 -12
  219. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +119 -10
  220. package/.framework/src/pages/admin/AIAgentsPage.tsx +48 -19
  221. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +187 -17
  222. package/.framework/src/pages/admin/APIKeysPage.tsx +98 -22
  223. package/.framework/src/pages/admin/AlertsConfigPage.tsx +113 -26
  224. package/.framework/src/pages/admin/AppDetailPage.tsx +125 -8
  225. package/.framework/src/pages/admin/AppsPage.tsx +49 -2
  226. package/.framework/src/pages/admin/DeliveriesPage.tsx +196 -0
  227. package/.framework/src/pages/admin/DesignedPage.tsx +32 -18
  228. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +215 -484
  229. package/.framework/src/pages/admin/EmbeddingsPage.tsx +116 -319
  230. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +34 -9
  231. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +34 -9
  232. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -16
  233. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +183 -26
  234. package/.framework/src/pages/admin/IntegrationsPage.tsx +48 -19
  235. package/.framework/src/pages/admin/InvitesPage.tsx +48 -14
  236. package/.framework/src/pages/admin/LogsPage.tsx +422 -75
  237. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +34 -9
  238. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +113 -24
  239. package/.framework/src/pages/admin/PipelineDetailPage.tsx +206 -34
  240. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +50 -17
  241. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -21
  242. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +378 -88
  243. package/.framework/src/pages/admin/PromptConfigsPage.tsx +125 -16
  244. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +49 -10
  245. package/.framework/src/pages/admin/RoleDetailPage.tsx +130 -10
  246. package/.framework/src/pages/admin/RolesPage.tsx +171 -18
  247. package/.framework/src/pages/admin/SelectTestPage.tsx +34 -9
  248. package/.framework/src/pages/admin/ShadcnTestPage.tsx +49 -11
  249. package/.framework/src/pages/admin/SimpleDashboard.tsx +49 -10
  250. package/.framework/src/pages/admin/TestRunDetailPage.tsx +49 -4
  251. package/.framework/src/pages/admin/TestingDashboard.tsx +98 -28
  252. package/.framework/src/pages/admin/TimerDetailPage.tsx +143 -33
  253. package/.framework/src/pages/admin/TimersPage.tsx +48 -20
  254. package/.framework/src/pages/admin/TriggerDetailPage.tsx +118 -30
  255. package/.framework/src/pages/admin/TriggersPage.tsx +64 -19
  256. package/.framework/src/pages/admin/TypeDetailPage.tsx +138 -15
  257. package/.framework/src/pages/admin/TypesPage.tsx +48 -20
  258. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +465 -0
  259. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +787 -0
  260. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +699 -0
  261. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +647 -0
  262. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +616 -0
  263. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +1402 -0
  264. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +614 -0
  265. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +571 -0
  266. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +731 -0
  267. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  268. package/.framework/src/pages/auth/AuthConfirmPage.tsx +52 -51
  269. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +31 -22
  270. package/.framework/src/pages/auth/LoginPage.tsx +50 -23
  271. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -15
  272. package/.framework/src/pages/auth/RegisterPage.tsx +281 -317
  273. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -0
  274. package/.framework/src/pages/spine-framework/CLIPage.tsx +33 -0
  275. package/.framework/src/types/auth.ts +128 -71
  276. package/.framework/src/types/types.ts +373 -157
  277. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +39 -0
  278. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +36 -0
  279. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +46 -0
  280. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +28 -0
  281. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +38 -0
  282. package/docs/README.md +9 -130
  283. package/netlify.toml +5 -2
  284. package/package.json +13 -25
  285. package/scripts/kb-ingest-docs.ts +203 -0
  286. package/spine.config.json +65 -4
  287. package/.framework/migrations/001_schema.sql +0 -425
  288. package/.framework/migrations/002_rls_and_functions.sql +0 -763
  289. package/.framework/migrations/004_enhancements.sql +0 -43
  290. package/.framework/migrations/005_auth_user_trigger.sql +0 -105
  291. package/.framework/migrations/006_fix_current_actor_id.sql +0 -21
  292. package/.framework/migrations/016_invites.sql +0 -27
  293. package/.framework/migrations/017_fix_get_account_apps.sql +0 -78
  294. package/.framework/migrations/018_apps_required_roles.sql +0 -17
  295. package/.framework/migrations/019_triggers_unique_constraint.sql +0 -13
  296. package/.framework/migrations/020_get_account_apps_role_filter.sql +0 -117
  297. package/.framework/migrations/021_app_domains.sql +0 -112
  298. package/.framework/migrations/022_route_prefix_domain_unique.sql +0 -21
  299. package/.framework/migrations/023_match_embeddings_rpc.sql +0 -78
  300. package/.framework/migrations/024_messages_account_id.sql +0 -9
  301. package/.framework/migrations/025_retype_threads_messages.sql +0 -240
  302. package/.framework/migrations/026_fix_rls_policies.sql +0 -47
  303. package/.framework/migrations/027_embedding_lifecycle.sql +0 -176
  304. package/.framework/migrations/028_fix_message_data_permissions.sql +0 -49
  305. package/.framework/migrations/029_tighten_support_triage_prompt.sql +0 -41
  306. package/.framework/migrations/030_messages_updated_by.sql +0 -6
  307. package/.framework/migrations/031_seed_upsert_constraints.sql +0 -35
  308. package/docs/APPS.md +0 -232
  309. package/docs/ARCHITECTURE.md +0 -246
  310. package/docs/INSTALL.md +0 -155
  311. package/docs/design-schema-spec.md +0 -1837
  312. package/docs/dev-guides/01-create-an-app.md +0 -183
  313. package/docs/dev-guides/02-create-a-function.md +0 -178
  314. package/docs/dev-guides/03-create-a-component.md +0 -178
  315. package/docs/dev-guides/04-webhook-handlers.md +0 -129
  316. package/docs/dev-guides/05-testing.md +0 -168
  317. package/docs/dev-guides/README.md +0 -40
  318. package/docs/npm-packages-plan.md +0 -288
  319. package/docs/spine-core--api-reference.md +0 -822
@@ -1,98 +1,47 @@
1
- // ─── CHUNK_START: ADMIN_DATA_OVERVIEW ──────────────────────────────────────────────
2
- /**
3
- * @module admin-data
4
- * @audience both
5
- * @layer api-handler
6
- * @stability stable
7
- *
8
- * Generic CRUD API for all runtime entities. Provides list, get, create,
9
- * update, delete, and stats operations over a validated set of entity tables.
10
- *
11
- * **Routed by:** `GET/POST/PATCH/DELETE /.netlify/functions/admin-data`
12
- *
13
- * **Dispatch table (query params → handler):**
14
- * | method | ?action | ?id present | Handler |
15
- * |--------|---------|-------------|---------|
16
- * | GET | list | any | `list` |
17
- * | GET | get | any | `get` |
18
- * | GET | stats | any | `stats` |
19
- * | GET | — | yes | `get` |
20
- * | GET | — | no | `list` |
21
- * | POST | — | — | `create`|
22
- * | PATCH | — | — | `update`|
23
- * | DELETE | — | — | `remove`|
24
- *
25
- * **Authorization:** All DB reads/writes use `ctx.db` (RLS-scoped client).
26
- * RLS policies on each table enforce account hierarchy access automatically.
27
- * `adminDb` is used only for lookups that need bypass (type resolution).
28
- *
29
- * **Valid entities:** `accounts`, `people`, `items`, `threads`, `messages`,
30
- * `links`, `attachments`, `watchers`.
31
- *
32
- * INVARIANT: every `create` call requires `type_id` in the body — all runtime
33
- * records must reference a type.
34
- * INVARIANT: trigger dispatch (create/update/delete) is fire-and-forget —
35
- * trigger failures are logged but never surface to the caller.
36
- * INVARIANT: all returned records are passed through `sanitizeRecordData`
37
- * before being returned — field-level permission stripping is always applied.
38
- *
39
- * @seeAlso middleware.ts (createHandler, CoreContext)
40
- * @seeAlso permissions.ts (sanitizeRecordData)
41
- * @seeAlso trigger-engine.ts (fire*Triggers)
42
- * @seeAlso types.ts (types table, design_schema, validation_schema)
43
- */
44
1
 
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
10
+ // CIL[BEGIN chunk_id="admin-data_module_header_1784019432" type="module_header" identity="imports" parent="none"]
11
+ // CIL[DOC-MACRO purpose="Import dependencies for generic CRUD operations"]
12
+ // CIL[DOC-RESPONSIBILITY responsibility="Provide middleware, permissions, database access, type resolution, and triggers"]
13
+ // CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/functions/_shared/db.ts\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\".framework/functions/_shared/principal.ts\",\".framework/functions/_shared/schema-utils.ts\",\"\\\"]"]
14
+ // CIL[DOC-CONTRACT inputs="none" outputs="imported_modules,constants" errors="none" side_effects="none"]
15
+ // CIL[DOC-MICRO logic_summary="Imports modules and defines entity validation constants" notable_decisions="[]"]
16
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
17
+ // CIL[DOC-STABILITY stability="stable"]
18
+ // CIL[DOC-AUDIENCE audience="developer"]
19
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/functions/_shared/db.ts, .framework/src/pages/admin/TypeDetailPage.tsx, .framework/functions/_shared/principal.ts, .framework/functions/_shared/schema-utils.ts, \."]
20
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
21
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
22
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
23
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
24
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
45
25
  import { createHandler } from './_shared/middleware'
46
- import { sanitizeRecordData } from './_shared/permissions'
26
+ import { sanitizeRecordData, resolveFirstSurfacePermissions, validateUpdatePermissions } from './_shared/permissions'
47
27
  import { adminDb } from './_shared/db'
48
- import { resolveTypeId } from './_shared/resolve-ids'
28
+ import { isSystemAdmin } from './_shared/principal'
29
+ import { resolveTypeId, resolveTypeIds, resolveTypeRecord } from './_shared/resolve-ids'
49
30
  import { fireCreateTriggers, fireUpdateTriggers, fireDeleteTriggers } from './_shared/trigger-engine'
50
- // ─── CHUNK_END: ADMIN_DATA_OVERVIEW ──────────────────────────────────────────────
51
-
52
- // ─── CHUNK_START: ADMIN_DATA_MODULE_HEADER ──────────────────────────────────────────────
53
- /**
54
- * @chunk-id ADMIN_DATA_MODULE_HEADER_1_0_0
55
- * @version 1.0.0
56
- * @hash TBD
57
- * @macro Module Header
58
- * @micro Module-level documentation with routing table and invariants
59
- * @inputs none
60
- * @outputs none
61
- * @depends-on [none]
62
- * @depended-by [all chunks]
63
- * @side-effects [none]
64
- * @tags admin, module, routing, invariants
65
- */
66
- // ─── CHUNK_END: ADMIN_DATA_MODULE_HEADER ────────────────────────────────────────────────
67
-
68
- // ─── CHUNK_START: ADMIN_DATA_IMPORTS_AND_CONSTANTS ──────────────────────────────────────────────
69
- /**
70
- * @chunk-id ADMIN_DATA_IMPORTS_AND_CONSTANTS_1_0_0
71
- * @version 1.0.0
72
- * @hash TBD
73
- * @macro Imports and Constants
74
- * @micro Module imports and constant definitions
75
- * @inputs none
76
- * @outputs none
77
- * @depends-on [none]
78
- * @depended-by [all handlers]
79
- * @side-effects [none]
80
- * @tags admin, imports, constants
81
- */
31
+ import { withAdminLogging } from './_shared/logging'
82
32
 
83
33
  const PERMISSIONS_ALL = {
84
34
  record_permissions: { all: ['create', 'read', 'update', 'delete'] },
85
35
  fields: {}
86
36
  }
87
37
 
88
- /**
89
- * Allowlist of entity table names accepted by this handler.
90
- * Any entity string not in this set causes a 400-equivalent throw.
91
- */
92
- const VALID_ENTITIES = ['accounts', 'people', 'items', 'threads', 'messages', 'links', 'attachments', 'watchers', 'item_progress']
38
+ const VALID_ENTITIES = ['accounts', 'people', 'items', 'threads', 'messages', 'links', 'attachments', 'watchers', 'item_progress', 'logs', 'test_runs', 'observability', 'test_cases', 'types', 'link_types', 'ai_agents', 'prompt_configs']
39
+
40
+ const RAW_ENTITIES = ['logs', 'test_runs', 'observability', 'test_cases']
93
41
 
94
- // Map admin-data entity names to the kind field used in the types table
95
42
  const ENTITY_TO_KIND: Record<string, string> = {
43
+ accounts: 'account',
44
+ people: 'person',
96
45
  items: 'item',
97
46
  threads: 'thread',
98
47
  messages: 'message',
@@ -101,440 +50,643 @@ const ENTITY_TO_KIND: Record<string, string> = {
101
50
  watchers: 'watcher',
102
51
  item_progress: 'item_progress'
103
52
  }
104
- // ─── CHUNK_END: ADMIN_DATA_IMPORTS_AND_CONSTANTS ────────────────────────────────────────────────
105
-
106
- // ─── CHUNK_START: ADMIN_DATA_HANDLERS_SECTION ──────────────────────────────────────────────
107
- /**
108
- * @chunk-id ADMIN_DATA_HANDLERS_SECTION_1_0_0
109
- * @version 1.0.0
110
- * @hash TBD
111
- * @macro Handlers Section Header
112
- * @micro Section marker for CRUD handler functions
113
- * @inputs none
114
- * @outputs none
115
- * @depends-on [none]
116
- * @depended-by [none]
117
- * @side-effects [none]
118
- * @tags admin, section, handlers
119
- */
120
- // ─── CHUNK_END: ADMIN_DATA_HANDLERS_SECTION ────────────────────────────────────────────────
121
-
122
- // ─── CHUNK_START: ADMIN_DATA_LIST ──────────────────────────────────────────────
123
- /**
124
- * @chunk-id ADMIN_DATA_LIST_1_0_0
125
- * @version 1.0.0
126
- * @hash 780f1f0c81ea3bff78d40970ba4b03e3211d892bb883e6a099c6e0a55ee53b0c
127
- * @macro Entity List Handler
128
- * @micro Lists records with filtering, search, sorting, and pagination
129
- * @inputs ctx: CoreContext — Request context with principal and database
130
- * @inputs _body: any — Request body (unused for GET)
131
- * @outputs Array of sanitized records or {data, schema, view} with view config
132
- * @depends-on [createHandler, sanitizeRecordData, adminDb, getSearchField]
133
- * @depended-by [Netlify function routing]
134
- * @side-effects [DB queries, permission sanitization]
135
- * @tags admin, crud, list, pagination, search
136
- */
53
+ // CIL[END chunk_id="admin-data_module_header_1784019432"]
54
+ // CIL[BEGIN chunk_id="admin-data_list_1784019432" type="function" identity="list" parent="none"]
55
+ // CIL[DOC-MACRO purpose="List entity records with filtering and pagination"]
56
+ // CIL[DOC-RESPONSIBILITY responsibility="Query, filter, paginate, and sanitize entity records"]
57
+ // CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[\"custom/apps/portal/pages/ContentPage.tsx\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/src/components/ui/dialog.tsx\",\".framework/functions/_shared/principal.ts\",\"\\\"]"]
58
+ // CIL[DOC-CONTRACT inputs="none" outputs="sanitized_records[] or {data,schema,view}" errors="entity_required,database_error" side_effects="database_queries,permission_filtering"]
59
+ // CIL[DOC-MICRO logic_summary="Builds query with filters, applies pagination, sanitizes results, optionally resolves views" notable_decisions="[]"]
60
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
61
+ // CIL[DOC-STABILITY stability="stable"]
62
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: custom/apps/portal/pages/ContentPage.tsx, .framework/src/pages/admin/TypeDetailPage.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/src/components/ui/dialog.tsx, .framework/functions/_shared/principal.ts, \."]
63
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
64
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
65
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
66
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
67
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
137
68
  export const list = createHandler(async (ctx, _body) => {
138
- // Extract all reserved query params to prevent them from being used as column filters
139
- const { entity, action, method, search, sort_field = 'created_at', sort_direction = 'desc', limit = 50, offset = 0, type_slug, view: viewSlug, ...filters } = ctx.query || {}
69
+ const { entity, action, method, search, sort_field = 'created_at', sort_direction = 'desc', limit = 50, offset = 0, type_slug, type_slugs, view: viewSlug, ...filters } = ctx.query || {}
70
+
71
+ return await withAdminLogging(
72
+ ctx,
73
+ {
74
+ entity: entity || 'unknown',
75
+ action: 'list',
76
+ method: method || 'GET',
77
+ filters,
78
+ typeSlug: type_slug
79
+ },
80
+ async () => {
81
+ if (!entity || !VALID_ENTITIES.includes(entity)) {
82
+ throw new Error('Valid entity parameter is required')
83
+ }
140
84
 
141
- if (!entity || !VALID_ENTITIES.includes(entity)) {
142
- throw new Error('Valid entity parameter is required')
143
- }
85
+ const db = (ctx.principal?.isInternal || isSystemAdmin(ctx.principal)) ? adminDb : ctx.db
86
+ let query = db.from(entity).select('*')
144
87
 
145
- // Use ctx.db - RLS-scoped client based on principal
146
- // RLS policies enforce account hierarchy access automatically
147
- let query = ctx.db.from(entity).select('*')
88
+ const targetSlugs: string[] = []
89
+ if (type_slug) targetSlugs.push(type_slug)
90
+ if (type_slugs) {
91
+ const raw = Array.isArray(type_slugs) ? type_slugs : (type_slugs as string).split(',').map(s => s.trim()).filter(Boolean)
92
+ targetSlugs.push(...raw)
93
+ }
94
+ if (targetSlugs.length) {
95
+ const kind = ENTITY_TO_KIND[entity]
96
+ if (kind) {
97
+ try {
98
+ const resolved = await resolveTypeIds(targetSlugs.map(slug => ({ kind, slug })))
99
+ const typeIds = Object.values(resolved)
100
+ if (typeIds.length === 1) {
101
+ query = query.eq('type_id', typeIds[0])
102
+ } else if (typeIds.length > 1) {
103
+ query = query.in('type_id', typeIds)
104
+ }
105
+ } catch (err: any) {
106
+ if (err?.message?.includes('Type not found')) {
107
+ return []
108
+ }
109
+ throw err
110
+ }
111
+ }
112
+ }
148
113
 
149
- // Apply type_slug filter if provided (for schema-driven entities)
150
- if (type_slug) {
151
- const kind = ENTITY_TO_KIND[entity]
152
- if (kind) {
153
- // Resolve the type ID from the slug via the shared resolver
154
- const typeId = await resolveTypeId(kind, type_slug)
155
- if (typeId) {
156
- query = query.eq('type_id', typeId)
114
+ if (search) {
115
+ const searchField = getSearchField(entity)
116
+ query = query.ilike(searchField, `%${search}%`)
157
117
  }
158
- }
159
- }
160
118
 
161
- // Apply search if provided
162
- if (search) {
163
- // Search in display field based on entity
164
- const searchField = getSearchField(entity)
165
- query = query.ilike(searchField, `%${search}%`)
166
- }
119
+ Object.entries(filters).forEach(([key, value]) => {
120
+ if (value !== undefined && value !== null && value !== '') {
121
+ if (key === 'is_active' || key === 'is_verified' || key === 'is_primary') {
122
+ query = query.eq(key, value === 'true')
123
+ } else {
124
+ query = query.eq(key, value)
125
+ }
126
+ }
127
+ })
167
128
 
168
- // Apply filters
169
- Object.entries(filters).forEach(([key, value]) => {
170
- if (value !== undefined && value !== null && value !== '') {
171
- if (key === 'is_active' || key === 'is_verified' || key === 'is_primary') {
172
- query = query.eq(key, value === 'true')
173
- } else {
174
- query = query.eq(key, value)
129
+ query = query.order(sort_field, { ascending: sort_direction === 'asc' })
130
+
131
+ const { count, error: countError } = await db.from(entity)
132
+ .select('*', { count: 'exact', head: true })
133
+
134
+ if (countError) {
135
+ console.error('List count error:', countError)
136
+ throw new Error(countError.message || 'Database error getting count')
175
137
  }
176
- }
177
- })
178
138
 
179
- // Apply sorting
180
- query = query.order(sort_field, { ascending: sort_direction === 'asc' })
139
+ query = query.range(parseInt(offset.toString()), parseInt(offset.toString()) + parseInt(limit.toString()) - 1)
181
140
 
182
- // Get total count (RLS filters automatically)
183
- const { count, error: countError } = await ctx.db.from(entity)
184
- .select('*', { count: 'exact', head: true })
141
+ const { data, error: err } = await query
185
142
 
186
- if (countError) {
187
- console.error('List count error:', countError)
188
- throw new Error(countError.message || 'Database error getting count')
189
- }
143
+ if (err) {
144
+ console.error('List query error:', err)
145
+ throw new Error(err.message || 'Database error listing records')
146
+ }
190
147
 
191
- // Apply pagination
192
- query = query.range(parseInt(offset.toString()), parseInt(offset.toString()) + parseInt(limit.toString()) - 1)
148
+ const sanitizedData = []
149
+ for (const record of data || []) {
150
+ const sanitizedRecord = await sanitizeRecordData(ctx, record, entity)
151
+ sanitizedData.push(sanitizedRecord)
152
+ }
193
153
 
194
- const { data, error: err } = await query
154
+ if (viewSlug && type_slug) {
155
+ const { data: typeRecord } = await adminDb
156
+ .from('types')
157
+ .select('design_schema')
158
+ .eq('slug', type_slug)
159
+ .eq('is_active', true)
160
+ .single()
161
+
162
+ if (typeRecord?.design_schema) {
163
+ const schema = typeRecord.design_schema
164
+ const resolvedView = schema.views?.[viewSlug] || null
165
+ return { data: sanitizedData, schema, view: resolvedView }
166
+ }
167
+ }
195
168
 
196
- if (err) {
197
- console.error('List query error:', err)
198
- throw new Error(err.message || 'Database error listing records')
169
+ return sanitizedData
170
+ }
171
+ )
172
+ })
173
+ // CIL[END chunk_id="admin-data_list_1784019432"]
174
+ // CIL[BEGIN chunk_id="admin-data_get_1784019432" type="function" identity="get" parent="none"]
175
+ // CIL[DOC-MACRO purpose="Get single entity record by ID"]
176
+ // CIL[DOC-RESPONSIBILITY responsibility="Retrieve and sanitize individual entity record"]
177
+ // CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[\".framework/functions/_shared/schema-utils.ts\",\".framework/src/pages/admin/TypeDetailPage.tsx\",\".framework/src/pages/admin/RolesPage.tsx\",\"custom/apps/cortex/functions/custom_cil-embeddings.ts\",\"\\\"]"]
178
+ // CIL[DOC-CONTRACT inputs="none" outputs="sanitized_record or {data,schema,view}" errors="entity_required,id_required,record_not_found" side_effects="database_query,permission_filtering"]
179
+ // CIL[DOC-MICRO logic_summary="Queries single record by ID, applies permission sanitization, optionally resolves view" notable_decisions="[]"]
180
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
181
+ // CIL[DOC-STABILITY stability="stable"]
182
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/functions/_shared/schema-utils.ts, .framework/src/pages/admin/TypeDetailPage.tsx, .framework/src/pages/admin/RolesPage.tsx, custom/apps/cortex/functions/custom_cil-embeddings.ts, \."]
183
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
184
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
185
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
186
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
187
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
188
+ async function attachLineage(record: any, ctx: any): Promise<void> {
189
+ const parentFields = ['epic_id', 'sprint_id', 'initiative_id', 'discovery_id', 'idea_id']
190
+ const childrenMap: Record<string, { field: string; childSlugs: string[] }> = {
191
+ workbench_idea: { field: 'idea_id', childSlugs: ['workbench_discovery'] },
192
+ workbench_discovery: { field: 'discovery_id', childSlugs: ['workbench_initiative'] },
193
+ workbench_initiative: { field: 'initiative_id', childSlugs: ['workbench_epic', 'workbench_work_item'] },
194
+ workbench_epic: { field: 'epic_id', childSlugs: ['workbench_work_item', 'workbench_sprint'] },
195
+ workbench_sprint: { field: 'sprint_id', childSlugs: ['workbench_work_item'] },
196
+ workbench_work_item: { field: '', childSlugs: [] },
199
197
  }
200
198
 
201
- // RLS policies already filtered the data - just sanitize
202
- const sanitizedData = []
203
- for (const record of data || []) {
204
- const sanitizedRecord = await sanitizeRecordData(ctx, record, entity)
205
- sanitizedData.push(sanitizedRecord)
199
+ async function parentIdFor(current: any, visited: Set<string>): Promise<string | null> {
200
+ const { data: links } = await ctx.db.from('links').select('target_id').eq('source_id', current.id).eq('link_type', 'workbench_originated_from').eq('source_type', 'items').limit(1)
201
+ const linkTarget = links?.[0]?.target_id
202
+ if (linkTarget && !visited.has(linkTarget)) return linkTarget
203
+ const d = current.data || {}
204
+ for (const key of parentFields) {
205
+ const id = d[key]
206
+ if (id && !visited.has(id)) return id
207
+ }
208
+ return null
206
209
  }
207
210
 
208
- // If ?view=slug was requested, resolve schema + view config from the type record
209
- if (viewSlug && type_slug) {
210
- const { data: typeRecord } = await adminDb
211
- .from('types')
212
- .select('design_schema')
213
- .eq('slug', type_slug)
214
- .eq('is_active', true)
215
- .single()
216
-
217
- if (typeRecord?.design_schema) {
218
- const schema = typeRecord.design_schema
219
- const resolvedView = schema.views?.[viewSlug] || null
220
- return { data: sanitizedData, schema, view: resolvedView }
211
+ async function getParents(record: any): Promise<any[]> {
212
+ const parents: any[] = []
213
+ const visited = new Set<string>([record.id])
214
+ let current: any = record
215
+ for (let i = 0; i < 5; i++) {
216
+ const parentId = await parentIdFor(current, visited)
217
+ if (!parentId) break
218
+ visited.add(parentId)
219
+ const { data, error } = await ctx.db.from('items').select('*').eq('id', parentId).single()
220
+ if (error || !data) break
221
+ current = data
222
+ const sanitized = await sanitizeRecordData(ctx, data, 'items')
223
+ parents.push(sanitized)
221
224
  }
225
+ return parents
222
226
  }
223
227
 
224
- return sanitizedData
225
- })
226
- // ─── CHUNK_END: ADMIN_DATA_LIST ────────────────────────────────────────────────
227
-
228
- // ─── CHUNK_START: ADMIN_DATA_GET ──────────────────────────────────────────────
229
- /**
230
- * @chunk-id ADMIN_DATA_GET_1_0_0
231
- * @version 1.0.0
232
- * @hash 7ee27702615d7ebd144e065d399635ddf7664845685345d8a8f316449f11a7cf
233
- * @macro Entity Get Handler
234
- * @micro Returns a single record by ID with optional view configuration
235
- * @inputs ctx: CoreContext — Request context with principal and database
236
- * @inputs _body: any — Request body (unused for GET)
237
- * @outputs Sanitized record or {data, schema, view} with view config
238
- * @depends-on [createHandler, sanitizeRecordData]
239
- * @depended-by [Netlify function routing]
240
- * @side-effects [DB single row query, permission sanitization]
241
- * @tags admin, crud, get, single-record
242
- */
228
+ async function getChildren(record: any): Promise<any[]> {
229
+ const childIds = new Set<string>()
230
+ const { data: linkRows } = await ctx.db.from('links').select('source_id').eq('target_id', record.id).eq('link_type', 'workbench_originated_from').eq('target_type', 'items').limit(50)
231
+ for (const row of linkRows || []) {
232
+ if (row.source_id && row.source_id !== record.id) childIds.add(row.source_id)
233
+ }
234
+ const { data: typeRow } = await adminDb.from('types').select('slug').eq('id', record.type_id).single()
235
+ const cfg = typeRow?.slug ? childrenMap[typeRow.slug] : null
236
+ if (cfg?.childSlugs.length) {
237
+ try {
238
+ const pairs = cfg.childSlugs.map(slug => ({ kind: 'item', slug }))
239
+ const resolved = await resolveTypeIds(pairs)
240
+ const typeIds = Object.values(resolved)
241
+ if (typeIds.length) {
242
+ const { data: dataChildren } = await ctx.db.from('items').select('id').in('type_id', typeIds).eq(`data->>${cfg.field}`, record.id)
243
+ for (const child of dataChildren || []) {
244
+ if (child.id && child.id !== record.id) childIds.add(child.id)
245
+ }
246
+ }
247
+ } catch (e) {
248
+ // ignore missing child types
249
+ }
250
+ }
251
+ if (childIds.size === 0) return []
252
+ const { data: rawChildren } = await ctx.db.from('items').select('*').in('id', Array.from(childIds))
253
+ const children: any[] = []
254
+ for (const child of rawChildren || []) {
255
+ if (child.id === record.id) continue
256
+ children.push(await sanitizeRecordData(ctx, child, 'items'))
257
+ }
258
+ return children
259
+ }
260
+
261
+ const [parents, children] = await Promise.all([getParents(record), getChildren(record)])
262
+
263
+ const typeIds = new Set<string>()
264
+ typeIds.add(record.type_id)
265
+ for (const p of parents) if (p?.type_id) typeIds.add(p.type_id)
266
+ for (const c of children) if (c?.type_id) typeIds.add(c.type_id)
267
+ const { data: typeRows } = await adminDb.from('types').select('id, slug').in('id', Array.from(typeIds))
268
+ const slugById = new Map((typeRows || []).map((t: any) => [t.id, t.slug] as [string, string]))
269
+ record.type_slug = slugById.get(record.type_id) || ''
270
+ for (const p of parents) p.type_slug = slugById.get(p.type_id) || ''
271
+ for (const c of children) c.type_slug = slugById.get(c.type_id) || ''
272
+
273
+ record.parents = parents
274
+ record.children = children
275
+ }
276
+
243
277
  export const get = createHandler(async (ctx, _body) => {
244
278
  const { entity, id, view: viewSlug } = ctx.query || {}
279
+ const method = ctx.query?.method || 'GET'
245
280
 
246
- if (!entity || !VALID_ENTITIES.includes(entity)) {
247
- throw new Error('Valid entity parameter is required')
248
- }
281
+ return await withAdminLogging(
282
+ ctx,
283
+ {
284
+ entity: entity || 'unknown',
285
+ action: 'get',
286
+ method,
287
+ typeSlug: viewSlug
288
+ },
289
+ async () => {
290
+ if (!entity || !VALID_ENTITIES.includes(entity)) {
291
+ throw new Error('Valid entity parameter is required')
292
+ }
249
293
 
250
- if (!id) {
251
- throw new Error('ID parameter is required')
252
- }
294
+ if (!id) {
295
+ throw new Error('ID parameter is required')
296
+ }
253
297
 
254
- // RLS will filter based on account hierarchy access
255
- const { data, error: err } = await ctx.db.from(entity)
256
- .select('*')
257
- .eq('id', id)
258
- .single()
298
+ const { data, error: err } = await ctx.db.from(entity)
299
+ .select('*')
300
+ .eq('id', id)
301
+ .single()
259
302
 
260
- if (err) throw err
303
+ if (err) throw err
261
304
 
262
- if (!data) {
263
- throw new Error('Record not found')
264
- }
305
+ if (!data) {
306
+ throw new Error('Record not found')
307
+ }
265
308
 
266
- const sanitizedRecord = await sanitizeRecordData(ctx, data, entity)
309
+ const sanitizedRecord = await sanitizeRecordData(ctx, data, entity)
267
310
 
268
- // If ?view=slug was requested, include schema + resolved view from the record's stamped schema
269
- if (viewSlug && sanitizedRecord?.design_schema) {
270
- const schema = sanitizedRecord.design_schema
271
- const resolvedView = schema.views?.[viewSlug] || null
272
- return { data: sanitizedRecord, schema, view: resolvedView }
273
- }
311
+ if (entity === 'items') {
312
+ await attachLineage(sanitizedRecord, ctx)
313
+ }
314
+
315
+ if (viewSlug && sanitizedRecord?.design_schema) {
316
+ const schema = sanitizedRecord.design_schema
317
+ const resolvedView = schema.views?.[viewSlug] || null
318
+ return { data: sanitizedRecord, schema, view: resolvedView }
319
+ }
274
320
 
275
- return sanitizedRecord
321
+ return sanitizedRecord
322
+ }
323
+ )
276
324
  })
277
- // ─── CHUNK_END: ADMIN_DATA_GET ────────────────────────────────────────────────
278
-
279
- // ─── CHUNK_START: ADMIN_DATA_CREATE ──────────────────────────────────────────────
280
- /**
281
- * @chunk-id ADMIN_DATA_CREATE_1_0_0
282
- * @version 1.0.0
283
- * @hash TBD
284
- * @macro Entity Create Handler
285
- * @micro Creates a new record for an entity with type resolution and triggers
286
- * @inputs ctx: CoreContext — Request context with principal and database
287
- * @inputs body: any — Request body with entity, type_id/type_slug, and record fields
288
- * @outputs Sanitized created record
289
- * @depends-on [createHandler, sanitizeRecordData, adminDb, resolveTypeId, fireCreateTriggers]
290
- * @depended-by [Netlify function routing]
291
- * @side-effects [DB write, type lookup, async trigger fire]
292
- * @tags admin, crud, create, type-resolution
293
- */
325
+ // CIL[END chunk_id="admin-data_get_1784019432"]
326
+ // CIL[BEGIN chunk_id="admin-data_create_1784019432" type="function" identity="create" parent="none"]
327
+ // CIL[DOC-LEARNING topic="runtime-type:generic/stamping" content="Every runtime record is stamped with type.design_schema and type.validation_schema from the types table. resolveTypeRecord resolves kind+slug or type_id. admin-data create inherits scope from parent, applies PERMISSIONS_ALL default when record_permissions is missing, sets system fields on the row and non-system fields in record.data, then fires triggers. This applies to all kinds: account, person, item, thread, message, link, attachment, watcher."]
328
+ // CIL[DOC-NOTE type="design_schema_caveat" content="If a type row lacks record_permissions, the created record receives PERMISSIONS_ALL but permissions.ts may still reduce it to id at read time if the stamped schema is later cleared. Missing scope defaults to account inheritance; ensure types define scope explicitly."]
329
+ // CIL[DOC-MACRO purpose="Create new entity record with type resolution"]
330
+ // CIL[DOC-RESPONSIBILITY responsibility="Validate, resolve type, create record with inheritance and triggers"]
331
+ // CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[\".framework/src/pages/admin/TypeDetailPage.tsx\",\".framework/functions/_shared/db.ts\",\".framework/src/components/ui/dialog.tsx\",\"custom/apps/cortex/pages/operations/OperationsDashboard.tsx\",\".framework/functions/_shared/schema-utils.ts\",\"\\\"]"]
332
+ // CIL[DOC-CONTRACT inputs="none" outputs="created_record" errors="entity_required,type_resolution_failed,parent_not_found" side_effects="database_insert,trigger_fire,permission_inheritance"]
333
+ // CIL[DOC-MICRO logic_summary="Resolves type, handles account inheritance, applies defaults, fires triggers" notable_decisions="[]"]
334
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="complex_dependencies"]
335
+ // CIL[DOC-STABILITY stability="stable"]
336
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/src/pages/admin/TypeDetailPage.tsx, .framework/functions/_shared/db.ts, .framework/src/components/ui/dialog.tsx, custom/apps/cortex/pages/operations/OperationsDashboard.tsx, .framework/functions/_shared/schema-utils.ts, \."]
337
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
338
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
339
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
340
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
341
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
294
342
  export const create = createHandler(async (ctx, body) => {
295
343
  const entity = body?.entity || ctx.query?.entity
296
- const { entity: _e, type_slug: _ts, design_schema: _ds, validation_schema: _vs, account_id: _ai, ...recordData } = body || {}
344
+ const type_slug = body?.type_slug
345
+ const method = ctx.query?.method || 'POST'
346
+
347
+ return await withAdminLogging(
348
+ ctx,
349
+ {
350
+ entity: entity || 'unknown',
351
+ action: 'create',
352
+ method,
353
+ typeSlug: type_slug
354
+ },
355
+ async () => {
356
+ const { entity: _e, type_slug: _ts, design_schema: _ds, validation_schema: _vs, account_id: requestedAccountId, id: _id, ...recordData } = body || {}
357
+
358
+ if (!entity || !VALID_ENTITIES.includes(entity)) {
359
+ const e: any = new Error('Valid entity parameter is required'); e.statusCode = 400; throw e
360
+ }
297
361
 
298
- if (!entity || !VALID_ENTITIES.includes(entity)) {
299
- const e: any = new Error('Valid entity parameter is required'); e.statusCode = 400; throw e
300
- }
362
+ if (RAW_ENTITIES.includes(entity)) {
363
+ const allowed: Record<string, string[]> = {
364
+ logs: ['level', 'message', 'context', 'source', 'source_type', 'source_id', 'person_id', 'account_id', 'app_id', 'created_by', 'metadata', 'created_at'],
365
+ test_runs: ['suite', 'status', 'started_at', 'finished_at', 'duration_ms', 'total', 'passed', 'failed', 'skipped', 'triggered_by', 'account_id'],
366
+ observability: ['app_id', 'account_id', 'type_id', 'metric_name', 'metric_value', 'metric_unit', 'context', 'metadata', 'created_at'],
367
+ test_cases: ['app_id', 'account_id', 'type_id', 'slug', 'name', 'description', 'config', 'metadata', 'is_active', 'is_system', 'created_at', 'updated_at']
368
+ }
369
+ const fields = allowed[entity] || []
370
+ const raw: any = {}
371
+ for (const key of fields) {
372
+ if (recordData[key] !== undefined) raw[key] = recordData[key]
373
+ }
374
+ if (fields.includes('account_id') && raw.account_id === undefined) raw.account_id = ctx.accountId
375
+ if (entity === 'logs' && !raw.created_at) raw.created_at = new Date().toISOString()
376
+ if (entity === 'logs' && (!raw.level || !raw.message)) { const e: any = new Error('level and message are required for logs'); e.statusCode = 400; throw e }
377
+ if (entity === 'test_runs' && !raw.suite) { const e: any = new Error('suite is required for test_runs'); e.statusCode = 400; throw e }
378
+ if (entity === 'observability' && (!raw.metric_name || raw.metric_value === undefined)) { const e: any = new Error('metric_name and metric_value are required for observability'); e.statusCode = 400; throw e }
379
+ if (entity === 'test_cases' && (!raw.slug || !raw.name)) { const e: any = new Error('slug and name are required for test_cases'); e.statusCode = 400; throw e }
380
+
381
+ const { data, error: err } = await ctx.db.from(entity).insert(raw).select().single()
382
+ if (err) throw err
383
+ fireCreateTriggers(entity, data.id, raw, ctx).catch(console.error)
384
+ return await sanitizeRecordData(ctx, data, entity)
385
+ }
301
386
 
302
- // Resolve type_id from type_slug if provided, otherwise require type_id directly
303
- let typeId = recordData.type_id
304
- if (!typeId && body?.type_slug) {
305
- const kind = ENTITY_TO_KIND[entity]
306
- if (!kind) {
307
- const e: any = new Error(`type_slug is not supported for entity: ${entity}`); e.statusCode = 400; throw e
308
- }
309
- typeId = await resolveTypeId(kind, body.type_slug)
310
- }
387
+ let typeRecord: { id: string; design_schema: Record<string, any>; validation_schema: Record<string, any> }
388
+
389
+ if (body?.type_slug) {
390
+ const kind = ENTITY_TO_KIND[entity]
391
+ if (!kind) {
392
+ const e: any = new Error(`type_slug is not supported for entity: ${entity}`); e.statusCode = 400; throw e
393
+ }
394
+ typeRecord = await resolveTypeRecord(kind, body.type_slug)
395
+ } else if (recordData.type_id) {
396
+ const { data, error: typeErr } = await adminDb
397
+ .from('types')
398
+ .select('id, design_schema, validation_schema, is_active')
399
+ .eq('id', recordData.type_id)
400
+ .single()
401
+ if (typeErr || !data) throw new Error(`type_id not found: ${recordData.type_id}`)
402
+ if (!data.is_active) throw new Error(`type_id references an inactive type: ${recordData.type_id}`)
403
+ typeRecord = { id: data.id, design_schema: data.design_schema || {}, validation_schema: data.validation_schema || {} }
404
+ } else {
405
+ const e: any = new Error('type_id or type_slug is required — every runtime record must reference a type'); e.statusCode = 400; throw e
406
+ }
311
407
 
312
- // type_id is required on all runtime record creation
313
- if (!typeId) {
314
- const e: any = new Error('type_id or type_slug is required — every runtime record must reference a type'); e.statusCode = 400; throw e
315
- }
316
- recordData.type_id = typeId
408
+ recordData.type_id = typeRecord.id
317
409
 
318
- // Look up the type to stamp design_schema and validation_schema
319
- const { data: typeRecord, error: typeErr } = await adminDb
320
- .from('types')
321
- .select('id, design_schema, validation_schema, is_active')
322
- .eq('id', typeId)
323
- .single()
410
+ let designSchema = typeRecord.design_schema || {}
411
+ if (!designSchema.record_permissions) {
412
+ designSchema = { ...PERMISSIONS_ALL, ...designSchema }
413
+ }
324
414
 
325
- if (typeErr || !typeRecord) {
326
- throw new Error(`type_id not found: ${typeId}`)
327
- }
415
+ let recordAccountId = requestedAccountId || ctx.accountId
416
+ const parentRef = recordData.thread_id || recordData.target_id || recordData.parent_id
417
+ if (parentRef) {
418
+ const parentTable = recordData.thread_id
419
+ ? 'threads'
420
+ : recordData.target_type === 'account'
421
+ ? 'accounts'
422
+ : recordData.target_type === 'person'
423
+ ? 'people'
424
+ : 'items'
425
+ const parentColumns = parentTable === 'accounts' ? 'id' : 'account_id, design_schema'
426
+ const { data: parentRecord } = await ctx.db
427
+ .from(parentTable)
428
+ .select(parentColumns)
429
+ .eq('id', parentRef)
430
+ .maybeSingle()
431
+ if (!parentRecord) {
432
+ const e: any = new Error('Parent record not found or not accessible'); e.statusCode = 403; throw e
433
+ }
434
+ if (parentTable === 'accounts') {
435
+ recordAccountId = parentRecord.id
436
+ } else {
437
+ recordAccountId = parentRecord.account_id
438
+ const parentScope = parentRecord.design_schema?.scope
439
+ if (parentScope) {
440
+ designSchema = { ...designSchema, scope: parentScope }
441
+ }
442
+ }
443
+ }
328
444
 
329
- if (!typeRecord.is_active) {
330
- throw new Error(`type_id references an inactive type: ${typeId}`)
331
- }
445
+ const resolvedScope: string = designSchema.scope ?? 'account'
446
+ if (resolvedScope === 'platform') {
447
+ const { data: sysAccount } = await adminDb
448
+ .from('accounts')
449
+ .select('id')
450
+ .eq('slug', 'spine-system')
451
+ .single()
452
+ if (sysAccount?.id) recordAccountId = sysAccount.id
453
+ }
332
454
 
333
- // Ensure the type has at least permissions=ALL (defensive — migration 062 guarantees this)
334
- let designSchema = typeRecord.design_schema || {}
335
- if (!designSchema.record_permissions) {
336
- designSchema = { ...PERMISSIONS_ALL, ...designSchema }
337
- }
455
+ const dataToInsert: Record<string, any> = {
456
+ ...recordData,
457
+ design_schema: designSchema,
458
+ validation_schema: typeRecord.validation_schema || {},
459
+ created_by: ctx.principal?.id,
460
+ created_at: new Date().toISOString(),
461
+ updated_at: new Date().toISOString()
462
+ }
338
463
 
339
- // Resolve account_id and propagate scope via parent inheritance.
340
- // Runtime records are the source of truth: the parent record's stamped scope
341
- // and account_id are inherited, not re-looked up from the type table.
342
- let recordAccountId = ctx.accountId
343
- const parentRef = recordData.thread_id || recordData.target_id || recordData.parent_id
344
- if (parentRef) {
345
- const parentTable = recordData.thread_id ? 'threads' : 'items'
346
- const { data: parentRecord } = await ctx.db
347
- .from(parentTable)
348
- .select('account_id, design_schema')
349
- .eq('id', parentRef)
350
- .maybeSingle()
351
- if (!parentRecord) {
352
- const e: any = new Error('Parent record not found or not accessible'); e.statusCode = 403; throw e
353
- }
354
- recordAccountId = parentRecord.account_id
355
- const parentScope = parentRecord.design_schema?.scope
356
- if (parentScope) {
357
- designSchema = { ...designSchema, scope: parentScope }
358
- }
359
- }
464
+ // accounts is the source-of-truth table; it does not have an account_id column
465
+ if (entity !== 'accounts') {
466
+ dataToInsert.account_id = recordAccountId
467
+ } else if (dataToInsert.parent_id === undefined) {
468
+ dataToInsert.parent_id = ctx.principal?.accountId || null
469
+ }
360
470
 
361
- // Platform-scoped records belong to the system account so they are visible
362
- // across all accounts. Child records already inherited the parent's scope above.
363
- const resolvedScope: string = designSchema.scope ?? 'account'
364
- if (resolvedScope === 'platform') {
365
- const { data: sysAccount } = await adminDb
366
- .from('accounts')
367
- .select('id')
368
- .eq('slug', 'spine-system')
369
- .single()
370
- if (sysAccount?.id) recordAccountId = sysAccount.id
371
- }
471
+ const permissionAccountId = entity === 'accounts' ? ctx.principal?.accountId : recordAccountId
472
+ const typeSlugForPermissions = type_slug || ENTITY_TO_KIND[entity]
473
+ const perms = await resolveFirstSurfacePermissions(
474
+ ctx.principal?.id,
475
+ permissionAccountId,
476
+ typeSlugForPermissions,
477
+ 'create',
478
+ designSchema
479
+ )
480
+ if (!perms.canCreate) {
481
+ const e: any = new Error('Insufficient permissions to create this record')
482
+ e.statusCode = 403
483
+ throw e
484
+ }
372
485
 
373
- // Add audit fields + stamped schema
374
- const dataToInsert = {
375
- ...recordData,
376
- design_schema: designSchema,
377
- validation_schema: typeRecord.validation_schema || {},
378
- created_by: ctx.principal?.id,
379
- account_id: recordAccountId,
380
- created_at: new Date().toISOString(),
381
- updated_at: new Date().toISOString()
382
- }
486
+ const insertDb = entity === 'accounts' ? adminDb : ctx.db
383
487
 
384
- // RLS will check if user has INSERT permission on this account
385
- const { data, error: err } = await ctx.db.from(entity)
386
- .insert(dataToInsert)
387
- .select()
388
- .single()
488
+ const { data, error: err } = await insertDb.from(entity)
489
+ .insert(dataToInsert)
490
+ .select()
491
+ .single()
389
492
 
390
- if (err) throw err
493
+ if (err) throw err
391
494
 
392
- // Fire triggers asynchronously (don't block response)
393
- const entityData = { ...dataToInsert, id: data.id }
394
- fireCreateTriggers(entity, data.id, entityData, ctx).catch(console.error)
495
+ const entityData = { ...dataToInsert, id: data.id }
496
+ fireCreateTriggers(entity, data.id, entityData, ctx).catch(console.error)
395
497
 
396
- return await sanitizeRecordData(ctx, data, entity)
498
+ return await sanitizeRecordData(ctx, data, entity)
499
+ }
500
+ )
397
501
  })
398
- // ─── CHUNK_END: ADMIN_DATA_CREATE ────────────────────────────────────────────────
399
-
400
- // ─── CHUNK_START: ADMIN_DATA_UPDATE ──────────────────────────────────────────────
401
- /**
402
- * @chunk-id ADMIN_DATA_UPDATE_1_0_0
403
- * @version 1.0.0
404
- * @hash TBD
405
- * @macro Entity Update Handler
406
- * @micro Updates an existing record by ID with audit fields and triggers
407
- * @inputs ctx: CoreContext — Request context with principal and database
408
- * @inputs body: any — Request body with partial record fields to update
409
- * @outputs Sanitized updated record
410
- * @depends-on [createHandler, sanitizeRecordData, fireUpdateTriggers]
411
- * @depended-by [Netlify function routing]
412
- * @side-effects [DB write, async trigger fire]
413
- * @tags admin, crud, update
414
- */
502
+ // CIL[END chunk_id="admin-data_create_1784019432"]
503
+ // CIL[BEGIN chunk_id="admin-data_update_1784019432" type="function" identity="update" parent="none"]
504
+ // CIL[DOC-MACRO purpose="Update existing entity record"]
505
+ // CIL[DOC-RESPONSIBILITY responsibility="Patch allowed fields on entity record with triggers"]
506
+ // CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[\".framework/src/pages/admin/TypeDetailPage.tsx\"]"]
507
+ // CIL[DOC-CONTRACT inputs="none" outputs="updated_record" errors="entity_required,id_required" side_effects="database_update,trigger_fire"]
508
+ // CIL[DOC-MICRO logic_summary="Filters protected fields, adds audit fields, updates record, fires triggers" notable_decisions="[]"]
509
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
510
+ // CIL[DOC-STABILITY stability="stable"]
511
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: .framework/src/pages/admin/TypeDetailPage.tsx."]
512
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
513
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
514
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
515
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
516
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
415
517
  export const update = createHandler(async (ctx, body) => {
416
518
  const { entity, id } = ctx.query || {}
417
- // Strip server-only fields — client must never override these on update
418
- const { design_schema: _ds, validation_schema: _vs, account_id: _ai, type_id: _ti, type_slug: _ts, ...recordData } = body || {}
519
+ const method = ctx.query?.method || 'PATCH'
520
+ const type_slug = body?.type_slug
521
+
522
+ return await withAdminLogging(
523
+ ctx,
524
+ {
525
+ entity: entity || 'unknown',
526
+ action: 'update',
527
+ method,
528
+ typeSlug: type_slug
529
+ },
530
+ async () => {
531
+ const { design_schema: _ds, validation_schema: _vs, type_id: _ti, type_slug: _ts, id: _id, account_id: requestedAccountId, ...recordData } = body || {}
532
+
533
+ if (!entity || !VALID_ENTITIES.includes(entity)) {
534
+ throw new Error('Valid entity parameter is required')
535
+ }
419
536
 
420
- if (!entity || !VALID_ENTITIES.includes(entity)) {
421
- throw new Error('Valid entity parameter is required')
422
- }
537
+ if (!id) {
538
+ throw new Error('ID is required for update')
539
+ }
423
540
 
424
- if (!id) {
425
- throw new Error('ID is required for update')
426
- }
541
+ const { data: existing, error: existingErr } = await ctx.db
542
+ .from(entity)
543
+ .select('*')
544
+ .eq('id', id)
545
+ .single()
427
546
 
428
- // Add audit fields
429
- const dataToUpdate = {
430
- ...recordData,
431
- updated_by: ctx.principal?.id,
432
- updated_at: new Date().toISOString()
433
- }
547
+ if (existingErr || !existing) {
548
+ throw new Error('Record not found or access denied')
549
+ }
550
+
551
+ if (entity === 'people' && requestedAccountId !== undefined) {
552
+ recordData.account_id = requestedAccountId
553
+ }
554
+
555
+ const typeSlugForPermissions = type_slug || entity
556
+ const validation = await validateUpdatePermissions(
557
+ ctx,
558
+ recordData,
559
+ existing,
560
+ typeSlugForPermissions
561
+ )
562
+
563
+ if (!validation.valid) {
564
+ const e: any = new Error(validation.error || 'Insufficient permissions to update this record')
565
+ e.statusCode = 403
566
+ throw e
567
+ }
434
568
 
435
- // RLS will check UPDATE permission on this record
436
- const { data, error: err } = await ctx.db.from(entity)
437
- .update(dataToUpdate)
438
- .eq('id', id)
439
- .select()
440
- .single()
569
+ const sanitizedUpdate = validation.sanitizedData || recordData
570
+ const dataToUpdate = RAW_ENTITIES.includes(entity)
571
+ ? { ...sanitizedUpdate }
572
+ : { ...sanitizedUpdate, updated_by: ctx.principal?.id, updated_at: new Date().toISOString() }
441
573
 
442
- if (err) throw err
574
+ const { data, error: err } = await ctx.db.from(entity)
575
+ .update(dataToUpdate)
576
+ .eq('id', id)
577
+ .select()
578
+ .single()
443
579
 
444
- // Fire triggers asynchronously (don't block response)
445
- const entityData = { ...data, ...dataToUpdate }
446
- fireUpdateTriggers(entity, id, entityData, ctx).catch(console.error)
580
+ if (err) throw err
447
581
 
448
- return await sanitizeRecordData(ctx, data, entity)
582
+ const entityData = { ...data, ...dataToUpdate }
583
+ fireUpdateTriggers(entity, id, entityData, ctx).catch(console.error)
584
+
585
+ return await sanitizeRecordData(ctx, data, entity)
586
+ }
587
+ )
449
588
  })
450
- // ─── CHUNK_END: ADMIN_DATA_UPDATE ────────────────────────────────────────────────
451
-
452
- // ─── CHUNK_START: ADMIN_DATA_REMOVE ──────────────────────────────────────────────
453
- /**
454
- * @chunk-id ADMIN_DATA_REMOVE_1_0_0
455
- * @version 1.0.0
456
- * @hash TBD
457
- * @macro Entity Delete Handler
458
- * @micro Deletes a record by ID with soft delete support and triggers
459
- * @inputs ctx: CoreContext — Request context with principal and database
460
- * @inputs _body: any — Request body (unused for DELETE)
461
- * @outputs { deleted: boolean, soft: boolean, data?: sanitizedRecord }
462
- * @depends-on [createHandler, sanitizeRecordData, entitySupportsSoftDelete, fireDeleteTriggers]
463
- * @depended-by [Netlify function routing]
464
- * @side-effects [DB write, async trigger fire]
465
- * @tags admin, crud, delete, soft-delete
466
- */
589
+ // CIL[END chunk_id="admin-data_update_1784019432"]
590
+ // CIL[BEGIN chunk_id="admin-data_remove_1784019432" type="function" identity="remove" parent="none"]
591
+ // CIL[DOC-MACRO purpose="Delete entity record with soft delete support"]
592
+ // CIL[DOC-RESPONSIBILITY responsibility="Perform hard or soft delete with triggers"]
593
+ // CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[]"]
594
+ // CIL[DOC-CONTRACT inputs="none" outputs="delete_confirmation" errors="entity_required,id_required" side_effects="database_delete,trigger_fire"]
595
+ // CIL[DOC-MICRO logic_summary="Checks soft delete support, performs appropriate delete method, fires triggers" notable_decisions="[]"]
596
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
597
+ // CIL[DOC-STABILITY stability="stable"]
598
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
599
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
600
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
601
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
602
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
603
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
467
604
  export const remove = createHandler(async (ctx, _body) => {
468
605
  const { entity, id, soft = 'true' } = ctx.query || {}
606
+ const method = ctx.query?.method || 'DELETE'
607
+
608
+ return await withAdminLogging(
609
+ ctx,
610
+ {
611
+ entity: entity || 'unknown',
612
+ action: 'remove',
613
+ method
614
+ },
615
+ async () => {
616
+ if (!entity || !VALID_ENTITIES.includes(entity)) {
617
+ throw new Error('Valid entity parameter is required')
618
+ }
469
619
 
470
- if (!entity || !VALID_ENTITIES.includes(entity)) {
471
- throw new Error('Valid entity parameter is required')
472
- }
620
+ if (!id) {
621
+ throw new Error('ID is required for delete')
622
+ }
473
623
 
474
- if (!id) {
475
- throw new Error('ID is required for delete')
476
- }
624
+ const { data: existing, error: existingErr } = await ctx.db
625
+ .from(entity)
626
+ .select('*')
627
+ .eq('id', id)
628
+ .single()
629
+
630
+ if (existingErr || !existing) {
631
+ throw new Error('Record not found or access denied')
632
+ }
477
633
 
478
- const isSoftDelete = soft === 'true'
634
+ const permissionAccountId = existing.account_id || ctx.principal?.accountId
635
+ const perms = await resolveFirstSurfacePermissions(
636
+ ctx.principal?.id,
637
+ permissionAccountId,
638
+ entity,
639
+ 'delete',
640
+ existing.design_schema
641
+ )
642
+
643
+ if (!perms.canDelete) {
644
+ const e: any = new Error('Insufficient permissions to delete this record')
645
+ e.statusCode = 403
646
+ throw e
647
+ }
479
648
 
480
- if (isSoftDelete && entitySupportsSoftDelete(entity)) {
481
- // Soft delete - set is_active to false
482
- // RLS will check DELETE permission on this record
483
- const { data, error: err } = await ctx.db.from(entity)
484
- .update({
485
- is_active: false,
486
- updated_by: ctx.principal?.id,
487
- updated_at: new Date().toISOString()
488
- })
489
- .eq('id', id)
490
- .select()
491
- .single()
492
-
493
- if (err) throw err
494
- return { deleted: true, soft: true, data: await sanitizeRecordData(ctx, data, entity) }
495
- } else {
496
- // Hard delete
497
- // RLS will check DELETE permission on this record
498
- const { error: err } = await ctx.db.from(entity)
499
- .delete()
500
- .eq('id', id)
501
-
502
- if (err) throw err
503
- return { deleted: true, soft: false }
504
- }
649
+ const isSoftDelete = soft === 'true'
650
+
651
+ if (isSoftDelete && entitySupportsSoftDelete(entity)) {
652
+ const { data, error: err } = await ctx.db.from(entity)
653
+ .update({
654
+ is_active: false,
655
+ updated_by: ctx.principal?.id,
656
+ updated_at: new Date().toISOString()
657
+ })
658
+ .eq('id', id)
659
+ .select()
660
+ .single()
661
+
662
+ if (err) throw err
663
+ return { deleted: true, soft: true, data: await sanitizeRecordData(ctx, data, entity) }
664
+ } else {
665
+ const { error: err } = await ctx.db.from(entity)
666
+ .delete()
667
+ .eq('id', id)
668
+
669
+ if (err) throw err
670
+ return { deleted: true, soft: false }
671
+ }
672
+ }
673
+ )
505
674
  })
506
- // ─── CHUNK_END: ADMIN_DATA_REMOVE ────────────────────────────────────────────────
507
-
508
- // ─── CHUNK_START: ADMIN_DATA_HELPERS_SECTION ──────────────────────────────────────────────
509
- /**
510
- * @chunk-id ADMIN_DATA_HELPERS_SECTION_1_0_0
511
- * @version 1.0.0
512
- * @hash TBD
513
- * @macro Helpers Section Header
514
- * @micro Section marker for helper functions
515
- * @inputs none
516
- * @outputs none
517
- * @depends-on [none]
518
- * @depended-by [none]
519
- * @side-effects [none]
520
- * @tags admin, section, helpers
521
- */
522
- // ─── CHUNK_END: ADMIN_DATA_HELPERS_SECTION ────────────────────────────────────────────────
523
-
524
- // ─── CHUNK_START: ADMIN_DATA_GET_SEARCH_FIELD ──────────────────────────────────────────────
525
- /**
526
- * @chunk-id ADMIN_DATA_GET_SEARCH_FIELD_1_0_0
527
- * @version 1.0.0
528
- * @hash 7c2615cf66a522b9b0df96cfbc8dfc50a2650016e967ceffd9172a770c78a26a
529
- * @macro Search Field Resolver
530
- * @micro Returns the primary display field for search functionality
531
- * @inputs entity: string — Entity name from VALID_ENTITIES
532
- * @outputs string — Column name to search in
533
- * @depends-on [none]
534
- * @depended-by [list]
535
- * @side-effects [none]
536
- * @tags admin, search, field-mapping
537
- */
675
+ // CIL[END chunk_id="admin-data_remove_1784019432"]
676
+ // CIL[BEGIN chunk_id="admin-data_getsearchfield_1784019432" type="function" identity="getSearchField" parent="none"]
677
+ // CIL[DOC-MACRO purpose="Return search field mapping for entity"]
678
+ // CIL[DOC-RESPONSIBILITY responsibility="Provide field name for search functionality"]
679
+ // CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[]"]
680
+ // CIL[DOC-CONTRACT inputs="none" outputs="search_field:string" errors="none" side_effects="none"]
681
+ // CIL[DOC-MICRO logic_summary="Maps entity types to their primary display fields for search" notable_decisions="[]"]
682
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
683
+ // CIL[DOC-STABILITY stability="stable"]
684
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
685
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
686
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
687
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
688
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
689
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
538
690
  function getSearchField(entity: string): string {
539
691
  const searchFields: Record<string, string> = {
540
692
  accounts: 'display_name',
@@ -545,114 +697,110 @@ function getSearchField(entity: string): string {
545
697
  links: 'link_type',
546
698
  attachments: 'filename',
547
699
  watchers: 'watch_type',
548
- item_progress: 'title'
700
+ item_progress: 'title',
701
+ logs: 'message',
702
+ test_runs: 'suite',
703
+ observability: 'metric_name',
704
+ test_cases: 'name'
549
705
  }
550
706
  return searchFields[entity] || 'id'
551
707
  }
552
- // ─── CHUNK_END: ADMIN_DATA_GET_SEARCH_FIELD ────────────────────────────────────────────────
553
-
554
- // ─── CHUNK_START: ADMIN_DATA_ENTITY_SUPPORTS_SOFT_DELETE ──────────────────────────────────────────────
555
- /**
556
- * @chunk-id ADMIN_DATA_ENTITY_SUPPORTS_SOFT_DELETE_1_0_0
557
- * @version 1.0.0
558
- * @hash 0438e650a11bfb4c25b90939780127d8912bdc5caeb772fe3049381d6497412d
559
- * @macro Soft Delete Support Checker
560
- * @micro Returns true if entity supports soft delete via is_active column
561
- * @inputs entity: string — Entity name from VALID_ENTITIES
562
- * @outputs boolean — True if entity has is_active column
563
- * @depends-on [none]
564
- * @depended-by [remove]
565
- * @side-effects [none]
566
- * @tags admin, soft-delete, entity-check
567
- */
708
+ // CIL[END chunk_id="admin-data_getsearchfield_1784019432"]
709
+ // CIL[BEGIN chunk_id="admin-data_entitysupportssoftdelete_1784019432" type="function" identity="entitySupportsSoftDelete" parent="none"]
710
+ // CIL[DOC-MACRO purpose="Check if entity supports soft delete"]
711
+ // CIL[DOC-RESPONSIBILITY responsibility="Determine soft delete capability for entity type"]
712
+ // CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[]"]
713
+ // CIL[DOC-CONTRACT inputs="none" outputs="boolean:supports_soft_delete" errors="none" side_effects="none"]
714
+ // CIL[DOC-MICRO logic_summary="Returns true if entity has is_active column for soft delete" notable_decisions="[]"]
715
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
716
+ // CIL[DOC-STABILITY stability="stable"]
717
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
718
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
719
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
720
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
721
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
722
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
568
723
  function entitySupportsSoftDelete(entity: string): boolean {
569
- const softDeleteEntities = ['accounts', 'people', 'items', 'threads', 'messages', 'watchers', 'item_progress']
724
+ const softDeleteEntities = ['accounts', 'people', 'items', 'threads', 'messages', 'watchers', 'item_progress', 'test_cases']
570
725
  return softDeleteEntities.includes(entity)
571
726
  }
572
- // ─── CHUNK_END: ADMIN_DATA_ENTITY_SUPPORTS_SOFT_DELETE ────────────────────────────────────────────────
573
-
574
- // ─── CHUNK_START: ADMIN_DATA_STATS ──────────────────────────────────────────────
575
- /**
576
- * @chunk-id ADMIN_DATA_STATS_1_0_0
577
- * @version 1.0.0
578
- * @hash 0bed523a260ab3959cb4e7150cca044f4e97ce91d7ae3386ab14b5f4182d74d7
579
- * @macro Entity Stats Handler
580
- * @micro Returns total record count for an entity scoped by account
581
- * @inputs ctx: CoreContext — Request context with principal and database
582
- * @inputs _body: any — Request body (unused for GET)
583
- * @outputs {entity: string, count: number} — Entity name and record count
584
- * @depends-on [createHandler]
585
- * @depended-by [Netlify function routing]
586
- * @side-effects [DB count query with RLS filtering]
587
- * @tags admin, stats, count, entity
588
- */
727
+ // CIL[END chunk_id="admin-data_entitysupportssoftdelete_1784019432"]
728
+ // CIL[BEGIN chunk_id="admin-data_stats_1784019432" type="function" identity="stats" parent="none"]
729
+ // CIL[DOC-MACRO purpose="Get record count for entity"]
730
+ // CIL[DOC-RESPONSIBILITY responsibility="Return total record count with RLS filtering"]
731
+ // CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[]"]
732
+ // CIL[DOC-CONTRACT inputs="none" outputs="{entity,count}" errors="entity_required,database_error" side_effects="database_count_query"]
733
+ // CIL[DOC-MICRO logic_summary="Performs count query with RLS filtering applied" notable_decisions="[]"]
734
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
735
+ // CIL[DOC-STABILITY stability="stable"]
736
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
737
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
738
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
739
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
740
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
741
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
589
742
  export const stats = createHandler(async (ctx, _body) => {
590
743
  const { entity } = ctx.query || {}
744
+ const method = ctx.query?.method || 'GET'
591
745
 
592
- if (!entity || !VALID_ENTITIES.includes(entity)) {
593
- throw new Error('Valid entity parameter is required')
594
- }
746
+ return await withAdminLogging(
747
+ ctx,
748
+ {
749
+ entity: entity || 'unknown',
750
+ action: 'stats',
751
+ method
752
+ },
753
+ async () => {
754
+ if (!entity || !VALID_ENTITIES.includes(entity)) {
755
+ throw new Error('Valid entity parameter is required')
756
+ }
595
757
 
596
- // RLS will filter count based on account hierarchy access
597
- const { count, error: err } = await ctx.db.from(entity)
598
- .select('*', { count: 'exact', head: true })
758
+ const { count, error: err } = await ctx.db.from(entity)
759
+ .select('*', { count: 'exact', head: true })
599
760
 
600
- if (err) throw err
761
+ if (err) throw err
601
762
 
602
- return { entity, count }
763
+ return { entity, count }
764
+ }
765
+ )
603
766
  })
604
- // ─── CHUNK_END: ADMIN_DATA_STATS ────────────────────────────────────────────────
605
-
606
- // ─── CHUNK_START: ADMIN_DATA_MAIN_HANDLER_SECTION ──────────────────────────────────────────────
607
- /**
608
- * @chunk-id ADMIN_DATA_MAIN_HANDLER_SECTION_1_0_0
609
- * @version 1.0.0
610
- * @hash TBD
611
- * @macro Main Handler Section Header
612
- * @micro Section marker for the main router handler
613
- * @inputs none
614
- * @outputs none
615
- * @depends-on [none]
616
- * @depended-by [none]
617
- * @side-effects [none]
618
- * @tags admin, section, main-handler
619
- */
620
- // ─── CHUNK_END: ADMIN_DATA_MAIN_HANDLER_SECTION ────────────────────────────────────────────────
621
-
622
- // ─── CHUNK_START: ADMIN_DATA_HANDLER ──────────────────────────────────────────────
623
- /**
624
- * @chunk-id ADMIN_DATA_HANDLER_1_0_0
625
- * @version 1.0.0
626
- * @hash edceffa0d5c8b3c05b1a89188a274a586b6ebe8793ede427e2326876d7090df9
627
- * @macro Admin Data Router
628
- * @micro Routes HTTP methods and actions to appropriate CRUD handlers
629
- * @inputs ctx: CoreContext — Request context with principal and database
630
- * @inputs body: any — Request body for POST/PATCH operations
631
- * @outputs Varies — Depends on routed handler (list/get/create/update/remove/stats)
632
- * @depends-on [createHandler, list, get, create, update, remove, stats]
633
- * @depended-by [Netlify function routing]
634
- * @side-effects [Delegates to appropriate handler]
635
- * @tags admin, router, crud, netlify-function
636
- */
767
+ // CIL[END chunk_id="admin-data_stats_1784019432"]
768
+ // CIL[BEGIN chunk_id="admin-data_handler_1784019432" type="function" identity="handler" parent="none"]
769
+ // CIL[DOC-MACRO purpose="Route HTTP methods to appropriate CRUD handlers"]
770
+ // CIL[DOC-RESPONSIBILITY responsibility="Dispatch requests based on method and action parameters"]
771
+ // CIL[DOC-CONTEXT module="admin-data" layer="api-handler" related_files="[]"]
772
+ // CIL[DOC-CONTRACT inputs="none" outputs="handler_response" errors="invalid_action_or_method" side_effects="delegates_to_crud_handlers"]
773
+ // CIL[DOC-MICRO logic_summary="Routes GET/POST/PATCH/DELETE to list/get/create/update/remove/stats" notable_decisions="[]"]
774
+ // CIL[DOC-CONFIDENCE pass="second" score="0.95" flags="none"]
775
+ // CIL[DOC-STABILITY stability="stable"]
776
+ // CIL[DOC-VECTOR type="context" content="Layer: api-handler. Direct callers: 0. Direct calls: 0. Related files: none."]
777
+ // CIL[DOC-VECTOR type="blast_radius" content="Direct callers: 0. Total affected: 0."]
778
+ // CIL[DOC-VECTOR type="configuration" content="No configuration dependencies."]
779
+ // CIL[DOC-VECTOR type="constraint" content="No test paths identified."]
780
+ // CIL[DOC-VECTOR type="implementation_pattern" content="" status="pending_human_input"]
781
+ // CIL[DOC-VECTOR type="user_journey" content="" status="pending_human_input"]
637
782
  export const handler = createHandler(async (ctx, body) => {
638
783
  const { action } = ctx.query || {}
639
784
  const method = ctx.query?.method || 'GET'
640
785
 
641
786
  switch (action) {
642
787
  case 'list':
643
- if (method === 'GET') {
644
- return await list(ctx, body)
645
- }
788
+ if (method === 'GET') return await list(ctx, body)
646
789
  break
647
790
  case 'get':
648
- if (method === 'GET') {
649
- return await get(ctx, body)
650
- }
791
+ if (method === 'GET') return await get(ctx, body)
651
792
  break
652
793
  case 'stats':
653
- if (method === 'GET') {
654
- return await stats(ctx, body)
655
- }
794
+ if (method === 'GET') return await stats(ctx, body)
795
+ break
796
+ case 'create':
797
+ if (method === 'POST') return await create(ctx, body)
798
+ break
799
+ case 'update':
800
+ if (method === 'PATCH') return await update(ctx, body)
801
+ break
802
+ case 'remove':
803
+ if (method === 'DELETE') return await remove(ctx, body)
656
804
  break
657
805
  default:
658
806
  if (method === 'GET' && ctx.query?.id) {
@@ -670,4 +818,4 @@ export const handler = createHandler(async (ctx, body) => {
670
818
 
671
819
  throw new Error('Invalid action or method')
672
820
  })
673
- // ─── CHUNK_END: ADMIN_DATA_HANDLER ────────────────────────────────────────────────
821
+ // CIL[END chunk_id="admin-data_handler_1784019432"]