@astrosheep/keiyaku 2.9.9 → 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 +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,14 +1,19 @@
|
|
|
1
1
|
import { availableParallelism } from "node:os";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
2
4
|
import { loadKeiyakuSettings } from "../../config/settings/loader.js";
|
|
3
5
|
import { observeGitRepository } from "../../git/branches.js";
|
|
6
|
+
import { currentLifecycleState } from "../repository-ledger/read-model.js";
|
|
7
|
+
import { parseCarrierIdentity } from "../contract-carrier.js";
|
|
8
|
+
import { KEIYAKU_FILE } from "../../keiyaku.js";
|
|
9
|
+
import { lookupCurrentContract, } from "../repository-ledger/read-model.js";
|
|
4
10
|
import { readExactProjectionStatus, readOperationalProjectionStatusBoard, } from "../projection/index.js";
|
|
5
11
|
import { deriveQueueReadModel, queueMembership, } from "../settlement/index.js";
|
|
6
|
-
import { findOpenArc, getMaxArc, receiptsAfterFence } from "../seal.js";
|
|
7
12
|
import { openTaskStore, readTaskBoard, taskRepositoryFromStore, taskUpdatedAt, } from "../task/index.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
13
|
+
import { replayBlockingFact, } from "./lifecycle.js";
|
|
14
|
+
import { findRegisteredContractWorktreePathFromModel, stableRepoRoot } from "../worktree-path.js";
|
|
15
|
+
import { evaluateBaseDrift, resolveDefaultBranchFromSettings, } from "./drift.js";
|
|
16
|
+
import { readRepositoryLifecycleSummaries as readStatusLifecycle } from "../repository-ledger/read-model.js";
|
|
12
17
|
// Git object reads benefit from overlap, but a hard cap avoids process and disk contention.
|
|
13
18
|
const STATUS_DRIFT_MAX_CONCURRENCY = 8;
|
|
14
19
|
const STATUS_DRIFT_CONCURRENCY = Math.min(STATUS_DRIFT_MAX_CONCURRENCY, availableParallelism());
|
|
@@ -24,10 +29,11 @@ function timestampAge(timestamp, now) {
|
|
|
24
29
|
return Number.isFinite(value) ? Math.max(0, now - value) : 0;
|
|
25
30
|
}
|
|
26
31
|
export function deriveTaskStatusSummary(board, now) {
|
|
27
|
-
const eligible = board.tasks.filter((task) => !task.contractId && (task.state === "open" || task.state === "in_progress"));
|
|
32
|
+
const eligible = board.tasks.filter((task) => !task.contractId && (task.state === "open" || task.state === "in_progress" || task.state === "on_hold"));
|
|
28
33
|
const inProgress = eligible.filter((task) => task.state === "in_progress").length;
|
|
34
|
+
const onHold = eligible.filter((task) => task.state === "on_hold").length;
|
|
29
35
|
const topTasks = eligible
|
|
30
|
-
.filter((task) => task.state === "in_progress")
|
|
36
|
+
.filter((task) => task.state === "in_progress" || task.state === "on_hold")
|
|
31
37
|
.sort(compareTaskStatusRows)
|
|
32
38
|
.map((task) => {
|
|
33
39
|
const updatedAt = taskUpdatedAt(task);
|
|
@@ -48,6 +54,7 @@ export function deriveTaskStatusSummary(board, now) {
|
|
|
48
54
|
return {
|
|
49
55
|
total: eligible.length,
|
|
50
56
|
inProgress,
|
|
57
|
+
onHold,
|
|
51
58
|
ready,
|
|
52
59
|
blocked,
|
|
53
60
|
omitted: ready + blocked,
|
|
@@ -64,6 +71,7 @@ async function readTaskStatusSummary(cwd, now) {
|
|
|
64
71
|
return {
|
|
65
72
|
total: 0,
|
|
66
73
|
inProgress: 0,
|
|
74
|
+
onHold: 0,
|
|
67
75
|
ready: 0,
|
|
68
76
|
blocked: 0,
|
|
69
77
|
omitted: 0,
|
|
@@ -77,10 +85,7 @@ function isQueueHeadStall(input) {
|
|
|
77
85
|
return (input.queuePosition === 1 &&
|
|
78
86
|
input.queueLength > 1 &&
|
|
79
87
|
input.state.state === "petitioned" &&
|
|
80
|
-
!
|
|
81
|
-
}
|
|
82
|
-
function findBindEntry(entries) {
|
|
83
|
-
return entries.find((entry) => entry.kind === "bind");
|
|
88
|
+
!input.hasVerdict);
|
|
84
89
|
}
|
|
85
90
|
function contractSlugFromId(id) {
|
|
86
91
|
const dot = id.indexOf(".");
|
|
@@ -88,44 +93,25 @@ function contractSlugFromId(id) {
|
|
|
88
93
|
return id.slice(0, dot);
|
|
89
94
|
return id;
|
|
90
95
|
}
|
|
91
|
-
function deriveCurrentArc(
|
|
92
|
-
// No explicit arc has ever existed: derive a0 from bind objective.
|
|
93
|
-
if (getMaxArc(entries) === 0) {
|
|
94
|
-
const a0Open = lifecycle.state === "bound" || lifecycle.state === "active";
|
|
95
|
-
return {
|
|
96
|
-
number: 0,
|
|
97
|
-
objective: bindObjective ?? "",
|
|
98
|
-
state: a0Open ? "open" : "closed",
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
const arcObjectives = new Map();
|
|
102
|
-
let currentArc = null;
|
|
103
|
-
for (const entry of entries) {
|
|
104
|
-
if (entry.kind === "arc") {
|
|
105
|
-
arcObjectives.set(entry.data.arc, entry.data.objective);
|
|
106
|
-
currentArc = {
|
|
107
|
-
number: entry.data.arc,
|
|
108
|
-
objective: entry.data.objective,
|
|
109
|
-
state: "open",
|
|
110
|
-
};
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (entry.kind === "seal" && entry.data.arc > 0) {
|
|
114
|
-
currentArc = {
|
|
115
|
-
number: entry.data.arc,
|
|
116
|
-
objective: arcObjectives.get(entry.data.arc) ?? bindObjective ?? "",
|
|
117
|
-
state: "closed",
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (currentArc)
|
|
122
|
-
return currentArc;
|
|
96
|
+
function deriveCurrentArc(model) {
|
|
123
97
|
return {
|
|
124
|
-
number:
|
|
125
|
-
objective:
|
|
126
|
-
state: "closed",
|
|
98
|
+
number: model.intent.arc,
|
|
99
|
+
objective: model.intent.objective,
|
|
100
|
+
state: model.phase === "active" ? "open" : "closed",
|
|
127
101
|
};
|
|
128
102
|
}
|
|
103
|
+
function bindTitle(model) {
|
|
104
|
+
const bind = model.history.find(({ entry }) => entry.kind === "bind")?.entry;
|
|
105
|
+
return bind?.kind === "bind" ? bind.data.title : model.intent.title;
|
|
106
|
+
}
|
|
107
|
+
function lifecycleDiseaseReason(read, contract) {
|
|
108
|
+
return read.diagnostics.filter((diagnostic) => diagnostic.contract === contract).map((diagnostic) => diagnostic.detail).join("; ");
|
|
109
|
+
}
|
|
110
|
+
function bootstrapReplayBlockers(model) {
|
|
111
|
+
return model.bootstrapReplay.state === "pending"
|
|
112
|
+
? [replayBlockingFact(model.contract, "bootstrap")]
|
|
113
|
+
: [];
|
|
114
|
+
}
|
|
129
115
|
function deriveNextStep(input) {
|
|
130
116
|
if (input.state.state === "corrupt") {
|
|
131
117
|
return { kind: "inspect-log", detail: "inspect log" };
|
|
@@ -152,8 +138,8 @@ function deriveNextStep(input) {
|
|
|
152
138
|
}
|
|
153
139
|
return { kind: "arc", detail: "arc" };
|
|
154
140
|
}
|
|
155
|
-
function deriveLastActiveAge(
|
|
156
|
-
const last =
|
|
141
|
+
function deriveLastActiveAge(model, now) {
|
|
142
|
+
const last = model.history.at(-1)?.entry;
|
|
157
143
|
if (!last)
|
|
158
144
|
return {};
|
|
159
145
|
const atMs = Date.parse(last.at);
|
|
@@ -202,7 +188,6 @@ export async function readKanshiBoard(cwd, now, opts) {
|
|
|
202
188
|
: exactProjection ? { rows: [exactProjection] } : null,
|
|
203
189
|
queueLength: 0,
|
|
204
190
|
diseases: loadedSettings.diseases,
|
|
205
|
-
claimReconciliation: null,
|
|
206
191
|
targetFilter,
|
|
207
192
|
// Task and contract boards are repository organs; a non-repository cwd
|
|
208
193
|
// cannot truthfully turn their absence into a zero count.
|
|
@@ -217,109 +202,121 @@ export async function readKanshiBoard(cwd, now, opts) {
|
|
|
217
202
|
catch {
|
|
218
203
|
defaultBranch = null;
|
|
219
204
|
}
|
|
220
|
-
const
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
terminalById.set(ledger.contractId, isTerminalState(deriveContractState(ledger.entries)));
|
|
228
|
-
}
|
|
229
|
-
const queueModel = deriveQueueReadModel(readableLedgers);
|
|
205
|
+
const lifecycle = await readStatusLifecycle(cwd);
|
|
206
|
+
const stableRoot = await stableRepoRoot(cwd);
|
|
207
|
+
const terminalById = new Map(lifecycle.contracts.map((model) => [
|
|
208
|
+
model.contract,
|
|
209
|
+
model.phase === "claimed" || model.phase === "forfeited",
|
|
210
|
+
]));
|
|
211
|
+
const queueModel = deriveQueueReadModel(lifecycle);
|
|
230
212
|
const boardQueueLength = queueModel.targetQueues.reduce((sum, queue) => sum + queue.ordered.length, 0);
|
|
231
|
-
const baseDrifts = await evaluateDistinctBaseDrifts(cwd,
|
|
232
|
-
const
|
|
233
|
-
return
|
|
234
|
-
const base = latestBase(item.entries);
|
|
235
|
-
return base ? [base] : [];
|
|
236
|
-
})();
|
|
213
|
+
const baseDrifts = await evaluateDistinctBaseDrifts(cwd, lifecycle.contracts.flatMap((model) => {
|
|
214
|
+
const outcome = lookupCurrentContract(lifecycle, model.contract);
|
|
215
|
+
return outcome.status === "healthy" && !terminalById.get(model.contract) ? [model.pins.base] : [];
|
|
237
216
|
}), defaultBranch);
|
|
238
217
|
const contracts = [];
|
|
239
218
|
const commissionPlaces = new Map();
|
|
240
|
-
//
|
|
219
|
+
// Folded target pins are retained only for post-assembly filtering.
|
|
241
220
|
const derivedTargetById = new Map();
|
|
242
|
-
for (const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
baseDrift: { state: "unknown", label: "n/a (corrupt ledger)" },
|
|
256
|
-
after: [],
|
|
257
|
-
corruptReason: item.error.message,
|
|
258
|
-
bootstrapDiagnostics: [],
|
|
259
|
-
queueDiagnostics: [],
|
|
260
|
-
});
|
|
221
|
+
for (const model of lifecycle.contracts) {
|
|
222
|
+
const outcome = lookupCurrentContract(lifecycle, model.contract);
|
|
223
|
+
const diseaseReason = lifecycleDiseaseReason(lifecycle, model.contract);
|
|
224
|
+
const state = outcome.status === "healthy"
|
|
225
|
+
? currentLifecycleState(model)
|
|
226
|
+
: { state: "corrupt", at: 0, reason: diseaseReason || "isolated lifecycle disease" };
|
|
227
|
+
commissionPlaces.set(model.contract, model.resources.worktree?.place ?? null);
|
|
228
|
+
derivedTargetById.set(model.contract, model.pins.target.ref);
|
|
229
|
+
const base = model.pins.base;
|
|
230
|
+
// Terminal lifecycle facts retire their carrier and worktree projections.
|
|
231
|
+
// Do not interpret a different active workspace-here carrier as terminal
|
|
232
|
+
// replay debt.
|
|
233
|
+
if (model.phase === "claimed" || model.phase === "forfeited")
|
|
261
234
|
continue;
|
|
262
|
-
}
|
|
263
|
-
const
|
|
264
|
-
commissionPlaces.set(item.contractId, bind?.data.place ?? null);
|
|
265
|
-
derivedTargetById.set(item.contractId, commissionTarget(item.entries)?.ref);
|
|
266
|
-
const base = latestBase(item.entries);
|
|
267
|
-
const state = deriveContractState(item.entries);
|
|
268
|
-
if (isTerminalState(state))
|
|
269
|
-
continue;
|
|
270
|
-
const after = (bind?.data.after ?? []).map((id) => ({ id, terminal: terminalById.get(id) ?? false }));
|
|
271
|
-
const currentArc = deriveCurrentArc(item.entries, bind?.data.objective, state);
|
|
235
|
+
const after = model.causal.after.map((id) => ({ id, terminal: terminalById.get(id) ?? false }));
|
|
236
|
+
const currentArc = deriveCurrentArc(model);
|
|
272
237
|
const baseDrift = base
|
|
273
238
|
? baseDrifts.get(base) ?? { state: "unknown", label: "n/a (base drift unavailable)" }
|
|
274
239
|
: { state: "unknown", label: "n/a (no default branch)" };
|
|
275
240
|
const nextStep = deriveNextStep({
|
|
276
|
-
id:
|
|
241
|
+
id: model.contract,
|
|
277
242
|
state,
|
|
278
243
|
currentArc,
|
|
279
244
|
baseDrift,
|
|
280
245
|
after,
|
|
281
246
|
});
|
|
282
247
|
if (nextStep.kind === "renew" && baseDrift.state === "drift") {
|
|
283
|
-
|
|
284
|
-
let receiptCount = 0;
|
|
285
|
-
if (openArc) {
|
|
286
|
-
try {
|
|
287
|
-
receiptCount = (await receiptsAfterFence(cwd, item.entries, `keiyaku/${item.contractId}`)).length;
|
|
288
|
-
}
|
|
289
|
-
catch {
|
|
290
|
-
// A ledger-only here contract may not have a delivery ref yet.
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
const sealForecast = openArc ? `seals a${openArc.data.arc} (${receiptCount} receipts), ` : "";
|
|
294
|
-
nextStep.hint = `run: keiyaku @${item.contractId} renew — ${sealForecast}follows ${baseDrift.defaultBranch} ▲${baseDrift.behindBy}`;
|
|
248
|
+
nextStep.hint = `run: keiyaku @${model.contract} renew — follows ${baseDrift.defaultBranch} ▲${baseDrift.behindBy}`;
|
|
295
249
|
}
|
|
296
|
-
const membership = queueMembership(queueModel,
|
|
250
|
+
const membership = queueMembership(queueModel, model.contract);
|
|
297
251
|
const queuePosition = membership?.position ?? null;
|
|
298
252
|
const targetQueueLength = membership?.queueLength ?? 0;
|
|
253
|
+
const replayBlockers = bootstrapReplayBlockers(model);
|
|
254
|
+
const carrierRoot = model.resources.worktree === null
|
|
255
|
+
? stableRoot
|
|
256
|
+
: await findRegisteredContractWorktreePathFromModel(stableRoot, model);
|
|
257
|
+
if (model.resources.worktree !== null && carrierRoot === undefined) {
|
|
258
|
+
replayBlockers.unshift(replayBlockingFact(model.contract, "worktree"));
|
|
259
|
+
}
|
|
260
|
+
else if (carrierRoot !== undefined) {
|
|
261
|
+
let carrierIdentity = null;
|
|
262
|
+
try {
|
|
263
|
+
carrierIdentity = parseCarrierIdentity(await fs.readFile(path.join(carrierRoot, KEIYAKU_FILE), "utf8"));
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
carrierIdentity = null;
|
|
267
|
+
}
|
|
268
|
+
if (carrierIdentity?.contractId !== model.contract || carrierIdentity.ledgerHead !== model.observedLedgerHead) {
|
|
269
|
+
const reason = carrierIdentity === null
|
|
270
|
+
? "missing-or-invalid"
|
|
271
|
+
: carrierIdentity.contractId !== model.contract
|
|
272
|
+
? "contract-id-mismatch"
|
|
273
|
+
: "ledger-head-mismatch";
|
|
274
|
+
replayBlockers.push(replayBlockingFact(model.contract, "carrier", {
|
|
275
|
+
reason,
|
|
276
|
+
expected: { contractId: model.contract, ledgerHead: model.observedLedgerHead },
|
|
277
|
+
observed: carrierIdentity,
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
299
281
|
contracts.push({
|
|
300
|
-
id:
|
|
301
|
-
slug: contractSlugFromId(
|
|
302
|
-
title:
|
|
282
|
+
id: model.contract,
|
|
283
|
+
slug: contractSlugFromId(model.contract),
|
|
284
|
+
title: bindTitle(model),
|
|
303
285
|
state,
|
|
304
286
|
queuePosition,
|
|
305
287
|
queueHeadStalled: isQueueHeadStall({
|
|
306
|
-
entries: item.entries,
|
|
307
288
|
queuePosition,
|
|
308
289
|
queueLength: targetQueueLength,
|
|
309
290
|
state,
|
|
291
|
+
hasVerdict: model.petition.state === "approved",
|
|
310
292
|
}),
|
|
311
|
-
arcs:
|
|
293
|
+
arcs: model.intent.arc,
|
|
312
294
|
currentArc,
|
|
313
295
|
nextStep,
|
|
314
|
-
...deriveLastActiveAge(
|
|
315
|
-
place:
|
|
296
|
+
...deriveLastActiveAge(model, now),
|
|
297
|
+
place: model.resources.worktree?.place ?? null,
|
|
316
298
|
base,
|
|
317
299
|
baseDrift,
|
|
318
300
|
after,
|
|
319
|
-
|
|
301
|
+
lifecycleDiagnostics: outcome.diagnostics,
|
|
302
|
+
replayBlockers,
|
|
320
303
|
queueDiagnostics: membership ? [...membership.diagnostics] : [],
|
|
321
304
|
});
|
|
322
305
|
}
|
|
306
|
+
for (const contract of lifecycle.isolatedContracts) {
|
|
307
|
+
if (lifecycle.contracts.some((model) => model.contract === contract))
|
|
308
|
+
continue;
|
|
309
|
+
const reason = lifecycleDiseaseReason(lifecycle, contract) || "isolated lifecycle disease";
|
|
310
|
+
derivedTargetById.set(contract, undefined);
|
|
311
|
+
contracts.push({
|
|
312
|
+
id: contract, slug: contractSlugFromId(contract), title: contractSlugFromId(contract),
|
|
313
|
+
state: { state: "corrupt", at: 0, reason }, queueHeadStalled: false, arcs: 0,
|
|
314
|
+
currentArc: { number: 0, objective: "", state: "closed" }, nextStep: { kind: "inspect-log", detail: "inspect log" },
|
|
315
|
+
baseDrift: { state: "unknown", label: "n/a (corrupt ledger)" }, after: [], corruptReason: reason,
|
|
316
|
+
lifecycleDiagnostics: lifecycle.diagnostics.filter((diagnostic) => diagnostic.contract === contract),
|
|
317
|
+
replayBlockers: [], queueDiagnostics: [],
|
|
318
|
+
});
|
|
319
|
+
}
|
|
323
320
|
const filteredContracts = targetFilter === null
|
|
324
321
|
? contracts
|
|
325
322
|
: contracts.filter((contract) => {
|
|
@@ -329,7 +326,6 @@ export async function readKanshiBoard(cwd, now, opts) {
|
|
|
329
326
|
const queueLength = targetFilter === null
|
|
330
327
|
? boardQueueLength
|
|
331
328
|
: (queueModel.targetQueues.find((queue) => queue.target === targetFilter)?.ordered.length ?? 0);
|
|
332
|
-
const claimReconciliation = await computeClaimReconciliation(cwd, defaultBranch);
|
|
333
329
|
const exactProjection = opts?.projectionId === undefined
|
|
334
330
|
? undefined
|
|
335
331
|
: await readExactProjectionStatus(cwd, now, opts.projectionId);
|
|
@@ -344,7 +340,6 @@ export async function readKanshiBoard(cwd, now, opts) {
|
|
|
344
340
|
defaultBranch: defaultBranch?.branch,
|
|
345
341
|
defaultHead: defaultBranch?.head,
|
|
346
342
|
diseases: loadedSettings.diseases,
|
|
347
|
-
claimReconciliation,
|
|
348
343
|
targetFilter,
|
|
349
344
|
...(tasks ? { tasks } : {}),
|
|
350
345
|
};
|
|
@@ -1,84 +1,11 @@
|
|
|
1
|
-
function transitionFromEntry(current, entry) {
|
|
2
|
-
if (current === "claimed" || current === "forfeited" || current === "corrupt") {
|
|
3
|
-
return "corrupt";
|
|
4
|
-
}
|
|
5
|
-
switch (entry.kind) {
|
|
6
|
-
case "bind":
|
|
7
|
-
return "corrupt";
|
|
8
|
-
case "bootstrap":
|
|
9
|
-
// Placement legality is owned exclusively by deriveContractState.
|
|
10
|
-
return "corrupt";
|
|
11
|
-
case "arc":
|
|
12
|
-
return current === "bound" || current === "active" ? "active" : "corrupt";
|
|
13
|
-
case "seal":
|
|
14
|
-
if (current === "active")
|
|
15
|
-
return "active";
|
|
16
|
-
// Derived default arc a0 may seal from bound (bind/bootstrap → seal0 → petition).
|
|
17
|
-
if (current === "bound" && entry.data.arc === 0)
|
|
18
|
-
return "active";
|
|
19
|
-
return "corrupt";
|
|
20
|
-
case "amend":
|
|
21
|
-
return current === "petitioned" ? "active" : current;
|
|
22
|
-
case "renew":
|
|
23
|
-
return current;
|
|
24
|
-
case "verdict":
|
|
25
|
-
if (current !== "petitioned")
|
|
26
|
-
return "corrupt";
|
|
27
|
-
return entry.data.result === "rejected" ? "active" : "petitioned";
|
|
28
|
-
case "petition":
|
|
29
|
-
return current === "bound" || current === "active" ? "petitioned" : "corrupt";
|
|
30
|
-
case "claim":
|
|
31
|
-
return current === "petitioned" ? "claimed" : "corrupt";
|
|
32
|
-
case "forfeit":
|
|
33
|
-
return "forfeited";
|
|
34
|
-
}
|
|
35
|
-
return "corrupt";
|
|
36
|
-
}
|
|
37
|
-
export function deriveContractState(entries) {
|
|
38
|
-
if (entries.length === 0 || entries[0]?.kind !== "bind") {
|
|
39
|
-
return { state: "corrupt", at: 0, reason: "ledger must start with bind" };
|
|
40
|
-
}
|
|
41
|
-
const bind = entries[0];
|
|
42
|
-
let state = "bound";
|
|
43
|
-
for (let index = 1; index < entries.length; index += 1) {
|
|
44
|
-
const entry = entries[index];
|
|
45
|
-
if (entry.kind === "bootstrap") {
|
|
46
|
-
if (index !== 1 || state !== "bound") {
|
|
47
|
-
return { state: "corrupt", at: index + 1, reason: "bootstrap must immediately follow bind" };
|
|
48
|
-
}
|
|
49
|
-
if (!bind.data.bootstrap) {
|
|
50
|
-
return { state: "corrupt", at: index + 1, reason: "bootstrap without bind plan" };
|
|
51
|
-
}
|
|
52
|
-
// Legal bootstrap is a durable fact and leaves the derived state bound.
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
const next = transitionFromEntry(state, entry);
|
|
56
|
-
if (next === "corrupt") {
|
|
57
|
-
return { state: "corrupt", at: index + 1, reason: `invalid ${entry.kind} after ${state}` };
|
|
58
|
-
}
|
|
59
|
-
state = next;
|
|
60
|
-
}
|
|
61
|
-
return { state };
|
|
62
|
-
}
|
|
63
|
-
export function deriveBootstrapDiagnostics(entries) {
|
|
64
|
-
const bind = entries.find((entry) => entry.kind === "bind");
|
|
65
|
-
if (!bind?.data.bootstrap)
|
|
66
|
-
return [];
|
|
67
|
-
if (entries.some((entry) => entry.kind === "bootstrap"))
|
|
68
|
-
return [];
|
|
69
|
-
return [{ kind: "plan-without-result" }];
|
|
70
|
-
}
|
|
71
1
|
export function isTerminalState(state) {
|
|
72
2
|
return state.state === "claimed" || state.state === "forfeited";
|
|
73
3
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
return false;
|
|
4
|
+
export function replayBlockingFact(contract, surface, carrierMismatch) {
|
|
5
|
+
return {
|
|
6
|
+
kind: "incomplete-replay",
|
|
7
|
+
surface,
|
|
8
|
+
recoveryCommand: `keiyaku bind --contract ${contract}`,
|
|
9
|
+
...(carrierMismatch ? { carrierMismatch } : {}),
|
|
10
|
+
};
|
|
84
11
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const STRUCTURED_QUERY_GRAMMAR = "FIELD OP VALUE[,VALUE...] [| FIELD OP VALUE[,VALUE...]]...";
|
|
2
|
+
export const STRUCTURED_QUERY_COMPLETION = "FIELD OP VALUE[,VALUE...]|FIELD OP VALUE[,VALUE...]";
|
|
3
|
+
export const STRUCTURED_QUERY_OPERATORS = ["=", "!=", "~", "<", "<=", ">", ">="];
|
|
4
|
+
export function parseStructuredQuery(input) {
|
|
5
|
+
const parsed = [];
|
|
6
|
+
for (const rawTerm of input.expression.split("|")) {
|
|
7
|
+
if (!rawTerm)
|
|
8
|
+
input.invalid(`'${input.expression}' must use ${STRUCTURED_QUERY_GRAMMAR}`, parsed);
|
|
9
|
+
const match = /^([a-z][a-z0-9_-]*)(!=|<=|>=|~|=|<|>)(.*)$/.exec(rawTerm);
|
|
10
|
+
if (!match)
|
|
11
|
+
input.invalid(`'${input.expression}' must use ${STRUCTURED_QUERY_GRAMMAR}`, parsed);
|
|
12
|
+
const field = match[1];
|
|
13
|
+
const operator = match[2];
|
|
14
|
+
if (!input.fields.has(field))
|
|
15
|
+
input.invalid(`unknown field '${field}'`, parsed);
|
|
16
|
+
const values = match[3].split(",");
|
|
17
|
+
if (values.some((value) => !value))
|
|
18
|
+
input.invalid(`field '${field}' has an empty value`, parsed);
|
|
19
|
+
input.validate(field, operator, values, parsed);
|
|
20
|
+
parsed.push({ field, operator, values });
|
|
21
|
+
}
|
|
22
|
+
return parsed;
|
|
23
|
+
}
|
package/build/core/target-ref.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { FlowError } from "../flow-error.js";
|
|
2
|
-
import { fullBranchRefSchema } from "./
|
|
2
|
+
import { fullBranchRefSchema } from "./repository-ledger/codec.js";
|
|
3
3
|
/** Prefix bare branch names and validate the full ref. Sole status/bind normalizer. */
|
|
4
4
|
export function normalizeTargetRef(raw) {
|
|
5
5
|
const ref = raw.startsWith("refs/heads/") ? raw : `refs/heads/${raw}`;
|
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) {
|