@astrosheep/keiyaku 2.9.11 → 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 (218) hide show
  1. package/README.md +79 -85
  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 +17 -4
  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/bind/meta.js +4 -4
  17. package/build/cli/commands/contract/petition/handler.js +2 -1
  18. package/build/cli/commands/contract/petition/meta.js +2 -2
  19. package/build/cli/commands/contract/renew/handler.js +44 -3
  20. package/build/cli/commands/contract/renew/meta.js +5 -4
  21. package/build/cli/commands/metadata.js +14 -10
  22. package/build/cli/commands/projection/call/handler.js +1 -1
  23. package/build/cli/commands/projection/call/meta.js +1 -1
  24. package/build/cli/commands/projection/catalog.js +2 -2
  25. package/build/cli/commands/projection/{revive → fork}/handler.js +10 -5
  26. package/build/cli/commands/projection/fork/meta.js +12 -0
  27. package/build/cli/commands/projection/history/handler.js +46 -2
  28. package/build/cli/commands/projection/history/meta.js +3 -3
  29. package/build/cli/commands/projection/kill/handler.js +4 -5
  30. package/build/cli/commands/projection/kill/meta.js +2 -2
  31. package/build/cli/commands/projection/status/handler.js +29 -13
  32. package/build/cli/commands/projection/status/meta.js +14 -3
  33. package/build/cli/commands/projection/tell/handler.js +8 -4
  34. package/build/cli/commands/projection/tell/meta.js +1 -1
  35. package/build/cli/commands/projection/wait/handler.js +45 -20
  36. package/build/cli/commands/projection/wait/meta.js +1 -1
  37. package/build/cli/commands/shared.js +2 -2
  38. package/build/cli/commands/system/catalog.js +2 -0
  39. package/build/cli/commands/task/add/handler.js +28 -12
  40. package/build/cli/commands/task/add/meta.js +18 -6
  41. package/build/cli/commands/task/catalog.js +4 -0
  42. package/build/cli/commands/task/compose/handler.js +18 -0
  43. package/build/cli/commands/task/compose/meta.js +10 -0
  44. package/build/cli/commands/task/doctor/handler.js +2 -3
  45. package/build/cli/commands/task/doctor/meta.js +2 -2
  46. package/build/cli/commands/task/done/meta.js +2 -2
  47. package/build/cli/commands/task/drop/handler.js +7 -2
  48. package/build/cli/commands/task/drop/meta.js +6 -3
  49. package/build/cli/commands/task/hold/meta.js +2 -2
  50. package/build/cli/commands/task/log/meta.js +2 -2
  51. package/build/cli/commands/task/ls/meta.js +2 -2
  52. package/build/cli/commands/task/note/handler.js +6 -0
  53. package/build/cli/commands/task/note/meta.js +11 -0
  54. package/build/cli/commands/task/resume/meta.js +2 -2
  55. package/build/cli/commands/task/shared.js +29 -15
  56. package/build/cli/commands/task/show/meta.js +2 -2
  57. package/build/cli/commands/task/start/meta.js +2 -2
  58. package/build/cli/commands/task/stop/meta.js +2 -2
  59. package/build/cli/commands/task/update/meta.js +3 -2
  60. package/build/cli/commands/verification/handler.js +43 -0
  61. package/build/cli/commands/verification/meta.js +13 -0
  62. package/build/cli/completion.js +16 -13
  63. package/build/cli/flags.js +48 -12
  64. package/build/cli/help.js +29 -30
  65. package/build/cli/index.js +14 -8
  66. package/build/cli/parse-flags.js +60 -21
  67. package/build/cli/parse-metadata.js +10 -4
  68. package/build/cli/parse-selectors.js +22 -4
  69. package/build/cli/parse.js +43 -30
  70. package/build/cli/projection-address.js +7 -7
  71. package/build/cli/render/arc.js +12 -3
  72. package/build/cli/render/audit.js +2 -2
  73. package/build/cli/render/call.js +4 -4
  74. package/build/cli/render/kanshi.js +122 -85
  75. package/build/cli/render/misc.js +2 -2
  76. package/build/cli/render/petition.js +16 -1
  77. package/build/cli/render/projection-history.js +17 -4
  78. package/build/cli/render/shared.js +5 -5
  79. package/build/cli/render/status-indicator.js +47 -0
  80. package/build/cli/render/status.js +106 -76
  81. package/build/cli/render/success-response.js +20 -5
  82. package/build/cli/render/task.js +33 -5
  83. package/build/cli/render/tell.js +10 -4
  84. package/build/cli/render/tool-command-normalization.js +137 -0
  85. package/build/cli/render/tool-presentation.js +2 -1
  86. package/build/cli/render/verification.js +23 -0
  87. package/build/cli/render/wait.js +101 -33
  88. package/build/cli/types.js +2 -1
  89. package/build/config/akuma-loader.js +21 -1
  90. package/build/config/settings/knobs.js +7 -1
  91. package/build/config/settings/schema.js +10 -5
  92. package/build/core/addressing.js +3 -3
  93. package/build/core/amend.js +102 -17
  94. package/build/core/arc.js +4 -4
  95. package/build/core/audit/candidate.js +10 -18
  96. package/build/core/audit/coordinates.js +12 -17
  97. package/build/core/audit/evidence.js +6 -4
  98. package/build/core/audit/facade.js +3 -3
  99. package/build/core/audit/report.js +1 -1
  100. package/build/core/bind-reconciliation.js +42 -23
  101. package/build/core/bind.js +182 -81
  102. package/build/core/call/call.js +6 -7
  103. package/build/core/call/context.js +46 -33
  104. package/build/core/call/execution.js +21 -6
  105. package/build/core/call/prompt.js +11 -42
  106. package/build/core/contract-carrier-runtime.js +209 -105
  107. package/build/core/contract-carrier.js +156 -73
  108. package/build/core/contract-view.js +43 -6
  109. package/build/core/contract.js +2 -1
  110. package/build/core/derived-replay.js +168 -35
  111. package/build/core/draft.js +60 -27
  112. package/build/core/forfeit.js +1 -1
  113. package/build/core/lifecycle-history.js +13 -0
  114. package/build/core/lifecycle-recovery.js +32 -27
  115. package/build/core/lifecycle-runner.js +14 -14
  116. package/build/core/log.js +1 -1
  117. package/build/core/outcome-base.js +2 -2
  118. package/build/core/projection/akuma-name.js +43 -0
  119. package/build/core/projection/generation/database.js +141 -79
  120. package/build/core/projection/generation/ledger.js +91 -0
  121. package/build/core/projection/generation/model.js +19 -14
  122. package/build/core/projection/generation/projection-generation-continuation.js +1 -1
  123. package/build/core/projection/generation/projection-generation-doorbell.js +88 -0
  124. package/build/core/projection/generation/projection-generation-execution.js +24 -0
  125. package/build/core/projection/generation/projection-generation-identity.js +4 -0
  126. package/build/core/projection/generation/projection-generation-launcher.js +72 -138
  127. package/build/core/projection/generation/projection-generation-process.js +16 -15
  128. package/build/core/projection/generation/projection-generation-runner.js +132 -76
  129. package/build/core/projection/generation/projection-generation-runtime.js +10 -61
  130. package/build/core/projection/generation/protocol.js +68 -0
  131. package/build/core/projection/generation/store.js +17 -7
  132. package/build/core/projection/generation/transitions.js +24 -14
  133. package/build/core/projection/index.js +5 -5
  134. package/build/core/projection/projection-core.js +6 -2
  135. package/build/core/projection/projection-execution-observer.js +1 -1
  136. package/build/core/projection/projection-history.js +8 -8
  137. package/build/core/projection/projection-kill.js +20 -10
  138. package/build/core/projection/projection-life-observer.js +8 -8
  139. package/build/core/projection/projection-mint.js +6 -8
  140. package/build/core/projection/projection-status-observation.js +77 -8
  141. package/build/core/projection/projection-status.js +151 -4
  142. package/build/core/projection/projection-wait.js +48 -29
  143. package/build/core/projection/projection-wake.js +28 -88
  144. package/build/core/projection/tell/launch-store.js +1 -1
  145. package/build/core/projection/tell/store.js +1 -1
  146. package/build/core/renew-build.js +204 -74
  147. package/build/core/renew-plan.js +34 -9
  148. package/build/core/renew-rewrite.js +56 -15
  149. package/build/core/renew.js +39 -35
  150. package/build/core/repository-ledger/accepted-fold-read.js +6 -3
  151. package/build/core/repository-ledger/claim-evidence.js +1 -1
  152. package/build/core/repository-ledger/codec.js +11 -5
  153. package/build/core/repository-ledger/current-state-store.js +464 -349
  154. package/build/core/repository-ledger/fold-repository.js +5 -6
  155. package/build/core/repository-ledger/identity.js +60 -0
  156. package/build/core/repository-ledger/inventory.js +3 -3
  157. package/build/core/repository-ledger/read-model.js +57 -9
  158. package/build/core/repository-ledger/write-transaction.js +24 -17
  159. package/build/core/run-control/connection-bound-close.js +41 -0
  160. package/build/core/run-control/detached-lease-bootstrap.js +208 -0
  161. package/build/core/run-control/detached-process.js +14 -0
  162. package/build/core/run-control/index.js +6 -0
  163. package/build/core/{process-group.js → run-control/process-tree.js} +1 -1
  164. package/build/core/run-control/runner-lease.js +172 -0
  165. package/build/core/run-control/sqlite-process-lifetime-lock.js +174 -0
  166. package/build/core/seal.js +44 -32
  167. package/build/core/settlement/claim-delivery.js +22 -9
  168. package/build/core/settlement/claim.js +8 -6
  169. package/build/core/settlement/petition-claim-gates.js +9 -8
  170. package/build/core/settlement/petition-head-guard.js +5 -6
  171. package/build/core/settlement/petition-preview.js +21 -5
  172. package/build/core/settlement/petition.js +51 -17
  173. package/build/core/settlement/settlement.js +20 -15
  174. package/build/core/settlement/verdict.js +2 -2
  175. package/build/core/settlement/verification.js +492 -364
  176. package/build/core/status/board.js +184 -75
  177. package/build/core/status/drift.js +8 -10
  178. package/build/core/task/board.js +1 -0
  179. package/build/core/task/commands.js +25 -0
  180. package/build/core/task/compose.js +328 -0
  181. package/build/core/task/document.js +102 -39
  182. package/build/core/task/index.js +4 -3
  183. package/build/core/task/settlement-git.js +70 -30
  184. package/build/core/task/settlement-policy.js +27 -14
  185. package/build/core/task/source-board.js +1 -0
  186. package/build/core/task/task-bind-preparation.js +59 -9
  187. package/build/core/task/task-contract.js +2 -2
  188. package/build/core/task/task-git-store.js +1 -0
  189. package/build/core/task/task-store-repository.js +2 -0
  190. package/build/core/task/task.js +2 -2
  191. package/build/core/transcripts.js +4 -4
  192. package/build/core/verification-declaration.js +95 -0
  193. package/build/core/worktree-bootstrap.js +1 -1
  194. package/build/core/worktree-path.js +7 -11
  195. package/build/flow-error.js +15 -2
  196. package/build/generated/version.js +2 -2
  197. package/build/git/branches.js +41 -70
  198. package/build/git/commits.js +92 -21
  199. package/build/git/core.js +9 -161
  200. package/build/git/diff/preview.js +4 -4
  201. package/build/git/diff/read.js +4 -4
  202. package/build/git/diff/structured.js +5 -5
  203. package/build/git/process.js +229 -0
  204. package/build/git/refs.js +30 -5
  205. package/build/git/staging.js +39 -115
  206. package/build/git/streaming-batch.js +9 -16
  207. package/build/git/worktree.js +104 -91
  208. package/build/index.js +7 -0
  209. package/package.json +2 -2
  210. package/skills/keiyaku-akuma/SKILL.md +27 -21
  211. package/skills/keiyaku-task/SKILL.md +61 -5
  212. package/skills/keiyaku-workflow/SKILL.md +81 -26
  213. package/build/cli/commands/projection/revive/meta.js +0 -12
  214. package/build/core/projection/projection-life-protocol.js +0 -115
  215. package/build/core/projection/projection-runner-lock.js +0 -382
  216. package/build/core/projection/tell/database.js +0 -127
  217. package/build/core/settlement/verification-coordination.js +0 -305
  218. package/build/core/settlement/verification-supervisor.js +0 -275
