@astrosheep/keiyaku 2.9.8 → 2.9.10
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 +50 -182
- 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 +10 -7
- 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 +242 -79
- 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 +65 -29
- package/build/cli/render/projection-history.js +64 -0
- package/build/cli/render/shared.js +23 -9
- package/build/cli/render/status.js +13 -35
- package/build/cli/render/task.js +51 -26
- package/build/cli/render/terminal-failure.js +3 -1
- package/build/cli/render/wait.js +45 -69
- package/build/cli/subagent-guard.js +1 -1
- package/build/cli/types.js +1 -1
- package/build/config/env-keys.js +3 -1
- package/build/config/env.js +24 -1
- 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 +13 -6
- package/build/core/projection/index.js +5 -4
- package/build/core/projection/projection-activity.js +29 -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 +35 -337
- 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 +125 -127
- 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
package/build/core/task/board.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FlowError } from "../../flow-error.js";
|
|
2
|
-
import {
|
|
2
|
+
import { formatTaskCoordinate, parseTaskCoordinate } from "./coordinate.js";
|
|
3
3
|
function compareText(left, right) {
|
|
4
4
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
5
5
|
}
|
|
@@ -14,7 +14,16 @@ function taskSort(left, right) {
|
|
|
14
14
|
return leftTerminal - rightTerminal
|
|
15
15
|
|| left.pri - right.pri
|
|
16
16
|
|| compareText(left.createdAt, right.createdAt)
|
|
17
|
-
|| compareText(left.id, right.id);
|
|
17
|
+
|| compareText(left.coordinate ?? left.id, right.coordinate ?? right.id);
|
|
18
|
+
}
|
|
19
|
+
function normalizeDocument(task) {
|
|
20
|
+
const namespace = task.namespace ?? null;
|
|
21
|
+
const coordinate = task.coordinate ?? formatTaskCoordinate(namespace, task.id);
|
|
22
|
+
const parsed = parseTaskCoordinate(coordinate);
|
|
23
|
+
if (parsed.namespace !== namespace || parsed.localId !== task.id) {
|
|
24
|
+
throw new FlowError("INVALID_TARGET", `task coordinate ${coordinate} does not match its namespace and local id`);
|
|
25
|
+
}
|
|
26
|
+
return { ...task, namespace, coordinate };
|
|
18
27
|
}
|
|
19
28
|
function cycleComponents(tasks) {
|
|
20
29
|
let nextIndex = 0;
|
|
@@ -59,6 +68,9 @@ function cycleComponents(tasks) {
|
|
|
59
68
|
visit(id);
|
|
60
69
|
return cycles;
|
|
61
70
|
}
|
|
71
|
+
function supersedesCycleComponents(tasks) {
|
|
72
|
+
return cycleComponents(new Map([...tasks].map(([id, task]) => [id, { ...task, needs: task.supersedes }])));
|
|
73
|
+
}
|
|
62
74
|
function parentCycleComponents(tasks) {
|
|
63
75
|
const cycles = [];
|
|
64
76
|
const visited = new Set();
|
|
@@ -85,21 +97,23 @@ function parentCycleComponents(tasks) {
|
|
|
85
97
|
}
|
|
86
98
|
/** Derive the complete storage-neutral board from domain records. */
|
|
87
99
|
export function buildTaskBoardFromDocuments(documents) {
|
|
100
|
+
documents = documents.map(normalizeDocument);
|
|
88
101
|
const diseases = [];
|
|
89
102
|
const counts = new Map();
|
|
90
103
|
for (const task of documents)
|
|
91
|
-
counts.set(task.
|
|
104
|
+
counts.set(task.coordinate, (counts.get(task.coordinate) ?? 0) + 1);
|
|
92
105
|
const candidates = new Map();
|
|
93
106
|
for (const task of documents) {
|
|
94
|
-
|
|
107
|
+
const coordinate = task.coordinate;
|
|
108
|
+
if (counts.get(coordinate) > 1) {
|
|
95
109
|
diseases.push({
|
|
96
110
|
code: "duplicate_id",
|
|
97
|
-
message: `duplicate authoritative task
|
|
98
|
-
id:
|
|
111
|
+
message: `duplicate authoritative task coordinate ${coordinate}`,
|
|
112
|
+
id: coordinate,
|
|
99
113
|
});
|
|
100
114
|
}
|
|
101
115
|
else {
|
|
102
|
-
candidates.set(
|
|
116
|
+
candidates.set(coordinate, task);
|
|
103
117
|
}
|
|
104
118
|
}
|
|
105
119
|
for (const component of cycleComponents(candidates)) {
|
|
@@ -114,6 +128,18 @@ export function buildTaskBoardFromDocuments(documents) {
|
|
|
114
128
|
candidates.delete(id);
|
|
115
129
|
}
|
|
116
130
|
}
|
|
131
|
+
for (const component of supersedesCycleComponents(candidates)) {
|
|
132
|
+
const relatedIds = [...component].sort();
|
|
133
|
+
for (const id of relatedIds) {
|
|
134
|
+
diseases.push({
|
|
135
|
+
code: "supersedes_cycle",
|
|
136
|
+
message: `task ${id} participates in a supersedes cycle`,
|
|
137
|
+
id,
|
|
138
|
+
relatedIds,
|
|
139
|
+
});
|
|
140
|
+
candidates.delete(id);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
117
143
|
for (const component of parentCycleComponents(candidates)) {
|
|
118
144
|
const relatedIds = [...component].sort();
|
|
119
145
|
for (const id of relatedIds) {
|
|
@@ -129,7 +155,7 @@ export function buildTaskBoardFromDocuments(documents) {
|
|
|
129
155
|
while (changed) {
|
|
130
156
|
changed = false;
|
|
131
157
|
for (const [id, task] of [...candidates].sort(([a], [b]) => compareText(a, b))) {
|
|
132
|
-
const missing = [...task.needs, ...(task.parent ? [task.parent] : []), ...task.from]
|
|
158
|
+
const missing = [...task.needs, ...(task.parent ? [task.parent] : []), ...task.from, ...task.supersedes, ...task.relatesTo]
|
|
133
159
|
.filter((related) => !candidates.has(related));
|
|
134
160
|
if (missing.length === 0)
|
|
135
161
|
continue;
|
|
@@ -145,65 +171,74 @@ export function buildTaskBoardFromDocuments(documents) {
|
|
|
145
171
|
}
|
|
146
172
|
}
|
|
147
173
|
const blocks = new Map();
|
|
174
|
+
const supersededBy = new Map();
|
|
175
|
+
const relatedTasks = new Map();
|
|
148
176
|
for (const task of candidates.values()) {
|
|
149
177
|
for (const need of task.needs)
|
|
150
|
-
blocks.set(need, [...(blocks.get(need) ?? []), task.
|
|
178
|
+
blocks.set(need, [...(blocks.get(need) ?? []), task.coordinate]);
|
|
179
|
+
for (const superseded of task.supersedes)
|
|
180
|
+
supersededBy.set(superseded, [...(supersededBy.get(superseded) ?? []), task.coordinate]);
|
|
181
|
+
for (const related of task.relatesTo)
|
|
182
|
+
relatedTasks.set(related, [...(relatedTasks.get(related) ?? []), task.coordinate]);
|
|
151
183
|
}
|
|
152
184
|
const tasks = [...candidates.values()].sort(taskSort).map((task) => {
|
|
153
185
|
const blockingNeeds = task.needs.filter((id) => {
|
|
154
186
|
const state = candidates.get(id).state;
|
|
155
|
-
return state === "open" || state === "in_progress";
|
|
187
|
+
return state === "open" || state === "in_progress" || state === "on_hold";
|
|
156
188
|
});
|
|
157
189
|
return {
|
|
158
190
|
id: task.id,
|
|
191
|
+
namespace: task.namespace ?? null,
|
|
192
|
+
coordinate: task.coordinate,
|
|
159
193
|
title: task.title,
|
|
160
194
|
state: task.state,
|
|
161
195
|
pri: task.pri,
|
|
162
196
|
needs: [...task.needs],
|
|
163
197
|
parent: task.parent,
|
|
164
198
|
from: [...task.from],
|
|
199
|
+
supersedes: [...task.supersedes],
|
|
200
|
+
relatesTo: [...task.relatesTo],
|
|
165
201
|
createdAt: task.createdAt,
|
|
166
202
|
...(task.updatedAt === undefined ? {} : { updatedAt: task.updatedAt }),
|
|
167
203
|
creator: task.creator,
|
|
168
204
|
...(task.startedAt === undefined ? {} : { startedAt: task.startedAt }),
|
|
169
205
|
...(task.contractId === undefined ? {} : { contractId: task.contractId }),
|
|
170
206
|
body: task.body,
|
|
171
|
-
blocks: [...(blocks.get(task.
|
|
207
|
+
blocks: [...(blocks.get(task.coordinate) ?? [])].sort(),
|
|
172
208
|
blocked: task.state === "open" && blockingNeeds.length > 0,
|
|
173
209
|
releasedNeeds: task.needs.filter((id) => candidates.get(id).state === "drop").sort(),
|
|
210
|
+
supersededBy: [...(supersededBy.get(task.coordinate) ?? [])].sort(),
|
|
211
|
+
relatedTasks: [...new Set([...task.relatesTo, ...(relatedTasks.get(task.coordinate) ?? [])])].sort(),
|
|
174
212
|
};
|
|
175
213
|
});
|
|
176
214
|
return {
|
|
177
215
|
tasks,
|
|
178
216
|
diseases: diseases.sort(diseaseSort),
|
|
179
|
-
byId: new Map(tasks.map((task) => [task.
|
|
217
|
+
byId: new Map(tasks.map((task) => [task.coordinate, task])),
|
|
180
218
|
};
|
|
181
219
|
}
|
|
182
220
|
export function resolveTaskReadId(board, input) {
|
|
183
221
|
return resolveTaskId(board, input);
|
|
184
222
|
}
|
|
185
223
|
export function resolveTaskId(board, input) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (board.byId.has(input))
|
|
190
|
-
return input;
|
|
224
|
+
const requested = parseTaskCoordinate(input);
|
|
225
|
+
if (board.byId.has(requested.coordinate))
|
|
226
|
+
return requested.coordinate;
|
|
191
227
|
const matches = board.tasks
|
|
192
|
-
.
|
|
193
|
-
.
|
|
228
|
+
.filter((task) => task.namespace === requested.namespace && task.id.startsWith(requested.localId))
|
|
229
|
+
.map((task) => task.coordinate)
|
|
194
230
|
.sort();
|
|
195
231
|
if (matches.length === 0)
|
|
196
232
|
throw new FlowError("INVALID_TARGET", `task '${input}' not found`);
|
|
197
233
|
if (matches.length > 1) {
|
|
198
|
-
throw Object.assign(new FlowError("
|
|
234
|
+
throw Object.assign(new FlowError("TASK_AMBIGUOUS", `task prefix '${input}' is ambiguous: ${matches.join(", ")}`, {
|
|
235
|
+
facts: { kind: "task_ambiguous", candidates: matches },
|
|
236
|
+
}), { candidates: matches });
|
|
199
237
|
}
|
|
200
238
|
return matches[0];
|
|
201
239
|
}
|
|
202
240
|
export function requireCanonicalTaskId(input) {
|
|
203
|
-
|
|
204
|
-
throw new FlowError("INVALID_TARGET", "task mutations require a canonical full task id");
|
|
205
|
-
}
|
|
206
|
-
return input;
|
|
241
|
+
return parseTaskCoordinate(input, "task mutation coordinate").coordinate;
|
|
207
242
|
}
|
|
208
243
|
export function requireHealthy(board) {
|
|
209
244
|
if (board.diseases.length > 0) {
|
|
@@ -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
|
+
}
|