@astrosheep/keiyaku 2.9.12 → 2.9.13

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 (212) hide show
  1. package/README.md +22 -18
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/call-terms.js +1 -0
  4. package/build/agents/providers/claude-agent-sdk/adapter.js +2 -2
  5. package/build/cli/commands/akuma/akuma/handler.js +2 -1
  6. package/build/cli/commands/akuma/akuma/meta.js +4 -3
  7. package/build/cli/commands/akuma/catalog.js +2 -0
  8. package/build/cli/commands/akuma/list/handler.js +1 -1
  9. package/build/cli/commands/akuma/list/meta-list.js +12 -0
  10. package/build/cli/commands/akuma/list/meta-ls.js +2 -2
  11. package/build/cli/commands/akuma.js +5 -0
  12. package/build/cli/commands/contract/amend/meta.js +4 -3
  13. package/build/cli/commands/contract/arc/meta.js +2 -0
  14. package/build/cli/commands/contract/audit/handler.js +28 -2
  15. package/build/cli/commands/contract/audit/meta.js +4 -3
  16. package/build/cli/commands/contract/petition/handler.js +2 -1
  17. package/build/cli/commands/contract/petition/meta.js +2 -2
  18. package/build/cli/commands/contract/renew/handler.js +44 -3
  19. package/build/cli/commands/contract/renew/meta.js +5 -4
  20. package/build/cli/commands/metadata.js +12 -9
  21. package/build/cli/commands/projection/call/handler.js +1 -1
  22. package/build/cli/commands/projection/call/meta.js +1 -1
  23. package/build/cli/commands/projection/catalog.js +2 -2
  24. package/build/cli/commands/projection/{revive → fork}/handler.js +10 -5
  25. package/build/cli/commands/projection/fork/meta.js +12 -0
  26. package/build/cli/commands/projection/history/handler.js +46 -2
  27. package/build/cli/commands/projection/history/meta.js +3 -3
  28. package/build/cli/commands/projection/kill/meta.js +2 -2
  29. package/build/cli/commands/projection/status/handler.js +29 -13
  30. package/build/cli/commands/projection/status/meta.js +14 -3
  31. package/build/cli/commands/projection/tell/handler.js +8 -4
  32. package/build/cli/commands/projection/tell/meta.js +1 -1
  33. package/build/cli/commands/projection/wait/handler.js +45 -20
  34. package/build/cli/commands/projection/wait/meta.js +1 -1
  35. package/build/cli/commands/shared.js +1 -1
  36. package/build/cli/commands/system/catalog.js +2 -0
  37. package/build/cli/commands/task/add/handler.js +28 -12
  38. package/build/cli/commands/task/add/meta.js +18 -6
  39. package/build/cli/commands/task/catalog.js +2 -0
  40. package/build/cli/commands/task/compose/handler.js +2 -2
  41. package/build/cli/commands/task/compose/meta.js +2 -2
  42. package/build/cli/commands/task/doctor/handler.js +2 -3
  43. package/build/cli/commands/task/doctor/meta.js +2 -2
  44. package/build/cli/commands/task/done/meta.js +2 -2
  45. package/build/cli/commands/task/drop/handler.js +7 -2
  46. package/build/cli/commands/task/drop/meta.js +6 -3
  47. package/build/cli/commands/task/hold/meta.js +2 -2
  48. package/build/cli/commands/task/log/meta.js +2 -2
  49. package/build/cli/commands/task/ls/meta.js +2 -2
  50. package/build/cli/commands/task/note/handler.js +6 -0
  51. package/build/cli/commands/task/note/meta.js +11 -0
  52. package/build/cli/commands/task/resume/meta.js +2 -2
  53. package/build/cli/commands/task/shared.js +29 -15
  54. package/build/cli/commands/task/show/meta.js +2 -2
  55. package/build/cli/commands/task/start/meta.js +2 -2
  56. package/build/cli/commands/task/stop/meta.js +2 -2
  57. package/build/cli/commands/task/update/meta.js +3 -2
  58. package/build/cli/commands/verification/handler.js +43 -0
  59. package/build/cli/commands/verification/meta.js +13 -0
  60. package/build/cli/completion.js +16 -13
  61. package/build/cli/flags.js +43 -11
  62. package/build/cli/help.js +29 -30
  63. package/build/cli/index.js +6 -5
  64. package/build/cli/parse-flags.js +39 -0
  65. package/build/cli/parse-metadata.js +5 -2
  66. package/build/cli/parse-selectors.js +18 -0
  67. package/build/cli/parse.js +21 -8
  68. package/build/cli/render/arc.js +12 -3
  69. package/build/cli/render/audit.js +2 -2
  70. package/build/cli/render/call.js +4 -4
  71. package/build/cli/render/kanshi.js +122 -85
  72. package/build/cli/render/misc.js +2 -2
  73. package/build/cli/render/petition.js +16 -1
  74. package/build/cli/render/projection-history.js +17 -4
  75. package/build/cli/render/shared.js +5 -5
  76. package/build/cli/render/status-indicator.js +47 -0
  77. package/build/cli/render/status.js +106 -76
  78. package/build/cli/render/success-response.js +20 -5
  79. package/build/cli/render/task.js +6 -5
  80. package/build/cli/render/tell.js +10 -4
  81. package/build/cli/render/tool-command-normalization.js +137 -0
  82. package/build/cli/render/tool-presentation.js +2 -1
  83. package/build/cli/render/verification.js +23 -0
  84. package/build/cli/render/wait.js +101 -33
  85. package/build/cli/types.js +2 -1
  86. package/build/config/akuma-loader.js +21 -1
  87. package/build/config/settings/knobs.js +7 -1
  88. package/build/config/settings/schema.js +10 -5
  89. package/build/core/amend.js +102 -17
  90. package/build/core/arc.js +4 -4
  91. package/build/core/audit/candidate.js +10 -18
  92. package/build/core/audit/coordinates.js +12 -17
  93. package/build/core/audit/evidence.js +6 -4
  94. package/build/core/audit/facade.js +3 -3
  95. package/build/core/audit/report.js +1 -1
  96. package/build/core/bind-reconciliation.js +42 -23
  97. package/build/core/bind.js +182 -81
  98. package/build/core/call/call.js +6 -7
  99. package/build/core/call/context.js +43 -30
  100. package/build/core/call/execution.js +21 -6
  101. package/build/core/call/prompt.js +11 -42
  102. package/build/core/contract-carrier-runtime.js +209 -105
  103. package/build/core/contract-carrier.js +156 -73
  104. package/build/core/contract-view.js +42 -5
  105. package/build/core/contract.js +2 -1
  106. package/build/core/derived-replay.js +168 -35
  107. package/build/core/draft.js +9 -5
  108. package/build/core/forfeit.js +1 -1
  109. package/build/core/lifecycle-history.js +13 -0
  110. package/build/core/lifecycle-recovery.js +32 -27
  111. package/build/core/lifecycle-runner.js +14 -14
  112. package/build/core/log.js +1 -1
  113. package/build/core/outcome-base.js +2 -2
  114. package/build/core/projection/akuma-name.js +43 -0
  115. package/build/core/projection/generation/database.js +141 -79
  116. package/build/core/projection/generation/ledger.js +91 -0
  117. package/build/core/projection/generation/model.js +18 -13
  118. package/build/core/projection/generation/projection-generation-continuation.js +1 -1
  119. package/build/core/projection/generation/projection-generation-doorbell.js +88 -0
  120. package/build/core/projection/generation/projection-generation-execution.js +24 -0
  121. package/build/core/projection/generation/projection-generation-identity.js +4 -0
  122. package/build/core/projection/generation/projection-generation-launcher.js +72 -138
  123. package/build/core/projection/generation/projection-generation-process.js +16 -15
  124. package/build/core/projection/generation/projection-generation-runner.js +125 -79
  125. package/build/core/projection/generation/projection-generation-runtime.js +10 -61
  126. package/build/core/projection/generation/protocol.js +68 -0
  127. package/build/core/projection/generation/store.js +17 -7
  128. package/build/core/projection/generation/transitions.js +14 -14
  129. package/build/core/projection/index.js +5 -5
  130. package/build/core/projection/projection-core.js +6 -2
  131. package/build/core/projection/projection-execution-observer.js +1 -1
  132. package/build/core/projection/projection-history.js +7 -7
  133. package/build/core/projection/projection-kill.js +15 -6
  134. package/build/core/projection/projection-life-observer.js +8 -8
  135. package/build/core/projection/projection-mint.js +6 -8
  136. package/build/core/projection/projection-status-observation.js +77 -8
  137. package/build/core/projection/projection-status.js +151 -4
  138. package/build/core/projection/projection-wait.js +46 -27
  139. package/build/core/projection/projection-wake.js +27 -89
  140. package/build/core/projection/tell/launch-store.js +1 -1
  141. package/build/core/projection/tell/store.js +1 -1
  142. package/build/core/renew-build.js +204 -74
  143. package/build/core/renew-plan.js +34 -9
  144. package/build/core/renew-rewrite.js +56 -15
  145. package/build/core/renew.js +39 -35
  146. package/build/core/repository-ledger/accepted-fold-read.js +6 -3
  147. package/build/core/repository-ledger/codec.js +11 -5
  148. package/build/core/repository-ledger/current-state-store.js +13 -16
  149. package/build/core/repository-ledger/fold-repository.js +5 -6
  150. package/build/core/repository-ledger/identity.js +60 -0
  151. package/build/core/repository-ledger/inventory.js +3 -3
  152. package/build/core/repository-ledger/read-model.js +54 -6
  153. package/build/core/repository-ledger/write-transaction.js +9 -4
  154. package/build/core/run-control/connection-bound-close.js +41 -0
  155. package/build/core/run-control/detached-lease-bootstrap.js +208 -0
  156. package/build/core/run-control/detached-process.js +14 -0
  157. package/build/core/run-control/index.js +6 -0
  158. package/build/core/{process-group.js → run-control/process-tree.js} +1 -1
  159. package/build/core/run-control/runner-lease.js +172 -0
  160. package/build/core/{sqlite-process-lifetime-lock.js → run-control/sqlite-process-lifetime-lock.js} +4 -24
  161. package/build/core/seal.js +44 -32
  162. package/build/core/settlement/claim-delivery.js +22 -9
  163. package/build/core/settlement/claim.js +8 -6
  164. package/build/core/settlement/petition-claim-gates.js +9 -8
  165. package/build/core/settlement/petition-head-guard.js +5 -6
  166. package/build/core/settlement/petition-preview.js +21 -5
  167. package/build/core/settlement/petition.js +51 -17
  168. package/build/core/settlement/settlement.js +20 -15
  169. package/build/core/settlement/verdict.js +2 -2
  170. package/build/core/settlement/verification.js +492 -364
  171. package/build/core/status/board.js +184 -75
  172. package/build/core/status/drift.js +8 -10
  173. package/build/core/task/board.js +1 -0
  174. package/build/core/task/commands.js +25 -0
  175. package/build/core/task/compose.js +3 -2
  176. package/build/core/task/document.js +102 -39
  177. package/build/core/task/index.js +3 -3
  178. package/build/core/task/settlement-git.js +1 -4
  179. package/build/core/task/settlement-policy.js +27 -14
  180. package/build/core/task/source-board.js +1 -0
  181. package/build/core/task/task-bind-preparation.js +59 -9
  182. package/build/core/task/task-contract.js +2 -2
  183. package/build/core/task/task-store-repository.js +1 -0
  184. package/build/core/task/task.js +2 -2
  185. package/build/core/transcripts.js +4 -4
  186. package/build/core/verification-declaration.js +95 -0
  187. package/build/core/worktree-bootstrap.js +1 -1
  188. package/build/core/worktree-path.js +7 -11
  189. package/build/flow-error.js +9 -1
  190. package/build/generated/version.js +2 -2
  191. package/build/git/branches.js +41 -70
  192. package/build/git/commits.js +92 -21
  193. package/build/git/core.js +9 -161
  194. package/build/git/diff/preview.js +4 -4
  195. package/build/git/diff/read.js +4 -4
  196. package/build/git/diff/structured.js +5 -5
  197. package/build/git/process.js +229 -0
  198. package/build/git/refs.js +30 -5
  199. package/build/git/staging.js +39 -115
  200. package/build/git/streaming-batch.js +9 -16
  201. package/build/git/worktree.js +104 -91
  202. package/build/index.js +7 -0
  203. package/package.json +1 -1
  204. package/skills/keiyaku-akuma/SKILL.md +21 -20
  205. package/skills/keiyaku-task/SKILL.md +38 -4
  206. package/skills/keiyaku-workflow/SKILL.md +51 -11
  207. package/build/cli/commands/projection/revive/meta.js +0 -12
  208. package/build/core/projection/projection-life-protocol.js +0 -115
  209. package/build/core/projection/projection-runner-lock.js +0 -211
  210. package/build/core/projection/tell/database.js +0 -127
  211. package/build/core/settlement/verification-coordination.js +0 -305
  212. package/build/core/settlement/verification-supervisor.js +0 -275
