@shivaedev/work-board 0.2.1 → 0.4.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 (420) hide show
  1. package/CHANGELOG.md +93 -0
  2. package/README.md +208 -11
  3. package/dist/attention/overviewHtml.d.ts +3 -0
  4. package/dist/attention/overviewHtml.d.ts.map +1 -0
  5. package/dist/attention/overviewHtml.js +41 -0
  6. package/dist/attention/overviewHtml.js.map +1 -0
  7. package/dist/attention/requests.d.ts +24 -0
  8. package/dist/attention/requests.d.ts.map +1 -0
  9. package/dist/attention/requests.js +40 -0
  10. package/dist/attention/requests.js.map +1 -0
  11. package/dist/attention/style.d.ts +2 -0
  12. package/dist/attention/style.d.ts.map +1 -0
  13. package/dist/attention/style.js +10 -0
  14. package/dist/attention/style.js.map +1 -0
  15. package/dist/board.d.ts.map +1 -1
  16. package/dist/board.js +15 -2
  17. package/dist/board.js.map +1 -1
  18. package/dist/cli.js +1 -1
  19. package/dist/files/changes.d.ts +1 -0
  20. package/dist/files/changes.d.ts.map +1 -1
  21. package/dist/files/changes.js +5 -10
  22. package/dist/files/changes.js.map +1 -1
  23. package/dist/files/home.d.ts.map +1 -1
  24. package/dist/files/home.js +4 -0
  25. package/dist/files/home.js.map +1 -1
  26. package/dist/files/list.d.ts +7 -0
  27. package/dist/files/list.d.ts.map +1 -1
  28. package/dist/files/list.js +23 -13
  29. package/dist/files/list.js.map +1 -1
  30. package/dist/files/markdownPath.d.ts +3 -0
  31. package/dist/files/markdownPath.d.ts.map +1 -0
  32. package/dist/files/markdownPath.js +31 -0
  33. package/dist/files/markdownPath.js.map +1 -0
  34. package/dist/files/url.d.ts +2 -0
  35. package/dist/files/url.d.ts.map +1 -0
  36. package/dist/files/url.js +4 -0
  37. package/dist/files/url.js.map +1 -0
  38. package/dist/files/watchDirectories.d.ts +9 -0
  39. package/dist/files/watchDirectories.d.ts.map +1 -0
  40. package/dist/files/watchDirectories.js +33 -0
  41. package/dist/files/watchDirectories.js.map +1 -0
  42. package/dist/history/baseline.d.ts +19 -0
  43. package/dist/history/baseline.d.ts.map +1 -0
  44. package/dist/history/baseline.js +40 -0
  45. package/dist/history/baseline.js.map +1 -0
  46. package/dist/history/compare.d.ts +14 -0
  47. package/dist/history/compare.d.ts.map +1 -0
  48. package/dist/history/compare.js +47 -0
  49. package/dist/history/compare.js.map +1 -0
  50. package/dist/history/documents.d.ts +10 -0
  51. package/dist/history/documents.d.ts.map +1 -0
  52. package/dist/history/documents.js +15 -0
  53. package/dist/history/documents.js.map +1 -0
  54. package/dist/history/fields.d.ts +3 -0
  55. package/dist/history/fields.d.ts.map +1 -0
  56. package/dist/history/fields.js +27 -0
  57. package/dist/history/fields.js.map +1 -0
  58. package/dist/history/html.d.ts +4 -0
  59. package/dist/history/html.d.ts.map +1 -0
  60. package/dist/history/html.js +22 -0
  61. package/dist/history/html.js.map +1 -0
  62. package/dist/history/limits.d.ts +3 -0
  63. package/dist/history/limits.d.ts.map +1 -0
  64. package/dist/history/limits.js +3 -0
  65. package/dist/history/limits.js.map +1 -0
  66. package/dist/history/response.d.ts +4 -0
  67. package/dist/history/response.d.ts.map +1 -0
  68. package/dist/history/response.js +20 -0
  69. package/dist/history/response.js.map +1 -0
  70. package/dist/history/schema.d.ts +24 -0
  71. package/dist/history/schema.d.ts.map +1 -0
  72. package/dist/history/schema.js +19 -0
  73. package/dist/history/schema.js.map +1 -0
  74. package/dist/history/script.d.ts +2 -0
  75. package/dist/history/script.d.ts.map +1 -0
  76. package/dist/history/script.js +113 -0
  77. package/dist/history/script.js.map +1 -0
  78. package/dist/http/assets.d.ts.map +1 -1
  79. package/dist/http/assets.js +18 -1
  80. package/dist/http/assets.js.map +1 -1
  81. package/dist/http/history.d.ts +7 -0
  82. package/dist/http/history.d.ts.map +1 -0
  83. package/dist/http/history.js +45 -0
  84. package/dist/http/history.js.map +1 -0
  85. package/dist/http/identity.d.ts +7 -0
  86. package/dist/http/identity.d.ts.map +1 -0
  87. package/dist/http/identity.js +38 -0
  88. package/dist/http/identity.js.map +1 -0
  89. package/dist/http/overview.d.ts +5 -0
  90. package/dist/http/overview.d.ts.map +1 -0
  91. package/dist/http/overview.js +13 -0
  92. package/dist/http/overview.js.map +1 -0
  93. package/dist/http/page.d.ts +3 -2
  94. package/dist/http/page.d.ts.map +1 -1
  95. package/dist/http/page.js +34 -13
  96. package/dist/http/page.js.map +1 -1
  97. package/dist/http/search.d.ts +5 -0
  98. package/dist/http/search.d.ts.map +1 -0
  99. package/dist/http/search.js +11 -0
  100. package/dist/http/search.js.map +1 -0
  101. package/dist/http/work.d.ts +6 -0
  102. package/dist/http/work.d.ts.map +1 -0
  103. package/dist/http/work.js +22 -0
  104. package/dist/http/work.js.map +1 -0
  105. package/dist/metadata/attentionProblems.d.ts +4 -0
  106. package/dist/metadata/attentionProblems.d.ts.map +1 -0
  107. package/dist/metadata/attentionProblems.js +16 -0
  108. package/dist/metadata/attentionProblems.js.map +1 -0
  109. package/dist/metadata/backlinks.d.ts +8 -0
  110. package/dist/metadata/backlinks.d.ts.map +1 -0
  111. package/dist/metadata/backlinks.js +34 -0
  112. package/dist/metadata/backlinks.js.map +1 -0
  113. package/dist/metadata/fields.d.ts +37 -0
  114. package/dist/metadata/fields.d.ts.map +1 -0
  115. package/dist/metadata/fields.js +25 -0
  116. package/dist/metadata/fields.js.map +1 -0
  117. package/dist/metadata/links.d.ts +4 -0
  118. package/dist/metadata/links.d.ts.map +1 -0
  119. package/dist/metadata/links.js +18 -0
  120. package/dist/metadata/links.js.map +1 -0
  121. package/dist/metadata/locations.d.ts +3 -0
  122. package/dist/metadata/locations.d.ts.map +1 -0
  123. package/dist/metadata/locations.js +35 -0
  124. package/dist/metadata/locations.js.map +1 -0
  125. package/dist/metadata/model.d.ts +15 -0
  126. package/dist/metadata/model.d.ts.map +1 -0
  127. package/dist/metadata/model.js +13 -0
  128. package/dist/metadata/model.js.map +1 -0
  129. package/dist/metadata/parse.d.ts +16 -0
  130. package/dist/metadata/parse.d.ts.map +1 -0
  131. package/dist/metadata/parse.js +48 -0
  132. package/dist/metadata/parse.js.map +1 -0
  133. package/dist/metadata/problems.d.ts +4 -0
  134. package/dist/metadata/problems.d.ts.map +1 -0
  135. package/dist/metadata/problems.js +59 -0
  136. package/dist/metadata/problems.js.map +1 -0
  137. package/dist/metadata/relationshipLabel.d.ts +2 -0
  138. package/dist/metadata/relationshipLabel.d.ts.map +1 -0
  139. package/dist/metadata/relationshipLabel.js +14 -0
  140. package/dist/metadata/relationshipLabel.js.map +1 -0
  141. package/dist/metadata/schema.d.ts +44 -0
  142. package/dist/metadata/schema.d.ts.map +1 -0
  143. package/dist/metadata/schema.js +36 -0
  144. package/dist/metadata/schema.js.map +1 -0
  145. package/dist/metadata/sourceLinks.d.ts +5 -0
  146. package/dist/metadata/sourceLinks.d.ts.map +1 -0
  147. package/dist/metadata/sourceLinks.js +44 -0
  148. package/dist/metadata/sourceLinks.js.map +1 -0
  149. package/dist/page/attentionDetails.d.ts +4 -0
  150. package/dist/page/attentionDetails.d.ts.map +1 -0
  151. package/dist/page/attentionDetails.js +13 -0
  152. package/dist/page/attentionDetails.js.map +1 -0
  153. package/dist/page/backlinksHtml.d.ts +3 -0
  154. package/dist/page/backlinksHtml.d.ts.map +1 -0
  155. package/dist/page/backlinksHtml.js +16 -0
  156. package/dist/page/backlinksHtml.js.map +1 -0
  157. package/dist/page/board.d.ts +1 -1
  158. package/dist/page/board.d.ts.map +1 -1
  159. package/dist/page/board.js +14 -4
  160. package/dist/page/board.js.map +1 -1
  161. package/dist/page/client.d.ts +2 -1
  162. package/dist/page/client.d.ts.map +1 -1
  163. package/dist/page/client.js +66 -7
  164. package/dist/page/client.js.map +1 -1
  165. package/dist/page/criteriaEvidence.d.ts +4 -0
  166. package/dist/page/criteriaEvidence.d.ts.map +1 -0
  167. package/dist/page/criteriaEvidence.js +27 -0
  168. package/dist/page/criteriaEvidence.js.map +1 -0
  169. package/dist/page/diagrams.d.ts +1 -1
  170. package/dist/page/diagrams.d.ts.map +1 -1
  171. package/dist/page/diagrams.js +12 -4
  172. package/dist/page/diagrams.js.map +1 -1
  173. package/dist/page/document.d.ts +1 -1
  174. package/dist/page/document.d.ts.map +1 -1
  175. package/dist/page/document.js +6 -3
  176. package/dist/page/document.js.map +1 -1
  177. package/dist/page/libraryScript.d.ts +2 -0
  178. package/dist/page/libraryScript.d.ts.map +1 -0
  179. package/dist/page/libraryScript.js +73 -0
  180. package/dist/page/libraryScript.js.map +1 -0
  181. package/dist/page/metadata.d.ts +5 -0
  182. package/dist/page/metadata.d.ts.map +1 -0
  183. package/dist/page/metadata.js +34 -0
  184. package/dist/page/metadata.js.map +1 -0
  185. package/dist/page/metadataDetails.d.ts +4 -0
  186. package/dist/page/metadataDetails.d.ts.map +1 -0
  187. package/dist/page/metadataDetails.js +23 -0
  188. package/dist/page/metadataDetails.js.map +1 -0
  189. package/dist/page/metadataLinks.d.ts +6 -0
  190. package/dist/page/metadataLinks.d.ts.map +1 -0
  191. package/dist/page/metadataLinks.js +31 -0
  192. package/dist/page/metadataLinks.js.map +1 -0
  193. package/dist/page/nav.d.ts.map +1 -1
  194. package/dist/page/nav.js +3 -3
  195. package/dist/page/nav.js.map +1 -1
  196. package/dist/page/navigationScript.d.ts +2 -0
  197. package/dist/page/navigationScript.d.ts.map +1 -0
  198. package/dist/page/navigationScript.js +104 -0
  199. package/dist/page/navigationScript.js.map +1 -0
  200. package/dist/page/readingStateScript.d.ts +2 -0
  201. package/dist/page/readingStateScript.d.ts.map +1 -0
  202. package/dist/page/readingStateScript.js +62 -0
  203. package/dist/page/readingStateScript.js.map +1 -0
  204. package/dist/page/readingStyles.d.ts +2 -0
  205. package/dist/page/readingStyles.d.ts.map +1 -0
  206. package/dist/page/readingStyles.js +29 -0
  207. package/dist/page/readingStyles.js.map +1 -0
  208. package/dist/page/readingTools.d.ts +3 -0
  209. package/dist/page/readingTools.d.ts.map +1 -0
  210. package/dist/page/readingTools.js +10 -0
  211. package/dist/page/readingTools.js.map +1 -0
  212. package/dist/page/recordedEvidence.d.ts +4 -0
  213. package/dist/page/recordedEvidence.d.ts.map +1 -0
  214. package/dist/page/recordedEvidence.js +10 -0
  215. package/dist/page/recordedEvidence.js.map +1 -0
  216. package/dist/page/searchDialog.d.ts +3 -0
  217. package/dist/page/searchDialog.d.ts.map +1 -0
  218. package/dist/page/searchDialog.js +16 -0
  219. package/dist/page/searchDialog.js.map +1 -0
  220. package/dist/page/searchResultsScript.d.ts +2 -0
  221. package/dist/page/searchResultsScript.d.ts.map +1 -0
  222. package/dist/page/searchResultsScript.js +64 -0
  223. package/dist/page/searchResultsScript.js.map +1 -0
  224. package/dist/page/searchScript.d.ts +2 -0
  225. package/dist/page/searchScript.d.ts.map +1 -0
  226. package/dist/page/searchScript.js +73 -0
  227. package/dist/page/searchScript.js.map +1 -0
  228. package/dist/page/shell.d.ts +1 -1
  229. package/dist/page/shell.d.ts.map +1 -1
  230. package/dist/page/shell.js +23 -5
  231. package/dist/page/shell.js.map +1 -1
  232. package/dist/page/stateScript.d.ts +2 -0
  233. package/dist/page/stateScript.d.ts.map +1 -0
  234. package/dist/page/stateScript.js +55 -0
  235. package/dist/page/stateScript.js.map +1 -0
  236. package/dist/page/style.d.ts.map +1 -1
  237. package/dist/page/style.js +15 -34
  238. package/dist/page/style.js.map +1 -1
  239. package/dist/page/swap.d.ts +1 -1
  240. package/dist/page/swap.d.ts.map +1 -1
  241. package/dist/page/swap.js +4 -1
  242. package/dist/page/swap.js.map +1 -1
  243. package/dist/page/workspaceStyles.d.ts +3 -0
  244. package/dist/page/workspaceStyles.d.ts.map +1 -0
  245. package/dist/page/workspaceStyles.js +79 -0
  246. package/dist/page/workspaceStyles.js.map +1 -0
  247. package/dist/render/documentHeadings.d.ts +667 -0
  248. package/dist/render/documentHeadings.d.ts.map +1 -0
  249. package/dist/render/documentHeadings.js +45 -0
  250. package/dist/render/documentHeadings.js.map +1 -0
  251. package/dist/render/documentLinks.d.ts +659 -0
  252. package/dist/render/documentLinks.d.ts.map +1 -0
  253. package/dist/render/documentLinks.js +19 -0
  254. package/dist/render/documentLinks.js.map +1 -0
  255. package/dist/render/markdown.d.ts +7 -1
  256. package/dist/render/markdown.d.ts.map +1 -1
  257. package/dist/render/markdown.js +13 -2
  258. package/dist/render/markdown.js.map +1 -1
  259. package/dist/search/attentionEntries.d.ts +4 -0
  260. package/dist/search/attentionEntries.d.ts.map +1 -0
  261. package/dist/search/attentionEntries.js +20 -0
  262. package/dist/search/attentionEntries.js.map +1 -0
  263. package/dist/search/collector.d.ts +13 -0
  264. package/dist/search/collector.d.ts.map +1 -0
  265. package/dist/search/collector.js +40 -0
  266. package/dist/search/collector.js.map +1 -0
  267. package/dist/search/entries.d.ts +10 -0
  268. package/dist/search/entries.d.ts.map +1 -0
  269. package/dist/search/entries.js +37 -0
  270. package/dist/search/entries.js.map +1 -0
  271. package/dist/search/linkCollector.d.ts +8 -0
  272. package/dist/search/linkCollector.d.ts.map +1 -0
  273. package/dist/search/linkCollector.js +16 -0
  274. package/dist/search/linkCollector.js.map +1 -0
  275. package/dist/search/match.d.ts +14 -0
  276. package/dist/search/match.d.ts.map +1 -0
  277. package/dist/search/match.js +26 -0
  278. package/dist/search/match.js.map +1 -0
  279. package/dist/search/metadataEntries.d.ts +4 -0
  280. package/dist/search/metadataEntries.d.ts.map +1 -0
  281. package/dist/search/metadataEntries.js +42 -0
  282. package/dist/search/metadataEntries.js.map +1 -0
  283. package/dist/search/snapshot.d.ts +13 -0
  284. package/dist/search/snapshot.d.ts.map +1 -0
  285. package/dist/search/snapshot.js +49 -0
  286. package/dist/search/snapshot.js.map +1 -0
  287. package/dist/views/items.d.ts +11 -0
  288. package/dist/views/items.d.ts.map +1 -0
  289. package/dist/views/items.js +48 -0
  290. package/dist/views/items.js.map +1 -0
  291. package/dist/views/projection.d.ts +15 -0
  292. package/dist/views/projection.d.ts.map +1 -0
  293. package/dist/views/projection.js +47 -0
  294. package/dist/views/projection.js.map +1 -0
  295. package/dist/views/savedScript.d.ts +2 -0
  296. package/dist/views/savedScript.d.ts.map +1 -0
  297. package/dist/views/savedScript.js +89 -0
  298. package/dist/views/savedScript.js.map +1 -0
  299. package/dist/views/state.d.ts +15 -0
  300. package/dist/views/state.d.ts.map +1 -0
  301. package/dist/views/state.js +45 -0
  302. package/dist/views/state.js.map +1 -0
  303. package/dist/views/tools.d.ts +3 -0
  304. package/dist/views/tools.d.ts.map +1 -0
  305. package/dist/views/tools.js +11 -0
  306. package/dist/views/tools.js.map +1 -0
  307. package/dist/views/workCards.d.ts +4 -0
  308. package/dist/views/workCards.d.ts.map +1 -0
  309. package/dist/views/workCards.js +23 -0
  310. package/dist/views/workCards.js.map +1 -0
  311. package/dist/views/workControls.d.ts +4 -0
  312. package/dist/views/workControls.d.ts.map +1 -0
  313. package/dist/views/workControls.js +32 -0
  314. package/dist/views/workControls.js.map +1 -0
  315. package/dist/views/workDetail.d.ts +13 -0
  316. package/dist/views/workDetail.d.ts.map +1 -0
  317. package/dist/views/workDetail.js +26 -0
  318. package/dist/views/workDetail.js.map +1 -0
  319. package/dist/views/workHtml.d.ts +15 -0
  320. package/dist/views/workHtml.d.ts.map +1 -0
  321. package/dist/views/workHtml.js +35 -0
  322. package/dist/views/workHtml.js.map +1 -0
  323. package/dist/views/workStyle.d.ts +2 -0
  324. package/dist/views/workStyle.d.ts.map +1 -0
  325. package/dist/views/workStyle.js +18 -0
  326. package/dist/views/workStyle.js.map +1 -0
  327. package/dist/views/workTable.d.ts +4 -0
  328. package/dist/views/workTable.d.ts.map +1 -0
  329. package/dist/views/workTable.js +19 -0
  330. package/dist/views/workTable.js.map +1 -0
  331. package/package.json +14 -10
  332. package/src/attention/overviewHtml.ts +44 -0
  333. package/src/attention/requests.ts +64 -0
  334. package/src/attention/style.ts +9 -0
  335. package/src/board.ts +16 -3
  336. package/src/cli.ts +1 -1
  337. package/src/files/changes.ts +6 -17
  338. package/src/files/home.ts +4 -0
  339. package/src/files/list.ts +25 -14
  340. package/src/files/markdownPath.ts +31 -0
  341. package/src/files/url.ts +3 -0
  342. package/src/files/watchDirectories.ts +60 -0
  343. package/src/history/baseline.ts +43 -0
  344. package/src/history/compare.ts +54 -0
  345. package/src/history/documents.ts +21 -0
  346. package/src/history/fields.ts +32 -0
  347. package/src/history/html.ts +23 -0
  348. package/src/history/limits.ts +2 -0
  349. package/src/history/response.ts +23 -0
  350. package/src/history/schema.ts +36 -0
  351. package/src/history/script.ts +112 -0
  352. package/src/http/assets.ts +18 -1
  353. package/src/http/history.ts +50 -0
  354. package/src/http/identity.ts +62 -0
  355. package/src/http/overview.ts +19 -0
  356. package/src/http/page.ts +52 -15
  357. package/src/http/search.ts +13 -0
  358. package/src/http/work.ts +41 -0
  359. package/src/metadata/attentionProblems.ts +18 -0
  360. package/src/metadata/backlinks.ts +46 -0
  361. package/src/metadata/fields.ts +26 -0
  362. package/src/metadata/links.ts +19 -0
  363. package/src/metadata/locations.ts +34 -0
  364. package/src/metadata/model.ts +27 -0
  365. package/src/metadata/parse.ts +62 -0
  366. package/src/metadata/problems.ts +66 -0
  367. package/src/metadata/relationshipLabel.ts +14 -0
  368. package/src/metadata/schema.ts +45 -0
  369. package/src/metadata/sourceLinks.ts +46 -0
  370. package/src/page/attentionDetails.ts +18 -0
  371. package/src/page/backlinksHtml.ts +18 -0
  372. package/src/page/board.ts +14 -4
  373. package/src/page/client.ts +66 -7
  374. package/src/page/criteriaEvidence.ts +36 -0
  375. package/src/page/diagrams.ts +12 -4
  376. package/src/page/document.ts +6 -3
  377. package/src/page/libraryScript.ts +72 -0
  378. package/src/page/metadata.ts +40 -0
  379. package/src/page/metadataDetails.ts +26 -0
  380. package/src/page/metadataLinks.ts +35 -0
  381. package/src/page/nav.ts +5 -4
  382. package/src/page/navigationScript.ts +103 -0
  383. package/src/page/readingStateScript.ts +61 -0
  384. package/src/page/readingStyles.ts +28 -0
  385. package/src/page/readingTools.ts +12 -0
  386. package/src/page/recordedEvidence.ts +12 -0
  387. package/src/page/searchDialog.ts +16 -0
  388. package/src/page/searchResultsScript.ts +63 -0
  389. package/src/page/searchScript.ts +72 -0
  390. package/src/page/shell.ts +32 -5
  391. package/src/page/stateScript.ts +54 -0
  392. package/src/page/style.ts +15 -35
  393. package/src/page/swap.ts +4 -1
  394. package/src/page/workspaceStyles.ts +79 -0
  395. package/src/render/documentHeadings.ts +52 -0
  396. package/src/render/documentLinks.ts +19 -0
  397. package/src/render/markdown.ts +21 -4
  398. package/src/search/attentionEntries.ts +22 -0
  399. package/src/search/collector.ts +44 -0
  400. package/src/search/entries.ts +51 -0
  401. package/src/search/linkCollector.ts +16 -0
  402. package/src/search/match.ts +28 -0
  403. package/src/search/metadataEntries.ts +44 -0
  404. package/src/search/snapshot.ts +59 -0
  405. package/src/views/items.ts +55 -0
  406. package/src/views/projection.ts +59 -0
  407. package/src/views/savedScript.ts +88 -0
  408. package/src/views/state.ts +47 -0
  409. package/src/views/tools.ts +11 -0
  410. package/src/views/workCards.ts +26 -0
  411. package/src/views/workControls.ts +38 -0
  412. package/src/views/workDetail.ts +28 -0
  413. package/src/views/workHtml.ts +39 -0
  414. package/src/views/workStyle.ts +17 -0
  415. package/src/views/workTable.ts +21 -0
  416. package/dist/index.d.ts +0 -2
  417. package/dist/index.d.ts.map +0 -1
  418. package/dist/index.js +0 -2
  419. package/dist/index.js.map +0 -1
  420. package/src/index.ts +0 -1
