kennel 0.2.3 → 0.2.5

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 (237) hide show
  1. package/README.md +23 -13
  2. package/dist/cli/dispatch.js +16 -3
  3. package/dist/cli/src/cli/dispatch.js +16 -3
  4. package/dist/cli/src/cli/usage.js +1 -1
  5. package/dist/cli/src/core/command.js +1 -0
  6. package/dist/cli/src/core/init.js +5 -5
  7. package/dist/cli/src/plugins/external.d.ts +28 -2
  8. package/dist/cli/src/plugins/external.js +75 -21
  9. package/dist/cli/src/site/server-plugins.d.ts +2 -7
  10. package/dist/cli/src/site/viewer.js +1 -0
  11. package/dist/cli/usage.js +1 -1
  12. package/dist/core/command.js +1 -0
  13. package/dist/core/init.js +5 -5
  14. package/dist/core/runtime.js +357 -3207
  15. package/dist/package/preview.js +1232 -5113
  16. package/dist/plugins/external.d.ts +28 -2
  17. package/dist/plugins/external.js +75 -21
  18. package/dist/site/server-plugins.d.ts +2 -7
  19. package/dist/site/server-plugins.js +552 -10447
  20. package/dist/site/viewer.js +1 -0
  21. package/kernel/apps/web/package.json +1 -5
  22. package/kernel/apps/web/scripts/dev.mjs +53 -0
  23. package/kernel/apps/web/src/app.css +0 -3
  24. package/kernel/apps/web/src/main.tsx +0 -1
  25. package/kernel/apps/web/src/plugins.ts +0 -30
  26. package/kernel/apps/web/tsconfig.json +1 -2
  27. package/kernel/apps/web/vite.config.ts +31 -13
  28. package/kernel/packages/api/package.json +0 -4
  29. package/kernel/packages/api/src/server/folder-metadata-plugin.ts +13 -38
  30. package/kernel/packages/api/src/server/index.ts +0 -1
  31. package/kernel/packages/api/src/server/plugins.ts +49 -16
  32. package/kernel/packages/api/test/core.test.ts +13 -25
  33. package/kernel/packages/markdown/README.md +1 -1
  34. package/package.json +1 -1
  35. package/web/assets/{abnfDiagram-VRR7QNED-Bgfb1nEB.js → abnfDiagram-VRR7QNED-xd_g1uK3.js} +1 -1
  36. package/web/assets/app-CX20nNIa.js +2 -0
  37. package/web/assets/{app-options-D-RJiQ-X.js → app-options-CkPJE5ep.js} +1 -1
  38. package/web/assets/{app-view-header-BLvLq02T.js → app-view-header-CNk5KnA-.js} +1 -1
  39. package/web/assets/{arc-Dk_KI36B.js → arc-B0u5yBJA.js} +1 -1
  40. package/web/assets/{architectureDiagram-ZJ3FMSHR-NR_KhRCf.js → architectureDiagram-ZJ3FMSHR-DfxOAo8P.js} +1 -1
  41. package/web/assets/{ask-tray-TzeTcO6-.js → ask-tray-SscJecDY.js} +1 -1
  42. package/web/assets/{blockDiagram-677ZJIJ3-CkLkwx9C.js → blockDiagram-677ZJIJ3-Ci3Usxh6.js} +1 -1
  43. package/web/assets/{c4Diagram-LMCZKHZV-CFCUH8e_.js → c4Diagram-LMCZKHZV-BqgwIVgh.js} +1 -1
  44. package/web/assets/channel-X7WHyxxT.js +1 -0
  45. package/web/assets/{chunk-2Q5K7J3B-B_BPLWsW.js → chunk-2Q5K7J3B-CfX69l2U.js} +1 -1
  46. package/web/assets/{chunk-32BRIVSS-B1VB1hOy.js → chunk-32BRIVSS-CuFlzZkl.js} +1 -1
  47. package/web/assets/{chunk-5VM5RSS4-KFcYr8Co.js → chunk-5VM5RSS4-DidCMlJa.js} +1 -1
  48. package/web/assets/{chunk-EX3LRPZG-CpBX3kuK.js → chunk-EX3LRPZG-B5CDm1dS.js} +1 -1
  49. package/web/assets/{chunk-JWPE2WC7-BPSl40wG.js → chunk-JWPE2WC7-D5fGV_sH.js} +1 -1
  50. package/web/assets/{chunk-MOJQB5TN-D2RcYmoU.js → chunk-MOJQB5TN-ayhH5Q-H.js} +1 -1
  51. package/web/assets/{chunk-RYQCIY6F-m7Gyn-t3.js → chunk-RYQCIY6F-C4BfOOZn.js} +1 -1
  52. package/web/assets/{chunk-V7JOEXUC-TDvdopxS.js → chunk-V7JOEXUC-DpUEIM9V.js} +1 -1
  53. package/web/assets/{chunk-VR4S4FIN-DZTsG51a.js → chunk-VR4S4FIN-DoQuFFRh.js} +1 -1
  54. package/web/assets/{chunk-XXDRQBXY-Dp0--RE2.js → chunk-XXDRQBXY-CI9eiLwn.js} +1 -1
  55. package/web/assets/classDiagram-OUVF2IWQ-BjsLQ3dz.js +1 -0
  56. package/web/assets/classDiagram-v2-EOCWNBFH-BjsLQ3dz.js +1 -0
  57. package/web/assets/client-7OWw5kW2.js +6 -0
  58. package/web/assets/client-DYj_td4Q.js +17 -0
  59. package/web/assets/{client-j2kQmxsO.js → client-Dk-NBvJR.js} +4 -4
  60. package/web/assets/command-zkzH4hVz.js +38 -0
  61. package/web/assets/{copy-context-C0Uqsp-3.js → copy-context-D5oiWrYC.js} +1 -1
  62. package/web/assets/{cose-bilkent-JH36ORCC-CPu3PI8h.js → cose-bilkent-JH36ORCC-BHRk1mO3.js} +1 -1
  63. package/web/assets/{cynefin-VYW2F7L2-DWxftYAm.js → cynefin-VYW2F7L2-C1B09TRE.js} +1 -1
  64. package/web/assets/{cynefinDiagram-TSTJHNR4-DsmN2V-p.js → cynefinDiagram-TSTJHNR4-B27qXQbv.js} +1 -1
  65. package/web/assets/{dagre-VKFMJZFB-COEbApyY.js → dagre-VKFMJZFB-B1qEi32g.js} +1 -1
  66. package/web/assets/{diagram-FQU43EPY-0O9WGWKB.js → diagram-FQU43EPY-BMkkgADD.js} +1 -1
  67. package/web/assets/{diagram-G47NLZAW-CutEdMVh.js → diagram-G47NLZAW-Cb8_g1A5.js} +1 -1
  68. package/web/assets/{diagram-NH7WQ7WH-C9KZc3B2.js → diagram-NH7WQ7WH-B51wuc5m.js} +1 -1
  69. package/web/assets/{diagram-OA4YK3LP-D8kkPXbY.js → diagram-OA4YK3LP-xnsecbbI.js} +1 -1
  70. package/web/assets/{diagram-WEI45ONY-_YqiTaGP.js → diagram-WEI45ONY-DSZdyaAV.js} +1 -1
  71. package/web/assets/{document-Dgt5eZFj.js → document-Dc2dftTW.js} +3 -3
  72. package/web/assets/{ebnfDiagram-CCIWWBDH-JO-zQh6g.js → ebnfDiagram-CCIWWBDH-aTHmcUGy.js} +1 -1
  73. package/web/assets/{entities-C3wz7FCx.js → entities-CCUtpskY.js} +1 -1
  74. package/web/assets/{erDiagram-Q63AITRT-9UjQwJ4P.js → erDiagram-Q63AITRT-CUUPBcvo.js} +1 -1
  75. package/web/assets/{file-viewer-DZJ8nYnl.js → file-viewer-DJ3rsmef.js} +1 -1
  76. package/web/assets/{flowDiagram-23GEKE2U-Ca08tnYM.js → flowDiagram-23GEKE2U-aTQXx2Mh.js} +1 -1
  77. package/web/assets/{folder-image-BfGgxo7i.js → folder-image-Bnv43tzP.js} +1 -1
  78. package/web/assets/{folder-landing-qUKBCnG-.js → folder-landing-DCnzYXXY.js} +1 -1
  79. package/web/assets/{folder-list-BfFjv7rR.js → folder-list-DvVkVjiR.js} +1 -1
  80. package/web/assets/{folder-metadata-DCw8xzHk.js → folder-metadata-C9hMMzIH.js} +1 -1
  81. package/web/assets/{folder-mobile-bar-Ubz4babf.js → folder-mobile-bar-VoTL1GS0.js} +1 -1
  82. package/web/assets/folder-plugin-view-Cvd804ZU.js +6 -0
  83. package/web/assets/{ganttDiagram-NO4QXBWP-KnHzX5W3.js → ganttDiagram-NO4QXBWP-DiwdUp7u.js} +1 -1
  84. package/web/assets/{gitGraphDiagram-IHSO6WYX-Cm8woNtK.js → gitGraphDiagram-IHSO6WYX-BL2gG_IS.js} +1 -1
  85. package/web/assets/{index-CNyy98F8.js → index-CUTtMSyJ.js} +2 -2
  86. package/web/assets/index-CegvVQ1C.js +1 -0
  87. package/web/assets/index-Dwhf8F_U.css +1 -0
  88. package/web/assets/{infoDiagram-FWYZ7A6U-yyqmUtIb.js → infoDiagram-FWYZ7A6U-B3n0zZpt.js} +1 -1
  89. package/web/assets/{ishikawaDiagram-FXEZZL3T-BfvR_Zy4.js → ishikawaDiagram-FXEZZL3T-D7YytbEv.js} +1 -1
  90. package/web/assets/{journeyDiagram-5HDEW3XC-BKuw06HM.js → journeyDiagram-5HDEW3XC-BYwqEl3C.js} +1 -1
  91. package/web/assets/{kanban-definition-HUTT4EX6-n0010rmQ.js → kanban-definition-HUTT4EX6-BQHNJgXZ.js} +1 -1
  92. package/web/assets/{linear-C0d0lT8O.js → linear-BPiQCLNk.js} +1 -1
  93. package/web/assets/{mermaid.core-BDVbShyO.js → mermaid.core-CD-KvBjV.js} +4 -4
  94. package/web/assets/{metadata-KV0dpp4G.js → metadata-COJ7fZul.js} +1 -1
  95. package/web/assets/{mindmap-definition-LN4V7U3C--_0ks3GF.js → mindmap-definition-LN4V7U3C-C1L_gyoj.js} +1 -1
  96. package/web/assets/{pegDiagram-2B236MQR-CBoNIpKR.js → pegDiagram-2B236MQR-BDAfItqS.js} +1 -1
  97. package/web/assets/{pieDiagram-ENE6RG2P-MA5tkYh5.js → pieDiagram-ENE6RG2P-CkZYH3Vz.js} +1 -1
  98. package/web/assets/{popover-DBD7FF-W.js → popover-0oA2lLUV.js} +2 -2
  99. package/web/assets/{project-search-dialog-DM1IkSZu.js → project-search-dialog-B2oqMT6h.js} +1 -1
  100. package/web/assets/{quadrantDiagram-ABIIQ3AL-CWT1ryqF.js → quadrantDiagram-ABIIQ3AL-Dy8Al3YZ.js} +1 -1
  101. package/web/assets/{railroadDiagram-RFXS5EU6-ZiLB5FGi.js → railroadDiagram-RFXS5EU6-Dd9_yo7B.js} +1 -1
  102. package/web/assets/{requirementDiagram-TGXJPOKE-CQq9uneq.js → requirementDiagram-TGXJPOKE-DPhIUsbs.js} +1 -1
  103. package/web/assets/{sankeyDiagram-HTMAVEWB-qDG-0JqI.js → sankeyDiagram-HTMAVEWB-B2QkYtub.js} +1 -1
  104. package/web/assets/{search-DNrQA5fF.js → search-C1J0-xiB.js} +1 -1
  105. package/web/assets/select-CtXC_PLC.js +30 -0
  106. package/web/assets/{selection-popup-DmtTeRs2.js → selection-popup-qhlUFzGq.js} +1 -1
  107. package/web/assets/{sequenceDiagram-DBY2YBRQ-CiApurLO.js → sequenceDiagram-DBY2YBRQ-Bx7fcZDx.js} +1 -1
  108. package/web/assets/{sizeCapture-X5ZJPWSS-DZSR5QXm.js → sizeCapture-X5ZJPWSS-CIXIhl_A.js} +1 -1
  109. package/web/assets/{stateDiagram-2N3HPSRC-CjJJR9_z.js → stateDiagram-2N3HPSRC-DCrdAflB.js} +1 -1
  110. package/web/assets/stateDiagram-v2-6OUMAXLB-0KCFLYeQ.js +1 -0
  111. package/web/assets/{swimlanes-5IMT3BWC-CKmo1sg6.js → swimlanes-5IMT3BWC-DA2vc7iZ.js} +2 -2
  112. package/web/assets/swimlanesDiagram-G3AALYLV-CAw3AV84.js +8 -0
  113. package/web/assets/{timeline-definition-FHXFAJF6-D-NMwS3y.js → timeline-definition-FHXFAJF6-lKfJiV9V.js} +1 -1
  114. package/web/assets/{vennDiagram-L72KCM5P-CfL6LBWw.js → vennDiagram-L72KCM5P-DO1q2KzG.js} +1 -1
  115. package/web/assets/{view-context-gyDl_p2V.js → view-context-DXKM3-l1.js} +4 -4
  116. package/web/assets/{viewer-capabilities-Bpp8Uvcg.js → viewer-capabilities-Cig_xM9u.js} +1 -1
  117. package/web/assets/{wardleyDiagram-EHGQE667-DgldMxaj.js → wardleyDiagram-EHGQE667-Dal9LkuZ.js} +1 -1
  118. package/web/assets/{xychartDiagram-FW5EYKEG-CIoAIFX8.js → xychartDiagram-FW5EYKEG-B3dPLPII.js} +1 -1
  119. package/web/index.html +2 -2
  120. package/kernel/packages/api/test/partial-loading.test.ts +0 -43
  121. package/kernel/plugins/calendar/SKILL.md +0 -173
  122. package/kernel/plugins/calendar/eslint.config.js +0 -10
  123. package/kernel/plugins/calendar/package.json +0 -73
  124. package/kernel/plugins/calendar/schema.json +0 -137
  125. package/kernel/plugins/calendar/src/calendar.tsx +0 -328
  126. package/kernel/plugins/calendar/src/client.tsx +0 -139
  127. package/kernel/plugins/calendar/src/context.ts +0 -48
  128. package/kernel/plugins/calendar/src/copy-button.tsx +0 -66
  129. package/kernel/plugins/calendar/src/event-dialog.tsx +0 -270
  130. package/kernel/plugins/calendar/src/index.ts +0 -4
  131. package/kernel/plugins/calendar/src/manifest.ts +0 -7
  132. package/kernel/plugins/calendar/src/parse.ts +0 -610
  133. package/kernel/plugins/calendar/src/server.ts +0 -160
  134. package/kernel/plugins/calendar/src/utils.ts +0 -80
  135. package/kernel/plugins/calendar/src/views.tsx +0 -418
  136. package/kernel/plugins/calendar/test/context.test.mjs +0 -60
  137. package/kernel/plugins/calendar/test/package.test.mjs +0 -43
  138. package/kernel/plugins/calendar/test/parse.test.mjs +0 -392
  139. package/kernel/plugins/calendar/test/server.test.mjs +0 -126
  140. package/kernel/plugins/calendar/tsconfig.json +0 -17
  141. package/kernel/plugins/mail/SKILL.md +0 -97
  142. package/kernel/plugins/mail/eslint.config.js +0 -10
  143. package/kernel/plugins/mail/package.json +0 -79
  144. package/kernel/plugins/mail/src/api.ts +0 -359
  145. package/kernel/plugins/mail/src/bun-ffi.d.ts +0 -14
  146. package/kernel/plugins/mail/src/client.tsx +0 -14
  147. package/kernel/plugins/mail/src/flock.ts +0 -137
  148. package/kernel/plugins/mail/src/folders.ts +0 -37
  149. package/kernel/plugins/mail/src/history.ts +0 -485
  150. package/kernel/plugins/mail/src/index.ts +0 -3
  151. package/kernel/plugins/mail/src/mail-card.tsx +0 -123
  152. package/kernel/plugins/mail/src/mail-conversation.tsx +0 -153
  153. package/kernel/plugins/mail/src/mail-detail-page.tsx +0 -467
  154. package/kernel/plugins/mail/src/mail-hooks.ts +0 -252
  155. package/kernel/plugins/mail/src/mail-page.tsx +0 -109
  156. package/kernel/plugins/mail/src/mail-request.ts +0 -78
  157. package/kernel/plugins/mail/src/mail-send-box.tsx +0 -175
  158. package/kernel/plugins/mail/src/mailos.ts +0 -723
  159. package/kernel/plugins/mail/src/manifest.ts +0 -7
  160. package/kernel/plugins/mail/src/node.ts +0 -171
  161. package/kernel/plugins/mail/src/parse.ts +0 -139
  162. package/kernel/plugins/mail/src/server.ts +0 -125
  163. package/kernel/plugins/mail/src/types.ts +0 -10
  164. package/kernel/plugins/mail/src/viewer.tsx +0 -147
  165. package/kernel/plugins/mail/test/flock.test.mjs +0 -156
  166. package/kernel/plugins/mail/test/folders.test.mjs +0 -138
  167. package/kernel/plugins/mail/test/history.test.mjs +0 -460
  168. package/kernel/plugins/mail/test/mailos.test.mjs +0 -752
  169. package/kernel/plugins/mail/test/node.test.mjs +0 -207
  170. package/kernel/plugins/mail/test/package.test.mjs +0 -54
  171. package/kernel/plugins/mail/test/parse.test.mjs +0 -32
  172. package/kernel/plugins/mail/test/server.test.mjs +0 -116
  173. package/kernel/plugins/mail/test/viewer.test.mjs +0 -192
  174. package/kernel/plugins/mail/tsconfig.json +0 -17
  175. package/kernel/plugins/projects/SKILL.md +0 -73
  176. package/kernel/plugins/projects/eslint.config.js +0 -10
  177. package/kernel/plugins/projects/package.json +0 -70
  178. package/kernel/plugins/projects/src/client.tsx +0 -17
  179. package/kernel/plugins/projects/src/data/model.ts +0 -129
  180. package/kernel/plugins/projects/src/index.ts +0 -17
  181. package/kernel/plugins/projects/src/manifest.ts +0 -7
  182. package/kernel/plugins/projects/src/project-context.ts +0 -41
  183. package/kernel/plugins/projects/src/server.ts +0 -276
  184. package/kernel/plugins/projects/src/ui/project-card.tsx +0 -61
  185. package/kernel/plugins/projects/src/ui/projects-page.tsx +0 -102
  186. package/kernel/plugins/projects/test/package.test.mjs +0 -51
  187. package/kernel/plugins/projects/tsconfig.json +0 -17
  188. package/kernel/plugins/tasks/SKILL.md +0 -341
  189. package/kernel/plugins/tasks/eslint.config.js +0 -10
  190. package/kernel/plugins/tasks/package.json +0 -74
  191. package/kernel/plugins/tasks/schema.json +0 -939
  192. package/kernel/plugins/tasks/src/api.ts +0 -212
  193. package/kernel/plugins/tasks/src/client.tsx +0 -23
  194. package/kernel/plugins/tasks/src/data/parse.ts +0 -544
  195. package/kernel/plugins/tasks/src/data/task-id.ts +0 -56
  196. package/kernel/plugins/tasks/src/data/todo-model.ts +0 -795
  197. package/kernel/plugins/tasks/src/index.ts +0 -34
  198. package/kernel/plugins/tasks/src/manifest.ts +0 -7
  199. package/kernel/plugins/tasks/src/pages/task-detail.tsx +0 -48
  200. package/kernel/plugins/tasks/src/pages/task-list.tsx +0 -88
  201. package/kernel/plugins/tasks/src/server.ts +0 -447
  202. package/kernel/plugins/tasks/src/styles.css +0 -9
  203. package/kernel/plugins/tasks/src/ui/compact-todo-list.tsx +0 -372
  204. package/kernel/plugins/tasks/src/ui/error-details.tsx +0 -78
  205. package/kernel/plugins/tasks/src/ui/task-page.tsx +0 -392
  206. package/kernel/plugins/tasks/src/ui/todo-agent-filter.tsx +0 -36
  207. package/kernel/plugins/tasks/src/ui/todo-filter-bar.tsx +0 -66
  208. package/kernel/plugins/tasks/src/ui/todo-item.tsx +0 -494
  209. package/kernel/plugins/tasks/src/ui/todo-status-filter.tsx +0 -60
  210. package/kernel/plugins/tasks/src/ui/todo-tag-filter.tsx +0 -48
  211. package/kernel/plugins/tasks/src/ui/todo-warnings.tsx +0 -106
  212. package/kernel/plugins/tasks/test/api.test.mjs +0 -99
  213. package/kernel/plugins/tasks/test/folder-sources.test.mjs +0 -46
  214. package/kernel/plugins/tasks/test/package.test.mjs +0 -54
  215. package/kernel/plugins/tasks/test/parse.test.mjs +0 -225
  216. package/kernel/plugins/tasks/test/server.test.mjs +0 -386
  217. package/kernel/plugins/tasks/tsconfig.json +0 -17
  218. package/web/assets/app-DoGr2Zi7.js +0 -2
  219. package/web/assets/channel-uMEjV8qc.js +0 -1
  220. package/web/assets/classDiagram-OUVF2IWQ-fbZocVor.js +0 -1
  221. package/web/assets/classDiagram-v2-EOCWNBFH-fbZocVor.js +0 -1
  222. package/web/assets/client-C6Nxdfif.js +0 -2
  223. package/web/assets/client-CXYfBko5.js +0 -17
  224. package/web/assets/client-CoKtOq-X.js +0 -7
  225. package/web/assets/client-DyRQz5RU.js +0 -53
  226. package/web/assets/client-OY6HUzuD.js +0 -98
  227. package/web/assets/client-YOYWZIFi.js +0 -34
  228. package/web/assets/command-BugZBGXE.js +0 -38
  229. package/web/assets/folder-plugin-view-DsWR1zmQ.js +0 -6
  230. package/web/assets/index-BaRnbYRw.js +0 -1
  231. package/web/assets/index-BexmLxIP.css +0 -1
  232. package/web/assets/index-DDirZMgx.js +0 -1
  233. package/web/assets/note-card-DBfX6gW1.js +0 -6
  234. package/web/assets/select-D8cqNgcp.js +0 -30
  235. package/web/assets/stateDiagram-v2-6OUMAXLB-BpeGSCu9.js +0 -1
  236. package/web/assets/swimlanesDiagram-G3AALYLV-D9RjLKb9.js +0 -8
  237. package/web/assets/use-folder-filter-LQyXns5Q.js +0 -1
