@taskmark/ui 0.3.0 → 0.3.1

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 (301) hide show
  1. package/README.md +1 -1
  2. package/app/globals.css +25 -0
  3. package/app/layout.tsx +15 -5
  4. package/app/setup/page.tsx +1 -1
  5. package/bin/lib/resolve-board.mjs +1 -4
  6. package/components/board/app-bar.tsx +3 -0
  7. package/components/board/board-screen.tsx +61 -117
  8. package/components/board/changelog-panel.tsx +26 -0
  9. package/components/board/epic-list.tsx +2 -0
  10. package/components/board/list-view-switcher.tsx +7 -3
  11. package/components/board/markdown-content.tsx +21 -1
  12. package/components/board/mermaid-diagram.tsx +88 -0
  13. package/components/board/metric-stat-card.tsx +4 -4
  14. package/components/board/overall-tree-list.tsx +467 -0
  15. package/components/board/overall-work-items-list.tsx +5 -1
  16. package/components/board/points-heatmap.tsx +183 -0
  17. package/components/board/project-status-metrics-strip.tsx +5 -22
  18. package/components/board/static-board-app.tsx +29 -94
  19. package/components/board/status-badge.tsx +8 -0
  20. package/components/board/story-list.tsx +2 -0
  21. package/components/board/task-list.tsx +5 -1
  22. package/components/board/work-item-detail-body.tsx +123 -8
  23. package/components/board/work-items-list.tsx +2 -0
  24. package/components/theme-provider.tsx +39 -0
  25. package/components/theme-toggle.tsx +27 -0
  26. package/components/ui/sonner.tsx +2 -2
  27. package/dist/prod-next/BUILD_ID +1 -1
  28. package/dist/prod-next/build-manifest.json +6 -6
  29. package/dist/prod-next/react-loadable-manifest.json +391 -1
  30. package/dist/prod-next/server/app/_global-error/page.js +2 -2
  31. package/dist/prod-next/server/app/_global-error/page.js.nft.json +1 -1
  32. package/dist/prod-next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  33. package/dist/prod-next/server/app/_global-error.html +1 -1
  34. package/dist/prod-next/server/app/_global-error.rsc +1 -1
  35. package/dist/prod-next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  36. package/dist/prod-next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  37. package/dist/prod-next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  38. package/dist/prod-next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  39. package/dist/prod-next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  40. package/dist/prod-next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  41. package/dist/prod-next/server/app/_not-found/page.js +2 -2
  42. package/dist/prod-next/server/app/_not-found/page.js.nft.json +1 -1
  43. package/dist/prod-next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  44. package/dist/prod-next/server/app/_not-found.html +1 -1
  45. package/dist/prod-next/server/app/_not-found.rsc +19 -17
  46. package/dist/prod-next/server/app/_not-found.segments/_full.segment.rsc +19 -17
  47. package/dist/prod-next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  48. package/dist/prod-next/server/app/_not-found.segments/_index.segment.rsc +8 -6
  49. package/dist/prod-next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  50. package/dist/prod-next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  51. package/dist/prod-next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  52. package/dist/prod-next/server/app/api/taskmark-reload/route.js +1 -1
  53. package/dist/prod-next/server/app/api/taskmark-reload/route.js.nft.json +1 -1
  54. package/dist/prod-next/server/app/board/page.js +2 -2
  55. package/dist/prod-next/server/app/board/page.js.nft.json +1 -1
  56. package/dist/prod-next/server/app/board/page_client-reference-manifest.js +1 -1
  57. package/dist/prod-next/server/app/favicon.ico/route.js +1 -1
  58. package/dist/prod-next/server/app/favicon.ico/route.js.nft.json +1 -1
  59. package/dist/prod-next/server/app/page.js +2 -2
  60. package/dist/prod-next/server/app/page.js.nft.json +1 -1
  61. package/dist/prod-next/server/app/page_client-reference-manifest.js +1 -1
  62. package/dist/prod-next/server/app/setup/page.js +2 -2
  63. package/dist/prod-next/server/app/setup/page.js.nft.json +1 -1
  64. package/dist/prod-next/server/app/setup/page_client-reference-manifest.js +1 -1
  65. package/dist/prod-next/server/chunks/102.js +90 -0
  66. package/dist/prod-next/server/chunks/11.js +1 -0
  67. package/dist/prod-next/server/chunks/1286.js +1 -0
  68. package/dist/prod-next/server/chunks/1412.js +38 -0
  69. package/dist/prod-next/server/chunks/1605.js +1 -0
  70. package/dist/prod-next/server/chunks/169.js +1 -0
  71. package/dist/prod-next/server/chunks/18.js +86 -0
  72. package/dist/prod-next/server/chunks/2.js +1 -0
  73. package/dist/prod-next/server/chunks/2022.js +1 -0
  74. package/dist/prod-next/server/chunks/2156.js +1 -0
  75. package/dist/prod-next/server/chunks/2157.js +1 -0
  76. package/dist/prod-next/server/chunks/2325.js +82 -0
  77. package/dist/prod-next/server/chunks/2838.js +1 -0
  78. package/dist/prod-next/server/chunks/2897.js +79 -0
  79. package/dist/prod-next/server/chunks/3227.js +1 -0
  80. package/dist/prod-next/server/chunks/3266.js +1 -0
  81. package/dist/prod-next/server/chunks/{192.js → 3307.js} +5 -5
  82. package/dist/prod-next/server/chunks/3387.js +19 -0
  83. package/dist/prod-next/server/chunks/3428.js +32 -0
  84. package/dist/prod-next/server/chunks/3479.js +1 -0
  85. package/dist/prod-next/server/chunks/367.js +115 -0
  86. package/dist/prod-next/server/chunks/3698.js +93 -0
  87. package/dist/prod-next/server/chunks/3709.js +148 -0
  88. package/dist/prod-next/server/chunks/41.js +1 -0
  89. package/dist/prod-next/server/chunks/4107.js +62 -0
  90. package/dist/prod-next/server/chunks/4569.js +34 -0
  91. package/dist/prod-next/server/chunks/{741.js → 4741.js} +1 -1
  92. package/dist/prod-next/server/chunks/4755.js +153 -0
  93. package/dist/prod-next/server/chunks/5002.js +1 -0
  94. package/dist/prod-next/server/chunks/5112.js +1 -0
  95. package/dist/prod-next/server/chunks/5289.js +8 -0
  96. package/dist/prod-next/server/chunks/5318.js +1 -0
  97. package/dist/prod-next/server/chunks/5452.js +23 -0
  98. package/dist/prod-next/server/chunks/5453.js +1 -0
  99. package/dist/prod-next/server/chunks/5749.js +27 -0
  100. package/dist/prod-next/server/chunks/5782.js +36 -0
  101. package/dist/prod-next/server/chunks/6163.js +104 -0
  102. package/dist/prod-next/server/chunks/6169.js +64 -0
  103. package/dist/prod-next/server/chunks/6208.js +172 -0
  104. package/dist/prod-next/server/chunks/6399.js +24 -0
  105. package/dist/prod-next/server/chunks/6496.js +1 -0
  106. package/dist/prod-next/server/chunks/6823.js +215 -0
  107. package/dist/prod-next/server/chunks/6888.js +1 -0
  108. package/dist/prod-next/server/chunks/6980.js +1 -0
  109. package/dist/prod-next/server/chunks/7245.js +77 -0
  110. package/dist/prod-next/server/chunks/7307.js +1 -0
  111. package/dist/prod-next/server/chunks/7372.js +287 -0
  112. package/dist/prod-next/server/chunks/7384.js +226 -0
  113. package/dist/prod-next/server/chunks/7516.js +1 -0
  114. package/dist/prod-next/server/chunks/7531.js +24 -0
  115. package/dist/prod-next/server/chunks/7591.js +1 -0
  116. package/dist/prod-next/server/chunks/7755.js +138 -0
  117. package/dist/prod-next/server/chunks/7916.js +1 -0
  118. package/dist/prod-next/server/chunks/8078.js +94 -0
  119. package/dist/prod-next/server/chunks/8253.js +41 -0
  120. package/dist/prod-next/server/chunks/8635.js +1 -0
  121. package/dist/prod-next/server/chunks/8688.js +1 -0
  122. package/dist/prod-next/server/chunks/8872.js +1 -0
  123. package/dist/prod-next/server/chunks/9006.js +1 -0
  124. package/dist/prod-next/server/chunks/9018.js +1 -0
  125. package/dist/prod-next/server/chunks/9047.js +1 -0
  126. package/dist/prod-next/server/chunks/9108.js +1 -0
  127. package/dist/prod-next/server/chunks/9280.js +256 -0
  128. package/dist/prod-next/server/chunks/9419.js +1 -0
  129. package/dist/prod-next/server/chunks/9430.js +1 -0
  130. package/dist/prod-next/server/chunks/9586.js +1 -0
  131. package/dist/prod-next/server/chunks/9775.js +1 -0
  132. package/dist/prod-next/server/chunks/992.js +1 -0
  133. package/dist/prod-next/server/chunks/9926.js +1 -0
  134. package/dist/prod-next/server/chunks/9984.js +8 -0
  135. package/dist/prod-next/server/middleware-build-manifest.js +1 -1
  136. package/dist/prod-next/server/middleware-react-loadable-manifest.js +1 -1
  137. package/dist/prod-next/server/pages/404.html +1 -1
  138. package/dist/prod-next/server/pages/500.html +1 -1
  139. package/dist/prod-next/server/server-reference-manifest.js +1 -1
  140. package/dist/prod-next/server/server-reference-manifest.json +1 -1
  141. package/dist/prod-next/server/webpack-runtime.js +1 -1
  142. package/dist/prod-next/static/chunks/1213.e5c6e70a79b5f7e6.js +24 -0
  143. package/dist/prod-next/static/chunks/1519.2f596e5f5af9ef57.js +62 -0
  144. package/dist/prod-next/static/chunks/1759-f5b8fb456f8fb420.js +1 -0
  145. package/dist/prod-next/static/chunks/1974.885c9a133b86d8a4.js +32 -0
  146. package/dist/prod-next/static/chunks/2280.6a7f540787704698.js +1 -0
  147. package/dist/prod-next/static/chunks/2521.7f745f50d30076e7.js +77 -0
  148. package/dist/prod-next/static/chunks/2585.4d6aa847c780c666.js +1 -0
  149. package/dist/prod-next/static/chunks/2589.1aad68c09021b1ac.js +1 -0
  150. package/dist/prod-next/static/chunks/2600.45ba17a0f756fc31.js +1 -0
  151. package/dist/prod-next/static/chunks/2619.9aac8983f30c7c8a.js +1 -0
  152. package/dist/prod-next/static/chunks/26d302b2.9f6383e42abdfe16.js +122 -0
  153. package/dist/prod-next/static/chunks/2768.305914f7900745b6.js +1 -0
  154. package/dist/prod-next/static/chunks/2931.c2acdbf72808b646.js +41 -0
  155. package/dist/prod-next/static/chunks/3012-838f69b72a0dc1b3.js +1 -0
  156. package/dist/prod-next/static/chunks/3016.406e568aa95ff867.js +1 -0
  157. package/dist/prod-next/static/chunks/3022.972047558a20553a.js +172 -0
  158. package/dist/prod-next/static/chunks/3064.3a2a1e7cd4fdca5f.js +1 -0
  159. package/dist/prod-next/static/chunks/3174.03fbed7b7d7a8256.js +1 -0
  160. package/dist/prod-next/static/chunks/3189.b048866768087750.js +1 -0
  161. package/dist/prod-next/static/chunks/3217.1a053843c58b70b5.js +24 -0
  162. package/dist/prod-next/static/chunks/3308.30b95d524f9c1daf.js +74 -0
  163. package/dist/prod-next/static/chunks/331.fc5d78e1e91daf43.js +1 -0
  164. package/dist/prod-next/static/chunks/3510.5d4d1c79533e080c.js +1 -0
  165. package/dist/prod-next/static/chunks/364.ce5400a9dd70b9dc.js +1 -0
  166. package/dist/prod-next/static/chunks/3661-1f08d73aaffb011d.js +1 -0
  167. package/dist/prod-next/static/chunks/370.57cfa27da8567918.js +1 -0
  168. package/dist/prod-next/static/chunks/3789.c418b69121761983.js +1 -0
  169. package/dist/prod-next/static/chunks/3794-c66b3d8325973459.js +32 -0
  170. package/dist/prod-next/static/chunks/3831.25ae62d10dccc27a.js +1 -0
  171. package/dist/prod-next/static/chunks/395.b41e9d76613d37b5.js +90 -0
  172. package/dist/prod-next/static/chunks/3a7aa434.93dbd20997cfd24f.js +82 -0
  173. package/dist/prod-next/static/chunks/4020.3cc19c463c49f969.js +23 -0
  174. package/dist/prod-next/static/chunks/4035.0acc428926d5c8dd.js +1 -0
  175. package/dist/prod-next/static/chunks/405.0902e4f6eb7bd9a2.js +215 -0
  176. package/dist/prod-next/static/chunks/4159.49ae537ff687bdc6.js +138 -0
  177. package/dist/prod-next/static/chunks/4208.384b381ca2757643.js +287 -0
  178. package/dist/prod-next/static/chunks/4263.ffa8d9bd8a0faaae.js +1 -0
  179. package/dist/prod-next/static/chunks/4561.d1a3aaca8bae3224.js +34 -0
  180. package/dist/prod-next/static/chunks/4592.d0f37bc216b9d988.js +93 -0
  181. package/dist/prod-next/static/chunks/4799.0dc7aa21bff2fd2a.js +1 -0
  182. package/dist/prod-next/static/chunks/4bd1b696-e356ca5ba0218e27.js +1 -0
  183. package/dist/prod-next/static/chunks/514.b804c51b26f5e126.js +1 -0
  184. package/dist/prod-next/static/chunks/5270.28592864d38e881c.js +1 -0
  185. package/dist/prod-next/static/chunks/52733464.3870f99cc446f9ff.js +1 -0
  186. package/dist/prod-next/static/chunks/5354.b559c717a607dc5c.js +1 -0
  187. package/dist/prod-next/static/chunks/5448.d3972d9fb464b54f.js +38 -0
  188. package/dist/prod-next/static/chunks/587.d52737d7288c11b4.js +1 -0
  189. package/dist/prod-next/static/chunks/5909.9e37f7b495b11995.js +115 -0
  190. package/dist/prod-next/static/chunks/5949.5504b3545fcc3d34.js +104 -0
  191. package/dist/prod-next/static/chunks/6322-4e97920c006f7933.js +1 -0
  192. package/dist/prod-next/static/chunks/6361.893d04ac34fa1b49.js +148 -0
  193. package/dist/prod-next/static/chunks/6774.8cd53427dbf32cac.js +86 -0
  194. package/dist/prod-next/static/chunks/6968.b80277c724dbc96e.js +1 -0
  195. package/dist/prod-next/static/chunks/7113.4b9e7ea870aa300a.js +8 -0
  196. package/dist/prod-next/static/chunks/7236.55a9728d35f5b8a6.js +1 -0
  197. package/dist/prod-next/static/chunks/7390.dde9b5bc2f6e24b8.js +1 -0
  198. package/dist/prod-next/static/chunks/7806.a903780fcfa3a30e.js +1 -0
  199. package/dist/prod-next/static/chunks/7997.be57e92e583fe635.js +1 -0
  200. package/dist/prod-next/static/chunks/8216.64a72d4d35ea7be0.js +1 -0
  201. package/dist/prod-next/static/chunks/830.5d5274d7ad1cb58e.js +1 -0
  202. package/dist/prod-next/static/chunks/8378.fa91de3406f554c9.js +226 -0
  203. package/dist/prod-next/static/chunks/8501.cd06aa1b0c7e1c6a.js +79 -0
  204. package/dist/prod-next/static/chunks/8663.6ec5792fb4952e45.js +27 -0
  205. package/dist/prod-next/static/chunks/872.96937e758ddce860.js +1 -0
  206. package/dist/prod-next/static/chunks/8803.75bde095fe3613f5.js +1 -0
  207. package/dist/prod-next/static/chunks/8863.72f5dbb820a0e4c3.js +1 -0
  208. package/dist/prod-next/static/chunks/90542734.f7a7b2972ef5a749.js +1 -0
  209. package/dist/prod-next/static/chunks/9379.5bc9c9343ec5a16c.js +64 -0
  210. package/dist/prod-next/static/chunks/9389.906fe7aa9cd6a170.js +1 -0
  211. package/dist/prod-next/static/chunks/9472.8818ee52cfa931a4.js +94 -0
  212. package/dist/prod-next/static/chunks/966.d6babbf8119162cc.js +1 -0
  213. package/dist/prod-next/static/chunks/9884.d4ac1a961b3366f1.js +36 -0
  214. package/dist/prod-next/static/chunks/9909.67cf9940b125243b.js +1 -0
  215. package/dist/prod-next/static/chunks/app/_global-error/page-8be5492b3b872a4c.js +1 -0
  216. package/dist/prod-next/static/chunks/app/_not-found/page-799e445a538b0024.js +1 -0
  217. package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-8be5492b3b872a4c.js +1 -0
  218. package/dist/prod-next/static/chunks/app/board/page-5c181f45738daf1c.js +1 -0
  219. package/dist/prod-next/static/chunks/app/layout-21782e6d13a6f275.js +1 -0
  220. package/dist/prod-next/static/chunks/app/page-51bfe4356fe233e3.js +1 -0
  221. package/dist/prod-next/static/chunks/app/setup/page-3ccfba00c10f9b17.js +1 -0
  222. package/dist/prod-next/static/chunks/d3ac728e.7964f816a1ca64e5.js +1 -0
  223. package/dist/prod-next/static/chunks/e413bedd.3a12a8cb02e28fdf.js +153 -0
  224. package/dist/prod-next/static/chunks/ea233666.f00b64a7d99b4950.js +62 -0
  225. package/dist/prod-next/static/chunks/framework-711ef29bc66f648c.js +1 -0
  226. package/dist/prod-next/static/chunks/main-app-dd26d4f9d120646e.js +1 -0
  227. package/dist/prod-next/static/chunks/{main-bb33c46a9c25dee8.js → main-df4bcaed74f274eb.js} +2 -2
  228. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-8be5492b3b872a4c.js +1 -0
  229. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-8be5492b3b872a4c.js +1 -0
  230. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/global-error-45a0cf77e03361ed.js +1 -0
  231. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-8be5492b3b872a4c.js +1 -0
  232. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-8be5492b3b872a4c.js +1 -0
  233. package/dist/prod-next/static/chunks/webpack-713747aaa9716c4c.js +1 -0
  234. package/dist/prod-next/static/css/cb78d4b0062d9a3f.css +1 -0
  235. package/dist/public/taskmark-snapshot.json +1 -1
  236. package/lib/taskmark/board-index.ts +101 -0
  237. package/lib/taskmark/build-snapshot.ts +28 -7
  238. package/lib/taskmark/derive-parents.test.ts +23 -0
  239. package/lib/taskmark/derive-parents.ts +323 -0
  240. package/lib/taskmark/detail-load.ts +55 -7
  241. package/lib/taskmark/detail-types.ts +10 -2
  242. package/lib/taskmark/dev-reload-token.ts +1 -1
  243. package/lib/taskmark/discover.ts +4 -7
  244. package/lib/taskmark/epic-types.ts +2 -1
  245. package/lib/taskmark/flat-work-item-types.ts +1 -4
  246. package/lib/taskmark/identity.ts +3 -3
  247. package/lib/taskmark/item-types.ts +2 -1
  248. package/lib/taskmark/list-filters.test.ts +23 -0
  249. package/lib/taskmark/list-filters.ts +7 -2
  250. package/lib/taskmark/list-view-mode.test.ts +45 -0
  251. package/lib/taskmark/list-view-mode.ts +19 -8
  252. package/lib/taskmark/load-changelog.test.ts +32 -0
  253. package/lib/taskmark/load-changelog.ts +18 -0
  254. package/lib/taskmark/overall-tree.test.ts +196 -0
  255. package/lib/taskmark/overall-tree.ts +194 -0
  256. package/lib/taskmark/parse-detail.ts +5 -5
  257. package/lib/taskmark/parse-epics.ts +19 -16
  258. package/lib/taskmark/parse-flat-lists.ts +32 -28
  259. package/lib/taskmark/parse-items.ts +47 -9
  260. package/lib/taskmark/parse-stories.ts +18 -15
  261. package/lib/taskmark/points-calendar.test.ts +72 -0
  262. package/lib/taskmark/points-calendar.ts +86 -0
  263. package/lib/taskmark/project-metrics.ts +11 -7
  264. package/lib/taskmark/snapshot-types.ts +2 -0
  265. package/lib/taskmark/story-types.ts +2 -1
  266. package/lib/taskmark/table-sort.ts +4 -3
  267. package/lib/taskmark/timing.ts +29 -18
  268. package/lib/taskmark/validate.ts +1 -1
  269. package/lib/theme-cookie.test.ts +27 -0
  270. package/lib/theme-cookie.ts +43 -0
  271. package/package.json +3 -1
  272. package/public/taskmark-snapshot.json +1 -1
  273. package/dist/prod-next/server/chunks/463.js +0 -1
  274. package/dist/prod-next/server/chunks/54.js +0 -19
  275. package/dist/prod-next/server/chunks/57.js +0 -8
  276. package/dist/prod-next/server/chunks/724.js +0 -1
  277. package/dist/prod-next/static/chunks/4bd1b696-deb4a0a1da1923b0.js +0 -1
  278. package/dist/prod-next/static/chunks/613-6f794154d28bb75a.js +0 -1
  279. package/dist/prod-next/static/chunks/636-626c380f021b95c8.js +0 -1
  280. package/dist/prod-next/static/chunks/669-8103441c67b185da.js +0 -1
  281. package/dist/prod-next/static/chunks/701-df3ee5f1cf837b1a.js +0 -1
  282. package/dist/prod-next/static/chunks/794-25668765560cb197.js +0 -32
  283. package/dist/prod-next/static/chunks/909.932f2af3be7f1e89.js +0 -1
  284. package/dist/prod-next/static/chunks/app/_global-error/page-a1e3320be3da9be1.js +0 -1
  285. package/dist/prod-next/static/chunks/app/_not-found/page-0b3785af737e7968.js +0 -1
  286. package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-a1e3320be3da9be1.js +0 -1
  287. package/dist/prod-next/static/chunks/app/board/page-636f60191db62f52.js +0 -1
  288. package/dist/prod-next/static/chunks/app/layout-856e8a282e69e7d0.js +0 -1
  289. package/dist/prod-next/static/chunks/app/page-562a33b50da7665b.js +0 -1
  290. package/dist/prod-next/static/chunks/app/setup/page-4cf527b758b1d51e.js +0 -1
  291. package/dist/prod-next/static/chunks/framework-b29f9083d3cda3b2.js +0 -1
  292. package/dist/prod-next/static/chunks/main-app-bfb36455d24b3e5a.js +0 -1
  293. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-a1e3320be3da9be1.js +0 -1
  294. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-a1e3320be3da9be1.js +0 -1
  295. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/global-error-dc53ab29e0491fe4.js +0 -1
  296. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-a1e3320be3da9be1.js +0 -1
  297. package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-a1e3320be3da9be1.js +0 -1
  298. package/dist/prod-next/static/chunks/webpack-d33b8a6298c94a64.js +0 -1
  299. package/dist/prod-next/static/css/2e6ec4c80894e3c0.css +0 -1
  300. /package/dist/prod-next/static/{yyRnYAFQrJjF5_b5ZIeqh → EI1zQaa_kQ7DqUSE4o3Qo}/_buildManifest.js +0 -0
  301. /package/dist/prod-next/static/{yyRnYAFQrJjF5_b5ZIeqh → EI1zQaa_kQ7DqUSE4o3Qo}/_ssgManifest.js +0 -0
