spine-framework 0.3.125 → 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 +169 -97
  6. package/.framework/cli/commands/generate.ts +82 -160
  7. package/.framework/cli/commands/init.ts +72 -34
  8. package/.framework/cli/commands/install-app.ts +260 -125
  9. package/.framework/cli/commands/items.ts +33 -44
  10. package/.framework/cli/commands/migrate.ts +122 -40
  11. package/.framework/cli/commands/migrations.ts +33 -40
  12. package/.framework/cli/commands/pipelines.ts +31 -39
  13. package/.framework/cli/commands/status.ts +47 -23
  14. package/.framework/cli/commands/system.ts +47 -37
  15. package/.framework/cli/commands/test.ts +112 -68
  16. package/.framework/cli/commands/validate-app.ts +215 -121
  17. package/.framework/cli/context.ts +112 -130
  18. package/.framework/cli/env-loader.ts +47 -18
  19. package/.framework/cli/index.ts +81 -42
  20. package/.framework/docs/runtime-types.md +1024 -0
  21. package/.framework/functions/_shared/agent-runner.ts +1296 -961
  22. package/.framework/functions/_shared/app-manifest.ts +82 -68
  23. package/.framework/functions/_shared/audit.ts +135 -83
  24. package/.framework/functions/_shared/{cil-state.ts → cil.ts} +147 -54
  25. package/.framework/functions/_shared/db.ts +80 -126
  26. package/.framework/functions/_shared/hash-api-key.ts +63 -0
  27. package/.framework/functions/_shared/index.ts +222 -347
  28. package/.framework/functions/_shared/kb.ts +319 -0
  29. package/.framework/functions/_shared/llm.ts +92 -0
  30. package/.framework/functions/_shared/logging.ts +288 -0
  31. package/.framework/functions/_shared/middleware.ts +329 -365
  32. package/.framework/functions/_shared/outcomes.ts +36 -0
  33. package/.framework/functions/_shared/permissions.ts +359 -643
  34. package/.framework/functions/_shared/pipeline-runner.ts +243 -316
  35. package/.framework/functions/_shared/principal.ts +376 -468
  36. package/.framework/functions/_shared/resolve-credentials.ts +102 -0
  37. package/.framework/functions/_shared/resolve-ids.ts +162 -14
  38. package/.framework/functions/_shared/retry-deliveries.ts +104 -0
  39. package/.framework/functions/_shared/schema-utils.ts +611 -373
  40. package/.framework/functions/_shared/testing.ts +207 -137
  41. package/.framework/functions/_shared/tracing.ts +276 -0
  42. package/.framework/functions/_shared/trigger-engine.ts +159 -211
  43. package/.framework/functions/_shared/webhook-registration.ts +95 -81
  44. package/.framework/functions/_shared/webhook-registry.ts +153 -75
  45. package/.framework/functions/account-nodes.ts +31 -53
  46. package/.framework/functions/adapters/generic-webhook-inbound.ts +56 -0
  47. package/.framework/functions/adapters/generic-webhook-outbound.ts +43 -0
  48. package/.framework/functions/adapters/openai-outbound.ts +40 -0
  49. package/.framework/functions/admin-data.ts +687 -533
  50. package/.framework/functions/admin-funnel.ts +31 -20
  51. package/.framework/functions/admin-install-funnel.ts +47 -28
  52. package/.framework/functions/admin-signals.ts +47 -29
  53. package/.framework/functions/ai-agents.ts +122 -147
  54. package/.framework/functions/ai-interactions.ts +180 -90
  55. package/.framework/functions/api-keys.ts +250 -192
  56. package/.framework/functions/apps.ts +203 -197
  57. package/.framework/functions/auth.ts +98 -91
  58. package/.framework/functions/cil-agent-ingest.ts +773 -0
  59. package/.framework/functions/cil-agent.ts +592 -0
  60. package/.framework/functions/cil-callmap.ts +1227 -0
  61. package/.framework/functions/cil-config.ts +583 -0
  62. package/.framework/functions/cil-context.ts +445 -0
  63. package/.framework/functions/cil-doc.ts +1752 -0
  64. package/.framework/functions/cil-embeddings.ts +1202 -0
  65. package/.framework/functions/cil-graph.ts +585 -0
  66. package/.framework/functions/cil-ingest.ts +2416 -0
  67. package/.framework/functions/cil-pipeline.ts +318 -0
  68. package/.framework/functions/cil-project.ts +499 -0
  69. package/.framework/functions/cil-validate.ts +469 -0
  70. package/.framework/functions/debug-auth.ts +73 -53
  71. package/.framework/functions/deliveries.ts +117 -0
  72. package/.framework/functions/embeddings.ts +528 -516
  73. package/.framework/functions/eval-harness.ts +158 -106
  74. package/.framework/functions/integration-outbound.ts +311 -0
  75. package/.framework/functions/integration-routes.ts +240 -466
  76. package/.framework/functions/integrations.ts +132 -140
  77. package/.framework/functions/invites.ts +401 -201
  78. package/.framework/functions/item-progress.ts +113 -53
  79. package/.framework/functions/kb.ts +28 -0
  80. package/.framework/functions/logs.ts +239 -222
  81. package/.framework/functions/observability.ts +308 -259
  82. package/.framework/functions/pipeline-executions.ts +179 -235
  83. package/.framework/functions/pipelines.ts +163 -205
  84. package/.framework/functions/prompt-configs.ts +109 -156
  85. package/.framework/functions/roles.ts +97 -147
  86. package/.framework/functions/system-cron.ts +132 -218
  87. package/.framework/functions/system.ts +112 -52
  88. package/.framework/functions/tests.ts +32 -28
  89. package/.framework/functions/timers.ts +162 -210
  90. package/.framework/functions/triggers.ts +282 -346
  91. package/.framework/functions/types.ts +283 -400
  92. package/.framework/migrations/001_core_schema.sql +630 -0
  93. package/.framework/migrations/002_core_rls_and_functions.sql +532 -0
  94. package/.framework/migrations/{003_system_data.sql → 003_core_system_data.sql} +26 -153
  95. package/.framework/migrations/004_core_rpc_and_broker.sql +421 -0
  96. package/.framework/migrations/005_core_observability.sql +598 -0
  97. package/.framework/migrations/006_security_hardening_rls.sql +97 -0
  98. package/.framework/migrations/007_kb_article_list.sql +237 -0
  99. package/.framework/migrations/008_items_target.sql +8 -0
  100. package/.framework/migrations/009_roles_slug_unique.sql +11 -0
  101. package/.framework/public/spine.config.json +91 -0
  102. package/.framework/scripts/app-install-cli.ts +112 -98
  103. package/.framework/src/App.tsx +67 -20
  104. package/.framework/src/apps/admin/index.tsx +97 -27
  105. package/.framework/src/components/AppWrapper.tsx +50 -11
  106. package/.framework/src/components/CustomAppLoader.tsx +131 -22
  107. package/.framework/src/components/admin/AdminListPage.tsx +75 -8
  108. package/.framework/src/components/admin/AdminSidebar.tsx +148 -36
  109. package/.framework/src/components/admin/AdminStatsCard.tsx +48 -29
  110. package/.framework/src/components/admin/PageInstructions.tsx +49 -0
  111. package/.framework/src/components/admin/SortableTableHeader.tsx +49 -2
  112. package/.framework/src/components/app-shell/GenericAppShell.tsx +69 -11
  113. package/.framework/src/components/app-shell/GenericDetailPage.tsx +55 -10
  114. package/.framework/src/components/app-shell/GenericListPage.tsx +76 -9
  115. package/.framework/src/components/app-sidebar.tsx +50 -3
  116. package/.framework/src/components/auth/ProtectedRoute.tsx +53 -45
  117. package/.framework/src/components/kb/KBArticleEditor.tsx +409 -0
  118. package/.framework/src/components/kb/KBArticleList.tsx +557 -0
  119. package/.framework/src/components/layout/AppShell.tsx +54 -16
  120. package/.framework/src/components/layout/Header.tsx +50 -38
  121. package/.framework/src/components/layout/Layout.tsx +48 -32
  122. package/.framework/src/components/layout/Sidebar.tsx +167 -16
  123. package/.framework/src/components/runtime/DataDetailHeader.tsx +33 -1
  124. package/.framework/src/components/runtime/DataDetailPage.tsx +289 -4
  125. package/.framework/src/components/runtime/DataFilters.tsx +78 -1
  126. package/.framework/src/components/runtime/DataHeader.tsx +32 -29
  127. package/.framework/src/components/runtime/DataListPage.tsx +161 -2
  128. package/.framework/src/components/runtime/DataStats.tsx +48 -1
  129. package/.framework/src/components/runtime/DataTable.tsx +153 -1
  130. package/.framework/src/components/runtime/SchemaDetailForm.tsx +77 -46
  131. package/.framework/src/components/runtime/index.ts +16 -9
  132. package/.framework/src/components/search-form.tsx +33 -1
  133. package/.framework/src/components/shared/AgentView.tsx +48 -51
  134. package/.framework/src/components/shared/FieldRenderer.tsx +84 -67
  135. package/.framework/src/components/shared/InviteManager.tsx +113 -52
  136. package/.framework/src/components/shared/MemberList.tsx +182 -0
  137. package/.framework/src/components/shared/SchemaFields.tsx +96 -78
  138. package/.framework/src/components/ui/DataTable.tsx +86 -56
  139. package/.framework/src/components/ui/Form.tsx +188 -109
  140. package/.framework/src/components/ui/ItemCard.tsx +86 -58
  141. package/.framework/src/components/ui/ItemListView.tsx +88 -61
  142. package/.framework/src/components/ui/LoadingSpinner.tsx +52 -27
  143. package/.framework/src/components/ui/Modal.tsx +81 -4
  144. package/.framework/src/components/ui/RichTextEditor.tsx +101 -9
  145. package/.framework/src/components/ui/accordion.tsx +97 -5
  146. package/.framework/src/components/ui/alert-dialog.tsx +225 -13
  147. package/.framework/src/components/ui/alert.tsx +113 -6
  148. package/.framework/src/components/ui/aspect-ratio.tsx +49 -2
  149. package/.framework/src/components/ui/avatar.tsx +129 -7
  150. package/.framework/src/components/ui/badge.tsx +65 -3
  151. package/.framework/src/components/ui/breadcrumb.tsx +145 -8
  152. package/.framework/src/components/ui/button-group.tsx +81 -4
  153. package/.framework/src/components/ui/button.tsx +49 -2
  154. package/.framework/src/components/ui/calendar.tsx +65 -3
  155. package/.framework/src/components/ui/card.tsx +145 -8
  156. package/.framework/src/components/ui/carousel.tsx +129 -7
  157. package/.framework/src/components/ui/chart.tsx +161 -10
  158. package/.framework/src/components/ui/checkbox.tsx +49 -2
  159. package/.framework/src/components/ui/collapsible.tsx +81 -5
  160. package/.framework/src/components/ui/combobox.tsx +305 -20
  161. package/.framework/src/components/ui/command.tsx +177 -11
  162. package/.framework/src/components/ui/context-menu.tsx +273 -17
  163. package/.framework/src/components/ui/dialog.tsx +209 -27
  164. package/.framework/src/components/ui/direction.tsx +64 -4
  165. package/.framework/src/components/ui/drawer.tsx +193 -12
  166. package/.framework/src/components/ui/dropdown-menu.tsx +273 -18
  167. package/.framework/src/components/ui/empty.tsx +145 -9
  168. package/.framework/src/components/ui/field.tsx +209 -14
  169. package/.framework/src/components/ui/hover-card.tsx +81 -4
  170. package/.framework/src/components/ui/input-group.tsx +162 -10
  171. package/.framework/src/components/ui/input-otp.tsx +98 -5
  172. package/.framework/src/components/ui/input.tsx +49 -2
  173. package/.framework/src/components/ui/item.tsx +225 -13
  174. package/.framework/src/components/ui/kbd.tsx +65 -3
  175. package/.framework/src/components/ui/label.tsx +49 -2
  176. package/.framework/src/components/ui/menubar.tsx +289 -17
  177. package/.framework/src/components/ui/native-select.tsx +97 -5
  178. package/.framework/src/components/ui/navigation-menu.tsx +177 -10
  179. package/.framework/src/components/ui/pagination.tsx +161 -9
  180. package/.framework/src/components/ui/popover.tsx +145 -8
  181. package/.framework/src/components/ui/progress.tsx +49 -2
  182. package/.framework/src/components/ui/radio-group.tsx +65 -3
  183. package/.framework/src/components/ui/resizable.tsx +82 -4
  184. package/.framework/src/components/ui/scroll-area.tsx +65 -3
  185. package/.framework/src/components/ui/select.tsx +193 -11
  186. package/.framework/src/components/ui/separator.tsx +49 -2
  187. package/.framework/src/components/ui/sheet.tsx +193 -11
  188. package/.framework/src/components/ui/sidebar.tsx +481 -38
  189. package/.framework/src/components/ui/skeleton.tsx +49 -2
  190. package/.framework/src/components/ui/slider.tsx +49 -2
  191. package/.framework/src/components/ui/sonner.tsx +49 -2
  192. package/.framework/src/components/ui/spinner.tsx +49 -2
  193. package/.framework/src/components/ui/switch.tsx +49 -2
  194. package/.framework/src/components/ui/table-primitives.tsx +160 -33
  195. package/.framework/src/components/ui/table.tsx +161 -9
  196. package/.framework/src/components/ui/tabs.tsx +113 -6
  197. package/.framework/src/components/ui/textarea.tsx +49 -2
  198. package/.framework/src/components/ui/toggle-group.tsx +81 -4
  199. package/.framework/src/components/ui/toggle.tsx +65 -3
  200. package/.framework/src/components/ui/tooltip.tsx +97 -5
  201. package/.framework/src/contexts/AppContext.tsx +129 -45
  202. package/.framework/src/contexts/AuthContext.tsx +173 -145
  203. package/.framework/src/hooks/use-mobile.ts +48 -2
  204. package/.framework/src/hooks/useApi.ts +208 -159
  205. package/.framework/src/hooks/useAppPath.ts +32 -23
  206. package/.framework/src/hooks/useApps.ts +51 -20
  207. package/.framework/src/hooks/useEntityList.ts +32 -78
  208. package/.framework/src/hooks/useEntityRecord.ts +35 -96
  209. package/.framework/src/hooks/useForm.ts +32 -92
  210. package/.framework/src/hooks/useListSchema.ts +49 -78
  211. package/.framework/src/hooks/useSchemaRecord.ts +114 -105
  212. package/.framework/src/lib/api.ts +192 -115
  213. package/.framework/src/lib/supabase.ts +98 -64
  214. package/.framework/src/lib/utils.ts +287 -151
  215. package/.framework/src/main.tsx +49 -3
  216. package/.framework/src/pages/DashboardPage.tsx +52 -29
  217. package/.framework/src/pages/NotFoundPage.tsx +32 -10
  218. package/.framework/src/pages/SetupPage.tsx +32 -12
  219. package/.framework/src/pages/admin/AIAgentDetailPage.tsx +119 -10
  220. package/.framework/src/pages/admin/AIAgentsPage.tsx +48 -19
  221. package/.framework/src/pages/admin/APIKeyDetailPage.tsx +187 -17
  222. package/.framework/src/pages/admin/APIKeysPage.tsx +98 -22
  223. package/.framework/src/pages/admin/AlertsConfigPage.tsx +113 -26
  224. package/.framework/src/pages/admin/AppDetailPage.tsx +125 -8
  225. package/.framework/src/pages/admin/AppsPage.tsx +49 -2
  226. package/.framework/src/pages/admin/DeliveriesPage.tsx +196 -0
  227. package/.framework/src/pages/admin/DesignedPage.tsx +32 -18
  228. package/.framework/src/pages/admin/EmbeddingDetailPage.tsx +215 -484
  229. package/.framework/src/pages/admin/EmbeddingsPage.tsx +116 -319
  230. package/.framework/src/pages/admin/ExtendedShadcnTestPage.tsx +34 -9
  231. package/.framework/src/pages/admin/IncrementalShadcnTestPage.tsx +34 -9
  232. package/.framework/src/pages/admin/IntegratedDashboard.tsx +32 -16
  233. package/.framework/src/pages/admin/IntegrationDetailPage.tsx +183 -26
  234. package/.framework/src/pages/admin/IntegrationsPage.tsx +48 -19
  235. package/.framework/src/pages/admin/InvitesPage.tsx +48 -14
  236. package/.framework/src/pages/admin/LogsPage.tsx +422 -75
  237. package/.framework/src/pages/admin/MinimalShadcnTestPage.tsx +34 -9
  238. package/.framework/src/pages/admin/ObservabilityDashboard.tsx +113 -24
  239. package/.framework/src/pages/admin/PipelineDetailPage.tsx +206 -34
  240. package/.framework/src/pages/admin/PipelineExecutionsPage.tsx +50 -17
  241. package/.framework/src/pages/admin/PipelinesPage.tsx +160 -21
  242. package/.framework/src/pages/admin/PromptConfigDetailPage.tsx +378 -88
  243. package/.framework/src/pages/admin/PromptConfigsPage.tsx +125 -16
  244. package/.framework/src/pages/admin/ProperlyDesignedPage.tsx +49 -10
  245. package/.framework/src/pages/admin/RoleDetailPage.tsx +130 -10
  246. package/.framework/src/pages/admin/RolesPage.tsx +171 -18
  247. package/.framework/src/pages/admin/SelectTestPage.tsx +34 -9
  248. package/.framework/src/pages/admin/ShadcnTestPage.tsx +49 -11
  249. package/.framework/src/pages/admin/SimpleDashboard.tsx +49 -10
  250. package/.framework/src/pages/admin/TestRunDetailPage.tsx +49 -4
  251. package/.framework/src/pages/admin/TestingDashboard.tsx +98 -28
  252. package/.framework/src/pages/admin/TimerDetailPage.tsx +143 -33
  253. package/.framework/src/pages/admin/TimersPage.tsx +48 -20
  254. package/.framework/src/pages/admin/TriggerDetailPage.tsx +118 -30
  255. package/.framework/src/pages/admin/TriggersPage.tsx +64 -19
  256. package/.framework/src/pages/admin/TypeDetailPage.tsx +138 -15
  257. package/.framework/src/pages/admin/TypesPage.tsx +48 -20
  258. package/.framework/src/pages/admin/cil/CILAgentPage.tsx +465 -0
  259. package/.framework/src/pages/admin/cil/CILChunkDetailPage.tsx +787 -0
  260. package/.framework/src/pages/admin/cil/CILDashboardPage.tsx +699 -0
  261. package/.framework/src/pages/admin/cil/CILFileListPage.tsx +647 -0
  262. package/.framework/src/pages/admin/cil/CILGraphPage.tsx +616 -0
  263. package/.framework/src/pages/admin/cil/CILIngestionPage.tsx +1402 -0
  264. package/.framework/src/pages/admin/cil/CILProjectsPage.tsx +614 -0
  265. package/.framework/src/pages/admin/cil/CILSearchPage.tsx +571 -0
  266. package/.framework/src/pages/admin/cil/FlexiblePipelinePage.tsx +731 -0
  267. package/.framework/src/pages/admin/kb/KBAdminPage.tsx +64 -0
  268. package/.framework/src/pages/auth/AuthConfirmPage.tsx +52 -51
  269. package/.framework/src/pages/auth/ForgotPasswordPage.tsx +31 -22
  270. package/.framework/src/pages/auth/LoginPage.tsx +50 -23
  271. package/.framework/src/pages/auth/PendingApprovalPage.tsx +31 -15
  272. package/.framework/src/pages/auth/RegisterPage.tsx +281 -317
  273. package/.framework/src/pages/spine-framework/APIPage.tsx +33 -0
  274. package/.framework/src/pages/spine-framework/CLIPage.tsx +33 -0
  275. package/.framework/src/types/auth.ts +128 -71
  276. package/.framework/src/types/types.ts +373 -157
  277. package/custom/.devin/skills/spine-workbench-discovery/SKILL.md +39 -0
  278. package/custom/.devin/skills/spine-workbench-idea/SKILL.md +36 -0
  279. package/custom/.devin/skills/spine-workbench-implement/SKILL.md +46 -0
  280. package/custom/.devin/skills/spine-workbench-initiative/SKILL.md +28 -0
  281. package/custom/.devin/skills/spine-workbench-plan/SKILL.md +38 -0
  282. package/docs/README.md +9 -130
  283. package/netlify.toml +3 -3
  284. package/package.json +13 -25
  285. package/scripts/kb-ingest-docs.ts +203 -0
  286. package/spine.config.json +65 -4
  287. package/.framework/migrations/001_schema.sql +0 -425
  288. package/.framework/migrations/002_rls_and_functions.sql +0 -763
  289. package/.framework/migrations/004_enhancements.sql +0 -43
  290. package/.framework/migrations/005_auth_user_trigger.sql +0 -105
  291. package/.framework/migrations/006_fix_current_actor_id.sql +0 -21
  292. package/.framework/migrations/016_invites.sql +0 -27
  293. package/.framework/migrations/017_fix_get_account_apps.sql +0 -78
  294. package/.framework/migrations/018_apps_required_roles.sql +0 -17
  295. package/.framework/migrations/019_triggers_unique_constraint.sql +0 -13
  296. package/.framework/migrations/020_get_account_apps_role_filter.sql +0 -117
  297. package/.framework/migrations/021_app_domains.sql +0 -112
  298. package/.framework/migrations/022_route_prefix_domain_unique.sql +0 -21
  299. package/.framework/migrations/023_match_embeddings_rpc.sql +0 -78
  300. package/.framework/migrations/024_messages_account_id.sql +0 -9
  301. package/.framework/migrations/025_retype_threads_messages.sql +0 -240
  302. package/.framework/migrations/026_fix_rls_policies.sql +0 -47
  303. package/.framework/migrations/027_embedding_lifecycle.sql +0 -176
  304. package/.framework/migrations/028_fix_message_data_permissions.sql +0 -49
  305. package/.framework/migrations/029_tighten_support_triage_prompt.sql +0 -41
  306. package/.framework/migrations/030_messages_updated_by.sql +0 -6
  307. package/.framework/migrations/031_seed_upsert_constraints.sql +0 -35
  308. package/docs/APPS.md +0 -232
  309. package/docs/ARCHITECTURE.md +0 -246
  310. package/docs/INSTALL.md +0 -155
  311. package/docs/design-schema-spec.md +0 -1837
  312. package/docs/dev-guides/01-create-an-app.md +0 -183
  313. package/docs/dev-guides/02-create-a-function.md +0 -178
  314. package/docs/dev-guides/03-create-a-component.md +0 -178
  315. package/docs/dev-guides/04-webhook-handlers.md +0 -129
  316. package/docs/dev-guides/05-testing.md +0 -168
  317. package/docs/dev-guides/README.md +0 -40
  318. package/docs/npm-packages-plan.md +0 -288
  319. package/docs/spine-core--api-reference.md +0 -822