@@ -0,0 +1,64 @@
1
+ import { identityUrl, referenceTarget } from "#metadata/links.ts";
2
+ import type { MetadataDocument } from "#metadata/model.ts";
3
+ import type { Metadata } from "#metadata/schema.ts";
4
+ import type { Snapshot } from "#search/snapshot.ts";
5
+
6
+ export type Request = NonNullable<Metadata["attention"]>[number];
7
+ export interface AttentionEntry {
8
+ readonly document: MetadataDocument;
9
+ readonly href: string;
10
+ readonly itemId: string;
11
+ readonly line: number;
12
+ readonly request: Request;
13
+ readonly title: string;
14
+ }
15
+ export interface SourceIssue {
16
+ readonly file: string;
17
+ readonly line: number;
18
+ readonly message: string;
19
+ }
20
+
21
+ function requestsFor(document: MetadataDocument, snapshot: Snapshot, title: string): readonly AttentionEntry[] {
22
+ const { fields, lines } = document.parsed;
23
+ const itemId = fields.id;
24
+ if (!itemId || snapshot.unavailable.length > 0 || snapshot.model.ids.get(itemId)?.length !== 1) {
25
+ return [];
26
+ }
27
+ const requests = fields.attention ?? [];
28
+ return requests.flatMap((request, index) => {
29
+ if (
30
+ request.state !== "open"
31
+ || requests.filter((other) => other.id === request.id).length !== 1
32
+ || request.unblocks.some((target) => !referenceTarget(target, snapshot.model))
33
+ ) {
34
+ return [];
35
+ }
36
+ return [
37
+ {
38
+ document,
39
+ href: `${identityUrl(itemId)}#attention-request-${encodeURIComponent(request.id)}`,
40
+ itemId,
41
+ line: lines[`attention.${index}`] ?? lines.attention ?? 2,
42
+ request,
43
+ title,
44
+ },
45
+ ];
46
+ });
47
+ }
48
+
49
+ export function attentionRequests(snapshot: Snapshot) {
50
+ const titles = new Map(snapshot.entries.filter((entry) => entry.kind === "document").map((entry) => [entry.file, entry.title]));
51
+ const issues: SourceIssue[] = snapshot.documents.flatMap((source) =>
52
+ (snapshot.model.diagnostics.get(source.file) ?? source.parsed.diagnostics).map((problem) => ({ ...problem, file: source.file })),
53
+ );
54
+ const entries = snapshot.documents.flatMap((source) => requestsFor(source, snapshot, titles.get(source.file) ?? source.file));
55
+ const collator = new Intl.Collator("en", { numeric: true, sensitivity: "base" });
56
+ entries.sort((a, b) => collator.compare(a.title, b.title) || a.itemId.localeCompare(b.itemId) || a.request.id.localeCompare(b.request.id));
57
+ return { complete: snapshot.unavailable.length === 0, entries, issues };
58
+ }
59
+
60
+ export function attentionSource(document: MetadataDocument, index: number): string {
61
+ const requests = document.parsed.fields.attention ?? [];
62
+ const id = requests[index]?.id;
63
+ return id && requests.filter((request) => request.id === id).length === 1 ? `attention-request-${id}` : `attention-record-${index}`;
64
+ }
@@ -0,0 +1,9 @@
1
+ export function attentionStyle(): string {
2
+ return `
3
+ .attention-total{margin:24px 0 8px;font-weight:600}.attention-order,.attention-footnote{font-size:13px;color:var(--muted);margin:0 0 24px;overflow-wrap:anywhere}
4
+ .attention-groups{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px;align-items:start}.attention-group{min-width:0}.attention-group>h2{display:flex;justify-content:space-between;gap:12px;font-size:16px;margin:0 0 16px}.attention-group>h2>span{font:12px var(--mono);color:var(--muted)}
5
+ .attention-card{border:1px solid var(--border);border-radius:10px;padding:var(--card-space);background:var(--card);margin:0 0 16px;overflow-wrap:anywhere}.attention-card h3{font-size:1rem;margin:6px 0}.attention-card h3 a{text-decoration:none}.attention-card dl{font-size:13px;margin:16px 0}.attention-card dt{color:var(--muted);margin-top:12px}.attention-card dd{margin:4px 0}.attention-source{font-size:12px}.attention-reason{font-size:14px;margin:0}.attention-footnote{margin-top:24px}.attention-incomplete{padding:16px;border:1px solid var(--danger);border-radius:8px;margin:24px 0;overflow-wrap:anywhere}
6
+ [id^="attention-request-"],[id^="attention-record-"]{scroll-margin-top:20px}
7
+ @media(max-width:1100px){.attention-groups{grid-template-columns:minmax(0,1fr)}}
8
+ `;
9
+ }
package/src/board.ts CHANGED
@@ -4,11 +4,17 @@ import { watchChanges } from "#files/changes.ts";
4
4
  import { type HomeMissing, homeIn } from "#files/home.ts";