@@ -0,0 +1,101 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+
4
+ import { asString, extractFrontmatter } from "@/lib/taskmark/frontmatter"
5
+ import { readActualTimingFromWorkLog } from "@/lib/taskmark/timing"
6
+
7
+ export type IndexedLeaf = {
8
+ filePath: string
9
+ raw: string
10
+ frontmatter: Record<string, unknown>
11
+ actualMinutes: number | null
12
+ actualMs: number | null
13
+ }
14
+
15
+ export type BoardIndex = {
16
+ boardPath: string
17
+ leaves: IndexedLeaf[]
18
+ leavesByEpic: ReadonlyMap<string, IndexedLeaf[]>
19
+ leavesByParent: ReadonlyMap<string, IndexedLeaf[]>
20
+ }
21
+
22
+ function addToMap(
23
+ map: Map<string, IndexedLeaf[]>,
24
+ key: string,
25
+ leaf: IndexedLeaf
26
+ ): void {
27
+ if (!key) return
28
+ const current = map.get(key)
29
+ if (current) current.push(leaf)
30
+ else map.set(key, [leaf])
31
+ }
32
+
33
+ function itemMarkdownFiles(dir: string): string[] {
34
+ let entries: fs.Dirent[]
35
+ try {
36
+ entries = fs.readdirSync(dir, { withFileTypes: true })
37
+ } catch {
38
+ return []
39
+ }
40
+
41
+ const files: string[] = []
42
+ for (const entry of entries) {
43
+ if (entry.name.startsWith(".")) continue
44
+ const filePath = path.join(dir, entry.name)
45
+ if (entry.isDirectory()) {
46
+ if (entry.name === "items") {
47
+ let itemEntries: fs.Dirent[]
48
+ try {
49
+ itemEntries = fs.readdirSync(filePath, { withFileTypes: true })
50
+ } catch {
51
+ continue
52
+ }
53
+ for (const item of itemEntries) {
54
+ if (
55
+ item.isFile() &&
56
+ !item.name.startsWith(".") &&
57
+ item.name.endsWith(".md")
58
+ ) {
59
+ files.push(path.join(filePath, item.name))
60
+ }
61
+ }
62
+ } else {
63
+ files.push(...itemMarkdownFiles(filePath))
64
+ }
65
+ }
66
+ }
67
+ return files
68
+ }
69
+
70
+ /** Read every task/bug once and group leaves for reuse during one render. */
71
+ export function buildBoardIndex(boardPath: string): BoardIndex {
72
+ const leaves: IndexedLeaf[] = []
73
+ const leavesByEpic = new Map<string, IndexedLeaf[]>()
74
+ const leavesByParent = new Map<string, IndexedLeaf[]>()
75
+
76
+ for (const filePath of itemMarkdownFiles(path.join(boardPath, "epics"))) {
77
+ try {
78
+ const raw = fs.readFileSync(filePath, "utf8")
79
+ const frontmatter = extractFrontmatter(raw)
80
+ if (!frontmatter) continue
81
+ const type = asString(frontmatter.type).toLowerCase()
82
+ const id = asString(frontmatter.id)
83
+ if ((type !== "task" && type !== "bug") || !id) continue
84
+
85
+ const leaf = {
86
+ filePath,
87
+ raw,
88
+ frontmatter,
89
+ ...readActualTimingFromWorkLog(raw),
90
+ }
91
+ leaves.push(leaf)
92
+ addToMap(leavesByEpic, asString(frontmatter.epic), leaf)
93
+ addToMap(leavesByParent, asString(frontmatter.parent), leaf)
94
+ } catch {
95
+ // Existing parsers surface malformed files in their own result objects.
96
+ }
97
+ }
98
+
99
+ leaves.sort((a, b) => a.filePath.localeCompare(b.filePath))
100
+ return { boardPath, leaves, leavesByEpic, leavesByParent }
101
+ }
@@ -2,6 +2,7 @@ import fs from "node:fs"
2
2
  import path from "node:path"