@@ -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, resolveTypeRecord } 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,434 +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
+ }
168
+
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: [] },
197
+ }
195
198
 
196
- if (err) {
197
- console.error('List query error:', err)
198
- throw new Error(err.message || 'Database error listing records')
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
199
209
  }
200
210
 
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)
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)
224
+ }
225
+ return parents
206
226
  }
207
227
 
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 }
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)
221
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
222
259
  }
223
260
 
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
- */
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
+ }
386
+
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
+ }
301
407
 
302
- // Resolve type record (id + design_schema + validation_schema) in one query.
303
- // Accepts either type_slug (resolved by kind) or a direct type_id already in recordData.
304
- let typeRecord: { id: string; design_schema: Record<string, any>; validation_schema: Record<string, any> }
408
+ recordData.type_id = typeRecord.id
305
409
 
306
- if (body?.type_slug) {
307
- const kind = ENTITY_TO_KIND[entity]
308
- if (!kind) {
309
- const e: any = new Error(`type_slug is not supported for entity: ${entity}`); e.statusCode = 400; throw e
310
- }
311
- typeRecord = await resolveTypeRecord(kind, body.type_slug)
312
- } else if (recordData.type_id) {
313
- const { data, error: typeErr } = await adminDb
314
- .from('types')
315
- .select('id, design_schema, validation_schema, is_active')
316
- .eq('id', recordData.type_id)
317
- .single()
318
- if (typeErr || !data) throw new Error(`type_id not found: ${recordData.type_id}`)
319
- if (!data.is_active) throw new Error(`type_id references an inactive type: ${recordData.type_id}`)
320
- typeRecord = { id: data.id, design_schema: data.design_schema || {}, validation_schema: data.validation_schema || {} }
321
- } else {
322
- const e: any = new Error('type_id or type_slug is required — every runtime record must reference a type'); e.statusCode = 400; throw e
323
- }
410
+ let designSchema = typeRecord.design_schema || {}
411
+ if (!designSchema.record_permissions) {
412
+ designSchema = { ...PERMISSIONS_ALL, ...designSchema }
413
+ }
324
414
 
