@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,112 @@
1
+ export function historyScript(): string {
2
+ return `
3
+ import { swap, remember } from "/_board/swap.js";
4
+ import { captureReading, restoreReading } from "/_board/reading-state.js";
5
+ const key = "work-board:changes:" + document.documentElement.dataset.workspace;
6
+ let memory;
7
+ let storageNotice = "";
8
+ let unavailableHtml = "";
9
+ let generation = 0;
10
+ let controller;
11
+ const baseline = () => {
12
+ if (memory !== undefined) return memory;
13
+ try { return localStorage.getItem(key); }
14
+ catch { memory = null; storageNotice = "Browser storage unavailable; history applies to this page only."; return memory; }
15
+ };
16
+ const forget = () => {
17
+ memory = null;
18
+ try { localStorage.removeItem(key); memory = undefined; }
19
+ catch { storageNotice = "Cleared for this page, but browser storage could not be cleared. Reload may restore older data."; }
20
+ };
21
+ const keep = (raw) => {
22
+ memory = raw;
23
+ try { localStorage.setItem(key, raw); memory = undefined; storageNotice = ""; }
24
+ catch {
25
+ storageNotice = "Snapshot remembered for this page only; browser storage is unavailable.";
26
+ try { localStorage.removeItem(key); }
27
+ catch { storageNotice += " Reload may restore older data."; }
28
+ }
29
+ };
30
+ const controls = (available, busy = false) => {
31
+ const mark = document.getElementById("history-mark");
32
+ const clear = document.getElementById("history-clear");
33
+ if (!mark || !clear) return;
34
+ const exists = baseline() !== null;
35
+ mark.textContent = exists ? "Mark current workspace seen" : "Start remembering changes";
36
+ mark.disabled = busy || !available;
37
+ clear.disabled = busy || !exists;
38
+ document.getElementById("history-storage").textContent = storageNotice;
39
+ };
40
+ const render = (html) => {
41
+ const content = document.getElementById("changes-content");
42
+ if (!content) return;
43
+ const reading = captureReading();
44
+ const next = content.cloneNode(false);
45
+ next.innerHTML = html;
46
+ swap(content, next);
47
+ remember(content);
48
+ restoreReading({ ...reading, selection: null }, false);
49
+ };
50
+ const query = async (action) => {
51
+ const mine = ++generation;
52
+ controller?.abort();
53
+ controller = new AbortController();
54
+ const raw = baseline();
55
+ const response = await fetch("/_board/history", {
56
+ method: "POST", headers: { "content-type": "application/json" },
57
+ body: JSON.stringify({ action, baseline: raw }), signal: controller.signal, cache: "no-store",
58
+ });
59
+ if (!response.ok) throw new Error("History query failed");
60
+ const result = await response.json();
61
+ if (mine !== generation) return null;
62
+ if (result.discardBaseline && baseline() === raw) { forget(); unavailableHtml = result.html; }
63
+ return result;
64
+ };
65
+ const refresh = async () => {
66
+ const visible = document.getElementById("doc")?.dataset.view === "changes";
67
+ try {
68
+ const result = await query(visible ? "compare" : "check");
69
+ if (!result) return;
70
+ if (visible && document.getElementById("doc")?.dataset.view === "changes") {
71
+ render(baseline() === null && unavailableHtml ? unavailableHtml : result.html);
72
+ controls(result.snapshot !== null);
73
+ }
74
+ } catch (error) {
75
+ if (error.name === "AbortError") return;
76
+ if (visible) {
77
+ render("<p>History is unavailable: the current observation could not be loaded. No comparison or removals are inferred.</p>");
78
+ controls(false);
79
+ }
80
+ }
81
+ };
82
+ document.addEventListener("click", async (event) => {
83
+ if (event.target.closest?.("#history-clear")) {
84
+ ++generation;
85
+ controller?.abort();
86
+ forget();
87
+ unavailableHtml = "";
88
+ await refresh();
89
+ }
90
+ if (event.target.closest?.("#history-mark")) {
91
+ controls(false, true);
92
+ try {
93
+ const result = await query("observe");
94
+ if (!result) return;
95
+ if (result.snapshot === null) { render(result.html); controls(false); return; }
96
+ keep(result.snapshot);
97
+ unavailableHtml = "";
98
+ await refresh();
99
+ } catch (error) {
100
+ if (error.name !== "AbortError") { render("<p>Could not remember the current observation. Your previous baseline is preserved.</p>"); controls(false); }
101
+ }
102
+ }
103
+ });
104
+ document.addEventListener("board-page", refresh);
105
+ document.addEventListener("visibilitychange", () => { if (!document.hidden) refresh(); });
106
+ window.addEventListener("storage", (event) => {
107
+ if (event.key === key || event.key === null) { memory = undefined; storageNotice = ""; unavailableHtml = ""; refresh(); }
108
+ });
109
+ setInterval(() => { if (baseline() !== null) refresh(); }, 60000);
110
+ refresh();
111
+ `;
112
+ }
@@ -1,15 +1,32 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { Data, Effect, FileSystem, Path } from "effect";
3
3
  import type { HttpRouter, HttpServerRequest } from "effect/unstable/http";
