@astrosheep/keiyaku 2.8.4 → 2.9.0

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 (229) hide show
  1. package/README.md +35 -15
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/{call-terms_v2.js → call-terms.js} +12 -8
  4. package/build/agents/harness/control-types.js +1 -0
  5. package/build/agents/harness/execution-handle.js +23 -3
  6. package/build/agents/harness/index.js +6 -607
  7. package/build/agents/harness/outcome.js +141 -0
  8. package/build/agents/harness/projection.js +310 -0
  9. package/build/agents/harness/router.js +201 -0
  10. package/build/agents/harness/runtime.js +2 -1
  11. package/build/agents/{launch-snapshot_v2.js → launch-snapshot/model.js} +69 -74
  12. package/build/agents/launch-snapshot/resolve.js +72 -0
  13. package/build/agents/opencode-sdk.js +24 -3
  14. package/build/agents/providers/{claude-agent-sdk.js → claude-agent-sdk/adapter.js} +46 -368
  15. package/build/agents/providers/claude-agent-sdk/events.js +218 -0
  16. package/build/agents/providers/claude-agent-sdk/session.js +158 -0
  17. package/build/agents/providers/codex-app-server/adapter.js +347 -0
  18. package/build/agents/providers/codex-app-server/events.js +352 -0
  19. package/build/agents/providers/codex-app-server/session.js +266 -0
  20. package/build/agents/providers/codex-sdk.js +23 -5
  21. package/build/agents/providers/opencode-sdk/adapter.js +344 -0
  22. package/build/agents/providers/opencode-sdk/events.js +354 -0
  23. package/build/agents/providers/opencode-sdk/session.js +180 -0
  24. package/build/agents/providers/pi/adapter.js +269 -0
  25. package/build/agents/providers/pi/checkpoint.js +41 -0
  26. package/build/agents/providers/pi/events.js +282 -0
  27. package/build/agents/{selector_v2.js → selector.js} +8 -6
  28. package/build/cli/commands/akuma/akuma/handler.js +4 -0
  29. package/build/cli/commands/akuma/akuma/meta.js +16 -0
  30. package/build/cli/commands/akuma/catalog.js +8 -0
  31. package/build/cli/commands/akuma/list/handler.js +15 -0
  32. package/build/cli/commands/akuma/list/meta-ls.js +9 -0
  33. package/build/cli/commands/akuma/view/handler.js +34 -0
  34. package/build/cli/commands/akuma/view/meta.js +8 -0
  35. package/build/cli/commands/akuma.js +1 -1
  36. package/build/cli/commands/contract/amend/handler.js +10 -0
  37. package/build/cli/commands/contract/amend/meta.js +12 -0
  38. package/build/cli/commands/contract/arc/handler.js +9 -0
  39. package/build/cli/commands/contract/arc/meta.js +26 -0
  40. package/build/cli/commands/contract/bind/handler.js +23 -0
  41. package/build/cli/commands/contract/bind/meta.js +30 -0
  42. package/build/cli/commands/contract/catalog.js +16 -0
  43. package/build/cli/commands/contract/forfeit/handler.js +13 -0
  44. package/build/cli/commands/contract/forfeit/meta.js +12 -0
  45. package/build/cli/commands/contract/log/handler.js +12 -0
  46. package/build/cli/commands/contract/log/meta.js +12 -0
  47. package/build/cli/commands/contract/petition/handler.js +26 -0
  48. package/build/cli/commands/contract/petition/meta.js +23 -0
  49. package/build/cli/commands/contract/renew/handler.js +10 -0
  50. package/build/cli/commands/contract/renew/meta.js +17 -0
  51. package/build/cli/commands/projection/call/handler.js +39 -0
  52. package/build/cli/commands/projection/call/meta.js +9 -0
  53. package/build/cli/commands/projection/catalog.js +14 -0
  54. package/build/cli/commands/projection/kill/handler.js +35 -0
  55. package/build/cli/commands/projection/kill/meta.js +11 -0
  56. package/build/cli/commands/projection/revive/handler.js +53 -0
  57. package/build/cli/commands/projection/revive/meta.js +9 -0
  58. package/build/cli/commands/projection/status/handler.js +23 -0
  59. package/build/cli/commands/projection/status/meta.js +9 -0
  60. package/build/cli/commands/projection/tell/handler.js +34 -0
  61. package/build/cli/commands/projection/tell/meta.js +9 -0
  62. package/build/cli/commands/projection/wait/handler.js +21 -0
  63. package/build/cli/commands/projection/wait/meta.js +11 -0
  64. package/build/cli/commands/registry.js +100 -0
  65. package/build/cli/commands/shared.js +71 -0
  66. package/build/cli/commands/skills/catalog.js +6 -0
  67. package/build/cli/commands/skills/install/handler.js +10 -0
  68. package/build/cli/commands/skills/install/meta.js +9 -0
  69. package/build/cli/commands/skills/skills/handler.js +4 -0
  70. package/build/cli/commands/skills/skills/meta.js +10 -0
  71. package/build/cli/commands/system/catalog.js +8 -0
  72. package/build/cli/commands/system/completion/handler.js +10 -0
  73. package/build/cli/commands/system/completion/meta.js +9 -0
  74. package/build/cli/commands/system/dump-env/handler.js +3 -0
  75. package/build/cli/commands/system/dump-env/meta.js +10 -0
  76. package/build/cli/commands/system/guide/handler.js +4 -0
  77. package/build/cli/commands/system/guide/meta.js +9 -0
  78. package/build/cli/commands/task/add/handler.js +21 -0
  79. package/build/cli/commands/task/add/meta.js +8 -0
  80. package/build/cli/commands/task/catalog.js +22 -0
  81. package/build/cli/commands/task/doctor/handler.js +4 -0
  82. package/build/cli/commands/task/doctor/meta.js +8 -0
  83. package/build/cli/commands/task/done/handler.js +17 -0
  84. package/build/cli/commands/task/done/meta.js +8 -0
  85. package/build/cli/commands/task/drop/handler.js +6 -0
  86. package/build/cli/commands/task/drop/meta.js +8 -0
  87. package/build/cli/commands/task/ls/handler.js +13 -0
  88. package/build/cli/commands/task/ls/meta.js +8 -0
  89. package/build/cli/commands/task/shared.js +131 -0
  90. package/build/cli/commands/task/start/handler.js +6 -0
  91. package/build/cli/commands/task/start/meta.js +8 -0
  92. package/build/cli/commands/task/stop/handler.js +17 -0
  93. package/build/cli/commands/task/stop/meta.js +8 -0
  94. package/build/cli/commands/task/task/handler.js +5 -0
  95. package/build/cli/commands/task/task/meta.js +11 -0
  96. package/build/cli/commands/task/update/handler.js +18 -0
  97. package/build/cli/commands/task/update/meta.js +8 -0
  98. package/build/cli/commands/task/view/handler.js +14 -0
  99. package/build/cli/commands/task/view/meta.js +8 -0
  100. package/build/cli/completion.js +26 -27
  101. package/build/cli/flags.js +79 -2
  102. package/build/cli/help.js +27 -49
  103. package/build/cli/index.js +121 -374
  104. package/build/cli/output-stream-error-policy.js +17 -0
  105. package/build/cli/parse.js +159 -72
  106. package/build/cli/projection-address.js +5 -0
  107. package/build/cli/render/arc.js +1 -1
  108. package/build/cli/render/errors.js +4 -3
  109. package/build/cli/render/format.js +14 -0
  110. package/build/cli/render/kanshi.js +106 -0
  111. package/build/cli/render/petition.js +24 -1
  112. package/build/cli/render/projection-activity.js +5 -1
  113. package/build/cli/render/shared.js +2 -2
  114. package/build/cli/render/status.js +79 -13
  115. package/build/cli/render/task.js +113 -0
  116. package/build/cli/render/wait.js +23 -3
  117. package/build/cli/types.js +1 -1
  118. package/build/config/{akuma-loader_v2.js → akuma-loader.js} +2 -2
  119. package/build/config/env.js +9 -4
  120. package/build/config/settings/disease.js +26 -0
  121. package/build/config/settings/knobs.js +8 -0
  122. package/build/config/settings/loader.js +244 -0
  123. package/build/config/settings/schema.js +276 -0
  124. package/build/core/addressing.js +1 -1
  125. package/build/core/amend.js +3 -3
  126. package/build/core/arc.js +5 -4
  127. package/build/core/bind-workspace.js +123 -0
  128. package/build/core/bind.js +75 -155
  129. package/build/core/call/call.js +224 -0
  130. package/build/core/call/context.js +238 -0
  131. package/build/core/call/execution.js +217 -0
  132. package/build/core/call/prompt.js +42 -0
  133. package/build/core/call/reference.js +24 -0
  134. package/build/core/call-persist.js +1 -1
  135. package/build/core/claim-delivery.js +247 -0
  136. package/build/core/claim.js +39 -277
  137. package/build/core/context.js +2 -1
  138. package/build/core/{render.js → contract-view.js} +12 -140
  139. package/build/core/draft.js +107 -9
  140. package/build/core/entry.js +9 -1
  141. package/build/core/execution-coordinate.js +4 -20
  142. package/build/core/execution-pact.js +16 -31
  143. package/build/core/forfeit.js +35 -22
  144. package/build/core/hints.js +2 -2
  145. package/build/core/log.js +2 -2
  146. package/build/core/petition-claim-gates.js +8 -6
  147. package/build/core/petition-preview.js +97 -0
  148. package/build/core/petition-run.js +3 -2
  149. package/build/core/petition.js +39 -26
  150. package/build/core/projection/generation/database.js +306 -0
  151. package/build/core/projection/generation/identity.js +84 -0
  152. package/build/core/projection/generation/model.js +201 -0
  153. package/build/core/projection/generation/store.js +73 -0
  154. package/build/core/projection/generation/transitions.js +258 -0
  155. package/build/core/projection/heart.js +0 -307
  156. package/build/core/projection/tell/database.js +109 -0
  157. package/build/core/projection/tell/model.js +97 -0
  158. package/build/core/{projection-tells.js → projection/tell/store.js} +3 -158
  159. package/build/core/projection-activity.js +23 -2
  160. package/build/core/projection-core.js +2 -1
  161. package/build/core/projection-execution-observer.js +259 -0
  162. package/build/core/projection-generation-continuation.js +1 -1
  163. package/build/core/projection-generation-execution.js +90 -20
  164. package/build/core/projection-generation-launcher.js +1 -1
  165. package/build/core/projection-generation-runner.js +198 -4
  166. package/build/core/projection-identity.js +1 -1
  167. package/build/core/projection-kill.js +50 -17
  168. package/build/core/projection-life-observer.js +1 -1
  169. package/build/core/projection-life-protocol.js +10 -0
  170. package/build/core/projection-mint.js +1 -1
  171. package/build/core/projection-status.js +45 -32
  172. package/build/core/projection-wait.js +12 -2
  173. package/build/core/projection-wake.js +5 -2
  174. package/build/core/{queue_v2.js → queue.js} +2 -1
  175. package/build/core/registry.js +1 -1
  176. package/build/core/renew.js +7 -7
  177. package/build/core/response-artifact-catalog-sqlite.js +112 -0
  178. package/build/core/response-artifact-catalog.js +7 -0
  179. package/build/core/seal.js +44 -16
  180. package/build/core/status/board.js +286 -0
  181. package/build/core/status/drift.js +99 -0
  182. package/build/core/status/lifecycle.js +84 -0
  183. package/build/core/status/reconciliation.js +131 -0
  184. package/build/core/stored-agent-event.js +28 -4
  185. package/build/core/target-ref.js +10 -0
  186. package/build/core/task/board.js +175 -0
  187. package/build/core/task/commands.js +211 -0
  188. package/build/core/task/document.js +189 -0
  189. package/build/core/task/model.js +1 -0
  190. package/build/core/task/settlement-git.js +207 -0
  191. package/build/core/task/settlement-policy.js +82 -0
  192. package/build/core/task/source-board.js +84 -0
  193. package/build/core/task/source-model.js +1 -0
  194. package/build/core/task/validation.js +44 -0
  195. package/build/core/task-contract.js +224 -0
  196. package/build/core/task-doctor.js +14 -0
  197. package/build/core/task-git-actor.js +38 -0
  198. package/build/core/task-git-runtime.js +34 -0
  199. package/build/core/task-git-store.js +375 -0
  200. package/build/core/task-repository.js +1 -0
  201. package/build/core/task-store-repository.js +86 -0
  202. package/build/core/task-store.js +1 -0
  203. package/build/core/task-worktree.js +51 -0
  204. package/build/core/task.js +4 -0
  205. package/build/core/transcripts.js +138 -56
  206. package/build/core/validation.js +17 -0
  207. package/build/core/verdict.js +2 -2
  208. package/build/core/worktree-path.js +24 -6
  209. package/build/flow-error.js +3 -2
  210. package/build/generated/version.js +1 -1
  211. package/build/git/commits.js +3 -0
  212. package/build/git/diff/parsers.js +0 -94
  213. package/build/index.js +2 -0
  214. package/build/keiyaku-constants.js +2 -0
  215. package/build/keiyaku.js +4 -3
  216. package/package.json +3 -2
  217. package/skills/keiyaku/SKILL.md +1 -1
  218. package/skills/keiyaku-akuma/SKILL.md +11 -6
  219. package/build/agents/harness/pump.js +0 -158
  220. package/build/agents/providers/codex-app-server.js +0 -864
  221. package/build/agents/providers/opencode-sdk.js +0 -363
  222. package/build/agents/providers/pi.js +0 -557
  223. package/build/config/settings.js +0 -533
  224. package/build/core/call.js +0 -686
  225. package/build/core/projection/generation.js +0 -412
  226. package/build/core/projection/identity.js +0 -81
  227. package/build/core/projection/mint.js +0 -174
  228. package/build/core/projection-generation-store.js +0 -707
  229. package/build/core/status.js +0 -539