325
- recordData.type_id = typeRecord.id
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
+ }
326
444
 
327
- // Ensure the type has at least permissions=ALL (defensive — migration 062 guarantees this)
328
- let designSchema = typeRecord.design_schema || {}
329
- if (!designSchema.record_permissions) {
330
- designSchema = { ...PERMISSIONS_ALL, ...designSchema }
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
- // Resolve account_id and propagate scope via parent inheritance.
334
- // Runtime records are the source of truth: the parent record's stamped scope
335
- // and account_id are inherited, not re-looked up from the type table.
336
- let recordAccountId = ctx.accountId
337
- const parentRef = recordData.thread_id || recordData.target_id || recordData.parent_id
338
- if (parentRef) {
339
- const parentTable = recordData.thread_id ? 'threads' : 'items'
340
- const { data: parentRecord } = await ctx.db
341
- .from(parentTable)
342
- .select('account_id, design_schema')
343
- .eq('id', parentRef)
344
- .maybeSingle()
345
- if (!parentRecord) {
346
- const e: any = new Error('Parent record not found or not accessible'); e.statusCode = 403; throw e
347
- }
348
- recordAccountId = parentRecord.account_id
349
- const parentScope = parentRecord.design_schema?.scope
350
- if (parentScope) {
351
- designSchema = { ...designSchema, scope: parentScope }
352
- }
353
- }
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
+ }
354
463
 
