sea-dev 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (784) hide show
  1. package/.claude/tasks/README.md +89 -0
  2. package/.cursor/rules/commits.mdc +31 -0
  3. package/.cursor/rules/general.mdc +84 -0
  4. package/.github/workflows/ci-cd.yml +141 -0
  5. package/CLAUDE.md +337 -0
  6. package/README.md +129 -0
  7. package/apps/api/.prettierignore +6 -0
  8. package/apps/api/.prettierrc.js +3 -0
  9. package/apps/api/dotenvx-safe.sh +11 -0
  10. package/apps/api/eslint.config.mjs +3 -0
  11. package/apps/api/package.json +58 -0
  12. package/apps/api/src/clients/posthog.ts +25 -0
  13. package/apps/api/src/dal/submission.ts +59 -0
  14. package/apps/api/src/errors.ts +55 -0
  15. package/apps/api/src/index.ts +21 -0
  16. package/apps/api/src/lib/channel.ts +28 -0
  17. package/apps/api/src/lib/config.ts +9 -0
  18. package/apps/api/src/lib/fmt.test.ts +9 -0
  19. package/apps/api/src/lib/fmt.ts +62 -0
  20. package/apps/api/src/lib/invariant.ts +23 -0
  21. package/apps/api/src/middleware/auth.ts +66 -0
  22. package/apps/api/src/routes/index.ts +20 -0
  23. package/apps/api/src/routes/v2/chat/handlers.ts +693 -0
  24. package/apps/api/src/routes/v2/chat/index.ts +257 -0
  25. package/apps/api/src/routes/v2/chat/schemas.ts +43 -0
  26. package/apps/api/src/routes/v2/deals/handlers.ts +64 -0
  27. package/apps/api/src/routes/v2/deals/index.ts +88 -0
  28. package/apps/api/src/routes/v2/deals/schemas.ts +38 -0
  29. package/apps/api/src/routes/v2/forms/handlers.ts +415 -0
  30. package/apps/api/src/routes/v2/forms/index.ts +382 -0
  31. package/apps/api/src/routes/v2/forms/schemas.ts +243 -0
  32. package/apps/api/src/routes/v2/index.ts +19 -0
  33. package/apps/api/src/routes/v2/pipelines/handlers.ts +261 -0
  34. package/apps/api/src/routes/v2/pipelines/index.ts +224 -0
  35. package/apps/api/src/routes/v2/pipelines/schemas.ts +173 -0
  36. package/apps/api/src/routes/v2/submissions/handlers.ts +555 -0
  37. package/apps/api/src/routes/v2/submissions/index.ts +366 -0
  38. package/apps/api/src/routes/v2/submissions/schemas.ts +233 -0
  39. package/apps/api/src/routes/v2/workflows/handlers.ts +81 -0
  40. package/apps/api/src/routes/v2/workflows/index.ts +88 -0
  41. package/apps/api/src/routes/v2/workflows/schemas.ts +40 -0
  42. package/apps/api/src/server.ts +146 -0
  43. package/apps/api/src/static/favicon.ico +0 -0
  44. package/apps/api/src/types/api.ts +14 -0
  45. package/apps/api/src/types/result.ts +3 -0
  46. package/apps/api/tsconfig.json +22 -0
  47. package/apps/api/vite.config.ts +28 -0
  48. package/apps/api/vitest.config.ts +14 -0
  49. package/apps/conversion-worker/Dockerfile +59 -0
  50. package/apps/conversion-worker/package.json +31 -0
  51. package/apps/conversion-worker/src/lib/config.ts +7 -0
  52. package/apps/conversion-worker/src/main.ts +22 -0
  53. package/apps/conversion-worker/src/workflows/convert-pptx.ts +116 -0
  54. package/apps/conversion-worker/tsconfig.json +27 -0
  55. package/apps/conversion-worker/vite.config.ts +33 -0
  56. package/apps/main/.prettierignore +6 -0
  57. package/apps/main/.prettierrc.js +3 -0
  58. package/apps/main/CLAUDE.md +245 -0
  59. package/apps/main/Procfile +1 -0
  60. package/apps/main/README.md +193 -0
  61. package/apps/main/db-tests.jsonl +116 -0
  62. package/apps/main/dotenvx-safe.sh +11 -0
  63. package/apps/main/drizzle/meta/_journal.json +1 -0
  64. package/apps/main/drizzle.config.ts +25 -0
  65. package/apps/main/eslint.config.mjs +3 -0
  66. package/apps/main/generate-routes.mjs +5 -0
  67. package/apps/main/package.json +131 -0
  68. package/apps/main/playwright.config.ts +23 -0
  69. package/apps/main/postcss.config.ts +5 -0
  70. package/apps/main/public/bg-dark.svg +10 -0
  71. package/apps/main/public/bg.svg +10 -0
  72. package/apps/main/public/favicon.ico +0 -0
  73. package/apps/main/run.sh +146 -0
  74. package/apps/main/scripts/browser.ts +14 -0
  75. package/apps/main/scripts/db-test-cov.ts +277 -0
  76. package/apps/main/scripts/login.ts +78 -0
  77. package/apps/main/scripts/repl.ts +61 -0
  78. package/apps/main/src/_foo.ts +31 -0
  79. package/apps/main/src/_tests/db.test.ts +19 -0
  80. package/apps/main/src/_tests/mock-db.ts +60 -0
  81. package/apps/main/src/client.tsx +13 -0
  82. package/apps/main/src/clients/loops.ts +13 -0
  83. package/apps/main/src/clients/polar.ts +12 -0
  84. package/apps/main/src/clients/posthog.ts +12 -0
  85. package/apps/main/src/components/chat/chat-context.tsx +99 -0
  86. package/apps/main/src/components/chat/chat-messages.tsx +184 -0
  87. package/apps/main/src/components/chat/chat-status.tsx +140 -0
  88. package/apps/main/src/components/chat/chat.tsx +458 -0
  89. package/apps/main/src/components/chat/citation-modal.tsx +54 -0
  90. package/apps/main/src/components/cta.tsx +21 -0
  91. package/apps/main/src/components/data-display/derived.tsx +40 -0
  92. package/apps/main/src/components/data-display/group-single.tsx +57 -0
  93. package/apps/main/src/components/data-display/group-table.tsx +165 -0
  94. package/apps/main/src/components/data-display/group-wrapper.tsx +54 -0
  95. package/apps/main/src/components/data-display/item.tsx +678 -0
  96. package/apps/main/src/components/error.tsx +45 -0
  97. package/apps/main/src/components/forms/error.tsx +22 -0
  98. package/apps/main/src/components/grid.tsx +7 -0
  99. package/apps/main/src/components/header/container.tsx +73 -0
  100. package/apps/main/src/components/header/header-bar.tsx +102 -0
  101. package/apps/main/src/components/modals/copy-display.tsx +37 -0
  102. package/apps/main/src/components/modals/copy-form.tsx +152 -0
  103. package/apps/main/src/components/modals/duplicate-workflow.tsx +89 -0
  104. package/apps/main/src/components/modals/field-correction.tsx +323 -0
  105. package/apps/main/src/components/modals/form-viewer.tsx +126 -0
  106. package/apps/main/src/components/modals/modals.tsx +44 -0
  107. package/apps/main/src/components/modals/new-deal.tsx +78 -0
  108. package/apps/main/src/components/modals/new-form.tsx +133 -0
  109. package/apps/main/src/components/modals/new-pipeline.tsx +70 -0
  110. package/apps/main/src/components/modals/new-submission.tsx +321 -0
  111. package/apps/main/src/components/modals/new-workflow.tsx +342 -0
  112. package/apps/main/src/components/modals/transformation-sources-modal.tsx +157 -0
  113. package/apps/main/src/components/modals/view-report.tsx +193 -0
  114. package/apps/main/src/components/not-found.tsx +14 -0
  115. package/apps/main/src/components/search/search-bar.tsx +178 -0
  116. package/apps/main/src/components/sheet-selector.tsx +135 -0
  117. package/apps/main/src/components/side-panel/doc-list.tsx +480 -0
  118. package/apps/main/src/components/sidebar/admin-sidebar.tsx +75 -0
  119. package/apps/main/src/components/sidebar/app-sidebar.tsx +417 -0
  120. package/apps/main/src/components/sidebar/model-select.tsx +134 -0
  121. package/apps/main/src/components/sidebar/settings-sidebar.tsx +132 -0
  122. package/apps/main/src/components/sidebar/sidebar-right.tsx +22 -0
  123. package/apps/main/src/components/sidebar/stop-impersonate.tsx +21 -0
  124. package/apps/main/src/components/svg/loading.tsx +33 -0
  125. package/apps/main/src/components/theme-selector.tsx +43 -0
  126. package/apps/main/src/components/unsaved-badge.tsx +19 -0
  127. package/apps/main/src/components/upload/file-upload.tsx +354 -0
  128. package/apps/main/src/fns/submission-groups.ts +28 -0
  129. package/apps/main/src/fns/submission-items.ts +11 -0
  130. package/apps/main/src/global-middleware.ts +16 -0
  131. package/apps/main/src/hooks/use-update-state.ts +18 -0
  132. package/apps/main/src/lib/auth-client.ts +16 -0
  133. package/apps/main/src/lib/auth.test.ts +359 -0
  134. package/apps/main/src/lib/auth.ts +144 -0
  135. package/apps/main/src/lib/billing.ts +23 -0
  136. package/apps/main/src/lib/config-iso.ts +76 -0
  137. package/apps/main/src/lib/config.ts +61 -0
  138. package/apps/main/src/lib/excel.ts +16 -0
  139. package/apps/main/src/lib/feedback-cache.ts +70 -0
  140. package/apps/main/src/lib/logger.ts +44 -0
  141. package/apps/main/src/lib/models.ts +22 -0
  142. package/apps/main/src/lib/not-found.ts +17 -0
  143. package/apps/main/src/lib/pdf.ts +16 -0
  144. package/apps/main/src/lib/tabularize.ts +54 -0
  145. package/apps/main/src/lib/utils.ts +10 -0
  146. package/apps/main/src/lib/zfd.ts +217 -0
  147. package/apps/main/src/middleware.ts +55 -0
  148. package/apps/main/src/routeTree.gen.ts +1255 -0
  149. package/apps/main/src/router.tsx +24 -0
  150. package/apps/main/src/routes/__root.tsx +92 -0
  151. package/apps/main/src/routes/_authed/_app/(dashboard)/index.tsx +227 -0
  152. package/apps/main/src/routes/_authed/_app/agents/$agentId/config.tsx +224 -0
  153. package/apps/main/src/routes/_authed/_app/agents/$agentId/index.tsx +206 -0
  154. package/apps/main/src/routes/_authed/_app/agents/-components/agent-actions-menu.tsx +94 -0
  155. package/apps/main/src/routes/_authed/_app/agents/-components/agent-artifacts.tsx +153 -0
  156. package/apps/main/src/routes/_authed/_app/agents/-components/agent-chat.tsx +220 -0
  157. package/apps/main/src/routes/_authed/_app/agents/-components/agent-history-menu.tsx +81 -0
  158. package/apps/main/src/routes/_authed/_app/agents/-components/agent-model-select.tsx +84 -0
  159. package/apps/main/src/routes/_authed/_app/agents/-components/agent-relevant-items.tsx +226 -0
  160. package/apps/main/src/routes/_authed/_app/agents/-components/agent-upload-button.tsx +298 -0
  161. package/apps/main/src/routes/_authed/_app/agents/-components/context-modal.tsx +187 -0
  162. package/apps/main/src/routes/_authed/_app/agents/-fns.ts +560 -0
  163. package/apps/main/src/routes/_authed/_app/agents/index.tsx +65 -0
  164. package/apps/main/src/routes/_authed/_app/deals/$dealId/$subId/-components/citation-tree.tsx +268 -0
  165. package/apps/main/src/routes/_authed/_app/deals/$dealId/$subId.tsx +655 -0
  166. package/apps/main/src/routes/_authed/_app/deals/$dealId/-components/doc-loading.tsx +37 -0
  167. package/apps/main/src/routes/_authed/_app/deals/$dealId/-components/share-link.tsx +42 -0
  168. package/apps/main/src/routes/_authed/_app/deals/$dealId/-components/submission-card.tsx +89 -0
  169. package/apps/main/src/routes/_authed/_app/deals/$dealId/-components/submission-filter.tsx +193 -0
  170. package/apps/main/src/routes/_authed/_app/deals/$dealId/-components/submissions.tsx +36 -0
  171. package/apps/main/src/routes/_authed/_app/deals/$dealId/-components/summary.tsx +82 -0
  172. package/apps/main/src/routes/_authed/_app/deals/$dealId/-components/upload-doc.tsx +120 -0
  173. package/apps/main/src/routes/_authed/_app/deals/$dealId/-fns.ts +653 -0
  174. package/apps/main/src/routes/_authed/_app/deals/$dealId/index.tsx +259 -0
  175. package/apps/main/src/routes/_authed/_app/deals/$dealId/route.tsx +29 -0
  176. package/apps/main/src/routes/_authed/_app/deals/index.tsx +104 -0
  177. package/apps/main/src/routes/_authed/_app/feedback/index.tsx +639 -0
  178. package/apps/main/src/routes/_authed/_app/feedback/insights.tsx +250 -0
  179. package/apps/main/src/routes/_authed/_app/pipelines/$pipelineId/$runId/-components/blockers-panel.tsx +260 -0
  180. package/apps/main/src/routes/_authed/_app/pipelines/$pipelineId/$runId/-components/manual-input-panel.tsx +301 -0
  181. package/apps/main/src/routes/_authed/_app/pipelines/$pipelineId/$runId/-components/submission-selector-modal.tsx +143 -0
  182. package/apps/main/src/routes/_authed/_app/pipelines/$pipelineId/$runId/-components/upload-doc.tsx +120 -0
  183. package/apps/main/src/routes/_authed/_app/pipelines/$pipelineId/$runId/index.tsx +1485 -0
  184. package/apps/main/src/routes/_authed/_app/pipelines/$pipelineId/-components/dag-view.tsx +296 -0
  185. package/apps/main/src/routes/_authed/_app/pipelines/$pipelineId/-components/step-config-modal.tsx +634 -0
  186. package/apps/main/src/routes/_authed/_app/pipelines/$pipelineId/index.tsx +911 -0
  187. package/apps/main/src/routes/_authed/_app/pipelines/-fns.ts +510 -0
  188. package/apps/main/src/routes/_authed/_app/pipelines/index.tsx +103 -0
  189. package/apps/main/src/routes/_authed/_app/reports/$reportId.tsx +397 -0
  190. package/apps/main/src/routes/_authed/_app/reports/-fns.ts +11 -0
  191. package/apps/main/src/routes/_authed/_app/reports/index.tsx +22 -0
  192. package/apps/main/src/routes/_authed/_app/route.tsx +48 -0
  193. package/apps/main/src/routes/_authed/_app/submissions/-columns.tsx +161 -0
  194. package/apps/main/src/routes/_authed/_app/submissions/-fns.ts +128 -0
  195. package/apps/main/src/routes/_authed/_app/submissions/index.tsx +190 -0
  196. package/apps/main/src/routes/_authed/_app/workflows/$wfSlug/$formId.tsx +542 -0
  197. package/apps/main/src/routes/_authed/_app/workflows/$wfSlug/-components/derived.tsx +154 -0
  198. package/apps/main/src/routes/_authed/_app/workflows/$wfSlug/-components/field.tsx +369 -0
  199. package/apps/main/src/routes/_authed/_app/workflows/$wfSlug/-components/group.tsx +475 -0
  200. package/apps/main/src/routes/_authed/_app/workflows/$wfSlug/index.tsx +263 -0
  201. package/apps/main/src/routes/_authed/_app/workflows/$wfSlug/route.tsx +33 -0
  202. package/apps/main/src/routes/_authed/_app/workflows/-components/form-card.tsx +315 -0
  203. package/apps/main/src/routes/_authed/_app/workflows/index.tsx +86 -0
  204. package/apps/main/src/routes/_authed/admin/index.tsx +12 -0
  205. package/apps/main/src/routes/_authed/admin/route.tsx +42 -0
  206. package/apps/main/src/routes/_authed/admin/users/-columns.tsx +124 -0
  207. package/apps/main/src/routes/_authed/admin/users/-fns.ts +30 -0
  208. package/apps/main/src/routes/_authed/admin/users/index.tsx +29 -0
  209. package/apps/main/src/routes/_authed/catchNotFound.tsx +114 -0
  210. package/apps/main/src/routes/_authed/redirects/forms.$id.tsx +29 -0
  211. package/apps/main/src/routes/_authed/redirects/submissions.$id.tsx +27 -0
  212. package/apps/main/src/routes/_authed/redirects/workflows.$id.tsx +27 -0
  213. package/apps/main/src/routes/_authed/route.tsx +51 -0
  214. package/apps/main/src/routes/_authed/settings/-components/new-api-key.tsx +85 -0
  215. package/apps/main/src/routes/_authed/settings/-components/new-invite.tsx +100 -0
  216. package/apps/main/src/routes/_authed/settings/analytics.tsx +1710 -0
  217. package/apps/main/src/routes/_authed/settings/billing/-components/price-table.tsx +129 -0
  218. package/apps/main/src/routes/_authed/settings/billing/-fns.ts +76 -0
  219. package/apps/main/src/routes/_authed/settings/billing/index.tsx +119 -0
  220. package/apps/main/src/routes/_authed/settings/embed.tsx +337 -0
  221. package/apps/main/src/routes/_authed/settings/index.tsx +12 -0
  222. package/apps/main/src/routes/_authed/settings/keys.tsx +157 -0
  223. package/apps/main/src/routes/_authed/settings/members.tsx +276 -0
  224. package/apps/main/src/routes/_authed/settings/route.tsx +22 -0
  225. package/apps/main/src/routes/_authed/settings/user.tsx +87 -0
  226. package/apps/main/src/routes/_authed/settings/workspace.tsx +206 -0
  227. package/apps/main/src/routes/_public/-components/sign-in-up.tsx +96 -0
  228. package/apps/main/src/routes/_public/embedded.tsx +57 -0
  229. package/apps/main/src/routes/_public/invite.$inviteId.tsx +143 -0
  230. package/apps/main/src/routes/_public/no-access.tsx +38 -0
  231. package/apps/main/src/routes/_public/no-invite.tsx +39 -0
  232. package/apps/main/src/routes/_public/otp.tsx +103 -0
  233. package/apps/main/src/routes/_public/route.tsx +15 -0
  234. package/apps/main/src/routes/_public/sign-in.tsx +111 -0
  235. package/apps/main/src/routes/_public/sign-up.tsx +114 -0
  236. package/apps/main/src/routes/api/auth/$.ts +11 -0
  237. package/apps/main/src/routes/api/billing/paid.ts +42 -0
  238. package/apps/main/src/routes/api/billing/webhooks.ts +70 -0
  239. package/apps/main/src/routes/api/chat/agent.ts +40 -0
  240. package/apps/main/src/routes/api/chat/key.ts +42 -0
  241. package/apps/main/src/routes/api/chat/member.ts +35 -0
  242. package/apps/main/src/routes/api/test/index.ts +19 -0
  243. package/apps/main/src/server.tsx +6 -0
  244. package/apps/main/src/styles/app.css +23 -0
  245. package/apps/main/src/vite-env.d.ts +7 -0
  246. package/apps/main/test.http +6 -0
  247. package/apps/main/tsconfig.json +17 -0
  248. package/apps/main/vite.config.ts +24 -0
  249. package/apps/main/vitest.config.js +17 -0
  250. package/apps/mcp/README.md +171 -0
  251. package/apps/mcp/eslint.config.mjs +3 -0
  252. package/apps/mcp/package.json +37 -0
  253. package/apps/mcp/src/index.ts +414 -0
  254. package/apps/mcp/tsconfig.json +19 -0
  255. package/apps/mcp/vite.config.ts +22 -0
  256. package/apps/posthog-proxy/index.html +9 -0
  257. package/apps/workers/.prettierignore +7 -0
  258. package/apps/workers/.prettierrc.js +3 -0
  259. package/apps/workers/dotenvx-safe.sh +11 -0
  260. package/apps/workers/eslint.config.mjs +3 -0
  261. package/apps/workers/package.json +65 -0
  262. package/apps/workers/src/lib/config.ts +7 -0
  263. package/apps/workers/src/lib/messages.ts +0 -0
  264. package/apps/workers/src/lib/posthog.ts +25 -0
  265. package/apps/workers/src/main.ts +58 -0
  266. package/apps/workers/src/workflows/extraction.ts +866 -0
  267. package/apps/workers/src/workflows/index.ts +3 -0
  268. package/apps/workers/src/workflows/pipeline-dag.ts +210 -0
  269. package/apps/workers/src/workflows/pipeline-steps.ts +1393 -0
  270. package/apps/workers/tsconfig.json +16 -0
  271. package/apps/workers/vite.config.ts +35 -0
  272. package/docs/CHANGELOG.md +84 -0
  273. package/docs/agent-templates-and-runs.md +859 -0
  274. package/docs/aws-migration-plan.md +267 -0
  275. package/docs/impl-p0-form-builder-improvements.md +683 -0
  276. package/docs/on-prem-deployment-spec.docx +0 -0
  277. package/docs/on-prem-deployment-spec.md +378 -0
  278. package/docs/prd-form-builder-strategy.md +1120 -0
  279. package/docs/widget-ng-apf-packaging-spec.md +43 -0
  280. package/infra/k8s/charts/seadotdev/Chart.yaml +6 -0
  281. package/infra/k8s/charts/seadotdev/templates/_helpers.tpl +27 -0
  282. package/infra/k8s/charts/seadotdev/templates/api-v2.yaml +105 -0
  283. package/infra/k8s/charts/seadotdev/templates/external-secrets.yaml +83 -0
  284. package/infra/k8s/charts/seadotdev/templates/ingress.yaml +54 -0
  285. package/infra/k8s/charts/seadotdev/templates/main-app.yaml +104 -0
  286. package/infra/k8s/charts/seadotdev/templates/workers.yaml +182 -0
  287. package/infra/k8s/charts/seadotdev/values.yaml +143 -0
  288. package/infra/terraform/main.tf +399 -0
  289. package/libs/ai/.prettierignore +2 -0
  290. package/libs/ai/.prettierrc.js +5 -0
  291. package/libs/ai/README.md +139 -0
  292. package/libs/ai/eslint.config.mjs +3 -0
  293. package/libs/ai/package.json +42 -0
  294. package/libs/ai/src/index.ts +5 -0
  295. package/libs/ai/src/models.ts +19 -0
  296. package/libs/ai/src/rag/index.ts +1 -0
  297. package/libs/ai/src/rag/rag.test.ts +99 -0
  298. package/libs/ai/src/rag/rag.ts +510 -0
  299. package/libs/ai/tsconfig.json +21 -0
  300. package/libs/ai/vite.config.ts +38 -0
  301. package/libs/cache/.prettierignore +2 -0
  302. package/libs/cache/eslint.config.mjs +3 -0
  303. package/libs/cache/package.json +35 -0
  304. package/libs/cache/src/feedback.ts +77 -0
  305. package/libs/cache/src/index.ts +2 -0
  306. package/libs/cache/tsconfig.json +19 -0
  307. package/libs/cache/vite.config.ts +36 -0
  308. package/libs/clients/.prettierignore +6 -0
  309. package/libs/clients/eslint.config.mjs +3 -0
  310. package/libs/clients/package.json +59 -0
  311. package/libs/clients/src/azure.ts +249 -0
  312. package/libs/clients/src/gcp.ts +220 -0
  313. package/libs/clients/src/hatchet.ts +86 -0
  314. package/libs/clients/src/index.ts +8 -0
  315. package/libs/clients/src/loops.ts +86 -0
  316. package/libs/clients/src/polar.ts +77 -0
  317. package/libs/clients/src/posthog.ts +55 -0
  318. package/libs/clients/tsconfig.json +19 -0
  319. package/libs/clients/vite.config.ts +35 -0
  320. package/libs/config/.prettierignore +6 -0
  321. package/libs/config/.prettierrc.js +12 -0
  322. package/libs/config/eslint.config.mjs +3 -0
  323. package/libs/config/package.json +50 -0
  324. package/libs/config/src/azure.ts +54 -0
  325. package/libs/config/src/db.ts +18 -0
  326. package/libs/config/src/gcp.ts +53 -0
  327. package/libs/config/src/google.ts +17 -0
  328. package/libs/config/src/hatchet.ts +20 -0
  329. package/libs/config/src/index.ts +108 -0
  330. package/libs/config/src/llm.ts +17 -0
  331. package/libs/config/src/polar.ts +24 -0
  332. package/libs/config/src/util.ts +8 -0
  333. package/libs/config/src/vercel.ts +26 -0
  334. package/libs/config/tsconfig.json +19 -0
  335. package/libs/config/vite.config.ts +34 -0
  336. package/libs/core/.prettierignore +2 -0
  337. package/libs/core/eslint.config.mjs +3 -0
  338. package/libs/core/package.json +59 -0
  339. package/libs/core/src/chat/derived.ts +97 -0
  340. package/libs/core/src/chat/feedback.ts +293 -0
  341. package/libs/core/src/chat/index.ts +6 -0
  342. package/libs/core/src/chat/model.ts +92 -0
  343. package/libs/core/src/chat/prepare-tools.ts +286 -0
  344. package/libs/core/src/chat/prompts.ts +623 -0
  345. package/libs/core/src/chat/stream.ts +311 -0
  346. package/libs/core/src/chat/summarize.ts +168 -0
  347. package/libs/core/src/chat/tools/agent.ts +403 -0
  348. package/libs/core/src/chat/tools/chart-agent.ts +526 -0
  349. package/libs/core/src/chat/tools/chart-helpers/sandbox.ts +47 -0
  350. package/libs/core/src/chat/tools/chart.ts +86 -0
  351. package/libs/core/src/chat/tools/credit-agent.ts +1383 -0
  352. package/libs/core/src/chat/tools/credit.ts +1435 -0
  353. package/libs/core/src/chat/tools/deep-dive-agent.ts +100 -0
  354. package/libs/core/src/chat/tools/deep-dive.ts +141 -0
  355. package/libs/core/src/chat/tools/form.ts +449 -0
  356. package/libs/core/src/chat/tools/helpers.ts +91 -0
  357. package/libs/core/src/chat/tools/index.ts +42 -0
  358. package/libs/core/src/chat/tools/pipeline-artifact.ts +76 -0
  359. package/libs/core/src/chat/tools/report.ts +40 -0
  360. package/libs/core/src/chat/tools/search.ts +390 -0
  361. package/libs/core/src/chat/tools/submission.ts +227 -0
  362. package/libs/core/src/chat/tools/workflow.ts +684 -0
  363. package/libs/core/src/chat/types.ts +3 -0
  364. package/libs/core/src/data-extraction/classification/azure.ts +168 -0
  365. package/libs/core/src/data-extraction/classification/index.ts +1 -0
  366. package/libs/core/src/data-extraction/dal.ts +246 -0
  367. package/libs/core/src/data-extraction/form-structure-extractor.ts +294 -0
  368. package/libs/core/src/data-extraction/index.ts +4 -0
  369. package/libs/core/src/data-extraction/layout/azure.ts +730 -0
  370. package/libs/core/src/data-extraction/layout/excel.ts +180 -0
  371. package/libs/core/src/data-extraction/layout/gcp.ts +1071 -0
  372. package/libs/core/src/data-extraction/layout/index.ts +266 -0
  373. package/libs/core/src/data-extraction/layout/plaintext.ts +45 -0
  374. package/libs/core/src/data-extraction/models.ts +38 -0
  375. package/libs/core/src/data-extraction/pdf-utils.ts +96 -0
  376. package/libs/core/src/data-extraction/structuring/bank-statement.ts +1182 -0
  377. package/libs/core/src/data-extraction/structuring/custom.ts +495 -0
  378. package/libs/core/src/data-extraction/structuring/index.ts +290 -0
  379. package/libs/core/src/data-extraction/structuring/prompts.ts +69 -0
  380. package/libs/core/src/data-extraction/type-guards.ts +110 -0
  381. package/libs/core/src/data-extraction/types.ts +84 -0
  382. package/libs/core/src/data-extraction/utils.ts +31 -0
  383. package/libs/core/src/data-extraction/validation/bank-statement.ts +127 -0
  384. package/libs/core/src/deals.ts +17 -0
  385. package/libs/core/src/documents.ts +152 -0
  386. package/libs/core/src/index.ts +5 -0
  387. package/libs/core/src/pipelines/display.ts +678 -0
  388. package/libs/core/src/pipelines/execute.ts +2342 -0
  389. package/libs/core/src/pipelines/index.ts +4 -0
  390. package/libs/core/src/pipelines/list.ts +12 -0
  391. package/libs/core/src/pipelines/runs.ts +53 -0
  392. package/libs/core/tsconfig.json +20 -0
  393. package/libs/core/vite.config.ts +56 -0
  394. package/libs/dal/.prettierignore +6 -0
  395. package/libs/dal/.prettierrc.js +12 -0
  396. package/libs/dal/eslint.config.mjs +3 -0
  397. package/libs/dal/package.json +57 -0
  398. package/libs/dal/src/_tests/db.test.ts +19 -0
  399. package/libs/dal/src/_tests/mock-db.ts +60 -0
  400. package/libs/dal/src/api-key.test.ts +397 -0
  401. package/libs/dal/src/api-key.ts +110 -0
  402. package/libs/dal/src/billing.ts +23 -0
  403. package/libs/dal/src/conversation.test.ts +655 -0
  404. package/libs/dal/src/conversation.ts +532 -0
  405. package/libs/dal/src/deal.test.ts +45 -0
  406. package/libs/dal/src/deal.ts +87 -0
  407. package/libs/dal/src/defaults-consumer-lending-uk.ts +33 -0
  408. package/libs/dal/src/defaults-consumer-lending-us.ts +33 -0
  409. package/libs/dal/src/defaults-private-credit.ts +57 -0
  410. package/libs/dal/src/defaults-private-equity.ts +51 -0
  411. package/libs/dal/src/defaults-smb-lending-us.ts +1569 -0
  412. package/libs/dal/src/defaults-sme-lending-uk-express.ts +1527 -0
  413. package/libs/dal/src/defaults-sme-lending-uk.ts +1669 -0
  414. package/libs/dal/src/defaults-types.ts +23 -0
  415. package/libs/dal/src/defaults.ts +550 -0
  416. package/libs/dal/src/document.test.ts +70 -0
  417. package/libs/dal/src/document.ts +192 -0
  418. package/libs/dal/src/feedback.ts +255 -0
  419. package/libs/dal/src/form.test.ts +637 -0
  420. package/libs/dal/src/form.ts +1165 -0
  421. package/libs/dal/src/index.ts +20 -0
  422. package/libs/dal/src/invitation.test.ts +746 -0
  423. package/libs/dal/src/invitation.ts +207 -0
  424. package/libs/dal/src/member.test.ts +185 -0
  425. package/libs/dal/src/member.ts +80 -0
  426. package/libs/dal/src/organization.ts +116 -0
  427. package/libs/dal/src/permission.ts +25 -0
  428. package/libs/dal/src/pipeline.test.ts +388 -0
  429. package/libs/dal/src/pipeline.ts +4222 -0
  430. package/libs/dal/src/report.ts +199 -0
  431. package/libs/dal/src/result.ts +16 -0
  432. package/libs/dal/src/search.ts +172 -0
  433. package/libs/dal/src/session.test.ts +110 -0
  434. package/libs/dal/src/session.ts +31 -0
  435. package/libs/dal/src/submission.test.ts +1304 -0
  436. package/libs/dal/src/submission.ts +1396 -0
  437. package/libs/dal/src/tool.ts +159 -0
  438. package/libs/dal/src/user.ts +16 -0
  439. package/libs/dal/src/workflow.test.ts +89 -0
  440. package/libs/dal/src/workflow.ts +262 -0
  441. package/libs/dal/tsconfig.build.json +4 -0
  442. package/libs/dal/tsconfig.json +22 -0
  443. package/libs/dal/vite.config.ts +34 -0
  444. package/libs/db/.prettierignore +6 -0
  445. package/libs/db/.prettierrc.js +12 -0
  446. package/libs/db/eslint.config.mjs +3 -0
  447. package/libs/db/package.json +52 -0
  448. package/libs/db/src/index.ts +24 -0
  449. package/libs/db/src/relations.ts +549 -0
  450. package/libs/db/src/schema.ts +2 -0
  451. package/libs/db/src/schemas/api.ts +35 -0
  452. package/libs/db/src/schemas/conversations.ts +175 -0
  453. package/libs/db/src/schemas/core.ts +359 -0
  454. package/libs/db/src/schemas/documents.ts +181 -0
  455. package/libs/db/src/schemas/feedback.ts +40 -0
  456. package/libs/db/src/schemas/index.ts +26 -0
  457. package/libs/db/src/schemas/organisations.ts +97 -0
  458. package/libs/db/src/schemas/pipelines.ts +440 -0
  459. package/libs/db/src/schemas/users.ts +95 -0
  460. package/libs/db/src/types.ts +190 -0
  461. package/libs/db/src/utils.ts +14 -0
  462. package/libs/db/tsconfig.json +19 -0
  463. package/libs/db/vite.config.ts +31 -0
  464. package/libs/lint/.prettierignore +6 -0
  465. package/libs/lint/eslint.config.mjs +61 -0
  466. package/libs/lint/package.json +29 -0
  467. package/libs/lint/prettier.config.js +12 -0
  468. package/libs/schemas/.prettierignore +6 -0
  469. package/libs/schemas/.prettierrc.js +12 -0
  470. package/libs/schemas/README.md +15 -0
  471. package/libs/schemas/eslint.config.mjs +3 -0
  472. package/libs/schemas/package.json +67 -0
  473. package/libs/schemas/src/core/chat.ts +67 -0
  474. package/libs/schemas/src/core/core-result.ts +15 -0
  475. package/libs/schemas/src/core/data-extraction.ts +184 -0
  476. package/libs/schemas/src/core/layout.ts +478 -0
  477. package/libs/schemas/src/core/pipeline.ts +128 -0
  478. package/libs/schemas/src/core/submission.ts +97 -0
  479. package/libs/schemas/src/db/account.ts +57 -0
  480. package/libs/schemas/src/db/apiKey.ts +57 -0
  481. package/libs/schemas/src/db/context.ts +33 -0
  482. package/libs/schemas/src/db/conversation.ts +65 -0
  483. package/libs/schemas/src/db/deal.ts +42 -0
  484. package/libs/schemas/src/db/document.ts +103 -0
  485. package/libs/schemas/src/db/documentCitation.ts +58 -0
  486. package/libs/schemas/src/db/documentExtraction.ts +69 -0
  487. package/libs/schemas/src/db/fieldCorrection.ts +85 -0
  488. package/libs/schemas/src/db/form.ts +45 -0
  489. package/libs/schemas/src/db/formField.ts +59 -0
  490. package/libs/schemas/src/db/formGroup.ts +42 -0
  491. package/libs/schemas/src/db/impersonation.ts +39 -0
  492. package/libs/schemas/src/db/index.ts +25 -0
  493. package/libs/schemas/src/db/invitation.ts +42 -0
  494. package/libs/schemas/src/db/member.ts +36 -0
  495. package/libs/schemas/src/db/message.ts +58 -0
  496. package/libs/schemas/src/db/organization.ts +62 -0
  497. package/libs/schemas/src/db/session.ts +48 -0
  498. package/libs/schemas/src/db/submission.ts +54 -0
  499. package/libs/schemas/src/db/submissionGroup.ts +36 -0
  500. package/libs/schemas/src/db/submissionItem.ts +33 -0
  501. package/libs/schemas/src/db/submissionItemVersion.ts +70 -0
  502. package/libs/schemas/src/db/user.ts +51 -0
  503. package/libs/schemas/src/db/utils.ts +3 -0
  504. package/libs/schemas/src/db/verification.ts +36 -0
  505. package/libs/schemas/src/db/workflow.ts +42 -0
  506. package/libs/schemas/src/index.ts +10 -0
  507. package/libs/schemas/tsconfig.json +21 -0
  508. package/libs/schemas/vite.config.ts +38 -0
  509. package/libs/ui/.prettierignore +6 -0
  510. package/libs/ui/.prettierrc.js +12 -0
  511. package/libs/ui/components.json +24 -0
  512. package/libs/ui/eslint.config.mjs +3 -0
  513. package/libs/ui/package.json +142 -0
  514. package/libs/ui/src/components/chart-viz/chart.tsx +255 -0
  515. package/libs/ui/src/components/chart-viz/converters.ts +474 -0
  516. package/libs/ui/src/components/chart-viz/dashboard.tsx +146 -0
  517. package/libs/ui/src/components/chart-viz/index.ts +37 -0
  518. package/libs/ui/src/components/chart-viz/markdown.tsx +344 -0
  519. package/libs/ui/src/components/chart-viz/table.tsx +446 -0
  520. package/libs/ui/src/components/chart-viz/theme-context.tsx +70 -0
  521. package/libs/ui/src/components/chart-viz/themes/dark.ts +98 -0
  522. package/libs/ui/src/components/chart-viz/themes/index.ts +69 -0
  523. package/libs/ui/src/components/chart-viz/themes/light.ts +98 -0
  524. package/libs/ui/src/components/chart-viz/themes/tailwind.ts +326 -0
  525. package/libs/ui/src/components/chart-viz/themes/types.ts +99 -0
  526. package/libs/ui/src/components/chart-viz/tool-display.tsx +150 -0
  527. package/libs/ui/src/components/chart-viz/types.ts +95 -0
  528. package/libs/ui/src/components/doc-viewers/excel/index.tsx +431 -0
  529. package/libs/ui/src/components/doc-viewers/excel/themes.ts +160 -0
  530. package/libs/ui/src/components/doc-viewers/image/index.tsx +410 -0
  531. package/libs/ui/src/components/doc-viewers/pdf/index.tsx +258 -0
  532. package/libs/ui/src/components/doc-viewers/pdf/virtualized-pdf.tsx +556 -0
  533. package/libs/ui/src/components/misc/rel-date.tsx +52 -0
  534. package/libs/ui/src/components/misc/styled-link.tsx +2 -0
  535. package/libs/ui/src/components/table/data-table.tsx +546 -0
  536. package/libs/ui/src/components/table/report-table.tsx +305 -0
  537. package/libs/ui/src/components/table/sortable-column.tsx +34 -0
  538. package/libs/ui/src/components/ui/accordion.tsx +62 -0
  539. package/libs/ui/src/components/ui/alert-dialog.tsx +142 -0
  540. package/libs/ui/src/components/ui/alert.tsx +62 -0
  541. package/libs/ui/src/components/ui/artifact.tsx +118 -0
  542. package/libs/ui/src/components/ui/attachments.tsx +388 -0
  543. package/libs/ui/src/components/ui/avatar.tsx +39 -0
  544. package/libs/ui/src/components/ui/badge.tsx +43 -0
  545. package/libs/ui/src/components/ui/breadcrumb.tsx +102 -0
  546. package/libs/ui/src/components/ui/button-group.tsx +78 -0
  547. package/libs/ui/src/components/ui/button.tsx +79 -0
  548. package/libs/ui/src/components/ui/card.tsx +32 -0
  549. package/libs/ui/src/components/ui/carousel.tsx +228 -0
  550. package/libs/ui/src/components/ui/chain-of-thought.tsx +198 -0
  551. package/libs/ui/src/components/ui/checkbox.tsx +27 -0
  552. package/libs/ui/src/components/ui/citation.tsx +34 -0
  553. package/libs/ui/src/components/ui/code-block.tsx +500 -0
  554. package/libs/ui/src/components/ui/collapsible.tsx +19 -0
  555. package/libs/ui/src/components/ui/command.tsx +161 -0
  556. package/libs/ui/src/components/ui/conversation.tsx +90 -0
  557. package/libs/ui/src/components/ui/dialog.tsx +142 -0
  558. package/libs/ui/src/components/ui/dropdown-menu.tsx +246 -0
  559. package/libs/ui/src/components/ui/highlight.tsx +3 -0
  560. package/libs/ui/src/components/ui/hover-card.tsx +36 -0
  561. package/libs/ui/src/components/ui/inline-citation.tsx +251 -0
  562. package/libs/ui/src/components/ui/input-group.tsx +156 -0
  563. package/libs/ui/src/components/ui/input-otp.tsx +78 -0
  564. package/libs/ui/src/components/ui/input.tsx +21 -0
  565. package/libs/ui/src/components/ui/label.tsx +19 -0
  566. package/libs/ui/src/components/ui/model-selector.tsx +174 -0
  567. package/libs/ui/src/components/ui/multisidebar.tsx +750 -0
  568. package/libs/ui/src/components/ui/popover.tsx +43 -0
  569. package/libs/ui/src/components/ui/progress.tsx +28 -0
  570. package/libs/ui/src/components/ui/reasoning.tsx +178 -0
  571. package/libs/ui/src/components/ui/resizable.tsx +49 -0
  572. package/libs/ui/src/components/ui/scroll-area.tsx +54 -0
  573. package/libs/ui/src/components/ui/select.tsx +171 -0
  574. package/libs/ui/src/components/ui/separator.tsx +26 -0
  575. package/libs/ui/src/components/ui/sheet.tsx +128 -0
  576. package/libs/ui/src/components/ui/shimmer.tsx +53 -0
  577. package/libs/ui/src/components/ui/skeleton.tsx +13 -0
  578. package/libs/ui/src/components/ui/sonner.tsx +23 -0
  579. package/libs/ui/src/components/ui/switch.tsx +26 -0
  580. package/libs/ui/src/components/ui/table.tsx +96 -0
  581. package/libs/ui/src/components/ui/tabs.tsx +52 -0
  582. package/libs/ui/src/components/ui/textarea.tsx +41 -0
  583. package/libs/ui/src/components/ui/tool.tsx +209 -0
  584. package/libs/ui/src/components/ui/tooltip.tsx +58 -0
  585. package/libs/ui/src/components/ui/typography.tsx +113 -0
  586. package/libs/ui/src/fonts/manrope-v15-latin-300.woff2 +0 -0
  587. package/libs/ui/src/fonts/manrope-v15-latin-400.woff2 +0 -0
  588. package/libs/ui/src/fonts/manrope-v15-latin-500.woff2 +0 -0
  589. package/libs/ui/src/fonts/manrope-v15-latin-600.woff2 +0 -0
  590. package/libs/ui/src/hooks/use-mobile.ts +19 -0
  591. package/libs/ui/src/lib/utils.ts +6 -0
  592. package/libs/ui/src/styles/fonts.css +35 -0
  593. package/libs/ui/src/styles/style.css +218 -0
  594. package/libs/ui/tsconfig.json +21 -0
  595. package/libs/ui/vite.config.ts +80 -0
  596. package/libs/ui-lit/README.md +245 -0
  597. package/libs/ui-lit/TESTING_GUIDE.md +296 -0
  598. package/libs/ui-lit/eslint.config.mjs +3 -0
  599. package/libs/ui-lit/package.json +41 -0
  600. package/libs/ui-lit/scripts/build-css.js +43 -0
  601. package/libs/ui-lit/src/components/sea-alert.ts +132 -0
  602. package/libs/ui-lit/src/components/sea-button.ts +95 -0
  603. package/libs/ui-lit/src/components/sea-card.ts +113 -0
  604. package/libs/ui-lit/src/components/sea-input.ts +184 -0
  605. package/libs/ui-lit/src/components/sea-spinner.ts +65 -0
  606. package/libs/ui-lit/src/index.ts +15 -0
  607. package/libs/ui-lit/src/lib/utils.ts +6 -0
  608. package/libs/ui-lit/src/styles/tailwind.css +76 -0
  609. package/libs/ui-lit/src/theme.css +66 -0
  610. package/libs/ui-lit/src/theme.ts +79 -0
  611. package/libs/ui-lit/src/vite-env.d.ts +6 -0
  612. package/libs/ui-lit/tailwind.config.ts +50 -0
  613. package/libs/ui-lit/test.html +289 -0
  614. package/libs/ui-lit/tsconfig.json +23 -0
  615. package/libs/ui-lit/vite.config.ts +31 -0
  616. package/libs/ui-lit/vite.css.config.ts +20 -0
  617. package/libs/util/.prettierignore +6 -0
  618. package/libs/util/.prettierrc.js +12 -0
  619. package/libs/util/eslint.config.mjs +3 -0
  620. package/libs/util/package.json +45 -0
  621. package/libs/util/src/billing.ts +10 -0
  622. package/libs/util/src/data-transform.ts +19 -0
  623. package/libs/util/src/encryption.ts +45 -0
  624. package/libs/util/src/fmt.test.ts +9 -0
  625. package/libs/util/src/fmt.ts +71 -0
  626. package/libs/util/src/fuzzy.ts +47 -0
  627. package/libs/util/src/id.ts +24 -0
  628. package/libs/util/src/invariant.ts +31 -0
  629. package/libs/util/src/sub-name.ts +7 -0
  630. package/libs/util/tsconfig.json +19 -0
  631. package/libs/util/vite.config.ts +34 -0
  632. package/package.json +28 -0
  633. package/packages/widget/.prettierignore +6 -0
  634. package/packages/widget/.prettierrc.js +12 -0
  635. package/packages/widget/README.md +95 -0
  636. package/packages/widget/eslint.config.mjs +11 -0
  637. package/packages/widget/openapi-ts.config.ts +8 -0
  638. package/packages/widget/package.json +89 -0
  639. package/packages/widget/postcss.config.mjs +10 -0
  640. package/packages/widget/src/clients/api/client/client.ts +187 -0
  641. package/packages/widget/src/clients/api/client/index.ts +22 -0
  642. package/packages/widget/src/clients/api/client/types.ts +192 -0
  643. package/packages/widget/src/clients/api/client/utils.ts +394 -0
  644. package/packages/widget/src/clients/api/client.gen.ts +18 -0
  645. package/packages/widget/src/clients/api/core/auth.ts +39 -0
  646. package/packages/widget/src/clients/api/core/bodySerializer.ts +74 -0
  647. package/packages/widget/src/clients/api/core/params.ts +132 -0
  648. package/packages/widget/src/clients/api/core/pathSerializer.ts +169 -0
  649. package/packages/widget/src/clients/api/core/types.ts +80 -0
  650. package/packages/widget/src/clients/api/index.ts +3 -0
  651. package/packages/widget/src/clients/api/sdk.gen.ts +805 -0
  652. package/packages/widget/src/clients/api/types.gen.ts +2085 -0
  653. package/packages/widget/src/components/container.tsx +42 -0
  654. package/packages/widget/src/components/data-display.tsx +384 -0
  655. package/packages/widget/src/components/data-viewer.tsx +311 -0
  656. package/packages/widget/src/components/doc-list.tsx +102 -0
  657. package/packages/widget/src/components/field-correction-modal.tsx +265 -0
  658. package/packages/widget/src/components/header.tsx +71 -0
  659. package/packages/widget/src/components/new-submission.tsx +290 -0
  660. package/packages/widget/src/components/sidebar-right.tsx +19 -0
  661. package/packages/widget/src/components/submission-card.tsx +66 -0
  662. package/packages/widget/src/components/submission-page.tsx +75 -0
  663. package/packages/widget/src/components/upload-doc.tsx +241 -0
  664. package/packages/widget/src/components/widget.tsx +101 -0
  665. package/packages/widget/src/index.tsx +167 -0
  666. package/packages/widget/src/lib/config.ts +2 -0
  667. package/packages/widget/src/lib/util.ts +40 -0
  668. package/packages/widget/src/styles/index.css +5 -0
  669. package/packages/widget/src/styles/tw-properties.css +337 -0
  670. package/packages/widget/src/vite-env.d.ts +3 -0
  671. package/packages/widget/tsconfig.app.json +35 -0
  672. package/packages/widget/tsconfig.json +4 -0
  673. package/packages/widget/tsconfig.node.json +24 -0
  674. package/packages/widget/vite.config.ts +116 -0
  675. package/packages/widget-lit/BOTTLENECKS.md +250 -0
  676. package/packages/widget-lit/IMPLEMENTATION_SUMMARY.md +295 -0
  677. package/packages/widget-lit/README.md +232 -0
  678. package/packages/widget-lit/eslint.config.mjs +3 -0
  679. package/packages/widget-lit/package.json +52 -0
  680. package/packages/widget-lit/src/api-client.ts +230 -0
  681. package/packages/widget-lit/src/api-client.ts.backup +218 -0
  682. package/packages/widget-lit/src/components/sea-chat.ts +382 -0
  683. package/packages/widget-lit/src/components/sea-submission-viewer.ts +267 -0
  684. package/packages/widget-lit/src/components/sea-widget.ts +317 -0
  685. package/packages/widget-lit/src/index.ts +48 -0
  686. package/packages/widget-lit/src/react.ts +58 -0
  687. package/packages/widget-lit/src/style.css +47 -0
  688. package/packages/widget-lit/tsconfig.json +24 -0
  689. package/packages/widget-lit/vite.config.ts +29 -0
  690. package/packages/widget-ng/DEVELOPMENT.md +74 -0
  691. package/packages/widget-ng/README.md +657 -0
  692. package/packages/widget-ng/dev.sh +14 -0
  693. package/packages/widget-ng/eslint.config.mjs +24 -0
  694. package/packages/widget-ng/ng-package.json +9 -0
  695. package/packages/widget-ng/package.json +85 -0
  696. package/packages/widget-ng/src/index.ts +45 -0
  697. package/packages/widget-ng/src/lib/components/sea-chat.component.ts +737 -0
  698. package/packages/widget-ng/src/lib/components/sea-data-viewer.component.ts +2240 -0
  699. package/packages/widget-ng/src/lib/components/sea-deal-form-modal.component.ts +702 -0
  700. package/packages/widget-ng/src/lib/components/sea-document-list.component.ts +350 -0
  701. package/packages/widget-ng/src/lib/components/sea-feedback-modal.component.ts +461 -0
  702. package/packages/widget-ng/src/lib/components/sea-file-upload.component.ts +655 -0
  703. package/packages/widget-ng/src/lib/components/sea-model-selection-modal.component.ts +367 -0
  704. package/packages/widget-ng/src/lib/components/sea-new-submission-modal.component.ts +414 -0
  705. package/packages/widget-ng/src/lib/components/sea-pdf-viewer.component.ts +869 -0
  706. package/packages/widget-ng/src/lib/components/sea-submission-card.component.ts +251 -0
  707. package/packages/widget-ng/src/lib/components/sea-widget.component.ts +684 -0
  708. package/packages/widget-ng/src/lib/models/submission.model.ts +170 -0
  709. package/packages/widget-ng/src/lib/pipes/markdown.pipe.ts +57 -0
  710. package/packages/widget-ng/src/lib/services/api-client.service.ts +715 -0
  711. package/packages/widget-ng/src/lib/services/chat.service.ts +330 -0
  712. package/packages/widget-ng/src/lib/services/config.service.ts +107 -0
  713. package/packages/widget-ng/src/web-component.ts +56 -0
  714. package/packages/widget-ng/tsconfig.json +25 -0
  715. package/packages/widget-ng/tsconfig.lib.json +9 -0
  716. package/packages/widget-ng/vite.config.elements.ts +26 -0
  717. package/packages/widget-ng/vitest.config.ts +19 -0
  718. package/packages/widget-ng/vitest.setup.ts +13 -0
  719. package/pnpm-workspace.yaml +18 -0
  720. package/render.yaml +136 -0
  721. package/scripts/README.md +57 -0
  722. package/scripts/package.json +22 -0
  723. package/scripts/python/.python-version +1 -0
  724. package/scripts/python/README.md +3 -0
  725. package/scripts/python/export-org-data.py +693 -0
  726. package/scripts/python/pyproject.toml +29 -0
  727. package/scripts/python/requirements-dev.lock +36 -0
  728. package/scripts/python/requirements.lock +36 -0
  729. package/scripts/python/src/gen.py +297 -0
  730. package/scripts/python/test.py +34 -0
  731. package/scripts/src/fix-storage-provider-mismatch.ts +239 -0
  732. package/scripts/src/sync-render-yaml.ts +290 -0
  733. package/scripts/src/test-chat-stream.ts +300 -0
  734. package/scripts/src/test-reconciliation.ts +230 -0
  735. package/scripts/tsconfig.json +15 -0
  736. package/tests/angular-test-app/.vscode/extensions.json +4 -0
  737. package/tests/angular-test-app/.vscode/launch.json +13 -0
  738. package/tests/angular-test-app/.vscode/tasks.json +24 -0
  739. package/tests/angular-test-app/README.md +59 -0
  740. package/tests/angular-test-app/angular.json +111 -0
  741. package/tests/angular-test-app/clean-start.sh +14 -0
  742. package/tests/angular-test-app/package.json +36 -0
  743. package/tests/angular-test-app/public/favicon.ico +0 -0
  744. package/tests/angular-test-app/src/app/app.component.ts +220 -0
  745. package/tests/angular-test-app/src/app/app.config.ts +5 -0
  746. package/tests/angular-test-app/src/env.d.ts +13 -0
  747. package/tests/angular-test-app/src/index.html +13 -0
  748. package/tests/angular-test-app/src/main.ts +6 -0
  749. package/tests/angular-test-app/src/styles.css +8 -0
  750. package/tests/angular-test-app/tsconfig.app.json +15 -0
  751. package/tests/angular-test-app/tsconfig.json +27 -0
  752. package/tests/crm-viewer-app/API_INTEGRATION_SUMMARY.md +295 -0
  753. package/tests/crm-viewer-app/CURRENT_ASSETS_FIELDS.md +148 -0
  754. package/tests/crm-viewer-app/FIELD_ID_MAPPING.md +206 -0
  755. package/tests/crm-viewer-app/INTEGRATION_GUIDE.md +309 -0
  756. package/tests/crm-viewer-app/README.md +174 -0
  757. package/tests/crm-viewer-app/REAL_API_INTEGRATION.md +240 -0
  758. package/tests/crm-viewer-app/UPDATED_IMPLEMENTATION.md +279 -0
  759. package/tests/crm-viewer-app/angular.json +114 -0
  760. package/tests/crm-viewer-app/package.json +35 -0
  761. package/tests/crm-viewer-app/src/app/app.component.ts +534 -0
  762. package/tests/crm-viewer-app/src/app/citation.service.ts +316 -0
  763. package/tests/crm-viewer-app/src/env.d.ts +16 -0
  764. package/tests/crm-viewer-app/src/index.html +19 -0
  765. package/tests/crm-viewer-app/src/main.ts +7 -0
  766. package/tests/crm-viewer-app/src/styles.css +409 -0
  767. package/tests/crm-viewer-app/src/template.html +2678 -0
  768. package/tests/crm-viewer-app/tsconfig.app.json +15 -0
  769. package/tests/crm-viewer-app/tsconfig.json +27 -0
  770. package/tests/e2e/package.json +17 -0
  771. package/tests/e2e/playwright.config.ts +75 -0
  772. package/tests/e2e/tests/api/health.spec.ts +10 -0
  773. package/tests/e2e/tests/app/example.spec.ts +10 -0
  774. package/tests/widget-test-app/.prettierignore +6 -0
  775. package/tests/widget-test-app/README.md +48 -0
  776. package/tests/widget-test-app/index.html +12 -0
  777. package/tests/widget-test-app/package.json +24 -0
  778. package/tests/widget-test-app/src/App.css +192 -0
  779. package/tests/widget-test-app/src/App.tsx +80 -0
  780. package/tests/widget-test-app/src/main.tsx +9 -0
  781. package/tests/widget-test-app/src/vite-env.d.ts +4 -0
  782. package/tests/widget-test-app/tsconfig.json +25 -0
  783. package/tests/widget-test-app/tsconfig.node.json +11 -0
  784. package/tests/widget-test-app/vite.config.ts +14 -0