5
5
  import { ASSETS, MERMAID_ROUTE, type MermaidMissing, mermaidFile, mermaidRoot } from "#http/assets.ts";
6
6
  import { events } from "#http/events.ts";
7
+ import { changesPage, history } from "#http/history.ts";
8
+ import { identity } from "#http/identity.ts";
7
9
  import { loopbackOnly } from "#http/loopback.ts";
10
+ import { overview } from "#http/overview.ts";
8
11
  import { page } from "#http/page.ts";
9
12
  import { respond } from "#http/respond.ts";
13
+ import { search } from "#http/search.ts";
14
+ import { work } from "#http/work.ts";
10
15
  import type { RenderFailed } from "#render/failed.ts";
11
16
  import { Highlighter } from "#render/highlighter.ts";
17
+ import { searchSnapshot } from "#search/snapshot.ts";
12
18
 
13
19
  export interface BoardOptions {
14
20
  readonly home?: string | undefined;
@@ -25,18 +31,25 @@ const routes = (options: BoardOptions) =>
25
31
  const mermaid = yield* mermaidRoot();
26
32
  const changes = yield* watchChanges(options.root);
27
33
  const context = yield* Effect.context<Services>();
28
- const serve = <E>(route: HttpRouter.PathInput, handler: Handler<E>) =>
34
+ const serve = <E>(route: HttpRouter.PathInput, handler: Handler<E>, method: "GET" | "POST" = "GET") =>
29
35
  router.add(
30
- "GET",
36
+ method,
31
37
  route,
32
38
  loopbackOnly((request) => Effect.provideContext(handler(request), context)),
33
39
  );
34
40
  yield* serve("/events", events(changes));
41
+ const index = yield* searchSnapshot(options.root, home, changes);
42
+ yield* serve("/_board/search", search(index));
43
+ yield* serve("/_board/work", work(index, changes, home));
44
+ yield* serve("/_board/overview", overview(index, changes, home));
45
+ yield* serve("/_board/changes", changesPage(changes, home));
46
+ yield* serve("/_board/history", history(index), "POST");
35
47
  for (const [route, body, contentType] of ASSETS) {
36
48
  yield* serve(route, () => Effect.succeed(respond(body, contentType)));
37
49
  }
38
50
  yield* serve(MERMAID_ROUTE, mermaidFile(mermaid));
39
- const pages = page({ home, root: options.root }, changes);
51
+ const pages = page({ home, root: options.root }, changes, index);
52
+ yield* serve("/_board/item/*", identity(index, pages, changes, home));
40
53
  yield* serve("/", pages);
41
54
  yield* serve("/*", pages);
42
55
  }),