@@ -0,0 +1,97 @@
1
+ import { createOathGate } from "./petition-claim-gates.js";
2
+ import { assertPetitionArcReady, assertPetitionFresh, assertPetitionable, loadPetitionPreflightContext, } from "./petition.js";
3
+ import { petitionSealSource, previewSeal } from "./seal.js";
4
+ export const PETITION_PREVIEW_GATE_NAMES = [
5
+ "contract",
6
+ "freshness",
7
+ "arc",
8
+ "state",
9
+ "seal",
10
+ "oath",
11
+ "verification",
12
+ "review",
13
+ ];
14
+ function failureDetail(error) {
15
+ return error instanceof Error ? error.message : String(error);
16
+ }
17
+ async function gate(name, evaluate) {
18
+ try {
19
+ await evaluate();
20
+ return { name, result: "pass" };
21
+ }
22
+ catch (error) {
23
+ return { name, result: "failed", detail: failureDetail(error) };
24
+ }
25
+ }
26
+ function sealPathDetail(paths) {
27
+ return `${paths.length} ${paths.length === 1 ? "file" : "files"} · ${paths.length > 0 ? paths.join(", ") : "(none)"}`;
28
+ }
29
+ const notEvaluated = (name) => ({
30
+ name,
31
+ result: "not_evaluated",
32
+ });
33
+ /**
34
+ * Build a petition preflight without crossing any settlement or provider boundary.
35
+ * Loading the addressed contract is construction: failure there leaves no truthful
36
+ * target/source coordinate and therefore propagates as dry-run exit 2.
37
+ */
38
+ export async function previewPetition(input) {
39
+ const context = await loadPetitionPreflightContext(input.cwd, input.contractId);
40
+ const gates = [{ name: "contract", result: "pass" }];
41
+ const freshness = await gate("freshness", async () => {
42
+ await assertPetitionFresh(input.cwd, input.contractId, context.ledger.entries, context.base);
43
+ });
44
+ gates.push(freshness);
45
+ const arc = freshness.result === "pass"
46
+ ? await gate("arc", async () => {
47
+ await assertPetitionArcReady(context.deliveryCwd, input.contractId, context.ledger.entries);
48
+ })
49
+ : notEvaluated("arc");
50
+ gates.push(arc);
51
+ const state = await gate("state", () => {
52
+ assertPetitionable(input.contractId, context.ledger.entries);
53
+ });
54
+ gates.push(state);
55
+ let seal;
56
+ if (freshness.result === "pass" && arc.result === "pass" && state.result === "pass") {
57
+ try {
58
+ const sealPreview = await previewSeal(context.deliveryCwd, context.ledger.entries, "HEAD", input.waivePaths);
59
+ seal = { name: "seal", result: "pass", detail: sealPathDetail(sealPreview.paths) };
60
+ }
61
+ catch (error) {
62
+ seal = { name: "seal", result: "failed", detail: failureDetail(error) };
63
+ }
64
+ }
65
+ else {
66
+ seal = notEvaluated("seal");
67
+ }
68
+ gates.push(seal);
69
+ const oathOwner = createOathGate();
70
+ const oath = await gate("oath", async () => {
71
+ const result = await oathOwner.evaluate({
72
+ cwd: input.cwd,
73
+ contractId: input.contractId,
74
+ input: {
75
+ cwd: input.cwd,
76
+ contractId: input.contractId,
77
+ intent: "CLAIM",
78
+ title: `Petition ${input.contractId}`,
79
+ plea: `Settle contract ${input.contractId} through the petition pipeline.`,
80
+ oath: input.oath,
81
+ },
82
+ verificationCommands: [],
83
+ baseBranch: context.target.branch,
84
+ reviewNumber: 0,
85
+ });
86
+ if (!result.pass)
87
+ throw new Error(result.message);
88
+ });
89
+ gates.push(oath, notEvaluated("verification"), notEvaluated("review"));
90
+ return {
91
+ contract: input.contractId,
92
+ target: context.target.ref,
93
+ arc: { source: petitionSealSource(context.ledger.entries) },
94
+ gates,
95
+ verdict: gates.some((item) => item.result === "failed") ? "would_fail" : "would_succeed",
96
+ };
97
+ }
@@ -1,8 +1,9 @@
1
- import { assertSettingsKnobUsable, loadKeiyakuSettings } from "../config/settings.js";
1
+ import { assertSettingsKnobUsable } from "../config/settings/disease.js";
2
+ import { loadKeiyakuSettings } from "../config/settings/loader.js";
2
3
  import { FlowError } from "../flow-error.js";