@@ -5,7 +5,7 @@ import { resolveAcceptedLifecycleTransaction } from "./repository-ledger/accepte
5
5
  import { reconcileCommittedMutation, } from "./lifecycle-recovery.js";
6
6
  function coordinateObject(pins, target) {
7
7
  return {
8
- ledgerHead: pins.ledgerHead,
8
+ ledgerHead: pins.repositoryLedgerHead,
9
9
  targetRef: pins.target.ref,
10
10
  targetHead: pins.target.head,
11
11
  deliveryRef: pins.delivery.ref,
@@ -20,7 +20,7 @@ function sameCoordinates(admission, session) {
20
20
  }
21
21
  function validateReplayCoordinate(session, coordinate) {
22
22
  if (coordinate.state === "bind-aligned") {
23
- const currentHead = session.lifecycle.observedLedgerHead;
23
+ const currentHead = session.pins.repositoryLedgerHead;
24
24
  if (coordinate.authoritativeHead !== currentHead) {
25
25
  return {
26
26
  code: "LIFECYCLE_REPLAY_COORDINATES_CHANGED",
@@ -55,7 +55,7 @@ function validateReplayCoordinate(session, coordinate) {
55
55
  },
56
56
  current: {
57
57
  status: resolved.status,
58
- authoritativeHead: session.lifecycle.observedLedgerHead,
58
+ authoritativeHead: session.pins.repositoryLedgerHead,
59
59
  },
60
60
  };
61
61
  }
@@ -115,12 +115,12 @@ export async function runLifecycle(plan) {
115
115
  };
116
116
  }
117
117
  const attempt = await withContractCarrierSession({ cwd: plan.cwd, contractId: plan.contractId, mode: "observe-only", signal: plan.signal }, async (session) => {
118
- observedHead = session.lifecycle.observedLedgerHead;
118
+ observedHead = session.pins.repositoryLedgerHead;
119
119
  const replayRefusal = validateReplayCoordinate(session, reconciliation.coordinate);
120
120
  if (replayRefusal) {
121
121
  return { kind: "refused", outcome: refusedLifecycleFacts({
122
122
  contractId: plan.contractId,
123
- authoritativeHead: session.lifecycle.observedLedgerHead,
123
+ authoritativeHead: session.pins.repositoryLedgerHead,
124
124
  ...surfaceCoordinateRefusal(plan.mutation, replayRefusal),
125
125
  }) };
126
126
  }
@@ -130,13 +130,13 @@ export async function runLifecycle(plan) {
130
130
  }
131
131
  catch (error) {
132
132
  if (error instanceof FlowError && !isCancellation(error)) {
133
- return { kind: "refused", outcome: refusedLifecycleFacts({ contractId: plan.contractId, authoritativeHead: session.lifecycle.observedLedgerHead, ...flowRefusal(error) }) };
133
+ return { kind: "refused", outcome: refusedLifecycleFacts({ contractId: plan.contractId, authoritativeHead: session.pins.repositoryLedgerHead, ...flowRefusal(error) }) };
134
134
  }
135
135
  throw error;
136
136
  }
137
137
  return {
138
138
  kind: "admitted",
139
- ledgerHead: session.lifecycle.observedLedgerHead,
139
+ ledgerHead: session.pins.repositoryLedgerHead,
140
140
  lifecycle: session.lifecycle,
141
141
  repository: session.repository,
142
142
  pins: session.pins,
@@ -172,20 +172,20 @@ export async function runLifecycle(plan) {
172
172
  throw error;
173
173
  }
174
174
  try {
175
- return await withContractCarrierSession({ cwd: plan.cwd, contractId: plan.contractId, mode: "mutation", signal: plan.signal, ...(plan.renewDeliveryProjector ? { renewDeliveryProjector: plan.renewDeliveryProjector } : {}), ...(plan.terminalWorkspaceCleanup ? { terminalWorkspaceCleanup: plan.terminalWorkspaceCleanup } : {}) }, async (session) => {
176
- observedHead = session.lifecycle.observedLedgerHead;
175
+ return await withContractCarrierSession({ cwd: plan.cwd, contractId: plan.contractId, mode: "mutation", signal: plan.signal, ...(plan.renewDeliveryProjector ? { renewDeliveryProjector: plan.renewDeliveryProjector } : {}), ...(plan.terminalWorkspaceCleanup ? { terminalWorkspaceCleanup: plan.terminalWorkspaceCleanup } : {}), ...(plan.transactionRunner ? { transactionRunner: plan.transactionRunner } : {}) }, async (session) => {
176
+ observedHead = session.pins.repositoryLedgerHead;
177
177
  const replayRefusal = validateReplayCoordinate(session, admission.replay);
178
178
  if (replayRefusal) {
179
179
  return { kind: "noncommitted", outcome: refusedLifecycleFacts({
180
180
  contractId: plan.contractId,
181
- authoritativeHead: session.lifecycle.observedLedgerHead,
181
+ authoritativeHead: session.pins.repositoryLedgerHead,
182
182
  ...surfaceCoordinateRefusal(plan.mutation, replayRefusal),
183
183
  }) };
184
184
  }
185
185
  if (!sameCoordinates(admission, session)) {
186
186
  return { kind: "noncommitted", outcome: refusedLifecycleFacts({
187
187
  contractId: plan.contractId,
188
- authoritativeHead: session.lifecycle.observedLedgerHead,
188
+ authoritativeHead: session.pins.repositoryLedgerHead,
189
189
  code: "LIFECYCLE_COORDINATES_CHANGED",
190
190
  message: "contract coordinates changed during lifecycle preparation",
191
191
  pinned: coordinateObject(admission.pins, admission.target),
@@ -202,7 +202,7 @@ export async function runLifecycle(plan) {
202
202
  refusal = flowRefusal(error);
203
203
  }
204
204
  if (refusal) {
205
- return { kind: "noncommitted", outcome: refusedLifecycleFacts({ contractId: plan.contractId, authoritativeHead: session.lifecycle.observedLedgerHead, ...refusal }) };
205
+ return { kind: "noncommitted", outcome: refusedLifecycleFacts({ contractId: plan.contractId, authoritativeHead: session.pins.repositoryLedgerHead, ...refusal }) };
206
206
  }
207
207
  const effectSession = Object.freeze({
208
208
  stableRoot: session.stableRoot,
@@ -239,7 +239,7 @@ export async function runLifecycle(plan) {
239
239
  if (error instanceof FlowError && !isCancellation(error)) {
240
240
  return { kind: "noncommitted", outcome: refusedLifecycleFacts({
241
241
  contractId: plan.contractId,
242
- authoritativeHead: session.lifecycle.observedLedgerHead,
242
+ authoritativeHead: session.pins.repositoryLedgerHead,
243
243
  ...flowRefusal(error),
244
244
  }) };
245
245
  }
@@ -274,7 +274,7 @@ export async function runLifecycle(plan) {
274
274
  kind: "noncommitted",
275
275
  outcome: precommitLifecycleFacts({
276
276
  contractId: plan.contractId,
277
- authoritativeHead: session.lifecycle.observedLedgerHead,
277
+ authoritativeHead: session.pins.repositoryLedgerHead,
278
278
  journal: session.journal,
279
279
  cancellationObserved: session.cancellationObserved(),
280
280
  }),
package/build/core/log.js CHANGED
@@ -44,7 +44,7 @@ export async function readContractLog(input) {
44
44
  at: entry.at,
45
45
  actor: entry.actor,
46
46
  })),
47
- markdown: renderCurrentContractView(model),
47
+ markdown: renderCurrentContractView(model, read.observedLedgerHead),
48
48
  ...(warning ? { amendmentWarning: warning } : {}),
49
49
  };
50
50
  }
@@ -88,14 +88,14 @@ export class EffectJournal {
88
88
  }
89
89
  }
90
90
  export function carrierLifecycleEffect(carrier) {
91
- const idempotencyKey = `carrier:${carrier.ledgerHead}`;
91
+ const idempotencyKey = `carrier:${carrier.contractHead}`;
92
92
  if (carrier.status === "failed") {
93
93
  return { effect: "carrier-projection", idempotencyKey, status: "failed", cause: { code: "CARRIER_MATERIALIZATION_FAILED", message: carrier.cause } };
94
94
  }
95
95
  if (carrier.status === "target-unavailable") {
96
96
  return { effect: "carrier-projection", idempotencyKey, status: "failed", cause: { code: "CARRIER_TARGET_UNAVAILABLE", message: carrier.reason } };
97
97
  }
98
- return { effect: "carrier-projection", idempotencyKey, status: "succeeded", receipt: { ledgerHead: carrier.ledgerHead, status: carrier.status } };
98
+ return { effect: "carrier-projection", idempotencyKey, status: "succeeded", receipt: { contractHead: carrier.contractHead, status: carrier.status } };
99
99
  }
100
100
  export function committedLifecycleFacts(input) {
101
101
  return {
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ const WINDOWS_RESERVED_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
3
+ const FORBIDDEN_CODE_POINT = /[\u0000-\u001f\u007f-\u009f\uD800-\uDFFF]/u;
4
+ const FORBIDDEN_PORTABLE_PATH_CHARACTER = /[\\/<>:"|?*]/u;
5
+ const MAX_AKUMA_NAME_BYTES = 255;
6
+ export function canonicalizeAkumaName(value) {
7
+ return value.normalize("NFC");
8
+ }
9
+ export function akumaNameProblem(value) {
10
+ if (value.length === 0)
11
+ return "must not be empty.";
12
+ if (value !== canonicalizeAkumaName(value))
13
+ return "must use Unicode NFC normalization.";
14
+ if (value === "." || value === ".." || value.startsWith(".")) {
15
+ return "must not be a dot path or hidden filesystem name.";
16
+ }
17
+ if (FORBIDDEN_CODE_POINT.test(value))
18
+ return "must not contain control characters or unpaired surrogates.";
19
+ if (/\p{White_Space}/u.test(value))
20
+ return "must not contain whitespace.";
21
+ if (FORBIDDEN_PORTABLE_PATH_CHARACTER.test(value))
22
+ return "must not contain portable filesystem separators or reserved punctuation.";
23
+ if (/[. ]$/u.test(value))
24
+ return "must not end with a dot or space.";
25
+ if (WINDOWS_RESERVED_NAME.test(value))
26
+ return "must not be a reserved Windows filename.";
27
+ if (Buffer.byteLength(value, "utf8") > MAX_AKUMA_NAME_BYTES) {
28
+ return `must contain at most ${MAX_AKUMA_NAME_BYTES} UTF-8 bytes.`;
29
+ }
30
+ return null;
31
+ }
32
+ /**
33
+ * Akuma identity is exact NFC text. Case, variation selectors, ZWJ sequences,
34
+ * and visually confusable code points are intentionally not folded.
35
+ */
36
+ export const akumaNameSchema = z.string().superRefine((value, ctx) => {
37
+ const problem = akumaNameProblem(value);
38
+ if (problem !== null)
39
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: problem });
40
+ });
41
+ export function isAkumaName(value) {
42
+ return akumaNameProblem(value) === null;
43
+ }
@@ -1,20 +1,22 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
+ import { ProjectionStateError } from "../../atomic-publish.js";
4
5
  import { configureHeartBusyTimeout } from "../heart.js";
5
6
  import { ProjectionGenerationStoreError, parseStoredRow, validateHistory, } from "./model.js";
7
+ import { ProjectionGenerationLedger } from "./ledger.js";
6
8
  export const PROJECTION_GENERATION_STORE_FILE = "heart";
7
- export const PROJECTION_GENERATION_SCHEMA_VERSION = 4;
8
- const SCHEMA_TABLE_SQL = `
9
+ export const PROJECTION_GENERATION_SCHEMA_VERSION = 6;
10
+ const GENERATION_SCHEMA_SQL = `
9
11
  CREATE TABLE generation_schema (
10
12
  singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
11
13
  version INTEGER NOT NULL CHECK (version = ${PROJECTION_GENERATION_SCHEMA_VERSION})
12
14
  ) STRICT
13
15
  `;
14
- const RECORD_TABLE_SQL = `
16
+ const GENERATION_RECORD_SQL = `
15
17
  CREATE TABLE generation_records (
16
18
  seq INTEGER PRIMARY KEY CHECK (seq > 0),
17
- kind TEXT NOT NULL CHECK (kind IN ('launch', 'adoption', 'kill-intent', 'interrupt-intent', 'verdict')),
19
+ activity_kind TEXT NOT NULL CHECK (activity_kind IN ('launch', 'adoption', 'kill-intent', 'interrupt-intent', 'verdict')),
18
20
  execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
19
21
  facts_json TEXT NOT NULL CHECK (json_valid(facts_json))
20
22
  ) STRICT
@@ -92,6 +94,34 @@ const TELL_LAUNCH_COORDINATE_TABLE_SQL = `
92
94
  ) STRICT
93
95
  `;
94
96
  const closedDatabases = new WeakSet();
97
+ function projectionHeartPath(projectionDirectory) {
98
+ return path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
99
+ }
100
+ function openHeartConnection(databasePath, readOnly, foreignKeys = false) {
101
+ const database = new DatabaseSync(databasePath, {
102
+ readOnly,
103
+ ...(foreignKeys ? { enableForeignKeyConstraints: true } : {}),
104
+ enableDoubleQuotedStringLiterals: false,
105
+ allowExtension: false,
106
+ });
107
+ try {
108
+ configureHeartBusyTimeout(database);
109
+ if (!readOnly) {
110
+ database.exec("PRAGMA journal_mode = DELETE");
111
+ database.exec("PRAGMA synchronous = FULL");
112
+ database.exec("PRAGMA foreign_keys = ON");
113
+ database.exec("PRAGMA trusted_schema = OFF");
114
+ }
115
+ return database;
116
+ }
117
+ catch (error) {
118
+ try {
119
+ database.close();
120
+ }
121
+ catch { /* preserve configuration failure */ }
122
+ throw error;
123
+ }
124
+ }
95
125
  /** Classify only proved missing schema evidence as historical incompatibility. */
96
126
  export function projectionGenerationStoreInspectionError(databasePath, error) {
97
127
  if (error instanceof ProjectionGenerationStoreError)
@@ -153,14 +183,16 @@ function validateSchemaShape(database) {
153
183
  throw new ProjectionGenerationStoreError(`projection generation schema contains unexpected objects: ${signature || "none"}`, { observation: "incompatible" });
154
184
  }
155
185
  const metadata = database.prepare("SELECT singleton, version FROM generation_schema").all();
156
- if (metadata.length !== 1 || Number(metadata[0].singleton) !== 1 || Number(metadata[0].version) !== PROJECTION_GENERATION_SCHEMA_VERSION) {
186
+ if (metadata.length !== 1
187
+ || Number(metadata[0].singleton) !== 1
188
+ || Number(metadata[0].version) !== PROJECTION_GENERATION_SCHEMA_VERSION) {
157
189
  throw new ProjectionGenerationStoreError("projection generation schema metadata is malformed", { observation: "incompatible" });
158
190
  }
159
- const recordColumns = database.prepare("PRAGMA table_info(generation_records)").all();
160
- const columnSignature = recordColumns
191
+ const generationColumns = database.prepare("PRAGMA table_info(generation_records)").all();
192
+ const generationSignature = generationColumns
161
193
  .map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
162
194
  .join(",");
163
- if (columnSignature !== "seq:INTEGER:0:1,kind:TEXT:1:0,execution_id:TEXT:1:0,facts_json:TEXT:1:0") {
195
+ if (generationSignature !== "seq:INTEGER:0:1,activity_kind:TEXT:1:0,execution_id:TEXT:1:0,facts_json:TEXT:1:0") {
164
196
  throw new ProjectionGenerationStoreError("projection generation record schema is malformed", { observation: "incompatible" });
165
197
  }
166
198
  const identityColumns = database.prepare("PRAGMA table_info(projection_identity)").all();
@@ -228,8 +260,10 @@ function initializeOrValidateSchema(database) {
228
260
  ORDER BY name
229
261
  `).all();
230
262
  if (version === 0 && objects.length === 0) {
231
- database.exec(SCHEMA_TABLE_SQL);
232
- database.exec(RECORD_TABLE_SQL);
263
+ database.exec(GENERATION_SCHEMA_SQL);
264
+ database.exec(GENERATION_RECORD_SQL);
265
+ database.prepare("INSERT INTO generation_schema (singleton, version) VALUES (1, ?)")
266
+ .run(PROJECTION_GENERATION_SCHEMA_VERSION);
233
267
  database.exec(IDENTITY_TABLE_SQL);
234
268
  database.exec(TELL_TABLE_SQL);
235
269
  database.exec(TELL_RECEIPT_TABLE_SQL);
@@ -237,8 +271,6 @@ function initializeOrValidateSchema(database) {
237
271
  database.exec(TELL_LAUNCH_BATCH_TABLE_SQL);
238
272
  database.exec(TELL_LAUNCH_ADMISSION_TABLE_SQL);
239
273
  database.exec(TELL_LAUNCH_COORDINATE_TABLE_SQL);
240
- database.prepare("INSERT INTO generation_schema (singleton, version) VALUES (1, ?)")
241
- .run(PROJECTION_GENERATION_SCHEMA_VERSION);
242
274
  database.exec(`PRAGMA user_version = ${PROJECTION_GENERATION_SCHEMA_VERSION}`);
243
275
  }
244
276
  else if (version !== PROJECTION_GENERATION_SCHEMA_VERSION) {
@@ -256,22 +288,10 @@ export function openMutableDatabase(projectionDirectory) {
256
288
  const stat = fs.statSync(projectionDirectory);
257
289
  if (!stat.isDirectory())
258
290
  throw new ProjectionGenerationStoreError("projection generation store parent must be a directory");
259
- const databasePath = path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
291
+ const databasePath = projectionHeartPath(projectionDirectory);
260
292
  let database;
261
293
  try {
262
- database = new DatabaseSync(databasePath, {
263
- enableForeignKeyConstraints: true,
264
- enableDoubleQuotedStringLiterals: false,
265
- allowExtension: false,
266
- });
267
- configureHeartBusyTimeout(database);
268
- // Rollback journal keeps observers genuinely read-only. A read-only WAL
269
- // connection creates/updates -wal and -shm coordination files, while
270
- // immutable WAL reads can miss concurrently committed facts.
271
- database.exec("PRAGMA journal_mode = DELETE");
272
- database.exec("PRAGMA synchronous = FULL");
273
- database.exec("PRAGMA foreign_keys = ON");
274
- database.exec("PRAGMA trusted_schema = OFF");
294
+ database = openHeartConnection(databasePath, false, true);
275
295
  }
276
296
  catch (error) {
277
297
  try {
@@ -282,7 +302,7 @@ export function openMutableDatabase(projectionDirectory) {
282
302
  }
283
303
  try {
284
304
  initializeOrValidateSchema(database);
285
- readValidatedHistory(database);
305
+ readValidatedHistory(database, false);
286
306
  return { databasePath, database };
287
307
  }
288
308
  catch (error) {
@@ -296,7 +316,7 @@ export function openReadOnlyDatabase(projectionDirectory) {
296
316
  const stat = fs.statSync(projectionDirectory);
297
317
  if (!stat.isDirectory())
298
318
  throw new ProjectionGenerationStoreError("projection generation store parent must be a directory");
299
- const databasePath = path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
319
+ const databasePath = projectionHeartPath(projectionDirectory);
300
320
  try {
301
321
  if (!fs.lstatSync(databasePath).isFile()) {
302
322
  throw new ProjectionGenerationStoreError("projection generation store must be a regular file", { observation: "incompatible" });
@@ -307,14 +327,9 @@ export function openReadOnlyDatabase(projectionDirectory) {
307
327
  }
308
328
  let database;
309
329
  try {
310
- database = new DatabaseSync(databasePath, {
311
- readOnly: true,
312
- enableDoubleQuotedStringLiterals: false,
313
- allowExtension: false,
314
- });
315
- configureHeartBusyTimeout(database);
330
+ database = openHeartConnection(databasePath, true);
316
331
  validateSchemaShape(database);
317
- readValidatedHistory(database);
332
+ readValidatedHistory(database, true);
318
333
  return { databasePath, database };
319
334
  }
320
335
  catch (error) {
@@ -325,13 +340,14 @@ export function openReadOnlyDatabase(projectionDirectory) {
325
340
  throw projectionGenerationStoreReadError(databasePath, error);
326
341
  }
327
342
  }
328
- /** Read the validated heart through one consistent SQLite snapshot. */
329
- export function withGenerationReadSnapshot(projectionDirectory, read) {
330
- const { database } = openReadOnlyDatabase(projectionDirectory);
343
+ function withProjectionHeartOperation(open, mode, operation) {
344
+ const database = open();
331
345
  try {
332
- database.exec("BEGIN");
346
+ if (mode === "read")
347
+ return operation(database);
348
+ database.exec(mode === "snapshot" ? "BEGIN" : "BEGIN IMMEDIATE");
333
349
  try {
334
- const result = read(database);
350
+ const result = operation(database);
335
351
  database.exec("COMMIT");
336
352
  return result;
337
353
  }
@@ -344,64 +360,110 @@ export function withGenerationReadSnapshot(projectionDirectory, read) {
344
360
  closeDatabase(database);
345
361
  }
346
362
  }
363
+ /** Read the validated heart through one consistent SQLite snapshot. */
364
+ export function withGenerationReadSnapshot(projectionDirectory, read) {
365
+ return withProjectionHeartOperation(() => openReadOnlyDatabase(projectionDirectory).database, "snapshot", read);
366
+ }
367
+ function openProjectionHeartDatabase(projectionDirectory, readOnly) {
368
+ const databasePath = projectionHeartPath(projectionDirectory);
369
+ if (!fs.lstatSync(databasePath).isFile()) {
370
+ throw new ProjectionStateError("projection heart must be a regular file");
371
+ }
372
+ return openHeartConnection(databasePath, readOnly, true);
373
+ }
374
+ export function withProjectionHeartRead(projectionDirectory, read) {
375
+ return withProjectionHeartOperation(() => openProjectionHeartDatabase(projectionDirectory, true), "read", read);
376
+ }
377
+ export function withProjectionHeartReadSnapshot(projectionDirectory, read) {
378
+ return withProjectionHeartOperation(() => openProjectionHeartDatabase(projectionDirectory, true), "snapshot", read);
379
+ }
380
+ export function withProjectionHeartWrite(projectionDirectory, write) {
381
+ return withProjectionHeartOperation(() => openProjectionHeartDatabase(projectionDirectory, false), "write", write);
382
+ }
383
+ export function openProjectionTellObserver(projectionDirectory) {
384
+ const database = openProjectionHeartDatabase(projectionDirectory, true);
385
+ let closed = false;
386
+ let observedDataVersion;
387
+ const assertOpen = () => {
388
+ if (closed)
389
+ throw new ProjectionStateError("projection tell observer is closed");
390
+ };
391
+ const readDataVersion = () => {
392
+ assertOpen();
393
+ const row = database.prepare("PRAGMA data_version").get();
394
+ const raw = row?.data_version;
395
+ const value = typeof raw === "bigint" ? Number(raw) : raw;
396
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
397
+ throw new ProjectionStateError("PRAGMA data_version returned an invalid value");
398
+ }
399
+ return value;
400
+ };
401
+ return {
402
+ hasDataVersionChanged() {
403
+ const current = readDataVersion();
404
+ if (current === observedDataVersion)
405
+ return false;
406
+ observedDataVersion = current;
407
+ return true;
408
+ },
409
+ hasInbox() {
410
+ assertOpen();
411
+ return database.prepare(`
412
+ SELECT 1
413
+ FROM projection_tells
414
+ WHERE state = 'inbox'
415
+ LIMIT 1
416
+ `).get() !== undefined;
417
+ },
418
+ close() {
419
+ if (closed)
420
+ return;
421
+ closed = true;
422
+ closeDatabase(database);
423
+ },
424
+ };
425
+ }
347
426
  export function assertDatabaseOpen(database) {
348
427
  if (closedDatabases.has(database))
349
428
  throw new ProjectionGenerationStoreError("projection generation store is closed");
350
429
  }
351
- export function readValidatedHistory(database) {
352
- const rows = database.prepare(`
353
- SELECT seq, kind, execution_id, facts_json
354
- FROM generation_records
355
- ORDER BY seq
356
- `).all();
357
- const records = rows.map(parseStoredRow);
430
+ export function connectionBoundGenerationStore(database, readOnly) {
431
+ return new ProjectionGenerationLedger(database, readOnly);
432
+ }
433
+ export function readValidatedHistory(database, readOnly) {
434
+ const records = connectionBoundGenerationStore(database, readOnly).readRecords().map((record) => parseStoredRow({
435
+ seq: record.seq,
436
+ kind: record.kind,
437
+ execution_id: record.executionId,
438
+ facts_json: JSON.stringify(record.facts),
439
+ }));
358
440
  return { records, current: validateHistory(records) };
359
441
  }
360
442
  export function appendRecord(database, kind, executionId, facts, seq) {
361
- const encoded = JSON.stringify(facts);
362
- database.prepare(`
363
- INSERT INTO generation_records (seq, kind, execution_id, facts_json)
364
- VALUES (?, ?, ?, ?)
365
- `).run(seq, kind, executionId, encoded);
366
- return { seq, kind, executionId, facts: JSON.parse(encoded) };
443
+ const record = connectionBoundGenerationStore(database, false)
444
+ .appendRecord(kind, executionId, facts, seq);
445
+ return {
446
+ seq: record.seq,
447
+ kind: record.kind,
448
+ executionId: record.executionId,
449
+ facts: record.facts,
450
+ };
367
451
  }
368
452
  export function writeTransition(database, readOnly, run) {
369
453
  assertDatabaseOpen(database);
370
- if (readOnly) {
371
- throw new ProjectionGenerationStoreError("projection generation store is read-only");
372
- }
373
- if (database.isTransaction)
374
- return run();
375
- database.exec("BEGIN IMMEDIATE");
376
- try {
377
- const result = run();
378
- database.exec("COMMIT");
379
- return result;
380
- }
381
- catch (error) {
382
- rollbackQuietly(database);
383
- throw error;
384
- }
454
+ return connectionBoundGenerationStore(database, readOnly).write(run);
385
455
  }
386
456
  export function beginGenerationWriteTransaction(database, readOnly) {
387
457
  assertDatabaseOpen(database);
388
- if (readOnly)
389
- throw new ProjectionGenerationStoreError("projection generation store is read-only");
390
- if (database.isTransaction)
391
- throw new ProjectionGenerationStoreError("projection generation write transaction is already open");
392
- database.exec("BEGIN IMMEDIATE");
458
+ connectionBoundGenerationStore(database, readOnly).beginWriteTransaction();
393
459
  }
394
460
  export function commitGenerationWriteTransaction(database) {
395
461
  assertDatabaseOpen(database);
396
- if (!database.isTransaction)
397
- throw new ProjectionGenerationStoreError("projection generation write transaction is not open");
398
- database.exec("COMMIT");
462
+ connectionBoundGenerationStore(database, false).commitWriteTransaction();
399
463
  }
400
464
  export function rollbackGenerationWriteTransaction(database) {
401
465
  assertDatabaseOpen(database);
402
- if (!database.isTransaction)
403
- return;
404
- rollbackQuietly(database);
466
+ connectionBoundGenerationStore(database, false).rollbackWriteTransaction();
405
467
  }
406
468
  export function closeDatabase(database) {
407
469
  if (closedDatabases.has(database))
@@ -0,0 +1,91 @@
1
+ import { assertExecutionId, assertPositiveSafeInteger, cloneFacts, ProjectionGenerationStoreError, } from "./model.js";
2
+ const RECORD_KINDS = new Set([
3
+ "launch", "adoption", "kill-intent", "interrupt-intent", "verdict",
4
+ ]);
5
+ /** The projection-owned persistence adapter for generation facts in heart. */
6
+ export class ProjectionGenerationLedger {
7
+ #database;
8
+ #readOnly;
9
+ constructor(database, readOnly) {
10
+ this.#database = database;
11
+ this.#readOnly = readOnly;
12
+ }
13
+ readRecords() {
14
+ const rows = this.#database.prepare(`
15
+ SELECT seq, activity_kind, execution_id, facts_json
16
+ FROM generation_records
17
+ ORDER BY seq
18
+ `).all();
19
+ return rows.map((row) => {
20
+ const seq = assertPositiveSafeInteger(Number(row.seq), "generation row seq");
21
+ if (typeof row.activity_kind !== "string" || !RECORD_KINDS.has(row.activity_kind)) {
22
+ throw new ProjectionGenerationStoreError(`generation row ${seq} has unknown kind '${String(row.activity_kind)}'`);
23
+ }
24
+ if (typeof row.execution_id !== "string")
25
+ throw new ProjectionGenerationStoreError(`generation row ${seq} has invalid execution_id`);
26
+ const executionId = assertExecutionId(row.execution_id);
27
+ if (typeof row.facts_json !== "string")
28
+ throw new ProjectionGenerationStoreError(`generation row ${seq} facts_json must be text`);
29
+ let facts;
30
+ try {
31
+ facts = cloneFacts(JSON.parse(row.facts_json), `generation row ${seq} facts`);
32
+ }
33
+ catch (cause) {
34
+ if (cause instanceof Error && cause.message.startsWith(`generation row ${seq}`))
35
+ throw cause;
36
+ throw new ProjectionGenerationStoreError(`generation row ${seq} facts_json is invalid JSON`, { cause });
37
+ }
38
+ return { seq, kind: row.activity_kind, executionId, facts };
39
+ });
40
+ }
41
+ appendRecord(kind, executionId, facts, seq) {
42
+ if (this.#readOnly)
43
+ throw new ProjectionGenerationStoreError("projection generation store is read-only");
44
+ const encoded = JSON.stringify(facts);
45
+ this.#database.prepare(`
46
+ INSERT INTO generation_records (seq, activity_kind, execution_id, facts_json)
47
+ VALUES (?, ?, ?, ?)
48
+ `).run(seq, kind, executionId, encoded);
49
+ return { seq, kind, executionId, facts: JSON.parse(encoded) };
50
+ }
51
+ write(run) {
52
+ if (this.#readOnly)
53
+ throw new ProjectionGenerationStoreError("projection generation store is read-only");
54
+ if (this.#database.isTransaction)
55
+ return run();
56
+ this.#database.exec("BEGIN IMMEDIATE");
57
+ try {
58
+ const result = run();
59
+ this.#database.exec("COMMIT");
60
+ return result;
61
+ }
62
+ catch (error) {
63
+ this.rollback();
64
+ throw error;
65
+ }
66
+ }
67
+ beginWriteTransaction() {
68
+ if (this.#readOnly)
69
+ throw new ProjectionGenerationStoreError("projection generation store is read-only");
70
+ if (this.#database.isTransaction)
71
+ throw new ProjectionGenerationStoreError("projection generation write transaction is already open");
72
+ this.#database.exec("BEGIN IMMEDIATE");
73
+ }
74
+ commitWriteTransaction() {
75
+ if (!this.#database.isTransaction)
76
+ throw new ProjectionGenerationStoreError("projection generation write transaction is not open");
77
+ this.#database.exec("COMMIT");
78
+ }
79
+ rollbackWriteTransaction() {
80
+ if (this.#database.isTransaction)
81
+ this.rollback();
82
+ }
83
+ rollback() {
84
+ try {
85
+ this.#database.exec("ROLLBACK");
86
+ }
87
+ catch {
88
+ // Preserve the failure that caused rollback.
89
+ }
90
+ }
91
+ }