@anchrd/intel 0.59.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 (364) hide show
  1. package/README.md +496 -0
  2. package/bin/intel.mjs +134 -0
  3. package/dist/adapters/cloudflare/cloudflare-flow-workflow.d.ts +42 -0
  4. package/dist/adapters/cloudflare/cloudflare-flow-workflow.js +89 -0
  5. package/dist/adapters/cloudflare/cloudflare.d.ts +29 -0
  6. package/dist/adapters/cloudflare/cloudflare.js +416 -0
  7. package/dist/adapters/cloudflare/cloudflare.types.d.ts +31 -0
  8. package/dist/adapters/cloudflare/cloudflare.types.js +1 -0
  9. package/dist/adapters/content/content.d.ts +3 -0
  10. package/dist/adapters/content/content.js +128 -0
  11. package/dist/adapters/content/content.types.d.ts +38 -0
  12. package/dist/adapters/content/content.types.js +1 -0
  13. package/dist/adapters/db/db-audit.d.ts +62 -0
  14. package/dist/adapters/db/db-audit.js +150 -0
  15. package/dist/adapters/db/db-boards.d.ts +6 -0
  16. package/dist/adapters/db/db-boards.js +324 -0
  17. package/dist/adapters/db/db-feed.d.ts +30 -0
  18. package/dist/adapters/db/db-feed.js +276 -0
  19. package/dist/adapters/db/db-flows.d.ts +42 -0
  20. package/dist/adapters/db/db-flows.js +1311 -0
  21. package/dist/adapters/db/db-grants.d.ts +133 -0
  22. package/dist/adapters/db/db-grants.js +269 -0
  23. package/dist/adapters/db/db-indexing.d.ts +3 -0
  24. package/dist/adapters/db/db-indexing.js +200 -0
  25. package/dist/adapters/db/db-oauth.d.ts +3 -0
  26. package/dist/adapters/db/db-oauth.js +26 -0
  27. package/dist/adapters/db/db-prompts.d.ts +46 -0
  28. package/dist/adapters/db/db-prompts.js +144 -0
  29. package/dist/adapters/db/db.d.ts +6 -0
  30. package/dist/adapters/db/db.js +1687 -0
  31. package/dist/adapters/db/db.types.d.ts +16 -0
  32. package/dist/adapters/db/db.types.js +1 -0
  33. package/dist/adapters/document-converter/document-converter.d.ts +2 -0
  34. package/dist/adapters/document-converter/document-converter.js +26 -0
  35. package/dist/adapters/document-converter/document-converter.types.d.ts +17 -0
  36. package/dist/adapters/document-converter/document-converter.types.js +1 -0
  37. package/dist/adapters/flow-runtime/flow-runtime.d.ts +3 -0
  38. package/dist/adapters/flow-runtime/flow-runtime.js +22 -0
  39. package/dist/adapters/flow-runtime/flow-runtime.types.d.ts +19 -0
  40. package/dist/adapters/flow-runtime/flow-runtime.types.js +1 -0
  41. package/dist/adapters/index-queue/index-queue.d.ts +10 -0
  42. package/dist/adapters/index-queue/index-queue.js +12 -0
  43. package/dist/adapters/index-queue/index-queue.types.d.ts +11 -0
  44. package/dist/adapters/index-queue/index-queue.types.js +1 -0
  45. package/dist/adapters/json-schema/json-schema.d.ts +2 -0
  46. package/dist/adapters/json-schema/json-schema.js +77 -0
  47. package/dist/adapters/openid/openid.d.ts +5 -0
  48. package/dist/adapters/openid/openid.js +301 -0
  49. package/dist/adapters/portal-tokens/portal-tokens.d.ts +8 -0
  50. package/dist/adapters/portal-tokens/portal-tokens.js +79 -0
  51. package/dist/adapters/remote-tools/remote-tools.d.ts +5 -0
  52. package/dist/adapters/remote-tools/remote-tools.js +129 -0
  53. package/dist/adapters/semantic-index/semantic-index.d.ts +2 -0
  54. package/dist/adapters/semantic-index/semantic-index.js +139 -0
  55. package/dist/adapters/semantic-index/semantic-index.types.d.ts +56 -0
  56. package/dist/adapters/semantic-index/semantic-index.types.js +1 -0
  57. package/dist/adapters/session-cookie/session-cookie.d.ts +5 -0
  58. package/dist/adapters/session-cookie/session-cookie.js +87 -0
  59. package/dist/adapters/tool-source-policy/tool-source-policy.d.ts +1 -0
  60. package/dist/adapters/tool-source-policy/tool-source-policy.js +22 -0
  61. package/dist/audit/audit.d.ts +2 -0
  62. package/dist/audit/audit.js +87 -0
  63. package/dist/audit/audit.types.d.ts +23 -0
  64. package/dist/audit/audit.types.js +1 -0
  65. package/dist/auth/auth.d.ts +2 -0
  66. package/dist/auth/auth.js +284 -0
  67. package/dist/auth/auth.types.d.ts +108 -0
  68. package/dist/auth/auth.types.js +1 -0
  69. package/dist/boards/boards.d.ts +44 -0
  70. package/dist/boards/boards.js +447 -0
  71. package/dist/boards/boards.types.d.ts +117 -0
  72. package/dist/boards/boards.types.js +1 -0
  73. package/dist/build/build.d.ts +2 -0
  74. package/dist/build/build.js +192 -0
  75. package/dist/build/build.types.d.ts +17 -0
  76. package/dist/build/build.types.js +1 -0
  77. package/dist/bundle/bundle.d.ts +4 -0
  78. package/dist/bundle/bundle.js +1194 -0
  79. package/dist/bundle/bundle.types.d.ts +33 -0
  80. package/dist/bundle/bundle.types.js +1 -0
  81. package/dist/cli/cli.d.ts +4 -0
  82. package/dist/cli/cli.js +262 -0
  83. package/dist/cli/cli.types.d.ts +14 -0
  84. package/dist/cli/cli.types.js +1 -0
  85. package/dist/contract/audit.d.ts +50 -0
  86. package/dist/contract/audit.js +72 -0
  87. package/dist/contract/board.d.ts +150 -0
  88. package/dist/contract/board.js +262 -0
  89. package/dist/contract/bundle.d.ts +147 -0
  90. package/dist/contract/bundle.js +139 -0
  91. package/dist/contract/contract.d.ts +112 -0
  92. package/dist/contract/contract.js +143 -0
  93. package/dist/contract/feed.d.ts +126 -0
  94. package/dist/contract/feed.js +149 -0
  95. package/dist/contract/flow-run.d.ts +346 -0
  96. package/dist/contract/flow-run.js +194 -0
  97. package/dist/contract/flow.d.ts +1057 -0
  98. package/dist/contract/flow.js +552 -0
  99. package/dist/contract/node.d.ts +551 -0
  100. package/dist/contract/node.js +456 -0
  101. package/dist/contract/share.d.ts +216 -0
  102. package/dist/contract/share.js +134 -0
  103. package/dist/contract/table.d.ts +171 -0
  104. package/dist/contract/table.js +117 -0
  105. package/dist/contract/tool.d.ts +122 -0
  106. package/dist/contract/tool.js +172 -0
  107. package/dist/feed/feed.d.ts +2 -0
  108. package/dist/feed/feed.js +77 -0
  109. package/dist/feed/feed.types.d.ts +23 -0
  110. package/dist/feed/feed.types.js +1 -0
  111. package/dist/flows/flows.d.ts +41 -0
  112. package/dist/flows/flows.js +2216 -0
  113. package/dist/flows/flows.types.d.ts +339 -0
  114. package/dist/flows/flows.types.js +1 -0
  115. package/dist/http/http.d.ts +9 -0
  116. package/dist/http/http.js +874 -0
  117. package/dist/http/http.types.d.ts +22 -0
  118. package/dist/http/http.types.js +1 -0
  119. package/dist/indexing/indexing.d.ts +6 -0
  120. package/dist/indexing/indexing.js +239 -0
  121. package/dist/indexing/indexing.types.d.ts +11 -0
  122. package/dist/indexing/indexing.types.js +1 -0
  123. package/dist/intel/intel.d.ts +3 -0
  124. package/dist/intel/intel.js +194 -0
  125. package/dist/intel/intel.types.d.ts +24 -0
  126. package/dist/intel/intel.types.js +1 -0
  127. package/dist/mcp/mcp.d.ts +2 -0
  128. package/dist/mcp/mcp.js +1292 -0
  129. package/dist/mcp/mcp.types.d.ts +25 -0
  130. package/dist/mcp/mcp.types.js +1 -0
  131. package/dist/nodes/document-links/document-links.d.ts +20 -0
  132. package/dist/nodes/document-links/document-links.js +57 -0
  133. package/dist/nodes/nodes.d.ts +2 -0
  134. package/dist/nodes/nodes.js +1667 -0
  135. package/dist/nodes/nodes.types.d.ts +511 -0
  136. package/dist/nodes/nodes.types.js +1 -0
  137. package/dist/prepare/prepare.d.ts +4 -0
  138. package/dist/prepare/prepare.js +16 -0
  139. package/dist/prepare/prepare.types.d.ts +9 -0
  140. package/dist/prepare/prepare.types.js +1 -0
  141. package/dist/prompts/prompts.d.ts +2 -0
  142. package/dist/prompts/prompts.js +65 -0
  143. package/dist/prompts/prompts.types.d.ts +71 -0
  144. package/dist/prompts/prompts.types.js +1 -0
  145. package/dist/shared/callers/callers.d.ts +28 -0
  146. package/dist/shared/callers/callers.js +30 -0
  147. package/dist/shared/csv/csv.d.ts +13 -0
  148. package/dist/shared/csv/csv.js +85 -0
  149. package/dist/shared/document-text/document-text.d.ts +21 -0
  150. package/dist/shared/document-text/document-text.js +31 -0
  151. package/dist/shared/gate-authorization/gate-authorization.d.ts +5 -0
  152. package/dist/shared/gate-authorization/gate-authorization.js +26 -0
  153. package/dist/shared/grant-expiry/grant-expiry.d.ts +19 -0
  154. package/dist/shared/grant-expiry/grant-expiry.js +26 -0
  155. package/dist/shared/intel-error/intel-error.d.ts +22 -0
  156. package/dist/shared/intel-error/intel-error.js +27 -0
  157. package/dist/shared/plain-title/plain-title.d.ts +16 -0
  158. package/dist/shared/plain-title/plain-title.js +45 -0
  159. package/dist/shared/problem-details/problem-details.d.ts +30 -0
  160. package/dist/shared/problem-details/problem-details.js +18 -0
  161. package/dist/shared/report-unexpected-error/report-unexpected-error.d.ts +1 -0
  162. package/dist/shared/report-unexpected-error/report-unexpected-error.js +37 -0
  163. package/dist/shared/safe-return-path/safe-return-path.d.ts +2 -0
  164. package/dist/shared/safe-return-path/safe-return-path.js +11 -0
  165. package/dist/shared/sha256/sha256.d.ts +2 -0
  166. package/dist/shared/sha256/sha256.js +14 -0
  167. package/dist/tools/tool-servers/tool-servers.d.ts +46 -0
  168. package/dist/tools/tool-servers/tool-servers.js +177 -0
  169. package/dist/tools/tools.d.ts +2 -0
  170. package/dist/tools/tools.js +207 -0
  171. package/dist/tools/tools.types.d.ts +56 -0
  172. package/dist/tools/tools.types.js +1 -0
  173. package/examples/branding/favicon.svg +5 -0
  174. package/examples/branding/logo.svg +5 -0
  175. package/examples/branding/theme.css +16 -0
  176. package/examples/dev.vars.example +7 -0
  177. package/examples/intel.json +9 -0
  178. package/migrations/0000_intel_foundation.sql +203 -0
  179. package/migrations/0001_portal_native_tools.sql +21 -0
  180. package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
  181. package/migrations/0003_folder_permissions.sql +211 -0
  182. package/migrations/0004_subflow_runs.sql +14 -0
  183. package/migrations/0005_flow_node_cleanup.sql +28 -0
  184. package/migrations/0005_tables_in_the_knowledge_tree.sql +68 -0
  185. package/migrations/0006_links_are_written_in_the_text.sql +20 -0
  186. package/migrations/0007_no_node_waits.sql +69 -0
  187. package/migrations/0008_three_layers.sql +130 -0
  188. package/migrations/0009_no_context_policy.sql +48 -0
  189. package/migrations/0010_no_run_waits_a_year.sql +27 -0
  190. package/migrations/0011_one_name_for_the_tree.sql +53 -0
  191. package/migrations/0012_table_snapshots.sql +29 -0
  192. package/migrations/0017_a_vector_per_card.sql +38 -0
  193. package/migrations/0018_no_context_policy_at_last.sql +97 -0
  194. package/migrations/0019_one_name_for_the_grants.sql +52 -0
  195. package/migrations/0020_cascade_purge_replay.sql +11 -0
  196. package/migrations/0021_a_flow_carries_its_own_grant.sql +44 -0
  197. package/migrations/0022_a_cursor_over_the_journal.sql +14 -0
  198. package/migrations/0023_a_board_and_its_tasks.sql +116 -0
  199. package/migrations/0024_the_archive_is_a_column.sql +33 -0
  200. package/migrations/0025_archiving_takes_the_subtree.sql +29 -0
  201. package/migrations/0026_one_feed_over_two_kinds.sql +23 -0
  202. package/migrations/0027_the_runs_of_every_flow.sql +27 -0
  203. package/migrations/0028_a_prompt_name_over_two_kinds.sql +70 -0
  204. package/package.json +118 -0
  205. package/src/contract/CLAUDE.md +152 -0
  206. package/src/contract/README.md +102 -0
  207. package/src/contract/audit.ts +85 -0
  208. package/src/contract/board.ts +306 -0
  209. package/src/contract/bundle.ts +157 -0
  210. package/src/contract/contract.ts +161 -0
  211. package/src/contract/feed.ts +165 -0
  212. package/src/contract/flow-run.ts +241 -0
  213. package/src/contract/flow.ts +675 -0
  214. package/src/contract/node.ts +583 -0
  215. package/src/contract/share.ts +196 -0
  216. package/src/contract/table.ts +178 -0
  217. package/src/contract/tool.ts +202 -0
  218. package/ui/README.md +110 -0
  219. package/ui/components.json +21 -0
  220. package/ui/index.html +13 -0
  221. package/ui/package.json +11 -0
  222. package/ui/scripts/lint-tokens.mjs +187 -0
  223. package/ui/src/access-summary/access-summary.tsx +385 -0
  224. package/ui/src/app/action-slot/action-slot.tsx +27 -0
  225. package/ui/src/app/app-sidebar/app-sidebar.tsx +71 -0
  226. package/ui/src/app/app-tree/app-tree.tsx +1175 -0
  227. package/ui/src/app/app.tsx +87 -0
  228. package/ui/src/app/header-search/header-search.tsx +294 -0
  229. package/ui/src/app/reindex-dialog/reindex-dialog.tsx +79 -0
  230. package/ui/src/app/settings-dialog/settings-dialog.tsx +135 -0
  231. package/ui/src/app/sidebar-preferences/sidebar-preferences.ts +68 -0
  232. package/ui/src/app/sidebar-preferences/sidebar-preferences.types.ts +15 -0
  233. package/ui/src/app/sidebar-resize-handle/sidebar-resize-handle.tsx +86 -0
  234. package/ui/src/app/tree-expansion/tree-expansion.ts +107 -0
  235. package/ui/src/app/tree-expansion/tree-expansion.types.ts +31 -0
  236. package/ui/src/app/tree-move/tree-move.tsx +427 -0
  237. package/ui/src/app/tree-reveal/tree-reveal.ts +64 -0
  238. package/ui/src/app/tree-reveal/tree-reveal.types.ts +16 -0
  239. package/ui/src/app/user-footer/user-footer.tsx +188 -0
  240. package/ui/src/app/view-menu/view-menu.tsx +115 -0
  241. package/ui/src/app-root/app-root.tsx +109 -0
  242. package/ui/src/app-root/app-root.types.ts +27 -0
  243. package/ui/src/archive/archive.tsx +417 -0
  244. package/ui/src/attachment-detail/attachment-detail.ts +52 -0
  245. package/ui/src/attachment-viewer/attachment-viewer.tsx +166 -0
  246. package/ui/src/blocknote-view/blocknote-view.tsx +30 -0
  247. package/ui/src/board/board-assignee/board-assignee-picker.tsx +132 -0
  248. package/ui/src/board/board-assignee/board-assignee.ts +48 -0
  249. package/ui/src/board/board-assignee/board-assignee.tsx +75 -0
  250. package/ui/src/board/board-chip/board-chip.tsx +46 -0
  251. package/ui/src/board/board-crumbs/board-crumbs.ts +56 -0
  252. package/ui/src/board/board-crumbs/board-crumbs.tsx +111 -0
  253. package/ui/src/board/board-data/board-data.ts +195 -0
  254. package/ui/src/board/board-data/board-data.types.ts +38 -0
  255. package/ui/src/board/board-dates/board-dates.ts +40 -0
  256. package/ui/src/board/board-dates/board-dates.tsx +105 -0
  257. package/ui/src/board/board-kanban/board-kanban.ts +162 -0
  258. package/ui/src/board/board-kanban/board-kanban.tsx +680 -0
  259. package/ui/src/board/board-open-task/board-open-task.ts +32 -0
  260. package/ui/src/board/board-panel/board-panel.tsx +259 -0
  261. package/ui/src/board/board-settings/board-settings.tsx +226 -0
  262. package/ui/src/board/board-status/board-status.tsx +60 -0
  263. package/ui/src/board/board-stripes/board-stripes.ts +128 -0
  264. package/ui/src/board/board-table/board-table.ts +53 -0
  265. package/ui/src/board/board-table/board-table.tsx +610 -0
  266. package/ui/src/board/board-task/board-task.ts +108 -0
  267. package/ui/src/board/board-task/board-task.tsx +335 -0
  268. package/ui/src/board/board-title-row/board-title-row.tsx +66 -0
  269. package/ui/src/branding/branding.tsx +17 -0
  270. package/ui/src/branding/custom-logo.ts +1 -0
  271. package/ui/src/branding/favicon.default.svg +5 -0
  272. package/ui/src/branding/favicon.svg +5 -0
  273. package/ui/src/components/ui/avatar.tsx +39 -0
  274. package/ui/src/components/ui/button.tsx +64 -0
  275. package/ui/src/components/ui/collapsible.tsx +20 -0
  276. package/ui/src/components/ui/command.tsx +160 -0
  277. package/ui/src/components/ui/dialog.tsx +143 -0
  278. package/ui/src/components/ui/dropdown-menu.tsx +187 -0
  279. package/ui/src/components/ui/input.tsx +21 -0
  280. package/ui/src/components/ui/popover.tsx +41 -0
  281. package/ui/src/components/ui/select.tsx +163 -0
  282. package/ui/src/components/ui/separator.tsx +26 -0
  283. package/ui/src/components/ui/sheet.tsx +136 -0
  284. package/ui/src/components/ui/sidebar.tsx +693 -0
  285. package/ui/src/components/ui/skeleton.tsx +13 -0
  286. package/ui/src/components/ui/switch.tsx +25 -0
  287. package/ui/src/components/ui/table.tsx +82 -0
  288. package/ui/src/components/ui/tabs.tsx +52 -0
  289. package/ui/src/components/ui/tooltip.tsx +51 -0
  290. package/ui/src/data/intel-data-provider/intel-data-provider.ts +685 -0
  291. package/ui/src/data/intel-data-provider/intel-data-provider.types.ts +266 -0
  292. package/ui/src/data/query-client.ts +18 -0
  293. package/ui/src/data/request-refusal/refusal-notice.tsx +36 -0
  294. package/ui/src/data/request-refusal/request-refusal.ts +51 -0
  295. package/ui/src/data/sign-in/sign-in.ts +97 -0
  296. package/ui/src/data/sign-in/sign-in.types.ts +15 -0
  297. package/ui/src/document-link/document-link.tsx +114 -0
  298. package/ui/src/document-markdown/document-markdown.ts +195 -0
  299. package/ui/src/editor-schema/editor-schema.ts +23 -0
  300. package/ui/src/entry-picker/entry-picker.tsx +238 -0
  301. package/ui/src/feed/feed.tsx +344 -0
  302. package/ui/src/file-preview/file-preview-view.tsx +496 -0
  303. package/ui/src/file-preview/file-preview.tsx +28 -0
  304. package/ui/src/file-preview/pdf-file-preview.tsx +5 -0
  305. package/ui/src/file-preview/presentation-file-preview.tsx +21 -0
  306. package/ui/src/file-preview/spreadsheet-file-preview.tsx +5 -0
  307. package/ui/src/file-preview/word-file-preview.tsx +5 -0
  308. package/ui/src/flow-runs/flow-runs.tsx +238 -0
  309. package/ui/src/flows/flows.tsx +1646 -0
  310. package/ui/src/flows/node-icon/node-icon.ts +34 -0
  311. package/ui/src/flows/node-palette/node-palette.tsx +214 -0
  312. package/ui/src/flows/node-palette/node-palette.types.ts +15 -0
  313. package/ui/src/folder-contents/folder-contents.tsx +321 -0
  314. package/ui/src/folder-index/folder-index.ts +55 -0
  315. package/ui/src/frontmatter/frontmatter.tsx +193 -0
  316. package/ui/src/graph-pane/graph-pane.tsx +53 -0
  317. package/ui/src/hooks/use-mobile.ts +19 -0
  318. package/ui/src/i18n/custom.json +4 -0
  319. package/ui/src/i18n/de.json +609 -0
  320. package/ui/src/i18n/en.json +609 -0
  321. package/ui/src/i18n/es.json +609 -0
  322. package/ui/src/i18n/i18n-context.tsx +65 -0
  323. package/ui/src/i18n/i18n-languages/i18n-languages.ts +83 -0
  324. package/ui/src/i18n/i18n.ts +32 -0
  325. package/ui/src/i18n/i18n.types.ts +49 -0
  326. package/ui/src/kind-icon.ts +97 -0
  327. package/ui/src/lib/utils.ts +6 -0
  328. package/ui/src/main.tsx +68 -0
  329. package/ui/src/modal/modal.tsx +53 -0
  330. package/ui/src/node-details/node-details.tsx +545 -0
  331. package/ui/src/node-editor/node-editor.tsx +304 -0
  332. package/ui/src/node-graph/graph-notice.tsx +21 -0
  333. package/ui/src/node-graph/node-graph.ts +214 -0
  334. package/ui/src/node-graph/node-graph.tsx +295 -0
  335. package/ui/src/node-import/node-import.tsx +418 -0
  336. package/ui/src/node-save-error.ts +46 -0
  337. package/ui/src/node-source/node-source.tsx +128 -0
  338. package/ui/src/node-table/node-table.tsx +107 -0
  339. package/ui/src/nodes/nodes.tsx +240 -0
  340. package/ui/src/rename-resource/rename-resource.ts +80 -0
  341. package/ui/src/resolved-names/resolved-names.tsx +67 -0
  342. package/ui/src/resource-error.ts +33 -0
  343. package/ui/src/resource-menu/resource-menu.tsx +1156 -0
  344. package/ui/src/router/router-context.ts +6 -0
  345. package/ui/src/router/router.tsx +67 -0
  346. package/ui/src/router/router.types.ts +8 -0
  347. package/ui/src/router/selection-search.ts +88 -0
  348. package/ui/src/save-button/save-button.tsx +94 -0
  349. package/ui/src/section-hint/section-hint.tsx +40 -0
  350. package/ui/src/sign-in-refused/sign-in-refused.tsx +35 -0
  351. package/ui/src/styles.css +189 -0
  352. package/ui/src/table-columns/table-columns.tsx +379 -0
  353. package/ui/src/theme/custom.css +1 -0
  354. package/ui/src/theme/theme-context.tsx +89 -0
  355. package/ui/src/theme/theme.ts +70 -0
  356. package/ui/src/time/relative-time.tsx +41 -0
  357. package/ui/src/time/time-context.tsx +78 -0
  358. package/ui/src/time/time.ts +165 -0
  359. package/ui/src/title-row/title-row.tsx +185 -0
  360. package/ui/src/tools/tools.tsx +546 -0
  361. package/ui/src/user-name/user-name.ts +61 -0
  362. package/ui/src/verbatim/verbatim.tsx +68 -0
  363. package/ui/tsconfig.json +17 -0
  364. package/ui/vite.config.ts +129 -0