355
- // Platform-scoped records belong to the system account so they are visible
356
- // across all accounts. Child records already inherited the parent's scope above.
357
- const resolvedScope: string = designSchema.scope ?? 'account'
358
- if (resolvedScope === 'platform') {
359
- const { data: sysAccount } = await adminDb
360
- .from('accounts')
361
- .select('id')
362
- .eq('slug', 'spine-system')
363
- .single()
364
- if (sysAccount?.id) recordAccountId = sysAccount.id
365
- }
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
+ }
366
470
 
367
- // Add audit fields + stamped schema
368
- const dataToInsert = {
369
- ...recordData,
370
- design_schema: designSchema,
371
- validation_schema: typeRecord.validation_schema || {},
372
- created_by: ctx.principal?.id,
373
- account_id: recordAccountId,
374
- created_at: new Date().toISOString(),
375
- updated_at: new Date().toISOString()
376
- }
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
+ }
377
485
 
378
- // RLS will check if user has INSERT permission on this account
379
- const { data, error: err } = await ctx.db.from(entity)
380
- .insert(dataToInsert)
381
- .select()
382
- .single()
486
+ const insertDb = entity === 'accounts' ? adminDb : ctx.db
383
487
 
384
- if (err) throw err
488
+ const { data, error: err } = await insertDb.from(entity)
489
+ .insert(dataToInsert)
490
+ .select()
491
+ .single()
385
492
 
