@astrosheep/keiyaku 2.9.9 → 2.9.11
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 +93 -174
- package/build/.tsbuildinfo +1 -1
- package/build/agents/harness/control-types.js +18 -1
- package/build/agents/harness/index.js +2 -0
- package/build/agents/harness/outcome-error.js +81 -0
- package/build/agents/harness/projection.js +2 -1
- package/build/agents/harness/router.js +5 -1
- package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
- package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
- package/build/agents/providers/claude-agent-sdk/session.js +19 -5
- package/build/agents/providers/codex-app-server/adapter.js +11 -6
- package/build/agents/providers/codex-app-server/events.js +67 -6
- package/build/agents/providers/codex-app-server/registration.js +11 -1
- package/build/agents/providers/opencode-sdk/adapter.js +8 -1
- package/build/agents/providers/opencode-sdk/events.js +24 -2
- package/build/agents/providers/opencode-sdk/registration.js +11 -1
- package/build/agents/providers/pi/checkpoint.js +9 -1
- package/build/agents/providers/pi/events.js +2 -0
- package/build/agents/providers/pi/registration.js +14 -1
- package/build/cli/commands/akuma/akuma/handler.js +1 -1
- package/build/cli/commands/akuma/akuma/meta.js +3 -3
- package/build/cli/commands/akuma/catalog.js +2 -2
- package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
- package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
- package/build/cli/commands/akuma.js +1 -1
- package/build/cli/commands/contract/amend/handler.js +5 -4
- package/build/cli/commands/contract/arc/handler.js +5 -4
- package/build/cli/commands/contract/audit/handler.js +2 -2
- package/build/cli/commands/contract/bind/handler.js +12 -3
- package/build/cli/commands/contract/bind/meta.js +45 -15
- package/build/cli/commands/contract/forfeit/handler.js +9 -5
- package/build/cli/commands/contract/log/handler.js +2 -2
- package/build/cli/commands/contract/petition/handler.js +34 -7
- package/build/cli/commands/contract/renew/handler.js +7 -7
- package/build/cli/commands/contract/renew/meta.js +3 -3
- package/build/cli/commands/metadata.js +10 -6
- package/build/cli/commands/projection/call/meta.js +1 -1
- package/build/cli/commands/projection/catalog.js +2 -0
- package/build/cli/commands/projection/history/handler.js +21 -0
- package/build/cli/commands/projection/history/meta.js +9 -0
- package/build/cli/commands/projection/shared.js +3 -4
- package/build/cli/commands/projection/status/handler.js +3 -3
- package/build/cli/commands/projection/wait/handler.js +20 -11
- package/build/cli/commands/task/add/handler.js +14 -5
- package/build/cli/commands/task/add/meta.js +2 -2
- package/build/cli/commands/task/catalog.js +4 -0
- package/build/cli/commands/task/done/handler.js +1 -4
- package/build/cli/commands/task/done/meta.js +1 -1
- package/build/cli/commands/task/drop/meta.js +1 -1
- package/build/cli/commands/task/hold/handler.js +3 -0
- package/build/cli/commands/task/hold/meta.js +8 -0
- package/build/cli/commands/task/log/handler.js +1 -1
- package/build/cli/commands/task/log/meta.js +2 -2
- package/build/cli/commands/task/resume/handler.js +3 -0
- package/build/cli/commands/task/resume/meta.js +8 -0
- package/build/cli/commands/task/shared.js +39 -14
- package/build/cli/commands/task/show/handler.js +1 -1
- package/build/cli/commands/task/show/meta.js +1 -1
- package/build/cli/commands/task/start/meta.js +1 -1
- package/build/cli/commands/task/stop/handler.js +1 -4
- package/build/cli/commands/task/stop/meta.js +1 -1
- package/build/cli/commands/task/update/handler.js +11 -3
- package/build/cli/commands/task/update/meta.js +3 -3
- package/build/cli/completion.js +3 -2
- package/build/cli/flags.js +51 -14
- package/build/cli/help.js +4 -1
- package/build/cli/index.js +40 -25
- package/build/cli/parse-flags.js +40 -7
- package/build/cli/parse-metadata.js +5 -2
- package/build/cli/parse-selectors.js +2 -3
- package/build/cli/parse.js +40 -6
- package/build/cli/projection-address.js +12 -10
- package/build/cli/render/amendment-warning.js +8 -0
- package/build/cli/render/arc.js +6 -2
- package/build/cli/render/audit.js +400 -81
- package/build/cli/render/kanshi.js +6 -1
- package/build/cli/render/lifecycle.js +56 -0
- package/build/cli/render/misc.js +24 -28
- package/build/cli/render/petition.js +5 -0
- package/build/cli/render/projection-activity.js +3 -2
- package/build/cli/render/projection-history.js +64 -0
- package/build/cli/render/shared.js +23 -9
- package/build/cli/render/status.js +9 -16
- package/build/cli/render/task.js +51 -26
- package/build/cli/render/terminal-failure.js +3 -1
- package/build/cli/render/wait.js +3 -5
- package/build/cli/subagent-guard.js +1 -1
- package/build/cli/types.js +1 -1
- package/build/config/env-keys.js +2 -0
- package/build/config/env.js +23 -0
- package/build/config/settings/schema.js +3 -1
- package/build/core/address-carrier.js +89 -0
- package/build/core/addressing.js +255 -196
- package/build/core/amend.js +115 -38
- package/build/core/amendment-criteria.js +17 -0
- package/build/core/amendment-warning.js +10 -0
- package/build/core/arc.js +184 -83
- package/build/core/audit/candidate.js +110 -9
- package/build/core/audit/coordinates.js +120 -65
- package/build/core/audit/evidence.js +19 -23
- package/build/core/audit/facade.js +34 -11
- package/build/core/audit/report.js +5 -5
- package/build/core/bind-reconciliation.js +390 -0
- package/build/core/bind-workspace.js +12 -20
- package/build/core/bind.js +242 -273
- package/build/core/call/call.js +1 -0
- package/build/core/call/context.js +46 -71
- package/build/core/call/prompt.js +6 -3
- package/build/core/context.js +49 -35
- package/build/core/contract-carrier-runtime.js +408 -0
- package/build/core/contract-carrier.js +270 -0
- package/build/core/contract-view.js +33 -27
- package/build/core/contract.js +5 -1
- package/build/core/derived-replay.js +207 -0
- package/build/core/draft.js +125 -57
- package/build/core/forfeit.js +113 -56
- package/build/core/identifier-slug.js +63 -13
- package/build/core/ids.js +4 -4
- package/build/core/lifecycle-history.js +35 -0
- package/build/core/lifecycle-recovery.js +117 -0
- package/build/core/lifecycle-runner.js +377 -0
- package/build/core/log.js +37 -11
- package/build/core/outcome-base.js +181 -0
- package/build/core/process-group.js +139 -0
- package/build/core/projection/generation/database.js +82 -7
- package/build/core/projection/generation/model.js +9 -36
- package/build/core/projection/generation/projection-generation-continuation.js +10 -22
- package/build/core/projection/generation/projection-generation-execution.js +6 -5
- package/build/core/projection/index.js +5 -4
- package/build/core/projection/projection-activity.js +27 -8
- package/build/core/projection/projection-alias.js +23 -10
- package/build/core/projection/projection-core.js +21 -7
- package/build/core/projection/projection-execution-observer.js +91 -39
- package/build/core/projection/projection-history.js +474 -0
- package/build/core/projection/projection-runner-lock.js +12 -0
- package/build/core/projection/projection-status-observation.js +365 -0
- package/build/core/projection/projection-status.js +21 -358
- package/build/core/projection/projection-terminal-failure.js +13 -8
- package/build/core/projection/projection-wait.js +7 -2
- package/build/core/projection/tell/launch-store.js +244 -0
- package/build/core/projection/tell/model.js +3 -3
- package/build/core/projection/tell/store.js +70 -118
- package/build/core/registry.js +22 -87
- package/build/core/renew-build.js +424 -0
- package/build/core/renew-plan.js +239 -0
- package/build/core/renew-rewrite.js +77 -0
- package/build/core/renew.js +295 -520
- package/build/core/repository-ledger/accepted-fold-read.js +19 -0
- package/build/core/repository-ledger/accepted-tail.js +116 -0
- package/build/core/repository-ledger/admission.js +38 -0
- package/build/core/repository-ledger/authoritative-transaction.js +1 -0
- package/build/core/repository-ledger/claim-evidence.js +147 -0
- package/build/core/repository-ledger/codec.js +297 -0
- package/build/core/repository-ledger/current-state-store.js +853 -0
- package/build/core/repository-ledger/fold-repository.js +375 -0
- package/build/core/repository-ledger/fold.js +248 -0
- package/build/core/repository-ledger/git-object-store.js +243 -0
- package/build/core/repository-ledger/history-read.js +15 -0
- package/build/core/repository-ledger/incremental-read.js +153 -0
- package/build/core/repository-ledger/index.js +15 -0
- package/build/core/repository-ledger/inventory-plan.js +75 -0
- package/build/core/repository-ledger/inventory.js +310 -0
- package/build/core/repository-ledger/publication-admission.js +125 -0
- package/build/core/repository-ledger/publication.js +353 -0
- package/build/core/repository-ledger/read-model.js +469 -0
- package/build/core/repository-ledger/transaction-membership.js +78 -0
- package/build/core/repository-ledger/traversal.js +404 -0
- package/build/core/repository-ledger/write-transaction.js +339 -0
- package/build/core/seal.js +168 -161
- package/build/core/settlement/claim-delivery.js +42 -215
- package/build/core/settlement/claim.js +235 -153
- package/build/core/settlement/index.js +4 -4
- package/build/core/settlement/petition-claim-gates.js +52 -33
- package/build/core/settlement/petition-forfeit.js +12 -5
- package/build/core/settlement/petition-head-guard.js +8 -5
- package/build/core/settlement/petition-preview.js +68 -56
- package/build/core/settlement/petition-window.js +50 -0
- package/build/core/settlement/petition.js +252 -131
- package/build/core/settlement/queue-read-model.js +72 -95
- package/build/core/settlement/queue-seat-allocation.js +35 -126
- package/build/core/settlement/queue.js +1 -1
- package/build/core/settlement/settlement.js +172 -80
- package/build/core/settlement/verdict.js +112 -66
- package/build/core/settlement/verification-coordination.js +305 -0
- package/build/core/settlement/verification-supervisor.js +275 -0
- package/build/core/settlement/verification.js +398 -31
- package/build/core/status/board.js +114 -119
- package/build/core/status/lifecycle.js +7 -80
- package/build/core/structured-query.js +23 -0
- package/build/core/target-ref.js +1 -1
- package/build/core/task/board.js +59 -24
- package/build/core/task/commands.js +181 -33
- package/build/core/task/coordinate.js +56 -0
- package/build/core/task/document.js +25 -9
- package/build/core/task/index.js +7 -4
- package/build/core/task/query.js +17 -27
- package/build/core/task/settlement-git.js +164 -179
- package/build/core/task/settlement-policy.js +15 -11
- package/build/core/task/source-board.js +31 -13
- package/build/core/task/task-bind-preparation.js +193 -0
- package/build/core/task/task-contract.js +102 -193
- package/build/core/task/task-file-transaction.js +171 -0
- package/build/core/task/task-git-runtime.js +76 -14
- package/build/core/task/task-git-store.js +68 -169
- package/build/core/task/task-store-repository.js +13 -6
- package/build/core/task/task-worktree.js +6 -33
- package/build/core/task/task.js +1 -1
- package/build/core/task/tree.js +3 -3
- package/build/core/task/validation.js +4 -3
- package/build/core/transcripts.js +93 -1
- package/build/core/worktree-bootstrap.js +44 -36
- package/build/core/worktree-path.js +210 -77
- package/build/flow-error.js +7 -7
- package/build/generated/version.js +2 -2
- package/build/git/core.js +76 -13
- package/build/git/refs.js +168 -46
- package/build/git/streaming-batch.js +73 -13
- package/package.json +1 -1
- package/skills/keiyaku/SKILL.md +1 -1
- package/skills/keiyaku-akuma/SKILL.md +3 -3
- package/skills/keiyaku-task/SKILL.md +4 -2
- package/skills/keiyaku-workflow/SKILL.md +88 -203
- package/build/core/entry.js +0 -305
- package/build/core/ledger-batch.js +0 -194
- package/build/core/ledger.js +0 -84
- package/build/core/projection/leash.js +0 -87
- package/build/core/ref-log.js +0 -119
- package/build/core/renew-session.js +0 -128
- package/build/core/status/ledger-batch.js +0 -1
- package/build/core/status/reconciliation.js +0 -137
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { FlowError } from "../../flow-error.js";
|
|
2
|
-
import { IdentifierSlugError,
|
|
2
|
+
import { IdentifierSlugError, taskIdentifierSlug, } from "../identifier-slug.js";
|
|
3
|
+
import { markdownSourceSections } from "../scope.js";
|
|
3
4
|
import { requireCanonicalTaskId, requireHealthy, resolveTaskId, } from "./board.js";
|
|
4
5
|
import { dateTimestamp, invalid, nonblank, } from "./validation.js";
|
|
5
6
|
import { advancedTaskUpdatedAt } from "./model.js";
|
|
7
|
+
import { formatTaskCoordinate, parseTaskCoordinate } from "./coordinate.js";
|
|
6
8
|
export async function readTaskBoard(repository) {
|
|
7
9
|
return repository.read();
|
|
8
10
|
}
|
|
9
11
|
function documentFromView(view) {
|
|
10
12
|
return {
|
|
11
13
|
id: view.id,
|
|
14
|
+
namespace: view.namespace,
|
|
15
|
+
coordinate: view.coordinate,
|
|
12
16
|
title: view.title,
|
|
13
17
|
state: view.state,
|
|
14
18
|
pri: view.pri,
|
|
15
19
|
needs: [...view.needs],
|
|
16
20
|
parent: view.parent,
|
|
17
21
|
from: [...view.from],
|
|
22
|
+
supersedes: [...view.supersedes],
|
|
23
|
+
relatesTo: [...view.relatesTo],
|
|
18
24
|
createdAt: view.createdAt,
|
|
19
25
|
...(view.updatedAt === undefined ? {} : { updatedAt: view.updatedAt }),
|
|
20
26
|
creator: view.creator,
|
|
@@ -23,14 +29,18 @@ function documentFromView(view) {
|
|
|
23
29
|
body: view.body,
|
|
24
30
|
};
|
|
25
31
|
}
|
|
32
|
+
function documentCoordinate(document) {
|
|
33
|
+
return document.coordinate ?? formatTaskCoordinate(document.namespace ?? null, document.id);
|
|
34
|
+
}
|
|
26
35
|
function replaceDocument(document, subject) {
|
|
27
36
|
return {
|
|
28
37
|
replacements: [document],
|
|
29
38
|
subject,
|
|
30
39
|
select(resulting) {
|
|
31
|
-
const
|
|
40
|
+
const coordinate = documentCoordinate(document);
|
|
41
|
+
const value = resulting.byId.get(coordinate);
|
|
32
42
|
if (!value)
|
|
33
|
-
throw new FlowError("INTERNAL_STATE", `mutated task ${
|
|
43
|
+
throw new FlowError("INTERNAL_STATE", `mutated task ${coordinate} disappeared from the board`);
|
|
34
44
|
return value;
|
|
35
45
|
},
|
|
36
46
|
};
|
|
@@ -44,24 +54,45 @@ function requireExisting(board, id) {
|
|
|
44
54
|
function resolveMutationIds(board, ids) {
|
|
45
55
|
return ids.map((id) => resolveTaskId(board, id));
|
|
46
56
|
}
|
|
57
|
+
function validateRelations(board, id, supersedes, relatesTo) {
|
|
58
|
+
if (supersedes.includes(id) || relatesTo.includes(id))
|
|
59
|
+
invalid(`task ${id} relations must not reference itself`);
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
const reachesSource = (current) => {
|
|
62
|
+
if (current === id)
|
|
63
|
+
return true;
|
|
64
|
+
if (seen.has(current))
|
|
65
|
+
return false;
|
|
66
|
+
seen.add(current);
|
|
67
|
+
return (board.byId.get(current)?.supersedes ?? []).some(reachesSource);
|
|
68
|
+
};
|
|
69
|
+
for (const target of supersedes) {
|
|
70
|
+
seen.clear();
|
|
71
|
+
if (reachesSource(target))
|
|
72
|
+
invalid(`task ${id} would create a supersedes cycle`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function updateRelations(board, document, field, additions, removals) {
|
|
76
|
+
const added = resolveMutationIds(board, additions ?? []);
|
|
77
|
+
if (new Set(added).size !== added.length)
|
|
78
|
+
invalid(`task ${field} edges must be unique`);
|
|
79
|
+
const removed = new Set(resolveMutationIds(board, removals ?? []));
|
|
80
|
+
const retained = (document[field] ?? []).filter((value) => !removed.has(value));
|
|
81
|
+
if (added.some((value) => retained.includes(value)))
|
|
82
|
+
invalid(`task ${field} edges must be unique`);
|
|
83
|
+
document[field] = [...retained, ...added];
|
|
84
|
+
}
|
|
47
85
|
export async function createTask(repository, input, deps) {
|
|
48
86
|
nonblank("title", input.title);
|
|
49
87
|
nonblank("creator", input.creator);
|
|
50
|
-
let baseId;
|
|
51
|
-
try {
|
|
52
|
-
baseId = normalizeIdentifierSlug(input.title, SUFFIXABLE_IDENTIFIER_SLUG_MAX_UTF8_BYTES);
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
if (error instanceof IdentifierSlugError) {
|
|
56
|
-
throw new FlowError("INVALID_TARGET", `task title ${error.message}`, { cause: error });
|
|
57
|
-
}
|
|
58
|
-
throw error;
|
|
59
|
-
}
|
|
60
88
|
const pri = input.pri ?? 2;
|
|
61
89
|
if (!Number.isInteger(pri) || pri < 0 || pri > 3)
|
|
62
90
|
invalid("task priority must be an integer from 0 through 3");
|
|
63
91
|
return repository.mutate((board) => {
|
|
64
92
|
requireHealthy(board);
|
|
93
|
+
const namespace = input.namespace ?? null;
|
|
94
|
+
if (namespace !== null)
|
|
95
|
+
parseTaskCoordinate(`${namespace}/task`, "task namespace");
|
|
65
96
|
const needs = resolveMutationIds(board, input.needs ?? []);
|
|
66
97
|
if (new Set(needs).size !== needs.length)
|
|
67
98
|
invalid("task needs must be unique");
|
|
@@ -69,34 +100,64 @@ export async function createTask(repository, input, deps) {
|
|
|
69
100
|
const from = resolveMutationIds(board, input.from ?? []);
|
|
70
101
|
if (new Set(from).size !== from.length)
|
|
71
102
|
invalid("task from edges must be unique");
|
|
72
|
-
|
|
73
|
-
|
|
103
|
+
const supersedes = resolveMutationIds(board, input.supersedes ?? []);
|
|
104
|
+
if (new Set(supersedes).size !== supersedes.length)
|
|
105
|
+
invalid("task supersedes edges must be unique");
|
|
106
|
+
const relatesTo = resolveMutationIds(board, input.relatesTo ?? []);
|
|
107
|
+
if (new Set(relatesTo).size !== relatesTo.length)
|
|
108
|
+
invalid("task relatesTo edges must be unique");
|
|
109
|
+
let ordinal = 1;
|
|
110
|
+
let id;
|
|
111
|
+
for (;;) {
|
|
74
112
|
if (!Number.isSafeInteger(ordinal)) {
|
|
75
|
-
throw new FlowError("INTERNAL_STATE",
|
|
113
|
+
throw new FlowError("INTERNAL_STATE", "task id collision ordinal exceeded the safe integer range");
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
id = taskIdentifierSlug(input.title, ordinal === 1 ? "" : `-${ordinal}`);
|
|
76
117
|
}
|
|
77
|
-
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof IdentifierSlugError) {
|
|
120
|
+
throw new FlowError("INVALID_TARGET", `task title ${error.message}`, { cause: error });
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
if (!board.byId.has(formatTaskCoordinate(namespace, id)))
|
|
125
|
+
break;
|
|
126
|
+
ordinal += 1;
|
|
78
127
|
}
|
|
128
|
+
const coordinate = formatTaskCoordinate(namespace, id);
|
|
129
|
+
validateRelations(board, coordinate, supersedes, relatesTo);
|
|
79
130
|
const now = deps.now();
|
|
80
131
|
const document = {
|
|
81
132
|
id,
|
|
133
|
+
namespace,
|
|
134
|
+
coordinate,
|
|
82
135
|
title: input.title,
|
|
83
136
|
state: "open",
|
|
84
137
|
pri,
|
|
85
138
|
needs,
|
|
86
139
|
parent,
|
|
87
140
|
from,
|
|
141
|
+
supersedes,
|
|
142
|
+
relatesTo,
|
|
88
143
|
createdAt: dateTimestamp("createdAt", now),
|
|
89
144
|
updatedAt: dateTimestamp("updatedAt", now),
|
|
90
145
|
creator: input.creator,
|
|
91
146
|
body: input.body ?? "",
|
|
92
147
|
};
|
|
93
|
-
return replaceDocument(document, `Add task ${
|
|
148
|
+
return replaceDocument(document, `Add task ${coordinate}`);
|
|
94
149
|
});
|
|
95
150
|
}
|
|
96
151
|
export async function updateTask(repository, fullIdValue, patch, now = new Date()) {
|
|
152
|
+
return (await updateTaskWithReceipt(repository, fullIdValue, patch, now)).task;
|
|
153
|
+
}
|
|
154
|
+
export async function updateTaskWithReceipt(repository, fullIdValue, patch, now = new Date()) {
|
|
97
155
|
const hasChange = Object.values(patch).some((value) => value !== undefined);
|
|
98
156
|
if (!hasChange)
|
|
99
157
|
throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
|
|
158
|
+
if (patch.body !== undefined && patch.appendBody !== undefined) {
|
|
159
|
+
throw new FlowError("EMPTY_PARAM", "task update cannot combine --body and --append");
|
|
160
|
+
}
|
|
100
161
|
return repository.mutate((board) => {
|
|
101
162
|
requireHealthy(board);
|
|
102
163
|
const id = resolveTaskId(board, fullIdValue);
|
|
@@ -106,6 +167,8 @@ export async function updateTask(repository, fullIdValue, patch, now = new Date(
|
|
|
106
167
|
document.title = nonblank("title", patch.title);
|
|
107
168
|
if (patch.body !== undefined)
|
|
108
169
|
document.body = patch.body;
|
|
170
|
+
if (patch.appendBody !== undefined)
|
|
171
|
+
document.body = appendBody(document.body, patch.appendBody);
|
|
109
172
|
if (patch.pri !== undefined) {
|
|
110
173
|
if (!Number.isInteger(patch.pri) || patch.pri < 0 || patch.pri > 3)
|
|
111
174
|
invalid("task priority must be an integer from 0 through 3");
|
|
@@ -119,18 +182,82 @@ export async function updateTask(repository, fullIdValue, patch, now = new Date(
|
|
|
119
182
|
if (patch.from !== undefined) {
|
|
120
183
|
document.from = [...new Set([...document.from, ...resolveMutationIds(board, patch.from)])];
|
|
121
184
|
}
|
|
185
|
+
updateRelations(board, document, "supersedes", patch.addSupersedes, patch.dropSupersedes);
|
|
186
|
+
updateRelations(board, document, "relatesTo", patch.addRelatesTo, patch.dropRelatesTo);
|
|
187
|
+
validateRelations(board, document.coordinate, document.supersedes, document.relatesTo);
|
|
122
188
|
if (JSON.stringify(document) === JSON.stringify(documentFromView(current))) {
|
|
123
189
|
throw new FlowError("EMPTY_PARAM", "task update requires at least one change");
|
|
124
190
|
}
|
|
125
191
|
document.updatedAt = advancedTaskUpdatedAt(current, now);
|
|
126
|
-
|
|
192
|
+
const bodyChange = patch.body === undefined ? undefined : describeBodyChange(current.body, document.body);
|
|
193
|
+
return {
|
|
194
|
+
replacements: [document],
|
|
195
|
+
subject: `Update task ${id}`,
|
|
196
|
+
select(resulting) {
|
|
197
|
+
const coordinate = documentCoordinate(document);
|
|
198
|
+
const task = resulting.byId.get(coordinate);
|
|
199
|
+
if (!task)
|
|
200
|
+
throw new FlowError("INTERNAL_STATE", `mutated task ${coordinate} disappeared from the board`);
|
|
201
|
+
return { task, ...(bodyChange === undefined ? {} : { bodyChange }) };
|
|
202
|
+
},
|
|
203
|
+
};
|
|
127
204
|
});
|
|
128
205
|
}
|
|
206
|
+
function appendBody(body, addition) {
|
|
207
|
+
if (!body || !addition || body.endsWith("\n\n") || addition.startsWith("\n"))
|
|
208
|
+
return body + addition;
|
|
209
|
+
return `${body}${body.endsWith("\n") ? "\n" : "\n\n"}${addition}`;
|
|
210
|
+
}
|
|
211
|
+
function markdownLevelTwoSections(body) {
|
|
212
|
+
return markdownSourceSections(body)
|
|
213
|
+
.filter((section) => section.level === 2)
|
|
214
|
+
.map((section) => section.title.trim());
|
|
215
|
+
}
|
|
216
|
+
function bodyLines(body) {
|
|
217
|
+
return body.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
218
|
+
}
|
|
219
|
+
function lineDiffstat(before, after) {
|
|
220
|
+
const oldLines = bodyLines(before);
|
|
221
|
+
const newLines = bodyLines(after);
|
|
222
|
+
const lcs = Array(newLines.length + 1).fill(0);
|
|
223
|
+
for (const oldLine of oldLines) {
|
|
224
|
+
let previous = 0;
|
|
225
|
+
for (let index = 1; index <= newLines.length; index += 1) {
|
|
226
|
+
const saved = lcs[index];
|
|
227
|
+
lcs[index] = oldLine === newLines[index - 1]
|
|
228
|
+
? previous + 1
|
|
229
|
+
: Math.max(lcs[index], lcs[index - 1]);
|
|
230
|
+
previous = saved;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const unchanged = lcs[newLines.length];
|
|
234
|
+
return { additions: newLines.length - unchanged, deletions: oldLines.length - unchanged };
|
|
235
|
+
}
|
|
236
|
+
function describeBodyChange(before, after) {
|
|
237
|
+
const remaining = new Map();
|
|
238
|
+
for (const section of markdownLevelTwoSections(after)) {
|
|
239
|
+
remaining.set(section, (remaining.get(section) ?? 0) + 1);
|
|
240
|
+
}
|
|
241
|
+
const oldBytes = Buffer.byteLength(before, "utf8");
|
|
242
|
+
const newBytes = Buffer.byteLength(after, "utf8");
|
|
243
|
+
return {
|
|
244
|
+
oldBytes,
|
|
245
|
+
newBytes,
|
|
246
|
+
removedSections: markdownLevelTwoSections(before).filter((section) => {
|
|
247
|
+
const count = remaining.get(section) ?? 0;
|
|
248
|
+
if (count === 0)
|
|
249
|
+
return true;
|
|
250
|
+
remaining.set(section, count - 1);
|
|
251
|
+
return false;
|
|
252
|
+
}),
|
|
253
|
+
diffstat: lineDiffstat(before, after),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
129
256
|
async function transition(repository, fullIdValue, apply, subject, now) {
|
|
130
257
|
return repository.mutate((board) => {
|
|
131
258
|
requireHealthy(board);
|
|
132
259
|
const id = fullIdValue === undefined
|
|
133
|
-
? selectSoleInProgress(board).
|
|
260
|
+
? selectSoleInProgress(board).coordinate
|
|
134
261
|
: resolveTaskId(board, fullIdValue);
|
|
135
262
|
const current = requireExisting(board, id);
|
|
136
263
|
const document = documentFromView(current);
|
|
@@ -141,17 +268,18 @@ async function transition(repository, fullIdValue, apply, subject, now) {
|
|
|
141
268
|
}
|
|
142
269
|
function rejectBound(document, action = "state") {
|
|
143
270
|
if (document.contractId !== undefined) {
|
|
271
|
+
const coordinate = documentCoordinate(document);
|
|
144
272
|
if (action === "drop") {
|
|
145
|
-
throw new FlowError("UNSUPPORTED_POLICY", `task ${
|
|
273
|
+
throw new FlowError("UNSUPPORTED_POLICY", `task ${coordinate} is bound to contract ${document.contractId}; forfeit the contract before dropping this task`);
|
|
146
274
|
}
|
|
147
|
-
throw new FlowError("UNSUPPORTED_POLICY", `task ${
|
|
275
|
+
throw new FlowError("UNSUPPORTED_POLICY", `task ${coordinate} is bound to contract ${document.contractId}; its state follows the contract`);
|
|
148
276
|
}
|
|
149
277
|
}
|
|
150
278
|
export async function startTask(repository, fullId, now) {
|
|
151
279
|
return transition(repository, fullId, (document) => {
|
|
152
280
|
rejectBound(document);
|
|
153
281
|
if (document.state !== "open")
|
|
154
|
-
invalid(`task ${document
|
|
282
|
+
invalid(`task ${documentCoordinate(document)} is ${document.state}, not open`);
|
|
155
283
|
document.state = "in_progress";
|
|
156
284
|
document.startedAt = dateTimestamp("startedAt", now);
|
|
157
285
|
}, (id) => `Start task ${id}`, now);
|
|
@@ -160,16 +288,34 @@ export async function stopTask(repository, fullId, now = new Date()) {
|
|
|
160
288
|
return transition(repository, fullId, (document) => {
|
|
161
289
|
rejectBound(document);
|
|
162
290
|
if (document.state !== "in_progress")
|
|
163
|
-
invalid(`task ${document
|
|
291
|
+
invalid(`task ${documentCoordinate(document)} is ${document.state}, not in_progress`);
|
|
164
292
|
document.state = "open";
|
|
165
293
|
delete document.startedAt;
|
|
166
294
|
}, (id) => `Stop task ${id}`, now);
|
|
167
295
|
}
|
|
168
|
-
export async function
|
|
296
|
+
export async function holdTask(repository, fullId, now = new Date()) {
|
|
169
297
|
return transition(repository, fullId, (document) => {
|
|
170
298
|
rejectBound(document);
|
|
171
299
|
if (document.state !== "open" && document.state !== "in_progress") {
|
|
172
|
-
invalid(`task ${document
|
|
300
|
+
invalid(`task ${documentCoordinate(document)} is ${document.state}, not open or in_progress`);
|
|
301
|
+
}
|
|
302
|
+
document.state = "on_hold";
|
|
303
|
+
delete document.startedAt;
|
|
304
|
+
}, (id) => `Hold task ${id}`, now);
|
|
305
|
+
}
|
|
306
|
+
export async function resumeTask(repository, fullId, now = new Date()) {
|
|
307
|
+
return transition(repository, fullId, (document) => {
|
|
308
|
+
rejectBound(document);
|
|
309
|
+
if (document.state !== "on_hold")
|
|
310
|
+
invalid(`task ${documentCoordinate(document)} is ${document.state}, not on_hold`);
|
|
311
|
+
document.state = "open";
|
|
312
|
+
}, (id) => `Resume task ${id}`, now);
|
|
313
|
+
}
|
|
314
|
+
export async function doneTask(repository, fullId, now = new Date()) {
|
|
315
|
+
return transition(repository, fullId, (document) => {
|
|
316
|
+
rejectBound(document);
|
|
317
|
+
if (document.state !== "open" && document.state !== "in_progress" && document.state !== "on_hold") {
|
|
318
|
+
invalid(`task ${documentCoordinate(document)} is ${document.state}, not open, in_progress, or on_hold`);
|
|
173
319
|
}
|
|
174
320
|
document.state = "done";
|
|
175
321
|
delete document.startedAt;
|
|
@@ -178,8 +324,8 @@ export async function doneTask(repository, fullId, now = new Date()) {
|
|
|
178
324
|
export async function dropTask(repository, fullId, now = new Date()) {
|
|
179
325
|
return transition(repository, fullId, (document) => {
|
|
180
326
|
rejectBound(document, "drop");
|
|
181
|
-
if (document.state !== "open" && document.state !== "in_progress") {
|
|
182
|
-
invalid(`task ${document
|
|
327
|
+
if (document.state !== "open" && document.state !== "in_progress" && document.state !== "on_hold") {
|
|
328
|
+
invalid(`task ${documentCoordinate(document)} is ${document.state}, not open, in_progress, or on_hold`);
|
|
183
329
|
}
|
|
184
330
|
document.state = "drop";
|
|
185
331
|
delete document.startedAt;
|
|
@@ -189,11 +335,13 @@ export function selectSoleInProgress(board) {
|
|
|
189
335
|
const candidates = board.tasks.filter((task) => task.state === "in_progress");
|
|
190
336
|
if (candidates.length === 1)
|
|
191
337
|
return candidates[0];
|
|
192
|
-
const ids = candidates.map((task) => task.
|
|
193
|
-
const code = candidates.length === 0 ? "INVALID_TARGET" : "
|
|
338
|
+
const ids = candidates.map((task) => task.coordinate).sort();
|
|
339
|
+
const code = candidates.length === 0 ? "INVALID_TARGET" : "TASK_AMBIGUOUS";
|
|
194
340
|
throw Object.assign(new FlowError(code, candidates.length === 0
|
|
195
341
|
? "no task in progress"
|
|
196
|
-
: "more than one task in progress; pass
|
|
342
|
+
: "more than one task in progress; pass a coordinate", candidates.length === 0 ? undefined : {
|
|
343
|
+
facts: { kind: "task_ambiguous", candidates: ids },
|
|
344
|
+
}), { candidates: ids, candidateTasks: candidates });
|
|
197
345
|
}
|
|
198
346
|
/** Reopen task pointers whose contract authority has disappeared. The caller
|
|
199
347
|
* supplies the ledger fact; this operation itself remains storage-neutral. */
|
|
@@ -201,7 +349,7 @@ export async function rollbackOrphanTaskPointers(repository, contractIds, now =
|
|
|
201
349
|
const before = await repository.read();
|
|
202
350
|
const orphans = before.tasks
|
|
203
351
|
.filter((task) => task.contractId !== undefined && !contractIds.has(task.contractId))
|
|
204
|
-
.map((task) => ({ taskId: task.
|
|
352
|
+
.map((task) => ({ taskId: task.coordinate, contractId: task.contractId }))
|
|
205
353
|
.sort((a, b) => a.taskId.localeCompare(b.taskId));
|
|
206
354
|
if (orphans.length === 0)
|
|
207
355
|
return [];
|
|
@@ -220,7 +368,7 @@ export async function rollbackOrphanTaskPointers(repository, contractIds, now =
|
|
|
220
368
|
if (replacements.length === 0)
|
|
221
369
|
throw new FlowError("CONCURRENT_MODIFICATION", "task pointers changed while doctor was reconciling them");
|
|
222
370
|
const subject = replacements.length === 1
|
|
223
|
-
? `Clear orphan contract pointer from task ${replacements[0]
|
|
371
|
+
? `Clear orphan contract pointer from task ${documentCoordinate(replacements[0])}`
|
|
224
372
|
: `Clear orphan contract pointers from ${replacements.length} tasks`;
|
|
225
373
|
return { replacements, subject, select: () => undefined };
|
|
226
374
|
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { FlowError } from "../../flow-error.js";
|
|
2
|
+
import { isNormalizedIdentifierSlug } from "../identifier-slug.js";
|
|
3
|
+
import { taskIdentifierSlug } from "../identifier-slug.js";
|
|
4
|
+
export function formatTaskCoordinate(namespace, localId) {
|
|
5
|
+
return namespace === null ? localId : `${namespace}/${localId}`;
|
|
6
|
+
}
|
|
7
|
+
export function parseTaskCoordinate(value, field = "task coordinate") {
|
|
8
|
+
const parts = value.split("/");
|
|
9
|
+
if (parts.length < 1 || parts.length > 2) {
|
|
10
|
+
throw new FlowError("INVALID_TARGET", `${field} must be <local-id> or <namespace>/<local-id>`);
|
|
11
|
+
}
|
|
12
|
+
const namespace = parts.length === 1 ? null : parts[0];
|
|
13
|
+
const localId = parts.at(-1);
|
|
14
|
+
if (!isNormalizedIdentifierSlug(localId) || (namespace !== null && !isNormalizedIdentifierSlug(namespace))) {
|
|
15
|
+
throw new FlowError("INVALID_TARGET", `${field} must be <local-id> or <namespace>/<local-id>`);
|
|
16
|
+
}
|
|
17
|
+
return { namespace, localId, coordinate: formatTaskCoordinate(namespace, localId) };
|
|
18
|
+
}
|
|
19
|
+
export function isTaskCoordinate(value) {
|
|
20
|
+
try {
|
|
21
|
+
parseTaskCoordinate(value);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function taskCoordinateFromSourcePath(sourcePath) {
|
|
29
|
+
const normalized = sourcePath.replaceAll("\\", "/");
|
|
30
|
+
const marker = "/.keiyaku/tasks/";
|
|
31
|
+
const markerIndex = normalized.lastIndexOf(marker);
|
|
32
|
+
const relative = markerIndex < 0 ? normalized : normalized.slice(markerIndex + marker.length);
|
|
33
|
+
if (!relative.endsWith(".md")) {
|
|
34
|
+
throw new FlowError("INVALID_TARGET", `task source must be a Markdown file below .keiyaku/tasks: ${sourcePath}`);
|
|
35
|
+
}
|
|
36
|
+
return parseTaskCoordinate(relative.slice(0, -3), "task source coordinate");
|
|
37
|
+
}
|
|
38
|
+
export const TASK_NAMESPACE_SCOPE_PREFIX = ".keiyaku/tasks/";
|
|
39
|
+
export function taskNamespaceScope(namespace) {
|
|
40
|
+
parseTaskCoordinate(`${namespace}/task`, "task namespace");
|
|
41
|
+
return `${TASK_NAMESPACE_SCOPE_PREFIX}${namespace}/**`;
|
|
42
|
+
}
|
|
43
|
+
export function taskNamespaceFromScope(term) {
|
|
44
|
+
const match = /^\.keiyaku\/tasks\/([^/]+)\/\*\*$/.exec(term);
|
|
45
|
+
if (!match || !isNormalizedIdentifierSlug(match[1]))
|
|
46
|
+
return undefined;
|
|
47
|
+
return match[1];
|
|
48
|
+
}
|
|
49
|
+
export function allocateTaskNamespace(title, occupied) {
|
|
50
|
+
for (let ordinal = 1; Number.isSafeInteger(ordinal); ordinal += 1) {
|
|
51
|
+
const candidate = taskIdentifierSlug(title, ordinal === 1 ? "" : `-${ordinal}`);
|
|
52
|
+
if (!occupied.has(candidate))
|
|
53
|
+
return candidate;
|
|
54
|
+
}
|
|
55
|
+
throw new FlowError("INTERNAL_STATE", "task namespace collision ordinal exceeded the safe integer range");
|
|
56
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAlias, isMap, isScalar, isSeq, parseDocument, stringify, } from "yaml";
|
|
2
|
-
import { canonicalTimestamp, fullId, invalid, nonblank, TASK_STATES, } from "./validation.js";
|
|
2
|
+
import { canonicalTimestamp, fullId, isTaskId, invalid, nonblank, TASK_STATES, } from "./validation.js";
|
|
3
3
|
const FIELD_ORDER = [
|
|
4
4
|
"id",
|
|
5
5
|
"title",
|
|
@@ -8,6 +8,8 @@ const FIELD_ORDER = [
|
|
|
8
8
|
"needs",
|
|
9
9
|
"parent",
|
|
10
10
|
"from",
|
|
11
|
+
"supersedes",
|
|
12
|
+
"relatesTo",
|
|
11
13
|
"createdAt",
|
|
12
14
|
"updatedAt",
|
|
13
15
|
"creator",
|
|
@@ -100,11 +102,13 @@ export function parseTaskDocument(source) {
|
|
|
100
102
|
invalid(`task field '${key}' must be a scalar or string array`);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
|
-
const id =
|
|
105
|
+
const id = nonblank("id", values.get("id"));
|
|
106
|
+
if (!isTaskId(id))
|
|
107
|
+
invalid("task field 'id' must be a canonical local task id");
|
|
104
108
|
const title = nonblank("title", values.get("title"));
|
|
105
109
|
const state = values.get("state");
|
|
106
110
|
if (typeof state !== "string" || !TASK_STATES.has(state)) {
|
|
107
|
-
invalid("task field 'state' must be open, in_progress, done, or drop");
|
|
111
|
+
invalid("task field 'state' must be open, in_progress, on_hold, done, or drop");
|
|
108
112
|
}
|
|
109
113
|
const pri = values.get("pri");
|
|
110
114
|
if (typeof pri !== "number" || !Number.isInteger(pri) || pri < 0 || pri > 3) {
|
|
@@ -120,10 +124,7 @@ export function parseTaskDocument(source) {
|
|
|
120
124
|
const value = values.get(field);
|
|
121
125
|
if (value === null)
|
|
122
126
|
return null;
|
|
123
|
-
|
|
124
|
-
if (related === id)
|
|
125
|
-
invalid(`task field '${field}' must not reference itself`);
|
|
126
|
-
return related;
|
|
127
|
+
return fullId(field, value);
|
|
127
128
|
};
|
|
128
129
|
const parent = relationship("parent");
|
|
129
130
|
const fromValue = values.get("from");
|
|
@@ -132,8 +133,19 @@ export function parseTaskDocument(source) {
|
|
|
132
133
|
const from = fromValue.map((related) => fullId("from", related));
|
|
133
134
|
if (new Set(from).size !== from.length)
|
|
134
135
|
invalid("task field 'from' must be unique");
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
const relationships = (field) => {
|
|
137
|
+
const value = values.get(field);
|
|
138
|
+
if (value === undefined)
|
|
139
|
+
return [];
|
|
140
|
+
if (!Array.isArray(value))
|
|
141
|
+
invalid(`task field '${field}' must be a string array`);
|
|
142
|
+
const related = value.map((item) => fullId(field, item));
|
|
143
|
+
if (new Set(related).size !== related.length)
|
|
144
|
+
invalid(`task field '${field}' must be unique`);
|
|
145
|
+
return related;
|
|
146
|
+
};
|
|
147
|
+
const supersedes = relationships("supersedes");
|
|
148
|
+
const relatesTo = relationships("relatesTo");
|
|
137
149
|
const createdAt = canonicalTimestamp("createdAt", values.get("createdAt"));
|
|
138
150
|
const updatedValue = values.get("updatedAt");
|
|
139
151
|
const updatedAt = updatedValue === undefined ? undefined : canonicalTimestamp("updatedAt", updatedValue);
|
|
@@ -156,6 +168,8 @@ export function parseTaskDocument(source) {
|
|
|
156
168
|
needs,
|
|
157
169
|
parent,
|
|
158
170
|
from,
|
|
171
|
+
supersedes,
|
|
172
|
+
relatesTo,
|
|
159
173
|
createdAt,
|
|
160
174
|
...(updatedAt === undefined ? {} : { updatedAt }),
|
|
161
175
|
creator,
|
|
@@ -177,6 +191,8 @@ function serializeUnchecked(document) {
|
|
|
177
191
|
needs: [...document.needs],
|
|
178
192
|
parent: document.parent,
|
|
179
193
|
from: document.from,
|
|
194
|
+
...(document.supersedes.length === 0 ? {} : { supersedes: document.supersedes }),
|
|
195
|
+
...(document.relatesTo.length === 0 ? {} : { relatesTo: document.relatesTo }),
|
|
180
196
|
createdAt: document.createdAt,
|
|
181
197
|
...(document.updatedAt === undefined ? {} : { updatedAt: document.updatedAt }),
|
|
182
198
|
creator: document.creator,
|
package/build/core/task/index.js
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
* Only symbols required by current external consumers are re-exported.
|
|
4
4
|
* Package-internal modules must import each other directly, not through this file.
|
|
5
5
|
*/
|
|
6
|
-
export { previewTaskBoardForBind, reconcileTaskForfeit,
|
|
6
|
+
export { previewTaskBoardForBind, reconcileTaskForfeit, replayClaimTasks, } from "./task-contract.js";
|
|
7
|
+
export { replayPreparedTaskBind, replayPreparedTaskBindLocked, replayCommittedTaskBind, replayCommittedTaskBindForfeit, replayPreparedTaskBindForfeit, withPreparedTaskBind, } from "./task-bind-preparation.js";
|
|
8
|
+
export { readTaskBoardGeneration } from "./task-git-store.js";
|
|
7
9
|
export { resolveTaskCreator } from "./task-git-actor.js";
|
|
8
|
-
export { openTaskCommandRepository, readTaskDoctor, taskCommandContractId, } from "./task-git-runtime.js";
|
|
9
|
-
export { openTaskStore, readTaskHistory } from "./task-git-store.js";
|
|
10
|
+
export { openTaskCommandRepository, openTaskMutationContext, readTaskDoctor, taskCommandContractId, taskCommandNamespace, } from "./task-git-runtime.js";
|
|
11
|
+
export { openTaskStore, readTaskHistory, readTaskNamespaces } from "./task-git-store.js";
|
|
10
12
|
export { taskRepositoryFromStore } from "./task-store-repository.js";
|
|
11
13
|
export { isTaskId } from "./validation.js";
|
|
12
|
-
export {
|
|
14
|
+
export { isTaskCoordinate, parseTaskCoordinate, taskCoordinateFromSourcePath } from "./coordinate.js";
|
|
15
|
+
export { buildTaskTree, createTask, doneTask, dropTask, parseTaskDocument, readTaskBoard, resolveTaskReadId, serializeTaskDocument, startTask, stopTask, holdTask, resumeTask, updateTask, updateTaskWithReceipt, deriveTaskDisplay, selectTaskList, taskUpdatedAt, advancedTaskUpdatedAt, } from "./task.js";
|
package/build/core/task/query.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { FlowError } from "../../flow-error.js";
|
|
2
2
|
import { isCommissionId } from "../ids.js";
|
|
3
3
|
import { taskUpdatedAt } from "./model.js";
|
|
4
|
-
import {
|
|
4
|
+
import { isTaskCoordinate } from "./coordinate.js";
|
|
5
|
+
import { parseStructuredQuery, STRUCTURED_QUERY_COMPLETION, STRUCTURED_QUERY_GRAMMAR, } from "../structured-query.js";
|
|
5
6
|
export const TASK_FILTER_FIELDS = [
|
|
6
7
|
"title", "state", "priority", "parent", "needs", "blocks", "contract", "created", "updated",
|
|
7
8
|
];
|
|
8
9
|
export const TASK_FILTER_OPERATORS = ["=", "!=", "~", "<", "<=", ">", ">="];
|
|
9
|
-
export const TASK_FILTER_GRAMMAR =
|
|
10
|
-
export const TASK_FILTER_COMPLETION =
|
|
10
|
+
export const TASK_FILTER_GRAMMAR = STRUCTURED_QUERY_GRAMMAR;
|
|
11
|
+
export const TASK_FILTER_COMPLETION = STRUCTURED_QUERY_COMPLETION;
|
|
11
12
|
const FILTER_FIELDS = new Set(TASK_FILTER_FIELDS);
|
|
12
13
|
const ORDERED_FIELDS = new Set(["priority", "created", "updated"]);
|
|
13
14
|
const TITLE_OPERATORS = new Set(["=", "!=", "~"]);
|
|
14
15
|
const ORDERED_OPERATORS = new Set(["=", "!=", "<", "<=", ">", ">="]);
|
|
15
|
-
const DISPLAY_VALUES = new Set(["ready", "blocked", "in_progress", "done", "dropped", "bound"]);
|
|
16
|
+
const DISPLAY_VALUES = new Set(["ready", "blocked", "in_progress", "on_hold", "done", "dropped", "bound"]);
|
|
16
17
|
const SORT_KEYS = new Set(["priority", "modified", "created"]);
|
|
17
18
|
const RELATIVE_TIME = /^now-([1-9][0-9]*)([smhdw])$/;
|
|
18
19
|
const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
|
|
@@ -21,6 +22,8 @@ export function deriveTaskDisplay(task) {
|
|
|
21
22
|
return "done";
|
|
22
23
|
if (task.state === "drop")
|
|
23
24
|
return "dropped";
|
|
25
|
+
if (task.state === "on_hold")
|
|
26
|
+
return "on_hold";
|
|
24
27
|
if (task.contractId !== undefined)
|
|
25
28
|
return "bound";
|
|
26
29
|
if (task.state === "in_progress")
|
|
@@ -43,14 +46,7 @@ function invalidFilter(detail, recoveredTerms = []) {
|
|
|
43
46
|
},
|
|
44
47
|
});
|
|
45
48
|
}
|
|
46
|
-
function
|
|
47
|
-
const match = /^([a-z][a-z0-9_-]*)(!=|<=|>=|~|=|<|>)(.*)$/.exec(term);
|
|
48
|
-
if (!match)
|
|
49
|
-
invalidFilter(`'${expression}' must use ${TASK_FILTER_GRAMMAR}`, recoveredTerms);
|
|
50
|
-
const field = match[1];
|
|
51
|
-
const operator = match[2];
|
|
52
|
-
if (!FILTER_FIELDS.has(field))
|
|
53
|
-
invalidFilter(`unknown field '${field}'`, recoveredTerms);
|
|
49
|
+
function validateTaskFilterTerm(field, operator, values, recoveredTerms) {
|
|
54
50
|
if (field === "title" && !TITLE_OPERATORS.has(operator)) {
|
|
55
51
|
invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
|
|
56
52
|
}
|
|
@@ -59,21 +55,15 @@ function parseTaskFilterTerm(term, expression, recoveredTerms) {
|
|
|
59
55
|
: operator !== "=" && operator !== "!=")) {
|
|
60
56
|
invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
|
|
61
57
|
}
|
|
62
|
-
const values = match[3].split(",");
|
|
63
|
-
if (values.some((value) => value.length === 0))
|
|
64
|
-
invalidFilter(`field '${field}' has an empty value`, recoveredTerms);
|
|
65
58
|
validateFilterValues(field, values, recoveredTerms);
|
|
66
|
-
return { field, operator, values };
|
|
67
59
|
}
|
|
68
60
|
export function parseTaskFilter(expression) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
return parsed;
|
|
61
|
+
return parseStructuredQuery({
|
|
62
|
+
expression,
|
|
63
|
+
fields: FILTER_FIELDS,
|
|
64
|
+
invalid: invalidFilter,
|
|
65
|
+
validate: validateTaskFilterTerm,
|
|
66
|
+
});
|
|
77
67
|
}
|
|
78
68
|
function parseTimestamp(value, nowMs, recoveredTerms = []) {
|
|
79
69
|
const relative = RELATIVE_TIME.exec(value);
|
|
@@ -114,8 +104,8 @@ function validateFilterValues(field, values, recoveredTerms) {
|
|
|
114
104
|
for (const value of values) {
|
|
115
105
|
if (field === "parent" && value === "none")
|
|
116
106
|
continue;
|
|
117
|
-
if (!
|
|
118
|
-
invalidFilter(`value '${value}' must be a canonical full task
|
|
107
|
+
if (!isTaskCoordinate(value))
|
|
108
|
+
invalidFilter(`value '${value}' must be a canonical full task coordinate`, recoveredTerms);
|
|
119
109
|
}
|
|
120
110
|
return;
|
|
121
111
|
}
|
|
@@ -183,7 +173,7 @@ function selected(task, selection) {
|
|
|
183
173
|
}
|
|
184
174
|
function displayOrder(task) {
|
|
185
175
|
const display = deriveTaskDisplay(task);
|
|
186
|
-
return display === "in_progress" ? 0 : display === "
|
|
176
|
+
return display === "in_progress" ? 0 : display === "on_hold" ? 1 : display === "ready" ? 2 : display === "blocked" ? 3 : display === "bound" ? 4 : display === "done" ? 5 : 6;
|
|
187
177
|
}
|
|
188
178
|
function defaultCompare(left, right) {
|
|
189
179
|
return displayOrder(left) - displayOrder(right)
|