@astrosheep/keiyaku 4.1.1 → 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.
Files changed (58) hide show
  1. package/build/src/akuma/akuma-errors.d.ts +6 -0
  2. package/build/src/akuma/akuma-errors.js +9 -0
  3. package/build/src/akuma/akuma-handle.d.ts +39 -0
  4. package/build/src/akuma/akuma-handle.js +263 -0
  5. package/build/src/akuma/akuma-observe.d.ts +19 -0
  6. package/build/src/akuma/akuma-observe.js +74 -0
  7. package/build/src/akuma/akuma-product-symbols.d.ts +1 -0
  8. package/build/src/akuma/akuma-product-symbols.js +1 -0
  9. package/build/src/akuma/akuma-product.d.ts +18 -0
  10. package/build/src/akuma/akuma-product.js +154 -0
  11. package/build/src/akuma/akuma.d.ts +23 -70
  12. package/build/src/akuma/akuma.js +17 -433
  13. package/build/src/akuma/body-supervisor.d.ts +26 -0
  14. package/build/src/akuma/body-supervisor.js +89 -0
  15. package/build/src/akuma/body.d.ts +1 -1
  16. package/build/src/akuma/body.js +3 -2
  17. package/build/src/akuma/projection-read.d.ts +17 -0
  18. package/build/src/akuma/projection-read.js +135 -0
  19. package/build/src/akuma/projection.d.ts +1 -18
  20. package/build/src/akuma/projection.js +21 -155
  21. package/build/src/akuma/request-execution.d.ts +6 -0
  22. package/build/src/akuma/request-execution.js +86 -0
  23. package/build/src/akuma/request-serve.d.ts +12 -1
  24. package/build/src/akuma/request-serve.js +2 -102
  25. package/build/src/akuma/{body-turn.d.ts → turn-drive.d.ts} +2 -25
  26. package/build/src/akuma/{body-turn.js → turn-drive.js} +4 -99
  27. package/build/src/cli/commands/akuma.d.ts +0 -6
  28. package/build/src/cli/commands/akuma.js +1 -11
  29. package/build/src/cli/commands/contract-invoke.d.ts +25 -0
  30. package/build/src/cli/commands/contract-invoke.js +193 -0
  31. package/build/src/cli/commands/contract.d.ts +12 -0
  32. package/build/src/cli/commands/contract.js +229 -0
  33. package/build/src/cli/invoke.js +24 -184
  34. package/build/src/cli/parse.d.ts +1 -1
  35. package/build/src/cli/parse.js +8 -222
  36. package/build/src/cli/render/akuma-activity.d.ts +38 -0
  37. package/build/src/cli/render/akuma-activity.js +362 -0
  38. package/build/src/cli/render/akuma.d.ts +1 -1
  39. package/build/src/cli/render/akuma.js +14 -371
  40. package/build/src/cli/render/contract-history.d.ts +2 -0
  41. package/build/src/cli/render/contract-history.js +83 -0
  42. package/build/src/cli/render/contract.d.ts +1 -2
  43. package/build/src/cli/render/contract.js +1 -83
  44. package/build/src/git/reconcile.d.ts +8 -1
  45. package/build/src/git/reconcile.js +13 -301
  46. package/build/src/git/scratch.d.ts +8 -5
  47. package/build/src/git/scratch.js +29 -2
  48. package/build/src/git/terminal-reconcile.d.ts +21 -0
  49. package/build/src/git/terminal-reconcile.js +278 -0
  50. package/build/src/library/contract-bind.d.ts +26 -0
  51. package/build/src/library/contract-bind.js +106 -0
  52. package/build/src/library/contract-operations.d.ts +77 -0
  53. package/build/src/library/contract-operations.js +130 -0
  54. package/build/src/library/contract-types.d.ts +95 -0
  55. package/build/src/library/contract-types.js +1 -0
  56. package/build/src/library/contract.d.ts +11 -136
  57. package/build/src/library/contract.js +9 -222
  58. package/package.json +1 -1
