@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
@@ -1,316 +1,9 @@
1
- import * as fs from "node:fs";
2
1
  import * as path from "node:path";
3
- import { DatabaseSync } from "node:sqlite";
4
- import { isErrnoException } from "../../errno.js";
5
2
  export const HEART_FILE = "heart";
6
- const HEART_SCHEMA_VERSION = 1;
7
3
  export const HEART_BUSY_TIMEOUT_MS = 5_000;
8
- export class ProjectionHeartError extends Error {
9
- code = "PROJECTION_HEART_ERROR";
10
- constructor(message, options) {
11
- super(message, options);
12
- this.name = "ProjectionHeartError";
13
- }
14
- }
15
- const HEART_SCHEMA_SQL = `
16
- CREATE TABLE heart_schema (
17
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
18
- version INTEGER NOT NULL CHECK (version = ${HEART_SCHEMA_VERSION})
19
- ) STRICT
20
- `;
21
- const IDENTITY_TABLE_SQL = `
22
- CREATE TABLE projection_identity (
23
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
24
- version INTEGER NOT NULL CHECK (version = 1),
25
- pid INTEGER NOT NULL CHECK (pid >= 0),
26
- akuma TEXT NOT NULL CHECK (length(trim(akuma)) > 0),
27
- provider TEXT NOT NULL CHECK (length(trim(provider)) > 0),
28
- authority_json TEXT NOT NULL CHECK (json_valid(authority_json)),
29
- binding_json TEXT NOT NULL CHECK (json_valid(binding_json)),
30
- minted_at TEXT NOT NULL CHECK (length(trim(minted_at)) > 0),
31
- workspace_root TEXT NOT NULL CHECK (length(trim(workspace_root)) > 0),
32
- effective_policy_json TEXT CHECK (
33
- effective_policy_json IS NULL OR json_valid(effective_policy_json)
34
- )
35
- ) STRICT
36
- `;
37
- const RECORD_TABLE_SQL = `
38
- CREATE TABLE generation_records (
39
- seq INTEGER PRIMARY KEY CHECK (seq > 0),
40
- kind TEXT NOT NULL CHECK (kind IN ('launch', 'adoption', 'verdict')),
41
- execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
42
- payload_json TEXT NOT NULL CHECK (json_valid(payload_json))
43
- ) STRICT
44
- `;
45
- const TELL_TABLE_SQL = `
46
- CREATE TABLE projection_tells (
47
- tell_id TEXT PRIMARY KEY CHECK (length(trim(tell_id)) > 0),
48
- text TEXT NOT NULL,
49
- created_at TEXT NOT NULL CHECK (length(trim(created_at)) > 0),
50
- effort TEXT CHECK (effort IS NULL OR length(trim(effort)) > 0),
51
- state TEXT NOT NULL CHECK (
52
- state IN ('inbox', 'inflight', 'submitted', 'consumed', 'delivered')
53
- ),
54
- execution_id TEXT CHECK (
55
- execution_id IS NULL OR length(trim(execution_id)) > 0
56
- ),
57
- fence_json TEXT CHECK (fence_json IS NULL OR json_valid(fence_json)),
58
- replayed_from_json TEXT CHECK (
59
- replayed_from_json IS NULL OR json_valid(replayed_from_json)
60
- ),
61
- delivered_at TEXT CHECK (
62
- delivered_at IS NULL OR length(trim(delivered_at)) > 0
63
- )
64
- ) STRICT
65
- `;
66
- const EXPECTED_OBJECT_SIGNATURE = "table:generation_records,table:heart_schema,table:projection_identity,table:projection_tells";
67
- const EXPECTED_RECORD_COLUMNS = "seq:INTEGER:0:1,kind:TEXT:1:0,execution_id:TEXT:1:0,payload_json:TEXT:1:0";
68
- const EXPECTED_IDENTITY_COLUMNS = "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";
69
- const EXPECTED_TELL_COLUMNS = "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";
70
4
  export function heartPath(projectionDirectory) {
71
5
  return path.join(projectionDirectory, HEART_FILE);
72
6
  }