@@ -6,6 +6,7 @@ import { withContractCarrierSession } from "./contract-carrier.js";
6
6
  import { runLifecycle } from "./lifecycle-runner.js";
7
7
  import { committedLifecycleFacts, derivedCleanupFacts, isCompleteLifecycleOutcome, } from "./outcome-base.js";
8
8
  import { abortDetachedRenewChain, readDetachedRenewScratch, renewScratchName, } from "./renew-rewrite.js";
9
+ import { scopePathEvidence } from "./scope.js";
9
10
  import { persistResponseHistory } from "./transcripts.js";
10
11
  import { admitRenew, checkCancellation, prepareRenew, sameAdmission, scopeEntry } from "./renew-plan.js";
11
12
  async function persistRenewResponse(journal, facts, persist) {
@@ -46,6 +47,7 @@ async function executeRenew(input, continuation) {
46
47
  },
47
48
  signal: input.signal,
48
49
  ...(input.renewDeliveryProjector ? { renewDeliveryProjector: input.renewDeliveryProjector } : {}),
50
+ ...(input.transactionRunner ? { transactionRunner: input.transactionRunner } : {}),
49
51
  admit: (session, replay) => admitRenew(session, input, replay, continuation?.scratch),
50
52
  prepare: (admission) => prepareRenew(admission.business, input, continuation),
51
53
  revalidate: async ({ session, admission }) => {
@@ -79,7 +81,11 @@ async function executeRenew(input, continuation) {
79
81
  if (admission.business.acceptedReplay) {
80
82
  const accepted = admission.business.acceptedReplay;
81
83
  const replayed = await context.replayCommitted({ transactionObjectId: accepted.transactionObjectId });
82
- const sealed = { ...accepted.seal.data, scopeViolationEvidence: [], ledgerHead: accepted.sealObjectId };
84
+ const sealed = {
85
+ ...accepted.seal.data,
86
+ scopeViolationEvidence: await scopePathEvidence(admission.business.targetRoot, admission.business.model.effectiveScope, accepted.seal.data.scopeAudit.violations),
87
+ ledgerHead: accepted.sealObjectId,
88
+ };
83
89
  const value = {
84
90
  ...committedLifecycleFacts({ head: accepted.transactionObjectId, entryKind: "renew", contractId: input.contractId, carrier: replayed.carrier }),
85
91
  contractId: input.contractId,
@@ -95,13 +101,6 @@ async function executeRenew(input, continuation) {
95
101
  carrier: replayed.carrier,
96
102
  targetRoot: admission.business.targetRoot,
97
103
  };
98
- if (continuation) {
99
- await context.journal.run({
100
- effect: "renew-scratch-cleanup",
101
- idempotencyKey: `${input.contractId}:${accepted.transactionObjectId}:renew-scratch-cleanup`,
102
- run: async () => abortDetachedRenewChain({ cwd: input.cwd, scratchName: continuation.scratch.scratchName }),
103
- });
104
- }
105
104
  await persistRenewResponse(context.journal, value, input.persistResponse);
106
105
  return { kind: "committed", commit: replayed, entryKind: "renew", value };
107
106
  }
@@ -140,7 +139,7 @@ async function executeRenew(input, continuation) {
140
139
  if (!admission.business.amendment) {
141
140
  const transactionObjectId = admission.replay.state === "verb-committed"
142
141
  ? admission.replay.transaction.transactionObjectId
143
- : admission.ledgerHead;
142
+ : admission.replay.transactionObjectId;
144
143
  const replayed = await context.replayCommitted({ transactionObjectId });
145
144
  const accepted = replayed.transaction.entries.at(-1);
146
145
  if (!accepted)
@@ -166,7 +165,7 @@ async function executeRenew(input, continuation) {
166
165
  const published = await context.publishAuthoritative({
167
166
  verb: "renew",
168
167
  admittedLedgerHead: admission.ledgerHead,
169
- acceptedLifecycleTail: context.session.lifecycle.history.at(-1).objectId,
168
+ acceptedLifecycleTail: context.session.lifecycle.contractHead,
170
169
  entries: [admission.business.amendment],
171
170
  });
172
171
  if (published.status === "refused-before-effects")
@@ -203,15 +202,19 @@ async function executeRenew(input, continuation) {
203
202
  const published = await context.publishAuthoritative({
204
203
  verb: "renew",
205
204
  admittedLedgerHead: admission.ledgerHead,
206
- acceptedLifecycleTail: context.session.lifecycle.history.at(-1).objectId,
205
+ acceptedLifecycleTail: context.session.lifecycle.contractHead,
207
206
  entries,
207
+ target: {
208
+ ref: admission.business.targetRef,
209
+ expectedOid: prepared.entry.data.newBase,
210
+ },
208
211
  delivery: {
209
212
  ref: admission.business.deliveryRef,
210
213
  expectedOid: prepared.entry.data.oldHead,
211
214
  },
212
215
  });
213
216
  if (published.status === "refused-before-effects")
214
- return { kind: "refused-before-effects", authoritativeHead: admission.ledgerHead, refusal: { code: "CONCURRENT_MODIFICATION", message: "repository ledger or delivery ref changed before renew transaction" } };
217
+ return { kind: "refused-before-effects", authoritativeHead: admission.ledgerHead, refusal: { code: "CONCURRENT_MODIFICATION", message: "repository target, ledger, or delivery ref changed before renew transaction" } };
215
218
  if (published.status === "indeterminate")
216
219
  return { kind: "precommit-effects-only", authoritativeHead: admission.ledgerHead };
217
220
  const sealIndex = published.transaction.entries.findIndex((entry) => entry.kind === "seal");
@@ -239,13 +242,6 @@ async function executeRenew(input, continuation) {
239
242
  carrier: published.carrier,
240
243
  targetRoot: admission.business.targetRoot,
241
244
  };
242
- if (continuation) {
243
- await context.journal.run({
244
- effect: "renew-scratch-cleanup",
245
- idempotencyKey: `${input.contractId}:${published.head}:renew-scratch-cleanup`,
246
- run: async () => abortDetachedRenewChain({ cwd: input.cwd, scratchName: continuation.scratch.scratchName }),
247
- });
248
- }
249
245
  await persistRenewResponse(context.journal, value, input.persistResponse);
250
246
  return { kind: "committed", commit: published, entryKind: "renew", value };
251
247
  },
@@ -261,19 +257,7 @@ export async function renewContract(input) {
261
257
  }
262
258
  export async function continueRenewContract(input) {
263
259
  const scratchName = renewScratchName(input.contractId);
264
- let scratch;
265
- try {
266
- scratch = await readDetachedRenewScratch({ cwd: input.cwd, scratchName });
267
- }
268
- catch (error) {
269
- throw new FlowError("RENEW_SCRATCH_NOT_FOUND", `no renew conflict scratch is open for ${input.contractId}`, { cause: error });
270
- }
271
- return executeRenew(input, { scratch, resolution: input.resolution ?? { kind: "scratch-files" } });
272
- }
273
- export async function abortRenewContract(input) {
274
- checkCancellation(input.signal);
275
- return withContractCarrierSession({ cwd: input.cwd, contractId: input.contractId, mode: "observe-only", signal: input.signal }, async (session) => {
276
- const scratchName = renewScratchName(input.contractId);
260
+ for (let attempt = 0; attempt < 2; attempt += 1) {
277
261
  let scratch;
278
262
  try {
279
263
  scratch = await readDetachedRenewScratch({ cwd: input.cwd, scratchName });
@@ -281,17 +265,37 @@ export async function abortRenewContract(input) {
281
265
  catch (error) {
282
266
  throw new FlowError("RENEW_SCRATCH_NOT_FOUND", `no renew conflict scratch is open for ${input.contractId}`, { cause: error });
283
267
  }
268
+ const result = await executeRenew(input, { scratch, resolution: input.resolution ?? { kind: "scratch-files" } });
269
+ const code = result.family === "refused-before-effects" ? result.failedInvariant.code : null;
270
+ if (attempt === 0 && (code === "LIFECYCLE_COORDINATES_CHANGED"
271
+ || code === "LIFECYCLE_REPLAY_COORDINATES_CHANGED"
272
+ || code === "RENEW_COORDINATES_CHANGED"
273
+ || code === "CONCURRENT_MODIFICATION"))
274
+ continue;
275
+ if (isCompleteLifecycleOutcome(result)) {
276
+ await abortDetachedRenewChain({ cwd: input.cwd, scratchName: scratch.scratchName });
277
+ }
278
+ return result;
279
+ }
280
+ throw new FlowError("INTERNAL_STATE", "bounded renew continuation retry exhausted unexpectedly");
281
+ }
282
+ export async function abortRenewContract(input) {
283
+ checkCancellation(input.signal);
284
+ return withContractCarrierSession({ cwd: input.cwd, contractId: input.contractId, mode: "observe-only", signal: input.signal }, async (session) => {
285
+ const scratchName = renewScratchName(input.contractId);
284
286
  checkCancellation(input.signal);
285
287
  const cleanup = await abortDetachedRenewChain({ cwd: input.cwd, scratchName });
288
+ if (cleanup.status === "absent") {
289
+ throw new FlowError("RENEW_SCRATCH_NOT_FOUND", `no renew conflict scratch is open for ${input.contractId}`);
290
+ }
286
291
  return {
287
292
  ...derivedCleanupFacts({
288
293
  contractId: input.contractId,
289
- authoritativeHead: session.lifecycle.observedLedgerHead,
294
+ authoritativeHead: session.pins.repositoryLedgerHead,
290
295
  effect: "renew-conflict-scratch-cleanup",
291
- receipt: { status: cleanup.status, scratchName, oldHead: scratch.oldHead },
296
+ receipt: { status: cleanup.status, scratchName },
292
297
  }),
293
298
  contractId: input.contractId,
294
- oldHead: scratch.oldHead,
295
299
  scratchName,
296
300
  carrier: session.initialCarrier,
297
301
  };
@@ -1,15 +1,18 @@
1
1
  import { resolveLocalBranchHead } from "../../git/refs.js";
2
2
  import { foldRepositoryLedger } from "./fold-repository.js";
3
3
  import { REPOSITORY_LEDGER_REF } from "./codec.js";
4
+ import { repositoryLedgerHead } from "./identity.js";
4
5
  import { traverseRepositoryLedger } from "./traversal.js";
5
6
  export async function observeRepositoryLedgerHead(cwd) {
6
- return (await resolveLocalBranchHead(cwd, REPOSITORY_LEDGER_REF))?.head ?? null;
7
+ const head = (await resolveLocalBranchHead(cwd, REPOSITORY_LEDGER_REF))?.head;
8
+ return head === undefined ? null : repositoryLedgerHead(head);
7
9
  }
8
10
  /** The shared current-head typed traversal and fold used by full and targeted reads. */
9
11
  export async function readAcceptedRepositoryFold(cwd, options = {}) {
10
- const observedLedgerHead = await (options.observeHead ?? observeRepositoryLedgerHead)(cwd);
11
- if (observedLedgerHead === null)
12
+ const observed = await (options.observeHead ?? observeRepositoryLedgerHead)(cwd);
13
+ if (observed === null)
12
14
  return { observedLedgerHead: null, traversed: null, folded: null };
15
+ const observedLedgerHead = repositoryLedgerHead(observed);
13
16
  const traversed = await traverseRepositoryLedger(cwd, observedLedgerHead, options.signal, { historicalSemanticEvidence: false });
14
17
  return {
15
18
  observedLedgerHead,
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { verificationDeclarationSchema } from "../verification-declaration.js";
2
3
  export const REPOSITORY_LEDGER_REF = "refs/heads/keiyaku-ledger";
3
4
  export const REPOSITORY_DELIVERY_REF_PREFIX = "refs/heads/keiyaku/";
4
5
  export function repositoryDeliveryRef(contract) {
@@ -61,7 +62,7 @@ function buildLifecycleEntrySchema(format) {
61
62
  design: nonblank,
62
63
  scope: z.array(nonblank).min(1),
63
64
  criteria: z.array(nonblank),
64
- verification: z.array(nonblank),
65
+ verification: z.array(verificationDeclarationSchema),
65
66
  extensions: z.array(extension),
66
67
  base: oid,
67
68
  target: fullBranchRefSchema,
@@ -103,7 +104,7 @@ function buildLifecycleEntrySchema(format) {
103
104
  }
104
105
  });
105
106
  return z.discriminatedUnion("kind", [
106
- lifecycleEnvelope("bind", 2, bindData),
107
+ lifecycleEnvelope("bind", 3, bindData),
107
108
  lifecycleEnvelope("bootstrap", 1, bootstrapData),
108
109
  lifecycleEnvelope("arc", 1, z.object({
109
110
  arc: safePositiveInteger,
@@ -124,10 +125,15 @@ function buildLifecycleEntrySchema(format) {
124
125
  }).strict(),
125
126
  scopeAudit: z.object({ violations: z.array(nonblank) }).strict(),
126
127
  }).strict()),
127
- lifecycleEnvelope("amend", 1, z.object({
128
- amendment: nonblank,
128
+ lifecycleEnvelope("amend", 2, z.object({
129
+ amendment: z.string(),
129
130
  scopeDelta: z.object({ add: z.array(nonblank).min(1) }).strict().optional(),
130
- }).strict()),
131
+ verificationDelta: z.object({ replace: z.array(verificationDeclarationSchema) }).strict().optional(),
132
+ }).strict().superRefine((value, context) => {
133
+ if (!value.amendment.trim() && !value.scopeDelta && !value.verificationDelta) {
134
+ context.addIssue({ code: "custom", message: "amend requires prose, a scope delta, or a verification delta" });
135
+ }
136
+ })),
131
137
  lifecycleEnvelope("renew", 1, z.object({
132
138
  oldBase: oid,
133
139
  newBase: oid,
@@ -3,10 +3,11 @@ import * as path from "node:path";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import { DatabaseSync } from "node:sqlite";
5
5
  import { runGitProcess } from "../../git/core.js";
6
- import { acquireSqliteProcessLifetimeLock } from "../sqlite-process-lifetime-lock.js";
6
+ import { acquireSqliteProcessLifetimeLock } from "../run-control/sqlite-process-lifetime-lock.js";
7
7
  import { applyFoldEntry } from "./fold.js";
8
8
  import { createLifecycleEntrySchema, evidenceObjectId, repositoryDeliveryRef } from "./codec.js";
9
- export const REPOSITORY_CURRENT_STATE_VERSION = 3;
9
+ import { repositoryLedgerHead } from "./identity.js";
10
+ export const REPOSITORY_CURRENT_STATE_VERSION = 4;
10
11
  export const REPOSITORY_CURRENT_STATE_PREFIX = "keiyaku-current-state-";
11
12
  export const REPOSITORY_CURRENT_STATE_SUFFIX = ".sqlite";
12
13
  const SNAPSHOT_READER_BUSY_MS = 5_000;
@@ -67,7 +68,7 @@ function exactRows(database, sql, ...params) {
67
68
  return database.prepare(sql).all(...params);
68
69
  }
69
70
  function pinDigest(pin) {
70
- return digest(["keiyaku-current-state-v3", REPOSITORY_CURRENT_STATE_VERSION, pin.head, pin.objectFormat, pin.emptyTree]);
71
+ return digest(["keiyaku-current-state-v4", REPOSITORY_CURRENT_STATE_VERSION, pin.head, pin.objectFormat, pin.emptyTree]);
71
72
  }
72
73
  function repositoryDigest(database) {
73
74
  return digest(["repository", exactRows(database, "SELECT singleton, diagnostics_json FROM snapshot_repository ORDER BY singleton")]);
@@ -335,8 +336,8 @@ function parseCurrent(text, contract) {
335
336
  const value = JSON.parse(text);
336
337
  if (!isRecord(value))
337
338
  throw new Error("snapshot current facts are not an object");
338
- assertClosedKeys(value, ["observedLedgerHead", "contract", "phase", "intent", "receiptSegments", "seal", "bootstrapReplay", "petition", "verdict", "pins", "effectiveScope", "causal", "resources"], "snapshot current facts");
339
- if (value.contract !== contract || typeof value.observedLedgerHead !== "string" || !isRecord(value.resources)) {
339
+ assertClosedKeys(value, ["contractHead", "contract", "phase", "intent", "receiptSegments", "seal", "bootstrapReplay", "petition", "verdict", "pins", "effectiveScope", "causal", "resources"], "snapshot current facts");
340
+ if (value.contract !== contract || typeof value.contractHead !== "string" || !isRecord(value.resources)) {
340
341
  throw new Error("snapshot current facts disagree with their row key");
341
342
  }
342
343
  return value;
@@ -387,11 +388,8 @@ function validateContractConsistency(database, input, pin, summaryOnly = false)
387
388
  const diagnostics = parseDiagnostics(diagnosticRow.diagnosticsJson, input.contract);
388
389
  if ((input.status === "healthy") !== (diagnostics.length === 0))
389
390
  throw new Error("snapshot status disagrees with diagnostics");
390
- if (summaryOnly) {
391
- if (model.observedLedgerHead !== pin.head)
392
- throw new Error("snapshot current summary disagrees with pin");
391
+ if (summaryOnly)
393
392
  return model;
394
- }
395
393
  const fold = JSON.parse(input.foldJson);
396
394
  if (!isRecord(fold) || fold.contract !== input.contract || !Array.isArray(fold.entryObjectIds))
397
395
  throw new Error("snapshot fold is invalid");
@@ -399,7 +397,7 @@ function validateContractConsistency(database, input, pin, summaryOnly = false)
399
397
  if (fold.entryObjectIds.length !== objectIds.length || fold.entryObjectIds.some((oid, index) => oid !== objectIds[index])) {
400
398
  throw new Error("snapshot fold and lifecycle history disagree");
401
399
  }
402
- if (model.observedLedgerHead !== pin.head || model.phase !== fold.phase || model.pins.base !== fold.bind?.data?.base) {
400
+ if (model.contractHead !== objectIds.at(-1) || model.phase !== fold.phase || model.pins.base !== fold.bind?.data?.base) {
403
401
  throw new Error("snapshot current facts disagree with fold or pin");
404
402
  }
405
403
  const typedFold = fold;
@@ -472,8 +470,8 @@ function storedContract(database, contract) {
472
470
  diagnostics: parseDiagnostics(row.diagnosticsJson, contract),
473
471
  };
474
472
  }
475
- function atPin(model, head) {
476
- return model.observedLedgerHead === head ? model : { ...model, observedLedgerHead: head };
473
+ function atPin(model, _head) {
474
+ return model;
477
475
  }
478
476
  export async function readPinnedCurrentContract(cwd, observedHead, contract, signal) {
479
477
  const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, signal);
@@ -633,7 +631,7 @@ export async function readPinnedRepositoryLifecycle(cwd, observedHead, signal, o
633
631
  validatePinReceipt(database, pin);
634
632
  validateRepositoryReceipts(database, rows.map((row) => row.contract), options.summaryOnly === true, options.includeTopology !== false);
635
633
  return {
636
- observedLedgerHead: pin.head,
634
+ observedLedgerHead: pin.head === null ? null : repositoryLedgerHead(pin.head),
637
635
  objectFormat: pin.objectFormat,
638
636
  emptyTree: pin.emptyTree,
639
637
  contracts: rows.flatMap((row) => {
@@ -855,14 +853,14 @@ export async function advanceRepositoryCurrentState(cwd, pinnedHead, observedHea
855
853
  const prerequisite = database.prepare("SELECT status, fold_json AS foldJson FROM snapshot_contracts WHERE contract = ?").get(dependency);
856
854
  if (!prerequisite || prerequisite.status !== "healthy" || prerequisite.foldJson === null)
857
855
  return true;
858
- return parseJson(prerequisite.foldJson).terminalObjectId === null;
856
+ return false;
859
857
  });
860
858
  if (unavailable.length > 0) {
861
859
  const diagnostic = {
862
860
  objectId: first.objectId,
863
861
  code: "causal-prerequisite",
864
862
  contract,
865
- detail: `after prerequisites are not admitted terminal contracts: ${unavailable.join(", ")}`,
863
+ detail: `after prerequisites are not admitted healthy contracts: ${unavailable.join(", ")}`,
866
864
  };
867
865
  database.prepare(`
868
866
  INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json)
@@ -944,7 +942,6 @@ export async function advanceRepositoryCurrentState(cwd, pinnedHead, observedHea
944
942
  database.exec("ROLLBACK");
945
943
  return false;
946
944
  }
947
- database.prepare("UPDATE snapshot_contracts SET current_json = json_set(current_json, '$.observedLedgerHead', ?) WHERE current_json IS NOT NULL").run(observedHead);
948
945
  database.prepare("UPDATE snapshot_pin SET ledger_head = ? WHERE singleton = 1").run(observedHead);
949
946
  writeIntegrityReceipts(database);
950
947
  database.exec("COMMIT");
@@ -216,15 +216,14 @@ function validateAfterPrerequisites(ledger, contracts, isolatedContracts, diseas
216
216
  const bindTransaction = containingTransactions.get(bindObjectId);
217
217
  const ready = state.bind.data.after.every((dependency) => {
218
218
  const prerequisite = contracts.get(dependency);
219
- const terminalTransaction = prerequisite?.terminalObjectId === null
219
+ const prerequisiteBindTransaction = prerequisite === undefined
220
220
  ? undefined
221
- : containingTransactions.get(prerequisite?.terminalObjectId ?? "");
221
+ : containingTransactions.get(prerequisite.entryObjectIds[0]);
222
222
  return prerequisite !== undefined &&
223
223
  valid.has(dependency) &&
224
- prerequisite.terminalObjectId !== null &&
225
- terminalTransaction !== undefined &&
224
+ prerequisiteBindTransaction !== undefined &&
226
225
  bindTransaction !== undefined &&
227
- isTypedLedgerAncestor(ledger, terminalTransaction, bindTransaction);
226
+ isTypedLedgerAncestor(ledger, prerequisiteBindTransaction, bindTransaction);
228
227
  });
229
228
  if (ready) {
230
229
  valid.add(contract);
@@ -240,7 +239,7 @@ function validateAfterPrerequisites(ledger, contracts, isolatedContracts, diseas
240
239
  objectId: bindObjectId,
241
240
  code: "causal-prerequisite",
242
241
  contract,
243
- detail: `after prerequisites were not terminal in typed ancestry: ${state.bind.data.after.join(", ")}`,
242
+ detail: `after prerequisite binds were not in typed ancestry: ${state.bind.data.after.join(", ")}`,
244
243
  });
245
244
  contracts.delete(contract);
246
245
  isolatedContracts.add(contract);
@@ -0,0 +1,60 @@
1
+ export function repositoryLedgerHead(value) {
2
+ return value;
3
+ }
4
+ export function lifecycleTransactionId(value) {
5
+ return value;
6
+ }
7
+ export function contractHead(value) {
8
+ return value;
9
+ }
10
+ export function carrierDigest(value) {
11
+ if (!/^[0-9a-f]{64}$/.test(value)) {
12
+ throw new Error("carrier digest must be a lowercase SHA-256 hex value");
13
+ }
14
+ return value;
15
+ }
16
+ export function carrierInputIdentity(input) {
17
+ return {
18
+ surface: "carrier",
19
+ contractId: input.contractId,
20
+ contractHead: input.contractHead,
21
+ bodyDigest: input.bodyDigest,
22
+ };
23
+ }
24
+ export function carrierRetirementInputIdentity(input) {
25
+ return { surface: "carrier-retirement", ...input };
26
+ }
27
+ export function worktreeInputIdentity(input) {
28
+ return {
29
+ surface: "worktree",
30
+ contractId: input.contractId,
31
+ contractHead: input.contractHead,
32
+ deliveryHead: input.deliveryHead,
33
+ };
34
+ }
35
+ export function contractViewInputIdentity(input) {
36
+ return {
37
+ surface: "contract-view",
38
+ contractId: input.contractId,
39
+ contractHead: input.contractHead,
40
+ };
41
+ }
42
+ export function narrowIgnoreInputIdentity(input) {
43
+ return { surface: "narrow-ignore", ...input };
44
+ }
45
+ export function renewDeliveryInputIdentity(input) {
46
+ return { surface: "renew-delivery", ...input };
47
+ }
48
+ export function taskBoardInputIdentity(input) {
49
+ return { surface: "task-board", ...input };
50
+ }
51
+ export function deliveryRefInputIdentity(input) {
52
+ return { surface: "delivery-ref", ...input };
53
+ }
54
+ export function targetWorktreeInputIdentity(input) {
55
+ return { surface: "target-worktree", ...input };
56
+ }
57
+ /** Structural equality keeps surface comparisons independent of process-local receipts. */
58
+ export function sameInputIdentity(left, right) {
59
+ return left !== null && JSON.stringify(left) === JSON.stringify(right);
60
+ }
@@ -295,12 +295,12 @@ export function activeWorkspaceHereContracts(read) {
295
295
  }
296
296
  /** Runtime ignore projection for one worktree from a single typed repository fold. */
297
297
  export function inventoryIgnorePatternsForWorktree(read, targetLifecycle) {
298
- if (read.observedLedgerHead !== targetLifecycle.observedLedgerHead) {
299
- throw new Error("narrow-ignore inventory requires one repository-ledger observation");
300
- }
301
298
  const acceptedTarget = read.contracts.find(({ contract }) => contract === targetLifecycle.contract);
302
299
  if (!acceptedTarget)
303
300
  throw new Error(`contract ${targetLifecycle.contract} is absent from narrow-ignore inventory`);
301
+ if (acceptedTarget.contractHead !== targetLifecycle.contractHead) {
302
+ throw new Error("narrow-ignore inventory requires the current contract semantic head");
303
+ }
304
304
  const target = foldedWorktreeCoordinate(acceptedTarget);
305
305
  const required = target.workspace === "here"
306
306
  ? activeWorkspaceHereContracts(read).length > 0
@@ -1,9 +1,11 @@
1
1
  import { openTaskAdmissionEvidenceReader, readTaskAdmissionEvidenceAtCommit, } from "../task/settlement-git.js";
2
2
  import { repositoryDeliveryRef, } from "./codec.js";
3
+ import { bindEffectiveScope } from "./fold.js";
3
4
  import { observeRepositoryLedgerHead, readAcceptedRepositoryFold } from "./accepted-fold-read.js";
4
5
  import { advanceRepositoryCurrentState, publishRepositoryCurrentState, readPinnedCurrentContract, readPinnedRepositoryLifecycle, readRepositoryCurrentStateNodeIds, readRepositoryCurrentStatePin, readRepositoryCurrentStatePredecessor } from "./current-state-store.js";
5
6
  import { IncrementalLedgerValidationError, readIncrementalRepositorySegment } from "./incremental-read.js";
6
7
  import { RepositoryObjectError } from "./git-object-store.js";
8
+ import { contractHead, repositoryLedgerHead } from "./identity.js";
7
9
  /** Public lifecycle state derived only from the accepted repository-ledger fold. */
8
10
  export function currentLifecycleState(model) {
9
11
  switch (model.phase) {
@@ -22,6 +24,50 @@ export function currentLifecycleState(model) {
22
24
  return { state: "forfeited" };
23
25
  }
24
26
  }
27
+ /** Purely advance an admitted repository snapshot with its just-committed bind. */
28
+ export function advanceRepositorySnapshotWithBind(input) {
29
+ const { repository, ledgerHead, entryObjectId, entry } = input;
30
+ const bootstrap = entry.data.workspace === "worktree" ? entry.data.bootstrap : undefined;
31
+ const model = deepFreeze({
32
+ contractHead: contractHead(entryObjectId),
33
+ contract: entry.contract,
34
+ phase: "active",
35
+ intent: { arc: 0, openedBy: entry.actor, title: entry.data.title, objective: entry.data.objective, brief: null },
36
+ receiptSegments: [],
37
+ seal: null,
38
+ bootstrapReplay: bootstrap
39
+ ? { state: "pending", plan: { command: [...bootstrap.command], timeoutMs: bootstrap.timeoutMs } }
40
+ : { state: "not-planned" },
41
+ petition: { state: "none" },
42
+ verdict: null,
43
+ pins: { base: entry.data.base, delivery: entry.data.base, target: { ref: entry.data.target, approvedHead: null } },
44
+ effectiveScope: bindEffectiveScope(entry),
45
+ causal: { after: [...(entry.data.after ?? [])], needs: null },
46
+ resources: {
47
+ deliveryRef: repositoryDeliveryRef(entry.contract),
48
+ worktree: entry.data.workspace === "worktree" ? { place: entry.data.place, state: "held" } : null,
49
+ tasks: [...(entry.data.taskIds ?? [])].map((taskId) => ({ taskId, state: "held" })),
50
+ },
51
+ history: [{ objectId: entryObjectId, entry: structuredClone(entry) }],
52
+ });
53
+ return deepFreeze({
54
+ ...structuredClone(repository),
55
+ observedLedgerHead: ledgerHead,
56
+ contracts: [...repository.contracts, model].sort((left, right) => left.contract.localeCompare(right.contract)),
57
+ topology: {
58
+ head: ledgerHead,
59
+ nodes: [...repository.topology.nodes, {
60
+ objectId: ledgerHead,
61
+ type: "transaction",
62
+ ledgerParents: repository.observedLedgerHead ? [repository.observedLedgerHead] : [],
63
+ verb: "bind",
64
+ contract: entry.contract,
65
+ entryObjectIds: [entryObjectId],
66
+ finalEntryObjectId: entryObjectId,
67
+ }],
68
+ },
69
+ });
70
+ }
25
71
  function deepFreeze(value) {
26
72
  if (value === null || typeof value !== "object" || Object.isFrozen(value))
27
73
  return value;
@@ -199,14 +245,14 @@ function bootstrapReplay(state) {
199
245
  }
200
246
  throw new Error(`failed bootstrap for ${state.contract} has no folded replay disposition`);
201
247
  }
202
- function lifecycleModel(observedLedgerHead, state, nodes, taskAdmissionEvidence) {
248
+ function lifecycleModel(_observedLedgerHead, state, nodes, taskAdmissionEvidence) {
203
249
  const history = acceptedHistory(state, nodes);
204
250
  const verdict = findLastRow(history, "verdict");
205
251
  const released = state.terminalObjectId !== null;
206
252
  const taskReleased = state.terminal?.kind === "forfeit";
207
253
  const selectedTasks = [...(state.bind.data.taskIds ?? [])];
208
254
  return deepFreeze({
209
- observedLedgerHead,
255
+ contractHead: contractHead(state.entryObjectIds.at(-1)),
210
256
  contract: state.contract,
211
257
  phase: state.phase,
212
258
  intent: currentIntent(state, history),
@@ -359,7 +405,9 @@ async function recoverRepositoryLifecycle(cwd, options = {}) {
359
405
  });
360
406
  }
361
407
  export async function readRepositoryLifecycle(cwd, options = {}) {
362
- const observe = options.observeHead ?? observeRepositoryLedgerHead;
408
+ const observe = options.observedHead !== undefined
409
+ ? async () => options.observedHead
410
+ : options.observeHead ?? observeRepositoryLedgerHead;
363
411
  const observedHead = await observe(cwd);
364
412
  const injected = options.observeHead !== undefined || options.readTaskAdmissionEvidence !== undefined;
365
413
  if (!injected) {
@@ -374,8 +422,8 @@ export async function readRepositoryLifecycle(cwd, options = {}) {
374
422
  const segment = await readIncrementalRepositorySegment(cwd, priorHead, observedHead, options.signal, acceptedObjectIds ?? undefined);
375
423
  if (segment && await advanceRepositoryCurrentState(cwd, priorHead, observedHead, segment, {
376
424
  signal: options.signal,
377
- confirmHead: () => observeRepositoryLedgerHead(cwd),
378
- materialize: (state, history) => materializeIncrementalContract(cwd, observedHead, state, history),
425
+ confirmHead: options.observedHead !== undefined ? async () => observedHead : () => observeRepositoryLedgerHead(cwd),
426
+ materialize: (state, history) => materializeIncrementalContract(cwd, repositoryLedgerHead(observedHead), state, history),
379
427
  })) {
380
428
  const advanced = await readPinnedRepositoryLifecycle(cwd, observedHead, options.signal, { includeTopology: options.includeTopology });
381
429
  if (advanced)
@@ -404,7 +452,7 @@ export async function readRepositoryLifecycle(cwd, options = {}) {
404
452
  if (!injected) {
405
453
  const published = await publishRepositoryCurrentState(cwd, recovered, {
406
454
  signal: options.signal,
407
- confirmHead: () => observeRepositoryLedgerHead(cwd),
455
+ confirmHead: options.observedHead !== undefined ? async () => observedHead : () => observeRepositoryLedgerHead(cwd),
408
456
  fold: recoveredFold,
409
457
  });
410
458
  if (!published)
@@ -1,7 +1,7 @@
1
1
  import { runGitProcess } from "../../git/core.js";
2
2
  import { commitRefTransaction, } from "../../git/refs.js";
3
3
  import { REPOSITORY_LEDGER_REF, evidenceObjectId, formatLifecycleEntry, formatLifecycleTransaction, repositoryDeliveryRef, } from "./codec.js";
4
- import { assertHostObjectId, assertReadableCommit, deriveHostGitCoordinates, } from "./git-object-store.js";
4
+ import { assertHostObjectId, assertReadableCommit, assertReadableCommitClosure, deriveHostGitCoordinates, } from "./git-object-store.js";
5
5
  import { assertHonestClaimEvidence } from "./claim-evidence.js";
6
6
  import { transactionMembershipError } from "./transaction-membership.js";
7
7
  function replayFailure(effect, error) {
@@ -76,7 +76,7 @@ async function assertAuthoritativeCommitTipClosure(cwd, coordinates, operations)
76
76
  if (operation.kind !== "update" || operation.ref === REPOSITORY_LEDGER_REF || checked.has(operation.newOid))
77
77
  continue;
78
78
  assertHostObjectId(coordinates, operation.newOid);
79
- await assertReadableCommit(cwd, coordinates, operation.newOid);
79
+ await assertReadableCommitClosure(cwd, operation.newOid);
80
80
  checked.add(operation.newOid);
81
81
  }
82
82
  }
@@ -248,6 +248,7 @@ async function publishLifecycleWrite(cwd, builtTransaction, final, ownedOperatio
248
248
  completion: replay.status,
249
249
  ledgerHead: builtTransaction.objectId,
250
250
  entry: final.entry,
251
+ entryObjectId: final.objectId,
251
252
  authoritativeOperations,
252
253
  authorityProof: transaction.proof,
253
254
  authorityObservations: transaction.observations,
@@ -280,6 +281,9 @@ export async function publishBindTransaction(input) {
280
281
  { kind: "update", ref: input.delivery.ref, newOid: input.delivery.head, expectedOldOid: null },
281
282
  ];
282
283
  if (input.taskBoard) {
284
+ if (input.taskBoard.ref !== input.entry.data.target) {
285
+ throw new Error("task-selected bind task-board ref must equal the entry target");
286
+ }
283
287
  operations.push({
284
288
  kind: "update",
285
289
  ref: input.taskBoard.ref,
@@ -313,11 +317,12 @@ export async function publishAuthoritativeTransaction(cwd, transaction, options
313
317
  const entry = final.entry;
314
318
  const operations = [];
315
319
  if (transaction.verb === "renew" && entry.kind === "renew") {
316
- assertAuthority("delivery" in transaction, "renew transaction must pin its delivery ref");
320
+ assertAuthority("delivery" in transaction && "target" in transaction, "renew transaction must pin its target and delivery refs");
321
+ assertAuthority(transaction.target.expectedOid === entry.data.newBase, "renew target CAS must equal newBase");
317
322
  assertAuthority(transaction.delivery.ref === repositoryDeliveryRef(entry.contract), "renew delivery ref does not name its contract");
318
323
  assertAuthority(transaction.delivery.expectedOid === entry.data.oldHead, "renew delivery CAS must equal oldHead");
319
324
  assertAuthority(entry.data.oldHead !== entry.data.newHead, "renew must replace the delivery head");
320
- operations.push({ kind: "update", ref: transaction.delivery.ref, expectedOldOid: transaction.delivery.expectedOid, newOid: entry.data.newHead });
325
+ operations.push({ kind: "verify", ref: transaction.target.ref, expectedOldOid: transaction.target.expectedOid }, { kind: "update", ref: transaction.delivery.ref, expectedOldOid: transaction.delivery.expectedOid, newOid: entry.data.newHead });
321
326
  }
322
327
  else if (transaction.verb === "forfeit") {
323
328
  assertAuthority(entry.kind === "forfeit", "forfeit transaction must end in a forfeit entry");
@@ -0,0 +1,41 @@
1
+ export function beginConnectionBoundRunnerClose(database, lease) {
2
+ database.exec("BEGIN IMMEDIATE");
3
+ let open = true;
4
+ let leaseOwnership = "retained";
5
+ const rollback = () => {
6
+ open = false;
7
+ database.exec("ROLLBACK");
8
+ };
9
+ return {
10
+ get leaseOwnership() { return leaseOwnership; },
11
+ commit() {
12
+ if (!open)
13
+ throw new Error("connection-bound runner close is not open");
14
+ try {
15
+ lease.close();
16
+ }
17
+ catch (error) {
18
+ leaseOwnership = "unknown";
19
+ rollback();
20
+ throw error;
21
+ }
22
+ leaseOwnership = "consumed";
23
+ try {
24
+ database.exec("COMMIT");
25
+ open = false;
26
+ }
27
+ catch (error) {
28
+ rollback();
29
+ throw error;
30
+ }
31
+ },
32
+ rollback() {
33
+ if (leaseOwnership !== "retained") {
34
+ throw new Error(`connection-bound runner lease ownership is ${leaseOwnership}`);
35
+ }
36
+ if (!open)
37
+ throw new Error("connection-bound runner close is no longer open");
38
+ rollback();
39
+ },
40
+ };
41
+ }