@astrosheep/keiyaku 4.1.0 → 4.1.2
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 +1 -1
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku/SKILL.md +3 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-akuma/SKILL.md +3 -0
- package/build/integrations/marketplace/plugins/keiyaku/skills/keiyaku-workflow/SKILL.md +3 -6
- package/build/src/akuma/akuma-errors.d.ts +6 -0
- package/build/src/akuma/akuma-errors.js +9 -0
- package/build/src/akuma/akuma-handle.d.ts +39 -0
- package/build/src/akuma/akuma-handle.js +263 -0
- package/build/src/akuma/akuma-observe.d.ts +19 -0
- package/build/src/akuma/akuma-observe.js +74 -0
- package/build/src/akuma/akuma-product-symbols.d.ts +1 -0
- package/build/src/akuma/akuma-product-symbols.js +1 -0
- package/build/src/akuma/akuma-product.d.ts +18 -0
- package/build/src/akuma/akuma-product.js +154 -0
- package/build/src/akuma/akuma.d.ts +37 -81
- package/build/src/akuma/akuma.js +39 -447
- package/build/src/akuma/allowed.d.ts +1 -0
- package/build/src/akuma/allowed.js +6 -0
- package/build/src/akuma/archetype.js +8 -5
- package/build/src/akuma/body-supervisor.d.ts +26 -0
- package/build/src/akuma/body-supervisor.js +89 -0
- package/build/src/akuma/body.d.ts +45 -4
- package/build/src/akuma/body.js +145 -35
- package/build/src/akuma/coordinate.js +17 -7
- package/build/src/akuma/heart/facts.js +1 -1
- package/build/src/akuma/heart/index.d.ts +1 -1
- package/build/src/akuma/heart/index.js +2 -4
- package/build/src/akuma/heart/request-authority.js +4 -4
- package/build/src/akuma/heart/request-rows.js +27 -16
- package/build/src/akuma/heart/rows.js +79 -41
- package/build/src/akuma/heart/soul.js +1 -1
- package/build/src/akuma/heart/storage.d.ts +1 -0
- package/build/src/akuma/heart/storage.js +76 -11
- package/build/src/akuma/heart/tells.js +35 -18
- package/build/src/akuma/heart/timeline.js +19 -11
- package/build/src/akuma/identity.d.ts +1 -0
- package/build/src/akuma/identity.js +1 -0
- package/build/src/akuma/index.d.ts +3 -3
- package/build/src/akuma/index.js +1 -1
- package/build/src/akuma/nuke.js +106 -14
- package/build/src/akuma/projection-read.d.ts +17 -0
- package/build/src/akuma/projection-read.js +135 -0
- package/build/src/akuma/projection.d.ts +1 -16
- package/build/src/akuma/projection.js +77 -154
- package/build/src/akuma/provider-recipe.js +11 -5
- package/build/src/akuma/provider.js +105 -62
- package/build/src/akuma/providers/acp/config.js +5 -6
- package/build/src/akuma/providers/acp/core.js +17 -10
- package/build/src/akuma/providers/acp/events.js +38 -29
- package/build/src/akuma/providers/acp/index.js +13 -9
- package/build/src/akuma/providers/claude/events.js +8 -12
- package/build/src/akuma/providers/claude/index.d.ts +1 -0
- package/build/src/akuma/providers/claude/index.js +55 -26
- package/build/src/akuma/providers/claude/input.js +6 -2
- package/build/src/akuma/providers/codex-app-server/events.js +61 -36
- package/build/src/akuma/providers/codex-app-server/index.js +25 -13
- package/build/src/akuma/providers/grok-build/index.js +22 -25
- package/build/src/akuma/providers/index.js +2 -18
- package/build/src/akuma/providers/opencode-sdk/events.js +32 -18
- package/build/src/akuma/providers/opencode-sdk/index.js +26 -17
- package/build/src/akuma/providers/opencode-sdk/session.js +21 -5
- package/build/src/akuma/providers/pi/events.js +26 -19
- package/build/src/akuma/providers/pi/index.js +58 -40
- package/build/src/akuma/publication.js +9 -9
- package/build/src/akuma/request-execution.d.ts +6 -0
- package/build/src/akuma/request-execution.js +86 -0
- package/build/src/akuma/request-serve.d.ts +12 -1
- package/build/src/akuma/request-serve.js +19 -122
- package/build/src/akuma/request-wire.d.ts +2 -2
- package/build/src/akuma/request-wire.js +75 -102
- package/build/src/akuma/requests.js +2 -2
- package/build/src/akuma/{body-turn.d.ts → turn-drive.d.ts} +2 -25
- package/build/src/akuma/{body-turn.js → turn-drive.js} +20 -103
- package/build/src/akuma-body.js +6 -5
- package/build/src/body/amend.d.ts +4 -1
- package/build/src/body/amend.js +14 -6
- package/build/src/body/decode.js +2 -4
- package/build/src/body/envelope.js +2 -4
- package/build/src/body/region.d.ts +1 -2
- package/build/src/body/region.js +2 -70
- package/build/src/body/render.js +17 -12
- package/build/src/body/shape.js +1 -7
- package/build/src/cli/accepted.d.ts +1 -1
- package/build/src/cli/accepted.js +8 -1
- package/build/src/cli/commands/akuma-invoke.js +18 -6
- package/build/src/cli/commands/akuma.d.ts +0 -6
- package/build/src/cli/commands/akuma.js +11 -24
- package/build/src/cli/commands/amend.d.ts +2 -2
- package/build/src/cli/commands/amend.js +2 -4
- package/build/src/cli/commands/bind.d.ts +4 -4
- package/build/src/cli/commands/bind.js +16 -6
- package/build/src/cli/commands/contract-invoke.d.ts +25 -0
- package/build/src/cli/commands/contract-invoke.js +193 -0
- package/build/src/cli/commands/contract.d.ts +109 -10
- package/build/src/cli/commands/contract.js +339 -16
- package/build/src/cli/commands/install.d.ts +1 -1
- package/build/src/cli/commands/install.js +23 -15
- package/build/src/cli/commands/task-invoke.js +187 -67
- package/build/src/cli/commands/task-query.js +48 -24
- package/build/src/cli/commands/task.js +150 -38
- package/build/src/cli/coordinates.js +2 -4
- package/build/src/cli/invoke.js +92 -208
- package/build/src/cli/main.js +4 -2
- package/build/src/cli/parse.d.ts +3 -90
- package/build/src/cli/parse.js +27 -233
- package/build/src/cli/render/akuma-activity.d.ts +38 -0
- package/build/src/cli/render/akuma-activity.js +362 -0
- package/build/src/cli/render/akuma-tool-command.js +6 -7
- package/build/src/cli/render/akuma-tool.js +10 -8
- package/build/src/cli/render/akuma.d.ts +1 -1
- package/build/src/cli/render/akuma.js +61 -353
- package/build/src/cli/render/audit.js +5 -15
- package/build/src/cli/render/board.d.ts +1 -0
- package/build/src/cli/render/board.js +17 -0
- package/build/src/cli/render/catalog.js +81 -8
- package/build/src/cli/render/contract-history.d.ts +2 -0
- package/build/src/cli/render/contract-history.js +83 -0
- package/build/src/cli/render/contract-observation.d.ts +10 -0
- package/build/src/cli/render/contract-observation.js +58 -0
- package/build/src/cli/render/contract.d.ts +1 -2
- package/build/src/cli/render/contract.js +41 -142
- package/build/src/cli/render/kanshi.js +239 -215
- package/build/src/cli/render/receipt.js +2 -9
- package/build/src/cli/render/refusal.js +10 -15
- package/build/src/cli/render/region.js +22 -6
- package/build/src/cli/render/settings.js +4 -4
- package/build/src/cli/render/task.js +34 -14
- package/build/src/cli/render/terminal.js +16 -5
- package/build/src/cli/result.d.ts +3 -3
- package/build/src/cli/runtime.d.ts +6 -1
- package/build/src/cli/runtime.js +84 -63
- package/build/src/cli/selectors.js +2 -4
- package/build/src/cli/usage.js +6 -5
- package/build/src/contract-worktree.d.ts +1 -23
- package/build/src/contract-worktree.js +79 -155
- package/build/src/coordination/durable-file.js +7 -3
- package/build/src/coordination/sqlite-transaction-lock.js +29 -12
- package/build/src/core/facts/codec.js +7 -3
- package/build/src/core/facts/types.d.ts +1 -1
- package/build/src/core/subject.js +16 -9
- package/build/src/core/verbs/abandon.d.ts +1 -1
- package/build/src/core/verbs/abandon.js +1 -1
- package/build/src/core/verbs/amend.d.ts +1 -1
- package/build/src/core/verbs/amend.js +7 -7
- package/build/src/core/verbs/attestation.js +4 -1
- package/build/src/core/verbs/bind.d.ts +1 -1
- package/build/src/core/verbs/bind.js +1 -1
- package/build/src/core/verbs/placement.d.ts +1 -1
- package/build/src/core/verbs/placement.js +10 -8
- package/build/src/dispatch/index.js +8 -6
- package/build/src/duration.js +5 -1
- package/build/src/git/admission.js +5 -3
- package/build/src/git/hooks.d.ts +8 -0
- package/build/src/git/hooks.js +63 -14
- package/build/src/git/identity.js +1 -4
- package/build/src/git/integration.d.ts +2 -3
- package/build/src/git/integration.js +32 -39
- package/build/src/git/nuke.d.ts +1 -1
- package/build/src/git/nuke.js +110 -57
- package/build/src/git/observe.d.ts +13 -1
- package/build/src/git/observe.js +51 -44
- package/build/src/git/process.js +4 -2
- package/build/src/git/read-observation.js +10 -8
- package/build/src/git/reconcile.d.ts +22 -2
- package/build/src/git/reconcile.js +38 -274
- package/build/src/git/repository.d.ts +2 -0
- package/build/src/git/repository.js +13 -22
- package/build/src/git/scratch.d.ts +8 -5
- package/build/src/git/scratch.js +29 -2
- package/build/src/git/target-placement.d.ts +9 -10
- package/build/src/git/target-placement.js +59 -62
- package/build/src/git/tender.js +13 -46
- package/build/src/git/terminal-reconcile.d.ts +21 -0
- package/build/src/git/terminal-reconcile.js +278 -0
- package/build/src/git/terminal-seal.js +15 -12
- package/build/src/git/tree.js +5 -2
- package/build/src/git/workspace.d.ts +24 -2
- package/build/src/git/workspace.js +120 -16
- package/build/src/identity/coordinates.js +3 -1
- package/build/src/identity/selector.js +6 -3
- package/build/src/kanshi/fleet.d.ts +6 -0
- package/build/src/kanshi/fleet.js +27 -0
- package/build/src/kanshi/index.d.ts +1 -1
- package/build/src/kanshi/read.js +60 -48
- package/build/src/kanshi/report.d.ts +11 -23
- package/build/src/kanshi/select.js +25 -24
- package/build/src/library/address.d.ts +2 -1
- package/build/src/library/address.js +7 -8
- package/build/src/library/akuma-creation.js +20 -12
- package/build/src/library/audit.d.ts +0 -1
- package/build/src/library/audit.js +1 -2
- package/build/src/library/bind.d.ts +53 -5
- package/build/src/library/bind.js +98 -22
- package/build/src/library/catalog.d.ts +1 -0
- package/build/src/library/catalog.js +6 -3
- package/build/src/library/configuration.js +3 -1
- package/build/src/library/continuation.d.ts +15 -4
- package/build/src/library/continuation.js +24 -16
- package/build/src/library/contract-bind.d.ts +26 -0
- package/build/src/library/contract-bind.js +106 -0
- package/build/src/library/contract-operations.d.ts +77 -0
- package/build/src/library/contract-operations.js +130 -0
- package/build/src/library/contract-types.d.ts +95 -0
- package/build/src/library/contract-types.js +1 -0
- package/build/src/library/contract.d.ts +19 -154
- package/build/src/library/contract.js +46 -261
- package/build/src/library/fleet.d.ts +1 -1
- package/build/src/library/fleet.js +19 -11
- package/build/src/library/input.d.ts +2 -0
- package/build/src/library/input.js +14 -0
- package/build/src/library/keiyaku.d.ts +8 -7
- package/build/src/library/keiyaku.js +3 -3
- package/build/src/library/nuke.js +14 -5
- package/build/src/library/reconcile.d.ts +9 -4
- package/build/src/library/reconcile.js +30 -24
- package/build/src/library/refusal.d.ts +13 -5
- package/build/src/library/refusal.js +7 -0
- package/build/src/library/region.d.ts +8 -7
- package/build/src/library/region.js +31 -11
- package/build/src/library/repo.js +2 -2
- package/build/src/markdown/lex.js +9 -2
- package/build/src/markdown/parse.js +11 -7
- package/build/src/markdown/query.js +4 -2
- package/build/src/protocol/amend.js +3 -1
- package/build/src/protocol/attempt.d.ts +7 -3
- package/build/src/protocol/attempt.js +9 -5
- package/build/src/protocol/audit.d.ts +1 -1
- package/build/src/protocol/audit.js +4 -14
- package/build/src/protocol/bind.d.ts +19 -6
- package/build/src/protocol/bind.js +28 -13
- package/build/src/protocol/completion.d.ts +0 -1
- package/build/src/protocol/completion.js +1 -3
- package/build/src/protocol/deliver.d.ts +1 -3
- package/build/src/protocol/deliver.js +15 -39
- package/build/src/protocol/intent.d.ts +1 -0
- package/build/src/protocol/intent.js +9 -7
- package/build/src/protocol/operations.d.ts +3 -7
- package/build/src/protocol/operations.js +5 -3
- package/build/src/protocol/placement.d.ts +0 -1
- package/build/src/protocol/placement.js +6 -6
- package/build/src/protocol/read/observation.d.ts +11 -0
- package/build/src/protocol/read/observation.js +37 -0
- package/build/src/protocol/read/status.d.ts +23 -12
- package/build/src/protocol/read/status.js +90 -72
- package/build/src/protocol/reconcile.d.ts +1 -0
- package/build/src/protocol/reconcile.js +2 -3
- package/build/src/protocol/reintegrate.js +1 -1
- package/build/src/protocol/review.d.ts +0 -1
- package/build/src/protocol/review.js +15 -22
- package/build/src/protocol/run.d.ts +1 -0
- package/build/src/protocol/run.js +4 -5
- package/build/src/runtime/proc/launch.d.ts +33 -0
- package/build/src/runtime/proc/launch.js +98 -0
- package/build/src/runtime/proc/line-rpc.js +9 -3
- package/build/src/runtime/proc/run.d.ts +12 -6
- package/build/src/runtime/proc/run.js +78 -21
- package/build/src/runtime/proc/stdio.js +7 -10
- package/build/src/settings.js +6 -2
- package/build/src/settlement/holder.js +13 -10
- package/build/src/settlement/settle.js +39 -6
- package/build/src/task/board.js +12 -6
- package/build/src/task/compose.js +85 -22
- package/build/src/task/context.js +21 -8
- package/build/src/task/document.js +46 -10
- package/build/src/task/identity.js +5 -2
- package/build/src/task/index.d.ts +0 -1
- package/build/src/task/index.js +119 -34
- package/build/src/task/input.js +29 -7
- package/build/src/task/mutation.js +61 -19
- package/build/src/task/operations.d.ts +6 -1
- package/build/src/task/operations.js +83 -29
- package/build/src/task/query.js +55 -26
- package/build/src/task/store.d.ts +3 -1
- package/build/src/task/store.js +81 -17
- package/build/src/verification/execution.js +14 -11
- package/build/src/workspace-place.js +19 -7
- package/build/src/world.js +16 -6
- package/package.json +8 -3
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { documentDiff } from "../markdown/diff.js";
|
|
2
2
|
import { createTaskRelations, projectDetailFacts, projectTaskBoardObservation, relationProblem, projectBlocked, projectRows, } from "./board.js";
|
|
3
|
-
import { parseTaskCreationDocument, serializeTaskDocument } from "./document.js";
|
|
3
|
+
import { parseTaskCreationDocument, serializeTaskDocument, } from "./document.js";
|
|
4
4
|
import { allocateLocalId, deriveLocalStem, formatTaskId, parseTaskId, sameNamespace } from "./identity.js";
|
|
5
|
-
import { authorityPath, nukeTaskAuthority, readBoard, replaceAuthority, withTaskLocks
|
|
6
|
-
import { DEFAULT_TASK_LIMIT, projectPage, projectQuery, queryUnderTargets, underExpression } from "./query.js";
|
|
7
|
-
export async function nukeTask(world) {
|
|
8
|
-
if (await nukeTaskAuthority(world) === "busy")
|
|
5
|
+
import { authorityPath, nukeTaskAuthority, readBoard, replaceAuthority, withTaskLocks } from "./store.js";
|
|
6
|
+
import { DEFAULT_TASK_LIMIT, projectPage, projectQuery, queryUnderTargets, underExpression, } from "./query.js";
|
|
7
|
+
export async function nukeTask(world, options) {
|
|
8
|
+
if ((await nukeTaskAuthority(world, options)) === "busy")
|
|
9
9
|
throw new Error("Task reset lock contention");
|
|
10
10
|
}
|
|
11
11
|
export function taskView(document) {
|
|
12
12
|
return { ...document, namespace: parseTaskId(document.id).namespace };
|
|
13
13
|
}
|
|
14
|
-
function refused(refusal) {
|
|
15
|
-
|
|
14
|
+
function refused(refusal) {
|
|
15
|
+
return { kind: "refused", refusal };
|
|
16
|
+
}
|
|
17
|
+
function retry(reason) {
|
|
18
|
+
return { kind: "retry", reason };
|
|
19
|
+
}
|
|
16
20
|
function occupied(board, namespace) {
|
|
17
21
|
return new Set([...board.tasks.values()].flatMap((task) => {
|
|
18
22
|
const coordinate = parseTaskId(task.id);
|
|
@@ -24,16 +28,16 @@ function currentTimestamp() {
|
|
|
24
28
|
}
|
|
25
29
|
/** Internal Task timestamp rule shared by ordinary mutations and compose. */
|
|
26
30
|
export function advanceTaskTimestamp(previous, candidate) {
|
|
27
|
-
return candidate > previous
|
|
28
|
-
? candidate
|
|
29
|
-
: new Date(Date.parse(previous) + 1).toISOString();
|
|
31
|
+
return candidate > previous ? candidate : new Date(Date.parse(previous) + 1).toISOString();
|
|
30
32
|
}
|
|
31
33
|
function addDocument(base, namespace, board, at, actor) {
|
|
32
34
|
const localId = allocateLocalId(deriveLocalStem(base.title), occupied(board, namespace));
|
|
33
35
|
return {
|
|
34
|
-
...base,
|
|
36
|
+
...base,
|
|
37
|
+
id: formatTaskId({ namespace, localId }),
|
|
35
38
|
...(actor === undefined ? {} : { createdBy: actor }),
|
|
36
|
-
createdAt: at,
|
|
39
|
+
createdAt: at,
|
|
40
|
+
updatedAt: at,
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
43
|
function boardWith(board, document) {
|
|
@@ -47,16 +51,29 @@ async function create(world, base, namespace, signal, actor) {
|
|
|
47
51
|
const problem = relationProblem(boardWith(snapshot.board, next), null, next);
|
|
48
52
|
if (problem !== null)
|
|
49
53
|
return refused({ kind: "invalid-graph", diagnostic: problem });
|
|
50
|
-
const replaced = await replaceAuthority({
|
|
51
|
-
|
|
54
|
+
const replaced = await replaceAuthority({
|
|
55
|
+
path: authorityPath(world, next.id),
|
|
56
|
+
expected: null,
|
|
57
|
+
next: serializeTaskDocument(next),
|
|
58
|
+
});
|
|
59
|
+
return replaced === "replaced"
|
|
60
|
+
? { kind: "accepted", value: taskView(next) }
|
|
61
|
+
: retry("concurrent-modification");
|
|
52
62
|
});
|
|
53
63
|
return result === "busy" ? retry("busy") : result;
|
|
54
64
|
}
|
|
55
65
|
export async function addTask(world, input) {
|
|
56
66
|
const namespace = input.namespace ?? [];
|
|
57
67
|
return create(world, {
|
|
58
|
-
title: input.title,
|
|
59
|
-
|
|
68
|
+
title: input.title,
|
|
69
|
+
body: input.body ?? "",
|
|
70
|
+
note: input.note ?? "",
|
|
71
|
+
state: input.state ?? "open",
|
|
72
|
+
priority: input.priority ?? 2,
|
|
73
|
+
needs: input.needs ?? [],
|
|
74
|
+
parent: input.parent ?? null,
|
|
75
|
+
supersedes: input.supersedes ?? [],
|
|
76
|
+
relates: input.relates ?? [],
|
|
60
77
|
}, namespace, input.signal, input.actor);
|
|
61
78
|
}
|
|
62
79
|
export async function addTaskDocument(world, input) {
|
|
@@ -73,6 +90,12 @@ function listChange(current, replacement, additions, removals) {
|
|
|
73
90
|
function relatedOwner(board, id, target) {
|
|
74
91
|
return board.tasks.get(target)?.relates.includes(id) ? target : null;
|
|
75
92
|
}
|
|
93
|
+
function appendTaskBody(current, addition) {
|
|
94
|
+
if (current.length === 0 || addition.length === 0 || current.endsWith("\n") || addition.startsWith("\n")) {
|
|
95
|
+
return current + addition;
|
|
96
|
+
}
|
|
97
|
+
return `${current}\n${addition}`;
|
|
98
|
+
}
|
|
76
99
|
function updateDocument(board, current, input) {
|
|
77
100
|
for (const related of input.dropRelates ?? [])
|
|
78
101
|
if (!current.relates.includes(related)) {
|
|
@@ -83,7 +106,7 @@ function updateDocument(board, current, input) {
|
|
|
83
106
|
return {
|
|
84
107
|
...current,
|
|
85
108
|
...(input.title === undefined ? {} : { title: input.title }),
|
|
86
|
-
body: input.body ?? (input.appendBody === undefined ? current.body : current.body
|
|
109
|
+
body: input.body ?? (input.appendBody === undefined ? current.body : appendTaskBody(current.body, input.appendBody)),
|
|
87
110
|
...(input.note === undefined ? {} : { note: input.note }),
|
|
88
111
|
...(input.priority === undefined ? {} : { priority: input.priority }),
|
|
89
112
|
needs: listChange(current.needs, input.needs, input.addNeeds, input.dropNeeds),
|
|
@@ -113,18 +136,25 @@ export async function updateTask(world, id, input) {
|
|
|
113
136
|
}
|
|
114
137
|
const afterBytes = serializeTaskDocument(next);
|
|
115
138
|
const after = Buffer.from(afterBytes).toString("utf8");
|
|
116
|
-
if (before !== after
|
|
117
|
-
|
|
139
|
+
if (before !== after &&
|
|
140
|
+
(await replaceAuthority({ path: authorityPath(world, id), expected: predecessor, next: afterBytes })) !==
|
|
141
|
+
"replaced") {
|
|
118
142
|
return { kind: "retry", reason: "concurrent-modification" };
|
|
119
143
|
}
|
|
120
144
|
const label = `${id}.md`;
|
|
121
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
kind: "accepted",
|
|
147
|
+
value: { task: taskView(next), documentDiff: documentDiff(label, label, before, after) },
|
|
148
|
+
};
|
|
122
149
|
});
|
|
123
150
|
return result === "busy" ? { kind: "retry", reason: "busy" } : result;
|
|
124
151
|
}
|
|
125
152
|
const TRANSITIONS = {
|
|
126
|
-
start: { open: "in_progress" },
|
|
127
|
-
|
|
153
|
+
start: { open: "in_progress" },
|
|
154
|
+
stop: { in_progress: "open" },
|
|
155
|
+
hold: { open: "on_hold", in_progress: "on_hold" },
|
|
156
|
+
resume: { on_hold: "open" },
|
|
157
|
+
done: { open: "done", in_progress: "done", on_hold: "done" },
|
|
128
158
|
drop: { open: "drop", in_progress: "drop", on_hold: "drop" },
|
|
129
159
|
};
|
|
130
160
|
export async function lifecycleTask(world, id, verb, signal, note) {
|
|
@@ -144,8 +174,13 @@ export async function lifecycleTask(world, id, verb, signal, note) {
|
|
|
144
174
|
updatedAt: advanceTaskTimestamp(current.updatedAt, at),
|
|
145
175
|
};
|
|
146
176
|
const bytes = serializeTaskDocument(next);
|
|
147
|
-
return await replaceAuthority({
|
|
148
|
-
|
|
177
|
+
return (await replaceAuthority({
|
|
178
|
+
path: authorityPath(world, id),
|
|
179
|
+
expected: snapshot.bytes.get(id),
|
|
180
|
+
next: bytes,
|
|
181
|
+
})) === "replaced"
|
|
182
|
+
? { kind: "accepted", value: taskView(next) }
|
|
183
|
+
: retry("concurrent-modification");
|
|
149
184
|
});
|
|
150
185
|
return result === "busy" ? retry("busy") : result;
|
|
151
186
|
}
|
|
@@ -165,7 +200,10 @@ export async function settleTask(world, id) {
|
|
|
165
200
|
if (current.state === "done")
|
|
166
201
|
return { kind: "unchanged" };
|
|
167
202
|
if (current.state === "drop") {
|
|
168
|
-
return {
|
|
203
|
+
return {
|
|
204
|
+
kind: "refused",
|
|
205
|
+
refusal: { kind: "invalid-lifecycle-transition", taskId: id, state: current.state, verb: "done" },
|
|
206
|
+
};
|
|
169
207
|
}
|
|
170
208
|
const at = currentTimestamp();
|
|
171
209
|
const next = {
|
|
@@ -173,7 +211,11 @@ export async function settleTask(world, id) {
|
|
|
173
211
|
state: "done",
|
|
174
212
|
updatedAt: advanceTaskTimestamp(current.updatedAt, at),
|
|
175
213
|
};
|
|
176
|
-
const replaced = await replaceAuthority({
|
|
214
|
+
const replaced = await replaceAuthority({
|
|
215
|
+
path: authorityPath(world, id),
|
|
216
|
+
expected: snapshot.bytes.get(id),
|
|
217
|
+
next: serializeTaskDocument(next),
|
|
218
|
+
});
|
|
177
219
|
return replaced === "replaced"
|
|
178
220
|
? { kind: "changed", task: taskView(next), action: "done" }
|
|
179
221
|
: { kind: "retry", reason: "concurrent-modification" };
|
|
@@ -181,12 +223,15 @@ export async function settleTask(world, id) {
|
|
|
181
223
|
return result === "busy" ? { kind: "retry", reason: "busy" } : result;
|
|
182
224
|
}
|
|
183
225
|
function readScope(namespace, scope) {
|
|
184
|
-
return scope === "world" ? null : namespace ?? [];
|
|
226
|
+
return scope === "world" ? null : (namespace ?? []);
|
|
185
227
|
}
|
|
186
228
|
export async function listTasks(world, namespace, selection, scope, limit = DEFAULT_TASK_LIMIT) {
|
|
187
229
|
const selected = readScope(namespace, scope);
|
|
188
230
|
const board = (await readBoard(world)).board;
|
|
189
|
-
return {
|
|
231
|
+
return {
|
|
232
|
+
kind: "accepted",
|
|
233
|
+
value: projectPage(projectRows(board, createTaskRelations(board), selected, selection), limit),
|
|
234
|
+
};
|
|
190
235
|
}
|
|
191
236
|
export async function readTaskDetails(world, ids) {
|
|
192
237
|
const board = (await readBoard(world)).board;
|
|
@@ -197,6 +242,12 @@ export async function readTaskDetails(world, ids) {
|
|
|
197
242
|
? { kind: "accepted", value: details }
|
|
198
243
|
: { kind: "refused", refusal: { kind: "task-missing", taskId: ids[missing] } };
|
|
199
244
|
}
|
|
245
|
+
export async function observeTaskDetails(world, ids) {
|
|
246
|
+
const observed = await readTaskDetails(world, ids);
|
|
247
|
+
if (observed.kind !== "accepted")
|
|
248
|
+
return observed;
|
|
249
|
+
return { kind: "accepted", value: observed.value.map((facts) => ({ ...facts, task: taskView(facts.task) })) };
|
|
250
|
+
}
|
|
200
251
|
export async function readyTasks(world, namespace, scope, parent, limit = DEFAULT_TASK_LIMIT) {
|
|
201
252
|
const selected = readScope(namespace, scope);
|
|
202
253
|
const board = (await readBoard(world)).board;
|
|
@@ -219,7 +270,10 @@ export async function blockedTasks(world, namespace, scope, parent, limit = DEFA
|
|
|
219
270
|
if (parent !== undefined && !board.tasks.has(parent))
|
|
220
271
|
return { kind: "refused", refusal: { kind: "task-missing", taskId: parent } };
|
|
221
272
|
const relations = createTaskRelations(board);
|
|
222
|
-
const blocked = {
|
|
273
|
+
const blocked = {
|
|
274
|
+
kind: "predicate",
|
|
275
|
+
predicate: { field: "blocked", operator: "=", value: true },
|
|
276
|
+
};
|
|
223
277
|
const expression = parent === undefined ? blocked : { kind: "and", terms: [blocked, underExpression(parent)] };
|
|
224
278
|
const selectedRows = projectQuery(board, relations, {
|
|
225
279
|
scope: selected,
|
package/build/src/task/query.js
CHANGED
|
@@ -8,7 +8,9 @@ function isTaskRelationPredicateField(value) {
|
|
|
8
8
|
}
|
|
9
9
|
const states = ["open", "in_progress", "on_hold", "done", "drop"];
|
|
10
10
|
const comparisons = ["=", "!=", "<", "<=", ">", ">=", "~"];
|
|
11
|
-
function fail(message) {
|
|
11
|
+
function fail(message) {
|
|
12
|
+
throw new TypeError(message);
|
|
13
|
+
}
|
|
12
14
|
function object(value, label) {
|
|
13
15
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
14
16
|
fail(`${label} must be an object`);
|
|
@@ -38,7 +40,9 @@ function comparison(value) {
|
|
|
38
40
|
return value;
|
|
39
41
|
}
|
|
40
42
|
function timestamp(value, field) {
|
|
41
|
-
if (typeof value !== "string" ||
|
|
43
|
+
if (typeof value !== "string" ||
|
|
44
|
+
!Number.isFinite(Date.parse(value)) ||
|
|
45
|
+
new Date(Date.parse(value)).toISOString() !== value) {
|
|
42
46
|
fail(`${field} must be a canonical UTC ISO timestamp`);
|
|
43
47
|
}
|
|
44
48
|
return value;
|
|
@@ -96,15 +100,24 @@ function predicate(value) {
|
|
|
96
100
|
fail("query predicate field is required");
|
|
97
101
|
const operator = comparison(input.operator);
|
|
98
102
|
switch (field) {
|
|
99
|
-
case "state":
|
|
100
|
-
|
|
101
|
-
case "
|
|
102
|
-
|
|
103
|
-
case "
|
|
104
|
-
|
|
105
|
-
case "
|
|
106
|
-
|
|
107
|
-
case "
|
|
103
|
+
case "state":
|
|
104
|
+
return normalizeStatePredicate(input, operator);
|
|
105
|
+
case "priority":
|
|
106
|
+
return normalizePriorityPredicate(input, operator);
|
|
107
|
+
case "title":
|
|
108
|
+
return normalizeTextPredicate(input, "title", operator);
|
|
109
|
+
case "id":
|
|
110
|
+
return normalizeTextPredicate(input, "id", operator);
|
|
111
|
+
case "parent":
|
|
112
|
+
return normalizeParentPredicate(input, operator);
|
|
113
|
+
case "ready":
|
|
114
|
+
return normalizeBooleanPredicate(input, "ready", operator);
|
|
115
|
+
case "blocked":
|
|
116
|
+
return normalizeBooleanPredicate(input, "blocked", operator);
|
|
117
|
+
case "created":
|
|
118
|
+
return normalizeTimestampPredicate(input, "created", operator);
|
|
119
|
+
case "updated":
|
|
120
|
+
return normalizeTimestampPredicate(input, "updated", operator);
|
|
108
121
|
default:
|
|
109
122
|
if (isTaskRelationPredicateField(field))
|
|
110
123
|
return normalizeRelationPredicate(input, field, operator);
|
|
@@ -142,7 +155,9 @@ function compare(left, operator, right) {
|
|
|
142
155
|
return left > right;
|
|
143
156
|
return left >= right;
|
|
144
157
|
}
|
|
145
|
-
function membership(left, operator, right) {
|
|
158
|
+
function membership(left, operator, right) {
|
|
159
|
+
return operator === "=" ? left === right : left !== right;
|
|
160
|
+
}
|
|
146
161
|
function descendants(relations, parent) {
|
|
147
162
|
const result = new Set(), pending = [parent];
|
|
148
163
|
while (pending.length > 0) {
|
|
@@ -170,18 +185,30 @@ function matchRelation(selected, operator) {
|
|
|
170
185
|
}
|
|
171
186
|
function matchesPredicate(board, relations, task, value, under) {
|
|
172
187
|
switch (value.field) {
|
|
173
|
-
case "state":
|
|
174
|
-
|
|
175
|
-
case "
|
|
176
|
-
|
|
177
|
-
case "
|
|
178
|
-
|
|
179
|
-
case "
|
|
180
|
-
|
|
181
|
-
case "
|
|
182
|
-
|
|
183
|
-
case "
|
|
184
|
-
|
|
188
|
+
case "state":
|
|
189
|
+
return compare(task.state, value.operator, value.value);
|
|
190
|
+
case "priority":
|
|
191
|
+
return compare(task.priority, value.operator, value.value);
|
|
192
|
+
case "title":
|
|
193
|
+
return matchText(task.title, value.operator, value.value);
|
|
194
|
+
case "id":
|
|
195
|
+
return matchText(task.id, value.operator, value.value);
|
|
196
|
+
case "parent":
|
|
197
|
+
return compare(task.parent ?? "", value.operator, value.value ?? "");
|
|
198
|
+
case "under":
|
|
199
|
+
return matchRelation(under.get(value.value)?.has(task.id) === true, value.operator);
|
|
200
|
+
case "needs":
|
|
201
|
+
return matchRelation(relation(board, relations, task.id, "needs", value.value), value.operator);
|
|
202
|
+
case "blocks":
|
|
203
|
+
return matchRelation(relation(board, relations, task.id, "blocks", value.value), value.operator);
|
|
204
|
+
case "ready":
|
|
205
|
+
return membership(taskDisposition(board, task) === "ready", value.operator, value.value);
|
|
206
|
+
case "blocked":
|
|
207
|
+
return membership(taskBlocked(board, task), value.operator, value.value);
|
|
208
|
+
case "created":
|
|
209
|
+
return compare(task.createdAt, value.operator, value.value);
|
|
210
|
+
case "updated":
|
|
211
|
+
return compare(task.updatedAt, value.operator, value.value);
|
|
185
212
|
}
|
|
186
213
|
}
|
|
187
214
|
function matches(board, relations, task, expression, under) {
|
|
@@ -200,7 +227,8 @@ function row(board, relations, task) {
|
|
|
200
227
|
parent: task.parent,
|
|
201
228
|
needs: task.needs.map((need) => taskRef(board, need)),
|
|
202
229
|
blocks: relations.blocks(task.id),
|
|
203
|
-
createdAt: task.createdAt,
|
|
230
|
+
createdAt: task.createdAt,
|
|
231
|
+
updatedAt: task.updatedAt,
|
|
204
232
|
};
|
|
205
233
|
}
|
|
206
234
|
function sortTasks(tasks, sort) {
|
|
@@ -217,7 +245,8 @@ function sortTasks(tasks, sort) {
|
|
|
217
245
|
export function projectQuery(board, relations, input) {
|
|
218
246
|
const { scope, expression, sort = "priority", limit = DEFAULT_TASK_LIMIT } = input;
|
|
219
247
|
const under = new Map(queryUnderTargets(expression).map((target) => [target, descendants(relations, target)]));
|
|
220
|
-
const selected = sortTasks([...board.tasks.values()]
|
|
248
|
+
const selected = sortTasks([...board.tasks.values()]
|
|
249
|
+
.filter((task) => scope === null || sameNamespace(parseTaskId(task.id).namespace, scope))
|
|
221
250
|
.filter((task) => matches(board, relations, task, expression, under)), sort);
|
|
222
251
|
const rows = selected.slice(0, limit).map((task) => row(board, relations, task));
|
|
223
252
|
return { rows, total: selected.length, returned: rows.length, truncated: rows.length < selected.length };
|
|
@@ -12,7 +12,9 @@ export declare function replaceAuthority(input: Readonly<{
|
|
|
12
12
|
expected: Uint8Array | null;
|
|
13
13
|
next: Uint8Array;
|
|
14
14
|
}>): Promise<"replaced" | "concurrent-modification">;
|
|
15
|
-
export declare function nukeTaskAuthority(world: WorldRoot
|
|
15
|
+
export declare function nukeTaskAuthority(world: WorldRoot, options?: Readonly<{
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
}>): Promise<void | "busy">;
|
|
16
18
|
export declare function withTaskLocks<T>(input: Readonly<{
|
|
17
19
|
world: WorldRoot;
|
|
18
20
|
allocation: boolean;
|
package/build/src/task/store.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { closeSync, fsyncSync, openSync, renameSync, writeFileSync
|
|
3
|
-
import { lstat, mkdir, readFile, readdir, unlink } from "node:fs/promises";
|
|
2
|
+
import { closeSync, fsyncSync, openSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { lstat, mkdir, readFile, readdir, rmdir, unlink } from "node:fs/promises";
|
|
4
4
|
import { dirname, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { acquireSqliteTransactionLock, SqliteTransactionLockError } from "../coordination/sqlite-transaction-lock.js";
|
|
5
|
+
import { acquireSqliteTransactionLock, SqliteTransactionLockError, } from "../coordination/sqlite-transaction-lock.js";
|
|
6
6
|
import { parseTaskDocument } from "./document.js";
|
|
7
7
|
import { formatTaskId, parseTaskId, taskAuthorityPath } from "./identity.js";
|
|
8
8
|
export const DEFAULT_TASK_LOCK_TIMEOUT_MS = 3_000;
|
|
@@ -17,8 +17,12 @@ function syncTaskDirectory(path) {
|
|
|
17
17
|
closeSync(directory);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
function tasksDirectory(world) {
|
|
21
|
-
|
|
20
|
+
function tasksDirectory(world) {
|
|
21
|
+
return resolve(world, ".keiyaku", "tasks");
|
|
22
|
+
}
|
|
23
|
+
function allocationLockPath(world) {
|
|
24
|
+
return resolve(world, ".keiyaku", "locks", "task-allocation.sqlite");
|
|
25
|
+
}
|
|
22
26
|
async function authorityFiles(directory) {
|
|
23
27
|
const files = [];
|
|
24
28
|
let entries;
|
|
@@ -33,7 +37,7 @@ async function authorityFiles(directory) {
|
|
|
33
37
|
for (const entry of entries) {
|
|
34
38
|
const path = resolve(directory, entry.name);
|
|
35
39
|
if (entry.isDirectory())
|
|
36
|
-
files.push(...await authorityFiles(path));
|
|
40
|
+
files.push(...(await authorityFiles(path)));
|
|
37
41
|
else if (entry.isFile() && entry.name.endsWith(".md"))
|
|
38
42
|
files.push(path);
|
|
39
43
|
}
|
|
@@ -45,6 +49,48 @@ function coordinateFromPath(tasksDirectory, path) {
|
|
|
45
49
|
throw new Error(`invalid Task authority path: ${path}`);
|
|
46
50
|
return parseTaskId(`task/${local.slice(0, -3).split(sep).join("/")}`);
|
|
47
51
|
}
|
|
52
|
+
function ownedTaskId(tasksDirectory, path) {
|
|
53
|
+
try {
|
|
54
|
+
return formatTaskId(coordinateFromPath(tasksDirectory, path));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function ownedAuthorityCandidates(directory) {
|
|
61
|
+
const candidates = [];
|
|
62
|
+
for (const path of await authorityFiles(directory)) {
|
|
63
|
+
const id = ownedTaskId(directory, path);
|
|
64
|
+
if (id !== null)
|
|
65
|
+
candidates.push({ id, path });
|
|
66
|
+
}
|
|
67
|
+
return candidates;
|
|
68
|
+
}
|
|
69
|
+
async function removeEmptyTaskDirectories(directory, removeSelf) {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error.code === "ENOENT")
|
|
76
|
+
return;
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (entry.isDirectory())
|
|
81
|
+
await removeEmptyTaskDirectories(resolve(directory, entry.name), true);
|
|
82
|
+
}
|
|
83
|
+
if (!removeSelf)
|
|
84
|
+
return;
|
|
85
|
+
try {
|
|
86
|
+
await rmdir(directory);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const code = error.code;
|
|
90
|
+
if (code !== "ENOENT" && code !== "ENOTEMPTY")
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
48
94
|
export async function readBoard(world) {
|
|
49
95
|
const directory = tasksDirectory(world);
|
|
50
96
|
const tasks = new Map();
|
|
@@ -98,21 +144,35 @@ export async function replaceAuthority(input) {
|
|
|
98
144
|
try {
|
|
99
145
|
await unlink(temporary);
|
|
100
146
|
}
|
|
101
|
-
catch {
|
|
147
|
+
catch {
|
|
148
|
+
/* renamed or best effort */
|
|
149
|
+
}
|
|
102
150
|
}
|
|
103
151
|
}
|
|
104
152
|
function lockPath(world, id) {
|
|
105
153
|
const coordinate = parseTaskId(id);
|
|
106
154
|
return resolve(world, ".keiyaku", "locks", "task", ...coordinate.namespace, `${coordinate.localId}.sqlite`);
|
|
107
155
|
}
|
|
108
|
-
export async function nukeTaskAuthority(world) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
156
|
+
export async function nukeTaskAuthority(world, options) {
|
|
157
|
+
const directory = tasksDirectory(world);
|
|
158
|
+
const lockOptions = options?.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs };
|
|
159
|
+
const result = await withTaskLocks({ world, allocation: true, ids: [], ...lockOptions }, async () => {
|
|
160
|
+
const candidates = await ownedAuthorityCandidates(directory);
|
|
161
|
+
const ids = [...new Set(candidates.map((candidate) => candidate.id))].sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)));
|
|
162
|
+
return await withTaskLocks({ world, allocation: false, ids, ...lockOptions }, async () => {
|
|
163
|
+
for (const candidate of candidates) {
|
|
164
|
+
try {
|
|
165
|
+
const stat = await lstat(candidate.path);
|
|
166
|
+
if (stat.isFile() && !stat.isSymbolicLink())
|
|
167
|
+
await unlink(candidate.path);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error.code !== "ENOENT")
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
await removeEmptyTaskDirectories(directory, false);
|
|
175
|
+
});
|
|
116
176
|
});
|
|
117
177
|
if (result === "busy")
|
|
118
178
|
return "busy";
|
|
@@ -120,7 +180,9 @@ export async function nukeTaskAuthority(world) {
|
|
|
120
180
|
export async function withTaskLocks(input, action) {
|
|
121
181
|
const paths = [
|
|
122
182
|
...(input.allocation ? [allocationLockPath(input.world)] : []),
|
|
123
|
-
...[...new Set(input.ids)]
|
|
183
|
+
...[...new Set(input.ids)]
|
|
184
|
+
.sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)))
|
|
185
|
+
.map((id) => lockPath(input.world, id)),
|
|
124
186
|
];
|
|
125
187
|
const held = [];
|
|
126
188
|
try {
|
|
@@ -143,4 +205,6 @@ export async function withTaskLocks(input, action) {
|
|
|
143
205
|
lock.close();
|
|
144
206
|
}
|
|
145
207
|
}
|
|
146
|
-
export function authorityPath(world, id) {
|
|
208
|
+
export function authorityPath(world, id) {
|
|
209
|
+
return taskAuthorityPath(tasksDirectory(world), parseTaskId(id));
|
|
210
|
+
}
|
|
@@ -63,7 +63,9 @@ export async function executeVerification(input) {
|
|
|
63
63
|
scratch = await input.materializeScratchCandidate(input.repository, input.candidate);
|
|
64
64
|
}
|
|
65
65
|
catch (error) {
|
|
66
|
-
return {
|
|
66
|
+
return {
|
|
67
|
+
outcome: { kind: "candidate-unavailable", diagnostic: error instanceof Error ? error.message : String(error) },
|
|
68
|
+
};
|
|
67
69
|
}
|
|
68
70
|
let outcome;
|
|
69
71
|
let cleanup;
|
|
@@ -74,16 +76,17 @@ export async function executeVerification(input) {
|
|
|
74
76
|
const hooks = worktreeHooksFrom({ settings: await input.projectSettings(scratch.cwd) });
|
|
75
77
|
destroy = hooks.destroy;
|
|
76
78
|
const readiness = await runHookCommands(scratch.cwd, hooks.create, input.signal);
|
|
77
|
-
outcome =
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
outcome =
|
|
80
|
+
readiness.kind === "cancelled"
|
|
81
|
+
? { kind: "cancelled" }
|
|
82
|
+
: readiness.kind === "failed"
|
|
83
|
+
? { kind: "environment-failure", command: readiness.command, detail: readiness.failure }
|
|
84
|
+
: await executeDeclarations({
|
|
85
|
+
declarations: input.declarations,
|
|
86
|
+
cwd: scratch.cwd,
|
|
87
|
+
environment: input.environment,
|
|
88
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
89
|
+
});
|
|
87
90
|
}
|
|
88
91
|
catch (error) {
|
|
89
92
|
outcome = { kind: "environment-failure", diagnostic: error instanceof Error ? error.message : String(error) };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { lstat, mkdir, readFile, unlink } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { replaceFileDurably } from "./coordination/durable-file.js";
|
|
@@ -312,7 +313,7 @@ async function mutatePlaceRegister(repository, mutate) {
|
|
|
312
313
|
const next = mutate(current);
|
|
313
314
|
const bytes = canonicalPlaceRegister(next);
|
|
314
315
|
try {
|
|
315
|
-
if (await readFile(location.authority, "utf8") === bytes)
|
|
316
|
+
if ((await readFile(location.authority, "utf8")) === bytes)
|
|
316
317
|
return next;
|
|
317
318
|
}
|
|
318
319
|
catch (error) {
|
|
@@ -331,20 +332,31 @@ function withAppointments(register, contracts) {
|
|
|
331
332
|
const additions = [];
|
|
332
333
|
const occupied = new Set(register.byPlace.keys());
|
|
333
334
|
const known = new Set(register.byContract.keys());
|
|
334
|
-
let cursor;
|
|
335
335
|
for (const contract of contracts) {
|
|
336
336
|
if (known.has(contract))
|
|
337
337
|
continue;
|
|
338
|
-
|
|
339
|
-
while (occupied.has(candidate))
|
|
340
|
-
candidate = nextPlace(candidate);
|
|
338
|
+
const candidate = firstUnoccupiedPlace(contract, occupied);
|
|
341
339
|
additions.push({ place: candidate, contract });
|
|
342
340
|
occupied.add(candidate);
|
|
343
341
|
known.add(contract);
|
|
344
|
-
cursor = candidate;
|
|
345
342
|
}
|
|
346
343
|
return additions.length === 0 ? register : indexedRegister([...register.appointments, ...additions]);
|
|
347
344
|
}
|
|
345
|
+
function stableStartIndex(contract) {
|
|
346
|
+
const digest = createHash("sha256").update(contract, "utf8").digest("hex");
|
|
347
|
+
return Number(BigInt(`0x${digest}`) % BigInt(CONTRACT_PLACES.length));
|
|
348
|
+
}
|
|
349
|
+
function firstUnoccupiedPlace(contract, occupied) {
|
|
350
|
+
const start = stableStartIndex(contract);
|
|
351
|
+
for (let generation = 1n;; generation += 1n) {
|
|
352
|
+
for (let offset = 0; offset < CONTRACT_PLACES.length; offset += 1) {
|
|
353
|
+
const base = CONTRACT_PLACES[(start + offset) % CONTRACT_PLACES.length];
|
|
354
|
+
const candidate = composePlace(base, generation);
|
|
355
|
+
if (!occupied.has(candidate))
|
|
356
|
+
return candidate;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
348
360
|
export async function appointManagedWorktrees(repository, contracts) {
|
|
349
361
|
return await mutatePlaceRegister(repository, (current) => withAppointments(current, contracts));
|
|
350
362
|
}
|
|
@@ -385,7 +397,7 @@ export async function nukeEmptyPlaceAuthority(repository) {
|
|
|
385
397
|
}
|
|
386
398
|
export async function readManagedWorktreeAppointment(repository, contract, register) {
|
|
387
399
|
try {
|
|
388
|
-
const snapshot = register ?? await readPlaceRegister(repository);
|
|
400
|
+
const snapshot = register ?? (await readPlaceRegister(repository));
|
|
389
401
|
const appointed = appointmentFor(snapshot, contract);
|
|
390
402
|
return appointed === undefined
|
|
391
403
|
? { kind: "unappointed" }
|
package/build/src/world.js
CHANGED
|
@@ -39,10 +39,14 @@ async function directory(input, label) {
|
|
|
39
39
|
}
|
|
40
40
|
return real;
|
|
41
41
|
}
|
|
42
|
-
function brand(path) {
|
|
43
|
-
|
|
42
|
+
function brand(path) {
|
|
43
|
+
return path;
|
|
44
|
+
}
|
|
45
|
+
function marker(root) {
|
|
46
|
+
return join(root, ".keiyaku");
|
|
47
|
+
}
|
|
44
48
|
async function rejectReservedRoot(root) {
|
|
45
|
-
if (root === await homeRoot()) {
|
|
49
|
+
if (root === (await homeRoot())) {
|
|
46
50
|
throw new WorldError("home-world", "the user home directory cannot be a Keiyaku world");
|
|
47
51
|
}
|
|
48
52
|
if (root === parse(root).root) {
|
|
@@ -130,7 +134,13 @@ async function resolved(input) {
|
|
|
130
134
|
});
|
|
131
135
|
}
|
|
132
136
|
export const World = Object.freeze({
|
|
133
|
-
resolve(input) {
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
resolve(input) {
|
|
138
|
+
return resolved(input);
|
|
139
|
+
},
|
|
140
|
+
async locate(input) {
|
|
141
|
+
return (await resolved(input)).root;
|
|
142
|
+
},
|
|
143
|
+
at(input) {
|
|
144
|
+
return exact(input);
|
|
145
|
+
},
|
|
136
146
|
});
|