386
- // Fire triggers asynchronously (don't block response)
387
- const entityData = { ...dataToInsert, id: data.id }
388
- fireCreateTriggers(entity, data.id, entityData, ctx).catch(console.error)
493
+ if (err) throw err
389
494
 
390
- return await sanitizeRecordData(ctx, data, entity)
495
+ const entityData = { ...dataToInsert, id: data.id }
496
+ fireCreateTriggers(entity, data.id, entityData, ctx).catch(console.error)
497
+
498
+ return await sanitizeRecordData(ctx, data, entity)
499
+ }
500
+ )
391
501
  })
392
- // ─── CHUNK_END: ADMIN_DATA_CREATE ────────────────────────────────────────────────
393
-
394
- // ─── CHUNK_START: ADMIN_DATA_UPDATE ──────────────────────────────────────────────
395
- /**
396
- * @chunk-id ADMIN_DATA_UPDATE_1_0_0
397
- * @version 1.0.0
398
- * @hash TBD
399
- * @macro Entity Update Handler
400
- * @micro Updates an existing record by ID with audit fields and triggers
401
- * @inputs ctx: CoreContext — Request context with principal and database
402
- * @inputs body: any — Request body with partial record fields to update
403
- * @outputs Sanitized updated record
404
- * @depends-on [createHandler, sanitizeRecordData, fireUpdateTriggers]
405
- * @depended-by [Netlify function routing]
406
- * @side-effects [DB write, async trigger fire]
407
- * @tags admin, crud, update
408
- */
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"]
409
517
  export const update = createHandler(async (ctx, body) => {
410
518
  const { entity, id } = ctx.query || {}
411
- // Strip server-only fields — client must never override these on update
412
- 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
+ }
413
536
 
414
- if (!entity || !VALID_ENTITIES.includes(entity)) {
415
- throw new Error('Valid entity parameter is required')
416
- }
537
+ if (!id) {
538
+ throw new Error('ID is required for update')
539
+ }
417
540
 
418
- if (!id) {
419
- throw new Error('ID is required for update')
420
- }
541
+ const { data: existing, error: existingErr } = await ctx.db
542
+ .from(entity)
543
+ .select('*')
544
+ .eq('id', id)
545
+ .single()
421
546
 
422
- // Add audit fields
423
- const dataToUpdate = {
424
- ...recordData,
425
- updated_by: ctx.principal?.id,
426
- updated_at: new Date().toISOString()
427
- }
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
+ }
428
568
 