3
4
  import { readCurrentContractContext } from "./context.js";
4
5
  import { readLedger } from "./ledger.js";
5
- import { commissionTarget } from "./status.js";
6
+ import { commissionTarget } from "./status/drift.js";
6
7
  import { getCurrentBranch, isGitRepo } from "../git/branches.js";
7
8
  import { claimKeiyaku } from "./petition-claim.js";
8
9
  import { forfeitKeiyaku } from "./petition-forfeit.js";
@@ -2,9 +2,10 @@ import { FlowError } from "../flow-error.js";
2
2
  import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../git/core.js";
3
3
  import { parseCommandMarkdown } from "./command-io.js";
4
4
  import { activeLedgerRef, appendWithRetry, readLedger } from "./ledger.js";
5
- import { allocateSettlementSeat, queueMembership, readQueueReadModel } from "./queue_v2.js";
6
- import { findOpenArc, receiptsAfterFence, sealOpenArc } from "./seal.js";
7
- import { commissionTarget, deriveContractState, latestBase } from "./status.js";
5
+ import { allocateSettlementSeat, queueMembership, readQueueReadModel } from "./queue.js";
6
+ import { findOpenArc, getMaxArc, receiptsAfterFence, sealCurrentArc } from "./seal.js";
7
+ import { deriveContractState } from "./status/lifecycle.js";
8
+ import { commissionTarget, latestBase } from "./status/drift.js";
8
9
  import { findContractWorktreePath } from "./worktree-path.js";