4
- import { client } from "#page/client.ts";
4
+ import { historyScript } from "#history/script.ts";
5
+ import { client, preferences } from "#page/client.ts";
5
6
  import { diagrams } from "#page/diagrams.ts";
7
+ import { libraryScript } from "#page/libraryScript.ts";
8
+ import { navigationScript } from "#page/navigationScript.ts";
9
+ import { readingStateScript } from "#page/readingStateScript.ts";
10
+ import { searchResultsScript } from "#page/searchResultsScript.ts";
11
+ import { searchScript } from "#page/searchScript.ts";
12
+ import { pageStateScript } from "#page/stateScript.ts";
6
13
  import { style } from "#page/style.ts";
7
14
  import { swap } from "#page/swap.ts";
15
+ import { savedScript } from "#views/savedScript.ts";
8
16
  import { respond } from "./respond.ts";
9
17
 
10
18
  export const ASSETS: ReadonlyArray<readonly [HttpRouter.PathInput, string, string]> = [
11
19
  ["/_board/style.css", style, "text/css"],
20
+ ["/_board/saved-views.js", savedScript(), "text/javascript"],
12
21
  ["/_board/client.js", client, "text/javascript"],
22
+ ["/_board/history.js", historyScript(), "text/javascript"],
23
+ ["/_board/preferences.js", preferences, "text/javascript"],
24
+ ["/_board/search.js", searchScript(), "text/javascript"],
25
+ ["/_board/search-results.js", searchResultsScript(), "text/javascript"],
26
+ ["/_board/library.js", libraryScript(), "text/javascript"],
27
+ ["/_board/navigation.js", navigationScript(), "text/javascript"],
28
+ ["/_board/page-state.js", pageStateScript(), "text/javascript"],
29
+ ["/_board/reading-state.js", readingStateScript(), "text/javascript"],
13
30
  ["/_board/diagrams.js", diagrams, "text/javascript"],
14
31
  ["/_board/swap.js", swap, "text/javascript"],
15
32
  ];