@@ -0,0 +1,447 @@
1
+ import { ARCHIVE_COLUMN_ID, DEFAULT_COLUMN_COLOR } from "../contract/board.js";
2
+ import { IntelError } from "../shared/intel-error/intel-error.js";
3
+ /**
4
+ * What a board with no configuration answers with. A board is usable the moment it is created, and
5
+ * nothing asks whoever made it to define columns before filing the first card.
6
+ *
7
+ * ⚠️ **This is the FALLBACK, not what a board made through the application gets.** Columns are
8
+ * shared data: two people on one board must read the same names, and the settings dialog edits one
9
+ * list. So the application writes them at creation, in the language of whoever created it (#675),
10
+ * and from then on they are data anybody can rename. This list only answers for a board nobody
11
+ * configured — one made over MCP, or one whose seeding did not land.
12
+ */
13
+ /**
14
+ * ⚠️ **The four start coloured, and the colours mean something** (Jack's decision 2026-08-22): grey
15
+ * for what is only lying there, yellow for what is ready, orange for what is being worked on, green
16
+ * for what is done. That order is the one people already read without a legend, which is the whole
17
+ * reason a default is worth having — a board that starts grey teaches nothing, and one that starts
18
+ * in arbitrary colours teaches something false.
19
+ *
20
+ * They are a suggestion, not a setting: every one of them can be overwritten, and a board that has
21
+ * been recoloured keeps its own values.
22
+ */
23
+ export const DEFAULT_COLUMNS = [
24
+ { id: "backlog", title: "Backlog", terminal: false, color: DEFAULT_COLUMN_COLOR },
25
+ { id: "todo", title: "Offen", terminal: false, color: "#eab308" },
26
+ { id: "doing", title: "Läuft", terminal: false, color: "#f97316" },
27
+ { id: "done", title: "Fertig", terminal: true, color: "#22c55e" },
28
+ ];
29
+ // The column a card lands in when nobody said and the board has none configured. One export rather
30
+ // than the literal `"todo"` in three places: the fallback exists FOR the unconfigured board, so a
31
+ // copy of it would drift exactly where it is the only thing deciding.
32
+ export const FIRST_DEFAULT_COLUMN = DEFAULT_COLUMNS[0].id;
33
+ // Below this a query answers empty rather than with everybody. A single letter is not a search, it
34
+ // is a listing under a different name — the same floor gate holds at its own door, stated here so
35
+ // this surface keeps its own promise (#700).
36
+ const MINIMUM_QUERY = 2;
37
+ // How many people a picker offers before anybody types. Three is a suggestion, not a directory: a
38
+ // longer list is one somebody reads instead of typing the name they already know (#757).
39
+ const SUGGESTIONS = 3;
40
+ /**
41
+ * Whether one person from the directory reaches this board (#700, #757).
42
+ *
43
+ * ⚠️ **Two ways in, and the second one cost a release to notice.** A grant names either a user id or
44
+ * an EMAIL, and both are real access. Counting only ids showed nobody on a board shared by address,
45
+ * which is how four of the nine grants on the reference installation are written.
46
+ *
47
+ * The address is compared lowercased on both sides: a grant written `Anton@…` and a directory hit
48
+ * that says `anton@…` are the same person, and a case-sensitive compare would quietly disagree.
49
+ */
50
+ function reachedBy(access, person) {
51
+ if (access.ownerIds.includes(person.id))
52
+ return true;
53
+ if (access.principalIds.includes(person.id))
54
+ return true;
55
+ return access.principalEmails.includes(person.email.toLowerCase());
56
+ }
57
+ export function createBoards(deps) {
58
+ /**
59
+ * ⚠️ The one place "which board, and may this actor DO THIS to it" is answered — and the verb is
60
+ * the whole question. It asked `read` for every caller once, including the three that were about
61
+ * to write: a board shared read-only could have its columns replaced and its cards moved. The
62
+ * refusal existed and asked the wrong thing, which is the harder failure to see.
63
+ *
64
+ * A `404` rather than a `403`, on purpose: "this board exists but you may not touch it" is itself
65
+ * information about somebody else's tree.
66
+ */
67
+ const columnsOrRefuse = async (actor, boardId, verb) => {
68
+ const columns = await deps.boards.columnsOf(actor, boardId, verb);
69
+ if (columns === null) {
70
+ throw new IntelError(404, "board_not_found", "No board with this id is readable for you.");
71
+ }
72
+ return columns.length > 0 ? columns : DEFAULT_COLUMNS.map((column) => column.id);
73
+ };
74
+ const viewOrRefuse = async (actor, input) => {
75
+ const view = await deps.boards.read(actor, input);
76
+ if (view === null) {
77
+ throw new IntelError(404, "board_not_found", "No board with this id is readable for you.");
78
+ }
79
+ // A board that was never configured answers with the default columns rather than none. An empty
80
+ // column list would render as a board with no columns at all — a screen with nowhere to put a
81
+ // card, which looks broken rather than new.
82
+ /**
83
+ * ⚠️ **A stored column may not claim the archive id, and the migration is not the only guard.**
84
+ * `0024` renames the ones that existed before the id was reserved, and `update` refuses new
85
+ * ones — but a row written straight into D1, or a migration that did not reach an installation,
86
+ * would still put two columns with one id in front of a reader. Dropping it here means the
87
+ * derived one always wins, which is the answer that cannot produce two.
88
+ */
89
+ const configured = (view.columns.length > 0 ? view.columns : [...DEFAULT_COLUMNS]).filter((column) => column.id !== ARCHIVE_COLUMN_ID);
90
+ /**
91
+ * ⚠️ **The archive column is appended here, after the fallback, and it is never stored.** A
92
+ * card is in it because `nodes.archived_at` is set; `board_tasks.status` keeps naming the
93
+ * working column underneath, which is what a card pulled back out returns to (D68).
94
+ *
95
+ * Always last, and not part of the list `board_update` writes: the archive is the same place on
96
+ * every board. A shelf that could be dragged between the working columns would be a shelf
97
+ * pretending to be a stage.
98
+ *
99
+ * ⚠️ **The title here is a fallback, not the label a reader sees.** Configured columns are
100
+ * shared data and are therefore written once in the creator's language (#675); this one is
101
+ * derived and identical on every board, so the browser says it in the reader's own words
102
+ * (`useBoard`). What travels is English, for everything without a catalog — MCP, an export, a
103
+ * report.
104
+ */
105
+ return {
106
+ ...view,
107
+ columns: view.archiveVisible
108
+ ? [...configured, { id: ARCHIVE_COLUMN_ID, title: "Archive", terminal: true }]
109
+ : configured,
110
+ };
111
+ };
112
+ const requireColumn = (columns, status) => {
113
+ if (columns.includes(status))
114
+ return;
115
+ throw new IntelError(400, "unknown_column", `This board has no column \`${status}\`. Its columns are: ${columns.join(", ")}.`);
116
+ };
117
+ /**
118
+ * The people this board can be handed to, without a search term (#757).
119
+ *
120
+ * ⚠️ **Named, not enumerated.** Gate's directory answers searches, not listings, so what can be
121
+ * offered here is exactly the set this repository already knows by name: the owners, the user
122
+ * grants, and the addresses of the email grants. An organization-wide board therefore suggests
123
+ * whatever explicit grants it also has and nothing more — "everybody" is not a list anyone can
124
+ * produce, and pretending otherwise would mean asking gate to hand over its directory.
125
+ *
126
+ * ⚠️ **The cap is visible.** `more` says how many are left over, so the picker can say "and 4
127
+ * more" instead of quietly showing three and looking complete.
128
+ */
129
+ const suggest = async (boardId) => {
130
+ if (!deps.directory)
131
+ return { items: [], more: 0 };
132
+ const access = await deps.boards.effectiveAccess(boardId);
133
+ const ids = [...new Set([...access.ownerIds, ...access.principalIds])];
134
+ // One call for every id at once. The addresses need one lookup each, because gate resolves ids
135
+ // and searches text and there is no third door — so only as many as it takes to FILL the cap.
136
+ const byId = ids.length === 0 ? [] : await deps.directory.resolve(ids);
137
+ const knownEmails = new Set(byId.map((person) => person.email.toLowerCase()));
138
+ /**
139
+ * ⚠️ **The count and the list are two different things, and the count comes FIRST.**
140
+ *
141
+ * An earlier version stopped looking after the cap and then reported the leftovers it happened
142
+ * to have — so a board with ten address grants said "and 1 more" instead of "and 7 more". The
143
+ * cap was visible and its number was wrong, which is worse than no number: it is a number
144
+ * somebody believes.
145
+ *
146
+ * Counted here without naming anybody: every address that is not already one of the resolved
147
+ * people is one more person who reaches this board. Whether gate can name them does not change
148
+ * how many there are.
149
+ */
150
+ const unnamedAddresses = access.principalEmails.filter((address) => !knownEmails.has(address));
151
+ const total = byId.length + unnamedAddresses.length;
152
+ /**
153
+ * ⚠️ **The cap counts LOOKUPS, not names found**, and the difference is a real hole. An address
154
+ * gate cannot name never grows `named`, so a bound on the names would never trigger: a board
155
+ * with a hundred address grants and no matching accounts would fire a hundred sequential calls
156
+ * to gate to open one menu. The test that sets six unnameable addresses walks exactly that path.
157
+ */
158
+ const named = [...byId];
159
+ let lookups = 0;
160
+ for (const address of unnamedAddresses) {
161
+ if (named.length >= SUGGESTIONS || lookups >= SUGGESTIONS)
162
+ break;
163
+ lookups += 1;
164
+ const found = await deps.directory.search(address);
165
+ const exact = found.find((person) => person.email.toLowerCase() === address);
166
+ if (exact !== undefined)
167
+ named.push(exact);
168
+ }
169
+ const items = named.slice(0, SUGGESTIONS);
170
+ return { items, more: Math.max(0, total - items.length) };
171
+ };
172
+ return {
173
+ async get(actor, input) {
174
+ return await viewOrRefuse(actor, input);
175
+ },
176
+ async update(actor, input) {
177
+ await columnsOrRefuse(actor, input.boardId, "write");
178
+ // ⚠️ Duplicate ids are refused rather than deduplicated. Two columns with one id means every
179
+ // card in them lands in whichever the view draws first, and silently dropping one would
180
+ // discard a column somebody just wrote.
181
+ const ids = input.columns.map((column) => column.id);
182
+ const duplicate = ids.find((id, index) => ids.indexOf(id) !== index);
183
+ if (duplicate !== undefined) {
184
+ throw new IntelError(400, "duplicate_column", `The column id \`${duplicate}\` appears more than once.`);
185
+ }
186
+ /**
187
+ * ⚠️ **The archive id cannot be claimed by a configured column.** It is derived — a card is in
188
+ * it because `archived_at` is set — so a stored column of the same id would give a board two
189
+ * columns with one id: every card in them lands in whichever the view draws first, and one of
190
+ * the two can never receive a card at all.
191
+ */
192
+ /**
193
+ * ⚠️ **Duplicate NAMES are refused here, not only in the dialog.** An id nobody sees may
194
+ * repeat without confusing anybody; a name is what a person reads, and two columns called the
195
+ * same thing cannot be told apart on the board, in the filter, or in the settings. Checked in
196
+ * the service because an agent calling `board_update` reaches no dialog — root `CLAUDE.md`:
197
+ * validation does not vary by surface.
198
+ */
199
+ const titles = input.columns.map((column) => column.title.trim());
200
+ const duplicateTitle = titles.find((title, index) => titles.indexOf(title) !== index);
201
+ if (duplicateTitle !== undefined) {
202
+ throw new IntelError(400, "duplicate_column_name", `Two columns are called \`${duplicateTitle}\`. A name is what tells them apart.`);
203
+ }
204
+ if (ids.includes(ARCHIVE_COLUMN_ID)) {
205
+ throw new IntelError(400, "reserved_column", `\`${ARCHIVE_COLUMN_ID}\` is the archive column, which every board has. Show or hide it instead of configuring it.`);
206
+ }
207
+ await deps.boards.setColumns(input.boardId, JSON.stringify(input.columns), deps.now().toISOString(), input.archiveVisible);
208
+ return await viewOrRefuse(actor, { boardId: input.boardId, includeArchived: false });
209
+ },
210
+ async createTask(actor, input) {
211
+ const columns = await columnsOrRefuse(actor, input.boardId, "write");
212
+ // Absent means the first column — a caller who does not care where a task starts should not
213
+ // have to read the board first. A named one is checked, because a typo would otherwise create
214
+ // a card in a column no view draws.
215
+ const status = input.status ?? columns[0] ?? DEFAULT_COLUMNS[0].id;
216
+ if (input.status !== undefined)
217
+ requireColumn(columns, status);
218
+ /**
219
+ * ⚠️ **The parent's board must BE the board that was asked for**, checked here rather than
220
+ * left to the node path. `nodes.create` resolves the destination board from the parent, and
221
+ * on creation there is no previous board to compare it with — so a call naming board B with a
222
+ * parent on board A files the card on **A**, answers with the view of **B**, and the caller
223
+ * gets neither an error nor their card. The position would have been counted against B too.
224
+ */
225
+ if (input.parentTaskId !== null) {
226
+ // ⚠️ `"write"` for the same reason `attachToBoard` uses it — filing a card here is a write
227
+ // to that board. It is defence in depth rather than the only guard: `nodes.create` checks
228
+ // `write` on the parent node itself and answers 403 first, which is why a behaviour test
229
+ // cannot tell `"read"` from `"write"` here. Named so the next reader does not go looking
230
+ // for the test that proves it.
231
+ const parentRow = await deps.boards.taskRow(actor, input.parentTaskId, "write");
232
+ /**
233
+ * ⚠️ **Only the wrong BOARD is refused here.** `taskRow` answers `null` for three different
234
+ * things — the node does not exist, it is not a task, or this actor may not write it — and
235
+ * each of those already has a precise answer further in: `404 node_not_found` from
236
+ * `requireVisible`, `task_belongs_to_a_board` from `refuseWrongParent`, `403` from the
237
+ * write check. Catching them here would answer "that parent card is not on this board" for
238
+ * a card that does not exist, which claims it does.
239
+ */
240
+ if (parentRow !== null && parentRow.boardId !== input.boardId) {
241
+ throw new IntelError(409, "task_cannot_change_board", "That parent card is not on this board. A subtask belongs to the same board as the card it hangs under.");
242
+ }
243
+ }
244
+ // ⚠️ Through the node service, not a second insert. The task gets the same authorization
245
+ // check, the same audit row and the same version chain every other node gets — and that audit
246
+ // row is what `anchrd/signals` reads (#620).
247
+ const node = await deps.nodes.create(actor, {
248
+ // ⚠️ A subtask's parent is the TASK, not the board — the hierarchy is the node tree (#669).
249
+ // The node service resolves which board that lands on and files the row there, so nothing
250
+ // here has to know: naming the board as parent would flatten every subtask on creation.
251
+ parentId: input.parentTaskId ?? input.boardId,
252
+ kind: "task",
253
+ title: input.title,
254
+ description: null,
255
+ idempotencyKey: input.idempotencyKey,
256
+ });
257
+ /**
258
+ * ⚠️ **`update`, not `attach` — the row already exists by the time this line runs**, and
259
+ * getting that wrong is silent. `nodes.create` files every new task through `attachToBoard`,
260
+ * with the board's first column and no fields; a second `INSERT` here is refused by the
261
+ * primary key, `ON CONFLICT DO NOTHING` swallows the refusal, and the caller's `status`,
262
+ * assignee and dates are **discarded without an error**. The card lands in the wrong column
263
+ * and nothing anywhere says why.
264
+ *
265
+ * It was written as an insert first, and three tests caught it: a card created in `doing`
266
+ * came back in `todo`. The lesson is the shape rather than the line — one row, one writer,
267
+ * and the writer is the node path.
268
+ */
269
+ /**
270
+ * ⚠️ **The position is computed only when the card is not already in that column**, and that
271
+ * is what makes a retry safe. `deps.nodes.create` honours the idempotency key and answers with
272
+ * the SAME node on a replay; recomputing here would push the card one place further on every
273
+ * attempt — a client retrying a timeout would watch its card walk down the column, with every
274
+ * call succeeding.
275
+ *
276
+ * The other fields are rewritten with the same values, which is idempotent by construction.
277
+ */
278
+ const filed = await deps.boards.taskRow(actor, node.id, "write");
279
+ await deps.boards.updateTask(node.id, {
280
+ status,
281
+ assignee_id: input.assigneeId,
282
+ labels_json: JSON.stringify(input.labels),
283
+ start_date: input.startDate,
284
+ due_date: input.dueDate,
285
+ depends_on: input.dependsOn,
286
+ ...(filed?.status === status
287
+ ? {}
288
+ : { position: await deps.boards.nextPosition(input.boardId, status) }),
289
+ }, deps.now().toISOString());
290
+ return await viewOrRefuse(actor, { boardId: input.boardId, includeArchived: false });
291
+ },
292
+ async updateTask(actor, input) {
293
+ const row = await deps.boards.taskRow(actor, input.taskId, "write");
294
+ if (row === null) {
295
+ throw new IntelError(404, "task_not_found", "No task with this id is readable for you.");
296
+ }
297
+ /**
298
+ * ⚠️ **Dropping a card on the archive column archives it; pulling it out restores it** (D68).
299
+ * Neither writes `board_tasks.status`: the working column underneath is what a restored card
300
+ * returns to, and overwriting it with `archived` would be the second stored truth this
301
+ * design exists against.
302
+ *
303
+ * `nodes.archive` is the same door `node_archive` uses — one path, one audit row, one set of
304
+ * refusals. ⚠️ Since D71 (#733) that door CASCADES: dropping a card with subtasks on the
305
+ * archive column takes them with it, where #669 used to refuse the drop. Pulling the card back
306
+ * out brings exactly those back, and pulling a SUBTASK out while its parent card is archived
307
+ * is refused instead — a live card under an archived one is what the purge reaches.
308
+ */
309
+ const intoArchive = input.status === ARCHIVE_COLUMN_ID;
310
+ const outOfArchive = input.status !== undefined && !intoArchive && row.archivedAt !== null;
311
+ if (intoArchive || outOfArchive) {
312
+ await deps.nodes.archive(actor, {
313
+ nodeId: input.taskId,
314
+ baseUpdatedAt: row.updatedAt,
315
+ archived: intoArchive,
316
+ idempotencyKey: `${input.idempotencyKey}:archive`,
317
+ });
318
+ }
319
+ if (input.status !== undefined && !intoArchive) {
320
+ requireColumn(await columnsOrRefuse(actor, row.boardId, "write"), input.status);
321
+ }
322
+ // Only what arrived. An absent field means "leave it"; `null` where the column is nullable
323
+ // means "clear it", which is why the check is against `undefined` and not against falsiness —
324
+ // `dueDate: null` and `position: 0` both have to get through.
325
+ const fields = {};
326
+ if (input.title !== undefined) {
327
+ // ⚠️ The title lives on the NODE, not here. `board_tasks` has no title column, and adding
328
+ // one would make two rows disagree about what a card is called.
329
+ throw new IntelError(400, "title_belongs_to_the_node", "Rename a task with `node_update`; the board row carries no title.");
330
+ }
331
+ // ⚠️ Not for a drop into the archive: the working column stays as it is, so the card knows
332
+ // where to go back to.
333
+ if (input.status !== undefined && !intoArchive)
334
+ fields.status = input.status;
335
+ // ⚠️ Not for a drop into the archive either. The kanban sends a position with EVERY drop,
336
+ // computed from the neighbours inside the column it was dropped on — and inside the archive
337
+ // those are cards from all over the board. Taken, it would overwrite the card's place in the
338
+ // working column with a number that means nothing there. The status is guarded one line up;
339
+ // without this line the guard only covered half the write.
340
+ if (input.position !== undefined && !intoArchive)
341
+ fields.position = input.position;
342
+ if (input.assigneeId !== undefined)
343
+ fields.assignee_id = input.assigneeId;
344
+ if (input.labels !== undefined)
345
+ fields.labels_json = JSON.stringify(input.labels);
346
+ if (input.startDate !== undefined)
347
+ fields.start_date = input.startDate;
348
+ if (input.dueDate !== undefined)
349
+ fields.due_date = input.dueDate;
350
+ if (input.dependsOn !== undefined)
351
+ fields.depends_on = input.dependsOn;
352
+ /**
353
+ * A move that names a new column but no position lands at the end of it — otherwise the card
354
+ * would keep the number it had in its old column and appear at an arbitrary place.
355
+ *
356
+ * ⚠️ **Only when the column actually CHANGES.** Asking again for the column a card is already
357
+ * in is what a retry looks like, and recomputing there would move the card one place further
358
+ * on every attempt while every call reported success. That is the idempotency the tool
359
+ * promises with `idempotentHint: true`, kept by construction rather than by a key lookup.
360
+ */
361
+ /**
362
+ * ⚠️ **Never for a drop into the archive**, and the reason is the same one that keeps
363
+ * `fields.status` unset there: `board_tasks.status` never holds `archived`, so
364
+ * `nextPosition(board, "archived")` counts zero rows and answers `1` — every time. The card
365
+ * keeps its working column but silently loses its place IN it, landing first among cards it
366
+ * used to sit behind. Nothing reports that, and the reader only sees it after restoring.
367
+ */
368
+ if (input.status !== undefined &&
369
+ input.position === undefined &&
370
+ row.status !== input.status &&
371
+ !intoArchive) {
372
+ fields.position = await deps.boards.nextPosition(row.boardId, input.status);
373
+ }
374
+ if (Object.keys(fields).length > 0) {
375
+ await deps.boards.updateTask(input.taskId, fields, deps.now().toISOString());
376
+ }
377
+ return await viewOrRefuse(actor, { boardId: row.boardId, includeArchived: false });
378
+ },
379
+ /**
380
+ * Who this card may be given to (D70, #700).
381
+ *
382
+ * ⚠️ THE ANSWER IS NARROWER THAN GATE'S, NEVER WIDER. gate is asked first and its hits are then
383
+ * dropped down to the people who can actually open this board. That order is what makes the
384
+ * door safe to offer to anyone who can read the board: whatever survives the filter, gate would
385
+ * have handed the same person anyway at its own `POST /directory/search`.
386
+ *
387
+ * The reverse order would have been the leak: asking the board first and then naming the
388
+ * principals would hand out a membership list for a board to anyone who can see it.
389
+ */
390
+ async searchAssignees(actor, input) {
391
+ // `read`, not `share`: offering an assignee is something everyone who can open the board
392
+ // does, while managing grants is not. The 404 from here is the same one every other board
393
+ // read gives, so a board one may not see stays indistinguishable from one that is not there.
394
+ await columnsOrRefuse(actor, input.boardId, "read");
395
+ if (!deps.directory)
396
+ return { items: [], more: 0 };
397
+ /**
398
+ * ⚠️ ENFORCED HERE, not only described. The contract's `query` says a single character
399
+ * answers empty, and until this line that sentence was true only because gate happens to
400
+ * hold the same floor — a promise this repository made and a different service kept.
401
+ *
402
+ * It is also the cheaper answer: a letter that can only ever produce a listing does not
403
+ * need to travel to gate first to be refused there.
404
+ */
405
+ const query = input.query.trim();
406
+ /**
407
+ * ⚠️ **Nothing typed is a QUESTION, not a refusal** (#757). The set of people who reach this
408
+ * board is short and known, so it can be named without a search term — and a picker that
409
+ * opens empty teaches the reader that there is nobody, which is the opposite of what it
410
+ * means.
411
+ *
412
+ * A single character stays refused, and the reason is unchanged: a letter is not a search, it
413
+ * is a listing under a different name. The two cases differ because one names a bounded set
414
+ * and the other asks the directory to filter one.
415
+ */
416
+ if (query.length === 0)
417
+ return await suggest(input.boardId);
418
+ if (query.length < MINIMUM_QUERY)
419
+ return { items: [], more: 0 };
420
+ const hits = await deps.directory.search(query);
421
+ if (hits.length === 0)
422
+ return { items: [], more: 0 };
423
+ const access = await deps.boards.effectiveAccess(input.boardId);
424
+ // A grant to the organization means everybody reaches the board, so there is nothing left to
425
+ // narrow — and narrowing anyway would produce an empty picker on exactly the boards that are
426
+ // shared with everyone, which is most of them.
427
+ if (access.organizationWide)
428
+ return { items: hits, more: 0 };
429
+ return { items: hits.filter((hit) => reachedBy(access, hit)), more: 0 };
430
+ },
431
+ /**
432
+ * What the people already recorded on cards are called (#258, #700).
433
+ *
434
+ * ⚠️ NOT narrowed by the board, and that is the difference from the search above. Someone whose
435
+ * grant was withdrawn, or whose account was switched off, is still the person this card was
436
+ * given to. Filtering them out would leave the card reading as unassigned, which is a worse
437
+ * answer than the truth — and it would not protect anything, because the caller already holds
438
+ * the id and got it from a card they may read.
439
+ */
440
+ async resolveNames(actor, input) {
441
+ void actor;
442
+ if (!deps.directory)
443
+ return { items: [] };
444
+ return { items: await deps.directory.resolve(input.ids) };
445
+ },
446
+ };
447
+ }
@@ -0,0 +1,117 @@
1
+ import type { BoardAssigneeSearchInput, BoardGetInput, BoardTaskCreateInput, BoardTaskUpdateInput, BoardUpdateInput, BoardView, DirectoryPerson, DirectoryResolveInput } from "../contract/board.js";
2
+ import type { Node } from "../contract/node.js";
3
+ import type { Actor } from "../nodes/nodes.types.js";
4
+ export type { BoardView };
5
+ export interface BoardTaskRow {
6
+ nodeId: string;
7
+ boardId: string;
8
+ status: string;
9
+ position: number;
10
+ /**
11
+ * From the NODE, not from `board_tasks` (D68, #674).
12
+ *
13
+ * ⚠️ Whether a card is archived is `nodes.archived_at` and nothing else. The row carries it so
14
+ * that a move can tell "into the archive" from "out of it" without a second read — and so that
15
+ * nobody is tempted to store the answer here, which would be the two truths D68 exists against.
16
+ */
17
+ archivedAt: string | null;
18
+ updatedAt: string;
19
+ }
20
+ export interface BoardTaskWrite {
21
+ nodeId: string;
22
+ boardId: string;
23
+ status: string;
24
+ assigneeId: string | null;
25
+ labels: string[];
26
+ startDate: string | null;
27
+ dueDate: string | null;
28
+ dependsOn: string | null;
29
+ position: number;
30
+ occurredAt: string;
31
+ }
32
+ export interface BoardRepository {
33
+ read(actor: Actor, input: BoardGetInput): Promise<BoardView | null>;
34
+ columnsOf(actor: Actor, boardId: string, verb: "read" | "write"): Promise<string[] | null>;
35
+ /**
36
+ * ⚠️ `archiveVisible` is `undefined` for "leave it as it is" — the dialog that writes the columns
37
+ * and the switch that hides the archive are the same call, and a caller who only reorders columns
38
+ * must not silently un-hide an archive somebody put away.
39
+ */
40
+ setColumns(boardId: string, columnsJson: string, occurredAt: string, archiveVisible?: boolean): Promise<void>;
41
+ attach(write: BoardTaskWrite): Promise<void>;
42
+ taskRow(actor: Actor, taskId: string, verb: "read" | "write"): Promise<BoardTaskRow | null>;
43
+ updateTask(taskId: string, fields: Record<string, string | number | null>, occurredAt: string): Promise<void>;
44
+ nextPosition(boardId: string, status: string): Promise<number>;
45
+ /**
46
+ * Who reaches this node once inheritance is resolved: every grant along the folders above plus
47
+ * the owners. The same answer `listEffectiveAccess` gives, reached through the SAME repository
48
+ * method, so the two can never disagree about who has access.
49
+ *
50
+ * ⚠️ Read here WITHOUT the `share` permission the application-level `listEffectiveAccess`
51
+ * demands, and that is deliberate rather than an oversight. The two callers ask for different
52
+ * reasons: managing sharing is an owner's job, while naming the people a card may be given to is
53
+ * something anyone who can open the board has to be able to do. The answer never leaves this
54
+ * service as a list of grants; it is only ever used to NARROW what gate already offers.
55
+ */
56
+ effectiveAccess(boardId: string): Promise<{
57
+ ownerIds: string[];
58
+ principalIds: string[];
59
+ principalEmails: string[];
60
+ organizationWide: boolean;
61
+ }>;
62
+ }
63
+ /**
64
+ * The one seam to gate's directory, built PER REQUEST around the caller's bearer.
65
+ *
66
+ * ⚠️ The bearer never reaches this service, and it must not: `Actor` is the resolved identity, not
67
+ * the credential that produced it. The edge already holds the token because it just authorized with
68
+ * it, so it closes over it here and hands in something that answers questions instead.
69
+ */
70
+ export interface DirectoryPort {
71
+ search(query: string): Promise<DirectoryPerson[]>;
72
+ resolve(ids: string[]): Promise<DirectoryPerson[]>;
73
+ }
74
+ export interface BoardNodePort {
75
+ create(actor: Actor, input: {
76
+ parentId: string;
77
+ kind: "task";
78
+ title: string;
79
+ description: null;
80
+ idempotencyKey: string;
81
+ }): Promise<Node>;
82
+ /**
83
+ * ⚠️ The same door `node_archive` uses. Dropping a card on the archive column must not become a
84
+ * second way to archive: one path means one audit row, one set of refusals — a card with live
85
+ * subtasks stays refused (#669) — and one answer to "what does archived mean".
86
+ */
87
+ archive(actor: Actor, input: {
88
+ nodeId: string;
89
+ baseUpdatedAt: string;
90
+ archived: boolean;
91
+ idempotencyKey: string;
92
+ }): Promise<Node>;
93
+ }
94
+ export interface BoardDeps {
95
+ boards: BoardRepository;
96
+ nodes: BoardNodePort;
97
+ now(): Date;
98
+ directory?: DirectoryPort;
99
+ }
100
+ export interface BoardService {
101
+ get(actor: Actor, input: BoardGetInput): Promise<BoardView>;
102
+ update(actor: Actor, input: BoardUpdateInput): Promise<BoardView>;
103
+ createTask(actor: Actor, input: BoardTaskCreateInput): Promise<BoardView>;
104
+ updateTask(actor: Actor, input: BoardTaskUpdateInput): Promise<BoardView>;
105
+ /**
106
+ * ⚠️ `more` says how many people reach this board beyond the ones named, so a capped list can say
107
+ * so instead of looking complete (#757). It is `0` on a search answer, where the cap belongs to
108
+ * gate and not to this door.
109
+ */
110
+ searchAssignees(actor: Actor, input: BoardAssigneeSearchInput): Promise<{
111
+ items: DirectoryPerson[];
112
+ more: number;
113
+ }>;
114
+ resolveNames(actor: Actor, input: DirectoryResolveInput): Promise<{
115
+ items: DirectoryPerson[];
116
+ }>;
117
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { BuildDeps, BuildResult } from "./build.types.js";
2
+ export declare function createBuild(deps: BuildDeps): BuildResult;