@shivaedev/work-board 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +70 -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 +6 -1
  17. package/dist/board.js.map +1 -1
  18. package/dist/cli.js +1 -1
  19. package/dist/files/changes.d.ts.map +1 -1
  20. package/dist/files/changes.js +4 -9
  21. package/dist/files/changes.js.map +1 -1
  22. package/dist/files/home.d.ts.map +1 -1
  23. package/dist/files/home.js +4 -0
  24. package/dist/files/home.js.map +1 -1
  25. package/dist/files/list.d.ts +7 -0
  26. package/dist/files/list.d.ts.map +1 -1
  27. package/dist/files/list.js +23 -13
  28. package/dist/files/list.js.map +1 -1
  29. package/dist/files/markdownPath.d.ts +3 -0
  30. package/dist/files/markdownPath.d.ts.map +1 -0
  31. package/dist/files/markdownPath.js +31 -0
  32. package/dist/files/markdownPath.js.map +1 -0
  33. package/dist/files/watchDirectories.d.ts +9 -0
  34. package/dist/files/watchDirectories.d.ts.map +1 -0
  35. package/dist/files/watchDirectories.js +33 -0
  36. package/dist/files/watchDirectories.js.map +1 -0
  37. package/dist/history/baseline.d.ts +19 -0
  38. package/dist/history/baseline.d.ts.map +1 -0
  39. package/dist/history/baseline.js +40 -0
  40. package/dist/history/baseline.js.map +1 -0
  41. package/dist/history/compare.d.ts +14 -0
  42. package/dist/history/compare.d.ts.map +1 -0
  43. package/dist/history/compare.js +47 -0
  44. package/dist/history/compare.js.map +1 -0
  45. package/dist/history/documents.d.ts +10 -0
  46. package/dist/history/documents.d.ts.map +1 -0
  47. package/dist/history/documents.js +15 -0
  48. package/dist/history/documents.js.map +1 -0
  49. package/dist/history/fields.d.ts +3 -0
  50. package/dist/history/fields.d.ts.map +1 -0
  51. package/dist/history/fields.js +27 -0
  52. package/dist/history/fields.js.map +1 -0
  53. package/dist/history/html.d.ts +4 -0
  54. package/dist/history/html.d.ts.map +1 -0
  55. package/dist/history/html.js +22 -0
  56. package/dist/history/html.js.map +1 -0
  57. package/dist/history/limits.d.ts +3 -0
  58. package/dist/history/limits.d.ts.map +1 -0
  59. package/dist/history/limits.js +3 -0
  60. package/dist/history/limits.js.map +1 -0
  61. package/dist/history/response.d.ts +4 -0
  62. package/dist/history/response.d.ts.map +1 -0
  63. package/dist/history/response.js +20 -0
  64. package/dist/history/response.js.map +1 -0
  65. package/dist/history/schema.d.ts +24 -0
  66. package/dist/history/schema.d.ts.map +1 -0
  67. package/dist/history/schema.js +19 -0
  68. package/dist/history/schema.js.map +1 -0
  69. package/dist/history/script.d.ts +2 -0
  70. package/dist/history/script.d.ts.map +1 -0
  71. package/dist/history/script.js +125 -0
  72. package/dist/history/script.js.map +1 -0
  73. package/dist/http/assets.d.ts.map +1 -1
  74. package/dist/http/assets.js +2 -0
  75. package/dist/http/assets.js.map +1 -1
  76. package/dist/http/history.d.ts +7 -0
  77. package/dist/http/history.d.ts.map +1 -0
  78. package/dist/http/history.js +45 -0
  79. package/dist/http/history.js.map +1 -0
  80. package/dist/http/overview.d.ts +5 -0
  81. package/dist/http/overview.d.ts.map +1 -0
  82. package/dist/http/overview.js +13 -0
  83. package/dist/http/overview.js.map +1 -0
  84. package/dist/http/page.d.ts +2 -2
  85. package/dist/http/page.d.ts.map +1 -1
  86. package/dist/http/page.js +5 -6
  87. package/dist/http/page.js.map +1 -1
  88. package/dist/metadata/attentionProblems.d.ts +4 -0
  89. package/dist/metadata/attentionProblems.d.ts.map +1 -0
  90. package/dist/metadata/attentionProblems.js +16 -0
  91. package/dist/metadata/attentionProblems.js.map +1 -0
  92. package/dist/metadata/backlinks.d.ts.map +1 -1
  93. package/dist/metadata/backlinks.js +5 -0
  94. package/dist/metadata/backlinks.js.map +1 -1
  95. package/dist/metadata/fields.d.ts +8 -0
  96. package/dist/metadata/fields.d.ts.map +1 -1
  97. package/dist/metadata/problems.d.ts.map +1 -1
  98. package/dist/metadata/problems.js +3 -1
  99. package/dist/metadata/problems.js.map +1 -1
  100. package/dist/metadata/relationshipLabel.d.ts.map +1 -1
  101. package/dist/metadata/relationshipLabel.js +1 -0
  102. package/dist/metadata/relationshipLabel.js.map +1 -1
  103. package/dist/metadata/schema.d.ts +10 -0
  104. package/dist/metadata/schema.d.ts.map +1 -1
  105. package/dist/metadata/schema.js +9 -0
  106. package/dist/metadata/schema.js.map +1 -1
  107. package/dist/page/attentionDetails.d.ts +4 -0
  108. package/dist/page/attentionDetails.d.ts.map +1 -0
  109. package/dist/page/attentionDetails.js +13 -0
  110. package/dist/page/attentionDetails.js.map +1 -0
  111. package/dist/page/client.d.ts +1 -1
  112. package/dist/page/client.d.ts.map +1 -1
  113. package/dist/page/client.js +1 -0
  114. package/dist/page/client.js.map +1 -1
  115. package/dist/page/metadataDetails.d.ts.map +1 -1
  116. package/dist/page/metadataDetails.js +2 -0
  117. package/dist/page/metadataDetails.js.map +1 -1
  118. package/dist/page/navigationScript.js +1 -1
  119. package/dist/page/shell.d.ts +1 -1
  120. package/dist/page/shell.d.ts.map +1 -1
  121. package/dist/page/shell.js +2 -2
  122. package/dist/page/shell.js.map +1 -1
  123. package/dist/page/stateScript.d.ts.map +1 -1
  124. package/dist/page/stateScript.js +6 -0
  125. package/dist/page/stateScript.js.map +1 -1
  126. package/dist/page/style.d.ts.map +1 -1
  127. package/dist/page/style.js +2 -1
  128. package/dist/page/style.js.map +1 -1
  129. package/dist/search/attentionEntries.d.ts +4 -0
  130. package/dist/search/attentionEntries.d.ts.map +1 -0
  131. package/dist/search/attentionEntries.js +20 -0
  132. package/dist/search/attentionEntries.js.map +1 -0
  133. package/dist/search/metadataEntries.d.ts.map +1 -1
  134. package/dist/search/metadataEntries.js +2 -0
  135. package/dist/search/metadataEntries.js.map +1 -1
  136. package/dist/search/snapshot.d.ts +2 -2
  137. package/dist/search/snapshot.d.ts.map +1 -1
  138. package/dist/search/snapshot.js +4 -5
  139. package/dist/search/snapshot.js.map +1 -1
  140. package/package.json +5 -4
  141. package/src/attention/overviewHtml.ts +44 -0
  142. package/src/attention/requests.ts +64 -0
  143. package/src/attention/style.ts +9 -0
  144. package/src/board.ts +7 -2
  145. package/src/cli.ts +1 -1
  146. package/src/files/changes.ts +4 -16
  147. package/src/files/home.ts +4 -0
  148. package/src/files/list.ts +25 -14
  149. package/src/files/markdownPath.ts +31 -0
  150. package/src/files/watchDirectories.ts +60 -0
  151. package/src/history/baseline.ts +43 -0
  152. package/src/history/compare.ts +54 -0
  153. package/src/history/documents.ts +21 -0
  154. package/src/history/fields.ts +32 -0
  155. package/src/history/html.ts +23 -0
  156. package/src/history/limits.ts +2 -0
  157. package/src/history/response.ts +23 -0
  158. package/src/history/schema.ts +36 -0
  159. package/src/history/script.ts +124 -0
  160. package/src/http/assets.ts +2 -0
  161. package/src/http/history.ts +50 -0
  162. package/src/http/overview.ts +19 -0
  163. package/src/http/page.ts +6 -6
  164. package/src/metadata/attentionProblems.ts +18 -0
  165. package/src/metadata/backlinks.ts +7 -0
  166. package/src/metadata/problems.ts +5 -1
  167. package/src/metadata/relationshipLabel.ts +1 -0
  168. package/src/metadata/schema.ts +9 -0
  169. package/src/page/attentionDetails.ts +18 -0
  170. package/src/page/client.ts +1 -0
  171. package/src/page/metadataDetails.ts +2 -0
  172. package/src/page/navigationScript.ts +1 -1
  173. package/src/page/shell.ts +11 -3
  174. package/src/page/stateScript.ts +6 -0
  175. package/src/page/style.ts +2 -1
  176. package/src/search/attentionEntries.ts +22 -0
  177. package/src/search/metadataEntries.ts +2 -0
  178. package/src/search/snapshot.ts +4 -5
