@shivaedev/work-board 0.9.1 → 0.11.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 +29 -0
- package/README.md +49 -1
- package/assets/native.js +35 -29
- package/dist/board.d.ts.map +1 -1
- package/dist/board.js +20 -1
- package/dist/board.js.map +1 -1
- package/dist/browser/client.d.ts +2 -0
- package/dist/browser/client.d.ts.map +1 -1
- package/dist/browser/client.js +2 -1
- package/dist/browser/client.js.map +1 -1
- package/dist/browser/entry.d.ts.map +1 -1
- package/dist/browser/entry.js +2 -0
- package/dist/browser/entry.js.map +1 -1
- package/dist/browser/handoffs/copy.d.ts +3 -0
- package/dist/browser/handoffs/copy.d.ts.map +1 -0
- package/dist/browser/handoffs/copy.js +47 -0
- package/dist/browser/handoffs/copy.js.map +1 -0
- package/dist/browser/handoffs/fields.d.ts +11 -0
- package/dist/browser/handoffs/fields.d.ts.map +1 -0
- package/dist/browser/handoffs/fields.js +45 -0
- package/dist/browser/handoffs/fields.js.map +1 -0
- package/dist/browser/handoffs/schema.d.ts +89 -0
- package/dist/browser/handoffs/schema.d.ts.map +1 -0
- package/dist/browser/handoffs/schema.js +38 -0
- package/dist/browser/handoffs/schema.js.map +1 -0
- package/dist/browser/handoffs/setup.d.ts +2 -0
- package/dist/browser/handoffs/setup.d.ts.map +1 -0
- package/dist/browser/handoffs/setup.js +108 -0
- package/dist/browser/handoffs/setup.js.map +1 -0
- package/dist/browser/responses/draftSession.d.ts +1 -1
- package/dist/browser/responses/draftSession.d.ts.map +1 -1
- package/dist/browser/responses/draftSession.js +4 -4
- package/dist/browser/responses/draftSession.js.map +1 -1
- package/dist/browser/responses/preview.js.map +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/files/list.d.ts.map +1 -1
- package/dist/handoffs/html.d.ts +37 -0
- package/dist/handoffs/html.d.ts.map +1 -0
- package/dist/handoffs/html.js +31 -0
- package/dist/handoffs/html.js.map +1 -0
- package/dist/handoffs/page.d.ts +7 -0
- package/dist/handoffs/page.d.ts.map +1 -0
- package/dist/handoffs/page.js +31 -0
- package/dist/handoffs/page.js.map +1 -0
- package/dist/handoffs/records.d.ts +7 -0
- package/dist/handoffs/records.d.ts.map +1 -0
- package/dist/handoffs/records.js +30 -0
- package/dist/handoffs/records.js.map +1 -0
- package/dist/handoffs/service.d.ts +55 -0
- package/dist/handoffs/service.d.ts.map +1 -0
- package/dist/handoffs/service.js +82 -0
- package/dist/handoffs/service.js.map +1 -0
- package/dist/handoffs/source.d.ts +11 -0
- package/dist/handoffs/source.d.ts.map +1 -0
- package/dist/handoffs/source.js +31 -0
- package/dist/handoffs/source.js.map +1 -0
- package/dist/http/assets.d.ts.map +1 -1
- package/dist/http/assets.js.map +1 -1
- package/dist/http/events.d.ts.map +1 -1
- package/dist/http/loopback.d.ts.map +1 -1
- package/dist/http/page.d.ts.map +1 -1
- package/dist/http/respond.d.ts.map +1 -1
- package/dist/metadata/backlinks.d.ts.map +1 -1
- package/dist/metadata/backlinks.js +1 -0
- package/dist/metadata/backlinks.js.map +1 -1
- package/dist/metadata/fields.d.ts +14 -1
- package/dist/metadata/fields.d.ts.map +1 -1
- package/dist/metadata/model.js.map +1 -1
- package/dist/metadata/problems.d.ts.map +1 -1
- package/dist/metadata/problems.js +1 -0
- package/dist/metadata/problems.js.map +1 -1
- package/dist/metadata/schema.d.ts +18 -1
- package/dist/metadata/schema.d.ts.map +1 -1
- package/dist/metadata/schema.js +3 -1
- package/dist/metadata/schema.js.map +1 -1
- package/dist/page/escape.d.ts.map +1 -1
- package/dist/page/metadata.d.ts.map +1 -1
- package/dist/page/metadata.js +12 -4
- package/dist/page/metadata.js.map +1 -1
- package/dist/page/metadataDetails.d.ts.map +1 -1
- package/dist/page/metadataDetails.js +3 -0
- package/dist/page/metadataDetails.js.map +1 -1
- package/dist/page/nav.d.ts.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.map +1 -1
- package/dist/page/startTemplates.d.ts +109 -3
- package/dist/page/startTemplates.d.ts.map +1 -1
- package/dist/page/startTemplates.js +20 -0
- package/dist/page/startTemplates.js.map +1 -1
- package/dist/page/style.d.ts.map +1 -1
- package/dist/page/style.js +6 -5
- package/dist/page/style.js.map +1 -1
- package/dist/page/swap.d.ts +1 -1
- package/dist/page/swap.d.ts.map +1 -1
- package/dist/page/swap.js +2 -2
- package/dist/page/updated.d.ts.map +1 -1
- package/dist/render/board.d.ts.map +1 -1
- package/dist/render/callouts.d.ts.map +1 -1
- package/dist/render/code.d.ts.map +1 -1
- package/dist/render/documentHeadings.d.ts.map +1 -1
- package/dist/render/documentLinks.d.ts.map +1 -1
- package/dist/render/footnotes.d.ts.map +1 -1
- package/dist/render/highlighter.d.ts.map +1 -1
- package/dist/render/highlighter.js.map +1 -1
- package/dist/render/markdown.js.map +1 -1
- package/dist/render/questionnaireDocuments.d.ts.map +1 -1
- package/dist/render/visualDocuments.d.ts.map +1 -1
- package/dist/responses/agent.d.ts +8 -8
- package/dist/responses/agent.js.map +1 -1
- package/dist/responses/flags.d.ts.map +1 -1
- package/dist/responses/outcome.d.ts.map +1 -1
- package/dist/responses/publish.d.ts +1 -1
- package/dist/responses/publish.d.ts.map +1 -1
- package/dist/responses/publish.js +7 -7
- package/dist/responses/publish.js.map +1 -1
- package/dist/responses/publishIn.d.ts +1 -1
- package/dist/responses/publishIn.d.ts.map +1 -1
- package/dist/responses/publishIn.js +3 -3
- package/dist/responses/publishIn.js.map +1 -1
- package/dist/responses/waitFor.d.ts.map +1 -1
- package/dist/results/criteria.d.ts +3 -0
- package/dist/results/criteria.d.ts.map +1 -0
- package/dist/results/criteria.js +34 -0
- package/dist/results/criteria.js.map +1 -0
- package/dist/results/feedback.d.ts +7 -0
- package/dist/results/feedback.d.ts.map +1 -0
- package/dist/results/feedback.js +32 -0
- package/dist/results/feedback.js.map +1 -0
- package/dist/results/page.d.ts +7 -0
- package/dist/results/page.d.ts.map +1 -0
- package/dist/results/page.js +40 -0
- package/dist/results/page.js.map +1 -0
- package/dist/rpc/contract.d.ts +67 -1
- package/dist/rpc/contract.d.ts.map +1 -1
- package/dist/rpc/contract.js +3 -2
- package/dist/rpc/contract.js.map +1 -1
- package/dist/rpc/handoffContract.d.ts +142 -0
- package/dist/rpc/handoffContract.d.ts.map +1 -0
- package/dist/rpc/handoffContract.js +26 -0
- package/dist/rpc/handoffContract.js.map +1 -0
- package/dist/rpc/server.d.ts +2 -0
- package/dist/rpc/server.d.ts.map +1 -1
- package/dist/rpc/server.js +4 -1
- package/dist/rpc/server.js.map +1 -1
- package/dist/search/collector.d.ts.map +1 -1
- package/dist/search/entries.js.map +1 -1
- package/dist/search/linkCollector.d.ts.map +1 -1
- package/dist/serve-cli.js +2 -2
- package/dist/serve-cli.js.map +1 -1
- package/dist/serve.d.ts.map +1 -1
- package/dist/views/state.js.map +1 -1
- package/package.json +3 -3
- package/src/board.ts +20 -1
- package/src/browser/client.ts +3 -1
- package/src/browser/entry.ts +2 -0
- package/src/browser/handoffs/copy.ts +45 -0
- package/src/browser/handoffs/fields.ts +50 -0
- package/src/browser/handoffs/schema.ts +44 -0
- package/src/browser/handoffs/setup.ts +113 -0
- package/src/browser/responses/draftSession.ts +11 -4
- package/src/handoffs/html.ts +42 -0
- package/src/handoffs/page.ts +45 -0
- package/src/handoffs/records.ts +34 -0
- package/src/handoffs/service.ts +103 -0
- package/src/handoffs/source.ts +35 -0
- package/src/metadata/backlinks.ts +1 -0
- package/src/metadata/problems.ts +1 -0
- package/src/metadata/schema.ts +3 -1
- package/src/page/metadata.ts +14 -4
- package/src/page/metadataDetails.ts +3 -0
- package/src/page/navigationScript.ts +1 -1
- package/src/page/shell.ts +1 -1
- package/src/page/startTemplates.ts +20 -0
- package/src/page/style.ts +6 -5
- package/src/page/swap.ts +2 -2
- package/src/responses/publish.ts +7 -7
- package/src/responses/publishIn.ts +10 -3
- package/src/results/criteria.ts +37 -0
- package/src/results/feedback.ts +44 -0
- package/src/results/page.ts +58 -0
- package/src/rpc/contract.ts +5 -2
- package/src/rpc/handoffContract.ts +26 -0
- package/src/rpc/server.ts +6 -1
- package/src/serve-cli.ts +2 -2
package/src/responses/publish.ts
CHANGED
|
@@ -9,20 +9,20 @@ function failure(cause: unknown): ResponseFailed {
|
|
|
9
9
|
? cause
|
|
10
10
|
: new ResponseFailed({
|
|
11
11
|
code: "Unavailable",
|
|
12
|
-
message: "The
|
|
12
|
+
message: "The local record could not be recorded. Keep the draft and retry after checking local filesystem access.",
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
-
export function publish(root: string, realRoot: string, id: string, content: string) {
|
|
15
|
+
export function publish(root: string, realRoot: string, id: string, content: string, namespace: "responses" | "handoffs" = "responses") {
|
|
16
16
|
return Effect.tryPromise({
|
|
17
17
|
catch: failure,
|
|
18
18
|
try: async () => {
|
|
19
19
|
if (/^[a-zA-Z\d][a-zA-Z\d._-]*$/u.exec(id) === null) {
|
|
20
|
-
throw new ResponseFailed({ code: "Conflict", message: "Invalid
|
|
20
|
+
throw new ResponseFailed({ code: "Conflict", message: "Invalid record identity." });
|
|
21
21
|
}
|
|
22
22
|
if (process.platform !== "linux") {
|
|
23
23
|
throw new ResponseFailed({
|
|
24
24
|
code: "Unsupported",
|
|
25
|
-
message: "
|
|
25
|
+
message: "Local record publication currently requires Linux directory descriptors. Reading remains available.",
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
28
|
const directory = await open(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
@@ -32,16 +32,16 @@ export function publish(root: string, realRoot: string, id: string, content: str
|
|
|
32
32
|
throw new ResponseFailed({ code: "Conflict", message: "The workspace root changed. Restart against the intended root." });
|
|
33
33
|
}
|
|
34
34
|
try {
|
|
35
|
-
await mkdir(`${base}
|
|
35
|
+
await mkdir(`${base}/${namespace}`);
|
|
36
36
|
} catch (cause) {
|
|
37
37
|
if (!(cause instanceof Error && "code" in cause && cause.code === "EEXIST")) {
|
|
38
38
|
throw cause;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
await directory.sync();
|
|
42
|
-
const target = await open(`${base}
|
|
42
|
+
const target = await open(`${base}/${namespace}`, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
43
43
|
try {
|
|
44
|
-
await publishIn(target, root, realRoot, id, content);
|
|
44
|
+
await publishIn(target, root, realRoot, id, content, namespace);
|
|
45
45
|
} finally {
|
|
46
46
|
await target.close();
|
|
47
47
|
}
|
|
@@ -4,7 +4,7 @@ import { type FileHandle, link, open, readFile, realpath, unlink } from "node:fs
|
|
|
4
4
|
import { ResponseFailed } from "#browser/responses/schema.ts";
|
|
5
5
|
|
|
6
6
|
function uncertain(cause: unknown) {
|
|
7
|
-
return new ResponseFailed({ cause, code: "Uncertain", message: "Publication may have completed. Re-read the
|
|
7
|
+
return new ResponseFailed({ cause, code: "Uncertain", message: "Publication may have completed. Re-read the record identity before retrying." });
|
|
8
8
|
}
|
|
9
9
|
function exists(cause: unknown) {
|
|
10
10
|
return cause instanceof Error && "code" in cause && cause.code === "EEXIST";
|
|
@@ -28,9 +28,16 @@ async function linkOrReconcile(temporary: string, name: string, content: string)
|
|
|
28
28
|
}
|
|
29
29
|
return false;
|
|
30
30
|
}
|
|
31
|
-
export async function publishIn(
|
|
31
|
+
export async function publishIn(
|
|
32
|
+
target: FileHandle,
|
|
33
|
+
root: string,
|
|
34
|
+
realRoot: string,
|
|
35
|
+
id: string,
|
|
36
|
+
content: string,
|
|
37
|
+
namespace: "responses" | "handoffs" = "responses",
|
|
38
|
+
) {
|
|
32
39
|
const folder = `/proc/self/fd/${target.fd}`;
|
|
33
|
-
const expected = `${realRoot}
|
|
40
|
+
const expected = `${realRoot}/${namespace}`;
|
|
34
41
|
async function validate() {
|
|
35
42
|
if ((await realpath(folder)) !== expected || (await realpath(root)) !== realRoot) {
|
|
36
43
|
throw new ResponseFailed({ code: "Conflict", message: "The write boundary changed." });
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { referenceTarget } from "#metadata/links.ts";
|
|
2
|
+
import type { MetadataDocument, MetadataModel } from "#metadata/model.ts";
|
|
3
|
+
import { escapeHtml } from "#page/escape.ts";
|
|
4
|
+
import { referenceHtml, sourceHtml, valueHtml } from "#page/metadataLinks.ts";
|
|
5
|
+
|
|
6
|
+
function criterionRow(reference: string, text: string, result: MetadataDocument, model: MetadataModel): string {
|
|
7
|
+
if (!referenceTarget(reference, model)) {
|
|
8
|
+
return `<li>${escapeHtml(text)} — criterion identity is ambiguous; evidence association unavailable.</li>`;
|
|
9
|
+
}
|
|
10
|
+
const claims = result.parsed.fields.evidence?.filter((claim) => claim.criterion === reference) ?? [];
|
|
11
|
+
const evidence =
|
|
12
|
+
claims.length > 0
|
|
13
|
+
? `<p>${claims.length} recorded claim(s) in this result; not verified acceptance.</p><ul>${claims.map((claim) => `<li>${sourceHtml(claim.source, result.file, model)}<dl><dt>Checked Git revision (reported)</dt><dd>${valueHtml(claim.checkedRevision)}</dd><dt>Method</dt><dd>${valueHtml(claim.method)}</dd><dt>Outcome (reported)</dt><dd>${valueHtml(claim.outcome)}</dd><dt>Observed time</dt><dd>${valueHtml(claim.observedAt)}</dd></dl></li>`).join("")}</ul>`
|
|
14
|
+
: "<p>No recorded evidence in this result naming this criterion. Acceptance is not established.</p>";
|
|
15
|
+
return `<li>${referenceHtml(text, reference, model)}${evidence}</li>`;
|
|
16
|
+
}
|
|
17
|
+
function targetRow(target: string, result: MetadataDocument, model: MetadataModel): string {
|
|
18
|
+
const [id, criterion] = target.split("#");
|
|
19
|
+
const document = model.ids.get(id ?? "")?.[0];
|
|
20
|
+
if (!(document && referenceTarget(target, model))) {
|
|
21
|
+
return `<li>${referenceHtml(target, target, model)} — criteria unavailable.</li>`;
|
|
22
|
+
}
|
|
23
|
+
const criteria = (document.parsed.fields.criteria ?? []).filter((item) => !criterion || item.id === criterion);
|
|
24
|
+
const rows =
|
|
25
|
+
criteria.length > 0
|
|
26
|
+
? `<ul>${criteria.map((item) => criterionRow(`${id}#${item.id}`, item.text, result, model)).join("")}</ul>`
|
|
27
|
+
: "<p>No criteria declared in this source.</p>";
|
|
28
|
+
return `<li>${referenceHtml(target, target, model)} · source status: ${valueHtml(document.parsed.fields.status)}${rows}</li>`;
|
|
29
|
+
}
|
|
30
|
+
export function resultCriteria(result: MetadataDocument, model: MetadataModel): string {
|
|
31
|
+
const targets = new Set(result.parsed.fields.relationships?.map((link) => link.target));
|
|
32
|
+
if (result.parsed.fields.id) {
|
|
33
|
+
targets.add(result.parsed.fields.id);
|
|
34
|
+
}
|
|
35
|
+
const rows = [...targets].map((target) => targetRow(target, result, model));
|
|
36
|
+
return `<section id="result-criteria"><h2>Criteria and this result’s claims</h2><p>Only explicit relationships and criterion references are associated. Missing provenance stays missing. Git revisions are authored records; Work Board does not compare them with the current checkout. Evidence freshness is unknown.</p>${rows.length > 0 ? `<ul>${rows.join("")}</ul>` : "<p>No explicitly linked work or criteria.</p>"}<p>${escapeHtml(result.file)} does not change the linked work’s status.</p></section>`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { fileUrl } from "#files/url.ts";
|
|
3
|
+
import type { MetadataDocument } from "#metadata/model.ts";
|
|
4
|
+
import { escapeHtml } from "#page/escape.ts";
|
|
5
|
+
import type { RenderFailed } from "#render/failed.ts";
|
|
6
|
+
import type { Highlighter } from "#render/highlighter.ts";
|
|
7
|
+
import { renderMarkdown } from "#render/markdown.ts";
|
|
8
|
+
import { readResponses } from "#responses/read.ts";
|
|
9
|
+
import { malformedResponse, questionFrom, revisionOf } from "#responses/records.ts";
|
|
10
|
+
import type { Snapshot } from "#search/snapshot.ts";
|
|
11
|
+
|
|
12
|
+
export const resultFeedback = Effect.fn("WorkBoard.resultFeedback")(function* (
|
|
13
|
+
result: MetadataDocument,
|
|
14
|
+
data: Snapshot,
|
|
15
|
+
): Effect.fn.Return<string, RenderFailed, Highlighter> {
|
|
16
|
+
const unknown =
|
|
17
|
+
data.unavailable.length > 0
|
|
18
|
+
|| data.documents.some((document) => malformedResponse(document) || (document.parsed.fields.kind === "question" && !questionFrom(document)));
|
|
19
|
+
const questions = data.documents.flatMap((document) => {
|
|
20
|
+
const question = questionFrom(document);
|
|
21
|
+
return question && question.question.item === result.parsed.fields.id ? [question] : [];
|
|
22
|
+
});
|
|
23
|
+
const read = readResponses(Effect.succeed(data));
|
|
24
|
+
const readings = yield* Effect.forEach(questions, (question) => Effect.result(read(question.id)));
|
|
25
|
+
if (unknown || readings.some((reading) => reading._tag === "Failure")) {
|
|
26
|
+
return '<section id="result-feedback"><h2>Recorded human feedback</h2><p role="status">Response history is unknown or ambiguous. Unreadable records do not establish that no feedback exists. Read the source files before relying on it.</p></section>';
|
|
27
|
+
}
|
|
28
|
+
const current = revisionOf((result.parsed.raw ?? "") + result.parsed.body);
|
|
29
|
+
const records = readings.flatMap((reading) =>
|
|
30
|
+
reading._tag === "Success" ? reading.success.responses.map((response) => ({ question: reading.success.question, response })) : [],
|
|
31
|
+
);
|
|
32
|
+
records.sort((a, b) => b.response.response.recordedAt - a.response.response.recordedAt || a.response.id.localeCompare(b.response.id));
|
|
33
|
+
const bodies = yield* Effect.forEach(records, ({ question, response }) =>
|
|
34
|
+
Effect.gen(function* () {
|
|
35
|
+
const matching = question.question.reviewedRevision === current && question.question.source === result.file;
|
|
36
|
+
const responseFile = data.model.ids.get(response.id)?.[0]?.file ?? `responses/${response.id}.md`;
|
|
37
|
+
const questionFile = data.model.ids.get(question.id)?.[0]?.file ?? `responses/${question.id}.md`;
|
|
38
|
+
const body = yield* renderMarkdown(response.body, { file: responseFile, safe: true });
|
|
39
|
+
const selected = response.response.answers?.map((answer) => `${answer.prompt}: ${answer.selected.join(", ") || "text only"}`).join("; ");
|
|
40
|
+
return `<article id="${escapeHtml(response.id)}"><h3>${escapeHtml(response.response.author)} · ${escapeHtml(response.response.type)}</h3><p><b>${matching ? "Feedback on this exact result revision" : "Earlier or moved result context — does not apply automatically to this revision"}</b></p><p>Reviewed source: ${escapeHtml(question.question.source)} · SHA-256 <code>${question.question.reviewedRevision}</code></p>${selected ? `<p>Recorded option IDs: ${escapeHtml(selected)}. Labels and reasoning are recorded below.</p>` : ""}${response.response.supersedes ? `<p>Explicitly supersedes <a href="${fileUrl(data.model.ids.get(response.response.supersedes)?.[0]?.file ?? `responses/${response.response.supersedes}.md`)}">${escapeHtml(response.response.supersedes)}</a>.</p>` : ""}${body}<p><a href="${fileUrl(responseFile)}">Response source</a> · <a href="${fileUrl(questionFile)}">Registered reviewed context</a></p><details><summary>Exact reviewed result source</summary><pre>${escapeHtml(question.context)}</pre></details></article>`;
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
return `<section id="result-feedback"><h2>Recorded human feedback</h2><p>Human direction is pinned to its reviewed report. Option IDs and authored acceptance text are not a computed verified-acceptance state. Completing a run or receiving feedback does not change work status.</p>${bodies.join("") || "<p>No readable response is recorded for this result.</p>"}</section>`;
|
|
44
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Effect, type FileSystem, type Path, type PlatformError } from "effect";
|
|
2
|
+
import type { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
|
3
|
+
import type { Changes } from "#files/changes.ts";
|
|
4
|
+
import { fileUrl } from "#files/url.ts";
|
|
5
|
+
import { respond } from "#http/respond.ts";
|
|
6
|
+
import type { MetadataDocument } from "#metadata/model.ts";
|
|
7
|
+
import { escapeHtml } from "#page/escape.ts";
|
|
8
|
+
import { metadataHtml } from "#page/metadata.ts";
|
|
9
|
+
import { navHtml } from "#page/nav.ts";
|
|
10
|
+
import { shell } from "#page/shell.ts";
|
|
11
|
+
import type { Highlighter } from "#render/highlighter.ts";
|
|
12
|
+
import { renderMarkdown } from "#render/markdown.ts";
|
|
13
|
+
import { revisionOf } from "#responses/records.ts";
|
|
14
|
+
import { resultCriteria } from "#results/criteria.ts";
|
|
15
|
+
import { resultFeedback } from "#results/feedback.ts";
|
|
16
|
+
import type { Snapshot } from "#search/snapshot.ts";
|
|
17
|
+
|
|
18
|
+
function reviewActions(result: MetadataDocument) {
|
|
19
|
+
const requests = result.parsed.fields.attention ?? [];
|
|
20
|
+
const actions = requests
|
|
21
|
+
.filter(
|
|
22
|
+
(attention) => attention.kind === "review" && attention.state === "open" && requests.filter((other) => other.id === attention.id).length === 1,
|
|
23
|
+
)
|
|
24
|
+
.map(
|
|
25
|
+
(attention) =>
|
|
26
|
+
`<li><a href="/_board/respond?item=${encodeURIComponent(result.parsed.fields.id ?? "")}&request=${encodeURIComponent(attention.id)}">Review ${escapeHtml(attention.id)}</a> — ${escapeHtml(attention.reason)}</li>`,
|
|
27
|
+
);
|
|
28
|
+
return `<section id="result-actions"><h2>Respond to this report</h2>${actions.length > 0 ? `<ul>${actions.join("")}</ul>` : "<p>No unique open review request is declared. The agent can add an explicit attention review and Markdown question packet with ordinary file editing. Work Board does not infer one from status.</p>"}</section>`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resultPage(index: Effect.Effect<Snapshot, unknown, FileSystem.FileSystem | Path.Path>, changes: Changes, home: string | undefined) {
|
|
32
|
+
return Effect.fn("WorkBoard.resultPage")(function* (
|
|
33
|
+
request: HttpServerRequest.HttpServerRequest,
|
|
34
|
+
): Effect.fn.Return<HttpServerResponse.HttpServerResponse, PlatformError.PlatformError, FileSystem.FileSystem | Path.Path | Highlighter> {
|
|
35
|
+
const item = new URL(request.url, "http://127.0.0.1").searchParams.get("item") ?? "";
|
|
36
|
+
const files = yield* changes.files;
|
|
37
|
+
function layout(body: string, status = 200) {
|
|
38
|
+
return respond(shell("Result review", navHtml(files, "", home), body, changes.realRoot, false, undefined, "result"), "text/html", status);
|
|
39
|
+
}
|
|
40
|
+
return yield* Effect.gen(function* () {
|
|
41
|
+
const data = yield* index;
|
|
42
|
+
const matches = data.model.ids.get(item);
|
|
43
|
+
const result = matches?.length === 1 ? matches[0] : undefined;
|
|
44
|
+
if (result?.parsed.fields.kind !== "result" || data.unavailable.length > 0) {
|
|
45
|
+
return layout(
|
|
46
|
+
'<h1>Result unavailable</h1><p role="status">Choose one uniquely identified, readable result. Missing or ambiguous work is not accepted.</p>',
|
|
47
|
+
409,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const report = yield* renderMarkdown(result.parsed.body, { file: result.file, safe: true });
|
|
51
|
+
const feedback = yield* resultFeedback(result, data);
|
|
52
|
+
const revision = revisionOf((result.parsed.raw ?? "") + result.parsed.body);
|
|
53
|
+
return layout(
|
|
54
|
+
`<section class="response-panel"><h1>Review returned result · ${escapeHtml(item)}</h1><p>Result status (source): <b>${escapeHtml(result.parsed.fields.status ?? "Not recorded")}</b>. This is a supplied claim, not an execution receipt or work acceptance.</p><p>Current report SHA-256: <code>${revision}</code> · <a href="${fileUrl(result.file)}">Read source</a></p>${metadataHtml(result.parsed, result.file, data.model)}${reviewActions(result)}${resultCriteria(result, data.model)}<section id="result-report"><h2>Report, evidence and limitations</h2>${report}</section>${feedback}</section>`,
|
|
55
|
+
);
|
|
56
|
+
}).pipe(Effect.catch((error) => Effect.succeed(layout(`<h1>Result review unavailable</h1><p>${escapeHtml(String(error))}</p>`, 409))));
|
|
57
|
+
});
|
|
58
|
+
}
|
package/src/rpc/contract.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { contract } from "@shivaedev/effect-contract/contract.ts";
|
|
|
4
4
|
import { LiveHint } from "@shivaedev/effect-contract/live.ts";
|
|
5
5
|
import { query } from "@shivaedev/effect-contract/operation.ts";
|
|
6
6
|
import { HistoryInput, HistoryOutput } from "#history/schema.ts";
|
|
7
|
+
import { handoffContract } from "#rpc/handoffContract.ts";
|
|
7
8
|
import { responseContract } from "#rpc/responseContract.ts";
|
|
8
9
|
import { index, navigation, watcher, workspace } from "./keys.ts";
|
|
9
10
|
import { pageKeys } from "./pageKeys.ts";
|
|
@@ -19,7 +20,9 @@ const PageUrl = Schema.String.check(
|
|
|
19
20
|
const path = new URL(value, "http://127.0.0.1").pathname;
|
|
20
21
|
return (
|
|
21
22
|
path === "/"
|
|
22
|
-
|| ["/_board/work", "/_board/overview", "/_board/changes", "/_board/start", "/_board/respond"].includes(
|
|
23
|
+
|| ["/_board/work", "/_board/overview", "/_board/changes", "/_board/start", "/_board/respond", "/_board/handoff", "/_board/result"].includes(
|
|
24
|
+
path,
|
|
25
|
+
)
|
|
23
26
|
|| /^\/_board\/item\/[^/]+\/$/u.exec(path) !== null
|
|
24
27
|
|| /\.md$/iu.exec(path) !== null
|
|
25
28
|
|| "Expected a local reading URL"
|
|
@@ -60,4 +63,4 @@ const ReadWatcher = query("watcher", {
|
|
|
60
63
|
});
|
|
61
64
|
export const workContract = contract("work-board", { queries: [ReadPage, ReadNavigation, ReadSearch, ReadHistory, ReadWatcher] });
|
|
62
65
|
export const Subscribe = Rpc.make("work-board.subscribe", { error: ReadFailed, stream: true, success: LiveHint });
|
|
63
|
-
export const workRpcs = workContract.add(Subscribe).merge(responseContract);
|
|
66
|
+
export const workRpcs = workContract.add(Subscribe).merge(responseContract).merge(handoffContract);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { contract } from "@shivaedev/effect-contract/contract.ts";
|
|
3
|
+
import { command, query } from "@shivaedev/effect-contract/operation.ts";
|
|
4
|
+
import { Handoff, HandoffInput, HandoffPreview, HandoffReading } from "#browser/handoffs/schema.ts";
|
|
5
|
+
import { ResponseFailed } from "#browser/responses/schema.ts";
|
|
6
|
+
import { index, workspace } from "#rpc/keys.ts";
|
|
7
|
+
|
|
8
|
+
const locate = query("handoffSource", {
|
|
9
|
+
payload: { item: Schema.String },
|
|
10
|
+
reads: () => [workspace.list, index.list],
|
|
11
|
+
rejections: { ResponseFailed },
|
|
12
|
+
success: HandoffPreview,
|
|
13
|
+
});
|
|
14
|
+
const read = query("handoffs", {
|
|
15
|
+
payload: { item: Schema.String },
|
|
16
|
+
reads: () => [workspace.list, index.list],
|
|
17
|
+
rejections: { ResponseFailed },
|
|
18
|
+
success: HandoffReading,
|
|
19
|
+
});
|
|
20
|
+
const prepare = command("prepareHandoff", {
|
|
21
|
+
invalidates: () => [workspace.list, index.list],
|
|
22
|
+
payload: HandoffInput,
|
|
23
|
+
rejections: { ResponseFailed },
|
|
24
|
+
success: Handoff,
|
|
25
|
+
});
|
|
26
|
+
export const handoffContract = contract("work-board", { commands: [prepare], queries: [locate, read] });
|
package/src/rpc/server.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Clock, Effect, Layer, Ref } from "effect";
|
|
|
2
2
|
import { HttpIncomingMessage, HttpServerResponse } from "effect/unstable/http";
|
|
3
3
|
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
|
|
4
4
|
import type { Changes } from "#files/changes.ts";
|
|
5
|
+
import type { handoffService } from "#handoffs/service.ts";
|
|
5
6
|
import { MAX_BYTES } from "#history/limits.ts";
|
|
6
7
|
import { historyResponse } from "#history/response.ts";
|
|
7
8
|
import { HEADERS } from "#http/respond.ts";
|
|
@@ -14,15 +15,18 @@ import { hints } from "./hints.ts";
|
|
|
14
15
|
|
|
15
16
|
export interface ServerOptions {
|
|
16
17
|
readonly changes: Changes;
|
|
18
|
+
readonly handoffs: Effect.Success<ReturnType<typeof handoffService>>;
|
|
17
19
|
readonly home: string | undefined;
|
|
18
20
|
readonly index: Effect.Effect<Snapshot, unknown>;
|
|
19
21
|
readonly page: (url: string) => Effect.Effect<HttpServerResponse.HttpServerResponse, unknown>;
|
|
20
22
|
readonly responses: Effect.Success<ReturnType<typeof responseService>>;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home, index, page, responses }: ServerOptions) {
|
|
25
|
+
export const server = Effect.fn("WorkBoard.server")(function* ({ changes, handoffs, home, index, page, responses }: ServerOptions) {
|
|
24
26
|
const handlers = workRpcs.toLayer({
|
|
25
27
|
"work-board.awaitResponse": responses.awaitResponse,
|
|
28
|
+
"work-board.handoffSource": ({ item }) => handoffs.locate(item),
|
|
29
|
+
"work-board.handoffs": ({ item }) => handoffs.read(item),
|
|
26
30
|
"work-board.history": ({ action, baseline }) =>
|
|
27
31
|
Effect.gen(function* () {
|
|
28
32
|
const now = yield* Clock.currentTimeMillis;
|
|
@@ -43,6 +47,7 @@ export const server = Effect.fn("WorkBoard.server")(function* ({ changes, home,
|
|
|
43
47
|
}),
|
|
44
48
|
Effect.catch((error) => Effect.fail(error instanceof ReadFailed ? error : new ReadFailed({ operation: "page", status: 500 }))),
|
|
45
49
|
),
|
|
50
|
+
"work-board.prepareHandoff": handoffs.prepare,
|
|
46
51
|
"work-board.question": ({ item, request }) => responses.locate(item, request),
|
|
47
52
|
"work-board.recordResponse": responses.record,
|
|
48
53
|
"work-board.registerQuestion": responses.register,
|
package/src/serve-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.11.0";
|
|
8
8
|
|
|
9
9
|
const serve = Effect.fn("WorkBoard.serve")(function* (input: {
|
|
10
10
|
readonly dir: string;
|
|
@@ -26,7 +26,7 @@ const workBoard = Command.make(
|
|
|
26
26
|
port: Flag.integer("port").pipe(Flag.withDefault(4747), Flag.withDescription(`The port to listen on at ${HOST}`)),
|
|
27
27
|
responses: Flag.boolean("responses").pipe(
|
|
28
28
|
Flag.withDefault(false),
|
|
29
|
-
Flag.withDescription("Explicitly enable local durable question and
|
|
29
|
+
Flag.withDescription("Explicitly enable local durable question, response and handoff writes (Linux)"),
|
|
30
30
|
),
|
|
31
31
|
},
|
|
32
32
|
serve,
|