73
- function rollbackQuietly(database) {
74
- try {
75
- database.exec("ROLLBACK");
76
- }
77
- catch {
78
- // Preserve the write or schema failure that caused the rollback.
79
- }
80
- }
81
- function assertParentDirectory(projectionDirectory) {
82
- let stat;
83
- try {
84
- stat = fs.statSync(projectionDirectory);
85
- }
86
- catch (error) {
87
- throw new ProjectionHeartError(`projection heart parent directory does not exist at ${projectionDirectory}`, { cause: error });
88
- }
89
- if (!stat.isDirectory()) {
90
- throw new ProjectionHeartError("projection heart parent must be a directory");
91
- }
92
- }
93
- function assertRegularHeartFile(databasePath) {
94
- let stat;
95
- try {
96
- stat = fs.lstatSync(databasePath);
97
- }
98
- catch (error) {
99
- throw new ProjectionHeartError(`projection heart does not exist at ${databasePath}`, { cause: error });
100
- }
101
- if (!stat.isFile()) {
102
- throw new ProjectionHeartError("projection heart must be a regular file");
103
- }
104
- }
105
7
  export function configureHeartBusyTimeout(database) {
106
8
  database.exec(`PRAGMA busy_timeout = ${HEART_BUSY_TIMEOUT_MS}`);
107
9
  }
