@astrosheep/keiyaku 4.0.0 → 4.0.2
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/README.md +133 -8
- package/build/integrations/marketplace/plugins/keiyaku/.claude-plugin/plugin.json +2 -2
- package/build/integrations/marketplace/plugins/keiyaku/.codex-plugin/plugin.json +4 -4
- package/build/integrations/marketplace/plugins/keiyaku/opencode.js +2 -2
- package/build/integrations/marketplace/plugins/keiyaku/package.json +2 -2
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +13 -5
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +91 -32
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/SKILL.md +134 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-bind/agents/openai.yaml +4 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-task/SKILL.md +41 -6
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +262 -46
- package/build/src/akuma/abort.d.ts +2 -0
- package/build/src/akuma/abort.js +50 -0
- package/build/src/akuma/akuma.d.ts +50 -33
- package/build/src/akuma/akuma.js +175 -127
- package/build/src/akuma/archetype.d.ts +14 -5
- package/build/src/akuma/archetype.js +85 -39
- package/build/src/akuma/body.d.ts +3 -5
- package/build/src/akuma/body.js +490 -183
- package/build/src/akuma/coordinate.d.ts +9 -0
- package/build/src/akuma/coordinate.js +22 -0
- package/build/src/akuma/heart/facts.d.ts +88 -45
- package/build/src/akuma/heart/facts.js +19 -8
- package/build/src/akuma/heart/index.d.ts +64 -55
- package/build/src/akuma/heart/index.js +172 -252
- package/build/src/akuma/heart/rows.d.ts +38 -83
- package/build/src/akuma/heart/rows.js +117 -155
- package/build/src/akuma/heart/schema.d.ts +1 -1
- package/build/src/akuma/heart/schema.js +61 -22
- package/build/src/akuma/heart/soul.d.ts +8 -0
- package/build/src/akuma/heart/soul.js +136 -0
- package/build/src/akuma/heart/storage.d.ts +47 -0
- package/build/src/akuma/heart/storage.js +224 -0
- package/build/src/akuma/heart/tells.d.ts +12 -0
- package/build/src/akuma/heart/tells.js +79 -0
- package/build/src/akuma/heart/timeline.d.ts +15 -0
- package/build/src/akuma/heart/timeline.js +86 -0
- package/build/src/akuma/identity.d.ts +2 -2
- package/build/src/akuma/identity.js +9 -9
- package/build/src/akuma/index.d.ts +10 -3
- package/build/src/akuma/index.js +7 -1
- package/build/src/akuma/projection.d.ts +176 -0
- package/build/src/akuma/projection.js +256 -0
- package/build/src/akuma/provider-recipe.d.ts +24 -0
- package/build/src/akuma/provider-recipe.js +100 -0
- package/build/src/akuma/provider.d.ts +67 -13
- package/build/src/akuma/provider.js +172 -105
- package/build/src/akuma/providers/acp/core.d.ts +19 -0
- package/build/src/akuma/providers/acp/core.js +159 -0
- package/build/src/akuma/providers/acp/events.d.ts +20 -0
- package/build/src/akuma/providers/acp/events.js +73 -0
- package/build/src/akuma/providers/acp/index.d.ts +12 -0
- package/build/src/akuma/providers/acp/index.js +94 -0
- package/build/src/akuma/providers/{claude.d.ts → claude/events.d.ts} +9 -41
- package/build/src/akuma/providers/claude/events.js +238 -0
- package/build/src/akuma/providers/claude/index.d.ts +47 -0
- package/build/src/akuma/providers/claude/index.js +279 -0
- package/build/src/akuma/providers/claude/input.d.ts +13 -0
- package/build/src/akuma/providers/claude/input.js +109 -0
- package/build/src/akuma/providers/codex-app-server/events.d.ts +1 -1
- package/build/src/akuma/providers/codex-app-server/events.js +15 -12
- package/build/src/akuma/providers/codex-app-server/index.d.ts +15 -13
- package/build/src/akuma/providers/codex-app-server/index.js +77 -33
- package/build/src/akuma/providers/grok-build/index.d.ts +4 -0
- package/build/src/akuma/providers/grok-build/index.js +70 -0
- package/build/src/akuma/providers/index.d.ts +5 -3
- package/build/src/akuma/providers/index.js +35 -56
- package/build/src/akuma/providers/opencode-sdk/events.d.ts +54 -0
- package/build/src/akuma/providers/opencode-sdk/events.js +235 -0
- package/build/src/akuma/providers/opencode-sdk/index.d.ts +33 -0
- package/build/src/akuma/providers/opencode-sdk/index.js +308 -0
- package/build/src/akuma/providers/opencode-sdk/session.d.ts +26 -0
- package/build/src/akuma/providers/opencode-sdk/session.js +63 -0
- package/build/src/akuma/providers/pi/events.d.ts +33 -0
- package/build/src/akuma/providers/pi/events.js +166 -0
- package/build/src/akuma/providers/pi/index.d.ts +11 -0
- package/build/src/akuma/providers/pi/index.js +191 -0
- package/build/src/akuma/publication.d.ts +3 -3
- package/build/src/akuma/publication.js +28 -40
- package/build/src/akuma/requests.d.ts +9 -6
- package/build/src/akuma/requests.js +95 -65
- package/build/src/alias/index.d.ts +4 -3
- package/build/src/alias/index.js +12 -13
- package/build/src/body/decode.js +7 -1
- package/build/src/body/region.d.ts +2 -0
- package/build/src/body/region.js +77 -7
- package/build/src/body/render.js +2 -1
- package/build/src/body/shape.js +7 -1
- package/build/src/body/verification.js +19 -4
- package/build/src/cli/accepted.d.ts +18 -12
- package/build/src/cli/accepted.js +73 -34
- package/build/src/cli/actor.js +3 -3
- package/build/src/cli/commands/akuma-invoke.d.ts +44 -19
- package/build/src/cli/commands/akuma-invoke.js +101 -52
- package/build/src/cli/commands/akuma.d.ts +27 -9
- package/build/src/cli/commands/akuma.js +151 -80
- package/build/src/cli/commands/contract.d.ts +155 -0
- package/build/src/cli/commands/contract.js +20 -0
- package/build/src/cli/commands/install.js +2 -0
- package/build/src/cli/commands/task-invoke.d.ts +19 -4
- package/build/src/cli/commands/task-invoke.js +65 -17
- package/build/src/cli/commands/task-query.d.ts +5 -0
- package/build/src/cli/commands/task-query.js +247 -0
- package/build/src/cli/commands/task.d.ts +3 -1
- package/build/src/cli/commands/task.js +52 -14
- package/build/src/cli/coordinates.d.ts +18 -0
- package/build/src/cli/coordinates.js +120 -0
- package/build/src/cli/draft.d.ts +8 -0
- package/build/src/cli/draft.js +96 -0
- package/build/src/cli/invoke.d.ts +9 -3
- package/build/src/cli/invoke.js +287 -108
- package/build/src/cli/main.js +54 -7
- package/build/src/cli/parse.d.ts +21 -117
- package/build/src/cli/parse.js +79 -84
- package/build/src/cli/render/akuma-tool-command.d.ts +6 -0
- package/build/src/cli/render/akuma-tool-command.js +144 -0
- package/build/src/cli/render/akuma-tool.d.ts +4 -2
- package/build/src/cli/render/akuma-tool.js +35 -5
- package/build/src/cli/render/akuma.d.ts +3 -1
- package/build/src/cli/render/akuma.js +247 -212
- package/build/src/cli/render/audit.d.ts +3 -0
- package/build/src/cli/render/audit.js +104 -0
- package/build/src/cli/render/catalog.d.ts +2 -0
- package/build/src/cli/render/catalog.js +16 -0
- package/build/src/cli/render/contract.d.ts +3 -3
- package/build/src/cli/render/contract.js +200 -54
- package/build/src/cli/render/kanshi.js +272 -129
- package/build/src/cli/render/receipt.d.ts +19 -0
- package/build/src/cli/render/receipt.js +106 -0
- package/build/src/cli/render/refusal.d.ts +16 -2
- package/build/src/cli/render/refusal.js +88 -3
- package/build/src/cli/render/region.d.ts +2 -0
- package/build/src/cli/render/region.js +17 -0
- package/build/src/cli/render/task.d.ts +2 -1
- package/build/src/cli/render/task.js +223 -60
- package/build/src/cli/render/terminal.d.ts +20 -0
- package/build/src/cli/render/terminal.js +121 -0
- package/build/src/cli/render/text.js +11 -3
- package/build/src/cli/result.d.ts +126 -19
- package/build/src/cli/usage.d.ts +1 -0
- package/build/src/cli/usage.js +3 -0
- package/build/src/contract-worktree.d.ts +44 -0
- package/build/src/contract-worktree.js +251 -0
- package/build/src/coordination/durable-file.d.ts +5 -1
- package/build/src/coordination/durable-file.js +57 -10
- package/build/src/coordination/sqlite-transaction-lock.d.ts +4 -0
- package/build/src/coordination/sqlite-transaction-lock.js +22 -5
- package/build/src/core/facts/codec.js +23 -4
- package/build/src/core/facts/eligibility.d.ts +1 -10
- package/build/src/core/facts/eligibility.js +0 -49
- package/build/src/core/facts/fold.js +1 -6
- package/build/src/core/facts/gate.d.ts +1 -0
- package/build/src/core/facts/gate.js +1 -0
- package/build/src/core/facts/observation.d.ts +3 -0
- package/build/src/core/facts/observation.js +12 -0
- package/build/src/core/facts/types.d.ts +10 -3
- package/build/src/core/subject.js +2 -2
- package/build/src/core/verbs/amend.d.ts +1 -1
- package/build/src/core/verbs/amend.js +3 -18
- package/build/src/core/verbs/bind.js +2 -16
- package/build/src/core/verbs/deliver.d.ts +1 -1
- package/build/src/core/verbs/deliver.js +14 -4
- package/build/src/core/verbs/placement.d.ts +1 -1
- package/build/src/core/verbs/placement.js +7 -9
- package/build/src/dispatch/index.d.ts +5 -3
- package/build/src/dispatch/index.js +30 -21
- package/build/src/duration.d.ts +10 -0
- package/build/src/duration.js +22 -0
- package/build/src/git/admission.d.ts +2 -2
- package/build/src/git/admission.js +33 -23
- package/build/src/git/hooks.d.ts +15 -0
- package/build/src/git/hooks.js +70 -18
- package/build/src/git/identity.d.ts +5 -1
- package/build/src/git/identity.js +23 -4
- package/build/src/git/integration.d.ts +48 -0
- package/build/src/git/integration.js +250 -0
- package/build/src/git/observe.d.ts +29 -15
- package/build/src/git/observe.js +185 -76
- package/build/src/git/read-observation.d.ts +41 -0
- package/build/src/git/read-observation.js +361 -0
- package/build/src/git/reconcile.d.ts +17 -5
- package/build/src/git/reconcile.js +305 -109
- package/build/src/git/repository.d.ts +41 -18
- package/build/src/git/repository.js +220 -114
- package/build/src/git/scratch.d.ts +17 -0
- package/build/src/git/scratch.js +73 -0
- package/build/src/git/target-placement.d.ts +92 -0
- package/build/src/git/target-placement.js +375 -0
- package/build/src/git/tender.d.ts +49 -0
- package/build/src/git/tender.js +63 -0
- package/build/src/git/terminal-seal.d.ts +24 -0
- package/build/src/git/terminal-seal.js +89 -0
- package/build/src/git/tree.d.ts +2 -2
- package/build/src/git/tree.js +4 -0
- package/build/src/git/workspace.d.ts +53 -0
- package/build/src/git/workspace.js +139 -0
- package/build/src/identity/selector.d.ts +6 -0
- package/build/src/identity/selector.js +19 -0
- package/build/src/index.d.ts +4 -2
- package/build/src/index.js +2 -1
- package/build/src/kanshi/index.d.ts +2 -2
- package/build/src/kanshi/index.js +1 -1
- package/build/src/kanshi/read.d.ts +7 -3
- package/build/src/kanshi/read.js +245 -45
- package/build/src/kanshi/report.d.ts +81 -6
- package/build/src/kanshi/select.d.ts +5 -0
- package/build/src/kanshi/select.js +33 -1
- package/build/src/library/address.d.ts +57 -0
- package/build/src/library/address.js +165 -0
- package/build/src/library/akuma-creation.d.ts +16 -7
- package/build/src/library/akuma-creation.js +96 -22
- package/build/src/library/audit.d.ts +18 -0
- package/build/src/library/audit.js +44 -0
- package/build/src/library/bind.d.ts +21 -0
- package/build/src/library/bind.js +56 -0
- package/build/src/library/catalog.d.ts +58 -0
- package/build/src/library/catalog.js +69 -0
- package/build/src/library/configuration.d.ts +6 -15
- package/build/src/library/configuration.js +14 -17
- package/build/src/library/contract.d.ts +23 -49
- package/build/src/library/contract.js +171 -185
- package/build/src/library/delivery.d.ts +24 -0
- package/build/src/library/delivery.js +21 -0
- package/build/src/library/fleet.d.ts +65 -0
- package/build/src/library/fleet.js +153 -0
- package/build/src/library/input.d.ts +3 -0
- package/build/src/library/input.js +19 -0
- package/build/src/library/keiyaku.d.ts +18 -3
- package/build/src/library/keiyaku.js +12 -1
- package/build/src/library/mutation.d.ts +39 -0
- package/build/src/library/mutation.js +37 -0
- package/build/src/library/reconcile.d.ts +29 -0
- package/build/src/library/reconcile.js +179 -0
- package/build/src/library/refusal.d.ts +21 -0
- package/build/src/library/refusal.js +29 -0
- package/build/src/library/region.d.ts +14 -1
- package/build/src/library/region.js +18 -4
- package/build/src/library/repo.d.ts +4 -13
- package/build/src/library/repo.js +14 -24
- package/build/src/protocol/attempt.d.ts +16 -3
- package/build/src/protocol/attempt.js +41 -10
- package/build/src/protocol/bind.d.ts +9 -2
- package/build/src/protocol/bind.js +48 -40
- package/build/src/protocol/intent.d.ts +57 -20
- package/build/src/protocol/intent.js +55 -79
- package/build/src/protocol/operations.d.ts +152 -43
- package/build/src/protocol/operations.js +482 -166
- package/build/src/protocol/outcome.d.ts +14 -3
- package/build/src/protocol/outcome.js +11 -3
- package/build/src/protocol/placement.d.ts +41 -0
- package/build/src/protocol/placement.js +147 -0
- package/build/src/protocol/read/documents.d.ts +2 -2
- package/build/src/protocol/read/documents.js +4 -4
- package/build/src/protocol/read/status.d.ts +18 -5
- package/build/src/protocol/read/status.js +73 -13
- package/build/src/protocol/run.d.ts +49 -7
- package/build/src/protocol/run.js +53 -25
- package/build/src/runtime/proc/line-rpc.d.ts +4 -8
- package/build/src/runtime/proc/line-rpc.js +22 -28
- package/build/src/runtime/proc/run.d.ts +23 -21
- package/build/src/runtime/proc/run.js +123 -89
- package/build/src/runtime/proc/stdio.d.ts +18 -0
- package/build/src/runtime/proc/stdio.js +58 -0
- package/build/src/settings.d.ts +7 -1
- package/build/src/settings.js +32 -17
- package/build/src/settlement/fence.d.ts +5 -0
- package/build/src/settlement/fence.js +14 -0
- package/build/src/settlement/holder.d.ts +35 -4
- package/build/src/settlement/holder.js +127 -28
- package/build/src/settlement/settle.d.ts +9 -1
- package/build/src/settlement/settle.js +74 -37
- package/build/src/task/board.d.ts +23 -5
- package/build/src/task/board.js +17 -10
- package/build/src/task/compose.d.ts +2 -2
- package/build/src/task/compose.js +18 -10
- package/build/src/task/context.d.ts +3 -3
- package/build/src/task/context.js +15 -20
- package/build/src/task/document.d.ts +2 -1
- package/build/src/task/document.js +15 -5
- package/build/src/task/index.d.ts +27 -15
- package/build/src/task/index.js +57 -35
- package/build/src/task/operations.d.ts +38 -13
- package/build/src/task/operations.js +80 -46
- package/build/src/task/query.d.ts +67 -0
- package/build/src/task/query.js +279 -0
- package/build/src/task/store.d.ts +5 -7
- package/build/src/task/store.js +43 -28
- package/build/src/verification/declaration.d.ts +1 -0
- package/build/src/verification/execution.d.ts +51 -0
- package/build/src/verification/execution.js +108 -0
- package/build/src/workspace-place.d.ts +41 -0
- package/build/src/workspace-place.js +386 -0
- package/build/src/world.d.ts +24 -0
- package/build/src/world.js +136 -0
- package/package.json +8 -4
- package/build/src/akuma/activity.d.ts +0 -66
- package/build/src/akuma/activity.js +0 -139
- package/build/src/akuma/providers/claude.js +0 -305
- package/build/src/context-root.d.ts +0 -4
- package/build/src/context-root.js +0 -15
- package/build/src/git/delivery.d.ts +0 -20
- package/build/src/git/delivery.js +0 -109
- package/build/src/git/verification.d.ts +0 -14
- package/build/src/git/verification.js +0 -56
- package/build/src/protocol/read/audit.d.ts +0 -25
- package/build/src/protocol/read/audit.js +0 -39
- package/build/src/verification/producer.d.ts +0 -23
- package/build/src/verification/producer.js +0 -54
|
@@ -1,19 +1,21 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import { documentDiff } from "../markdown/diff.js";
|
|
2
3
|
import { installNamespaceContext, readNamespaceContext } from "./context.js";
|
|
3
|
-
import { relationProblem, projectBlocked,
|
|
4
|
+
import { relationProblem, projectBlocked, projectRows, projectStatusRows } from "./board.js";
|
|
4
5
|
import { parseTaskCreationDocument, serializeTaskDocument } from "./document.js";
|
|
5
6
|
import { allocateLocalId, deriveLocalStem, formatTaskId, parseTaskId, sameNamespace } from "./identity.js";
|
|
6
7
|
import { authorityPath, readBoard, replaceAuthority, withTaskLocks, } from "./store.js";
|
|
8
|
+
import { DEFAULT_TASK_LIMIT, projectPage, projectQuery, queryUnderTargets, underExpression } from "./query.js";
|
|
7
9
|
export function taskView(document) {
|
|
8
10
|
return { ...document, namespace: parseTaskId(document.id).namespace };
|
|
9
11
|
}
|
|
10
12
|
function refused(refusal) { return { kind: "refused", refusal }; }
|
|
11
13
|
function retry(reason) { return { kind: "retry", reason }; }
|
|
12
|
-
function context(world, explicit) {
|
|
14
|
+
async function context(world, explicit) {
|
|
13
15
|
if (explicit !== undefined)
|
|
14
16
|
return explicit;
|
|
15
|
-
const current = readNamespaceContext(world
|
|
16
|
-
return current === "malformed" ? { kind: "invalid-namespace-context", path:
|
|
17
|
+
const current = await readNamespaceContext(world);
|
|
18
|
+
return current === "malformed" ? { kind: "invalid-namespace-context", path: resolve(world, ".keiyaku", "namespace", "current") }
|
|
17
19
|
: current === "absent" ? [] : current;
|
|
18
20
|
}
|
|
19
21
|
function occupied(board, namespace) {
|
|
@@ -27,40 +29,44 @@ function advancedTimestamp(previous) {
|
|
|
27
29
|
const current = currentTimestamp();
|
|
28
30
|
return current > previous ? current : new Date(Date.parse(previous) + 1).toISOString();
|
|
29
31
|
}
|
|
30
|
-
function addDocument(base, namespace, board, at) {
|
|
32
|
+
function addDocument(base, namespace, board, at, actor) {
|
|
31
33
|
const localId = allocateLocalId(deriveLocalStem(base.title), occupied(board, namespace));
|
|
32
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
...base, id: formatTaskId({ namespace, localId }),
|
|
36
|
+
...(actor === undefined ? {} : { createdBy: actor }),
|
|
37
|
+
createdAt: at, updatedAt: at,
|
|
38
|
+
};
|
|
33
39
|
}
|
|
34
40
|
function boardWith(board, document) {
|
|
35
41
|
const tasks = new Map(board.tasks);
|
|
36
42
|
tasks.set(document.id, document);
|
|
37
43
|
return { tasks };
|
|
38
44
|
}
|
|
39
|
-
async function create(world, base, namespace, signal) {
|
|
45
|
+
async function create(world, base, namespace, signal, actor) {
|
|
40
46
|
const result = await withTaskLocks({ world, allocation: true, ids: [], ...(signal === undefined ? {} : { signal }) }, async () => {
|
|
41
|
-
const snapshot = readBoard(world), next = addDocument(base, namespace, snapshot.board, currentTimestamp());
|
|
47
|
+
const snapshot = await readBoard(world), next = addDocument(base, namespace, snapshot.board, currentTimestamp(), actor);
|
|
42
48
|
const problem = relationProblem(boardWith(snapshot.board, next), null, next);
|
|
43
49
|
if (problem !== null)
|
|
44
50
|
return refused({ kind: "invalid-graph", diagnostic: problem });
|
|
45
|
-
const replaced = replaceAuthority({ path: authorityPath(world, next.id), expected: null, next: serializeTaskDocument(next) });
|
|
51
|
+
const replaced = await replaceAuthority({ path: authorityPath(world, next.id), expected: null, next: serializeTaskDocument(next) });
|
|
46
52
|
return replaced === "replaced" ? { kind: "accepted", value: taskView(next) } : retry("concurrent-modification");
|
|
47
53
|
});
|
|
48
54
|
return result === "busy" ? retry("busy") : result;
|
|
49
55
|
}
|
|
50
56
|
export async function addTask(world, input) {
|
|
51
|
-
const namespace = context(world, input.namespace);
|
|
57
|
+
const namespace = await context(world, input.namespace);
|
|
52
58
|
if (!Array.isArray(namespace))
|
|
53
59
|
return refused(namespace);
|
|
54
60
|
return create(world, {
|
|
55
61
|
title: input.title, body: input.body ?? "", note: input.note ?? "", state: input.state ?? "open", priority: input.priority ?? 2, needs: input.needs ?? [], parent: input.parent ?? null,
|
|
56
62
|
supersedes: input.supersedes ?? [], relates: input.relates ?? [],
|
|
57
|
-
}, namespace, input.signal);
|
|
63
|
+
}, namespace, input.signal, input.actor);
|
|
58
64
|
}
|
|
59
65
|
export async function addTaskDocument(world, input) {
|
|
60
|
-
const namespace = context(world, input.namespace);
|
|
66
|
+
const namespace = await context(world, input.namespace);
|
|
61
67
|
if (!Array.isArray(namespace))
|
|
62
68
|
return refused(namespace);
|
|
63
|
-
return create(world, parseTaskCreationDocument(input.markdown), namespace, input.signal);
|
|
69
|
+
return create(world, parseTaskCreationDocument(input.markdown), namespace, input.signal, input.actor);
|
|
64
70
|
}
|
|
65
71
|
function listChange(current, replacement, additions, removals) {
|
|
66
72
|
const next = [...(replacement ?? current)];
|
|
@@ -93,7 +99,7 @@ function updateDocument(board, current, input) {
|
|
|
93
99
|
}
|
|
94
100
|
export async function updateTask(world, id, input) {
|
|
95
101
|
const result = await withTaskLocks({ world, allocation: false, ids: [id], ...(input.signal === undefined ? {} : { signal: input.signal }) }, async () => {
|
|
96
|
-
const snapshot = readBoard(world), current = snapshot.board.tasks.get(id);
|
|
102
|
+
const snapshot = await readBoard(world), current = snapshot.board.tasks.get(id);
|
|
97
103
|
if (current === undefined)
|
|
98
104
|
return { kind: "refused", refusal: { kind: "task-missing", taskId: id } };
|
|
99
105
|
const candidate = updateDocument(snapshot.board, current, input);
|
|
@@ -106,7 +112,7 @@ export async function updateTask(world, id, input) {
|
|
|
106
112
|
const changed = !Buffer.from(serializeTaskDocument(candidate)).equals(Buffer.from(predecessor));
|
|
107
113
|
const next = changed ? { ...candidate, updatedAt: advancedTimestamp(current.updatedAt) } : candidate;
|
|
108
114
|
const afterBytes = serializeTaskDocument(next), after = Buffer.from(afterBytes).toString("utf8");
|
|
109
|
-
if (before !== after && replaceAuthority({ path: authorityPath(world, id), expected: predecessor, next: afterBytes }) !== "replaced")
|
|
115
|
+
if (before !== after && await replaceAuthority({ path: authorityPath(world, id), expected: predecessor, next: afterBytes }) !== "replaced")
|
|
110
116
|
return { kind: "retry", reason: "concurrent-modification" };
|
|
111
117
|
const label = `${id}.md`;
|
|
112
118
|
return { kind: "accepted", value: { task: taskView(next), documentDiff: documentDiff(label, label, before, after) } };
|
|
@@ -120,7 +126,7 @@ const TRANSITIONS = {
|
|
|
120
126
|
};
|
|
121
127
|
export async function lifecycleTask(world, id, verb, signal, note) {
|
|
122
128
|
const result = await withTaskLocks({ world, allocation: false, ids: [id], ...(signal === undefined ? {} : { signal }) }, async () => {
|
|
123
|
-
const snapshot = readBoard(world), current = snapshot.board.tasks.get(id);
|
|
129
|
+
const snapshot = await readBoard(world), current = snapshot.board.tasks.get(id);
|
|
124
130
|
if (current === undefined)
|
|
125
131
|
return refused({ kind: "task-missing", taskId: id });
|
|
126
132
|
const state = TRANSITIONS[verb][current.state];
|
|
@@ -128,7 +134,7 @@ export async function lifecycleTask(world, id, verb, signal, note) {
|
|
|
128
134
|
return refused({ kind: "invalid-lifecycle-transition", taskId: id, state: current.state, verb });
|
|
129
135
|
const next = { ...current, state, ...(note === undefined ? {} : { note }), updatedAt: advancedTimestamp(current.updatedAt) };
|
|
130
136
|
const bytes = serializeTaskDocument(next);
|
|
131
|
-
return replaceAuthority({ path: authorityPath(world, id), expected: snapshot.bytes.get(id), next: bytes }) === "replaced"
|
|
137
|
+
return await replaceAuthority({ path: authorityPath(world, id), expected: snapshot.bytes.get(id), next: bytes }) === "replaced"
|
|
132
138
|
? { kind: "accepted", value: taskView(next) } : retry("concurrent-modification");
|
|
133
139
|
});
|
|
134
140
|
return result === "busy" ? retry("busy") : result;
|
|
@@ -141,54 +147,82 @@ export async function batchTasks(world, verb, ids, signal, note) {
|
|
|
141
147
|
}
|
|
142
148
|
return { items };
|
|
143
149
|
}
|
|
144
|
-
export async function settleTask(world, id
|
|
150
|
+
export async function settleTask(world, id) {
|
|
145
151
|
const result = await withTaskLocks({ world, allocation: false, ids: [id] }, async () => {
|
|
146
|
-
const snapshot = readBoard(world), current = snapshot.board.tasks.get(id);
|
|
152
|
+
const snapshot = await readBoard(world), current = snapshot.board.tasks.get(id);
|
|
147
153
|
if (current === undefined)
|
|
148
154
|
return { kind: "refused", refusal: { kind: "task-missing", taskId: id } };
|
|
149
|
-
if (
|
|
150
|
-
if (current.state === "done")
|
|
151
|
-
return { kind: "unchanged" };
|
|
152
|
-
if (current.state === "drop") {
|
|
153
|
-
return { kind: "refused", refusal: { kind: "invalid-lifecycle-transition", taskId: id, state: current.state, verb: "done" } };
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
else if (current.state !== "done")
|
|
155
|
+
if (current.state === "done")
|
|
157
156
|
return { kind: "unchanged" };
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
157
|
+
if (current.state === "drop") {
|
|
158
|
+
return { kind: "refused", refusal: { kind: "invalid-lifecycle-transition", taskId: id, state: current.state, verb: "done" } };
|
|
159
|
+
}
|
|
160
|
+
const next = { ...current, state: "done", updatedAt: advancedTimestamp(current.updatedAt) };
|
|
161
|
+
const replaced = await replaceAuthority({ path: authorityPath(world, id), expected: snapshot.bytes.get(id), next: serializeTaskDocument(next) });
|
|
161
162
|
return replaced === "replaced"
|
|
162
|
-
? { kind: "changed", task: taskView(next), action:
|
|
163
|
+
? { kind: "changed", task: taskView(next), action: "done" }
|
|
163
164
|
: { kind: "retry", reason: "concurrent-modification" };
|
|
164
165
|
});
|
|
165
166
|
return result === "busy" ? { kind: "retry", reason: "busy" } : result;
|
|
166
167
|
}
|
|
167
|
-
function readScope(world, scope) {
|
|
168
|
+
async function readScope(world, scope) {
|
|
168
169
|
if (scope === "world")
|
|
169
170
|
return null;
|
|
170
|
-
return context(world);
|
|
171
|
+
return await context(world);
|
|
171
172
|
}
|
|
172
|
-
export function listTasks(world, selection, scope) {
|
|
173
|
-
const selected = readScope(world, scope);
|
|
173
|
+
export async function listTasks(world, selection, scope, limit = DEFAULT_TASK_LIMIT) {
|
|
174
|
+
const selected = await readScope(world, scope);
|
|
174
175
|
if (selected !== null && !Array.isArray(selected))
|
|
175
176
|
return { kind: "refused", refusal: selected };
|
|
176
|
-
return { kind: "accepted", value: projectRows(readBoard(world).board, selected, selection) };
|
|
177
|
+
return { kind: "accepted", value: projectPage(projectRows((await readBoard(world)).board, selected, selection), limit) };
|
|
177
178
|
}
|
|
178
|
-
export function readyTasks(world, scope) {
|
|
179
|
-
const selected = readScope(world, scope);
|
|
179
|
+
export async function readyTasks(world, scope, parent, limit = DEFAULT_TASK_LIMIT) {
|
|
180
|
+
const selected = await readScope(world, scope);
|
|
180
181
|
if (selected !== null && !Array.isArray(selected))
|
|
181
182
|
return { kind: "refused", refusal: selected };
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
183
|
+
const board = (await readBoard(world)).board;
|
|
184
|
+
if (parent !== undefined && !board.tasks.has(parent))
|
|
185
|
+
return { kind: "refused", refusal: { kind: "task-missing", taskId: parent } };
|
|
186
|
+
const ready = { kind: "predicate", predicate: { field: "ready", operator: "=", value: true } };
|
|
187
|
+
const expression = parent === undefined ? ready : { kind: "and", terms: [ready, underExpression(parent)] };
|
|
188
|
+
const selectedRows = projectQuery(board, selected, expression, "priority", Math.max(1, board.tasks.size)).rows;
|
|
189
|
+
const rows = selectedRows.map(({ parent: _parent, needs: _needs, blocks: _blocks, createdAt: _createdAt, updatedAt: _updatedAt, ...row }) => row);
|
|
190
|
+
return { kind: "accepted", value: projectPage(rows, limit) };
|
|
191
|
+
}
|
|
192
|
+
export async function blockedTasks(world, scope, parent, limit = DEFAULT_TASK_LIMIT) {
|
|
193
|
+
const selected = await readScope(world, scope);
|
|
186
194
|
if (selected !== null && !Array.isArray(selected))
|
|
187
195
|
return { kind: "refused", refusal: selected };
|
|
188
|
-
|
|
196
|
+
const board = (await readBoard(world)).board;
|
|
197
|
+
if (parent !== undefined && !board.tasks.has(parent))
|
|
198
|
+
return { kind: "refused", refusal: { kind: "task-missing", taskId: parent } };
|
|
199
|
+
const blocked = { kind: "predicate", predicate: { field: "blocked", operator: "=", value: true } };
|
|
200
|
+
const expression = parent === undefined ? blocked : { kind: "and", terms: [blocked, underExpression(parent)] };
|
|
201
|
+
const selectedRows = projectQuery(board, selected, expression, "priority", Math.max(1, board.tasks.size)).rows;
|
|
202
|
+
const ids = new Set(selectedRows.map((row) => row.id));
|
|
203
|
+
return { kind: "accepted", value: projectPage(projectBlocked(board, selected).filter((row) => ids.has(row.id)), limit) };
|
|
204
|
+
}
|
|
205
|
+
export async function queryTasks(world, expression, scope, sort = "priority", limit = DEFAULT_TASK_LIMIT) {
|
|
206
|
+
const selected = await readScope(world, scope);
|
|
207
|
+
if (selected !== null && !Array.isArray(selected))
|
|
208
|
+
return { kind: "refused", refusal: selected };
|
|
209
|
+
const board = (await readBoard(world)).board;
|
|
210
|
+
for (const target of queryUnderTargets(expression))
|
|
211
|
+
if (!board.tasks.has(target)) {
|
|
212
|
+
return { kind: "refused", refusal: { kind: "task-missing", taskId: target } };
|
|
213
|
+
}
|
|
214
|
+
return { kind: "accepted", value: projectQuery(board, selected, expression, sort, limit) };
|
|
215
|
+
}
|
|
216
|
+
/** Internal composite observation from one complete Task board read. */
|
|
217
|
+
export async function observeTaskStatusRows(world) {
|
|
218
|
+
return projectStatusRows((await readBoard(world)).board, null);
|
|
219
|
+
}
|
|
220
|
+
/** Internal identity catalog from one complete Task board read. */
|
|
221
|
+
export async function observeTaskCatalogRows(world) {
|
|
222
|
+
return projectRows((await readBoard(world)).board, null, "all");
|
|
189
223
|
}
|
|
190
|
-
export function setCurrentNamespace(world, namespace) { installNamespaceContext(world
|
|
191
|
-
export function currentNamespace(world) {
|
|
192
|
-
const selected = context(world);
|
|
224
|
+
export async function setCurrentNamespace(world, namespace) { await installNamespaceContext(world, namespace); }
|
|
225
|
+
export async function currentNamespace(world) {
|
|
226
|
+
const selected = await context(world);
|
|
193
227
|
return selected;
|
|
194
228
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { TaskPriority, TaskState } from "./document.js";
|
|
2
|
+
import { type TaskId } from "./identity.js";
|
|
3
|
+
import type { BlockedTaskRow, TaskBoard, TaskRef, TaskRow } from "./board.js";
|
|
4
|
+
export declare const DEFAULT_TASK_LIMIT = 100;
|
|
5
|
+
export declare const MAX_TASK_LIMIT = 1000;
|
|
6
|
+
export type TaskQueryComparison = "=" | "!=" | "<" | "<=" | ">" | ">=" | "~";
|
|
7
|
+
export type TaskQueryPredicate = Readonly<{
|
|
8
|
+
field: "state";
|
|
9
|
+
operator: "=" | "!=";
|
|
10
|
+
value: TaskState;
|
|
11
|
+
}> | Readonly<{
|
|
12
|
+
field: "priority";
|
|
13
|
+
operator: "=" | "!=" | "<" | "<=" | ">" | ">=";
|
|
14
|
+
value: TaskPriority;
|
|
15
|
+
}> | Readonly<{
|
|
16
|
+
field: "title" | "id";
|
|
17
|
+
operator: "=" | "!=" | "~";
|
|
18
|
+
value: string;
|
|
19
|
+
}> | Readonly<{
|
|
20
|
+
field: "parent";
|
|
21
|
+
operator: "=" | "!=";
|
|
22
|
+
value: TaskId | null;
|
|
23
|
+
}> | Readonly<{
|
|
24
|
+
field: "under" | "needs" | "blocks";
|
|
25
|
+
operator: "=" | "!=";
|
|
26
|
+
value: TaskId;
|
|
27
|
+
}> | Readonly<{
|
|
28
|
+
field: "ready" | "blocked";
|
|
29
|
+
operator: "=" | "!=";
|
|
30
|
+
value: boolean;
|
|
31
|
+
}> | Readonly<{
|
|
32
|
+
field: "created" | "updated";
|
|
33
|
+
operator: "=" | "!=" | "<" | "<=" | ">" | ">=";
|
|
34
|
+
value: string;
|
|
35
|
+
}>;
|
|
36
|
+
export type TaskQueryExpression = Readonly<{
|
|
37
|
+
kind: "and" | "or";
|
|
38
|
+
terms: readonly TaskQueryExpression[];
|
|
39
|
+
}> | Readonly<{
|
|
40
|
+
kind: "not";
|
|
41
|
+
term: TaskQueryExpression;
|
|
42
|
+
}> | Readonly<{
|
|
43
|
+
kind: "predicate";
|
|
44
|
+
predicate: TaskQueryPredicate;
|
|
45
|
+
}>;
|
|
46
|
+
export type TaskQuerySort = "priority" | "created" | "updated" | "id";
|
|
47
|
+
export type TaskQueryRow = TaskRow & Readonly<{
|
|
48
|
+
parent: TaskId | null;
|
|
49
|
+
needs: readonly TaskRef[];
|
|
50
|
+
blocks: readonly TaskRef[];
|
|
51
|
+
createdAt: string;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
}>;
|
|
54
|
+
export type TaskPage<Row> = Readonly<{
|
|
55
|
+
rows: readonly Row[];
|
|
56
|
+
total: number;
|
|
57
|
+
returned: number;
|
|
58
|
+
truncated: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
export declare function normalizeTaskQuery(value: unknown): TaskQueryExpression;
|
|
61
|
+
export declare function projectQuery(board: TaskBoard, scope: readonly string[] | null, expression: TaskQueryExpression, sort?: TaskQuerySort, limit?: number): TaskPage<TaskQueryRow>;
|
|
62
|
+
export declare function projectPage<Row>(rows: readonly Row[], limit?: number): TaskPage<Row>;
|
|
63
|
+
export declare function underExpression(parent: TaskId): TaskQueryExpression;
|
|
64
|
+
export declare function queryUnderTargets(expression: TaskQueryExpression): readonly TaskId[];
|
|
65
|
+
export declare function isValidTaskLimit(value: number): boolean;
|
|
66
|
+
export type TaskQueryPage = TaskPage<TaskQueryRow>;
|
|
67
|
+
export type TaskBlockedPage = TaskPage<BlockedTaskRow>;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { formatTaskId, parseTaskId, sameNamespace } from "./identity.js";
|
|
2
|
+
import { taskDisposition } from "./board.js";
|
|
3
|
+
export const DEFAULT_TASK_LIMIT = 100;
|
|
4
|
+
export const MAX_TASK_LIMIT = 1_000;
|
|
5
|
+
const states = ["open", "in_progress", "on_hold", "done", "drop"];
|
|
6
|
+
const comparisons = ["=", "!=", "<", "<=", ">", ">=", "~"];
|
|
7
|
+
function fail(message) { throw new TypeError(message); }
|
|
8
|
+
function object(value, label) {
|
|
9
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
10
|
+
fail(`${label} must be an object`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function closed(value, fields, label) {
|
|
14
|
+
for (const key of Object.keys(value))
|
|
15
|
+
if (!fields.includes(key))
|
|
16
|
+
fail(`${label} has unknown field: ${key}`);
|
|
17
|
+
}
|
|
18
|
+
function taskId(value, label) {
|
|
19
|
+
if (typeof value !== "string")
|
|
20
|
+
fail(`${label} must be a TaskId`);
|
|
21
|
+
try {
|
|
22
|
+
const coordinate = parseTaskId(value);
|
|
23
|
+
if (formatTaskId(coordinate) !== value)
|
|
24
|
+
fail(`${label} must be a canonical TaskId`);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
fail(`${label} must be a canonical TaskId`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function comparison(value) {
|
|
32
|
+
if (typeof value !== "string" || !comparisons.includes(value))
|
|
33
|
+
fail("query operator is invalid");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function timestamp(value, field) {
|
|
37
|
+
if (typeof value !== "string" || !Number.isFinite(Date.parse(value)) || new Date(Date.parse(value)).toISOString() !== value) {
|
|
38
|
+
fail(`${field} must be a canonical UTC ISO timestamp`);
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
function equalityOperator(operator, field) {
|
|
43
|
+
if (operator !== "=" && operator !== "!=")
|
|
44
|
+
fail(`${field} supports only = and !=`);
|
|
45
|
+
return operator;
|
|
46
|
+
}
|
|
47
|
+
function orderedOperator(operator, field) {
|
|
48
|
+
if (operator === "~")
|
|
49
|
+
fail(`${field} does not support ~`);
|
|
50
|
+
return operator;
|
|
51
|
+
}
|
|
52
|
+
function normalizeStatePredicate(input, operator) {
|
|
53
|
+
if (typeof input.value !== "string" || !states.includes(input.value))
|
|
54
|
+
fail("state value is invalid");
|
|
55
|
+
return { field: "state", operator: equalityOperator(operator, "state"), value: input.value };
|
|
56
|
+
}
|
|
57
|
+
function normalizePriorityPredicate(input, operator) {
|
|
58
|
+
if (typeof input.value !== "number" || !Number.isInteger(input.value) || input.value < 0 || input.value > 3) {
|
|
59
|
+
fail("priority value must be 0..3");
|
|
60
|
+
}
|
|
61
|
+
return { field: "priority", operator: orderedOperator(operator, "priority"), value: input.value };
|
|
62
|
+
}
|
|
63
|
+
function normalizeTextPredicate(input, field, operator) {
|
|
64
|
+
if (typeof input.value !== "string" || input.value.length === 0)
|
|
65
|
+
fail(`${field} value must be nonblank`);
|
|
66
|
+
if (operator !== "=" && operator !== "!=" && operator !== "~")
|
|
67
|
+
fail(`${field} supports only =, !=, and ~`);
|
|
68
|
+
if (field === "id" && operator !== "~")
|
|
69
|
+
taskId(input.value, "id value");
|
|
70
|
+
return { field, operator, value: input.value };
|
|
71
|
+
}
|
|
72
|
+
function normalizeParentPredicate(input, operator) {
|
|
73
|
+
const value = input.value === null ? null : taskId(input.value, "parent value");
|
|
74
|
+
return { field: "parent", operator: equalityOperator(operator, "parent"), value };
|
|
75
|
+
}
|
|
76
|
+
function normalizeRelationPredicate(input, field, operator) {
|
|
77
|
+
return { field, operator: equalityOperator(operator, field), value: taskId(input.value, `${field} value`) };
|
|
78
|
+
}
|
|
79
|
+
function normalizeBooleanPredicate(input, field, operator) {
|
|
80
|
+
if (typeof input.value !== "boolean")
|
|
81
|
+
fail(`${field} value must be boolean`);
|
|
82
|
+
return { field, operator: equalityOperator(operator, field), value: input.value };
|
|
83
|
+
}
|
|
84
|
+
function normalizeTimestampPredicate(input, field, operator) {
|
|
85
|
+
return { field, operator: orderedOperator(operator, field), value: timestamp(input.value, `${field} value`) };
|
|
86
|
+
}
|
|
87
|
+
function predicate(value) {
|
|
88
|
+
const input = object(value, "query predicate");
|
|
89
|
+
closed(input, ["field", "operator", "value"], "query predicate");
|
|
90
|
+
const field = input.field;
|
|
91
|
+
if (typeof field !== "string")
|
|
92
|
+
fail("query predicate field is required");
|
|
93
|
+
const operator = comparison(input.operator);
|
|
94
|
+
switch (field) {
|
|
95
|
+
case "state": return normalizeStatePredicate(input, operator);
|
|
96
|
+
case "priority": return normalizePriorityPredicate(input, operator);
|
|
97
|
+
case "title": return normalizeTextPredicate(input, "title", operator);
|
|
98
|
+
case "id": return normalizeTextPredicate(input, "id", operator);
|
|
99
|
+
case "parent": return normalizeParentPredicate(input, operator);
|
|
100
|
+
case "under": return normalizeRelationPredicate(input, "under", operator);
|
|
101
|
+
case "needs": return normalizeRelationPredicate(input, "needs", operator);
|
|
102
|
+
case "blocks": return normalizeRelationPredicate(input, "blocks", operator);
|
|
103
|
+
case "ready": return normalizeBooleanPredicate(input, "ready", operator);
|
|
104
|
+
case "blocked": return normalizeBooleanPredicate(input, "blocked", operator);
|
|
105
|
+
case "created": return normalizeTimestampPredicate(input, "created", operator);
|
|
106
|
+
case "updated": return normalizeTimestampPredicate(input, "updated", operator);
|
|
107
|
+
default: return fail(`unknown query field: ${field}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export function normalizeTaskQuery(value) {
|
|
111
|
+
const input = object(value, "query expression");
|
|
112
|
+
if (input.kind === "predicate") {
|
|
113
|
+
closed(input, ["kind", "predicate"], "query expression");
|
|
114
|
+
return { kind: "predicate", predicate: predicate(input.predicate) };
|
|
115
|
+
}
|
|
116
|
+
if (input.kind === "not") {
|
|
117
|
+
closed(input, ["kind", "term"], "query expression");
|
|
118
|
+
return { kind: "not", term: normalizeTaskQuery(input.term) };
|
|
119
|
+
}
|
|
120
|
+
if (input.kind === "and" || input.kind === "or") {
|
|
121
|
+
closed(input, ["kind", "terms"], "query expression");
|
|
122
|
+
if (!Array.isArray(input.terms) || input.terms.length === 0)
|
|
123
|
+
fail(`${input.kind} query requires terms`);
|
|
124
|
+
return { kind: input.kind, terms: input.terms.map((term) => normalizeTaskQuery(term)) };
|
|
125
|
+
}
|
|
126
|
+
fail("query expression kind is invalid");
|
|
127
|
+
}
|
|
128
|
+
function compare(left, operator, right) {
|
|
129
|
+
if (operator === "=")
|
|
130
|
+
return left === right;
|
|
131
|
+
if (operator === "!=")
|
|
132
|
+
return left !== right;
|
|
133
|
+
if (operator === "<")
|
|
134
|
+
return left < right;
|
|
135
|
+
if (operator === "<=")
|
|
136
|
+
return left <= right;
|
|
137
|
+
if (operator === ">")
|
|
138
|
+
return left > right;
|
|
139
|
+
return left >= right;
|
|
140
|
+
}
|
|
141
|
+
function membership(left, operator, right) { return operator === "=" ? left === right : left !== right; }
|
|
142
|
+
function indexChildren(board) {
|
|
143
|
+
const children = new Map();
|
|
144
|
+
for (const task of board.tasks.values()) {
|
|
145
|
+
if (task.parent === null)
|
|
146
|
+
continue;
|
|
147
|
+
const siblings = children.get(task.parent) ?? [];
|
|
148
|
+
siblings.push(task.id);
|
|
149
|
+
children.set(task.parent, siblings);
|
|
150
|
+
}
|
|
151
|
+
return children;
|
|
152
|
+
}
|
|
153
|
+
function indexReverseNeeds(board) {
|
|
154
|
+
const reverse = new Map();
|
|
155
|
+
for (const task of board.tasks.values()) {
|
|
156
|
+
for (const need of task.needs) {
|
|
157
|
+
const blockers = reverse.get(need) ?? new Set();
|
|
158
|
+
blockers.add(task.id);
|
|
159
|
+
reverse.set(need, blockers);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return reverse;
|
|
163
|
+
}
|
|
164
|
+
function descendants(children, parent) {
|
|
165
|
+
const result = new Set(), pending = [parent];
|
|
166
|
+
while (pending.length > 0) {
|
|
167
|
+
const current = pending.shift();
|
|
168
|
+
for (const child of children.get(current) ?? []) {
|
|
169
|
+
if (result.has(child))
|
|
170
|
+
continue;
|
|
171
|
+
result.add(child);
|
|
172
|
+
pending.push(child);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
function relation(board, reverseNeeds, id, field, target) {
|
|
178
|
+
const task = board.tasks.get(id);
|
|
179
|
+
if (task === undefined)
|
|
180
|
+
return false;
|
|
181
|
+
return field === "needs" ? task.needs.includes(target) : reverseNeeds.get(target)?.has(id) === true;
|
|
182
|
+
}
|
|
183
|
+
function matchText(left, operator, right) {
|
|
184
|
+
return operator === "~" ? left.includes(right) : compare(left, operator, right);
|
|
185
|
+
}
|
|
186
|
+
function matchRelation(selected, operator) {
|
|
187
|
+
return operator === "=" ? selected : !selected;
|
|
188
|
+
}
|
|
189
|
+
function isBlocked(board, task) {
|
|
190
|
+
const active = task.state === "open" || task.state === "in_progress";
|
|
191
|
+
return active && task.needs.some((need) => {
|
|
192
|
+
const target = board.tasks.get(need);
|
|
193
|
+
return target === undefined || (target.state !== "done" && target.state !== "drop");
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function taskRef(board, id) {
|
|
197
|
+
const task = board.tasks.get(id);
|
|
198
|
+
return task === undefined ? { id, title: null, state: "missing" } : { id, title: task.title, state: task.state };
|
|
199
|
+
}
|
|
200
|
+
function matchesPredicate(board, reverseNeeds, task, value, under) {
|
|
201
|
+
switch (value.field) {
|
|
202
|
+
case "state": return compare(task.state, value.operator, value.value);
|
|
203
|
+
case "priority": return compare(task.priority, value.operator, value.value);
|
|
204
|
+
case "title": return matchText(task.title, value.operator, value.value);
|
|
205
|
+
case "id": return matchText(task.id, value.operator, value.value);
|
|
206
|
+
case "parent": return compare(task.parent ?? "", value.operator, value.value ?? "");
|
|
207
|
+
case "under": return matchRelation(under.get(value.value)?.has(task.id) === true, value.operator);
|
|
208
|
+
case "needs": return matchRelation(relation(board, reverseNeeds, task.id, "needs", value.value), value.operator);
|
|
209
|
+
case "blocks": return matchRelation(relation(board, reverseNeeds, task.id, "blocks", value.value), value.operator);
|
|
210
|
+
case "ready": return membership(taskDisposition(board, task) === "ready", value.operator, value.value);
|
|
211
|
+
case "blocked": return membership(isBlocked(board, task), value.operator, value.value);
|
|
212
|
+
case "created": return compare(task.createdAt, value.operator, value.value);
|
|
213
|
+
case "updated": return compare(task.updatedAt, value.operator, value.value);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function matches(board, reverseNeeds, task, expression, under) {
|
|
217
|
+
if (expression.kind === "not")
|
|
218
|
+
return !matches(board, reverseNeeds, task, expression.term, under);
|
|
219
|
+
if (expression.kind !== "predicate") {
|
|
220
|
+
return expression.kind === "and"
|
|
221
|
+
? expression.terms.every((term) => matches(board, reverseNeeds, task, term, under))
|
|
222
|
+
: expression.terms.some((term) => matches(board, reverseNeeds, task, term, under));
|
|
223
|
+
}
|
|
224
|
+
return matchesPredicate(board, reverseNeeds, task, expression.predicate, under);
|
|
225
|
+
}
|
|
226
|
+
function row(board, reverseNeeds, task) {
|
|
227
|
+
return {
|
|
228
|
+
id: task.id, title: task.title, state: task.state, priority: task.priority, disposition: taskDisposition(board, task),
|
|
229
|
+
parent: task.parent,
|
|
230
|
+
needs: task.needs.map((need) => taskRef(board, need)),
|
|
231
|
+
blocks: [...(reverseNeeds.get(task.id) ?? [])]
|
|
232
|
+
.sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)))
|
|
233
|
+
.map((id) => taskRef(board, id)),
|
|
234
|
+
createdAt: task.createdAt, updatedAt: task.updatedAt,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function sortTasks(tasks, sort) {
|
|
238
|
+
return [...tasks].sort((left, right) => {
|
|
239
|
+
const tie = Buffer.compare(Buffer.from(left.id), Buffer.from(right.id));
|
|
240
|
+
if (sort === "id")
|
|
241
|
+
return tie;
|
|
242
|
+
if (sort === "priority")
|
|
243
|
+
return left.priority - right.priority || tie;
|
|
244
|
+
const compared = left[sort === "created" ? "createdAt" : "updatedAt"].localeCompare(right[sort === "created" ? "createdAt" : "updatedAt"]);
|
|
245
|
+
return (sort === "updated" ? -compared : compared) || tie;
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
export function projectQuery(board, scope, expression, sort = "priority", limit = DEFAULT_TASK_LIMIT) {
|
|
249
|
+
const children = indexChildren(board);
|
|
250
|
+
const reverseNeeds = indexReverseNeeds(board);
|
|
251
|
+
const under = new Map(queryUnderTargets(expression).map((target) => [target, descendants(children, target)]));
|
|
252
|
+
const selected = sortTasks([...board.tasks.values()].filter((task) => scope === null || sameNamespace(parseTaskId(task.id).namespace, scope))
|
|
253
|
+
.filter((task) => matches(board, reverseNeeds, task, expression, under)), sort);
|
|
254
|
+
const rows = selected.slice(0, limit).map((task) => row(board, reverseNeeds, task));
|
|
255
|
+
return { rows, total: selected.length, returned: rows.length, truncated: rows.length < selected.length };
|
|
256
|
+
}
|
|
257
|
+
export function projectPage(rows, limit = DEFAULT_TASK_LIMIT) {
|
|
258
|
+
const visible = rows.slice(0, limit);
|
|
259
|
+
return { rows: visible, total: rows.length, returned: visible.length, truncated: visible.length < rows.length };
|
|
260
|
+
}
|
|
261
|
+
export function underExpression(parent) {
|
|
262
|
+
return { kind: "predicate", predicate: { field: "under", operator: "=", value: parent } };
|
|
263
|
+
}
|
|
264
|
+
export function queryUnderTargets(expression) {
|
|
265
|
+
const targets = new Set();
|
|
266
|
+
function collect(value) {
|
|
267
|
+
if (value.kind === "predicate" && value.predicate.field === "under")
|
|
268
|
+
targets.add(value.predicate.value);
|
|
269
|
+
else if (value.kind === "not")
|
|
270
|
+
collect(value.term);
|
|
271
|
+
else if (value.kind !== "predicate")
|
|
272
|
+
value.terms.forEach(collect);
|
|
273
|
+
}
|
|
274
|
+
collect(expression);
|
|
275
|
+
return [...targets];
|
|
276
|
+
}
|
|
277
|
+
export function isValidTaskLimit(value) {
|
|
278
|
+
return Number.isInteger(value) && value > 0 && value <= MAX_TASK_LIMIT;
|
|
279
|
+
}
|
|
@@ -1,22 +1,20 @@
|
|
|
1
|
+
import type { WorldRoot } from "../world.js";
|
|
1
2
|
import { type TaskId } from "./identity.js";
|
|
2
3
|
import type { TaskBoard } from "./board.js";
|
|
3
|
-
export type TaskWorld = Readonly<{
|
|
4
|
-
root: string;
|
|
5
|
-
}>;
|
|
6
4
|
export type BoardSnapshot = Readonly<{
|
|
7
5
|
board: TaskBoard;
|
|
8
6
|
bytes: ReadonlyMap<TaskId, Uint8Array>;
|
|
9
7
|
}>;
|
|
10
|
-
export declare function readBoard(world:
|
|
8
|
+
export declare function readBoard(world: WorldRoot): Promise<BoardSnapshot>;
|
|
11
9
|
export declare function replaceAuthority(input: Readonly<{
|
|
12
10
|
path: string;
|
|
13
11
|
expected: Uint8Array | null;
|
|
14
12
|
next: Uint8Array;
|
|
15
|
-
}>): "replaced" | "concurrent-modification"
|
|
13
|
+
}>): Promise<"replaced" | "concurrent-modification">;
|
|
16
14
|
export declare function withTaskLocks<T>(input: Readonly<{
|
|
17
|
-
world:
|
|
15
|
+
world: WorldRoot;
|
|
18
16
|
allocation: boolean;
|
|
19
17
|
ids: readonly TaskId[];
|
|
20
18
|
signal?: AbortSignal;
|
|
21
19
|
}>, action: () => Promise<T>): Promise<T | "busy">;
|
|
22
|
-
export declare function authorityPath(world:
|
|
20
|
+
export declare function authorityPath(world: WorldRoot, id: TaskId): string;
|