@@ -1 +1 @@
1
- {"version":3,"file":"stateScript.d.ts","sourceRoot":"","sources":["../../src/page/stateScript.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,IAAI,MAAM,CA+CxC"}
1
+ {"version":3,"file":"stateScript.d.ts","sourceRoot":"","sources":["../../src/page/stateScript.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,IAAI,MAAM,CAqDxC"}
@@ -18,6 +18,10 @@ export const applyPage = (page, preserve = false) => {
18
18
  const reading = preserve ? captureReading() : null;
19
19
  const cachedScheme = incoming.dataset.scheme;
20
20
  if (preserve && (current.dataset.file === incoming.dataset.file || (current.dataset.identity && current.dataset.identity === incoming.dataset.identity))) {
21
+ if (current.dataset.view === "changes" && incoming.dataset.view === "changes") {
22
+ const content = current.querySelector("#changes-content");
23
+ if (content) incoming.querySelector("#changes-content")?.replaceWith(content.cloneNode(true));
24
+ }
21
25
  swap(current, incoming);
22
26
  } else {
23
27
  current.replaceWith(document.importNode(incoming, true));
@@ -30,6 +34,8 @@ export const applyPage = (page, preserve = false) => {
30
34
  doc.dataset.url = incoming.dataset.url;
31
35
  if (incoming.dataset.view) doc.dataset.view = incoming.dataset.view; else delete doc.dataset.view;
32
36
  document.getElementById("work-open")?.setAttribute("aria-current", doc.dataset.view === "work" ? "page" : "false");
37
+ document.getElementById("overview-open")?.setAttribute("aria-current", doc.dataset.view === "overview" ? "page" : "false");
38
+ document.getElementById("changes-open")?.setAttribute("aria-current", doc.dataset.view === "changes" ? "page" : "false");
33
39
  doc.dataset.scheme = document.documentElement.dataset.scheme;
34
40
  swap(document.getElementById("files"), page.getElementById("files"));
35
41
  const crumbs = page.getElementById("breadcrumbs");
@@ -1 +1 @@
1
- {"version":3,"file":"stateScript.js","sourceRoot":"","sources":["../../src/page/stateScript.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,eAAe;IAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6CP,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"stateScript.js","sourceRoot":"","sources":["../../src/page/stateScript.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,eAAe;IAC9B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmDP,CAAC;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AAyDA,eAAO,MAAM,KAAK,QAA4G,CAAC"}
1
+ {"version":3,"file":"style.d.ts","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AA0DA,eAAO,MAAM,KAAK,QAA+H,CAAC"}
@@ -1,3 +1,4 @@
1
+ import { attentionStyle } from "#attention/style.ts";
1
2
  import { workStyle } from "#views/workStyle.ts";
2
3
  import { metadataStyles } from "./metadata.js";
3
4
  import { readingStyles } from "./readingStyles.js";
@@ -51,5 +52,5 @@ figure.diagram[data-state="failed"] .diagram-source { display: block; }
51
52
  figure.diagram[data-state="failed"]::after { content: attr(data-error); color: var(--danger); font-size: 0.875em; }
52
53
  @media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
53
54
  `;
54
- export const style = theme + layout + prose + board + diagrams + readingStyles() + searchStyles + metadataStyles + workStyle();
55
+ export const style = theme + layout + prose + board + diagrams + readingStyles() + searchStyles + metadataStyles + workStyle() + attentionStyle();
55
56
  //# sourceMappingURL=style.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"style.js","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAErD,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Bb,CAAC;AAEF,MAAM,KAAK,GAAG;;;;;;;;;;;;CAYb,CAAC;AAEF,MAAM,QAAQ,GAAG;;;;;;;CAOhB,CAAC;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,aAAa,EAAE,GAAG,YAAY,GAAG,cAAc,GAAG,SAAS,EAAE,CAAC"}
1
+ {"version":3,"file":"style.js","sourceRoot":"","sources":["../../src/page/style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAErD,MAAM,KAAK,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Bb,CAAC;AAEF,MAAM,KAAK,GAAG;;;;;;;;;;;;CAYb,CAAC;AAEF,MAAM,QAAQ,GAAG;;;;;;;CAOhB,CAAC;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,aAAa,EAAE,GAAG,YAAY,GAAG,cAAc,GAAG,SAAS,EAAE,GAAG,cAAc,EAAE,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { MetadataDocument } from "#metadata/model.ts";
2
+ import type { Entry } from "./entries.ts";
3
+ export declare function attentionEntries(document: MetadataDocument, href: string): readonly Entry[];
4
+ //# sourceMappingURL=attentionEntries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attentionEntries.d.ts","sourceRoot":"","sources":["../../src/search/attentionEntries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,KAAK,EAAE,CAkB3F"}
@@ -0,0 +1,20 @@
1
+ export function attentionEntries(document, href) {
2
+ const requests = document.parsed.fields.attention ?? [];
3
+ return requests.flatMap((request, index) => {
4
+ if (requests.filter((other) => other.id === request.id).length !== 1) {
5
+ return [];
6
+ }
7
+ const line = document.parsed.lines[`attention.${index}`] ?? document.parsed.lines.attention;
8
+ return [
9
+ {
10
+ file: document.file,
11
+ href: `${href}#attention-request-${encodeURIComponent(request.id)}`,
12
+ kind: "passage",
13
+ ...(line === undefined ? {} : { line }),
14
+ text: [request.reason, request.id, request.kind, request.state, ...request.responseFrom, ...request.unblocks].join(" "),
15
+ title: request.reason,
16
+ },
17
+ ];
18
+ });
19
+ }
20
+ //# sourceMappingURL=attentionEntries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attentionEntries.js","sourceRoot":"","sources":["../../src/search/attentionEntries.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,gBAAgB,CAAC,QAA0B,EAAE,IAAY;IACxE,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC;IACxD,OAAO,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;QAC1C,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,OAAO,CAAC,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtE,OAAO,EAAE,CAAC;QACX,CAAC;QACD,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,aAAa,KAAK,EAAE,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC;QAC5F,OAAO;YACN;gBACC,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,IAAI,EAAE,GAAG,IAAI,sBAAsB,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE;gBACnE,IAAI,EAAE,SAAS;gBACf,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;gBACvC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,KAAK,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;gBACvH,KAAK,EAAE,OAAO,CAAC,MAAM;aACrB;SACD,CAAC;IACH,CAAC,CAAC,CAAC;AACJ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"metadataEntries.d.ts","sourceRoot":"","sources":["../../src/search/metadataEntries.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAM1C,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,KAAK,EAAE,EAAE,SAAS,EAAE,SAAS,gBAAgB,EAAE,EAAE,KAAK,EAAE,aAAa,GAAG,SAAS,KAAK,EAAE,CAgCzI"}
1
+ {"version":3,"file":"metadataEntries.d.ts","sourceRoot":"","sources":["../../src/search/metadataEntries.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE1E,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAM1C,wBAAgB,eAAe,CAAC,OAAO,EAAE,SAAS,KAAK,EAAE,EAAE,SAAS,EAAE,SAAS,gBAAgB,EAAE,EAAE,KAAK,EAAE,aAAa,GAAG,SAAS,KAAK,EAAE,CAiCzI"}
@@ -1,5 +1,6 @@
1
1
  import { fileUrl } from "#files/url.ts";
2
2
  import { identityUrl } from "#metadata/links.ts";
3
+ import { attentionEntries } from "./attentionEntries.js";
3
4
  function itemUrl(document, model) {
4
5
  const id = document.parsed.fields.id;
5
6
  return model.unavailable.length === 0 && id && model.ids.get(id)?.length === 1 ? identityUrl(id) : fileUrl(document.file);
@@ -19,6 +20,7 @@ export function metadataEntries(entries, documents, model) {
19
20
  };
20
21
  });
21
22
  for (const document of documents) {
23
+ enriched.push(...attentionEntries(document, itemUrl(document, model)));
22
24
  const criteria = document.parsed.fields.criteria ?? [];
23
25
  for (const [index, criterion] of criteria.entries()) {
24
26
  if (criteria.filter((item) => item.id === criterion.id).length !== 1) {
@@ -1 +1 @@
1
- {"version":3,"file":"metadataEntries.js","sourceRoot":"","sources":["../../src/search/metadataEntries.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIjD,SAAS,OAAO,CAAC,QAA0B,EAAE,KAAoB;IAChE,MAAM,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;IACrC,OAAO,KAAK,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;AAC3H,CAAC;AACD,MAAM,UAAU,eAAe,CAAC,OAAyB,EAAE,SAAsC,EAAE,KAAoB;IACtH,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAChF,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACtC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,KAAK,CAAC,IAAI,KAAK,UAAU,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5C,OAAO,KAAK,CAAC;QACd,CAAC;QACD,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC;QACtC,OAAO;YACN,GAAG,KAAK;YACR,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC;YAC9B,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;SACpH,CAAC;IACH,CAAC,CAAC,CAAC;IACH,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAC;QACvD,KAAK,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;YACrD,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,SAAS,CAAC,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACtE,SAAS;YACV,CAAC;YACD,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,KAAK,EAAE,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;YAC1F,QAAQ,CAAC,IAAI,CAAC;gBACb,IAAI,EAAE,QAAQ,CAAC,IAAI;gBACnB,IAAI,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,cAAc,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE;gBACjF,IAAI,EAAE,SAAS;gBACf,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;gBACvC,IAAI,EAAE,SAAS,CAAC,IAAI;gBACpB,KAAK,EAAE,SAAS,CAAC,IAAI;aACrB,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IACD,OAAO,QAAQ,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"metadataEntries.js","sourceRoot":"","sources":["../../src/search/metadataEntries.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { Effect, FileSystem, Path } from "effect";
1
+ import { Effect, FileSystem } from "effect";
2
2
  import type { Changes } from "#files/changes.ts";
3
3
  import { type MetadataDocument, metadataModel } from "#metadata/model.ts";
4
4
  import { type Entry } from "./entries.ts";
@@ -9,5 +9,5 @@ export interface Snapshot {
9
9
  readonly revision: number;
10
10
  readonly unavailable: readonly string[];
11
11
  }
12
- export declare const searchSnapshot: (root: string, home: string | undefined, changes: Changes) => Effect.Effect<Effect.Effect<Snapshot, import("effect/PlatformError").PlatformError, FileSystem.FileSystem | Path.Path>, never, never>;
12
+ export declare const searchSnapshot: (root: string, home: string | undefined, changes: Changes) => Effect.Effect<Effect.Effect<Snapshot, import("effect/PlatformError").PlatformError, FileSystem.FileSystem | import("effect/Path").Path>, never, never>;
13
13
  //# sourceMappingURL=snapshot.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/search/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAU,IAAI,EAAkB,MAAM,QAAQ,CAAC;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,KAAK,gBAAgB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE1E,OAAO,EAAE,KAAK,KAAK,EAAa,MAAM,cAAc,CAAC;AAGrD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,SAAS,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,SAAS,KAAK,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,eAAO,MAAM,cAAc,qMA2CzB,CAAC"}
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/search/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAA0B,MAAM,QAAQ,CAAC;AACpE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,EAAE,KAAK,gBAAgB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE1E,OAAO,EAAE,KAAK,KAAK,EAAa,MAAM,cAAc,CAAC;AAGrD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,SAAS,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,SAAS,KAAK,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;IACjD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,eAAO,MAAM,cAAc,sNA0CzB,CAAC"}
@@ -1,5 +1,5 @@
1
- import { Effect, FileSystem, Option, Path, Ref, Semaphore } from "effect";
2
- import { within } from "#files/list.ts";
1
+ import { Effect, FileSystem, Option, Ref, Semaphore } from "effect";
2
+ import { markdownPath } from "#files/markdownPath.ts";
3
3
  import { metadataModel } from "#metadata/model.ts";
4
4
  import { metadataParse } from "#metadata/parse.ts";
5
5
  import { entriesOf } from "./entries.js";
@@ -14,15 +14,14 @@ export const searchSnapshot = Effect.fn("WorkBoard.searchSnapshot")(function* (r
14
14
  return hit;
15
15
  }
16
16
  const fs = yield* FileSystem.FileSystem;
17
- const path = yield* Path.Path;
18
17
  const files = yield* changes.files;
19
18
  const entries = [];
20
19
  const documents = [];
21
20
  const unavailable = [];
22
21
  for (const file of files) {
23
22
  const found = yield* Effect.option(Effect.gen(function* () {
24
- const real = yield* fs.realPath(path.join(root, file.path));
25
- if (!within(changes.realRoot, path.sep, real)) {
23
+ const real = yield* markdownPath(root, changes.realRoot, file.path);
24
+ if (real === undefined) {
26
25
  return undefined;
27
26
  }
28
27
  const source = yield* fs.readFileString(real);
@@ -1 +1 @@
1
- {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/search/snapshot.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/search/snapshot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAEpE,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAyB,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAc,SAAS,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAUvD,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,EAAE,CAAC,0BAA0B,CAAC,CAAC,QAAQ,CAAC,EAAE,IAAY,EAAE,IAAwB,EAAE,OAAgB;IACtI,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAuB,SAAS,CAAC,CAAC;IAChE,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAChC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC;QACzC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnC,IAAI,GAAG,EAAE,QAAQ,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;YACtE,OAAO,GAAG,CAAC;QACZ,CAAC;QACD,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC;QACxC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;QACnC,MAAM,OAAO,GAAY,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAuB,EAAE,CAAC;QACzC,MAAM,WAAW,GAAa,EAAE,CAAC;QACjC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CACjC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBACnB,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;gBACpE,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;oBACxB,OAAO,SAAS,CAAC;gBAClB,CAAC;gBACD,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;gBAC9C,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;gBACrC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;gBAChC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,KAAK,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;gBAC/H,OAAO,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;YAChD,CAAC,CAAC,CACF,CAAC;YACF,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;gBACzC,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;gBAC1F,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACvC,CAAC;iBAAM,CAAC;gBACP,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7B,CAAC;QACF,CAAC;QACD,MAAM,KAAK,GAAG,aAAa,CAAC,SAAS,EAAE,WAAW,EAAE,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;QAC5E,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QAC5D,MAAM,QAAQ,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;QAChF,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;QACtE,OAAO,QAAQ,CAAC;IACjB,CAAC,CAAC,CAAC;IACH,OAAO,SAAS,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/work-board",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Serve a folder of markdown files as a live local page that updates in place when a file changes",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/work-board#readme",
6
6
  "bugs": {
@@ -15,7 +15,7 @@
15
15
  "sideEffects": false,
16
16
  "type": "module",
17
17
  "imports": {
18
- "#test/*": "./test/*",
18
+ "#test/*": "./src/test-support/*",
19
19
  "#*.ts": {
20
20
  "source": "./src/*.ts",
21
21
  "types": "./dist/*.d.ts",
@@ -39,6 +39,7 @@
39
39
  "src",
40
40
  "!src/**/*.test.ts",
41
41
  "!src/**/*.spec.ts",
42
+ "!src/**/test-support",
42
43
  "CHANGELOG.md",
43
44
  "README.md"
44
45
  ],
@@ -51,7 +52,7 @@
51
52
  "devDependencies": {
52
53
  "@effect/platform-node": "4.0.0-rc.112",
53
54
  "@effect/platform-node-shared": "4.0.0-rc.112",
54
- "@shivaedev/quality": "0.9.0",
55
+ "@shivaedev/quality": "0.15.0",
55
56
  "@types/hast": "3.0.5",
56
57
  "@types/mdast": "4.0.4",
57
58
  "@types/node": "24.10.1",
@@ -59,7 +60,7 @@
59
60
  "effect": "4.0.0-rc.112",
60
61
  "happy-dom": "20.11.2",
61
62
  "typescript": "npm:@typescript/typescript6@6.0.2",
62
- "vitest": "4.1.9"
63
+ "vitest": "4.1.11"
63
64
  },
64
65
  "peerDependencies": {
65
66
  "@effect/platform-node": "4.0.0-rc.112",
@@ -0,0 +1,44 @@
1
+ import { fileUrl } from "#files/url.ts";
2
+ import { escapeHtml } from "#page/escape.ts";
3
+ import { referenceHtml } from "#page/metadataLinks.ts";
4
+ import type { Snapshot } from "#search/snapshot.ts";
5
+ import { type AttentionEntry, attentionRequests } from "./requests.ts";
6
+
7
+ function card(entry: AttentionEntry, snapshot: Snapshot): string {
8
+ const { request } = entry;
9
+ return `<article class="attention-card" data-key="attention:${escapeHtml(entry.itemId)}:${escapeHtml(request.id)}" data-attention-item="${escapeHtml(entry.itemId)}" data-request="${escapeHtml(request.id)}">
10
+ <p class="work-kind">${escapeHtml(request.kind)} request</p><h3><a href="${escapeHtml(entry.href)}">${escapeHtml(entry.title)}</a></h3>
11
+ <p class="work-id">${escapeHtml(entry.itemId)} / ${escapeHtml(request.id)}</p><p class="attention-reason">${escapeHtml(request.reason)}</p>
12
+ <dl><dt>Response needed from</dt><dd>${request.responseFrom.map(escapeHtml).join(", ")}</dd><dt>Recorded to unblock</dt><dd>${request.unblocks.map((target) => referenceHtml(target, target, snapshot.model)).join(", ")}</dd></dl>
13
+ <a class="attention-source" href="${escapeHtml(fileUrl(entry.document.file))}#attention-request-${encodeURIComponent(request.id)}">Source: ${escapeHtml(entry.document.file)}:${entry.line}</a></article>`;
14
+ }
15
+
16
+ export function overviewHtml(snapshot: Snapshot): string {
17
+ const projection = attentionRequests(snapshot);
18
+ const header =
19
+ '<header class="work-head"><p class="work-eyebrow">WORKSPACE · OVERVIEW</p><h1>What needs a response?</h1><p>Choose a recorded request to read its context, reasoning and evidence.</p><a href="/_board/work?view=board">Browse all work</a></header>';
20
+ const issues =
21
+ projection.issues.length > 0
22
+ ? `<details class="view-issues" data-key="attention-issues"><summary>Unclassified requests and source issues (${projection.issues.length})</summary><p>Ambiguous or invalid requests stay outside the queues. Read their original sources.</p><ul>${projection.issues.map((issue) => `<li><a href="${escapeHtml(fileUrl(issue.file))}">${escapeHtml(issue.file)}:${issue.line}</a> — ${escapeHtml(issue.message)}</li>`).join("")}</ul></details>`
23
+ : "";
24
+ if (!projection.complete) {
25
+ return `${header}<p class="attention-incomplete" role="status">Workspace incomplete. Could not read ${snapshot.unavailable.map(escapeHtml).join(", ")}. Requests are not counted or classified until the index is complete.</p>${issues}`;
26
+ }
27
+ let summary = "All quiet — no explicit open requests recorded.";
28
+ if (projection.entries.length > 0) {
29
+ summary = `${projection.entries.length} validated open request${projection.entries.length === 1 ? "" : "s"}`;
30
+ } else if (projection.issues.length > 0) {
31
+ summary = "No validated open requests. Check unresolved source issues before treating this workspace as quiet.";
32
+ }
33
+ const groups = [
34
+ { empty: "No open decision requests.", kind: "decision", title: "Decisions" },
35
+ { empty: "No open review requests.", kind: "review", title: "Reviews" },
36
+ { empty: "No open blocker requests.", kind: "blocker", title: "Blockers" },
37
+ ]
38
+ .map((group) => {
39
+ const entries = projection.entries.filter((entry) => entry.request.kind === group.kind);
40
+ return `<section class="attention-group" aria-labelledby="attention-${group.kind}"><h2 id="attention-${group.kind}" tabindex="-1">${group.title}<span>${entries.length}</span></h2>${entries.length > 0 ? entries.map((entry) => card(entry, snapshot)).join("") : `<p class="empty">${group.empty}</p>`}</section>`;
41
+ })
42
+ .join("");
43
+ return `${header}<p class="attention-total" role="status">${summary}</p><p class="attention-order">Decisions, reviews, then blockers. Within each group: source title, item ID, request ID. Recipients are recorded labels.</p>${issues}<div class="attention-groups">${groups}</div><p class="attention-footnote">These are authored requests. Closing one does not verify evidence or record an answer. Status, owner, file activity and finished runs do not create requests.</p>`;
44
+ }
@@ -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,8 +4,10 @@ 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";
7
8
  import { identity } from "#http/identity.ts";
8
9
  import { loopbackOnly } from "#http/loopback.ts";
10
+ import { overview } from "#http/overview.ts";
9
11
  import { page } from "#http/page.ts";
10
12
  import { respond } from "#http/respond.ts";
11
13
  import { search } from "#http/search.ts";
@@ -29,9 +31,9 @@ const routes = (options: BoardOptions) =>
29
31
  const mermaid = yield* mermaidRoot();
30
32
  const changes = yield* watchChanges(options.root);
31
33
  const context = yield* Effect.context<Services>();
32
- const serve = <E>(route: HttpRouter.PathInput, handler: Handler<E>) =>
34
+ const serve = <E>(route: HttpRouter.PathInput, handler: Handler<E>, method: "GET" | "POST" = "GET") =>
33
35
  router.add(
34
- "GET",
36
+ method,
35
37
  route,
36
38
  loopbackOnly((request) => Effect.provideContext(handler(request), context)),
37
39
  );
@@ -39,6 +41,9 @@ const routes = (options: BoardOptions) =>
39
41
  const index = yield* searchSnapshot(options.root, home, changes);
40
42
  yield* serve("/_board/search", search(index));
41
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");
42
47
  for (const [route, body, contentType] of ASSETS) {
43
48
  yield* serve(route, () => Effect.succeed(respond(body, contentType)));
44
49
  }
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.3.0";
7
+ const VERSION = "0.4.1";
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
 
@@ -16,10 +17,6 @@ interface Cached {
16
17
  readonly generation: number;
17
18
  }
18
19
 
19
- class WatchEnded extends Data.TaggedError("WatchEnded") {}
20
-
21
- const SETTLE = "100 millis";
22
- const BATCH = 256;
23
20
  const BACKLOG = 16;
24
21
  const RETRY = Schedule.min([Schedule.exponential("100 millis"), Schedule.spaced("5 seconds")]);
25
22
 
@@ -33,7 +30,6 @@ export const watchChanges = Effect.fn("WorkBoard.watchChanges")(function* (root:
33
30
  const cached = yield* Ref.make(Option.none<Cached>());
34
31
  const settled = yield* Deferred.make<void>();
35
32
  const stale = Ref.update(generation, (count) => count + 1);
36
- const relative = (changed: string) => (path.isAbsolute(changed) ? path.relative(root, changed) : changed).split(path.sep).join("/");
37
33
  const setWatching = (now: boolean) =>
38
34
  Effect.andThen(
39
35
  Effect.flatMap(Ref.getAndSet(watching, now), (was) =>
@@ -41,15 +37,7 @@ export const watchChanges = Effect.fn("WorkBoard.watchChanges")(function* (root:
41
37
  ),
42
38
  Deferred.succeed(settled, undefined),
43
39
  );
44
- const watchOnce = fs.watch(root, { recursive: true }).pipe(
45
- Stream.tap(() => stale),
46
- Stream.map((event) => relative(event.path)),
47
- Stream.filter(isMarkdown),
48
- Stream.groupedWithin(BATCH, SETTLE),
49
- Stream.runForEach((paths) => PubSub.publish(events, { _tag: "Changed", paths: [...new Set(paths)] })),
50
- Effect.andThen(Effect.fail(new WatchEnded())),
51
- );
52
- 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 }));
53
41
  yield* attempt.pipe(
54
42
  Effect.sandbox,
55
43
  Effect.tapError((cause) => Effect.andThen(setWatching(false), Effect.logWarning("Watching the folder failed; retrying", cause))),
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,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
+ });