429
- // RLS will check UPDATE permission on this record
430
- const { data, error: err } = await ctx.db.from(entity)
431
- .update(dataToUpdate)
432
- .eq('id', id)
433
- .select()
434
- .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() }
435
573
 
436
- 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()
437
579
 
438
- // Fire triggers asynchronously (don't block response)
439
- const entityData = { ...data, ...dataToUpdate }
440
- fireUpdateTriggers(entity, id, entityData, ctx).catch(console.error)
580
+ if (err) throw err
441
581
 
442
- 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
+ )
443
588
  })
444
- // ─── CHUNK_END: ADMIN_DATA_UPDATE ────────────────────────────────────────────────
445
-
446
- // ─── CHUNK_START: ADMIN_DATA_REMOVE ──────────────────────────────────────────────
447
- /**
448
- * @chunk-id ADMIN_DATA_REMOVE_1_0_0
449
- * @version 1.0.0
450
- * @hash TBD
451
- * @macro Entity Delete Handler
452
- * @micro Deletes a record by ID with soft delete support and triggers
453
- * @inputs ctx: CoreContext — Request context with principal and database
454
- * @inputs _body: any — Request body (unused for DELETE)
455
- * @outputs { deleted: boolean, soft: boolean, data?: sanitizedRecord }
456
- * @depends-on [createHandler, sanitizeRecordData, entitySupportsSoftDelete, fireDeleteTriggers]
457
- * @depended-by [Netlify function routing]
458
- * @side-effects [DB write, async trigger fire]
459
- * @tags admin, crud, delete, soft-delete
460
- */
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"]
461
604
  export const remove = createHandler(async (ctx, _body) => {
462
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
+ }
463
619
 
464
- if (!entity || !VALID_ENTITIES.includes(entity)) {
465
- throw new Error('Valid entity parameter is required')
466
- }
620
+ if (!id) {
621
+ throw new Error('ID is required for delete')
622
+ }
467
623
 
468
- if (!id) {
469
- throw new Error('ID is required for delete')
470
- }
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
+ }
471
633
 