108
- function configureWritable(database) {
109
- configureHeartBusyTimeout(database);
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
- function columnSignature(database, table) {
116
- const columns = database.prepare(`PRAGMA table_info(${table})`).all();
117
- return columns
118
- .map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
119
- .join(",");
120
- }
121
- function validateSchemaShape(database) {
122
- const objects = database.prepare(`
123
- SELECT name, type
124
- FROM sqlite_schema
125
- WHERE name NOT LIKE 'sqlite_%'
126
- ORDER BY name
127
- `).all();
128
- const signature = objects.map((object) => `${object.type}:${object.name}`).join(",");
129
- if (signature !== EXPECTED_OBJECT_SIGNATURE) {
130
- throw new ProjectionHeartError(`projection heart schema contains unexpected objects: ${signature || "none"}`);
131
- }
132
- const metadata = database.prepare("SELECT singleton, version FROM heart_schema").all();
133
- if (metadata.length !== 1
134
- || Number(metadata[0].singleton) !== 1
135
- || Number(metadata[0].version) !== HEART_SCHEMA_VERSION) {
136
- throw new ProjectionHeartError("projection heart schema metadata is malformed");
137
- }
138
- if (columnSignature(database, "generation_records") !== EXPECTED_RECORD_COLUMNS) {
139
- throw new ProjectionHeartError("projection heart generation_records schema is malformed");
140
- }
141
- if (columnSignature(database, "projection_identity") !== EXPECTED_IDENTITY_COLUMNS) {
142
- throw new ProjectionHeartError("projection heart projection_identity schema is malformed");
143
- }
144
- if (columnSignature(database, "projection_tells") !== EXPECTED_TELL_COLUMNS) {
145
- throw new ProjectionHeartError("projection heart projection_tells schema is malformed");
146
- }
147
- const quickCheck = database.prepare("PRAGMA quick_check").get();
148
- if (!quickCheck || Object.values(quickCheck)[0] !== "ok") {
149
- throw new ProjectionHeartError("projection heart database failed quick_check");
150
- }
151
- }
152
- function installSchema(database) {
153
- database.exec("BEGIN EXCLUSIVE");
154
- try {
155
- const objects = database.prepare(`
156
- SELECT name, type
157
- FROM sqlite_schema
158
- WHERE name NOT LIKE 'sqlite_%'
159
- ORDER BY name
160
- `).all();
161
- if (objects.length !== 0) {
162
- throw new ProjectionHeartError("projection heart already contains schema objects");
163
- }
164
- database.exec(HEART_SCHEMA_SQL);
165
- database.exec(IDENTITY_TABLE_SQL);
166
- database.exec(RECORD_TABLE_SQL);
167
- database.exec(TELL_TABLE_SQL);
168
- database.prepare("INSERT INTO heart_schema (singleton, version) VALUES (1, ?)")
169
- .run(HEART_SCHEMA_VERSION);
170
- validateSchemaShape(database);
171
- database.exec("COMMIT");
172
- }
173
- catch (error) {
174
- rollbackQuietly(database);
175
- throw error;
176
- }
177
- }
178
- export class ProjectionHeart {
179
- databasePath;
180
- readOnly;
181
- #database;
182
- #closed = false;
183
- constructor(databasePath, database, readOnly) {
184
- this.databasePath = databasePath;
185
- this.#database = database;
186
- this.readOnly = readOnly;
187
- }
188
- /** Create a brand-new heart database with schema v1. Fails if the file already exists. */
189
- static create(projectionDirectory) {
190
- assertParentDirectory(projectionDirectory);
191
- const databasePath = heartPath(projectionDirectory);
192
- let reserved = false;
193
- try {
194
- const fd = fs.openSync(databasePath, "wx");
195
- fs.closeSync(fd);
196
- reserved = true;
197
- }
198
- catch (error) {
199
- if (isErrnoException(error) && error.code === "EEXIST") {
200
- throw new ProjectionHeartError(`projection heart already exists at ${databasePath}`, { cause: error });
201
- }
202
- throw new ProjectionHeartError(`cannot create projection heart at ${databasePath}`, { cause: error });
203
- }
204
- let database;
205
- try {
206
- database = new DatabaseSync(databasePath, {
207
- enableForeignKeyConstraints: true,
208
- enableDoubleQuotedStringLiterals: false,
209
- allowExtension: false,
210
- });
211
- configureWritable(database);
212
- installSchema(database);
213
- return new ProjectionHeart(databasePath, database, false);
214
- }
215
- catch (error) {
216
- try {
217
- database?.close();
218
- }
219
- catch { /* preserve the causal create failure */ }
220
- if (reserved) {
221
- try {
222
- fs.unlinkSync(databasePath);
223
- }
224
- catch {
225
- // Leave a partial file only if unlink itself fails.
226
- }
227
- }
228
- if (error instanceof ProjectionHeartError)
229
- throw error;
230
- throw new ProjectionHeartError(`cannot create projection heart at ${databasePath}`, { cause: error });
231
- }
232
- }
233
- /** Open an existing heart read-only. Never creates, mutates, or migrates. */
234
- static openReadOnly(projectionDirectory) {
235
- assertParentDirectory(projectionDirectory);
236
- const databasePath = heartPath(projectionDirectory);
237
- assertRegularHeartFile(databasePath);
238
- let database;
239
- try {
240
- database = new DatabaseSync(databasePath, {
241
- readOnly: true,
242
- enableDoubleQuotedStringLiterals: false,
243
- allowExtension: false,
244
- });
245
- configureHeartBusyTimeout(database);
246
- validateSchemaShape(database);
247
- return new ProjectionHeart(databasePath, database, true);
248
- }
249
- catch (error) {
250
- try {
251
- database?.close();
252
- }
253
- catch { /* preserve open failure */ }
254
- if (error instanceof ProjectionHeartError)
255
- throw error;
256
- throw new ProjectionHeartError(`cannot open projection heart at ${databasePath}`, { cause: error });
257
- }
258
- }
259
- /** Open an existing heart for read/write. Never creates or migrates. */
260
- static openWritable(projectionDirectory) {
261
- assertParentDirectory(projectionDirectory);
262
- const databasePath = heartPath(projectionDirectory);
263
- assertRegularHeartFile(databasePath);
264
- let database;
265
- try {
266
- database = new DatabaseSync(databasePath, {
267
- enableForeignKeyConstraints: true,
268
- enableDoubleQuotedStringLiterals: false,
269
- allowExtension: false,
270
- });
271
- configureWritable(database);
272
- validateSchemaShape(database);
273
- return new ProjectionHeart(databasePath, database, false);
274
- }
275
- catch (error) {
276
- try {
277
- database?.close();
278
- }
279
- catch { /* preserve open failure */ }
280
- if (error instanceof ProjectionHeartError)
281
- throw error;
282
- throw new ProjectionHeartError(`cannot open projection heart at ${databasePath}`, { cause: error });
283
- }
284
- }
285
- read(fn) {
286
- this.#assertOpen();
287
- return fn(this.#database);
288
- }
289
- write(fn) {
290
- this.#assertOpen();
291
- if (this.readOnly) {
292
- throw new ProjectionHeartError("projection heart is read-only");
293
- }
294
- this.#database.exec("BEGIN IMMEDIATE");
295
- try {
296
- const result = fn(this.#database);
297
- this.#database.exec("COMMIT");
298
- return result;
299
- }
300
- catch (error) {
301
- rollbackQuietly(this.#database);
302
- throw error;
303
- }
304
- }
305
- close() {
306
- if (this.#closed)
307
- return;
308
- this.#closed = true;
309
- this.#database.close();
310
- }
311
- #assertOpen() {
312
- if (this.#closed) {
313
- throw new ProjectionHeartError("projection heart is closed");
314
- }
315
- }
316
- }
@@ -0,0 +1,109 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { ProjectionStateError } from "../../atomic-publish.js";
5
+ import { PROJECTION_GENERATION_STORE_FILE, } from "../generation/store.js";
6
+ import { configureHeartBusyTimeout } from "../heart.js";
7
+ function databasePath(projectionDirectory) {
8
+ return path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
9
+ }
10
+ function openDatabase(projectionDirectory, readOnly) {
11
+ const target = databasePath(projectionDirectory);
12
+ if (!fs.lstatSync(target).isFile()) {
13
+ throw new ProjectionStateError("projection heart must be a regular file");
14
+ }
15
+ const database = new DatabaseSync(target, {
16
+ readOnly,
17
+ enableForeignKeyConstraints: true,
18
+ enableDoubleQuotedStringLiterals: false,
19
+ allowExtension: false,
20
+ });
21
+ configureHeartBusyTimeout(database);
22
+ if (!readOnly) {
23
+ database.exec("PRAGMA foreign_keys = ON");
24
+ database.exec("PRAGMA trusted_schema = OFF");
25
+ }
26
+ return database;
27
+ }
28
+ function rollbackQuietly(database) {
29
+ try {
30
+ database.exec("ROLLBACK");
31
+ }
32
+ catch {
33
+ // Preserve the transition failure.
34
+ }
35
+ }
36
+ export function withRead(projectionDirectory, read) {
37
+ const database = openDatabase(projectionDirectory, true);
38
+ try {
39
+ return read(database);
40
+ }
41
+ finally {
42
+ database.close();
43
+ }
44
+ }
45
+ export function withWrite(projectionDirectory, write) {
46
+ const database = openDatabase(projectionDirectory, false);
47
+ try {
48
+ database.exec("BEGIN IMMEDIATE");
49
+ try {
50
+ const result = write(database);
51
+ database.exec("COMMIT");
52
+ return result;
53
+ }
54
+ catch (error) {
55
+ rollbackQuietly(database);
56
+ throw error;
57
+ }
58
+ }
59
+ finally {
60
+ database.close();
61
+ }
62
+ }
63
+ export function openProjectionTellObserver(projectionDirectory) {
64
+ const database = openDatabase(projectionDirectory, true);
65
+ let closed = false;
66
+ let observedDataVersion;
67
+ const assertOpen = () => {
68
+ if (closed)
69
+ throw new ProjectionStateError("projection tell observer is closed");
70
+ };
71
+ const readDataVersion = () => {
72
+ assertOpen();
73
+ const row = database.prepare("PRAGMA data_version").get();
74
+ const raw = row?.data_version;
75
+ const value = typeof raw === "bigint" ? Number(raw) : raw;
76
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
77
+ throw new ProjectionStateError("PRAGMA data_version returned an invalid value");
78
+ }
79
+ return value;
80
+ };
81
+ return {
82
+ hasDataVersionChanged() {
83
+ const current = readDataVersion();
84
+ if (observedDataVersion === undefined) {
85
+ observedDataVersion = current;
86
+ return true;
87
+ }
88
+ if (current === observedDataVersion)
89
+ return false;
90
+ observedDataVersion = current;
91
+ return true;
92
+ },
93
+ hasInbox() {
94
+ assertOpen();
95
+ return database.prepare(`
96
+ SELECT 1
97
+ FROM projection_tells
98
+ WHERE state = 'inbox'
99
+ LIMIT 1
100
+ `).get() !== undefined;
101
+ },
102
+ close() {
103
+ if (closed)
104
+ return;
105
+ closed = true;
106
+ database.close();
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,97 @@
1
+ import { ProjectionStateError } from "../../atomic-publish.js";
2
+ export const PROJECTION_TELL_STATES = [
3
+ "inbox",
4
+ "inflight",
5
+ "submitted",
6
+ "consumed",
7
+ "delivered",
8
+ ];
9
+ export function requireExecutionId(value) {
10
+ if (typeof value !== "string" || value.trim() === "") {
11
+ throw new ProjectionStateError("submitted tell executionId must be a non-empty string");
12
+ }
13
+ return value;
14
+ }
15
+ export function parseFence(value) {
16
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
17
+ throw new ProjectionStateError("tell fence is required");
18
+ }
19
+ const record = value;
20
+ if (record.kind === "turn" && typeof record.turnId === "string" && record.turnId.length > 0) {
21
+ return { kind: "turn", turnId: record.turnId };
22
+ }
23
+ if (record.kind === "ordinal"
24
+ && typeof record.ordinal === "number"
25
+ && Number.isSafeInteger(record.ordinal)
26
+ && record.ordinal >= 0) {
27
+ return { kind: "ordinal", ordinal: record.ordinal };
28
+ }
29
+ throw new ProjectionStateError("tell fence must be turn(string) or ordinal(non-negative integer)");
30
+ }
31
+ function decodeJson(value, location) {
32
+ try {
33
+ return JSON.parse(value);
34
+ }
35
+ catch (error) {
36
+ throw new ProjectionStateError(`${location} contains malformed JSON`, { cause: error });
37
+ }
38
+ }
39
+ function parseReplay(value) {
40
+ if (value === null)
41
+ return [];
42
+ const decoded = decodeJson(value, "tell replay history");
43
+ if (!Array.isArray(decoded))
44
+ throw new ProjectionStateError("tell replay history must be an array");
45
+ return decoded.map((item) => {
46
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
47
+ throw new ProjectionStateError("tell replay history entry must be an object");
48
+ }
49
+ const entry = item;
50
+ return {
51
+ executionId: requireExecutionId(entry.executionId),
52
+ fence: parseFence(entry.fence),
53
+ };
54
+ });
55
+ }
56
+ export function originalFromRow(row) {
57
+ return {
58
+ version: 1,
59
+ text: row.text,
60
+ createdAt: row.created_at,
61
+ ...(row.effort ? { effort: row.effort } : {}),
62
+ };
63
+ }
64
+ export function submittedFromRow(row) {
65
+ if (row.state !== "submitted" && row.state !== "consumed" && row.state !== "delivered") {
66
+ throw new ProjectionStateError(`tell '${row.tell_id}' is not submitted`);
67
+ }
68
+ if (row.execution_id === null || row.fence_json === null) {
69
+ throw new ProjectionStateError(`tell '${row.tell_id}' is missing submission facts`);
70
+ }
71
+ const replayedFrom = parseReplay(row.replayed_from_json);
72
+ return {
73
+ ...originalFromRow(row),
74
+ executionId: requireExecutionId(row.execution_id),
75
+ fence: parseFence(decodeJson(row.fence_json, "tell fence")),
76
+ ...(replayedFrom.length > 0 ? { replayedFrom } : {}),
77
+ };
78
+ }
79
+ export function assertTellIdRandomHex(randomHex) {
80
+ if (!/^[0-9a-f]{8}$/.test(randomHex)) {
81
+ throw new ProjectionStateError(`tell id random segment must be 8 lowercase hex chars, got ${randomHex}`);
82
+ }
83
+ return randomHex;
84
+ }
85
+ /** The only tell-consumption predicate. */
86
+ export function isTellCausallyConsumed(submitted, checkpoint) {
87
+ if (submitted.kind !== checkpoint.kind) {
88
+ throw new ProjectionStateError(`tell fence kind mismatch: ${submitted.kind} vs ${checkpoint.kind}`);
89
+ }
90
+ if (submitted.kind === "ordinal" && checkpoint.kind === "ordinal") {
91
+ return checkpoint.ordinal > submitted.ordinal;
92
+ }
93
+ if (submitted.kind === "turn" && checkpoint.kind === "turn") {
94
+ return checkpoint.turnId === submitted.turnId;
95
+ }
96
+ throw new ProjectionStateError("unreachable tell fence comparison");
97
+ }
@@ -1,143 +1,7 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { DatabaseSync } from "node:sqlite";
4
- import { ProjectionStateError, randomHex8 } from "./atomic-publish.js";
5
- import { PROJECTION_GENERATION_STORE_FILE, } from "./projection-generation-store.js";
6
- import { configureHeartBusyTimeout } from "./projection/heart.js";
7
- export const PROJECTION_TELL_STATES = [
8
- "inbox",
9
- "inflight",
10
- "submitted",
11
- "consumed",
12
- "delivered",
13
- ];
1
+ import { ProjectionStateError, randomHex8 } from "../../atomic-publish.js";
2
+ import { withRead, withWrite } from "./database.js";
3
+ import { assertTellIdRandomHex, originalFromRow, parseFence, requireExecutionId, submittedFromRow, } from "./model.js";
14
4
  const PENDING_TELL_LIMIT = 10_000;
15
- function databasePath(projectionDirectory) {
16
- return path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
17
- }
18
- function openDatabase(projectionDirectory, readOnly) {
19
- const target = databasePath(projectionDirectory);
20
- if (!fs.lstatSync(target).isFile()) {
21
- throw new ProjectionStateError("projection heart must be a regular file");
22
- }
23
- const database = new DatabaseSync(target, {
24
- readOnly,
25
- enableForeignKeyConstraints: true,
26
- enableDoubleQuotedStringLiterals: false,
27
- allowExtension: false,
28
- });
29
- configureHeartBusyTimeout(database);
30
- if (!readOnly) {
31
- database.exec("PRAGMA foreign_keys = ON");
32
- database.exec("PRAGMA trusted_schema = OFF");
33
- }
34
- return database;
35
- }
36
- function rollbackQuietly(database) {
37
- try {
38
- database.exec("ROLLBACK");
39
- }
40
- catch {
41
- // Preserve the transition failure.
42
- }
43
- }
44
- function withRead(projectionDirectory, read) {
45
- const database = openDatabase(projectionDirectory, true);
46
- try {
47
- return read(database);
48
- }
49
- finally {
50
- database.close();
51
- }
52
- }
53
- function withWrite(projectionDirectory, write) {
54
- const database = openDatabase(projectionDirectory, false);
55
- try {
56
- database.exec("BEGIN IMMEDIATE");
57
- try {
58
- const result = write(database);
59
- database.exec("COMMIT");
60
- return result;
61
- }
62
- catch (error) {
63
- rollbackQuietly(database);
64
- throw error;
65
- }
66
- }
67
- finally {
68
- database.close();
69
- }
70
- }
71
- function requireExecutionId(value) {
72
- if (typeof value !== "string" || value.trim() === "") {
73
- throw new ProjectionStateError("submitted tell executionId must be a non-empty string");
74
- }
75
- return value;
76
- }
77
- function parseFence(value) {
78
- if (!value || typeof value !== "object" || Array.isArray(value)) {
79
- throw new ProjectionStateError("tell fence is required");
80
- }
81
- const record = value;
82
- if (record.kind === "turn" && typeof record.turnId === "string" && record.turnId.length > 0) {
83
- return { kind: "turn", turnId: record.turnId };
84
- }
85
- if (record.kind === "ordinal"
86
- && typeof record.ordinal === "number"
87
- && Number.isSafeInteger(record.ordinal)
88
- && record.ordinal >= 0) {
89
- return { kind: "ordinal", ordinal: record.ordinal };
90
- }
91
- throw new ProjectionStateError("tell fence must be turn(string) or ordinal(non-negative integer)");
92
- }
93
- function decodeJson(value, location) {
94
- try {
95
- return JSON.parse(value);
96
- }
97
- catch (error) {
98
- throw new ProjectionStateError(`${location} contains malformed JSON`, { cause: error });
99
- }
100
- }
101
- function parseReplay(value) {
102
- if (value === null)
103
- return [];
104
- const decoded = decodeJson(value, "tell replay history");
105
- if (!Array.isArray(decoded))
106
- throw new ProjectionStateError("tell replay history must be an array");
107
- return decoded.map((item) => {
108
- if (!item || typeof item !== "object" || Array.isArray(item)) {
109
- throw new ProjectionStateError("tell replay history entry must be an object");
110
- }
111
- const entry = item;
112
- return {
113
- executionId: requireExecutionId(entry.executionId),
114
- fence: parseFence(entry.fence),
115
- };
116
- });
117
- }
118
- function originalFromRow(row) {
119
- return {
120
- version: 1,
121
- text: row.text,
122
- createdAt: row.created_at,
123
- ...(row.effort ? { effort: row.effort } : {}),
124
- };
125
- }
126
- function submittedFromRow(row) {
127
- if (row.state !== "submitted" && row.state !== "consumed" && row.state !== "delivered") {
128
- throw new ProjectionStateError(`tell '${row.tell_id}' is not submitted`);
129
- }
130
- if (row.execution_id === null || row.fence_json === null) {
131
- throw new ProjectionStateError(`tell '${row.tell_id}' is missing submission facts`);
132
- }
133
- const replayedFrom = parseReplay(row.replayed_from_json);
134
- return {
135
- ...originalFromRow(row),
136
- executionId: requireExecutionId(row.execution_id),
137
- fence: parseFence(decodeJson(row.fence_json, "tell fence")),
138
- ...(replayedFrom.length > 0 ? { replayedFrom } : {}),
139
- };
140
- }
141
5
  function selectTell(database, tellId) {
142
6
  return database.prepare(`
143
7
  SELECT
@@ -154,12 +18,6 @@ function selectTell(database, tellId) {
154
18
  WHERE tell_id = ?
155
19
  `).get(tellId);
156
20
  }
157
- function assertTellIdRandomHex(randomHex) {
158
- if (!/^[0-9a-f]{8}$/.test(randomHex)) {
159
- throw new ProjectionStateError(`tell id random segment must be 8 lowercase hex chars, got ${randomHex}`);
160
- }
161
- return randomHex;
162
- }
163
21
  export function listTellIds(projectionDirectory, state) {
164
22
  return withRead(projectionDirectory, (database) => database.prepare(`
165
23
  SELECT tell_id
@@ -386,16 +244,3 @@ export function demoteSubmittedTellsForReplay(projectionDirectory, executionId)
386
244
  return rows.map((row) => row.tell_id);
387
245
  });
388
246
  }
389
- /** The only tell-consumption predicate. */
390
- export function isTellCausallyConsumed(submitted, checkpoint) {
391
- if (submitted.kind !== checkpoint.kind) {
392
- throw new ProjectionStateError(`tell fence kind mismatch: ${submitted.kind} vs ${checkpoint.kind}`);
393
- }
394
- if (submitted.kind === "ordinal" && checkpoint.kind === "ordinal") {
395
- return checkpoint.ordinal > submitted.ordinal;
396
- }
397
- if (submitted.kind === "turn" && checkpoint.kind === "turn") {
398
- return checkpoint.turnId === submitted.turnId;
399
- }
400
- throw new ProjectionStateError("unreachable tell fence comparison");
401
- }