@@ -0,0 +1,50 @@
1
+ import { Clock, Effect, FileSystem, Schema } from "effect";
2
+ import { HttpIncomingMessage, HttpServerRequest } from "effect/unstable/http";
3
+ import type { Changes } from "#files/changes.ts";
4
+ import { changesHtml } from "#history/html.ts";
5
+ import { MAX_BYTES } from "#history/limits.ts";
6
+ import { historyResponse } from "#history/response.ts";
7
+ import { HistoryInput, HistoryOutput } from "#history/schema.ts";
8
+ import { navHtml } from "#page/nav.ts";
9
+ import { shell } from "#page/shell.ts";
10
+ import type { searchSnapshot } from "#search/snapshot.ts";
11
+ import { respond } from "./respond.ts";
12
+
13
+ export function changesPage(changes: Changes, home: string | undefined) {
14
+ return Effect.fn("WorkBoard.changesPage")(function* () {
15
+ const nav = navHtml(yield* changes.files, "", home);
16
+ return respond(shell("Changes", nav, changesHtml(), changes.realRoot, true, undefined, "changes"), "text/html");
17
+ });
18
+ }
19
+ function sameOrigin(request: HttpServerRequest.HttpServerRequest): boolean {
20
+ const origin = request.headers.origin;
21
+ if (origin === undefined) {
22
+ return true;
23
+ }
24
+ try {
25
+ const url = new URL(origin);
26
+ return url.protocol === "http:" && url.host === request.headers.host;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+ export function history(index: Effect.Success<ReturnType<typeof searchSnapshot>>) {
32
+ return Effect.fn("WorkBoard.history")(function* (request: HttpServerRequest.HttpServerRequest) {
33
+ if (!sameOrigin(request)) {
34
+ return respond("Local same-origin requests only", "text/plain", 403);
35
+ }
36
+ const decoded = yield* HttpServerRequest.schemaBodyJson(HistoryInput, { onExcessProperty: "error" }).pipe(
37
+ Effect.provideService(HttpServerRequest.HttpServerRequest, request),
38
+ Effect.provideService(HttpIncomingMessage.MaxBodySize, FileSystem.Size(4 * MAX_BYTES + 4096)),
39
+ Effect.result,
40
+ );
41
+ if (decoded._tag === "Failure") {
42
+ return respond("Invalid history query", "text/plain", 400);
43
+ }
44
+ const input = decoded.success;
45
+ const now = yield* Clock.currentTimeMillis;
46
+ const snapshot = input.action === "check" ? undefined : yield* index;
47
+ const output = historyResponse(input.baseline, now, snapshot);
48
+ return respond(Schema.encodeSync(Schema.fromJsonString(HistoryOutput))(output), "application/json");
49
+ });
50
+ }
@@ -0,0 +1,62 @@
1
+ import { Effect } from "effect";
2
+ import type { HttpServerRequest } from "effect/unstable/http";
3
+ import type { Changes } from "#files/changes.ts";
4
+ import { fileUrl } from "#files/url.ts";
5
+ import { escapeHtml } from "#page/escape.ts";
6
+ import { navHtml } from "#page/nav.ts";
7
+ import { shell } from "#page/shell.ts";
8
+ import type { searchSnapshot } from "#search/snapshot.ts";
9
+ import type { page } from "./page.ts";
10
+ import { respond } from "./respond.ts";
11
+
12
+ function requestedIdentity(pathname: string): string {
13
+ try {
14
+ return decodeURIComponent(pathname.slice("/_board/item/".length).replace(/\/$/u, ""));
15
+ } catch {
16
+ return "";
17
+ }
18
+ }
19
+
20
+ export function identity(
21
+ index: Effect.Success<ReturnType<typeof searchSnapshot>>,
22
+ pages: ReturnType<typeof page>,
23
+ changes: Changes,
24
+ home: string | undefined,
25
+ ) {
26
+ return Effect.fn("WorkBoard.identity")(function* (request: HttpServerRequest.HttpServerRequest) {
27
+ const pathname = new URL(request.url, "http://127.0.0.1").pathname;
28
+ const id = requestedIdentity(pathname);
29
+ if (!pathname.endsWith("/") && /\.md$/iu.exec(id) !== null) {
30
+ return yield* pages(request);
31
+ }
32
+ const snapshot = yield* index;
33
+ const matches = snapshot.model.ids.get(id) ?? [];
34
+ if (snapshot.unavailable.length > 0) {
35
+ return respond(
36
+ shell(
37
+ `Item ${id}`,
38
+ "",
39
+ `<p class="empty">The workspace index is incomplete. Could not read ${escapeHtml(snapshot.unavailable.join(", "))}; item identity could not be validated. Retry when the files are readable.</p>`,
40
+ changes.realRoot,
41
+ false,
42
+ ),
43
+ "text/html",
44
+ 503,
45
+ );
46
+ }
47
+ if (matches.length === 1 && matches[0]) {
48
+ return yield* pages(request, matches[0].file, id);
49
+ }
50
+ const text =
51
+ matches.length > 0
52
+ ? `The ID ${id} is duplicated. No document was selected.`
53
+ : `No item with ID ${id} is available. It may be missing, renamed, or invalid.`;
54
+ const choices = matches.map((document) => `<li><a href="${escapeHtml(fileUrl(document.file))}">${escapeHtml(document.file)}</a></li>`).join("");
55
+ const nav = navHtml(yield* changes.files, "", home);
56
+ return respond(
57
+ shell(`Item ${id}`, nav, `<p class="empty">${escapeHtml(text)}</p>${choices ? `<ul>${choices}</ul>` : ""}`, changes.realRoot, false),
58
+ "text/html",
59
+ matches.length > 0 ? 409 : 404,
60
+ );
61
+ });
62
+ }
@@ -0,0 +1,19 @@
1
+ import { Effect } from "effect";
2
+ import { overviewHtml } from "#attention/overviewHtml.ts";
3
+ import type { Changes } from "#files/changes.ts";
4
+ import { navHtml } from "#page/nav.ts";
5
+ import { shell } from "#page/shell.ts";
6
+ import type { searchSnapshot } from "#search/snapshot.ts";
7
+ import { respond } from "./respond.ts";
8
+
9
+ export function overview(index: Effect.Success<ReturnType<typeof searchSnapshot>>, changes: Changes, home: string | undefined) {
10
+ return Effect.fn("WorkBoard.overview")(function* () {
11
+ const snapshot = yield* index;
12
+ const nav = navHtml(yield* changes.files, "", home);
13
+ return respond(
14
+ shell("Overview", nav, overviewHtml(snapshot), changes.realRoot, true, undefined, "overview"),
15
+ "text/html",
16
+ snapshot.unavailable.length > 0 ? 503 : 200,
17
+ );
18
+ });
19
+ }
package/src/http/page.ts CHANGED
@@ -1,13 +1,18 @@
1
- import { Effect, FileSystem, Option, Path } from "effect";
1
+ import { Effect, FileSystem, Option } from "effect";
2
2
  import type { HttpServerRequest } from "effect/unstable/http";
3
3
  import type { Changes } from "#files/changes.ts";
4
- import { type MarkdownFile, within } from "#files/list.ts";
4
+ import type { MarkdownFile } from "#files/list.ts";
5
+ import { markdownPath } from "#files/markdownPath.ts";
6
+ import { metadataModel } from "#metadata/model.ts";
7
+ import { metadataParse } from "#metadata/parse.ts";
5
8
  import { boardHtml } from "#page/board.ts";
6
9
  import { documentHtml } from "#page/document.ts";
7
10
  import { escapeHtml } from "#page/escape.ts";
11
+ import { metadataHtml } from "#page/metadata.ts";
8
12
  import { navHtml } from "#page/nav.ts";
9
13
  import { shell } from "#page/shell.ts";
10
14
  import { boardOf } from "#render/board.ts";
15
+ import type { searchSnapshot } from "#search/snapshot.ts";
11
16
  import { respond } from "./respond.ts";
12
17
 
13
18
  export interface PageOptions {
@@ -26,29 +31,61 @@ const requestedPath = (url: string): string => {
26
31
 
27
32
  const message = (text: string): string => `<p class="empty">${escapeHtml(text)}</p>`;
28
33
 
29
- export const page = (options: PageOptions, changes: Changes) => {
30
- const body = Effect.fn("WorkBoard.pageBody")(function* (file: MarkdownFile) {
34
+ export const page = (options: PageOptions, changes: Changes, index: Effect.Success<ReturnType<typeof searchSnapshot>>) => {
35
+ const body = Effect.fn("WorkBoard.pageBody")(function* (file: MarkdownFile, expectedIdentity?: string) {
31
36
  const fs = yield* FileSystem.FileSystem;
32
- const path = yield* Path.Path;
33
- const real = yield* Effect.option(fs.realPath(path.join(options.root, file.path)));
34
- if (Option.isNone(real) || !within(changes.realRoot, path.sep, real.value)) {
37
+ const real = yield* markdownPath(options.root, changes.realRoot, file.path);
38
+ if (real === undefined) {
35
39
  return Option.none();
36
40
  }
37
- const source = yield* fs.readFileString(real.value);
38
- return Option.some(file.path === options.home ? yield* boardHtml(boardOf(source), file.path, file.modified) : yield* documentHtml(source, file));
41
+ const source = yield* fs.readFileString(real);
42
+ const parsed = metadataParse(source);
43
+ let identity: string | undefined;
44
+ const snapshot = yield* index;
45
+ const model = metadataModel(
46
+ [...snapshot.documents.filter((document) => document.file !== file.path), { file: file.path, parsed }],
47
+ snapshot.unavailable,
48
+ options.home ?? (yield* changes.files)[0]?.path,
49
+ );
50
+ const metadata = metadataHtml(parsed, file.path, model);
51
+ if (snapshot.unavailable.length === 0 && parsed.fields.id && model.ids.get(parsed.fields.id)?.length === 1) {
52
+ identity = parsed.fields.id;
53
+ }
54
+ if (expectedIdentity && identity !== expectedIdentity) {
55
+ return Option.none();
56
+ }
57
+ const html =
58
+ file.path === options.home
59
+ ? yield* boardHtml(boardOf(parsed.body), file.path, file.modified, metadata)
60
+ : yield* documentHtml(parsed.body, file, metadata);
61
+ return Option.some({ html, identity });
39
62
  });
40
- return Effect.fn("WorkBoard.page")(function* (request: HttpServerRequest.HttpServerRequest) {
63
+ return Effect.fn("WorkBoard.page")(function* (request: HttpServerRequest.HttpServerRequest, resolved?: string, expectedIdentity?: string) {
41
64
  const files = yield* changes.files;
42
- const requested = requestedPath(request.url) || options.home || files[0]?.path || "";
65
+ const requested = resolved ?? (requestedPath(request.url) || options.home || files[0]?.path || "");
43
66
  const file = files.find((candidate) => candidate.path === requested);
44
67
  const nav = navHtml(files, requested, options.home);
45
- const missing = () => respond(shell(requested, nav, message(`No markdown file at ${requested || options.root}.`)), "text/html", 404);
68
+ function layout(content: string, identity?: string) {
69
+ return shell(requested, nav, content, changes.realRoot, requested === options.home, identity);
70
+ }
71
+ const missing = () =>
72
+ respond(
73
+ layout(
74
+ message(
75
+ expectedIdentity
76
+ ? `No item with ID ${expectedIdentity} at ${requested}; its source identity changed or is invalid.`
77
+ : `No markdown file at ${requested || options.root}.`,
78
+ ),
79
+ ),
80
+ "text/html",
81
+ 404,
82
+ );
46
83
  if (file === undefined) {
47
84
  return missing();
48
85
  }
49
- return yield* body(file).pipe(
50
- Effect.map(Option.match({ onNone: missing, onSome: (main) => respond(shell(file.path, nav, main), "text/html") })),
51
- Effect.catch(() => Effect.succeed(respond(shell(file.path, nav, message(`${file.path} could not be read.`)), "text/html", 500))),
86
+ return yield* body(file, expectedIdentity).pipe(
87
+ Effect.map(Option.match({ onNone: missing, onSome: (main) => respond(layout(main.html, main.identity), "text/html") })),
88
+ Effect.catch(() => Effect.succeed(respond(layout(message(`${file.path} could not be read.`)), "text/html", 500))),
52
89
  );
53
90
  });
54
91
  };
@@ -0,0 +1,13 @@
1
+ import { Effect } from "effect";
2
+ import type { HttpServerRequest } from "effect/unstable/http";
3
+ import { searchMatch } from "#search/match.ts";
4
+ import type { searchSnapshot } from "#search/snapshot.ts";
5
+ import { respond } from "./respond.ts";
6
+
7
+ export function search(index: Effect.Success<ReturnType<typeof searchSnapshot>>) {
8
+ return Effect.fn("WorkBoard.search")(function* (request: HttpServerRequest.HttpServerRequest) {
9
+ const query = new URL(request.url, "http://127.0.0.1").searchParams.get("q") ?? "";
10
+ const snapshot = yield* index;
11
+ return respond(JSON.stringify({ ...searchMatch(snapshot.entries, query), unavailable: snapshot.unavailable }), "application/json");
12
+ });
13
+ }
@@ -0,0 +1,41 @@
1
+ import { Effect } from "effect";
2
+ import type { HttpServerRequest } from "effect/unstable/http";
3
+ import type { Changes } from "#files/changes.ts";
4
+ import { escapeHtml } from "#page/escape.ts";
5
+ import { navHtml } from "#page/nav.ts";
6
+ import { shell } from "#page/shell.ts";
7
+ import type { searchSnapshot } from "#search/snapshot.ts";
8
+ import { stateOf } from "#views/state.ts";
9
+ import { workHtml } from "#views/workHtml.ts";
10
+ import { respond } from "./respond.ts";
11
+
12
+ export function work(index: Effect.Success<ReturnType<typeof searchSnapshot>>, changes: Changes, home: string | undefined) {
13
+ return Effect.fn("WorkBoard.work")(function* (request: HttpServerRequest.HttpServerRequest) {
14
+ const snapshot = yield* index;
15
+ const nav = navHtml(yield* changes.files, "", home);
16
+ if (snapshot.unavailable.length > 0) {
17
+ return respond(
18
+ shell(
19
+ "Work",
20
+ nav,
21
+ `<h1>Workspace incomplete</h1><p class="empty">Could not read ${escapeHtml(snapshot.unavailable.join(", "))}. Work identity and view counts cannot be validated. Read available source files or retry when the workspace is readable.</p>`,
22
+ changes.realRoot,
23
+ true,
24
+ undefined,
25
+ true,
26
+ ),
27
+ "text/html",
28
+ 503,
29
+ );
30
+ }
31
+ const result = yield* workHtml(snapshot, stateOf(request.url)).pipe(
32
+ Effect.catch(() =>
33
+ Effect.succeed({
34
+ html: '<h1>Work unavailable</h1><p class="empty">The selected source could not be rendered. Read its Markdown file or retry.</p>',
35
+ status: 500,
36
+ }),
37
+ ),
38
+ );
39
+ return respond(shell("Work", nav, result.html, changes.realRoot, true, undefined, true), "text/html", result.status);
40
+ });
41
+ }
@@ -0,0 +1,18 @@
1
+ import type { MetadataDocument } from "./model.ts";
2
+ import type { Diagnostic } from "./parse.ts";
3
+
4
+ export function attentionProblems(document: MetadataDocument): readonly Diagnostic[] {
5
+ const { fields, lines } = document.parsed;
6
+ const requests = fields.attention ?? [];
7
+ return requests.flatMap((request, index) => {
8
+ const line = lines[`attention.${index}.id`] ?? lines.attention ?? 2;
9
+ const problems: Diagnostic[] = [];
10
+ if (!fields.id) {
11
+ problems.push({ field: "attention", line, message: "Attention requests require an explicit item ID; this source remains unclassified." });
12
+ }
13
+ if (requests.filter((other) => other.id === request.id).length > 1) {
14
+ problems.push({ field: "attention", line, message: `Duplicate attention request ID ${request.id}; neither request is selected.` });
15
+ }
16
+ return problems;
17
+ });
18
+ }
@@ -0,0 +1,46 @@
1
+ import type { MetadataDocument, MetadataModel } from "./model.ts";
2
+ import { referenceFile, sourceFile } from "./sourceLinks.ts";
3
+
4
+ export interface Backlink {
5
+ readonly file: string;
6
+ readonly kind: string;
7
+ readonly line?: number;
8
+ }
9
+
10
+ function references(document: MetadataDocument, model: MetadataModel) {
11
+ const { fields, lines } = document.parsed;
12
+ return [
13
+ ...(fields.attention?.flatMap((request, index) =>
14
+ request.unblocks.map((target, targetIndex) => ({
15
+ kind: "attention target",
16
+ line: lines[`attention.${index}.unblocks.${targetIndex}`],
17
+ target: referenceFile(target, model),
18
+ })),
19
+ ) ?? []),
20
+ ...(fields.relationships?.map((link, index) => ({
21
+ kind: link.kind,
22
+ line: lines[`relationships.${index}.target`],
23
+ target: referenceFile(link.target, model),
24
+ })) ?? []),
25
+ ...(fields.items?.map((item, index) => ({ kind: "board member", line: lines[`items.${index}`], target: referenceFile(item, model) })) ?? []),
26
+ ...(fields.evidence?.flatMap((evidence, index) => [
27
+ { kind: "evidence source", line: lines[`evidence.${index}.source`], target: sourceFile(evidence.source, document.file, model) },
28
+ {
29
+ kind: "criterion evidence",
30
+ line: lines[`evidence.${index}.criterion`],
31
+ target: evidence.criterion ? referenceFile(evidence.criterion, model) : undefined,
32
+ },
33
+ ]) ?? []),
34
+ ...(document.links?.map((link) => ({ kind: "Markdown link", line: undefined, target: sourceFile(link, document.file, model) })) ?? []),
35
+ ];
36
+ }
37
+
38
+ export function backlinksOf(file: string, model: MetadataModel): readonly Backlink[] {
39
+ return model.documents.flatMap((document) =>
40
+ document.file === file
41
+ ? []
42
+ : references(document, model)
43
+ .filter((reference) => reference.target === file)
44
+ .map((reference) => ({ file: document.file, kind: reference.kind, ...(reference.line === undefined ? {} : { line: reference.line }) })),
45
+ );
46
+ }
@@ -0,0 +1,26 @@
1
+ import { Option, Schema } from "effect";
2
+ import type { Diagnostic } from "./parse.ts";
3
+ import { Metadata } from "./schema.ts";
4
+
5
+ const schemas: ReadonlyMap<string, Schema.ConstraintDecoder<unknown, never>> = new Map(Object.entries(Metadata.to.fields));
6
+ const sourceKeys = new Map([["next_action", "nextAction"]]);
7
+
8
+ export function metadataFields(data: object, lines: Readonly<Record<string, number>>) {
9
+ const fields: Record<string, unknown> = {};
10
+ const diagnostics: Diagnostic[] = [];
11
+ for (const [field, value] of Object.entries(data)) {
12
+ const key = sourceKeys.get(field) ?? field;
13
+ const schema = field === "nextAction" ? undefined : schemas.get(key);
14
+ if (!schema) {
15
+ diagnostics.push({ field, line: lines[field] ?? 2, message: `Unknown field ${field}; kept in the original frontmatter.` });
16
+ continue;
17
+ }
18
+ const decoded = Schema.decodeUnknownOption(schema, { onExcessProperty: "error" })(value);
19
+ if (Option.isNone(decoded)) {
20
+ diagnostics.push({ field, line: lines[field] ?? 2, message: `Invalid ${field}; kept in the original frontmatter and not interpreted.` });
21
+ } else {
22
+ fields[key] = decoded.value;
23
+ }
24
+ }
25
+ return { diagnostics, fields: Schema.decodeUnknownSync(Schema.toType(Metadata))(fields) };
26
+ }
@@ -0,0 +1,19 @@
1
+ import type { MetadataModel } from "./model.ts";
2
+
3
+ export function identityUrl(id: string): string {
4
+ return `/_board/item/${encodeURIComponent(id)}/`;
5
+ }
6
+ export function referenceTarget(reference: string, model: MetadataModel): string | undefined {
7
+ if (model.unavailable.length > 0) {
8
+ return undefined;
9
+ }
10
+ const [id = "", criterion] = reference.split("#");
11
+ const targets = model.ids.get(id);
12
+ if (targets?.length !== 1) {
13
+ return undefined;
14
+ }
15
+ if (criterion && targets[0]?.parsed.fields.criteria?.filter((item) => item.id === criterion).length !== 1) {
16
+ return undefined;
17
+ }
18
+ return identityUrl(id) + (criterion ? `#criterion-${encodeURIComponent(criterion)}` : "");
19
+ }
@@ -0,0 +1,34 @@
1
+ import { isMap, isNode, isScalar, isSeq, type LineCounter, type Node, type YAMLMap } from "yaml";
2
+
3
+ function recordMap(node: YAMLMap, field: string, counter: LineCounter, lines: Record<string, number>) {
4
+ for (const pair of node.items) {
5
+ if (!isScalar(pair.key) || typeof pair.key.value !== "string") {
6
+ continue;
7
+ }
8
+ const key = field ? `${field}.${pair.key.value}` : pair.key.value;
9
+ visit(pair.value, key, counter, lines);
10
+ if (pair.key.range) {
11
+ lines[key] = counter.linePos(pair.key.range[0]).line + 1;
12
+ }
13
+ }
14
+ }
15
+ function visit(value: unknown, field: string, counter: LineCounter, lines: Record<string, number>) {
16
+ if (!isNode(value)) {
17
+ return;
18
+ }
19
+ if (value.range && field) {
20
+ lines[field] = counter.linePos(value.range[0]).line + 1;
21
+ }
22
+ if (isMap(value)) {
23
+ recordMap(value, field, counter, lines);
24
+ } else if (isSeq(value)) {
25
+ value.items.forEach((item, index) => {
26
+ visit(item, `${field}.${index}`, counter, lines);
27
+ });
28
+ }
29
+ }
30
+ export function metadataLocations(node: Node | null, counter: LineCounter): Readonly<Record<string, number>> {
31
+ const lines: Record<string, number> = {};
32
+ visit(node, "", counter, lines);
33
+ return lines;
34
+ }
@@ -0,0 +1,27 @@
1
+ import type { Diagnostic, ParsedMetadata } from "./parse.ts";
2
+ import { metadataProblems } from "./problems.ts";
3
+
4
+ export interface MetadataDocument {
5
+ readonly file: string;
6
+ readonly links?: readonly string[];
7
+ readonly parsed: ParsedMetadata;
8
+ }
9
+ export interface MetadataModel {
10
+ readonly diagnostics: ReadonlyMap<string, readonly Diagnostic[]>;
11
+ readonly documents: readonly MetadataDocument[];
12
+ readonly ids: ReadonlyMap<string, readonly MetadataDocument[]>;
13
+ readonly rootFile: string | undefined;
14
+ readonly unavailable: readonly string[];
15
+ }
16
+
17
+ export function metadataModel(documents: readonly MetadataDocument[], unavailable: readonly string[] = [], rootFile?: string): MetadataModel {
18
+ const ids = new Map<string, MetadataDocument[]>();
19
+ for (const document of documents) {
20
+ const id = document.parsed.fields.id;
21
+ if (id) {
22
+ ids.set(id, [...(ids.get(id) ?? []), document]);
23
+ }
24
+ }
25
+ const diagnostics = new Map(documents.map((document) => [document.file, metadataProblems(document, ids, unavailable)]));
26
+ return { diagnostics, documents, ids, rootFile, unavailable };
27
+ }