@open-tomato/rafa 0.44.0 → 0.46.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 (142) hide show
  1. package/README.md +280 -42
  2. package/dist/PROMPT.md +18 -1
  3. package/dist/adapters/logger/settings.d.ts +132 -0
  4. package/dist/bugs/closest.d.ts +19 -0
  5. package/dist/bugs/fenced.d.ts +8 -0
  6. package/dist/bugs/places.d.ts +20 -0
  7. package/dist/bugs/record.d.ts +145 -0
  8. package/dist/bugs/title.d.ts +83 -0
  9. package/dist/bundled/agents/doc-updater.md +17 -3
  10. package/dist/bundled/agents/loop-implementer.md +14 -2
  11. package/dist/bundled/operators/skills/rafa-stretch-pit-stop/SKILL.md +10 -3
  12. package/dist/bundled/operators/skills/rafa-stretch-sweep/SKILL.md +5 -0
  13. package/dist/bundled/skills/dev-planner/SKILL.md +120 -24
  14. package/dist/bundled/skills/documentation/SKILL.md +2 -12
  15. package/dist/cli.js +72387 -59411
  16. package/dist/config/entry.d.ts +119 -0
  17. package/dist/config/index.d.ts +36 -0
  18. package/dist/config/index.js +51 -0
  19. package/dist/config/shape.d.ts +232 -0
  20. package/dist/config-registry.d.ts +127 -0
  21. package/dist/config-schema-board-project.d.ts +157 -0
  22. package/dist/config-schema-bugs.d.ts +71 -0
  23. package/dist/config-schema-errors.d.ts +54 -0
  24. package/dist/config-schema-features.d.ts +64 -0
  25. package/dist/config-schema-hub.d.ts +150 -0
  26. package/dist/config-schema-issues.d.ts +95 -0
  27. package/dist/config-schema-logger.d.ts +91 -0
  28. package/dist/config-schema-loop-continue.d.ts +122 -0
  29. package/dist/config-schema-loop-retries.d.ts +94 -0
  30. package/dist/config-schema-release.d.ts +307 -0
  31. package/dist/config-schema-tests.d.ts +170 -0
  32. package/dist/config-schema-triage.d.ts +142 -0
  33. package/dist/config-schema-wrap-up.d.ts +96 -0
  34. package/dist/core/command/command.d.ts +228 -0
  35. package/dist/core/command/confirm.d.ts +67 -0
  36. package/dist/core/command/index.d.ts +112 -0
  37. package/dist/core/command/lazy.d.ts +54 -0
  38. package/dist/core/command/registry-types.d.ts +96 -0
  39. package/dist/core/command/running.d.ts +96 -0
  40. package/dist/core/command/spends.d.ts +39 -0
  41. package/dist/core/command/types.d.ts +92 -0
  42. package/dist/core/command/version.d.ts +4 -0
  43. package/dist/core/config/branch-template.d.ts +20 -0
  44. package/dist/core/config/config-items.d.ts +86 -0
  45. package/dist/core/config/config-load.d.ts +47 -0
  46. package/dist/core/config/config-origins.d.ts +57 -0
  47. package/dist/core/config/config-readers.d.ts +167 -0
  48. package/dist/core/config/config-schema.d.ts +207 -0
  49. package/dist/core/config/config-sections.d.ts +493 -0
  50. package/dist/core/config/config.d.ts +320 -0
  51. package/dist/core/config/core-section.d.ts +39 -0
  52. package/dist/core/config/dangerous.d.ts +58 -0
  53. package/dist/core/config/default-routing.d.ts +16 -0
  54. package/dist/core/config/index.d.ts +141 -0
  55. package/dist/core/config/scope.d.ts +42 -0
  56. package/dist/core/config/section.d.ts +206 -0
  57. package/dist/core/config/sections.d.ts +45 -0
  58. package/dist/core/config/standard-schema.d.ts +27 -0
  59. package/dist/core/git/commit.d.ts +232 -0
  60. package/dist/core/git/gh-runner.d.ts +24 -0
  61. package/dist/core/git/git.d.ts +22 -0
  62. package/dist/core/git/index.d.ts +85 -0
  63. package/dist/core/loop/claude.d.ts +388 -0
  64. package/dist/core/loop/hop-record.d.ts +52 -0
  65. package/dist/core/loop/index.d.ts +179 -0
  66. package/dist/core/loop/session-env.d.ts +20 -0
  67. package/dist/core/loop/session-record-parse.d.ts +63 -0
  68. package/dist/core/loop/sessions.d.ts +408 -0
  69. package/dist/core/loop/suite-failure.d.ts +25 -0
  70. package/dist/core/output/active.d.ts +100 -0
  71. package/dist/core/output/stream.d.ts +20 -0
  72. package/dist/core/output/text.d.ts +55 -0
  73. package/dist/core/plan/block-reader.d.ts +46 -0
  74. package/dist/core/plan/blocks.d.ts +139 -0
  75. package/dist/core/plan/declaration.d.ts +366 -0
  76. package/dist/core/plan/plan-stamp.d.ts +75 -0
  77. package/dist/core/runtime/identity.d.ts +21 -0
  78. package/dist/core/runtime/index.d.ts +46 -0
  79. package/dist/effort/store/bring-forward.d.ts +131 -0
  80. package/dist/effort/store/check-rows.d.ts +31 -0
  81. package/dist/effort/store/commit-stats.d.ts +42 -0
  82. package/dist/effort/store/copy.d.ts +35 -0
  83. package/dist/effort/store/development-build.d.ts +105 -0
  84. package/dist/effort/store/effort-levels.d.ts +13 -0
  85. package/dist/effort/store/findings.d.ts +214 -0
  86. package/dist/effort/store/fix-schema.d.ts +184 -0
  87. package/dist/effort/store/index.js +10 -7
  88. package/dist/effort/store/location.d.ts +44 -0
  89. package/dist/effort/store/merge-commit-gaps.d.ts +68 -0
  90. package/dist/effort/store/merge-conflicts.d.ts +91 -0
  91. package/dist/effort/store/merge-rules.d.ts +82 -0
  92. package/dist/effort/store/merge-store.d.ts +211 -0
  93. package/dist/effort/store/merge-union.d.ts +92 -0
  94. package/dist/effort/store/migrate.d.ts +146 -0
  95. package/dist/effort/store/migrations.d.ts +92 -0
  96. package/dist/effort/store/ndjson-files.d.ts +114 -0
  97. package/dist/effort/store/origins.d.ts +116 -0
  98. package/dist/effort/store/plan-stubs.d.ts +79 -0
  99. package/dist/effort/store/rebuild-aside.d.ts +172 -0
  100. package/dist/effort/store/schema-plan.d.ts +102 -0
  101. package/dist/effort/store/session-row-types.d.ts +64 -0
  102. package/dist/effort/store/settings.d.ts +13 -0
  103. package/dist/effort/store/sqlite.d.ts +340 -0
  104. package/dist/effort/store/store-generation.d.ts +17 -0
  105. package/dist/effort/store/store-identity.d.ts +220 -0
  106. package/dist/effort/store/store-meta.d.ts +305 -0
  107. package/dist/effort/store/store-replaced.d.ts +72 -0
  108. package/dist/effort/store/types.d.ts +223 -0
  109. package/dist/errors/codes.d.ts +56 -0
  110. package/dist/errors/rafa-codes.d.ts +36 -0
  111. package/dist/index-2a7nb3an.js +125 -0
  112. package/dist/{index-57mmzkcn.js → index-39kbcw68.js} +516 -217
  113. package/dist/{index-nv34ptht.js → index-77ajfffn.js} +14 -9
  114. package/dist/index-cy98y87p.js +2597 -0
  115. package/dist/index-gqz4jn6j.js +13447 -0
  116. package/dist/index-gvek4r8m.js +710 -0
  117. package/dist/index-j4k2vdz3.js +1125 -0
  118. package/dist/index-jh1r4kcn.js +1067 -0
  119. package/dist/index-ykggr043.js +540 -0
  120. package/dist/index-z1w83f81.js +4 -0
  121. package/dist/index.js +16091 -22798
  122. package/dist/learning/index.js +5 -6
  123. package/dist/plan/index.js +12 -11
  124. package/dist/plan-prompt.md +11 -2
  125. package/dist/ports/index.d.ts +732 -0
  126. package/dist/ports/learning.d.ts +101 -0
  127. package/dist/ports/log-level.d.ts +13 -0
  128. package/dist/ports/spec-review.d.ts +66 -0
  129. package/dist/pr/checks.d.ts +133 -0
  130. package/dist/pr/types.d.ts +422 -0
  131. package/dist/ready-4f8edezt.js +2016 -0
  132. package/dist/release-templates/release-bot-fragment.yml +153 -0
  133. package/dist/release-templates/release-publish.yml +110 -0
  134. package/dist/release-templates/release-settle.yml +122 -0
  135. package/dist/release-templates/release-tag.yml +102 -0
  136. package/dist/report/parse.d.ts +415 -0
  137. package/dist/schema/project-id.d.ts +79 -0
  138. package/dist/schema/tiers.d.ts +107 -0
  139. package/package.json +42 -4
  140. package/dist/index-6q30dtgm.js +0 -6342
  141. package/dist/index-cknayrpc.js +0 -2414
  142. package/dist/index-hf2k497d.js +0 -16
