@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAlias, isMap, isScalar, isSeq, parseDocument, stringify, } from "yaml";
|
|
2
|
-
import { canonicalTimestamp, fullId, invalid, nonblank, TASK_STATES, } from "./validation.js";
|
|
2
|
+
import { canonicalTimestamp, fullId, isTaskId, invalid, nonblank, TASK_STATES, } from "./validation.js";
|
|
3
3
|
const FIELD_ORDER = [
|
|
4
4
|
"id",
|
|
5
5
|
"title",
|
|
@@ -8,6 +8,8 @@ const FIELD_ORDER = [
|
|
|
8
8
|
"needs",
|
|
9
9
|
"parent",
|
|
10
10
|
"from",
|
|
11
|
+
"supersedes",
|
|
12
|
+
"relatesTo",
|
|
11
13
|
"createdAt",
|
|
12
14
|
"updatedAt",
|
|
13
15
|
"creator",
|
|
@@ -100,11 +102,13 @@ export function parseTaskDocument(source) {
|
|
|
100
102
|
invalid(`task field '${key}' must be a scalar or string array`);
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
|
-
const id =
|
|
105
|
+
const id = nonblank("id", values.get("id"));
|
|
106
|
+
if (!isTaskId(id))
|
|
107
|
+
invalid("task field 'id' must be a canonical local task id");
|
|
104
108
|
const title = nonblank("title", values.get("title"));
|
|
105
109
|
const state = values.get("state");
|
|
106
110
|
if (typeof state !== "string" || !TASK_STATES.has(state)) {
|
|
107
|
-
invalid("task field 'state' must be open, in_progress, done, or drop");
|
|
111
|
+
invalid("task field 'state' must be open, in_progress, on_hold, done, or drop");
|
|
108
112
|
}
|
|
109
113
|
const pri = values.get("pri");
|
|
110
114
|
if (typeof pri !== "number" || !Number.isInteger(pri) || pri < 0 || pri > 3) {
|
|
@@ -120,10 +124,7 @@ export function parseTaskDocument(source) {
|
|
|
120
124
|
const value = values.get(field);
|
|
121
125
|
if (value === null)
|
|
122
126
|
return null;
|
|
123
|
-
|
|
124
|
-
if (related === id)
|
|
125
|
-
invalid(`task field '${field}' must not reference itself`);
|
|
126
|
-
return related;
|
|
127
|
+
return fullId(field, value);
|
|
127
128
|
};
|
|
128
129
|
const parent = relationship("parent");
|
|
129
130
|
const fromValue = values.get("from");
|
|
@@ -132,8 +133,19 @@ export function parseTaskDocument(source) {
|
|
|
132
133
|
const from = fromValue.map((related) => fullId("from", related));
|
|
133
134
|
if (new Set(from).size !== from.length)
|
|
134
135
|
invalid("task field 'from' must be unique");
|
|
135
|
-
|
|
136
|
-
|
|
136
|
+
const relationships = (field) => {
|
|
137
|
+
const value = values.get(field);
|
|
138
|
+
if (value === undefined)
|
|
139
|
+
return [];
|
|
140
|
+
if (!Array.isArray(value))
|
|
141
|
+
invalid(`task field '${field}' must be a string array`);
|
|
142
|
+
const related = value.map((item) => fullId(field, item));
|
|
143
|
+
if (new Set(related).size !== related.length)
|
|
144
|
+
invalid(`task field '${field}' must be unique`);
|
|
145
|
+
return related;
|
|
146
|
+
};
|
|
147
|
+
const supersedes = relationships("supersedes");
|
|
148
|
+
const relatesTo = relationships("relatesTo");
|
|
137
149
|
const createdAt = canonicalTimestamp("createdAt", values.get("createdAt"));
|
|
138
150
|
const updatedValue = values.get("updatedAt");
|
|
139
151
|
const updatedAt = updatedValue === undefined ? undefined : canonicalTimestamp("updatedAt", updatedValue);
|
|
@@ -156,6 +168,8 @@ export function parseTaskDocument(source) {
|
|
|
156
168
|
needs,
|
|
157
169
|
parent,
|
|
158
170
|
from,
|
|
171
|
+
supersedes,
|
|
172
|
+
relatesTo,
|
|
159
173
|
createdAt,
|
|
160
174
|
...(updatedAt === undefined ? {} : { updatedAt }),
|
|
161
175
|
creator,
|
|
@@ -177,6 +191,8 @@ function serializeUnchecked(document) {
|
|
|
177
191
|
needs: [...document.needs],
|
|
178
192
|
parent: document.parent,
|
|
179
193
|
from: document.from,
|
|
194
|
+
...(document.supersedes.length === 0 ? {} : { supersedes: document.supersedes }),
|
|
195
|
+
...(document.relatesTo.length === 0 ? {} : { relatesTo: document.relatesTo }),
|
|
180
196
|
createdAt: document.createdAt,
|
|
181
197
|
...(document.updatedAt === undefined ? {} : { updatedAt: document.updatedAt }),
|
|
182
198
|
creator: document.creator,
|
package/build/core/task/index.js
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
* Only symbols required by current external consumers are re-exported.
|
|
4
4
|
* Package-internal modules must import each other directly, not through this file.
|
|
5
5
|
*/
|
|
6
|
-
export { previewTaskBoardForBind, reconcileTaskForfeit,
|
|
6
|
+
export { previewTaskBoardForBind, reconcileTaskForfeit, replayClaimTasks, } from "./task-contract.js";
|
|
7
|
+
export { replayPreparedTaskBind, replayPreparedTaskBindLocked, replayCommittedTaskBind, replayCommittedTaskBindForfeit, replayPreparedTaskBindForfeit, withPreparedTaskBind, } from "./task-bind-preparation.js";
|
|
8
|
+
export { readTaskBoardGeneration } from "./task-git-store.js";
|
|
7
9
|
export { resolveTaskCreator } from "./task-git-actor.js";
|
|
8
|
-
export { openTaskCommandRepository, readTaskDoctor, taskCommandContractId, } from "./task-git-runtime.js";
|
|
9
|
-
export { openTaskStore, readTaskHistory } from "./task-git-store.js";
|
|
10
|
+
export { openTaskCommandRepository, openTaskMutationContext, readTaskDoctor, taskCommandContractId, taskCommandNamespace, } from "./task-git-runtime.js";
|
|
11
|
+
export { openTaskStore, readTaskHistory, readTaskNamespaces } from "./task-git-store.js";
|
|
10
12
|
export { taskRepositoryFromStore } from "./task-store-repository.js";
|
|
11
13
|
export { isTaskId } from "./validation.js";
|
|
12
|
-
export {
|
|
14
|
+
export { isTaskCoordinate, parseTaskCoordinate, taskCoordinateFromSourcePath } from "./coordinate.js";
|
|
15
|
+
export { buildTaskTree, createTask, doneTask, dropTask, parseTaskDocument, readTaskBoard, resolveTaskReadId, serializeTaskDocument, startTask, stopTask, holdTask, resumeTask, updateTask, updateTaskWithReceipt, deriveTaskDisplay, selectTaskList, taskUpdatedAt, advancedTaskUpdatedAt, } from "./task.js";
|
package/build/core/task/query.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { FlowError } from "../../flow-error.js";
|
|
2
2
|
import { isCommissionId } from "../ids.js";
|
|
3
3
|
import { taskUpdatedAt } from "./model.js";
|
|
4
|
-
import {
|
|
4
|
+
import { isTaskCoordinate } from "./coordinate.js";
|
|
5
|
+
import { parseStructuredQuery, STRUCTURED_QUERY_COMPLETION, STRUCTURED_QUERY_GRAMMAR, } from "../structured-query.js";
|
|
5
6
|
export const TASK_FILTER_FIELDS = [
|
|
6
7
|
"title", "state", "priority", "parent", "needs", "blocks", "contract", "created", "updated",
|
|
7
8
|
];
|
|
8
9
|
export const TASK_FILTER_OPERATORS = ["=", "!=", "~", "<", "<=", ">", ">="];
|
|
9
|
-
export const TASK_FILTER_GRAMMAR =
|
|
10
|
-
export const TASK_FILTER_COMPLETION =
|
|
10
|
+
export const TASK_FILTER_GRAMMAR = STRUCTURED_QUERY_GRAMMAR;
|
|
11
|
+
export const TASK_FILTER_COMPLETION = STRUCTURED_QUERY_COMPLETION;
|
|
11
12
|
const FILTER_FIELDS = new Set(TASK_FILTER_FIELDS);
|
|
12
13
|
const ORDERED_FIELDS = new Set(["priority", "created", "updated"]);
|
|
13
14
|
const TITLE_OPERATORS = new Set(["=", "!=", "~"]);
|
|
14
15
|
const ORDERED_OPERATORS = new Set(["=", "!=", "<", "<=", ">", ">="]);
|
|
15
|
-
const DISPLAY_VALUES = new Set(["ready", "blocked", "in_progress", "done", "dropped", "bound"]);
|
|
16
|
+
const DISPLAY_VALUES = new Set(["ready", "blocked", "in_progress", "on_hold", "done", "dropped", "bound"]);
|
|
16
17
|
const SORT_KEYS = new Set(["priority", "modified", "created"]);
|
|
17
18
|
const RELATIVE_TIME = /^now-([1-9][0-9]*)([smhdw])$/;
|
|
18
19
|
const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
|
|
@@ -21,6 +22,8 @@ export function deriveTaskDisplay(task) {
|
|
|
21
22
|
return "done";
|
|
22
23
|
if (task.state === "drop")
|
|
23
24
|
return "dropped";
|
|
25
|
+
if (task.state === "on_hold")
|
|
26
|
+
return "on_hold";
|
|
24
27
|
if (task.contractId !== undefined)
|
|
25
28
|
return "bound";
|
|
26
29
|
if (task.state === "in_progress")
|
|
@@ -43,14 +46,7 @@ function invalidFilter(detail, recoveredTerms = []) {
|
|
|
43
46
|
},
|
|
44
47
|
});
|
|
45
48
|
}
|
|
46
|
-
function
|
|
47
|
-
const match = /^([a-z][a-z0-9_-]*)(!=|<=|>=|~|=|<|>)(.*)$/.exec(term);
|
|
48
|
-
if (!match)
|
|
49
|
-
invalidFilter(`'${expression}' must use ${TASK_FILTER_GRAMMAR}`, recoveredTerms);
|
|
50
|
-
const field = match[1];
|
|
51
|
-
const operator = match[2];
|
|
52
|
-
if (!FILTER_FIELDS.has(field))
|
|
53
|
-
invalidFilter(`unknown field '${field}'`, recoveredTerms);
|
|
49
|
+
function validateTaskFilterTerm(field, operator, values, recoveredTerms) {
|
|
54
50
|
if (field === "title" && !TITLE_OPERATORS.has(operator)) {
|
|
55
51
|
invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
|
|
56
52
|
}
|
|
@@ -59,21 +55,15 @@ function parseTaskFilterTerm(term, expression, recoveredTerms) {
|
|
|
59
55
|
: operator !== "=" && operator !== "!=")) {
|
|
60
56
|
invalidFilter(`field '${field}' does not support operator '${operator}'`, recoveredTerms);
|
|
61
57
|
}
|
|
62
|
-
const values = match[3].split(",");
|
|
63
|
-
if (values.some((value) => value.length === 0))
|
|
64
|
-
invalidFilter(`field '${field}' has an empty value`, recoveredTerms);
|
|
65
58
|
validateFilterValues(field, values, recoveredTerms);
|
|
66
|
-
return { field, operator, values };
|
|
67
59
|
}
|
|
68
60
|
export function parseTaskFilter(expression) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
return parsed;
|
|
61
|
+
return parseStructuredQuery({
|
|
62
|
+
expression,
|
|
63
|
+
fields: FILTER_FIELDS,
|
|
64
|
+
invalid: invalidFilter,
|
|
65
|
+
validate: validateTaskFilterTerm,
|
|
66
|
+
});
|
|
77
67
|
}
|
|
78
68
|
function parseTimestamp(value, nowMs, recoveredTerms = []) {
|
|
79
69
|
const relative = RELATIVE_TIME.exec(value);
|
|
@@ -114,8 +104,8 @@ function validateFilterValues(field, values, recoveredTerms) {
|
|
|
114
104
|
for (const value of values) {
|
|
115
105
|
if (field === "parent" && value === "none")
|
|
116
106
|
continue;
|
|
117
|
-
if (!
|
|
118
|
-
invalidFilter(`value '${value}' must be a canonical full task
|
|
107
|
+
if (!isTaskCoordinate(value))
|
|
108
|
+
invalidFilter(`value '${value}' must be a canonical full task coordinate`, recoveredTerms);
|
|
119
109
|
}
|
|
120
110
|
return;
|
|
121
111
|
}
|
|
@@ -183,7 +173,7 @@ function selected(task, selection) {
|
|
|
183
173
|
}
|
|
184
174
|
function displayOrder(task) {
|
|
185
175
|
const display = deriveTaskDisplay(task);
|
|
186
|
-
return display === "in_progress" ? 0 : display === "
|
|
176
|
+
return display === "in_progress" ? 0 : display === "on_hold" ? 1 : display === "ready" ? 2 : display === "blocked" ? 3 : display === "bound" ? 4 : display === "done" ? 5 : 6;
|
|
187
177
|
}
|
|
188
178
|
function defaultCompare(left, right) {
|
|
189
179
|
return displayOrder(left) - displayOrder(right)
|
|
@@ -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, ["
|
|
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, ["rev-parse", `${commit}^{tree}`])).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
|
}
|