@@ -1,544 +0,0 @@
1
- /**
2
- * Validation functions for the raw `todo.json` source document.
3
- *
4
- * This module is the tasks plugin's "validate" seam: it turns a source file
5
- * into either a typed document or a list of precise, human-readable issues
6
- * (`path` + `message`). It is deliberately dependency-free and shared by the
7
- * server entry (detection + data loading), the client error panel, and the
8
- * plugin's node tests.
9
- *
10
- * Parsing never throws: invalid JSON and structural mistakes are reported as
11
- * issues so the host and viewer can show exactly what is wrong instead of a
12
- * 404 or a generic "could not parse" message.
13
- */
14
-
15
- import type { ContentIssue } from "@raggle-ai/folder-plugin/validation"
16
- import {
17
- issueSummary,
18
- parseJsonSource,
19
- } from "@raggle-ai/folder-plugin/validation"
20
-
21
- export const TASK_TYPES = [
22
- "task",
23
- "bug",
24
- "feature",
25
- "chore",
26
- "spike",
27
- "epic",
28
- "milestone",
29
- ] as const
30
-
31
- export const TASK_STATUSES = [
32
- "todo",
33
- "in_progress",
34
- "blocked",
35
- "review",
36
- "done",
37
- "canceled",
38
- ] as const
39
-
40
- export const RELATION_TYPES = [
41
- "blocks",
42
- "blocked_by",
43
- "related_to",
44
- "duplicates",
45
- "duplicated_by",
46
- "parent_of",
47
- "child_of",
48
- ] as const
49
-
50
- export const LINK_PROVIDERS = ["github", "linear", "jira", "web"] as const
51
-
52
- export const LINK_KINDS = [
53
- "issue",
54
- "pull_request",
55
- "commit",
56
- "repository",
57
- "project",
58
- "document",
59
- "design",
60
- "discussion",
61
- "external",
62
- ] as const
63
-
64
- export const LINK_RELATIONS = [
65
- "references",
66
- "implemented_by",
67
- "fixes",
68
- "reported_by",
69
- "spec",
70
- "design",
71
- "review",
72
- "deployment",
73
- "blocked_by",
74
- ] as const
75
-
76
- const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/
77
-
78
- /** One precise problem found in a task document. `path` is JSON-pointer-ish. */
79
- export type TodoIssue = ContentIssue
80
-
81
- /** The source document after validation; tasks keep their raw shapes. */
82
- export type TodoFile = {
83
- $schema?: string
84
- id?: string
85
- tasks: Array<Record<string, unknown>>
86
- title?: string
87
- version?: number
88
- }
89
-
90
- export type ParsedTodo =
91
- { ok: true; document: TodoFile } | { ok: false; issues: TodoIssue[] }
92
-
93
- function isRecord(value: unknown): value is Record<string, unknown> {
94
- return Boolean(value) && typeof value === "object" && !Array.isArray(value)
95
- }
96
-
97
- function nonEmptyString(value: unknown): value is string {
98
- return typeof value === "string" && value.trim().length > 0
99
- }
100
-
101
- /** Compact summary for nav badges and error headers. */
102
- export function todoIssueSummary(issues: TodoIssue[]): string {
103
- return issueSummary(issues, "todo.json")
104
- }
105
-
106
- /**
107
- * Validate an already-parsed JSON value. Returns every problem found; an
108
- * empty array means the document is structurally valid. Enums below mirror
109
- * the published schema (`https://raggle.co/schemas/tasks.json`) and the
110
- * client decoder in `./ui` (this plugin's vendored todo UI).
111
- */
112
- export function validateTodoFile(value: unknown): TodoIssue[] {
113
- if (!isRecord(value)) {
114
- return [{ path: "#", message: "todo.json must contain a JSON object" }]
115
- }
116
-
117
- const issues: TodoIssue[] = []
118
-
119
- if (
120
- value.version !== undefined &&
121
- value.version !== 1 &&
122
- value.version !== 2
123
- ) {
124
- issues.push({
125
- path: "version",
126
- message: `must be 1 or 2; got ${JSON.stringify(value.version)}`,
127
- })
128
- }
129
-
130
- if (value.$schema !== undefined && !nonEmptyString(value.$schema)) {
131
- issues.push({ path: "$schema", message: "must be a non-empty string" })
132
- }
133
-
134
- if (value.id !== undefined && !nonEmptyString(value.id)) {
135
- issues.push({ path: "id", message: "must be a non-empty string" })
136
- }
137
-
138
- if (value.title !== undefined && !nonEmptyString(value.title)) {
139
- issues.push({ path: "title", message: "must be a non-empty string" })
140
- }
141
-
142
- if (!Array.isArray(value.tasks)) {
143
- issues.push({ path: "tasks", message: "must be an array of tasks" })
144
- return issues
145
- }
146
-
147
- const ids = new Set<string>()
148
- const keys = new Set<string>()
149
- value.tasks.forEach((task, index) => {
150
- validateTask(task, `tasks[${index}]`, issues, ids, keys)
151
- })
152
-
153
- // Cross-task integrity: relation targets must point at a known task id.
154
- value.tasks.forEach((task, index) => {
155
- if (!isRecord(task) || !Array.isArray(task.relations)) return
156
- task.relations.forEach((relation, relIndex) => {
157
- if (!isRecord(relation)) return
158
- const target = relation.targetTaskId
159
- if (typeof target === "string" && !ids.has(target)) {
160
- issues.push({
161
- path: `tasks[${index}].relations[${relIndex}].targetTaskId`,
162
- message: `references unknown task id ${JSON.stringify(target)}`,
163
- })
164
- }
165
- })
166
- })
167
-
168
- return issues
169
- }
170
-
171
- function validateTask(
172
- task: unknown,
173
- path: string,
174
- issues: TodoIssue[],
175
- ids: Set<string>,
176
- keys: Set<string>,
177
- ) {
178
- if (!isRecord(task)) {
179
- issues.push({ path, message: "must be a task object" })
180
- return
181
- }
182
-
183
- if (!nonEmptyString(task.name)) {
184
- issues.push({
185
- path: `${path}.name`,
186
- message: "required; a task needs a non-empty name",
187
- })
188
- }
189
-
190
- if (task.id !== undefined) {
191
- if (!nonEmptyString(task.id)) {
192
- issues.push({ path: `${path}.id`, message: "must be a non-empty string" })
193
- } else if (ids.has(task.id)) {
194
- issues.push({
195
- path: `${path}.id`,
196
- message: `duplicate task id ${JSON.stringify(task.id)}`,
197
- })
198
- } else {
199
- ids.add(task.id)
200
- }
201
- }
202
-
203
- if (task.key !== undefined) {
204
- if (!nonEmptyString(task.key)) {
205
- issues.push({
206
- path: `${path}.key`,
207
- message: "must be a non-empty string",
208
- })
209
- } else if (keys.has(task.key)) {
210
- issues.push({
211
- path: `${path}.key`,
212
- message: `duplicate task key ${JSON.stringify(task.key)}`,
213
- })
214
- } else {
215
- keys.add(task.key)
216
- }
217
- }
218
-
219
- if (task.type !== undefined && !inEnum(task.type, TASK_TYPES)) {
220
- issues.push({
221
- path: `${path}.type`,
222
- message: `must be one of ${TASK_TYPES.join(", ")}; got ${JSON.stringify(task.type)}`,
223
- })
224
- }
225
-
226
- if (task.status !== undefined && !inEnum(task.status, TASK_STATUSES)) {
227
- issues.push({
228
- path: `${path}.status`,
229
- message: `must be one of ${TASK_STATUSES.join(", ")}; got ${JSON.stringify(task.status)}`,
230
- })
231
- }
232
-
233
- if (task.done !== undefined && typeof task.done !== "boolean") {
234
- issues.push({ path: `${path}.done`, message: "must be a boolean" })
235
- }
236
-
237
- if (task.priority !== undefined && task.priority !== null) {
238
- const priority = task.priority
239
- if (
240
- typeof priority !== "number" ||
241
- !Number.isInteger(priority) ||
242
- priority < 1 ||
243
- priority > 5
244
- ) {
245
- issues.push({
246
- path: `${path}.priority`,
247
- message: "must be an integer between 1 and 5",
248
- })
249
- }
250
- }
251
-
252
- if (
253
- task.deadline !== undefined &&
254
- task.deadline !== null &&
255
- (typeof task.deadline !== "string" || !DATE_PATTERN.test(task.deadline))
256
- ) {
257
- issues.push({
258
- path: `${path}.deadline`,
259
- message: "must be an ISO date (YYYY-MM-DD)",
260
- })
261
- }
262
-
263
- for (const section of ["acceptanceCriteria", "steps"]) {
264
- const items = task[section]
265
- if (items === undefined) continue
266
- if (!Array.isArray(items)) {
267
- issues.push({
268
- path: `${path}.${section}`,
269
- message: "must be an array of check items",
270
- })
271
- continue
272
- }
273
- items.forEach((item, index) => {
274
- validateCheckItem(item, `${path}.${section}[${index}]`, issues)
275
- })
276
- }
277
-
278
- if (task.links !== undefined) {
279
- if (!Array.isArray(task.links)) {
280
- issues.push({
281
- path: `${path}.links`,
282
- message: "must be an array of links",
283
- })
284
- } else {
285
- task.links.forEach((link, index) => {
286
- validateLink(link, `${path}.links[${index}]`, issues)
287
- })
288
- }
289
- }
290
-
291
- if (task.notes !== undefined) {
292
- if (!Array.isArray(task.notes)) {
293
- issues.push({
294
- path: `${path}.notes`,
295
- message: "must be an array of note references",
296
- })
297
- } else {
298
- task.notes.forEach((note, index) => {
299
- validateNoteRef(note, `${path}.notes[${index}]`, issues)
300
- })
301
- }
302
- }
303
- }
304
-
305
- const UNSAFE_NOTE_REFERENCE = /^(?:\/|\\\\|[a-z][a-z0-9+.-]*:|~)/i
306
-
307
- /**
308
- * One `notes` entry: an https URL, or a folder-relative note path with an
309
- * optional `#section` heading (for example "notes/spec.md#Plan").
310
- */
311
- function validateNoteRef(note: unknown, path: string, issues: TodoIssue[]) {
312
- if (!nonEmptyString(note)) {
313
- issues.push({ path, message: "must be a non-empty string" })
314
- return
315
- }
316
- if (/^https?:\/\//i.test(note)) {
317
- let url: URL
318
- try {
319
- url = new URL(note)
320
- } catch {
321
- issues.push({ path, message: "is not a valid absolute URL" })
322
- return
323
- }
324
- if (url.username || url.password) {
325
- issues.push({ path, message: "must not contain embedded credentials" })
326
- return
327
- }
328
- const isLocalhost =
329
- url.hostname === "localhost" ||
330
- url.hostname === "127.0.0.1" ||
331
- url.hostname === "::1"
332
- if (
333
- url.protocol !== "https:" &&
334
- !(url.protocol === "http:" && isLocalhost)
335
- ) {
336
- issues.push({
337
- path,
338
- message: "must use https, except localhost development links",
339
- })
340
- }
341
- return
342
- }
343
- if (UNSAFE_NOTE_REFERENCE.test(note) || note.split(/[\\/]/).includes("..")) {
344
- issues.push({
345
- path,
346
- message: "must be a folder-relative path (or an https URL)",
347
- })
348
- }
349
- }
350
-
351
- function validateCheckItem(item: unknown, path: string, issues: TodoIssue[]) {
352
- if (!isRecord(item)) {
353
- issues.push({
354
- path,
355
- message: "must be an object with id, text and done",
356
- })
357
- return
358
- }
359
- if (!nonEmptyString(item.id)) {
360
- issues.push({ path: `${path}.id`, message: "required non-empty string" })
361
- }
362
- if (!nonEmptyString(item.text)) {
363
- issues.push({ path: `${path}.text`, message: "required non-empty string" })
364
- }
365
- if (typeof item.done !== "boolean") {
366
- issues.push({ path: `${path}.done`, message: "required boolean" })
367
- }
368
- }
369
-
370
- function validateLink(link: unknown, path: string, issues: TodoIssue[]) {
371
- if (!isRecord(link)) {
372
- issues.push({ path, message: "must be a link object" })
373
- return
374
- }
375
- for (const field of ["id", "provider", "kind", "relation", "url"]) {
376
- if (!nonEmptyString(link[field])) {
377
- issues.push({
378
- path: `${path}.${field}`,
379
- message: "required non-empty string",
380
- })
381
- }
382
- }
383
- if (link.provider !== undefined && !inEnum(link.provider, LINK_PROVIDERS)) {
384
- issues.push({
385
- path: `${path}.provider`,
386
- message: `must be one of ${LINK_PROVIDERS.join(", ")}; got ${JSON.stringify(link.provider)}`,
387
- })
388
- }
389
- if (link.kind !== undefined && !inEnum(link.kind, LINK_KINDS)) {
390
- issues.push({
391
- path: `${path}.kind`,
392
- message: `must be one of ${LINK_KINDS.join(", ")}; got ${JSON.stringify(link.kind)}`,
393
- })
394
- }
395
- if (link.relation !== undefined && !inEnum(link.relation, LINK_RELATIONS)) {
396
- issues.push({
397
- path: `${path}.relation`,
398
- message: `must be one of ${LINK_RELATIONS.join(", ")}; got ${JSON.stringify(link.relation)}`,
399
- })
400
- }
401
-
402
- const urlValue = link.url
403
- if (typeof urlValue === "string") {
404
- let url: URL
405
- try {
406
- url = new URL(urlValue)
407
- } catch {
408
- issues.push({
409
- path: `${path}.url`,
410
- message: "is not a valid absolute URL",
411
- })
412
- return
413
- }
414
- if (url.username || url.password) {
415
- issues.push({
416
- path: `${path}.url`,
417
- message: "must not contain embedded credentials",
418
- })
419
- }
420
- const isLocalhost =
421
- url.hostname === "localhost" ||
422
- url.hostname === "127.0.0.1" ||
423
- url.hostname === "::1"
424
- if (
425
- url.protocol !== "https:" &&
426
- !(url.protocol === "http:" && isLocalhost)
427
- ) {
428
- issues.push({
429
- path: `${path}.url`,
430
- message: "must use https, except localhost development links",
431
- })
432
- }
433
- if (link.provider === "github" && url.hostname !== "github.com") {
434
- issues.push({
435
- path: `${path}.url`,
436
- message: "github links must use github.com",
437
- })
438
- }
439
- if (
440
- link.provider === "linear" &&
441
- url.hostname !== "linear.app" &&
442
- !url.hostname.endsWith(".linear.app")
443
- ) {
444
- issues.push({
445
- path: `${path}.url`,
446
- message: "linear links must use linear.app",
447
- })
448
- }
449
- if (
450
- link.provider === "jira" &&
451
- !url.hostname.endsWith(".atlassian.net") &&
452
- url.hostname !== "jira.atlassian.com"
453
- ) {
454
- issues.push({
455
- path: `${path}.url`,
456
- message: "jira links must use an Atlassian Jira host",
457
- })
458
- }
459
- }
460
- }
461
-
462
- function inEnum(value: unknown, allowed: readonly string[]) {
463
- return allowed.includes(String(value))
464
- }
465
-
466
- /**
467
- * Advisory checks that do not invalidate the file. Today there is one: a
468
- * task with no `notes` entries warns, because todos are meant to be
469
- * accompanied by notes — link a note file or an https reference. Paths use
470
- * the same `tasks[i]` convention as `validateTodoFile` issues, relative to
471
- * the file the task lives in.
472
- */
473
- export function todoFileWarnings(value: unknown): TodoIssue[] {
474
- if (!isRecord(value) || !Array.isArray(value.tasks)) return []
475
- return value.tasks.flatMap((task, index) =>
476
- isRecord(task) &&
477
- Array.isArray(task.notes) &&
478
- task.notes.some((note) => nonEmptyString(note))
479
- ? []
480
- : [
481
- {
482
- message:
483
- "no notes; link the accompanying note file or https reference",
484
- path: `tasks[${index}].notes`,
485
- severity: "warning" as const,
486
- },
487
- ],
488
- )
489
- }
490
-
491
- /**
492
- * Parse a `todo.json` source string into a validated document. Never throws:
493
- * invalid JSON and structural mistakes come back as issues with paths, so the
494
- * caller can render "what is wrong and where" directly.
495
- */
496
- export function parseTodoSource(source: string): ParsedTodo {
497
- return parseJsonSource<TodoFile>(source, validateTodoFile)
498
- }
499
-
500
- /**
501
- * Starter `todo.json` for `kennel init`: a valid v2 document with one sample
502
- * task showing the common fields. The folder name becomes the project title.
503
- */
504
- export function starterTodoSource(folder: string): string {
505
- const title = folder.trim().split(/[\\/]/).filter(Boolean).at(-1) ?? "Tasks"
506
- return `${JSON.stringify(
507
- {
508
- $schema: "https://raggle.co/schemas/tasks.json",
509
- version: 2,
510
- id: "todo",
511
- title,
512
- people: [],
513
- projects: [],
514
- cycles: [],
515
- milestones: [],
516
- components: [],
517
- areas: [],
518
- integrations: [],
519
- customFields: [],
520
- automations: [],
521
- tasks: [
522
- {
523
- id: "todo:0",
524
- key: "TODO-1",
525
- index: 0,
526
- rank: "0001",
527
- name: "First task",
528
- description: "Describe what needs to happen.",
529
- type: "task",
530
- status: "todo",
531
- done: false,
532
- priority: 3,
533
- deadline: null,
534
- assigneeIds: [],
535
- acceptanceCriteria: [
536
- { id: "ac-1", text: "Acceptance check", done: false },
537
- ],
538
- },
539
- ],
540
- },
541
- null,
542
- 2,
543
- )}\n`
544
- }
@@ -1,56 +0,0 @@
1
- import { folderPathId } from "@raggle-ai/folder-plugin/client"
2
-
3
- /**
4
- * Tasks from several `todo.json` sources merge into one workspace list, and
5
- * file-local ids such as `todo:1` repeat across sources. Task addresses
6
- * therefore qualify the task id with its owning folder:
7
- *
8
- * `<folderPathId(source folder)>@<task id|key>`
9
- *
10
- * `folderPathId` is the viewer's stable base64url folder id, so the address
11
- * stays one URL-safe path segment and survives the single-segment
12
- * `/folder/<id>/views/tasks/<subpath>` route. A bare `<taskId|key>` remains
13
- * valid as a legacy address and resolves to the first matching source (the
14
- * previous behavior), but every address the plugin builds is qualified.
15
- */
16
-
17
- const QUALIFIER = "@"
18
-
19
- /** Base64url output of `folderPathId`; a task id ref must look like one. */
20
- const REF_PATTERN = /^[A-Za-z0-9_-]+$/
21
-
22
- /**
23
- * Qualified task address for a task inside `sourceFolder`, or undefined when
24
- * the task has neither id nor key (or the source folder is unknown).
25
- */
26
- export function qualifiedTaskId(
27
- sourceFolder: unknown,
28
- task: { id?: unknown; key?: unknown },
29
- ): string | undefined {
30
- if (typeof sourceFolder !== "string" || !sourceFolder) return undefined
31
- const target =
32
- typeof task.id === "string" && task.id.trim()
33
- ? task.id.trim()
34
- : typeof task.key === "string" && task.key.trim()
35
- ? task.key.trim()
36
- : undefined
37
- if (!target) return undefined
38
- return `${folderPathId(sourceFolder)}${QUALIFIER}${target}`
39
- }
40
-
41
- /** Splits a qualified address; undefined for a plain task id/key string. */
42
- export function parseQualifiedTaskId(
43
- value: string,
44
- ): { id: string; ref: string } | undefined {
45
- const at = value.indexOf(QUALIFIER)
46
- if (at < 1) return undefined
47
- const ref = value.slice(0, at)
48
- const id = value.slice(at + 1)
49
- if (!id || !REF_PATTERN.test(ref)) return undefined
50
- return { id, ref }
51
- }
52
-
53
- /** True when `ref` is the view-stable id of `sourceFolder`. */
54
- export function refMatchesFolder(ref: string, sourceFolder: unknown): boolean {
55
- return typeof sourceFolder === "string" && folderPathId(sourceFolder) === ref
56
- }