@@ -0,0 +1,1067 @@
1
+ // @bun
2
+ import {
3
+ CORE_ADAPTER_REGISTRY,
4
+ EFFORT_KEY_PROJECTIONS,
5
+ MIGRATION_LOG_TABLE,
6
+ SQLITE_MIGRATIONS,
7
+ SQLITE_STORE_FILE_NAME,
8
+ activeStoreSettings,
9
+ appendRows,
10
+ appliedByName,
11
+ bringForward,
12
+ carryStoreIdentity,
13
+ collectedKeys,
14
+ effortStoreDir,
15
+ effortStoreFileName,
16
+ guardTestProcess,
17
+ isPidAlive,
18
+ planSchema,
19
+ readProjectIdentity,
20
+ readRuntimeIdentity,
21
+ readSessions,
22
+ readStoreMeta,
23
+ readStoreRows,
24
+ readStoreSchema,
25
+ refuseUnownedDevelopmentWrite,
26
+ sqliteCatalogue,
27
+ storeProjectRoot,
28
+ writeNames,
29
+ writeStoreGeneration
30
+ } from "./index-cy98y87p.js";
31
+ import {
32
+ messageOf
33
+ } from "./index-ykggr043.js";
34
+
35
+ // packages/rafa/src/effort/store/ndjson.ts
36
+ import { join } from "path";
37
+ function kindPath(repoRoot, kind) {
38
+ return join(effortStoreDir(repoRoot), effortStoreFileName(kind));
39
+ }
40
+ function openedKindPath(repoRoot, kind) {
41
+ const path = kindPath(repoRoot, kind);
42
+ guardTestProcess(path);
43
+ return path;
44
+ }
45
+ function readKind(repoRoot, kind) {
46
+ return readStoreRows(openedKindPath(repoRoot, kind));
47
+ }
48
+ function keysOfKind(repoRoot, kind) {
49
+ const path = openedKindPath(repoRoot, kind);
50
+ return collectedKeys(path, EFFORT_KEY_PROJECTIONS[kind]);
51
+ }
52
+ function appendKind(repoRoot, kind, rows) {
53
+ const path = openedKindPath(repoRoot, kind);
54
+ return appendRows(path, rows, EFFORT_KEY_PROJECTIONS[kind]);
55
+ }
56
+ function openNdjsonStore(repoRoot) {
57
+ return {
58
+ append: (kind, rows) => appendKind(repoRoot, kind, rows),
59
+ keys: (kind) => keysOfKind(repoRoot, kind),
60
+ read: (kind) => readKind(repoRoot, kind).rows,
61
+ path: (kind) => kindPath(repoRoot, kind),
62
+ readRows: (kind) => readKind(repoRoot, kind)
63
+ };
64
+ }
65
+
66
+ // packages/rafa/src/effort/store/merge-store.ts
67
+ import { randomUUID } from "crypto";
68
+ import { existsSync as existsSync3, mkdtempSync, rmSync as rmSync3 } from "fs";
69
+ import { tmpdir } from "os";
70
+ import { dirname, join as join3 } from "path";
71
+ import { Database as Database3, SQLiteError as SQLiteError2 } from "bun:sqlite";
72
+
73
+ // packages/rafa/src/effort/store/copy.ts
74
+ import { copyFileSync, constants, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "fs";
75
+ import { join as join2, resolve } from "path";
76
+ import { Database } from "bun:sqlite";
77
+ var NDJSON_KINDS = { sessions: true, commits: true };
78
+ var EFFORT_COPY_FILE_NAMES = [
79
+ SQLITE_STORE_FILE_NAME,
80
+ ...Object.keys(NDJSON_KINDS).map((kind) => effortStoreFileName(kind))
81
+ ];
82
+
83
+ class EffortCopyRefusal extends Error {
84
+ name = "EffortCopyRefusal";
85
+ }
86
+
87
+ class EffortCopyFailure extends Error {
88
+ name = "EffortCopyFailure";
89
+ }
90
+ function refuseUsedTarget(target) {
91
+ if (!existsSync(target))
92
+ return;
93
+ let entries;
94
+ try {
95
+ if (!statSync(target).isDirectory()) {
96
+ throw new EffortCopyRefusal(`${target} is a file, not a directory; name an empty or absent directory with --to=<dir>.`);
97
+ }
98
+ entries = readdirSync(target);
99
+ } catch (error) {
100
+ if (error instanceof EffortCopyRefusal)
101
+ throw error;
102
+ throw new EffortCopyFailure(`${target} cannot be read: ${String(error)}`);
103
+ }
104
+ if (entries.length === 0)
105
+ return;
106
+ throw new EffortCopyRefusal(`${target} is not empty (${String(entries.length)} entries); nothing was copied.` + " Name an empty or absent directory with --to=<dir>.");
107
+ }
108
+ function vacuumInto(source, target) {
109
+ const db = new Database(source, { readonly: true });
110
+ try {
111
+ db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
112
+ db.run("VACUUM INTO ?", [target]);
113
+ } finally {
114
+ db.close();
115
+ }
116
+ }
117
+ function copyOne(name, source, target) {
118
+ if (name === SQLITE_STORE_FILE_NAME) {
119
+ vacuumInto(join2(source, name), join2(target, name));
120
+ return;
121
+ }
122
+ copyFileSync(join2(source, name), join2(target, name), constants.COPYFILE_EXCL);
123
+ }
124
+ function undo(target, written, madeDir) {
125
+ try {
126
+ if (madeDir === undefined) {
127
+ for (const name of written)
128
+ rmSync(join2(target, name), { force: true });
129
+ } else {
130
+ rmSync(madeDir, { recursive: true, force: true });
131
+ }
132
+ return "";
133
+ } catch (error) {
134
+ return ` Removing what it made failed too: ${String(error)}`;
135
+ }
136
+ }
137
+ function copyEffortStore(request) {
138
+ const source = resolve(request.source);
139
+ const target = resolve(request.target);
140
+ guardTestProcess(source);
141
+ guardTestProcess(target);
142
+ const files = EFFORT_COPY_FILE_NAMES.filter((name) => existsSync(join2(source, name)));
143
+ if (files.length === 0) {
144
+ throw new EffortCopyRefusal(`no effort store at ${source}: it holds none of ${EFFORT_COPY_FILE_NAMES.join(", ")}. Nothing to copy.`);
145
+ }
146
+ refuseUsedTarget(target);
147
+ const written = [];
148
+ let madeDir;
149
+ try {
150
+ madeDir = mkdirSync(target, { recursive: true });
151
+ for (const name of files) {
152
+ written.push(name);
153
+ copyOne(name, source, target);
154
+ }
155
+ } catch (error) {
156
+ const left = undo(target, written, madeDir);
157
+ throw new EffortCopyFailure(`copying ${source} to ${target} failed: ${String(error)}.${left}`);
158
+ }
159
+ return Object.freeze({ source, directory: target, files: Object.freeze(files) });
160
+ }
161
+
162
+ // packages/rafa/src/effort/store/fix-schema.ts
163
+ import { Database as Database2 } from "bun:sqlite";
164
+
165
+ // packages/rafa/src/effort/store/rebuild-aside.ts
166
+ import { closeSync, existsSync as existsSync2, fsyncSync, openSync, renameSync, rmSync as rmSync2 } from "fs";
167
+ class RebuildRefusal extends Error {
168
+ constructor(message, options) {
169
+ super(message, options);
170
+ this.name = "RebuildRefusal";
171
+ }
172
+ }
173
+ function quoted(name) {
174
+ return `"${name.replaceAll('"', '""')}"`;
175
+ }
176
+ function count(db, sql) {
177
+ return db.query(sql).get()?.n ?? 0;
178
+ }
179
+ function refuseInFlight(path) {
180
+ for (const suffix of ["-journal", "-wal"]) {
181
+ if (existsSync2(`${path}${suffix}`)) {
182
+ throw new RebuildRefusal(`${path}${suffix} is there, so a write is in flight or was interrupted; stop whatever uses the store` + " and open it once with rafa before repairing it");
183
+ }
184
+ }
185
+ }
186
+ function checkedCounts(db, tables, original, added = {}) {
187
+ return tables.map((table) => {
188
+ const rows = count(db, `SELECT count(*) AS n FROM main.${quoted(table)}`);
189
+ const originalRows = count(db, `SELECT count(*) AS n FROM ${original}.${quoted(table)}`);
190
+ const addedRows = added[table] ?? 0;
191
+ if (rows !== originalRows + addedRows) {
192
+ const plus = addedRows === 0 ? "" : ` plus ${String(addedRows)} added`;
193
+ throw new RebuildRefusal(`the rebuilt ${table} holds ${String(rows)} rows, the store ${String(originalRows)}${plus}`);
194
+ }
195
+ return { table, rows };
196
+ });
197
+ }
198
+ function refuseCorrupt(db) {
199
+ const answer = db.query("PRAGMA main.integrity_check").get()?.integrity_check;
200
+ if (answer !== "ok")
201
+ throw new RebuildRefusal(`the rebuilt store failed integrity_check: ${answer ?? "no answer"}`);
202
+ }
203
+ function removeParallel(parallelPath) {
204
+ rmSync2(parallelPath, { force: true });
205
+ rmSync2(`${parallelPath}-journal`, { force: true });
206
+ }
207
+ var STEP_TEXT = {
208
+ backup: ({ path, backupPath }) => ({
209
+ doing: `writing ${path} out to the backup ${backupPath}`,
210
+ backup: `any part of the backup written at ${backupPath}`
211
+ }),
212
+ carry: ({ path, parallelPath, backupPath }) => ({
213
+ doing: `carrying the store id of ${path} onto ${parallelPath}`,
214
+ backup: `the whole backup at ${backupPath}`
215
+ }),
216
+ record: ({ path, parallelPath, backupPath }) => ({
217
+ doing: `writing the generation carried onto ${parallelPath} to the side record of ${path}`,
218
+ backup: `the whole backup at ${backupPath}`
219
+ }),
220
+ rename: ({ path, parallelPath, backupPath }) => ({
221
+ doing: `renaming ${parallelPath} over ${path}`,
222
+ backup: `the whole backup at ${backupPath}`
223
+ })
224
+ };
225
+
226
+ class SwapFailure extends RebuildRefusal {
227
+ step;
228
+ parallelPath;
229
+ backupPath;
230
+ constructor(step, files, cause) {
231
+ const { doing, backup } = STEP_TEXT[step](files);
232
+ const reason = cause instanceof Error ? cause.message : String(cause);
233
+ super(`${doing} failed (${reason}), so ${files.path} was not replaced and is unchanged.` + ` The rebuilt store is left at ${files.parallelPath}, and ${backup}.`, { cause });
234
+ this.name = "SwapFailure";
235
+ this.step = step;
236
+ this.parallelPath = files.parallelPath;
237
+ this.backupPath = files.backupPath;
238
+ }
239
+ }
240
+ function writeBackup(path, backupPath) {
241
+ vacuumInto(path, backupPath);
242
+ const backup = openSync(backupPath, "r");
243
+ try {
244
+ fsyncSync(backup);
245
+ } finally {
246
+ closeSync(backup);
247
+ }
248
+ }
249
+ var SWAP_STEPS = {
250
+ backup: writeBackup,
251
+ carry: (path, parallelPath) => carryStoreIdentity(path, parallelPath),
252
+ record: writeStoreGeneration,
253
+ rename: renameSync
254
+ };
255
+ function runStep(step, files, act) {
256
+ try {
257
+ return act();
258
+ } catch (error) {
259
+ throw new SwapFailure(step, files, error);
260
+ }
261
+ }
262
+ function swapIn(path, parallelPath, backupPath, steps = {}) {
263
+ const { backup, carry, record, rename } = { ...SWAP_STEPS, ...steps };
264
+ const files = { path, parallelPath, backupPath };
265
+ runStep("backup", files, () => backup(path, backupPath));
266
+ const carried = runStep("carry", files, () => carry(path, parallelPath));
267
+ if (carried.action === "carried" && carried.generation !== null) {
268
+ const { generation } = carried;
269
+ runStep("record", files, () => record(path, generation));
270
+ }
271
+ runStep("rename", files, () => rename(parallelPath, path));
272
+ }
273
+ function refuseTaken(paths) {
274
+ for (const taken of paths) {
275
+ if (existsSync2(taken))
276
+ throw new RebuildRefusal(`${taken} is already there; move it aside or pass another stamp`);
277
+ }
278
+ }
279
+ function rebuildAside(options) {
280
+ const { path, parallelPath, backupPath, dryRun, build } = options;
281
+ refuseInFlight(path);
282
+ refuseTaken([parallelPath, backupPath]);
283
+ let built;
284
+ try {
285
+ built = build(parallelPath);
286
+ } catch (error) {
287
+ removeParallel(parallelPath);
288
+ throw error;
289
+ }
290
+ if (dryRun) {
291
+ removeParallel(parallelPath);
292
+ return { built, backupPath: null };
293
+ }
294
+ swapIn(path, parallelPath, backupPath);
295
+ return { built, backupPath };
296
+ }
297
+ // packages/rafa/src/effort/store/fix-schema.ts
298
+ function tableNames(db, schema) {
299
+ return db.query(`SELECT name FROM ${schema}.sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name <> ? ORDER BY name`).all(MIGRATION_LOG_TABLE).map((row) => row.name);
300
+ }
301
+ function readOnlySchema(path) {
302
+ const db = new Database2(path, { readonly: true });
303
+ try {
304
+ return readStoreSchema(db, path);
305
+ } finally {
306
+ db.close();
307
+ }
308
+ }
309
+ function refuseUnusable(db, parallelPath, migrations) {
310
+ const plan = planSchema(readStoreSchema(db, parallelPath), sqliteCatalogue(migrations), "write", "open");
311
+ if (plan.verdict === "refuse")
312
+ throw new RebuildRefusal(`the rebuilt store is refused as ${plan.reason}: ${plan.message}`);
313
+ const needs = writeNames(plan);
314
+ if (needs.length > 0)
315
+ throw new RebuildRefusal(`the rebuilt store still needs ${needs.join(", ")}`);
316
+ }
317
+
318
+ // packages/rafa/src/effort/store/commit-stats.ts
319
+ var MS_PER_MINUTE = 60000;
320
+ var ELAPSED_DECIMALS = 3;
321
+ function minutesBetween(earlier, later) {
322
+ const from = Date.parse(earlier);
323
+ const to = Date.parse(later);
324
+ if (Number.isNaN(from) || Number.isNaN(to))
325
+ return null;
326
+ const minutes = (to - from) / MS_PER_MINUTE;
327
+ return Number(minutes.toFixed(ELAPSED_DECIMALS));
328
+ }
329
+
330
+ // packages/rafa/src/effort/store/merge-commit-gaps.ts
331
+ var GAP_FIELD = "minutesSincePrevious";
332
+ var UPDATE_GAP = "UPDATE commits SET row_json = ? WHERE seq = ?";
333
+ function parsedObject(text) {
334
+ try {
335
+ const value = JSON.parse(text);
336
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
337
+ } catch {
338
+ return null;
339
+ }
340
+ }
341
+ function withoutGap(rowJson) {
342
+ const row = parsedObject(rowJson);
343
+ if (row === null)
344
+ return rowJson;
345
+ const rest = Object.fromEntries(Object.entries(row).filter(([key]) => key !== GAP_FIELD));
346
+ return JSON.stringify(rest);
347
+ }
348
+ function readCommits(db) {
349
+ return db.query("SELECT seq, sha, row_json FROM commits").all().map(({ seq, sha, row_json: rowJson }) => {
350
+ const row = parsedObject(rowJson);
351
+ const timestamp = typeof row?.timestamp === "string" ? row.timestamp : null;
352
+ return { seq, sha, row, timestamp, epoch: timestamp === null ? Number.NaN : Date.parse(timestamp) };
353
+ });
354
+ }
355
+ function timeOrder(entries) {
356
+ return entries.filter((entry) => !Number.isNaN(entry.epoch)).sort((left, right) => left.epoch - right.epoch || compareText(left.sha, right.sha));
357
+ }
358
+ function compareText(left, right) {
359
+ if (left === right)
360
+ return 0;
361
+ return left < right ? -1 : 1;
362
+ }
363
+ function storedGap(entry) {
364
+ const value = entry.row?.[GAP_FIELD];
365
+ return typeof value === "number" ? value : null;
366
+ }
367
+ function namedRows(inserted, positions, order) {
368
+ const position = positions.get(inserted.seq);
369
+ if (position === undefined)
370
+ return [inserted.seq];
371
+ const next = order[position + 1];
372
+ return next === undefined ? [inserted.seq] : [inserted.seq, next.seq];
373
+ }
374
+ function recomputedGap(entry, positions, order) {
375
+ const position = positions.get(entry.seq);
376
+ if (position === undefined || position === 0)
377
+ return null;
378
+ const previous = order[position - 1];
379
+ if (previous?.timestamp == null || entry.timestamp === null)
380
+ return null;
381
+ return minutesBetween(previous.timestamp, entry.timestamp);
382
+ }
383
+ function recomputeCommitGaps(db, inserted) {
384
+ return db.transaction(() => {
385
+ const entries = readCommits(db);
386
+ const bySeq = new Map(entries.map((entry) => [entry.seq, entry]));
387
+ const order = timeOrder(entries);
388
+ const positions = new Map(order.map((entry, position) => [entry.seq, position]));
389
+ const named = new Set(inserted.flatMap((seq) => {
390
+ const entry = bySeq.get(seq);
391
+ return entry === undefined ? [] : namedRows(entry, positions, order);
392
+ }));
393
+ const targets = [...named].map((seq) => bySeq.get(seq)).filter((entry) => entry?.row != null).sort((left, right) => (positions.get(left.seq) ?? -1) - (positions.get(right.seq) ?? -1));
394
+ return targets.flatMap((entry) => {
395
+ const before = storedGap(entry);
396
+ const after = recomputedGap(entry, positions, order);
397
+ if (before === after && entry.row !== null && Object.hasOwn(entry.row, GAP_FIELD))
398
+ return [];
399
+ db.query(UPDATE_GAP).run(JSON.stringify({ ...entry.row, [GAP_FIELD]: after }), entry.seq);
400
+ return [{ seq: entry.seq, sha: entry.sha, before, after }];
401
+ });
402
+ })();
403
+ }
404
+
405
+ // packages/rafa/src/effort/store/merge-rules.ts
406
+ var SET_ONCE = "set-once";
407
+ var RECOMPUTED = "recomputed";
408
+ var APPEND_ONLY = {};
409
+ function appendOnly(...identity) {
410
+ return { scope: "merged", identity, edited: APPEND_ONLY };
411
+ }
412
+ var MERGE_RULES = {
413
+ sessions: appendOnly("session_id"),
414
+ commits: { scope: "merged", identity: ["sha"], edited: { row_json: RECOMPUTED } },
415
+ findings: { scope: "merged", identity: ["id"], edited: { tracker_ref: SET_ONCE } },
416
+ blockers: appendOnly("id"),
417
+ out_of_scope_bugs: appendOnly("id"),
418
+ report_absences: appendOnly("id"),
419
+ task_reports: appendOnly("id"),
420
+ preflight: appendOnly("run_id", "position"),
421
+ dispatches: appendOnly("session_id"),
422
+ changes: appendOnly("id"),
423
+ skill_invocations: appendOnly("session_id", "name", "sidechain"),
424
+ plan_ci: appendOnly("pr", "head_sha", "read_at"),
425
+ stale_docs: appendOnly("id"),
426
+ bug_routes: appendOnly("id"),
427
+ schema_migrations: { scope: "local" },
428
+ store_meta: { scope: "local" },
429
+ store_replaced: { scope: "local" },
430
+ merges: { scope: "local" },
431
+ merge_conflicts: { scope: "local" }
432
+ };
433
+
434
+ // packages/rafa/src/effort/store/merge-union.ts
435
+ import { SQLiteError } from "bun:sqlite";
436
+ class UnionSchemaMismatch extends Error {
437
+ constructor(message) {
438
+ super(message);
439
+ this.name = "UnionSchemaMismatch";
440
+ }
441
+ }
442
+ var UNIQUE_REFUSAL = "SQLITE_CONSTRAINT_UNIQUE";
443
+ function mergedTables() {
444
+ return Object.entries(MERGE_RULES).flatMap(([table, rule]) => rule.scope === "merged" ? [{ table, identity: rule.identity }] : []);
445
+ }
446
+ function carriedColumns(db, table) {
447
+ return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name }) => name).filter((name) => name !== "seq");
448
+ }
449
+ function refuseColumnMismatch(local, other, tables) {
450
+ const faults = tables.flatMap(({ table }) => {
451
+ const here = new Set(carriedColumns(local, table));
452
+ const there = new Set(carriedColumns(other, table));
453
+ const onlyHere = [...here].filter((column) => !there.has(column)).map((column) => `${table}.${column} only here`);
454
+ const onlyThere = [...there].filter((column) => !here.has(column)).map((column) => `${table}.${column} only there`);
455
+ return [...onlyHere, ...onlyThere];
456
+ });
457
+ if (faults.length > 0) {
458
+ throw new UnionSchemaMismatch(`the two stores hold different columns (${faults.join(", ")}); bring both to the same migrations first`);
459
+ }
460
+ }
461
+ function valueOf(row, column) {
462
+ return row[column] ?? null;
463
+ }
464
+ function byOrigin(local, table, row) {
465
+ const store = valueOf(row, "origin_store");
466
+ const seq = valueOf(row, "origin_seq");
467
+ if (store === null || seq === null)
468
+ return null;
469
+ return local.query(`SELECT seq FROM ${quoted(table)} WHERE origin_store = ? AND origin_seq = ?`).get(store, seq)?.seq ?? null;
470
+ }
471
+ function byIdentity(local, { table, identity }, row) {
472
+ const where = identity.map((column) => `${quoted(column)} IS ?`).join(" AND ");
473
+ return local.query(`SELECT seq FROM ${quoted(table)} WHERE ${where} ORDER BY seq LIMIT 1`).get(...identity.map((column) => valueOf(row, column)))?.seq ?? null;
474
+ }
475
+ function insertRow(local, table, columns, row, incomingSeq) {
476
+ const columnList = columns.map(quoted).join(", ");
477
+ const placeholders = columns.map(() => "?").join(", ");
478
+ const nextSeq = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${quoted(table)})`;
479
+ try {
480
+ const inserted = local.query(`INSERT INTO ${quoted(table)} (seq, ${columnList}) VALUES (${nextSeq}, ${placeholders}) RETURNING seq`).get(...columns.map((column) => valueOf(row, column)));
481
+ if (inserted === null)
482
+ throw new Error(`the insert into ${table} returned no seq`);
483
+ return { kind: "added", localSeq: inserted.seq };
484
+ } catch (error) {
485
+ if (!(error instanceof SQLiteError) || error.code !== UNIQUE_REFUSAL)
486
+ throw error;
487
+ return { kind: "collided", collision: { incomingSeq, reason: error.message } };
488
+ }
489
+ }
490
+ function placeRow(local, merged, columns, row) {
491
+ const incomingSeq = Number(valueOf(row, "seq"));
492
+ const origin = byOrigin(local, merged.table, row);
493
+ if (origin !== null)
494
+ return { kind: "matched", match: { localSeq: origin, incomingSeq, by: "origin" } };
495
+ const identity = byIdentity(local, merged, row);
496
+ if (identity !== null)
497
+ return { kind: "matched", match: { localSeq: identity, incomingSeq, by: "identity" } };
498
+ return insertRow(local, merged.table, columns, row, incomingSeq);
499
+ }
500
+ function unionTable(local, other, merged) {
501
+ const columns = carriedColumns(local, merged.table);
502
+ const incoming = other.query(`SELECT seq, ${columns.map(quoted).join(", ")} FROM ${quoted(merged.table)} ORDER BY seq`).all();
503
+ const outcomes = incoming.map((row) => placeRow(local, merged, columns, row));
504
+ return {
505
+ table: merged.table,
506
+ added: outcomes.flatMap((outcome) => outcome.kind === "added" ? [outcome.localSeq] : []),
507
+ matched: outcomes.flatMap((outcome) => outcome.kind === "matched" ? [outcome.match] : []),
508
+ collided: outcomes.flatMap((outcome) => outcome.kind === "collided" ? [outcome.collision] : [])
509
+ };
510
+ }
511
+ function unionStores(local, other) {
512
+ const tables = mergedTables();
513
+ refuseColumnMismatch(local, other, tables);
514
+ return local.transaction(() => tables.map((merged) => unionTable(local, other, merged)))();
515
+ }
516
+
517
+ // packages/rafa/src/effort/store/merge-conflicts.ts
518
+ var SET_ONCE_FILLS = {
519
+ "findings.tracker_ref": "UPDATE findings SET tracker_ref = ? WHERE seq = ? AND tracker_ref IS NULL"
520
+ };
521
+ var RECOMPUTED_COMPARISONS = {
522
+ "commits.row_json": withoutGap
523
+ };
524
+ var NOT_CONTENT = new Set(["seq", "origin_store", "origin_seq"]);
525
+ var INSERT_CONFLICT = `INSERT INTO merge_conflicts (merge_id, table_name, local_seq, field, incoming, recorded_at)
526
+ VALUES (?, ?, ?, ?, ?, ?)`;
527
+ function rowAt(db, table, columns, seq, side) {
528
+ const row = db.query(`SELECT seq, ${columns.map(quoted).join(", ")} FROM ${quoted(table)} WHERE seq = ?`).get(seq);
529
+ if (row === null)
530
+ throw new Error(`the union matched ${table} row ${seq} ${side}, which that store does not hold`);
531
+ return row;
532
+ }
533
+ function sameValue(left, right) {
534
+ if (left instanceof Uint8Array && right instanceof Uint8Array)
535
+ return Buffer.from(left).equals(right);
536
+ return (left ?? null) === (right ?? null);
537
+ }
538
+ function fillStatement(table, field) {
539
+ const statement = SET_ONCE_FILLS[`${table}.${field}`];
540
+ if (statement === undefined) {
541
+ throw new Error(`${table}.${field} is set-once in MERGE_RULES and has no statement in SET_ONCE_FILLS`);
542
+ }
543
+ return statement;
544
+ }
545
+ function settleSetOnce(local, table, field, here, there) {
546
+ const incoming = there[field] ?? null;
547
+ if (incoming === null)
548
+ return { skipped: true, filled: [], conflictFields: [] };
549
+ if ((here[field] ?? null) !== null)
550
+ return { skipped: false, filled: [], conflictFields: [field] };
551
+ local.query(fillStatement(table, field)).run(incoming, Number(here.seq));
552
+ return { skipped: true, filled: [field], conflictFields: [] };
553
+ }
554
+ function settleRecomputed(table, field, here, there) {
555
+ const comparable = RECOMPUTED_COMPARISONS[`${table}.${field}`];
556
+ if (comparable === undefined) {
557
+ throw new Error(`${table}.${field} is recomputed in MERGE_RULES and has no entry in RECOMPUTED_COMPARISONS`);
558
+ }
559
+ const hereValue = here[field];
560
+ const thereValue = there[field];
561
+ const same = typeof hereValue === "string" && typeof thereValue === "string" && comparable(hereValue) === comparable(thereValue);
562
+ return same ? { skipped: true, filled: [], conflictFields: [] } : { skipped: false, filled: [], conflictFields: [null] };
563
+ }
564
+ function settlePair(local, table, edited, here, there) {
565
+ const differing = Object.keys(here).filter((column) => !NOT_CONTENT.has(column) && !sameValue(here[column], there[column]));
566
+ if (differing.length === 0)
567
+ return { skipped: true, filled: [], conflictFields: [] };
568
+ if (differing.some((column) => !Object.hasOwn(edited, column)))
569
+ return { skipped: false, filled: [], conflictFields: [null] };
570
+ const outcomes = differing.map((field) => {
571
+ if (edited[field] === SET_ONCE)
572
+ return settleSetOnce(local, table, field, here, there);
573
+ if (edited[field] === RECOMPUTED)
574
+ return settleRecomputed(table, field, here, there);
575
+ throw new Error(`${table}.${field} has the rule ${edited[field]}, which the merge does not apply`);
576
+ });
577
+ return {
578
+ skipped: outcomes.every((outcome) => outcome.skipped),
579
+ filled: outcomes.flatMap((outcome) => outcome.filled),
580
+ conflictFields: outcomes.flatMap((outcome) => outcome.conflictFields)
581
+ };
582
+ }
583
+ function settleTable(local, other, union, trail) {
584
+ const rule = MERGE_RULES[union.table];
585
+ if (rule?.scope !== "merged")
586
+ throw new Error(`${union.table} is not a merged table in MERGE_RULES`);
587
+ const columns = carriedColumns(local, union.table);
588
+ const settled = union.matched.map((match) => {
589
+ const here = rowAt(local, union.table, columns, match.localSeq, "here");
590
+ const there = rowAt(other, union.table, columns, match.incomingSeq, "in the other store");
591
+ const outcome = settlePair(local, union.table, rule.edited, here, there);
592
+ const conflicts = outcome.conflictFields.map((field) => {
593
+ local.query(INSERT_CONFLICT).run(trail.mergeId, union.table, match.localSeq, field, JSON.stringify(there), trail.recordedAt);
594
+ return { localSeq: match.localSeq, incomingSeq: match.incomingSeq, field };
595
+ });
596
+ return { match, outcome, conflicts };
597
+ });
598
+ return {
599
+ table: union.table,
600
+ skipped: settled.flatMap(({ match, outcome }) => outcome.skipped ? [match.localSeq] : []),
601
+ filled: settled.flatMap(({ match, outcome }) => outcome.filled.map((field) => ({ localSeq: match.localSeq, field }))),
602
+ conflicts: settled.flatMap(({ conflicts }) => conflicts)
603
+ };
604
+ }
605
+ function settleMatches(local, other, unions, trail) {
606
+ return local.transaction(() => unions.map((union) => settleTable(local, other, union, trail)))();
607
+ }
608
+ // packages/rafa/src/effort/store/migrate.ts
609
+ function liveLoop(path, options, nothingDone = "Nothing was migrated.", ownSessionId = null) {
610
+ const root = storeProjectRoot(path);
611
+ if (root === null)
612
+ return;
613
+ let records;
614
+ try {
615
+ records = readSessions(root, { isAlive: options.isAlive ?? isPidAlive });
616
+ } catch (error) {
617
+ throw new RebuildRefusal(`the loop's run records cannot be read, so a live loop on ${path} cannot be ruled out` + ` (${messageOf(error)}). ${nothingDone}`);
618
+ }
619
+ return records.find((record) => (record.state === "running" || record.state === "paused") && record.sessionId !== ownSessionId);
620
+ }
621
+
622
+ // packages/rafa/src/effort/store/merge-store.ts
623
+ var MOVE_TO_SQLITE = "rafa effort move --to=sqlite";
624
+
625
+ class MergeRefusal extends RebuildRefusal {
626
+ reason;
627
+ constructor(reason, message) {
628
+ super(message);
629
+ this.name = "MergeRefusal";
630
+ this.reason = reason;
631
+ }
632
+ }
633
+ var LIVE = "live";
634
+ var MERGES_TABLE = "merges";
635
+ var INSERT_MERGE = `INSERT INTO merges (id, other_store, merged_at, rows_added, rows_skipped, rows_in_conflict)
636
+ VALUES (?, ?, ?, ?, ?, ?)`;
637
+ var NOTHING_MERGED = "Nothing was merged.";
638
+ function refuseNdjson(backend) {
639
+ if (backend !== "ndjson")
640
+ return;
641
+ throw new MergeRefusal("ndjson", `REFUSED \u2014 this project keeps its sessions and commits as NDJSON (store: ndjson), and a merge needs the SQLite store. ${NOTHING_MERGED}` + ` Next safe step: ${MOVE_TO_SQLITE}`);
642
+ }
643
+ function refuseMissing(path, which) {
644
+ if (existsSync3(path))
645
+ return;
646
+ throw new MergeRefusal("missing", `REFUSED \u2014 ${which} ${path} is not there. ${NOTHING_MERGED}`);
647
+ }
648
+ function refuseDamaged(path) {
649
+ let answers;
650
+ try {
651
+ const db = new Database3(path, { readonly: true });
652
+ try {
653
+ answers = db.query("PRAGMA integrity_check").all().map((row) => row.integrity_check);
654
+ } finally {
655
+ db.close();
656
+ }
657
+ } catch (error) {
658
+ if (!(error instanceof SQLiteError2))
659
+ throw error;
660
+ throw new MergeRefusal("damaged", `REFUSED \u2014 ${path} cannot be read as a SQLite store (${error.message}). ${NOTHING_MERGED}` + " Take a consistent copy on its device with rafa effort copy, and merge that.");
661
+ }
662
+ if (answers.length === 1 && answers[0] === "ok")
663
+ return;
664
+ throw new MergeRefusal("damaged", `REFUSED \u2014 ${path} failed integrity_check: ${answers.join("; ")}. ${NOTHING_MERGED}` + " Take a consistent copy on its device with rafa effort copy, and merge that.");
665
+ }
666
+ function writablePlan(path, migrations) {
667
+ const plan = planSchema(readOnlySchema(path), sqliteCatalogue(migrations), "write", "open");
668
+ if (plan.verdict === "refuse")
669
+ throw new MergeRefusal("schema", `REFUSED \u2014 ${path}: ${plan.message}`);
670
+ return plan;
671
+ }
672
+ function recordedMeta(path) {
673
+ const db = new Database3(path, { readonly: true });
674
+ try {
675
+ const hasTable = db.query("SELECT count(*) AS n FROM sqlite_master WHERE type = 'table' AND name = 'store_meta'").get()?.n === 1;
676
+ return hasTable ? readStoreMeta(db) : null;
677
+ } finally {
678
+ db.close();
679
+ }
680
+ }
681
+ function refuseOtherProject(options) {
682
+ const other = recordedMeta(options.otherPath);
683
+ const here = recordedMeta(options.path)?.projectRootCommit ?? (options.readProject ?? readProjectIdentity)(dirname(options.path)).rootCommit;
684
+ const there = other?.projectRootCommit ?? null;
685
+ if (here !== null && there !== null && here !== there) {
686
+ throw new MergeRefusal("other-project", `REFUSED \u2014 ${options.otherPath} is the effort store of another project (root commit ${there});` + ` this store's project has root commit ${here}. ${NOTHING_MERGED}`);
687
+ }
688
+ return other?.storeId ?? null;
689
+ }
690
+ function refuseLiveLoop(path, options, sessionId) {
691
+ const loop = liveLoop(path, options, NOTHING_MERGED, sessionId);
692
+ if (loop === undefined)
693
+ return;
694
+ throw new MergeRefusal("live-loop", `REFUSED \u2014 loop ${loop.sessionId} (pid ${String(loop.pid)}, plan ${loop.planStub ?? loop.plan}) is running on this store,` + ` and swapping the merged store in under it would lose what it writes. ${NOTHING_MERGED}` + " Finish or stop that loop, then run it again.");
695
+ }
696
+ function openOther(otherPath, plan, stamp, tempRoot) {
697
+ const needs = writeNames(plan);
698
+ if (needs.length === 0) {
699
+ const db = new Database3(otherPath, { readonly: true });
700
+ return { db, broughtForward: [], release: () => db.close() };
701
+ }
702
+ const dir = mkdtempSync(join3(tempRoot, "rafa-merge-other-"));
703
+ const release = (db) => {
704
+ db?.close();
705
+ rmSync3(dir, { recursive: true, force: true });
706
+ };
707
+ try {
708
+ const copyPath = join3(dir, "other.sqlite");
709
+ vacuumInto(otherPath, copyPath);
710
+ const db = new Database3(copyPath, { readwrite: true });
711
+ try {
712
+ bringForward(db, copyPath, "write", "open", { ...stamp, builtAside: true });
713
+ } catch (error) {
714
+ release(db);
715
+ throw error;
716
+ }
717
+ return { db, broughtForward: needs, release: () => release(db) };
718
+ } catch (error) {
719
+ release();
720
+ throw error;
721
+ }
722
+ }
723
+ function mergeInto(db, other, mergeId, otherStore, mergedAt) {
724
+ return db.transaction(() => {
725
+ const unions = unionStores(db, other);
726
+ const settlements = settleMatches(db, other, unions, { mergeId, recordedAt: mergedAt });
727
+ const gaps = recomputeCommitGaps(db, unions.find(({ table }) => table === "commits")?.added ?? []);
728
+ const tables = unions.map((union, index) => {
729
+ const settled = settlements[index];
730
+ return {
731
+ table: union.table,
732
+ added: union.added.length,
733
+ skipped: settled?.skipped.length ?? 0,
734
+ inConflict: settled?.conflicts.length ?? 0,
735
+ filled: settled?.filled.length ?? 0,
736
+ collided: union.collided.length
737
+ };
738
+ });
739
+ const sum = (field) => tables.reduce((total, entry) => total + entry[field], 0);
740
+ const totals = { rowsAdded: sum("added"), rowsSkipped: sum("skipped"), rowsInConflict: sum("inConflict") };
741
+ db.query(INSERT_MERGE).run(mergeId, otherStore, mergedAt, totals.rowsAdded, totals.rowsSkipped, totals.rowsInConflict);
742
+ return { tables, ...totals, gapsRewritten: gaps.length };
743
+ })();
744
+ }
745
+ function addedRows(merged) {
746
+ return {
747
+ ...Object.fromEntries(merged.tables.map(({ table, added }) => [table, added])),
748
+ [MERGES_TABLE]: 1,
749
+ merge_conflicts: merged.rowsInConflict
750
+ };
751
+ }
752
+ function buildMerged(path, parallelPath, other, merge) {
753
+ vacuumInto(path, parallelPath);
754
+ const db = new Database3(parallelPath, { readwrite: true });
755
+ try {
756
+ bringForward(db, parallelPath, "write", "open", { ...merge.stamp, builtAside: true });
757
+ const merged = mergeInto(db, other, merge.id, merge.otherStore, merge.stamp.now().toISOString());
758
+ db.run(`ATTACH DATABASE ? AS ${LIVE}`, [path]);
759
+ const live = new Set(tableNames(db, LIVE));
760
+ checkedCounts(db, tableNames(db, "main").filter((table) => live.has(table)), LIVE, addedRows(merged));
761
+ db.run(`DETACH DATABASE ${LIVE}`);
762
+ refuseCorrupt(db);
763
+ refuseUnusable(db, parallelPath, merge.stamp.migrations);
764
+ return merged;
765
+ } finally {
766
+ db.close();
767
+ }
768
+ }
769
+ function refuseUnmergeable(options, migrations) {
770
+ const { path, otherPath } = options;
771
+ refuseNdjson(options.backend);
772
+ refuseMissing(path, "this store");
773
+ refuseMissing(otherPath, "the other store");
774
+ refuseInFlight(path);
775
+ refuseInFlight(otherPath);
776
+ refuseDamaged(otherPath);
777
+ const otherPlan = writablePlan(otherPath, migrations);
778
+ const localPlan = writablePlan(path, migrations);
779
+ const otherStore = refuseOtherProject(options);
780
+ if (!options.dryRun) {
781
+ refuseUnownedDevelopmentWrite(path, [...writeNames(localPlan), MERGES_TABLE], options);
782
+ refuseLiveLoop(path, options, options.sessionId ?? null);
783
+ }
784
+ return { otherPlan, otherStore };
785
+ }
786
+ function mergeStore(options) {
787
+ const { path, otherPath, dryRun, stamp } = options;
788
+ guardTestProcess(path);
789
+ guardTestProcess(otherPath);
790
+ const migrations = options.migrations ?? SQLITE_MIGRATIONS;
791
+ const { otherPlan, otherStore } = refuseUnmergeable(options, migrations);
792
+ const mergeStamp = {
793
+ migrations,
794
+ appliedBy: appliedByName(options.identity ?? readRuntimeIdentity()),
795
+ now: options.now ?? (() => new Date)
796
+ };
797
+ const mergeId = (options.newMergeId ?? randomUUID)();
798
+ const other = openOther(otherPath, otherPlan, mergeStamp, options.tempDir ?? tmpdir());
799
+ try {
800
+ const { built, backupPath } = rebuildAside({
801
+ path,
802
+ parallelPath: `${path}.merge-${stamp}`,
803
+ backupPath: `${path}.before-merge-${stamp}.bak`,
804
+ dryRun,
805
+ build: (parallelPath) => buildMerged(path, parallelPath, other.db, { id: mergeId, otherStore, stamp: mergeStamp })
806
+ });
807
+ const status = dryRun ? "would-merge" : "merged";
808
+ return { path, otherPath, status, mergeId, otherStore, otherBroughtForward: other.broughtForward, ...built, backupPath };
809
+ } finally {
810
+ other.release();
811
+ }
812
+ }
813
+
814
+ // packages/rafa/src/effort/store/wire.ts
815
+ import { existsSync as existsSync4, mkdtempSync as mkdtempSync2, rmSync as rmSync4 } from "fs";
816
+ import { tmpdir as tmpdir2 } from "os";
817
+ import { join as join4 } from "path";
818
+ import { Database as Database4, SQLiteError as SQLiteError3 } from "bun:sqlite";
819
+ var WIRE_FORMAT = "rafa-effort-wire";
820
+ var WIRE_VERSION = 1;
821
+
822
+ class WireFormatError extends Error {
823
+ name = "WireFormatError";
824
+ }
825
+
826
+ class WireExportRefusal extends Error {
827
+ name = "WireExportRefusal";
828
+ }
829
+ var MERGED_TABLES = Object.entries(MERGE_RULES).filter(([, rule]) => rule.scope === "merged").map(([table]) => table);
830
+ var ORIGIN_COLUMNS = ["origin_store", "origin_seq"];
831
+ function isRecord(value) {
832
+ return typeof value === "object" && value !== null && !Array.isArray(value);
833
+ }
834
+ function isCount(value) {
835
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
836
+ }
837
+ function isMergedTable(name) {
838
+ return MERGED_TABLES.includes(name);
839
+ }
840
+ function checkCursor(value) {
841
+ if (!isRecord(value))
842
+ throw new WireFormatError("wire: cursor is not an object");
843
+ const entries = Object.entries(value).map(([table, seq]) => {
844
+ if (!isMergedTable(table))
845
+ throw new WireFormatError(`wire: cursor names ${JSON.stringify(table)}, which is no merged table`);
846
+ if (!isCount(seq))
847
+ throw new WireFormatError(`wire: cursor for ${table} is ${JSON.stringify(seq)}, expected a whole number of 0 or more`);
848
+ return [table, seq];
849
+ });
850
+ return Object.freeze(Object.fromEntries(entries));
851
+ }
852
+ function checkRow(table, value, index) {
853
+ const where = `wire: ${table} row ${String(index)}`;
854
+ if (!isRecord(value))
855
+ throw new WireFormatError(`${where} is not an object`);
856
+ for (const [column, cell] of Object.entries(value)) {
857
+ const carried = cell === null || typeof cell === "string" || typeof cell === "number" && Number.isFinite(cell);
858
+ if (!carried)
859
+ throw new WireFormatError(`${where} holds ${JSON.stringify(cell) ?? typeof cell} in ${column}, expected a string, a number or null`);
860
+ }
861
+ const seq = value.seq;
862
+ if (!isCount(seq) || seq === 0)
863
+ throw new WireFormatError(`${where} has seq ${JSON.stringify(seq) ?? "undefined"}, expected a whole number above 0`);
864
+ return Object.freeze({ ...value });
865
+ }
866
+ function checkMigrations(value) {
867
+ if (!Array.isArray(value))
868
+ throw new WireFormatError("wire: migrations is not a list");
869
+ const ids = value.map((id) => {
870
+ if (typeof id !== "string" || id === "")
871
+ throw new WireFormatError(`wire: migration id ${JSON.stringify(id)} is not a name`);
872
+ return id;
873
+ });
874
+ const twice = ids.filter((id, index) => ids.indexOf(id) !== index);
875
+ if (twice.length > 0)
876
+ throw new WireFormatError(`wire: migrations name ${twice.join(", ")} more than once`);
877
+ return Object.freeze(ids);
878
+ }
879
+ function checkTables(value) {
880
+ if (!isRecord(value))
881
+ throw new WireFormatError("wire: tables is not an object");
882
+ const entries = Object.entries(value).map(([table, rows]) => {
883
+ if (!isMergedTable(table))
884
+ throw new WireFormatError(`wire: tables names ${JSON.stringify(table)}, which is no merged table`);
885
+ if (!Array.isArray(rows))
886
+ throw new WireFormatError(`wire: tables.${table} is not a list`);
887
+ return [table, Object.freeze(rows.map((row, index) => checkRow(table, row, index)))];
888
+ });
889
+ return Object.freeze(Object.fromEntries(entries));
890
+ }
891
+ function checkPayload(value) {
892
+ if (!isRecord(value))
893
+ throw new WireFormatError("wire: the payload is not an object");
894
+ if (value.format !== WIRE_FORMAT)
895
+ throw new WireFormatError(`wire: format is ${JSON.stringify(value.format) ?? "undefined"}, expected ${JSON.stringify(WIRE_FORMAT)}`);
896
+ if (value.version !== WIRE_VERSION)
897
+ throw new WireFormatError(`wire: version is ${JSON.stringify(value.version) ?? "undefined"}; this rafa reads version ${String(WIRE_VERSION)}`);
898
+ return Object.freeze({
899
+ format: WIRE_FORMAT,
900
+ version: WIRE_VERSION,
901
+ migrations: checkMigrations(value.migrations),
902
+ tables: checkTables(value.tables),
903
+ cursor: checkCursor(value.cursor)
904
+ });
905
+ }
906
+ function encodeWirePayload(payload) {
907
+ return JSON.stringify(payload);
908
+ }
909
+ function decodeWirePayload(text) {
910
+ let parsed;
911
+ try {
912
+ parsed = JSON.parse(text);
913
+ } catch (error) {
914
+ const reason = error instanceof Error ? error.message : String(error);
915
+ throw new WireFormatError(`wire: the payload is not JSON (${reason})`);
916
+ }
917
+ return checkPayload(parsed);
918
+ }
919
+ function readLoggedIds(db) {
920
+ const hasLog = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(MIGRATION_LOG_TABLE) !== null;
921
+ if (!hasLog)
922
+ return null;
923
+ return db.query(`SELECT id FROM ${MIGRATION_LOG_TABLE} ORDER BY seq`).all().map(({ id }) => id);
924
+ }
925
+ function columnsOf(db, table) {
926
+ return db.query(`PRAGMA table_info(${quoted(table)})`).all().map(({ name }) => name);
927
+ }
928
+ function wireValue(value, where) {
929
+ if (typeof value === "bigint") {
930
+ const number = Number(value);
931
+ if (Number.isSafeInteger(number))
932
+ return number;
933
+ throw new WireExportRefusal(`wire: ${where} holds ${String(value)}, past the whole numbers JSON carries exactly`);
934
+ }
935
+ if (value instanceof Uint8Array)
936
+ throw new WireExportRefusal(`wire: ${where} holds a BLOB, which the wire does not carry`);
937
+ return value;
938
+ }
939
+ function readRows(db, table, columns, after, path) {
940
+ const named = columns.map(quoted).join(", ");
941
+ return db.query(`SELECT ${named} FROM ${quoted(table)} WHERE seq > ? ORDER BY seq`).all(after).map((row) => Object.freeze(Object.fromEntries(Object.entries(row).map(([column, value]) => [
942
+ column,
943
+ wireValue(value, `${path} ${table}.${column} at seq ${String(row.seq)}`)
944
+ ]))));
945
+ }
946
+ function readPayload(db, path, cursor) {
947
+ const migrations = readLoggedIds(db);
948
+ if (migrations === null) {
949
+ throw new WireExportRefusal(`wire: ${path} has no migration log; any rafa command that writes the store brings it forward`);
950
+ }
951
+ const tables = {};
952
+ const next = { ...cursor };
953
+ for (const table of MERGED_TABLES) {
954
+ const columns = columnsOf(db, table);
955
+ if (columns.length === 0)
956
+ continue;
957
+ const missing = ORIGIN_COLUMNS.filter((column) => !columns.includes(column));
958
+ if (missing.length > 0) {
959
+ throw new WireExportRefusal(`wire: ${path} holds no ${missing.map((column) => `${table}.${column}`).join(", ")}, so its rows carry no origin pair`);
960
+ }
961
+ const rows = readRows(db, table, columns, cursor[table] ?? 0, path);
962
+ tables[table] = Object.freeze(rows);
963
+ const last = rows[rows.length - 1];
964
+ next[table] = last === undefined ? cursor[table] ?? 0 : Number(last.seq);
965
+ }
966
+ return Object.freeze({
967
+ format: WIRE_FORMAT,
968
+ version: WIRE_VERSION,
969
+ migrations: Object.freeze([...migrations]),
970
+ tables: Object.freeze(tables),
971
+ cursor: Object.freeze(next)
972
+ });
973
+ }
974
+ function exportWirePayload(options) {
975
+ const { path } = options;
976
+ const cursor = checkCursor(options.cursor ?? {});
977
+ guardTestProcess(path);
978
+ if (!existsSync4(path))
979
+ throw new WireExportRefusal(`wire: ${path} is not there`);
980
+ const db = new Database4(path, { readonly: true, safeIntegers: true });
981
+ try {
982
+ db.run(`PRAGMA busy_timeout = ${String(activeStoreSettings().busyTimeoutMs)}`);
983
+ return db.transaction(() => readPayload(db, path, cursor))();
984
+ } finally {
985
+ db.close();
986
+ }
987
+ }
988
+ function namedMigrations(ids, catalogue) {
989
+ const known = new Set(catalogue.map(({ id }) => id));
990
+ const unknown = ids.filter((id) => !known.has(id));
991
+ if (unknown.length > 0) {
992
+ throw new WireFormatError(`wire: the payload's store holds migration ${unknown.join(", ")}, which this rafa does not know; update rafa to take its rows`);
993
+ }
994
+ return catalogue.filter(({ id }) => ids.includes(id));
995
+ }
996
+ function insertRows(db, table, rows) {
997
+ const columns = columnsOf(db, table);
998
+ if (columns.length === 0)
999
+ throw new WireFormatError(`wire: the payload carries ${table}, which its migrations do not make`);
1000
+ const statement = db.query(`INSERT INTO ${quoted(table)} (${columns.map(quoted).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`);
1001
+ const expected = [...columns].sort().join(",");
1002
+ rows.forEach((row, index) => {
1003
+ const where = `wire: ${table} row ${String(index)}`;
1004
+ const held = Object.keys(row);
1005
+ if ([...held].sort().join(",") !== expected) {
1006
+ throw new WireFormatError(`${where} holds columns ${held.join(", ")}; its migrations make ${columns.join(", ")}`);
1007
+ }
1008
+ try {
1009
+ statement.run(...columns.map((column) => row[column] ?? null));
1010
+ } catch (error) {
1011
+ if (!(error instanceof SQLiteError3))
1012
+ throw error;
1013
+ throw new WireFormatError(`${where} (seq ${String(row.seq)}) is refused by the table: ${error.message}`);
1014
+ }
1015
+ });
1016
+ }
1017
+ function buildStore(path, payload, options) {
1018
+ const migrations = namedMigrations(payload.migrations, options.migrations ?? SQLITE_MIGRATIONS);
1019
+ const db = new Database4(path, { create: true, readwrite: true });
1020
+ try {
1021
+ bringForward(db, path, "write", "open", {
1022
+ migrations,
1023
+ appliedBy: appliedByName(options.identity ?? readRuntimeIdentity()),
1024
+ now: options.now,
1025
+ builtAside: true
1026
+ });
1027
+ db.transaction(() => {
1028
+ for (const [table, rows] of Object.entries(payload.tables))
1029
+ insertRows(db, table, rows);
1030
+ })();
1031
+ } finally {
1032
+ db.close();
1033
+ }
1034
+ }
1035
+ function materialiseWirePayload(payload, options = {}) {
1036
+ const checked = checkPayload(payload);
1037
+ const directory = mkdtempSync2(join4(options.tempDir ?? tmpdir2(), "rafa-wire-"));
1038
+ const release = () => rmSync4(directory, { recursive: true, force: true });
1039
+ const path = join4(directory, SQLITE_STORE_FILE_NAME);
1040
+ try {
1041
+ guardTestProcess(path);
1042
+ buildStore(path, checked, options);
1043
+ } catch (error) {
1044
+ release();
1045
+ throw error;
1046
+ }
1047
+ return Object.freeze({ directory, path, release });
1048
+ }
1049
+
1050
+ // packages/rafa/src/effort/store/index.ts
1051
+ function describeValue(value) {
1052
+ if (typeof value === "string")
1053
+ return JSON.stringify(value);
1054
+ if (typeof value === "object" && value !== null)
1055
+ return "an object";
1056
+ return String(value);
1057
+ }
1058
+ function selectEffortStore(repoRoot, config) {
1059
+ const adapter = CORE_ADAPTER_REGISTRY.find("store", config.store);
1060
+ if (adapter === undefined) {
1061
+ const expected = CORE_ADAPTER_REGISTRY.kinds("store").join(", ");
1062
+ throw new TypeError(`effort store: store is ${describeValue(config.store)},` + ` expected one of: ${expected}`);
1063
+ }
1064
+ return adapter.create({ repoRoot });
1065
+ }
1066
+
1067
+ export { openNdjsonStore, copyEffortStore, minutesBetween, MOVE_TO_SQLITE, mergeStore, WIRE_FORMAT, WIRE_VERSION, WireFormatError, WireExportRefusal, encodeWirePayload, decodeWirePayload, exportWirePayload, materialiseWirePayload, selectEffortStore };