package/src/cli.ts CHANGED
@@ -4,7 +4,7 @@ import { Effect, Layer, Option, Path } from "effect";
4
4
  import { Argument, Command, Flag } from "effect/unstable/cli";
5
5
  import { HOST, serveBoard } from "./serve.ts";
6
6
 
7
- const VERSION = "0.2.1";
7
+ const VERSION = "0.4.0";
8
8
 
9
9
  const serve = Effect.fn("WorkBoard.serve")(function* (input: { readonly dir: string; readonly port: number; readonly home: Option.Option<string> }) {
10
10
  const path = yield* Path.Path;
@@ -1,5 +1,6 @@
1
- import { Data, Deferred, Effect, FileSystem, Option, Path, type PlatformError, PubSub, Ref, Schedule, Stream } from "effect";
2
- import { isMarkdown, listMarkdown, type MarkdownFile } from "./list.ts";
1
+ import { Deferred, Effect, FileSystem, Option, Path, type PlatformError, PubSub, Ref, Schedule } from "effect";
2
+ import { listMarkdown, type MarkdownFile } from "./list.ts";
3
+ import { watchDirectories } from "./watchDirectories.ts";
3
4
 
4
5
  export type Change = { readonly _tag: "Changed"; readonly paths: readonly string[] } | { readonly _tag: "Watching"; readonly watching: boolean };
5
6
 
@@ -7,6 +8,7 @@ export interface Changes {
7
8
  readonly events: PubSub.PubSub<Change>;
8
9
  readonly files: Effect.Effect<readonly MarkdownFile[], PlatformError.PlatformError>;
9
10
  readonly realRoot: string;
11
+ readonly revision: Effect.Effect<number>;
10
12
  readonly watching: Ref.Ref<boolean>;
11
13
  }
12
14
 
@@ -15,10 +17,6 @@ interface Cached {
15
17
  readonly generation: number;
16
18
  }
17
19
 
18
- class WatchEnded extends Data.TaggedError("WatchEnded") {}
19
-
20
- const SETTLE = "100 millis";
21
- const BATCH = 256;
22
20
  const BACKLOG = 16;
23
21
  const RETRY = Schedule.min([Schedule.exponential("100 millis"), Schedule.spaced("5 seconds")]);
24
22
 
@@ -32,7 +30,6 @@ export const watchChanges = Effect.fn("WorkBoard.watchChanges")(function* (root:
32
30
  const cached = yield* Ref.make(Option.none<Cached>());
33
31
  const settled = yield* Deferred.make<void>();
34
32
  const stale = Ref.update(generation, (count) => count + 1);
35
- const relative = (changed: string) => (path.isAbsolute(changed) ? path.relative(root, changed) : changed).split(path.sep).join("/");
36
33
  const setWatching = (now: boolean) =>
37
34
  Effect.andThen(
38
35
  Effect.flatMap(Ref.getAndSet(watching, now), (was) =>
@@ -40,15 +37,7 @@ export const watchChanges = Effect.fn("WorkBoard.watchChanges")(function* (root:
40
37
  ),
41
38
  Deferred.succeed(settled, undefined),
42
39
  );
43
- const watchOnce = fs.watch(root, { recursive: true }).pipe(
44
- Stream.tap(() => stale),
45
- Stream.map((event) => relative(event.path)),
46
- Stream.filter(isMarkdown),
47
- Stream.groupedWithin(BATCH, SETTLE),
48
- Stream.runForEach((paths) => PubSub.publish(events, { _tag: "Changed", paths: [...new Set(paths)] })),
49
- Effect.andThen(Effect.fail(new WatchEnded())),
50
- );
51
- const attempt = Effect.scoped(Effect.andThen(Effect.forkScoped(Effect.delay(setWatching(true), SETTLE)), watchOnce));
40
+ const attempt = watchDirectories(root, realRoot, stale, setWatching, (paths) => PubSub.publish(events, { _tag: "Changed", paths }));
52
41
  yield* attempt.pipe(
53
42
  Effect.sandbox,
54
43
  Effect.tapError((cause) => Effect.andThen(setWatching(false), Effect.logWarning("Watching the folder failed; retrying", cause))),
@@ -66,6 +55,6 @@ export const watchChanges = Effect.fn("WorkBoard.watchChanges")(function* (root:
66
55
  yield* Ref.set(cached, Option.some({ files: listed, generation: now }));
67
56
  return listed;
68
57
  });
69
- const changes: Changes = { events, files, realRoot, watching };
58
+ const changes: Changes = { events, files, realRoot, revision: Ref.get(generation), watching };
70
59
  return changes;
71
60
  });
package/src/files/home.ts CHANGED
@@ -20,6 +20,10 @@ export const homeIn = Effect.fn("WorkBoard.homeIn")(function* (root: string, hom
20
20
  return yield* missing;
21
21
  }
22
22
  const files = yield* listMarkdown(root, realRoot);
23
+ const logical = path.relative(root, path.resolve(root, home)).split(path.sep).join("/");
24
+ if (files.some((file) => file.path === logical)) {
25
+ return logical;
26
+ }
23
27
  const relative = path.relative(realRoot, real.value).split(path.sep).join("/");
24
28
  if (files.some((file) => file.path === relative)) {
25
29
  return relative;
package/src/files/list.ts CHANGED
@@ -17,14 +17,14 @@ const byFolder = (left: string, right: string): number => folderOf(left).localeC
17
17
 
18
18
  type Listing = Effect.Effect<readonly MarkdownFile[], PlatformError.PlatformError>;
19
19
 
20
- export const listMarkdown = Effect.fn("WorkBoard.listMarkdown")(function* (root: string, realRoot: string) {
20
+ export const scanMarkdown = Effect.fn("WorkBoard.scanMarkdown")(function* (root: string, realRoot: string) {
21
21
  const fs = yield* FileSystem.FileSystem;
22
22
  const path = yield* Path.Path;
23
- const visited = new Set<string>();
24
- const entry = (relative: string): Listing =>
23
+ const roots: Array<{ readonly real: string; readonly relative: string }> = [{ real: realRoot, relative: "" }];
24
+ const entry = (relative: string, boundary: string, ancestors: ReadonlySet<string>, parent: string): Listing =>
25
25
  Effect.gen(function* () {
26
26
  const real = yield* Effect.option(fs.realPath(path.join(root, relative)));
27
- if (Option.isNone(real) || !within(realRoot, path.sep, real.value)) {
27
+ if (Option.isNone(real)) {
28
28
  return [];
29
29
  }
30
30
  const info = yield* Effect.option(fs.stat(real.value));
@@ -32,24 +32,35 @@ export const listMarkdown = Effect.fn("WorkBoard.listMarkdown")(function* (root:
32
32
  return [];
33
33
  }
34
34
  if (info.value.type === "Directory") {
35
- return yield* Effect.orElseSucceed(folder(relative, real.value), () => []);
35
+ return yield* directory(relative, real.value, boundary, ancestors, real.value !== path.join(parent, path.basename(relative)));
36
36
  }
37
37
  const modified = Option.match(info.value.mtime, { onNone: () => 0, onSome: (date) => date.getTime() });
38
- return info.value.type === "File" && relative.endsWith(".md") ? [{ modified, path: relative }] : [];
38
+ return info.value.type === "File" && relative.endsWith(".md") && within(boundary, path.sep, real.value) ? [{ modified, path: relative }] : [];
39
39
  });
40
- const folder = (relative: string, real: string): Listing =>
40
+ function directory(relative: string, real: string, boundary: string, ancestors: ReadonlySet<string>, alias: boolean): Listing {
41
+ if (ancestors.has(real) || within(real, path.sep, realRoot)) {
42
+ return Effect.succeed([]);
43
+ }
44
+ const linked = !within(boundary, path.sep, real);
45
+ if (alias) {
46
+ roots.push({ real, relative });
47
+ }
48
+ return Effect.orElseSucceed(folder(relative, real, linked ? real : boundary, ancestors), () => []);
49
+ }
50
+ const folder = (relative: string, real: string, boundary: string, ancestors: ReadonlySet<string>): Listing =>
41
51
  Effect.gen(function* () {
42
- if (visited.has(real)) {
43
- return [];
44
- }
45
- visited.add(real);
52
+ const branch = new Set([...ancestors, real]);
46
53
  const names = yield* fs.readDirectory(real);
47
54
  const found = yield* Effect.forEach(
48
55
  names.filter((name) => !skipped(name)),
49
- (name) => entry(relative === "" ? name : `${relative}/${name}`),
56
+ (name) => entry(relative === "" ? name : `${relative}/${name}`, boundary, branch, real),
50
57
  );
51
58
  return found.flat();
52
59
  });
53
- const files = yield* folder("", realRoot);
54
- return files.toSorted((left, right) => byFolder(left.path, right.path));
60
+ const files = yield* folder("", realRoot, realRoot, new Set());
61
+ return { files: files.toSorted((left, right) => byFolder(left.path, right.path)), roots };
62
+ });
63
+
64
+ export const listMarkdown = Effect.fn("WorkBoard.listMarkdown")(function* (root: string, realRoot: string) {
65
+ return (yield* scanMarkdown(root, realRoot)).files;
55
66
  });
@@ -0,0 +1,31 @@
1
+ import { Effect, FileSystem, Option, Path } from "effect";
2
+ import { isMarkdown, within } from "./list.ts";
3
+
4
+ export const markdownPath = Effect.fn("WorkBoard.markdownPath")(function* (root: string, realRoot: string, relative: string) {
5
+ if (!isMarkdown(relative) || relative.split("/").some((segment) => segment === "" || segment === "..")) {
6
+ return undefined;
7
+ }
8
+ const fs = yield* FileSystem.FileSystem;
9
+ const path = yield* Path.Path;
10
+ const segments = relative.split("/");
11
+ const ancestors = new Set([realRoot]);
12
+ let boundary = realRoot;
13
+ for (let index = 0; index < segments.length; index += 1) {
14
+ const real = yield* Effect.option(fs.realPath(path.join(root, ...segments.slice(0, index + 1))));
15
+ if (Option.isNone(real)) {
16
+ return undefined;
17
+ }
18
+ if (index === segments.length - 1) {
19
+ return within(boundary, path.sep, real.value) ? real.value : undefined;
20
+ }
21
+ const info = yield* Effect.option(fs.stat(real.value));
22
+ if (Option.isNone(info) || info.value.type !== "Directory" || ancestors.has(real.value) || within(real.value, path.sep, realRoot)) {
23
+ return undefined;
24
+ }
25
+ ancestors.add(real.value);
26
+ if (!within(boundary, path.sep, real.value)) {
27
+ boundary = real.value;
28
+ }
29
+ }
30
+ return undefined;
31
+ });
@@ -0,0 +1,3 @@
1
+ export function fileUrl(file: string): string {
2
+ return `/${file.split("/").map(encodeURIComponent).join("/")}`;
3
+ }
@@ -0,0 +1,60 @@
1
+ import { Data, Deferred, Effect, FileSystem, Option, Path, Stream } from "effect";
2
+ import { isMarkdown, scanMarkdown } from "./list.ts";
3
+
4
+ class WatchEnded extends Data.TaggedError("WatchEnded") {}
5
+
6
+ const SETTLE = "100 millis";
7
+ const BATCH = 256;
8
+
9
+ export const watchDirectories = Effect.fn("WorkBoard.watchDirectories")(function* (
10
+ root: string,
11
+ realRoot: string,
12
+ stale: Effect.Effect<void>,
13
+ setWatching: (watching: boolean) => Effect.Effect<unknown>,
14
+ publish: (paths: readonly string[]) => Effect.Effect<unknown>,
15
+ ) {
16
+ const fs = yield* FileSystem.FileSystem;
17
+ const path = yield* Path.Path;
18
+ const cycle = Effect.scoped(
19
+ Effect.gen(function* () {
20
+ const { roots } = yield* scanMarkdown(root, realRoot);
21
+ const topology = yield* Deferred.make<void>();
22
+ // Node's recursive watcher can miss replacement of directory symlinks.
23
+ const parents = yield* Effect.forEach(
24
+ roots.filter((folder) => folder.relative !== ""),
25
+ (folder) =>
26
+ Effect.map(fs.realPath(path.dirname(path.join(root, folder.relative))), (real) => ({ real, relative: path.dirname(folder.relative) })),
27
+ );
28
+ const watched = [...roots.map((folder) => ({ ...folder, recursive: true })), ...parents.map((folder) => ({ ...folder, recursive: false }))];
29
+ const streams = watched.map((folder) =>
30
+ fs.watch(folder.real, { recursive: folder.recursive }).pipe(
31
+ Stream.map((event) => {
32
+ const local = path.isAbsolute(event.path) ? path.relative(folder.real, event.path) : event.path;
33
+ return path.join(folder.relative, local).split(path.sep).join("/");
34
+ }),
35
+ ),
36
+ );
37
+ const watch = Stream.mergeAll(streams, { concurrency: "unbounded" }).pipe(
38
+ Stream.tap(() => stale),
39
+ Stream.tap((relative) =>
40
+ Effect.gen(function* () {
41
+ const info = yield* Effect.option(fs.stat(path.join(root, relative)));
42
+ const removedLink = roots.some((folder) => folder.relative === relative || folder.relative.startsWith(`${relative}/`));
43
+ if (Option.match(info, { onNone: () => removedLink, onSome: (value) => value.type === "Directory" })) {
44
+ yield* Effect.forkScoped(Effect.delay(Deferred.succeed(topology, undefined), SETTLE));
45
+ }
46
+ }),
47
+ ),
48
+ Stream.filter(isMarkdown),
49
+ Stream.interruptWhen(Deferred.await(topology)),
50
+ Stream.groupedWithin(BATCH, SETTLE),
51
+ Stream.runForEach((paths) => publish([...new Set(paths)])),
52
+ Effect.andThen(Effect.flatMap(Deferred.isDone(topology), (changed) => (changed ? Effect.void : Effect.fail(new WatchEnded())))),
53
+ );
54
+ yield* Effect.forkScoped(Effect.delay(setWatching(true), SETTLE));
55
+ yield* watch;
56
+ yield* setWatching(false);
57
+ }),
58
+ );
59
+ yield* Effect.forever(cycle);
60
+ });
@@ -0,0 +1,43 @@
1
+ import { Option, Schema } from "effect";
2
+ import type { Snapshot } from "#search/snapshot.ts";
3
+ import { MAX_AGE, MAX_BYTES } from "./limits.ts";
4
+ import { Baseline } from "./schema.ts";
5
+
6
+ const JsonBaseline = Schema.fromJsonString(Baseline);
7
+ function byteLength(text: string): number {
8
+ return new TextEncoder().encode(text).length;
9
+ }
10
+
11
+ export function readBaseline(raw: string | null, now: number) {
12
+ if (raw === null) {
13
+ return { baseline: undefined, discard: false, reason: "No previous snapshot. Start remembering changes to compare future source observations." };
14
+ }
15
+ if (byteLength(raw) > MAX_BYTES) {
16
+ return { baseline: undefined, discard: true, reason: "Remembered snapshot exceeds 2 MiB; history is unavailable." };
17
+ }
18
+ const decoded = Schema.decodeUnknownOption(JsonBaseline, { onExcessProperty: "error" })(raw);
19
+ if (Option.isNone(decoded)) {
20
+ return { baseline: undefined, discard: true, reason: "Remembered snapshot is invalid or unsupported; history is unavailable." };
21
+ }
22
+ const elapsed = now - Date.parse(decoded.value.recordedAt);
23
+ if (elapsed < 0) {
24
+ return { baseline: undefined, discard: true, reason: "Remembered observation is in the future; history is unavailable." };
25
+ }
26
+ if (elapsed >= MAX_AGE) {
27
+ return { baseline: undefined, discard: true, reason: "Remembered snapshot expired after 30 days; start again to establish a new baseline." };
28
+ }
29
+ return { baseline: decoded.value, discard: false, reason: "" };
30
+ }
31
+
32
+ export function observeBaseline(snapshot: Snapshot, now: number): string | undefined {
33
+ if (snapshot.unavailable.length > 0) {
34
+ return undefined;
35
+ }
36
+ const baseline: Baseline = {
37
+ documents: snapshot.documents.map((document) => ({ file: document.file, source: (document.parsed.raw ?? "") + document.parsed.body })),
38
+ recordedAt: new Date(now).toISOString(),
39
+ version: 1,
40
+ };
41
+ const raw = Schema.encodeSync(JsonBaseline)(baseline);
42
+ return byteLength(raw) <= MAX_BYTES ? raw : undefined;
43
+ }
@@ -0,0 +1,54 @@
1
+ import { type HistoryDocument, historyDocuments, historyIdentities } from "./documents.ts";
2
+ import { historyFields } from "./fields.ts";
3
+ import type { Baseline } from "./schema.ts";
4
+
5
+ export interface SourceChange {
6
+ readonly after?: HistoryDocument;
7
+ readonly before?: HistoryDocument;
8
+ readonly kind: "added" | "removed" | "changed";
9
+ readonly reasons: readonly string[];
10
+ }
11
+
12
+ export function compareHistory(baseline: Baseline, current: Baseline) {
13
+ const before = historyDocuments(baseline);
14
+ const after = historyDocuments(current);
15
+ const oldIds = historyIdentities(before);
16
+ const newIds = historyIdentities(after);
17
+ const paired = new Set<HistoryDocument>();
18
+ const remaining = new Set(after);
19
+ const changes: SourceChange[] = [];
20
+ function pair(old: HistoryDocument, next: HistoryDocument) {
21
+ paired.add(old);
22
+ remaining.delete(next);
23
+ if (old.source !== next.source || old.file !== next.file) {
24
+ changes.push({ after: next, before: old, kind: "changed", reasons: historyFields(old, next) });
25
+ }
26
+ }
27
+ for (const [id, old] of oldIds) {
28
+ const next = newIds.get(id);
29
+ if (old && next) {
30
+ pair(old, next);
31
+ }
32
+ }
33
+ const byFile = new Map([...remaining].map((document) => [document.file, document]));
34
+ for (const old of before) {
35
+ if (paired.has(old)) {
36
+ continue;
37
+ }
38
+ const next = byFile.get(old.file);
39
+ if (next) {
40
+ pair(old, next);
41
+ } else {
42
+ changes.push({ before: old, kind: "removed", reasons: ["Source absent from the complete current observation"] });
43
+ }
44
+ }
45
+ for (const next of remaining) {
46
+ changes.push({ after: next, kind: "added", reasons: ["Source added since the remembered observation"] });
47
+ }
48
+ const issues = [
49
+ ...[...oldIds].filter(([, document]) => !document).map(([id]) => `Duplicate ID ${id} in remembered sources; no identity winner selected.`),
50
+ ...[...newIds].filter(([, document]) => !document).map(([id]) => `Duplicate ID ${id} in current sources; no identity winner selected.`),
51
+ ];
52
+ changes.sort((a, b) => (a.after?.file ?? a.before?.file ?? "").localeCompare(b.after?.file ?? b.before?.file ?? "", "en"));
53
+ return { changes, issues, newIds };
54
+ }
@@ -0,0 +1,21 @@
1
+ import { metadataParse, type ParsedMetadata } from "#metadata/parse.ts";
2
+ import type { Baseline } from "./schema.ts";
3
+
4
+ export interface HistoryDocument {
5
+ readonly file: string;
6
+ readonly parsed: ParsedMetadata;
7
+ readonly source: string;
8
+ }
9
+ export function historyDocuments(baseline: Baseline): readonly HistoryDocument[] {
10
+ return baseline.documents.map((document) => ({ ...document, parsed: metadataParse(document.source) }));
11
+ }
12
+ export function historyIdentities(documents: readonly HistoryDocument[]): ReadonlyMap<string, HistoryDocument | undefined> {
13
+ const ids = new Map<string, HistoryDocument | undefined>();
14
+ for (const document of documents) {
15
+ const id = document.parsed.fields.id;
16
+ if (id) {
17
+ ids.set(id, ids.has(id) ? undefined : document);
18
+ }
19
+ }
20
+ return ids;
21
+ }
@@ -0,0 +1,32 @@
1
+ import type { Metadata } from "#metadata/schema.ts";
2
+ import type { HistoryDocument } from "./documents.ts";
3
+
4
+ const labels: ReadonlyArray<readonly [keyof Metadata, string]> = [
5
+ ["attention", "Recorded attention requests changed"],
6
+ ["criteria", "Criteria changed"],
7
+ ["evidence", "Recorded evidence changed (not verified acceptance)"],
8
+ ["id", "Item ID changed"],
9
+ ["items", "Declared board membership changed"],
10
+ ["kind", "Item kind changed"],
11
+ ["nextAction", "Next action changed"],
12
+ ["owner", "Recorded owner changed (not authorship)"],
13
+ ["relationships", "Explicit relationships changed"],
14
+ ["status", "Recorded status changed (not acceptance)"],
15
+ ];
16
+
17
+ export function historyFields(before: HistoryDocument, after: HistoryDocument): readonly string[] {
18
+ const changes = labels.flatMap(([field, label]) =>
19
+ JSON.stringify(before.parsed.fields[field]) === JSON.stringify(after.parsed.fields[field]) ? [] : [label],
20
+ );
21
+ if (before.parsed.body !== after.parsed.body) {
22
+ const decision = before.parsed.fields.kind === "decision" || after.parsed.fields.kind === "decision";
23
+ changes.push(decision ? "Decision Markdown changed" : "Markdown body changed");
24
+ }
25
+ if (changes.length === 0 && before.source !== after.source) {
26
+ changes.push("Other frontmatter content or source formatting changed; no further interpretation");
27
+ }
28
+ if (before.file !== after.file) {
29
+ changes.unshift("Source path changed for the same unique item ID");
30
+ }
31
+ return changes;
32
+ }
@@ -0,0 +1,23 @@
1
+ import { fileUrl } from "#files/url.ts";
2
+ import { escapeHtml } from "#page/escape.ts";
3
+ import { compareHistory, type SourceChange } from "./compare.ts";
4
+ import type { Baseline } from "./schema.ts";
5
+
6
+ function source(label: string, text: string, key: string): string {
7
+ return `<details data-key="${escapeHtml(key)}:${label}" data-history-source="${label.toLowerCase()}"><summary>${label} source (plain text)</summary><pre class="history-source">${escapeHtml(text)}</pre></details>`;
8
+ }
9
+ function changeHtml(change: SourceChange): string {
10
+ const file = change.after?.file ?? change.before?.file ?? "";
11
+ const key = change.before ? `before:${change.before.file}` : `added:${file}`;
12
+ return `<article class="history-change" data-key="${escapeHtml(key)}"><h2>${change.kind}: ${escapeHtml(file)}</h2>${change.before && change.after && change.before.file !== file ? `<p>Previously ${escapeHtml(change.before.file)}</p>` : ""}<ul>${change.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join("")}</ul>${change.after ? `<a href="${escapeHtml(fileUrl(change.after.file))}">Read current source</a>` : "<p>Removed source; no current document link.</p>"}${change.before ? source("Remembered", change.before.source, key) : ""}${change.after ? source("Current", change.after.source, key) : ""}</article>`;
13
+ }
14
+ export function historyHtml(before: Baseline, current: Baseline): string {
15
+ const result = compareHistory(before, current);
16
+ const counts = ["added", "removed", "changed"]
17
+ .map((kind) => `${result.changes.filter((change) => change.kind === kind).length} ${kind}`)
18
+ .join(" · ");
19
+ return `<p>Compared with your observation at <time>${escapeHtml(before.recordedAt)}</time>.</p><p role="status">${counts}</p>${result.changes.length === 0 ? "<p>No source changes since that observation.</p>" : ""}${result.issues.length > 0 ? `<aside aria-label="Identity limitations"><ul>${result.issues.map((issue) => `<li>${escapeHtml(issue)}</li>`).join("")}</ul><p>Ambiguous identities are compared by source path only.</p></aside>` : ""}${result.changes.map(changeHtml).join("")}`;
20
+ }
21
+ export function changesHtml(): string {
22
+ return `<section class="history"><h1>Changes since seen</h1><p>Compare current Markdown with one observation you explicitly remember in this browser. This is an observed source comparison, not an event journal, Git history, authorship, or verified acceptance.</p><p>Remembered source can include old or deleted content. One snapshot per workspace, up to 2 MiB, expires after 30 days and is removed on your next workspace visit. Mark seen replaces it; Clear forgets it. No source writes or cloud storage.</p><div class="history-controls"><button id="history-mark" type="button" disabled>Start remembering changes</button><button id="history-clear" type="button" disabled>Clear remembered history</button><span id="history-storage" role="status"></span></div><noscript><p>Remembering and comparing changes requires JavaScript and browser storage. Markdown, Overview and Work remain readable.</p></noscript><div id="changes-content" aria-live="polite"><p>No comparison has been loaded.</p></div></section><style>.history-controls{display:flex;flex-wrap:wrap;gap:.6rem;align-items:center}.history-change{border-top:1px solid var(--border);padding:1rem 0}.history-source{white-space:pre-wrap;overflow-wrap:anywhere;max-height:32rem;overflow:auto}.history h2{overflow-wrap:anywhere}</style>`;
23
+ }
@@ -0,0 +1,2 @@
1
+ export const MAX_BYTES = 2 * 1024 * 1024;
2
+ export const MAX_AGE = 30 * 24 * 60 * 60 * 1000;
@@ -0,0 +1,23 @@
1
+ import { escapeHtml } from "#page/escape.ts";
2
+ import type { Snapshot } from "#search/snapshot.ts";
3
+ import { observeBaseline, readBaseline } from "./baseline.ts";
4
+ import { historyHtml } from "./html.ts";
5
+ import type { HistoryOutput } from "./schema.ts";
6
+
7
+ export function historyResponse(raw: string | null, now: number, snapshot?: Snapshot): HistoryOutput {
8
+ const prior = readBaseline(raw, now);
9
+ const output: HistoryOutput = { discardBaseline: prior.discard, html: `<p>${escapeHtml(prior.reason)}</p>`, reason: prior.reason, snapshot: null };
10
+ if (snapshot === undefined) {
11
+ return output;
12
+ }
13
+ const currentRaw = observeBaseline(snapshot, now);
14
+ if (currentRaw === undefined) {
15
+ const reason =
16
+ snapshot.unavailable.length > 0
17
+ ? "Current workspace observation is incomplete; no comparison or removals are inferred. Your valid remembered snapshot is preserved."
18
+ : "Complete current snapshot exceeds 2 MiB; no comparison or partial history is retained.";
19
+ return { ...output, html: `<p role="status">${escapeHtml(reason)}</p>`, reason };
20
+ }
21
+ const current = readBaseline(currentRaw, now).baseline;
22
+ return { ...output, html: prior.baseline && current ? historyHtml(prior.baseline, current) : output.html, snapshot: currentRaw };
23
+ }
@@ -0,0 +1,36 @@
1
+ import { Schema } from "effect";
2
+ import { isMarkdown } from "#files/list.ts";
3
+
4
+ const File = Schema.String.check(
5
+ Schema.makeFilter(
6
+ (value) =>
7
+ (isMarkdown(value) && !value.includes("\\") && !value.includes("\0") && value.split("/").every((part) => part.length > 0 && part !== ".."))
8
+ || "Expected a relative readable Markdown path",
9
+ ),
10
+ );
11
+ const ObservedTime = Schema.String.check(
12
+ Schema.makeFilter(
13
+ (value) =>
14
+ (Number.isFinite(Date.parse(value)) && /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/u.exec(value) !== null)
15
+ || "Expected an ISO observation time with timezone",
16
+ ),
17
+ );
18
+ export const Baseline = Schema.Struct({
19
+ documents: Schema.Array(Schema.Struct({ file: File, source: Schema.String })).check(
20
+ Schema.makeFilter(
21
+ (values) => new Set(values.map((value) => value.file)).size === values.length || "Duplicate source paths are not a complete observation",
22
+ ),
23
+ ),
24
+ recordedAt: ObservedTime,
25
+ version: Schema.Literal(1),
26
+ }).pipe(Schema.encodeKeys({ recordedAt: "recorded_at" }));
27
+ export type Baseline = typeof Baseline.Type;
28
+
29
+ export const HistoryInput = Schema.Struct({ action: Schema.Literals(["check", "compare", "observe"]), baseline: Schema.NullOr(Schema.String) });
30
+ export const HistoryOutput = Schema.Struct({
31
+ discardBaseline: Schema.Boolean,
32
+ html: Schema.String,
33
+ reason: Schema.String,
34
+ snapshot: Schema.NullOr(Schema.String),
35
+ });
36
+ export type HistoryOutput = typeof HistoryOutput.Type;