9
10
  async function resolveCommit(cwd, ref) {
10
11
  const git = createGit(cwd);
@@ -17,7 +18,26 @@ async function resolveCommit(cwd, ref) {
17
18
  throw wrapGitError(`rev-parse ${ref}`, error, cwd);
18
19
  }
19
20
  }
20
- async function assertFresh(cwd, contractId, entries, base) {
21
+ export async function loadPetitionPreflightContext(cwd, contractId) {
22
+ const ledger = await readLedger(cwd, contractId);
23
+ if (!ledger) {
24
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} not found or not active`);
25
+ }
26
+ const base = latestBase(ledger.entries);
27
+ if (!base)
28
+ throw new FlowError("INTERNAL_STATE", "ledger must contain bind entry");
29
+ const target = commissionTarget(ledger.entries);
30
+ if (!target) {
31
+ throw new FlowError("INTERNAL_STATE", `contract ${contractId} missing commission target`);
32
+ }
33
+ return {
34
+ ledger,
35
+ deliveryCwd: (await findContractWorktreePath(cwd, contractId)) ?? cwd,
36
+ base,
37
+ target,
38
+ };
39
+ }
40
+ export async function assertPetitionFresh(cwd, contractId, entries, base) {
21
41
  const target = commissionTarget(entries);
22
42
  if (!target) {
23
43
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} missing commission target`);
@@ -30,31 +50,28 @@ async function assertFresh(cwd, contractId, entries, base) {
30
50
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} must renew onto ${target.branch} before petition`, { hints: [`keiyaku @${contractId} renew — renews delivery before petition`] });
31
51
  }
32
52
  }
33
- function assertPetitionable(contractId, entries) {
53
+ export async function assertPetitionArcReady(deliveryCwd, contractId, entries) {
54
+ if (getMaxArc(entries) <= 0 || findOpenArc(entries))
55
+ return;
56
+ const loose = await receiptsAfterFence(deliveryCwd, entries);
57
+ if (loose.length > 0) {
58
+ throw new FlowError("NO_OPEN_ARC", `${loose.length} commits after fence have no arc to receipt them`, { hints: [`keiyaku @${contractId} arc — opens a new arc to receipt them, then petition`] });
59
+ }
60
+ }
61
+ export function assertPetitionable(contractId, entries) {
34
62
  const state = deriveContractState(entries);
35
- if (state.state !== "active") {
63
+ // bound is legal for derived a0 (sealCurrentArc appends seal 0 before petition).
64
+ if (state.state !== "active" && state.state !== "bound") {
36
65
  throw new FlowError("INTERNAL_STATE", `contract ${contractId} cannot petition from state ${state.state}`);
37
66
  }
38
67
  }
39
68
  export async function petitionContract(input) {
40
69
  const at = new Date(input.nowMs).toISOString();
41
- const ledger = await readLedger(input.cwd, input.contractId);
42
- if (!ledger) {
43
- throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} not found or not active`);
44
- }
45
- const deliveryCwd = (await findContractWorktreePath(input.cwd, input.contractId)) ?? input.cwd;
46
- const base = latestBase(ledger.entries);
47
- if (!base)
48
- throw new FlowError("INTERNAL_STATE", "ledger must contain bind entry");
49
- await assertFresh(input.cwd, input.contractId, ledger.entries, base);
50
- if (!findOpenArc(ledger.entries)) {
51
- const loose = await receiptsAfterFence(deliveryCwd, ledger.entries);
52
- if (loose.length > 0) {
53
- throw new FlowError("NO_OPEN_ARC", `${loose.length} commits after fence have no arc to receipt them`, { hints: [`keiyaku @${input.contractId} arc — opens a new arc to receipt them, then petition`] });
54
- }
55
- }
70
+ const { ledger, deliveryCwd, base, target } = await loadPetitionPreflightContext(input.cwd, input.contractId);
71
+ await assertPetitionFresh(input.cwd, input.contractId, ledger.entries, base);
72
+ await assertPetitionArcReady(deliveryCwd, input.contractId, ledger.entries);
56
73
  assertPetitionable(input.contractId, ledger.entries);