@@ -0,0 +1,278 @@
1
+ import { access } from "node:fs/promises";
2
+ import { isKeiyakuOwnedRef, readRef, writeCommit, worktreeGitDirectory } from "./repository.js";
3
+ import { gitObjectIdForSnapshot, mintSnapshotId } from "./identity.js";
4
+ import { runDestroyHooks } from "./hooks.js";
5
+ import { terminalSealExpectations as decodeTerminalSealExpectations, terminalSealSnapshots, observeTerminalWorkspace, } from "./terminal-seal.js";
6
+ import { GitPlumbingError, runGit } from "./process.js";
7
+ import { worktreePath } from "./workspace.js";
8
+ const pathExists = (path) => access(path).then(() => true, () => false);
9
+ async function removeTerminalWorktree(repository, topology, path) {
10
+ const registered = topology.paths.has(path);
11
+ if (!registered) {
12
+ return {
13
+ effect: { kind: "worktree", path, action: "unchanged" },
14
+ retained: await pathExists(path),
15
+ };
16
+ }
17
+ if (!(await pathExists(path))) {
18
+ await runGit(repository, ["worktree", "remove", path]);
19
+ topology.paths.delete(path);
20
+ return { effect: { kind: "worktree", path, action: "removed" }, retained: false };
21
+ }
22
+ try {
23
+ await runGit(repository, ["worktree", "remove", "--force", path]);
24
+ }
25
+ catch {
26
+ return { effect: { kind: "worktree", path, action: "unchanged" }, retained: true };
27
+ }
28
+ topology.paths.delete(path);
29
+ return { effect: { kind: "worktree", path, action: "removed" }, retained: false };
30
+ }
31
+ export async function updateRef(repository, ref, desired) {
32
+ const before = await readRef(repository, ref);
33
+ if (before === desired)
34
+ return { kind: "ref", name: ref, action: "unchanged", before, after: desired };
35
+ await runGit(repository, [
36
+ "update-ref",
37
+ "--no-deref",
38
+ ref,
39
+ gitObjectIdForSnapshot(desired),
40
+ before ?? "0".repeat(desired.length),
41
+ ]);
42
+ return { kind: "ref", name: ref, action: before === null ? "created" : "updated", before, after: desired };
43
+ }
44
+ export async function removeRef(repository, ref) {
45
+ const before = await readRef(repository, ref);
46
+ if (before === null)
47
+ return { kind: "ref", name: ref, action: "unchanged", before: null, after: null };
48
+ await runGit(repository, ["update-ref", "--no-deref", "-d", ref, before]);
49
+ return { kind: "ref", name: ref, action: "removed", before, after: null };
50
+ }
51
+ async function removeRefWithCustody(repository, ref, custodian, expectedCustodian) {
52
+ const before = await readRef(repository, ref);
53
+ if (before === null)
54
+ return { kind: "ref", name: ref, action: "unchanged", before: null, after: null };
55
+ if ((await readRef(repository, custodian)) !== expectedCustodian) {
56
+ return { kind: "ref", name: ref, action: "unchanged", before, after: before };
57
+ }
58
+ await runGit(repository, ["update-ref", "--stdin", "--no-deref"], [
59
+ "start",
60
+ `verify ${custodian} ${gitObjectIdForSnapshot(expectedCustodian)}`,
61
+ `delete ${ref} ${before}`,
62
+ "prepare",
63
+ "commit",
64
+ "",
65
+ ].join("\n"));
66
+ return { kind: "ref", name: ref, action: "removed", before, after: null };
67
+ }
68
+ function retainedRefEffect(ref, snapshot) {
69
+ return { kind: "ref", name: ref, action: "unchanged", before: snapshot, after: snapshot };
70
+ }
71
+ async function refRows(repository) {
72
+ const output = (await runGit(repository, ["for-each-ref", "--format=%(refname)%00%(objectname)"])).toString("utf8");
73
+ if (output.length === 0)
74
+ return [];
75
+ return output
76
+ .trimEnd()
77
+ .split("\n")
78
+ .map((row) => {
79
+ const [ref, oid, extra] = row.split("\0");
80
+ if (ref === undefined || oid === undefined || extra !== undefined)
81
+ throw new Error("Git ref row is malformed");
82
+ return { ref, oid };
83
+ });
84
+ }
85
+ async function commitIsAncestor(repository, ancestor, descendant) {
86
+ try {
87
+ await runGit(repository, ["merge-base", "--is-ancestor", gitObjectIdForSnapshot(ancestor), descendant]);
88
+ return true;
89
+ }
90
+ catch (error) {
91
+ if (error instanceof GitPlumbingError && error.status === 1)
92
+ return false;
93
+ throw error;
94
+ }
95
+ }
96
+ async function snapshotCustodian(repository, snapshot) {
97
+ for (const row of await refRows(repository)) {
98
+ if (isKeiyakuOwnedRef(row.ref))
99
+ continue;
100
+ if (await commitIsAncestor(repository, snapshot, row.oid))
101
+ return { ref: row.ref, oid: mintSnapshotId(row.oid) };
102
+ }
103
+ return null;
104
+ }
105
+ function fromPrimaryWorktree(repository) {
106
+ return { ...repository, effectiveCwd: repository.primaryWorktree };
107
+ }
108
+ function complete(effects = [], lag = []) {
109
+ return { effects, lag };
110
+ }
111
+ function retainTerminalWorktree(path, retainedLag, { effects, lag }) {
112
+ effects.push({ kind: "worktree", path, action: "unchanged" });
113
+ lag.push(retainedLag);
114
+ return complete(effects, lag);
115
+ }
116
+ async function terminalSealExpectations(channel, state) {
117
+ const snapshots = terminalSealSnapshots(state);
118
+ const objects = await channel.readObjects(snapshots.map(gitObjectIdForSnapshot));
119
+ return decodeTerminalSealExpectations(state, objects);
120
+ }
121
+ async function removeSealedTerminalWorktree({ repository, topology, path, state, expected, hooks, retryHooks, acc, }) {
122
+ if (topology.paths.has(path) && (await pathExists(path))) {
123
+ const terminal = state.terminal;
124
+ if (terminal === null)
125
+ throw new Error(`terminal worktree cleanup received active Contract ${state.id}`);
126
+ const canRecover = terminal.kind === "abandoned";
127
+ let recovery = null;
128
+ const recordRecovery = async (workspace) => {
129
+ const snapshot = mintSnapshotId(await writeCommit({
130
+ repository,
131
+ tree: workspace.tree,
132
+ parent: gitObjectIdForSnapshot(recovery?.snapshot ?? workspace.head),
133
+ message: `${state.id}: ephemeral abandoned-workspace recovery\n\nKeiyaku-Contract: ${state.id}`,
134
+ actor: "Keiyaku Recovery",
135
+ at: terminal.at,
136
+ }));
137
+ const effect = {
138
+ kind: "recovery-snapshot",
139
+ action: "created",
140
+ snapshot,
141
+ retention: "ephemeral",
142
+ };
143
+ const existing = acc.effects.findIndex((item) => item.kind === "recovery-snapshot");
144
+ if (existing < 0)
145
+ acc.effects.push(effect);
146
+ else
147
+ acc.effects[existing] = effect;
148
+ return { snapshot, workspace };
149
+ };
150
+ const before = await observeTerminalWorkspace(repository, path, expected);
151
+ const beforeWorkspace = before.workspace;
152
+ const beforeHooks = before.unsealed;
153
+ if (beforeHooks !== null) {
154
+ if (!canRecover || beforeWorkspace.submodules.length > 0) {
155
+ return retainTerminalWorktree(path, beforeHooks, acc);
156
+ }
157
+ recovery = await recordRecovery(beforeWorkspace);
158
+ }
159
+ const hookLag = await runDestroyHooks(path, await worktreeGitDirectory(repository, path), hooks, retryHooks);
160
+ if (hookLag !== null)
161
+ return retainTerminalWorktree(path, hookLag, acc);
162
+ const after = await observeTerminalWorkspace(repository, path, expected);
163
+ const afterWorkspace = after.workspace;
164
+ const afterHooks = after.unsealed;
165
+ if (afterHooks !== null) {
166
+ if (!canRecover || afterWorkspace.submodules.length > 0) {
167
+ return retainTerminalWorktree(path, afterHooks, acc);
168
+ }
169
+ }
170
+ if (canRecover &&
171
+ (recovery === null ||
172
+ recovery.workspace.head !== afterWorkspace.head ||
173
+ recovery.workspace.tree !== afterWorkspace.tree) &&
174
+ (recovery !== null ||
175
+ beforeWorkspace.head !== afterWorkspace.head ||
176
+ beforeWorkspace.tree !== afterWorkspace.tree)) {
177
+ recovery = await recordRecovery(afterWorkspace);
178
+ }
179
+ }
180
+ const removal = await removeTerminalWorktree(repository, topology, path);
181
+ if (removal.retained)
182
+ return retainTerminalWorktree(path, { kind: "worktree-retained", path }, acc);
183
+ acc.effects.push(removal.effect);
184
+ return null;
185
+ }
186
+ async function targetCustodyForClaimedIntegration(repository, state, integration) {
187
+ if (state.terminal?.kind !== "claimed" || state.coordinates.target === undefined)
188
+ return null;
189
+ const target = await readRef(repository, state.coordinates.target);
190
+ if (target === null)
191
+ return null;
192
+ const oid = mintSnapshotId(target);
193
+ return (await commitIsAncestor(repository, integration, oid)) ? { ref: state.coordinates.target, oid } : null;
194
+ }
195
+ function sealedTree(expected, snapshot) {
196
+ const tree = expected.treeBySnapshot.get(snapshot);
197
+ if (tree === undefined)
198
+ throw new Error(`terminal seal tree was not resolved: ${snapshot}`);
199
+ return tree;
200
+ }
201
+ async function releaseTerminalCustody({ repository, state, resolveSeal, ref, pin, acc: { effects }, }) {
202
+ const [deliveryRef, candidatePin] = await Promise.all([readRef(repository, ref), readRef(repository, pin)]);
203
+ if (deliveryRef === null && candidatePin === null)
204
+ return;
205
+ if (state.delivery === null) {
206
+ const custodian = await snapshotCustodian(repository, state.coordinates.start);
207
+ if (deliveryRef !== null) {
208
+ effects.push(custodian === null
209
+ ? retainedRefEffect(ref, deliveryRef)
210
+ : await removeRefWithCustody(repository, ref, custodian.ref, custodian.oid));
211
+ }
212
+ if (candidatePin !== null)
213
+ effects.push(await removeRef(repository, pin));
214
+ return;
215
+ }
216
+ const tender = state.delivery.data.tenderSnapshot;
217
+ const integration = state.currentIntegration?.snapshot ?? state.delivery.data.integration.snapshot;
218
+ const target = await targetCustodyForClaimedIntegration(repository, state, integration);
219
+ if (deliveryRef !== null && target !== null) {
220
+ if (tender === integration) {
221
+ effects.push(await removeRefWithCustody(repository, ref, target.ref, target.oid));
222
+ }
223
+ else {
224
+ const expected = await resolveSeal();
225
+ if (sealedTree(expected, tender) === sealedTree(expected, integration)) {
226
+ effects.push(await removeRefWithCustody(repository, ref, target.ref, target.oid));
227
+ }
228
+ else {
229
+ effects.push(retainedRefEffect(ref, deliveryRef));
230
+ }
231
+ }
232
+ }
233
+ else if (deliveryRef !== null) {
234
+ effects.push(retainedRefEffect(ref, deliveryRef));
235
+ }
236
+ if (candidatePin !== null) {
237
+ if (target !== null || (tender === integration && deliveryRef !== null)) {
238
+ effects.push(target !== null
239
+ ? await removeRefWithCustody(repository, pin, target.ref, target.oid)
240
+ : await removeRefWithCustody(repository, pin, ref, tender));
241
+ }
242
+ else {
243
+ effects.push(retainedRefEffect(pin, candidatePin));
244
+ }
245
+ }
246
+ }
247
+ export async function reconcileTerminalManagedWorktree({ repository, channel, hooks, retryHooks, retainTerminalWorktree, place }, state, topology, acc, { ref, pin }) {
248
+ const primary = fromPrimaryWorktree(repository);
249
+ const path = place === undefined ? undefined : worktreePath(repository, place);
250
+ const resolveSeal = async () => await terminalSealExpectations(channel, state);
251
+ if (path === undefined) {
252
+ if (retainTerminalWorktree === true)
253
+ return complete(acc.effects, acc.lag);
254
+ await releaseTerminalCustody({ repository: primary, state, resolveSeal, ref, pin, acc });
255
+ return complete(acc.effects, acc.lag);
256
+ }
257
+ const expected = await resolveSeal();
258
+ acc.effects.push(await updateRef(primary, ref, state.delivery?.data.tenderSnapshot ?? state.coordinates.start));
259
+ if (state.delivery !== null) {
260
+ acc.effects.push(await updateRef(primary, pin, state.currentIntegration?.snapshot ?? state.delivery.data.integration.snapshot));
261
+ }
262
+ if (retainTerminalWorktree === true)
263
+ return complete(acc.effects, acc.lag);
264
+ const retained = await removeSealedTerminalWorktree({
265
+ repository: primary,
266
+ topology,
267
+ path,
268
+ state,
269
+ expected,
270
+ hooks,
271
+ retryHooks,
272
+ acc,
273
+ });
274
+ if (retained !== null)
275
+ return retained;
276
+ await releaseTerminalCustody({ repository: primary, state, resolveSeal: async () => expected, ref, pin, acc });
277
+ return complete(acc.effects, acc.lag);
278
+ }
@@ -0,0 +1,26 @@
1
+ import { type ContractId } from "../core/facts/types.js";
2
+ import { type MutationResult } from "./mutation.js";
3
+ import { type RegionObservation } from "./region.js";
4
+ import { Repo } from "./repo.js";
5
+ import type { WorktreeHooks } from "./configuration.js";
6
+ import type { RepositoryScope } from "../protocol/operations.js";
7
+ type BindInput = Readonly<{
8
+ repo: Repo;
9
+ markdown?: unknown;
10
+ forkOf?: unknown;
11
+ task?: unknown;
12
+ target?: unknown;
13
+ workspace?: unknown;
14
+ actor?: unknown;
15
+ after?: unknown;
16
+ gates?: unknown;
17
+ hooks?: WorktreeHooks;
18
+ }>;
19
+ type BindResult<Handle> = Readonly<Omit<MutationResult<Handle>, "value"> & {
20
+ keiyaku: Handle;
21
+ } & RegionObservation>;
22
+ type HandleFactory<Handle> = (contractId: ContractId, scope: RepositoryScope) => Handle;
23
+ export declare function bindKeiyaku<Handle>(input: BindInput, createHandle: HandleFactory<Handle>): Promise<BindResult<Handle>>;
24
+ /** Internal CLI composition; not exported from the package root. */
25
+ export declare function bindFromCli<Handle>(input: BindInput, createHandle: HandleFactory<Handle>): Promise<BindResult<Handle>>;
26
+ export {};
@@ -0,0 +1,106 @@
1
+ import { decodeContractDocument } from "../body/decode.js";
2
+ import { contractId } from "../core/facts/types.js";
3
+ import { withGitDecodeChannel } from "../git/read-observation.js";
4
+ import { admitForkBindWithAppointment, prepareMarkdownBind } from "./bind.js";
5
+ import { actorOption, normalizedGates, normalizedList, requireInput, requireMarkdown, taskOption } from "./input.js";
6
+ import { completionInput, completeHolderMutation, completeMutation } from "./mutation.js";
7
+ import { requireAccepted } from "./refusal.js";
8
+ import { observeRegion } from "./region.js";
9
+ import { scopeForRepo } from "./repo.js";
10
+ import { worktreeHooksOption } from "./configuration.js";
11
+ function acceptedBindResult(result, region) {
12
+ const { facts, head, effects, lags, settlement } = result;
13
+ return { facts, head, keiyaku: result.value, effects, lags, settlement, ...region };
14
+ }
15
+ export async function bindKeiyaku(input, createHandle) {
16
+ const values = requireInput(input, "Keiyaku.bind input");
17
+ const hooks = worktreeHooksOption(values.hooks);
18
+ const forkOf = values.forkOf;
19
+ if (forkOf !== undefined) {
20
+ for (const key of Object.keys(values)) {
21
+ if (!["repo", "forkOf", "target", "workspace", "actor", "hooks"].includes(key)) {
22
+ throw new TypeError(`fork bind input has unknown field: ${key}`);
23
+ }
24
+ }
25
+ if (typeof forkOf !== "string")
26
+ throw new TypeError("forkOf must be a ContractId");
27
+ let sourceId;
28
+ try {
29
+ sourceId = contractId(forkOf);
30
+ }
31
+ catch (error) {
32
+ throw new TypeError(error instanceof Error ? error.message : "forkOf must be a ContractId");
33
+ }
34
+ const sourceScope = scopeForRepo(values.repo);
35
+ if ((values.workspace ?? "worktree") !== "worktree")
36
+ throw new TypeError("workspace must be worktree");
37
+ const target = values.target;
38
+ if (target !== undefined && typeof target !== "string")
39
+ throw new TypeError("target must be a string");
40
+ const actor = actorOption(values.actor);
41
+ return withGitDecodeChannel(sourceScope, async (channel) => {
42
+ const fork = await admitForkBindWithAppointment({
43
+ scope: sourceScope,
44
+ channel,
45
+ sourceId,
46
+ ...(target === undefined ? {} : { target }),
47
+ ...actor,
48
+ });
49
+ const admission = requireAccepted(fork.admission);
50
+ const id = admission.value.contractId;
51
+ const toHandle = ({ contractId: contract }) => createHandle(contract, sourceScope);
52
+ const result = await completeMutation({
53
+ ...completionInput(sourceScope, channel, id, toHandle, hooks),
54
+ accepted: admission,
55
+ });
56
+ return acceptedBindResult(result, await observeRegion(sourceScope, channel, id, fork.document.region));
57
+ });
58
+ }
59
+ return bindMarkdownFromValues(values, "targetless", createHandle);
60
+ }
61
+ /** Internal CLI composition; not exported from the package root. */
62
+ export async function bindFromCli(input, createHandle) {
63
+ const values = requireInput(input, "Keiyaku.bind input");
64
+ return values.forkOf !== undefined
65
+ ? bindKeiyaku(input, createHandle)
66
+ : bindMarkdownFromValues(values, "current-branch", createHandle);
67
+ }
68
+ async function bindMarkdownFromValues(values, omittedTarget, createHandle) {
69
+ const hooks = worktreeHooksOption(values.hooks);
70
+ const scope = scopeForRepo(values.repo);
71
+ const task = taskOption(values.task);
72
+ const document = decodeContractDocument(requireMarkdown(values.markdown));
73
+ if ((values.workspace ?? "worktree") !== "worktree")
74
+ throw new TypeError("workspace must be worktree");
75
+ const target = values.target;
76
+ if (target !== undefined && typeof target !== "string")
77
+ throw new TypeError("target must be a string");
78
+ const actor = actorOption(values.actor);
79
+ const gates = normalizedGates(values.gates);
80
+ const after = normalizedList(values.after, "after", contractId);
81
+ const targetSelection = target !== undefined ? { kind: "explicit", target } : { kind: omittedTarget };
82
+ return withGitDecodeChannel(scope, async (channel) => {
83
+ const admission = await prepareMarkdownBind({
84
+ scope,
85
+ channel,
86
+ document,
87
+ gates,
88
+ after,
89
+ workspace: "worktree",
90
+ targetSelection,
91
+ ...(task === undefined ? {} : { task }),
92
+ ...actor,
93
+ });
94
+ const accepted = requireAccepted(admission.admission === null ? admission.result : admission.admission.result);
95
+ const id = accepted.value.contractId;
96
+ const toHandle = ({ contractId: contract }) => createHandle(contract, scope);
97
+ const result = admission.admission === null
98
+ ? await completeMutation({ ...completionInput(scope, channel, id, toHandle, hooks), accepted })
99
+ : await completeHolderMutation({
100
+ completion: completionInput(scope, channel, id, toHandle, hooks),
101
+ admission: admission.admission,
102
+ requireAccepted,
103
+ });
104
+ return acceptedBindResult(result, await observeRegion(scope, channel, id, document.region));
105
+ });
106
+ }
@@ -0,0 +1,77 @@
1
+ import { type RepositoryScope } from "../protocol/operations.js";
2
+ import { type IntegrationConflictMaterialized } from "../protocol/deliver.js";
3
+ import { type ReviewValue } from "../protocol/review.js";
4
+ import { type ContinuationReport } from "./continuation.js";
5
+ import { type MutationResult } from "./mutation.js";
6
+ import { type KeiyakuRefusal, type KeiyakuRetryReason } from "./refusal.js";
7
+ import { actorOption } from "./input.js";
8
+ import { type WorktreeHooks } from "./configuration.js";
9
+ import { Repo } from "./repo.js";
10
+ import { type ContractId } from "../core/facts/types.js";
11
+ import type { UpstreamRequestOutcome } from "../akuma/requests.js";
12
+ import type { DeliveryValue } from "./delivery.js";
13
+ export type DeliveryExecutionInput = Readonly<{
14
+ scope: RepositoryScope;
15
+ contractId: ContractId;
16
+ actor?: ReturnType<typeof actorOption>["actor"];
17
+ message?: string;
18
+ requireBranchesToBeUpToDate: boolean;
19
+ includeDirty: boolean;
20
+ materializeConflict: boolean;
21
+ signal?: AbortSignal;
22
+ hooks: WorktreeHooks;
23
+ }>;
24
+ export type ReviewExecutionInput = Readonly<{
25
+ scope: RepositoryScope;
26
+ contractId: ContractId;
27
+ actor?: ReturnType<typeof actorOption>["actor"];
28
+ verdict: AttestationVerdict;
29
+ summary?: string;
30
+ hooks: WorktreeHooks;
31
+ }>;
32
+ export type AttestationVerdict = "satisfied" | "unsatisfied";
33
+ export type Review = ReviewValue & Readonly<{
34
+ continuation?: ContinuationReport;
35
+ }>;
36
+ export type ForwardedMutationReceipt<Value> = Readonly<{
37
+ kind: "accepted";
38
+ result: MutationResult<Value>;
39
+ }> | Readonly<{
40
+ kind: "refused";
41
+ refusal: KeiyakuRefusal;
42
+ }> | Readonly<{
43
+ kind: "retry";
44
+ reason: KeiyakuRetryReason;
45
+ }>;
46
+ export type ForwardedDeliveryReceipt = ForwardedMutationReceipt<DeliveryValue> | IntegrationConflictMaterialized;
47
+ export type ForwardedReviewReceipt = ForwardedMutationReceipt<Review>;
48
+ export declare function executeLocalDelivery(input: DeliveryExecutionInput): Promise<MutationResult<DeliveryValue> | IntegrationConflictMaterialized>;
49
+ export declare function executeLocalReview(input: ReviewExecutionInput): Promise<MutationResult<Review>>;
50
+ export declare function forwardedReceipt<Receipt>(outcome: UpstreamRequestOutcome, action: string): Receipt;
51
+ export declare function requireForwarded(receipt: ForwardedDeliveryReceipt): MutationResult<DeliveryValue> | IntegrationConflictMaterialized;
52
+ export declare function requireForwarded(receipt: ForwardedReviewReceipt): MutationResult<Review>;
53
+ export declare function executeForwardedDeliver(input: Readonly<{
54
+ repo: Repo;
55
+ contractId: string;
56
+ requester: string;
57
+ message?: string;
58
+ includeDirty: boolean;
59
+ materializeConflict: boolean;
60
+ requireBranchesToBeUpToDate: boolean;
61
+ hooks: WorktreeHooks;
62
+ signal?: AbortSignal;
63
+ }>): Promise<Readonly<{
64
+ result: ForwardedDeliveryReceipt;
65
+ deliveryFactId?: string;
66
+ }>>;
67
+ export declare function executeForwardedReview(input: Readonly<{
68
+ repo: Repo;
69
+ contractId: string;
70
+ requester: string;
71
+ verdict: AttestationVerdict;
72
+ summary?: string;
73
+ hooks: WorktreeHooks;
74
+ }>): Promise<Readonly<{
75
+ result: ForwardedReviewReceipt;
76
+ reviewFactId?: string;
77
+ }>>;
@@ -0,0 +1,130 @@
1
+ import { decodeContractDocument } from "../body/decode.js";
2
+ import { deliverOperation } from "../protocol/deliver.js";
3
+ import { reviewOperation } from "../protocol/review.js";
4
+ import { continueAcceptedCompletion } from "./continuation.js";
5
+ import { completionInput, completeMutation } from "./mutation.js";
6
+ import { forwardedMutationFailure, KeiyakuRefused, KeiyakuRetry, requireAccepted, } from "./refusal.js";
7
+ import { actorOption, documentDerivation, optionalNonblank } from "./input.js";
8
+ import { scopeForRepo } from "./repo.js";
9
+ import { contractId } from "../core/facts/types.js";
10
+ import { withGitDecodeChannel } from "../git/read-observation.js";
11
+ function derivedDocument(state) {
12
+ return documentDerivation(decodeContractDocument(state.terms.document.bytes), state.terms.gates, state.id);
13
+ }
14
+ function operationContext(scope, channel, contractId) {
15
+ return { scope, channel, contractId, deriveDocument: derivedDocument };
16
+ }
17
+ export async function executeLocalDelivery(input) {
18
+ return withGitDecodeChannel(input.scope, async (channel) => {
19
+ const operation = operationContext(input.scope, channel, input.contractId);
20
+ const outcome = await deliverOperation({
21
+ ...operation,
22
+ ...(input.actor === undefined ? {} : { actor: input.actor }),
23
+ ...(input.message === undefined ? {} : { message: input.message }),
24
+ requireBranchesToBeUpToDate: input.requireBranchesToBeUpToDate,
25
+ includeDirty: input.includeDirty,
26
+ materializeConflict: input.materializeConflict,
27
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
28
+ });
29
+ if (outcome.kind === "integration-conflict-materialized")
30
+ return outcome;
31
+ const accepted = requireAccepted(outcome);
32
+ const continued = await continueAcceptedCompletion({
33
+ ...operation,
34
+ accepted,
35
+ ...(input.actor === undefined ? {} : { actor: input.actor }),
36
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
37
+ });
38
+ return completeMutation({
39
+ ...completionInput(input.scope, channel, input.contractId, (delivery) => delivery, input.hooks),
40
+ accepted: continued,
41
+ });
42
+ });
43
+ }
44
+ export async function executeLocalReview(input) {
45
+ return withGitDecodeChannel(input.scope, async (channel) => {
46
+ const operation = operationContext(input.scope, channel, input.contractId);
47
+ const accepted = requireAccepted(await reviewOperation({
48
+ ...operation,
49
+ verdict: input.verdict,
50
+ ...(input.summary === undefined ? {} : { summary: input.summary }),
51
+ ...(input.actor === undefined ? {} : { actor: input.actor }),
52
+ }));
53
+ const continued = await continueAcceptedCompletion({
54
+ ...operation,
55
+ accepted,
56
+ ...(input.actor === undefined ? {} : { actor: input.actor }),
57
+ });
58
+ return completeMutation({
59
+ ...completionInput(input.scope, channel, input.contractId, (review) => review, input.hooks),
60
+ accepted: continued,
61
+ });
62
+ });
63
+ }
64
+ export function forwardedReceipt(outcome, action) {
65
+ if (outcome.kind === "failed") {
66
+ throw new Error(outcome.failure.kind === "failed"
67
+ ? outcome.failure.diagnostic
68
+ : `Unexpected Akuma target failure for ${action}: ${outcome.failure.id}`);
69
+ }
70
+ return outcome.result;
71
+ }
72
+ export function requireForwarded(receipt) {
73
+ if (receipt.kind === "refused")
74
+ throw new KeiyakuRefused(receipt.refusal);
75
+ if (receipt.kind === "retry")
76
+ throw new KeiyakuRetry(receipt.reason);
77
+ return receipt.kind === "accepted" ? receipt.result : receipt;
78
+ }
79
+ export async function executeForwardedDeliver(input) {
80
+ const id = contractId(input.contractId);
81
+ const actor = actorOption(input.requester).actor;
82
+ const message = optionalNonblank(input.message, "deliver message");
83
+ try {
84
+ const result = await executeLocalDelivery({
85
+ scope: scopeForRepo(input.repo),
86
+ contractId: id,
87
+ ...(actor === undefined ? {} : { actor }),
88
+ ...(message === undefined ? {} : { message }),
89
+ requireBranchesToBeUpToDate: input.requireBranchesToBeUpToDate,
90
+ includeDirty: input.includeDirty,
91
+ materializeConflict: input.materializeConflict,
92
+ ...(input.signal === undefined ? {} : { signal: input.signal }),
93
+ hooks: input.hooks,
94
+ });
95
+ if (!("facts" in result))
96
+ return { result };
97
+ const delivery = result.facts.find((fact) => fact.kind === "deliver");
98
+ if (delivery === undefined)
99
+ throw new Error("accepted delivery is missing its journal fact");
100
+ return { result: { kind: "accepted", result }, deliveryFactId: delivery.entry };
101
+ }
102
+ catch (error) {
103
+ return forwardedMutationFailure(error);
104
+ }
105
+ }
106
+ export async function executeForwardedReview(input) {
107
+ const id = contractId(input.contractId);
108
+ const actor = actorOption(input.requester).actor;
109
+ if (input.verdict !== "satisfied" && input.verdict !== "unsatisfied") {
110
+ throw new TypeError("verdict must be satisfied or unsatisfied");
111
+ }
112
+ const summary = optionalNonblank(input.summary, "review summary");
113
+ try {
114
+ const result = await executeLocalReview({
115
+ scope: scopeForRepo(input.repo),
116
+ contractId: id,
117
+ ...(actor === undefined ? {} : { actor }),
118
+ verdict: input.verdict,
119
+ ...(summary === undefined ? {} : { summary }),
120
+ hooks: input.hooks,
121
+ });
122
+ const review = result.facts.find((fact) => fact.kind === "attestation");
123
+ if (review === undefined)
124
+ throw new Error("accepted review is missing its journal fact");
125
+ return { result: { kind: "accepted", result }, reviewFactId: review.entry };
126
+ }
127
+ catch (error) {
128
+ return forwardedMutationFailure(error);
129
+ }
130
+ }