@shivaedev/work-board 0.3.0 → 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.
- package/CHANGELOG.md +27 -0
- package/README.md +70 -11
- package/dist/attention/overviewHtml.d.ts +3 -0
- package/dist/attention/overviewHtml.d.ts.map +1 -0
- package/dist/attention/overviewHtml.js +41 -0
- package/dist/attention/overviewHtml.js.map +1 -0
- package/dist/attention/requests.d.ts +24 -0
- package/dist/attention/requests.d.ts.map +1 -0
- package/dist/attention/requests.js +40 -0
- package/dist/attention/requests.js.map +1 -0
- package/dist/attention/style.d.ts +2 -0
- package/dist/attention/style.d.ts.map +1 -0
- package/dist/attention/style.js +10 -0
- package/dist/attention/style.js.map +1 -0
- package/dist/board.d.ts.map +1 -1
- package/dist/board.js +6 -1
- package/dist/board.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/files/changes.d.ts.map +1 -1
- package/dist/files/changes.js +4 -9
- package/dist/files/changes.js.map +1 -1
- package/dist/files/home.d.ts.map +1 -1
- package/dist/files/home.js +4 -0
- package/dist/files/home.js.map +1 -1
- package/dist/files/list.d.ts +7 -0
- package/dist/files/list.d.ts.map +1 -1
- package/dist/files/list.js +23 -13
- package/dist/files/list.js.map +1 -1
- package/dist/files/markdownPath.d.ts +3 -0
- package/dist/files/markdownPath.d.ts.map +1 -0
- package/dist/files/markdownPath.js +31 -0
- package/dist/files/markdownPath.js.map +1 -0
- package/dist/files/watchDirectories.d.ts +9 -0
- package/dist/files/watchDirectories.d.ts.map +1 -0
- package/dist/files/watchDirectories.js +33 -0
- package/dist/files/watchDirectories.js.map +1 -0
- package/dist/history/baseline.d.ts +19 -0
- package/dist/history/baseline.d.ts.map +1 -0
- package/dist/history/baseline.js +40 -0
- package/dist/history/baseline.js.map +1 -0
- package/dist/history/compare.d.ts +14 -0
- package/dist/history/compare.d.ts.map +1 -0
- package/dist/history/compare.js +47 -0
- package/dist/history/compare.js.map +1 -0
- package/dist/history/documents.d.ts +10 -0
- package/dist/history/documents.d.ts.map +1 -0
- package/dist/history/documents.js +15 -0
- package/dist/history/documents.js.map +1 -0
- package/dist/history/fields.d.ts +3 -0
- package/dist/history/fields.d.ts.map +1 -0
- package/dist/history/fields.js +27 -0
- package/dist/history/fields.js.map +1 -0
- package/dist/history/html.d.ts +4 -0
- package/dist/history/html.d.ts.map +1 -0
- package/dist/history/html.js +22 -0
- package/dist/history/html.js.map +1 -0
- package/dist/history/limits.d.ts +3 -0
- package/dist/history/limits.d.ts.map +1 -0
- package/dist/history/limits.js +3 -0
- package/dist/history/limits.js.map +1 -0
- package/dist/history/response.d.ts +4 -0
- package/dist/history/response.d.ts.map +1 -0
- package/dist/history/response.js +20 -0
- package/dist/history/response.js.map +1 -0
- package/dist/history/schema.d.ts +24 -0
- package/dist/history/schema.d.ts.map +1 -0
- package/dist/history/schema.js +19 -0
- package/dist/history/schema.js.map +1 -0
- package/dist/history/script.d.ts +2 -0
- package/dist/history/script.d.ts.map +1 -0
- package/dist/history/script.js +113 -0
- package/dist/history/script.js.map +1 -0
- package/dist/http/assets.d.ts.map +1 -1
- package/dist/http/assets.js +2 -0
- package/dist/http/assets.js.map +1 -1
- package/dist/http/history.d.ts +7 -0
- package/dist/http/history.d.ts.map +1 -0
- package/dist/http/history.js +45 -0
- package/dist/http/history.js.map +1 -0
- package/dist/http/overview.d.ts +5 -0
- package/dist/http/overview.d.ts.map +1 -0
- package/dist/http/overview.js +13 -0
- package/dist/http/overview.js.map +1 -0
- package/dist/http/page.d.ts +2 -2
- package/dist/http/page.d.ts.map +1 -1
- package/dist/http/page.js +5 -6
- package/dist/http/page.js.map +1 -1
- package/dist/metadata/attentionProblems.d.ts +4 -0
- package/dist/metadata/attentionProblems.d.ts.map +1 -0
- package/dist/metadata/attentionProblems.js +16 -0
- package/dist/metadata/attentionProblems.js.map +1 -0
- package/dist/metadata/backlinks.d.ts.map +1 -1
- package/dist/metadata/backlinks.js +5 -0
- package/dist/metadata/backlinks.js.map +1 -1
- package/dist/metadata/fields.d.ts +8 -0
- package/dist/metadata/fields.d.ts.map +1 -1
- package/dist/metadata/problems.d.ts.map +1 -1
- package/dist/metadata/problems.js +3 -1
- package/dist/metadata/problems.js.map +1 -1
- package/dist/metadata/relationshipLabel.d.ts.map +1 -1
- package/dist/metadata/relationshipLabel.js +1 -0
- package/dist/metadata/relationshipLabel.js.map +1 -1
- package/dist/metadata/schema.d.ts +10 -0
- package/dist/metadata/schema.d.ts.map +1 -1
- package/dist/metadata/schema.js +9 -0
- package/dist/metadata/schema.js.map +1 -1
- package/dist/page/attentionDetails.d.ts +4 -0
- package/dist/page/attentionDetails.d.ts.map +1 -0
- package/dist/page/attentionDetails.js +13 -0
- package/dist/page/attentionDetails.js.map +1 -0
- package/dist/page/client.d.ts +1 -1
- package/dist/page/client.d.ts.map +1 -1
- package/dist/page/client.js +1 -0
- package/dist/page/client.js.map +1 -1
- package/dist/page/metadataDetails.d.ts.map +1 -1
- package/dist/page/metadataDetails.js +2 -0
- package/dist/page/metadataDetails.js.map +1 -1
- package/dist/page/navigationScript.js +1 -1
- package/dist/page/shell.d.ts +1 -1
- package/dist/page/shell.d.ts.map +1 -1
- package/dist/page/shell.js +2 -2
- package/dist/page/shell.js.map +1 -1
- package/dist/page/stateScript.d.ts.map +1 -1
- package/dist/page/stateScript.js +6 -0
- package/dist/page/stateScript.js.map +1 -1
- package/dist/page/style.d.ts.map +1 -1
- package/dist/page/style.js +2 -1
- package/dist/page/style.js.map +1 -1
- package/dist/search/attentionEntries.d.ts +4 -0
- package/dist/search/attentionEntries.d.ts.map +1 -0
- package/dist/search/attentionEntries.js +20 -0
- package/dist/search/attentionEntries.js.map +1 -0
- package/dist/search/metadataEntries.d.ts.map +1 -1
- package/dist/search/metadataEntries.js +2 -0
- package/dist/search/metadataEntries.js.map +1 -1
- package/dist/search/snapshot.d.ts +2 -2
- package/dist/search/snapshot.d.ts.map +1 -1
- package/dist/search/snapshot.js +4 -5
- package/dist/search/snapshot.js.map +1 -1
- package/package.json +5 -4
- package/src/attention/overviewHtml.ts +44 -0
- package/src/attention/requests.ts +64 -0
- package/src/attention/style.ts +9 -0
- package/src/board.ts +7 -2
- package/src/cli.ts +1 -1
- package/src/files/changes.ts +4 -16
- package/src/files/home.ts +4 -0
- package/src/files/list.ts +25 -14
- package/src/files/markdownPath.ts +31 -0
- package/src/files/watchDirectories.ts +60 -0
- package/src/history/baseline.ts +43 -0
- package/src/history/compare.ts +54 -0
- package/src/history/documents.ts +21 -0
- package/src/history/fields.ts +32 -0
- package/src/history/html.ts +23 -0
- package/src/history/limits.ts +2 -0
- package/src/history/response.ts +23 -0
- package/src/history/schema.ts +36 -0
- package/src/history/script.ts +112 -0
- package/src/http/assets.ts +2 -0
- package/src/http/history.ts +50 -0
- package/src/http/overview.ts +19 -0
- package/src/http/page.ts +6 -6
- package/src/metadata/attentionProblems.ts +18 -0
- package/src/metadata/backlinks.ts +7 -0
- package/src/metadata/problems.ts +5 -1
- package/src/metadata/relationshipLabel.ts +1 -0
- package/src/metadata/schema.ts +9 -0
- package/src/page/attentionDetails.ts +18 -0
- package/src/page/client.ts +1 -0
- package/src/page/metadataDetails.ts +2 -0
- package/src/page/navigationScript.ts +1 -1
- package/src/page/shell.ts +11 -3
- package/src/page/stateScript.ts +6 -0
- package/src/page/style.ts +2 -1
- package/src/search/attentionEntries.ts +22 -0
- package/src/search/metadataEntries.ts +2 -0
- package/src/search/snapshot.ts +4 -5
package/dist/page/style.js
CHANGED
|
@@ -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
|
package/dist/page/style.js.map
CHANGED
|
@@ -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 @@
|
|
|
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;
|
|
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;
|
|
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;AAEjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGzD,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,QAAQ,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;QACvE,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,4 +1,4 @@
|
|
|
1
|
-
import { Effect, FileSystem
|
|
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,
|
|
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"}
|
package/dist/search/snapshot.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Effect, FileSystem, Option,
|
|
2
|
-
import {
|
|
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*
|
|
25
|
-
if (
|
|
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":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/search/snapshot.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/work-board",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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.
|
|
55
|
+
"@shivaedev/quality": "0.14.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.
|
|
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
|
-
|
|
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.
|
|
7
|
+
const VERSION = "0.4.0";
|
|
8
8
|
|
|
9
9
|
const serve = Effect.fn("WorkBoard.serve")(function* (input: { readonly dir: string; readonly port: number; readonly home: Option.Option<string> }) {
|
|
10
10
|
const path = yield* Path.Path;
|
package/src/files/changes.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
|
+
import type { Snapshot } from "#search/snapshot.ts";
|
|
3
|
+
import { MAX_AGE, MAX_BYTES } from "./limits.ts";
|
|
4
|
+
import { Baseline } from "./schema.ts";
|
|
5
|
+
|
|
6
|
+
const JsonBaseline = Schema.fromJsonString(Baseline);
|
|
7
|
+
function byteLength(text: string): number {
|
|
8
|
+
return new TextEncoder().encode(text).length;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function readBaseline(raw: string | null, now: number) {
|
|
12
|
+
if (raw === null) {
|
|
13
|
+
return { baseline: undefined, discard: false, reason: "No previous snapshot. Start remembering changes to compare future source observations." };
|
|
14
|
+
}
|
|
15
|
+
if (byteLength(raw) > MAX_BYTES) {
|
|
16
|
+
return { baseline: undefined, discard: true, reason: "Remembered snapshot exceeds 2 MiB; history is unavailable." };
|
|
17
|
+
}
|
|
18
|
+
const decoded = Schema.decodeUnknownOption(JsonBaseline, { onExcessProperty: "error" })(raw);
|
|
19
|
+
if (Option.isNone(decoded)) {
|
|
20
|
+
return { baseline: undefined, discard: true, reason: "Remembered snapshot is invalid or unsupported; history is unavailable." };
|
|
21
|
+
}
|
|
22
|
+
const elapsed = now - Date.parse(decoded.value.recordedAt);
|
|
23
|
+
if (elapsed < 0) {
|
|
24
|
+
return { baseline: undefined, discard: true, reason: "Remembered observation is in the future; history is unavailable." };
|
|
25
|
+
}
|
|
26
|
+
if (elapsed >= MAX_AGE) {
|
|
27
|
+
return { baseline: undefined, discard: true, reason: "Remembered snapshot expired after 30 days; start again to establish a new baseline." };
|
|
28
|
+
}
|
|
29
|
+
return { baseline: decoded.value, discard: false, reason: "" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function observeBaseline(snapshot: Snapshot, now: number): string | undefined {
|
|
33
|
+
if (snapshot.unavailable.length > 0) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const baseline: Baseline = {
|
|
37
|
+
documents: snapshot.documents.map((document) => ({ file: document.file, source: (document.parsed.raw ?? "") + document.parsed.body })),
|
|
38
|
+
recordedAt: new Date(now).toISOString(),
|
|
39
|
+
version: 1,
|
|
40
|
+
};
|
|
41
|
+
const raw = Schema.encodeSync(JsonBaseline)(baseline);
|
|
42
|
+
return byteLength(raw) <= MAX_BYTES ? raw : undefined;
|
|
43
|
+
}
|