@@ -0,0 +1,4222 @@
1
+ import { db, PIPELINE_RUN_STATUS, PIPELINE_RUN_STEP_STATUS, schema } from "@sea/db";
2
+ import type {
3
+ PipelineRunStepBlockerDetails,
4
+ PipelineRunStepBlockerType,
5
+ SubmissionItemValue,
6
+ } from "@sea/db/schema";
7
+ import type {
8
+ Artifact,
9
+ FormGroupHydrated,
10
+ InsertDataTransformation,
11
+ InsertSubmissionGroup,
12
+ InsertSubmissionItem,
13
+ InsertSubmissionItemVersion,
14
+ Member,
15
+ Pipeline,
16
+ PipelineRun,
17
+ PipelineRunStep,
18
+ PipelineRunTimeline,
19
+ PipelineStep,
20
+ PipelineStepIntegrationConfig,
21
+ PipelineWithSteps,
22
+ Submission,
23
+ SubmissionGroup,
24
+ ToolCall,
25
+ } from "@sea/db/types";
26
+ import type { OutputData, OutputFieldTarget, OutputGroupData } from "@sea/schemas/core/pipeline";
27
+ import { formatISODate } from "@sea/util/fmt";
28
+ import { nanoid } from "@sea/util/id";
29
+ import { invariant } from "@sea/util/invariant";
30
+ import { and, asc, count, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
31
+ import type { DalResult } from "./result";
32
+ import { internalErrorResult, notFoundResult, okResult } from "./result";
33
+
34
+ function normalizeSteps<T extends { toolsAllowed: string[] | null }>(
35
+ steps: T[],
36
+ ): (Omit<T, "toolsAllowed"> & { toolsAllowed: string[] })[] {
37
+ return steps.map((step) => ({
38
+ ...step,
39
+ toolsAllowed: step.toolsAllowed ?? [],
40
+ }));
41
+ }
42
+
43
+ function normalizeSubmissionItemValue(value: unknown): SubmissionItemValue | null {
44
+ if (value == null) return null;
45
+ if (typeof value === "string" || typeof value === "number") return value;
46
+ return String(value);
47
+ }
48
+
49
+ async function getPipelineRun({
50
+ member,
51
+ runId,
52
+ }: {
53
+ member: Member;
54
+ runId: string;
55
+ }): Promise<DalResult<PipelineRun>> {
56
+ try {
57
+ const run = await db.query.pipelineRun.findFirst({
58
+ where: and(
59
+ eq(schema.pipelineRun.id, runId),
60
+ eq(schema.pipelineRun.organizationId, member.organizationId),
61
+ ),
62
+ });
63
+ if (!run) return notFoundResult;
64
+ return okResult(run);
65
+ } catch {
66
+ return notFoundResult;
67
+ }
68
+ }
69
+
70
+ async function getPipelineRunStep({
71
+ member,
72
+ runStepId,
73
+ }: {
74
+ member: Member;
75
+ runStepId: string;
76
+ }): Promise<DalResult<PipelineRunStep>> {
77
+ try {
78
+ const runStep = await db.query.pipelineRunStep.findFirst({
79
+ where: eq(schema.pipelineRunStep.id, runStepId),
80
+ with: { run: true },
81
+ });
82
+ if (!runStep || runStep.run.organizationId !== member.organizationId) {
83
+ return notFoundResult;
84
+ }
85
+ return okResult(runStep);
86
+ } catch (err) {
87
+ console.error("[getPipelineRunStep] Exception", err);
88
+ return notFoundResult;
89
+ }
90
+ }
91
+
92
+ export async function createPipelineRun({
93
+ member,
94
+ pipelineId,
95
+ dealId,
96
+ triggerType = "manual",
97
+ }: {
98
+ member: Member;
99
+ pipelineId: string;
100
+ dealId: string;
101
+ triggerType?: string;
102
+ }): Promise<DalResult<PipelineRun>> {
103
+ try {
104
+ const pipeline = await db.query.pipeline.findFirst({
105
+ where: and(
106
+ eq(schema.pipeline.id, pipelineId),
107
+ eq(schema.pipeline.organizationId, member.organizationId),
108
+ isNull(schema.pipeline.deletedAt),
109
+ ),
110
+ with: {
111
+ steps: {
112
+ with: {
113
+ parentDependencies: true,
114
+ },
115
+ },
116
+ },
117
+ });
118
+ if (!pipeline) return notFoundResult;
119
+
120
+ const [createdRun] = await db
121
+ .insert(schema.pipelineRun)
122
+ .values({
123
+ organizationId: member.organizationId,
124
+ pipelineId,
125
+ dealId,
126
+ status: "pending",
127
+ triggerType,
128
+ })
129
+ .returning();
130
+
131
+ if (!createdRun) return notFoundResult;
132
+
133
+ // Create run steps for all pipeline steps
134
+ for (const step of pipeline.steps) {
135
+ // Check if this step has any parent dependency with multiFormGroupId
136
+ // If so, this step should be a template that spawns instances per group
137
+ const shouldBeTemplate = step.parentDependencies.some((d) => d.multiFormGroupId !== null);
138
+
139
+ // Create run step - mark as template if it will spawn instances, otherwise normal
140
+ await db.insert(schema.pipelineRunStep).values({
141
+ runId: createdRun.id,
142
+ stepId: step.id,
143
+ spawnType: shouldBeTemplate ? "template" : "normal",
144
+ status: "pending",
145
+ });
146
+ }
147
+
148
+ return okResult(createdRun);
149
+ } catch (error) {
150
+ console.error("[createPipelineRun] Error:", error);
151
+ return notFoundResult;
152
+ }
153
+ }
154
+
155
+ export async function updatePipelineRunStatus({
156
+ member,
157
+ runId,
158
+ status,
159
+ errorSummary,
160
+ }: {
161
+ member: Member;
162
+ runId: string;
163
+ status: string;
164
+ errorSummary?: string;
165
+ }): Promise<DalResult<PipelineRun>> {
166
+ const runResult = await getPipelineRun({ member, runId });
167
+ if (!runResult.ok) return runResult;
168
+ const sets: Record<string, unknown> = { status };
169
+ if (errorSummary !== undefined) sets.errorSummary = errorSummary;
170
+ if (status === PIPELINE_RUN_STATUS.RUNNING) sets.startedAt = new Date();
171
+ if (
172
+ status === PIPELINE_RUN_STATUS.COMPLETED ||
173
+ status === PIPELINE_RUN_STATUS.FAILED ||
174
+ status === "cancelled"
175
+ ) {
176
+ sets.completedAt = new Date();
177
+ }
178
+
179
+ try {
180
+ const [run] = await db
181
+ .update(schema.pipelineRun)
182
+ .set(sets)
183
+ .where(eq(schema.pipelineRun.id, runId))
184
+ .returning();
185
+ if (!run) return notFoundResult;
186
+ return okResult(run);
187
+ } catch {
188
+ return notFoundResult;
189
+ }
190
+ }
191
+
192
+ export async function getPipelines({
193
+ member,
194
+ limit,
195
+ }: {
196
+ member: Member;
197
+ limit?: number;
198
+ }): Promise<PipelineWithSteps[]> {
199
+ const pipelines = await db.query.pipeline.findMany({
200
+ where: and(
201
+ eq(schema.pipeline.organizationId, member.organizationId),
202
+ isNull(schema.pipeline.deletedAt),
203
+ eq(schema.pipeline.isActive, true),
204
+ ),
205
+ orderBy: desc(schema.pipeline.updatedAt),
206
+ limit,
207
+ with: {
208
+ steps: {
209
+ with: {
210
+ outputForms: true,
211
+ parentDependencies: true,
212
+ },
213
+ },
214
+ },
215
+ });
216
+ return pipelines.map((pipeline) => ({
217
+ ...pipeline,
218
+ steps: normalizeSteps(pipeline.steps),
219
+ }));
220
+ }
221
+
222
+ export async function getPipeline({
223
+ member,
224
+ pipelineId,
225
+ }: {
226
+ member: Member;
227
+ pipelineId: string;
228
+ }): Promise<DalResult<Pipeline>> {
229
+ try {
230
+ const pipeline = await db.query.pipeline.findFirst({
231
+ where: and(
232
+ eq(schema.pipeline.id, pipelineId),
233
+ eq(schema.pipeline.organizationId, member.organizationId),
234
+ isNull(schema.pipeline.deletedAt),
235
+ ),
236
+ });
237
+ if (!pipeline) return notFoundResult;
238
+ return okResult(pipeline);
239
+ } catch {
240
+ return notFoundResult;
241
+ }
242
+ }
243
+
244
+ export async function getPipelineWithSteps({
245
+ member,
246
+ pipelineId,
247
+ }: {
248
+ member: Member;
249
+ pipelineId: string;
250
+ }): Promise<DalResult<PipelineWithSteps>> {
251
+ try {
252
+ const pipeline = await db.query.pipeline.findFirst({
253
+ where: and(
254
+ eq(schema.pipeline.id, pipelineId),
255
+ eq(schema.pipeline.organizationId, member.organizationId),
256
+ isNull(schema.pipeline.deletedAt),
257
+ eq(schema.pipeline.isActive, true),
258
+ ),
259
+ with: {
260
+ steps: {
261
+ with: {
262
+ outputForms: true,
263
+ parentDependencies: true,
264
+ },
265
+ },
266
+ },
267
+ });
268
+ if (!pipeline) return notFoundResult;
269
+ return okResult({
270
+ ...pipeline,
271
+ steps: normalizeSteps(pipeline.steps),
272
+ });
273
+ } catch {
274
+ return notFoundResult;
275
+ }
276
+ }
277
+
278
+ export async function createPipeline({
279
+ member,
280
+ name = "Untitled Pipeline",
281
+ description = "",
282
+ }: {
283
+ member: Member;
284
+ name?: string;
285
+ description?: string;
286
+ }): Promise<DalResult<PipelineWithSteps>> {
287
+ try {
288
+ const [pipeline] = await db
289
+ .insert(schema.pipeline)
290
+ .values({
291
+ organizationId: member.organizationId,
292
+ name,
293
+ description,
294
+ isActive: true,
295
+ version: "1",
296
+ metadata: {},
297
+ })
298
+ .returning();
299
+ if (!pipeline) return notFoundResult;
300
+
301
+ return okResult({ ...pipeline, steps: [] });
302
+ } catch {
303
+ return notFoundResult;
304
+ }
305
+ }
306
+
307
+ export async function createPipelineRunStep({
308
+ member,
309
+ runId,
310
+ stepId,
311
+ spawnType = "normal",
312
+ submissionGroupId,
313
+ stepRunIndex,
314
+ }: {
315
+ member: Member;
316
+ runId: string;
317
+ stepId: string;
318
+ spawnType?: "normal" | "template" | "instance";
319
+ submissionGroupId?: string;
320
+ stepRunIndex?: number;
321
+ }): Promise<DalResult<PipelineRunStep>> {
322
+ const runResult = await getPipelineRun({ member, runId });
323
+ if (!runResult.ok) return runResult;
324
+
325
+ try {
326
+ const [runStep] = await db
327
+ .insert(schema.pipelineRunStep)
328
+ .values({
329
+ runId,
330
+ stepId,
331
+ spawnType,
332
+ submissionGroupId: submissionGroupId ?? null,
333
+ stepRunIndex: stepRunIndex ?? null,
334
+ status: "pending",
335
+ })
336
+ .returning();
337
+ if (!runStep) return notFoundResult;
338
+ return okResult(runStep);
339
+ } catch (error) {
340
+ console.error("[createPipelineRunStep] Error:", error);
341
+ return internalErrorResult;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Get all output submissions from a parent step's run steps
347
+ */
348
+ export async function getParentStepOutputs({
349
+ member,
350
+ runId,
351
+ parentStepId,
352
+ }: {
353
+ member: Member;
354
+ runId: string;
355
+ parentStepId: string;
356
+ }) {
357
+ const runResult = await getPipelineRun({ member, runId });
358
+ if (!runResult.ok) return runResult;
359
+
360
+ try {
361
+ // Get run steps for the parent step
362
+ const runSteps = await db.query.pipelineRunStep.findMany({
363
+ where: and(
364
+ eq(schema.pipelineRunStep.runId, runId),
365
+ eq(schema.pipelineRunStep.stepId, parentStepId),
366
+ ),
367
+ });
368
+
369
+ if (runSteps.length === 0) {
370
+ return okResult([]);
371
+ }
372
+
373
+ // Get output submissions from the junction table
374
+ const runStepIds = runSteps.map((rs) => rs.id);
375
+ const outputMappings = await db.query.pipelineRunStepOutputSubmission.findMany({
376
+ with: {
377
+ submission: {
378
+ with: {
379
+ form: true,
380
+ groups: { with: { formGroup: { with: { fields: true, derived: true } }, items: true } },
381
+ },
382
+ },
383
+ },
384
+ where: inArray(schema.pipelineRunStepOutputSubmission.runStepId, runStepIds),
385
+ });
386
+
387
+ const outputSubmissions = outputMappings.map((m) => m.submission);
388
+
389
+ return okResult(outputSubmissions);
390
+ } catch (error) {
391
+ console.error("[getParentStepOutputs] Error:", error);
392
+ return internalErrorResult;
393
+ }
394
+ }
395
+
396
+ export async function updatePipelineRunStepStatus({
397
+ member,
398
+ runStepId,
399
+ status,
400
+ error,
401
+ }: {
402
+ member: Member;
403
+ runStepId: string;
404
+ status: string;
405
+ error?: string;
406
+ }): Promise<DalResult<PipelineRunStep>> {
407
+ const runStepResult = await getPipelineRunStep({ member, runStepId });
408
+ if (!runStepResult.ok) return runStepResult;
409
+
410
+ const now = new Date();
411
+ const sets: Record<string, unknown> = { status };
412
+ if (error !== undefined) sets.error = error;
413
+ if (status === PIPELINE_RUN_STEP_STATUS.RUNNING) sets.startedAt = now;
414
+ if (
415
+ status === PIPELINE_RUN_STEP_STATUS.COMPLETED ||
416
+ status === PIPELINE_RUN_STEP_STATUS.FAILED ||
417
+ status === "skipped"
418
+ ) {
419
+ sets.completedAt = now;
420
+ }
421
+
422
+ try {
423
+ const [updated] = await db
424
+ .update(schema.pipelineRunStep)
425
+ .set(sets)
426
+ .where(eq(schema.pipelineRunStep.id, runStepId))
427
+ .returning();
428
+ if (!updated) return notFoundResult;
429
+ return okResult(updated);
430
+ } catch {
431
+ return notFoundResult;
432
+ }
433
+ }
434
+
435
+ export async function logToolCall({
436
+ member,
437
+ runStepId,
438
+ toolId,
439
+ args,
440
+ result,
441
+ status = PIPELINE_RUN_STEP_STATUS.COMPLETED,
442
+ latencyMs,
443
+ }: {
444
+ member: Member;
445
+ runStepId: string;
446
+ toolId: string;
447
+ args: object;
448
+ result: object;
449
+ status?: string;
450
+ latencyMs?: number;
451
+ }): Promise<DalResult<ToolCall>> {
452
+ const runStepResult = await getPipelineRunStep({ member, runStepId });
453
+ if (!runStepResult.ok) return runStepResult;
454
+
455
+ try {
456
+ const [toolCall] = await db
457
+ .insert(schema.toolCall)
458
+ .values({
459
+ toolId,
460
+ args,
461
+ result,
462
+ status,
463
+ latencyMs,
464
+ })
465
+ .returning();
466
+ if (!toolCall) return notFoundResult;
467
+ return okResult(toolCall);
468
+ } catch {
469
+ return notFoundResult;
470
+ }
471
+ }
472
+
473
+ export async function getOutputFieldTargets({
474
+ member,
475
+ submissionId,
476
+ }: {
477
+ member: Member;
478
+ submissionId: string;
479
+ }): Promise<DalResult<OutputFieldTarget[]>> {
480
+ try {
481
+ const submission = await db.query.submission.findFirst({
482
+ where: and(
483
+ eq(schema.submission.id, submissionId),
484
+ eq(schema.submission.organizationId, member.organizationId),
485
+ isNull(schema.submission.deletedAt),
486
+ ),
487
+ });
488
+ if (!submission) return notFoundResult;
489
+
490
+ const targets = await db
491
+ .select({
492
+ formFieldId: schema.formField.id,
493
+ fieldName: schema.formField.name,
494
+ groupName: schema.formGroup.name,
495
+ formGroupId: schema.formGroup.id,
496
+ multi: schema.formGroup.multi,
497
+ })
498
+ .from(schema.formField)
499
+ .innerJoin(schema.formGroup, eq(schema.formGroup.id, schema.formField.formGroupId))
500
+ .where(eq(schema.formGroup.formId, submission.formId));
501
+
502
+ return okResult(
503
+ targets.map((target) => ({
504
+ ...target,
505
+ defaultGroupOrder: 0,
506
+ })),
507
+ );
508
+ } catch {
509
+ return notFoundResult;
510
+ }
511
+ }
512
+ /**
513
+ * Stores extracted data to the submission by creating submission groups, items, and citations.
514
+ * Handles both single and multi-instance form groups, validating and linking all data to source references.
515
+ *
516
+ * @param params - Configuration object
517
+ * @param params.submissionId - Target submission ID
518
+ * @param params.documentExtractionId - Source document extraction ID
519
+ * @param params.formGroupId - Form group being processed
520
+ * @param params.data - Structured extracted data
521
+ * @param params.referenceMap - Maps reference IDs to document locations
522
+ * @param params.validationData - Optional validation results for marking invalid groups
523
+ */
524
+ export async function storePipelineOutputGroupData({
525
+ submissionId,
526
+ formGroupId,
527
+ data,
528
+ }: {
529
+ submissionId: string;
530
+ formGroupId: string;
531
+ data: OutputGroupData;
532
+ }) {
533
+ if (data === null) {
534
+ console.warn(`Deserialization aborted: data is null`);
535
+ return;
536
+ }
537
+
538
+ const submissionGroups: InsertSubmissionGroup[] = [];
539
+ const submissionItems: InsertSubmissionItem[] = [];
540
+ const itemVersions: InsertSubmissionItemVersion[] = [];
541
+ const dataTransformations: InsertDataTransformation[] = [];
542
+ const versionToSourceItemIds = new Map<number, string[]>();
543
+ const versionToReasoning = new Map<number, string>();
544
+
545
+ const formGroup = await db.query.formGroup.findFirst({
546
+ where: eq(schema.formGroup.id, formGroupId),
547
+ with: {
548
+ fields: true,
549
+ derived: true,
550
+ submissionGroups: {
551
+ where: eq(schema.submissionGroup.submissionId, submissionId),
552
+ with: { items: true },
553
+ orderBy: asc(schema.submissionGroup.order),
554
+ },
555
+ },
556
+ });
557
+ invariant(formGroup, `No form group found for id ${formGroupId}`);
558
+
559
+ if (formGroup.multi) {
560
+ if (typeof data !== "object" || !Array.isArray(data)) {
561
+ console.warn(`Form group marked as multi, but data not an array`);
562
+ return;
563
+ }
564
+ const orderOffset =
565
+ formGroup.submissionGroups.length > 0
566
+ ? Math.max(...formGroup.submissionGroups.map((g) => g.order)) + 1
567
+ : 0;
568
+
569
+ for (const [order, groupElement] of data.entries()) {
570
+ const newSubmissionGroup: InsertSubmissionGroup = {
571
+ id: nanoid(),
572
+ submissionId,
573
+ formGroupId: formGroup.id,
574
+ order: order + orderOffset,
575
+ };
576
+ submissionGroups.push(newSubmissionGroup);
577
+
578
+ const newSubmissionItems = formGroup.fields.map((field) => ({
579
+ id: nanoid(),
580
+ submissionGroupId: newSubmissionGroup.id!,
581
+ formFieldId: field.id,
582
+ }));
583
+ submissionItems.push(...newSubmissionItems);
584
+
585
+ const result = await processGroupData(
586
+ formGroup,
587
+ newSubmissionItems,
588
+ itemVersions,
589
+ dataTransformations,
590
+ groupElement,
591
+ order,
592
+ );
593
+ // Merge the maps from this iteration
594
+ result.versionToSourceItemIds.forEach((value, key) => versionToSourceItemIds.set(key, value));
595
+ result.versionToReasoning.forEach((value, key) => versionToReasoning.set(key, value));
596
+ }
597
+ } else {
598
+ const submissionGroup = formGroup.submissionGroups[0];
599
+ invariant(submissionGroup);
600
+
601
+ const result = await processGroupData(
602
+ formGroup,
603
+ submissionGroup.items,
604
+ itemVersions,
605
+ dataTransformations,
606
+ data,
607
+ );
608
+ // Merge the maps from the single call
609
+ result.versionToSourceItemIds.forEach((value, key) => versionToSourceItemIds.set(key, value));
610
+ result.versionToReasoning.forEach((value, key) => versionToReasoning.set(key, value));
611
+ }
612
+
613
+ await db.transaction(async (tx) => {
614
+ if (submissionGroups.length > 0)
615
+ await tx.insert(schema.submissionGroup).values(submissionGroups);
616
+ if (submissionItems.length > 0) await tx.insert(schema.submissionItem).values(submissionItems);
617
+
618
+ // Use the collected mappings from processGroupData
619
+
620
+ if (itemVersions.length > 0 && versionToSourceItemIds) {
621
+ for (let index = 0; index < itemVersions.length; index++) {
622
+ const itemVersion = itemVersions[index];
623
+ if (!itemVersion) continue;
624
+
625
+ const sourceItemIds = versionToSourceItemIds.get(index);
626
+
627
+ if (!sourceItemIds || sourceItemIds.length === 0) {
628
+ // No transformation needed, insert version as-is
629
+ await tx.insert(schema.submissionItemVersion).values([itemVersion]);
630
+ continue;
631
+ }
632
+
633
+ // Fetch the latest version IDs for each source submission item
634
+ const sourceVersions = await tx
635
+ .select({
636
+ id: schema.submissionItemVersion.id,
637
+ submissionItemId: schema.submissionItemVersion.submissionItemId,
638
+ })
639
+ .from(schema.submissionItemVersion)
640
+ .where(
641
+ and(
642
+ inArray(schema.submissionItemVersion.submissionItemId, sourceItemIds),
643
+ eq(schema.submissionItemVersion.selected, true),
644
+ ),
645
+ );
646
+
647
+ const sourceVersionIds = sourceVersions.map((v) => v.id);
648
+
649
+ if (sourceVersionIds.length === 0) {
650
+ // No source versions found, insert without transformation
651
+ await tx.insert(schema.submissionItemVersion).values([itemVersion]);
652
+ continue;
653
+ }
654
+
655
+ // Get reasoning for this field (if available)
656
+ const reasoning = versionToReasoning?.get(index);
657
+
658
+ // Create dataTransformation record
659
+ const transformation: InsertDataTransformation = {
660
+ id: nanoid(),
661
+ sourceItemVersionIds: sourceVersionIds,
662
+ description: reasoning || null,
663
+ };
664
+
665
+ await tx.insert(schema.dataTransformation).values([transformation]);
666
+
667
+ // Update item version to reference this transformation
668
+ const versionWithTransformation: typeof itemVersion = {
669
+ ...itemVersion,
670
+ sourceId: transformation.id!,
671
+ };
672
+
673
+ await tx.insert(schema.submissionItemVersion).values([versionWithTransformation]);
674
+ }
675
+ } else if (itemVersions.length > 0) {
676
+ // No transformations needed, insert all versions
677
+ await tx.insert(schema.submissionItemVersion).values(itemVersions);
678
+ }
679
+ });
680
+ }
681
+
682
+ /**
683
+ * Processes pipeline output data for a single form group and creates submission items with data transformations.
684
+ * Creates dataTransformation records linking output items to their source items from parent steps.
685
+ *
686
+ * @param formGroup - Form group definition with field metadata
687
+ * @param submissionItems - Array to populate with submission items
688
+ * @param itemVersions - Array to populate with submission item versions
689
+ * @param dataTransformations - Array to populate with data transformations
690
+ * @param groupData - Pipeline output data for this group
691
+ * @param order - Optional order index for multi-instance groups
692
+ */
693
+ export async function processGroupData(
694
+ formGroup: FormGroupHydrated,
695
+ submissionItems: InsertSubmissionItem[],
696
+ itemVersions: InsertSubmissionItemVersion[],
697
+ dataTransformations: InsertDataTransformation[],
698
+ groupData: OutputGroupData,
699
+ order: number | null = null,
700
+ ): Promise<{
701
+ versionToSourceItemIds: Map<number, string[]>;
702
+ versionToReasoning: Map<number, string>;
703
+ }> {
704
+ const versionToSourceItemIds = new Map<number, string[]>();
705
+ const versionToReasoning = new Map<number, string>();
706
+
707
+ if (!groupData || typeof groupData !== "object" || Array.isArray(groupData)) {
708
+ console.warn(
709
+ `Single group element ${formGroup.name} is missing or data is not an object, skipping...`,
710
+ );
711
+ return { versionToSourceItemIds, versionToReasoning };
712
+ }
713
+
714
+ // Process each field and track source item IDs and reasoning
715
+
716
+ for (const field of formGroup.fields) {
717
+ const submissionItem = submissionItems.find((item) => item.formFieldId === field.id);
718
+ const fieldData = groupData[field.name];
719
+ if (!fieldData || !submissionItem) continue;
720
+
721
+ const { value, refId, reasoning } = fieldData;
722
+ const sourceItemIds = refId.split(",").filter((id: string) => id.trim());
723
+
724
+ // Debug: Log only when reasoning is missing
725
+ if (!reasoning) {
726
+ console.warn(`[processGroupData] Field '${field.name}' missing reasoning`);
727
+ }
728
+
729
+ // Create item version with transformation source type
730
+ const newItemVersion: InsertSubmissionItemVersion = {
731
+ submissionItemId: submissionItem.id!,
732
+ sourceType: "transformation" as const,
733
+ selected: true,
734
+ sourceId: null,
735
+ value: normalizeSubmissionItemValue(value),
736
+ };
737
+
738
+ const versionIndex = itemVersions.length;
739
+ itemVersions.push(newItemVersion);
740
+
741
+ // Track source item IDs and reasoning for this version to create transformation later
742
+ versionToSourceItemIds.set(versionIndex, sourceItemIds);
743
+ if (reasoning && typeof reasoning === "string") {
744
+ versionToReasoning.set(versionIndex, reasoning);
745
+ }
746
+ }
747
+
748
+ return { versionToSourceItemIds, versionToReasoning };
749
+ }
750
+
751
+ /**
752
+ * Parses and normalizes field values into the submission item value format.
753
+ * Dates are converted to ISO format strings, booleans to numbers (0/1), and other values are passed through.
754
+ *
755
+ * @param value - Raw value from extraction (string, number, boolean, or Date)
756
+ * @returns Normalized value suitable for storing in submission items
757
+ */
758
+ function parseItemValue(value: string | number | boolean | Date): SubmissionItemValue {
759
+ if (value instanceof Date) {
760
+ return formatISODate(value);
761
+ }
762
+ if (typeof value === "boolean") {
763
+ return Number(value);
764
+ }
765
+
766
+ return value;
767
+ }
768
+
769
+ export async function storePipelineOutputData({
770
+ member,
771
+ outputSubmissionId,
772
+ outputData,
773
+ }: {
774
+ member: Member;
775
+ outputSubmissionId: string;
776
+ outputData: OutputData;
777
+ }): Promise<DalResult<{ success: true }>> {
778
+ try {
779
+ // Fetch the submission with its form and groups
780
+ const submission = await db.query.submission.findFirst({
781
+ where: and(
782
+ eq(schema.submission.id, outputSubmissionId),
783
+ eq(schema.submission.organizationId, member.organizationId),
784
+ isNull(schema.submission.deletedAt),
785
+ ),
786
+ with: {
787
+ form: {
788
+ with: {
789
+ groups: {
790
+ with: {
791
+ fields: true,
792
+ },
793
+ orderBy: asc(schema.formGroup.order),
794
+ },
795
+ },
796
+ },
797
+ },
798
+ });
799
+
800
+ if (!submission) return notFoundResult;
801
+
802
+ // Process each group in the output data
803
+ for (const group of submission.form.groups) {
804
+ const groupData = outputData?.[group.name];
805
+ if (groupData) {
806
+ await storePipelineOutputGroupData({
807
+ submissionId: outputSubmissionId,
808
+ formGroupId: group.id,
809
+ data: groupData,
810
+ });
811
+ }
812
+ }
813
+
814
+ return okResult({ success: true });
815
+ } catch (error) {
816
+ console.error("[storePipelineOutputData] Error:", error);
817
+ return notFoundResult;
818
+ }
819
+ }
820
+
821
+ /**
822
+ * Get runtime submission (allows both extraction and transformation types).
823
+ * Used in pipeline execution to access both input and intermediate submissions.
824
+ */
825
+ export async function getRuntimeSubmission({
826
+ member,
827
+ submissionId,
828
+ }: {
829
+ member: Member;
830
+ submissionId: string;
831
+ }): Promise<DalResult<Submission>> {
832
+ try {
833
+ const submission = await db.query.submission.findFirst({
834
+ where: and(
835
+ eq(schema.submission.id, submissionId),
836
+ eq(schema.submission.organizationId, member.organizationId),
837
+ isNull(schema.submission.deletedAt),
838
+ ),
839
+ });
840
+ if (!submission) return notFoundResult;
841
+ return okResult(submission);
842
+ } catch (error) {
843
+ console.error("[getRuntimeSubmission] Error:", error);
844
+ return internalErrorResult;
845
+ }
846
+ }
847
+
848
+ /**
849
+ * Get runtime submission with denormalized data (groups, items, etc).
850
+ * Used in multi-group spawning and data filtering logic.
851
+ */
852
+ export async function getRuntimeSubmissionWithData({
853
+ member,
854
+ submissionId,
855
+ }: {
856
+ member: Member;
857
+ submissionId: string;
858
+ }): Promise<
859
+ DalResult<
860
+ Submission & {
861
+ groups: Array<{
862
+ id: string;
863
+ formGroup: {
864
+ id: string;
865
+ name: string;
866
+ multi: boolean;
867
+ };
868
+ items: Array<{
869
+ id: string;
870
+ formFieldId: string;
871
+ }>;
872
+ }>;
873
+ }
874
+ >
875
+ > {
876
+ try {
877
+ const submission = await db.query.submission.findFirst({
878
+ where: and(
879
+ eq(schema.submission.id, submissionId),
880
+ eq(schema.submission.organizationId, member.organizationId),
881
+ isNull(schema.submission.deletedAt),
882
+ ),
883
+ with: {
884
+ groups: {
885
+ orderBy: asc(schema.submissionGroup.order),
886
+ with: {
887
+ formGroup: true,
888
+ items: true,
889
+ },
890
+ },
891
+ },
892
+ });
893
+ if (!submission) return notFoundResult;
894
+ return okResult(submission);
895
+ } catch (error) {
896
+ console.error("[getRuntimeSubmissionWithData] Error:", error);
897
+ return internalErrorResult;
898
+ }
899
+ }
900
+
901
+ export async function getPipelineRunWithSteps({
902
+ member,
903
+ runId,
904
+ }: {
905
+ member: Member;
906
+ runId: string;
907
+ }) {
908
+ try {
909
+ const run = await db.query.pipelineRun.findFirst({
910
+ where: and(
911
+ eq(schema.pipelineRun.id, runId),
912
+ eq(schema.pipelineRun.organizationId, member.organizationId),
913
+ ),
914
+ with: {
915
+ pipeline: {
916
+ with: {
917
+ steps: {
918
+ orderBy: asc(schema.pipelineStep.createdAt),
919
+ },
920
+ },
921
+ },
922
+ },
923
+ });
924
+
925
+ if (!run) return notFoundResult;
926
+ return okResult(run);
927
+ } catch (error) {
928
+ console.error("[getPipelineRunWithSteps] Error:", error);
929
+ return notFoundResult;
930
+ }
931
+ }
932
+
933
+ export async function getPipelineRuns({
934
+ member,
935
+ pipelineId,
936
+ limit = 50,
937
+ }: {
938
+ member: Member;
939
+ pipelineId: string;
940
+ limit?: number;
941
+ }): Promise<DalResult<PipelineRun[]>> {
942
+ try {
943
+ const pipeline = await db.query.pipeline.findFirst({
944
+ where: and(
945
+ eq(schema.pipeline.id, pipelineId),
946
+ eq(schema.pipeline.organizationId, member.organizationId),
947
+ isNull(schema.pipeline.deletedAt),
948
+ ),
949
+ });
950
+ if (!pipeline) return notFoundResult;
951
+
952
+ const runs = await db.query.pipelineRun.findMany({
953
+ where: and(
954
+ eq(schema.pipelineRun.organizationId, member.organizationId),
955
+ eq(schema.pipelineRun.pipelineId, pipelineId),
956
+ ),
957
+ orderBy: desc(schema.pipelineRun.createdAt),
958
+ limit,
959
+ with: {
960
+ steps: {
961
+ orderBy: asc(schema.pipelineRunStep.createdAt),
962
+ },
963
+ },
964
+ });
965
+ return okResult(runs);
966
+ } catch {
967
+ return notFoundResult;
968
+ }
969
+ }
970
+
971
+ /**
972
+ * Get all output submissions for a pipeline run, grouped by run step ID
973
+ */
974
+ export async function getRunOutputSubmissions({
975
+ member,
976
+ runId,
977
+ }: {
978
+ member: Member;
979
+ runId: string;
980
+ }): Promise<DalResult<Map<string, string[]>>> {
981
+ const runResult = await getPipelineRun({ member, runId });
982
+ if (!runResult.ok) return runResult;
983
+
984
+ try {
985
+ // Get all run steps for this run
986
+ const runSteps = await db.query.pipelineRunStep.findMany({
987
+ where: eq(schema.pipelineRunStep.runId, runId),
988
+ });
989
+
990
+ if (runSteps.length === 0) {
991
+ return okResult(new Map());
992
+ }
993
+
994
+ // Get all output submissions
995
+ const runStepIds = runSteps.map((rs) => rs.id);
996
+ const outputMappings = await db.query.pipelineRunStepOutputSubmission.findMany({
997
+ where: inArray(schema.pipelineRunStepOutputSubmission.runStepId, runStepIds),
998
+ });
999
+
1000
+ // Group by run step ID
1001
+ const submissionsByRunStep = new Map<string, string[]>();
1002
+ for (const mapping of outputMappings) {
1003
+ const existing = submissionsByRunStep.get(mapping.runStepId) || [];
1004
+ existing.push(mapping.submissionId);
1005
+ submissionsByRunStep.set(mapping.runStepId, existing);
1006
+ }
1007
+
1008
+ return okResult(submissionsByRunStep);
1009
+ } catch (error) {
1010
+ console.error("[getRunOutputSubmissions] Error:", error);
1011
+ return internalErrorResult;
1012
+ }
1013
+ }
1014
+
1015
+ export async function getRunTimeline({
1016
+ member,
1017
+ runId,
1018
+ }: {
1019
+ member: Member;
1020
+ runId: string;
1021
+ }): Promise<DalResult<PipelineRunTimeline>> {
1022
+ try {
1023
+ const run = await db.query.pipelineRun.findFirst({
1024
+ where: and(
1025
+ eq(schema.pipelineRun.id, runId),
1026
+ eq(schema.pipelineRun.organizationId, member.organizationId),
1027
+ ),
1028
+ with: {
1029
+ steps: {
1030
+ orderBy: asc(schema.pipelineRunStep.createdAt),
1031
+ // Filter out template steps - they're just placeholders for spawning instances
1032
+ // Only show normal steps and actual instances
1033
+ where: (step, { ne }) => ne(step.spawnType, "template"),
1034
+ },
1035
+ pipeline: true,
1036
+ },
1037
+ });
1038
+ if (!run) return notFoundResult;
1039
+
1040
+ const stepIds = run.steps.map((s) => s.id);
1041
+ if (stepIds.length === 0) {
1042
+ // No steps, return early with empty arrays
1043
+ return okResult({
1044
+ ...run,
1045
+ steps: [],
1046
+ artifacts: [],
1047
+ });
1048
+ }
1049
+
1050
+ // Load tool calls for all steps
1051
+ const toolCallMappings =
1052
+ run.steps.length > 0
1053
+ ? await db.query.pipelineRunStepToolCall.findMany({
1054
+ where: inArray(
1055
+ schema.pipelineRunStepToolCall.runStepId,
1056
+ run.steps.map((s) => s.id),
1057
+ ),
1058
+ with: {
1059
+ toolCall: true,
1060
+ },
1061
+ })
1062
+ : [];
1063
+
1064
+ // Load artifacts for all steps
1065
+ const stepArtifactMappings =
1066
+ run.steps.length > 0
1067
+ ? await db.query.pipelineRunArtifact.findMany({
1068
+ where: and(
1069
+ eq(schema.pipelineRunArtifact.runId, runId),
1070
+ inArray(
1071
+ schema.pipelineRunArtifact.runStepId,
1072
+ run.steps.map((s) => s.id),
1073
+ ),
1074
+ ),
1075
+ with: {
1076
+ artifact: true,
1077
+ },
1078
+ })
1079
+ : [];
1080
+
1081
+ // Load run-level artifacts
1082
+ const runArtifactMappings = await db.query.pipelineRunArtifact.findMany({
1083
+ where: and(
1084
+ eq(schema.pipelineRunArtifact.runId, runId),
1085
+ isNull(schema.pipelineRunArtifact.runStepId),
1086
+ ),
1087
+ with: {
1088
+ artifact: true,
1089
+ },
1090
+ });
1091
+
1092
+ // Group by step
1093
+ const toolCallsByStep = new Map<string, typeof toolCallMappings>();
1094
+ for (const mapping of toolCallMappings) {
1095
+ const existing = toolCallsByStep.get(mapping.runStepId) || [];
1096
+ existing.push(mapping);
1097
+ toolCallsByStep.set(mapping.runStepId, existing);
1098
+ }
1099
+
1100
+ const artifactsByStep = new Map<string, typeof stepArtifactMappings>();
1101
+ for (const mapping of stepArtifactMappings) {
1102
+ if (mapping.runStepId) {
1103
+ const existing = artifactsByStep.get(mapping.runStepId) || [];
1104
+ existing.push(mapping);
1105
+ artifactsByStep.set(mapping.runStepId, existing);
1106
+ }
1107
+ }
1108
+
1109
+ // Collect all artifacts (both run-level and step-level)
1110
+ const allArtifacts = [
1111
+ ...runArtifactMappings.map((m) => m.artifact),
1112
+ ...stepArtifactMappings.map((m) => m.artifact),
1113
+ ];
1114
+
1115
+ // Transform the data to match the expected type
1116
+ const transformedRun: PipelineRunTimeline = {
1117
+ ...run,
1118
+ steps: run.steps.map((step) => ({
1119
+ ...step,
1120
+ toolCalls: (toolCallsByStep.get(step.id) || []).map((m) => m.toolCall),
1121
+ artifacts: (artifactsByStep.get(step.id) || []).map((m) => m.artifact),
1122
+ })),
1123
+ artifacts: allArtifacts,
1124
+ };
1125
+
1126
+ return okResult(transformedRun);
1127
+ } catch (error) {
1128
+ console.error("getRunTimeline error:", error);
1129
+ return notFoundResult;
1130
+ }
1131
+ }
1132
+
1133
+ export async function getPipelineDetail({
1134
+ member,
1135
+ pipelineId,
1136
+ }: {
1137
+ member: Member;
1138
+ pipelineId: string;
1139
+ }) {
1140
+ try {
1141
+ const pipeline = await db.query.pipeline.findFirst({
1142
+ where: and(
1143
+ eq(schema.pipeline.id, pipelineId),
1144
+ eq(schema.pipeline.organizationId, member.organizationId),
1145
+ isNull(schema.pipeline.deletedAt),
1146
+ ),
1147
+ with: {
1148
+ steps: {
1149
+ with: {
1150
+ outputForms: true,
1151
+ integrationConfig: true,
1152
+ parentDependencies: {
1153
+ with: {
1154
+ parentStep: true,
1155
+ },
1156
+ },
1157
+ },
1158
+ },
1159
+ },
1160
+ });
1161
+ if (!pipeline) return notFoundResult;
1162
+
1163
+ // Get execution levels for all steps
1164
+ const levelsResult = await getPipelineStepsWithLevels({ member, pipelineId });
1165
+ const levelsByStepId = new Map<string, number>();
1166
+ if (levelsResult.ok) {
1167
+ for (const step of levelsResult.data) {
1168
+ levelsByStepId.set(step.id, step.executionLevel);
1169
+ }
1170
+ }
1171
+
1172
+ // Sort steps by execution level, then by createdAt
1173
+ const sortedSteps = [...pipeline.steps].sort((a, b) => {
1174
+ const levelA = levelsByStepId.get(a.id) ?? 0;
1175
+ const levelB = levelsByStepId.get(b.id) ?? 0;
1176
+ if (levelA !== levelB) return levelA - levelB;
1177
+ return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
1178
+ });
1179
+
1180
+ const pipelineWithNormalizedSteps = {
1181
+ ...pipeline,
1182
+ steps: normalizeSteps(sortedSteps),
1183
+ };
1184
+
1185
+ return okResult(pipelineWithNormalizedSteps);
1186
+ } catch {
1187
+ return notFoundResult;
1188
+ }
1189
+ }
1190
+
1191
+ export async function deletePipeline({
1192
+ member,
1193
+ pipelineId,
1194
+ }: {
1195
+ member: Member;
1196
+ pipelineId: string;
1197
+ }): Promise<DalResult<{ success: true }>> {
1198
+ try {
1199
+ const pipeline = await db.query.pipeline.findFirst({
1200
+ where: and(
1201
+ eq(schema.pipeline.id, pipelineId),
1202
+ eq(schema.pipeline.organizationId, member.organizationId),
1203
+ isNull(schema.pipeline.deletedAt),
1204
+ ),
1205
+ });
1206
+ if (!pipeline) return notFoundResult;
1207
+
1208
+ await db
1209
+ .update(schema.pipeline)
1210
+ .set({ deletedAt: new Date() })
1211
+ .where(eq(schema.pipeline.id, pipelineId));
1212
+
1213
+ return okResult({ success: true });
1214
+ } catch {
1215
+ return notFoundResult;
1216
+ }
1217
+ }
1218
+
1219
+ export async function togglePipelineActive({
1220
+ member,
1221
+ pipelineId,
1222
+ isActive,
1223
+ }: {
1224
+ member: Member;
1225
+ pipelineId: string;
1226
+ isActive: boolean;
1227
+ }): Promise<DalResult<{ success: true }>> {
1228
+ try {
1229
+ const pipeline = await db.query.pipeline.findFirst({
1230
+ where: and(
1231
+ eq(schema.pipeline.id, pipelineId),
1232
+ eq(schema.pipeline.organizationId, member.organizationId),
1233
+ isNull(schema.pipeline.deletedAt),
1234
+ ),
1235
+ });
1236
+ if (!pipeline) return notFoundResult;
1237
+
1238
+ await db.update(schema.pipeline).set({ isActive }).where(eq(schema.pipeline.id, pipelineId));
1239
+
1240
+ return okResult({ success: true });
1241
+ } catch {
1242
+ return notFoundResult;
1243
+ }
1244
+ }
1245
+
1246
+ export async function updatePipelineMetadata({
1247
+ member,
1248
+ pipelineId,
1249
+ name,
1250
+ description,
1251
+ }: {
1252
+ member: Member;
1253
+ pipelineId: string;
1254
+ name: string;
1255
+ description: string;
1256
+ }): Promise<DalResult<{ success: true }>> {
1257
+ try {
1258
+ const pipeline = await db.query.pipeline.findFirst({
1259
+ where: and(
1260
+ eq(schema.pipeline.id, pipelineId),
1261
+ eq(schema.pipeline.organizationId, member.organizationId),
1262
+ isNull(schema.pipeline.deletedAt),
1263
+ ),
1264
+ });
1265
+ if (!pipeline) return notFoundResult;
1266
+
1267
+ await db
1268
+ .update(schema.pipeline)
1269
+ .set({ name, description })
1270
+ .where(eq(schema.pipeline.id, pipelineId));
1271
+
1272
+ return okResult({ success: true });
1273
+ } catch {
1274
+ return notFoundResult;
1275
+ }
1276
+ }
1277
+
1278
+ export async function createPipelineStep({
1279
+ member,
1280
+ pipelineId,
1281
+ name,
1282
+ description,
1283
+ prompt,
1284
+ type,
1285
+ role,
1286
+ outputFormIds,
1287
+ toolsAllowed,
1288
+ integrationConfig,
1289
+ }: {
1290
+ member: Member;
1291
+ pipelineId: string;
1292
+ name: string;
1293
+ description: string;
1294
+ prompt?: string;
1295
+ type: "single" | "multi";
1296
+ role: "extraction" | "integration" | "analysis" | null;
1297
+ outputFormIds?: string[];
1298
+ toolsAllowed?: string[];
1299
+ integrationConfig?: {
1300
+ method: "GET" | "POST" | "PUT" | "DELETE";
1301
+ urlTemplate: string;
1302
+ headers: Record<string, string>;
1303
+ bodyTemplate?: string;
1304
+ };
1305
+ }): Promise<DalResult<PipelineStep>> {
1306
+ try {
1307
+ // Verify pipeline exists and member has access
1308
+ const pipeline = await db.query.pipeline.findFirst({
1309
+ where: and(
1310
+ eq(schema.pipeline.id, pipelineId),
1311
+ eq(schema.pipeline.organizationId, member.organizationId),
1312
+ isNull(schema.pipeline.deletedAt),
1313
+ ),
1314
+ });
1315
+ if (!pipeline) return notFoundResult;
1316
+
1317
+ const [step] = await db
1318
+ .insert(schema.pipelineStep)
1319
+ .values({
1320
+ pipelineId,
1321
+ name,
1322
+ description,
1323
+ prompt: prompt ?? "",
1324
+ type,
1325
+ role,
1326
+ toolsAllowed: toolsAllowed ?? [],
1327
+ // integrationConfig deliberately NOT written - use pipelineStepIntegrationConfig table instead
1328
+ retryPolicy: {},
1329
+ })
1330
+ .returning();
1331
+
1332
+ if (!step) return notFoundResult;
1333
+
1334
+ // Insert into pipeline_step_output_form table
1335
+ if (outputFormIds && outputFormIds.length > 0) {
1336
+ await db.insert(schema.pipelineStepOutputForm).values(
1337
+ outputFormIds.map((formId) => ({
1338
+ stepId: step.id,
1339
+ formId,
1340
+ })),
1341
+ );
1342
+ }
1343
+
1344
+ // Insert into pipeline_step_integration_config table if provided
1345
+ if (integrationConfig && role === "integration") {
1346
+ await db.insert(schema.pipelineStepIntegrationConfig).values({
1347
+ id: nanoid(),
1348
+ stepId: step.id,
1349
+ method: integrationConfig.method,
1350
+ urlTemplate: integrationConfig.urlTemplate,
1351
+ headers: integrationConfig.headers,
1352
+ bodyTemplate: integrationConfig.bodyTemplate ?? null,
1353
+ });
1354
+ }
1355
+
1356
+ return okResult(step);
1357
+ } catch (error) {
1358
+ console.error("[createPipelineStep] Error:", error);
1359
+ return notFoundResult;
1360
+ }
1361
+ }
1362
+
1363
+ export async function updatePipelineStepConfig({
1364
+ member,
1365
+ stepId,
1366
+ name,
1367
+ description,
1368
+ prompt,
1369
+ type,
1370
+ role,
1371
+ outputFormIds,
1372
+ toolsAllowed,
1373
+ integrationConfig,
1374
+ }: {
1375
+ member: Member;
1376
+ stepId: string;
1377
+ name?: string;
1378
+ description?: string;
1379
+ prompt?: string;
1380
+ type?: "single" | "multi";
1381
+ role?: "extraction" | "integration" | "analysis" | null;
1382
+ outputFormIds?: string[];
1383
+ toolsAllowed?: string[];
1384
+ integrationConfig?: {
1385
+ method: "GET" | "POST" | "PUT" | "DELETE";
1386
+ urlTemplate: string;
1387
+ headers: Record<string, string>;
1388
+ bodyTemplate?: string;
1389
+ } | null;
1390
+ }): Promise<DalResult<{ success: true }>> {
1391
+ try {
1392
+ const step = await db.query.pipelineStep.findFirst({
1393
+ where: eq(schema.pipelineStep.id, stepId),
1394
+ with: {
1395
+ pipeline: true,
1396
+ },
1397
+ });
1398
+ if (!step || step.pipeline.organizationId !== member.organizationId) return notFoundResult;
1399
+
1400
+ // Build update object with only provided fields
1401
+ const updateData: Record<string, unknown> = {};
1402
+ if (name !== undefined) updateData.name = name;
1403
+ if (description !== undefined) updateData.description = description;
1404
+ if (prompt !== undefined) updateData.prompt = prompt;
1405
+ if (type !== undefined) updateData.type = type;
1406
+ if (role !== undefined) updateData.role = role;
1407
+ if (toolsAllowed !== undefined) updateData.toolsAllowed = toolsAllowed;
1408
+ // integrationConfig deliberately NOT written - use pipelineStepIntegrationConfig table instead
1409
+
1410
+ await db.update(schema.pipelineStep).set(updateData).where(eq(schema.pipelineStep.id, stepId));
1411
+
1412
+ // Update pipeline_step_output_form table
1413
+ if (outputFormIds !== undefined) {
1414
+ // Delete existing output form associations
1415
+ await db
1416
+ .delete(schema.pipelineStepOutputForm)
1417
+ .where(eq(schema.pipelineStepOutputForm.stepId, stepId));
1418
+
1419
+ // Insert new output form associations
1420
+ if (outputFormIds.length > 0) {
1421
+ await db.insert(schema.pipelineStepOutputForm).values(
1422
+ outputFormIds.map((formId) => ({
1423
+ stepId,
1424
+ formId,
1425
+ })),
1426
+ );
1427
+ }
1428
+ }
1429
+
1430
+ // Update pipeline_step_integration_config table
1431
+ if (integrationConfig !== undefined) {
1432
+ if (integrationConfig === null) {
1433
+ // Delete config if explicitly set to null
1434
+ await db
1435
+ .delete(schema.pipelineStepIntegrationConfig)
1436
+ .where(eq(schema.pipelineStepIntegrationConfig.stepId, stepId));
1437
+ } else {
1438
+ // Upsert integration config via upsertStepIntegrationConfig
1439
+ const upsertResult = await upsertStepIntegrationConfig({
1440
+ member,
1441
+ stepId,
1442
+ config: integrationConfig,
1443
+ });
1444
+ if (!upsertResult.ok) return upsertResult;
1445
+ }
1446
+ }
1447
+
1448
+ return okResult({ success: true });
1449
+ } catch (error) {
1450
+ console.error("[updatePipelineStepConfig] Error:", error);
1451
+ return notFoundResult;
1452
+ }
1453
+ }
1454
+
1455
+ export async function getPipelineStepById({
1456
+ member,
1457
+ stepId,
1458
+ }: {
1459
+ member: Member;
1460
+ stepId: string;
1461
+ }): Promise<
1462
+ DalResult<
1463
+ PipelineStep & {
1464
+ pipeline: Pipeline;
1465
+ outputForms: Array<{ id: string; stepId: string; formId: string; createdAt: Date }>;
1466
+ integrationConfig: PipelineStepIntegrationConfig | null;
1467
+ }
1468
+ >
1469
+ > {
1470
+ try {
1471
+ const step = await db.query.pipelineStep.findFirst({
1472
+ where: eq(schema.pipelineStep.id, stepId),
1473
+ with: {
1474
+ pipeline: true,
1475
+ outputForms: true,
1476
+ integrationConfig: true,
1477
+ },
1478
+ });
1479
+ if (!step || step.pipeline.organizationId !== member.organizationId) return notFoundResult;
1480
+ return okResult(step);
1481
+ } catch (error) {
1482
+ console.error("[getPipelineStepById] Error:", error);
1483
+ return notFoundResult;
1484
+ }
1485
+ }
1486
+
1487
+ export async function deletePipelineStep({
1488
+ member,
1489
+ stepId,
1490
+ }: {
1491
+ member: Member;
1492
+ stepId: string;
1493
+ }): Promise<DalResult<{ success: true }>> {
1494
+ try {
1495
+ const step = await db.query.pipelineStep.findFirst({
1496
+ where: eq(schema.pipelineStep.id, stepId),
1497
+ with: {
1498
+ pipeline: true,
1499
+ },
1500
+ });
1501
+ if (!step || step.pipeline.organizationId !== member.organizationId) return notFoundResult;
1502
+
1503
+ await db.delete(schema.pipelineStep).where(eq(schema.pipelineStep.id, stepId));
1504
+
1505
+ return okResult({ success: true });
1506
+ } catch {
1507
+ return notFoundResult;
1508
+ }
1509
+ }
1510
+
1511
+ export async function getPipelineRunStepWithDetails({
1512
+ member,
1513
+ runStepId,
1514
+ }: {
1515
+ member: Member;
1516
+ runStepId: string;
1517
+ }): Promise<
1518
+ DalResult<{
1519
+ step: PipelineRunStep;
1520
+ run: PipelineRun & {
1521
+ pipeline: PipelineWithSteps | null;
1522
+ steps: PipelineRunStep[];
1523
+ resources: { submissionIds: string[]; documentIds: string[] };
1524
+ };
1525
+ }>
1526
+ > {
1527
+ try {
1528
+ const step = await db.query.pipelineRunStep.findFirst({
1529
+ where: eq(schema.pipelineRunStep.id, runStepId),
1530
+ with: {
1531
+ run: {
1532
+ with: {
1533
+ pipeline: {
1534
+ with: {
1535
+ steps: {
1536
+ orderBy: asc(schema.pipelineStep.createdAt),
1537
+ with: {
1538
+ outputForms: true,
1539
+ parentDependencies: true,
1540
+ },
1541
+ },
1542
+ },
1543
+ },
1544
+ steps: {
1545
+ orderBy: asc(schema.pipelineRunStep.createdAt),
1546
+ },
1547
+ },
1548
+ },
1549
+ },
1550
+ });
1551
+
1552
+ if (!step) return notFoundResult;
1553
+ if (step.run.organizationId !== member.organizationId) return notFoundResult;
1554
+
1555
+ // TODO: Get run submissions from appropriate table once schema is finalized
1556
+ const submissionIds: string[] = [];
1557
+
1558
+ return okResult({
1559
+ step,
1560
+ run: {
1561
+ ...step.run,
1562
+ pipeline: step.run.pipeline
1563
+ ? {
1564
+ ...step.run.pipeline,
1565
+ steps: normalizeSteps(step.run.pipeline.steps),
1566
+ }
1567
+ : null,
1568
+ resources: { submissionIds, documentIds: [] },
1569
+ },
1570
+ });
1571
+ } catch {
1572
+ return notFoundResult;
1573
+ }
1574
+ }
1575
+
1576
+ export async function getSubmissionsGroupedByForm({
1577
+ member,
1578
+ formIds,
1579
+ limit = 50,
1580
+ }: {
1581
+ member: Member;
1582
+ formIds?: string[];
1583
+ limit?: number;
1584
+ }): Promise<
1585
+ DalResult<
1586
+ Array<{
1587
+ formId: string;
1588
+ formName: string;
1589
+ submissions: Array<{
1590
+ id: string;
1591
+ name: string;
1592
+ createdAt: Date;
1593
+ updatedAt: Date;
1594
+ status: string;
1595
+ }>;
1596
+ }>
1597
+ >
1598
+ > {
1599
+ try {
1600
+ const whereConditions = [
1601
+ eq(schema.submission.organizationId, member.organizationId),
1602
+ eq(schema.submission.type, "extraction"),
1603
+ isNull(schema.submission.deletedAt),
1604
+ ];
1605
+
1606
+ if (formIds && formIds.length > 0) {
1607
+ whereConditions.push(inArray(schema.submission.formId, formIds));
1608
+ }
1609
+
1610
+ const submissions = await db.query.submission.findMany({
1611
+ where: and(...whereConditions),
1612
+ orderBy: desc(schema.submission.updatedAt),
1613
+ limit,
1614
+ with: {
1615
+ form: true,
1616
+ },
1617
+ });
1618
+
1619
+ // Group submissions by form
1620
+ const groupedMap = new Map<
1621
+ string,
1622
+ {
1623
+ formId: string;
1624
+ formName: string;
1625
+ submissions: Array<{
1626
+ id: string;
1627
+ name: string;
1628
+ createdAt: Date;
1629
+ updatedAt: Date;
1630
+ status: string;
1631
+ }>;
1632
+ }
1633
+ >();
1634
+
1635
+ for (const submission of submissions) {
1636
+ const formId = submission.formId;
1637
+ const formName = submission.form.name;
1638
+
1639
+ if (!groupedMap.has(formId)) {
1640
+ groupedMap.set(formId, {
1641
+ formId,
1642
+ formName,
1643
+ submissions: [],
1644
+ });
1645
+ }
1646
+
1647
+ groupedMap.get(formId)!.submissions.push({
1648
+ id: submission.id,
1649
+ name: submission.name || "Untitled",
1650
+ createdAt: submission.createdAt,
1651
+ updatedAt: submission.updatedAt,
1652
+ status: submission.status,
1653
+ });
1654
+ }
1655
+
1656
+ return okResult(Array.from(groupedMap.values()));
1657
+ } catch {
1658
+ return notFoundResult;
1659
+ }
1660
+ }
1661
+
1662
+ // Placeholder functions for pipeline execution
1663
+ export async function createTransformation({
1664
+ member,
1665
+ runStepId,
1666
+ kind,
1667
+ expression,
1668
+ metadata = {},
1669
+ }: {
1670
+ member: Member;
1671
+ runStepId: string;
1672
+ kind: string;
1673
+ expression?: string;
1674
+ metadata?: object;
1675
+ }): Promise<DalResult<{ id: string }>> {
1676
+ // TODO: Implement transformation creation
1677
+ return notFoundResult;
1678
+ }
1679
+
1680
+ export async function upsertTransformationOutputVersions({
1681
+ member,
1682
+ outputSubmissionId,
1683
+ transformationId,
1684
+ outputData,
1685
+ }: {
1686
+ member: Member;
1687
+ outputSubmissionId: string;
1688
+ transformationId: string;
1689
+ outputData: OutputData;
1690
+ }): Promise<DalResult<{ outputVersionIds: string[]; inputVersionIds: string[] }>> {
1691
+ // Use the renamed function
1692
+ const result = await storePipelineOutputData({
1693
+ member,
1694
+ outputSubmissionId,
1695
+ outputData,
1696
+ });
1697
+
1698
+ if (!result.ok) return notFoundResult;
1699
+
1700
+ // TODO: Return actual version IDs once we track them
1701
+ return okResult({ outputVersionIds: [], inputVersionIds: [] });
1702
+ }
1703
+
1704
+ export async function createPipelineArtifact({
1705
+ member,
1706
+ runId,
1707
+ runStepId,
1708
+ type = "markdown",
1709
+ title,
1710
+ content,
1711
+ metadata = {},
1712
+ }: {
1713
+ member: Member;
1714
+ runId: string;
1715
+ runStepId: string;
1716
+ type?: string;
1717
+ title: string;
1718
+ content: object;
1719
+ metadata?: object;
1720
+ }): Promise<DalResult<Artifact>> {
1721
+ try {
1722
+ // Verify run belongs to member's organization
1723
+ const run = await db.query.pipelineRun.findFirst({
1724
+ where: and(
1725
+ eq(schema.pipelineRun.id, runId),
1726
+ eq(schema.pipelineRun.organizationId, member.organizationId),
1727
+ ),
1728
+ });
1729
+ if (!run) return notFoundResult;
1730
+
1731
+ // Verify run step exists and belongs to the run
1732
+ const runStep = await db.query.pipelineRunStep.findFirst({
1733
+ where: and(eq(schema.pipelineRunStep.id, runStepId), eq(schema.pipelineRunStep.runId, runId)),
1734
+ });
1735
+ if (!runStep) return notFoundResult;
1736
+
1737
+ // Create the artifact
1738
+ const [artifact] = await db
1739
+ .insert(schema.artifact)
1740
+ .values({
1741
+ type,
1742
+ title,
1743
+ content,
1744
+ metadata,
1745
+ })
1746
+ .returning();
1747
+
1748
+ if (!artifact) return notFoundResult;
1749
+
1750
+ // Link artifact to pipeline run and step
1751
+ await db.insert(schema.pipelineRunArtifact).values({
1752
+ runId,
1753
+ artifactId: artifact.id,
1754
+ runStepId,
1755
+ });
1756
+
1757
+ return okResult(artifact);
1758
+ } catch (error) {
1759
+ console.error("[createPipelineArtifact] Error:", error);
1760
+ return notFoundResult;
1761
+ }
1762
+ }
1763
+
1764
+ export async function logPipelineToolCall({
1765
+ member,
1766
+ runStepId,
1767
+ toolName,
1768
+ args,
1769
+ result,
1770
+ status = PIPELINE_RUN_STEP_STATUS.COMPLETED,
1771
+ latencyMs,
1772
+ }: {
1773
+ member: Member;
1774
+ runStepId: string;
1775
+ toolName: string;
1776
+ args: object;
1777
+ result: object;
1778
+ status?: string;
1779
+ latencyMs?: number;
1780
+ }): Promise<DalResult<ToolCall>> {
1781
+ // TODO: Implement tool call logging
1782
+ return notFoundResult;
1783
+ }
1784
+
1785
+ // *****************************************
1786
+ // DAG Pipeline Execution Levels (Recursive CTE)
1787
+ // *****************************************
1788
+
1789
+ /**
1790
+ * Get pipeline steps with their execution levels using recursive CTE.
1791
+ * Level 0 = root steps (no dependencies)
1792
+ * Level 1 = depend on level 0
1793
+ * Level N = depend on level N-1
1794
+ *
1795
+ * This enables proper topological ordering for DAG execution.
1796
+ */
1797
+ export async function getPipelineStepsWithLevels({
1798
+ member,
1799
+ pipelineId,
1800
+ }: {
1801
+ member: Member;
1802
+ pipelineId: string;
1803
+ }) {
1804
+ try {
1805
+ // Use raw SQL for recursive CTE since Drizzle's CTE builder has limitations with recursive joins
1806
+ // Validate pipeline ownership first
1807
+ const result = await db.execute<{
1808
+ id: string;
1809
+ name: string;
1810
+ created_at: Date;
1811
+ role: string | null;
1812
+ type: string;
1813
+ execution_level: number;
1814
+ }>(sql`
1815
+ WITH RECURSIVE step_levels AS (
1816
+ -- Base case: steps with no dependencies (root steps at level 0)
1817
+ SELECT
1818
+ ps.id,
1819
+ ps.name,
1820
+ ps.created_at,
1821
+ ps.role,
1822
+ ps.type,
1823
+ 0 as level
1824
+ FROM pipeline_step ps
1825
+ INNER JOIN pipeline p ON ps.pipeline_id = p.id
1826
+ LEFT JOIN pipeline_step_dependency psd ON ps.id = psd.child_step_id
1827
+ WHERE ps.pipeline_id = ${pipelineId}
1828
+ AND p.organization_id = ${member.organizationId}
1829
+ AND p.deleted_at IS NULL
1830
+ AND psd.parent_step_id IS NULL
1831
+
1832
+ UNION ALL
1833
+
1834
+ -- Recursive case: steps that depend on steps already in step_levels
1835
+ SELECT
1836
+ ps.id,
1837
+ ps.name,
1838
+ ps.created_at,
1839
+ ps.role,
1840
+ ps.type,
1841
+ sl.level + 1
1842
+ FROM pipeline_step ps
1843
+ INNER JOIN pipeline_step_dependency psd ON ps.id = psd.child_step_id
1844
+ INNER JOIN step_levels sl ON psd.parent_step_id = sl.id
1845
+ WHERE ps.pipeline_id = ${pipelineId}
1846
+ )
1847
+ -- Get max level per step (in case multiple paths exist)
1848
+ SELECT
1849
+ id,
1850
+ name,
1851
+ created_at,
1852
+ role,
1853
+ type,
1854
+ MAX(level) as execution_level
1855
+ FROM step_levels
1856
+ GROUP BY id, name, created_at, role, type
1857
+ ORDER BY execution_level, created_at
1858
+ `);
1859
+
1860
+ return okResult(
1861
+ result.rows.map((row) => ({
1862
+ id: row.id,
1863
+ name: row.name,
1864
+ createdAt: row.created_at,
1865
+ role: row.role,
1866
+ type: row.type,
1867
+ executionLevel: row.execution_level,
1868
+ })),
1869
+ );
1870
+ } catch (error) {
1871
+ console.error("[getPipelineStepsWithLevels] Error:", error);
1872
+ return notFoundResult;
1873
+ }
1874
+ }
1875
+
1876
+ /**
1877
+ * Get unblocked steps for a pipeline run that are ready to execute.
1878
+ * Uses recursive CTE + SELECT FOR UPDATE SKIP LOCKED for safe concurrent worker access.
1879
+ *
1880
+ * A step is "unblocked" if:
1881
+ * - It has no unresolved blockers
1882
+ * - All parent step runs are completed
1883
+ * - It's currently in 'pending' status
1884
+ */
1885
+ export async function getUnblockedStepsForRun({
1886
+ member,
1887
+ runId,
1888
+ limit = 10,
1889
+ }: {
1890
+ member: Member;
1891
+ runId: string;
1892
+ limit?: number;
1893
+ }) {
1894
+ try {
1895
+ const run = await db.query.pipelineRun.findFirst({
1896
+ where: and(
1897
+ eq(schema.pipelineRun.id, runId),
1898
+ eq(schema.pipelineRun.organizationId, member.organizationId),
1899
+ ),
1900
+ });
1901
+ if (!run) return notFoundResult;
1902
+
1903
+ // Get execution levels first to understand ordering
1904
+ const levelsResult = await getPipelineStepsWithLevels({
1905
+ member,
1906
+ pipelineId: run.pipelineId,
1907
+ });
1908
+ if (!levelsResult.ok) return levelsResult;
1909
+
1910
+ // Find step runs that are ready to execute
1911
+ const readyStepRuns = await db.transaction(async (tx) => {
1912
+ return await tx
1913
+ .select({
1914
+ id: schema.pipelineRunStep.id,
1915
+ runId: schema.pipelineRunStep.runId,
1916
+ stepId: schema.pipelineRunStep.stepId,
1917
+ status: schema.pipelineRunStep.status,
1918
+ })
1919
+ .from(schema.pipelineRunStep)
1920
+ .where(
1921
+ and(
1922
+ eq(schema.pipelineRunStep.runId, runId),
1923
+ eq(schema.pipelineRunStep.status, "pending"),
1924
+ // Check no unresolved blockers exist
1925
+ sql`NOT EXISTS (
1926
+ SELECT 1 FROM ${schema.pipelineRunStepBlocker}
1927
+ WHERE ${schema.pipelineRunStepBlocker.runStepId} = ${schema.pipelineRunStep.id}
1928
+ AND ${schema.pipelineRunStepBlocker.resolved} = false
1929
+ )`,
1930
+ // Check all parent step runs are completed
1931
+ sql`NOT EXISTS (
1932
+ SELECT 1 FROM ${schema.pipelineStepDependency} dep
1933
+ INNER JOIN ${schema.pipelineRunStep} parent_run_step
1934
+ ON parent_run_step.step_id = dep.parent_step_id
1935
+ AND parent_run_step.run_id = ${schema.pipelineRunStep.runId}
1936
+ WHERE dep.child_step_id = ${schema.pipelineRunStep.stepId}
1937
+ AND parent_run_step.status != 'completed'
1938
+ )`,
1939
+ ),
1940
+ )
1941
+ .limit(limit)
1942
+ .for("update", { skipLocked: true });
1943
+ });
1944
+
1945
+ return okResult(readyStepRuns);
1946
+ } catch (error) {
1947
+ console.error("[getUnblockedStepsForRun] Error:", error);
1948
+ return notFoundResult;
1949
+ }
1950
+ }
1951
+
1952
+ /**
1953
+ * Get all parent step runs for a given step run (via dependencies)
1954
+ */
1955
+ export async function getParentStepRuns({
1956
+ member,
1957
+ runStepId,
1958
+ }: {
1959
+ member: Member;
1960
+ runStepId: string;
1961
+ }) {
1962
+ try {
1963
+ const runStep = await db.query.pipelineRunStep.findFirst({
1964
+ where: eq(schema.pipelineRunStep.id, runStepId),
1965
+ with: { run: true },
1966
+ });
1967
+ if (!runStep || runStep.run.organizationId !== member.organizationId) {
1968
+ return notFoundResult;
1969
+ }
1970
+
1971
+ // Get parent steps via dependency table
1972
+ const parentStepRuns = await db
1973
+ .select({
1974
+ id: schema.pipelineRunStep.id,
1975
+ stepId: schema.pipelineRunStep.stepId,
1976
+ status: schema.pipelineRunStep.status,
1977
+ })
1978
+ .from(schema.pipelineStepDependency)
1979
+ .innerJoin(
1980
+ schema.pipelineRunStep,
1981
+ and(
1982
+ eq(schema.pipelineStepDependency.parentStepId, schema.pipelineRunStep.stepId),
1983
+ eq(schema.pipelineRunStep.runId, runStep.runId),
1984
+ ),
1985
+ )
1986
+ .where(eq(schema.pipelineStepDependency.childStepId, runStep.stepId!));
1987
+
1988
+ return okResult(parentStepRuns);
1989
+ } catch (error) {
1990
+ console.error("[getParentStepRuns] Error:", error);
1991
+ return notFoundResult;
1992
+ }
1993
+ }
1994
+
1995
+ /**
1996
+ * Get parent step runs with their dependency configurations (including formIdMapping)
1997
+ */
1998
+ export async function getParentStepRunsWithDependencies({
1999
+ member,
2000
+ runStepId,
2001
+ }: {
2002
+ member: Member;
2003
+ runStepId: string;
2004
+ }) {
2005
+ try {
2006
+ const runStep = await db.query.pipelineRunStep.findFirst({
2007
+ where: eq(schema.pipelineRunStep.id, runStepId),
2008
+ with: { run: true },
2009
+ });
2010
+ if (!runStep || runStep.run.organizationId !== member.organizationId) {
2011
+ return notFoundResult;
2012
+ }
2013
+
2014
+ // Get parent steps with dependency config
2015
+ const parentStepRuns = await db
2016
+ .select({
2017
+ id: schema.pipelineRunStep.id,
2018
+ stepId: schema.pipelineRunStep.stepId,
2019
+ status: schema.pipelineRunStep.status,
2020
+ formIdMapping: schema.pipelineStepDependency.formIdMapping,
2021
+ multiFormGroupId: schema.pipelineStepDependency.multiFormGroupId,
2022
+ })
2023
+ .from(schema.pipelineStepDependency)
2024
+ .innerJoin(
2025
+ schema.pipelineRunStep,
2026
+ and(
2027
+ eq(schema.pipelineStepDependency.parentStepId, schema.pipelineRunStep.stepId),
2028
+ eq(schema.pipelineRunStep.runId, runStep.runId),
2029
+ ),
2030
+ )
2031
+ .where(eq(schema.pipelineStepDependency.childStepId, runStep.stepId!));
2032
+
2033
+ return okResult(parentStepRuns);
2034
+ } catch (error) {
2035
+ console.error("[getParentStepRunsWithDependencies] Error:", error);
2036
+ return notFoundResult;
2037
+ }
2038
+ }
2039
+
2040
+ /**
2041
+ * Get all child step runs for a given step run (via dependencies)
2042
+ */
2043
+ export async function getChildStepRuns({
2044
+ member,
2045
+ runStepId,
2046
+ }: {
2047
+ member: Member;
2048
+ runStepId: string;
2049
+ }) {
2050
+ try {
2051
+ const runStep = await db.query.pipelineRunStep.findFirst({
2052
+ where: eq(schema.pipelineRunStep.id, runStepId),
2053
+ with: { run: true },
2054
+ });
2055
+ if (!runStep || runStep.run.organizationId !== member.organizationId) {
2056
+ return notFoundResult;
2057
+ }
2058
+
2059
+ // Get child steps via dependency table
2060
+ const childStepRuns = await db
2061
+ .select({
2062
+ id: schema.pipelineRunStep.id,
2063
+ stepId: schema.pipelineRunStep.stepId,
2064
+ status: schema.pipelineRunStep.status,
2065
+ })
2066
+ .from(schema.pipelineStepDependency)
2067
+ .innerJoin(
2068
+ schema.pipelineRunStep,
2069
+ and(
2070
+ eq(schema.pipelineStepDependency.childStepId, schema.pipelineRunStep.stepId),
2071
+ eq(schema.pipelineRunStep.runId, runStep.runId),
2072
+ ),
2073
+ )
2074
+ .where(eq(schema.pipelineStepDependency.parentStepId, runStep.stepId!));
2075
+
2076
+ return okResult(childStepRuns);
2077
+ } catch (error) {
2078
+ console.error("[getChildStepRuns] Error:", error);
2079
+ return notFoundResult;
2080
+ }
2081
+ }
2082
+
2083
+ // *****************************************
2084
+ // Multi-Group → Step Run Linkage
2085
+ // *****************************************
2086
+
2087
+ /**
2088
+ * Get submission groups for a specific formGroupId.
2089
+ * Used to fetch groups from parent output when spawning multi-instance steps.
2090
+ */
2091
+ export async function getSubmissionGroups({
2092
+ member,
2093
+ submissionId,
2094
+ formGroupId,
2095
+ }: {
2096
+ member: Member;
2097
+ submissionId: string;
2098
+ formGroupId: string;
2099
+ }): Promise<DalResult<SubmissionGroup[]>> {
2100
+ try {
2101
+ const submission = await db.query.submission.findFirst({
2102
+ where: and(
2103
+ eq(schema.submission.id, submissionId),
2104
+ eq(schema.submission.organizationId, member.organizationId),
2105
+ isNull(schema.submission.deletedAt),
2106
+ ),
2107
+ });
2108
+ if (!submission) return notFoundResult;
2109
+
2110
+ const groups = await db.query.submissionGroup.findMany({
2111
+ where: and(
2112
+ eq(schema.submissionGroup.submissionId, submissionId),
2113
+ eq(schema.submissionGroup.formGroupId, formGroupId),
2114
+ ),
2115
+ orderBy: asc(schema.submissionGroup.order),
2116
+ });
2117
+
2118
+ return okResult(groups);
2119
+ } catch (error) {
2120
+ console.error("[getSubmissionGroups] Error:", error);
2121
+ return internalErrorResult;
2122
+ }
2123
+ }
2124
+
2125
+ /**
2126
+ * Create child step runs for each group produced by a multi-type parent step.
2127
+ * Links submission groups to their triggered step runs.
2128
+ *
2129
+ * Example: Extraction produces 3 month groups → spawns 3 child analysis runs
2130
+ *
2131
+ * @deprecated This function will be removed. Multi-instance spawning now uses template run steps.
2132
+ */
2133
+ export async function createChildRunsFromGroups({
2134
+ member,
2135
+ parentRunStepId,
2136
+ submissionId,
2137
+ childStepId,
2138
+ }: {
2139
+ member: Member;
2140
+ parentRunStepId: string;
2141
+ submissionId: string;
2142
+ childStepId: string;
2143
+ }) {
2144
+ try {
2145
+ const parentRunStep = await db.query.pipelineRunStep.findFirst({
2146
+ where: eq(schema.pipelineRunStep.id, parentRunStepId),
2147
+ with: {
2148
+ run: true,
2149
+ step: true,
2150
+ },
2151
+ });
2152
+
2153
+ if (!parentRunStep || parentRunStep.run.organizationId !== member.organizationId) {
2154
+ return notFoundResult;
2155
+ }
2156
+
2157
+ // Get dependency config to find which formGroup to spawn for
2158
+ const dependency = await db.query.pipelineStepDependency.findFirst({
2159
+ where: eq(schema.pipelineStepDependency.childStepId, childStepId),
2160
+ });
2161
+
2162
+ if (!dependency?.multiFormGroupId) {
2163
+ return okResult({ childStepRuns: [] }); // Not a multi step, no spawning
2164
+ }
2165
+
2166
+ // Get all groups matching the configured formGroup
2167
+ const groups = await db.query.submissionGroup.findMany({
2168
+ where: and(
2169
+ eq(schema.submissionGroup.submissionId, submissionId),
2170
+ eq(schema.submissionGroup.formGroupId, dependency.multiFormGroupId),
2171
+ ),
2172
+ orderBy: (groups, { asc }) => [asc(groups.order)],
2173
+ });
2174
+
2175
+ if (groups.length === 0) {
2176
+ return okResult({ childStepRuns: [] });
2177
+ }
2178
+
2179
+ // Create a child step run for each group
2180
+ const childStepRuns = [];
2181
+
2182
+ for (const group of groups) {
2183
+ // Create child step run
2184
+ const [childStepRun] = await db
2185
+ .insert(schema.pipelineRunStep)
2186
+ .values({
2187
+ runId: parentRunStep.runId,
2188
+ stepId: childStepId,
2189
+ status: "pending",
2190
+ })
2191
+ .returning();
2192
+
2193
+ if (!childStepRun) continue;
2194
+
2195
+ childStepRuns.push(childStepRun);
2196
+
2197
+ // Note: Input data filtering will be handled via formIdMapping at execution time.
2198
+ // Each child will receive filtered parent data based on the specific group instance.
2199
+ }
2200
+
2201
+ return okResult({ childStepRuns });
2202
+ } catch (error) {
2203
+ console.error("[createChildRunsFromGroups] Error:", error);
2204
+ return notFoundResult;
2205
+ }
2206
+ }
2207
+
2208
+ /**
2209
+ * Get the submission group that triggered a specific step run
2210
+ */
2211
+ export async function getGroupForStepRun({
2212
+ member,
2213
+ runStepId,
2214
+ }: {
2215
+ member: Member;
2216
+ runStepId: string;
2217
+ }) {
2218
+ try {
2219
+ const runStep = await db.query.pipelineRunStep.findFirst({
2220
+ where: eq(schema.pipelineRunStep.id, runStepId),
2221
+ with: { run: true, step: true },
2222
+ });
2223
+
2224
+ if (
2225
+ !runStep ||
2226
+ runStep.run.organizationId !== member.organizationId ||
2227
+ !runStep.submissionGroupId
2228
+ ) {
2229
+ console.error("[getGroupForStepRun] No step associated with run step:", runStepId);
2230
+ return notFoundResult; // No step associated
2231
+ }
2232
+
2233
+ // Find the unique submissionGroup in the output that matches the formGroupId
2234
+ const group = await db.query.submissionGroup.findFirst({
2235
+ with: {
2236
+ formGroup: true,
2237
+ items: {
2238
+ with: {
2239
+ formField: true,
2240
+ versions: {
2241
+ orderBy: [
2242
+ desc(schema.submissionItemVersion.selected),
2243
+ desc(schema.submissionItemVersion.updatedAt),
2244
+ ],
2245
+ with: { documentSource: true },
2246
+ limit: 1,
2247
+ },
2248
+ },
2249
+ },
2250
+ derived: {
2251
+ with: {
2252
+ formDerived: true,
2253
+ },
2254
+ },
2255
+ },
2256
+ where: eq(schema.submissionGroup.id, runStep.submissionGroupId),
2257
+ });
2258
+ if (!group) {
2259
+ return notFoundResult;
2260
+ }
2261
+
2262
+ const { formGroup, items, derived, ...groupData } = group;
2263
+ const groupDenorm = {
2264
+ ...groupData,
2265
+ items: items.map(({ formField, versions, ...itemData }) => ({
2266
+ ...itemData,
2267
+ name: formField.name,
2268
+ type: formField.type,
2269
+ hint: formField.hint,
2270
+ value: versions.at(0)?.value ?? null,
2271
+ })),
2272
+ derived: derived.map(({ formDerived, ...derivedData }) => ({
2273
+ ...derivedData,
2274
+ name: formDerived.name,
2275
+ prompt: formDerived.prompt,
2276
+ })),
2277
+ name: formGroup.name,
2278
+ hint: formGroup.hint,
2279
+ multi: formGroup.multi,
2280
+ };
2281
+
2282
+ return okResult(groupDenorm);
2283
+ } catch (error) {
2284
+ console.error("[getGroupForStepRun] Error:", error);
2285
+ return notFoundResult;
2286
+ }
2287
+ }
2288
+
2289
+ // *****************************************
2290
+ // Data Sources: Request Calls & Manual Inputs
2291
+ // *****************************************
2292
+
2293
+ /**
2294
+ * Log an API request call made by an integration step
2295
+ */
2296
+ export async function logRequestCall({
2297
+ member,
2298
+ runStepId,
2299
+ method,
2300
+ url,
2301
+ requestHeaders = {},
2302
+ requestBody,
2303
+ statusCode,
2304
+ responseHeaders,
2305
+ responseBody,
2306
+ durationMs,
2307
+ error,
2308
+ submissionItemVersionId,
2309
+ }: {
2310
+ member: Member;
2311
+ runStepId: string;
2312
+ method: string;
2313
+ url: string;
2314
+ requestHeaders?: Record<string, string>;
2315
+ requestBody?: unknown;
2316
+ statusCode?: number;
2317
+ responseHeaders?: Record<string, string>;
2318
+ responseBody?: unknown;
2319
+ durationMs?: number;
2320
+ error?: string;
2321
+ submissionItemVersionId?: string;
2322
+ }) {
2323
+ try {
2324
+ const runStep = await db.query.pipelineRunStep.findFirst({
2325
+ where: eq(schema.pipelineRunStep.id, runStepId),
2326
+ with: { run: true },
2327
+ });
2328
+
2329
+ if (!runStep || runStep.run.organizationId !== member.organizationId) {
2330
+ return notFoundResult;
2331
+ }
2332
+
2333
+ const [requestCall] = await db
2334
+ .insert(schema.pipelineRunIntegrationRequest)
2335
+ .values({
2336
+ runStepId,
2337
+ submissionItemVersionId,
2338
+ method,
2339
+ url,
2340
+ requestHeaders,
2341
+ requestBody,
2342
+ statusCode,
2343
+ responseHeaders,
2344
+ responseBody,
2345
+ durationMs,
2346
+ error,
2347
+ })
2348
+ .returning();
2349
+
2350
+ if (!requestCall) return notFoundResult;
2351
+ return okResult(requestCall);
2352
+ } catch (error) {
2353
+ console.error("[logRequestCall] Error:", error);
2354
+ return notFoundResult;
2355
+ }
2356
+ }
2357
+
2358
+ /**
2359
+ * Get all request calls made by a step run
2360
+ */
2361
+ export async function getRequestCallsForStepRun({
2362
+ member,
2363
+ runStepId,
2364
+ }: {
2365
+ member: Member;
2366
+ runStepId: string;
2367
+ }) {
2368
+ try {
2369
+ const runStep = await db.query.pipelineRunStep.findFirst({
2370
+ where: eq(schema.pipelineRunStep.id, runStepId),
2371
+ with: { run: true },
2372
+ });
2373
+
2374
+ if (!runStep || runStep.run.organizationId !== member.organizationId) {
2375
+ return notFoundResult;
2376
+ }
2377
+
2378
+ const requestCalls = await db.query.pipelineRunIntegrationRequest.findMany({
2379
+ where: eq(schema.pipelineRunIntegrationRequest.runStepId, runStepId),
2380
+ orderBy: (calls, { desc }) => [desc(calls.createdAt)],
2381
+ });
2382
+
2383
+ return okResult(requestCalls);
2384
+ } catch (error) {
2385
+ console.error("[getRequestCallsForStepRun] Error:", error);
2386
+ return notFoundResult;
2387
+ }
2388
+ }
2389
+
2390
+ /**
2391
+ * Get a specific request call by ID
2392
+ */
2393
+ export async function getRequestCall({
2394
+ member,
2395
+ requestCallId,
2396
+ }: {
2397
+ member: Member;
2398
+ requestCallId: string;
2399
+ }) {
2400
+ try {
2401
+ const requestCall = await db.query.pipelineRunIntegrationRequest.findFirst({
2402
+ where: eq(schema.pipelineRunIntegrationRequest.id, requestCallId),
2403
+ with: {
2404
+ runStep: {
2405
+ with: {
2406
+ run: true,
2407
+ step: true,
2408
+ },
2409
+ },
2410
+ },
2411
+ });
2412
+
2413
+ if (!requestCall || requestCall.runStep.run.organizationId !== member.organizationId) {
2414
+ return notFoundResult;
2415
+ }
2416
+
2417
+ return okResult(requestCall);
2418
+ } catch (error) {
2419
+ console.error("[getRequestCall] Error:", error);
2420
+ return notFoundResult;
2421
+ }
2422
+ }
2423
+
2424
+ /**
2425
+ * Remove a manual input from a step run
2426
+ */
2427
+ export async function removeManualInput({
2428
+ member,
2429
+ manualInputId,
2430
+ }: {
2431
+ member: Member;
2432
+ manualInputId: string;
2433
+ }) {
2434
+ try {
2435
+ // TODO: Implement manual input table
2436
+ // const manualInput = await db.query.pipelineRunStepManualInput.findFirst({
2437
+ // where: eq(schema.pipelineRunStepManualInput.id, manualInputId),
2438
+ // with: {
2439
+ // runStep: {
2440
+ // with: { run: true },
2441
+ // },
2442
+ // },
2443
+ // });
2444
+
2445
+ // if (!manualInput || manualInput.runStep.run.organizationId !== member.organizationId) {
2446
+ // return notFoundResult;
2447
+ // }
2448
+
2449
+ // await db
2450
+ // .delete(schema.pipelineRunStepManualInput)
2451
+ // .where(eq(schema.pipelineRunStepManualInput.id, manualInputId));
2452
+
2453
+ // return okResult({ success: true });
2454
+ return notFoundResult;
2455
+ } catch (error) {
2456
+ console.error("[removeManualInput] Error:", error);
2457
+ return notFoundResult;
2458
+ }
2459
+ }
2460
+
2461
+ // *****************************************
2462
+ // Step Dependency Management
2463
+ // *****************************************
2464
+
2465
+ /**
2466
+ * Add a parent dependency to a step
2467
+ */
2468
+ export async function addStepDependency({
2469
+ member,
2470
+ childStepId,
2471
+ parentStepId,
2472
+ multiFormGroupId,
2473
+ }: {
2474
+ member: Member;
2475
+ childStepId: string;
2476
+ parentStepId: string;
2477
+ multiFormGroupId?: string | null;
2478
+ }) {
2479
+ try {
2480
+ // Verify both steps exist and belong to the member's organization
2481
+ const childStep = await db.query.pipelineStep.findFirst({
2482
+ where: eq(schema.pipelineStep.id, childStepId),
2483
+ with: { pipeline: true },
2484
+ });
2485
+
2486
+ if (!childStep || childStep.pipeline.organizationId !== member.organizationId) {
2487
+ return notFoundResult;
2488
+ }
2489
+
2490
+ const parentStep = await db.query.pipelineStep.findFirst({
2491
+ where: eq(schema.pipelineStep.id, parentStepId),
2492
+ with: { pipeline: true },
2493
+ });
2494
+
2495
+ if (!parentStep || parentStep.pipeline.organizationId !== member.organizationId) {
2496
+ return notFoundResult;
2497
+ }
2498
+
2499
+ // Check if they're in the same pipeline
2500
+ if (childStep.pipelineId !== parentStep.pipelineId) {
2501
+ return {
2502
+ ok: false,
2503
+ error: "validation-error",
2504
+ message: "Both steps must be in the same pipeline",
2505
+ };
2506
+ }
2507
+
2508
+ // Check if dependency already exists
2509
+ const existing = await db.query.pipelineStepDependency.findFirst({
2510
+ where: and(
2511
+ eq(schema.pipelineStepDependency.childStepId, childStepId),
2512
+ eq(schema.pipelineStepDependency.parentStepId, parentStepId),
2513
+ ),
2514
+ });
2515
+
2516
+ if (existing) {
2517
+ return {
2518
+ ok: false,
2519
+ error: "validation-error",
2520
+ message: "Dependency already exists",
2521
+ };
2522
+ }
2523
+
2524
+ // Create the dependency
2525
+ const [dependency] = await db
2526
+ .insert(schema.pipelineStepDependency)
2527
+ .values({
2528
+ id: nanoid(),
2529
+ childStepId,
2530
+ parentStepId,
2531
+ multiFormGroupId: multiFormGroupId ?? null,
2532
+ })
2533
+ .returning();
2534
+
2535
+ if (!dependency) return internalErrorResult;
2536
+
2537
+ return okResult({
2538
+ id: dependency.id,
2539
+ childStepId: dependency.childStepId,
2540
+ parentStepId: dependency.parentStepId,
2541
+ multiFormGroupId: dependency.multiFormGroupId,
2542
+ });
2543
+ } catch (error) {
2544
+ console.error("[addStepDependency] Error:", error);
2545
+ return internalErrorResult;
2546
+ }
2547
+ }
2548
+
2549
+ /**
2550
+ * Remove a step dependency
2551
+ */
2552
+ export async function removeStepDependency({
2553
+ member,
2554
+ dependencyId,
2555
+ }: {
2556
+ member: Member;
2557
+ dependencyId: string;
2558
+ }) {
2559
+ try {
2560
+ const dependency = await db.query.pipelineStepDependency.findFirst({
2561
+ where: eq(schema.pipelineStepDependency.id, dependencyId),
2562
+ with: {
2563
+ childStep: {
2564
+ with: { pipeline: true },
2565
+ },
2566
+ },
2567
+ });
2568
+
2569
+ if (!dependency || dependency.childStep.pipeline.organizationId !== member.organizationId) {
2570
+ return notFoundResult;
2571
+ }
2572
+
2573
+ await db
2574
+ .delete(schema.pipelineStepDependency)
2575
+ .where(eq(schema.pipelineStepDependency.id, dependencyId));
2576
+
2577
+ return okResult({ success: true });
2578
+ } catch (error) {
2579
+ console.error("[removeStepDependency] Error:", error);
2580
+ return internalErrorResult;
2581
+ }
2582
+ }
2583
+
2584
+ export async function getStepDependenciesByParentStepId({
2585
+ member,
2586
+ parentStepId,
2587
+ }: {
2588
+ member: Member;
2589
+ parentStepId: string;
2590
+ }) {
2591
+ try {
2592
+ const parentStep = await db.query.pipelineStep.findFirst({
2593
+ where: eq(schema.pipelineStep.id, parentStepId),
2594
+ with: { pipeline: true },
2595
+ });
2596
+
2597
+ if (!parentStep || parentStep.pipeline.organizationId !== member.organizationId) {
2598
+ return notFoundResult;
2599
+ }
2600
+
2601
+ const dependencies = await db.query.pipelineStepDependency.findMany({
2602
+ where: eq(schema.pipelineStepDependency.parentStepId, parentStepId),
2603
+ with: {
2604
+ childStep: true,
2605
+ },
2606
+ });
2607
+
2608
+ return okResult(dependencies);
2609
+ } catch (error) {
2610
+ console.error("[getStepDependenciesByParentStepId] Error:", error);
2611
+ return internalErrorResult;
2612
+ }
2613
+ }
2614
+
2615
+ // *****************************************
2616
+ // Document Management (Extraction Steps)
2617
+ // *****************************************
2618
+
2619
+ /**
2620
+ * Get documents linked to extraction step run
2621
+ */
2622
+ export async function getStepDocuments({
2623
+ member,
2624
+ runStepId,
2625
+ }: {
2626
+ member: Member;
2627
+ runStepId: string;
2628
+ }) {
2629
+ try {
2630
+ const runStep = await db.query.pipelineRunStep.findFirst({
2631
+ where: eq(schema.pipelineRunStep.id, runStepId),
2632
+ with: {
2633
+ run: {
2634
+ with: { pipeline: true },
2635
+ },
2636
+ documents: true,
2637
+ },
2638
+ });
2639
+
2640
+ if (!runStep || runStep.run.pipeline.organizationId !== member.organizationId) {
2641
+ return notFoundResult;
2642
+ }
2643
+
2644
+ // Load full document records
2645
+ const documentIds = runStep.documents.map((d) => d.documentId);
2646
+ if (documentIds.length === 0) {
2647
+ return okResult([]);
2648
+ }
2649
+
2650
+ const documents = await db.query.document.findMany({
2651
+ where: inArray(schema.document.id, documentIds),
2652
+ });
2653
+
2654
+ return okResult(documents);
2655
+ } catch (error) {
2656
+ console.error("[getStepDocuments] Error:", error);
2657
+ return internalErrorResult;
2658
+ }
2659
+ }
2660
+
2661
+ /**
2662
+ * Link document to extraction step run
2663
+ */
2664
+ export async function addStepDocument({
2665
+ member,
2666
+ runStepId,
2667
+ documentId,
2668
+ }: {
2669
+ member: Member;
2670
+ runStepId: string;
2671
+ documentId: string;
2672
+ }) {
2673
+ try {
2674
+ const runStep = await db.query.pipelineRunStep.findFirst({
2675
+ where: eq(schema.pipelineRunStep.id, runStepId),
2676
+ with: {
2677
+ run: {
2678
+ with: { pipeline: true },
2679
+ },
2680
+ },
2681
+ });
2682
+
2683
+ if (!runStep || runStep.run.pipeline.organizationId !== member.organizationId) {
2684
+ return notFoundResult;
2685
+ }
2686
+
2687
+ // Verify document exists and belongs to organization
2688
+ const document = await db.query.document.findFirst({
2689
+ where: eq(schema.document.id, documentId),
2690
+ with: {
2691
+ deal: true,
2692
+ },
2693
+ });
2694
+
2695
+ if (!document || document.deal.organizationId !== member.organizationId) {
2696
+ return notFoundResult;
2697
+ }
2698
+
2699
+ // Check if already linked
2700
+ const existing = await db.query.pipelineRunStepDocument.findFirst({
2701
+ where: and(
2702
+ eq(schema.pipelineRunStepDocument.runStepId, runStepId),
2703
+ eq(schema.pipelineRunStepDocument.documentId, documentId),
2704
+ ),
2705
+ });
2706
+
2707
+ if (existing) {
2708
+ return okResult({ success: true }); // Already linked, no-op
2709
+ }
2710
+
2711
+ await db.insert(schema.pipelineRunStepDocument).values({
2712
+ id: nanoid(),
2713
+ runStepId,
2714
+ documentId,
2715
+ });
2716
+
2717
+ return okResult({ success: true });
2718
+ } catch (error) {
2719
+ console.error("[addStepDocument] Error:", error);
2720
+ return internalErrorResult;
2721
+ }
2722
+ }
2723
+
2724
+ /**
2725
+ * Unlink document from extraction step run
2726
+ */
2727
+ export async function removeStepDocument({
2728
+ member,
2729
+ runStepId,
2730
+ documentId,
2731
+ }: {
2732
+ member: Member;
2733
+ runStepId: string;
2734
+ documentId: string;
2735
+ }) {
2736
+ try {
2737
+ const runStep = await db.query.pipelineRunStep.findFirst({
2738
+ where: eq(schema.pipelineRunStep.id, runStepId),
2739
+ with: {
2740
+ run: {
2741
+ with: { pipeline: true },
2742
+ },
2743
+ },
2744
+ });
2745
+
2746
+ if (!runStep || runStep.run.pipeline.organizationId !== member.organizationId) {
2747
+ return notFoundResult;
2748
+ }
2749
+
2750
+ await db
2751
+ .delete(schema.pipelineRunStepDocument)
2752
+ .where(
2753
+ and(
2754
+ eq(schema.pipelineRunStepDocument.runStepId, runStepId),
2755
+ eq(schema.pipelineRunStepDocument.documentId, documentId),
2756
+ ),
2757
+ );
2758
+
2759
+ return okResult({ success: true });
2760
+ } catch (error) {
2761
+ console.error("[removeStepDocument] Error:", error);
2762
+ return internalErrorResult;
2763
+ }
2764
+ }
2765
+
2766
+ // *****************************************
2767
+ // Integration Config Management
2768
+ // *****************************************
2769
+
2770
+ /**
2771
+ * Get integration config for step
2772
+ */
2773
+ export async function getStepIntegrationConfig({
2774
+ member,
2775
+ stepId,
2776
+ }: {
2777
+ member: Member;
2778
+ stepId: string;
2779
+ }): Promise<DalResult<PipelineStepIntegrationConfig | null>> {
2780
+ try {
2781
+ const step = await db.query.pipelineStep.findFirst({
2782
+ where: eq(schema.pipelineStep.id, stepId),
2783
+ with: {
2784
+ pipeline: true,
2785
+ integrationConfig: true,
2786
+ },
2787
+ });
2788
+
2789
+ if (!step || step.pipeline.organizationId !== member.organizationId) {
2790
+ return notFoundResult;
2791
+ }
2792
+
2793
+ return okResult(step.integrationConfig);
2794
+ } catch (error) {
2795
+ console.error("[getStepIntegrationConfig] Error:", error);
2796
+ return internalErrorResult;
2797
+ }
2798
+ }
2799
+
2800
+ /**
2801
+ * Create/update integration config for step
2802
+ */
2803
+ export async function upsertStepIntegrationConfig({
2804
+ member,
2805
+ stepId,
2806
+ config,
2807
+ }: {
2808
+ member: Member;
2809
+ stepId: string;
2810
+ config: {
2811
+ method: string;
2812
+ urlTemplate: string;
2813
+ headers: Record<string, string>;
2814
+ bodyTemplate?: string;
2815
+ };
2816
+ }): Promise<DalResult<PipelineStepIntegrationConfig>> {
2817
+ try {
2818
+ const step = await db.query.pipelineStep.findFirst({
2819
+ where: eq(schema.pipelineStep.id, stepId),
2820
+ with: {
2821
+ pipeline: true,
2822
+ integrationConfig: true,
2823
+ },
2824
+ });
2825
+
2826
+ if (!step || step.pipeline.organizationId !== member.organizationId) {
2827
+ return notFoundResult;
2828
+ }
2829
+
2830
+ if (step.integrationConfig) {
2831
+ // Update existing
2832
+ const [updated] = await db
2833
+ .update(schema.pipelineStepIntegrationConfig)
2834
+ .set({
2835
+ method: config.method,
2836
+ urlTemplate: config.urlTemplate,
2837
+ headers: config.headers,
2838
+ bodyTemplate: config.bodyTemplate,
2839
+ updatedAt: new Date(),
2840
+ })
2841
+ .where(eq(schema.pipelineStepIntegrationConfig.stepId, stepId))
2842
+ .returning();
2843
+
2844
+ if (!updated) {
2845
+ return internalErrorResult;
2846
+ }
2847
+
2848
+ return okResult(updated);
2849
+ } else {
2850
+ // Create new
2851
+ const [created] = await db
2852
+ .insert(schema.pipelineStepIntegrationConfig)
2853
+ .values({
2854
+ id: nanoid(),
2855
+ stepId,
2856
+ method: config.method,
2857
+ urlTemplate: config.urlTemplate,
2858
+ headers: config.headers,
2859
+ bodyTemplate: config.bodyTemplate,
2860
+ })
2861
+ .returning();
2862
+
2863
+ if (!created) {
2864
+ return internalErrorResult;
2865
+ }
2866
+
2867
+ return okResult(created);
2868
+ }
2869
+ } catch (error) {
2870
+ console.error("[upsertStepIntegrationConfig] Error:", error);
2871
+ return internalErrorResult;
2872
+ }
2873
+ }
2874
+
2875
+ /**
2876
+ * Delete integration config
2877
+ */
2878
+ export async function deleteStepIntegrationConfig({
2879
+ member,
2880
+ stepId,
2881
+ }: {
2882
+ member: Member;
2883
+ stepId: string;
2884
+ }): Promise<DalResult<{ success: true }>> {
2885
+ try {
2886
+ const step = await db.query.pipelineStep.findFirst({
2887
+ where: eq(schema.pipelineStep.id, stepId),
2888
+ with: {
2889
+ pipeline: true,
2890
+ },
2891
+ });
2892
+
2893
+ if (!step || step.pipeline.organizationId !== member.organizationId) {
2894
+ return notFoundResult;
2895
+ }
2896
+
2897
+ await db
2898
+ .delete(schema.pipelineStepIntegrationConfig)
2899
+ .where(eq(schema.pipelineStepIntegrationConfig.stepId, stepId));
2900
+
2901
+ return okResult({ success: true });
2902
+ } catch (error) {
2903
+ console.error("[deleteStepIntegrationConfig] Error:", error);
2904
+ return internalErrorResult;
2905
+ }
2906
+ }
2907
+
2908
+ // *****************************************
2909
+ // Recursive Citation Chain
2910
+ // *****************************************
2911
+
2912
+ export type CitationNode =
2913
+ | {
2914
+ type: "document";
2915
+ documentCitationId: string;
2916
+ documentExtractionId: string;
2917
+ documentId: string;
2918
+ documentName: string;
2919
+ path: string;
2920
+ text: string;
2921
+ sourceRef: string;
2922
+ }
2923
+ | {
2924
+ type: "integration";
2925
+ requestCallId: string;
2926
+ method: string;
2927
+ url: string;
2928
+ statusCode: number | null;
2929
+ durationMs: number | null;
2930
+ }
2931
+ | {
2932
+ type: "transformation";
2933
+ submissionItemVersionId: string;
2934
+ sourceItemVersionIds: string[];
2935
+ children: CitationNode[];
2936
+ }
2937
+ | {
2938
+ type: "manual";
2939
+ submissionItemVersionId: string;
2940
+ value: string | number | null;
2941
+ };
2942
+
2943
+ /**
2944
+ * Recursively walk the citation chain for a submission item version.
2945
+ * Returns a tree showing all source data until reaching leaf nodes (documents or API calls).
2946
+ *
2947
+ * Example output:
2948
+ * {
2949
+ * type: "transformation",
2950
+ * children: [
2951
+ * { type: "document", documentName: "Q1 Report.pdf", page: 3 },
2952
+ * { type: "integration", url: "GET /financials/q2", statusCode: 200 }
2953
+ * ]
2954
+ * }
2955
+ */
2956
+ export async function getCitationChain({
2957
+ member,
2958
+ submissionItemVersionId,
2959
+ }: {
2960
+ member: Member;
2961
+ submissionItemVersionId: string;
2962
+ }): Promise<DalResult<CitationNode>> {
2963
+ try {
2964
+ // First get the version, then check org access via joins
2965
+ const version = await db.query.submissionItemVersion.findFirst({
2966
+ where: eq(schema.submissionItemVersion.id, submissionItemVersionId),
2967
+ });
2968
+
2969
+ if (!version) return notFoundResult;
2970
+
2971
+ // Verify organization access
2972
+ const item = await db.query.submissionItem.findFirst({
2973
+ where: eq(schema.submissionItem.id, version.submissionItemId),
2974
+ });
2975
+ if (!item) return notFoundResult;
2976
+
2977
+ const group = await db.query.submissionGroup.findFirst({
2978
+ where: eq(schema.submissionGroup.id, item.submissionGroupId),
2979
+ });
2980
+ if (!group) return notFoundResult;
2981
+
2982
+ const submission = await db.query.submission.findFirst({
2983
+ where: eq(schema.submission.id, group.submissionId),
2984
+ });
2985
+ if (!submission || submission.organizationId !== member.organizationId) {
2986
+ return notFoundResult;
2987
+ }
2988
+
2989
+ const node = await buildCitationNode(version);
2990
+ return okResult(node);
2991
+ } catch (error) {
2992
+ console.error("[getCitationChain] Error:", error);
2993
+ return notFoundResult;
2994
+ }
2995
+ }
2996
+
2997
+ /**
2998
+ * Recursive helper to build a citation node tree
2999
+ */
3000
+ async function buildCitationNode(
3001
+ version: typeof schema.submissionItemVersion.$inferSelect,
3002
+ ): Promise<CitationNode> {
3003
+ switch (version.sourceType) {
3004
+ case "document": {
3005
+ if (!version.sourceId) {
3006
+ throw new Error("Document source missing sourceId");
3007
+ }
3008
+
3009
+ const citation = await db.query.documentCitation.findFirst({
3010
+ where: eq(schema.documentCitation.id, version.sourceId),
3011
+ });
3012
+
3013
+ if (!citation) {
3014
+ throw new Error(`Document citation ${version.sourceId} not found`);
3015
+ }
3016
+
3017
+ const extraction = await db.query.documentExtraction.findFirst({
3018
+ where: eq(schema.documentExtraction.id, citation.documentExtractionId),
3019
+ });
3020
+
3021
+ const document = extraction
3022
+ ? await db.query.document.findFirst({
3023
+ where: eq(schema.document.id, extraction.documentId),
3024
+ })
3025
+ : null;
3026
+
3027
+ return {
3028
+ type: "document",
3029
+ documentCitationId: citation.id,
3030
+ documentExtractionId: citation.documentExtractionId,
3031
+ documentId: document?.id || "",
3032
+ documentName: document?.name || "Unknown Document",
3033
+ path: citation.path,
3034
+ text: citation.text,
3035
+ sourceRef: citation.sourceRef,
3036
+ };
3037
+ }
3038
+
3039
+ case "integration": {
3040
+ if (!version.sourceId) {
3041
+ throw new Error("Integration source missing sourceId");
3042
+ }
3043
+
3044
+ const requestCall = await db.query.pipelineRunIntegrationRequest.findFirst({
3045
+ where: eq(schema.pipelineRunIntegrationRequest.id, version.sourceId),
3046
+ });
3047
+
3048
+ if (!requestCall) {
3049
+ throw new Error(`Request call ${version.sourceId} not found`);
3050
+ }
3051
+
3052
+ return {
3053
+ type: "integration",
3054
+ requestCallId: requestCall.id,
3055
+ method: requestCall.method,
3056
+ url: requestCall.url,
3057
+ statusCode: requestCall.statusCode,
3058
+ durationMs: requestCall.durationMs,
3059
+ };
3060
+ }
3061
+
3062
+ case "transformation": {
3063
+ // Get the transformation that produced this version using sourceId
3064
+ if (!version.sourceId) {
3065
+ return {
3066
+ type: "transformation",
3067
+ submissionItemVersionId: version.id,
3068
+ sourceItemVersionIds: [],
3069
+ children: [],
3070
+ };
3071
+ }
3072
+
3073
+ const transformation = await db.query.dataTransformation.findFirst({
3074
+ where: eq(schema.dataTransformation.id, version.sourceId),
3075
+ });
3076
+
3077
+ if (!transformation || !transformation.sourceItemVersionIds) {
3078
+ return {
3079
+ type: "transformation",
3080
+ submissionItemVersionId: version.id,
3081
+ sourceItemVersionIds: [],
3082
+ children: [],
3083
+ };
3084
+ }
3085
+
3086
+ // Recursively get source versions
3087
+ const sourceVersions = await db.query.submissionItemVersion.findMany({
3088
+ where: sql`${schema.submissionItemVersion.id} = ANY(ARRAY[${sql.join(
3089
+ transformation.sourceItemVersionIds.map((id) => sql`${id}`),
3090
+ sql`, `,
3091
+ )}]::text[])`,
3092
+ });
3093
+
3094
+ const children = await Promise.all(sourceVersions.map((sv) => buildCitationNode(sv)));
3095
+
3096
+ return {
3097
+ type: "transformation",
3098
+ submissionItemVersionId: version.id,
3099
+ sourceItemVersionIds: transformation.sourceItemVersionIds,
3100
+ children,
3101
+ };
3102
+ }
3103
+
3104
+ case "manual": {
3105
+ return {
3106
+ type: "manual",
3107
+ submissionItemVersionId: version.id,
3108
+ value: version.value,
3109
+ };
3110
+ }
3111
+
3112
+ case "merged":
3113
+ case "message":
3114
+ default: {
3115
+ // For other types, return as transformation with no children for now
3116
+ return {
3117
+ type: "transformation",
3118
+ submissionItemVersionId: version.id,
3119
+ sourceItemVersionIds: [],
3120
+ children: [],
3121
+ };
3122
+ }
3123
+ }
3124
+ }
3125
+
3126
+ /**
3127
+ * Get all leaf citations (documents and API calls) for a submission item version.
3128
+ * Flattens the recursive tree into a list of leaf nodes.
3129
+ */
3130
+ export async function getLeafCitations({
3131
+ member,
3132
+ submissionItemVersionId,
3133
+ }: {
3134
+ member: Member;
3135
+ submissionItemVersionId: string;
3136
+ }) {
3137
+ try {
3138
+ const chainResult = await getCitationChain({ member, submissionItemVersionId });
3139
+ if (!chainResult.ok) return chainResult;
3140
+
3141
+ const leaves: CitationNode[] = [];
3142
+
3143
+ function collectLeaves(node: CitationNode) {
3144
+ if (node.type === "document" || node.type === "integration" || node.type === "manual") {
3145
+ leaves.push(node);
3146
+ } else if (node.type === "transformation") {
3147
+ for (const child of node.children) {
3148
+ collectLeaves(child);
3149
+ }
3150
+ }
3151
+ }
3152
+
3153
+ collectLeaves(chainResult.data);
3154
+ return okResult(leaves);
3155
+ } catch (error) {
3156
+ console.error("[getLeafCitations] Error:", error);
3157
+ return notFoundResult;
3158
+ }
3159
+ }
3160
+
3161
+ // ========================================================================================
3162
+ // Blocker Management
3163
+ // ========================================================================================
3164
+
3165
+ /**
3166
+ * Create a pipeline run with pre-flight validation and blocker creation.
3167
+ * Checks all steps for potential blockers before execution begins.
3168
+ */
3169
+ export async function createPipelineRunWithBlockers({
3170
+ member,
3171
+ pipelineId,
3172
+ dealId,
3173
+ inputDocumentIds = [],
3174
+ inputSubmissionIds = [],
3175
+ triggerType = "manual",
3176
+ }: {
3177
+ member: Member;
3178
+ pipelineId: string;
3179
+ dealId: string;
3180
+ inputDocumentIds?: string[];
3181
+ inputSubmissionIds?: string[];
3182
+ triggerType?: string;
3183
+ }): Promise<
3184
+ DalResult<{ runId: string; blockers: Array<{ stepId: string; type: string; details: object }> }>
3185
+ > {
3186
+ try {
3187
+ // First create the run using existing function
3188
+ const runResult = await createPipelineRun({
3189
+ member,
3190
+ pipelineId,
3191
+ dealId,
3192
+ triggerType,
3193
+ });
3194
+
3195
+ if (!runResult.ok) return runResult;
3196
+ const run = runResult.data;
3197
+
3198
+ // Get pipeline with steps
3199
+ const pipeline = await db.query.pipeline.findFirst({
3200
+ where: and(
3201
+ eq(schema.pipeline.id, pipelineId),
3202
+ eq(schema.pipeline.organizationId, member.organizationId),
3203
+ isNull(schema.pipeline.deletedAt),
3204
+ ),
3205
+ with: {
3206
+ steps: {
3207
+ orderBy: asc(schema.pipelineStep.createdAt),
3208
+ with: {
3209
+ outputForms: true,
3210
+ integrationConfig: true,
3211
+ },
3212
+ },
3213
+ },
3214
+ });
3215
+
3216
+ if (!pipeline) return notFoundResult;
3217
+
3218
+ const blockers: Array<{ stepId: string; type: string; details: object }> = [];
3219
+
3220
+ // Get all dependencies for all steps
3221
+ const allDependencies = await db.query.pipelineStepDependency.findMany({
3222
+ where: inArray(
3223
+ schema.pipelineStepDependency.childStepId,
3224
+ pipeline.steps.map((s) => s.id),
3225
+ ),
3226
+ });
3227
+
3228
+ // Group dependencies by child step
3229
+ const dependenciesByStep = new Map<string, typeof allDependencies>();
3230
+ for (const dep of allDependencies) {
3231
+ const existing = dependenciesByStep.get(dep.childStepId) || [];
3232
+ existing.push(dep);
3233
+ dependenciesByStep.set(dep.childStepId, existing);
3234
+ }
3235
+
3236
+ // Create run steps for all pipeline steps
3237
+ const runSteps = await Promise.all(
3238
+ pipeline.steps.map(async (step) => {
3239
+ const runStepResult = await createPipelineRunStep({
3240
+ member,
3241
+ runId: run.id,
3242
+ stepId: step.id,
3243
+ });
3244
+ return runStepResult.ok ? runStepResult.data : null;
3245
+ }),
3246
+ );
3247
+
3248
+ // Check each step for blockers
3249
+ for (let i = 0; i < pipeline.steps.length; i++) {
3250
+ const step = pipeline.steps[i];
3251
+ const runStep = runSteps[i];
3252
+
3253
+ if (!runStep || !step) continue;
3254
+
3255
+ // Check extraction steps have input documents
3256
+ if (step.role === "extraction") {
3257
+ if (inputDocumentIds.length === 0 && inputSubmissionIds.length === 0) {
3258
+ await createStepRunBlocker({
3259
+ runStepId: runStep.id,
3260
+ blockerType: "missing_upload",
3261
+ blockerDetails: {
3262
+ type: "missing_upload",
3263
+ message: "No input documents or submissions provided",
3264
+ },
3265
+ });
3266
+ blockers.push({
3267
+ stepId: step.id,
3268
+ type: "missing_upload",
3269
+ details: { message: "No input documents or submissions provided" },
3270
+ });
3271
+ }
3272
+ }
3273
+
3274
+ // Check integration steps have config
3275
+ if (step.role === "integration" && !step.integrationConfig) {
3276
+ await createStepRunBlocker({
3277
+ runStepId: runStep.id,
3278
+ blockerType: "missing_config",
3279
+ blockerDetails: {
3280
+ type: "missing_config",
3281
+ message: "Integration step missing configuration",
3282
+ },
3283
+ });
3284
+ blockers.push({
3285
+ stepId: step.id,
3286
+ type: "missing_config",
3287
+ details: { message: "Integration step missing configuration" },
3288
+ });
3289
+ }
3290
+
3291
+ // Check analysis steps have output form
3292
+ if ((step.role === "analysis" || step.role === null) && step.outputForms.length === 0) {
3293
+ await createStepRunBlocker({
3294
+ runStepId: runStep.id,
3295
+ blockerType: "missing_config",
3296
+ blockerDetails: { type: "missing_config", message: "Analysis step missing output form" },
3297
+ });
3298
+ blockers.push({
3299
+ stepId: step.id,
3300
+ type: "missing_config",
3301
+ details: { message: "Analysis step missing output form" },
3302
+ });
3303
+ }
3304
+
3305
+ // Check manual steps have output forms and create blockers for each required form
3306
+ if (step.role === "manual") {
3307
+ if (step.outputForms.length === 0) {
3308
+ await createStepRunBlocker({
3309
+ runStepId: runStep.id,
3310
+ blockerType: "missing_config",
3311
+ blockerDetails: {
3312
+ type: "missing_config",
3313
+ message: "Manual step missing output form configuration",
3314
+ },
3315
+ });
3316
+ blockers.push({
3317
+ stepId: step.id,
3318
+ type: "missing_config",
3319
+ details: { message: "Manual step missing output form configuration" },
3320
+ });
3321
+ } else {
3322
+ // Create a blocker for each required output form
3323
+ const forms = await db.query.form.findMany({
3324
+ where: inArray(
3325
+ schema.form.id,
3326
+ step.outputForms.map((f) => f.formId),
3327
+ ),
3328
+ });
3329
+ const formIdToName = new Map(forms.map((f) => [f.id, f.name]));
3330
+
3331
+ for (const outputForm of step.outputForms) {
3332
+ await createStepRunBlocker({
3333
+ runStepId: runStep.id,
3334
+ blockerType: "missing_manual_input",
3335
+ blockerDetails: {
3336
+ type: "missing_manual_input",
3337
+ stepName: step.name,
3338
+ stepDescription: step.description,
3339
+ requiredFormId: outputForm.formId,
3340
+ requiredFormName: formIdToName.get(outputForm.formId) || outputForm.formId,
3341
+ },
3342
+ });
3343
+ blockers.push({
3344
+ stepId: step.id,
3345
+ type: "missing_manual_input",
3346
+ details: {
3347
+ stepName: step.name,
3348
+ stepDescription: step.description,
3349
+ requiredFormId: outputForm.formId,
3350
+ requiredFormName: formIdToName.get(outputForm.formId) || outputForm.formId,
3351
+ },
3352
+ });
3353
+ }
3354
+ }
3355
+ }
3356
+
3357
+ // Mark steps with dependencies as blocked by parent_incomplete
3358
+ const stepDependencies = dependenciesByStep.get(step.id) || [];
3359
+ if (stepDependencies.length > 0) {
3360
+ await createStepRunBlocker({
3361
+ runStepId: runStep.id,
3362
+ blockerType: "parent_incomplete",
3363
+ blockerDetails: {
3364
+ type: "parent_incomplete",
3365
+ parentStepIds: stepDependencies.map((d) => d.parentStepId),
3366
+ message: "Waiting for parent steps to complete",
3367
+ },
3368
+ });
3369
+ blockers.push({
3370
+ stepId: step.id,
3371
+ type: "parent_incomplete",
3372
+ details: {
3373
+ type: "parent_incomplete",
3374
+ parentStepIds: stepDependencies.map((d) => d.parentStepId),
3375
+ message: "Waiting for parent steps to complete",
3376
+ },
3377
+ });
3378
+ }
3379
+ }
3380
+
3381
+ return okResult({ runId: run.id, blockers });
3382
+ } catch (error) {
3383
+ console.error("[createPipelineRunWithBlockers] Error:", error);
3384
+ return notFoundResult;
3385
+ }
3386
+ }
3387
+
3388
+ /**
3389
+ * Sync blockers for an existing pipeline run
3390
+ * Removes outdated blockers and creates new ones based on current state
3391
+ */
3392
+ export async function syncPipelineRunBlockers({
3393
+ member,
3394
+ runId,
3395
+ }: {
3396
+ member: Member;
3397
+ runId: string;
3398
+ }) {
3399
+ try {
3400
+ // Get the run with pipeline and steps
3401
+ const run = await db.query.pipelineRun.findFirst({
3402
+ where: and(
3403
+ eq(schema.pipelineRun.id, runId),
3404
+ eq(schema.pipelineRun.organizationId, member.organizationId),
3405
+ ),
3406
+ with: {
3407
+ pipeline: {
3408
+ with: {
3409
+ steps: {
3410
+ orderBy: asc(schema.pipelineStep.createdAt),
3411
+ with: {
3412
+ outputForms: true,
3413
+ integrationConfig: true,
3414
+ },
3415
+ },
3416
+ },
3417
+ },
3418
+ steps: {
3419
+ with: {
3420
+ documents: true,
3421
+ },
3422
+ },
3423
+ },
3424
+ });
3425
+
3426
+ if (!run) return notFoundResult;
3427
+
3428
+ // Get all dependencies for pipeline steps
3429
+ const allDependencies = await db.query.pipelineStepDependency.findMany({
3430
+ where: inArray(
3431
+ schema.pipelineStepDependency.childStepId,
3432
+ run.pipeline.steps.map((s) => s.id),
3433
+ ),
3434
+ });
3435
+
3436
+ // Group dependencies by child step
3437
+ const dependenciesByStep = new Map<string, typeof allDependencies>();
3438
+ for (const dep of allDependencies) {
3439
+ const existing = dependenciesByStep.get(dep.childStepId) || [];
3440
+ existing.push(dep);
3441
+ dependenciesByStep.set(dep.childStepId, existing);
3442
+ }
3443
+
3444
+ // TODO: Get existing run submissions once schema is finalized
3445
+ const hasInputs = false;
3446
+ let syncedCount = 0;
3447
+
3448
+ // Delete all existing unresolved blockers (we'll recreate them if still needed)
3449
+ await db.delete(schema.pipelineRunStepBlocker).where(
3450
+ and(
3451
+ inArray(
3452
+ schema.pipelineRunStepBlocker.runStepId,
3453
+ run.steps.map((s) => s.id),
3454
+ ),
3455
+ eq(schema.pipelineRunStepBlocker.resolved, false),
3456
+ ),
3457
+ );
3458
+
3459
+ // Check each run step for blockers
3460
+ for (const runStep of run.steps) {
3461
+ const step = run.pipeline.steps.find((s) => s.id === runStep.stepId);
3462
+ if (!step) continue;
3463
+
3464
+ // Skip completed or failed steps
3465
+ if (
3466
+ runStep.status === PIPELINE_RUN_STEP_STATUS.COMPLETED ||
3467
+ runStep.status === PIPELINE_RUN_STEP_STATUS.FAILED
3468
+ ) {
3469
+ continue;
3470
+ }
3471
+
3472
+ // Check extraction steps have input documents
3473
+ if (step.role === "extraction" && runStep.documents.length === 0) {
3474
+ await createStepRunBlocker({
3475
+ runStepId: runStep.id,
3476
+ blockerType: "missing_upload",
3477
+ blockerDetails: {
3478
+ type: "missing_upload",
3479
+ message: "No documents linked to extraction step",
3480
+ },
3481
+ });
3482
+ syncedCount++;
3483
+ }
3484
+
3485
+ // Check integration steps have config
3486
+ if (step.role === "integration" && !step.integrationConfig) {
3487
+ await createStepRunBlocker({
3488
+ runStepId: runStep.id,
3489
+ blockerType: "missing_config",
3490
+ blockerDetails: {
3491
+ type: "missing_config",
3492
+ message: "Integration step missing configuration",
3493
+ },
3494
+ });
3495
+ syncedCount++;
3496
+ }
3497
+
3498
+ // Check analysis steps have output form
3499
+ if ((step.role === "analysis" || step.role === null) && step.outputForms.length === 0) {
3500
+ await createStepRunBlocker({
3501
+ runStepId: runStep.id,
3502
+ blockerType: "missing_config",
3503
+ blockerDetails: { type: "missing_config", message: "Analysis step missing output form" },
3504
+ });
3505
+ syncedCount++;
3506
+ }
3507
+
3508
+ // Check manual steps have output submissions for all required forms
3509
+ if (step.role === "manual") {
3510
+ if (step.outputForms.length === 0) {
3511
+ await createStepRunBlocker({
3512
+ runStepId: runStep.id,
3513
+ blockerType: "missing_config",
3514
+ blockerDetails: {
3515
+ type: "missing_config",
3516
+ message: "Manual step missing output form configuration",
3517
+ },
3518
+ });
3519
+ syncedCount++;
3520
+ } else {
3521
+ // Get existing output submissions for this run step
3522
+ const outputSubmissions = await db.query.pipelineRunStepOutputSubmission.findMany({
3523
+ where: eq(schema.pipelineRunStepOutputSubmission.runStepId, runStep.id),
3524
+ });
3525
+
3526
+ // Load submission forms for all outputs
3527
+ const outputSubmissionDetails = await Promise.all(
3528
+ outputSubmissions.map(async (output) => {
3529
+ const submission = await db.query.submission.findFirst({
3530
+ where: eq(schema.submission.id, output.submissionId),
3531
+ });
3532
+ return { ...output, formId: submission?.formId };
3533
+ }),
3534
+ );
3535
+
3536
+ // Load form names
3537
+ const forms = await db.query.form.findMany({
3538
+ where: inArray(
3539
+ schema.form.id,
3540
+ step.outputForms.map((f) => f.formId),
3541
+ ),
3542
+ });
3543
+ const formIdToName = new Map(forms.map((f) => [f.id, f.name]));
3544
+
3545
+ // Create a blocker for each required form that doesn't have a submission
3546
+ for (const outputForm of step.outputForms) {
3547
+ const hasSubmissionForForm = outputSubmissionDetails.some(
3548
+ (output) => output.formId === outputForm.formId,
3549
+ );
3550
+
3551
+ if (!hasSubmissionForForm) {
3552
+ await createStepRunBlocker({
3553
+ runStepId: runStep.id,
3554
+ blockerType: "missing_manual_input",
3555
+ blockerDetails: {
3556
+ type: "missing_manual_input",
3557
+ stepName: step.name,
3558
+ stepDescription: step.description,
3559
+ requiredFormId: outputForm.formId,
3560
+ requiredFormName: formIdToName.get(outputForm.formId) || outputForm.formId,
3561
+ },
3562
+ });
3563
+ syncedCount++;
3564
+ }
3565
+ }
3566
+ }
3567
+ }
3568
+
3569
+ // Mark steps with dependencies as blocked by parent_incomplete
3570
+ const stepDependencies = dependenciesByStep.get(step.id) || [];
3571
+ if (stepDependencies.length > 0) {
3572
+ // Check if any parent steps are incomplete
3573
+ const parentRunSteps = run.steps.filter((rs) =>
3574
+ stepDependencies.some((d) => d.parentStepId === rs.stepId),
3575
+ );
3576
+ const hasIncompleteParents = parentRunSteps.some(
3577
+ (ps) =>
3578
+ ps.status !== PIPELINE_RUN_STEP_STATUS.COMPLETED &&
3579
+ ps.status !== PIPELINE_RUN_STEP_STATUS.FAILED,
3580
+ );
3581
+
3582
+ if (hasIncompleteParents) {
3583
+ await createStepRunBlocker({
3584
+ runStepId: runStep.id,
3585
+ blockerType: "parent_incomplete",
3586
+ blockerDetails: {
3587
+ type: "parent_incomplete",
3588
+ parentStepIds: stepDependencies.map((d) => d.parentStepId),
3589
+ message: "Waiting for parent steps to complete",
3590
+ },
3591
+ });
3592
+ syncedCount++;
3593
+ }
3594
+ }
3595
+ }
3596
+
3597
+ return okResult({ blockersSynced: syncedCount });
3598
+ } catch (error) {
3599
+ console.error("[syncPipelineRunBlockers] Error:", error);
3600
+ return internalErrorResult;
3601
+ }
3602
+ }
3603
+
3604
+ /**
3605
+ * Resolve blockers for a step when it completes successfully
3606
+ */
3607
+ export async function resolveStepBlockersOnCompletion({
3608
+ member,
3609
+ runStepId,
3610
+ }: {
3611
+ member: Member;
3612
+ runStepId: string;
3613
+ }) {
3614
+ try {
3615
+ const runStep = await db.query.pipelineRunStep.findFirst({
3616
+ where: eq(schema.pipelineRunStep.id, runStepId),
3617
+ with: {
3618
+ run: true,
3619
+ step: true,
3620
+ },
3621
+ });
3622
+
3623
+ if (!runStep || runStep.run.organizationId !== member.organizationId) {
3624
+ return notFoundResult;
3625
+ }
3626
+
3627
+ let resolvedCount = 0;
3628
+
3629
+ console.log(`[resolveStepBlockersOnCompletion] Starting for runStepId: ${runStepId}`);
3630
+ console.log(`[resolveStepBlockersOnCompletion] Run step details:`, {
3631
+ runStepId: runStep.id,
3632
+ stepId: runStep.stepId,
3633
+ status: runStep.status,
3634
+ });
3635
+
3636
+ // 1. Resolve this step's own blockers
3637
+ const ownBlockers = await db
3638
+ .update(schema.pipelineRunStepBlocker)
3639
+ .set({
3640
+ resolved: true,
3641
+ resolvedAt: new Date(),
3642
+ })
3643
+ .where(
3644
+ and(
3645
+ eq(schema.pipelineRunStepBlocker.runStepId, runStepId),
3646
+ eq(schema.pipelineRunStepBlocker.resolved, false),
3647
+ ),
3648
+ )
3649
+ .returning({ id: schema.pipelineRunStepBlocker.id });
3650
+
3651
+ resolvedCount += ownBlockers.length;
3652
+ console.log(`[resolveStepBlockersOnCompletion] Resolved ${ownBlockers.length} own blocker(s)`);
3653
+
3654
+ // 2. Find child steps and check if their parent_incomplete blockers can be resolved
3655
+ console.log(
3656
+ `[resolveStepBlockersOnCompletion] Looking for child steps with parentStepId: ${runStep.stepId}`,
3657
+ );
3658
+
3659
+ const childStepRuns = await db
3660
+ .select({
3661
+ id: schema.pipelineRunStep.id,
3662
+ stepId: schema.pipelineRunStep.stepId,
3663
+ })
3664
+ .from(schema.pipelineStepDependency)
3665
+ .innerJoin(
3666
+ schema.pipelineRunStep,
3667
+ and(
3668
+ eq(schema.pipelineStepDependency.childStepId, schema.pipelineRunStep.stepId),
3669
+ eq(schema.pipelineRunStep.runId, runStep.runId),
3670
+ ),
3671
+ )
3672
+ .where(eq(schema.pipelineStepDependency.parentStepId, runStep.stepId!));
3673
+
3674
+ console.log(
3675
+ `[resolveStepBlockersOnCompletion] Found ${childStepRuns.length} child run step(s)`,
3676
+ );
3677
+ if (childStepRuns.length > 0) {
3678
+ console.log(
3679
+ `[resolveStepBlockersOnCompletion] Child run steps:`,
3680
+ childStepRuns.map((c) => ({ id: c.id, stepId: c.stepId })),
3681
+ );
3682
+ }
3683
+
3684
+ for (const childStepRun of childStepRuns) {
3685
+ console.log(
3686
+ `[resolveStepBlockersOnCompletion] Checking child run step: ${childStepRun.id} (stepId: ${childStepRun.stepId})`,
3687
+ );
3688
+
3689
+ // Check if all parents of this child are completed
3690
+ const parentStepRuns = await db
3691
+ .select({
3692
+ id: schema.pipelineRunStep.id,
3693
+ status: schema.pipelineRunStep.status,
3694
+ })
3695
+ .from(schema.pipelineStepDependency)
3696
+ .innerJoin(
3697
+ schema.pipelineRunStep,
3698
+ and(
3699
+ eq(schema.pipelineStepDependency.parentStepId, schema.pipelineRunStep.stepId),
3700
+ eq(schema.pipelineRunStep.runId, runStep.runId),
3701
+ ),
3702
+ )
3703
+ .where(eq(schema.pipelineStepDependency.childStepId, childStepRun.stepId!));
3704
+
3705
+ console.log(
3706
+ `[resolveStepBlockersOnCompletion] Found ${parentStepRuns.length} parent run step(s) for child ${childStepRun.id}`,
3707
+ );
3708
+ console.log(
3709
+ `[resolveStepBlockersOnCompletion] Parent statuses:`,
3710
+ parentStepRuns.map((p) => ({ id: p.id, status: p.status })),
3711
+ );
3712
+
3713
+ const allParentsComplete = parentStepRuns.every(
3714
+ (p) => p.status === PIPELINE_RUN_STEP_STATUS.COMPLETED,
3715
+ );
3716
+
3717
+ console.log(`[resolveStepBlockersOnCompletion] All parents complete? ${allParentsComplete}`);
3718
+
3719
+ if (allParentsComplete) {
3720
+ // Resolve parent_incomplete blockers for this child
3721
+ console.log(
3722
+ `[resolveStepBlockersOnCompletion] Resolving parent_incomplete blockers for child ${childStepRun.id}`,
3723
+ );
3724
+ const childBlockers = await db
3725
+ .update(schema.pipelineRunStepBlocker)
3726
+ .set({
3727
+ resolved: true,
3728
+ resolvedAt: new Date(),
3729
+ })
3730
+ .where(
3731
+ and(
3732
+ eq(schema.pipelineRunStepBlocker.runStepId, childStepRun.id),
3733
+ eq(schema.pipelineRunStepBlocker.blockerType, "parent_incomplete"),
3734
+ eq(schema.pipelineRunStepBlocker.resolved, false),
3735
+ ),
3736
+ )
3737
+ .returning({ id: schema.pipelineRunStepBlocker.id });
3738
+
3739
+ console.log(
3740
+ `[resolveStepBlockersOnCompletion] Resolved ${childBlockers.length} blocker(s) for child ${childStepRun.id}`,
3741
+ );
3742
+ resolvedCount += childBlockers.length;
3743
+ }
3744
+ }
3745
+
3746
+ console.log(`[resolveStepBlockersOnCompletion] Total resolved: ${resolvedCount}`);
3747
+ return okResult({ resolvedCount });
3748
+ } catch (error) {
3749
+ console.error("[resolveStepBlockersOnCompletion] Error:", error);
3750
+ return notFoundResult;
3751
+ }
3752
+ }
3753
+
3754
+ /**
3755
+ * Create a blocker for a pipeline run step
3756
+ */
3757
+ export async function createStepRunBlocker<T extends PipelineRunStepBlockerType>(params: {
3758
+ runStepId: string;
3759
+ blockerType: T;
3760
+ blockerDetails: Extract<PipelineRunStepBlockerDetails, { type: T }>;
3761
+ }) {
3762
+ try {
3763
+ const [blocker] = await db
3764
+ .insert(schema.pipelineRunStepBlocker)
3765
+ .values({
3766
+ runStepId: params.runStepId,
3767
+ blockerType: params.blockerType,
3768
+ blockerDetails: params.blockerDetails,
3769
+ })
3770
+ .returning({ id: schema.pipelineRunStepBlocker.id });
3771
+
3772
+ if (!blocker) return notFoundResult;
3773
+ return okResult(blocker);
3774
+ } catch (error) {
3775
+ console.error("[createStepRunBlocker] Error:", error);
3776
+ return notFoundResult;
3777
+ }
3778
+ }
3779
+
3780
+ /**
3781
+ * Resolve a blocker by ID
3782
+ */
3783
+ export async function resolveBlocker(blockerId: string) {
3784
+ try {
3785
+ const [updated] = await db
3786
+ .update(schema.pipelineRunStepBlocker)
3787
+ .set({
3788
+ resolved: true,
3789
+ resolvedAt: new Date(),
3790
+ })
3791
+ .where(eq(schema.pipelineRunStepBlocker.id, blockerId))
3792
+ .returning({ id: schema.pipelineRunStepBlocker.id });
3793
+
3794
+ if (!updated) return notFoundResult;
3795
+ return okResult({ success: true });
3796
+ } catch (error) {
3797
+ console.error("[resolveBlocker] Error:", error);
3798
+ return notFoundResult;
3799
+ }
3800
+ }
3801
+
3802
+ /**
3803
+ * Get all blockers for a step run (optionally filter by resolved status)
3804
+ */
3805
+ export async function getStepRunBlockers(params: { runStepId: string; resolved?: boolean }) {
3806
+ try {
3807
+ const conditions = [eq(schema.pipelineRunStepBlocker.runStepId, params.runStepId)];
3808
+
3809
+ if (params.resolved !== undefined) {
3810
+ conditions.push(eq(schema.pipelineRunStepBlocker.resolved, params.resolved));
3811
+ }
3812
+
3813
+ const blockers = await db
3814
+ .select({
3815
+ id: schema.pipelineRunStepBlocker.id,
3816
+ blockerType: schema.pipelineRunStepBlocker.blockerType,
3817
+ blockerDetails: schema.pipelineRunStepBlocker.blockerDetails,
3818
+ resolved: schema.pipelineRunStepBlocker.resolved,
3819
+ resolvedAt: schema.pipelineRunStepBlocker.resolvedAt,
3820
+ createdAt: schema.pipelineRunStepBlocker.createdAt,
3821
+ })
3822
+ .from(schema.pipelineRunStepBlocker)
3823
+ .where(and(...conditions));
3824
+
3825
+ return okResult(blockers);
3826
+ } catch (error) {
3827
+ console.error("[getStepRunBlockers] Error:", error);
3828
+ return notFoundResult;
3829
+ }
3830
+ }
3831
+
3832
+ /**
3833
+ * Check if a step run has any unresolved blockers
3834
+ */
3835
+ export async function hasUnresolvedBlockers(runStepId: string) {
3836
+ try {
3837
+ const [result] = await db
3838
+ .select({ count: count() })
3839
+ .from(schema.pipelineRunStepBlocker)
3840
+ .where(
3841
+ and(
3842
+ eq(schema.pipelineRunStepBlocker.runStepId, runStepId),
3843
+ eq(schema.pipelineRunStepBlocker.resolved, false),
3844
+ ),
3845
+ );
3846
+
3847
+ return okResult((result?.count ?? 0) > 0);
3848
+ } catch (error) {
3849
+ console.error("[hasUnresolvedBlockers] Error:", error);
3850
+ return notFoundResult;
3851
+ }
3852
+ }
3853
+
3854
+ /**
3855
+ * Get all blockers for all steps in a pipeline run, grouped by step.
3856
+ * Only returns level 0 blockers (user-actionable) - filters out parent_incomplete
3857
+ * since those are automatically resolved when dependencies complete.
3858
+ */
3859
+ export async function getAllBlockersForRun({ member, runId }: { member: Member; runId: string }) {
3860
+ try {
3861
+ // Verify run belongs to member's organization
3862
+ const run = await db.query.pipelineRun.findFirst({
3863
+ where: eq(schema.pipelineRun.id, runId),
3864
+ });
3865
+
3866
+ if (!run || run.organizationId !== member.organizationId) {
3867
+ return notFoundResult;
3868
+ }
3869
+
3870
+ // Get all run steps with their blockers (exclude templates - they're just placeholders)
3871
+ const runSteps = await db.query.pipelineRunStep.findMany({
3872
+ where: and(
3873
+ eq(schema.pipelineRunStep.runId, runId),
3874
+ // Filter out template steps - only show normal and instance steps
3875
+ or(
3876
+ eq(schema.pipelineRunStep.spawnType, "normal"),
3877
+ eq(schema.pipelineRunStep.spawnType, "instance"),
3878
+ ),
3879
+ ),
3880
+ with: {
3881
+ step: true,
3882
+ blockers: {
3883
+ orderBy: [desc(schema.pipelineRunStepBlocker.createdAt)],
3884
+ },
3885
+ },
3886
+ orderBy: [asc(schema.pipelineRunStep.createdAt)],
3887
+ });
3888
+
3889
+ const result = runSteps.map((runStep) => ({
3890
+ runStepId: runStep.id,
3891
+ stepName: runStep.step?.name ?? "Unknown Step",
3892
+ stepOrder: runStep.stepRunIndex ?? 0,
3893
+ // Filter out parent_incomplete blockers - they're not user-actionable
3894
+ // and are automatically resolved when dependencies complete
3895
+ blockers: runStep.blockers
3896
+ .filter((blocker) => blocker.blockerType !== "parent_incomplete")
3897
+ .map((blocker) => ({
3898
+ id: blocker.id,
3899
+ blockerType: blocker.blockerType,
3900
+ blockerDetails: blocker.blockerDetails,
3901
+ resolved: blocker.resolved,
3902
+ resolvedAt: blocker.resolvedAt,
3903
+ createdAt: blocker.createdAt,
3904
+ })),
3905
+ }));
3906
+
3907
+ return okResult(result);
3908
+ } catch (error) {
3909
+ console.error("[getAllBlockersForRun] Error:", error);
3910
+ return internalErrorResult;
3911
+ }
3912
+ }
3913
+
3914
+ /**
3915
+ * Get a pipeline by ID with basic fields (for verification)
3916
+ */
3917
+ export async function getPipelineById({
3918
+ member,
3919
+ pipelineId,
3920
+ }: {
3921
+ member: Member;
3922
+ pipelineId: string;
3923
+ }) {
3924
+ try {
3925
+ const pipeline = await db.query.pipeline.findFirst({
3926
+ where: and(
3927
+ eq(schema.pipeline.id, pipelineId),
3928
+ eq(schema.pipeline.organizationId, member.organizationId),
3929
+ isNull(schema.pipeline.deletedAt),
3930
+ ),
3931
+ columns: {
3932
+ id: true,
3933
+ name: true,
3934
+ description: true,
3935
+ organizationId: true,
3936
+ },
3937
+ });
3938
+
3939
+ if (!pipeline) {
3940
+ return notFoundResult;
3941
+ }
3942
+
3943
+ return okResult(pipeline);
3944
+ } catch (error) {
3945
+ console.error("[getPipelineById] Error:", error);
3946
+ return internalErrorResult;
3947
+ }
3948
+ }
3949
+
3950
+ /**
3951
+ * Get a pipeline with its input forms
3952
+ */
3953
+ export async function getPipelineWithInputForms({
3954
+ member,
3955
+ pipelineId,
3956
+ }: {
3957
+ member: Member;
3958
+ pipelineId: string;
3959
+ }) {
3960
+ try {
3961
+ const pipeline = await db.query.pipeline.findFirst({
3962
+ where: and(
3963
+ eq(schema.pipeline.id, pipelineId),
3964
+ eq(schema.pipeline.organizationId, member.organizationId),
3965
+ isNull(schema.pipeline.deletedAt),
3966
+ ),
3967
+ columns: {
3968
+ id: true,
3969
+ name: true,
3970
+ },
3971
+ });
3972
+
3973
+ if (!pipeline) {
3974
+ return notFoundResult;
3975
+ }
3976
+
3977
+ return okResult(pipeline);
3978
+ } catch (error) {
3979
+ console.error("[getPipelineWithInputForms] Error:", error);
3980
+ return internalErrorResult;
3981
+ }
3982
+ }
3983
+
3984
+ /**
3985
+ * Create an empty pipeline with basic metadata
3986
+ */
3987
+ export async function createEmptyPipeline({
3988
+ member,
3989
+ name = "Untitled Pipeline",
3990
+ description = "",
3991
+ }: {
3992
+ member: Member;
3993
+ name?: string;
3994
+ description?: string;
3995
+ }): Promise<
3996
+ DalResult<{
3997
+ id: string;
3998
+ name: string;
3999
+ }>
4000
+ > {
4001
+ try {
4002
+ const [pipeline] = await db
4003
+ .insert(schema.pipeline)
4004
+ .values({
4005
+ organizationId: member.organizationId,
4006
+ name,
4007
+ description,
4008
+ isActive: true,
4009
+ version: "1",
4010
+ metadata: {},
4011
+ })
4012
+ .returning({ id: schema.pipeline.id, name: schema.pipeline.name });
4013
+ invariant(pipeline);
4014
+
4015
+ return okResult(pipeline);
4016
+ } catch (error) {
4017
+ console.error("[createEmptyPipeline] Error:", error);
4018
+ return internalErrorResult;
4019
+ }
4020
+ }
4021
+
4022
+ /**
4023
+ * Create a complete pipeline with first step and conversation (transaction)
4024
+ */
4025
+ export async function createPipelineWithStepAndConversation({
4026
+ member,
4027
+ name,
4028
+ description,
4029
+ stepPrompt,
4030
+ }: {
4031
+ member: Member;
4032
+ name: string;
4033
+ description: string;
4034
+ stepPrompt: string;
4035
+ }) {
4036
+ try {
4037
+ const created = await db.transaction(async (tx) => {
4038
+ const [pipeline] = await tx
4039
+ .insert(schema.pipeline)
4040
+ .values({
4041
+ organizationId: member.organizationId,
4042
+ name,
4043
+ description,
4044
+ metadata: { createdBy: "pipeline_ui" },
4045
+ })
4046
+ .returning();
4047
+ invariant(pipeline);
4048
+
4049
+ // Create conversation for this pipeline
4050
+ const [conversation] = await tx
4051
+ .insert(schema.conversation)
4052
+ .values({
4053
+ organizationId: member.organizationId,
4054
+ agent: true,
4055
+ name: `Pipeline: ${name}`,
4056
+ model: "openai/gpt-5.2",
4057
+ memory: {},
4058
+ })
4059
+ .returning();
4060
+ invariant(conversation);
4061
+
4062
+ // Link conversation to pipeline via conversation_resource
4063
+ await tx.insert(schema.conversationResource).values({
4064
+ conversationId: conversation.id,
4065
+ resourceType: "pipeline",
4066
+ resourceId: pipeline.id,
4067
+ source: "system",
4068
+ });
4069
+
4070
+ // TODO: Create pipeline-form mapping once pipelineForm table is created
4071
+
4072
+ const [step] = await tx
4073
+ .insert(schema.pipelineStep)
4074
+ .values({
4075
+ pipelineId: pipeline.id,
4076
+ name: "Step 1",
4077
+ description,
4078
+ prompt: stepPrompt,
4079
+ toolsAllowed: [],
4080
+ retryPolicy: {},
4081
+ })
4082
+ .returning();
4083
+ invariant(step);
4084
+
4085
+ return pipeline;
4086
+ });
4087
+
4088
+ return okResult({
4089
+ id: created.id,
4090
+ name: created.name,
4091
+ description: created.description,
4092
+ });
4093
+ } catch (error) {
4094
+ console.error("[createPipelineWithStepAndConversation] Error:", error);
4095
+ return internalErrorResult;
4096
+ }
4097
+ }
4098
+
4099
+ // *****************************************
4100
+ // Blocker Resolution Functions
4101
+ // *****************************************
4102
+
4103
+ /**
4104
+ * Link documents to a pipeline run step.
4105
+ * Used when resolving missing_upload blockers.
4106
+ */
4107
+ export async function linkDocumentsToRunStep({
4108
+ member,
4109
+ runStepId,
4110
+ documentIds,
4111
+ }: {
4112
+ member: Member;
4113
+ runStepId: string;
4114
+ documentIds: string[];
4115
+ }): Promise<DalResult<void>> {
4116
+ try {
4117
+ // Verify the run step exists and belongs to the member's org
4118
+ const runStepResult = await getPipelineRunStep({ member, runStepId });
4119
+ if (!runStepResult.ok) return runStepResult;
4120
+
4121
+ // Insert documents into pipelineRunStepDocument
4122
+ if (documentIds.length > 0) {
4123
+ await db.insert(schema.pipelineRunStepDocument).values(
4124
+ documentIds.map((documentId) => ({
4125
+ runStepId,
4126
+ documentId,
4127
+ })),
4128
+ );
4129
+ }
4130
+
4131
+ return okResult(undefined);
4132
+ } catch (error) {
4133
+ console.error("[linkDocumentsToRunStep] Error:", error);
4134
+ return internalErrorResult;
4135
+ }
4136
+ }
4137
+
4138
+ /**
4139
+ * Link manual input submissions to a pipeline run step as output submissions.
4140
+ * Used when resolving missing_manual_input blockers.
4141
+ */
4142
+ export async function linkManualInputsToRunStep({
4143
+ member,
4144
+ runStepId,
4145
+ submissionIds,
4146
+ addedBy,
4147
+ reason,
4148
+ }: {
4149
+ member: Member;
4150
+ runStepId: string;
4151
+ submissionIds: string[];
4152
+ addedBy: string;
4153
+ reason?: string;
4154
+ }): Promise<DalResult<void>> {
4155
+ try {
4156
+ // Verify the run step exists and belongs to the member's org
4157
+ const runStepResult = await getPipelineRunStep({ member, runStepId });
4158
+ if (!runStepResult.ok) return runStepResult;
4159
+
4160
+ // Insert submissions into pipelineRunStepOutputSubmission
4161
+ if (submissionIds.length > 0) {
4162
+ // Check for existing entries to avoid duplicates
4163
+ const existing = await db.query.pipelineRunStepOutputSubmission.findMany({
4164
+ where: and(
4165
+ eq(schema.pipelineRunStepOutputSubmission.runStepId, runStepId),
4166
+ inArray(schema.pipelineRunStepOutputSubmission.submissionId, submissionIds),
4167
+ ),
4168
+ });
4169
+
4170
+ const existingSubmissionIds = new Set(existing.map((e) => e.submissionId));
4171
+ const newSubmissionIds = submissionIds.filter((id) => !existingSubmissionIds.has(id));
4172
+
4173
+ if (newSubmissionIds.length > 0) {
4174
+ await db.insert(schema.pipelineRunStepOutputSubmission).values(
4175
+ newSubmissionIds.map((submissionId) => ({
4176
+ runStepId,
4177
+ submissionId,
4178
+ })),
4179
+ );
4180
+ console.log(
4181
+ `[linkManualInputsToRunStep] Added ${newSubmissionIds.length} output submission(s) to step ${runStepId}`,
4182
+ );
4183
+ }
4184
+ }
4185
+
4186
+ return okResult(undefined);
4187
+ } catch (error) {
4188
+ console.error("[linkManualInputsToRunStep] Error:", error);
4189
+ return internalErrorResult;
4190
+ }
4191
+ }
4192
+
4193
+ /**
4194
+ * Retry a pipeline run step by marking it as pending.
4195
+ * This allows the workflow engine to pick it up again.
4196
+ */
4197
+ export async function retryRunStep({
4198
+ member,
4199
+ runStepId,
4200
+ }: {
4201
+ member: Member;
4202
+ runStepId: string;
4203
+ }): Promise<DalResult<void>> {
4204
+ try {
4205
+ // Verify the run step exists and belongs to the member's org
4206
+ const runStepResult = await getPipelineRunStep({ member, runStepId });
4207
+ if (!runStepResult.ok) return runStepResult;
4208
+
4209
+ // Update the run step status to pending
4210
+ await db
4211
+ .update(schema.pipelineRunStep)
4212
+ .set({
4213
+ status: PIPELINE_RUN_STEP_STATUS.PENDING,
4214
+ })
4215
+ .where(eq(schema.pipelineRunStep.id, runStepId));
4216
+
4217
+ return okResult(undefined);
4218
+ } catch (error) {
4219
+ console.error("[retryRunStep] Error:", error);
4220
+ return internalErrorResult;
4221
+ }
4222
+ }