472
- 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
+ }
473
648
 
474
- if (isSoftDelete && entitySupportsSoftDelete(entity)) {
475
- // Soft delete - set is_active to false
476
- // RLS will check DELETE permission on this record
477
- const { data, error: err } = await ctx.db.from(entity)
478
- .update({
479
- is_active: false,
480
- updated_by: ctx.principal?.id,
481
- updated_at: new Date().toISOString()
482
- })
483
- .eq('id', id)
484
- .select()
485
- .single()
486
-
487
- if (err) throw err
488
- return { deleted: true, soft: true, data: await sanitizeRecordData(ctx, data, entity) }
489
- } else {
490
- // Hard delete
491
- // RLS will check DELETE permission on this record
492
- const { error: err } = await ctx.db.from(entity)
493
- .delete()
494
- .eq('id', id)
495
-
496
- if (err) throw err
497
- return { deleted: true, soft: false }
498
- }
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
+ )
499
674
  })
500
- // ─── CHUNK_END: ADMIN_DATA_REMOVE ────────────────────────────────────────────────
501
-
502
- // ─── CHUNK_START: ADMIN_DATA_HELPERS_SECTION ──────────────────────────────────────────────
503
- /**
504
- * @chunk-id ADMIN_DATA_HELPERS_SECTION_1_0_0
505
- * @version 1.0.0
506
- * @hash TBD
507
- * @macro Helpers Section Header
508
- * @micro Section marker for helper functions
509
- * @inputs none
510
- * @outputs none
511
- * @depends-on [none]
512
- * @depended-by [none]
513
- * @side-effects [none]
514
- * @tags admin, section, helpers
515
- */
516
- // ─── CHUNK_END: ADMIN_DATA_HELPERS_SECTION ────────────────────────────────────────────────
517
-
518
- // ─── CHUNK_START: ADMIN_DATA_GET_SEARCH_FIELD ──────────────────────────────────────────────
519
- /**
520
- * @chunk-id ADMIN_DATA_GET_SEARCH_FIELD_1_0_0
521
- * @version 1.0.0
522
- * @hash 7c2615cf66a522b9b0df96cfbc8dfc50a2650016e967ceffd9172a770c78a26a
523
- * @macro Search Field Resolver
524
- * @micro Returns the primary display field for search functionality
525
- * @inputs entity: string — Entity name from VALID_ENTITIES
526
- * @outputs string — Column name to search in
527
- * @depends-on [none]
528
- * @depended-by [list]
529
- * @side-effects [none]
530
- * @tags admin, search, field-mapping
531
- */
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"]
532
690
  function getSearchField(entity: string): string {
533
691
  const searchFields: Record<string, string> = {
534
692
  accounts: 'display_name',
@@ -539,114 +697,110 @@ function getSearchField(entity: string): string {
539
697
  links: 'link_type',
540
698
  attachments: 'filename',
541
699
  watchers: 'watch_type',
542
- item_progress: 'title'
700
+ item_progress: 'title',
701
+ logs: 'message',
702
+ test_runs: 'suite',
703
+ observability: 'metric_name',
704
+ test_cases: 'name'
543
705
  }
544
706
  return searchFields[entity] || 'id'
545
707
  }
546
- // ─── CHUNK_END: ADMIN_DATA_GET_SEARCH_FIELD ────────────────────────────────────────────────
547
-
548
- // ─── CHUNK_START: ADMIN_DATA_ENTITY_SUPPORTS_SOFT_DELETE ──────────────────────────────────────────────
549
- /**
550
- * @chunk-id ADMIN_DATA_ENTITY_SUPPORTS_SOFT_DELETE_1_0_0
551
- * @version 1.0.0
552
- * @hash 0438e650a11bfb4c25b90939780127d8912bdc5caeb772fe3049381d6497412d
553
- * @macro Soft Delete Support Checker
554
- * @micro Returns true if entity supports soft delete via is_active column
555
- * @inputs entity: string — Entity name from VALID_ENTITIES
556
- * @outputs boolean — True if entity has is_active column
557
- * @depends-on [none]
558
- * @depended-by [remove]
559
- * @side-effects [none]
560
- * @tags admin, soft-delete, entity-check
561
- */
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"]
562
723
  function entitySupportsSoftDelete(entity: string): boolean {
563
- const softDeleteEntities = ['accounts', 'people', 'items', 'threads', 'messages', 'watchers', 'item_progress']
724
+ const softDeleteEntities = ['accounts', 'people', 'items', 'threads', 'messages', 'watchers', 'item_progress', 'test_cases']
564
725
  return softDeleteEntities.includes(entity)
565
726
  }
566
- // ─── CHUNK_END: ADMIN_DATA_ENTITY_SUPPORTS_SOFT_DELETE ────────────────────────────────────────────────
567
-
568
- // ─── CHUNK_START: ADMIN_DATA_STATS ──────────────────────────────────────────────
569
- /**
570
- * @chunk-id ADMIN_DATA_STATS_1_0_0
571
- * @version 1.0.0
572
- * @hash 0bed523a260ab3959cb4e7150cca044f4e97ce91d7ae3386ab14b5f4182d74d7
573
- * @macro Entity Stats Handler
574
- * @micro Returns total record count for an entity scoped by account
575
- * @inputs ctx: CoreContext — Request context with principal and database
576
- * @inputs _body: any — Request body (unused for GET)
577
- * @outputs {entity: string, count: number} — Entity name and record count
578
- * @depends-on [createHandler]
579
- * @depended-by [Netlify function routing]
580
- * @side-effects [DB count query with RLS filtering]
581
- * @tags admin, stats, count, entity
582
- */
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"]
583
742
  export const stats = createHandler(async (ctx, _body) => {
584
743
  const { entity } = ctx.query || {}
744
+ const method = ctx.query?.method || 'GET'
585
745
 
586
- if (!entity || !VALID_ENTITIES.includes(entity)) {
587
- throw new Error('Valid entity parameter is required')
588
- }
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
+ }
589
757
 
590
- // RLS will filter count based on account hierarchy access
591
- const { count, error: err } = await ctx.db.from(entity)
592
- .select('*', { count: 'exact', head: true })
758
+ const { count, error: err } = await ctx.db.from(entity)
759
+ .select('*', { count: 'exact', head: true })
593
760
 
594
- if (err) throw err
761
+ if (err) throw err
595
762
 
596
- return { entity, count }
763
+ return { entity, count }
764
+ }
765
+ )
597
766
  })