57
- const sealed = await sealOpenArc({
74
+ const sealed = await sealCurrentArc({
58
75
  cwd: deliveryCwd,
59
76
  renderCwd: deliveryCwd,
60
77
  contractId: input.contractId,
@@ -62,10 +79,6 @@ export async function petitionContract(input) {
62
79
  nowMs: input.nowMs,
63
80
  waivePaths: input.intent === "claim" ? input.waivePaths : undefined,
64
81
  });
65
- const target = commissionTarget(ledger.entries);
66
- if (!target) {
67
- throw new FlowError("INTERNAL_STATE", `contract ${input.contractId} missing commission target`);
68
- }
69
82
  const allocated = await allocateSettlementSeat(input.cwd, {
70
83
  target: target.ref,
71
84
  actor: input.actor,
@@ -0,0 +1,306 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { configureHeartBusyTimeout } from "../heart.js";
5
+ import { ProjectionGenerationStoreError, parseStoredRow, validateHistory, } from "./model.js";
6
+ export const PROJECTION_GENERATION_STORE_FILE = "heart";
7
+ export const PROJECTION_GENERATION_SCHEMA_VERSION = 2;
8
+ const SCHEMA_TABLE_SQL = `
9
+ CREATE TABLE generation_schema (
10
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
11
+ version INTEGER NOT NULL CHECK (version = ${PROJECTION_GENERATION_SCHEMA_VERSION})
12
+ ) STRICT
13
+ `;
14
+ const RECORD_TABLE_SQL = `
15
+ CREATE TABLE generation_records (
16
+ seq INTEGER PRIMARY KEY CHECK (seq > 0),
17
+ kind TEXT NOT NULL CHECK (kind IN ('launch', 'adoption', 'kill-intent', 'verdict')),
18
+ execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
19
+ facts_json TEXT NOT NULL CHECK (json_valid(facts_json))
20
+ ) STRICT
21
+ `;
22
+ const IDENTITY_TABLE_SQL = `
23
+ CREATE TABLE projection_identity (
24
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
25
+ version INTEGER NOT NULL CHECK (version = 1),
26
+ pid INTEGER NOT NULL CHECK (pid >= 0),
27
+ akuma TEXT NOT NULL CHECK (length(trim(akuma)) > 0),
28
+ provider TEXT NOT NULL CHECK (length(trim(provider)) > 0),
29
+ authority_json TEXT NOT NULL CHECK (json_valid(authority_json)),
30
+ binding_json TEXT NOT NULL CHECK (json_valid(binding_json)),
31
+ minted_at TEXT NOT NULL CHECK (length(trim(minted_at)) > 0),
32
+ workspace_root TEXT NOT NULL CHECK (length(trim(workspace_root)) > 0),
33
+ effective_policy_json TEXT CHECK (
34
+ effective_policy_json IS NULL OR json_valid(effective_policy_json)
35
+ )
36
+ ) STRICT
37
+ `;
38
+ const TELL_TABLE_SQL = `
39
+ CREATE TABLE projection_tells (
40
+ tell_id TEXT PRIMARY KEY CHECK (length(trim(tell_id)) > 0),
41
+ text TEXT NOT NULL,
42
+ created_at TEXT NOT NULL CHECK (length(trim(created_at)) > 0),
43
+ effort TEXT CHECK (effort IS NULL OR length(trim(effort)) > 0),
44
+ state TEXT NOT NULL CHECK (
45
+ state IN ('inbox', 'inflight', 'submitted', 'consumed', 'delivered')
46
+ ),
47
+ execution_id TEXT CHECK (
48
+ execution_id IS NULL OR length(trim(execution_id)) > 0
49
+ ),
50
+ fence_json TEXT CHECK (fence_json IS NULL OR json_valid(fence_json)),
51
+ replayed_from_json TEXT CHECK (
52
+ replayed_from_json IS NULL OR json_valid(replayed_from_json)
53
+ ),
54
+ delivered_at TEXT CHECK (
55
+ delivered_at IS NULL OR length(trim(delivered_at)) > 0
56
+ )
57
+ ) STRICT
58
+ `;
59
+ const closedDatabases = new WeakSet();
60
+ function pragmaNumber(database, pragma) {
61
+ const row = database.prepare(`PRAGMA ${pragma}`).get();
62
+ const value = row ? Object.values(row)[0] : undefined;
63
+ if (typeof value !== "number" && typeof value !== "bigint") {
64
+ throw new ProjectionGenerationStoreError(`PRAGMA ${pragma} returned an invalid value`);
65
+ }
66
+ return Number(value);
67
+ }
68
+ export function rollbackQuietly(database) {
69
+ try {
70
+ database.exec("ROLLBACK");
71
+ }
72
+ catch {
73
+ // Preserve the transition or schema failure that caused the rollback.
74
+ }
75
+ }
76
+ function validateSchemaShape(database) {
77
+ const version = pragmaNumber(database, "user_version");
78
+ if (version !== PROJECTION_GENERATION_SCHEMA_VERSION) {
79
+ throw new ProjectionGenerationStoreError(`projection generation schema version ${version} is not supported; expected ${PROJECTION_GENERATION_SCHEMA_VERSION}`);
80
+ }
81
+ const objects = database.prepare(`
82
+ SELECT name, type
83
+ FROM sqlite_schema
84
+ WHERE name NOT LIKE 'sqlite_%'
85
+ ORDER BY name
86
+ `).all();
87
+ const signature = objects.map((object) => `${object.type}:${object.name}`).join(",");
88
+ if (signature !== "table:generation_records,table:generation_schema,table:projection_identity,table:projection_tells") {
89
+ throw new ProjectionGenerationStoreError(`projection generation schema contains unexpected objects: ${signature || "none"}`);
90
+ }
91
+ const metadata = database.prepare("SELECT singleton, version FROM generation_schema").all();
92
+ if (metadata.length !== 1 || Number(metadata[0].singleton) !== 1 || Number(metadata[0].version) !== PROJECTION_GENERATION_SCHEMA_VERSION) {
93
+ throw new ProjectionGenerationStoreError("projection generation schema metadata is malformed");
94
+ }
95
+ const recordColumns = database.prepare("PRAGMA table_info(generation_records)").all();
96
+ const columnSignature = recordColumns
97
+ .map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
98
+ .join(",");
99
+ if (columnSignature !== "seq:INTEGER:0:1,kind:TEXT:1:0,execution_id:TEXT:1:0,facts_json:TEXT:1:0") {
100
+ throw new ProjectionGenerationStoreError("projection generation record schema is malformed");
101
+ }
102
+ const identityColumns = database.prepare("PRAGMA table_info(projection_identity)").all();
103
+ const identitySignature = identityColumns
104
+ .map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
105
+ .join(",");
106
+ if (identitySignature !== "singleton:INTEGER:0:1,version:INTEGER:1:0,pid:INTEGER:1:0,akuma:TEXT:1:0,provider:TEXT:1:0,authority_json:TEXT:1:0,binding_json:TEXT:1:0,minted_at:TEXT:1:0,workspace_root:TEXT:1:0,effective_policy_json:TEXT:0:0") {
107
+ throw new ProjectionGenerationStoreError("projection identity schema is malformed");
108
+ }
109
+ const tellColumns = database.prepare("PRAGMA table_info(projection_tells)").all();
110
+ const tellSignature = tellColumns
111
+ .map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
112
+ .join(",");
113
+ if (tellSignature !== "tell_id:TEXT:1:1,text:TEXT:1:0,created_at:TEXT:1:0,effort:TEXT:0:0,state:TEXT:1:0,execution_id:TEXT:0:0,fence_json:TEXT:0:0,replayed_from_json:TEXT:0:0,delivered_at:TEXT:0:0") {
114
+ throw new ProjectionGenerationStoreError("projection tell schema is malformed");
115
+ }
116
+ const quickCheck = database.prepare("PRAGMA quick_check").get();
117
+ if (!quickCheck || Object.values(quickCheck)[0] !== "ok") {
118
+ throw new ProjectionGenerationStoreError("projection generation database failed quick_check");
119
+ }
120
+ }
121
+ function initializeOrValidateSchema(database) {
122
+ database.exec("BEGIN EXCLUSIVE");
123
+ try {
124
+ const version = pragmaNumber(database, "user_version");
125
+ const objects = database.prepare(`
126
+ SELECT name, type
127
+ FROM sqlite_schema
128
+ WHERE name NOT LIKE 'sqlite_%'
129
+ ORDER BY name
130
+ `).all();
131
+ if (version === 0 && objects.length === 0) {
132
+ database.exec(SCHEMA_TABLE_SQL);
133
+ database.exec(RECORD_TABLE_SQL);
134
+ database.exec(IDENTITY_TABLE_SQL);
135
+ database.exec(TELL_TABLE_SQL);
136
+ database.prepare("INSERT INTO generation_schema (singleton, version) VALUES (1, ?)")
137
+ .run(PROJECTION_GENERATION_SCHEMA_VERSION);
138
+ database.exec(`PRAGMA user_version = ${PROJECTION_GENERATION_SCHEMA_VERSION}`);
139
+ }
140
+ else if (version !== PROJECTION_GENERATION_SCHEMA_VERSION) {
141
+ throw new ProjectionGenerationStoreError(`projection generation schema version ${version} is not supported; expected ${PROJECTION_GENERATION_SCHEMA_VERSION}`);
142
+ }
143
+ validateSchemaShape(database);
144
+ database.exec("COMMIT");
145
+ }
146
+ catch (error) {
147
+ rollbackQuietly(database);
148
+ throw error;
149
+ }
150
+ }
151
+ export function openMutableDatabase(projectionDirectory) {
152
+ const stat = fs.statSync(projectionDirectory);
153
+ if (!stat.isDirectory())
154
+ throw new ProjectionGenerationStoreError("projection generation store parent must be a directory");
155
+ const databasePath = path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
156
+ let database;
157
+ try {
158
+ database = new DatabaseSync(databasePath, {
159
+ enableForeignKeyConstraints: true,
160
+ enableDoubleQuotedStringLiterals: false,
161
+ allowExtension: false,
162
+ });
163
+ configureHeartBusyTimeout(database);
164
+ // Rollback journal keeps observers genuinely read-only. A read-only WAL
165
+ // connection creates/updates -wal and -shm coordination files, while
166
+ // immutable WAL reads can miss concurrently committed facts.
167
+ database.exec("PRAGMA journal_mode = DELETE");
168
+ database.exec("PRAGMA synchronous = FULL");
169
+ database.exec("PRAGMA foreign_keys = ON");
170
+ database.exec("PRAGMA trusted_schema = OFF");
171
+ }
172
+ catch (error) {
173
+ try {
174
+ database?.close();
175
+ }
176
+ catch { /* preserve the open/configuration failure */ }
177
+ throw new ProjectionGenerationStoreError(`cannot open projection generation store at ${databasePath}`, { cause: error });
178
+ }
179
+ try {
180
+ initializeOrValidateSchema(database);
181
+ readValidatedHistory(database);
182
+ return { databasePath, database };
183
+ }
184
+ catch (error) {
185
+ database.close();
186
+ if (error instanceof ProjectionGenerationStoreError)
187
+ throw error;
188
+ throw new ProjectionGenerationStoreError(`invalid projection generation store at ${databasePath}`, { cause: error });
189
+ }
190
+ }
191
+ export function openReadOnlyDatabase(projectionDirectory) {
192
+ const stat = fs.statSync(projectionDirectory);
193
+ if (!stat.isDirectory())
194
+ throw new ProjectionGenerationStoreError("projection generation store parent must be a directory");
195
+ const databasePath = path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
196
+ try {
197
+ if (!fs.lstatSync(databasePath).isFile()) {
198
+ throw new ProjectionGenerationStoreError("projection generation store must be a regular file");
199
+ }
200
+ }
201
+ catch (error) {
202
+ if (error instanceof ProjectionGenerationStoreError)
203
+ throw error;
204
+ throw new ProjectionGenerationStoreError(`projection generation store does not exist at ${databasePath}`, { cause: error });
205
+ }
206
+ let database;
207
+ try {
208
+ database = new DatabaseSync(databasePath, {
209
+ readOnly: true,
210
+ enableDoubleQuotedStringLiterals: false,
211
+ allowExtension: false,
212
+ });
213
+ configureHeartBusyTimeout(database);
214
+ validateSchemaShape(database);
215
+ readValidatedHistory(database);
216
+ return { databasePath, database };
217
+ }
218
+ catch (error) {
219
+ try {
220
+ database?.close();
221
+ }
222
+ catch { /* preserve the read/open failure */ }
223
+ if (error instanceof ProjectionGenerationStoreError)
224
+ throw error;
225
+ throw new ProjectionGenerationStoreError(`cannot read projection generation store at ${databasePath}`, { cause: error });
226
+ }
227
+ }
228
+ export function assertDatabaseOpen(database) {
229
+ if (closedDatabases.has(database))
230
+ throw new ProjectionGenerationStoreError("projection generation store is closed");
231
+ }
232
+ export function readValidatedHistory(database) {
233
+ const rows = database.prepare(`
234
+ SELECT seq, kind, execution_id, facts_json
235
+ FROM generation_records
236
+ ORDER BY seq
237
+ `).all();
238
+ const records = rows.map(parseStoredRow);
239
+ return { records, current: validateHistory(records) };
240
+ }
241
+ export function appendRecord(database, kind, executionId, facts, seq) {
242
+ const encoded = JSON.stringify(facts);
243
+ database.prepare(`
244
+ INSERT INTO generation_records (seq, kind, execution_id, facts_json)
245
+ VALUES (?, ?, ?, ?)
246
+ `).run(seq, kind, executionId, encoded);
247
+ return { seq, kind, executionId, facts: JSON.parse(encoded) };
248
+ }
249
+ export function writeTransition(database, readOnly, run) {
250
+ assertDatabaseOpen(database);
251
+ if (readOnly) {
252
+ throw new ProjectionGenerationStoreError("projection generation store is read-only");
253
+ }
254
+ database.exec("BEGIN IMMEDIATE");
255
+ try {
256
+ const result = run();
257
+ database.exec("COMMIT");
258
+ return result;
259
+ }
260
+ catch (error) {
261
+ rollbackQuietly(database);
262
+ throw error;
263
+ }
264
+ }
265
+ export function closeDatabase(database) {
266
+ if (closedDatabases.has(database))
267
+ return;
268
+ closedDatabases.add(database);
269
+ database.close();
270
+ }
271
+ /**
272
+ * Discovery-only predicate: a readable regular heart whose PRAGMA user_version
273
+ * is exactly 1 is recognized historical residue. Never opens through the store
274
+ * class, never mutates, and any observation failure returns false so callers
275
+ * proceed into the strict current-schema reader.
276
+ */
277
+ export function isProjectionGenerationSchemaV1Residue(projectionDirectory) {
278
+ const databasePath = path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
279
+ let database;
280
+ let residue = false;
281
+ try {
282
+ if (!fs.lstatSync(databasePath).isFile())
283
+ return false;
284
+ database = new DatabaseSync(databasePath, {
285
+ readOnly: true,
286
+ enableDoubleQuotedStringLiterals: false,
287
+ allowExtension: false,
288
+ });
289
+ configureHeartBusyTimeout(database);
290
+ residue = pragmaNumber(database, "user_version") === 1;
291
+ }
292
+ catch {
293
+ return false;
294
+ }
295
+ finally {
296
+ if (database) {
297
+ try {
298
+ database.close();
299
+ }
300
+ catch {
301
+ residue = false;
302
+ }
303
+ }
304
+ }
305
+ return residue;
306
+ }
@@ -0,0 +1,84 @@
1
+ import { parseExecutionPact, } from "../../execution-pact.js";
2
+ import { assertDatabaseOpen, rollbackQuietly, } from "./database.js";
3
+ import { ProjectionGenerationStoreError } from "./model.js";
4
+ export function initializeIdentity(database, databasePath, readOnly, identity) {
5
+ assertDatabaseOpen(database);
6
+ if (readOnly) {
7
+ throw new ProjectionGenerationStoreError("projection identity store is read-only");
8
+ }
9
+ const validated = parseExecutionPact(identity, `${databasePath}#projection_identity`);
10
+ database.exec("BEGIN IMMEDIATE");
11
+ try {
12
+ const existing = database.prepare("SELECT COUNT(*) AS count FROM projection_identity").get();
13
+ if (Number(existing?.count ?? 0) !== 0) {
14
+ throw new ProjectionGenerationStoreError("projection identity is already initialized");
15
+ }
16
+ database.prepare(`
17
+ INSERT INTO projection_identity (
18
+ singleton,
19
+ version,
20
+ pid,
21
+ akuma,
22
+ provider,
23
+ authority_json,
24
+ binding_json,
25
+ minted_at,
26
+ workspace_root,
27
+ effective_policy_json
28
+ ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
29
+ `).run(validated.version, validated.pid, validated.akuma, validated.provider, JSON.stringify(validated.authority), JSON.stringify(validated.binding), validated.mintedAt, validated.workspaceRoot, validated.effectivePolicy === undefined
30
+ ? null
31
+ : JSON.stringify(validated.effectivePolicy));
32
+ database.exec("COMMIT");
33
+ }
34
+ catch (error) {
35
+ rollbackQuietly(database);
36
+ throw error;
37
+ }
38
+ }
39
+ export function readIdentity(database, databasePath) {
40
+ assertDatabaseOpen(database);
41
+ const rows = database.prepare(`
42
+ SELECT
43
+ version,
44
+ pid,
45
+ akuma,
46
+ provider,
47
+ authority_json,
48
+ binding_json,
49
+ minted_at,
50
+ workspace_root,
51
+ effective_policy_json
52
+ FROM projection_identity
53
+ `).all();
54
+ if (rows.length !== 1) {
55
+ throw new ProjectionGenerationStoreError(`projection identity row count must be 1, found ${rows.length}`);
56
+ }
57
+ const row = rows[0];
58
+ let authority;
59
+ let binding;
60
+ let effectivePolicy;
61
+ try {
62
+ authority = JSON.parse(row.authority_json);
63
+ binding = JSON.parse(row.binding_json);
64
+ effectivePolicy = row.effective_policy_json === null
65
+ ? undefined
66
+ : JSON.parse(row.effective_policy_json);
67
+ }
68
+ catch (error) {
69
+ throw new ProjectionGenerationStoreError("projection identity contains malformed JSON", {
70
+ cause: error,
71
+ });
72
+ }
73
+ return parseExecutionPact({
74
+ version: Number(row.version),
75
+ pid: Number(row.pid),
76
+ akuma: row.akuma,
77
+ provider: row.provider,
78
+ authority,
79
+ binding,
80
+ mintedAt: row.minted_at,
81
+ workspaceRoot: row.workspace_root,
82
+ ...(effectivePolicy === undefined ? {} : { effectivePolicy }),
83
+ }, `${databasePath}#projection_identity`);
84
+ }