@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
|
@@ -2,13 +2,17 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { FlowError } from "../../flow-error.js";
|
|
4
4
|
import { runGitProcess } from "../../git/core.js";
|
|
5
|
+
import { openGitStreamingBatch } from "../../git/streaming-batch.js";
|
|
5
6
|
import { parseTaskDocument } from "./task.js";
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
import { parseTaskCoordinate } from "./coordinate.js";
|
|
8
|
+
async function gitResult(cwd, args, input, env, signal) {
|
|
9
|
+
return await runGitProcess(cwd, args, { input, env, signal });
|
|
8
10
|
}
|
|
9
|
-
async function git(cwd, args, input, env) {
|
|
10
|
-
const result = await gitResult(cwd, args, input, env);
|
|
11
|
+
async function git(cwd, args, input, env, signal) {
|
|
12
|
+
const result = await gitResult(cwd, args, input, env, signal);
|
|
11
13
|
if (result.error) {
|
|
14
|
+
if (result.error.name === "AbortError")
|
|
15
|
+
throw result.error;
|
|
12
16
|
throw new FlowError("INTERNAL_STATE", `git ${args[0]} failed: ${result.error.message}`, { cause: result.error });
|
|
13
17
|
}
|
|
14
18
|
if (result.status !== 0) {
|
|
@@ -23,15 +27,17 @@ function boardPath(root, source) {
|
|
|
23
27
|
}
|
|
24
28
|
return relative;
|
|
25
29
|
}
|
|
26
|
-
export async function inspectCurrentBranch(cwd) {
|
|
27
|
-
const branch = await gitResult(cwd, ["symbolic-ref", "--quiet", "HEAD"]);
|
|
30
|
+
export async function inspectCurrentBranch(cwd, signal) {
|
|
31
|
+
const branch = await gitResult(cwd, ["symbolic-ref", "--quiet", "HEAD"], undefined, undefined, signal);
|
|
28
32
|
if (branch.error) {
|
|
33
|
+
if (branch.error.name === "AbortError")
|
|
34
|
+
throw branch.error;
|
|
29
35
|
throw new FlowError("INTERNAL_STATE", `cannot inspect HEAD: ${branch.error.message}`, { cause: branch.error });
|
|
30
36
|
}
|
|
31
37
|
if (branch.status !== 0 || branch.stdout.trim() === "") {
|
|
32
38
|
throw new FlowError("DETACHED_HEAD", "cannot publish task board from detached HEAD");
|
|
33
39
|
}
|
|
34
|
-
return { ref: branch.stdout.trim(), head: (await git(cwd, ["rev-parse", "HEAD"])).trim() };
|
|
40
|
+
return { ref: branch.stdout.trim(), head: (await git(cwd, ["rev-parse", "HEAD"], undefined, undefined, signal)).trim() };
|
|
35
41
|
}
|
|
36
42
|
export async function createTaskBoardCommit(input) {
|
|
37
43
|
const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-task-index-"));
|
|
@@ -61,55 +67,64 @@ export async function createTaskMutationCommit(input) {
|
|
|
61
67
|
const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-task-index-"));
|
|
62
68
|
const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
|
|
63
69
|
try {
|
|
64
|
-
await git(input.cwd, ["read-tree", input.parent], undefined, env);
|
|
70
|
+
await git(input.cwd, ["read-tree", input.parent], undefined, env, input.signal);
|
|
65
71
|
for (const write of input.writes) {
|
|
66
|
-
const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], write.text)).trim();
|
|
72
|
+
const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], write.text, undefined, input.signal)).trim();
|
|
67
73
|
await git(input.cwd, [
|
|
68
74
|
"update-index",
|
|
69
75
|
"--add",
|
|
70
76
|
"--cacheinfo",
|
|
71
77
|
`100644,${oid},${taskMutationPath(write)}`,
|
|
72
|
-
], undefined, env);
|
|
78
|
+
], undefined, env, input.signal);
|
|
73
79
|
}
|
|
74
|
-
const tree = (await git(input.cwd, ["write-tree"], undefined, env)).trim();
|
|
75
|
-
const parentTree = (await git(input.cwd, ["show", "-s", "--format=%T", input.parent])).trim();
|
|
80
|
+
const tree = (await git(input.cwd, ["write-tree"], undefined, env, input.signal)).trim();
|
|
81
|
+
const parentTree = (await git(input.cwd, ["show", "-s", "--format=%T", input.parent], undefined, undefined, input.signal)).trim();
|
|
76
82
|
if (tree === parentTree)
|
|
77
83
|
return input.parent;
|
|
78
|
-
return (await git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject])).trim();
|
|
84
|
+
return (await git(input.cwd, ["commit-tree", tree, "-p", input.parent, "-m", input.subject], undefined, undefined, input.signal)).trim();
|
|
79
85
|
}
|
|
80
86
|
finally {
|
|
81
87
|
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
82
88
|
}
|
|
83
89
|
}
|
|
84
90
|
/** Stage only the mutated task paths into the real index; leave unrelated entries alone. */
|
|
85
|
-
export async function reconcileTaskMutationIndex(cwd, writes) {
|
|
91
|
+
export async function reconcileTaskMutationIndex(cwd, writes, signal) {
|
|
86
92
|
for (const write of writes) {
|
|
87
|
-
const oid = (await git(cwd, ["hash-object", "-w", "--stdin"], write.text)).trim();
|
|
93
|
+
const oid = (await git(cwd, ["hash-object", "-w", "--stdin"], write.text, undefined, signal)).trim();
|
|
88
94
|
await git(cwd, [
|
|
89
95
|
"update-index",
|
|
90
96
|
"--add",
|
|
91
97
|
"--cacheinfo",
|
|
92
98
|
`100644,${oid},${taskMutationPath(write)}`,
|
|
93
|
-
]);
|
|
99
|
+
], undefined, undefined, signal);
|
|
94
100
|
}
|
|
95
101
|
}
|
|
96
102
|
/**
|
|
97
|
-
* Atomically publish a pathspec-scoped task micro-commit
|
|
103
|
+
* Atomically publish a pathspec-scoped task micro-commit on the current checkout branch.
|
|
98
104
|
* Unrelated staged and unstaged non-task bytes are never read into the commit.
|
|
99
105
|
*/
|
|
100
106
|
export async function publishTaskMutationCommit(input, deps = {}) {
|
|
101
107
|
let lastError;
|
|
102
108
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
103
|
-
|
|
109
|
+
input.signal?.throwIfAborted();
|
|
110
|
+
const branch = await inspectCurrentBranch(input.cwd, input.signal);
|
|
104
111
|
const commit = await createTaskMutationCommit({
|
|
105
112
|
cwd: input.cwd,
|
|
106
113
|
gitDir: input.gitDir,
|
|
107
114
|
parent: branch.head,
|
|
108
115
|
writes: input.writes,
|
|
109
116
|
subject: input.subject,
|
|
117
|
+
signal: input.signal,
|
|
110
118
|
});
|
|
111
119
|
if (commit === branch.head) {
|
|
112
|
-
|
|
120
|
+
input.observeEffect?.({ effect: "task-reconciliation", status: "succeeded", commit, detail: "current" });
|
|
121
|
+
try {
|
|
122
|
+
await reconcileTaskMutationIndex(input.cwd, input.writes, input.signal);
|
|
123
|
+
input.observeEffect?.({ effect: "task-index-reconciliation", status: "succeeded", commit });
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
input.observeEffect?.({ effect: "task-index-reconciliation", status: "failed", commit, error });
|
|
127
|
+
}
|
|
113
128
|
return commit;
|
|
114
129
|
}
|
|
115
130
|
try {
|
|
@@ -118,12 +133,16 @@ export async function publishTaskMutationCommit(input, deps = {}) {
|
|
|
118
133
|
ref: branch.ref,
|
|
119
134
|
next: commit,
|
|
120
135
|
expected: branch.head,
|
|
136
|
+
signal: input.signal,
|
|
121
137
|
}, deps);
|
|
138
|
+
input.observeEffect?.({ effect: "task-reconciliation", status: "succeeded", commit, detail: "published" });
|
|
122
139
|
try {
|
|
123
|
-
await reconcileTaskMutationIndex(input.cwd, input.writes);
|
|
140
|
+
await reconcileTaskMutationIndex(input.cwd, input.writes, input.signal);
|
|
141
|
+
input.observeEffect?.({ effect: "task-index-reconciliation", status: "succeeded", commit });
|
|
124
142
|
}
|
|
125
|
-
catch {
|
|
143
|
+
catch (error) {
|
|
126
144
|
// Branch fact remains authoritative; index may be repaired by a later mutation.
|
|
145
|
+
input.observeEffect?.({ effect: "task-index-reconciliation", status: "failed", commit, error });
|
|
127
146
|
}
|
|
128
147
|
return commit;
|
|
129
148
|
}
|
|
@@ -131,6 +150,7 @@ export async function publishTaskMutationCommit(input, deps = {}) {
|
|
|
131
150
|
lastError = error;
|
|
132
151
|
}
|
|
133
152
|
}
|
|
153
|
+
input.observeEffect?.({ effect: "task-reconciliation", status: "failed", error: lastError });
|
|
134
154
|
throw lastError;
|
|
135
155
|
}
|
|
136
156
|
async function taskTreeEntries(cwd, commit) {
|
|
@@ -167,23 +187,139 @@ export async function refHead(cwd, ref) {
|
|
|
167
187
|
return (await git(cwd, ["rev-parse", ref])).trim();
|
|
168
188
|
}
|
|
169
189
|
export async function taskContractAtCommit(cwd, commit, id) {
|
|
170
|
-
const
|
|
190
|
+
const coordinate = parseTaskCoordinate(id).coordinate;
|
|
191
|
+
const sourcePath = `.keiyaku/tasks/${coordinate}.md`;
|
|
171
192
|
const result = await gitResult(cwd, ["show", `${commit}:${sourcePath}`]);
|
|
172
193
|
if (result.status !== 0)
|
|
173
194
|
return undefined;
|
|
174
195
|
return parseTaskDocument({ path: sourcePath, text: result.stdout }).contractId;
|
|
175
196
|
}
|
|
197
|
+
async function taskDocumentAtCommit(cwd, commit, taskId) {
|
|
198
|
+
let coordinate;
|
|
199
|
+
try {
|
|
200
|
+
coordinate = parseTaskCoordinate(taskId);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
throw new FlowError("INTERNAL_STATE", `bind base ${commit} names noncanonical task id ${taskId}`);
|
|
204
|
+
}
|
|
205
|
+
const sourcePath = `.keiyaku/tasks/${coordinate.coordinate}.md`;
|
|
206
|
+
const result = await gitResult(cwd, ["show", `${commit}:${sourcePath}`]);
|
|
207
|
+
if (result.status !== 0 || result.error) {
|
|
208
|
+
throw new FlowError("INTERNAL_STATE", `bind base ${commit} is missing task ${taskId}`, { cause: result.error });
|
|
209
|
+
}
|
|
210
|
+
const document = parseTaskDocument({ path: sourcePath, text: result.stdout });
|
|
211
|
+
if (document.id !== coordinate.localId) {
|
|
212
|
+
throw new FlowError("INTERNAL_STATE", `bind base ${commit} task path ${taskId} contains ${document.id}`);
|
|
213
|
+
}
|
|
214
|
+
return document;
|
|
215
|
+
}
|
|
216
|
+
async function readTaskAdmissionEvidence(input, readDocument = taskDocumentAtCommit) {
|
|
217
|
+
if (input.selectedTaskIds.length === 0 || new Set(input.selectedTaskIds).size !== input.selectedTaskIds.length) {
|
|
218
|
+
throw new FlowError("INTERNAL_STATE", `task-selected bind ${input.bindObjectId} must name unique selected tasks`);
|
|
219
|
+
}
|
|
220
|
+
const cache = new Map();
|
|
221
|
+
const read = async (taskId) => {
|
|
222
|
+
const cached = cache.get(taskId);
|
|
223
|
+
if (cached)
|
|
224
|
+
return cached;
|
|
225
|
+
const document = await readDocument(input.cwd, input.base, taskId);
|
|
226
|
+
cache.set(taskId, document);
|
|
227
|
+
return document;
|
|
228
|
+
};
|
|
229
|
+
const selected = [];
|
|
230
|
+
for (const taskId of input.selectedTaskIds) {
|
|
231
|
+
const document = await read(taskId);
|
|
232
|
+
if (document.contractId !== input.contract || document.state !== "open") {
|
|
233
|
+
throw new FlowError("INTERNAL_STATE", `bind base ${input.base} task ${taskId} does not carry open linkage to ${input.contract}`);
|
|
234
|
+
}
|
|
235
|
+
const needs = [];
|
|
236
|
+
for (const need of document.needs) {
|
|
237
|
+
const prerequisite = await read(need);
|
|
238
|
+
if (prerequisite.state !== "done" && prerequisite.state !== "drop") {
|
|
239
|
+
throw new FlowError("INTERNAL_STATE", `bind base ${input.base} task ${taskId} has unsatisfied need ${need}`);
|
|
240
|
+
}
|
|
241
|
+
needs.push({ taskId: need, state: prerequisite.state });
|
|
242
|
+
}
|
|
243
|
+
selected.push({ taskId, needs });
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
bindObjectId: input.bindObjectId,
|
|
247
|
+
base: input.base,
|
|
248
|
+
selected,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/** Immutable current bind-time task linkage and needs evidence; never reads the current board. */
|
|
252
|
+
export async function readTaskAdmissionEvidenceAtCommit(input) {
|
|
253
|
+
return await readTaskAdmissionEvidence(input);
|
|
254
|
+
}
|
|
255
|
+
/** Read every bind-time task document in one lifecycle observation through one Git process. */
|
|
256
|
+
export function openTaskAdmissionEvidenceReader(cwd) {
|
|
257
|
+
const batch = openGitStreamingBatch(cwd);
|
|
258
|
+
const documents = new Map();
|
|
259
|
+
let transportFailed = false;
|
|
260
|
+
const readDocument = async (_cwd, commit, taskId) => {
|
|
261
|
+
let coordinate;
|
|
262
|
+
try {
|
|
263
|
+
coordinate = parseTaskCoordinate(taskId);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
throw new FlowError("INTERNAL_STATE", `bind base ${commit} names noncanonical task id ${taskId}`);
|
|
267
|
+
}
|
|
268
|
+
const sourcePath = `.keiyaku/tasks/${coordinate.coordinate}.md`;
|
|
269
|
+
const expression = `${commit}:${sourcePath}`;
|
|
270
|
+
const cached = documents.get(expression);
|
|
271
|
+
if (cached)
|
|
272
|
+
return cached;
|
|
273
|
+
let result;
|
|
274
|
+
try {
|
|
275
|
+
result = await batch.request(expression);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
transportFailed = true;
|
|
279
|
+
throw new FlowError("INTERNAL_STATE", `cannot read bind base ${commit} task ${taskId}`, { cause: error });
|
|
280
|
+
}
|
|
281
|
+
if (result.kind !== "object" || result.record.type !== "blob") {
|
|
282
|
+
throw new FlowError("INTERNAL_STATE", `bind base ${commit} is missing task ${taskId}`);
|
|
283
|
+
}
|
|
284
|
+
const document = parseTaskDocument({ path: sourcePath, text: result.record.content.toString("utf8") });
|
|
285
|
+
if (document.id !== coordinate.localId) {
|
|
286
|
+
throw new FlowError("INTERNAL_STATE", `bind base ${commit} task path ${taskId} contains ${document.id}`);
|
|
287
|
+
}
|
|
288
|
+
documents.set(expression, document);
|
|
289
|
+
return document;
|
|
290
|
+
};
|
|
291
|
+
return {
|
|
292
|
+
read: async (input) => await readTaskAdmissionEvidence(input, readDocument),
|
|
293
|
+
close: async () => {
|
|
294
|
+
try {
|
|
295
|
+
const fact = await batch.close();
|
|
296
|
+
if (!transportFailed && (fact.timedOut || fact.code !== 0 || fact.signal !== null)) {
|
|
297
|
+
const detail = fact.stderr.toString("utf8").trim() || `exit ${String(fact.code)}`;
|
|
298
|
+
throw new FlowError("INTERNAL_STATE", `git cat-file --batch failed while reading task evidence: ${detail}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
if (transportFailed)
|
|
303
|
+
return;
|
|
304
|
+
if (error instanceof FlowError)
|
|
305
|
+
throw error;
|
|
306
|
+
throw new FlowError("INTERNAL_STATE", "cannot close task evidence batch reader", { cause: error });
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
};
|
|
310
|
+
}
|
|
176
311
|
/** Bound task ids and their complete parent ancestry from the immutable bind base. */
|
|
177
312
|
export async function taskSettlementOwnershipAtCommit(cwd, commit, contractId) {
|
|
178
313
|
const documents = new Map();
|
|
179
314
|
for (const entry of await taskTreeEntries(cwd, commit)) {
|
|
180
315
|
const text = await git(cwd, ["cat-file", "blob", entry.oid]);
|
|
181
316
|
const document = parseTaskDocument({ path: entry.path, text });
|
|
182
|
-
|
|
317
|
+
const coordinate = parseTaskCoordinate(entry.path.slice(".keiyaku/tasks/".length, -3)).coordinate;
|
|
318
|
+
documents.set(coordinate, document);
|
|
183
319
|
}
|
|
184
|
-
const taskIds = [...documents
|
|
185
|
-
.filter((document) => document.contractId === contractId)
|
|
186
|
-
.map((
|
|
320
|
+
const taskIds = [...documents]
|
|
321
|
+
.filter(([, document]) => document.contractId === contractId)
|
|
322
|
+
.map(([coordinate]) => coordinate)
|
|
187
323
|
.sort();
|
|
188
324
|
const ownedIds = new Set();
|
|
189
325
|
for (const taskId of taskIds) {
|
|
@@ -208,7 +344,7 @@ export async function updateBranch(input, deps) {
|
|
|
208
344
|
if (deps.updateBranch)
|
|
209
345
|
await deps.updateBranch(input);
|
|
210
346
|
else
|
|
211
|
-
await git(input.cwd, ["update-ref", input.ref, input.next, input.expected]);
|
|
347
|
+
await git(input.cwd, ["update-ref", input.ref, input.next, input.expected], undefined, undefined, input.signal);
|
|
212
348
|
}
|
|
213
349
|
export async function publishForwardTaskBoardCommit(input, deps) {
|
|
214
350
|
let lastError;
|
|
@@ -229,71 +365,6 @@ export async function publishForwardTaskBoardCommit(input, deps) {
|
|
|
229
365
|
}
|
|
230
366
|
throw lastError;
|
|
231
367
|
}
|
|
232
|
-
async function treeWithoutTasks(cwd, commit, gitDir) {
|
|
233
|
-
const temporaryDirectory = fs.mkdtempSync(path.join(gitDir, "keiyaku-landing-index-"));
|
|
234
|
-
const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
|
|
235
|
-
try {
|
|
236
|
-
await git(cwd, ["read-tree", commit], undefined, env);
|
|
237
|
-
await git(cwd, ["rm", "-r", "--cached", "-f", "--ignore-unmatch", "--", ".keiyaku/tasks"], undefined, env);
|
|
238
|
-
return (await git(cwd, ["write-tree"], undefined, env)).trim();
|
|
239
|
-
}
|
|
240
|
-
finally {
|
|
241
|
-
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
export async function composeLandingTree(input) {
|
|
245
|
-
const baseTree = await treeWithoutTasks(input.cwd, input.base, input.gitDir);
|
|
246
|
-
const targetTree = await treeWithoutTasks(input.cwd, input.target, input.gitDir);
|
|
247
|
-
const deliveryTree = await treeWithoutTasks(input.cwd, input.delivery, input.gitDir);
|
|
248
|
-
let mergedTree;
|
|
249
|
-
try {
|
|
250
|
-
mergedTree = (await git(input.cwd, [
|
|
251
|
-
"merge-tree",
|
|
252
|
-
"--write-tree",
|
|
253
|
-
"--merge-base",
|
|
254
|
-
baseTree,
|
|
255
|
-
targetTree,
|
|
256
|
-
deliveryTree,
|
|
257
|
-
])).trim();
|
|
258
|
-
}
|
|
259
|
-
catch (error) {
|
|
260
|
-
throw new FlowError("CLOSE_MERGE_CONFLICT", "claim merge conflict outside task board", { cause: error });
|
|
261
|
-
}
|
|
262
|
-
const temporaryDirectory = fs.mkdtempSync(path.join(input.gitDir, "keiyaku-landing-index-"));
|
|
263
|
-
const env = { GIT_INDEX_FILE: path.join(temporaryDirectory, "index") };
|
|
264
|
-
try {
|
|
265
|
-
await git(input.cwd, ["read-tree", mergedTree], undefined, env);
|
|
266
|
-
// merge-tree deliberately excluded task paths. Restore the target board as
|
|
267
|
-
// the inherited baseline, then overlay only settlement-authorized writes.
|
|
268
|
-
for (const entry of await taskTreeEntries(input.cwd, input.target)) {
|
|
269
|
-
await git(input.cwd, [
|
|
270
|
-
"update-index",
|
|
271
|
-
"--add",
|
|
272
|
-
"--cacheinfo",
|
|
273
|
-
`${entry.mode},${entry.oid},${entry.path}`,
|
|
274
|
-
], undefined, env);
|
|
275
|
-
}
|
|
276
|
-
for (const source of input.sources) {
|
|
277
|
-
const oid = (await git(input.cwd, ["hash-object", "-w", "--stdin"], source.text)).trim();
|
|
278
|
-
await git(input.cwd, [
|
|
279
|
-
"update-index",
|
|
280
|
-
"--add",
|
|
281
|
-
"--cacheinfo",
|
|
282
|
-
`100644,${oid},${boardPath(input.root, source)}`,
|
|
283
|
-
], undefined, env);
|
|
284
|
-
}
|
|
285
|
-
return (await git(input.cwd, ["write-tree"], undefined, env)).trim();
|
|
286
|
-
}
|
|
287
|
-
finally {
|
|
288
|
-
fs.rmSync(temporaryDirectory, { recursive: true, force: true });
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
function landingCommitMessage(contractId, targetBranch) {
|
|
292
|
-
return `keiyaku: claim ${contractId}\n\nMerge contract ${contractId} into ${targetBranch}.\n`;
|
|
293
|
-
}
|
|
294
|
-
async function isAncestor(cwd, ancestor, descendant) {
|
|
295
|
-
return (await gitResult(cwd, ["merge-base", "--is-ancestor", ancestor, descendant])).status === 0;
|
|
296
|
-
}
|
|
297
368
|
export async function taskSourcesAtCommit(cwd, root, commit, paths) {
|
|
298
369
|
const sources = [];
|
|
299
370
|
for (const sourcePath of paths) {
|
|
@@ -304,95 +375,9 @@ export async function taskSourcesAtCommit(cwd, root, commit, paths) {
|
|
|
304
375
|
}
|
|
305
376
|
return sources;
|
|
306
377
|
}
|
|
307
|
-
/**
|
|
308
|
-
* Find the durable landing receipt on the target's first-parent history.
|
|
309
|
-
* Equivalent trees and lookalike messages are insufficient: the commit must
|
|
310
|
-
* have our parent shape, settle every bind-base-owned task, and exactly match
|
|
311
|
-
* a recomposed landing tree over its own first parent.
|
|
312
|
-
*/
|
|
313
|
-
export async function findAcceptedLandingCommit(input) {
|
|
314
|
-
const commits = (await git(input.cwd, ["rev-list", "--first-parent", input.target, `^${input.base}`]))
|
|
315
|
-
.trim()
|
|
316
|
-
.split("\n")
|
|
317
|
-
.filter(Boolean);
|
|
318
|
-
for (const commit of commits) {
|
|
319
|
-
const message = await git(input.cwd, ["show", "-s", "--format=%B", commit]);
|
|
320
|
-
if (message.trimEnd() !== landingCommitMessage(input.contractId, input.targetBranch).trimEnd())
|
|
321
|
-
continue;
|
|
322
|
-
const record = (await git(input.cwd, ["rev-list", "--parents", "-n", "1", commit])).trim().split(/\s+/);
|
|
323
|
-
const parents = record.slice(1);
|
|
324
|
-
const parentShapeAccepted = parents.length === 2
|
|
325
|
-
? parents[1] === input.delivery
|
|
326
|
-
: parents.length === 1 && await isAncestor(input.cwd, input.delivery, parents[0]);
|
|
327
|
-
if (!parentShapeAccepted)
|
|
328
|
-
continue;
|
|
329
|
-
const sources = await taskSourcesAtCommit(input.cwd, input.root, commit, input.ownedPaths);
|
|
330
|
-
if (!sources)
|
|
331
|
-
continue;
|
|
332
|
-
let settled = false;
|
|
333
|
-
try {
|
|
334
|
-
const sourceByPath = new Map(sources.map((source) => [boardPath(input.root, source), source]));
|
|
335
|
-
settled = input.settledPaths.every((settledPath) => {
|
|
336
|
-
const source = sourceByPath.get(settledPath);
|
|
337
|
-
if (!source)
|
|
338
|
-
return false;
|
|
339
|
-
const document = parseTaskDocument(source);
|
|
340
|
-
return document.state === "done" && document.contractId === undefined;
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
catch {
|
|
344
|
-
// A malformed lookalike is not this operation's durable receipt.
|
|
345
|
-
}
|
|
346
|
-
if (!settled)
|
|
347
|
-
continue;
|
|
348
|
-
const expectedTree = await composeLandingTree({
|
|
349
|
-
cwd: input.cwd,
|
|
350
|
-
gitDir: input.gitDir,
|
|
351
|
-
root: input.root,
|
|
352
|
-
base: input.base,
|
|
353
|
-
target: parents[0],
|
|
354
|
-
delivery: input.delivery,
|
|
355
|
-
sources,
|
|
356
|
-
});
|
|
357
|
-
const actualTree = (await git(input.cwd, ["show", "-s", "--format=%T", commit])).trim();
|
|
358
|
-
if (actualTree === expectedTree)
|
|
359
|
-
return commit;
|
|
360
|
-
}
|
|
361
|
-
return undefined;
|
|
362
|
-
}
|
|
363
|
-
export async function containsCommit(cwd, ancestor, descendant) {
|
|
364
|
-
return await isAncestor(cwd, ancestor, descendant);
|
|
365
|
-
}
|
|
366
|
-
/** Create the landing commit over a previously composed candidate tree. */
|
|
367
|
-
export async function createLandingCommit(input) {
|
|
368
|
-
// Once delivery is already reachable from target, a single-parent landing
|
|
369
|
-
// records this claim without fabricating a redundant merge parent.
|
|
370
|
-
const parents = input.target === input.delivery || await isAncestor(input.cwd, input.delivery, input.target)
|
|
371
|
-
? ["-p", input.target]
|
|
372
|
-
: ["-p", input.target, "-p", input.delivery];
|
|
373
|
-
return (await git(input.cwd, [
|
|
374
|
-
"commit-tree",
|
|
375
|
-
input.tree,
|
|
376
|
-
...parents,
|
|
377
|
-
"-m",
|
|
378
|
-
`keiyaku: claim ${input.contractId}`,
|
|
379
|
-
"-m",
|
|
380
|
-
`Merge contract ${input.contractId} into ${input.targetBranch}.`,
|
|
381
|
-
])).trim();
|
|
382
|
-
}
|
|
383
|
-
export async function updateTargetRef(cwd, ref, next, expected) {
|
|
384
|
-
await git(cwd, ["update-ref", ref, next, expected]);
|
|
385
|
-
}
|
|
386
|
-
export function sameTaskSourceBytes(root, left, right) {
|
|
387
|
-
const snapshot = (sources) => new Map(sources.map((source) => [boardPath(root, source), source.text]));
|
|
388
|
-
const leftSnapshot = snapshot(left);
|
|
389
|
-
const rightSnapshot = snapshot(right);
|
|
390
|
-
return leftSnapshot.size === rightSnapshot.size
|
|
391
|
-
&& [...leftSnapshot].every(([sourcePath, sourceText]) => rightSnapshot.get(sourcePath) === sourceText);
|
|
392
|
-
}
|
|
393
378
|
export function taskWritesFromSources(sources) {
|
|
394
379
|
return sources.map((source) => ({
|
|
395
|
-
id:
|
|
380
|
+
id: parseTaskCoordinate(source.path.replaceAll("\\", "/").split("/.keiyaku/tasks/").at(-1).slice(0, -3)).coordinate,
|
|
396
381
|
text: source.text,
|
|
397
382
|
}));
|
|
398
383
|
}
|
|
@@ -10,12 +10,16 @@ export function requireHealthySettlementBoard(board) {
|
|
|
10
10
|
function settlementDocument(task, contractId, updatedAt) {
|
|
11
11
|
return {
|
|
12
12
|
id: task.id,
|
|
13
|
+
namespace: task.namespace,
|
|
14
|
+
coordinate: task.coordinate,
|
|
13
15
|
title: task.title,
|
|
14
16
|
state: task.state,
|
|
15
17
|
pri: task.pri,
|
|
16
18
|
needs: [...task.needs],
|
|
17
19
|
parent: task.parent,
|
|
18
20
|
from: [...task.from],
|
|
21
|
+
supersedes: [...task.supersedes],
|
|
22
|
+
relatesTo: [...task.relatesTo],
|
|
19
23
|
createdAt: task.createdAt,
|
|
20
24
|
updatedAt: advancedTaskUpdatedAt(task, updatedAt),
|
|
21
25
|
creator: task.creator,
|
|
@@ -35,22 +39,22 @@ export function reopenedTaskDocument(task, updatedAt) {
|
|
|
35
39
|
}
|
|
36
40
|
function validateReadyForBind(board, task) {
|
|
37
41
|
if (task.state === "in_progress") {
|
|
38
|
-
throw new FlowError("INVALID_TARGET", `task ${task.
|
|
42
|
+
throw new FlowError("INVALID_TARGET", `task ${task.coordinate} is in_progress; stop it before bind`);
|
|
39
43
|
}
|
|
40
44
|
if (task.state !== "open") {
|
|
41
|
-
throw new FlowError("INVALID_TARGET", `task ${task.
|
|
45
|
+
throw new FlowError("INVALID_TARGET", `task ${task.coordinate} is ${task.state}, not ready for bind`);
|
|
42
46
|
}
|
|
43
47
|
const unsatisfied = task.needs
|
|
44
48
|
.filter((need) => {
|
|
45
49
|
const dependency = board.byId.get(need);
|
|
46
|
-
return dependency.state === "open" || dependency.state === "in_progress";
|
|
50
|
+
return dependency.state === "open" || dependency.state === "in_progress" || dependency.state === "on_hold";
|
|
47
51
|
})
|
|
48
52
|
.sort();
|
|
49
53
|
if (unsatisfied.length > 0) {
|
|
50
|
-
throw new FlowError("INVALID_TARGET", `task ${task.
|
|
54
|
+
throw new FlowError("INVALID_TARGET", `task ${task.coordinate} is blocked by: ${unsatisfied.join(", ")}`);
|
|
51
55
|
}
|
|
52
56
|
if (task.contractId !== undefined) {
|
|
53
|
-
throw new FlowError("INVALID_TARGET", `task ${task.
|
|
57
|
+
throw new FlowError("INVALID_TARGET", `task ${task.coordinate} is already bound to contract ${task.contractId}`);
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
export function selectReadyTasksForBind(board, taskIds) {
|
|
@@ -64,17 +68,17 @@ export function selectReadyTasksForBind(board, taskIds) {
|
|
|
64
68
|
const tasks = [];
|
|
65
69
|
for (const rootId of rootIds) {
|
|
66
70
|
const root = board.byId.get(rootId);
|
|
67
|
-
if (!admitted.has(root.
|
|
68
|
-
admitted.add(root.
|
|
71
|
+
if (!admitted.has(root.coordinate)) {
|
|
72
|
+
admitted.add(root.coordinate);
|
|
69
73
|
tasks.push(root);
|
|
70
74
|
}
|
|
71
75
|
for (const candidate of board.tasks) {
|
|
72
|
-
if (candidate.parent === null || admitted.has(candidate.
|
|
76
|
+
if (candidate.parent === null || admitted.has(candidate.coordinate))
|
|
73
77
|
continue;
|
|
74
78
|
if (candidate.state === "done" || candidate.state === "drop")
|
|
75
79
|
continue;
|
|
76
80
|
if (isDescendantOf(board, candidate, rootId)) {
|
|
77
|
-
admitted.add(candidate.
|
|
81
|
+
admitted.add(candidate.coordinate);
|
|
78
82
|
tasks.push(candidate);
|
|
79
83
|
}
|
|
80
84
|
}
|
|
@@ -96,7 +100,7 @@ export function tasksForContract(board, contractId) {
|
|
|
96
100
|
requireHealthySettlementBoard(board);
|
|
97
101
|
return board.tasks
|
|
98
102
|
.filter((task) => task.contractId === contractId)
|
|
99
|
-
.sort((left, right) => left.
|
|
103
|
+
.sort((left, right) => left.coordinate.localeCompare(right.coordinate));
|
|
100
104
|
}
|
|
101
105
|
export function doneTaskReplacements(board, contractId, taskIds, updatedAt) {
|
|
102
106
|
return taskIds.flatMap((taskId) => {
|
|
@@ -104,7 +108,7 @@ export function doneTaskReplacements(board, contractId, taskIds, updatedAt) {
|
|
|
104
108
|
if (task.state === "done" && task.contractId === undefined)
|
|
105
109
|
return [];
|
|
106
110
|
if (task.contractId !== contractId) {
|
|
107
|
-
throw new FlowError("INTERNAL_STATE", `task ${task.
|
|
111
|
+
throw new FlowError("INTERNAL_STATE", `task ${task.coordinate} no longer points to contract ${contractId} while settling its immutable bind-base ownership`);
|
|
108
112
|
}
|
|
109
113
|
const settled = settlementDocument(task, undefined, updatedAt);
|
|
110
114
|
settled.state = "done";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildTaskBoardFromDocuments } from "./board.js";
|
|
2
2
|
import { parseTaskDocument } from "./document.js";
|
|
3
3
|
import { invalid } from "./validation.js";
|
|
4
|
+
import { taskCoordinateFromSourcePath } from "./coordinate.js";
|
|
4
5
|
function compareText(left, right) {
|
|
5
6
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
6
7
|
}
|
|
@@ -10,18 +11,19 @@ function diseaseSort(left, right) {
|
|
|
10
11
|
|| compareText(left.id ?? "", right.id ?? "")
|
|
11
12
|
|| compareText(left.message, right.message);
|
|
12
13
|
}
|
|
13
|
-
function sourceBasename(sourcePath) {
|
|
14
|
-
return sourcePath.split(/[\\/]/).at(-1) ?? "";
|
|
15
|
-
}
|
|
16
14
|
function taskDocument(parsed) {
|
|
17
15
|
return {
|
|
18
16
|
id: parsed.id,
|
|
17
|
+
namespace: parsed.namespace ?? null,
|
|
18
|
+
coordinate: parsed.coordinate ?? parsed.id,
|
|
19
19
|
title: parsed.title,
|
|
20
20
|
state: parsed.state,
|
|
21
21
|
pri: parsed.pri,
|
|
22
22
|
needs: [...parsed.needs],
|
|
23
23
|
parent: parsed.parent,
|
|
24
24
|
from: [...parsed.from],
|
|
25
|
+
supersedes: [...parsed.supersedes],
|
|
26
|
+
relatesTo: [...parsed.relatesTo],
|
|
25
27
|
createdAt: parsed.createdAt,
|
|
26
28
|
...(parsed.updatedAt === undefined ? {} : { updatedAt: parsed.updatedAt }),
|
|
27
29
|
creator: parsed.creator,
|
|
@@ -34,36 +36,52 @@ export function buildTaskSourceBoard(sources) {
|
|
|
34
36
|
const sourceDiseases = [];
|
|
35
37
|
const authoritative = [];
|
|
36
38
|
for (const source of [...sources].sort((a, b) => compareText(a.path, b.path))) {
|
|
39
|
+
let sourceCoordinate;
|
|
37
40
|
try {
|
|
41
|
+
const coordinate = taskCoordinateFromSourcePath(source.path);
|
|
42
|
+
sourceCoordinate = coordinate.coordinate;
|
|
38
43
|
const parsed = parseTaskDocument(source);
|
|
39
|
-
if (
|
|
44
|
+
if (coordinate.localId !== parsed.id) {
|
|
40
45
|
invalid(`task source basename must be ${parsed.id}.md`);
|
|
41
46
|
}
|
|
42
|
-
|
|
47
|
+
for (const [field, relations] of [
|
|
48
|
+
["needs", parsed.needs],
|
|
49
|
+
["from", parsed.from],
|
|
50
|
+
["supersedes", parsed.supersedes],
|
|
51
|
+
["relatesTo", parsed.relatesTo],
|
|
52
|
+
]) {
|
|
53
|
+
if (relations.includes(coordinate.coordinate))
|
|
54
|
+
invalid(`task field '${field}' must not reference itself`);
|
|
55
|
+
}
|
|
56
|
+
if (parsed.parent === coordinate.coordinate)
|
|
57
|
+
invalid("task field 'parent' must not reference itself");
|
|
58
|
+
authoritative.push({ ...parsed, namespace: coordinate.namespace, coordinate: coordinate.coordinate });
|
|
43
59
|
}
|
|
44
60
|
catch (error) {
|
|
45
61
|
sourceDiseases.push({
|
|
46
62
|
code: "invalid_document",
|
|
47
63
|
path: source.path,
|
|
48
|
-
message: error instanceof Error ? error.message : String(error)
|
|
64
|
+
message: `task ${sourceCoordinate ?? source.path} invalid document: ${error instanceof Error ? error.message : String(error)}`,
|
|
65
|
+
...(sourceCoordinate === undefined ? {} : { id: sourceCoordinate }),
|
|
49
66
|
});
|
|
50
67
|
}
|
|
51
68
|
}
|
|
52
69
|
const counts = new Map();
|
|
53
70
|
for (const task of authoritative)
|
|
54
|
-
counts.set(task.
|
|
71
|
+
counts.set(task.coordinate, (counts.get(task.coordinate) ?? 0) + 1);
|
|
55
72
|
const candidates = new Map();
|
|
56
73
|
for (const task of authoritative) {
|
|
57
|
-
|
|
74
|
+
const coordinate = task.coordinate;
|
|
75
|
+
if (counts.get(coordinate) > 1) {
|
|
58
76
|
sourceDiseases.push({
|
|
59
77
|
code: "duplicate_id",
|
|
60
78
|
path: task.source.path,
|
|
61
|
-
message: `duplicate authoritative task
|
|
62
|
-
id:
|
|
79
|
+
message: `duplicate authoritative task coordinate ${coordinate}`,
|
|
80
|
+
id: coordinate,
|
|
63
81
|
});
|
|
64
82
|
}
|
|
65
83
|
else {
|
|
66
|
-
candidates.set(
|
|
84
|
+
candidates.set(coordinate, task);
|
|
67
85
|
}
|
|
68
86
|
}
|
|
69
87
|
const domainBoard = buildTaskBoardFromDocuments([...candidates.values()].map(taskDocument));
|
|
@@ -73,12 +91,12 @@ export function buildTaskSourceBoard(sources) {
|
|
|
73
91
|
}));
|
|
74
92
|
const tasks = domainBoard.tasks.map((task) => ({
|
|
75
93
|
...task,
|
|
76
|
-
path: candidates.get(task.
|
|
94
|
+
path: candidates.get(task.coordinate).source.path,
|
|
77
95
|
}));
|
|
78
96
|
return {
|
|
79
97
|
tasks,
|
|
80
98
|
diseases: [...sourceDiseases, ...graphDiseases].sort(diseaseSort),
|
|
81
|
-
byId: new Map(tasks.map((task) => [task.
|
|
99
|
+
byId: new Map(tasks.map((task) => [task.coordinate, task])),
|
|
82
100
|
};
|
|
83
101
|
}
|
|
84
102
|
/** Compatibility name for source-backed board consumers. */
|