3
3
 
4
4
  import { loadWorkItemDetailSync } from "@/lib/taskmark/detail-load"
5
+ import { buildBoardIndex } from "@/lib/taskmark/board-index"
5
6
  import type { WorkItemRef } from "@/lib/taskmark/detail-types"
6
7
  import { parseEpicsForProject } from "@/lib/taskmark/parse-epics"
7
8
  import { parseWorkItemsForEpic, parseWorkItemsViewForProject } from "@/lib/taskmark/parse-flat-lists"
@@ -16,6 +17,7 @@ import {
16
17
  storyKey,
17
18
  } from "@/lib/taskmark/snapshot-types"
18
19
  import type { DiscoveredProject } from "@/lib/taskmark/types"
20
+ import { loadBoardChangelogMarkdown } from "@/lib/taskmark/load-changelog"
19
21
  import { loadWorkspace } from "@/lib/taskmark/workspace"
20
22
  import { asString, extractFrontmatter } from "@/lib/taskmark/frontmatter"
21
23
 
@@ -127,19 +129,26 @@ export function buildBoardSnapshot(
127
129
  projects: DiscoveredProject[],
128
130
  active: DiscoveredProject
129
131
  ): BoardSnapshot {
130
- const epics = parseEpicsForProject(active)
132
+ const boardIndex = buildBoardIndex(active.boardPath)
133
+ const epics = parseEpicsForProject(active, boardIndex)
131
134
  const workItemsByEpic: BoardSnapshot["workItemsByEpic"] = {}
132
135
  const itemsByStory: BoardSnapshot["itemsByStory"] = {}
133
136
 
134
137
  for (const epic of epics.epics) {
135
- const epicList = parseWorkItemsForEpic(active, epic.id, epic.title)
138
+ const epicList = parseWorkItemsForEpic(
139
+ active,
140
+ epic.id,
141
+ epic.title,
142
+ boardIndex
143
+ )
136
144
  workItemsByEpic[epic.id] = epicList
137
145
  for (const row of epicList.rows) {
138
146
  if (row.kind !== "story") continue
139
147
  itemsByStory[storyKey(epic.id, row.id)] = parseItemsForStory(
140
148
  active,
141
149
  epic.id,
142
- row.id
150
+ row.id,
151
+ boardIndex
143
152
  )
144
153
  }
145
154
  }
@@ -152,7 +161,12 @@ export function buildBoardSnapshot(
152
161
  refsById[ref.id] = ref
153
162
  const hint =
154
163
  ref.kind === "epic" ? "epic" : ref.kind === "story" ? "story" : "item"
155
- const detail = loadWorkItemDetailSync(projects, ref.filePath, hint)
164
+ const detail = loadWorkItemDetailSync(
165
+ projects,
166
+ ref.filePath,
167
+ hint,
168
+ boardIndex
169
+ )
156
170
  if (detail.ok) {
157
171
  detailsByPath[ref.filePath] = detail.detail
158
172
  }
@@ -166,19 +180,26 @@ export function buildBoardSnapshot(
166
180
  workItems: false,
167
181
  } as const
168
182
 
183
+ const metricLeaves = collectMetricLeaves(active, boardIndex)
184
+
169
185
  return {
170
186
  version: 1,
171
187
  builtAt: new Date().toISOString(),
172
188
  project: active,
173
189
  projects,
174
190
  epics,
175
- workItemsView: parseWorkItemsViewForProject(active),
191
+ workItemsView: parseWorkItemsViewForProject(active, boardIndex),
176
192
  workItemsByEpic,
177
193
  itemsByStory,
178
- statusMetrics: computeProjectStatusMetrics(active),
194
+ statusMetrics: computeProjectStatusMetrics(
195
+ active,
196
+ boardIndex,
197
+ metricLeaves
198
+ ),
179
199
  countableCompletions: collectCompletedLeafPointSamples(
180
- collectMetricLeaves(active)
200
+ metricLeaves
181
201
  ),
202
+ changelogMarkdown: loadBoardChangelogMarkdown(active.boardPath),
182
203
  hideCompletedDefaults: { ...hideCompletedDefaults },
183
204
  detailsByPath,
184
205
  refsById,
@@ -0,0 +1,23 @@
1
+ import assert from "node:assert/strict"
2
+ import test from "node:test"
3
+
4
+ import { deriveParentStatus } from "./derive-parents"
5
+
6
+ test("rolls all shelved descendants up to shelved", () => {
7
+ assert.equal(deriveParentStatus(["shelved", "shelved"]), "shelved")
8
+ })
9
+
10
+ test("rolls a terminal mix containing done up to done", () => {
11
+ assert.equal(
12
+ deriveParentStatus(["shelved", "cancelled", "done"]),
13
+ "done"
14
+ )
15
+ })
16
+
17
+ test("rolls shelved and cancelled without done up to shelved", () => {
18
+ assert.equal(deriveParentStatus(["shelved", "cancelled"]), "shelved")
19
+ })
20
+
21
+ test("keeps a parent in progress while any descendant is open", () => {
22
+ assert.equal(deriveParentStatus(["shelved", "backlog"]), "in_progress")
23
+ })
@@ -0,0 +1,323 @@
1
+ import {
2
+ asNumberOrNull,
3
+ asString,
4
+ } from "@/lib/taskmark/frontmatter"
5
+ import {
6
+ buildBoardIndex,
7
+ type BoardIndex,
8
+ type IndexedLeaf,
9
+ } from "@/lib/taskmark/board-index"
10
+ import type { ContributorIdentity } from "@/lib/taskmark/identity"
11
+ import { asContributorList } from "@/lib/taskmark/identity"
12
+ import { parseWorkItemDetailFromRaw } from "@/lib/taskmark/parse-detail"
13
+ import type {
14
+ CommitRow,
15
+ PromptFeedbackRow,
16
+ RowWorkItemRef,
17
+ WorkLogRow,
18
+ } from "@/lib/taskmark/detail-types"
19
+
20
+ export const STATIC_SIZE_POINTS = {
21
+ XS: 1,
22
+ S: 3,
23
+ M: 5,
24
+ L: 8,
25
+ XL: 13,
26
+ XXL: 21,
27
+ } as const
28
+
29
+ type ParentKind = "epic" | "story"
30
+
31
+ type LeafRecord = {
32
+ id: string
33
+ title: string
34
+ parent: string
35
+ epic: string
36
+ status: string
37
+ points: number
38
+ reporters: ContributorIdentity[]
39
+ resolvers: ContributorIdentity[]
40
+ updated: string
41
+ startedAt: string
42
+ completedAt: string
43
+ actualMinutes: number | null
44
+ actualMs: number | null
45
+ promptFeedback: PromptFeedbackRow[]
46
+ commits: CommitRow[]
47
+ workLog: WorkLogRow[]
48
+ }
49
+
50
+ export type ParentRollup = {
51
+ status: string
52
+ size: string
53
+ points: number
54
+ actualMinutes: number
55
+ actualMs: number
56
+ resolvers: ContributorIdentity[]
57
+ updated: string
58
+ startedAt: string
59
+ completedAt: string
60
+ promptFeedback: PromptFeedbackRow[]
61
+ commits: CommitRow[]
62
+ workLog: WorkLogRow[]
63
+ leafCount: number
64
+ doneLeafCount: number
65
+ }
66
+
67
+ function leafPoints(frontmatter: Record<string, unknown>): number {
68
+ const explicit = asNumberOrNull(frontmatter.points)
69
+ if (explicit != null) return Math.max(0, explicit)
70
+ const size = asString(frontmatter.size).trim().toUpperCase()
71
+ return STATIC_SIZE_POINTS[size as keyof typeof STATIC_SIZE_POINTS] ?? 0
72
+ }
73
+
74
+ function readLeaf(indexed: IndexedLeaf, includeDetails: boolean): LeafRecord | null {
75
+ try {
76
+ const { filePath, raw, frontmatter } = indexed
77
+ const type = asString(frontmatter.type).toLowerCase()
78
+ if (type !== "task" && type !== "bug") return null
79
+ const id = asString(frontmatter.id)
80
+ if (!id) return null
81
+ const timing = {
82
+ actualMinutes: indexed.actualMinutes,
83
+ actualMs: indexed.actualMs,
84
+ }
85
+ const parsed = includeDetails
86
+ ? parseWorkItemDetailFromRaw(raw, filePath, "item")
87
+ : null
88
+ const itemDetail =
89
+ parsed?.ok &&
90
+ parsed.detail.type !== "epic" &&
91
+ parsed.detail.type !== "story"
92
+ ? parsed.detail
93
+ : null
94
+ return {
95
+ id,
96
+ title: asString(frontmatter.title, id),
97
+ parent: asString(frontmatter.parent),
98
+ epic: asString(frontmatter.epic),
99
+ status: asString(frontmatter.status, "backlog"),
100
+ points: leafPoints(frontmatter),
101
+ reporters: asContributorList(frontmatter.reporters),
102
+ resolvers: asContributorList(frontmatter.resolvers),
103
+ updated: asString(frontmatter.updated),
104
+ startedAt: asString(frontmatter.started_at),
105
+ completedAt: asString(frontmatter.completed_at),
106
+ ...timing,
107
+ promptFeedback: itemDetail?.promptFeedback ?? [],
108
+ commits: itemDetail?.commits ?? [],
109
+ workLog: itemDetail?.workLog ?? [],
110
+ }
111
+ } catch {
112
+ return null
113
+ }
114
+ }
115
+
116
+ function leavesForParent(
117
+ parentId: string,
118
+ kind: ParentKind,
119
+ index: BoardIndex,
120
+ includeDetails: boolean
121
+ ): LeafRecord[] {
122
+ const indexed =
123
+ kind === "story"
124
+ ? index.leavesByParent.get(parentId) ?? []
125
+ : [
126
+ ...(index.leavesByEpic.get(parentId) ?? []),
127
+ ...(index.leavesByParent.get(parentId) ?? []),
128
+ ].filter(
129
+ (leaf, position, all) =>
130
+ all.findIndex((candidate) => candidate.filePath === leaf.filePath) ===
131
+ position
132
+ )
133
+ return indexed
134
+ .map((leaf) => readLeaf(leaf, includeDetails))
135
+ .filter((leaf): leaf is LeafRecord => leaf != null)
136
+ }
137
+
138
+ export function deriveParentStatus(statusesInput: readonly string[]): string {
139
+ if (statusesInput.length === 0) return "backlog"
140
+ const statuses = statusesInput.map((status) => status.trim().toLowerCase())
141
+ const terminal = statuses.every(
142
+ (status) =>
143
+ status === "done" || status === "shelved" || status === "cancelled"
144
+ )
145
+ if (terminal && statuses.some((status) => status === "done")) return "done"
146
+ if (terminal && statuses.some((status) => status === "shelved")) {
147
+ return "shelved"
148
+ }
149
+ if (terminal) return "cancelled"
150
+ if (statuses.every((status) => status === "backlog")) return "backlog"
151
+ return "in_progress"
152
+ }
153
+
154
+ function parentSize(points: number, leafCount: number): string {
155
+ if (leafCount === 0) return "—"
156
+ if (points <= 1) return "XS"
157
+ if (points <= 3) return "S"
158
+ if (points <= 5) return "M"
159
+ if (points <= 8) return "L"
160
+ if (points <= 13) return "XL"
161
+ return "XXL"
162
+ }
163
+
164
+ function dateValue(value: string): number | null {
165
+ if (!value.trim()) return null
166
+ const parsed = Date.parse(value)
167
+ return Number.isFinite(parsed) ? parsed : null
168
+ }
169
+
170
+ function selectDate(
171
+ values: readonly string[],
172
+ direction: "min" | "max"
173
+ ): string {
174
+ return values
175
+ .map((value) => ({ value, time: dateValue(value) }))
176
+ .filter((entry): entry is { value: string; time: number } => entry.time != null)
177
+ .sort((a, b) => direction === "min" ? a.time - b.time : b.time - a.time)[0]
178
+ ?.value ?? ""
179
+ }
180
+
181
+ function identityKey(identity: ContributorIdentity): string {
182
+ return identity.email.trim().toLowerCase() ||
183
+ identity.name.trim().toLowerCase()
184
+ }
185
+
186
+ function uniqueResolvers(leaves: readonly LeafRecord[]): ContributorIdentity[] {
187
+ const identities = new Map<string, ContributorIdentity>()
188
+ for (const leaf of leaves) {
189
+ for (const identity of leaf.resolvers) {
190
+ const key = identityKey(identity)
191
+ if (key && !identities.has(key)) identities.set(key, identity)
192
+ }
193
+ }
194
+ return [...identities.values()]
195
+ }
196
+
197
+ /**
198
+ * Collapse rows that leaves repeat verbatim into one row carrying every
199
+ * contributing work item.
200
+ */
201
+ function groupRowsByContent<T extends object>(
202
+ leaves: readonly LeafRecord[],
203
+ rowsOf: (leaf: LeafRecord) => readonly T[],
204
+ contentKey: (row: T) => string,
205
+ merge?: (existing: T, incoming: T) => T
206
+ ): (T & { workItems: RowWorkItemRef[] })[] {
207
+ const grouped = new Map<string, T & { workItems: RowWorkItemRef[] }>()
208
+
209
+ for (const leaf of leaves) {
210
+ for (const row of rowsOf(leaf)) {
211
+ const key = contentKey(row)
212
+ const existing = grouped.get(key)
213
+ const workItem = { id: leaf.id, title: leaf.title }
214
+ if (existing) {
215
+ if (merge) Object.assign(existing, merge(existing, row))
216
+ if (!existing.workItems.some((item) => item.id === leaf.id)) {
217
+ existing.workItems.push(workItem)
218
+ }
219
+ continue
220
+ }
221
+ grouped.set(key, {
222
+ ...row,
223
+ workItems: [workItem],
224
+ })
225
+ }
226
+ }
227
+
228
+ return [...grouped.values()]
229
+ }
230
+
231
+ function normalizeLogText(value: string): string {
232
+ return value.trim().replace(/\s+/g, " ").toLowerCase()
233
+ }
234
+
235
+ function workLogContentKey(row: WorkLogRow): string {
236
+ const actor = normalizeLogText(row.actor)
237
+ const summary = normalizeLogText(row.summary)
238
+ if (!summary) {
239
+ return [actor, row.started, row.ended, ""].join("\u0000")
240
+ }
241
+ return [actor, summary].join("\u0000")
242
+ }
243
+
244
+ function mergeWorkLogSpan(existing: WorkLogRow, incoming: WorkLogRow): WorkLogRow {
245
+ const started = selectDate([existing.started, incoming.started], "min")
246
+ const ended = selectDate([existing.ended, incoming.ended], "max")
247
+ return { ...existing, started, ended }
248
+ }
249
+
250
+ function sortByDate<T>(rows: T[], date: (row: T) => string): T[] {
251
+ return rows.sort((a, b) => {
252
+ const left = dateValue(date(a)) ?? Number.MAX_SAFE_INTEGER
253
+ const right = dateValue(date(b)) ?? Number.MAX_SAFE_INTEGER
254
+ return left - right
255
+ })
256
+ }
257
+
258
+ /** Derive every mutable parent field exclusively from task/bug leaves. */
259
+ export function deriveParentRollup(
260
+ boardPath: string,
261
+ parentId: string,
262
+ kind: ParentKind,
263
+ index: BoardIndex = buildBoardIndex(boardPath),
264
+ includeDetails = false
265
+ ): ParentRollup {
266
+ const leaves = leavesForParent(parentId, kind, index, includeDetails)
267
+ const status = deriveParentStatus(leaves.map((leaf) => leaf.status))
268
+ const points = leaves.reduce((sum, leaf) => sum + leaf.points, 0)
269
+ const actualMs = leaves.reduce(
270
+ (sum, leaf) =>
271
+ sum + (leaf.actualMs ?? (leaf.actualMinutes ?? 0) * 60_000),
272
+ 0
273
+ )
274
+ const completedLeaves = leaves.filter(
275
+ (leaf) =>
276
+ leaf.status.trim().toLowerCase() === "done" ||
277
+ leaf.status.trim().toLowerCase() === "shelved" ||
278
+ leaf.status.trim().toLowerCase() === "cancelled"
279
+ )
280
+
281
+ return {
282
+ status,
283
+ size: parentSize(points, leaves.length),
284
+ points,
285
+ actualMinutes: Math.floor(actualMs / 60_000),
286
+ actualMs,
287
+ resolvers: uniqueResolvers(leaves),
288
+ updated: selectDate(leaves.map((leaf) => leaf.updated), "max"),
289
+ startedAt: selectDate(leaves.map((leaf) => leaf.startedAt), "min"),
290
+ completedAt:
291
+ completedLeaves.length === leaves.length
292
+ ? selectDate(completedLeaves.map((leaf) => leaf.completedAt), "max")
293
+ : "",
294
+ promptFeedback: sortByDate(
295
+ groupRowsByContent(
296
+ leaves,
297
+ (leaf) => leaf.promptFeedback,
298
+ (row) => [row.when, row.kind, row.author, row.summary].join("\u0000")
299
+ ),
300
+ (row) => row.when
301
+ ).map((row, position) => ({ ...row, index: String(position + 1) })),
302
+ commits: sortByDate(
303
+ groupRowsByContent(
304
+ leaves,
305
+ (leaf) => leaf.commits,
306
+ (row) =>
307
+ [row.sha, row.repo, row.date, row.author, row.message].join("\u0000")
308
+ ),
309
+ (row) => row.date
310
+ ),
311
+ workLog: sortByDate(
312
+ groupRowsByContent(
313
+ leaves,
314
+ (leaf) => leaf.workLog,
315
+ workLogContentKey,
316
+ mergeWorkLogSpan
317
+ ),
318
+ (row) => row.started
319
+ ).map((row, position) => ({ ...row, session: String(position + 1) })),
320
+ leafCount: leaves.length,
321
+ doneLeafCount: completedLeaves.length,
322
+ }
323
+ }
@@ -18,6 +18,11 @@ import {
18
18
  findWorkItemOnBoard,
19
19
  } from "@/lib/taskmark/resolve-work-item"
20
20
  import type { WorkItemRef } from "@/lib/taskmark/detail-types"
21
+ import { deriveParentRollup } from "@/lib/taskmark/derive-parents"
22
+ import {
23
+ buildBoardIndex,
24
+ type BoardIndex,
25
+ } from "@/lib/taskmark/board-index"
21
26
 
22
27
  function isPathInside(parent: string, child: string): boolean {
23
28
  const rel = path.relative(parent, child)
@@ -37,11 +42,12 @@ export function findProjectForFile(
37
42
 
38
43
  export function attachChildren(
39
44
  detail: WorkItemDetail,
40
- project: DiscoveredProject
45
+ project: DiscoveredProject,
46
+ index?: BoardIndex
41
47
  ): WorkItemDetail {
42
48
  if (detail.type === "epic") {
43
49
  const children: DetailChildItem[] = []
44
- const stories = parseStoriesForEpic(project, detail.id)
50
+ const stories = parseStoriesForEpic(project, detail.id, index)
45
51
  for (const story of stories.stories) {
46
52
  children.push({
47
53
  id: story.id,
@@ -52,7 +58,7 @@ export function attachChildren(
52
58
  filePath: story.filePath,
53
59
  })
54
60
  }
55
- const epicItems = parseItemsForEpic(project, detail.id)
61
+ const epicItems = parseItemsForEpic(project, detail.id, index)
56
62
  for (const item of epicItems.items) {
57
63
  children.push({
58
64
  id: item.id,
@@ -70,7 +76,7 @@ export function attachChildren(
70
76
  const epicId = detail.epic || detail.parent
71
77
  if (!epicId) return detail
72
78
  const children: DetailChildItem[] = []
73
- const items = parseItemsForStory(project, epicId, detail.id)
79
+ const items = parseItemsForStory(project, epicId, detail.id, index)
74
80
  for (const item of items.items) {
75
81
  children.push({
76
82
  id: item.id,
@@ -87,10 +93,41 @@ export function attachChildren(
87
93
  return detail
88
94
  }
89
95
 
96
+ function attachDerivedParentFields(
97
+ detail: WorkItemDetail,
98
+ project: DiscoveredProject,
99
+ index?: BoardIndex
100
+ ): WorkItemDetail {
101
+ if (detail.type !== "epic" && detail.type !== "story") return detail
102
+ const derived = deriveParentRollup(
103
+ project.boardPath,
104
+ detail.id,
105
+ detail.type,
106
+ index,
107
+ true
108
+ )
109
+ return {
110
+ ...detail,
111
+ status: derived.status,
112
+ size: derived.size,
113
+ points: derived.points,
114
+ actualMinutes: derived.actualMinutes,
115
+ actualMs: derived.actualMs,
116
+ resolvers: derived.resolvers,
117
+ updated: derived.updated,
118
+ startedAt: derived.startedAt,
119
+ completedAt: derived.completedAt,
120
+ promptFeedback: derived.promptFeedback,
121
+ commits: derived.commits,
122
+ workLog: derived.workLog,
123
+ }
124
+ }
125
+
90
126
  export function loadWorkItemDetailSync(
91
127
  projects: DiscoveredProject[],
92
128
  filePath: string,
93
- hint?: "epic" | "story" | "item"
129
+ hint?: "epic" | "story" | "item",
130
+ index?: BoardIndex
94
131
  ): WorkItemDetailResult {
95
132
  if (!filePath || typeof filePath !== "string") {
96
133
  return { ok: false, filePath: filePath ?? "", message: "Missing file path" }
@@ -123,8 +160,13 @@ export function loadWorkItemDetailSync(
123
160
 
124
161
  const result = parseWorkItemDetailFromRaw(raw, resolved, hint)
125
162
  if (!result.ok) return result
163
+ if (result.detail.type !== "epic" && result.detail.type !== "story") {
164
+ return result
165
+ }
126
166
 
127
- return { ok: true, detail: attachChildren(result.detail, project) }
167
+ const boardIndex = index ?? buildBoardIndex(project.boardPath)
168
+ const derived = attachDerivedParentFields(result.detail, project, boardIndex)
169
+ return { ok: true, detail: attachChildren(derived, project, boardIndex) }
128
170
  }
129
171
 
130
172
  export function resolveWorkItemByIdSync(
@@ -175,6 +217,12 @@ export function resolveWorkItemByIdSync(
175
217
  }
176
218
  }
177
219
 
178
- const { boardPath: _b, projectId: _p, ...ref } = resolved
220
+ const ref: WorkItemRef = {
221
+ kind: resolved.kind,
222
+ id: resolved.id,
223
+ title: resolved.title,
224
+ filePath: resolved.filePath,
225
+ ...(resolved.itemType ? { itemType: resolved.itemType } : {}),
226
+ }
179
227
  return { ok: true, ref }
180
228
  }
@@ -13,12 +13,19 @@ export type MarkdownTable = {
13
13
  rows: string[][]
14
14
  }
15
15
 
16
+ /** Descendant task/bug that contributed a row to a derived parent view. */
17
+ export type RowWorkItemRef = {
18
+ id: string
19
+ title: string
20
+ }
21
+
16
22
  export type PromptFeedbackRow = {
17
23
  index: string
18
24
  when: string
19
25
  kind: string
20
26
  author: string
21
27
  summary: string
28
+ workItems?: RowWorkItemRef[]
22
29
  }
23
30
 
24
31
  export type CommitRow = {
@@ -27,6 +34,7 @@ export type CommitRow = {
27
34
  date: string
28
35
  author: string
29
36
  message: string
37
+ workItems?: RowWorkItemRef[]
30
38
  }
31
39
 
32
40
  export type WorkLogRow = {
@@ -35,6 +43,7 @@ export type WorkLogRow = {
35
43
  started: string
36
44
  ended: string
37
45
  summary: string
46
+ workItems?: RowWorkItemRef[]
38
47
  }
39
48
 
40
49
  export type WorkItemMeta = {
@@ -45,11 +54,9 @@ export type WorkItemMeta = {
45
54
  priority: string
46
55
  size: string
47
56
  points: number | null
48
- estimateMinutes: number | null
49
57
  actualMinutes: number | null
50
58
  actualMs: number | null
51
59
  tags: string[]
52
- owner: string
53
60
  reporters: ContributorIdentity[]
54
61
  resolvers: ContributorIdentity[]
55
62
  blocked: boolean
@@ -82,6 +89,7 @@ export type EpicDetail = WorkItemMeta & {
82
89
  storiesMarkdown: string
83
90
  /** Stories + epic-direct tasks/bugs under this epic. */
84
91
  children: DetailChildItem[]
92
+ promptFeedback: PromptFeedbackRow[]
85
93
  commits: CommitRow[]
86
94
  workLog: WorkLogRow[]
87
95
  }