598
- // ─── CHUNK_END: ADMIN_DATA_STATS ────────────────────────────────────────────────
599
-
600
- // ─── CHUNK_START: ADMIN_DATA_MAIN_HANDLER_SECTION ──────────────────────────────────────────────
601
- /**
602
- * @chunk-id ADMIN_DATA_MAIN_HANDLER_SECTION_1_0_0
603
- * @version 1.0.0
604
- * @hash TBD
605
- * @macro Main Handler Section Header
606
- * @micro Section marker for the main router handler
607
- * @inputs none
608
- * @outputs none
609
- * @depends-on [none]
610
- * @depended-by [none]
611
- * @side-effects [none]
612
- * @tags admin, section, main-handler
613
- */
614
- // ─── CHUNK_END: ADMIN_DATA_MAIN_HANDLER_SECTION ────────────────────────────────────────────────
615
-
616
- // ─── CHUNK_START: ADMIN_DATA_HANDLER ──────────────────────────────────────────────
617
- /**
618
- * @chunk-id ADMIN_DATA_HANDLER_1_0_0
619
- * @version 1.0.0
620
- * @hash edceffa0d5c8b3c05b1a89188a274a586b6ebe8793ede427e2326876d7090df9
621
- * @macro Admin Data Router
622
- * @micro Routes HTTP methods and actions to appropriate CRUD handlers
623
- * @inputs ctx: CoreContext — Request context with principal and database
624
- * @inputs body: any — Request body for POST/PATCH operations
625
- * @outputs Varies — Depends on routed handler (list/get/create/update/remove/stats)
626
- * @depends-on [createHandler, list, get, create, update, remove, stats]
627
- * @depended-by [Netlify function routing]
628
- * @side-effects [Delegates to appropriate handler]
629
- * @tags admin, router, crud, netlify-function
630
- */
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"]
631
782
  export const handler = createHandler(async (ctx, body) => {
632
783
  const { action } = ctx.query || {}
633
784
  const method = ctx.query?.method || 'GET'
634
785
 
635
786
  switch (action) {
636
787
  case 'list':
637
- if (method === 'GET') {
638
- return await list(ctx, body)
639
- }
788
+ if (method === 'GET') return await list(ctx, body)
640
789
  break
641
790
  case 'get':
642
- if (method === 'GET') {
643
- return await get(ctx, body)
644
- }
791
+ if (method === 'GET') return await get(ctx, body)
645
792
  break
646
793
  case 'stats':
647
- if (method === 'GET') {
648
- return await stats(ctx, body)
649
- }
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)
650
804
  break
651
805
  default:
652
806
  if (method === 'GET' && ctx.query?.id) {
@@ -664,4 +818,4 @@ export const handler = createHandler(async (ctx, body) => {
664
818
 
665
819
  throw new Error('Invalid action or method')
666
820
  })
667
- // ─── CHUNK_END: ADMIN_DATA_HANDLER ────────────────────────────────────────────────
821
+ // CIL[END chunk_id="admin-data_handler_1784019432"]